Compare commits

...

21 Commits

Author SHA1 Message Date
wkc
aa08ab4711 员工亲属实体关联 2026-04-24 13:29:13 +08:00
wkc
b7db711906 完成员工亲属实体关联改造并清理旧数据 2026-04-24 08:55:05 +08:00
wkc
b7d020c0b2 调整实体库管理数据来源维护规则 2026-04-23 17:31:56 +08:00
wkc
d444eafd5f 调整征信记录维护页面搜索区并去掉顶部标题 2026-04-23 17:24:52 +08:00
wkc
c9398881f3 新增员工亲属实体关联维护实施计划 2026-04-23 16:52:15 +08:00
wkc
d79a60ab8c 新增员工亲属实体关联维护设计文档 2026-04-23 16:47:26 +08:00
wkc
e9403662e2 新增信息维护页面搜索区四列栅格前端实施计划 2026-04-23 16:40:06 +08:00
wkc
fd79bfe62f 新增信息维护页面搜索区四列栅格统一设计文档 2026-04-23 16:21:13 +08:00
wkc
c7f4982451 补充招投标供应商企业详情查看 2026-04-23 16:12:56 +08:00
wkc
0b2571b962 统一信息维护页面头部按钮布局 2026-04-23 16:07:17 +08:00
wkc
129e44c808 新增信息维护页面头部按钮统一前端实施计划 2026-04-23 15:53:32 +08:00
wkc
50c177da78 新增信息维护页面头部按钮统一设计文档 2026-04-23 15:50:44 +08:00
wkc
c660025bcc 完善招投标供应商企业详情设计与计划 2026-04-23 15:40:42 +08:00
wkc
bd51991248 新增招投标供应商企业详情设计文档 2026-04-23 15:18:12 +08:00
wkc
a2ba044ebe 完成招聘双Sheet导入改造 2026-04-23 10:27:08 +08:00
wkc
110817abba 新增招聘信息双Sheet导入实施计划 2026-04-23 09:45:50 +08:00
wkc
ff9627d0d9 新增招聘信息双Sheet导入设计文档 2026-04-23 09:40:15 +08:00
wkc
2d1b02474c Add import page test guidelines 2026-04-22 16:38:52 +08:00
wkc
5a9b79d4ee 完善招投标导入测试与文档 2026-04-22 16:20:37 +08:00
wkc
0c5fa6b2c8 Remove obsolete export APIs and persist recruitment work history 2026-04-22 13:38:43 +08:00
wkc
94507e3747 Unify staff recruitment work table collation 2026-04-22 10:07:05 +08:00
197 changed files with 12118 additions and 2788 deletions

BIN
.DS_Store vendored

Binary file not shown.

6
.gitignore vendored
View File

@@ -89,4 +89,8 @@ ruoyi-ui/vue.config.js
tests/
tongweb_62318.properties
tongweb_62318.properties
.superpowers/
tmp/

View File

@@ -80,7 +80,7 @@
mvn clean compile
# 启动主应用Jar
cd ruoyi-admin/target && java -jar ruoyi-admin.jar
sh bin/restart_java_backend.sh
# 打包全部模块
mvn clean package
@@ -225,6 +225,18 @@ return AjaxResult.success(result);
- 返回结果仅展示失败数据
- 大数据量导入优先采用 EasyExcel + 异步处理
### 导入页面测试规范
- 导入功能测试必须进入真实业务页面执行,先在页面内下载当前导入模板,再基于该模板生成测试文件,禁止手工凭记忆新建表头或脱离页面直接构造上传文件
- 双 Sheet 模板的导入测试必须覆盖两个 Sheet 的联动关系;除“缺少 Sheet / 空 Sheet”专项场景外默认两个 Sheet 都要准备测试数据
- 导入测试文件优先放在 `output/spreadsheet/``output/playwright/`,不提交到 git
- 需要按场景拆分测试文件,避免多个互斥校验互相覆盖;至少覆盖空模板、主信息必填、主信息格式与金额、主从关系异常、供应商校验、缺少/空 Sheet、成功导入、成功与失败混合、失败记录查看、导入后清理回滚
- 主从关系异常测试至少覆盖:已存在主键、供应商有数据但主信息缺失、主信息重复、供应商 Sheet 中采购事项 ID 为空
- 供应商校验测试至少覆盖:重复供应商、多条中标、供应商名称为空、名称超长、联系人超长、银行账户超长、联系电话非法、统一信用代码非法、是否中标枚举非法
- 页面上传后必须核对页面提示、导入状态、失败记录弹窗和列表总数变化;异步导入场景还要核对任务状态从 `PROCESSING` 到最终状态的变化
- 对“成功导入 + 异常数据混合”的样本,必须额外核对成功数据是否真正入库、异常数据是否被拦截,以及是否存在被静默忽略的行
- 导入测试结束后,必须删除本轮成功写入的测试数据,清理页面本地导入任务缓存,并关闭测试过程中启动的前后端进程
---
## 当前仓库结构
@@ -348,4 +360,3 @@ ccdi/
- `sql/migration/` 用于增量迁移脚本,新增修复脚本优先按日期或功能命名
- 启动前后端或 Mock 服务做验证后,结束测试时要主动停止进程,避免残留占用端口
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本

View File

@@ -215,7 +215,7 @@ follow_logs() {
start_action() {
running_pids=$(collect_pids)
if [ -n "${running_pids:-}" ]; then
log_error "检测到已有后端进程在运行: $running_pids,请先执行 stop 或 restart"
log_error "检测到已有后端进程在运行: ${running_pids},请先执行 stop 或 restart"
exit 1
fi

View File

@@ -80,18 +80,6 @@ public class CcdiAccountInfoController extends BaseController {
return success(accountInfoService.selectAccountInfoById(id));
}
/**
* 导出账户库列表
*/
@Operation(summary = "导出账户库列表")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:export')")
@Log(title = "账户库管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiAccountInfoQueryDTO queryDTO) {
List<CcdiAccountInfoExcel> list = accountInfoService.selectAccountInfoListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiAccountInfoExcel.class, "账户库管理");
}
/**
* 新增账户
*/

View File

@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.*;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
@@ -45,6 +47,9 @@ public class CcdiBaseStaffController extends BaseController {
@Resource
private ICcdiBaseStaffImportService importAsyncService;
@Resource
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
/**
* 查询员工列表
*/
@@ -70,18 +75,6 @@ public class CcdiBaseStaffController extends BaseController {
return success(list);
}
/**
* 导出员工列表
*/
@Operation(summary = "导出员工列表")
@PreAuthorize("@ss.hasPermi('ccdi:baseStaff:export')")
@Log(title = "员工信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiBaseStaffQueryDTO queryDTO) {
List<CcdiBaseStaffExcel> list = baseStaffService.selectBaseStaffListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiBaseStaffExcel.class, "员工信息");
}
/**
* 获取员工详细信息
*/
@@ -132,7 +125,14 @@ public class CcdiBaseStaffController extends BaseController {
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiBaseStaffExcel.class, "员工信息");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiBaseStaffExcel.class,
"员工信息",
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息",
"员工信息维护导入模板"
);
}
/**
@@ -142,21 +142,33 @@ public class CcdiBaseStaffController extends BaseController {
@PreAuthorize("@ss.hasPermi('ccdi:baseStaff:import')")
@Log(title = "员工信息", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
List<CcdiBaseStaffExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiBaseStaffExcel.class);
public AjaxResult importData(MultipartFile file) throws Exception {
List<CcdiBaseStaffExcel> staffList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiBaseStaffExcel.class,
"员工信息"
);
List<CcdiBaseStaffAssetInfoExcel> assetList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息"
);
if (list == null || list.isEmpty()) {
boolean hasStaffRows = staffList != null && !staffList.isEmpty();
boolean hasAssetRows = assetList != null && !assetList.isEmpty();
if (!hasStaffRows && !hasAssetRows) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = baseStaffService.importBaseStaff(list, updateSupport);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO();
if (hasStaffRows) {
result.setStaffTaskId(baseStaffService.importBaseStaff(staffList));
}
if (hasAssetRows) {
result.setAssetTaskId(baseStaffAssetImportService.importAssetInfo(assetList));
}
result.setMessage(buildImportSubmitMessage(hasStaffRows, hasAssetRows));
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
@@ -202,4 +214,14 @@ public class CcdiBaseStaffController extends BaseController {
return getDataTable(pageData, failures.size());
}
private String buildImportSubmitMessage(boolean hasStaffRows, boolean hasAssetRows) {
if (hasStaffRows && hasAssetRows) {
return "已提交员工信息和员工资产信息导入任务";
}
if (hasStaffRows) {
return "已提交员工信息导入任务";
}
return "已提交员工资产信息导入任务";
}
}

View File

@@ -63,18 +63,6 @@ public class CcdiCustEnterpriseRelationController extends BaseController {
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 导出信贷客户实体关联列表
*/
@Operation(summary = "导出信贷客户实体关联列表")
@PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:export')")
@Log(title = "信贷客户实体关联信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiCustEnterpriseRelationQueryDTO queryDTO) {
List<CcdiCustEnterpriseRelationExcel> list = relationService.selectRelationListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiCustEnterpriseRelationExcel.class, "信贷客户实体关联信息");
}
/**
* 获取信贷客户实体关联详细信息
*/

View File

@@ -103,17 +103,6 @@ public class CcdiCustFmyRelationController extends BaseController {
return toAjax(relationService.deleteRelationByIds(ids));
}
/**
* 导出信贷客户家庭关系
*/
@Operation(summary = "导出信贷客户家庭关系")
@PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:export')")
@Log(title = "信贷客户家庭关系", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiCustFmyRelationQueryDTO query) {
relationService.exportRelations(query, response);
}
/**
* 下载带字典下拉框的导入模板
* 使用@DictDropdown注解自动添加下拉框

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
@@ -33,12 +34,12 @@ import java.util.ArrayList;
import java.util.List;
/**
* 采购交易信息Controller
* 招投标信息维护Controller
*
* @author ruoyi
* @date 2026-02-06
*/
@Tag(name = "采购交易信息管理")
@Tag(name = "招投标信息维护")
@RestController
@RequestMapping("/ccdi/purchaseTransaction")
public class CcdiPurchaseTransactionController extends BaseController {
@@ -50,9 +51,9 @@ public class CcdiPurchaseTransactionController extends BaseController {
private ICcdiPurchaseTransactionImportService transactionImportService;
/**
* 查询采购交易列表
* 查询招投标信息列表
*/
@Operation(summary = "查询采购交易列表")
@Operation(summary = "查询招投标信息列表")
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:list')")
@GetMapping("/list")
public TableDataInfo list(CcdiPurchaseTransactionQueryDTO queryDTO) {
@@ -64,21 +65,9 @@ public class CcdiPurchaseTransactionController extends BaseController {
}
/**
* 导出采购交易列表
* 获取招投标信息详细信息
*/
@Operation(summary = "导出采购交易列表")
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:export')")
@Log(title = "采购交易信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiPurchaseTransactionQueryDTO queryDTO) {
List<CcdiPurchaseTransactionExcel> list = transactionService.selectTransactionListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiPurchaseTransactionExcel.class, "采购交易信息");
}
/**
* 获取采购交易详细信息
*/
@Operation(summary = "获取采购交易详细信息")
@Operation(summary = "获取招投标信息详细信息")
@Parameter(name = "purchaseId", description = "采购事项ID", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:query')")
@GetMapping(value = "/{purchaseId}")
@@ -87,66 +76,81 @@ public class CcdiPurchaseTransactionController extends BaseController {
}
/**
* 新增采购交易
* 新增招投标信息
*/
@Operation(summary = "新增采购交易")
@Operation(summary = "新增招投标信息")
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:add')")
@Log(title = "采购交易信息", businessType = BusinessType.INSERT)
@Log(title = "招投标信息维护", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiPurchaseTransactionAddDTO addDTO) {
return toAjax(transactionService.insertTransaction(addDTO));
}
/**
* 修改采购交易
* 修改招投标信息
*/
@Operation(summary = "修改采购交易")
@Operation(summary = "修改招投标信息")
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:edit')")
@Log(title = "采购交易信息", businessType = BusinessType.UPDATE)
@Log(title = "招投标信息维护", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiPurchaseTransactionEditDTO editDTO) {
return toAjax(transactionService.updateTransaction(editDTO));
}
/**
* 删除采购交易
* 删除招投标信息
*/
@Operation(summary = "删除采购交易")
@Operation(summary = "删除招投标信息")
@Parameter(name = "purchaseIds", description = "采购事项ID数组", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:remove')")
@Log(title = "采购交易信息", businessType = BusinessType.DELETE)
@Log(title = "招投标信息维护", businessType = BusinessType.DELETE)
@DeleteMapping("/{purchaseIds}")
public AjaxResult remove(@PathVariable String[] purchaseIds) {
return toAjax(transactionService.deleteTransactionByIds(purchaseIds));
}
/**
* 下载带字典下拉框的导入模板
* 使用@DictDropdown注解自动添加下拉框
* 下载双Sheet导入模板
*/
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiPurchaseTransactionExcel.class, "采购交易信息");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiPurchaseTransactionExcel.class,
"招投标主信息",
CcdiPurchaseTransactionSupplierExcel.class,
"供应商明细",
"招投标信息维护导入模板"
);
}
/**
* 异步导入采购交易
* 异步导入招投标信息
*/
@Operation(summary = "异步导入采购交易")
@Operation(summary = "异步导入招投标信息")
@Parameter(name = "file", description = "导入文件", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:import')")
@Log(title = "采购交易信息", businessType = BusinessType.IMPORT)
@Log(title = "招投标信息维护", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiPurchaseTransactionExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiPurchaseTransactionExcel.class);
List<CcdiPurchaseTransactionExcel> mainList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiPurchaseTransactionExcel.class,
"招投标主信息"
);
List<CcdiPurchaseTransactionSupplierExcel> supplierList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiPurchaseTransactionSupplierExcel.class,
"供应商明细"
);
if (list == null || list.isEmpty()) {
if ((mainList == null || mainList.isEmpty()) && (supplierList == null || supplierList.isEmpty())) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = transactionService.importTransaction(list);
String taskId = transactionService.importTransaction(mainList, supplierList);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
@@ -33,12 +34,12 @@ import java.util.ArrayList;
import java.util.List;
/**
* 员工实体关系信息Controller
* 员工亲属实体关Controller
*
* @author ruoyi
* @date 2026-02-09
*/
@Tag(name = "员工实体关系信息管理")
@Tag(name = "员工亲属实体关管理")
@RestController
@RequestMapping("/ccdi/staffEnterpriseRelation")
public class CcdiStaffEnterpriseRelationController extends BaseController {
@@ -50,9 +51,9 @@ public class CcdiStaffEnterpriseRelationController extends BaseController {
private ICcdiStaffEnterpriseRelationImportService relationImportService;
/**
* 查询员工实体关列表
* 查询员工亲属实体关列表
*/
@Operation(summary = "查询员工实体关列表")
@Operation(summary = "查询员工亲属实体关列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:list')")
@GetMapping("/list")
public TableDataInfo list(CcdiStaffEnterpriseRelationQueryDTO queryDTO) {
@@ -64,21 +65,20 @@ public class CcdiStaffEnterpriseRelationController extends BaseController {
}
/**
* 导出员工实体关系列表
* 查询有效员工亲属下拉列表
*/
@Operation(summary = "导出员工实体关系列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:export')")
@Log(title = "员工实体关系信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiStaffEnterpriseRelationQueryDTO queryDTO) {
List<CcdiStaffEnterpriseRelationExcel> list = relationService.selectRelationListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiStaffEnterpriseRelationExcel.class, "员工实体关系信息");
@Operation(summary = "查询有效员工亲属下拉列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:list')")
@GetMapping("/familyOptions")
public AjaxResult familyOptions(@RequestParam(required = false) String query) {
List<CcdiStaffEnterpriseRelationOptionVO> list = relationService.selectFamilyOptions(query);
return success(list);
}
/**
* 获取员工实体关详细信息
* 获取员工亲属实体关详细信息
*/
@Operation(summary = "获取员工实体关详细信息")
@Operation(summary = "获取员工亲属实体关详细信息")
@Parameter(name = "id", description = "主键ID", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:query')")
@GetMapping(value = "/{id}")
@@ -87,34 +87,34 @@ public class CcdiStaffEnterpriseRelationController extends BaseController {
}
/**
* 新增员工实体关
* 新增员工亲属实体关
*/
@Operation(summary = "新增员工实体关")
@Operation(summary = "新增员工亲属实体关")
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:add')")
@Log(title = "员工实体关系信息", businessType = BusinessType.INSERT)
@Log(title = "员工亲属实体关", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiStaffEnterpriseRelationAddDTO addDTO) {
return toAjax(relationService.insertRelation(addDTO));
}
/**
* 修改员工实体关
* 修改员工亲属实体关
*/
@Operation(summary = "修改员工实体关")
@Operation(summary = "修改员工亲属实体关")
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:edit')")
@Log(title = "员工实体关系信息", businessType = BusinessType.UPDATE)
@Log(title = "员工亲属实体关", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiStaffEnterpriseRelationEditDTO editDTO) {
return toAjax(relationService.updateRelation(editDTO));
}
/**
* 删除员工实体关
* 删除员工亲属实体关
*/
@Operation(summary = "删除员工实体关")
@Operation(summary = "删除员工亲属实体关")
@Parameter(name = "ids", description = "主键ID数组", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:remove')")
@Log(title = "员工实体关系信息", businessType = BusinessType.DELETE)
@Log(title = "员工亲属实体关", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(relationService.deleteRelationByIds(ids));
@@ -127,16 +127,16 @@ public class CcdiStaffEnterpriseRelationController extends BaseController {
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffEnterpriseRelationExcel.class, "员工实体关系信息");
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffEnterpriseRelationExcel.class, "员工亲属实体关");
}
/**
* 异步导入员工实体关
* 异步导入员工亲属实体关
*/
@Operation(summary = "异步导入员工实体关")
@Operation(summary = "异步导入员工亲属实体关")
@Parameter(name = "file", description = "导入文件", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffEnterpriseRelation:import')")
@Log(title = "员工实体关系信息", businessType = BusinessType.IMPORT)
@Log(title = "员工亲属实体关", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiStaffEnterpriseRelationExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffEnterpriseRelationExcel.class);
@@ -152,9 +152,9 @@ public class CcdiStaffEnterpriseRelationController extends BaseController {
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
result.setMessage("员工亲属实体关联导入任务已提交,正在后台处理");
return AjaxResult.success("导入任务已提交,正在后台处理", result);
return AjaxResult.success("员工亲属实体关联导入任务已提交,正在后台处理", result);
}
/**

View File

@@ -4,11 +4,13 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
@@ -49,6 +51,9 @@ public class CcdiStaffFmyRelationController extends BaseController {
@Resource
private ICcdiStaffFmyRelationImportService relationImportService;
@Resource
private ICcdiAssetInfoImportService assetInfoImportService;
/**
* 查询员工亲属关系列表
*/
@@ -63,18 +68,6 @@ public class CcdiStaffFmyRelationController extends BaseController {
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 导出员工亲属关系列表
*/
@Operation(summary = "导出员工亲属关系列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:export')")
@Log(title = "员工亲属关系", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiStaffFmyRelationQueryDTO queryDTO) {
List<CcdiStaffFmyRelationExcel> list = relationService.selectRelationListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiStaffFmyRelationExcel.class, "员工亲属关系信息");
}
/**
* 获取员工亲属关系详细信息
*/
@@ -127,7 +120,14 @@ public class CcdiStaffFmyRelationController extends BaseController {
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffFmyRelationExcel.class, "员工亲属关系信息");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiStaffFmyRelationExcel.class,
"员工亲属关系信息",
CcdiAssetInfoExcel.class,
"亲属资产信息",
"员工亲属关系维护导入模板"
);
}
/**
@@ -139,20 +139,32 @@ public class CcdiStaffFmyRelationController extends BaseController {
@Log(title = "员工亲属关系", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiStaffFmyRelationExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffFmyRelationExcel.class);
List<CcdiStaffFmyRelationExcel> relationList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiStaffFmyRelationExcel.class,
"员工亲属关系信息"
);
List<CcdiAssetInfoExcel> assetList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiAssetInfoExcel.class,
"亲属资产信息"
);
if (list == null || list.isEmpty()) {
boolean hasRelationRows = relationList != null && !relationList.isEmpty();
boolean hasAssetRows = assetList != null && !assetList.isEmpty();
if (!hasRelationRows && !hasAssetRows) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = relationService.importRelation(list);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
StaffFmyRelationImportSubmitResultVO result = new StaffFmyRelationImportSubmitResultVO();
if (hasRelationRows) {
result.setRelationTaskId(relationService.importRelation(relationList));
}
if (hasAssetRows) {
result.setAssetTaskId(assetInfoImportService.importAssetInfo(assetList));
}
result.setMessage(buildImportSubmitMessage(hasRelationRows, hasAssetRows));
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
@@ -198,4 +210,14 @@ public class CcdiStaffFmyRelationController extends BaseController {
return getDataTable(pageData, failures.size());
}
private String buildImportSubmitMessage(boolean hasRelationRows, boolean hasAssetRows) {
if (hasRelationRows && hasAssetRows) {
return "已提交员工亲属关系和亲属资产信息导入任务";
}
if (hasRelationRows) {
return "已提交员工亲属关系导入任务";
}
return "已提交亲属资产信息导入任务";
}
}

View File

@@ -64,18 +64,6 @@ public class CcdiStaffRecruitmentController extends BaseController {
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 导出招聘信息列表
*/
@Operation(summary = "导出招聘信息列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:export')")
@Log(title = "员工招聘信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiStaffRecruitmentQueryDTO queryDTO) {
List<CcdiStaffRecruitmentExcel> list = recruitmentService.selectRecruitmentListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiStaffRecruitmentExcel.class, "员工招聘信息");
}
/**
* 获取招聘信息详细信息
*/
@@ -126,16 +114,14 @@ public class CcdiStaffRecruitmentController extends BaseController {
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentExcel.class, "员工招聘信息");
}
/**
* 下载历史工作经历导入模板
*/
@Operation(summary = "下载历史工作经历导入模板")
@PostMapping("/workImportTemplate")
public void workImportTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentWorkExcel.class, "历史工作经历");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiStaffRecruitmentExcel.class,
"招聘信息",
CcdiStaffRecruitmentWorkExcel.class,
"历史工作经历",
"招聘信息管理导入模板"
);
}
/**
@@ -147,16 +133,25 @@ public class CcdiStaffRecruitmentController extends BaseController {
@Log(title = "员工招聘信息", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiStaffRecruitmentExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffRecruitmentExcel.class);
List<CcdiStaffRecruitmentExcel> recruitmentList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiStaffRecruitmentExcel.class,
"招聘信息"
);
List<CcdiStaffRecruitmentWorkExcel> workList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiStaffRecruitmentWorkExcel.class,
"历史工作经历"
);
if (list == null || list.isEmpty()) {
boolean hasRecruitmentRows = recruitmentList != null && !recruitmentList.isEmpty();
boolean hasWorkRows = workList != null && !workList.isEmpty();
if (!hasRecruitmentRows && !hasWorkRows) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = recruitmentService.importRecruitment(list);
String taskId = recruitmentService.importRecruitment(recruitmentList, workList);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
@@ -165,31 +160,6 @@ public class CcdiStaffRecruitmentController extends BaseController {
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
/**
* 异步导入历史工作经历
*/
@Operation(summary = "异步导入历史工作经历")
@Parameter(name = "file", description = "导入文件", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:import')")
@Log(title = "员工招聘历史工作经历", businessType = BusinessType.IMPORT)
@PostMapping("/importWorkData")
public AjaxResult importWorkData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiStaffRecruitmentWorkExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffRecruitmentWorkExcel.class);
if (list == null || list.isEmpty()) {
return error("至少需要一条数据");
}
String taskId = recruitmentService.importRecruitmentWork(list);
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("历史工作经历导入任务已提交,正在后台处理");
return AjaxResult.success("历史工作经历导入任务已提交,正在后台处理", result);
}
/**
* 查询导入状态
*/

View File

@@ -63,18 +63,6 @@ public class CcdiStaffTransferController extends BaseController {
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 导出员工调动记录列表
*/
@Operation(summary = "导出员工调动记录列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:export')")
@Log(title = "员工调动记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiStaffTransferQueryDTO queryDTO) {
List<CcdiStaffTransferExcel> list = transferService.selectTransferListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiStaffTransferExcel.class, "员工调动记录信息");
}
/**
* 获取员工调动记录详细信息
*/

View File

