feat 员工亲属关系

This commit is contained in:
wkc
2026-02-10 00:30:06 +08:00
parent 9a7fedcd74
commit bf19a9daa8
22 changed files with 3312 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
-- 修复员工基础表和员工家庭关系表字符集排序规则
-- 问题: ccdi_staff_fmy_relation.person_id (utf8mb4_general_ci) 与 ccdi_base_staff.id_card (utf8mb4_unicode_ci) 字符集不一致
-- 解决方案: 统一为 utf8mb4_general_ci
-- 执行时间: 2026-02-09
USE `ruoyi-vue-pro`;
-- 修改 ccdi_base_staff 表的 id_card 字段字符集,统一为 utf8mb4_general_ci
ALTER TABLE `ccdi_base_staff`
MODIFY COLUMN `id_card` VARCHAR(100) DEFAULT NULL COLLATE `utf8mb4_general_ci` COMMENT '身份证号';
-- 验证修改
SELECT
TABLE_NAME,
COLUMN_NAME,
CHARACTER_SET_NAME,
COLLATION_NAME
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = 'ruoyi-vue-pro'
AND TABLE_NAME IN ('ccdi_staff_fmy_relation', 'ccdi_base_staff')
AND COLUMN_NAME IN ('person_id', 'id_card')
ORDER BY
TABLE_NAME, COLUMN_NAME;
-- 说明: 执行后两个表的字符集应该都显示为 utf8mb4_general_ci

View File

@@ -0,0 +1,120 @@
-- ========================================
-- 验证员工家庭关系表 JOIN 字符集问题修复
-- ========================================
-- 目的: 验证 ccdi_staff_fmy_relation 与 ccdi_base_staff 表的 JOIN 操作正常
-- 创建时间: 2026-02-09
-- ========================================
USE `ccdi`;
-- 测试 1: 验证字符集统一性
-- ========================================
SELECT '=== 字符集验证 ===' AS test_section;
SELECT
TABLE_NAME AS '表名',
COLUMN_NAME AS '字段名',
CHARACTER_SET_NAME AS '字符集',
COLLATION_NAME AS '排序规则',
CASE
WHEN COLLATION_NAME = 'utf8mb4_general_ci' THEN '✅ 正确'
ELSE '❌ 错误'
END AS '状态'
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = 'ccdi'
AND TABLE_NAME IN ('ccdi_staff_fmy_relation', 'ccdi_base_staff')
AND COLUMN_NAME IN ('person_id', 'id_card')
ORDER BY
TABLE_NAME, COLUMN_NAME;
-- 测试 2: 验证 JOIN 操作(模拟实际查询)
-- ========================================
SELECT '=== JOIN 操作验证 ===' AS test_section;
SELECT
r.id,
r.person_id,
s.name AS person_name,
r.relation_type,
r.relation_name,
r.gender,
s.id_card AS staff_id_card,
CASE
WHEN r.person_id = s.id_card THEN '✅ 匹配成功'
ELSE '❌ 匹配失败'
END AS join_status
FROM
ccdi_staff_fmy_relation r
LEFT JOIN ccdi_base_staff s ON r.person_id = s.id_card
WHERE
r.is_emp_family = 1
LIMIT 5;
-- 测试 3: 统计数据
-- ========================================
SELECT '=== 数据统计 ===' AS test_section;
SELECT
COUNT(*) AS total_records,
COUNT(s.name) AS matched_records,
COUNT(*) - COUNT(s.name) AS unmatched_records,
CONCAT(
ROUND(COUNT(s.name) / COUNT(*) * 100, 2),
'%'
) AS match_rate
FROM
ccdi_staff_fmy_relation r
LEFT JOIN ccdi_base_staff s ON r.person_id = s.id_card
WHERE
r.is_emp_family = 1;
-- 测试 4: 查找未匹配的记录
-- ========================================
SELECT '=== 未匹配记录 ===' AS test_section;
SELECT
r.id,
r.person_id,
r.relation_name,
'未找到对应员工信息' AS warning
FROM
ccdi_staff_fmy_relation r
LEFT JOIN ccdi_base_staff s ON r.person_id = s.id_card
WHERE
r.is_emp_family = 1
AND s.id_card IS NULL
LIMIT 5;
-- 测试 5: 字符集转换测试
-- ========================================
SELECT '=== 字符集转换测试 ===' AS test_section;
SELECT
r.person_id,
s.id_card,
CHAR_LENGTH(r.person_id) AS len_person_id,
CHAR_LENGTH(s.id_card) AS len_id_card,
HEX(r.person_id) AS hex_person_id,
HEX(s.id_card) AS hex_id_card,
CASE
WHEN r.person_id = s.id_card THEN '✅ 完全相等'
ELSE '❌ 不相等'
END AS compare_result
FROM
ccdi_staff_fmy_relation r
INNER JOIN ccdi_base_staff s ON r.person_id = s.id_card
WHERE
r.is_emp_family = 1
LIMIT 3;
-- ========================================
-- 验证结果说明:
-- ========================================
-- 1. 字符集验证: 所有字段应显示 ✅ 正确
-- 2. JOIN 操作: 应能正常执行,无字符集冲突错误
-- 3. 数据统计: 显示匹配率
-- 4. 未匹配记录: 显示哪些员工关系记录未关联到员工
-- 5. 字符集转换: 验证字段在 JOIN 时能正确比较
-- ========================================

View File

@@ -0,0 +1,195 @@
package com.ruoyi.ccdi.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.ccdi.domain.vo.ImportResultVO;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.StaffFmyRelationImportFailureVO;
import com.ruoyi.ccdi.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.ccdi.service.ICcdiStaffFmyRelationService;
import com.ruoyi.ccdi.utils.EasyExcelUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.enums.BusinessType;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 员工亲属关系Controller
*
* @author ruoyi
* @date 2026-02-09
*/
@Tag(name = "员工亲属关系管理")
@RestController
@RequestMapping("/ccdi/staffFmyRelation")
public class CcdiStaffFmyRelationController extends BaseController {
@Resource
private ICcdiStaffFmyRelationService relationService;
@Resource
private ICcdiStaffFmyRelationImportService relationImportService;
/**
* 查询员工亲属关系列表
*/
@Operation(summary = "查询员工亲属关系列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:list')")
@GetMapping("/list")
public TableDataInfo list(CcdiStaffFmyRelationQueryDTO queryDTO) {
// 使用MyBatis Plus分页
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiStaffFmyRelationVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiStaffFmyRelationVO> result = relationService.selectRelationPage(page, queryDTO);
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, "员工亲属关系信息");
}
/**
* 获取员工亲属关系详细信息
*/
@Operation(summary = "获取员工亲属关系详细信息")
@Parameter(name = "id", description = "主键ID", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
return success(relationService.selectRelationById(id));
}
/**
* 新增员工亲属关系
*/
@Operation(summary = "新增员工亲属关系")
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:add')")
@Log(title = "员工亲属关系", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiStaffFmyRelationAddDTO addDTO) {
return toAjax(relationService.insertRelation(addDTO));
}
/**
* 修改员工亲属关系
*/
@Operation(summary = "修改员工亲属关系")
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:edit')")
@Log(title = "员工亲属关系", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiStaffFmyRelationEditDTO editDTO) {
return toAjax(relationService.updateRelation(editDTO));
}
/**
* 删除员工亲属关系
*/
@Operation(summary = "删除员工亲属关系")
@Parameter(name = "ids", description = "主键ID数组", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:remove')")
@Log(title = "员工亲属关系", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(relationService.deleteRelationByIds(ids));
}
/**
* 下载带字典下拉框的导入模板
* 使用@DictDropdown注解自动添加下拉框
*/
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffFmyRelationExcel.class, "员工亲属关系信息");
}
/**
* 异步导入员工亲属关系
*/
@Operation(summary = "异步导入员工亲属关系")
@Parameter(name = "file", description = "导入文件", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:import')")
@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);
if (list == null || list.isEmpty()) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = relationService.importRelation(list);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
/**
* 查询导入状态
*/
@Operation(summary = "查询导入状态")
@Parameter(name = "taskId", description = "任务ID", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:import')")
@GetMapping("/importStatus/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
ImportStatusVO statusVO = relationImportService.getImportStatus(taskId);
return success(statusVO);
}
/**
* 查询导入失败记录
*/
@Operation(summary = "查询导入失败记录")
@Parameter(name = "taskId", description = "任务ID", required = true)
@Parameter(name = "pageNum", description = "页码", required = false)
@Parameter(name = "pageSize", description = "每页条数", required = false)
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:import')")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
List<StaffFmyRelationImportFailureVO> failures = relationImportService.getImportFailures(taskId);
// 手动分页
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, failures.size());
List<StaffFmyRelationImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
return getDataTable(pageData, failures.size());
}
}

View File

@@ -0,0 +1,107 @@
package com.ruoyi.ccdi.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_staff_fmy_relation
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
public class CcdiStaffFmyRelation implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 员工身份证号 */
private String personId;
/** 关系类型 */
private String relationType;
/** 关系人姓名 */
private String relationName;
/** 性别M-男F-女O-其他 */
private String gender;
/** 出生日期 */
private Date birthDate;
/** 关系人证件类型 */
private String relationCertType;
/** 关系人证件号码 */
private String relationCertNo;
/** 手机号码1 */
private String mobilePhone1;
/** 手机号码2 */
private String mobilePhone2;
/** 微信名称1 */
private String wechatNo1;
/** 微信名称2 */
private String wechatNo2;
/** 微信名称3 */
private String wechatNo3;
/** 详细联系地址 */
private String contactAddress;
/** 关系详细描述 */
private String relationDesc;
/** 生效日期 */
private Date effectiveDate;
/** 失效日期 */
private Date invalidDate;
/** 状态0-无效1-有效 */
private Integer status;
/** 备注 */
private String remark;
/** 数据来源MANUAL-手工录入IMPORT-导入 */
private String dataSource;
/** 是否是员工亲属0-否1-是 */
private Boolean isEmpFamily;
/** 是否是客户亲属0-否1-是 */
private Boolean isCustFamily;
/** 创建时间 */
@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

