From f4d8bc1463267df6a60cccd43ed625f09401111f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?= <12369755+htcloud1@user.noreply.gitee.com> Date: Sun, 30 Aug 2026 10:42:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A1=B9=E7=9B=AE=E5=AF=BC=E5=87=BA=20?= =?UTF-8?q?+=20=E4=BC=9A=E8=AE=AE=E8=B7=B3=E8=BD=AC=20+=20=E6=9C=BA?= =?UTF-8?q?=E6=9E=84=E8=BD=AF=E5=88=A0=E9=99=A4=20+=20=E4=BE=9B=E5=BA=94?= =?UTF-8?q?=E5=95=86=E8=B4=A6=E5=8F=B7=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 项目/项目评价/报名专家 3 类导出, 勾选透传 projectIds - 项目列表 总场次/已执行/未执行 三列点击跳对应会议列表 (executor 复用后端 role 隔离) - 机构软删除 + 级联软删机构下账号 - 供应商开放接口账号同步 Co-Authored-By: Claude --- .../BizExecutionIntentController.java | 29 +++- .../BizMeetingAttendeeController.java | 25 +++ .../controller/BizMeetingController.java | 28 ++- .../controller/BizProjectController.java | 164 ++++++++++++++++++ .../com/ruoyi/business/domain/BizOrg.java | 8 + .../com/ruoyi/business/domain/BizProject.java | 4 + .../domain/vo/BizProjectExportVo.java | 105 +++++++++++ .../domain/vo/BizProjectRatingExportVo.java | 59 +++++++ .../domain/vo/BizSignupExpertExportVo.java | 70 ++++++-- .../ruoyi/business/mapper/BizOrgMapper.java | 6 + .../SupplierAccountPullScheduler.java | 73 +++++++- .../impl/BizMeetingAttendeeServiceImpl.java | 57 ------ .../service/impl/BizOrgServiceImpl.java | 7 +- .../supplier/SupplierAccountSyncService.java | 54 +++++- .../business/BizExecutionIntentMapper.xml | 6 + .../mapper/business/BizMeetingMapper.xml | 1 + .../mapper/business/BizOrgMapper.xml | 54 +++++- .../business/BizProjectRatingMapper.xml | 7 + ry-vue3/src/api/public.js | 11 ++ ry-vue3/src/views/executor/Meetings.vue | 10 +- ry-vue3/src/views/executor/Projects.vue | 28 ++- ry-vue3/src/views/manager/Projects.vue | 117 ++++++++++--- ry-vue3/src/views/meetings/MeetingDetail.vue | 106 ++++++----- ry-vue3/src/views/meetings/MeetingNew.vue | 17 +- ry-vue3/src/views/meetings/Meetings.vue | 5 +- 25 files changed, 880 insertions(+), 171 deletions(-) create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectExportVo.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectRatingExportVo.java diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java index 7d58e73..7f038ed 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java @@ -14,9 +14,11 @@ import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.system.mapper.SysUserMapper; +import com.ruoyi.business.domain.BizExpert; import com.ruoyi.business.domain.BizExecutionIntent; import com.ruoyi.business.domain.BizProject; import com.ruoyi.business.domain.vo.BizSignupExpertExportVo; +import com.ruoyi.business.service.IBizExpertService; import com.ruoyi.business.service.IBizExecutionIntentService; import com.ruoyi.business.service.IBizProjectService; @@ -33,6 +35,8 @@ public class BizExecutionIntentController extends BaseController private IBizExecutionIntentService bizExecutionIntentService; @Autowired private IBizProjectService bizProjectService; + @Autowired + private IBizExpertService bizExpertService; /** * 公开门户点击"立即报名" — 写入意向 (user_id = 当前登录用户) @@ -140,12 +144,31 @@ public class BizExecutionIntentController extends BaseController List exportList = new ArrayList<>(list.size()); for (BizExecutionIntent e : list) { BizSignupExpertExportVo v = new BizSignupExpertExportVo(); + v.setProjectNo(e.getProjectNo()); + v.setProjectName(e.getProjectName()); v.setName(e.getName()); - v.setDepartment(e.getDepartment()); - v.setWorkUnit(e.getWorkUnit()); - v.setPosition(e.getPosition()); v.setPhone(e.getPhone()); v.setCreateTime(e.getCreateTime()); + // 医生档案: 报名专家 userId → biz_expert (科室/医院/职称 报名表常为空, 从档案兜底补全) + BizExpert expert = e.getUserId() != null ? bizExpertService.getByUserId(e.getUserId()) : null; + String department = e.getDepartment(); + String workUnit = e.getWorkUnit(); + String position = e.getPosition(); + if (expert != null) { + if (department == null || department.isEmpty()) department = expert.getDepartment(); + if (workUnit == null || workUnit.isEmpty()) workUnit = expert.getWorkUnit(); + if (position == null || position.isEmpty()) position = expert.getTitle(); + v.setRegion(expert.getRegion()); + v.setIdCard(expert.getIdCard()); + v.setBankCard(expert.getBankCard()); + v.setBankName(expert.getBankName()); + v.setBankBranch(expert.getBankBranch()); + v.setBankRegion(expert.getBankRegion()); + v.setBankAddress(expert.getBankAddress()); + } + v.setDepartment(department); + v.setWorkUnit(workUnit); + v.setPosition(position); exportList.add(v); } ExcelUtil util = new ExcelUtil<>(BizSignupExpertExportVo.class); diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java index 1f50321..083815c 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java @@ -9,6 +9,7 @@ import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.business.domain.BizMeeting; @@ -93,6 +94,8 @@ public class BizMeetingAttendeeController extends BaseController { @Log(title = "参会人", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody BizMeetingAttendee body) { + // 参会人名单写操作拦截: executor 劳务提交后禁止 (新增) + checkAttendeeEditable(body.getMeetingId()); Long attendeeId = attendeeService.insertByPhoneWithProfile(body); // 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中) bizMeetingService.recomputeLaborFee(body.getMeetingId()); @@ -111,6 +114,8 @@ public class BizMeetingAttendeeController extends BaseController { return error("id 不能为空"); } BizMeetingAttendee before = attendeeService.selectById(body.getId()); + // 参会人名单写操作拦截: executor 劳务提交后禁止 (编辑) + checkAttendeeEditable(before != null ? before.getMeetingId() : null); body.setUpdateBy(SecurityUtils.getUsername()); int rows = attendeeService.updateProfile(body); // 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中) @@ -128,6 +133,8 @@ public class BizMeetingAttendeeController extends BaseController { @DeleteMapping("/{id}") public AjaxResult remove(@PathVariable("id") Long id) { BizMeetingAttendee before = attendeeService.selectById(id); + // 参会人名单写操作拦截: executor 劳务提交后禁止 (删除) + checkAttendeeEditable(before != null ? before.getMeetingId() : null); int rows = attendeeService.deleteByPrimaryKey(id); // 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中) if (before != null) { @@ -192,6 +199,8 @@ public class BizMeetingAttendeeController extends BaseController { @PostMapping("/importData") public AjaxResult importData(@RequestParam("file") MultipartFile file, @RequestParam("meetingId") Long meetingId) throws Exception { + // 参会人名单写操作拦截: executor 劳务提交后禁止 (导入) + checkAttendeeEditable(meetingId); // 解析 + 入库 (邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发) ImportResult result = attendeeService.importFromExcel(file, meetingId, SecurityUtils.getUsername()); // 人员变化 → 立即重算劳务费 (有成功导入才需重算) @@ -299,4 +308,20 @@ public class BizMeetingAttendeeController extends BaseController { return success(updated); } + /** + * 参会人名单写操作拦截: executor 在劳务提交后 (labor_audit_stage 非 NOT_SUBMITTED/REJECTED) 禁止新增/导入/编辑/删除. + * 退回 (REJECTED) 视为可重新编辑, 与前端 laborEditable 口径一致; manager/admin 不受限. + */ + private void checkAttendeeEditable(Long meetingId) { + if (meetingId == null) return; + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"executor".equals(roleType)) return; + BizMeeting m = bizMeetingService.getById(meetingId); + if (m == null) return; + String stage = m.getLaborAuditStage(); + if (stage != null && !"NOT_SUBMITTED".equals(stage) && !"REJECTED".equals(stage)) { + throw new ServiceException("劳务已提交, 不能修改参会人"); + } + } + } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java index 5274a00..6992aba 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java @@ -1,5 +1,6 @@ package com.ruoyi.business.controller; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; @@ -187,6 +188,8 @@ public class BizMeetingController extends BaseController { bizMeeting.setTotalPeriods(proj.getTotalSessions()); } bizMeeting.setProjectForm(proj.getProjectForm()); + // 会议时间区间必须在项目起止时间区间内 (闭区间; 项目未设起止则跳过对应边界) + validateMeetingWithinProject(bizMeeting, proj); } } int rows = bizMeetingService.insert(bizMeeting); @@ -198,6 +201,19 @@ public class BizMeetingController extends BaseController { return toAjax(rows); } + /** 会议时间区间必须在项目起止时间区间内 (闭区间, 等于允许); 项目未设起止则跳过对应边界. */ + private void validateMeetingWithinProject(BizMeeting meeting, BizProject proj) { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + if (meeting.getStartTime() != null && proj.getStartTime() != null + && meeting.getStartTime().before(proj.getStartTime())) { + throw new ServiceException("会议开始时间不能早于项目开始时间 " + fmt.format(proj.getStartTime())); + } + if (meeting.getEndTime() != null && proj.getEndTime() != null + && meeting.getEndTime().after(proj.getEndTime())) { + throw new ServiceException("会议结束时间不能晚于项目结束时间 " + fmt.format(proj.getEndTime())); + } + } + @Log(title = "会议", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody BizMeeting bizMeeting) { @@ -234,6 +250,8 @@ public class BizMeetingController extends BaseController { BizProject proj = bizProjectService.getById(bizMeeting.getProjectId()); if (proj != null) { bizMeeting.setProjectForm(proj.getProjectForm()); + // 会议时间区间必须在项目起止时间区间内 (闭区间; 项目未设起止则跳过对应边界) + validateMeetingWithinProject(bizMeeting, proj); } } int rows = bizMeetingService.updateByPrimaryKey(bizMeeting); @@ -576,8 +594,14 @@ public class BizMeetingController extends BaseController { List existingMaterials = bizMeetingMaterialService.selectByMeetingId(meetingId); boolean hasLv = existingMaterials != null && existingMaterials.stream().anyMatch(v -> "LV_PAYMENT".equals(v.getSubType()) && v.getOssUrl() != null && !v.getOssUrl().isEmpty()); boolean hasSv = existingMaterials != null && existingMaterials.stream().anyMatch(v -> "SV_PAYMENT".equals(v.getSubType()) && v.getOssUrl() != null && !v.getOssUrl().isEmpty()); - if (!hasLv || !hasSv) { - throw new ServiceException("请先上传付款凭证"); + if (!hasLv && !hasSv) { + throw new ServiceException("请先上传劳务凭证与会务凭证"); + } + if (!hasLv) { + throw new ServiceException("请先上传劳务凭证"); + } + if (!hasSv) { + throw new ServiceException("请先上传会务凭证"); } m.setIsSettled(1); diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java index e05a9f7..ba271e8 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Objects; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import jakarta.servlet.http.HttpServletResponse; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; @@ -16,11 +17,17 @@ import com.ruoyi.common.core.page.TableDataInfo; 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.BizExpert; 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.domain.vo.BizProjectExportVo; +import com.ruoyi.business.domain.vo.BizProjectRatingExportVo; +import com.ruoyi.business.domain.vo.BizSignupExpertExportVo; +import com.ruoyi.business.service.IBizExpertService; import com.ruoyi.business.service.IBizOrgService; import com.ruoyi.business.service.IBizProjectService; import com.ruoyi.business.service.IBizExecutionIntentService; @@ -66,6 +73,8 @@ public class BizProjectController extends BaseController private SysUserMapper sysUserMapper; @Autowired private IBizOrgService bizOrgService; + @Autowired + private IBizExpertService bizExpertService; /** * 我报名的项目 (当前用户在 biz_execution_intent 里有意向的项目) @@ -616,4 +625,159 @@ public class BizProjectController extends BaseController ajax.put("errors", errors); return ajax; } + + // ============== 导出 (后端 ExcelUtil 写 .xlsx, 跟随当前筛选) ============== + + /** + * 导出项目列表 (跟随当前筛选条件, 与 list() 同一口径, 无 startPage 全量导出) + * POST /business/project/export + * admin 导出全部; manager 只导出本人创建的 (与 list 一致) + */ + @Log(title = "导出项目", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, BizProject bizProject) + { + applyManagerScope(bizProject); + List list = bizProjectService.selectList(bizProject); + List exportList = new ArrayList<>(list.size()); + for (BizProject p : list) { + BizProjectExportVo v = new BizProjectExportVo(); + v.setProjectNo(p.getProjectNo()); + v.setProjectName(p.getProjectName()); + v.setTotalSessions(p.getTotalSessions()); + v.setDoneSessions(p.getDoneSessions()); + v.setTodoSessions(p.getTodoSessions()); + v.setTotalAmount(p.getTotalAmount()); + v.setAvailableAmount(p.getAvailableAmount()); + v.setPaidLaborAmount(p.getPaidLaborAmount()); + v.setPaidMeetingAmount(p.getPaidMeetingAmount()); + v.setManagerScore(p.getManagerScore()); + v.setSponsorScore(p.getSponsorScore()); + v.setSponsorOrgName(p.getSponsorOrgName()); + v.setExecOrgNames(p.getExecOrgNames()); + v.setProjectForm(p.getProjectForm()); + v.setIsFinished("1".equals(p.getIsFinished()) ? "已结题" : "未结题"); + v.setStartTime(p.getStartTime()); + v.setEndTime(p.getEndTime()); + exportList.add(v); + } + ExcelUtil util = new ExcelUtil<>(BizProjectExportVo.class); + util.exportExcel(response, exportList, "项目列表"); + } + + /** + * 导出项目评价 (4 维度评分明细, 跟随当前筛选的项目范围) + * POST /business/project/ratingExport + * 先按筛选取项目 → 收集 projectId → 查 biz_project_rating 明细 + */ + @Log(title = "导出项目评价", businessType = BusinessType.EXPORT) + @PostMapping("/ratingExport") + public void exportRating(HttpServletResponse response, BizProject bizProject) + { + applyManagerScope(bizProject); + List projects = bizProjectService.selectList(bizProject); + // projectId → project 反查表 (评分明细表 project_no/project_name 可能为空, 用主表补齐) + Map projectById = new HashMap<>(); + List projectIds = new ArrayList<>(); + for (BizProject p : projects) { + if (p.getProjectId() != null) { + projectIds.add(p.getProjectId()); + projectById.put(p.getProjectId(), p); + } + } + List ratings = new ArrayList<>(); + if (!projectIds.isEmpty()) { + BizProjectRating q = new BizProjectRating(); + q.getParams().put("projectIds", projectIds); + ratings = bizProjectRatingService.selectList(q); + } + List exportList = new ArrayList<>(ratings.size()); + for (BizProjectRating r : ratings) { + BizProject p = projectById.get(r.getProjectId()); + BizProjectRatingExportVo v = new BizProjectRatingExportVo(); + v.setProjectNo(p != null ? p.getProjectNo() : r.getProjectNo()); + v.setProjectName(p != null ? p.getProjectName() : r.getProjectName()); + v.setRaterRole("sponsor".equals(r.getRaterRole()) ? "支持方" : "合规管理员"); + v.setQualityScore(r.getQualityScore()); + v.setResponseScore(r.getResponseScore()); + v.setCooperationScore(r.getCooperationScore()); + v.setComplianceScore(r.getComplianceScore()); + v.setCreateTime(r.getCreateTime()); + exportList.add(v); + } + ExcelUtil util = new ExcelUtil<>(BizProjectRatingExportVo.class); + util.exportExcel(response, exportList, "项目评价"); + } + + /** + * 导出已报名专家 (跟随当前筛选的项目范围, 仅 manager) + * POST /business/project/signupExpertExport + * 先按筛选取项目 → 收集 projectNo → 查 biz_execution_intent 报名专家 + */ + @Log(title = "导出报名专家", businessType = BusinessType.EXPORT) + @PostMapping("/signupExpertExport") + public void exportSignupExpert(HttpServletResponse response, BizProject bizProject) + { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if (!"manager".equals(roleType)) { + throw new ServiceException("只有合规管理员可导出报名专家"); + } + applyManagerScope(bizProject); + List projects = bizProjectService.selectList(bizProject); + List projectNos = new ArrayList<>(); + for (BizProject p : projects) { + if (p.getProjectNo() != null && !p.getProjectNo().isEmpty()) projectNos.add(p.getProjectNo()); + } + List intents = new ArrayList<>(); + if (!projectNos.isEmpty()) { + BizExecutionIntent q = new BizExecutionIntent(); + q.getParams().put("projectNos", projectNos); + intents = bizExecutionIntentService.selectList(q); + } + List exportList = new ArrayList<>(intents.size()); + for (BizExecutionIntent e : intents) { + BizSignupExpertExportVo v = new BizSignupExpertExportVo(); + v.setProjectNo(e.getProjectNo()); + v.setProjectName(e.getProjectName()); + v.setName(e.getName()); + v.setPhone(e.getPhone()); + v.setCreateTime(e.getCreateTime()); + // 医生档案: 报名专家 userId → biz_expert (科室/医院/职称 报名表常为空, 从档案兜底补全) + BizExpert expert = e.getUserId() != null ? bizExpertService.getByUserId(e.getUserId()) : null; + String department = e.getDepartment(); + String workUnit = e.getWorkUnit(); + String position = e.getPosition(); + if (expert != null) { + if (department == null || department.isEmpty()) department = expert.getDepartment(); + if (workUnit == null || workUnit.isEmpty()) workUnit = expert.getWorkUnit(); + if (position == null || position.isEmpty()) position = expert.getTitle(); + v.setRegion(expert.getRegion()); + v.setIdCard(expert.getIdCard()); + v.setBankCard(expert.getBankCard()); + v.setBankName(expert.getBankName()); + v.setBankBranch(expert.getBankBranch()); + v.setBankRegion(expert.getBankRegion()); + v.setBankAddress(expert.getBankAddress()); + } + v.setDepartment(department); + v.setWorkUnit(workUnit); + v.setPosition(position); + exportList.add(v); + } + ExcelUtil util = new ExcelUtil<>(BizSignupExpertExportVo.class); + util.exportExcel(response, exportList, "报名专家"); + } + + /** 与 list() 一致的导出数据范围: manager 只看本人创建的项目; 勾选时按 projectIds 过滤 */ + private void applyManagerScope(BizProject bizProject) + { + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if ("manager".equals(roleType)) { + bizProject.getParams().put("createUserId", SecurityUtils.getUserId()); + } + // 勾选的项目: 前端勾选时传 projectIds, 转 params.projectIds 做 IN 过滤 + if (bizProject.getProjectIds() != null && !bizProject.getProjectIds().isEmpty()) { + bizProject.getParams().put("projectIds", bizProject.getProjectIds()); + } + } } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java index f7b29aa..831bb1f 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java @@ -50,6 +50,10 @@ public class BizOrg extends BaseEntity { /** update_time */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; + /** del_flag 删除标志 0正常 1删除 */ + private String delFlag; + /** is_synced 同步来源标记 0注册 1供应商同步 */ + private Integer isSynced; public Long getOrgId() { return orgId; } public void setOrgId(Long orgId) { this.orgId = orgId; } @@ -77,4 +81,8 @@ public class BizOrg extends BaseEntity { public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } + public String getDelFlag() { return delFlag; } + public void setDelFlag(String delFlag) { this.delFlag = delFlag; } + public Integer getIsSynced() { return isSynced; } + public void setIsSynced(Integer isSynced) { this.isSynced = isSynced; } } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java index 4dbee32..e79193a 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java @@ -56,6 +56,8 @@ public class BizProject extends BaseEntity { private String sponsorAdminUserName; /** 支持单位名称 (列表展示列, JOIN biz_org 取 org_name, org_type='sponsor') */ private String sponsorOrgName; + /** 项目ID筛选 (导出时勾选项目透传, IN 查询) */ + private List projectIds; /** 支持单位筛选 (biz_org.org_id 列表, IN 查询, 多选 select 透传) */ private List sponsorOrgIds; /** 服务机构名称列表, 多个用英文逗号连接 (GROUP_CONCAT 派生, 不入库, 仅展示) */ @@ -181,6 +183,8 @@ 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 getProjectIds() { return projectIds; } + public void setProjectIds(List projectIds) { this.projectIds = projectIds; } public List getSponsorOrgIds() { return sponsorOrgIds; } public void setSponsorOrgIds(List sponsorOrgIds) { this.sponsorOrgIds = sponsorOrgIds; } public String getExecOrgNames() { return execOrgNames; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectExportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectExportVo.java new file mode 100644 index 0000000..aa60d9c --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectExportVo.java @@ -0,0 +1,105 @@ +package com.ruoyi.business.domain.vo; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; + +/** + * 项目列表导出 VO (中文列头, 与 manager/Projects.vue 列表列一致) + * + *

