From b0256ccac3a185c78cb2f5597282813e367cd2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?= <12369755+htcloud1@user.noreply.gitee.com> Date: Wed, 26 Aug 2026 18:13:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AE=AE=E5=8A=B3=E5=8A=A1/?= =?UTF-8?q?=E4=BC=9A=E5=8A=A1=E6=9D=90=E6=96=99=E5=88=86=E8=BD=A8=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=20+=20=E6=B3=A8=E5=86=8C=E9=A1=B5=E6=A0=87=E9=A2=98?= =?UTF-8?q?=20+=20=E4=B8=8B=E6=8B=89=E7=A9=BA=E5=80=BC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 会议材料分轨 (material_audit_stage 单轨 → labor/service 双轨): - biz_meeting: labor_audit_stage/labor_compliance_approved + service_audit_stage/service_compliance_approved 四列替换原单轨两列 - StageDeriver: 单轨状态机(R/N/C0/C1/A) + 角色优先级选代表轨 + 会议物理态取两轨最小进度 - MeetingAuditStageEnum / 前端 meetingStage.js 镜像: 分轨推导 + 颜色跟随角色展示阶段(避免文案与颜色错位) - BizMeetingController/ServiceImpl/AuditLog + mapper + MeetingStageScheduler 同步分轨字段 - MeetingDetail/Meetings/doctor/executor Meetings/ManagerProjectDetail/ManagerProjectsAssign/PublicityDetail 同步双轨 其它: - 注册页 (执行单位/专家/支持单位) 加居中页标题 - DoctorDeptSelect/DoctorTitleSelect 空字符串统一 null, 避免挂载必填项飘红 - BizOrgMapper sponsor/executor options 加 status 字段 - sponsor/NewPerson 编辑态 orgName 只读 --- .../controller/BizMeetingController.java | 229 +++++++---- .../com/ruoyi/business/domain/BizMeeting.java | 24 +- .../business/domain/BizMeetingAuditLog.java | 8 +- .../scheduler/MeetingStageScheduler.java | 11 +- .../ruoyi/business/service/StageDeriver.java | 138 ++++--- .../service/impl/BizMeetingServiceImpl.java | 7 +- .../business/BizMeetingAuditLogMapper.xml | 5 +- .../mapper/business/BizMeetingMapper.xml | 33 +- .../mapper/business/BizOrgMapper.xml | 2 + .../common/enums/MeetingAuditStageEnum.java | 7 +- ry-vue3/src/components/DoctorDeptSelect.vue | 3 +- ry-vue3/src/components/DoctorTitleSelect.vue | 3 +- ry-vue3/src/utils/meetingStage.js | 161 +++++--- ry-vue3/src/views/auth/RegisterExecutor.vue | 3 + ry-vue3/src/views/auth/RegisterExpert.vue | 3 + ry-vue3/src/views/auth/RegisterSponsor.vue | 3 + ry-vue3/src/views/doctor/Meetings.vue | 2 +- ry-vue3/src/views/executor/Meetings.vue | 2 +- .../views/manager/ManagerProjectDetail.vue | 68 ++-- .../views/manager/ManagerProjectsAssign.vue | 11 +- ry-vue3/src/views/meetings/MeetingDetail.vue | 354 +++++++++++------- ry-vue3/src/views/meetings/Meetings.vue | 82 ++-- ry-vue3/src/views/portal/PublicityDetail.vue | 9 +- ry-vue3/src/views/sponsor/NewPerson.vue | 2 +- ry-vue3/src/views/sponsor/SponsorPeople.vue | 31 +- 25 files changed, 752 insertions(+), 449 deletions(-) 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 可多选, 每轨独立提交). *
- * 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
* 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
- * 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
* 记录会议审核的每一次流转: 谁、什么时间、什么意见、当前阶段.
- * 与 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 / 审核时间 …),
+ * 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的两类事实落地:
*
@@ -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 状态机, 会议级阶段由两轨汇合:
*
- * 优先级自上而下命中即返回:
+ * 固定顶格: 冻结中 → 已完结 → 已结算. 否则按角色优先级选「代表轨」再渲染单轨措辞:
*
* 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), 不在此调度器范围.
*
- *
*
* @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