完成账户库管理功能开发与验收

This commit is contained in:
wjj
2026-04-14 10:16:16 +08:00
parent 9c22e8a3ce
commit 1bb24ab0a2
32 changed files with 4825 additions and 15 deletions

View File

@@ -0,0 +1,181 @@
package com.ruoyi.info.collection.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.annotation.Log;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
import com.ruoyi.info.collection.domain.vo.AccountInfoImportFailureVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffOptionVO;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.service.ICcdiAccountInfoService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
import io.swagger.v3.oas.annotations.Operation;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 账户库Controller
*
* @author ruoyi
* @date 2026-04-13
*/
@Tag(name = "账户库管理")
@RestController
@RequestMapping("/ccdi/accountInfo")
public class CcdiAccountInfoController extends BaseController {
@Resource
private ICcdiAccountInfoService accountInfoService;
@Resource
private ICcdiBaseStaffService baseStaffService;
/**
* 查询账户库列表
*/
@Operation(summary = "查询账户库列表")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:list')")
@GetMapping("/list")
public TableDataInfo list(CcdiAccountInfoQueryDTO queryDTO) {
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiAccountInfoVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiAccountInfoVO> result = accountInfoService.selectAccountInfoPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 查询账户库详情
*/
@Operation(summary = "查询账户库详情")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
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, "账户库管理");
}
/**
* 新增账户
*/
@Operation(summary = "新增账户")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:add')")
@Log(title = "账户库管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiAccountInfoAddDTO addDTO) {
return toAjax(accountInfoService.insertAccountInfo(addDTO));
}
/**
* 修改账户
*/
@Operation(summary = "修改账户")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:edit')")
@Log(title = "账户库管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiAccountInfoEditDTO editDTO) {
return toAjax(accountInfoService.updateAccountInfo(editDTO));
}
/**
* 删除账户
*/
@Operation(summary = "删除账户")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:remove')")
@Log(title = "账户库管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(accountInfoService.deleteAccountInfoByIds(ids));
}
/**
* 查询账户归属员工下拉
*/
@Operation(summary = "查询账户归属员工下拉")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:list')")
@GetMapping("/staffOptions")
public AjaxResult getStaffOptions(@RequestParam(required = false) String query) {
List<CcdiBaseStaffOptionVO> list = baseStaffService.selectStaffOptions(query);
return success(list);
}
/**
* 查询关系人下拉
*/
@Operation(summary = "查询关系人下拉")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:list')")
@GetMapping("/relationOptions")
public AjaxResult getRelationOptions(@RequestParam Long staffId) {
return success(accountInfoService.selectRelationOptionsByStaffId(staffId));
}
/**
* 下载导入模板
*/
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateExcel(response, CcdiAccountInfoExcel.class, "账户库管理");
}
/**
* 导入账户库信息
*/
@Operation(summary = "导入账户库信息")
@PreAuthorize("@ss.hasPermi('ccdi:accountInfo:import')")
@Log(title = "账户库管理", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
List<CcdiAccountInfoExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiAccountInfoExcel.class);
if (list == null || list.isEmpty()) {
return error("至少需要一条数据");
}
ImportResult result = accountInfoService.importAccountInfo(list, updateSupport);
List<AccountInfoImportFailureVO> failures = accountInfoService.getLatestImportFailures();
Map<String, Object> data = new HashMap<>(4);
data.put("totalCount", result.getTotalCount());
data.put("successCount", result.getSuccessCount());
data.put("failureCount", result.getFailureCount());
data.put("failures", failures);
String message = "导入完成,共 " + result.getTotalCount() + " 条,成功 " + result.getSuccessCount()
+ " 条,失败 " + result.getFailureCount() + "";
return AjaxResult.success(message, data);
}
}

View File

@@ -0,0 +1,83 @@
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 com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 账户基础信息对象 ccdi_account_info
*
* @author ruoyi
* @date 2026-04-13
*/
@Data
@TableName("ccdi_account_info")
public class CcdiAccountInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(value = "account_id", type = IdType.AUTO)
private Long id;
/** 所属人类型EMPLOYEE/RELATION/INTERMEDIARY/EXTERNAL */
private String ownerType;
/** 所属人标识 */
private String ownerId;
/** 账户号码 */
private String accountNo;
/** 账户类型 */
private String accountType;
/** 账户范围INTERNAL/EXTERNAL */
private String bankScope;
/** 账户姓名 */
private String accountName;
/** 开户机构 */
@TableField("bank")
private String openBank;
/** 银行代码 */
private String bankCode;
/** 币种 */
private String currency;
/** 状态1-正常 2-已销户 */
private Integer status;
/** 生效日期 */
private Date effectiveDate;
/** 失效日期 */
private Date invalidDate;
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}

View File

@@ -0,0 +1,86 @@
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 com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 账户分析结果对象 ccdi_account_result
*
* @author ruoyi
* @date 2026-04-13
*/
@Data
@TableName("ccdi_account_result")
public class CcdiAccountResult implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(value = "result_id", type = IdType.AUTO)
private Long resultId;
/** 账户号码 */
private String accountNo;
/** 是否实控账户0-否 1-是 */
@TableField("is_self_account")
private Integer isActualControl;
/** 月均交易笔数 */
@TableField("monthly_avg_trans_count")
private Integer avgMonthTxnCount;
/** 月均交易金额 */
@TableField("monthly_avg_trans_amount")
private BigDecimal avgMonthTxnAmount;
/** 交易频率等级 */
@TableField("trans_freq_type")
private String txnFrequencyLevel;
/** 借方单笔最高额 */
@TableField("dr_max_single_amount")
private BigDecimal debitSingleMaxAmount;
/** 贷方单笔最高额 */
@TableField("cr_max_single_amount")
private BigDecimal creditSingleMaxAmount;
/** 借方日累计最高额 */
@TableField("dr_max_daily_amount")
private BigDecimal debitDailyMaxAmount;
/** 贷方日累计最高额 */
@TableField("cr_max_daily_amount")
private BigDecimal creditDailyMaxAmount;
/** 风险等级 */
@TableField("trans_risk_level")
private String txnRiskLevel;
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}

View File

