From 591a43fe6124839a81d23d7c8a9af6c9fe9176c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?= <12369755+htcloud1@user.noreply.gitee.com> Date: Tue, 25 Aug 2026 06:53:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AE=AE=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=89=A9=E4=BD=99=E6=97=B6=E9=97=B4+=E8=A7=A3=E5=86=BB=20+=20?= =?UTF-8?q?=E4=BC=9A=E5=8A=A1/=E5=8A=B3=E5=8A=A1=E6=9D=90=E6=96=99?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E4=B8=8B=E8=BD=BD=20+=20=E5=A4=9A=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E4=BA=BA=E5=91=98/=E8=B4=A6=E5=8F=B7=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- .../src/main/resources/application.yml | 10 +- .../controller/BizAuthController.java | 83 +-- .../BizMeetingAttendeeController.java | 86 ++- .../controller/BizMeetingController.java | 214 ++++--- .../BizMeetingExecutorController.java | 10 +- .../BizMeetingMaterialController.java | 108 ++++ .../BizMeetingSupervisorController.java | 10 +- .../business/controller/BizOrgController.java | 26 + .../controller/BizPersonController.java | 95 +++- .../controller/BizProjectController.java | 63 ++- .../com/ruoyi/business/domain/BizMeeting.java | 26 +- .../business/domain/BizMeetingAuditLog.java | 2 +- .../business/domain/BizMeetingExecutor.java | 14 +- .../business/domain/BizMeetingSupervisor.java | 14 +- .../com/ruoyi/business/domain/BizPerson.java | 9 +- .../domain/BizPersonExecutorImportVO.java | 49 ++ .../business/domain/BizPersonImportVO.java | 11 +- .../com/ruoyi/business/domain/BizProject.java | 39 +- .../business/domain/BizProjectAssign.java | 6 +- .../domain/BizProjectExecutorAssign.java | 8 +- .../ruoyi/business/domain/BizProjectPlan.java | 4 + .../domain/BizProjectSponsorAssign.java | 7 +- .../domain/vo/BizMeetingAttendeeImportVo.java | 27 + .../business/domain/vo/BizOrgImportVo.java | 59 ++ .../mapper/BizMeetingExecutorMapper.java | 3 - .../mapper/BizMeetingMaterialMapper.java | 13 + .../mapper/BizMeetingSupervisorMapper.java | 3 - .../ruoyi/business/mapper/BizOrgMapper.java | 13 +- .../business/mapper/BizPersonMapper.java | 3 - .../business/mapper/BizProjectMapper.java | 4 + .../business/ocr/config/OcrProperties.java | 2 +- .../ocr/service/RecognizeService.java | 2 + .../com/ruoyi/business/oss/OssConfMeta.java | 3 + .../com/ruoyi/business/oss/OssZipService.java | 177 ++++++ .../scheduler/MeetingStageScheduler.java | 4 +- .../scheduler/OpenStatusScheduler.java | 42 ++ .../service/IBizMeetingAttendeeService.java | 51 ++ .../service/IBizMeetingExecutorService.java | 5 +- .../service/IBizMeetingMaterialService.java | 56 ++ .../service/IBizMeetingSupervisorService.java | 5 +- .../business/service/IBizOrgService.java | 18 +- .../business/service/IBizPersonService.java | 5 + .../business/service/IBizProjectService.java | 6 + .../ruoyi/business/service/StageDeriver.java | 19 +- .../impl/BizMeetingAttendeeServiceImpl.java | 226 ++++++++ .../impl/BizMeetingExecutorServiceImpl.java | 15 +- .../impl/BizMeetingMaterialServiceImpl.java | 534 ++++++++++++++++++ .../service/impl/BizMeetingServiceImpl.java | 5 +- .../impl/BizMeetingSupervisorServiceImpl.java | 15 +- .../service/impl/BizOrgServiceImpl.java | 93 +++ .../service/impl/BizPersonServiceImpl.java | 56 +- .../impl/BizProjectAssignServiceImpl.java | 29 +- .../BizProjectExecutorAssignServiceImpl.java | 2 +- .../service/impl/BizProjectServiceImpl.java | 12 + .../BizProjectSponsorAssignServiceImpl.java | 2 +- .../business/BizMeetingAttendeeMapper.xml | 1 + .../business/BizMeetingExecutorMapper.xml | 32 +- .../mapper/business/BizMeetingMapper.xml | 72 +-- .../business/BizMeetingMaterialMapper.xml | 13 + .../business/BizMeetingSupervisorMapper.xml | 32 +- .../mapper/business/BizOrgMapper.xml | 49 +- .../mapper/business/BizPersonMapper.xml | 22 +- .../business/BizProjectAssignMapper.xml | 7 +- .../BizProjectExecutorAssignMapper.xml | 9 +- .../mapper/business/BizProjectMapper.xml | 113 ++-- .../mapper/business/BizProjectPlanMapper.xml | 6 +- .../BizProjectSponsorAssignMapper.xml | 9 +- .../com/ruoyi/common/config/RuoYiConfig.java | 4 + .../common/enums/MeetingAuditStageEnum.java | 4 +- .../web/service/SysLoginService.java | 7 +- .../web/service/UserDetailsServiceImpl.java | 16 + .../ruoyi/system/mapper/SysUserMapper.java | 24 + .../ruoyi/system/service/ISysUserService.java | 8 + .../service/impl/SysUserServiceImpl.java | 9 + .../resources/mapper/system/SysUserMapper.xml | 26 + ry-vue3/index.html | 2 +- ry-vue3/src/api/public.js | 9 +- ry-vue3/src/api/system.js | 7 +- ry-vue3/src/components/AreaCascader.vue | 24 +- ry-vue3/src/components/Preview.vue | 15 +- ry-vue3/src/layout/AdminLayout.vue | 9 +- ry-vue3/src/router/index.js | 21 +- ry-vue3/src/utils/meetingStage.js | 14 +- ry-vue3/src/utils/request.js | 9 +- ry-vue3/src/utils/roleMap.js | 37 +- ry-vue3/src/views/admin/Workbench.vue | 4 - ry-vue3/src/views/auth/Login.vue | 8 +- ry-vue3/src/views/auth/RegisterSponsor.vue | 49 +- ry-vue3/src/views/doctor/Account.vue | 116 +--- ry-vue3/src/views/doctor/Meetings.vue | 107 +++- ry-vue3/src/views/doctor/Projects.vue | 3 - ry-vue3/src/views/doctor/Submissions.vue | 20 +- .../src/views/executor-orgs/ExecutorOrgs.vue | 30 +- .../views/executor-people/ExecutorPeople.vue | 112 ++-- .../executor-people/ExecutorPersonDetail.vue | 20 +- .../executor-people/ExecutorPersonNew.vue | 22 +- ry-vue3/src/views/executor/Account.vue | 3 +- ry-vue3/src/views/executor/Meetings.vue | 30 +- ry-vue3/src/views/executor/NewPerson.vue | 14 +- ry-vue3/src/views/executor/People.vue | 9 +- ry-vue3/src/views/executor/PersonDetail.vue | 14 +- ry-vue3/src/views/executor/Projects.vue | 6 +- ry-vue3/src/views/expert/Experts.vue | 1 + ry-vue3/src/views/manager/Accounts.vue | 3 +- .../views/manager/ManagerProjectDetail.vue | 9 +- .../views/manager/ManagerProjectsAssign.vue | 70 +-- ry-vue3/src/views/manager/PlanEdit.vue | 211 +++++++ ry-vue3/src/views/manager/Plans.vue | 105 ++-- ry-vue3/src/views/manager/Projects.vue | 96 ++-- ry-vue3/src/views/manager/ProjectsNew.vue | 14 +- ry-vue3/src/views/meetings/MeetingDetail.vue | 479 +++++++++------- ry-vue3/src/views/meetings/MeetingNew.vue | 9 +- ry-vue3/src/views/meetings/Meetings.vue | 114 +++- ry-vue3/src/views/portal/Publicity.vue | 9 +- ry-vue3/src/views/portal/PublicityDetail.vue | 45 +- .../src/views/sponsor-orgs/SponsorOrgs.vue | 124 +++- .../views/sponsor-people/SponsorPeople.vue | 98 +++- .../sponsor-people/SponsorPersonDetail.vue | 9 +- .../views/sponsor-people/SponsorPersonNew.vue | 7 +- ry-vue3/src/views/sponsor/Account.vue | 3 +- ry-vue3/src/views/sponsor/NewPerson.vue | 19 +- ry-vue3/src/views/sponsor/PersonDetail.vue | 13 +- ry-vue3/src/views/sponsor/Projects.vue | 10 +- ry-vue3/src/views/sponsor/SponsorPeople.vue | 20 +- ry-vue3/src/views/sponsor/SponsorProjects.vue | 6 +- 会议状态流转.md | 69 ++- 126 files changed, 3789 insertions(+), 1334 deletions(-) create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonExecutorImportVO.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizOrgImportVo.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssZipService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/OpenStatusScheduler.java create mode 100644 ry-vue3/src/views/manager/PlanEdit.vue diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml index 82087de..608bd98 100644 --- a/ry-api/ruoyi-admin/src/main/resources/application.yml +++ b/ry-api/ruoyi-admin/src/main/resources/application.yml @@ -23,6 +23,8 @@ ruoyi: accessKeySecret: gA0fTFa8arUYaYA5Us2ClPKVSM1Ypv dirPrefix: ry8080/ expireSeconds: 3600 + # 阿里云函数计算 (FC) 打 zip 端点 (与 hwt-serve 共用同一 bucket, 参考 OssApi.ossDownload) + zip-func-url: https://zip-oss-func-zip-oss-swqpembfsl.cn-beijing.fcapp.run # 阿里云短信配置 (hwt-code ali.sms 模式) # dev/prod 区分走代码: phone 以 "10" 开头视为 dev 测试 (固定码 1234), 其它走 aliyun 真发 sms: @@ -45,7 +47,7 @@ ruoyi: page-timeout-s: 15 total-timeout-s: 60 # QR 命中后是否继续跑全量 OCR (false = 快路径只返回 QR 3 字段) - qr-full-ocr: true + qr-full-ocr: false lang: ch # PDF 优先抽内嵌文本层 (电子发票秒出, 扫描件自动回退 ONNX) use-pdf-text-first: true @@ -78,9 +80,9 @@ server: # 日志配置 logging: level: - com.ruoyi: info - org.springframework: info - com.ruoyi.business: info + com.ruoyi: debug + org.springframework: debug + com.ruoyi.business: debug # 用户配置 user: 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 a52f228..adf0e38 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 @@ -6,9 +6,11 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.ruoyi.business.domain.BizOrg; import com.ruoyi.business.domain.BizPerson; @@ -159,7 +161,7 @@ public class BizAuthController extends BaseController { return error("所属主账号不存在, 请联系管理员"); } if (UserStatus.DISABLE.getCode().equals(parent.getStatus())) { - return error("所属企业主账号已被停用, 请联系企业管理员"); + return error("您所属的机构已禁用"); } } @@ -254,8 +256,7 @@ public class BizAuthController extends BaseController { self.setPhone(phone); self.setOrgId(orgId); self.setDepartment("管理部"); - self.setPosition("总负责人"); - self.setRole("admin"); // 主账号 = 管理员 (与子账号的会议执行区分) + self.setPosition("管理员"); self.setUnitType("executor"); self.setUserId(userId); self.setCreateBy(username); @@ -266,31 +267,55 @@ public class BizAuthController extends BaseController { } /** - * 注册支持方 (主账号), 流程与 executor 一致: - * 校验 → 查重 → 写 sys_user + role_type=sponsor → 写 biz_org (sponsor 类型) + * 支持方注册下拉选项 (匿名公开): 从 biz_org(sponsor 类型) 搜企业, 含无主账号的 org. + * 返回 [{orgId, orgName, mainUserId}], mainUserId 为 null 表示该企业还没有主账号. + */ + @GetMapping("/sponsorOrgOptions") + public AjaxResult sponsorOrgOptions(@RequestParam(value = "orgName", required = false) String orgName) { + BizOrg query = new BizOrg(); + query.setOrgName(orgName); + return success(bizOrgService.selectSponsorRegisterOptions(query)); + } + + /** + * 注册支持方 (子账号 SUB): 从已存在的 sponsor 企业里选一家注册, 不再新建企业. + * 校验 → 查重 → 写 sys_user (account_type=SUB + role_type=sponsor) → 写 biz_person 关联所选企业. + * 主账号归属 (见 /sponsorOrgOptions 的 mainUserId): + * - 企业已有主账号 (biz_org.user_id != null) → parent_user_id = 主账号 user_id + * - 企业暂无主账号 (biz_org.user_id == null) → parent_user_id 留空, 待 admin/manager 后续分配 */ @PostMapping("/registerSponsor") public AjaxResult registerSponsor(@RequestBody Map body) { String username = (String) body.get("username"); - String unitName = (String) body.get("unitName"); - String businessNature = (String) body.get("businessNature"); + String orgIdStr = body.get("orgId") == null ? null : body.get("orgId").toString(); String phone = (String) body.get("phone"); String code = (String) body.get("smsCode"); String password = (String) body.get("password"); String confirmPassword = (String) body.get("confirmPassword"); String uuid = (String) body.get("uuid"); - // 1. 基础校验 (与 executor 同) + // 1. 基础校验 if (username == null || username.length() < 4 || username.length() > 20) return error("用户名长度 4-20 位"); if (!username.matches("^[A-Za-z0-9_]+$")) return error("用户名只能包含字母/数字/下划线"); - if (unitName == null || unitName.isEmpty()) return error("企业名称不能为空"); - if (businessNature == null || businessNature.isEmpty()) return error("企业性质不能为空"); + if (orgIdStr == null || orgIdStr.isEmpty()) return error("请选择企业"); if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误"); if (code == null || code.isEmpty()) return error("请输入短信验证码"); if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位"); if (!password.equals(confirmPassword)) return error("两次密码输入不一致"); - // 2. 校验短信验证码 + // 2. 校验所选企业存在且为 sponsor (不新建企业) + Long orgId; + try { + orgId = Long.valueOf(orgIdStr); + } catch (NumberFormatException e) { + return error("企业参数错误"); + } + BizOrg org = bizOrgService.getById(orgId); + if (org == null) return error("所选企业不存在"); + if (!"sponsor".equals(org.getOrgType())) return error("所选企业不是支持方"); + Long mainUserId = org.getUserId(); // 可为 null: 无主账号时留空待分配 + + // 3. 校验短信验证码 SmsValidForm smsForm = new SmsValidForm(); smsForm.setPhone(phone); smsForm.setSmsCode(code); @@ -301,7 +326,7 @@ public class BizAuthController extends BaseController { return error("验证码错误或已过期: " + e.getMessage()); } - // 3. 查重 + // 4. 查重 if (userService.isPhoneRegistered(phone)) { return error("该手机号已注册, 请直接登录"); } @@ -311,42 +336,28 @@ public class BizAuthController extends BaseController { return error("用户名已被占用: " + username); } - // 4. 写 sys_user (role_type=sponsor) + // 5. 写 sys_user: 全部注册为 SUB 子账号 + // role_type 显式写 sponsor, 避免被 sys_user.role_type DB DEFAULT 'executor' 覆盖 SysUser user = new SysUser(); user.setUserName(username); - user.setNickName(unitName); + user.setNickName(org.getOrgName()); // 昵称用所选企业名 user.setPhonenumber(phone); user.setPassword(passwordEncoder.encode(password)); user.setStatus("0"); + user.setAccountType("SUB"); + user.setParentUserId(mainUserId); // 有主账号则绑定, 无则留空待分配 + user.setRoleType("sponsor"); userService.insertUser(user); Long userId = user.getUserId(); - userService.updateRoleType(userId, "sponsor"); - // 5. 写 biz_org (sponsor 类型) - BizOrg org = new BizOrg(); - org.setOrgId(null); - org.setUserId(userId); // 关联主账号, 让系统能反查 "我的公司" - org.setOrgName(unitName); - org.setOrgType("sponsor"); - org.setBusinessNature(businessNature); - org.setContactPhone(phone); - org.setContactName(unitName); - org.setStatus("0"); - bizOrgService.insert(org); - Long orgId = org.getOrgId(); - - // 6. 同步建 biz_person (主账号自己 = 管理员, 让"人员管理"能看到自己) - // 注意: 不能走 IBizPersonService.insert(BizPerson, Long), 那个方法是创建 SUB 子账号的 - // (强制 accountType='SUB' + 需 loginUsername, 主账号自己不需要). - // sys_user 已在 step 4 建好, 这里只需把 biz_person 关联到主账号自己. + // 6. 写 biz_person (关联到所选企业, 不再新建 biz_org) BizPerson self = new BizPerson(); SnowflakeId.injectIfEmpty(self, "personId"); - self.setName(unitName); + self.setName(username); // 表单无姓名字段, 用登录用户名占位 self.setPhone(phone); self.setOrgId(orgId); - self.setDepartment("管理部"); - self.setPosition("总负责人"); - self.setRole("admin"); // 主账号 = 管理员 (与子账号的监察员区分) + self.setDepartment("待分配"); + self.setPosition("员工"); self.setUnitType("sponsor"); self.setUserId(userId); self.setCreateBy(username); 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 8f95910..e361226 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 @@ -9,6 +9,7 @@ import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.business.domain.BizMeeting; @@ -210,27 +211,7 @@ public class BizMeetingAttendeeController extends BaseController { 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); + exportList.add(BizMeetingAttendeeImportVo.from(a)); } ExcelUtil util = new ExcelUtil<>(BizMeetingAttendeeImportVo.class); util.exportExcel(response, exportList, "参会人"); @@ -267,4 +248,67 @@ public class BizMeetingAttendeeController extends BaseController { } return toAjax(rows); } + + /** + * 下载"劳务协议"空目录模板 zip (参会人管理 "下载协议模板" 按钮). + * zip 内为 劳务协议/{序号}_{姓名}/ 空目录, 用户把签好的协议放进对应目录后重新压缩上传. + * 仅 admin/manager 可触发. + */ + @GetMapping("/agreementTemplate/{meetingId}") + public void agreementTemplate(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception { + requireManagerOrAdmin(); + byte[] data = attendeeService.buildAgreementTemplateZip(meetingId); + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment;filename=agreement-template.zip"); + response.getOutputStream().write(data); + response.getOutputStream().flush(); + } + + /** + * 上传"劳务协议" zip, 解压后按 劳务协议/{序号}_{姓名}/ 目录匹配参会人, + * 上传 OSS 并回填 labor_protocol. 仅 admin/manager 可触发. + */ + @Log(title = "劳务协议回填", businessType = BusinessType.UPDATE) + @PostMapping("/uploadAgreements") + public AjaxResult uploadAgreements(@RequestParam("file") MultipartFile file, + @RequestParam("meetingId") Long meetingId) throws Exception { + requireManagerOrAdmin(); + int updated = attendeeService.uploadAgreements(file, meetingId); + return success(updated); + } + + /** + * 下载"专家照片"空目录模板 zip (专家照片 "下载目录模板" 按钮). + * zip 内为 专家照片/{序号}_{姓名}/ 空目录. 仅 admin/manager 可触发. + */ + @GetMapping("/expertPhotoTemplate/{meetingId}") + public void expertPhotoTemplate(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception { + requireManagerOrAdmin(); + byte[] data = attendeeService.buildExpertPhotoTemplateZip(meetingId); + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment;filename=expert-photo-template.zip"); + response.getOutputStream().write(data); + response.getOutputStream().flush(); + } + + /** + * 上传"专家照片" zip, 解压后按 专家照片/{序号}_{姓名}/ 目录匹配参会人, + * 上传 OSS 并逗号拼接回填 on_site_photos. 仅 admin/manager 可触发. + */ + @Log(title = "专家照片回填", businessType = BusinessType.UPDATE) + @PostMapping("/uploadExpertPhotos") + public AjaxResult uploadExpertPhotos(@RequestParam("file") MultipartFile file, + @RequestParam("meetingId") Long meetingId) throws Exception { + requireManagerOrAdmin(); + int updated = attendeeService.uploadExpertPhotos(file, meetingId); + return success(updated); + } + + /** 仅 admin/manager 可操作 (参会人劳务协议/专家照片的批量收发是管理端功能) */ + private void requireManagerOrAdmin() { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"admin".equals(roleType) && !"manager".equals(roleType)) { + throw new ServiceException("只有管理员或合规经理可操作"); + } + } } \ No newline at end of file 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 04df21b..4ae0f1b 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 @@ -1,5 +1,6 @@ package com.ruoyi.business.controller; +import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Map; @@ -22,9 +23,11 @@ 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.BizOrg; import com.ruoyi.business.domain.BizProject; import com.ruoyi.business.notify.BizNotifyService; import com.ruoyi.business.service.IBizMeetingService; +import com.ruoyi.business.service.IBizOrgService; import com.ruoyi.business.service.IBizProjectService; import com.ruoyi.business.service.IBizMeetingAttendeeService; import com.ruoyi.business.service.StageDeriver; @@ -58,6 +61,8 @@ public class BizMeetingController extends BaseController { @Autowired private IBizProjectService bizProjectService; @Autowired + private IBizOrgService bizOrgService; + @Autowired private StageDeriver stageDeriver; @Autowired private PosterService posterService; @@ -70,7 +75,7 @@ public class BizMeetingController extends BaseController { if ("doctor".equals(roleType) || "expert".equals(roleType)) { bizMeeting.setUserId(uid); } - // sponsor 数据权限: 只看"我的项目"下的会议 (MAIN 走 sponsor_admin_user_id, SUB 走 sponsor_assign.monitor_user_id). + // sponsor 数据权限: 只看"我的项目"下的会议 (MAIN 走 sponsor_org_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); @@ -81,7 +86,7 @@ public class BizMeetingController extends BaseController { } } // executor 数据权限: 只看"我的项目"下的会议 (与项目列表 selectExecutorList/selectExecutorStaffList 同源). - // MAIN 走 biz_project_assign (exec_user_id / execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id. + // MAIN 走 biz_project_assign (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())) { @@ -129,6 +134,8 @@ public class BizMeetingController extends BaseController { bizMeeting.setCreateTime(new Date()); bizMeeting.setUpdateBy(SecurityUtils.getUsername()); bizMeeting.setUpdateTime(new Date()); + // 提交截止时间: 建会时按 end_time + 项目 submit_deadline_days 天 落库 (项目未设天数则为 null → 永不冻结) + bizMeeting.setSubmitDeadline(computeSubmitDeadline(bizMeeting.getProjectId(), bizMeeting.getEndTime())); int rows = bizMeetingService.insert(bizMeeting); Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds(); if (attendeeUserIds != null && attendeeUserIds.length > 0) { @@ -230,45 +237,10 @@ public class BizMeetingController extends BaseController { } /** - * 执行人员提交凭证 (校验 LV_PAYMENT + SV_PAYMENT) - */ - @Log(title = "会议审核", businessType = BusinessType.UPDATE) - @PostMapping("/{meetingId}/submit-voucher") - public AjaxResult submitVoucher(@PathVariable("meetingId") Long meetingId) { - BizMeeting m = bizMeetingService.getById(meetingId); - if (m == null) throw new ServiceException("会议不存在"); - - Long userId = SecurityUtils.getUserId(); - 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); - boolean hasLv = mats.stream().anyMatch(x -> "LV_PAYMENT".equals(x.getSubType())); - boolean hasSv = mats.stream().anyMatch(x -> "SV_PAYMENT".equals(x.getSubType())); - if (!hasLv || !hasSv) { - throw new ServiceException("请同时上传劳务凭证和会务凭证"); - } - - m.setVoucherAuditStage("SUBMITTED"); - m.setVoucherComplianceApproved(0); - m.setVoucherAuditTime(new Date()); - bizMeetingService.updateByPrimaryKey(m); - appendAuditLog(m, "VOUCHER", "SUBMITTED", "执行人员提交凭证"); - return success("SUBMITTED"); - } - - /** - * 合规审核 (role_type=manager), 两级审核中的第一级. + * 合规审核 (role_type=manager), 材料两级审核中的第一级. *