@@ -0,0 +1,64 @@
package com.ruoyi.info.collection.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 招投标供应商明细对象 ccdi_purchase_transaction_supplier
*/
@Data
public class CcdiPurchaseTransactionSupplier implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 采购事项ID */
private String purchaseId;
/** 供应商名称 */
private String supplierName;
/** 供应商统一信用代码 */
private String supplierUscc;
/** 供应商联系人 */
private String contactPerson;
/** 供应商联系电话 */
private String contactPhone;
/** 供应商银行账户 */
private String supplierBankAccount;
/** 是否中标1-是0-否 */
private Integer isBidWinner;
/** 排序 */
private Integer sortOrder;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/** 创建人 */
@TableField(fill = FieldFill.INSERT)
private String createdBy;
/** 更新人 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updatedBy;
}

View File

@@ -89,7 +89,6 @@ public class CcdiEnterpriseBaseInfoAddDTO implements Serializable {
private String shareholder5;
@Schema(description = "经营状态")
@NotBlank(message = "经营状态不能为空")
@Size(max = 50, message = "经营状态长度不能超过50个字符")
private String status;
@@ -102,6 +101,5 @@ public class CcdiEnterpriseBaseInfoAddDTO implements Serializable {
private String entSource;
@Schema(description = "数据来源")
@NotBlank(message = "数据来源不能为空")
private String dataSource;
}

View File

@@ -89,7 +89,6 @@ public class CcdiEnterpriseBaseInfoEditDTO implements Serializable {
private String shareholder5;
@Schema(description = "经营状态")
@NotBlank(message = "经营状态不能为空")
@Size(max = 50, message = "经营状态长度不能超过50个字符")
private String status;

View File

@@ -2,6 +2,7 @@ package com.ruoyi.info.collection.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
@@ -9,15 +10,16 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 采购交易信息新增DTO
* 招投标信息新增DTO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息新增")
@Schema(description = "招投标信息新增")
public class CcdiPurchaseTransactionAddDTO implements Serializable {
@Serial
@@ -88,30 +90,10 @@ public class CcdiPurchaseTransactionAddDTO implements Serializable {
@Schema(description = "采购方式")
private String purchaseMethod;
/** 中标供应商名称 */
@Size(max = 200, message = "中标供应商名称长度不能超过200个字符")
@Schema(description = "中标供应商名称")
private String supplierName;
/** 供应商联系人 */
@Size(max = 50, message = "供应商联系人长度不能超过50个字符")
@Schema(description = "供应商联系人")
private String contactPerson;
/** 供应商联系电话 */
@Pattern(regexp = "^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$", message = "供应商联系电话格式不正确")
@Schema(description = "供应商联系电话")
private String contactPhone;
/** 供应商统一信用代码 */
@Pattern(regexp = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", message = "供应商统一信用代码格式不正确")
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
/** 供应商银行账户 */
@Size(max = 50, message = "供应商银行账户长度不能超过50个字符")
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
/** 供应商明细 */
@Valid
@Schema(description = "供应商明细列表")
private List<CcdiPurchaseTransactionSupplierDTO> supplierList;
/** 采购申请日期(或立项日期) */
@NotNull(message = "采购申请日期不能为空")

View File

@@ -2,6 +2,7 @@ package com.ruoyi.info.collection.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
@@ -9,15 +10,16 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 采购交易信息编辑DTO
* 招投标信息编辑DTO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息编辑")
@Schema(description = "招投标信息编辑")
public class CcdiPurchaseTransactionEditDTO implements Serializable {
@Serial
@@ -88,30 +90,10 @@ public class CcdiPurchaseTransactionEditDTO implements Serializable {
@Schema(description = "采购方式")
private String purchaseMethod;
/** 中标供应商名称 */
@Size(max = 200, message = "中标供应商名称长度不能超过200个字符")
@Schema(description = "中标供应商名称")
private String supplierName;
/** 供应商联系人 */
@Size(max = 50, message = "供应商联系人长度不能超过50个字符")
@Schema(description = "供应商联系人")
private String contactPerson;
/** 供应商联系电话 */
@Pattern(regexp = "^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$", message = "供应商联系电话格式不正确")
@Schema(description = "供应商联系电话")
private String contactPhone;
/** 供应商统一信用代码 */
@Pattern(regexp = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", message = "供应商统一信用代码格式不正确")
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
/** 供应商银行账户 */
@Size(max = 50, message = "供应商银行账户长度不能超过50个字符")
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
/** 供应商明细 */
@Valid
@Schema(description = "供应商明细列表")
private List<CcdiPurchaseTransactionSupplierDTO> supplierList;
/** 采购申请日期(或立项日期) */
@NotNull(message = "采购申请日期不能为空")

View File

@@ -9,13 +9,13 @@ import java.io.Serializable;
import java.util.Date;
/**
* 采购交易信息查询DTO
* 招投标信息查询DTO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息查询条件")
@Schema(description = "招投标信息查询条件")
public class CcdiPurchaseTransactionQueryDTO implements Serializable {
@Serial

View File

@@ -0,0 +1,42 @@
package com.ruoyi.info.collection.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 招投标供应商明细DTO
*/
@Data
@Schema(description = "招投标供应商明细")
public class CcdiPurchaseTransactionSupplierDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotBlank(message = "供应商名称不能为空")
@Schema(description = "供应商名称")
private String supplierName;
@NotBlank(message = "供应商统一信用代码不能为空")
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
@Schema(description = "供应商联系人")
private String contactPerson;
@Schema(description = "供应商联系电话")
private String contactPhone;
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
@Schema(description = "是否中标1-是0-否")
private Integer isBidWinner;
@Schema(description = "排序")
private Integer sortOrder;
}

View File

@@ -10,22 +10,22 @@ import java.io.Serial;
import java.io.Serializable;
/**
* 员工实体关系信息新增DTO
* 员工亲属实体关新增DTO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工实体关系信息新增")
@Schema(description = "员工亲属实体关新增")
public class CcdiStaffEnterpriseRelationAddDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 身份证号 */
@NotBlank(message = "身份证号不能为空")
@Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$", message = "身份证号格式不正确")
@Schema(description = "身份证号")
/** 亲属身份证号 */
@NotBlank(message = "亲属身份证号不能为空")
@Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$", message = "亲属身份证号格式不正确")
@Schema(description = "亲属身份证号")
private String personId;
/** 关联人在企业的职务 */

View File

@@ -10,13 +10,13 @@ import java.io.Serial;
import java.io.Serializable;
/**
* 员工实体关系信息编辑DTO
* 员工亲属实体关编辑DTO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工实体关系信息编辑")
@Schema(description = "员工亲属实体关编辑")
public class CcdiStaffEnterpriseRelationEditDTO implements Serializable {
@Serial
@@ -27,8 +27,8 @@ public class CcdiStaffEnterpriseRelationEditDTO implements Serializable {
@Schema(description = "主键ID")
private Long id;
/** 身份证号 */
@Schema(description = "身份证号(不可修改)")
/** 亲属身份证号 */
@Schema(description = "亲属身份证号(不可修改)")
private String personId;
/** 关联人在企业的职务 */

View File

@@ -7,22 +7,30 @@ import java.io.Serial;
import java.io.Serializable;
/**
* 员工实体关系信息查询DTO
* 员工亲属实体关查询DTO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工实体关系信息查询条件")
@Schema(description = "员工亲属实体关查询条件")
public class CcdiStaffEnterpriseRelationQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 身份证号 */
@Schema(description = "身份证号")
/** 亲属身份证号 */
@Schema(description = "亲属身份证号")
private String personId;
/** 亲属姓名 */
@Schema(description = "亲属姓名")
private String relationName;
/** 关联员工 */
@Schema(description = "关联员工")
private String staffPersonName;
/** 统一社会信用代码 */
@Schema(description = "统一社会信用代码")
private String socialCreditCode;

View File

@@ -7,10 +7,12 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.validation.Valid;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 员工招聘信息编辑DTO
@@ -91,4 +93,8 @@ public class CcdiStaffRecruitmentEditDTO implements Serializable {
/** 面试官2工号 */
@Size(max = 10, message = "面试官2工号长度不能超过10个字符")
private String interviewerId2;
/** 历史工作经历列表 */
@Valid
private List<CcdiStaffRecruitmentWorkEditDTO> workExperienceList;
}

View File

@@ -0,0 +1,56 @@
package com.ruoyi.info.collection.domain.dto;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 招聘记录历史工作经历编辑DTO
*
* @author ruoyi
* @date 2026-04-22
*/
@Data
public class CcdiStaffRecruitmentWorkEditDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 排序号 */
private Integer sortOrder;
/** 工作单位 */
@Size(max = 200, message = "工作单位长度不能超过200个字符")
private String companyName;
/** 所属部门 */
@Size(max = 100, message = "所属部门长度不能超过100个字符")
private String departmentName;
/** 岗位名称 */
@Size(max = 100, message = "岗位名称长度不能超过100个字符")
private String positionName;
/** 入职年月 */
@Pattern(regexp = "^$|^((19|20)\\d{2})-(0[1-9]|1[0-2])$", message = "入职年月格式不正确,应为YYYY-MM")
private String jobStartMonth;
/** 离职年月 */
@Pattern(regexp = "^$|^((19|20)\\d{2})-(0[1-9]|1[0-2])$", message = "离职年月格式不正确,应为YYYY-MM")
private String jobEndMonth;
/** 离职原因 */
@Size(max = 500, message = "离职原因长度不能超过500个字符")
private String departureReason;
/** 主要工作内容 */
@Size(max = 1000, message = "主要工作内容长度不能超过1000个字符")
private String workContent;
/** 备注 */
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
}

View File

@@ -88,7 +88,7 @@ public class CcdiEnterpriseBaseInfoExcel implements Serializable {
@ColumnWidth(18)
private String shareholder5;
@ExcelProperty(value = "经营状态*", index = 16)
@ExcelProperty(value = "经营状态", index = 16)
@ColumnWidth(16)
private String status;
@@ -99,8 +99,4 @@ public class CcdiEnterpriseBaseInfoExcel implements Serializable {
@ExcelProperty(value = "企业来源*", index = 18)
@ColumnWidth(18)
private String entSource;
@ExcelProperty(value = "数据来源*", index = 19)
@ColumnWidth(18)
private String dataSource;
}

View File

@@ -11,7 +11,7 @@ import java.math.BigDecimal;
import java.util.Date;
/**
* 采购交易信息Excel导入导出对象
* 招投标主信息Excel导入导出对象
*
* @author ruoyi
* @date 2026-02-06
@@ -88,107 +88,82 @@ public class CcdiPurchaseTransactionExcel implements Serializable {
@Required
private String purchaseMethod;
/** 中标供应商名称 */
@ExcelProperty(value = "中标供应商名称", index = 12)
@ColumnWidth(25)
private String supplierName;
/** 供应商联系人 */
@ExcelProperty(value = "供应商联系人", index = 13)
@ColumnWidth(15)
private String contactPerson;
/** 供应商联系电话 */
@ExcelProperty(value = "供应商联系电话", index = 14)
@ColumnWidth(18)
private String contactPhone;
/** 供应商统一信用代码 */
@ExcelProperty(value = "供应商统一信用代码", index = 15)
@ColumnWidth(25)
private String supplierUscc;
/** 供应商银行账户 */
@ExcelProperty(value = "供应商银行账户", index = 16)
@ColumnWidth(20)
private String supplierBankAccount;
/** 采购申请日期(或立项日期) */
@ExcelProperty(value = "采购申请日期", index = 17)
@ExcelProperty(value = "采购申请日期", index = 12)
@ColumnWidth(18)
@Required
private Date applyDate;
/** 采购计划批准日期 */
@ExcelProperty(value = "采购计划批准日期", index = 18)
@ExcelProperty(value = "采购计划批准日期", index = 13)
@ColumnWidth(18)
private Date planApproveDate;
/** 采购公告发布日期 */
@ExcelProperty(value = "采购公告发布日期", index = 19)
@ExcelProperty(value = "采购公告发布日期", index = 14)
@ColumnWidth(18)
private Date announceDate;
/** 开标日期 */
@ExcelProperty(value = "开标日期", index = 20)
@ExcelProperty(value = "开标日期", index = 15)
@ColumnWidth(18)
private Date bidOpenDate;
/** 合同签订日期 */
@ExcelProperty(value = "合同签订日期", index = 21)
@ExcelProperty(value = "合同签订日期", index = 16)
@ColumnWidth(18)
private Date contractSignDate;
/** 预计交货日期 */
@ExcelProperty(value = "预计交货日期", index = 22)
@ExcelProperty(value = "预计交货日期", index = 17)
@ColumnWidth(18)
private Date expectedDeliveryDate;
/** 实际交货日期 */
@ExcelProperty(value = "实际交货日期", index = 23)
@ExcelProperty(value = "实际交货日期", index = 18)
@ColumnWidth(18)
private Date actualDeliveryDate;
/** 验收日期 */
@ExcelProperty(value = "验收日期", index = 24)
@ExcelProperty(value = "验收日期", index = 19)
@ColumnWidth(18)
private Date acceptanceDate;
/** 结算日期 */
@ExcelProperty(value = "结算日期", index = 25)
@ExcelProperty(value = "结算日期", index = 20)
@ColumnWidth(18)
private Date settlementDate;
/** 申请人工号 */
@ExcelProperty(value = "申请人工号", index = 26)
@ExcelProperty(value = "申请人工号", index = 21)
@ColumnWidth(15)
@Required
private String applicantId;
/** 申请人姓名 */
@ExcelProperty(value = "申请人姓名", index = 27)
@ExcelProperty(value = "申请人姓名", index = 22)
@ColumnWidth(15)
@Required
private String applicantName;
/** 申请部门 */
@ExcelProperty(value = "申请部门", index = 28)
@ExcelProperty(value = "申请部门", index = 23)
@ColumnWidth(18)
@Required
private String applyDepartment;
/** 采购负责人工号 */
@ExcelProperty(value = "采购负责人工号", index = 29)
@ExcelProperty(value = "采购负责人工号", index = 24)
@ColumnWidth(15)
private String purchaseLeaderId;
/** 采购负责人姓名 */
@ExcelProperty(value = "采购负责人姓名", index = 30)
@ExcelProperty(value = "采购负责人姓名", index = 25)
@ColumnWidth(15)
private String purchaseLeaderName;
/** 采购部门 */
@ExcelProperty(value = "采购部门", index = 31)
@ExcelProperty(value = "采购部门", index = 26)
@ColumnWidth(18)
private String purchaseDepartment;
}

View File

@@ -0,0 +1,55 @@
package com.ruoyi.info.collection.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 招投标供应商明细Excel对象
*/
@Data
public class CcdiPurchaseTransactionSupplierExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "采购事项ID", index = 0)
@ColumnWidth(20)
@Required
private String purchaseId;
@ExcelProperty(value = "供应商名称", index = 1)
@ColumnWidth(25)
@Required
private String supplierName;
@ExcelProperty(value = "供应商统一信用代码", index = 2)
@ColumnWidth(25)
private String supplierUscc;
@ExcelProperty(value = "供应商联系人", index = 3)
@ColumnWidth(15)
private String contactPerson;
@ExcelProperty(value = "供应商联系电话", index = 4)
@ColumnWidth(18)
private String contactPhone;
@ExcelProperty(value = "供应商银行账户", index = 5)
@ColumnWidth(20)
private String supplierBankAccount;
@ExcelProperty(value = "是否中标", index = 6)
@ColumnWidth(12)
@DictDropdown(dictType = "ccdi_yes_no_flag")
private String isBidWinner;
@ExcelProperty(value = "排序", index = 7)
@ColumnWidth(10)
private Integer sortOrder;
}

View File

@@ -10,23 +10,23 @@ import java.io.Serial;
import java.io.Serializable;
/**
* 员工实体关系信息Excel导入导出对象
* 员工亲属实体关Excel导入导出对象
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工实体关系信息Excel导入导出对象")
@Schema(description = "员工亲属实体关Excel导入导出对象")
public class CcdiStaffEnterpriseRelationExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 身份证号 */
@ExcelProperty(value = "身份证号", index = 0)
/** 亲属身份证号 */
@ExcelProperty(value = "亲属身份证号", index = 0)
@ColumnWidth(20)
@Required
@Schema(description = "身份证号")
@Schema(description = "亲属身份证号")
private String personId;
/** 统一社会信用代码 */

View File

@@ -15,6 +15,14 @@ import java.math.BigDecimal;
@Schema(description = "亲属资产信息导入失败记录")
public class AssetImportFailureVO {
/** Sheet名称 */
@Schema(description = "Sheet名称")
private String sheetName;
/** Excel行号 */
@Schema(description = "Excel行号")
private Integer rowNum;
/** 亲属证件号 */
@Schema(description = "亲属证件号")
private String personId;

View File

@@ -15,6 +15,14 @@ import java.math.BigDecimal;
@Schema(description = "员工资产信息导入失败记录")
public class BaseStaffAssetImportFailureVO {
/** Sheet名称 */
@Schema(description = "Sheet名称")
private String sheetName;
/** Excel行号 */
@Schema(description = "Excel行号")
private Integer rowNum;
/** 员工身份证号 */
@Schema(description = "员工身份证号")
private String personId;

View File

@@ -0,0 +1,23 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 员工双Sheet导入提交结果
*
* @author ruoyi
*/
@Data
@Schema(description = "员工双Sheet导入提交结果")
public class BaseStaffImportSubmitResultVO {
@Schema(description = "员工信息导入任务ID")
private String staffTaskId;
@Schema(description = "员工资产信息导入任务ID")
private String assetTaskId;
@Schema(description = "提交说明")
private String message;
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 招投标供应商明细VO
*/
@Data
@Schema(description = "招投标供应商明细")
public class CcdiPurchaseTransactionSupplierVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
private Long id;
@Schema(description = "采购事项ID")
private String purchaseId;
@Schema(description = "供应商名称")
private String supplierName;
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
@Schema(description = "供应商联系人")
private String contactPerson;
@Schema(description = "供应商联系电话")
private String contactPhone;
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
@Schema(description = "是否中标1-是0-否")
private Integer isBidWinner;
@Schema(description = "排序")
private Integer sortOrder;
}

View File

@@ -8,15 +8,16 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 采购交易信息VO
* 招投标信息VO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息")
@Schema(description = "招投标信息")
public class CcdiPurchaseTransactionVO implements Serializable {
@Serial
@@ -90,6 +91,14 @@ public class CcdiPurchaseTransactionVO implements Serializable {
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
/** 参与供应商数 */
@Schema(description = "参与供应商数")
private Integer supplierCount;
/** 供应商明细 */
@Schema(description = "供应商明细列表")
private List<CcdiPurchaseTransactionSupplierVO> supplierList;
/** 采购申请日期(或立项日期) */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "采购申请日期")

View File

@@ -0,0 +1,37 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 员工亲属实体关联下拉选项VO
*
* @author ruoyi
* @date 2026-04-23
*/
@Data
@Schema(description = "员工亲属实体关联下拉选项")
public class CcdiStaffEnterpriseRelationOptionVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 亲属身份证号 */
@Schema(description = "亲属身份证号")
private String relationCertNo;
/** 亲属姓名 */
@Schema(description = "亲属姓名")
private String relationName;
/** 关联员工身份证号 */
@Schema(description = "关联员工身份证号")
private String staffPersonId;
/** 关联员工姓名 */
@Schema(description = "关联员工姓名")
private String staffPersonName;
}

View File

@@ -9,13 +9,13 @@ import java.io.Serializable;
import java.util.Date;
/**
* 员工实体关系信息VO
* 员工亲属实体关VO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工实体关系信息")
@Schema(description = "员工亲属实体关")
public class CcdiStaffEnterpriseRelationVO implements Serializable {
@Serial
@@ -25,13 +25,21 @@ public class CcdiStaffEnterpriseRelationVO implements Serializable {
@Schema(description = "主键ID")
private Long id;
/** 身份证号 */
@Schema(description = "身份证号")
/** 亲属身份证号 */
@Schema(description = "亲属身份证号")
private String personId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String personName;
/** 亲属姓名 */
@Schema(description = "亲属姓名")
private String relationName;
/** 关联员工身份证号 */
@Schema(description = "关联员工身份证号")
private String staffPersonId;
/** 关联员工姓名 */
@Schema(description = "关联员工姓名")
private String staffPersonName;
/** 关联人在企业的职务 */
@Schema(description = "关联人在企业的职务")

View File

@@ -14,8 +14,14 @@ import java.math.BigDecimal;
@Schema(description = "导入失败记录")
public class ImportFailureVO {
@Schema(description = "Sheet名称")
private String sheetName;
@Schema(description = "Excel行号")
private Integer rowNum;
@Schema(description = "柜员号")
private Long employeeId;
private Long staffId;
@Schema(description = "姓名")
private String name;

View File

@@ -6,15 +6,23 @@ import lombok.Data;
import java.math.BigDecimal;
/**
* 采购交易信息导入失败记录VO
* 招投标信息导入失败记录VO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息导入失败记录")
@Schema(description = "招投标信息导入失败记录")
public class PurchaseTransactionImportFailureVO {
/** 失败来源Sheet */
@Schema(description = "失败来源Sheet")
private String sheetName;
/** 失败行号 */
@Schema(description = "失败行号")
private String sheetRowNum;
/** 采购事项ID */
@Schema(description = "采购事项ID")
private String purchaseId;

View File

@@ -13,6 +13,12 @@ import lombok.Data;
@Schema(description = "招聘信息导入失败记录")
public class RecruitmentImportFailureVO {
@Schema(description = "失败Sheet")
private String sheetName;
@Schema(description = "失败行号")
private String sheetRowNum;
@Schema(description = "招聘项目编号")
private String recruitId;

View File

@@ -7,22 +7,26 @@ import java.io.Serial;
import java.io.Serializable;
/**
* 员工实体关系信息导入失败记录VO
* 员工亲属实体关导入失败记录VO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工实体关系信息导入失败记录")
@Schema(description = "员工亲属实体关导入失败记录")
public class StaffEnterpriseRelationImportFailureVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 身份证号 */
@Schema(description = "身份证号")
/** 亲属身份证号 */
@Schema(description = "亲属身份证号")
private String personId;
/** 亲属姓名 */
@Schema(description = "亲属姓名")
private String relationName;
/** 统一社会信用代码 */
@Schema(description = "统一社会信用代码")
private String socialCreditCode;

View File

@@ -15,6 +15,14 @@ import java.math.BigDecimal;
@Schema(description = "员工亲属关系信息导入失败记录")
public class StaffFmyRelationImportFailureVO {
/** Sheet名称 */
@Schema(description = "Sheet名称")
private String sheetName;
/** Excel行号 */
@Schema(description = "Excel行号")
private Integer rowNum;
/** 员工身份证号 */
@Schema(description = "员工身份证号")
private String personId;

View File

@@ -0,0 +1,24 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 员工亲属关系双Sheet导入提交结果
*
* @author ruoyi
* @date 2026-04-22
*/
@Data
@Schema(description = "员工亲属关系双Sheet导入提交结果")
public class StaffFmyRelationImportSubmitResultVO {
@Schema(description = "员工亲属关系导入任务ID")
private String relationTaskId;
@Schema(description = "亲属资产信息导入任务ID")
private String assetTaskId;
@Schema(description = "提交结果提示")
private String message;
}

View File

@@ -35,6 +35,14 @@ public interface CcdiPurchaseTransactionMapper extends BaseMapper<CcdiPurchaseTr
*/
CcdiPurchaseTransactionVO selectTransactionById(@Param("purchaseId") String purchaseId);
/**
* 删除指定采购事项ID对应的供应商明细
*
* @param purchaseIds 采购事项ID列表
* @return 删除行数
*/
int deleteSuppliersByPurchaseIds(@Param("purchaseIds") List<String> purchaseIds);
/**
* 批量插入采购交易数据
*

View File

@@ -0,0 +1,12 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier;
import org.apache.ibatis.annotations.Mapper;
/**
* 招投标供应商明细Mapper
*/
@Mapper
public interface CcdiPurchaseTransactionSupplierMapper extends BaseMapper<CcdiPurchaseTransactionSupplier> {
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationQueryDTO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -38,6 +39,14 @@ public interface CcdiStaffEnterpriseRelationMapper extends BaseMapper<CcdiStaffE
*/
CcdiStaffEnterpriseRelationVO selectRelationById(@Param("id") Long id);
/**
* 查询有效员工亲属下拉选项
*
* @param query 搜索关键词
* @return 下拉选项
*/
List<CcdiStaffEnterpriseRelationOptionVO> selectFamilyOptions(@Param("query") String query);
/**
* 判断身份证号和统一社会信用代码的组合是否已存在
*
@@ -57,6 +66,14 @@ public interface CcdiStaffEnterpriseRelationMapper extends BaseMapper<CcdiStaffE
*/
Set<String> batchExistsByCombinations(@Param("combinations") List<String> combinations);
/**
* 根据亲属身份证号批量置无效
*
* @param personId 亲属身份证号
* @return 影响行数
*/
int invalidateByFamilyCertNo(@Param("personId") String personId);
/**
* 批量插入员工实体关系数据
*

View File

@@ -15,10 +15,9 @@ public interface ICcdiBaseStaffImportService {
/**
* 异步导入员工数据
*
* @param excelList Excel数据列表
* @param isUpdateSupport 是否更新已存在的数据
* @param excelList Excel数据列表
*/
void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport, String taskId);
void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, String taskId);
/**
* 查询导入状态

View File

@@ -78,11 +78,10 @@ public interface ICcdiBaseStaffService {
/**
* 导入员工数据
*
* @param excelList Excel实体列表
* @param isUpdateSupport 是否更新支持
* @param excelList Excel实体列表
* @return 结果
*/
String importBaseStaff(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport);
String importBaseStaff(List<CcdiBaseStaffExcel> excelList);
/**
* 查询员工下拉列表

View File

@@ -1,6 +1,7 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.PurchaseTransactionImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
@@ -17,11 +18,17 @@ public interface ICcdiPurchaseTransactionImportService {
/**
* 异步导入采购交易数据
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @param userName 当前用户名
* @param mainExcelList 主信息Excel数据列表
* @param supplierExcelList 供应商明细Excel数据列表
* @param taskId 任务ID
* @param userName 当前用户名
*/
void importTransactionAsync(List<CcdiPurchaseTransactionExcel> excelList, String taskId, String userName);
void importTransactionAsync(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList,
String taskId,
String userName
);
/**
* 查询导入状态

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO;
import java.util.List;
@@ -77,8 +78,12 @@ public interface ICcdiPurchaseTransactionService {
/**
* 导入采购交易数据(异步)
*
* @param excelList Excel实体列表
* @param mainExcelList 主信息Excel实体列表
* @param supplierExcelList 供应商明细Excel实体列表
* @return 任务ID
*/
String importTransaction(List<CcdiPurchaseTransactionExcel> excelList);
String importTransaction(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
);
}

View File

@@ -7,7 +7,7 @@ import com.ruoyi.info.collection.domain.vo.StaffEnterpriseRelationImportFailureV
import java.util.List;
/**
* 员工实体关系信息异步导入服务层
* 员工亲属实体关异步导入服务层
*
* @author ruoyi
* @date 2026-02-09

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationVO;
import java.util.List;
@@ -42,6 +43,14 @@ public interface ICcdiStaffEnterpriseRelationService {
*/
List<CcdiStaffEnterpriseRelationExcel> selectRelationListForExport(CcdiStaffEnterpriseRelationQueryDTO queryDTO);
/**
* 查询有效员工亲属下拉选项
*
* @param query 搜索关键词
* @return 下拉选项
*/
List<CcdiStaffEnterpriseRelationOptionVO> selectFamilyOptions(String query);
/**
* 查询员工实体关系详情
*

View File

@@ -22,21 +22,11 @@ public interface ICcdiStaffRecruitmentImportService {
* @param taskId 任务ID
* @param userName 用户名
*/
void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList,
String taskId,
String userName);
/**
* 异步导入招聘记录历史工作经历数据
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @param userName 用户名
*/
void importRecruitmentWorkAsync(List<CcdiStaffRecruitmentWorkExcel> excelList,
String taskId,
String userName);
/**
* 查询导入状态
*

View File

@@ -81,13 +81,6 @@ public interface ICcdiStaffRecruitmentService {
* @param excelList Excel实体列表
* @return 结果
*/
String importRecruitment(List<CcdiStaffRecruitmentExcel> excelList);
/**
* 导入招聘记录历史工作经历数据(异步)
*
* @param excelList Excel实体列表
* @return 任务ID
*/
String importRecruitmentWork(List<CcdiStaffRecruitmentWorkExcel> excelList);
String importRecruitment(List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList);
}

View File

@@ -39,6 +39,8 @@ import java.util.concurrent.TimeUnit;
public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportService {
private static final String STATUS_KEY_PREFIX = "import:assetInfo:";
private static final String SHEET_NAME = "亲属资产信息";
private static final int EXCEL_DATA_START_ROW = 2;
@Resource
private CcdiAssetInfoMapper assetInfoMapper;
@@ -91,7 +93,8 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
for (CcdiAssetInfoExcel excel : excelList) {
for (int i = 0; i < excelList.size(); i++) {
CcdiAssetInfoExcel excel = excelList.get(i);
try {
validateExcel(excel);
Set<String> familyIds = ownerMap.get(excel.getPersonId());
@@ -111,6 +114,8 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
} catch (Exception e) {
AssetImportFailureVO failureVO = new AssetImportFailureVO();
BeanUtils.copyProperties(excel, failureVO);
failureVO.setSheetName(SHEET_NAME);
failureVO.setRowNum(i + EXCEL_DATA_START_ROW);
failureVO.setErrorMessage(e.getMessage());
failures.add(failureVO);
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
@@ -90,14 +91,24 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
.toList();
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
Set<String> existingAssetKeys = buildExistingAssetKeys(personIds);
Set<String> importedAssetKeys = new java.util.LinkedHashSet<>();
for (CcdiBaseStaffAssetInfoExcel excel : excelList) {
for (int i = 0; i < excelList.size(); i++) {
CcdiBaseStaffAssetInfoExcel excel = excelList.get(i);
try {
validateExcel(excel);
Set<String> familyIds = ownerMap.get(excel.getPersonId());
if (familyIds == null || familyIds.isEmpty()) {
throw new RuntimeException("员工资产导入仅支持员工本人证件号");
}
String assetKey = buildAssetKey(excel.getPersonId(), excel.getAssetMainType(), excel.getAssetSubType(), excel.getAssetName());
if (existingAssetKeys.contains(assetKey)) {
throw new RuntimeException("资产记录已存在");
}
if (!importedAssetKeys.add(assetKey)) {
throw new RuntimeException("资产记录在导入文件中重复");
}
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
BeanUtils.copyProperties(excel, assetInfo);
@@ -109,6 +120,8 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
} catch (Exception e) {
BaseStaffAssetImportFailureVO failureVO = new BaseStaffAssetImportFailureVO();
BeanUtils.copyProperties(excel, failureVO);
failureVO.setSheetName("员工资产信息");
failureVO.setRowNum(i + 2);
failureVO.setErrorMessage(e.getMessage());
failures.add(failureVO);
}
@@ -168,6 +181,18 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
return result;
}
private Set<String> buildExistingAssetKeys(List<String> personIds) {
if (personIds == null || personIds.isEmpty()) {
return Set.of();
}
LambdaQueryWrapper<CcdiAssetInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiAssetInfo::getPersonId, personIds);
return assetInfoMapper.selectList(wrapper).stream()
.filter(asset -> StringUtils.equals(asset.getFamilyId(), asset.getPersonId()))
.map(asset -> buildAssetKey(asset.getPersonId(), asset.getAssetMainType(), asset.getAssetSubType(), asset.getAssetName()))
.collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
}
private void mergeOwnerMappings(Map<String, Set<String>> result, List<Map<String, String>> mappings) {
if (mappings == null) {
return;
@@ -203,6 +228,14 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
}
}
private String buildAssetKey(String personId, String assetMainType, String assetSubType, String assetName) {
return String.join("|",
StringUtils.nvl(personId, ""),
StringUtils.nvl(assetMainType, ""),
StringUtils.nvl(assetSubType, ""),
StringUtils.nvl(assetName, ""));
}
private void updateImportStatus(String taskId, String status, ImportResult result) {
Map<String, Object> statusData = new HashMap<>();
statusData.put("status", status);

View File

@@ -2,6 +2,7 @@ package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
@@ -13,6 +14,7 @@ import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.IdCardUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,16 +45,18 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private SysDeptMapper deptMapper;
@Override
@Async
public void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport, String taskId) {
public void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, String taskId) {
long startTime = System.currentTimeMillis();
// 记录导入开始
ImportLogUtils.logImportStart(log, taskId, "员工基础信息", excelList.size(), "系统");
List<CcdiBaseStaff> newRecords = new ArrayList<>();
List<CcdiBaseStaff> updateRecords = new ArrayList<>();
List<ImportFailureVO> failures = new ArrayList<>();
// 批量查询已存在的员工ID和身份证号
@@ -75,13 +79,12 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
CcdiBaseStaffAddDTO addDTO = new CcdiBaseStaffAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 验证数据(支持更新模式)
validateStaffData(addDTO, isUpdateSupport, existingIds, existingIdCards);
validateStaffData(addDTO, existingIds, existingIdCards);
CcdiBaseStaff staff = new CcdiBaseStaff();
BeanUtils.copyProperties(excel, staff);
// 统一检查Excel内重复(更新和新增两个分支都需要检查)
// 统一检查Excel内重复
if (processedStaffIds.contains(excel.getStaffId())) {
throw new RuntimeException(String.format("员工ID[%d]在导入文件中重复,已跳过此条记录", excel.getStaffId()));
}
@@ -90,20 +93,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
throw new RuntimeException(String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard()));
}
// 检查员工ID是否在数据库中已存在
if (existingIds.contains(excel.getStaffId())) {
// 员工ID已存在于数据库
if (!isUpdateSupport) {
throw new RuntimeException("员工ID已存在且未启用更新支持");
}
// 通过检查,添加到更新列表
updateRecords.add(staff);
} else {
// 员工ID不存在,添加到新增列表
newRecords.add(staff);
}
newRecords.add(staff);
// 统一标记为已处理(只有成功添加到列表后才会执行到这里)
if (excel.getStaffId() != null) {
@@ -115,11 +105,13 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
// 记录进度
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
newRecords.size() + updateRecords.size(), failures.size());
newRecords.size(), failures.size());
} catch (Exception e) {
ImportFailureVO failure = new ImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName("员工信息");
failure.setRowNum(i + 2);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
@@ -137,13 +129,6 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
saveBatch(newRecords, 500);
}
// 批量更新已有数据(先删除再插入)
if (!updateRecords.isEmpty() && isUpdateSupport) {
ImportLogUtils.logBatchOperationStart(log, taskId, "更新",
(updateRecords.size() + 499) / 500, 500);
baseStaffMapper.insertOrUpdateBatch(updateRecords);
}
// 保存失败记录到Redis
if (!failures.isEmpty()) {
try {
@@ -157,7 +142,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size() + updateRecords.size());
result.setSuccessCount(newRecords.size());
result.setFailureCount(failures.size());
// 更新最终状态
@@ -299,11 +284,10 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
* 验证员工数据
*
* @param addDTO 新增DTO
* @param isUpdateSupport 是否支持更新
* @param existingIds 已存在的员工ID集合(导入场景使用,传null表示单条新增)
* @param existingIdCards 已存在的身份证号集合(导入场景使用,传null表示单条新增)
*/
public void validateStaffData(CcdiBaseStaffAddDTO addDTO, Boolean isUpdateSupport, Set<Long> existingIds, Set<String> existingIdCards) {
public void validateStaffData(CcdiBaseStaffAddDTO addDTO, Set<Long> existingIds, Set<String> existingIdCards) {
// 验证必填字段
if (StringUtils.isEmpty(addDTO.getName())) {
throw new RuntimeException("姓名不能为空");
@@ -326,6 +310,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
if (StringUtils.isEmpty(addDTO.getStatus())) {
throw new RuntimeException("状态不能为空");
}
validateDeptId(addDTO.getDeptId());
// 验证身份证号格式
String idCardError = IdCardUtil.getErrorMessage(addDTO.getIdCard());
@@ -347,12 +332,11 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
throw new RuntimeException("该身份证号已存在");
}
} else {
// 导入场景:如果员工ID不存在,才检查身份证号唯一性
if (!existingIds.contains(addDTO.getStaffId())) {
// 使用批量查询的结果检查身份证号唯一性
if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) {
throw new RuntimeException("该身份证号已存在");
}
if (existingIds.contains(addDTO.getStaffId())) {
throw new RuntimeException("该员工ID已存在");
}
if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) {
throw new RuntimeException("该身份证号已存在");
}
}
@@ -378,4 +362,11 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
throw new RuntimeException(fieldLabel + "最多保留2位小数");
}
}
private void validateDeptId(Long deptId) {
SysDept dept = deptMapper.selectDeptById(deptId);
if (dept == null || !"0".equals(dept.getStatus()) || !"0".equals(dept.getDelFlag())) {
throw new RuntimeException(String.format("所属部门ID[%d]不存在或已停用/删除,请检查机构号", deptId));
}
}
}