@@ -0,0 +1,138 @@
package com.ruoyi.info.collection.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 账户库新增DTO
*
* @author ruoyi
* @date 2026-04-13
*/
@Data
@Schema(description = "账户库新增")
public class CcdiAccountInfoAddDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 所属人类型 */
@NotBlank(message = "所属人类型不能为空")
@Schema(description = "所属人类型")
private String ownerType;
/** 所属人标识 */
@Schema(description = "所属人标识")
private String ownerId;
/** 账户号码 */
@NotBlank(message = "账户号码不能为空")
@Size(max = 240, message = "账户号码长度不能超过240个字符")
@Schema(description = "账户号码")
private String accountNo;
/** 账户类型 */
@NotBlank(message = "账户类型不能为空")
@Size(max = 30, message = "账户类型长度不能超过30个字符")
@Schema(description = "账户类型")
private String accountType;
/** 账户范围 */
@NotBlank(message = "账户范围不能为空")
@Size(max = 20, message = "账户范围长度不能超过20个字符")
@Schema(description = "账户范围")
private String bankScope;
/** 账户姓名 */
@NotBlank(message = "账户姓名不能为空")
@Size(max = 100, message = "账户姓名长度不能超过100个字符")
@Schema(description = "账户姓名")
private String accountName;
/** 开户机构 */
@NotBlank(message = "开户机构不能为空")
@Size(max = 100, message = "开户机构长度不能超过100个字符")
@Schema(description = "开户机构")
private String openBank;
/** 银行代码 */
@Size(max = 20, message = "银行代码长度不能超过20个字符")
@Schema(description = "银行代码")
private String bankCode;
/** 币种 */
@Size(max = 3, message = "币种长度不能超过3个字符")
@Schema(description = "币种")
private String currency;
/** 状态 */
@NotNull(message = "状态不能为空")
@Schema(description = "状态")
private Integer status;
/** 生效日期 */
@NotNull(message = "生效日期不能为空")
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期")
private Date effectiveDate;
/** 失效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "失效日期")
private Date invalidDate;
/** 是否实控账户 */
@Schema(description = "是否实控账户")
private Integer isActualControl;
/** 月均交易笔数 */
@Min(value = 0, message = "月均交易笔数不能小于0")
@Schema(description = "月均交易笔数")
private Integer avgMonthTxnCount;
/** 月均交易金额 */
@DecimalMin(value = "0", message = "月均交易金额不能小于0")
@Schema(description = "月均交易金额")
private BigDecimal avgMonthTxnAmount;
/** 频率等级 */
@Size(max = 20, message = "频率等级长度不能超过20个字符")
@Schema(description = "频率等级")
private String txnFrequencyLevel;
/** 借方单笔最高额 */
@DecimalMin(value = "0", message = "借方单笔最高额不能小于0")
@Schema(description = "借方单笔最高额")
private BigDecimal debitSingleMaxAmount;
/** 贷方单笔最高额 */
@DecimalMin(value = "0", message = "贷方单笔最高额不能小于0")
@Schema(description = "贷方单笔最高额")
private BigDecimal creditSingleMaxAmount;
/** 借方日累计最高额 */
@DecimalMin(value = "0", message = "借方日累计最高额不能小于0")
@Schema(description = "借方日累计最高额")
private BigDecimal debitDailyMaxAmount;
/** 贷方日累计最高额 */
@DecimalMin(value = "0", message = "贷方日累计最高额不能小于0")
@Schema(description = "贷方日累计最高额")
private BigDecimal creditDailyMaxAmount;
/** 风险等级 */
@Size(max = 10, message = "风险等级长度不能超过10个字符")
@Schema(description = "风险等级")
private String txnRiskLevel;
}

View File

@@ -0,0 +1,143 @@
package com.ruoyi.info.collection.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 账户库编辑DTO
*
* @author ruoyi
* @date 2026-04-13
*/
@Data
@Schema(description = "账户库编辑")
public class CcdiAccountInfoEditDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@NotNull(message = "主键ID不能为空")
@Schema(description = "主键ID")
private Long id;
/** 所属人类型 */
@NotBlank(message = "所属人类型不能为空")
@Schema(description = "所属人类型")
private String ownerType;
/** 所属人标识 */
@Schema(description = "所属人标识")
private String ownerId;
/** 账户号码 */
@NotBlank(message = "账户号码不能为空")
@Size(max = 240, message = "账户号码长度不能超过240个字符")
@Schema(description = "账户号码")
private String accountNo;
/** 账户类型 */
@NotBlank(message = "账户类型不能为空")
@Size(max = 30, message = "账户类型长度不能超过30个字符")
@Schema(description = "账户类型")
private String accountType;
/** 账户范围 */
@NotBlank(message = "账户范围不能为空")
@Size(max = 20, message = "账户范围长度不能超过20个字符")
@Schema(description = "账户范围")
private String bankScope;
/** 账户姓名 */
@NotBlank(message = "账户姓名不能为空")
@Size(max = 100, message = "账户姓名长度不能超过100个字符")
@Schema(description = "账户姓名")
private String accountName;
/** 开户机构 */
@NotBlank(message = "开户机构不能为空")
@Size(max = 100, message = "开户机构长度不能超过100个字符")
@Schema(description = "开户机构")
private String openBank;
/** 银行代码 */
@Size(max = 20, message = "银行代码长度不能超过20个字符")
@Schema(description = "银行代码")
private String bankCode;
/** 币种 */
@Size(max = 3, message = "币种长度不能超过3个字符")
@Schema(description = "币种")
private String currency;
/** 状态 */
@NotNull(message = "状态不能为空")
@Schema(description = "状态")
private Integer status;
/** 生效日期 */
@NotNull(message = "生效日期不能为空")
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期")
private Date effectiveDate;
/** 失效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "失效日期")
private Date invalidDate;
/** 是否实控账户 */
@Schema(description = "是否实控账户")
private Integer isActualControl;
/** 月均交易笔数 */
@Min(value = 0, message = "月均交易笔数不能小于0")
@Schema(description = "月均交易笔数")
private Integer avgMonthTxnCount;
/** 月均交易金额 */
@DecimalMin(value = "0", message = "月均交易金额不能小于0")
@Schema(description = "月均交易金额")
private BigDecimal avgMonthTxnAmount;
/** 频率等级 */
@Size(max = 20, message = "频率等级长度不能超过20个字符")
@Schema(description = "频率等级")
private String txnFrequencyLevel;
/** 借方单笔最高额 */
@DecimalMin(value = "0", message = "借方单笔最高额不能小于0")
@Schema(description = "借方单笔最高额")
private BigDecimal debitSingleMaxAmount;
/** 贷方单笔最高额 */
@DecimalMin(value = "0", message = "贷方单笔最高额不能小于0")
@Schema(description = "贷方单笔最高额")
private BigDecimal creditSingleMaxAmount;
/** 借方日累计最高额 */
@DecimalMin(value = "0", message = "借方日累计最高额不能小于0")
@Schema(description = "借方日累计最高额")
private BigDecimal debitDailyMaxAmount;
/** 贷方日累计最高额 */
@DecimalMin(value = "0", message = "贷方日累计最高额不能小于0")
@Schema(description = "贷方日累计最高额")
private BigDecimal creditDailyMaxAmount;
/** 风险等级 */
@Size(max = 10, message = "风险等级长度不能超过10个字符")
@Schema(description = "风险等级")
private String txnRiskLevel;
}

View File

@@ -0,0 +1,57 @@
package com.ruoyi.info.collection.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 账户库查询DTO
*
* @author ruoyi
* @date 2026-04-13
*/
@Data
@Schema(description = "账户库查询条件")
public class CcdiAccountInfoQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String staffName;
/** 所属人类型 */
@Schema(description = "所属人类型")
private String ownerType;
/** 账户范围 */
@Schema(description = "账户范围")
private String bankScope;
/** 关系类型 */
@Schema(description = "关系类型")
private String relationType;
/** 账户姓名 */
@Schema(description = "账户姓名")
private String accountName;
/** 账户类型 */
@Schema(description = "账户类型")
private String accountType;
/** 是否实控账户 */
@Schema(description = "是否实控账户")
private Integer isActualControl;
/** 风险等级 */
@Schema(description = "风险等级")
private String riskLevel;
/** 状态 */
@Schema(description = "状态")
private Integer status;
}