@@ -0,0 +1,119 @@
package com.ruoyi.ccdi.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工亲属关系新增DTO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工亲属关系新增")
public class CcdiStaffFmyRelationAddDTO 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}[\\dXx]$", message = "员工身份证号格式不正确")
@Schema(description = "员工身份证号")
private String personId;
/** 关系类型 */
@NotBlank(message = "关系类型不能为空")
@Size(max = 50, message = "关系类型长度不能超过50个字符")
@Schema(description = "关系类型")
private String relationType;
/** 关系人姓名 */
@NotBlank(message = "关系人姓名不能为空")
@Size(max = 100, message = "关系人姓名长度不能超过100个字符")
@Schema(description = "关系人姓名")
private String relationName;
/** 性别 */
@Pattern(regexp = "^[MFO]$", message = "性别只能是M、F或O")
@Schema(description = "性别M-男F-女O-其他")
private String gender;
/** 出生日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "出生日期")
private Date birthDate;
/** 关系人证件类型 */
@NotBlank(message = "关系人证件类型不能为空")
@Size(max = 50, message = "关系人证件类型长度不能超过50个字符")
@Schema(description = "关系人证件类型")
private String relationCertType;
/** 关系人证件号码 */
@NotBlank(message = "关系人证件号码不能为空")
@Size(max = 100, message = "关系人证件号码长度不能超过100个字符")
@Schema(description = "关系人证件号码")
private String relationCertNo;
/** 手机号码1 */
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码1格式不正确")
@Schema(description = "手机号码1")
private String mobilePhone1;
/** 手机号码2 */
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码2格式不正确")
@Schema(description = "手机号码2")
private String mobilePhone2;
/** 微信名称1 */
@Size(max = 50, message = "微信名称1长度不能超过50个字符")
@Schema(description = "微信名称1")
private String wechatNo1;
/** 微信名称2 */
@Size(max = 50, message = "微信名称2长度不能超过50个字符")
@Schema(description = "微信名称2")
private String wechatNo2;
/** 微信名称3 */
@Size(max = 50, message = "微信名称3长度不能超过50个字符")
@Schema(description = "微信名称3")
private String wechatNo3;
/** 详细联系地址 */
@Size(max = 500, message = "详细联系地址长度不能超过500个字符")
@Schema(description = "详细联系地址")
private String contactAddress;
/** 关系详细描述 */
@Size(max = 500, message = "关系详细描述长度不能超过500个字符")
@Schema(description = "关系详细描述")
private String relationDesc;
/** 生效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期")
private Date effectiveDate;
/** 失效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "失效日期")
private Date invalidDate;
/** 状态 */
@Schema(description = "状态0-无效1-有效")
private Integer status;
/** 备注 */
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,125 @@
package com.ruoyi.ccdi.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工亲属关系编辑DTO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工亲属关系编辑")
public class CcdiStaffFmyRelationEditDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@NotNull(message = "ID不能为空")
@Schema(description = "主键ID")
private Long id;
/** 员工身份证号 */
@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}[\\dXx]$", message = "员工身份证号格式不正确")
@Schema(description = "员工身份证号")
private String personId;
/** 关系类型 */
@NotBlank(message = "关系类型不能为空")
@Size(max = 50, message = "关系类型长度不能超过50个字符")
@Schema(description = "关系类型")
private String relationType;
/** 关系人姓名 */
@NotBlank(message = "关系人姓名不能为空")
@Size(max = 100, message = "关系人姓名长度不能超过100个字符")
@Schema(description = "关系人姓名")
private String relationName;
/** 性别 */
@Pattern(regexp = "^[MFO]$", message = "性别只能是M、F或O")
@Schema(description = "性别M-男F-女O-其他")
private String gender;
/** 出生日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "出生日期")
private Date birthDate;
/** 关系人证件类型 */
@NotBlank(message = "关系人证件类型不能为空")
@Size(max = 50, message = "关系人证件类型长度不能超过50个字符")
@Schema(description = "关系人证件类型")
private String relationCertType;
/** 关系人证件号码 */
@NotBlank(message = "关系人证件号码不能为空")
@Size(max = 100, message = "关系人证件号码长度不能超过100个字符")
@Schema(description = "关系人证件号码")
private String relationCertNo;
/** 手机号码1 */
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码1格式不正确")
@Schema(description = "手机号码1")
private String mobilePhone1;
/** 手机号码2 */
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码2格式不正确")
@Schema(description = "手机号码2")
private String mobilePhone2;
/** 微信名称1 */
@Size(max = 50, message = "微信名称1长度不能超过50个字符")
@Schema(description = "微信名称1")
private String wechatNo1;
/** 微信名称2 */
@Size(max = 50, message = "微信名称2长度不能超过50个字符")
@Schema(description = "微信名称2")
private String wechatNo2;
/** 微信名称3 */
@Size(max = 50, message = "微信名称3长度不能超过50个字符")
@Schema(description = "微信名称3")
private String wechatNo3;
/** 详细联系地址 */
@Size(max = 500, message = "详细联系地址长度不能超过500个字符")
@Schema(description = "详细联系地址")
private String contactAddress;
/** 关系详细描述 */
@Size(max = 500, message = "关系详细描述长度不能超过500个字符")
@Schema(description = "关系详细描述")
private String relationDesc;
/** 生效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期")
private Date effectiveDate;
/** 失效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "失效日期")
private Date invalidDate;
/** 状态 */
@Schema(description = "状态0-无效1-有效")
private Integer status;
/** 备注 */
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,57 @@
package com.ruoyi.ccdi.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工亲属关系查询DTO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工亲属关系查询")
public class CcdiStaffFmyRelationQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工身份证号 */
@Schema(description = "员工身份证号")
private String personId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String personName;
/** 关系类型 */
@Schema(description = "关系类型")
private String relationType;
/** 关系人姓名 */
@Schema(description = "关系人姓名")
private String relationName;
/** 状态 */
@Schema(description = "状态0-无效1-有效")
private Integer status;
/** 数据来源 */
@Schema(description = "数据来源")
private String dataSource;
/** 生效日期开始 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期开始")
private Date effectiveDateStart;
/** 生效日期结束 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期结束")
private Date effectiveDateEnd;
}

View File

@@ -0,0 +1,116 @@
package com.ruoyi.ccdi.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导入导出对象
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
public class CcdiStaffFmyRelationExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工身份证号 */
@ExcelProperty(value = "员工身份证号*", index = 0)
@ColumnWidth(20)
@Required
private String personId;
/** 关系类型 */
@ExcelProperty(value = "关系类型*", index = 1)
@ColumnWidth(15)
@DictDropdown(dictType = "ccdi_relation_type")
@Required
private String relationType;
/** 关系人姓名 */
@ExcelProperty(value = "关系人姓名*", index = 2)
@ColumnWidth(15)
@Required
private String relationName;
/** 性别 */
@ExcelProperty(value = "性别", index = 3)
@ColumnWidth(10)
@DictDropdown(dictType = "ccdi_indiv_gender")
private String gender;
/** 关系人证件类型 */
@ExcelProperty(value = "关系人证件类型*", index = 4)
@ColumnWidth(15)
@DictDropdown(dictType = "ccdi_certificate_type")
@Required
private String relationCertType;
/** 关系人证件号码 */
@ExcelProperty(value = "关系人证件号码*", index = 5)
@ColumnWidth(20)
@Required
private String relationCertNo;
/** 手机号码1 */
@ExcelProperty(value = "手机号码1", index = 6)
@ColumnWidth(15)
private String mobilePhone1;
/** 手机号码2 */
@ExcelProperty(value = "手机号码2", index = 7)
@ColumnWidth(15)
private String mobilePhone2;
/** 微信名称1 */
@ExcelProperty(value = "微信名称1", index = 8)
@ColumnWidth(15)
private String wechatNo1;
/** 微信名称2 */
@ExcelProperty(value = "微信名称2", index = 9)
@ColumnWidth(15)
private String wechatNo2;
/** 微信名称3 */
@ExcelProperty(value = "微信名称3", index = 10)
@ColumnWidth(15)
private String wechatNo3;
/** 详细联系地址 */
@ExcelProperty(value = "详细联系地址", index = 11)
@ColumnWidth(30)
private String contactAddress;
/** 关系详细描述 */
@ExcelProperty(value = "关系详细描述", index = 12)
@ColumnWidth(30)
private String relationDesc;
/** 生效日期 */
@ExcelProperty(value = "生效日期", index = 13)
@ColumnWidth(15)
private String effectiveDate;
/** 失效日期 */
@ExcelProperty(value = "失效日期", index = 14)
@ColumnWidth(15)
private String invalidDate;
/** 状态 */
@ExcelProperty(value = "状态", index = 15)
@ColumnWidth(10)
private Integer status;
/** 备注 */
@ExcelProperty(value = "备注", index = 16)
@ColumnWidth(30)
private String remark;
}

View File

