diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml index 608bd98..1db5b3a 100644 --- a/ry-api/ruoyi-admin/src/main/resources/application.yml +++ b/ry-api/ruoyi-admin/src/main/resources/application.yml @@ -84,14 +84,6 @@ logging: org.springframework: debug com.ruoyi.business: debug -# 用户配置 -user: - password: - # 密码最大错误次数 - maxRetryCount: 5 - # 密码锁定时间(默认10分钟) - lockTime: 10 - # Spring配置 spring: # 资源信息 @@ -145,8 +137,8 @@ token: header: Authorization # 令牌密钥 secret: abcdefghijklmnopqrstuvwxyz - # 令牌有效期(默认30分钟) - expireTime: 30 + # 令牌有效期(4小时) + expireTime: 240 # MyBatis配置 mybatis: diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAdminUserController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAdminUserController.java new file mode 100644 index 0000000..e617f40 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAdminUserController.java @@ -0,0 +1,34 @@ +package com.ruoyi.business.controller; + +import org.springframework.beans.factory.annotation.Autowired; +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.RestController; +import com.ruoyi.business.domain.dto.AdminUserCreateBody; +import com.ruoyi.business.service.IBizAdminUserService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * admin/后台 新建用户 (按角色级联). + * POST /business/adminUser/create — 仅后台管理员可调. + */ +@RestController +@RequestMapping("/business/adminUser") +public class BizAdminUserController extends BaseController { + + @Autowired + private IBizAdminUserService bizAdminUserService; + + @PostMapping("/create") + public AjaxResult create(@RequestBody AdminUserCreateBody body) { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"admin".equals(roleType)) { + throw new ServiceException("只有后台管理员可新建用户"); + } + return success(bizAdminUserService.create(body)); + } +} 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 e361226..6c56842 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,7 +9,6 @@ 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; @@ -252,11 +251,10 @@ public class BizMeetingAttendeeController extends BaseController { /** * 下载"劳务协议"空目录模板 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"); @@ -266,24 +264,22 @@ public class BizMeetingAttendeeController extends BaseController { /** * 上传"劳务协议" zip, 解压后按 劳务协议/{序号}_{姓名}/ 目录匹配参会人, - * 上传 OSS 并回填 labor_protocol. 仅 admin/manager 可触发. + * 上传 OSS 并回填 labor_protocol. 不限角色可触发. */ @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 可触发. + * zip 内为 专家照片/{序号}_{姓名}/ 空目录. 不限角色可触发. */ @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"); @@ -293,22 +289,14 @@ public class BizMeetingAttendeeController extends BaseController { /** * 上传"专家照片" zip, 解压后按 专家照片/{序号}_{姓名}/ 目录匹配参会人, - * 上传 OSS 并逗号拼接回填 on_site_photos. 仅 admin/manager 可触发. + * 上传 OSS 并逗号拼接回填 on_site_photos. 不限角色可触发. */ @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 4ae0f1b..0985412 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 @@ -95,6 +95,10 @@ public class BizMeetingController extends BaseController { bizMeeting.getParams().put("executorUserId", uid); } } + // 合规人员(manager) 数据权限: 只看"本人创建的项目"下的会议 (project_id ∈ create_user_id = 自己的项目) + else if ("manager".equals(roleType)) { + bizMeeting.getParams().put("managerCreateUserId", uid); + } startPage(); List list = bizMeetingService.selectList(bizMeeting); return getDataTable(list); @@ -110,7 +114,7 @@ public class BizMeetingController extends BaseController { public AjaxResult add(@RequestBody BizMeeting bizMeeting) { // executor 建会限额: 执行机构人员 (MAIN/SUB 只要能看到项目) 都可建会, 但该项目的会议数不得超过分配给本公司的场次. // 场次是公司维度: SUB 执行人反查主账号 parent_user_id 聚合 (与项目列表 assigned_sessions 口径一致). - // 注意: 会议数按"该项目下全部未软删会议"计数 (biz_meeting 无执行方归属列, 无法区分是哪个执行方建的) — 见 memory [[ry-executor-staff-project-visibility]]. + // 会议数按"本执行方 (execution_unit_id)"计数, 不再按项目全量计数 — 多执行方分摊时各自独立. String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); if ("executor".equals(roleType)) { Long uid = SecurityUtils.getUserId(); @@ -123,8 +127,23 @@ public class BizMeetingController extends BaseController { if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) { aggUid = current.getParentUserId(); } + // 执行方归属 (单一可信源反查): 主账号 (或 SUB 执行人的主账号) → executor biz_org.org_id, 落 execution_unit_id + Long executionUnitId = bizOrgService.selectOrgIdByUserId(aggUid); + bizMeeting.setExecutionUnitId(executionUnitId); int assigned = bizProjectService.countAssignedSessions(projectId, aggUid); - int existing = bizMeetingService.countByProjectId(projectId); + // 期数校验: 期数不得超过分配给本公司的场次 + Long periodNo = bizMeeting.getPeriodNo(); + if (periodNo != null && periodNo > assigned) { + throw new ServiceException("期数不能超过分配给本公司的场次 (共 " + assigned + " 场)"); + } + // 相同期数冲突校验: 本机构已创建同项目、同期数的会议则报错 + if (periodNo != null) { + int dup = bizMeetingService.countByProjectIdExecutionUnitPeriod(projectId, executionUnitId, periodNo); + if (dup > 0) { + throw new ServiceException("本机构已创建第 " + periodNo + " 期会议, 请勿重复"); + } + } + int existing = bizMeetingService.countByProjectIdAndExecutionUnit(projectId, executionUnitId); if (existing >= assigned) { throw new ServiceException("本项目分配给本公司的场次为 " + assigned + " 场, 已建 " + existing + " 场, 已达上限"); } @@ -136,6 +155,17 @@ public class BizMeetingController extends BaseController { bizMeeting.setUpdateTime(new Date()); // 提交截止时间: 建会时按 end_time + 项目 submit_deadline_days 天 落库 (项目未设天数则为 null → 永不冻结) bizMeeting.setSubmitDeadline(computeSubmitDeadline(bizMeeting.getProjectId(), bizMeeting.getEndTime())); + // 总期数 = 项目设置的总期数 (biz_project.total_sessions); 期数(第几期)由前端 period_no 填, 不用 DB 默认 1 + // 项目形式从项目继承 (biz_meeting.project_form 此前从未写入, 导致会议列表"项目形式"列一直为空) + if (bizMeeting.getProjectId() != null) { + BizProject proj = bizProjectService.getById(bizMeeting.getProjectId()); + if (proj != null) { + if (proj.getTotalSessions() != null) { + bizMeeting.setTotalPeriods(proj.getTotalSessions()); + } + bizMeeting.setProjectForm(proj.getProjectForm()); + } + } int rows = bizMeetingService.insert(bizMeeting); Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds(); if (attendeeUserIds != null && attendeeUserIds.length > 0) { @@ -150,6 +180,13 @@ public class BizMeetingController extends BaseController { public AjaxResult edit(@RequestBody BizMeeting bizMeeting) { bizMeeting.setUpdateBy(SecurityUtils.getUsername()); bizMeeting.setUpdateTime(new Date()); + // 修改会议时项目形式也从项目继承 (与 add 一致, 避免 project_form 残留为空) + if (bizMeeting.getProjectId() != null) { + BizProject proj = bizProjectService.getById(bizMeeting.getProjectId()); + if (proj != null) { + bizMeeting.setProjectForm(proj.getProjectForm()); + } + } int rows = bizMeetingService.updateByPrimaryKey(bizMeeting); Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds(); if (attendeeUserIds != null && attendeeUserIds.length > 0) { @@ -376,10 +413,6 @@ public class BizMeetingController extends BaseController { m.setSettleTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - // 结算成功 → 触发项目金额重算 (全量 SUM 已结算会议, 幂等) - if (m.getProjectId() != null) { - bizProjectService.recomputeSettledAmounts(m.getProjectId()); - } appendAuditLog(m, "SETTLE", "APPROVED", "会议结算"); return success("SETTLED"); } 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 4175273..ee93c5d 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 @@ -124,6 +124,16 @@ public class BizPersonController extends BaseController { return toAjax(bizPersonService.changeOrgAdmin(bizPerson.getPersonId())); } + /** + * 重置人员登录密码 (默认 123456) + * body: { personId } — 按 personId 反查 sys_user, 密码重置为默认值 + */ + @Log(title = "重置人员密码", businessType = BusinessType.UPDATE) + @PutMapping("/resetPassword") + public AjaxResult resetPassword(@RequestBody BizPerson bizPerson) + { + return toAjax(bizPersonService.resetPassword(bizPerson.getPersonId())); + } @Log(title = "人员", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") public AjaxResult remove(@PathVariable String[] ids) 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 9a418db..abea150 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 @@ -97,6 +97,11 @@ public class BizProjectController extends BaseController @GetMapping("/list") public TableDataInfo list(BizProject bizProject) { + // 合规人员(manager): 只看本人创建的项目 (create_user_id = 当前用户); admin 不限制 + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if ("manager".equals(roleType)) { + bizProject.getParams().put("createUserId", SecurityUtils.getUserId()); + } startPage(); List list = bizProjectService.selectList(bizProject); return getDataTable(list); @@ -183,6 +188,26 @@ public class BizProjectController extends BaseController { return success(bizProjectService.getById(projectId)); } + + /** + * executor 建会页"总场次"口径: 分配给本执行方 (公司) 的场次, 而非项目总场次. + * GET /business/project/{projectId}/assignedSessions + * 返回 { assignedSessions: N } — MAIN 主账号按自己聚合; SUB 执行人反查主账号 (parent_user_id) 聚合. + */ + @GetMapping("/{projectId}/assignedSessions") + public AjaxResult assignedSessions(@PathVariable("projectId") Long projectId) + { + Long uid = SecurityUtils.getUserId(); + Long aggUid = uid; + SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid); + if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) { + aggUid = current.getParentUserId(); + } + int assigned = bizProjectService.countAssignedSessions(projectId, aggUid); + Map data = new HashMap<>(); + data.put("assignedSessions", assigned); + return success(data); + } @Log(title = "项目", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody BizProject bizProject) 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 4b6c00b..705414f 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 @@ -59,6 +59,8 @@ public class BizMeeting extends BaseEntity { private Date updateTime; /** 所属项目ID */ private Long projectId; + /** 执行方归属 (biz_org.org_id, org_type='executor') — 会议级费用/场次按执行方隔离的单一可信源 */ + private Long executionUnitId; /** 项目名称 */ private String projectName; /** 所属公司名称 (派生字段, 由 biz_project.sponsor_org_id JOIN biz_org.org_name 得出, 不落库) */ @@ -158,6 +160,8 @@ public class BizMeeting extends BaseEntity { public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } public Long getProjectId() { return projectId; } public void setProjectId(Long projectId) { this.projectId = projectId; } + public Long getExecutionUnitId() { return executionUnitId; } + public void setExecutionUnitId(Long executionUnitId) { this.executionUnitId = executionUnitId; } public String getProjectName() { return projectName; } public void setProjectName(String projectName) { this.projectName = projectName; } public String getOrgName() { return orgName; } @@ -165,6 +169,10 @@ public class BizMeeting extends BaseEntity { private String address; public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } + /** 备注 */ + private String remark; + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } public String getSupervisionOpinion() { return supervisionOpinion; } public void setSupervisionOpinion(String supervisionOpinion) { this.supervisionOpinion = supervisionOpinion; } public String getSupervisionBy() { return supervisionBy; } 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 6fe51ae..4dbee32 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 @@ -129,8 +129,6 @@ public class BizProject extends BaseEntity { /** 发布时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date publishTime; - /** 公告类型 (邀请函/支持函/通知/日程/公示) */ - private String announcementType; /** 开通截止时间 (date, 到期后由 OpenStatusScheduler 置 open_status=N) */ @JsonFormat(pattern = "yyyy-MM-dd") private Date openDeadline; @@ -241,8 +239,6 @@ public class BizProject extends BaseEntity { 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; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/dto/AdminUserCreateBody.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/dto/AdminUserCreateBody.java new file mode 100644 index 0000000..a7f8015 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/dto/AdminUserCreateBody.java @@ -0,0 +1,91 @@ +package com.ruoyi.business.domain.dto; + +/** + * admin/后台「新建用户」请求体 (按 roleType 级联, 各角色只填自己需要的字段). + *

