+37
@@ -23,6 +23,9 @@ import com.ruoyi.framework.web.service.SysPermissionService;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import com.ruoyi.system.service.ISysMenuService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
import com.ruoyi.business.domain.BizExpert;
|
||||
import com.ruoyi.business.service.IBizExpertService;
|
||||
|
||||
/**
|
||||
* 登录验证
|
||||
@@ -47,6 +50,12 @@ public class SysLoginController
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private ISysUserService userService;
|
||||
|
||||
@Autowired
|
||||
private IBizExpertService expertService;
|
||||
|
||||
/**
|
||||
* 登录方法
|
||||
*
|
||||
@@ -56,6 +65,11 @@ public class SysLoginController
|
||||
@PostMapping("/login")
|
||||
public AjaxResult login(@RequestBody LoginBody loginBody)
|
||||
{
|
||||
// 待审核专家拦截: role_type=doctor 且 biz_expert.audit_status='1' → 禁止登录
|
||||
if (isPendingExpert(loginBody.getUsername()))
|
||||
{
|
||||
return AjaxResult.error("您的信息正在审核中");
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
// 生成令牌
|
||||
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
|
||||
@@ -64,6 +78,29 @@ public class SysLoginController
|
||||
return ajax;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否为待审核专家 (role_type=doctor 且 biz_expert.audit_status='1')
|
||||
*/
|
||||
private boolean isPendingExpert(String username)
|
||||
{
|
||||
if (username == null || username.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
SysUser probe = userService.selectUserByUserName(username);
|
||||
// 邮箱登录兜底 (用户名查不到且含 @ 时按邮箱反查)
|
||||
if (probe == null && username.contains("@"))
|
||||
{
|
||||
probe = userService.selectUserByEmail(username);
|
||||
}
|
||||
if (probe == null || probe.getUserId() == null || !"doctor".equals(probe.getRoleType()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BizExpert expert = expertService.getByUserId(probe.getUserId());
|
||||
return expert != null && "1".equals(expert.getAuditStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*
|
||||
|
||||
@@ -28,12 +28,12 @@ ruoyi:
|
||||
# 阿里云短信配置 (hwt-code ali.sms 模式)
|
||||
# dev/prod 区分走代码: phone 以 "10" 开头视为 dev 测试 (固定码 1234), 其它走 aliyun 真发
|
||||
sms:
|
||||
accessKeyId: LTAI5t7S88DmdTxHPzPtJTwG
|
||||
accessKeySecret: UIkwjMpmlYjgX5IMLjPj8FQNPthdlR
|
||||
signName: 北京仙仁掌医学科技发展
|
||||
template: SMS_321560247
|
||||
esignTemplate: SMS_492460505
|
||||
esignBaseUrl: https://risingdoctor.com/hg
|
||||
accessKeyId: LTAI5tAgeAUviVdPSsxYCkPY
|
||||
accessKeySecret: 556LX6mXnJIPZgf0vFXcl4mKCzzgKq
|
||||
signName: 北京整合医学学会
|
||||
template: SMS_291440833
|
||||
esignTemplate: SMS_512040098
|
||||
esignBaseUrl: https://hegui.bahim.org.cn
|
||||
endpoint: dysmsapi.aliyuncs.com
|
||||
regionId: cn-hangzhou
|
||||
# 发票 OCR (本地 Java 识别, PaddleOCR ONNX Runtime, 替代原 ry-ocr Python 微服务)
|
||||
|
||||
+17
-2
@@ -14,8 +14,10 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizPerson;
|
||||
import com.ruoyi.business.domain.BizExpert;
|
||||
import com.ruoyi.business.dto.SmsValidForm;
|
||||
import com.ruoyi.business.mapper.BizPersonMapper;
|
||||
import com.ruoyi.business.service.IBizExpertService;
|
||||
import com.ruoyi.business.service.IBizOrgService;
|
||||
import com.ruoyi.business.service.SysSmsService;
|
||||
import com.ruoyi.common.utils.id.SnowflakeId;
|
||||
@@ -64,6 +66,9 @@ public class BizAuthController extends BaseController {
|
||||
@Autowired
|
||||
private IBizOrgService bizOrgService;
|
||||
|
||||
@Autowired
|
||||
private IBizExpertService expertService;
|
||||
|
||||
@Autowired
|
||||
private BizPersonMapper bizPersonMapper;
|
||||
|
||||
@@ -165,6 +170,14 @@ public class BizAuthController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
// 4.6 待审核专家拦截: role_type=doctor 且 biz_expert.audit_status='1' → 禁止登录
|
||||
if ("doctor".equals(user.getRoleType())) {
|
||||
BizExpert expert = expertService.getByUserId(user.getUserId());
|
||||
if (expert != null && "1".equals(expert.getAuditStatus())) {
|
||||
return error("您的信息正在审核中");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 构造 LoginUser 并设 SecurityContext (兼容后续 spring security 鉴权)
|
||||
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
@@ -288,6 +301,7 @@ public class BizAuthController extends BaseController {
|
||||
public AjaxResult registerSponsor(@RequestBody Map<String, Object> body) {
|
||||
String username = (String) body.get("username");
|
||||
String orgIdStr = body.get("orgId") == null ? null : body.get("orgId").toString();
|
||||
String realName = (String) body.get("realName");
|
||||
String phone = (String) body.get("phone");
|
||||
String code = (String) body.get("smsCode");
|
||||
String password = (String) body.get("password");
|
||||
@@ -298,6 +312,7 @@ public class BizAuthController extends BaseController {
|
||||
if (username == null || username.length() < 4 || username.length() > 20) return error("用户名长度 4-20 位");
|
||||
if (!username.matches("^[A-Za-z0-9_]+$")) return error("用户名只能包含字母/数字/下划线");
|
||||
if (orgIdStr == null || orgIdStr.isEmpty()) return error("请选择企业");
|
||||
if (realName == null || realName.trim().isEmpty()) return error("请输入联系人姓名");
|
||||
if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
|
||||
if (code == null || code.isEmpty()) return error("请输入短信验证码");
|
||||
if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位");
|
||||
@@ -340,7 +355,7 @@ public class BizAuthController extends BaseController {
|
||||
// role_type 显式写 sponsor, 避免被 sys_user.role_type DB DEFAULT 'executor' 覆盖
|
||||
SysUser user = new SysUser();
|
||||
user.setUserName(username);
|
||||
user.setNickName(org.getOrgName()); // 昵称用所选企业名
|
||||
user.setNickName(realName); // 昵称用联系人姓名
|
||||
user.setPhonenumber(phone);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setStatus("0");
|
||||
@@ -353,7 +368,7 @@ public class BizAuthController extends BaseController {
|
||||
// 6. 写 biz_person (关联到所选企业, 不再新建 biz_org)
|
||||
BizPerson self = new BizPerson();
|
||||
SnowflakeId.injectIfEmpty(self, "personId");
|
||||
self.setName(username); // 表单无姓名字段, 用登录用户名占位
|
||||
self.setName(realName);
|
||||
self.setPhone(phone);
|
||||
self.setOrgId(orgId);
|
||||
self.setDepartment("待分配");
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ public class BizExecutionIntentController extends BaseController
|
||||
SysUser u = sysUserMapper.selectUserById(uid);
|
||||
if (u != null) {
|
||||
if (bizExecutionIntent.getName() == null || bizExecutionIntent.getName().isEmpty())
|
||||
bizExecutionIntent.setName(u.getUserName());
|
||||
bizExecutionIntent.setName(u.getNickName());
|
||||
if (bizExecutionIntent.getPhone() == null || bizExecutionIntent.getPhone().isEmpty())
|
||||
bizExecutionIntent.setPhone(u.getPhonenumber());
|
||||
}
|
||||
|
||||
+8
-8
@@ -94,8 +94,8 @@ public class BizMeetingAttendeeController extends BaseController {
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody BizMeetingAttendee body) {
|
||||
Long attendeeId = attendeeService.insertByPhoneWithProfile(body);
|
||||
// 人员变化 → 会议费用待重算
|
||||
bizMeetingService.markFeeCalcPending(body.getMeetingId());
|
||||
// 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中)
|
||||
bizMeetingService.recomputeLaborFee(body.getMeetingId());
|
||||
// 邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发
|
||||
return success(attendeeId);
|
||||
}
|
||||
@@ -113,9 +113,9 @@ public class BizMeetingAttendeeController extends BaseController {
|
||||
BizMeetingAttendee before = attendeeService.selectById(body.getId());
|
||||
body.setUpdateBy(SecurityUtils.getUsername());
|
||||
int rows = attendeeService.updateProfile(body);
|
||||
// 人员变化 → 会议费用待重算
|
||||
// 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中)
|
||||
if (before != null) {
|
||||
bizMeetingService.markFeeCalcPending(before.getMeetingId());
|
||||
bizMeetingService.recomputeLaborFee(before.getMeetingId());
|
||||
}
|
||||
return toAjax(rows);
|
||||
}
|
||||
@@ -129,9 +129,9 @@ public class BizMeetingAttendeeController extends BaseController {
|
||||
public AjaxResult remove(@PathVariable("id") Long id) {
|
||||
BizMeetingAttendee before = attendeeService.selectById(id);
|
||||
int rows = attendeeService.deleteByPrimaryKey(id);
|
||||
// 人员变化 → 会议费用待重算
|
||||
// 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中)
|
||||
if (before != null) {
|
||||
bizMeetingService.markFeeCalcPending(before.getMeetingId());
|
||||
bizMeetingService.recomputeLaborFee(before.getMeetingId());
|
||||
}
|
||||
return toAjax(rows);
|
||||
}
|
||||
@@ -194,9 +194,9 @@ public class BizMeetingAttendeeController extends BaseController {
|
||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||
// 解析 + 入库 (邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发)
|
||||
ImportResult result = attendeeService.importFromExcel(file, meetingId, SecurityUtils.getUsername());
|
||||
// 人员变化 → 会议费用待重算 (有成功导入才需重算, 但幂等, 直接标记)
|
||||
// 人员变化 → 立即重算劳务费 (有成功导入才需重算)
|
||||
if (result != null && result.getOkNum() > 0) {
|
||||
bizMeetingService.markFeeCalcPending(meetingId);
|
||||
bizMeetingService.recomputeLaborFee(meetingId);
|
||||
}
|
||||
return success(result);
|
||||
}
|
||||
|
||||
+48
-1
@@ -99,6 +99,12 @@ public class BizMeetingController extends BaseController {
|
||||
} else {
|
||||
bizMeeting.getParams().put("executorUserId", uid);
|
||||
}
|
||||
// assignedSessions 聚合用 (公司维度): MAIN 用自己, SUB 反查主账号 parent_user_id; 与过滤参数分离, 避免改变 SUB 可见会议范围
|
||||
Long aggUid = uid;
|
||||
if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) {
|
||||
aggUid = current.getParentUserId();
|
||||
}
|
||||
bizMeeting.getParams().put("assignedExecutorUserId", aggUid);
|
||||
}
|
||||
// 合规人员(manager) 数据权限: 只看"本人创建的项目"下的会议 (project_id ∈ create_user_id = 自己的项目)
|
||||
else if ("manager".equals(roleType)) {
|
||||
@@ -111,7 +117,19 @@ public class BizMeetingController extends BaseController {
|
||||
|
||||
@GetMapping("/{meetingId}")
|
||||
public AjaxResult getInfo(@PathVariable("meetingId") Long meetingId) {
|
||||
return success(bizMeetingService.getById(meetingId));
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
// executor 详情: "总期数/期数分母" = 分配给本执行方的场次, 而非项目总场次 (total_periods 对执行方不可见)
|
||||
if (m != null && m.getProjectId() != null
|
||||
&& "executor".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) {
|
||||
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();
|
||||
}
|
||||
m.setAssignedSessions((long) bizProjectService.countAssignedSessions(m.getProjectId(), aggUid));
|
||||
}
|
||||
return success(m);
|
||||
}
|
||||
|
||||
@Log(title = "会议", businessType = BusinessType.INSERT)
|
||||
@@ -183,6 +201,32 @@ public class BizMeetingController extends BaseController {
|
||||
@Log(title = "会议", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody BizMeeting bizMeeting) {
|
||||
// executor 修改会议同样校验 (与 add 一致, 只是期数冲突检测排除自身): 期数不得超过分配给本公司的场次 + 相同期数不重复.
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if ("executor".equals(roleType)) {
|
||||
Long uid = SecurityUtils.getUserId();
|
||||
Long projectId = bizMeeting.getProjectId();
|
||||
if (projectId == null) {
|
||||
throw new ServiceException("修改会议必须指定 projectId");
|
||||
}
|
||||
Long aggUid = uid;
|
||||
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||
if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) {
|
||||
aggUid = current.getParentUserId();
|
||||
}
|
||||
Long executionUnitId = bizOrgService.selectOrgIdByUserId(aggUid);
|
||||
int assigned = bizProjectService.countAssignedSessions(projectId, aggUid);
|
||||
Long periodNo = bizMeeting.getPeriodNo();
|
||||
if (periodNo != null && periodNo > assigned) {
|
||||
throw new ServiceException("期数不能超过分配给本公司的场次 (共 " + assigned + " 场)");
|
||||
}
|
||||
if (periodNo != null) {
|
||||
int dup = bizMeetingService.countByProjectIdExecutionUnitPeriodExclude(projectId, executionUnitId, periodNo, bizMeeting.getMeetingId());
|
||||
if (dup > 0) {
|
||||
throw new ServiceException("本机构已创建第 " + periodNo + " 期会议, 请勿重复");
|
||||
}
|
||||
}
|
||||
}
|
||||
bizMeeting.setUpdateBy(SecurityUtils.getUsername());
|
||||
bizMeeting.setUpdateTime(new Date());
|
||||
// 修改会议时项目形式也从项目继承 (与 add 一致, 避免 project_form 残留为空)
|
||||
@@ -534,6 +578,9 @@ public class BizMeetingController extends BaseController {
|
||||
|
||||
m.setIsSettled(1);
|
||||
m.setSettleTime(new Date());
|
||||
// 结算即终态: 直接落完结标记, 省去二次「完结」动作 (auto-finish)
|
||||
m.setIsFinished(1);
|
||||
m.setFinishTime(new Date());
|
||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
appendAuditLog(m, "SETTLE", "APPROVED", "会议结算", null);
|
||||
|
||||
+50
-16
@@ -1,5 +1,6 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -10,7 +11,9 @@ 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.file.FileUtils;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.oss.OssZipService;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
|
||||
@@ -29,6 +32,8 @@ public class BizMeetingMaterialController extends BaseController {
|
||||
private IBizMeetingMaterialService bizMeetingMaterialService;
|
||||
@Autowired
|
||||
private IBizMeetingService bizMeetingService;
|
||||
@Autowired
|
||||
private OssZipService ossZipService;
|
||||
|
||||
/**
|
||||
* 查该会议的所有材料记录
|
||||
@@ -56,9 +61,14 @@ public class BizMeetingMaterialController extends BaseController {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 先判断材料是否实际变化 (必须在 replaceByMeetingId 之前, 后者会先删旧记录).
|
||||
// 会务材料没变时不置"统计中", 避免无谓重算 + 前端"统计中"闪烁.
|
||||
boolean changed = bizMeetingMaterialService.isMaterialSetChanged(meetingId, list);
|
||||
List<BizMeetingMaterial> saved = bizMeetingMaterialService.replaceByMeetingId(meetingId, list);
|
||||
// 材料变化 → 会议费用待重算 (FeeCalcScheduler 汇总回写)
|
||||
bizMeetingService.markFeeCalcPending(meetingId);
|
||||
if (changed) {
|
||||
// 材料变化 → 立即重算会议费用 (发票未 OCR 完则回滚 0 走调度器兜底)
|
||||
bizMeetingService.recomputeMeetingFee(meetingId);
|
||||
}
|
||||
return success(saved);
|
||||
}
|
||||
|
||||
@@ -92,58 +102,79 @@ public class BizMeetingMaterialController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 会务下载: 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打 zip, 返回下载 URL.
|
||||
* 会务下载: 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打 zip, 后端代理改名下发.
|
||||
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
||||
* 文件名: 项目编号_项目名称_第N期_会务.zip
|
||||
*/
|
||||
@GetMapping("/{meetingId}/downloadZip")
|
||||
public AjaxResult downloadZip(@PathVariable("meetingId") Long meetingId) {
|
||||
public void downloadZip(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
||||
}
|
||||
// 注意: 不能用 success(String), 否则 URL 会被塞进 msg 字段 (BaseController.success(String) 重载陷阱)
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildServiceZipUrl(meetingId));
|
||||
String url = bizMeetingMaterialService.buildServiceZipUrl(meetingId);
|
||||
streamZip(url, bizMeetingMaterialService.serviceZipFilename(meetingId), response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 劳务下载: 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息在 OSS 端打 zip, 返回下载 URL.
|
||||
* 劳务下载: 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息在 OSS 端打 zip, 后端代理改名下发.
|
||||
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
||||
* 文件名: 项目编号_项目名称_第N期_劳务.zip
|
||||
*/
|
||||
@GetMapping("/{meetingId}/downloadLaborZip")
|
||||
public AjaxResult downloadLaborZip(@PathVariable("meetingId") Long meetingId) {
|
||||
public void downloadLaborZip(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
||||
}
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildLaborZipUrl(meetingId));
|
||||
String url = bizMeetingMaterialService.buildLaborZipUrl(meetingId);
|
||||
streamZip(url, bizMeetingMaterialService.laborZipFilename(meetingId), response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量会务下载: 多会议会务材料合并打一个 zip, 返回下载 URL.
|
||||
* 批量会务下载: 多会议会务材料合并打一个 zip, 后端代理改名下发.
|
||||
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
||||
* 文件名: 会务_下载时间.zip
|
||||
*/
|
||||
@PostMapping("/batchDownloadZip")
|
||||
public AjaxResult batchDownloadZip(@RequestBody(required = false) BatchDownloadBody body) {
|
||||
public void batchDownloadZip(@RequestBody(required = false) BatchDownloadBody body, HttpServletResponse response) throws Exception {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
||||
}
|
||||
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchServiceZipUrl(meetingIds));
|
||||
String url = bizMeetingMaterialService.buildBatchServiceZipUrl(meetingIds);
|
||||
streamZip(url, bizMeetingMaterialService.batchServiceZipFilename(), response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量劳务下载: 多会议劳务材料(含参会人信息)合并打一个 zip, 返回下载 URL.
|
||||
* 批量劳务下载: 多会议劳务材料(含参会人信息)合并打一个 zip, 后端代理改名下发.
|
||||
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
||||
* 文件名: 劳务_下载时间.zip
|
||||
*/
|
||||
@PostMapping("/batchDownloadLaborZip")
|
||||
public AjaxResult batchDownloadLaborZip(@RequestBody(required = false) BatchDownloadBody body) {
|
||||
public void batchDownloadLaborZip(@RequestBody(required = false) BatchDownloadBody body, HttpServletResponse response) throws Exception {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
||||
}
|
||||
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchLaborZipUrl(meetingIds));
|
||||
String url = bizMeetingMaterialService.buildBatchLaborZipUrl(meetingIds);
|
||||
streamZip(url, bizMeetingMaterialService.batchLaborZipFilename(), response);
|
||||
}
|
||||
|
||||
/** 从 FC 打包返回的签名 URL 拉取 zip 字节流, 以自定义文件名写回 response (Content-Disposition 走 UTF-8 编码) */
|
||||
private void streamZip(String url, String filename, HttpServletResponse response) throws Exception {
|
||||
response.setContentType("application/zip");
|
||||
FileUtils.setAttachmentResponseHeader(response, filename);
|
||||
try (InputStream in = ossZipService.openZipStream(url)) {
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) {
|
||||
response.getOutputStream().write(buf, 0, n);
|
||||
}
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +198,10 @@ public class BizMeetingMaterialController extends BaseController {
|
||||
public AjaxResult uploadServiceMaterials(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||
int updated = bizMeetingMaterialService.uploadServiceMaterials(file, meetingId);
|
||||
bizMeetingService.markFeeCalcPending(meetingId);
|
||||
// 仅当真有材料被替换 (内容变化, updated>0) 才立即重算会务费; 全量未变不置"统计中"
|
||||
if (updated > 0) {
|
||||
bizMeetingService.recomputeMeetingFee(meetingId);
|
||||
}
|
||||
return success(updated);
|
||||
}
|
||||
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
|
||||
/**
|
||||
* 会议阶段统计端点 — 独立 Controller (仅 sponsor/Home KPI 用).
|
||||
*
|
||||
* <p>独立成 Controller 的原因: BizMeetingController 有 `GET /{meetingId}` 详情端点,
|
||||
* Spring 6.x 会把同 Controller 内的字面量 `/stageStats` 路由到 `/{meetingId}` 上
|
||||
* (报 "参数类型不匹配"). 拆到独立 Controller 后两条路径不在同一个映射表竞争, 零冲突.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/meeting")
|
||||
public class BizMeetingStageStatsController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IBizMeetingService bizMeetingService;
|
||||
@Autowired
|
||||
private SysUserMapper sysUserMapper;
|
||||
|
||||
/**
|
||||
* 按 current_stage 分组计数, 只做 sponsor 数据权限隔离 (MAIN=本支持单位全部, SUB=自己负责的项目).
|
||||
* 返回 { stage: cnt, ... }, 前端据此算: 未执行=NOT_STARTED, 已执行=排除 NOT_STARTED/IN_PROGRESS 的其余 stage (含 FROZEN).
|
||||
* 其他角色的统计端点单独实现, 此端点非 sponsor 调用返回空 map (不泄露全量数据).
|
||||
*/
|
||||
@GetMapping("/stageStats")
|
||||
public AjaxResult stageStats() {
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
if (!"sponsor".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) {
|
||||
return success(result);
|
||||
}
|
||||
BizMeeting q = new BizMeeting();
|
||||
Long uid = SecurityUtils.getUserId();
|
||||
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||
if (current != null && "SUB".equals(current.getAccountType())) {
|
||||
q.getParams().put("monitorUserId", uid);
|
||||
} else {
|
||||
q.getParams().put("sponsorAdminUserId", uid);
|
||||
}
|
||||
for (Map<String, Object> r : bizMeetingService.selectStageStats(q)) {
|
||||
// resultType=map 的 key 大小写随 JDBC 驱动, 做大小写不敏感匹配, 避免"统计恒 0"
|
||||
String stage = null;
|
||||
long cnt = 0L;
|
||||
for (Map.Entry<String, Object> e : r.entrySet()) {
|
||||
String k = e.getKey();
|
||||
if (k == null) continue;
|
||||
if (k.equalsIgnoreCase("currentStage") || k.equalsIgnoreCase("stage")) {
|
||||
stage = e.getValue() == null ? null : String.valueOf(e.getValue());
|
||||
} else if (k.equalsIgnoreCase("cnt") || k.equalsIgnoreCase("count")) {
|
||||
cnt = e.getValue() == null ? 0L : ((Number) e.getValue()).longValue();
|
||||
}
|
||||
}
|
||||
if (stage != null) {
|
||||
result.put(stage, cnt);
|
||||
}
|
||||
}
|
||||
return success(result);
|
||||
}
|
||||
}
|
||||
+20
-6
@@ -40,20 +40,34 @@ public class BizMessageController extends BaseController
|
||||
|
||||
/**
|
||||
* 当前登录用户的最近消息 (含未读统计)
|
||||
* GET /business/message/my?limit=50
|
||||
* 返回 {rows: [...], unread: 12}
|
||||
* unread 用 countUnread 跟 limit 解耦, SSE 推的也是真值
|
||||
* 两种模式:
|
||||
* - 分页: GET /business/message/my?pageNum=1&pageSize=20 (返回真实 total)
|
||||
* - 快捷: GET /business/message/my?limit=50 (嵌入式列表, total=rows.size)
|
||||
* 返回 {rows: [...], unread: 12, total: n}
|
||||
* unread 用 countUnread 跟分页解耦, SSE 推的也是真值
|
||||
*/
|
||||
@GetMapping("/my")
|
||||
public AjaxResult my(@RequestParam(value = "limit", required = false, defaultValue = "50") Integer limit)
|
||||
public AjaxResult my(@RequestParam(value = "pageNum", required = false) Integer pageNum,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||
@RequestParam(value = "limit", required = false) Integer limit)
|
||||
{
|
||||
Long uid = SecurityUtils.getUserId();
|
||||
List<BizMessage> rows = bizMessageService.selectMyRecent(uid, limit);
|
||||
List<BizMessage> rows;
|
||||
int total;
|
||||
if (pageNum != null && pageSize != null && pageNum > 0 && pageSize > 0) {
|
||||
int offset = (pageNum - 1) * pageSize;
|
||||
rows = bizMessageService.selectMyPage(uid, offset, pageSize);
|
||||
total = bizMessageService.countMy(uid);
|
||||
} else {
|
||||
int lim = (limit == null || limit <= 0) ? 50 : limit;
|
||||
rows = bizMessageService.selectMyRecent(uid, lim);
|
||||
total = rows.size();
|
||||
}
|
||||
int unread = bizMessageService.countUnread(uid);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rows", rows);
|
||||
data.put("unread", unread);
|
||||
data.put("total", rows.size());
|
||||
data.put("total", total);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
|
||||
+21
-4
@@ -15,6 +15,7 @@ import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.business.domain.BizPerson;
|
||||
import com.ruoyi.business.domain.BizPersonExecutorImportVO;
|
||||
import com.ruoyi.business.domain.BizPersonImportVO;
|
||||
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||
import com.ruoyi.business.service.IBizPersonService;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
@@ -33,6 +34,9 @@ public class BizPersonController extends BaseController
|
||||
@Autowired
|
||||
private SysUserMapper sysUserMapper;
|
||||
|
||||
@Autowired
|
||||
private BizOrgMapper bizOrgMapper;
|
||||
|
||||
/**
|
||||
* 业务角色白名单: 这些 roleType 的 MAIN 账号调 /list 会自动按"本机构子账号"隔离
|
||||
* (sys_user.parent_user_id → sys_user.user_id → biz_person.user_id 链路过滤)
|
||||
@@ -69,8 +73,9 @@ public class BizPersonController extends BaseController
|
||||
@GetMapping("/sponsorList")
|
||||
public TableDataInfo sponsorList(BizPerson bizPerson)
|
||||
{
|
||||
Long mainUid = getUserId();
|
||||
bizPerson.getParams().put("sponsorOwnerUid", mainUid);
|
||||
// 严格按 org_id 圈本机构所有用户 (MAIN + SUB), 而非 parent_user_id 归属
|
||||
Long orgId = bizOrgMapper.selectOrgIdByUserId(getUserId());
|
||||
bizPerson.getParams().put("sponsorOrgId", orgId);
|
||||
startPage();
|
||||
List<BizPerson> list = bizPersonService.selectSponsorList(bizPerson);
|
||||
return getDataTable(list);
|
||||
@@ -82,8 +87,9 @@ public class BizPersonController extends BaseController
|
||||
@GetMapping("/executorList")
|
||||
public TableDataInfo executorList(BizPerson bizPerson)
|
||||
{
|
||||
Long mainUid = getUserId();
|
||||
bizPerson.getParams().put("executorOwnerUid", mainUid);
|
||||
// 严格按 org_id 圈本机构所有用户 (MAIN + SUB), 而非 parent_user_id 归属
|
||||
Long orgId = bizOrgMapper.selectOrgIdByUserId(getUserId());
|
||||
bizPerson.getParams().put("executorOrgId", orgId);
|
||||
startPage();
|
||||
List<BizPerson> list = bizPersonService.selectExecutorList(bizPerson);
|
||||
return getDataTable(list);
|
||||
@@ -114,6 +120,17 @@ public class BizPersonController extends BaseController
|
||||
bizPerson.setUpdateBy(getUsername());
|
||||
return toAjax(bizPersonService.updateByPrimaryKey(bizPerson));
|
||||
}
|
||||
/**
|
||||
* 个人资料编辑 (sponsor/executor 账号管理页): 只改当前登录人自己的姓名/手机号
|
||||
* 按 user_id 反查 personId, 同步写 biz_person.name + sys_user.nick_name (单一可信源)
|
||||
*/
|
||||
@Log(title = "个人资料", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/profile")
|
||||
public AjaxResult updateProfile(@RequestBody BizPerson bizPerson)
|
||||
{
|
||||
bizPerson.setUserId(getUserId());
|
||||
return toAjax(bizPersonService.updateProfileByUserId(bizPerson));
|
||||
}
|
||||
/**
|
||||
* 更换机构管理员 (admin/sponsor-people 管理员 switch)
|
||||
* body: { personId } — 把该人员晋升为机构 MAIN, 原管理员降为 SUB
|
||||
|
||||
+64
-25
@@ -252,7 +252,15 @@ public class BizProjectController extends BaseController
|
||||
@GetMapping("/{projectId}/assigns")
|
||||
public AjaxResult getAssigns(@PathVariable("projectId") Long projectId)
|
||||
{
|
||||
return success(bizProjectAssignService.selectByProjectId(projectId));
|
||||
List<BizProjectAssign> list = bizProjectAssignService.selectByProjectId(projectId);
|
||||
// 执行方只看本单位 (严格隔离, 不依赖前端): 过滤掉其它执行单位
|
||||
if ("executor".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) {
|
||||
Long myOrgId = bizOrgService.selectOrgIdByUserId(SecurityUtils.getUserId());
|
||||
if (myOrgId != null) {
|
||||
list.removeIf(a -> a.getExecutionUnitId() == null || !a.getExecutionUnitId().equals(myOrgId));
|
||||
}
|
||||
}
|
||||
return success(list);
|
||||
}
|
||||
|
||||
@Log(title = "项目执行方分配", businessType = BusinessType.INSERT)
|
||||
@@ -350,46 +358,77 @@ public class BizProjectController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用评分 upsert (任何角色: sponsor / executor / compliance)
|
||||
* 评分 upsert
|
||||
* POST /business/project/rate
|
||||
* 不做可见性校验 — 评分公开
|
||||
* 评分写入 biz_project_rating 后, 同步回写 biz_project.manager_score
|
||||
* = 该项目所有 compliance 角色评分的 4 维度总分之平均 (decimal(3,1) 1 位小数)
|
||||
* 评分公开可读; 写入仅限 admin/manager → 'manager', sponsor → 'sponsor'
|
||||
* raterRole 由后端按登录人 role_type 派生, 不信任前端 (杜绝医生/执行方伪造评分)
|
||||
* 评分写入 biz_project_rating 后, 同步回写聚合分:
|
||||
* manager → biz_project.manager_score
|
||||
* sponsor → biz_project.sponsor_score
|
||||
* 聚合口径 = 该角色所有评分记录 4 个维度值的平均 (decimal(3,1) 1 位小数), 多人评分取平均而非最后一次覆盖
|
||||
*/
|
||||
@Log(title = "项目评分", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/rate")
|
||||
public AjaxResult rate(@RequestBody BizProjectRating body)
|
||||
{
|
||||
if (body.getProjectId() == null || body.getRaterRole() == null) {
|
||||
return error("projectId / raterRole 必填");
|
||||
if (body.getProjectId() == null) {
|
||||
return error("projectId 必填");
|
||||
}
|
||||
// 评分人以当前登录用户为准, 不信任前端传入的 raterId
|
||||
body.setRaterId(SecurityUtils.getUserId());
|
||||
body.setCreateBy(SecurityUtils.getUsername());
|
||||
body.setUpdateBy(SecurityUtils.getUsername());
|
||||
|
||||
// raterRole 由登录人真实角色派生 (role_type 单一可信源), 杜绝医生/执行方伪造 manager/sponsor 评分
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
String raterRole;
|
||||
if ("admin".equals(roleType) || "manager".equals(roleType)) {
|
||||
raterRole = "manager";
|
||||
}
|
||||
else if ("sponsor".equals(roleType)) {
|
||||
raterRole = "sponsor";
|
||||
}
|
||||
else {
|
||||
return error("当前角色不可评分");
|
||||
}
|
||||
body.setRaterRole(raterRole);
|
||||
|
||||
int rows = bizProjectRatingService.upsertRating(body);
|
||||
|
||||
// 同步回写 biz_project.manager_score (仅 compliance 角色聚合)
|
||||
if ("compliance".equals(body.getRaterRole())) {
|
||||
BizProjectRating q = new BizProjectRating();
|
||||
q.setProjectId(body.getProjectId());
|
||||
q.setRaterRole("compliance");
|
||||
List<BizProjectRating> all = bizProjectRatingService.selectList(q);
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
int cnt = 0;
|
||||
for (BizProjectRating r : all) {
|
||||
long s = safeLong(r.getQualityScore()) + safeLong(r.getResponseScore())
|
||||
+ safeLong(r.getCooperationScore()) + safeLong(r.getComplianceScore());
|
||||
if (s > 0) { sum = sum.add(BigDecimal.valueOf(s)); cnt++; }
|
||||
}
|
||||
BizProject p = new BizProject();
|
||||
p.setProjectId(body.getProjectId());
|
||||
p.setManagerScore(cnt == 0 ? null : sum.divide(BigDecimal.valueOf(cnt), 1, RoundingMode.HALF_UP));
|
||||
bizProjectService.updateByPrimaryKey(p); // mapper `<if test="managerScore != null">` 仅更新这一列
|
||||
}
|
||||
// 写明细后重算聚合分 (聚合分只由后端从明细算, 前端不再手写单值)
|
||||
recomputeScore(body.getProjectId(), body.getRaterRole());
|
||||
return toAjax(rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 biz_project_rating 实时重算某项目某角色的聚合分 (4 维度平均, 覆盖该角色所有评分人)
|
||||
* 聚合 = 该角色所有评分记录 4 维度值之和 / (记录数 × 4)
|
||||
*/
|
||||
private void recomputeScore(Long projectId, String raterRole)
|
||||
{
|
||||
BizProjectRating q = new BizProjectRating();
|
||||
q.setProjectId(projectId);
|
||||
q.setRaterRole(raterRole);
|
||||
List<BizProjectRating> all = bizProjectRatingService.selectList(q);
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
int cnt = 0;
|
||||
for (BizProjectRating r : all) {
|
||||
long s = safeLong(r.getQualityScore()) + safeLong(r.getResponseScore())
|
||||
+ safeLong(r.getCooperationScore()) + safeLong(r.getComplianceScore());
|
||||
if (s > 0) { sum = sum.add(BigDecimal.valueOf(s)); cnt++; }
|
||||
}
|
||||
if (cnt == 0) return; // 无有效评分, 不动聚合分
|
||||
BigDecimal avg = sum.divide(BigDecimal.valueOf(cnt * 4L), 1, RoundingMode.HALF_UP);
|
||||
BizProject p = new BizProject();
|
||||
p.setProjectId(projectId);
|
||||
if ("sponsor".equals(raterRole)) {
|
||||
p.setSponsorScore(avg);
|
||||
} else {
|
||||
p.setManagerScore(avg);
|
||||
}
|
||||
bizProjectService.updateByPrimaryKey(p); // mapper `<if test="...Score != null">` 仅更新对应列
|
||||
}
|
||||
|
||||
private static long safeLong(Long v) { return v == null ? 0 : v; }
|
||||
|
||||
/**
|
||||
|
||||
+4
-1
@@ -17,7 +17,7 @@ import com.ruoyi.business.service.IBizProjectPlanService;
|
||||
*
|
||||
* 角色权限:
|
||||
* - 投稿角色 (doctor/executor/sponsor): 只看自己投的稿 (submitter_id = 当前用户); 新建/编辑强制 submitter_id 写自己, status 默认 '0'
|
||||
* - 管理角色 (admin/manager): 全部可见, 不强制 submitter (用于审核/结算)
|
||||
* - 管理角色 (admin/manager): 排除未提交草稿 (status='0'), 不强制 submitter (用于审核/结算)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/projectPlan")
|
||||
@@ -32,6 +32,9 @@ public class BizProjectPlanController extends BaseController
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (isSubmitterRole(roleType)) {
|
||||
BizProjectPlan.setSubmitterId(SecurityUtils.getUserId());
|
||||
} else {
|
||||
// 管理角色 (admin/manager): 未提交的草稿 (status='0') 不进列表, 只审已提交的稿
|
||||
BizProjectPlan.getParams().put("excludeDraft", true);
|
||||
}
|
||||
startPage();
|
||||
List<BizProjectPlan> list = BizProjectPlanService.selectList(BizProjectPlan);
|
||||
|
||||
-2
@@ -232,7 +232,6 @@ public class BizPublicityIntentController extends BaseController {
|
||||
v.setPosition(e.getPosition());
|
||||
v.setPhone(e.getPhone());
|
||||
v.setUserStatus(e.getUserId() != null ? "存在" : "不存在");
|
||||
v.setIntentStatus(e.getIntentStatus() == null || e.getIntentStatus().isEmpty() ? "待审核" : e.getIntentStatus());
|
||||
v.setCreateTime(e.getCreateTime());
|
||||
exportList.add(v);
|
||||
}
|
||||
@@ -259,7 +258,6 @@ public class BizPublicityIntentController extends BaseController {
|
||||
v.setPosition(e.getPosition());
|
||||
v.setPhone(e.getPhone());
|
||||
v.setUserStatus(e.getUserId() != null ? "存在" : "不存在");
|
||||
v.setIntentStatus(e.getIntentStatus() == null || e.getIntentStatus().isEmpty() ? "待审核" : e.getIntentStatus());
|
||||
v.setCreateTime(e.getCreateTime());
|
||||
exportList.add(v);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ public class BizExpert extends BaseEntity {
|
||||
private String bankCard;
|
||||
/** 银行名称 */
|
||||
private String bankName;
|
||||
/** 开户行 (支行) */
|
||||
private String bankBranch;
|
||||
/** 开户行省/市 (1-3 段, / 分隔, 直辖市省=市) */
|
||||
private String bankRegion;
|
||||
/** 开户行地址 */
|
||||
@@ -120,6 +122,8 @@ public class BizExpert extends BaseEntity {
|
||||
public void setBankCard(String bankCard) { this.bankCard = bankCard; }
|
||||
public String getBankName() { return bankName; }
|
||||
public void setBankName(String bankName) { this.bankName = bankName; }
|
||||
public String getBankBranch() { return bankBranch; }
|
||||
public void setBankBranch(String bankBranch) { this.bankBranch = bankBranch; }
|
||||
public String getBankRegion() { return bankRegion; }
|
||||
public void setBankRegion(String bankRegion) { this.bankRegion = bankRegion; }
|
||||
public String getBankAddress() { return bankAddress; }
|
||||
|
||||
@@ -118,10 +118,14 @@ public class BizMeeting extends BaseEntity {
|
||||
private String laborSigned;
|
||||
/** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */
|
||||
private transient Long userId;
|
||||
/** 会议列表筛选: current_stage NOT IN (非持久化, 逗号分隔; 前端 sponsor Home "已执行会议"跳转传 'NOT_STARTED,IN_PROGRESS') */
|
||||
private transient String currentStageNotIn;
|
||||
/** 当前登录医生/专家在本会议的参会人记录 id (非持久化, mapper 子查询填充; 用于 /doctor/meetings 签署劳务链接) */
|
||||
private transient Long attendeeId;
|
||||
/** 当前登录医生/专家在本会议的已签劳务 PDF URL (非持久化, mapper 子查询填充; null=未签) */
|
||||
private transient String attendeeLaborProtocol;
|
||||
/** 分配给本执行方的场次 (非持久化; executor 会议详情用, controller 按登录执行方反查 biz_project_assign.sessions 之和, 作为"期数"分母/总期数) */
|
||||
private transient Long assignedSessions;
|
||||
/** 新增/编辑会议时传入, 自动批量写入 biz_meeting_attendee 中间表 (非持久化) */
|
||||
private Long[] attendeeUserIds;
|
||||
/** 劳务费用 = 参会人应发金额 (fee_pre_tax) 合计 (后台定时任务汇总回写) */
|
||||
@@ -223,10 +227,14 @@ public class BizMeeting extends BaseEntity {
|
||||
public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getCurrentStageNotIn() { return currentStageNotIn; }
|
||||
public void setCurrentStageNotIn(String currentStageNotIn) { this.currentStageNotIn = currentStageNotIn; }
|
||||
public Long getAttendeeId() { return attendeeId; }
|
||||
public void setAttendeeId(Long attendeeId) { this.attendeeId = attendeeId; }
|
||||
public String getAttendeeLaborProtocol() { return attendeeLaborProtocol; }
|
||||
public void setAttendeeLaborProtocol(String attendeeLaborProtocol) { this.attendeeLaborProtocol = attendeeLaborProtocol; }
|
||||
public Long getAssignedSessions() { return assignedSessions; }
|
||||
public void setAssignedSessions(Long assignedSessions) { this.assignedSessions = assignedSessions; }
|
||||
public Integer getIsDeleted() { return isDeleted; }
|
||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||
public Long[] getAttendeeUserIds() { return attendeeUserIds; }
|
||||
|
||||
@@ -62,6 +62,8 @@ public class BizMeetingAttendee extends BaseEntity {
|
||||
private transient Date endTime;
|
||||
private transient String projectName;
|
||||
private transient String projectNo;
|
||||
/** 该参会人是否已报名该项目 (biz_execution_intent 存在 user_id + project_no): 1已报名 0未报名; 会议详情参会人列表用, 未报名姓名标红 */
|
||||
private transient Integer hasIntent;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
@@ -131,6 +133,8 @@ public class BizMeetingAttendee extends BaseEntity {
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
public String getProjectNo() { return projectNo; }
|
||||
public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
|
||||
public Integer getHasIntent() { return hasIntent; }
|
||||
public void setHasIntent(Integer hasIntent) { this.hasIntent = hasIntent; }
|
||||
/** 软删除标记 0否1是 (admin 删除会议时级联置1, 查询需 WHERE is_deleted=0) */
|
||||
private Integer isDeleted;
|
||||
public Integer getIsDeleted() { return isDeleted; }
|
||||
|
||||
@@ -45,6 +45,8 @@ public class BizPerson extends BaseEntity {
|
||||
private Date updateTime;
|
||||
/** 关联系统用户ID */
|
||||
private Long userId;
|
||||
/** 是否供应商同步过来的用户 (1=是, 0=否) */
|
||||
private Integer isSynced;
|
||||
/** 启停状态 '0'/'1' (前端 toggle 用, BizPersonServiceImpl 同步到 sys_user.status) - 非持久化字段 */
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("status")
|
||||
private transient String status;
|
||||
@@ -94,6 +96,8 @@ public class BizPerson extends BaseEntity {
|
||||
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Integer getIsSynced() { return isSynced; }
|
||||
public void setIsSynced(Integer isSynced) { this.isSynced = isSynced; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getUnitType() { return unitType; }
|
||||
|
||||
@@ -63,6 +63,9 @@ public class BizProjectPlan extends BaseEntity {
|
||||
@Excel(name = "update_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
/** 提交时间 (状态→待审核 '1' 时写入, 列表按此倒序) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date submitTime;
|
||||
/** 审核意见 */
|
||||
private String auditOpinion;
|
||||
/** 审核人 */
|
||||
@@ -115,6 +118,8 @@ public class BizProjectPlan extends BaseEntity {
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
public Date getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
||||
public Date getSubmitTime() { return submitTime; }
|
||||
public void setSubmitTime(Date submitTime) { this.submitTime = submitTime; }
|
||||
public String getAuditOpinion() { return auditOpinion; }
|
||||
public void setAuditOpinion(String auditOpinion) { this.auditOpinion = auditOpinion; }
|
||||
public String getAuditBy() { return auditBy; }
|
||||
|
||||
+1
-6
@@ -38,10 +38,7 @@ public class BizPublicityExecutionIntentExportVo {
|
||||
@Excel(name = "账号状态", sort = 8)
|
||||
private String userStatus;
|
||||
|
||||
@Excel(name = "审核状态", sort = 9)
|
||||
private String intentStatus;
|
||||
|
||||
@Excel(name = "创建时间", sort = 10, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "创建时间", sort = 9, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
@@ -61,8 +58,6 @@ public class BizPublicityExecutionIntentExportVo {
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
public String getUserStatus() { return userStatus; }
|
||||
public void setUserStatus(String userStatus) { this.userStatus = userStatus; }
|
||||
public String getIntentStatus() { return intentStatus; }
|
||||
public void setIntentStatus(String intentStatus) { this.intentStatus = intentStatus; }
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
}
|
||||
|
||||
+1
-6
@@ -38,10 +38,7 @@ public class BizPublicitySupportIntentExportVo {
|
||||
@Excel(name = "账号状态", sort = 8)
|
||||
private String userStatus;
|
||||
|
||||
@Excel(name = "审核状态", sort = 9)
|
||||
private String intentStatus;
|
||||
|
||||
@Excel(name = "创建时间", sort = 10, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "创建时间", sort = 9, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
@@ -61,8 +58,6 @@ public class BizPublicitySupportIntentExportVo {
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
public String getUserStatus() { return userStatus; }
|
||||
public void setUserStatus(String userStatus) { this.userStatus = userStatus; }
|
||||
public String getIntentStatus() { return intentStatus; }
|
||||
public void setIntentStatus(String intentStatus) { this.intentStatus = intentStatus; }
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
}
|
||||
|
||||
+24
-3
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
|
||||
@@ -11,6 +12,11 @@ public interface BizMeetingMapper
|
||||
{
|
||||
BizMeeting selectByPrimaryKey(Long meetingId);
|
||||
List<BizMeeting> selectList(BizMeeting entity);
|
||||
/**
|
||||
* 会议阶段统计 (仅 sponsor/Home KPI 用): 按 current_stage 分组计数, 只套 sponsor 数据权限 (不参与分页).
|
||||
* 返回 [{currentStage: 'RUNNING', cnt: 10}, ...]. 其他角色的统计另行实现, 不在此复用多角色过滤.
|
||||
*/
|
||||
List<Map<String, Object>> selectStageStats(BizMeeting entity);
|
||||
int insert(BizMeeting entity);
|
||||
int updateByPrimaryKey(BizMeeting entity);
|
||||
int deleteByPrimaryKey(Long meetingId);
|
||||
@@ -25,8 +31,15 @@ public interface BizMeetingMapper
|
||||
int countByProjectIdAndExecutionUnit(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId);
|
||||
/** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */
|
||||
int countByProjectIdExecutionUnitPeriod(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId, @Param("periodNo") Long periodNo);
|
||||
/** 修改校验用 (执行方隔离): 同上, 但排除指定 meetingId (修改自身会议不触发相同期数误报) */
|
||||
int countByProjectIdExecutionUnitPeriodExclude(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId, @Param("periodNo") Long periodNo, @Param("excludeMeetingId") Long excludeMeetingId);
|
||||
/**
|
||||
* 自动流转: start_time 已过 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 is_executed=1 并转 RUNNING.
|
||||
* 自动流转: start_time 已过 且 end_time 未到 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 current_stage = IN_PROGRESS (执行中).
|
||||
* <p>由 MeetingStageScheduler 每分钟触发. 只写 current_stage 缓存, 不动 is_executed 事实.
|
||||
*/
|
||||
int markInProgress();
|
||||
/**
|
||||
* 自动流转: end_time 已过 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 is_executed=1 并转 RUNNING (已执行).
|
||||
* <p>由 MeetingStageScheduler 每分钟触发. 事实 + current_stage 缓存一起写.
|
||||
*/
|
||||
int markExecuted();
|
||||
@@ -40,9 +53,10 @@ public interface BizMeetingMapper
|
||||
*/
|
||||
List<Long> selectPendingFeeCalcIds();
|
||||
/**
|
||||
* 置未汇总 (人员/材料变化触发, 幂等).
|
||||
* 费用重算状态机用: 直接置 fee_calc_status = #{status} (-1 计算中 / 0 待算兜底).
|
||||
* <p>成功(1) 由 {@link #updateFeeSummary} 一并写入, 不单独置.
|
||||
*/
|
||||
int markFeeCalcPending(Long meetingId);
|
||||
int updateFeeCalcStatus(@Param("meetingId") Long meetingId, @Param("status") int status);
|
||||
/**
|
||||
* 汇总回写 labor_fee/meeting_fee/total_fee 并置 fee_calc_status=1.
|
||||
*/
|
||||
@@ -50,4 +64,11 @@ public interface BizMeetingMapper
|
||||
@Param("laborFee") BigDecimal laborFee,
|
||||
@Param("meetingFee") BigDecimal meetingFee,
|
||||
@Param("totalFee") BigDecimal totalFee);
|
||||
/**
|
||||
* 同步重算劳务费 (参会人增删改后立即调用): 只回写 labor_fee + total_fee,
|
||||
* 不动 meeting_fee / fee_calc_status (参会人变化不影响会务费, 也不触发整体重算).
|
||||
*/
|
||||
int updateLaborFee(@Param("meetingId") Long meetingId,
|
||||
@Param("laborFee") BigDecimal laborFee,
|
||||
@Param("totalFee") BigDecimal totalFee);
|
||||
}
|
||||
@@ -18,6 +18,9 @@ public interface BizMessageMapper
|
||||
/** 收件人未读总数 (SSE 推送用, 跟 limit 解耦) */
|
||||
int countUnread(Long receiverUserId);
|
||||
|
||||
/** 收件人的全部消息总数 (分页 total 用, 跟未读解耦) */
|
||||
int countMy(Long receiverUserId);
|
||||
|
||||
int insert(BizMessage entity);
|
||||
|
||||
int updateByPrimaryKey(BizMessage entity);
|
||||
|
||||
@@ -31,4 +31,6 @@ public interface BizOrgMapper {
|
||||
/** user_id → org_id 单一可信源反查: COALESCE(biz_org.user_id, biz_person.user_id)
|
||||
* MAIN 主账号走 biz_org.user_id; SUB 子账号走 biz_person.org_id; 都没有返回 null */
|
||||
Long selectOrgIdByUserId(Long userId);
|
||||
/** 单位名称查重: 同 orgType 下 org_name 精确匹配的条数 (新增单位前判重) */
|
||||
int countByOrgName(BizOrg entity);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ public interface BizPersonMapper
|
||||
List<BizPerson> selectSponsorList(BizPerson entity);
|
||||
/** executor 专属: 同 sponsor, SQL 硬编码 unit_type='executor' (前端绕不开) */
|
||||
List<BizPerson> selectExecutorList(BizPerson entity);
|
||||
/** 按 user_id 反查人员档案 (个人资料编辑: 拿 personId 再走 updateByPrimaryKey) */
|
||||
BizPerson selectByUserId(Long userId);
|
||||
/** 按 org_id 反查该机构主账号 user_id (sys_user.account_type='MAIN' + biz_person.org_id 绑定), 不走 biz_org.user_id (可能脏) */
|
||||
Long selectMainUserIdByOrgId(Long orgId);
|
||||
int insert(BizPerson entity);
|
||||
int updateByPrimaryKey(BizPerson entity);
|
||||
int deleteByPrimaryKey(String personId);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.ruoyi.business.oss;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
@@ -199,4 +203,18 @@ public class OssZipService
|
||||
}
|
||||
return location.replaceFirst("^http://", "https://");
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载已打包好的 zip 字节流 (从 FC 返回的签名 URL 拉取).
|
||||
* 后端代理改名下发用: 先拿到 zip 字节流, 再以自定义文件名写回 response (见 BizMeetingMaterialController),
|
||||
* 绕开 FC 固定命名 output_1-xxx.zip 的问题.
|
||||
*/
|
||||
public InputStream openZipStream(String signedUrl) throws IOException
|
||||
{
|
||||
URI uri = URI.create(signedUrl);
|
||||
URLConnection conn = uri.toURL().openConnection();
|
||||
conn.setConnectTimeout(10000);
|
||||
conn.setReadTimeout(120000);
|
||||
return conn.getInputStream();
|
||||
}
|
||||
}
|
||||
|
||||
+11
-79
@@ -1,29 +1,25 @@
|
||||
package com.ruoyi.business.scheduler;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 会议费用汇总调度器 (每分钟一次, 多线程无锁).
|
||||
* 会议费用汇总调度器 (每分钟一次, 多线程无锁) — 兜底通道.
|
||||
* <p>
|
||||
* 两态设计 (不用抢占锁): 会议 fee_calc_status (0未汇总/1已汇总) + 材料 fee_status (0未计算/1已计算).
|
||||
* 主通道: 材料保存/上传、发票 OCR 完成后由 {@link IBizMeetingService#recomputeMeetingFee} 立即同步重算,
|
||||
* 状态机 -1(计算中) → 1(成功) / 0(失败回滚). 本调度器只兜底扫描 fee_calc_status=0 的会议重试,
|
||||
* 覆盖"立即算时发票还没 OCR 完 / 算错"留下的 0 态.
|
||||
* <pre>
|
||||
* 每分钟: 查 fee_calc_status=0 的会议
|
||||
* → 任一材料 fee_status=0 (发票还没 OCR 完) → 跳过, 等下轮
|
||||
* → 全部 fee_status=1 → SUM 汇总 labor_fee/meeting_fee/total_fee → fee_calc_status=1
|
||||
* 每分钟: 查 fee_calc_status=0 的会议 → 逐个交给 recomputeMeetingFee 重算
|
||||
* </pre>
|
||||
* 为什么不需要锁: 汇总前已检查"材料全算完", 天然防半成品; 汇总纯 SUM 幂等, 并发重复无副作用;
|
||||
* 置 1 后不再被扫到, 天然去重. 重算 = 只对 material + attendee 重新求和, 不碰 OCR.
|
||||
* 为什么不需要锁: 立即算/兜底都是纯 SUM 幂等, 置 1 后不再被扫到, 天然去重; -1 期间也不会被扫 (=0 才扫).
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -32,15 +28,13 @@ public class FeeCalcScheduler
|
||||
@Autowired
|
||||
private BizMeetingMapper meetingMapper;
|
||||
@Autowired
|
||||
private BizMeetingMaterialMapper materialMapper;
|
||||
@Autowired
|
||||
private BizMeetingAttendeeMapper attendeeMapper;
|
||||
private IBizMeetingService bizMeetingService;
|
||||
@Autowired
|
||||
@Qualifier("feeCalcExecutor")
|
||||
private ExecutorService feeCalcExecutor;
|
||||
|
||||
/**
|
||||
* 每分钟: 扫描 fee_calc_status=0 的会议, 并行汇总.
|
||||
* 每分钟: 扫描 fee_calc_status=0 的会议, 并行兜底重算.
|
||||
*/
|
||||
@Scheduled(fixedRate = 60_000, initialDelay = 60_000)
|
||||
public void calcFees()
|
||||
@@ -52,11 +46,11 @@ public class FeeCalcScheduler
|
||||
{
|
||||
return;
|
||||
}
|
||||
log.info("[FeeCalcScheduler] 待汇总会议 {} 个", ids.size());
|
||||
log.info("[FeeCalcScheduler] 待兜底汇总会议 {} 个", ids.size());
|
||||
for (Long meetingId : ids)
|
||||
{
|
||||
if (meetingId == null) continue;
|
||||
feeCalcExecutor.submit(() -> processMeeting(meetingId));
|
||||
feeCalcExecutor.submit(() -> bizMeetingService.recomputeMeetingFee(meetingId));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -64,66 +58,4 @@ public class FeeCalcScheduler
|
||||
log.warn("[FeeCalcScheduler] 扫描异常 (跳过, 下分钟再试)", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processMeeting(Long meetingId)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<BizMeetingMaterial> mats = materialMapper.selectByMeetingId(meetingId);
|
||||
// 任一材料 fee_status=0 (发票待 OCR) → 跳过
|
||||
if (mats != null)
|
||||
{
|
||||
for (BizMeetingMaterial m : mats)
|
||||
{
|
||||
if (m.getFeeStatus() != null && m.getFeeStatus() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal laborFee = attendeeMapper.sumFeePreTaxByMeetingId(meetingId);
|
||||
if (laborFee == null) laborFee = BigDecimal.ZERO;
|
||||
BigDecimal meetingFee = computeMeetingFee(mats);
|
||||
BigDecimal totalFee = laborFee.add(meetingFee);
|
||||
|
||||
meetingMapper.updateFeeSummary(meetingId, laborFee, meetingFee, totalFee);
|
||||
log.info("[FeeCalcScheduler] 汇总完成 meetingId={} labor={} meeting={} total={}",
|
||||
meetingId, laborFee, meetingFee, totalFee);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("[FeeCalcScheduler] 汇总失败 meetingId={} err={}", meetingId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会务费口径: 有总发票 (M_INVOICE 金额>0) 则只用总发票; 否则各 SUB 子类 (M_ 开头) 金额之和,
|
||||
* 排除 M_INVOICE / M_SETTLEMENT (两者是汇总单据, 非子类发票).
|
||||
*/
|
||||
private BigDecimal computeMeetingFee(List<BizMeetingMaterial> mats)
|
||||
{
|
||||
if (mats == null || mats.isEmpty()) return BigDecimal.ZERO;
|
||||
BigDecimal main = null;
|
||||
for (BizMeetingMaterial m : mats)
|
||||
{
|
||||
if ("M_INVOICE".equals(m.getSubType()))
|
||||
{
|
||||
main = m.getAmount();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (main != null && main.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
return main;
|
||||
}
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
for (BizMeetingMaterial m : mats)
|
||||
{
|
||||
if (m.getSubType() == null || !m.getSubType().startsWith("M_")) continue;
|
||||
if ("M_INVOICE".equals(m.getSubType()) || "M_SETTLEMENT".equals(m.getSubType())) continue;
|
||||
if (m.getAmount() != null) sum = sum.add(m.getAmount());
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -11,10 +11,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
* <p>
|
||||
* 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实
|
||||
* (is_executed / is_frozen / 劳务·会务两轨 audit_stage / 审核时间 …),
|
||||
* 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的两类事实落地:
|
||||
* 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的三类事实落地:
|
||||
* <pre>
|
||||
* 1) start_time 到 → is_executed=1 (执行中)
|
||||
* 2) submit_deadline 到 且 任一轨未提交/已退回 → is_frozen=1 (冻结)
|
||||
* 1) start_time 到 → current_stage = IN_PROGRESS (执行中)
|
||||
* 2) end_time 到 → is_executed=1 (已执行)
|
||||
* 3) submit_deadline 到 且 任一轨未提交/已退回 → is_frozen=1 (冻结)
|
||||
* </pre>
|
||||
* 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围.
|
||||
* <p>
|
||||
@@ -28,7 +29,27 @@ public class MeetingStageScheduler
|
||||
private BizMeetingMapper meetingMapper;
|
||||
|
||||
/**
|
||||
* 每分钟: start_time 已过 且 劳务·会务两轨均未提交 且未执行 → 置执行中.
|
||||
* 每分钟: start_time 已过 且 end_time 未到 且 劳务·会务两轨均未提交 且未执行 → 置执行中 (IN_PROGRESS).
|
||||
*/
|
||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||
public void markInProgress()
|
||||
{
|
||||
try
|
||||
{
|
||||
int affected = meetingMapper.markInProgress();
|
||||
if (affected > 0)
|
||||
{
|
||||
log.info("[MeetingStageScheduler] 自动置执行中: 本次更新 {} 行", affected);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("[MeetingStageScheduler] 置执行中异常 (跳过, 下分钟再试)", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每分钟: end_time 已过 且 劳务·会务两轨均未提交 且未执行 → 置已执行 (is_executed=1).
|
||||
*/
|
||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||
public void markExecuted()
|
||||
@@ -38,12 +59,12 @@ public class MeetingStageScheduler
|
||||
int affected = meetingMapper.markExecuted();
|
||||
if (affected > 0)
|
||||
{
|
||||
log.info("[MeetingStageScheduler] 自动置执行中: 本次更新 {} 行", affected);
|
||||
log.info("[MeetingStageScheduler] 自动置已执行: 本次更新 {} 行", affected);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("[MeetingStageScheduler] 置执行中异常 (跳过, 下分钟再试)", e);
|
||||
log.warn("[MeetingStageScheduler] 置已执行异常 (跳过, 下分钟再试)", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -3,27 +3,31 @@ package com.ruoyi.business.scheduler;
|
||||
import java.net.URLEncoder;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.business.supplier.SupplierAccount;
|
||||
import com.ruoyi.business.supplier.SupplierAccountApiCodec;
|
||||
import com.ruoyi.business.supplier.SupplierAccountSyncService;
|
||||
import com.ruoyi.common.utils.http.HttpUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 供应商账号数据拉取调度器: 每分钟拉取最近 5 分钟更新的账号, 解密后打印.
|
||||
* 供应商账号数据拉取调度器: 每分钟拉取最近 N 分钟更新的账号, 解密后同步到执行方侧.
|
||||
* <p>
|
||||
* 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts}
|
||||
* 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回.
|
||||
* 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密.
|
||||
* <p>
|
||||
* 说明: 只打印不落库 (后续需要持久化时再扩展).
|
||||
* 解密后按邮箱 upsert 到 sys_user / biz_org / biz_person (见 {@link SupplierAccountSyncService}).
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -44,6 +48,9 @@ public class SupplierAccountPullScheduler
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private SupplierAccountSyncService supplierAccountSyncService;
|
||||
|
||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||
public void pullAccounts()
|
||||
{
|
||||
@@ -87,8 +94,14 @@ public class SupplierAccountPullScheduler
|
||||
int size = rows.isArray() ? rows.size() : 0;
|
||||
fetched += size;
|
||||
|
||||
// 只打印即可: 整页明文 JSON 打出来 (供观察/后续落库)
|
||||
log.info("[SupplierAccountPull] page={} total={} 本页={} 明文: {}", pageNum, total, size, plain);
|
||||
// 同步到执行方 sys_user / biz_org / biz_person (按邮箱 upsert)
|
||||
if (size > 0)
|
||||
{
|
||||
List<SupplierAccount> accounts =
|
||||
objectMapper.convertValue(rows, new TypeReference<List<SupplierAccount>>() {});
|
||||
supplierAccountSyncService.sync(accounts);
|
||||
}
|
||||
log.info("[SupplierAccountPull] page={} total={} 本页={} 同步完成", pageNum, total, size);
|
||||
|
||||
if (size == 0 || fetched >= total)
|
||||
{
|
||||
|
||||
+21
@@ -28,6 +28,15 @@ public interface IBizMeetingMaterialService {
|
||||
*/
|
||||
List<BizMeetingMaterial> replaceByMeetingId(Long meetingId, List<BizMeetingMaterial> list);
|
||||
|
||||
/**
|
||||
* 判断保存的材料集合相对库里是否有实际变化 (增/删 subType, 或同 subType 的 ossUrl 变化).
|
||||
* <p>
|
||||
* 用于"保存"按钮: 会务材料没变时不应把会议费用置"统计中" (fee_calc_status=0),
|
||||
* 避免无谓的汇总重算与前端"统计中"闪烁.
|
||||
* 必须在 {@link #replaceByMeetingId} 之前调用 (后者会先删旧记录).
|
||||
*/
|
||||
boolean isMaterialSetChanged(Long meetingId, List<BizMeetingMaterial> list);
|
||||
|
||||
/**
|
||||
* 单条更新 amount (OCR 识别为发票后回写).
|
||||
* 不动其他字段, 不抛异常 (失败仅 log).
|
||||
@@ -81,6 +90,18 @@ public interface IBizMeetingMaterialService {
|
||||
*/
|
||||
String buildBatchLaborZipUrl(List<Long> meetingIds);
|
||||
|
||||
/** 单个会务下载文件名 (项目编号_项目名称_第N期_会务.zip) */
|
||||
String serviceZipFilename(Long meetingId);
|
||||
|
||||
/** 单个劳务下载文件名 (项目编号_项目名称_第N期_劳务.zip) */
|
||||
String laborZipFilename(Long meetingId);
|
||||
|
||||
/** 批量会务下载文件名 (会务_下载时间.zip) */
|
||||
String batchServiceZipFilename();
|
||||
|
||||
/** 批量劳务下载文件名 (劳务_下载时间.zip) */
|
||||
String batchLaborZipFilename();
|
||||
|
||||
/**
|
||||
* 生成"会务材料"空目录模板 zip (会务材料 tab "打包上传" 的"下载目录"按钮).
|
||||
* <p>
|
||||
|
||||
+21
-2
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
|
||||
/**
|
||||
@@ -10,6 +11,11 @@ public interface IBizMeetingService
|
||||
{
|
||||
BizMeeting getById(Long meetingId);
|
||||
List<BizMeeting> selectList(BizMeeting entity);
|
||||
/**
|
||||
* 会议阶段统计 (仅 sponsor/Home KPI 用): 按 current_stage 分组计数, 只套 sponsor 数据权限.
|
||||
* 返回 [{currentStage: 'RUNNING', cnt: 10}, ...]. 其他角色的统计另行实现.
|
||||
*/
|
||||
List<Map<String, Object>> selectStageStats(BizMeeting entity);
|
||||
int insert(BizMeeting entity);
|
||||
int updateByPrimaryKey(BizMeeting entity);
|
||||
int deleteByPrimaryKey(Long meetingId);
|
||||
@@ -28,6 +34,19 @@ public interface IBizMeetingService
|
||||
int countByProjectIdAndExecutionUnit(Long projectId, Long executionUnitId);
|
||||
/** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */
|
||||
int countByProjectIdExecutionUnitPeriod(Long projectId, Long executionUnitId, Long periodNo);
|
||||
/** 标记会议费用待重算 (人员/材料变化触发, 幂等; 由 FeeCalcScheduler 汇总回写) */
|
||||
void markFeeCalcPending(Long meetingId);
|
||||
/** 修改校验用 (执行方隔离): 同上, 但排除指定 meetingId (修改自身会议不触发相同期数误报) */
|
||||
int countByProjectIdExecutionUnitPeriodExclude(Long projectId, Long executionUnitId, Long periodNo, Long excludeMeetingId);
|
||||
/**
|
||||
* 立即重算会议费用 (会务材料保存/上传、发票 OCR 完成后同步触发, 不再等 FeeCalcScheduler 每分钟扫).
|
||||
* <p>状态机: -1(计算中) → 算成功 1 / 材料发票仍未 OCR 完或异常 回滚 0 走 FeeCalcScheduler 兜底.
|
||||
* 全程一个事务, 失败不落 -1 残留.
|
||||
*/
|
||||
void recomputeMeetingFee(Long meetingId);
|
||||
/**
|
||||
* 同步重算劳务费 (参会人增删改后立即调用).
|
||||
* <p>labor_fee = 参会人应发金额之和, 纯 DB 求和不依赖 OCR, 可当场算;
|
||||
* total_fee = labor_fee + 当前 meeting_fee. 不动 meeting_fee / fee_calc_status
|
||||
* (参会人变化不影响会务费, 也不触发整体重算, 避免"统计中"等待 60s 调度器).
|
||||
*/
|
||||
void recomputeLaborFee(Long meetingId);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ public interface IBizMessageService
|
||||
/** 收件人的最近消息 (含未读) */
|
||||
List<BizMessage> selectMyRecent(Long receiverUserId, Integer limit);
|
||||
|
||||
/** 收件人的分页消息 (offset/limit, 未读在前) */
|
||||
List<BizMessage> selectMyPage(Long receiverUserId, int offset, int pageSize);
|
||||
|
||||
/** 收件人的全部消息总数 (分页 total) */
|
||||
int countMy(Long receiverUserId);
|
||||
|
||||
/** 收件人未读总数 (SSE 推送用) */
|
||||
int countUnread(Long receiverUserId);
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ public interface IBizPersonService
|
||||
List<BizPerson> selectExecutorList(BizPerson entity);
|
||||
SysUser insert(BizPerson entity, Long mainUserId);
|
||||
int updateByPrimaryKey(BizPerson entity);
|
||||
/** 个人资料编辑: 按 user_id 反查 personId 后走 updateByPrimaryKey (同步 nick_name/phonenumber/email) */
|
||||
int updateProfileByUserId(BizPerson entity);
|
||||
int deleteByPrimaryKey(String personId);
|
||||
int deleteByPrimaryKeys(String[] personId);
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
|
||||
@@ -50,6 +52,8 @@ public class StageDeriver
|
||||
* 10 值物理阶段 (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/
|
||||
* SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN), 由事实推导.
|
||||
* 两轨取最小进度 (流程最靠前的一轨决定会议物理态), 用于 current_stage 缓存 (列表筛选按物理态精确匹配).
|
||||
* <p>注意: IN_PROGRESS(执行中) 是时间窗口态, 由 MeetingStageScheduler 在 start_time 到点直接写 current_stage,
|
||||
* 不在此处由事实推导 (本函数只在已执行后的材料动作里被调, 那时早已越过该窗口).
|
||||
*/
|
||||
public String derivePhysicalStage(BizMeeting m)
|
||||
{
|
||||
@@ -85,7 +89,7 @@ public class StageDeriver
|
||||
if (t(m.getIsSettled())) return "已结算";
|
||||
|
||||
int chosen = chooseState(role, laborState(m), serviceState(m));
|
||||
return render(role, chosen, t(m.getIsExecuted()));
|
||||
return render(role, chosen, executionPhase(m));
|
||||
}
|
||||
|
||||
/** 按角色优先级选代表轨 (两轨中优先级更高的那轨). */
|
||||
@@ -130,15 +134,16 @@ public class StageDeriver
|
||||
}
|
||||
|
||||
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
||||
private static String render(String role, int s, boolean executed)
|
||||
private static String render(String role, int s, int phase)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case 0: // R 退回
|
||||
return "executor".equals(role) ? "已退回" : "待整改";
|
||||
case 1: // N 未提交
|
||||
if (!executed) return "未执行";
|
||||
return "executor".equals(role) ? "执行中" : "已执行未传材料";
|
||||
case 1: // N 未提交 (时间驱动三态: 未执行 → 执行中 → 已执行, 所有角色统一)
|
||||
if (phase == 0) return "未执行";
|
||||
if (phase == 1) return "执行中";
|
||||
return "已执行";
|
||||
case 2: // C0 合规审中
|
||||
if ("sponsor".equals(role)) return "已执行未传材料"; // 只读
|
||||
return "待审核"; // executor / manager / admin
|
||||
@@ -149,4 +154,19 @@ public class StageDeriver
|
||||
return "待结算";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行进度三态: 0=未执行 (now < start_time), 1=执行中 (start_time ≤ now < end_time),
|
||||
* 2=已执行 (now ≥ end_time 或 is_executed=1). 仅用于材料未提交 (N) 的展示措辞.
|
||||
* <p>is_executed=1 是 scheduler 在 end_time 到点落库的「已执行」事实, 优先采信;
|
||||
* 未落库前用 start_time/end_time 现场判断.
|
||||
*/
|
||||
private static int executionPhase(BizMeeting m)
|
||||
{
|
||||
if (t(m.getIsExecuted())) return 2;
|
||||
Date now = new Date();
|
||||
if (m.getEndTime() != null && !m.getEndTime().after(now)) return 2;
|
||||
if (m.getStartTime() != null && !m.getStartTime().after(now)) return 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-3
@@ -108,10 +108,23 @@ public class BizAdminUserServiceImpl implements IBizAdminUserService {
|
||||
return u;
|
||||
}
|
||||
|
||||
/** admin / manager: 纯 sys_user */
|
||||
/** admin: 纯 sys_user; manager: sys_user + biz_person (unit_type='manager', 让 biz_project.create_user_name 反查得到姓名) */
|
||||
private SysUser insertPlain(AdminUserCreateBody b, String role, String encPwd) {
|
||||
SysUser u = baseUser(b, role, encPwd);
|
||||
sysUserService.insertUser(u);
|
||||
if ("manager".equals(role)) {
|
||||
BizPerson p = new BizPerson();
|
||||
SnowflakeId.injectIfEmpty(p, "personId");
|
||||
p.setName(u.getNickName() == null || u.getNickName().isEmpty() ? "用户" : u.getNickName());
|
||||
p.setPhone(b.getPhonenumber() == null ? "" : b.getPhonenumber());
|
||||
p.setDepartment("合规部");
|
||||
p.setPosition("合规员");
|
||||
p.setUnitType("manager");
|
||||
p.setUserId(u.getUserId());
|
||||
p.setCreateBy(SecurityUtils.getUsername());
|
||||
p.setUpdateBy(SecurityUtils.getUsername());
|
||||
bizPersonMapper.insert(p);
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
@@ -170,7 +183,7 @@ public class BizAdminUserServiceImpl implements IBizAdminUserService {
|
||||
|
||||
BizPerson self = new BizPerson();
|
||||
SnowflakeId.injectIfEmpty(self, "personId");
|
||||
self.setName(contactName);
|
||||
self.setName(u.getNickName());
|
||||
self.setPhone(contactPhone);
|
||||
self.setOrgId(org.getOrgId());
|
||||
self.setDepartment("管理部");
|
||||
@@ -195,13 +208,18 @@ public class BizAdminUserServiceImpl implements IBizAdminUserService {
|
||||
if (!role.equals(org.getOrgType())) {
|
||||
throw new ServiceException("所选单位类型与角色不匹配");
|
||||
}
|
||||
// 主账号 user_id: 按 biz_person.org_id + sys_user.account_type='MAIN' 反查, 不取 biz_org.user_id (可能脏/过期)
|
||||
Long mainUserId = bizPersonMapper.selectMainUserIdByOrgId(b.getOrgId());
|
||||
if (mainUserId == null) {
|
||||
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 (单位暂无主账号时留空待分配)
|
||||
u.setParentUserId(mainUserId); // 主账号 user_id (按 account_type='MAIN' 反查)
|
||||
sysUserService.insertUser(u);
|
||||
|
||||
BizPerson p = new BizPerson();
|
||||
|
||||
+29
-3
@@ -121,14 +121,21 @@ public class BizExpertServiceImpl implements IBizExpertService
|
||||
@Override
|
||||
public int updateByPrimaryKey(BizExpert entity)
|
||||
{
|
||||
BizExpert existed = null;
|
||||
String oldAuditStatus = null;
|
||||
if (entity.getExpertId() != null) {
|
||||
BizExpert existed = bizExpertMapper.selectByPrimaryKey(entity.getExpertId());
|
||||
existed = bizExpertMapper.selectByPrimaryKey(entity.getExpertId());
|
||||
if (existed != null) {
|
||||
oldAuditStatus = existed.getAuditStatus();
|
||||
}
|
||||
}
|
||||
int n = bizExpertMapper.updateByPrimaryKey(entity);
|
||||
// 姓名变更 → 同步 sys_user.nick_name (单一可信源 nick_name, biz_expert.name 作镜像)
|
||||
if (n > 0 && entity.getName() != null && !entity.getName().isEmpty()) {
|
||||
Long uid = entity.getUserId() != null ? entity.getUserId()
|
||||
: (existed != null ? existed.getUserId() : null);
|
||||
syncNickName(uid, entity.getName());
|
||||
}
|
||||
if (n > 0 && entity.getAuditStatus() != null
|
||||
&& !entity.getAuditStatus().equals(oldAuditStatus)) {
|
||||
// 重新读一次拿 userId (entity 可能只传了 expertId+auditStatus)
|
||||
@@ -174,12 +181,31 @@ public class BizExpertServiceImpl implements IBizExpertService
|
||||
@Override
|
||||
public int updateProfileByUserId(BizExpert entity)
|
||||
{
|
||||
int n;
|
||||
BizExpert existed = bizExpertMapper.selectByUserId(entity.getUserId());
|
||||
if (existed == null) {
|
||||
return bizExpertMapper.insertWithUserId(entity);
|
||||
n = bizExpertMapper.insertWithUserId(entity);
|
||||
} else {
|
||||
n = bizExpertMapper.updateByUserId(entity);
|
||||
}
|
||||
return bizExpertMapper.updateByUserId(entity);
|
||||
// 姓名变更 → 同步 sys_user.nick_name
|
||||
if (n > 0 && entity.getUserId() != null
|
||||
&& entity.getName() != null && !entity.getName().isEmpty()) {
|
||||
syncNickName(entity.getUserId(), entity.getName());
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** 同步 sys_user.nick_name (单一可信源), userId/name 为空时静默跳过 */
|
||||
private void syncNickName(Long userId, String name) {
|
||||
if (userId == null || name == null || name.isEmpty()) return;
|
||||
SysUser u = new SysUser();
|
||||
u.setUserId(userId);
|
||||
u.setNickName(name);
|
||||
u.setUpdateBy(SecurityUtils.getUsername());
|
||||
sysUserService.updateUser(u);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByPrimaryKey(Long expertId)
|
||||
{ return bizExpertMapper.deleteByPrimaryKey(expertId); }
|
||||
|
||||
+141
-7
@@ -25,11 +25,13 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.business.domain.BizExpert;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||
import com.ruoyi.business.domain.BizProject;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
import com.ruoyi.business.domain.vo.BizMeetingAttendeeImportVo;
|
||||
import com.ruoyi.business.mapper.BizExpertMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||
import com.ruoyi.business.mapper.BizProjectMapper;
|
||||
@@ -61,6 +63,8 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
private SysUserMapper sysUserMapper;
|
||||
@Autowired
|
||||
private ISysUserService sysUserService;
|
||||
@Autowired
|
||||
private BizExpertMapper bizExpertMapper;
|
||||
|
||||
/** Jackson (Spring Boot 自带), 解析 biz_project.role_labor JSON 数组 [{role, customName, amount}] */
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
@@ -116,6 +120,10 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
}
|
||||
phone = phone.trim();
|
||||
|
||||
// 校验实发金额不超所选角色劳务金额合计 (与手动新增弹窗 MeetingDetail.validateFee 一致;
|
||||
// 无角色/无金额/项目无 role_labor 时跳过)
|
||||
validateFeeAgainstRole(loadRoleLabor(body.getMeetingId()), body.getLaborForm(), body.getFee());
|
||||
|
||||
// 1. 按 phone 查 sys_user (单条 IN 查, selectByPhoneList 接受 List<String>)
|
||||
List<SysUser> hits = sysUserMapper.selectByPhoneList(Collections.singletonList(phone));
|
||||
Long userId;
|
||||
@@ -152,6 +160,9 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
throw new ServiceException("该手机号参会人已在会议中, 无需重复添加");
|
||||
}
|
||||
|
||||
// 3.5 确保专家档案存在 (不存在则新建, 存在则补齐空字段), 让参会人在专家库可见
|
||||
ensureExpertProfile(body, userId);
|
||||
|
||||
// 4. 写完整档案行 (attendee.id 用雪花 ID, 不走 DB 自增)
|
||||
body.setUserId(userId);
|
||||
body.setCreateBy(SecurityUtils.getUsername());
|
||||
@@ -162,8 +173,71 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保参会人对应的 biz_expert 记录存在且字段尽量完整:
|
||||
* - 不存在 → 新建 (status='Y' 正常, auditStatus='2' 通过, 与 importExpert 约定一致)
|
||||
* - 存在但部分字段为空 → 只用参会人档案里的非空值补齐空字段, 不覆盖已有值
|
||||
*/
|
||||
private void ensureExpertProfile(BizMeetingAttendee body, Long userId) {
|
||||
if (userId == null) return;
|
||||
BizExpert existed = bizExpertMapper.selectByUserId(userId);
|
||||
if (existed == null) {
|
||||
BizExpert e = new BizExpert();
|
||||
e.setExpertId(IdGenerator.generateId());
|
||||
e.setUserId(userId);
|
||||
e.setName(isBlank(body.getName()) ? body.getPhone() : body.getName());
|
||||
e.setPhone(body.getPhone());
|
||||
e.setWorkUnit(body.getWorkUnit());
|
||||
e.setDepartment(body.getDepartment());
|
||||
e.setTitle(body.getTitle());
|
||||
e.setIdCard(body.getIdCard());
|
||||
e.setBankCard(body.getBankCard());
|
||||
e.setBankName(body.getBankName());
|
||||
e.setBankBranch(body.getBankBranch());
|
||||
e.setBankRegion(body.getBankRegion());
|
||||
e.setBankAddress(body.getBankAddress());
|
||||
e.setIdCardAttachments(body.getIdCardAttachments());
|
||||
e.setStatus("Y");
|
||||
e.setAuditStatus("2");
|
||||
e.setAuditBy(SecurityUtils.getUsername());
|
||||
e.setAuditTime(new Date());
|
||||
e.setCreateBy(SecurityUtils.getUsername());
|
||||
e.setCreateTime(new Date());
|
||||
bizExpertMapper.insert(e);
|
||||
return;
|
||||
}
|
||||
// 补充: 只填 biz_expert 里为空的字段, 已有值不覆盖
|
||||
BizExpert u = new BizExpert();
|
||||
u.setUserId(userId);
|
||||
boolean changed = false;
|
||||
if (isBlank(existed.getName()) && !isBlank(body.getName())) { u.setName(body.getName()); changed = true; }
|
||||
if (isBlank(existed.getPhone()) && !isBlank(body.getPhone())) { u.setPhone(body.getPhone()); changed = true; }
|
||||
if (isBlank(existed.getWorkUnit()) && !isBlank(body.getWorkUnit())) { u.setWorkUnit(body.getWorkUnit()); changed = true; }
|
||||
if (isBlank(existed.getDepartment()) && !isBlank(body.getDepartment())) { u.setDepartment(body.getDepartment()); changed = true; }
|
||||
if (isBlank(existed.getTitle()) && !isBlank(body.getTitle())) { u.setTitle(body.getTitle()); changed = true; }
|
||||
if (isBlank(existed.getIdCard()) && !isBlank(body.getIdCard())) { u.setIdCard(body.getIdCard()); changed = true; }
|
||||
if (isBlank(existed.getBankCard()) && !isBlank(body.getBankCard())) { u.setBankCard(body.getBankCard()); changed = true; }
|
||||
if (isBlank(existed.getBankName()) && !isBlank(body.getBankName())) { u.setBankName(body.getBankName()); changed = true; }
|
||||
if (isBlank(existed.getBankBranch()) && !isBlank(body.getBankBranch())) { u.setBankBranch(body.getBankBranch()); changed = true; }
|
||||
if (isBlank(existed.getBankRegion()) && !isBlank(body.getBankRegion())) { u.setBankRegion(body.getBankRegion()); changed = true; }
|
||||
if (isBlank(existed.getBankAddress()) && !isBlank(body.getBankAddress())) { u.setBankAddress(body.getBankAddress()); changed = true; }
|
||||
if (isBlank(existed.getIdCardAttachments()) && !isBlank(body.getIdCardAttachments())) { u.setIdCardAttachments(body.getIdCardAttachments()); changed = true; }
|
||||
if (changed) {
|
||||
bizExpertMapper.updateByUserId(u);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); }
|
||||
|
||||
@Override
|
||||
public int updateProfile(BizMeetingAttendee entity) {
|
||||
Long meetingId = entity.getMeetingId();
|
||||
if (meetingId == null) {
|
||||
BizMeetingAttendee old = mapper.selectById(entity.getId());
|
||||
if (old != null) meetingId = old.getMeetingId();
|
||||
}
|
||||
// 校验实发金额不超所选角色劳务金额合计 (与手动新增弹窗 MeetingDetail.validateFee 一致)
|
||||
validateFeeAgainstRole(loadRoleLabor(meetingId), entity.getLaborForm(), entity.getFee());
|
||||
return mapper.updateProfile(entity);
|
||||
}
|
||||
|
||||
@@ -304,18 +378,36 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
}
|
||||
|
||||
/**
|
||||
* 在项目角色劳务 JSON 数组 [{role, customName, amount}] 里按角色名匹配劳务金额.
|
||||
* 解析某会议的 project.role_labor JSON 数组, 失败/无数据返回 null.
|
||||
* 供"实发金额 ≤ 角色设置金额"校验复用 (单条 add/edit 各自加载).
|
||||
*/
|
||||
private JsonNode loadRoleLabor(Long meetingId) {
|
||||
if (meetingId == null) return null;
|
||||
try {
|
||||
BizMeeting meeting = meetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (meeting == null || meeting.getProjectId() == null) return null;
|
||||
BizProject project = projectMapper.selectByPrimaryKey(meeting.getProjectId());
|
||||
if (project == null || project.getRoleLabor() == null || project.getRoleLabor().trim().isEmpty()) return null;
|
||||
return objectMapper.readTree(project.getRoleLabor());
|
||||
} catch (Exception e) {
|
||||
log.warn("[attendee] 解析项目角色劳务失败 meetingId={}", meetingId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在项目角色劳务 JSON 数组 [{role, customName, amount}] 里按单个角色名匹配劳务金额.
|
||||
* 匹配规则与前端 ProjectRoleSelect 一致: role === '其他' 时用 customName 作 label, 否则用 role.
|
||||
*
|
||||
* @param nodes 已解析的 role_labor JSON (可为 null/非数组)
|
||||
* @param laborForm 参会人填的角色名 (可为 null/空)
|
||||
* @param roleLabel 单个角色名 (可为 null/空)
|
||||
* @return 匹配到的 amount (BigDecimal); 没匹配到或 amount 非法 → null
|
||||
*/
|
||||
private BigDecimal findRoleAmount(JsonNode nodes, String laborForm) {
|
||||
if (nodes == null || !nodes.isArray() || laborForm == null || laborForm.trim().isEmpty()) {
|
||||
private BigDecimal matchRoleAmount(JsonNode nodes, String roleLabel) {
|
||||
if (nodes == null || !nodes.isArray() || roleLabel == null || roleLabel.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String target = laborForm.trim();
|
||||
String target = roleLabel.trim();
|
||||
for (JsonNode n : nodes) {
|
||||
if (n == null || n.isNull()) continue;
|
||||
String role = n.path("role").asText("");
|
||||
@@ -334,6 +426,47 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在项目角色劳务 JSON 里按角色名匹配劳务金额 (导入自动带出金额用, 单个角色).
|
||||
* 保留原 findRoleAmount 语义: 单个 laborForm 精确匹配.
|
||||
*/
|
||||
private BigDecimal findRoleAmount(JsonNode nodes, String laborForm) {
|
||||
return matchRoleAmount(nodes, laborForm);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参会人角色 (laborForm, 可能逗号分隔多选) 对应的角色劳务金额合计.
|
||||
* 与前端 MeetingDetail.roleAmountSum 一致: 按逗号拆分逐个匹配求和.
|
||||
*
|
||||
* @return 金额合计; nodes 为空/非数组 → null (无 role_labor 数据, 无法比较)
|
||||
*/
|
||||
private BigDecimal sumRoleAmount(JsonNode nodes, String laborForm) {
|
||||
if (nodes == null || !nodes.isArray() || laborForm == null || laborForm.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
for (String item : laborForm.split(",")) {
|
||||
BigDecimal a = matchRoleAmount(nodes, item.trim());
|
||||
if (a != null) sum = sum.add(a);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验实发金额(fee)不超所选角色劳务金额合计 (与前端 MeetingDetail.validateFee 一致).
|
||||
* laborForm 为空 / fee 为空 / 项目无 role_labor 数据时跳过 (无"设置金额"可比较).
|
||||
*/
|
||||
private void validateFeeAgainstRole(JsonNode roleLaborNodes, String laborForm, BigDecimal fee) {
|
||||
if (laborForm == null || laborForm.trim().isEmpty()) return;
|
||||
if (fee == null) return;
|
||||
BigDecimal sum = sumRoleAmount(roleLaborNodes, laborForm);
|
||||
if (sum == null) return;
|
||||
if (fee.compareTo(sum) > 0) {
|
||||
throw new ServiceException("实发金额不能超过角色金额 "
|
||||
+ sum.setScale(2, RoundingMode.HALF_UP).toPlainString() + " 元");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入参会人 (Excel → biz_meeting_attendee).
|
||||
*
|
||||
@@ -467,11 +600,12 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
}
|
||||
String meetingName = m != null ? m.getMeetingName() : null;
|
||||
Date startTime = m != null ? m.getStartTime() : null;
|
||||
Date endTime = m != null ? m.getEndTime() : null;
|
||||
|
||||
// 1. 短信 (电子签模板: name=参会人姓名, date=会议日期, link=签署链接)
|
||||
// 1. 短信 (邀请签署模板: name=参会人姓名, time=会议时间, attendeeId=参会人id)
|
||||
String phone = a.getPhone();
|
||||
if (phone != null && !phone.trim().isEmpty()) {
|
||||
aliyunSmsSender.sendEsign(phone.trim(), a.getName(), startTime, aliyunSmsSender.esignLink(id));
|
||||
aliyunSmsSender.sendEsign(phone.trim(), a.getName(), startTime, endTime, id);
|
||||
}
|
||||
// 2. 站内信 (劳务协议待签署 + 签署链接, 与短信同一链接)
|
||||
bizNotifyService.esignPushed(a.getUserId(), id, mid, meetingName, aliyunSmsSender.esignLink(id));
|
||||
|
||||
+140
-21
@@ -5,12 +5,14 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.net.URI;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -202,6 +204,45 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMaterialSetChanged(Long meetingId, List<BizMeetingMaterial> list) {
|
||||
// 只关心"影响会务费"的服务类发票 (M_*): 劳务材料/凭证/现场照片等不影响会务费, 直接忽略.
|
||||
// 会务费口径 (见 FeeCalcScheduler.computeMeetingFee): 有总发票 M_INVOICE 就用它, 否则各 M_* 子发票之和.
|
||||
List<BizMeetingMaterial> oldList = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||
Map<String, String> oldUrl = new HashMap<>();
|
||||
if (oldList != null) {
|
||||
for (BizMeetingMaterial o : oldList) {
|
||||
if (isFeeRelevant(o.getSubType())) oldUrl.put(o.getSubType(), o.getOssUrl());
|
||||
}
|
||||
}
|
||||
Map<String, String> newUrl = new HashMap<>();
|
||||
if (list != null) {
|
||||
for (BizMeetingMaterial m : list) {
|
||||
if (isFeeRelevant(m.getSubType())) newUrl.put(m.getSubType(), m.getOssUrl());
|
||||
}
|
||||
}
|
||||
boolean hasInvoice = oldUrl.containsKey("M_INVOICE") || newUrl.containsKey("M_INVOICE");
|
||||
if (hasInvoice) {
|
||||
// 有总发票: 会务费只看总发票, 其余子发票被覆盖 → 仅总发票变化才算变
|
||||
return !Objects.equals(oldUrl.get("M_INVOICE"), newUrl.get("M_INVOICE"));
|
||||
}
|
||||
// 无总发票: 任一 M_* 子发票 (排除 M_INVOICE / M_SETTLEMENT 两个汇总单据) 增删或换 URL 都算变
|
||||
Set<String> subs = new HashSet<>();
|
||||
subs.addAll(oldUrl.keySet());
|
||||
subs.addAll(newUrl.keySet());
|
||||
subs.remove("M_INVOICE");
|
||||
subs.remove("M_SETTLEMENT");
|
||||
for (String sub : subs) {
|
||||
if (!Objects.equals(oldUrl.get(sub), newUrl.get(sub))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 是否影响会务费: 仅服务类发票 M_* (劳务 L_*、凭证 LV/SV、结算单等都不算) */
|
||||
private static boolean isFeeRelevant(String subType) {
|
||||
return subType != null && subType.startsWith("M_");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateAmount(Long materialId, BigDecimal amount) {
|
||||
if (materialId == null || amount == null) return 0;
|
||||
@@ -217,7 +258,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
/**
|
||||
* 扫码拍照回传: ry-h5 手机端拍照直传 OSS 后回传 URL, 按 (meetingId, subType) upsert 单行.
|
||||
* 公开端点 (匿名) — 白名单 subType + 会议存在校验兜底.
|
||||
* 照片类 NON_OCR, 不触发 OCR, 不影响会议费用, 故不 markFeeCalcPending.
|
||||
* 照片类 NON_OCR, 不触发 OCR, 不影响会议费用, 故不触发费用重算.
|
||||
* extraOssUrl: 签到表(L_SIGN_IN)拍照时额外生成的高斯模糊版 URL, sponsor 只看这个; 其他 subType 传空.
|
||||
*/
|
||||
@Override
|
||||
@@ -309,9 +350,10 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (meeting == null) throw new ServiceException("会议不存在");
|
||||
String prefix = "download/huiwu/" + meetingId + "/";
|
||||
String name = singleZipName(meeting, "会务");
|
||||
String prefix = "download/huiwu/" + name + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
int copied = stageServiceMaterials(meetingId, prefix, projectFolderName(meeting));
|
||||
int copied = stageServiceMaterials(meetingId, prefix, name);
|
||||
if (copied == 0) throw new ServiceException("该会议暂无可下载的会务材料");
|
||||
return ossZipService.zipDownload(prefix);
|
||||
}
|
||||
@@ -327,9 +369,11 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (meeting == null) throw new ServiceException("会议不存在");
|
||||
String prefix = "download/labor/" + meetingId + "/";
|
||||
String name = singleZipName(meeting, "劳务");
|
||||
String prefix = "download/labor/" + name + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
int copied = stageLaborMaterials(meetingId, prefix, projectFolderName(meeting));
|
||||
int copied = stageLaborMaterials(meetingId, prefix, name);
|
||||
copied += stageSchedulePoster(meeting, prefix, name);
|
||||
if (copied == 0) throw new ServiceException("该会议暂无可下载的劳务材料");
|
||||
return ossZipService.zipDownload(prefix);
|
||||
}
|
||||
@@ -343,7 +387,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
public String buildBatchServiceZipUrl(List<Long> meetingIds)
|
||||
{
|
||||
List<Long> ids = normalizeIds(meetingIds);
|
||||
String prefix = "download/huiwu/batch/" + System.currentTimeMillis() + "/";
|
||||
String prefix = "download/huiwu/batch/" + batchZipName("会务") + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
Set<String> usedFolders = new HashSet<>();
|
||||
int copied = 0;
|
||||
@@ -362,7 +406,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
public String buildBatchLaborZipUrl(List<Long> meetingIds)
|
||||
{
|
||||
List<Long> ids = normalizeIds(meetingIds);
|
||||
String prefix = "download/labor/batch/" + System.currentTimeMillis() + "/";
|
||||
String prefix = "download/labor/batch/" + batchZipName("劳务") + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
Set<String> usedFolders = new HashSet<>();
|
||||
int copied = 0;
|
||||
@@ -370,12 +414,40 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(id);
|
||||
if (meeting == null) continue;
|
||||
copied += stageLaborMaterials(id, prefix, uniqueFolder(usedFolders, batchMeetingFolderName(meeting)));
|
||||
String folder = uniqueFolder(usedFolders, batchMeetingFolderName(meeting));
|
||||
copied += stageLaborMaterials(id, prefix, folder);
|
||||
copied += stageSchedulePoster(meeting, prefix, folder);
|
||||
}
|
||||
if (copied == 0) throw new ServiceException("所选会议暂无可下载的劳务材料");
|
||||
return ossZipService.zipDownload(prefix);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 下载文件名 (FC 固定命名 output_1-xxx.zip, 需后端代理改名下发)
|
||||
// ===================================================================
|
||||
|
||||
@Override
|
||||
public String serviceZipFilename(Long meetingId)
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (meeting == null) throw new ServiceException("会议不存在");
|
||||
return singleZipName(meeting, "会务") + ".zip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String laborZipFilename(Long meetingId)
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (meeting == null) throw new ServiceException("会议不存在");
|
||||
return singleZipName(meeting, "劳务") + ".zip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String batchServiceZipFilename() { return batchZipName("会务") + ".zip"; }
|
||||
|
||||
@Override
|
||||
public String batchLaborZipFilename() { return batchZipName("劳务") + ".zip"; }
|
||||
|
||||
/** 收集该会议会务材料并 copy 到 staging 前缀 prefix + folderName 下, 返回 copy 的文件数 (0 = 无会务材料) */
|
||||
private int stageServiceMaterials(Long meetingId, String prefix, String folderName)
|
||||
{
|
||||
@@ -432,16 +504,38 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
return copied;
|
||||
}
|
||||
|
||||
/** 单会议 zip 顶层目录名: 项目名 (为空回退项目编号, 再回退会议ID), 统一消毒 */
|
||||
private static String projectFolderName(BizMeeting meeting)
|
||||
/** 把会议日程海报 (biz_meeting.schedule_url) staging 到 zip 的"日程海报"目录, 返回写入的文件数 (0 = 无日程海报) */
|
||||
private int stageSchedulePoster(BizMeeting meeting, String prefix, String folderName)
|
||||
{
|
||||
String name = meeting.getProjectName();
|
||||
if (name == null || name.trim().isEmpty())
|
||||
if (meeting == null || meeting.getScheduleUrl() == null || meeting.getScheduleUrl().isEmpty()) return 0;
|
||||
String srcKey = ossZipService.extractKey(meeting.getScheduleUrl());
|
||||
if (srcKey == null || srcKey.isEmpty()) return 0;
|
||||
String ext = extOf(srcKey);
|
||||
String dstKey = prefix + folderName + "/日程海报/日程海报" + ext;
|
||||
ossZipService.copyObject(srcKey, dstKey);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** 单会议下载命名 (zip 文件名 + 顶层目录): 项目编号_项目名称_第N期_{suffix} (缺项跳过, 全空回退会议ID), 统一消毒 */
|
||||
private static String singleZipName(BizMeeting meeting, String suffix)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendPart(sb, meeting.getProjectNo());
|
||||
appendPart(sb, meeting.getProjectName());
|
||||
if (meeting.getPeriodNo() != null)
|
||||
{
|
||||
name = (meeting.getProjectNo() != null && !meeting.getProjectNo().trim().isEmpty())
|
||||
? meeting.getProjectNo() : "会议" + meeting.getMeetingId();
|
||||
appendPart(sb, "第" + meeting.getPeriodNo() + "期");
|
||||
}
|
||||
return safeName(name);
|
||||
String base = sb.toString();
|
||||
if (base.isEmpty()) base = "会议" + meeting.getMeetingId();
|
||||
return safeName(base) + "_" + suffix;
|
||||
}
|
||||
|
||||
/** 批量下载命名 (zip 文件名): {suffix}_下载时间 (yyyyMMdd_HHmmss) */
|
||||
private static String batchZipName(String suffix)
|
||||
{
|
||||
String time = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
|
||||
return suffix + "_" + time;
|
||||
}
|
||||
|
||||
/** 批量 zip 每个会议顶层目录名: 项目编号_会议名_第N期 (缺项跳过, 全空回退会议ID) */
|
||||
@@ -586,24 +680,33 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
if (files == null || files.isEmpty()) continue;
|
||||
List<String> names = folderNames.get(subType);
|
||||
String label = SERVICE_SUBTYPE_LABEL.getOrDefault(subType, subType);
|
||||
BizMeetingMaterial old = existingBySubType.get(subType);
|
||||
|
||||
String ossUrl;
|
||||
// 先算好要上传的字节 + 文件名 (单文件原样 / 多文件先打 zip), 便于做"内容未变"判断
|
||||
String fileName;
|
||||
byte[] toUpload;
|
||||
boolean isZip = files.size() > 1;
|
||||
if (files.size() == 1) {
|
||||
fileName = (names != null && !names.isEmpty()) ? names.get(0) : (label + ".jpg");
|
||||
ossUrl = ossUploader.upload(files.get(0), fileName, subDir);
|
||||
toUpload = files.get(0);
|
||||
} else {
|
||||
// 目录内多文件 → 先打 zip 再上传 OSS
|
||||
fileName = label + ".zip";
|
||||
ossUrl = ossUploader.upload(zipFiles(files, names), fileName, subDir);
|
||||
toUpload = zipFiles(files, names);
|
||||
}
|
||||
|
||||
// 内容未变且已计算过 (fee_status=1) → 跳过重传重 OCR, 保留旧金额, 不触发会务费重算
|
||||
if (old != null && old.getFeeStatus() != null && old.getFeeStatus() == 1
|
||||
&& contentEquals(old.getOssUrl(), toUpload)) {
|
||||
log.info("[material] 会务材料 {} 内容未变, 跳过重传重 OCR", subType);
|
||||
continue;
|
||||
}
|
||||
|
||||
String ossUrl = ossUploader.upload(toUpload, fileName, subDir);
|
||||
|
||||
// 会务材料都是发票类 (M_* 不在 NON_OCR 集合): 单文件可识别 (jpg/png/pdf) 或多文件打 zip → 待 OCR(0), 其余已计算(1)
|
||||
boolean isZip = files.size() > 1;
|
||||
boolean ocrNeeded = isZip || (ossUrl != null && RECOGNIZABLE.matcher(ossUrl).find());
|
||||
int feeStatus = ocrNeeded ? 0 : 1;
|
||||
|
||||
BizMeetingMaterial old = existingBySubType.get(subType);
|
||||
Long materialId;
|
||||
Long oldMaterialId;
|
||||
if (old != null) {
|
||||
@@ -696,6 +799,22 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/** 下载 OSS 旧文件并与新文件字节比对是否一致. 下载失败按"不一致"处理 (走正常重传), 不阻塞打包上传. */
|
||||
private boolean contentEquals(String ossUrl, byte[] newBytes) {
|
||||
if (ossUrl == null || ossUrl.isEmpty() || newBytes == null) return false;
|
||||
try {
|
||||
java.net.URLConnection conn = URI.create(ossUrl).toURL().openConnection();
|
||||
conn.setConnectTimeout(10000);
|
||||
conn.setReadTimeout(30000);
|
||||
try (InputStream in = conn.getInputStream()) {
|
||||
return Arrays.equals(readAllBytes(in), newBytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[material] 下载旧材料对比失败, 按不一致处理 url={} err={}", ossUrl, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否会被前端提交 OCR (与 MeetingDetail.saveMaterials 门控一致):
|
||||
* 文件可识别 (jpg/jpeg/png/pdf) 且 不在非发票 subType 集合里.
|
||||
|
||||
+85
-3
@@ -1,14 +1,19 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingSupervisorMapper;
|
||||
@@ -23,6 +28,8 @@ import com.ruoyi.common.utils.id.IdGenerator;
|
||||
@Service
|
||||
public class BizMeetingServiceImpl implements IBizMeetingService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(BizMeetingServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private BizMeetingMapper bizMeetingMapper;
|
||||
@Autowired
|
||||
@@ -48,6 +55,9 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
||||
public List<BizMeeting> selectList(BizMeeting entity)
|
||||
{ return bizMeetingMapper.selectList(entity); }
|
||||
@Override
|
||||
public List<Map<String, Object>> selectStageStats(BizMeeting entity)
|
||||
{ return bizMeetingMapper.selectStageStats(entity); }
|
||||
@Override
|
||||
public int insert(BizMeeting entity) {
|
||||
// meetingId: 从 DB AUTO_INCREMENT 改为应用赋值 — 10 位数字会议ID = 开始日期(yyMMdd) + 4 位序列号(0001 起, 每开始日期 Redis 独立计数)
|
||||
if (entity.getMeetingId() == null) {
|
||||
@@ -145,9 +155,81 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
||||
{ return bizMeetingMapper.countByProjectIdExecutionUnitPeriod(projectId, executionUnitId, periodNo); }
|
||||
|
||||
@Override
|
||||
public void markFeeCalcPending(Long meetingId) {
|
||||
if (meetingId != null) {
|
||||
bizMeetingMapper.markFeeCalcPending(meetingId);
|
||||
public int countByProjectIdExecutionUnitPeriodExclude(Long projectId, Long executionUnitId, Long periodNo, Long excludeMeetingId)
|
||||
{ return bizMeetingMapper.countByProjectIdExecutionUnitPeriodExclude(projectId, executionUnitId, periodNo, excludeMeetingId); }
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void recomputeMeetingFee(Long meetingId) {
|
||||
if (meetingId == null) return;
|
||||
// ① 置计算中 -1, 让 FeeCalcScheduler (只扫 =0) 不并发重复算
|
||||
bizMeetingMapper.updateFeeCalcStatus(meetingId, -1);
|
||||
try {
|
||||
List<BizMeetingMaterial> mats = materialMapper.selectByMeetingId(meetingId);
|
||||
// ② 任一材料发票仍未 OCR 完 (fee_status=0) → 会务费金额未定, 回滚 0 走调度器兜底 (选项 A)
|
||||
boolean ocrReady = true;
|
||||
if (mats != null) {
|
||||
for (BizMeetingMaterial m : mats) {
|
||||
if (m.getFeeStatus() != null && m.getFeeStatus() == 0) {
|
||||
ocrReady = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ocrReady) {
|
||||
bizMeetingMapper.updateFeeCalcStatus(meetingId, 0);
|
||||
return;
|
||||
}
|
||||
// ③ 汇总: labor_fee + meeting_fee → total_fee, 成功置 1 (updateFeeSummary 内含)
|
||||
BigDecimal laborFee = attendeeMapper.sumFeePreTaxByMeetingId(meetingId);
|
||||
if (laborFee == null) laborFee = BigDecimal.ZERO;
|
||||
BigDecimal meetingFee = computeMeetingFee(mats);
|
||||
BigDecimal totalFee = laborFee.add(meetingFee);
|
||||
bizMeetingMapper.updateFeeSummary(meetingId, laborFee, meetingFee, totalFee);
|
||||
log.info("[recomputeMeetingFee] 立即汇总完成 meetingId={} labor={} meeting={} total={}",
|
||||
meetingId, laborFee, meetingFee, totalFee);
|
||||
} catch (Exception e) {
|
||||
// ④ 算错了 → 回滚 0, 交给 FeeCalcScheduler 下分钟兜底 (不让 -1 残留卡死)
|
||||
log.warn("[recomputeMeetingFee] 立即汇总失败, 回滚 0 走调度器兜底 meetingId={} err={}", meetingId, e.getMessage(), e);
|
||||
bizMeetingMapper.updateFeeCalcStatus(meetingId, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会务费口径: 有总发票 (M_INVOICE 金额>0) 则只用总发票; 否则各 SUB 子类 (M_ 开头) 金额之和,
|
||||
* 排除 M_INVOICE / M_SETTLEMENT (两者是汇总单据, 非子类发票).
|
||||
*/
|
||||
private BigDecimal computeMeetingFee(List<BizMeetingMaterial> mats) {
|
||||
if (mats == null || mats.isEmpty()) return BigDecimal.ZERO;
|
||||
BigDecimal main = null;
|
||||
for (BizMeetingMaterial m : mats) {
|
||||
if ("M_INVOICE".equals(m.getSubType())) {
|
||||
main = m.getAmount();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (main != null && main.compareTo(BigDecimal.ZERO) > 0) {
|
||||
return main;
|
||||
}
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
for (BizMeetingMaterial m : mats) {
|
||||
if (m.getSubType() == null || !m.getSubType().startsWith("M_")) continue;
|
||||
if ("M_INVOICE".equals(m.getSubType()) || "M_SETTLEMENT".equals(m.getSubType())) continue;
|
||||
if (m.getAmount() != null) sum = sum.add(m.getAmount());
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void recomputeLaborFee(Long meetingId) {
|
||||
if (meetingId == null) return;
|
||||
BigDecimal laborFee = attendeeMapper.sumFeePreTaxByMeetingId(meetingId);
|
||||
if (laborFee == null) laborFee = BigDecimal.ZERO;
|
||||
BizMeeting m = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (m == null) return;
|
||||
BigDecimal meetingFee = m.getMeetingFee() != null ? m.getMeetingFee() : BigDecimal.ZERO;
|
||||
BigDecimal totalFee = laborFee.add(meetingFee);
|
||||
bizMeetingMapper.updateLaborFee(meetingId, laborFee, totalFee);
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -38,6 +38,22 @@ public class BizMessageServiceImpl implements IBizMessageService
|
||||
return bizMessageMapper.selectMyRecent(q);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMessage> selectMyPage(Long receiverUserId, int offset, int pageSize)
|
||||
{
|
||||
BizMessage q = new BizMessage();
|
||||
q.setReceiverUserId(receiverUserId);
|
||||
q.getParams().put("offset", offset);
|
||||
q.getParams().put("limit", pageSize);
|
||||
return bizMessageMapper.selectMyRecent(q);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countMy(Long receiverUserId)
|
||||
{
|
||||
return bizMessageMapper.countMy(receiverUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countUnread(Long receiverUserId)
|
||||
{
|
||||
|
||||
+24
-1
@@ -34,12 +34,35 @@ public class BizOrgServiceImpl implements IBizOrgService {
|
||||
|
||||
@Override
|
||||
public int insert(BizOrg entity) {
|
||||
// 单位名称查重: 同 orgType 下 org_name 不能重复 (trim 后精确匹配)
|
||||
String orgName = entity.getOrgName() == null ? null : entity.getOrgName().trim();
|
||||
if (orgName != null && !orgName.isEmpty()) {
|
||||
BizOrg probe = new BizOrg();
|
||||
probe.setOrgType(entity.getOrgType());
|
||||
probe.setOrgName(orgName);
|
||||
if (bizOrgMapper.countByOrgName(probe) > 0) {
|
||||
throw new ServiceException("单位名称「" + orgName + "」已存在");
|
||||
}
|
||||
}
|
||||
// 主键由 AUTO_INCREMENT 自增, 不需要 Snowflake
|
||||
return bizOrgMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateByPrimaryKey(BizOrg entity) { return bizOrgMapper.updateByPrimaryKey(entity); }
|
||||
public int updateByPrimaryKey(BizOrg entity) {
|
||||
// 改名时同样查重: 同 orgType 下 org_name 不能重复 (trim 精确匹配), 排除自身
|
||||
String orgName = entity.getOrgName() == null ? null : entity.getOrgName().trim();
|
||||
if (orgName != null && !orgName.isEmpty()) {
|
||||
BizOrg probe = new BizOrg();
|
||||
probe.setOrgType(entity.getOrgType());
|
||||
probe.setOrgName(orgName);
|
||||
probe.setOrgId(entity.getOrgId());
|
||||
if (bizOrgMapper.countByOrgName(probe) > 0) {
|
||||
throw new ServiceException("单位名称「" + orgName + "」已存在");
|
||||
}
|
||||
}
|
||||
return bizOrgMapper.updateByPrimaryKey(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByPrimaryKeys(Long[] orgIds) {
|
||||
|
||||
+21
-1
@@ -79,6 +79,15 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
}
|
||||
|
||||
// 1. 创建 sys_user 子账号
|
||||
// 归属父账号: 优先取所属机构主账号 (biz_org.user_id), 而非 getUserId() (当前登录人).
|
||||
// 否则 admin/manager 在 sponsor-people 建人时, 子账号会错挂到 admin 自己名下,
|
||||
// 导致 sponsor 主账号在自己的 sponsor/people 页 (parent_user_id 过滤) 看不到这些人.
|
||||
Long parentUid = mainUserId;
|
||||
BizOrg ownerOrg = bizOrgMapper.selectByPrimaryKey(entity.getOrgId());
|
||||
if (ownerOrg != null && ownerOrg.getUserId() != null) {
|
||||
parentUid = ownerOrg.getUserId();
|
||||
}
|
||||
|
||||
// role_type 取 person.unitType (sponsor/executor/doctor), 而非继承创建者角色:
|
||||
// 否则 admin/manager 在 admin/sponsor-people 建人会把子账号错位成 admin/manager (后台管理员).
|
||||
// 仅当 unitType 缺失时才回退到主账号 role_type 兜底.
|
||||
@@ -94,7 +103,7 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
newUser.setEmail(entity.getEmail());
|
||||
newUser.setPassword(SecurityUtils.encryptPassword(entity.getLoginPassword()));
|
||||
newUser.setAccountType("SUB");
|
||||
newUser.setParentUserId(mainUserId);
|
||||
newUser.setParentUserId(parentUid);
|
||||
newUser.setStatus("0");
|
||||
newUser.setDelFlag("0");
|
||||
if (roleType != null) {
|
||||
@@ -139,6 +148,17 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
return n;
|
||||
}
|
||||
|
||||
/** 个人资料编辑: 按 user_id 反查 personId, 再复用 updateByPrimaryKey (更新 person + 同步 nick_name/phonenumber/email) */
|
||||
@Override
|
||||
public int updateProfileByUserId(BizPerson entity) {
|
||||
BizPerson existed = bizPersonMapper.selectByUserId(entity.getUserId());
|
||||
if (existed == null) {
|
||||
throw new ServiceException("未找到人员档案");
|
||||
}
|
||||
entity.setPersonId(existed.getPersonId());
|
||||
return updateByPrimaryKey(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByPrimaryKey(String personId)
|
||||
{
|
||||
|
||||
+14
-1
@@ -1,8 +1,10 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizProjectPlan;
|
||||
import com.ruoyi.business.mapper.BizProjectPlanMapper;
|
||||
import com.ruoyi.business.notify.BizNotifyService;
|
||||
@@ -23,7 +25,14 @@ public class BizProjectPlanServiceImpl implements IBizProjectPlanService
|
||||
public List<BizProjectPlan> selectList(BizProjectPlan entity)
|
||||
{ return bizProjectPlanMapper.selectList(entity); }
|
||||
@Override
|
||||
public int insert(BizProjectPlan entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "planId"); return bizProjectPlanMapper.insert(entity); }
|
||||
public int insert(BizProjectPlan entity)
|
||||
{
|
||||
com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "planId");
|
||||
// 补 create_by / create_time (历史一直没写, 导致"提交时间"为空; 列表按 create_time 倒序)
|
||||
if (entity.getCreateBy() == null) entity.setCreateBy(SecurityUtils.getUsername());
|
||||
if (entity.getCreateTime() == null) entity.setCreateTime(new Date());
|
||||
return bizProjectPlanMapper.insert(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 update. #4 触发点 (方案审核结果通知) 在这里插桩:
|
||||
@@ -42,6 +51,10 @@ public class BizProjectPlanServiceImpl implements IBizProjectPlanService
|
||||
oldStatus = existed.getStatus();
|
||||
}
|
||||
}
|
||||
// 提交动作 (状态 → '1' 待审核): 写入提交时间; 拒绝 '3' 重提也会刷新
|
||||
if ("1".equals(entity.getStatus()) && !"1".equals(oldStatus)) {
|
||||
entity.setSubmitTime(new Date());
|
||||
}
|
||||
int n = bizProjectPlanMapper.updateByPrimaryKey(entity);
|
||||
if (n > 0 && entity.getStatus() != null && !entity.getStatus().equals(oldStatus)) {
|
||||
// 重新读一次拿 submitterId/planName/auditOpinion (entity 可能只传了 planId+status+opinion)
|
||||
|
||||
+7
@@ -62,6 +62,13 @@ public class BizProjectServiceImpl implements IBizProjectService
|
||||
if (entity.getCreateUserId() == null) {
|
||||
entity.setCreateUserId(SecurityUtils.getUserId());
|
||||
}
|
||||
// 补 create_by / create_time (历史一直没写, 导致项目列表"创建人/创建时间"两列为空)
|
||||
if (entity.getCreateBy() == null) {
|
||||
entity.setCreateBy(SecurityUtils.getUsername());
|
||||
}
|
||||
if (entity.getCreateTime() == null) {
|
||||
entity.setCreateTime(new java.util.Date());
|
||||
}
|
||||
return bizProjectMapper.insert(entity);
|
||||
}
|
||||
@Override
|
||||
|
||||
+27
-2
@@ -16,6 +16,7 @@ import com.ruoyi.business.domain.BizExpert;
|
||||
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizProject;
|
||||
import com.ruoyi.business.mapper.BizExpertMapper;
|
||||
import com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper;
|
||||
@@ -23,6 +24,7 @@ import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||
import com.ruoyi.business.mapper.BizProjectMapper;
|
||||
import com.ruoyi.business.service.BizSignService;
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
import com.ruoyi.business.service.IBizOrgService;
|
||||
import com.ruoyi.business.service.PdfService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
@@ -42,6 +44,8 @@ public class BizSignServiceImpl implements BizSignService {
|
||||
private PdfService pdfService;
|
||||
@Autowired
|
||||
private BizProjectMapper projectMapper;
|
||||
@Autowired
|
||||
private IBizOrgService bizOrgService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getSignInfo(Long attendeeId) {
|
||||
@@ -134,7 +138,12 @@ public class BizSignServiceImpl implements BizSignService {
|
||||
result.put("attendeeId", attendeeId);
|
||||
result.put("meetingName", meeting != null ? meeting.getMeetingName() : "");
|
||||
result.put("periodNo", meeting != null ? meeting.getPeriodNo() : null);
|
||||
result.put("totalPeriods", meeting != null ? meeting.getTotalPeriods() : null);
|
||||
result.put("totalPeriods", assignedTotalPeriods(meeting));
|
||||
|
||||
// 已签署状态: laborProtocol 非空即已签 (重复打开签署链接 → 前端直接展示 PDF)
|
||||
boolean signed = attendee.getLaborProtocol() != null && !attendee.getLaborProtocol().trim().isEmpty();
|
||||
result.put("signed", signed);
|
||||
result.put("laborProtocol", attendee.getLaborProtocol());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -159,11 +168,27 @@ public class BizSignServiceImpl implements BizSignService {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("meetingName", meeting.getMeetingName());
|
||||
result.put("periodNo", meeting.getPeriodNo());
|
||||
result.put("totalPeriods", meeting.getTotalPeriods());
|
||||
result.put("totalPeriods", assignedTotalPeriods(meeting));
|
||||
result.put("attendeeId", attendeeId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 期数分母: 分配给执行方 (本公司) 的总场次 (biz_project_assign.sessions 之和, 按 execution_unit_id 隔离).
|
||||
* 会议无 execution_unit_id / 反查不到执行方 MAIN / assigned 算出 0 时, 回退项目总期数, 避免显示 1/0. */
|
||||
private Long assignedTotalPeriods(BizMeeting meeting) {
|
||||
if (meeting == null) return null;
|
||||
if (meeting.getExecutionUnitId() == null || meeting.getProjectId() == null) {
|
||||
return meeting.getTotalPeriods();
|
||||
}
|
||||
BizOrg org = bizOrgService.getById(meeting.getExecutionUnitId());
|
||||
Long executorUserId = org == null ? null : org.getUserId();
|
||||
if (executorUserId == null) {
|
||||
return meeting.getTotalPeriods();
|
||||
}
|
||||
int assigned = projectMapper.countAssignedSessions(meeting.getProjectId(), executorUserId);
|
||||
return assigned > 0 ? (long) assigned : meeting.getTotalPeriods();
|
||||
}
|
||||
|
||||
private Map<String, String> map(String value, String label) {
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("value", value);
|
||||
|
||||
+8
@@ -7,6 +7,7 @@ import java.util.List;
|
||||
|
||||
import com.ruoyi.business.service.IBizMeetingInvoiceService;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -58,6 +59,9 @@ public class InvoiceOcrService
|
||||
@Autowired
|
||||
private IBizMeetingMaterialService materialService;
|
||||
|
||||
@Autowired
|
||||
private IBizMeetingService bizMeetingService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("ocrExecutor")
|
||||
private ExecutorService ocrExecutor;
|
||||
@@ -138,6 +142,8 @@ public class InvoiceOcrService
|
||||
{
|
||||
// OCR 处理完毕 (成功/非发票/失败), 该材料金额最终确定 → fee_status=1
|
||||
materialService.updateFeeStatus(materialId, 1);
|
||||
// 立即尝试重算会务费 (若仍有其它发票待 OCR 会回滚 0 走兜底), 不再等 FeeCalcScheduler 每分钟扫
|
||||
bizMeetingService.recomputeMeetingFee(meetingId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -302,6 +308,8 @@ public class InvoiceOcrService
|
||||
{
|
||||
// 兜底 OCR 处理完毕, 金额最终确定 → fee_status=1
|
||||
materialService.updateFeeStatus(inv.getMaterialId(), 1);
|
||||
// 立即尝试重算会务费 (若仍有其它发票待 OCR 会回滚 0 走兜底)
|
||||
bizMeetingService.recomputeMeetingFee(inv.getMeetingId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,10 +75,11 @@ public class AliyunSmsSender {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送电子签短信 (模板占位符 name=姓名, date=日期 MM月dd日, link=签署链接).
|
||||
* 发送邀请签署短信 (模板 SMS_512040098 占位符 name=姓名, time=会议时间, attendeeId=参会人id).
|
||||
* 与验证码模板 (ruoyi.sms.template) 分离, 成功返回 true.
|
||||
* time 由 startTime/endTime 统一格式化: 当天 "8月13日18:20 - 19:20", 跨天 "8月13日18:20 - 8月16日19:20".
|
||||
*/
|
||||
public boolean sendEsign(String phone, String name, Date date, String link) {
|
||||
public boolean sendEsign(String phone, String name, Date startTime, Date endTime, Long attendeeId) {
|
||||
try {
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
req.setPhoneNumbers(phone);
|
||||
@@ -86,28 +87,42 @@ public class AliyunSmsSender {
|
||||
req.setTemplateCode(esignTemplate);
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
params.put("name", name != null ? name : "");
|
||||
params.put("date", date != null ? new SimpleDateFormat("MM月dd日").format(date) : "");
|
||||
params.put("link", link != null ? link : "");
|
||||
params.put("time", formatMeetingTime(startTime, endTime));
|
||||
params.put("attendeeId", attendeeId != null ? String.valueOf(attendeeId) : "");
|
||||
req.setTemplateParam(JSONUtil.toJsonStr(params));
|
||||
SendSmsResponse resp = getClient().getAcsResponse(req);
|
||||
if ("OK".equalsIgnoreCase(resp.getCode())) {
|
||||
log.info("[SMS] 电子签短信发送成功 phone={}, bizId={}", phone, resp.getBizId());
|
||||
log.info("[SMS] 邀请签署短信发送成功 phone={}, bizId={}", phone, resp.getBizId());
|
||||
return true;
|
||||
}
|
||||
log.error("[SMS] 电子签短信发送失败 phone={}, code={}, msg={}, requestId={}",
|
||||
log.error("[SMS] 邀请签署短信发送失败 phone={}, code={}, msg={}, requestId={}",
|
||||
phone, resp.getCode(), resp.getMessage(), resp.getRequestId());
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("[SMS] 电子签短信异常 phone={}", phone, e);
|
||||
log.error("[SMS] 邀请签署短信异常 phone={}", phone, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 会议时间统一格式化: 当天 "M月d日HH:mm - HH:mm", 跨天 "M月d日HH:mm - M月d日HH:mm" */
|
||||
private String formatMeetingTime(Date start, Date end) {
|
||||
if (start == null && end == null) return "";
|
||||
SimpleDateFormat full = new SimpleDateFormat("M月d日HH:mm");
|
||||
SimpleDateFormat hm = new SimpleDateFormat("HH:mm");
|
||||
if (start == null) return full.format(end);
|
||||
if (end == null) return full.format(start);
|
||||
SimpleDateFormat day = new SimpleDateFormat("yyyyMMdd");
|
||||
if (day.format(start).equals(day.format(end))) {
|
||||
return full.format(start) + " - " + hm.format(end);
|
||||
}
|
||||
return full.format(start) + " - " + full.format(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼电子签签署链接: {esignBaseUrl}/#/doctor/sign-fill?attendeeId={attendeeId}
|
||||
* 例: https://risingdoctor.com/hg/#/doctor/sign-fill?attendeeId=123
|
||||
* (nginx 子路径 /hg 已配在 ruoyi.sms.esignBaseUrl 里, 前端 Vue Router 是 hash 模式,
|
||||
* 所以 Java 只拼 #/doctor/sign-fill 路由 + attendeeId)
|
||||
* 例: https://hegui.bahim.org.cn/#/doctor/sign-fill?attendeeId=123
|
||||
* (前端 Vue Router 是 hash 模式, Java 只拼 #/doctor/sign-fill 路由 + attendeeId,
|
||||
* 域名/子路径由 ruoyi.sms.esignBaseUrl 提供)
|
||||
*/
|
||||
public String esignLink(Long attendeeId) {
|
||||
return esignBaseUrl + "/#/doctor/sign-fill?attendeeId=" + attendeeId;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ruoyi.business.supplier;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* 供应商账号接口解密后的单条账号 (对应 refer/bindingdemo 里 decrypted rows[] 的一行).
|
||||
* <p>
|
||||
* 登录名 = account/email (邮箱); password 为 BCrypt 哈希 (不可逆), 同步时写入 sys_user.password2.
|
||||
* 字段名与接口返回 JSON 的 camelCase 一致, 直接由 Jackson 反序列化.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SupplierAccount
|
||||
{
|
||||
/** 登录账号 (= 邮箱) */
|
||||
private String account;
|
||||
/** 邮箱 */
|
||||
private String email;
|
||||
/** 密码 (BCrypt 哈希, 不可逆) */
|
||||
private String password;
|
||||
/** 联系人 */
|
||||
private String contactName;
|
||||
/** 联系电话 */
|
||||
private String contactPhone;
|
||||
/** 企业名称 */
|
||||
private String enterpriseName;
|
||||
/** 企业类型 */
|
||||
private String enterpriseType;
|
||||
/** 公司地址 */
|
||||
private String companyAddress;
|
||||
/** 税号 */
|
||||
private String taxNo;
|
||||
/** 删除标识 (true=已删除) */
|
||||
private Boolean deleted;
|
||||
/** 禁用标识 (true=已禁用) */
|
||||
private Boolean disabled;
|
||||
/** 供应商编码 */
|
||||
private String supplierCode;
|
||||
|
||||
public String getAccount() { return account; }
|
||||
public void setAccount(String account) { this.account = account; }
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
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 getEnterpriseName() { return enterpriseName; }
|
||||
public void setEnterpriseName(String enterpriseName) { this.enterpriseName = enterpriseName; }
|
||||
public String getEnterpriseType() { return enterpriseType; }
|
||||
public void setEnterpriseType(String enterpriseType) { this.enterpriseType = enterpriseType; }
|
||||
public String getCompanyAddress() { return companyAddress; }
|
||||
public void setCompanyAddress(String companyAddress) { this.companyAddress = companyAddress; }
|
||||
public String getTaxNo() { return taxNo; }
|
||||
public void setTaxNo(String taxNo) { this.taxNo = taxNo; }
|
||||
public Boolean getDeleted() { return deleted; }
|
||||
public void setDeleted(Boolean deleted) { this.deleted = deleted; }
|
||||
public Boolean getDisabled() { return disabled; }
|
||||
public void setDisabled(Boolean disabled) { this.disabled = disabled; }
|
||||
public String getSupplierCode() { return supplierCode; }
|
||||
public void setSupplierCode(String supplierCode) { this.supplierCode = supplierCode; }
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
package com.ruoyi.business.supplier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizPerson;
|
||||
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||
import com.ruoyi.business.mapper.BizPersonMapper;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.utils.id.SnowflakeId;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 供应商账号同步: 把供应商接口拉取/解密的账号, upsert 到执行方侧三张表.
|
||||
* <p>
|
||||
* 去重键 = 邮箱 (account == email). 每个供应商账号对应:
|
||||
* 1. sys_user — 登录账号 (user_name/email=邮箱, password2=供应商 BCrypt 哈希, role_type=executor)
|
||||
* 2. biz_org — 企业档案 (org_type=executor)
|
||||
* 3. biz_person — 联系人档案 (unit_type=executor, is_synced=1)
|
||||
* <p>
|
||||
* 状态同步: disabled → sys_user.status='1' + biz_org.status='1'; deleted → sys_user.del_flag='2'.
|
||||
* 幂等: 每分钟重跑, 按邮箱 upsert, 单条失败只 log 不影响后续.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SupplierAccountSyncService
|
||||
{
|
||||
@Autowired
|
||||
private SysUserMapper sysUserMapper;
|
||||
|
||||
@Autowired
|
||||
private BizOrgMapper bizOrgMapper;
|
||||
|
||||
@Autowired
|
||||
private BizPersonMapper bizPersonMapper;
|
||||
|
||||
public int sync(List<SupplierAccount> accounts)
|
||||
{
|
||||
if (accounts == null || accounts.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
int done = 0;
|
||||
int failed = 0;
|
||||
for (SupplierAccount a : accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
syncOne(a);
|
||||
done++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
failed++;
|
||||
log.warn("[SupplierAccountSync] 同步失败 account={} err={}", a.getAccount(), e.getMessage());
|
||||
}
|
||||
}
|
||||
log.info("[SupplierAccountSync] 本轮 {} 条, 成功 {} 失败 {}", accounts.size(), done, failed);
|
||||
return done;
|
||||
}
|
||||
|
||||
private void syncOne(SupplierAccount a)
|
||||
{
|
||||
String email = firstNonBlank(a.getEmail(), a.getAccount());
|
||||
if (email == null)
|
||||
{
|
||||
log.warn("[SupplierAccountSync] 账号缺邮箱, 跳过 supplierCode={}", a.getSupplierCode());
|
||||
return;
|
||||
}
|
||||
String status = Boolean.TRUE.equals(a.getDisabled()) ? "1" : "0";
|
||||
String delFlag = Boolean.TRUE.equals(a.getDeleted()) ? "2" : "0";
|
||||
|
||||
// 供应商侧部分账号缺联系人/电话, 做非空回退 + 按列宽截断 (避免 NOT NULL/UNIQUE/超长报错)
|
||||
String nickName = truncate(firstNonBlank(a.getContactName(), a.getEnterpriseName(), email), 30); // sys_user.nick_name
|
||||
String personName = truncate(firstNonBlank(a.getContactName(), a.getEnterpriseName(), email), 50); // biz_person.name
|
||||
String phone = truncate(firstNonBlank(a.getContactPhone(), a.getSupplierCode(), email), 20); // biz_person.phone 非空唯一
|
||||
String phonenumber = truncate(a.getContactPhone(), 11); // sys_user.phonenumber 可空
|
||||
String orgName = truncate(firstNonBlank(a.getEnterpriseName(), email), 200); // biz_org.org_name
|
||||
String businessNature = truncate(a.getEnterpriseType(), 20);
|
||||
String address = truncate(a.getCompanyAddress(), 500);
|
||||
String taxNo = truncate(a.getTaxNo(), 50);
|
||||
|
||||
// 1. sys_user upsert (去重键=邮箱, 不过滤 del_flag, 以支持"删除→恢复")
|
||||
SysUser user = sysUserMapper.selectUserByEmailIgnoreDel(email);
|
||||
Long userId;
|
||||
if (user == null)
|
||||
{
|
||||
SysUser nu = new SysUser();
|
||||
nu.setUserName(email);
|
||||
nu.setNickName(nickName);
|
||||
nu.setEmail(email);
|
||||
nu.setPhonenumber(phonenumber);
|
||||
nu.setPassword2(a.getPassword());
|
||||
nu.setStatus(status);
|
||||
nu.setDelFlag(delFlag);
|
||||
nu.setAccountType("MAIN");
|
||||
nu.setRoleType("executor");
|
||||
sysUserMapper.insertSyncedUser(nu);
|
||||
userId = nu.getUserId();
|
||||
}
|
||||
else
|
||||
{
|
||||
userId = user.getUserId();
|
||||
SysUser upd = new SysUser();
|
||||
upd.setUserId(userId);
|
||||
upd.setPassword2(a.getPassword());
|
||||
upd.setNickName(nickName);
|
||||
upd.setPhonenumber(phonenumber);
|
||||
upd.setEmail(email);
|
||||
upd.setStatus(status);
|
||||
upd.setDelFlag(delFlag);
|
||||
sysUserMapper.updateSyncedUser(upd);
|
||||
}
|
||||
|
||||
// 2. biz_org upsert (按主账号 user_id)
|
||||
BizOrg q = new BizOrg();
|
||||
q.setUserId(userId);
|
||||
q.setOrgType("executor");
|
||||
List<BizOrg> orgs = bizOrgMapper.selectList(q);
|
||||
BizOrg org = orgs.isEmpty() ? null : orgs.get(0);
|
||||
if (org == null)
|
||||
{
|
||||
BizOrg no = new BizOrg();
|
||||
no.setUserId(userId);
|
||||
no.setOrgName(orgName);
|
||||
no.setOrgType("executor");
|
||||
no.setBusinessNature(businessNature);
|
||||
no.setAddress(address);
|
||||
no.setTaxNo(taxNo);
|
||||
no.setContactName(a.getContactName());
|
||||
no.setContactPhone(a.getContactPhone());
|
||||
no.setStatus(status);
|
||||
bizOrgMapper.insert(no);
|
||||
org = no;
|
||||
}
|
||||
else
|
||||
{
|
||||
org.setOrgName(orgName);
|
||||
org.setBusinessNature(businessNature);
|
||||
org.setAddress(address);
|
||||
org.setTaxNo(taxNo);
|
||||
org.setContactName(a.getContactName());
|
||||
org.setContactPhone(a.getContactPhone());
|
||||
org.setStatus(status);
|
||||
bizOrgMapper.updateByPrimaryKey(org);
|
||||
}
|
||||
Long orgId = org.getOrgId();
|
||||
|
||||
// 3. biz_person upsert (按 user_id), 打同步标记
|
||||
BizPerson person = bizPersonMapper.selectByUserId(userId);
|
||||
if (person == null)
|
||||
{
|
||||
BizPerson np = new BizPerson();
|
||||
SnowflakeId.injectIfEmpty(np, "personId");
|
||||
np.setName(personName);
|
||||
np.setPhone(phone);
|
||||
np.setOrgId(orgId);
|
||||
np.setUnitType("executor");
|
||||
np.setUserId(userId);
|
||||
np.setIsSynced(1);
|
||||
np.setCreateBy("supplier-sync");
|
||||
np.setUpdateBy("supplier-sync");
|
||||
bizPersonMapper.insert(np);
|
||||
}
|
||||
else
|
||||
{
|
||||
person.setName(personName);
|
||||
person.setPhone(phone);
|
||||
person.setOrgId(orgId);
|
||||
person.setIsSynced(1);
|
||||
person.setUpdateBy("supplier-sync");
|
||||
bizPersonMapper.updateByPrimaryKey(person);
|
||||
}
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String... vals)
|
||||
{
|
||||
for (String v : vals)
|
||||
{
|
||||
if (v != null && !v.isBlank())
|
||||
{
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String truncate(String s, int max)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return s.length() > max ? s.substring(0, max) : s;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
<result property="titleCertUrl" column="title_cert_url" />
|
||||
<result property="bankCard" column="bank_card" />
|
||||
<result property="bankName" column="bank_name" />
|
||||
<result property="bankBranch" column="bank_branch" />
|
||||
<result property="bankRegion" column="bank_region" />
|
||||
<result property="bankAddress" column="bank_address" />
|
||||
<result property="idCardAttachments" column="id_card_attachments" />
|
||||
@@ -27,7 +28,7 @@
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
<sql id="selectFields">
|
||||
select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_region, bank_address, id_card_attachments, audit_status, audit_by, audit_time, status, create_by, create_time, update_by, update_time
|
||||
select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_branch, bank_region, bank_address, id_card_attachments, audit_status, audit_by, audit_time, status, create_by, create_time, update_by, update_time
|
||||
from biz_expert
|
||||
</sql>
|
||||
<select id="selectByUserId" resultMap="BizExpertResult" parameterType="Long">
|
||||
@@ -68,6 +69,7 @@
|
||||
<if test="titleCertUrl != null">title_cert_url,</if>
|
||||
<if test="bankCard != null">bank_card,</if>
|
||||
<if test="bankName != null">bank_name,</if>
|
||||
<if test="bankBranch != null">bank_branch,</if>
|
||||
<if test="bankRegion != null">bank_region,</if>
|
||||
<if test="bankAddress != null">bank_address,</if>
|
||||
<if test="auditStatus != null">audit_status,</if>
|
||||
@@ -94,6 +96,7 @@
|
||||
<if test="titleCertUrl != null">#{titleCertUrl},</if>
|
||||
<if test="bankCard != null">#{bankCard},</if>
|
||||
<if test="bankName != null">#{bankName},</if>
|
||||
<if test="bankBranch != null">#{bankBranch},</if>
|
||||
<if test="bankRegion != null">#{bankRegion},</if>
|
||||
<if test="bankAddress != null">#{bankAddress},</if>
|
||||
<if test="auditStatus != null">#{auditStatus},</if>
|
||||
@@ -118,6 +121,7 @@
|
||||
<if test="idCard != null">id_card = #{idCard},</if>
|
||||
<if test="bankCard != null">bank_card = #{bankCard},</if>
|
||||
<if test="bankName != null">bank_name = #{bankName},</if>
|
||||
<if test="bankBranch != null">bank_branch = #{bankBranch},</if>
|
||||
<if test="bankRegion != null">bank_region = #{bankRegion},</if>
|
||||
<if test="bankAddress != null">bank_address = #{bankAddress},</if>
|
||||
<if test="idCardAttachments != null">id_card_attachments = #{idCardAttachments},</if>
|
||||
@@ -144,6 +148,7 @@
|
||||
<if test="idCard != null">id_card,</if>
|
||||
<if test="bankCard != null">bank_card,</if>
|
||||
<if test="bankName != null">bank_name,</if>
|
||||
<if test="bankBranch != null">bank_branch,</if>
|
||||
<if test="bankRegion != null">bank_region,</if>
|
||||
<if test="bankAddress != null">bank_address,</if>
|
||||
<if test="idCardAttachments != null">id_card_attachments,</if>
|
||||
@@ -162,6 +167,7 @@
|
||||
<if test="idCard != null">#{idCard},</if>
|
||||
<if test="bankCard != null">#{bankCard},</if>
|
||||
<if test="bankName != null">#{bankName},</if>
|
||||
<if test="bankBranch != null">#{bankBranch},</if>
|
||||
<if test="bankRegion != null">#{bankRegion},</if>
|
||||
<if test="bankAddress != null">#{bankAddress},</if>
|
||||
<if test="idCardAttachments != null">#{idCardAttachments},</if>
|
||||
@@ -178,6 +184,7 @@
|
||||
<if test="idCard != null and idCard != ''">id_card = #{idCard},</if>
|
||||
<if test="bankCard != null and bankCard != ''">bank_card = #{bankCard},</if>
|
||||
<if test="bankName != null and bankName != ''">bank_name = #{bankName},</if>
|
||||
<if test="bankBranch != null and bankBranch != ''">bank_branch = #{bankBranch},</if>
|
||||
<if test="bankRegion != null and bankRegion != ''">bank_region = #{bankRegion},</if>
|
||||
<if test="bankAddress != null and bankAddress != ''">bank_address = #{bankAddress},</if>
|
||||
<if test="idCardAttachments != null and idCardAttachments != ''">id_card_attachments = #{idCardAttachments},</if>
|
||||
@@ -201,7 +208,7 @@
|
||||
(expert_id, user_id, name, phone, region, id_card,
|
||||
work_unit, department, title,
|
||||
practice_cert_url, title_cert_url,
|
||||
bank_card, bank_name, bank_region, bank_address,
|
||||
bank_card, bank_name, bank_branch, bank_region, bank_address,
|
||||
id_card_attachments,
|
||||
audit_status, audit_by, audit_time, status,
|
||||
create_by, create_time)
|
||||
@@ -210,7 +217,7 @@
|
||||
(#{e.expertId}, #{e.userId}, #{e.name}, #{e.phone}, #{e.region}, #{e.idCard},
|
||||
#{e.workUnit}, #{e.department}, #{e.title},
|
||||
#{e.practiceCertUrl}, #{e.titleCertUrl},
|
||||
#{e.bankCard}, #{e.bankName}, #{e.bankRegion}, #{e.bankAddress},
|
||||
#{e.bankCard}, #{e.bankName}, #{e.bankBranch}, #{e.bankRegion}, #{e.bankAddress},
|
||||
#{e.idCardAttachments},
|
||||
#{e.auditStatus}, #{e.auditBy}, #{e.auditTime}, #{e.status},
|
||||
#{e.createBy}, sysdate())
|
||||
|
||||
+7
-1
@@ -41,6 +41,7 @@
|
||||
<result property="isDeleted" column="is_deleted" />
|
||||
<result property="isEsigned" column="is_esigned" />
|
||||
<result property="isInvited" column="is_invited" />
|
||||
<result property="hasIntent" column="has_intent" />
|
||||
</resultMap>
|
||||
<insert id="insert" parameterType="BizMeetingAttendee">
|
||||
insert into biz_meeting_attendee(id, meeting_id, user_id, create_by, create_time)
|
||||
@@ -152,7 +153,12 @@
|
||||
delete from biz_meeting_attendee where meeting_id = #{meetingId} and user_id = #{userId}
|
||||
</delete>
|
||||
<select id="selectByMeetingId" resultMap="BizMeetingAttendeeResult" parameterType="Long">
|
||||
select id, meeting_id, user_id, name, phone, work_unit, department, title, id_card, bank_card, bank_name, bank_branch, bank_region, bank_address, account_name, id_card_attachments, labor_form, fee_pre_tax, tax, fee, vat_and_surcharge, summary, on_site_photos, signed_at, signed_ip, handsign, labor_protocol, labor_protocol_masked, create_by, create_time, is_deleted, is_esigned, is_invited
|
||||
select id, meeting_id, user_id, name, phone, work_unit, department, title, id_card, bank_card, bank_name, bank_branch, bank_region, bank_address, account_name, id_card_attachments, labor_form, fee_pre_tax, tax, fee, vat_and_surcharge, summary, on_site_photos, signed_at, signed_ip, handsign, labor_protocol, labor_protocol_masked, create_by, create_time, is_deleted, is_esigned, is_invited,
|
||||
(select if(exists(
|
||||
select 1 from biz_execution_intent e
|
||||
where e.user_id = biz_meeting_attendee.user_id
|
||||
and e.project_no collate utf8mb4_unicode_ci = (select p.project_no collate utf8mb4_unicode_ci from biz_meeting m join biz_project p on m.project_id = p.project_id where m.meeting_id = #{meetingId})
|
||||
), 1, 0)) as has_intent
|
||||
from biz_meeting_attendee where meeting_id = #{meetingId} and is_deleted = 0
|
||||
order by id
|
||||
</select>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<result property="meetingName" column="meeting_name" />
|
||||
<result property="periodNo" column="period_no" />
|
||||
<result property="totalPeriods" column="total_periods" />
|
||||
<result property="assignedSessions" column="assigned_sessions" />
|
||||
<result property="projectForm" column="project_form" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
@@ -62,12 +63,14 @@
|
||||
from biz_meeting
|
||||
where meeting_id = #{meetingId} and is_deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizMeetingResult" parameterType="BizMeeting">
|
||||
select
|
||||
<if test="userId != null">(select a.id from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_id,</if>
|
||||
<if test="userId != null">(select a.labor_protocol from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_labor_protocol,</if>
|
||||
(select o.org_name from biz_project p join biz_org o on o.org_id = p.sponsor_org_id where p.project_id = biz_meeting.project_id limit 1) as org_name,
|
||||
(select p.invitation_url from biz_project p where p.project_id = biz_meeting.project_id limit 1) as project_invitation_url,
|
||||
<if test="params.assignedExecutorUserId != null">(select coalesce(sum(a.sessions), 0) from biz_project_assign a where a.project_id = biz_meeting.project_id and a.is_deleted = 0 and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.assignedExecutorUserId} and org_type = 'executor')) as assigned_sessions,</if>
|
||||
<include refid="selectFields"/>
|
||||
from biz_meeting
|
||||
<where>
|
||||
@@ -78,15 +81,15 @@
|
||||
<if test="periodNo != null">and period_no = #{periodNo}</if>
|
||||
<if test="projectForm != null and projectForm != ''">and project_form = #{projectForm}</if>
|
||||
<if test="currentStage != null and currentStage != ''">and current_stage = #{currentStage}</if>
|
||||
<if test="currentStageNotIn != null and currentStageNotIn != ''">
|
||||
and current_stage not in
|
||||
<foreach collection="currentStageNotIn.split(',')" item="s" open="(" separator="," close=")">#{s}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null">and start_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and end_time <= #{endTime}</if>
|
||||
<!-- doctor 角色按 user_id 过滤 (走 biz_meeting_attendee 中间表, 同时 attendee 也需 is_deleted=0) -->
|
||||
<if test="userId != null">and exists (select 1 from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0)</if>
|
||||
<!-- sponsor 数据权限: 只看"我的项目"下的会议 (project_id ∈ 我的项目). MAIN 走 sponsor_org_id (经 user_id 反查 org_id), SUB 走 sponsor_assign.monitor_user_id.
|
||||
刻意不带 biz_publicity_support_intent 关联 (与项目列表 selectSponsorList 的区别点) -->
|
||||
<if test="params.sponsorAdminUserId != null">and project_id in (select project_id from biz_project where sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor') and is_deleted = 0)</if>
|
||||
<if test="params.monitorUserId != null">and project_id in (select distinct project_id from biz_project_sponsor_assign where monitor_user_id = #{params.monitorUserId} and is_deleted = 0)</if>
|
||||
<!-- 结题且未开通的项目, sponsor 会议不可见 (与项目列表 selectSponsorList 同口径) -->
|
||||
<if test="params.sponsorAdminUserId != null or params.monitorUserId != null">
|
||||
and not exists (
|
||||
select 1 from biz_project p2
|
||||
@@ -95,20 +98,39 @@
|
||||
and p2.is_finished = '1' and p2.open_status = 'N'
|
||||
)
|
||||
</if>
|
||||
<!-- executor 数据权限: MAIN 看本执行单位项目下的会议 (biz_project_assign.execution_unit_id), SUB(执行人) 只看本人创建的会议 (create_by) -->
|
||||
<if test="params.executorUserId != null">and project_id in (
|
||||
select distinct a.project_id from biz_project_assign a
|
||||
where a.is_deleted = 0
|
||||
and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')
|
||||
)</if>
|
||||
<if test="params.executorCreatorUsername != null and params.executorCreatorUsername != ''">and create_by = #{params.executorCreatorUsername}</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>
|
||||
|
||||
<!-- 会议阶段统计 (仅 sponsor/Home KPI 用): 按 current_stage 分组计数, 只套 sponsor 数据权限 (其他角色单独修, 不在此复用多角色过滤) -->
|
||||
<select id="selectStageStats" resultType="java.util.HashMap" parameterType="BizMeeting">
|
||||
select current_stage as currentStage, count(*) as cnt
|
||||
from biz_meeting
|
||||
<where>
|
||||
is_deleted = 0
|
||||
<if test="params.sponsorAdminUserId != null">and project_id in (select project_id from biz_project where sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor') and is_deleted = 0)</if>
|
||||
<if test="params.monitorUserId != null">and project_id in (select distinct project_id from biz_project_sponsor_assign where monitor_user_id = #{params.monitorUserId} and is_deleted = 0)</if>
|
||||
<if test="params.sponsorAdminUserId != null or params.monitorUserId != null">
|
||||
and not exists (
|
||||
select 1 from biz_project p2
|
||||
where p2.project_id = biz_meeting.project_id
|
||||
and p2.is_deleted = 0
|
||||
and p2.is_finished = '1' and p2.open_status = 'N'
|
||||
)
|
||||
</if>
|
||||
</where>
|
||||
group by current_stage
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="BizMeeting">
|
||||
insert into biz_meeting
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
@@ -250,7 +272,27 @@
|
||||
and execution_unit_id = #{executionUnitId}
|
||||
and period_no = #{periodNo}
|
||||
</select>
|
||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): start_time 已过 且 material 未提交 且未执行的会议 → is_executed=1 + RUNNING. 已软删/已冻结/已提交材料的不动. -->
|
||||
<!-- 修改校验用 (执行方隔离): 同上, 但排除指定 meetingId (修改自身会议不触发相同期数误报) -->
|
||||
<select id="countByProjectIdExecutionUnitPeriodExclude" resultType="int">
|
||||
select count(*) from biz_meeting where project_id = #{projectId} and is_deleted = 0
|
||||
and execution_unit_id = #{executionUnitId}
|
||||
and period_no = #{periodNo}
|
||||
and meeting_id != #{excludeMeetingId}
|
||||
</select>
|
||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): start_time 已过 且 end_time 未到 且 material 未提交 且未执行的会议 → current_stage = IN_PROGRESS (执行中). 已软删/已冻结/已执行的不动. -->
|
||||
<update id="markInProgress">
|
||||
update biz_meeting
|
||||
set current_stage = 'IN_PROGRESS'
|
||||
where is_deleted = 0
|
||||
and is_executed = 0
|
||||
and is_frozen = 0
|
||||
and start_time is not null
|
||||
and start_time <= NOW()
|
||||
and end_time > NOW()
|
||||
and labor_audit_stage = 'NOT_SUBMITTED'
|
||||
and service_audit_stage = 'NOT_SUBMITTED'
|
||||
</update>
|
||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): end_time 已过 且 material 未提交 且未执行的会议 → is_executed=1 + RUNNING (已执行). 已软删/已冻结/已提交材料的不动. -->
|
||||
<update id="markExecuted">
|
||||
update biz_meeting
|
||||
set is_executed = 1,
|
||||
@@ -259,8 +301,8 @@
|
||||
where is_deleted = 0
|
||||
and is_executed = 0
|
||||
and is_frozen = 0
|
||||
and start_time is not null
|
||||
and start_time <= NOW()
|
||||
and end_time is not null
|
||||
and end_time <= NOW()
|
||||
and labor_audit_stage = 'NOT_SUBMITTED'
|
||||
and service_audit_stage = 'NOT_SUBMITTED'
|
||||
</update>
|
||||
@@ -282,9 +324,9 @@
|
||||
<select id="selectPendingFeeCalcIds" resultType="Long">
|
||||
select meeting_id from biz_meeting where fee_calc_status = 0 and is_deleted = 0
|
||||
</select>
|
||||
<!-- 置未汇总 (人员/材料变化触发, 幂等) -->
|
||||
<update id="markFeeCalcPending" parameterType="Long">
|
||||
update biz_meeting set fee_calc_status = 0 where meeting_id = #{meetingId}
|
||||
<!-- 费用重算状态机: 直接置 fee_calc_status (-1 计算中 / 0 待算兜底); 成功(1) 由 updateFeeSummary 一并写入 -->
|
||||
<update id="updateFeeCalcStatus">
|
||||
update biz_meeting set fee_calc_status = #{status} where meeting_id = #{meetingId}
|
||||
</update>
|
||||
<!-- 汇总回写 3 个费用字段 + 置已汇总 -->
|
||||
<update id="updateFeeSummary">
|
||||
@@ -295,4 +337,11 @@
|
||||
fee_calc_status = 1
|
||||
where meeting_id = #{meetingId}
|
||||
</update>
|
||||
<!-- 同步重算劳务费 (参会人增删改后立即调用): 只回写 labor_fee + total_fee, 不动 meeting_fee / fee_calc_status -->
|
||||
<update id="updateLaborFee">
|
||||
update biz_meeting
|
||||
set labor_fee = #{laborFee},
|
||||
total_fee = #{totalFee}
|
||||
where meeting_id = #{meetingId}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -21,9 +21,11 @@
|
||||
<sql id="selectFields">
|
||||
select m.msg_id, m.receiver_user_id, m.msg_type, m.title, m.content, m.biz_type, m.biz_id,
|
||||
m.is_read, m.read_time, m.create_by, m.create_time, m.update_by, m.update_time,
|
||||
u.user_name as receiver_name
|
||||
COALESCE(NULLIF(bp.name, ''), NULLIF(be.name, ''), u.nick_name) as receiver_name
|
||||
from biz_message m
|
||||
left join sys_user u on u.user_id = m.receiver_user_id
|
||||
left join biz_person bp on bp.user_id = m.receiver_user_id
|
||||
left join biz_expert be on be.user_id = m.receiver_user_id
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizMessageResult" parameterType="Long">
|
||||
@@ -49,9 +51,23 @@
|
||||
m.receiver_user_id = #{receiverUserId}
|
||||
</where>
|
||||
order by m.is_read asc, m.msg_id desc
|
||||
<if test="params.limit != null">
|
||||
limit #{params.limit}
|
||||
</if>
|
||||
<!-- 分页: params.offset 有值时用 offset,limit; 否则只用 limit (嵌入式列表) -->
|
||||
<choose>
|
||||
<when test="params.offset != null">
|
||||
limit #{params.offset}, #{params.limit}
|
||||
</when>
|
||||
<otherwise>
|
||||
<if test="params.limit != null">
|
||||
limit #{params.limit}
|
||||
</if>
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
<!-- 某用户的全部消息总数 (分页 total 用, 与 countUnread 解耦) -->
|
||||
<select id="countMy" resultType="int" parameterType="Long">
|
||||
select count(*) from biz_message
|
||||
where receiver_user_id = #{receiverUserId}
|
||||
</select>
|
||||
|
||||
<select id="countUnread" resultType="int" parameterType="Long">
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
||||
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
order by o.org_id desc
|
||||
LIMIT 5
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!--
|
||||
@@ -146,7 +146,7 @@
|
||||
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
||||
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
order by o.org_id desc
|
||||
LIMIT 5
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!--
|
||||
@@ -198,4 +198,12 @@
|
||||
(select org_id from biz_person where user_id = #{userId} limit 1)
|
||||
)
|
||||
</select>
|
||||
|
||||
<!-- 单位名称查重 (新增/改名前): 同 orgType 下 org_name 精确匹配 (trim 后) 的条数; orgId 传非空则排除自身 (改名用) -->
|
||||
<select id="countByOrgName" parameterType="BizOrg" resultType="int">
|
||||
select count(*) from biz_org
|
||||
where org_type = #{orgType}
|
||||
and trim(org_name) = #{orgName}
|
||||
<if test="orgId != null"> and org_id != #{orgId}</if>
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<result property="department" column="department" />
|
||||
<result property="position" column="position" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="isSynced" column="is_synced" />
|
||||
<result property="unitType" column="unit_type" />
|
||||
<result property="accountType" column="account_type" />
|
||||
<result property="parentUserId" column="parent_user_id" />
|
||||
@@ -23,14 +24,14 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select person_id, name, phone, org_id, department, position, unit_type, user_id, create_by, create_time, update_by, update_time
|
||||
select person_id, name, phone, org_id, department, position, unit_type, is_synced, user_id, create_by, create_time, update_by, update_time
|
||||
from biz_person
|
||||
</sql>
|
||||
|
||||
<!-- 通用列表: LEFT JOIN biz_org 取公司名/类型, LEFT JOIN sys_user 取账号状态/类型 -->
|
||||
<sql id="selectFieldsWithAccount">
|
||||
select p.person_id, p.name, p.phone, p.org_id, o.org_name, o.org_type,
|
||||
p.department, p.position, p.unit_type, p.user_id,
|
||||
p.department, p.position, p.unit_type, p.is_synced, p.user_id,
|
||||
p.create_by, p.create_time, p.update_by, p.update_time,
|
||||
u.account_type, u.parent_user_id, u.status, u.del_flag as user_del_flag,
|
||||
u.user_name as user_name,
|
||||
@@ -82,6 +83,7 @@
|
||||
<if test="department != null">department,</if>
|
||||
<if test="position != null">position,</if>
|
||||
<if test="unitType != null">unit_type,</if>
|
||||
<if test="isSynced != null">is_synced,</if>
|
||||
<if test="userId != null">user_id,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
create_time,
|
||||
@@ -96,6 +98,7 @@
|
||||
<if test="department != null">#{department},</if>
|
||||
<if test="position != null">#{position},</if>
|
||||
<if test="unitType != null">#{unitType},</if>
|
||||
<if test="isSynced != null">#{isSynced},</if>
|
||||
<if test="userId != null">#{userId},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
sysdate(),
|
||||
@@ -110,6 +113,7 @@
|
||||
<if test="userId != null and userId != ''">user_id = #{userId},</if>
|
||||
<if test="orgId != null">org_id = #{orgId},</if>
|
||||
<if test="unitType != null and unitType != ''">unit_type = #{unitType},</if>
|
||||
<if test="isSynced != null">is_synced = #{isSynced},</if>
|
||||
<if test="name != null">name = #{name},</if>
|
||||
<if test="phone != null">phone = #{phone},</if>
|
||||
<if test="department != null">department = #{department},</if>
|
||||
@@ -145,15 +149,13 @@
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<!-- sponsor 专属: 通过 biz_person.user_id 关联 sys_user, 利用 sys_user.parent_user_id 过滤主账号归属 -->
|
||||
<!-- 主账号自己 (parent_user_id=NULL) 也通过 OR u.user_id=#{sponsorOwnerUid} 一并带上 (否则主账号看不到自己) -->
|
||||
<!-- sponsor 专属: SQL 硬编码 unit_type='sponsor' + 严格按 org_id 圈本机构所有用户 (含主账号自己) -->
|
||||
<select id="selectSponsorList" resultMap="BizPersonResult" parameterType="BizPerson">
|
||||
<include refid="selectFieldsWithAccount"/>
|
||||
<where>
|
||||
u.del_flag = '0'
|
||||
and p.unit_type = 'sponsor'
|
||||
and (u.parent_user_id = #{params.sponsorOwnerUid}
|
||||
or u.user_id = #{params.sponsorOwnerUid})
|
||||
and p.org_id = #{params.sponsorOrgId}
|
||||
<if test="name != null and name != ''"> and p.name like concat('%', #{name}, '%')</if>
|
||||
<if test="phone != null and phone != ''"> and p.phone = #{phone}</if>
|
||||
<if test="orgId != null"> and p.org_id = #{orgId}</if>
|
||||
@@ -165,15 +167,13 @@
|
||||
order by p.person_id desc
|
||||
</select>
|
||||
|
||||
<!-- executor 专属: 同 sponsor, SQL 硬编码 unit_type='executor' + parent_user_id=当前主账号 (前端绕不开) -->
|
||||
<!-- 主账号自己 (parent_user_id=NULL) 也通过 OR u.user_id=#{executorOwnerUid} 一并带上 (否则主账号看不到自己) -->
|
||||
<!-- executor 专属: SQL 硬编码 unit_type='executor' + 严格按 org_id 圈本机构所有用户 (含主账号自己) -->
|
||||
<select id="selectExecutorList" resultMap="BizPersonResult" parameterType="BizPerson">
|
||||
<include refid="selectFieldsWithAccount"/>
|
||||
<where>
|
||||
u.del_flag = '0'
|
||||
and p.unit_type = 'executor'
|
||||
and (u.parent_user_id = #{params.executorOwnerUid}
|
||||
or u.user_id = #{params.executorOwnerUid})
|
||||
and p.org_id = #{params.executorOrgId}
|
||||
<if test="name != null and name != ''"> and p.name like concat('%', #{name}, '%')</if>
|
||||
<if test="phone != null and phone != ''"> and p.phone = #{phone}</if>
|
||||
<if test="orgId != null"> and p.org_id = #{orgId}</if>
|
||||
@@ -185,4 +185,21 @@
|
||||
order by p.person_id desc
|
||||
</select>
|
||||
|
||||
<!-- 按 user_id 反查人员档案 (个人资料编辑: 拿 personId 再走 updateByPrimaryKey) -->
|
||||
<select id="selectByUserId" resultMap="BizPersonResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where user_id = #{userId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 按 org_id 反查该机构主账号 user_id: biz_person.org_id 绑定 + sys_user.account_type='MAIN', 不依赖 biz_org.user_id -->
|
||||
<select id="selectMainUserIdByOrgId" resultType="java.lang.Long" parameterType="Long">
|
||||
select u.user_id
|
||||
from biz_person p
|
||||
join sys_user u on u.user_id = p.user_id
|
||||
where p.org_id = #{orgId} and u.account_type = 'MAIN' and u.del_flag = '0'
|
||||
order by u.user_id
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
+4
-2
@@ -35,12 +35,14 @@
|
||||
|
||||
<select id="selectByProjectId" resultMap="BaseResultMap">
|
||||
SELECT a.*,
|
||||
e.user_name AS executor_user_name,
|
||||
s.user_name AS staff_user_name
|
||||
COALESCE(NULLIF(ep.name, ''), e.nick_name) AS executor_user_name,
|
||||
COALESCE(NULLIF(sp.name, ''), s.nick_name) AS staff_user_name
|
||||
FROM biz_project_executor_assign a
|
||||
LEFT JOIN biz_org o ON o.org_id = a.executor_org_id
|
||||
LEFT JOIN sys_user e ON e.user_id = o.user_id
|
||||
LEFT JOIN biz_person ep ON ep.user_id = o.user_id
|
||||
LEFT JOIN sys_user s ON a.staff_user_id = s.user_id
|
||||
LEFT JOIN biz_person sp ON sp.user_id = a.staff_user_id
|
||||
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
||||
ORDER BY a.create_time DESC
|
||||
</select>
|
||||
|
||||
@@ -51,7 +51,15 @@
|
||||
<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,
|
||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.total_amount,
|
||||
<!-- 已执行会议数: stage 走过 NOT_STARTED/IN_PROGRESS 就算 (含 FROZEN 已结算异常态) -->
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')) as done_sessions,
|
||||
<!-- 未执行会议数: 仅 NOT_STARTED -->
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage = 'NOT_STARTED') as todo_sessions,
|
||||
(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)
|
||||
@@ -59,8 +67,8 @@
|
||||
- (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.publish_time, 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,
|
||||
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
from biz_project_assign bpa
|
||||
@@ -70,13 +78,21 @@
|
||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||
left join sys_user su on su.user_id = o.user_id
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person sup on sup.user_id = o.user_id
|
||||
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
</sql>
|
||||
|
||||
<!-- 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,
|
||||
select p.project_id, p.project_no, p.project_name, p.total_sessions,
|
||||
p.total_amount,
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')) as done_sessions,
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage = 'NOT_STARTED') as todo_sessions,
|
||||
(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)
|
||||
@@ -89,8 +105,8 @@
|
||||
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,
|
||||
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
from biz_project_assign bpa
|
||||
@@ -100,6 +116,8 @@
|
||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||
left join sys_user su on su.user_id = o.user_id
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person sup on sup.user_id = o.user_id
|
||||
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
</sql>
|
||||
<select id="selectByPrimaryKey" resultMap="BizProjectResult" parameterType="Long">
|
||||
@@ -150,6 +168,7 @@
|
||||
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
||||
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
||||
<if test="isSettled != null and isSettled != ''">and p.is_settled = #{isSettled}</if>
|
||||
</where>
|
||||
order by p.project_id desc
|
||||
</select>
|
||||
@@ -164,10 +183,10 @@
|
||||
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.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,
|
||||
select distinct p.project_id, p.project_no, p.project_name, p.total_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,
|
||||
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
from biz_project_assign bpa2
|
||||
@@ -213,7 +232,17 @@
|
||||
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
|
||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as meeting_count,
|
||||
<!-- 执行方级已执行: 同上 execution_unit_id 隔离, FROZEN 算已执行 -->
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')
|
||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as done_sessions,
|
||||
<!-- 执行方级未执行 -->
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage = 'NOT_STARTED'
|
||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as todo_sessions
|
||||
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
|
||||
@@ -222,6 +251,8 @@
|
||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||
left join sys_user su on su.user_id = o.user_id
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person sup on sup.user_id = o.user_id
|
||||
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
<where>
|
||||
p.is_deleted = 0
|
||||
@@ -230,6 +261,7 @@
|
||||
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
||||
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
||||
<if test="isSettled != null and isSettled != ''">and p.is_settled = #{isSettled}</if>
|
||||
</where>
|
||||
order by p.project_id desc
|
||||
</select>
|
||||
@@ -242,10 +274,10 @@
|
||||
已支付劳务/会务/可用金额同样按 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.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,
|
||||
select p.project_id, p.project_no, p.project_name, p.total_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,
|
||||
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
from biz_project_assign bpa2
|
||||
@@ -291,11 +323,22 @@
|
||||
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
|
||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as meeting_count,
|
||||
<!-- 执行人视角: 仍按主账号 org_id 聚合 (执行人看公司数据不是个人), FROZEN 算已执行 -->
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')
|
||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as done_sessions,
|
||||
(select count(*) from biz_meeting m
|
||||
where m.project_id = p.project_id and m.is_deleted = 0
|
||||
and m.current_stage = 'NOT_STARTED'
|
||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as todo_sessions
|
||||
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
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person sup on sup.user_id = o.user_id
|
||||
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
<where>
|
||||
p.is_deleted = 0
|
||||
@@ -309,6 +352,7 @@
|
||||
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
||||
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
||||
<if test="isSettled != null and isSettled != ''">and p.is_settled = #{isSettled}</if>
|
||||
</where>
|
||||
order by p.project_id desc
|
||||
</select>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="submitTime" column="submit_time" />
|
||||
<result property="isDeleted" column="is_deleted" />
|
||||
</resultMap>
|
||||
<sql id="selectFields">
|
||||
@@ -32,9 +33,9 @@
|
||||
s.title as plan_direction_title,
|
||||
p.plan_category, p.project_form, p.design_file_url, p.subject_direction, p.status, p.is_settled,
|
||||
p.project_no, p.remark, p.audit_opinion, p.audit_by, p.audit_time, p.submitter_id,
|
||||
COALESCE(bp.name, u.user_name) as submitter_name,
|
||||
COALESCE(bp.name, u.nick_name) as submitter_name,
|
||||
proj.project_name as project_name,
|
||||
p.create_by, p.create_time, p.update_by, p.update_time, p.is_deleted
|
||||
p.create_by, p.create_time, p.update_by, p.update_time, p.submit_time, p.is_deleted
|
||||
from biz_project_plan p
|
||||
left join biz_special_plan s on s.id = p.plan_direction_id
|
||||
left join sys_user u on u.user_id = p.submitter_id
|
||||
@@ -49,6 +50,8 @@
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
p.is_deleted = 0
|
||||
<!-- 管理角色 (admin/manager): 未提交草稿 status='0' 不进列表 (controller 注入 params.excludeDraft) -->
|
||||
<if test="params.excludeDraft == true"> and p.status != '0'</if>
|
||||
<if test="planName != null and planName != ''"> and p.plan_name like concat('%', #{planName}, '%')</if>
|
||||
<if test="planDirectionId != null"> and p.plan_direction_id = #{planDirectionId}</if>
|
||||
<if test="planCategory != null and planCategory != ''"> and p.plan_category = #{planCategory}</if>
|
||||
@@ -57,11 +60,11 @@
|
||||
<choose>
|
||||
<!-- 选了具体状态: 等值匹配 -->
|
||||
<when test="status != null and status != ''"> and p.status = #{status}</when>
|
||||
<!-- 未选: 不加 status 过滤, 由前端按角色决定默认值 (经理侧默认查 1/2/3, 医生侧默认查全部含 0) -->
|
||||
<!-- 未选: 不加 status 等值过滤; 未提交草稿 '0' 已由上方 params.excludeDraft 对管理角色排除 -->
|
||||
</choose>
|
||||
<if test="remark != null and remark != ''"> and p.remark like concat('%', #{remark}, '%')</if>
|
||||
</where>
|
||||
order by p.plan_id desc
|
||||
order by (p.submit_time is null), p.submit_time desc, p.plan_id desc
|
||||
</select>
|
||||
<insert id="insert" parameterType="BizProjectPlan">
|
||||
insert into biz_project_plan
|
||||
@@ -79,6 +82,8 @@
|
||||
<if test="projectNo != null and projectNo != ''">project_no,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
<if test="submitterId != null">submitter_id,</if>
|
||||
<if test="createBy != null">create_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="planId != null and planId != ''">#{planId},</if>
|
||||
@@ -94,6 +99,8 @@
|
||||
<if test="projectNo != null and projectNo != ''">#{projectNo},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
<if test="submitterId != null">#{submitterId},</if>
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
<update id="updateByPrimaryKey" parameterType="BizProjectPlan">
|
||||
@@ -114,6 +121,7 @@
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
<if test="submitterId != null">submitter_id = #{submitterId},</if>
|
||||
<if test="submitTime != null">submit_time = #{submitTime},</if>
|
||||
</trim>
|
||||
where plan_id = #{planId}
|
||||
</update>
|
||||
|
||||
+5
-3
@@ -35,12 +35,14 @@
|
||||
|
||||
<select id="selectByProjectId" resultMap="BaseResultMap">
|
||||
SELECT a.*,
|
||||
s.user_name AS sponsor_user_name,
|
||||
m.user_name AS monitor_user_name
|
||||
COALESCE(NULLIF(sp.name, ''), s.nick_name) AS sponsor_user_name,
|
||||
COALESCE(NULLIF(p.name, ''), m.nick_name) AS monitor_user_name
|
||||
FROM biz_project_sponsor_assign a
|
||||
LEFT JOIN biz_org o ON o.org_id = a.sponsor_org_id
|
||||
LEFT JOIN sys_user s ON s.user_id = o.user_id
|
||||
LEFT JOIN sys_user m ON a.monitor_user_id = m.user_id
|
||||
LEFT JOIN biz_person sp ON sp.user_id = o.user_id
|
||||
LEFT JOIN biz_person p ON p.user_id = a.monitor_user_id
|
||||
INNER JOIN sys_user m ON a.monitor_user_id = m.user_id
|
||||
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
||||
ORDER BY a.create_time DESC
|
||||
</select>
|
||||
|
||||
@@ -48,6 +48,9 @@ public class SysUser extends BaseEntity
|
||||
/** 密码 */
|
||||
private String password;
|
||||
|
||||
/** 供应商同步密码 (BCrypt 哈希, 登录时优先于 password 校验) */
|
||||
private String password2;
|
||||
|
||||
/** 账号状态(0正常 1停用) */
|
||||
@Excel(name = "账号状态", readConverterExp = "0=正常,1=停用")
|
||||
private String status;
|
||||
@@ -180,6 +183,15 @@ public class SysUser extends BaseEntity
|
||||
{
|
||||
this.password = password;
|
||||
}
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
public String getPassword2()
|
||||
{
|
||||
return password2;
|
||||
}
|
||||
public void setPassword2(String password2)
|
||||
{
|
||||
this.password2 = password2;
|
||||
}
|
||||
public String getStatus()
|
||||
{
|
||||
return status;
|
||||
|
||||
@@ -7,7 +7,9 @@ package com.ruoyi.common.enums;
|
||||
* <pre>
|
||||
* NOT_STARTED 未执行 会议开始时间前
|
||||
* ↓ (过 startTime, scheduler)
|
||||
* RUNNING 执行中 已开始, 执行方未提交材料
|
||||
* IN_PROGRESS 执行中 会议进行中 (开始时间已到, 结束时间未到)
|
||||
* ↓ (过 endTime, scheduler)
|
||||
* RUNNING 已执行 会议已结束, 执行方未提交材料
|
||||
* ↓ (执行方提交材料)
|
||||
* AWAITING_COMPLIANCE 待合规审核 执行方已提交, 等合规人员审核 (支持方此时只读, 不能审)
|
||||
* ↓ (合规审通过) ↘ (合规退回)
|
||||
@@ -41,8 +43,11 @@ public enum BizMeetingStageEnum
|
||||
/** 会议开始时间前 */
|
||||
NOT_STARTED("NOT_STARTED", "未执行", "会议开始时间前"),
|
||||
|
||||
/** 已开始, 执行方未提交材料 */
|
||||
RUNNING("RUNNING", "执行中", "已开始, 执行方未提交"),
|
||||
/** 会议进行中 (开始时间已到, 结束时间未到) */
|
||||
IN_PROGRESS("IN_PROGRESS", "执行中", "会议进行中 (开始时间已到, 结束时间未到)"),
|
||||
|
||||
/** 会议已结束, 执行方未提交材料 */
|
||||
RUNNING("RUNNING", "已执行", "会议已结束, 执行方未提交材料"),
|
||||
|
||||
/** 执行方已提交, 等合规人员审核 (支持方此阶段只读, 不可审) */
|
||||
AWAITING_COMPLIANCE("AWAITING_COMPLIANCE", "待合规审核", "执行方提交, 等合规审"),
|
||||
|
||||
+6
@@ -70,6 +70,12 @@ public class UserDetailsServiceImpl implements UserDetailsService
|
||||
}
|
||||
}
|
||||
|
||||
// 供应商同步账号: 优先用 password2 (供应商 BCrypt 哈希) 参与密码比对
|
||||
if (StringUtils.isNotBlank(user.getPassword2()))
|
||||
{
|
||||
user.setPassword(user.getPassword2());
|
||||
}
|
||||
|
||||
passwordService.validate(user);
|
||||
|
||||
return createLoginUser(user);
|
||||
|
||||
@@ -61,6 +61,14 @@ public interface SysUserMapper
|
||||
*/
|
||||
public SysUser selectUserByEmail(String email);
|
||||
|
||||
/**
|
||||
* 通过邮箱查询用户, 不过滤 del_flag (供应商账号同步去重用: 已软删的账号也要能查到以便恢复)
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return 用户对象信息
|
||||
*/
|
||||
public SysUser selectUserByEmailIgnoreDel(String email);
|
||||
|
||||
/**
|
||||
* 通过用户ID查询用户
|
||||
*
|
||||
@@ -77,6 +85,14 @@ public interface SysUserMapper
|
||||
*/
|
||||
public int insertUser(SysUser user);
|
||||
|
||||
/**
|
||||
* 新增供应商同步用户 (写 password2/del_flag 等, 不写 password 列)
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertSyncedUser(SysUser user);
|
||||
|
||||
/**
|
||||
* 修改用户信息
|
||||
*
|
||||
@@ -85,6 +101,14 @@ public interface SysUserMapper
|
||||
*/
|
||||
public int updateUser(SysUser user);
|
||||
|
||||
/**
|
||||
* 更新供应商同步用户 (password2/nick_name/phonenumber/email/status/del_flag)
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateSyncedUser(SysUser user);
|
||||
|
||||
/**
|
||||
* 修改用户头像
|
||||
*
|
||||
|
||||
@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="sex" column="sex" />
|
||||
<result property="avatar" column="avatar" />
|
||||
<result property="password" column="password" />
|
||||
<result property="password2" column="password2" />
|
||||
<result property="status" column="status" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="loginIp" column="login_ip" />
|
||||
@@ -57,7 +58,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectUserVo">
|
||||
select u.user_id, u.dept_id, u.user_name, u.nick_name, u.account_type, u.parent_user_id, u.role_type, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.update_by, u.update_time, u.remark,
|
||||
select u.user_id, u.dept_id, u.user_name, u.nick_name, u.account_type, u.parent_user_id, u.role_type, u.email, u.avatar, u.phonenumber, u.password, u.password2, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.update_by, u.update_time, u.remark,
|
||||
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
|
||||
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
|
||||
from sys_user u
|
||||
@@ -108,6 +109,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from sys_user u
|
||||
left join sys_dept d on u.dept_id = d.dept_id
|
||||
where u.del_flag = '0'
|
||||
<!-- 支持方/执行人: 单位不存在的过滤掉 (单位被硬删后 org_name 为 NULL, 孤儿账号不展示) -->
|
||||
AND (
|
||||
coalesce(u.role_type, '') not in ('sponsor', 'executor')
|
||||
OR exists (select 1 from biz_org o where o.user_id = u.user_id)
|
||||
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)
|
||||
)
|
||||
<if test="userId != null and userId != 0">
|
||||
AND u.user_id = #{userId}
|
||||
</if>
|
||||
@@ -350,4 +357,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
select user_id from sys_user where parent_user_id = #{parentUserId} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectUserByEmailIgnoreDel" parameterType="String" resultMap="SysUserResult">
|
||||
<include refid="selectUserVo"/>
|
||||
where u.email = #{email}
|
||||
</select>
|
||||
|
||||
<!-- 供应商账号同步: 新建执行方登录账号 (密码哈希写入 password2, 不写 password) -->
|
||||
<insert id="insertSyncedUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
|
||||
insert into sys_user(user_name, nick_name, email, phonenumber, password2, status, del_flag, account_type, role_type, create_time)
|
||||
values(#{userName}, #{nickName}, #{email}, #{phonenumber}, #{password2}, #{status}, #{delFlag}, #{accountType}, #{roleType}, sysdate())
|
||||
</insert>
|
||||
|
||||
<!-- 供应商账号同步: 更新执行方登录账号 (含恢复/删除 del_flag) -->
|
||||
<update id="updateSyncedUser" parameterType="SysUser">
|
||||
update sys_user
|
||||
set password2 = #{password2},
|
||||
nick_name = #{nickName},
|
||||
phonenumber = #{phonenumber},
|
||||
email = #{email},
|
||||
status = #{status},
|
||||
del_flag = #{delFlag},
|
||||
update_time = sysdate()
|
||||
where user_id = #{userId}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user