feat(attendee): Excel 批量导入 (后端 /importData + /importTemplate + 5邀请差集推送, 前端 dialog-upload 套用 dialog-import SKILL)
This commit is contained in:
+58
@@ -1,19 +1,27 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
import com.ruoyi.business.domain.vo.BizMeetingAttendeeImportVo;
|
||||
import com.ruoyi.business.notify.BizNotifyService;
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
import com.ruoyi.business.service.IBizMeetingService;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 会议参会人 Controller (劳务协议 / 手写签名 + 管理端 CRUD)
|
||||
*
|
||||
@@ -119,6 +127,56 @@ public class BizMeetingAttendeeController extends BaseController {
|
||||
return toAjax(attendeeService.updateHandsign(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入参会人 — Excel 模板下载 (Manager/Admin 在 MeetingDetail "批量导入" 按钮触发).
|
||||
*
|
||||
* <p>复用 {@link BizMeetingAttendeeImportVo} 的 @Excel 注解自动生成表头.
|
||||
*/
|
||||
@GetMapping("/importTemplate")
|
||||
public void importTemplate(HttpServletResponse response) {
|
||||
ExcelUtil<BizMeetingAttendeeImportVo> util = new ExcelUtil<>(BizMeetingAttendeeImportVo.class);
|
||||
util.importTemplateExcel(response, "参会人导入");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入参会人 — 上传 Excel + 解析入库 + #5 会议邀请差集推送.
|
||||
*
|
||||
* <p>三步:
|
||||
* <ol>
|
||||
* <li>查"导入前"该会议已存在的参会人 userIds (Set)</li>
|
||||
* <li>调 {@link IBizMeetingAttendeeService#importFromExcel} 逐行处理, 失败的进 ngList</li>
|
||||
* <li>查"导入后"该会议 userIds, 与"前"做差集 → 仅给"新加入"的 userId 推 #5 会议邀请</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>返回 ImportResult { okNum, ngNum, ngList: [{rowNum, message}] }, 前端 ImportResultDialog 直接渲染.
|
||||
*
|
||||
* @param meetingId 会议 id (query 传, 决定导入到哪个会议)
|
||||
*/
|
||||
@Log(title = "参会人批量导入", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/importData")
|
||||
public AjaxResult importData(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||
// 1. 导入前快照
|
||||
Set<Long> preUserIds = new HashSet<>(attendeeService.selectUserIdsByMeetingId(meetingId));
|
||||
|
||||
// 2. 解析 + 入库
|
||||
ImportResult result = attendeeService.importFromExcel(file, meetingId, SecurityUtils.getUsername());
|
||||
|
||||
// 3. 差集 → 仅对"新加入" userId 推 #5 邀请
|
||||
Set<Long> postUserIds = new HashSet<>(attendeeService.selectUserIdsByMeetingId(meetingId));
|
||||
postUserIds.removeAll(preUserIds);
|
||||
if (!postUserIds.isEmpty()) {
|
||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||
String meetingName = m != null ? m.getMeetingName() : null;
|
||||
java.util.Date startTime = m != null ? m.getStartTime() : null;
|
||||
for (Long uid : postUserIds) {
|
||||
bizNotifyService.meetingInvitation(uid, meetingId, meetingName, startTime);
|
||||
}
|
||||
}
|
||||
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新劳务协议 URL (OSS 上传后调本接口).
|
||||
*
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.ruoyi.business.domain.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 会议参会人 Excel 导入 VO
|
||||
*
|
||||
* <p>仅用于 Excel 批量导入 + 模板下载. 字段顺序与 Excel 列一一对应, 调 sort 时同步改模板下载.
|
||||
*
|
||||
* <p>关键约束:
|
||||
* <ul>
|
||||
* <li>手机号 必填, 11 位 — 按手机号查/建 sys_user (roleType='doctor')</li>
|
||||
* <li>其它档案字段均 可选, 不填则存 NULL/0</li>
|
||||
* <li>meetingId 不在 Excel 里, 通过 controller 的 ?meetingId=xxx query 传</li>
|
||||
* <li>不导入签字字段 (handsign / labor_protocol / signed_at / signed_ip) — 由医生端流程触发</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
public class BizMeetingAttendeeImportVo {
|
||||
|
||||
/** 手机号 (必填, 按此查/建 sys_user) */
|
||||
@Excel(name = "手机号", sort = 1)
|
||||
private String phone;
|
||||
|
||||
/** 姓名 */
|
||||
@Excel(name = "姓名", sort = 2)
|
||||
private String name;
|
||||
|
||||
/** 工作单位 */
|
||||
@Excel(name = "工作单位", sort = 3)
|
||||
private String workUnit;
|
||||
|
||||
/** 科室 */
|
||||
@Excel(name = "科室", sort = 4)
|
||||
private String department;
|
||||
|
||||
/** 职称 */
|
||||
@Excel(name = "职称", sort = 5)
|
||||
private String title;
|
||||
|
||||
/** 身份证号 */
|
||||
@Excel(name = "身份证号", sort = 6)
|
||||
private String idCard;
|
||||
|
||||
/** 银行 */
|
||||
@Excel(name = "银行", sort = 7)
|
||||
private String bankName;
|
||||
|
||||
/** 银行卡号码 */
|
||||
@Excel(name = "银行卡号码", sort = 8)
|
||||
private String bankCard;
|
||||
|
||||
/** 开户行 */
|
||||
@Excel(name = "开户行", sort = 9)
|
||||
private String bankBranch;
|
||||
|
||||
/** 角色 (原"劳务形式": 授课/主持/评审...) */
|
||||
@Excel(name = "角色", sort = 10)
|
||||
private String laborForm;
|
||||
|
||||
/** 应发金额 (decimal, 元) */
|
||||
@Excel(name = "应发金额", sort = 11)
|
||||
private BigDecimal feePreTax;
|
||||
|
||||
/** 个税税金 (decimal, 元) */
|
||||
@Excel(name = "个税税金", sort = 12)
|
||||
private BigDecimal tax;
|
||||
|
||||
/** 增值税及附加成本 (decimal, 元) */
|
||||
@Excel(name = "增值税及附加成本", sort = 13)
|
||||
private BigDecimal vatAndSurcharge;
|
||||
|
||||
/** 实发金额 (decimal, 元) */
|
||||
@Excel(name = "实发金额", sort = 14)
|
||||
private BigDecimal fee;
|
||||
|
||||
/** 摘要 */
|
||||
@Excel(name = "摘要", sort = 15)
|
||||
private String summary;
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
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 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 getBankName() { return bankName; }
|
||||
public void setBankName(String bankName) { this.bankName = bankName; }
|
||||
public String getBankCard() { return bankCard; }
|
||||
public void setBankCard(String bankCard) { this.bankCard = bankCard; }
|
||||
public String getBankBranch() { return bankBranch; }
|
||||
public void setBankBranch(String bankBranch) { this.bankBranch = bankBranch; }
|
||||
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 getVatAndSurcharge() { return vatAndSurcharge; }
|
||||
public void setVatAndSurcharge(BigDecimal vatAndSurcharge) { this.vatAndSurcharge = vatAndSurcharge; }
|
||||
public BigDecimal getFee() { return fee; }
|
||||
public void setFee(BigDecimal fee) { this.fee = fee; }
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
}
|
||||
+15
@@ -1,7 +1,9 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
|
||||
public interface IBizMeetingAttendeeService {
|
||||
int insert(BizMeetingAttendee entity);
|
||||
@@ -45,4 +47,17 @@ public interface IBizMeetingAttendeeService {
|
||||
* 列表实现层直接返 mapper 结果; 业务方通常用 {@code new HashSet<>(service.selectUserIdsByMeetingId(mid))} 做 contains 判断.
|
||||
*/
|
||||
List<Long> selectUserIdsByMeetingId(Long meetingId);
|
||||
|
||||
/**
|
||||
* Excel 批量导入参会人 (MeetingDetail "批量导入" 按钮).
|
||||
*
|
||||
* <p>每行处理: 解析 → 校验手机号 → 复用 {@link #insertByPhoneWithProfile} (查/建 sys_user + 写 attendee 行) →
|
||||
* 单行失败不入 ngList, 其它行继续. 返回的 ImportResult 含 okNum/ngNum/ngList 给前端 ImportResultDialog.
|
||||
*
|
||||
* <p>注意: 本方法**只插数据**, 不触发 #5 会议邀请通知. 调用方 (controller) 拿到成功行后统一调
|
||||
* {@code bizNotifyService.meetingInvitation} 推通知, 避免在 service 层耦合 notify 依赖.
|
||||
*
|
||||
* @return ImportResult { okNum, ngNum, ngList: [{rowNum, message}] }
|
||||
*/
|
||||
ImportResult importFromExcel(MultipartFile file, Long meetingId, String operName) throws Exception;
|
||||
}
|
||||
+70
-1
@@ -6,12 +6,16 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
import com.ruoyi.business.domain.vo.BizMeetingAttendeeImportVo;
|
||||
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||
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.poi.ExcelUtil;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
|
||||
@@ -160,4 +164,69 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
||||
public List<Long> selectUserIdsByMeetingId(Long meetingId) {
|
||||
return mapper.selectUserIdsByMeetingId(meetingId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入参会人 (Excel → biz_meeting_attendee).
|
||||
*
|
||||
* <p>复用 {@link #insertByPhoneWithProfile} 做单行处理, 让"按手机号查/建 sys_user + 写 attendee"
|
||||
* 逻辑和单条新增一致. 单行失败只计入 ngList, 不中断其它行.
|
||||
*
|
||||
* <p>注意: 本方法只插数据, 不发 #5 通知. controller 在 import 前快照 userIds, import 后 diff,
|
||||
* 只对"新加入"的 userId 调 notify, 避免给已存在的参会人重发邀请.
|
||||
*/
|
||||
@Override
|
||||
public ImportResult importFromExcel(MultipartFile file, Long meetingId, String operName) throws Exception {
|
||||
if (meetingId == null) throw new ServiceException("meetingId 不能为空");
|
||||
|
||||
ExcelUtil<BizMeetingAttendeeImportVo> util = new ExcelUtil<>(BizMeetingAttendeeImportVo.class);
|
||||
List<BizMeetingAttendeeImportVo> rows = util.importExcel(file.getInputStream());
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
}
|
||||
|
||||
ImportResult result = new ImportResult();
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
BizMeetingAttendeeImportVo vo = rows.get(i);
|
||||
int rowNo = i + 2; // Excel 行号: 1=表头, 2=第一行数据
|
||||
try {
|
||||
// 行级校验: 手机号必填 + 格式
|
||||
if (vo.getPhone() == null || vo.getPhone().trim().isEmpty()) {
|
||||
result.fail(rowNo, "手机号不能为空");
|
||||
continue;
|
||||
}
|
||||
String phone = vo.getPhone().trim();
|
||||
if (!phone.matches("^1[3-9]\\d{9}$")) {
|
||||
result.fail(rowNo, "手机号格式不正确: " + phone);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构造 attendee 实体, 复用 insertByPhoneWithProfile (内部查/建 sys_user + 写 attendee)
|
||||
BizMeetingAttendee body = new BizMeetingAttendee();
|
||||
body.setMeetingId(meetingId);
|
||||
body.setPhone(phone);
|
||||
body.setName(vo.getName());
|
||||
body.setWorkUnit(vo.getWorkUnit());
|
||||
body.setDepartment(vo.getDepartment());
|
||||
body.setTitle(vo.getTitle());
|
||||
body.setIdCard(vo.getIdCard());
|
||||
body.setBankName(vo.getBankName());
|
||||
body.setBankCard(vo.getBankCard());
|
||||
body.setBankBranch(vo.getBankBranch());
|
||||
body.setLaborForm(vo.getLaborForm());
|
||||
body.setFeePreTax(vo.getFeePreTax());
|
||||
body.setTax(vo.getTax());
|
||||
body.setVatAndSurcharge(vo.getVatAndSurcharge());
|
||||
body.setFee(vo.getFee());
|
||||
body.setSummary(vo.getSummary());
|
||||
|
||||
insertByPhoneWithProfile(body); // 失败抛 ServiceException, 被 catch
|
||||
result.ok();
|
||||
} catch (Exception e) {
|
||||
String msg = e.getMessage();
|
||||
result.fail(rowNo, msg == null ? "导入失败" : msg);
|
||||
}
|
||||
}
|
||||
log.info("[attendee] 批量导入完成 meetingId={} ok={} ng={}",
|
||||
meetingId, result.getOkNum(), result.getNgNum());
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user