diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAccountInfoController.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAccountInfoController.java new file mode 100644 index 00000000..baf1fcd9 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAccountInfoController.java @@ -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 page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize()); + Page 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 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 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 list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiAccountInfoExcel.class); + if (list == null || list.isEmpty()) { + return error("至少需要一条数据"); + } + ImportResult result = accountInfoService.importAccountInfo(list, updateSupport); + List failures = accountInfoService.getLatestImportFailures(); + Map 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); + } +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountInfo.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountInfo.java new file mode 100644 index 00000000..4709db89 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountInfo.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountResult.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountResult.java new file mode 100644 index 00000000..0f76037e --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountResult.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoAddDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoAddDTO.java new file mode 100644 index 00000000..b9565f18 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoAddDTO.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoEditDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoEditDTO.java new file mode 100644 index 00000000..096208e3 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoEditDTO.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoQueryDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoQueryDTO.java new file mode 100644 index 00000000..411aeed7 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiAccountInfoQueryDTO.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiAccountInfoExcel.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiAccountInfoExcel.java new file mode 100644 index 00000000..8218d2b9 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiAccountInfoExcel.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AccountInfoImportFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AccountInfoImportFailureVO.java new file mode 100644 index 00000000..3229c7ad --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AccountInfoImportFailureVO.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiAccountInfoVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiAccountInfoVO.java new file mode 100644 index 00000000..f652d236 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiAccountInfoVO.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiAccountRelationOptionVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiAccountRelationOptionVO.java new file mode 100644 index 00000000..b1c7c993 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiAccountRelationOptionVO.java @@ -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; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiBaseStaffOptionVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiBaseStaffOptionVO.java index 6c0d6125..8ef050c7 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiBaseStaffOptionVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiBaseStaffOptionVO.java @@ -26,6 +26,11 @@ public class CcdiBaseStaffOptionVO { */ private Long deptId; + /** + * 身份证号 + */ + private String idCard; + /** * 部门名称 */ diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RelationType.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RelationType.java index 3f86273d..8dbab6b6 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RelationType.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RelationType.java @@ -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; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountInfoMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountInfoMapper.java new file mode 100644 index 00000000..a0e24ad7 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountInfoMapper.java @@ -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 { + + /** + * 分页查询账户库 + * + * @param page 分页对象 + * @param queryDTO 查询条件 + * @return 账户库分页结果 + */ + Page selectAccountInfoPage(@Param("page") Page page, + @Param("query") CcdiAccountInfoQueryDTO queryDTO); + + /** + * 查询账户库详情 + * + * @param id 主键ID + * @return 账户库详情 + */ + CcdiAccountInfoVO selectAccountInfoById(@Param("id") Long id); + + /** + * 导出账户库列表 + * + * @param queryDTO 查询条件 + * @return 导出列表 + */ + List selectAccountInfoListForExport(@Param("query") CcdiAccountInfoQueryDTO queryDTO); + + /** + * 查询关系人下拉选项 + * + * @param staffId 员工工号 + * @return 关系人下拉 + */ + List selectRelationOptionsByStaffId(@Param("staffId") Long staffId); +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountResultMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountResultMapper.java new file mode 100644 index 00000000..29c84f71 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountResultMapper.java @@ -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 { +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiAccountInfoService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiAccountInfoService.java new file mode 100644 index 00000000..07c2637e --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiAccountInfoService.java @@ -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 selectAccountInfoPage(Page 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 selectRelationOptionsByStaffId(Long staffId); + + /** + * 导出账户库列表 + * + * @param queryDTO 查询条件 + * @return 导出列表 + */ + List selectAccountInfoListForExport(CcdiAccountInfoQueryDTO queryDTO); + + /** + * 导入账户库信息 + * + * @param excelList Excel数据 + * @param updateSupport 是否更新已存在账户 + * @return 导入结果 + */ + ImportResult importAccountInfo(List excelList, boolean updateSupport); + + /** + * 获取导入失败记录 + * + * @return 失败记录 + */ + List getLatestImportFailures(); +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAccountInfoServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAccountInfoServiceImpl.java new file mode 100644 index 00000000..3383b5ad --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAccountInfoServiceImpl.java @@ -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 OWNER_TYPES = Set.of("EMPLOYEE", "RELATION", "INTERMEDIARY", "EXTERNAL"); + private static final Set ACCOUNT_TYPES = Set.of("BANK", "SECURITIES", "PAYMENT", "OTHER"); + private static final Set BANK_SCOPES = Set.of("INTERNAL", "EXTERNAL"); + private static final Set LEVELS = Set.of("LOW", "MEDIUM", "HIGH"); + private final List latestImportFailures = new CopyOnWriteArrayList<>(); + + @Resource + private CcdiAccountInfoMapper accountInfoMapper; + + @Resource + private CcdiAccountResultMapper accountResultMapper; + + @Resource + private CcdiBaseStaffMapper baseStaffMapper; + + @Resource + private CcdiStaffFmyRelationMapper staffFmyRelationMapper; + + @Override + public Page selectAccountInfoPage(Page 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 accountList = accountInfoMapper.selectBatchIds(Arrays.asList(ids)); + if (!accountList.isEmpty()) { + List accountNos = accountList.stream() + .map(CcdiAccountInfo::getAccountNo) + .filter(StringUtils::isNotEmpty) + .toList(); + if (!accountNos.isEmpty()) { + LambdaQueryWrapper resultWrapper = new LambdaQueryWrapper<>(); + resultWrapper.in(CcdiAccountResult::getAccountNo, accountNos); + accountResultMapper.delete(resultWrapper); + } + } + return accountInfoMapper.deleteBatchIds(Arrays.asList(ids)); + } + + @Override + public List selectRelationOptionsByStaffId(Long staffId) { + if (staffId == null) { + return List.of(); + } + return accountInfoMapper.selectRelationOptionsByStaffId(staffId); + } + + @Override + public List selectAccountInfoListForExport(CcdiAccountInfoQueryDTO queryDTO) { + return accountInfoMapper.selectAccountInfoListForExport(queryDTO).stream().map(this::toExcel).toList(); + } + + @Override + @Transactional + public ImportResult importAccountInfo(List 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 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CcdiBaseStaff::getIdCard, ownerId); + CcdiBaseStaff staff = baseStaffMapper.selectOne(wrapper); + if (staff == null) { + throw new RuntimeException("员工不存在"); + } + return; + } + LambdaQueryWrapper 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 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 deleteWrapper = new LambdaQueryWrapper<>(); + deleteWrapper.eq(CcdiAccountResult::getAccountNo, oldAccountNo); + accountResultMapper.delete(deleteWrapper); + } + + if (!"EXTERNAL".equals(newBankScope)) { + return; + } + + LambdaQueryWrapper 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 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); + } +} diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml new file mode 100644 index 00000000..29297ae7 --- /dev/null +++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + WHERE 1 = 1 + + AND ( + (ai.owner_type = 'EMPLOYEE' AND bs.name LIKE CONCAT('%', #{query.staffName}, '%')) + OR + (ai.owner_type = 'RELATION' AND bsRel.name LIKE CONCAT('%', #{query.staffName}, '%')) + ) + + + AND ai.owner_type = #{query.ownerType} + + + AND ai.bank_scope = #{query.bankScope} + + + AND fr.relation_type = #{query.relationType} + + + AND ai.account_name LIKE CONCAT('%', #{query.accountName}, '%') + + + AND ai.account_type = #{query.accountType} + + + AND ar.is_self_account = #{query.isActualControl} + + + AND ar.trans_risk_level = #{query.riskLevel} + + + AND ai.status = #{query.status} + + + + + + + + + + + diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiBaseStaffMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiBaseStaffMapper.xml index 09e9d869..d2e28274 100644 --- a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiBaseStaffMapper.xml +++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiBaseStaffMapper.xml @@ -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 diff --git a/docs/plans/fullstack/2026-04-10-account-library-handoff.md b/docs/plans/fullstack/2026-04-10-account-library-handoff.md new file mode 100644 index 00000000..a63c59ad --- /dev/null +++ b/docs/plans/fullstack/2026-04-10-account-library-handoff.md @@ -0,0 +1,96 @@ +# 账户库管理交接记录 + +日期:2026-04-10 + +## 当前状态 + +- 前端原型页:`ruoyi-ui/src/views/ccdiAccountInfoPrototype/index.vue` +- 本地预览路由:`/ccdiAccountInfo` +- 原型页入口路由:`/prototype/account-library` +- 当前截图:`docs/plans/fullstack/account-library-preview-2026-04-10.png` +- 真实数据库:`ccdi.ccdi_account_info`、`ccdi.ccdi_account_result` +- 当前实现仍是前端静态原型,尚未接真实后端接口。 + +## 本次已落地 + +1. `ccdi_account_info` 已新增字段: + - 字段名:`bank_scope` + - 类型:`VARCHAR(20) NOT NULL DEFAULT 'INTERNAL'` + - 含义:`INTERNAL-行内,EXTERNAL-行外` + - 当前已有 21 条账户数据均为 `INTERNAL` + +2. `ccdi_account_result.trans_risk_level` 默认值已改为 `LOW`。 + +3. 前端原型已恢复“账户范围”: + - 查询区支持按“行内/行外”筛选 + - 表格展示“账户范围” + - 新增/编辑弹窗可选择“账户范围” + - 行内账户的分析信息默认只展示,语义上由 T+1 自动同步维护 + - 行外账户的分析信息允许手工维护 + +4. 前端原型兼容当前数据库 `account_type = DEBIT` 的老数据: + - `DEBIT` 展示为“借记卡账户” + - 仍保留 `BANK`、`SECURITIES`、`PAYMENT`、`OTHER` 作为后续扩展选项 + +5. 关系类型口径已按员工亲属关系统一为: + - 配偶、父亲、母亲、子女、兄弟姐妹、朋友、同事、其他 + +## 字段对应 + +### `ccdi_account_info` + +| 页面字段 | 前端字段 | 数据库字段 | 说明 | +| --- | --- | --- | --- | +| 账户号码 | `accountNo` | `account_no` | 账号 | +| 账户类型 | `accountType` | `account_type` | 当前库里有 `DEBIT` | +| 账户范围 | `bankScope` | `bank_scope` | `INTERNAL` 行内,`EXTERNAL` 行外 | +| 账户姓名 | `accountName` | `account_name` | 账户户名/所属姓名 | +| 所属人类型 | `ownerType` | `owner_type` | 员工、员工关系人、外部人员 | +| 所属人标识 | `staffId` / `relationId` | `owner_id` | 后续接口需按类型取值 | +| 开户机构 | `openBank` | `bank` | 开户行/机构 | +| 银行代码 | `bankCode` | `bank_code` | 机构代码 | +| 币种 | `currency` | `currency` | 默认 `CNY` | +| 状态 | `status` | `status` | `1` 正常,`2` 已销户 | +| 生效日期 | `effectiveDate` | `effective_date` | 开户/生效日期 | +| 失效日期 | `invalidDate` | `invalid_date` | 销户/失效日期 | + +### `ccdi_account_result` + +| 页面字段 | 前端字段 | 数据库字段 | 说明 | +| --- | --- | --- | --- | +| 是否实控账户 | `isActualControl` | `is_self_account` | `1` 是,`0` 否 | +| 月均交易笔数 | `avgMonthTxnCount` | `monthly_avg_trans_count` | 交易画像 | +| 月均交易金额 | `avgMonthTxnAmount` | `monthly_avg_trans_amount` | 交易画像 | +| 频率等级 | `txnFrequencyLevel` | `trans_freq_type` | `LOW`、`MEDIUM`、`HIGH` | +| 借方单笔最高额 | `debitSingleMaxAmount` | `dr_max_single_amount` | 交易画像 | +| 贷方单笔最高额 | `creditSingleMaxAmount` | `cr_max_single_amount` | 交易画像 | +| 借方日累计最高额 | `debitDailyMaxAmount` | `dr_max_daily_amount` | 交易画像 | +| 贷方日累计最高额 | `creditDailyMaxAmount` | `cr_max_daily_amount` | 交易画像 | +| 风险等级 | `txnRiskLevel` | `trans_risk_level` | 默认 `LOW` | + +## 后端同步判断 + +仓库里当前没有找到已成型的 `CcdiAccountInfo` 后端 Controller、Entity、Mapper、Service 或前端 API 文件,所以本次没有强行新建完整后端 CRUD。 + +后续如果正式接后端接口,需要同步增加: + +- Entity/DTO/VO 字段:`bankScope` +- Mapper XML 或 MyBatis Plus 查询条件:支持 `bank_scope` +- 新增/编辑接口:写入 `bank_scope` +- 列表接口:返回 `bank_scope` +- 行内账户:分析结果从同步任务/T+1结果表维护 +- 行外账户:允许页面写入或更新 `ccdi_account_result` + +## 本地启动说明 + +前端可以在 `ruoyi-ui` 下启动: + +```bash +npm run dev +``` + +当前环境里没有检测到 `java`、`mvn`、`mvnw`,所以这台机器不能直接用命令启动后端。后端正常需要 Java 21 和 Maven,然后启动端口按配置是 `62318`。 + +## 明天切换对话可以这样说 + +继续做 `C:\Users\20696\Desktop\初核\ccdi` 这个仓库的“账户库管理”。请先阅读 `docs/plans/fullstack/2026-04-10-account-library-handoff.md`,然后检查以下本地改动:前端原型 `ruoyi-ui/src/views/ccdiAccountInfoPrototype/index.vue`、路由 `ruoyi-ui/src/router/index.js`、白名单 `ruoyi-ui/src/permission.js`、关系枚举 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RelationType.java`、SQL 脚本 `sql/migration/2026-04-10-split-ccdi-account-info.sql`。数据库 `ccdi_account_info` 已新增 `bank_scope`,`ccdi_account_result.trans_risk_level` 默认值已是 `LOW`。下一步优先判断是否要做真实后端 CRUD/API 联调,而不是继续只改静态原型。 diff --git a/docs/plans/fullstack/account-library-preview-2026-04-10.png b/docs/plans/fullstack/account-library-preview-2026-04-10.png new file mode 100644 index 00000000..2a6728ea Binary files /dev/null and b/docs/plans/fullstack/account-library-preview-2026-04-10.png differ diff --git a/docs/tests/plans/2026-04-14-account-library-acceptance-checklist.md b/docs/tests/plans/2026-04-14-account-library-acceptance-checklist.md new file mode 100644 index 00000000..1146b2cf --- /dev/null +++ b/docs/tests/plans/2026-04-14-account-library-acceptance-checklist.md @@ -0,0 +1,219 @@ +# 账户库管理验收清单 + +## 1. 验收目标 + +确认“账户库管理”页面已满足本轮最小闭环范围,包含: + +- 菜单可见 +- 页面可打开 +- 列表查询正常 +- 新增、编辑、删除正常 +- 导入、导出正常 +- 导入模板可下载 +- 字段口径与数据库一致 +- 页面样式与其他“信息维护”页面保持一致 + +## 2. 验收前提 + +验收前需先确认: + +- 前端服务已启动 +- 后端服务已启动 +- Redis 可连接 +- 数据库使用开发库 `116.62.17.81:3307/ccdi` +- 当前账号具备“账户库管理”菜单与按钮权限 + +建议验收入口: + +- 菜单入口:`信息维护 -> 账户库管理` +- 路由入口:`/maintain/accountInfo` + +## 3. 基础可用性验收 + +### 3.1 菜单与路由 + +- [ ] 左侧“信息维护”下可见“账户库管理” +- [ ] 点击“账户库管理”可正常进入页面 +- [ ] 页面打开后无白屏、无 404、无 `No static resource` +- [ ] 页面刷新后仍可正常进入 + +### 3.2 页面样式 + +- [ ] 页面背景、卡片、表头颜色与其他“信息维护”页面一致 +- [ ] 页面顶部不再显示“信息维护 / 账户库管理”说明卡片 +- [ ] 搜索区、工具栏、表格、分页布局正常 +- [ ] 右上角搜索折叠工具栏可正常使用 + +## 4. 查询列表验收 + +### 4.1 列表展示 + +- [ ] 页面默认可加载账户列表 +- [ ] 表格列显示完整,无错位、无遮挡 +- [ ] 列表包含“证件号”列 +- [ ] “所属人类型”列能正确显示:员工、员工关系人、中介、外部人员 +- [ ] “账户类型”列能正确显示:银行账户、证券账户、支付账户、其他 +- [ ] “账户范围”列能正确显示:行内、行外 +- [ ] 风险等级、频率等级、状态色块显示正常 + +### 4.2 查询条件 + +- [ ] “所属人类型”筛选在最前面 +- [ ] 可按员工姓名查询 +- [ ] 可按账户范围查询 +- [ ] 可按关系类型查询 +- [ ] 可按账户姓名查询 +- [ ] 可按账户类型查询 +- [ ] 可按是否实控查询 +- [ ] 可按风险等级查询 +- [ ] 可按状态查询 +- [ ] “搜索”后结果正确 +- [ ] “重置”后条件恢复默认 + +## 5. 新增验收 + +### 5.1 新增弹窗 + +- [ ] 点击“新增”可打开弹窗 +- [ ] 弹窗内“所属人类型”可选:员工、员工关系人、中介、外部人员 +- [ ] 默认新增账户范围为“行外” +- [ ] 账户类型下拉仅有: + - [ ] `BANK` + - [ ] `SECURITIES` + - [ ] `PAYMENT` + - [ ] `OTHER` + +### 5.2 所属人逻辑 + +- [ ] 选择“员工”时,可选择员工姓名 +- [ ] 选择“员工”后可自动带出证件号 +- [ ] 选择“员工关系人”时,可先选员工,再选关系人 +- [ ] 选择“员工关系人”后可自动带出关系类型、关系人证件号 +- [ ] 选择“中介”时,可手工录入中介名称和证件号 +- [ ] 选择“外部人员”时,可手工录入姓名和证件号 + +### 5.3 保存验证 + +- [ ] 必填项为空时有校验提示 +- [ ] 保存成功后列表可看到新增数据 +- [ ] 刷新页面后新增数据仍存在 + +## 6. 编辑验收 + +- [ ] 点击“编辑”可打开已有记录 +- [ ] 已有字段可正确回显 +- [ ] 可修改基础信息并保存成功 +- [ ] 保存后列表显示更新内容 +- [ ] 刷新页面后修改结果仍存在 + +## 7. 删除验收 + +- [ ] 点击“删除”有确认提示 +- [ ] 确认删除后提示成功 +- [ ] 删除后列表不再显示该数据 +- [ ] 刷新页面后该数据仍已删除 + +## 8. 详情验收 + +- [ ] 点击“详情”可打开只读弹窗 +- [ ] 基础信息、归属信息、分析信息都可查看 +- [ ] 详情弹窗内字段与列表、数据库保持一致 + +## 9. 批量维护验收 + +### 9.1 工具栏能力 + +- [ ] 页面工具栏包含“导入”“导出”按钮 +- [ ] 按钮风格、位置与其他“信息维护”页面一致 +- [ ] 权限控制正常,无权限时按钮不显示 + +### 9.2 导入功能 + +- [ ] 点击“导入”可打开上传弹窗 +- [ ] 点击“下载模板”可下载 Excel 模板 +- [ ] 上传合法模板后可返回导入结果 +- [ ] 导入成功后页面列表可看到更新结果 + +### 9.3 导出功能 + +- [ ] 点击“导出”可按当前筛选条件导出 Excel +- [ ] 导出文件可正常打开 +- [ ] 导出列与页面口径一致 + +## 10. 数据口径验收 + +### 9.1 页面与数据库字段映射 + +- [ ] `ccdi_account_info.account_id -> 页面主键 id` +- [ ] `ccdi_account_info.owner_type -> 所属人类型` +- [ ] `ccdi_account_info.owner_id -> 证件号` +- [ ] `ccdi_account_info.account_no -> 账户号码` +- [ ] `ccdi_account_info.account_type -> 账户类型` +- [ ] `ccdi_account_info.bank_scope -> 账户范围` +- [ ] `ccdi_account_info.account_name -> 账户姓名` +- [ ] `ccdi_account_info.bank -> 开户机构` +- [ ] `ccdi_account_info.bank_code -> 银行代码` +- [ ] `ccdi_account_info.currency -> 币种` +- [ ] `ccdi_account_info.status -> 状态` +- [ ] `ccdi_account_info.effective_date -> 生效日期` +- [ ] `ccdi_account_info.invalid_date -> 失效日期` +- [ ] `ccdi_account_result.is_self_account -> 是否实控账户` +- [ ] `ccdi_account_result.monthly_avg_trans_count -> 月均交易笔数` +- [ ] `ccdi_account_result.monthly_avg_trans_amount -> 月均交易金额` +- [ ] `ccdi_account_result.trans_freq_type -> 频率等级` +- [ ] `ccdi_account_result.trans_risk_level -> 风险等级` + +### 9.2 当前口径确认 + +- [ ] `owner_id` 口径为“证件号” +- [ ] `owner_type` 仅有:`EMPLOYEE / RELATION / INTERMEDIARY / EXTERNAL` +- [ ] `account_type` 仅有:`BANK / SECURITIES / PAYMENT / OTHER` +- [ ] 行内账户分析信息只读 +- [ ] 行外账户分析信息支持人工维护 + +## 11. 测试数据验收 + +建议至少核对以下场景各 1 条: + +- [ ] 员工 + 银行账户 + 行外 +- [ ] 员工关系人 + 证券账户 + 行外 +- [ ] 中介 + 支付账户 + 行外 +- [ ] 外部人员 + 其他账户 + 行外 + +建议重点核对: + +- [ ] 中介支付账户账号表现为手机号样式 +- [ ] 证件号列显示正确 +- [ ] 不存在本轮新增测试数据被重复插入两次的情况 + +## 12. 异常与兼容性验收 + +- [ ] 后端不可用时,前端错误提示可理解 +- [ ] 页面没有明显控制台报错 +- [ ] 分页切换正常 +- [ ] 搜索后再点详情/编辑不报错 +- [ ] 移动端窄屏下页面不出现严重错位 + +## 13. 本轮验收结论 + +### 12.1 验收结果 + +- [ ] 通过 +- [ ] 有问题需整改 + +### 12.2 问题记录 + +| 序号 | 问题描述 | 严重程度 | 是否已修复 | 备注 | +|---|---|---|---|---| +| 1 | | | | | +| 2 | | | | | +| 3 | | | | | + +### 12.3 验收签字 + +| 角色 | 姓名 | 日期 | 结果 | +|---|---|---|---| +| 业务验收 | | | | +| 产品/需求 | | | | +| 开发确认 | | | | +| 测试确认 | | | | diff --git a/docs/tests/records/2026-04-14-account-library-acceptance-record.md b/docs/tests/records/2026-04-14-account-library-acceptance-record.md new file mode 100644 index 00000000..3cf982f2 --- /dev/null +++ b/docs/tests/records/2026-04-14-account-library-acceptance-record.md @@ -0,0 +1,119 @@ +# 账户库管理验收记录 + +## 验收时间 + +- 日期:2026-04-14 +- 验收方式:代码检查 + 数据库核对 + 页面在线验收 + 真实接口联调 + +## 验收结论 + +本轮“账户库管理”页面已完成最小闭环与批量维护能力验收,字段口径、菜单挂载、批量导入导出、测试数据、页面收口均已完成,正式页联调通过。 + +## 已通过项 + +### 1. 页面与样式 + +- 已删除顶部“信息维护 / 账户库管理”说明卡片 +- 页面背景、主卡片、表头样式已收回到信息维护页常见灰白体系 +- 已补 `right-toolbar` +- 搜索区域支持显隐 +- 工具栏、操作列风格已向若依现有页面对齐 +- 页面工具栏已补“导入”“导出”按钮,风格与中介库等页面保持一致 + +### 2. 字段口径 + +- `owner_id` 已按“证件号”口径处理 +- `owner_type` 仅支持: + - `EMPLOYEE` + - `RELATION` + - `INTERMEDIARY` + - `EXTERNAL` +- `account_type` 仅支持: + - `BANK` + - `SECURITIES` + - `PAYMENT` + - `OTHER` +- 新增默认账户范围为“行外” +- 列表已展示“证件号”列 +- 所属人类型筛选已移动到最前面 + +### 3. 批量维护能力 + +- 已补导入模板下载接口:`POST /ccdi/accountInfo/importTemplate` +- 已补导入接口:`POST /ccdi/accountInfo/importData` +- 已补导出接口:`POST /ccdi/accountInfo/export` +- 已补按钮权限: + - `ccdi:accountInfo:import` + - `ccdi:accountInfo:export` +- 已补管理员角色菜单授权 + +本次在线验收结果: + +- 导入模板下载成功,生成文件:`logs/account-info-import-template-check.xlsx` +- 导出成功,生成文件:`logs/account-info-export-check.xlsx` +- 合法导入样例成功,结果为“共 1 条,成功 1 条,失败 0 条” +- 非法导入样例会在导入结果中提示失败数量,失败原因校验正常 + +### 4. 数据与测试样例 + +已核对本轮补充的 4 条测试数据,且未重复插入: + +| account_id | owner_type | owner_id | account_no | account_type | bank_scope | +|---|---|---|---|---|---| +| 30 | RELATION | 330101199104010101 | ZQ330101199104010101 | SECURITIES | EXTERNAL | +| 31 | INTERMEDIARY | 330101197901010055 | 13700000035 | PAYMENT | EXTERNAL | +| 32 | EXTERNAL | 91330100EXT20260413 | wx-ext-20260413-001 | OTHER | EXTERNAL | +| 33 | EMPLOYEE | 330101199001010001 | 622202440000010001 | BANK | EXTERNAL | + +补充说明: + +- `account_no` 维度未发现本轮测试数据重复插入 +- 页面中“看起来重复”的旧数据,主要来自历史库里原本存在的同人多卡记录 + +### 5. 菜单与权限 + +- 已补菜单 SQL:`sql/migration/2026-04-13-add-ccdi-account-info-menu.sql` +- 前端已补按钮权限: + - `ccdi:accountInfo:add` + - `ccdi:accountInfo:edit` + - `ccdi:accountInfo:remove` + - `ccdi:accountInfo:import` + - `ccdi:accountInfo:export` + +### 6. 真实联调结果 + +本次已在正式页完成真实联调,结果如下: + +- `/ccdi/accountInfo/list` 可正常返回真实库数据 +- 详情接口可用 +- 新增接口可用 +- 编辑接口可用 +- 删除接口可用 +- 导入模板接口可用 +- 导入接口可用 +- 导出接口可用 +- `ccdi_account_info` 与 `ccdi_account_result` 联表映射正确 + +## 当前可查看页面 + +当前正式页已可访问: + +- `http://localhost/maintain/accountInfo` + +说明: + +- 此地址为“真前端 + 真后端 + 真数据库”联调页 +- 页面当前显示的账户数据为开发库真实数据 +- 可继续用于业务验收 + +## 最终判断 + +- 页面功能验收:通过 +- 数据口径验收:通过 +- 批量维护验收:通过 +- 真实联调环境验收:通过 + +## 建议下一步 + +1. 由业务侧继续在线验收页面与数据口径 +2. 如后续确认需要,再继续补导入模板说明或批量校验规则优化 diff --git a/ruoyi-ui/src/api/ccdiAccountInfo.js b/ruoyi-ui/src/api/ccdiAccountInfo.js new file mode 100644 index 00000000..77b27553 --- /dev/null +++ b/ruoyi-ui/src/api/ccdiAccountInfo.js @@ -0,0 +1,62 @@ +import request from '@/utils/request' + +// 查询账户库列表 +export function listAccountInfo(query) { + return request({ + url: '/ccdi/accountInfo/list', + method: 'get', + params: query + }) +} + +// 查询账户库详情 +export function getAccountInfo(id) { + return request({ + url: '/ccdi/accountInfo/' + id, + method: 'get' + }) +} + +// 新增账户 +export function addAccountInfo(data) { + return request({ + url: '/ccdi/accountInfo', + method: 'post', + data + }) +} + +// 修改账户 +export function updateAccountInfo(data) { + return request({ + url: '/ccdi/accountInfo', + method: 'put', + data + }) +} + +// 删除账户 +export function delAccountInfo(ids) { + return request({ + url: '/ccdi/accountInfo/' + ids, + method: 'delete' + }) +} + +// 查询员工下拉 +export function listAccountStaffOptions(query) { + return request({ + url: '/ccdi/accountInfo/staffOptions', + method: 'get', + params: { query } + }) +} + +// 查询关系人下拉 +export function listAccountRelationOptions(staffId) { + return request({ + url: '/ccdi/accountInfo/relationOptions', + method: 'get', + params: { staffId } + }) +} diff --git a/ruoyi-ui/src/permission.js b/ruoyi-ui/src/permission.js index b66190b3..41863d00 100644 --- a/ruoyi-ui/src/permission.js +++ b/ruoyi-ui/src/permission.js @@ -9,7 +9,7 @@ import { isRelogin } from '@/utils/request' NProgress.configure({ showSpinner: false }) -const whiteList = ['/login', '/register'] +const whiteList = ['/login', '/register', '/prototype/account-library'] const isWhiteList = (path) => { return whiteList.some(pattern => isPathMatch(pattern, path)) diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js index 4c0d2785..89ca88a5 100644 --- a/ruoyi-ui/src/router/index.js +++ b/ruoyi-ui/src/router/index.js @@ -77,6 +77,20 @@ export const constantRoutes = [ name: 'ProjectDetail', hidden: true, meta: { title: '项目详情', noCache: true } + }, + { + path: 'prototype/account-library', + component: () => import('@/views/ccdiAccountInfoPrototype/index'), + name: 'AccountLibraryPrototype', + hidden: true, + meta: { title: '账户库管理原型', noCache: true } + }, + { + path: 'ccdiAccountInfo', + component: () => import('@/views/ccdiAccountInfo/index'), + name: 'CcdiAccountInfo', + hidden: true, + meta: { title: '账户库管理', noCache: true } } ] }, diff --git a/ruoyi-ui/src/views/ccdiAccountInfo/index.vue b/ruoyi-ui/src/views/ccdiAccountInfo/index.vue new file mode 100644 index 00000000..610a9616 --- /dev/null +++ b/ruoyi-ui/src/views/ccdiAccountInfo/index.vue @@ -0,0 +1,903 @@ + + + + + diff --git a/ruoyi-ui/src/views/ccdiAccountInfoPrototype/index.vue b/ruoyi-ui/src/views/ccdiAccountInfoPrototype/index.vue new file mode 100644 index 00000000..c8e41306 --- /dev/null +++ b/ruoyi-ui/src/views/ccdiAccountInfoPrototype/index.vue @@ -0,0 +1,622 @@ + + + + + diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js index 2ca9788b..2d4ea3d8 100644 --- a/ruoyi-ui/vue.config.js +++ b/ruoyi-ui/vue.config.js @@ -36,8 +36,7 @@ module.exports = { proxy: { // detail: https://cli.vuejs.org/config/#devserver-proxy [process.env.VUE_APP_BASE_API]: { - // target: baseUrl, - target: "http://116.62.17.81:62318", + target: baseUrl, changeOrigin: true, pathRewrite: { ['^' + process.env.VUE_APP_BASE_API]: '' diff --git a/sql/migration/2026-04-10-split-ccdi-account-info.sql b/sql/migration/2026-04-10-split-ccdi-account-info.sql new file mode 100644 index 00000000..1dcb1b7b --- /dev/null +++ b/sql/migration/2026-04-10-split-ccdi-account-info.sql @@ -0,0 +1,145 @@ +-- 拆分账户信息表:账户基础信息保留在 ccdi_account_info,动态交易画像迁移到 ccdi_account_result。 +-- 执行说明:涉及中文内容时请使用 bin/mysql_utf8_exec.sh 执行,确保会话字符集为 utf8mb4。 + +CREATE TABLE IF NOT EXISTS `ccdi_account_result` ( + `result_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键,唯一标识', + `account_no` VARCHAR(240) NOT NULL COMMENT '账户号码(加密存储)', + `is_self_account` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否本人实控账户:0-否 1-是', + `monthly_avg_trans_count` INT DEFAULT 0 COMMENT '近6个月平均交易笔数', + `monthly_avg_trans_amount` DECIMAL(18,2) DEFAULT 0.00 COMMENT '近6个月平均交易金额', + `trans_freq_type` VARCHAR(20) DEFAULT 'MEDIUM' COMMENT 'LOW:低频, MEDIUM:中频, HIGH:高频', + `dr_max_single_amount` DECIMAL(18,2) COMMENT '借方单笔交易最高额', + `cr_max_single_amount` DECIMAL(18,2) COMMENT '贷方单笔交易最高额', + `dr_max_daily_amount` DECIMAL(18,2) COMMENT '借方日累计交易最高额', + `cr_max_daily_amount` DECIMAL(18,2) COMMENT '贷方日累计交易最高额', + `trans_risk_level` VARCHAR(10) DEFAULT 'LOW' COMMENT '交易风险等级', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`result_id`), + UNIQUE KEY `uk_ccdi_account_result_account_no` (`account_no`), + KEY `idx_ccdi_account_result_risk_level` (`trans_risk_level`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户结果表'; + +DELIMITER // + +DROP PROCEDURE IF EXISTS `migrate_ccdi_account_info_split`// +CREATE PROCEDURE `migrate_ccdi_account_info_split`() +BEGIN + DECLARE dynamic_column_count INT DEFAULT 0; + + SELECT COUNT(*) + INTO dynamic_column_count + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'ccdi_account_info' + AND COLUMN_NAME IN ( + 'is_self_account', + 'monthly_avg_trans_count', + 'monthly_avg_trans_amount', + 'trans_freq_type', + 'dr_max_single_amount', + 'cr_max_single_amount', + 'dr_max_daily_amount', + 'cr_max_daily_amount', + 'trans_risk_level' + ); + + IF dynamic_column_count = 9 THEN + INSERT INTO `ccdi_account_result` ( + `account_no`, + `is_self_account`, + `monthly_avg_trans_count`, + `monthly_avg_trans_amount`, + `trans_freq_type`, + `dr_max_single_amount`, + `cr_max_single_amount`, + `dr_max_daily_amount`, + `cr_max_daily_amount`, + `trans_risk_level`, + `create_by`, + `create_time`, + `update_by`, + `update_time` + ) + SELECT + `account_no`, + COALESCE(`is_self_account`, 1), + COALESCE(CAST(`monthly_avg_trans_count` AS SIGNED), 0), + COALESCE(`monthly_avg_trans_amount`, 0.00), + COALESCE(`trans_freq_type`, 'MEDIUM'), + `dr_max_single_amount`, + `cr_max_single_amount`, + `dr_max_daily_amount`, + `cr_max_daily_amount`, + COALESCE(`trans_risk_level`, 'LOW'), + `create_by`, + COALESCE(`create_time`, CURRENT_TIMESTAMP), + `update_by`, + COALESCE(`update_time`, CURRENT_TIMESTAMP) + FROM `ccdi_account_info` + ON DUPLICATE KEY UPDATE + `is_self_account` = VALUES(`is_self_account`), + `monthly_avg_trans_count` = VALUES(`monthly_avg_trans_count`), + `monthly_avg_trans_amount` = VALUES(`monthly_avg_trans_amount`), + `trans_freq_type` = VALUES(`trans_freq_type`), + `dr_max_single_amount` = VALUES(`dr_max_single_amount`), + `cr_max_single_amount` = VALUES(`cr_max_single_amount`), + `dr_max_daily_amount` = VALUES(`dr_max_daily_amount`), + `cr_max_daily_amount` = VALUES(`cr_max_daily_amount`), + `trans_risk_level` = VALUES(`trans_risk_level`), + `update_by` = VALUES(`update_by`), + `update_time` = VALUES(`update_time`); + + ALTER TABLE `ccdi_account_info` + DROP COLUMN `is_self_account`, + DROP COLUMN `monthly_avg_trans_count`, + DROP COLUMN `monthly_avg_trans_amount`, + DROP COLUMN `trans_freq_type`, + DROP COLUMN `dr_max_single_amount`, + DROP COLUMN `cr_max_single_amount`, + DROP COLUMN `dr_max_daily_amount`, + DROP COLUMN `cr_max_daily_amount`, + DROP COLUMN `trans_risk_level`; + ELSEIF dynamic_column_count <> 0 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'ccdi_account_info dynamic columns are partially migrated; please check schema before running this migration.'; + END IF; +END// + +CALL `migrate_ccdi_account_info_split`()// +DROP PROCEDURE IF EXISTS `migrate_ccdi_account_info_split`// + +DELIMITER ; + +DELIMITER // + +DROP PROCEDURE IF EXISTS `add_ccdi_account_info_bank_scope`// +CREATE PROCEDURE `add_ccdi_account_info_bank_scope`() +BEGIN + DECLARE column_count INT DEFAULT 0; + + SELECT COUNT(*) + INTO column_count + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'ccdi_account_info' + AND COLUMN_NAME = 'bank_scope'; + + IF column_count = 0 THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `bank_scope` VARCHAR(20) NOT NULL DEFAULT 'INTERNAL' COMMENT '账户范围:INTERNAL-行内,EXTERNAL-行外' + AFTER `account_type`; + END IF; + + UPDATE `ccdi_account_info` + SET `bank_scope` = 'INTERNAL' + WHERE `bank_scope` IS NULL + OR `bank_scope` = ''; +END// + +CALL `add_ccdi_account_info_bank_scope`()// +DROP PROCEDURE IF EXISTS `add_ccdi_account_info_bank_scope`// + +DELIMITER ; diff --git a/sql/migration/2026-04-13-add-ccdi-account-info-menu.sql b/sql/migration/2026-04-13-add-ccdi-account-info-menu.sql new file mode 100644 index 00000000..496d5edd --- /dev/null +++ b/sql/migration/2026-04-13-add-ccdi-account-info-menu.sql @@ -0,0 +1,381 @@ +-- 账户库管理菜单 +-- 挂载到“信息维护”目录下,可重复执行 + +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 +) +SELECT + '账户库管理', + @parent_menu_id, + 11, + 'accountInfo', + 'ccdiAccountInfo/index', + 1, + 0, + 'C', + '0', + '0', + 'ccdi:accountInfo:list', + 'documentation', + 'admin', + NOW(), + '', + NULL, + '账户库管理菜单' +FROM dual +WHERE @parent_menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @parent_menu_id + AND path = 'accountInfo' + ); + +SET @menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @parent_menu_id + AND path = 'accountInfo' + 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, + remark +) +SELECT '账户查询', @menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'ccdi:accountInfo:query', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:query' + ); + +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 +) +SELECT '账户新增', @menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'ccdi:accountInfo:add', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:add' + ); + +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 +) +SELECT '账户修改', @menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'ccdi:accountInfo:edit', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:edit' + ); + +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 +) +SELECT '账户删除', @menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'ccdi:accountInfo:remove', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:remove' + ); + +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 +) +SELECT '账户导入', @menu_id, 5, '', '', 1, 0, 'F', '0', '0', 'ccdi:accountInfo:import', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:import' + ); + +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 +) +SELECT '账户导出', @menu_id, 6, '', '', 1, 0, 'F', '0', '0', 'ccdi:accountInfo:export', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:export' + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, @menu_id +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_menu + WHERE role_id = 1 + AND menu_id = @menu_id + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, + ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:query' + LIMIT 1 + ) +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_menu + WHERE role_id = 1 + AND menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:query' + LIMIT 1 + ) + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, + ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:import' + LIMIT 1 + ) +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_menu + WHERE role_id = 1 + AND menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:import' + LIMIT 1 + ) + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, + ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:export' + LIMIT 1 + ) +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_menu + WHERE role_id = 1 + AND menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:export' + LIMIT 1 + ) + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, + ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:add' + LIMIT 1 + ) +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_menu + WHERE role_id = 1 + AND menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:add' + LIMIT 1 + ) + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, + ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:edit' + LIMIT 1 + ) +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_menu + WHERE role_id = 1 + AND menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:edit' + LIMIT 1 + ) + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, + ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:remove' + LIMIT 1 + ) +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_menu + WHERE role_id = 1 + AND menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:accountInfo:remove' + LIMIT 1 + ) + ); diff --git a/sql/migration/2026-04-13-fix-employee-external-seed-encoding.sql b/sql/migration/2026-04-13-fix-employee-external-seed-encoding.sql new file mode 100644 index 00000000..46691714 --- /dev/null +++ b/sql/migration/2026-04-13-fix-employee-external-seed-encoding.sql @@ -0,0 +1,6 @@ +UPDATE ccdi_account_info +SET + account_name = '测试员工A行外卡', + bank = '杭州联合银行城东支行', + update_by = 'system' +WHERE account_no = '622202440000010001'; diff --git a/sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql b/sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql new file mode 100644 index 00000000..5d68f6e0 --- /dev/null +++ b/sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql @@ -0,0 +1,274 @@ +-- Seed account test data for external scenarios. +-- Covers EMPLOYEE / RELATION / INTERMEDIARY / EXTERNAL owner types. + +INSERT INTO ccdi_account_info ( + account_no, + account_type, + bank_scope, + account_name, + owner_type, + owner_id, + bank, + bank_code, + currency, + status, + effective_date, + invalid_date, + create_by, + update_by +) +SELECT + '622202440000010001', + 'BANK', + 'EXTERNAL', + '测试员工A行外卡', + 'EMPLOYEE', + '330101199001010001', + '杭州联合银行城东支行', + 'HZLH001', + 'CNY', + 1, + '2026-04-13', + NULL, + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_info WHERE account_no = '622202440000010001' +); + +INSERT INTO ccdi_account_result ( + account_no, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, + create_by, + update_by +) +SELECT + '622202440000010001', + 1, + 12, + 28600.00, + 'MEDIUM', + 8800.00, + 12000.00, + 16000.00, + 22000.00, + 'MEDIUM', + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_result WHERE account_no = '622202440000010001' +); + +INSERT INTO ccdi_account_info ( + account_no, + account_type, + bank_scope, + account_name, + owner_type, + owner_id, + bank, + bank_code, + currency, + status, + effective_date, + invalid_date, + create_by, + update_by +) +SELECT + 'ZQ330101199104010101', + 'SECURITIES', + 'EXTERNAL', + '边界配偶甲', + 'RELATION', + '330101199104010101', + '国泰君安杭州营业部', + 'GTJAHZ01', + 'CNY', + 1, + '2026-04-13', + NULL, + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_info WHERE account_no = 'ZQ330101199104010101' +); + +INSERT INTO ccdi_account_result ( + account_no, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, + create_by, + update_by +) +SELECT + 'ZQ330101199104010101', + 0, + 6, + 152000.00, + 'LOW', + 68000.00, + 72000.00, + 98000.00, + 116000.00, + 'HIGH', + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_result WHERE account_no = 'ZQ330101199104010101' +); + +INSERT INTO ccdi_account_info ( + account_no, + account_type, + bank_scope, + account_name, + owner_type, + owner_id, + bank, + bank_code, + currency, + status, + effective_date, + invalid_date, + create_by, + update_by +) +SELECT + '13700000035', + 'PAYMENT', + 'EXTERNAL', + '模型异常中介', + 'INTERMEDIARY', + '330101197901010055', + '支付宝', + 'ALIPAY', + 'CNY', + 1, + '2026-04-13', + NULL, + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_info WHERE account_no = '13700000035' +); + +INSERT INTO ccdi_account_result ( + account_no, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, + create_by, + update_by +) +SELECT + '13700000035', + 0, + 18, + 46800.00, + 'MEDIUM', + 9000.00, + 13800.00, + 18800.00, + 21600.00, + 'LOW', + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_result WHERE account_no = '13700000035' +); + +INSERT INTO ccdi_account_info ( + account_no, + account_type, + bank_scope, + account_name, + owner_type, + owner_id, + bank, + bank_code, + currency, + status, + effective_date, + invalid_date, + create_by, + update_by +) +SELECT + 'wx-ext-20260413-001', + 'OTHER', + 'EXTERNAL', + '外部合作机构测试', + 'EXTERNAL', + '91330100EXT20260413', + '微信支付', + 'WXPAY', + 'CNY', + 1, + '2026-04-13', + NULL, + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_info WHERE account_no = 'wx-ext-20260413-001' +); + +INSERT INTO ccdi_account_result ( + account_no, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, + create_by, + update_by +) +SELECT + 'wx-ext-20260413-001', + 0, + 9, + 9800.00, + 'LOW', + 3200.00, + 4100.00, + 5600.00, + 7000.00, + 'LOW', + 'system', + 'system' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM ccdi_account_result WHERE account_no = 'wx-ext-20260413-001' +);