diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml index aec7500..a2cd81a 100644 --- a/ry-api/ruoyi-admin/src/main/resources/application.yml +++ b/ry-api/ruoyi-admin/src/main/resources/application.yml @@ -8,6 +8,8 @@ ruoyi: copyrightYear: 2026 # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) profile: D:/ruoyi/uploadPath + # 医生劳务协议签署 — token 签名密钥 (HMAC-SHA1) + sign-token-secret: hwt-sign-secret-2026 # 获取ip地址开关 addressEnabled: false # 验证码类型 math 数字计算 char 字符验证 diff --git a/ry-api/ruoyi-business/pom.xml b/ry-api/ruoyi-business/pom.xml index b21ee2e..4bbce92 100644 --- a/ry-api/ruoyi-business/pom.xml +++ b/ry-api/ruoyi-business/pom.xml @@ -39,6 +39,12 @@ aliyun-java-sdk-core 4.6.4 + + + com.itextpdf + html2pdf + 3.0.2 + diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java index 0ae0088..a52f228 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java @@ -131,9 +131,14 @@ public class BizAuthController extends BaseController { return error("验证码错误或已过期: " + e.getMessage()); } - // 3. 查用户 (mapper.checkPhoneUnique 返回 SysUser 或 null) - SysUser user = userMapper.checkPhoneUnique(phone); - if (user == null || user.getUserId() == null) { + // 3. 查用户 (checkPhoneUnique 只 select user_id+phonenumber, 缺 role_type/user_name/status, + // 需再按 userId 查完整 user, 否则 LoginUser 里 roleType=null 导致前端拿不到角色而跳回登录页) + SysUser probe = userMapper.checkPhoneUnique(phone); + if (probe == null || probe.getUserId() == null) { + return error("该手机号未注册"); + } + SysUser user = userMapper.selectUserById(probe.getUserId()); + if (user == null) { return error("该手机号未注册"); } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java index 27972dc..2f1b6be 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java @@ -3,7 +3,6 @@ package com.ruoyi.business.controller; import java.util.List; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.annotation.Log; @@ -15,6 +14,7 @@ import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.domain.dto.ImportResult; import com.ruoyi.business.domain.vo.BizExpertExportVo; import com.ruoyi.business.domain.vo.BizExpertImportVo; import com.ruoyi.business.service.IBizExpertService; @@ -98,7 +98,7 @@ public class BizExpertController extends BaseController * 导出专家列表 (中文列头) */ @Log(title = "专家", businessType = BusinessType.EXPORT) - @PreAuthorize("@ss.hasPermi('business:expert:export')") + // 注: 不加 @PreAuthorize, 与本 controller 其他 CRUD 端点一致 — manager 是业务角色, 不走 RuoYi RBAC 权限 @PostMapping("/export") public void export(HttpServletResponse response, BizExpert bizExpert) { @@ -116,8 +116,7 @@ public class BizExpertController extends BaseController v.setIdCard(e.getIdCard()); v.setBankCard(e.getBankCard()); v.setBankName(e.getBankName()); - v.setBankProvince(e.getBankProvince()); - v.setBankCity(e.getBankCity()); + v.setBankRegion(e.getBankRegion()); v.setBankAddress(e.getBankAddress()); v.setStatus(e.getStatus()); v.setAuditStatus(e.getAuditStatus()); @@ -141,19 +140,22 @@ public class BizExpertController extends BaseController } /** - * 批量导入专家 (Excel), 参考 RuoYi 系统用户导入模式 - * updateSupport=true: 同一手机号已存在 → 跳过 (按成功计) - * 返回 message 含成功/失败计数 + 失败明细 + * 批量导入专家 (3 阶段批量): + *
    + *
  1. Phase A: IN 查 sys_user (100/批), 记录已存在 phone
  2. + *
  3. Phase B: batch insert sys_user (新 phone) (100/批)
  4. + *
  5. Phase C: batch insert biz_expert (100/批)
  6. + *
