diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java index 4dbdf4d..c63c100 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java @@ -31,6 +31,7 @@ import com.ruoyi.business.notify.BizNotifyService; import com.ruoyi.business.service.IBizMeetingService; import com.ruoyi.business.service.IBizOrgService; import com.ruoyi.business.service.IBizProjectService; +import com.ruoyi.business.service.IBizProjectSponsorAssignService; import com.ruoyi.business.service.IBizMeetingAttendeeService; import com.ruoyi.business.service.StageDeriver; import com.ruoyi.business.service.PosterService; @@ -63,6 +64,8 @@ public class BizMeetingController extends BaseController { @Autowired private IBizProjectService bizProjectService; @Autowired + private IBizProjectSponsorAssignService bizProjectSponsorAssignService; + @Autowired private IBizOrgService bizOrgService; @Autowired private StageDeriver stageDeriver; @@ -87,12 +90,12 @@ public class BizMeetingController extends BaseController { bizMeeting.getParams().put("sponsorAdminUserId", uid); } } - // executor 数据权限: 只看"我的项目"下的会议 (与项目列表 selectExecutorList/selectExecutorStaffList 同源). - // MAIN 走 biz_project_assign (execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id. + // executor 数据权限: MAIN 看"我的执行单位"下的会议 (走 biz_project_assign.execution_unit_id); + // SUB(执行人) 只看"本人创建的会议" (按 create_by 过滤, 不再看整个分配项目的会议). else if ("executor".equals(roleType)) { SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid); if (current != null && "SUB".equals(current.getAccountType())) { - bizMeeting.getParams().put("executorStaffUserId", uid); + bizMeeting.getParams().put("executorCreatorUsername", SecurityUtils.getUsername()); } else { bizMeeting.getParams().put("executorUserId", uid); } @@ -233,18 +236,19 @@ public class BizMeetingController extends BaseController { // =================================================================== /** - * 执行人员提交材料 + * 执行人员提交材料 (分轨: 劳务 LABOR / 会务 SERVICE 可多选, 每轨独立提交). * - * 通过后 material → SUBMITTED (compliance_approved=0), 记 audit_log. + * body: { "types": ["LABOR","SERVICE"] } + * 通过后该轨 → SUBMITTED (compliance_approved=0), 每轨记一条 audit_log. */ @Log(title = "会议审核", businessType = BusinessType.UPDATE) @PostMapping("/{meetingId}/submit-material") - public AjaxResult submitMaterial(@PathVariable("meetingId") Long meetingId) { + public AjaxResult submitMaterial(@PathVariable("meetingId") Long meetingId, @RequestBody SubmitBody body) { BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); @@ -254,33 +258,52 @@ public class BizMeetingController extends BaseController { } if (!isExecuted(m)) throw new ServiceException("会议尚未执行, 不能提交材料"); if (isFrozen(m)) throw new ServiceException("会议已冻结, 不能提交材料"); - String stage = m.getMaterialAuditStage(); - if (!"NOT_SUBMITTED".equals(stage) && !"REJECTED".equals(stage)) { - throw new ServiceException("当前阶段 (" + stage + ") 不允许提交材料"); + + List types = body == null ? null : body.getTypes(); + if (types == null || types.isEmpty()) throw new ServiceException("请选择要提交的材料类型"); + for (String type : types) { + if (!"LABOR".equals(type) && !"SERVICE".equals(type)) throw new ServiceException("未知材料类型: " + type); } List mats = bizMeetingMaterialService.selectByMeetingId(meetingId); boolean hasLabor = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("L_")); boolean hasService = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("M_")); - if (!hasLabor || !hasService) { - throw new ServiceException("劳务材料和会务材料各至少上传一条"); + + for (String type : types) { + String label = "LABOR".equals(type) ? "劳务材料" : "会务材料"; + String stage = "LABOR".equals(type) ? m.getLaborAuditStage() : m.getServiceAuditStage(); + if (!"NOT_SUBMITTED".equals(stage) && !"REJECTED".equals(stage)) { + throw new ServiceException(label + "当前阶段 (" + stage + ") 不允许提交"); + } + boolean has = "LABOR".equals(type) ? hasLabor : hasService; + if (!has) throw new ServiceException(label + "至少上传一条"); } - m.setMaterialAuditStage("SUBMITTED"); - m.setMaterialComplianceApproved(0); + for (String type : types) { + if ("LABOR".equals(type)) { + m.setLaborAuditStage("SUBMITTED"); + m.setLaborComplianceApproved(0); + } else { + m.setServiceAuditStage("SUBMITTED"); + m.setServiceComplianceApproved(0); + } + } m.setMaterialAuditTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - appendAuditLog(m, "MATERIAL", "SUBMITTED", "执行人员提交材料"); + for (String type : types) { + appendAuditLog(m, "MATERIAL", "SUBMITTED", "执行人员提交" + ("LABOR".equals(type) ? "劳务" : "会务") + "材料", type); + } return success("SUBMITTED"); } /** - * 合规审核 (role_type=manager), 材料两级审核中的第一级. + * 合规审核 (role_type=manager), 材料两级审核中的第一级 (分轨). *