@@ -0,0 +1,144 @@
package com.ruoyi.ccdi.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工亲属关系VO
*
* @author ruoyi
* @date 2026-02-09
*/
@Data
@Schema(description = "员工亲属关系")
public class CcdiStaffFmyRelationVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@Schema(description = "主键ID")
private Long id;
/** 员工身份证号 */
@Schema(description = "员工身份证号")
private String personId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String personName;
/** 关系类型 */
@Schema(description = "关系类型")
private String relationType;
/** 关系人姓名 */
@Schema(description = "关系人姓名")
private String relationName;
/** 性别 */
@Schema(description = "性别")
private String gender;
/** 性别名称 */
@Schema(description = "性别名称")
private String genderName;
/** 出生日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "出生日期")
private Date birthDate;
/** 关系人证件类型 */
@Schema(description = "关系人证件类型")
private String relationCertType;
/** 关系人证件号码 */
@Schema(description = "关系人证件号码")
private String relationCertNo;
/** 手机号码1 */
@Schema(description = "手机号码1")
private String mobilePhone1;
/** 手机号码2 */
@Schema(description = "手机号码2")
private String mobilePhone2;
/** 微信名称1 */
@Schema(description = "微信名称1")
private String wechatNo1;
/** 微信名称2 */
@Schema(description = "微信名称2")
private String wechatNo2;
/** 微信名称3 */
@Schema(description = "微信名称3")
private String wechatNo3;
/** 详细联系地址 */
@Schema(description = "详细联系地址")
private String contactAddress;
/** 关系详细描述 */
@Schema(description = "关系详细描述")
private String relationDesc;
/** 生效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期")
private Date effectiveDate;
/** 失效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "失效日期")
private Date invalidDate;
/** 状态 */
@Schema(description = "状态")
private Integer status;
/** 状态名称 */
@Schema(description = "状态名称")
private String statusName;
/** 备注 */
@Schema(description = "备注")
private String remark;
/** 数据来源 */
@Schema(description = "数据来源")
private String dataSource;
/** 是否是员工亲属 */
@Schema(description = "是否是员工亲属")
private Boolean isEmpFamily;
/** 是否是客户亲属 */
@Schema(description = "是否是客户亲属")
private Boolean isCustFamily;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新时间")
private Date updateTime;
/** 创建人 */
@Schema(description = "创建人")
private String createdBy;
/** 更新人 */
@Schema(description = "更新人")
private String updatedBy;
}

View File

@@ -0,0 +1,53 @@
package com.ruoyi.ccdi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.CcdiStaffFmyRelation;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.vo.CcdiStaffFmyRelationVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 员工亲属关系 数据层
*
* @author ruoyi
* @date 2026-02-09
*/
public interface CcdiStaffFmyRelationMapper extends BaseMapper<CcdiStaffFmyRelation> {
/**
* 分页查询员工亲属关系列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 员工亲属关系VO分页结果
*/
Page<CcdiStaffFmyRelationVO> selectRelationPage(@Param("page") Page<CcdiStaffFmyRelationVO> page,
@Param("query") CcdiStaffFmyRelationQueryDTO queryDTO);
/**
* 查询员工亲属关系详情
*
* @param id 主键ID
* @return 员工亲属关系VO
*/
CcdiStaffFmyRelationVO selectRelationById(@Param("id") Long id);
/**
* 查询员工亲属关系列表(用于导出)
*
* @param queryDTO 查询条件
* @return 员工亲属关系VO列表
*/
List<CcdiStaffFmyRelationVO> selectRelationListForExport(@Param("query") CcdiStaffFmyRelationQueryDTO queryDTO);
/**
* 批量插入员工亲属关系数据
*
* @param list 员工亲属关系列表
* @return 插入行数
*/
int insertBatch(@Param("list") List<CcdiStaffFmyRelation> list);
}

View File

@@ -0,0 +1,41 @@
package com.ruoyi.ccdi.service;
import com.ruoyi.ccdi.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.StaffFmyRelationImportFailureVO;
import java.util.List;
/**
* 员工亲属关系异步导入 服务层
*
* @author ruoyi
* @date 2026-02-09
*/
public interface ICcdiStaffFmyRelationImportService {
/**
* 异步导入员工亲属关系数据
*
* @param excelList Excel实体列表
* @param taskId 任务ID
* @param userName 用户名
*/
void importRelationAsync(List<CcdiStaffFmyRelationExcel> excelList, String taskId, String userName);
/**
* 查询导入失败记录
*
* @param taskId 任务ID
* @return 失败记录列表
*/
List<StaffFmyRelationImportFailureVO> getImportFailures(String taskId);
/**
* 查询导入状态
*
* @param taskId 任务ID
* @return 导入状态信息
*/
ImportStatusVO getImportStatus(String taskId);
}

View File

@@ -0,0 +1,84 @@
package com.ruoyi.ccdi.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CcdiStaffFmyRelationVO;
import java.util.List;
/**
* 员工亲属关系 服务层
*
* @author ruoyi
* @date 2026-02-09
*/
public interface ICcdiStaffFmyRelationService {
/**
* 查询员工亲属关系列表
*
* @param queryDTO 查询条件
* @return 员工亲属关系VO集合
*/
List<CcdiStaffFmyRelationVO> selectRelationList(CcdiStaffFmyRelationQueryDTO queryDTO);
/**
* 分页查询员工亲属关系列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 员工亲属关系VO分页结果
*/
Page<CcdiStaffFmyRelationVO> selectRelationPage(Page<CcdiStaffFmyRelationVO> page, CcdiStaffFmyRelationQueryDTO queryDTO);
/**
* 查询员工亲属关系详情
*
* @param id 主键ID
* @return 员工亲属关系VO
*/
CcdiStaffFmyRelationVO selectRelationById(Long id);
/**
* 新增员工亲属关系
*
* @param addDTO 新增DTO
* @return 结果
*/
int insertRelation(CcdiStaffFmyRelationAddDTO addDTO);
/**
* 修改员工亲属关系
*
* @param editDTO 编辑DTO
* @return 结果
*/
int updateRelation(CcdiStaffFmyRelationEditDTO editDTO);
/**
* 批量删除员工亲属关系
*
* @param ids 需要删除的主键ID
* @return 结果
*/
int deleteRelationByIds(Long[] ids);
/**
* 查询员工亲属关系列表(用于导出)
*
* @param queryDTO 查询条件
* @return 员工亲属关系Excel实体集合
*/
List<CcdiStaffFmyRelationExcel> selectRelationListForExport(CcdiStaffFmyRelationQueryDTO queryDTO);
/**
* 导入员工亲属关系数据(异步)
*
* @param excelList Excel实体列表
* @return 任务ID
*/
String importRelation(List<CcdiStaffFmyRelationExcel> excelList);
}

View File

@@ -0,0 +1,237 @@
package com.ruoyi.ccdi.service.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.ccdi.domain.CcdiStaffFmyRelation;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.ccdi.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.ImportResult;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.StaffFmyRelationImportFailureVO;
import com.ruoyi.ccdi.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.ccdi.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
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;
/**
* 员工亲属关系异步导入服务层处理
*
* @author ruoyi
* @date 2026-02-09
*/
@Service
@EnableAsync
public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelationImportService {
@Resource
private CcdiStaffFmyRelationMapper relationMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async
@Transactional
public void importRelationAsync(List<CcdiStaffFmyRelationExcel> excelList, String taskId, String userName) {
List<CcdiStaffFmyRelation> newRecords = new ArrayList<>();
List<StaffFmyRelationImportFailureVO> failures = new ArrayList<>();
// 用于跟踪Excel文件内已处理的唯一键personId + relationCertNo
Set<String> processedKeys = new HashSet<>();
// 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffFmyRelationExcel excel = excelList.get(i);
try {
// 转换为AddDTO进行验证
CcdiStaffFmyRelationAddDTO addDTO = new CcdiStaffFmyRelationAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 验证数据
validateRelationData(addDTO);
// 生成唯一键
String uniqueKey = excel.getPersonId() + "|" + excel.getRelationCertNo();
// 检查是否在文件内重复
if (processedKeys.contains(uniqueKey)) {
throw new RuntimeException(String.format("员工[%s]的关系人[%s]在导入文件中重复", excel.getPersonId(), excel.getRelationCertNo()));
}
CcdiStaffFmyRelation relation = new CcdiStaffFmyRelation();
BeanUtils.copyProperties(excel, relation);
relation.setCreatedBy(userName);
relation.setUpdatedBy(userName);
relation.setIsEmpFamily(true);
relation.setIsCustFamily(false);
relation.setDataSource("IMPORT");
if (relation.getStatus() == null) {
relation.setStatus(1);
}
newRecords.add(relation);
processedKeys.add(uniqueKey);
} catch (Exception e) {
StaffFmyRelationImportFailureVO failure = new StaffFmyRelationImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
}
}
// 批量插入新数据
if (!newRecords.isEmpty()) {
saveBatch(newRecords, 500);
}
// 保存失败记录到Redis
if (!failures.isEmpty()) {
String failuresKey = "import:staffFmyRelation:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size());
result.setFailureCount(failures.size());
// 更新最终状态
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
updateImportStatus(taskId, finalStatus, result);
}
/**
* 获取导入失败记录
*
* @param taskId 任务ID
* @return 失败记录列表
*/
@Override
public List<StaffFmyRelationImportFailureVO> getImportFailures(String taskId) {
String key = "import:staffFmyRelation:" + taskId + ":failures";
Object failuresObj = redisTemplate.opsForValue().get(key);
if (failuresObj == null) {
return Collections.emptyList();
}
return JSON.parseArray(JSON.toJSONString(failuresObj), StaffFmyRelationImportFailureVO.class);
}
/**
* 查询导入状态
*
* @param taskId 任务ID
* @return 导入状态信息
*/
@Override
public ImportStatusVO getImportStatus(String taskId) {
String key = "import:staffFmyRelation:" + taskId;
Boolean hasKey = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(hasKey)) {
throw new RuntimeException("任务不存在或已过期");
}
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
ImportStatusVO statusVO = new ImportStatusVO();
statusVO.setTaskId((String) statusMap.get("taskId"));
statusVO.setStatus((String) statusMap.get("status"));
statusVO.setTotalCount((Integer) statusMap.get("totalCount"));
statusVO.setSuccessCount((Integer) statusMap.get("successCount"));
statusVO.setFailureCount((Integer) statusMap.get("failureCount"));
statusVO.setProgress((Integer) statusMap.get("progress"));
statusVO.setStartTime((Long) statusMap.get("startTime"));
statusVO.setEndTime((Long) statusMap.get("endTime"));
statusVO.setMessage((String) statusMap.get("message"));
return statusVO;
}
/**
* 更新导入状态
*/
private void updateImportStatus(String taskId, String status, ImportResult result) {
String key = "import:staffFmyRelation:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("status", status);
statusData.put("successCount", result.getSuccessCount());
statusData.put("failureCount", result.getFailureCount());
statusData.put("progress", 100);
statusData.put("endTime", System.currentTimeMillis());
if ("SUCCESS".equals(status)) {
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
} else {
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "");
}
redisTemplate.opsForHash().putAll(key, statusData);
}
/**
* 批量保存
*/
private void saveBatch(List<CcdiStaffFmyRelation> list, int batchSize) {
// 使用真正的批量插入,分批次执行以提高性能
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiStaffFmyRelation> subList = list.subList(i, end);
relationMapper.insertBatch(subList);
}
}
/**
* 验证员工亲属关系数据
*
* @param addDTO 新增DTO
*/
private void validateRelationData(CcdiStaffFmyRelationAddDTO addDTO) {
// 验证必填字段
if (StringUtils.isEmpty(addDTO.getPersonId())) {
throw new RuntimeException("员工身份证号不能为空");
}
if (StringUtils.isEmpty(addDTO.getRelationType())) {
throw new RuntimeException("关系类型不能为空");
}
if (StringUtils.isEmpty(addDTO.getRelationName())) {
throw new RuntimeException("关系人姓名不能为空");
}
if (StringUtils.isEmpty(addDTO.getRelationCertType())) {
throw new RuntimeException("关系人证件类型不能为空");
}
if (StringUtils.isEmpty(addDTO.getRelationCertNo())) {
throw new RuntimeException("关系人证件号码不能为空");
}
// 验证身份证号格式
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}[\\dXx]$")) {
throw new RuntimeException("员工身份证号格式不正确");
}
// 验证手机号格式(如果提供)
if (StringUtils.isNotEmpty(addDTO.getMobilePhone1()) && !addDTO.getMobilePhone1().matches("^1[3-9]\\d{9}$")) {
throw new RuntimeException("手机号码1格式不正确");
}
if (StringUtils.isNotEmpty(addDTO.getMobilePhone2()) && !addDTO.getMobilePhone2().matches("^1[3-9]\\d{9}$")) {
throw new RuntimeException("手机号码2格式不正确");
}
// 验证性别值(如果提供)
if (StringUtils.isNotEmpty(addDTO.getGender()) && !addDTO.getGender().matches("^[MFO]$")) {
throw new RuntimeException("性别只能是M、F或O");
}
}
}