- * body: { "auditType": "MATERIAL"|"VOUCHER"|"BOTH", "approved": true|false, "opinion": "..." } - *

合规审中判据: stage=SUBMITTED 且 compliance_approved=0. + * body: { "approved": true|false, "opinion": "..." } + *

合规审中判据: material_audit_stage=SUBMITTED 且 compliance_approved=0. * 通过 → compliance_approved=1 (转入支持方审), 拒绝 → REJECTED (退回执行方). */ @Log(title = "会议审核", businessType = BusinessType.UPDATE) @@ -280,97 +252,97 @@ public class BizMeetingController extends BaseController { BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); - 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 = 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()); + Integer compliance = m.getMaterialComplianceApproved(); + if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || (compliance != null && compliance == 1)) { + throw new ServiceException("当前阶段不允许合规审核"); } + + String result = approved ? "APPROVED" : "REJECTED"; + if (approved) { + m.setMaterialComplianceApproved(1); + } else { + m.setMaterialAuditStage("REJECTED"); + // 退回 → 提交截止时间重新计算 (now + 项目天数) + m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date())); + } + m.setMaterialAuditTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - for (String type : types) { - appendAuditLog(m, type, result, body.getOpinion()); - } + appendAuditLog(m, "MATERIAL", result, body.getOpinion()); return success(result); } /** - * 支持方(监察员) 审核, 两级审核中的第二级. + * 支持方(监察员) 审核, 材料两级审核中的第二级. *

- * body: { "auditType": "MATERIAL"|"VOUCHER"|"BOTH", "approved": true|false, "opinion": "..." } - *

支持方审中判据: stage=SUBMITTED 且 compliance_approved=1. - * 通过 → APPROVED (材料通过时一并写监管意见), 拒绝 → REJECTED (退回执行方). + * body: { "approved": true|false, "opinion": "..." } + *

支持方审中判据: material_audit_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(); + Long myOrgId = bizOrgService.selectOrgIdByUserId(userId); BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); - // 授权: 监察员 (biz_meeting_supervisor) 或 支持方 MAIN 账号 (biz_project.sponsor_admin_user_id) 均可审 + // 授权: 监察员 (biz_meeting_supervisor.sponsor_org_id) 或 支持方企业 (biz_project.sponsor_org_id) 均可审 boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream() - .anyMatch(s -> userId.equals(s.getUserId())); + .anyMatch(s -> myOrgId != null && myOrgId.equals(s.getSponsorOrgId())); BizProject project = m.getProjectId() == null ? null : bizProjectService.getById(m.getProjectId()); - boolean isSponsorMain = project != null && userId.equals(project.getSponsorAdminUserId()); + boolean isSponsorMain = project != null && myOrgId != null && myOrgId.equals(project.getSponsorOrgId()); if (!isSupervisor && !isSponsorMain) { throw new ServiceException("您不是该会议监察员, 无权监察"); } - 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 = 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()); + Integer compliance = m.getMaterialComplianceApproved(); + if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || compliance == null || compliance != 1) { + throw new ServiceException("当前阶段不允许监察审核"); } + + String result = approved ? "APPROVED" : "REJECTED"; + m.setMaterialAuditStage(approved ? "APPROVED" : "REJECTED"); + // 退回 → 提交截止时间重新计算 (now + 项目天数) + if (!approved) { + m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date())); + } + m.setMaterialAuditTime(new Date()); // 材料通过 → 写监管意见 (支持方的书面意见) - if (approved && java.util.Arrays.asList(types).contains("MATERIAL")) { + if (approved) { m.setSupervisionOpinion(body.getOpinion()); m.setSupervisionBy(SecurityUtils.getUsername()); m.setSupervisionTime(new Date()); } m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - for (String type : types) { - appendAuditLog(m, type, result, body.getOpinion()); - } + appendAuditLog(m, "MATERIAL", result, body.getOpinion()); // 退回 → 通知执行方 (待整改 + 说明) if (!approved) { for (BizMeetingExecutor e : bizMeetingExecutorService.selectByMeetingId(meetingId)) { - bizNotifyService.meetingSupervisionRejected(e.getUserId(), meetingId, m.getMeetingName(), body.getOpinion()); + bizNotifyService.meetingSupervisionRejected(resolveMainUserIdByOrgId(e.getExecutorOrgId()), meetingId, m.getMeetingName(), body.getOpinion()); } } return success(result); } /** - * 结算 (合规/管理员 手动点击). 前置: material+voucher 都 APPROVED. + * 结算 (合规/管理员 手动点击). + *

+ * 前置: 材料已 APPROVED + 费用已汇总 (fee_calc_status=1) + 劳务/会务付款凭证都已上传. + * 凭证由合规人员在"劳务凭证/会务凭证" tab 随时上传保存, 结算时校验是否已存在, 不再随结算一起提交. */ @Log(title = "会议审核", businessType = BusinessType.UPDATE) @PostMapping("/{meetingId}/settle") @@ -382,8 +354,8 @@ public class BizMeetingController extends BaseController { } BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); - if (!"APPROVED".equals(m.getMaterialAuditStage()) || !"APPROVED".equals(m.getVoucherAuditStage())) { - throw new ServiceException("材料与凭证均审核通过后才能结算"); + if (!"APPROVED".equals(m.getMaterialAuditStage())) { + throw new ServiceException("材料审核通过后才能结算"); } if (isSettled(m)) throw new ServiceException("会议已结算"); // 费用未汇总完 (fee_calc_status=0) 禁止结算: 此时 labor_fee/meeting_fee 可能为旧值/0, 直接回写会污染项目金额. @@ -391,6 +363,15 @@ public class BizMeetingController extends BaseController { if (m.getFeeCalcStatus() == null || m.getFeeCalcStatus() != 1) { throw new ServiceException("会议费用尚未汇总完成,无法结算"); } + + // 凭证校验: 劳务(LV_PAYMENT) + 会务(SV_PAYMENT) 必须已上传保存, 否则拒绝结算 + List existingMaterials = bizMeetingMaterialService.selectByMeetingId(meetingId); + boolean hasLv = existingMaterials != null && existingMaterials.stream().anyMatch(v -> "LV_PAYMENT".equals(v.getSubType()) && v.getOssUrl() != null && !v.getOssUrl().isEmpty()); + boolean hasSv = existingMaterials != null && existingMaterials.stream().anyMatch(v -> "SV_PAYMENT".equals(v.getSubType()) && v.getOssUrl() != null && !v.getOssUrl().isEmpty()); + if (!hasLv || !hasSv) { + throw new ServiceException("请先上传付款凭证"); + } + m.setIsSettled(1); m.setSettleTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); @@ -425,6 +406,30 @@ public class BizMeetingController extends BaseController { return success("FINISHED"); } + /** + * 解冻 (合规/管理员 手动点击). 前置: is_frozen=1. + * 清除冻结标记 (is_frozen=0), 保留 freeze_time 作为"曾被冻结过"的历史凭证 (列表据此显示"超时提交"), + * 提交截止时间重新计算 (now + 项目天数) → 冻结倒计时重新开始. + */ + @Log(title = "会议", businessType = BusinessType.UPDATE) + @PostMapping("/{meetingId}/unfreeze") + public AjaxResult unfreeze(@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 (!isFrozen(m)) throw new ServiceException("会议未冻结, 无需解冻"); + m.setIsFrozen(0); + m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date())); + m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); + m.setUpdateBy(SecurityUtils.getUsername()); + m.setUpdateTime(new Date()); + bizMeetingService.updateByPrimaryKey(m); + return success("UNFROZEN"); + } + /** * 审核轨迹 (audit_log 列表, 按时间排序) */ @@ -445,32 +450,25 @@ public class BizMeetingController extends BaseController { 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); + /** org_id → MAIN 主账号 user_id (biz_org.user_id), null 安全 */ + private Long resolveMainUserIdByOrgId(Long orgId) { + if (orgId == null) return null; + BizOrg org = bizOrgService.getById(orgId); + return org == null ? null : org.getUserId(); } - /** 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"); + /** + * 计算提交截止时间 = 基准时刻 + 项目 submit_deadline_days 天. + * 项目缺失或未设天数 → 返回 null (会议永不冻结, 与旧 markFrozen 的 p.submit_deadline_days is not null 语义一致). + */ + private Date computeSubmitDeadline(Long projectId, Date base) { + if (projectId == null) return null; + BizProject p = bizProjectService.getById(projectId); + if (p == null || p.getSubmitDeadlineDays() == null) return null; + Calendar cal = Calendar.getInstance(); + cal.setTime(base == null ? new Date() : base); + cal.add(Calendar.DAY_OF_MONTH, p.getSubmitDeadlineDays()); + return cal.getTime(); } /** @@ -494,14 +492,12 @@ public class BizMeetingController extends BaseController { /** request body for audit endpoints */ public static class AuditBody { - private String auditType; // MATERIAL / VOUCHER / BOTH private Boolean approved; // true=通过 false=拒绝 private String opinion; // 意见 - public String getAuditType() { return auditType; } - public void setAuditType(String auditType) { this.auditType = auditType; } public Boolean getApproved() { return approved; } public void setApproved(Boolean approved) { this.approved = approved; } public String getOpinion() { return opinion; } public void setOpinion(String opinion) { this.opinion = opinion; } } + } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingExecutorController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingExecutorController.java index 6965b11..0d55f02 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingExecutorController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingExecutorController.java @@ -29,20 +29,20 @@ public class BizMeetingExecutorController extends BaseController { /** * 分配执行人员 (全删全插) - * body: { "userIds": [1, 2, 3] } + * body: { "orgIds": [1, 2, 3] } (执行方企业 org_id 列表) */ @Log(title = "会议-执行人员", businessType = BusinessType.UPDATE) @PutMapping("/{meetingId}") public AjaxResult assign(@PathVariable("meetingId") Long meetingId, @RequestBody AssignBody body) { Long assignedBy = SecurityUtils.getUserId(); - int n = bizMeetingExecutorService.replaceByMeetingId(meetingId, body.getUserIds(), assignedBy); + int n = bizMeetingExecutorService.replaceByMeetingId(meetingId, body.getOrgIds(), assignedBy); return success(n); } /** request body wrapper */ public static class AssignBody { - private List userIds; - public List getUserIds() { return userIds; } - public void setUserIds(List userIds) { this.userIds = userIds; } + private List orgIds; + public List getOrgIds() { return orgIds; } + public void setOrgIds(List orgIds) { this.orgIds = orgIds; } } } \ No newline at end of file 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 2cd7d1e..c40dc6f 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 @@ -3,15 +3,19 @@ package com.ruoyi.business.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +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.enums.BusinessType; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.business.domain.BizMeetingMaterial; import com.ruoyi.business.service.IBizMeetingMaterialService; import com.ruoyi.business.service.IBizMeetingService; +import jakarta.servlet.http.HttpServletResponse; + /** * 会议材料 Controller *

@@ -58,6 +62,23 @@ public class BizMeetingMaterialController extends BaseController { return success(saved); } + /** + * 合规/管理员 随时保存付款凭证 (LV_PAYMENT + SV_PAYMENT). + *

+ * 只清/插这两个 subType, 不碰其他材料; 付款凭证非发票, 不触发 OCR, 不影响会议费用. + * 结算前必须先已上传保存; 结算接口只校验凭证是否已存在. + */ + @Log(title = "会议材料", businessType = BusinessType.UPDATE) + @PostMapping("/{meetingId}/saveVouchers") + public AjaxResult saveVouchers(@PathVariable("meetingId") Long meetingId, @RequestBody List vouchers) { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"manager".equals(roleType) && !"admin".equals(roleType)) { + throw new ServiceException("只有合规或管理员可上传凭证"); + } + bizMeetingMaterialService.saveVouchers(meetingId, vouchers); + return success(); + } + /** * 扫码拍照回传 (公开端点, ry-h5 手机端拍照直传 OSS 后回传 URL). *

@@ -69,4 +90,91 @@ public class BizMeetingMaterialController extends BaseController { bizMeetingMaterialService.upsertFromCamera(body.getMeetingId(), body.getSubType(), body.getOssUrl(), body.getExtraOssUrl()); return success(); } + + /** + * 会务下载: 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打 zip, 返回下载 URL. + * 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮). + */ + @GetMapping("/{meetingId}/downloadZip") + public AjaxResult downloadZip(@PathVariable("meetingId") Long meetingId) { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"admin".equals(roleType) && !"manager".equals(roleType)) { + throw new ServiceException("只有管理员或合规经理可下载会务材料"); + } + // 注意: 不能用 success(String), 否则 URL 会被塞进 msg 字段 (BaseController.success(String) 重载陷阱) + return AjaxResult.success("操作成功", bizMeetingMaterialService.buildServiceZipUrl(meetingId)); + } + + /** + * 劳务下载: 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息在 OSS 端打 zip, 返回下载 URL. + * 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮). + */ + @GetMapping("/{meetingId}/downloadLaborZip") + public AjaxResult downloadLaborZip(@PathVariable("meetingId") Long meetingId) { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"admin".equals(roleType) && !"manager".equals(roleType)) { + throw new ServiceException("只有管理员或合规经理可下载劳务材料"); + } + return AjaxResult.success("操作成功", bizMeetingMaterialService.buildLaborZipUrl(meetingId)); + } + + /** + * 批量会务下载: 多会议会务材料合并打一个 zip, 返回下载 URL. + * body: { "meetingIds": [1,2,3] }, 仅 admin/manager. + */ + @PostMapping("/batchDownloadZip") + public AjaxResult batchDownloadZip(@RequestBody(required = false) BatchDownloadBody body) { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"admin".equals(roleType) && !"manager".equals(roleType)) { + throw new ServiceException("只有管理员或合规经理可下载会务材料"); + } + List meetingIds = body == null ? null : body.getMeetingIds(); + return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchServiceZipUrl(meetingIds)); + } + + /** + * 批量劳务下载: 多会议劳务材料(含参会人信息)合并打一个 zip, 返回下载 URL. + * body: { "meetingIds": [1,2,3] }, 仅 admin/manager. + */ + @PostMapping("/batchDownloadLaborZip") + public AjaxResult batchDownloadLaborZip(@RequestBody(required = false) BatchDownloadBody body) { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"admin".equals(roleType) && !"manager".equals(roleType)) { + throw new ServiceException("只有管理员或合规经理可下载劳务材料"); + } + List meetingIds = body == null ? null : body.getMeetingIds(); + return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchLaborZipUrl(meetingIds)); + } + + /** + * 会务材料"打包上传" — 下载空目录模板 zip (会务材料/物料制作·酒店·…/ 全空目录). + */ + @GetMapping("/serviceTemplate/{meetingId}") + public void serviceTemplate(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception { + byte[] data = bizMeetingMaterialService.buildServiceTemplateZip(meetingId); + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment;filename=service-material-template.zip"); + response.getOutputStream().write(data); + response.getOutputStream().flush(); + } + + /** + * 会务材料"打包上传" — 上传 zip, 解压后按目录名匹配 subType, 单文件直传 OSS / 多文件打 zip 传 OSS, + * 回填 biz_meeting_material. 材料变化 → 会议费用待重算. + */ + @Log(title = "会务材料回填", businessType = BusinessType.UPDATE) + @PostMapping("/uploadServiceMaterials") + public AjaxResult uploadServiceMaterials(@RequestParam("file") MultipartFile file, + @RequestParam("meetingId") Long meetingId) throws Exception { + int updated = bizMeetingMaterialService.uploadServiceMaterials(file, meetingId); + bizMeetingService.markFeeCalcPending(meetingId); + return success(updated); + } + + /** request body for batch download endpoints */ + public static class BatchDownloadBody { + private List meetingIds; + public List getMeetingIds() { return meetingIds; } + public void setMeetingIds(List meetingIds) { this.meetingIds = meetingIds; } + } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSupervisorController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSupervisorController.java index a047cdd..3e53d20 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSupervisorController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSupervisorController.java @@ -29,20 +29,20 @@ public class BizMeetingSupervisorController extends BaseController { /** * 分配监察员 (全删全插) - * body: { "userIds": [1, 2, 3] } + * body: { "orgIds": [1, 2, 3] } (支持方企业 org_id 列表) */ @Log(title = "会议-监察员", businessType = BusinessType.UPDATE) @PutMapping("/{meetingId}") public AjaxResult assign(@PathVariable("meetingId") Long meetingId, @RequestBody AssignBody body) { Long assignedBy = SecurityUtils.getUserId(); - int n = bizMeetingSupervisorService.replaceByMeetingId(meetingId, body.getUserIds(), assignedBy); + int n = bizMeetingSupervisorService.replaceByMeetingId(meetingId, body.getOrgIds(), assignedBy); return success(n); } /** request body wrapper */ public static class AssignBody { - private List userIds; - public List getUserIds() { return userIds; } - public void setUserIds(List userIds) { this.userIds = userIds; } + private List orgIds; + public List getOrgIds() { return orgIds; } + public void setOrgIds(List orgIds) { this.orgIds = orgIds; } } } \ No newline at end of file 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 f50da60..6664379 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 @@ -2,14 +2,19 @@ package com.ruoyi.business.controller; import java.util.List; import java.util.Map; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +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.utils.poi.ExcelUtil; import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.domain.dto.ImportResult; +import com.ruoyi.business.domain.vo.BizOrgImportVo; import com.ruoyi.business.service.IBizOrgService; import com.ruoyi.common.utils.SecurityUtils; @@ -111,4 +116,25 @@ public class BizOrgController extends BaseController { public AjaxResult remove(@PathVariable Long[] orgIds) { return toAjax(bizOrgService.deleteByPrimaryKeys(orgIds)); } + + /** + * 下载支持单位导入模板 + */ + @GetMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) { + ExcelUtil util = new ExcelUtil<>(BizOrgImportVo.class); + util.importTemplateExcel(response, "支持单位数据"); + } + + /** + * 批量导入支持单位 (只导 org, 不新增人员 — 不建 sys_user, userId 留空) + * 返回 {@link ImportResult} 含成功/失败计数 + 失败明细 (行号 + 原因) + */ + @Log(title = "公司管理", businessType = BusinessType.IMPORT) + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file) throws Exception { + String operName = SecurityUtils.getUsername(); + ImportResult result = bizOrgService.importOrg(file, operName); + return success(result); + } } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java index d58fee7..4175273 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java @@ -13,6 +13,7 @@ import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.business.domain.BizPerson; +import com.ruoyi.business.domain.BizPersonExecutorImportVO; import com.ruoyi.business.domain.BizPersonImportVO; import com.ruoyi.business.service.IBizPersonService; import com.ruoyi.common.core.domain.entity.SysUser; @@ -37,7 +38,7 @@ public class BizPersonController extends BaseController * (sys_user.parent_user_id → sys_user.user_id → biz_person.user_id 链路过滤) */ private static final java.util.Set BUSINESS_MAIN_ROLES = java.util.Set.of( - "executor", "sponsor", "doctor", "manager" + "executor", "sponsor", "doctor" ); @GetMapping("/list") @@ -113,6 +114,16 @@ public class BizPersonController extends BaseController bizPerson.setUpdateBy(getUsername()); return toAjax(bizPersonService.updateByPrimaryKey(bizPerson)); } + /** + * 更换机构管理员 (admin/sponsor-people 管理员 switch) + * body: { personId } — 把该人员晋升为机构 MAIN, 原管理员降为 SUB + */ + @Log(title = "更换机构管理员", businessType = BusinessType.UPDATE) + @PutMapping("/changeAdmin") + public AjaxResult changeAdmin(@RequestBody BizPerson bizPerson) + { + return toAjax(bizPersonService.changeOrgAdmin(bizPerson.getPersonId())); + } @Log(title = "人员", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") public AjaxResult remove(@PathVariable String[] ids) @@ -124,7 +135,7 @@ public class BizPersonController extends BaseController /** * 下载 sponsor 端人员导入模板 (.xlsx) - * 模板列头按 BizPersonImportVO @Excel 注解自动生成 (中文: 姓名/手机号/工作单位/部门/职务/角色/状态) + * 模板列头按 BizPersonImportVO @Excel 注解自动生成 (中文: 姓名/手机号/部门/职务) */ @GetMapping("/sponsorImportTemplate") public void sponsorImportTemplate(HttpServletResponse response) @@ -140,9 +151,10 @@ public class BizPersonController extends BaseController */ @Log(title = "人员批量导入", businessType = BusinessType.INSERT) @PostMapping("/sponsorImport") - public AjaxResult sponsorImport(@RequestParam("file") MultipartFile file) throws Exception + public AjaxResult sponsorImport(@RequestParam("file") MultipartFile file, + @RequestParam(value = "orgId", required = false) Long orgId) throws Exception { - return doImport(file, "sponsor"); + return doImport(file, "sponsor", orgId); } /** @@ -152,22 +164,23 @@ public class BizPersonController extends BaseController @GetMapping("/executorImportTemplate") public void executorImportTemplate(HttpServletResponse response) { - ExcelUtil util = new ExcelUtil<>(BizPersonImportVO.class); + ExcelUtil util = new ExcelUtil<>(BizPersonExecutorImportVO.class); util.importTemplateExcel(response, "人员导入"); } @Log(title = "人员批量导入", businessType = BusinessType.INSERT) @PostMapping("/executorImport") - public AjaxResult executorImport(@RequestParam("file") MultipartFile file) throws Exception + public AjaxResult executorImport(@RequestParam("file") MultipartFile file, + @RequestParam(value = "orgId", required = false) Long orgId) throws Exception { - return doImport(file, "executor"); + return doExecutorImport(file, orgId); } /** * 通用人员导入逻辑 (sponsor / executor 共用) * @param unitType 'sponsor' 或 'executor' - 决定 orgName 匹配和 person.unitType 字段 */ - private AjaxResult doImport(MultipartFile file, String unitType) throws Exception + private AjaxResult doImport(MultipartFile file, String unitType, Long orgId) throws Exception { if (file == null || file.isEmpty()) { throw new ServiceException("请选择要上传的文件"); @@ -187,16 +200,14 @@ public class BizPersonController extends BaseController r.put("name", row.getName()); r.put("phone", row.getPhone()); try { - // 1. orgId 由 BizPersonServiceImpl.insert 按 mainUid + unitType 自动反查主账号 biz_org 兜底 (无需 Excel 提供) - // 2. role 跟 unitType 强绑定: sponsor→supervisor, executor→meetingExecutor (无需 Excel 提供) - BizPerson p = new BizPerson(); p.setName(row.getName()); p.setPhone(row.getPhone()); - // p.setOrgId(orgId); ← 不再设置, service 层按 mainUserId 兜底 + if (orgId != null) { + p.setOrgId(orgId); + } p.setDepartment(row.getDepartment()); p.setPosition(row.getPosition()); - p.setRole("sponsor".equals(unitType) ? "supervisor" : "meetingExecutor"); p.setStatus("0"); // 默认正常 p.setUnitType(unitType); p.setLoginUsername(row.getPhone()); // 用户名 = 手机号 (要求唯一) @@ -219,4 +230,62 @@ public class BizPersonController extends BaseController ajax.put("results", results); return ajax; } + + /** + * executor 端人员导入 (模板多一个必填「邮箱」列, 与 sponsor 分开, 不污染 sponsor 模板) + */ + private AjaxResult doExecutorImport(MultipartFile file, Long orgId) throws Exception + { + if (file == null || file.isEmpty()) { + throw new ServiceException("请选择要上传的文件"); + } + ExcelUtil util = new ExcelUtil<>(BizPersonExecutorImportVO.class); + List rows = util.importExcel(file.getInputStream(), 0); + if (rows == null || rows.isEmpty()) { + throw new ServiceException("导入文件无有效数据"); + } + Long mainUid = getUserId(); + List> results = new ArrayList<>(); + int ok = 0; + for (int i = 0; i < rows.size(); i++) { + BizPersonExecutorImportVO row = rows.get(i); + java.util.Map r = new java.util.LinkedHashMap<>(); + r.put("rowNo", i + 2); + r.put("name", row.getName()); + r.put("phone", row.getPhone()); + try { + if (row.getEmail() == null || row.getEmail().trim().isEmpty()) { + throw new ServiceException("邮箱不能为空"); + } + BizPerson p = new BizPerson(); + p.setName(row.getName()); + p.setPhone(row.getPhone()); + p.setEmail(row.getEmail()); + p.setDepartment(row.getDepartment()); + p.setPosition(row.getPosition()); + p.setStatus("0"); + p.setUnitType("executor"); + if (orgId != null) { + p.setOrgId(orgId); + } + p.setLoginUsername(row.getPhone()); + p.setLoginPassword("123456"); + p.setCreateBy(getUsername()); + p.setUpdateBy(getUsername()); + bizPersonService.insert(p, mainUid); + ok++; + r.put("ok", true); + r.put("message", "成功"); + } catch (Exception e) { + r.put("ok", false); + r.put("message", e.getMessage()); + } + results.add(r); + } + AjaxResult ajax = success(); + ajax.put("total", rows.size()); + ajax.put("ok", ok); + ajax.put("results", results); + return ajax; + } } 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 0a58291..9a418db 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 @@ -17,9 +17,11 @@ import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.business.domain.BizExecutionIntent; +import com.ruoyi.business.domain.BizOrg; import com.ruoyi.business.domain.BizProject; import com.ruoyi.business.domain.BizProjectAssign; import com.ruoyi.business.domain.BizProjectRating; +import com.ruoyi.business.service.IBizOrgService; import com.ruoyi.business.service.IBizProjectService; import com.ruoyi.business.service.IBizExecutionIntentService; import com.ruoyi.business.domain.BizProjectSponsorAssign; @@ -62,6 +64,8 @@ public class BizProjectController extends BaseController private BizSysUserQueryMapper bizSysUserQueryMapper; @Autowired private SysUserMapper sysUserMapper; + @Autowired + private IBizOrgService bizOrgService; /** * 我报名的项目 (当前用户在 biz_execution_intent 里有意向的项目) @@ -75,10 +79,7 @@ public class BizProjectController extends BaseController q.setUserId(uid); List intents = bizExecutionIntentService.selectList(q); if (intents == null || intents.isEmpty()) { - TableDataInfo r = new TableDataInfo(); - r.setRows(new ArrayList<>()); - r.setTotal(0); - return r; + return getDataTable(new ArrayList<>()); } java.util.Set noSet = new java.util.LinkedHashSet<>(); for (BizExecutionIntent it : intents) { @@ -198,15 +199,30 @@ public class BizProjectController extends BaseController @DeleteMapping("/{ids}") public AjaxResult remove(@PathVariable Long[] ids) { - // 后端角色兜底: 仅 admin / manager 可删项目 (前端 Projects.vue 已 v-if, 此处防 devtools 绕过) + // 后端角色兜底: 仅 admin 可删项目 (前端 Projects.vue 已 v-if, 此处防 devtools 绕过) String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); - if (!"admin".equals(roleType) && !"manager".equals(roleType)) { - throw new ServiceException("只有管理员或经理可删除项目"); + if (!"admin".equals(roleType)) { + throw new ServiceException("只有管理员可删除项目"); } bizProjectService.softDeleteCascadeBatch(ids); return success(); } + /** + * 删除公告: 将 3 个公示 URL 字段置 NULL (未发布). + * 后端角色兜底: 仅 admin / manager 可删公告 (前端 Projects.vue 已 v-if, 此处防 devtools 绕过). + */ + @Log(title = "项目", businessType = BusinessType.UPDATE) + @DeleteMapping("/{projectId}/announcement") + public AjaxResult clearAnnouncement(@PathVariable("projectId") Long projectId) + { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"admin".equals(roleType) && !"manager".equals(roleType)) { + throw new ServiceException("只有管理员或经理可删除公告"); + } + return toAjax(bizProjectService.clearAnnouncement(projectId)); + } + // ========== 项目执行方分配 sub-resource ========== @GetMapping("/{projectId}/assigns") public AjaxResult getAssigns(@PathVariable("projectId") Long projectId) @@ -220,12 +236,12 @@ public class BizProjectController extends BaseController { if (assigns == null) assigns = new ArrayList<>(); bizProjectAssignService.validateSum(projectId, assigns); - // #3 通知去重: 拉旧数据按 execUserId 索引, 同 (execUserId, sessions, amount) → 无变化 → 跳过 - // 业务场景: manager 仅调整其他执行人时, 老的 executor 不应被打扰 + // #3 通知去重: 拉旧数据按 executionUnitId 索引, 同 (executionUnitId, sessions, amount) → 无变化 → 跳过 + // 业务场景: manager 仅调整其他执行单位时, 老的 executor 不应被打扰 List oldList = bizProjectAssignService.selectByProjectId(projectId); Map oldByExec = new HashMap<>(); for (BizProjectAssign o : oldList) { - if (o.getExecUserId() != null) oldByExec.put(o.getExecUserId(), o); + if (o.getExecutionUnitId() != null) oldByExec.put(o.getExecutionUnitId(), o); } bizProjectAssignService.deleteByProjectId(projectId); // #3 通知: 查一次项目名, 避免循环里重复查 DB @@ -235,16 +251,18 @@ public class BizProjectController extends BaseController a.setProjectId(projectId); if (a.getStatus() == null) a.setStatus("0"); bizProjectAssignService.insert(a); - // 跳过空 execUserId 行 (notify 内部也会跳过, 这里直接 continue 省 lookup) - if (a.getExecUserId() == null) continue; - BizProjectAssign old = oldByExec.get(a.getExecUserId()); + // 跳过空 executionUnitId 行 (notify 内部也会跳过, 这里直接 continue 省 lookup) + if (a.getExecutionUnitId() == null) continue; + BizProjectAssign old = oldByExec.get(a.getExecutionUnitId()); if (isAssignUnchanged(old, a)) { - logger.debug("[projectAssign] execUserId={} (sessions, amount) 未变, 跳过通知", a.getExecUserId()); + logger.debug("[projectAssign] executionUnitId={} (sessions, amount) 未变, 跳过通知", a.getExecutionUnitId()); continue; } // #3 通知被分配的 executor (待办: 去承接). 业务事务回滚时通知自动回滚 + // 执行单位 org_id → 主账号 MAIN user_id (biz_org.user_id) 派生 + Long mainUserId = resolveMainUserIdByOrgId(a.getExecutionUnitId()); bizNotifyService.projectAssignedToExecutor( - a.getExecUserId(), projectId, projectName, a.getSessions(), a.getAmount()); + mainUserId, projectId, projectName, a.getSessions(), a.getAmount()); } return success(); } @@ -264,6 +282,13 @@ public class BizProjectController extends BaseController return oa.compareTo(na) == 0; } + /** 执行单位 org_id → 主账号 MAIN user_id (biz_org.user_id), 无主账号返回 null */ + private Long resolveMainUserIdByOrgId(Long orgId) { + if (orgId == null) return null; + BizOrg org = bizOrgService.getById(orgId); + return org == null ? null : org.getUserId(); + } + /** * 比较两条 sponsor 分配是否对监察员而言"未变". * 判定维度: assignDesc + assignPoints. (projectId 隐含相同, 旧数据就是本 projectId 的) @@ -374,7 +399,7 @@ public class BizProjectController extends BaseController } } body.setCreateBy(SecurityUtils.getUsername()); - body.setSponsorUserId(SecurityUtils.getUserId()); + body.setSponsorOrgId(bizOrgService.selectOrgIdByUserId(SecurityUtils.getUserId())); // 一项目支持 N 监察员: service 内一次性 delete + 逐个 insert, 不会循环 delete int inserted = bizProjectSponsorAssignService.assignMonitorsForProject(body, mids); @@ -427,7 +452,7 @@ public class BizProjectController extends BaseController } } body.setCreateBy(SecurityUtils.getUsername()); - body.setExecutorUserId(SecurityUtils.getUserId()); + body.setExecutorOrgId(bizOrgService.selectOrgIdByUserId(SecurityUtils.getUserId())); // 一项目支持 N 执行人: service 内一次性 delete + 逐个 insert, 不会循环 delete int inserted = bizProjectExecutorAssignService.assignStaffForProject(body, sids); return toAjax(inserted); @@ -453,7 +478,7 @@ public class BizProjectController extends BaseController public TableDataInfo listByRole(SysUserExtendVo vo) { if (vo.getRoleType() == null || vo.getRoleType().isEmpty()) { - return new TableDataInfo(); + return getDataTable(new ArrayList<>()); } startPage(); List list = bizSysUserQueryMapper.selectActiveByRole(vo); @@ -483,7 +508,7 @@ public class BizProjectController extends BaseController throw new IllegalArgumentException("projectId / monitorUserId 必填"); } body.setCreateBy(loginName); - body.setSponsorUserId(loginUid); + body.setSponsorOrgId(bizOrgService.selectOrgIdByUserId(loginUid)); // 通知去重: 拉旧分配, 找同 monitorUserId, 比较 assignDesc/assignPoints 是否变化 BizProjectSponsorAssign old = null; List oldList = bizProjectSponsorAssignService.listByProjectId(body.getProjectId()); 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 33c4381..4b6c00b 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 @@ -61,7 +61,7 @@ public class BizMeeting extends BaseEntity { private Long projectId; /** 项目名称 */ private String projectName; - /** 所属公司名称 (冗余字段, 由 biz_project.sponsor_admin_user_name 同步) */ + /** 所属公司名称 (派生字段, 由 biz_project.sponsor_org_id JOIN biz_org.org_name 得出, 不落库) */ private String orgName; /** 监察意见 */ private String supervisionOpinion; @@ -72,8 +72,6 @@ public class BizMeeting extends BaseEntity { private Date supervisionTime; /** 材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */ private String materialAuditStage; - /** 凭证审核阶段 (同上) */ - private String voucherAuditStage; /** 是否执行 0否1是 (会议开始时间到, scheduler 置1) */ private Integer isExecuted; /** 执行时间 */ @@ -97,15 +95,15 @@ public class BizMeeting extends BaseEntity { /** 材料最近一次审核动作时间 */ @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 */ + /** 提交截止时间 (建会 = end_time + 项目 submit_deadline_days 天; 退回/解冻 = now + 天数; null = 项目未设天数, 永不冻结) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submitDeadline; + /** 邀请函URL (会议级 biz_meeting.invitation_url) */ private String invitationUrl; + /** 项目邀请函URL (派生字段, 由 biz_project.invitation_url 子查询得出, 不落库; 医生会议列表用) */ + private String projectInvitationUrl; /** 日程海报URL */ private String scheduleUrl; /** 生成的海报URL (生成海报按钮产出) */ @@ -175,6 +173,8 @@ public class BizMeeting extends BaseEntity { public void setSupervisionTime(Date supervisionTime) { this.supervisionTime = supervisionTime; } public String getInvitationUrl() { return invitationUrl; } public void setInvitationUrl(String invitationUrl) { this.invitationUrl = invitationUrl; } + public String getProjectInvitationUrl() { return projectInvitationUrl; } + public void setProjectInvitationUrl(String projectInvitationUrl) { this.projectInvitationUrl = projectInvitationUrl; } public String getScheduleUrl() { return scheduleUrl; } public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; } public String getPosterUrl() { return posterUrl; } @@ -183,8 +183,6 @@ public class BizMeeting extends BaseEntity { 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; } @@ -203,12 +201,10 @@ public class BizMeeting extends BaseEntity { public void setFreezeTime(Date freezeTime) { this.freezeTime = freezeTime; } public Date getMaterialAuditTime() { return materialAuditTime; } public void setMaterialAuditTime(Date materialAuditTime) { this.materialAuditTime = materialAuditTime; } - public Date 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 Date getSubmitDeadline() { return submitDeadline; } + public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getAttendeeId() { return attendeeId; } 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 96b0833..25054a3 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAuditLog.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAuditLog.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; * 会议审核流程日志对象 biz_meeting_audit_log *