View File

@@ -0,0 +1,105 @@
package com.ruoyi.info.collection.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 账户库导入导出对象
*
* @author ruoyi
* @date 2026-04-14
*/
@Data
public class CcdiAccountInfoExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "所属人类型*", index = 0)
@ColumnWidth(16)
private String ownerType;
@ExcelProperty(value = "证件号*", index = 1)
@ColumnWidth(24)
private String ownerId;
@ExcelProperty(value = "账户姓名*", index = 2)
@ColumnWidth(18)
private String accountName;
@ExcelProperty(value = "账户号码*", index = 3)
@ColumnWidth(28)
private String accountNo;
@ExcelProperty(value = "账户类型*", index = 4)
@ColumnWidth(16)
private String accountType;
@ExcelProperty(value = "账户范围*", index = 5)
@ColumnWidth(14)
private String bankScope;
@ExcelProperty(value = "开户机构*", index = 6)
@ColumnWidth(28)
private String openBank;
@ExcelProperty(value = "银行代码", index = 7)
@ColumnWidth(16)
private String bankCode;
@ExcelProperty(value = "币种", index = 8)
@ColumnWidth(10)
private String currency;
@ExcelProperty(value = "状态*", index = 9)
@ColumnWidth(12)
private String status;
@ExcelProperty(value = "生效日期*(yyyy-MM-dd)", index = 10)
@ColumnWidth(18)
private String effectiveDate;
@ExcelProperty(value = "失效日期(yyyy-MM-dd)", index = 11)
@ColumnWidth(18)
private String invalidDate;
@ExcelProperty(value = "是否实控账户", index = 12)
@ColumnWidth(14)
private String isActualControl;
@ExcelProperty(value = "月均交易笔数", index = 13)
@ColumnWidth(14)
private String avgMonthTxnCount;
@ExcelProperty(value = "月均交易金额", index = 14)
@ColumnWidth(16)
private String avgMonthTxnAmount;
@ExcelProperty(value = "频率等级", index = 15)
@ColumnWidth(12)
private String txnFrequencyLevel;
@ExcelProperty(value = "借方单笔最高额", index = 16)
@ColumnWidth(16)
private String debitSingleMaxAmount;
@ExcelProperty(value = "贷方单笔最高额", index = 17)
@ColumnWidth(16)
private String creditSingleMaxAmount;
@ExcelProperty(value = "借方日累计最高额", index = 18)
@ColumnWidth(16)
private String debitDailyMaxAmount;
@ExcelProperty(value = "贷方日累计最高额", index = 19)
@ColumnWidth(16)
private String creditDailyMaxAmount;
@ExcelProperty(value = "风险等级", index = 20)
@ColumnWidth(12)
private String txnRiskLevel;
}

View File

@@ -0,0 +1,30 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 账户库导入失败记录
*
* @author ruoyi
* @date 2026-04-14
*/
@Data
@Schema(description = "账户库导入失败记录")
public class AccountInfoImportFailureVO {
@Schema(description = "行号")
private Integer rowNum;
@Schema(description = "所属人类型")
private String ownerType;
@Schema(description = "证件号")
private String ownerId;
@Schema(description = "账户号码")
private String accountNo;
@Schema(description = "错误信息")
private String errorMessage;
}

View File

@@ -0,0 +1,156 @@
package com.ruoyi.info.collection.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.math.BigDecimal;
import java.util.Date;
/**
* 账户库VO
*
* @author ruoyi
* @date 2026-04-13
*/
@Data
@Schema(description = "账户库信息")
public class CcdiAccountInfoVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@Schema(description = "主键ID")
private Long id;
/** 所属人类型 */
@Schema(description = "所属人类型")
private String ownerType;
/** 所属人标识 */
@Schema(description = "所属人标识")
private String ownerId;
/** 员工工号 */
@Schema(description = "员工工号")
private Long staffId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String staffName;
/** 关系人ID */
@Schema(description = "关系人ID")
private Long relationId;
/** 关系类型 */
@Schema(description = "关系类型")
private String relationType;
/** 关系人姓名 */
@Schema(description = "关系人姓名")
private String relationName;
/** 关系人证件号 */
@Schema(description = "关系人证件号")
private String relationCertNo;
/** 账户号码 */
@Schema(description = "账户号码")
private String accountNo;
/** 账户类型 */
@Schema(description = "账户类型")
private String accountType;
/** 账户范围 */
@Schema(description = "账户范围")
private String bankScope;
/** 账户姓名 */
@Schema(description = "账户姓名")
private String accountName;
/** 开户机构 */
@Schema(description = "开户机构")
private String openBank;
/** 银行代码 */
@Schema(description = "银行代码")
private String bankCode;
/** 币种 */
@Schema(description = "币种")
private String currency;
/** 状态 */
@Schema(description = "状态")
private Integer status;
/** 生效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "生效日期")
private Date effectiveDate;
/** 失效日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "失效日期")
private Date invalidDate;
/** 是否实控账户 */
@Schema(description = "是否实控账户")
private Integer isActualControl;
/** 月均交易笔数 */
@Schema(description = "月均交易笔数")
private Integer avgMonthTxnCount;
/** 月均交易金额 */
@Schema(description = "月均交易金额")
private BigDecimal avgMonthTxnAmount;
/** 频率等级 */
@Schema(description = "频率等级")
private String txnFrequencyLevel;
/** 借方单笔最高额 */
@Schema(description = "借方单笔最高额")
private BigDecimal debitSingleMaxAmount;
/** 贷方单笔最高额 */
@Schema(description = "贷方单笔最高额")
private BigDecimal creditSingleMaxAmount;
/** 借方日累计最高额 */
@Schema(description = "借方日累计最高额")
private BigDecimal debitDailyMaxAmount;
/** 贷方日累计最高额 */
@Schema(description = "贷方日累计最高额")
private BigDecimal creditDailyMaxAmount;
/** 风险等级 */
@Schema(description = "风险等级")
private String txnRiskLevel;
/** 创建者 */
@Schema(description = "创建者")
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
/** 更新者 */
@Schema(description = "更新者")
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新时间")
private Date updateTime;
}

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-13
*/
@Data
@Schema(description = "账户库关系人下拉选项")
public class CcdiAccountRelationOptionVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 关系人ID */
@Schema(description = "关系人ID")
private Long id;
/** 关系人姓名 */
@Schema(description = "关系人姓名")
private String relationName;
/** 关系类型 */
@Schema(description = "关系类型")
private String relationType;
/** 关系人证件号 */
@Schema(description = "关系人证件号")
private String relationCertNo;
}

View File