View File

@@ -0,0 +1,187 @@
package com.ruoyi.ccdi.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.CcdiStaffFmyRelation;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.ccdi.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.ccdi.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.ccdi.service.ICcdiStaffFmyRelationService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 员工亲属关系 服务层处理
*
* @author ruoyi
* @date 2026-02-09
*/
@Service
public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationService {
@Resource
private CcdiStaffFmyRelationMapper relationMapper;
@Resource
private ICcdiStaffFmyRelationImportService relationImportService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 查询员工亲属关系列表
*
* @param queryDTO 查询条件
* @return 员工亲属关系VO集合
*/
@Override
public java.util.List<CcdiStaffFmyRelationVO> selectRelationList(CcdiStaffFmyRelationQueryDTO queryDTO) {
Page<CcdiStaffFmyRelationVO> page = new Page<>(1, Integer.MAX_VALUE);
Page<CcdiStaffFmyRelationVO> resultPage = relationMapper.selectRelationPage(page, queryDTO);
return resultPage.getRecords();
}
/**
* 分页查询员工亲属关系列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 员工亲属关系VO分页结果
*/
@Override
public Page<CcdiStaffFmyRelationVO> selectRelationPage(Page<CcdiStaffFmyRelationVO> page, CcdiStaffFmyRelationQueryDTO queryDTO) {
return relationMapper.selectRelationPage(page, queryDTO);
}
/**
* 查询员工亲属关系列表(用于导出)
*
* @param queryDTO 查询条件
* @return 员工亲属关系Excel实体集合
*/
@Override
public java.util.List<CcdiStaffFmyRelationExcel> selectRelationListForExport(CcdiStaffFmyRelationQueryDTO queryDTO) {
return relationMapper.selectRelationListForExport(queryDTO).stream().map(vo -> {
CcdiStaffFmyRelationExcel excel = new CcdiStaffFmyRelationExcel();
BeanUtils.copyProperties(vo, excel);
return excel;
}).collect(Collectors.toList());
}
/**
* 查询员工亲属关系详情
*
* @param id 主键ID
* @return 员工亲属关系VO
*/
@Override
public CcdiStaffFmyRelationVO selectRelationById(Long id) {
return relationMapper.selectRelationById(id);
}
/**
* 新增员工亲属关系
*
* @param addDTO 新增DTO
* @return 结果
*/
@Override
@Transactional
public int insertRelation(CcdiStaffFmyRelationAddDTO addDTO) {
CcdiStaffFmyRelation relation = new CcdiStaffFmyRelation();
BeanUtils.copyProperties(addDTO, relation);
// 设置默认值
relation.setIsEmpFamily(true);
relation.setIsCustFamily(false);
relation.setDataSource("MANUAL");
if (relation.getStatus() == null) {
relation.setStatus(1);
}
int result = relationMapper.insert(relation);
return result;
}
/**
* 修改员工亲属关系
*
* @param editDTO 编辑DTO
* @return 结果
*/
@Override
@Transactional
public int updateRelation(CcdiStaffFmyRelationEditDTO editDTO) {
CcdiStaffFmyRelation relation = new CcdiStaffFmyRelation();
BeanUtils.copyProperties(editDTO, relation);
int result = relationMapper.updateById(relation);
return result;
}
/**
* 批量删除员工亲属关系
*
* @param ids 需要删除的主键ID
* @return 结果
*/
@Override
@Transactional
public int deleteRelationByIds(Long[] ids) {
return relationMapper.deleteBatchIds(java.util.List.of(ids));
}
/**
* 导入员工亲属关系数据(异步)
*
* @param excelList Excel实体列表
* @return 任务ID
*/
@Override
@Transactional
public String importRelation(java.util.List<CcdiStaffFmyRelationExcel> excelList) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
throw new RuntimeException("至少需要一条数据");
}
// 生成任务ID
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 获取当前用户名
String userName = SecurityUtils.getUsername();
// 初始化Redis状态
String statusKey = "import:staffFmyRelation:" + 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);
// 调用异步导入服务
relationImportService.importRelationAsync(excelList, taskId, userName);
return taskId;
}
}

View File

