com.aliyun
aliyun-java-sdk-core
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 97b31e4..d45a96f 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
@@ -38,4 +38,15 @@ public class OcrExecutorConfig
{
return Executors.newFixedThreadPool(8);
}
+
+ /**
+ * zip 批量上传执行器 (劳务协议 / 专家照片 / 会务材料): 解压 zip + 逐文件上传 OSS (网络 IO 密集).
+ *
+ * 上传接口改为"提交即返回 jobId" (处理移后台), 前端轮询进度; 4 线程并发处理不同用户的上传任务.
+ */
+ @Bean(name = "zipUploadExecutor", destroyMethod = "shutdown")
+ public ExecutorService zipUploadExecutor()
+ {
+ return Executors.newFixedThreadPool(4);
+ }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
index 9cb6489..020809d 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
@@ -281,7 +281,7 @@ public class BizAuthController extends BaseController {
self.setUpdateBy(username);
bizPersonMapper.insert(self);
- return success("注册成功, 请等待审核").put("userId", userId).put("orgId", orgId);
+ return success("注册成功").put("userId", userId).put("orgId", orgId);
}
/**
@@ -384,6 +384,6 @@ public class BizAuthController extends BaseController {
self.setUpdateBy(username);
bizPersonMapper.insert(self);
- return success("注册成功, 请等待审核").put("userId", userId).put("orgId", orgId);
+ return success("注册成功").put("userId", userId).put("orgId", orgId);
}
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizInviteController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizInviteController.java
new file mode 100644
index 0000000..bd3a255
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizInviteController.java
@@ -0,0 +1,90 @@
+package com.ruoyi.business.controller;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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.BizInvite;
+import com.ruoyi.business.domain.BizInviteRecipient;
+import com.ruoyi.business.service.IBizInviteService;
+
+/**
+ * 发送邀请 Controller (manager 工作台).
+ *
独立于项目/会议, 项目编号为手输文本. manager 是业务角色, 不走 RuoYi RBAC 权限.
+ */
+@RestController
+@RequestMapping("/business/invite")
+public class BizInviteController extends BaseController
+{
+ @Autowired
+ private IBizInviteService inviteService;
+
+ @GetMapping("/list")
+ public TableDataInfo list(BizInvite bizInvite)
+ {
+ startPage();
+ List list = inviteService.selectList(bizInvite);
+ return getDataTable(list);
+ }
+
+ @GetMapping("/{id}")
+ public AjaxResult getInfo(@PathVariable("id") Long id)
+ {
+ BizInvite invite = inviteService.getById(id);
+ if (invite == null)
+ {
+ return error("邀请不存在");
+ }
+ Map data = new HashMap<>();
+ data.put("invite", invite);
+ data.put("recipients", inviteService.listRecipients(id));
+ return success(data);
+ }
+
+ /**
+ * 新建邀请 (仅保存草稿, 不发送).
+ * 返回 {id}, 前端拿 data.id 继续编辑或调用 /{id}/send 发送.
+ */
+ @Log(title = "发送邀请", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody BizInvite bizInvite)
+ {
+ Long id = inviteService.create(bizInvite, false);
+ Map data = new HashMap<>();
+ data.put("id", id);
+ return success(data);
+ }
+
+ /**
+ * 发送邀请 (对 remark=1 的收件人触发短信/邮件), 返回实际发送条数.
+ */
+ @Log(title = "发送邀请", businessType = BusinessType.UPDATE)
+ @PostMapping("/{id}/send")
+ public AjaxResult send(@PathVariable("id") Long id)
+ {
+ try
+ {
+ int sent = inviteService.send(id);
+ Map data = new HashMap<>();
+ data.put("sent", sent);
+ return success(data);
+ }
+ catch (RuntimeException e)
+ {
+ return error(e.getMessage());
+ }
+ }
+
+ @Log(title = "发送邀请", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public AjaxResult remove(@PathVariable Long[] ids)
+ {
+ return toAjax(inviteService.deleteByPrimaryKeys(ids));
+ }
+}
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 29320a9..3b8369f 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
@@ -10,6 +10,7 @@ import org.springframework.web.multipart.MultipartFile;
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.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
@@ -54,13 +55,15 @@ public class BizMeetingAttendeeController extends BaseController {
/**
* 当前登录用户的"待签署协议"列表 (已推送电子签 is_esigned=1 且 任一未签)
- * 用于 /doctor/home 工作台
+ * 用于 /doctor/home 工作台, 支持分页 (前端按 5 条/页)
+ * pageNum / pageSize 从 request param 读 (RuoYi 标准做法, TableSupport 自动解析)
*/
@GetMapping("/unsigned")
- public AjaxResult listUnsigned() {
+ public TableDataInfo listUnsigned() {
Long userId = SecurityUtils.getUserId();
+ startPage(); // PageHelper 仅拦截同一线程的下一条 select, 所以放 service.list() 之前
List rows = attendeeService.selectUnsignedByUserId(userId);
- return success(rows);
+ return getDataTable(rows);
}
/**
@@ -342,7 +345,9 @@ public class BizMeetingAttendeeController extends BaseController {
@PostMapping("/uploadAgreements")
public AjaxResult uploadAgreements(@RequestParam("file") MultipartFile file,
@RequestParam("meetingId") Long meetingId) throws Exception {
- Map result = attendeeService.uploadAgreements(file, meetingId);
+ String jobId = attendeeService.submitAgreementUpload(file, meetingId);
+ Map result = new HashMap<>();
+ result.put("jobId", jobId);
return success(result);
}
@@ -367,7 +372,9 @@ public class BizMeetingAttendeeController extends BaseController {
@PostMapping("/uploadExpertPhotos")
public AjaxResult uploadExpertPhotos(@RequestParam("file") MultipartFile file,
@RequestParam("meetingId") Long meetingId) throws Exception {
- Map result = attendeeService.uploadExpertPhotos(file, meetingId);
+ String jobId = attendeeService.submitExpertPhotoUpload(file, meetingId);
+ Map result = new HashMap<>();
+ result.put("jobId", jobId);
return success(result);
}
@@ -406,13 +413,14 @@ public class BizMeetingAttendeeController extends BaseController {
a.setIdCard(maskIdCard(a.getIdCard()));
}
- /** 姓名脱敏: 保留首字 (姓), 其余打码 (张三 → 张*) */
+ /** 姓名脱敏: 保留首尾, 中间打码 (张三 → 张*, 李小明 → 李*明) */
private String maskName(String v) {
if (v == null) return null;
String s = v.trim();
if (s.isEmpty()) return v;
- if (s.length() <= 1) return "*";
- return s.charAt(0) + "*".repeat(s.length() - 1);
+ if (s.length() == 1) return "*";
+ if (s.length() == 2) return s.charAt(0) + "*";
+ return s.charAt(0) + "*".repeat(s.length() - 2) + s.charAt(s.length() - 1);
}
/** 手机号脱敏: 前3 + **** + 后4 */
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 197c58c..e7fbba2 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
@@ -74,11 +74,18 @@ public class BizMeetingController extends BaseController {
private PosterService posterService;
@GetMapping("/list")
- public TableDataInfo list(BizMeeting bizMeeting) {
+ public TableDataInfo list(BizMeeting bizMeeting,
+ @RequestParam(value = "signedStatus", required = false) String signedStatus) {
Long uid = SecurityUtils.getUserId();
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
- // 医生/专家角色: 后端兜底只查"我参加的会议" (走 biz_meeting_attendee 中间表)
- if ("doctor".equals(roleType) || "expert".equals(roleType)) {
+ // doctor 列表页"签署状态"下拉: unsigned=待签署 / signed=已签署 / 其他=不过滤
+ // URL 不会自动绑到 BaseEntity.params, controller 显式注入供 mapper 读取
+ if (signedStatus != null && !signedStatus.isEmpty()
+ && ("unsigned".equals(signedStatus) || "signed".equals(signedStatus))) {
+ bizMeeting.getParams().put("signedStatus", signedStatus);
+ }
+ // 医生角色: 后端兜底只查"我参加的会议" (走 biz_meeting_attendee 中间表)
+ if ("doctor".equals(roleType)) {
bizMeeting.setUserId(uid);
}
// sponsor 数据权限: 只看"我的项目"下的会议 (MAIN 走 sponsor_org_id, SUB 走 sponsor_assign.monitor_user_id).
@@ -196,6 +203,8 @@ public class BizMeetingController extends BaseController {
validateMeetingWithinProject(bizMeeting, proj);
}
}
+ // 物理阶段初值: 建会即落 current_stage (NOT_STARTED / IN_PROGRESS), 否则 DB 默认 '0' 使项目列表「未执行」统计永远 0
+ bizMeeting.setCurrentStage(computePhysicalStage(bizMeeting));
int rows = bizMeetingService.insert(bizMeeting);
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
@@ -218,6 +227,26 @@ public class BizMeetingController extends BaseController {
}
}
+ /**
+ * 会议物理阶段 (含时间窗口): 事实推导 + start_time~end_time 窗口补 IN_PROGRESS.
+ * 用于建会/编辑时落 current_stage 初值 — 否则建会落 DB 默认 '0'、编辑时间后残留旧值,
+ * 项目列表「未执行」统计 (current_stage='NOT_STARTED') 会与会议列表「未执行」展示 (时间实时推导) 错位.
+ *
{@link StageDeriver#derivePhysicalStage} 只产 NOT_STARTED/RUNNING 不产 IN_PROGRESS
+ * (时间窗口态原由 scheduler 写), 此处补上窗口, 让 add/edit 一落地即准确.
+ */
+ private String computePhysicalStage(BizMeeting m) {
+ String stage = stageDeriver.derivePhysicalStage(m);
+ // 仅「未执行 + 无材料」这一物理态需要补时间窗口: start_time 已到且 end_time 未到 → 执行中
+ if ("NOT_STARTED".equals(stage)) {
+ Date now = new Date();
+ if (m.getStartTime() != null && !m.getStartTime().after(now)
+ && (m.getEndTime() == null || m.getEndTime().after(now))) {
+ stage = "IN_PROGRESS";
+ }
+ }
+ return stage;
+ }
+
@Log(title = "会议", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody BizMeeting bizMeeting) {
@@ -258,6 +287,24 @@ public class BizMeetingController extends BaseController {
validateMeetingWithinProject(bizMeeting, proj);
}
}
+ // 编辑: 时间变更后重算物理阶段 (加载完整事实, 用请求中的新时间覆盖后再推导),
+ // 否则 start_time 推到未来后 current_stage 残留 IN_PROGRESS 等旧值, 项目列表「未执行」统计错位.
+ BizMeeting existing = bizMeetingService.getById(bizMeeting.getMeetingId());
+ if (existing != null) {
+ Date oldEndTime = existing.getEndTime();
+ if (bizMeeting.getStartTime() != null) existing.setStartTime(bizMeeting.getStartTime());
+ if (bizMeeting.getEndTime() != null) existing.setEndTime(bizMeeting.getEndTime());
+ bizMeeting.setCurrentStage(computePhysicalStage(existing));
+ // 结束时间变更 → 重算提交截止时间 (end_time + 项目天数), 让冻结判定跟着新时间走 (否则改时间后 deadline 仍是建会时的旧值, 冻结不触发).
+ // 已冻结 / 已退回 (锚点=退回时刻) 的不重算, 避免错误缩短整改窗口.
+ boolean endChanged = bizMeeting.getEndTime() != null && !bizMeeting.getEndTime().equals(oldEndTime);
+ boolean frozen = existing.getIsFrozen() != null && existing.getIsFrozen() == 1;
+ boolean rejected = "REJECTED".equals(existing.getLaborAuditStage())
+ || "REJECTED".equals(existing.getServiceAuditStage());
+ if (endChanged && !frozen && !rejected) {
+ bizMeeting.setSubmitDeadline(computeSubmitDeadline(existing.getProjectId(), bizMeeting.getEndTime()));
+ }
+ }
int rows = bizMeetingService.updateByPrimaryKey(bizMeeting);
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
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 5ed7b55..8ec6cd2 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
@@ -1,7 +1,9 @@
package com.ruoyi.business.controller;
import java.io.InputStream;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -92,12 +94,12 @@ public class BizMeetingMaterialController extends BaseController {
/**
* 扫码拍照回传 (公开端点, ry-h5 手机端拍照直传 OSS 后回传 URL).
*
- * body: { meetingId, subType, ossUrl }. 后端白名单 subType + 会议存在校验.
- * 照片类 NON_OCR, 不影响会议费用.
+ * body: { meetingId, subType, ossUrls[], extraOssUrls[] }. 后端白名单 subType + 会议存在校验.
+ * 签到表连拍多张传多个 URL, 前后全景传单个. 照片类 NON_OCR, 不影响会议费用.
*/
@PostMapping("/cameraUpload")
public AjaxResult cameraUpload(@RequestBody BizMeetingMaterial body) {
- bizMeetingMaterialService.upsertFromCamera(body.getMeetingId(), body.getSubType(), body.getOssUrl(), body.getExtraOssUrl());
+ bizMeetingMaterialService.upsertFromCamera(body.getMeetingId(), body.getSubType(), body.getOssUrls(), body.getExtraOssUrls());
return success();
}
@@ -197,12 +199,25 @@ public class BizMeetingMaterialController extends BaseController {
@PostMapping("/uploadServiceMaterials")
public AjaxResult uploadServiceMaterials(@RequestParam("file") MultipartFile file,
@RequestParam("meetingId") Long meetingId) throws Exception {
- int updated = bizMeetingMaterialService.uploadServiceMaterials(file, meetingId);
- // 仅当真有材料被替换 (内容变化, updated>0) 才立即重算会务费; 全量未变不置"统计中"
- if (updated > 0) {
- bizMeetingService.recomputeMeetingFee(meetingId);
- }
- return success(updated);
+ String jobId = bizMeetingMaterialService.submitServiceMaterialsUpload(file, meetingId);
+ Map result = new HashMap<>();
+ result.put("jobId", jobId);
+ return success(result);
+ }
+
+ /**
+ * 电子签到表 (L_ESIGN_IN) 多文件/zip 上传 (同步, 追加式保存).
+ *
+ * file 为单个 Excel (xls/xlsx/csv) 或 zip (后台解压并分析其中所有 Excel).
+ * 后端逐个解析脱敏 + 直传 OSS, 单行 L_ESIGN_IN 存多文件 (oss_url 存 JSON 数组).
+ * 已有文件保留, 新文件追加到末尾. 任一文件解析失败 → 整体失败. 仅材料可编辑角色可上传.
+ */
+ @Log(title = "电子签到表上传", businessType = BusinessType.UPDATE)
+ @PostMapping("/uploadEsignIn")
+ public AjaxResult uploadEsignIn(@RequestParam("file") MultipartFile file,
+ @RequestParam("meetingId") Long meetingId) throws Exception {
+ Map result = bizMeetingMaterialService.uploadEsignIn(file, meetingId);
+ return success(result);
}
/** request body for batch download endpoints */
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java
index 6664379..f9c4584 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java
@@ -69,14 +69,15 @@ public class BizOrgController extends BaseController {
}
/**
- * 当前登录 sponsor 的所属公司 (供 /sponsor/account 页面回显)
- * 返回 { orgId, orgName, isOwner: 1|0 }
- * 主账号 isOwner=1 可改名, 子账号 isOwner=0 只读
+ * 当前登录账号的所属公司 (sponsor + executor 共用, 供 /sponsor/account + /executor/account 页面回显)
+ * 根据 sys_user.role_type 自动选 sponsor/executor
+ * 返回 { orgId, orgName, orgType, isOwner: 1|0 }
+ * 注: 账号信息页只读展示, 主账号也不可改单位名称
*/
@GetMapping("/myCompany")
public AjaxResult myCompany() {
Long uid = SecurityUtils.getUserId();
- Map data = bizOrgService.selectMySponsorCompany(uid);
+ Map data = bizOrgService.selectMyCompany(uid);
return success(data);
}
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 8ba151c..f0567b0 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
@@ -31,6 +31,7 @@ import com.ruoyi.business.service.IBizExpertService;
import com.ruoyi.business.service.IBizOrgService;
import com.ruoyi.business.service.IBizProjectService;
import com.ruoyi.business.service.IBizExecutionIntentService;
+import com.ruoyi.business.service.IBizMeetingService;
import com.ruoyi.business.domain.BizProjectSponsorAssign;
import com.ruoyi.business.domain.BizProjectExecutorAssign;
import com.ruoyi.business.notify.BizNotifyService;
@@ -75,6 +76,8 @@ public class BizProjectController extends BaseController
private IBizOrgService bizOrgService;
@Autowired
private IBizExpertService bizExpertService;
+ @Autowired
+ private IBizMeetingService bizMeetingService;
/**
* 我报名的项目 (当前用户在 biz_execution_intent 里有意向的项目)
@@ -230,7 +233,22 @@ public class BizProjectController extends BaseController
@PutMapping
public AjaxResult edit(@RequestBody BizProject bizProject)
{
- return toAjax(bizProjectService.updateByPrimaryKey(bizProject));
+ // submit_deadline_days 变更 → 级联重算该项目存量「从未退回、且仍有轨未提交」会议的提交截止时间.
+ // 建会时 deadline 一次性落库, 改项目天数本不影响已有会议; 这里补上级联, 避免"改了天数会议却不按新天数冻结".
+ Integer newDays = bizProject.getSubmitDeadlineDays();
+ boolean daysChanged = false;
+ if (bizProject.getProjectId() != null && newDays != null)
+ {
+ BizProject old = bizProjectService.getById(bizProject.getProjectId());
+ Integer oldDays = old == null ? null : old.getSubmitDeadlineDays();
+ daysChanged = !Objects.equals(oldDays, newDays);
+ }
+ int rows = bizProjectService.updateByPrimaryKey(bizProject);
+ if (daysChanged)
+ {
+ bizMeetingService.recomputeSubmitDeadlinesByProject(bizProject.getProjectId(), newDays);
+ }
+ return toAjax(rows);
}
@Log(title = "项目", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@@ -358,7 +376,7 @@ public class BizProjectController extends BaseController
if (projectId == null) throw new ServiceException("项目不存在");
BizProject p = bizProjectService.getById(projectId);
if (p == null) throw new ServiceException("项目不存在");
- if ("1".equals(p.getIsFinished())) throw new ServiceException("已结题项目不能分配");
+ if ("Y".equals(p.getIsFinished())) throw new ServiceException("已结题项目不能分配");
return p;
}
@@ -531,7 +549,7 @@ public class BizProjectController extends BaseController
return error("projectId 必填");
}
// 已结题项目不能分配
- requireAssignableProject(parseProjectId(body.getProjectId()));
+ requireAssignableProject(body.getProjectId());
java.util.List sids = body.getStaffUserIds();
if (sids == null || sids.isEmpty()) {
// 向后兼容: 单值 staffUserId
@@ -551,11 +569,13 @@ public class BizProjectController extends BaseController
/**
* 查询项目已分配的执行人列表 (供前端 dialog 重开时回显)
* GET /business/project/{projectId}/executorAssigns
+ * 按当前登录执行方的 executor_org_id 隔离, 只回显本执行方自己的执行人 (避免串读别家执行人显示成裸 user_id)
*/
@GetMapping("/{projectId}/executorAssigns")
- public AjaxResult listExecutorAssigns(@PathVariable("projectId") String projectId)
+ public AjaxResult listExecutorAssigns(@PathVariable("projectId") Long projectId)
{
- return success(bizProjectExecutorAssignService.listByProjectId(projectId));
+ Long executorOrgId = bizOrgService.selectOrgIdByUserId(SecurityUtils.getUserId());
+ return success(bizProjectExecutorAssignService.listByProjectId(projectId, executorOrgId));
}
/**
@@ -659,7 +679,7 @@ public class BizProjectController extends BaseController
v.setSponsorOrgName(p.getSponsorOrgName());
v.setExecOrgNames(p.getExecOrgNames());
v.setProjectForm(p.getProjectForm());
- v.setIsFinished("1".equals(p.getIsFinished()) ? "已结题" : "未结题");
+ v.setIsFinished("Y".equals(p.getIsFinished()) ? "已结题" : "未结题");
v.setStartTime(p.getStartTime());
v.setEndTime(p.getEndTime());
exportList.add(v);
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java
index d35250c..0940da4 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java
@@ -5,14 +5,11 @@ import java.util.List;
import java.util.Map;
import java.net.URLEncoder;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import org.springframework.core.env.Environment;
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.utils.http.HttpUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -23,10 +20,13 @@ import com.ruoyi.business.domain.BizProject;
import com.ruoyi.business.domain.BizSupportLetter;
import com.ruoyi.business.domain.BizInvitation;
import com.ruoyi.business.domain.BizProjectPlan;
+import com.ruoyi.business.domain.BizInvite;
+import com.ruoyi.business.domain.BizInviteRecipient;
import com.ruoyi.business.service.IBizArticleService;
import com.ruoyi.business.service.IBizProjectService;
import com.ruoyi.business.service.IBizSupportLetterService;
import com.ruoyi.business.service.IBizInvitationService;
+import com.ruoyi.business.service.IBizInviteService;
import com.ruoyi.business.service.IBizProjectPlanService;
import com.ruoyi.business.service.IBizSpecialPlanService;
import com.ruoyi.business.service.WxShareService;
@@ -42,6 +42,7 @@ public class BizPublicController extends BaseController {
@Autowired private IBizProjectService projectService;
@Autowired private IBizSupportLetterService supportLetterService;
@Autowired private IBizInvitationService invitationService;
+ @Autowired private IBizInviteService inviteService;
@Autowired private IBizProjectPlanService projectPlanService;
@Autowired private IBizArticleService articleService;
@Autowired private IBizSpecialPlanService specialPlanService;
@@ -85,7 +86,7 @@ public class BizPublicController extends BaseController {
public AjaxResult index() {
Map map = new HashMap<>();
BizProject publishedQuery = new BizProject();
- publishedQuery.setIsPublished("1");
+ publishedQuery.setIsPublished("Y");
List announcements = projectService.selectList(publishedQuery);
List plans = projectPlanService.selectList(new BizProjectPlan());
map.put("announcements", announcements);
@@ -95,10 +96,11 @@ public class BizPublicController extends BaseController {
}
@GetMapping("/announcements")
- public AjaxResult announcements() {
- // 公示列表按发布时间倒序 (后端排序), 只返回已发布项目
- List list = projectService.selectPublicAnnouncements();
- return success(list);
+ public TableDataInfo announcements(@RequestParam(required = false) String keyword)
+ {
+ // 公示列表: 只查公示页展示列 (不跑关联子查询), PageHelper 分页, keyword 按项目名模糊
+ startPage();
+ return getDataTable(projectService.selectPublicAnnouncements(keyword));
}
@GetMapping("/project/{projectId}")
@@ -183,4 +185,38 @@ public class BizPublicController extends BaseController {
public AjaxResult apply(String annId) {
return success();
}
+
+ /**
+ * 公开邀请函 (发送邀请 短信/邮件里的链接): 按 token 查邀请内容 + 收件人确认状态.
+ * 无需登录, token 即唯一凭证.
+ */
+ @GetMapping("/invite/{token}")
+ public AjaxResult inviteByToken(@PathVariable("token") String token) {
+ BizInviteRecipient recipient = inviteService.getRecipientByToken(token);
+ if (recipient == null) {
+ return error("邀请链接无效");
+ }
+ BizInvite invite = inviteService.getById(recipient.getInviteId());
+ if (invite == null) {
+ return error("邀请不存在");
+ }
+ Map data = new HashMap<>();
+ data.put("invite", invite);
+ data.put("recipient", recipient);
+ return success(data);
+ }
+
+ /**
+ * 公开确认 (粘在底部的确认按钮): 置确认状态=已确认, 不可逆.
+ * 幂等: 重复确认返回成功 (已确认态不再翻转).
+ */
+ @PostMapping("/invite/{token}/confirm")
+ public AjaxResult inviteConfirm(@PathVariable("token") String token) {
+ BizInviteRecipient recipient = inviteService.getRecipientByToken(token);
+ if (recipient == null) {
+ return error("邀请链接无效");
+ }
+ inviteService.confirmByToken(token);
+ return success("确认成功");
+ }
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/UploadProgressController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/UploadProgressController.java
new file mode 100644
index 0000000..a353fea
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/UploadProgressController.java
@@ -0,0 +1,48 @@
+package com.ruoyi.business.controller;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.business.upload.UploadProgressRegistry;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+
+/**
+ * 批量上传进度轮询端点 (zip 上传 → 后台解压/回填 → 前端轮询)
+ */
+@RestController
+@RequestMapping("/business/upload")
+public class UploadProgressController extends BaseController
+{
+ @Autowired
+ private UploadProgressRegistry uploadProgressRegistry;
+
+ /**
+ * 查询某上传任务的实时进度.
+ *
+ * 返回 { jobId, done, total, finished, error, message, result };
+ * 任务不存在 (尚未登记或已清理) 返回 success() (data=null), 前端视为"准备中".
+ */
+ @GetMapping("/progress/{jobId}")
+ public AjaxResult progress(@PathVariable("jobId") String jobId)
+ {
+ UploadProgressRegistry.Progress p = uploadProgressRegistry.get(jobId);
+ if (p == null)
+ {
+ return success();
+ }
+ Map out = new HashMap<>();
+ out.put("jobId", p.jobId);
+ out.put("done", p.done);
+ out.put("total", p.total);
+ out.put("finished", p.finished);
+ out.put("error", p.error);
+ out.put("message", p.message);
+ out.put("result", p.result);
+ return success(out);
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizArticle.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizArticle.java
index 44b629d..e1d8529 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizArticle.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizArticle.java
@@ -24,8 +24,8 @@ public class BizArticle extends BaseEntity
@Excel(name = "正文")
private String content;
- /** 状态 0启用 1停用 */
- @Excel(name = "状态", readConverterExp = "0=启用,1=停用")
+ /** 状态 Y启用 N停用 */
+ @Excel(name = "状态", readConverterExp = "Y=启用,N=停用")
private String status;
public Long getId() { return id; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java
index 76921b2..f671ca3 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java
@@ -17,8 +17,8 @@ public class BizDepartment extends BaseEntity
@Excel(name = "排序")
private Integer sort;
- /** 0=启用 1=停用 */
- @Excel(name = "状态", readConverterExp = "0=启用,1=停用")
+ /** Y=启用 N=停用 */
+ @Excel(name = "状态", readConverterExp = "Y=启用,N=停用")
private String status;
public Long getDeptId() { return deptId; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInvite.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInvite.java
new file mode 100644
index 0000000..ecf5e26
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInvite.java
@@ -0,0 +1,68 @@
+package com.ruoyi.business.domain;
+
+import java.util.Date;
+import java.util.List;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 发送邀请 主表对象 biz_invite.
+ * 独立于项目/会议: 此时项目在 OA 尚未创建, 项目编号/会议名称/期数/会议时间 均为手输文本,
+ * 不关联 biz_project / biz_meeting, 也不复用 biz_invitation (那是另一套邀请函逻辑).
+ */
+public class BizInvite extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 主键 (雪花 ID, 调用方生成) */
+ private Long id;
+ /** 项目编号 (手输) */
+ private String projectNo;
+ /** 会议名称 (手输) */
+ private String meetingName;
+ /** 期数 (手输) */
+ private String periodNo;
+ /** 会议时间 (手输) */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date meetingTime;
+ /** 发送时间 (发送动作回填) */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date sendTime;
+ /** 邀请内容 (富文本 HTML) */
+ private String content;
+ /** 红头文件/附件 URL */
+ private String redheadFileUrl;
+ /** 发送渠道: sms / email / both */
+ private String sendChannel;
+ /** 邀请专家数 (聚合, 非库列) */
+ private Integer recipientCount;
+ /** 已确认数 (聚合, 非库列) */
+ private Integer confirmedCount;
+ /** 收件人明细 (前端提交用, 非库列) */
+ private List recipients;
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+ public String getProjectNo() { return projectNo; }
+ public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
+ public String getMeetingName() { return meetingName; }
+ public void setMeetingName(String meetingName) { this.meetingName = meetingName; }
+ public String getPeriodNo() { return periodNo; }
+ public void setPeriodNo(String periodNo) { this.periodNo = periodNo; }
+ public Date getMeetingTime() { return meetingTime; }
+ public void setMeetingTime(Date meetingTime) { this.meetingTime = meetingTime; }
+ public Date getSendTime() { return sendTime; }
+ public void setSendTime(Date sendTime) { this.sendTime = sendTime; }
+ public String getContent() { return content; }
+ public void setContent(String content) { this.content = content; }
+ public String getRedheadFileUrl() { return redheadFileUrl; }
+ public void setRedheadFileUrl(String redheadFileUrl) { this.redheadFileUrl = redheadFileUrl; }
+ public String getSendChannel() { return sendChannel; }
+ public void setSendChannel(String sendChannel) { this.sendChannel = sendChannel; }
+ public Integer getRecipientCount() { return recipientCount; }
+ public void setRecipientCount(Integer recipientCount) { this.recipientCount = recipientCount; }
+ public Integer getConfirmedCount() { return confirmedCount; }
+ public void setConfirmedCount(Integer confirmedCount) { this.confirmedCount = confirmedCount; }
+ public List getRecipients() { return recipients; }
+ public void setRecipients(List recipients) { this.recipients = recipients; }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInviteRecipient.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInviteRecipient.java
new file mode 100644
index 0000000..bab264a
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInviteRecipient.java
@@ -0,0 +1,64 @@
+package com.ruoyi.business.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 发送邀请 收件人明细对象 biz_invite_recipient.
+ */
+public class BizInviteRecipient extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 主键 (雪花 ID, 调用方生成) */
+ private Long id;
+ /** 邀请主表 id */
+ private Long inviteId;
+ /** 专家姓名 */
+ private String name;
+ /** 手机号 */
+ private String phone;
+ /** 邮箱 */
+ private String email;
+ /** 来源: E=专家库 / I=导入 */
+ private String source;
+ /** 备注: 1=发送 / 空=不发送 */
+ private String remark;
+ /** 发送状态: 0=未发送 / 1=成功 / 2=失败 */
+ private String sendStatus;
+ /** 发送失败原因 */
+ private String failReason;
+ /** 确认状态: 0=未确认 / 1=已确认 */
+ private String confirmStatus;
+ /** 确认时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date confirmedAt;
+ /** 公开邀请链接 token (uuid) */
+ private String token;
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+ public Long getInviteId() { return inviteId; }
+ public void setInviteId(Long inviteId) { this.inviteId = inviteId; }
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ public String getPhone() { return phone; }
+ public void setPhone(String phone) { this.phone = phone; }
+ public String getEmail() { return email; }
+ public void setEmail(String email) { this.email = email; }
+ public String getSource() { return source; }
+ public void setSource(String source) { this.source = source; }
+ public String getRemark() { return remark; }
+ public void setRemark(String remark) { this.remark = remark; }
+ public String getSendStatus() { return sendStatus; }
+ public void setSendStatus(String sendStatus) { this.sendStatus = sendStatus; }
+ public String getFailReason() { return failReason; }
+ public void setFailReason(String failReason) { this.failReason = failReason; }
+ public String getConfirmStatus() { return confirmStatus; }
+ public void setConfirmStatus(String confirmStatus) { this.confirmStatus = confirmStatus; }
+ public Date getConfirmedAt() { return confirmedAt; }
+ public void setConfirmedAt(Date confirmedAt) { this.confirmedAt = confirmedAt; }
+ public String getToken() { return token; }
+ public void setToken(String token) { this.token = token; }
+}
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 6e461fa..5b11d0b 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
@@ -120,7 +120,7 @@ public class BizMeeting extends BaseEntity {
private String scheduleUrl;
/** 生成的海报URL (生成海报按钮产出) */
private String posterUrl;
- /** 签署劳务 0未签 1已签 */
+ /** 签署劳务 N未签 Y已签 */
private String laborSigned;
/** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */
private transient Long userId;
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 f285fc8..bb93b28 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
@@ -2,7 +2,9 @@ package com.ruoyi.business.domain;
import java.math.BigDecimal;
import java.util.Date;
+import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 会议材料对象 biz_meeting_material (单表)
@@ -10,7 +12,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
* 包含 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 / L_ENTERPRISE_BENEFIT / L_SIGN_IN / L_PANORAMA / L_EXPERT_PHOTO / 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_ESIGN_IN / L_SIGN_IN / L_PANORAMA / L_EXPERT_PHOTO / SV_PAYMENT / LV_PAYMENT
*
*
* 注意: 不继承 BaseEntity — 不要 create_by / update_by / update_time 字段.
@@ -41,6 +43,15 @@ public class BizMeetingMaterial {
/** 脱敏版 OSS URL (签到表拍照时额外生成的高斯模糊版, sponsor 只看这个以隐藏手机号/身份证号) */
private String extraOssUrl;
+ /** 电子签到表解析后的脱敏 JSON (表头+行, sponsor 预览用; 保存时后端解析生成) */
+ private String maskedJson;
+
+ /** 扫码拍照回传的 OSS URL 列表 (仅 cameraUpload 端点入参; 签到表连拍多张, 前后全景单张). 存库时 L_SIGN_IN 序列化为 JSON 数组字符串写 ossUrl */
+ private List ossUrls;
+
+ /** 扫码拍照回传的脱敏版 OSS URL 列表 (与 ossUrls 一一对应, 仅 cameraUpload 端点入参) */
+ private List extraOssUrls;
+
/** 金额 (发票专用, 其他类型 = 0) */
private BigDecimal amount;
@@ -78,6 +89,17 @@ public class BizMeetingMaterial {
public String getExtraOssUrl() { return extraOssUrl; }
public void setExtraOssUrl(String extraOssUrl) { this.extraOssUrl = extraOssUrl; }
+ public String getMaskedJson() { return maskedJson; }
+ public void setMaskedJson(String maskedJson) { this.maskedJson = maskedJson; }
+
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ public List getOssUrls() { return ossUrls; }
+ public void setOssUrls(List ossUrls) { this.ossUrls = ossUrls; }
+
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ public List getExtraOssUrls() { return extraOssUrls; }
+ public void setExtraOssUrls(List extraOssUrls) { this.extraOssUrls = extraOssUrls; }
+
public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }
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 e79193a..24ae2df 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
@@ -10,7 +10,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
/** 项目对象 BizProject */
public class BizProject extends BaseEntity {
private static final long serialVersionUID = 1L;
- /** projectId (与 DB bigint 对齐, AUTO_INCREMENT) */
+ /** projectId (雪花 ID, 与 DB bigint 对齐) */
private Long projectId;
/** project_no */
@Excel(name = "project_no")
@@ -126,7 +126,7 @@ public class BizProject extends BaseEntity {
private String publishUrl;
/** 日程文件URL */
private String scheduleUrl;
- /** 是否已发布公示 0否 1是 */
+ /** 是否已发布公示 N否 Y是 */
private String isPublished;
/** 发布时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
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
index 0651e88..67bac64 100644
--- 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
@@ -8,7 +8,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
public class BizProjectExecutorAssign extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long id;
- private String projectId;
+ private Long projectId;
/** 分配方企业 org_id (biz_org.org_id, 分配人审计; 原 executor_user_id) */
private Long executorOrgId;
/** 执行人 user_id (被分配) */
@@ -26,15 +26,15 @@ public class BizProjectExecutorAssign extends BaseEntity {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
- /** 软删除标记 0否1是 (项目级联删除时按 project_id (String) 置1, 查询需 WHERE is_deleted=0) */
+ /** 软删除标记 0否1是 (项目级联删除时按 project_id 置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 getProjectId() { return projectId; }
+ public void setProjectId(Long projectId) { this.projectId = projectId; }
public Long getExecutorOrgId() { return executorOrgId; }
public void setExecutorOrgId(Long executorOrgId) { this.executorOrgId = executorOrgId; }
public Long getStaffUserId() { return staffUserId; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mail/InviteMailSender.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mail/InviteMailSender.java
new file mode 100644
index 0000000..79d4a67
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mail/InviteMailSender.java
@@ -0,0 +1,56 @@
+package com.ruoyi.business.mail;
+
+import jakarta.mail.internet.MimeMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+
+/**
+ * 邀请邮件发送器.
+ * 参考 xxl-job {@code EmailJobAlarm} 模式, 走 spring-boot-starter-mail (JavaMailSender + MimeMessageHelper).
+ * 发件账号/主题/正文由调用方传入; 发件人 from 取 spring.mail.username (SMTP 账号本身).
+ */
+@Service
+public class InviteMailSender
+{
+ private static final Logger log = LoggerFactory.getLogger(InviteMailSender.class);
+
+ @Autowired(required = false)
+ private JavaMailSender mailSender;
+
+ @Value("${spring.mail.username:}")
+ private String emailFrom;
+
+ /**
+ * 发送 HTML 邀请邮件. 成功返回 true, 失败返回 false (不抛, 由调用方回写发送状态).
+ */
+ public boolean sendInvite(String to, String subject, String html)
+ {
+ if (mailSender == null)
+ {
+ log.warn("[邮件] 未配置 spring.mail (JavaMailSender 不存在), 跳过发送 to={}", to);
+ return false;
+ }
+ try
+ {
+ MimeMessage mimeMessage = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
+ helper.setFrom(emailFrom);
+ helper.setTo(to);
+ helper.setSubject(subject == null ? "" : subject);
+ helper.setText(html == null ? "" : html, true);
+ mailSender.send(mimeMessage);
+ log.info("[邮件] 邀请邮件发送成功 to={}", to);
+ return true;
+ }
+ catch (Exception e)
+ {
+ log.error("[邮件] 邀请邮件发送失败 to={}", to, e);
+ return false;
+ }
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInviteMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInviteMapper.java
new file mode 100644
index 0000000..eb00e1a
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInviteMapper.java
@@ -0,0 +1,22 @@
+package com.ruoyi.business.mapper;
+
+import java.util.Date;
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.business.domain.BizInvite;
+
+/**
+ * 发送邀请主表 Mapper 接口
+ */
+public interface BizInviteMapper
+{
+ BizInvite selectByPrimaryKey(Long id);
+ /** 列表 (含聚合列 recipient_count / confirmed_count), 不含富文本 content */
+ List selectList(BizInvite entity);
+ int insert(BizInvite entity);
+ int updateByPrimaryKey(BizInvite entity);
+ int deleteByPrimaryKey(Long id);
+ int deleteByPrimaryKeys(Long[] ids);
+ /** 发送后回填 send_time */
+ int updateSendTime(@Param("id") Long id, @Param("sendTime") Date sendTime);
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInviteRecipientMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInviteRecipientMapper.java
new file mode 100644
index 0000000..f4fad0b
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInviteRecipientMapper.java
@@ -0,0 +1,21 @@
+package com.ruoyi.business.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.business.domain.BizInviteRecipient;
+
+/**
+ * 发送邀请收件人明细 Mapper 接口
+ */
+public interface BizInviteRecipientMapper
+{
+ List selectByInviteId(Long inviteId);
+ BizInviteRecipient selectByToken(String token);
+ int insert(BizInviteRecipient entity);
+ int insertBatch(List list);
+ int deleteByInviteId(Long inviteId);
+ /** 公开确认: 置 confirm_status=1 + confirmed_at=sysdate() */
+ int updateConfirmByToken(String token);
+ /** 发送结果回写: send_status + fail_reason */
+ int updateSendStatus(@Param("id") Long id, @Param("sendStatus") String sendStatus, @Param("failReason") String failReason);
+}
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 221b270..7d19cae 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
@@ -35,6 +35,11 @@ public interface BizMeetingAttendeeMapper {
List selectByMeetingId(Long meetingId);
List selectByUserId(Long userId);
BizMeetingAttendee selectById(Long id);
+ /**
+ * 按 (meetingId, userId) 查参会人行 id (导入覆盖更新用: 手机号已在该会议时拿旧行 id).
+ * 无匹配返回 null.
+ */
+ Long selectIdByMeetingIdAndUserId(@Param("meetingId") Long meetingId, @Param("userId") Long userId);
List selectUnsignedByUserId(Long userId);
/** 当前用户的"待参加"会议 (已邀请参会 is_invited=1), 联表取会议名/时间 */
List selectInvitedByUserId(Long userId);
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 fc0048d..a1a3bde 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
@@ -48,6 +48,12 @@ public interface BizMeetingMapper
* 由 MeetingStageScheduler 每分钟触发.
*/
int markFrozen();
+ /**
+ * 自动流转 (反向纠偏): start_time 被改到未来、但 current_stage 仍残留 IN_PROGRESS 的会议 → 置回 NOT_STARTED.
+ *
markInProgress 只做单向 (NOT_STARTED→IN_PROGRESS), 编辑把 start_time 推到未来时无人回退; 本方法由
+ * MeetingStageScheduler 每分钟触发, 与 markInProgress 互为逆操作, 保证时间窗口态双向一致.
+ */
+ int markNotStarted();
/**
* 费用汇总调度器用: 查 fee_calc_status=0 且未软删的会议 id 列表.
*/
@@ -71,4 +77,11 @@ public interface BizMeetingMapper
int updateLaborFee(@Param("meetingId") Long meetingId,
@Param("laborFee") BigDecimal laborFee,
@Param("totalFee") BigDecimal totalFee);
+ /**
+ * 项目 submit_deadline_days 变更时级联重算: 该项目下「从未退回、且仍有轨未提交」的会议 (未软删/未冻结),
+ * 提交截止时间 = end_time + days 天 (days 为 null 则置 NULL → 永不冻结).
+ *
排除 REJECTED 轨: 退回时截止时间已被重置为 now+天数 (锚点=退回时刻), 按 end_time 重算会错误缩短整改窗口.
+ * 已冻结/两轨均已提交的会议不动 (其截止时间已无意义).
+ */
+ int recomputeSubmitDeadlinesByProject(@Param("projectId") Long projectId, @Param("days") Integer days);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java
index 0ff4193..6f1b541 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java
@@ -24,12 +24,13 @@ public interface BizOrgMapper {
* 与 selectSponsorOrgOptions 区别: 不 JOIN sys_user (无主账号的 org 也可选), 返回 orgId + mainUserId
* 返回 Map: orgId / orgName / mainUserId (= biz_org.user_id, 可为 null 表示该企业暂无主账号) */
List