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 状态机, 会议级阶段由两轨汇合:
*
- * - {@link #derivePhysicalStage(BizMeeting)}: 10 值物理阶段 (current_stage 缓存 + 列表筛选).
- * - {@link #deriveDisplay(String, BizMeeting)}: 各角色展示名 (audit_log 4 列 + 前端镜像).
+ * - {@link #derivePhysicalStage(BizMeeting)}: 10 值物理阶段 = 两轨最小进度 (current_stage 缓存 + 列表筛选).
+ * - {@link #deriveDisplay(String, BizMeeting)}: 各角色展示名 (audit_log 4 列 + 前端镜像),
+ * 按角色优先级选代表轨, 再用单轨措辞渲染.
*
*
* @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