feat: 项目导出 + 会议跳转 + 机构软删除 + 供应商账号同步
- 项目/项目评价/报名专家 3 类导出, 勾选透传 projectIds - 项目列表 总场次/已执行/未执行 三列点击跳对应会议列表 (executor 复用后端 role 隔离) - 机构软删除 + 级联软删机构下账号 - 供应商开放接口账号同步 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+26
-3
@@ -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<BizSignupExpertExportVo> 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<BizSignupExpertExportVo> util = new ExcelUtil<>(BizSignupExpertExportVo.class);
|
||||
|
||||
+25
@@ -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("劳务已提交, 不能修改参会人");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+26
-2
@@ -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<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("请先上传付款凭证");
|
||||
if (!hasLv && !hasSv) {
|
||||
throw new ServiceException("请先上传劳务凭证与会务凭证");
|
||||
}
|
||||
if (!hasLv) {
|
||||
throw new ServiceException("请先上传劳务凭证");
|
||||
}
|
||||
if (!hasSv) {
|
||||
throw new ServiceException("请先上传会务凭证");
|
||||
}
|
||||
|
||||
m.setIsSettled(1);
|
||||
|
||||
+164
@@ -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<BizProject> list = bizProjectService.selectList(bizProject);
|
||||
List<BizProjectExportVo> 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<BizProjectExportVo> 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<BizProject> projects = bizProjectService.selectList(bizProject);
|
||||
// projectId → project 反查表 (评分明细表 project_no/project_name 可能为空, 用主表补齐)
|
||||
Map<Long, BizProject> projectById = new HashMap<>();
|
||||
List<Long> projectIds = new ArrayList<>();
|
||||
for (BizProject p : projects) {
|
||||
if (p.getProjectId() != null) {
|
||||
projectIds.add(p.getProjectId());
|
||||
projectById.put(p.getProjectId(), p);
|
||||
}
|
||||
}
|
||||
List<BizProjectRating> ratings = new ArrayList<>();
|
||||
if (!projectIds.isEmpty()) {
|
||||
BizProjectRating q = new BizProjectRating();
|
||||
q.getParams().put("projectIds", projectIds);
|
||||
ratings = bizProjectRatingService.selectList(q);
|
||||
}
|
||||
List<BizProjectRatingExportVo> 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<BizProjectRatingExportVo> 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<BizProject> projects = bizProjectService.selectList(bizProject);
|
||||
List<String> projectNos = new ArrayList<>();
|
||||
for (BizProject p : projects) {
|
||||
if (p.getProjectNo() != null && !p.getProjectNo().isEmpty()) projectNos.add(p.getProjectNo());
|
||||
}
|
||||
List<BizExecutionIntent> intents = new ArrayList<>();
|
||||
if (!projectNos.isEmpty()) {
|
||||
BizExecutionIntent q = new BizExecutionIntent();
|
||||
q.getParams().put("projectNos", projectNos);
|
||||
intents = bizExecutionIntentService.selectList(q);
|
||||
}
|
||||
List<BizSignupExpertExportVo> 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<BizSignupExpertExportVo> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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<Long> projectIds;
|
||||
/** 支持单位筛选 (biz_org.org_id 列表, IN 查询, 多选 select 透传) */
|
||||
private List<Long> 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<Long> getProjectIds() { return projectIds; }
|
||||
public void setProjectIds(List<Long> projectIds) { this.projectIds = projectIds; }
|
||||
public List<Long> getSponsorOrgIds() { return sponsorOrgIds; }
|
||||
public void setSponsorOrgIds(List<Long> sponsorOrgIds) { this.sponsorOrgIds = sponsorOrgIds; }
|
||||
public String getExecOrgNames() { return execOrgNames; }
|
||||
|
||||
+105
@@ -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 列表列一致)
|
||||
*
|
||||
* <p>数据源: 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; }
|
||||
}
|
||||
+59
@@ -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 维度评分明细)
|
||||
*
|
||||
* <p>数据源: 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; }
|
||||
}
|
||||
+58
-12
@@ -7,32 +7,64 @@ import com.ruoyi.common.annotation.Excel;
|
||||
/**
|
||||
* 报名专家导出 VO (中文列头)
|
||||
*
|
||||
* <p>数据源: biz_execution_intent (公开门户"立即报名"写入的执行意向表, 即已报名专家).
|
||||
* <p>数据源: 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; }
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -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<Map<String, Object>> 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);
|
||||
}
|
||||
|
||||
+65
-8
@@ -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 分钟更新的账号, 解密后同步到执行方侧.
|
||||
* 供应商账号数据拉取调度器.
|
||||
* <p>
|
||||
* 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts}
|
||||
* 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回.
|
||||
* 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密.
|
||||
* <p>
|
||||
* 三个触发点:
|
||||
* 1. 启动后立即 (异步线程池) 全量拉 1 个月 ({@code full-pull-days}, 默认 30 天)
|
||||
* 2. 每分钟增量拉最近 5 分钟 ({@code incremental-minutes}, 默认 5)
|
||||
* 3. 每晚 2 点全量拉 1 个月 ({@code full-pull-days})
|
||||
* <p>
|
||||
* 解密后按邮箱 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);
|
||||
}
|
||||
}
|
||||
|
||||
-57
@@ -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<String>)
|
||||
List<SysUser> 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).
|
||||
*
|
||||
|
||||
+6
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+45
-9
@@ -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<BizOrg> 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);
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
<where>
|
||||
<if test="userId != null"> and user_id = #{userId}</if>
|
||||
<if test="projectNo != null and projectNo != ''"> and project_no = #{projectNo}</if>
|
||||
<if test="params.projectNos != null and params.projectNos.size() > 0">
|
||||
and project_no in
|
||||
<foreach collection="params.projectNos" item="no" open="(" separator="," close=")">
|
||||
#{no}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="projectName != null and projectName != ''"> and project_name = #{projectName}</if>
|
||||
<if test="name != null and name != ''"> and name = #{name}</if>
|
||||
<if test="department != null and department != ''"> and department = #{department}</if>
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
<where>
|
||||
is_deleted = 0
|
||||
<if test="projectNo != null and projectNo != ''">and project_no like concat('%', #{projectNo}, '%')</if>
|
||||
<if test="projectId != null">and project_id = #{projectId}</if>
|
||||
<if test="meetingId != null">and meeting_id = #{meetingId}</if>
|
||||
<if test="meetingName != null and meetingName != ''">and meeting_name like concat('%', #{meetingName}, '%')</if>
|
||||
<if test="periodNo != null">and period_no = #{periodNo}</if>
|
||||
|
||||
@@ -17,23 +17,27 @@
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="isSynced" column="is_synced" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizOrgResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where org_id = #{orgId}
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BizOrgResult" parameterType="BizOrg">
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
del_flag = '0'
|
||||
<if test="userId != null"> and user_id = #{userId}</if>
|
||||
<if test="orgType != null and orgType != ''"> and org_type = #{orgType}</if>
|
||||
<if test="orgName != null and orgName != ''"> and org_name like concat('%', #{orgName}, '%')</if>
|
||||
@@ -56,6 +60,7 @@
|
||||
<if test="contactPhone != null and contactPhone != ''">contact_phone,</if>
|
||||
<if test="intentCount != null">intent_count,</if>
|
||||
<if test="status != null and status != ''">status,</if>
|
||||
<if test="isSynced != null">is_synced,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
create_time,
|
||||
</trim>
|
||||
@@ -70,6 +75,7 @@
|
||||
<if test="contactPhone != null and contactPhone != ''">#{contactPhone},</if>
|
||||
<if test="intentCount != null">#{intentCount},</if>
|
||||
<if test="status != null and status != ''">#{status},</if>
|
||||
<if test="isSynced != null">#{isSynced},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
sysdate(),
|
||||
</trim>
|
||||
@@ -88,18 +94,29 @@
|
||||
<if test="contactPhone != null">contact_phone = #{contactPhone},</if>
|
||||
<if test="intentCount != null">intent_count = #{intentCount},</if>
|
||||
<if test="status != null and status != ''">status = #{status},</if>
|
||||
<if test="isSynced != null">is_synced = #{isSynced},</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
update_time = sysdate(),
|
||||
</trim>
|
||||
where org_id = #{orgId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPrimaryKeys" parameterType="Long">
|
||||
delete from biz_org where org_id in
|
||||
<update id="deleteByPrimaryKeys" parameterType="Long">
|
||||
update biz_org set del_flag = '1' where org_id in
|
||||
<foreach collection="array" item="orgId" open="(" separator="," close=")">
|
||||
#{orgId}
|
||||
</foreach>
|
||||
</delete>
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<!-- 级联软删机构下所有账号 (主账号 biz_org.user_id + 子账号 biz_person.user_id), 删除机构时调用 -->
|
||||
<update id="softDeleteUsersByOrgId" parameterType="Long">
|
||||
update sys_user set del_flag = '1'
|
||||
where del_flag = '0' and (
|
||||
user_id in (select user_id from biz_person where org_id = #{orgId})
|
||||
or user_id = (select user_id from biz_org where org_id = #{orgId})
|
||||
)
|
||||
</update>
|
||||
|
||||
<!--
|
||||
支持方下拉选项: JOIN sys_user 取主账号 user_name (供分配弹窗缓存 biz_project.sponsor_admin_user_name 用)
|
||||
@@ -116,6 +133,7 @@
|
||||
from biz_org o
|
||||
join sys_user u on u.user_id = o.user_id
|
||||
where o.org_type = 'sponsor'
|
||||
and o.del_flag = '0'
|
||||
and u.del_flag = '0'
|
||||
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
||||
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
@@ -141,6 +159,7 @@
|
||||
from biz_org o
|
||||
join sys_user u on u.user_id = o.user_id
|
||||
where o.org_type = 'executor'
|
||||
and o.del_flag = '0'
|
||||
and u.del_flag = '0'
|
||||
and u.parent_user_id is null
|
||||
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
||||
@@ -162,6 +181,7 @@
|
||||
from biz_org o
|
||||
where o.org_type = 'sponsor'
|
||||
and o.status = '0'
|
||||
and o.del_flag = '0'
|
||||
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||
order by o.org_id desc
|
||||
limit 20
|
||||
@@ -180,9 +200,10 @@
|
||||
case when p.user_id is null then 1 else 0 end as isOwner
|
||||
from sys_user u
|
||||
left join biz_person p on p.user_id = u.user_id and p.unit_type = 'sponsor'
|
||||
left join biz_org own on own.user_id = u.user_id and own.org_type = 'sponsor'
|
||||
left join biz_org own on own.user_id = u.user_id and own.org_type = 'sponsor' and own.del_flag = '0'
|
||||
left join biz_org o on o.org_id = COALESCE(own.org_id, p.org_id)
|
||||
where u.user_id = #{userId}
|
||||
and o.del_flag = '0'
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
@@ -194,7 +215,7 @@
|
||||
-->
|
||||
<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_org where user_id = #{userId} and del_flag = '0' limit 1),
|
||||
(select org_id from biz_person where user_id = #{userId} limit 1)
|
||||
)
|
||||
</select>
|
||||
@@ -203,7 +224,28 @@
|
||||
<select id="countByOrgName" parameterType="BizOrg" resultType="int">
|
||||
select count(*) from biz_org
|
||||
where org_type = #{orgType}
|
||||
and del_flag = '0'
|
||||
and trim(org_name) = #{orgName}
|
||||
<if test="orgId != null"> and org_id != #{orgId}</if>
|
||||
</select>
|
||||
|
||||
<!-- 供应商同步 org 去重: 按税号精确定位 (org_type='executor' 且税号非空), 取 org_id 最小的一条 -->
|
||||
<select id="selectByTaxNo" parameterType="BizOrg" resultMap="BizOrgResult">
|
||||
<include refid="selectFields"/>
|
||||
where org_type = 'executor'
|
||||
and del_flag = '0'
|
||||
and tax_no = #{taxNo}
|
||||
order by org_id asc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 供应商同步 org 去重兜底: 税号缺失时按企业名精确定位 (org_type='executor'), 取 org_id 最小的一条 -->
|
||||
<select id="selectByNameAndType" parameterType="BizOrg" resultMap="BizOrgResult">
|
||||
<include refid="selectFields"/>
|
||||
where org_type = 'executor'
|
||||
and del_flag = '0'
|
||||
and trim(org_name) = #{orgName}
|
||||
order by org_id asc
|
||||
limit 1
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<result property="projectId" column="project_id" />
|
||||
<result property="raterId" column="rater_id" />
|
||||
<result property="ratingTime" column="rating_time" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="isDeleted" column="is_deleted" />
|
||||
</resultMap>
|
||||
<sql id="selectFields">
|
||||
@@ -30,6 +31,12 @@
|
||||
<where>
|
||||
is_deleted = 0
|
||||
<if test="projectId != null"> and project_id = #{projectId}</if>
|
||||
<if test="params.projectIds != null and params.projectIds.size() > 0">
|
||||
and project_id in
|
||||
<foreach collection="params.projectIds" item="pid" open="(" separator="," close=")">
|
||||
#{pid}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="raterId != null"> and rater_id = #{raterId}</if>
|
||||
<if test="raterRole != null and raterRole != ''"> and rater_role = #{raterRole}</if>
|
||||
<if test="remark != null and remark != ''"> and remark = #{remark}</if>
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,10 +50,23 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总场次/总期数" width="130" align="center">
|
||||
<template #default="{ row }">{{ row.assignedSessions || 0 }}</template>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.assignedSessions" :underline="false" type="primary" @click="goMeetings(row, 'all')">{{ row.assignedSessions }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.doneSessions" :underline="false" type="primary" @click="goMeetings(row, 'done')">{{ row.doneSessions }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.todoSessions" :underline="false" type="primary" @click="goMeetings(row, 'todo')">{{ row.todoSessions }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
|
||||
<el-table-column label="总金额" width="120" align="right">
|
||||
<template #default="{ row }">¥ {{ formatNum(row.assignedAmount) }}</template>
|
||||
</el-table-column>
|
||||
@@ -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) 一并返回但由前端置灰 (不可选)
|
||||
|
||||
@@ -60,23 +60,21 @@
|
||||
<el-option label="已结算" value="Y" /><el-option label="未结算" value="N" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<el-button @click="doExport">导出</el-button>
|
||||
<el-button @click="doExportRating">项目评价导出</el-button>
|
||||
<el-button v-if="isManager" @click="doExportSignupExpert">已报名专家导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button class="action-btn" @click="openNewProject">新建项目</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openAssign">
|
||||
项目分配<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('score')">
|
||||
批量评分<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('close')">
|
||||
批量结题<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('activate')">
|
||||
批量开通<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
<el-button @click="openAssign">项目分配</el-button>
|
||||
<el-button @click="openBatch('score')">批量评分</el-button>
|
||||
<el-button @click="openBatch('close')">批量结题</el-button>
|
||||
<el-button @click="openBatch('activate')">批量开通</el-button>
|
||||
<el-button v-if="canDelete" class="action-btn secondary" type="danger" :disabled="!selection.length" @click="onBatchDelete">
|
||||
批量删除<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||
</el-button>
|
||||
@@ -96,9 +94,24 @@
|
||||
<el-link :underline="false" type="primary" @click="viewDetail(row)">{{ row.projectName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="130" align="center" />
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.totalSessions" :underline="false" type="primary" @click="goMeetings(row, 'all')">{{ row.totalSessions }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.doneSessions" :underline="false" type="primary" @click="goMeetings(row, 'done')">{{ row.doneSessions }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.todoSessions" :underline="false" type="primary" @click="goMeetings(row, 'todo')">{{ row.todoSessions }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="总金额" width="130" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="availableAmount" label="可用金额" width="130" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="paidLaborAmount" label="已支付劳务费" width="140" align="right" :formatter="fmtMoney" />
|
||||
@@ -273,7 +286,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
||||
import { bizList, bizAdd, bizUpdate, bizDelete, exportProjectList, exportProjectRating, exportSignupExpert } from '@/api/public'
|
||||
import { listExecutorOrgs, listSponsorOrgs } from '@/api/system'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -379,9 +392,63 @@ function readQueryFromRoute() {
|
||||
if (q2.isSettled != null && q2.isSettled !== '') q.value.isSettled = String(q2.isSettled)
|
||||
}
|
||||
|
||||
// 导出
|
||||
function exportProjects() { ElMessage.info('导出项目功能开发中') }
|
||||
function exportEval() { ElMessage.info('项目评价导出功能开发中') }
|
||||
// 仅 manager 可见「已报名专家导出」 (前端 v-if, 后端 controller 再兜底 role 校验)
|
||||
const isManager = computed(() => userStore.role === 'manager')
|
||||
|
||||
// ========== 导出 (跟随当前筛选, 与 load() 同一口径) ==========
|
||||
function buildExportParams() {
|
||||
// 勾选了项目 → 只导出勾选项; 否则按当前筛选导出全部
|
||||
if (selection.value.length > 0) {
|
||||
return { projectIds: selection.value.map(r => r.projectId) }
|
||||
}
|
||||
const params = { ...q.value }
|
||||
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
|
||||
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
|
||||
return params
|
||||
}
|
||||
function downloadBlob(res, filename) {
|
||||
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
function dateStamp() {
|
||||
const d = new Date()
|
||||
const p = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}`
|
||||
}
|
||||
async function doExport() {
|
||||
try {
|
||||
const res = await exportProjectList(buildExportParams())
|
||||
downloadBlob(res, `项目列表_${dateStamp()}.xlsx`)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '导出失败')
|
||||
}
|
||||
}
|
||||
async function doExportRating() {
|
||||
try {
|
||||
const res = await exportProjectRating(buildExportParams())
|
||||
downloadBlob(res, `项目评价_${dateStamp()}.xlsx`)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '导出失败')
|
||||
}
|
||||
}
|
||||
async function doExportSignupExpert() {
|
||||
try {
|
||||
const res = await exportSignupExpert(buildExportParams())
|
||||
downloadBlob(res, `报名专家_${dateStamp()}.xlsx`)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 单行操作按钮 handlers(弹 5 个独立 dialog) ==========
|
||||
function openClose(row) {
|
||||
@@ -481,6 +548,16 @@ function viewDetail(row) {
|
||||
router.push(`${base}/projects/detail/${row.projectId}`)
|
||||
}
|
||||
|
||||
// 点击「总场次/已执行/未执行」列 → 跳对应角色的会议列表, 按项目 + 阶段筛选 (会议列表后端按 role 隔离数据权限)
|
||||
// scope: 'all'(全部) | 'done'(已执行) | 'todo'(未执行)
|
||||
function goMeetings(row, scope) {
|
||||
const base = userStore.role === 'admin' ? '/admin' : '/manager'
|
||||
const q = { projectId: row.projectId }
|
||||
if (scope === 'done') q.currentStageNotIn = 'NOT_STARTED,IN_PROGRESS'
|
||||
if (scope === 'todo') q.currentStage = 'NOT_STARTED'
|
||||
router.push({ path: `${base}/meetings`, query: q })
|
||||
}
|
||||
|
||||
// 点击列表「执行单位评分(合规/支持)」列 → 只读显示该角色 4 维度评分明细
|
||||
async function openScoreDetail(row, role) {
|
||||
scoreDetailTitle.value = role === 'sponsor' ? '执行单位评分详情(支持方)' : '执行单位评分详情(合规)'
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
<!-- 参会人管理 (放在 劳务明细 + 劳务协议 上面, 无表题直接出表) -->
|
||||
<div class="attendee-section">
|
||||
<div v-if="!isSponsor && !isReadonly" class="attendee-toolbar">
|
||||
<el-button type="primary" size="small" :loading="attendeeLoading" @click="openAttendeeDialog()">+ 新增参会人</el-button>
|
||||
<el-button size="small" @click="openAttendeeImport">批量导入</el-button>
|
||||
<el-button type="primary" size="small" :loading="attendeeLoading" :disabled="!attendeeEditable" @click="openAttendeeDialog()">+ 新增参会人</el-button>
|
||||
<el-button size="small" :disabled="!attendeeEditable" @click="openAttendeeImport">批量导入</el-button>
|
||||
<el-button size="small" :disabled="!selectedAttendees.length" :loading="inviteSending" @click="onBatchInvite">邀请参会</el-button>
|
||||
<el-button size="small" :disabled="!selectedAttendees.length" :loading="esignSending" @click="onBatchEsign">推送电子签</el-button>
|
||||
<el-button size="small" :loading="exporting" @click="onExportAttendees">导出</el-button>
|
||||
@@ -79,7 +79,7 @@
|
||||
让内容超出时容器内出现横向滚动条, 不影响外层 grid 布局.
|
||||
-->
|
||||
<div class="attendee-table-wrap">
|
||||
<el-table :data="attendeeRows" v-loading="attendeeLoading" border size="small" style="width: 100%;" show-summary :summary-method="attendeeSummary" @selection-change="onAttendeeSelectionChange" class="attendee-table">
|
||||
<el-table :data="attendeeRows" v-loading="attendeeLoading" border size="small" style="width: 100%;" show-summary :summary-method="attendeeSummary" @selection-change="onAttendeeSelectionChange" class="attendee-table" :row-class-name="attendeeRowClass">
|
||||
<el-table-column v-if="!isSponsor && !isReadonly" type="selection" width="42" />
|
||||
<el-table-column type="index" label="序号" width="50" align="center" />
|
||||
<el-table-column label="医生" min-width="90">
|
||||
@@ -157,7 +157,7 @@
|
||||
<el-table-column v-if="!isSponsor && !isReadonly" label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-link :underline="false" type="primary" @click="openAttendeeDialog(row)">编辑</el-link>
|
||||
<el-link :underline="false" type="primary" :disabled="!attendeeEditable" @click="openAttendeeDialog(row)">编辑</el-link>
|
||||
<el-dropdown trigger="click" @command="(cmd) => onAttendeeCommand(cmd, row)">
|
||||
<el-link :underline="false" type="primary" :disabled="inviteLoadingId === row.id || esignLoadingId === row.id">
|
||||
更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
@@ -166,7 +166,7 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="invite" :disabled="inviteLoadingId === row.id">邀请参会</el-dropdown-item>
|
||||
<el-dropdown-item command="esign" :disabled="esignLoadingId === row.id">推送电子签</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided><span style="color: #f56c6c;">删除</span></el-dropdown-item>
|
||||
<el-dropdown-item command="delete" :disabled="!attendeeEditable" divided><span style="color: #f56c6c;">删除</span></el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -313,14 +313,14 @@
|
||||
<div :class="['timeline-item', eventStatus(ev)]">
|
||||
<div class="timeline-title">
|
||||
{{ stepLabel(ev.step) }}
|
||||
<span v-if="trackTag(ev)" class="track-tag">{{ trackTag(ev) }}</span>
|
||||
<span v-for="(l, i) in trackParts(ev)" :key="i" :class="['track-tag', l === '会务' ? 'track-service' : 'track-labor']">{{ l }}</span>
|
||||
</div>
|
||||
<div v-for="(e, i) in ev.entries" :key="i" class="timeline-meta">
|
||||
<span v-if="ev.entries.length > 1" class="track-mini">{{ e.label }}</span>
|
||||
<span v-if="ev.entries.length > 1" :class="['track-mini', e.label === '会务' ? 'track-service' : 'track-labor']">{{ e.label }}</span>
|
||||
<span>{{ e.auditor || '—' }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(e.auditTime) }}</span>
|
||||
<el-tag v-if="e.auditResult" size="small" :type="e.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ e.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
<el-tag v-if="e.auditResult" size="small" effect="dark" :type="e.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ e.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</div>
|
||||
<div v-for="(e, i) in ev.entries" :key="'op' + i">
|
||||
<div v-if="e.opinion" :class="['timeline-opinion', e.auditResult === 'REJECTED' ? 'opinion-reject' : 'opinion-approve']">💬 {{ e.opinion }}</div>
|
||||
@@ -330,7 +330,7 @@
|
||||
<div v-if="pendingNode" class="timeline-item pending">
|
||||
<div class="timeline-title">
|
||||
{{ stepLabel(pendingNode.step) }}
|
||||
<span v-if="trackTag(pendingNode)" class="track-tag">{{ trackTag(pendingNode) }}</span>
|
||||
<span v-for="(l, i) in trackParts(pendingNode)" :key="i" :class="['track-tag', l === '会务' ? 'track-service' : 'track-labor']">{{ l }}</span>
|
||||
</div>
|
||||
<div class="timeline-desc pending-text">{{ pendingDesc(pendingNode.step) }}</div>
|
||||
</div>
|
||||
@@ -385,7 +385,7 @@
|
||||
|
||||
<!-- 参会人 dialog (新增/编辑通用, 2 列布局) -->
|
||||
<el-dialog v-model="attendeeDialog.show" :title="attendeeDialog.title" width="760px" append-to-body :close-on-click-modal="false" @closed="resetAttendeeForm">
|
||||
<el-form ref="attendeeFormRef" :model="attendeeDialog.form" :rules="attendeeRules" label-width="100px" class="attendee-form-2col">
|
||||
<el-form ref="attendeeFormRef" :model="attendeeDialog.form" label-width="100px" class="attendee-form-2col">
|
||||
<!-- Col 1: 基础档案 (8 字段) -->
|
||||
<el-form-item label="联系方式" required>
|
||||
<el-input v-model="attendeeDialog.form.phone" :disabled="attendeeDialog.editing" placeholder="11位手机号" maxlength="11">
|
||||
@@ -443,8 +443,10 @@
|
||||
<el-form-item label="增值税附加">
|
||||
<el-input-number v-model="attendeeDialog.form.vatAndSurcharge" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="实发金额" prop="fee">
|
||||
<el-input-number v-model="attendeeDialog.form.fee" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="实发金额">
|
||||
<div :class="{ 'fee-warn': feeDeviates }" style="width:100%">
|
||||
<el-input-number v-model="attendeeDialog.form.fee" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="摘要">
|
||||
<el-input v-model="attendeeDialog.form.summary" placeholder="备注/说明" maxlength="500" show-word-limit />
|
||||
@@ -773,20 +775,9 @@ function stepLabel(step) {
|
||||
if (step === 'compliance') return '合规审核'
|
||||
return '监察意见'
|
||||
}
|
||||
/** 节点右侧轨标注: 提交节点带轮次 (劳务·第1次), 审核节点只标轨 (劳务·会务). */
|
||||
function trackTag(node) {
|
||||
const labels = node.entries.map(e => e.label).filter(Boolean)
|
||||
if (!labels.length) return ''
|
||||
if (node.step === 'submit') {
|
||||
const withRound = node.entries.filter(e => e.roundNo != null)
|
||||
if (withRound.length) {
|
||||
if (withRound.length === node.entries.length && withRound.every(e => e.roundNo === withRound[0].roundNo)) {
|
||||
return `${labels.join('·')} · 第${withRound[0].roundNo}次`
|
||||
}
|
||||
return node.entries.map(e => (e.roundNo != null ? `${e.label}·第${e.roundNo}次` : e.label)).filter(Boolean).join(' · ')
|
||||
}
|
||||
}
|
||||
return labels.join('·')
|
||||
/** 节点右侧轨标注: 每轨一个徽标 (劳务=primary, 会务=warning). */
|
||||
function trackParts(node) {
|
||||
return (node.entries || []).map(e => e && e.label).filter(Boolean)
|
||||
}
|
||||
function eventStatus(ev) {
|
||||
return ev.entries.some(e => e.auditResult === 'REJECTED') ? 'rejected' : 'done'
|
||||
@@ -836,6 +827,12 @@ const frozen = computed(() => isOne(row.value.isFrozen))
|
||||
/** 单轨是否处于「可提交/可编辑」态: 执行方仅未提交/被驳回可编辑; 合规(manager)/管理员 永远可编辑 */
|
||||
const laborEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
||||
const serviceEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
||||
// 参会人名单是否可改 (新增/导入/编辑/删除): sponsor/只读不可; executor 劳务提交后(SUBMITTED/APPROVED)锁定, 未提交/退回可改; manager/admin 恒可改
|
||||
const attendeeEditable = computed(() => {
|
||||
if (isSponsor.value || isReadonly.value) return false
|
||||
if (isExecutor.value) return ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage)
|
||||
return true
|
||||
})
|
||||
/** 单轨状态判定: C0=已提交待合规审 / C1=已提交待支持方审 */
|
||||
function isC0(stage, compliance) { return stage === 'SUBMITTED' && !isOne(compliance) }
|
||||
function isC1(stage, compliance) { return stage === 'SUBMITTED' && isOne(compliance) }
|
||||
@@ -1022,24 +1019,28 @@ function roleAmountSum(laborForm) {
|
||||
return result
|
||||
}
|
||||
|
||||
/** 实发金额校验: 不超所选角色的劳务金额合计 (未选角色时跳过) */
|
||||
function validateFee(rule, value, callback) {
|
||||
/** 实发金额是否偏离配置值 (所选角色劳务金额合计); 少于或多于都算, 未选/未匹配到配置不提示 */
|
||||
const feeDeviates = computed(() => {
|
||||
const laborForm = attendeeDialog.value.form.laborForm
|
||||
if (!laborForm || !String(laborForm).trim()) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const { sum } = roleAmountSum(laborForm)
|
||||
const fee = Number(value) || 0
|
||||
if (fee > sum) {
|
||||
callback(new Error(`实发金额不能超过角色金额 ${sum.toFixed(2)} 元`))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
if (!laborForm || !String(laborForm).trim()) return false
|
||||
const { sum, matchedAny } = roleAmountSum(laborForm)
|
||||
if (!matchedAny) return false
|
||||
const fee = Number(attendeeDialog.value.form.fee) || 0
|
||||
return Number(fee.toFixed(2)) !== Number(sum.toFixed(2))
|
||||
})
|
||||
|
||||
/** 参会人列表行: 实发金额偏离角色劳务配置值 (少/多都算), 未选/未匹配到配置不提示 */
|
||||
function attendeeFeeDeviates(row) {
|
||||
if (!row || !row.laborForm || !String(row.laborForm).trim()) return false
|
||||
const { sum, matchedAny } = roleAmountSum(row.laborForm)
|
||||
if (!matchedAny) return false
|
||||
const fee = Number(row.fee) || 0
|
||||
return Number(fee.toFixed(2)) !== Number(sum.toFixed(2))
|
||||
}
|
||||
|
||||
const attendeeRules = {
|
||||
fee: [{ validator: validateFee, trigger: ['blur', 'change'] }]
|
||||
/** el-table row-class-name: 偏离行加 attendee-fee-warn 淡红背景 */
|
||||
function attendeeRowClass({ row }) {
|
||||
return attendeeFeeDeviates(row) ? 'attendee-fee-warn' : ''
|
||||
}
|
||||
|
||||
// ===================== 金额联动 (照搬 hwt guest.vue 单向链) =====================
|
||||
@@ -1824,6 +1825,16 @@ function submitOcrForMaterials(items) {
|
||||
/** 提交前先 saveMaterials() 落库: 后端 submit-material 校验的是 biz_meeting_material 表 */
|
||||
async function onSubmitMaterials(types) {
|
||||
if (!types || !types.length) { ElMessage.warning('无可提交的材料轨'); return }
|
||||
// 有「已上传未保存」的改动 → 先保存再提交 (与结算同款拦截)
|
||||
if (hasUnsavedChanges.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'检测到尚未保存的上传内容,是否先保存再提交?',
|
||||
'未保存的改动',
|
||||
{ type: 'warning', confirmButtonText: '保存并提交', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch { return }
|
||||
}
|
||||
busy.value.submitMaterial = true
|
||||
try {
|
||||
await saveMaterials()
|
||||
@@ -2122,8 +2133,12 @@ onBeforeUnmount(stopFeePolling)
|
||||
.track-block:last-child { margin-bottom: 0; }
|
||||
.track-label-row { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
|
||||
.cycle-no { font-size: 11px; color: #909399; }
|
||||
.track-tag { display: inline-block; margin-left: 6px; font-size: 11px; font-weight: 400; color: #909399; background: #f5f7fa; border-radius: 3px; padding: 1px 6px; line-height: 1.6; vertical-align: middle; }
|
||||
.track-mini { display: inline-block; min-width: 34px; font-size: 11px; color: #909399; }
|
||||
.track-tag { display: inline-block; margin-left: 6px; font-size: 11px; font-weight: 400; border-radius: 3px; padding: 1px 6px; line-height: 1.6; vertical-align: middle; }
|
||||
.track-tag.track-labor { color: var(--brand-primary); background: var(--brand-primary-mix); }
|
||||
.track-tag.track-service { color: var(--el-color-warning); background: var(--el-color-warning-light-9); }
|
||||
.track-mini { display: inline-block; min-width: 34px; font-size: 11px; }
|
||||
.track-mini.track-labor { color: var(--brand-primary); }
|
||||
.track-mini.track-service { color: var(--el-color-warning); }
|
||||
.timeline-opinion { margin-top: 4px; font-size: 12px; padding: 4px 8px; border-radius: 3px; line-height: 1.5; word-break: break-all; }
|
||||
.timeline-opinion.opinion-reject { color: #f56c6c; background: #fef0f0; }
|
||||
.timeline-opinion.opinion-approve { color: #389e0d; background: #f6ffed; }
|
||||
@@ -2143,12 +2158,17 @@ onBeforeUnmount(stopFeePolling)
|
||||
.attendee-form-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px; }
|
||||
.attendee-form-2col :deep(.el-form-item) { margin-bottom: 12px; }
|
||||
.attendee-form-2col :deep(.el-form-item__content) { min-width: 0; }
|
||||
/* 实发金额偏离配置值 → 输入框红框软提示 (不拦截录入). */
|
||||
.fee-warn :deep(.el-input__wrapper) { box-shadow: 0 0 0 1px #f56c6c inset !important; }
|
||||
/* 手机号放大镜 suffix: 可点 + hover 高亮 */
|
||||
.phone-lookup { cursor: pointer; color: var(--el-text-color-secondary); transition: color 0.2s; }
|
||||
.phone-lookup:hover { color: var(--brand-primary, #409eff); }
|
||||
/* 表格外层横向滚动容器: 内容总宽 ~1680px 超出 left-col 宽度时, 容器内出现横向滚动条, 不撑爆外层 grid */
|
||||
.attendee-table-wrap { overflow-x: auto; max-width: 100%; min-width: 0; }
|
||||
.row-actions { display: inline-flex; align-items: center; gap: 4px; }
|
||||
/* 参会人实发金额偏离角色配置值 → 整行淡红背景 (软提示, 不拦截); hover 时加深保持反馈 */
|
||||
.attendee-table :deep(tr.attendee-fee-warn > td.el-table__cell) { background-color: #fef0f0 !important; }
|
||||
.attendee-table :deep(tr.attendee-fee-warn:hover > td.el-table__cell) { background-color: #fde2e2 !important; }
|
||||
|
||||
|
||||
/* ========================================
|
||||
|
||||
@@ -110,7 +110,9 @@ const snap = reactive({
|
||||
totalSessions: '',
|
||||
sponsorOrgName: '',
|
||||
createUserName: '',
|
||||
createTime: ''
|
||||
createTime: '',
|
||||
projectStartTime: '',
|
||||
projectEndTime: ''
|
||||
})
|
||||
|
||||
// 期数上限 = 总场次/总期数 (executor 快照 totalSessions 已换成分配给本机构的场次口径)
|
||||
@@ -151,6 +153,8 @@ async function loadProjectSnapshot(pid) {
|
||||
snap.sponsorOrgName = d.sponsorOrgName || ''
|
||||
snap.createUserName = d.createUserName || ''
|
||||
snap.createTime = d.createTime || ''
|
||||
snap.projectStartTime = d.startTime || ''
|
||||
snap.projectEndTime = d.endTime || ''
|
||||
// executor: "总场次" = 分配给本执行方(公司)的场次, 不是项目总场次
|
||||
if (roleSegment.value === 'executor') {
|
||||
try {
|
||||
@@ -196,6 +200,17 @@ async function onSave() {
|
||||
ElMessage.error('会议开始时间不能晚于会议结束时间')
|
||||
return
|
||||
}
|
||||
// 会议时间必须在项目起止时间区间内 (闭区间; 项目未设起止则跳过对应边界)
|
||||
if (snap.projectStartTime && form.startTime
|
||||
&& new Date(form.startTime.replace(' ', 'T')) < new Date(snap.projectStartTime.replace(' ', 'T'))) {
|
||||
ElMessage.error('会议开始时间不能早于项目开始时间')
|
||||
return
|
||||
}
|
||||
if (snap.projectEndTime && form.endTime
|
||||
&& new Date(form.endTime.replace(' ', 'T')) > new Date(snap.projectEndTime.replace(' ', 'T'))) {
|
||||
ElMessage.error('会议结束时间不能晚于项目结束时间')
|
||||
return
|
||||
}
|
||||
// 期数不得高于总场次/总期数 (executor 快照 totalSessions 已换成分配给本机构的场次口径)
|
||||
const pn = Number(form.periodNo)
|
||||
const total = Number(snap.totalSessions)
|
||||
|
||||
@@ -176,7 +176,7 @@ const newRouteName = computed(() => isAdmin.value ? 'admin-meetings-new' : 'mana
|
||||
|
||||
// ========== 筛选 (按实际 8 项: 项目编号/会议ID/会议名称/第?期/会议时间/项目形式/当前阶段/备注) ==========
|
||||
const q = ref({
|
||||
projectNo: '', meetingId: '', meetingName: '', periodNo: null,
|
||||
projectNo: '', projectId: '', meetingId: '', meetingName: '', periodNo: null,
|
||||
projectForm: '', currentStage: '', currentStageNotIn: '', remark: '',
|
||||
startTime: '', endTime: ''
|
||||
})
|
||||
@@ -214,7 +214,7 @@ async function load() {
|
||||
}
|
||||
function reset() {
|
||||
q.value = {
|
||||
projectNo:'', meetingId:'', meetingName:'', periodNo:null,
|
||||
projectNo:'', projectId:'', meetingId:'', meetingName:'', periodNo:null,
|
||||
projectForm:'', currentStage:'', currentStageNotIn:'', remark:'',
|
||||
startTime:'', endTime:''
|
||||
}
|
||||
@@ -226,6 +226,7 @@ function reset() {
|
||||
// 只读不改 URL — KPI 是 source of truth, 列表页内 reset()/search 不反向写 URL
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user