* 记录会议审核的每一次流转: 谁、什么时间、什么意见、当前阶段. - * 与 biz_meeting.material_audit_stage / voucher_audit_stage 配合, 还原审核轨迹. + * 与 biz_meeting.material_audit_stage 配合, 还原审核轨迹. */ public class BizMeetingAuditLog { diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingExecutor.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingExecutor.java index 50b2be9..d1683f8 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingExecutor.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingExecutor.java @@ -18,8 +18,11 @@ public class BizMeetingExecutor { /** 会议ID */ private Long meetingId; - /** sys_user.user_id (executor 主账号, 后续支持子账号) */ - private Long userId; + /** executor 企业 org_id (原 sys_user.user_id 主账号, 单一可信源) */ + private Long executorOrgId; + + /** 执行企业名称 (派生字段, JOIN biz_org.org_name, 不落库) */ + private String orgName; /** 分配人 user_id (审计) */ private Long assignedBy; @@ -37,8 +40,11 @@ public class BizMeetingExecutor { public Long getMeetingId() { return meetingId; } public void setMeetingId(Long meetingId) { this.meetingId = meetingId; } - public Long getUserId() { return userId; } - public void setUserId(Long userId) { this.userId = userId; } + public Long getExecutorOrgId() { return executorOrgId; } + public void setExecutorOrgId(Long executorOrgId) { this.executorOrgId = executorOrgId; } + + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } public Long getAssignedBy() { return assignedBy; } public void setAssignedBy(Long assignedBy) { this.assignedBy = assignedBy; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSupervisor.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSupervisor.java index f362c16..028a7d6 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSupervisor.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSupervisor.java @@ -18,8 +18,11 @@ public class BizMeetingSupervisor { /** 会议ID */ private Long meetingId; - /** sys_user.user_id (sponsor 主账号, 后续支持子账号) */ - private Long userId; + /** sponsor 企业 org_id (原 sys_user.user_id 主账号, 单一可信源) */ + private Long sponsorOrgId; + + /** 监察企业名称 (派生字段, JOIN biz_org.org_name, 不落库) */ + private String orgName; /** 分配人 user_id (审计) */ private Long assignedBy; @@ -37,8 +40,11 @@ public class BizMeetingSupervisor { public Long getMeetingId() { return meetingId; } public void setMeetingId(Long meetingId) { this.meetingId = meetingId; } - public Long getUserId() { return userId; } - public void setUserId(Long userId) { this.userId = userId; } + public Long getSponsorOrgId() { return sponsorOrgId; } + public void setSponsorOrgId(Long sponsorOrgId) { this.sponsorOrgId = sponsorOrgId; } + + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } public Long getAssignedBy() { return assignedBy; } public void setAssignedBy(Long assignedBy) { this.assignedBy = assignedBy; } 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 50d287e..f5ef8d3 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 @@ -29,9 +29,6 @@ public class BizPerson extends BaseEntity { /** position */ @Excel(name = "position") private String position; - /** role */ - @Excel(name = "role") - private String role; /** create_by */ @Excel(name = "create_by") private String createBy; @@ -60,6 +57,8 @@ public class BizPerson extends BaseEntity { /** 登录账号 (来自 sys_user.user_name, 跟 accountType / parentUserId 一样仅展示, 不入库) */ @com.fasterxml.jackson.annotation.JsonProperty("account") private String account; + /** 邮箱 (来自 sys_user.email, 展示 + 前端回传写入 sys_user, 不入 biz_person 表) */ + private String email; /** 子账号登录账号 (前端传入, 用于创建 sys_user 子账号) - 非持久化字段 */ @com.fasterxml.jackson.annotation.JsonProperty("userName") private transient String loginUsername; @@ -82,8 +81,6 @@ public class BizPerson extends BaseEntity { public void setDepartment(String department) { this.department = department; } public String getPosition() { return position; } public void setPosition(String position) { this.position = position; } - public String getRole() { return role; } - public void setRole(String role) { this.role = role; } /** 兼容前端调用, 实际读 sys_user.delFlag */ public String getDelFlag() { return null; } public void setDelFlag(String delFlag) { /* noop - 跟 sys_user 同步 */ } @@ -107,6 +104,8 @@ public class BizPerson extends BaseEntity { public void setParentUserId(Long parentUserId) { this.parentUserId = parentUserId; } public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } 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/BizPersonExecutorImportVO.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonExecutorImportVO.java new file mode 100644 index 0000000..37e0565 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonExecutorImportVO.java @@ -0,0 +1,49 @@ +package com.ruoyi.business.domain; + +import com.ruoyi.common.annotation.Excel; + +/** + * executor 端人员批量导入 VO (中文列头, 用 ruoyi ExcelUtil 解析) + * + *

区别于 sponsor 端 {@link BizPersonImportVO}: executor 多一个必填「邮箱」列, + * 列头 5 列: 姓名/手机号/邮箱/部门/职务. 所属公司由后端兜底: + *

    + *
  • 所属公司: 由 BizPersonServiceImpl.insert 按 mainUid + unitType 自动反查主账号的 biz_org
  • + *
+ * + *

导入后端会按以下规则创建 sys_user 子账号 (parent_user_id=当前登录主账号): + *

    + *
  • loginUsername = 手机号 (要求唯一)
  • + *
  • loginPassword = "123456" (默认密码)
  • + *
  • email = 邮箱 (必填, 写入 sys_user.email)
  • + *
  • unitType = "executor"
  • + *
+ */ +public class BizPersonExecutorImportVO +{ + @Excel(name = "姓名", sort = 1) + private String name; + + @Excel(name = "手机号", sort = 2) + private String phone; + + @Excel(name = "邮箱", sort = 3) + private String email; + + @Excel(name = "部门", sort = 4) + private String department; + + @Excel(name = "职务", sort = 5) + private String position; + + 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 getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getPosition() { return position; } + public void setPosition(String position) { this.position = position; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java index e34b5b3..cccd8cb 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java @@ -6,10 +6,9 @@ import com.ruoyi.common.annotation.Excel; * sponsor 端人员批量导入 VO (中文列头, 用 ruoyi ExcelUtil 解析) * *

区别于 BizPerson 实体: 这里只暴露用户能填写的业务字段, 不包含内部 id/审计字段, - * 列头精简到 4 列: 姓名/手机号/部门/职务. 所属公司 + 角色 由后端兜底: + * 列头精简到 4 列: 姓名/手机号/部门/职务. 所属公司由后端兜底: *

    *
  • 所属公司: 由 BizPersonServiceImpl.insert 按 mainUid + unitType 自动反查主账号的 biz_org
  • - *
  • 角色: sponsor → supervisor (监察员), executor → meetingExecutor (会议执行), 跟 unitType 强绑定
  • *
* *

导入后端会按以下规则创建 sys_user 子账号 (parent_user_id=当前登录主账号): @@ -20,7 +19,7 @@ import com.ruoyi.common.annotation.Excel; *

  • orgId = service 层按 mainUserId + unitType 兜底 (无需 Excel 提供)
  • * * - *

    历史兼容: orgName/role 字段及 setter 保留, 但不再标注 @Excel; 用户上传老模板多列也能解析, 值被忽略. + *

    历史兼容: orgName 字段及 setter 保留, 但不再标注 @Excel; 用户上传老模板多列也能解析, 值被忽略. */ public class BizPersonImportVO { @@ -40,10 +39,6 @@ public class BizPersonImportVO @Excel(name = "职务", sort = 4) private String position; - // 角色: 跟 unitType 强绑定, sponsor→supervisor / executor→meetingExecutor, 无需用户填 - // 保留 setter 兼容老模板, 但不再导出为 Excel 列 - private String role; - public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } @@ -54,6 +49,4 @@ public class BizPersonImportVO public void setDepartment(String department) { this.department = department; } public String getPosition() { return position; } public void setPosition(String position) { this.position = position; } - public String getRole() { return role; } - public void setRole(String role) { this.role = role; } } 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 a550186..6fe51ae 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 @@ -56,12 +56,12 @@ public class BizProject extends BaseEntity { private String sponsorAdminUserName; /** 支持单位名称 (列表展示列, JOIN biz_org 取 org_name, org_type='sponsor') */ private String sponsorOrgName; - /** 支持单位筛选 (sys_user.user_id 列表, IN 查询, 多选 select 透传) */ - private List sponsorAdminUserIds; + /** 支持单位筛选 (biz_org.org_id 列表, IN 查询, 多选 select 透传) */ + private List sponsorOrgIds; /** 服务机构名称列表, 多个用英文逗号连接 (GROUP_CONCAT 派生, 不入库, 仅展示) */ private String execOrgNames; - /** 服务机构筛选 (biz_project_assign.exec_user_id 列表, IN 查询, 多选 select 透传) */ - private List execAdminUserIds; + /** 服务机构筛选 (biz_project_assign.execution_unit_id 列表, IN 查询, 多选 select 透传) */ + private List execOrgIds; /** project_form */ @Excel(name = "project_form") private String projectForm; @@ -71,9 +71,9 @@ public class BizProject extends BaseEntity { /** is_settled */ @Excel(name = "is_settled") private String isSettled; - /** 支持方负责人用户ID (sys_user.user_id, role_type=sponsor) — 原 org_id */ - @Excel(name = "sponsor_admin_user_id") - private Long sponsorAdminUserId; + /** 支持方企业ID (biz_org.org_id, org_type='sponsor') — 单一可信源, 负责人 MAIN user_id 由 biz_org.user_id 派生 */ + @Excel(name = "sponsor_org_id") + private Long sponsorOrgId; /** 项目负责人 user_id (sys_user.user_id, role_type=manager 合规管理员) */ @Excel(name = "lead_user_id") private Long leadUserId; @@ -122,6 +122,8 @@ public class BizProject extends BaseEntity { private String supportLetterUrl; /** 已发布公告URL */ private String publishUrl; + /** 日程文件URL */ + private String scheduleUrl; /** 是否已发布公示 0否 1是 */ private String isPublished; /** 发布时间 */ @@ -129,6 +131,11 @@ public class BizProject extends BaseEntity { private Date publishTime; /** 公告类型 (邀请函/支持函/通知/日程/公示) */ private String announcementType; + /** 开通截止时间 (date, 到期后由 OpenStatusScheduler 置 open_status=N) */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date openDeadline; + /** 开通状态 Y/N (默认 N; Y 由经理"开通"置位, 到期回收为 N) */ + private String openStatus; /* ============ 支持方评分字段 (来自 biz_project_sponsor 中间表, 仅展示用) ============ */ /** 当前 login 用户对该项目的平均分 */ @@ -176,20 +183,20 @@ public class BizProject extends BaseEntity { public void setSponsorAdminUserName(String sponsorAdminUserName) { this.sponsorAdminUserName = sponsorAdminUserName; } public String getSponsorOrgName() { return sponsorOrgName; } public void setSponsorOrgName(String sponsorOrgName) { this.sponsorOrgName = sponsorOrgName; } - public List getSponsorAdminUserIds() { return sponsorAdminUserIds; } - public void setSponsorAdminUserIds(List sponsorAdminUserIds) { this.sponsorAdminUserIds = sponsorAdminUserIds; } + public List getSponsorOrgIds() { return sponsorOrgIds; } + public void setSponsorOrgIds(List sponsorOrgIds) { this.sponsorOrgIds = sponsorOrgIds; } public String getExecOrgNames() { return execOrgNames; } public void setExecOrgNames(String execOrgNames) { this.execOrgNames = execOrgNames; } - public List getExecAdminUserIds() { return execAdminUserIds; } - public void setExecAdminUserIds(List execAdminUserIds) { this.execAdminUserIds = execAdminUserIds; } + public List getExecOrgIds() { return execOrgIds; } + public void setExecOrgIds(List execOrgIds) { this.execOrgIds = execOrgIds; } public String getProjectForm() { return projectForm; } public void setProjectForm(String projectForm) { this.projectForm = projectForm; } public String getIsFinished() { return isFinished; } public void setIsFinished(String isFinished) { this.isFinished = isFinished; } public String getIsSettled() { return isSettled; } public void setIsSettled(String isSettled) { this.isSettled = isSettled; } - public Long getSponsorAdminUserId() { return sponsorAdminUserId; } - public void setSponsorAdminUserId(Long sponsorAdminUserId) { this.sponsorAdminUserId = sponsorAdminUserId; } + public Long getSponsorOrgId() { return sponsorOrgId; } + public void setSponsorOrgId(Long sponsorOrgId) { this.sponsorOrgId = sponsorOrgId; } public Long getLeadUserId() { return leadUserId; } public void setLeadUserId(Long leadUserId) { this.leadUserId = leadUserId; } public String getLeadUserName() { return leadUserName; } @@ -228,12 +235,18 @@ public class BizProject extends BaseEntity { public void setSupportLetterUrl(String supportLetterUrl) { this.supportLetterUrl = supportLetterUrl; } public String getPublishUrl() { return publishUrl; } public void setPublishUrl(String publishUrl) { this.publishUrl = publishUrl; } + public String getScheduleUrl() { return scheduleUrl; } + public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; } public String getIsPublished() { return isPublished; } public void setIsPublished(String isPublished) { this.isPublished = isPublished; } public Date getPublishTime() { return publishTime; } public void setPublishTime(Date publishTime) { this.publishTime = publishTime; } public String getAnnouncementType() { return announcementType; } public void setAnnouncementType(String announcementType) { this.announcementType = announcementType; } + public Date getOpenDeadline() { return openDeadline; } + public void setOpenDeadline(Date openDeadline) { this.openDeadline = openDeadline; } + public String getOpenStatus() { return openStatus; } + public void setOpenStatus(String openStatus) { this.openStatus = openStatus; } public BigDecimal getSponsorRating() { return sponsorRating; } public void setSponsorRating(BigDecimal sponsorRating) { this.sponsorRating = sponsorRating; } public Integer getSponsorQ1() { return sponsorQ1; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java index 346d856..ebaed1d 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java @@ -14,10 +14,8 @@ public class BizProjectAssign extends BaseEntity { private String assignId; /** project_id */ private Long projectId; - /** 执行单位ID (FK: biz_org.org_id, NOT NULL) - service 层从 execUserId 反查 biz_org 写入 */ + /** 执行单位ID (FK: biz_org.org_id, NOT NULL) - 业务主键, 前端直接传, 单一可信源 */ private Long executionUnitId; - /** 执行方用户ID (sys_user.user_id, executor 主账号) - 业务主键,前端直接传 */ - private Long execUserId; /** 分配场次 */ private Integer sessions; /** 分配金额 */ @@ -35,8 +33,6 @@ public class BizProjectAssign extends BaseEntity { public void setProjectId(Long projectId) { this.projectId = projectId; } public Long getExecutionUnitId() { return executionUnitId; } public void setExecutionUnitId(Long executionUnitId) { this.executionUnitId = executionUnitId; } - public Long getExecUserId() { return execUserId; } - public void setExecUserId(Long execUserId) { this.execUserId = execUserId; } public Integer getSessions() { return sessions; } public void setSessions(Integer sessions) { this.sessions = sessions; } public BigDecimal getAmount() { return amount; } 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 61a449c..0651e88 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 @@ -9,8 +9,8 @@ public class BizProjectExecutorAssign extends BaseEntity { private static final long serialVersionUID = 1L; private Long id; private String projectId; - /** 执行方主账号 user_id (分配人, 审计) */ - private Long executorUserId; + /** 分配方企业 org_id (biz_org.org_id, 分配人审计; 原 executor_user_id) */ + private Long executorOrgId; /** 执行人 user_id (被分配) */ private Long staffUserId; /** 多个执行人 userId (前端 multi-select 传入, 控制器循环 insert, 不入库) */ @@ -35,8 +35,8 @@ public class BizProjectExecutorAssign extends BaseEntity { 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 getExecutorOrgId() { return executorOrgId; } + public void setExecutorOrgId(Long executorOrgId) { this.executorOrgId = executorOrgId; } public Long getStaffUserId() { return staffUserId; } public void setStaffUserId(Long staffUserId) { this.staffUserId = staffUserId; } public java.util.List getStaffUserIds() { return staffUserIds; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java index 97c3a2b..ecc40ba 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java @@ -30,6 +30,8 @@ public class BizProjectPlan extends BaseEntity { /** design_file_url */ @Excel(name = "design_file_url") private String designFileUrl; + /** 学科方向 */ + private String subjectDirection; /** status */ @Excel(name = "status") private String status; @@ -91,6 +93,8 @@ public class BizProjectPlan extends BaseEntity { public void setProjectForm(String projectForm) { this.projectForm = projectForm; } public String getDesignFileUrl() { return designFileUrl; } public void setDesignFileUrl(String designFileUrl) { this.designFileUrl = designFileUrl; } + public String getSubjectDirection() { return subjectDirection; } + public void setSubjectDirection(String subjectDirection) { this.subjectDirection = subjectDirection; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getIsSettled() { return isSettled; } 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 c0343d0..1098856 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 @@ -9,7 +9,8 @@ public class BizProjectSponsorAssign extends BaseEntity { private static final long serialVersionUID = 1L; private Long id; private String projectId; - private Long sponsorUserId; + /** 分配方企业 org_id (biz_org.org_id, 分配人审计; 原 sponsor_user_id) */ + private Long sponsorOrgId; private Long monitorUserId; /** 多个监察员 userId (前端 multi-select 传入, 控制器循环 insert, 不入库) */ @com.fasterxml.jackson.annotation.JsonProperty("monitorUserIds") @@ -33,8 +34,8 @@ public class BizProjectSponsorAssign extends BaseEntity { public void setId(Long id) { this.id = id; } public String getProjectId() { return projectId; } public void setProjectId(String projectId) { this.projectId = projectId; } - public Long getSponsorUserId() { return sponsorUserId; } - public void setSponsorUserId(Long sponsorUserId) { this.sponsorUserId = sponsorUserId; } + public Long getSponsorOrgId() { return sponsorOrgId; } + public void setSponsorOrgId(Long sponsorOrgId) { this.sponsorOrgId = sponsorOrgId; } public Long getMonitorUserId() { return monitorUserId; } public void setMonitorUserId(Long monitorUserId) { this.monitorUserId = monitorUserId; } public java.util.List getMonitorUserIds() { return monitorUserIds; } 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 5dbe59a..d92f8e7 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 @@ -1,6 +1,7 @@ package com.ruoyi.business.domain.vo; import java.math.BigDecimal; +import com.ruoyi.business.domain.BizMeetingAttendee; import com.ruoyi.common.annotation.Excel; /** @@ -134,4 +135,30 @@ public class BizMeetingAttendeeImportVo { public void setBankAddress(String bankAddress) { this.bankAddress = bankAddress; } public String getIdCardAttachments() { return idCardAttachments; } public void setIdCardAttachments(String idCardAttachments) { this.idCardAttachments = idCardAttachments; } + + /** 从参会人实体转导出 VO (与 /export 端点、劳务下载打包共用同一列映射) */ + public static BizMeetingAttendeeImportVo from(BizMeetingAttendee a) { + BizMeetingAttendeeImportVo v = new BizMeetingAttendeeImportVo(); + if (a == null) return v; + 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()); + return v; + } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizOrgImportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizOrgImportVo.java new file mode 100644 index 0000000..556a73a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizOrgImportVo.java @@ -0,0 +1,59 @@ +package com.ruoyi.business.domain.vo; + +import com.ruoyi.common.annotation.Excel; + +/** + * 支持单位导入 VO + * + *

    仅用于 Excel 批量导入 / 模板下载, 不参与业务逻辑. + * 字段顺序与 Excel 列一致, 调整时同步修改模板下载体验. + * + *

    只导入 org (biz_org), 不新增人员 (不建 sys_user / biz_person), + * 因此不包含登录账号相关字段. 后端固定默认值: + *

      + *
    • orgType — 固定 'sponsor' (本入口是支持单位管理页)
    • + *
    • status — 默认 '0' (正常)
    • + *
    • userId — 不设置 (无主账号, 后续单独在人员管理里关联)
    • + *
    + * + * @author guoju + */ +public class BizOrgImportVo { + + /** 公司名称 (必填) */ + @Excel(name = "公司名称", sort = 1) + private String orgName; + + /** 税号/统一社会信用代码 */ + @Excel(name = "税号/统一社会信用代码", sort = 2) + private String taxNo; + + /** 企业性质 私营/国营/中外合资/外资/其他 */ + @Excel(name = "企业性质", sort = 3) + private String businessNature; + + /** 公司地址 */ + @Excel(name = "公司地址", sort = 4) + private String address; + + /** 联系人 */ + @Excel(name = "联系人", sort = 5) + private String contactName; + + /** 联系电话 */ + @Excel(name = "联系电话", sort = 6) + private String contactPhone; + + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } + public String getTaxNo() { return taxNo; } + public void setTaxNo(String taxNo) { this.taxNo = taxNo; } + public String getBusinessNature() { return businessNature; } + public void setBusinessNature(String businessNature) { this.businessNature = businessNature; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public String getContactName() { return contactName; } + public void setContactName(String contactName) { this.contactName = contactName; } + public String getContactPhone() { return contactPhone; } + public void setContactPhone(String contactPhone) { this.contactPhone = contactPhone; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingExecutorMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingExecutorMapper.java index 5d8b5fa..667f314 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingExecutorMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingExecutorMapper.java @@ -14,9 +14,6 @@ public interface BizMeetingExecutorMapper { /** 按会议ID查该会议的所有执行人员 */ List selectByMeetingId(Long meetingId); - /** 按 userId 查该执行人员被分配到哪些会议 */ - List selectByUserId(Long userId); - /** 条件查询 */ List selectList(BizMeetingExecutor entity); 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 3c876c7..5e5e533 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 @@ -14,6 +14,10 @@ public interface BizMeetingMaterialMapper { /** 按会议ID查该会议所有材料记录 */ List selectByMeetingId(Long meetingId); + /** 按 (meetingId, subType) 查单条 (subType 会议内唯一), 用于批量插入后查回真实 id */ + BizMeetingMaterial selectByMeetingAndSubType(@org.apache.ibatis.annotations.Param("meetingId") Long meetingId, + @org.apache.ibatis.annotations.Param("subType") String subType); + /** 插入 (id 走 DB AUTO_INCREMENT, 不接受前端传入的 id) */ int insert(BizMeetingMaterial entity); @@ -37,4 +41,13 @@ public interface BizMeetingMaterialMapper { /** 单条更新 fee_status (OCR 完成/材料保存时设置 0未算 1已算) */ int updateFeeStatus(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("feeStatus") Integer feeStatus); + + /** + * 会务材料"打包上传"回填: 替换单条材料的文件 URL/文件名, 并清空金额(待重新 OCR) + 置 fee_status. + * 与 updateByPrimaryKey 不同, 这里显式把 amount 置 null (updateByPrimaryKey 的 if 会跳过 null). + */ + int updateFile(@org.apache.ibatis.annotations.Param("id") Long id, + @org.apache.ibatis.annotations.Param("ossUrl") String ossUrl, + @org.apache.ibatis.annotations.Param("fileName") String fileName, + @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/BizMeetingSupervisorMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingSupervisorMapper.java index 5a340c5..c63db5d 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingSupervisorMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingSupervisorMapper.java @@ -14,9 +14,6 @@ public interface BizMeetingSupervisorMapper { /** 按会议ID查该会议的所有监察员 */ List selectByMeetingId(Long meetingId); - /** 按 userId 查该监察员被分配到哪些会议 */ - List selectByUserId(Long userId); - /** 条件查询 */ List selectList(BizMeetingSupervisor entity); 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 ad9bee8..b587725 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 @@ -10,18 +10,25 @@ public interface BizOrgMapper { int insert(BizOrg entity); int updateByPrimaryKey(BizOrg entity); int deleteByPrimaryKeys(Long[] orgIds); - /** 支持方下拉选项 (JOIN sys_user.user_name), 用于 manager 项目分配弹窗 */ + /** 支持方下拉选项 (JOIN sys_user.user_name), 用于 manager 项目分配弹窗, 返回 orgId/orgName/userName */ List> selectSponsorOrgOptions(BizOrg entity); /** 执行方下拉选项 (JOIN sys_user MAIN 账号 user_name), 用于 manager 项目分配弹窗 - * 返回 Map: userId / orgName / userName + * 返回 Map: orgId / orgName / userName * 过滤条件: orgType='executor' + sys_user MAIN 账号 (parent_user_id IS NULL) - * 注意: value 必须 MAIN user_id (写入 biz_project_assign.exec_user_id); + * 注意: value 必须 org_id (写入 biz_project_assign.execution_unit_id); * label 必须 org_name, 不用 user_name 以避免把同公司的普通员工带出来 */ List> selectExecutorOrgOptions(BizOrg entity); + /** 支持方注册下拉选项 (匿名公开, register-sponsor 选企业注册 SUB 子账号) + * 与 selectSponsorOrgOptions 区别: 不 JOIN sys_user (无主账号的 org 也可选), 返回 orgId + mainUserId + * 返回 Map: orgId / orgName / mainUserId (= biz_org.user_id, 可为 null 表示该企业暂无主账号) */ + List> selectSponsorRegisterOptions(BizOrg entity); /** 当前登录 sponsor 的所属公司 * 主账号 (sys_user.parent_user_id IS NULL): biz_org.user_id = #{userId} * 子账号 (有 biz_person 记录): biz_org.org_id = biz_person.org_id * 返回 Map: orgId / orgName / isOwner (1=主账号, 0=子账号) * 用于 /sponsor/account 页面回显 + 主账号修改 org_name */ Map selectMySponsorCompany(Long userId); + /** user_id → org_id 单一可信源反查: COALESCE(biz_org.user_id, biz_person.user_id) + * MAIN 主账号走 biz_org.user_id; SUB 子账号走 biz_person.org_id; 都没有返回 null */ + Long selectOrgIdByUserId(Long userId); } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java index d7dba34..61ad0fb 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java @@ -1,6 +1,5 @@ package com.ruoyi.business.mapper; import java.util.List; -import org.apache.ibatis.annotations.Param; import com.ruoyi.business.domain.BizPerson; /** @@ -20,6 +19,4 @@ public interface BizPersonMapper int deleteByPrimaryKeys(String[] personIds); /** 联动: person 逻辑删除后, 把对应 sys_user 子账号也逻辑删除 */ int softDeleteSysUserByPersonIds(String[] personIds); - /** 校验某 org 下是否已有 admin 角色 (注册时兜底, 保证一公司一管理员) */ - int countAdminByOrgId(@Param("orgId") Long orgId); } \ No newline at end of file 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 3e5431b..805f1cb 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 @@ -34,4 +34,8 @@ public interface BizProjectMapper * 并重算 available_amount = total_amount - manage_fee - 已支付劳务 - 已支付会务 (幂等, 无累计副作用). */ int recomputeSettledAmounts(@org.apache.ibatis.annotations.Param("projectId") Long projectId); + /** 删除公告: 将 invitation_url / support_letter_url / publish_url 置 NULL */ + int clearAnnouncement(@org.apache.ibatis.annotations.Param("projectId") Long projectId); + /** 开通到期回收: 到期(open_deadline <= 今天)的 open_status='Y' 置回 'N' */ + int closeExpiredOpenStatus(); } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/config/OcrProperties.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/config/OcrProperties.java index f18853f..7287bdc 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/config/OcrProperties.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/config/OcrProperties.java @@ -45,7 +45,7 @@ public class OcrProperties { /** 整流程 OCR 超时 (秒) */ private int totalTimeoutS = 60; /** QR 命中后是否继续跑全量 OCR + 字段抽取 (false = 快路径, 只返回 QR 3 字段) */ - private boolean qrFullOcr = true; + private boolean qrFullOcr = false; /** OCR 语言: ch (简中) / en / chinese_cht */ private String lang = "ch"; /** 模型目录: 相对路径 → classpath:models/, 绝对路径 → 直读 */ diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/service/RecognizeService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/service/RecognizeService.java index 5badece..e33fce1 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/service/RecognizeService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/service/RecognizeService.java @@ -284,6 +284,8 @@ public class RecognizeService { private InvoiceResult buildQrOnlyResult(QrDecodeResult qr, int elapsedMs, int pageCount) { InvoiceFields fields = new InvoiceFields(); + // QR 命中即证明是电子发票; isRecognizedAsInvoice 门控要求 invoiceType 非空, 这里补上类型 + fields.setInvoiceType("电子发票"); fields.setInvoiceNo(qr.getInvoiceNo()); fields.setAmount(qr.getAmount()); fields.setInvoiceDate(qr.getInvoiceDate()); 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 3c229ea..0b31fb2 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 @@ -22,6 +22,7 @@ public class OssConfMeta private final String bucket; private final String accessKeyId; private final String accessKeySecret; + private final String zipFuncUrl; public OssConfMeta(RuoYiConfig cfg) { @@ -33,6 +34,7 @@ public class OssConfMeta this.bucket = p.getBucket(); this.accessKeyId = p.getAccessKeyId(); this.accessKeySecret = p.getAccessKeySecret(); + this.zipFuncUrl = p.getZipFuncUrl(); // endpoint: 剥协议头 + 剥 bucket 前缀 → 纯 OSS endpoint (OSSClient 构造/URL 拼接都要求裸 endpoint, 不带 bucket) this.endpoint = stripBucketPrefix(stripScheme(p.getEndpoint()), this.bucket); } @@ -65,4 +67,5 @@ public class OssConfMeta public String getBucket() { return bucket; } public String getAccessKeyId() { return accessKeyId; } public String getAccessKeySecret() { return accessKeySecret; } + public String getZipFuncUrl() { return zipFuncUrl; } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssZipService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssZipService.java new file mode 100644 index 0000000..4a7bf4f --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/oss/OssZipService.java @@ -0,0 +1,177 @@ +package com.ruoyi.business.oss; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.model.DeleteObjectsRequest; +import com.aliyun.oss.model.DeleteObjectsResult; +import com.aliyun.oss.model.ObjectListing; +import com.aliyun.oss.model.ObjectMetadata; +import com.aliyun.oss.model.OSSObjectSummary; +import com.ruoyi.common.exception.ServiceException; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * 阿里云 OSS 打 zip (复用 hwt-serve 的阿里云函数计算 FC 端点) + *

    + * 思路 (与 hwt-serve OssApi.ossDownload 一致): 不在 Java 里用 ZipOutputStream 打 zip, + * 而是把"在 OSS 端打包"外包给阿里云函数计算 (FC). Java 只做: + *

      + *
    1. 把要打包的文件先 copy 到一个 staging 前缀 (目录结构 = 最终 zip 内目录结构)
    2. + *
    3. POST {bucket, source-dir} 到 FC 端点
    4. + *
    5. 读 301/302 的 Location 头, 拿到 zip 签名下载 URL
    6. + *
    + * 与 hwt-serve 的差异: 不再用 unirest (本项目无此依赖), 改用 hutool-http. + */ +@Slf4j +@Component +public class OssZipService +{ + @Autowired + private OssConfMeta ossConfMeta; + + private OSSClient newClient() + { + return new OSSClient( + ossConfMeta.getEndpoint(), + ossConfMeta.getAccessKeyId(), + ossConfMeta.getAccessKeySecret()); + } + + /** + * 从完整 ossUrl 提取 object key (去掉 https://{bucket}.{endpoint}/ 前缀, 顺带去掉 query) + * 例: https://hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com/ry8080/meeting/1/service/xxx.jpg + * → ry8080/meeting/1/service/xxx.jpg + */ + public String extractKey(String ossUrl) + { + if (ossUrl == null) return null; + String u = ossUrl; + int q = u.indexOf('?'); + if (q >= 0) u = u.substring(0, q); + + String httpsHost = "https://" + ossConfMeta.getBucket() + "." + ossConfMeta.getEndpoint() + "/"; + if (u.startsWith(httpsHost)) return u.substring(httpsHost.length()); + String httpHost = "http://" + ossConfMeta.getBucket() + "." + ossConfMeta.getEndpoint() + "/"; + if (u.startsWith(httpHost)) return u.substring(httpHost.length()); + + // 兜底: 取 "://" 之后第一个 "/" 之后的部分 (CNAME / 自定义域名) + int i = u.indexOf("://"); + if (i >= 0) + { + int slash = u.indexOf('/', i + 3); + if (slash >= 0) return u.substring(slash + 1); + } + return u; + } + + /** OSS 服务端 copy (同 bucket 内), 用于把散落的材料文件复制到 staging 前缀, 不经过 Java 内存 */ + public void copyObject(String srcKey, String dstKey) + { + OSSClient client = newClient(); + try + { + client.copyObject(ossConfMeta.getBucket(), srcKey, ossConfMeta.getBucket(), dstKey); + } + finally + { + client.shutdown(); + } + } + + /** 上传字节到指定 OSS key (把后端生成的参会人信息 Excel 直接放进 staging 前缀, 不经过本地文件) */ + public void putObject(String key, byte[] data, String contentType) + { + OSSClient client = newClient(); + try + { + ObjectMetadata meta = new ObjectMetadata(); + meta.setContentType(contentType); + client.putObject(ossConfMeta.getBucket(), key, new ByteArrayInputStream(data), meta); + } + finally + { + client.shutdown(); + } + } + + /** 删除某前缀下所有对象 (staging 清场, 避免上次下载的残留文件混进本次 zip) */ + public void clearPrefix(String prefix) + { + OSSClient client = newClient(); + try + { + ObjectListing listing = client.listObjects(ossConfMeta.getBucket(), prefix); + List keys = new ArrayList<>(); + for (OSSObjectSummary s : listing.getObjectSummaries()) + { + keys.add(s.getKey()); + } + if (!keys.isEmpty()) + { + DeleteObjectsResult r = client.deleteObjects( + new DeleteObjectsRequest(ossConfMeta.getBucket()).withKeys(keys)); + log.info("[OSS-ZIP] 清场 {} 个对象, 前缀 {}", r.getDeletedObjects().size(), prefix); + } + } + finally + { + client.shutdown(); + } + } + + /** + * 调阿里云 FC 在 OSS 端打 zip, 返回 zip 的签名下载 URL. + *

    + * FC 打完后返回 301/302, Location 头即 zip 签名 URL; unirest/hutool 默认不跟随重定向, + * 所以能直接读到 Location. http→https 强转防浏览器 Mixed Content (OSS 双协议都支持). + */ + public String zipDownload(String sourceDir) + { + String funcUrl = ossConfMeta.getZipFuncUrl(); + if (funcUrl == null || funcUrl.isEmpty()) + { + throw new ServiceException("OSS 打 zip 端点未配置 (ruoyi.oss.zip-func-url)"); + } + + Map body = new HashMap<>(); + body.put("bucket", ossConfMeta.getBucket()); + body.put("source-dir", sourceDir); + log.info("[OSS-ZIP] 请求 FC 打包 bucket={} source-dir={}", ossConfMeta.getBucket(), sourceDir); + + HttpResponse resp; + try + { + resp = HttpRequest.post(funcUrl) + .body(JSONUtil.toJsonStr(body), "application/json") + .setFollowRedirects(false) + .timeout(60000) + .execute(); + } + catch (Exception e) + { + throw new ServiceException("OSS 打 zip 请求失败: " + e.getMessage()); + } + + int status = resp.getStatus(); + String location = resp.header("Location"); + if (status != 301 && status != 302) + { + log.error("[OSS-ZIP] FC 响应异常 status={}", status); + throw new ServiceException("打包失败, FC 响应状态 " + status); + } + if (location == null || location.isEmpty()) + { + throw new ServiceException("打包失败, 未获取到下载链接"); + } + return location.replaceFirst("^http://", "https://"); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java index ee4ca93..c1c3a06 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/MeetingStageScheduler.java @@ -10,12 +10,12 @@ import lombok.extern.slf4j.Slf4j; * 会议事实/阶段 自动流转调度器 (每分钟一次). *

    * 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实 - * (is_executed / is_frozen / material_audit_stage / voucher_audit_stage / 审核时间 …), + * (is_executed / is_frozen / material_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)
    + *   3) material 通过 且 审核时间过 24h → 待结算 (current_stage 缓存翻 AWAITING_SETTLEMENT)
      * 
    * 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围. *

    diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/OpenStatusScheduler.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/OpenStatusScheduler.java new file mode 100644 index 0000000..d6fbdf6 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/OpenStatusScheduler.java @@ -0,0 +1,42 @@ +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.service.IBizProjectService; +import lombok.extern.slf4j.Slf4j; + +/** + * 项目"开通"到期回收调度器 (每天 00:05 一次). + *

    + * 开通状态流转: 默认 N → 经理"开通"置 Y + open_deadline → 到期(open_deadline <= 今天)回收为 N. + * 结题(is_finished=1)且 open_status=N 的项目, sponsor 不可见 + * (见 BizProjectMapper.selectSponsorList / BizMeetingMapper.selectList 的过滤). + *

    + * 需要启动类 {@code @EnableScheduling} 才会生效 (RuoYiApplication 已有). + */ +@Slf4j +@Component +public class OpenStatusScheduler +{ + @Autowired + private IBizProjectService bizProjectService; + + /** 每天 00:05:00 执行一次 */ + @Scheduled(cron = "0 5 0 * * ?") + public void closeExpiredOpenStatus() + { + try + { + int n = bizProjectService.closeExpiredOpenStatus(); + if (n > 0) + { + log.info("[OpenStatusScheduler] 开通到期回收 {} 个项目", n); + } + } + catch (Exception e) + { + log.warn("[OpenStatusScheduler] 回收异常 (跳过, 明天再试)", e); + } + } +} 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 d8366ef..f100b84 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 @@ -84,4 +84,55 @@ public interface IBizMeetingAttendeeService { * @return 成功邀请 (站内信+标记) 的人数 */ int invite(List attendeeIds); + + /** + * 生成"参会人信息" Excel 字节 (劳务下载打包用). + * 复用 BizMeetingAttendeeImportVo 的 @Excel 列头, 与 /export 端点同列. + * + * @param list 已查出的参会人 (is_deleted=0) + * @return xlsx 字节 + */ + byte[] buildAttendeeInfoExcel(List list); + + /** + * 生成"劳务协议"空目录模板 zip (参会人管理 "下载协议模板" 按钮). + *

    + * zip 内为 劳务协议/{序号}_{姓名}/ 空目录, 每个参会人一个; 序号 = 列表 1 起下标 + * (与前端表格 type="index" 一致, 依赖 selectByMeetingId 的 order by id 稳定排序). + * + * @param meetingId 会议 id + * @return zip 字节 (UTF-8 目录名) + */ + byte[] buildAgreementTemplateZip(Long meetingId); + + /** + * 上传"劳务协议" zip, 解压后按 劳务协议/{序号}_{姓名}/ 目录匹配参会人, + * 取目录内第一个文件上传 OSS 并回填 labor_protocol (覆盖式). + * + * @param file 用户重新压缩的 zip + * @param meetingId 会议 id (决定 OSS 子目录 + 参会人列表) + * @return 成功回填的参会人数 + * @throws Exception 文件为空 / 无参会人 / zip 解析异常 + */ + int uploadAgreements(MultipartFile file, Long meetingId) throws Exception; + + /** + * 生成"专家照片"空目录模板 zip (专家照片 "下载目录模板" 按钮). + * zip 内为 专家照片/{序号}_{姓名}/ 空目录, 结构与劳务协议模板一致. + * + * @param meetingId 会议 id + * @return zip 字节 (UTF-8 目录名) + */ + byte[] buildExpertPhotoTemplateZip(Long meetingId); + + /** + * 上传"专家照片" zip, 解压后按 专家照片/{序号}_{姓名}/ 目录匹配参会人, + * 目录内所有文件上传 OSS 后逗号拼接, 覆盖式回填 on_site_photos. + * + * @param file 用户重新压缩的 zip + * @param meetingId 会议 id + * @return 成功回填的参会人数 + * @throws Exception 文件为空 / 无参会人 / zip 解析异常 + */ + int uploadExpertPhotos(MultipartFile file, Long meetingId) throws Exception; } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingExecutorService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingExecutorService.java index e8876c4..ee6922b 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingExecutorService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingExecutorService.java @@ -12,11 +12,10 @@ public interface IBizMeetingExecutorService { List selectByMeetingId(Long meetingId); - List selectByUserId(Long userId); - /** * 替换该会议的执行人员 (全删全插, 一个事务) + * @param orgIds 执行方企业 org_id 列表 * @param assignedBy 分配人 user_id */ - int replaceByMeetingId(Long meetingId, List userIds, Long assignedBy); + int replaceByMeetingId(Long meetingId, List orgIds, Long assignedBy); } \ 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 8fb186e..14fb1f0 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 @@ -1,6 +1,7 @@ package com.ruoyi.business.service; import java.util.List; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.business.domain.BizMeetingMaterial; /** @@ -46,4 +47,59 @@ public interface IBizMeetingMaterialService { * 白名单 subType + 会议存在校验 (公开端点防滥用). */ void upsertFromCamera(Long meetingId, String subType, String ossUrl, String extraOssUrl); + + /** + * 结算时保存付款凭证 (LV_PAYMENT + SV_PAYMENT, 合规人员上传). + * 只清/插这两个 subType, 不碰其他材料; 付款凭证非发票, 不触发 OCR, 不影响会议费用. + * 与 replaceByMeetingId 不同, 这里不做金额回填 (凭证无金额). + */ + void saveVouchers(Long meetingId, List vouchers); + + /** + * 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打成一个 zip, 返回下载 URL. + *

    + * zip 内目录结构: {项目名称}/{材料类型中文名}/{材料文件}. + * 实现: 先 staging copy 到 download/huiwu/{meetingId}/ 前缀 (目录结构即 zip 结构), + * 再调阿里云 FC 在 OSS 端打 zip. + */ + String buildServiceZipUrl(Long meetingId); + + /** + * 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息 Excel 在 OSS 端打成一个 zip, 返回下载 URL. + * 前全景/后全景两张图归入"会议现场全景"目录. + */ + String buildLaborZipUrl(Long meetingId); + + /** + * 批量会务下载: 多个会议的会务材料合并打一个 zip, 返回下载 URL. + * meetingIds 去重去空, 上限 20 个; 单个会议无材料跳过, 全部无材料抛异常. + */ + String buildBatchServiceZipUrl(List meetingIds); + + /** + * 批量劳务下载: 多个会议的劳务材料(含参会人信息)合并打一个 zip, 返回下载 URL. + */ + String buildBatchLaborZipUrl(List meetingIds); + + /** + * 生成"会务材料"空目录模板 zip (会务材料 tab "打包上传" 的"下载目录"按钮). + *

    + * zip 内为 会务材料/{材料类型中文名}/ 空目录 (物料制作/酒店/大交通/小交通/执行费/设计费/其他/总结算单/总发票), + * 用户把材料文件放进对应目录后重新压缩上传. + * + * @param meetingId 会议 id + * @return zip 字节 (UTF-8 目录名) + */ + byte[] buildServiceTemplateZip(Long meetingId); + + /** + * 上传"会务材料" zip, 解压后按 会务材料/{材料类型中文名}/ 目录匹配 subType, + * 单文件直接上传 OSS, 多文件先打 zip 再上传 OSS, 按 (meetingId, SERVICE, subType) upsert 回填. + * + * @param file 用户重新压缩的 zip + * @param meetingId 会议 id (决定 OSS 子目录) + * @return 成功回填的材料类数 + * @throws Exception 文件为空 / zip 解析异常 / 未找到有效目录 + */ + int uploadServiceMaterials(MultipartFile file, Long meetingId) throws Exception; } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSupervisorService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSupervisorService.java index 317753e..a4da190 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSupervisorService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSupervisorService.java @@ -12,11 +12,10 @@ public interface IBizMeetingSupervisorService { List selectByMeetingId(Long meetingId); - List selectByUserId(Long userId); - /** * 替换该会议的监察员 (全删全插, 一个事务) + * @param orgIds 支持方企业 org_id 列表 * @param assignedBy 分配人 user_id (前端 / manager 自己) */ - int replaceByMeetingId(Long meetingId, List userIds, Long assignedBy); + int replaceByMeetingId(Long meetingId, List orgIds, Long assignedBy); } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java index e365f90..e4f0405 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java @@ -2,7 +2,9 @@ package com.ruoyi.business.service; import java.util.List; import java.util.Map; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.domain.dto.ImportResult; public interface IBizOrgService { BizOrg getById(Long orgId); @@ -10,14 +12,19 @@ public interface IBizOrgService { int insert(BizOrg entity); int updateByPrimaryKey(BizOrg entity); int deleteByPrimaryKeys(Long[] orgIds); - /** 支持方下拉选项 (userId/orgName/userName), 用于 manager 项目分配弹窗 */ + /** 支持方下拉选项 (orgId/orgName/userName), 用于 manager 项目分配弹窗 */ List> selectSponsorOrgOptions(BizOrg entity); - /** 执行方下拉选项 (userId/orgName/userName), 用于 manager 项目分配弹窗 + /** 执行方下拉选项 (orgId/orgName/userName), 用于 manager 项目分配弹窗 * 只返回 biz_org.org_type='executor' 对应的 MAIN 账号 (parent_user_id IS NULL) */ List> selectExecutorOrgOptions(BizOrg entity); + /** 支持方注册下拉选项 (匿名公开, 含无主账号 org): 返回 orgId / orgName / mainUserId */ + List> selectSponsorRegisterOptions(BizOrg entity); /** 当前登录 sponsor 的所属公司 (主账号可改, 子账号只读) */ Map selectMySponsorCompany(Long userId); + /** user_id → org_id 单一可信源反查 (MAIN 走 biz_org, SUB 走 biz_person), 找不到返回 null */ + Long selectOrgIdByUserId(Long userId); + /** * 启用/禁用 org, 同步主账号 sys_user.status * biz_org.status: '禁用' → sys_user.status='1', '正常'/'合作中' → '0' @@ -25,4 +32,11 @@ public interface IBizOrgService { * (子账号登录时由 BizAuthController 校验主账号状态拦截) */ int toggleStatus(Long orgId, String newBizOrgStatus); + + /** + * 批量导入支持单位 (只导 org, 不新增人员) + * orgType 固定 'sponsor', status 固定 '0', 不建 sys_user (userId 留空) + * 返回 {@link ImportResult} 含成功/失败计数 + 失败明细 (行号 + 原因) + */ + ImportResult importOrg(MultipartFile file, String operName) throws Exception; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java index 31e5758..0ca2700 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java @@ -23,4 +23,9 @@ public interface IBizPersonService int updateByPrimaryKey(BizPerson entity); int deleteByPrimaryKey(String personId); int deleteByPrimaryKeys(String[] personId); + /** + * 更换机构管理员: 把 personId 对应人员晋升为该机构 MAIN (管理员), + * 原管理员由 MAIN 降为 SUB, 其余子账号 re-point 到新管理员, biz_org.user_id 同步指向新管理员 + */ + int changeOrgAdmin(String personId); } 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 a467e8f..c1773fa 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 @@ -45,4 +45,10 @@ public interface IBizProjectService * 并重算 available_amount. 幂等 (每次全量重算), 无累计副作用. */ void recomputeSettledAmounts(Long projectId); + + /** 删除公告: 将 invitation_url / support_letter_url / publish_url 置 NULL (未发布) */ + int clearAnnouncement(Long projectId); + + /** 开通到期回收: 到期(open_deadline <= 今天)的 open_status='Y' 置回 'N' */ + int closeExpiredOpenStatus(); } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java index 103a518..769de54 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java @@ -1,6 +1,5 @@ package com.ruoyi.business.service; -import java.util.Date; import org.springframework.stereotype.Component; import com.ruoyi.business.domain.BizMeeting; @@ -8,7 +7,7 @@ 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), 各角色看到的 + * + material_audit_stage + 审核时间 + compliance_approved), 各角色看到的 * 「阶段名称」由本类实时计算. *

      *
    • {@link #derivePhysicalStage(BizMeeting)}: 10 值物理阶段 (current_stage 缓存 + 列表筛选).
    • @@ -22,20 +21,12 @@ public class StageDeriver { private static final long H24 = 24L * 3600 * 1000; - /** 待结算: 材料+凭证都审核通过 且 最晚通过时间已超 24h (用户拍板口径) */ + /** 待结算: 材料审核通过 且 材料审核时间已超 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; + if (!"APPROVED".equals(m.getMaterialAuditStage())) return false; + if (m.getMaterialAuditTime() == null) return false; + return System.currentTimeMillis() - m.getMaterialAuditTime().getTime() >= H24; } private static boolean t(Integer v) 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 3c3d506..82a0d7d 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,13 +1,23 @@ package com.ruoyi.business.service.impl; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; 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 java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +34,7 @@ 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.oss.OssUploader; import com.ruoyi.business.service.IBizMeetingAttendeeService; import com.ruoyi.business.service.IBizMeetingService; import com.ruoyi.business.sms.AliyunSmsSender; @@ -60,6 +71,8 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService private BizNotifyService bizNotifyService; @Autowired private IBizMeetingService bizMeetingService; + @Autowired + private OssUploader ossUploader; @Override public int insert(BizMeetingAttendee entity) { @@ -507,4 +520,217 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService return sent; } + @Override + public byte[] buildAttendeeInfoExcel(List list) { + List voList = new ArrayList<>(); + if (list != null) { + for (BizMeetingAttendee a : list) { + voList.add(BizMeetingAttendeeImportVo.from(a)); + } + } + SXSSFWorkbook wb = new SXSSFWorkbook(500); + try { + ExcelUtil util = new ExcelUtil<>(BizMeetingAttendeeImportVo.class); + util.initWithWorkbook(wb, voList, "参会人", null); + util.writeSheet(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + wb.write(out); + return out.toByteArray(); + } catch (Exception e) { + throw new ServiceException("生成参会人信息失败: " + e.getMessage()); + } finally { + wb.dispose(); + } + } + + /** 劳务协议 zip 顶层目录名 */ + private static final String AGREEMENT_ZIP_ROOT = "劳务协议"; + /** 专家照片 zip 顶层目录名 */ + private static final String EXPERT_PHOTO_ZIP_ROOT = "专家照片"; + + /** 目录名里的姓名段 (下划线后) 要剔除路径/zip 非法字符, 避免 Windows 解压异常 */ + private static String safeAttendeeName(String name) { + if (name == null || name.trim().isEmpty()) return "未知"; + return name.replaceAll("[\\\\/:*?\"<>|\\r\\n\\t]", "").trim(); + } + + @Override + public byte[] buildAgreementTemplateZip(Long meetingId) { + return buildTemplateZip(meetingId, AGREEMENT_ZIP_ROOT); + } + + @Override + public byte[] buildExpertPhotoTemplateZip(Long meetingId) { + return buildTemplateZip(meetingId, EXPERT_PHOTO_ZIP_ROOT); + } + + /** 生成空目录模板 zip: {rootDir}/{序号}_{姓名}/, 每个参会人一个空目录 */ + private byte[] buildTemplateZip(Long meetingId, String rootDir) { + List list = selectByMeetingId(meetingId); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) { + for (int i = 0; i < list.size(); i++) { + BizMeetingAttendee a = list.get(i); + // 空目录 entry (以 / 结尾), 序号 = 列表 1 起下标, 与前端表格 type="index" 一致 + String dir = rootDir + "/" + (i + 1) + "_" + safeAttendeeName(a.getName()) + "/"; + zos.putNextEntry(new ZipEntry(dir)); + zos.closeEntry(); + } + } catch (IOException e) { + throw new ServiceException("生成目录模板失败: " + e.getMessage()); + } + return baos.toByteArray(); + } + + @Override + public int uploadAgreements(MultipartFile file, Long meetingId) throws Exception { + if (meetingId == null) throw new ServiceException("meetingId 不能为空"); + if (file == null || file.isEmpty()) throw new ServiceException("请选择要上传的 zip 文件"); + + List list = selectByMeetingId(meetingId); + if (list.isEmpty()) throw new ServiceException("该会议暂无参会人, 无法回填劳务协议"); + + byte[] zipBytes = file.getBytes(); + + // 目录序号 (下划线前数字, 0-based) → 该目录下文件字节 + 文件名 + Map> folderBytes = new HashMap<>(); + Map> folderNames = new HashMap<>(); + try { + readAttendeeZip(zipBytes, StandardCharsets.UTF_8, folderBytes, folderNames); + } catch (IllegalArgumentException e) { + // 中文 Windows 打包器 (WinRAR/资源管理器"发送到压缩文件夹") 用 GBK 编码条目名且不置 UTF-8 标志, + // 强制 UTF-8 解码会抛 malformed input → 回退 GBK 重新解析 + log.warn("[attendee] 协议 zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage()); + folderBytes.clear(); + folderNames.clear(); + readAttendeeZip(zipBytes, Charset.forName("GBK"), folderBytes, folderNames); + } + + int updated = 0; + for (Map.Entry> e : folderBytes.entrySet()) { + int idx = e.getKey(); + if (idx < 0 || idx >= list.size()) { + log.warn("[attendee] 协议 zip 目录序号 {} 越界 (参会人共 {}), 跳过", idx + 1, list.size()); + continue; + } + BizMeetingAttendee a = list.get(idx); + List names = folderNames.get(idx); + String fileName = (names != null && !names.isEmpty()) ? names.get(0) : "labor-protocol.pdf"; + // labor_protocol 是单 URL, 每目录只取第一个文件作为协议 + String url = ossUploader.upload(e.getValue().get(0), fileName, "ry8080/meeting/" + meetingId + "/labor-protocol"); + + BizMeetingAttendee entity = new BizMeetingAttendee(); + entity.setId(a.getId()); + entity.setLaborProtocol(url); + entity.setUpdateBy(SecurityUtils.getUsername()); + updateLaborProtocol(entity); + updated++; + } + log.info("[attendee] 劳务协议回填完成 meetingId={} 共{}个", meetingId, updated); + return updated; + } + + @Override + public int uploadExpertPhotos(MultipartFile file, Long meetingId) throws Exception { + if (meetingId == null) throw new ServiceException("meetingId 不能为空"); + if (file == null || file.isEmpty()) throw new ServiceException("请选择要上传的 zip 文件"); + + List list = selectByMeetingId(meetingId); + if (list.isEmpty()) throw new ServiceException("该会议暂无参会人, 无法回填专家照片"); + + byte[] zipBytes = file.getBytes(); + Map> folderBytes = new HashMap<>(); + Map> folderNames = new HashMap<>(); + try { + readAttendeeZip(zipBytes, StandardCharsets.UTF_8, folderBytes, folderNames); + } catch (IllegalArgumentException e) { + log.warn("[attendee] 专家照片 zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage()); + folderBytes.clear(); + folderNames.clear(); + readAttendeeZip(zipBytes, Charset.forName("GBK"), folderBytes, folderNames); + } + + int updated = 0; + for (Map.Entry> e : folderBytes.entrySet()) { + int idx = e.getKey(); + if (idx < 0 || idx >= list.size()) { + log.warn("[attendee] 专家照片 zip 目录序号 {} 越界 (参会人共 {}), 跳过", idx + 1, list.size()); + continue; + } + BizMeetingAttendee a = list.get(idx); + List files = e.getValue(); + List names = folderNames.get(idx); + // 专家照片可多张: 目录内所有文件都上传, 逗号拼接覆盖式回填 on_site_photos + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < files.size(); i++) { + String fileName = (names != null && i < names.size()) ? names.get(i) : ("photo-" + (i + 1) + ".jpg"); + String url = ossUploader.upload(files.get(i), fileName, "ry8080/meeting/" + meetingId + "/expert-photo"); + if (sb.length() > 0) sb.append(","); + sb.append(url); + } + + BizMeetingAttendee entity = new BizMeetingAttendee(); + entity.setId(a.getId()); + entity.setOnSitePhotos(sb.toString()); + entity.setUpdateBy(SecurityUtils.getUsername()); + updateProfile(entity); + updated++; + } + log.info("[attendee] 专家照片回填完成 meetingId={} 共{}个", meetingId, updated); + return updated; + } + + /** 按指定字符集解压 zip, 把 {根目录}/{序号}_姓名/ 目录下的文件归集到对应参会人序号 */ + private void readAttendeeZip(byte[] zipBytes, Charset charset, + Map> folderBytes, + Map> folderNames) throws IOException { + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes), charset)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + String path = entry.getName(); + // 跳过 macOS 打包产生的元数据 + if (path.contains("__MACOSX") || path.endsWith(".DS_Store")) continue; + Integer idx = parseAgreementIndex(path); + if (idx == null) continue; // 不落在 {序号}_姓名 目录下, 忽略 + byte[] data = readAllBytes(zis); + folderBytes.computeIfAbsent(idx, k -> new ArrayList<>()).add(data); + folderNames.computeIfAbsent(idx, k -> new ArrayList<>()).add(lastSegment(path)); + } + } + } + + /** 从 zip 条目路径里解析出 {序号}_姓名 目录的 0-based 序号; 不匹配返回 null */ + private static Integer parseAgreementIndex(String path) { + if (path == null || path.isEmpty()) return null; + for (String seg : path.split("/")) { + if (seg.isEmpty()) continue; + int underscore = seg.indexOf('_'); + if (underscore <= 0) continue; + String numPart = seg.substring(0, underscore); + if (!numPart.matches("\\d+")) continue; + try { + return Integer.parseInt(numPart) - 1; + } catch (NumberFormatException ex) { + return null; + } + } + return null; + } + + private static String lastSegment(String path) { + int i = path.lastIndexOf('/'); + return i >= 0 ? path.substring(i + 1) : path; + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingExecutorServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingExecutorServiceImpl.java index 1b958f3..222438c 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingExecutorServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingExecutorServiceImpl.java @@ -25,23 +25,18 @@ public class BizMeetingExecutorServiceImpl implements IBizMeetingExecutorService return bizMeetingExecutorMapper.selectByMeetingId(meetingId); } - @Override - public List selectByUserId(Long userId) { - return bizMeetingExecutorMapper.selectByUserId(userId); - } - @Override @Transactional(rollbackFor = Exception.class) - public int replaceByMeetingId(Long meetingId, List userIds, Long assignedBy) { + public int replaceByMeetingId(Long meetingId, List orgIds, Long assignedBy) { bizMeetingExecutorMapper.deleteByMeetingId(meetingId); - if (userIds == null || userIds.isEmpty()) { + if (orgIds == null || orgIds.isEmpty()) { return 0; } - List list = new ArrayList<>(userIds.size()); - for (Long uid : userIds) { + List list = new ArrayList<>(orgIds.size()); + for (Long orgId : orgIds) { BizMeetingExecutor m = new BizMeetingExecutor(); m.setMeetingId(meetingId); - m.setUserId(uid); + m.setExecutorOrgId(orgId); m.setAssignedBy(assignedBy); list.add(m); } 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 0b8fc24..95debd3 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,6 +1,7 @@ package com.ruoyi.business.service.impl; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; @@ -10,15 +11,34 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.business.domain.BizMeeting; +import com.ruoyi.business.domain.BizMeetingAttendee; import com.ruoyi.business.domain.BizMeetingMaterial; import com.ruoyi.business.mapper.BizMeetingMapper; import com.ruoyi.business.mapper.BizMeetingMaterialMapper; +import com.ruoyi.business.oss.OssUploader; +import com.ruoyi.business.oss.OssZipService; +import com.ruoyi.business.service.IBizMeetingAttendeeService; import com.ruoyi.business.service.IBizMeetingMaterialService; @Service @@ -35,10 +55,73 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService /** 可识别文件扩展名 (与前端 isRecognizable 对齐): 图片/PDF 才触发 OCR */ private static final Pattern RECOGNIZABLE = Pattern.compile("\\.(jpe?g|png|pdf)$", Pattern.CASE_INSENSITIVE); + /** 会务下载范围: 会务材料 (SERVICE) + 会务凭证 (SERVICE_VOUCHER) */ + private static final Set SERVICE_MATERIAL_TYPES = new HashSet<>(Arrays.asList("SERVICE", "SERVICE_VOUCHER")); + + /** 会务 subType → 中文材料类型名 (与前端 MeetingDetail.ROW_CONFIG 对齐), 作为 zip 内 "材料类型" 目录名 */ + private static final Map SERVICE_SUBTYPE_LABEL = new HashMap<>(); + static { + SERVICE_SUBTYPE_LABEL.put("M_MATERIAL", "物料制作"); + SERVICE_SUBTYPE_LABEL.put("M_HOTEL", "酒店"); + SERVICE_SUBTYPE_LABEL.put("M_TRAFFIC_BIG", "大交通"); + SERVICE_SUBTYPE_LABEL.put("M_TRAFFIC_SMALL", "小交通"); + SERVICE_SUBTYPE_LABEL.put("M_EXECUTION", "执行费"); + SERVICE_SUBTYPE_LABEL.put("M_DESIGN", "设计费"); + SERVICE_SUBTYPE_LABEL.put("M_OTHER", "其他"); + SERVICE_SUBTYPE_LABEL.put("M_SETTLEMENT", "总结算单"); + SERVICE_SUBTYPE_LABEL.put("M_INVOICE", "总发票"); + SERVICE_SUBTYPE_LABEL.put("SV_PAYMENT", "会务付款凭证"); + } + + /** 会务材料 (SERVICE) 子类顺序 (与前端 ROW_CONFIG SERVICE 行对齐), 用于"打包上传"下载空目录模板 + 回填匹配 */ + private static final List SERVICE_MATERIAL_SUBTYPES = Arrays.asList( + "M_MATERIAL", "M_HOTEL", "M_TRAFFIC_BIG", "M_TRAFFIC_SMALL", + "M_EXECUTION", "M_DESIGN", "M_OTHER", "M_SETTLEMENT", "M_INVOICE"); + + /** 会务材料中文名 → subType (仅 M_* 会务材料, 不含 SV_PAYMENT 付款凭证), 用于上传 zip 按目录名匹配 */ + private static final Map SERVICE_LABEL_TO_SUBTYPE = new HashMap<>(); + static { + for (String sub : SERVICE_MATERIAL_SUBTYPES) { + SERVICE_LABEL_TO_SUBTYPE.put(SERVICE_SUBTYPE_LABEL.get(sub), sub); + } + } + + /** 会务材料"打包上传" zip 顶层目录名 */ + private static final String SERVICE_ZIP_ROOT = "会务材料"; + + /** 劳务下载范围: 劳务材料 (LABOR) + 劳务凭证 (LABOR_VOUCHER) */ + private static final Set LABOR_MATERIAL_TYPES = new HashSet<>(Arrays.asList("LABOR", "LABOR_VOUCHER")); + + /** 劳务 subType → 中文材料类型名 (与前端 MeetingDetail.ROW_CONFIG 对齐) */ + private static final Map LABOR_SUBTYPE_LABEL = new HashMap<>(); + static { + LABOR_SUBTYPE_LABEL.put("L_DETAIL", "劳务明细表"); + LABOR_SUBTYPE_LABEL.put("L_AGREEMENT", "劳务协议"); + LABOR_SUBTYPE_LABEL.put("L_ENTERPRISE_BENEFIT", "企业权益"); + LABOR_SUBTYPE_LABEL.put("L_SIGN_IN", "签到表"); + LABOR_SUBTYPE_LABEL.put("L_PANORAMA_FRONT", "前全景"); + LABOR_SUBTYPE_LABEL.put("L_PANORAMA_BACK", "后全景"); + LABOR_SUBTYPE_LABEL.put("L_EXPERT_PHOTO", "专家照片"); + LABOR_SUBTYPE_LABEL.put("LV_PAYMENT", "劳务付款凭证"); + } + + private static final String XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + @Autowired private BizMeetingMaterialMapper bizMeetingMaterialMapper; @Autowired private BizMeetingMapper bizMeetingMapper; + @Autowired + private OssZipService ossZipService; + @Autowired + private IBizMeetingAttendeeService attendeeService; + @Autowired + private OssUploader ossUploader; + @Autowired + @Lazy + private InvoiceOcrService invoiceOcrService; + + private static final Logger log = LoggerFactory.getLogger(BizMeetingMaterialServiceImpl.class); @Override public BizMeetingMaterial getById(Long id) { @@ -103,6 +186,19 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService } catch (DuplicateKeyException e) { throw new ServiceException("材料上传重复, 请检查 (同一会议下同一资料类型同一子分类只能有一条记录)"); } + // foreach 批量插入的 useGeneratedKeys 不可靠, 插完后再查回真实 id (subType 会议内唯一), 前端据此触发 OCR + List inserted = bizMeetingMaterialMapper.selectByMeetingId(meetingId); + Map idBySubType = new HashMap<>(); + if (inserted != null) { + for (BizMeetingMaterial im : inserted) { + if (im.getSubType() != null) idBySubType.put(im.getSubType(), im.getId()); + } + } + for (BizMeetingMaterial m : list) { + if (m.getSubType() != null) { + m.setId(idBySubType.get(m.getSubType())); + } + } return list; } @@ -162,6 +258,444 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService } } + /** + * 结算时保存付款凭证 (LV_PAYMENT + SV_PAYMENT, 合规人员上传). + * 只清/插这两个 subType, 不碰其他材料; 付款凭证非发票, 不触发 OCR, 不影响会议费用. + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void saveVouchers(Long meetingId, List vouchers) { + // 删旧 LV_PAYMENT / SV_PAYMENT (幂等, 避免 uk_meeting_type_sub 冲突) + List existing = bizMeetingMaterialMapper.selectByMeetingId(meetingId); + if (existing != null) { + for (BizMeetingMaterial m : existing) { + if ("LV_PAYMENT".equals(m.getSubType()) || "SV_PAYMENT".equals(m.getSubType())) { + bizMeetingMaterialMapper.deleteByPrimaryKey(m.getId()); + } + } + } + if (vouchers == null || vouchers.isEmpty()) return; + List toInsert = new java.util.ArrayList<>(); + for (BizMeetingMaterial v : vouchers) { + if (v == null || v.getSubType() == null || v.getOssUrl() == null || v.getOssUrl().isEmpty()) continue; + if (!"LV_PAYMENT".equals(v.getSubType()) && !"SV_PAYMENT".equals(v.getSubType())) continue; + v.setId(null); + v.setMeetingId(meetingId); + v.setMaterialType("LV_PAYMENT".equals(v.getSubType()) ? "LABOR_VOUCHER" : "SERVICE_VOUCHER"); + v.setAmount(null); + v.setFeeStatus(1); // 付款凭证非发票, 直接标记已计算, 不参与费用汇总 + v.setCreateTime(new Date()); + v.setCreatorId(SecurityUtils.getUserId()); + toInsert.add(v); + } + if (!toInsert.isEmpty()) { + bizMeetingMaterialMapper.insertBatch(toInsert); + } + } + + /** + * 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打成一个 zip, 返回下载 URL. + *

      + * zip 内目录结构: {项目名称}/{材料类型中文名}/{材料文件}. 实现分两步: + *

        + *
      1. staging copy: 把散落的材料文件复制到 download/huiwu/{meetingId}/ 前缀下, + * key 结构即 zip 内目录结构 (FC 按 source-dir 前缀打包, zip 内相对路径 = key 相对前缀部分)
      2. + *
      3. FC 打包: POST {bucket, source-dir} 给阿里云函数计算, 拿 zip 签名 URL
      4. + *
      + * 文件名: 优先用 fileName (历史数据可能有), 否则用 {材料类型中文名}{扩展名}. + */ + @Override + public String buildServiceZipUrl(Long meetingId) + { + BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId); + if (meeting == null) throw new ServiceException("会议不存在"); + String prefix = "download/huiwu/" + meetingId + "/"; + ossZipService.clearPrefix(prefix); + int copied = stageServiceMaterials(meetingId, prefix, projectFolderName(meeting)); + if (copied == 0) throw new ServiceException("该会议暂无可下载的会务材料"); + return ossZipService.zipDownload(prefix); + } + + /** + * 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息 Excel 在 OSS 端打成一个 zip, 返回下载 URL. + *

      + * zip 内目录结构: {项目名称}/{材料类型中文名}/{材料文件}, 外加 {项目名称}/参会人信息/参会人信息.xlsx. + * 前全景(L_PANORAMA_FRONT)/后全景(L_PANORAMA_BACK) 归入"会议现场全景"目录 (文件名分别用 前全景/后全景 区分, 避免同目录重名). + */ + @Override + public String buildLaborZipUrl(Long meetingId) + { + BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId); + if (meeting == null) throw new ServiceException("会议不存在"); + String prefix = "download/labor/" + meetingId + "/"; + ossZipService.clearPrefix(prefix); + int copied = stageLaborMaterials(meetingId, prefix, projectFolderName(meeting)); + if (copied == 0) throw new ServiceException("该会议暂无可下载的劳务材料"); + return ossZipService.zipDownload(prefix); + } + + // =================================================================== + // 批量下载 (多会议合并打一个 zip) + // =================================================================== + + /** 批量会务下载: 多个会议的会务材料合并 staging 到一个 batch 前缀, 一次 FC 打 zip 返回 URL */ + @Override + public String buildBatchServiceZipUrl(List meetingIds) + { + List ids = normalizeIds(meetingIds); + String prefix = "download/huiwu/batch/" + System.currentTimeMillis() + "/"; + ossZipService.clearPrefix(prefix); + Set usedFolders = new HashSet<>(); + int copied = 0; + for (Long id : ids) + { + BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(id); + if (meeting == null) continue; // 已软删/不存在, 跳过 + copied += stageServiceMaterials(id, prefix, uniqueFolder(usedFolders, batchMeetingFolderName(meeting))); + } + if (copied == 0) throw new ServiceException("所选会议暂无可下载的会务材料"); + return ossZipService.zipDownload(prefix); + } + + /** 批量劳务下载: 多个会议的劳务材料(含参会人信息)合并 staging 到一个 batch 前缀, 一次 FC 打 zip 返回 URL */ + @Override + public String buildBatchLaborZipUrl(List meetingIds) + { + List ids = normalizeIds(meetingIds); + String prefix = "download/labor/batch/" + System.currentTimeMillis() + "/"; + ossZipService.clearPrefix(prefix); + Set usedFolders = new HashSet<>(); + int copied = 0; + for (Long id : ids) + { + BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(id); + if (meeting == null) continue; + copied += stageLaborMaterials(id, prefix, uniqueFolder(usedFolders, batchMeetingFolderName(meeting))); + } + if (copied == 0) throw new ServiceException("所选会议暂无可下载的劳务材料"); + return ossZipService.zipDownload(prefix); + } + + /** 收集该会议会务材料并 copy 到 staging 前缀 prefix + folderName 下, 返回 copy 的文件数 (0 = 无会务材料) */ + private int stageServiceMaterials(Long meetingId, String prefix, String folderName) + { + int copied = 0; + List all = bizMeetingMaterialMapper.selectByMeetingId(meetingId); + if (all == null) return copied; + for (BizMeetingMaterial m : all) + { + if (m.getOssUrl() == null || m.getOssUrl().isEmpty()) continue; + if (m.getMaterialType() == null || !SERVICE_MATERIAL_TYPES.contains(m.getMaterialType())) continue; + String srcKey = ossZipService.extractKey(m.getOssUrl()); + if (srcKey == null || srcKey.isEmpty()) continue; + String label = SERVICE_SUBTYPE_LABEL.getOrDefault(m.getSubType(), (m.getSubType() == null ? "其他" : m.getSubType())); + String ext = extOf(srcKey); + String fileName = (m.getFileName() != null && !m.getFileName().trim().isEmpty()) + ? safeName(m.getFileName()) : safeName(label) + ext; + String dstKey = prefix + folderName + "/" + safeName(label) + "/" + fileName; + ossZipService.copyObject(srcKey, dstKey); + copied++; + } + return copied; + } + + /** 收集该会议劳务材料(含参会人信息 Excel)并 staging 到 prefix + folderName 下, 返回写入的文件数 (0 = 无劳务材料且无参会人) */ + private int stageLaborMaterials(Long meetingId, String prefix, String folderName) + { + int copied = 0; + List all = bizMeetingMaterialMapper.selectByMeetingId(meetingId); + if (all != null) + { + for (BizMeetingMaterial m : all) + { + if (m.getOssUrl() == null || m.getOssUrl().isEmpty()) continue; + if (m.getMaterialType() == null || !LABOR_MATERIAL_TYPES.contains(m.getMaterialType())) continue; + String srcKey = ossZipService.extractKey(m.getOssUrl()); + if (srcKey == null || srcKey.isEmpty()) continue; + String folder = laborFolderName(m.getSubType()); + String fileBase = laborFileLabel(m.getSubType()); + String ext = extOf(srcKey); + String fileName = (m.getFileName() != null && !m.getFileName().trim().isEmpty()) + ? safeName(m.getFileName()) : safeName(fileBase) + ext; + String dstKey = prefix + folderName + "/" + safeName(folder) + "/" + fileName; + ossZipService.copyObject(srcKey, dstKey); + copied++; + } + } + List attendees = attendeeService.selectByMeetingId(meetingId); + if (attendees != null && !attendees.isEmpty()) + { + byte[] excel = attendeeService.buildAttendeeInfoExcel(attendees); + ossZipService.putObject(prefix + folderName + "/参会人信息/参会人信息.xlsx", excel, XLSX_CONTENT_TYPE); + copied++; + } + return copied; + } + + /** 单会议 zip 顶层目录名: 项目名 (为空回退项目编号, 再回退会议ID), 统一消毒 */ + private static String projectFolderName(BizMeeting meeting) + { + String name = meeting.getProjectName(); + if (name == null || name.trim().isEmpty()) + { + name = (meeting.getProjectNo() != null && !meeting.getProjectNo().trim().isEmpty()) + ? meeting.getProjectNo() : "会议" + meeting.getMeetingId(); + } + return safeName(name); + } + + /** 批量 zip 每个会议顶层目录名: 项目编号_会议名_第N期 (缺项跳过, 全空回退会议ID) */ + private static String batchMeetingFolderName(BizMeeting meeting) + { + StringBuilder sb = new StringBuilder(); + appendPart(sb, meeting.getProjectNo()); + appendPart(sb, meeting.getMeetingName()); + if (meeting.getPeriodNo() != null) + { + appendPart(sb, "第" + meeting.getPeriodNo() + "期"); + } + String name = sb.toString(); + if (name.isEmpty()) return "会议" + meeting.getMeetingId(); + return safeName(name); + } + + private static void appendPart(StringBuilder sb, String v) + { + if (v == null || v.trim().isEmpty()) return; + if (sb.length() > 0) sb.append("_"); + sb.append(v.trim()); + } + + /** 目录名去重: 同名会议加 _2/_3 后缀, 避免 OSS 同名 key 静默覆盖丢数据 */ + private static String uniqueFolder(Set used, String base) + { + String name = base; + int i = 2; + while (!used.add(name)) + { + name = base + "_" + (i++); + } + return name; + } + + /** 批量 id 清洗: 去空 + 去重 + 保序 + 上限校验 */ + private static List normalizeIds(List meetingIds) + { + if (meetingIds == null || meetingIds.isEmpty()) throw new ServiceException("请先选择要下载的会议"); + Set seen = new HashSet<>(); + List ids = new ArrayList<>(); + for (Long id : meetingIds) + { + if (id == null || !seen.add(id)) continue; + ids.add(id); + } + if (ids.size() > 20) throw new ServiceException("一次最多批量下载 20 个会议"); + return ids; + } + + /** 劳务材料目录名: 前/后全景归入"会议现场全景", 其余用 subType 中文名 */ + private static String laborFolderName(String subType) + { + if ("L_PANORAMA_FRONT".equals(subType) || "L_PANORAMA_BACK".equals(subType)) return "会议现场全景"; + return LABOR_SUBTYPE_LABEL.getOrDefault(subType, (subType == null ? "其他" : subType)); + } + + /** 劳务材料文件名 (fileName 为空时用它+扩展名): 前/后全景分别用 前全景/后全景 区分 */ + private static String laborFileLabel(String subType) + { + return LABOR_SUBTYPE_LABEL.getOrDefault(subType, (subType == null ? "其他" : subType)); + } + + /** 取 object key 的扩展名 (含点), 无则空串 */ + private static String extOf(String key) + { + int i = key.lastIndexOf('.'); + return i >= 0 ? key.substring(i) : ""; + } + + /** 目录/文件名消毒: 去掉 OSS key 与 zip 路径里的非法字符 (斜杠/反斜杠/冒号等) */ + private static String safeName(String s) + { + if (s == null) return ""; + String r = s.replaceAll("[\\\\/:*?\"<>|\\r\\n\\t]", "_").trim(); + return r.isEmpty() ? "_" : r; + } + + /** + * 生成"会务材料"空目录模板 zip (会务材料 tab "打包上传" 的"下载目录"按钮). + * zip 内为 会务材料/{材料类型中文名}/ 空目录 (物料制作/酒店/大交通/小交通/执行费/设计费/其他/总结算单/总发票), + * 用户把材料文件放进对应目录后重新压缩上传. + */ + @Override + public byte[] buildServiceTemplateZip(Long meetingId) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) { + for (String sub : SERVICE_MATERIAL_SUBTYPES) { + String label = SERVICE_SUBTYPE_LABEL.getOrDefault(sub, sub); + String dir = SERVICE_ZIP_ROOT + "/" + label + "/"; + zos.putNextEntry(new ZipEntry(dir)); + zos.closeEntry(); + } + } catch (IOException e) { + throw new ServiceException("生成会务材料目录模板失败: " + e.getMessage()); + } + return baos.toByteArray(); + } + + /** + * 上传"会务材料" zip, 解压后按 会务材料/{材料类型中文名}/ 目录匹配 subType, + * 单文件直接上传 OSS, 多文件先打 zip 再上传 OSS, 按 (meetingId, SERVICE, subType) upsert 回填. + * 替换文件会清空 amount; 单文件可识别 (jpg/png/pdf) 或多文件打 zip → 直接后台触发 OCR 回写金额. + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int uploadServiceMaterials(MultipartFile file, Long meetingId) throws Exception { + if (meetingId == null) throw new ServiceException("meetingId 不能为空"); + if (file == null || file.isEmpty()) throw new ServiceException("请选择要上传的 zip 文件"); + + byte[] zipBytes = file.getBytes(); + + // subType -> 该目录下文件字节 + 文件名 + Map> folderBytes = new HashMap<>(); + Map> folderNames = new HashMap<>(); + try { + readServiceZip(zipBytes, StandardCharsets.UTF_8, folderBytes, folderNames); + } catch (IllegalArgumentException e) { + // 中文 Windows 打包器 (WinRAR/资源管理器) 用 GBK 编码条目名且不置 UTF-8 标志, 回退 GBK 重解析 + log.warn("[material] 会务材料 zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage()); + folderBytes.clear(); + folderNames.clear(); + readServiceZip(zipBytes, Charset.forName("GBK"), folderBytes, folderNames); + } + if (folderBytes.isEmpty()) throw new ServiceException("未在 zip 中找到会务材料目录, 请按下载模板的目录结构放置文件"); + + // 已存在的会务材料 subType -> 行 (决定 update / insert) + Map existingBySubType = new HashMap<>(); + List existing = bizMeetingMaterialMapper.selectByMeetingId(meetingId); + if (existing != null) { + for (BizMeetingMaterial m : existing) { + if (m.getSubType() != null) existingBySubType.put(m.getSubType(), m); + } + } + + String subDir = "ry8080/meeting/" + meetingId + "/service"; + int updated = 0; + for (Map.Entry> e : folderBytes.entrySet()) { + String subType = e.getKey(); + List files = e.getValue(); + if (files == null || files.isEmpty()) continue; + List names = folderNames.get(subType); + String label = SERVICE_SUBTYPE_LABEL.getOrDefault(subType, subType); + + String ossUrl; + String fileName; + if (files.size() == 1) { + fileName = (names != null && !names.isEmpty()) ? names.get(0) : (label + ".jpg"); + ossUrl = ossUploader.upload(files.get(0), fileName, subDir); + } else { + // 目录内多文件 → 先打 zip 再上传 OSS + fileName = label + ".zip"; + ossUrl = ossUploader.upload(zipFiles(files, names), fileName, subDir); + } + + // 会务材料都是发票类 (M_* 不在 NON_OCR 集合): 单文件可识别 (jpg/png/pdf) 或多文件打 zip → 待 OCR(0), 其余已计算(1) + boolean isZip = files.size() > 1; + boolean ocrNeeded = isZip || (ossUrl != null && RECOGNIZABLE.matcher(ossUrl).find()); + int feeStatus = ocrNeeded ? 0 : 1; + + BizMeetingMaterial old = existingBySubType.get(subType); + Long materialId; + Long oldMaterialId; + if (old != null) { + materialId = old.getId(); + bizMeetingMaterialMapper.updateFile(old.getId(), ossUrl, fileName, feeStatus); + oldMaterialId = old.getId(); // 替换: 清理该 material 的旧 invoice 行 + } else { + BizMeetingMaterial ins = new BizMeetingMaterial(); + ins.setMeetingId(meetingId); + ins.setMaterialType("SERVICE"); + ins.setSubType(subType); + ins.setOssUrl(ossUrl); + ins.setFileName(fileName); + ins.setAmount(null); + ins.setFeeStatus(feeStatus); + ins.setCreatorId(SecurityUtils.getUserId()); + ins.setCreateTime(new Date()); + bizMeetingMaterialMapper.insertBatch(Collections.singletonList(ins)); + // foreach 批量插入 useGeneratedKeys 不可靠, 按 subType 查回真实 id + BizMeetingMaterial inserted = bizMeetingMaterialMapper.selectByMeetingAndSubType(meetingId, subType); + materialId = inserted != null ? inserted.getId() : null; + oldMaterialId = null; + } + // 与前端"保存"流程一致, 后台触发 OCR (识别为发票回写 amount → 会议费用汇总) + if (ocrNeeded) { + invoiceOcrService.submitRecognition(materialId, meetingId, ossUrl, isZip, oldMaterialId); + } + updated++; + } + log.info("[material] 会务材料回填完成 meetingId={} 共{}类", meetingId, updated); + return updated; + } + + /** 按指定字符集解压 zip, 把 会务材料/{材料中文名}/ 目录下的文件归集到对应 subType */ + private void readServiceZip(byte[] zipBytes, Charset charset, + Map> folderBytes, + Map> folderNames) throws IOException { + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes), charset)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + String path = entry.getName(); + if (path.contains("__MACOSX") || path.endsWith(".DS_Store")) continue; + String subType = matchServiceLabel(path); + if (subType == null) continue; // 不落在 会务材料/{材料中文名}/ 目录下, 忽略 + byte[] data = readAllBytes(zis); + folderBytes.computeIfAbsent(subType, k -> new ArrayList<>()).add(data); + folderNames.computeIfAbsent(subType, k -> new ArrayList<>()).add(lastSegment(path)); + } + } + } + + /** 从 zip 条目路径里匹配出会务材料 subType (扫描路径段命中材料中文名); 不匹配返回 null */ + private static String matchServiceLabel(String path) { + if (path == null || path.isEmpty()) return null; + for (String seg : path.split("/")) { + if (seg.isEmpty()) continue; + String sub = SERVICE_LABEL_TO_SUBTYPE.get(seg); + if (sub != null) return sub; + } + return null; + } + + /** 把目录内多个文件打成内存 zip (保留原始文件名) */ + private static byte[] zipFiles(List files, List names) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) { + for (int i = 0; i < files.size(); i++) { + String name = (names != null && i < names.size()) ? names.get(i) : ("file-" + (i + 1)); + zos.putNextEntry(new ZipEntry(name)); + zos.write(files.get(i)); + zos.closeEntry(); + } + } + return baos.toByteArray(); + } + + private static String lastSegment(String path) { + int i = path.lastIndexOf('/'); + return i >= 0 ? path.substring(i + 1) : path; + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + /** * 是否会被前端提交 OCR (与 MeetingDetail.saveMaterials 门控一致): * 文件可识别 (jpg/jpeg/png/pdf) 且 不在非发票 subType 集合里. 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 076cbd9..05fb66c 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 @@ -46,16 +46,13 @@ public class BizMeetingServiceImpl implements IBizMeetingService } // 新建会议初始状态: DB 列默认值 '0' 会让前端 stageLabel 显示成 0 而不是 enum 项, 这里显式兜底成 enum. // current_stage = NOT_STARTED (未执行, 等 scheduler 过 startTime 置 is_executed 转 RUNNING) - // 材料/凭证审核子状态 = NOT_SUBMITTED (未提交, 供执行方 submit-material/submit-voucher 校验) + // 材料审核子状态 = NOT_SUBMITTED (未提交, 供执行方 submit-material 校验) 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 diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSupervisorServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSupervisorServiceImpl.java index 483b45a..b146efb 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSupervisorServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSupervisorServiceImpl.java @@ -25,23 +25,18 @@ public class BizMeetingSupervisorServiceImpl implements IBizMeetingSupervisorSer return bizMeetingSupervisorMapper.selectByMeetingId(meetingId); } - @Override - public List selectByUserId(Long userId) { - return bizMeetingSupervisorMapper.selectByUserId(userId); - } - @Override @Transactional(rollbackFor = Exception.class) - public int replaceByMeetingId(Long meetingId, List userIds, Long assignedBy) { + public int replaceByMeetingId(Long meetingId, List orgIds, Long assignedBy) { bizMeetingSupervisorMapper.deleteByMeetingId(meetingId); - if (userIds == null || userIds.isEmpty()) { + if (orgIds == null || orgIds.isEmpty()) { return 0; } - List list = new ArrayList<>(userIds.size()); - for (Long uid : userIds) { + List list = new ArrayList<>(orgIds.size()); + for (Long orgId : orgIds) { BizMeetingSupervisor m = new BizMeetingSupervisor(); m.setMeetingId(meetingId); - m.setUserId(uid); + m.setSponsorOrgId(orgId); m.setAssignedBy(assignedBy); list.add(m); } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java index 23a056a..c09db9d 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java @@ -1,15 +1,21 @@ package com.ruoyi.business.service.impl; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.domain.dto.ImportResult; +import com.ruoyi.business.domain.vo.BizOrgImportVo; import com.ruoyi.business.mapper.BizOrgMapper; import com.ruoyi.business.service.IBizOrgService; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.id.SnowflakeId; +import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.system.mapper.SysUserMapper; @Service @@ -52,11 +58,22 @@ public class BizOrgServiceImpl implements IBizOrgService { return bizOrgMapper.selectExecutorOrgOptions(entity); } + @Override + public List> selectSponsorRegisterOptions(BizOrg entity) { + return bizOrgMapper.selectSponsorRegisterOptions(entity); + } + @Override public Map selectMySponsorCompany(Long userId) { return bizOrgMapper.selectMySponsorCompany(userId); } + @Override + public Long selectOrgIdByUserId(Long userId) { + if (userId == null) return null; + return bizOrgMapper.selectOrgIdByUserId(userId); + } + /** * 启用/禁用 org, 同步主账号 sys_user.status * biz_org.status: '0' (正常) / '1' (禁用) → sys_user.status 同码 ('0' 正常, '1' 停用) @@ -82,4 +99,80 @@ public class BizOrgServiceImpl implements IBizOrgService { return rows; } + + /** + * 批量导入支持单位 (只导 org, 不新增人员): + *

        + *
      1. ExcelUtil 反序列化 + 行级校验 (公司名称必填)
      2. + *
      3. 批内去重 (公司名称 / 税号) + DB 去重 (sponsor 下同税号已存在)
      4. + *
      5. 逐行 insert biz_org: orgType='sponsor', status='0', userId 留空 (不建 sys_user)
      6. + *
      + * 失败行为: 单行失败只记 ngList, 不影响其它行 (跨行不回滚). + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ImportResult importOrg(MultipartFile file, String operName) throws Exception { + ExcelUtil util = new ExcelUtil<>(BizOrgImportVo.class); + List importList = util.importExcel(file.getInputStream()); + if (importList == null || importList.isEmpty()) { + throw new ServiceException("导入数据不能为空"); + } + ImportResult result = new ImportResult(); + + Set seenOrgName = new HashSet<>(); + Set seenTaxNo = new HashSet<>(); + + for (int i = 0; i < importList.size(); i++) { + BizOrgImportVo vo = importList.get(i); + int rowNo = i + 2; // Excel 行号 (1=表头) + try { + if (vo.getOrgName() == null || vo.getOrgName().trim().isEmpty()) { + throw new ServiceException("公司名称不能为空"); + } + String orgName = vo.getOrgName().trim(); + String taxNo = vo.getTaxNo() == null ? "" : vo.getTaxNo().trim(); + + // 批内去重 + if (!seenOrgName.add(orgName)) { + throw new ServiceException("公司名称「" + orgName + "」在文件中重复"); + } + if (!taxNo.isEmpty()) { + if (!seenTaxNo.add(taxNo)) { + throw new ServiceException("税号「" + taxNo + "」在文件中重复"); + } + // DB 去重: sponsor 下同税号已存在 + BizOrg probe = new BizOrg(); + probe.setOrgType("sponsor"); + probe.setTaxNo(taxNo); + if (!bizOrgMapper.selectList(probe).isEmpty()) { + throw new ServiceException("税号「" + taxNo + "」已存在"); + } + } + + BizOrg org = new BizOrg(); + org.setOrgType("sponsor"); + org.setOrgName(orgName); + org.setTaxNo(taxNo.isEmpty() ? null : taxNo); + org.setBusinessNature(trimToNull(vo.getBusinessNature())); + org.setAddress(trimToNull(vo.getAddress())); + org.setContactName(trimToNull(vo.getContactName())); + org.setContactPhone(trimToNull(vo.getContactPhone())); + org.setStatus("0"); + org.setCreateBy(operName); + bizOrgMapper.insert(org); + + result.ok(); + } catch (Exception e) { + result.fail(rowNo, e.getMessage() == null ? "导入失败" : e.getMessage()); + } + } + return result; + } + + /** 空串转 null (避免插空字符串脏数据) */ + private String trimToNull(String s) { + if (s == null) return null; + String t = s.trim(); + return t.isEmpty() ? null : t; + } } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java index be07895..f4c9b73 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java @@ -3,6 +3,7 @@ package com.ruoyi.business.service.impl; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.ruoyi.business.domain.BizOrg; import com.ruoyi.business.domain.BizPerson; import com.ruoyi.business.mapper.BizOrgMapper; @@ -69,7 +70,12 @@ public class BizPersonServiceImpl implements IBizPersonService } } if (entity.getOrgId() == null) { - throw new ServiceException("请填写所属公司 (主账号尚未注册公司, 请联系 admin 在 [支持单位管理/服务机构管理] 录入 orgType=" + entity.getUnitType() + " 的公司并关联 user_id)"); + // 支持单位: 公司由 admin 在 [支持单位管理] 预录, 支持方注册时选公司 (不新建); + // 执行单位: 主账号注册时自行创建公司 (registerExecutor 建 biz_org), 不走 admin 录入. + if ("executor".equals(entity.getUnitType())) { + throw new ServiceException("请填写所属公司 (执行单位需自行创建公司, 请先完成公司注册)"); + } + throw new ServiceException("请填写所属公司 (主账号尚未注册公司, 请联系 admin 在 [支持单位管理] 录入 orgType=" + entity.getUnitType() + " 的公司并关联 user_id)"); } // 1. 创建 sys_user 子账号 @@ -80,6 +86,7 @@ public class BizPersonServiceImpl implements IBizPersonService newUser.setUserName(entity.getLoginUsername()); newUser.setNickName(entity.getName()); newUser.setPhonenumber(entity.getPhone()); + newUser.setEmail(entity.getEmail()); newUser.setPassword(SecurityUtils.encryptPassword(entity.getLoginPassword())); newUser.setAccountType("SUB"); newUser.setParentUserId(mainUserId); @@ -116,6 +123,7 @@ public class BizPersonServiceImpl implements IBizPersonService u.setUserId(entity.getUserId()); u.setNickName(entity.getName()); u.setPhonenumber(entity.getPhone()); + u.setEmail(entity.getEmail()); u.setUpdateBy(SecurityUtils.getUsername()); // status 同步 (如果前端传了 status 字段) if (entity.getStatus() != null) { @@ -139,4 +147,50 @@ public class BizPersonServiceImpl implements IBizPersonService int n = bizPersonMapper.deleteByPrimaryKeys(personIds); return n; } + + /** + * 更换机构管理员: 目标人员晋升 MAIN (管理员), 原管理员降为 SUB + * 数据口径: MAIN ⇔ parent_user_id IS NULL, SUB ⇔ parent_user_id = 主账号 user_id + * 操作顺序避免出现三层/环: 先 re-point 其余子账号 → 晋升新管理员 → 降级原管理员 → 改 biz_org 归属 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeOrgAdmin(String personId) { + BizPerson target = bizPersonMapper.selectByPrimaryKey(personId); + if (target == null || target.getUserId() == null) { + throw new ServiceException("人员不存在或未关联登录账号"); + } + Long newAdminUid = target.getUserId(); + SysUser newUser = sysUserMapper.selectUserById(newAdminUid); + if (newUser == null) { + throw new ServiceException("登录账号不存在"); + } + if ("MAIN".equals(newUser.getAccountType())) { + return 0; // 已是管理员, 无需操作 + } + if (target.getOrgId() == null) { + throw new ServiceException("该人员未关联机构, 无法设为管理员"); + } + BizOrg org = bizOrgMapper.selectByPrimaryKey(target.getOrgId()); + Long oldAdminUid = (org == null) ? null : org.getUserId(); + + // 1. 原管理员的其他子账号 re-point 到新管理员 (排除新管理员自己, 其 parent 当前仍指向原管理员) + if (oldAdminUid != null && !oldAdminUid.equals(newAdminUid)) { + sysUserMapper.updateParentUserId(oldAdminUid, newAdminUid); + } + // 2. 新管理员晋升 MAIN (parent_user_id = NULL) + sysUserMapper.updateAccountType(newAdminUid, "MAIN", null); + // 3. 原管理员降级 SUB (parent 指向新管理员) + if (oldAdminUid != null && !oldAdminUid.equals(newAdminUid)) { + sysUserMapper.updateAccountType(oldAdminUid, "SUB", newAdminUid); + } + // 4. biz_org.user_id 同步指向新管理员 (机构归属/反查) + if (org != null) { + BizOrg u = new BizOrg(); + u.setOrgId(target.getOrgId()); + u.setUserId(newAdminUid); + bizOrgMapper.updateByPrimaryKey(u); + } + return 1; + } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java index e32c325..ae5f5cd 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java @@ -5,10 +5,8 @@ import java.util.Date; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.ruoyi.business.domain.BizOrg; import com.ruoyi.business.domain.BizProject; import com.ruoyi.business.domain.BizProjectAssign; -import com.ruoyi.business.mapper.BizOrgMapper; import com.ruoyi.business.mapper.BizProjectAssignMapper; import com.ruoyi.business.mapper.BizProjectMapper; import com.ruoyi.business.service.IBizProjectAssignService; @@ -22,8 +20,6 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService @Autowired private BizProjectAssignMapper bizProjectAssignMapper; @Autowired - private BizOrgMapper bizOrgMapper; - @Autowired private BizProjectMapper bizProjectMapper; @Override @@ -37,30 +33,19 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService /** * 插入项目执行方分配: - * 1. execUserId → biz_org(org_type='executor') 反查 org_id 写入 executionUnitId (NOT NULL, 找不到 throw) + * 1. executionUnitId (前端必传, 单一可信源 biz_org.org_id) * 2. 自动补 audit 字段 (createBy/createTime/updateBy/updateTime) 从 SecurityUtils * 3. SnowflakeId 主键 * 4. status='0' 默认 */ @Override public int insert(BizProjectAssign entity) { - // 0. 必须有 execUserId (前端必传) - if (entity.getExecUserId() == null) { - throw new ServiceException("执行方用户ID不能为空"); + // 0. 必须有 executionUnitId (前端必传) + if (entity.getExecutionUnitId() == null) { + throw new ServiceException("执行单位ID不能为空"); } - // 1. execUserId → biz_org(executor) → executionUnitId - BizOrg q = new BizOrg(); - q.setUserId(entity.getExecUserId()); - q.setOrgType("executor"); - List matched = bizOrgMapper.selectList(q); - if (matched == null || matched.isEmpty()) { - throw new ServiceException("执行方用户 " + entity.getExecUserId() + " 未关联执行单位 (请检查 biz_org.org_type='executor' + user_id 是否绑定)"); - } - BizOrg org = matched.get(0); - entity.setExecutionUnitId(org.getOrgId()); - - // 2. audit 字段 + // 1. audit 字段 Date now = new Date(); String operator = SecurityUtils.getUsername(); if (entity.getCreateBy() == null || entity.getCreateBy().isEmpty()) entity.setCreateBy(operator); @@ -68,10 +53,10 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService entity.setUpdateBy(operator); entity.setUpdateTime(now); - // 3. 默认值 + // 2. 默认值 if (entity.getStatus() == null || entity.getStatus().isEmpty()) entity.setStatus("0"); - // 4. SnowflakeId + // 3. SnowflakeId SnowflakeId.injectIfEmpty(entity, "assignId"); return bizProjectAssignMapper.insert(entity); 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 index e8970da..c0e2d2b 100644 --- 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 @@ -39,7 +39,7 @@ public class BizProjectExecutorAssignServiceImpl implements IBizProjectExecutorA item.setAssignDesc(body.getAssignDesc()); item.setAssignPoints(body.getAssignPoints()); item.setCreateBy(body.getCreateBy()); - item.setExecutorUserId(body.getExecutorUserId()); + item.setExecutorOrgId(body.getExecutorOrgId()); inserted += mapper.insertAssign(item); } return inserted; 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 0203834..2d4044b 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 @@ -138,4 +138,16 @@ public class BizProjectServiceImpl implements IBizProjectService if (projectId == null) return; bizProjectMapper.recomputeSettledAmounts(projectId); } + + /** 删除公告: 将 3 个公示 URL 置 NULL (未发布) */ + @Override + public int clearAnnouncement(Long projectId) { + return bizProjectMapper.clearAnnouncement(projectId); + } + + /** 开通到期回收 */ + @Override + public int closeExpiredOpenStatus() { + return bizProjectMapper.closeExpiredOpenStatus(); + } } 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 2b547f2..68dd6fd 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 @@ -39,7 +39,7 @@ public class BizProjectSponsorAssignServiceImpl implements IBizProjectSponsorAss item.setAssignDesc(body.getAssignDesc()); item.setAssignPoints(body.getAssignPoints()); item.setCreateBy(body.getCreateBy()); - item.setSponsorUserId(body.getSponsorUserId()); + item.setSponsorOrgId(body.getSponsorOrgId()); inserted += mapper.insertAssign(item); } return inserted; diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingAttendeeMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingAttendeeMapper.xml index bb66187..2ed97f9 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingAttendeeMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingAttendeeMapper.xml @@ -154,6 +154,7 @@ - - @@ -47,30 +45,30 @@ insert into biz_meeting_executor meeting_id, - user_id, + executor_org_id, assigned_by, create_time, #{meetingId}, - #{userId}, + #{executorOrgId}, #{assignedBy}, #{createTime}, - insert into biz_meeting_executor (meeting_id, user_id, assigned_by, create_time) + insert into biz_meeting_executor (meeting_id, executor_org_id, assigned_by, create_time) values - (#{item.meetingId}, #{item.userId}, #{item.assignedBy}, #{item.createTime}) + (#{item.meetingId}, #{item.executorOrgId}, #{item.assignedBy}, #{item.createTime}) update biz_meeting_executor - user_id = #{userId}, + executor_org_id = #{executorOrgId}, assigned_by = #{assignedBy}, where id = #{id} diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml index faea603..1199fa6 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml @@ -20,7 +20,6 @@ - @@ -30,10 +29,10 @@ - - + + @@ -50,10 +49,12 @@ - meeting_id, business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, org_name, address, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, voucher_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, voucher_audit_time, material_compliance_approved, voucher_compliance_approved, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted + meeting_id, business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, address, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, material_compliance_approved, submit_deadline, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted @@ -61,6 +62,8 @@ select (select a.id from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_id, (select a.labor_protocol from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_labor_protocol, + (select o.org_name from biz_project p join biz_org o on o.org_id = p.sponsor_org_id where p.project_id = biz_meeting.project_id limit 1) as org_name, + (select p.invitation_url from biz_project p where p.project_id = biz_meeting.project_id limit 1) as project_invitation_url, from biz_meeting @@ -75,16 +78,24 @@ and end_time <= #{endTime} and exists (select 1 from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0) - - and project_id in (select project_id from biz_project where sponsor_admin_user_id = #{params.sponsorAdminUserId} and is_deleted = 0) + and project_id in (select project_id from biz_project where sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor') and is_deleted = 0) and project_id in (select distinct project_id from biz_project_sponsor_assign where monitor_user_id = #{params.monitorUserId} and is_deleted = 0) - + + + and not exists ( + select 1 from biz_project p2 + where p2.project_id = biz_meeting.project_id + and p2.is_deleted = 0 + and p2.is_finished = '1' and p2.open_status = 'N' + ) + + and project_id in ( select distinct a.project_id from biz_project_assign a where a.is_deleted = 0 - and (a.exec_user_id = #{params.executorUserId} - or a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) + and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor') ) and project_id in ( select distinct a.project_id from biz_project_executor_assign a @@ -107,14 +118,13 @@ project_form, start_time, end_time, - org_name, address, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, - voucher_audit_stage, + submit_deadline, invitation_url, schedule_url, poster_url, @@ -136,14 +146,13 @@ #{projectForm}, #{startTime}, #{endTime}, - #{orgName}, #{address}, #{currentStage}, #{supervisionOpinion}, #{supervisionBy}, #{supervisionTime}, #{materialAuditStage}, - #{voucherAuditStage}, + #{submitDeadline}, #{invitationUrl}, #{scheduleUrl}, #{posterUrl}, @@ -168,14 +177,12 @@ start_time = #{startTime}, end_time = #{endTime}, - org_name = #{orgName}, address = #{address}, current_stage = #{currentStage}, supervision_opinion = #{supervisionOpinion}, supervision_by = #{supervisionBy}, supervision_time = #{supervisionTime}, material_audit_stage = #{materialAuditStage}, - voucher_audit_stage = #{voucherAuditStage}, is_executed = #{isExecuted}, execute_time = #{executeTime}, is_settled = #{isSettled}, @@ -185,9 +192,8 @@ is_frozen = #{isFrozen}, freeze_time = #{freezeTime}, material_audit_time = #{materialAuditTime}, - voucher_audit_time = #{voucherAuditTime}, material_compliance_approved = #{materialComplianceApproved}, - voucher_compliance_approved = #{voucherComplianceApproved}, + submit_deadline = #{submitDeadline}, invitation_url = #{invitationUrl}, schedule_url = #{scheduleUrl}, poster_url = #{posterUrl}, @@ -231,21 +237,20 @@ and start_time <= NOW() and material_audit_stage = 'NOT_SUBMITTED' - + - update biz_meeting m - join biz_project p on p.project_id = m.project_id and p.is_deleted = 0 - set m.is_frozen = 1, - m.freeze_time = NOW(), - m.current_stage = 'FROZEN' - where m.is_deleted = 0 - and m.is_frozen = 0 - and m.material_audit_stage = 'NOT_SUBMITTED' - and m.end_time is not null - and p.submit_deadline_days is not null - and date_add(m.end_time, interval p.submit_deadline_days day) <= NOW() + update biz_meeting + set is_frozen = 1, + freeze_time = NOW(), + current_stage = 'FROZEN' + where is_deleted = 0 + and is_frozen = 0 + and material_audit_stage in ('NOT_SUBMITTED', 'REJECTED') + and submit_deadline is not null + and submit_deadline <= NOW() - + update biz_meeting set current_stage = 'AWAITING_SETTLEMENT' @@ -254,10 +259,9 @@ and is_settled = 0 and is_finished = 0 and material_audit_stage = 'APPROVED' - and voucher_audit_stage = 'APPROVED' and current_stage = 'SUPERVISION_APPROVED' - and greatest(material_audit_time, voucher_audit_time) is not null - and greatest(material_audit_time, voucher_audit_time) <= (NOW() - INTERVAL 1 DAY) + and material_audit_time is not null + and material_audit_time <= (NOW() - INTERVAL 1 DAY) + + insert into biz_meeting_material @@ -89,6 +95,13 @@ update biz_meeting_material set fee_status = #{feeStatus} where id = #{id} + + + update biz_meeting_material + set oss_url = #{ossUrl}, file_name = #{fileName}, amount = null, fee_status = #{feeStatus} + where id = #{id} + + delete from biz_meeting_material where id = #{id} diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSupervisorMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSupervisorMapper.xml index 8cedee6..7312f57 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSupervisorMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSupervisorMapper.xml @@ -5,14 +5,15 @@ - + + - select id, meeting_id, user_id, assigned_by, create_time, is_deleted + select id, meeting_id, sponsor_org_id, assigned_by, create_time, is_deleted from biz_meeting_supervisor @@ -22,15 +23,12 @@ - - @@ -47,30 +45,30 @@ insert into biz_meeting_supervisor meeting_id, - user_id, + sponsor_org_id, assigned_by, create_time, #{meetingId}, - #{userId}, + #{sponsorOrgId}, #{assignedBy}, #{createTime}, - insert into biz_meeting_supervisor (meeting_id, user_id, assigned_by, create_time) + insert into biz_meeting_supervisor (meeting_id, sponsor_org_id, assigned_by, create_time) values - (#{item.meetingId}, #{item.userId}, #{item.assignedBy}, #{item.createTime}) + (#{item.meetingId}, #{item.sponsorOrgId}, #{item.assignedBy}, #{item.createTime}) update biz_meeting_supervisor - user_id = #{userId}, + sponsor_org_id = #{sponsorOrgId}, assigned_by = #{assignedBy}, where id = #{id} diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml index 5b8b21b..29ee44f 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml @@ -103,20 +103,20 @@ - select o.user_id as userId, + select o.org_id as orgId, o.org_name as orgName, u.user_name as userName from biz_org o @@ -141,12 +141,30 @@ where o.org_type = 'executor' and u.del_flag = '0' and u.parent_user_id is null - and o.user_id = #{userId} + and o.org_id = #{orgId} and o.org_name like concat('%', #{orgName}, '%') order by o.org_id desc LIMIT 5 + + + + diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml index a29bbb5..2a980ea 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml @@ -10,12 +10,12 @@ - + @@ -23,17 +23,18 @@ - select person_id, name, phone, org_id, department, position, role, unit_type, user_id, create_by, create_time, update_by, update_time + select person_id, name, phone, org_id, department, position, unit_type, user_id, create_by, create_time, update_by, update_time from biz_person select p.person_id, p.name, p.phone, p.org_id, o.org_name, o.org_type, - p.department, p.position, p.role, p.unit_type, p.user_id, + p.department, p.position, p.unit_type, p.user_id, p.create_by, p.create_time, p.update_by, p.update_time, u.account_type, u.parent_user_id, u.status, u.del_flag as user_del_flag, - u.user_name as user_name + u.user_name as user_name, + u.email as email from biz_person p left join biz_org o on p.org_id = o.org_id left join sys_user u on p.user_id = u.user_id @@ -51,12 +52,12 @@ and p.unit_type = #{unitType} and p.name like concat('%', #{name}, '%') and p.phone = #{phone} + and u.email = #{email} and p.org_id = #{orgId} and o.org_name like concat('%', #{orgName}, '%') and o.org_type = #{orgType} and p.department = #{department} and p.position = #{position} - and p.role = #{role} and u.status = #{status} and u.parent_user_id = #{parentUserId} and p.user_id = #{userId} @@ -80,7 +81,6 @@ org_id, department, position, - role, unit_type, user_id, create_by, @@ -95,7 +95,6 @@ #{orgId}, #{department}, #{position}, - #{role}, #{unitType}, #{userId}, #{createBy}, @@ -115,7 +114,6 @@ phone = #{phone}, department = #{department}, position = #{position}, - role = #{role}, update_by = #{updateBy}, update_time = sysdate(), @@ -162,7 +160,6 @@ and o.org_name like concat('%', #{orgName}, '%') and p.department = #{department} and p.position = #{position} - and p.role = #{role} and u.status = #{status} order by p.person_id desc @@ -183,16 +180,9 @@ and o.org_name like concat('%', #{orgName}, '%') and p.department = #{department} and p.position = #{position} - and p.role = #{role} and u.status = #{status} order by p.person_id desc - - diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml index 126d7e6..54b599c 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml @@ -5,7 +5,6 @@ - @@ -18,7 +17,7 @@ - select assign_id, project_id, execution_unit_id, exec_user_id, + select assign_id, project_id, execution_unit_id, sessions, amount, remark, status, create_by, create_time, update_by, update_time, is_deleted from biz_project_assign @@ -41,7 +40,6 @@ is_deleted = 0 and project_id = #{projectId} and execution_unit_id = #{executionUnitId} - and exec_user_id = #{execUserId} and status = #{status} order by assign_id desc @@ -57,7 +55,6 @@ assign_id, project_id, execution_unit_id, - exec_user_id, sessions, amount, remark, @@ -71,7 +68,6 @@ #{assignId}, #{projectId}, #{executionUnitId}, - #{execUserId}, #{sessions}, #{amount}, #{remark}, @@ -87,7 +83,6 @@ update biz_project_assign execution_unit_id = #{executionUnitId}, - exec_user_id = #{execUserId}, sessions = #{sessions}, amount = #{amount}, remark = #{remark}, diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectExecutorAssignMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectExecutorAssignMapper.xml index 2bc7a9b..7c4e159 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectExecutorAssignMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectExecutorAssignMapper.xml @@ -5,7 +5,7 @@ - + @@ -18,9 +18,9 @@ INSERT INTO biz_project_executor_assign - (project_id, executor_user_id, staff_user_id, assign_desc, assign_points, create_by, create_time) + (project_id, executor_org_id, staff_user_id, assign_desc, assign_points, create_by, create_time) VALUES - (#{projectId}, #{executorUserId}, #{staffUserId}, #{assignDesc}, #{assignPoints}, #{createBy}, sysdate()) + (#{projectId}, #{executorOrgId}, #{staffUserId}, #{assignDesc}, #{assignPoints}, #{createBy}, sysdate()) @@ -38,7 +38,8 @@ e.user_name AS executor_user_name, s.user_name AS staff_user_name FROM biz_project_executor_assign a - LEFT JOIN sys_user e ON a.executor_user_id = e.user_id + LEFT JOIN biz_org o ON o.org_id = a.executor_org_id + LEFT JOIN sys_user e ON e.user_id = o.user_id LEFT JOIN sys_user s ON a.staff_user_id = s.user_id WHERE a.project_id = #{projectId} and a.is_deleted = 0 ORDER BY a.create_time DESC diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml index e6f8c32..bcb70db 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml @@ -18,7 +18,7 @@ - + @@ -43,11 +43,15 @@ + + + - select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, p.is_deleted, + select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted, o.org_name as sponsor_org_name, + su.user_name as sponsor_admin_user_name, lu.user_name as lead_user_name, bp.name as create_user_name, (select group_concat(distinct o2.org_name separator ',') @@ -55,7 +59,8 @@ join biz_org o2 on o2.org_id = bpa.execution_unit_id and o2.org_type = 'executor' where bpa.project_id = p.project_id and bpa.is_deleted = 0) as exec_org_names from biz_project p - left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor' + left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor' + left join sys_user su on su.user_id = o.user_id left join sys_user lu on lu.user_id = p.lead_user_id left join biz_person bp on bp.user_id = p.create_user_id @@ -64,13 +69,14 @@ select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, - p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, - p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, + p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, + p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, - p.publish_url, p.is_deleted, + p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted, o.org_name as sponsor_org_name, + su.user_name as sponsor_admin_user_name, lu.user_name as lead_user_name, bp.name as create_user_name, (select group_concat(distinct o2.org_name separator ',') @@ -78,7 +84,8 @@ join biz_org o2 on o2.org_id = bpa.execution_unit_id and o2.org_type = 'executor' where bpa.project_id = p.project_id and bpa.is_deleted = 0) as exec_org_names from biz_project p - left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor' + left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor' + left join sys_user su on su.user_id = o.user_id left join sys_user lu on lu.user_id = p.lead_user_id left join biz_person bp on bp.user_id = p.create_user_id @@ -91,6 +98,8 @@ p.is_deleted = 0 + + and not (p.is_finished = '1' and p.open_status = 'N') and p.project_id in @@ -98,12 +107,12 @@ and ( - p.sponsor_admin_user_id = #{params.sponsorAdminUserId} + p.sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor') or p.project_id in ( select distinct i.project_id from biz_publicity_support_intent i @@ -140,8 +149,9 @@ 注: executor 角色无评分/聚合分需求, 复用 selectFields (含 sponsor_score / manager_score) --> - select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, + select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, o.org_name as sponsor_org_name, + su.user_name as sponsor_admin_user_name, lu.user_name as lead_user_name, bp.name as create_user_name, (select group_concat(distinct o2.org_name separator ',') @@ -200,17 +209,16 @@ from biz_project_assign bpa3 where bpa3.project_id = p.project_id and bpa3.is_deleted = 0 - and (bpa3.exec_user_id = #{params.executorUserId} - or bpa3.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_sessions, + and bpa3.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as assigned_sessions, (select coalesce(sum(bpa4.amount), 0) from biz_project_assign bpa4 where bpa4.project_id = p.project_id and bpa4.is_deleted = 0 - and (bpa4.exec_user_id = #{params.executorUserId} - or bpa4.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_amount, + and bpa4.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as assigned_amount, (select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0) as meeting_count from biz_project p - left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor' + left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor' + left join sys_user su on su.user_id = o.user_id left join sys_user lu on lu.user_id = p.lead_user_id left join biz_person bp on bp.user_id = p.create_user_id @@ -248,25 +256,26 @@ and project_no like concat('%', #{projectNo}, '%') and project_name like concat('%', #{projectName}, '%') and project_form = #{projectForm} - - and sponsor_admin_user_id in - + + and sponsor_org_id in + #{id} - + and exists ( select 1 from biz_project_assign bpa - where bpa.project_id = project_id - and bpa.exec_user_id in - + where bpa.project_id = p.project_id + and bpa.execution_unit_id in + #{id} ) and is_finished = #{isFinished} and is_settled = #{isSettled} - and manager_score = #{managerScore} + and start_time >= #{startTime} + and end_time <= #{endTime} order by project_id desc @@ -285,11 +294,10 @@ paid_meeting_amount, manager_score, sponsor_score, - sponsor_admin_user_name, project_form, is_finished, is_settled, - sponsor_admin_user_id, + sponsor_org_id, lead_user_id, is_bid_project, create_by, @@ -307,6 +315,9 @@ invitation_url, support_letter_url, publish_url, + schedule_url, + open_deadline, + open_status, #{projectId}, @@ -321,11 +332,10 @@ #{paidMeetingAmount}, #{managerScore}, #{sponsorScore}, - #{sponsorAdminUserName}, #{projectForm}, #{isFinished}, #{isSettled}, - #{sponsorAdminUserId}, + #{sponsorOrgId}, #{leadUserId}, #{isBidProject}, #{createBy}, @@ -343,6 +353,9 @@ #{invitationUrl}, #{supportLetterUrl}, #{publishUrl}, + #{scheduleUrl}, + #{openDeadline}, + #{openStatus}, @@ -358,6 +371,9 @@ invitation_url = #{invitationUrl}, support_letter_url = #{supportLetterUrl}, publish_url = #{publishUrl}, + schedule_url = #{scheduleUrl}, + open_deadline = #{openDeadline}, + open_status = #{openStatus}, project_no = #{projectNo}, project_name = #{projectName}, total_sessions = #{totalSessions}, @@ -369,11 +385,10 @@ paid_meeting_amount = #{paidMeetingAmount}, manager_score = #{managerScore}, sponsor_score = #{sponsorScore}, - sponsor_admin_user_name = #{sponsorAdminUserName}, project_form = #{projectForm}, is_finished = #{isFinished}, is_settled = #{isSettled}, - sponsor_admin_user_id = #{sponsorAdminUserId}, + sponsor_org_id = #{sponsorOrgId}, lead_user_id = #{leadUserId}, is_bid_project = #{isBidProject}, create_by = #{createBy}, @@ -384,6 +399,24 @@ where project_id = #{projectId} + + + update biz_project + set invitation_url = null, + support_letter_url = null, + publish_url = null, + schedule_url = null + where project_id = #{projectId} + + + + update biz_project + set open_status = 'N' + where open_status = 'Y' + and open_deadline is not null + and open_deadline <= CURDATE() + and is_deleted = 0 + delete from biz_project where project_id = #{projectId} @@ -407,8 +440,7 @@ from biz_project_assign a where a.project_id = #{projectId} and a.is_deleted = 0 - and (a.exec_user_id = #{executorUserId} - or a.execution_unit_id = (select org_id from biz_org where user_id = #{executorUserId} and org_type = 'executor')) + and a.execution_unit_id = (select org_id from biz_org where user_id = #{executorUserId} and org_type = 'executor') @@ -38,7 +38,8 @@ s.user_name AS sponsor_user_name, m.user_name AS monitor_user_name FROM biz_project_sponsor_assign a - LEFT JOIN sys_user s ON a.sponsor_user_id = s.user_id + LEFT JOIN biz_org o ON o.org_id = a.sponsor_org_id + LEFT JOIN sys_user s ON s.user_id = o.user_id LEFT JOIN sys_user m ON a.monitor_user_id = m.user_id WHERE a.project_id = #{projectId} and a.is_deleted = 0 ORDER BY a.create_time DESC diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java index 0b05a28..7136525 100644 --- a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java @@ -170,6 +170,8 @@ public class RuoYiConfig private String accessKeySecret; private String dirPrefix = "ry8080/"; private int expireSeconds = 3600; + /** 阿里云函数计算 (FC) 打 zip 端点 (参考 hwt-serve OssApi.ossDownload, 在 OSS 端打包) */ + private String zipFuncUrl; public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } @@ -185,5 +187,7 @@ public class RuoYiConfig public void setDirPrefix(String dirPrefix) { this.dirPrefix = dirPrefix; } public int getExpireSeconds() { return expireSeconds; } public void setExpireSeconds(int expireSeconds) { this.expireSeconds = expireSeconds; } + public String getZipFuncUrl() { return zipFuncUrl; } + public void setZipFuncUrl(String zipFuncUrl) { this.zipFuncUrl = zipFuncUrl; } } } \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/MeetingAuditStageEnum.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/MeetingAuditStageEnum.java index b207a85..a03d4e2 100644 --- a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/MeetingAuditStageEnum.java +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/MeetingAuditStageEnum.java @@ -1,10 +1,10 @@ package com.ruoyi.common.enums; /** - * 材料/凭证 审核阶段 (biz_meeting.material_audit_stage / voucher_audit_stage) + * 材料 审核阶段 (biz_meeting.material_audit_stage) *

      * 4 值 (用户拍板「已提交/待审核」合并): 提交后进入 SUBMITTED, 两级审核(合规先-支持方后) - * 用 material_compliance_approved / voucher_compliance_approved 布尔区分「合规审中 vs 支持方审中」. + * 用 material_compliance_approved 布尔区分「合规审中 vs 支持方审中」. *

        *   NOT_SUBMITTED  未提交     执行方还没交
        *       ↓ (执行方提交)
      diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
      index 8a77ead..3818a1c 100644
      --- a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
      +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
      @@ -148,9 +148,10 @@ public class SysLoginService
                   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                   throw new UserPasswordNotMatchException();
               }
      -        // 用户名不在指定范围内 错误
      -        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
      -                || username.length() > UserConstants.USERNAME_MAX_LENGTH)
      +        // 用户名不在指定范围内 错误 (邮箱登录含 @ 不受 2-20 位限制)
      +        if (!username.contains("@")
      +                && (username.length() < UserConstants.USERNAME_MIN_LENGTH
      +                    || username.length() > UserConstants.USERNAME_MAX_LENGTH))
               {
                   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                   throw new UserPasswordNotMatchException();
      diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
      index 5dcdf90..682226f 100644
      --- a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
      +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
      @@ -38,6 +38,11 @@ public class UserDetailsServiceImpl implements UserDetailsService
           public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
           {
               SysUser user = userService.selectUserByUserName(username);
      +        // 用户名查不到且输入含 @ → 视为邮箱登录 (邮箱+密码)
      +        if (StringUtils.isNull(user) && username.contains("@"))
      +        {
      +            user = userService.selectUserByEmail(username);
      +        }
               if (StringUtils.isNull(user))
               {
                   log.info("登录用户:{} 不存在.", username);
      @@ -54,6 +59,17 @@ public class UserDetailsServiceImpl implements UserDetailsService
                   throw new ServiceException(MessageUtils.message("user.blocked"));
               }
       
      +        // 子账号专属: 主账号 (parent_user_id) 被禁用 → 拦截登录
      +        if (user.getParentUserId() != null)
      +        {
      +            SysUser parent = userService.selectUserById(user.getParentUserId());
      +            if (parent != null && UserStatus.DISABLE.getCode().equals(parent.getStatus()))
      +            {
      +                log.info("登录用户:{} 所属机构主账号已禁用.", username);
      +                throw new ServiceException("您所属的机构已禁用");
      +            }
      +        }
      +
               passwordService.validate(user);
       
               return createLoginUser(user);
      diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
      index 7196919..830d566 100644
      --- a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
      +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
      @@ -53,6 +53,14 @@ public interface SysUserMapper
            */
           public SysUser selectUserByUserName(String userName);
       
      +    /**
      +     * 通过邮箱查询用户 (登录支持邮箱+密码)
      +     *
      +     * @param email 邮箱
      +     * @return 用户对象信息
      +     */
      +    public SysUser selectUserByEmail(String email);
      +
           /**
            * 通过用户ID查询用户
            * 
      @@ -96,6 +104,22 @@ public interface SysUserMapper
           public int updateUserStatus(@Param("userId") Long userId, @Param("status") String status);
           public int updateRoleType(@Param("userId") Long userId, @Param("roleType") String roleType);
       
      +    /**
      +     * 更换机构管理员: 改账号类型 + 主账号指向
      +     * parentUserId 传 null = 晋升主账号 (account_type='MAIN', parent_user_id=NULL)
      +     *
      +     * @param userId       目标 sys_user.user_id
      +     * @param accountType  'MAIN' / 'SUB'
      +     * @param parentUserId 子账号指向的主账号 user_id (MAIN 传 null)
      +     */
      +    public int updateAccountType(@Param("userId") Long userId, @Param("accountType") String accountType, @Param("parentUserId") Long parentUserId);
      +
      +    /**
      +     * 把原管理员的所有子账号 re-point 到新管理员 (更换管理员时, 避免出现三层树)
      +     * 排除新管理员自己 (其 parent_user_id 当前仍指向原管理员)
      +     */
      +    public int updateParentUserId(@Param("oldParentUserId") Long oldParentUserId, @Param("newParentUserId") Long newParentUserId);
      +
           /**
            * 更新用户登录信息(IP和登录时间)
            *
      diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
      index da00a51..d0474dc 100644
      --- a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
      +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
      @@ -52,6 +52,14 @@ public interface ISysUserService
            */
           public SysUser selectUserByUserName(String userName);
       
      +    /**
      +     * 通过邮箱查询用户 (登录支持邮箱+密码)
      +     *
      +     * @param email 邮箱
      +     * @return 用户对象信息
      +     */
      +    public SysUser selectUserByEmail(String email);
      +
           /**
            * 通过用户ID查询用户
            * 
      diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
      index 63e0164..95b8b8d 100644
      --- a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
      +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
      @@ -131,6 +131,15 @@ public class SysUserServiceImpl implements ISysUserService
               return userMapper.selectUserByUserName(userName);
           }
       
      +    /**
      +     * 通过邮箱查询用户 (登录支持邮箱+密码)
      +     */
      +    @Override
      +    public SysUser selectUserByEmail(String email)
      +    {
      +        return userMapper.selectUserByEmail(email);
      +    }
      +
           /**
            * 通过用户ID查询用户
            * 
      diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
      index 4520aa9..b6348d3 100644
      --- a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
      +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
      @@ -170,6 +170,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       	    
       		where u.user_name = #{userName} and u.del_flag = '0'
       	
      +
      +	
       	
       	
      +              
                   
       
                   
      @@ -197,7 +197,7 @@ const showRoleModal = ref(false) const registerUserType = ref('') // 登录模式: 'password' | 'sms' -const loginMode = ref('sms') +const loginMode = ref('password') const smsForm = reactive({ phone: '', smsCode: '', uuid: '' }) const smsSmsCountdown = ref(0) let smsSmsTimer = null @@ -356,7 +356,7 @@ const roleHome = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', - executor: '/executor/overview', + executor: '/executor/submissions', sponsor: '/sponsor/home', } diff --git a/ry-vue3/src/views/auth/RegisterSponsor.vue b/ry-vue3/src/views/auth/RegisterSponsor.vue index f80fa77..921326f 100644 --- a/ry-vue3/src/views/auth/RegisterSponsor.vue +++ b/ry-vue3/src/views/auth/RegisterSponsor.vue @@ -16,17 +16,22 @@ - - - - - - - - - - - + + + @@ -86,10 +91,10 @@ + + diff --git a/ry-vue3/src/views/manager/Plans.vue b/ry-vue3/src/views/manager/Plans.vue index ec187b9..78b2d29 100644 --- a/ry-vue3/src/views/manager/Plans.vue +++ b/ry-vue3/src/views/manager/Plans.vue @@ -1,6 +1,6 @@ - - - - - - - - + + + + + + 通过 + 退回 + + + + + - - + + - - - - - - - - - @@ -147,6 +143,7 @@ @@ -334,4 +416,6 @@ onMounted(() => { readQueryFromRoute(); load() }) .stage-waiting { color: #ff4d4f; border-color: #fbc4c4; background: #fef0f0; } .stage-frozen { color: #909399; border-color: #dcdfe6; background: #f5f7fa; opacity: 0.6; } .stage-default { color: #606266; border-color: #dcdfe6; background: #fafafa; } +/* 提交剩余时间: 已冻结/逾期 → 红色 0小时 */ +.remain-danger { color: #ff4d4f; font-weight: 600; } \ No newline at end of file diff --git a/ry-vue3/src/views/portal/Publicity.vue b/ry-vue3/src/views/portal/Publicity.vue index ecc008a..a91f222 100644 --- a/ry-vue3/src/views/portal/Publicity.vue +++ b/ry-vue3/src/views/portal/Publicity.vue @@ -167,9 +167,12 @@ async function load() { const type = r.announcementType || '公示' // 根据 announcementType 选择对应 URL const urlMap = { - invitation: r.invitationUrl, - support: r.supportLetterUrl, - publish: r.publishUrl + invitation: r.invitationUrl, + support: r.supportLetterUrl, + supportLetter: r.supportLetterUrl, + publish: r.publishUrl, + notice: r.publishUrl, + schedule: r.scheduleUrl } return { annId: r.projectId, diff --git a/ry-vue3/src/views/portal/PublicityDetail.vue b/ry-vue3/src/views/portal/PublicityDetail.vue index 0100bbe..bc4b2db 100644 --- a/ry-vue3/src/views/portal/PublicityDetail.vue +++ b/ry-vue3/src/views/portal/PublicityDetail.vue @@ -90,20 +90,28 @@
      - +