- * body: { "approved": true|false, "opinion": "..." } - *

合规审中判据: material_audit_stage=SUBMITTED 且 compliance_approved=0. - * 通过 → compliance_approved=1 (转入支持方审), 拒绝 → REJECTED (退回执行方). + * body: { "items": [{ "type": "LABOR|SERVICE", "approved": true|false }], "opinion": "..." } + *

逐轨审核: 该轨 C0 (SUBMITTED 且 compliance_approved=0) 才可审; + * 通过 → compliance_approved=1 (转支持方审), 拒绝 → REJECTED (退回执行方). + * 一轨给通过、另一轨给拒绝 在同一 body 里各自表达. */ @Log(title = "会议审核", businessType = BusinessType.UPDATE) @PostMapping("/{meetingId}/audit-compliance") @@ -290,17 +313,25 @@ public class BizMeetingController extends BaseController { } BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); - boolean approved = Boolean.TRUE.equals(body.getApproved()); - if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) { - throw new ServiceException("拒绝时意见不能为空"); + List items = body == null ? null : body.getItems(); + if (items == null || items.isEmpty()) throw new ServiceException("请选择要审核的材料类型"); + for (AuditItem it : items) { + if (!"LABOR".equals(it.getType()) && !"SERVICE".equals(it.getType())) throw new ServiceException("未知材料类型: " + it.getType()); + if (!Boolean.TRUE.equals(it.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) { + throw new ServiceException("拒绝时意见不能为空"); + } } - return success(doComplianceAudit(m, approved, body.getOpinion())); + for (AuditItem it : items) { + doComplianceAuditTrack(m, it.getType(), Boolean.TRUE.equals(it.getApproved()), body.getOpinion()); + } + return success("OK"); } /** * 批量合规审核 (role_type=manager): 一次对多个会议执行材料一级审核. *

* body: { "meetingIds": [..], "approved": true|false, "opinion": "..." } + *

分轨简化: 一个结果应用到每个会议所有处于合规审中 (C0) 的轨 (劳务/会务), 不做逐轨不同结果. *

best-effort: 逐条审核, 状态不符的会议跳过并回执失败原因, 不整批回滚. */ @Log(title = "会议审核", businessType = BusinessType.UPDATE) @@ -325,7 +356,7 @@ public class BizMeetingController extends BaseController { try { BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); - doComplianceAudit(m, approved, body.getOpinion()); + doComplianceAuditAllC0(m, approved, body.getOpinion()); successIds.add(meetingId); } catch (ServiceException e) { Map f = new HashMap<>(); @@ -343,35 +374,53 @@ public class BizMeetingController extends BaseController { return success(data); } + /** 某轨是否处于合规审中 (C0): SUBMITTED 且 compliance_approved != 1. */ + private static boolean isCompliancePending(String stage, Integer compliance) { + return "SUBMITTED".equals(stage) && (compliance == null || compliance != 1); + } + /** - * 内部: 合规审核核心流转 (阶段校验 + 状态写入 + audit_log). 单条/批量共用. + * 内部: 合规审核单轨核心流转 (阶段校验 + 状态写入 + audit_log). 单条/批量共用. */ - private String doComplianceAudit(BizMeeting m, boolean approved, String opinion) { - Integer compliance = m.getMaterialComplianceApproved(); - if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || (compliance != null && compliance == 1)) { - throw new ServiceException("当前阶段不允许合规审核"); + private void doComplianceAuditTrack(BizMeeting m, String type, boolean approved, String opinion) { + boolean labor = "LABOR".equals(type); + String stage = labor ? m.getLaborAuditStage() : m.getServiceAuditStage(); + Integer compliance = labor ? m.getLaborComplianceApproved() : m.getServiceComplianceApproved(); + if (!isCompliancePending(stage, compliance)) { + throw new ServiceException((labor ? "劳务材料" : "会务材料") + "当前阶段不允许合规审核"); } - String result = approved ? "APPROVED" : "REJECTED"; if (approved) { - m.setMaterialComplianceApproved(1); + if (labor) m.setLaborComplianceApproved(1); else m.setServiceComplianceApproved(1); } else { - m.setMaterialAuditStage("REJECTED"); + if (labor) m.setLaborAuditStage("REJECTED"); else m.setServiceAuditStage("REJECTED"); // 退回 → 提交截止时间重新计算 (now + 项目天数) m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date())); } m.setMaterialAuditTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - appendAuditLog(m, "MATERIAL", result, opinion); - return result; + appendAuditLog(m, "MATERIAL", approved ? "APPROVED" : "REJECTED", opinion, type); } /** - * 支持方(监察员) 审核, 材料两级审核中的第二级. + * 内部: 批量合规审核 — 一个结果应用到该会议所有处于合规审中 (C0) 的轨 (简化, 不做逐轨不同结果). + */ + private void doComplianceAuditAllC0(BizMeeting m, boolean approved, String opinion) { + List targets = new ArrayList<>(); + if (isCompliancePending(m.getLaborAuditStage(), m.getLaborComplianceApproved())) targets.add("LABOR"); + if (isCompliancePending(m.getServiceAuditStage(), m.getServiceComplianceApproved())) targets.add("SERVICE"); + if (targets.isEmpty()) throw new ServiceException("当前阶段不允许合规审核"); + for (String type : targets) { + doComplianceAuditTrack(m, type, approved, opinion); + } + } + + /** + * 支持方(监察员) 审核, 材料两级审核中的第二级 (分轨). *

- * body: { "approved": true|false, "opinion": "..." } - *

支持方审中判据: material_audit_stage=SUBMITTED 且 compliance_approved=1. - * 通过 → APPROVED (一并写监管意见), 拒绝 → REJECTED (退回执行方). + * body: { "items": [{ "type": "LABOR|SERVICE", "approved": true|false }], "opinion": "..." } + *

逐轨审核: 该轨 C1 (SUBMITTED 且 compliance_approved=1) 才可审; + * 通过 → APPROVED (任一轨通过即写监管意见), 拒绝 → REJECTED (退回执行方 + 通知). */ @Log(title = "会议审核", businessType = BusinessType.UPDATE) @PostMapping("/{meetingId}/audit-supervision") @@ -382,27 +431,57 @@ public class BizMeetingController extends BaseController { BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); - // 授权: 监察员 (biz_meeting_supervisor.sponsor_org_id) 或 支持方企业 (biz_project.sponsor_org_id) 均可审 + // 授权 (与列表可见性同源, 原则: 能看到就能监察): + // 1) 项目级监察员 (biz_project_sponsor_assign.monitor_user_id = 当前 user_id) — SUB 监察员列表同源 + // 2) 会议级监察员机构 (biz_meeting_supervisor.sponsor_org_id = 我的 org_id) + // 3) 支持方主账号 (biz_project.sponsor_org_id = 我的 org_id) + BizProject project = m.getProjectId() == null ? null : bizProjectService.getById(m.getProjectId()); + boolean isProjectMonitor = m.getProjectId() != null && userId != null + && bizProjectSponsorAssignService.listByProjectId(String.valueOf(m.getProjectId())).stream() + .anyMatch(a -> userId.equals(a.getMonitorUserId())); boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream() .anyMatch(s -> myOrgId != null && myOrgId.equals(s.getSponsorOrgId())); - BizProject project = m.getProjectId() == null ? null : bizProjectService.getById(m.getProjectId()); boolean isSponsorMain = project != null && myOrgId != null && myOrgId.equals(project.getSponsorOrgId()); - if (!isSupervisor && !isSponsorMain) { + if (!isProjectMonitor && !isSupervisor && !isSponsorMain) { throw new ServiceException("您不是该会议监察员, 无权监察"); } - boolean approved = Boolean.TRUE.equals(body.getApproved()); - if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) { - throw new ServiceException("拒绝时意见不能为空"); + List items = body == null ? null : body.getItems(); + if (items == null || items.isEmpty()) throw new ServiceException("请选择要审核的材料类型"); + boolean anyRejected = false; + for (AuditItem it : items) { + if (!"LABOR".equals(it.getType()) && !"SERVICE".equals(it.getType())) throw new ServiceException("未知材料类型: " + it.getType()); + if (!Boolean.TRUE.equals(it.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) { + throw new ServiceException("拒绝时意见不能为空"); + } + } + for (AuditItem it : items) { + boolean approved = Boolean.TRUE.equals(it.getApproved()); + doSupervisionAuditTrack(m, it.getType(), approved, body.getOpinion()); + if (!approved) anyRejected = true; } - Integer compliance = m.getMaterialComplianceApproved(); - if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || compliance == null || compliance != 1) { - throw new ServiceException("当前阶段不允许监察审核"); + // 退回 → 通知执行方 (待整改 + 说明, 任一轨被拒即通知一次) + if (anyRejected) { + for (BizMeetingExecutor e : bizMeetingExecutorService.selectByMeetingId(meetingId)) { + bizNotifyService.meetingSupervisionRejected(resolveMainUserIdByOrgId(e.getExecutorOrgId()), meetingId, m.getMeetingName(), body.getOpinion()); + } } + return success("OK"); + } - String result = approved ? "APPROVED" : "REJECTED"; - m.setMaterialAuditStage(approved ? "APPROVED" : "REJECTED"); + /** + * 内部: 支持方审核单轨核心流转 (阶段校验 + 状态写入 + audit_log). + */ + private void doSupervisionAuditTrack(BizMeeting m, String type, boolean approved, String opinion) { + boolean labor = "LABOR".equals(type); + String stage = labor ? m.getLaborAuditStage() : m.getServiceAuditStage(); + Integer compliance = labor ? m.getLaborComplianceApproved() : m.getServiceComplianceApproved(); + if (!"SUBMITTED".equals(stage) || compliance == null || compliance != 1) { + throw new ServiceException((labor ? "劳务材料" : "会务材料") + "当前阶段不允许监察审核"); + } + if (labor) m.setLaborAuditStage(approved ? "APPROVED" : "REJECTED"); + else m.setServiceAuditStage(approved ? "APPROVED" : "REJECTED"); // 退回 → 提交截止时间重新计算 (now + 项目天数) if (!approved) { m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date())); @@ -410,21 +489,13 @@ public class BizMeetingController extends BaseController { m.setMaterialAuditTime(new Date()); // 材料通过 → 写监管意见 (支持方的书面意见) if (approved) { - m.setSupervisionOpinion(body.getOpinion()); + m.setSupervisionOpinion(opinion); m.setSupervisionBy(SecurityUtils.getUsername()); m.setSupervisionTime(new Date()); } m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - appendAuditLog(m, "MATERIAL", result, body.getOpinion()); - - // 退回 → 通知执行方 (待整改 + 说明) - if (!approved) { - for (BizMeetingExecutor e : bizMeetingExecutorService.selectByMeetingId(meetingId)) { - bizNotifyService.meetingSupervisionRejected(resolveMainUserIdByOrgId(e.getExecutorOrgId()), meetingId, m.getMeetingName(), body.getOpinion()); - } - } - return success(result); + appendAuditLog(m, "MATERIAL", approved ? "APPROVED" : "REJECTED", opinion, type); } /** @@ -443,8 +514,8 @@ public class BizMeetingController extends BaseController { } BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); - if (!"APPROVED".equals(m.getMaterialAuditStage())) { - throw new ServiceException("材料审核通过后才能结算"); + if (!"APPROVED".equals(m.getLaborAuditStage()) || !"APPROVED".equals(m.getServiceAuditStage())) { + throw new ServiceException("劳务与会务材料都审核通过后才能结算"); } if (isSettled(m)) throw new ServiceException("会议已结算"); // 费用未汇总完 (fee_calc_status=0) 禁止结算: 此时 labor_fee/meeting_fee 可能为旧值/0, 直接回写会污染项目金额. @@ -465,7 +536,7 @@ public class BizMeetingController extends BaseController { m.setSettleTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - appendAuditLog(m, "SETTLE", "APPROVED", "会议结算"); + appendAuditLog(m, "SETTLE", "APPROVED", "会议结算", null); return success("SETTLED"); } @@ -487,7 +558,7 @@ public class BizMeetingController extends BaseController { m.setFinishTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - appendAuditLog(m, "FINISH", "APPROVED", "会议完结"); + appendAuditLog(m, "FINISH", "APPROVED", "会议完结", null); return success("FINISHED"); } @@ -559,13 +630,14 @@ public class BizMeetingController extends BaseController { /** * 内部: 写一条 audit_log (4 列角色展示状态由 post-transition 事实推导). */ - private void appendAuditLog(BizMeeting m, String auditType, String result, String opinion) { + private void appendAuditLog(BizMeeting m, String auditType, String result, String opinion, String materialType) { BizMeetingAuditLog log = new BizMeetingAuditLog(); log.setMeetingId(m.getMeetingId()); log.setAuditor(SecurityUtils.getUsername()); log.setAuditType(auditType); log.setAuditResult(result); log.setOpinion(opinion); + log.setMaterialType(materialType); log.setCreateTime(new Date()); log.setAuditTime(new Date()); log.setExecutorStage(stageDeriver.deriveDisplay("executor", m)); @@ -575,16 +647,33 @@ public class BizMeetingController extends BaseController { bizMeetingAuditLogService.insert(log); } - /** request body for audit endpoints */ + /** request body for audit endpoints (逐轨: 每项一个材料类型 + 通过/拒绝) */ public static class AuditBody { - private Boolean approved; // true=通过 false=拒绝 - private String opinion; // 意见 - public Boolean getApproved() { return approved; } - public void setApproved(Boolean approved) { this.approved = approved; } + private List items; // 逐轨审核项 + private String opinion; // 意见 (拒绝时必填) + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } public String getOpinion() { return opinion; } public void setOpinion(String opinion) { this.opinion = opinion; } } + /** 单轨审核项 */ + public static class AuditItem { + private String type; // LABOR / SERVICE + private Boolean approved; // true=通过 false=拒绝 + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public Boolean getApproved() { return approved; } + public void setApproved(Boolean approved) { this.approved = approved; } + } + + /** request body for submit-material (分轨多选) */ + public static class SubmitBody { + private List types; // ["LABOR","SERVICE"] + public List getTypes() { return types; } + public void setTypes(List types) { this.types = types; } + } + /** request body for batch audit endpoint */ public static class BatchAuditBody { private List meetingIds; diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java index 705414f..5c26b4f 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java @@ -72,8 +72,10 @@ public class BizMeeting extends BaseEntity { /** 监察时间 (与 DB datetime 对齐) */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date supervisionTime; - /** 材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */ - private String materialAuditStage; + /** 劳务材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */ + private String laborAuditStage; + /** 会务材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */ + private String serviceAuditStage; /** 是否执行 0否1是 (会议开始时间到, scheduler 置1) */ private Integer isExecuted; /** 执行时间 */ @@ -97,8 +99,10 @@ public class BizMeeting extends BaseEntity { /** 材料最近一次审核动作时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date materialAuditTime; - /** 材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */ - private Integer materialComplianceApproved; + /** 劳务材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */ + private Integer laborComplianceApproved; + /** 会务材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */ + private Integer serviceComplianceApproved; /** 提交截止时间 (建会 = end_time + 项目 submit_deadline_days 天; 退回/解冻 = now + 天数; null = 项目未设天数, 永不冻结) */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date submitDeadline; @@ -189,8 +193,10 @@ public class BizMeeting extends BaseEntity { public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; } public String getLaborSigned() { return laborSigned; } public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; } - public String getMaterialAuditStage() { return materialAuditStage; } - public void setMaterialAuditStage(String materialAuditStage) { this.materialAuditStage = materialAuditStage; } + public String getLaborAuditStage() { return laborAuditStage; } + public void setLaborAuditStage(String laborAuditStage) { this.laborAuditStage = laborAuditStage; } + public String getServiceAuditStage() { return serviceAuditStage; } + public void setServiceAuditStage(String serviceAuditStage) { this.serviceAuditStage = serviceAuditStage; } public Integer getIsExecuted() { return isExecuted; } public void setIsExecuted(Integer isExecuted) { this.isExecuted = isExecuted; } public Date getExecuteTime() { return executeTime; } @@ -209,8 +215,10 @@ public class BizMeeting extends BaseEntity { public void setFreezeTime(Date freezeTime) { this.freezeTime = freezeTime; } public Date getMaterialAuditTime() { return materialAuditTime; } public void setMaterialAuditTime(Date materialAuditTime) { this.materialAuditTime = materialAuditTime; } - public Integer getMaterialComplianceApproved() { return materialComplianceApproved; } - public void setMaterialComplianceApproved(Integer materialComplianceApproved) { this.materialComplianceApproved = materialComplianceApproved; } + public Integer getLaborComplianceApproved() { return laborComplianceApproved; } + public void setLaborComplianceApproved(Integer laborComplianceApproved) { this.laborComplianceApproved = laborComplianceApproved; } + public Integer getServiceComplianceApproved() { return serviceComplianceApproved; } + public void setServiceComplianceApproved(Integer serviceComplianceApproved) { this.serviceComplianceApproved = serviceComplianceApproved; } public Date getSubmitDeadline() { return submitDeadline; } public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; } public Long getUserId() { return userId; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAuditLog.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAuditLog.java index 25054a3..6368c55 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAuditLog.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAuditLog.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; * 会议审核流程日志对象 biz_meeting_audit_log *

* 记录会议审核的每一次流转: 谁、什么时间、什么意见、当前阶段. - * 与 biz_meeting.material_audit_stage 配合, 还原审核轨迹. + * 与 biz_meeting 劳务/会务两轨 audit_stage 配合, 还原审核轨迹. */ public class BizMeetingAuditLog { @@ -51,6 +51,9 @@ public class BizMeetingAuditLog { /** 审核结果 (APPROVED / REJECTED) */ private String auditResult; + /** 材料类型 (LABOR / SERVICE; NULL = 会议级操作如结算/完结, 或历史数据) */ + private String materialType; + /** 软删除标记 0否1是 (admin 删除会议时级联置1, 查询需 WHERE is_deleted=0) */ private Integer isDeleted; @@ -90,6 +93,9 @@ public class BizMeetingAuditLog { public String getAuditResult() { return auditResult; } public void setAuditResult(String auditResult) { this.auditResult = auditResult; } + public String getMaterialType() { return materialType; } + public void setMaterialType(String materialType) { this.materialType = materialType; } + public Integer getIsDeleted() { return isDeleted; } public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java index 73fb7a8..c88ebf5 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java @@ -10,12 +10,11 @@ import lombok.extern.slf4j.Slf4j; * 会议事实/阶段 自动流转调度器 (每分钟一次). *

* 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实 - * (is_executed / is_frozen / material_audit_stage / 审核时间 …), - * 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的三类事实落地: + * (is_executed / is_frozen / 劳务·会务两轨 audit_stage / 审核时间 …), + * 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的两类事实落地: *

  *   1) start_time 到 → is_executed=1 (执行中)
- *   2) end_time + submit_deadline_days 到 且 material 未提交 → is_frozen=1 (冻结)
- *   3) material 通过 且 审核时间过 24h → 待结算 (current_stage 缓存翻 AWAITING_SETTLEMENT)
+ *   2) submit_deadline 到 且 任一轨未提交/已退回 → is_frozen=1 (冻结)
  * 
* 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围. *

@@ -29,7 +28,7 @@ public class MeetingStageScheduler private BizMeetingMapper meetingMapper; /** - * 每分钟: start_time 已过 且 material 未提交 且未执行 → 置执行中. + * 每分钟: start_time 已过 且 劳务·会务两轨均未提交 且未执行 → 置执行中. */ @Scheduled(fixedRate = 60_000, initialDelay = 30_000) public void markExecuted() @@ -49,7 +48,7 @@ public class MeetingStageScheduler } /** - * 每分钟: material 未提交 且 提交截止时间 (end_time + submit_deadline_days) 已过 → 冻结. + * 每分钟: 任一轨未提交/已退回 且 提交截止时间已过 → 冻结 (会议级). */ @Scheduled(fixedRate = 60_000, initialDelay = 30_000) public void markFrozen() diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java index 51db81f..ff10d7a 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java @@ -7,11 +7,14 @@ import com.ruoyi.business.domain.BizMeeting; * 会议阶段推导器 (单一可信源). *

* 事实与展示分离: {@code biz_meeting} 只存事实 (is_executed/is_settled/is_finished/is_frozen - * + material_audit_stage + 审核时间 + compliance_approved), 各角色看到的 + * + 劳务/会务两轨各自 audit_stage + compliance_approved + 审核时间), 各角色看到的 * 「阶段名称」由本类实时计算. + *

+ * 劳务/会务材料分轨后, 每条轨道独立走 N/C0/C1/A/R 状态机, 会议级阶段由两轨汇合: *

* * @author guoju @@ -24,10 +27,29 @@ public class StageDeriver return v != null && v == 1; } + /** 单轨状态码: R=0, N=1, C0=2, C1=3, A=4 (数值越小 = 流程越靠前). */ + private static int stateOf(String stage, Integer complianceApproved) + { + if ("REJECTED".equals(stage)) return 0; + if ("SUBMITTED".equals(stage)) return t(complianceApproved) ? 3 : 2; + if ("APPROVED".equals(stage)) return 4; + return 1; // NOT_SUBMITTED (或 null 兜底) + } + + private static int laborState(BizMeeting m) + { + return stateOf(m.getLaborAuditStage(), m.getLaborComplianceApproved()); + } + + private static int serviceState(BizMeeting m) + { + return stateOf(m.getServiceAuditStage(), m.getServiceComplianceApproved()); + } + /** * 10 值物理阶段 (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/ * SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN), 由事实推导. - * 用于 current_stage 缓存 (列表筛选仍按物理态精确匹配). + * 两轨取最小进度 (流程最靠前的一轨决定会议物理态), 用于 current_stage 缓存 (列表筛选按物理态精确匹配). */ public String derivePhysicalStage(BizMeeting m) { @@ -35,26 +57,25 @@ public class StageDeriver if (t(m.getIsFinished())) return "FINISHED"; if (t(m.getIsSettled())) return "SETTLED"; - String material = m.getMaterialAuditStage(); - if ("REJECTED".equals(material)) return "RECTIFYING"; - if ("APPROVED".equals(material)) + int s = Math.min(laborState(m), serviceState(m)); + switch (s) { - return "AWAITING_SETTLEMENT"; + case 0: return "RECTIFYING"; + case 2: return "AWAITING_COMPLIANCE"; + case 3: return "AWAITING_SUPERVISION"; + case 4: return "AWAITING_SETTLEMENT"; + default: return t(m.getIsExecuted()) ? "RUNNING" : "NOT_STARTED"; // s = 1 (N) } - if ("SUBMITTED".equals(material)) - { - return t(m.getMaterialComplianceApproved()) ? "AWAITING_SUPERVISION" : "AWAITING_COMPLIANCE"; - } - // NOT_SUBMITTED (或 null 兜底) - return t(m.getIsExecuted()) ? "RUNNING" : "NOT_STARTED"; } /** * 各角色展示阶段名. role ∈ {executor, sponsor, manager, admin}; doctor/expert 等非流程角色按 admin 中性. *

- * 优先级自上而下命中即返回: + * 固定顶格: 冻结中 → 已完结 → 已结算. 否则按角色优先级选「代表轨」再渲染单轨措辞: *

-     *   冻结 → 完结 → 已结算 → (材料) 审核驳回 → 审核通过 → (材料) APPROVED → SUBMITTED → NOT_SUBMITTED
+     *   默认 (executor/admin/doctor): 最小进度 (R > N > C0 > C1 > A)
+     *   manager: 优先 C0(待合规审核) > R > N > C1 > A  → 有活「待审核」, 无活按最早环节优先
+     *   sponsor: 优先 C1(待支持方审核) > R > N > C0 > A  → 有活「待审核」, 无活按最早环节优先
      * 
*/ public String deriveDisplay(String role, BizMeeting m) @@ -63,44 +84,69 @@ public class StageDeriver if (t(m.getIsFinished())) return "已完结"; if (t(m.getIsSettled())) return "已结算"; - String material = m.getMaterialAuditStage(); + int chosen = chooseState(role, laborState(m), serviceState(m)); + return render(role, chosen, t(m.getIsExecuted())); + } - // 退回: 执行方看「已退回」, 其他方看「待整改」 - if ("REJECTED".equals(material)) + /** 按角色优先级选代表轨 (两轨中优先级更高的那轨). */ + private static int chooseState(String role, int labor, int service) + { + if ("manager".equals(role)) { - return "executor".equals(role) ? "已退回" : "待整改"; + return managerPriority(labor) <= managerPriority(service) ? labor : service; } - - // 材料已支持方通过 → 待结算 (支持方审通过即待结算, 不再有 24h 慢路径) - if ("APPROVED".equals(material)) + if ("sponsor".equals(role)) { - return "待结算"; + return sponsorPriority(labor) <= sponsorPriority(service) ? labor : service; } + // 默认: 最小进度 (状态码本身即 R + - select id, meeting_id, auditor, opinion, executor_stage, sponsor_stage, manager_stage, admin_stage, create_time, audit_time, audit_type, audit_result, is_deleted + select id, meeting_id, auditor, opinion, executor_stage, sponsor_stage, manager_stage, admin_stage, create_time, audit_time, audit_type, audit_result, material_type, is_deleted from biz_meeting_audit_log @@ -52,6 +53,7 @@ audit_time, audit_type, audit_result, + material_type, #{meetingId}, @@ -65,6 +67,7 @@ #{auditTime}, #{auditType}, #{auditResult}, + #{materialType}, diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml index 2cef332..6475c6a 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml @@ -21,7 +21,8 @@ - + + @@ -31,7 +32,8 @@ - + + @@ -51,7 +53,7 @@ - meeting_id, business_id, project_id, execution_unit_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, address, remark, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, material_compliance_approved, submit_deadline, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted + meeting_id, business_id, project_id, execution_unit_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, address, remark, current_stage, supervision_opinion, supervision_by, supervision_time, labor_audit_stage, service_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, labor_compliance_approved, service_compliance_approved, submit_deadline, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted select o.org_id as orgId, o.org_name as orgName, + o.status as status, u.user_name as userName from biz_org o join sys_user u on u.user_id = o.user_id @@ -135,6 +136,7 @@