+ * updateSupport: 当前实现固定绑定已存在 phone → user_id (不再"跳过", 复用既有账号) + * 返回 {@link ImportResult} 含成功/失败计数 + 失败明细 (行号 + 原因) */ @Log(title = "专家", businessType = BusinessType.IMPORT) - @PreAuthorize("@ss.hasPermi('business:expert:import')") + // 注: 不加 @PreAuthorize, 与本 controller 其他 CRUD 端点一致 — manager 是业务角色, 不走 RuoYi RBAC 权限 @PostMapping("/importData") public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { - ExcelUtil util = new ExcelUtil(BizExpertImportVo.class); - List importList = util.importExcel(file.getInputStream()); String operName = SecurityUtils.getUsername(); - String message = bizExpertService.importExpert(importList, updateSupport, operName); - return success(message); + ImportResult result = bizExpertService.importExpert(file, updateSupport, operName); + return success(result); } } 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 dd8e359..db873cd 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,6 +1,8 @@ package com.ruoyi.business.controller; import java.util.List; + +import com.ruoyi.business.service.IBizMeetingAttendeeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import com.ruoyi.common.annotation.Log; @@ -38,17 +40,32 @@ public class BizMeetingController extends BaseController { return success(bizMeetingService.getById(meetingId)); } + @Autowired + private IBizMeetingAttendeeService attendeeService; + @Log(title = "会议", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody BizMeeting bizMeeting) { - return toAjax(bizMeetingService.insert(bizMeeting)); + int rows = bizMeetingService.insert(bizMeeting); + // 同步创建参会人中间表 (可选: 前端传 attendeeUserIds) + Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds(); + if (attendeeUserIds != null && attendeeUserIds.length > 0) { + attendeeService.insertBatch(bizMeeting.getMeetingId(), attendeeUserIds); + } + return toAjax(rows); } @Log(title = "会议", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody BizMeeting bizMeeting) { - return toAjax(bizMeetingService.updateByPrimaryKey(bizMeeting)); + int rows = bizMeetingService.updateByPrimaryKey(bizMeeting); + // 同步追加参会人 (不去重, 由前端控制) + Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds(); + if (attendeeUserIds != null && attendeeUserIds.length > 0) { + attendeeService.insertBatch(bizMeeting.getMeetingId(), attendeeUserIds); + } + return toAjax(rows); } @Log(title = "会议", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java index 56e0f63..e40d5a6 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java @@ -1,6 +1,8 @@ package com.ruoyi.business.controller; +import java.util.ArrayList; import java.util.List; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import com.ruoyi.common.annotation.Log; @@ -9,9 +11,12 @@ import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.business.domain.BizProject; import com.ruoyi.business.domain.BizPublicityExecutionIntent; import com.ruoyi.business.domain.BizPublicitySupportIntent; +import com.ruoyi.business.domain.vo.BizPublicityExecutionIntentExportVo; +import com.ruoyi.business.domain.vo.BizPublicitySupportIntentExportVo; import com.ruoyi.business.service.IBizProjectService; import com.ruoyi.business.service.IBizPublicityExecutionIntentService; import com.ruoyi.business.service.IBizPublicitySupportIntentService; @@ -205,4 +210,60 @@ public class BizPublicityIntentController extends BaseController { public AjaxResult removeExecution(@PathVariable Long[] ids) { return toAjax(executionIntentService.deleteByPrimaryKeys(ids)); } + + // ============== 导出 (后端 ExcelUtil 写 .xlsx) ============== + + /** + * 导出执行意向 (按页面筛选条件透传, 无 startPage, 全量导出) + * POST /business/publicityExecutionIntent/export?projectNo=&name=&... + */ + @Log(title = "导出执行意向", businessType = BusinessType.EXPORT) + @PostMapping("/business/publicityExecutionIntent/export") + public void exportExecution(HttpServletResponse response, BizPublicityExecutionIntent intent) { + List list = executionIntentService.selectList(intent); + List exportList = new ArrayList<>(list.size()); + for (BizPublicityExecutionIntent e : list) { + BizPublicityExecutionIntentExportVo v = new BizPublicityExecutionIntentExportVo(); + v.setProjectNo(e.getProjectNo()); + v.setProjectName(e.getProjectName()); + v.setName(e.getName()); + v.setWorkUnit(e.getWorkUnit()); + v.setDepartment(e.getDepartment()); + v.setPosition(e.getPosition()); + v.setPhone(e.getPhone()); + v.setUserStatus(e.getUserId() != null ? "存在" : "不存在"); + v.setIntentStatus(e.getIntentStatus() == null || e.getIntentStatus().isEmpty() ? "待审核" : e.getIntentStatus()); + v.setCreateTime(e.getCreateTime()); + exportList.add(v); + } + ExcelUtil util = new ExcelUtil<>(BizPublicityExecutionIntentExportVo.class); + util.exportExcel(response, exportList, "执行意向"); + } + + /** + * 导出支持意向 + * POST /business/publicitySupportIntent/export + */ + @Log(title = "导出支持意向", businessType = BusinessType.EXPORT) + @PostMapping("/business/publicitySupportIntent/export") + public void exportSupport(HttpServletResponse response, BizPublicitySupportIntent intent) { + List list = supportIntentService.selectList(intent); + List exportList = new ArrayList<>(list.size()); + for (BizPublicitySupportIntent e : list) { + BizPublicitySupportIntentExportVo v = new BizPublicitySupportIntentExportVo(); + v.setProjectNo(e.getProjectNo()); + v.setProjectName(e.getProjectName()); + v.setName(e.getName()); + v.setWorkUnit(e.getWorkUnit()); + v.setDepartment(e.getDepartment()); + v.setPosition(e.getPosition()); + v.setPhone(e.getPhone()); + v.setUserStatus(e.getUserId() != null ? "存在" : "不存在"); + v.setIntentStatus(e.getIntentStatus() == null || e.getIntentStatus().isEmpty() ? "待审核" : e.getIntentStatus()); + v.setCreateTime(e.getCreateTime()); + exportList.add(v); + } + ExcelUtil util = new ExcelUtil<>(BizPublicitySupportIntentExportVo.class); + util.exportExcel(response, exportList, "支持意向"); + } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSignController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSignController.java new file mode 100644 index 0000000..64929f4 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSignController.java @@ -0,0 +1,71 @@ +package com.ruoyi.business.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.business.domain.BizMeetingAttendee; +import com.ruoyi.business.service.BizSignService; + +/** + * 医生劳务协议签署 Controller + * 简化版: 不用 token, 走 SecurityUtils.getUserId() 识别医生 (用户登录后访问) + * + * 接口: + * - GET /business/sign/info?attendeeId=X 拉填写页所有数据 + * - POST /business/sign/saveProfile 保存医生填的字段 + * - GET /business/sign/contract?attendeeId=X 拉完整协议 HTML + * - POST /business/sign/submit 提交签字 + */ +@RestController +@RequestMapping("/business/sign") +public class BizSignController extends BaseController { + + @Autowired + private BizSignService signService; + + /** 医生填写页 GET /info?attendeeId=X */ + @GetMapping("/info") + public AjaxResult getSignInfo(@RequestParam("attendeeId") Long attendeeId) { + return success(signService.getSignInfo(attendeeId)); + } + + /** 医生点 "保存" / "下一步" 后调 */ + @PostMapping("/saveProfile") + public AjaxResult saveProfile(@RequestParam("attendeeId") Long attendeeId, + @RequestBody BizMeetingAttendee form) { + signService.saveProfile(attendeeId, form); + return success("已保存"); + } + + /** 拉完整协议 HTML (占位符已替换) */ + @GetMapping("/contract") + public AjaxResult getContractHtml(@RequestParam("attendeeId") Long attendeeId) { + // 注意: 不能用 success(String) (会把 HTML 当 msg), 必须显式传 data + return AjaxResult.success("操作成功", signService.getContractHtml(attendeeId)); + } + + /** 医生点 "提交" 签字 */ + @PostMapping("/submit") + public AjaxResult submitSign(@RequestParam("attendeeId") Long attendeeId, + @RequestBody java.util.Map body, + @RequestHeader(value = "X-Real-IP", required = false) String realIp) { + String handsign = body.get("handsign"); + String contentHtml = body.get("contentHtml"); + String signedIp = (realIp != null && !realIp.isEmpty()) ? realIp : currentRequestIp(); + java.util.Map pdfUrls = signService.submitSign(attendeeId, handsign, contentHtml, signedIp); + return success(pdfUrls); + } + + private String currentRequestIp() { + try { + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attrs != null && attrs.getRequest() != null) { + return attrs.getRequest().getRemoteAddr(); + } + } catch (Exception ignored) {} + return "unknown"; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java index 5cf2476..4b284ce 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java @@ -66,16 +66,12 @@ public class BizExpert extends BaseEntity { private String bankCard; /** 银行名称 */ private String bankName; - /** 开户行省份 */ - private String bankProvince; - /** 开户行城市 */ - private String bankCity; + /** 开户行省/市 (1-3 段, / 分隔, 直辖市省=市) */ + private String bankRegion; /** 开户行地址 */ private String bankAddress; - /** 身份证正面URL */ - private String idCardFrontUrl; - /** 身份证反面URL */ - private String idCardBackUrl; + /** 身份证正面/反面 URL (CSV, 逗号分隔, 空段保留位) */ + private String idCardAttachments; /** 审核意见 */ private String auditOpinion; /** 状态 0正常 1禁用 */ @@ -124,14 +120,10 @@ public class BizExpert extends BaseEntity { public void setBankCard(String bankCard) { this.bankCard = bankCard; } public String getBankName() { return bankName; } public void setBankName(String bankName) { this.bankName = bankName; } - public String getBankProvince() { return bankProvince; } - public void setBankProvince(String bankProvince) { this.bankProvince = bankProvince; } - public String getBankCity() { return bankCity; } - public void setBankCity(String bankCity) { this.bankCity = bankCity; } + 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 String getIdCardFrontUrl() { return idCardFrontUrl; } - public void setIdCardFrontUrl(String idCardFrontUrl) { this.idCardFrontUrl = idCardFrontUrl; } - public String getIdCardBackUrl() { return idCardBackUrl; } - public void setIdCardBackUrl(String idCardBackUrl) { this.idCardBackUrl = idCardBackUrl; } + public String getIdCardAttachments() { return idCardAttachments; } + public void setIdCardAttachments(String idCardAttachments) { this.idCardAttachments = idCardAttachments; } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java index 641a428..cb0d2be 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java @@ -78,6 +78,8 @@ public class BizMeeting extends BaseEntity { private String laborSigned; /** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */ private transient Long userId; + /** 新增/编辑会议时传入, 自动批量写入 biz_meeting_attendee 中间表 (非持久化) */ + private Long[] attendeeUserIds; public Long getMeetingId() { return meetingId; } public void setMeetingId(Long meetingId) { this.meetingId = meetingId; } public String getProjectNo() { return projectNo; } @@ -112,6 +114,9 @@ public class BizMeeting extends BaseEntity { public void setProjectName(String projectName) { this.projectName = projectName; } public String getOrgName() { return orgName; } public void setOrgName(String orgName) { this.orgName = orgName; } + private String address; + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } public String getSupervisionOpinion() { return supervisionOpinion; } public void setSupervisionOpinion(String supervisionOpinion) { this.supervisionOpinion = supervisionOpinion; } public String getSupervisionBy() { return supervisionBy; } @@ -126,4 +131,6 @@ public class BizMeeting extends BaseEntity { public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } + public Long[] getAttendeeUserIds() { return attendeeUserIds; } + public void setAttendeeUserIds(Long[] attendeeUserIds) { this.attendeeUserIds = attendeeUserIds; } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java index b1962a7..fb48e96 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java @@ -1,5 +1,6 @@ package com.ruoyi.business.domain; +import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import com.ruoyi.common.annotation.Excel; @@ -7,7 +8,7 @@ import com.ruoyi.common.core.domain.BaseEntity; /** * 会议参会人 (biz_meeting_attendee 中间表) - * 用于按 user_id 过滤"我参加的会议", 替代在 biz_meeting 上加冗余字段 + * 用于按 user_id 过滤"我参加的会议" + 医生签字劳务协议的快照数据 */ public class BizMeetingAttendee extends BaseEntity { private static final long serialVersionUID = 1L; @@ -17,10 +18,36 @@ public class BizMeetingAttendee extends BaseEntity { private Long meetingId; /** user_id (FK sys_user.user_id) */ private Long userId; + /* ===== 签字快照字段 (从 biz_expert 预填,医生可改) ===== */ + private String name; + private String phone; + private String workUnit; + private String department; + private String title; + private String idCard; + private String bankCard; + private String bankName; + private String bankBranch; + /** 开户行省/市 (1-3 段, / 分隔, 直辖市省=市) */ + private String bankRegion; + private String bankAddress; + private String accountName; + private String idCardAttachments; + /* ===== 劳务信息 (admin 预填,前端只读) ===== */ + private String laborForm; + private BigDecimal feePreTax; + private BigDecimal tax; + private BigDecimal fee; + /* ===== 审计 + 签字结果 ===== */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date signedAt; + private String signedIp; /** 手写签名 Base64 (longtext) — 由前端手写板生成 */ private String handsign; /** 劳务协议 URL (OSS) */ private String laborProtocol; + /** 脱敏版协议 URL (身份证/银行卡用 * 替换) */ + private String laborProtocolMasked; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; /* ====== 非持久化字段, 用于 selectUnsignedByUserId 联表查询 ====== */ @@ -29,16 +56,57 @@ public class BizMeetingAttendee extends BaseEntity { private transient Date endTime; private transient String projectName; private transient String projectNo; + public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getMeetingId() { return meetingId; } public void setMeetingId(Long meetingId) { this.meetingId = meetingId; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getWorkUnit() { return workUnit; } + public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String 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 String getAccountName() { return accountName; } + public void setAccountName(String accountName) { this.accountName = accountName; } + public String getIdCardAttachments() { return idCardAttachments; } + public void setIdCardAttachments(String idCardAttachments) { this.idCardAttachments = idCardAttachments; } + public String getLaborForm() { return laborForm; } + public void setLaborForm(String laborForm) { this.laborForm = laborForm; } + public BigDecimal getFeePreTax() { return feePreTax; } + public void setFeePreTax(BigDecimal feePreTax) { this.feePreTax = feePreTax; } + public BigDecimal getTax() { return tax; } + public void setTax(BigDecimal tax) { this.tax = tax; } + public BigDecimal getFee() { return fee; } + public void setFee(BigDecimal fee) { this.fee = fee; } + public Date getSignedAt() { return signedAt; } + public void setSignedAt(Date signedAt) { this.signedAt = signedAt; } + public String getSignedIp() { return signedIp; } + public void setSignedIp(String signedIp) { this.signedIp = signedIp; } public String getHandsign() { return handsign; } public void setHandsign(String handsign) { this.handsign = handsign; } public String getLaborProtocol() { return laborProtocol; } public void setLaborProtocol(String laborProtocol) { this.laborProtocol = laborProtocol; } + public String getLaborProtocolMasked() { return laborProtocolMasked; } + public void setLaborProtocolMasked(String laborProtocolMasked) { this.laborProtocolMasked = laborProtocolMasked; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public String getMeetingName() { return meetingName; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/dto/ImportResult.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/dto/ImportResult.java new file mode 100644 index 0000000..1cc1699 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/dto/ImportResult.java @@ -0,0 +1,60 @@ +package com.ruoyi.business.domain.dto; + +import java.util.ArrayList; +import java.util.List; + +/** + * Excel 批量导入结果 (前端通用) + * + *