View File

@@ -211,13 +211,12 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
/**
* 导入员工数据
*
* @param excelList Excel实体列表
* @param isUpdateSupport 是否更新支持
* @param excelList Excel实体列表
* @return 结果
*/
@Override
@Transactional
public String importBaseStaff(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport) {
public String importBaseStaff(List<CcdiBaseStaffExcel> excelList) {
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
@@ -236,7 +235,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, java.util.concurrent.TimeUnit.DAYS);
importAsyncService.importBaseStaffAsync(excelList, isUpdateSupport, taskId);
importAsyncService.importBaseStaffAsync(excelList, taskId);
return taskId;
}

View File

@@ -131,10 +131,6 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
if (!excel.getSocialCreditCode().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) {
throw new RuntimeException("统一社会信用代码格式不正确");
}
if (StringUtils.isEmpty(excel.getStatus())) {
throw new RuntimeException("经营状态不能为空");
}
String riskLevel = EnterpriseRiskLevel.resolveCode(StringUtils.trim(excel.getRiskLevel()));
if (riskLevel == null) {
throw new RuntimeException("风险等级不在允许范围内");
@@ -143,10 +139,6 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
if (entSource == null) {
throw new RuntimeException("企业来源不在允许范围内");
}
String dataSource = resolveDataSourceCode(StringUtils.trim(excel.getDataSource()));
if (dataSource == null) {
throw new RuntimeException("数据来源不在允许范围内");
}
if (existingCreditCodes.contains(excel.getSocialCreditCode())) {
throw new RuntimeException(String.format("统一社会信用代码[%s]已存在,请勿重复导入", excel.getSocialCreditCode()));
@@ -159,8 +151,8 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
BeanUtils.copyProperties(excel, entity);
entity.setRiskLevel(riskLevel);
entity.setEntSource(entSource);
entity.setDataSource(dataSource);
entity.setStatus(StringUtils.trim(excel.getStatus()));
entity.setDataSource(DataSource.IMPORT.getCode());
entity.setStatus(trimToNull(excel.getStatus()));
entity.setCreatedBy(userName);
entity.setUpdatedBy(userName);
return entity;
@@ -206,15 +198,6 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
redisTemplate.opsForHash().putAll(buildStatusKey(taskId), statusData);
}
private String resolveDataSourceCode(String value) {
for (DataSource source : DataSource.values()) {
if (source.getCode().equals(value) || source.getDesc().equals(value)) {
return source.getCode();
}
}
return null;
}
private String buildStatusKey(String taskId) {
return "import:enterpriseBaseInfo:" + taskId;
}
@@ -222,4 +205,11 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
private String buildFailuresKey(String taskId) {
return "import:enterpriseBaseInfo:" + taskId + ":failures";
}
private String trimToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -85,10 +85,12 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
if (enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode()) != null) {
throw new RuntimeException("该统一社会信用代码已存在");
}
validateEnumFields(addDTO.getStatus(), addDTO.getRiskLevel(), addDTO.getEntSource(), addDTO.getDataSource());
validateRiskLevelAndEnterpriseSource(addDTO.getRiskLevel(), addDTO.getEntSource());
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
BeanUtils.copyProperties(addDTO, entity);
entity.setStatus(trimToNull(addDTO.getStatus()));
entity.setDataSource(DataSource.MANUAL.getCode());
return enterpriseBaseInfoMapper.insert(entity);
}
@@ -103,6 +105,8 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
BeanUtils.copyProperties(editDTO, entity);
entity.setStatus(trimToNull(editDTO.getStatus()));
entity.setDataSource(existing.getDataSource());
return enterpriseBaseInfoMapper.updateById(entity);
}
@@ -176,18 +180,22 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
}
private void validateEnumFields(String status, String riskLevel, String entSource, String dataSource) {
if (StringUtils.isEmpty(status)) {
throw new RuntimeException("经营状态不能为空");
validateRiskLevelAndEnterpriseSource(riskLevel, entSource);
if (StringUtils.isNotEmpty(status) && StringUtils.trim(status).length() > 50) {
throw new RuntimeException("经营状态长度不能超过50个字符");
}
if (!containsDataSource(dataSource)) {
throw new RuntimeException("数据来源不在允许范围内");
}
}
private void validateRiskLevelAndEnterpriseSource(String riskLevel, String entSource) {
if (!EnterpriseRiskLevel.contains(riskLevel)) {
throw new RuntimeException("风险等级不在允许范围内");
}
if (!EnterpriseSource.contains(entSource)) {
throw new RuntimeException("企业来源不在允许范围内");
}
if (!containsDataSource(dataSource)) {
throw new RuntimeException("数据来源不在允许范围内");
}
}
private boolean containsDataSource(String code) {
@@ -199,6 +207,13 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
return false;
}
private String trimToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
return value.trim();
}
private void validateDeleteRelations(String socialCreditCode) {
StringJoiner relationTypes = new StringJoiner("");
if (staffEnterpriseRelationMapper.selectCount(new LambdaQueryWrapper<CcdiStaffEnterpriseRelation>()

View File

@@ -2,12 +2,15 @@ package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransaction;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.PurchaseTransactionImportFailureVO;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionMapper;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionSupplierMapper;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionImportService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.StringUtils;
@@ -37,83 +40,155 @@ import java.util.stream.Collectors;
public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTransactionImportService {
private static final Logger log = LoggerFactory.getLogger(CcdiPurchaseTransactionImportServiceImpl.class);
private static final String MAIN_SHEET_NAME = "招投标主信息";
private static final String SUPPLIER_SHEET_NAME = "供应商明细";
private static final int EXCEL_DATA_START_ROW = 2;
@Resource
private CcdiPurchaseTransactionMapper transactionMapper;
@Resource
private CcdiPurchaseTransactionSupplierMapper supplierMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async
@Transactional
public void importTransactionAsync(List<CcdiPurchaseTransactionExcel> excelList, String taskId, String userName) {
public void importTransactionAsync(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList,
String taskId,
String userName
) {
long startTime = System.currentTimeMillis();
List<CcdiPurchaseTransactionExcel> safeMainList = mainExcelList == null ? List.of() : mainExcelList;
List<CcdiPurchaseTransactionSupplierExcel> safeSupplierList = supplierExcelList == null ? List.of() : supplierExcelList;
List<MainImportRow> indexedMainRows = buildMainImportRows(safeMainList);
List<SupplierImportRow> indexedSupplierRows = buildSupplierImportRows(safeSupplierList);
int totalCount = countImportUnits(safeMainList, safeSupplierList);
// 记录导入开始
ImportLogUtils.logImportStart(log, taskId, "采购交易信息", excelList.size(), userName);
ImportLogUtils.logImportStart(log, taskId, "招投标信息维护", totalCount, userName);
List<CcdiPurchaseTransaction> newRecords = new ArrayList<>();
List<CcdiPurchaseTransaction> newTransactions = new ArrayList<>();
List<CcdiPurchaseTransactionSupplier> newSuppliers = new ArrayList<>();
List<PurchaseTransactionImportFailureVO> failures = new ArrayList<>();
// 批量查询已存在的采购事项ID
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的采购事项ID", excelList.size());
Set<String> existingIds = getExistingPurchaseIds(excelList);
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的采购事项ID", safeMainList.size());
Set<String> existingIds = getExistingPurchaseIds(safeMainList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "采购事项ID", existingIds.size());
// 用于跟踪Excel文件内已处理的采购事项ID
Set<String> processedIds = new HashSet<>();
Map<String, List<MainImportRow>> mainGroupMap = indexedMainRows.stream()
.filter(item -> StringUtils.isNotEmpty(item.data().getPurchaseId()))
.collect(Collectors.groupingBy(
item -> item.data().getPurchaseId(),
LinkedHashMap::new,
Collectors.toList()
));
Map<String, List<SupplierImportRow>> supplierGroupMap = indexedSupplierRows.stream()
.filter(item -> StringUtils.isNotEmpty(item.data().getPurchaseId()))
.collect(Collectors.groupingBy(
item -> item.data().getPurchaseId(),
LinkedHashMap::new,
Collectors.toList()
));
LinkedHashSet<String> purchaseIds = new LinkedHashSet<>();
purchaseIds.addAll(mainGroupMap.keySet());
purchaseIds.addAll(supplierGroupMap.keySet());
// 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiPurchaseTransactionExcel excel = excelList.get(i);
for (SupplierImportRow supplierExcel : indexedSupplierRows) {
if (StringUtils.isEmpty(supplierExcel.data().getPurchaseId())) {
failures.add(buildFailure(
null,
null,
SUPPLIER_SHEET_NAME,
String.valueOf(supplierExcel.sheetRowNum()),
"供应商明细Sheet中的采购事项ID不能为空"
));
}
}
int index = 0;
for (String purchaseId : purchaseIds) {
index++;
List<MainImportRow> mainRows = mainGroupMap.getOrDefault(purchaseId, List.of());
List<SupplierImportRow> supplierRows = supplierGroupMap.getOrDefault(purchaseId, List.of());
try {
// 转换为AddDTO进行验证
CcdiPurchaseTransactionAddDTO addDTO = new CcdiPurchaseTransactionAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 验证数据
validateTransactionData(addDTO, existingIds);
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(excel, transaction);
if (existingIds.contains(excel.getPurchaseId())) {
// 采购事项ID已存在直接报错
throw new RuntimeException(String.format("采购事项ID[%s]已存在,请勿重复导入", excel.getPurchaseId()));
} else if (processedIds.contains(excel.getPurchaseId())) {
// Excel文件内部重复
throw new RuntimeException(String.format("采购事项ID[%s]在导入文件中重复,已跳过此条记录", excel.getPurchaseId()));
} else {
transaction.setCreatedBy(userName);
transaction.setUpdatedBy(userName);
newRecords.add(transaction);
processedIds.add(excel.getPurchaseId()); // 标记为已处理
if (existingIds.contains(purchaseId)) {
throw buildValidationException(
MAIN_SHEET_NAME,
extractMainRowNums(mainRows),
String.format("采购事项ID[%s]已存在,请勿重复导入", purchaseId)
);
}
if (mainRows.isEmpty()) {
throw buildValidationException(
SUPPLIER_SHEET_NAME,
extractSupplierRowNums(supplierRows),
String.format("采购事项ID[%s]缺少招投标主信息", purchaseId)
);
}
if (mainRows.size() > 1) {
throw buildValidationException(
MAIN_SHEET_NAME,
extractMainRowNums(mainRows),
String.format("采购事项ID[%s]在招投标主信息Sheet中重复", purchaseId)
);
}
MainImportRow mainRow = mainRows.getFirst();
CcdiPurchaseTransactionExcel mainExcel = mainRow.data();
CcdiPurchaseTransactionAddDTO addDTO = new CcdiPurchaseTransactionAddDTO();
BeanUtils.copyProperties(mainExcel, addDTO);
validateTransactionData(addDTO, mainRow.sheetRowNum());
List<CcdiPurchaseTransactionSupplier> suppliers = buildSupplierEntities(purchaseId, supplierRows, userName);
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(mainExcel, transaction);
fillWinnerSummary(transaction, suppliers);
transaction.setCreatedBy(userName);
transaction.setUpdatedBy(userName);
newTransactions.add(transaction);
newSuppliers.addAll(suppliers);
// 记录进度
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
newRecords.size(), failures.size());
ImportLogUtils.logProgress(log, taskId, index, Math.max(totalCount, purchaseIds.size()),
newTransactions.size(), failures.size());
} catch (Exception e) {
PurchaseTransactionImportFailureVO failure = new PurchaseTransactionImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
MainImportRow mainRow = mainRows.isEmpty() ? null : mainRows.getFirst();
CcdiPurchaseTransactionExcel mainExcel = mainRow == null ? null : mainRow.data();
FailureMeta failureMeta = resolveFailureMeta(e, mainRows, supplierRows);
failures.add(buildFailure(
mainExcel,
purchaseId,
failureMeta.sheetName(),
failureMeta.sheetRowNum(),
e.getMessage()
));
// 记录验证失败日志
String keyData = String.format("采购事项ID=%s, 采购类别=%s, 标的物=%s",
excel.getPurchaseId(), excel.getPurchaseCategory(), excel.getSubjectName());
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
purchaseId,
mainExcel == null ? "" : mainExcel.getPurchaseCategory(),
mainExcel == null ? "" : mainExcel.getSubjectName());
ImportLogUtils.logValidationError(log, taskId, index, e.getMessage(), keyData);
}
}
// 批量插入新数据
if (!newRecords.isEmpty()) {
if (!newTransactions.isEmpty()) {
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
(newRecords.size() + 499) / 500, 500);
saveBatch(newRecords, 500);
(newTransactions.size() + 499) / 500, 500);
saveBatch(newTransactions, 500);
}
if (!newSuppliers.isEmpty()) {
saveSupplierBatch(newSuppliers, 500);
}
// 保存失败记录到Redis
@@ -128,8 +203,8 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size());
result.setTotalCount(totalCount);
result.setSuccessCount(newTransactions.size());
result.setFailureCount(failures.size());
// 更新最终状态
@@ -138,8 +213,8 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
// 记录导入完成
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "采购交易信息",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
ImportLogUtils.logImportComplete(log, taskId, "招投标信息维护",
totalCount, result.getSuccessCount(), result.getFailureCount(), duration);
}
/**
@@ -243,71 +318,338 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
}
}
private void saveSupplierBatch(List<CcdiPurchaseTransactionSupplier> list, int batchSize) {
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiPurchaseTransactionSupplier> subList = list.subList(i, end);
for (CcdiPurchaseTransactionSupplier supplier : subList) {
supplierMapper.insert(supplier);
}
}
}
/**
* 验证采购交易数据
*
* @param addDTO 新增DTO
* @param existingIds 已存在的采购事项ID集合
* @param addDTO 新增DTO
*/
private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO, Set<String> existingIds) {
private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO, int sheetRowNum) {
// 验证必填字段
if (StringUtils.isEmpty(addDTO.getPurchaseId())) {
throw new RuntimeException("采购事项ID不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购事项ID不能为空");
}
if (StringUtils.isEmpty(addDTO.getPurchaseCategory())) {
throw new RuntimeException("采购类别不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购类别不能为空");
}
if (StringUtils.isEmpty(addDTO.getSubjectName())) {
throw new RuntimeException("标的物名称不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "标的物名称不能为空");
}
if (addDTO.getPurchaseQty() == null) {
throw new RuntimeException("采购数量不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购数量不能为空");
}
if (addDTO.getBudgetAmount() == null) {
throw new RuntimeException("预算金额不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "预算金额不能为空");
}
if (StringUtils.isEmpty(addDTO.getPurchaseMethod())) {
throw new RuntimeException("采购方式不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购方式不能为空");
}
if (addDTO.getApplyDate() == null) {
throw new RuntimeException("采购申请日期不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购申请日期不能为空");
}
if (StringUtils.isEmpty(addDTO.getApplicantId())) {
throw new RuntimeException("申请人工号不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人工号不能为空");
}
if (StringUtils.isEmpty(addDTO.getApplicantName())) {
throw new RuntimeException("申请人姓名不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人姓名不能为空");
}
if (StringUtils.isEmpty(addDTO.getApplyDepartment())) {
throw new RuntimeException("申请部门不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请部门不能为空");
}
// 验证工号格式7位数字
if (!addDTO.getApplicantId().matches("^\\d{7}$")) {
throw new RuntimeException("申请人工号必须为7位数字");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人工号必须为7位数字");
}
if (StringUtils.isNotEmpty(addDTO.getPurchaseLeaderId()) && !addDTO.getPurchaseLeaderId().matches("^\\d{7}$")) {
throw new RuntimeException("采购负责人工号必须为7位数字");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购负责人工号必须为7位数字");
}
// 验证金额非负
if (addDTO.getPurchaseQty().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("采购数量必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购数量必须大于0");
}
if (addDTO.getBudgetAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("预算金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "预算金额必须大于0");
}
if (addDTO.getBidAmount() != null && addDTO.getBidAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("中标金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "中标金额必须大于0");
}
if (addDTO.getActualAmount() != null && addDTO.getActualAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("实际采购金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "实际采购金额必须大于0");
}
if (addDTO.getContractAmount() != null && addDTO.getContractAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("合同金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "合同金额必须大于0");
}
if (addDTO.getSettlementAmount() != null && addDTO.getSettlementAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("结算金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "结算金额必须大于0");
}
}
private List<CcdiPurchaseTransactionSupplier> buildSupplierEntities(
String purchaseId,
List<SupplierImportRow> supplierRows,
String userName
) {
List<SupplierImportRow> normalizedRows = supplierRows == null
? List.of()
: supplierRows.stream()
.filter(Objects::nonNull)
.filter(item -> hasAnySupplierValue(item.data()))
.toList();
List<Integer> winnerRowNums = new ArrayList<>();
Map<String, Integer> supplierKeyRowMap = new LinkedHashMap<>();
List<CcdiPurchaseTransactionSupplier> result = new ArrayList<>();
for (SupplierImportRow supplierImportRow : normalizedRows) {
CcdiPurchaseTransactionSupplierExcel supplierRow = supplierImportRow.data();
int isBidWinner = validateSupplierRow(supplierImportRow);
if (isBidWinner == 1) {
winnerRowNums.add(supplierImportRow.sheetRowNum());
}
String duplicateKey = StringUtils.trimToEmpty(supplierRow.getSupplierName()) + "|"
+ StringUtils.trimToEmpty(supplierRow.getSupplierUscc());
Integer firstRowNum = supplierKeyRowMap.putIfAbsent(duplicateKey, supplierImportRow.sheetRowNum());
if (firstRowNum != null) {
throw buildValidationException(
SUPPLIER_SHEET_NAME,
List.of(firstRowNum, supplierImportRow.sheetRowNum()),
String.format("采购事项ID[%s]存在重复供应商", purchaseId)
);
}
CcdiPurchaseTransactionSupplier supplier = new CcdiPurchaseTransactionSupplier();
supplier.setPurchaseId(purchaseId);
supplier.setSupplierName(StringUtils.trim(supplierRow.getSupplierName()));
supplier.setSupplierUscc(StringUtils.trimToNull(supplierRow.getSupplierUscc()));
supplier.setContactPerson(StringUtils.trimToNull(supplierRow.getContactPerson()));
supplier.setContactPhone(StringUtils.trimToNull(supplierRow.getContactPhone()));
supplier.setSupplierBankAccount(StringUtils.trimToNull(supplierRow.getSupplierBankAccount()));
supplier.setIsBidWinner(isBidWinner);
supplier.setSortOrder(supplierRow.getSortOrder() == null ? result.size() + 1 : supplierRow.getSortOrder());
supplier.setCreatedBy(userName);
supplier.setUpdatedBy(userName);
result.add(supplier);
}
if (winnerRowNums.size() > 1) {
throw buildValidationException(
SUPPLIER_SHEET_NAME,
winnerRowNums,
String.format("采购事项ID[%s]存在多条中标供应商", purchaseId)
);
}
return result;
}
private int validateSupplierRow(SupplierImportRow supplierImportRow) {
CcdiPurchaseTransactionSupplierExcel supplierRow = supplierImportRow.data();
if (StringUtils.isEmpty(supplierRow.getSupplierName())) {
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商名称不能为空");
}
if (StringUtils.length(supplierRow.getSupplierName()) > 200) {
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商名称长度不能超过200个字符");
}
if (StringUtils.length(supplierRow.getContactPerson()) > 50) {
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商联系人长度不能超过50个字符");
}
if (StringUtils.length(supplierRow.getSupplierBankAccount()) > 50) {
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商银行账户长度不能超过50个字符");
}
if (StringUtils.isNotEmpty(supplierRow.getContactPhone())
&& !supplierRow.getContactPhone().matches("^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$")) {
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商联系电话格式不正确");
}
if (StringUtils.isNotEmpty(supplierRow.getSupplierUscc())
&& !supplierRow.getSupplierUscc().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) {
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商统一信用代码格式不正确");
}
return parseIsBidWinner(supplierRow.getIsBidWinner(), supplierImportRow.sheetRowNum());
}
private boolean hasAnySupplierValue(CcdiPurchaseTransactionSupplierExcel supplierRow) {
return StringUtils.isNotEmpty(supplierRow.getPurchaseId())
|| StringUtils.isNotEmpty(supplierRow.getSupplierName())
|| StringUtils.isNotEmpty(supplierRow.getSupplierUscc())
|| StringUtils.isNotEmpty(supplierRow.getContactPerson())
|| StringUtils.isNotEmpty(supplierRow.getContactPhone())
|| StringUtils.isNotEmpty(supplierRow.getSupplierBankAccount())
|| StringUtils.isNotEmpty(supplierRow.getIsBidWinner())
|| supplierRow.getSortOrder() != null;
}
private int parseIsBidWinner(String rawValue, Integer sheetRowNum) {
if (StringUtils.isEmpty(rawValue)) {
return 0;
}
String normalized = StringUtils.trim(rawValue);
if ("1".equals(normalized) || "".equals(normalized) || "Y".equalsIgnoreCase(normalized)
|| "TRUE".equalsIgnoreCase(normalized)) {
return 1;
}
if ("0".equals(normalized) || "".equals(normalized) || "N".equalsIgnoreCase(normalized)
|| "FALSE".equalsIgnoreCase(normalized)) {
return 0;
}
throw buildValidationException(
SUPPLIER_SHEET_NAME,
sheetRowNum == null ? List.of() : List.of(sheetRowNum),
"是否中标仅支持填写“是/否”或“1/0”"
);
}
private void fillWinnerSummary(
CcdiPurchaseTransaction transaction,
List<CcdiPurchaseTransactionSupplier> supplierList
) {
CcdiPurchaseTransactionSupplier winner = supplierList.stream()
.filter(item -> Objects.equals(item.getIsBidWinner(), 1))
.findFirst()
.orElse(null);
if (winner == null) {
transaction.setSupplierName(null);
transaction.setSupplierUscc(null);
transaction.setContactPerson(null);
transaction.setContactPhone(null);
transaction.setSupplierBankAccount(null);
return;
}
transaction.setSupplierName(winner.getSupplierName());
transaction.setSupplierUscc(winner.getSupplierUscc());
transaction.setContactPerson(winner.getContactPerson());
transaction.setContactPhone(winner.getContactPhone());
transaction.setSupplierBankAccount(winner.getSupplierBankAccount());
}
private PurchaseTransactionImportFailureVO buildFailure(
CcdiPurchaseTransactionExcel mainExcel,
String purchaseId,
String sheetName,
String sheetRowNum,
String errorMessage
) {
PurchaseTransactionImportFailureVO failure = new PurchaseTransactionImportFailureVO();
if (mainExcel != null) {
BeanUtils.copyProperties(mainExcel, failure);
}
failure.setSheetName(sheetName);
failure.setSheetRowNum(sheetRowNum);
if (StringUtils.isNotEmpty(purchaseId)) {
failure.setPurchaseId(purchaseId);
}
failure.setErrorMessage(errorMessage);
return failure;
}
private int countImportUnits(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
) {
LinkedHashSet<String> purchaseIds = new LinkedHashSet<>();
purchaseIds.addAll(
mainExcelList.stream()
.map(CcdiPurchaseTransactionExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
purchaseIds.addAll(
supplierExcelList.stream()
.map(CcdiPurchaseTransactionSupplierExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
return purchaseIds.size();
}
private List<MainImportRow> buildMainImportRows(List<CcdiPurchaseTransactionExcel> mainExcelList) {
List<MainImportRow> rows = new ArrayList<>();
for (int i = 0; i < mainExcelList.size(); i++) {
rows.add(new MainImportRow(mainExcelList.get(i), i + EXCEL_DATA_START_ROW));
}
return rows;
}
private List<SupplierImportRow> buildSupplierImportRows(List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList) {
List<SupplierImportRow> rows = new ArrayList<>();
for (int i = 0; i < supplierExcelList.size(); i++) {
rows.add(new SupplierImportRow(supplierExcelList.get(i), i + EXCEL_DATA_START_ROW));
}
return rows;
}
private List<Integer> extractMainRowNums(List<MainImportRow> rows) {
return rows.stream().map(MainImportRow::sheetRowNum).toList();
}
private List<Integer> extractSupplierRowNums(List<SupplierImportRow> rows) {
return rows.stream().map(SupplierImportRow::sheetRowNum).toList();
}
private ImportValidationException buildValidationException(String sheetName, List<Integer> rowNums, String message) {
return new ImportValidationException(sheetName, formatSheetRowNum(rowNums), message);
}
private FailureMeta resolveFailureMeta(
Exception exception,
List<MainImportRow> mainRows,
List<SupplierImportRow> supplierRows
) {
if (exception instanceof ImportValidationException validationException) {
return new FailureMeta(validationException.getSheetName(), validationException.getSheetRowNum());
}
if (!mainRows.isEmpty()) {
return new FailureMeta(MAIN_SHEET_NAME, formatSheetRowNum(extractMainRowNums(mainRows)));
}
if (!supplierRows.isEmpty()) {
return new FailureMeta(SUPPLIER_SHEET_NAME, formatSheetRowNum(extractSupplierRowNums(supplierRows)));
}
return new FailureMeta("", "");
}
private String formatSheetRowNum(List<Integer> rowNums) {
if (rowNums == null || rowNums.isEmpty()) {
return "";
}
return rowNums.stream()
.filter(Objects::nonNull)
.distinct()
.sorted()
.map(String::valueOf)
.collect(Collectors.joining(""));
}
private record MainImportRow(CcdiPurchaseTransactionExcel data, int sheetRowNum) {}
private record SupplierImportRow(CcdiPurchaseTransactionSupplierExcel data, int sheetRowNum) {}
private record FailureMeta(String sheetName, String sheetRowNum) {}
private static class ImportValidationException extends RuntimeException {
private final String sheetName;
private final String sheetRowNum;
private ImportValidationException(String sheetName, String sheetRowNum, String message) {
super(message);
this.sheetName = sheetName;
this.sheetRowNum = sheetRowNum;
}
public String getSheetName() {
return sheetName;
}
public String getSheetRowNum() {
return sheetRowNum;
}
}
}

View File

@@ -2,24 +2,34 @@ package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransaction;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionQueryDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionSupplierDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionSupplierVO;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionMapper;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionSupplierMapper;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionImportService;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -39,6 +49,9 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
@Resource
private ICcdiPurchaseTransactionImportService transactionImportService;
@Resource
private CcdiPurchaseTransactionSupplierMapper supplierMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@@ -93,7 +106,14 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
*/
@Override
public CcdiPurchaseTransactionVO selectTransactionById(String purchaseId) {
return transactionMapper.selectTransactionById(purchaseId);
CcdiPurchaseTransactionVO detail = transactionMapper.selectTransactionById(purchaseId);
if (detail == null) {
return null;
}
List<CcdiPurchaseTransactionSupplierVO> supplierList = selectSupplierListByPurchaseId(purchaseId);
detail.setSupplierList(supplierList);
detail.setSupplierCount(supplierList.size());
return detail;
}
/**
@@ -110,9 +130,12 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
throw new RuntimeException("该采购事项ID已存在");
}
List<CcdiPurchaseTransactionSupplier> supplierList = buildSupplierEntities(addDTO.getPurchaseId(), addDTO.getSupplierList());
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(addDTO, transaction);
fillWinnerSummary(transaction, supplierList);
int result = transactionMapper.insert(transaction);
saveSuppliers(supplierList);
return result;
}
@@ -126,9 +149,13 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
@Override
@Transactional
public int updateTransaction(CcdiPurchaseTransactionEditDTO editDTO) {
List<CcdiPurchaseTransactionSupplier> supplierList = buildSupplierEntities(editDTO.getPurchaseId(), editDTO.getSupplierList());
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(editDTO, transaction);
fillWinnerSummary(transaction, supplierList);
int result = transactionMapper.updateById(transaction);
transactionMapper.deleteSuppliersByPurchaseIds(List.of(editDTO.getPurchaseId()));
saveSuppliers(supplierList);
return result;
}
@@ -142,19 +169,24 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
@Override
@Transactional
public int deleteTransactionByIds(String[] purchaseIds) {
transactionMapper.deleteSuppliersByPurchaseIds(List.of(purchaseIds));
return transactionMapper.deleteBatchIds(java.util.List.of(purchaseIds));
}
/**
* 导入采购交易数据(异步)
*
* @param excelList Excel实体列表
* @param mainExcelList 主信息Excel实体列表
* @param supplierExcelList 供应商明细Excel实体列表
* @return 任务ID
*/
@Override
@Transactional
public String importTransaction(java.util.List<CcdiPurchaseTransactionExcel> excelList) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
public String importTransaction(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
) {
if ((mainExcelList == null || mainExcelList.isEmpty()) && (supplierExcelList == null || supplierExcelList.isEmpty())) {
throw new RuntimeException("至少需要一条数据");
}
@@ -170,7 +202,7 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("totalCount", countImportUnits(mainExcelList, supplierExcelList));
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
@@ -181,8 +213,134 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
// 调用异步导入服务
transactionImportService.importTransactionAsync(excelList, taskId, userName);
transactionImportService.importTransactionAsync(mainExcelList, supplierExcelList, taskId, userName);
return taskId;
}
private int countImportUnits(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
) {
LinkedHashSet<String> purchaseIds = new LinkedHashSet<>();
if (mainExcelList != null) {
purchaseIds.addAll(
mainExcelList.stream()
.map(CcdiPurchaseTransactionExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
}
if (supplierExcelList != null) {
purchaseIds.addAll(
supplierExcelList.stream()
.map(CcdiPurchaseTransactionSupplierExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
}
return purchaseIds.isEmpty() ? 0 : purchaseIds.size();
}
private List<CcdiPurchaseTransactionSupplier> buildSupplierEntities(
String purchaseId,
List<CcdiPurchaseTransactionSupplierDTO> supplierDTOList
) {
List<CcdiPurchaseTransactionSupplierDTO> normalizedList = normalizeSupplierList(supplierDTOList);
validateSupplierList(normalizedList);
List<CcdiPurchaseTransactionSupplier> supplierList = new ArrayList<>();
for (int i = 0; i < normalizedList.size(); i++) {
CcdiPurchaseTransactionSupplierDTO dto = normalizedList.get(i);
CcdiPurchaseTransactionSupplier supplier = new CcdiPurchaseTransactionSupplier();
BeanUtils.copyProperties(dto, supplier);
supplier.setPurchaseId(purchaseId);
supplier.setIsBidWinner(Objects.equals(dto.getIsBidWinner(), 1) ? 1 : 0);
supplier.setSortOrder(dto.getSortOrder() == null ? i + 1 : dto.getSortOrder());
supplierList.add(supplier);
}
return supplierList;
}
private List<CcdiPurchaseTransactionSupplierDTO> normalizeSupplierList(
List<CcdiPurchaseTransactionSupplierDTO> supplierDTOList
) {
if (supplierDTOList == null) {
return List.of();
}
return supplierDTOList.stream()
.filter(Objects::nonNull)
.filter(this::hasAnySupplierValue)
.toList();
}
private boolean hasAnySupplierValue(CcdiPurchaseTransactionSupplierDTO supplierDTO) {
return StringUtils.isNotEmpty(supplierDTO.getSupplierName())
|| StringUtils.isNotEmpty(supplierDTO.getSupplierUscc())
|| StringUtils.isNotEmpty(supplierDTO.getContactPerson())
|| StringUtils.isNotEmpty(supplierDTO.getContactPhone())
|| StringUtils.isNotEmpty(supplierDTO.getSupplierBankAccount())
|| supplierDTO.getIsBidWinner() != null
|| supplierDTO.getSortOrder() != null;
}
private void validateSupplierList(List<CcdiPurchaseTransactionSupplierDTO> supplierList) {
long winnerCount = supplierList.stream()
.filter(item -> Objects.equals(item.getIsBidWinner(), 1))
.count();
if (winnerCount > 1) {
throw new RuntimeException("同一招投标事项仅允许维护一条中标供应商");
}
LinkedHashSet<String> duplicateKeys = new LinkedHashSet<>();
for (CcdiPurchaseTransactionSupplierDTO supplier : supplierList) {
String duplicateKey = StringUtils.trimToEmpty(supplier.getSupplierName()) + "|"
+ StringUtils.trimToEmpty(supplier.getSupplierUscc());
if (!duplicateKeys.add(duplicateKey)) {
throw new RuntimeException("同一招投标事项存在重复供应商,请检查供应商名称和统一信用代码");
}
}
}
private void fillWinnerSummary(
CcdiPurchaseTransaction transaction,
List<CcdiPurchaseTransactionSupplier> supplierList
) {
CcdiPurchaseTransactionSupplier winnerSupplier = supplierList.stream()
.filter(item -> Objects.equals(item.getIsBidWinner(), 1))
.findFirst()
.orElse(null);
if (winnerSupplier == null) {
transaction.setSupplierName(null);
transaction.setSupplierUscc(null);
transaction.setContactPerson(null);
transaction.setContactPhone(null);
transaction.setSupplierBankAccount(null);
return;
}
transaction.setSupplierName(winnerSupplier.getSupplierName());
transaction.setSupplierUscc(winnerSupplier.getSupplierUscc());
transaction.setContactPerson(winnerSupplier.getContactPerson());
transaction.setContactPhone(winnerSupplier.getContactPhone());
transaction.setSupplierBankAccount(winnerSupplier.getSupplierBankAccount());
}
private void saveSuppliers(List<CcdiPurchaseTransactionSupplier> supplierList) {
for (CcdiPurchaseTransactionSupplier supplier : supplierList) {
supplierMapper.insert(supplier);
}
}
private List<CcdiPurchaseTransactionSupplierVO> selectSupplierListByPurchaseId(String purchaseId) {
return supplierMapper.selectList(
new LambdaQueryWrapper<CcdiPurchaseTransactionSupplier>()
.eq(CcdiPurchaseTransactionSupplier::getPurchaseId, purchaseId)
.orderByAsc(CcdiPurchaseTransactionSupplier::getSortOrder)
.orderByAsc(CcdiPurchaseTransactionSupplier::getId)
).stream().map(entity -> {
CcdiPurchaseTransactionSupplierVO vo = new CcdiPurchaseTransactionSupplierVO();
BeanUtils.copyProperties(entity, vo);
return vo;
}).collect(Collectors.toList());
}
}

View File

@@ -2,14 +2,14 @@ package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationAddDTO;
import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.StaffEnterpriseRelationImportFailureVO;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiStaffEnterpriseRelationImportService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
@@ -29,7 +29,7 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 员工实体关系信息异步导入服务层处理
* 员工亲属实体关异步导入服务层处理
*
* @author ruoyi
* @date 2026-02-09
@@ -47,7 +47,7 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
private RedisTemplate<String, Object> redisTemplate;
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
private CcdiStaffFmyRelationMapper familyRelationMapper;
@Override
@Async
@@ -56,37 +56,48 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
long startTime = System.currentTimeMillis();
// 记录导入开始
ImportLogUtils.logImportStart(log, taskId, "员工实体关", excelList.size(), userName);
ImportLogUtils.logImportStart(log, taskId, "员工亲属实体关", excelList.size(), userName);
List<CcdiStaffEnterpriseRelation> newRecords = new ArrayList<>();
List<StaffEnterpriseRelationImportFailureVO> failures = new ArrayList<>();
// 批量验证员工身份证号是否存在
Set<String> excelPersonIds = excelList.stream()
.map(CcdiStaffEnterpriseRelationExcel::getPersonId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toSet());
Set<String> existingPersonIds = new HashSet<>();
Map<String, CcdiStaffFmyRelation> validFamilies = new HashMap<>();
Set<String> knownFamilyCertNos = new HashSet<>();
Map<String, String> familyNameMap = new HashMap<>();
if (!excelPersonIds.isEmpty()) {
ImportLogUtils.logBatchQueryStart(log, taskId, "员工身份证号", excelPersonIds.size());
ImportLogUtils.logBatchQueryStart(log, taskId, "员工亲属关系", excelPersonIds.size());
LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
wrapper.select(CcdiBaseStaff::getIdCard)
.in(CcdiBaseStaff::getIdCard, excelPersonIds);
LambdaQueryWrapper<CcdiStaffFmyRelation> wrapper = new LambdaQueryWrapper<>();
wrapper.select(
CcdiStaffFmyRelation::getRelationCertNo,
CcdiStaffFmyRelation::getRelationName,
CcdiStaffFmyRelation::getPersonId,
CcdiStaffFmyRelation::getStatus,
CcdiStaffFmyRelation::getIsEmpFamily
)
.in(CcdiStaffFmyRelation::getRelationCertNo, excelPersonIds);
List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
existingPersonIds = existingStaff.stream()
.map(CcdiBaseStaff::getIdCard)
.collect(Collectors.toSet());
List<CcdiStaffFmyRelation> familyRelations = familyRelationMapper.selectList(wrapper);
for (CcdiStaffFmyRelation familyRelation : familyRelations) {
knownFamilyCertNos.add(familyRelation.getRelationCertNo());
familyNameMap.putIfAbsent(familyRelation.getRelationCertNo(), familyRelation.getRelationName());
if (Boolean.TRUE.equals(familyRelation.getIsEmpFamily()) && Integer.valueOf(1).equals(familyRelation.getStatus())) {
validFamilies.putIfAbsent(familyRelation.getRelationCertNo(), familyRelation);
}
}
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工身份证号", existingPersonIds.size());
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工亲属关系", familyRelations.size());
}
// 批量查询已存在的person_id + social_credit_code组合
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的员工企业关系组合", excelList.size());
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的员工亲属实体关联组合", excelList.size());
Set<String> existingCombinations = getExistingCombinations(excelList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工企业关系组合", existingCombinations.size());
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工亲属实体关联组合", existingCombinations.size());
// 用于跟踪Excel文件内已处理的组合
Set<String> processedCombinations = new HashSet<>();
@@ -103,41 +114,18 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
// 验证数据
validateRelationData(addDTO);
// 身份证号存在性检查(在基本验证之后)
if (!existingPersonIds.contains(excel.getPersonId())) {
throw new RuntimeException(String.format(
"第%d行: 身份证号[%s]不存在于员工信息表中,请先添加员工信息",
i + 1, excel.getPersonId()));
}
CcdiStaffFmyRelation familyRelation = validFamilies.get(excel.getPersonId());
CcdiStaffEnterpriseRelation relation = validateAndBuildEntity(
excel,
familyRelation,
knownFamilyCertNos,
existingCombinations,
processedCombinations,
userName
);
String combination = excel.getPersonId() + "|" + excel.getSocialCreditCode();
CcdiStaffEnterpriseRelation relation = new CcdiStaffEnterpriseRelation();
BeanUtils.copyProperties(excel, relation);
if (existingCombinations.contains(combination)) {
// 组合已存在,直接报错
throw new RuntimeException(String.format("身份证号[%s]和统一社会信用代码[%s]的组合已存在,请勿重复导入",
excel.getPersonId(), excel.getSocialCreditCode()));
} else if (processedCombinations.contains(combination)) {
// Excel文件内部重复
throw new RuntimeException(String.format("身份证号[%s]和统一社会信用代码[%s]的组合在导入文件中重复,已跳过此条记录",
excel.getPersonId(), excel.getSocialCreditCode()));
} else {
relation.setCreatedBy(userName);
relation.setUpdatedBy(userName);
// 设置默认值
relation.setStatus(1);
relation.setIsEmployee(0);
relation.setIsEmpFamily(1);
relation.setIsCustomer(0);
relation.setIsCustFamily(0);
relation.setDataSource("IMPORT");
newRecords.add(relation);
processedCombinations.add(combination); // 标记为已处理
}
newRecords.add(relation);
processedCombinations.add(excel.getPersonId() + "|" + excel.getSocialCreditCode());
// 记录进度
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
@@ -146,11 +134,12 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
} catch (Exception e) {
StaffEnterpriseRelationImportFailureVO failure = new StaffEnterpriseRelationImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setRelationName(familyNameMap.get(excel.getPersonId()));
failure.setErrorMessage(e.getMessage());
failures.add(failure);
// 记录验证失败日志
String keyData = String.format("身份证号=%s, 统一社会信用代码=%s, 企业名称=%s",
String keyData = String.format("亲属身份证号=%s, 统一社会信用代码=%s, 企业名称=%s",
excel.getPersonId(), excel.getSocialCreditCode(), excel.getEnterpriseName());
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
}
@@ -166,7 +155,7 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
// 保存失败记录到Redis
if (!failures.isEmpty()) {
try {
String failuresKey = "import:staffEnterpriseRelation:" + taskId + ":failures";
String failuresKey = "import:staffEnterpriseRelation:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
} catch (Exception e) {
@@ -185,7 +174,7 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
// 记录导入完成
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "员工实体关",
ImportLogUtils.logImportComplete(log, taskId, "员工亲属实体关",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
}
@@ -251,9 +240,9 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
statusData.put("endTime", System.currentTimeMillis());
if ("SUCCESS".equals(status)) {
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
statusData.put("message", "员工亲属实体关联导入全部成功!共导入" + result.getTotalCount() + "条数据");
} else {
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "");
statusData.put("message", "员工亲属实体关联导入成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "");
}
redisTemplate.opsForHash().putAll(key, statusData);
@@ -297,14 +286,14 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
}
/**
* 验证员工实体关数据
* 验证员工亲属实体关联基础数据
*
* @param addDTO 新增DTO
*/
private void validateRelationData(CcdiStaffEnterpriseRelationAddDTO addDTO) {
// 验证必填字段
if (StringUtils.isEmpty(addDTO.getPersonId())) {
throw new RuntimeException("身份证号不能为空");
throw new RuntimeException("亲属身份证号不能为空");
}
if (StringUtils.isEmpty(addDTO.getSocialCreditCode())) {
throw new RuntimeException("统一社会信用代码不能为空");
@@ -313,9 +302,9 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
throw new RuntimeException("企业名称不能为空");
}
// 验证身份证号格式18位
// 验证亲属身份证号格式18位
if (!addDTO.getPersonId().matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$")) {
throw new RuntimeException("身份证号格式不正确必须为18位有效身份证号");
throw new RuntimeException("亲属身份证号格式不正确必须为18位有效身份证号");
}
// 验证统一社会信用代码格式18位
@@ -331,4 +320,38 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
throw new RuntimeException("企业名称长度不能超过200个字符");
}
}
CcdiStaffEnterpriseRelation validateAndBuildEntity(CcdiStaffEnterpriseRelationExcel excel,
CcdiStaffFmyRelation familyRelation,
Set<String> knownFamilyCertNos,
Set<String> existingCombinations,
Set<String> processedCombinations,
String userName) {
String combination = excel.getPersonId() + "|" + excel.getSocialCreditCode();
if (familyRelation == null) {
if (knownFamilyCertNos.contains(excel.getPersonId())) {
throw new RuntimeException("亲属身份证号[" + excel.getPersonId() + "]不是有效员工亲属,请先维护有效的员工亲属关系");
}
throw new RuntimeException("亲属身份证号[" + excel.getPersonId() + "]不存在,请先维护员工亲属关系");
}
if (existingCombinations.contains(combination)) {
throw new RuntimeException("亲属身份证号[" + excel.getPersonId() + "]和统一社会信用代码[" + excel.getSocialCreditCode() + "]的组合已存在,请勿重复导入");
}
if (processedCombinations.contains(combination)) {
throw new RuntimeException("亲属身份证号[" + excel.getPersonId() + "]和统一社会信用代码[" + excel.getSocialCreditCode() + "]的组合在导入文件中重复,已跳过此条记录");
}
CcdiStaffEnterpriseRelation relation = new CcdiStaffEnterpriseRelation();
BeanUtils.copyProperties(excel, relation);
relation.setCreatedBy(userName);
relation.setUpdatedBy(userName);
relation.setStatus(1);
relation.setIsEmployee(0);
relation.setIsEmpFamily(1);
relation.setIsCustomer(0);
relation.setIsCustFamily(0);
relation.setDataSource("IMPORT");
return relation;
}
}

View File

@@ -3,11 +3,14 @@ package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationVO;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiStaffEnterpriseRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffEnterpriseRelationService;
@@ -37,6 +40,9 @@ public class CcdiStaffEnterpriseRelationServiceImpl implements ICcdiStaffEnterpr
@Resource
private CcdiStaffEnterpriseRelationMapper relationMapper;
@Resource
private CcdiStaffFmyRelationMapper familyRelationMapper;
@Resource
private ICcdiStaffEnterpriseRelationImportService relationImportService;
@@ -86,6 +92,11 @@ public class CcdiStaffEnterpriseRelationServiceImpl implements ICcdiStaffEnterpr
}).collect(Collectors.toList());
}
@Override
public java.util.List<CcdiStaffEnterpriseRelationOptionVO> selectFamilyOptions(String query) {
return relationMapper.selectFamilyOptions(query);
}
/**
* 查询员工实体关系详情
*
@@ -106,16 +117,15 @@ public class CcdiStaffEnterpriseRelationServiceImpl implements ICcdiStaffEnterpr
@Override
@Transactional
public int insertRelation(CcdiStaffEnterpriseRelationAddDTO addDTO) {
// 检查身份证号+统一社会信用代码唯一性
validateEffectiveFamily(addDTO.getPersonId());
if (relationMapper.existsByPersonIdAndSocialCreditCode(addDTO.getPersonId(), addDTO.getSocialCreditCode())) {
throw new RuntimeException("该身份证号和统一社会信用代码组合已存在");
throw new RuntimeException("亲属身份证号和统一社会信用代码组合已存在");
}
CcdiStaffEnterpriseRelation relation = new CcdiStaffEnterpriseRelation();
BeanUtils.copyProperties(addDTO, relation);
// 设置默认值
// 新增时强制设置状态为有效
relation.setStatus(1);
if (relation.getIsEmployee() == null) {
@@ -159,7 +169,7 @@ public class CcdiStaffEnterpriseRelationServiceImpl implements ICcdiStaffEnterpr
updateWrapper.set(editDTO.getRemark() != null, CcdiStaffEnterpriseRelation::getRemark, editDTO.getRemark());
// 注意:以下字段不可修改
// - personId身份证号业务主键
// - personId亲属身份证号,业务主键)
// - socialCreditCode统一社会信用代码业务主键
// - dataSource数据来源系统字段
// - isEmployee是否为员工系统字段
@@ -224,4 +234,28 @@ public class CcdiStaffEnterpriseRelationServiceImpl implements ICcdiStaffEnterpr
return taskId;
}
private CcdiStaffFmyRelation validateEffectiveFamily(String familyCertNo) {
CcdiStaffFmyRelation validFamily = familyRelationMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CcdiStaffFmyRelation>()
.eq(CcdiStaffFmyRelation::getRelationCertNo, familyCertNo)
.eq(CcdiStaffFmyRelation::getIsEmpFamily, Boolean.TRUE)
.eq(CcdiStaffFmyRelation::getStatus, 1)
.last("LIMIT 1")
);
if (validFamily != null) {
return validFamily;
}
CcdiStaffFmyRelation existingFamily = familyRelationMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CcdiStaffFmyRelation>()
.select(CcdiStaffFmyRelation::getId)
.eq(CcdiStaffFmyRelation::getRelationCertNo, familyCertNo)
.last("LIMIT 1")
);
if (existingFamily == null) {
throw new RuntimeException("亲属身份证号[" + familyCertNo + "]不存在,请先维护员工亲属关系");
}
throw new RuntimeException("亲属身份证号[" + familyCertNo + "]不是有效员工亲属,请先维护有效的员工亲属关系");
}
}

View File

@@ -41,6 +41,8 @@ import java.util.stream.Collectors;
public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelationImportService {
private static final Logger log = LoggerFactory.getLogger(CcdiStaffFmyRelationImportServiceImpl.class);
private static final String SHEET_NAME = "员工亲属关系信息";
private static final int EXCEL_DATA_START_ROW = 2;
@Resource
private CcdiStaffFmyRelationMapper relationMapper;
@@ -168,6 +170,8 @@ public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelat
} catch (Exception e) {
StaffFmyRelationImportFailureVO failure = new StaffFmyRelationImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName(SHEET_NAME);
failure.setRowNum(i + EXCEL_DATA_START_ROW);
failure.setErrorMessage(e.getMessage());
failures.add(failure);

View File

@@ -3,12 +3,13 @@ package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.vo.CcdiAssetInfoVO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiAssetInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.service.ICcdiAssetInfoService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
@@ -50,6 +51,9 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
@Resource
private ICcdiAssetInfoService assetInfoService;
@Resource
private CcdiStaffEnterpriseRelationMapper staffEnterpriseRelationMapper;
/**
* 查询员工亲属关系列表
*
@@ -161,6 +165,9 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
CcdiStaffFmyRelation relation = new CcdiStaffFmyRelation();
BeanUtils.copyProperties(editDTO, relation);
int result = relationMapper.updateById(relation);
if (Integer.valueOf(1).equals(existing.getStatus()) && Integer.valueOf(0).equals(editDTO.getStatus())) {
staffEnterpriseRelationMapper.invalidateByFamilyCertNo(existing.getRelationCertNo());
}
assetInfoService.replaceByFamilyIdAndPersonId(editDTO.getPersonId(), editDTO.getRelationCertNo(), editDTO.getAssetInfoList());
return result;
}

View File

@@ -2,6 +2,8 @@ package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.utils.IdCardUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiStaffRecruitment;
import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork;
import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO;
@@ -16,9 +18,19 @@ import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper;
import com.ruoyi.info.collection.service.ICcdiStaffRecruitmentImportService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.IdCardUtil;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
@@ -28,10 +40,6 @@ import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 招聘信息异步导入Service实现
*
@@ -44,6 +52,10 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
private static final Logger log = LoggerFactory.getLogger(CcdiStaffRecruitmentImportServiceImpl.class);
private static final String MAIN_SHEET_NAME = "招聘信息";
private static final String WORK_SHEET_NAME = "历史工作经历";
private static final int EXCEL_DATA_START_ROW = 2;
@Resource
private CcdiStaffRecruitmentMapper recruitmentMapper;
@@ -56,181 +68,56 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
@Override
@Async
@Transactional
public void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
public void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList,
String taskId,
String userName) {
List<CcdiStaffRecruitmentExcel> safeRecruitmentList = recruitmentList == null
? Collections.emptyList()
: recruitmentList;
List<CcdiStaffRecruitmentWorkExcel> safeWorkList = workList == null
? Collections.emptyList()
: workList;
int totalCount = safeRecruitmentList.size() + safeWorkList.size();
long startTime = System.currentTimeMillis();
// 记录导入开始
ImportLogUtils.logImportStart(log, taskId, "招聘信息", excelList.size(), userName);
ImportLogUtils.logImportStart(log, taskId, "招聘信息双Sheet", totalCount, userName);
List<CcdiStaffRecruitment> newRecords = new ArrayList<>();
List<RecruitmentImportFailureVO> failures = new ArrayList<>();
List<MainImportRow> indexedMainRows = buildMainImportRows(safeRecruitmentList);
List<WorkImportRow> indexedWorkRows = buildWorkImportRows(safeWorkList);
// 批量查询已存在的招聘记录编号
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的招聘记录编号", excelList.size());
Set<String> existingRecruitIds = getExistingRecruitIds(excelList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "招聘记录编号", existingRecruitIds.size());
MainImportResult mainImportResult = importMainSheet(indexedMainRows, failures, userName, taskId);
int workSuccessCount = importWorkSheet(
indexedWorkRows,
mainImportResult.importedRecruitmentMap(),
failures,
userName,
taskId
);
// 用于检测Excel内部的重复ID
Set<String> excelProcessedIds = new HashSet<>();
// 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffRecruitmentExcel excel = excelList.get(i);
try {
// 转换为AddDTO进行验证
CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO();
BeanUtils.copyProperties(excel, addDTO);
addDTO.setRecruitType(RecruitType.inferCode(addDTO.getRecruitName()));
// 验证数据
validateRecruitmentData(addDTO, existingRecruitIds);
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
BeanUtils.copyProperties(excel, recruitment);
recruitment.setRecruitType(addDTO.getRecruitType());
if (existingRecruitIds.contains(excel.getRecruitId())) {
// 招聘记录编号在数据库中已存在,直接报错
throw new RuntimeException(String.format("招聘记录编号[%s]已存在,请勿重复导入", excel.getRecruitId()));
} else if (excelProcessedIds.contains(excel.getRecruitId())) {
// 招聘记录编号在Excel文件内部重复
throw new RuntimeException(String.format("招聘记录编号[%s]在导入文件中重复,已跳过此条记录", excel.getRecruitId()));
} else {
recruitment.setCreatedBy(userName);
recruitment.setUpdatedBy(userName);
newRecords.add(recruitment);
excelProcessedIds.add(excel.getRecruitId()); // 标记为已处理
}
// 记录进度
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
newRecords.size(), failures.size());
} catch (Exception e) {
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
// 记录验证失败日志
String keyData = String.format("招聘记录编号=%s, 项目名称=%s, 应聘人员=%s",
excel.getRecruitId(), excel.getRecruitName(), excel.getCandName());
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
}
}
// 批量插入新数据
if (!newRecords.isEmpty()) {
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
(newRecords.size() + 499) / 500, 500);
saveBatch(newRecords, 500);
}
// 保存失败记录到Redis
if (!failures.isEmpty()) {
try {
String failuresKey = "import:recruitment:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
} catch (Exception e) {
ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e);
}
saveFailures(taskId, failures);
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size());
result.setFailureCount(failures.size());
result.setTotalCount(totalCount);
result.setSuccessCount(mainImportResult.successCount() + workSuccessCount);
result.setFailureCount(Math.max(totalCount - result.getSuccessCount(), 0));
// 更新最终状态
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
updateImportStatus(taskId, finalStatus, result);
// 记录导入完成
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "招聘信息",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
}
@Override
@Async
@Transactional
public void importRecruitmentWorkAsync(List<CcdiStaffRecruitmentWorkExcel> excelList,
String taskId,
String userName) {
long startTime = System.currentTimeMillis();
ImportLogUtils.logImportStart(log, taskId, "招聘历史工作经历", excelList.size(), userName);
List<RecruitmentImportFailureVO> failures = new ArrayList<>();
List<CcdiStaffRecruitmentWork> validRecords = new ArrayList<>();
Set<String> failedRecruitIds = new HashSet<>();
Set<String> processedRecruitSortKeys = new HashSet<>();
Map<String, CcdiStaffRecruitment> recruitmentMap = getRecruitmentMap(excelList);
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffRecruitmentWorkExcel excel = excelList.get(i);
try {
CcdiStaffRecruitment recruitment = recruitmentMap.get(trim(excel.getRecruitId()));
validateRecruitmentWorkData(excel, recruitment, processedRecruitSortKeys);
CcdiStaffRecruitmentWork work = new CcdiStaffRecruitmentWork();
BeanUtils.copyProperties(excel, work);
work.setRecruitId(trim(excel.getRecruitId()));
work.setCreatedBy(userName);
work.setUpdatedBy(userName);
validRecords.add(work);
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
validRecords.size(), failures.size());
} catch (Exception e) {
failedRecruitIds.add(trim(excel.getRecruitId()));
failures.add(buildWorkFailure(excel, e.getMessage()));
String keyData = String.format("招聘记录编号=%s, 候选人=%s, 工作单位=%s",
excel.getRecruitId(), excel.getCandName(), excel.getCompanyName());
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
}
}
List<CcdiStaffRecruitmentWork> importRecords = validRecords.stream()
.filter(work -> !failedRecruitIds.contains(work.getRecruitId()))
.toList();
appendSkippedFailures(validRecords, failedRecruitIds, failures);
if (!importRecords.isEmpty()) {
Set<String> importRecruitIds = importRecords.stream()
.map(CcdiStaffRecruitmentWork::getRecruitId)
.collect(Collectors.toSet());
LambdaQueryWrapper<CcdiStaffRecruitmentWork> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.in(CcdiStaffRecruitmentWork::getRecruitId, importRecruitIds);
recruitmentWorkMapper.delete(deleteWrapper);
importRecords.forEach(recruitmentWorkMapper::insert);
}
if (!failures.isEmpty()) {
try {
String failuresKey = "import:recruitment:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
} catch (Exception e) {
ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e);
}
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(importRecords.size());
result.setFailureCount(failures.size());
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
String finalStatus = resolveFinalStatus(result);
updateImportStatus(taskId, finalStatus, result);
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "招聘历史工作经历",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
ImportLogUtils.logImportComplete(
log,
taskId,
"招聘信息双Sheet",
totalCount,
result.getSuccessCount(),
result.getFailureCount(),
duration
);
}
@Override
@@ -270,14 +157,188 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
return JSON.parseArray(JSON.toJSONString(failuresObj), RecruitmentImportFailureVO.class);
}
/**
* 批量查询已存在的招聘记录编号
*/
private Set<String> getExistingRecruitIds(List<CcdiStaffRecruitmentExcel> excelList) {
List<String> recruitIds = excelList.stream()
.map(CcdiStaffRecruitmentExcel::getRecruitId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
private MainImportResult importMainSheet(List<MainImportRow> mainRows,
List<RecruitmentImportFailureVO> failures,
String userName,
String taskId) {
if (mainRows.isEmpty()) {
return new MainImportResult(Collections.emptyMap(), 0);
}
Set<String> existingRecruitIds = getExistingRecruitIds(
mainRows.stream().map(MainImportRow::data).toList()
);
Set<String> processedRecruitIds = new HashSet<>();
List<CcdiStaffRecruitment> newRecords = new ArrayList<>();
Map<String, CcdiStaffRecruitment> importedRecruitmentMap = new LinkedHashMap<>();
for (int index = 0; index < mainRows.size(); index++) {
MainImportRow mainRow = mainRows.get(index);
CcdiStaffRecruitmentExcel excel = mainRow.data();
try {
CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO();
BeanUtils.copyProperties(excel, addDTO);
addDTO.setRecruitType(RecruitType.inferCode(addDTO.getRecruitName()));
validateRecruitmentData(addDTO, mainRow.sheetRowNum());
String recruitId = trim(excel.getRecruitId());
if (existingRecruitIds.contains(recruitId)) {
throw buildValidationException(
MAIN_SHEET_NAME,
List.of(mainRow.sheetRowNum()),
String.format("招聘记录编号[%s]已存在,请勿重复导入", recruitId)
);
}
if (!processedRecruitIds.add(recruitId)) {
throw buildValidationException(
MAIN_SHEET_NAME,
List.of(mainRow.sheetRowNum()),
String.format("招聘记录编号[%s]在导入文件中重复,已跳过此条记录", recruitId)
);
}
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
BeanUtils.copyProperties(excel, recruitment);
recruitment.setRecruitId(recruitId);
recruitment.setRecruitType(addDTO.getRecruitType());
recruitment.setCreatedBy(userName);
recruitment.setUpdatedBy(userName);
newRecords.add(recruitment);
importedRecruitmentMap.put(recruitId, recruitment);
ImportLogUtils.logProgress(log, taskId, index + 1, mainRows.size(), newRecords.size(), failures.size());
} catch (Exception exception) {
FailureMeta failureMeta = resolveFailureMeta(exception, List.of(mainRow.sheetRowNum()), MAIN_SHEET_NAME);
failures.add(buildFailure(excel, failureMeta.sheetName(), failureMeta.sheetRowNum(), exception.getMessage()));
ImportLogUtils.logValidationError(
log,
taskId,
index + 1,
exception.getMessage(),
String.format("招聘记录编号=%s, 项目名称=%s, 应聘人员=%s", excel.getRecruitId(), excel.getRecruitName(), excel.getCandName())
);
}
}
if (!newRecords.isEmpty()) {
ImportLogUtils.logBatchOperationStart(log, taskId, "插入招聘信息", (newRecords.size() + 499) / 500, 500);
saveBatch(newRecords, 500);
}
return new MainImportResult(importedRecruitmentMap, newRecords.size());
}
private int importWorkSheet(List<WorkImportRow> workRows,
Map<String, CcdiStaffRecruitment> importedRecruitmentMap,
List<RecruitmentImportFailureVO> failures,
String userName,
String taskId) {
if (workRows.isEmpty()) {
return 0;
}
Map<String, CcdiStaffRecruitment> existingRecruitmentMap =
getExistingRecruitmentMap(workRows, importedRecruitmentMap);
Map<String, List<WorkImportRow>> groupedRows = groupWorkRows(workRows);
int successCount = 0;
int processedGroups = 0;
for (List<WorkImportRow> recruitWorkRows : groupedRows.values()) {
processedGroups++;
WorkImportRow firstRow = recruitWorkRows.get(0);
String recruitId = trim(firstRow.data().getRecruitId());
CcdiStaffRecruitment recruitment = importedRecruitmentMap.get(recruitId);
if (recruitment == null) {
recruitment = existingRecruitmentMap.get(recruitId);
}
try {
validateWorkGroup(recruitWorkRows, recruitment);
if (StringUtils.isNotEmpty(recruitId) && hasExistingWorkHistory(recruitId)) {
throw buildValidationException(
WORK_SHEET_NAME,
extractWorkRowNums(recruitWorkRows),
String.format("招聘记录编号[%s]已存在历史工作经历,不允许重复导入", recruitId)
);
}
List<CcdiStaffRecruitmentWork> entities = buildWorkEntities(recruitWorkRows, userName);
entities.forEach(entity -> recruitmentWorkMapper.insert(entity));
successCount += recruitWorkRows.size();
ImportLogUtils.logProgress(log, taskId, processedGroups, groupedRows.size(), successCount, failures.size());
} catch (Exception exception) {
FailureMeta failureMeta = resolveFailureMeta(exception, extractWorkRowNums(recruitWorkRows), WORK_SHEET_NAME);
failures.add(buildFailure(firstRow.data(), failureMeta.sheetName(), failureMeta.sheetRowNum(), exception.getMessage()));
ImportLogUtils.logValidationError(
log,
taskId,
processedGroups,
exception.getMessage(),
String.format(
"招聘记录编号=%s, 候选人=%s, 工作单位=%s",
firstRow.data().getRecruitId(),
firstRow.data().getCandName(),
firstRow.data().getCompanyName()
)
);
}
}
return successCount;
}
private Map<String, List<WorkImportRow>> groupWorkRows(List<WorkImportRow> workRows) {
Map<String, List<WorkImportRow>> groupedRows = new LinkedHashMap<>();
for (WorkImportRow workRow : workRows) {
groupedRows.computeIfAbsent(buildWorkGroupKey(workRow), key -> new ArrayList<>()).add(workRow);
}
return groupedRows;
}
private String buildWorkGroupKey(WorkImportRow workRow) {
String recruitId = trim(workRow.data().getRecruitId());
if (StringUtils.isNotEmpty(recruitId)) {
return recruitId;
}
return "__ROW__" + workRow.sheetRowNum();
}
private Map<String, CcdiStaffRecruitment> getExistingRecruitmentMap(List<WorkImportRow> workRows,
Map<String, CcdiStaffRecruitment> importedRecruitmentMap) {
LinkedHashSet<String> recruitIds = workRows.stream()
.map(row -> trim(row.data().getRecruitId()))
.filter(StringUtils::isNotEmpty)
.filter(recruitId -> !importedRecruitmentMap.containsKey(recruitId))
.collect(Collectors.toCollection(LinkedHashSet::new));
if (recruitIds.isEmpty()) {
return Collections.emptyMap();
}
List<CcdiStaffRecruitment> recruitments = recruitmentMapper.selectBatchIds(recruitIds);
return recruitments.stream().collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item));
}
private List<CcdiStaffRecruitmentWork> buildWorkEntities(List<WorkImportRow> workRows, String userName) {
List<CcdiStaffRecruitmentWork> entities = new ArrayList<>();
for (WorkImportRow workRow : workRows) {
CcdiStaffRecruitmentWork entity = new CcdiStaffRecruitmentWork();
BeanUtils.copyProperties(workRow.data(), entity);
entity.setRecruitId(trim(workRow.data().getRecruitId()));
entity.setCreatedBy(userName);
entity.setUpdatedBy(userName);
entities.add(entity);
}
return entities;
}
private Set<String> getExistingRecruitIds(List<CcdiStaffRecruitmentExcel> recruitmentList) {
List<String> recruitIds = recruitmentList.stream()
.map(CcdiStaffRecruitmentExcel::getRecruitId)
.map(this::trim)
.filter(StringUtils::isNotEmpty)
.toList();
if (recruitIds.isEmpty()) {
return Collections.emptySet();
@@ -288,148 +349,138 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
List<CcdiStaffRecruitment> existingRecruitments = recruitmentMapper.selectList(wrapper);
return existingRecruitments.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
}
/**
* 验证招聘信息数据
*/
private void validateRecruitmentData(CcdiStaffRecruitmentAddDTO addDTO,
Set<String> existingRecruitIds) {
// 验证必填字段
private boolean hasExistingWorkHistory(String recruitId) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId);
return recruitmentWorkMapper.selectCount(wrapper) > 0;
}
private void validateRecruitmentData(CcdiStaffRecruitmentAddDTO addDTO, int sheetRowNum) {
if (StringUtils.isEmpty(addDTO.getRecruitId())) {
throw new RuntimeException("招聘记录编号不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不能为空");
}
if (StringUtils.isEmpty(addDTO.getRecruitName())) {
throw new RuntimeException("招聘项目名称不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘项目名称不能为空");
}
if (StringUtils.isEmpty(addDTO.getPosName())) {
throw new RuntimeException("职位名称不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位名称不能为空");
}
if (StringUtils.isEmpty(addDTO.getPosCategory())) {
throw new RuntimeException("职位类别不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位类别不能为空");
}
if (StringUtils.isEmpty(addDTO.getPosDesc())) {
throw new RuntimeException("职位描述不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位描述不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandName())) {
throw new RuntimeException("应聘人员姓名不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员姓名不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandEdu())) {
throw new RuntimeException("应聘人员学历不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员学历不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandId())) {
throw new RuntimeException("证件号码不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "证件号码不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandSchool())) {
throw new RuntimeException("应聘人员毕业院校不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业院校不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandMajor())) {
throw new RuntimeException("应聘人员专业不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员专业不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandGrad())) {
throw new RuntimeException("应聘人员毕业年月不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业年月不能为空");
}
if (StringUtils.isEmpty(addDTO.getAdmitStatus())) {
throw new RuntimeException("录用情况不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "录用情况不能为空");
}
if (StringUtils.isEmpty(addDTO.getRecruitType())) {
throw new RuntimeException("招聘类型不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘类型不能为空");
}
// 验证证件号码格式
String idCardError = IdCardUtil.getErrorMessage(addDTO.getCandId());
if (idCardError != null) {
throw new RuntimeException("证件号码" + idCardError);
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "证件号码" + idCardError);
}
// 验证毕业年月格式(YYYYMM)
if (!addDTO.getCandGrad().matches("^((19|20)\\d{2})(0[1-9]|1[0-2])$")) {
throw new RuntimeException("毕业年月格式不正确,应为YYYYMM");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "毕业年月格式不正确,应为YYYYMM");
}
// 验证录用状态
if (AdmitStatus.getDescByCode(addDTO.getAdmitStatus()) == null) {
throw new RuntimeException("录用情况只能填写'录用'、'未录用'或'放弃'");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "录用情况只能填写'录用'、'未录用'或'放弃'");
}
if (RecruitType.getDescByCode(addDTO.getRecruitType()) == null) {
throw new RuntimeException("招聘类型只能填写'SOCIAL'或'CAMPUS'");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘类型只能填写'SOCIAL'或'CAMPUS'");
}
}
private Map<String, CcdiStaffRecruitment> getRecruitmentMap(List<CcdiStaffRecruitmentWorkExcel> excelList) {
List<String> recruitIds = excelList.stream()
.map(CcdiStaffRecruitmentWorkExcel::getRecruitId)
.map(this::trim)
.filter(StringUtils::isNotEmpty)
.distinct()
.toList();
if (recruitIds.isEmpty()) {
return Collections.emptyMap();
private void validateWorkGroup(List<WorkImportRow> workRows, CcdiStaffRecruitment recruitment) {
Set<Integer> processedSortOrders = new HashSet<>();
for (WorkImportRow workRow : workRows) {
validateRecruitmentWorkData(workRow.data(), recruitment, processedSortOrders, workRow.sheetRowNum());
}
List<CcdiStaffRecruitment> recruitments = recruitmentMapper.selectBatchIds(recruitIds);
return recruitments.stream()
.collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item));
}
private void validateRecruitmentWorkData(CcdiStaffRecruitmentWorkExcel excel,
CcdiStaffRecruitment recruitment,
Set<String> processedRecruitSortKeys) {
Set<Integer> processedSortOrders,
int sheetRowNum) {
if (StringUtils.isEmpty(trim(excel.getRecruitId()))) {
throw new RuntimeException("招聘记录编号不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不能为空");
}
if (StringUtils.isEmpty(trim(excel.getCandName()))) {
throw new RuntimeException("候选人姓名不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "候选人姓名不能为空");
}
if (StringUtils.isEmpty(trim(excel.getRecruitName()))) {
throw new RuntimeException("招聘项目名称不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘项目名称不能为空");
}
if (StringUtils.isEmpty(trim(excel.getPosName()))) {
throw new RuntimeException("职位名称不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "职位名称不能为空");
}
if (excel.getSortOrder() == null || excel.getSortOrder() <= 0) {
throw new RuntimeException("排序号不能为空且必须大于0");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "排序号不能为空且必须大于0");
}
if (!processedSortOrders.add(excel.getSortOrder())) {
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "同一招聘记录编号下排序号重复");
}
if (StringUtils.isEmpty(trim(excel.getCompanyName()))) {
throw new RuntimeException("工作单位不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "工作单位不能为空");
}
if (StringUtils.isEmpty(trim(excel.getPositionName()))) {
throw new RuntimeException("岗位不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "岗位不能为空");
}
if (StringUtils.isEmpty(trim(excel.getJobStartMonth()))) {
throw new RuntimeException("入职年月不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "入职年月不能为空");
}
validateMonth(excel.getJobStartMonth(), "入职年月");
validateMonth(excel.getJobStartMonth(), "入职年月", sheetRowNum);
if (StringUtils.isNotEmpty(trim(excel.getJobEndMonth()))) {
validateMonth(excel.getJobEndMonth(), "离职年月");
validateMonth(excel.getJobEndMonth(), "离职年月", sheetRowNum);
}
if (recruitment == null) {
throw new RuntimeException("招聘记录编号不存在,请先维护招聘主信息");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不存在,请先维护招聘主信息");
}
if (!"SOCIAL".equals(recruitment.getRecruitType())) {
throw new RuntimeException("该招聘记录不是社招,不允许导入历史工作经历");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "该招聘记录不是社招,不允许导入历史工作经历");
}
if (!sameText(excel.getCandName(), recruitment.getCandName())) {
throw new RuntimeException("招聘记录编号与候选人姓名不匹配");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与候选人姓名不匹配");
}
if (!sameText(excel.getRecruitName(), recruitment.getRecruitName())) {
throw new RuntimeException("招聘记录编号与招聘项目名称不匹配");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与招聘项目名称不匹配");
}
if (!sameText(excel.getPosName(), recruitment.getPosName())) {
throw new RuntimeException("招聘记录编号与职位名称不匹配");
}
String duplicateKey = trim(excel.getRecruitId()) + "#" + excel.getSortOrder();
if (!processedRecruitSortKeys.add(duplicateKey)) {
throw new RuntimeException("同一招聘记录编号下排序号重复");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与职位名称不匹配");
}
}
private void validateMonth(String value, String fieldName) {
private void validateMonth(String value, String fieldName, int sheetRowNum) {
String month = trim(value);
if (!month.matches("^((19|20)\\d{2})-(0[1-9]|1[0-2])$")) {
throw new RuntimeException(fieldName + "格式不正确应为YYYY-MM");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), fieldName + "格式不正确应为YYYY-MM");
}
}
@@ -441,32 +492,50 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
return value == null ? null : value.trim();
}
private RecruitmentImportFailureVO buildWorkFailure(CcdiStaffRecruitmentWorkExcel excel, String errorMessage) {
private void saveFailures(String taskId, List<RecruitmentImportFailureVO> failures) {
try {
String failuresKey = "import:recruitment:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
} catch (Exception exception) {
ImportLogUtils.logRedisError(log, taskId, "保存失败记录", exception);
}
}
private RecruitmentImportFailureVO buildFailure(CcdiStaffRecruitmentExcel excel,
String sheetName,
String sheetRowNum,
String errorMessage) {
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName(sheetName);
failure.setSheetRowNum(sheetRowNum);
failure.setErrorMessage(errorMessage);
return failure;
}
private void appendSkippedFailures(List<CcdiStaffRecruitmentWork> validRecords,
Set<String> failedRecruitIds,
List<RecruitmentImportFailureVO> failures) {
Set<String> appendedRecruitIds = new HashSet<>();
for (CcdiStaffRecruitmentWork work : validRecords) {
if (failedRecruitIds.contains(work.getRecruitId()) && appendedRecruitIds.add(work.getRecruitId())) {
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
failure.setRecruitId(work.getRecruitId());
failure.setCompanyName(work.getCompanyName());
failure.setPositionName(work.getPositionName());
failure.setErrorMessage("同一招聘记录编号存在失败行,已跳过该编号下全部工作经历,避免覆盖旧数据");
failures.add(failure);
}
}
private RecruitmentImportFailureVO buildFailure(CcdiStaffRecruitmentWorkExcel excel,
String sheetName,
String sheetRowNum,
String errorMessage) {
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName(sheetName);
failure.setSheetRowNum(sheetRowNum);
failure.setErrorMessage(errorMessage);
return failure;
}
private String resolveFinalStatus(ImportResult result) {
if (result.getFailureCount() == 0) {
return "SUCCESS";
}
if (result.getSuccessCount() == 0) {
return "FAILED";
}
return "PARTIAL_SUCCESS";
}
/**
* 更新导入状态
*/
private void updateImportStatus(String taskId, String status, ImportResult result) {
String key = "import:recruitment:" + taskId;
Map<String, Object> statusData = new HashMap<>();
@@ -486,35 +555,100 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
redisTemplate.opsForHash().putAll(key, statusData);
}
/**
* 批量保存
*/
private void saveBatch(List<CcdiStaffRecruitment> list, int batchSize) {
// 使用真正的批量插入,分批次执行以提高性能
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiStaffRecruitment> subList = list.subList(i, end);
// 过滤掉已存在的记录,防止主键冲突
List<String> recruitIds = subList.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toList());
.map(CcdiStaffRecruitment::getRecruitId)
.toList();
if (recruitIds.isEmpty()) {
continue;
}
if (!recruitIds.isEmpty()) {
List<CcdiStaffRecruitment> existingRecords = recruitmentMapper.selectBatchIds(recruitIds);
Set<String> existingIds = existingRecords.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
List<CcdiStaffRecruitment> existingRecords = recruitmentMapper.selectBatchIds(recruitIds);
Set<String> existingIds = existingRecords.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
// 只插入不存在的记录
List<CcdiStaffRecruitment> toInsert = subList.stream()
.filter(r -> !existingIds.contains(r.getRecruitId()))
.collect(Collectors.toList());
if (!toInsert.isEmpty()) {
recruitmentMapper.insertBatch(toInsert);
}
List<CcdiStaffRecruitment> toInsert = subList.stream()
.filter(record -> !existingIds.contains(record.getRecruitId()))
.toList();
if (!toInsert.isEmpty()) {
recruitmentMapper.insertBatch(toInsert);
}
}
}
private List<MainImportRow> buildMainImportRows(List<CcdiStaffRecruitmentExcel> recruitmentList) {
List<MainImportRow> rows = new ArrayList<>();
for (int i = 0; i < recruitmentList.size(); i++) {
rows.add(new MainImportRow(recruitmentList.get(i), i + EXCEL_DATA_START_ROW));
}
return rows;
}
private List<WorkImportRow> buildWorkImportRows(List<CcdiStaffRecruitmentWorkExcel> workList) {
List<WorkImportRow> rows = new ArrayList<>();
for (int i = 0; i < workList.size(); i++) {
rows.add(new WorkImportRow(workList.get(i), i + EXCEL_DATA_START_ROW));
}
return rows;
}
private List<Integer> extractWorkRowNums(List<WorkImportRow> rows) {
return rows.stream().map(WorkImportRow::sheetRowNum).toList();
}
private FailureMeta resolveFailureMeta(Exception exception, List<Integer> rowNums, String defaultSheetName) {
if (exception instanceof ImportValidationException validationException) {
return new FailureMeta(validationException.getSheetName(), validationException.getSheetRowNum());
}
return new FailureMeta(defaultSheetName, formatSheetRowNum(rowNums));
}
private ImportValidationException buildValidationException(String sheetName, List<Integer> rowNums, String message) {
return new ImportValidationException(sheetName, formatSheetRowNum(rowNums), message);
}
private String formatSheetRowNum(List<Integer> rowNums) {
if (rowNums == null || rowNums.isEmpty()) {
return "";
}
return rowNums.stream()
.filter(Objects::nonNull)
.distinct()
.sorted()
.map(String::valueOf)
.collect(Collectors.joining(""));
}
private record MainImportRow(CcdiStaffRecruitmentExcel data, int sheetRowNum) {}
private record WorkImportRow(CcdiStaffRecruitmentWorkExcel data, int sheetRowNum) {}
private record MainImportResult(Map<String, CcdiStaffRecruitment> importedRecruitmentMap, int successCount) {}
private record FailureMeta(String sheetName, String sheetRowNum) {}
private static class ImportValidationException extends RuntimeException {
private final String sheetName;
private final String sheetRowNum;
private ImportValidationException(String sheetName, String sheetRowNum, String message) {
super(message);
this.sheetName = sheetName;
this.sheetRowNum = sheetRowNum;
}
public String getSheetName() {
return sheetName;
}
public String getSheetRowNum() {
return sheetRowNum;
}
}
}

View File

@@ -7,11 +7,13 @@ import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork;
import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentQueryDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentWorkEditDTO;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffRecruitmentVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffRecruitmentWorkVO;
import com.ruoyi.info.collection.enums.AdmitStatus;
import com.ruoyi.info.collection.enums.RecruitType;
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper;
import com.ruoyi.info.collection.service.ICcdiStaffRecruitmentImportService;
@@ -28,6 +30,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -151,6 +154,7 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
BeanUtils.copyProperties(editDTO, recruitment);
int result = recruitmentMapper.updateById(recruitment);
replaceWorkExperienceList(editDTO);
return result;
}
@@ -178,24 +182,26 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
*/
@Override
@Transactional
public String importRecruitment(java.util.List<CcdiStaffRecruitmentExcel> excelList) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
public String importRecruitment(List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList) {
recruitmentList = recruitmentList == null ? List.of() : recruitmentList;
workList = workList == null ? List.of() : workList;
boolean noRecruitmentRows = StringUtils.isNull(recruitmentList) || recruitmentList.isEmpty();
boolean noWorkRows = StringUtils.isNull(workList) || workList.isEmpty();
if (noRecruitmentRows && noWorkRows) {
throw new RuntimeException("至少需要一条数据");
}
// 生成任务ID
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 获取当前用户名
String userName = SecurityUtils.getUsername();
int totalCount = recruitmentList.size() + workList.size();
// 初始化Redis状态
String statusKey = "import:recruitment:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("totalCount", totalCount);
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
@@ -205,44 +211,7 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
// 调用异步导入服务
recruitmentImportService.importRecruitmentAsync(excelList, taskId, userName);
return taskId;
}
/**
* 导入招聘记录历史工作经历数据(异步)
*
* @param excelList Excel实体列表
* @return 任务ID
*/
@Override
@Transactional
public String importRecruitmentWork(List<CcdiStaffRecruitmentWorkExcel> excelList) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
throw new RuntimeException("至少需要一条数据");
}
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
String userName = SecurityUtils.getUsername();
String statusKey = "import:recruitment:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
statusData.put("startTime", startTime);
statusData.put("message", "正在处理历史工作经历...");
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
recruitmentImportService.importRecruitmentWorkAsync(excelList, taskId, userName);
recruitmentImportService.importRecruitmentAsync(recruitmentList, workList, taskId, userName);
return taskId;
}
@@ -262,4 +231,43 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
return vo;
}).toList();
}
private void replaceWorkExperienceList(CcdiStaffRecruitmentEditDTO editDTO) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, editDTO.getRecruitId());
if (!Objects.equals(RecruitType.SOCIAL.getCode(), editDTO.getRecruitType())) {
recruitmentWorkMapper.delete(deleteWrapper);
return;
}
if (editDTO.getWorkExperienceList() == null) {
return;
}
recruitmentWorkMapper.delete(deleteWrapper);
List<CcdiStaffRecruitmentWork> workList = buildWorkExperienceEntities(editDTO);
workList.forEach(recruitmentWorkMapper::insert);
}
private List<CcdiStaffRecruitmentWork> buildWorkExperienceEntities(CcdiStaffRecruitmentEditDTO editDTO) {
List<CcdiStaffRecruitmentWorkEditDTO> workExperienceList = editDTO.getWorkExperienceList();
if (workExperienceList == null || workExperienceList.isEmpty()) {
return new ArrayList<>();
}
List<CcdiStaffRecruitmentWork> entityList = new ArrayList<>();
for (int i = 0; i < workExperienceList.size(); i++) {
CcdiStaffRecruitmentWorkEditDTO item = workExperienceList.get(i);
if (item == null || StringUtils.isBlank(item.getCompanyName()) || StringUtils.isBlank(item.getJobStartMonth())) {
continue;
}
CcdiStaffRecruitmentWork work = new CcdiStaffRecruitmentWork();
BeanUtils.copyProperties(item, work);
work.setRecruitId(editDTO.getRecruitId());
work.setSortOrder(i + 1);
entityList.add(work);
}
return entityList;
}
}

View File

@@ -1,6 +1,8 @@
package com.ruoyi.info.collection.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ruoyi.info.collection.handler.DictDropdownWriteHandler;
@@ -98,6 +100,23 @@ public class EasyExcelUtil {
}
}
/**
* 导入Excel按指定Sheet名称
*
* @param inputStream 输入流
* @param clazz 实体类
* @param sheetName 工作表名称
* @param <T> 泛型
* @return 数据列表
*/
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz, String sheetName) {
try {
return EasyExcel.read(inputStream).head(clazz).sheet(sheetName).doReadSync();
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
}
/**
* 下载导入模板
*
@@ -210,6 +229,45 @@ public class EasyExcelUtil {
}
}
/**
* 下载双Sheet导入模板带字典下拉框
*
* @param response 响应对象
* @param firstClazz 第一张Sheet实体类
* @param firstSheetName 第一张Sheet名称
* @param secondClazz 第二张Sheet实体类
* @param secondSheetName 第二张Sheet名称
* @param fileName 文件名称
* @param <T1> 第一张Sheet泛型
* @param <T2> 第二张Sheet泛型
*/
public static <T1, T2> void importTemplateWithDictDropdown(
HttpServletResponse response,
Class<T1> firstClazz,
String firstSheetName,
Class<T2> secondClazz,
String secondSheetName,
String fileName
) {
setResponseHeader(response, fileName);
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream()).build()) {
writer.write(List.of(), buildTemplateSheet(0, firstClazz, firstSheetName));
writer.write(List.of(), buildTemplateSheet(1, secondClazz, secondSheetName));
} catch (IOException e) {
throw new RuntimeException("下载双Sheet导入模板失败", e);
}
}
private static <T> WriteSheet buildTemplateSheet(int sheetNo, Class<T> clazz, String sheetName) {
return EasyExcel.writerSheet(sheetNo, sheetName)
.head(clazz)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
.registerWriteHandler(new TextFormatWriteHandler(clazz))
.registerWriteHandler(new RequiredFieldWriteHandler(clazz))
.build();
}
/**
* 导出Excel带字典下拉框
* 导出的数据包含实际值,但模板中有下拉框供后续编辑使用

View File

@@ -23,6 +23,7 @@
<result property="contactPhone" column="contact_phone"/>
<result property="supplierUscc" column="supplier_uscc"/>
<result property="supplierBankAccount" column="supplier_bank_account"/>
<result property="supplierCount" column="supplier_count"/>
<result property="applyDate" column="apply_date"/>
<result property="planApproveDate" column="plan_approve_date"/>
<result property="announceDate" column="announce_date"/>
@@ -47,49 +48,61 @@
<!-- 分页查询采购交易列表 -->
<select id="selectTransactionPage" resultMap="CcdiPurchaseTransactionVOResult">
SELECT
purchase_id, purchase_category, project_name, subject_name, subject_desc,
purchase_qty, budget_amount, bid_amount, actual_amount, contract_amount, settlement_amount,
purchase_method, supplier_name, contact_person, contact_phone, supplier_uscc, supplier_bank_account,
apply_date, plan_approve_date, announce_date, bid_open_date, contract_sign_date,
expected_delivery_date, actual_delivery_date, acceptance_date, settlement_date,
applicant_id, applicant_name, apply_department, purchase_leader_id, purchase_leader_name, purchase_department,
created_by, create_time, updated_by, update_time
FROM ccdi_purchase_transaction
t.purchase_id, t.purchase_category, t.project_name, t.subject_name, t.subject_desc,
t.purchase_qty, t.budget_amount, t.bid_amount, t.actual_amount, t.contract_amount, t.settlement_amount,
t.purchase_method, t.supplier_name, t.contact_person, t.contact_phone, t.supplier_uscc, t.supplier_bank_account,
IFNULL(supplier_stats.supplier_count, 0) AS supplier_count,
t.apply_date, t.plan_approve_date, t.announce_date, t.bid_open_date, t.contract_sign_date,
t.expected_delivery_date, t.actual_delivery_date, t.acceptance_date, t.settlement_date,
t.applicant_id, t.applicant_name, t.apply_department, t.purchase_leader_id, t.purchase_leader_name, t.purchase_department,
t.created_by, t.create_time, t.updated_by, t.update_time
FROM ccdi_purchase_transaction t
LEFT JOIN (
SELECT purchase_id, COUNT(*) AS supplier_count
FROM ccdi_purchase_transaction_supplier
GROUP BY purchase_id
) supplier_stats ON supplier_stats.purchase_id = t.purchase_id
<where>
<if test="query.projectName != null and query.projectName != ''">
AND project_name LIKE CONCAT('%', #{query.projectName}, '%')
AND t.project_name LIKE CONCAT('%', #{query.projectName}, '%')
</if>
<if test="query.subjectName != null and query.subjectName != ''">
AND subject_name LIKE CONCAT('%', #{query.subjectName}, '%')
AND t.subject_name LIKE CONCAT('%', #{query.subjectName}, '%')
</if>
<if test="query.applicantName != null and query.applicantName != ''">
AND applicant_name LIKE CONCAT('%', #{query.applicantName}, '%')
AND t.applicant_name LIKE CONCAT('%', #{query.applicantName}, '%')
</if>
<if test="query.applicantId != null and query.applicantId != ''">
AND applicant_id = #{query.applicantId}
AND t.applicant_id = #{query.applicantId}
</if>
<if test="query.applyDateStart != null">
AND apply_date &gt;= #{query.applyDateStart}
AND t.apply_date &gt;= #{query.applyDateStart}
</if>
<if test="query.applyDateEnd != null">
AND apply_date &lt;= #{query.applyDateEnd}
AND t.apply_date &lt;= #{query.applyDateEnd}
</if>
</where>
ORDER BY create_time DESC
ORDER BY t.create_time DESC
</select>
<!-- 查询采购交易详情 -->
<select id="selectTransactionById" resultMap="CcdiPurchaseTransactionVOResult">
SELECT
purchase_id, purchase_category, project_name, subject_name, subject_desc,
purchase_qty, budget_amount, bid_amount, actual_amount, contract_amount, settlement_amount,
purchase_method, supplier_name, contact_person, contact_phone, supplier_uscc, supplier_bank_account,
apply_date, plan_approve_date, announce_date, bid_open_date, contract_sign_date,
expected_delivery_date, actual_delivery_date, acceptance_date, settlement_date,
applicant_id, applicant_name, apply_department, purchase_leader_id, purchase_leader_name, purchase_department,
created_by, create_time, updated_by, update_time
FROM ccdi_purchase_transaction
WHERE purchase_id = #{purchaseId}
t.purchase_id, t.purchase_category, t.project_name, t.subject_name, t.subject_desc,
t.purchase_qty, t.budget_amount, t.bid_amount, t.actual_amount, t.contract_amount, t.settlement_amount,
t.purchase_method, t.supplier_name, t.contact_person, t.contact_phone, t.supplier_uscc, t.supplier_bank_account,
IFNULL(supplier_stats.supplier_count, 0) AS supplier_count,
t.apply_date, t.plan_approve_date, t.announce_date, t.bid_open_date, t.contract_sign_date,
t.expected_delivery_date, t.actual_delivery_date, t.acceptance_date, t.settlement_date,
t.applicant_id, t.applicant_name, t.apply_department, t.purchase_leader_id, t.purchase_leader_name, t.purchase_department,
t.created_by, t.create_time, t.updated_by, t.update_time
FROM ccdi_purchase_transaction t
LEFT JOIN (
SELECT purchase_id, COUNT(*) AS supplier_count
FROM ccdi_purchase_transaction_supplier
GROUP BY purchase_id
) supplier_stats ON supplier_stats.purchase_id = t.purchase_id
WHERE t.purchase_id = #{purchaseId}
</select>
<!-- 批量插入采购交易数据 -->
@@ -137,4 +150,12 @@
</foreach>
</update>
<delete id="deleteSuppliersByPurchaseIds">
DELETE FROM ccdi_purchase_transaction_supplier
WHERE purchase_id IN
<foreach collection="purchaseIds" item="purchaseId" open="(" separator="," close=")">
#{purchaseId}
</foreach>
</delete>
</mapper>

View File

@@ -8,7 +8,9 @@
<resultMap type="com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationVO" id="CcdiStaffEnterpriseRelationVOResult">
<id property="id" column="id"/>
<result property="personId" column="person_id"/>
<result property="personName" column="person_name"/>
<result property="relationName" column="relation_name"/>
<result property="staffPersonId" column="staff_person_id"/>
<result property="staffPersonName" column="staff_person_name"/>
<result property="relationPersonPost" column="relation_person_post"/>
<result property="socialCreditCode" column="social_credit_code"/>
<result property="enterpriseName" column="enterprise_name"/>
@@ -28,17 +30,28 @@
<!-- 分页查询员工实体关系列表 -->
<select id="selectRelationPage" resultMap="CcdiStaffEnterpriseRelationVOResult">
SELECT
ser.id, ser.person_id, bs.name as person_name, ser.relation_person_post,
ser.id, ser.person_id, sfr.relation_name, sfr.person_id AS staff_person_id, bs.name AS staff_person_name,
ser.relation_person_post,
ser.social_credit_code, ser.enterprise_name, ser.status, ser.remark,
ser.data_source, ser.is_employee, ser.is_emp_family, ser.is_customer,
ser.is_cust_family, ser.created_by, ser.create_time, ser.updated_by,
ser.update_time
FROM ccdi_staff_enterprise_relation ser
LEFT JOIN ccdi_base_staff bs ON ser.person_id = bs.id_card
LEFT JOIN ccdi_staff_fmy_relation sfr
ON ser.person_id = sfr.relation_cert_no
AND sfr.is_emp_family = 1
LEFT JOIN ccdi_base_staff bs ON sfr.person_id = bs.id_card
<where>
<if test="query.personId != null and query.personId != ''">
AND ser.person_id LIKE CONCAT('%', #{query.personId}, '%')
</if>
<if test="query.relationName != null and query.relationName != ''">
AND sfr.relation_name LIKE CONCAT('%', #{query.relationName}, '%')
</if>
<if test="query.staffPersonName != null and query.staffPersonName != ''">
AND (sfr.person_id LIKE CONCAT('%', #{query.staffPersonName}, '%')
OR bs.name LIKE CONCAT('%', #{query.staffPersonName}, '%'))
</if>
<if test="query.socialCreditCode != null and query.socialCreditCode != ''">
AND ser.social_credit_code LIKE CONCAT('%', #{query.socialCreditCode}, '%')
</if>
@@ -55,16 +68,40 @@
<!-- 查询员工实体关系详情 -->
<select id="selectRelationById" resultMap="CcdiStaffEnterpriseRelationVOResult">
SELECT
ser.id, ser.person_id, bs.name as person_name, ser.relation_person_post,
ser.id, ser.person_id, sfr.relation_name, sfr.person_id AS staff_person_id, bs.name AS staff_person_name,
ser.relation_person_post,
ser.social_credit_code, ser.enterprise_name, ser.status, ser.remark,
ser.data_source, ser.is_employee, ser.is_emp_family, ser.is_customer,
ser.is_cust_family, ser.created_by, ser.create_time, ser.updated_by,
ser.update_time
FROM ccdi_staff_enterprise_relation ser
LEFT JOIN ccdi_base_staff bs ON ser.person_id = bs.id_card
LEFT JOIN ccdi_staff_fmy_relation sfr
ON ser.person_id = sfr.relation_cert_no
AND sfr.is_emp_family = 1
LEFT JOIN ccdi_base_staff bs ON sfr.person_id = bs.id_card
WHERE ser.id = #{id}
</select>
<!-- 查询有效员工亲属下拉选项 -->
<select id="selectFamilyOptions" resultType="com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO">
SELECT
sfr.relation_cert_no AS relationCertNo,
sfr.relation_name AS relationName,
sfr.person_id AS staffPersonId,
bs.name AS staffPersonName
FROM ccdi_staff_fmy_relation sfr
LEFT JOIN ccdi_base_staff bs ON sfr.person_id = bs.id_card
<where>
sfr.is_emp_family = 1
AND sfr.status = 1
<if test="query != null and query != ''">
AND sfr.relation_cert_no LIKE CONCAT('%', #{query}, '%')
</if>
</where>
ORDER BY sfr.create_time DESC
LIMIT 100
</select>
<!-- 判断身份证号和统一社会信用代码的组合是否已存在 -->
<select id="existsByPersonIdAndSocialCreditCode" resultType="boolean">
SELECT COUNT(1) > 0
@@ -84,6 +121,14 @@
</foreach>
</select>
<update id="invalidateByFamilyCertNo">
UPDATE ccdi_staff_enterprise_relation
SET status = 0,
update_time = NOW()
WHERE person_id = #{personId}
AND status != 0
</update>
<!-- 批量插入员工实体关系数据 -->
<insert id="insertBatch">
INSERT INTO ccdi_staff_enterprise_relation

View File

@@ -40,10 +40,10 @@
r.created_by, r.create_time, r.updated_by, r.update_time
FROM ccdi_staff_recruitment r
LEFT JOIN (
SELECT recruit_id, COUNT(1) AS work_experience_count
SELECT recruit_id COLLATE utf8mb4_general_ci AS recruit_id, COUNT(1) AS work_experience_count
FROM ccdi_staff_recruitment_work
GROUP BY recruit_id
) w ON w.recruit_id = r.recruit_id
GROUP BY recruit_id COLLATE utf8mb4_general_ci
) w ON w.recruit_id COLLATE utf8mb4_general_ci = r.recruit_id COLLATE utf8mb4_general_ci
<where>
<if test="query.recruitName != null and query.recruitName != ''">
AND r.recruit_name LIKE CONCAT('%', #{query.recruitName}, '%')

View File

@@ -96,8 +96,12 @@ class CcdiAssetInfoControllerTest {
@Test
void getImportFailures_shouldReturnPagedRows() {
AssetImportFailureVO failure1 = new AssetImportFailureVO();
failure1.setSheetName("亲属资产信息");
failure1.setRowNum(2);
failure1.setPersonId("A1");
AssetImportFailureVO failure2 = new AssetImportFailureVO();
failure2.setSheetName("亲属资产信息");
failure2.setRowNum(3);
failure2.setPersonId("A2");
when(assetInfoImportService.getImportFailures("task-3")).thenReturn(List.of(failure1, failure2));
@@ -105,7 +109,10 @@ class CcdiAssetInfoControllerTest {
assertEquals(2, result.getTotal());
assertEquals(1, result.getRows().size());
assertEquals("A2", ((AssetImportFailureVO) result.getRows().get(0)).getPersonId());
AssetImportFailureVO row = (AssetImportFailureVO) result.getRows().get(0);
assertEquals("亲属资产信息", row.getSheetName());
assertEquals(3, row.getRowNum());
assertEquals("A2", row.getPersonId());
}
@Test

View File

@@ -0,0 +1,148 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.pagehelper.parser.defaults.DefaultCountSqlParser;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationQueryDTO;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiStaffEnterpriseRelationMapperTest {
private static final String RESOURCE = "mapper/info/collection/CcdiStaffEnterpriseRelationMapper.xml";
@Test
void selectRelationPage_shouldJoinFamilyRelationAndStaff() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper.selectRelationPage");
CcdiStaffEnterpriseRelationQueryDTO queryDTO = new CcdiStaffEnterpriseRelationQueryDTO();
queryDTO.setRelationName("");
queryDTO.setStaffPersonName("");
String sql = renderSql(mappedStatement, Map.of(
"page", new Page<>(1, 10),
"query", queryDTO
));
String countSql = normalizeSql(new DefaultCountSqlParser().getSmartCountSql(sql, "0"));
assertTrue(sql.contains("LEFT JOIN ccdi_staff_fmy_relation sfr"), sql);
assertTrue(sql.contains("LEFT JOIN ccdi_base_staff bs ON sfr.person_id = bs.id_card"), sql);
assertTrue(sql.contains("sfr.relation_name"), sql);
assertTrue(sql.contains("staff_person_id"), sql);
assertTrue(sql.contains("bs.name AS staff_person_name"), sql);
assertTrue(sql.contains("sfr.relation_name LIKE CONCAT('%', ?, '%')"), sql);
assertTrue(sql.contains("bs.name LIKE CONCAT('%', ?, '%')"), sql);
assertFalse(countSql.contains("1AND"), countSql);
}
@Test
void selectFamilyOptions_shouldOnlyQueryEffectiveEmployeeFamilies() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper.selectFamilyOptions");
String sql = renderSql(mappedStatement, Map.of("query", "320101"));
assertTrue(sql.contains("sfr.is_emp_family = 1"), sql);
assertTrue(sql.contains("sfr.status = 1"), sql);
assertTrue(sql.contains("sfr.relation_cert_no LIKE CONCAT('%', ?, '%')"), sql);
}
@Test
void mapperXml_shouldContainInvalidateByFamilyCertNo() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("<update id=\"invalidateByFamilyCertNo\">"), xml);
assertTrue(xml.contains("WHERE person_id = #{personId}"), xml);
assertTrue(xml.contains("SET status = 0"), xml);
}
}
private MappedStatement loadMappedStatement(String statementId) throws Exception {
Configuration configuration = new Configuration();
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
registerTypeAliases(configuration.getTypeAliasRegistry());
configuration.getLanguageRegistry().register(XMLLanguageDriver.class);
configuration.addMapper(CcdiStaffEnterpriseRelationMapper.class);
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
XMLMapperBuilder xmlMapperBuilder =
new XMLMapperBuilder(inputStream, configuration, RESOURCE, configuration.getSqlFragments());
xmlMapperBuilder.parse();
}
return configuration.getMappedStatement(statementId);
}
private String renderSql(MappedStatement mappedStatement, Map<String, Object> params) {
BoundSql boundSql = mappedStatement.getBoundSql(new HashMap<>(params));
return normalizeSql(boundSql.getSql());
}
private String normalizeSql(String sql) {
return sql.replaceAll("\\s+", " ").trim();
}
private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) {
typeAliasRegistry.registerAlias("map", Map.class);
}
private static class NoOpDataSource implements DataSource {
@Override
public java.sql.Connection getConnection() {
throw new UnsupportedOperationException("Not required for SQL rendering tests");
}
@Override
public java.sql.Connection getConnection(String username, String password) {
throw new UnsupportedOperationException("Not required for SQL rendering tests");
}
@Override
public java.io.PrintWriter getLogWriter() {
return null;
}
@Override
public void setLogWriter(java.io.PrintWriter out) {
}
@Override
public void setLoginTimeout(int seconds) {
}
@Override
public int getLoginTimeout() {
return 0;
}
@Override
public java.util.logging.Logger getParentLogger() {
return java.util.logging.Logger.getGlobal();
}
@Override
public <T> T unwrap(Class<T> iface) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public boolean isWrapperFor(Class<?> iface) {
return false;
}
}
}

View File

@@ -93,10 +93,39 @@ class CcdiBaseStaffAssetImportServiceImplTest {
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaffAsset:task-2:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
BaseStaffAssetImportFailureVO failure = (BaseStaffAssetImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工资产信息", failure.getSheetName());
assertEquals(2, failure.getRowNum());
assertEquals("320101199201010022", failure.getPersonId());
assertTrue(failure.getErrorMessage().contains("员工资产导入仅支持员工本人证件号"));
}
@Test
void importAssetInfoAsync_shouldFailWhenAssetAlreadyExists() {
CcdiBaseStaffAssetInfoExcel excel = buildExcel("320101199001010011", "房产");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(List.of("320101199001010011")))
.thenReturn(List.of(owner("320101199001010011", "320101199001010011")));
CcdiAssetInfo existing = new CcdiAssetInfo();
existing.setFamilyId("320101199001010011");
existing.setPersonId("320101199001010011");
existing.setAssetMainType("房产");
existing.setAssetSubType("房产小类");
existing.setAssetName("房产名称");
when(assetInfoMapper.selectList(any())).thenReturn(List.of(existing));
service.importAssetInfoAsync(List.of(excel), "task-duplicate", "tester");
verify(assetInfoMapper, never()).insertBatch(any());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaffAsset:task-duplicate:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
BaseStaffAssetImportFailureVO failure = (BaseStaffAssetImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工资产信息", failure.getSheetName());
assertEquals(2, failure.getRowNum());
assertTrue(failure.getErrorMessage().contains("资产记录已存在"));
}
@Test
void getImportStatusAndFailures_shouldUseBaseStaffAssetPrefixes() {
when(redisTemplate.opsForHash()).thenReturn(hashOperations);

View File

@@ -1,8 +1,14 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl;
import com.ruoyi.system.mapper.SysDeptMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Collections;
@@ -11,60 +17,109 @@ import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffImportServiceImplTest {
private final CcdiBaseStaffImportServiceImpl service = new CcdiBaseStaffImportServiceImpl();
@InjectMocks
private CcdiBaseStaffImportServiceImpl service;
@Mock
private SysDeptMapper deptMapper;
@Test
void validateStaffData_shouldAllowEmptyAnnualIncome() {
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), false, Collections.emptySet(), Collections.emptySet()));
mockNormalDept();
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldAllowZeroAndTwoDecimalAnnualIncome() {
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), false, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), false, Collections.emptySet(), Collections.emptySet()));
mockNormalDept();
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldAllowPartyMemberValuesZeroAndOne() {
mockNormalDept();
CcdiBaseStaffAddDTO nonPartyMember = buildDto(null);
nonPartyMember.setPartyMember(0);
CcdiBaseStaffAddDTO partyMember = buildDto(null);
partyMember.setPartyMember(1);
assertDoesNotThrow(() -> service.validateStaffData(nonPartyMember, false, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(partyMember, false, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(nonPartyMember, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(partyMember, Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldRejectInvalidPartyMemberValue() {
mockNormalDept();
CcdiBaseStaffAddDTO dto = buildDto(null);
dto.setPartyMember(2);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(dto, false, Set.of(), Set.of()));
() -> service.validateStaffData(dto, Set.of(), Set.of()));
assertEquals("是否党员只能填写'0'或'1'", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectNegativeAnnualIncome() {
mockNormalDept();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), false, Set.of(), Set.of()));
() -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), Set.of(), Set.of()));
assertEquals("年收入不能为负数", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectAnnualIncomeWithMoreThanTwoDecimals() {
mockNormalDept();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(new BigDecimal("12.345")), false, Set.of(), Set.of()));
() -> service.validateStaffData(buildDto(new BigDecimal("12.345")), Set.of(), Set.of()));
assertEquals("年收入最多保留2位小数", exception.getMessage());
}
@Test
void validateStaffData_shouldAllowWhenDeptIsNormalAndNotDeleted() {
mockNormalDept();
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
}
@Test
void validateStaffData_shouldRejectWhenDeptDoesNotExist() {
when(deptMapper.selectDeptById(10L)).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectWhenDeptIsDisabled() {
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("1", "0"));
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectWhenDeptIsDeleted() {
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("0", "2"));
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage());
}
private CcdiBaseStaffAddDTO buildDto(BigDecimal annualIncome) {
CcdiBaseStaffAddDTO dto = new CcdiBaseStaffAddDTO();
dto.setName("张三");
@@ -77,4 +132,17 @@ class CcdiBaseStaffImportServiceImplTest {
dto.setAnnualIncome(annualIncome);
return dto;
}
private SysDept buildDept(String status, String delFlag) {
SysDept dept = new SysDept();
dept.setDeptId(10L);
dept.setDeptName("测试部门");
dept.setStatus(status);
dept.setDelFlag(delFlag);
return dept;
}
private void mockNormalDept() {
lenient().when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("0", "0"));
}
}

View File

@@ -0,0 +1,77 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.service.impl.CcdiEnterpriseBaseInfoImportServiceImpl;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CcdiEnterpriseBaseInfoImportServiceImplTest {
private final CcdiEnterpriseBaseInfoImportServiceImpl service = new CcdiEnterpriseBaseInfoImportServiceImpl();
@Test
void validateAndBuildEntity_shouldRejectWhenDatabaseAlreadyContainsCreditCode() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateAndBuildEntity(excel, Set.of("91310000123456789A"), new HashSet<>(), "admin"));
assertEquals("统一社会信用代码[91310000123456789A]已存在,请勿重复导入", exception.getMessage());
}
@Test
void validateAndBuildEntity_shouldRejectWhenExcelContainsDuplicateCreditCode() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateAndBuildEntity(excel, Set.of(), new HashSet<>(Set.of("91310000123456789A")), "admin"));
assertEquals("统一社会信用代码[91310000123456789A]在导入文件中重复,已跳过此条记录", exception.getMessage());
}
@Test
void validateAndBuildEntity_shouldNormalizeEnumTextToCode() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
excel.setRiskLevel("高风险");
excel.setEntSource("一般企业");
CcdiEnterpriseBaseInfo entity = service.validateAndBuildEntity(excel, Set.of(), new HashSet<>(), "admin");
assertEquals("1", entity.getRiskLevel());
assertEquals("GENERAL", entity.getEntSource());
assertEquals("IMPORT", entity.getDataSource());
assertEquals("admin", entity.getCreatedBy());
}
@Test
void validateAndBuildEntity_shouldAllowBlankStatus() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
excel.setStatus(null);
CcdiEnterpriseBaseInfo entity = service.validateAndBuildEntity(excel, Set.of(), new HashSet<>(), "admin");
assertNull(entity.getStatus());
assertEquals("IMPORT", entity.getDataSource());
}
private CcdiEnterpriseBaseInfoExcel buildExcel() {
CcdiEnterpriseBaseInfoExcel excel = new CcdiEnterpriseBaseInfoExcel();
excel.setSocialCreditCode("91310000123456789A");
excel.setEnterpriseName("测试企业");
excel.setEnterpriseType("有限责任公司");
excel.setEnterpriseNature("民营企业");
excel.setIndustryClass("制造业");
excel.setIndustryName("电子设备");
excel.setStatus("存续");
excel.setRiskLevel("1");
excel.setEntSource("GENERAL");
return excel;
}
}

View File

@@ -0,0 +1,204 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
import com.ruoyi.info.collection.domain.vo.CcdiEnterpriseBaseInfoVO;
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.impl.CcdiEnterpriseBaseInfoServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiEnterpriseBaseInfoServiceImplTest {
@InjectMocks
private CcdiEnterpriseBaseInfoServiceImpl service;
@Mock
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
@Mock
private CcdiStaffEnterpriseRelationMapper staffEnterpriseRelationMapper;
@Mock
private CcdiCustEnterpriseRelationMapper custEnterpriseRelationMapper;
@Mock
private CcdiIntermediaryEnterpriseRelationMapper intermediaryEnterpriseRelationMapper;
@Mock
private ICcdiEnterpriseBaseInfoImportService enterpriseBaseInfoImportService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Test
void insertEnterpriseBaseInfo_shouldPersistWhenSocialCreditCodeIsUnique() {
CcdiEnterpriseBaseInfoAddDTO addDTO = buildAddDto();
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
when(enterpriseBaseInfoMapper.insert(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.insertEnterpriseBaseInfo(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
verify(enterpriseBaseInfoMapper).insert(captor.capture());
assertEquals("测试企业", captor.getValue().getEnterpriseName());
assertEquals("GENERAL", captor.getValue().getEntSource());
}
@Test
void insertEnterpriseBaseInfo_shouldSetManualDataSourceAndAllowBlankStatus() {
CcdiEnterpriseBaseInfoAddDTO addDTO = buildAddDto();
addDTO.setStatus(null);
addDTO.setDataSource("API");
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
when(enterpriseBaseInfoMapper.insert(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.insertEnterpriseBaseInfo(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
verify(enterpriseBaseInfoMapper).insert(captor.capture());
assertEquals("MANUAL", captor.getValue().getDataSource());
assertNull(captor.getValue().getStatus());
}
@Test
void insertEnterpriseBaseInfo_shouldRejectInvalidRiskLevel() {
CcdiEnterpriseBaseInfoAddDTO addDTO = buildAddDto();
addDTO.setRiskLevel("9");
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.insertEnterpriseBaseInfo(addDTO));
assertEquals("风险等级不在允许范围内", exception.getMessage());
}
@Test
void updateEnterpriseBaseInfo_shouldRejectWhenRecordMissing() {
CcdiEnterpriseBaseInfoEditDTO editDTO = buildEditDto();
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.updateEnterpriseBaseInfo(editDTO));
assertEquals("实体库记录不存在", exception.getMessage());
}
@Test
void updateEnterpriseBaseInfo_shouldKeepExistingDataSource() {
CcdiEnterpriseBaseInfoEditDTO editDTO = buildEditDto();
editDTO.setDataSource("API");
CcdiEnterpriseBaseInfo existing = new CcdiEnterpriseBaseInfo();
existing.setSocialCreditCode(editDTO.getSocialCreditCode());
existing.setDataSource("MANUAL");
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(existing);
when(enterpriseBaseInfoMapper.updateById(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.updateEnterpriseBaseInfo(editDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
verify(enterpriseBaseInfoMapper).updateById(captor.capture());
assertEquals("MANUAL", captor.getValue().getDataSource());
}
@Test
void selectEnterpriseBaseInfoById_shouldConvertEntityToVo() {
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
entity.setSocialCreditCode("91310000123456789A");
entity.setEnterpriseName("测试企业");
entity.setRiskLevel("1");
entity.setEntSource("GENERAL");
when(enterpriseBaseInfoMapper.selectById("91310000123456789A")).thenReturn(entity);
CcdiEnterpriseBaseInfoVO vo = service.selectEnterpriseBaseInfoById("91310000123456789A");
assertNotNull(vo);
assertEquals("测试企业", vo.getEnterpriseName());
assertEquals("1", vo.getRiskLevel());
}
@Test
void deleteEnterpriseBaseInfoByIds_shouldDeleteInBatch() {
when(staffEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(custEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(intermediaryEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(enterpriseBaseInfoMapper.deleteBatchIds(java.util.List.of("91310000123456789A", "91310000123456789B")))
.thenReturn(2);
int result = service.deleteEnterpriseBaseInfoByIds(new String[]{"91310000123456789A", "91310000123456789B"});
assertEquals(2, result);
verify(enterpriseBaseInfoMapper).deleteBatchIds(java.util.List.of("91310000123456789A", "91310000123456789B"));
}
@Test
void deleteEnterpriseBaseInfoByIds_shouldRejectWhenStaffRelationExists() {
when(staffEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
when(custEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(intermediaryEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.deleteEnterpriseBaseInfoByIds(new String[]{"91310000123456789A"}));
assertEquals("统一社会信用代码[91310000123456789A]已关联员工,删除失败", exception.getMessage());
}
@Test
void deleteEnterpriseBaseInfoByIds_shouldRejectWhenMultipleRelationsExist() {
when(staffEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
when(custEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
when(intermediaryEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.deleteEnterpriseBaseInfoByIds(new String[]{"91310000123456789A"}));
assertEquals("统一社会信用代码[91310000123456789A]已关联员工、信贷客户、中介,删除失败", exception.getMessage());
}
private CcdiEnterpriseBaseInfoAddDTO buildAddDto() {
CcdiEnterpriseBaseInfoAddDTO dto = new CcdiEnterpriseBaseInfoAddDTO();
dto.setSocialCreditCode("91310000123456789A");
dto.setEnterpriseName("测试企业");
dto.setEnterpriseType("有限责任公司");
dto.setEnterpriseNature("民营企业");
dto.setIndustryClass("制造业");
dto.setIndustryName("电子设备");
dto.setStatus("存续");
dto.setRiskLevel("1");
dto.setEntSource("GENERAL");
dto.setDataSource("MANUAL");
return dto;
}
private CcdiEnterpriseBaseInfoEditDTO buildEditDto() {
CcdiEnterpriseBaseInfoEditDTO dto = new CcdiEnterpriseBaseInfoEditDTO();
dto.setSocialCreditCode("91310000123456789A");
dto.setEnterpriseName("测试企业");
dto.setStatus("存续");
dto.setRiskLevel("1");
dto.setEntSource("GENERAL");
dto.setDataSource("MANUAL");
return dto;
}
}

View File

@@ -0,0 +1,138 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.service.impl.CcdiStaffEnterpriseRelationImportServiceImpl;
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CcdiStaffEnterpriseRelationImportServiceImplTest {
private final CcdiStaffEnterpriseRelationImportServiceImpl service = new CcdiStaffEnterpriseRelationImportServiceImpl();
@Test
void validateAndBuildEntity_shouldBuildImportEntityForValidFamily() throws Exception {
CcdiStaffEnterpriseRelationExcel excel = buildExcel();
CcdiStaffFmyRelation familyRelation = buildFamily();
CcdiStaffEnterpriseRelation entity = invokeValidateAndBuildEntity(
excel,
familyRelation,
Set.of(excel.getPersonId()),
Set.of(),
new HashSet<>(),
"admin"
);
assertEquals("IMPORT", entity.getDataSource());
assertEquals(1, entity.getStatus());
assertEquals(1, entity.getIsEmpFamily());
assertEquals("admin", entity.getCreatedBy());
}
@Test
void validateAndBuildEntity_shouldRejectWhenFamilyDoesNotExist() throws Exception {
CcdiStaffEnterpriseRelationExcel excel = buildExcel();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> invokeValidateAndBuildEntity(excel, null, Set.of(), Set.of(), new HashSet<>(), "admin"));
assertEquals("亲属身份证号[" + excel.getPersonId() + "]不存在,请先维护员工亲属关系", exception.getMessage());
}
@Test
void validateAndBuildEntity_shouldRejectWhenFamilyIsInvalid() throws Exception {
CcdiStaffEnterpriseRelationExcel excel = buildExcel();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> invokeValidateAndBuildEntity(excel, null, Set.of(excel.getPersonId()), Set.of(), new HashSet<>(), "admin"));
assertEquals("亲属身份证号[" + excel.getPersonId() + "]不是有效员工亲属,请先维护有效的员工亲属关系", exception.getMessage());
}
@Test
void validateAndBuildEntity_shouldRejectWhenCombinationAlreadyExistsInDatabase() throws Exception {
CcdiStaffEnterpriseRelationExcel excel = buildExcel();
CcdiStaffFmyRelation familyRelation = buildFamily();
String combination = excel.getPersonId() + "|" + excel.getSocialCreditCode();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> invokeValidateAndBuildEntity(excel, familyRelation, Set.of(excel.getPersonId()), Set.of(combination), new HashSet<>(), "admin"));
assertEquals("亲属身份证号[" + excel.getPersonId() + "]和统一社会信用代码[" + excel.getSocialCreditCode() + "]的组合已存在,请勿重复导入", exception.getMessage());
}
@Test
void validateAndBuildEntity_shouldRejectWhenCombinationAlreadyExistsInExcel() throws Exception {
CcdiStaffEnterpriseRelationExcel excel = buildExcel();
CcdiStaffFmyRelation familyRelation = buildFamily();
String combination = excel.getPersonId() + "|" + excel.getSocialCreditCode();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> invokeValidateAndBuildEntity(excel, familyRelation, Set.of(excel.getPersonId()), Set.of(), new HashSet<>(Set.of(combination)), "admin"));
assertEquals("亲属身份证号[" + excel.getPersonId() + "]和统一社会信用代码[" + excel.getSocialCreditCode() + "]的组合在导入文件中重复,已跳过此条记录", exception.getMessage());
}
private CcdiStaffEnterpriseRelation invokeValidateAndBuildEntity(CcdiStaffEnterpriseRelationExcel excel,
CcdiStaffFmyRelation familyRelation,
Set<String> knownFamilyCertNos,
Set<String> existingCombinations,
Set<String> processedCombinations,
String userName) throws Exception {
Method method = CcdiStaffEnterpriseRelationImportServiceImpl.class.getDeclaredMethod(
"validateAndBuildEntity",
CcdiStaffEnterpriseRelationExcel.class,
CcdiStaffFmyRelation.class,
Set.class,
Set.class,
Set.class,
String.class
);
method.setAccessible(true);
try {
return (CcdiStaffEnterpriseRelation) method.invoke(
service,
excel,
familyRelation,
knownFamilyCertNos,
existingCombinations,
processedCombinations,
userName
);
} catch (InvocationTargetException ex) {
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw ex;
}
}
private CcdiStaffEnterpriseRelationExcel buildExcel() {
CcdiStaffEnterpriseRelationExcel excel = new CcdiStaffEnterpriseRelationExcel();
excel.setPersonId("320101199001010022");
excel.setSocialCreditCode("91310000123456789A");
excel.setEnterpriseName("测试企业");
excel.setRelationPersonPost("董事");
return excel;
}
private CcdiStaffFmyRelation buildFamily() {
CcdiStaffFmyRelation familyRelation = new CcdiStaffFmyRelation();
familyRelation.setPersonId("320101199001010011");
familyRelation.setRelationCertNo("320101199001010022");
familyRelation.setRelationName("李四");
familyRelation.setStatus(1);
familyRelation.setIsEmpFamily(true);
return familyRelation;
}
}

View File

@@ -0,0 +1,156 @@
package com.ruoyi.info.collection.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationEditDTO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.service.impl.CcdiStaffEnterpriseRelationServiceImpl;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiStaffEnterpriseRelationServiceImplTest {
@BeforeAll
static void initTableInfo() {
registerTableInfo(CcdiStaffEnterpriseRelation.class, CcdiStaffEnterpriseRelationMapper.class.getName());
registerTableInfo(CcdiStaffFmyRelation.class, CcdiStaffFmyRelationMapper.class.getName());
}
@InjectMocks
private CcdiStaffEnterpriseRelationServiceImpl service;
@Mock
private CcdiStaffEnterpriseRelationMapper relationMapper;
@Mock
private CcdiStaffFmyRelationMapper familyRelationMapper;
@Mock
private ICcdiStaffEnterpriseRelationImportService relationImportService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Test
void insertRelation_shouldAllowValidFamily() {
CcdiStaffEnterpriseRelationAddDTO addDTO = buildAddDto();
CcdiStaffFmyRelation familyRelation = new CcdiStaffFmyRelation();
familyRelation.setRelationCertNo(addDTO.getPersonId());
familyRelation.setRelationName("李四");
familyRelation.setPersonId("320101199001010011");
when(familyRelationMapper.selectOne(any())).thenReturn(familyRelation);
when(relationMapper.existsByPersonIdAndSocialCreditCode(addDTO.getPersonId(), addDTO.getSocialCreditCode())).thenReturn(false);
when(relationMapper.insert(any(CcdiStaffEnterpriseRelation.class))).thenReturn(1);
int result = service.insertRelation(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiStaffEnterpriseRelation> captor = ArgumentCaptor.forClass(CcdiStaffEnterpriseRelation.class);
verify(relationMapper).insert(captor.capture());
assertEquals(1, captor.getValue().getStatus());
assertEquals("MANUAL", captor.getValue().getDataSource());
assertEquals(1, captor.getValue().getIsEmpFamily());
}
@Test
void insertRelation_shouldRejectInvalidFamily() {
CcdiStaffEnterpriseRelationAddDTO addDTO = buildAddDto();
when(familyRelationMapper.selectOne(any()))
.thenReturn(null)
.thenReturn(new CcdiStaffFmyRelation());
RuntimeException exception = assertThrows(RuntimeException.class, () -> service.insertRelation(addDTO));
assertEquals("亲属身份证号[" + addDTO.getPersonId() + "]不是有效员工亲属,请先维护有效的员工亲属关系", exception.getMessage());
}
@Test
void insertRelation_shouldRejectMissingFamily() {
CcdiStaffEnterpriseRelationAddDTO addDTO = buildAddDto();
when(familyRelationMapper.selectOne(any())).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class, () -> service.insertRelation(addDTO));
assertEquals("亲属身份证号[" + addDTO.getPersonId() + "]不存在,请先维护员工亲属关系", exception.getMessage());
}
@Test
void updateRelation_shouldExecuteEditFlow() {
CcdiStaffEnterpriseRelationEditDTO editDTO = new CcdiStaffEnterpriseRelationEditDTO();
editDTO.setId(1L);
editDTO.setPersonId("320101199001010022");
editDTO.setSocialCreditCode("91310000123456789A");
editDTO.setEnterpriseName("测试企业");
editDTO.setRelationPersonPost("董事");
editDTO.setStatus(0);
editDTO.setRemark("测试备注");
when(relationMapper.update(isNull(), any(LambdaUpdateWrapper.class))).thenReturn(1);
int result = service.updateRelation(editDTO);
assertEquals(1, result);
verify(relationMapper).update(isNull(), any(LambdaUpdateWrapper.class));
}
@Test
void selectFamilyOptions_shouldDelegateToMapper() {
CcdiStaffEnterpriseRelationOptionVO option = new CcdiStaffEnterpriseRelationOptionVO();
option.setRelationCertNo("320101199001010022");
option.setRelationName("李四");
option.setStaffPersonId("320101199001010011");
option.setStaffPersonName("张三");
List<CcdiStaffEnterpriseRelationOptionVO> expected = List.of(option);
when(relationMapper.selectFamilyOptions("320101")).thenReturn(expected);
List<CcdiStaffEnterpriseRelationOptionVO> result = service.selectFamilyOptions("320101");
assertSame(expected, result);
}
private CcdiStaffEnterpriseRelationAddDTO buildAddDto() {
CcdiStaffEnterpriseRelationAddDTO addDTO = new CcdiStaffEnterpriseRelationAddDTO();
addDTO.setPersonId("320101199001010022");
addDTO.setSocialCreditCode("91310000123456789A");
addDTO.setEnterpriseName("测试企业");
addDTO.setRelationPersonPost("董事");
return addDTO;
}
private static void registerTableInfo(Class<?> entityClass, String namespace) {
if (TableInfoHelper.getTableInfo(entityClass) != null) {
return;
}
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new Configuration(), "");
assistant.setCurrentNamespace(namespace);
TableInfoHelper.initTableInfo(assistant, entityClass);
}
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiAssetInfoDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.service.impl.CcdiStaffFmyRelationServiceImpl;
import org.junit.jupiter.api.Test;
@@ -46,6 +47,9 @@ class CcdiStaffFmyRelationServiceImplTest {
@Mock
private ICcdiAssetInfoService assetInfoService;
@Mock
private CcdiStaffEnterpriseRelationMapper staffEnterpriseRelationMapper;
@Test
void selectRelationById_shouldAggregateAssetInfoList() {
CcdiStaffFmyRelationVO relationVO = new CcdiStaffFmyRelationVO();
@@ -171,6 +175,36 @@ class CcdiStaffFmyRelationServiceImplTest {
verify(assetInfoService).replaceByFamilyIdAndPersonId("320101199001010011", "A123456789", editDTO.getAssetInfoList());
}
@Test
void updateRelation_shouldInvalidateEnterpriseRelationsWhenFamilyBecomesInvalid() {
CcdiStaffFmyRelation existing = new CcdiStaffFmyRelation();
existing.setId(10L);
existing.setRelationCertType("护照");
existing.setRelationCertNo("A123456789");
existing.setStatus(1);
CcdiStaffFmyRelationEditDTO editDTO = new CcdiStaffFmyRelationEditDTO();
editDTO.setId(10L);
editDTO.setPersonId("320101199001010011");
editDTO.setRelationType("配偶");
editDTO.setRelationName("李四");
editDTO.setRelationCertType("护照");
editDTO.setRelationCertNo("A123456789");
editDTO.setStatus(0);
editDTO.setAssetInfoList(List.of(buildAssetDto("车辆")));
when(relationMapper.selectById(10L)).thenReturn(existing);
when(relationMapper.updateById(any(CcdiStaffFmyRelation.class))).thenReturn(1);
int result = service.updateRelation(editDTO);
assertEquals(1, result);
var order = inOrder(relationMapper, staffEnterpriseRelationMapper, assetInfoService);
order.verify(relationMapper).updateById(any(CcdiStaffFmyRelation.class));
order.verify(staffEnterpriseRelationMapper).invalidateByFamilyCertNo("A123456789");
order.verify(assetInfoService).replaceByFamilyIdAndPersonId("320101199001010011", "A123456789", editDTO.getAssetInfoList());
}
@Test
void deleteRelationByIds_shouldDeleteRelativeAssetsBeforeDeletingRelations() {
CcdiStaffFmyRelation relation1 = new CcdiStaffFmyRelation();

View File

@@ -0,0 +1,64 @@
package com.ruoyi.info.collection.service;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiStaffRecruitmentDualImportContractTest {
@Test
void shouldExposeSingleDualSheetImportEntry() throws Exception {
String controller = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java")
);
assertTrue(controller.contains("\"招聘信息\""));
assertTrue(controller.contains("\"历史工作经历\""));
assertFalse(controller.contains("workImportTemplate"));
assertFalse(controller.contains("importWorkData"));
String service = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java")
);
assertTrue(service.contains("String importRecruitment("));
assertTrue(service.contains("List<CcdiStaffRecruitmentExcel> recruitmentList"));
assertTrue(service.contains("List<CcdiStaffRecruitmentWorkExcel> workList"));
assertFalse(service.contains("importRecruitmentWork("));
String importService = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java")
);
assertTrue(importService.contains("void importRecruitmentAsync("));
assertTrue(importService.contains("List<CcdiStaffRecruitmentExcel> recruitmentList"));
assertTrue(importService.contains("List<CcdiStaffRecruitmentWorkExcel> workList"));
assertFalse(importService.contains("importRecruitmentWorkAsync("));
}
@Test
void shouldExposeFailureSheetFieldsAndSingleTaskInit() throws Exception {
assertHasField(
"com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO",
"sheetName"
);
assertHasField(
"com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO",
"sheetRowNum"
);
String serviceImpl = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java")
);
assertTrue(serviceImpl.contains("recruitmentList.size() + workList.size()"));
assertFalse(serviceImpl.contains("importRecruitmentWork("));
}
private void assertHasField(String className, String fieldName) throws Exception {
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField(fieldName);
assertNotNull(field);
}
}

View File

@@ -0,0 +1,98 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiStaffRecruitment;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
import com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO;
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper;
import com.ruoyi.info.collection.service.impl.CcdiStaffRecruitmentImportServiceImpl;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiStaffRecruitmentImportServiceImplTest {
@InjectMocks
private CcdiStaffRecruitmentImportServiceImpl service;
@Mock
private CcdiStaffRecruitmentMapper recruitmentMapper;
@Mock
private CcdiStaffRecruitmentWorkMapper recruitmentWorkMapper;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private ValueOperations<String, Object> valueOperations;
@Mock
private HashOperations<String, Object, Object> hashOperations;
@Test
void shouldFailWholeWorkGroupWhenExistingHistoryExists() {
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(recruitmentMapper.selectBatchIds(any())).thenReturn(List.of(buildRecruitment("RC001")));
when(recruitmentWorkMapper.selectCount(any())).thenReturn(1L);
CcdiStaffRecruitmentWorkExcel workRow = new CcdiStaffRecruitmentWorkExcel();
workRow.setRecruitId("RC001");
workRow.setCandName("张三");
workRow.setRecruitName("社会招聘项目");
workRow.setPosName("Java工程师");
workRow.setSortOrder(1);
workRow.setCompanyName("测试科技");
workRow.setPositionName("开发工程师");
workRow.setJobStartMonth("2022-01");
service.importRecruitmentAsync(Collections.emptyList(), List.of(workRow), "task-1", "admin");
verify(recruitmentWorkMapper, never()).delete(any());
verify(recruitmentWorkMapper, never()).insert(any(com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork.class));
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:recruitment:task-1:failures"), failureCaptor.capture(), anyLong(), any());
Object rawFailures = failureCaptor.getValue();
assertNotNull(rawFailures);
assertInstanceOf(List.class, rawFailures);
List<?> failures = (List<?>) rawFailures;
assertFalse(failures.isEmpty());
RecruitmentImportFailureVO failure = (RecruitmentImportFailureVO) failures.get(0);
assertEquals("历史工作经历", failure.getSheetName());
assertEquals("2", failure.getSheetRowNum());
assertEquals("招聘记录编号[RC001]已存在历史工作经历,不允许重复导入", failure.getErrorMessage());
}
private CcdiStaffRecruitment buildRecruitment(String recruitId) {
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
recruitment.setRecruitId(recruitId);
recruitment.setRecruitType("SOCIAL");
recruitment.setCandName("张三");
recruitment.setRecruitName("社会招聘项目");
recruitment.setPosName("Java工程师");
return recruitment;
}
}

View File

@@ -4,9 +4,13 @@ import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
@@ -98,6 +102,59 @@ class EasyExcelUtilTemplateTest {
}
}
@Test
void importTemplateWithDictDropdown_shouldCreateRecruitmentDualSheetTemplate() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
try (MockedStatic<DictUtils> mocked = mockStatic(DictUtils.class)) {
mocked.when(() -> DictUtils.getDictCache("ccdi_admit_status"))
.thenReturn(List.of(
buildDictData("录用"),
buildDictData("未录用"),
buildDictData("放弃")
));
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiStaffRecruitmentExcel.class,
"招聘信息",
CcdiStaffRecruitmentWorkExcel.class,
"历史工作经历",
"招聘信息管理导入模板"
);
}
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
assertEquals(2, workbook.getNumberOfSheets(), "招聘导入模板应输出双Sheet");
assertEquals("招聘信息", workbook.getSheetAt(0).getSheetName());
assertEquals("历史工作经历", workbook.getSheetAt(1).getSheetName());
}
}
@Test
void importTemplateWithDictDropdown_shouldUseUpdatedEnterpriseHeaders() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
try (MockedStatic<DictUtils> mocked = mockStatic(DictUtils.class)) {
mocked.when(() -> DictUtils.getDictCache("ccdi_entity_type"))
.thenReturn(List.of(buildDictData("有限责任公司")));
mocked.when(() -> DictUtils.getDictCache("ccdi_enterprise_nature"))
.thenReturn(List.of(buildDictData("民营企业")));
mocked.when(() -> DictUtils.getDictCache("ccdi_certificate_type"))
.thenReturn(List.of(buildDictData("居民身份证")));
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiEnterpriseBaseInfoExcel.class, "实体库管理");
}
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
Row headerRow = workbook.getSheetAt(0).getRow(0);
assertEquals("经营状态", headerRow.getCell(16).getStringCellValue());
assertEquals("风险等级*", headerRow.getCell(17).getStringCellValue());
assertEquals("企业来源*", headerRow.getCell(18).getStringCellValue());
assertEquals(19, headerRow.getLastCellNum());
}
}
private void assertTextColumn(Sheet sheet, int columnIndex) {
CellStyle style = sheet.getColumnStyle(columnIndex);
assertNotNull(style, "文本列应设置默认样式");

View File

@@ -1,6 +1,7 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.List;
import lombok.Data;
/**
@@ -80,4 +81,6 @@ public class CcdiProjectExtendedPurchaseDetailVO {
private String updatedBy;
private String updateTime;
private List<CcdiProjectExtendedPurchaseSupplierVO> supplierList;
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 专项核查采购供应商明细
*/
@Data
public class CcdiProjectExtendedPurchaseSupplierVO {
private Long id;
private String purchaseId;
private String supplierName;
private String supplierUscc;
private String contactPerson;
private String contactPhone;
private String supplierBankAccount;
private Integer isBidWinner;
private Integer sortOrder;
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseSupplierVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
@@ -96,6 +97,18 @@ public interface CcdiProjectSpecialCheckMapper {
@Param("purchaseId") String purchaseId
);
/**
* 查询专项核查采购供应商明细
*
* @param projectId 项目ID
* @param purchaseId 采购事项ID
* @return 供应商明细
*/
List<CcdiProjectExtendedPurchaseSupplierVO> selectExtendedPurchaseSuppliers(
@Param("projectId") Long projectId,
@Param("purchaseId") String purchaseId
);
/**
* 查询专项核查招聘拓展列表
*

View File

@@ -103,6 +103,9 @@ public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCh
if (detail == null) {
throw new ServiceException("当前记录不属于该项目专项核查范围");
}
detail.setSupplierList(defaultList(
specialCheckMapper.selectExtendedPurchaseSuppliers(queryDTO.getProjectId(), queryDTO.getPurchaseId())
));
return detail;
}

View File

@@ -574,6 +574,33 @@
where p.purchase_id = #{purchaseId}
</select>
<select id="selectExtendedPurchaseSuppliers"
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseSupplierVO">
select
s.id,
s.purchase_id,
s.supplier_name,
s.supplier_uscc,
s.contact_person,
s.contact_phone,
s.supplier_bank_account,
s.is_bid_winner,
s.sort_order
from ccdi_purchase_transaction_supplier s
inner join ccdi_purchase_transaction p on p.purchase_id = s.purchase_id
inner join (
select distinct scope.staff_name
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name is not null
and scope.staff_name != ''
) scoped_staff
on scoped_staff.staff_name = p.applicant_name
where s.purchase_id = #{purchaseId}
order by s.sort_order asc, s.id asc
</select>
<select id="selectExtendedRecruitmentPage" resultMap="ExtendedRecruitmentListItemResultMap">
<bind name="projectId" value="query.projectId"/>
select distinct r.recruit_id,

View File

@@ -0,0 +1,408 @@
# 招聘信息管理双 Sheet 导入设计文档
## 1. 背景
当前招聘信息管理已经具备以下能力:
- 主信息维护:`ccdi_staff_recruitment`
- 历史工作经历维护:`ccdi_staff_recruitment_work`
- 页面编辑态支持手工维护历史工作经历
- 导入能力仍拆分为两条独立入口:
- `招聘信息导入`
- `历史工作经历导入`
对比已经完成改造的招投标信息维护,招聘信息管理当前导入模式存在三个问题:
1. 导入入口与当前主从结构不一致,用户需要自行判断先后顺序
2. 双 Sheet 场景无法在同一个文件中同时提交主信息和工作经历
3. 失败记录无法直接定位失败来源 Sheet 和 Excel 行号
本次需求要求参考招投标信息维护的双 Sheet 模式,将招聘信息管理改造成单入口双 Sheet 导入,并补齐失败定位信息。
## 2. 目标与范围
### 2.1 目标
1. 招聘信息管理页面只保留一个导入按钮
2. 导入模板改为双 Sheet
- `招聘信息`
- `历史工作经历`
3. 后端统一用一个异步任务处理整份文件
4. 支持以下三类导入方式:
- 只导 `招聘信息` Sheet
- 只导 `历史工作经历` Sheet
- 两个 Sheet 同时导入
5. 失败列表必须展示:
- 失败 Sheet
- 失败行号
- 失败原因
### 2.2 已确认业务口径
1. 页面导入入口合并为单按钮
2. `历史工作经历` Sheet 允许独立导入
3. `历史工作经历` 独立导入时,继续按库内已存在的招聘主信息匹配
4. 如果某个招聘记录已经存在历史工作经历,再次导入该招聘记录的工作经历时直接报错,不允许覆盖旧数据
5. 失败展示统一为一个列表,通过 `失败Sheet``失败行号``失败原因` 定位问题
### 2.3 非目标
1. 不调整招聘信息手工新增、编辑、详情、列表查询主体逻辑
2. 不调整编辑页中手工维护历史工作经历的交互
3. 不新增数据库表,不修改现有表结构
4. 不保留旧的“导入工作经历”按钮或旧接口作为兼容入口
5. 不新增独立任务中心或额外状态体系
## 3. 现状分析
### 3.1 前端现状
招聘信息管理页面当前有两个导入按钮:
1. `导入`
2. `导入工作经历`
两个按钮分别对应不同模板和不同上传接口:
- `/ccdi/staffRecruitment/importTemplate`
- `/ccdi/staffRecruitment/workImportTemplate`
- `/ccdi/staffRecruitment/importData`
- `/ccdi/staffRecruitment/importWorkData`
页面当前仅维护一套失败记录弹窗,但会根据 `currentImportType` 切换展示字段,本质仍然是两条独立导入链路。
### 3.2 后端现状
当前招聘导入服务拆成两条独立任务:
1. `importRecruitment(List<CcdiStaffRecruitmentExcel>)`
2. `importRecruitmentWork(List<CcdiStaffRecruitmentWorkExcel>)`
对应的异步实现也拆成:
1. `importRecruitmentAsync(...)`
2. `importRecruitmentWorkAsync(...)`
其中历史工作经历导入现有语义为:
- 允许独立导入
-`recruitId` 匹配招聘主信息
- 若同一 `recruitId` 任一行失败,则整组不落库
- 导入前会删除该 `recruitId` 旧工作经历,再导入新数据
本次要调整的是最后一条覆盖语义:改为“已有旧记录则失败,不覆盖”。
## 4. 方案对比
### 4.1 方案 A单入口 + 单任务编排 + 单失败列表
做法:
- 页面只保留一个导入按钮
- 后端一次读取双 Sheet
- 用一个异步任务统一处理主信息和工作经历
- 用一个失败列表展示全部失败记录
优点:
1. 与招投标信息维护双 Sheet 模式一致
2. 支持同文件主从联动导入
3. 失败展示口径最统一
4. 页面交互最简单
缺点:
1. 需要把当前两条招聘导入任务重构为一条统一任务
### 4.2 方案 B单入口 + 双任务复用 + 前端聚合失败
做法:
- 上传一个双 Sheet 文件
- 后端仍拆成主信息任务和工作经历任务
- 前端再聚合两个任务的失败记录
缺点:
1. 工作经历 Sheet 无法天然引用“本次主 Sheet 刚成功导入的数据”
2. 会引入跨任务依赖和前端拼装逻辑
3. 导入状态与失败记录口径不够干净
### 4.3 方案 C仅合并模板和按钮不做真实双 Sheet 联动
做法:
- 页面只有一个导入按钮
- 模板做成双 Sheet
- 但工作经历仍只能引用库内已有招聘主信息
缺点:
1. 无法完成同文件主从联动
2. 不能真正对齐招投标信息维护的双 Sheet 模式
3. 业务理解成本仍然偏高
### 4.4 最终选择
采用方案 A`单入口 + 单异步任务 + 单失败列表`
理由:
1. 最符合“参考招投标信息维护双 Sheet 导入”的目标
2. 业务链路闭环最完整
3. 用户侧交互最清晰
4. 后续维护成本最低
## 5. 总体设计
### 5.1 模板设计
统一模板文件名:`招聘信息管理导入模板`
模板包含两个 Sheet
1. `招聘信息`
2. `历史工作经历`
约束:
1. 两个 Sheet 都允许为空表头,但整份文件至少一个 Sheet 有数据
2. `历史工作经历` Sheet 保持当前字段结构,不新增业务字段
3. `招聘信息` Sheet 保持当前字段结构,不新增业务字段
### 5.2 页面交互设计
页面顶部仅保留一个 `导入` 按钮。
上传弹窗说明文案明确提示:
- 模板包含 `招聘信息``历史工作经历` 两个 Sheet
- 支持只填写一个 Sheet也支持两个 Sheet 同时填写
上传成功后:
1. 只返回一个 `taskId`
2. 前端只启动一套轮询
3. 页面只保留一个“查看导入失败记录”入口
### 5.3 接口设计
保留并调整以下接口:
1. `POST /ccdi/staffRecruitment/importTemplate`
- 输出双 Sheet 模板
2. `POST /ccdi/staffRecruitment/importData`
- 一次接收整份双 Sheet 文件
- 返回统一 `taskId`
3. `GET /ccdi/staffRecruitment/importStatus/{taskId}`
- 查询统一任务状态
4. `GET /ccdi/staffRecruitment/importFailures/{taskId}`
- 查询统一失败记录
移除以下独立导入入口:
1. `POST /ccdi/staffRecruitment/workImportTemplate`
2. `POST /ccdi/staffRecruitment/importWorkData`
## 6. 后端处理流程设计
### 6.1 任务编排
`/importData` 收到文件后,一次读取两个 Sheet
- `招聘信息` -> `List<CcdiStaffRecruitmentExcel>`
- `历史工作经历` -> `List<CcdiStaffRecruitmentWorkExcel>`
若两个 Sheet 都为空,直接返回错误:`至少需要一条数据`
否则初始化一条统一导入任务,并异步执行以下两个阶段:
1. 阶段一:处理 `招聘信息` Sheet
2. 阶段二:处理 `历史工作经历` Sheet
### 6.2 阶段一:招聘信息 Sheet
沿用现有主信息导入规则:
1. 招聘记录编号不能为空
2. 招聘项目名称、职位名称、职位类别、职位描述、候选人姓名、学历、证件号码、毕业院校、专业、毕业年月、录用情况不能为空
3. 证件号码格式必须合法
4. 毕业年月必须为 `YYYYMM`
5. 录用情况只能填写合法枚举
6. 招聘类型继续按现有逻辑推断
7. 数据库中已存在的 `recruitId` 直接失败
8. 文件内重复 `recruitId` 直接失败
阶段一成功后,建立“本次主 Sheet 成功导入的招聘记录映射”,供阶段二匹配使用。
### 6.3 阶段二:历史工作经历 Sheet
#### 6.3.1 匹配规则
工作经历按 `recruitId` 分组处理。
每组匹配顺序固定为:
1. 先匹配本次 `招聘信息` Sheet 中成功导入的招聘主信息
2. 再匹配数据库中已存在的招聘主信息
若都匹配不到,整组失败。
#### 6.3.2 基础校验
沿用并收敛现有规则:
1. `recruitId` 不能为空
2. 候选人姓名不能为空
3. 招聘项目名称不能为空
4. 职位名称不能为空
5. 排序号不能为空且必须大于 0
6. 工作单位不能为空
7. 岗位不能为空
8. 入职年月不能为空,格式必须为 `YYYY-MM`
9. 离职年月若有值,格式必须为 `YYYY-MM`
10. 候选人姓名、招聘项目名称、职位名称必须与匹配到的招聘主信息一致
11. 匹配到的招聘主信息 `recruitType` 必须为 `SOCIAL`
12. 同一 `recruitId` 下,文件内 `sortOrder` 不能重复
#### 6.3.3 已有工作经历报错规则
若数据库中某个 `recruitId` 已经存在任意历史工作经历记录,则本次再导入该 `recruitId` 的工作经历时:
1. 直接整组失败
2. 不删除旧记录
3. 不做覆盖
4. 不做部分追加
失败原因统一返回明确文案,例如:
`招聘记录编号[xxx]已存在历史工作经历,不允许重复导入`
#### 6.3.4 分组成功与失败策略
`recruitId` 维度整组处理:
1. 若该组任意一行失败,则整组不落库
2. 若该组全部通过校验,则整组批量插入
这样可以保证同一招聘记录下的工作经历要么整组成功,要么整组失败,不会出现排序半成功半失败的问题。
### 6.4 失败记录结构
统一失败记录对象新增以下字段:
1. `sheetName`
2. `sheetRowNum`
3. `errorMessage`
并继续保留必要业务定位字段:
1. `recruitId`
2. `recruitName`
3. `posName`
4. `candName`
5. `candId`
6. `companyName`
7. `positionName`
其中:
- 主 Sheet 失败时,`sheetName = 招聘信息`
- 工作经历 Sheet 失败时,`sheetName = 历史工作经历`
- `sheetRowNum` 返回 Excel 实际数据行号
## 7. 前端展示设计
### 7.1 导入入口
页面只保留一个导入按钮,不再保留 `导入工作经历`
### 7.2 轮询状态
前端本地只维护一条最近导入任务状态:
1. `taskId`
2. `status`
3. `hasFailures`
4. `totalCount`
5. `successCount`
6. `failureCount`
不再按导入类型区分任务状态。
### 7.3 失败弹窗
失败弹窗统一展示一个列表,至少包含以下列:
1. `失败Sheet`
2. `失败行号`
3. `招聘记录编号`
4. `招聘项目名称`
5. `职位名称`
6. `候选人姓名`
7. `工作单位`
8. `失败原因`
展示规则:
1. 主 Sheet 失败时,`工作单位` 为空
2. 工作经历 Sheet 失败时,`工作单位` 有值
3. `失败行号` 展示为 `第X行`
## 8. 影响范围
### 8.1 后端
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java`
### 8.2 前端
- `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- `ruoyi-ui/src/api/ccdiStaffRecruitment.js`
### 8.3 不影响范围
1. 招聘详情页展示结构
2. 招聘手工新增、编辑、删除接口
3. 数据库结构与 SQL 迁移
4. 路由、菜单、权限标识
## 9. 验证方案
### 9.1 后端验证
1. `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile`
### 9.2 前端验证
1. `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod`
### 9.3 页面实测
必须通过真实页面下载模板后再构造测试文件,至少覆盖:
1. 只导 `招聘信息` Sheet 成功
2. 只导 `历史工作经历` Sheet匹配库内已有主信息成功
3. 双 Sheet 同时导入,工作经历引用本次主 Sheet 成功
4. 工作经历命中已存在旧记录时报错
5. 工作经历命中非社招主信息时报错
6. 文件内工作经历排序号重复时报错
7. 失败弹窗正确展示 `失败Sheet``失败行号``失败原因`
测试结束后需关闭测试过程中启动的前后端进程。
## 10. 结论
本次采用“单入口 + 双 Sheet 模板 + 单异步任务 + 单失败列表”的最短路径实现:
1. 页面导入入口统一为一个按钮
2. 双 Sheet 模板统一为 `招聘信息` + `历史工作经历`
3. 工作经历继续允许独立导入
4. 工作经历匹配顺序为“本次主 Sheet 成功数据优先,其次数据库已有主信息”
5. 数据库里已存在工作经历时,本次重复导入直接报错,不覆盖旧数据
6. 失败列表统一展示失败 Sheet、失败行号和失败原因

View File

@@ -0,0 +1,37 @@
# 员工信息维护双 Sheet 导入后端实施计划
## 目标
- 将员工信息维护导入模板改为 `员工信息` + `员工资产信息` 双 Sheet。
- 统一由 `/ccdi/baseStaff/importData` 接收单文件上传,并按有数据的 Sheet 分别调用现有员工导入与员工资产导入方法。
- 员工信息导入取消“更新已存在员工”能力,命中现有员工 ID 或身份证号时直接记失败。
- 两类失败记录统一补充 `sheetName``rowNum``errorMessage`,便于直接定位 Excel 中的失败位置。
## 实施内容
- 控制器改造
- 修改 `CcdiBaseStaffController#importTemplate`,下载双 Sheet 模板,文件名统一为“员工信息维护导入模板”。
- 修改 `CcdiBaseStaffController#importData`,按 Sheet 名分别读取 `CcdiBaseStaffExcel``CcdiBaseStaffAssetInfoExcel`
- 两个 Sheet 均为空时返回错误;任一 Sheet 有数据时,仅提交对应导入任务。
- 返回新的双任务提交结果对象,包含 `staffTaskId``assetTaskId``message`
- 服务改造
- 修改 `ICcdiBaseStaffService``CcdiBaseStaffServiceImpl`,移除 `updateSupport` 参数。
- 修改 `ICcdiBaseStaffImportService``CcdiBaseStaffImportServiceImpl`,移除更新分支与 `insertOrUpdateBatch` 调用。
- 员工导入校验统一为:
- 员工 ID 已存在:失败
- 身份证号已存在:失败
- Excel 内重复:失败
- 员工资产导入补充重复校验:
- 数据库中存在同一 `personId + assetMainType + assetSubType + assetName`:失败
- 导入文件中存在同一组合重复:失败
- VO 修正
- 新增员工双 Sheet 提交结果 VO。
- 修正员工导入失败记录 VO 字段名为 `staffId`,与前端表格字段保持一致。
- 员工与员工资产失败记录 VO 均增加 `sheetName``rowNum`
## 验证
- `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile`
- 补充控制器与服务层回归测试,覆盖双 Sheet 分发与“已存在即失败”规则。
## 影响范围
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/`

View File

@@ -0,0 +1,26 @@
# 2026-04-22 招投标导入失败展示增强后端实施计划
## 1. 目标
- 为招投标导入失败记录补充失败来源 `Sheet`
- 为失败记录补充 Excel 失败行号
- 保持现有导入校验逻辑不变,仅增强失败记录元数据
## 2. 涉及范围
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/PurchaseTransactionImportFailureVO.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java`
## 3. 实施步骤
1. 在失败记录 VO 中新增 `sheetName``sheetRowNum` 字段,供前端弹窗直接读取
2. 在导入服务中为主信息 Sheet 和供应商明细 Sheet 建立“Excel 数据行号”上下文
3. 在主信息校验、供应商校验、主从关系校验、空采购事项 ID 供应商校验等失败分支中,统一写入对应的 `Sheet` 与行号
4. 对跨多行触发的失败场景,行号以合并字符串形式返回,便于页面直接展示
5. 保留原有失败原因与业务字段,避免影响已有失败记录查询接口
## 4. 验证方式
- 执行后端编译,确认新增字段和异常封装无编译错误
- 通过真实页面上传失败样本,核对失败记录接口返回 `sheetName / sheetRowNum / errorMessage`
- 覆盖至少一个主信息失败样本和一个供应商明细失败样本

View File

@@ -0,0 +1,35 @@
# 招投标信息维护后端实施计划
## 目标
- 将现有 `purchaseTransaction` 后端链路改造为“招投标主信息 + 供应商明细子表”结构。
- 保留原有 URL、权限前缀和内部类名统一用户可见文案为“招投标信息维护”。
- 支持详情查询返回全部供应商明细,列表返回中标供应商摘要和参与供应商数。
- 支持双 Sheet 导入模板与按 `purchaseId` 聚合校验的异步导入。
## 实施内容
- 数据层
- 新增 `ccdi_purchase_transaction_supplier` 明细表初始化 SQL 与增量迁移脚本。
- 迁移脚本回填历史中标供应商数据,并将菜单名称更新为“招投标信息维护”。
- 领域模型
- 新增供应商 entity、DTO、VO、Excel 模型。
- 主 DTO/VO 增加 `supplierList`,主 VO 增加 `supplierCount`
- 主 Excel 模板改为仅承载招投标主信息,供应商明细独立建模。
- 接口与服务
- 列表 SQL 增加供应商数聚合。
- 详情查询补充供应商明细列表。
- 新增/修改时由 `supplierList` 自动回填主表中标供应商摘要字段。
- 删除主记录时级联删除供应商明细。
- 导入链路改为“双 Sheet 读取 + 按事项聚合校验 + 主从同落库”。
- 项目专项核查
- 项目采购详情 VO、Mapper、Service 增加供应商明细查询能力,保持项目详情与信息维护详情口径一致。
## 验证
- `mvn -pl ccdi-info-collection,ccdi-project -am -DskipTests compile`
- `./bin/restart_java_backend.sh restart`
- 浏览器验证列表接口、详情接口与项目详情供应商明细展示。
## 产出文件
- `sql/ccdi_purchase_transaction.sql`
- `sql/ccdi_purchase_transaction_menu.sql`
- `sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql`
- `ccdi-info-collection``ccdi-project` 相关后端代码

View File

@@ -0,0 +1,39 @@
# CCDI 数据库默认排序规则修复实施计划
## 保存路径确认
- 路径:`docs/plans/backend/2026-04-22-ccdi-database-default-collation-backend-implementation.md`
- 归类:后端实施计划
## 目标
-`ccdi` 数据库默认字符集统一为 `utf8mb4`
-`ccdi` 数据库默认排序规则统一为 `utf8mb4_general_ci`
## 背景
- 当前 `ccdi` 数据库默认排序规则为 `utf8mb4_unicode_ci`
- 仓库数据库规范要求业务库默认排序规则统一为 `utf8mb4_general_ci`,避免新建表或新增字符字段继续继承错误默认值。
## 实施步骤
1. 新增数据库增量脚本,执行 `ALTER DATABASE ccdi CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci`
2. 使用 `bin/mysql_utf8_exec.sh` 在当前开发库执行脚本。
3. 查询 `information_schema.SCHEMATA` 回查默认字符集与默认排序规则是否生效。
4. 补充实施记录,说明变更范围与验证结果。
## 验证命令
```bash
bin/mysql_utf8_exec.sh sql/migration/2026-04-22-fix-ccdi-database-default-collation.sql
```
```bash
mysql ... -e "SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='ccdi';"
```
## 完成标准
- `ccdi``DEFAULT_CHARACTER_SET_NAME``utf8mb4`
- `ccdi``DEFAULT_COLLATION_NAME``utf8mb4_general_ci`
- 本次变更已形成实施记录

View File

@@ -0,0 +1,47 @@
# 信息维护移除导出与菜单排序后端实施计划
**Goal:** 移除信息维护相关模块的后端导出接口与导出权限,并通过增量 SQL 统一“信息维护”目录下的菜单顺序。
**Architecture:** 后端仅收口 `ccdi-info-collection` 控制器层的 `/export` 接口,不调整列表、详情、导入与删除链路;菜单治理通过 `sql/migration` 新增一份可重复执行脚本完成,脚本同时删除导出权限按钮并更新 `order_num`
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MySQL, Markdown
---
## 文件结构与职责
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/`
移除信息维护模块各控制器的 `/export` 接口。
- `sql/migration/2026-04-22-remove-info-maintenance-export-and-sort-menus.sql`
删除导出权限菜单并统一“信息维护”子菜单排序。
- `sql/*.sql`
修正仓库内已有菜单脚本,避免新库初始化时继续带出导出权限或错误顺序。
## 实施步骤
- [x] 盘点信息维护模块现存 `/export` 接口与导出权限点
- [x] 移除员工、关系、招聘、调动、采购、账户等模块的控制器导出接口
- [x] 新增菜单增量脚本,删除导出权限并统一菜单排序
- [x] 同步修正仓库内已有菜单 SQL避免新环境重新带回导出权限
- [x] 运行检索校验,确认控制器层不再暴露信息维护导出接口
## 验证
```bash
rg -n "@PostMapping\\(\"/export\"\\)|hasPermi\\('ccdi:.*:export'\\)" \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAccountInfoController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCustEnterpriseRelationController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCustFmyRelationController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiPurchaseTransactionController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffEnterpriseRelationController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffTransferController.java
```
## 完成标准
- 信息维护相关控制器不再提供 `/export` 接口
- “信息维护”菜单下相关导出权限按钮已可通过增量 SQL 清理
- 菜单排序调整为统一且可重复执行的固定顺序

View File

@@ -0,0 +1,39 @@
# 员工亲属关系维护双 Sheet 导入后端实施计划
## 目标
- 将员工亲属关系维护导入模板改为双 Sheet
- `员工亲属关系信息`
- `亲属资产信息`
- 将导入提交入口统一到 `/ccdi/staffFmyRelation/importData`
- 统一补充失败记录定位字段,支持前端展示 `Sheet / Excel行号 / 失败原因`
## 实施内容
- Controller 调整
- `CcdiStaffFmyRelationController#importTemplate` 改为输出双 Sheet 模板,模板文件名统一为“员工亲属关系维护导入模板”。
- `CcdiStaffFmyRelationController#importData` 一次读取两个 Sheet。
- 按有数据的 Sheet 分别提交亲属关系导入任务和亲属资产导入任务。
- 返回新的提交结果 VO包含 `relationTaskId``assetTaskId` 和提示文案。
- VO 调整
- `StaffFmyRelationImportFailureVO` 增加 `sheetName``rowNum`
- `AssetImportFailureVO` 增加 `sheetName``rowNum`
- 新增 `StaffFmyRelationImportSubmitResultVO`
- 导入服务调整
- `CcdiStaffFmyRelationImportServiceImpl` 失败记录写入固定 `sheetName=员工亲属关系信息`,并记录 Excel 数据行号。
- `CcdiAssetInfoImportServiceImpl` 失败记录写入固定 `sheetName=亲属资产信息`,并记录 Excel 数据行号。
- 兼容策略
- 保留原 `CcdiAssetInfoController` 的状态查询与失败记录查询接口,前端继续复用原有资产任务轮询与失败记录查看能力。
## 验证
- 后端优先验证:
- `CcdiStaffFmyRelationControllerTest`
- `CcdiAssetInfoControllerTest`
- 编译验证:
- `mvn -pl ccdi-info-collection -am -Dmaven.test.skip=true compile`
## 影响范围
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportFailureVO.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportSubmitResultVO.java`

View File

@@ -0,0 +1,31 @@
# 员工招聘列表排序规则冲突修复实施计划
## 保存路径确认
- 路径:`docs/plans/backend/2026-04-22-staff-recruitment-collation-fix-backend-implementation.md`
- 归类:后端实施计划
## 背景
- 员工招聘列表查询执行 `ccdi_staff_recruitment``ccdi_staff_recruitment_work``recruit_id` 关联时,报错 `Illegal mix of collations (utf8mb4_0900_ai_ci) and (utf8mb4_general_ci)`
- 现有招聘主表已被纳入全库统一排序规则脚本,但历史工作经历子表在建表时未显式声明 `utf8mb4_general_ci`,且未被纳入统一修复脚本,导致在 MySQL 8 环境中可能沿用默认 `utf8mb4_0900_ai_ci`
## 实施范围
- 后端 MyBatis 查询 XML
- 招聘相关 SQL 建表脚本
- 数据库增量迁移脚本
## 实施步骤
1. 修改招聘列表查询 SQL`ccdi_staff_recruitment_work.recruit_id` 聚合与关联时显式使用 `utf8mb4_general_ci`,先恢复查询可用性。
2. 修正 `2026-04-15-add-staff-recruitment-social-work-summary.sql`,为 `ccdi_staff_recruitment_work` 建表语句补齐 `COLLATE=utf8mb4_general_ci`
3. 补充增量脚本,将现有库中的 `ccdi_staff_recruitment_work` 转换为 `utf8mb4_general_ci`
4. 更新全库统一排序规则脚本,将该表纳入统一修复范围,避免后续漏执行。
5. 编译受影响模块,确认 Mapper XML 与资源装配正常。
## 验证要点
- `selectRecruitmentPage` 查询不再因 `recruit_id` 关联报排序规则冲突。
- `ccdi_staff_recruitment_work` 表级与字符字段排序规则统一为 `utf8mb4_general_ci`
- `mvn -pl ccdi-info-collection -am compile` 通过。

View File

@@ -0,0 +1,36 @@
# 招聘信息历史工作经历手动编辑后端实施计划
## 文档信息
- 保存路径:`docs/plans/backend/2026-04-22-staff-recruitment-work-experience-manual-edit-plan.md`
- 适用范围:招聘信息管理编辑接口
- 需求目标:在招聘信息编辑页支持手动维护历史工作经历,并保证保存后落到 `ccdi_staff_recruitment_work` 子表
## 实施范围
1. 扩展招聘信息编辑 DTO允许接收历史工作经历列表。
2. 增加历史工作经历子项 DTO并对字符长度、年月格式进行基础校验。
3. 调整招聘信息编辑服务:
- 主表 `ccdi_staff_recruitment` 继续按原逻辑更新;
- 当招聘类型为 `SOCIAL` 且前端传入工作经历列表时,按招聘记录编号先删后插覆盖子表;
- 当招聘类型改为 `CAMPUS` 时,删除该记录已存在的历史工作经历。
## 实施步骤
1. 新增历史工作经历编辑 DTO约束 `companyName``departmentName``positionName`、年月等字段长度与格式。
2.`CcdiStaffRecruitmentEditDTO` 中增加 `workExperienceList` 字段,并启用嵌套校验。
3.`CcdiStaffRecruitmentServiceImpl.updateRecruitment` 中增加子表覆盖保存逻辑。
4. 保持详情查询逻辑不变,继续通过已有 `selectWorkExperienceList` 返回子表明细。
## 影响评估
- 仅影响招聘信息编辑接口,不影响招聘信息导入、详情查询、列表分页逻辑。
- 不新增数据库结构变更,不新增菜单或权限。
- 旧前端若未传 `workExperienceList`,社招编辑仍保留已有工作经历数据,不会被误删。
## 验证要点
1. 编辑社招记录时可提交多条历史工作经历并成功保存。
2. 编辑社招记录时删除全部历史工作经历后提交,子表数据应被清空。
3. 将社招记录改为校招后提交,历史工作经历应自动删除。
4. 非法年月格式或超长字段应被后端校验拒绝。

View File

@@ -0,0 +1,32 @@
# 员工信息导入机构号校验后端实施计划
## 目标
- 在员工信息 Excel 导入链路中校验 `所属部门IDdeptId` 是否对应有效机构号。
- 有效口径统一为 `sys_dept` 中“正常且未删除”的部门,即 `status = '0'``del_flag = '0'`
- 命中不存在、已停用或已删除的部门时,不入库,直接进入员工导入失败记录。
## 实施内容
- 导入服务改造
- 修改 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java`
-`validateStaffData` 中于必填校验后增加 `deptId` 有效性校验。
- 新增私有方法按 `deptId` 查询部门并校验 `status``delFlag`
- 校验失败时抛出统一错误文案:`所属部门ID[xxx]不存在或已停用/删除,请检查机构号`
- 部门 Mapper 对齐
- 修改 `ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml`
-`selectDeptById` 查询补齐 `d.del_flag` 字段,保证导入服务可同时判断停用与逻辑删除状态。
- 单元测试补充
- 修改 `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffImportServiceImplTest.java`
- 增加部门存在、停用、删除三类校验测试。
- 修改 `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java`
- 增加混合导入场景测试,验证合法员工成功入库、非法 `deptId` 写入失败记录且任务状态为 `PARTIAL_SUCCESS`
## 验证
- 定向单测:
- `mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiBaseStaffImportServiceImplTest,CcdiBaseStaffDualImportServiceTest test`
- 编译校验:
- `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile`
## 影响范围
- 员工信息导入后端异步校验逻辑
- 系统部门主键查询字段映射
- 员工导入相关单元测试

View File

@@ -0,0 +1,23 @@
# 招投标供应商校验后端实施计划
## 目标
- 让招投标信息维护页面的新增、编辑接口仅保留供应商名称和统一信用代码必填校验。
- 移除供应商联系人、联系电话、银行账户,以及供应商名称/统一信用代码的内容格式校验,避免页面保存被接口层拦截。
## 实施内容
- 调整 `CcdiPurchaseTransactionSupplierDTO`
- 保留 `supplierName``@NotBlank`
-`supplierUscc` 增加 `@NotBlank` 必填校验。
- 移除 `supplierName` 的长度校验。
- 移除 `supplierUscc` 的格式校验。
- 移除 `contactPerson``contactPhone``supplierBankAccount` 的内容校验注解。
## 验证
- `mvn -pl ccdi-info-collection -am -DskipTests compile`
- `sh bin/restart_java_backend.sh`
- 结合真实页面验证:
- 新增弹窗提交 `supplierUscc=ABC``contactPhone=123` 成功
- 编辑弹窗提交 `supplierUscc=XYZ``contactPhone=abc123` 成功
## 产出文件
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiPurchaseTransactionSupplierDTO.java`

View File

@@ -0,0 +1,50 @@
# 实体库管理新增弹窗后端实施计划
## 文档路径确认
- 后端实施计划保存路径:`docs/plans/backend/`
- 本文档文件名:`2026-04-23-enterprise-base-info-add-dialog-backend-implementation.md`
## 需求目标
- 实体库管理新增场景不再要求前端传入数据来源。
- 新增时由后端自动写入数据来源,人工新增统一落为 `MANUAL`
- 编辑时后端保留原有数据来源,不允许通过请求修改。
- 经营状态调整为非必填项。
- 导入模板同步去除数据来源输入列,并将经营状态改为非必填展示。
- 导入时由后端自动写入 `IMPORT`,不再依赖模板传值。
## 实施范围
- `CcdiEnterpriseBaseInfoAddDTO`
- `CcdiEnterpriseBaseInfoEditDTO`
- `CcdiEnterpriseBaseInfoServiceImpl`
- `CcdiEnterpriseBaseInfoImportServiceImpl`
- `CcdiEnterpriseBaseInfoExcel`
## 实施步骤
1. 去除新增 DTO 中 `status``dataSource` 的必填约束,编辑 DTO 中去除 `status` 的必填约束。
2. 调整新增服务逻辑,仅校验风险等级和企业来源,新增数据时后端固定写入 `MANUAL`
3. 调整编辑服务逻辑,更新时始终沿用数据库中的原始数据来源。
4. 调整状态字段赋值逻辑,将空白字符串统一收敛为 `null`,避免出现空串脏数据。
5. 调整导入服务逻辑,去掉对模板数据来源的解析和校验,导入时固定写入 `IMPORT`
6. 修改导入模板对象,移除数据来源列,并把“经营状态*”调整为“经营状态”。
7. 补充/更新单元测试,覆盖新增自动写入 `MANUAL`、编辑保留原数据来源、导入自动写入 `IMPORT`、经营状态非必填和模板表头变更。
## 验证方案
- 执行定向测试:
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiEnterpriseBaseInfoServiceImplTest,CcdiEnterpriseBaseInfoImportServiceImplTest,EasyExcelUtilTemplateTest test`
- 通过真实后端接口验证:
- 新增不传 `dataSource`、不传 `status` 仍可成功
- 查询结果中 `dataSource=MANUAL`
- 删除测试数据成功
- 下载导入模板并核对首行表头:
- 包含 `经营状态`
- 不包含 `数据来源`
## 风险与注意事项
- 当前工作区存在其他未提交改动,本次仅处理实体库管理相关文件,不回退无关内容。
- 导入模板对象同时承担导入解析职责,本次变更后模板和导入字段保持一致。

Some files were not shown because too many files have changed in this diff Show More