diff --git a/src/main/java/com/budwk/app/flow/listenter/ProcessTaskStartEventListener.java b/src/main/java/com/budwk/app/flow/listenter/ProcessTaskStartEventListener.java index 5519fde..13619de 100644 --- a/src/main/java/com/budwk/app/flow/listenter/ProcessTaskStartEventListener.java +++ b/src/main/java/com/budwk/app/flow/listenter/ProcessTaskStartEventListener.java @@ -1,80 +1,27 @@ package com.budwk.app.flow.listenter; -import cn.hutool.core.util.StrUtil; -import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.engine.event.ProcessEvent; import com.budwk.app.flow.engine.event.ProcessEventListener; -import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessTask; -import com.budwk.app.flow.entity.ProcessTaskActor; import com.budwk.app.flow.enums.ProcessEventTypeEnum; -import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService; -import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService; -import lombok.extern.slf4j.Slf4j; -import org.nutz.dao.Cnd; +import com.budwk.app.flow.service.FlowTaskMessageService; import org.nutz.dao.Dao; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.json.Json; -import org.nutz.lang.util.NutMap; -import java.util.List; - -/** - * 流程任务开始监听器 - */ +/** 普通节点、首次会签及转办任务统一从开始事件进入待办消息 service。 */ @IocBean -@Slf4j public class ProcessTaskStartEventListener implements ProcessEventListener { - @Inject private Dao dao; @Inject - private GlobalMessageSendService globalMessageSendService; - @Inject - private DifficultHelpCommonService difficultHelpCommonService; + private FlowTaskMessageService flowTaskMessageService; @Override public void onEvent(ProcessEvent event) { if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) { - Long sourceId = event.getSourceId(); - ProcessTask task = dao.fetch(ProcessTask.class, sourceId); - if (task == null) return; - sendMessage(task); - // 节点及接收人判断集中在业务 service;通知失败不向流程提交接口抛出异常。 - try { - difficultHelpCommonService.notifyOfficeApproval(task); - } catch (Exception e) { - log.error("困难帮扶企业微信待办通知异常,taskId={}", sourceId, e); - } + ProcessTask task = dao.fetch(ProcessTask.class, event.getSourceId()); + flowTaskMessageService.notifyTask(task, false); } } - - - private void sendMessage(ProcessTask task) { - // 说明是流程开始 用户首次完成任务 - if (task.getTaskParentId() == 0 && task.getTaskName().equals("startTask")) { - return; - } - - // 节点名称 - String taskDisplayName = task.getDisplayName(); - // 实例名称 - Long instanceId = task.getProcessInstanceId(); - ProcessInstance instance = dao.fetch(ProcessInstance.class, instanceId); - String instanceName = Json.fromJson(NutMap.class, instance.getVariable()).getString(FlowConst.PROCESS_INSTANCE_NAME); - - // 任务接收人 - List taskActors = dao.query(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId())); - - for (ProcessTaskActor taskActor : taskActors) { - // 模拟发送消息 - String message = StrUtil.format("您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName); - log.info(message); - globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null); - } - - - } - } diff --git a/src/main/java/com/budwk/app/flow/service/FlowTaskMessageService.java b/src/main/java/com/budwk/app/flow/service/FlowTaskMessageService.java new file mode 100644 index 0000000..1570e81 --- /dev/null +++ b/src/main/java/com/budwk/app/flow/service/FlowTaskMessageService.java @@ -0,0 +1,132 @@ +package com.budwk.app.flow.service; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.entity.ProcessDefine; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.entity.ProcessTaskActor; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService; +import lombok.extern.slf4j.Slf4j; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; + +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** WF 待办统一消息入口;站内消息、短信和企业微信沿用全局消息渠道。 */ +@IocBean +@Slf4j +public class FlowTaskMessageService { + private static final String HOME = "/platform/home"; + private static final Set TASK_PARAMS = Set.of("taskId", "bizId", "taskKey", "tab"); + @Inject + private Dao dao; + @Inject + private GlobalMessageSendService globalMessageSendService; + + /** + * task 为参与人已落库的 WF 任务;includeStartTask 仅驳回重建时传 true, + * 使退回申请人的 startTask 也收到通知,首次提交的申请节点不通知本人。 + * 无返回值:仅通知进行中的任务,异常记录 taskId,不中断业务审核。 + * config 的 wfTaskId 标识流程通知,wfLoginNames 为办理人的 loginname, + * wxWorkLink 为卡片地址;微信策略据此切换到官方企业微信接口。 + */ + @Aop(TransAop.READ_COMMITTED) + public void notifyTask(ProcessTask task, boolean includeStartTask) { + if (task == null || task.getId() == null || task.getProcessInstanceId() == null + || !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) return; + if (!includeStartTask && "startTask".equals(task.getTaskName()) + && (task.getTaskParentId() == null || task.getTaskParentId() == 0L)) return; + try { + ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId()); + if (instance == null) { + log.warn("WF 待办通知跳过:实例不存在,taskId={}", task.getId()); + return; + } + ProcessDefine define = instance.getProcessDefineId() == null ? null + : dao.fetch(ProcessDefine.class, instance.getProcessDefineId()); + List actors = dao.query(ProcessTaskActor.class, + Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId())); + List receiverIds = actors.stream().map(ProcessTaskActor::getActorId) + .filter(StrUtil::isNotBlank).distinct().toList(); + if (receiverIds.isEmpty()) { + log.warn("WF 待办通知跳过:任务没有实际办理人,taskId={}", task.getId()); + return; + } + // 只用实际参与人的账号,不按角色扩大范围,不以内部用户 ID 代替企业微信 userid。 + List loginNames = actors.stream().filter(actor -> StrUtil.isNotBlank(actor.getActorId())) + .map(ProcessTaskActor::getActorAccount).filter(StrUtil::isNotBlank) + .map(String::trim).distinct().toList(); + if (actors.stream().anyMatch(actor -> StrUtil.isBlank(actor.getActorAccount()))) { + log.warn("WF 待办存在未绑定 loginname 的办理人,taskId={}", task.getId()); + } + String instanceName = define == null ? "流程审批" : StrUtil.blankToDefault(define.getDisplayName(), "流程审批"); + if (StrUtil.isNotBlank(instance.getVariable())) { + try { + instanceName = StrUtil.blankToDefault(JSONUtil.parseObj(instance.getVariable()) + .getStr(FlowConst.PROCESS_INSTANCE_NAME), instanceName); + } catch (Exception e) { + log.warn("WF 实例名称解析失败,使用流程定义名称,taskId={}", task.getId()); + } + } + String nodeName = StrUtil.blankToDefault(task.getDisplayName(), "待办审核"); + String content = "您有一条待办任务,实例:" + instanceName + ",处理环节:" + nodeName; + JSONObject config = JSONUtil.createObj().set("wfTaskId", task.getId()) + .set("wfLoginNames", loginNames).set("wxWorkLink", buildLink(task, instance, define)); + // 系统发送身份兼容定时推进等没有当前登录会话的流程入口。 + globalMessageSendService.sendSystemMessage(nodeName, content, 2, receiverIds, config); + } catch (Exception e) { + log.error("WF 待办通知异常,taskId={}", task.getId(), e); + } + } + + /** 优先节点手机表单;缺失或非法时回首页。保留原查询参数和锚点,替换过期任务参数。 */ + private String buildLink(ProcessTask task, ProcessInstance instance, ProcessDefine define) { + String form = task.getH5FormKey(); + // KNBF 历史定义中的办公室地址过时,沿用已经验证的实际审核入口。 + if (define != null && "KNBF".equals(define.getName()) + && "36ebafda-44c2-4bd3-bd7f-10839a575a0d".equals(task.getTaskName())) { + form = "/platform/difficultHelp/branchUnionApproval/h5"; + } + if (StrUtil.isBlank(form) || "null".equalsIgnoreCase(form.trim()) + || StrUtil.isBlank(instance.getBusinessNo()) || StrUtil.isBlank(task.getTaskName())) return HOME; + try { + form = form.trim(); + URI uri = URI.create(form); + if (uri.getUserInfo() != null || (uri.isAbsolute() + ? !("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme())) + || uri.getHost() == null + : !form.startsWith("/") || uri.getRawAuthority() != null)) return HOME; + List params = new ArrayList<>(); + if (StrUtil.isNotBlank(uri.getRawQuery())) { + for (String param : uri.getRawQuery().split("&")) { + String key = URLDecoder.decode(param.split("=", 2)[0], StandardCharsets.UTF_8); + if (!TASK_PARAMS.contains(key)) params.add(param); + } + } + params.add("taskId=" + task.getId()); + params.add("bizId=" + URLEncoder.encode(instance.getBusinessNo(), StandardCharsets.UTF_8)); + params.add("taskKey=" + URLEncoder.encode(task.getTaskName(), StandardCharsets.UTF_8)); + params.add("tab=todo"); + String base = form.split("[?#]", 2)[0]; + return base + "?" + String.join("&", params) + + (uri.getRawFragment() == null ? "" : "#" + uri.getRawFragment()); + } catch (IllegalArgumentException e) { + log.warn("WF 手机表单地址无效,卡片回退首页,taskId={}", task.getId()); + return HOME; + } + } +} diff --git a/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java b/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java index 376d9c2..50f9a3d 100644 --- a/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java +++ b/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java @@ -26,6 +26,7 @@ import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTaskActor; import com.budwk.app.flow.enums.*; +import com.budwk.app.flow.service.FlowTaskMessageService; import com.budwk.app.flow.service.ProcessDefineService; import com.budwk.app.flow.service.ProcessInstanceService; import com.budwk.app.flow.service.ProcessTaskService; @@ -60,6 +61,9 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl impleme @Inject private SchoolOaTodoService schoolOaTodoService; + @Inject + private FlowTaskMessageService flowTaskMessageService; + @Override public boolean save(ProcessTask param) { param.setId(null); @@ -292,6 +296,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl impleme addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor)); // 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。 schoolOaTodoService.createTodo(clone); + // 并行加签不经过 CreateTaskHandler,只为本次新增任务通知一次。 + flowTaskMessageService.notifyTask(clone, false); }); } @@ -362,6 +368,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl impleme addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator())); // 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。 schoolOaTodoService.createTodo(task); + // 驳回重建不发布开始事件;退回申请节点时也通知申请人重新办理。 + flowTaskMessageService.notifyTask(task, true); return task; } @@ -441,6 +449,12 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl impleme ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class); // 更新会签变量到流程实例参数中 processInstanceService.updateCountersignVariable(taskModel, execution, taskActors); + // 串行会签后续办理人由 CountersignHandler 直接创建,不经过开始事件;首次会签仍由监听器通知。 + String counterPrefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + taskModel.getName() + "_"; + if (CountersignTypeEnum.SEQUENTIAL.equals(taskModel.getCountersignType()) + && execution.getArgs().get(counterPrefix + FlowConst.LOOP_COUNTER, -1) >= 0) { + processTaskList.forEach(task -> flowTaskMessageService.notifyTask(task, false)); + } return processTaskList; } diff --git a/src/main/java/com/budwk/app/flow/service/impl/SchoolOaTodoServiceImpl.java b/src/main/java/com/budwk/app/flow/service/impl/SchoolOaTodoServiceImpl.java index 398fd3a..a874823 100644 --- a/src/main/java/com/budwk/app/flow/service/impl/SchoolOaTodoServiceImpl.java +++ b/src/main/java/com/budwk/app/flow/service/impl/SchoolOaTodoServiceImpl.java @@ -84,10 +84,12 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService { task.getId(), task.getProcessInstanceId(), task.getTaskState()); } - /** 系统参数优先于文件配置,只有明确为 true 才启用;未配置不访问数据库或 OA。 */ + /** + * OA 待办接口联调尚未完成,暂时在代码中固定关闭,避免远端异常阻断内部 WF 流转。 + * 不受 AppSchoolOa 或 enabled 配置影响;恢复对接时需显式恢复配置开关逻辑。 + */ private boolean isEnabled() { - Object enabled = Globals.MyConfig.get("AppSchoolOa"); - return Boolean.parseBoolean(enabled == null ? conf.get(PREFIX + "enabled", "false") : enabled.toString()); + return false; } private void requireTask(ProcessTask task) { diff --git a/src/main/java/com/budwk/app/sys/controller/SysRoleController.java b/src/main/java/com/budwk/app/sys/controller/SysRoleController.java index cc2dfcf..9efc977 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysRoleController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysRoleController.java @@ -37,6 +37,7 @@ import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.GET; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; +import org.nutz.mvc.annotation.POST; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; @@ -499,6 +500,24 @@ public class SysRoleController { return Result.success(list); } + /** + * 为全部 member=1 的用户设置会员角色,无需传入用户 ID、角色 ID 或列表筛选参数。 + * + * @return JSON Result,data.addedCount 为新增关联数,data.enabledCount 为恢复启用关联数 + */ + @At + @POST + @Ok("json") + @SaCheckPermission("sys.manager.role.user") + @SLog(tag = "一键设置会员角色", msg = "为全部member=1的人员补齐会员角色") + @Aop(TransAop.READ_COMMITTED) + public Result doSettingMemberRole() { + NutMap counts = sysRoleService.settingMemberRole(); + sysRoleService.clearCache(); + sysUserService.clearCache(); + return Result.success("会员角色设置完成", counts); + } + @At @Ok("json") @SaCheckPermission("sys.manager.role") diff --git a/src/main/java/com/budwk/app/sys/services/SysRoleService.java b/src/main/java/com/budwk/app/sys/services/SysRoleService.java index 1c6d6eb..08ee63b 100644 --- a/src/main/java/com/budwk/app/sys/services/SysRoleService.java +++ b/src/main/java/com/budwk/app/sys/services/SysRoleService.java @@ -8,6 +8,7 @@ import com.budwk.app.sys.models.Sys_role; import com.budwk.app.sys.models.Sys_unit; import java.util.List; +import org.nutz.lang.util.NutMap; /** * Created by wizzer on 2016/12/22. @@ -128,6 +129,13 @@ public interface SysRoleService extends BaseService { */ List getRoleIdsByCode(List roleConstantList); + /** + * 为用户表中 member=1 的全部人员补齐启用的 MEMBER 角色,不受列表筛选影响。 + * + * @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数;已有启用关联不重复添加 + */ + NutMap settingMemberRole(); + /** * 清空缓存 */ diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysHomeNewsServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysHomeNewsServiceImpl.java index 3528ba5..3f614f2 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysHomeNewsServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysHomeNewsServiceImpl.java @@ -1,6 +1,5 @@ package com.budwk.app.sys.services.impl; -import com.budwk.app.base.exception.BaseException; import com.budwk.app.sys.services.SysHomeNewsService; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -35,7 +34,7 @@ public class SysHomeNewsServiceImpl implements SysHomeNewsService { /** * 无需参数,一次抓取首页中的三个栏目,并保留原有一天有效期的缓存写入。 * - * @return label/value 栏目列表,文章包含 text/time/href;正常空栏目返回空列表。 + * @return label/value 栏目列表,文章包含 text/time/href;无文章的栏目保留空列表,抓取失败返回空栏目列表。 */ @Override @Aop(TransAop.READ_COMMITTED) @@ -51,7 +50,8 @@ public class SysHomeNewsServiceImpl implements SysHomeNewsService { return news; } catch (IOException e) { log.warn("获取重庆工贸职业技术学院工会首页失败", e); - throw new BaseException("学校工会网站暂时无法访问,请稍后重试"); + // 学校网站连接失败或超时时返回空列表,让首页展示空状态,不向用户报错,也不覆盖已有缓存。 + return new ArrayList<>(); } } diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysRoleServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysRoleServiceImpl.java index 86e79a7..35bdb80 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysRoleServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysRoleServiceImpl.java @@ -20,6 +20,7 @@ import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; import org.nutz.plugins.wkcache.annotation.CacheDefaults; import org.nutz.plugins.wkcache.annotation.CacheRemoveAll; import org.nutz.plugins.wkcache.annotation.CacheResult; @@ -284,4 +285,44 @@ public class SysRoleServiceImpl extends BaseServiceImpl implements Sys public void clearCache() { } + + /** + * 批量补齐会员角色。仅处理 member=1 的用户,保留其他角色及账号禁用状态。 + * + * @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数 + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public NutMap settingMemberRole() { + // 锁定同一会员角色,使本接口的并发请求依次执行,避免重复插入用户角色关系。 + Sql roleSql = Sqls.create("SELECT * FROM sys_role WHERE code=@code FOR UPDATE"); + roleSql.setParam("code", RoleConstant.MEMBER.name()); + roleSql.setEntity(dao().getEntity(Sys_role.class)); + roleSql.setCallback(Sqls.callback.entities()); + dao().execute(roleSql); + List roles = roleSql.getList(Sys_role.class); + if (roles.isEmpty()) { + throw new BaseException("会员角色(MEMBER)不存在,请先配置会员角色"); + } + Sys_role role = roles.get(0); + if (role.isDisabled()) { + throw new BaseException("会员角色(MEMBER)已禁用,请先启用会员角色"); + } + + // 历史关联可能已停用或未填写 enable;恢复关联即可,无需重新插入。 + Sql enableSql = Sqls.create("UPDATE sys_user_role SET enable=1 WHERE roleId=@roleId " + + "AND (enable IS NULL OR enable<>1) " + + "AND EXISTS (SELECT 1 FROM sys_user u WHERE u.id=sys_user_role.userId AND u.member=1)"); + enableSql.setParam("roleId", role.getId()); + dao().execute(enableSql); + + // 按用户、角色判重,只为缺少会员关联的人员添加记录;不删除任何已有角色。 + Sql insertSql = Sqls.create("INSERT INTO sys_user_role (userId,roleId,enable) " + + "SELECT u.id,@roleId,1 FROM sys_user u WHERE u.member=1 " + + "AND NOT EXISTS (SELECT 1 FROM sys_user_role ur WHERE ur.userId=u.id AND ur.roleId=@roleId)"); + insertSql.setParam("roleId", role.getId()); + dao().execute(insertSql); + return NutMap.NEW().addv("addedCount", insertSql.getUpdateCount()) + .addv("enabledCount", enableSql.getUpdateCount()); + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/message/service/GlobalMessageSendService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/message/service/GlobalMessageSendService.java index 6441caf..7fc828e 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/message/service/GlobalMessageSendService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/message/service/GlobalMessageSendService.java @@ -193,7 +193,8 @@ public class GlobalMessageSendService { try { strategy.send(request.getTitle(), request.getContent(), request.getType(), request.getReceiverIds(), request.getConfig()); - log.info("异步发送消息成功,渠道:{}", strategy.getChannel().getValue()); + // 策略可能因开关关闭而跳过,实际发送结果由策略日志记录。 + log.info("异步消息策略执行完成,渠道:{}", strategy.getChannel().getValue()); } catch (Exception e) { log.error("异步发送消息失败,渠道:{},错误:{}", strategy.getChannel().getValue(), e.getMessage(), e); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java b/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java index a4b0e19..cf32fbc 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java @@ -1,16 +1,23 @@ package com.budwk.app.zhgh.dayofficework.message.strategy.impl; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HtmlUtil; import cn.hutool.json.JSONObject; import com.budwk.app.base.sms.SmsService; +import com.budwk.app.base.wxwork.WxWorkMessageService; import com.budwk.app.sys.models.Sys_user; import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel; import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy; import lombok.extern.slf4j.Slf4j; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import java.nio.charset.StandardCharsets; import java.util.List; /** @@ -24,6 +31,8 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy { private Dao dao; @Inject private SmsService smsService; + @Inject + private WxWorkMessageService wxWorkMessageService; @Override public GlobalMessageChannel getChannel() { @@ -36,7 +45,13 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy { } @Override + @Aop(TransAop.READ_COMMITTED) public void send(String title, String content, Integer type, List receiverIds, JSONObject config) { + // WF 的接收账号已在任务事务内取得,异步线程不再反查尚未提交的任务数据。 + if (config != null && config.getLong("wfTaskId") != null) { + sendFlowCard(title, content, config); + return; + } try { log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size()); @@ -52,7 +67,12 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy { } @Override + @Aop(TransAop.READ_COMMITTED) public void sendByLoginName(String title, String content, Integer type, List receiverLoginNames, JSONObject config) { + if (config != null && config.getLong("wfTaskId") != null) { + sendFlowCard(title, content, config); + return; + } try { log.info("开始发送微信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size()); @@ -67,6 +87,58 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy { } } + /** + * WF 专用分支:wfTaskId 为任务ID,wfLoginNames 为实际办理人的 loginname 列表, + * wxWorkLink 为非空手机入口。无返回值;关闭开关或无账号时跳过,失败抛给全局异步日志处理, + * 不回退旧微信通道,避免双发。非流程消息继续沿用原有通讯平台。 + */ + private void sendFlowCard(String title, String content, JSONObject config) { + Long taskId = config.getLong("wfTaskId"); + List loginNames = config.getJSONArray("wfLoginNames") == null ? List.of() + : config.getJSONArray("wfLoginNames").toList(String.class); + if (loginNames.isEmpty()) { + log.warn("WF 企业微信卡片跳过:办理人 loginname 为空,taskId={}", taskId); + return; + } + String link = StrUtil.blankToDefault(config.getStr("wxWorkLink"), "/platform/home"); + NutMap result; + try { + result = wxWorkMessageService.massSend(loginNames, cardText(title, 128, false), + cardText(content, 512, true), link); + } catch (RuntimeException e) { + // 异步线程已脱离任务调用栈,保留任务 ID 便于从错误日志定位具体待办。 + throw new IllegalStateException("WF 企业微信卡片接口异常,taskId=" + taskId, e); + } + if (result != null && result.getBoolean("skipped", false)) { + log.info("WF 企业微信卡片跳过:消息开关关闭,taskId={}", taskId); + } else if (result != null && result.getBoolean("success", false)) { + log.info("WF 企业微信卡片发送成功,taskId={},接收账号数={}", taskId, loginNames.size()); + } else { + log.error("WF 企业微信卡片发送失败或部分失败,taskId={},result={}", taskId, result); + throw new IllegalStateException("WF 企业微信卡片发送失败或部分失败,taskId=" + taskId); + } + } + + /** 卡片有 UTF-8 字节上限;按 Unicode 码点缩短摘要,并计入正文 HTML 转义后的长度。 */ + private String cardText(String value, int limit, boolean description) { + String text = HtmlUtil.unescape(HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(value, "流程待办"))) + .replace('\r', ' ').replace('\n', ' '); + if (text.isBlank()) text = "流程待办"; + StringBuilder result = new StringBuilder(); + int bytes = 0; + for (int offset = 0; offset < text.length();) { + int codePoint = text.codePointAt(offset); + String character = new String(Character.toChars(codePoint)); + int size = (description ? HtmlUtil.escape(character) : character).getBytes(StandardCharsets.UTF_8).length; + // 留出省略号的三个 UTF-8 字节,避免长流程名称使整条通知发送失败。 + if (bytes + size > limit - 3) return result.append("…").toString(); + result.append(character); + bytes += size; + offset += Character.charCount(codePoint); + } + return result.toString(); + } + /** * 发送微信文本消息。 * 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送; diff --git a/src/main/java/com/budwk/app/zhgh/democratic/grassrootscongress/controller/material/GrassrootsCongressMaterialMineController.java b/src/main/java/com/budwk/app/zhgh/democratic/grassrootscongress/controller/material/GrassrootsCongressMaterialMineController.java index cdf0456..dfb44de 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/grassrootscongress/controller/material/GrassrootsCongressMaterialMineController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/grassrootscongress/controller/material/GrassrootsCongressMaterialMineController.java @@ -86,7 +86,8 @@ public class GrassrootsCongressMaterialMineController { cnd.and("info.createdBy", "=", SecurityUtil.getUserId()); } if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { - cnd.and(Cnd.likeEX("info.meetingName", pageForm.getSearchKeyword())); + // searchKeyword 为资料名称关键词,按资料表的 materialsName 字段模糊匹配。 + cnd.and(Cnd.likeEX("info.materialsName", pageForm.getSearchKeyword())); } cnd.andEX("YEAR(info.createdTime)", "=", pageForm.getYear()); cnd.desc("info.createdTime"); diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/service/DifficultHelpCommonService.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/service/DifficultHelpCommonService.java index ea7a9de..5deeb26 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/service/DifficultHelpCommonService.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/service/DifficultHelpCommonService.java @@ -1,7 +1,6 @@ package com.budwk.app.zhgh.staffbenefit.difficulthelp.service; import com.budwk.app.base.service.BaseService; -import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam; import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm; @@ -22,14 +21,6 @@ public interface DifficultHelpCommonService extends BaseService implements DifficultHelpCommonService { - private static final Log log = Logs.get(); - private static final String OFFICE_APPROVAL_TASK = "36ebafda-44c2-4bd3-bd7f-10839a575a0d"; public DifficultHelpCommonServiceImpl(Dao dao) { super(dao); @@ -63,55 +53,6 @@ public class DifficultHelpCommonServiceImpl extends BaseServiceImpl actors = dao().query(ProcessTaskActor.class, - Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId())); - List loginNames = actors.stream().map(ProcessTaskActor::getActorAccount) - .filter(StrUtil::isNotBlank).map(String::trim).distinct().toList(); - // 不按角色扩大接收范围,未绑定工号时不以内部用户 ID 代替企业微信 userid。 - if (loginNames.isEmpty()) { - log.warnf("困难帮扶企业微信通知跳过:审核人 loginname 为空,taskId=%s", task.getId()); - return; - } - String content = "您有一条困难帮扶申请待审核,受助人:" - + StrUtil.blankToDefault(info.getUserName(), "未填写") + ",请进入校工会办公室审批页面办理。"; - // 流程定义中仍保留旧的 unionLeaderApproval 链接,本节点实际办理入口为 branchUnionApproval。 - NutMap result = wxWorkMessageService.massSend(loginNames, "困难帮扶待办提醒", content, - "/platform/difficultHelp/branchUnionApproval/h5"); - if (!result.getBoolean("skipped", false) && !result.getBoolean("success", false)) { - log.warnf("困难帮扶企业微信通知发送失败或部分失败,taskId=%s,result=%s", task.getId(), result); - } - } catch (Exception e) { - // 外部消息不改变审批结果;日志保留任务 ID,便于定位接口、账号或配置问题。 - log.error("困难帮扶企业微信通知异常,taskId=" + task.getId(), e); - } - } @Override public void validateApplyUser(DifficultHelpInfo difficultHelpInfo) { diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyController.java index 90899d9..277a4eb 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyController.java @@ -20,6 +20,7 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.base.Globals; import com.budwk.app.zhgh.staffmanage.member.models.MemberApplyRecord; import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService; +import com.budwk.app.zhgh.staffmanage.member.service.MemberApplyRecordService; import io.swagger.annotations.ApiOperation; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; @@ -54,6 +55,8 @@ public class MemberApplyController { private FlowCommonService flowCommonService; @Inject private MemberCommonService memberCommonService; + @Inject + private MemberApplyRecordService memberApplyRecordService; @At("") @Ok("beetl:/platform/zhgh/staffmanage/member/apply/submit/index.html") @@ -70,13 +73,14 @@ public class MemberApplyController { @At @ApiOperation("保存申请") + @Aop(TransAop.READ_COMMITTED) @SaCheckPermission("member.apply.submit") @SLog(tag = "会员入会申请", msg = "保存申请,申请人: ${args[0].username}") public Result save(@Param("data") MemberApplyRecord memberApplyRecord) { if (StrUtil.isBlank(memberApplyRecord.getId())) { memberCommonService.validateApplyOrChangeIsDoing(null, "apply"); } - dao.insertOrUpdate(memberApplyRecord); + memberApplyRecordService.saveApplyRecord(memberApplyRecord); return Result.success(); } @@ -105,7 +109,7 @@ public class MemberApplyController { .and(new Static("JSON_CONTAINS(unitIds, '\"%s\"')".formatted(SecurityUtil.getUnitId())))); args.set("type", Lang.isEmpty(group) ? "branch" : "group"); - dao.insertOrUpdate(memberApplyRecord); + memberApplyRecordService.saveApplyRecord(memberApplyRecord); args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode()); args.set(FlowConst.FORM_DATA, memberApplyRecord); @@ -143,7 +147,7 @@ public class MemberApplyController { .and(new Static("JSON_CONTAINS(unitIds, '\"%s\"')".formatted(SecurityUtil.getUnitId())))); dict.set("type", Lang.isEmpty(group) ? "branch" : "group"); - dao.insertOrUpdate(memberApplyRecord); + memberApplyRecordService.saveApplyRecord(memberApplyRecord); dict.set(FlowConst.PROCESS_TASK_ID_KEY, taskId); dict.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.RE_APPLY.getCode()); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberBranchUnionAuditController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberBranchUnionAuditController.java index 6cac513..9435887 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberBranchUnionAuditController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberBranchUnionAuditController.java @@ -64,7 +64,7 @@ public class MemberBranchUnionAuditController { info.sex, info.userName, info.loginName, - info.unitName, + COALESCE(changeUnit.name, info.unitName) AS unitName, info.unionName, info.userState, info.preparedBy, @@ -91,6 +91,7 @@ public class MemberBranchUnionAuditController { wf_process_task t LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId LEFT JOIN member_change_record info ON info.id = ins.businessNo + LEFT JOIN sys_unit changeUnit ON changeUnit.id = info.unitId LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10 @@ -111,7 +112,8 @@ public class MemberBranchUnionAuditController { if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { cnd.desc("info.applyDateTime"); } else { - cnd.orderBy("info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + // 单位列按实际显示名称排序,其他字段保持原有排序规则。 + cnd.orderBy("unitName".equals(pageForm.getPageOrderName()) ? "unitName" : "info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); } cnd.groupBy("t.id"); sql.setCondition(cnd); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeApplyController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeApplyController.java index 9135d09..4776374 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeApplyController.java @@ -72,6 +72,7 @@ public class MemberChangeApplyController { @At @ApiOperation("保存申请") + @Aop(TransAop.READ_COMMITTED) @SaCheckPermission("member.apply.submit") @SLog(tag = "会员变更申请", msg = "保存申请,申请人: ${args[0].username}") public Result save(@Param("data") MemberChangeRecord memberChangeRecord) { @@ -147,7 +148,7 @@ public class MemberChangeApplyController { @ApiModelProperty("根据id查询变更记录") @SaCheckPermission("member.change.apply") public Result findChangeById(String id){ - return Result.success().addData(dao.fetch(MemberChangeRecord.class, id)); + return Result.success().addData(memberCommonService.findChangeRecordWithUnitName(id)); } /** diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java index 4a87955..fe6a28e 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java @@ -76,7 +76,7 @@ public class MemberChangeMineController { info.username, info.loginname, info.sex, - info.unitName, + COALESCE(changeUnit.name, info.unitName) AS unitName, info.unionName, info.userState, info.preparedBy, @@ -103,6 +103,7 @@ public class MemberChangeMineController { member_change_record info LEFT JOIN `wf_process_instance` ins ON ins.businessNo = info.id LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10 + LEFT JOIN sys_unit changeUnit ON changeUnit.id = info.unitId LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id $condition """); @@ -124,7 +125,8 @@ public class MemberChangeMineController { if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { cnd.desc("info.applyDateTime"); } else { - cnd.orderBy("info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + // 单位列按实际显示名称排序,其他字段保持原有排序规则。 + cnd.orderBy("unitName".equals(pageForm.getPageOrderName()) ? "unitName" : "info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); } cnd.groupBy("t.id"); sql.setCondition(cnd); @@ -166,7 +168,7 @@ public class MemberChangeMineController { @ApiOperation("根据id查询变更记录") @SaCheckPermission(value = {"member.change.mine", "member.change.unionGroup.audit"}, mode = SaMode.OR) public Result findMemberChangeRecord(String id){ - MemberChangeRecord record = dao.fetch(MemberChangeRecord.class, id); + MemberChangeRecord record = memberCommonService.findChangeRecordWithUnitName(id); NutMap nutMap = Lang.obj2nutmap(record); // 变更记录只保存三级单位 ID,查看时补充当前单位名称供页面展示。 diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberSchoolUnionAuditController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberSchoolUnionAuditController.java index 029eb5c..bcf7dd3 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberSchoolUnionAuditController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberSchoolUnionAuditController.java @@ -92,7 +92,7 @@ public class MemberSchoolUnionAuditController { info.sex, info.userName, info.loginName, - info.unitName, + COALESCE(changeUnit.name, info.unitName) AS unitName, info.unionName, info.userState, info.preparedBy, @@ -119,6 +119,7 @@ public class MemberSchoolUnionAuditController { wf_process_task t LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId LEFT JOIN member_change_record info ON info.id = ins.businessNo + LEFT JOIN sys_unit changeUnit ON changeUnit.id = info.unitId LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10 @@ -139,7 +140,8 @@ public class MemberSchoolUnionAuditController { if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { cnd.desc("info.applyDateTime"); } else { - cnd.orderBy("info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + // 单位列按实际显示名称排序,其他字段保持原有排序规则。 + cnd.orderBy("unitName".equals(pageForm.getPageOrderName()) ? "unitName" : "info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); } cnd.groupBy("t.id"); sql.setCondition(cnd); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberUnionGroupAuditController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberUnionGroupAuditController.java index b9066a7..d03bea7 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberUnionGroupAuditController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberUnionGroupAuditController.java @@ -60,7 +60,7 @@ public class MemberUnionGroupAuditController { info.sex, info.userName, info.loginName, - info.unitName, + COALESCE(changeUnit.name, info.unitName) AS unitName, info.unionName, info.userState, info.preparedBy, @@ -87,6 +87,7 @@ public class MemberUnionGroupAuditController { wf_process_task t LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId LEFT JOIN member_change_record info ON info.id = ins.businessNo + LEFT JOIN sys_unit changeUnit ON changeUnit.id = info.unitId LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10 @@ -107,7 +108,8 @@ public class MemberUnionGroupAuditController { if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { cnd.desc("info.applyDateTime"); } else { - cnd.orderBy("info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + // 单位列按实际显示名称排序,其他字段保持原有排序规则。 + cnd.orderBy("unitName".equals(pageForm.getPageOrderName()) ? "unitName" : "info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); } cnd.groupBy("t.id"); sql.setCondition(cnd); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/interceptor/MemberApplySaveInterceptor.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/interceptor/MemberApplySaveInterceptor.java index 84c8ead..08cde75 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/interceptor/MemberApplySaveInterceptor.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/interceptor/MemberApplySaveInterceptor.java @@ -7,6 +7,7 @@ import com.budwk.app.flow.engine.core.ServiceContext; import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.web.commons.base.Globals; import com.budwk.app.zhgh.staffmanage.member.models.MemberApplyRecord; +import com.budwk.app.zhgh.staffmanage.member.service.MemberApplyRecordService; import org.nutz.dao.Chain; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; @@ -35,7 +36,8 @@ public class MemberApplySaveInterceptor implements FlowInterceptor { MemberApplyRecord record = Json.fromJson(MemberApplyRecord.class, formDataStr); Dao dao = ServiceContext.find(Dao.class); - dao.insertOrUpdate(record); + // WF 保存同样经过日期校验,防止绕过普通申请接口写入无效日期。 + ServiceContext.find(MemberApplyRecordService.class).saveApplyRecord(record); execution.getArgs().set(FlowConst.FORM_DATA, Json.toJson(record)); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberApplyRecordService.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberApplyRecordService.java index c666eff..ac4bb9c 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberApplyRecordService.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberApplyRecordService.java @@ -8,4 +8,10 @@ import com.budwk.app.zhgh.staffmanage.member.models.MemberApplyRecord; */ public interface MemberApplyRecordService extends BaseService { + /** + * 校验入职时间并保存入会申请,统一供保存、提交、重新提交及 WF 保存入口调用。 + * @param record 申请数据,arrivalAtSchoolDate 可为空,非空时必须为有效的 yyyy-MM-dd 日期 + * @return 无返回值;新增时记录 ID 回填到 record,非空日期非法时抛出业务异常 + */ + void saveApplyRecord(MemberApplyRecord record); } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberCommonService.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberCommonService.java index 4b2879d..c1cd55c 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberCommonService.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/MemberCommonService.java @@ -44,6 +44,13 @@ public interface MemberCommonService extends BaseService { Pagination memberHistoryRecords(MemberManagePageForm pageForm); + /** + * 读取变更申请并按申请单位 ID 校准显示名称,不写回历史记录。 + * @param id 会员变更申请记录 ID + * @return 申请记录,unitName 对应本次申请的 unitId;单位不存在时保留原名称 + */ + MemberChangeRecord findChangeRecordWithUnitName(String id); + /** * 获取可变更字段 * @return diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberApplyRecordServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberApplyRecordServiceImpl.java index cb59a3a..8ea3231 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberApplyRecordServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberApplyRecordServiceImpl.java @@ -4,6 +4,13 @@ import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.zhgh.staffmanage.member.models.MemberApplyRecord; import com.budwk.app.zhgh.staffmanage.member.service.MemberApplyRecordService; import org.nutz.dao.Dao; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import com.budwk.app.base.exception.BaseException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; import org.nutz.ioc.loader.annotation.IocBean; @IocBean(args = {"refer:dao"}) @@ -12,4 +19,37 @@ public class MemberApplyRecordServiceImpl extends BaseServiceImpl implements // 获取系统用户 Cnd cnd = Cnd.NEW(); List dataUserList = dao().query(Sys_user.class, cnd); - Map userMap = dataUserList.stream().collect(Collectors.toMap(Sys_user::getLoginname, v -> v)); + // 无工号人员保留原有资料和会员身份,仅不参与本次按工号匹配,避免多个空工号导致 Map 键冲突。 + Map userMap = dataUserList.stream() + .filter(user -> StrUtil.isNotBlank(user.getLoginname())) + .collect(Collectors.toMap(Sys_user::getLoginname, v -> v)); List unitList = dao().query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1").groupBy("name")); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java index 6bc1cbb..47c079c 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java @@ -159,20 +159,21 @@ public class MemberManageServiceImpl extends BaseServiceImpl implement his.changeTypes, his.changeOrigin, his.changeReason, - his.remark, gh.`name` AS unionName, - unit.`name` AS unitName, + COALESCE(unit.name, changeRecord.unitName) AS unitName, u.id AS userId FROM `sys_user_history` his LEFT JOIN sys_user u ON u.loginname = his.loginname - LEFT JOIN sys_unit unit ON unit.id = u.unitid + LEFT JOIN member_change_record changeRecord ON changeRecord.id = his.recordId + LEFT JOIN sys_unit unit ON unit.id = COALESCE(NULLIF(changeRecord.unitId, ''), his.unitId) LEFT JOIN sys_union gh ON gh.id = unit.unionId $condition """); Cnd cnd = Cnd.NEW(); cnd.andEX("gh.id", "=", pageForm.getUnionId()); - cnd.andEX("his.unitId", "=", pageForm.getUnitId()); + // 优先使用本次变更目标单位;无关联申请时使用历史记录自身单位,筛选与显示一致。 + cnd.andEX("COALESCE(NULLIF(changeRecord.unitId, ''), his.unitId)", "=", pageForm.getUnitId()); cnd.andEX("his.personType", "=", pageForm.getPersonType()); cnd.andEX("his.userState", "=", pageForm.getUserState()); // 变更记录查询自行拼接历史表条件,需要单独补充人员属性多选及旧单值兼容。 @@ -251,6 +252,8 @@ public class MemberManageServiceImpl extends BaseServiceImpl implement @Override public List getChangeInfos(MemberChangeRecord record) { + // 普通提交、WF 保存拦截器和审核落库均先比较变更,在此统一校准申请单位名称。 + normalizeChangeUnitName(record); View_user info = dao().fetch(View_user.class, Cnd.where("id", "=", record.getUserId())); NutMap map = getDictAllowChangeFields(); @@ -537,6 +540,36 @@ public class MemberManageServiceImpl extends BaseServiceImpl implement } + /** + * 按申请 ID 读取详情并校准单位名称;仅修正返回对象,不改写历史记录。 + * @param id 会员变更申请 ID + * @return 含一致 unitId、unitName 的申请记录;记录不存在时提示错误 + */ + @Override + public MemberChangeRecord findChangeRecordWithUnitName(String id) { + MemberChangeRecord record = dao().fetch(MemberChangeRecord.class, id); + if (record == null) { + throw new BaseException("未找到会员变更申请记录"); + } + normalizeChangeUnitName(record); + return record; + } + + /** + * 名称以申请目标单位 ID 为准,避免前端残留原单位名称;不改变单位 ID 或所属工会。 + * 单位已删除或未填写 ID 时保留原名称,避免用人员当前单位覆盖历史申请含义。 + * @param record 待保存或待展示的申请对象,仅更新内存中的 unitName,不执行数据库更新 + */ + private void normalizeChangeUnitName(MemberChangeRecord record) { + if (record == null || StrUtil.isBlank(record.getUnitId())) { + return; + } + Sys_unit unit = dao().fetch(Sys_unit.class, record.getUnitId()); + if (unit != null && unit.getName() != null) { + record.setUnitName(unit.getName()); + } + } + /** * 统一规范变更记录中的到校日期,避免 Date 对象复制后以英文字符串形式参与比较或直接落库。 */ diff --git a/src/main/resources/static/assets/platform/css/home-portal.css b/src/main/resources/static/assets/platform/css/home-portal.css new file mode 100644 index 0000000..1f9174f --- /dev/null +++ b/src/main/resources/static/assets/platform/css/home-portal.css @@ -0,0 +1,207 @@ +/* 首页服务门户布局单独限定作用域,避免影响应用中心和其他业务页。 */ +#v4-home-app.home-portal { + width: 100%; + max-width: none; + margin: 0; + color: #20344d; + background: #f2f6fb; + font-family: "Microsoft YaHei", "PingFang SC", sans-serif; + font-size: 14px; +} +#v4-home-app.home-portal * { box-sizing: border-box; } +#v4-home-app .portal-top { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; max-width: 1720px; margin: auto; padding: 18px 24px 0; } +/* 使用完整不透明背景铺满横幅,会员信息独立成卡,不再覆盖校园景观。 */ +#v4-home-app .portal-hero { position: relative; min-width: 0; min-height: 260px; overflow: hidden; border-radius: 12px; background: #e8f4ff; } +#v4-home-app .portal-hero::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, #e8f4ff 0%, #e8f4ff 28%, rgba(232,244,255,.92) 39%, rgba(232,244,255,.35) 54%, rgba(232,244,255,0) 68%); } +#v4-home-app .portal-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; min-height: 260px; height: 100%; padding: 32px 40px; } +#v4-home-app .portal-hero-copy { max-width: 80%; } +#v4-home-app .portal-hero-eyebrow { margin-bottom: 12px; color: #527ca1; font-size: 14px; line-height: 22px; letter-spacing: 2px; } +#v4-home-app .portal-hero-copy h1 { margin: 0 0 10px; font-size: 36px; line-height: 1.3; font-weight: 700; color: #083765; } +#v4-home-app .portal-hero-copy p { margin: 0; color: #335675; font-size: 17px; line-height: 1.8; } +#v4-home-app .portal-values { display: flex; flex-wrap: wrap; gap: 22px 36px; margin-top: 24px; } +#v4-home-app .portal-values span { display: inline-flex; align-items: center; gap: 12px; } +#v4-home-app .portal-values i { color: #3599f2; font-size: 23px; } +/* 首次渲染时分层淡入文字,只改变透明度和位移,不影响卡片布局。 */ +@keyframes portal-text-enter { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} +#v4-home-app .portal-hero-eyebrow, +#v4-home-app .portal-hero-copy h1, +#v4-home-app .portal-hero-copy p, +#v4-home-app .portal-values > span, +#v4-home-app .portal-member-date, +#v4-home-app .portal-member-heading, +#v4-home-app .portal-member .meta-row { animation: portal-text-enter .6s cubic-bezier(.22, 1, .36, 1) backwards; } +#v4-home-app .portal-hero-copy h1 { animation-delay: .08s; } +#v4-home-app .portal-hero-copy p { animation-delay: .16s; } +#v4-home-app .portal-values > span:nth-child(1) { animation-delay: .24s; } +#v4-home-app .portal-values > span:nth-child(2) { animation-delay: .30s; } +#v4-home-app .portal-values > span:nth-child(3) { animation-delay: .36s; } +#v4-home-app .portal-values > span:nth-child(4) { animation-delay: .42s; } +#v4-home-app .portal-member-date { animation-delay: .10s; } +#v4-home-app .portal-member-heading { animation-delay: .18s; } +#v4-home-app .portal-member .meta-row:nth-child(1) { animation-delay: .26s; } +#v4-home-app .portal-member .meta-row:nth-child(2) { animation-delay: .34s; } +#v4-home-app .portal-member .meta-row:nth-child(3) { animation-delay: .42s; } +@media (prefers-reduced-motion: reduce) { + #v4-home-app .portal-hero-eyebrow, + #v4-home-app .portal-hero-copy h1, + #v4-home-app .portal-hero-copy p, + #v4-home-app .portal-values > span, + #v4-home-app .portal-member-date, + #v4-home-app .portal-member-heading, + #v4-home-app .portal-member .meta-row { animation: none; } +} +#v4-home-app .portal-content { max-width: 1720px; margin: auto; padding: 18px 24px 28px; } +/* 同行共用网格行高,统一横纵间距;待办使用紧凑两行信息,避免撑出大块留白。 */ +#v4-home-app .portal-columns { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; margin-top: 18px; align-items: stretch; } +#v4-home-app .portal-columns > * { min-width: 0; } +#v4-home-app .portal-news-wide { grid-column: 1 / -1; } +#v4-home-app .portal-panel, +#v4-home-app .entry-wrapper, +#v4-home-app .todo-center, +#v4-home-app .work-template-wrapper, +#v4-home-app .jcdt-container { background: #fff; border: 0; border-radius: 12px; padding: 20px; box-shadow: 0 3px 14px rgba(30, 66, 108, .035); } +#v4-home-app .home-section-header, +#v4-home-app .portal-todo-heading, +#v4-home-app .work-template-header, +#v4-home-app .portal-news-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; min-height: 28px; margin: 0 0 16px; } +#v4-home-app .home-section-title, +#v4-home-app .work-template-title, +#v4-home-app .portal-news-title, +#v4-home-app .portal-todo-heading h3 { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 0; border: 0; font-size: 20px; line-height: 1.4; font-weight: 600; color: #123356; } +#v4-home-app .home-section-title i, +#v4-home-app .work-template-title i, +#v4-home-app .portal-news-title > i:first-child, +#v4-home-app .portal-todo-heading i { color: #1687f5; font-size: 19px; } +#v4-home-app .portal-more { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; border: 0; padding: 4px 0; background: transparent; color: #1687f5; font: inherit; font-size: 13px; cursor: pointer; text-decoration: none; } +#v4-home-app .portal-more:hover { color: #0565c6; } +#v4-home-app button:focus-visible, +#v4-home-app a:focus-visible, +#v4-home-app [tabindex]:focus-visible { outline: 2px solid #1687f5; outline-offset: 3px; } +#v4-home-app .entry-card { min-width: 0; } +#v4-home-app .entry-wrapper { padding-bottom: 0; } +#v4-home-app .portal-entry-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #e9eff6; } +#v4-home-app .portal-entry-tabs { display: flex; gap: 34px; } +#v4-home-app .portal-entry-tabs button { border: 0; border-bottom: 3px solid transparent; padding: 0 0 13px; background: transparent; font: inherit; font-size: 19px; color: #465d78; font-weight: 600; cursor: pointer; } +#v4-home-app .portal-entry-tabs button.active { color: #0784fa; border-bottom-color: #0784fa; } +#v4-home-app .entry-section + .entry-section { margin-top: 0; } +#v4-home-app .app-grid { display: flex; justify-content: flex-start; align-items: center; gap: 12px; min-height: 0; overflow-x: auto; padding: 20px 0; border: 0; border-radius: 0; box-shadow: none; } +#v4-home-app .app-item { flex: 0 0 96px; border: 0; font: inherit; padding: 0 4px; max-width: none; } +#v4-home-app .app-item:hover { background: #f4f9ff; box-shadow: none; transform: translateY(-2px); } +#v4-home-app .app-icon { background: transparent; } +#v4-home-app .app-name { display: block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; line-height: 20px; min-height: 0; margin-top: 2px; } +#v4-home-app .app-empty { padding: 10px 0; } +#v4-home-app .act-card { padding: 0; border: 0; border-radius: 0; box-shadow: none; } +#v4-home-app .activity-swiper-container { padding: 0 0 4px; } +#v4-home-app .activity-swiper .swiper-slide .item { display: flex; flex-direction: column; height: 100%; } +/* 保留横向卡片比例,海报等比完整居中显示,比例不一致时以浅色背景补齐空白。 */ +#v4-home-app .activity-swiper .swiper-slide .item .img-box { flex-shrink: 0; height: auto; aspect-ratio: 1.5; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #f0f5fb; isolation: isolate; } +#v4-home-app .img-box .el-image { display: block; width: 100%; height: 100%; position: absolute; inset: 0; border-radius: inherit; overflow: hidden; } +#v4-home-app .activity-swiper .swiper-slide .item .img-box img { height: 100% !important; max-height: none !important; object-fit: contain !important; object-position: center; border-radius: 10px; } +#v4-home-app .activity-swiper .swiper-slide .item .img-box .status { top: 8px; left: 8px; border-radius: 5px; padding: 4px 7px; background: #ed4859; } +#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-ended { background: #8b97a7; } +#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-upcoming { background: #1687f5; } +#v4-home-app .activity-swiper .swiper-slide .item h4 { flex: 1; margin: 10px 0 8px; height: auto; min-height: 22px; max-height: 44px; font-size: 15px; color: #20344d; line-height: 22px; } +#v4-home-app .activity-swiper .swiper-slide .item .time p { white-space: normal; font-size: 12px; color: #7b899d; line-height: 1.6; } +#v4-home-app .activity-swiper-container .swiper-button-next, +#v4-home-app .activity-swiper-container .swiper-button-prev { width: 44px; height: 44px; margin-top: 0; padding: 0; transform: translateY(-50%); border: 1px solid #dbeaff; background: rgba(255,255,255,.96); color: #1687f5; box-shadow: 0 3px 12px rgba(30, 66, 108, .18); } +#v4-home-app .activity-swiper-container .swiper-button-next { right: 8px; } +#v4-home-app .activity-swiper-container .swiper-button-prev { left: 8px; } +#v4-home-app .activity-swiper-container .swiper-button-next:hover, +#v4-home-app .activity-swiper-container .swiper-button-prev:hover { transform: translateY(-50%) scale(1.05); background: #1687f5; color: #fff; } +#v4-home-app .activity-swiper-container .swiper-button-next::after, +#v4-home-app .activity-swiper-container .swiper-button-prev::after { font-size: 18px; font-weight: 600; } +#v4-home-app .portal-cover-empty { height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 9px; color: #94a7bd; background: #f0f5fb; } +#v4-home-app .portal-cover-empty i { font-size: 28px; } +#v4-home-app .user-wrapper { background: transparent; } +#v4-home-app .user-container { display: block; width: 100%; height: 100%; margin: 0; } +#v4-home-app .todo-center { display: flex; flex-direction: column; min-width: 0; height: 100%; } +#v4-home-app .todo-center .header { display: block; margin: 0; } +#v4-home-app .todo-center .el-tabs { margin: 0 0 8px; } +#v4-home-app .todo-center .el-tabs__header { margin: 0; } +#v4-home-app .todo-center .el-tabs__nav { display: flex; width: 100%; border-color: #e9eff6; border-radius: 6px 6px 0 0; } +#v4-home-app .todo-center .el-tabs__item { flex: 1; min-width: 0; padding: 0 7px; font-size: 12px; height: 36px; line-height: 36px; text-align: center; } +#v4-home-app .todo-center .el-tabs__item.is-active { border-bottom: 2px solid #1687f5; color: #1687f5; } +#v4-home-app .portal-task-list { display: flex; flex-direction: column; justify-content: space-evenly; flex: 1; min-height: 180px; } +#v4-home-app .portal-task-row { display: flex; gap: 10px; align-items: center; padding: 8px 6px; border-bottom: 1px solid #eef3f8; } +#v4-home-app .portal-task-row:last-child { border-bottom: 0; } +#v4-home-app .portal-task-row:hover { background: #f7fbff; } +#v4-home-app .portal-task-icon { width: 34px; height: 40px; display: flex; flex: 0 0 34px; align-items: center; justify-content: center; border-radius: 8px; background: #eef7ff; color: #1687f5; font-size: 22px; } +#v4-home-app .portal-task-content { min-width: 0; flex: 1; } +#v4-home-app .portal-task-title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #263548; font-weight: 600; font-size: 14px; line-height: 22px; } +#v4-home-app .portal-task-meta { display: flex; align-items: baseline; gap: 10px; font-size: 12px; color: #8390a2; line-height: 21px; } +#v4-home-app .portal-task-meta span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +#v4-home-app .portal-task-meta > span:first-child { max-width: 45%; flex-shrink: 0; } +#v4-home-app .portal-task-date { flex-shrink: 0; font-size: 12px; color: #7b899d; } +#v4-home-app .portal-task-node { flex: 1; min-width: 0; color: #8b98a9; font-size: 11px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +#v4-home-app .portal-task-row .el-button { flex-shrink: 0; padding: 8px 14px; } +#v4-home-app .portal-task-pagination { text-align: right; margin-top: 8px; } +#v4-home-app .portal-empty { min-height: 132px; display: flex; align-items: center; justify-content: center; gap: 10px; color: #8a9aac; } +#v4-home-app .portal-empty i { font-size: 26px; color: #83b9ee; } +#v4-home-app .work-template-wrapper { width: 100%; margin: 0; } +#v4-home-app .work-template-card { min-height: 98px; padding: 0; border: 0; border-radius: 0; box-shadow: none; } +#v4-home-app .work-template-grid { display: flex; align-items: flex-start; gap: 14px; overflow-x: auto; padding: 4px 0 10px; scroll-snap-type: x proximity; } +#v4-home-app .work-template-item { flex: 0 0 88px; width: 88px; padding: 4px; border: 0; font: inherit; scroll-snap-align: start; } +#v4-home-app .work-template-item:hover { background: #f4f9ff; box-shadow: none; } +#v4-home-app .work-template-icon, +#v4-home-app .work-template-icon .el-image { width: 48px; height: 48px; } +#v4-home-app .work-template-icon img { width: 48px !important; height: 48px !important; object-fit: contain; } +#v4-home-app .work-template-icon i { font-size: 30px; line-height: 48px; color: #83b9ee; } +#v4-home-app .work-template-name { font-size: 13px !important; line-height: 20px; min-height: 40px; max-height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } +#v4-home-app .work-template-wrapper .no-activity-placeholder { padding: 12px; } +#v4-home-app .jcdt-container { width: 100%; max-width: none; height: 100%; margin: 0; } +#v4-home-app .portal-news-title { background: transparent; cursor: pointer; font-family: inherit; } +#v4-home-app .portal-news-title > i:last-child:not(:first-child) { color: #8092a6; font-size: 14px; } +#v4-home-app .portal-news-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 0; border: 0; background: transparent; color: #42536a; font: inherit; text-align: left; cursor: pointer; } +#v4-home-app .portal-news-row time { flex-shrink: 0; padding: 3px 7px; background: #eff5fc; border-radius: 4px; color: #45698f; font-size: 13px; } +#v4-home-app .portal-news-row span { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +#v4-home-app .portal-news-row:hover span { color: #1687f5; } +@media (min-width: 1600px) { + #v4-home-app .portal-hero-copy h1 { font-size: 42px; } + #v4-home-app .app-item { flex-basis: 104px; } +} +@media (max-width: 1199px) { + #v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr) 300px; padding: 18px 18px 0; } + #v4-home-app .portal-hero-inner { padding-left: 32px; } + #v4-home-app .portal-content { padding: 18px 18px 24px; } + #v4-home-app .portal-columns { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); } + #v4-home-app .portal-values { gap: 16px; } + #v4-home-app .portal-panel, #v4-home-app .todo-center { padding: 16px; } + #v4-home-app .portal-task-row { gap: 8px; padding: 8px 0; } + #v4-home-app .portal-task-icon { display: none; } +} +@media (max-width: 900px) { + #v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr); } + #v4-home-app .portal-columns { grid-template-columns: minmax(0, 1fr); } + #v4-home-app .portal-activities { order: 1; } + #v4-home-app .portal-todo { order: 2; } + #v4-home-app .portal-templates { order: 3; } + #v4-home-app .portal-news { order: 4; } + #v4-home-app .portal-hero-copy { max-width: 68%; } + #v4-home-app .portal-hero-copy h1 { font-size: 30px; } + #v4-home-app .portal-hero-copy p { font-size: 14px; } + #v4-home-app .portal-task-icon { display: flex; } + #v4-home-app .portal-task-meta { flex-wrap: nowrap; } +} +@media (max-width: 600px) { + #v4-home-app .portal-top { padding: 10px 10px 8px; } + #v4-home-app .portal-hero-inner { padding: 24px 20px; } + #v4-home-app .portal-hero-copy { max-width: 100%; } + #v4-home-app .portal-hero-copy h1 { font-size: 28px; } + #v4-home-app .portal-values { gap: 12px 18px; font-size: 12px; } + #v4-home-app .portal-values i { font-size: 17px; } + #v4-home-app .portal-content { padding: 10px; } + #v4-home-app .entry-wrapper { padding: 16px 12px 0; } + #v4-home-app .portal-entry-tabs { gap: 20px; } + #v4-home-app .portal-entry-tabs button { font-size: 16px; } + #v4-home-app .app-item { flex-basis: 84px; } + #v4-home-app .home-section-title, #v4-home-app .portal-todo-heading h3, #v4-home-app .work-template-title, #v4-home-app .portal-news-title { font-size: 18px; } + #v4-home-app .portal-task-row .el-button { padding: 8px 10px; } +} + +/* 使用参考项目本地横幅图片,文字遮罩保持首页的可读性。 */ +#v4-home-app .portal-hero-background { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: right bottom; } +/* 已展示全部活动时明确禁用入口,避免出现可点击但没有内容切换的按钮。 */ +#v4-home-app .portal-more:disabled { color: #8a9aac; cursor: default; } diff --git a/src/main/resources/static/assets/platform/img/home/service-portal-banner-campus-v4.png b/src/main/resources/static/assets/platform/img/home/service-portal-banner-campus-v4.png new file mode 100644 index 0000000..34b9058 Binary files /dev/null and b/src/main/resources/static/assets/platform/img/home/service-portal-banner-campus-v4.png differ diff --git a/src/main/resources/static/components/plugins/sysSignature/pc.vue b/src/main/resources/static/components/plugins/sysSignature/pc.vue index 730d926..a34c061 100644 --- a/src/main/resources/static/components/plugins/sysSignature/pc.vue +++ b/src/main/resources/static/components/plugins/sysSignature/pc.vue @@ -52,7 +52,8 @@ module.exports = { */ init() { this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id - console.log(this.signatureAddress) + // 打印当前二维码的签字入口,便于从控制台复制地址进行签字测试。 + console.info("signatureAddress:", this.signatureAddress) return this.$axios.post("/platform/signature/get") .then((res) => { if (this._isBeingDestroyed || this._isDestroyed) { diff --git a/src/main/resources/views/layouts/v4/home/act.js b/src/main/resources/views/layouts/v4/home/act.js index 2ed2ca0..032a4e6 100644 --- a/src/main/resources/views/layouts/v4/home/act.js +++ b/src/main/resources/views/layouts/v4/home/act.js @@ -10,22 +10,23 @@ const act = {
-
+
- - + +
暂无图片
+
+ {{getStatusText(activity)}}
-

+

{{ activity.name}}

-

+

- 时间:{{ activity.startDate }}至{{ activity.endDate }} + {{ formatActivityDate(activity.startDate) }} 至 {{ formatActivityDate(activity.endDate) }}

@@ -34,20 +35,21 @@ const act = {
-
-
+ +
-
📅
+

暂无近期活动

敬请关注后续通知

+
`, data() { @@ -57,19 +59,57 @@ const act = { } }, methods: { - /*查询活动*/ + // 轮播初始化或尺寸变化后,将左右箭头对齐当前封面的垂直中心。 + updateNavigationPosition() { + this.syncBrowseState() + const swiperElement = this.$refs.activitySwiper + const cover = swiperElement && swiperElement.querySelector('.img-box') + if (!cover) return + // 按实际封面高度定位,避免标题换行或响应式列数变化导致箭头偏移。 + const container = swiperElement.parentElement + const top = cover.getBoundingClientRect().top - container.getBoundingClientRect().top + cover.offsetHeight / 2 + ;[this.$refs.prevButton, this.$refs.nextButton].forEach(button => { + if (button) button.style.top = top + 'px' + }) + }, + // value 为接口返回的活动日期;无日期时显示待定,避免展示 Invalid date。 + formatActivityDate(value) { + return value ? this.$moment(value).format('YYYY-MM-DD') : '待定' + }, + // 按轮播实际停靠点判断是否还有内容,尺寸改变后同步首页按钮文案和可用状态。 + syncBrowseState() { + const enabled = !!(this.swiper && !this.swiper.destroyed && !this.swiper.isLocked && this.swiper.snapGrid.length > 1) + this.$emit('browse-state', { + enabled: enabled, + label: enabled ? '浏览更多' : (this.actList.length ? '已展示全部活动' : '暂无活动') + }) + }, + // 点击前刷新尺寸,避免异步布局导致轮播状态过期;到末组后返回首组。 + browseNext() { + if (!this.swiper || this.swiper.destroyed) this.initSwiper() + if (!this.swiper) return + this.swiper.update() + this.syncBrowseState() + if (this.swiper.isLocked || this.swiper.snapGrid.length <= 1) return + this.swiper.isEnd ? this.swiper.slideTo(0) : this.swiper.slideNext() + }, + // 无请求参数;data 为当前用户可见且尚未结束的活动数组,保留本校筛选规则。 listAct() { this.$axios.post("/platform/home/listHomeActivity").then((res) => { if (res.code === 0) { // for (let i = 0; i < 10; i++) { // this.actList = this.actList.concat(res.data) // } - this.actList = res.data + this.actList = res.data || [] // 数据加载完成后初始化Swiper this.$nextTick(() => { this.initSwiper() }) + } else { + this.$emit('browse-state', {enabled: false, label: '活动加载失败'}) } + }).catch(() => { + this.$emit('browse-state', {enabled: false, label: '活动加载失败'}) }) }, /*初始化Swiper*/ @@ -80,6 +120,7 @@ const act = { } const swiperElement = this.$refs.activitySwiper if (!swiperElement) { + this.syncBrowseState() return } // 仅操作当前组件的轮播节点,避免页面其他轮播或尺寸观察器影响卡片宽度。 @@ -89,13 +130,22 @@ const act = { centeredSlides: false, loop: false, watchOverflow: true, + // 双栏中的活动按容器宽度排布,避免用整个窗口宽度挤出过多卡片。 + breakpointsBase: 'container', observer: false, observeParents: false, - resizeObserver: false, + // 页面内容加载后可能出现滚动条,需重新计算列宽,避免末张封面及圆角被裁切。 + resizeObserver: true, + roundLengths: true, + on: { + init: () => this.$nextTick(() => this.updateNavigationPosition()), + resize: () => this.$nextTick(() => this.updateNavigationPosition()), + slidesUpdated: () => this.$nextTick(() => this.updateNavigationPosition()), + }, // 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。 mousewheel: { enabled: true, - forceToAxis: false, + forceToAxis: true, releaseOnEdges: true, sensitivity: 0.8, }, @@ -108,17 +158,15 @@ const act = { type: 'progressbar', }, breakpoints: { - 768: { + 480: { slidesPerView: 2, }, - 1024: { - slidesPerView: 4, - }, - 1200: { - slidesPerView: 5, + 740: { + slidesPerView: 3, } } }) + this.syncBrowseState() }, // 活动状态 @@ -308,12 +356,9 @@ const act = { height: 100% !important; max-width: 100% !important; max-height: 128px !important; - object-fit: fill !important; - transition: transform 0.3s ease; - } - - .activity-swiper .swiper-slide .item:hover .img-box img { - transform: scale(1.1); + /* 活动海报含有文字,等比完整显示且悬停时不放大,避免内容被裁切。 */ + object-fit: contain !important; + object-position: center; } .activity-swiper .swiper-slide .item h4 { diff --git a/src/main/resources/views/layouts/v4/home/entry.js b/src/main/resources/views/layouts/v4/home/entry.js index f27d663..01b3fa7 100644 --- a/src/main/resources/views/layouts/v4/home/entry.js +++ b/src/main/resources/views/layouts/v4/home/entry.js @@ -1,18 +1,19 @@ const entry = { template: /*language=HTML*/ `
-
-
-
- - {{ section.title }} - /Applications -
+
+
+
+ 全部应用 +
+
-
-
+
-
{{ item.name }}
-
+
{{ item.name }}
+
+
`, data() { return { + activeSection: 'serv', hasMore: true, entries: { serv: [], @@ -57,8 +60,12 @@ const entry = { this.getFavorite() }, methods: { - // 查询推荐服务 - async getRecommendService() { + // sectionKey 为 serv(推荐服务)、app(推荐应用)或 fav(我的收藏);通过组件方法更新栏目,避免内联事件中 this 指向错误。 + switchSection(sectionKey) { + this.$set(this, 'activeSection', sectionKey) + }, + // platform 固定传 PC;Result.data 为当前用户有权限的推荐服务数组。 + getRecommendService() { this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => { if (res.code === 0) { this.entries.serv = res.data @@ -66,8 +73,8 @@ const entry = { }) }, - // 获取推荐应用 - async getRecommendApp() { + // platform 固定传 PC;Result.data 为当前用户可进入的推荐应用数组。 + getRecommendApp() { this.$axios.post('/platform/home/listRecommendApp', {platform: "PC"}).then(res => { if (res.code === 0) { this.entries.app = res.data @@ -75,8 +82,8 @@ const entry = { }) }, - // 获取我的收藏 - async getFavorite() { + // platform 固定传 PC;Result.data 为当前用户收藏且有权限的菜单数组。 + getFavorite() { this.$axios.post('/platform/home/listFavorite', {platform: "PC"}).then(res => { if (res.code === 0) { this.entries.fav = res.data @@ -127,11 +134,6 @@ const entry = { margin-top: 22px; } - /* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */ - .entry-section-fav { - display: none; - } - .entry-section-header { display: flex; align-items: center; diff --git a/src/main/resources/views/layouts/v4/home/index.html b/src/main/resources/views/layouts/v4/home/index.html index 60e7f4e..34aaa19 100644 --- a/src/main/resources/views/layouts/v4/home/index.html +++ b/src/main/resources/views/layouts/v4/home/index.html @@ -181,42 +181,65 @@ layout("/layouts/v4/baseLayout.html"){ -
+ + + + + +
-
- `, data() { @@ -36,6 +38,15 @@ const workTemplate = { } }, methods: { + // 每次滚动一屏模板,到末尾后回到开头;无模板时不执行。 + browseTemplates() { + const grid = this.$refs.templateGrid + if (!grid) return + // 模板仍使用全部真实数据,横向翻页不截断管理员可用的入口。 + const atEnd = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 2 + grid.scrollTo({left: atEnd ? 0 : grid.scrollLeft + grid.clientWidth, behavior: 'smooth'}) + }, + // 无请求参数;Result.data 为当前管理员可用的工作模板数组。 listTemplate() { this.$axios.post("/platform/home/listHomeTemplate").then((res) => { if (res.code === 0) { diff --git a/src/main/resources/views/layouts/v4/home/user.js b/src/main/resources/views/layouts/v4/home/user.js index 6436415..e5e38f0 100644 --- a/src/main/resources/views/layouts/v4/home/user.js +++ b/src/main/resources/views/layouts/v4/home/user.js @@ -4,40 +4,37 @@ const user = {
-