feat: 会议ID应用赋值 + 会议表单/详情优化 + admin工作台统计与用户管理

- meetingId 由 DB 自增改为应用赋值 10 位数字 (yyMMdd + 4 位序列, Redis 按日期计数)
- 会议开始时间不能晚于结束时间校验; 参会人表单身份证附件/角色单独一行
- 会议详情左右列比例 7:3 -> 8:2 (材料审核列收窄)
- admin/workbench 用户总数/角色类型数/各角色分布统计修复 (改用 listByRole 按 role_type 过滤)
- 新增 admin 用户管理接口 + 门户页脚 + H5 签到/上传优化
This commit is contained in:
郭庆泰
2026-08-25 23:33:14 +08:00
parent 591a43fe61
commit ea2024d083
66 changed files with 1454 additions and 1228 deletions
@@ -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:
@@ -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));
}
}
@@ -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("只有管理员或合规经理可操作");
}
}
}
@@ -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<BizMeeting> 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");
}
@@ -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)
@@ -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<BizProject> 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<String, Object> data = new HashMap<>();
data.put("assignedSessions", assigned);
return success(data);
}
@Log(title = "项目", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BizProject bizProject)
@@ -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; }
@@ -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; }
@@ -0,0 +1,91 @@
package com.ruoyi.business.domain.dto;
/**
* admin/后台「新建用户」请求体 (按 roleType 级联, 各角色只填自己需要的字段).
* <p>
* roleType 决定级联分支:
* <ul>
* <li>admin / manager: 纯 sys_user (userName/nickName/phonenumber/email/password)</li>
* <li>doctor: sys_user + biz_expert (workUnit/department/title/cert)</li>
* <li>executor / sponsor MAIN: sys_user + biz_org + biz_person (orgName/businessNature/contact)</li>
* <li>executor / sponsor SUB: sys_user + biz_person (orgId 选已有单位)</li>
* </ul>
*/
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; }
}
@@ -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;
/** 账户名称(持卡人姓名) */
@@ -21,6 +21,10 @@ public interface BizMeetingMapper
List<Long> 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.
* <p>由 MeetingStageScheduler 每分钟触发. 事实 + current_stage 缓存一起写.
@@ -31,11 +35,6 @@ public interface BizMeetingMapper
* <p>由 MeetingStageScheduler 每分钟触发.
*/
int markFrozen();
/**
* 自动流转: material/voucher 都 APPROVED 且 最晚审核时间已过 1 自然日 且 current_stage 仍为 SUPERVISION_APPROVED → AWAITING_SETTLEMENT.
* <p>由 MeetingStageScheduler 每分钟触发 (待结算的 24h 慢路径).
*/
int markSettlementReady();
/**
* 费用汇总调度器用: 查 fee_calc_status=0 且未软删的会议 id 列表.
*/
@@ -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' */
@@ -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.
* <p>
* 前端 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.
* <p>
* 旧数据若已是明文 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 内存 */
@@ -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);
}
}
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
@@ -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)
@@ -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<String> 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;
}
}
@@ -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<BizMeetingAttendee> 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" 一致
@@ -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) {
@@ -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)
{
@@ -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) {
@@ -41,10 +41,13 @@
<select id="selectList" resultMap="BizExpertResult" parameterType="BizExpert">
<include refid="selectFields"/>
<where>
<if test="name != null and name != ''"> and name = #{name}</if>
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
<if test="phone != null and phone != ''"> and phone = #{phone}</if>
<if test="workUnit != null and workUnit != ''"> and work_unit like concat('%', #{workUnit}, '%')</if>
<if test="department != null and department != ''"> and department = #{department}</if>
<if test="title != null and title != ''"> and title = #{title}</if>
<if test="auditStatus != null and auditStatus != ''"> and audit_status = #{auditStatus}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
order by expert_id desc
</select>
@@ -5,6 +5,7 @@
<id property="meetingId" column="meeting_id" />
<result property="businessId" column="business_id" />
<result property="projectId" column="project_id" />
<result property="executionUnitId" column="execution_unit_id" />
<result property="projectNo" column="project_no" />
<result property="projectName" column="project_name" />
<result property="meetingName" column="meeting_name" />
@@ -15,6 +16,7 @@
<result property="endTime" column="end_time" />
<result property="orgName" column="org_name" />
<result property="address" column="address" />
<result property="remark" column="remark" />
<result property="currentStage" column="current_stage" />
<result property="supervisionOpinion" column="supervision_opinion" />
<result property="supervisionBy" column="supervision_by" />
@@ -49,7 +51,7 @@
<result property="isDeleted" column="is_deleted" />
</resultMap>
<sql id="selectFields">
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
</sql>
<select id="selectByPrimaryKey" resultMap="BizMeetingResult" parameterType="Long">
select
@@ -101,6 +103,10 @@
select distinct a.project_id from biz_project_executor_assign a
where a.is_deleted = 0 and a.staff_user_id = #{params.executorStaffUserId}
)</if>
<!-- 合规人员(manager) 数据权限: 只看本人创建项目的会议 -->
<if test="params.managerCreateUserId != null">and project_id in (
select project_id from biz_project where create_user_id = #{params.managerCreateUserId} and is_deleted = 0
)</if>
</where>
order by meeting_id desc
</select>
@@ -110,6 +116,7 @@
<if test="meetingId != null">meeting_id,</if>
<if test="businessId != null and businessId != ''">business_id,</if>
<if test="projectId != null">project_id,</if>
<if test="executionUnitId != null">execution_unit_id,</if>
<if test="projectNo != null and projectNo != ''">project_no,</if>
<if test="projectName != null and projectName != ''">project_name,</if>
<if test="meetingName != null and meetingName != ''">meeting_name,</if>
@@ -119,6 +126,7 @@
<if test="startTime != null">start_time,</if>
<if test="endTime != null">end_time,</if>
<if test="address != null and address != ''">address,</if>
<if test="remark != null and remark != ''">remark,</if>
<if test="currentStage != null and currentStage != ''">current_stage,</if>
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion,</if>
<if test="supervisionBy != null and supervisionBy != ''">supervision_by,</if>
@@ -138,6 +146,7 @@
<if test="meetingId != null">#{meetingId},</if>
<if test="businessId != null and businessId != ''">#{businessId},</if>
<if test="projectId != null">#{projectId},</if>
<if test="executionUnitId != null">#{executionUnitId},</if>
<if test="projectNo != null and projectNo != ''">#{projectNo},</if>
<if test="projectName != null and projectName != ''">#{projectName},</if>
<if test="meetingName != null and meetingName != ''">#{meetingName},</if>
@@ -147,6 +156,7 @@
<if test="startTime != null">#{startTime},</if>
<if test="endTime != null">#{endTime},</if>
<if test="address != null and address != ''">#{address},</if>
<if test="remark != null and remark != ''">#{remark},</if>
<if test="currentStage != null and currentStage != ''">#{currentStage},</if>
<if test="supervisionOpinion != null and supervisionOpinion != ''">#{supervisionOpinion},</if>
<if test="supervisionBy != null and supervisionBy != ''">#{supervisionBy},</if>
@@ -168,6 +178,7 @@
<trim prefix="SET" suffixOverrides=",">
<if test="businessId != null and businessId != ''">business_id = #{businessId},</if>
<if test="projectId != null">project_id = #{projectId},</if>
<if test="executionUnitId != null">execution_unit_id = #{executionUnitId},</if>
<if test="projectNo != null and projectNo != ''">project_no = #{projectNo},</if>
<if test="projectName != null and projectName != ''">project_name = #{projectName},</if>
<if test="meetingName != null and meetingName != ''">meeting_name = #{meetingName},</if>
@@ -178,6 +189,7 @@
<if test="startTime != null">start_time = #{startTime},</if>
<if test="endTime != null">end_time = #{endTime},</if>
<if test="address != null and address != ''">address = #{address},</if>
<if test="remark != null and remark != ''">remark = #{remark},</if>
<if test="currentStage != null and currentStage != ''">current_stage = #{currentStage},</if>
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion = #{supervisionOpinion},</if>
<if test="supervisionBy != null and supervisionBy != ''">supervision_by = #{supervisionBy},</if>
@@ -224,6 +236,17 @@
<select id="countByProjectId" resultType="int" parameterType="Long">
select count(*) from biz_meeting where project_id = #{projectId} and is_deleted = 0
</select>
<!-- 建会限额用 (执行方隔离): 某项目下、某执行方未软删的会议数 -->
<select id="countByProjectIdAndExecutionUnit" resultType="int">
select count(*) from biz_meeting where project_id = #{projectId} and is_deleted = 0
and execution_unit_id = #{executionUnitId}
</select>
<!-- 建会校验用 (执行方隔离): 某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) -->
<select id="countByProjectIdExecutionUnitPeriod" resultType="int">
select count(*) from biz_meeting where project_id = #{projectId} and is_deleted = 0
and execution_unit_id = #{executionUnitId}
and period_no = #{periodNo}
</select>
<!-- 自动流转 (MeetingStageScheduler 每分钟调): start_time 已过 且 material 未提交 且未执行的会议 → is_executed=1 + RUNNING. 已软删/已冻结/已提交材料的不动. -->
<update id="markExecuted">
update biz_meeting
@@ -250,19 +273,6 @@
and submit_deadline is not null
and submit_deadline &lt;= NOW()
</update>
<!-- 自动流转 (MeetingStageScheduler 每分钟调): 材料 APPROVED 且 材料审核时间已过 1 自然日 → AWAITING_SETTLEMENT (待结算 24h 慢路径) -->
<update id="markSettlementReady">
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 &lt;= (NOW() - INTERVAL 1 DAY)
</update>
<!-- 费用汇总调度器: 查 fee_calc_status=0 且未软删的会议 id -->
<select id="selectPendingFeeCalcIds" resultType="Long">
select meeting_id from biz_meeting where fee_calc_status = 0 and is_deleted = 0
@@ -27,6 +27,7 @@
<result property="projectForm" column="project_form" />
<result property="isFinished" column="is_finished" />
<result property="isSettled" column="is_settled" />
<result property="isPublished" column="is_published" />
<result property="createBy" column="create_by" />
<result property="createUserId" column="create_user_id" />
<result property="createUserName" column="create_user_name" />
@@ -49,7 +50,13 @@
<result property="isDeleted" column="is_deleted" />
</resultMap>
<sql id="selectFields">
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,
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_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) 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) as paid_meeting_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)) as available_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, 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,
@@ -68,8 +75,13 @@
<!-- sponsor 端专属查询: 与 selectFields 等价 (sponsor 评分改走 biz_project_rating 子表, 这里只查项目本体) -->
<sql id="selectFieldsForSponsor">
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.total_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) 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) as paid_meeting_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)) as available_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,
@@ -147,9 +159,11 @@
1) a.exec_user_id = 当前 user_id (子账号直接被指派)
2) a.execution_unit_id = 当前 user_id 对应的 executor biz_org.org_id (主账号整公司被指派)
注: executor 角色无评分/聚合分需求, 复用 selectFields (含 sponsor_score / manager_score)
金额列口径 (执行方级): 总金额=assigned_amount(本执行方被分配), 已支付劳务/会务/可用金额 均按
biz_meeting.execution_unit_id = 本执行方 org 过滤, 与项目级 total_amount/available_amount 无关.
-->
<select id="selectExecutorList" resultMap="BizProjectResult" parameterType="BizProject">
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: 把 executor 限定条件放进 ON (而不是 WHERE), 这样 join 只命中分给当前执行方的 assignment, 1 行/项目. SELECT DISTINCT 保留以防 LEFT JOIN 副作用 -->
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 过滤 (执行方级).
-->
<select id="selectExecutorStaffList" resultMap="BizProjectResult" parameterType="BizProject">
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,
select 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,
@@ -215,7 +260,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
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
@@ -241,6 +316,8 @@
<include refid="selectFields"/>
<where>
p.is_deleted = 0
<if test="params.createUserId != null">and create_user_id = #{params.createUserId}</if>
<if test="isPublished != null and isPublished != ''">and p.is_published = #{isPublished}</if>
<if test="params.projectIds != null and params.projectIds.size() > 0">
and project_id in
<foreach collection="params.projectIds" item="id" open="(" separator="," close=")">
@@ -289,14 +366,12 @@
<if test="doneSessions != null">done_sessions,</if>
<if test="todoSessions != null">todo_sessions,</if>
<if test="totalAmount != null">total_amount,</if>
<if test="availableAmount != null">available_amount,</if>
<if test="paidLaborAmount != null">paid_labor_amount,</if>
<if test="paidMeetingAmount != null">paid_meeting_amount,</if>
<if test="managerScore != null">manager_score,</if>
<if test="sponsorScore != null">sponsor_score,</if>
<if test="projectForm != null and projectForm != ''">project_form,</if>
<if test="isFinished != null and isFinished != ''">is_finished,</if>
<if test="isSettled != null and isSettled != ''">is_settled,</if>
<if test="isPublished != null and isPublished != ''">is_published,</if>
<if test="sponsorOrgId != null">sponsor_org_id,</if>
<if test="leadUserId != null">lead_user_id,</if>
<if test="isBidProject != null and isBidProject != ''">is_bid_project,</if>
@@ -335,6 +410,7 @@
<if test="projectForm != null and projectForm != ''">#{projectForm},</if>
<if test="isFinished != null and isFinished != ''">#{isFinished},</if>
<if test="isSettled != null and isSettled != ''">#{isSettled},</if>
<if test="isPublished != null and isPublished != ''">#{isPublished},</if>
<if test="sponsorOrgId != null">#{sponsorOrgId},</if>
<if test="leadUserId != null">#{leadUserId},</if>
<if test="isBidProject != null and isBidProject != ''">#{isBidProject},</if>
@@ -380,14 +456,12 @@
<if test="doneSessions != null">done_sessions = #{doneSessions},</if>
<if test="todoSessions != null">todo_sessions = #{todoSessions},</if>
<if test="totalAmount != null">total_amount = #{totalAmount},</if>
<if test="availableAmount != null">available_amount = #{availableAmount},</if>
<if test="paidLaborAmount != null">paid_labor_amount = #{paidLaborAmount},</if>
<if test="paidMeetingAmount != null">paid_meeting_amount = #{paidMeetingAmount},</if>
<if test="managerScore != null">manager_score = #{managerScore},</if>
<if test="sponsorScore != null">sponsor_score = #{sponsorScore},</if>
<if test="projectForm != null and projectForm != ''">project_form = #{projectForm},</if>
<if test="isFinished != null and isFinished != ''">is_finished = #{isFinished},</if>
<if test="isSettled != null and isSettled != ''">is_settled = #{isSettled},</if>
<if test="isPublished != null and isPublished != ''">is_published = #{isPublished},</if>
<if test="sponsorOrgId != null">sponsor_org_id = #{sponsorOrgId},</if>
<if test="leadUserId != null">lead_user_id = #{leadUserId},</if>
<if test="isBidProject != null and isBidProject != ''">is_bid_project = #{isBidProject},</if>
@@ -399,13 +473,14 @@
</trim>
where project_id = #{projectId}
</update>
<!-- 删除公告: 将 4 个公示 URL 字段全部置 NULL (未发布态由 URL 是否为空判定, 表里无独立 is_published) -->
<!-- 删除公告: 将 4 个公示 URL 字段置 NULL is_published='0' (取消发布) -->
<update id="clearAnnouncement" parameterType="Long">
update biz_project
set invitation_url = null,
support_letter_url = null,
publish_url = null,
schedule_url = null
schedule_url = null,
is_published = '0'
where project_id = #{projectId}
</update>
<!-- 开通到期回收: 每天 00:05 由 OpenStatusScheduler 调用, 到期(open_deadline <= 今天)的 Y 置回 N -->
@@ -461,28 +536,4 @@
and b.staff_user_id = #{userId}
) t
</select>
<!-- 会议结算后重算项目金额: 全量 SUM 已结算会议 (is_settled=1) 的 labor_fee/meeting_fee,
幂等回写 paid_labor_amount / paid_meeting_amount, 并重算 available_amount = 总金额 - 管理费 - 已支付劳务 - 已支付会务.
单条 UPDATE 原子执行, 多会议并发结算同一项目时无丢失更新 (每次都是全量重算). -->
<update id="recomputeSettledAmounts" parameterType="Long">
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}
</update>
</mapper>
@@ -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)));
}
}
@@ -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)
@@ -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;
}
}
@@ -53,6 +53,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<!-- 用户扩展视图 resultMap: 继承 SysUserResult, 附加 sys_user.role_type 业务角色 -->
<resultMap type="SysUserExtendVo" id="SysUserExtendResult" extends="SysUserResult">
<result property="roleType" column="u_role_type" />
<result property="orgName" column="org_name" />
</resultMap>
<sql id="selectUserVo">
@@ -99,7 +100,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-->
<select id="selectUserExtendList" parameterType="SysUserExtendVo" resultMap="SysUserExtendResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.update_by, u.update_time, u.remark, d.dept_name, d.leader,
u.role_type as u_role_type
u.role_type as u_role_type,
coalesce(
(select o.org_name from biz_org o where o.user_id = u.user_id limit 1),
(select o.org_name from biz_org o join biz_person p on p.org_id = o.org_id where p.user_id = u.user_id limit 1)
) as org_name
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
@@ -124,6 +129,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deptId != null and deptId != 0">
AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))
</if>
<if test="nickName != null and nickName != ''"><!-- 姓名过滤 -->
AND u.nick_name like concat('%', #{nickName}, '%')
</if>
<if test="orgName != null and orgName != ''"><!-- 单位名称过滤 (MAIN 走 biz_org, SUB 走 biz_person) -->
AND (
exists (select 1 from biz_org o where o.user_id = u.user_id and o.org_name like concat('%', #{orgName}, '%'))
or exists (select 1 from biz_person p join biz_org o2 on o2.org_id = p.org_id where p.user_id = u.user_id and o2.org_name like concat('%', #{orgName}, '%'))
)
</if>
<if test="roleType != null and roleType != ''"><!-- 业务角色过滤 -->
AND u.role_type = #{roleType}
</if>