@@ -26,6 +26,11 @@ public class CcdiBaseStaffOptionVO {
*/
private Long deptId;
/**
* 身份证号
*/
private String idCard;
/**
* 部门名称
*/

View File

@@ -11,26 +11,26 @@ public enum RelationType {
/** 配偶 */
SPOUSE("配偶", "配偶"),
/** 父 */
FATHER_SON("", ""),
/** 父 */
FATHER("", ""),
/** 母 */
MOTHER_DAUGHTER("", ""),
/** 母 */
MOTHER("", ""),
/** 兄弟 */
BROTHER("兄弟", "兄弟"),
/** 子女 */
CHILDREN("子女", "子女"),
/** 姐妹 */
SISTER("姐妹", "姐妹"),
/** 亲属 */
RELATIVE("亲属", "亲属"),
/** 兄弟姐妹 */
SIBLINGS("兄弟姐妹", "兄弟姐妹"),
/** 朋友 */
FRIEND("朋友", "朋友"),
/** 同事 */
COLLEAGUE("同事", "同事");
COLLEAGUE("同事", "同事"),
/** 其他 */
OTHER("其他", "其他");
private final String code;
private final String desc;

View File

@@ -0,0 +1,54 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiAccountInfo;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoQueryDTO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountRelationOptionVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 账户库数据层
*
* @author ruoyi
* @date 2026-04-13
*/
public interface CcdiAccountInfoMapper extends BaseMapper<CcdiAccountInfo> {
/**
* 分页查询账户库
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 账户库分页结果
*/
Page<CcdiAccountInfoVO> selectAccountInfoPage(@Param("page") Page<CcdiAccountInfoVO> page,
@Param("query") CcdiAccountInfoQueryDTO queryDTO);
/**
* 查询账户库详情
*
* @param id 主键ID
* @return 账户库详情
*/
CcdiAccountInfoVO selectAccountInfoById(@Param("id") Long id);
/**
* 导出账户库列表
*
* @param queryDTO 查询条件
* @return 导出列表
*/
List<CcdiAccountInfoVO> selectAccountInfoListForExport(@Param("query") CcdiAccountInfoQueryDTO queryDTO);
/**
* 查询关系人下拉选项
*
* @param staffId 员工工号
* @return 关系人下拉
*/
List<CcdiAccountRelationOptionVO> selectRelationOptionsByStaffId(@Param("staffId") Long staffId);
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.info.collection.domain.CcdiAccountResult;
/**
* 账户分析结果数据层
*
* @author ruoyi
* @date 2026-04-13
*/
public interface CcdiAccountResultMapper extends BaseMapper<CcdiAccountResult> {
}

View File

@@ -0,0 +1,95 @@
package com.ruoyi.info.collection.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
import com.ruoyi.info.collection.domain.vo.AccountInfoImportFailureVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import java.util.List;
/**
* 账户库服务层
*
* @author ruoyi
* @date 2026-04-13
*/
public interface ICcdiAccountInfoService {
/**
* 分页查询账户库
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiAccountInfoVO> selectAccountInfoPage(Page<CcdiAccountInfoVO> page, CcdiAccountInfoQueryDTO queryDTO);
/**
* 查询账户库详情
*
* @param id 主键ID
* @return 账户库详情
*/
CcdiAccountInfoVO selectAccountInfoById(Long id);
/**
* 新增账户
*
* @param addDTO 新增DTO
* @return 影响行数
*/
int insertAccountInfo(CcdiAccountInfoAddDTO addDTO);
/**
* 修改账户
*
* @param editDTO 编辑DTO
* @return 影响行数
*/
int updateAccountInfo(CcdiAccountInfoEditDTO editDTO);
/**
* 批量删除账户
*
* @param ids 主键ID数组
* @return 影响行数
*/
int deleteAccountInfoByIds(Long[] ids);
/**
* 查询关系人下拉
*
* @param staffId 员工工号
* @return 下拉列表
*/
List<CcdiAccountRelationOptionVO> selectRelationOptionsByStaffId(Long staffId);
/**
* 导出账户库列表
*
* @param queryDTO 查询条件
* @return 导出列表
*/
List<CcdiAccountInfoExcel> selectAccountInfoListForExport(CcdiAccountInfoQueryDTO queryDTO);
/**
* 导入账户库信息
*
* @param excelList Excel数据
* @param updateSupport 是否更新已存在账户
* @return 导入结果
*/
ImportResult importAccountInfo(List<CcdiAccountInfoExcel> excelList, boolean updateSupport);
/**
* 获取导入失败记录
*
* @return 失败记录
*/
List<AccountInfoImportFailureVO> getLatestImportFailures();
}

View File

@@ -0,0 +1,618 @@
package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiAccountInfo;
import com.ruoyi.info.collection.domain.CcdiAccountResult;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
import com.ruoyi.info.collection.domain.vo.AccountInfoImportFailureVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiAccountResultMapper;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.service.ICcdiAccountInfoService;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 账户库服务实现
*
* @author ruoyi
* @date 2026-04-13
*/
@Service
public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private static final Set<String> OWNER_TYPES = Set.of("EMPLOYEE", "RELATION", "INTERMEDIARY", "EXTERNAL");
private static final Set<String> ACCOUNT_TYPES = Set.of("BANK", "SECURITIES", "PAYMENT", "OTHER");
private static final Set<String> BANK_SCOPES = Set.of("INTERNAL", "EXTERNAL");
private static final Set<String> LEVELS = Set.of("LOW", "MEDIUM", "HIGH");
private final List<AccountInfoImportFailureVO> latestImportFailures = new CopyOnWriteArrayList<>();
@Resource
private CcdiAccountInfoMapper accountInfoMapper;
@Resource
private CcdiAccountResultMapper accountResultMapper;
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
@Resource
private CcdiStaffFmyRelationMapper staffFmyRelationMapper;
@Override
public Page<CcdiAccountInfoVO> selectAccountInfoPage(Page<CcdiAccountInfoVO> page, CcdiAccountInfoQueryDTO queryDTO) {
return accountInfoMapper.selectAccountInfoPage(page, queryDTO);
}
@Override
public CcdiAccountInfoVO selectAccountInfoById(Long id) {
return accountInfoMapper.selectAccountInfoById(id);
}
@Override
@Transactional
public int insertAccountInfo(CcdiAccountInfoAddDTO addDTO) {
normalizeAddDto(addDTO);
validateDto(addDTO.getOwnerType(), addDTO.getOwnerId(), addDTO.getAccountType(), addDTO.getBankScope(),
addDTO.getStatus(), addDTO.getEffectiveDate(), addDTO.getInvalidDate(), addDTO.getTxnFrequencyLevel(),
addDTO.getTxnRiskLevel(), addDTO.getAvgMonthTxnAmount(), addDTO.getDebitSingleMaxAmount(),
addDTO.getCreditSingleMaxAmount(), addDTO.getDebitDailyMaxAmount(), addDTO.getCreditDailyMaxAmount());
validateDuplicateAccountNo(addDTO.getAccountNo(), null);
CcdiAccountInfo accountInfo = new CcdiAccountInfo();
BeanUtils.copyProperties(addDTO, accountInfo);
int result = accountInfoMapper.insert(accountInfo);
syncAccountResult(accountInfo.getBankScope(), null, accountInfo.getAccountNo(), addDTO);
return result;
}
@Override
@Transactional
public int updateAccountInfo(CcdiAccountInfoEditDTO editDTO) {
normalizeEditDto(editDTO);
validateDto(editDTO.getOwnerType(), editDTO.getOwnerId(), editDTO.getAccountType(), editDTO.getBankScope(),
editDTO.getStatus(), editDTO.getEffectiveDate(), editDTO.getInvalidDate(), editDTO.getTxnFrequencyLevel(),
editDTO.getTxnRiskLevel(), editDTO.getAvgMonthTxnAmount(), editDTO.getDebitSingleMaxAmount(),
editDTO.getCreditSingleMaxAmount(), editDTO.getDebitDailyMaxAmount(), editDTO.getCreditDailyMaxAmount());
CcdiAccountInfo existing = accountInfoMapper.selectById(editDTO.getId());
if (existing == null) {
throw new RuntimeException("账户不存在");
}
validateDuplicateAccountNo(editDTO.getAccountNo(), editDTO.getId());
CcdiAccountInfo accountInfo = new CcdiAccountInfo();
BeanUtils.copyProperties(editDTO, accountInfo);
int result = accountInfoMapper.updateById(accountInfo);
syncAccountResult(accountInfo.getBankScope(), existing, accountInfo.getAccountNo(), editDTO);
return result;
}
@Override
@Transactional
public int deleteAccountInfoByIds(Long[] ids) {
List<CcdiAccountInfo> accountList = accountInfoMapper.selectBatchIds(Arrays.asList(ids));
if (!accountList.isEmpty()) {
List<String> accountNos = accountList.stream()
.map(CcdiAccountInfo::getAccountNo)
.filter(StringUtils::isNotEmpty)
.toList();
if (!accountNos.isEmpty()) {
LambdaQueryWrapper<CcdiAccountResult> resultWrapper = new LambdaQueryWrapper<>();
resultWrapper.in(CcdiAccountResult::getAccountNo, accountNos);
accountResultMapper.delete(resultWrapper);
}
}
return accountInfoMapper.deleteBatchIds(Arrays.asList(ids));
}
@Override
public List<CcdiAccountRelationOptionVO> selectRelationOptionsByStaffId(Long staffId) {
if (staffId == null) {
return List.of();
}
return accountInfoMapper.selectRelationOptionsByStaffId(staffId);
}
@Override
public List<CcdiAccountInfoExcel> selectAccountInfoListForExport(CcdiAccountInfoQueryDTO queryDTO) {
return accountInfoMapper.selectAccountInfoListForExport(queryDTO).stream().map(this::toExcel).toList();
}
@Override
@Transactional
public ImportResult importAccountInfo(List<CcdiAccountInfoExcel> excelList, boolean updateSupport) {
latestImportFailures.clear();
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
int successCount = 0;
int rowNum = 1;
for (CcdiAccountInfoExcel excel : excelList) {
rowNum++;
try {
importSingleRow(excel, updateSupport);
successCount++;
} catch (Exception e) {
latestImportFailures.add(buildFailure(rowNum, excel, e.getMessage()));
}
}
result.setSuccessCount(successCount);
result.setFailureCount(latestImportFailures.size());
return result;
}
@Override
public List<AccountInfoImportFailureVO> getLatestImportFailures() {
return new ArrayList<>(latestImportFailures);
}
private void validateDto(String ownerType, String ownerId, String accountType, String bankScope, Integer status,
java.util.Date effectiveDate, java.util.Date invalidDate, String txnFrequencyLevel,
String txnRiskLevel, BigDecimal avgMonthTxnAmount, BigDecimal debitSingleMaxAmount,
BigDecimal creditSingleMaxAmount, BigDecimal debitDailyMaxAmount,
BigDecimal creditDailyMaxAmount) {
if (!OWNER_TYPES.contains(ownerType)) {
throw new RuntimeException("所属人类型不合法");
}
if (!ACCOUNT_TYPES.contains(accountType)) {
throw new RuntimeException("账户类型不合法");
}
if (!BANK_SCOPES.contains(bankScope)) {
throw new RuntimeException("账户范围不合法");
}
if (status == null || (status != 1 && status != 2)) {
throw new RuntimeException("状态不合法");
}
if (effectiveDate == null) {
throw new RuntimeException("生效日期不能为空");
}
if (invalidDate != null && invalidDate.before(effectiveDate)) {
throw new RuntimeException("失效日期不能早于生效日期");
}
if (StringUtils.isNotEmpty(txnFrequencyLevel) && !LEVELS.contains(txnFrequencyLevel)) {
throw new RuntimeException("频率等级不合法");
}
if (StringUtils.isNotEmpty(txnRiskLevel) && !LEVELS.contains(txnRiskLevel)) {
throw new RuntimeException("风险等级不合法");
}
validateAmount(avgMonthTxnAmount, "月均交易金额");
validateAmount(debitSingleMaxAmount, "借方单笔最高额");
validateAmount(creditSingleMaxAmount, "贷方单笔最高额");
validateAmount(debitDailyMaxAmount, "借方日累计最高额");
validateAmount(creditDailyMaxAmount, "贷方日累计最高额");
validateOwner(ownerType, ownerId);
}
private void validateOwner(String ownerType, String ownerId) {
if (StringUtils.isEmpty(ownerId)) {
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
throw new RuntimeException("证件号不能为空");
}
throw new RuntimeException("所属人不能为空");
}
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
return;
}
if ("EMPLOYEE".equals(ownerType)) {
LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiBaseStaff::getIdCard, ownerId);
CcdiBaseStaff staff = baseStaffMapper.selectOne(wrapper);
if (staff == null) {
throw new RuntimeException("员工不存在");
}
return;
}
LambdaQueryWrapper<CcdiStaffFmyRelation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiStaffFmyRelation::getRelationCertNo, ownerId);
CcdiStaffFmyRelation relation = staffFmyRelationMapper.selectOne(wrapper);
if (relation == null) {
throw new RuntimeException("关系人不存在");
}
}
private void validateDuplicateAccountNo(String accountNo, Long excludeId) {
LambdaQueryWrapper<CcdiAccountInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiAccountInfo::getAccountNo, accountNo);
if (excludeId != null) {
wrapper.ne(CcdiAccountInfo::getId, excludeId);
}
if (accountInfoMapper.selectCount(wrapper) > 0) {
throw new RuntimeException("账户号码已存在");
}
}
private void syncAccountResult(String newBankScope, CcdiAccountInfo existing, String accountNo, Object dto) {
String oldBankScope = existing == null ? null : existing.getBankScope();
String oldAccountNo = existing == null ? null : existing.getAccountNo();
if (existing != null && "EXTERNAL".equals(oldBankScope)
&& (!"EXTERNAL".equals(newBankScope) || !StringUtils.equals(oldAccountNo, accountNo))) {
LambdaQueryWrapper<CcdiAccountResult> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(CcdiAccountResult::getAccountNo, oldAccountNo);
accountResultMapper.delete(deleteWrapper);
}
if (!"EXTERNAL".equals(newBankScope)) {
return;
}
LambdaQueryWrapper<CcdiAccountResult> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiAccountResult::getAccountNo, accountNo);
CcdiAccountResult existingResult = accountResultMapper.selectOne(wrapper);
CcdiAccountResult accountResult = new CcdiAccountResult();
BeanUtils.copyProperties(dto, accountResult);
accountResult.setAccountNo(accountNo);
if (accountResult.getIsActualControl() == null) {
accountResult.setIsActualControl(1);
}
if (accountResult.getAvgMonthTxnCount() == null) {
accountResult.setAvgMonthTxnCount(0);
}
if (accountResult.getAvgMonthTxnAmount() == null) {
accountResult.setAvgMonthTxnAmount(BigDecimal.ZERO);
}
if (StringUtils.isEmpty(accountResult.getTxnFrequencyLevel())) {
accountResult.setTxnFrequencyLevel("MEDIUM");
}
if (StringUtils.isEmpty(accountResult.getTxnRiskLevel())) {
accountResult.setTxnRiskLevel("LOW");
}
if (existingResult == null) {
accountResultMapper.insert(accountResult);
return;
}
accountResult.setResultId(existingResult.getResultId());
accountResultMapper.updateById(accountResult);
}
private void validateAmount(BigDecimal amount, String fieldLabel) {
if (amount == null) {
return;
}
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException(fieldLabel + "不能为负数");
}
if (amount.scale() > 2) {
throw new RuntimeException(fieldLabel + "最多保留2位小数");
}
}
private void normalizeAddDto(CcdiAccountInfoAddDTO addDTO) {
addDTO.setOwnerType(toUpper(addDTO.getOwnerType()));
addDTO.setAccountType(toUpper(addDTO.getAccountType()));
addDTO.setBankScope(toUpper(addDTO.getBankScope()));
addDTO.setCurrency(normalizeCurrency(addDTO.getCurrency()));
addDTO.setTxnFrequencyLevel(toUpper(addDTO.getTxnFrequencyLevel()));
addDTO.setTxnRiskLevel(toUpper(addDTO.getTxnRiskLevel()));
addDTO.setOwnerId(normalizeOwnerId(addDTO.getOwnerId()));
}
private void normalizeEditDto(CcdiAccountInfoEditDTO editDTO) {
editDTO.setOwnerType(toUpper(editDTO.getOwnerType()));
editDTO.setAccountType(toUpper(editDTO.getAccountType()));
editDTO.setBankScope(toUpper(editDTO.getBankScope()));
editDTO.setCurrency(normalizeCurrency(editDTO.getCurrency()));
editDTO.setTxnFrequencyLevel(toUpper(editDTO.getTxnFrequencyLevel()));
editDTO.setTxnRiskLevel(toUpper(editDTO.getTxnRiskLevel()));
editDTO.setOwnerId(normalizeOwnerId(editDTO.getOwnerId()));
}
private String normalizeCurrency(String currency) {
if (StringUtils.isEmpty(currency)) {
return "CNY";
}
return currency.trim().toUpperCase(Locale.ROOT);
}
private String toUpper(String value) {
if (StringUtils.isEmpty(value)) {
return value;
}
return value.trim().toUpperCase(Locale.ROOT);
}
private String normalizeOwnerId(String ownerId) {
if (StringUtils.isEmpty(ownerId)) {
return ownerId;
}
return ownerId.trim();
}
private CcdiAccountInfoExcel toExcel(CcdiAccountInfoVO vo) {
CcdiAccountInfoExcel excel = new CcdiAccountInfoExcel();
excel.setOwnerType(ownerTypeLabel(vo.getOwnerType()));
excel.setOwnerId(vo.getOwnerId());
excel.setAccountName(vo.getAccountName());
excel.setAccountNo(vo.getAccountNo());
excel.setAccountType(accountTypeLabel(vo.getAccountType()));
excel.setBankScope(bankScopeLabel(vo.getBankScope()));
excel.setOpenBank(vo.getOpenBank());
excel.setBankCode(vo.getBankCode());
excel.setCurrency(vo.getCurrency());
excel.setStatus(vo.getStatus() == null ? "" : (vo.getStatus() == 1 ? "正常" : "已销户"));
excel.setEffectiveDate(formatDate(vo.getEffectiveDate()));
excel.setInvalidDate(formatDate(vo.getInvalidDate()));
excel.setIsActualControl(formatYesNo(vo.getIsActualControl()));
excel.setAvgMonthTxnCount(vo.getAvgMonthTxnCount() == null ? "" : String.valueOf(vo.getAvgMonthTxnCount()));
excel.setAvgMonthTxnAmount(formatNumber(vo.getAvgMonthTxnAmount()));
excel.setTxnFrequencyLevel(StringUtils.isEmpty(vo.getTxnFrequencyLevel()) ? "" : vo.getTxnFrequencyLevel());
excel.setDebitSingleMaxAmount(formatNumber(vo.getDebitSingleMaxAmount()));
excel.setCreditSingleMaxAmount(formatNumber(vo.getCreditSingleMaxAmount()));
excel.setDebitDailyMaxAmount(formatNumber(vo.getDebitDailyMaxAmount()));
excel.setCreditDailyMaxAmount(formatNumber(vo.getCreditDailyMaxAmount()));
excel.setTxnRiskLevel(StringUtils.isEmpty(vo.getTxnRiskLevel()) ? "" : vo.getTxnRiskLevel());
return excel;
}
private void importSingleRow(CcdiAccountInfoExcel excel, boolean updateSupport) {
CcdiAccountInfo existing = findByAccountNo(excel.getAccountNo());
if (existing != null && !updateSupport) {
throw new RuntimeException("账户号码已存在,请勾选更新已存在数据后重试");
}
if (existing != null) {
CcdiAccountInfoEditDTO editDTO = toEditDto(existing.getId(), excel);
updateAccountInfo(editDTO);
return;
}
CcdiAccountInfoAddDTO addDTO = toAddDto(excel);
insertAccountInfo(addDTO);
}
private CcdiAccountInfoAddDTO toAddDto(CcdiAccountInfoExcel excel) {
CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
dto.setOwnerType(parseOwnerType(excel.getOwnerType()));
dto.setOwnerId(normalizeOwnerId(excel.getOwnerId()));
dto.setAccountName(trimToNull(excel.getAccountName()));
dto.setAccountNo(trimToNull(excel.getAccountNo()));
dto.setAccountType(parseAccountType(excel.getAccountType()));
dto.setBankScope(parseBankScope(excel.getBankScope()));
dto.setOpenBank(trimToNull(excel.getOpenBank()));
dto.setBankCode(trimToNull(excel.getBankCode()));
dto.setCurrency(normalizeCurrency(excel.getCurrency()));
dto.setStatus(parseStatus(excel.getStatus()));
dto.setEffectiveDate(parseDateRequired(excel.getEffectiveDate(), "生效日期"));
dto.setInvalidDate(parseDateOptional(excel.getInvalidDate()));
dto.setIsActualControl(parseBooleanFlag(excel.getIsActualControl(), 1));
dto.setAvgMonthTxnCount(parseInteger(excel.getAvgMonthTxnCount()));
dto.setAvgMonthTxnAmount(parseDecimal(excel.getAvgMonthTxnAmount()));
dto.setTxnFrequencyLevel(parseLevel(excel.getTxnFrequencyLevel(), "频率等级"));
dto.setDebitSingleMaxAmount(parseDecimal(excel.getDebitSingleMaxAmount()));
dto.setCreditSingleMaxAmount(parseDecimal(excel.getCreditSingleMaxAmount()));
dto.setDebitDailyMaxAmount(parseDecimal(excel.getDebitDailyMaxAmount()));
dto.setCreditDailyMaxAmount(parseDecimal(excel.getCreditDailyMaxAmount()));
dto.setTxnRiskLevel(parseLevel(excel.getTxnRiskLevel(), "风险等级"));
return dto;
}
private CcdiAccountInfoEditDTO toEditDto(Long id, CcdiAccountInfoExcel excel) {
CcdiAccountInfoEditDTO dto = new CcdiAccountInfoEditDTO();
BeanUtils.copyProperties(toAddDto(excel), dto);
dto.setId(id);
return dto;
}
private CcdiAccountInfo findByAccountNo(String accountNo) {
LambdaQueryWrapper<CcdiAccountInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiAccountInfo::getAccountNo, trimToNull(accountNo));
return accountInfoMapper.selectOne(wrapper);
}
private AccountInfoImportFailureVO buildFailure(int rowNum, CcdiAccountInfoExcel excel, String errorMessage) {
AccountInfoImportFailureVO failure = new AccountInfoImportFailureVO();
failure.setRowNum(rowNum);
failure.setOwnerType(excel.getOwnerType());
failure.setOwnerId(excel.getOwnerId());
failure.setAccountNo(excel.getAccountNo());
failure.setErrorMessage(errorMessage);
return failure;
}
private String parseOwnerType(String value) {
String normalized = toUpper(value);
if ("员工".equals(value)) {
return "EMPLOYEE";
}
if ("员工关系人".equals(value)) {
return "RELATION";
}
if ("中介".equals(value)) {
return "INTERMEDIARY";
}
if ("外部人员".equals(value)) {
return "EXTERNAL";
}
return normalized;
}
private String parseAccountType(String value) {
String normalized = toUpper(value);
return switch (normalized) {
case "银行账户" -> "BANK";
case "证券账户" -> "SECURITIES";
case "支付账户" -> "PAYMENT";
case "其他" -> "OTHER";
default -> normalized;
};
}
private String parseBankScope(String value) {
String normalized = toUpper(value);
return switch (normalized) {
case "行内" -> "INTERNAL";
case "行外" -> "EXTERNAL";
default -> normalized;
};
}
private Integer parseStatus(String value) {
String normalized = trimToNull(value);
if (normalized == null) {
return null;
}
return switch (normalized) {
case "1", "正常" -> 1;
case "2", "已销户" -> 2;
default -> throw new RuntimeException("状态仅支持“正常/已销户”或“1/2”");
};
}
private Integer parseBooleanFlag(String value, Integer defaultValue) {
String normalized = trimToNull(value);
if (normalized == null) {
return defaultValue;
}
return switch (normalized) {
case "1", "", "Y", "YES", "TRUE", "true" -> 1;
case "0", "", "N", "NO", "FALSE", "false" -> 0;
default -> throw new RuntimeException("是否实控账户仅支持“是/否”或“1/0”");
};
}
private Integer parseInteger(String value) {
String normalized = trimToNull(value);
if (normalized == null) {
return null;
}
try {
return Integer.valueOf(normalized);
} catch (NumberFormatException e) {
throw new RuntimeException("月均交易笔数格式不正确");
}
}
private BigDecimal parseDecimal(String value) {
String normalized = trimToNull(value);
if (normalized == null) {
return null;
}
try {
return new BigDecimal(normalized);
} catch (NumberFormatException e) {
throw new RuntimeException("金额字段格式不正确");
}
}
private String parseLevel(String value, String fieldLabel) {
String normalized = toUpper(value);
if (StringUtils.isEmpty(normalized)) {
return null;
}
return switch (normalized) {
case "", "LOW" -> "LOW";
case "", "MEDIUM" -> "MEDIUM";
case "", "HIGH" -> "HIGH";
default -> throw new RuntimeException(fieldLabel + "仅支持 LOW/MEDIUM/HIGH");
};
}
private Date parseDateRequired(String value, String fieldLabel) {
Date date = parseDateOptional(value);
if (date == null) {
throw new RuntimeException(fieldLabel + "不能为空");
}
return date;
}
private Date parseDateOptional(String value) {
String normalized = trimToNull(value);
if (normalized == null) {
return null;
}
try {
synchronized (DATE_FORMAT) {
return DATE_FORMAT.parse(normalized);
}
} catch (ParseException e) {
throw new RuntimeException("日期格式需为 yyyy-MM-dd");
}
}
private String trimToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
return value.trim();
}
private String ownerTypeLabel(String value) {
return switch (value) {
case "EMPLOYEE" -> "员工";
case "RELATION" -> "员工关系人";
case "INTERMEDIARY" -> "中介";
case "EXTERNAL" -> "外部人员";
default -> value;
};
}
private String accountTypeLabel(String value) {
return switch (value) {
case "BANK" -> "银行账户";
case "SECURITIES" -> "证券账户";
case "PAYMENT" -> "支付账户";
case "OTHER" -> "其他";
default -> value;
};
}
private String bankScopeLabel(String value) {
return switch (value) {
case "INTERNAL" -> "行内";
case "EXTERNAL" -> "行外";
default -> value;
};
}
private String formatYesNo(Integer value) {
if (value == null) {
return "";
}
return value == 1 ? "" : "";
}
private String formatDate(Date value) {
if (value == null) {
return "";
}
synchronized (DATE_FORMAT) {
return DATE_FORMAT.format(value);
}
}
private String formatNumber(Number value) {
return value == null ? "" : String.valueOf(value);
}
}