+ * roleType 决定级联分支: + *

    + *
  • admin / manager: 纯 sys_user (userName/nickName/phonenumber/email/password)
  • + *
  • doctor: sys_user + biz_expert (workUnit/department/title/cert)
  • + *
  • executor / sponsor MAIN: sys_user + biz_org + biz_person (orgName/businessNature/contact)
  • + *
  • executor / sponsor SUB: sys_user + biz_person (orgId 选已有单位)
  • + *
+ */ +public class AdminUserCreateBody { + /** 业务角色 admin/manager/doctor/executor/sponsor */ + private String roleType; + /** 登录账号 (4-20 位字母/数字/下划线) */ + private String userName; + /** 姓名 (doctor/person 必填; MAIN 账号默认用联系人/单位名) */ + private String nickName; + /** 明文密码 (管理员手填, 后端加密) */ + private String password; + /** 手机号 */ + private String phonenumber; + /** 邮箱 */ + private String email; + /** 状态 '0' 启用 / '1' 停用 */ + private String status; + + // ===== doctor 专用 ===== + private String workUnit; + private String department; + private String title; + private String practiceCertUrl; + private String titleCertUrl; + + // ===== executor / sponsor 专用 ===== + /** MAIN=新建单位(主账号) / SUB=选已有单位(子账号) */ + private String accountType; + /** SUB: 所属单位 biz_org.org_id */ + private Long orgId; + /** MAIN: 单位名称 */ + private String orgName; + /** MAIN: 企业性质 */ + private String businessNature; + /** MAIN: 联系人 */ + private String contactName; + /** MAIN: 联系电话 */ + private String contactPhone; + /** person 职务 (SUB 用) */ + private String position; + + public String getRoleType() { return roleType; } + public void setRoleType(String roleType) { this.roleType = roleType; } + public String getUserName() { return userName; } + public void setUserName(String userName) { this.userName = userName; } + public String getNickName() { return nickName; } + public void setNickName(String nickName) { this.nickName = nickName; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public String getPhonenumber() { return phonenumber; } + public void setPhonenumber(String phonenumber) { this.phonenumber = phonenumber; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getWorkUnit() { return workUnit; } + public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getPracticeCertUrl() { return practiceCertUrl; } + public void setPracticeCertUrl(String practiceCertUrl) { this.practiceCertUrl = practiceCertUrl; } + public String getTitleCertUrl() { return titleCertUrl; } + public void setTitleCertUrl(String titleCertUrl) { this.titleCertUrl = titleCertUrl; } + public String getAccountType() { return accountType; } + public void setAccountType(String accountType) { this.accountType = accountType; } + public Long getOrgId() { return orgId; } + public void setOrgId(Long orgId) { this.orgId = orgId; } + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } + public String getBusinessNature() { return businessNature; } + public void setBusinessNature(String businessNature) { this.businessNature = businessNature; } + 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; } + 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/vo/BizMeetingAttendeeImportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java index d92f8e7..c436ed1 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 @@ -22,39 +22,39 @@ import com.ruoyi.common.annotation.Excel; public class BizMeetingAttendeeImportVo { /** 医生 (原"姓名") */ - @Excel(name = "医生", sort = 1) + @Excel(name = "医生##ysxm", sort = 1) private String name; /** 联系电话 (原"手机号", 必填, 按此查/建 sys_user) */ - @Excel(name = "联系电话", sort = 2) + @Excel(name = "联系电话##lxdh", sort = 2) private String phone; /** 医院 (原"工作单位") */ - @Excel(name = "医院", sort = 3) + @Excel(name = "医院##yy", sort = 3) private String workUnit; /** 科室 */ - @Excel(name = "科室", sort = 4) + @Excel(name = "科室##ks", sort = 4) private String department; /** 职称 */ - @Excel(name = "职称", sort = 5) + @Excel(name = "职称##zc", sort = 5) private String title; /** 身份证号 */ - @Excel(name = "身份证号", sort = 6) + @Excel(name = "身份证号##sfzh", sort = 6) private String idCard; /** 银行 */ - @Excel(name = "银行", sort = 7) + @Excel(name = "银行##yh", sort = 7) private String bankName; /** 银行卡号码 */ - @Excel(name = "银行卡号码", sort = 8) + @Excel(name = "银行卡号码##yhkh", sort = 8) private String bankCard; /** 开户行 */ - @Excel(name = "开户行", sort = 9) + @Excel(name = "开户行##khh", sort = 9) private String bankBranch; /** 角色 (原"劳务形式": 授课/主持/评审...) */ @@ -62,23 +62,23 @@ public class BizMeetingAttendeeImportVo { private String laborForm; /** 应发金额 (decimal, 元) */ - @Excel(name = "应发金额", sort = 11) + @Excel(name = "应发金额##yfje", sort = 11) private BigDecimal feePreTax; /** 个税税金 (decimal, 元) */ - @Excel(name = "个税税金", sort = 12) + @Excel(name = "个税税金##sj", sort = 12) private BigDecimal tax; /** 增值税及附加成本 (decimal, 元) */ - @Excel(name = "增值税及附加成本", sort = 13) + @Excel(name = "增值税及附加成本##zzsjfjcb", sort = 13) private BigDecimal vatAndSurcharge; /** 实发金额 (decimal, 元) */ - @Excel(name = "实发金额", sort = 14) + @Excel(name = "实发金额##sfje", sort = 14) private BigDecimal fee; /** 摘要 */ - @Excel(name = "摘要", sort = 15) + @Excel(name = "摘要##zy", sort = 15) private String summary; /** 账户名称(持卡人姓名) */ diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java index e9fb681..69c7be6 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java @@ -21,6 +21,10 @@ public interface BizMeetingMapper List selectIdListByProjectId(Long projectId); /** 建会限额用: 统计某项目下未软删的会议数 (executor 建会不得超过分配的场次) */ int countByProjectId(Long projectId); + /** 建会限额用 (执行方隔离): 统计某项目下、某执行方 (biz_meeting.execution_unit_id) 未软删的会议数 */ + int countByProjectIdAndExecutionUnit(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId); + /** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */ + int countByProjectIdExecutionUnitPeriod(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId, @Param("periodNo") Long periodNo); /** * 自动流转: start_time 已过 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 is_executed=1 并转 RUNNING. *

由 MeetingStageScheduler 每分钟触发. 事实 + current_stage 缓存一起写. @@ -31,11 +35,6 @@ public interface BizMeetingMapper *

由 MeetingStageScheduler 每分钟触发. */ int markFrozen(); - /** - * 自动流转: material/voucher 都 APPROVED 且 最晚审核时间已过 1 自然日 且 current_stage 仍为 SUPERVISION_APPROVED → AWAITING_SETTLEMENT. - *

由 MeetingStageScheduler 每分钟触发 (待结算的 24h 慢路径). - */ - int markSettlementReady(); /** * 费用汇总调度器用: 查 fee_calc_status=0 且未软删的会议 id 列表. */ 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 805f1cb..65e4256 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 @@ -29,11 +29,6 @@ public interface BizProjectMapper /** 提交权限用: 判断 user 是否该项目的执行方 (MAIN biz_project_assign 或 SUB biz_project_executor_assign), >0 即命中 */ int countExecutorOfProject(@org.apache.ibatis.annotations.Param("projectId") Long projectId, @org.apache.ibatis.annotations.Param("userId") Long userId); - /** - * 会议结算后重算项目金额: 按"所有已结算会议"全量 SUM 回写 paid_labor_amount / paid_meeting_amount, - * 并重算 available_amount = total_amount - manage_fee - 已支付劳务 - 已支付会务 (幂等, 无累计副作用). - */ - int recomputeSettledAmounts(@org.apache.ibatis.annotations.Param("projectId") Long projectId); /** 删除公告: 将 invitation_url / support_letter_url / publish_url 置 NULL */ int clearAnnouncement(@org.apache.ibatis.annotations.Param("projectId") Long projectId); /** 开通到期回收: 到期(open_deadline <= 今天)的 open_status='Y' 置回 'N' */ 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 index 4a7bf4f..dfb7f21 100644 --- 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 @@ -1,6 +1,8 @@ package com.ruoyi.business.oss; import java.io.ByteArrayInputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -59,18 +61,41 @@ public class OssZipService if (q >= 0) u = u.substring(0, q); String httpsHost = "https://" + ossConfMeta.getBucket() + "." + ossConfMeta.getEndpoint() + "/"; - if (u.startsWith(httpsHost)) return u.substring(httpsHost.length()); + if (u.startsWith(httpsHost)) return decodeKey(u.substring(httpsHost.length())); String httpHost = "http://" + ossConfMeta.getBucket() + "." + ossConfMeta.getEndpoint() + "/"; - if (u.startsWith(httpHost)) return u.substring(httpHost.length()); + if (u.startsWith(httpHost)) return decodeKey(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); + if (slash >= 0) return decodeKey(u.substring(slash + 1)); + } + return decodeKey(u); + } + + /** + * 还原前端 encodeURIComponent 编码的 key. + *

+ * 前端 uploadToOss() 用「明文中文」做 OSS key 上传, 但把 encodeURIComponent 后的 URL 存进 DB, + * 所以 DB 里的 URL 是编码过的 (如 2.6M-%E6%B5%8B%E8%AF%95_xx.pdf), + * 而 OSS 里真实 key 是明文 (2.6M-测试_xx.pdf). 不还原直接 copyObject 会 NoSuchKey. + *

+ * 旧数据若已是明文 URL, decode 对其幂等 (中文非 %XX 序列, 不会被改变), 无副作用. + */ + private String decodeKey(String key) + { + if (key == null) return null; + try + { + return URLDecoder.decode(key, StandardCharsets.UTF_8); + } + catch (Exception e) + { + // 非法 % 序列 / 已解码旧 key, 原样返回 + return key; } - return u; } /** OSS 服务端 copy (同 bucket 内), 用于把散落的材料文件复制到 staging 前缀, 不经过 Java 内存 */ 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 c1c3a06..73fb7a8 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 @@ -67,24 +67,4 @@ public class MeetingStageScheduler log.warn("[MeetingStageScheduler] 冻结异常 (跳过, 下分钟再试)", e); } } - - /** - * 每分钟: material+voucher 都 APPROVED 且最晚审核时间已过 1 自然日 → 待结算 (24h 慢路径). - */ - @Scheduled(fixedRate = 60_000, initialDelay = 30_000) - public void markSettlementReady() - { - try - { - int affected = meetingMapper.markSettlementReady(); - if (affected > 0) - { - log.info("[MeetingStageScheduler] 自动转待结算: 本次更新 {} 行", affected); - } - } - catch (Exception e) - { - log.warn("[MeetingStageScheduler] 转待结算异常 (跳过, 下分钟再试)", e); - } - } } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizAdminUserService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizAdminUserService.java new file mode 100644 index 0000000..670afef --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizAdminUserService.java @@ -0,0 +1,20 @@ +package com.ruoyi.business.service; + +import com.ruoyi.business.domain.dto.AdminUserCreateBody; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * admin/后台 新建用户 Service (按 roleType 级联) + */ +public interface IBizAdminUserService { + /** + * 按 roleType 级联创建用户: + * admin/manager → sys_user + * doctor → sys_user + biz_expert + * executor/sponsor MAIN → sys_user + biz_org + biz_person + * executor/sponsor SUB → sys_user + biz_person + * + * @return 新建的 SysUser (含 userId) + */ + SysUser create(AdminUserCreateBody body); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java index 817bdb1..be29a18 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java @@ -24,6 +24,10 @@ public interface IBizMeetingService void softDeleteCascadeBatch(Long[] meetingIds); /** 建会限额用: 统计某项目下未软删的会议数 (executor 建会不得超过分配的场次) */ int countByProjectId(Long projectId); + /** 建会限额用 (执行方隔离): 统计某项目下、某执行方未软删的会议数 */ + int countByProjectIdAndExecutionUnit(Long projectId, Long executionUnitId); + /** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */ + int countByProjectIdExecutionUnitPeriod(Long projectId, Long executionUnitId, Long periodNo); /** 标记会议费用待重算 (人员/材料变化触发, 幂等; 由 FeeCalcScheduler 汇总回写) */ void markFeeCalcPending(Long meetingId); } 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 0ca2700..59c00e4 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 @@ -28,4 +28,8 @@ public interface IBizPersonService * 原管理员由 MAIN 降为 SUB, 其余子账号 re-point 到新管理员, biz_org.user_id 同步指向新管理员 */ int changeOrgAdmin(String personId); + /** + * 重置人员登录密码: 按 personId 反查 sys_user, 密码重置为默认 123456 (与新建人员默认密码一致) + */ + int resetPassword(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 c1773fa..ec292fc 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 @@ -40,12 +40,6 @@ public interface IBizProjectService */ boolean isExecutorOfProject(Long projectId, Long userId); - /** - * 会议结算后重算项目金额: 按"所有已结算会议"全量 SUM 回写 paid_labor_amount / paid_meeting_amount, - * 并重算 available_amount. 幂等 (每次全量重算), 无累计副作用. - */ - void recomputeSettledAmounts(Long projectId); - /** 删除公告: 将 invitation_url / support_letter_url / publish_url 置 NULL (未发布) */ int clearAnnouncement(Long projectId); 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 769de54..51db81f 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 @@ -19,16 +19,6 @@ import com.ruoyi.business.domain.BizMeeting; @Component public class StageDeriver { - private static final long H24 = 24L * 3600 * 1000; - - /** 待结算: 材料审核通过 且 材料审核时间已超 24h (用户拍板口径) */ - private boolean settlementReady(BizMeeting m) - { - 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) { return v != null && v == 1; @@ -49,7 +39,7 @@ public class StageDeriver if ("REJECTED".equals(material)) return "RECTIFYING"; if ("APPROVED".equals(material)) { - return settlementReady(m) ? "AWAITING_SETTLEMENT" : "SUPERVISION_APPROVED"; + return "AWAITING_SETTLEMENT"; } if ("SUBMITTED".equals(material)) { @@ -81,10 +71,10 @@ public class StageDeriver return "executor".equals(role) ? "已退回" : "待整改"; } - // 材料已支持方通过 + // 材料已支持方通过 → 待结算 (支持方审通过即待结算, 不再有 24h 慢路径) if ("APPROVED".equals(material)) { - return settlementReady(m) ? "待结算" : "审核通过"; + return "待结算"; } // 材料在审 (SUBMITTED) diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizAdminUserServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizAdminUserServiceImpl.java new file mode 100644 index 0000000..f69cc68 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizAdminUserServiceImpl.java @@ -0,0 +1,222 @@ +package com.ruoyi.business.service.impl; + +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.domain.BizPerson; +import com.ruoyi.business.domain.dto.AdminUserCreateBody; +import com.ruoyi.business.mapper.BizExpertMapper; +import com.ruoyi.business.mapper.BizPersonMapper; +import com.ruoyi.business.service.IBizAdminUserService; +import com.ruoyi.business.service.IBizOrgService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.id.IdGenerator; +import com.ruoyi.common.utils.id.SnowflakeId; +import com.ruoyi.system.service.ISysUserService; + +/** + * admin/后台 新建用户 (按 roleType 级联, 一个事务). + * 角色单一可信源 = sys_user.role_type (biz_user_role_bind 已废). + */ +@Service +public class BizAdminUserServiceImpl implements IBizAdminUserService { + + private static final Set ROLE_WHITELIST = Set.of("admin", "manager", "doctor", "executor", "sponsor"); + + @Autowired + private ISysUserService sysUserService; + @Autowired + private IBizOrgService bizOrgService; + @Autowired + private BizPersonMapper bizPersonMapper; + @Autowired + private BizExpertMapper bizExpertMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public SysUser create(AdminUserCreateBody b) { + String role = b.getRoleType(); + if (role == null || !ROLE_WHITELIST.contains(role)) { + throw new ServiceException("非法角色"); + } + String userName = b.getUserName(); + if (userName == null || !userName.matches("^[A-Za-z0-9_]{4,20}$")) { + throw new ServiceException("登录账号需 4-20 位字母/数字/下划线"); + } + String password = b.getPassword(); + if (password == null || password.length() < 6 || password.length() > 20) { + throw new ServiceException("密码长度 6-20 位"); + } + + // 查重 (username / phone / email) + SysUser dup = new SysUser(); + dup.setUserName(userName); + if (!sysUserService.checkUserNameUnique(dup)) { + throw new ServiceException("登录账号已存在"); + } + if (b.getPhonenumber() != null && !b.getPhonenumber().isEmpty()) { + SysUser p = new SysUser(); + p.setPhonenumber(b.getPhonenumber()); + if (!sysUserService.checkPhoneUnique(p)) { + throw new ServiceException("手机号已存在"); + } + } + if (b.getEmail() != null && !b.getEmail().isEmpty()) { + SysUser e = new SysUser(); + e.setEmail(b.getEmail()); + if (!sysUserService.checkEmailUnique(e)) { + throw new ServiceException("邮箱已存在"); + } + } + + String encPwd = SecurityUtils.encryptPassword(password); + + switch (role) { + case "admin": + case "manager": + return insertPlain(b, role, encPwd); + case "doctor": + return insertDoctor(b, encPwd); + case "executor": + case "sponsor": + if ("SUB".equals(b.getAccountType())) { + return insertSub(b, role, encPwd); + } + return insertMain(b, role, encPwd); + default: + throw new ServiceException("非法角色"); + } + } + + /** 通用 sys_user 基础字段 */ + private SysUser baseUser(AdminUserCreateBody b, String role, String encPwd) { + SysUser u = new SysUser(); + u.setUserName(b.getUserName()); + u.setNickName(b.getNickName()); + u.setPhonenumber(b.getPhonenumber()); + u.setEmail(b.getEmail()); + u.setPassword(encPwd); + u.setRoleType(role); + u.setStatus(b.getStatus() == null ? "0" : b.getStatus()); + u.setDelFlag("0"); + u.setCreateBy(SecurityUtils.getUsername()); + return u; + } + + /** admin / manager: 纯 sys_user */ + private SysUser insertPlain(AdminUserCreateBody b, String role, String encPwd) { + SysUser u = baseUser(b, role, encPwd); + sysUserService.insertUser(u); + return u; + } + + /** doctor: sys_user + biz_expert (admin 直接建档, audit_status=通过) */ + private SysUser insertDoctor(AdminUserCreateBody b, String encPwd) { + if (b.getNickName() == null || b.getNickName().isEmpty()) { + throw new ServiceException("姓名不能为空"); + } + SysUser u = baseUser(b, "doctor", encPwd); + sysUserService.insertUser(u); + + BizExpert ex = new BizExpert(); + ex.setExpertId(IdGenerator.generateId()); + ex.setUserId(u.getUserId()); + ex.setName(b.getNickName()); + ex.setPhone(b.getPhonenumber()); + ex.setWorkUnit(b.getWorkUnit()); + ex.setDepartment(b.getDepartment()); + ex.setTitle(b.getTitle()); + ex.setPracticeCertUrl(b.getPracticeCertUrl()); + ex.setTitleCertUrl(b.getTitleCertUrl()); + ex.setAuditStatus("2"); // 通过 + ex.setStatus("Y"); + ex.setCreateBy(SecurityUtils.getUsername()); + bizExpertMapper.insert(ex); + return u; + } + + /** executor/sponsor MAIN: sys_user(MAIN) + biz_org + biz_person(自己=管理员) */ + private SysUser insertMain(AdminUserCreateBody b, String role, String encPwd) { + String orgName = b.getOrgName(); + if (orgName == null || orgName.isEmpty()) { + throw new ServiceException("单位名称不能为空"); + } + String contactName = b.getContactName() != null ? b.getContactName() : orgName; + String contactPhone = b.getContactPhone() != null ? b.getContactPhone() : b.getPhonenumber(); + + SysUser u = baseUser(b, role, encPwd); + u.setAccountType("MAIN"); + u.setPhonenumber(contactPhone); + if (u.getNickName() == null || u.getNickName().isEmpty()) { + u.setNickName(contactName); + } + sysUserService.insertUser(u); + + BizOrg org = new BizOrg(); + org.setUserId(u.getUserId()); + org.setOrgName(orgName); + org.setOrgType(role); + org.setBusinessNature(b.getBusinessNature()); + org.setContactName(contactName); + org.setContactPhone(contactPhone); + org.setStatus("0"); + org.setCreateBy(SecurityUtils.getUsername()); + bizOrgService.insert(org); + + BizPerson self = new BizPerson(); + SnowflakeId.injectIfEmpty(self, "personId"); + self.setName(contactName); + self.setPhone(contactPhone); + self.setOrgId(org.getOrgId()); + self.setDepartment("管理部"); + self.setPosition("管理员"); + self.setUnitType(role); + self.setUserId(u.getUserId()); + self.setCreateBy(SecurityUtils.getUsername()); + self.setUpdateBy(SecurityUtils.getUsername()); + bizPersonMapper.insert(self); + return u; + } + + /** executor/sponsor SUB: sys_user(SUB, parent=主账号) + biz_person */ + private SysUser insertSub(AdminUserCreateBody b, String role, String encPwd) { + if (b.getOrgId() == null) { + throw new ServiceException("请选择所属单位"); + } + BizOrg org = bizOrgService.getById(b.getOrgId()); + if (org == null) { + throw new ServiceException("所属单位不存在"); + } + if (!role.equals(org.getOrgType())) { + throw new ServiceException("所选单位类型与角色不匹配"); + } + if (b.getNickName() == null || b.getNickName().isEmpty()) { + throw new ServiceException("姓名不能为空"); + } + + SysUser u = baseUser(b, role, encPwd); + u.setAccountType("SUB"); + u.setParentUserId(org.getUserId()); // 主账号 user_id, 可为 null (单位暂无主账号时留空待分配) + sysUserService.insertUser(u); + + BizPerson p = new BizPerson(); + SnowflakeId.injectIfEmpty(p, "personId"); + p.setName(b.getNickName()); + p.setPhone(b.getPhonenumber()); + p.setEmail(b.getEmail()); + p.setOrgId(b.getOrgId()); + p.setDepartment(b.getDepartment()); + p.setPosition(b.getPosition()); + p.setUnitType(role); + p.setUserId(u.getUserId()); + p.setCreateBy(SecurityUtils.getUsername()); + p.setUpdateBy(SecurityUtils.getUsername()); + bizPersonMapper.insert(p); + return u; + } +} 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 82a0d7d..b88b810 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 @@ -564,11 +564,16 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService return buildTemplateZip(meetingId, EXPERT_PHOTO_ZIP_ROOT); } - /** 生成空目录模板 zip: {rootDir}/{序号}_{姓名}/, 每个参会人一个空目录 */ + /** 生成空目录模板 zip: 根目录 README.txt + {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)) { + // 根目录 README.txt: 保证 zip 里至少有一个真实文件, 否则纯空目录的 zip 部分工具无法解压 + zos.putNextEntry(new ZipEntry("README.txt")); + zos.write("请把文件放入指定目录后,压缩成zip包并上传".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + for (int i = 0; i < list.size(); i++) { BizMeetingAttendee a = list.get(i); // 空目录 entry (以 / 结尾), 序号 = 列表 1 起下标, 与前端表格 type="index" 一致 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 05fb66c..476a5bc 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 @@ -1,5 +1,7 @@ package com.ruoyi.business.service.impl; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -13,6 +15,7 @@ import com.ruoyi.business.mapper.BizMeetingMaterialMapper; import com.ruoyi.business.mapper.BizMeetingAuditLogMapper; import com.ruoyi.business.service.IBizMeetingService; import com.ruoyi.common.enums.BizMeetingStageEnum; +import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.utils.id.IdGenerator; @Service @@ -30,6 +33,11 @@ public class BizMeetingServiceImpl implements IBizMeetingService private BizMeetingMaterialMapper materialMapper; @Autowired private BizMeetingAuditLogMapper auditLogMapper; + @Autowired + private RedisCache redisCache; + + /** 会议ID (meeting_id) 每开始日期的序列号 Redis key 前缀: biz:meeting:id:{yyMMdd} */ + private static final String MEETING_ID_SEQ_KEY_PREFIX = "biz:meeting:id:"; @Override public BizMeeting getById(Long meetingId) @@ -39,8 +47,11 @@ public class BizMeetingServiceImpl implements IBizMeetingService { return bizMeetingMapper.selectList(entity); } @Override public int insert(BizMeeting entity) { - // meetingId 走 DB AUTO_INCREMENT (BizProject 同模式: SnowflakeId.injectIfEmpty 对 Long setter 会 NoSuchMethodException 被吞掉, 无副作用); - // businessId DDL NOT NULL UNIQUE, 前端无字段, 兜底用雪花 ID 字符串 (跟 meetingId 同源, 保证唯一) + // meetingId: 从 DB AUTO_INCREMENT 改为应用赋值 — 10 位数字会议ID = 开始日期(yyMMdd) + 4 位序列号(0001 起, 每开始日期 Redis 独立计数) + if (entity.getMeetingId() == null) { + entity.setMeetingId(generateMeetingId(entity.getStartTime())); + } + // businessId DDL NOT NULL UNIQUE, 前端无字段, 兜底用雪花 ID 字符串 (保持原逻辑不变) if (entity.getBusinessId() == null || entity.getBusinessId().isEmpty()) { entity.setBusinessId(String.valueOf(IdGenerator.generateId())); } @@ -55,6 +66,20 @@ public class BizMeetingServiceImpl implements IBizMeetingService } return bizMeetingMapper.insert(entity); } + + /** + * 生成 10 位数字会议ID = 开始日期 yyMMdd + 4 位序列号. + * 序列号存 Redis (key = biz:meeting:id:{yyMMdd}), INCR 原子自增, 无需加锁; + * 数据量小, 单日超过 9999 的场景不考虑 (String.format %04d 溢出会变 5 位, 仍唯一). + */ + private Long generateMeetingId(Date startTime) { + Date d = (startTime != null) ? startTime : new Date(); + String yyMMdd = new SimpleDateFormat("yyMMdd").format(d); + Long seq = redisCache.redisTemplate.opsForValue().increment(MEETING_ID_SEQ_KEY_PREFIX + yyMMdd); + long seqVal = (seq == null) ? 1L : seq; + return Long.parseLong(yyMMdd + String.format("%04d", seqVal)); + } + @Override public int updateByPrimaryKey(BizMeeting entity) { return bizMeetingMapper.updateByPrimaryKey(entity); } @@ -94,6 +119,14 @@ public class BizMeetingServiceImpl implements IBizMeetingService public int countByProjectId(Long projectId) { return bizMeetingMapper.countByProjectId(projectId); } + @Override + public int countByProjectIdAndExecutionUnit(Long projectId, Long executionUnitId) + { return bizMeetingMapper.countByProjectIdAndExecutionUnit(projectId, executionUnitId); } + + @Override + public int countByProjectIdExecutionUnitPeriod(Long projectId, Long executionUnitId, Long periodNo) + { return bizMeetingMapper.countByProjectIdExecutionUnitPeriod(projectId, executionUnitId, periodNo); } + @Override public void markFeeCalcPending(Long meetingId) { if (meetingId != null) { 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 f4c9b73..8658ab1 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 @@ -141,6 +141,19 @@ public class BizPersonServiceImpl implements IBizPersonService return n; } + /** + * 重置人员登录密码为默认 123456 (与新建人员默认密码一致) + * 走 sys_user.resetUserPwd 顺带刷新 pwd_update_date, 不走 updateUser 的动态 set (语义更精准) + */ + @Override + public int resetPassword(String personId) { + BizPerson p = bizPersonMapper.selectByPrimaryKey(personId); + if (p == null || p.getUserId() == null) { + throw new ServiceException("人员不存在或未关联登录账号"); + } + return sysUserMapper.resetUserPwd(p.getUserId(), SecurityUtils.encryptPassword("123456")); + } + @Override public int deleteByPrimaryKeys(String[] personIds) { 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 2d4044b..f0e9f4c 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 @@ -132,13 +132,6 @@ public class BizProjectServiceImpl implements IBizProjectService public boolean isExecutorOfProject(Long projectId, Long userId) { return bizProjectMapper.countExecutorOfProject(projectId, userId) > 0; } - /** 会议结算后重算项目金额 (全量 SUM 已结算会议, 幂等) */ - @Override - public void recomputeSettledAmounts(Long projectId) { - if (projectId == null) return; - bizProjectMapper.recomputeSettledAmounts(projectId); - } - /** 删除公告: 将 3 个公示 URL 置 NULL (未发布) */ @Override public int clearAnnouncement(Long projectId) { diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml index 233819d..42c6deb 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml @@ -41,10 +41,13 @@ 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 1199fa6..2cef332 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 @@ -5,6 +5,7 @@ + @@ -15,6 +16,7 @@ + @@ -49,7 +51,7 @@ - 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 + meeting_id, business_id, project_id, execution_unit_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, address, remark, 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 @@ -110,6 +116,7 @@ meeting_id, business_id, project_id, + execution_unit_id, project_no, project_name, meeting_name, @@ -119,6 +126,7 @@ start_time, end_time, address, + remark, current_stage, supervision_opinion, supervision_by, @@ -138,6 +146,7 @@ #{meetingId}, #{businessId}, #{projectId}, + #{executionUnitId}, #{projectNo}, #{projectName}, #{meetingName}, @@ -147,6 +156,7 @@ #{startTime}, #{endTime}, #{address}, + #{remark}, #{currentStage}, #{supervisionOpinion}, #{supervisionBy}, @@ -168,6 +178,7 @@ business_id = #{businessId}, project_id = #{projectId}, + execution_unit_id = #{executionUnitId}, project_no = #{projectNo}, project_name = #{projectName}, meeting_name = #{meetingName}, @@ -178,6 +189,7 @@ start_time = #{startTime}, end_time = #{endTime}, address = #{address}, + remark = #{remark}, current_stage = #{currentStage}, supervision_opinion = #{supervisionOpinion}, supervision_by = #{supervisionBy}, @@ -224,6 +236,17 @@ + + + + update biz_meeting @@ -250,19 +273,6 @@ and submit_deadline is not null and submit_deadline <= NOW() - - - update biz_meeting - set current_stage = 'AWAITING_SETTLEMENT' - where is_deleted = 0 - and is_frozen = 0 - and is_settled = 0 - and is_finished = 0 - and material_audit_stage = 'APPROVED' - and current_stage = 'SUPERVISION_APPROVED' - and material_audit_time is not null - and material_audit_time <= (NOW() - INTERVAL 1 DAY) - - select distinct 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, + select distinct p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, 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, @@ -168,7 +182,37 @@ where bpa4.project_id = p.project_id and bpa4.is_deleted = 0 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 + (select ifnull(sum(m.labor_fee), 0) + from biz_meeting m + where m.project_id = p.project_id + and m.is_settled = 1 + and m.is_deleted = 0 + and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as paid_labor_amount, + (select ifnull(sum(m.meeting_fee), 0) + from biz_meeting m + where m.project_id = p.project_id + and m.is_settled = 1 + and m.is_deleted = 0 + and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as paid_meeting_amount, + (select coalesce(sum(bpa5.amount), 0) + from biz_project_assign bpa5 + where bpa5.project_id = p.project_id + and bpa5.is_deleted = 0 + and bpa5.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) + - (select ifnull(sum(m.labor_fee), 0) + from biz_meeting m + where m.project_id = p.project_id + and m.is_settled = 1 + and m.is_deleted = 0 + and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) + - (select ifnull(sum(m.meeting_fee), 0) + from biz_meeting m + where m.project_id = p.project_id + and m.is_settled = 1 + and m.is_deleted = 0 + and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as available_amount, + (select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0 + and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as meeting_count from biz_project p join biz_project_assign a on a.project_id = p.project_id @@ -194,9 +238,10 @@ (主账号把执行人派到项目上后, 执行人登录看自己被派到的项目) 严格隔离: 不 JOIN biz_project_assign (避免主账号过滤), 不 UNION intent (executor 与 biz_*_intent 完全无关) 场次/金额列 assigned_sessions/assigned_amount 按 params.executorUserId (主账号/本公司) 聚合 — 执行人看到的是公司数据, 不是个人数据 + 已支付劳务/会务/可用金额同样按 biz_meeting.execution_unit_id = 本公司 org 过滤 (执行方级). --> - - - update biz_project p - set p.paid_labor_amount = ( - select ifnull(sum(m.labor_fee), 0) - from biz_meeting m - where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0 - ), - p.paid_meeting_amount = ( - select ifnull(sum(m.meeting_fee), 0) - from biz_meeting m - where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0 - ), - p.available_amount = p.total_amount - ifnull(p.manage_fee, 0) - - (select ifnull(sum(m.labor_fee), 0) - from biz_meeting m - where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) - - (select ifnull(sum(m.meeting_fee), 0) - from biz_meeting m - where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) - where p.project_id = #{projectId} - \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java index e1789f8..a781740 100644 --- a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java @@ -11,7 +11,6 @@ import com.alibaba.fastjson2.JSON; import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.ServletUtils; -import com.ruoyi.common.utils.StringUtils; /** * 认证失败处理类 返回未授权 @@ -28,7 +27,7 @@ public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, S throws IOException { int code = HttpStatus.UNAUTHORIZED; - String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + String msg = "认证失败,请重新登陆"; ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); } } diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java index 6728c7b..977b040 100644 --- a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java @@ -1,15 +1,12 @@ package com.ruoyi.framework.web.service; -import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.exception.user.UserPasswordNotMatchException; -import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.framework.security.context.AuthenticationContextHolder; @@ -24,12 +21,6 @@ public class SysPasswordService @Autowired private RedisCache redisCache; - @Value(value = "${user.password.maxRetryCount}") - private int maxRetryCount; - - @Value(value = "${user.password.lockTime}") - private int lockTime; - /** * 登录账户密码错误次数缓存键名 * @@ -44,31 +35,13 @@ public class SysPasswordService public void validate(SysUser user) { Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); - String username = usernamePasswordAuthenticationToken.getName(); String password = usernamePasswordAuthenticationToken.getCredentials().toString(); - Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); - - if (retryCount == null) - { - retryCount = 0; - } - - if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) - { - throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); - } - + // 去掉多次登录失败锁定: 只校验密码, 错误直接抛, 不再计数/锁定 if (!matches(user, password)) { - retryCount = retryCount + 1; - redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); throw new UserPasswordNotMatchException(); } - else - { - clearLoginRecordCache(username); - } } public boolean matches(SysUser user, String rawPassword) diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserExtendVo.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserExtendVo.java index 5126a70..479a5f6 100644 --- a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserExtendVo.java +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserExtendVo.java @@ -16,6 +16,9 @@ public class SysUserExtendVo extends SysUser /** 业务角色 (admin / manager / doctor / executor / sponsor) */ private String roleType; + /** 单位名称 (MAIN 走 biz_org.user_id, SUB 走 biz_person.org_id 反查, 仅 list 展示) */ + private String orgName; + public String getRoleType() { return roleType; @@ -25,4 +28,14 @@ public class SysUserExtendVo extends SysUser { this.roleType = roleType; } + + public String getOrgName() + { + return orgName; + } + + public void setOrgName(String orgName) + { + this.orgName = orgName; + } } 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 b6348d3..1da3337 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 @@ -53,6 +53,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -99,7 +100,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" -->