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 阶段批量版):
+ *
+ * - Phase 0: ExcelUtil 反序列化 + 行级必填校验, 失败行累计到 result.ngList
+ * - Phase A: 100/批 IN 查 sys_user, 记录已有 phone → userId 映射
+ * - Phase B: 100/批 batch insert sys_user (新 phone), 插完再 IN 查回 userId; 同步 updateRoleType='doctor'
+ * - Phase C: 100/批 batch insert biz_expert (所有有效行), 已存在 phone 复用 Phase A userId
+ *
+ * 失败行为:
+ *
+ * - 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