与前端 {@code } 直接对应: + *

    + *
  • {@code okNum} / {@code ngNum} — 顶部汇总
  • + *
  • {@code ngList} — 失败明细, 每条含 rowNum + message
  • + *
+ * + *

当前仅 biz_expert 批量导入使用; 后续如需复用, 移至 ruoyi-common。 + * + * @author guoju + */ +public class ImportResult { + + /** 成功条数 */ + private int okNum; + + /** 失败条数 */ + private int ngNum; + + /** 失败明细 (Excel 行号 + 失败原因) */ + private List ngList = new ArrayList<>(); + + public void ok() { okNum++; } + + public void fail(int rowNum, String message) { + ngNum++; + ngList.add(new NgRow(rowNum, message)); + } + + public boolean hasFailure() { return ngNum > 0; } + + public int getOkNum() { return okNum; } + public void setOkNum(int okNum) { this.okNum = okNum; } + public int getNgNum() { return ngNum; } + public void setNgNum(int ngNum) { this.ngNum = ngNum; } + public List getNgList() { return ngList; } + public void setNgList(List ngList) { this.ngList = ngList; } + + /** 失败行 (Excel 行号 + 原因) */ + public static class NgRow { + private int rowNum; + private String message; + public NgRow() {} + public NgRow(int rowNum, String message) { + this.rowNum = rowNum; + this.message = message; + } + public int getRowNum() { return rowNum; } + public void setRowNum(int rowNum) { this.rowNum = rowNum; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertExportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertExportVo.java index 7fd0104..2f41ce8 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertExportVo.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertExportVo.java @@ -40,13 +40,10 @@ public class BizExpertExportVo { @Excel(name = "银行名称", sort = 9) private String bankName; - @Excel(name = "开户行省份", sort = 10) - private String bankProvince; + @Excel(name = "开户行省/市", sort = 10) + private String bankRegion; - @Excel(name = "开户行城市", sort = 11) - private String bankCity; - - @Excel(name = "开户行地址", sort = 12) + @Excel(name = "开户行地址", sort = 11) private String bankAddress; @Excel(name = "状态", sort = 13, readConverterExp = "Y=正常,N=禁用") @@ -84,10 +81,8 @@ public class BizExpertExportVo { public void setBankCard(String bankCard) { this.bankCard = bankCard; } public String getBankName() { return bankName; } public void setBankName(String bankName) { this.bankName = bankName; } - public String getBankProvince() { return bankProvince; } - public void setBankProvince(String bankProvince) { this.bankProvince = bankProvince; } - public String getBankCity() { return bankCity; } - public void setBankCity(String bankCity) { this.bankCity = bankCity; } + 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 String getStatus() { return status; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertImportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertImportVo.java index 6ac7808..b4f864a 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertImportVo.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizExpertImportVo.java @@ -8,6 +8,14 @@ import com.ruoyi.common.annotation.Excel; *

仅用于 Excel 批量导入 / 模板下载 / 导出, 不参与业务逻辑. * 字段顺序与 Excel 列一致, 调整时同步修改模板下载体验. * + *

不包含的字段 (固定后端默认值或走其他导入通道): + *

    + *
  • 状态 — 默认 'Y'(正常)
  • + *
  • 审核状态 — 默认 '2'(通过)
  • + *
  • sys_user.status — 默认 '0'(正常)
  • + *
  • 身份证正面/反面 URL, 执业证书 URL, 职称证明 URL — 走单独的证书导入
  • + *
+ * * @author guoju */ public class BizExpertImportVo { @@ -40,46 +48,22 @@ public class BizExpertImportVo { @Excel(name = "身份证件号码", sort = 7) private String idCard; - /** 身份证件照片 正面URL */ - @Excel(name = "身份证正面URL", sort = 8) - private String idCardFrontUrl; - - /** 身份证件照片 反面URL */ - @Excel(name = "身份证反面URL", sort = 9) - private String idCardBackUrl; - - /** 执业医师证书 URL */ - @Excel(name = "执业医师证书URL", sort = 10) - private String practiceCertUrl; - - /** 职称证明 URL */ - @Excel(name = "职称证明URL", sort = 11) - private String titleCertUrl; - /** 银行卡号 */ - @Excel(name = "银行卡号", sort = 12) + @Excel(name = "银行卡号", sort = 8) private String bankCard; /** 银行名称 */ - @Excel(name = "银行名称", sort = 13) + @Excel(name = "银行名称", sort = 9) private String bankName; - /** 开户行省份 */ - @Excel(name = "开户行省份", sort = 14) - private String bankProvince; - - /** 开户行城市 */ - @Excel(name = "开户行城市", sort = 15) - private String bankCity; + /** 开户行省/市 (与 DB bank_region 一致, /分隔) */ + @Excel(name = "开户行省/市", sort = 10) + private String bankRegion; /** 开户行地址 */ - @Excel(name = "开户行地址", sort = 16) + @Excel(name = "开户行地址", sort = 11) private String bankAddress; - /** 状态 (Y=正常 N=禁用, 留空默认 Y) */ - @Excel(name = "状态", sort = 17, readConverterExp = "Y=正常,N=禁用") - private String status; - public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } @@ -94,24 +78,12 @@ public class BizExpertImportVo { public void setRegion(String region) { this.region = region; } public String getIdCard() { return idCard; } public void setIdCard(String idCard) { this.idCard = idCard; } - public String getIdCardFrontUrl() { return idCardFrontUrl; } - public void setIdCardFrontUrl(String idCardFrontUrl) { this.idCardFrontUrl = idCardFrontUrl; } - public String getIdCardBackUrl() { return idCardBackUrl; } - public void setIdCardBackUrl(String idCardBackUrl) { this.idCardBackUrl = idCardBackUrl; } - public String getPracticeCertUrl() { return practiceCertUrl; } - public void setPracticeCertUrl(String practiceCertUrl) { this.practiceCertUrl = practiceCertUrl; } - public String getTitleCertUrl() { return titleCertUrl; } - public void setTitleCertUrl(String titleCertUrl) { this.titleCertUrl = titleCertUrl; } public String 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 getBankProvince() { return bankProvince; } - public void setBankProvince(String bankProvince) { this.bankProvince = bankProvince; } - public String getBankCity() { return bankCity; } - public void setBankCity(String bankCity) { this.bankCity = bankCity; } + 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 String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizPublicityExecutionIntentExportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizPublicityExecutionIntentExportVo.java new file mode 100644 index 0000000..3332056 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizPublicityExecutionIntentExportVo.java @@ -0,0 +1,68 @@ +package com.ruoyi.business.domain.vo; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; + +/** + * 管理端 - 公示页执行意向导出 VO (中文列头) + * + *

数据源: biz_publicity_execution_intent (匿名+管理后台共用) + * 仅用于 Excel 导出, 不参与业务逻辑. + * + * @author guoju + */ +public class BizPublicityExecutionIntentExportVo { + + @Excel(name = "项目编号", sort = 1) + private String projectNo; + + @Excel(name = "项目名称", sort = 2) + private String projectName; + + @Excel(name = "姓名", sort = 3) + private String name; + + @Excel(name = "工作单位", sort = 4) + private String workUnit; + + @Excel(name = "部门", sort = 5) + private String department; + + @Excel(name = "职务", sort = 6) + private String position; + + @Excel(name = "手机号", sort = 7) + private String phone; + + @Excel(name = "账号状态", sort = 8) + private String userStatus; + + @Excel(name = "审核状态", sort = 9) + private String intentStatus; + + @Excel(name = "创建时间", sort = 10, dateFormat = "yyyy-MM-dd HH:mm:ss") + @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 getName() { return name; } + public void setName(String name) { this.name = name; } + public String getWorkUnit() { return workUnit; } + public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getPosition() { return position; } + public void setPosition(String position) { this.position = position; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getUserStatus() { return userStatus; } + public void setUserStatus(String userStatus) { this.userStatus = userStatus; } + public String getIntentStatus() { return intentStatus; } + public void setIntentStatus(String intentStatus) { this.intentStatus = intentStatus; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizPublicitySupportIntentExportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizPublicitySupportIntentExportVo.java new file mode 100644 index 0000000..3fa160b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizPublicitySupportIntentExportVo.java @@ -0,0 +1,68 @@ +package com.ruoyi.business.domain.vo; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; + +/** + * 管理端 - 公示页支持意向导出 VO (中文列头) + * + *

数据源: biz_publicity_support_intent (匿名+管理后台共用) + * 仅用于 Excel 导出, 不参与业务逻辑. + * + * @author guoju + */ +public class BizPublicitySupportIntentExportVo { + + @Excel(name = "项目编号", sort = 1) + private String projectNo; + + @Excel(name = "项目名称", sort = 2) + private String projectName; + + @Excel(name = "姓名", sort = 3) + private String name; + + @Excel(name = "工作单位", sort = 4) + private String workUnit; + + @Excel(name = "部门", sort = 5) + private String department; + + @Excel(name = "职务", sort = 6) + private String position; + + @Excel(name = "手机号", sort = 7) + private String phone; + + @Excel(name = "账号状态", sort = 8) + private String userStatus; + + @Excel(name = "审核状态", sort = 9) + private String intentStatus; + + @Excel(name = "创建时间", sort = 10, dateFormat = "yyyy-MM-dd HH:mm:ss") + @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 getName() { return name; } + public void setName(String name) { this.name = name; } + public String getWorkUnit() { return workUnit; } + public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getPosition() { return position; } + public void setPosition(String position) { this.position = position; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getUserStatus() { return userStatus; } + public void setUserStatus(String userStatus) { this.userStatus = userStatus; } + public String getIntentStatus() { return intentStatus; } + public void setIntentStatus(String intentStatus) { this.intentStatus = intentStatus; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java index 3fb9021..a78ff20 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java @@ -11,6 +11,12 @@ public interface BizExpertMapper BizExpert selectByUserId(Long userId); List selectList(BizExpert entity); int insert(BizExpert entity); + /** + * 批量插入 biz_expert (foreach, 100/批由 service 层 chunk 控制) + * @param list BizExpert 列表, caller 须自己 chunk 到 100 以内 + * @return 影响行数 + */ + int batchInsert(List list); int insertWithUserId(BizExpert entity); int updateByPrimaryKey(BizExpert entity); int updateByUserId(BizExpert entity); diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java index e8f7cbe..3d1dae3 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java @@ -1,15 +1,23 @@ package com.ruoyi.business.mapper; import java.util.List; +import org.apache.ibatis.annotations.Param; import com.ruoyi.business.domain.BizMeetingAttendee; public interface BizMeetingAttendeeMapper { int insert(BizMeetingAttendee entity); + /** 批量插入参会人 (BizMeetingController.add 调用) */ + int insertBatch(@Param("meetingId") Long meetingId, @Param("userIds") Long[] userIds, @Param("createBy") String createBy); + /** 医生填写信息保存草稿: 更新签字快照字段 + 劳务信息 (不含 handsign/PDF) */ + int updateProfile(BizMeetingAttendee entity); int updateHandsign(BizMeetingAttendee entity); + /** 提交签字: 一次性存 handsign + labor_protocol + signed_at + signed_ip */ + int updateSign(BizMeetingAttendee entity); int updateLaborProtocol(BizMeetingAttendee entity); int deleteByMeetingId(Long meetingId); int deleteByMeetingIdAndUserId(BizMeetingAttendee entity); List selectByMeetingId(Long meetingId); List selectByUserId(Long userId); + BizMeetingAttendee selectById(Long id); List selectUnsignedByUserId(Long userId); } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizSignService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizSignService.java new file mode 100644 index 0000000..dc3fce1 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizSignService.java @@ -0,0 +1,20 @@ +package com.ruoyi.business.service; + +import com.ruoyi.business.domain.BizMeetingAttendee; +import java.util.Map; + +/** + * 医生劳务协议签署 service + * 简化版: 不用 token, 走标准 Spring Security 的 SecurityUtils.getUserId() 识别医生 + * doctor 登录后, 移动端 /doctor/sign-fill?attendeeId=X 打开, 后端从 SecurityUtils 取用户 + */ +public interface BizSignService { + /** 医生填写页 GET /info?attendeeId=X: 返回默认值 (biz_expert 预填) + 已存 attendee 字段 + 选项 */ + Map getSignInfo(Long attendeeId); + /** 医生填写页 POST /saveProfile: 批量 UPDATE attendee 字段 (不含签名) */ + void saveProfile(Long attendeeId, BizMeetingAttendee form); + /** 签署页 GET /contract?attendeeId=X: 渲染完整 HTML (占位符替换 + 身份证附件 + 手写签名) */ + String getContractHtml(Long attendeeId); + /** 签署页 POST /submit: 返回 {fullPdfUrl, maskedPdfUrl} */ + Map submitSign(Long attendeeId, String handsignBase64, String contentHtml, String signedIp); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java index e3b768a..b7981ab 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java @@ -2,8 +2,10 @@ package com.ruoyi.business.service; import java.util.List; import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.domain.dto.ImportResult; import com.ruoyi.business.domain.vo.BizExpertImportVo; import com.ruoyi.common.core.domain.entity.SysUser; +import org.springframework.web.multipart.MultipartFile; /** * 专家Service接口 @@ -31,8 +33,19 @@ public interface IBizExpertService int deleteByPrimaryKeys(Long[] expertId); /** - * 批量导入专家: 每行调用 insert, updateSupport=true 时跳过已存在手机号(视为成功) - * 返回 String 含成功/失败计数 + 失败明细 (前端 toast 显示) + * 批量导入专家 (3 阶段批量版): + *

    + *
  • Phase A: IN 查 sys_user (100/批), 记录已存在 phone → userId
  • + *
  • Phase B: 新 phone batch insert sys_user (100/批), 插完再 IN 查回 userId
  • + *
  • Phase C: batch insert biz_expert (100/批), 已存在 phone 复用 Phase A userId
  • + *
+ * 单行校验失败不中断, 错误累计到 {@link ImportResult}。 + * 默认审核通过 (audit_status='2'), 用户名=密码=手机号, role_type='doctor'。 + * + * @param file Excel 文件 (multipart) + * @param updateSupport 已存在 phone 是否覆盖 update (当前实现: 始终绑定已有 user_id, 仅插 biz_expert) + * @param operName 操作人 (写入 create_by / audit_by) + * @return 导入结果 */ - String importExpert(List importList, boolean updateSupport, String operName); + ImportResult importExpert(MultipartFile file, boolean updateSupport, String operName) throws Exception; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java index 87c3f4b..fdab009 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java @@ -5,12 +5,18 @@ import com.ruoyi.business.domain.BizMeetingAttendee; public interface IBizMeetingAttendeeService { int insert(BizMeetingAttendee entity); + /** 批量插入参会人 (BizMeetingController.add 调用) */ + int insertBatch(Long meetingId, Long[] userIds); + /** 医生填写信息保存草稿: 更新签字快照字段 + 劳务信息 (不含 handsign/PDF) */ + int updateProfile(BizMeetingAttendee entity); int updateHandsign(BizMeetingAttendee entity); + /** 提交签字: 一次性存 handsign + labor_protocol + signed_at + signed_ip */ + int updateSign(BizMeetingAttendee entity); int updateLaborProtocol(BizMeetingAttendee entity); int deleteByMeetingId(Long meetingId); int deleteByMeetingIdAndUserId(BizMeetingAttendee entity); List selectByMeetingId(Long meetingId); List selectByUserId(Long userId); - /** 当前用户的"待签署"会议列表 (handsign 或 labor_protocol 任一为空) */ + BizMeetingAttendee selectById(Long id); List selectUnsignedByUserId(Long userId); } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java new file mode 100644 index 0000000..f380a35 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java @@ -0,0 +1,102 @@ +package com.ruoyi.business.service; + +import com.itextpdf.html2pdf.ConverterProperties; +import com.itextpdf.html2pdf.HtmlConverter; +import com.itextpdf.io.font.PdfEncodings; +import com.itextpdf.kernel.font.PdfFont; +import com.itextpdf.kernel.font.PdfFontFactory; +import com.itextpdf.kernel.geom.PageSize; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.layout.font.FontProvider; +import com.ruoyi.common.config.RuoYiConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * HTML 转 PDF 服务 + * 用 iText 7 html2pdf (HtmlConverter) + 内置宋体 (simsun.ttc) 渲染中文 + * 输入: HTML 字符串 → 输出: PDF 文件 (存到 ruoyi.profile 目录, 返回 URL) + * 说明: 相比 Flying Saucer (xhtmlrenderer 严格 XML 解析), html2pdf 走 jsoup HTML 解析, + * 能容忍前端拼出的非 XHTML 内容 (如 未自闭合), 不会报 SAXParseException。 + */ +@Service +public class PdfService { + + @Autowired + private RuoYiConfig ruoyiConfig; + + /** + * HTML 字符串 → PDF 字节流 + */ + public byte[] htmlToPdf(String htmlContent) { + if (htmlContent == null || htmlContent.trim().isEmpty()) { + throw new IllegalArgumentException("HTML 内容为空"); + } + String fullHtml = wrapHtml(htmlContent); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + PdfWriter writer = new PdfWriter(baos); + PdfDocument pdfDocument = new PdfDocument(writer); + pdfDocument.setDefaultPageSize(PageSize.A4); + + ConverterProperties properties = new ConverterProperties(); + FontProvider fontProvider = new FontProvider(); + // 中文字体 (对齐 hwt-serve): 基础目录下 simsun.ttc, ",0" 取第 0 个 face + PdfFont cjkFont = PdfFontFactory.createFont("simsun.ttc,0", PdfEncodings.IDENTITY_H, false); + fontProvider.addFont(cjkFont.getFontProgram(), PdfEncodings.IDENTITY_H); + properties.setFontProvider(fontProvider); + + HtmlConverter.convertToPdf(fullHtml, pdfDocument, properties); + pdfDocument.close(); + return baos.toByteArray(); + } catch (Exception e) { + throw new RuntimeException("HTML 转 PDF 失败: " + e.getMessage(), e); + } + } + + /** + * HTML → PDF 文件 (存到本地) + * @return 完整 URL (前端可直接打开) + */ + public String htmlToPdfFile(String htmlContent, String bizPath) { + byte[] pdfBytes = htmlToPdf(htmlContent); + // 按 RuoYi 风格分目录: profile/labor/{date}/{filename} + SimpleDateFormat dateDir = new SimpleDateFormat("yyyy-MM-dd"); + String today = dateDir.format(new Date()); + String datePath = (bizPath == null || bizPath.isEmpty() ? "labor" : bizPath) + "/" + today; + String filename = System.currentTimeMillis() + "_" + (int)(Math.random() * 1000) + ".pdf"; + + String profilePath = ruoyiConfig.getProfile(); + File dir = new File(profilePath + File.separator + datePath); + if (!dir.exists()) { + dir.mkdirs(); + } + File pdfFile = new File(dir, filename); + try (FileOutputStream fos = new FileOutputStream(pdfFile)) { + fos.write(pdfBytes); + } catch (Exception e) { + throw new RuntimeException("保存 PDF 失败: " + e.getMessage(), e); + } + // 返回 URL 路径 (前端拼 origin) + String url = "/profile/" + datePath + "/" + filename; + return url; + } + + /** + * 包裹完整 HTML 结构 (HTML5, 供 html2pdf 解析) + */ + private String wrapHtml(String content) { + String lower = content.trim().toLowerCase(); + if (lower.startsWith("" + + content + ""; + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java index 71ae77a..4da79ea 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java @@ -1,9 +1,19 @@ package com.ruoyi.business.service.impl; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.domain.dto.ImportResult; import com.ruoyi.business.domain.vo.BizExpertImportVo; import com.ruoyi.business.mapper.BizExpertMapper; import com.ruoyi.business.service.IBizExpertService; @@ -11,17 +21,27 @@ import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.id.IdGenerator; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.mapper.SysUserMapper; import com.ruoyi.system.service.ISysUserService; @Service public class BizExpertServiceImpl implements IBizExpertService { + private static final Logger log = LoggerFactory.getLogger(BizExpertServiceImpl.class); + + /** 批量导入 chunk 大小, IN 查和 INSERT 都是这个粒度 */ + private static final int BATCH_SIZE = 100; + @Autowired private BizExpertMapper bizExpertMapper; @Autowired private ISysUserService sysUserService; + @Autowired + private SysUserMapper sysUserMapper; + @Override public BizExpert getById(Long expertId) { return bizExpertMapper.selectByPrimaryKey(expertId); } @@ -139,105 +159,228 @@ public class BizExpertServiceImpl implements IBizExpertService { return bizExpertMapper.deleteByPrimaryKeys(expertId); } /** - * 批量导入专家: - * 1. 每行 必填校验 (姓名/手机号/工作单位/科室/职称, 手机号正则 1[0-9]{10}) - * 2. status 留空 → Y (正常); 非法值 → 抛错 - * 3. sys_user 同步创建 (与单条 insert 共用 sys_userService.insertUser) - * 4. updateSupport=true 时: 同一手机号已存在 → 跳过 (按成功计) - * 5. 单行失败不中断, 错误信息累计 + * 批量导入专家 (3 阶段批量版): + *
    + *
  1. Phase 0: ExcelUtil 反序列化 + 行级必填校验, 失败行累计到 result.ngList
  2. + *
  3. Phase A: 100/批 IN 查 sys_user, 记录已有 phone → userId 映射
  4. + *
  5. Phase B: 100/批 batch insert sys_user (新 phone), 插完再 IN 查回 userId; 同步 updateRoleType='doctor'
  6. + *
  7. Phase C: 100/批 batch insert biz_expert (所有有效行), 已存在 phone 复用 Phase A userId
  8. + *
+ * 失败行为: + *
    + *
  • Phase B 整批失败 → 退化为单条逐行 insert, 失败的行记 ngList
  • + *
  • Phase C 整批失败 → 同上
  • + *
  • 跨批失败不回滚已成功的批 (避免一条脏数据拖垮整批)
  • + *
*/ @Override - public String importExpert(List importList, boolean updateSupport, String operName) { + @Transactional(rollbackFor = Exception.class) + public ImportResult importExpert(MultipartFile file, boolean updateSupport, String operName) throws Exception { + // ==================== Phase 0: 解析 + 行级校验 ==================== + ExcelUtil util = new ExcelUtil<>(BizExpertImportVo.class); + List importList = util.importExcel(file.getInputStream()); if (importList == null || importList.isEmpty()) { throw new ServiceException("导入数据不能为空"); } - int successNum = 0; - int failureNum = 0; - StringBuilder successMsg = new StringBuilder(); - StringBuilder failureMsg = new StringBuilder(); + ImportResult result = new ImportResult(); + // validRows: 校验通过的 {vo, rowNo}; status 固定 'Y'(正常), auditStatus 固定 '2'(通过) + List validRows = new ArrayList<>(importList.size()); for (int i = 0; i < importList.size(); i++) { BizExpertImportVo vo = importList.get(i); int rowNo = i + 2; // Excel 行号 (1=表头) try { - // 必填校验 - if (vo.getName() == null || vo.getName().trim().isEmpty()) { - throw new ServiceException("姓名不能为空"); - } - if (vo.getPhone() == null || vo.getPhone().trim().isEmpty()) { - throw new ServiceException("手机号不能为空"); - } - if (!vo.getPhone().matches("^1[0-9]\\d{9}$")) { - throw new ServiceException("手机号格式不正确"); - } - if (vo.getWorkUnit() == null || vo.getWorkUnit().trim().isEmpty()) { - throw new ServiceException("工作单位不能为空"); - } - if (vo.getDepartment() == null || vo.getDepartment().trim().isEmpty()) { - throw new ServiceException("科室不能为空"); - } - if (vo.getTitle() == null || vo.getTitle().trim().isEmpty()) { - throw new ServiceException("职称不能为空"); - } - // status 校验, 留空 → Y - String status = vo.getStatus(); - if (status == null || status.trim().isEmpty()) { - status = "Y"; - } else if (!"Y".equals(status) && !"N".equals(status)) { - throw new ServiceException("状态只能是 Y(正常) 或 N(禁用)"); - } - - // 同一手机号已存在 → updateSupport=true 跳过, 否则报错 - if (sysUserService.isPhoneRegistered(vo.getPhone())) { - if (updateSupport) { - successNum++; - successMsg.append("
" + successNum + "、手机号 " + vo.getPhone() + " 已存在, 已跳过"); - continue; - } - throw new ServiceException("手机号 " + vo.getPhone() + " 已存在"); - } - - // 复用单条 insert 逻辑 (含 sys_user 创建) - BizExpert entity = new BizExpert(); - entity.setName(vo.getName()); - entity.setPhone(vo.getPhone()); - entity.setWorkUnit(vo.getWorkUnit()); - entity.setDepartment(vo.getDepartment()); - entity.setTitle(vo.getTitle()); - entity.setRegion(vo.getRegion()); - entity.setIdCard(vo.getIdCard()); - entity.setIdCardFrontUrl(vo.getIdCardFrontUrl()); - entity.setIdCardBackUrl(vo.getIdCardBackUrl()); - entity.setPracticeCertUrl(vo.getPracticeCertUrl()); - entity.setTitleCertUrl(vo.getTitleCertUrl()); - entity.setBankCard(vo.getBankCard()); - entity.setBankName(vo.getBankName()); - entity.setBankProvince(vo.getBankProvince()); - entity.setBankCity(vo.getBankCity()); - entity.setBankAddress(vo.getBankAddress()); - entity.setStatus(status); - // admin 批量导入默认通过审核 (与单条 admin 创建一致) - entity.setAuditStatus("2"); - entity.setAuditBy(operName); - entity.setAuditTime(new java.util.Date()); - entity.setCreateBy(operName); - - insert(entity); - successNum++; - successMsg.append("
" + successNum + "、账号 " + vo.getPhone() + " 导入成功"); + ValidatedRow vr = validateRow(vo, rowNo); + validRows.add(vr); } catch (Exception e) { - failureNum++; - String msg = e.getMessage(); - if (msg == null) msg = e.getClass().getSimpleName(); - failureMsg.append("
" + failureNum + "、第 " + rowNo + " 行: " + msg); + result.fail(rowNo, e.getMessage() == null ? "校验失败" : e.getMessage()); } } - if (failureNum > 0) { - failureMsg.insert(0, "很抱歉, 部分导入失败, 共 " + failureNum + " 条:"); + if (validRows.isEmpty()) { + return result; } - if (successNum > 0) { - successMsg.insert(0, "恭喜您, 数据已全部导入成功!共 " + successNum + " 条, 数据如下:"); + + // ==================== Phase A: IN 查 sys_user (100/批) ==================== + // phone -> userId, 含已存在 user (del_flag='0') + Map phoneToUserId = new HashMap<>(); + List allPhones = validRows.stream() + .map(vr -> vr.vo.getPhone()) + .distinct() + .collect(Collectors.toList()); + for (List chunk : chunk(allPhones, BATCH_SIZE)) { + List existing = sysUserMapper.selectByPhoneList(chunk); + for (SysUser u : existing) { + phoneToUserId.put(u.getPhonenumber(), u.getUserId()); + } + } + + // ==================== Phase B: batch insert sys_user (新 phone) ==================== + List needNewUser = validRows.stream() + .filter(vr -> !phoneToUserId.containsKey(vr.vo.getPhone())) + .collect(Collectors.toList()); + + for (List chunk : chunk(needNewUser, BATCH_SIZE)) { + List newUsers = new ArrayList<>(chunk.size()); + List chunkPhones = new ArrayList<>(chunk.size()); + for (ValidatedRow vr : chunk) { + SysUser u = new SysUser(); + u.setUserName(vr.vo.getPhone()); + u.setNickName(vr.vo.getName()); + u.setPhonenumber(vr.vo.getPhone()); + u.setPassword(SecurityUtils.encryptPassword(vr.vo.getPhone())); + u.setStatus("0"); + u.setDelFlag("0"); + u.setRoleType("doctor"); + u.setCreateBy(operName); + newUsers.add(u); + chunkPhones.add(vr.vo.getPhone()); + } + try { + sysUserMapper.batchInsertUsers(newUsers); + // 整批 insert 后再 IN 查回 userId (foreach+useGeneratedKeys 不可靠) + List created = sysUserMapper.selectByPhoneList(chunkPhones); + for (SysUser u : created) { + phoneToUserId.put(u.getPhonenumber(), u.getUserId()); + } + // 兜底: 若某条 phone 仍未查到 userId, 退化为单条逐行, 计入 ngList + for (ValidatedRow vr : chunk) { + if (!phoneToUserId.containsKey(vr.vo.getPhone())) { + try { + insert(toEntity(vr.vo, operName, null)); + // 单条 insert 已经写了 sys_user, 再查一次 + SysUser u2 = sysUserMapper.selectUserByUserName(vr.vo.getPhone()); + if (u2 != null) phoneToUserId.put(vr.vo.getPhone(), u2.getUserId()); + else throw new ServiceException("建账号失败, 请重试"); + } catch (Exception ex) { + result.fail(vr.rowNo, "建账号失败: " + ex.getMessage()); + } + } + } + } catch (Exception ex) { + // 整批失败 → 单条逐行退化 + log.warn("batchInsertUsers 整批失败, 退化单条, 原因: {}", ex.getMessage()); + for (ValidatedRow vr : chunk) { + try { + insert(toEntity(vr.vo, operName, null)); + SysUser u2 = sysUserMapper.selectUserByUserName(vr.vo.getPhone()); + if (u2 != null) phoneToUserId.put(vr.vo.getPhone(), u2.getUserId()); + else throw new ServiceException("建账号失败, 请重试"); + } catch (Exception ex2) { + result.fail(vr.rowNo, "建账号失败: " + ex2.getMessage()); + } + } + } + } + + // ==================== Phase C: batch insert biz_expert (100/批) ==================== + // 过滤出已有 userId 的有效行 + List readyForExpert = new ArrayList<>(validRows.size()); + for (ValidatedRow vr : validRows) { + if (phoneToUserId.containsKey(vr.vo.getPhone())) { + readyForExpert.add(vr); + } + // 否则该行已在 Phase B 计入 ngList, 不再尝试插 biz_expert + } + + for (List chunk : chunk(readyForExpert, BATCH_SIZE)) { + List experts = new ArrayList<>(chunk.size()); + for (ValidatedRow vr : chunk) { + experts.add(toEntity(vr.vo, operName, phoneToUserId.get(vr.vo.getPhone()))); + } + try { + bizExpertMapper.batchInsert(experts); + // 整批成功 → 累加 okNum + for (int k = 0; k < chunk.size(); k++) { + result.ok(); + } + } catch (Exception ex) { + // 整批失败 → 单条逐行退化, 失败行记 ngList + log.warn("batchInsert experts 整批失败, 退化单条, 原因: {}", ex.getMessage()); + for (ValidatedRow vr : chunk) { + try { + bizExpertMapper.insert(toEntity(vr.vo, operName, phoneToUserId.get(vr.vo.getPhone()))); + result.ok(); + } catch (Exception ex2) { + result.fail(vr.rowNo, "插 biz_expert 失败: " + ex2.getMessage()); + } + } + } + } + + return result; + } + + /** + * 行级校验, 失败抛 ServiceException + */ + private ValidatedRow validateRow(BizExpertImportVo vo, int rowNo) { + if (vo.getName() == null || vo.getName().trim().isEmpty()) { + throw new ServiceException("姓名不能为空"); + } + if (vo.getPhone() == null || vo.getPhone().trim().isEmpty()) { + throw new ServiceException("手机号不能为空"); + } + if (!vo.getPhone().matches("^1[0-9]\\d{9}$")) { + throw new ServiceException("手机号格式不正确"); + } + if (vo.getWorkUnit() == null || vo.getWorkUnit().trim().isEmpty()) { + throw new ServiceException("工作单位不能为空"); + } + if (vo.getDepartment() == null || vo.getDepartment().trim().isEmpty()) { + throw new ServiceException("科室不能为空"); + } + if (vo.getTitle() == null || vo.getTitle().trim().isEmpty()) { + throw new ServiceException("职称不能为空"); + } + return new ValidatedRow(vo, rowNo); + } + + /** + * Vo → BizExpert (含 userId / 雪花 expertId / 默认 status='Y', auditStatus='2') + * 不设 URL 字段 (idCardFrontUrl / idCardBackUrl / practiceCertUrl / titleCertUrl), + * 由单独的证书导入通道写入. + */ + private BizExpert toEntity(BizExpertImportVo vo, String operName, Long userId) { + BizExpert entity = new BizExpert(); + entity.setExpertId(IdGenerator.generateId()); + if (userId != null) entity.setUserId(userId); + entity.setName(vo.getName()); + entity.setPhone(vo.getPhone()); + entity.setWorkUnit(vo.getWorkUnit()); + entity.setDepartment(vo.getDepartment()); + entity.setTitle(vo.getTitle()); + entity.setRegion(vo.getRegion()); + entity.setIdCard(vo.getIdCard()); + entity.setBankCard(vo.getBankCard()); + entity.setBankName(vo.getBankName()); + entity.setBankRegion(vo.getBankRegion()); + entity.setBankAddress(vo.getBankAddress()); + entity.setStatus("Y"); + entity.setAuditStatus("2"); + entity.setAuditBy(operName); + entity.setAuditTime(new Date()); + entity.setCreateBy(operName); + return entity; + } + + /** + * 通用 chunk 工具: list → List>, 单批 ≤size + */ + private static List> chunk(List list, int size) { + List> out = new ArrayList<>(); + for (int i = 0; i < list.size(); i += size) { + out.add(list.subList(i, Math.min(i + size, list.size()))); + } + return out; + } + + /** 校验通过的中间结构 */ + private static class ValidatedRow { + final BizExpertImportVo vo; + final int rowNo; + ValidatedRow(BizExpertImportVo vo, int rowNo) { + this.vo = vo; + this.rowNo = rowNo; } - return successMsg.toString() + failureMsg.toString(); } } 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 ea0ce5a..d0165cc 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 @@ -6,50 +6,72 @@ import org.springframework.stereotype.Service; import com.ruoyi.business.domain.BizMeetingAttendee; import com.ruoyi.business.mapper.BizMeetingAttendeeMapper; import com.ruoyi.business.service.IBizMeetingAttendeeService; +import com.ruoyi.common.utils.SecurityUtils; @Service public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService { @Autowired - private BizMeetingAttendeeMapper bizMeetingAttendeeMapper; + private BizMeetingAttendeeMapper mapper; @Override public int insert(BizMeetingAttendee entity) { - return bizMeetingAttendeeMapper.insert(entity); + return mapper.insert(entity); + } + + @Override + public int insertBatch(Long meetingId, Long[] userIds) { + if (userIds == null || userIds.length == 0) return 0; + return mapper.insertBatch(meetingId, userIds, SecurityUtils.getUsername()); + } + + @Override + public int updateProfile(BizMeetingAttendee entity) { + return mapper.updateProfile(entity); } @Override public int updateHandsign(BizMeetingAttendee entity) { - return bizMeetingAttendeeMapper.updateHandsign(entity); + return mapper.updateHandsign(entity); + } + + @Override + public int updateSign(BizMeetingAttendee entity) { + return mapper.updateSign(entity); } @Override public int updateLaborProtocol(BizMeetingAttendee entity) { - return bizMeetingAttendeeMapper.updateLaborProtocol(entity); + return mapper.updateLaborProtocol(entity); } @Override public int deleteByMeetingId(Long meetingId) { - return bizMeetingAttendeeMapper.deleteByMeetingId(meetingId); + return mapper.deleteByMeetingId(meetingId); } @Override public int deleteByMeetingIdAndUserId(BizMeetingAttendee entity) { - return bizMeetingAttendeeMapper.deleteByMeetingIdAndUserId(entity); + return mapper.deleteByMeetingIdAndUserId(entity); } @Override public List selectByMeetingId(Long meetingId) { - return bizMeetingAttendeeMapper.selectByMeetingId(meetingId); + return mapper.selectByMeetingId(meetingId); } @Override public List selectByUserId(Long userId) { - return bizMeetingAttendeeMapper.selectByUserId(userId); + return mapper.selectByUserId(userId); + } + + @Override + public BizMeetingAttendee selectById(Long id) { + return mapper.selectById(id); } @Override public List selectUnsignedByUserId(Long userId) { - return bizMeetingAttendeeMapper.selectUnsignedByUserId(userId); + return mapper.selectUnsignedByUserId(userId); } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java new file mode 100644 index 0000000..13a14e7 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java @@ -0,0 +1,293 @@ +package com.ruoyi.business.service.impl; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.domain.BizLaborProtocolTemplate; +import com.ruoyi.business.domain.BizMeeting; +import com.ruoyi.business.domain.BizMeetingAttendee; +import com.ruoyi.business.mapper.BizExpertMapper; +import com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper; +import com.ruoyi.business.mapper.BizMeetingMapper; +import com.ruoyi.business.service.BizSignService; +import com.ruoyi.business.service.IBizMeetingAttendeeService; +import com.ruoyi.business.service.PdfService; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; + +@Service +public class BizSignServiceImpl implements BizSignService { + + @Autowired + private IBizMeetingAttendeeService attendeeService; + @Autowired + private BizExpertMapper expertMapper; + @Autowired + private BizMeetingMapper meetingMapper; + @Autowired + private BizLaborProtocolTemplateMapper templateMapper; + @Autowired + private PdfService pdfService; + + @Override + public Map getSignInfo(Long attendeeId) { + BizMeetingAttendee attendee = attendeeService.selectById(attendeeId); + if (attendee == null) { + throw new ServiceException("参会人记录不存在"); + } + // 安全: 当前登录用户必须是该 attendee 的 user_id + Long currentUserId = SecurityUtils.getUserId(); + if (currentUserId == null || !currentUserId.equals(attendee.getUserId())) { + throw new ServiceException("无权访问该协议"); + } + // 从 biz_expert 取默认值 (按 user_id 查) + BizExpert expert = expertMapper.selectByUserId(attendee.getUserId()); + Map defaults = new HashMap<>(); + if (expert != null) { + defaults.put("name", expert.getName() != null ? expert.getName() : ""); + defaults.put("phone", expert.getPhone() != null ? expert.getPhone() : ""); + defaults.put("workUnit", expert.getWorkUnit() != null ? expert.getWorkUnit() : ""); + defaults.put("department", expert.getDepartment() != null ? expert.getDepartment() : ""); + defaults.put("title", expert.getTitle() != null ? expert.getTitle() : ""); + defaults.put("idCard", expert.getIdCard() != null ? expert.getIdCard() : ""); + defaults.put("bankCard", expert.getBankCard() != null ? expert.getBankCard() : ""); + defaults.put("bankName", expert.getBankName() != null ? expert.getBankName() : ""); + defaults.put("bankRegion", expert.getBankRegion() != null ? expert.getBankRegion() : ""); + defaults.put("bankAddress", expert.getBankAddress() != null ? expert.getBankAddress() : ""); + } + Map current = new HashMap<>(); + current.put("name", attendee.getName() != null ? attendee.getName() : (String) defaults.getOrDefault("name", "")); + current.put("phone", attendee.getPhone() != null ? attendee.getPhone() : (String) defaults.getOrDefault("phone", "")); + current.put("workUnit", attendee.getWorkUnit() != null ? attendee.getWorkUnit() : (String) defaults.getOrDefault("workUnit", "")); + current.put("department", attendee.getDepartment() != null ? attendee.getDepartment() : (String) defaults.getOrDefault("department", "")); + current.put("title", attendee.getTitle() != null ? attendee.getTitle() : (String) defaults.getOrDefault("title", "")); + current.put("idCard", attendee.getIdCard() != null ? attendee.getIdCard() : (String) defaults.getOrDefault("idCard", "")); + current.put("bankCard", attendee.getBankCard() != null ? attendee.getBankCard() : (String) defaults.getOrDefault("bankCard", "")); + current.put("bankName", attendee.getBankName() != null ? attendee.getBankName() : (String) defaults.getOrDefault("bankName", "")); + current.put("bankBranch", attendee.getBankBranch() != null ? attendee.getBankBranch() : ""); + current.put("bankRegion", attendee.getBankRegion() != null ? attendee.getBankRegion() : (String) defaults.getOrDefault("bankRegion", "")); + current.put("bankAddress", attendee.getBankAddress() != null ? attendee.getBankAddress() : (String) defaults.getOrDefault("bankAddress", "")); + current.put("accountName", attendee.getAccountName() != null ? attendee.getAccountName() : ""); + current.put("idCardAttachments", attendee.getIdCardAttachments() != null ? attendee.getIdCardAttachments() : ""); + current.put("laborForm", attendee.getLaborForm() != null ? attendee.getLaborForm() : ""); + current.put("feePreTax", attendee.getFeePreTax()); + current.put("tax", attendee.getTax()); + current.put("fee", attendee.getFee()); + + List> laborFormOptions = Arrays.asList( + map("讲课", "讲课"), map("讨论", "讨论"), + map("主持", "主持"), map("主席", "主席"), + map("__other__", "其他")); + List titleOptions = Arrays.asList( + "主任医师", "副主任医师", "主治(主管)医师", "医士", + "主任药师", "药师", "药士", + "主任护师", "副主任护师", "主管护师", "护师", "护士", + "主任技师", "副主任技师", "主管技师", "技士", + "研究员", "副研究员", "助理研究员", "研究实习员"); + + Map result = new HashMap<>(); + result.put("defaults", defaults); + result.put("current", current); + result.put("laborFormOptions", laborFormOptions); + result.put("titleOptions", titleOptions); + result.put("attendeeId", attendeeId); + return result; + } + + private Map map(String value, String label) { + Map m = new HashMap<>(); + m.put("value", value); + m.put("label", label); + return m; + } + + @Override + public void saveProfile(Long attendeeId, BizMeetingAttendee form) { + BizMeetingAttendee attendee = attendeeService.selectById(attendeeId); + if (attendee == null) { + throw new ServiceException("参会人记录不存在"); + } + Long currentUserId = SecurityUtils.getUserId(); + if (currentUserId == null || !currentUserId.equals(attendee.getUserId())) { + throw new ServiceException("无权访问该协议"); + } + BizMeetingAttendee entity = new BizMeetingAttendee(); + entity.setId(attendeeId); + entity.setName(form.getName()); + entity.setPhone(form.getPhone()); + entity.setWorkUnit(form.getWorkUnit()); + entity.setDepartment(form.getDepartment()); + entity.setTitle(form.getTitle()); + entity.setIdCard(form.getIdCard()); + entity.setBankCard(form.getBankCard()); + entity.setBankName(form.getBankName()); + entity.setBankBranch(form.getBankBranch()); + entity.setBankRegion(form.getBankRegion()); + entity.setBankAddress(form.getBankAddress()); + entity.setAccountName(form.getAccountName()); + entity.setIdCardAttachments(form.getIdCardAttachments()); + entity.setLaborForm(form.getLaborForm()); + entity.setUpdateBy(SecurityUtils.getUsername()); + attendeeService.updateProfile(entity); + } + + @Override + public String getContractHtml(Long attendeeId) { + BizMeetingAttendee attendee = attendeeService.selectById(attendeeId); + if (attendee == null) { + throw new ServiceException("参会人记录不存在"); + } + Long currentUserId = SecurityUtils.getUserId(); + if (currentUserId == null || !currentUserId.equals(attendee.getUserId())) { + throw new ServiceException("无权访问该协议"); + } + BizLaborProtocolTemplate template = templateMapper.selectDefault(); + if (template == null) { + throw new ServiceException("系统未配置默认劳务协议模板"); + } + String html = template.getTemplateContent(); + html = replacePlaceholders(html, attendee); + BizMeeting meeting = meetingMapper.selectByPrimaryKey(attendee.getMeetingId()); + if (meeting != null) { + html = html.replaceAll("\\{会议名称\\}", nullToEmpty(meeting.getMeetingName())); + html = html.replaceAll("\\{会议地址\\}", nullToEmpty(meeting.getAddress())); + html = html.replaceAll("\\{会议时间\\}", formatDateTime(meeting.getStartTime())); + html = html.replaceAll("\\{日期\\}", formatDate(meeting.getStartTime())); + } + if (attendee.getIdCardAttachments() != null && !attendee.getIdCardAttachments().isEmpty()) { + String[] urls = attendee.getIdCardAttachments().split(","); + StringBuilder imgs = new StringBuilder(); + for (String u : urls) { + if (u.trim().isEmpty()) continue; + imgs.append("
"); + } + html = html.replaceAll("\\{身份证附件\\}", imgs.toString()); + } + html = html.replaceAll("\\{费用总额\\}", attendee.getFee() != null ? attendee.getFee().toPlainString() : ""); + if (attendee.getHandsign() != null && !attendee.getHandsign().isEmpty()) { + html = html.replaceAll("\\{手写签名\\}", + ""); + } + return html; + } + + private String replacePlaceholders(String html, BizMeetingAttendee a) { + html = html.replaceAll("\\{name\\}", nullToEmpty(a.getName())); + html = html.replaceAll("\\{phone\\}", nullToEmpty(a.getPhone())); + html = html.replaceAll("\\{dept\\}", nullToEmpty(a.getWorkUnit())); + html = html.replaceAll("\\{工作单位\\}", nullToEmpty(a.getWorkUnit())); + html = html.replaceAll("\\{科室\\}", nullToEmpty(a.getDepartment())); + html = html.replaceAll("\\{劳务形式\\}", nullToEmpty(a.getLaborForm())); + html = html.replaceAll("\\{账户名称(持卡人姓名)\\}", nullToEmpty(a.getAccountName())); + html = html.replaceAll("\\{身份证号(外宾填写护照号)\\}", nullToEmpty(a.getIdCard())); + html = html.replaceAll("\\{银行名称\\}", nullToEmpty(a.getBankName())); + html = html.replaceAll("\\{开户银行名称(具体到支行)\\}", nullToEmpty(a.getBankBranch())); + // 新版占位符 (合并字段): {开户行省/市} → bankRegion, {开户行地址} → bankAddress + html = html.replaceAll("\\{开户行省/市\\}", nullToEmpty(a.getBankRegion())); + html = html.replaceAll("\\{开户行地址\\}", nullToEmpty(a.getBankAddress())); + // 旧版占位符 {开户银行地址} 保留向下兼容: 合并 bankRegion + bankAddress + html = html.replaceAll("\\{开户银行地址\\}", + nullToEmpty(a.getBankRegion()) + " " + nullToEmpty(a.getBankAddress())); + html = html.replaceAll("\\{银行卡号\\}", nullToEmpty(a.getBankCard())); + html = html.replaceAll("\\{医务职称\\}", nullToEmpty(a.getTitle())); + return html; + } + + private String nullToEmpty(String s) { return s == null ? "" : s; } + + /** 日期格式: yyyy-MM-dd (例如 2026-01-10) */ + private String formatDate(java.util.Date d) { + if (d == null) return ""; + java.util.Calendar c = java.util.Calendar.getInstance(); + c.setTime(d); + return String.format("%04d-%02d-%02d", + c.get(java.util.Calendar.YEAR), + c.get(java.util.Calendar.MONTH) + 1, + c.get(java.util.Calendar.DAY_OF_MONTH)); + } + + /** 日期时间格式: yyyy-MM-dd HH:mm (例如 2026-01-10 10:00) */ + private String formatDateTime(java.util.Date d) { + if (d == null) return ""; + java.util.Calendar c = java.util.Calendar.getInstance(); + c.setTime(d); + return String.format("%04d-%02d-%02d %02d:%02d", + c.get(java.util.Calendar.YEAR), + c.get(java.util.Calendar.MONTH) + 1, + c.get(java.util.Calendar.DAY_OF_MONTH), + c.get(java.util.Calendar.HOUR_OF_DAY), + c.get(java.util.Calendar.MINUTE)); + } + + @Override + public Map submitSign(Long attendeeId, String handsignBase64, String contentHtml, String signedIp) { + BizMeetingAttendee attendee = attendeeService.selectById(attendeeId); + if (attendee == null) { + throw new ServiceException("参会人记录不存在"); + } + Long currentUserId = SecurityUtils.getUserId(); + if (currentUserId == null || !currentUserId.equals(attendee.getUserId())) { + throw new ServiceException("无权访问该协议"); + } + if (handsignBase64 == null || handsignBase64.isEmpty()) { + throw new ServiceException("手写签名不能为空"); + } + String fullPdfUrl = pdfService.htmlToPdfFile(contentHtml, "labor/" + attendee.getMeetingId()); + String maskedHtml = desensitizeHtml(contentHtml); + String maskedPdfUrl = pdfService.htmlToPdfFile(maskedHtml, "labor/" + attendee.getMeetingId() + "/masked"); + BizMeetingAttendee update = new BizMeetingAttendee(); + update.setId(attendeeId); + update.setHandsign(handsignBase64); + update.setLaborProtocol(fullPdfUrl); + update.setLaborProtocolMasked(maskedPdfUrl); + update.setSignedAt(new java.util.Date()); + update.setSignedIp(signedIp); + update.setUpdateBy(SecurityUtils.getUsername()); + attendeeService.updateSign(update); + Map result = new HashMap<>(); + result.put("fullPdfUrl", fullPdfUrl); + result.put("maskedPdfUrl", maskedPdfUrl); + return result; + } + + private String desensitizeHtml(String html) { + if (html == null) return null; + java.util.regex.Pattern idCardP = java.util.regex.Pattern.compile("(? - - + - - + @@ -29,7 +27,7 @@ - select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_province, bank_city, bank_address, id_card_front_url, id_card_back_url, audit_status, audit_by, audit_time, status, create_by, create_time, update_by, update_time + select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_region, bank_address, id_card_attachments, audit_status, audit_by, audit_time, status, create_by, create_time, update_by, update_time from biz_expert - select id, meeting_id, user_id, handsign, labor_protocol, create_by, create_time + select id, meeting_id, user_id, name, phone, work_unit, department, title, id_card, bank_card, bank_name, bank_branch, bank_region, bank_address, account_name, id_card_attachments, labor_form, fee_pre_tax, tax, fee, signed_at, signed_ip, handsign, labor_protocol, create_by, create_time from biz_meeting_attendee where meeting_id = #{meetingId} + + + + + + insert into sys_user(user_name, nick_name, phonenumber, password, status, del_flag, role_type, create_by, create_time) + values + + (#{u.userName}, #{u.nickName}, #{u.phonenumber}, + #{u.password}, #{u.status}, #{u.delFlag}, #{u.roleType}, + #{u.createBy}, sysdate()) + + + diff --git a/ry-api/simsun.ttc b/ry-api/simsun.ttc new file mode 100644 index 0000000..6ca8de3 Binary files /dev/null and b/ry-api/simsun.ttc differ diff --git a/ry-vue3/src/api/business/expert.js b/ry-vue3/src/api/business/expert.js index beb9f03..aaae85e 100644 --- a/ry-vue3/src/api/business/expert.js +++ b/ry-vue3/src/api/business/expert.js @@ -7,4 +7,35 @@ import request from '@/utils/request' */ export function getMyExpertProfile() { return request({ url: '/business/expert/profile', method: 'get' }) +} + +/** + * 批量导入专家 (axios 版, 后端 3 阶段批量: IN查 sys_user → batchInsert → batchInsert biz_expert) + * 用于"非 el-upload"场景 (比如自定义拖拽 / 直接 fetch). + * + * @param {File} file .xlsx / .xls 文件 + * @param {boolean} updateSupport 已存在 phone 是否跳过 (当前实现固定绑定已有 user_id) + * @returns {Promise<{code,msg,data:ImportResult}>} + */ +export function importExpert(file, updateSupport = false) { + const form = new FormData() + form.append('file', file) + return request({ + url: '/business/expert/importData', + method: 'post', + data: form, + params: { updateSupport }, + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +/** + * 下载专家批量导入模板 (返回 blob, 前端用 a[download] 触发保存) + */ +export function downloadExpertImportTpl() { + return request({ + url: '/business/expert/importTemplate', + method: 'get', + responseType: 'blob' + }) } \ No newline at end of file diff --git a/ry-vue3/src/api/business/sign.js b/ry-vue3/src/api/business/sign.js new file mode 100644 index 0000000..7a8eff1 --- /dev/null +++ b/ry-vue3/src/api/business/sign.js @@ -0,0 +1,19 @@ +import request from '@/utils/request' + +/** + * 医生劳务协议签署 API + * 简化版: 不用 token, 走标准 session 验证 + * 医生登录后, 移动端 /doctor/sign-fill?attendeeId=X 打开 + */ +export function getSignInfo(attendeeId) { + return request({ url: '/business/sign/info', method: 'get', params: { attendeeId } }) +} +export function saveSignProfile(attendeeId, form) { + return request({ url: '/business/sign/saveProfile', method: 'post', params: { attendeeId }, data: form, __silentError: true }) +} +export function getSignContract(attendeeId) { + return request({ url: '/business/sign/contract', method: 'get', params: { attendeeId }, __silentError: true }) +} +export function submitSign(attendeeId, handsign, contentHtml) { + return request({ url: '/business/sign/submit', method: 'post', params: { attendeeId }, data: { handsign, contentHtml }, __silentError: true }) +} \ No newline at end of file diff --git a/ry-vue3/src/api/public.js b/ry-vue3/src/api/public.js index 6fb3c69..9085d54 100644 --- a/ry-vue3/src/api/public.js +++ b/ry-vue3/src/api/public.js @@ -148,3 +148,12 @@ export function deletePublicitySupportIntent(ids) { export function deletePublicityExecutionIntent(ids) { return request.delete(`/business/publicityExecutionIntent/${Array.isArray(ids) ? ids.join(',') : ids}`) } + +// 导出 (后端 ExcelUtil 写 .xlsx, responseType=blob, 由前端触发下载) +// 用法: exportPublicityExecutionIntent(q.value).then(res => { /* res: { data: blob } */ }) +export function exportPublicityExecutionIntent(params) { + return request.post('/business/publicityExecutionIntent/export', null, { params, responseType: 'blob' }) +} +export function exportPublicitySupportIntent(params) { + return request.post('/business/publicitySupportIntent/export', null, { params, responseType: 'blob' }) +} diff --git a/ry-vue3/src/components/AreaCascader.vue b/ry-vue3/src/components/AreaCascader.vue index 967f383..8c7eaab 100644 --- a/ry-vue3/src/components/AreaCascader.vue +++ b/ry-vue3/src/components/AreaCascader.vue @@ -42,6 +42,7 @@ const props = defineProps({ separator: { type: String, default: '/' }, format: { type: String, default: 'array' }, // 'array' | 'string' joinSep: { type: String, default: '/' }, + maxLevel: { type: Number, default: 3 }, // 级数限制: 2=省/市, 3=省/市/区 props: { type: Object, default: () => ({}) } }) @@ -76,8 +77,19 @@ function stringToValues(str) { return toValues(labels) } -// 静态引用, 组件树只在加载时构建一次 -const options = areaData +// 按 maxLevel 截断级数 (2=省/市, 3=省/市/区) +const options = computed(() => { + if (props.maxLevel >= 3) return areaData + return areaData.map(prov => ({ + ...prov, + children: props.maxLevel >= 2 + ? (prov.children || []).map(city => { + const { children, ...rest } = city + return rest + }) + : undefined + })) +}) const cascaderProps = computed(() => ({ value: 'value', diff --git a/ry-vue3/src/components/IdCardUploader.vue b/ry-vue3/src/components/IdCardUploader.vue index 107aa2a..03ea224 100644 --- a/ry-vue3/src/components/IdCardUploader.vue +++ b/ry-vue3/src/components/IdCardUploader.vue @@ -1,11 +1,7 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - 会议执行 (执行方人员固定, 暂唯一子类型) - - - - -
- 保存 - 取消 -
-
- - - - - \ No newline at end of file diff --git a/ry-vue3/src/views/admin/ExpertDetail.vue b/ry-vue3/src/views/admin/ExpertDetail.vue index dfa3caa..9b75609 100644 --- a/ry-vue3/src/views/admin/ExpertDetail.vue +++ b/ry-vue3/src/views/admin/ExpertDetail.vue @@ -7,7 +7,7 @@
@@ -97,8 +97,7 @@ @@ -120,16 +119,11 @@ - + - - - - - - - - + + + @@ -154,7 +148,7 @@ - - diff --git a/ry-vue3/src/views/auth/Login.vue b/ry-vue3/src/views/auth/Login.vue index 738bdc1..03a122f 100644 --- a/ry-vue3/src/views/auth/Login.vue +++ b/ry-vue3/src/views/auth/Login.vue @@ -34,8 +34,8 @@
@@ -172,7 +172,7 @@ + + \ No newline at end of file diff --git a/ry-vue3/src/views/doctor/SignFill.vue b/ry-vue3/src/views/doctor/SignFill.vue new file mode 100644 index 0000000..0c7bb90 --- /dev/null +++ b/ry-vue3/src/views/doctor/SignFill.vue @@ -0,0 +1,287 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/doctor/SignSuccess.vue b/ry-vue3/src/views/doctor/SignSuccess.vue new file mode 100644 index 0000000..be6311b --- /dev/null +++ b/ry-vue3/src/views/doctor/SignSuccess.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/ry-vue3/src/views/admin/ExecutorOrgs.vue b/ry-vue3/src/views/executor-orgs/ExecutorOrgs.vue similarity index 65% rename from ry-vue3/src/views/admin/ExecutorOrgs.vue rename to ry-vue3/src/views/executor-orgs/ExecutorOrgs.vue index 5095ea3..f01a64e 100644 --- a/ry-vue3/src/views/admin/ExecutorOrgs.vue +++ b/ry-vue3/src/views/executor-orgs/ExecutorOrgs.vue @@ -1,12 +1,17 @@