数据源: BizProject.selectList 派生字段 (含会议子查询聚合列). + * 仅用于 Excel 导出, 不参与业务逻辑. + * + * @author guoju + */ +public class BizProjectExportVo { + + @Excel(name = "项目编号", sort = 1) + private String projectNo; + + @Excel(name = "项目名称", sort = 2) + private String projectName; + + @Excel(name = "总场次/总期数", sort = 3) + private Long totalSessions; + + @Excel(name = "已执行", sort = 4) + private Long doneSessions; + + @Excel(name = "未执行", sort = 5) + private Long todoSessions; + + @Excel(name = "总金额", sort = 6) + private BigDecimal totalAmount; + + @Excel(name = "可用金额", sort = 7) + private BigDecimal availableAmount; + + @Excel(name = "已支付劳务费", sort = 8) + private BigDecimal paidLaborAmount; + + @Excel(name = "已支付会务费", sort = 9) + private BigDecimal paidMeetingAmount; + + @Excel(name = "执行单位评分(合规)", sort = 10) + private BigDecimal managerScore; + + @Excel(name = "执行单位评分(支持)", sort = 11) + private BigDecimal sponsorScore; + + @Excel(name = "支持单位", sort = 12) + private String sponsorOrgName; + + @Excel(name = "服务机构", sort = 13) + private String execOrgNames; + + @Excel(name = "项目形式", sort = 14) + private String projectForm; + + @Excel(name = "是否结题", sort = 15) + private String isFinished; + + @Excel(name = "项目开始时间", sort = 16, dateFormat = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @Excel(name = "项目结束时间", sort = 17, dateFormat = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public Long getTotalSessions() { return totalSessions; } + public void setTotalSessions(Long totalSessions) { this.totalSessions = totalSessions; } + public Long getDoneSessions() { return doneSessions; } + public void setDoneSessions(Long doneSessions) { this.doneSessions = doneSessions; } + public Long getTodoSessions() { return todoSessions; } + public void setTodoSessions(Long todoSessions) { this.todoSessions = todoSessions; } + public BigDecimal getTotalAmount() { return totalAmount; } + public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } + public BigDecimal getAvailableAmount() { return availableAmount; } + public void setAvailableAmount(BigDecimal availableAmount) { this.availableAmount = availableAmount; } + public BigDecimal getPaidLaborAmount() { return paidLaborAmount; } + public void setPaidLaborAmount(BigDecimal paidLaborAmount) { this.paidLaborAmount = paidLaborAmount; } + public BigDecimal getPaidMeetingAmount() { return paidMeetingAmount; } + public void setPaidMeetingAmount(BigDecimal paidMeetingAmount) { this.paidMeetingAmount = paidMeetingAmount; } + public BigDecimal getManagerScore() { return managerScore; } + public void setManagerScore(BigDecimal managerScore) { this.managerScore = managerScore; } + public BigDecimal getSponsorScore() { return sponsorScore; } + public void setSponsorScore(BigDecimal sponsorScore) { this.sponsorScore = sponsorScore; } + public String getSponsorOrgName() { return sponsorOrgName; } + public void setSponsorOrgName(String sponsorOrgName) { this.sponsorOrgName = sponsorOrgName; } + public String getExecOrgNames() { return execOrgNames; } + public void setExecOrgNames(String execOrgNames) { this.execOrgNames = execOrgNames; } + 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 Date getStartTime() { return startTime; } + public void setStartTime(Date startTime) { this.startTime = startTime; } + public Date getEndTime() { return endTime; } + public void setEndTime(Date endTime) { this.endTime = endTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectRatingExportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectRatingExportVo.java new file mode 100644 index 0000000..ebe8433 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizProjectRatingExportVo.java @@ -0,0 +1,59 @@ +package com.ruoyi.business.domain.vo; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; + +/** + * 项目评价导出 VO (中文列头, 4 维度评分明细) + * + *

数据源: biz_project_rating (每个项目 × 每个评分角色 一条评分记录). + * 项目编号/项目名称由导出时用 project_id 反查 biz_project 主表补齐 (明细表这两列可能为空). + * 仅用于 Excel 导出, 不参与业务逻辑. + * + * @author guoju + */ +public class BizProjectRatingExportVo { + + @Excel(name = "项目编号", sort = 1) + private String projectNo; + + @Excel(name = "项目名称", sort = 2) + private String projectName; + + @Excel(name = "评分角色", sort = 3) + private String raterRole; + + @Excel(name = "履约质量", sort = 4) + private Long qualityScore; + + @Excel(name = "时效响应", sort = 5) + private Long responseScore; + + @Excel(name = "配合度", sort = 6) + private Long cooperationScore; + + @Excel(name = "合规安全", sort = 7) + private Long complianceScore; + + @Excel(name = "评分时间", sort = 8, dateFormat = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public String getRaterRole() { return raterRole; } + public void setRaterRole(String raterRole) { this.raterRole = raterRole; } + public Long getQualityScore() { return qualityScore; } + public void setQualityScore(Long qualityScore) { this.qualityScore = qualityScore; } + public Long getResponseScore() { return responseScore; } + public void setResponseScore(Long responseScore) { this.responseScore = responseScore; } + public Long getCooperationScore() { return cooperationScore; } + public void setCooperationScore(Long cooperationScore) { this.cooperationScore = cooperationScore; } + public Long getComplianceScore() { return complianceScore; } + public void setComplianceScore(Long complianceScore) { this.complianceScore = complianceScore; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizSignupExpertExportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizSignupExpertExportVo.java index ec21053..85f8a1e 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizSignupExpertExportVo.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizSignupExpertExportVo.java @@ -7,32 +7,64 @@ import com.ruoyi.common.annotation.Excel; /** * 报名专家导出 VO (中文列头) * - *

数据源: biz_execution_intent (公开门户"立即报名"写入的执行意向表, 即已报名专家). + *

数据源: biz_execution_intent (公开门户"立即报名"写入的执行意向表, 即已报名专家) + * + biz_expert (医生档案, 报名专家 userId → biz_expert.userId 反查补全银行/证件/地区/科室/医院/职称). * 仅用于 Excel 导出, 不参与业务逻辑. * * @author guoju */ public class BizSignupExpertExportVo { - @Excel(name = "专家姓名", sort = 1) + @Excel(name = "项目编号", sort = 1) + private String projectNo; + + @Excel(name = "项目名称", sort = 2) + private String projectName; + + @Excel(name = "专家姓名", sort = 3) private String name; - @Excel(name = "科室", sort = 2) + @Excel(name = "科室", sort = 4) private String department; - @Excel(name = "医院", sort = 3) + @Excel(name = "医院", sort = 5) private String workUnit; - @Excel(name = "职称", sort = 4) + @Excel(name = "职称", sort = 6) private String position; - @Excel(name = "报名时间", sort = 5, dateFormat = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "手机号", sort = 7) + private String phone; + + @Excel(name = "地区", sort = 8) + private String region; + + @Excel(name = "身份证件号码", sort = 9) + private String idCard; + + @Excel(name = "银行卡号", sort = 10) + private String bankCard; + + @Excel(name = "银行名称", sort = 11) + private String bankName; + + @Excel(name = "开户行支行", sort = 12) + private String bankBranch; + + @Excel(name = "开户行省/市", sort = 13) + private String bankRegion; + + @Excel(name = "开户行地址", sort = 14) + private String bankAddress; + + @Excel(name = "报名时间", sort = 15, dateFormat = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; - @Excel(name = "手机号", sort = 6) - private String phone; - + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDepartment() { return department; } @@ -41,8 +73,22 @@ public class BizSignupExpertExportVo { public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } public String getPosition() { return position; } public void setPosition(String position) { this.position = position; } - public Date getCreateTime() { return createTime; } - public void setCreateTime(Date createTime) { this.createTime = createTime; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } -} \ No newline at end of file + public String getRegion() { return region; } + public void setRegion(String region) { this.region = region; } + public String getIdCard() { return idCard; } + public void setIdCard(String idCard) { this.idCard = idCard; } + public String getBankCard() { return bankCard; } + public void setBankCard(String bankCard) { this.bankCard = bankCard; } + public String getBankName() { return bankName; } + public void setBankName(String bankName) { this.bankName = bankName; } + public String getBankBranch() { return bankBranch; } + public void setBankBranch(String bankBranch) { this.bankBranch = bankBranch; } + public String getBankRegion() { return bankRegion; } + public void setBankRegion(String bankRegion) { this.bankRegion = bankRegion; } + public String getBankAddress() { return bankAddress; } + public void setBankAddress(String bankAddress) { this.bankAddress = bankAddress; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java index ff13a0d..0ff4193 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java @@ -10,6 +10,8 @@ public interface BizOrgMapper { int insert(BizOrg entity); int updateByPrimaryKey(BizOrg entity); int deleteByPrimaryKeys(Long[] orgIds); + /** 级联软删机构下所有账号 (主 + 子), 删除机构时调用 */ + int softDeleteUsersByOrgId(Long orgId); /** 支持方下拉选项 (JOIN sys_user.user_name), 用于 manager 项目分配弹窗, 返回 orgId/orgName/userName */ List> selectSponsorOrgOptions(BizOrg entity); /** 执行方下拉选项 (JOIN sys_user MAIN 账号 user_name), 用于 manager 项目分配弹窗 @@ -33,4 +35,8 @@ public interface BizOrgMapper { Long selectOrgIdByUserId(Long userId); /** 单位名称查重: 同 orgType 下 org_name 精确匹配的条数 (新增单位前判重) */ int countByOrgName(BizOrg entity); + /** 供应商同步 org 去重: 按税号精确定位 executor org (取 org_id 最小) */ + BizOrg selectByTaxNo(BizOrg entity); + /** 供应商同步 org 去重兜底: 税号缺失时按企业名精确定位 executor org (取 org_id 最小) */ + BizOrg selectByNameAndType(BizOrg entity); } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java index 4af1100..e40e4a9 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java @@ -4,6 +4,10 @@ import java.net.URLEncoder; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; @@ -21,12 +25,17 @@ import com.ruoyi.common.utils.http.HttpUtils; import lombok.extern.slf4j.Slf4j; /** - * 供应商账号数据拉取调度器: 每分钟拉取最近 N 分钟更新的账号, 解密后同步到执行方侧. + * 供应商账号数据拉取调度器. *

* 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts} * 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回. * 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密. *

+ * 三个触发点: + * 1. 启动后立即 (异步线程池) 全量拉 1 个月 ({@code full-pull-days}, 默认 30 天) + * 2. 每分钟增量拉最近 5 分钟 ({@code incremental-minutes}, 默认 5) + * 3. 每晚 2 点全量拉 1 个月 ({@code full-pull-days}) + *

* 解密后按邮箱 upsert 到 sys_user / biz_org / biz_person (见 {@link SupplierAccountSyncService}). */ @Slf4j @@ -36,7 +45,8 @@ public class SupplierAccountPullScheduler private static final String KEY = "supplier-account-api-aes.key"; private static final String BASE_URL = "supplier-account-api.base-url"; private static final String PAGE_SIZE = "supplier-account-api.page-size"; - private static final String PULL_MINUTES = "supplier-account-api.pull-minutes"; + private static final String INCREMENTAL_MINUTES = "supplier-account-api.incremental-minutes"; + private static final String FULL_PULL_DAYS = "supplier-account-api.full-pull-days"; private static final String DEFAULT_BASE_URL = "https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts"; @@ -51,8 +61,46 @@ public class SupplierAccountPullScheduler @Autowired private SupplierAccountSyncService supplierAccountSyncService; - @Scheduled(fixedRate = 60_000, initialDelay = 30_000) - public void pullAccounts() + /** 启动全量拉取专用线程池 (daemon 单线程, 不阻塞启动, 也不阻塞 JVM 退出) */ + private final ExecutorService startupExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "supplier-account-pull-startup"); + t.setDaemon(true); + return t; + }); + + /** 启动后立即全量拉取 1 个月数据 (异步, 不阻塞 Spring 启动) */ + @PostConstruct + public void init() + { + startupExecutor.submit(() -> { + try + { + log.info("[SupplierAccountPull] 启动全量拉取开始 (近 {} 天)", fullPullDays()); + pull(fullPullDays() * 24 * 60); + } + catch (Exception e) + { + log.warn("[SupplierAccountPull] 启动全量拉取失败", e); + } + }); + } + + /** 每分钟增量拉最近 N 分钟 (默认 5 分钟) 的账号 */ + @Scheduled(fixedRate = 60_000, initialDelay = 60_000) + public void pullIncremental() + { + pull(incrementalMinutes()); + } + + /** 每晚 2 点全量拉取 1 个月数据 */ + @Scheduled(cron = "0 0 2 * * ?") + public void pullFullNightly() + { + pull(fullPullDays() * 24 * 60); + } + + /** 拉取最近 minutes 分钟更新的账号并同步 */ + private void pull(int minutes) { try { @@ -64,8 +112,7 @@ public class SupplierAccountPullScheduler } String baseUrl = env.getProperty(BASE_URL, DEFAULT_BASE_URL); int pageSize = env.getProperty(PAGE_SIZE, Integer.class, 20); - int pullMinutes = env.getProperty(PULL_MINUTES, Integer.class, 5*24*60*60); - String lastUpdatedTime = LocalDateTime.now().minusMinutes(pullMinutes).format(TIME_FMT); + String lastUpdatedTime = LocalDateTime.now().minusMinutes(minutes).format(TIME_FMT); int pageNum = 1; int fetched = 0; @@ -109,11 +156,21 @@ public class SupplierAccountPullScheduler } pageNum++; } - log.info("[SupplierAccountPull] 本轮完成, 共 {} 条", fetched); + log.info("[SupplierAccountPull] 本轮完成, 窗口={}分钟, 共 {} 条", minutes, fetched); } catch (Exception e) { - log.warn("[SupplierAccountPull] 拉取失败 (跳过, 下分钟再试)", e); + log.warn("[SupplierAccountPull] 拉取失败 (跳过, 下轮再试)", e); } } + + private int incrementalMinutes() + { + return env.getProperty(INCREMENTAL_MINUTES, Integer.class, 5); + } + + private int fullPullDays() + { + return env.getProperty(FULL_PULL_DAYS, Integer.class, 30); + } } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java index 1960988..6b8f7c6 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java @@ -120,10 +120,6 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService } phone = phone.trim(); - // 校验实发金额不超所选角色劳务金额合计 (与手动新增弹窗 MeetingDetail.validateFee 一致; - // 无角色/无金额/项目无 role_labor 时跳过) - validateFeeAgainstRole(loadRoleLabor(body.getMeetingId()), body.getLaborForm(), body.getFee()); - // 1. 按 phone 查 sys_user (单条 IN 查, selectByPhoneList 接受 List) List hits = sysUserMapper.selectByPhoneList(Collections.singletonList(phone)); Long userId; @@ -236,8 +232,6 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService BizMeetingAttendee old = mapper.selectById(entity.getId()); if (old != null) meetingId = old.getMeetingId(); } - // 校验实发金额不超所选角色劳务金额合计 (与手动新增弹窗 MeetingDetail.validateFee 一致) - validateFeeAgainstRole(loadRoleLabor(meetingId), entity.getLaborForm(), entity.getFee()); return mapper.updateProfile(entity); } @@ -377,24 +371,6 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService return fee; } - /** - * 解析某会议的 project.role_labor JSON 数组, 失败/无数据返回 null. - * 供"实发金额 ≤ 角色设置金额"校验复用 (单条 add/edit 各自加载). - */ - private JsonNode loadRoleLabor(Long meetingId) { - if (meetingId == null) return null; - try { - BizMeeting meeting = meetingMapper.selectByPrimaryKey(meetingId); - if (meeting == null || meeting.getProjectId() == null) return null; - BizProject project = projectMapper.selectByPrimaryKey(meeting.getProjectId()); - if (project == null || project.getRoleLabor() == null || project.getRoleLabor().trim().isEmpty()) return null; - return objectMapper.readTree(project.getRoleLabor()); - } catch (Exception e) { - log.warn("[attendee] 解析项目角色劳务失败 meetingId={}", meetingId, e); - return null; - } - } - /** * 在项目角色劳务 JSON 数组 [{role, customName, amount}] 里按单个角色名匹配劳务金额. * 匹配规则与前端 ProjectRoleSelect 一致: role === '其他' 时用 customName 作 label, 否则用 role. @@ -434,39 +410,6 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService return matchRoleAmount(nodes, laborForm); } - /** - * 参会人角色 (laborForm, 可能逗号分隔多选) 对应的角色劳务金额合计. - * 与前端 MeetingDetail.roleAmountSum 一致: 按逗号拆分逐个匹配求和. - * - * @return 金额合计; nodes 为空/非数组 → null (无 role_labor 数据, 无法比较) - */ - private BigDecimal sumRoleAmount(JsonNode nodes, String laborForm) { - if (nodes == null || !nodes.isArray() || laborForm == null || laborForm.trim().isEmpty()) { - return null; - } - BigDecimal sum = BigDecimal.ZERO; - for (String item : laborForm.split(",")) { - BigDecimal a = matchRoleAmount(nodes, item.trim()); - if (a != null) sum = sum.add(a); - } - return sum; - } - - /** - * 校验实发金额(fee)不超所选角色劳务金额合计 (与前端 MeetingDetail.validateFee 一致). - * laborForm 为空 / fee 为空 / 项目无 role_labor 数据时跳过 (无"设置金额"可比较). - */ - private void validateFeeAgainstRole(JsonNode roleLaborNodes, String laborForm, BigDecimal fee) { - if (laborForm == null || laborForm.trim().isEmpty()) return; - if (fee == null) return; - BigDecimal sum = sumRoleAmount(roleLaborNodes, laborForm); - if (sum == null) return; - if (fee.compareTo(sum) > 0) { - throw new ServiceException("实发金额不能超过角色金额 " - + sum.setScale(2, RoundingMode.HALF_UP).toPlainString() + " 元"); - } - } - /** * 批量导入参会人 (Excel → biz_meeting_attendee). * diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java index b93668c..82d103b 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java @@ -65,9 +65,14 @@ public class BizOrgServiceImpl implements IBizOrgService { } @Override + @Transactional public int deleteByPrimaryKeys(Long[] orgIds) { int rows = 0; - for (Long id : orgIds) { rows += bizOrgMapper.deleteByPrimaryKeys(new Long[]{id}); } + for (Long id : orgIds) { + // 先级联软删机构下所有账号 (主 + 子), 再软删机构 (删除机构=软删除, 账号也随之软删) + bizOrgMapper.softDeleteUsersByOrgId(id); + rows += bizOrgMapper.deleteByPrimaryKeys(new Long[]{id}); + } return rows; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountSyncService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountSyncService.java index a276fcc..f269bb9 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountSyncService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountSyncService.java @@ -88,7 +88,8 @@ public class SupplierAccountSyncService // 1. sys_user upsert (去重键=邮箱, 不过滤 del_flag, 以支持"删除→恢复") SysUser user = sysUserMapper.selectUserByEmailIgnoreDel(email); Long userId; - if (user == null) + boolean isNewUser = user == null; + if (isNewUser) { SysUser nu = new SysUser(); nu.setUserName(email); @@ -117,14 +118,25 @@ public class SupplierAccountSyncService sysUserMapper.updateSyncedUser(upd); } - // 2. biz_org upsert (按主账号 user_id) - BizOrg q = new BizOrg(); - q.setUserId(userId); - q.setOrgType("executor"); - List orgs = bizOrgMapper.selectList(q); - BizOrg org = orgs.isEmpty() ? null : orgs.get(0); + // 2. biz_org 去重定位: 税号优先, 缺则企业名兜底 (同企业只存一个 org) + BizOrg org = null; + if (taxNo != null && !taxNo.isEmpty()) + { + BizOrg probe = new BizOrg(); + probe.setTaxNo(taxNo); + org = bizOrgMapper.selectByTaxNo(probe); + } if (org == null) { + BizOrg probe = new BizOrg(); + probe.setOrgName(orgName); + org = bizOrgMapper.selectByNameAndType(probe); + } + + Long orgId; + if (org == null) + { + // 无同企业 org → 新建, 当前账号当 MAIN 管理员 (org.user_id = 当前 userId) BizOrg no = new BizOrg(); no.setUserId(userId); no.setOrgName(orgName); @@ -135,11 +147,36 @@ public class SupplierAccountSyncService no.setContactName(a.getContactName()); no.setContactPhone(a.getContactPhone()); no.setStatus(status); + no.setIsSynced(1); bizOrgMapper.insert(no); - org = no; + orgId = no.getOrgId(); + // 旧账号 (update 分支) 此前可能是 SUB 子账号, 新建 org 后须晋升为 MAIN + if (!isNewUser) + { + sysUserMapper.updateAccountType(userId, "MAIN", null); + } } else { + // 复用已有 org: id 最小 = MAIN (当前 userId < org.user_id 时让位) + orgId = org.getOrgId(); + Long mainUserId = org.getUserId(); + Long newMainUserId = mainUserId; + if (mainUserId == null || userId < mainUserId) + { + newMainUserId = userId; + sysUserMapper.updateAccountType(userId, "MAIN", null); + if (mainUserId != null) + { + sysUserMapper.updateAccountType(mainUserId, "SUB", userId); + } + } + else if (!mainUserId.equals(userId)) + { + sysUserMapper.updateAccountType(userId, "SUB", mainUserId); + } + // 刷新 org 档案字段 (含补写税号 / 主账号指向) + org.setUserId(newMainUserId); org.setOrgName(orgName); org.setBusinessNature(businessNature); org.setAddress(address); @@ -149,7 +186,6 @@ public class SupplierAccountSyncService org.setStatus(status); bizOrgMapper.updateByPrimaryKey(org); } - Long orgId = org.getOrgId(); // 3. biz_person upsert (按 user_id), 打同步标记 BizPerson person = bizPersonMapper.selectByUserId(userId); diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml index 850454b..9f63216 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml @@ -27,6 +27,12 @@ and user_id = #{userId} and project_no = #{projectNo} + + and project_no in + + #{no} + + and project_name = #{projectName} and name = #{name} and department = #{department} diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml index ac2a4c1..5e531ad 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml @@ -82,6 +82,7 @@ is_deleted = 0 and project_no like concat('%', #{projectNo}, '%') + and project_id = #{projectId} and meeting_id = #{meetingId} and meeting_name like concat('%', #{meetingName}, '%') and period_no = #{periodNo} diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml index 6d4fcd0..710bc2a 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml @@ -17,23 +17,27 @@ + + select org_id, user_id, org_name, org_type, business_nature, address, tax_no, status, create_time, contact_name, contact_phone, intent_count, - create_by, update_by, update_time + create_by, update_by, update_time, del_flag, is_synced from biz_org select coalesce( - (select org_id from biz_org where user_id = #{userId} limit 1), + (select org_id from biz_org where user_id = #{userId} and del_flag = '0' limit 1), (select org_id from biz_person where user_id = #{userId} limit 1) ) @@ -203,7 +224,28 @@ + + + + + + diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml index 7cbc8a2..a140631 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml @@ -15,6 +15,7 @@ + @@ -30,6 +31,12 @@ is_deleted = 0 and project_id = #{projectId} + + and project_id in + + #{pid} + + and rater_id = #{raterId} and rater_role = #{raterRole} and remark = #{remark} diff --git a/ry-vue3/src/api/public.js b/ry-vue3/src/api/public.js index f6d33e6..24cfc6f 100644 --- a/ry-vue3/src/api/public.js +++ b/ry-vue3/src/api/public.js @@ -169,3 +169,14 @@ export function exportPublicityExecutionIntent(params) { export function exportPublicitySupportIntent(params) { return request.post('/business/publicitySupportIntent/export', null, { params, responseType: 'blob' }) } + +// ========== 项目导出 (manager/Projects.vue, 跟随当前筛选) ========== +export function exportProjectList(params) { + return request.post('/business/project/export', null, { params, responseType: 'blob' }) +} +export function exportProjectRating(params) { + return request.post('/business/project/ratingExport', null, { params, responseType: 'blob' }) +} +export function exportSignupExpert(params) { + return request.post('/business/project/signupExpertExport', null, { params, responseType: 'blob' }) +} diff --git a/ry-vue3/src/views/executor/Meetings.vue b/ry-vue3/src/views/executor/Meetings.vue index d4ec698..b46d088 100644 --- a/ry-vue3/src/views/executor/Meetings.vue +++ b/ry-vue3/src/views/executor/Meetings.vue @@ -89,8 +89,8 @@ import { stageLabel, STAGE_OPTIONS, stageClass } from '@/utils/meetingStage' // 与 manager/meetings/Meetings.vue 一致: 8 项筛选 (项目编号/会议ID/会议名称/期数/会议时间/项目形式/当前阶段/备注) const q = ref({ - projectNo: '', meetingId: '', meetingName: '', periodNo: null, - projectForm: '', currentStage: '', remark: '', + projectNo: '', projectId: '', meetingId: '', meetingName: '', periodNo: null, + projectForm: '', currentStage: '', currentStageNotIn: '', remark: '', startTime: '', endTime: '' }) const rows = ref([]) @@ -125,13 +125,15 @@ const route = useRoute() const router = useRouter() function readQueryFromRoute() { const q2 = route.query + if (q2.projectId != null && q2.projectId !== '') q.value.projectId = String(q2.projectId) if (q2.currentStage != null && q2.currentStage !== '') q.value.currentStage = String(q2.currentStage) + if (q2.currentStageNotIn != null && q2.currentStageNotIn !== '') q.value.currentStageNotIn = String(q2.currentStageNotIn) } function reset() { q.value = { - projectNo:'', meetingId:'', meetingName:'', periodNo:null, - projectForm:'', currentStage:'', remark:'', + projectNo:'', projectId:'', meetingId:'', meetingName:'', periodNo:null, + projectForm:'', currentStage:'', currentStageNotIn:'', remark:'', startTime:'', endTime:'' } page.pageNum = 1 diff --git a/ry-vue3/src/views/executor/Projects.vue b/ry-vue3/src/views/executor/Projects.vue index 8d0ba43..2783b25 100644 --- a/ry-vue3/src/views/executor/Projects.vue +++ b/ry-vue3/src/views/executor/Projects.vue @@ -50,10 +50,23 @@ - + + + + + + + - - @@ -196,6 +209,15 @@ function reset() { function viewDetail(row) { router.push(`/executor/projects/detail/${row.projectId}`) } +// 点击「总场次/已执行/未执行」列 → 跳 executor 会议列表, 按项目 + 阶段筛选 +// 数据权限完全交给会议列表后端: MAIN → execution_unit_id 本公司; SUB → create_by 本人, 跳转不新增任何越权参数 +function goMeetings(row, scope) { + const q = { projectId: row.projectId } + if (scope === 'done') q.currentStageNotIn = 'NOT_STARTED,IN_PROGRESS' + if (scope === 'todo') q.currentStage = 'NOT_STARTED' + router.push({ path: '/executor/meetings', query: q }) +} + async function loadStaff() { // 本 org 下所有执行方用户 (MAIN 管理员 + SUB 执行人员), 走 listExecutorPerson (后端强制 unit_type='executor' + 当前主账号隔离) // status='0': 只拉启用账号; 管理员 (MAIN) 一并返回但由前端置灰 (不可选) diff --git a/ry-vue3/src/views/manager/Projects.vue b/ry-vue3/src/views/manager/Projects.vue index 63e2ffa..36bb15d 100644 --- a/ry-vue3/src/views/manager/Projects.vue +++ b/ry-vue3/src/views/manager/Projects.vue @@ -60,23 +60,21 @@ - 查询重置 + + 查询 + 重置 + 导出 + 项目评价导出 + 已报名专家导出 +

新建项目 - - 项目分配{{ selection.length }} - - - 批量评分{{ selection.length }} - - - 批量结题{{ selection.length }} - - - 批量开通{{ selection.length }} - + 项目分配 + 批量评分 + 批量结题 + 批量开通 批量删除{{ selection.length }} @@ -96,9 +94,24 @@ {{ row.projectName }} - - - + + + + + + + + + @@ -273,7 +286,7 @@