diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..8c681fc
Binary files /dev/null and b/favicon.ico differ
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..6b95ee1
Binary files /dev/null and b/logo.png differ
diff --git a/qrcode_1.png b/qrcode_1.png
new file mode 100644
index 0000000..77d52db
Binary files /dev/null and b/qrcode_1.png differ
diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CameraController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CameraController.java
new file mode 100644
index 0000000..1bb3722
--- /dev/null
+++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CameraController.java
@@ -0,0 +1,36 @@
+package com.ruoyi.web.controller.common;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.core.domain.AjaxResult;
+
+/**
+ * 扫码拍照相机配置服务
+ *
+ *
前端生成二维码前调用, 拿到相机 H5 网页基础地址 (ruoyi.camera.base-url),
+ * 再拼接 ry-h5 页面路由 + meetingId + subType.
+ *
+ *
公开访问, 无需 token (SecurityConfig 已对 /common/camera/** 公开).
+ */
+@RestController
+@RequestMapping("/common/camera")
+public class CameraController
+{
+ @org.springframework.beans.factory.annotation.Autowired
+ private RuoYiConfig ruoyiConfig;
+
+ /**
+ * 返回相机网页基础地址
+ * @return {baseUrl: "http://localhost:8090/camera/"}
+ */
+ @GetMapping("/config")
+ public AjaxResult config()
+ {
+ AjaxResult ajax = AjaxResult.success();
+ String baseUrl = ruoyiConfig.getCamera() != null ? ruoyiConfig.getCamera().getBaseUrl() : null;
+ ajax.put("baseUrl", baseUrl);
+ return ajax;
+ }
+}
diff --git a/ry-api/ruoyi-admin/src/main/resources/application-druid.yml b/ry-api/ruoyi-admin/src/main/resources/application-druid.yml
index 95480d3..2398376 100644
--- a/ry-api/ruoyi-admin/src/main/resources/application-druid.yml
+++ b/ry-api/ruoyi-admin/src/main/resources/application-druid.yml
@@ -5,9 +5,9 @@ spring:
driverClassName: com.mysql.cj.jdbc.Driver
druid:
master:
- url: jdbc:mysql://127.0.0.1:3306/guoju0808?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ url: jdbc:mysql://mmos.uvwcloud.com:3306/guoju0808?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
- password: cu2oh2co3
+ password: Qljl1rh_
slave:
enabled: false
url:
diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml
index 5cfef2a..df90b96 100644
--- a/ry-api/ruoyi-admin/src/main/resources/application.yml
+++ b/ry-api/ruoyi-admin/src/main/resources/application.yml
@@ -30,12 +30,16 @@ ruoyi:
accessKeySecret: UIkwjMpmlYjgX5IMLjPj8FQNPthdlR
signName: 北京仙仁掌医学科技发展
template: SMS_321560247
- inviteTemplate: SMS_492460505
+ esignTemplate: SMS_492460505
+ esignBaseUrl: https://ringdoctor.com/hg
endpoint: dysmsapi.aliyuncs.com
regionId: cn-hangzhou
# 发票 OCR (ry-ocr 微服务, PaddleOCR + FastAPI, 默认 http://127.0.0.1:8801)
ocr:
base-url: http://127.0.0.1:8801
+ # 扫码拍照 (ry-h5 相机网页地址, 前端二维码目标 URL)
+ camera:
+ base-url: https://ringdoctor.com/camera/
# 开发环境配置
server:
@@ -58,9 +62,9 @@ server:
# 日志配置
logging:
level:
- com.ruoyi: debug
- org.springframework: debug
- com.ruoyi.business: debug
+ com.ruoyi: info
+ org.springframework: info
+ com.ruoyi.business: info
# 用户配置
user:
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/config/OcrExecutorConfig.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/config/OcrExecutorConfig.java
index 749a2d2..97b31e4 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/config/OcrExecutorConfig.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/config/OcrExecutorConfig.java
@@ -27,4 +27,15 @@ public class OcrExecutorConfig
{
return Executors.newFixedThreadPool(16);
}
+
+ /**
+ * 费用汇总执行器 (FeeCalcScheduler 用): 8 线程并行汇总各会议.
+ *
+ * 汇总本身纯 SUM 幂等, 无锁; 线程数 8 足够 (每会议 3 次轻量查询 + 1 次回写).
+ */
+ @Bean(name = "feeCalcExecutor", destroyMethod = "shutdown")
+ public ExecutorService feeCalcExecutor()
+ {
+ return Executors.newFixedThreadPool(8);
+ }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java
index bf04930..bcbd07e 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java
@@ -36,9 +36,17 @@ public class BizDashboardController extends BaseController {
map.put("totalProjects", projects.size());
map.put("totalMeetings", meetings.size());
map.put("totalExperts", experts.size());
- map.put("todoMeetings", meetings.stream().filter(m -> "未执行".equals(m.getCurrentStage())).count());
- map.put("doingMeetings", meetings.stream().filter(m -> "待监管".equals(m.getCurrentStage()) || "待整改".equals(m.getCurrentStage())).count());
- map.put("doneMeetings", meetings.stream().filter(m -> "已结算".equals(m.getCurrentStage()) || "已结题".equals(m.getCurrentStage())).count());
+ // current_stage 是 10 值物理阶段 code (BizMeetingStageEnum), 不是中文 label (旧代码比对中文永远为 0).
+ map.put("todoMeetings", meetings.stream().filter(m -> "NOT_STARTED".equals(m.getCurrentStage())).count());
+ map.put("doingMeetings", meetings.stream().filter(m -> {
+ String s = m.getCurrentStage();
+ return "RUNNING".equals(s) || "AWAITING_COMPLIANCE".equals(s)
+ || "AWAITING_SUPERVISION".equals(s) || "RECTIFYING".equals(s);
+ }).count());
+ map.put("doneMeetings", meetings.stream().filter(m -> {
+ String s = m.getCurrentStage();
+ return "SETTLED".equals(s) || "FINISHED".equals(s);
+ }).count());
return success(map);
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java
index ca014f9..8f95910 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java
@@ -1,8 +1,7 @@
package com.ruoyi.business.controller;
-import java.util.HashSet;
+import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -48,7 +47,7 @@ public class BizMeetingAttendeeController extends BaseController {
private BizNotifyService bizNotifyService;
/**
- * 当前登录用户的"待签署"列表 (handsign 或 labor_protocol 任一为空)
+ * 当前登录用户的"待签署协议"列表 (已推送电子签 is_esigned=1 且 任一未签)
* 用于 /doctor/home 工作台
*/
@GetMapping("/unsigned")
@@ -58,6 +57,17 @@ public class BizMeetingAttendeeController extends BaseController {
return success(rows);
}
+ /**
+ * 当前登录用户的"待参加"会议列表 (已邀请参会 is_invited=1)
+ * 用于 /doctor/home 工作台
+ */
+ @GetMapping("/invited")
+ public AjaxResult listInvited() {
+ Long userId = SecurityUtils.getUserId();
+ List rows = attendeeService.selectInvitedByUserId(userId);
+ return success(rows);
+ }
+
/**
* 管理端: 某会议的全部参会人 (MeetingDetail 参会人 CRUD 用).
* 返回全字段, 前端按需展示.
@@ -71,7 +81,7 @@ public class BizMeetingAttendeeController extends BaseController {
* 管理端: 按手机号新增参会人.
*
* 后端流程: 按 body.phone 查 sys_user → 查到用之, 查不到新建 (用户名=密码=phone, role_type='doctor')
- * → 写完整档案行 (含 name/work_unit/fee...) → 触发 #5 会议邀请通知.
+ * → 写完整档案行 (含 name/work_unit/fee...). 邀请参会已改为手动, 此处不再自动推.
*
*
请求体示例:
*
@@ -84,11 +94,9 @@ public class BizMeetingAttendeeController extends BaseController {
@PostMapping
public AjaxResult add(@RequestBody BizMeetingAttendee body) {
Long attendeeId = attendeeService.insertByPhoneWithProfile(body);
- // #5 触发: 新参会人发邀请 (走 BizMeetingController.edit 同一路径, 不需要 dedup — 这是新行)
- BizMeeting m = bizMeetingService.getById(body.getMeetingId());
- bizNotifyService.meetingInvitation(body.getUserId(), body.getMeetingId(),
- m != null ? m.getMeetingName() : null,
- m != null ? m.getStartTime() : null);
+ // 人员变化 → 会议费用待重算
+ bizMeetingService.markFeeCalcPending(body.getMeetingId());
+ // 邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发
return success(attendeeId);
}
@@ -102,8 +110,14 @@ public class BizMeetingAttendeeController extends BaseController {
if (body.getId() == null) {
return error("id 不能为空");
}
+ BizMeetingAttendee before = attendeeService.selectById(body.getId());
body.setUpdateBy(SecurityUtils.getUsername());
- return toAjax(attendeeService.updateProfile(body));
+ int rows = attendeeService.updateProfile(body);
+ // 人员变化 → 会议费用待重算
+ if (before != null) {
+ bizMeetingService.markFeeCalcPending(before.getMeetingId());
+ }
+ return toAjax(rows);
}
/**
@@ -113,7 +127,33 @@ public class BizMeetingAttendeeController extends BaseController {
@Log(title = "参会人", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public AjaxResult remove(@PathVariable("id") Long id) {
- return toAjax(attendeeService.deleteByPrimaryKey(id));
+ BizMeetingAttendee before = attendeeService.selectById(id);
+ int rows = attendeeService.deleteByPrimaryKey(id);
+ // 人员变化 → 会议费用待重算
+ if (before != null) {
+ bizMeetingService.markFeeCalcPending(before.getMeetingId());
+ }
+ return toAjax(rows);
+ }
+
+ /**
+ * 推送电子签: body = attendee.id 数组 [1,2,3] (批量=勾选后传选中 id, 每行=传 [row.id]).
+ * 逐个发短信 + 推站内信 + 置 is_esigned=1, 返回成功条数.
+ */
+ @Log(title = "推送电子签", businessType = BusinessType.UPDATE)
+ @PostMapping("/esign")
+ public AjaxResult esign(@RequestBody List attendeeIds) {
+ return success(attendeeService.pushEsign(attendeeIds));
+ }
+
+ /**
+ * 邀请参会: body = attendee.id 数组 [1,2,3] (批量=勾选后传选中 id, 每行=传 [row.id]).
+ * 逐个推"会议邀请"站内信 (不发短信) + 置 is_invited=1, 返回成功条数.
+ */
+ @Log(title = "邀请参会", businessType = BusinessType.UPDATE)
+ @PostMapping("/invite")
+ public AjaxResult invite(@RequestBody List attendeeIds) {
+ return success(attendeeService.invite(attendeeIds));
}
/** 更新手写签名 (Base64 字符串, 直接存 DB longtext) */
@@ -139,14 +179,10 @@ public class BizMeetingAttendeeController extends BaseController {
}
/**
- * 批量导入参会人 — 上传 Excel + 解析入库 + #5 会议邀请差集推送.
+ * 批量导入参会人 — 上传 Excel + 解析入库.
*
- * 三步:
- *
- * - 查"导入前"该会议已存在的参会人 userIds (Set)
- * - 调 {@link IBizMeetingAttendeeService#importFromExcel} 逐行处理, 失败的进 ngList
- * - 查"导入后"该会议 userIds, 与"前"做差集 → 仅给"新加入"的 userId 推 #5 会议邀请
- *
+ * 调 {@link IBizMeetingAttendeeService#importFromExcel} 逐行处理, 失败的进 ngList.
+ * 邀请参会已改为手动, 此处不再自动推"会议邀请".
*
*
返回 ImportResult { okNum, ngNum, ngList: [{rowNum, message}] }, 前端 ImportResultDialog 直接渲染.
*
@@ -156,27 +192,50 @@ public class BizMeetingAttendeeController extends BaseController {
@PostMapping("/importData")
public AjaxResult importData(@RequestParam("file") MultipartFile file,
@RequestParam("meetingId") Long meetingId) throws Exception {
- // 1. 导入前快照
- Set preUserIds = new HashSet<>(attendeeService.selectUserIdsByMeetingId(meetingId));
-
- // 2. 解析 + 入库
+ // 解析 + 入库 (邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发)
ImportResult result = attendeeService.importFromExcel(file, meetingId, SecurityUtils.getUsername());
-
- // 3. 差集 → 仅对"新加入" userId 推 #5 邀请
- Set postUserIds = new HashSet<>(attendeeService.selectUserIdsByMeetingId(meetingId));
- postUserIds.removeAll(preUserIds);
- if (!postUserIds.isEmpty()) {
- BizMeeting m = bizMeetingService.getById(meetingId);
- String meetingName = m != null ? m.getMeetingName() : null;
- java.util.Date startTime = m != null ? m.getStartTime() : null;
- for (Long uid : postUserIds) {
- bizNotifyService.meetingInvitation(uid, meetingId, meetingName, startTime);
- }
+ // 人员变化 → 会议费用待重算 (有成功导入才需重算, 但幂等, 直接标记)
+ if (result != null && result.getOkNum() > 0) {
+ bizMeetingService.markFeeCalcPending(meetingId);
}
-
return success(result);
}
+ /**
+ * 导出某会议的参会人档案 (Excel, 复用导入 VO 的 @Excel 列头, 含账户名称/开户行地址/银行详细地址/身份证附件).
+ */
+ @Log(title = "参会人导出", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, @RequestParam("meetingId") Long meetingId) {
+ List list = attendeeService.selectByMeetingId(meetingId);
+ List exportList = new ArrayList<>(list.size());
+ for (BizMeetingAttendee a : list) {
+ BizMeetingAttendeeImportVo v = new BizMeetingAttendeeImportVo();
+ v.setName(a.getName());
+ v.setPhone(a.getPhone());
+ v.setWorkUnit(a.getWorkUnit());
+ v.setDepartment(a.getDepartment());
+ v.setTitle(a.getTitle());
+ v.setIdCard(a.getIdCard());
+ v.setBankName(a.getBankName());
+ v.setBankCard(a.getBankCard());
+ v.setBankBranch(a.getBankBranch());
+ v.setAccountName(a.getAccountName());
+ v.setBankRegion(a.getBankRegion());
+ v.setBankAddress(a.getBankAddress());
+ v.setIdCardAttachments(a.getIdCardAttachments());
+ v.setLaborForm(a.getLaborForm());
+ v.setFeePreTax(a.getFeePreTax());
+ v.setTax(a.getTax());
+ v.setVatAndSurcharge(a.getVatAndSurcharge());
+ v.setFee(a.getFee());
+ v.setSummary(a.getSummary());
+ exportList.add(v);
+ }
+ ExcelUtil util = new ExcelUtil<>(BizMeetingAttendeeImportVo.class);
+ util.exportExcel(response, exportList, "参会人");
+ }
+
/**
* 更新劳务协议 URL (OSS 上传后调本接口).
*
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 b5a48f9..04df21b 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
@@ -2,6 +2,7 @@ package com.ruoyi.business.controller;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import com.ruoyi.business.domain.BizMeetingMaterial;
import com.ruoyi.business.domain.BizMeetingAuditLog;
import com.ruoyi.business.domain.BizMeetingSupervisor;
@@ -11,6 +12,7 @@ import com.ruoyi.business.service.IBizMeetingAuditLogService;
import com.ruoyi.business.service.IBizMeetingSupervisorService;
import com.ruoyi.business.service.IBizMeetingExecutorService;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
@@ -20,9 +22,15 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.business.domain.BizMeeting;
+import com.ruoyi.business.domain.BizProject;
import com.ruoyi.business.notify.BizNotifyService;
import com.ruoyi.business.service.IBizMeetingService;
+import com.ruoyi.business.service.IBizProjectService;
import com.ruoyi.business.service.IBizMeetingAttendeeService;
+import com.ruoyi.business.service.StageDeriver;
+import com.ruoyi.business.service.PosterService;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.system.mapper.SysUserMapper;
/**
* 会议Controller
@@ -45,13 +53,42 @@ public class BizMeetingController extends BaseController {
private IBizMeetingExecutorService bizMeetingExecutorService;
@Autowired
private BizNotifyService bizNotifyService;
+ @Autowired
+ private SysUserMapper sysUserMapper;
+ @Autowired
+ private IBizProjectService bizProjectService;
+ @Autowired
+ private StageDeriver stageDeriver;
+ @Autowired
+ private PosterService posterService;
@GetMapping("/list")
public TableDataInfo list(BizMeeting bizMeeting) {
- // 医生/专家角色: 后端兜底只查"我参加的会议" (走 biz_meeting_attendee 中间表)
+ Long uid = SecurityUtils.getUserId();
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
+ // 医生/专家角色: 后端兜底只查"我参加的会议" (走 biz_meeting_attendee 中间表)
if ("doctor".equals(roleType) || "expert".equals(roleType)) {
- bizMeeting.setUserId(SecurityUtils.getUserId());
+ bizMeeting.setUserId(uid);
+ }
+ // sponsor 数据权限: 只看"我的项目"下的会议 (MAIN 走 sponsor_admin_user_id, SUB 走 sponsor_assign.monitor_user_id).
+ // 与项目列表 selectSponsorList 的 MAIN/SUB 判定平行, 但刻意不带 biz_publicity_support_intent 关联.
+ else if ("sponsor".equals(roleType)) {
+ SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
+ if (current != null && "SUB".equals(current.getAccountType())) {
+ bizMeeting.getParams().put("monitorUserId", uid);
+ } else {
+ bizMeeting.getParams().put("sponsorAdminUserId", uid);
+ }
+ }
+ // executor 数据权限: 只看"我的项目"下的会议 (与项目列表 selectExecutorList/selectExecutorStaffList 同源).
+ // MAIN 走 biz_project_assign (exec_user_id / execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id.
+ 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);
+ } else {
+ bizMeeting.getParams().put("executorUserId", uid);
+ }
}
startPage();
List list = bizMeetingService.selectList(bizMeeting);
@@ -66,17 +103,37 @@ public class BizMeetingController extends BaseController {
@Log(title = "会议", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BizMeeting bizMeeting) {
+ // executor 建会限额: 执行机构人员 (MAIN/SUB 只要能看到项目) 都可建会, 但该项目的会议数不得超过分配给本公司的场次.
+ // 场次是公司维度: SUB 执行人反查主账号 parent_user_id 聚合 (与项目列表 assigned_sessions 口径一致).
+ // 注意: 会议数按"该项目下全部未软删会议"计数 (biz_meeting 无执行方归属列, 无法区分是哪个执行方建的) — 见 memory [[ry-executor-staff-project-visibility]].
+ String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
+ if ("executor".equals(roleType)) {
+ Long uid = SecurityUtils.getUserId();
+ Long projectId = bizMeeting.getProjectId();
+ if (projectId == null) {
+ throw new ServiceException("建会必须指定 projectId");
+ }
+ Long aggUid = uid;
+ SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
+ if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) {
+ aggUid = current.getParentUserId();
+ }
+ int assigned = bizProjectService.countAssignedSessions(projectId, aggUid);
+ int existing = bizMeetingService.countByProjectId(projectId);
+ if (existing >= assigned) {
+ throw new ServiceException("本项目分配给本公司的场次为 " + assigned + " 场, 已建 " + existing + " 场, 已达上限");
+ }
+ }
+ // 创建人/时间: DB 列无默认值, 需代码显式落库 (否则详情页 createBy/createTime 为空)
+ bizMeeting.setCreateBy(SecurityUtils.getUsername());
+ bizMeeting.setCreateTime(new Date());
+ bizMeeting.setUpdateBy(SecurityUtils.getUsername());
+ bizMeeting.setUpdateTime(new Date());
int rows = bizMeetingService.insert(bizMeeting);
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
attendeeService.insertBatch(bizMeeting.getMeetingId(), attendeeUserIds);
- // #5 会议邀请通知 (新增会议, 全部新参会人都要通知)
- String meetingName = bizMeeting.getMeetingName();
- Date startTime = bizMeeting.getStartTime();
- for (Long uid : attendeeUserIds) {
- if (uid == null) continue;
- bizNotifyService.meetingInvitation(uid, bizMeeting.getMeetingId(), meetingName, startTime);
- }
+ // 邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发
}
return toAjax(rows);
}
@@ -84,23 +141,32 @@ public class BizMeetingController extends BaseController {
@Log(title = "会议", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody BizMeeting bizMeeting) {
+ bizMeeting.setUpdateBy(SecurityUtils.getUsername());
+ bizMeeting.setUpdateTime(new Date());
int rows = bizMeetingService.updateByPrimaryKey(bizMeeting);
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
Long meetingId = bizMeeting.getMeetingId();
- // #5 dedup: 先拿已有的 userId 集合, 仅给"新增"的 userId 发通知, 避免重复打扰已参会医生
- java.util.Set existingUids = new java.util.HashSet<>(attendeeService.selectUserIdsByMeetingId(meetingId));
attendeeService.insertBatch(meetingId, attendeeUserIds);
- String meetingName = bizMeeting.getMeetingName();
- Date startTime = bizMeeting.getStartTime();
- for (Long uid : attendeeUserIds) {
- if (uid == null || existingUids.contains(uid)) continue;
- bizNotifyService.meetingInvitation(uid, meetingId, meetingName, startTime);
- }
+ // 邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发
}
return toAjax(rows);
}
+ /**
+ * 生成海报: 下载日程海报 (width=1200) → Java2D 叠加会议信息 → 上传 OSS → 回写 poster_url.
+ * 返回生成海报的 OSS URL.
+ */
+ @Log(title = "生成海报", businessType = BusinessType.UPDATE)
+ @PostMapping("/{meetingId}/generate-poster")
+ public AjaxResult generatePoster(@PathVariable("meetingId") Long meetingId,
+ @RequestBody(required = false) Map body) {
+ boolean addText = body != null && Boolean.TRUE.equals(body.get("addText"));
+ String textColor = body != null && body.get("textColor") != null ? body.get("textColor").toString() : "#FFFFFF";
+ String url = posterService.generatePoster(meetingId, addText, textColor);
+ return AjaxResult.success("海报生成成功", url);
+ }
+
/**
* 软删除会议 (admin/manager 会议管理用, 后端强校验 role_type)
* 级联置 biz_meeting + 5 张子表 is_deleted=1, 数据保留审计追溯
@@ -117,17 +183,18 @@ public class BizMeetingController extends BaseController {
}
// ===================================================================
- // 审核流程端点 (5 个)
+ // 审核流程端点 (事实模型)
// ===================================================================
/**
* 执行人员提交材料
*
- * - 校验 1: 当前用户是该会议执行人员 (强校验)
- * - 校验 2: material_audit_stage = INIT
- * - 校验 3: biz_meeting_material 至少 1 条 L_* + 至少 1 条 M_*
+ * - 校验 1: 当前用户是该会议执行方 (项目级归属, 强校验)
+ * - 校验 2: 已执行 (is_executed=1) 且未冻结
+ * - 校验 3: material_audit_stage ∈ {NOT_SUBMITTED, REJECTED}
+ * - 校验 4: biz_meeting_material 劳务(L_*)与会务(M_*)各至少 1 条, 不必全部子类型填满
*
- * 通过后 material_audit_stage INIT → SUBMITTED, 记 audit_log.
+ * 通过后 material → SUBMITTED (compliance_approved=0), 记 audit_log.
*/
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
@PostMapping("/{meetingId}/submit-material")
@@ -136,24 +203,29 @@ public class BizMeetingController extends BaseController {
if (m == null) throw new ServiceException("会议不存在");
Long userId = SecurityUtils.getUserId();
- boolean isExec = bizMeetingExecutorService.selectByMeetingId(meetingId).stream()
- .anyMatch(e -> userId.equals(e.getUserId()));
- if (!isExec) throw new ServiceException("您不是该会议执行人员, 无法提交材料");
-
- if (!"INIT".equals(m.getMaterialAuditStage())) {
- throw new ServiceException("当前阶段 (" + m.getMaterialAuditStage() + ") 不允许提交材料");
+ if (!bizProjectService.isExecutorOfProject(m.getProjectId(), userId)) {
+ throw new ServiceException("您不是该项目的执行方, 无法提交材料");
+ }
+ 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 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("请同时上传劳务材料和会务材料");
+ throw new ServiceException("劳务材料和会务材料各至少上传一条");
}
m.setMaterialAuditStage("SUBMITTED");
+ m.setMaterialComplianceApproved(0);
+ m.setMaterialAuditTime(new Date());
+ m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
bizMeetingService.updateByPrimaryKey(m);
- appendAuditLog(meetingId, "MATERIAL", "SUBMITTED", "APPROVED", "执行人员提交材料");
+ appendAuditLog(m, "MATERIAL", "SUBMITTED", "执行人员提交材料");
return success("SUBMITTED");
}
@@ -167,12 +239,14 @@ public class BizMeetingController extends BaseController {
if (m == null) throw new ServiceException("会议不存在");
Long userId = SecurityUtils.getUserId();
- boolean isExec = bizMeetingExecutorService.selectByMeetingId(meetingId).stream()
- .anyMatch(e -> userId.equals(e.getUserId()));
- if (!isExec) throw new ServiceException("您不是该会议执行人员, 无法提交凭证");
-
- if (!"INIT".equals(m.getVoucherAuditStage())) {
- throw new ServiceException("当前阶段 (" + m.getVoucherAuditStage() + ") 不允许提交凭证");
+ if (!bizProjectService.isExecutorOfProject(m.getProjectId(), userId)) {
+ throw new ServiceException("您不是该项目的执行方, 无法提交凭证");
+ }
+ if (!isExecuted(m)) throw new ServiceException("会议尚未执行, 不能提交凭证");
+ if (isFrozen(m)) throw new ServiceException("会议已冻结, 不能提交凭证");
+ String stage = m.getVoucherAuditStage();
+ if (!"NOT_SUBMITTED".equals(stage) && !"REJECTED".equals(stage)) {
+ throw new ServiceException("当前阶段 (" + stage + ") 不允许提交凭证");
}
List mats = bizMeetingMaterialService.selectByMeetingId(meetingId);
@@ -183,14 +257,19 @@ public class BizMeetingController extends BaseController {
}
m.setVoucherAuditStage("SUBMITTED");
+ m.setVoucherComplianceApproved(0);
+ m.setVoucherAuditTime(new Date());
bizMeetingService.updateByPrimaryKey(m);
- appendAuditLog(meetingId, "VOUCHER", "SUBMITTED", "APPROVED", "执行人员提交凭证");
+ appendAuditLog(m, "VOUCHER", "SUBMITTED", "执行人员提交凭证");
return success("SUBMITTED");
}
/**
- * 合规审核 (role_type=manager)
- * body: { "auditType": "MATERIAL"|"VOUCHER", "approved": true|false, "opinion": "..." }
+ * 合规审核 (role_type=manager), 两级审核中的第一级.
+ *
+ * body: { "auditType": "MATERIAL"|"VOUCHER"|"BOTH", "approved": true|false, "opinion": "..." }
+ *
合规审中判据: stage=SUBMITTED 且 compliance_approved=0.
+ * 通过 → compliance_approved=1 (转入支持方审), 拒绝 → REJECTED (退回执行方).
*/
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
@PostMapping("/{meetingId}/audit-compliance")
@@ -201,67 +280,149 @@ public class BizMeetingController extends BaseController {
BizMeeting m = bizMeetingService.getById(meetingId);
if (m == null) throw new ServiceException("会议不存在");
- String auditType = body.getAuditType();
- if (!"MATERIAL".equals(auditType) && !"VOUCHER".equals(auditType)) {
- throw new ServiceException("auditType 必须是 MATERIAL 或 VOUCHER");
- }
- String currentStage = "MATERIAL".equals(auditType) ? m.getMaterialAuditStage() : m.getVoucherAuditStage();
- if (!"SUBMITTED".equals(currentStage)) {
- throw new ServiceException("当前阶段 (" + currentStage + ") 不允许合规审核");
- }
- if (Boolean.FALSE.equals(body.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
+ String[] types = resolveTypes(body.getAuditType());
+ boolean approved = Boolean.TRUE.equals(body.getApproved());
+ if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
throw new ServiceException("拒绝时意见不能为空");
}
- String result = Boolean.TRUE.equals(body.getApproved()) ? "APPROVED" : "REJECTED";
- String newStage = Boolean.TRUE.equals(body.getApproved()) ? "COMPLIANCE_APPROVED" : "SUBMITTED";
- if ("MATERIAL".equals(auditType)) {
- m.setMaterialAuditStage(newStage);
- } else {
- m.setVoucherAuditStage(newStage);
+ String result = approved ? "APPROVED" : "REJECTED";
+ for (String type : types) {
+ String stage = stageOf(m, type);
+ boolean complianceDone = complianceApprovedOf(m, type);
+ if (!"SUBMITTED".equals(stage) || complianceDone) {
+ throw new ServiceException(type + " 当前阶段不允许合规审核");
+ }
+ if (approved) {
+ setComplianceApproved(m, type, 1);
+ } else {
+ setStage(m, type, "REJECTED");
+ }
+ setAuditTime(m, type, new Date());
}
+ m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
bizMeetingService.updateByPrimaryKey(m);
- appendAuditLog(meetingId, auditType, newStage, result, body.getOpinion());
- return success(newStage);
+ for (String type : types) {
+ appendAuditLog(m, type, result, body.getOpinion());
+ }
+ return success(result);
}
/**
- * 监察审核 (强校验: 当前用户必须是该会议监察员)
- * body: { "auditType": "MATERIAL"|"VOUCHER", "approved": true|false, "opinion": "..." }
+ * 支持方(监察员) 审核, 两级审核中的第二级.
+ *
+ * body: { "auditType": "MATERIAL"|"VOUCHER"|"BOTH", "approved": true|false, "opinion": "..." }
+ *
支持方审中判据: stage=SUBMITTED 且 compliance_approved=1.
+ * 通过 → APPROVED (材料通过时一并写监管意见), 拒绝 → REJECTED (退回执行方).
*/
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
@PostMapping("/{meetingId}/audit-supervision")
public AjaxResult auditSupervision(@PathVariable("meetingId") Long meetingId, @RequestBody AuditBody body) {
Long userId = SecurityUtils.getUserId();
- boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream()
- .anyMatch(s -> userId.equals(s.getUserId()));
- if (!isSupervisor) throw new ServiceException("您不是该会议监察员, 无权监察");
BizMeeting m = bizMeetingService.getById(meetingId);
if (m == null) throw new ServiceException("会议不存在");
- String auditType = body.getAuditType();
- if (!"MATERIAL".equals(auditType) && !"VOUCHER".equals(auditType)) {
- throw new ServiceException("auditType 必须是 MATERIAL 或 VOUCHER");
+ // 授权: 监察员 (biz_meeting_supervisor) 或 支持方 MAIN 账号 (biz_project.sponsor_admin_user_id) 均可审
+ boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream()
+ .anyMatch(s -> userId.equals(s.getUserId()));
+ BizProject project = m.getProjectId() == null ? null : bizProjectService.getById(m.getProjectId());
+ boolean isSponsorMain = project != null && userId.equals(project.getSponsorAdminUserId());
+ if (!isSupervisor && !isSponsorMain) {
+ throw new ServiceException("您不是该会议监察员, 无权监察");
}
- String currentStage = "MATERIAL".equals(auditType) ? m.getMaterialAuditStage() : m.getVoucherAuditStage();
- if (!"COMPLIANCE_APPROVED".equals(currentStage)) {
- throw new ServiceException("当前阶段 (" + currentStage + ") 不允许监察");
- }
- if (Boolean.FALSE.equals(body.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
+
+ String[] types = resolveTypes(body.getAuditType());
+ boolean approved = Boolean.TRUE.equals(body.getApproved());
+ if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
throw new ServiceException("拒绝时意见不能为空");
}
- String result = Boolean.TRUE.equals(body.getApproved()) ? "APPROVED" : "REJECTED";
- String newStage = Boolean.TRUE.equals(body.getApproved()) ? "APPROVED" : "SUBMITTED";
- if ("MATERIAL".equals(auditType)) {
- m.setMaterialAuditStage(newStage);
- } else {
- m.setVoucherAuditStage(newStage);
+ String result = approved ? "APPROVED" : "REJECTED";
+ for (String type : types) {
+ String stage = stageOf(m, type);
+ boolean complianceDone = complianceApprovedOf(m, type);
+ if (!"SUBMITTED".equals(stage) || !complianceDone) {
+ throw new ServiceException(type + " 当前阶段不允许监察审核");
+ }
+ setStage(m, type, approved ? "APPROVED" : "REJECTED");
+ setAuditTime(m, type, new Date());
}
+ // 材料通过 → 写监管意见 (支持方的书面意见)
+ if (approved && java.util.Arrays.asList(types).contains("MATERIAL")) {
+ m.setSupervisionOpinion(body.getOpinion());
+ m.setSupervisionBy(SecurityUtils.getUsername());
+ m.setSupervisionTime(new Date());
+ }
+ m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
bizMeetingService.updateByPrimaryKey(m);
- appendAuditLog(meetingId, auditType, newStage, result, body.getOpinion());
- return success(newStage);
+ for (String type : types) {
+ appendAuditLog(m, type, result, body.getOpinion());
+ }
+
+ // 退回 → 通知执行方 (待整改 + 说明)
+ if (!approved) {
+ for (BizMeetingExecutor e : bizMeetingExecutorService.selectByMeetingId(meetingId)) {
+ bizNotifyService.meetingSupervisionRejected(e.getUserId(), meetingId, m.getMeetingName(), body.getOpinion());
+ }
+ }
+ return success(result);
+ }
+
+ /**
+ * 结算 (合规/管理员 手动点击). 前置: material+voucher 都 APPROVED.
+ */
+ @Log(title = "会议审核", businessType = BusinessType.UPDATE)
+ @PostMapping("/{meetingId}/settle")
+ @Transactional(rollbackFor = Exception.class)
+ public AjaxResult settle(@PathVariable("meetingId") Long meetingId) {
+ String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
+ if (!"manager".equals(roleType) && !"admin".equals(roleType)) {
+ throw new ServiceException("只有合规或管理员可结算");
+ }
+ BizMeeting m = bizMeetingService.getById(meetingId);
+ if (m == null) throw new ServiceException("会议不存在");
+ if (!"APPROVED".equals(m.getMaterialAuditStage()) || !"APPROVED".equals(m.getVoucherAuditStage())) {
+ throw new ServiceException("材料与凭证均审核通过后才能结算");
+ }
+ if (isSettled(m)) throw new ServiceException("会议已结算");
+ // 费用未汇总完 (fee_calc_status=0) 禁止结算: 此时 labor_fee/meeting_fee 可能为旧值/0, 直接回写会污染项目金额.
+ // 无发票的材料 fee_status 恒为 1, 调度器只会因"存在待 OCR 发票"而停在 0, 故无发票的会议天然不会被此校验误挡.
+ if (m.getFeeCalcStatus() == null || m.getFeeCalcStatus() != 1) {
+ throw new ServiceException("会议费用尚未汇总完成,无法结算");
+ }
+ m.setIsSettled(1);
+ m.setSettleTime(new Date());
+ m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
+ bizMeetingService.updateByPrimaryKey(m);
+ // 结算成功 → 触发项目金额重算 (全量 SUM 已结算会议, 幂等)
+ if (m.getProjectId() != null) {
+ bizProjectService.recomputeSettledAmounts(m.getProjectId());
+ }
+ appendAuditLog(m, "SETTLE", "APPROVED", "会议结算");
+ return success("SETTLED");
+ }
+
+ /**
+ * 完结 (合规/管理员 手动点击). 前置: 已结算 (is_settled=1).
+ */
+ @Log(title = "会议审核", businessType = BusinessType.UPDATE)
+ @PostMapping("/{meetingId}/finish")
+ public AjaxResult finish(@PathVariable("meetingId") Long meetingId) {
+ String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
+ if (!"manager".equals(roleType) && !"admin".equals(roleType)) {
+ throw new ServiceException("只有合规或管理员可完结");
+ }
+ BizMeeting m = bizMeetingService.getById(meetingId);
+ if (m == null) throw new ServiceException("会议不存在");
+ if (!isSettled(m)) throw new ServiceException("会议尚未结算, 不能完结");
+ if (isFinished(m)) throw new ServiceException("会议已完结");
+ m.setIsFinished(1);
+ m.setFinishTime(new Date());
+ m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
+ bizMeetingService.updateByPrimaryKey(m);
+ appendAuditLog(m, "FINISH", "APPROVED", "会议完结");
+ return success("FINISHED");
}
/**
@@ -275,25 +436,65 @@ public class BizMeetingController extends BaseController {
return success(list);
}
+ // ===================================================================
+ // 事实字段辅助 (null 安全)
+ // ===================================================================
+
+ private static boolean isExecuted(BizMeeting m) { return m.getIsExecuted() != null && m.getIsExecuted() == 1; }
+ private static boolean isFrozen(BizMeeting m) { return m.getIsFrozen() != null && m.getIsFrozen() == 1; }
+ private static boolean isSettled(BizMeeting m) { return m.getIsSettled() != null && m.getIsSettled() == 1; }
+ private static boolean isFinished(BizMeeting m) { return m.getIsFinished() != null && m.getIsFinished() == 1; }
+
+ /** 材料/凭证 子状态读取 */
+ private static String stageOf(BizMeeting m, String type) {
+ return "MATERIAL".equals(type) ? m.getMaterialAuditStage() : m.getVoucherAuditStage();
+ }
+ private static boolean complianceApprovedOf(BizMeeting m, String type) {
+ Integer v = "MATERIAL".equals(type) ? m.getMaterialComplianceApproved() : m.getVoucherComplianceApproved();
+ return v != null && v == 1;
+ }
+ private static void setStage(BizMeeting m, String type, String stage) {
+ if ("MATERIAL".equals(type)) m.setMaterialAuditStage(stage);
+ else m.setVoucherAuditStage(stage);
+ }
+ private static void setComplianceApproved(BizMeeting m, String type, int v) {
+ if ("MATERIAL".equals(type)) m.setMaterialComplianceApproved(v);
+ else m.setVoucherComplianceApproved(v);
+ }
+ private static void setAuditTime(BizMeeting m, String type, Date t) {
+ if ("MATERIAL".equals(type)) m.setMaterialAuditTime(t);
+ else m.setVoucherAuditTime(t);
+ }
+
+ /** auditType: MATERIAL / VOUCHER / BOTH → 处理类型数组 */
+ private static String[] resolveTypes(String auditType) {
+ if ("BOTH".equals(auditType)) return new String[] { "MATERIAL", "VOUCHER" };
+ if ("MATERIAL".equals(auditType) || "VOUCHER".equals(auditType)) return new String[] { auditType };
+ throw new ServiceException("auditType 必须是 MATERIAL / VOUCHER / BOTH");
+ }
+
/**
- * 内部: 写一条 audit_log
+ * 内部: 写一条 audit_log (4 列角色展示状态由 post-transition 事实推导).
*/
- private void appendAuditLog(Long meetingId, String auditType, String stage, String result, String opinion) {
+ private void appendAuditLog(BizMeeting m, String auditType, String result, String opinion) {
BizMeetingAuditLog log = new BizMeetingAuditLog();
- log.setMeetingId(meetingId);
+ log.setMeetingId(m.getMeetingId());
log.setAuditor(SecurityUtils.getUsername());
log.setAuditType(auditType);
- log.setCurrentStage(stage);
log.setAuditResult(result);
log.setOpinion(opinion);
log.setCreateTime(new Date());
log.setAuditTime(new Date());
+ log.setExecutorStage(stageDeriver.deriveDisplay("executor", m));
+ log.setSponsorStage(stageDeriver.deriveDisplay("sponsor", m));
+ log.setManagerStage(stageDeriver.deriveDisplay("manager", m));
+ log.setAdminStage(stageDeriver.deriveDisplay("admin", m));
bizMeetingAuditLogService.insert(log);
}
/** request body for audit endpoints */
public static class AuditBody {
- private String auditType; // MATERIAL / VOUCHER
+ private String auditType; // MATERIAL / VOUCHER / BOTH
private Boolean approved; // true=通过 false=拒绝
private String opinion; // 意见
public String getAuditType() { return auditType; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingMaterialController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingMaterialController.java
index 7ac0d01..2cd7d1e 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingMaterialController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingMaterialController.java
@@ -10,6 +10,7 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.business.domain.BizMeetingMaterial;
import com.ruoyi.business.service.IBizMeetingMaterialService;
+import com.ruoyi.business.service.IBizMeetingService;
/**
* 会议材料 Controller
@@ -22,6 +23,8 @@ public class BizMeetingMaterialController extends BaseController {
@Autowired
private IBizMeetingMaterialService bizMeetingMaterialService;
+ @Autowired
+ private IBizMeetingService bizMeetingService;
/**
* 查该会议的所有材料记录
@@ -50,6 +53,20 @@ public class BizMeetingMaterialController extends BaseController {
}
}
List saved = bizMeetingMaterialService.replaceByMeetingId(meetingId, list);
+ // 材料变化 → 会议费用待重算 (FeeCalcScheduler 汇总回写)
+ bizMeetingService.markFeeCalcPending(meetingId);
return success(saved);
}
+
+ /**
+ * 扫码拍照回传 (公开端点, ry-h5 手机端拍照直传 OSS 后回传 URL).
+ *
+ * body: { meetingId, subType, ossUrl }. 后端白名单 subType + 会议存在校验.
+ * 照片类 NON_OCR, 不影响会议费用.
+ */
+ @PostMapping("/cameraUpload")
+ public AjaxResult cameraUpload(@RequestBody BizMeetingMaterial body) {
+ bizMeetingMaterialService.upsertFromCamera(body.getMeetingId(), body.getSubType(), body.getOssUrl(), body.getExtraOssUrl());
+ return success();
+ }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java
index 67023a5..0a58291 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java
@@ -23,12 +23,16 @@ import com.ruoyi.business.domain.BizProjectRating;
import com.ruoyi.business.service.IBizProjectService;
import com.ruoyi.business.service.IBizExecutionIntentService;
import com.ruoyi.business.domain.BizProjectSponsorAssign;
+import com.ruoyi.business.domain.BizProjectExecutorAssign;
import com.ruoyi.business.notify.BizNotifyService;
import com.ruoyi.business.service.IBizProjectAssignService;
import com.ruoyi.business.service.IBizProjectRatingService;
import com.ruoyi.business.service.IBizProjectSponsorAssignService;
+import com.ruoyi.business.service.IBizProjectExecutorAssignService;
import com.ruoyi.system.domain.vo.SysUserExtendVo;
import com.ruoyi.business.mapper.BizSysUserQueryMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.system.mapper.SysUserMapper;
/**
* 项目Controller
@@ -53,7 +57,11 @@ public class BizProjectController extends BaseController
@Autowired
private IBizProjectSponsorAssignService bizProjectSponsorAssignService;
@Autowired
+ private IBizProjectExecutorAssignService bizProjectExecutorAssignService;
+ @Autowired
private BizSysUserQueryMapper bizSysUserQueryMapper;
+ @Autowired
+ private SysUserMapper sysUserMapper;
/**
* 我报名的项目 (当前用户在 biz_execution_intent 里有意向的项目)
@@ -116,15 +124,26 @@ public class BizProjectController extends BaseController
}
/**
- * sponsor 专属项目列表 (按当前登录 sponsor 的 user_id 过滤)
+ * sponsor 专属项目列表 (按当前登录 sponsor 的账号类型分两种过滤)
* GET /business/project/sponsorList
- * 注: biz_project.sponsor_admin_user_id 永远是主账号 user_id, 子账号登录也应能看主账号的项目 — 简化: 直接用当前 user_id 过滤
- * 若需要子账号看主账号项目, 改 SQL 改为 (sponsor_admin_user_id = uid OR sponsor_admin_user_id IN (parent_user_id=uid 的子账号所属主账号))
+ *
+ * - MAIN 主账号: 看自己 sponsor_admin_user_id 下的项目 (用 biz_project.sponsor_admin_user_id)
+ * - SUB 子账号: 看自己被分配 (作为监察员 monitor) 的项目 (走 biz_project_sponsor_assign.monitor_user_id)
+ *
+ * 注: 支持方不能创建项目, 这里只看分配结果. 子账号不再回退到 MAIN 路径, 严格隔离.
*/
@GetMapping("/sponsorList")
public TableDataInfo sponsorList(BizProject bizProject)
{
- bizProject.getParams().put("sponsorAdminUserId", SecurityUtils.getUserId());
+ Long uid = SecurityUtils.getUserId();
+ SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
+ if (current != null && "SUB".equals(current.getAccountType())) {
+ // SUB 子账号视角: 走 sponsor_assign.monitor_user_id (监察员身份)
+ bizProject.getParams().put("monitorUserId", uid);
+ } else {
+ // MAIN 主账号视角 (含 admin / 其它兜底): 走 sponsor_admin_user_id
+ bizProject.getParams().put("sponsorAdminUserId", uid);
+ }
startPage();
List list = bizProjectService.selectSponsorList(bizProject);
return getDataTable(list);
@@ -140,7 +159,20 @@ public class BizProjectController extends BaseController
@GetMapping("/executorList")
public TableDataInfo executorList(BizProject bizProject)
{
- bizProject.getParams().put("executorUserId", SecurityUtils.getUserId());
+ Long uid = SecurityUtils.getUserId();
+ SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
+ if (current != null && "SUB".equals(current.getAccountType())) {
+ // 执行人 (SUB 子账号) 视角: 反查 biz_project_executor_assign.staff_user_id, 严格隔离
+ // (不 JOIN biz_project_assign, 不 UNION intent — executor 与 biz_*_intent 完全无关)
+ // 场次/金额列按"本公司"聚合: executorUserId 传 parent_user_id(主账号), 执行人看到的是公司数据而非个人数据
+ bizProject.getParams().put("executorStaffUserId", uid);
+ bizProject.getParams().put("executorUserId", current.getParentUserId());
+ startPage();
+ List list = bizProjectService.selectExecutorStaffList(bizProject);
+ return getDataTable(list);
+ }
+ // 主账号视角 (原逻辑): biz_project_assign.exec_user_id / execution_unit_id
+ bizProject.getParams().put("executorUserId", uid);
startPage();
List list = bizProjectService.selectExecutorList(bizProject);
return getDataTable(list);
@@ -232,6 +264,23 @@ public class BizProjectController extends BaseController
return oa.compareTo(na) == 0;
}
+ /**
+ * 比较两条 sponsor 分配是否对监察员而言"未变".
+ * 判定维度: assignDesc + assignPoints. (projectId 隐含相同, 旧数据就是本 projectId 的)
+ */
+ private static boolean isSponsorAssignUnchanged(BizProjectSponsorAssign old, String assignDesc, String assignPoints) {
+ if (old == null) return false; // 新分配 → 算变化
+ if (!Objects.equals(old.getAssignDesc(), assignDesc)) return false;
+ if (!Objects.equals(old.getAssignPoints(), assignPoints)) return false;
+ return true;
+ }
+
+ /** String projectId → Long (sponsor_assign/executor_assign 表主键是 String, 查 biz_project 主表需 Long) */
+ private static Long parseProjectId(String s) {
+ if (s == null || s.isEmpty()) return null;
+ try { return Long.parseLong(s); } catch (NumberFormatException e) { return null; }
+ }
+
@Log(title = "项目执行方分配", businessType = BusinessType.DELETE)
@DeleteMapping("/{projectId}/assigns")
public AjaxResult clearAssigns(@PathVariable("projectId") Long projectId)
@@ -297,15 +346,101 @@ public class BizProjectController extends BaseController
/**
* 支持方分配监察员 (写 biz_project_sponsor_assign)
* POST /business/project/sponsorAssign
+ * body: { projectId, monitorUserIds: [Long, ...] } — 多选 (前端 sponsor/my-projects 走这条)
+ * 兼容单值 { projectId, monitorUserId: Long } (前端 sponsor/Projects.vue 仍走单值)
*/
@Log(title = "支持方分配监察员", businessType = BusinessType.INSERT)
@PostMapping("/sponsorAssign")
public AjaxResult sponsorAssign(@RequestBody BizProjectSponsorAssign body)
{
+ if (body.getProjectId() == null) {
+ return error("projectId 必填");
+ }
+ java.util.List mids = body.getMonitorUserIds();
+ if (mids == null || mids.isEmpty()) {
+ // 向后兼容: 单值 monitorUserId
+ if (body.getMonitorUserId() != null) {
+ mids = java.util.Collections.singletonList(body.getMonitorUserId());
+ } else {
+ return error("monitorUserIds / monitorUserId 必填");
+ }
+ }
+ // 通知去重: 拉旧监察员列表按 monitorUserId 索引, 同 (assignDesc, assignPoints) → 跳过 (修改不重复发)
+ List oldList = bizProjectSponsorAssignService.listByProjectId(body.getProjectId());
+ Map oldByMonitor = new HashMap<>();
+ if (oldList != null) {
+ for (BizProjectSponsorAssign o : oldList) {
+ if (o.getMonitorUserId() != null) oldByMonitor.put(o.getMonitorUserId(), o);
+ }
+ }
body.setCreateBy(SecurityUtils.getUsername());
body.setSponsorUserId(SecurityUtils.getUserId());
- int rows = bizProjectSponsorAssignService.insertAssign(body);
- return toAjax(rows);
+ // 一项目支持 N 监察员: service 内一次性 delete + 逐个 insert, 不会循环 delete
+ int inserted = bizProjectSponsorAssignService.assignMonitorsForProject(body, mids);
+
+ // 通知被分配的监察员 (新增 / 说明或积分变化才发). projectId 在 sponsor_assign 是 String, 转 Long 查主表
+ Long projectIdLong = parseProjectId(body.getProjectId());
+ BizProject project = projectIdLong != null ? bizProjectService.getById(projectIdLong) : null;
+ String projectName = project != null ? project.getProjectName() : null;
+ for (Long mid : mids) {
+ if (mid == null) continue;
+ BizProjectSponsorAssign old = oldByMonitor.get(mid);
+ if (isSponsorAssignUnchanged(old, body.getAssignDesc(), body.getAssignPoints())) {
+ logger.debug("[sponsorAssign] monitorUserId={} (assignDesc, assignPoints) 未变, 跳过通知", mid);
+ continue;
+ }
+ bizNotifyService.projectAssignedToSponsor(mid, projectIdLong, projectName, body.getAssignDesc(), body.getAssignPoints());
+ }
+ return toAjax(inserted);
+ }
+
+ /**
+ * 查询项目已分配的监察员列表 (供前端 dialog 重开时回显)
+ * GET /business/project/{projectId}/sponsorAssigns
+ */
+ @GetMapping("/{projectId}/sponsorAssigns")
+ public AjaxResult listSponsorAssigns(@PathVariable("projectId") String projectId)
+ {
+ return success(bizProjectSponsorAssignService.listByProjectId(projectId));
+ }
+
+ /**
+ * 执行方分配执行人 (写 biz_project_executor_assign)
+ * POST /business/project/executorAssign
+ * body: { projectId, staffUserIds: [Long, ...] } — 多选 (执行方给自己的项目分配执行人)
+ * 兼容单值 { projectId, staffUserId: Long }
+ */
+ @Log(title = "执行方分配执行人", businessType = BusinessType.INSERT)
+ @PostMapping("/executorAssign")
+ public AjaxResult executorAssign(@RequestBody BizProjectExecutorAssign body)
+ {
+ if (body.getProjectId() == null) {
+ return error("projectId 必填");
+ }
+ java.util.List sids = body.getStaffUserIds();
+ if (sids == null || sids.isEmpty()) {
+ // 向后兼容: 单值 staffUserId
+ if (body.getStaffUserId() != null) {
+ sids = java.util.Collections.singletonList(body.getStaffUserId());
+ } else {
+ return error("staffUserIds / staffUserId 必填");
+ }
+ }
+ body.setCreateBy(SecurityUtils.getUsername());
+ body.setExecutorUserId(SecurityUtils.getUserId());
+ // 一项目支持 N 执行人: service 内一次性 delete + 逐个 insert, 不会循环 delete
+ int inserted = bizProjectExecutorAssignService.assignStaffForProject(body, sids);
+ return toAjax(inserted);
+ }
+
+ /**
+ * 查询项目已分配的执行人列表 (供前端 dialog 重开时回显)
+ * GET /business/project/{projectId}/executorAssigns
+ */
+ @GetMapping("/{projectId}/executorAssigns")
+ public AjaxResult listExecutorAssigns(@PathVariable("projectId") String projectId)
+ {
+ return success(bizProjectExecutorAssignService.listByProjectId(projectId));
}
/**
@@ -349,7 +484,22 @@ public class BizProjectController extends BaseController
}
body.setCreateBy(loginName);
body.setSponsorUserId(loginUid);
+ // 通知去重: 拉旧分配, 找同 monitorUserId, 比较 assignDesc/assignPoints 是否变化
+ BizProjectSponsorAssign old = null;
+ List oldList = bizProjectSponsorAssignService.listByProjectId(body.getProjectId());
+ if (oldList != null) {
+ for (BizProjectSponsorAssign o : oldList) {
+ if (Objects.equals(o.getMonitorUserId(), body.getMonitorUserId())) { old = o; break; }
+ }
+ }
+ boolean changed = !isSponsorAssignUnchanged(old, body.getAssignDesc(), body.getAssignPoints());
bizProjectSponsorAssignService.insertAssign(body);
+ if (changed) {
+ Long pid = parseProjectId(body.getProjectId());
+ BizProject project = pid != null ? bizProjectService.getById(pid) : null;
+ String projectName = project != null ? project.getProjectName() : null;
+ bizNotifyService.projectAssignedToSponsor(body.getMonitorUserId(), pid, projectName, body.getAssignDesc(), body.getAssignPoints());
+ }
ok++;
} catch (Exception e) {
errors.add("第" + (i + 1) + "条 (projectId=" + body.getProjectId() + "): " + e.getMessage());
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSignController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSignController.java
index 64929f4..92508a7 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSignController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSignController.java
@@ -32,6 +32,12 @@ public class BizSignController extends BaseController {
return success(signService.getSignInfo(attendeeId));
}
+ /** 扫码入口: 只有 meetingId (无 attendeeId) 时, 返回 {meetingName, periodNo, totalPeriods, attendeeId} (不在邀请之列 attendeeId=null) */
+ @GetMapping("/resolve")
+ public AjaxResult resolveByMeeting(@RequestParam("meetingId") Long meetingId) {
+ return success(signService.resolveByMeeting(meetingId));
+ }
+
/** 医生点 "保存" / "下一步" 后调 */
@PostMapping("/saveProfile")
public AjaxResult saveProfile(@RequestParam("attendeeId") Long attendeeId,
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportIntentController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportIntentController.java
deleted file mode 100644
index 0512234..0000000
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportIntentController.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.ruoyi.business.controller;
-
-import java.util.List;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
-import com.ruoyi.common.annotation.Log;
-import com.ruoyi.common.core.controller.BaseController;
-import com.ruoyi.common.core.domain.AjaxResult;
-import com.ruoyi.common.core.page.TableDataInfo;
-import com.ruoyi.common.enums.BusinessType;
-import com.ruoyi.business.domain.BizSupportIntent;
-import com.ruoyi.business.service.IBizSupportIntentService;
-
-/**
- * 支持意向Controller
- */
-@RestController
-@RequestMapping("/business/supportIntent")
-public class BizSupportIntentController extends BaseController
-{
- @Autowired
- private IBizSupportIntentService bizSupportIntentService;
- @GetMapping("/list")
- public TableDataInfo list(BizSupportIntent bizSupportIntent)
- {
- startPage();
- List list = bizSupportIntentService.selectList(bizSupportIntent);
- return getDataTable(list);
- }
- @GetMapping("/{intentId}")
- public AjaxResult getInfo(@PathVariable("intentId") String intentId)
- {
- return success(bizSupportIntentService.getById(intentId));
- }
- @Log(title = "支持意向", businessType = BusinessType.INSERT)
- @PostMapping
- public AjaxResult add(@RequestBody BizSupportIntent bizSupportIntent)
- {
- return toAjax(bizSupportIntentService.insert(bizSupportIntent));
- }
- @Log(title = "支持意向", businessType = BusinessType.UPDATE)
- @PutMapping
- public AjaxResult edit(@RequestBody BizSupportIntent bizSupportIntent)
- {
- return toAjax(bizSupportIntentService.updateByPrimaryKey(bizSupportIntent));
- }
- @Log(title = "支持意向", businessType = BusinessType.DELETE)
- @DeleteMapping("/{ids}")
- public AjaxResult remove(@PathVariable String[] ids)
- {
- return toAjax(bizSupportIntentService.deleteByPrimaryKeys(ids));
- }
-}
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 a6c5fae..33c4381 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
@@ -40,7 +40,7 @@ public class BizMeeting extends BaseEntity {
/** period_no */
@Excel(name = "period_no")
private Long periodNo;
- /** current_stage */
+ /** current_stage: 10 值枚举, 见 com.ruoyi.common.enums.BizMeetingStageEnum (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN). 字段类型保持 String 是因为 MyBatis 默认 EnumTypeHandler 需要额外注册, 直接 String + Enum 常量更简单. */
@Excel(name = "current_stage")
private String currentStage;
/** create_by */
@@ -70,20 +70,64 @@ public class BizMeeting extends BaseEntity {
/** 监察时间 (与 DB datetime 对齐) */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date supervisionTime;
- /** 材料审核阶段 (INIT=待提交, 后续阶段开发中定) */
+ /** 材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */
private String materialAuditStage;
- /** 凭证审核阶段 (INIT=待提交, 后续阶段开发中定) */
+ /** 凭证审核阶段 (同上) */
private String voucherAuditStage;
+ /** 是否执行 0否1是 (会议开始时间到, scheduler 置1) */
+ private Integer isExecuted;
+ /** 执行时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date executeTime;
+ /** 是否结算 0否1是 (合规点击结算置1) */
+ private Integer isSettled;
+ /** 结算时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date settleTime;
+ /** 是否完结 0否1是 (合规/管理员点击完结置1) */
+ private Integer isFinished;
+ /** 完结时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date finishTime;
+ /** 是否冻结 0否1是 (逾期未提交, scheduler 置1) */
+ private Integer isFrozen;
+ /** 冻结时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date freezeTime;
+ /** 材料最近一次审核动作时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date materialAuditTime;
+ /** 凭证最近一次审核动作时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date voucherAuditTime;
+ /** 材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */
+ private Integer materialComplianceApproved;
+ /** 凭证合规是否已通过 0否1是 */
+ private Integer voucherComplianceApproved;
/** 邀请函URL */
private String invitationUrl;
/** 日程海报URL */
private String scheduleUrl;
+ /** 生成的海报URL (生成海报按钮产出) */
+ private String posterUrl;
/** 签署劳务 0未签 1已签 */
private String laborSigned;
/** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */
private transient Long userId;
+ /** 当前登录医生/专家在本会议的参会人记录 id (非持久化, mapper 子查询填充; 用于 /doctor/meetings 签署劳务链接) */
+ private transient Long attendeeId;
+ /** 当前登录医生/专家在本会议的已签劳务 PDF URL (非持久化, mapper 子查询填充; null=未签) */
+ private transient String attendeeLaborProtocol;
/** 新增/编辑会议时传入, 自动批量写入 biz_meeting_attendee 中间表 (非持久化) */
private Long[] attendeeUserIds;
+ /** 劳务费用 = 参会人应发金额 (fee_pre_tax) 合计 (后台定时任务汇总回写) */
+ private BigDecimal laborFee;
+ /** 会务费用 = 总发票(M_INVOICE)覆盖 SUB 子类发票金额 (后台定时任务汇总回写) */
+ private BigDecimal meetingFee;
+ /** 总费用 = 劳务费用 + 会务费用 */
+ private BigDecimal totalFee;
+ /** 费用汇总状态 0未汇总 1已汇总 (经费变化置0, 定时任务汇总后置1) */
+ private Integer feeCalcStatus;
/** 软删除标记 0否1是 (admin 删除会议时置1, 查询需 WHERE is_deleted=0) */
private Integer isDeleted;
public Long getMeetingId() { return meetingId; }
@@ -133,16 +177,54 @@ public class BizMeeting extends BaseEntity {
public void setInvitationUrl(String invitationUrl) { this.invitationUrl = invitationUrl; }
public String getScheduleUrl() { return scheduleUrl; }
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
+ public String getPosterUrl() { return posterUrl; }
+ 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 getVoucherAuditStage() { return voucherAuditStage; }
public void setVoucherAuditStage(String voucherAuditStage) { this.voucherAuditStage = voucherAuditStage; }
+ public Integer getIsExecuted() { return isExecuted; }
+ public void setIsExecuted(Integer isExecuted) { this.isExecuted = isExecuted; }
+ public Date getExecuteTime() { return executeTime; }
+ public void setExecuteTime(Date executeTime) { this.executeTime = executeTime; }
+ public Integer getIsSettled() { return isSettled; }
+ public void setIsSettled(Integer isSettled) { this.isSettled = isSettled; }
+ public Date getSettleTime() { return settleTime; }
+ public void setSettleTime(Date settleTime) { this.settleTime = settleTime; }
+ public Integer getIsFinished() { return isFinished; }
+ public void setIsFinished(Integer isFinished) { this.isFinished = isFinished; }
+ public Date getFinishTime() { return finishTime; }
+ public void setFinishTime(Date finishTime) { this.finishTime = finishTime; }
+ public Integer getIsFrozen() { return isFrozen; }
+ public void setIsFrozen(Integer isFrozen) { this.isFrozen = isFrozen; }
+ public Date getFreezeTime() { return freezeTime; }
+ public void setFreezeTime(Date freezeTime) { this.freezeTime = freezeTime; }
+ public Date getMaterialAuditTime() { return materialAuditTime; }
+ public void setMaterialAuditTime(Date materialAuditTime) { this.materialAuditTime = materialAuditTime; }
+ public Date getVoucherAuditTime() { return voucherAuditTime; }
+ public void setVoucherAuditTime(Date voucherAuditTime) { this.voucherAuditTime = voucherAuditTime; }
+ public Integer getMaterialComplianceApproved() { return materialComplianceApproved; }
+ public void setMaterialComplianceApproved(Integer materialComplianceApproved) { this.materialComplianceApproved = materialComplianceApproved; }
+ public Integer getVoucherComplianceApproved() { return voucherComplianceApproved; }
+ public void setVoucherComplianceApproved(Integer voucherComplianceApproved) { this.voucherComplianceApproved = voucherComplianceApproved; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
+ public Long getAttendeeId() { return attendeeId; }
+ public void setAttendeeId(Long attendeeId) { this.attendeeId = attendeeId; }
+ public String getAttendeeLaborProtocol() { return attendeeLaborProtocol; }
+ public void setAttendeeLaborProtocol(String attendeeLaborProtocol) { this.attendeeLaborProtocol = attendeeLaborProtocol; }
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
public Long[] getAttendeeUserIds() { return attendeeUserIds; }
public void setAttendeeUserIds(Long[] attendeeUserIds) { this.attendeeUserIds = attendeeUserIds; }
+ public BigDecimal getLaborFee() { return laborFee; }
+ public void setLaborFee(BigDecimal laborFee) { this.laborFee = laborFee; }
+ public BigDecimal getMeetingFee() { return meetingFee; }
+ public void setMeetingFee(BigDecimal meetingFee) { this.meetingFee = meetingFee; }
+ public BigDecimal getTotalFee() { return totalFee; }
+ public void setTotalFee(BigDecimal totalFee) { this.totalFee = totalFee; }
+ public Integer getFeeCalcStatus() { return feeCalcStatus; }
+ public void setFeeCalcStatus(Integer feeCalcStatus) { this.feeCalcStatus = feeCalcStatus; }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java
index 5e92b0a..f843157 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java
@@ -135,4 +135,14 @@ public class BizMeetingAttendee extends BaseEntity {
private Integer isDeleted;
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
+
+ /** 是否已推送电子签 0否1是 (admin/manager 点"推送电子签"后置1) */
+ private Integer isEsigned;
+ public Integer getIsEsigned() { return isEsigned; }
+ public void setIsEsigned(Integer isEsigned) { this.isEsigned = isEsigned; }
+
+ /** 是否已邀请参会 0否1是 (点"邀请参会"后置1) */
+ private Integer isInvited;
+ public Integer getIsInvited() { return isInvited; }
+ public void setIsInvited(Integer isInvited) { this.isInvited = isInvited; }
}
\ No newline at end of file
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 20ce7d1..96b0833 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
@@ -25,8 +25,17 @@ public class BizMeetingAuditLog {
/** 审核意见 */
private String opinion;
- /** 当前阶段 (INIT / ... 后续开发中定) */
- private String currentStage;
+ /** 执行方当时展示状态 */
+ private String executorStage;
+
+ /** 支持方当时展示状态 */
+ private String sponsorStage;
+
+ /** 合规当时展示状态 */
+ private String managerStage;
+
+ /** 管理员当时展示状态 */
+ private String adminStage;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -57,8 +66,17 @@ public class BizMeetingAuditLog {
public String getOpinion() { return opinion; }
public void setOpinion(String opinion) { this.opinion = opinion; }
- public String getCurrentStage() { return currentStage; }
- public void setCurrentStage(String currentStage) { this.currentStage = currentStage; }
+ public String getExecutorStage() { return executorStage; }
+ public void setExecutorStage(String executorStage) { this.executorStage = executorStage; }
+
+ public String getSponsorStage() { return sponsorStage; }
+ public void setSponsorStage(String sponsorStage) { this.sponsorStage = sponsorStage; }
+
+ public String getManagerStage() { return managerStage; }
+ public void setManagerStage(String managerStage) { this.managerStage = managerStage; }
+
+ public String getAdminStage() { return adminStage; }
+ public void setAdminStage(String adminStage) { this.adminStage = adminStage; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingMaterial.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingMaterial.java
index a780d2b..f285fc8 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingMaterial.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingMaterial.java
@@ -7,10 +7,10 @@ import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 会议材料对象 biz_meeting_material (单表)
*
- * 包含 4 大类 13 子类:
+ * 包含 4 大类 17 子类:
*
* - material_type: SERVICE=会务材料, LABOR=劳务材料, SERVICE_VOUCHER=会务凭证, LABOR_VOUCHER=劳务凭证
- * - sub_type: M_MATERIAL / M_HOTEL / M_TRAFFIC_BIG / M_TRAFFIC_SMALL / M_EXECUTION / M_DESIGN / M_OTHER / M_SETTLEMENT / M_INVOICE / L_DETAIL / L_AGREEMENT / SV_PAYMENT / LV_PAYMENT
+ * - sub_type: M_MATERIAL / M_HOTEL / M_TRAFFIC_BIG / M_TRAFFIC_SMALL / M_EXECUTION / M_DESIGN / M_OTHER / M_SETTLEMENT / M_INVOICE / L_DETAIL / L_AGREEMENT / L_ENTERPRISE_BENEFIT / L_SIGN_IN / L_PANORAMA / L_EXPERT_PHOTO / SV_PAYMENT / LV_PAYMENT
*
*
* 注意: 不继承 BaseEntity — 不要 create_by / update_by / update_time 字段.
@@ -29,7 +29,7 @@ public class BizMeetingMaterial {
/** 资料类型 (4 种): SERVICE / LABOR / SERVICE_VOUCHER / LABOR_VOUCHER */
private String materialType;
- /** 子分类 (13 种): M_MATERIAL / M_HOTEL / ... / SV_PAYMENT / LV_PAYMENT */
+ /** 子分类 (17 种): M_MATERIAL / M_HOTEL / ... / SV_PAYMENT / LV_PAYMENT */
private String subType;
/** 文件名称 */
@@ -38,6 +38,9 @@ public class BizMeetingMaterial {
/** OSS URL */
private String ossUrl;
+ /** 脱敏版 OSS URL (签到表拍照时额外生成的高斯模糊版, sponsor 只看这个以隐藏手机号/身份证号) */
+ private String extraOssUrl;
+
/** 金额 (发票专用, 其他类型 = 0) */
private BigDecimal amount;
@@ -51,6 +54,9 @@ public class BizMeetingMaterial {
/** 软删除标记 0否1是 (admin 删除会议时级联置1, 查询需 WHERE is_deleted=0) */
private Integer isDeleted;
+ /** 该材料发票金额是否已计算 0未计算(待OCR) 1已计算(OCR完 或 本就不需OCR) */
+ private Integer feeStatus;
+
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@@ -69,6 +75,9 @@ public class BizMeetingMaterial {
public String getOssUrl() { return ossUrl; }
public void setOssUrl(String ossUrl) { this.ossUrl = ossUrl; }
+ public String getExtraOssUrl() { return extraOssUrl; }
+ public void setExtraOssUrl(String extraOssUrl) { this.extraOssUrl = extraOssUrl; }
+
public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }
@@ -80,4 +89,7 @@ public class BizMeetingMaterial {
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
+
+ public Integer getFeeStatus() { return feeStatus; }
+ public void setFeeStatus(Integer feeStatus) { this.feeStatus = feeStatus; }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java
index 6e4fad1..50d287e 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java
@@ -57,6 +57,9 @@ public class BizPerson extends BaseEntity {
private String accountType;
/** 主账号ID (来自 sys_user.parent_user_id, 子账号指向其主账号) - 仅展示用 */
private Long parentUserId;
+ /** 登录账号 (来自 sys_user.user_name, 跟 accountType / parentUserId 一样仅展示, 不入库) */
+ @com.fasterxml.jackson.annotation.JsonProperty("account")
+ private String account;
/** 子账号登录账号 (前端传入, 用于创建 sys_user 子账号) - 非持久化字段 */
@com.fasterxml.jackson.annotation.JsonProperty("userName")
private transient String loginUsername;
@@ -102,6 +105,8 @@ public class BizPerson extends BaseEntity {
public void setAccountType(String accountType) { this.accountType = accountType; }
public Long getParentUserId() { return parentUserId; }
public void setParentUserId(Long parentUserId) { this.parentUserId = parentUserId; }
+ public String getAccount() { return account; }
+ public void setAccount(String account) { this.account = account; }
public String getLoginUsername() { return loginUsername; }
public void setLoginUsername(String loginUsername) { this.loginUsername = loginUsername; }
public String getLoginPassword() { return loginPassword; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java
index 69fcf4e..a550186 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java
@@ -25,6 +25,8 @@ public class BizProject extends BaseEntity {
private Long assignedSessions;
/** 分配给当前执行方的金额 (biz_project_assign.amount 之和, 仅 executor 端使用) */
private java.math.BigDecimal assignedAmount;
+ /** 该项目下已建的会议数 (biz_meeting 计数, 仅 executor 端建会限额用) */
+ private Long meetingCount;
/** done_sessions */
@Excel(name = "done_sessions")
private Long doneSessions;
@@ -152,6 +154,8 @@ public class BizProject extends BaseEntity {
public void setAssignedSessions(Long assignedSessions) { this.assignedSessions = assignedSessions; }
public java.math.BigDecimal getAssignedAmount() { return assignedAmount; }
public void setAssignedAmount(java.math.BigDecimal assignedAmount) { this.assignedAmount = assignedAmount; }
+ public Long getMeetingCount() { return meetingCount; }
+ public void setMeetingCount(Long meetingCount) { this.meetingCount = meetingCount; }
public Long getDoneSessions() { return doneSessions; }
public void setDoneSessions(Long doneSessions) { this.doneSessions = doneSessions; }
public Long getTodoSessions() { return todoSessions; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectExecutorAssign.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectExecutorAssign.java
new file mode 100644
index 0000000..61a449c
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectExecutorAssign.java
@@ -0,0 +1,54 @@
+package com.ruoyi.business.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/** 项目-执行方-执行人分配记录 (biz_project_executor_assign) */
+public class BizProjectExecutorAssign extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+ private Long id;
+ private String projectId;
+ /** 执行方主账号 user_id (分配人, 审计) */
+ private Long executorUserId;
+ /** 执行人 user_id (被分配) */
+ private Long staffUserId;
+ /** 多个执行人 userId (前端 multi-select 传入, 控制器循环 insert, 不入库) */
+ @com.fasterxml.jackson.annotation.JsonProperty("staffUserIds")
+ private java.util.List staffUserIds;
+ private String assignDesc;
+ private String assignPoints;
+
+ /** 关联展示字段 (非持久化) */
+ private String executorUserName;
+ private String staffUserName;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ /** 软删除标记 0否1是 (项目级联删除时按 project_id (String) 置1, 查询需 WHERE is_deleted=0) */
+ private Integer isDeleted;
+ public Integer getIsDeleted() { return isDeleted; }
+ public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+ public String getProjectId() { return projectId; }
+ public void setProjectId(String projectId) { this.projectId = projectId; }
+ public Long getExecutorUserId() { return executorUserId; }
+ public void setExecutorUserId(Long executorUserId) { this.executorUserId = executorUserId; }
+ public Long getStaffUserId() { return staffUserId; }
+ public void setStaffUserId(Long staffUserId) { this.staffUserId = staffUserId; }
+ public java.util.List getStaffUserIds() { return staffUserIds; }
+ public void setStaffUserIds(java.util.List staffUserIds) { this.staffUserIds = staffUserIds; }
+ public String getAssignDesc() { return assignDesc; }
+ public void setAssignDesc(String assignDesc) { this.assignDesc = assignDesc; }
+ public String getAssignPoints() { return assignPoints; }
+ public void setAssignPoints(String assignPoints) { this.assignPoints = assignPoints; }
+ public String getExecutorUserName() { return executorUserName; }
+ public void setExecutorUserName(String executorUserName) { this.executorUserName = executorUserName; }
+ public String getStaffUserName() { return staffUserName; }
+ public void setStaffUserName(String staffUserName) { this.staffUserName = staffUserName; }
+ public Date getCreateTime() { return createTime; }
+ public void setCreateTime(Date createTime) { this.createTime = createTime; }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java
index 76574a5..c0343d0 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java
@@ -11,6 +11,9 @@ public class BizProjectSponsorAssign extends BaseEntity {
private String projectId;
private Long sponsorUserId;
private Long monitorUserId;
+ /** 多个监察员 userId (前端 multi-select 传入, 控制器循环 insert, 不入库) */
+ @com.fasterxml.jackson.annotation.JsonProperty("monitorUserIds")
+ private java.util.List monitorUserIds;
private String assignDesc;
private String assignPoints;
@@ -34,6 +37,8 @@ public class BizProjectSponsorAssign extends BaseEntity {
public void setSponsorUserId(Long sponsorUserId) { this.sponsorUserId = sponsorUserId; }
public Long getMonitorUserId() { return monitorUserId; }
public void setMonitorUserId(Long monitorUserId) { this.monitorUserId = monitorUserId; }
+ public java.util.List getMonitorUserIds() { return monitorUserIds; }
+ public void setMonitorUserIds(java.util.List monitorUserIds) { this.monitorUserIds = monitorUserIds; }
public String getAssignDesc() { return assignDesc; }
public void setAssignDesc(String assignDesc) { this.assignDesc = assignDesc; }
public String getAssignPoints() { return assignPoints; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicitySupportIntent.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicitySupportIntent.java
index 4b7a43e..5fc0708 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicitySupportIntent.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicitySupportIntent.java
@@ -8,7 +8,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
/**
* 公示页-支持意向 (匿名快照, 与 sys_user 解耦)
* 数据源: /publicity/:projectId 页面 "表达支持意向" 按钮
- * 与 biz_support_intent (旧表, 已登录用户流程) 语义不同, 物理表独立
+ * 与旧 biz_support_intent (已删除) 语义不同: 本表允许 user_id=NULL (匿名), 旧表强绑已登录用户
*/
public class BizPublicitySupportIntent extends BaseEntity {
private static final long serialVersionUID = 1L;
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportIntent.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportIntent.java
deleted file mode 100644
index 423b5ab..0000000
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportIntent.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package com.ruoyi.business.domain;
-
-import java.math.BigDecimal;
-import java.util.Date;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.ruoyi.common.annotation.Excel;
-import com.ruoyi.common.core.domain.BaseEntity;
-
-/** 支持意向对象 BizSupportIntent */
-public class BizSupportIntent extends BaseEntity {
- private static final long serialVersionUID = 1L;
- /** intentId */
- private String intentId;
- /** project_no */
- @Excel(name = "project_no")
- private String projectNo;
- /** project_name */
- @Excel(name = "project_name")
- private String projectName;
- /** name */
- @Excel(name = "name")
- private String name;
- /** work_unit */
- @Excel(name = "work_unit")
- private String workUnit;
- /** department */
- @Excel(name = "department")
- private String department;
- /** position */
- @Excel(name = "position")
- private String position;
- /** phone */
- @Excel(name = "phone")
- private String phone;
- /** account_status */
- @Excel(name = "account_status")
- private String accountStatus;
- /** create_by */
- @Excel(name = "create_by")
- private String createBy;
- /** create_time */
- @Excel(name = "create_time")
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
- private Date createTime;
- /** update_by */
- @Excel(name = "update_by")
- private String updateBy;
- /** update_time */
- @Excel(name = "update_time")
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
- private Date updateTime;
- public String getIntentId() { return intentId; }
- public void setIntentId(String intentId) { this.intentId = intentId; }
- public String getProjectNo() { return projectNo; }
- public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
- public String getProjectName() { return projectName; }
- public void setProjectName(String projectName) { this.projectName = projectName; }
- public String getName() { return name; }
- public void setName(String name) { this.name = name; }
- public String getWorkUnit() { return workUnit; }
- public void setWorkUnit(String workUnit) { this.workUnit = workUnit; }
- public String getDepartment() { return department; }
- public void setDepartment(String department) { this.department = department; }
- public String getPosition() { return position; }
- public void setPosition(String position) { this.position = position; }
- public String getPhone() { return phone; }
- public void setPhone(String phone) { this.phone = phone; }
- public String getAccountStatus() { return accountStatus; }
- public void setAccountStatus(String accountStatus) { this.accountStatus = accountStatus; }
- public String getCreateBy() { return createBy; }
- public void setCreateBy(String createBy) { this.createBy = createBy; }
- public Date getCreateTime() { return createTime; }
- public void setCreateTime(Date createTime) { this.createTime = createTime; }
- public String getUpdateBy() { return updateBy; }
- public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
- public Date getUpdateTime() { return updateTime; }
- public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
-}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java
index ccba4f2..5dbe59a 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java
@@ -80,6 +80,22 @@ public class BizMeetingAttendeeImportVo {
@Excel(name = "摘要", sort = 15)
private String summary;
+ /** 账户名称(持卡人姓名) */
+ @Excel(name = "账户名称(持卡人姓名)", sort = 16)
+ private String accountName;
+
+ /** 开户银行地址(省/市) */
+ @Excel(name = "开户银行地址", sort = 17)
+ private String bankRegion;
+
+ /** 银行详细地址 */
+ @Excel(name = "银行详细地址", sort = 18)
+ private String bankAddress;
+
+ /** 身份证附件(正反面 URL CSV) */
+ @Excel(name = "身份证附件", sort = 19)
+ private String idCardAttachments;
+
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getName() { return name; }
@@ -110,4 +126,12 @@ public class BizMeetingAttendeeImportVo {
public void setFee(BigDecimal fee) { this.fee = fee; }
public String getSummary() { return summary; }
public void setSummary(String summary) { this.summary = summary; }
+ public String getAccountName() { return accountName; }
+ public void setAccountName(String accountName) { this.accountName = accountName; }
+ public String getBankRegion() { return bankRegion; }
+ public void setBankRegion(String bankRegion) { this.bankRegion = bankRegion; }
+ public String getBankAddress() { return bankAddress; }
+ public void setBankAddress(String bankAddress) { this.bankAddress = bankAddress; }
+ public String getIdCardAttachments() { return idCardAttachments; }
+ public void setIdCardAttachments(String idCardAttachments) { this.idCardAttachments = idCardAttachments; }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java
index a1ca660..63957fe 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java
@@ -6,8 +6,8 @@ import com.ruoyi.business.domain.BizMeetingAttendee;
public interface BizMeetingAttendeeMapper {
int insert(BizMeetingAttendee entity);
- /** 批量插入参会人 (BizMeetingController.add 调用) */
- int insertBatch(@Param("meetingId") Long meetingId, @Param("userIds") Long[] userIds, @Param("createBy") String createBy);
+ /** 批量插入参会人 (BizMeetingController.add/edit 调用), id 由调用方雪花 ID 填好 */
+ int insertBatch(@Param("list") List list);
/**
* 管理端"新增参会人"用: 一次性插入完整档案 (含 name/phone/workUnit 等).
* 与 {@link #insert} 区别: insert 只写 meeting_id+user_id+create_by (医生端"刚被加入"零信息行),
@@ -20,6 +20,10 @@ public interface BizMeetingAttendeeMapper {
/** 提交签字: 一次性存 handsign + labor_protocol + signed_at + signed_ip */
int updateSign(BizMeetingAttendee entity);
int updateLaborProtocol(BizMeetingAttendee entity);
+ /** 推送电子签后置 is_esigned=1 (单条) */
+ int markEsignedById(@Param("id") Long id);
+ /** 邀请参会后置 is_invited=1 (单条) */
+ int markInvitedById(@Param("id") Long id);
int deleteByMeetingId(Long meetingId);
/** 管理端按 attendee.id 单删 (MeetingDetail 参会人 CRUD 用) */
int deleteByPrimaryKey(Long id);
@@ -30,9 +34,16 @@ public interface BizMeetingAttendeeMapper {
List selectByUserId(Long userId);
BizMeetingAttendee selectById(Long id);
List selectUnsignedByUserId(Long userId);
+ /** 当前用户的"待参加"会议 (已邀请参会 is_invited=1), 联表取会议名/时间 */
+ List selectInvitedByUserId(Long userId);
/**
* 拿某会议已存在的参会人 userId 列表 (用于 add/edit 时 diff 新加入的人, 仅通知增量)
* 性能: 只查 user_id 一列, 走 meeting_id 索引; meeting 参会人通常 < 100, 无压力.
*/
List selectUserIdsByMeetingId(Long meetingId);
+ /**
+ * 费用汇总用: 某会议所有参会人应发金额 (fee_pre_tax) 之和 (is_deleted=0).
+ * 空/无参会人 → 返回 0 (SQL ifnull 兜底).
+ */
+ java.math.BigDecimal sumFeePreTaxByMeetingId(Long meetingId);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java
index 779f531..e9fb681 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java
@@ -1,5 +1,7 @@
package com.ruoyi.business.mapper;
+import java.math.BigDecimal;
import java.util.List;
+import org.apache.ibatis.annotations.Param;
import com.ruoyi.business.domain.BizMeeting;
/**
@@ -17,4 +19,36 @@ public interface BizMeetingMapper
int softDeleteByPrimaryKey(Long meetingId);
/** 项目级联删除时用: 查项目下所有 meeting_id (不过滤 is_deleted, 软删 idempotent) */
List selectIdListByProjectId(Long projectId);
+ /** 建会限额用: 统计某项目下未软删的会议数 (executor 建会不得超过分配的场次) */
+ int countByProjectId(Long projectId);
+ /**
+ * 自动流转: start_time 已过 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 is_executed=1 并转 RUNNING.
+ * 由 MeetingStageScheduler 每分钟触发. 事实 + current_stage 缓存一起写.
+ */
+ int markExecuted();
+ /**
+ * 自动流转: material 未提交 且 end_time + biz_project.submit_deadline_days 已过 → 置 is_frozen=1 并转 FROZEN.
+ *
由 MeetingStageScheduler 每分钟触发.
+ */
+ int markFrozen();
+ /**
+ * 自动流转: material/voucher 都 APPROVED 且 最晚审核时间已过 1 自然日 且 current_stage 仍为 SUPERVISION_APPROVED → AWAITING_SETTLEMENT.
+ *
由 MeetingStageScheduler 每分钟触发 (待结算的 24h 慢路径).
+ */
+ int markSettlementReady();
+ /**
+ * 费用汇总调度器用: 查 fee_calc_status=0 且未软删的会议 id 列表.
+ */
+ List selectPendingFeeCalcIds();
+ /**
+ * 置未汇总 (人员/材料变化触发, 幂等).
+ */
+ int markFeeCalcPending(Long meetingId);
+ /**
+ * 汇总回写 labor_fee/meeting_fee/total_fee 并置 fee_calc_status=1.
+ */
+ int updateFeeSummary(@Param("meetingId") Long meetingId,
+ @Param("laborFee") BigDecimal laborFee,
+ @Param("meetingFee") BigDecimal meetingFee,
+ @Param("totalFee") BigDecimal totalFee);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMaterialMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMaterialMapper.java
index 300d869..3c876c7 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMaterialMapper.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMaterialMapper.java
@@ -34,4 +34,7 @@ public interface BizMeetingMaterialMapper {
/** 单条更新 amount (OCR 识别为发票后回写, 不动其他字段) */
int updateAmount(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount);
+
+ /** 单条更新 fee_status (OCR 完成/材料保存时设置 0未算 1已算) */
+ int updateFeeStatus(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("feeStatus") Integer feeStatus);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectExecutorAssignMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectExecutorAssignMapper.java
new file mode 100644
index 0000000..48d41a1
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectExecutorAssignMapper.java
@@ -0,0 +1,13 @@
+package com.ruoyi.business.mapper;
+
+import java.util.List;
+import com.ruoyi.business.domain.BizProjectExecutorAssign;
+
+public interface BizProjectExecutorAssignMapper {
+ int insertAssign(BizProjectExecutorAssign entity);
+ List selectByProjectId(String projectId);
+ /** 按 project_id 全删 (执行方分配: 先删后插策略) */
+ int deleteByProjectId(String projectId);
+ /** 软删除: 项目级联删除时按 project_id (String) 置 is_deleted=1 */
+ int softDeleteByProjectId(String projectId);
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java
index 5144ddc..3e5431b 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java
@@ -13,6 +13,8 @@ public interface BizProjectMapper
List selectSponsorList(BizProject entity);
/** executor 端专属: JOIN biz_project_assign 过滤, 只返回分给当前 user 的项目 */
List selectExecutorList(BizProject entity);
+ /** executor 执行人 (SUB 子账号) 专属: 反查 biz_project_executor_assign.staff_user_id, 只看自己被派到的项目 */
+ List selectExecutorStaffList(BizProject entity);
int insert(BizProject entity);
int updateByPrimaryKey(BizProject entity);
int deleteByPrimaryKey(Long projectId);
@@ -21,4 +23,15 @@ public interface BizProjectMapper
int softDeleteByProjectId(Long projectId);
/** 级联删除时用: 查 project_no (不过滤 is_deleted, 避免已软删项目查不到 projectNo) */
String selectProjectNoById(Long projectId);
+ /** 建会限额用: 统计某项目分配给该执行方 (MAIN) 的总场次 biz_project_assign.sessions 之和 */
+ int countAssignedSessions(@org.apache.ibatis.annotations.Param("projectId") Long projectId,
+ @org.apache.ibatis.annotations.Param("executorUserId") Long executorUserId);
+ /** 提交权限用: 判断 user 是否该项目的执行方 (MAIN biz_project_assign 或 SUB biz_project_executor_assign), >0 即命中 */
+ int countExecutorOfProject(@org.apache.ibatis.annotations.Param("projectId") Long projectId,
+ @org.apache.ibatis.annotations.Param("userId") Long userId);
+ /**
+ * 会议结算后重算项目金额: 按"所有已结算会议"全量 SUM 回写 paid_labor_amount / paid_meeting_amount,
+ * 并重算 available_amount = total_amount - manage_fee - 已支付劳务 - 已支付会务 (幂等, 无累计副作用).
+ */
+ int recomputeSettledAmounts(@org.apache.ibatis.annotations.Param("projectId") Long projectId);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportIntentMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportIntentMapper.java
deleted file mode 100644
index 6799f57..0000000
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportIntentMapper.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.ruoyi.business.mapper;
-import java.util.List;
-import com.ruoyi.business.domain.BizSupportIntent;
-
-/**
- * 支持意向Mapper接口
- */
-public interface BizSupportIntentMapper
-{
- BizSupportIntent selectByPrimaryKey(String intentId);
- List selectList(BizSupportIntent entity);
- int insert(BizSupportIntent entity);
- int updateByPrimaryKey(BizSupportIntent entity);
- int deleteByPrimaryKey(String intentId);
- int deleteByPrimaryKeys(String[] intentIds);
-}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/notify/BizNotifyService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/notify/BizNotifyService.java
index 09f0499..529a376 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/notify/BizNotifyService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/notify/BizNotifyService.java
@@ -195,6 +195,40 @@ public class BizNotifyService
log.info("[notify] meetingInvitation 已发 uid={} meetingId={}", userId, meetingId);
}
+ /**
+ * 支持方(监察员) 审批退回 → 通知执行方 (待整改 + 说明).
+ *
+ * 调用方: {@link com.ruoyi.business.controller.BizMeetingController#supervisionOpinion}
+ * 退回分支内, 对每个 biz_meeting_executor 逐条调 (待办: 需要整改后重新提交).
+ *
+ * @param execUserId 执行方 sys_user.user_id (nullable, 跳过)
+ * @param meetingId biz_meeting.meeting_id
+ * @param meetingName 会议名 (可空, 兜底)
+ * @param opinion 退回意见 (可空)
+ */
+ public void meetingSupervisionRejected(Long execUserId, Long meetingId, String meetingName, String opinion)
+ {
+ if (execUserId == null) {
+ log.warn("[notify] meetingSupervisionRejected: execUserId 为空, 跳过 (meetingId={})", meetingId);
+ return;
+ }
+ String name = meetingName != null ? meetingName : ("会议 #" + meetingId);
+ StringBuilder content = new StringBuilder("会议【").append(name).append("】监管未通过, 请整改后重新提交");
+ if (opinion != null && !opinion.isEmpty()) content.append("。意见: ").append(opinion);
+ content.append("。");
+
+ BizMessage msg = new BizMessage();
+ msg.setReceiverUserId(execUserId);
+ msg.setMsgType(TYPE_TODO); // 待办: 执行方需要整改后重新提交
+ msg.setTitle("会议监管退回: " + name);
+ msg.setContent(content.toString());
+ msg.setBizType(BIZ_MEETING);
+ msg.setBizId(meetingId);
+ msg.setCreateBy("system");
+ bizMessageService.insert(msg);
+ log.info("[notify] meetingSupervisionRejected 已发 uid={} meetingId={}", execUserId, meetingId);
+ }
+
/**
* #6 劳务协议待签 → 通知参会人协议已生成, 请手写签字.
*
@@ -225,6 +259,41 @@ public class BizNotifyService
log.info("[notify] agreementAwaitingSign 已发 uid={} attendeeId={}", userId, attendeeId);
}
+ /**
+ * #7 推送电子签 → 通知参会人劳务协议待签署 (带签署链接).
+ *
+ *
调用方: {@link com.ruoyi.business.service.impl.BizMeetingAttendeeServiceImpl#pushEsign},
+ * 与短信同批推送, 站内信附带签署链接 (与短信同一链接).
+ *
+ * @param userId 被通知人 sys_user.user_id (参会人的 user_id)
+ * @param attendeeId biz_meeting_attendee.id (用于 bizId 跳转 + 拼链接)
+ * @param meetingId biz_meeting.meeting_id (会议 ID, 用于 title/兜底)
+ * @param meetingName 会议名 (可空, 兜底)
+ * @param link 签署链接 (可空, 空则不展示)
+ */
+ public void esignPushed(Long userId, Long attendeeId, Long meetingId, String meetingName, String link)
+ {
+ if (userId == null) {
+ log.warn("[notify] esignPushed: userId 为空, 跳过 (attendeeId={})", attendeeId);
+ return;
+ }
+ String name = meetingName != null ? meetingName : ("会议 #" + meetingId);
+ StringBuilder content = new StringBuilder("会议【").append(name).append("】的劳务协议已生成, 请点击链接签署: ");
+ if (link != null && !link.isEmpty()) {
+ content.append(link);
+ }
+ BizMessage msg = new BizMessage();
+ msg.setReceiverUserId(userId);
+ msg.setMsgType(TYPE_TODO); // 待办: 医生需要签署
+ msg.setTitle("劳务协议待签署: " + name);
+ msg.setContent(content.toString());
+ msg.setBizType(BIZ_AGREEMENT);
+ msg.setBizId(attendeeId);
+ msg.setCreateBy("system");
+ bizMessageService.insert(msg);
+ log.info("[notify] esignPushed 已发 uid={} attendeeId={}", userId, attendeeId);
+ }
+
/**
* #3 项目分配执行方 → 通知被分配的 executor (待办: 去承接).
*
@@ -264,4 +333,46 @@ public class BizNotifyService
bizMessageService.insert(msg);
log.info("[notify] projectAssignedToExecutor 已发 uid={} projectId={}", execUserId, projectId);
}
+
+ /**
+ * #3b 项目分配监察员 → 通知被分配的 sponsor/监察员 (待办: 去查看项目).
+ *
+ *
调用方: {@link com.ruoyi.business.controller.BizProjectController#sponsorAssign} /
+ * {@code sponsorAssignBatch}, 在 biz_project_sponsor_assign 写入后, 对"新增"或"内容变化"的
+ * 监察员逐条调本方法. 去重由调用方保证 (旧数据差集 + assignDesc/assignPoints 未变跳过), 本方法不再判断.
+ *
+ * @param monitorUserId 被分配的监察员 sys_user.user_id (nullable, 跳过)
+ * @param projectId biz_project.project_id (Long, 用于 bizId; sponsor_assign 表里是 String, 调用方负责转)
+ * @param projectName 项目名 (可空, 兜底)
+ * @param assignDesc 分配说明 (可空)
+ * @param assignPoints 分配积分 (可空)
+ */
+ public void projectAssignedToSponsor(Long monitorUserId, Long projectId, String projectName,
+ String assignDesc, String assignPoints)
+ {
+ if (monitorUserId == null) {
+ log.warn("[notify] projectAssignedToSponsor: monitorUserId 为空, 跳过 (projectId={})", projectId);
+ return;
+ }
+ String name = projectName != null ? projectName : ("项目 #" + projectId);
+ StringBuilder content = new StringBuilder("您被分配为【").append(name).append("】的监察员");
+ if (assignDesc != null && !assignDesc.trim().isEmpty()) {
+ content.append(",说明: ").append(assignDesc.trim());
+ }
+ if (assignPoints != null && !assignPoints.trim().isEmpty()) {
+ content.append(",积分: ").append(assignPoints.trim());
+ }
+ content.append("。请登录系统查看。");
+
+ BizMessage msg = new BizMessage();
+ msg.setReceiverUserId(monitorUserId);
+ msg.setMsgType(TYPE_TODO);
+ msg.setTitle("项目监察分配: " + name);
+ msg.setContent(content.toString());
+ msg.setBizType(BIZ_PROJECT);
+ msg.setBizId(projectId);
+ msg.setCreateBy("system");
+ bizMessageService.insert(msg);
+ log.info("[notify] projectAssignedToSponsor 已发 uid={} projectId={}", monitorUserId, projectId);
+ }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssConfMeta.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssConfMeta.java
index 93ab9dd..3c229ea 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssConfMeta.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssConfMeta.java
@@ -30,10 +30,11 @@ public class OssConfMeta
{
throw new IllegalStateException("OSS 未配置 (application.yml 缺 ruoyi.oss.*)");
}
- this.endpoint = stripScheme(p.getEndpoint());
this.bucket = p.getBucket();
this.accessKeyId = p.getAccessKeyId();
this.accessKeySecret = p.getAccessKeySecret();
+ // endpoint: 剥协议头 + 剥 bucket 前缀 → 纯 OSS endpoint (OSSClient 构造/URL 拼接都要求裸 endpoint, 不带 bucket)
+ this.endpoint = stripBucketPrefix(stripScheme(p.getEndpoint()), this.bucket);
}
/**
@@ -50,6 +51,16 @@ public class OssConfMeta
return slash >= 0 ? s.substring(0, slash) : s;
}
+ /**
+ * 剥 bucket 前缀, 得到纯 OSS endpoint
+ * 例: hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com + bucket=hwtossbamlorgcn → oss-cn-beijing.aliyuncs.com
+ */
+ private static String stripBucketPrefix(String endpoint, String bucket)
+ {
+ if (endpoint == null || bucket == null) return endpoint;
+ return endpoint.startsWith(bucket + ".") ? endpoint.substring(bucket.length() + 1) : endpoint;
+ }
+
public String getEndpoint() { return endpoint; }
public String getBucket() { return bucket; }
public String getAccessKeyId() { return accessKeyId; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/FeeCalcScheduler.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/FeeCalcScheduler.java
new file mode 100644
index 0000000..6476ab8
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/FeeCalcScheduler.java
@@ -0,0 +1,129 @@
+package com.ruoyi.business.scheduler;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import com.ruoyi.business.domain.BizMeetingMaterial;
+import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
+import com.ruoyi.business.mapper.BizMeetingMapper;
+import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 会议费用汇总调度器 (每分钟一次, 多线程无锁).
+ *
+ * 两态设计 (不用抢占锁): 会议 fee_calc_status (0未汇总/1已汇总) + 材料 fee_status (0未计算/1已计算).
+ *
+ * 每分钟: 查 fee_calc_status=0 的会议
+ * → 任一材料 fee_status=0 (发票还没 OCR 完) → 跳过, 等下轮
+ * → 全部 fee_status=1 → SUM 汇总 labor_fee/meeting_fee/total_fee → fee_calc_status=1
+ *
+ * 为什么不需要锁: 汇总前已检查"材料全算完", 天然防半成品; 汇总纯 SUM 幂等, 并发重复无副作用;
+ * 置 1 后不再被扫到, 天然去重. 重算 = 只对 material + attendee 重新求和, 不碰 OCR.
+ */
+@Slf4j
+@Component
+public class FeeCalcScheduler
+{
+ @Autowired
+ private BizMeetingMapper meetingMapper;
+ @Autowired
+ private BizMeetingMaterialMapper materialMapper;
+ @Autowired
+ private BizMeetingAttendeeMapper attendeeMapper;
+ @Autowired
+ @Qualifier("feeCalcExecutor")
+ private ExecutorService feeCalcExecutor;
+
+ /**
+ * 每分钟: 扫描 fee_calc_status=0 的会议, 并行汇总.
+ */
+ @Scheduled(fixedRate = 60_000, initialDelay = 60_000)
+ public void calcFees()
+ {
+ try
+ {
+ List ids = meetingMapper.selectPendingFeeCalcIds();
+ if (ids == null || ids.isEmpty())
+ {
+ return;
+ }
+ log.info("[FeeCalcScheduler] 待汇总会议 {} 个", ids.size());
+ for (Long meetingId : ids)
+ {
+ if (meetingId == null) continue;
+ feeCalcExecutor.submit(() -> processMeeting(meetingId));
+ }
+ }
+ catch (Exception e)
+ {
+ log.warn("[FeeCalcScheduler] 扫描异常 (跳过, 下分钟再试)", e);
+ }
+ }
+
+ private void processMeeting(Long meetingId)
+ {
+ try
+ {
+ List mats = materialMapper.selectByMeetingId(meetingId);
+ // 任一材料 fee_status=0 (发票待 OCR) → 跳过
+ if (mats != null)
+ {
+ for (BizMeetingMaterial m : mats)
+ {
+ if (m.getFeeStatus() != null && m.getFeeStatus() == 0)
+ {
+ return;
+ }
+ }
+ }
+
+ BigDecimal laborFee = attendeeMapper.sumFeePreTaxByMeetingId(meetingId);
+ if (laborFee == null) laborFee = BigDecimal.ZERO;
+ BigDecimal meetingFee = computeMeetingFee(mats);
+ BigDecimal totalFee = laborFee.add(meetingFee);
+
+ meetingMapper.updateFeeSummary(meetingId, laborFee, meetingFee, totalFee);
+ log.info("[FeeCalcScheduler] 汇总完成 meetingId={} labor={} meeting={} total={}",
+ meetingId, laborFee, meetingFee, totalFee);
+ }
+ catch (Exception e)
+ {
+ log.warn("[FeeCalcScheduler] 汇总失败 meetingId={} err={}", meetingId, e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 会务费口径: 有总发票 (M_INVOICE 金额>0) 则只用总发票; 否则各 SUB 子类 (M_ 开头) 金额之和,
+ * 排除 M_INVOICE / M_SETTLEMENT (两者是汇总单据, 非子类发票).
+ */
+ private BigDecimal computeMeetingFee(List mats)
+ {
+ if (mats == null || mats.isEmpty()) return BigDecimal.ZERO;
+ BigDecimal main = null;
+ for (BizMeetingMaterial m : mats)
+ {
+ if ("M_INVOICE".equals(m.getSubType()))
+ {
+ main = m.getAmount();
+ break;
+ }
+ }
+ if (main != null && main.compareTo(BigDecimal.ZERO) > 0)
+ {
+ return main;
+ }
+ BigDecimal sum = BigDecimal.ZERO;
+ for (BizMeetingMaterial m : mats)
+ {
+ if (m.getSubType() == null || !m.getSubType().startsWith("M_")) continue;
+ if ("M_INVOICE".equals(m.getSubType()) || "M_SETTLEMENT".equals(m.getSubType())) continue;
+ if (m.getAmount() != null) sum = sum.add(m.getAmount());
+ }
+ return sum;
+ }
+}
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
new file mode 100644
index 0000000..ee4ca93
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java
@@ -0,0 +1,90 @@
+package com.ruoyi.business.scheduler;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import com.ruoyi.business.mapper.BizMeetingMapper;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 会议事实/阶段 自动流转调度器 (每分钟一次).
+ *
+ * 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实
+ * (is_executed / is_frozen / material_audit_stage / voucher_audit_stage / 审核时间 …),
+ * 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的三类事实落地:
+ *
+ * 1) start_time 到 → is_executed=1 (执行中)
+ * 2) end_time + submit_deadline_days 到 且 material 未提交 → is_frozen=1 (冻结)
+ * 3) material+voucher 都通过 且 最晚审核时间过 24h → 待结算 (current_stage 缓存翻 AWAITING_SETTLEMENT)
+ *
+ * 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围.
+ *
+ * 需要启动类加 {@code @EnableScheduling} (RuoyiApplication 已有).
+ */
+@Slf4j
+@Component
+public class MeetingStageScheduler
+{
+ @Autowired
+ private BizMeetingMapper meetingMapper;
+
+ /**
+ * 每分钟: start_time 已过 且 material 未提交 且未执行 → 置执行中.
+ */
+ @Scheduled(fixedRate = 60_000, initialDelay = 30_000)
+ public void markExecuted()
+ {
+ try
+ {
+ int affected = meetingMapper.markExecuted();
+ if (affected > 0)
+ {
+ log.info("[MeetingStageScheduler] 自动置执行中: 本次更新 {} 行", affected);
+ }
+ }
+ catch (Exception e)
+ {
+ log.warn("[MeetingStageScheduler] 置执行中异常 (跳过, 下分钟再试)", e);
+ }
+ }
+
+ /**
+ * 每分钟: material 未提交 且 提交截止时间 (end_time + submit_deadline_days) 已过 → 冻结.
+ */
+ @Scheduled(fixedRate = 60_000, initialDelay = 30_000)
+ public void markFrozen()
+ {
+ try
+ {
+ int affected = meetingMapper.markFrozen();
+ if (affected > 0)
+ {
+ log.info("[MeetingStageScheduler] 自动冻结: 本次更新 {} 行", affected);
+ }
+ }
+ catch (Exception e)
+ {
+ log.warn("[MeetingStageScheduler] 冻结异常 (跳过, 下分钟再试)", e);
+ }
+ }
+
+ /**
+ * 每分钟: material+voucher 都 APPROVED 且最晚审核时间已过 1 自然日 → 待结算 (24h 慢路径).
+ */
+ @Scheduled(fixedRate = 60_000, initialDelay = 30_000)
+ public void markSettlementReady()
+ {
+ try
+ {
+ int affected = meetingMapper.markSettlementReady();
+ if (affected > 0)
+ {
+ log.info("[MeetingStageScheduler] 自动转待结算: 本次更新 {} 行", affected);
+ }
+ }
+ catch (Exception e)
+ {
+ log.warn("[MeetingStageScheduler] 转待结算异常 (跳过, 下分钟再试)", e);
+ }
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizSignService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizSignService.java
index dc3fce1..a43395f 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizSignService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizSignService.java
@@ -11,6 +11,8 @@ import java.util.Map;
public interface BizSignService {
/** 医生填写页 GET /info?attendeeId=X: 返回默认值 (biz_expert 预填) + 已存 attendee 字段 + 选项 */
Map getSignInfo(Long attendeeId);
+ /** 扫码直登: 只有 meetingId (无 attendeeId) 时, 校验当前用户是否在会议人员列表, 返回 {meetingName, periodNo, totalPeriods, attendeeId} */
+ Map resolveByMeeting(Long meetingId);
/** 医生填写页 POST /saveProfile: 批量 UPDATE attendee 字段 (不含签名) */
void saveProfile(Long attendeeId, BizMeetingAttendee form);
/** 签署页 GET /contract?attendeeId=X: 渲染完整 HTML (占位符替换 + 身份证附件 + 手写签名) */
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java
index 6af9ad8..d8366ef 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java
@@ -42,6 +42,8 @@ public interface IBizMeetingAttendeeService {
List selectByUserId(Long userId);
BizMeetingAttendee selectById(Long id);
List selectUnsignedByUserId(Long userId);
+ /** 当前用户的"待参加"会议 (已邀请参会 is_invited=1) */
+ List selectInvitedByUserId(Long userId);
/**
* 拿某会议已存在的参会人 userId 列表 (#5 会议邀请 dedup 用).
* 列表实现层直接返 mapper 结果; 业务方通常用 {@code new HashSet<>(service.selectUserIdsByMeetingId(mid))} 做 contains 判断.
@@ -60,4 +62,26 @@ public interface IBizMeetingAttendeeService {
* @return ImportResult { okNum, ngNum, ngList: [{rowNum, message}] }
*/
ImportResult importFromExcel(MultipartFile file, Long meetingId, String operName) throws Exception;
+
+ /**
+ * 推送电子签 (批量/单条通用): 对给定 attendee.id 列表逐个
+ * 发短信 (电子签模板) + 推站内信 + 置 is_esigned=1.
+ *
+ * 单条失败 (手机号空/短信异常) 不中断其它人, 返回成功推送条数.
+ *
+ * @param attendeeIds biz_meeting_attendee.id 列表
+ * @return 成功推送 (短信+站内信+标记) 的人数
+ */
+ int pushEsign(List attendeeIds);
+
+ /**
+ * 邀请参会 (批量/单条通用): 对给定 attendee.id 列表逐个
+ * 推"会议邀请"站内信 (不发短信) + 置 is_invited=1.
+ *
+ * 单条失败不中断其它人, 返回成功邀请条数.
+ *
+ * @param attendeeIds biz_meeting_attendee.id 列表
+ * @return 成功邀请 (站内信+标记) 的人数
+ */
+ int invite(List attendeeIds);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingMaterialService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingMaterialService.java
index ddf4ed8..8fb186e 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingMaterialService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingMaterialService.java
@@ -32,4 +32,18 @@ public interface IBizMeetingMaterialService {
* 不动其他字段, 不抛异常 (失败仅 log).
*/
int updateAmount(Long materialId, java.math.BigDecimal amount);
+
+ /**
+ * 单条更新 fee_status (0未计算 1已计算).
+ * OCR 完成后置 1; 材料保存时按需置 0/1.
+ */
+ int updateFeeStatus(Long materialId, Integer feeStatus);
+
+ /**
+ * 扫码拍照回传: ry-h5 手机端拍照直传 OSS 后, 回传 URL 到此存库.
+ * 按 (meetingId, subType) upsert 单行 (存在改 ossUrl, 不存在 insert).
+ * extraOssUrl: 签到表拍照时额外生成的高斯模糊版 URL (sponsor 只看这个), 其他 subType 传空.
+ * 白名单 subType + 会议存在校验 (公开端点防滥用).
+ */
+ void upsertFromCamera(Long meetingId, String subType, String ossUrl, String extraOssUrl);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java
index 93b4a64..817bdb1 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java
@@ -22,4 +22,8 @@ public interface IBizMeetingService
void softDeleteCascade(Long meetingId);
/** 批量软删 (admin 会议管理页一次选多个) */
void softDeleteCascadeBatch(Long[] meetingIds);
+ /** 建会限额用: 统计某项目下未软删的会议数 (executor 建会不得超过分配的场次) */
+ int countByProjectId(Long projectId);
+ /** 标记会议费用待重算 (人员/材料变化触发, 幂等; 由 FeeCalcScheduler 汇总回写) */
+ void markFeeCalcPending(Long meetingId);
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectExecutorAssignService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectExecutorAssignService.java
new file mode 100644
index 0000000..eab90a4
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectExecutorAssignService.java
@@ -0,0 +1,13 @@
+package com.ruoyi.business.service;
+
+import java.util.List;
+import com.ruoyi.business.domain.BizProjectExecutorAssign;
+
+public interface IBizProjectExecutorAssignService {
+ /** 执行方单条分配 (策略: 按 project_id 先删后插) */
+ int insertAssign(BizProjectExecutorAssign entity);
+ /** 执行方多条分配 (一个项目 ↔ N 执行人: 先按 project_id 删, 再逐个 insert, 不会循环 delete) */
+ int assignStaffForProject(BizProjectExecutorAssign body, List staffUserIds);
+ List listByProjectId(String projectId);
+ int deleteByProjectId(String projectId);
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java
index f1abf23..a467e8f 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java
@@ -14,6 +14,8 @@ public interface IBizProjectService
List selectSponsorList(BizProject entity);
/** executor 端专属: JOIN biz_project_assign 过滤, 只返回分给当前 user 的项目 */
List selectExecutorList(BizProject entity);
+ /** executor 执行人 (SUB 子账号) 专属: 反查 biz_project_executor_assign.staff_user_id, 只看自己被派到的项目 */
+ List selectExecutorStaffList(BizProject entity);
int insert(BizProject entity);
int updateByPrimaryKey(BizProject entity);
int deleteByPrimaryKey(Long projectId);
@@ -28,4 +30,19 @@ public interface IBizProjectService
/** 软删除项目 (批量): 逐条 cascade, 失败粒度细 */
void softDeleteCascadeBatch(Long[] projectIds);
+
+ /** 建会限额用: 统计某项目分配给该执行方 (MAIN) 的总场次 biz_project_assign.sessions 之和 */
+ int countAssignedSessions(Long projectId, Long executorUserId);
+
+ /**
+ * 提交材料/凭证权限用: 判断当前 user 是否该项目的执行方 (MAIN 走 biz_project_assign, SUB 走 biz_project_executor_assign).
+ * 与会议列表 executor 可见性同源, 替代原来的 biz_meeting_executor (会议级执行人员) 判定.
+ */
+ boolean isExecutorOfProject(Long projectId, Long userId);
+
+ /**
+ * 会议结算后重算项目金额: 按"所有已结算会议"全量 SUM 回写 paid_labor_amount / paid_meeting_amount,
+ * 并重算 available_amount. 幂等 (每次全量重算), 无累计副作用.
+ */
+ void recomputeSettledAmounts(Long projectId);
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java
index 4323e0d..c8bb511 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java
@@ -4,8 +4,10 @@ import java.util.List;
import com.ruoyi.business.domain.BizProjectSponsorAssign;
public interface IBizProjectSponsorAssignService {
- /** 支持方分配 (策略: 按 project_id 先删后插, 一个项目只分配一个 sponsor) */
+ /** 支持方单条分配 (策略: 按 project_id 先删后插, 一个项目只分配一个 sponsor) */
int insertAssign(BizProjectSponsorAssign entity);
+ /** 支持方多条分配 (一个项目 ↔ N 监察员: 先按 project_id 删, 再逐个 insert, 不会循环 delete) */
+ int assignMonitorsForProject(BizProjectSponsorAssign body, List monitorUserIds);
List listByProjectId(String projectId);
int deleteByProjectId(String projectId);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportIntentService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportIntentService.java
deleted file mode 100644
index 8a50cfc..0000000
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportIntentService.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.ruoyi.business.service;
-
-import java.util.List;
-import com.ruoyi.business.domain.BizSupportIntent;
-
-/**
- * 支持意向Service接口
- */
-public interface IBizSupportIntentService
-{
- BizSupportIntent getById(String intentId);
- List selectList(BizSupportIntent entity);
- int insert(BizSupportIntent entity);
- int updateByPrimaryKey(BizSupportIntent entity);
- int deleteByPrimaryKey(String intentId);
- int deleteByPrimaryKeys(String[] intentId);
-}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java
index f380a35..5a41683 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java
@@ -9,20 +9,16 @@ import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.font.FontProvider;
-import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.business.oss.OssUploader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.text.SimpleDateFormat;
-import java.util.Date;
/**
* HTML 转 PDF 服务
* 用 iText 7 html2pdf (HtmlConverter) + 内置宋体 (simsun.ttc) 渲染中文
- * 输入: HTML 字符串 → 输出: PDF 文件 (存到 ruoyi.profile 目录, 返回 URL)
+ * 输入: HTML 字符串 → 输出: PDF 字节 (上传 OSS, 返回完整 URL)
* 说明: 相比 Flying Saucer (xhtmlrenderer 严格 XML 解析), html2pdf 走 jsoup HTML 解析,
* 能容忍前端拼出的非 XHTML 内容 (如
未自闭合), 不会报 SAXParseException。
*/
@@ -30,7 +26,7 @@ import java.util.Date;
public class PdfService {
@Autowired
- private RuoYiConfig ruoyiConfig;
+ private OssUploader ossUploader;
/**
* HTML 字符串 → PDF 字节流
@@ -61,31 +57,17 @@ public class PdfService {
}
/**
- * HTML → PDF 文件 (存到本地)
- * @return 完整 URL (前端可直接打开)
+ * HTML → PDF 文件 (上传 OSS)
+ * @return 完整 OSS URL (前端可直接打开, 与身份证附件/现场照片等字段一致)
*/
public String htmlToPdfFile(String htmlContent, String bizPath) {
byte[] pdfBytes = htmlToPdf(htmlContent);
- // 按 RuoYi 风格分目录: profile/labor/{date}/{filename}
- SimpleDateFormat dateDir = new SimpleDateFormat("yyyy-MM-dd");
- String today = dateDir.format(new Date());
- String datePath = (bizPath == null || bizPath.isEmpty() ? "labor" : bizPath) + "/" + today;
- String filename = System.currentTimeMillis() + "_" + (int)(Math.random() * 1000) + ".pdf";
-
- String profilePath = ruoyiConfig.getProfile();
- File dir = new File(profilePath + File.separator + datePath);
- if (!dir.exists()) {
- dir.mkdirs();
- }
- File pdfFile = new File(dir, filename);
- try (FileOutputStream fos = new FileOutputStream(pdfFile)) {
- fos.write(pdfBytes);
- } catch (Exception e) {
- throw new RuntimeException("保存 PDF 失败: " + e.getMessage(), e);
- }
- // 返回 URL 路径 (前端拼 origin)
- String url = "/profile/" + datePath + "/" + filename;
- return url;
+ String filename = System.currentTimeMillis() + "_" + (int) (Math.random() * 1000) + ".pdf";
+ // bizPath 直接作为 OSS key 前缀 (例 "labor/123", 去掉首尾斜杠)
+ String subDir = (bizPath == null || bizPath.trim().isEmpty())
+ ? "labor"
+ : bizPath.replaceAll("^/+|/+$", "");
+ return ossUploader.upload(pdfBytes, filename, subDir);
}
/**
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PosterService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PosterService.java
new file mode 100644
index 0000000..a4bfc8f
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PosterService.java
@@ -0,0 +1,191 @@
+package com.ruoyi.business.service;
+
+import cn.hutool.http.HttpUtil;
+import com.ruoyi.business.domain.BizMeeting;
+import com.ruoyi.business.oss.OssUploader;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.imageio.ImageIO;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 会议海报生成服务 (日程海报 → 叠加会议信息 → 上传 OSS).
+ *
+ * 流程 (前端"生成海报"按钮触发):
+ *
+ * - 按 {@code schedule_url} 下载源海报, OSS 图片处理参数 width 固定 1200
+ * - Java2D 在海报底部画半透明信息条: 会议名称 / 期数 / 起止时间 / 支持单位
+ * - 编码 PNG → {@link OssUploader} 上传 OSS
+ * - 回写 {@code biz_meeting.poster_url}, 前端"预览海报"用该 URL 弹 dialog
+ *
+ */
+@Slf4j
+@Service
+public class PosterService {
+
+ @Autowired
+ private IBizMeetingService bizMeetingService;
+ @Autowired
+ private OssUploader ossUploader;
+
+ /**
+ * 生成海报并回写 poster_url.
+ *
+ * @return 生成海报的 OSS URL
+ */
+ public String generatePoster(Long meetingId, boolean addText, String textColor) {
+ BizMeeting m = bizMeetingService.getById(meetingId);
+ if (m == null) throw new RuntimeException("会议不存在");
+ if (m.getScheduleUrl() == null || m.getScheduleUrl().trim().isEmpty()) {
+ throw new RuntimeException("请先上传日程海报");
+ }
+
+ // 1. 下载源海报 (OSS 图片处理: width=1200)
+ byte[] imgBytes = download(m.getScheduleUrl().trim(), 1200);
+
+ // 2. 解析图片
+ BufferedImage img;
+ try {
+ img = ImageIO.read(new ByteArrayInputStream(imgBytes));
+ } catch (Exception e) {
+ throw new RuntimeException("海报图片解析失败: " + e.getMessage(), e);
+ }
+ if (img == null) throw new RuntimeException("海报图片解析失败 (不支持的格式)");
+
+ // 3. (可选) 画会议信息 (addText=false 时只输出处理后的背景海报)
+ if (addText) {
+ drawInfo(img, m, textColor);
+ }
+
+ // 4. 编码 PNG
+ byte[] out;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ ImageIO.write(img, "png", baos);
+ out = baos.toByteArray();
+ } catch (Exception e) {
+ throw new RuntimeException("海报编码失败: " + e.getMessage(), e);
+ }
+
+ // 5. 上传 OSS
+ String url = ossUploader.upload(out, "poster.png", "meeting/poster");
+
+ // 6. 回写 poster_url
+ BizMeeting upd = new BizMeeting();
+ upd.setMeetingId(meetingId);
+ upd.setPosterUrl(url);
+ bizMeetingService.updateByPrimaryKey(upd);
+
+ log.info("[poster] 生成海报完成 meetingId={} url={}", meetingId, url);
+ return url;
+ }
+
+ /** 下载海报 (OSS 图片处理: 缩宽 + 压质量 + 转 JPEG, 减小体积) */
+ private byte[] download(String url, int width) {
+ String sep = url.contains("?") ? "&" : "?";
+ // resize,w_1200 只缩宽且默认不放大、保持原格式 (PNG 无损, 体积不缩);
+ // 加 quality,q_80 + format,jpg 强制转 JPEG 并压质量, 显著减小体积 (海报无透明需求)
+ String dl = url + sep + "x-oss-process=image/resize,w_" + width + "/quality,q_80/format,jpg";
+ log.info("[poster] 下载海报 width={}: {}", width, dl);
+ try {
+ return HttpUtil.downloadBytes(dl);
+ } catch (Exception e) {
+ throw new RuntimeException("下载海报失败: " + e.getMessage(), e);
+ }
+ }
+
+ /** 在海报顶部 (距顶 200px) 居中打印会议信息 (会议名称/期数/起止时间/支持单位) */
+ private void drawInfo(BufferedImage img, BizMeeting m, String textColor) {
+ int w = img.getWidth();
+ Graphics2D g = img.createGraphics();
+ try {
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+ // 字号按宽度 1200 等比缩放 (下载时已 resize 到 1200, 但留个余量)
+ float scale = w / 1200.0f;
+ float titleSize = 44 * scale;
+ float infoSize = 28 * scale;
+
+ String title = (m.getMeetingName() == null || m.getMeetingName().trim().isEmpty())
+ ? "会议" : m.getMeetingName().trim();
+ String period = m.getPeriodNo() != null ? "第 " + m.getPeriodNo() + " 期" : null;
+ String time = buildTime(m.getStartTime(), m.getEndTime());
+ String org = (m.getOrgName() != null && !m.getOrgName().trim().isEmpty())
+ ? "支持单位:" + m.getOrgName().trim() : null;
+
+ List lines = new ArrayList<>();
+ if (period != null) lines.add(period);
+ if (time != null) lines.add(time);
+ if (org != null) lines.add(org);
+
+ // 文字距顶端 200px (等比缩放), 每行水平居中, 无背景条
+ int lineGap = Math.round(6 * scale);
+ int infoLineH = Math.round(infoSize * 1.5f);
+
+ int y = Math.round(200 * scale);
+
+ Color fg = parseColor(textColor, Color.WHITE);
+ g.setColor(fg);
+ g.setFont(loadFont(titleSize, Font.BOLD));
+ g.drawString(title, centerX(g, title, w), y + titleSize);
+ y += titleSize + lineGap;
+
+ g.setFont(loadFont(infoSize, Font.PLAIN));
+ for (String line : lines) {
+ y += infoLineH + lineGap;
+ g.drawString(line, centerX(g, line, w), y);
+ }
+ } finally {
+ g.dispose();
+ }
+ }
+
+ /** 文字水平居中: 用 FontMetrics 测宽算 x */
+ private int centerX(Graphics2D g, String text, int w) {
+ int textW = g.getFontMetrics().stringWidth(text);
+ return (w - textW) / 2;
+ }
+
+ /** 解析前端传来的 hex 文字颜色 (如 #FFFFFF), 非法则回退 fallback */
+ private Color parseColor(String hex, Color fallback) {
+ if (hex == null || hex.trim().isEmpty()) return fallback;
+ try {
+ return Color.decode(hex.trim());
+ } catch (Exception e) {
+ log.warn("[poster] 非法文字颜色 {}: {}", hex, e.getMessage());
+ return fallback;
+ }
+ }
+
+ private String buildTime(Date start, Date end) {
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+ if (start != null && end != null) return fmt.format(start) + " ~ " + fmt.format(end);
+ if (start != null) return fmt.format(start);
+ if (end != null) return fmt.format(end);
+ return null;
+ }
+
+ /** 加载中文字体 (优先项目内 simhei.ttc 黑体; 失败降级系统黑体) */
+ private Font loadFont(float size, int style) {
+ try {
+ Font base = Font.createFont(Font.TRUETYPE_FONT, new File("simhei.ttc"));
+ return base.deriveFont(style, size);
+ } catch (Exception e) {
+ log.warn("[poster] 加载 simhei.ttc 失败, 降级系统字体: {}", e.getMessage());
+ return new Font("黑体", style, Math.round(size));
+ }
+ }
+}
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
new file mode 100644
index 0000000..103a518
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java
@@ -0,0 +1,125 @@
+package com.ruoyi.business.service;
+
+import java.util.Date;
+import org.springframework.stereotype.Component;
+import com.ruoyi.business.domain.BizMeeting;
+
+/**
+ * 会议阶段推导器 (单一可信源).
+ *
+ * 事实与展示分离: {@code biz_meeting} 只存事实 (is_executed/is_settled/is_finished/is_frozen
+ * + material_audit_stage/voucher_audit_stage + 审核时间 + compliance_approved), 各角色看到的
+ * 「阶段名称」由本类实时计算.
+ *
+ * - {@link #derivePhysicalStage(BizMeeting)}: 10 值物理阶段 (current_stage 缓存 + 列表筛选).
+ * - {@link #deriveDisplay(String, BizMeeting)}: 各角色展示名 (audit_log 4 列 + 前端镜像).
+ *
+ *
+ * @author guoju
+ */
+@Component
+public class StageDeriver
+{
+ private static final long H24 = 24L * 3600 * 1000;
+
+ /** 待结算: 材料+凭证都审核通过 且 最晚通过时间已超 24h (用户拍板口径) */
+ private boolean settlementReady(BizMeeting m)
+ {
+ if (!"APPROVED".equals(m.getVoucherAuditStage())) return false;
+ Date later = later(m.getMaterialAuditTime(), m.getVoucherAuditTime());
+ if (later == null) return false;
+ return System.currentTimeMillis() - later.getTime() >= H24;
+ }
+
+ private Date later(Date a, Date b)
+ {
+ if (a == null) return b;
+ if (b == null) return a;
+ return a.after(b) ? a : b;
+ }
+
+ private static boolean t(Integer v)
+ {
+ return v != null && v == 1;
+ }
+
+ /**
+ * 10 值物理阶段 (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/
+ * SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN), 由事实推导.
+ * 用于 current_stage 缓存 (列表筛选仍按物理态精确匹配).
+ */
+ public String derivePhysicalStage(BizMeeting m)
+ {
+ if (t(m.getIsFrozen())) return "FROZEN";
+ 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))
+ {
+ return settlementReady(m) ? "AWAITING_SETTLEMENT" : "SUPERVISION_APPROVED";
+ }
+ 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
+ *
+ */
+ public String deriveDisplay(String role, BizMeeting m)
+ {
+ if (t(m.getIsFrozen())) return "冻结中";
+ if (t(m.getIsFinished())) return "已完结";
+ if (t(m.getIsSettled())) return "已结算";
+
+ String material = m.getMaterialAuditStage();
+
+ // 退回: 执行方看「已退回」, 其他方看「待整改」
+ if ("REJECTED".equals(material))
+ {
+ return "executor".equals(role) ? "已退回" : "待整改";
+ }
+
+ // 材料已支持方通过
+ if ("APPROVED".equals(material))
+ {
+ return settlementReady(m) ? "待结算" : "审核通过";
+ }
+
+ // 材料在审 (SUBMITTED)
+ if ("SUBMITTED".equals(material))
+ {
+ if (t(m.getMaterialComplianceApproved()))
+ {
+ // 支持方审中
+ if ("manager".equals(role)) return "审核通过";
+ if ("sponsor".equals(role)) return "待审核";
+ return "待审核"; // executor / admin
+ }
+ else
+ {
+ // 合规审中
+ if ("sponsor".equals(role)) return "已执行未传材料"; // 只读
+ if ("manager".equals(role)) return "待审核";
+ return "待审核"; // executor / admin
+ }
+ }
+
+ // 未提交
+ if (t(m.getIsExecuted()))
+ {
+ return "executor".equals(role) ? "执行中" : "已执行未传材料";
+ }
+ return "未执行";
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java
index f735be5..3c3d506 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java
@@ -1,20 +1,36 @@
package com.ruoyi.business.service.impl;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.business.domain.BizMeeting;
import com.ruoyi.business.domain.BizMeetingAttendee;
+import com.ruoyi.business.domain.BizProject;
import com.ruoyi.business.domain.dto.ImportResult;
import com.ruoyi.business.domain.vo.BizMeetingAttendeeImportVo;
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
+import com.ruoyi.business.mapper.BizMeetingMapper;
+import com.ruoyi.business.mapper.BizProjectMapper;
+import com.ruoyi.business.notify.BizNotifyService;
import com.ruoyi.business.service.IBizMeetingAttendeeService;
+import com.ruoyi.business.service.IBizMeetingService;
+import com.ruoyi.business.sms.AliyunSmsSender;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.id.IdGenerator;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.ISysUserService;
@@ -27,19 +43,46 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
@Autowired
private BizMeetingAttendeeMapper mapper;
@Autowired
+ private BizMeetingMapper meetingMapper;
+ @Autowired
+ private BizProjectMapper projectMapper;
+ @Autowired
private SysUserMapper sysUserMapper;
@Autowired
private ISysUserService sysUserService;
+ /** Jackson (Spring Boot 自带), 解析 biz_project.role_labor JSON 数组 [{role, customName, amount}] */
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Autowired
+ private AliyunSmsSender aliyunSmsSender;
+ @Autowired
+ private BizNotifyService bizNotifyService;
+ @Autowired
+ private IBizMeetingService bizMeetingService;
+
@Override
public int insert(BizMeetingAttendee entity) {
+ if (entity.getId() == null) {
+ entity.setId(IdGenerator.generateId());
+ }
return mapper.insert(entity);
}
@Override
public int insertBatch(Long meetingId, Long[] userIds) {
if (userIds == null || userIds.length == 0) return 0;
- return mapper.insertBatch(meetingId, userIds, SecurityUtils.getUsername());
+ String createBy = SecurityUtils.getUsername();
+ List list = new ArrayList<>(userIds.length);
+ for (Long uid : userIds) {
+ BizMeetingAttendee a = new BizMeetingAttendee();
+ a.setId(IdGenerator.generateId());
+ a.setMeetingId(meetingId);
+ a.setUserId(uid);
+ a.setCreateBy(createBy);
+ list.add(a);
+ }
+ return mapper.insertBatch(list);
}
/**
@@ -96,9 +139,10 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
throw new ServiceException("该手机号参会人已在会议中, 无需重复添加");
}
- // 4. 写完整档案行
+ // 4. 写完整档案行 (attendee.id 用雪花 ID, 不走 DB 自增)
body.setUserId(userId);
body.setCreateBy(SecurityUtils.getUsername());
+ body.setId(IdGenerator.generateId());
mapper.insertWithProfile(body);
Long newId = body.getId();
log.info("[attendee] 新增参会人 meetingId={} userId={} attendeeId={}", body.getMeetingId(), userId, newId);
@@ -160,11 +204,123 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
return mapper.selectUnsignedByUserId(userId);
}
+ @Override
+ public List selectInvitedByUserId(Long userId) {
+ return mapper.selectInvitedByUserId(userId);
+ }
+
@Override
public List selectUserIdsByMeetingId(Long meetingId) {
return mapper.selectUserIdsByMeetingId(meetingId);
}
+ /**
+ * 劳务报酬个税累进计算 (照搬 hwt BizActGatherService.calcLaborTax).
+ * 阈值 800/3360/21000/49500 是"实发(税后)"分界点, 对应税前 800/4000/25000/62500.
+ *
+ * @param fee 实发金额(税后), ≥ 0
+ * @return 个税税金, 保留 2 位小数 (HALF_UP)
+ */
+ private BigDecimal calcLaborTax(BigDecimal fee) {
+ if (fee == null || fee.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+ BigDecimal tax;
+ if (fee.compareTo(new BigDecimal("800")) <= 0) {
+ tax = BigDecimal.ZERO;
+ } else if (fee.compareTo(new BigDecimal("3360")) <= 0) {
+ tax = fee.subtract(new BigDecimal("800"))
+ .divide(new BigDecimal("4"), 2, RoundingMode.HALF_UP);
+ } else if (fee.compareTo(new BigDecimal("21000")) <= 0) {
+ tax = fee.multiply(new BigDecimal("0.16"))
+ .divide(new BigDecimal("0.84"), 2, RoundingMode.HALF_UP);
+ } else if (fee.compareTo(new BigDecimal("49500")) <= 0) {
+ tax = fee.multiply(new BigDecimal("0.24")).subtract(new BigDecimal("2000"))
+ .divide(new BigDecimal("0.76"), 2, RoundingMode.HALF_UP);
+ } else {
+ tax = fee.multiply(new BigDecimal("0.32")).subtract(new BigDecimal("7000"))
+ .divide(new BigDecimal("0.68"), 2, RoundingMode.HALF_UP);
+ }
+ return tax.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * 由应发金额(feePreTax)反推实发金额(fee), 按劳务报酬个税累进公式分段求解
+ * (照搬 hwt BizActGatherService.reverseCalcFeeFromFee2).
+ * 关系式: feePreTax = (fee + tax) * 1.0151, 其中 1.0151 = 1 + 1.51%(增值税及附加).
+ *
+ * @param fee2 应发金额(税前)
+ * @return 反推的实发金额(税后); fee2 为空/负时返回 null
+ */
+ private BigDecimal reverseCalcFeeFromFee2(BigDecimal fee2) {
+ if (fee2 == null || fee2.compareTo(BigDecimal.ZERO) <= 0) {
+ return null;
+ }
+ BigDecimal r10151 = new BigDecimal("1.0151");
+ // 段1: fee ≤ 800, tax=0, fee2 = fee * 1.0151
+ BigDecimal fee = fee2.divide(r10151, 6, RoundingMode.HALF_UP);
+ if (fee.compareTo(new BigDecimal("800")) <= 0) {
+ return fee;
+ }
+ // 段2: 800 < fee ≤ 3360, fee = (4*fee2/1.0151 + 800)/5
+ fee = fee2.multiply(new BigDecimal("4"))
+ .divide(r10151, 6, RoundingMode.HALF_UP)
+ .add(new BigDecimal("800"))
+ .divide(new BigDecimal("5"), 6, RoundingMode.HALF_UP);
+ if (fee.compareTo(new BigDecimal("800")) > 0 && fee.compareTo(new BigDecimal("3360")) <= 0) {
+ return fee;
+ }
+ // 段3: 3360 < fee ≤ 21000, fee = fee2*84/(100*1.0151)
+ fee = fee2.multiply(new BigDecimal("84"))
+ .divide(new BigDecimal("101.51"), 6, RoundingMode.HALF_UP);
+ if (fee.compareTo(new BigDecimal("3360")) > 0 && fee.compareTo(new BigDecimal("21000")) <= 0) {
+ return fee;
+ }
+ // 段4: 21000 < fee ≤ 49500, fee = 0.76*fee2/1.0151 + 2000
+ fee = fee2.multiply(new BigDecimal("0.76"))
+ .divide(r10151, 6, RoundingMode.HALF_UP)
+ .add(new BigDecimal("2000"));
+ if (fee.compareTo(new BigDecimal("21000")) > 0 && fee.compareTo(new BigDecimal("49500")) <= 0) {
+ return fee;
+ }
+ // 段5: fee > 49500, fee = 0.68*fee2/1.0151 + 7000
+ fee = fee2.multiply(new BigDecimal("0.68"))
+ .divide(r10151, 6, RoundingMode.HALF_UP)
+ .add(new BigDecimal("7000"));
+ return fee;
+ }
+
+ /**
+ * 在项目角色劳务 JSON 数组 [{role, customName, amount}] 里按角色名匹配劳务金额.
+ * 匹配规则与前端 ProjectRoleSelect 一致: role === '其他' 时用 customName 作 label, 否则用 role.
+ *
+ * @param nodes 已解析的 role_labor JSON (可为 null/非数组)
+ * @param laborForm 参会人填的角色名 (可为 null/空)
+ * @return 匹配到的 amount (BigDecimal); 没匹配到或 amount 非法 → null
+ */
+ private BigDecimal findRoleAmount(JsonNode nodes, String laborForm) {
+ if (nodes == null || !nodes.isArray() || laborForm == null || laborForm.trim().isEmpty()) {
+ return null;
+ }
+ String target = laborForm.trim();
+ for (JsonNode n : nodes) {
+ if (n == null || n.isNull()) continue;
+ String role = n.path("role").asText("");
+ String customName = n.path("customName").asText("");
+ String label = "其他".equals(role) ? customName.trim() : role;
+ if (!target.equals(label)) continue;
+ JsonNode amountNode = n.get("amount");
+ if (amountNode == null || amountNode.isNull()) return null;
+ try {
+ if (amountNode.isNumber()) return amountNode.decimalValue();
+ return new BigDecimal(amountNode.asText());
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
/**
* 批量导入参会人 (Excel → biz_meeting_attendee).
*
@@ -184,6 +340,20 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
throw new ServiceException("导入数据不能为空");
}
+ // 项目角色劳务 (只解析一次): 导入行只填角色、没填任何金额时, 按角色带出 amount 补算
+ JsonNode roleLaborNodes = null;
+ try {
+ BizMeeting meeting = meetingMapper.selectByPrimaryKey(meetingId);
+ if (meeting != null && meeting.getProjectId() != null) {
+ BizProject project = projectMapper.selectByPrimaryKey(meeting.getProjectId());
+ if (project != null && project.getRoleLabor() != null && !project.getRoleLabor().trim().isEmpty()) {
+ roleLaborNodes = objectMapper.readTree(project.getRoleLabor());
+ }
+ }
+ } catch (Exception e) {
+ log.warn("[attendee] 解析项目角色劳务失败, 跳过按角色补算金额 meetingId={}", meetingId, e);
+ }
+
ImportResult result = new ImportResult();
for (int i = 0; i < rows.size(); i++) {
BizMeetingAttendeeImportVo vo = rows.get(i);
@@ -195,7 +365,7 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
continue;
}
String phone = vo.getPhone().trim();
- if (!phone.matches("^1[3-9]\\d{9}$")) {
+ if (!phone.matches("^1\\d{10}$")) {
result.fail(rowNo, "手机号格式不正确: " + phone);
continue;
}
@@ -212,11 +382,43 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
body.setBankName(vo.getBankName());
body.setBankCard(vo.getBankCard());
body.setBankBranch(vo.getBankBranch());
+ body.setAccountName(vo.getAccountName());
+ body.setBankRegion(vo.getBankRegion());
+ body.setBankAddress(vo.getBankAddress());
+ body.setIdCardAttachments(vo.getIdCardAttachments());
body.setLaborForm(vo.getLaborForm());
- body.setFeePreTax(vo.getFeePreTax());
- body.setTax(vo.getTax());
- body.setVatAndSurcharge(vo.getVatAndSurcharge());
- body.setFee(vo.getFee());
+ // 金额联动补算 (照搬 hwt importLaborData): 已有值优先, 空白才按链补算, 避免覆盖人工填写
+ BigDecimal fee = vo.getFee();
+ BigDecimal tax = vo.getTax();
+ BigDecimal vat = vo.getVatAndSurcharge();
+ BigDecimal feePreTax = vo.getFeePreTax();
+ if (fee != null) {
+ // 用户填了实发金额 → 正向: fee → tax → vat → feePreTax
+ if (tax == null) tax = calcLaborTax(fee);
+ if (vat == null) vat = fee.add(tax).multiply(new BigDecimal("0.0151")).setScale(2, RoundingMode.HALF_UP);
+ if (feePreTax == null) feePreTax = fee.add(tax).add(vat).setScale(2, RoundingMode.HALF_UP);
+ } else if (feePreTax != null) {
+ // 用户只填了应发金额 → 反推实发, 再正向补 tax/vat (保留用户原填 feePreTax)
+ BigDecimal feeBd = reverseCalcFeeFromFee2(feePreTax);
+ if (feeBd != null) {
+ fee = feeBd;
+ if (tax == null) tax = calcLaborTax(fee);
+ if (vat == null) vat = fee.add(tax).multiply(new BigDecimal("0.0151")).setScale(2, RoundingMode.HALF_UP);
+ }
+ } else if (vo.getLaborForm() != null && !vo.getLaborForm().trim().isEmpty() && roleLaborNodes != null) {
+ // 什么金额都没填, 只填了角色 → 按项目角色劳务 amount 带出实发, 再正向补 tax/vat/feePreTax
+ BigDecimal amount = findRoleAmount(roleLaborNodes, vo.getLaborForm());
+ if (amount != null) {
+ fee = amount;
+ tax = calcLaborTax(fee);
+ vat = fee.add(tax).multiply(new BigDecimal("0.0151")).setScale(2, RoundingMode.HALF_UP);
+ feePreTax = fee.add(tax).add(vat).setScale(2, RoundingMode.HALF_UP);
+ }
+ }
+ body.setFeePreTax(feePreTax);
+ body.setTax(tax);
+ body.setVatAndSurcharge(vat);
+ body.setFee(fee);
body.setSummary(vo.getSummary());
insertByPhoneWithProfile(body); // 失败抛 ServiceException, 被 catch
@@ -231,4 +433,78 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
return result;
}
+ @Override
+ public int pushEsign(List attendeeIds) {
+ if (attendeeIds == null || attendeeIds.isEmpty()) return 0;
+ int sent = 0;
+ Map meetingCache = new HashMap<>();
+ for (Long id : attendeeIds) {
+ if (id == null) continue;
+ try {
+ BizMeetingAttendee a = mapper.selectById(id);
+ if (a == null) continue;
+ Long mid = a.getMeetingId();
+ BizMeeting m = null;
+ if (mid != null) {
+ m = meetingCache.get(mid);
+ if (m == null) {
+ m = bizMeetingService.getById(mid);
+ meetingCache.put(mid, m);
+ }
+ }
+ String meetingName = m != null ? m.getMeetingName() : null;
+ Date startTime = m != null ? m.getStartTime() : null;
+
+ // 1. 短信 (电子签模板: name=参会人姓名, date=会议日期, link=签署链接)
+ String phone = a.getPhone();
+ if (phone != null && !phone.trim().isEmpty()) {
+ aliyunSmsSender.sendEsign(phone.trim(), a.getName(), startTime, aliyunSmsSender.esignLink(id));
+ }
+ // 2. 站内信 (劳务协议待签署 + 签署链接, 与短信同一链接)
+ bizNotifyService.esignPushed(a.getUserId(), id, mid, meetingName, aliyunSmsSender.esignLink(id));
+ // 3. 标记已推送电子签
+ mapper.markEsignedById(id);
+ sent++;
+ } catch (Exception e) {
+ log.warn("[attendee] 推送电子签失败 attendeeId={}: {}", id, e.getMessage());
+ }
+ }
+ log.info("[attendee] 推送电子签完成 共{}条, 成功{}", attendeeIds.size(), sent);
+ return sent;
+ }
+
+ @Override
+ public int invite(List attendeeIds) {
+ if (attendeeIds == null || attendeeIds.isEmpty()) return 0;
+ int sent = 0;
+ Map meetingCache = new HashMap<>();
+ for (Long id : attendeeIds) {
+ if (id == null) continue;
+ try {
+ BizMeetingAttendee a = mapper.selectById(id);
+ if (a == null) continue;
+ Long mid = a.getMeetingId();
+ BizMeeting m = null;
+ if (mid != null) {
+ m = meetingCache.get(mid);
+ if (m == null) {
+ m = bizMeetingService.getById(mid);
+ meetingCache.put(mid, m);
+ }
+ }
+ String meetingName = m != null ? m.getMeetingName() : null;
+ Date startTime = m != null ? m.getStartTime() : null;
+
+ // 推"会议邀请"站内信 (不发短信) + 置 is_invited=1
+ bizNotifyService.meetingInvitation(a.getUserId(), mid, meetingName, startTime);
+ mapper.markInvitedById(id);
+ sent++;
+ } catch (Exception e) {
+ log.warn("[attendee] 邀请参会失败 attendeeId={}: {}", id, e.getMessage());
+ }
+ }
+ log.info("[attendee] 邀请参会完成 共{}条, 成功{}", attendeeIds.size(), sent);
+ return sent;
+ }
+
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java
index 59c71ae..0b8fc24 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java
@@ -1,20 +1,44 @@
package com.ruoyi.business.service.impl;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.business.domain.BizMeeting;
import com.ruoyi.business.domain.BizMeetingMaterial;
+import com.ruoyi.business.mapper.BizMeetingMapper;
import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
import com.ruoyi.business.service.IBizMeetingMaterialService;
@Service
public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService {
+ /** 非发票 subType (与前端 MeetingDetail.NON_OCR_SUBTYPES 对齐): 现场照片/签到表等不 OCR, 避免误识别脏 amount */
+ private static final Set NON_OCR_SUBTYPES = new HashSet<>(Arrays.asList(
+ "L_ENTERPRISE_BENEFIT", "L_SIGN_IN", "L_PANORAMA_FRONT", "L_PANORAMA_BACK", "L_EXPERT_PHOTO"));
+
+ /** 扫码拍照白名单 subType (公开端点只允许这三类照片回传) */
+ private static final Set CAMERA_SUBTYPES = new HashSet<>(Arrays.asList(
+ "L_SIGN_IN", "L_PANORAMA_FRONT", "L_PANORAMA_BACK"));
+
+ /** 可识别文件扩展名 (与前端 isRecognizable 对齐): 图片/PDF 才触发 OCR */
+ private static final Pattern RECOGNIZABLE = Pattern.compile("\\.(jpe?g|png|pdf)$", Pattern.CASE_INSENSITIVE);
+
@Autowired
private BizMeetingMaterialMapper bizMeetingMaterialMapper;
+ @Autowired
+ private BizMeetingMapper bizMeetingMapper;
@Override
public BizMeetingMaterial getById(Long id) {
@@ -34,10 +58,25 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
* 翻译成友好中文提示, 避免暴露 SQL 堆栈.
*
* 返回插入后的 list (各元素 id 字段被 useGeneratedKeys 回填), 前端可借此触发 OCR.
+ *
+ * 金额保留: 前端全删全插只传 ossUrl 不传 amount. 为避免"重新上传一张发票导致其余未变发票金额被清零",
+ * 先快照旧材料, 对 (subType + ossUrl) 未变的材料回填旧 amount 并置 fee_status=1 (无需重算);
+ * 新增/替换的会 OCR 发票置 fee_status=0 (等 OCR 回写金额后置 1).
*/
@Override
@Transactional(rollbackFor = Exception.class)
public List replaceByMeetingId(Long meetingId, List list) {
+ // 快照旧材料: subType -> 旧材料 (用于回填 amount + 判未变)
+ List oldList = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
+ Map oldBySubType = new HashMap<>();
+ if (oldList != null) {
+ for (BizMeetingMaterial o : oldList) {
+ if (o.getSubType() != null) {
+ oldBySubType.put(o.getSubType(), o);
+ }
+ }
+ }
+
bizMeetingMaterialMapper.deleteByMeetingId(meetingId);
if (list == null || list.isEmpty()) {
return list;
@@ -46,6 +85,18 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
for (BizMeetingMaterial m : list) {
m.setId(null);
m.setMeetingId(meetingId);
+ BizMeetingMaterial old = oldBySubType.get(m.getSubType());
+ boolean unchanged = old != null && Objects.equals(old.getOssUrl(), m.getOssUrl());
+ if (unchanged) {
+ // 未变: 回填旧金额, 已计算; 同时保留脱敏版 URL (签到表高斯模糊版, 前端全删全插不传 extraOssUrl)
+ m.setAmount(old.getAmount());
+ m.setFeeStatus(1);
+ m.setExtraOssUrl(old.getExtraOssUrl());
+ } else {
+ // 新增/替换: 金额清零, 会 OCR 的发票标记待计算
+ m.setAmount(null);
+ m.setFeeStatus(needsOcr(m) ? 0 : 1);
+ }
}
try {
bizMeetingMaterialMapper.insertBatch(list);
@@ -56,8 +107,68 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
}
@Override
- public int updateAmount(Long materialId, java.math.BigDecimal amount) {
+ public int updateAmount(Long materialId, BigDecimal amount) {
if (materialId == null || amount == null) return 0;
return bizMeetingMaterialMapper.updateAmount(materialId, amount);
}
-}
\ No newline at end of file
+
+ @Override
+ public int updateFeeStatus(Long materialId, Integer feeStatus) {
+ if (materialId == null || feeStatus == null) return 0;
+ return bizMeetingMaterialMapper.updateFeeStatus(materialId, feeStatus);
+ }
+
+ /**
+ * 扫码拍照回传: ry-h5 手机端拍照直传 OSS 后回传 URL, 按 (meetingId, subType) upsert 单行.
+ * 公开端点 (匿名) — 白名单 subType + 会议存在校验兜底.
+ * 照片类 NON_OCR, 不触发 OCR, 不影响会议费用, 故不 markFeeCalcPending.
+ * extraOssUrl: 签到表(L_SIGN_IN)拍照时额外生成的高斯模糊版 URL, sponsor 只看这个; 其他 subType 传空.
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void upsertFromCamera(Long meetingId, String subType, String ossUrl, String extraOssUrl) {
+ if (meetingId == null) throw new ServiceException("缺少 meetingId");
+ if (subType == null || !CAMERA_SUBTYPES.contains(subType)) throw new ServiceException("非法的拍照类型");
+ if (ossUrl == null || ossUrl.isEmpty()) throw new ServiceException("缺少照片 URL");
+
+ BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
+ if (meeting == null) throw new ServiceException("会议不存在");
+
+ // 找该 subType 现有行 (同会议同 subType 唯一)
+ BizMeetingMaterial row = null;
+ List existing = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
+ if (existing != null) {
+ for (BizMeetingMaterial m : existing) {
+ if (subType.equals(m.getSubType())) { row = m; break; }
+ }
+ }
+
+ if (row != null) {
+ // 更新 ossUrl (updateByPrimaryKey 只改非空字段, 不动 amount/fee_status)
+ BizMeetingMaterial upd = new BizMeetingMaterial();
+ upd.setId(row.getId());
+ upd.setOssUrl(ossUrl);
+ upd.setExtraOssUrl(extraOssUrl);
+ bizMeetingMaterialMapper.updateByPrimaryKey(upd);
+ } else {
+ BizMeetingMaterial ins = new BizMeetingMaterial();
+ ins.setMeetingId(meetingId);
+ ins.setMaterialType(subType.startsWith("L_") ? "LABOR" : "SERVICE");
+ ins.setSubType(subType);
+ ins.setOssUrl(ossUrl);
+ ins.setExtraOssUrl(extraOssUrl);
+ ins.setCreateTime(new Date());
+ bizMeetingMaterialMapper.insert(ins);
+ }
+ }
+
+ /**
+ * 是否会被前端提交 OCR (与 MeetingDetail.saveMaterials 门控一致):
+ * 文件可识别 (jpg/jpeg/png/pdf) 且 不在非发票 subType 集合里.
+ */
+ private boolean needsOcr(BizMeetingMaterial m) {
+ if (m == null || m.getOssUrl() == null || m.getOssUrl().isEmpty()) return false;
+ if (m.getSubType() != null && NON_OCR_SUBTYPES.contains(m.getSubType())) return false;
+ return RECOGNIZABLE.matcher(m.getOssUrl()).find();
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java
index 658e4b3..076cbd9 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java
@@ -12,6 +12,7 @@ import com.ruoyi.business.mapper.BizMeetingExecutorMapper;
import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
import com.ruoyi.business.mapper.BizMeetingAuditLogMapper;
import com.ruoyi.business.service.IBizMeetingService;
+import com.ruoyi.common.enums.BizMeetingStageEnum;
import com.ruoyi.common.utils.id.IdGenerator;
@Service
@@ -43,6 +44,18 @@ public class BizMeetingServiceImpl implements IBizMeetingService
if (entity.getBusinessId() == null || entity.getBusinessId().isEmpty()) {
entity.setBusinessId(String.valueOf(IdGenerator.generateId()));
}
+ // 新建会议初始状态: DB 列默认值 '0' 会让前端 stageLabel 显示成 0 而不是 enum 项, 这里显式兜底成 enum.
+ // current_stage = NOT_STARTED (未执行, 等 scheduler 过 startTime 置 is_executed 转 RUNNING)
+ // 材料/凭证审核子状态 = NOT_SUBMITTED (未提交, 供执行方 submit-material/submit-voucher 校验)
+ if (entity.getCurrentStage() == null || entity.getCurrentStage().isEmpty()) {
+ entity.setCurrentStage(BizMeetingStageEnum.NOT_STARTED.getCode());
+ }
+ if (entity.getMaterialAuditStage() == null || entity.getMaterialAuditStage().isEmpty()) {
+ entity.setMaterialAuditStage("NOT_SUBMITTED");
+ }
+ if (entity.getVoucherAuditStage() == null || entity.getVoucherAuditStage().isEmpty()) {
+ entity.setVoucherAuditStage("NOT_SUBMITTED");
+ }
return bizMeetingMapper.insert(entity);
}
@Override
@@ -79,4 +92,15 @@ public class BizMeetingServiceImpl implements IBizMeetingService
if (id != null) softDeleteCascade(id);
}
}
+
+ @Override
+ public int countByProjectId(Long projectId)
+ { return bizMeetingMapper.countByProjectId(projectId); }
+
+ @Override
+ public void markFeeCalcPending(Long meetingId) {
+ if (meetingId != null) {
+ bizMeetingMapper.markFeeCalcPending(meetingId);
+ }
+ }
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectExecutorAssignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectExecutorAssignServiceImpl.java
new file mode 100644
index 0000000..e8970da
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectExecutorAssignServiceImpl.java
@@ -0,0 +1,57 @@
+package com.ruoyi.business.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.business.domain.BizProjectExecutorAssign;
+import com.ruoyi.business.mapper.BizProjectExecutorAssignMapper;
+import com.ruoyi.business.service.IBizProjectExecutorAssignService;
+
+@Service
+public class BizProjectExecutorAssignServiceImpl implements IBizProjectExecutorAssignService {
+ @Autowired
+ private BizProjectExecutorAssignMapper mapper;
+
+ @Override
+ public int insertAssign(BizProjectExecutorAssign entity) {
+ // 执行方分配策略: 先按 project_id 删, 再插
+ mapper.deleteByProjectId(entity.getProjectId());
+ return mapper.insertAssign(entity);
+ }
+
+ /**
+ * 多执行人分配 (一个项目 ↔ N 执行人).
+ *
+ * 策略: 先按 project_id 物理删除旧分配, 再逐个插入.
+ * 注意 ⚠️ 不能直接复用 insertAssign() — insertAssign() 内部会 deleteByProjectId, 循环里第二次 delete 会把刚 insert 的清掉.
+ */
+ @Override
+ public int assignStaffForProject(BizProjectExecutorAssign body, java.util.List staffUserIds) {
+ if (body == null || body.getProjectId() == null) return 0;
+ if (staffUserIds == null || staffUserIds.isEmpty()) return 0;
+ // 一次性清旧, 不在循环里清
+ mapper.deleteByProjectId(body.getProjectId());
+ int inserted = 0;
+ for (Long sid : staffUserIds) {
+ BizProjectExecutorAssign item = new BizProjectExecutorAssign();
+ item.setProjectId(body.getProjectId());
+ item.setStaffUserId(sid);
+ item.setAssignDesc(body.getAssignDesc());
+ item.setAssignPoints(body.getAssignPoints());
+ item.setCreateBy(body.getCreateBy());
+ item.setExecutorUserId(body.getExecutorUserId());
+ inserted += mapper.insertAssign(item);
+ }
+ return inserted;
+ }
+
+ @Override
+ public List listByProjectId(String projectId) {
+ return mapper.selectByProjectId(projectId);
+ }
+
+ @Override
+ public int deleteByProjectId(String projectId) {
+ return mapper.deleteByProjectId(projectId);
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java
index 1d8a537..0203834 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java
@@ -9,6 +9,7 @@ import com.ruoyi.business.mapper.BizProjectMapper;
import com.ruoyi.business.mapper.BizProjectPlanMapper;
import com.ruoyi.business.mapper.BizProjectAssignMapper;
import com.ruoyi.business.mapper.BizProjectSponsorAssignMapper;
+import com.ruoyi.business.mapper.BizProjectExecutorAssignMapper;
import com.ruoyi.business.mapper.BizProjectRatingMapper;
import com.ruoyi.business.mapper.BizMeetingMapper;
import com.ruoyi.business.service.IBizProjectService;
@@ -27,6 +28,8 @@ public class BizProjectServiceImpl implements IBizProjectService
@Autowired
private BizProjectSponsorAssignMapper bizProjectSponsorAssignMapper;
@Autowired
+ private BizProjectExecutorAssignMapper bizProjectExecutorAssignMapper;
+ @Autowired
private BizProjectRatingMapper bizProjectRatingMapper;
@Autowired
private BizMeetingMapper bizMeetingMapper;
@@ -46,6 +49,9 @@ public class BizProjectServiceImpl implements IBizProjectService
public List selectExecutorList(BizProject entity)
{ return bizProjectMapper.selectExecutorList(entity); }
@Override
+ public List selectExecutorStaffList(BizProject entity)
+ { return bizProjectMapper.selectExecutorStaffList(entity); }
+ @Override
// 注: biz_project.project_id 用 DB AUTO_INCREMENT, 不需要 SnowflakeId 注入;
// 项目 ID 用 Long 后, SnowflakeId.injectIfEmpty 反射 setProjectId(String) 会 NoSuchMethodException 被吞掉 (SnowflakeId.java:30-31), 行为安全.
// create_user_id 走当前登录用户 (前台 API 无 @DataScope, 不会被过滤; 后台 @PreAuthorize 受角色限制)
@@ -97,6 +103,8 @@ public class BizProjectServiceImpl implements IBizProjectService
bizProjectAssignMapper.softDeleteByProjectId(projectId);
// 5) sponsor assign (String)
bizProjectSponsorAssignMapper.softDeleteByProjectId(String.valueOf(projectId));
+ // 5.5) executor assign (String)
+ bizProjectExecutorAssignMapper.softDeleteByProjectId(String.valueOf(projectId));
// 6) rating
bizProjectRatingMapper.softDeleteByProjectId(projectId);
// 7) 会议链: 查项目下所有 meeting → 调 BizMeetingService.softDeleteCascadeBatch
@@ -115,4 +123,19 @@ public class BizProjectServiceImpl implements IBizProjectService
if (id != null) softDeleteCascade(id);
}
}
+
+ @Override
+ public int countAssignedSessions(Long projectId, Long executorUserId)
+ { return bizProjectMapper.countAssignedSessions(projectId, executorUserId); }
+
+ @Override
+ public boolean isExecutorOfProject(Long projectId, Long userId)
+ { return bizProjectMapper.countExecutorOfProject(projectId, userId) > 0; }
+
+ /** 会议结算后重算项目金额 (全量 SUM 已结算会议, 幂等) */
+ @Override
+ public void recomputeSettledAmounts(Long projectId) {
+ if (projectId == null) return;
+ bizProjectMapper.recomputeSettledAmounts(projectId);
+ }
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java
index c47456f..2b547f2 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java
@@ -19,6 +19,32 @@ public class BizProjectSponsorAssignServiceImpl implements IBizProjectSponsorAss
return mapper.insertAssign(entity);
}
+ /**
+ * 多监察员分配 (一个项目 ↔ N 监察员).
+ *
+ * 策略: 先按 project_id 物理删除旧分配, 再逐个插入 (事务内由 Service 默认单 insert 即可, 失败单条不影响其它).
+ * 注意 ⚠️ 不能直接复用 insertAssign() — insertAssign() 内部会 deleteByProjectId, 循环里第二次 delete 会把刚 insert 的清掉, 提交后只剩最后 1 条.
+ */
+ @Override
+ public int assignMonitorsForProject(BizProjectSponsorAssign body, java.util.List monitorUserIds) {
+ if (body == null || body.getProjectId() == null) return 0;
+ if (monitorUserIds == null || monitorUserIds.isEmpty()) return 0;
+ // 一次性清旧, 不在循环里清
+ mapper.deleteByProjectId(body.getProjectId());
+ int inserted = 0;
+ for (Long mid : monitorUserIds) {
+ BizProjectSponsorAssign item = new BizProjectSponsorAssign();
+ item.setProjectId(body.getProjectId());
+ item.setMonitorUserId(mid);
+ item.setAssignDesc(body.getAssignDesc());
+ item.setAssignPoints(body.getAssignPoints());
+ item.setCreateBy(body.getCreateBy());
+ item.setSponsorUserId(body.getSponsorUserId());
+ inserted += mapper.insertAssign(item);
+ }
+ return inserted;
+ }
+
@Override
public List listByProjectId(String projectId) {
return mapper.selectByProjectId(projectId);
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java
index 13a14e7..168d290 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java
@@ -1,6 +1,11 @@
package com.ruoyi.business.service.impl;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+
import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -11,9 +16,11 @@ import com.ruoyi.business.domain.BizExpert;
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
import com.ruoyi.business.domain.BizMeeting;
import com.ruoyi.business.domain.BizMeetingAttendee;
+import com.ruoyi.business.domain.BizProject;
import com.ruoyi.business.mapper.BizExpertMapper;
import com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper;
import com.ruoyi.business.mapper.BizMeetingMapper;
+import com.ruoyi.business.mapper.BizProjectMapper;
import com.ruoyi.business.service.BizSignService;
import com.ruoyi.business.service.IBizMeetingAttendeeService;
import com.ruoyi.business.service.PdfService;
@@ -33,6 +40,8 @@ public class BizSignServiceImpl implements BizSignService {
private BizLaborProtocolTemplateMapper templateMapper;
@Autowired
private PdfService pdfService;
+ @Autowired
+ private BizProjectMapper projectMapper;
@Override
public Map getSignInfo(Long attendeeId) {
@@ -79,10 +88,37 @@ public class BizSignServiceImpl implements BizSignService {
current.put("tax", attendee.getTax());
current.put("fee", attendee.getFee());
- List