Compare commits
5 Commits
6e6419c116
...
57fd1f486d
| Author | SHA1 | Date | |
|---|---|---|---|
| 57fd1f486d | |||
| 52301e02c4 | |||
| 8f9fc09338 | |||
| 82cb751b8f | |||
| 88186f37a6 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -75,4 +75,6 @@ db_config.conf
|
||||
|
||||
output/
|
||||
|
||||
logs/
|
||||
|
||||
ruoyi-admin/src/main/resources/logback.xml
|
||||
@@ -25,6 +25,7 @@
|
||||
- 前端开发直接在当前分支进行,不需要额外创建 git worktree
|
||||
- 测试结束后,自动关闭测试过程中启动的前后端进程
|
||||
- 遇到 MCP 数据库操作时,使用项目配置文件中的数据库连接信息
|
||||
- 执行包含中文内容的 MySQL SQL 脚本时,禁止直接手写 `mysql -e` 或普通重定向执行;必须优先使用 `bin/mysql_utf8_exec.sh <sql-file>`,确保会话字符集为 `utf8mb4`,避免写入乱码
|
||||
|
||||
---
|
||||
|
||||
@@ -160,6 +161,7 @@ return AjaxResult.success(result);
|
||||
- 非业务字段如 `create_by`、`create_time` 由后端自动维护
|
||||
- 前端表单不要暴露通用审计字段
|
||||
- 新增菜单、字典、初始化数据时,同步补充 SQL 脚本
|
||||
- 执行数据库脚本前,需确认客户端会话字符集为 `utf8mb4`;涉及中文插入、更新时默认使用 `bin/mysql_utf8_exec.sh`
|
||||
|
||||
### 前端规范
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ id,project_id,model_code,model_name,param_code,param_name,param_desc,param_value
|
||||
6,0,LARGE_TRANSACTION,大额交易模型,FREQUENT_TRANSFER,单笔大额转账金额,单日转账次数超过,100001,次/日
|
||||
,,,,,,,,
|
||||
7,0,SUSPICIOUS_PART_TIME,可疑兼职模型,MONTHLY_FIXED_INCOME,月度非本行工资收入金额,"除本行工资外,每月固定收入超过",5000,元/月
|
||||
8,0,SUSPICIOUS_PART_TIME,可疑兼职模型,FIXED_COUNTERPARTY_TRANSFER,季度稳定收入金额,每季从固定交易对手转入金额,15000,元/季
|
||||
8,0,SUSPICIOUS_PART_TIME,可疑兼职模型,FIXED_COUNTERPARTY_TRANSFER_MIN,季度稳定收入金额下限,每季从固定交易对手转入金额下限,3000,元/季
|
||||
9,0,SUSPICIOUS_PART_TIME,可疑兼职模型,FIXED_COUNTERPARTY_TRANSFER_MAX,季度稳定收入金额上限,每季从固定交易对手转入金额上限,15000,元/季
|
||||
,,,,,,,,
|
||||
10,0,SUSPICIOUS_FOREIGN_EXCHANGE,可疑外汇交易模型,SINGLE_PURCHASE_AMOUNT,单笔购汇金额,单笔购汇超过该金额,50000,美元/笔
|
||||
11,0,SUSPICIOUS_FOREIGN_EXCHANGE,可疑外汇交易模型,SINGLE_SETTLEMENT_AMOUNT,单笔结汇金额,单笔结汇超过该金额,50000,美元/笔
|
||||
|
||||
|
55
bin/mysql_utf8_exec.sh
Executable file
55
bin/mysql_utf8_exec.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: bin/mysql_utf8_exec.sh <sql-file>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
APP_CONFIG="${PROJECT_ROOT}/ruoyi-admin/src/main/resources/application-dev.yml"
|
||||
SQL_FILE_INPUT="$1"
|
||||
|
||||
if [ ! -f "${APP_CONFIG}" ]; then
|
||||
echo "Config file not found: ${APP_CONFIG}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${SQL_FILE_INPUT}" ]; then
|
||||
echo "SQL file not found: ${SQL_FILE_INPUT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v mysql >/dev/null 2>&1; then
|
||||
echo "mysql command not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_URL="$(awk '/master:/{flag=1;next} flag && /url: jdbc:mysql:\/\//{sub(/.*url: /, ""); print; exit}' "${APP_CONFIG}")"
|
||||
DB_USER="$(awk '/master:/{flag=1;next} flag && /username:/{sub(/.*username: /, ""); print; exit}' "${APP_CONFIG}")"
|
||||
DB_PASS="$(awk '/master:/{flag=1;next} flag && /password:/{sub(/.*password: /, ""); print; exit}' "${APP_CONFIG}")"
|
||||
|
||||
if [ -z "${DB_URL}" ] || [ -z "${DB_USER}" ]; then
|
||||
echo "Failed to parse database config from application-dev.yml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_URL_NO_PREFIX="${DB_URL#jdbc:mysql://}"
|
||||
DB_HOST_PORT="${DB_URL_NO_PREFIX%%/*}"
|
||||
DB_NAME="${DB_URL_NO_PREFIX#*/}"
|
||||
DB_NAME="${DB_NAME%%\?*}"
|
||||
|
||||
DB_HOST="${DB_HOST_PORT%%:*}"
|
||||
DB_PORT="${DB_HOST_PORT##*:}"
|
||||
|
||||
ABS_SQL_FILE="$(cd "$(dirname "${SQL_FILE_INPUT}")" && pwd)/$(basename "${SQL_FILE_INPUT}")"
|
||||
|
||||
MYSQL_PWD="${DB_PASS}" mysql \
|
||||
-h "${DB_HOST}" \
|
||||
-P "${DB_PORT}" \
|
||||
-u "${DB_USER}" \
|
||||
--default-character-set=utf8mb4 \
|
||||
--init-command="SET NAMES utf8mb4" \
|
||||
"${DB_NAME}" < "${ABS_SQL_FILE}"
|
||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -36,6 +37,9 @@ public class CcdiBaseStaff implements Serializable {
|
||||
/** 电话 */
|
||||
private String phone;
|
||||
|
||||
/** 年收入 */
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 入职时间 */
|
||||
private Date hireDate;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -53,6 +54,9 @@ public class CcdiStaffFmyRelation implements Serializable {
|
||||
/** 手机号码2 */
|
||||
private String mobilePhone2;
|
||||
|
||||
/** 家庭成员年收入 */
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 微信名称1 */
|
||||
private String wechatNo1;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -46,6 +47,9 @@ public class CcdiBaseStaffAddDTO implements Serializable {
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "电话格式不正确")
|
||||
private String phone;
|
||||
|
||||
/** 年收入 */
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 入职时间 */
|
||||
private Date hireDate;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -45,6 +46,9 @@ public class CcdiBaseStaffEditDTO implements Serializable {
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "电话格式不正确")
|
||||
private String phone;
|
||||
|
||||
/** 年收入 */
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 入职时间 */
|
||||
private Date hireDate;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -75,6 +76,10 @@ public class CcdiStaffFmyRelationAddDTO implements Serializable {
|
||||
@Schema(description = "手机号码2")
|
||||
private String mobilePhone2;
|
||||
|
||||
/** 家庭成员年收入 */
|
||||
@Schema(description = "家庭成员年收入(元/年)")
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 微信名称1 */
|
||||
@Size(max = 50, message = "微信名称1长度不能超过50个字符")
|
||||
@Schema(description = "微信名称1")
|
||||
|
||||
@@ -10,6 +10,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -81,6 +82,10 @@ public class CcdiStaffFmyRelationEditDTO implements Serializable {
|
||||
@Schema(description = "手机号码2")
|
||||
private String mobilePhone2;
|
||||
|
||||
/** 家庭成员年收入 */
|
||||
@Schema(description = "家庭成员年收入(元/年)")
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 微信名称1 */
|
||||
@Size(max = 50, message = "微信名称1长度不能超过50个字符")
|
||||
@Schema(description = "微信名称1")
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -52,13 +53,18 @@ public class CcdiBaseStaffExcel implements Serializable {
|
||||
@Required
|
||||
private String phone;
|
||||
|
||||
/** 年收入 */
|
||||
@ExcelProperty(value = "年收入(元/年)", index = 5)
|
||||
@ColumnWidth(18)
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 入职时间 */
|
||||
@ExcelProperty(value = "入职时间", index = 5)
|
||||
@ExcelProperty(value = "入职时间", index = 6)
|
||||
@ColumnWidth(15)
|
||||
private Date hireDate;
|
||||
|
||||
/** 状态 */
|
||||
@ExcelProperty(value = "状态", index = 6)
|
||||
@ExcelProperty(value = "状态", index = 7)
|
||||
@ColumnWidth(10)
|
||||
@DictDropdown(dictType = "ccdi_employee_status")
|
||||
@Required
|
||||
|
||||
@@ -9,6 +9,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -77,43 +78,48 @@ public class CcdiStaffFmyRelationExcel implements Serializable {
|
||||
@ColumnWidth(15)
|
||||
private String mobilePhone2;
|
||||
|
||||
/** 家庭成员年收入 */
|
||||
@ExcelProperty(value = "家庭成员年收入(元/年)", index = 9)
|
||||
@ColumnWidth(18)
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 微信名称1 */
|
||||
@ExcelProperty(value = "微信名称1", index = 9)
|
||||
@ExcelProperty(value = "微信名称1", index = 10)
|
||||
@ColumnWidth(15)
|
||||
private String wechatNo1;
|
||||
|
||||
/** 微信名称2 */
|
||||
@ExcelProperty(value = "微信名称2", index = 10)
|
||||
@ExcelProperty(value = "微信名称2", index = 11)
|
||||
@ColumnWidth(15)
|
||||
private String wechatNo2;
|
||||
|
||||
/** 微信名称3 */
|
||||
@ExcelProperty(value = "微信名称3", index = 11)
|
||||
@ExcelProperty(value = "微信名称3", index = 12)
|
||||
@ColumnWidth(15)
|
||||
private String wechatNo3;
|
||||
|
||||
/** 详细联系地址 */
|
||||
@ExcelProperty(value = "详细联系地址", index = 12)
|
||||
@ExcelProperty(value = "详细联系地址", index = 13)
|
||||
@ColumnWidth(30)
|
||||
private String contactAddress;
|
||||
|
||||
/** 关系详细描述 */
|
||||
@ExcelProperty(value = "关系详细描述", index = 13)
|
||||
@ExcelProperty(value = "关系详细描述", index = 14)
|
||||
@ColumnWidth(30)
|
||||
private String relationDesc;
|
||||
|
||||
/** 生效日期 */
|
||||
@ExcelProperty(value = "生效日期", index = 14)
|
||||
@ExcelProperty(value = "生效日期", index = 15)
|
||||
@ColumnWidth(15)
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 失效日期 */
|
||||
@ExcelProperty(value = "失效日期", index = 15)
|
||||
@ExcelProperty(value = "失效日期", index = 16)
|
||||
@ColumnWidth(15)
|
||||
private Date invalidDate;
|
||||
|
||||
/** 备注 */
|
||||
@ExcelProperty(value = "备注", index = 16)
|
||||
@ExcelProperty(value = "备注", index = 17)
|
||||
@ColumnWidth(30)
|
||||
private String remark;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -37,6 +38,9 @@ public class CcdiBaseStaffVO implements Serializable {
|
||||
/** 电话 */
|
||||
private String phone;
|
||||
|
||||
/** 年收入 */
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 入职时间 */
|
||||
private Date hireDate;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -71,6 +72,10 @@ public class CcdiStaffFmyRelationVO implements Serializable {
|
||||
@Schema(description = "手机号码2")
|
||||
private String mobilePhone2;
|
||||
|
||||
/** 家庭成员年收入 */
|
||||
@Schema(description = "家庭成员年收入(元/年)")
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 微信名称1 */
|
||||
@Schema(description = "微信名称1")
|
||||
private String wechatNo1;
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.ruoyi.info.collection.domain.vo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 导入失败记录VO
|
||||
*
|
||||
@@ -27,6 +29,9 @@ public class ImportFailureVO {
|
||||
@Schema(description = "电话")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "年收入")
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.ruoyi.info.collection.domain.vo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 员工亲属关系信息导入失败记录VO
|
||||
*
|
||||
@@ -41,6 +43,10 @@ public class StaffFmyRelationImportFailureVO {
|
||||
@Schema(description = "手机号码1")
|
||||
private String mobilePhone1;
|
||||
|
||||
/** 家庭成员年收入 */
|
||||
@Schema(description = "家庭成员年收入")
|
||||
private BigDecimal annualIncome;
|
||||
|
||||
/** 状态 */
|
||||
@Schema(description = "状态")
|
||||
private Integer status;
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -356,5 +357,19 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
|
||||
if (!"0".equals(addDTO.getStatus()) && !"1".equals(addDTO.getStatus())) {
|
||||
throw new RuntimeException("状态只能填写'在职'或'离职'");
|
||||
}
|
||||
|
||||
validateAnnualIncome(addDTO.getAnnualIncome(), "年收入");
|
||||
}
|
||||
|
||||
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
|
||||
if (annualIncome == null) {
|
||||
return;
|
||||
}
|
||||
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new RuntimeException(fieldLabel + "不能为负数");
|
||||
}
|
||||
if (annualIncome.scale() > 2) {
|
||||
throw new RuntimeException(fieldLabel + "最多保留2位小数");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -129,6 +130,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertBaseStaff(CcdiBaseStaffAddDTO addDTO) {
|
||||
validateAnnualIncome(addDTO.getAnnualIncome(), "年收入");
|
||||
// 检查员工ID唯一性
|
||||
if (baseStaffMapper.selectById(addDTO.getStaffId()) != null) {
|
||||
throw new RuntimeException("该员工ID已存在");
|
||||
@@ -158,6 +160,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateBaseStaff(CcdiBaseStaffEditDTO editDTO) {
|
||||
validateAnnualIncome(editDTO.getAnnualIncome(), "年收入");
|
||||
CcdiBaseStaff existing = baseStaffMapper.selectById(editDTO.getStaffId());
|
||||
if (existing == null) {
|
||||
throw new RuntimeException("员工不存在");
|
||||
@@ -276,4 +279,16 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
|
||||
if (annualIncome == null) {
|
||||
return;
|
||||
}
|
||||
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new RuntimeException(fieldLabel + "不能为负数");
|
||||
}
|
||||
if (annualIncome.scale() > 2) {
|
||||
throw new RuntimeException(fieldLabel + "最多保留2位小数");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -338,5 +339,19 @@ public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelat
|
||||
throw new RuntimeException("性别只能是:男、女、其他 或 M、F、O");
|
||||
}
|
||||
}
|
||||
|
||||
validateAnnualIncome(addDTO.getAnnualIncome(), "家庭成员年收入");
|
||||
}
|
||||
|
||||
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
|
||||
if (annualIncome == null) {
|
||||
return;
|
||||
}
|
||||
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new RuntimeException(fieldLabel + "不能为负数");
|
||||
}
|
||||
if (annualIncome.scale() > 2) {
|
||||
throw new RuntimeException(fieldLabel + "最多保留2位小数");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -121,6 +122,7 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertRelation(CcdiStaffFmyRelationAddDTO addDTO) {
|
||||
validateAnnualIncome(addDTO.getAnnualIncome(), "家庭成员年收入");
|
||||
CcdiStaffFmyRelation relation = new CcdiStaffFmyRelation();
|
||||
BeanUtils.copyProperties(addDTO, relation);
|
||||
|
||||
@@ -146,6 +148,7 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateRelation(CcdiStaffFmyRelationEditDTO editDTO) {
|
||||
validateAnnualIncome(editDTO.getAnnualIncome(), "家庭成员年收入");
|
||||
CcdiStaffFmyRelation existing = relationMapper.selectById(editDTO.getId());
|
||||
if (existing == null) {
|
||||
throw new RuntimeException("员工亲属关系不存在");
|
||||
@@ -228,4 +231,16 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
|
||||
BeanUtils.copyProperties(assetInfo, assetInfoVO);
|
||||
return assetInfoVO;
|
||||
}
|
||||
|
||||
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
|
||||
if (annualIncome == null) {
|
||||
return;
|
||||
}
|
||||
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new RuntimeException(fieldLabel + "不能为负数");
|
||||
}
|
||||
if (annualIncome.scale() > 2) {
|
||||
throw new RuntimeException(fieldLabel + "最多保留2位小数");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<result property="deptName" column="dept_name"/>
|
||||
<result property="idCard" column="id_card"/>
|
||||
<result property="phone" column="phone"/>
|
||||
<result property="annualIncome" column="annual_income"/>
|
||||
<result property="hireDate" column="hire_date"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
<select id="selectBaseStaffPageWithDept" resultMap="CcdiBaseStaffVOResult">
|
||||
SELECT
|
||||
e.staff_id, e.name, e.dept_id, e.id_card, e.phone, e.hire_date, e.status, e.create_time,
|
||||
e.staff_id, e.name, e.dept_id, e.id_card, e.phone, e.annual_income, e.hire_date, e.status, e.create_time,
|
||||
d.dept_name
|
||||
FROM ccdi_base_staff e
|
||||
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id
|
||||
@@ -46,18 +47,19 @@
|
||||
<!-- 批量插入或更新员工信息(只更新非null字段) -->
|
||||
<insert id="insertOrUpdateBatch" parameterType="java.util.List">
|
||||
INSERT INTO ccdi_base_staff
|
||||
(staff_id, name, dept_id, id_card, phone, hire_date, status,
|
||||
(staff_id, name, dept_id, id_card, phone, annual_income, hire_date, status,
|
||||
create_time, create_by, update_by, update_time)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.staffId}, #{item.name}, #{item.deptId}, #{item.idCard},
|
||||
#{item.phone}, #{item.hireDate}, #{item.status}, NOW(),
|
||||
#{item.phone}, #{item.annualIncome}, #{item.hireDate}, #{item.status}, NOW(),
|
||||
#{item.createBy}, #{item.updateBy}, NOW())
|
||||
</foreach>
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = COALESCE(VALUES(name), name),
|
||||
dept_id = COALESCE(VALUES(dept_id), dept_id),
|
||||
phone = COALESCE(VALUES(phone), phone),
|
||||
annual_income = COALESCE(VALUES(annual_income), annual_income),
|
||||
hire_date = COALESCE(VALUES(hire_date), hire_date),
|
||||
status = COALESCE(VALUES(status), status),
|
||||
update_by = COALESCE(VALUES(update_by), update_by),
|
||||
@@ -67,12 +69,12 @@
|
||||
<!-- 批量插入员工信息 -->
|
||||
<insert id="insertBatch" parameterType="java.util.List">
|
||||
INSERT INTO ccdi_base_staff
|
||||
(staff_id, name, dept_id, id_card, phone, hire_date, status,
|
||||
(staff_id, name, dept_id, id_card, phone, annual_income, hire_date, status,
|
||||
create_time, create_by, update_by, update_time)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.staffId}, #{item.name}, #{item.deptId}, #{item.idCard},
|
||||
#{item.phone}, #{item.hireDate}, #{item.status}, NOW(),
|
||||
#{item.phone}, #{item.annualIncome}, #{item.hireDate}, #{item.status}, NOW(),
|
||||
#{item.createBy}, #{item.updateBy}, NOW())
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<result property="relationCertNo" column="relation_cert_no"/>
|
||||
<result property="mobilePhone1" column="mobile_phone1"/>
|
||||
<result property="mobilePhone2" column="mobile_phone2"/>
|
||||
<result property="annualIncome" column="annual_income"/>
|
||||
<result property="wechatNo1" column="wechat_no1"/>
|
||||
<result property="wechatNo2" column="wechat_no2"/>
|
||||
<result property="wechatNo3" column="wechat_no3"/>
|
||||
@@ -40,7 +41,7 @@
|
||||
SELECT
|
||||
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
|
||||
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
|
||||
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
|
||||
r.mobile_phone1, r.mobile_phone2, r.annual_income, r.wechat_no1, r.wechat_no2, r.wechat_no3,
|
||||
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
|
||||
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
|
||||
r.created_by, r.create_time, r.updated_by, r.update_time
|
||||
@@ -80,7 +81,7 @@
|
||||
SELECT
|
||||
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
|
||||
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
|
||||
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
|
||||
r.mobile_phone1, r.mobile_phone2, r.annual_income, r.wechat_no1, r.wechat_no2, r.wechat_no3,
|
||||
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
|
||||
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
|
||||
r.created_by, r.create_time, r.updated_by, r.update_time
|
||||
@@ -94,7 +95,7 @@
|
||||
SELECT
|
||||
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
|
||||
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
|
||||
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
|
||||
r.mobile_phone1, r.mobile_phone2, r.annual_income, r.wechat_no1, r.wechat_no2, r.wechat_no3,
|
||||
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
|
||||
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
|
||||
r.created_by, r.create_time, r.updated_by, r.update_time
|
||||
@@ -133,14 +134,14 @@
|
||||
<insert id="insertBatch">
|
||||
INSERT INTO ccdi_staff_fmy_relation
|
||||
(person_id, relation_type, relation_name, gender, birth_date, relation_cert_type, relation_cert_no,
|
||||
mobile_phone1, mobile_phone2, wechat_no1, wechat_no2, wechat_no3, contact_address, relation_desc,
|
||||
mobile_phone1, mobile_phone2, annual_income, wechat_no1, wechat_no2, wechat_no3, contact_address, relation_desc,
|
||||
effective_date, invalid_date, status, remark, data_source, is_emp_family, is_cust_family,
|
||||
created_by, create_time, updated_by, update_time)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.personId}, #{item.relationType}, #{item.relationName}, #{item.gender},
|
||||
#{item.birthDate}, #{item.relationCertType}, #{item.relationCertNo}, #{item.mobilePhone1},
|
||||
#{item.mobilePhone2}, #{item.wechatNo1}, #{item.wechatNo2}, #{item.wechatNo3}, #{item.contactAddress},
|
||||
#{item.mobilePhone2}, #{item.annualIncome}, #{item.wechatNo1}, #{item.wechatNo2}, #{item.wechatNo3}, #{item.contactAddress},
|
||||
#{item.relationDesc}, #{item.effectiveDate}, #{item.invalidDate}, #{item.status}, #{item.remark},
|
||||
#{item.dataSource}, #{item.isEmpFamily}, #{item.isCustFamily}, #{item.createdBy}, NOW(), #{item.updatedBy}, NOW())
|
||||
</foreach>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.info.collection.mapper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiBaseStaffMapperTest {
|
||||
|
||||
@Test
|
||||
void mapperXml_shouldContainAnnualIncomeColumnsInCustomSql() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader()
|
||||
.getResourceAsStream("mapper/info/collection/CcdiBaseStaffMapper.xml")) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(xml.contains("annual_income"), xml);
|
||||
assertTrue(xml.contains("#{item.annualIncome}"), xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,16 @@ class CcdiStaffFmyRelationMapperTest {
|
||||
assertTrue(sql.contains("WHERE 1 = 1 AND r.is_emp_family = 1 AND r.person_id = ?"), sql);
|
||||
assertFalse(sql.contains("1AND"), sql);
|
||||
assertFalse(countSql.contains("1AND"), countSql);
|
||||
assertTrue(sql.contains("r.annual_income"), sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapperXml_shouldContainAnnualIncomeInBatchInsert() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
String xml = new String(inputStream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
||||
assertTrue(xml.contains("annual_income"), xml);
|
||||
assertTrue(xml.contains("#{item.annualIncome}"), xml);
|
||||
}
|
||||
}
|
||||
|
||||
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.info.collection.service;
|
||||
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
|
||||
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class CcdiBaseStaffImportServiceImplTest {
|
||||
|
||||
private final CcdiBaseStaffImportServiceImpl service = new CcdiBaseStaffImportServiceImpl();
|
||||
|
||||
@Test
|
||||
void validateStaffData_shouldAllowEmptyAnnualIncome() {
|
||||
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), false, Collections.emptySet(), Collections.emptySet()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateStaffData_shouldAllowZeroAndTwoDecimalAnnualIncome() {
|
||||
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), false, Collections.emptySet(), Collections.emptySet()));
|
||||
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), false, Collections.emptySet(), Collections.emptySet()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateStaffData_shouldRejectNegativeAnnualIncome() {
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), false, Set.of(), Set.of()));
|
||||
|
||||
assertEquals("年收入不能为负数", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateStaffData_shouldRejectAnnualIncomeWithMoreThanTwoDecimals() {
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.validateStaffData(buildDto(new BigDecimal("12.345")), false, Set.of(), Set.of()));
|
||||
|
||||
assertEquals("年收入最多保留2位小数", exception.getMessage());
|
||||
}
|
||||
|
||||
private CcdiBaseStaffAddDTO buildDto(BigDecimal annualIncome) {
|
||||
CcdiBaseStaffAddDTO dto = new CcdiBaseStaffAddDTO();
|
||||
dto.setName("张三");
|
||||
dto.setStaffId(1001L);
|
||||
dto.setDeptId(10L);
|
||||
dto.setIdCard("320101199001010014");
|
||||
dto.setPhone("13812345678");
|
||||
dto.setStatus("0");
|
||||
dto.setAnnualIncome(annualIncome);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ class CcdiBaseStaffServiceImplTest {
|
||||
addDTO.setIdCard("320101199001010011");
|
||||
addDTO.setPhone("13812345678");
|
||||
addDTO.setStatus("0");
|
||||
addDTO.setAnnualIncome(new BigDecimal("12345.67"));
|
||||
addDTO.setAssetInfoList(List.of(
|
||||
buildAssetDto("房产"),
|
||||
buildAssetDto("车辆")
|
||||
@@ -67,6 +68,9 @@ class CcdiBaseStaffServiceImplTest {
|
||||
int result = service.insertBaseStaff(addDTO);
|
||||
|
||||
assertEquals(1, result);
|
||||
ArgumentCaptor<CcdiBaseStaff> staffCaptor = ArgumentCaptor.forClass(CcdiBaseStaff.class);
|
||||
verify(baseStaffMapper).insert(staffCaptor.capture());
|
||||
assertEquals(new BigDecimal("12345.67"), staffCaptor.getValue().getAnnualIncome());
|
||||
ArgumentCaptor<List<CcdiAssetInfoDTO>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(assetInfoService).replaceByFamilyId(eq("320101199001010011"), captor.capture());
|
||||
List<CcdiAssetInfoDTO> savedAssets = captor.getValue();
|
||||
@@ -88,6 +92,7 @@ class CcdiBaseStaffServiceImplTest {
|
||||
editDTO.setIdCard("320101199001010011");
|
||||
editDTO.setPhone("13812345678");
|
||||
editDTO.setStatus("0");
|
||||
editDTO.setAnnualIncome(new BigDecimal("45678.90"));
|
||||
editDTO.setAssetInfoList(List.of(buildAssetDto("车辆")));
|
||||
|
||||
when(baseStaffMapper.selectById(1001L)).thenReturn(existing);
|
||||
@@ -97,6 +102,9 @@ class CcdiBaseStaffServiceImplTest {
|
||||
int result = service.updateBaseStaff(editDTO);
|
||||
|
||||
assertEquals(1, result);
|
||||
ArgumentCaptor<CcdiBaseStaff> staffCaptor = ArgumentCaptor.forClass(CcdiBaseStaff.class);
|
||||
verify(baseStaffMapper).updateById(staffCaptor.capture());
|
||||
assertEquals(new BigDecimal("45678.90"), staffCaptor.getValue().getAnnualIncome());
|
||||
verify(assetInfoService, never()).deleteByFamilyId("320101199001010011");
|
||||
verify(assetInfoService).replaceByFamilyId("320101199001010011", editDTO.getAssetInfoList());
|
||||
}
|
||||
@@ -133,6 +141,7 @@ class CcdiBaseStaffServiceImplTest {
|
||||
staff.setName("张三");
|
||||
staff.setIdCard("320101199001010011");
|
||||
staff.setStatus("0");
|
||||
staff.setAnnualIncome(new BigDecimal("88888.88"));
|
||||
|
||||
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
|
||||
assetInfo.setFamilyId("320101199001010011");
|
||||
@@ -149,6 +158,7 @@ class CcdiBaseStaffServiceImplTest {
|
||||
CcdiBaseStaffVO result = service.selectBaseStaffById(1001L);
|
||||
|
||||
assertNotNull(result.getAssetInfoList());
|
||||
assertEquals(new BigDecimal("88888.88"), result.getAnnualIncome());
|
||||
assertEquals(1, result.getAssetInfoList().size());
|
||||
assertEquals("320101199201010022", result.getAssetInfoList().get(0).getPersonId());
|
||||
assertEquals("车辆", result.getAssetInfoList().get(0).getAssetMainType());
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ruoyi.info.collection.service;
|
||||
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
|
||||
import com.ruoyi.info.collection.service.impl.CcdiStaffFmyRelationImportServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class CcdiStaffFmyRelationImportServiceImplTest {
|
||||
|
||||
private final CcdiStaffFmyRelationImportServiceImpl service = new CcdiStaffFmyRelationImportServiceImpl();
|
||||
|
||||
@Test
|
||||
void validateRelationData_shouldRejectNegativeAnnualIncome() throws Exception {
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> invokeValidateRelationData(buildDto(new BigDecimal("-1.00"))));
|
||||
|
||||
assertEquals("家庭成员年收入不能为负数", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateRelationData_shouldRejectAnnualIncomeWithMoreThanTwoDecimals() throws Exception {
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> invokeValidateRelationData(buildDto(new BigDecimal("12.345"))));
|
||||
|
||||
assertEquals("家庭成员年收入最多保留2位小数", exception.getMessage());
|
||||
}
|
||||
|
||||
private void invokeValidateRelationData(CcdiStaffFmyRelationAddDTO dto) throws Exception {
|
||||
Method method = CcdiStaffFmyRelationImportServiceImpl.class
|
||||
.getDeclaredMethod("validateRelationData", CcdiStaffFmyRelationAddDTO.class);
|
||||
method.setAccessible(true);
|
||||
try {
|
||||
method.invoke(service, dto);
|
||||
} catch (InvocationTargetException ex) {
|
||||
Throwable targetException = ex.getTargetException();
|
||||
if (targetException instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private CcdiStaffFmyRelationAddDTO buildDto(BigDecimal annualIncome) {
|
||||
CcdiStaffFmyRelationAddDTO dto = new CcdiStaffFmyRelationAddDTO();
|
||||
dto.setPersonId("320101199001010014");
|
||||
dto.setRelationType("配偶");
|
||||
dto.setRelationName("李四");
|
||||
dto.setRelationCertType("护照");
|
||||
dto.setRelationCertNo("A123456789");
|
||||
dto.setAnnualIncome(annualIncome);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ class CcdiStaffFmyRelationServiceImplTest {
|
||||
addDTO.setRelationName("李四");
|
||||
addDTO.setRelationCertType("护照");
|
||||
addDTO.setRelationCertNo("A123456789");
|
||||
addDTO.setAnnualIncome(new BigDecimal("23456.78"));
|
||||
addDTO.setAssetInfoList(List.of(buildAssetDto("房产")));
|
||||
|
||||
when(relationMapper.insert(any(CcdiStaffFmyRelation.class))).thenReturn(1);
|
||||
@@ -89,6 +90,7 @@ class CcdiStaffFmyRelationServiceImplTest {
|
||||
assertEquals("MANUAL", relationCaptor.getValue().getDataSource());
|
||||
assertEquals(Boolean.TRUE, relationCaptor.getValue().getIsEmpFamily());
|
||||
assertEquals(Boolean.FALSE, relationCaptor.getValue().getIsCustFamily());
|
||||
assertEquals(new BigDecimal("23456.78"), relationCaptor.getValue().getAnnualIncome());
|
||||
verify(assetInfoService).replaceByFamilyIdAndPersonId("320101199001010011", "A123456789", addDTO.getAssetInfoList());
|
||||
}
|
||||
|
||||
@@ -154,6 +156,7 @@ class CcdiStaffFmyRelationServiceImplTest {
|
||||
editDTO.setRelationName("李四");
|
||||
editDTO.setRelationCertType("护照");
|
||||
editDTO.setRelationCertNo("A123456789");
|
||||
editDTO.setAnnualIncome(new BigDecimal("76543.21"));
|
||||
editDTO.setAssetInfoList(List.of(buildAssetDto("车辆")));
|
||||
|
||||
when(relationMapper.selectById(10L)).thenReturn(existing);
|
||||
@@ -162,6 +165,9 @@ class CcdiStaffFmyRelationServiceImplTest {
|
||||
int result = service.updateRelation(editDTO);
|
||||
|
||||
assertEquals(1, result);
|
||||
ArgumentCaptor<CcdiStaffFmyRelation> relationCaptor = ArgumentCaptor.forClass(CcdiStaffFmyRelation.class);
|
||||
verify(relationMapper).updateById(relationCaptor.capture());
|
||||
assertEquals(new BigDecimal("76543.21"), relationCaptor.getValue().getAnnualIncome());
|
||||
verify(assetInfoService).replaceByFamilyIdAndPersonId("320101199001010011", "A123456789", editDTO.getAssetInfoList());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ruoyi.ccdi.project.sql;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiModelParamSqlDefaultsTest {
|
||||
|
||||
@Test
|
||||
void defaultSql_shouldUseQuarterlyStableIncomeMinAndMaxParams() throws IOException {
|
||||
String initSql = readProjectFile("sql", "ccdi_model_param.sql");
|
||||
String updateSql = readProjectFile("sql", "2026-03-16-update-ccdi-model-param-defaults.sql");
|
||||
|
||||
assertQuarterlyStableIncomeRangeConfig(initSql);
|
||||
assertQuarterlyStableIncomeRangeConfig(updateSql);
|
||||
}
|
||||
|
||||
private void assertQuarterlyStableIncomeRangeConfig(String sqlContent) {
|
||||
assertAll(
|
||||
() -> assertTrue(sqlContent.contains("FIXED_COUNTERPARTY_TRANSFER_MIN"),
|
||||
"应包含季度稳定收入金额下限参数编码"),
|
||||
() -> assertTrue(sqlContent.contains("FIXED_COUNTERPARTY_TRANSFER_MAX"),
|
||||
"应包含季度稳定收入金额上限参数编码"),
|
||||
() -> assertTrue(sqlContent.contains("季度稳定收入金额下限"),
|
||||
"应包含季度稳定收入金额下限参数名称"),
|
||||
() -> assertTrue(sqlContent.contains("季度稳定收入金额上限"),
|
||||
"应包含季度稳定收入金额上限参数名称"),
|
||||
() -> assertTrue(sqlContent.contains("'3000'"),
|
||||
"应包含季度稳定收入金额下限默认值3000"),
|
||||
() -> assertTrue(sqlContent.contains("'15000'"),
|
||||
"应包含季度稳定收入金额上限默认值15000"),
|
||||
() -> assertFalse(sqlContent.contains("'FIXED_COUNTERPARTY_TRANSFER'"),
|
||||
"不应继续保留旧的单值季度稳定收入金额参数编码")
|
||||
);
|
||||
}
|
||||
|
||||
private String readProjectFile(String... parts) throws IOException {
|
||||
Path path = Path.of("..", parts);
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.ccdi.project.sql;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MysqlUtf8ExecScriptTest {
|
||||
|
||||
@Test
|
||||
void mysqlUtf8ExecScript_shouldForceUtf8SessionAndSourceSqlFile() throws IOException {
|
||||
String script = Files.readString(
|
||||
Path.of("..", "bin", "mysql_utf8_exec.sh"),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
assertAll(
|
||||
() -> assertTrue(script.contains("application-dev.yml"),
|
||||
"脚本应读取 application-dev.yml 中的数据库连接信息"),
|
||||
() -> assertTrue(script.contains("--default-character-set=utf8mb4"),
|
||||
"脚本应强制 mysql 客户端使用 utf8mb4"),
|
||||
() -> assertTrue(script.contains("--init-command")
|
||||
&& script.contains("SET NAMES utf8mb4"),
|
||||
"脚本应在 mysql 会话初始化时显式执行 SET NAMES utf8mb4"),
|
||||
() -> assertTrue(script.contains("< \"${ABS_SQL_FILE}\""),
|
||||
"脚本应直接重定向 SQL 文件执行")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# 信息维护年收入字段后端实施计划
|
||||
|
||||
## 目标
|
||||
- 员工信息维护新增 `annual_income`
|
||||
- 员工亲属关系维护新增 `annual_income`
|
||||
- 覆盖数据库、实体、DTO/VO、Mapper、导入导出、失败记录、服务层校验
|
||||
|
||||
## 实施内容
|
||||
1. 数据库变更
|
||||
- 在 `ccdi_base_staff` 新增 `annual_income DECIMAL(15,2) NULL`
|
||||
- 在 `ccdi_staff_fmy_relation` 新增 `annual_income DECIMAL(15,2) NULL`
|
||||
- 增量脚本落库到 `sql/migration/2026-03-17-add-annual-income-to-info-maintenance.sql`
|
||||
|
||||
2. 员工信息链路
|
||||
- 扩展 `CcdiBaseStaff`、`CcdiBaseStaffAddDTO`、`CcdiBaseStaffEditDTO`、`CcdiBaseStaffVO`
|
||||
- 扩展 `CcdiBaseStaffExcel` 与 `ImportFailureVO`
|
||||
- 更新 `CcdiBaseStaffMapper.xml` 的分页查询、批量插入、批量更新 SQL
|
||||
- 在 `CcdiBaseStaffServiceImpl` 与 `CcdiBaseStaffImportServiceImpl` 增加非负且最多两位小数校验
|
||||
|
||||
3. 员工亲属关系链路
|
||||
- 扩展 `CcdiStaffFmyRelation`、`CcdiStaffFmyRelationAddDTO`、`CcdiStaffFmyRelationEditDTO`、`CcdiStaffFmyRelationVO`
|
||||
- 扩展 `CcdiStaffFmyRelationExcel` 与 `StaffFmyRelationImportFailureVO`
|
||||
- 更新 `CcdiStaffFmyRelationMapper.xml` 的列表、详情、导出、批量插入 SQL
|
||||
- 在 `CcdiStaffFmyRelationServiceImpl` 与 `CcdiStaffFmyRelationImportServiceImpl` 增加非负且最多两位小数校验
|
||||
|
||||
## 测试
|
||||
- `CcdiBaseStaffServiceImplTest`
|
||||
- `CcdiStaffFmyRelationServiceImplTest`
|
||||
- `CcdiBaseStaffImportServiceImplTest`
|
||||
- `CcdiStaffFmyRelationImportServiceImplTest`
|
||||
- `CcdiBaseStaffMapperTest`
|
||||
- `CcdiStaffFmyRelationMapperTest`
|
||||
|
||||
## 验收标准
|
||||
- 列表、详情、导出和导入都能透传 `annualIncome`
|
||||
- 服务层可拦截负数与超过两位小数
|
||||
- 失败记录可返回原始年收入值
|
||||
@@ -0,0 +1,38 @@
|
||||
# 信息维护年收入字段前端实施计划
|
||||
|
||||
## 目标
|
||||
- 在员工信息维护页面新增“年收入”
|
||||
- 在员工亲属关系维护页面新增“家庭成员年收入”
|
||||
- 保持现有页面结构和接口调用方式不变
|
||||
|
||||
## 实施内容
|
||||
1. 员工信息维护页
|
||||
- 列表新增“年收入”列
|
||||
- 新增/编辑弹窗新增“年收入”输入框
|
||||
- 详情弹窗展示“年收入”
|
||||
- 导入失败记录弹窗新增“年收入”列
|
||||
- 提交前做空值兼容与金额格式校验
|
||||
|
||||
2. 员工亲属关系维护页
|
||||
- 列表新增“家庭成员年收入”列
|
||||
- 新增/编辑弹窗新增“家庭成员年收入”输入框
|
||||
- 详情弹窗展示“家庭成员年收入”
|
||||
- 导入失败记录弹窗新增“家庭成员年收入”列
|
||||
- 提交前做空值兼容与金额格式校验
|
||||
|
||||
3. 前端校验规则
|
||||
- 允许空值
|
||||
- 非空时仅允许非负金额
|
||||
- 最多保留两位小数
|
||||
- 不做单位换算,直接按“元/年”展示和提交
|
||||
|
||||
## 测试
|
||||
- `ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js`
|
||||
- `ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js`
|
||||
- `ruoyi-ui/tests/unit/staff-family-asset-submit-flow.test.js`
|
||||
- `ruoyi-ui/tests/unit/staff-family-asset-maintenance-layout.test.js`
|
||||
|
||||
## 验收标准
|
||||
- 两个页面都能新增、编辑、查看年收入字段
|
||||
- 历史空值数据页面正常展示
|
||||
- 前端能阻止非法金额提交
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,286 @@
|
||||
# 银行流水模型补齐占位设计
|
||||
|
||||
## 背景
|
||||
|
||||
当前项目的银行流水打标能力已经具备基础框架,包括:
|
||||
|
||||
- 规则定义表 `ccdi_bank_tag_rule`
|
||||
- 结果表 `ccdi_bank_statement_tag_result`
|
||||
- 任务表 `ccdi_bank_tag_task`
|
||||
- 规则执行入口 `CcdiBankTagServiceImpl`
|
||||
- 技术口径 SQL `CcdiBankTagAnalysisMapper.xml`
|
||||
|
||||
现阶段仅落地了“大额交易”模型下的 8 条规则。根据 [assets/模型信息.xlsx](../../../assets/模型信息.xlsx),其余模型规则尚未补齐到现有打标框架中,导致规则元数据、执行分发、XML SQL 坑位与参数配置之间不完整。
|
||||
|
||||
本次需求是基于现有银行流水模型打标能力,补齐尚未添加的模型规则,并自动填充必要字段。由于这些规则的正式 SQL 尚未准备好,本期先在 XML 中预留规则级 SQL 位置,并使用恒不命中的假 SQL 保证执行时不报错。
|
||||
|
||||
## 目标
|
||||
|
||||
- 按 `assets/模型信息.xlsx` 补齐现有缺失的模型规则
|
||||
- 保证新增规则进入现有打标执行框架,可被统一调度执行
|
||||
- 对每条新增规则预留独立的 XML SQL 坑位,便于后续逐条替换为真实 SQL
|
||||
- 在缺少正式 SQL 的阶段,保证规则执行返回空结果,不影响现有任务稳定性
|
||||
- 自动补齐规则初始化所需的最小字段,减少手工维护成本
|
||||
|
||||
## 非目标
|
||||
|
||||
- 本期不编写真实业务 SQL
|
||||
- 本期不调整现有 8 条“大额交易”规则的编码、参数与 SQL 逻辑
|
||||
- 本期不新增前端页面或结果展示逻辑
|
||||
- 本期不为 Excel 中缺少英文指标名的规则补充参数配置
|
||||
- 本期不引入动态 SQL 配置能力
|
||||
|
||||
## 现状分析
|
||||
|
||||
### 现有规则实现范围
|
||||
|
||||
当前 `CcdiBankTagServiceImpl` 仅实现以下规则分发:
|
||||
|
||||
- `HOUSE_OR_CAR_EXPENSE`
|
||||
- `TAX_EXPENSE`
|
||||
- `SINGLE_LARGE_INCOME`
|
||||
- `CUMULATIVE_INCOME`
|
||||
- `ANNUAL_TURNOVER`
|
||||
- `LARGE_CASH_DEPOSIT`
|
||||
- `FREQUENT_CASH_DEPOSIT`
|
||||
- `LARGE_TRANSFER`
|
||||
|
||||
这些规则均属于 `LARGE_TRANSACTION` 模型。
|
||||
|
||||
### 现有参数配置范围
|
||||
|
||||
当前默认参数脚本中已经包含以下模型:
|
||||
|
||||
- `LARGE_TRANSACTION`
|
||||
- `SUSPICIOUS_PART_TIME`
|
||||
- `SUSPICIOUS_FOREIGN_EXCHANGE`
|
||||
- `ABNORMAL_BEHAVIOR`
|
||||
- `SUSPICIOUS_GAMBLING`
|
||||
|
||||
但其中多数模型只补了参数,尚未补规则元数据和打标执行入口。
|
||||
|
||||
### Excel 规则范围
|
||||
|
||||
`assets/模型信息.xlsx` 共 33 条规则,其中:
|
||||
|
||||
- 已落地 8 条
|
||||
- 待补齐 25 条
|
||||
|
||||
涉及以下模型组:
|
||||
|
||||
- 异常交易
|
||||
- 疑似赌博
|
||||
- 可疑关系
|
||||
- 可疑兼职
|
||||
- 可疑财产
|
||||
- 可疑外汇交易
|
||||
- 可疑付息
|
||||
- 可疑采购
|
||||
- 异常行为
|
||||
|
||||
## 方案对比
|
||||
|
||||
### 方案一:仅补规则初始化 SQL
|
||||
|
||||
优点:
|
||||
|
||||
- 改动最少
|
||||
|
||||
缺点:
|
||||
|
||||
- Java 分发与 XML 坑位仍然缺失
|
||||
- 后续接入真实 SQL 时仍需再次补完整链路
|
||||
- 无法满足“先留好位置”的目标
|
||||
|
||||
### 方案二:补规则元数据、Java 分发与 XML 占位 SQL
|
||||
|
||||
优点:
|
||||
|
||||
- 与现有大额交易模型实现方式一致
|
||||
- 每条规则都有独立执行入口与独立 SQL 坑位
|
||||
- 后续替换真实 SQL 时只需改对应 XML 或局部参数映射
|
||||
- 风险可控,不会产生误命中数据
|
||||
|
||||
缺点:
|
||||
|
||||
- 本期一次性需要补齐较多占位方法与规则定义
|
||||
|
||||
### 方案三:新增统一空实现兜底
|
||||
|
||||
优点:
|
||||
|
||||
- 代码量更少
|
||||
|
||||
缺点:
|
||||
|
||||
- 规则边界不清晰
|
||||
- 后续逐条补 SQL 时定位成本更高
|
||||
- 不利于维护和评审
|
||||
|
||||
## 最终方案
|
||||
|
||||
采用方案二:
|
||||
|
||||
- 补齐规则元数据
|
||||
- 补齐 `CcdiBankTagAnalysisMapper` 方法定义
|
||||
- 补齐 `CcdiBankTagServiceImpl` 的规则分发
|
||||
- 在 `CcdiBankTagAnalysisMapper.xml` 中为每条新规则增加单独的占位 SQL
|
||||
- 对缺少正式 SQL 的规则统一返回空结果,保证执行成功但不命中
|
||||
|
||||
## 编码设计
|
||||
|
||||
### 模型编码映射
|
||||
|
||||
新增模型统一映射为稳定的 `model_code`:
|
||||
|
||||
- `异常交易` -> `ABNORMAL_TRANSACTION`
|
||||
- `疑似赌博` -> `SUSPICIOUS_GAMBLING`
|
||||
- `可疑关系` -> `SUSPICIOUS_RELATION`
|
||||
- `可疑兼职` -> `SUSPICIOUS_PART_TIME`
|
||||
- `可疑财产` -> `SUSPICIOUS_PROPERTY`
|
||||
- `可疑外汇交易` -> `SUSPICIOUS_FOREIGN_EXCHANGE`
|
||||
- `可疑付息` -> `SUSPICIOUS_INTEREST_PAYMENT`
|
||||
- `可疑采购` -> `SUSPICIOUS_PURCHASE`
|
||||
- `异常行为` -> `ABNORMAL_BEHAVIOR`
|
||||
|
||||
已在参数表中使用的模型编码保持不变,避免后续参数关联断裂。
|
||||
|
||||
### 规则编码生成
|
||||
|
||||
规则编码采用以下优先级:
|
||||
|
||||
1. 已有现存规则编码的,保持不变
|
||||
2. Excel 提供了明确英文指标名且适合复用为规则编码的,直接使用英文指标名
|
||||
3. Excel 未提供英文指标名的,使用稳定占位编码
|
||||
|
||||
占位编码格式:
|
||||
|
||||
- `<MODEL_CODE>_<两位序号>`
|
||||
|
||||
示例:
|
||||
|
||||
- `ABNORMAL_TRANSACTION_01`
|
||||
- `SUSPICIOUS_PROPERTY_03`
|
||||
|
||||
这样可确保编码稳定、可批量生成、可回溯到 Excel 顺序,同时避免为无英文名规则主观造词。
|
||||
|
||||
### 自动补齐字段规则
|
||||
|
||||
- `model_code`:由中文模型名映射生成
|
||||
- `model_name`:使用 Excel 中的模型名称
|
||||
- `rule_code`:按上述规则生成
|
||||
- `rule_name`:使用“核心异常点(展示在前端页面)”
|
||||
- `indicator_code`:有英文指标名则写入,无则留空
|
||||
- `business_caliber`:使用 Excel 中“业务口径”
|
||||
- `result_type`:
|
||||
- `可疑结果返回` 包含“流水明细” -> `STATEMENT`
|
||||
- 其他返回形式 -> `OBJECT`
|
||||
- `risk_level`:
|
||||
- `高风险` -> `HIGH`
|
||||
- `一般` -> `GENERAL`
|
||||
- 空值 -> `NULL`
|
||||
|
||||
## 参数配置策略
|
||||
|
||||
只为 Excel 中明确给出英文指标名、且明显属于阈值参数的规则补默认参数。
|
||||
|
||||
处理原则:
|
||||
|
||||
- 有英文指标名且已存在默认参数的,保持现状
|
||||
- 有英文指标名但默认参数缺失的,补充默认参数初始化数据
|
||||
- 无英文指标名的,不新增参数记录
|
||||
- `BankTagRuleConfigResolver` 仅补需要参数解析的规则映射
|
||||
|
||||
本期默认假设:
|
||||
|
||||
- 无英文指标名规则不依赖参数配置
|
||||
- 其 `indicator_code` 可为空
|
||||
|
||||
## 占位 SQL 设计
|
||||
|
||||
### 设计原则
|
||||
|
||||
- 每条规则拥有单独 `<select>`,不共用通用 SQL
|
||||
- SQL 必须可被 MyBatis 正常解析和执行
|
||||
- SQL 必须返回与结果映射一致的字段结构
|
||||
- SQL 必须恒不命中,避免生成伪造结果
|
||||
|
||||
### STATEMENT 规则占位模板
|
||||
|
||||
对流水级结果使用如下结构:
|
||||
|
||||
```sql
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
'占位SQL,待补充真实规则' AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
where 1 = 0
|
||||
```
|
||||
|
||||
### OBJECT 规则占位模板
|
||||
|
||||
对对象级结果使用如下结构:
|
||||
|
||||
```sql
|
||||
select
|
||||
'STAFF_ID_CARD' AS objectType,
|
||||
'' AS objectKey,
|
||||
'占位SQL,待补充真实规则' AS reasonDetail
|
||||
from dual
|
||||
where 1 = 0
|
||||
```
|
||||
|
||||
如果数据库兼容性不适合 `dual`,则改为从现有业务表取空结果,核心要求是不报错且字段齐全。
|
||||
|
||||
## 详细变更范围
|
||||
|
||||
### 后端代码
|
||||
|
||||
需要修改以下文件:
|
||||
|
||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java`
|
||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
|
||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java`
|
||||
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml`
|
||||
|
||||
### SQL 脚本
|
||||
|
||||
需要修改以下文件:
|
||||
|
||||
- `sql/2026-03-16-bank-tagging.sql`
|
||||
- `sql/ccdi_model_param.sql`
|
||||
- `sql/2026-03-16-update-ccdi-model-param-defaults.sql`
|
||||
|
||||
## 兼容性与风险控制
|
||||
|
||||
- 现有 8 条大额交易规则不改编码、不改 SQL、不改参数映射
|
||||
- 新增规则即使被执行,也只会返回空结果
|
||||
- 不会写入脏数据到 `ccdi_bank_statement_tag_result`
|
||||
- 不会因为找不到 Mapper 方法或 XML 语句导致任务失败
|
||||
- 无参数规则不会加入必填参数集合,避免产生无意义缺参告警
|
||||
|
||||
## 验证方案
|
||||
|
||||
最小验证包括:
|
||||
|
||||
- 编译通过,确保 Java 接口与 XML 绑定一致
|
||||
- 触发打标任务时,不出现 `Invalid bound statement` 或参数解析异常
|
||||
- 现有 8 条规则仍保持原有执行行为
|
||||
- 新增占位规则执行后返回空结果,不新增命中数据
|
||||
|
||||
建议执行:
|
||||
|
||||
- `mvn -pl ccdi-project -am test`
|
||||
- 如测试范围过大,至少执行 `mvn -pl ccdi-project -am compile`
|
||||
|
||||
## 实施边界
|
||||
|
||||
本期完成后,系统将具备“规则骨架完整、真实 SQL 待补”的状态。后续每补一条真实规则时,只需要:
|
||||
|
||||
1. 替换对应 XML 占位 SQL
|
||||
2. 如需阈值参数,再补 `BankTagRuleConfigResolver` 映射或默认参数
|
||||
3. 按规则粒度补测试
|
||||
|
||||
无需再次调整规则框架、结果表、任务调度和规则定义结构。
|
||||
BIN
logs/spring.log.2026-03-17.0.gz
Normal file
BIN
logs/spring.log.2026-03-17.0.gz
Normal file
Binary file not shown.
@@ -115,6 +115,7 @@
|
||||
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="电话" align="center" prop="phone" width="120"/>
|
||||
<el-table-column label="年收入" align="center" prop="annualIncome" width="140"/>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === '0'" type="success">在职</el-tag>
|
||||
@@ -204,11 +205,19 @@
|
||||
<el-input v-model="form.phone" placeholder="请输入电话" maxlength="11" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="年收入" prop="annualIncome">
|
||||
<el-input v-model="form.annualIncome" placeholder="请输入年收入(元/年)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="入职时间" prop="hireDate">
|
||||
<el-date-picker v-model="form.hireDate" type="date" placeholder="选择入职时间" value-format="yyyy-MM-dd" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" />
|
||||
</el-row>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
@@ -318,6 +327,9 @@
|
||||
<el-descriptions-item label="所属部门">{{ employeeDetail.deptName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ employeeDetail.idCard || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ employeeDetail.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="年收入">
|
||||
{{ employeeDetail.annualIncome !== null && employeeDetail.annualIncome !== undefined && employeeDetail.annualIncome !== '' ? employeeDetail.annualIncome : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="入职时间">
|
||||
{{ employeeDetail.hireDate ? parseTime(employeeDetail.hireDate, '{y}-{m}-{d}') : '-' }}
|
||||
</el-descriptions-item>
|
||||
@@ -453,6 +465,7 @@
|
||||
<el-table-column label="柜员号" prop="staffId" align="center" />
|
||||
<el-table-column label="身份证号" prop="idCard" align="center" />
|
||||
<el-table-column label="电话" prop="phone" align="center" />
|
||||
<el-table-column label="年收入" prop="annualIncome" align="center" width="140" />
|
||||
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
|
||||
@@ -606,6 +619,9 @@ export default {
|
||||
{ required: true, message: "电话不能为空", trigger: "blur" },
|
||||
{ pattern: phonePattern, message: "请输入正确的11位手机号", trigger: "blur" }
|
||||
],
|
||||
annualIncome: [
|
||||
{ validator: (rule, value, callback) => this.validateAnnualIncomeRule(value, callback, "年收入"), trigger: "blur" }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: "请选择状态", trigger: "change" }
|
||||
]
|
||||
@@ -938,6 +954,7 @@ export default {
|
||||
deptId: null,
|
||||
idCard: null,
|
||||
phone: null,
|
||||
annualIncome: null,
|
||||
hireDate: null,
|
||||
status: "0",
|
||||
relatives: [],
|
||||
@@ -945,6 +962,36 @@ export default {
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
normalizeAnnualIncome(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const normalized = String(value).trim();
|
||||
return normalized === "" ? null : normalized;
|
||||
},
|
||||
validateAnnualIncome(value, fieldLabel = "年收入") {
|
||||
const normalized = this.normalizeAnnualIncome(value);
|
||||
if (normalized === null) {
|
||||
return true;
|
||||
}
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
|
||||
this.$modal.msgError(`${fieldLabel}格式不正确,需为非负金额且最多保留2位小数`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validateAnnualIncomeRule(value, callback, fieldLabel = "年收入") {
|
||||
const normalized = this.normalizeAnnualIncome(value);
|
||||
if (normalized === null) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
|
||||
callback(new Error(`${fieldLabel}格式不正确,需为非负金额且最多保留2位小数`));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
normalizeAssetInfoList() {
|
||||
const assetInfoList = Array.isArray(this.form.assetInfoList)
|
||||
? this.form.assetInfoList
|
||||
@@ -1114,6 +1161,7 @@ export default {
|
||||
...response.data,
|
||||
assetInfoList: response.data.assetInfoList || []
|
||||
};
|
||||
this.form.annualIncome = response.data.annualIncome;
|
||||
this.open = true;
|
||||
this.title = "编辑员工";
|
||||
});
|
||||
@@ -1123,18 +1171,27 @@ export default {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.form.assetInfoList = this.normalizeAssetInfoList();
|
||||
if (!this.validateAssetInfoList(this.form.assetInfoList)) {
|
||||
const payload = {
|
||||
...this.form,
|
||||
assetInfoList: this.form.assetInfoList
|
||||
};
|
||||
payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);
|
||||
this.form.assetInfoList = payload.assetInfoList;
|
||||
if (!this.validateAnnualIncome(payload.annualIncome, "年收入")) {
|
||||
return;
|
||||
}
|
||||
if (!this.validateAssetInfoList(payload.assetInfoList)) {
|
||||
return;
|
||||
}
|
||||
if (this.isAdd) {
|
||||
addBaseStaff(this.form).then(response => {
|
||||
addBaseStaff(payload).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.isAdd = false;
|
||||
this.getList();
|
||||
});
|
||||
} else {
|
||||
updateBaseStaff(this.form).then(response => {
|
||||
updateBaseStaff(payload).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关系人姓名" align="center" prop="relationName" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="家庭成员年收入" align="center" prop="annualIncome" width="160"/>
|
||||
<el-table-column label="性别" align="center" prop="gender" width="80">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.ccdi_indiv_gender" :value="scope.row.gender"/>
|
||||
@@ -282,6 +283,13 @@
|
||||
<el-input v-model="form.mobilePhone2" placeholder="请输入手机号码2" maxlength="20" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="家庭成员年收入" prop="annualIncome">
|
||||
<el-input v-model="form.annualIncome" placeholder="请输入家庭成员年收入(元/年)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="微信名称1" prop="wechatNo1">
|
||||
<el-input v-model="form.wechatNo1" placeholder="请输入微信名称1" maxlength="50" />
|
||||
@@ -466,6 +474,9 @@
|
||||
<el-descriptions-item label="出生日期">{{ relationDetail.birthDate || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号码1">{{ relationDetail.mobilePhone1 || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号码2">{{ relationDetail.mobilePhone2 || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="家庭成员年收入">
|
||||
{{ relationDetail.annualIncome !== null && relationDetail.annualIncome !== undefined && relationDetail.annualIncome !== '' ? relationDetail.annualIncome : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信名称1">{{ relationDetail.wechatNo1 || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="微信名称2">{{ relationDetail.wechatNo2 || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="微信名称3">{{ relationDetail.wechatNo3 || '-' }}</el-descriptions-item>
|
||||
@@ -616,6 +627,7 @@
|
||||
<el-table-column label="员工身份证号" prop="personId" align="center" width="180"/>
|
||||
<el-table-column label="关系类型" prop="relationType" align="center" width="100"/>
|
||||
<el-table-column label="关系人姓名" prop="relationName" align="center" width="120"/>
|
||||
<el-table-column label="家庭成员年收入" prop="annualIncome" align="center" width="160"/>
|
||||
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
|
||||
@@ -766,6 +778,9 @@ export default {
|
||||
],
|
||||
mobilePhone2: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
|
||||
],
|
||||
annualIncome: [
|
||||
{ validator: (rule, value, callback) => this.validateAnnualIncomeRule(value, callback, "家庭成员年收入"), trigger: "blur" }
|
||||
]
|
||||
},
|
||||
// 导入参数
|
||||
@@ -963,6 +978,7 @@ export default {
|
||||
relationCertNo: null,
|
||||
mobilePhone1: null,
|
||||
mobilePhone2: null,
|
||||
annualIncome: null,
|
||||
wechatNo1: null,
|
||||
wechatNo2: null,
|
||||
wechatNo3: null,
|
||||
@@ -977,6 +993,36 @@ export default {
|
||||
this.staffOptions = [];
|
||||
this.resetForm("form");
|
||||
},
|
||||
normalizeAnnualIncome(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const normalized = String(value).trim();
|
||||
return normalized === "" ? null : normalized;
|
||||
},
|
||||
validateAnnualIncome(value, fieldLabel = "家庭成员年收入") {
|
||||
const normalized = this.normalizeAnnualIncome(value);
|
||||
if (normalized === null) {
|
||||
return true;
|
||||
}
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
|
||||
this.$modal.msgError(`${fieldLabel}格式不正确,需为非负金额且最多保留2位小数`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validateAnnualIncomeRule(value, callback, fieldLabel = "家庭成员年收入") {
|
||||
const normalized = this.normalizeAnnualIncome(value);
|
||||
if (normalized === null) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
|
||||
callback(new Error(`${fieldLabel}格式不正确,需为非负金额且最多保留2位小数`));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
createEmptyAssetRow() {
|
||||
return {
|
||||
assetMainType: "",
|
||||
@@ -1098,6 +1144,7 @@ export default {
|
||||
...response.data,
|
||||
assetInfoList: response.data.assetInfoList || []
|
||||
};
|
||||
this.form.annualIncome = response.data.annualIncome;
|
||||
// 加载员工信息以支持下拉显示
|
||||
if (this.form.personId) {
|
||||
this.searchStaff(this.form.personId);
|
||||
@@ -1131,6 +1178,10 @@ export default {
|
||||
...this.form,
|
||||
assetInfoList: this.form.assetInfoList.map(item => ({ ...item }))
|
||||
};
|
||||
payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);
|
||||
if (!this.validateAnnualIncome(payload.annualIncome, "家庭成员年收入")) {
|
||||
return;
|
||||
}
|
||||
payload.assetInfoList.forEach((asset, index) => {
|
||||
delete payload.assetInfoList[index].familyId;
|
||||
delete payload.assetInfoList[index].personId;
|
||||
|
||||
@@ -9,6 +9,9 @@ const componentPath = path.resolve(
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
[
|
||||
"年收入",
|
||||
"v-model=\"form.annualIncome\"",
|
||||
"employeeDetail.annualIncome",
|
||||
"<el-dialog :title=\"title\" :visible.sync=\"open\" width=\"80%\"",
|
||||
"<el-dialog title=\"员工详情\" :visible.sync=\"detailOpen\" width=\"80%\"",
|
||||
"资产信息",
|
||||
|
||||
@@ -9,6 +9,13 @@ const componentPath = path.resolve(
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
[
|
||||
"annualIncome: null",
|
||||
"label=\"年收入\"",
|
||||
"v-model=\"form.annualIncome\"",
|
||||
"this.form.annualIncome = response.data.annualIncome",
|
||||
"payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);",
|
||||
"normalizeAnnualIncome(value)",
|
||||
"validateAnnualIncome(value, fieldLabel = \"年收入\")",
|
||||
"assetInfoList: []",
|
||||
"normalizeAssetInfoList()",
|
||||
"this.form.assetInfoList = [];",
|
||||
|
||||
@@ -9,6 +9,9 @@ const componentPath = path.resolve(
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
[
|
||||
"家庭成员年收入",
|
||||
"v-model=\"form.annualIncome\"",
|
||||
"relationDetail.annualIncome",
|
||||
"亲属资产信息",
|
||||
"新增资产",
|
||||
"handleAddAsset()",
|
||||
|
||||
@@ -9,6 +9,13 @@ const componentPath = path.resolve(
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
[
|
||||
"annualIncome: null",
|
||||
"label=\"家庭成员年收入\"",
|
||||
"v-model=\"form.annualIncome\"",
|
||||
"this.form.annualIncome = response.data.annualIncome",
|
||||
"payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);",
|
||||
"normalizeAnnualIncome(value)",
|
||||
"validateAnnualIncome(value, fieldLabel = \"家庭成员年收入\")",
|
||||
"assetInfoList: []",
|
||||
"normalizeAssetInfoList()",
|
||||
"response.data.assetInfoList || []",
|
||||
|
||||
@@ -23,7 +23,8 @@ INSERT INTO ccdi_model_param (
|
||||
(0, 'LARGE_TRANSACTION', '大额交易模型', 'FREQUENT_CASH_DEPOSIT', '单日多次存现次数', '24小时内累计存现超过', '5', '次', 5, 'admin', '系统默认参数'),
|
||||
(0, 'LARGE_TRANSACTION', '大额交易模型', 'FREQUENT_TRANSFER', '单笔大额转账金额', '单日转账次数超过', '100001', '次/日', 6, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'MONTHLY_FIXED_INCOME', '月度非本行工资收入金额', '除本行工资外,每月固定收入超过', '5000', '元/月', 1, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'FIXED_COUNTERPARTY_TRANSFER', '季度稳定收入金额', '每季从固定交易对手转入金额', '15000', '元/季', 2, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'FIXED_COUNTERPARTY_TRANSFER_MIN', '季度稳定收入金额下限', '每季从固定交易对手转入金额下限', '3000', '元/季', 2, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'FIXED_COUNTERPARTY_TRANSFER_MAX', '季度稳定收入金额上限', '每季从固定交易对手转入金额上限', '15000', '元/季', 3, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_FOREIGN_EXCHANGE', '可疑外汇交易模型', 'SINGLE_PURCHASE_AMOUNT', '单笔购汇金额', '单笔购汇超过该金额', '50000', '美元/笔', 1, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_FOREIGN_EXCHANGE', '可疑外汇交易模型', 'SINGLE_SETTLEMENT_AMOUNT', '单笔结汇金额', '单笔结汇超过该金额', '50000', '美元/笔', 2, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_FOREIGN_EXCHANGE', '可疑外汇交易模型', 'CROSS_BORDER_REMITTANCE', '跨境汇款金额', '跨境汇款金额超过', '200000', '美元/笔', 3, 'admin', '系统默认参数'),
|
||||
|
||||
@@ -35,7 +35,8 @@ INSERT INTO ccdi_model_param (project_id, model_code, model_name, param_code, pa
|
||||
(0, 'LARGE_TRANSACTION', '大额交易模型', 'FREQUENT_CASH_DEPOSIT', '单日多次存现次数', '24小时内累计存现超过', '5', '次', 5, 'admin', '系统默认参数'),
|
||||
(0, 'LARGE_TRANSACTION', '大额交易模型', 'FREQUENT_TRANSFER', '单笔大额转账金额', '单日转账次数超过', '100001', '次/日', 6, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'MONTHLY_FIXED_INCOME', '月度非本行工资收入金额', '除本行工资外,每月固定收入超过', '5000', '元/月', 1, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'FIXED_COUNTERPARTY_TRANSFER', '季度稳定收入金额', '每季从固定交易对手转入金额', '15000', '元/季', 2, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'FIXED_COUNTERPARTY_TRANSFER_MIN', '季度稳定收入金额下限', '每季从固定交易对手转入金额下限', '3000', '元/季', 2, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_PART_TIME', '可疑兼职模型', 'FIXED_COUNTERPARTY_TRANSFER_MAX', '季度稳定收入金额上限', '每季从固定交易对手转入金额上限', '15000', '元/季', 3, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_FOREIGN_EXCHANGE', '可疑外汇交易模型', 'SINGLE_PURCHASE_AMOUNT', '单笔购汇金额', '单笔购汇超过该金额', '50000', '美元/笔', 1, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_FOREIGN_EXCHANGE', '可疑外汇交易模型', 'SINGLE_SETTLEMENT_AMOUNT', '单笔结汇金额', '单笔结汇超过该金额', '50000', '美元/笔', 2, 'admin', '系统默认参数'),
|
||||
(0, 'SUSPICIOUS_FOREIGN_EXCHANGE', '可疑外汇交易模型', 'CROSS_BORDER_REMITTANCE', '跨境汇款金额', '跨境汇款金额超过', '200000', '美元/笔', 3, 'admin', '系统默认参数'),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
SET @base_staff_annual_income_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'ccdi_base_staff'
|
||||
AND column_name = 'annual_income'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `ccdi_base_staff` ADD COLUMN `annual_income` DECIMAL(15, 2) NULL COMMENT ''年收入(元/年)'' AFTER `phone`'
|
||||
);
|
||||
|
||||
PREPARE base_staff_annual_income_stmt FROM @base_staff_annual_income_sql;
|
||||
EXECUTE base_staff_annual_income_stmt;
|
||||
DEALLOCATE PREPARE base_staff_annual_income_stmt;
|
||||
|
||||
SET @staff_fmy_relation_annual_income_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'ccdi_staff_fmy_relation'
|
||||
AND column_name = 'annual_income'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `ccdi_staff_fmy_relation` ADD COLUMN `annual_income` DECIMAL(15, 2) NULL COMMENT ''家庭成员年收入(元/年)'' AFTER `mobile_phone2`'
|
||||
);
|
||||
|
||||
PREPARE staff_fmy_relation_annual_income_stmt FROM @staff_fmy_relation_annual_income_sql;
|
||||
EXECUTE staff_fmy_relation_annual_income_stmt;
|
||||
DEALLOCATE PREPARE staff_fmy_relation_annual_income_stmt;
|
||||
Reference in New Issue
Block a user