feat: 会议提交剩余时间+解冻 + 会务/劳务材料批量下载 + 多角色人员/账号模块完善
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ ruoyi:
|
||||
accessKeySecret: gA0fTFa8arUYaYA5Us2ClPKVSM1Ypv
|
||||
dirPrefix: ry8080/
|
||||
expireSeconds: 3600
|
||||
# 阿里云函数计算 (FC) 打 zip 端点 (与 hwt-serve 共用同一 bucket, 参考 OssApi.ossDownload)
|
||||
zip-func-url: https://zip-oss-func-zip-oss-swqpembfsl.cn-beijing.fcapp.run
|
||||
# 阿里云短信配置 (hwt-code ali.sms 模式)
|
||||
# dev/prod 区分走代码: phone 以 "10" 开头视为 dev 测试 (固定码 1234), 其它走 aliyun 真发
|
||||
sms:
|
||||
@@ -45,7 +47,7 @@ ruoyi:
|
||||
page-timeout-s: 15
|
||||
total-timeout-s: 60
|
||||
# QR 命中后是否继续跑全量 OCR (false = 快路径只返回 QR 3 字段)
|
||||
qr-full-ocr: true
|
||||
qr-full-ocr: false
|
||||
lang: ch
|
||||
# PDF 优先抽内嵌文本层 (电子发票秒出, 扫描件自动回退 ONNX)
|
||||
use-pdf-text-first: true
|
||||
@@ -78,9 +80,9 @@ server:
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.ruoyi: info
|
||||
org.springframework: info
|
||||
com.ruoyi.business: info
|
||||
com.ruoyi: debug
|
||||
org.springframework: debug
|
||||
com.ruoyi.business: debug
|
||||
|
||||
# 用户配置
|
||||
user:
|
||||
|
||||
+47
-36
@@ -6,9 +6,11 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizPerson;
|
||||
@@ -159,7 +161,7 @@ public class BizAuthController extends BaseController {
|
||||
return error("所属主账号不存在, 请联系管理员");
|
||||
}
|
||||
if (UserStatus.DISABLE.getCode().equals(parent.getStatus())) {
|
||||
return error("所属企业主账号已被停用, 请联系企业管理员");
|
||||
return error("您所属的机构已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,8 +256,7 @@ public class BizAuthController extends BaseController {
|
||||
self.setPhone(phone);
|
||||
self.setOrgId(orgId);
|
||||
self.setDepartment("管理部");
|
||||
self.setPosition("总负责人");
|
||||
self.setRole("admin"); // 主账号 = 管理员 (与子账号的会议执行区分)
|
||||
self.setPosition("管理员");
|
||||
self.setUnitType("executor");
|
||||
self.setUserId(userId);
|
||||
self.setCreateBy(username);
|
||||
@@ -266,31 +267,55 @@ public class BizAuthController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册支持方 (主账号), 流程与 executor 一致:
|
||||
* 校验 → 查重 → 写 sys_user + role_type=sponsor → 写 biz_org (sponsor 类型)
|
||||
* 支持方注册下拉选项 (匿名公开): 从 biz_org(sponsor 类型) 搜企业, 含无主账号的 org.
|
||||
* 返回 [{orgId, orgName, mainUserId}], mainUserId 为 null 表示该企业还没有主账号.
|
||||
*/
|
||||
@GetMapping("/sponsorOrgOptions")
|
||||
public AjaxResult sponsorOrgOptions(@RequestParam(value = "orgName", required = false) String orgName) {
|
||||
BizOrg query = new BizOrg();
|
||||
query.setOrgName(orgName);
|
||||
return success(bizOrgService.selectSponsorRegisterOptions(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册支持方 (子账号 SUB): 从已存在的 sponsor 企业里选一家注册, 不再新建企业.
|
||||
* 校验 → 查重 → 写 sys_user (account_type=SUB + role_type=sponsor) → 写 biz_person 关联所选企业.
|
||||
* 主账号归属 (见 /sponsorOrgOptions 的 mainUserId):
|
||||
* - 企业已有主账号 (biz_org.user_id != null) → parent_user_id = 主账号 user_id
|
||||
* - 企业暂无主账号 (biz_org.user_id == null) → parent_user_id 留空, 待 admin/manager 后续分配
|
||||
*/
|
||||
@PostMapping("/registerSponsor")
|
||||
public AjaxResult registerSponsor(@RequestBody Map<String, Object> body) {
|
||||
String username = (String) body.get("username");
|
||||
String unitName = (String) body.get("unitName");
|
||||
String businessNature = (String) body.get("businessNature");
|
||||
String orgIdStr = body.get("orgId") == null ? null : body.get("orgId").toString();
|
||||
String phone = (String) body.get("phone");
|
||||
String code = (String) body.get("smsCode");
|
||||
String password = (String) body.get("password");
|
||||
String confirmPassword = (String) body.get("confirmPassword");
|
||||
String uuid = (String) body.get("uuid");
|
||||
|
||||
// 1. 基础校验 (与 executor 同)
|
||||
// 1. 基础校验
|
||||
if (username == null || username.length() < 4 || username.length() > 20) return error("用户名长度 4-20 位");
|
||||
if (!username.matches("^[A-Za-z0-9_]+$")) return error("用户名只能包含字母/数字/下划线");
|
||||
if (unitName == null || unitName.isEmpty()) return error("企业名称不能为空");
|
||||
if (businessNature == null || businessNature.isEmpty()) return error("企业性质不能为空");
|
||||
if (orgIdStr == null || orgIdStr.isEmpty()) return error("请选择企业");
|
||||
if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
|
||||
if (code == null || code.isEmpty()) return error("请输入短信验证码");
|
||||
if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位");
|
||||
if (!password.equals(confirmPassword)) return error("两次密码输入不一致");
|
||||
|
||||
// 2. 校验短信验证码
|
||||
// 2. 校验所选企业存在且为 sponsor (不新建企业)
|
||||
Long orgId;
|
||||
try {
|
||||
orgId = Long.valueOf(orgIdStr);
|
||||
} catch (NumberFormatException e) {
|
||||
return error("企业参数错误");
|
||||
}
|
||||
BizOrg org = bizOrgService.getById(orgId);
|
||||
if (org == null) return error("所选企业不存在");
|
||||
if (!"sponsor".equals(org.getOrgType())) return error("所选企业不是支持方");
|
||||
Long mainUserId = org.getUserId(); // 可为 null: 无主账号时留空待分配
|
||||
|
||||
// 3. 校验短信验证码
|
||||
SmsValidForm smsForm = new SmsValidForm();
|
||||
smsForm.setPhone(phone);
|
||||
smsForm.setSmsCode(code);
|
||||
@@ -301,7 +326,7 @@ public class BizAuthController extends BaseController {
|
||||
return error("验证码错误或已过期: " + e.getMessage());
|
||||
}
|
||||
|
||||
// 3. 查重
|
||||
// 4. 查重
|
||||
if (userService.isPhoneRegistered(phone)) {
|
||||
return error("该手机号已注册, 请直接登录");
|
||||
}
|
||||
@@ -311,42 +336,28 @@ public class BizAuthController extends BaseController {
|
||||
return error("用户名已被占用: " + username);
|
||||
}
|
||||
|
||||
// 4. 写 sys_user (role_type=sponsor)
|
||||
// 5. 写 sys_user: 全部注册为 SUB 子账号
|
||||
// role_type 显式写 sponsor, 避免被 sys_user.role_type DB DEFAULT 'executor' 覆盖
|
||||
SysUser user = new SysUser();
|
||||
user.setUserName(username);
|
||||
user.setNickName(unitName);
|
||||
user.setNickName(org.getOrgName()); // 昵称用所选企业名
|
||||
user.setPhonenumber(phone);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setStatus("0");
|
||||
user.setAccountType("SUB");
|
||||
user.setParentUserId(mainUserId); // 有主账号则绑定, 无则留空待分配
|
||||
user.setRoleType("sponsor");
|
||||
userService.insertUser(user);
|
||||
Long userId = user.getUserId();
|
||||
userService.updateRoleType(userId, "sponsor");
|
||||
|
||||
// 5. 写 biz_org (sponsor 类型)
|
||||
BizOrg org = new BizOrg();
|
||||
org.setOrgId(null);
|
||||
org.setUserId(userId); // 关联主账号, 让系统能反查 "我的公司"
|
||||
org.setOrgName(unitName);
|
||||
org.setOrgType("sponsor");
|
||||
org.setBusinessNature(businessNature);
|
||||
org.setContactPhone(phone);
|
||||
org.setContactName(unitName);
|
||||
org.setStatus("0");
|
||||
bizOrgService.insert(org);
|
||||
Long orgId = org.getOrgId();
|
||||
|
||||
// 6. 同步建 biz_person (主账号自己 = 管理员, 让"人员管理"能看到自己)
|
||||
// 注意: 不能走 IBizPersonService.insert(BizPerson, Long), 那个方法是创建 SUB 子账号的
|
||||
// (强制 accountType='SUB' + 需 loginUsername, 主账号自己不需要).
|
||||
// sys_user 已在 step 4 建好, 这里只需把 biz_person 关联到主账号自己.
|
||||
// 6. 写 biz_person (关联到所选企业, 不再新建 biz_org)
|
||||
BizPerson self = new BizPerson();
|
||||
SnowflakeId.injectIfEmpty(self, "personId");
|
||||
self.setName(unitName);
|
||||
self.setName(username); // 表单无姓名字段, 用登录用户名占位
|
||||
self.setPhone(phone);
|
||||
self.setOrgId(orgId);
|
||||
self.setDepartment("管理部");
|
||||
self.setPosition("总负责人");
|
||||
self.setRole("admin"); // 主账号 = 管理员 (与子账号的监察员区分)
|
||||
self.setDepartment("待分配");
|
||||
self.setPosition("员工");
|
||||
self.setUnitType("sponsor");
|
||||
self.setUserId(userId);
|
||||
self.setCreateBy(username);
|
||||
|
||||
+65
-21
@@ -9,6 +9,7 @@ import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
@@ -210,27 +211,7 @@ public class BizMeetingAttendeeController extends BaseController {
|
||||
List<BizMeetingAttendee> list = attendeeService.selectByMeetingId(meetingId);
|
||||
List<BizMeetingAttendeeImportVo> exportList = new ArrayList<>(list.size());
|
||||
for (BizMeetingAttendee a : list) {
|
||||
BizMeetingAttendeeImportVo v = new BizMeetingAttendeeImportVo();
|
||||
v.setName(a.getName());
|
||||
v.setPhone(a.getPhone());
|
||||
v.setWorkUnit(a.getWorkUnit());
|
||||
v.setDepartment(a.getDepartment());
|
||||
v.setTitle(a.getTitle());
|
||||
v.setIdCard(a.getIdCard());
|
||||
v.setBankName(a.getBankName());
|
||||
v.setBankCard(a.getBankCard());
|
||||
v.setBankBranch(a.getBankBranch());
|
||||
v.setAccountName(a.getAccountName());
|
||||
v.setBankRegion(a.getBankRegion());
|
||||
v.setBankAddress(a.getBankAddress());
|
||||
v.setIdCardAttachments(a.getIdCardAttachments());
|
||||
v.setLaborForm(a.getLaborForm());
|
||||
v.setFeePreTax(a.getFeePreTax());
|
||||
v.setTax(a.getTax());
|
||||
v.setVatAndSurcharge(a.getVatAndSurcharge());
|
||||
v.setFee(a.getFee());
|
||||
v.setSummary(a.getSummary());
|
||||
exportList.add(v);
|
||||
exportList.add(BizMeetingAttendeeImportVo.from(a));
|
||||
}
|
||||
ExcelUtil<BizMeetingAttendeeImportVo> util = new ExcelUtil<>(BizMeetingAttendeeImportVo.class);
|
||||
util.exportExcel(response, exportList, "参会人");
|
||||
@@ -267,4 +248,67 @@ public class BizMeetingAttendeeController extends BaseController {
|
||||
}
|
||||
return toAjax(rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载"劳务协议"空目录模板 zip (参会人管理 "下载协议模板" 按钮).
|
||||
* zip 内为 劳务协议/{序号}_{姓名}/ 空目录, 用户把签好的协议放进对应目录后重新压缩上传.
|
||||
* 仅 admin/manager 可触发.
|
||||
*/
|
||||
@GetMapping("/agreementTemplate/{meetingId}")
|
||||
public void agreementTemplate(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception {
|
||||
requireManagerOrAdmin();
|
||||
byte[] data = attendeeService.buildAgreementTemplateZip(meetingId);
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=agreement-template.zip");
|
||||
response.getOutputStream().write(data);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传"劳务协议" zip, 解压后按 劳务协议/{序号}_{姓名}/ 目录匹配参会人,
|
||||
* 上传 OSS 并回填 labor_protocol. 仅 admin/manager 可触发.
|
||||
*/
|
||||
@Log(title = "劳务协议回填", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/uploadAgreements")
|
||||
public AjaxResult uploadAgreements(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||
requireManagerOrAdmin();
|
||||
int updated = attendeeService.uploadAgreements(file, meetingId);
|
||||
return success(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载"专家照片"空目录模板 zip (专家照片 "下载目录模板" 按钮).
|
||||
* zip 内为 专家照片/{序号}_{姓名}/ 空目录. 仅 admin/manager 可触发.
|
||||
*/
|
||||
@GetMapping("/expertPhotoTemplate/{meetingId}")
|
||||
public void expertPhotoTemplate(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception {
|
||||
requireManagerOrAdmin();
|
||||
byte[] data = attendeeService.buildExpertPhotoTemplateZip(meetingId);
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=expert-photo-template.zip");
|
||||
response.getOutputStream().write(data);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传"专家照片" zip, 解压后按 专家照片/{序号}_{姓名}/ 目录匹配参会人,
|
||||
* 上传 OSS 并逗号拼接回填 on_site_photos. 仅 admin/manager 可触发.
|
||||
*/
|
||||
@Log(title = "专家照片回填", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/uploadExpertPhotos")
|
||||
public AjaxResult uploadExpertPhotos(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||
requireManagerOrAdmin();
|
||||
int updated = attendeeService.uploadExpertPhotos(file, meetingId);
|
||||
return success(updated);
|
||||
}
|
||||
|
||||
/** 仅 admin/manager 可操作 (参会人劳务协议/专家照片的批量收发是管理端功能) */
|
||||
private void requireManagerOrAdmin() {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可操作");
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
-109
@@ -1,5 +1,6 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -22,9 +23,11 @@ import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizProject;
|
||||
import com.ruoyi.business.notify.BizNotifyService;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
import com.ruoyi.business.service.IBizOrgService;
|
||||
import com.ruoyi.business.service.IBizProjectService;
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
import com.ruoyi.business.service.StageDeriver;
|
||||
@@ -58,6 +61,8 @@ public class BizMeetingController extends BaseController {
|
||||
@Autowired
|
||||
private IBizProjectService bizProjectService;
|
||||
@Autowired
|
||||
private IBizOrgService bizOrgService;
|
||||
@Autowired
|
||||
private StageDeriver stageDeriver;
|
||||
@Autowired
|
||||
private PosterService posterService;
|
||||
@@ -70,7 +75,7 @@ public class BizMeetingController extends BaseController {
|
||||
if ("doctor".equals(roleType) || "expert".equals(roleType)) {
|
||||
bizMeeting.setUserId(uid);
|
||||
}
|
||||
// sponsor 数据权限: 只看"我的项目"下的会议 (MAIN 走 sponsor_admin_user_id, SUB 走 sponsor_assign.monitor_user_id).
|
||||
// sponsor 数据权限: 只看"我的项目"下的会议 (MAIN 走 sponsor_org_id, SUB 走 sponsor_assign.monitor_user_id).
|
||||
// 与项目列表 selectSponsorList 的 MAIN/SUB 判定平行, 但刻意不带 biz_publicity_support_intent 关联.
|
||||
else if ("sponsor".equals(roleType)) {
|
||||
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||
@@ -81,7 +86,7 @@ public class BizMeetingController extends BaseController {
|
||||
}
|
||||
}
|
||||
// executor 数据权限: 只看"我的项目"下的会议 (与项目列表 selectExecutorList/selectExecutorStaffList 同源).
|
||||
// MAIN 走 biz_project_assign (exec_user_id / execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id.
|
||||
// MAIN 走 biz_project_assign (execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id.
|
||||
else if ("executor".equals(roleType)) {
|
||||
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||
if (current != null && "SUB".equals(current.getAccountType())) {
|
||||
@@ -129,6 +134,8 @@ public class BizMeetingController extends BaseController {
|
||||
bizMeeting.setCreateTime(new Date());
|
||||
bizMeeting.setUpdateBy(SecurityUtils.getUsername());
|
||||
bizMeeting.setUpdateTime(new Date());
|
||||
// 提交截止时间: 建会时按 end_time + 项目 submit_deadline_days 天 落库 (项目未设天数则为 null → 永不冻结)
|
||||
bizMeeting.setSubmitDeadline(computeSubmitDeadline(bizMeeting.getProjectId(), bizMeeting.getEndTime()));
|
||||
int rows = bizMeetingService.insert(bizMeeting);
|
||||
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
|
||||
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
|
||||
@@ -230,45 +237,10 @@ public class BizMeetingController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行人员提交凭证 (校验 LV_PAYMENT + SV_PAYMENT)
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/submit-voucher")
|
||||
public AjaxResult submitVoucher(@PathVariable("meetingId") Long meetingId) {
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
if (!bizProjectService.isExecutorOfProject(m.getProjectId(), userId)) {
|
||||
throw new ServiceException("您不是该项目的执行方, 无法提交凭证");
|
||||
}
|
||||
if (!isExecuted(m)) throw new ServiceException("会议尚未执行, 不能提交凭证");
|
||||
if (isFrozen(m)) throw new ServiceException("会议已冻结, 不能提交凭证");
|
||||
String stage = m.getVoucherAuditStage();
|
||||
if (!"NOT_SUBMITTED".equals(stage) && !"REJECTED".equals(stage)) {
|
||||
throw new ServiceException("当前阶段 (" + stage + ") 不允许提交凭证");
|
||||
}
|
||||
|
||||
List<BizMeetingMaterial> mats = bizMeetingMaterialService.selectByMeetingId(meetingId);
|
||||
boolean hasLv = mats.stream().anyMatch(x -> "LV_PAYMENT".equals(x.getSubType()));
|
||||
boolean hasSv = mats.stream().anyMatch(x -> "SV_PAYMENT".equals(x.getSubType()));
|
||||
if (!hasLv || !hasSv) {
|
||||
throw new ServiceException("请同时上传劳务凭证和会务凭证");
|
||||
}
|
||||
|
||||
m.setVoucherAuditStage("SUBMITTED");
|
||||
m.setVoucherComplianceApproved(0);
|
||||
m.setVoucherAuditTime(new Date());
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
appendAuditLog(m, "VOUCHER", "SUBMITTED", "执行人员提交凭证");
|
||||
return success("SUBMITTED");
|
||||
}
|
||||
|
||||
/**
|
||||
* 合规审核 (role_type=manager), 两级审核中的第一级.
|
||||
* 合规审核 (role_type=manager), 材料两级审核中的第一级.
|
||||
* <p>
|
||||
* body: { "auditType": "MATERIAL"|"VOUCHER"|"BOTH", "approved": true|false, "opinion": "..." }
|
||||
* <p>合规审中判据: stage=SUBMITTED 且 compliance_approved=0.
|
||||
* body: { "approved": true|false, "opinion": "..." }
|
||||
* <p>合规审中判据: material_audit_stage=SUBMITTED 且 compliance_approved=0.
|
||||
* 通过 → compliance_approved=1 (转入支持方审), 拒绝 → REJECTED (退回执行方).
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@@ -280,97 +252,97 @@ public class BizMeetingController extends BaseController {
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
|
||||
String[] types = resolveTypes(body.getAuditType());
|
||||
boolean approved = Boolean.TRUE.equals(body.getApproved());
|
||||
if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
||||
throw new ServiceException("拒绝时意见不能为空");
|
||||
}
|
||||
|
||||
String result = approved ? "APPROVED" : "REJECTED";
|
||||
for (String type : types) {
|
||||
String stage = stageOf(m, type);
|
||||
boolean complianceDone = complianceApprovedOf(m, type);
|
||||
if (!"SUBMITTED".equals(stage) || complianceDone) {
|
||||
throw new ServiceException(type + " 当前阶段不允许合规审核");
|
||||
}
|
||||
if (approved) {
|
||||
setComplianceApproved(m, type, 1);
|
||||
} else {
|
||||
setStage(m, type, "REJECTED");
|
||||
}
|
||||
setAuditTime(m, type, new Date());
|
||||
Integer compliance = m.getMaterialComplianceApproved();
|
||||
if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || (compliance != null && compliance == 1)) {
|
||||
throw new ServiceException("当前阶段不允许合规审核");
|
||||
}
|
||||
|
||||
String result = approved ? "APPROVED" : "REJECTED";
|
||||
if (approved) {
|
||||
m.setMaterialComplianceApproved(1);
|
||||
} else {
|
||||
m.setMaterialAuditStage("REJECTED");
|
||||
// 退回 → 提交截止时间重新计算 (now + 项目天数)
|
||||
m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date()));
|
||||
}
|
||||
m.setMaterialAuditTime(new Date());
|
||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
for (String type : types) {
|
||||
appendAuditLog(m, type, result, body.getOpinion());
|
||||
}
|
||||
appendAuditLog(m, "MATERIAL", result, body.getOpinion());
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持方(监察员) 审核, 两级审核中的第二级.
|
||||
* 支持方(监察员) 审核, 材料两级审核中的第二级.
|
||||
* <p>
|
||||
* body: { "auditType": "MATERIAL"|"VOUCHER"|"BOTH", "approved": true|false, "opinion": "..." }
|
||||
* <p>支持方审中判据: stage=SUBMITTED 且 compliance_approved=1.
|
||||
* 通过 → APPROVED (材料通过时一并写监管意见), 拒绝 → REJECTED (退回执行方).
|
||||
* body: { "approved": true|false, "opinion": "..." }
|
||||
* <p>支持方审中判据: material_audit_stage=SUBMITTED 且 compliance_approved=1.
|
||||
* 通过 → APPROVED (一并写监管意见), 拒绝 → REJECTED (退回执行方).
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/audit-supervision")
|
||||
public AjaxResult auditSupervision(@PathVariable("meetingId") Long meetingId, @RequestBody AuditBody body) {
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
Long myOrgId = bizOrgService.selectOrgIdByUserId(userId);
|
||||
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
|
||||
// 授权: 监察员 (biz_meeting_supervisor) 或 支持方 MAIN 账号 (biz_project.sponsor_admin_user_id) 均可审
|
||||
// 授权: 监察员 (biz_meeting_supervisor.sponsor_org_id) 或 支持方企业 (biz_project.sponsor_org_id) 均可审
|
||||
boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream()
|
||||
.anyMatch(s -> userId.equals(s.getUserId()));
|
||||
.anyMatch(s -> myOrgId != null && myOrgId.equals(s.getSponsorOrgId()));
|
||||
BizProject project = m.getProjectId() == null ? null : bizProjectService.getById(m.getProjectId());
|
||||
boolean isSponsorMain = project != null && userId.equals(project.getSponsorAdminUserId());
|
||||
boolean isSponsorMain = project != null && myOrgId != null && myOrgId.equals(project.getSponsorOrgId());
|
||||
if (!isSupervisor && !isSponsorMain) {
|
||||
throw new ServiceException("您不是该会议监察员, 无权监察");
|
||||
}
|
||||
|
||||
String[] types = resolveTypes(body.getAuditType());
|
||||
boolean approved = Boolean.TRUE.equals(body.getApproved());
|
||||
if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
||||
throw new ServiceException("拒绝时意见不能为空");
|
||||
}
|
||||
|
||||
String result = approved ? "APPROVED" : "REJECTED";
|
||||
for (String type : types) {
|
||||
String stage = stageOf(m, type);
|
||||
boolean complianceDone = complianceApprovedOf(m, type);
|
||||
if (!"SUBMITTED".equals(stage) || !complianceDone) {
|
||||
throw new ServiceException(type + " 当前阶段不允许监察审核");
|
||||
}
|
||||
setStage(m, type, approved ? "APPROVED" : "REJECTED");
|
||||
setAuditTime(m, type, new Date());
|
||||
Integer compliance = m.getMaterialComplianceApproved();
|
||||
if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || compliance == null || compliance != 1) {
|
||||
throw new ServiceException("当前阶段不允许监察审核");
|
||||
}
|
||||
|
||||
String result = approved ? "APPROVED" : "REJECTED";
|
||||
m.setMaterialAuditStage(approved ? "APPROVED" : "REJECTED");
|
||||
// 退回 → 提交截止时间重新计算 (now + 项目天数)
|
||||
if (!approved) {
|
||||
m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date()));
|
||||
}
|
||||
m.setMaterialAuditTime(new Date());
|
||||
// 材料通过 → 写监管意见 (支持方的书面意见)
|
||||
if (approved && java.util.Arrays.asList(types).contains("MATERIAL")) {
|
||||
if (approved) {
|
||||
m.setSupervisionOpinion(body.getOpinion());
|
||||
m.setSupervisionBy(SecurityUtils.getUsername());
|
||||
m.setSupervisionTime(new Date());
|
||||
}
|
||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
for (String type : types) {
|
||||
appendAuditLog(m, type, result, body.getOpinion());
|
||||
}
|
||||
appendAuditLog(m, "MATERIAL", result, body.getOpinion());
|
||||
|
||||
// 退回 → 通知执行方 (待整改 + 说明)
|
||||
if (!approved) {
|
||||
for (BizMeetingExecutor e : bizMeetingExecutorService.selectByMeetingId(meetingId)) {
|
||||
bizNotifyService.meetingSupervisionRejected(e.getUserId(), meetingId, m.getMeetingName(), body.getOpinion());
|
||||
bizNotifyService.meetingSupervisionRejected(resolveMainUserIdByOrgId(e.getExecutorOrgId()), meetingId, m.getMeetingName(), body.getOpinion());
|
||||
}
|
||||
}
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算 (合规/管理员 手动点击). 前置: material+voucher 都 APPROVED.
|
||||
* 结算 (合规/管理员 手动点击).
|
||||
* <p>
|
||||
* 前置: 材料已 APPROVED + 费用已汇总 (fee_calc_status=1) + 劳务/会务付款凭证都已上传.
|
||||
* 凭证由合规人员在"劳务凭证/会务凭证" tab 随时上传保存, 结算时校验是否已存在, 不再随结算一起提交.
|
||||
*/
|
||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/settle")
|
||||
@@ -382,8 +354,8 @@ public class BizMeetingController extends BaseController {
|
||||
}
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
if (!"APPROVED".equals(m.getMaterialAuditStage()) || !"APPROVED".equals(m.getVoucherAuditStage())) {
|
||||
throw new ServiceException("材料与凭证均审核通过后才能结算");
|
||||
if (!"APPROVED".equals(m.getMaterialAuditStage())) {
|
||||
throw new ServiceException("材料审核通过后才能结算");
|
||||
}
|
||||
if (isSettled(m)) throw new ServiceException("会议已结算");
|
||||
// 费用未汇总完 (fee_calc_status=0) 禁止结算: 此时 labor_fee/meeting_fee 可能为旧值/0, 直接回写会污染项目金额.
|
||||
@@ -391,6 +363,15 @@ public class BizMeetingController extends BaseController {
|
||||
if (m.getFeeCalcStatus() == null || m.getFeeCalcStatus() != 1) {
|
||||
throw new ServiceException("会议费用尚未汇总完成,无法结算");
|
||||
}
|
||||
|
||||
// 凭证校验: 劳务(LV_PAYMENT) + 会务(SV_PAYMENT) 必须已上传保存, 否则拒绝结算
|
||||
List<BizMeetingMaterial> existingMaterials = bizMeetingMaterialService.selectByMeetingId(meetingId);
|
||||
boolean hasLv = existingMaterials != null && existingMaterials.stream().anyMatch(v -> "LV_PAYMENT".equals(v.getSubType()) && v.getOssUrl() != null && !v.getOssUrl().isEmpty());
|
||||
boolean hasSv = existingMaterials != null && existingMaterials.stream().anyMatch(v -> "SV_PAYMENT".equals(v.getSubType()) && v.getOssUrl() != null && !v.getOssUrl().isEmpty());
|
||||
if (!hasLv || !hasSv) {
|
||||
throw new ServiceException("请先上传付款凭证");
|
||||
}
|
||||
|
||||
m.setIsSettled(1);
|
||||
m.setSettleTime(new Date());
|
||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||
@@ -425,6 +406,30 @@ public class BizMeetingController extends BaseController {
|
||||
return success("FINISHED");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解冻 (合规/管理员 手动点击). 前置: is_frozen=1.
|
||||
* 清除冻结标记 (is_frozen=0), 保留 freeze_time 作为"曾被冻结过"的历史凭证 (列表据此显示"超时提交"),
|
||||
* 提交截止时间重新计算 (now + 项目天数) → 冻结倒计时重新开始.
|
||||
*/
|
||||
@Log(title = "会议", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/unfreeze")
|
||||
public AjaxResult unfreeze(@PathVariable("meetingId") Long meetingId) {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"manager".equals(roleType) && !"admin".equals(roleType)) {
|
||||
throw new ServiceException("只有合规或管理员可解冻");
|
||||
}
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
if (m == null) throw new ServiceException("会议不存在");
|
||||
if (!isFrozen(m)) throw new ServiceException("会议未冻结, 无需解冻");
|
||||
m.setIsFrozen(0);
|
||||
m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date()));
|
||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||
m.setUpdateBy(SecurityUtils.getUsername());
|
||||
m.setUpdateTime(new Date());
|
||||
bizMeetingService.updateByPrimaryKey(m);
|
||||
return success("UNFROZEN");
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核轨迹 (audit_log 列表, 按时间排序)
|
||||
*/
|
||||
@@ -445,32 +450,25 @@ public class BizMeetingController extends BaseController {
|
||||
private static boolean isSettled(BizMeeting m) { return m.getIsSettled() != null && m.getIsSettled() == 1; }
|
||||
private static boolean isFinished(BizMeeting m) { return m.getIsFinished() != null && m.getIsFinished() == 1; }
|
||||
|
||||
/** 材料/凭证 子状态读取 */
|
||||
private static String stageOf(BizMeeting m, String type) {
|
||||
return "MATERIAL".equals(type) ? m.getMaterialAuditStage() : m.getVoucherAuditStage();
|
||||
}
|
||||
private static boolean complianceApprovedOf(BizMeeting m, String type) {
|
||||
Integer v = "MATERIAL".equals(type) ? m.getMaterialComplianceApproved() : m.getVoucherComplianceApproved();
|
||||
return v != null && v == 1;
|
||||
}
|
||||
private static void setStage(BizMeeting m, String type, String stage) {
|
||||
if ("MATERIAL".equals(type)) m.setMaterialAuditStage(stage);
|
||||
else m.setVoucherAuditStage(stage);
|
||||
}
|
||||
private static void setComplianceApproved(BizMeeting m, String type, int v) {
|
||||
if ("MATERIAL".equals(type)) m.setMaterialComplianceApproved(v);
|
||||
else m.setVoucherComplianceApproved(v);
|
||||
}
|
||||
private static void setAuditTime(BizMeeting m, String type, Date t) {
|
||||
if ("MATERIAL".equals(type)) m.setMaterialAuditTime(t);
|
||||
else m.setVoucherAuditTime(t);
|
||||
/** org_id → MAIN 主账号 user_id (biz_org.user_id), null 安全 */
|
||||
private Long resolveMainUserIdByOrgId(Long orgId) {
|
||||
if (orgId == null) return null;
|
||||
BizOrg org = bizOrgService.getById(orgId);
|
||||
return org == null ? null : org.getUserId();
|
||||
}
|
||||
|
||||
/** auditType: MATERIAL / VOUCHER / BOTH → 处理类型数组 */
|
||||
private static String[] resolveTypes(String auditType) {
|
||||
if ("BOTH".equals(auditType)) return new String[] { "MATERIAL", "VOUCHER" };
|
||||
if ("MATERIAL".equals(auditType) || "VOUCHER".equals(auditType)) return new String[] { auditType };
|
||||
throw new ServiceException("auditType 必须是 MATERIAL / VOUCHER / BOTH");
|
||||
/**
|
||||
* 计算提交截止时间 = 基准时刻 + 项目 submit_deadline_days 天.
|
||||
* 项目缺失或未设天数 → 返回 null (会议永不冻结, 与旧 markFrozen 的 p.submit_deadline_days is not null 语义一致).
|
||||
*/
|
||||
private Date computeSubmitDeadline(Long projectId, Date base) {
|
||||
if (projectId == null) return null;
|
||||
BizProject p = bizProjectService.getById(projectId);
|
||||
if (p == null || p.getSubmitDeadlineDays() == null) return null;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(base == null ? new Date() : base);
|
||||
cal.add(Calendar.DAY_OF_MONTH, p.getSubmitDeadlineDays());
|
||||
return cal.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -494,14 +492,12 @@ public class BizMeetingController extends BaseController {
|
||||
|
||||
/** request body for audit endpoints */
|
||||
public static class AuditBody {
|
||||
private String auditType; // MATERIAL / VOUCHER / BOTH
|
||||
private Boolean approved; // true=通过 false=拒绝
|
||||
private String opinion; // 意见
|
||||
public String getAuditType() { return auditType; }
|
||||
public void setAuditType(String auditType) { this.auditType = auditType; }
|
||||
public Boolean getApproved() { return approved; }
|
||||
public void setApproved(Boolean approved) { this.approved = approved; }
|
||||
public String getOpinion() { return opinion; }
|
||||
public void setOpinion(String opinion) { this.opinion = opinion; }
|
||||
}
|
||||
|
||||
}
|
||||
+5
-5
@@ -29,20 +29,20 @@ public class BizMeetingExecutorController extends BaseController {
|
||||
|
||||
/**
|
||||
* 分配执行人员 (全删全插)
|
||||
* body: { "userIds": [1, 2, 3] }
|
||||
* body: { "orgIds": [1, 2, 3] } (执行方企业 org_id 列表)
|
||||
*/
|
||||
@Log(title = "会议-执行人员", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{meetingId}")
|
||||
public AjaxResult assign(@PathVariable("meetingId") Long meetingId, @RequestBody AssignBody body) {
|
||||
Long assignedBy = SecurityUtils.getUserId();
|
||||
int n = bizMeetingExecutorService.replaceByMeetingId(meetingId, body.getUserIds(), assignedBy);
|
||||
int n = bizMeetingExecutorService.replaceByMeetingId(meetingId, body.getOrgIds(), assignedBy);
|
||||
return success(n);
|
||||
}
|
||||
|
||||
/** request body wrapper */
|
||||
public static class AssignBody {
|
||||
private List<Long> userIds;
|
||||
public List<Long> getUserIds() { return userIds; }
|
||||
public void setUserIds(List<Long> userIds) { this.userIds = userIds; }
|
||||
private List<Long> orgIds;
|
||||
public List<Long> getOrgIds() { return orgIds; }
|
||||
public void setOrgIds(List<Long> orgIds) { this.orgIds = orgIds; }
|
||||
}
|
||||
}
|
||||
+108
@@ -3,15 +3,19 @@ package com.ruoyi.business.controller;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 会议材料 Controller
|
||||
* <p>
|
||||
@@ -58,6 +62,23 @@ public class BizMeetingMaterialController extends BaseController {
|
||||
return success(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合规/管理员 随时保存付款凭证 (LV_PAYMENT + SV_PAYMENT).
|
||||
* <p>
|
||||
* 只清/插这两个 subType, 不碰其他材料; 付款凭证非发票, 不触发 OCR, 不影响会议费用.
|
||||
* 结算前必须先已上传保存; 结算接口只校验凭证是否已存在.
|
||||
*/
|
||||
@Log(title = "会议材料", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{meetingId}/saveVouchers")
|
||||
public AjaxResult saveVouchers(@PathVariable("meetingId") Long meetingId, @RequestBody List<BizMeetingMaterial> vouchers) {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"manager".equals(roleType) && !"admin".equals(roleType)) {
|
||||
throw new ServiceException("只有合规或管理员可上传凭证");
|
||||
}
|
||||
bizMeetingMaterialService.saveVouchers(meetingId, vouchers);
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码拍照回传 (公开端点, ry-h5 手机端拍照直传 OSS 后回传 URL).
|
||||
* <p>
|
||||
@@ -69,4 +90,91 @@ public class BizMeetingMaterialController extends BaseController {
|
||||
bizMeetingMaterialService.upsertFromCamera(body.getMeetingId(), body.getSubType(), body.getOssUrl(), body.getExtraOssUrl());
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 会务下载: 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打 zip, 返回下载 URL.
|
||||
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
||||
*/
|
||||
@GetMapping("/{meetingId}/downloadZip")
|
||||
public AjaxResult downloadZip(@PathVariable("meetingId") Long meetingId) {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
||||
}
|
||||
// 注意: 不能用 success(String), 否则 URL 会被塞进 msg 字段 (BaseController.success(String) 重载陷阱)
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildServiceZipUrl(meetingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 劳务下载: 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息在 OSS 端打 zip, 返回下载 URL.
|
||||
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
||||
*/
|
||||
@GetMapping("/{meetingId}/downloadLaborZip")
|
||||
public AjaxResult downloadLaborZip(@PathVariable("meetingId") Long meetingId) {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
||||
}
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildLaborZipUrl(meetingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量会务下载: 多会议会务材料合并打一个 zip, 返回下载 URL.
|
||||
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
||||
*/
|
||||
@PostMapping("/batchDownloadZip")
|
||||
public AjaxResult batchDownloadZip(@RequestBody(required = false) BatchDownloadBody body) {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
||||
}
|
||||
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchServiceZipUrl(meetingIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量劳务下载: 多会议劳务材料(含参会人信息)合并打一个 zip, 返回下载 URL.
|
||||
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
||||
*/
|
||||
@PostMapping("/batchDownloadLaborZip")
|
||||
public AjaxResult batchDownloadLaborZip(@RequestBody(required = false) BatchDownloadBody body) {
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
||||
}
|
||||
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchLaborZipUrl(meetingIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 会务材料"打包上传" — 下载空目录模板 zip (会务材料/物料制作·酒店·…/ 全空目录).
|
||||
*/
|
||||
@GetMapping("/serviceTemplate/{meetingId}")
|
||||
public void serviceTemplate(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception {
|
||||
byte[] data = bizMeetingMaterialService.buildServiceTemplateZip(meetingId);
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=service-material-template.zip");
|
||||
response.getOutputStream().write(data);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 会务材料"打包上传" — 上传 zip, 解压后按目录名匹配 subType, 单文件直传 OSS / 多文件打 zip 传 OSS,
|
||||
* 回填 biz_meeting_material. 材料变化 → 会议费用待重算.
|
||||
*/
|
||||
@Log(title = "会务材料回填", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/uploadServiceMaterials")
|
||||
public AjaxResult uploadServiceMaterials(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||
int updated = bizMeetingMaterialService.uploadServiceMaterials(file, meetingId);
|
||||
bizMeetingService.markFeeCalcPending(meetingId);
|
||||
return success(updated);
|
||||
}
|
||||
|
||||
/** request body for batch download endpoints */
|
||||
public static class BatchDownloadBody {
|
||||
private List<Long> meetingIds;
|
||||
public List<Long> getMeetingIds() { return meetingIds; }
|
||||
public void setMeetingIds(List<Long> meetingIds) { this.meetingIds = meetingIds; }
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -29,20 +29,20 @@ public class BizMeetingSupervisorController extends BaseController {
|
||||
|
||||
/**
|
||||
* 分配监察员 (全删全插)
|
||||
* body: { "userIds": [1, 2, 3] }
|
||||
* body: { "orgIds": [1, 2, 3] } (支持方企业 org_id 列表)
|
||||
*/
|
||||
@Log(title = "会议-监察员", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{meetingId}")
|
||||
public AjaxResult assign(@PathVariable("meetingId") Long meetingId, @RequestBody AssignBody body) {
|
||||
Long assignedBy = SecurityUtils.getUserId();
|
||||
int n = bizMeetingSupervisorService.replaceByMeetingId(meetingId, body.getUserIds(), assignedBy);
|
||||
int n = bizMeetingSupervisorService.replaceByMeetingId(meetingId, body.getOrgIds(), assignedBy);
|
||||
return success(n);
|
||||
}
|
||||
|
||||
/** request body wrapper */
|
||||
public static class AssignBody {
|
||||
private List<Long> userIds;
|
||||
public List<Long> getUserIds() { return userIds; }
|
||||
public void setUserIds(List<Long> userIds) { this.userIds = userIds; }
|
||||
private List<Long> orgIds;
|
||||
public List<Long> getOrgIds() { return orgIds; }
|
||||
public void setOrgIds(List<Long> orgIds) { this.orgIds = orgIds; }
|
||||
}
|
||||
}
|
||||
+26
@@ -2,14 +2,19 @@ package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
import com.ruoyi.business.domain.vo.BizOrgImportVo;
|
||||
import com.ruoyi.business.service.IBizOrgService;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
|
||||
@@ -111,4 +116,25 @@ public class BizOrgController extends BaseController {
|
||||
public AjaxResult remove(@PathVariable Long[] orgIds) {
|
||||
return toAjax(bizOrgService.deleteByPrimaryKeys(orgIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载支持单位导入模板
|
||||
*/
|
||||
@GetMapping("/importTemplate")
|
||||
public void importTemplate(HttpServletResponse response) {
|
||||
ExcelUtil<BizOrgImportVo> util = new ExcelUtil<>(BizOrgImportVo.class);
|
||||
util.importTemplateExcel(response, "支持单位数据");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入支持单位 (只导 org, 不新增人员 — 不建 sys_user, userId 留空)
|
||||
* 返回 {@link ImportResult} 含成功/失败计数 + 失败明细 (行号 + 原因)
|
||||
*/
|
||||
@Log(title = "公司管理", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/importData")
|
||||
public AjaxResult importData(MultipartFile file) throws Exception {
|
||||
String operName = SecurityUtils.getUsername();
|
||||
ImportResult result = bizOrgService.importOrg(file, operName);
|
||||
return success(result);
|
||||
}
|
||||
}
|
||||
|
||||
+82
-13
@@ -13,6 +13,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.business.domain.BizPerson;
|
||||
import com.ruoyi.business.domain.BizPersonExecutorImportVO;
|
||||
import com.ruoyi.business.domain.BizPersonImportVO;
|
||||
import com.ruoyi.business.service.IBizPersonService;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
@@ -37,7 +38,7 @@ public class BizPersonController extends BaseController
|
||||
* (sys_user.parent_user_id → sys_user.user_id → biz_person.user_id 链路过滤)
|
||||
*/
|
||||
private static final java.util.Set<String> BUSINESS_MAIN_ROLES = java.util.Set.of(
|
||||
"executor", "sponsor", "doctor", "manager"
|
||||
"executor", "sponsor", "doctor"
|
||||
);
|
||||
|
||||
@GetMapping("/list")
|
||||
@@ -113,6 +114,16 @@ public class BizPersonController extends BaseController
|
||||
bizPerson.setUpdateBy(getUsername());
|
||||
return toAjax(bizPersonService.updateByPrimaryKey(bizPerson));
|
||||
}
|
||||
/**
|
||||
* 更换机构管理员 (admin/sponsor-people 管理员 switch)
|
||||
* body: { personId } — 把该人员晋升为机构 MAIN, 原管理员降为 SUB
|
||||
*/
|
||||
@Log(title = "更换机构管理员", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/changeAdmin")
|
||||
public AjaxResult changeAdmin(@RequestBody BizPerson bizPerson)
|
||||
{
|
||||
return toAjax(bizPersonService.changeOrgAdmin(bizPerson.getPersonId()));
|
||||
}
|
||||
@Log(title = "人员", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable String[] ids)
|
||||
@@ -124,7 +135,7 @@ public class BizPersonController extends BaseController
|
||||
|
||||
/**
|
||||
* 下载 sponsor 端人员导入模板 (.xlsx)
|
||||
* 模板列头按 BizPersonImportVO @Excel 注解自动生成 (中文: 姓名/手机号/工作单位/部门/职务/角色/状态)
|
||||
* 模板列头按 BizPersonImportVO @Excel 注解自动生成 (中文: 姓名/手机号/部门/职务)
|
||||
*/
|
||||
@GetMapping("/sponsorImportTemplate")
|
||||
public void sponsorImportTemplate(HttpServletResponse response)
|
||||
@@ -140,9 +151,10 @@ public class BizPersonController extends BaseController
|
||||
*/
|
||||
@Log(title = "人员批量导入", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/sponsorImport")
|
||||
public AjaxResult sponsorImport(@RequestParam("file") MultipartFile file) throws Exception
|
||||
public AjaxResult sponsorImport(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "orgId", required = false) Long orgId) throws Exception
|
||||
{
|
||||
return doImport(file, "sponsor");
|
||||
return doImport(file, "sponsor", orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,22 +164,23 @@ public class BizPersonController extends BaseController
|
||||
@GetMapping("/executorImportTemplate")
|
||||
public void executorImportTemplate(HttpServletResponse response)
|
||||
{
|
||||
ExcelUtil<BizPersonImportVO> util = new ExcelUtil<>(BizPersonImportVO.class);
|
||||
ExcelUtil<BizPersonExecutorImportVO> util = new ExcelUtil<>(BizPersonExecutorImportVO.class);
|
||||
util.importTemplateExcel(response, "人员导入");
|
||||
}
|
||||
|
||||
@Log(title = "人员批量导入", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/executorImport")
|
||||
public AjaxResult executorImport(@RequestParam("file") MultipartFile file) throws Exception
|
||||
public AjaxResult executorImport(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "orgId", required = false) Long orgId) throws Exception
|
||||
{
|
||||
return doImport(file, "executor");
|
||||
return doExecutorImport(file, orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用人员导入逻辑 (sponsor / executor 共用)
|
||||
* @param unitType 'sponsor' 或 'executor' - 决定 orgName 匹配和 person.unitType 字段
|
||||
*/
|
||||
private AjaxResult doImport(MultipartFile file, String unitType) throws Exception
|
||||
private AjaxResult doImport(MultipartFile file, String unitType, Long orgId) throws Exception
|
||||
{
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("请选择要上传的文件");
|
||||
@@ -187,16 +200,14 @@ public class BizPersonController extends BaseController
|
||||
r.put("name", row.getName());
|
||||
r.put("phone", row.getPhone());
|
||||
try {
|
||||
// 1. orgId 由 BizPersonServiceImpl.insert 按 mainUid + unitType 自动反查主账号 biz_org 兜底 (无需 Excel 提供)
|
||||
// 2. role 跟 unitType 强绑定: sponsor→supervisor, executor→meetingExecutor (无需 Excel 提供)
|
||||
|
||||
BizPerson p = new BizPerson();
|
||||
p.setName(row.getName());
|
||||
p.setPhone(row.getPhone());
|
||||
// p.setOrgId(orgId); ← 不再设置, service 层按 mainUserId 兜底
|
||||
if (orgId != null) {
|
||||
p.setOrgId(orgId);
|
||||
}
|
||||
p.setDepartment(row.getDepartment());
|
||||
p.setPosition(row.getPosition());
|
||||
p.setRole("sponsor".equals(unitType) ? "supervisor" : "meetingExecutor");
|
||||
p.setStatus("0"); // 默认正常
|
||||
p.setUnitType(unitType);
|
||||
p.setLoginUsername(row.getPhone()); // 用户名 = 手机号 (要求唯一)
|
||||
@@ -219,4 +230,62 @@ public class BizPersonController extends BaseController
|
||||
ajax.put("results", results);
|
||||
return ajax;
|
||||
}
|
||||
|
||||
/**
|
||||
* executor 端人员导入 (模板多一个必填「邮箱」列, 与 sponsor 分开, 不污染 sponsor 模板)
|
||||
*/
|
||||
private AjaxResult doExecutorImport(MultipartFile file, Long orgId) throws Exception
|
||||
{
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("请选择要上传的文件");
|
||||
}
|
||||
ExcelUtil<BizPersonExecutorImportVO> util = new ExcelUtil<>(BizPersonExecutorImportVO.class);
|
||||
List<BizPersonExecutorImportVO> rows = util.importExcel(file.getInputStream(), 0);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
throw new ServiceException("导入文件无有效数据");
|
||||
}
|
||||
Long mainUid = getUserId();
|
||||
List<java.util.Map<String, Object>> results = new ArrayList<>();
|
||||
int ok = 0;
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
BizPersonExecutorImportVO row = rows.get(i);
|
||||
java.util.Map<String, Object> r = new java.util.LinkedHashMap<>();
|
||||
r.put("rowNo", i + 2);
|
||||
r.put("name", row.getName());
|
||||
r.put("phone", row.getPhone());
|
||||
try {
|
||||
if (row.getEmail() == null || row.getEmail().trim().isEmpty()) {
|
||||
throw new ServiceException("邮箱不能为空");
|
||||
}
|
||||
BizPerson p = new BizPerson();
|
||||
p.setName(row.getName());
|
||||
p.setPhone(row.getPhone());
|
||||
p.setEmail(row.getEmail());
|
||||
p.setDepartment(row.getDepartment());
|
||||
p.setPosition(row.getPosition());
|
||||
p.setStatus("0");
|
||||
p.setUnitType("executor");
|
||||
if (orgId != null) {
|
||||
p.setOrgId(orgId);
|
||||
}
|
||||
p.setLoginUsername(row.getPhone());
|
||||
p.setLoginPassword("123456");
|
||||
p.setCreateBy(getUsername());
|
||||
p.setUpdateBy(getUsername());
|
||||
bizPersonService.insert(p, mainUid);
|
||||
ok++;
|
||||
r.put("ok", true);
|
||||
r.put("message", "成功");
|
||||
} catch (Exception e) {
|
||||
r.put("ok", false);
|
||||
r.put("message", e.getMessage());
|
||||
}
|
||||
results.add(r);
|
||||
}
|
||||
AjaxResult ajax = success();
|
||||
ajax.put("total", rows.size());
|
||||
ajax.put("ok", ok);
|
||||
ajax.put("results", results);
|
||||
return ajax;
|
||||
}
|
||||
}
|
||||
|
||||
+44
-19
@@ -17,9 +17,11 @@ import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizExecutionIntent;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizProject;
|
||||
import com.ruoyi.business.domain.BizProjectAssign;
|
||||
import com.ruoyi.business.domain.BizProjectRating;
|
||||
import com.ruoyi.business.service.IBizOrgService;
|
||||
import com.ruoyi.business.service.IBizProjectService;
|
||||
import com.ruoyi.business.service.IBizExecutionIntentService;
|
||||
import com.ruoyi.business.domain.BizProjectSponsorAssign;
|
||||
@@ -62,6 +64,8 @@ public class BizProjectController extends BaseController
|
||||
private BizSysUserQueryMapper bizSysUserQueryMapper;
|
||||
@Autowired
|
||||
private SysUserMapper sysUserMapper;
|
||||
@Autowired
|
||||
private IBizOrgService bizOrgService;
|
||||
|
||||
/**
|
||||
* 我报名的项目 (当前用户在 biz_execution_intent 里有意向的项目)
|
||||
@@ -75,10 +79,7 @@ public class BizProjectController extends BaseController
|
||||
q.setUserId(uid);
|
||||
List<BizExecutionIntent> intents = bizExecutionIntentService.selectList(q);
|
||||
if (intents == null || intents.isEmpty()) {
|
||||
TableDataInfo r = new TableDataInfo();
|
||||
r.setRows(new ArrayList<>());
|
||||
r.setTotal(0);
|
||||
return r;
|
||||
return getDataTable(new ArrayList<>());
|
||||
}
|
||||
java.util.Set<String> noSet = new java.util.LinkedHashSet<>();
|
||||
for (BizExecutionIntent it : intents) {
|
||||
@@ -198,15 +199,30 @@ public class BizProjectController extends BaseController
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable Long[] ids)
|
||||
{
|
||||
// 后端角色兜底: 仅 admin / manager 可删项目 (前端 Projects.vue 已 v-if, 此处防 devtools 绕过)
|
||||
// 后端角色兜底: 仅 admin 可删项目 (前端 Projects.vue 已 v-if, 此处防 devtools 绕过)
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或经理可删除项目");
|
||||
if (!"admin".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员可删除项目");
|
||||
}
|
||||
bizProjectService.softDeleteCascadeBatch(ids);
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除公告: 将 3 个公示 URL 字段置 NULL (未发布).
|
||||
* 后端角色兜底: 仅 admin / manager 可删公告 (前端 Projects.vue 已 v-if, 此处防 devtools 绕过).
|
||||
*/
|
||||
@Log(title = "项目", businessType = BusinessType.UPDATE)
|
||||
@DeleteMapping("/{projectId}/announcement")
|
||||
public AjaxResult clearAnnouncement(@PathVariable("projectId") Long projectId)
|
||||
{
|
||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||
throw new ServiceException("只有管理员或经理可删除公告");
|
||||
}
|
||||
return toAjax(bizProjectService.clearAnnouncement(projectId));
|
||||
}
|
||||
|
||||
// ========== 项目执行方分配 sub-resource ==========
|
||||
@GetMapping("/{projectId}/assigns")
|
||||
public AjaxResult getAssigns(@PathVariable("projectId") Long projectId)
|
||||
@@ -220,12 +236,12 @@ public class BizProjectController extends BaseController
|
||||
{
|
||||
if (assigns == null) assigns = new ArrayList<>();
|
||||
bizProjectAssignService.validateSum(projectId, assigns);
|
||||
// #3 通知去重: 拉旧数据按 execUserId 索引, 同 (execUserId, sessions, amount) → 无变化 → 跳过
|
||||
// 业务场景: manager 仅调整其他执行人时, 老的 executor 不应被打扰
|
||||
// #3 通知去重: 拉旧数据按 executionUnitId 索引, 同 (executionUnitId, sessions, amount) → 无变化 → 跳过
|
||||
// 业务场景: manager 仅调整其他执行单位时, 老的 executor 不应被打扰
|
||||
List<BizProjectAssign> oldList = bizProjectAssignService.selectByProjectId(projectId);
|
||||
Map<Long, BizProjectAssign> oldByExec = new HashMap<>();
|
||||
for (BizProjectAssign o : oldList) {
|
||||
if (o.getExecUserId() != null) oldByExec.put(o.getExecUserId(), o);
|
||||
if (o.getExecutionUnitId() != null) oldByExec.put(o.getExecutionUnitId(), o);
|
||||
}
|
||||
bizProjectAssignService.deleteByProjectId(projectId);
|
||||
// #3 通知: 查一次项目名, 避免循环里重复查 DB
|
||||
@@ -235,16 +251,18 @@ public class BizProjectController extends BaseController
|
||||
a.setProjectId(projectId);
|
||||
if (a.getStatus() == null) a.setStatus("0");
|
||||
bizProjectAssignService.insert(a);
|
||||
// 跳过空 execUserId 行 (notify 内部也会跳过, 这里直接 continue 省 lookup)
|
||||
if (a.getExecUserId() == null) continue;
|
||||
BizProjectAssign old = oldByExec.get(a.getExecUserId());
|
||||
// 跳过空 executionUnitId 行 (notify 内部也会跳过, 这里直接 continue 省 lookup)
|
||||
if (a.getExecutionUnitId() == null) continue;
|
||||
BizProjectAssign old = oldByExec.get(a.getExecutionUnitId());
|
||||
if (isAssignUnchanged(old, a)) {
|
||||
logger.debug("[projectAssign] execUserId={} (sessions, amount) 未变, 跳过通知", a.getExecUserId());
|
||||
logger.debug("[projectAssign] executionUnitId={} (sessions, amount) 未变, 跳过通知", a.getExecutionUnitId());
|
||||
continue;
|
||||
}
|
||||
// #3 通知被分配的 executor (待办: 去承接). 业务事务回滚时通知自动回滚
|
||||
// 执行单位 org_id → 主账号 MAIN user_id (biz_org.user_id) 派生
|
||||
Long mainUserId = resolveMainUserIdByOrgId(a.getExecutionUnitId());
|
||||
bizNotifyService.projectAssignedToExecutor(
|
||||
a.getExecUserId(), projectId, projectName, a.getSessions(), a.getAmount());
|
||||
mainUserId, projectId, projectName, a.getSessions(), a.getAmount());
|
||||
}
|
||||
return success();
|
||||
}
|
||||
@@ -264,6 +282,13 @@ public class BizProjectController extends BaseController
|
||||
return oa.compareTo(na) == 0;
|
||||
}
|
||||
|
||||
/** 执行单位 org_id → 主账号 MAIN user_id (biz_org.user_id), 无主账号返回 null */
|
||||
private Long resolveMainUserIdByOrgId(Long orgId) {
|
||||
if (orgId == null) return null;
|
||||
BizOrg org = bizOrgService.getById(orgId);
|
||||
return org == null ? null : org.getUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两条 sponsor 分配是否对监察员而言"未变".
|
||||
* 判定维度: assignDesc + assignPoints. (projectId 隐含相同, 旧数据就是本 projectId 的)
|
||||
@@ -374,7 +399,7 @@ public class BizProjectController extends BaseController
|
||||
}
|
||||
}
|
||||
body.setCreateBy(SecurityUtils.getUsername());
|
||||
body.setSponsorUserId(SecurityUtils.getUserId());
|
||||
body.setSponsorOrgId(bizOrgService.selectOrgIdByUserId(SecurityUtils.getUserId()));
|
||||
// 一项目支持 N 监察员: service 内一次性 delete + 逐个 insert, 不会循环 delete
|
||||
int inserted = bizProjectSponsorAssignService.assignMonitorsForProject(body, mids);
|
||||
|
||||
@@ -427,7 +452,7 @@ public class BizProjectController extends BaseController
|
||||
}
|
||||
}
|
||||
body.setCreateBy(SecurityUtils.getUsername());
|
||||
body.setExecutorUserId(SecurityUtils.getUserId());
|
||||
body.setExecutorOrgId(bizOrgService.selectOrgIdByUserId(SecurityUtils.getUserId()));
|
||||
// 一项目支持 N 执行人: service 内一次性 delete + 逐个 insert, 不会循环 delete
|
||||
int inserted = bizProjectExecutorAssignService.assignStaffForProject(body, sids);
|
||||
return toAjax(inserted);
|
||||
@@ -453,7 +478,7 @@ public class BizProjectController extends BaseController
|
||||
public TableDataInfo listByRole(SysUserExtendVo vo)
|
||||
{
|
||||
if (vo.getRoleType() == null || vo.getRoleType().isEmpty()) {
|
||||
return new TableDataInfo();
|
||||
return getDataTable(new ArrayList<>());
|
||||
}
|
||||
startPage();
|
||||
List<SysUserExtendVo> list = bizSysUserQueryMapper.selectActiveByRole(vo);
|
||||
@@ -483,7 +508,7 @@ public class BizProjectController extends BaseController
|
||||
throw new IllegalArgumentException("projectId / monitorUserId 必填");
|
||||
}
|
||||
body.setCreateBy(loginName);
|
||||
body.setSponsorUserId(loginUid);
|
||||
body.setSponsorOrgId(bizOrgService.selectOrgIdByUserId(loginUid));
|
||||
// 通知去重: 拉旧分配, 找同 monitorUserId, 比较 assignDesc/assignPoints 是否变化
|
||||
BizProjectSponsorAssign old = null;
|
||||
List<BizProjectSponsorAssign> oldList = bizProjectSponsorAssignService.listByProjectId(body.getProjectId());
|
||||
|
||||
@@ -61,7 +61,7 @@ public class BizMeeting extends BaseEntity {
|
||||
private Long projectId;
|
||||
/** 项目名称 */
|
||||
private String projectName;
|
||||
/** 所属公司名称 (冗余字段, 由 biz_project.sponsor_admin_user_name 同步) */
|
||||
/** 所属公司名称 (派生字段, 由 biz_project.sponsor_org_id JOIN biz_org.org_name 得出, 不落库) */
|
||||
private String orgName;
|
||||
/** 监察意见 */
|
||||
private String supervisionOpinion;
|
||||
@@ -72,8 +72,6 @@ public class BizMeeting extends BaseEntity {
|
||||
private Date supervisionTime;
|
||||
/** 材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */
|
||||
private String materialAuditStage;
|
||||
/** 凭证审核阶段 (同上) */
|
||||
private String voucherAuditStage;
|
||||
/** 是否执行 0否1是 (会议开始时间到, scheduler 置1) */
|
||||
private Integer isExecuted;
|
||||
/** 执行时间 */
|
||||
@@ -97,15 +95,15 @@ public class BizMeeting extends BaseEntity {
|
||||
/** 材料最近一次审核动作时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date materialAuditTime;
|
||||
/** 凭证最近一次审核动作时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date voucherAuditTime;
|
||||
/** 材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */
|
||||
private Integer materialComplianceApproved;
|
||||
/** 凭证合规是否已通过 0否1是 */
|
||||
private Integer voucherComplianceApproved;
|
||||
/** 邀请函URL */
|
||||
/** 提交截止时间 (建会 = end_time + 项目 submit_deadline_days 天; 退回/解冻 = now + 天数; null = 项目未设天数, 永不冻结) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date submitDeadline;
|
||||
/** 邀请函URL (会议级 biz_meeting.invitation_url) */
|
||||
private String invitationUrl;
|
||||
/** 项目邀请函URL (派生字段, 由 biz_project.invitation_url 子查询得出, 不落库; 医生会议列表用) */
|
||||
private String projectInvitationUrl;
|
||||
/** 日程海报URL */
|
||||
private String scheduleUrl;
|
||||
/** 生成的海报URL (生成海报按钮产出) */
|
||||
@@ -175,6 +173,8 @@ public class BizMeeting extends BaseEntity {
|
||||
public void setSupervisionTime(Date supervisionTime) { this.supervisionTime = supervisionTime; }
|
||||
public String getInvitationUrl() { return invitationUrl; }
|
||||
public void setInvitationUrl(String invitationUrl) { this.invitationUrl = invitationUrl; }
|
||||
public String getProjectInvitationUrl() { return projectInvitationUrl; }
|
||||
public void setProjectInvitationUrl(String projectInvitationUrl) { this.projectInvitationUrl = projectInvitationUrl; }
|
||||
public String getScheduleUrl() { return scheduleUrl; }
|
||||
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
|
||||
public String getPosterUrl() { return posterUrl; }
|
||||
@@ -183,8 +183,6 @@ public class BizMeeting extends BaseEntity {
|
||||
public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; }
|
||||
public String getMaterialAuditStage() { return materialAuditStage; }
|
||||
public void setMaterialAuditStage(String materialAuditStage) { this.materialAuditStage = materialAuditStage; }
|
||||
public String getVoucherAuditStage() { return voucherAuditStage; }
|
||||
public void setVoucherAuditStage(String voucherAuditStage) { this.voucherAuditStage = voucherAuditStage; }
|
||||
public Integer getIsExecuted() { return isExecuted; }
|
||||
public void setIsExecuted(Integer isExecuted) { this.isExecuted = isExecuted; }
|
||||
public Date getExecuteTime() { return executeTime; }
|
||||
@@ -203,12 +201,10 @@ public class BizMeeting extends BaseEntity {
|
||||
public void setFreezeTime(Date freezeTime) { this.freezeTime = freezeTime; }
|
||||
public Date getMaterialAuditTime() { return materialAuditTime; }
|
||||
public void setMaterialAuditTime(Date materialAuditTime) { this.materialAuditTime = materialAuditTime; }
|
||||
public Date getVoucherAuditTime() { return voucherAuditTime; }
|
||||
public void setVoucherAuditTime(Date voucherAuditTime) { this.voucherAuditTime = voucherAuditTime; }
|
||||
public Integer getMaterialComplianceApproved() { return materialComplianceApproved; }
|
||||
public void setMaterialComplianceApproved(Integer materialComplianceApproved) { this.materialComplianceApproved = materialComplianceApproved; }
|
||||
public Integer getVoucherComplianceApproved() { return voucherComplianceApproved; }
|
||||
public void setVoucherComplianceApproved(Integer voucherComplianceApproved) { this.voucherComplianceApproved = voucherComplianceApproved; }
|
||||
public Date getSubmitDeadline() { return submitDeadline; }
|
||||
public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getAttendeeId() { return attendeeId; }
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
* 会议审核流程日志对象 biz_meeting_audit_log
|
||||
* <p>
|
||||
* 记录会议审核的每一次流转: 谁、什么时间、什么意见、当前阶段.
|
||||
* 与 biz_meeting.material_audit_stage / voucher_audit_stage 配合, 还原审核轨迹.
|
||||
* 与 biz_meeting.material_audit_stage 配合, 还原审核轨迹.
|
||||
*/
|
||||
public class BizMeetingAuditLog {
|
||||
|
||||
|
||||
+10
-4
@@ -18,8 +18,11 @@ public class BizMeetingExecutor {
|
||||
/** 会议ID */
|
||||
private Long meetingId;
|
||||
|
||||
/** sys_user.user_id (executor 主账号, 后续支持子账号) */
|
||||
private Long userId;
|
||||
/** executor 企业 org_id (原 sys_user.user_id 主账号, 单一可信源) */
|
||||
private Long executorOrgId;
|
||||
|
||||
/** 执行企业名称 (派生字段, JOIN biz_org.org_name, 不落库) */
|
||||
private String orgName;
|
||||
|
||||
/** 分配人 user_id (审计) */
|
||||
private Long assignedBy;
|
||||
@@ -37,8 +40,11 @@ public class BizMeetingExecutor {
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getExecutorOrgId() { return executorOrgId; }
|
||||
public void setExecutorOrgId(Long executorOrgId) { this.executorOrgId = executorOrgId; }
|
||||
|
||||
public String getOrgName() { return orgName; }
|
||||
public void setOrgName(String orgName) { this.orgName = orgName; }
|
||||
|
||||
public Long getAssignedBy() { return assignedBy; }
|
||||
public void setAssignedBy(Long assignedBy) { this.assignedBy = assignedBy; }
|
||||
|
||||
+10
-4
@@ -18,8 +18,11 @@ public class BizMeetingSupervisor {
|
||||
/** 会议ID */
|
||||
private Long meetingId;
|
||||
|
||||
/** sys_user.user_id (sponsor 主账号, 后续支持子账号) */
|
||||
private Long userId;
|
||||
/** sponsor 企业 org_id (原 sys_user.user_id 主账号, 单一可信源) */
|
||||
private Long sponsorOrgId;
|
||||
|
||||
/** 监察企业名称 (派生字段, JOIN biz_org.org_name, 不落库) */
|
||||
private String orgName;
|
||||
|
||||
/** 分配人 user_id (审计) */
|
||||
private Long assignedBy;
|
||||
@@ -37,8 +40,11 @@ public class BizMeetingSupervisor {
|
||||
public Long getMeetingId() { return meetingId; }
|
||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getSponsorOrgId() { return sponsorOrgId; }
|
||||
public void setSponsorOrgId(Long sponsorOrgId) { this.sponsorOrgId = sponsorOrgId; }
|
||||
|
||||
public String getOrgName() { return orgName; }
|
||||
public void setOrgName(String orgName) { this.orgName = orgName; }
|
||||
|
||||
public Long getAssignedBy() { return assignedBy; }
|
||||
public void setAssignedBy(Long assignedBy) { this.assignedBy = assignedBy; }
|
||||
|
||||
@@ -29,9 +29,6 @@ public class BizPerson extends BaseEntity {
|
||||
/** position */
|
||||
@Excel(name = "position")
|
||||
private String position;
|
||||
/** role */
|
||||
@Excel(name = "role")
|
||||
private String role;
|
||||
/** create_by */
|
||||
@Excel(name = "create_by")
|
||||
private String createBy;
|
||||
@@ -60,6 +57,8 @@ public class BizPerson extends BaseEntity {
|
||||
/** 登录账号 (来自 sys_user.user_name, 跟 accountType / parentUserId 一样仅展示, 不入库) */
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("account")
|
||||
private String account;
|
||||
/** 邮箱 (来自 sys_user.email, 展示 + 前端回传写入 sys_user, 不入 biz_person 表) */
|
||||
private String email;
|
||||
/** 子账号登录账号 (前端传入, 用于创建 sys_user 子账号) - 非持久化字段 */
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("userName")
|
||||
private transient String loginUsername;
|
||||
@@ -82,8 +81,6 @@ public class BizPerson extends BaseEntity {
|
||||
public void setDepartment(String department) { this.department = department; }
|
||||
public String getPosition() { return position; }
|
||||
public void setPosition(String position) { this.position = position; }
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String role) { this.role = role; }
|
||||
/** 兼容前端调用, 实际读 sys_user.delFlag */
|
||||
public String getDelFlag() { return null; }
|
||||
public void setDelFlag(String delFlag) { /* noop - 跟 sys_user 同步 */ }
|
||||
@@ -107,6 +104,8 @@ public class BizPerson extends BaseEntity {
|
||||
public void setParentUserId(Long parentUserId) { this.parentUserId = parentUserId; }
|
||||
public String getAccount() { return account; }
|
||||
public void setAccount(String account) { this.account = account; }
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
public String getLoginUsername() { return loginUsername; }
|
||||
public void setLoginUsername(String loginUsername) { this.loginUsername = loginUsername; }
|
||||
public String getLoginPassword() { return loginPassword; }
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.business.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
* executor 端人员批量导入 VO (中文列头, 用 ruoyi ExcelUtil 解析)
|
||||
*
|
||||
* <p>区别于 sponsor 端 {@link BizPersonImportVO}: executor 多一个必填「邮箱」列,
|
||||
* 列头 5 列: 姓名/手机号/邮箱/部门/职务. 所属公司由后端兜底:
|
||||
* <ul>
|
||||
* <li>所属公司: 由 BizPersonServiceImpl.insert 按 mainUid + unitType 自动反查主账号的 biz_org</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>导入后端会按以下规则创建 sys_user 子账号 (parent_user_id=当前登录主账号):
|
||||
* <ul>
|
||||
* <li>loginUsername = 手机号 (要求唯一)</li>
|
||||
* <li>loginPassword = "123456" (默认密码)</li>
|
||||
* <li>email = 邮箱 (必填, 写入 sys_user.email)</li>
|
||||
* <li>unitType = "executor"</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class BizPersonExecutorImportVO
|
||||
{
|
||||
@Excel(name = "姓名", sort = 1)
|
||||
private String name;
|
||||
|
||||
@Excel(name = "手机号", sort = 2)
|
||||
private String phone;
|
||||
|
||||
@Excel(name = "邮箱", sort = 3)
|
||||
private String email;
|
||||
|
||||
@Excel(name = "部门", sort = 4)
|
||||
private String department;
|
||||
|
||||
@Excel(name = "职务", sort = 5)
|
||||
private String position;
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
public String getDepartment() { return department; }
|
||||
public void setDepartment(String department) { this.department = department; }
|
||||
public String getPosition() { return position; }
|
||||
public void setPosition(String position) { this.position = position; }
|
||||
}
|
||||
+2
-9
@@ -6,10 +6,9 @@ import com.ruoyi.common.annotation.Excel;
|
||||
* sponsor 端人员批量导入 VO (中文列头, 用 ruoyi ExcelUtil 解析)
|
||||
*
|
||||
* <p>区别于 BizPerson 实体: 这里只暴露用户能填写的业务字段, 不包含内部 id/审计字段,
|
||||
* 列头精简到 4 列: 姓名/手机号/部门/职务. 所属公司 + 角色 由后端兜底:
|
||||
* 列头精简到 4 列: 姓名/手机号/部门/职务. 所属公司由后端兜底:
|
||||
* <ul>
|
||||
* <li>所属公司: 由 BizPersonServiceImpl.insert 按 mainUid + unitType 自动反查主账号的 biz_org</li>
|
||||
* <li>角色: sponsor → supervisor (监察员), executor → meetingExecutor (会议执行), 跟 unitType 强绑定</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>导入后端会按以下规则创建 sys_user 子账号 (parent_user_id=当前登录主账号):
|
||||
@@ -20,7 +19,7 @@ import com.ruoyi.common.annotation.Excel;
|
||||
* <li>orgId = service 层按 mainUserId + unitType 兜底 (无需 Excel 提供)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>历史兼容: orgName/role 字段及 setter 保留, 但不再标注 @Excel; 用户上传老模板多列也能解析, 值被忽略.
|
||||
* <p>历史兼容: orgName 字段及 setter 保留, 但不再标注 @Excel; 用户上传老模板多列也能解析, 值被忽略.
|
||||
*/
|
||||
public class BizPersonImportVO
|
||||
{
|
||||
@@ -40,10 +39,6 @@ public class BizPersonImportVO
|
||||
@Excel(name = "职务", sort = 4)
|
||||
private String position;
|
||||
|
||||
// 角色: 跟 unitType 强绑定, sponsor→supervisor / executor→meetingExecutor, 无需用户填
|
||||
// 保留 setter 兼容老模板, 但不再导出为 Excel 列
|
||||
private String role;
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getPhone() { return phone; }
|
||||
@@ -54,6 +49,4 @@ public class BizPersonImportVO
|
||||
public void setDepartment(String department) { this.department = department; }
|
||||
public String getPosition() { return position; }
|
||||
public void setPosition(String position) { this.position = position; }
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String role) { this.role = role; }
|
||||
}
|
||||
|
||||
@@ -56,12 +56,12 @@ public class BizProject extends BaseEntity {
|
||||
private String sponsorAdminUserName;
|
||||
/** 支持单位名称 (列表展示列, JOIN biz_org 取 org_name, org_type='sponsor') */
|
||||
private String sponsorOrgName;
|
||||
/** 支持单位筛选 (sys_user.user_id 列表, IN 查询, 多选 select 透传) */
|
||||
private List<Long> sponsorAdminUserIds;
|
||||
/** 支持单位筛选 (biz_org.org_id 列表, IN 查询, 多选 select 透传) */
|
||||
private List<Long> sponsorOrgIds;
|
||||
/** 服务机构名称列表, 多个用英文逗号连接 (GROUP_CONCAT 派生, 不入库, 仅展示) */
|
||||
private String execOrgNames;
|
||||
/** 服务机构筛选 (biz_project_assign.exec_user_id 列表, IN 查询, 多选 select 透传) */
|
||||
private List<Long> execAdminUserIds;
|
||||
/** 服务机构筛选 (biz_project_assign.execution_unit_id 列表, IN 查询, 多选 select 透传) */
|
||||
private List<Long> execOrgIds;
|
||||
/** project_form */
|
||||
@Excel(name = "project_form")
|
||||
private String projectForm;
|
||||
@@ -71,9 +71,9 @@ public class BizProject extends BaseEntity {
|
||||
/** is_settled */
|
||||
@Excel(name = "is_settled")
|
||||
private String isSettled;
|
||||
/** 支持方负责人用户ID (sys_user.user_id, role_type=sponsor) — 原 org_id */
|
||||
@Excel(name = "sponsor_admin_user_id")
|
||||
private Long sponsorAdminUserId;
|
||||
/** 支持方企业ID (biz_org.org_id, org_type='sponsor') — 单一可信源, 负责人 MAIN user_id 由 biz_org.user_id 派生 */
|
||||
@Excel(name = "sponsor_org_id")
|
||||
private Long sponsorOrgId;
|
||||
/** 项目负责人 user_id (sys_user.user_id, role_type=manager 合规管理员) */
|
||||
@Excel(name = "lead_user_id")
|
||||
private Long leadUserId;
|
||||
@@ -122,6 +122,8 @@ public class BizProject extends BaseEntity {
|
||||
private String supportLetterUrl;
|
||||
/** 已发布公告URL */
|
||||
private String publishUrl;
|
||||
/** 日程文件URL */
|
||||
private String scheduleUrl;
|
||||
/** 是否已发布公示 0否 1是 */
|
||||
private String isPublished;
|
||||
/** 发布时间 */
|
||||
@@ -129,6 +131,11 @@ public class BizProject extends BaseEntity {
|
||||
private Date publishTime;
|
||||
/** 公告类型 (邀请函/支持函/通知/日程/公示) */
|
||||
private String announcementType;
|
||||
/** 开通截止时间 (date, 到期后由 OpenStatusScheduler 置 open_status=N) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date openDeadline;
|
||||
/** 开通状态 Y/N (默认 N; Y 由经理"开通"置位, 到期回收为 N) */
|
||||
private String openStatus;
|
||||
|
||||
/* ============ 支持方评分字段 (来自 biz_project_sponsor 中间表, 仅展示用) ============ */
|
||||
/** 当前 login 用户对该项目的平均分 */
|
||||
@@ -176,20 +183,20 @@ public class BizProject extends BaseEntity {
|
||||
public void setSponsorAdminUserName(String sponsorAdminUserName) { this.sponsorAdminUserName = sponsorAdminUserName; }
|
||||
public String getSponsorOrgName() { return sponsorOrgName; }
|
||||
public void setSponsorOrgName(String sponsorOrgName) { this.sponsorOrgName = sponsorOrgName; }
|
||||
public List<Long> getSponsorAdminUserIds() { return sponsorAdminUserIds; }
|
||||
public void setSponsorAdminUserIds(List<Long> sponsorAdminUserIds) { this.sponsorAdminUserIds = sponsorAdminUserIds; }
|
||||
public List<Long> getSponsorOrgIds() { return sponsorOrgIds; }
|
||||
public void setSponsorOrgIds(List<Long> sponsorOrgIds) { this.sponsorOrgIds = sponsorOrgIds; }
|
||||
public String getExecOrgNames() { return execOrgNames; }
|
||||
public void setExecOrgNames(String execOrgNames) { this.execOrgNames = execOrgNames; }
|
||||
public List<Long> getExecAdminUserIds() { return execAdminUserIds; }
|
||||
public void setExecAdminUserIds(List<Long> execAdminUserIds) { this.execAdminUserIds = execAdminUserIds; }
|
||||
public List<Long> getExecOrgIds() { return execOrgIds; }
|
||||
public void setExecOrgIds(List<Long> execOrgIds) { this.execOrgIds = execOrgIds; }
|
||||
public String getProjectForm() { return projectForm; }
|
||||
public void setProjectForm(String projectForm) { this.projectForm = projectForm; }
|
||||
public String getIsFinished() { return isFinished; }
|
||||
public void setIsFinished(String isFinished) { this.isFinished = isFinished; }
|
||||
public String getIsSettled() { return isSettled; }
|
||||
public void setIsSettled(String isSettled) { this.isSettled = isSettled; }
|
||||
public Long getSponsorAdminUserId() { return sponsorAdminUserId; }
|
||||
public void setSponsorAdminUserId(Long sponsorAdminUserId) { this.sponsorAdminUserId = sponsorAdminUserId; }
|
||||
public Long getSponsorOrgId() { return sponsorOrgId; }
|
||||
public void setSponsorOrgId(Long sponsorOrgId) { this.sponsorOrgId = sponsorOrgId; }
|
||||
public Long getLeadUserId() { return leadUserId; }
|
||||
public void setLeadUserId(Long leadUserId) { this.leadUserId = leadUserId; }
|
||||
public String getLeadUserName() { return leadUserName; }
|
||||
@@ -228,12 +235,18 @@ public class BizProject extends BaseEntity {
|
||||
public void setSupportLetterUrl(String supportLetterUrl) { this.supportLetterUrl = supportLetterUrl; }
|
||||
public String getPublishUrl() { return publishUrl; }
|
||||
public void setPublishUrl(String publishUrl) { this.publishUrl = publishUrl; }
|
||||
public String getScheduleUrl() { return scheduleUrl; }
|
||||
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
|
||||
public String getIsPublished() { return isPublished; }
|
||||
public void setIsPublished(String isPublished) { this.isPublished = isPublished; }
|
||||
public Date getPublishTime() { return publishTime; }
|
||||
public void setPublishTime(Date publishTime) { this.publishTime = publishTime; }
|
||||
public String getAnnouncementType() { return announcementType; }
|
||||
public void setAnnouncementType(String announcementType) { this.announcementType = announcementType; }
|
||||
public Date getOpenDeadline() { return openDeadline; }
|
||||
public void setOpenDeadline(Date openDeadline) { this.openDeadline = openDeadline; }
|
||||
public String getOpenStatus() { return openStatus; }
|
||||
public void setOpenStatus(String openStatus) { this.openStatus = openStatus; }
|
||||
public BigDecimal getSponsorRating() { return sponsorRating; }
|
||||
public void setSponsorRating(BigDecimal sponsorRating) { this.sponsorRating = sponsorRating; }
|
||||
public Integer getSponsorQ1() { return sponsorQ1; }
|
||||
|
||||
@@ -14,10 +14,8 @@ public class BizProjectAssign extends BaseEntity {
|
||||
private String assignId;
|
||||
/** project_id */
|
||||
private Long projectId;
|
||||
/** 执行单位ID (FK: biz_org.org_id, NOT NULL) - service 层从 execUserId 反查 biz_org 写入 */
|
||||
/** 执行单位ID (FK: biz_org.org_id, NOT NULL) - 业务主键, 前端直接传, 单一可信源 */
|
||||
private Long executionUnitId;
|
||||
/** 执行方用户ID (sys_user.user_id, executor 主账号) - 业务主键,前端直接传 */
|
||||
private Long execUserId;
|
||||
/** 分配场次 */
|
||||
private Integer sessions;
|
||||
/** 分配金额 */
|
||||
@@ -35,8 +33,6 @@ public class BizProjectAssign extends BaseEntity {
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
public Long getExecutionUnitId() { return executionUnitId; }
|
||||
public void setExecutionUnitId(Long executionUnitId) { this.executionUnitId = executionUnitId; }
|
||||
public Long getExecUserId() { return execUserId; }
|
||||
public void setExecUserId(Long execUserId) { this.execUserId = execUserId; }
|
||||
public Integer getSessions() { return sessions; }
|
||||
public void setSessions(Integer sessions) { this.sessions = sessions; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@ public class BizProjectExecutorAssign extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private Long id;
|
||||
private String projectId;
|
||||
/** 执行方主账号 user_id (分配人, 审计) */
|
||||
private Long executorUserId;
|
||||
/** 分配方企业 org_id (biz_org.org_id, 分配人审计; 原 executor_user_id) */
|
||||
private Long executorOrgId;
|
||||
/** 执行人 user_id (被分配) */
|
||||
private Long staffUserId;
|
||||
/** 多个执行人 userId (前端 multi-select 传入, 控制器循环 insert, 不入库) */
|
||||
@@ -35,8 +35,8 @@ public class BizProjectExecutorAssign extends BaseEntity {
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getProjectId() { return projectId; }
|
||||
public void setProjectId(String projectId) { this.projectId = projectId; }
|
||||
public Long getExecutorUserId() { return executorUserId; }
|
||||
public void setExecutorUserId(Long executorUserId) { this.executorUserId = executorUserId; }
|
||||
public Long getExecutorOrgId() { return executorOrgId; }
|
||||
public void setExecutorOrgId(Long executorOrgId) { this.executorOrgId = executorOrgId; }
|
||||
public Long getStaffUserId() { return staffUserId; }
|
||||
public void setStaffUserId(Long staffUserId) { this.staffUserId = staffUserId; }
|
||||
public java.util.List<Long> getStaffUserIds() { return staffUserIds; }
|
||||
|
||||
@@ -30,6 +30,8 @@ public class BizProjectPlan extends BaseEntity {
|
||||
/** design_file_url */
|
||||
@Excel(name = "design_file_url")
|
||||
private String designFileUrl;
|
||||
/** 学科方向 */
|
||||
private String subjectDirection;
|
||||
/** status */
|
||||
@Excel(name = "status")
|
||||
private String status;
|
||||
@@ -91,6 +93,8 @@ public class BizProjectPlan extends BaseEntity {
|
||||
public void setProjectForm(String projectForm) { this.projectForm = projectForm; }
|
||||
public String getDesignFileUrl() { return designFileUrl; }
|
||||
public void setDesignFileUrl(String designFileUrl) { this.designFileUrl = designFileUrl; }
|
||||
public String getSubjectDirection() { return subjectDirection; }
|
||||
public void setSubjectDirection(String subjectDirection) { this.subjectDirection = subjectDirection; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getIsSettled() { return isSettled; }
|
||||
|
||||
+4
-3
@@ -9,7 +9,8 @@ public class BizProjectSponsorAssign extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private Long id;
|
||||
private String projectId;
|
||||
private Long sponsorUserId;
|
||||
/** 分配方企业 org_id (biz_org.org_id, 分配人审计; 原 sponsor_user_id) */
|
||||
private Long sponsorOrgId;
|
||||
private Long monitorUserId;
|
||||
/** 多个监察员 userId (前端 multi-select 传入, 控制器循环 insert, 不入库) */
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("monitorUserIds")
|
||||
@@ -33,8 +34,8 @@ public class BizProjectSponsorAssign extends BaseEntity {
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getProjectId() { return projectId; }
|
||||
public void setProjectId(String projectId) { this.projectId = projectId; }
|
||||
public Long getSponsorUserId() { return sponsorUserId; }
|
||||
public void setSponsorUserId(Long sponsorUserId) { this.sponsorUserId = sponsorUserId; }
|
||||
public Long getSponsorOrgId() { return sponsorOrgId; }
|
||||
public void setSponsorOrgId(Long sponsorOrgId) { this.sponsorOrgId = sponsorOrgId; }
|
||||
public Long getMonitorUserId() { return monitorUserId; }
|
||||
public void setMonitorUserId(Long monitorUserId) { this.monitorUserId = monitorUserId; }
|
||||
public java.util.List<Long> getMonitorUserIds() { return monitorUserIds; }
|
||||
|
||||
+27
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.business.domain.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
@@ -134,4 +135,30 @@ public class BizMeetingAttendeeImportVo {
|
||||
public void setBankAddress(String bankAddress) { this.bankAddress = bankAddress; }
|
||||
public String getIdCardAttachments() { return idCardAttachments; }
|
||||
public void setIdCardAttachments(String idCardAttachments) { this.idCardAttachments = idCardAttachments; }
|
||||
|
||||
/** 从参会人实体转导出 VO (与 /export 端点、劳务下载打包共用同一列映射) */
|
||||
public static BizMeetingAttendeeImportVo from(BizMeetingAttendee a) {
|
||||
BizMeetingAttendeeImportVo v = new BizMeetingAttendeeImportVo();
|
||||
if (a == null) return v;
|
||||
v.setName(a.getName());
|
||||
v.setPhone(a.getPhone());
|
||||
v.setWorkUnit(a.getWorkUnit());
|
||||
v.setDepartment(a.getDepartment());
|
||||
v.setTitle(a.getTitle());
|
||||
v.setIdCard(a.getIdCard());
|
||||
v.setBankName(a.getBankName());
|
||||
v.setBankCard(a.getBankCard());
|
||||
v.setBankBranch(a.getBankBranch());
|
||||
v.setAccountName(a.getAccountName());
|
||||
v.setBankRegion(a.getBankRegion());
|
||||
v.setBankAddress(a.getBankAddress());
|
||||
v.setIdCardAttachments(a.getIdCardAttachments());
|
||||
v.setLaborForm(a.getLaborForm());
|
||||
v.setFeePreTax(a.getFeePreTax());
|
||||
v.setTax(a.getTax());
|
||||
v.setVatAndSurcharge(a.getVatAndSurcharge());
|
||||
v.setFee(a.getFee());
|
||||
v.setSummary(a.getSummary());
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ruoyi.business.domain.vo;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 支持单位导入 VO
|
||||
*
|
||||
* <p>仅用于 Excel 批量导入 / 模板下载, 不参与业务逻辑.
|
||||
* 字段顺序与 Excel 列一致, 调整时同步修改模板下载体验.
|
||||
*
|
||||
* <p>只导入 org (biz_org), <b>不新增人员</b> (不建 sys_user / biz_person),
|
||||
* 因此不包含登录账号相关字段. 后端固定默认值:
|
||||
* <ul>
|
||||
* <li>orgType — 固定 'sponsor' (本入口是支持单位管理页)</li>
|
||||
* <li>status — 默认 '0' (正常)</li>
|
||||
* <li>userId — 不设置 (无主账号, 后续单独在人员管理里关联)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
public class BizOrgImportVo {
|
||||
|
||||
/** 公司名称 (必填) */
|
||||
@Excel(name = "公司名称", sort = 1)
|
||||
private String orgName;
|
||||
|
||||
/** 税号/统一社会信用代码 */
|
||||
@Excel(name = "税号/统一社会信用代码", sort = 2)
|
||||
private String taxNo;
|
||||
|
||||
/** 企业性质 私营/国营/中外合资/外资/其他 */
|
||||
@Excel(name = "企业性质", sort = 3)
|
||||
private String businessNature;
|
||||
|
||||
/** 公司地址 */
|
||||
@Excel(name = "公司地址", sort = 4)
|
||||
private String address;
|
||||
|
||||
/** 联系人 */
|
||||
@Excel(name = "联系人", sort = 5)
|
||||
private String contactName;
|
||||
|
||||
/** 联系电话 */
|
||||
@Excel(name = "联系电话", sort = 6)
|
||||
private String contactPhone;
|
||||
|
||||
public String getOrgName() { return orgName; }
|
||||
public void setOrgName(String orgName) { this.orgName = orgName; }
|
||||
public String getTaxNo() { return taxNo; }
|
||||
public void setTaxNo(String taxNo) { this.taxNo = taxNo; }
|
||||
public String getBusinessNature() { return businessNature; }
|
||||
public void setBusinessNature(String businessNature) { this.businessNature = businessNature; }
|
||||
public String getAddress() { return address; }
|
||||
public void setAddress(String address) { this.address = address; }
|
||||
public String getContactName() { return contactName; }
|
||||
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||
public String getContactPhone() { return contactPhone; }
|
||||
public void setContactPhone(String contactPhone) { this.contactPhone = contactPhone; }
|
||||
}
|
||||
-3
@@ -14,9 +14,6 @@ public interface BizMeetingExecutorMapper {
|
||||
/** 按会议ID查该会议的所有执行人员 */
|
||||
List<BizMeetingExecutor> selectByMeetingId(Long meetingId);
|
||||
|
||||
/** 按 userId 查该执行人员被分配到哪些会议 */
|
||||
List<BizMeetingExecutor> selectByUserId(Long userId);
|
||||
|
||||
/** 条件查询 */
|
||||
List<BizMeetingExecutor> selectList(BizMeetingExecutor entity);
|
||||
|
||||
|
||||
+13
@@ -14,6 +14,10 @@ public interface BizMeetingMaterialMapper {
|
||||
/** 按会议ID查该会议所有材料记录 */
|
||||
List<BizMeetingMaterial> selectByMeetingId(Long meetingId);
|
||||
|
||||
/** 按 (meetingId, subType) 查单条 (subType 会议内唯一), 用于批量插入后查回真实 id */
|
||||
BizMeetingMaterial selectByMeetingAndSubType(@org.apache.ibatis.annotations.Param("meetingId") Long meetingId,
|
||||
@org.apache.ibatis.annotations.Param("subType") String subType);
|
||||
|
||||
/** 插入 (id 走 DB AUTO_INCREMENT, 不接受前端传入的 id) */
|
||||
int insert(BizMeetingMaterial entity);
|
||||
|
||||
@@ -37,4 +41,13 @@ public interface BizMeetingMaterialMapper {
|
||||
|
||||
/** 单条更新 fee_status (OCR 完成/材料保存时设置 0未算 1已算) */
|
||||
int updateFeeStatus(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("feeStatus") Integer feeStatus);
|
||||
|
||||
/**
|
||||
* 会务材料"打包上传"回填: 替换单条材料的文件 URL/文件名, 并清空金额(待重新 OCR) + 置 fee_status.
|
||||
* 与 updateByPrimaryKey 不同, 这里显式把 amount 置 null (updateByPrimaryKey 的 if 会跳过 null).
|
||||
*/
|
||||
int updateFile(@org.apache.ibatis.annotations.Param("id") Long id,
|
||||
@org.apache.ibatis.annotations.Param("ossUrl") String ossUrl,
|
||||
@org.apache.ibatis.annotations.Param("fileName") String fileName,
|
||||
@org.apache.ibatis.annotations.Param("feeStatus") Integer feeStatus);
|
||||
}
|
||||
-3
@@ -14,9 +14,6 @@ public interface BizMeetingSupervisorMapper {
|
||||
/** 按会议ID查该会议的所有监察员 */
|
||||
List<BizMeetingSupervisor> selectByMeetingId(Long meetingId);
|
||||
|
||||
/** 按 userId 查该监察员被分配到哪些会议 */
|
||||
List<BizMeetingSupervisor> selectByUserId(Long userId);
|
||||
|
||||
/** 条件查询 */
|
||||
List<BizMeetingSupervisor> selectList(BizMeetingSupervisor entity);
|
||||
|
||||
|
||||
@@ -10,18 +10,25 @@ public interface BizOrgMapper {
|
||||
int insert(BizOrg entity);
|
||||
int updateByPrimaryKey(BizOrg entity);
|
||||
int deleteByPrimaryKeys(Long[] orgIds);
|
||||
/** 支持方下拉选项 (JOIN sys_user.user_name), 用于 manager 项目分配弹窗 */
|
||||
/** 支持方下拉选项 (JOIN sys_user.user_name), 用于 manager 项目分配弹窗, 返回 orgId/orgName/userName */
|
||||
List<Map<String, Object>> selectSponsorOrgOptions(BizOrg entity);
|
||||
/** 执行方下拉选项 (JOIN sys_user MAIN 账号 user_name), 用于 manager 项目分配弹窗
|
||||
* 返回 Map: userId / orgName / userName
|
||||
* 返回 Map: orgId / orgName / userName
|
||||
* 过滤条件: orgType='executor' + sys_user MAIN 账号 (parent_user_id IS NULL)
|
||||
* 注意: value 必须 MAIN user_id (写入 biz_project_assign.exec_user_id);
|
||||
* 注意: value 必须 org_id (写入 biz_project_assign.execution_unit_id);
|
||||
* label 必须 org_name, 不用 user_name 以避免把同公司的普通员工带出来 */
|
||||
List<Map<String, Object>> selectExecutorOrgOptions(BizOrg entity);
|
||||
/** 支持方注册下拉选项 (匿名公开, register-sponsor 选企业注册 SUB 子账号)
|
||||
* 与 selectSponsorOrgOptions 区别: 不 JOIN sys_user (无主账号的 org 也可选), 返回 orgId + mainUserId
|
||||
* 返回 Map: orgId / orgName / mainUserId (= biz_org.user_id, 可为 null 表示该企业暂无主账号) */
|
||||
List<Map<String, Object>> selectSponsorRegisterOptions(BizOrg entity);
|
||||
/** 当前登录 sponsor 的所属公司
|
||||
* 主账号 (sys_user.parent_user_id IS NULL): biz_org.user_id = #{userId}
|
||||
* 子账号 (有 biz_person 记录): biz_org.org_id = biz_person.org_id
|
||||
* 返回 Map: orgId / orgName / isOwner (1=主账号, 0=子账号)
|
||||
* 用于 /sponsor/account 页面回显 + 主账号修改 org_name */
|
||||
Map<String, Object> selectMySponsorCompany(Long userId);
|
||||
/** user_id → org_id 单一可信源反查: COALESCE(biz_org.user_id, biz_person.user_id)
|
||||
* MAIN 主账号走 biz_org.user_id; SUB 子账号走 biz_person.org_id; 都没有返回 null */
|
||||
Long selectOrgIdByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.business.domain.BizPerson;
|
||||
|
||||
/**
|
||||
@@ -20,6 +19,4 @@ public interface BizPersonMapper
|
||||
int deleteByPrimaryKeys(String[] personIds);
|
||||
/** 联动: person 逻辑删除后, 把对应 sys_user 子账号也逻辑删除 */
|
||||
int softDeleteSysUserByPersonIds(String[] personIds);
|
||||
/** 校验某 org 下是否已有 admin 角色 (注册时兜底, 保证一公司一管理员) */
|
||||
int countAdminByOrgId(@Param("orgId") Long orgId);
|
||||
}
|
||||
@@ -34,4 +34,8 @@ public interface BizProjectMapper
|
||||
* 并重算 available_amount = total_amount - manage_fee - 已支付劳务 - 已支付会务 (幂等, 无累计副作用).
|
||||
*/
|
||||
int recomputeSettledAmounts(@org.apache.ibatis.annotations.Param("projectId") Long projectId);
|
||||
/** 删除公告: 将 invitation_url / support_letter_url / publish_url 置 NULL */
|
||||
int clearAnnouncement(@org.apache.ibatis.annotations.Param("projectId") Long projectId);
|
||||
/** 开通到期回收: 到期(open_deadline <= 今天)的 open_status='Y' 置回 'N' */
|
||||
int closeExpiredOpenStatus();
|
||||
}
|
||||
+1
-1
@@ -45,7 +45,7 @@ public class OcrProperties {
|
||||
/** 整流程 OCR 超时 (秒) */
|
||||
private int totalTimeoutS = 60;
|
||||
/** QR 命中后是否继续跑全量 OCR + 字段抽取 (false = 快路径, 只返回 QR 3 字段) */
|
||||
private boolean qrFullOcr = true;
|
||||
private boolean qrFullOcr = false;
|
||||
/** OCR 语言: ch (简中) / en / chinese_cht */
|
||||
private String lang = "ch";
|
||||
/** 模型目录: 相对路径 → classpath:models/, 绝对路径 → 直读 */
|
||||
|
||||
+2
@@ -284,6 +284,8 @@ public class RecognizeService {
|
||||
|
||||
private InvoiceResult buildQrOnlyResult(QrDecodeResult qr, int elapsedMs, int pageCount) {
|
||||
InvoiceFields fields = new InvoiceFields();
|
||||
// QR 命中即证明是电子发票; isRecognizedAsInvoice 门控要求 invoiceType 非空, 这里补上类型
|
||||
fields.setInvoiceType("电子发票");
|
||||
fields.setInvoiceNo(qr.getInvoiceNo());
|
||||
fields.setAmount(qr.getAmount());
|
||||
fields.setInvoiceDate(qr.getInvoiceDate());
|
||||
|
||||
@@ -22,6 +22,7 @@ public class OssConfMeta
|
||||
private final String bucket;
|
||||
private final String accessKeyId;
|
||||
private final String accessKeySecret;
|
||||
private final String zipFuncUrl;
|
||||
|
||||
public OssConfMeta(RuoYiConfig cfg)
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class OssConfMeta
|
||||
this.bucket = p.getBucket();
|
||||
this.accessKeyId = p.getAccessKeyId();
|
||||
this.accessKeySecret = p.getAccessKeySecret();
|
||||
this.zipFuncUrl = p.getZipFuncUrl();
|
||||
// endpoint: 剥协议头 + 剥 bucket 前缀 → 纯 OSS endpoint (OSSClient 构造/URL 拼接都要求裸 endpoint, 不带 bucket)
|
||||
this.endpoint = stripBucketPrefix(stripScheme(p.getEndpoint()), this.bucket);
|
||||
}
|
||||
@@ -65,4 +67,5 @@ public class OssConfMeta
|
||||
public String getBucket() { return bucket; }
|
||||
public String getAccessKeyId() { return accessKeyId; }
|
||||
public String getAccessKeySecret() { return accessKeySecret; }
|
||||
public String getZipFuncUrl() { return zipFuncUrl; }
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.ruoyi.business.oss;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.aliyun.oss.OSSClient;
|
||||
import com.aliyun.oss.model.DeleteObjectsRequest;
|
||||
import com.aliyun.oss.model.DeleteObjectsResult;
|
||||
import com.aliyun.oss.model.ObjectListing;
|
||||
import com.aliyun.oss.model.ObjectMetadata;
|
||||
import com.aliyun.oss.model.OSSObjectSummary;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 阿里云 OSS 打 zip (复用 hwt-serve 的阿里云函数计算 FC 端点)
|
||||
* <p>
|
||||
* 思路 (与 hwt-serve OssApi.ossDownload 一致): 不在 Java 里用 ZipOutputStream 打 zip,
|
||||
* 而是把"在 OSS 端打包"外包给阿里云函数计算 (FC). Java 只做:
|
||||
* <ol>
|
||||
* <li>把要打包的文件先 copy 到一个 staging 前缀 (目录结构 = 最终 zip 内目录结构)</li>
|
||||
* <li>POST {bucket, source-dir} 到 FC 端点</li>
|
||||
* <li>读 301/302 的 Location 头, 拿到 zip 签名下载 URL</li>
|
||||
* </ol>
|
||||
* 与 hwt-serve 的差异: 不再用 unirest (本项目无此依赖), 改用 hutool-http.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class OssZipService
|
||||
{
|
||||
@Autowired
|
||||
private OssConfMeta ossConfMeta;
|
||||
|
||||
private OSSClient newClient()
|
||||
{
|
||||
return new OSSClient(
|
||||
ossConfMeta.getEndpoint(),
|
||||
ossConfMeta.getAccessKeyId(),
|
||||
ossConfMeta.getAccessKeySecret());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从完整 ossUrl 提取 object key (去掉 https://{bucket}.{endpoint}/ 前缀, 顺带去掉 query)
|
||||
* 例: https://hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com/ry8080/meeting/1/service/xxx.jpg
|
||||
* → ry8080/meeting/1/service/xxx.jpg
|
||||
*/
|
||||
public String extractKey(String ossUrl)
|
||||
{
|
||||
if (ossUrl == null) return null;
|
||||
String u = ossUrl;
|
||||
int q = u.indexOf('?');
|
||||
if (q >= 0) u = u.substring(0, q);
|
||||
|
||||
String httpsHost = "https://" + ossConfMeta.getBucket() + "." + ossConfMeta.getEndpoint() + "/";
|
||||
if (u.startsWith(httpsHost)) return u.substring(httpsHost.length());
|
||||
String httpHost = "http://" + ossConfMeta.getBucket() + "." + ossConfMeta.getEndpoint() + "/";
|
||||
if (u.startsWith(httpHost)) return u.substring(httpHost.length());
|
||||
|
||||
// 兜底: 取 "://" 之后第一个 "/" 之后的部分 (CNAME / 自定义域名)
|
||||
int i = u.indexOf("://");
|
||||
if (i >= 0)
|
||||
{
|
||||
int slash = u.indexOf('/', i + 3);
|
||||
if (slash >= 0) return u.substring(slash + 1);
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
/** OSS 服务端 copy (同 bucket 内), 用于把散落的材料文件复制到 staging 前缀, 不经过 Java 内存 */
|
||||
public void copyObject(String srcKey, String dstKey)
|
||||
{
|
||||
OSSClient client = newClient();
|
||||
try
|
||||
{
|
||||
client.copyObject(ossConfMeta.getBucket(), srcKey, ossConfMeta.getBucket(), dstKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传字节到指定 OSS key (把后端生成的参会人信息 Excel 直接放进 staging 前缀, 不经过本地文件) */
|
||||
public void putObject(String key, byte[] data, String contentType)
|
||||
{
|
||||
OSSClient client = newClient();
|
||||
try
|
||||
{
|
||||
ObjectMetadata meta = new ObjectMetadata();
|
||||
meta.setContentType(contentType);
|
||||
client.putObject(ossConfMeta.getBucket(), key, new ByteArrayInputStream(data), meta);
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除某前缀下所有对象 (staging 清场, 避免上次下载的残留文件混进本次 zip) */
|
||||
public void clearPrefix(String prefix)
|
||||
{
|
||||
OSSClient client = newClient();
|
||||
try
|
||||
{
|
||||
ObjectListing listing = client.listObjects(ossConfMeta.getBucket(), prefix);
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (OSSObjectSummary s : listing.getObjectSummaries())
|
||||
{
|
||||
keys.add(s.getKey());
|
||||
}
|
||||
if (!keys.isEmpty())
|
||||
{
|
||||
DeleteObjectsResult r = client.deleteObjects(
|
||||
new DeleteObjectsRequest(ossConfMeta.getBucket()).withKeys(keys));
|
||||
log.info("[OSS-ZIP] 清场 {} 个对象, 前缀 {}", r.getDeletedObjects().size(), prefix);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调阿里云 FC 在 OSS 端打 zip, 返回 zip 的签名下载 URL.
|
||||
* <p>
|
||||
* FC 打完后返回 301/302, Location 头即 zip 签名 URL; unirest/hutool 默认不跟随重定向,
|
||||
* 所以能直接读到 Location. http→https 强转防浏览器 Mixed Content (OSS 双协议都支持).
|
||||
*/
|
||||
public String zipDownload(String sourceDir)
|
||||
{
|
||||
String funcUrl = ossConfMeta.getZipFuncUrl();
|
||||
if (funcUrl == null || funcUrl.isEmpty())
|
||||
{
|
||||
throw new ServiceException("OSS 打 zip 端点未配置 (ruoyi.oss.zip-func-url)");
|
||||
}
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("bucket", ossConfMeta.getBucket());
|
||||
body.put("source-dir", sourceDir);
|
||||
log.info("[OSS-ZIP] 请求 FC 打包 bucket={} source-dir={}", ossConfMeta.getBucket(), sourceDir);
|
||||
|
||||
HttpResponse resp;
|
||||
try
|
||||
{
|
||||
resp = HttpRequest.post(funcUrl)
|
||||
.body(JSONUtil.toJsonStr(body), "application/json")
|
||||
.setFollowRedirects(false)
|
||||
.timeout(60000)
|
||||
.execute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ServiceException("OSS 打 zip 请求失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
int status = resp.getStatus();
|
||||
String location = resp.header("Location");
|
||||
if (status != 301 && status != 302)
|
||||
{
|
||||
log.error("[OSS-ZIP] FC 响应异常 status={}", status);
|
||||
throw new ServiceException("打包失败, FC 响应状态 " + status);
|
||||
}
|
||||
if (location == null || location.isEmpty())
|
||||
{
|
||||
throw new ServiceException("打包失败, 未获取到下载链接");
|
||||
}
|
||||
return location.replaceFirst("^http://", "https://");
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -10,12 +10,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
* 会议事实/阶段 自动流转调度器 (每分钟一次).
|
||||
* <p>
|
||||
* 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实
|
||||
* (is_executed / is_frozen / material_audit_stage / voucher_audit_stage / 审核时间 …),
|
||||
* (is_executed / is_frozen / material_audit_stage / 审核时间 …),
|
||||
* 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的三类事实落地:
|
||||
* <pre>
|
||||
* 1) start_time 到 → is_executed=1 (执行中)
|
||||
* 2) end_time + submit_deadline_days 到 且 material 未提交 → is_frozen=1 (冻结)
|
||||
* 3) material+voucher 都通过 且 最晚审核时间过 24h → 待结算 (current_stage 缓存翻 AWAITING_SETTLEMENT)
|
||||
* 3) material 通过 且 审核时间过 24h → 待结算 (current_stage 缓存翻 AWAITING_SETTLEMENT)
|
||||
* </pre>
|
||||
* 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围.
|
||||
* <p>
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.ruoyi.business.scheduler;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.business.service.IBizProjectService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 项目"开通"到期回收调度器 (每天 00:05 一次).
|
||||
* <p>
|
||||
* 开通状态流转: 默认 N → 经理"开通"置 Y + open_deadline → 到期(open_deadline <= 今天)回收为 N.
|
||||
* 结题(is_finished=1)且 open_status=N 的项目, sponsor 不可见
|
||||
* (见 BizProjectMapper.selectSponsorList / BizMeetingMapper.selectList 的过滤).
|
||||
* <p>
|
||||
* 需要启动类 {@code @EnableScheduling} 才会生效 (RuoYiApplication 已有).
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class OpenStatusScheduler
|
||||
{
|
||||
@Autowired
|
||||
private IBizProjectService bizProjectService;
|
||||
|
||||
/** 每天 00:05:00 执行一次 */
|
||||
@Scheduled(cron = "0 5 0 * * ?")
|
||||
public void closeExpiredOpenStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
int n = bizProjectService.closeExpiredOpenStatus();
|
||||
if (n > 0)
|
||||
{
|
||||
log.info("[OpenStatusScheduler] 开通到期回收 {} 个项目", n);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("[OpenStatusScheduler] 回收异常 (跳过, 明天再试)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -84,4 +84,55 @@ public interface IBizMeetingAttendeeService {
|
||||
* @return 成功邀请 (站内信+标记) 的人数
|
||||
*/
|
||||
int invite(List<Long> attendeeIds);
|
||||
|
||||
/**
|
||||
* 生成"参会人信息" Excel 字节 (劳务下载打包用).
|
||||
* 复用 BizMeetingAttendeeImportVo 的 @Excel 列头, 与 /export 端点同列.
|
||||
*
|
||||
* @param list 已查出的参会人 (is_deleted=0)
|
||||
* @return xlsx 字节
|
||||
*/
|
||||
byte[] buildAttendeeInfoExcel(List<BizMeetingAttendee> list);
|
||||
|
||||
/**
|
||||
* 生成"劳务协议"空目录模板 zip (参会人管理 "下载协议模板" 按钮).
|
||||
* <p>
|
||||
* zip 内为 劳务协议/{序号}_{姓名}/ 空目录, 每个参会人一个; 序号 = 列表 1 起下标
|
||||
* (与前端表格 type="index" 一致, 依赖 selectByMeetingId 的 order by id 稳定排序).
|
||||
*
|
||||
* @param meetingId 会议 id
|
||||
* @return zip 字节 (UTF-8 目录名)
|
||||
*/
|
||||
byte[] buildAgreementTemplateZip(Long meetingId);
|
||||
|
||||
/**
|
||||
* 上传"劳务协议" zip, 解压后按 劳务协议/{序号}_{姓名}/ 目录匹配参会人,
|
||||
* 取目录内第一个文件上传 OSS 并回填 labor_protocol (覆盖式).
|
||||
*
|
||||
* @param file 用户重新压缩的 zip
|
||||
* @param meetingId 会议 id (决定 OSS 子目录 + 参会人列表)
|
||||
* @return 成功回填的参会人数
|
||||
* @throws Exception 文件为空 / 无参会人 / zip 解析异常
|
||||
*/
|
||||
int uploadAgreements(MultipartFile file, Long meetingId) throws Exception;
|
||||
|
||||
/**
|
||||
* 生成"专家照片"空目录模板 zip (专家照片 "下载目录模板" 按钮).
|
||||
* zip 内为 专家照片/{序号}_{姓名}/ 空目录, 结构与劳务协议模板一致.
|
||||
*
|
||||
* @param meetingId 会议 id
|
||||
* @return zip 字节 (UTF-8 目录名)
|
||||
*/
|
||||
byte[] buildExpertPhotoTemplateZip(Long meetingId);
|
||||
|
||||
/**
|
||||
* 上传"专家照片" zip, 解压后按 专家照片/{序号}_{姓名}/ 目录匹配参会人,
|
||||
* 目录内所有文件上传 OSS 后逗号拼接, 覆盖式回填 on_site_photos.
|
||||
*
|
||||
* @param file 用户重新压缩的 zip
|
||||
* @param meetingId 会议 id
|
||||
* @return 成功回填的参会人数
|
||||
* @throws Exception 文件为空 / 无参会人 / zip 解析异常
|
||||
*/
|
||||
int uploadExpertPhotos(MultipartFile file, Long meetingId) throws Exception;
|
||||
}
|
||||
+2
-3
@@ -12,11 +12,10 @@ public interface IBizMeetingExecutorService {
|
||||
|
||||
List<BizMeetingExecutor> selectByMeetingId(Long meetingId);
|
||||
|
||||
List<BizMeetingExecutor> selectByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 替换该会议的执行人员 (全删全插, 一个事务)
|
||||
* @param orgIds 执行方企业 org_id 列表
|
||||
* @param assignedBy 分配人 user_id
|
||||
*/
|
||||
int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy);
|
||||
int replaceByMeetingId(Long meetingId, List<Long> orgIds, Long assignedBy);
|
||||
}
|
||||
+56
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
|
||||
/**
|
||||
@@ -46,4 +47,59 @@ public interface IBizMeetingMaterialService {
|
||||
* 白名单 subType + 会议存在校验 (公开端点防滥用).
|
||||
*/
|
||||
void upsertFromCamera(Long meetingId, String subType, String ossUrl, String extraOssUrl);
|
||||
|
||||
/**
|
||||
* 结算时保存付款凭证 (LV_PAYMENT + SV_PAYMENT, 合规人员上传).
|
||||
* 只清/插这两个 subType, 不碰其他材料; 付款凭证非发票, 不触发 OCR, 不影响会议费用.
|
||||
* 与 replaceByMeetingId 不同, 这里不做金额回填 (凭证无金额).
|
||||
*/
|
||||
void saveVouchers(Long meetingId, List<BizMeetingMaterial> vouchers);
|
||||
|
||||
/**
|
||||
* 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打成一个 zip, 返回下载 URL.
|
||||
* <p>
|
||||
* zip 内目录结构: {项目名称}/{材料类型中文名}/{材料文件}.
|
||||
* 实现: 先 staging copy 到 download/huiwu/{meetingId}/ 前缀 (目录结构即 zip 结构),
|
||||
* 再调阿里云 FC 在 OSS 端打 zip.
|
||||
*/
|
||||
String buildServiceZipUrl(Long meetingId);
|
||||
|
||||
/**
|
||||
* 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息 Excel 在 OSS 端打成一个 zip, 返回下载 URL.
|
||||
* 前全景/后全景两张图归入"会议现场全景"目录.
|
||||
*/
|
||||
String buildLaborZipUrl(Long meetingId);
|
||||
|
||||
/**
|
||||
* 批量会务下载: 多个会议的会务材料合并打一个 zip, 返回下载 URL.
|
||||
* meetingIds 去重去空, 上限 20 个; 单个会议无材料跳过, 全部无材料抛异常.
|
||||
*/
|
||||
String buildBatchServiceZipUrl(List<Long> meetingIds);
|
||||
|
||||
/**
|
||||
* 批量劳务下载: 多个会议的劳务材料(含参会人信息)合并打一个 zip, 返回下载 URL.
|
||||
*/
|
||||
String buildBatchLaborZipUrl(List<Long> meetingIds);
|
||||
|
||||
/**
|
||||
* 生成"会务材料"空目录模板 zip (会务材料 tab "打包上传" 的"下载目录"按钮).
|
||||
* <p>
|
||||
* zip 内为 会务材料/{材料类型中文名}/ 空目录 (物料制作/酒店/大交通/小交通/执行费/设计费/其他/总结算单/总发票),
|
||||
* 用户把材料文件放进对应目录后重新压缩上传.
|
||||
*
|
||||
* @param meetingId 会议 id
|
||||
* @return zip 字节 (UTF-8 目录名)
|
||||
*/
|
||||
byte[] buildServiceTemplateZip(Long meetingId);
|
||||
|
||||
/**
|
||||
* 上传"会务材料" zip, 解压后按 会务材料/{材料类型中文名}/ 目录匹配 subType,
|
||||
* 单文件直接上传 OSS, 多文件先打 zip 再上传 OSS, 按 (meetingId, SERVICE, subType) upsert 回填.
|
||||
*
|
||||
* @param file 用户重新压缩的 zip
|
||||
* @param meetingId 会议 id (决定 OSS 子目录)
|
||||
* @return 成功回填的材料类数
|
||||
* @throws Exception 文件为空 / zip 解析异常 / 未找到有效目录
|
||||
*/
|
||||
int uploadServiceMaterials(MultipartFile file, Long meetingId) throws Exception;
|
||||
}
|
||||
+2
-3
@@ -12,11 +12,10 @@ public interface IBizMeetingSupervisorService {
|
||||
|
||||
List<BizMeetingSupervisor> selectByMeetingId(Long meetingId);
|
||||
|
||||
List<BizMeetingSupervisor> selectByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 替换该会议的监察员 (全删全插, 一个事务)
|
||||
* @param orgIds 支持方企业 org_id 列表
|
||||
* @param assignedBy 分配人 user_id (前端 / manager 自己)
|
||||
*/
|
||||
int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy);
|
||||
int replaceByMeetingId(Long meetingId, List<Long> orgIds, Long assignedBy);
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
|
||||
public interface IBizOrgService {
|
||||
BizOrg getById(Long orgId);
|
||||
@@ -10,14 +12,19 @@ public interface IBizOrgService {
|
||||
int insert(BizOrg entity);
|
||||
int updateByPrimaryKey(BizOrg entity);
|
||||
int deleteByPrimaryKeys(Long[] orgIds);
|
||||
/** 支持方下拉选项 (userId/orgName/userName), 用于 manager 项目分配弹窗 */
|
||||
/** 支持方下拉选项 (orgId/orgName/userName), 用于 manager 项目分配弹窗 */
|
||||
List<Map<String, Object>> selectSponsorOrgOptions(BizOrg entity);
|
||||
/** 执行方下拉选项 (userId/orgName/userName), 用于 manager 项目分配弹窗
|
||||
/** 执行方下拉选项 (orgId/orgName/userName), 用于 manager 项目分配弹窗
|
||||
* 只返回 biz_org.org_type='executor' 对应的 MAIN 账号 (parent_user_id IS NULL) */
|
||||
List<Map<String, Object>> selectExecutorOrgOptions(BizOrg entity);
|
||||
/** 支持方注册下拉选项 (匿名公开, 含无主账号 org): 返回 orgId / orgName / mainUserId */
|
||||
List<Map<String, Object>> selectSponsorRegisterOptions(BizOrg entity);
|
||||
/** 当前登录 sponsor 的所属公司 (主账号可改, 子账号只读) */
|
||||
Map<String, Object> selectMySponsorCompany(Long userId);
|
||||
|
||||
/** user_id → org_id 单一可信源反查 (MAIN 走 biz_org, SUB 走 biz_person), 找不到返回 null */
|
||||
Long selectOrgIdByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 启用/禁用 org, 同步主账号 sys_user.status
|
||||
* biz_org.status: '禁用' → sys_user.status='1', '正常'/'合作中' → '0'
|
||||
@@ -25,4 +32,11 @@ public interface IBizOrgService {
|
||||
* (子账号登录时由 BizAuthController 校验主账号状态拦截)
|
||||
*/
|
||||
int toggleStatus(Long orgId, String newBizOrgStatus);
|
||||
|
||||
/**
|
||||
* 批量导入支持单位 (只导 org, 不新增人员)
|
||||
* orgType 固定 'sponsor', status 固定 '0', 不建 sys_user (userId 留空)
|
||||
* 返回 {@link ImportResult} 含成功/失败计数 + 失败明细 (行号 + 原因)
|
||||
*/
|
||||
ImportResult importOrg(MultipartFile file, String operName) throws Exception;
|
||||
}
|
||||
|
||||
@@ -23,4 +23,9 @@ public interface IBizPersonService
|
||||
int updateByPrimaryKey(BizPerson entity);
|
||||
int deleteByPrimaryKey(String personId);
|
||||
int deleteByPrimaryKeys(String[] personId);
|
||||
/**
|
||||
* 更换机构管理员: 把 personId 对应人员晋升为该机构 MAIN (管理员),
|
||||
* 原管理员由 MAIN 降为 SUB, 其余子账号 re-point 到新管理员, biz_org.user_id 同步指向新管理员
|
||||
*/
|
||||
int changeOrgAdmin(String personId);
|
||||
}
|
||||
|
||||
@@ -45,4 +45,10 @@ public interface IBizProjectService
|
||||
* 并重算 available_amount. 幂等 (每次全量重算), 无累计副作用.
|
||||
*/
|
||||
void recomputeSettledAmounts(Long projectId);
|
||||
|
||||
/** 删除公告: 将 invitation_url / support_letter_url / publish_url 置 NULL (未发布) */
|
||||
int clearAnnouncement(Long projectId);
|
||||
|
||||
/** 开通到期回收: 到期(open_deadline <= 今天)的 open_status='Y' 置回 'N' */
|
||||
int closeExpiredOpenStatus();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.Date;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
|
||||
@@ -8,7 +7,7 @@ import com.ruoyi.business.domain.BizMeeting;
|
||||
* 会议阶段推导器 (单一可信源).
|
||||
* <p>
|
||||
* 事实与展示分离: {@code biz_meeting} 只存事实 (is_executed/is_settled/is_finished/is_frozen
|
||||
* + material_audit_stage/voucher_audit_stage + 审核时间 + compliance_approved), 各角色看到的
|
||||
* + material_audit_stage + 审核时间 + compliance_approved), 各角色看到的
|
||||
* 「阶段名称」由本类实时计算.
|
||||
* <ul>
|
||||
* <li>{@link #derivePhysicalStage(BizMeeting)}: 10 值物理阶段 (current_stage 缓存 + 列表筛选).</li>
|
||||
@@ -22,20 +21,12 @@ public class StageDeriver
|
||||
{
|
||||
private static final long H24 = 24L * 3600 * 1000;
|
||||
|
||||
/** 待结算: 材料+凭证都审核通过 且 最晚通过时间已超 24h (用户拍板口径) */
|
||||
/** 待结算: 材料审核通过 且 材料审核时间已超 24h (用户拍板口径) */
|
||||
private boolean settlementReady(BizMeeting m)
|
||||
{
|
||||
if (!"APPROVED".equals(m.getVoucherAuditStage())) return false;
|
||||
Date later = later(m.getMaterialAuditTime(), m.getVoucherAuditTime());
|
||||
if (later == null) return false;
|
||||
return System.currentTimeMillis() - later.getTime() >= H24;
|
||||
}
|
||||
|
||||
private Date later(Date a, Date b)
|
||||
{
|
||||
if (a == null) return b;
|
||||
if (b == null) return a;
|
||||
return a.after(b) ? a : b;
|
||||
if (!"APPROVED".equals(m.getMaterialAuditStage())) return false;
|
||||
if (m.getMaterialAuditTime() == null) return false;
|
||||
return System.currentTimeMillis() - m.getMaterialAuditTime().getTime() >= H24;
|
||||
}
|
||||
|
||||
private static boolean t(Integer v)
|
||||
|
||||
+226
@@ -1,13 +1,23 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -24,6 +34,7 @@ import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||
import com.ruoyi.business.mapper.BizProjectMapper;
|
||||
import com.ruoyi.business.notify.BizNotifyService;
|
||||
import com.ruoyi.business.oss.OssUploader;
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
import com.ruoyi.business.sms.AliyunSmsSender;
|
||||
@@ -60,6 +71,8 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
private BizNotifyService bizNotifyService;
|
||||
@Autowired
|
||||
private IBizMeetingService bizMeetingService;
|
||||
@Autowired
|
||||
private OssUploader ossUploader;
|
||||
|
||||
@Override
|
||||
public int insert(BizMeetingAttendee entity) {
|
||||
@@ -507,4 +520,217 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
return sent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] buildAttendeeInfoExcel(List<BizMeetingAttendee> list) {
|
||||
List<BizMeetingAttendeeImportVo> voList = new ArrayList<>();
|
||||
if (list != null) {
|
||||
for (BizMeetingAttendee a : list) {
|
||||
voList.add(BizMeetingAttendeeImportVo.from(a));
|
||||
}
|
||||
}
|
||||
SXSSFWorkbook wb = new SXSSFWorkbook(500);
|
||||
try {
|
||||
ExcelUtil<BizMeetingAttendeeImportVo> util = new ExcelUtil<>(BizMeetingAttendeeImportVo.class);
|
||||
util.initWithWorkbook(wb, voList, "参会人", null);
|
||||
util.writeSheet();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
wb.write(out);
|
||||
return out.toByteArray();
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("生成参会人信息失败: " + e.getMessage());
|
||||
} finally {
|
||||
wb.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/** 劳务协议 zip 顶层目录名 */
|
||||
private static final String AGREEMENT_ZIP_ROOT = "劳务协议";
|
||||
/** 专家照片 zip 顶层目录名 */
|
||||
private static final String EXPERT_PHOTO_ZIP_ROOT = "专家照片";
|
||||
|
||||
/** 目录名里的姓名段 (下划线后) 要剔除路径/zip 非法字符, 避免 Windows 解压异常 */
|
||||
private static String safeAttendeeName(String name) {
|
||||
if (name == null || name.trim().isEmpty()) return "未知";
|
||||
return name.replaceAll("[\\\\/:*?\"<>|\\r\\n\\t]", "").trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] buildAgreementTemplateZip(Long meetingId) {
|
||||
return buildTemplateZip(meetingId, AGREEMENT_ZIP_ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] buildExpertPhotoTemplateZip(Long meetingId) {
|
||||
return buildTemplateZip(meetingId, EXPERT_PHOTO_ZIP_ROOT);
|
||||
}
|
||||
|
||||
/** 生成空目录模板 zip: {rootDir}/{序号}_{姓名}/, 每个参会人一个空目录 */
|
||||
private byte[] buildTemplateZip(Long meetingId, String rootDir) {
|
||||
List<BizMeetingAttendee> list = selectByMeetingId(meetingId);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) {
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
BizMeetingAttendee a = list.get(i);
|
||||
// 空目录 entry (以 / 结尾), 序号 = 列表 1 起下标, 与前端表格 type="index" 一致
|
||||
String dir = rootDir + "/" + (i + 1) + "_" + safeAttendeeName(a.getName()) + "/";
|
||||
zos.putNextEntry(new ZipEntry(dir));
|
||||
zos.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("生成目录模板失败: " + e.getMessage());
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int uploadAgreements(MultipartFile file, Long meetingId) throws Exception {
|
||||
if (meetingId == null) throw new ServiceException("meetingId 不能为空");
|
||||
if (file == null || file.isEmpty()) throw new ServiceException("请选择要上传的 zip 文件");
|
||||
|
||||
List<BizMeetingAttendee> list = selectByMeetingId(meetingId);
|
||||
if (list.isEmpty()) throw new ServiceException("该会议暂无参会人, 无法回填劳务协议");
|
||||
|
||||
byte[] zipBytes = file.getBytes();
|
||||
|
||||
// 目录序号 (下划线前数字, 0-based) → 该目录下文件字节 + 文件名
|
||||
Map<Integer, List<byte[]>> folderBytes = new HashMap<>();
|
||||
Map<Integer, List<String>> folderNames = new HashMap<>();
|
||||
try {
|
||||
readAttendeeZip(zipBytes, StandardCharsets.UTF_8, folderBytes, folderNames);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 中文 Windows 打包器 (WinRAR/资源管理器"发送到压缩文件夹") 用 GBK 编码条目名且不置 UTF-8 标志,
|
||||
// 强制 UTF-8 解码会抛 malformed input → 回退 GBK 重新解析
|
||||
log.warn("[attendee] 协议 zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage());
|
||||
folderBytes.clear();
|
||||
folderNames.clear();
|
||||
readAttendeeZip(zipBytes, Charset.forName("GBK"), folderBytes, folderNames);
|
||||
}
|
||||
|
||||
int updated = 0;
|
||||
for (Map.Entry<Integer, List<byte[]>> e : folderBytes.entrySet()) {
|
||||
int idx = e.getKey();
|
||||
if (idx < 0 || idx >= list.size()) {
|
||||
log.warn("[attendee] 协议 zip 目录序号 {} 越界 (参会人共 {}), 跳过", idx + 1, list.size());
|
||||
continue;
|
||||
}
|
||||
BizMeetingAttendee a = list.get(idx);
|
||||
List<String> names = folderNames.get(idx);
|
||||
String fileName = (names != null && !names.isEmpty()) ? names.get(0) : "labor-protocol.pdf";
|
||||
// labor_protocol 是单 URL, 每目录只取第一个文件作为协议
|
||||
String url = ossUploader.upload(e.getValue().get(0), fileName, "ry8080/meeting/" + meetingId + "/labor-protocol");
|
||||
|
||||
BizMeetingAttendee entity = new BizMeetingAttendee();
|
||||
entity.setId(a.getId());
|
||||
entity.setLaborProtocol(url);
|
||||
entity.setUpdateBy(SecurityUtils.getUsername());
|
||||
updateLaborProtocol(entity);
|
||||
updated++;
|
||||
}
|
||||
log.info("[attendee] 劳务协议回填完成 meetingId={} 共{}个", meetingId, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int uploadExpertPhotos(MultipartFile file, Long meetingId) throws Exception {
|
||||
if (meetingId == null) throw new ServiceException("meetingId 不能为空");
|
||||
if (file == null || file.isEmpty()) throw new ServiceException("请选择要上传的 zip 文件");
|
||||
|
||||
List<BizMeetingAttendee> list = selectByMeetingId(meetingId);
|
||||
if (list.isEmpty()) throw new ServiceException("该会议暂无参会人, 无法回填专家照片");
|
||||
|
||||
byte[] zipBytes = file.getBytes();
|
||||
Map<Integer, List<byte[]>> folderBytes = new HashMap<>();
|
||||
Map<Integer, List<String>> folderNames = new HashMap<>();
|
||||
try {
|
||||
readAttendeeZip(zipBytes, StandardCharsets.UTF_8, folderBytes, folderNames);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("[attendee] 专家照片 zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage());
|
||||
folderBytes.clear();
|
||||
folderNames.clear();
|
||||
readAttendeeZip(zipBytes, Charset.forName("GBK"), folderBytes, folderNames);
|
||||
}
|
||||
|
||||
int updated = 0;
|
||||
for (Map.Entry<Integer, List<byte[]>> e : folderBytes.entrySet()) {
|
||||
int idx = e.getKey();
|
||||
if (idx < 0 || idx >= list.size()) {
|
||||
log.warn("[attendee] 专家照片 zip 目录序号 {} 越界 (参会人共 {}), 跳过", idx + 1, list.size());
|
||||
continue;
|
||||
}
|
||||
BizMeetingAttendee a = list.get(idx);
|
||||
List<byte[]> files = e.getValue();
|
||||
List<String> names = folderNames.get(idx);
|
||||
// 专家照片可多张: 目录内所有文件都上传, 逗号拼接覆盖式回填 on_site_photos
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
String fileName = (names != null && i < names.size()) ? names.get(i) : ("photo-" + (i + 1) + ".jpg");
|
||||
String url = ossUploader.upload(files.get(i), fileName, "ry8080/meeting/" + meetingId + "/expert-photo");
|
||||
if (sb.length() > 0) sb.append(",");
|
||||
sb.append(url);
|
||||
}
|
||||
|
||||
BizMeetingAttendee entity = new BizMeetingAttendee();
|
||||
entity.setId(a.getId());
|
||||
entity.setOnSitePhotos(sb.toString());
|
||||
entity.setUpdateBy(SecurityUtils.getUsername());
|
||||
updateProfile(entity);
|
||||
updated++;
|
||||
}
|
||||
log.info("[attendee] 专家照片回填完成 meetingId={} 共{}个", meetingId, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** 按指定字符集解压 zip, 把 {根目录}/{序号}_姓名/ 目录下的文件归集到对应参会人序号 */
|
||||
private void readAttendeeZip(byte[] zipBytes, Charset charset,
|
||||
Map<Integer, List<byte[]>> folderBytes,
|
||||
Map<Integer, List<String>> folderNames) throws IOException {
|
||||
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes), charset)) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
if (entry.isDirectory()) continue;
|
||||
String path = entry.getName();
|
||||
// 跳过 macOS 打包产生的元数据
|
||||
if (path.contains("__MACOSX") || path.endsWith(".DS_Store")) continue;
|
||||
Integer idx = parseAgreementIndex(path);
|
||||
if (idx == null) continue; // 不落在 {序号}_姓名 目录下, 忽略
|
||||
byte[] data = readAllBytes(zis);
|
||||
folderBytes.computeIfAbsent(idx, k -> new ArrayList<>()).add(data);
|
||||
folderNames.computeIfAbsent(idx, k -> new ArrayList<>()).add(lastSegment(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 zip 条目路径里解析出 {序号}_姓名 目录的 0-based 序号; 不匹配返回 null */
|
||||
private static Integer parseAgreementIndex(String path) {
|
||||
if (path == null || path.isEmpty()) return null;
|
||||
for (String seg : path.split("/")) {
|
||||
if (seg.isEmpty()) continue;
|
||||
int underscore = seg.indexOf('_');
|
||||
if (underscore <= 0) continue;
|
||||
String numPart = seg.substring(0, underscore);
|
||||
if (!numPart.matches("\\d+")) continue;
|
||||
try {
|
||||
return Integer.parseInt(numPart) - 1;
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String lastSegment(String path) {
|
||||
int i = path.lastIndexOf('/');
|
||||
return i >= 0 ? path.substring(i + 1) : path;
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
}
|
||||
+5
-10
@@ -25,23 +25,18 @@ public class BizMeetingExecutorServiceImpl implements IBizMeetingExecutorService
|
||||
return bizMeetingExecutorMapper.selectByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingExecutor> selectByUserId(Long userId) {
|
||||
return bizMeetingExecutorMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy) {
|
||||
public int replaceByMeetingId(Long meetingId, List<Long> orgIds, Long assignedBy) {
|
||||
bizMeetingExecutorMapper.deleteByMeetingId(meetingId);
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
if (orgIds == null || orgIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
List<BizMeetingExecutor> list = new ArrayList<>(userIds.size());
|
||||
for (Long uid : userIds) {
|
||||
List<BizMeetingExecutor> list = new ArrayList<>(orgIds.size());
|
||||
for (Long orgId : orgIds) {
|
||||
BizMeetingExecutor m = new BizMeetingExecutor();
|
||||
m.setMeetingId(meetingId);
|
||||
m.setUserId(uid);
|
||||
m.setExecutorOrgId(orgId);
|
||||
m.setAssignedBy(assignedBy);
|
||||
list.add(m);
|
||||
}
|
||||
|
||||
+534
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -10,15 +11,34 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||
import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
|
||||
import com.ruoyi.business.oss.OssUploader;
|
||||
import com.ruoyi.business.oss.OssZipService;
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||
|
||||
@Service
|
||||
@@ -35,10 +55,73 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
/** 可识别文件扩展名 (与前端 isRecognizable 对齐): 图片/PDF 才触发 OCR */
|
||||
private static final Pattern RECOGNIZABLE = Pattern.compile("\\.(jpe?g|png|pdf)$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/** 会务下载范围: 会务材料 (SERVICE) + 会务凭证 (SERVICE_VOUCHER) */
|
||||
private static final Set<String> SERVICE_MATERIAL_TYPES = new HashSet<>(Arrays.asList("SERVICE", "SERVICE_VOUCHER"));
|
||||
|
||||
/** 会务 subType → 中文材料类型名 (与前端 MeetingDetail.ROW_CONFIG 对齐), 作为 zip 内 "材料类型" 目录名 */
|
||||
private static final Map<String, String> SERVICE_SUBTYPE_LABEL = new HashMap<>();
|
||||
static {
|
||||
SERVICE_SUBTYPE_LABEL.put("M_MATERIAL", "物料制作");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_HOTEL", "酒店");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_TRAFFIC_BIG", "大交通");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_TRAFFIC_SMALL", "小交通");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_EXECUTION", "执行费");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_DESIGN", "设计费");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_OTHER", "其他");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_SETTLEMENT", "总结算单");
|
||||
SERVICE_SUBTYPE_LABEL.put("M_INVOICE", "总发票");
|
||||
SERVICE_SUBTYPE_LABEL.put("SV_PAYMENT", "会务付款凭证");
|
||||
}
|
||||
|
||||
/** 会务材料 (SERVICE) 子类顺序 (与前端 ROW_CONFIG SERVICE 行对齐), 用于"打包上传"下载空目录模板 + 回填匹配 */
|
||||
private static final List<String> SERVICE_MATERIAL_SUBTYPES = Arrays.asList(
|
||||
"M_MATERIAL", "M_HOTEL", "M_TRAFFIC_BIG", "M_TRAFFIC_SMALL",
|
||||
"M_EXECUTION", "M_DESIGN", "M_OTHER", "M_SETTLEMENT", "M_INVOICE");
|
||||
|
||||
/** 会务材料中文名 → subType (仅 M_* 会务材料, 不含 SV_PAYMENT 付款凭证), 用于上传 zip 按目录名匹配 */
|
||||
private static final Map<String, String> SERVICE_LABEL_TO_SUBTYPE = new HashMap<>();
|
||||
static {
|
||||
for (String sub : SERVICE_MATERIAL_SUBTYPES) {
|
||||
SERVICE_LABEL_TO_SUBTYPE.put(SERVICE_SUBTYPE_LABEL.get(sub), sub);
|
||||
}
|
||||
}
|
||||
|
||||
/** 会务材料"打包上传" zip 顶层目录名 */
|
||||
private static final String SERVICE_ZIP_ROOT = "会务材料";
|
||||
|
||||
/** 劳务下载范围: 劳务材料 (LABOR) + 劳务凭证 (LABOR_VOUCHER) */
|
||||
private static final Set<String> LABOR_MATERIAL_TYPES = new HashSet<>(Arrays.asList("LABOR", "LABOR_VOUCHER"));
|
||||
|
||||
/** 劳务 subType → 中文材料类型名 (与前端 MeetingDetail.ROW_CONFIG 对齐) */
|
||||
private static final Map<String, String> LABOR_SUBTYPE_LABEL = new HashMap<>();
|
||||
static {
|
||||
LABOR_SUBTYPE_LABEL.put("L_DETAIL", "劳务明细表");
|
||||
LABOR_SUBTYPE_LABEL.put("L_AGREEMENT", "劳务协议");
|
||||
LABOR_SUBTYPE_LABEL.put("L_ENTERPRISE_BENEFIT", "企业权益");
|
||||
LABOR_SUBTYPE_LABEL.put("L_SIGN_IN", "签到表");
|
||||
LABOR_SUBTYPE_LABEL.put("L_PANORAMA_FRONT", "前全景");
|
||||
LABOR_SUBTYPE_LABEL.put("L_PANORAMA_BACK", "后全景");
|
||||
LABOR_SUBTYPE_LABEL.put("L_EXPERT_PHOTO", "专家照片");
|
||||
LABOR_SUBTYPE_LABEL.put("LV_PAYMENT", "劳务付款凭证");
|
||||
}
|
||||
|
||||
private static final String XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
|
||||
@Autowired
|
||||
private BizMeetingMaterialMapper bizMeetingMaterialMapper;
|
||||
@Autowired
|
||||
private BizMeetingMapper bizMeetingMapper;
|
||||
@Autowired
|
||||
private OssZipService ossZipService;
|
||||
@Autowired
|
||||
private IBizMeetingAttendeeService attendeeService;
|
||||
@Autowired
|
||||
private OssUploader ossUploader;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private InvoiceOcrService invoiceOcrService;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BizMeetingMaterialServiceImpl.class);
|
||||
|
||||
@Override
|
||||
public BizMeetingMaterial getById(Long id) {
|
||||
@@ -103,6 +186,19 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
} catch (DuplicateKeyException e) {
|
||||
throw new ServiceException("材料上传重复, 请检查 (同一会议下同一资料类型同一子分类只能有一条记录)");
|
||||
}
|
||||
// foreach 批量插入的 useGeneratedKeys 不可靠, 插完后再查回真实 id (subType 会议内唯一), 前端据此触发 OCR
|
||||
List<BizMeetingMaterial> inserted = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||
Map<String, Long> idBySubType = new HashMap<>();
|
||||
if (inserted != null) {
|
||||
for (BizMeetingMaterial im : inserted) {
|
||||
if (im.getSubType() != null) idBySubType.put(im.getSubType(), im.getId());
|
||||
}
|
||||
}
|
||||
for (BizMeetingMaterial m : list) {
|
||||
if (m.getSubType() != null) {
|
||||
m.setId(idBySubType.get(m.getSubType()));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -162,6 +258,444 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算时保存付款凭证 (LV_PAYMENT + SV_PAYMENT, 合规人员上传).
|
||||
* 只清/插这两个 subType, 不碰其他材料; 付款凭证非发票, 不触发 OCR, 不影响会议费用.
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveVouchers(Long meetingId, List<BizMeetingMaterial> vouchers) {
|
||||
// 删旧 LV_PAYMENT / SV_PAYMENT (幂等, 避免 uk_meeting_type_sub 冲突)
|
||||
List<BizMeetingMaterial> existing = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||
if (existing != null) {
|
||||
for (BizMeetingMaterial m : existing) {
|
||||
if ("LV_PAYMENT".equals(m.getSubType()) || "SV_PAYMENT".equals(m.getSubType())) {
|
||||
bizMeetingMaterialMapper.deleteByPrimaryKey(m.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vouchers == null || vouchers.isEmpty()) return;
|
||||
List<BizMeetingMaterial> toInsert = new java.util.ArrayList<>();
|
||||
for (BizMeetingMaterial v : vouchers) {
|
||||
if (v == null || v.getSubType() == null || v.getOssUrl() == null || v.getOssUrl().isEmpty()) continue;
|
||||
if (!"LV_PAYMENT".equals(v.getSubType()) && !"SV_PAYMENT".equals(v.getSubType())) continue;
|
||||
v.setId(null);
|
||||
v.setMeetingId(meetingId);
|
||||
v.setMaterialType("LV_PAYMENT".equals(v.getSubType()) ? "LABOR_VOUCHER" : "SERVICE_VOUCHER");
|
||||
v.setAmount(null);
|
||||
v.setFeeStatus(1); // 付款凭证非发票, 直接标记已计算, 不参与费用汇总
|
||||
v.setCreateTime(new Date());
|
||||
v.setCreatorId(SecurityUtils.getUserId());
|
||||
toInsert.add(v);
|
||||
}
|
||||
if (!toInsert.isEmpty()) {
|
||||
bizMeetingMaterialMapper.insertBatch(toInsert);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打成一个 zip, 返回下载 URL.
|
||||
* <p>
|
||||
* zip 内目录结构: {项目名称}/{材料类型中文名}/{材料文件}. 实现分两步:
|
||||
* <ol>
|
||||
* <li>staging copy: 把散落的材料文件复制到 download/huiwu/{meetingId}/ 前缀下,
|
||||
* key 结构即 zip 内目录结构 (FC 按 source-dir 前缀打包, zip 内相对路径 = key 相对前缀部分)</li>
|
||||
* <li>FC 打包: POST {bucket, source-dir} 给阿里云函数计算, 拿 zip 签名 URL</li>
|
||||
* </ol>
|
||||
* 文件名: 优先用 fileName (历史数据可能有), 否则用 {材料类型中文名}{扩展名}.
|
||||
*/
|
||||
@Override
|
||||
public String buildServiceZipUrl(Long meetingId)
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (meeting == null) throw new ServiceException("会议不存在");
|
||||
String prefix = "download/huiwu/" + meetingId + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
int copied = stageServiceMaterials(meetingId, prefix, projectFolderName(meeting));
|
||||
if (copied == 0) throw new ServiceException("该会议暂无可下载的会务材料");
|
||||
return ossZipService.zipDownload(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息 Excel 在 OSS 端打成一个 zip, 返回下载 URL.
|
||||
* <p>
|
||||
* zip 内目录结构: {项目名称}/{材料类型中文名}/{材料文件}, 外加 {项目名称}/参会人信息/参会人信息.xlsx.
|
||||
* 前全景(L_PANORAMA_FRONT)/后全景(L_PANORAMA_BACK) 归入"会议现场全景"目录 (文件名分别用 前全景/后全景 区分, 避免同目录重名).
|
||||
*/
|
||||
@Override
|
||||
public String buildLaborZipUrl(Long meetingId)
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||
if (meeting == null) throw new ServiceException("会议不存在");
|
||||
String prefix = "download/labor/" + meetingId + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
int copied = stageLaborMaterials(meetingId, prefix, projectFolderName(meeting));
|
||||
if (copied == 0) throw new ServiceException("该会议暂无可下载的劳务材料");
|
||||
return ossZipService.zipDownload(prefix);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 批量下载 (多会议合并打一个 zip)
|
||||
// ===================================================================
|
||||
|
||||
/** 批量会务下载: 多个会议的会务材料合并 staging 到一个 batch 前缀, 一次 FC 打 zip 返回 URL */
|
||||
@Override
|
||||
public String buildBatchServiceZipUrl(List<Long> meetingIds)
|
||||
{
|
||||
List<Long> ids = normalizeIds(meetingIds);
|
||||
String prefix = "download/huiwu/batch/" + System.currentTimeMillis() + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
Set<String> usedFolders = new HashSet<>();
|
||||
int copied = 0;
|
||||
for (Long id : ids)
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(id);
|
||||
if (meeting == null) continue; // 已软删/不存在, 跳过
|
||||
copied += stageServiceMaterials(id, prefix, uniqueFolder(usedFolders, batchMeetingFolderName(meeting)));
|
||||
}
|
||||
if (copied == 0) throw new ServiceException("所选会议暂无可下载的会务材料");
|
||||
return ossZipService.zipDownload(prefix);
|
||||
}
|
||||
|
||||
/** 批量劳务下载: 多个会议的劳务材料(含参会人信息)合并 staging 到一个 batch 前缀, 一次 FC 打 zip 返回 URL */
|
||||
@Override
|
||||
public String buildBatchLaborZipUrl(List<Long> meetingIds)
|
||||
{
|
||||
List<Long> ids = normalizeIds(meetingIds);
|
||||
String prefix = "download/labor/batch/" + System.currentTimeMillis() + "/";
|
||||
ossZipService.clearPrefix(prefix);
|
||||
Set<String> usedFolders = new HashSet<>();
|
||||
int copied = 0;
|
||||
for (Long id : ids)
|
||||
{
|
||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(id);
|
||||
if (meeting == null) continue;
|
||||
copied += stageLaborMaterials(id, prefix, uniqueFolder(usedFolders, batchMeetingFolderName(meeting)));
|
||||
}
|
||||
if (copied == 0) throw new ServiceException("所选会议暂无可下载的劳务材料");
|
||||
return ossZipService.zipDownload(prefix);
|
||||
}
|
||||
|
||||
/** 收集该会议会务材料并 copy 到 staging 前缀 prefix + folderName 下, 返回 copy 的文件数 (0 = 无会务材料) */
|
||||
private int stageServiceMaterials(Long meetingId, String prefix, String folderName)
|
||||
{
|
||||
int copied = 0;
|
||||
List<BizMeetingMaterial> all = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||
if (all == null) return copied;
|
||||
for (BizMeetingMaterial m : all)
|
||||
{
|
||||
if (m.getOssUrl() == null || m.getOssUrl().isEmpty()) continue;
|
||||
if (m.getMaterialType() == null || !SERVICE_MATERIAL_TYPES.contains(m.getMaterialType())) continue;
|
||||
String srcKey = ossZipService.extractKey(m.getOssUrl());
|
||||
if (srcKey == null || srcKey.isEmpty()) continue;
|
||||
String label = SERVICE_SUBTYPE_LABEL.getOrDefault(m.getSubType(), (m.getSubType() == null ? "其他" : m.getSubType()));
|
||||
String ext = extOf(srcKey);
|
||||
String fileName = (m.getFileName() != null && !m.getFileName().trim().isEmpty())
|
||||
? safeName(m.getFileName()) : safeName(label) + ext;
|
||||
String dstKey = prefix + folderName + "/" + safeName(label) + "/" + fileName;
|
||||
ossZipService.copyObject(srcKey, dstKey);
|
||||
copied++;
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
/** 收集该会议劳务材料(含参会人信息 Excel)并 staging 到 prefix + folderName 下, 返回写入的文件数 (0 = 无劳务材料且无参会人) */
|
||||
private int stageLaborMaterials(Long meetingId, String prefix, String folderName)
|
||||
{
|
||||
int copied = 0;
|
||||
List<BizMeetingMaterial> all = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||
if (all != null)
|
||||
{
|
||||
for (BizMeetingMaterial m : all)
|
||||
{
|
||||
if (m.getOssUrl() == null || m.getOssUrl().isEmpty()) continue;
|
||||
if (m.getMaterialType() == null || !LABOR_MATERIAL_TYPES.contains(m.getMaterialType())) continue;
|
||||
String srcKey = ossZipService.extractKey(m.getOssUrl());
|
||||
if (srcKey == null || srcKey.isEmpty()) continue;
|
||||
String folder = laborFolderName(m.getSubType());
|
||||
String fileBase = laborFileLabel(m.getSubType());
|
||||
String ext = extOf(srcKey);
|
||||
String fileName = (m.getFileName() != null && !m.getFileName().trim().isEmpty())
|
||||
? safeName(m.getFileName()) : safeName(fileBase) + ext;
|
||||
String dstKey = prefix + folderName + "/" + safeName(folder) + "/" + fileName;
|
||||
ossZipService.copyObject(srcKey, dstKey);
|
||||
copied++;
|
||||
}
|
||||
}
|
||||
List<BizMeetingAttendee> attendees = attendeeService.selectByMeetingId(meetingId);
|
||||
if (attendees != null && !attendees.isEmpty())
|
||||
{
|
||||
byte[] excel = attendeeService.buildAttendeeInfoExcel(attendees);
|
||||
ossZipService.putObject(prefix + folderName + "/参会人信息/参会人信息.xlsx", excel, XLSX_CONTENT_TYPE);
|
||||
copied++;
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
/** 单会议 zip 顶层目录名: 项目名 (为空回退项目编号, 再回退会议ID), 统一消毒 */
|
||||
private static String projectFolderName(BizMeeting meeting)
|
||||
{
|
||||
String name = meeting.getProjectName();
|
||||
if (name == null || name.trim().isEmpty())
|
||||
{
|
||||
name = (meeting.getProjectNo() != null && !meeting.getProjectNo().trim().isEmpty())
|
||||
? meeting.getProjectNo() : "会议" + meeting.getMeetingId();
|
||||
}
|
||||
return safeName(name);
|
||||
}
|
||||
|
||||
/** 批量 zip 每个会议顶层目录名: 项目编号_会议名_第N期 (缺项跳过, 全空回退会议ID) */
|
||||
private static String batchMeetingFolderName(BizMeeting meeting)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendPart(sb, meeting.getProjectNo());
|
||||
appendPart(sb, meeting.getMeetingName());
|
||||
if (meeting.getPeriodNo() != null)
|
||||
{
|
||||
appendPart(sb, "第" + meeting.getPeriodNo() + "期");
|
||||
}
|
||||
String name = sb.toString();
|
||||
if (name.isEmpty()) return "会议" + meeting.getMeetingId();
|
||||
return safeName(name);
|
||||
}
|
||||
|
||||
private static void appendPart(StringBuilder sb, String v)
|
||||
{
|
||||
if (v == null || v.trim().isEmpty()) return;
|
||||
if (sb.length() > 0) sb.append("_");
|
||||
sb.append(v.trim());
|
||||
}
|
||||
|
||||
/** 目录名去重: 同名会议加 _2/_3 后缀, 避免 OSS 同名 key 静默覆盖丢数据 */
|
||||
private static String uniqueFolder(Set<String> used, String base)
|
||||
{
|
||||
String name = base;
|
||||
int i = 2;
|
||||
while (!used.add(name))
|
||||
{
|
||||
name = base + "_" + (i++);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/** 批量 id 清洗: 去空 + 去重 + 保序 + 上限校验 */
|
||||
private static List<Long> normalizeIds(List<Long> meetingIds)
|
||||
{
|
||||
if (meetingIds == null || meetingIds.isEmpty()) throw new ServiceException("请先选择要下载的会议");
|
||||
Set<Long> seen = new HashSet<>();
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (Long id : meetingIds)
|
||||
{
|
||||
if (id == null || !seen.add(id)) continue;
|
||||
ids.add(id);
|
||||
}
|
||||
if (ids.size() > 20) throw new ServiceException("一次最多批量下载 20 个会议");
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** 劳务材料目录名: 前/后全景归入"会议现场全景", 其余用 subType 中文名 */
|
||||
private static String laborFolderName(String subType)
|
||||
{
|
||||
if ("L_PANORAMA_FRONT".equals(subType) || "L_PANORAMA_BACK".equals(subType)) return "会议现场全景";
|
||||
return LABOR_SUBTYPE_LABEL.getOrDefault(subType, (subType == null ? "其他" : subType));
|
||||
}
|
||||
|
||||
/** 劳务材料文件名 (fileName 为空时用它+扩展名): 前/后全景分别用 前全景/后全景 区分 */
|
||||
private static String laborFileLabel(String subType)
|
||||
{
|
||||
return LABOR_SUBTYPE_LABEL.getOrDefault(subType, (subType == null ? "其他" : subType));
|
||||
}
|
||||
|
||||
/** 取 object key 的扩展名 (含点), 无则空串 */
|
||||
private static String extOf(String key)
|
||||
{
|
||||
int i = key.lastIndexOf('.');
|
||||
return i >= 0 ? key.substring(i) : "";
|
||||
}
|
||||
|
||||
/** 目录/文件名消毒: 去掉 OSS key 与 zip 路径里的非法字符 (斜杠/反斜杠/冒号等) */
|
||||
private static String safeName(String s)
|
||||
{
|
||||
if (s == null) return "";
|
||||
String r = s.replaceAll("[\\\\/:*?\"<>|\\r\\n\\t]", "_").trim();
|
||||
return r.isEmpty() ? "_" : r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成"会务材料"空目录模板 zip (会务材料 tab "打包上传" 的"下载目录"按钮).
|
||||
* zip 内为 会务材料/{材料类型中文名}/ 空目录 (物料制作/酒店/大交通/小交通/执行费/设计费/其他/总结算单/总发票),
|
||||
* 用户把材料文件放进对应目录后重新压缩上传.
|
||||
*/
|
||||
@Override
|
||||
public byte[] buildServiceTemplateZip(Long meetingId) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) {
|
||||
for (String sub : SERVICE_MATERIAL_SUBTYPES) {
|
||||
String label = SERVICE_SUBTYPE_LABEL.getOrDefault(sub, sub);
|
||||
String dir = SERVICE_ZIP_ROOT + "/" + label + "/";
|
||||
zos.putNextEntry(new ZipEntry(dir));
|
||||
zos.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("生成会务材料目录模板失败: " + e.getMessage());
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传"会务材料" zip, 解压后按 会务材料/{材料类型中文名}/ 目录匹配 subType,
|
||||
* 单文件直接上传 OSS, 多文件先打 zip 再上传 OSS, 按 (meetingId, SERVICE, subType) upsert 回填.
|
||||
* 替换文件会清空 amount; 单文件可识别 (jpg/png/pdf) 或多文件打 zip → 直接后台触发 OCR 回写金额.
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int uploadServiceMaterials(MultipartFile file, Long meetingId) throws Exception {
|
||||
if (meetingId == null) throw new ServiceException("meetingId 不能为空");
|
||||
if (file == null || file.isEmpty()) throw new ServiceException("请选择要上传的 zip 文件");
|
||||
|
||||
byte[] zipBytes = file.getBytes();
|
||||
|
||||
// subType -> 该目录下文件字节 + 文件名
|
||||
Map<String, List<byte[]>> folderBytes = new HashMap<>();
|
||||
Map<String, List<String>> folderNames = new HashMap<>();
|
||||
try {
|
||||
readServiceZip(zipBytes, StandardCharsets.UTF_8, folderBytes, folderNames);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 中文 Windows 打包器 (WinRAR/资源管理器) 用 GBK 编码条目名且不置 UTF-8 标志, 回退 GBK 重解析
|
||||
log.warn("[material] 会务材料 zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage());
|
||||
folderBytes.clear();
|
||||
folderNames.clear();
|
||||
readServiceZip(zipBytes, Charset.forName("GBK"), folderBytes, folderNames);
|
||||
}
|
||||
if (folderBytes.isEmpty()) throw new ServiceException("未在 zip 中找到会务材料目录, 请按下载模板的目录结构放置文件");
|
||||
|
||||
// 已存在的会务材料 subType -> 行 (决定 update / insert)
|
||||
Map<String, BizMeetingMaterial> existingBySubType = new HashMap<>();
|
||||
List<BizMeetingMaterial> existing = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||
if (existing != null) {
|
||||
for (BizMeetingMaterial m : existing) {
|
||||
if (m.getSubType() != null) existingBySubType.put(m.getSubType(), m);
|
||||
}
|
||||
}
|
||||
|
||||
String subDir = "ry8080/meeting/" + meetingId + "/service";
|
||||
int updated = 0;
|
||||
for (Map.Entry<String, List<byte[]>> e : folderBytes.entrySet()) {
|
||||
String subType = e.getKey();
|
||||
List<byte[]> files = e.getValue();
|
||||
if (files == null || files.isEmpty()) continue;
|
||||
List<String> names = folderNames.get(subType);
|
||||
String label = SERVICE_SUBTYPE_LABEL.getOrDefault(subType, subType);
|
||||
|
||||
String ossUrl;
|
||||
String fileName;
|
||||
if (files.size() == 1) {
|
||||
fileName = (names != null && !names.isEmpty()) ? names.get(0) : (label + ".jpg");
|
||||
ossUrl = ossUploader.upload(files.get(0), fileName, subDir);
|
||||
} else {
|
||||
// 目录内多文件 → 先打 zip 再上传 OSS
|
||||
fileName = label + ".zip";
|
||||
ossUrl = ossUploader.upload(zipFiles(files, names), fileName, subDir);
|
||||
}
|
||||
|
||||
// 会务材料都是发票类 (M_* 不在 NON_OCR 集合): 单文件可识别 (jpg/png/pdf) 或多文件打 zip → 待 OCR(0), 其余已计算(1)
|
||||
boolean isZip = files.size() > 1;
|
||||
boolean ocrNeeded = isZip || (ossUrl != null && RECOGNIZABLE.matcher(ossUrl).find());
|
||||
int feeStatus = ocrNeeded ? 0 : 1;
|
||||
|
||||
BizMeetingMaterial old = existingBySubType.get(subType);
|
||||
Long materialId;
|
||||
Long oldMaterialId;
|
||||
if (old != null) {
|
||||
materialId = old.getId();
|
||||
bizMeetingMaterialMapper.updateFile(old.getId(), ossUrl, fileName, feeStatus);
|
||||
oldMaterialId = old.getId(); // 替换: 清理该 material 的旧 invoice 行
|
||||
} else {
|
||||
BizMeetingMaterial ins = new BizMeetingMaterial();
|
||||
ins.setMeetingId(meetingId);
|
||||
ins.setMaterialType("SERVICE");
|
||||
ins.setSubType(subType);
|
||||
ins.setOssUrl(ossUrl);
|
||||
ins.setFileName(fileName);
|
||||
ins.setAmount(null);
|
||||
ins.setFeeStatus(feeStatus);
|
||||
ins.setCreatorId(SecurityUtils.getUserId());
|
||||
ins.setCreateTime(new Date());
|
||||
bizMeetingMaterialMapper.insertBatch(Collections.singletonList(ins));
|
||||
// foreach 批量插入 useGeneratedKeys 不可靠, 按 subType 查回真实 id
|
||||
BizMeetingMaterial inserted = bizMeetingMaterialMapper.selectByMeetingAndSubType(meetingId, subType);
|
||||
materialId = inserted != null ? inserted.getId() : null;
|
||||
oldMaterialId = null;
|
||||
}
|
||||
// 与前端"保存"流程一致, 后台触发 OCR (识别为发票回写 amount → 会议费用汇总)
|
||||
if (ocrNeeded) {
|
||||
invoiceOcrService.submitRecognition(materialId, meetingId, ossUrl, isZip, oldMaterialId);
|
||||
}
|
||||
updated++;
|
||||
}
|
||||
log.info("[material] 会务材料回填完成 meetingId={} 共{}类", meetingId, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** 按指定字符集解压 zip, 把 会务材料/{材料中文名}/ 目录下的文件归集到对应 subType */
|
||||
private void readServiceZip(byte[] zipBytes, Charset charset,
|
||||
Map<String, List<byte[]>> folderBytes,
|
||||
Map<String, List<String>> folderNames) throws IOException {
|
||||
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes), charset)) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
if (entry.isDirectory()) continue;
|
||||
String path = entry.getName();
|
||||
if (path.contains("__MACOSX") || path.endsWith(".DS_Store")) continue;
|
||||
String subType = matchServiceLabel(path);
|
||||
if (subType == null) continue; // 不落在 会务材料/{材料中文名}/ 目录下, 忽略
|
||||
byte[] data = readAllBytes(zis);
|
||||
folderBytes.computeIfAbsent(subType, k -> new ArrayList<>()).add(data);
|
||||
folderNames.computeIfAbsent(subType, k -> new ArrayList<>()).add(lastSegment(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 zip 条目路径里匹配出会务材料 subType (扫描路径段命中材料中文名); 不匹配返回 null */
|
||||
private static String matchServiceLabel(String path) {
|
||||
if (path == null || path.isEmpty()) return null;
|
||||
for (String seg : path.split("/")) {
|
||||
if (seg.isEmpty()) continue;
|
||||
String sub = SERVICE_LABEL_TO_SUBTYPE.get(seg);
|
||||
if (sub != null) return sub;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 把目录内多个文件打成内存 zip (保留原始文件名) */
|
||||
private static byte[] zipFiles(List<byte[]> files, List<String> names) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) {
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
String name = (names != null && i < names.size()) ? names.get(i) : ("file-" + (i + 1));
|
||||
zos.putNextEntry(new ZipEntry(name));
|
||||
zos.write(files.get(i));
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static String lastSegment(String path) {
|
||||
int i = path.lastIndexOf('/');
|
||||
return i >= 0 ? path.substring(i + 1) : path;
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否会被前端提交 OCR (与 MeetingDetail.saveMaterials 门控一致):
|
||||
* 文件可识别 (jpg/jpeg/png/pdf) 且 不在非发票 subType 集合里.
|
||||
|
||||
+1
-4
@@ -46,16 +46,13 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
||||
}
|
||||
// 新建会议初始状态: DB 列默认值 '0' 会让前端 stageLabel 显示成 0 而不是 enum 项, 这里显式兜底成 enum.
|
||||
// current_stage = NOT_STARTED (未执行, 等 scheduler 过 startTime 置 is_executed 转 RUNNING)
|
||||
// 材料/凭证审核子状态 = NOT_SUBMITTED (未提交, 供执行方 submit-material/submit-voucher 校验)
|
||||
// 材料审核子状态 = NOT_SUBMITTED (未提交, 供执行方 submit-material 校验)
|
||||
if (entity.getCurrentStage() == null || entity.getCurrentStage().isEmpty()) {
|
||||
entity.setCurrentStage(BizMeetingStageEnum.NOT_STARTED.getCode());
|
||||
}
|
||||
if (entity.getMaterialAuditStage() == null || entity.getMaterialAuditStage().isEmpty()) {
|
||||
entity.setMaterialAuditStage("NOT_SUBMITTED");
|
||||
}
|
||||
if (entity.getVoucherAuditStage() == null || entity.getVoucherAuditStage().isEmpty()) {
|
||||
entity.setVoucherAuditStage("NOT_SUBMITTED");
|
||||
}
|
||||
return bizMeetingMapper.insert(entity);
|
||||
}
|
||||
@Override
|
||||
|
||||
+5
-10
@@ -25,23 +25,18 @@ public class BizMeetingSupervisorServiceImpl implements IBizMeetingSupervisorSer
|
||||
return bizMeetingSupervisorMapper.selectByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMeetingSupervisor> selectByUserId(Long userId) {
|
||||
return bizMeetingSupervisorMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int replaceByMeetingId(Long meetingId, List<Long> userIds, Long assignedBy) {
|
||||
public int replaceByMeetingId(Long meetingId, List<Long> orgIds, Long assignedBy) {
|
||||
bizMeetingSupervisorMapper.deleteByMeetingId(meetingId);
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
if (orgIds == null || orgIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
List<BizMeetingSupervisor> list = new ArrayList<>(userIds.size());
|
||||
for (Long uid : userIds) {
|
||||
List<BizMeetingSupervisor> list = new ArrayList<>(orgIds.size());
|
||||
for (Long orgId : orgIds) {
|
||||
BizMeetingSupervisor m = new BizMeetingSupervisor();
|
||||
m.setMeetingId(meetingId);
|
||||
m.setUserId(uid);
|
||||
m.setSponsorOrgId(orgId);
|
||||
m.setAssignedBy(assignedBy);
|
||||
list.add(m);
|
||||
}
|
||||
|
||||
+93
@@ -1,15 +1,21 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
import com.ruoyi.business.domain.vo.BizOrgImportVo;
|
||||
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||
import com.ruoyi.business.service.IBizOrgService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.id.SnowflakeId;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
|
||||
@Service
|
||||
@@ -52,11 +58,22 @@ public class BizOrgServiceImpl implements IBizOrgService {
|
||||
return bizOrgMapper.selectExecutorOrgOptions(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> selectSponsorRegisterOptions(BizOrg entity) {
|
||||
return bizOrgMapper.selectSponsorRegisterOptions(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> selectMySponsorCompany(Long userId) {
|
||||
return bizOrgMapper.selectMySponsorCompany(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long selectOrgIdByUserId(Long userId) {
|
||||
if (userId == null) return null;
|
||||
return bizOrgMapper.selectOrgIdByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用 org, 同步主账号 sys_user.status
|
||||
* biz_org.status: '0' (正常) / '1' (禁用) → sys_user.status 同码 ('0' 正常, '1' 停用)
|
||||
@@ -82,4 +99,80 @@ public class BizOrgServiceImpl implements IBizOrgService {
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入支持单位 (只导 org, 不新增人员):
|
||||
* <ol>
|
||||
* <li>ExcelUtil 反序列化 + 行级校验 (公司名称必填)</li>
|
||||
* <li>批内去重 (公司名称 / 税号) + DB 去重 (sponsor 下同税号已存在)</li>
|
||||
* <li>逐行 insert biz_org: orgType='sponsor', status='0', userId 留空 (不建 sys_user)</li>
|
||||
* </ol>
|
||||
* 失败行为: 单行失败只记 ngList, 不影响其它行 (跨行不回滚).
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ImportResult importOrg(MultipartFile file, String operName) throws Exception {
|
||||
ExcelUtil<BizOrgImportVo> util = new ExcelUtil<>(BizOrgImportVo.class);
|
||||
List<BizOrgImportVo> importList = util.importExcel(file.getInputStream());
|
||||
if (importList == null || importList.isEmpty()) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
}
|
||||
ImportResult result = new ImportResult();
|
||||
|
||||
Set<String> seenOrgName = new HashSet<>();
|
||||
Set<String> seenTaxNo = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < importList.size(); i++) {
|
||||
BizOrgImportVo vo = importList.get(i);
|
||||
int rowNo = i + 2; // Excel 行号 (1=表头)
|
||||
try {
|
||||
if (vo.getOrgName() == null || vo.getOrgName().trim().isEmpty()) {
|
||||
throw new ServiceException("公司名称不能为空");
|
||||
}
|
||||
String orgName = vo.getOrgName().trim();
|
||||
String taxNo = vo.getTaxNo() == null ? "" : vo.getTaxNo().trim();
|
||||
|
||||
// 批内去重
|
||||
if (!seenOrgName.add(orgName)) {
|
||||
throw new ServiceException("公司名称「" + orgName + "」在文件中重复");
|
||||
}
|
||||
if (!taxNo.isEmpty()) {
|
||||
if (!seenTaxNo.add(taxNo)) {
|
||||
throw new ServiceException("税号「" + taxNo + "」在文件中重复");
|
||||
}
|
||||
// DB 去重: sponsor 下同税号已存在
|
||||
BizOrg probe = new BizOrg();
|
||||
probe.setOrgType("sponsor");
|
||||
probe.setTaxNo(taxNo);
|
||||
if (!bizOrgMapper.selectList(probe).isEmpty()) {
|
||||
throw new ServiceException("税号「" + taxNo + "」已存在");
|
||||
}
|
||||
}
|
||||
|
||||
BizOrg org = new BizOrg();
|
||||
org.setOrgType("sponsor");
|
||||
org.setOrgName(orgName);
|
||||
org.setTaxNo(taxNo.isEmpty() ? null : taxNo);
|
||||
org.setBusinessNature(trimToNull(vo.getBusinessNature()));
|
||||
org.setAddress(trimToNull(vo.getAddress()));
|
||||
org.setContactName(trimToNull(vo.getContactName()));
|
||||
org.setContactPhone(trimToNull(vo.getContactPhone()));
|
||||
org.setStatus("0");
|
||||
org.setCreateBy(operName);
|
||||
bizOrgMapper.insert(org);
|
||||
|
||||
result.ok();
|
||||
} catch (Exception e) {
|
||||
result.fail(rowNo, e.getMessage() == null ? "导入失败" : e.getMessage());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 空串转 null (避免插空字符串脏数据) */
|
||||
private String trimToNull(String s) {
|
||||
if (s == null) return null;
|
||||
String t = s.trim();
|
||||
return t.isEmpty() ? null : t;
|
||||
}
|
||||
}
|
||||
|
||||
+55
-1
@@ -3,6 +3,7 @@ package com.ruoyi.business.service.impl;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizPerson;
|
||||
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||
@@ -69,7 +70,12 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
}
|
||||
}
|
||||
if (entity.getOrgId() == null) {
|
||||
throw new ServiceException("请填写所属公司 (主账号尚未注册公司, 请联系 admin 在 [支持单位管理/服务机构管理] 录入 orgType=" + entity.getUnitType() + " 的公司并关联 user_id)");
|
||||
// 支持单位: 公司由 admin 在 [支持单位管理] 预录, 支持方注册时选公司 (不新建);
|
||||
// 执行单位: 主账号注册时自行创建公司 (registerExecutor 建 biz_org), 不走 admin 录入.
|
||||
if ("executor".equals(entity.getUnitType())) {
|
||||
throw new ServiceException("请填写所属公司 (执行单位需自行创建公司, 请先完成公司注册)");
|
||||
}
|
||||
throw new ServiceException("请填写所属公司 (主账号尚未注册公司, 请联系 admin 在 [支持单位管理] 录入 orgType=" + entity.getUnitType() + " 的公司并关联 user_id)");
|
||||
}
|
||||
|
||||
// 1. 创建 sys_user 子账号
|
||||
@@ -80,6 +86,7 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
newUser.setUserName(entity.getLoginUsername());
|
||||
newUser.setNickName(entity.getName());
|
||||
newUser.setPhonenumber(entity.getPhone());
|
||||
newUser.setEmail(entity.getEmail());
|
||||
newUser.setPassword(SecurityUtils.encryptPassword(entity.getLoginPassword()));
|
||||
newUser.setAccountType("SUB");
|
||||
newUser.setParentUserId(mainUserId);
|
||||
@@ -116,6 +123,7 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
u.setUserId(entity.getUserId());
|
||||
u.setNickName(entity.getName());
|
||||
u.setPhonenumber(entity.getPhone());
|
||||
u.setEmail(entity.getEmail());
|
||||
u.setUpdateBy(SecurityUtils.getUsername());
|
||||
// status 同步 (如果前端传了 status 字段)
|
||||
if (entity.getStatus() != null) {
|
||||
@@ -139,4 +147,50 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
int n = bizPersonMapper.deleteByPrimaryKeys(personIds);
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更换机构管理员: 目标人员晋升 MAIN (管理员), 原管理员降为 SUB
|
||||
* 数据口径: MAIN ⇔ parent_user_id IS NULL, SUB ⇔ parent_user_id = 主账号 user_id
|
||||
* 操作顺序避免出现三层/环: 先 re-point 其余子账号 → 晋升新管理员 → 降级原管理员 → 改 biz_org 归属
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int changeOrgAdmin(String personId) {
|
||||
BizPerson target = bizPersonMapper.selectByPrimaryKey(personId);
|
||||
if (target == null || target.getUserId() == null) {
|
||||
throw new ServiceException("人员不存在或未关联登录账号");
|
||||
}
|
||||
Long newAdminUid = target.getUserId();
|
||||
SysUser newUser = sysUserMapper.selectUserById(newAdminUid);
|
||||
if (newUser == null) {
|
||||
throw new ServiceException("登录账号不存在");
|
||||
}
|
||||
if ("MAIN".equals(newUser.getAccountType())) {
|
||||
return 0; // 已是管理员, 无需操作
|
||||
}
|
||||
if (target.getOrgId() == null) {
|
||||
throw new ServiceException("该人员未关联机构, 无法设为管理员");
|
||||
}
|
||||
BizOrg org = bizOrgMapper.selectByPrimaryKey(target.getOrgId());
|
||||
Long oldAdminUid = (org == null) ? null : org.getUserId();
|
||||
|
||||
// 1. 原管理员的其他子账号 re-point 到新管理员 (排除新管理员自己, 其 parent 当前仍指向原管理员)
|
||||
if (oldAdminUid != null && !oldAdminUid.equals(newAdminUid)) {
|
||||
sysUserMapper.updateParentUserId(oldAdminUid, newAdminUid);
|
||||
}
|
||||
// 2. 新管理员晋升 MAIN (parent_user_id = NULL)
|
||||
sysUserMapper.updateAccountType(newAdminUid, "MAIN", null);
|
||||
// 3. 原管理员降级 SUB (parent 指向新管理员)
|
||||
if (oldAdminUid != null && !oldAdminUid.equals(newAdminUid)) {
|
||||
sysUserMapper.updateAccountType(oldAdminUid, "SUB", newAdminUid);
|
||||
}
|
||||
// 4. biz_org.user_id 同步指向新管理员 (机构归属/反查)
|
||||
if (org != null) {
|
||||
BizOrg u = new BizOrg();
|
||||
u.setOrgId(target.getOrgId());
|
||||
u.setUserId(newAdminUid);
|
||||
bizOrgMapper.updateByPrimaryKey(u);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+7
-22
@@ -5,10 +5,8 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.business.domain.BizOrg;
|
||||
import com.ruoyi.business.domain.BizProject;
|
||||
import com.ruoyi.business.domain.BizProjectAssign;
|
||||
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||
import com.ruoyi.business.mapper.BizProjectAssignMapper;
|
||||
import com.ruoyi.business.mapper.BizProjectMapper;
|
||||
import com.ruoyi.business.service.IBizProjectAssignService;
|
||||
@@ -22,8 +20,6 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService
|
||||
@Autowired
|
||||
private BizProjectAssignMapper bizProjectAssignMapper;
|
||||
@Autowired
|
||||
private BizOrgMapper bizOrgMapper;
|
||||
@Autowired
|
||||
private BizProjectMapper bizProjectMapper;
|
||||
|
||||
@Override
|
||||
@@ -37,30 +33,19 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService
|
||||
|
||||
/**
|
||||
* 插入项目执行方分配:
|
||||
* 1. execUserId → biz_org(org_type='executor') 反查 org_id 写入 executionUnitId (NOT NULL, 找不到 throw)
|
||||
* 1. executionUnitId (前端必传, 单一可信源 biz_org.org_id)
|
||||
* 2. 自动补 audit 字段 (createBy/createTime/updateBy/updateTime) 从 SecurityUtils
|
||||
* 3. SnowflakeId 主键
|
||||
* 4. status='0' 默认
|
||||
*/
|
||||
@Override
|
||||
public int insert(BizProjectAssign entity) {
|
||||
// 0. 必须有 execUserId (前端必传)
|
||||
if (entity.getExecUserId() == null) {
|
||||
throw new ServiceException("执行方用户ID不能为空");
|
||||
// 0. 必须有 executionUnitId (前端必传)
|
||||
if (entity.getExecutionUnitId() == null) {
|
||||
throw new ServiceException("执行单位ID不能为空");
|
||||
}
|
||||
|
||||
// 1. execUserId → biz_org(executor) → executionUnitId
|
||||
BizOrg q = new BizOrg();
|
||||
q.setUserId(entity.getExecUserId());
|
||||
q.setOrgType("executor");
|
||||
List<BizOrg> matched = bizOrgMapper.selectList(q);
|
||||
if (matched == null || matched.isEmpty()) {
|
||||
throw new ServiceException("执行方用户 " + entity.getExecUserId() + " 未关联执行单位 (请检查 biz_org.org_type='executor' + user_id 是否绑定)");
|
||||
}
|
||||
BizOrg org = matched.get(0);
|
||||
entity.setExecutionUnitId(org.getOrgId());
|
||||
|
||||
// 2. audit 字段
|
||||
// 1. audit 字段
|
||||
Date now = new Date();
|
||||
String operator = SecurityUtils.getUsername();
|
||||
if (entity.getCreateBy() == null || entity.getCreateBy().isEmpty()) entity.setCreateBy(operator);
|
||||
@@ -68,10 +53,10 @@ public class BizProjectAssignServiceImpl implements IBizProjectAssignService
|
||||
entity.setUpdateBy(operator);
|
||||
entity.setUpdateTime(now);
|
||||
|
||||
// 3. 默认值
|
||||
// 2. 默认值
|
||||
if (entity.getStatus() == null || entity.getStatus().isEmpty()) entity.setStatus("0");
|
||||
|
||||
// 4. SnowflakeId
|
||||
// 3. SnowflakeId
|
||||
SnowflakeId.injectIfEmpty(entity, "assignId");
|
||||
|
||||
return bizProjectAssignMapper.insert(entity);
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public class BizProjectExecutorAssignServiceImpl implements IBizProjectExecutorA
|
||||
item.setAssignDesc(body.getAssignDesc());
|
||||
item.setAssignPoints(body.getAssignPoints());
|
||||
item.setCreateBy(body.getCreateBy());
|
||||
item.setExecutorUserId(body.getExecutorUserId());
|
||||
item.setExecutorOrgId(body.getExecutorOrgId());
|
||||
inserted += mapper.insertAssign(item);
|
||||
}
|
||||
return inserted;
|
||||
|
||||
+12
@@ -138,4 +138,16 @@ public class BizProjectServiceImpl implements IBizProjectService
|
||||
if (projectId == null) return;
|
||||
bizProjectMapper.recomputeSettledAmounts(projectId);
|
||||
}
|
||||
|
||||
/** 删除公告: 将 3 个公示 URL 置 NULL (未发布) */
|
||||
@Override
|
||||
public int clearAnnouncement(Long projectId) {
|
||||
return bizProjectMapper.clearAnnouncement(projectId);
|
||||
}
|
||||
|
||||
/** 开通到期回收 */
|
||||
@Override
|
||||
public int closeExpiredOpenStatus() {
|
||||
return bizProjectMapper.closeExpiredOpenStatus();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public class BizProjectSponsorAssignServiceImpl implements IBizProjectSponsorAss
|
||||
item.setAssignDesc(body.getAssignDesc());
|
||||
item.setAssignPoints(body.getAssignPoints());
|
||||
item.setCreateBy(body.getCreateBy());
|
||||
item.setSponsorUserId(body.getSponsorUserId());
|
||||
item.setSponsorOrgId(body.getSponsorOrgId());
|
||||
inserted += mapper.insertAssign(item);
|
||||
}
|
||||
return inserted;
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
<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
|
||||
from biz_meeting_attendee where meeting_id = #{meetingId} and is_deleted = 0
|
||||
order by id
|
||||
</select>
|
||||
<select id="selectByUserId" 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, create_by, create_time, is_deleted, is_esigned, is_invited
|
||||
|
||||
+15
-17
@@ -5,14 +5,15 @@
|
||||
<resultMap type="BizMeetingExecutor" id="BizMeetingExecutorResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="meetingId" column="meeting_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="executorOrgId" column="executor_org_id" />
|
||||
<result property="orgName" column="org_name" />
|
||||
<result property="assignedBy" column="assigned_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="isDeleted" column="is_deleted" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, meeting_id, user_id, assigned_by, create_time, is_deleted
|
||||
select id, meeting_id, executor_org_id, assigned_by, create_time, is_deleted
|
||||
from biz_meeting_executor
|
||||
</sql>
|
||||
|
||||
@@ -22,15 +23,12 @@
|
||||
</select>
|
||||
|
||||
<select id="selectByMeetingId" resultMap="BizMeetingExecutorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where meeting_id = #{meetingId} and is_deleted = 0
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectByUserId" resultMap="BizMeetingExecutorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where user_id = #{userId} and is_deleted = 0
|
||||
order by id desc
|
||||
select e.id, e.meeting_id, e.executor_org_id, e.assigned_by, e.create_time, e.is_deleted,
|
||||
o.org_name
|
||||
from biz_meeting_executor e
|
||||
left join biz_org o on o.org_id = e.executor_org_id
|
||||
where e.meeting_id = #{meetingId} and e.is_deleted = 0
|
||||
order by e.id asc
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizMeetingExecutorResult" parameterType="BizMeetingExecutor">
|
||||
@@ -38,7 +36,7 @@
|
||||
<where>
|
||||
is_deleted = 0
|
||||
<if test="meetingId != null">and meeting_id = #{meetingId}</if>
|
||||
<if test="userId != null">and user_id = #{userId}</if>
|
||||
<if test="executorOrgId != null">and executor_org_id = #{executorOrgId}</if>
|
||||
</where>
|
||||
order by id asc
|
||||
</select>
|
||||
@@ -47,30 +45,30 @@
|
||||
insert into biz_meeting_executor
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id,</if>
|
||||
<if test="userId != null">user_id,</if>
|
||||
<if test="executorOrgId != null">executor_org_id,</if>
|
||||
<if test="assignedBy != null">assigned_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">#{meetingId},</if>
|
||||
<if test="userId != null">#{userId},</if>
|
||||
<if test="executorOrgId != null">#{executorOrgId},</if>
|
||||
<if test="assignedBy != null">#{assignedBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List">
|
||||
insert into biz_meeting_executor (meeting_id, user_id, assigned_by, create_time)
|
||||
insert into biz_meeting_executor (meeting_id, executor_org_id, assigned_by, create_time)
|
||||
values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.meetingId}, #{item.userId}, #{item.assignedBy}, #{item.createTime})
|
||||
(#{item.meetingId}, #{item.executorOrgId}, #{item.assignedBy}, #{item.createTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="BizMeetingExecutor">
|
||||
update biz_meeting_executor
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="userId != null">user_id = #{userId},</if>
|
||||
<if test="executorOrgId != null">executor_org_id = #{executorOrgId},</if>
|
||||
<if test="assignedBy != null">assigned_by = #{assignedBy},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
<result property="supervisionBy" column="supervision_by" />
|
||||
<result property="supervisionTime" column="supervision_time" />
|
||||
<result property="materialAuditStage" column="material_audit_stage" />
|
||||
<result property="voucherAuditStage" column="voucher_audit_stage" />
|
||||
<result property="isExecuted" column="is_executed" />
|
||||
<result property="executeTime" column="execute_time" />
|
||||
<result property="isSettled" column="is_settled" />
|
||||
@@ -30,10 +29,10 @@
|
||||
<result property="isFrozen" column="is_frozen" />
|
||||
<result property="freezeTime" column="freeze_time" />
|
||||
<result property="materialAuditTime" column="material_audit_time" />
|
||||
<result property="voucherAuditTime" column="voucher_audit_time" />
|
||||
<result property="materialComplianceApproved" column="material_compliance_approved" />
|
||||
<result property="voucherComplianceApproved" column="voucher_compliance_approved" />
|
||||
<result property="submitDeadline" column="submit_deadline" />
|
||||
<result property="invitationUrl" column="invitation_url" />
|
||||
<result property="projectInvitationUrl" column="project_invitation_url" />
|
||||
<result property="scheduleUrl" column="schedule_url" />
|
||||
<result property="posterUrl" column="poster_url" />
|
||||
<result property="laborSigned" column="labor_signed" />
|
||||
@@ -50,10 +49,12 @@
|
||||
<result property="isDeleted" column="is_deleted" />
|
||||
</resultMap>
|
||||
<sql id="selectFields">
|
||||
meeting_id, business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, org_name, address, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, voucher_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, voucher_audit_time, material_compliance_approved, voucher_compliance_approved, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted
|
||||
meeting_id, business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, address, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, material_compliance_approved, submit_deadline, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted
|
||||
</sql>
|
||||
<select id="selectByPrimaryKey" resultMap="BizMeetingResult" parameterType="Long">
|
||||
select <include refid="selectFields"/>
|
||||
select
|
||||
(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,
|
||||
<include refid="selectFields"/>
|
||||
from biz_meeting
|
||||
where meeting_id = #{meetingId} and is_deleted = 0
|
||||
</select>
|
||||
@@ -61,6 +62,8 @@
|
||||
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,
|
||||
<include refid="selectFields"/>
|
||||
from biz_meeting
|
||||
<where>
|
||||
@@ -75,16 +78,24 @@
|
||||
<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_admin_user_id, SUB 走 sponsor_assign.monitor_user_id.
|
||||
<!-- 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_admin_user_id = #{params.sponsorAdminUserId} and is_deleted = 0)</if>
|
||||
<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>
|
||||
<!-- executor 数据权限: 只看"我的项目"下的会议. MAIN 走 biz_project_assign (exec_user_id / execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id -->
|
||||
<!-- 结题且未开通的项目, sponsor 会议不可见 (与项目列表 selectSponsorList 同口径) -->
|
||||
<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>
|
||||
<!-- executor 数据权限: 只看"我的项目"下的会议. MAIN 走 biz_project_assign (execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id -->
|
||||
<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.exec_user_id = #{params.executorUserId}
|
||||
or a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))
|
||||
and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')
|
||||
)</if>
|
||||
<if test="params.executorStaffUserId != null">and project_id in (
|
||||
select distinct a.project_id from biz_project_executor_assign a
|
||||
@@ -107,14 +118,13 @@
|
||||
<if test="projectForm != null and projectForm != ''">project_form,</if>
|
||||
<if test="startTime != null">start_time,</if>
|
||||
<if test="endTime != null">end_time,</if>
|
||||
<if test="orgName != null and orgName != ''">org_name,</if>
|
||||
<if test="address != null and address != ''">address,</if>
|
||||
<if test="currentStage != null and currentStage != ''">current_stage,</if>
|
||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion,</if>
|
||||
<if test="supervisionBy != null and supervisionBy != ''">supervision_by,</if>
|
||||
<if test="supervisionTime != null">supervision_time,</if>
|
||||
<if test="materialAuditStage != null and materialAuditStage != ''">material_audit_stage,</if>
|
||||
<if test="voucherAuditStage != null and voucherAuditStage != ''">voucher_audit_stage,</if>
|
||||
<if test="submitDeadline != null">submit_deadline,</if>
|
||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url,</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url,</if>
|
||||
<if test="posterUrl != null and posterUrl != ''">poster_url,</if>
|
||||
@@ -136,14 +146,13 @@
|
||||
<if test="projectForm != null and projectForm != ''">#{projectForm},</if>
|
||||
<if test="startTime != null">#{startTime},</if>
|
||||
<if test="endTime != null">#{endTime},</if>
|
||||
<if test="orgName != null and orgName != ''">#{orgName},</if>
|
||||
<if test="address != null and address != ''">#{address},</if>
|
||||
<if test="currentStage != null and currentStage != ''">#{currentStage},</if>
|
||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">#{supervisionOpinion},</if>
|
||||
<if test="supervisionBy != null and supervisionBy != ''">#{supervisionBy},</if>
|
||||
<if test="supervisionTime != null">#{supervisionTime},</if>
|
||||
<if test="materialAuditStage != null and materialAuditStage != ''">#{materialAuditStage},</if>
|
||||
<if test="voucherAuditStage != null and voucherAuditStage != ''">#{voucherAuditStage},</if>
|
||||
<if test="submitDeadline != null">#{submitDeadline},</if>
|
||||
<if test="invitationUrl != null and invitationUrl != ''">#{invitationUrl},</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">#{scheduleUrl},</if>
|
||||
<if test="posterUrl != null and posterUrl != ''">#{posterUrl},</if>
|
||||
@@ -168,14 +177,12 @@
|
||||
<!-- Date/Long 字段不能用 != '' (OGNL 会把 Date 和 String 做非法比较), 只判 null -->
|
||||
<if test="startTime != null">start_time = #{startTime},</if>
|
||||
<if test="endTime != null">end_time = #{endTime},</if>
|
||||
<if test="orgName != null and orgName != ''">org_name = #{orgName},</if>
|
||||
<if test="address != null and address != ''">address = #{address},</if>
|
||||
<if test="currentStage != null and currentStage != ''">current_stage = #{currentStage},</if>
|
||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion = #{supervisionOpinion},</if>
|
||||
<if test="supervisionBy != null and supervisionBy != ''">supervision_by = #{supervisionBy},</if>
|
||||
<if test="supervisionTime != null">supervision_time = #{supervisionTime},</if>
|
||||
<if test="materialAuditStage != null and materialAuditStage != ''">material_audit_stage = #{materialAuditStage},</if>
|
||||
<if test="voucherAuditStage != null and voucherAuditStage != ''">voucher_audit_stage = #{voucherAuditStage},</if>
|
||||
<if test="isExecuted != null">is_executed = #{isExecuted},</if>
|
||||
<if test="executeTime != null">execute_time = #{executeTime},</if>
|
||||
<if test="isSettled != null">is_settled = #{isSettled},</if>
|
||||
@@ -185,9 +192,8 @@
|
||||
<if test="isFrozen != null">is_frozen = #{isFrozen},</if>
|
||||
<if test="freezeTime != null">freeze_time = #{freezeTime},</if>
|
||||
<if test="materialAuditTime != null">material_audit_time = #{materialAuditTime},</if>
|
||||
<if test="voucherAuditTime != null">voucher_audit_time = #{voucherAuditTime},</if>
|
||||
<if test="materialComplianceApproved != null">material_compliance_approved = #{materialComplianceApproved},</if>
|
||||
<if test="voucherComplianceApproved != null">voucher_compliance_approved = #{voucherComplianceApproved},</if>
|
||||
<if test="submitDeadline != null">submit_deadline = #{submitDeadline},</if>
|
||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url = #{invitationUrl},</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url = #{scheduleUrl},</if>
|
||||
<if test="posterUrl != null and posterUrl != ''">poster_url = #{posterUrl},</if>
|
||||
@@ -231,21 +237,20 @@
|
||||
and start_time <= NOW()
|
||||
and material_audit_stage = 'NOT_SUBMITTED'
|
||||
</update>
|
||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): material 未提交 且 end_time + submit_deadline_days 已过 → 冻结 -->
|
||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): material 未提交/已退回 且 submit_deadline 已过 → 冻结.
|
||||
submit_deadline 由建会/退回/解冻 时写入 (end_time 或 now + 项目 submit_deadline_days 天), null = 项目未设天数 → 永不冻结. -->
|
||||
<update id="markFrozen">
|
||||
update biz_meeting m
|
||||
join biz_project p on p.project_id = m.project_id and p.is_deleted = 0
|
||||
set m.is_frozen = 1,
|
||||
m.freeze_time = NOW(),
|
||||
m.current_stage = 'FROZEN'
|
||||
where m.is_deleted = 0
|
||||
and m.is_frozen = 0
|
||||
and m.material_audit_stage = 'NOT_SUBMITTED'
|
||||
and m.end_time is not null
|
||||
and p.submit_deadline_days is not null
|
||||
and date_add(m.end_time, interval p.submit_deadline_days day) <= NOW()
|
||||
update biz_meeting
|
||||
set is_frozen = 1,
|
||||
freeze_time = NOW(),
|
||||
current_stage = 'FROZEN'
|
||||
where is_deleted = 0
|
||||
and is_frozen = 0
|
||||
and material_audit_stage in ('NOT_SUBMITTED', 'REJECTED')
|
||||
and submit_deadline is not null
|
||||
and submit_deadline <= NOW()
|
||||
</update>
|
||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): material+voucher 都 APPROVED 且 最晚审核时间已过 1 自然日 → AWAITING_SETTLEMENT (待结算 24h 慢路径) -->
|
||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): 材料 APPROVED 且 材料审核时间已过 1 自然日 → AWAITING_SETTLEMENT (待结算 24h 慢路径) -->
|
||||
<update id="markSettlementReady">
|
||||
update biz_meeting
|
||||
set current_stage = 'AWAITING_SETTLEMENT'
|
||||
@@ -254,10 +259,9 @@
|
||||
and is_settled = 0
|
||||
and is_finished = 0
|
||||
and material_audit_stage = 'APPROVED'
|
||||
and voucher_audit_stage = 'APPROVED'
|
||||
and current_stage = 'SUPERVISION_APPROVED'
|
||||
and greatest(material_audit_time, voucher_audit_time) is not null
|
||||
and greatest(material_audit_time, voucher_audit_time) <= (NOW() - INTERVAL 1 DAY)
|
||||
and material_audit_time is not null
|
||||
and material_audit_time <= (NOW() - INTERVAL 1 DAY)
|
||||
</update>
|
||||
<!-- 费用汇总调度器: 查 fee_calc_status=0 且未软删的会议 id -->
|
||||
<select id="selectPendingFeeCalcIds" resultType="Long">
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectByMeetingAndSubType" resultMap="BizMeetingMaterialResult">
|
||||
<include refid="selectFields"/>
|
||||
where meeting_id = #{meetingId} and sub_type = #{subType} and is_deleted = 0
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="BizMeetingMaterial" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into biz_meeting_material
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
@@ -89,6 +95,13 @@
|
||||
update biz_meeting_material set fee_status = #{feeStatus} where id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- 会务材料"打包上传"回填: 替换文件 URL/文件名, 清空金额(待重 OCR) + 置 fee_status -->
|
||||
<update id="updateFile">
|
||||
update biz_meeting_material
|
||||
set oss_url = #{ossUrl}, file_name = #{fileName}, amount = null, fee_status = #{feeStatus}
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||
delete from biz_meeting_material where id = #{id}
|
||||
</delete>
|
||||
|
||||
+15
-17
@@ -5,14 +5,15 @@
|
||||
<resultMap type="BizMeetingSupervisor" id="BizMeetingSupervisorResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="meetingId" column="meeting_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="sponsorOrgId" column="sponsor_org_id" />
|
||||
<result property="orgName" column="org_name" />
|
||||
<result property="assignedBy" column="assigned_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="isDeleted" column="is_deleted" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, meeting_id, user_id, assigned_by, create_time, is_deleted
|
||||
select id, meeting_id, sponsor_org_id, assigned_by, create_time, is_deleted
|
||||
from biz_meeting_supervisor
|
||||
</sql>
|
||||
|
||||
@@ -22,15 +23,12 @@
|
||||
</select>
|
||||
|
||||
<select id="selectByMeetingId" resultMap="BizMeetingSupervisorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where meeting_id = #{meetingId} and is_deleted = 0
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectByUserId" resultMap="BizMeetingSupervisorResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where user_id = #{userId} and is_deleted = 0
|
||||
order by id desc
|
||||
select s.id, s.meeting_id, s.sponsor_org_id, s.assigned_by, s.create_time, s.is_deleted,
|
||||
o.org_name
|
||||
from biz_meeting_supervisor s
|
||||
left join biz_org o on o.org_id = s.sponsor_org_id
|
||||
where s.meeting_id = #{meetingId} and s.is_deleted = 0
|
||||
order by s.id asc
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizMeetingSupervisorResult" parameterType="BizMeetingSupervisor">
|
||||
@@ -38,7 +36,7 @@
|
||||
<where>
|
||||
is_deleted = 0
|
||||
<if test="meetingId != null">and meeting_id = #{meetingId}</if>
|
||||
<if test="userId != null">and user_id = #{userId}</if>
|
||||
<if test="sponsorOrgId != null">and sponsor_org_id = #{sponsorOrgId}</if>
|
||||
</where>
|
||||
order by id asc
|
||||
</select>
|
||||
@@ -47,30 +45,30 @@
|
||||
insert into biz_meeting_supervisor
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">meeting_id,</if>
|
||||
<if test="userId != null">user_id,</if>
|
||||
<if test="sponsorOrgId != null">sponsor_org_id,</if>
|
||||
<if test="assignedBy != null">assigned_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="meetingId != null">#{meetingId},</if>
|
||||
<if test="userId != null">#{userId},</if>
|
||||
<if test="sponsorOrgId != null">#{sponsorOrgId},</if>
|
||||
<if test="assignedBy != null">#{assignedBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List">
|
||||
insert into biz_meeting_supervisor (meeting_id, user_id, assigned_by, create_time)
|
||||
insert into biz_meeting_supervisor (meeting_id, sponsor_org_id, assigned_by, create_time)
|
||||
values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.meetingId}, #{item.userId}, #{item.assignedBy}, #{item.createTime})
|
||||
(#{item.meetingId}, #{item.sponsorOrgId}, #{item.assignedBy}, #{item.createTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<update id="updateByPrimaryKey" parameterType="BizMeetingSupervisor">
|
||||
update biz_meeting_supervisor
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="userId != null">user_id = #{userId},</if>
|
||||
<if test="sponsorOrgId != null">sponsor_org_id = #{sponsorOrgId},</if>
|
||||
<if test="assignedBy != null">assigned_by = #{assignedBy},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
|
||||
@@ -103,20 +103,20 @@
|
||||
|
||||
<!--
|
||||
支持方下拉选项: JOIN sys_user 取主账号 user_name (供分配弹窗缓存 biz_project.sponsor_admin_user_name 用)
|
||||
返回 Map: userId / orgName / userName
|
||||
返回 Map: orgId / orgName / userName
|
||||
EXPLAIN 实际命中: idx_org_type_status (org_type,status) (优化器在数据量小时倾向小索引),
|
||||
org_name LIKE 走 Using where 二次过滤, ORDER BY org_id DESC 走 filesort
|
||||
过滤条件: orgName 模糊匹配 (主用) / userId 精确匹配 (拉回已选项)
|
||||
过滤条件: orgName 模糊匹配 (主用) / orgId 精确匹配 (拉回已选项)
|
||||
-->
|
||||
<select id="selectSponsorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
|
||||
select o.user_id as userId,
|
||||
select o.org_id as orgId,
|
||||
o.org_name as orgName,
|
||||
u.user_name as userName
|
||||
from biz_org o
|
||||
join sys_user u on u.user_id = o.user_id
|
||||
where o.org_type = 'sponsor'
|
||||
and u.del_flag = '0'
|
||||
<if test="userId != null">and o.user_id = #{userId}</if>
|
||||
<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
|
||||
@@ -124,16 +124,16 @@
|
||||
|
||||
<!--
|
||||
执行方下拉选项: JOIN sys_user 取 MAIN 账号 user_name (parent_user_id IS NULL 限定主账号)
|
||||
返回 Map: userId / orgName / userName
|
||||
返回 Map: orgId / orgName / userName
|
||||
EXPLAIN 实际命中: idx_org_type_status (org_type,status) (优化器在数据量小时倾向小索引),
|
||||
sys_user 走 PRIMARY eq_ref, parent_user_id IS NULL 走 Using where
|
||||
关键: 只查 MAIN 账号 (parent_user_id IS NULL), 过滤掉同一公司下的普通员工子账号
|
||||
value=userId (MAIN 账号 sys_user.user_id, 直接写 biz_project_assign.exec_user_id)
|
||||
value=orgId (biz_org.org_id, 直接写 biz_project_assign.execution_unit_id)
|
||||
label=orgName (执行单位名称, 不带 user_name 避免人名/昵称混淆)
|
||||
过滤条件: orgName 模糊匹配 (主用) / userId 精确匹配 (拉回已选项)
|
||||
过滤条件: orgName 模糊匹配 (主用) / orgId 精确匹配 (拉回已选项)
|
||||
-->
|
||||
<select id="selectExecutorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
|
||||
select o.user_id as userId,
|
||||
select o.org_id as orgId,
|
||||
o.org_name as orgName,
|
||||
u.user_name as userName
|
||||
from biz_org o
|
||||
@@ -141,12 +141,30 @@
|
||||
where o.org_type = 'executor'
|
||||
and u.del_flag = '0'
|
||||
and u.parent_user_id is null
|
||||
<if test="userId != null">and o.user_id = #{userId}</if>
|
||||
<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
|
||||
</select>
|
||||
|
||||
<!--
|
||||
支持方注册下拉选项 (匿名公开, register-sponsor 选企业注册 SUB 子账号)
|
||||
与 selectSponsorOrgOptions 的区别: 不 JOIN sys_user → 无主账号 (user_id IS NULL) 的 org 也能被选到
|
||||
返回 Map: orgId / orgName / mainUserId (= biz_org.user_id, 可为 null)
|
||||
mainUserId 用于 registerSponsor 判断: 有主账号 → SUB 绑 parent_user_id; 无主账号 → parent_user_id 留空待 admin/manager 分配
|
||||
-->
|
||||
<select id="selectSponsorRegisterOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
|
||||
select o.org_id as orgId,
|
||||
o.org_name as orgName,
|
||||
o.user_id as mainUserId
|
||||
from biz_org o
|
||||
where o.org_type = 'sponsor'
|
||||
and o.status = '0'
|
||||
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
order by o.org_id desc
|
||||
limit 20
|
||||
</select>
|
||||
|
||||
<!--
|
||||
当前登录 sponsor 的所属公司 (供 /sponsor/account 页面回显 + 主账号改名)
|
||||
主账号 (sys_user.parent_user_id IS NULL): 用 own (biz_org.user_id = #{userId})
|
||||
@@ -165,4 +183,17 @@
|
||||
where u.user_id = #{userId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!--
|
||||
user_id → org_id 单一可信源反查:
|
||||
MAIN 主账号: biz_org.user_id = #{userId} (企业表直接关联主账号)
|
||||
SUB 子账号: biz_person.org_id (person 表 user_id = #{userId} 反查企业)
|
||||
COALESCE 二选一, 都没有返回 NULL
|
||||
-->
|
||||
<select id="selectOrgIdByUserId" parameterType="Long" resultType="Long">
|
||||
select coalesce(
|
||||
(select org_id from biz_org where user_id = #{userId} limit 1),
|
||||
(select org_id from biz_person where user_id = #{userId} limit 1)
|
||||
)
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
<result property="orgType" column="org_type" />
|
||||
<result property="department" column="department" />
|
||||
<result property="position" column="position" />
|
||||
<result property="role" column="role" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="unitType" column="unit_type" />
|
||||
<result property="accountType" column="account_type" />
|
||||
<result property="parentUserId" column="parent_user_id" />
|
||||
<result property="account" column="user_name" />
|
||||
<result property="email" column="email" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
@@ -23,17 +23,18 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select person_id, name, phone, org_id, department, position, role, unit_type, user_id, create_by, create_time, update_by, update_time
|
||||
select person_id, name, phone, org_id, department, position, unit_type, user_id, create_by, create_time, update_by, update_time
|
||||
from biz_person
|
||||
</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.role, p.unit_type, p.user_id,
|
||||
p.department, p.position, p.unit_type, p.user_id,
|
||||
p.create_by, p.create_time, p.update_by, p.update_time,
|
||||
u.account_type, u.parent_user_id, u.status, u.del_flag as user_del_flag,
|
||||
u.user_name as user_name
|
||||
u.user_name as user_name,
|
||||
u.email as email
|
||||
from biz_person p
|
||||
left join biz_org o on p.org_id = o.org_id
|
||||
left join sys_user u on p.user_id = u.user_id
|
||||
@@ -51,12 +52,12 @@
|
||||
<if test="unitType != null and unitType != ''"> and p.unit_type = #{unitType}</if>
|
||||
<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="email != null and email != ''"> and u.email = #{email}</if>
|
||||
<if test="orgId != null"> and p.org_id = #{orgId}</if>
|
||||
<if test="orgName != null and orgName != ''"> and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
<if test="orgType != null and orgType != ''"> and o.org_type = #{orgType}</if>
|
||||
<if test="department != null and department != ''"> and p.department = #{department}</if>
|
||||
<if test="position != null and position != ''"> and p.position = #{position}</if>
|
||||
<if test="role != null and role != ''"> and p.role = #{role}</if>
|
||||
<if test="status != null and status != ''"> and u.status = #{status}</if>
|
||||
<if test="parentUserId != null"> and u.parent_user_id = #{parentUserId}</if>
|
||||
<if test="userId != null"> and p.user_id = #{userId}</if>
|
||||
@@ -80,7 +81,6 @@
|
||||
<if test="orgId != null">org_id,</if>
|
||||
<if test="department != null">department,</if>
|
||||
<if test="position != null">position,</if>
|
||||
<if test="role != null">role,</if>
|
||||
<if test="unitType != null">unit_type,</if>
|
||||
<if test="userId != null">user_id,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
@@ -95,7 +95,6 @@
|
||||
<if test="orgId != null">#{orgId},</if>
|
||||
<if test="department != null">#{department},</if>
|
||||
<if test="position != null">#{position},</if>
|
||||
<if test="role != null">#{role},</if>
|
||||
<if test="unitType != null">#{unitType},</if>
|
||||
<if test="userId != null">#{userId},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
@@ -115,7 +114,6 @@
|
||||
<if test="phone != null">phone = #{phone},</if>
|
||||
<if test="department != null">department = #{department},</if>
|
||||
<if test="position != null">position = #{position},</if>
|
||||
<if test="role != null">role = #{role},</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
update_time = sysdate(),
|
||||
</trim>
|
||||
@@ -162,7 +160,6 @@
|
||||
<if test="orgName != null and orgName != ''"> and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
<if test="department != null and department != ''"> and p.department = #{department}</if>
|
||||
<if test="position != null and position != ''"> and p.position = #{position}</if>
|
||||
<if test="role != null and role != ''"> and p.role = #{role}</if>
|
||||
<if test="status != null and status != ''"> and u.status = #{status}</if>
|
||||
</where>
|
||||
order by p.person_id desc
|
||||
@@ -183,16 +180,9 @@
|
||||
<if test="orgName != null and orgName != ''"> and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
<if test="department != null and department != ''"> and p.department = #{department}</if>
|
||||
<if test="position != null and position != ''"> and p.position = #{position}</if>
|
||||
<if test="role != null and role != ''"> and p.role = #{role}</if>
|
||||
<if test="status != null and status != ''"> and u.status = #{status}</if>
|
||||
</where>
|
||||
order by p.person_id desc
|
||||
</select>
|
||||
|
||||
<!-- 校验某 org 下是否已有 admin 角色 (主账号自带 biz_person.role='admin', 每公司唯一) -->
|
||||
<select id="countAdminByOrgId" resultType="int">
|
||||
select count(*) from biz_person p
|
||||
left join sys_user u on p.user_id = u.user_id
|
||||
where p.org_id = #{orgId} and p.role = 'admin' and u.del_flag = '0'
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<id property="assignId" column="assign_id" />
|
||||
<result property="projectId" column="project_id" />
|
||||
<result property="executionUnitId" column="execution_unit_id" />
|
||||
<result property="execUserId" column="exec_user_id" />
|
||||
<result property="sessions" column="sessions" />
|
||||
<result property="amount" column="amount" />
|
||||
<result property="remark" column="remark" />
|
||||
@@ -18,7 +17,7 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select assign_id, project_id, execution_unit_id, exec_user_id,
|
||||
select assign_id, project_id, execution_unit_id,
|
||||
sessions, amount, remark, status,
|
||||
create_by, create_time, update_by, update_time, is_deleted
|
||||
from biz_project_assign
|
||||
@@ -41,7 +40,6 @@
|
||||
is_deleted = 0
|
||||
<if test="projectId != null"> and project_id = #{projectId}</if>
|
||||
<if test="executionUnitId != null"> and execution_unit_id = #{executionUnitId}</if>
|
||||
<if test="execUserId != null"> and exec_user_id = #{execUserId}</if>
|
||||
<if test="status != null and status != ''"> and status = #{status}</if>
|
||||
</where>
|
||||
order by assign_id desc
|
||||
@@ -57,7 +55,6 @@
|
||||
<if test="assignId != null and assignId != ''">assign_id,</if>
|
||||
<if test="projectId != null">project_id,</if>
|
||||
<if test="executionUnitId != null">execution_unit_id,</if>
|
||||
<if test="execUserId != null">exec_user_id,</if>
|
||||
<if test="sessions != null">sessions,</if>
|
||||
<if test="amount != null">amount,</if>
|
||||
<if test="remark != null and remark != ''">remark,</if>
|
||||
@@ -71,7 +68,6 @@
|
||||
<if test="assignId != null and assignId != ''">#{assignId},</if>
|
||||
<if test="projectId != null">#{projectId},</if>
|
||||
<if test="executionUnitId != null">#{executionUnitId},</if>
|
||||
<if test="execUserId != null">#{execUserId},</if>
|
||||
<if test="sessions != null">#{sessions},</if>
|
||||
<if test="amount != null">#{amount},</if>
|
||||
<if test="remark != null and remark != ''">#{remark},</if>
|
||||
@@ -87,7 +83,6 @@
|
||||
update biz_project_assign
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="executionUnitId != null">execution_unit_id = #{executionUnitId},</if>
|
||||
<if test="execUserId != null">exec_user_id = #{execUserId},</if>
|
||||
<if test="sessions != null">sessions = #{sessions},</if>
|
||||
<if test="amount != null">amount = #{amount},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
|
||||
+5
-4
@@ -5,7 +5,7 @@
|
||||
<resultMap id="BaseResultMap" type="BizProjectExecutorAssign">
|
||||
<id property="id" column="id" />
|
||||
<result property="projectId" column="project_id" />
|
||||
<result property="executorUserId" column="executor_user_id" />
|
||||
<result property="executorOrgId" column="executor_org_id" />
|
||||
<result property="staffUserId" column="staff_user_id" />
|
||||
<result property="assignDesc" column="assign_desc" />
|
||||
<result property="assignPoints" column="assign_points" />
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
<insert id="insertAssign" parameterType="BizProjectExecutorAssign" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO biz_project_executor_assign
|
||||
(project_id, executor_user_id, staff_user_id, assign_desc, assign_points, create_by, create_time)
|
||||
(project_id, executor_org_id, staff_user_id, assign_desc, assign_points, create_by, create_time)
|
||||
VALUES
|
||||
(#{projectId}, #{executorUserId}, #{staffUserId}, #{assignDesc}, #{assignPoints}, #{createBy}, sysdate())
|
||||
(#{projectId}, #{executorOrgId}, #{staffUserId}, #{assignDesc}, #{assignPoints}, #{createBy}, sysdate())
|
||||
</insert>
|
||||
|
||||
<!-- 按 project_id 全删 (执行方分配策略: 先删后插) -->
|
||||
@@ -38,7 +38,8 @@
|
||||
e.user_name AS executor_user_name,
|
||||
s.user_name AS staff_user_name
|
||||
FROM biz_project_executor_assign a
|
||||
LEFT JOIN sys_user e ON a.executor_user_id = e.user_id
|
||||
LEFT JOIN biz_org o ON o.org_id = a.executor_org_id
|
||||
LEFT JOIN sys_user e ON e.user_id = o.user_id
|
||||
LEFT JOIN sys_user s ON a.staff_user_id = s.user_id
|
||||
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
||||
ORDER BY a.create_time DESC
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<result property="managerScore" column="manager_score" />
|
||||
<result property="sponsorScore" column="sponsor_score" />
|
||||
<result property="sponsorAdminUserName" column="sponsor_admin_user_name" />
|
||||
<result property="sponsorAdminUserId" column="sponsor_admin_user_id" />
|
||||
<result property="sponsorOrgId" column="sponsor_org_id" />
|
||||
<result property="leadUserId" column="lead_user_id" />
|
||||
<result property="leadUserName" column="lead_user_name" />
|
||||
<result property="isBidProject" column="is_bid_project" />
|
||||
@@ -43,11 +43,15 @@
|
||||
<result property="invitationUrl" column="invitation_url" />
|
||||
<result property="supportLetterUrl" column="support_letter_url" />
|
||||
<result property="publishUrl" column="publish_url" />
|
||||
<result property="scheduleUrl" column="schedule_url" />
|
||||
<result property="openDeadline" column="open_deadline" />
|
||||
<result property="openStatus" column="open_status" />
|
||||
<result property="isDeleted" column="is_deleted" />
|
||||
</resultMap>
|
||||
<sql id="selectFields">
|
||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, p.is_deleted,
|
||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted,
|
||||
o.org_name as sponsor_org_name,
|
||||
su.user_name as sponsor_admin_user_name,
|
||||
lu.user_name as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
@@ -55,7 +59,8 @@
|
||||
join biz_org o2 on o2.org_id = bpa.execution_unit_id and o2.org_type = 'executor'
|
||||
where bpa.project_id = p.project_id and bpa.is_deleted = 0) as exec_org_names
|
||||
from biz_project p
|
||||
left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor'
|
||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||
left join sys_user su on su.user_id = o.user_id
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
</sql>
|
||||
@@ -64,13 +69,14 @@
|
||||
<sql id="selectFieldsForSponsor">
|
||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions,
|
||||
p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount,
|
||||
p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled,
|
||||
p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project,
|
||||
p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled,
|
||||
p.sponsor_org_id, p.lead_user_id, p.is_bid_project,
|
||||
p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor,
|
||||
p.start_time, p.end_time, p.submit_deadline_days,
|
||||
p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url,
|
||||
p.publish_url, p.is_deleted,
|
||||
p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted,
|
||||
o.org_name as sponsor_org_name,
|
||||
su.user_name as sponsor_admin_user_name,
|
||||
lu.user_name as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
@@ -78,7 +84,8 @@
|
||||
join biz_org o2 on o2.org_id = bpa.execution_unit_id and o2.org_type = 'executor'
|
||||
where bpa.project_id = p.project_id and bpa.is_deleted = 0) as exec_org_names
|
||||
from biz_project p
|
||||
left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor'
|
||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||
left join sys_user su on su.user_id = o.user_id
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
</sql>
|
||||
@@ -91,6 +98,8 @@
|
||||
<include refid="selectFieldsForSponsor"/>
|
||||
<where>
|
||||
p.is_deleted = 0
|
||||
<!-- 结题(is_finished=1)且未开通(open_status=N)的项目, sponsor 不可见 -->
|
||||
and not (p.is_finished = '1' and p.open_status = 'N')
|
||||
<if test="params.projectIds != null and params.projectIds.size() > 0">
|
||||
and p.project_id in
|
||||
<foreach collection="params.projectIds" item="id" open="(" separator="," close=")">
|
||||
@@ -98,12 +107,12 @@
|
||||
</foreach>
|
||||
</if>
|
||||
<!--
|
||||
sponsor 视角: 可见项目 = MAIN/ADMIN 默认 (sponsor_admin_user_id) OR SUB/监察员 (sponsor_assign.monitor_user_id), 任一路径同时 UNION biz_publicity_support_intent
|
||||
sponsor 视角: 可见项目 = MAIN/ADMIN 默认 (sponsor_org_id, 经 user_id 反查 org_id) OR SUB/监察员 (sponsor_assign.monitor_user_id), 任一路径同时 UNION biz_publicity_support_intent
|
||||
业务: sponsor (无论 MAIN/SUB) 提交过支持意向的项目, 同样要在 /sponsor/my-projects 看到, 跟被分配监察员同等地位
|
||||
分页: PageHelper 加 LIMIT 到外层, 自动生成 COUNT(*) FROM biz_project WHERE ... — 子查询内 DISTINCT 避免重复 count
|
||||
-->
|
||||
<if test="params.sponsorAdminUserId != null">and (
|
||||
p.sponsor_admin_user_id = #{params.sponsorAdminUserId}
|
||||
p.sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor')
|
||||
or p.project_id in (
|
||||
select distinct i.project_id
|
||||
from biz_publicity_support_intent i
|
||||
@@ -140,8 +149,9 @@
|
||||
注: executor 角色无评分/聚合分需求, 复用 selectFields (含 sponsor_score / manager_score)
|
||||
-->
|
||||
<select id="selectExecutorList" resultMap="BizProjectResult" parameterType="BizProject">
|
||||
select distinct p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
||||
select distinct p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
||||
o.org_name as sponsor_org_name,
|
||||
su.user_name as sponsor_admin_user_name,
|
||||
lu.user_name as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
@@ -152,22 +162,20 @@
|
||||
from biz_project_assign bpa3
|
||||
where bpa3.project_id = p.project_id
|
||||
and bpa3.is_deleted = 0
|
||||
and (bpa3.exec_user_id = #{params.executorUserId}
|
||||
or bpa3.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_sessions,
|
||||
and bpa3.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as assigned_sessions,
|
||||
(select coalesce(sum(bpa4.amount), 0)
|
||||
from biz_project_assign bpa4
|
||||
where bpa4.project_id = p.project_id
|
||||
and bpa4.is_deleted = 0
|
||||
and (bpa4.exec_user_id = #{params.executorUserId}
|
||||
or bpa4.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_amount,
|
||||
and bpa4.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as assigned_amount,
|
||||
(select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0) as meeting_count
|
||||
from biz_project p
|
||||
<!-- 执行方专属 join: 把 executor 限定条件放进 ON (而不是 WHERE), 这样 join 只命中分给当前执行方的 assignment, 1 行/项目. SELECT DISTINCT 保留以防 LEFT JOIN 副作用 -->
|
||||
join biz_project_assign a on a.project_id = p.project_id
|
||||
and a.is_deleted = 0
|
||||
and (a.exec_user_id = #{params.executorUserId}
|
||||
or a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))
|
||||
left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor'
|
||||
and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')
|
||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||
left join sys_user su on su.user_id = o.user_id
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
<where>
|
||||
@@ -188,8 +196,9 @@
|
||||
场次/金额列 assigned_sessions/assigned_amount 按 params.executorUserId (主账号/本公司) 聚合 — 执行人看到的是公司数据, 不是个人数据
|
||||
-->
|
||||
<select id="selectExecutorStaffList" resultMap="BizProjectResult" parameterType="BizProject">
|
||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
||||
o.org_name as sponsor_org_name,
|
||||
su.user_name as sponsor_admin_user_name,
|
||||
lu.user_name as lead_user_name,
|
||||
bp.name as create_user_name,
|
||||
(select group_concat(distinct o2.org_name separator ',')
|
||||
@@ -200,17 +209,16 @@
|
||||
from biz_project_assign bpa3
|
||||
where bpa3.project_id = p.project_id
|
||||
and bpa3.is_deleted = 0
|
||||
and (bpa3.exec_user_id = #{params.executorUserId}
|
||||
or bpa3.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_sessions,
|
||||
and bpa3.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as assigned_sessions,
|
||||
(select coalesce(sum(bpa4.amount), 0)
|
||||
from biz_project_assign bpa4
|
||||
where bpa4.project_id = p.project_id
|
||||
and bpa4.is_deleted = 0
|
||||
and (bpa4.exec_user_id = #{params.executorUserId}
|
||||
or bpa4.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_amount,
|
||||
and bpa4.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as assigned_amount,
|
||||
(select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0) as meeting_count
|
||||
from biz_project p
|
||||
left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor'
|
||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||
left join sys_user su on su.user_id = o.user_id
|
||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||
left join biz_person bp on bp.user_id = p.create_user_id
|
||||
<where>
|
||||
@@ -248,25 +256,26 @@
|
||||
<if test="projectNo != null and projectNo != ''">and project_no like concat('%', #{projectNo}, '%')</if>
|
||||
<if test="projectName != null and projectName != ''">and project_name like concat('%', #{projectName}, '%')</if>
|
||||
<if test="projectForm != null and projectForm != ''">and project_form = #{projectForm}</if>
|
||||
<if test="sponsorAdminUserIds != null and sponsorAdminUserIds.size() > 0">
|
||||
and sponsor_admin_user_id in
|
||||
<foreach collection="sponsorAdminUserIds" item="id" open="(" separator="," close=")">
|
||||
<if test="sponsorOrgIds != null and sponsorOrgIds.size() > 0">
|
||||
and sponsor_org_id in
|
||||
<foreach collection="sponsorOrgIds" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="execAdminUserIds != null and execAdminUserIds.size() > 0">
|
||||
<if test="execOrgIds != null and execOrgIds.size() > 0">
|
||||
and exists (
|
||||
select 1 from biz_project_assign bpa
|
||||
where bpa.project_id = project_id
|
||||
and bpa.exec_user_id in
|
||||
<foreach collection="execAdminUserIds" item="id" open="(" separator="," close=")">
|
||||
where bpa.project_id = p.project_id
|
||||
and bpa.execution_unit_id in
|
||||
<foreach collection="execOrgIds" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
)
|
||||
</if>
|
||||
<if test="isFinished != null and isFinished != ''">and is_finished = #{isFinished}</if>
|
||||
<if test="isSettled != null and isSettled != ''">and is_settled = #{isSettled}</if>
|
||||
<if test="managerScore != null">and manager_score = #{managerScore}</if>
|
||||
<if test="startTime != null">and start_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and end_time <= #{endTime}</if>
|
||||
</where>
|
||||
order by project_id desc
|
||||
</select>
|
||||
@@ -285,11 +294,10 @@
|
||||
<if test="paidMeetingAmount != null">paid_meeting_amount,</if>
|
||||
<if test="managerScore != null">manager_score,</if>
|
||||
<if test="sponsorScore != null">sponsor_score,</if>
|
||||
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">sponsor_admin_user_name,</if>
|
||||
<if test="projectForm != null and projectForm != ''">project_form,</if>
|
||||
<if test="isFinished != null and isFinished != ''">is_finished,</if>
|
||||
<if test="isSettled != null and isSettled != ''">is_settled,</if>
|
||||
<if test="sponsorAdminUserId != null">sponsor_admin_user_id,</if>
|
||||
<if test="sponsorOrgId != null">sponsor_org_id,</if>
|
||||
<if test="leadUserId != null">lead_user_id,</if>
|
||||
<if test="isBidProject != null and isBidProject != ''">is_bid_project,</if>
|
||||
<if test="createBy != null">create_by,</if>
|
||||
@@ -307,6 +315,9 @@
|
||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url,</if>
|
||||
<if test="supportLetterUrl != null and supportLetterUrl != ''">support_letter_url,</if>
|
||||
<if test="publishUrl != null and publishUrl != ''">publish_url,</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url,</if>
|
||||
<if test="openDeadline != null">open_deadline,</if>
|
||||
<if test="openStatus != null and openStatus != ''">open_status,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="projectId != null and projectId != ''">#{projectId},</if>
|
||||
@@ -321,11 +332,10 @@
|
||||
<if test="paidMeetingAmount != null">#{paidMeetingAmount},</if>
|
||||
<if test="managerScore != null">#{managerScore},</if>
|
||||
<if test="sponsorScore != null">#{sponsorScore},</if>
|
||||
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">#{sponsorAdminUserName},</if>
|
||||
<if test="projectForm != null and projectForm != ''">#{projectForm},</if>
|
||||
<if test="isFinished != null and isFinished != ''">#{isFinished},</if>
|
||||
<if test="isSettled != null and isSettled != ''">#{isSettled},</if>
|
||||
<if test="sponsorAdminUserId != null">#{sponsorAdminUserId},</if>
|
||||
<if test="sponsorOrgId != null">#{sponsorOrgId},</if>
|
||||
<if test="leadUserId != null">#{leadUserId},</if>
|
||||
<if test="isBidProject != null and isBidProject != ''">#{isBidProject},</if>
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
@@ -343,6 +353,9 @@
|
||||
<if test="invitationUrl != null and invitationUrl != ''">#{invitationUrl},</if>
|
||||
<if test="supportLetterUrl != null and supportLetterUrl != ''">#{supportLetterUrl},</if>
|
||||
<if test="publishUrl != null and publishUrl != ''">#{publishUrl},</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">#{scheduleUrl},</if>
|
||||
<if test="openDeadline != null">#{openDeadline},</if>
|
||||
<if test="openStatus != null and openStatus != ''">#{openStatus},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
<update id="updateByPrimaryKey" parameterType="BizProject">
|
||||
@@ -358,6 +371,9 @@
|
||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url = #{invitationUrl},</if>
|
||||
<if test="supportLetterUrl != null and supportLetterUrl != ''">support_letter_url = #{supportLetterUrl},</if>
|
||||
<if test="publishUrl != null and publishUrl != ''">publish_url = #{publishUrl},</if>
|
||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url = #{scheduleUrl},</if>
|
||||
<if test="openDeadline != null">open_deadline = #{openDeadline},</if>
|
||||
<if test="openStatus != null and openStatus != ''">open_status = #{openStatus},</if>
|
||||
<if test="projectNo != null and projectNo != ''">project_no = #{projectNo},</if>
|
||||
<if test="projectName != null and projectName != ''">project_name = #{projectName},</if>
|
||||
<if test="totalSessions != null">total_sessions = #{totalSessions},</if>
|
||||
@@ -369,11 +385,10 @@
|
||||
<if test="paidMeetingAmount != null">paid_meeting_amount = #{paidMeetingAmount},</if>
|
||||
<if test="managerScore != null">manager_score = #{managerScore},</if>
|
||||
<if test="sponsorScore != null">sponsor_score = #{sponsorScore},</if>
|
||||
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">sponsor_admin_user_name = #{sponsorAdminUserName},</if>
|
||||
<if test="projectForm != null and projectForm != ''">project_form = #{projectForm},</if>
|
||||
<if test="isFinished != null and isFinished != ''">is_finished = #{isFinished},</if>
|
||||
<if test="isSettled != null and isSettled != ''">is_settled = #{isSettled},</if>
|
||||
<if test="sponsorAdminUserId != null">sponsor_admin_user_id = #{sponsorAdminUserId},</if>
|
||||
<if test="sponsorOrgId != null">sponsor_org_id = #{sponsorOrgId},</if>
|
||||
<if test="leadUserId != null">lead_user_id = #{leadUserId},</if>
|
||||
<if test="isBidProject != null and isBidProject != ''">is_bid_project = #{isBidProject},</if>
|
||||
<if test="createBy != null">create_by = #{createBy},</if>
|
||||
@@ -384,6 +399,24 @@
|
||||
</trim>
|
||||
where project_id = #{projectId}
|
||||
</update>
|
||||
<!-- 删除公告: 将 4 个公示 URL 字段全部置 NULL (未发布态由 URL 是否为空判定, 表里无独立 is_published 列) -->
|
||||
<update id="clearAnnouncement" parameterType="Long">
|
||||
update biz_project
|
||||
set invitation_url = null,
|
||||
support_letter_url = null,
|
||||
publish_url = null,
|
||||
schedule_url = null
|
||||
where project_id = #{projectId}
|
||||
</update>
|
||||
<!-- 开通到期回收: 每天 00:05 由 OpenStatusScheduler 调用, 到期(open_deadline <= 今天)的 Y 置回 N -->
|
||||
<update id="closeExpiredOpenStatus">
|
||||
update biz_project
|
||||
set open_status = 'N'
|
||||
where open_status = 'Y'
|
||||
and open_deadline is not null
|
||||
and open_deadline <= CURDATE()
|
||||
and is_deleted = 0
|
||||
</update>
|
||||
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||
delete from biz_project where project_id = #{projectId}
|
||||
</delete>
|
||||
@@ -407,8 +440,7 @@
|
||||
from biz_project_assign a
|
||||
where a.project_id = #{projectId}
|
||||
and a.is_deleted = 0
|
||||
and (a.exec_user_id = #{executorUserId}
|
||||
or a.execution_unit_id = (select org_id from biz_org where user_id = #{executorUserId} and org_type = 'executor'))
|
||||
and a.execution_unit_id = (select org_id from biz_org where user_id = #{executorUserId} and org_type = 'executor')
|
||||
</select>
|
||||
<!-- 提交权限用: 判断 user 是否该项目的执行方.
|
||||
MAIN: biz_project_assign (exec_user_id 或 其执行单位 org); SUB(执行人): biz_project_executor_assign.staff_user_id.
|
||||
@@ -420,8 +452,7 @@
|
||||
from biz_project_assign a
|
||||
where a.project_id = #{projectId}
|
||||
and a.is_deleted = 0
|
||||
and (a.exec_user_id = #{userId}
|
||||
or a.execution_unit_id = (select org_id from biz_org where user_id = #{userId} and org_type = 'executor'))
|
||||
and a.execution_unit_id = (select org_id from biz_org where user_id = #{userId} and org_type = 'executor')
|
||||
union
|
||||
select 1
|
||||
from biz_project_executor_assign b
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<result property="planCategory" column="plan_category" />
|
||||
<result property="projectForm" column="project_form" />
|
||||
<result property="designFileUrl" column="design_file_url" />
|
||||
<result property="subjectDirection" column="subject_direction" />
|
||||
<result property="status" column="status" />
|
||||
<result property="isSettled" column="is_settled" />
|
||||
<result property="projectNo" column="project_no" />
|
||||
@@ -29,7 +30,7 @@
|
||||
<sql id="selectFields">
|
||||
select p.plan_id, p.plan_name, p.plan_direction, p.plan_direction_id,
|
||||
s.title as plan_direction_title,
|
||||
p.plan_category, p.project_form, p.design_file_url, p.status, p.is_settled,
|
||||
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,
|
||||
proj.project_name as project_name,
|
||||
@@ -70,6 +71,7 @@
|
||||
<if test="planCategory != null and planCategory != ''">plan_category,</if>
|
||||
<if test="projectForm != null and projectForm != ''">project_form,</if>
|
||||
<if test="designFileUrl != null and designFileUrl != ''">design_file_url,</if>
|
||||
<if test="subjectDirection != null and subjectDirection != ''">subject_direction,</if>
|
||||
<if test="status != null">status,</if>
|
||||
<if test="isSettled != null and isSettled != ''">is_settled,</if>
|
||||
<if test="projectNo != null and projectNo != ''">project_no,</if>
|
||||
@@ -84,6 +86,7 @@
|
||||
<if test="planCategory != null and planCategory != ''">#{planCategory},</if>
|
||||
<if test="projectForm != null and projectForm != ''">#{projectForm},</if>
|
||||
<if test="designFileUrl != null and designFileUrl != ''">#{designFileUrl},</if>
|
||||
<if test="subjectDirection != null and subjectDirection != ''">#{subjectDirection},</if>
|
||||
<if test="status != null">#{status},</if>
|
||||
<if test="isSettled != null and isSettled != ''">#{isSettled},</if>
|
||||
<if test="projectNo != null and projectNo != ''">#{projectNo},</if>
|
||||
@@ -100,6 +103,7 @@
|
||||
<if test="planCategory != null and planCategory != ''">plan_category = #{planCategory},</if>
|
||||
<if test="projectForm != null and projectForm != ''">project_form = #{projectForm},</if>
|
||||
<if test="designFileUrl != null and designFileUrl != ''">design_file_url = #{designFileUrl},</if>
|
||||
<if test="subjectDirection != null and subjectDirection != ''">subject_direction = #{subjectDirection},</if>
|
||||
<if test="isSettled != null and isSettled != ''">is_settled = #{isSettled},</if>
|
||||
<if test="projectNo != null and projectNo != ''">project_no = #{projectNo},</if>
|
||||
<if test="auditOpinion != null and auditOpinion != ''">audit_opinion = #{auditOpinion},</if>
|
||||
|
||||
+5
-4
@@ -5,7 +5,7 @@
|
||||
<resultMap id="BaseResultMap" type="BizProjectSponsorAssign">
|
||||
<id property="id" column="id" />
|
||||
<result property="projectId" column="project_id" />
|
||||
<result property="sponsorUserId" column="sponsor_user_id" />
|
||||
<result property="sponsorOrgId" column="sponsor_org_id" />
|
||||
<result property="monitorUserId" column="monitor_user_id" />
|
||||
<result property="assignDesc" column="assign_desc" />
|
||||
<result property="assignPoints" column="assign_points" />
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
<insert id="insertAssign" parameterType="BizProjectSponsorAssign" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO biz_project_sponsor_assign
|
||||
(project_id, sponsor_user_id, monitor_user_id, assign_desc, assign_points, create_by, create_time)
|
||||
(project_id, sponsor_org_id, monitor_user_id, assign_desc, assign_points, create_by, create_time)
|
||||
VALUES
|
||||
(#{projectId}, #{sponsorUserId}, #{monitorUserId}, #{assignDesc}, #{assignPoints}, #{createBy}, sysdate())
|
||||
(#{projectId}, #{sponsorOrgId}, #{monitorUserId}, #{assignDesc}, #{assignPoints}, #{createBy}, sysdate())
|
||||
</insert>
|
||||
|
||||
<!-- 按 project_id 全删 (支持方分配策略: 一个项目只分配一个 sponsor, 先删后插) -->
|
||||
@@ -38,7 +38,8 @@
|
||||
s.user_name AS sponsor_user_name,
|
||||
m.user_name AS monitor_user_name
|
||||
FROM biz_project_sponsor_assign a
|
||||
LEFT JOIN sys_user s ON a.sponsor_user_id = s.user_id
|
||||
LEFT JOIN biz_org o ON o.org_id = a.sponsor_org_id
|
||||
LEFT JOIN sys_user s ON s.user_id = o.user_id
|
||||
LEFT JOIN sys_user m ON a.monitor_user_id = m.user_id
|
||||
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
||||
ORDER BY a.create_time DESC
|
||||
|
||||
@@ -170,6 +170,8 @@ public class RuoYiConfig
|
||||
private String accessKeySecret;
|
||||
private String dirPrefix = "ry8080/";
|
||||
private int expireSeconds = 3600;
|
||||
/** 阿里云函数计算 (FC) 打 zip 端点 (参考 hwt-serve OssApi.ossDownload, 在 OSS 端打包) */
|
||||
private String zipFuncUrl;
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
@@ -185,5 +187,7 @@ public class RuoYiConfig
|
||||
public void setDirPrefix(String dirPrefix) { this.dirPrefix = dirPrefix; }
|
||||
public int getExpireSeconds() { return expireSeconds; }
|
||||
public void setExpireSeconds(int expireSeconds) { this.expireSeconds = expireSeconds; }
|
||||
public String getZipFuncUrl() { return zipFuncUrl; }
|
||||
public void setZipFuncUrl(String zipFuncUrl) { this.zipFuncUrl = zipFuncUrl; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.ruoyi.common.enums;
|
||||
|
||||
/**
|
||||
* 材料/凭证 审核阶段 (biz_meeting.material_audit_stage / voucher_audit_stage)
|
||||
* 材料 审核阶段 (biz_meeting.material_audit_stage)
|
||||
* <p>
|
||||
* 4 值 (用户拍板「已提交/待审核」合并): 提交后进入 SUBMITTED, 两级审核(合规先-支持方后)
|
||||
* 用 material_compliance_approved / voucher_compliance_approved 布尔区分「合规审中 vs 支持方审中」.
|
||||
* 用 material_compliance_approved 布尔区分「合规审中 vs 支持方审中」.
|
||||
* <pre>
|
||||
* NOT_SUBMITTED 未提交 执行方还没交
|
||||
* ↓ (执行方提交)
|
||||
|
||||
+4
-3
@@ -148,9 +148,10 @@ public class SysLoginService
|
||||
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
|
||||
throw new UserPasswordNotMatchException();
|
||||
}
|
||||
// 用户名不在指定范围内 错误
|
||||
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|
||||
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
|
||||
// 用户名不在指定范围内 错误 (邮箱登录含 @ 不受 2-20 位限制)
|
||||
if (!username.contains("@")
|
||||
&& (username.length() < UserConstants.USERNAME_MIN_LENGTH
|
||||
|| username.length() > UserConstants.USERNAME_MAX_LENGTH))
|
||||
{
|
||||
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
|
||||
throw new UserPasswordNotMatchException();
|
||||
|
||||
+16
@@ -38,6 +38,11 @@ public class UserDetailsServiceImpl implements UserDetailsService
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
|
||||
{
|
||||
SysUser user = userService.selectUserByUserName(username);
|
||||
// 用户名查不到且输入含 @ → 视为邮箱登录 (邮箱+密码)
|
||||
if (StringUtils.isNull(user) && username.contains("@"))
|
||||
{
|
||||
user = userService.selectUserByEmail(username);
|
||||
}
|
||||
if (StringUtils.isNull(user))
|
||||
{
|
||||
log.info("登录用户:{} 不存在.", username);
|
||||
@@ -54,6 +59,17 @@ public class UserDetailsServiceImpl implements UserDetailsService
|
||||
throw new ServiceException(MessageUtils.message("user.blocked"));
|
||||
}
|
||||
|
||||
// 子账号专属: 主账号 (parent_user_id) 被禁用 → 拦截登录
|
||||
if (user.getParentUserId() != null)
|
||||
{
|
||||
SysUser parent = userService.selectUserById(user.getParentUserId());
|
||||
if (parent != null && UserStatus.DISABLE.getCode().equals(parent.getStatus()))
|
||||
{
|
||||
log.info("登录用户:{} 所属机构主账号已禁用.", username);
|
||||
throw new ServiceException("您所属的机构已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
passwordService.validate(user);
|
||||
|
||||
return createLoginUser(user);
|
||||
|
||||
@@ -53,6 +53,14 @@ public interface SysUserMapper
|
||||
*/
|
||||
public SysUser selectUserByUserName(String userName);
|
||||
|
||||
/**
|
||||
* 通过邮箱查询用户 (登录支持邮箱+密码)
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return 用户对象信息
|
||||
*/
|
||||
public SysUser selectUserByEmail(String email);
|
||||
|
||||
/**
|
||||
* 通过用户ID查询用户
|
||||
*
|
||||
@@ -96,6 +104,22 @@ public interface SysUserMapper
|
||||
public int updateUserStatus(@Param("userId") Long userId, @Param("status") String status);
|
||||
public int updateRoleType(@Param("userId") Long userId, @Param("roleType") String roleType);
|
||||
|
||||
/**
|
||||
* 更换机构管理员: 改账号类型 + 主账号指向
|
||||
* parentUserId 传 null = 晋升主账号 (account_type='MAIN', parent_user_id=NULL)
|
||||
*
|
||||
* @param userId 目标 sys_user.user_id
|
||||
* @param accountType 'MAIN' / 'SUB'
|
||||
* @param parentUserId 子账号指向的主账号 user_id (MAIN 传 null)
|
||||
*/
|
||||
public int updateAccountType(@Param("userId") Long userId, @Param("accountType") String accountType, @Param("parentUserId") Long parentUserId);
|
||||
|
||||
/**
|
||||
* 把原管理员的所有子账号 re-point 到新管理员 (更换管理员时, 避免出现三层树)
|
||||
* 排除新管理员自己 (其 parent_user_id 当前仍指向原管理员)
|
||||
*/
|
||||
public int updateParentUserId(@Param("oldParentUserId") Long oldParentUserId, @Param("newParentUserId") Long newParentUserId);
|
||||
|
||||
/**
|
||||
* 更新用户登录信息(IP和登录时间)
|
||||
*
|
||||
|
||||
@@ -52,6 +52,14 @@ public interface ISysUserService
|
||||
*/
|
||||
public SysUser selectUserByUserName(String userName);
|
||||
|
||||
/**
|
||||
* 通过邮箱查询用户 (登录支持邮箱+密码)
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return 用户对象信息
|
||||
*/
|
||||
public SysUser selectUserByEmail(String email);
|
||||
|
||||
/**
|
||||
* 通过用户ID查询用户
|
||||
*
|
||||
|
||||
+9
@@ -131,6 +131,15 @@ public class SysUserServiceImpl implements ISysUserService
|
||||
return userMapper.selectUserByUserName(userName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过邮箱查询用户 (登录支持邮箱+密码)
|
||||
*/
|
||||
@Override
|
||||
public SysUser selectUserByEmail(String email)
|
||||
{
|
||||
return userMapper.selectUserByEmail(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过用户ID查询用户
|
||||
*
|
||||
|
||||
@@ -170,6 +170,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<include refid="selectUserVo"/>
|
||||
where u.user_name = #{userName} and u.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectUserByEmail" parameterType="String" resultMap="SysUserResult">
|
||||
<include refid="selectUserVo"/>
|
||||
where u.email = #{email} and u.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
|
||||
<include refid="selectUserVo"/>
|
||||
@@ -275,6 +280,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
update sys_user set role_type = #{roleType} where user_id = #{userId}
|
||||
</update>
|
||||
|
||||
<!-- 更换管理员: 改 account_type + parent_user_id (parentUserId 为 null 时显式写 NULL = 晋升主账号) -->
|
||||
<update id="updateAccountType">
|
||||
update sys_user
|
||||
set account_type = #{accountType},
|
||||
<choose>
|
||||
<when test="parentUserId != null">parent_user_id = #{parentUserId},</when>
|
||||
<otherwise>parent_user_id = null,</otherwise>
|
||||
</choose>
|
||||
update_time = sysdate()
|
||||
where user_id = #{userId}
|
||||
</update>
|
||||
|
||||
<!-- 更换管理员: 原管理员的所有子账号 re-point 到新管理员 (排除新管理员自己) -->
|
||||
<update id="updateParentUserId">
|
||||
update sys_user
|
||||
set parent_user_id = #{newParentUserId}, update_time = sysdate()
|
||||
where parent_user_id = #{oldParentUserId}
|
||||
and user_id != #{newParentUserId}
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="updateUserStatus" parameterType="SysUser">
|
||||
update sys_user set status = #{status}, update_time = sysdate() where user_id = #{userId}
|
||||
</update>
|
||||
|
||||
Reference in New Issue
Block a user