@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.mapper.CcdiStaffFmyRelationMapper">
<!-- 员工亲属关系ResultMap -->
<resultMap type="com.ruoyi.ccdi.domain.vo.CcdiStaffFmyRelationVO" id="CcdiStaffFmyRelationVOResult">
<id property="id" column="id"/>
<result property="personId" column="person_id"/>
<result property="personName" column="person_name"/>
<result property="relationType" column="relation_type"/>
<result property="relationName" column="relation_name"/>
<result property="gender" column="gender"/>
<result property="birthDate" column="birth_date"/>
<result property="relationCertType" column="relation_cert_type"/>
<result property="relationCertNo" column="relation_cert_no"/>
<result property="mobilePhone1" column="mobile_phone1"/>
<result property="mobilePhone2" column="mobile_phone2"/>
<result property="wechatNo1" column="wechat_no1"/>
<result property="wechatNo2" column="wechat_no2"/>
<result property="wechatNo3" column="wechat_no3"/>
<result property="contactAddress" column="contact_address"/>
<result property="relationDesc" column="relation_desc"/>
<result property="effectiveDate" column="effective_date"/>
<result property="invalidDate" column="invalid_date"/>
<result property="status" column="status"/>
<result property="remark" column="remark"/>
<result property="dataSource" column="data_source"/>
<result property="isEmpFamily" column="is_emp_family"/>
<result property="isCustFamily" column="is_cust_family"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="createdBy" column="created_by"/>
<result property="updatedBy" column="updated_by"/>
</resultMap>
<!-- 分页查询员工亲属关系列表 -->
<select id="selectRelationPage" resultMap="CcdiStaffFmyRelationVOResult">
SELECT
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
r.created_by, r.create_time, r.updated_by, r.update_time
FROM ccdi_staff_fmy_relation r
LEFT JOIN ccdi_base_staff s ON r.person_id = s.id_card
<where>
r.is_emp_family = 1
<if test="query.personId != null and query.personId != ''">
AND r.person_id = #{query.personId}
</if>
<if test="query.personName != null and query.personName != ''">
AND s.name LIKE CONCAT('%', #{query.personName}, '%')
</if>
<if test="query.relationType != null and query.relationType != ''">
AND r.relation_type = #{query.relationType}
</if>
<if test="query.relationName != null and query.relationName != ''">
AND r.relation_name LIKE CONCAT('%', #{query.relationName}, '%')
</if>
<if test="query.status != null">
AND r.status = #{query.status}
</if>
<if test="query.dataSource != null and query.dataSource != ''">
AND r.data_source = #{query.dataSource}
</if>
<if test="query.effectiveDateStart != null">
AND r.effective_date &gt;= #{query.effectiveDateStart}
</if>
<if test="query.effectiveDateEnd != null">
AND r.effective_date &lt;= #{query.effectiveDateEnd}
</if>
</where>
ORDER BY r.create_time DESC
</select>
<!-- 查询员工亲属关系详情 -->
<select id="selectRelationById" resultMap="CcdiStaffFmyRelationVOResult">
SELECT
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
r.created_by, r.create_time, r.updated_by, r.update_time
FROM ccdi_staff_fmy_relation r
LEFT JOIN ccdi_base_staff s ON r.person_id = s.id_card
WHERE r.id = #{id}
</select>
<!-- 查询员工亲属关系列表(用于导出) -->
<select id="selectRelationListForExport" resultMap="CcdiStaffFmyRelationVOResult">
SELECT
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
r.created_by, r.create_time, r.updated_by, r.update_time
FROM ccdi_staff_fmy_relation r
LEFT JOIN ccdi_base_staff s ON r.person_id = s.id_card
<where>
r.is_emp_family = 1
<if test="query.personId != null and query.personId != ''">
AND r.person_id = #{query.personId}
</if>
<if test="query.personName != null and query.personName != ''">
AND s.name LIKE CONCAT('%', #{query.personName}, '%')
</if>
<if test="query.relationType != null and query.relationType != ''">
AND r.relation_type = #{query.relationType}
</if>
<if test="query.relationName != null and query.relationName != ''">
AND r.relation_name LIKE CONCAT('%', #{query.relationName}, '%')
</if>
<if test="query.status != null">
AND r.status = #{query.status}
</if>
<if test="query.dataSource != null and query.dataSource != ''">
AND r.data_source = #{query.dataSource}
</if>
<if test="query.effectiveDateStart != null">
AND r.effective_date &gt;= #{query.effectiveDateStart}
</if>
<if test="query.effectiveDateEnd != null">
AND r.effective_date &lt;= #{query.effectiveDateEnd}
</if>
</where>
ORDER BY r.create_time DESC
</select>
<!-- 批量插入员工亲属关系数据 -->
<insert id="insertBatch">
INSERT INTO ccdi_staff_fmy_relation
(person_id, relation_type, relation_name, gender, birth_date, relation_cert_type, relation_cert_no,
mobile_phone1, mobile_phone2, wechat_no1, wechat_no2, wechat_no3, contact_address, relation_desc,
effective_date, invalid_date, status, remark, data_source, is_emp_family, is_cust_family,
created_by, create_time, updated_by, update_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.personId}, #{item.relationType}, #{item.relationName}, #{item.gender},
#{item.birthDate}, #{item.relationCertType}, #{item.relationCertNo}, #{item.mobilePhone1},
#{item.mobilePhone2}, #{item.wechatNo1}, #{item.wechatNo2}, #{item.wechatNo3}, #{item.contactAddress},
#{item.relationDesc}, #{item.effectiveDate}, #{item.invalidDate}, #{item.status}, #{item.remark},
#{item.dataSource}, #{item.isEmpFamily}, #{item.isCustFamily}, #{item.createdBy}, NOW(), #{item.updatedBy}, NOW())
</foreach>
</insert>
</mapper>

View File

@@ -0,0 +1,99 @@
import request from '@/utils/request'
// 查询员工亲属关系列表
export function listRelation(query) {
return request({
url: '/ccdi/staffFmyRelation/list',
method: 'get',
params: query
})
}
// 查询员工亲属关系详情
export function getRelation(id) {
return request({
url: '/ccdi/staffFmyRelation/' + id,
method: 'get'
})
}
// 新增员工亲属关系
export function addRelation(data) {
return request({
url: '/ccdi/staffFmyRelation',
method: 'post',
data: data
})
}
// 修改员工亲属关系
export function updateRelation(data) {
return request({
url: '/ccdi/staffFmyRelation',
method: 'put',
data: data
})
}
// 删除员工亲属关系
export function delRelation(ids) {
return request({
url: '/ccdi/staffFmyRelation/' + ids,
method: 'delete'
})
}
// 导出员工亲属关系
export function exportRelation(query) {
return request({
url: '/ccdi/staffFmyRelation/export',
method: 'post',
params: query
})
}
// 下载导入模板
export function importTemplate() {
return request({
url: '/ccdi/staffFmyRelation/importTemplate',
method: 'post'
})
}
// 导入员工亲属关系
export function importData(file, updateSupport) {
const formData = new FormData()
formData.append('file', file)
formData.append('updateSupport', updateSupport)
return request({
url: '/ccdi/staffFmyRelation/importData',
method: 'post',
data: formData
})
}
// 查询导入状态
export function getImportStatus(taskId) {
return request({
url: '/ccdi/staffFmyRelation/importStatus/' + taskId,
method: 'get'
})
}
// 查询导入失败记录
export function getImportFailures(taskId, pageNum, pageSize) {
return request({
url: '/ccdi/staffFmyRelation/importFailures/' + taskId,
method: 'get',
params: { pageNum, pageSize }
})
}
// 获取员工列表(用于下拉选择)
export function getStaffList(name) {
return request({
url: '/ccdi/staffFmyRelation/staffList',
method: 'get',
params: { name }
})
}

View File

@@ -0,0 +1,988 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="员工姓名" prop="personName">
<el-input
v-model="queryParams.personName"
placeholder="请输入员工姓名"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="关系类型" prop="relationType">
<el-select v-model="queryParams.relationType" placeholder="请选择关系类型" clearable style="width: 240px">
<el-option
v-for="dict in dict.type.ccdi_relation_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关系人姓名" prop="relationName">
<el-input
v-model="queryParams.relationName"
placeholder="请输入关系人姓名"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 240px">
<el-option label="有效" :value="1" />
<el-option label="无效" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['ccdi:staffFmyRelation:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-upload2"
size="mini"
@click="handleImport"
v-hasPermi="['ccdi:staffFmyRelation:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['ccdi:staffFmyRelation:export']"
>导出</el-button>
</el-col>
<el-col :span="1.5" v-if="showFailureButton">
<el-tooltip
:content="getLastImportTooltip()"
placement="top"
>
<el-button
type="warning"
plain
icon="el-icon-warning"
size="mini"
@click="viewImportFailures"
>查看导入失败记录</el-button>
</el-tooltip>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="员工姓名" align="center" prop="personName" :show-overflow-tooltip="true"/>
<el-table-column label="员工身份证号" align="center" prop="personId" width="180"/>
<el-table-column label="关系类型" align="center" prop="relationType" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.ccdi_relation_type" :value="scope.row.relationType"/>
</template>
</el-table-column>
<el-table-column label="关系人姓名" align="center" prop="relationName" :show-overflow-tooltip="true"/>
<el-table-column label="性别" align="center" prop="gender" width="80">
<template slot-scope="scope">
<dict-tag :options="dict.type.ccdi_indiv_gender" :value="scope.row.gender"/>
</template>
</el-table-column>
<el-table-column label="手机号码" align="center" prop="mobilePhone1" :show-overflow-tooltip="true"/>
<el-table-column label="状态" align="center" prop="status" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '有效' : '无效' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
v-hasPermi="['ccdi:staffFmyRelation:query']"
>详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['ccdi:staffFmyRelation:edit']"
>编辑</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['ccdi:staffFmyRelation:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改对话框 -->
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="140px">
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="员工身份证号" prop="personId">
<el-select
v-model="form.personId"
filterable
remote
reserve-keyword
placeholder="请输入身份证号搜索员工"
:remote-method="searchStaff"
:loading="staffLoading"
:disabled="!isAdd"
@focus="handleSelectFocus"
style="width: 100%"
>
<el-option
v-for="item in staffOptions"
:key="item.idCard"
:label="item.idCard + ' - ' + item.name"
:value="item.idCard"
>
<span style="float: left">{{ item.idCard }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.name }}</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关系类型" prop="relationType">
<el-select v-model="form.relationType" placeholder="请选择关系类型" style="width: 100%">
<el-option
v-for="dict in dict.type.ccdi_relation_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="关系人姓名" prop="relationName">
<el-input v-model="form.relationName" placeholder="请输入关系人姓名" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="form.gender" placeholder="请选择性别" style="width: 100%">
<el-option
v-for="dict in dict.type.ccdi_indiv_gender"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="关系人证件类型" prop="relationCertType">
<el-select v-model="form.relationCertType" placeholder="请选择证件类型" style="width: 100%">
<el-option
v-for="dict in dict.type.ccdi_certificate_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关系人证件号码" prop="relationCertNo">
<el-input v-model="form.relationCertNo" placeholder="请输入证件号码" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="出生日期" prop="birthDate">
<el-date-picker
v-model="form.birthDate"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号码1" prop="mobilePhone1">
<el-input v-model="form.mobilePhone1" placeholder="请输入手机号码1" maxlength="20" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="手机号码2" prop="mobilePhone2">
<el-input v-model="form.mobilePhone2" placeholder="请输入手机号码2" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="微信名称1" prop="wechatNo1">
<el-input v-model="form.wechatNo1" placeholder="请输入微信名称1" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="微信名称2" prop="wechatNo2">
<el-input v-model="form.wechatNo2" placeholder="请输入微信名称2" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="微信名称3" prop="wechatNo3">
<el-input v-model="form.wechatNo3" placeholder="请输入微信名称3" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="状态" prop="status" v-if="!isAdd">
<el-radio-group v-model="form.status">
<el-radio :label="1">有效</el-radio>
<el-radio :label="0">无效</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="!isAdd">
<!-- 占位,保持布局对齐 -->
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker
v-model="form.effectiveDate"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="invalidDate">
<el-date-picker
v-model="form.invalidDate"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="详细联系地址" prop="contactAddress">
<el-input v-model="form.contactAddress" type="textarea" :rows="2" placeholder="请输入详细联系地址" />
</el-form-item>
<el-form-item label="关系详细描述" prop="relationDesc">
<el-input v-model="form.relationDesc" type="textarea" :rows="2" placeholder="请输入关系详细描述" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="员工亲属关系详情" :visible.sync="detailOpen" width="800px" append-to-body>
<div class="detail-container">
<el-divider content-position="left">基本信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="员工姓名">{{ relationDetail.personName || '-' }}</el-descriptions-item>
<el-descriptions-item label="员工身份证号">{{ relationDetail.personId || '-' }}</el-descriptions-item>
<el-descriptions-item label="关系类型">
<dict-tag :options="dict.type.ccdi_relation_type" :value="relationDetail.relationType"/>
</el-descriptions-item>
<el-descriptions-item label="关系人姓名">{{ relationDetail.relationName || '-' }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="dict.type.ccdi_indiv_gender" :value="relationDetail.gender"/>
</el-descriptions-item>
<el-descriptions-item label="关系人证件类型">{{ relationDetail.relationCertType || '-' }}</el-descriptions-item>
<el-descriptions-item label="关系人证件号码" :span="2">{{ relationDetail.relationCertNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="出生日期">{{ relationDetail.birthDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="手机号码1">{{ relationDetail.mobilePhone1 || '-' }}</el-descriptions-item>
<el-descriptions-item label="手机号码2">{{ relationDetail.mobilePhone2 || '-' }}</el-descriptions-item>
<el-descriptions-item label="微信名称1">{{ relationDetail.wechatNo1 || '-' }}</el-descriptions-item>
<el-descriptions-item label="微信名称2">{{ relationDetail.wechatNo2 || '-' }}</el-descriptions-item>
<el-descriptions-item label="微信名称3">{{ relationDetail.wechatNo3 || '-' }}</el-descriptions-item>
<el-descriptions-item label="详细联系地址" :span="2">{{ relationDetail.contactAddress || '-' }}</el-descriptions-item>
<el-descriptions-item label="关系详细描述" :span="2">{{ relationDetail.relationDesc || '-' }}</el-descriptions-item>
<el-descriptions-item label="生效日期">{{ relationDetail.effectiveDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="失效日期">{{ relationDetail.invalidDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="relationDetail.status === 1 ? 'success' : 'danger'">
{{ relationDetail.status === 1 ? '有效' : '无效' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="数据来源">{{ relationDetail.dataSource || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ relationDetail.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">审计信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="创建时间">
{{ relationDetail.createTime ? parseTime(relationDetail.createTime) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建人">{{ relationDetail.createdBy || '-' }}</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ relationDetail.updateTime ? parseTime(relationDetail.updateTime) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="更新人">{{ relationDetail.updatedBy || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailOpen = false" icon="el-icon-close"> </el-button>
</div>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog
:title="upload.title"
:visible.sync="upload.open"
width="400px"
append-to-body
@close="handleImportDialogClose"
>
<el-upload
ref="upload"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline;" @click="importTemplate">下载模板</el-link>
</div>
<div class="el-upload__tip" slot="tip">
<span>仅允许导入"xls""xlsx"格式文件</span>
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileForm" :loading="upload.isUploading"> </el-button>
<el-button @click="upload.open = false" :disabled="upload.isUploading"> </el-button>
</div>
</el-dialog>
<!-- 导入失败记录对话框 -->
<el-dialog
title="导入失败记录"
:visible.sync="failureDialogVisible"
width="1200px"
append-to-body
>
<el-alert
v-if="lastImportInfo"
:title="lastImportInfo"
type="info"
:closable="false"
style="margin-bottom: 15px"
/>
<el-table :data="failureList" v-loading="failureLoading">
<el-table-column label="员工身份证号" prop="personId" align="center" width="180"/>
<el-table-column label="关系类型" prop="relationType" align="center" width="100"/>
<el-table-column label="关系人姓名" prop="relationName" align="center" width="120"/>
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
</el-table>
<pagination
v-show="failureTotal > 0"
:total="failureTotal"
:page.sync="failureQueryParams.pageNum"
:limit.sync="failureQueryParams.pageSize"
@pagination="getFailureList"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="failureDialogVisible = false">关闭</el-button>
<el-button type="danger" plain @click="clearImportHistory">清除历史记录</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
addRelation,
delRelation,
getImportFailures,
getImportStatus,
getRelation,
listRelation,
updateRelation
} from "@/api/ccdiStaffFmyRelation";
import {listBaseStaff} from "@/api/ccdiBaseStaff";
import {getToken} from "@/utils/auth";
export default {
name: "StaffFmyRelation",
dicts: ['ccdi_relation_type', 'ccdi_indiv_gender', 'ccdi_certificate_type'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 员工亲属关系表格数据
relationList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否显示详情弹出层
detailOpen: false,
// 员工亲属关系详情
relationDetail: {},
// 是否为新增操作
isAdd: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
personName: null,
relationType: null,
relationName: null,
status: null
},
// 表单参数
form: {},
// 表单校验
rules: {
personId: [
{ required: true, message: "员工身份证号不能为空", trigger: "blur" },
{ pattern: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/, message: "请输入正确的18位身份证号", trigger: "blur" }
],
relationType: [
{ required: true, message: "关系类型不能为空", trigger: "change" }
],
relationName: [
{ required: true, message: "关系人姓名不能为空", trigger: "blur" }
],
relationCertType: [
{ required: true, message: "关系人证件类型不能为空", trigger: "change" }
],
relationCertNo: [
{ required: true, message: "关系人证件号码不能为空", trigger: "blur" }
],
mobilePhone1: [
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
],
mobilePhone2: [
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
]
},
// 导入参数
upload: {
// 是否显示弹出层
open: false,
// 弹出层标题
title: "",
// 是否禁用上传
isUploading: false,
// 设置上传的请求头部
headers: { Authorization: "Bearer " + getToken() },
// 上传的地址
url: process.env.VUE_APP_BASE_API + "/ccdi/staffFmyRelation/importData"
},
// 导入轮询定时器
importPollingTimer: null,
// 是否显示查看失败记录按钮
showFailureButton: false,
// 当前导入任务ID
currentTaskId: null,
// 失败记录对话框
failureDialogVisible: false,
failureList: [],
failureLoading: false,
failureTotal: 0,
failureQueryParams: {
pageNum: 1,
pageSize: 10
},
// 员工选项
staffOptions: [],
staffLoading: false
};
},
computed: {
/**
* 上次导入信息摘要
*/
lastImportInfo() {
const savedTask = this.getImportTaskFromStorage();
if (savedTask && savedTask.totalCount) {
return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}`;
}
return '';
}
},
created() {
this.getList();
this.restoreImportState();
},
beforeDestroy() {
if (this.importPollingTimer) {
clearInterval(this.importPollingTimer);
this.importPollingTimer = null;
}
},
methods: {
/** 查询员工亲属关系列表 */
getList() {
this.loading = true;
listRelation(this.queryParams).then(response => {
this.relationList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 搜索员工 */
searchStaff(query) {
this.staffLoading = true;
// 如果输入为空,查询所有员工;否则根据身份证号模糊查询
const params = query !== '' ? { idCard: query, pageNum: 1, pageSize: 10 } : { pageNum: 1, pageSize: 10 };
listBaseStaff(params).then(response => {
this.staffOptions = response.rows;
this.staffLoading = false;
}).catch(() => {
this.staffLoading = false;
});
},
/** 下拉框获得焦点时加载员工列表 */
handleSelectFocus() {
// 如果选项列表为空,自动加载所有员工
if (!this.staffOptions || this.staffOptions.length === 0) {
this.searchStaff('');
}
},
/**
* 恢复导入状态
*/
restoreImportState() {
const savedTask = this.getImportTaskFromStorage();
if (!savedTask) {
this.showFailureButton = false;
this.currentTaskId = null;
return;
}
if (savedTask.hasFailures && savedTask.taskId) {
this.currentTaskId = savedTask.taskId;
this.showFailureButton = true;
}
},
/**
* 获取上次导入的提示信息
*/
getLastImportTooltip() {
const savedTask = this.getImportTaskFromStorage();
if (savedTask && savedTask.saveTime) {
const date = new Date(savedTask.saveTime);
const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}');
return `上次导入: ${timeStr}`;
}
return '';
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
id: null,
personId: null,
relationType: null,
relationName: null,
gender: null,
birthDate: null,
relationCertType: null,
relationCertNo: null,
mobilePhone1: null,
mobilePhone2: null,
wechatNo1: null,
wechatNo2: null,
wechatNo3: null,
contactAddress: null,
relationDesc: null,
effectiveDate: null,
invalidDate: null,
status: 1,
remark: null
};
this.staffOptions = [];
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加员工亲属关系";
this.isAdd = true;
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id || this.ids[0];
getRelation(id).then(response => {
this.form = response.data;
// 加载员工信息以支持下拉显示
if (this.form.personId) {
this.searchStaff(this.form.personId);
}
this.open = true;
this.title = "修改员工亲属关系";
this.isAdd = false;
});
},
/** 详情按钮操作 */
handleDetail(row) {
const id = row.id;
getRelation(id).then(response => {
this.relationDetail = response.data;
this.detailOpen = true;
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.isAdd) {
addRelation(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
} else {
updateRelation(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除该数据项?').then(function() {
return delRelation(ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.download('ccdi/staffFmyRelation/export', {
...this.queryParams
}, `员工亲属关系_${new Date().getTime()}.xlsx`);
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = "员工亲属关系数据导入";
this.upload.open = true;
},
/** 下载模板操作 */
importTemplate() {
this.download('ccdi/staffFmyRelation/importTemplate', {}, `员工亲属关系导入模板_${new Date().getTime()}.xlsx`);
},
// 文件上传中处理
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true;
},
// 文件上传成功处理
handleFileSuccess(response, file, fileList) {
this.upload.isUploading = false;
this.upload.open = false;
if (response.code === 200) {
if (!response.data || !response.data.taskId) {
this.$modal.msgError('导入任务创建失败:缺少任务ID');
this.upload.isUploading = false;
this.upload.open = true;
return;
}
const taskId = response.data.taskId;
if (this.importPollingTimer) {
clearInterval(this.importPollingTimer);
this.importPollingTimer = null;
}
this.clearImportTaskFromStorage();
this.saveImportTaskToStorage({
taskId: taskId,
status: 'PROCESSING',
timestamp: Date.now(),
hasFailures: false
});
this.showFailureButton = false;
this.currentTaskId = taskId;
this.$notify({
title: '导入任务已提交',
message: '正在后台处理中,处理完成后将通知您',
type: 'info',
duration: 3000
});
this.startImportStatusPolling(taskId);
} else {
this.$modal.msgError(response.msg);
}
},
/** 开始轮询导入状态 */
startImportStatusPolling(taskId) {
let pollCount = 0;
const maxPolls = 150;
this.importPollingTimer = setInterval(async () => {
try {
pollCount++;
if (pollCount > maxPolls) {
clearInterval(this.importPollingTimer);
this.$modal.msgWarning('导入任务处理超时,请联系管理员');
return;
}
const response = await getImportStatus(taskId);
if (response.data && response.data.status !== 'PROCESSING') {
clearInterval(this.importPollingTimer);
this.handleImportComplete(response.data);
}
} catch (error) {
clearInterval(this.importPollingTimer);
this.$modal.msgError('查询导入状态失败: ' + error.message);
}
}, 2000);
},
/** 查询失败记录列表 */
getFailureList() {
this.failureLoading = true;
getImportFailures(
this.currentTaskId,
this.failureQueryParams.pageNum,
this.failureQueryParams.pageSize
).then(response => {
this.failureList = response.rows;
this.failureTotal = response.total;
this.failureLoading = false;
}).catch(error => {
this.failureLoading = false;
if (error.response && error.response.status === 404) {
this.$modal.msgWarning('导入记录已过期,无法查看失败记录');
this.clearImportTaskFromStorage();
this.showFailureButton = false;
this.currentTaskId = null;
this.failureDialogVisible = false;
} else {
this.$modal.msgError('查询失败记录失败');
}
});
},
/** 查看导入失败记录 */
viewImportFailures() {
this.failureDialogVisible = true;
this.getFailureList();
},
/** 处理导入完成 */
handleImportComplete(statusResult) {
this.saveImportTaskToStorage({
taskId: statusResult.taskId,
status: statusResult.status,
hasFailures: statusResult.failureCount > 0,
totalCount: statusResult.totalCount,
successCount: statusResult.successCount,
failureCount: statusResult.failureCount
});
if (statusResult.status === 'SUCCESS') {
this.$notify({
title: '导入完成',
message: `全部成功!共导入${statusResult.totalCount}条数据`,
type: 'success',
duration: 5000
});
this.showFailureButton = false;
this.getList();
} else if (statusResult.failureCount > 0) {
this.$notify({
title: '导入完成',
message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}`,
type: 'warning',
duration: 5000
});
this.showFailureButton = true;
this.currentTaskId = statusResult.taskId;
this.getList();
}
},
// 提交上传文件
submitFileForm() {
this.$refs.upload.submit();
},
// 关闭导入对话框
handleImportDialogClose() {
this.upload.isUploading = false;
this.$refs.upload.clearFiles();
},
/**
* 保存导入任务到localStorage
*/
saveImportTaskToStorage(taskData) {
try {
const data = {
...taskData,
saveTime: Date.now()
};
localStorage.setItem('staff_fmy_relation_import_last_task', JSON.stringify(data));
} catch (error) {
console.error('保存导入任务状态失败:', error);
}
},
/**
* 从localStorage读取导入任务
*/
getImportTaskFromStorage() {
try {
const data = localStorage.getItem('staff_fmy_relation_import_last_task');
if (!data) return null;
const task = JSON.parse(data);
if (!task || !task.taskId) {
this.clearImportTaskFromStorage();
return null;
}
const sevenDays = 7 * 24 * 60 * 60 * 1000;
if (Date.now() - task.saveTime > sevenDays) {
this.clearImportTaskFromStorage();
return null;
}
return task;
} catch (error) {
console.error('读取导入任务状态失败:', error);
this.clearImportTaskFromStorage();
return null;
}
},
/**
* 清除导入历史记录
*/
clearImportHistory() {
this.$confirm('确认清除上次导入记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.clearImportTaskFromStorage();
this.showFailureButton = false;
this.currentTaskId = null;
this.failureDialogVisible = false;
this.$message.success('已清除');
}).catch(() => {});
},
/**
* 清除localStorage中的导入任务
*/
clearImportTaskFromStorage() {
try {
localStorage.removeItem('staff_fmy_relation_import_last_task');
} catch (error) {
console.error('清除导入任务状态失败:', error);
}
}
}
};
</script>
<style scoped>
.detail-container {
padding: 0 20px;
}
</style>

View File

@@ -0,0 +1,44 @@
-- 员工亲属关系表
-- 创建时间: 2026-02-09
-- 说明: 存储员工家庭成员关系信息,仅处理员工家庭关系(is_emp_family=1)
CREATE TABLE IF NOT EXISTS `ccdi_staff_fmy_relation` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`person_id` VARCHAR(100) NOT NULL COMMENT '员工身份证号',
`relation_type` VARCHAR(50) NOT NULL COMMENT '关系类型(配偶、子女、父母、兄弟姐妹等)',
`relation_name` VARCHAR(100) NOT NULL COMMENT '关系人姓名',
`gender` CHAR(1) DEFAULT NULL COMMENT '性别M-男F-女O-其他',
`birth_date` DATE DEFAULT NULL COMMENT '关系人出生日期',
`relation_cert_type` VARCHAR(50) NOT NULL COMMENT '关系人证件类型',
`relation_cert_no` VARCHAR(50) NOT NULL COMMENT '关系人证件号码',
`mobile_phone1` VARCHAR(20) DEFAULT NULL COMMENT '手机号码1',
`mobile_phone2` VARCHAR(20) DEFAULT NULL COMMENT '手机号码2',
`wechat_no1` VARCHAR(50) DEFAULT NULL COMMENT '微信名称1',
`wechat_no2` VARCHAR(50) DEFAULT NULL COMMENT '微信名称2',
`wechat_no3` VARCHAR(50) DEFAULT NULL COMMENT '微信名称3',
`contact_address` VARCHAR(500) DEFAULT NULL COMMENT '详细联系地址',
`relation_desc` VARCHAR(500) DEFAULT NULL COMMENT '关系详细描述',
`status` INT(11) NOT NULL DEFAULT 1 COMMENT '状态0-无效1-有效',
`effective_date` DATETIME DEFAULT NULL COMMENT '关系生效日期',
`invalid_date` DATETIME DEFAULT NULL COMMENT '关系失效日期',
`remark` TEXT DEFAULT NULL COMMENT '备注信息',
`data_source` VARCHAR(50) DEFAULT NULL COMMENT '数据来源MANUAL-手工录入SYSTEM-系统同步IMPORT-批量导入API-接口获取',
`is_emp_family` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否是员工的家庭关系0-否 1-是',
`is_cust_family` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是信贷客户的家庭关系0-否 1-是',
`created_by` VARCHAR(100) NOT NULL COMMENT '记录创建人',
`updated_by` VARCHAR(100) DEFAULT NULL COMMENT '记录更新人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_person_cert` (`person_id`, `relation_cert_no`) COMMENT '员工身份证号+关系人证件号码唯一',
KEY `idx_person_id` (`person_id`) COMMENT '员工身份证号索引',
KEY `idx_relation_cert_no` (`relation_cert_no`) COMMENT '关系人证件号码索引',
KEY `idx_status` (`status`) COMMENT '状态索引',
KEY `idx_data_source` (`data_source`) COMMENT '数据来源索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工家庭关系表';
-- 测试数据
-- INSERT INTO `ccdi_staff_fmy_relation`
-- (`person_id`, `relation_type`, `relation_name`, `gender`, `birth_date`, `relation_cert_type`, `relation_cert_no`, `mobile_phone1`, `contact_address`, `status`, `data_source`, `is_emp_family`, `is_cust_family`, `created_by`)
-- VALUES
-- ('110101199001011234', '配偶', '张三', 'M', '1990-05-15', '身份证', '110101199001015678', '13800138000', '北京市朝阳区', 1, 'MANUAL', 1, 0, 'admin');

View File

@@ -0,0 +1,45 @@
-- 添加员工亲属关系维护菜单
-- 注意: 执行前请确认已存在"信息维护"父菜单
-- 如果不存在,请先执行以下语句创建父菜单:
-- INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
-- VALUES (2000, '信息维护', 0, 4, 'dpc', NULL, '', '', 1, 0, 'M', '0', '0', '', 'example', 'admin', NOW(), '信息维护目录');
-- 查询信息维护父菜单ID
SET @parent_menu_id = (SELECT menu_id FROM sys_menu WHERE menu_name='信息维护' AND parent_id=0 LIMIT 1);
-- 添加员工亲属关系维护菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES
('员工亲属关系维护', @parent_menu_id, 3, 'staffFmyRelation', 'ccdiStaffFmyRelation/index', 1, 0, 'C', '0', '0', 'ccdi:staffFmyRelation:list', 'peoples', 'admin', NOW(), '', NULL, '员工亲属关系信息管理菜单');
-- 获取刚插入的菜单ID
SET @menu_id = LAST_INSERT_ID();
-- 添加按钮权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES
('亲属关系查询', @menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'ccdi:staffFmyRelation:query', '#', 'admin', NOW(), ''),
('亲属关系新增', @menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'ccdi:staffFmyRelation:add', '#', 'admin', NOW(), ''),
('亲属关系修改', @menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'ccdi:staffFmyRelation:edit', '#', 'admin', NOW(), ''),
('亲属关系删除', @menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'ccdi:staffFmyRelation:remove', '#', 'admin', NOW(), ''),
('亲属关系导出', @menu_id, 5, '', '', 1, 0, 'F', '0', '0', 'ccdi:staffFmyRelation:export', '#', 'admin', NOW(), ''),
('亲属关系导入', @menu_id, 6, '', '', 1, 0, 'F', '0', '0', 'ccdi:staffFmyRelation:import', '#', 'admin', NOW(), '');
-- 查询结果验证
SELECT
m.menu_id AS '菜单ID',
m.menu_name AS '菜单名称',
m.parent_id AS '父菜单ID',
p.menu_name AS '父菜单名称',
m.order_num AS '显示顺序',
m.path AS '路由地址',
m.component AS '组件路径',
m.menu_type AS '菜单类型',
m.perms AS '权限标识',
m.icon AS '菜单图标',
m.visible AS '显示状态',
m.status AS '菜单状态',
m.create_time AS '创建时间'
FROM sys_menu m
LEFT JOIN sys_menu p ON m.parent_id = p.menu_id
WHERE m.menu_name = '员工亲属关系维护' OR m.parent_id = @menu_id
ORDER BY m.parent_id, m.order_num;

4
test_result.json Normal file
View File

@@ -0,0 +1,4 @@
{
"msg": "\r\n### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'r.contact_phone' in 'field list'\r\n### The error may exist in file [D:\\ccdi\\ccdi\\ruoyi-ccdi\\target\\classes\\mapper\\ccdi\\CcdiStaffFmyRelationMapper.xml]\r\n### The error may involve defaultParameterMap\r\n### The error occurred while setting parameters\r\n### SQL: SELECT r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name, r.gender, r.relation_cert_type, r.relation_cert_no, r.contact_phone, r.address, r.occupation, r.work_unit, r.effective_date, r.expiry_date, r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family, r.created_by, r.create_time, r.updated_by, r.update_time FROM ccdi_staff_fmy_relation r LEFT JOIN ccdi_base_staff s ON r.person_id = s.person_id WHERE r.is_emp_family = 1 ORDER BY r.create_time DESC LIMIT ?\r\n### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'r.contact_phone' in 'field list'\n; bad SQL grammar []",
"code": 500
}

View File

@@ -0,0 +1,264 @@
"""
员工亲属关系功能测试脚本
测试员工身份证号字段改造后的功能
"""
import requests
import json
BASE_URL = "http://localhost:8080"
# 登录获取 token
def login():
url = f"{BASE_URL}/login/test"
data = {
"username": "admin",
"password": "admin123"
}
response = requests.post(url, json=data)
result = response.json()
if result.get("code") == 200:
token = result.get("token")
print("✓ 登录成功")
return token
else:
print(f"✗ 登录失败: {result.get('msg')}")
return None
# 获取请求头
def get_headers(token):
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# 测试1: 查询员工列表(用于下拉选择)
def test_list_staff():
print("\n【测试1】查询员工列表")
url = f"{BASE_URL}/ccdi/baseStaff/list"
params = {
"pageNum": 1,
"pageSize": 10
}
response = requests.get(url, params=params, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
rows = result.get("rows", [])
print(f"✓ 查询成功,共 {len(rows)} 条员工记录")
if rows:
print(f" 示例: {rows[0].get('name')} - {rows[0].get('idCard')}")
return rows[0].get("idCard") # 返回第一个员工的身份证号
else:
print(" 警告: 暂无员工数据")
return None
else:
print(f"✗ 查询失败: {result.get('msg')}")
return None
# 测试2: 根据身份证号搜索员工
def test_search_staff(keyword):
print(f"\n【测试2】根据身份证号搜索员工: {keyword}")
url = f"{BASE_URL}/ccdi/baseStaff/list"
params = {
"idCard": keyword,
"pageNum": 1,
"pageSize": 10
}
response = requests.get(url, params=params, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
rows = result.get("rows", [])
print(f"✓ 搜索成功,找到 {len(rows)} 条记录")
for row in rows[:3]: # 只显示前3条
print(f" - {row.get('name')} - {row.get('idCard')}")
return rows
else:
print(f"✗ 搜索失败: {result.get('msg')}")
return []
# 测试3: 查询已有亲属关系(避免重复插入)
def test_get_existing_relation(person_id):
print(f"\n【测试3】查询已有员工亲属关系")
url = f"{BASE_URL}/ccdi/staffFmyRelation/list"
params = {
"personId": person_id,
"pageNum": 1,
"pageSize": 1
}
response = requests.get(url, params=params, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
rows = result.get("rows", [])
if rows:
relation_id = rows[0].get("id")
print(f"✓ 找到已有记录, ID: {relation_id}")
return relation_id
else:
print(" 未找到已有记录,准备新增测试数据")
return None
else:
print(f"✗ 查询失败: {result.get('msg')}")
return None
# 测试4: 新增员工亲属关系(如果没有现有数据)
def test_add_relation(person_id):
print(f"\n【测试4】新增员工亲属关系")
url = f"{BASE_URL}/ccdi/staffFmyRelation"
import random
random_suffix = random.randint(10000, 99999)
data = {
"personId": person_id,
"relationType": "01", # 配偶
"relationName": "测试亲属",
"gender": "M",
"relationCertType": "01", # 身份证
"relationCertNo": f"11010119900101{random_suffix}", # 随机生成避免重复
"status": 1
}
response = requests.post(url, json=data, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
# 若依框架新增接口返回的是 "操作成功" 消息,没有返回ID
# 需要通过查询列表来获取新增的记录
print(f"✓ 新增成功")
# 查询列表获取最新记录的ID
list_url = f"{BASE_URL}/ccdi/staffFmyRelation/list"
list_params = {
"personId": person_id,
"pageNum": 1,
"pageSize": 1
}
list_response = requests.get(list_url, params=list_params, headers=get_headers(token))
list_result = list_response.json()
if list_result.get("rows"):
relation_id = list_result.get("rows")[0].get("id")
print(f" 获取到记录ID: {relation_id}")
return relation_id
return None
else:
print(f"✗ 新增失败: {result.get('msg')}")
return None
# 测试5: 查询员工亲属关系列表
def test_list_relations(person_id):
print(f"\n【测试5】查询员工亲属关系列表")
url = f"{BASE_URL}/ccdi/staffFmyRelation/list"
params = {
"personId": person_id,
"pageNum": 1,
"pageSize": 10
}
response = requests.get(url, params=params, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
rows = result.get("rows", [])
total = result.get("total", 0)
print(f"✓ 查询成功,共 {total} 条记录")
for row in rows:
print(f" - {row.get('personName')}({row.get('personId')}) 的{row.get('relationType')} - {row.get('relationName')}")
return rows
else:
print(f"✗ 查询失败: {result.get('msg')}")
return []
# 测试6: 查询亲属关系详情
def test_get_relation_detail(relation_id):
print(f"\n【测试6】查询亲属关系详情: ID={relation_id}")
url = f"{BASE_URL}/ccdi/staffFmyRelation/{relation_id}"
response = requests.get(url, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
data = result.get("data")
print(f"✓ 查询成功")
print(f" 员工: {data.get('personName')}({data.get('personId')})")
print(f" 关系: {data.get('relationType')} - {data.get('relationName')}")
print(f" 状态: {'有效' if data.get('status') == 1 else '无效'}")
return data
else:
print(f"✗ 查询失败: {result.get('msg')}")
return None
# 测试7: 编辑员工亲属关系(只修改非身份证号字段)
def test_update_relation(relation_id):
print(f"\n【测试7】编辑员工亲属关系: ID={relation_id}")
url = f"{BASE_URL}/ccdi/staffFmyRelation"
data = {
"id": relation_id,
"relationName": "测试亲属(已修改)",
"mobilePhone1": "13800138000",
"status": 1
}
response = requests.put(url, json=data, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
print(f"✓ 修改成功")
return True
else:
print(f"✗ 修改失败: {result.get('msg')}")
return False
# 测试8: 删除员工亲属关系
def test_delete_relation(relation_id):
print(f"\n【测试8】删除员工亲属关系: ID={relation_id}")
url = f"{BASE_URL}/ccdi/staffFmyRelation/{relation_id}"
response = requests.delete(url, headers=get_headers(token))
result = response.json()
if result.get("code") == 200:
print(f"✓ 删除成功")
return True
else:
print(f"✗ 删除失败: {result.get('msg')}")
return False
# 主测试流程
def main():
global token
print("=" * 60)
print("员工亲属关系功能测试")
print("=" * 60)
# 登录
token = login()
if not token:
return
# 测试1: 查询员工列表
person_id = test_list_staff()
if not person_id:
print("\n提示: 请先在系统中添加员工数据")
return
# 测试2: 搜索员工
test_search_staff(person_id[:6]) # 使用身份证号前6位搜索
# 测试3: 查询已有亲属关系
relation_id = test_get_existing_relation(person_id)
# 如果没有已有记录,则新增
if not relation_id:
relation_id = test_add_relation(person_id)
if not relation_id:
print("\n提示: 无法获取测试记录ID,跳过后续测试")
return
# 测试5: 查询亲属关系列表
test_list_relations(person_id)
# 测试6: 查询详情
test_get_relation_detail(relation_id)
# 测试7: 编辑亲属关系
test_update_relation(relation_id)
# 测试8: 再次查看详情(验证修改)
test_get_relation_detail(relation_id)
# 清理测试数据(仅限新增的数据)
print("\n【清理】测试结束(保留现有数据)")
print("\n" + "=" * 60)
print("测试完成!")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,105 @@
import requests
import json
# 配置
BASE_URL = "http://localhost:8080"
LOGIN_URL = f"{BASE_URL}/login/test"
LIST_URL = f"{BASE_URL}/ccdi/staffFmyRelation/list"
# 测试账号
test_user = {
"username": "admin",
"password": "admin123"
}
def test_fix():
"""测试修复是否成功"""
print("=" * 60)
print("开始测试员工家庭关系接口修复")
print("=" * 60)
# Step 1: 获取token
print("\n[1/2] 正在登录获取token...")
headers = {
"Content-Type": "application/json"
}
login_response = requests.post(LOGIN_URL, json=test_user, headers=headers)
if login_response.status_code != 200:
print(f"❌ 登录失败: {login_response.status_code}")
print(login_response.text)
return False
login_data = login_response.json()
if login_data.get("code") != 200:
print(f"❌ 登录失败: {login_data.get('msg')}")
return False
token = login_data.get("token")
print(f"✅ 登录成功获取到token: {token[:20]}...")
# Step 2: 调用分页查询接口
print("\n[2/2] 正在测试分页查询接口...")
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"pageNum": 1,
"pageSize": 10,
"isEmpFamily": 1
}
list_response = requests.get(LIST_URL, params=params, headers=headers)
print(f"\n响应状态码: {list_response.status_code}")
if list_response.status_code != 200:
print(f"❌ 接口调用失败")
print(list_response.text)
return False
result = list_response.json()
# 保存完整响应
with open("test_result.json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"\n完整响应已保存到 test_result.json")
if result.get("code") == 200:
rows = result.get("rows", [])
total = result.get("total", 0)
print(f"✅ 接口调用成功!")
print(f" - 数据总数: {total}")
print(f" - 当前页记录数: {len(rows)}")
if rows:
print(f"\n示例数据 (第一条):")
first_row = rows[0]
for key, value in first_row.items():
if key not in ["createTime", "updateTime"]:
print(f" {key}: {value}")
print("\n" + "=" * 60)
print("✅ 修复验证成功!数据库字段映射问题已解决")
print("=" * 60)
return True
else:
print(f"❌ 接口返回错误: {result.get('msg')}")
print(f"错误详情: {result}")
return False
if __name__ == "__main__":
try:
success = test_fix()
exit(0 if success else 1)
except Exception as e:
print(f"\n❌ 测试过程中发生异常: {str(e)}")
import traceback
traceback.print_exc()
exit(1)