View File

@@ -0,0 +1,168 @@
<?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.info.collection.mapper.CcdiAccountInfoMapper">
<resultMap id="CcdiAccountInfoVOResult" type="com.ruoyi.info.collection.domain.vo.CcdiAccountInfoVO">
<id property="id" column="id"/>
<result property="ownerType" column="ownerType"/>
<result property="ownerId" column="ownerId"/>
<result property="staffId" column="staffId"/>
<result property="staffName" column="staffName"/>
<result property="relationId" column="relationId"/>
<result property="relationType" column="relationType"/>
<result property="relationName" column="relationName"/>
<result property="relationCertNo" column="relationCertNo"/>
<result property="accountNo" column="accountNo"/>
<result property="accountType" column="accountType"/>
<result property="bankScope" column="bankScope"/>
<result property="accountName" column="accountName"/>
<result property="openBank" column="openBank"/>
<result property="bankCode" column="bankCode"/>
<result property="currency" column="currency"/>
<result property="status" column="status"/>
<result property="effectiveDate" column="effectiveDate"/>
<result property="invalidDate" column="invalidDate"/>
<result property="isActualControl" column="isActualControl"/>
<result property="avgMonthTxnCount" column="avgMonthTxnCount"/>
<result property="avgMonthTxnAmount" column="avgMonthTxnAmount"/>
<result property="txnFrequencyLevel" column="txnFrequencyLevel"/>
<result property="debitSingleMaxAmount" column="debitSingleMaxAmount"/>
<result property="creditSingleMaxAmount" column="creditSingleMaxAmount"/>
<result property="debitDailyMaxAmount" column="debitDailyMaxAmount"/>
<result property="creditDailyMaxAmount" column="creditDailyMaxAmount"/>
<result property="txnRiskLevel" column="txnRiskLevel"/>
<result property="createBy" column="createBy"/>
<result property="createTime" column="createTime"/>
<result property="updateBy" column="updateBy"/>
<result property="updateTime" column="updateTime"/>
</resultMap>
<sql id="AccountInfoSelectColumns">
ai.account_id AS id,
ai.owner_type AS ownerType,
ai.owner_id AS ownerId,
CASE
WHEN ai.owner_type = 'EMPLOYEE' THEN bs.staff_id
WHEN ai.owner_type = 'RELATION' THEN bsRel.staff_id
ELSE NULL
END AS staffId,
CASE
WHEN ai.owner_type = 'EMPLOYEE' THEN bs.name
WHEN ai.owner_type = 'RELATION' THEN bsRel.name
ELSE NULL
END AS staffName,
CASE WHEN ai.owner_type = 'RELATION' THEN fr.id ELSE NULL END AS relationId,
CASE WHEN ai.owner_type = 'RELATION' THEN fr.relation_type ELSE NULL END AS relationType,
CASE WHEN ai.owner_type = 'RELATION' THEN fr.relation_name ELSE NULL END AS relationName,
CASE WHEN ai.owner_type = 'RELATION' THEN fr.relation_cert_no ELSE NULL END AS relationCertNo,
ai.account_no AS accountNo,
ai.account_type AS accountType,
ai.bank_scope AS bankScope,
ai.account_name AS accountName,
ai.bank AS openBank,
ai.bank_code AS bankCode,
ai.currency AS currency,
ai.status AS status,
ai.effective_date AS effectiveDate,
ai.invalid_date AS invalidDate,
ar.is_self_account AS isActualControl,
ar.monthly_avg_trans_count AS avgMonthTxnCount,
ar.monthly_avg_trans_amount AS avgMonthTxnAmount,
ar.trans_freq_type AS txnFrequencyLevel,
ar.dr_max_single_amount AS debitSingleMaxAmount,
ar.cr_max_single_amount AS creditSingleMaxAmount,
ar.dr_max_daily_amount AS debitDailyMaxAmount,
ar.cr_max_daily_amount AS creditDailyMaxAmount,
ar.trans_risk_level AS txnRiskLevel,
ai.create_by AS createBy,
ai.create_time AS createTime,
ai.update_by AS updateBy,
ai.update_time AS updateTime
</sql>
<sql id="AccountInfoWhereClause">
WHERE 1 = 1
<if test="query.staffName != null and query.staffName != ''">
AND (
(ai.owner_type = 'EMPLOYEE' AND bs.name LIKE CONCAT('%', #{query.staffName}, '%'))
OR
(ai.owner_type = 'RELATION' AND bsRel.name LIKE CONCAT('%', #{query.staffName}, '%'))
)
</if>
<if test="query.ownerType != null and query.ownerType != ''">
AND ai.owner_type = #{query.ownerType}
</if>
<if test="query.bankScope != null and query.bankScope != ''">
AND ai.bank_scope = #{query.bankScope}
</if>
<if test="query.relationType != null and query.relationType != ''">
AND fr.relation_type = #{query.relationType}
</if>
<if test="query.accountName != null and query.accountName != ''">
AND ai.account_name LIKE CONCAT('%', #{query.accountName}, '%')
</if>
<if test="query.accountType != null and query.accountType != ''">
AND ai.account_type = #{query.accountType}
</if>
<if test="query.isActualControl != null">
AND ar.is_self_account = #{query.isActualControl}
</if>
<if test="query.riskLevel != null and query.riskLevel != ''">
AND ar.trans_risk_level = #{query.riskLevel}
</if>
<if test="query.status != null">
AND ai.status = #{query.status}
</if>
</sql>
<select id="selectAccountInfoPage" resultMap="CcdiAccountInfoVOResult">
SELECT
<include refid="AccountInfoSelectColumns"/>
FROM ccdi_account_info ai
LEFT JOIN ccdi_account_result ar ON ai.account_no = ar.account_no
LEFT JOIN ccdi_base_staff bs ON ai.owner_type = 'EMPLOYEE' AND ai.owner_id = bs.id_card
LEFT JOIN ccdi_staff_fmy_relation fr ON ai.owner_type = 'RELATION' AND ai.owner_id = fr.relation_cert_no
LEFT JOIN ccdi_base_staff bsRel ON fr.person_id = bsRel.id_card
<include refid="AccountInfoWhereClause"/>
ORDER BY ai.update_time DESC, ai.account_id DESC
</select>
<select id="selectAccountInfoListForExport" resultMap="CcdiAccountInfoVOResult">
SELECT
<include refid="AccountInfoSelectColumns"/>
FROM ccdi_account_info ai
LEFT JOIN ccdi_account_result ar ON ai.account_no = ar.account_no
LEFT JOIN ccdi_base_staff bs ON ai.owner_type = 'EMPLOYEE' AND ai.owner_id = bs.id_card
LEFT JOIN ccdi_staff_fmy_relation fr ON ai.owner_type = 'RELATION' AND ai.owner_id = fr.relation_cert_no
LEFT JOIN ccdi_base_staff bsRel ON fr.person_id = bsRel.id_card
<include refid="AccountInfoWhereClause"/>
ORDER BY ai.update_time DESC, ai.account_id DESC
</select>
<select id="selectAccountInfoById" resultMap="CcdiAccountInfoVOResult">
SELECT
<include refid="AccountInfoSelectColumns"/>
FROM ccdi_account_info ai
LEFT JOIN ccdi_account_result ar ON ai.account_no = ar.account_no
LEFT JOIN ccdi_base_staff bs ON ai.owner_type = 'EMPLOYEE' AND ai.owner_id = bs.id_card
LEFT JOIN ccdi_staff_fmy_relation fr ON ai.owner_type = 'RELATION' AND ai.owner_id = fr.relation_cert_no
LEFT JOIN ccdi_base_staff bsRel ON fr.person_id = bsRel.id_card
WHERE ai.account_id = #{id}
</select>
<select id="selectRelationOptionsByStaffId" resultType="com.ruoyi.info.collection.domain.vo.CcdiAccountRelationOptionVO">
SELECT
fr.id,
fr.relation_name AS relationName,
fr.relation_type AS relationType,
fr.relation_cert_no AS relationCertNo
FROM ccdi_staff_fmy_relation fr
INNER JOIN ccdi_base_staff bs ON fr.person_id = bs.id_card
WHERE bs.staff_id = #{staffId}
AND fr.is_emp_family = 1
AND fr.status = 1
ORDER BY fr.id DESC
</select>
</mapper>

View File

@@ -86,6 +86,7 @@
e.staff_id,
e.name,
e.dept_id,
e.id_card,
d.dept_name
FROM ccdi_base_staff e
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id