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 9ad2156..5274a00 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
@@ -285,7 +285,7 @@ public class BizMeetingController extends BaseController {
*
校验 1: 当前用户是该会议执行方 (项目级归属, 强校验)
* 校验 2: 已执行 (is_executed=1) 且未冻结
* 校验 3: 所选轨 audit_stage ∈ {NOT_SUBMITTED, REJECTED}
- * 校验 4: 所选轨在 biz_meeting_material 各至少 1 条 (劳务 L_* / 会务 M_*)
+ * 校验 4: 会务轨在 biz_meeting_material 至少 1 条 (M_*); 劳务轨 = 参会人员列表至少 1 人 (非上传文件)
*
* body: { "types": ["LABOR","SERVICE"] }
* 通过后该轨 → SUBMITTED (compliance_approved=0), 每轨记一条 audit_log.
@@ -310,8 +310,9 @@ public class BizMeetingController extends BaseController {
}
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_"));
+ // 劳务材料 = 参会人员列表 (不是上传的文件), 提交门槛 = 至少 1 名参会人
+ boolean hasLabor = !attendeeService.selectByMeetingId(meetingId).isEmpty();
for (String type : types) {
String label = "LABOR".equals(type) ? "劳务材料" : "会务材料";
@@ -319,8 +320,11 @@ public class BizMeetingController extends BaseController {
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 + "至少上传一条");
+ if ("LABOR".equals(type)) {
+ if (!hasLabor) throw new ServiceException("请先添加参会人员 (劳务材料)");
+ } else if (!hasService) {
+ throw new ServiceException("会务材料至少上传一条");
+ }
}
for (String type : types) {
@@ -685,8 +689,12 @@ public class BizMeetingController extends BaseController {
log.setAuditResult(result);
log.setOpinion(opinion);
log.setMaterialType(materialType);
- log.setCreateTime(new Date());
- log.setAuditTime(new Date());
+ Date now = new Date();
+ log.setCreateTime(now);
+ log.setAuditTime(now);
+ // 本次动作所在轨「最近时间」记为 now, 供 deriveDisplay 按最近时间合并两轨 (4 列阶段快照)
+ if ("LABOR".equals(materialType)) m.setLaborAuditTime(now);
+ else if ("SERVICE".equals(materialType)) m.setServiceAuditTime(now);
log.setExecutorStage(stageDeriver.deriveDisplay("executor", m));
log.setSponsorStage(stageDeriver.deriveDisplay("sponsor", m));
log.setManagerStage(stageDeriver.deriveDisplay("manager", m));
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 90aaec7..6e461fa 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
@@ -99,6 +99,12 @@ public class BizMeeting extends BaseEntity {
/** 材料最近一次审核动作时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date materialAuditTime;
+ /** 劳务材料最后一次动作时间 (派生字段, 标量子查询 max(audit_time) where material_type='LABOR', 不落库) */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date laborAuditTime;
+ /** 会务材料最后一次动作时间 (派生字段, 标量子查询 max(audit_time) where material_type='SERVICE', 不落库) */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date serviceAuditTime;
/** 劳务材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */
private Integer laborComplianceApproved;
/** 会务材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */
@@ -219,6 +225,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 Date getLaborAuditTime() { return laborAuditTime; }
+ public void setLaborAuditTime(Date laborAuditTime) { this.laborAuditTime = laborAuditTime; }
+ public Date getServiceAuditTime() { return serviceAuditTime; }
+ public void setServiceAuditTime(Date serviceAuditTime) { this.serviceAuditTime = serviceAuditTime; }
public Integer getLaborComplianceApproved() { return laborComplianceApproved; }
public void setLaborComplianceApproved(Integer laborComplianceApproved) { this.laborComplianceApproved = laborComplianceApproved; }
public Integer getServiceComplianceApproved() { return serviceComplianceApproved; }
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 1d6eb95..205f792 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
@@ -88,8 +88,42 @@ public class StageDeriver
if (t(m.getIsFinished())) return "已完结";
if (t(m.getIsSettled())) return "已结算";
- int chosen = chooseState(role, laborState(m), serviceState(m));
- return render(role, chosen, executionPhase(m));
+ int phase = executionPhase(m);
+ int labor = laborState(m);
+ int service = serviceState(m);
+
+ // 中性角色 (admin/doctor/expert): 维持原逻辑 (最小进度 + 单轨措辞), 不加前缀
+ if (!"executor".equals(role) && !"sponsor".equals(role) && !"manager".equals(role))
+ {
+ return render(role, Math.min(labor, service), phase);
+ }
+
+ // 三流程角色: 两轨合并 + 劳务/会务前缀
+ String laborLabel = render(role, labor, phase);
+ String serviceLabel = render(role, service, phase);
+ if (laborLabel.equals(serviceLabel)) return laborLabel;
+
+ // 待办态最高优先: manager=C0(2), sponsor=C1(3), executor=R(0)
+ int urgent = "manager".equals(role) ? 2 : "sponsor".equals(role) ? 3 : 0;
+ String track;
+ String label;
+ if (labor == urgent) { track = "labor"; label = laborLabel; }
+ else if (service == urgent) { track = "service"; label = serviceLabel; }
+ else
+ {
+ Date lt = m.getLaborAuditTime();
+ Date st = m.getServiceAuditTime();
+ boolean laborNewer = lt != null && (st == null || !lt.before(st));
+ if (laborNewer) { track = "labor"; label = laborLabel; }
+ else { track = "service"; label = serviceLabel; }
+ }
+
+ // 仅 待审核/审核通过/待整改/待结算 四类加前缀
+ if ("待审核".equals(label) || "审核通过".equals(label) || "待整改".equals(label) || "待结算".equals(label))
+ {
+ return ("labor".equals(track) ? "劳务" : "会务") + label;
+ }
+ return label;
}
/** 按角色优先级选代表轨 (两轨中优先级更高的那轨). */
@@ -139,7 +173,7 @@ public class StageDeriver
switch (s)
{
case 0: // R 退回
- return "executor".equals(role) ? "已退回" : "待整改";
+ return "待整改";
case 1: // N 未提交 (时间驱动三态: 未执行 → 执行中 → 已执行, 所有角色统一)
if (phase == 0) return "未执行";
if (phase == 1) return "执行中";
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 a4c7640..ac2a4c1 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
@@ -33,6 +33,8 @@
+
+
@@ -59,6 +61,8 @@