From 82cb751b8f0655146cbe9c600a586254f12435f0 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Tue, 17 Mar 2026 17:22:27 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=AD=A3=E5=BA=A6=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=94=B6=E5=85=A5=E5=8F=82=E6=95=B0=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E5=85=85UTF8=E6=89=A7=E8=A1=8C=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + assets/模型默认参数.csv | 3 +- bin/mysql_utf8_exec.sh | 55 +++++++++++++++++++ .../sql/CcdiModelParamSqlDefaultsTest.java | 48 ++++++++++++++++ .../project/sql/MysqlUtf8ExecScriptTest.java | 34 ++++++++++++ ...03-16-update-ccdi-model-param-defaults.sql | 3 +- sql/ccdi_model_param.sql | 3 +- 7 files changed, 145 insertions(+), 3 deletions(-) create mode 100755 bin/mysql_utf8_exec.sh create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiModelParamSqlDefaultsTest.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/MysqlUtf8ExecScriptTest.java diff --git a/AGENTS.md b/AGENTS.md index a10b2498..bba6a050 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,7 @@ - 前端开发直接在当前分支进行,不需要额外创建 git worktree - 测试结束后,自动关闭测试过程中启动的前后端进程 - 遇到 MCP 数据库操作时,使用项目配置文件中的数据库连接信息 +- 执行包含中文内容的 MySQL SQL 脚本时,禁止直接手写 `mysql -e` 或普通重定向执行;必须优先使用 `bin/mysql_utf8_exec.sh `,确保会话字符集为 `utf8mb4`,避免写入乱码 --- @@ -160,6 +161,7 @@ return AjaxResult.success(result); - 非业务字段如 `create_by`、`create_time` 由后端自动维护 - 前端表单不要暴露通用审计字段 - 新增菜单、字典、初始化数据时,同步补充 SQL 脚本 +- 执行数据库脚本前,需确认客户端会话字符集为 `utf8mb4`;涉及中文插入、更新时默认使用 `bin/mysql_utf8_exec.sh` ### 前端规范 diff --git a/assets/模型默认参数.csv b/assets/模型默认参数.csv index d1f17970..943af74b 100644 --- a/assets/模型默认参数.csv +++ b/assets/模型默认参数.csv @@ -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,美元/笔 diff --git a/bin/mysql_utf8_exec.sh b/bin/mysql_utf8_exec.sh new file mode 100755 index 00000000..74a8d118 --- /dev/null +++ b/bin/mysql_utf8_exec.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ "$#" -ne 1 ]; then + echo "Usage: bin/mysql_utf8_exec.sh " >&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}" diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiModelParamSqlDefaultsTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiModelParamSqlDefaultsTest.java new file mode 100644 index 00000000..2d3ddadb --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiModelParamSqlDefaultsTest.java @@ -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); + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/MysqlUtf8ExecScriptTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/MysqlUtf8ExecScriptTest.java new file mode 100644 index 00000000..3842dbb9 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/MysqlUtf8ExecScriptTest.java @@ -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 文件执行") + ); + } +} diff --git a/sql/2026-03-16-update-ccdi-model-param-defaults.sql b/sql/2026-03-16-update-ccdi-model-param-defaults.sql index 8ce58218..76bffd45 100644 --- a/sql/2026-03-16-update-ccdi-model-param-defaults.sql +++ b/sql/2026-03-16-update-ccdi-model-param-defaults.sql @@ -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', '系统默认参数'), diff --git a/sql/ccdi_model_param.sql b/sql/ccdi_model_param.sql index 69bb2262..8c4fea23 100644 --- a/sql/ccdi_model_param.sql +++ b/sql/ccdi_model_param.sql @@ -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', '系统默认参数'),