53 Commits

Author SHA1 Message Date
wkc
5a80653917 git ignore 2026-04-17 09:31:32 +08:00
wkc
bfe1b346d9 调整流程详情与对公新增弹窗展示并补充测试 2026-04-17 09:21:43 +08:00
wkc
ef40675422 gitignore 2026-04-16 16:09:13 +08:00
wkc
f1e4b26800 调整个人最终测算利率展示并重排详情卡片 2026-04-11 15:33:40 +08:00
wkc
235672304a delete 2026-04-11 15:02:57 +08:00
wkc
ec4a7c09db delete 2026-04-10 15:21:45 +08:00
wkc
5839a76f87 gitignore 2026-04-10 15:20:54 +08:00
wkc
fa0b446699 统一个人测算入参与重启脚本进程识别 2026-04-10 15:04:56 +08:00
wkc
f001047d0c 补充后端模型输入参数确认文档 2026-04-09 16:08:31 +08:00
wkc
09707d312e 新增上虞个人测算输入参数设计与计划文档 2026-04-09 16:05:38 +08:00
wkc
8e6eb5b382 生产prod 2026-04-08 15:35:46 +08:00
wkc
62784ee81a 修改字段 登陆 2026-04-03 10:47:16 +08:00
wkc
1e9340bbda 移除登录页默认账号密码 2026-04-03 10:45:58 +08:00
wkc
541969a837 完善贷款定价前后端实现与联调 2026-04-03 09:45:15 +08:00
wkc
351fae8cd3 收紧部署脚本jar进程识别 2026-04-01 11:08:32 +08:00
wkc
54eabaebd8 忽略部署脚本defunct进程 2026-04-01 11:06:53 +08:00
wkc
f874e2d942 适配部署压缩包目录结构 2026-04-01 11:00:36 +08:00
wkc
9b35d04e50 简化生产一键部署脚本 2026-04-01 10:53:00 +08:00
wkc
3a8f37f547 改用ps-ef识别部署进程 2026-04-01 10:49:25 +08:00
wkc
f8b2bf2afc 补充部署脚本netstat端口检测 2026-04-01 10:47:40 +08:00
wkc
3ce3c438a9 新增生产一键部署脚本 2026-04-01 10:32:57 +08:00
wkc
db5735897d 更新AGENTS双计划适用范围 2026-04-01 10:27:58 +08:00
wkc
99cdaacf10 新增生产一键部署脚本实施计划 2026-04-01 10:24:18 +08:00
wkc
0f9c9b30cd 新增生产一键部署脚本设计文档 2026-04-01 10:21:35 +08:00
wkc
14e72f0e5e 补充生产初始化脚本贷款定价菜单 2026-03-31 16:17:00 +08:00
wkc
1c2171ba24 新增生产初始化数据库脚本与实施记录 2026-03-31 16:11:10 +08:00
wkc
d96b8a8740 补充生产初始化数据库导出实施计划 2026-03-31 16:07:24 +08:00
wkc
4db4f542a5 新增生产初始化数据库导出设计文档 2026-03-31 16:04:59 +08:00
wkc
7c563b3315 修正后端重启脚本进程识别与端口配置 2026-03-31 09:55:14 +08:00
wkc
1245849f56 统一后端端口为63310 2026-03-30 17:36:49 +08:00
wkc
396a5e6c4c 补充AGENTS协作与测试规范文档 2026-03-30 14:03:14 +08:00
wkc
e89da2cd21 补充模型输出基本信息脱敏 2026-03-30 13:58:10 +08:00
wkc
2eb1e6b8ee 修正贷款定价敏感信息脱敏设计与计划 2026-03-30 13:39:12 +08:00
wkc
51890506b9 补充贷款定价敏感字段前端实施记录 2026-03-30 11:07:09 +08:00
wkc
44f48d5625 调整流程列表按客户内码查询 2026-03-30 11:07:05 +08:00
wkc
f37f2981f9 补充贷款定价敏感字段后端实施记录 2026-03-30 11:04:16 +08:00
wkc
85871e1380 接入流程详情脱敏与模型调用解密 2026-03-30 10:56:02 +08:00
wkc
a1db88e4c7 接入流程敏感字段加密与列表脱敏 2026-03-30 10:54:23 +08:00
wkc
b16a08eb1a 新增贷款定价敏感字段加解密服务 2026-03-30 10:51:44 +08:00
wkc
d7c305b26c 完成密码加密传输前端实现 2026-03-30 10:49:28 +08:00
wkc
8552514dcb 接入用户密码接口前端加密 2026-03-30 10:48:40 +08:00
wkc
b276654ac2 接入个人修改密码加密 2026-03-30 10:47:58 +08:00
wkc
2854c0bb38 新增贷款定价敏感信息加密实施计划 2026-03-30 10:47:25 +08:00
wkc
a7d5661275 接入登录注册密码加密 2026-03-30 10:47:18 +08:00
wkc
fdd1ce5525 新增前端密码加密工具 2026-03-30 10:46:33 +08:00
wkc
21208d8965 完成密码加密传输后端实现 2026-03-30 10:44:04 +08:00
wkc
717defc06e 新增贷款定价敏感信息加密设计文档 2026-03-30 10:43:15 +08:00
wkc
e8959805e5 接入用户密码接口解密 2026-03-30 10:43:13 +08:00
wkc
92bdd58d27 接入个人修改密码解密 2026-03-30 10:41:56 +08:00
wkc
68f823f0bc 接入登录注册密码解密 2026-03-30 10:40:24 +08:00
wkc
1623b77b4a 新增密码传输解密服务 2026-03-30 10:39:19 +08:00
wkc
92b67b6697 补充密码加密传输前后端实施计划 2026-03-30 10:36:31 +08:00
wkc
bcfda34009 补充密码加密传输设计文档 2026-03-30 10:32:33 +08:00
171 changed files with 29635 additions and 486 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,5 +0,0 @@
{
"permissions": {
"allow": []
}
}

View File

@@ -1,30 +0,0 @@
{
"permissions": {
"allow": [
"Bash(java:*)",
"Bash(binrun.bat:*)",
"Bash(mvn clean package:*)",
"Bash(curl:*)",
"Bash(pkill:*)",
"Bash(bash:*)",
"Bash(pip install:*)",
"Bash(findstr:*)",
"Bash(chcp:*)",
"Bash(cmd.exe:*)",
"Bash(powershell -Command:*)",
"Bash(git add:*)",
"Bash(cd:*)",
"mcp__zai-mcp-server__extract_text_from_screenshot",
"Bash(mvn test:*)",
"Bash(mvn install:*)",
"Bash(mvn clean install:*)",
"mcp__web-reader__webReader",
"Skill(superpowers:brainstorming)",
"Skill(superpowers:writing-plans)",
"Skill(superpowers:executing-plans)"
],
"additionalDirectories": [
"d:\\利率定价\\loan-pricing-892\\loan-pricing-892-v2.0"
]
}
}

View File

@@ -1,24 +0,0 @@
{
"paths": {
"specs": ".claude/specs",
"steering": ".claude/steering",
"settings": ".claude/settings"
},
"views": {
"specs": {
"visible": true
},
"steering": {
"visible": true
},
"mcp": {
"visible": true
},
"hooks": {
"visible": true
},
"settings": {
"visible": false
}
}
}

7
.gitignore vendored
View File

@@ -48,3 +48,10 @@ nbdist/
logs/
ruoyi-ui/dist.zip
*/src/test/
ruoyi-ui/tests
.playwright-cli
tongweb_63310.properties
audit.log

View File

@@ -0,0 +1,30 @@
# AGENTS.md - AI Coding Assistant Guide
## GIT
- git提交时使用中文添加描述
- 无视`.DS_Store`
## AGENT
- 不开启subagent
## 文档
- 如果是前后端开发任务,根据设计文档产出实施计划时,输出两份执行文档,一份为后端的实施计划,一份为前端的实施计划
- 每一次改动都需要留下实施文档,记录修改的内容
- 每次写设计文档的时候,都要检查一下保存路径是否正确
## 测试
- 开发完成后必须执行与本次改动直接对应的验证步骤,完成验证后才能结束本次任务
- 如果是接口开发完成,先重启后端进程,确保最新代码已经生效,再调用接口进行测试
- 接口测试时必须覆盖多种情况,至少包含正常场景、必填/参数错误场景、分支场景;如接口逻辑包含状态、类型、金额、期限等关键条件,需要分别验证对应分支
- 如果是前端页面开发完成,必须启动前端页面并调用浏览器检查功能是否正常,确认页面展示、交互流程、接口联动和关键提示信息符合预期
- 测试结束后,自动结束测试时开启的前后端进程
## 开发
- 在开发前端的时候不需要使用git worktree直接在当前分支进行开发
## 方案规范
- 当需要你给出方案时,必须符合以下规范
- 不允许给出兼容性或补丁性的方案
- 不允许过度设计,保持最短路径实现,且不能违反上一条要求
- 不允许自行给出我提供的需求以外的方案,例如一些兜底和降级方案,这可能导致业务逻辑偏移问题
- 必须确保方案的逻辑正确,必须经过全链路的逻辑验证

BIN
bin/.DS_Store vendored Normal file

Binary file not shown.

257
bin/prod/deploy_from_package.sh Executable file
View File

@@ -0,0 +1,257 @@
#!/bin/sh
set -eu
JAVA_BIN="/home/webapp/env/java/bin/java"
BACKEND_PORT=63310
SPRING_PROFILE="uat"
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
BACKEND_DIR="$SCRIPT_DIR/backend"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
BACKEND_JAR_TARGET="$BACKEND_DIR/ruoyi-admin.jar"
BACKEND_PID_FILE="$BACKEND_DIR/backend.pid"
BACKEND_LOG_FILE="$BACKEND_DIR/backend-console.log"
FRONTEND_DIST_ARCHIVE="$FRONTEND_DIR/dist.zip"
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
BACKEND_MARKER="-Dloan.pricing.home=$SCRIPT_DIR"
usage() {
cat <<'EOF'
用法:
./deploy_from_package.sh
EOF
}
timestamp() {
date "+%Y%m%d%H%M%S"
}
log_info() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
}
log_error() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
}
cleanup() {
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
fi
}
require_dir() {
if [ ! -d "$1" ]; then
log_error "缺少目录: $1"
exit 1
fi
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
find_release_archive() {
archives=$(find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip')
count=$(printf '%s\n' "$archives" | sed '/^$/d' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "脚本同目录发布 zip 数量不正确,期望 1 个,实际 $count"
exit 1
fi
printf '%s\n' "$archives"
}
extract_release_package() {
release_archive="$1"
release_extract_dir="$2"
mkdir -p "$release_extract_dir"
unzip -oq "$release_archive" -d "$release_extract_dir"
}
assert_single_jar() {
search_dir="$1"
count=$(find "$search_dir" -type f -name '*.jar' ! -path '*/__MACOSX/*' ! -name '._*' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "后端 jar 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name '*.jar' ! -path '*/__MACOSX/*' ! -name '._*' | head -n 1
}
assert_single_dist_zip() {
search_dir="$1"
count=$(find "$search_dir" -type f -name 'dist.zip' ! -path '*/__MACOSX/*' ! -name '._*' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "前端 dist.zip 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name 'dist.zip' ! -path '*/__MACOSX/*' ! -name '._*' | head -n 1
}
backup_backend_jar() {
if [ -f "$BACKEND_JAR_TARGET" ]; then
mv "$BACKEND_JAR_TARGET" "$BACKEND_JAR_TARGET.$(timestamp).bak"
fi
}
backup_frontend_dist() {
if [ -d "$FRONTEND_DIST_DIR" ]; then
mv "$FRONTEND_DIST_DIR" "$FRONTEND_DIR/dist-$(timestamp)"
fi
}
deploy_backend_jar() {
source_jar="$1"
mv "$source_jar" "$BACKEND_JAR_TARGET"
}
deploy_frontend_dist() {
source_dist_zip="$1"
rm -f "$FRONTEND_DIST_ARCHIVE"
rm -rf "$FRONTEND_DIST_DIR"
mv "$source_dist_zip" "$FRONTEND_DIST_ARCHIVE"
unzip -oq "$FRONTEND_DIST_ARCHIVE" -d "$FRONTEND_DIR"
if [ ! -d "$FRONTEND_DIST_DIR" ]; then
log_error "dist.zip 解压后未找到 $FRONTEND_DIST_DIR"
exit 1
fi
}
collect_backend_pids() {
ps -ef | awk -v marker="$BACKEND_MARKER" -v jar="$BACKEND_JAR_TARGET" '
index($0, "<defunct>") == 0 && index($0, marker) > 0 {
for (i = 1; i < NF; i++) {
if ($i == "-jar" && $(i + 1) == jar) {
print $2
break
}
}
}
' | xargs 2>/dev/null || true
}
stop_backend() {
pids=$(collect_backend_pids)
if [ -z "${pids:-}" ]; then
rm -f "$BACKEND_PID_FILE"
log_info "未发现运行中的后端进程"
return 0
fi
log_info "停止后端进程: $pids"
for pid in $pids; do
kill -TERM "$pid" 2>/dev/null || true
done
elapsed=0
remaining="$pids"
while [ "$elapsed" -lt 30 ]; do
remaining=""
for pid in $pids; do
if kill -0 "$pid" 2>/dev/null; then
remaining="$remaining $pid"
fi
done
remaining=$(echo "$remaining" | xargs 2>/dev/null || true)
if [ -z "${remaining:-}" ]; then
break
fi
sleep 1
elapsed=$((elapsed + 1))
done
if [ -n "${remaining:-}" ]; then
log_info "执行强制停止: $remaining"
for pid in $remaining; do
kill -KILL "$pid" 2>/dev/null || true
done
fi
rm -f "$BACKEND_PID_FILE"
}
start_backend() {
if [ ! -x "$JAVA_BIN" ]; then
log_error "未检测到可执行 Java: $JAVA_BIN"
exit 1
fi
if [ ! -f "$BACKEND_JAR_TARGET" ]; then
log_error "未找到后端 jar: $BACKEND_JAR_TARGET"
exit 1
fi
if [ -n "$(collect_backend_pids)" ]; then
log_error "检测到后端已在运行,请先停止旧进程"
exit 1
fi
printf '\n===== %s deploy =====\n' "$(date '+%Y-%m-%d %H:%M:%S')" >> "$BACKEND_LOG_FILE"
nohup "$JAVA_BIN" $JAVA_OPTS "$BACKEND_MARKER" -jar "$BACKEND_JAR_TARGET" \
--spring.profiles.active="$SPRING_PROFILE" \
--server.port="$BACKEND_PORT" >> "$BACKEND_LOG_FILE" 2>&1 &
backend_pid=$!
printf '%s\n' "$backend_pid" > "$BACKEND_PID_FILE"
sleep 1
if ! kill -0 "$backend_pid" 2>/dev/null; then
log_error "后端启动失败,请检查日志: $BACKEND_LOG_FILE"
exit 1
fi
log_info "后端已启动PID: $backend_pid"
}
main() {
if [ "$#" -ne 0 ]; then
usage
exit 1
fi
require_dir "$BACKEND_DIR"
require_dir "$FRONTEND_DIR"
require_command unzip
require_command find
require_command ps
require_command nohup
release_archive=$(find_release_archive)
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/deploy_from_package.XXXXXX")
trap cleanup EXIT INT TERM
extract_release_package "$release_archive" "$WORK_DIR/package"
backend_jar_source=$(assert_single_jar "$WORK_DIR/package")
frontend_dist_source=$(assert_single_dist_zip "$WORK_DIR/package")
backup_backend_jar
backup_frontend_dist
stop_backend
deploy_backend_jar "$backend_jar_source"
deploy_frontend_dist "$frontend_dist_source"
start_backend
log_info "部署完成"
log_info "后端 jar: $BACKEND_JAR_TARGET"
log_info "前端目录: $FRONTEND_DIST_DIR"
}
main "$@"

View File

@@ -0,0 +1,262 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
SCRIPT_UNDER_TEST="$ROOT_DIR/bin/prod/deploy_from_package.sh"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_file_exists() {
file_path="$1"
[ -e "$file_path" ] || fail "expected file to exist: $file_path"
}
assert_grep() {
pattern="$1"
target="$2"
if ! grep -Eq "$pattern" "$target"; then
fail "expected pattern [$pattern] in $target"
fi
}
create_fake_java() {
fake_java="$1"
cat > "$fake_java" <<'EOF'
#!/bin/sh
set -eu
port=""
for arg in "$@"; do
case "$arg" in
--server.port=*)
port=${arg#--server.port=}
;;
esac
done
if [ -z "$port" ]; then
echo "missing port" >&2
exit 1
fi
while :; do
sleep 1
done
EOF
chmod +x "$fake_java"
}
create_release_zip() {
release_dir="$1"
release_zip_name="$2"
mkdir -p "$release_dir/package/deploy" "$release_dir/package/__MACOSX/deploy"
mkdir -p "$release_dir/package/frontend_payload/dist" "$release_dir/package/frontend_payload/__MACOSX/dist"
printf 'new-jar\n' > "$release_dir/package/deploy/ruoyi-admin.jar"
printf 'macos-meta\n' > "$release_dir/package/__MACOSX/deploy/._ruoyi-admin.jar"
printf '<html>new</html>\n' > "$release_dir/package/frontend_payload/dist/index.html"
printf 'macos-meta\n' > "$release_dir/package/frontend_payload/__MACOSX/dist/._index.html"
(
cd "$release_dir/package/frontend_payload"
zip -qr "$release_dir/package/dist.zip" dist __MACOSX
)
mv "$release_dir/package/dist.zip" "$release_dir/package/deploy/dist.zip"
(
cd "$release_dir/package"
zip -qr "$release_dir/$release_zip_name" deploy __MACOSX
)
}
find_free_port() {
python3 - <<'PY'
import socket
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1])
sock.close()
PY
}
prepare_release_dir() {
release_dir="$1"
backend_port="$2"
mkdir -p "$release_dir/backend" "$release_dir/frontend" "$release_dir/fake-java-bin"
printf 'old-jar\n' > "$release_dir/backend/ruoyi-admin.jar"
mkdir -p "$release_dir/frontend/dist"
printf '<html>old</html>\n' > "$release_dir/frontend/dist/index.html"
create_fake_java "$release_dir/fake-java-bin/java"
create_release_zip "$release_dir" "deploy.zip"
cp "$SCRIPT_UNDER_TEST" "$release_dir/deploy_from_package.sh"
perl -0pi -e "s#JAVA_BIN=\"/home/webapp/env/java/bin/java\"#JAVA_BIN=\"$release_dir/fake-java-bin/java\"#" \
"$release_dir/deploy_from_package.sh"
perl -0pi -e "s/BACKEND_PORT=63310/BACKEND_PORT=$backend_port/" \
"$release_dir/deploy_from_package.sh"
chmod +x "$release_dir/deploy_from_package.sh"
}
cleanup_release_dir() {
release_dir="$1"
if [ -f "$release_dir/backend/backend.pid" ]; then
backend_pid=$(cat "$release_dir/backend/backend.pid" 2>/dev/null || true)
if [ -n "${backend_pid:-}" ]; then
kill "$backend_pid" 2>/dev/null || true
wait "$backend_pid" 2>/dev/null || true
fi
fi
rm -rf "$release_dir"
}
test_deploy_success() {
release_dir=$(mktemp -d)
backend_port=$(find_free_port)
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
prepare_release_dir "$release_dir" "$backend_port"
(
cd "$release_dir"
./deploy_from_package.sh
)
assert_file_exists "$release_dir/backend/ruoyi-admin.jar"
assert_file_exists "$release_dir/frontend/dist.zip"
assert_file_exists "$release_dir/frontend/dist/index.html"
assert_file_exists "$release_dir/backend/backend.pid"
assert_file_exists "$release_dir/backend/backend-console.log"
assert_grep 'new' "$release_dir/frontend/dist/index.html"
backup_jar_count=$(find "$release_dir/backend" -maxdepth 1 -type f -name 'ruoyi-admin.jar.*.bak' | wc -l | tr -d ' ')
[ "$backup_jar_count" -eq 1 ] || fail "expected one backup jar, got $backup_jar_count"
backup_dist_count=$(find "$release_dir/frontend" -maxdepth 1 -type d -name 'dist-*' | wc -l | tr -d ' ')
[ "$backup_dist_count" -eq 1 ] || fail "expected one backup dist dir, got $backup_dist_count"
backend_pid=$(cat "$release_dir/backend/backend.pid")
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend pid to be running"
trap - EXIT INT TERM
cleanup_release_dir "$release_dir"
}
test_multiple_release_zip_should_fail() {
release_dir=$(mktemp -d)
backend_port=$(find_free_port)
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
prepare_release_dir "$release_dir" "$backend_port"
cp "$release_dir/deploy.zip" "$release_dir/deploy-copy.zip"
if (
cd "$release_dir"
./deploy_from_package.sh >/tmp/deploy_from_package_test.stderr 2>&1
); then
fail "expected deploy_from_package.sh to fail when multiple release zips exist"
fi
assert_file_exists /tmp/deploy_from_package_test.stderr
assert_grep '发布 zip 数量不正确' /tmp/deploy_from_package_test.stderr
rm -f /tmp/deploy_from_package_test.stderr
trap - EXIT INT TERM
cleanup_release_dir "$release_dir"
}
test_defunct_process_should_be_ignored() {
release_dir=$(mktemp -d)
backend_port=$(find_free_port)
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
prepare_release_dir "$release_dir" "$backend_port"
mkdir -p "$release_dir/fake-ps-bin"
cat > "$release_dir/fake-ps-bin/ps" <<EOF
#!/bin/sh
if [ "\$1" = "-ef" ]; then
cat <<'PSOUT'
UID PID PPID C STIME TTY TIME CMD
root 99999 1 0 00:00 ? 00:00:00 [java] <defunct> -Dloan.pricing.home=$release_dir -jar $release_dir/backend/ruoyi-admin.jar
PSOUT
exit 0
fi
/bin/ps "\$@"
EOF
chmod +x "$release_dir/fake-ps-bin/ps"
(
cd "$release_dir"
PATH="$release_dir/fake-ps-bin:/usr/bin:/bin" ./deploy_from_package.sh
)
backend_pid=$(cat "$release_dir/backend/backend.pid")
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend pid to be running when defunct process is ignored"
trap - EXIT INT TERM
cleanup_release_dir "$release_dir"
}
test_only_current_project_jar_should_match() {
release_dir=$(mktemp -d)
backend_port=$(find_free_port)
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
prepare_release_dir "$release_dir" "$backend_port"
mkdir -p "$release_dir/fake-ps-bin"
cat > "$release_dir/fake-ps-bin/ps" <<EOF
#!/bin/sh
if [ "\$1" = "-ef" ]; then
cat <<'PSOUT'
UID PID PPID C STIME TTY TIME CMD
root 88888 1 0 00:00 ? 00:00:00 java -Dloan.pricing.home=$release_dir -jar $release_dir/backend/ruoyi-admin.jar.bak --spring.profiles.active=pro
PSOUT
exit 0
fi
/bin/ps "\$@"
EOF
chmod +x "$release_dir/fake-ps-bin/ps"
(
cd "$release_dir"
PATH="$release_dir/fake-ps-bin:/usr/bin:/bin" ./deploy_from_package.sh
)
backend_pid=$(cat "$release_dir/backend/backend.pid")
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend pid to be running when non-target jar process is ignored"
trap - EXIT INT TERM
cleanup_release_dir "$release_dir"
}
test_should_use_ps_ef_for_process_detection() {
if rg -n 'pgrep' "$SCRIPT_UNDER_TEST" >/dev/null 2>&1; then
fail "expected deploy_from_package.sh not to depend on pgrep"
fi
if ! rg -n 'ps -ef' "$SCRIPT_UNDER_TEST" >/dev/null 2>&1; then
fail "expected deploy_from_package.sh to use ps -ef for process detection"
fi
if rg -n '\b(ss|lsof|netstat|resolve_frontend_source_dir|is_port_listening)\b' "$SCRIPT_UNDER_TEST" >/dev/null 2>&1; then
fail "expected deploy_from_package.sh to remove port detection and unzip compatibility helpers"
fi
}
main() {
[ -f "$SCRIPT_UNDER_TEST" ] || fail "script under test not found: $SCRIPT_UNDER_TEST"
test_should_use_ps_ef_for_process_detection
test_deploy_success
test_multiple_release_zip_should_fail
test_defunct_process_should_be_ignored
test_only_current_project_jar_should_match
printf 'PASS: deploy_from_package tests\n'
}
main "$@"

245
bin/prod/deploy_release.sh Executable file
View File

@@ -0,0 +1,245 @@
#!/bin/sh
set -eu
WEBAPP_ROOT="/home/webapp"
ENV_ROOT="$WEBAPP_ROOT/env"
APP_ROOT="$WEBAPP_ROOT/loan-pricing"
JAVA_HOME="$ENV_ROOT/java"
NGINX_HOME="$ENV_ROOT/nginx"
NGINX_CONF="$NGINX_HOME/conf/nginx.conf"
BACKEND_DIR="$APP_ROOT/backend"
FRONTEND_DIR="$APP_ROOT/frontend"
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
BACKUP_DIR="$APP_ROOT/backup"
LOG_DIR="$APP_ROOT/logs"
RUN_DIR="$APP_ROOT/run"
TMP_DIR="$APP_ROOT/tmp"
BACKEND_JAR="$BACKEND_DIR/ruoyi-admin.jar"
FRONTEND_PORT=63311
JAVA_RESTART_SCRIPT="$WEBAPP_ROOT/restart_java.sh"
timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}
log_info() {
printf '[%s] %s\n' "$(timestamp)" "$1"
}
log_error() {
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}
usage() {
cat <<'EOF'
用法:
./bin/prod/deploy_release.sh <发布压缩包路径>
EOF
}
require_root() {
if [ "$(id -u)" -ne 0 ]; then
log_error "请使用 root 用户执行部署脚本"
exit 1
fi
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
ensure_runtime_dirs() {
mkdir -p "$BACKEND_DIR" "$FRONTEND_DIR" "$BACKUP_DIR" "$LOG_DIR" "$RUN_DIR" "$TMP_DIR"
}
cleanup() {
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
fi
}
extract_release_package() {
release_archive="$1"
release_extract_dir="$2"
mkdir -p "$release_extract_dir"
case "$release_archive" in
*.zip)
unzip -oq "$release_archive" -d "$release_extract_dir"
;;
*.tar.gz|*.tgz)
tar -xzf "$release_archive" -C "$release_extract_dir"
;;
*.tar)
tar -xf "$release_archive" -C "$release_extract_dir"
;;
*)
log_error "不支持的发布包格式: $release_archive"
exit 1
;;
esac
}
assert_single_file() {
search_dir="$1"
file_name="$2"
description="$3"
count=$(find "$search_dir" -type f -name "$file_name" | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "$description 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name "$file_name" | head -n 1
}
assert_single_jar() {
search_dir="$1"
count=$(find "$search_dir" -type f -name '*.jar' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "后端 jar 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name '*.jar' | head -n 1
}
backup_current_release() {
backup_stamp=$(date "+%Y%m%d%H%M%S")
CURRENT_BACKUP_DIR="$BACKUP_DIR/$backup_stamp"
mkdir -p "$CURRENT_BACKUP_DIR/backend" "$CURRENT_BACKUP_DIR/frontend"
if [ -f "$BACKEND_JAR" ]; then
cp -a "$BACKEND_JAR" "$CURRENT_BACKUP_DIR/backend/"
fi
if [ -d "$FRONTEND_DIST_DIR" ]; then
cp -a "$FRONTEND_DIST_DIR" "$CURRENT_BACKUP_DIR/frontend/"
fi
log_info "旧版本已备份到: $CURRENT_BACKUP_DIR"
}
deploy_backend() {
source_jar="$1"
rm -f "$BACKEND_DIR"/*.jar
cp "$source_jar" "$BACKEND_JAR"
}
resolve_frontend_source_dir() {
unzip_dir="$1"
if [ -f "$unzip_dir/index.html" ]; then
printf '%s\n' "$unzip_dir"
return 0
fi
if [ -f "$unzip_dir/dist/index.html" ]; then
printf '%s\n' "$unzip_dir/dist"
return 0
fi
candidate=$(find "$unzip_dir" -type f -name 'index.html' | head -n 1)
if [ -z "${candidate:-}" ]; then
log_error "dist.zip 解压后未找到 index.html"
exit 1
fi
dirname "$candidate"
}
deploy_frontend() {
dist_zip="$1"
dist_unpack_dir="$WORK_DIR/frontend"
mkdir -p "$dist_unpack_dir"
unzip -oq "$dist_zip" -d "$dist_unpack_dir"
frontend_source_dir=$(resolve_frontend_source_dir "$dist_unpack_dir")
rm -rf "$FRONTEND_DIST_DIR"
mkdir -p "$FRONTEND_DIST_DIR"
cp -a "$frontend_source_dir"/. "$FRONTEND_DIST_DIR"/
}
reload_nginx() {
nginx_pid_file="$RUN_DIR/nginx.pid"
"$NGINX_HOME/sbin/nginx" -t -c "$NGINX_CONF"
if [ -f "$nginx_pid_file" ]; then
nginx_pid=$(cat "$nginx_pid_file" 2>/dev/null || true)
if [ -n "${nginx_pid:-}" ] && kill -0 "$nginx_pid" 2>/dev/null; then
"$NGINX_HOME/sbin/nginx" -c "$NGINX_CONF" -s reload
log_info "Nginx 已重载,前端端口: $FRONTEND_PORT"
return 0
fi
fi
"$NGINX_HOME/sbin/nginx" -c "$NGINX_CONF"
log_info "Nginx 已启动,前端端口: $FRONTEND_PORT"
}
main() {
if [ "$#" -ne 1 ]; then
usage
exit 1
fi
require_root
require_command tar
require_command unzip
require_command find
release_archive="$1"
if [ ! -f "$release_archive" ]; then
log_error "发布压缩包不存在: $release_archive"
exit 1
fi
if [ ! -x "$JAVA_HOME/bin/java" ]; then
log_error "未检测到 Java请先执行 ./bin/prod/install_env.sh"
exit 1
fi
if [ ! -x "$NGINX_HOME/sbin/nginx" ]; then
log_error "未检测到 Nginx请先执行 ./bin/prod/install_env.sh"
exit 1
fi
if [ ! -x "$JAVA_RESTART_SCRIPT" ]; then
log_error "未检测到 Java 重启脚本: $JAVA_RESTART_SCRIPT"
exit 1
fi
ensure_runtime_dirs
WORK_DIR=$(mktemp -d "$TMP_DIR/release.XXXXXX")
trap cleanup EXIT INT TERM
extract_release_package "$release_archive" "$WORK_DIR/package"
backend_jar_source=$(assert_single_jar "$WORK_DIR/package")
frontend_dist_source=$(assert_single_file "$WORK_DIR/package" 'dist.zip' '前端 dist.zip')
backup_current_release
"$JAVA_RESTART_SCRIPT" stop
deploy_backend "$backend_jar_source"
deploy_frontend "$frontend_dist_source"
"$JAVA_RESTART_SCRIPT" start
reload_nginx
log_info "部署完成"
log_info "后端 jar: $BACKEND_JAR"
log_info "前端目录: $FRONTEND_DIST_DIR"
log_info "备份目录: $CURRENT_BACKUP_DIR"
}
main "$@"

244
bin/prod/install_env.sh Executable file
View File

@@ -0,0 +1,244 @@
#!/bin/sh
set -eu
WEBAPP_ROOT="/home/webapp"
ENV_ROOT="$WEBAPP_ROOT/env"
APP_ROOT="$WEBAPP_ROOT/loan-pricing"
JAVA_HOME="$ENV_ROOT/java"
NGINX_HOME="$ENV_ROOT/nginx"
NGINX_CONF="$NGINX_HOME/conf/nginx.conf"
BACKEND_DIR="$APP_ROOT/backend"
FRONTEND_DIR="$APP_ROOT/frontend"
BACKUP_DIR="$APP_ROOT/backup"
LOG_DIR="$APP_ROOT/logs"
RUN_DIR="$APP_ROOT/run"
TMP_DIR="$APP_ROOT/tmp"
BACKEND_PORT=63310
FRONTEND_PORT=63311
timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}
log_info() {
printf '[%s] %s\n' "$(timestamp)" "$1"
}
log_error() {
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}
require_root() {
if [ "$(id -u)" -ne 0 ]; then
log_error "请使用 root 用户执行安装脚本"
exit 1
fi
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
ensure_base_dirs() {
mkdir -p "$ENV_ROOT" "$BACKEND_DIR" "$FRONTEND_DIR" "$BACKUP_DIR" "$LOG_DIR" "$RUN_DIR" "$TMP_DIR"
}
find_archive() {
search_kind="$1"
found=""
case "$search_kind" in
java)
set -- "$WEBAPP_ROOT/openjdk" "$WEBAPP_ROOT"
patterns='openjdk*.tar.gz openjdk*.tgz jdk*.tar.gz jdk*.tgz'
;;
nginx)
set -- "$WEBAPP_ROOT/nginx" "$ENV_ROOT" "$WEBAPP_ROOT"
patterns='nginx-*.tar.gz nginx-*.tgz'
;;
*)
log_error "未知的安装包类型: $search_kind"
exit 1
;;
esac
for dir in "$@"; do
if [ ! -d "$dir" ]; then
continue
fi
for pattern in $patterns; do
candidate=$(find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | head -n 1)
if [ -n "${candidate:-}" ]; then
found="$candidate"
break
fi
done
if [ -n "${found:-}" ]; then
break
fi
done
if [ -z "${found:-}" ]; then
log_error "未找到 $search_kind 安装包"
exit 1
fi
printf '%s\n' "$found"
}
install_yum_dependencies() {
require_command yum
log_info "安装系统依赖"
yum install -y \
gcc \
make \
pcre \
pcre-devel \
zlib \
zlib-devel \
openssl \
openssl-devel \
tar \
gzip \
unzip \
which \
findutils \
procps-ng \
iproute
}
install_java() {
java_archive="$1"
log_info "安装 Java: $java_archive"
rm -rf "$JAVA_HOME"
mkdir -p "$JAVA_HOME"
tar -xzf "$java_archive" -C "$JAVA_HOME" --strip-components=1
if [ ! -x "$JAVA_HOME/bin/java" ]; then
log_error "Java 安装失败,未找到 $JAVA_HOME/bin/java"
exit 1
fi
"$JAVA_HOME/bin/java" -version >/dev/null 2>&1
}
install_nginx() {
nginx_archive="$1"
build_dir=$(mktemp -d "$ENV_ROOT/nginx-build.XXXXXX")
log_info "编译安装 Nginx: $nginx_archive"
rm -rf "$NGINX_HOME"
mkdir -p "$NGINX_HOME"
tar -xzf "$nginx_archive" -C "$build_dir"
source_dir=$(find "$build_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)
if [ -z "${source_dir:-}" ]; then
rm -rf "$build_dir"
log_error "Nginx 源码目录解析失败"
exit 1
fi
jobs=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
(
cd "$source_dir"
./configure --prefix="$NGINX_HOME" --with-http_ssl_module
make -j"$jobs"
make install
)
rm -rf "$build_dir"
if [ ! -x "$NGINX_HOME/sbin/nginx" ]; then
log_error "Nginx 安装失败,未找到 $NGINX_HOME/sbin/nginx"
exit 1
fi
}
write_nginx_conf() {
log_info "写入 Nginx 配置: $NGINX_CONF"
mkdir -p "$NGINX_HOME/conf" "$NGINX_HOME/logs" "$FRONTEND_DIR/dist"
cat > "$NGINX_CONF" <<EOF
user nobody;
worker_processes 1;
error_log $LOG_DIR/nginx-error.log warn;
pid $RUN_DIR/nginx.pid;
events {
worker_connections 1024;
}
http {
include $NGINX_HOME/conf/mime.types;
default_type application/octet-stream;
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
'\$status \$body_bytes_sent "\$http_referer" '
'"\$http_user_agent" "\$http_x_forwarded_for"';
access_log $LOG_DIR/nginx-access.log main;
sendfile on;
keepalive_timeout 65;
client_max_body_size 100m;
server {
listen $FRONTEND_PORT;
server_name _;
root $FRONTEND_DIR/dist;
index index.html;
location /prod-api/ {
proxy_pass http://127.0.0.1:$BACKEND_PORT/;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
location / {
try_files \$uri \$uri/ /index.html;
}
}
}
EOF
"$NGINX_HOME/sbin/nginx" -t -c "$NGINX_CONF"
}
main() {
require_root
require_command tar
require_command find
ensure_base_dirs
install_yum_dependencies
java_archive=$(find_archive java)
nginx_archive=$(find_archive nginx)
log_info "检测到 Java 安装包: $java_archive"
log_info "检测到 Nginx 安装包: $nginx_archive"
install_java "$java_archive"
install_nginx "$nginx_archive"
write_nginx_conf
log_info "环境安装完成"
log_info "JAVA_HOME=$JAVA_HOME"
log_info "NGINX_HOME=$NGINX_HOME"
log_info "前端端口=$FRONTEND_PORT,后端端口=$BACKEND_PORT"
}
main "$@"

208
bin/prod/restart_java.sh Executable file
View File

@@ -0,0 +1,208 @@
#!/bin/sh
set -eu
WEBAPP_ROOT="/home/webapp"
ENV_ROOT="$WEBAPP_ROOT/env"
APP_ROOT="$WEBAPP_ROOT/loan-pricing"
JAVA_HOME="$ENV_ROOT/jdk"
BACKEND_DIR="$APP_ROOT/backend"
LOG_DIR="$APP_ROOT/logs"
RUN_DIR="$APP_ROOT/run"
BACKEND_PID_FILE="$RUN_DIR/backend.pid"
BACKEND_JAR="$BACKEND_DIR/ruoyi-admin.jar"
BACKEND_CONSOLE_LOG="$LOG_DIR/backend-console.log"
BACKEND_PORT=63310
BACKEND_MARKER="-Dloan.pricing.home=$APP_ROOT"
JAVA_OPTS="$BACKEND_MARKER -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}
log_info() {
printf '[%s] %s\n' "$(timestamp)" "$1"
}
log_error() {
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}
usage() {
cat <<'EOF'
用法: ./restart_java.sh [start|stop|restart|status]
默认动作:
restart 重启后端 Java 进程
EOF
}
ensure_runtime_dirs() {
mkdir -p "$BACKEND_DIR" "$LOG_DIR" "$RUN_DIR"
}
is_managed_backend_pid() {
pid="$1"
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
args=$(ps -o args= -p "$pid" 2>/dev/null || true)
if [ -z "${args:-}" ]; then
return 1
fi
case "$args" in
*"$BACKEND_MARKER"*"$BACKEND_JAR"*|*"$BACKEND_JAR"*"$BACKEND_MARKER"*)
return 0
;;
esac
return 1
}
collect_backend_pids() {
pids=""
if [ -f "$BACKEND_PID_FILE" ]; then
file_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true)
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
pids="$pids $file_pid"
fi
fi
marker_pids=$(ps -ef | awk -v marker="$BACKEND_MARKER" -v jar="$BACKEND_JAR" '
index($0, "<defunct>") == 0 && index($0, marker) > 0 {
for (i = 1; i < NF; i++) {
if ($i == "-jar" && $(i + 1) == jar) {
print $2
break
}
}
}
' | xargs 2>/dev/null || true)
if [ -n "${marker_pids:-}" ]; then
for pid in $marker_pids; do
if is_managed_backend_pid "$pid"; then
pids="$pids $pid"
fi
done
fi
printf '%s\n' "$(echo "$pids" | xargs 2>/dev/null || true)"
}
stop_backend() {
pids=$(collect_backend_pids)
if [ -z "${pids:-}" ]; then
rm -f "$BACKEND_PID_FILE"
log_info "未发现运行中的后端进程"
return 0
fi
log_info "停止后端进程: $pids"
for pid in $pids; do
kill -TERM "$pid" 2>/dev/null || true
done
elapsed=0
while [ "$elapsed" -lt 30 ]; do
remaining=""
for pid in $pids; do
if kill -0 "$pid" 2>/dev/null; then
remaining="$remaining $pid"
fi
done
remaining=$(echo "$remaining" | xargs 2>/dev/null || true)
if [ -z "${remaining:-}" ]; then
break
fi
sleep 1
elapsed=$((elapsed + 1))
done
if [ -n "${remaining:-}" ]; then
log_info "执行强制停止: $remaining"
for pid in $remaining; do
kill -KILL "$pid" 2>/dev/null || true
done
fi
rm -f "$BACKEND_PID_FILE"
}
start_backend() {
ensure_runtime_dirs
if [ ! -x "$JAVA_HOME/bin/java" ]; then
log_error "未检测到可执行 Java: $JAVA_HOME/bin/java"
exit 1
fi
if [ ! -f "$BACKEND_JAR" ]; then
log_error "未找到后端 jar: $BACKEND_JAR"
exit 1
fi
if [ -n "$(collect_backend_pids)" ]; then
log_error "检测到后端已在运行,请先执行 stop 或 restart"
exit 1
fi
printf '\n===== %s restart =====\n' "$(timestamp)" >> "$BACKEND_CONSOLE_LOG"
(
cd "$BACKEND_DIR"
nohup "$JAVA_HOME/bin/java" $JAVA_OPTS -jar "$BACKEND_JAR" --spring.profiles.active=pro --server.port="$BACKEND_PORT" >> "$BACKEND_CONSOLE_LOG" 2>&1 &
echo $! > "$BACKEND_PID_FILE"
)
sleep 3
backend_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true)
if [ -z "${backend_pid:-}" ] || ! kill -0 "$backend_pid" 2>/dev/null; then
log_error "后端启动失败,请检查日志: $BACKEND_CONSOLE_LOG"
exit 1
fi
log_info "后端已启动PID: $backend_pid"
}
status_backend() {
pids=$(collect_backend_pids)
if [ -n "${pids:-}" ]; then
log_info "后端正在运行,进程: $pids"
return 0
fi
log_info "后端未运行"
}
main() {
action="${1:-restart}"
case "$action" in
start)
start_backend
;;
stop)
stop_backend
;;
restart)
stop_backend
start_backend
;;
status)
status_backend
;;
*)
usage
exit 1
;;
esac
}
main "$@"

View File

@@ -0,0 +1,116 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
SCRIPT_UNDER_TEST="$ROOT_DIR/bin/prod/restart_java.sh"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_grep() {
pattern="$1"
target="$2"
if ! grep -Eq -- "$pattern" "$target"; then
fail "expected pattern [$pattern] in $target"
fi
}
assert_not_grep() {
pattern="$1"
target="$2"
if grep -Eq -- "$pattern" "$target"; then
fail "did not expect pattern [$pattern] in $target"
fi
}
create_fake_java() {
fake_java="$1"
cat > "$fake_java" <<'EOF'
#!/bin/sh
set -eu
while :; do
sleep 1
done
EOF
chmod +x "$fake_java"
}
prepare_script_env() {
work_dir="$1"
mkdir -p "$work_dir/env/jdk/bin" "$work_dir/loan-pricing/backend" "$work_dir/loan-pricing/logs" "$work_dir/loan-pricing/run"
create_fake_java "$work_dir/env/jdk/bin/java"
printf 'fake-jar\n' > "$work_dir/loan-pricing/backend/ruoyi-admin.jar"
cp "$SCRIPT_UNDER_TEST" "$work_dir/restart_java.sh"
perl -0pi -e "s#WEBAPP_ROOT=\"/home/webapp\"#WEBAPP_ROOT=\"$work_dir\"#" "$work_dir/restart_java.sh"
chmod +x "$work_dir/restart_java.sh"
}
cleanup_work_dir() {
work_dir="$1"
if [ -f "$work_dir/loan-pricing/run/backend.pid" ]; then
backend_pid=$(cat "$work_dir/loan-pricing/run/backend.pid" 2>/dev/null || true)
if [ -n "${backend_pid:-}" ]; then
kill "$backend_pid" 2>/dev/null || true
wait "$backend_pid" 2>/dev/null || true
fi
fi
rm -rf "$work_dir"
}
test_script_contract() {
assert_grep 'JAVA_HOME="\$ENV_ROOT/jdk"' "$SCRIPT_UNDER_TEST"
assert_grep '--spring\.profiles\.active=pro' "$SCRIPT_UNDER_TEST"
assert_grep 'ps -ef' "$SCRIPT_UNDER_TEST"
assert_not_grep 'pgrep' "$SCRIPT_UNDER_TEST"
assert_not_grep 'mvn' "$SCRIPT_UNDER_TEST"
assert_not_grep 'require_root' "$SCRIPT_UNDER_TEST"
assert_not_grep '\b(ss|lsof|netstat)\b' "$SCRIPT_UNDER_TEST"
}
test_restart_flow() {
work_dir=$(mktemp -d)
trap 'cleanup_work_dir "$work_dir"' EXIT INT TERM
prepare_script_env "$work_dir"
"$work_dir/restart_java.sh" start
if [ ! -f "$work_dir/loan-pricing/run/backend.pid" ]; then
fail "expected backend pid file after start"
fi
backend_pid=$(cat "$work_dir/loan-pricing/run/backend.pid")
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend process to be running after start"
status_output=$("$work_dir/restart_java.sh" status 2>&1 || true)
printf '%s\n' "$status_output" | grep -q '后端正在运行' || fail "expected status output to show running"
"$work_dir/restart_java.sh" restart
restarted_pid=$(cat "$work_dir/loan-pricing/run/backend.pid")
kill -0 "$restarted_pid" 2>/dev/null || fail "expected backend process to be running after restart"
"$work_dir/restart_java.sh" stop
if [ -f "$work_dir/loan-pricing/run/backend.pid" ]; then
fail "expected backend pid file to be removed after stop"
fi
trap - EXIT INT TERM
cleanup_work_dir "$work_dir"
}
main() {
[ -f "$SCRIPT_UNDER_TEST" ] || fail "script under test not found: $SCRIPT_UNDER_TEST"
test_script_contract
test_restart_flow
printf 'PASS: restart_java tests\n'
}
main "$@"

View File

@@ -9,10 +9,10 @@ CONSOLE_LOG="$LOG_DIR/backend-console.log"
PID_FILE="$LOG_DIR/backend-java.pid"
TARGET_DIR="$ROOT_DIR/ruoyi-admin/target"
JAR_NAME="ruoyi-admin.jar"
SERVER_PORT=8080
SERVER_PORT=63310
STOP_WAIT_SECONDS=30
APP_KEYWORD="$JAR_NAME"
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
APP_MARKER="-Dccdi.backend.root=$ROOT_DIR"
JAVA_OPTS="$APP_MARKER -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
timestamp() {
date "+%Y-%m-%d %H:%M:%S"
@@ -42,24 +42,63 @@ ensure_command() {
fi
}
is_managed_backend_pid() {
pid="$1"
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
args=$(ps -o args= -p "$pid" 2>/dev/null || true)
if [ -z "${args:-}" ]; then
return 1
fi
case "$args" in
*"$APP_MARKER"*"$JAR_NAME"*|*"$JAR_NAME"*"$APP_MARKER"*)
return 0
;;
esac
if [ -f "$PID_FILE" ]; then
file_pid=$(cat "$PID_FILE" 2>/dev/null || true)
if [ "${file_pid:-}" = "$pid" ]; then
case "$args" in
*"java"*"-jar"*"$JAR_NAME"*)
return 0
;;
esac
fi
fi
return 1
}
collect_pids() {
all_pids=""
if [ -f "$PID_FILE" ]; then
file_pid=$(cat "$PID_FILE" 2>/dev/null || true)
if [ -n "${file_pid:-}" ] && kill -0 "$file_pid" 2>/dev/null; then
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
all_pids="$all_pids $file_pid"
fi
fi
port_pids=$(lsof -tiTCP:"$SERVER_PORT" -sTCP:LISTEN 2>/dev/null || true)
if [ -n "${port_pids:-}" ]; then
all_pids="$all_pids $port_pids"
fi
app_pids=$(pgrep -f "$APP_KEYWORD" 2>/dev/null || true)
if [ -n "${app_pids:-}" ]; then
all_pids="$all_pids $app_pids"
marker_pids=$(ps -ef | awk -v marker="$APP_MARKER" -v jar="$JAR_NAME" '
index($0, "<defunct>") == 0 && index($0, marker) > 0 {
for (i = 1; i < NF; i++) {
if ($i == "-jar" && $(i + 1) == jar) {
print $2
break
}
}
}
' | xargs 2>/dev/null || true)
if [ -n "${marker_pids:-}" ]; then
for pid in $marker_pids; do
if is_managed_backend_pid "$pid"; then
all_pids="$all_pids $pid"
fi
done
fi
unique_pids=""
@@ -155,6 +194,12 @@ status_backend() {
pids=$(collect_pids)
if [ -n "${pids:-}" ]; then
log_info "后端正在运行,进程: $pids"
return 0
fi
port_pids=$(lsof -tiTCP:"$SERVER_PORT" -sTCP:LISTEN 2>/dev/null || true)
if [ -n "${port_pids:-}" ]; then
log_info "未发现脚本托管的后端进程,但端口 $SERVER_PORT 被其他进程占用: $port_pids"
else
log_info "后端未运行"
fi
@@ -189,7 +234,7 @@ restart_action() {
main() {
ensure_command mvn
ensure_command lsof
ensure_command pgrep
ensure_command ps
ensure_command tail
action="${1:-restart}"

View File

@@ -0,0 +1,42 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
SCRIPT_UNDER_TEST="$ROOT_DIR/bin/restart_java_backend.sh"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_grep() {
pattern="$1"
target="$2"
if ! grep -Eq -- "$pattern" "$target"; then
fail "expected pattern [$pattern] in $target"
fi
}
assert_not_grep() {
pattern="$1"
target="$2"
if grep -Eq -- "$pattern" "$target"; then
fail "did not expect pattern [$pattern] in $target"
fi
}
test_script_contract() {
assert_grep 'ps -ef' "$SCRIPT_UNDER_TEST"
assert_not_grep 'pgrep' "$SCRIPT_UNDER_TEST"
assert_grep 'APP_MARKER=' "$SCRIPT_UNDER_TEST"
assert_grep 'status_backend\(\)' "$SCRIPT_UNDER_TEST"
}
main() {
[ -f "$SCRIPT_UNDER_TEST" ] || fail "script under test not found: $SCRIPT_UNDER_TEST"
test_script_contract
printf 'PASS: restart_java_backend tests\n'
}
main "$@"

View File

@@ -0,0 +1,190 @@
# 本地安装 Nginx 和 Java 手册
## 适用范围
本手册适用于需要在本地 Linux 环境手动安装贷款定价系统运行环境的场景,安装结果与当前生产脚本约定保持一致:
- Java 安装到 `/home/webapp/env/java`
- Nginx 安装到 `/home/webapp/env/nginx`
- 项目部署目录使用 `/home/webapp/loan-pricing`
- 后端服务端口固定为 `63310`
- 前端 Nginx 端口固定为 `63311`
## 前置条件
安装前请先确认:
- 当前用户具备 `root` 权限
- 本机已配置可用的 `yum`
- `/home/webapp` 目录已存在
- `/home/webapp` 下已准备安装包:
- `openjdk-21.0.2_linux-aarch64_bin.tar.gz`
- `nginx-1.20.2.tar.gz`
如果安装包文件名不同,只要仍是 Java 的 `tar.gz` 包和 Nginx 的源码 `tar.gz` 包,也可以使用同样步骤。
## 目录规划
安装完成后目录结构如下:
```text
/home/webapp
├── env
│ ├── java
│ └── nginx
└── loan-pricing
├── backend
├── frontend
├── backup
├── logs
├── run
└── tmp
```
## 第一步:安装系统依赖
执行以下命令安装编译 Nginx 和运行部署脚本所需依赖:
```sh
yum install -y \
gcc \
make \
pcre \
pcre-devel \
zlib \
zlib-devel \
openssl \
openssl-devel \
tar \
gzip \
unzip \
which \
findutils \
procps-ng \
iproute
```
## 第二步:创建目录
执行以下命令初始化目录:
```sh
mkdir -p \
/home/webapp/env \
/home/webapp/loan-pricing/backend \
/home/webapp/loan-pricing/frontend \
/home/webapp/loan-pricing/backup \
/home/webapp/loan-pricing/logs \
/home/webapp/loan-pricing/run \
/home/webapp/loan-pricing/tmp
```
## 第三步:安装 Java
解压 Java 安装包到目标目录:
```sh
rm -rf /home/webapp/env/java
mkdir -p /home/webapp/env/java
tar -xzf /home/webapp/openjdk-21.0.2_linux-aarch64_bin.tar.gz -C /home/webapp/env/java --strip-components=1
```
验证安装结果:
```sh
/home/webapp/env/java/bin/java -version
```
如果能正常输出 Java 版本,说明安装成功。
## 第四步:安装 Nginx
Nginx 安装包为源码包,需要先解压、编译、安装:
```sh
rm -rf /home/webapp/env/nginx
mkdir -p /home/webapp/env/nginx
mkdir -p /home/webapp/env/nginx-build
tar -xzf /home/webapp/nginx-1.20.2.tar.gz -C /home/webapp/env/nginx-build
cd /home/webapp/env/nginx-build/nginx-1.20.2
./configure --prefix=/home/webapp/env/nginx --with-http_ssl_module
make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)"
make install
```
安装完成后可执行文件位置为:
```text
/home/webapp/env/nginx/sbin/nginx
```
## 第五步:写入 Nginx 配置
仓库已提供可直接参考的配置文件:
```text
deploy/nginx.conf
```
将该文件内容写入 `/home/webapp/env/nginx/conf/nginx.conf` 即可。
## 第六步:校验 Nginx 配置
执行:
```sh
/home/webapp/env/nginx/sbin/nginx -t -c /home/webapp/env/nginx/conf/nginx.conf
```
如果输出 `syntax is ok``test is successful`,说明配置可用。
## 第七步:启动 Nginx
执行:
```sh
/home/webapp/env/nginx/sbin/nginx -c /home/webapp/env/nginx/conf/nginx.conf
```
如果后续修改了配置,可执行:
```sh
/home/webapp/env/nginx/sbin/nginx -c /home/webapp/env/nginx/conf/nginx.conf -s reload
```
## 第八步:验证端口
执行:
```sh
ss -lnt | grep 63311
```
如果能看到 `63311` 监听记录,说明前端 Nginx 已启动成功。
## 建议执行方式
如果本机已经放置了以下脚本,也可以直接使用脚本完成安装:
```sh
cd /home/webapp
./install_env.sh
```
如果只需要管理后端 Java 进程,可执行:
```sh
cd /home/webapp
./restart_java.sh start
./restart_java.sh stop
./restart_java.sh restart
./restart_java.sh status
```
## 常见检查项
- `yum` 不可用:先确认系统已配置可用的 `yum`
- Java 未安装成功:检查 `/home/webapp/openjdk-*.tar.gz` 是否存在且未损坏
- Nginx 编译失败:检查 `gcc``make``pcre-devel``zlib-devel``openssl-devel` 是否已安装
- Nginx 启动失败:先执行 `nginx -t` 查看配置是否正确
- 前端无法访问后端:检查本机 `63310` 端口是否已有 Java 服务监听

BIN
deploy/deploy.zip Normal file

Binary file not shown.

45
deploy/nginx.conf Normal file
View File

@@ -0,0 +1,45 @@
user nobody;
worker_processes 1;
error_log /home/webapp/loan-pricing/logs/nginx-error.log warn;
pid /home/webapp/loan-pricing/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /home/webapp/env/nginx/conf/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /home/webapp/loan-pricing/logs/nginx-access.log main;
sendfile on;
keepalive_timeout 65;
client_max_body_size 100m;
server {
listen 63311;
server_name _;
root /home/webapp/loan-pricing/frontend/dist;
index index.html;
location /prod-api/ {
proxy_pass http://127.0.0.1:63310/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
}
}

View File

@@ -0,0 +1,341 @@
# 贷款定价流程客户敏感信息加密改造设计文档
## 1. 背景
贷款定价流程当前对客户名称 `custName`、证件号码 `idNum` 采用明文传输、明文存储、明文展示的方式处理,不满足客户敏感信息安全要求。
本次需求已经明确限定为:
- 仅覆盖贷款定价流程主链
- 敏感字段仅包含 `custName``idNum`
- `custIsn` 不属于本次敏感字段范围
- 不改造传输层字段加密
- 仅要求存储加密、展示脱敏
- 页面不提供任何明文查看入口
- 流程查询改为仅允许通过客户内码 `custIsn` 查询
- 现有存量数据不迁移,直接清空历史流程数据
- 加密密钥从后端配置文件读取,按当前项目最短路径落地
## 2. 已确认约束
- 仅修改贷款定价流程相关前后端与数据库脚本,不扩散到系统其他模块
- 不覆盖模型输出表的存储加密治理
- 详情页模型输出“基本信息”中的 `custName``idNum` 必须纳入展示脱敏范围
- 不新增前端字段级加密协议
- 不引入外部密钥管理系统
- 不新增兼容性字段、补丁式逻辑或降级分支
- 必须保证新流程数据落库为密文
- 必须保证列表页、详情页始终只展示脱敏值
- 必须保证模型调用链路仍能拿到业务所需明文
## 3. 现状分析
当前贷款定价流程主链如下:
1. 前端个人/企业建单弹窗提交明文 `custName``idNum`
2. 后端 `LoanPricingWorkflowServiceImpl` 通过 `LoanPricingConverter` 将 DTO 转为 `LoanPricingWorkflow`
3. `loan_pricing_workflow.cust_name``loan_pricing_workflow.id_num` 直接保存明文
4. 列表页查询 SQL 直接查询 `lpw.cust_name`
5. 详情页接口直接返回流程实体中的 `custName``idNum`
6. `LoanPricingModelService` 从流程表读取数据后直接组装模型调用 DTO
现状问题有三类:
1. 存储层风险:数据库中存在明文客户名称和证件号码
2. 展示层风险:前端列表页、详情页直接展示敏感明文
3. 查询链路风险:当前列表页仍允许按客户名称查询,与密文存储目标冲突
## 4. 方案对比
### 方案一:应用层统一 AES 加解密,返回前统一脱敏
做法:
- 在后端新增贷款定价专用敏感字段加解密组件
- 创建流程时对 `custName``idNum` 加密后再落库
- 查询详情、模型调用前在服务内部解密
- 返回前端前统一转成脱敏值
- 前端仅负责调整查询条件和展示,不做加解密
优点:
- 改动集中,符合最短路径实现
- 与数据库实现解耦,不绑定数据库方言
- 业务边界清晰,易于控制哪些链路允许拿明文
- 便于测试和排查
缺点:
- 需要明确服务内部解密和对外脱敏的边界,避免遗漏
### 方案二MyBatis TypeHandler 自动加解密
做法:
- 为实体敏感字段挂载统一的 TypeHandler
- 插入自动加密、查询自动解密
- 返回前再补充脱敏
优点:
- 业务代码表面改动更少
缺点:
- 链路不直观,联表 SQL、VO 查询、模型调用等位置容易出现加解密边界不清
- 调试复杂度高,不符合本次最短路径目标
### 方案三:数据库函数处理加解密
做法:
- 在 SQL 中直接调用数据库加解密函数
- 应用层只负责脱敏
优点:
- 应用层代码改动相对少
缺点:
- 强依赖数据库能力
- 维护成本高
- 联表与分页查询复杂度上升
- 不符合本次直接、清晰的实现要求
## 5. 设计结论
采用方案一:应用层统一 AES 加解密,返回前统一脱敏。
最终设计原则如下:
- `loan_pricing_workflow.cust_name``loan_pricing_workflow.id_num` 仅保存密文
- 服务内部按需短暂解密,仅供业务处理和模型调用使用
- 面向前端返回时,`custName``idNum` 永远转换为脱敏值
- 列表查询去掉客户名称条件,只保留客户内码等非敏感查询项
- 存量流程数据通过清空历史数据处理,不做迁移兼容
## 6. 架构设计
本次在贷款定价流程模块内新增两类职责:
### 6.1 敏感字段加解密服务
新增贷款定价专用组件 `SensitiveFieldCryptoService`,负责:
- 从后端配置文件读取 AES 密钥
-`custName``idNum` 执行加密
- 对已落库密文执行解密
- 在密钥缺失或解密失败时抛出明确异常
该组件只处理加密和解密,不参与脱敏展示逻辑。
### 6.2 敏感字段展示服务
新增贷款定价专用组件 `LoanPricingSensitiveDisplayService`,负责:
- 对客户名称进行脱敏
- 对证件号码进行脱敏
- 对流程详情对象和列表对象中的敏感字段做统一替换
该组件不依赖当前系统管理员免脱敏逻辑,严格执行全员脱敏规则。
## 7. 数据链路设计
### 7.1 创建流程链路
1. 前端建单弹窗提交明文 `custName``idNum`
2. 后端 DTO 转实体后,在 `createLoanPricing` 入库前统一加密
3. `loan_pricing_workflow` 表保存密文
4. 创建成功后后续流程继续使用同一主记录
### 7.2 列表查询链路
1. 前端列表页移除客户名称搜索项,新增或保留客户内码查询
2. 后端分页查询不再按 `custName` 过滤
3. 列表 SQL 仍查询 `cust_name` 字段,但查询结果为密文
4. 服务返回前将列表 VO 中 `custName` 转为脱敏值
5. 前端直接展示后端返回的脱敏结果
### 7.3 详情查询链路
1. 后端根据流水号查询流程记录,拿到密文 `custName``idNum`
2. 服务内部先解密得到业务所需明文
3. 若需要组装详情对象或进行后续处理,使用解密后的值
4. 返回前调用展示服务,将 `custName``idNum` 替换为脱敏值
5. 前端详情页只展示脱敏内容
### 7.4 模型调用链路
1. `LoanPricingModelService` 根据流程主键读取流程记录
2. 读取到的 `custName``idNum` 为密文
3. 调用模型前先在服务内部解密
4. 将解密后的明文复制到 `ModelInvokeDTO`
5. 模型调用完成后,模型输出链路保持现状,不纳入本次改造
### 7.5 模型输出展示链路
1. 详情接口查询到 `ModelRetailOutputFields``ModelCorpOutputFields`
2. 模型输出实体中的 `custName``idNum` 保持当前存储方式,不做表级加密改造
3. 在详情接口返回前,对模型输出“基本信息”中的 `custName``idNum` 做统一脱敏
4. 前端 `ModelOutputDisplay` 组件直接展示后端返回的脱敏值
5. 不提供模型输出基本信息的明文查看入口
## 8. 后端改造设计
### 8.1 配置项
后端新增贷款定价敏感字段加密配置项,例如:
- 是否启用敏感字段加解密
- AES 密钥
本次仅要求配置文件读取固定密钥,不扩展到数据库参数表或外部密钥系统。
### 8.2 服务层改造
需要修改以下关键点:
1. `LoanPricingWorkflowServiceImpl#createLoanPricing`
`loanPricingWorkflowMapper.insert` 前统一加密 `custName``idNum`
2. `LoanPricingWorkflowServiceImpl#selectLoanPricingBySerialNum`
查询详情后先解密,再在返回前脱敏
3. `LoanPricingWorkflowServiceImpl#selectLoanPricingPage`
分页结果中的 `custName` 统一转为脱敏值
4. `LoanPricingWorkflowServiceImpl#buildQueryWrapper`
删除 `custName` 查询条件,改为仅支持 `custIsn`、创建者、机构号等非敏感字段
5. `LoanPricingModelService#invokeModelAsync`
模型调用前解密 `custName``idNum`,确保模型收到明文业务数据
6. `LoanPricingWorkflowServiceImpl#selectLoanPricingBySerialNum`
在组装 `ModelRetailOutputFields``ModelCorpOutputFields` 返回值时,对其 `custName``idNum` 进行脱敏替换
### 8.3 对象边界
本次不新增明文返回字段,也不保留“密文字段 + 展示字段”双轨结构,避免对象语义膨胀。
返回前对象中的敏感字段直接替换为脱敏值,确保控制器和前端都不会拿到明文。
## 9. 前端改造设计
### 9.1 列表页
修改 `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- 移除“客户名称”查询项
- 改为支持客户内码查询
- 表格中 `custName` 继续展示,但其值来自后端脱敏结果
### 9.2 详情页
修改个人与企业详情组件:
- 保持字段布局不变
- `custName``idNum` 直接展示后端返回的脱敏值
- 不新增“查看明文”“复制原值”等交互入口
- 模型输出组件 `ModelOutputDisplay.vue` 的“基本信息”页签继续直接消费后端字段,但要求后端返回值已完成脱敏
### 9.3 建单页
个人和企业建单弹窗仍然录入明文 `custName``idNum`,不新增前端字段加密逻辑。
## 10. 数据库处理设计
本次数据库处理遵循最短路径:
1. 不修改 `loan_pricing_workflow` 表结构
2. 不新增密文字段、副本字段或检索字段
3. 实施前执行历史流程数据清空脚本
4. 清空范围仅限贷款定价流程相关存量数据
因为 `custName``idNum` 不再承担查询职责,所以现有字段直接存密文即可。
## 11. 错误处理设计
本次不做兼容性补丁逻辑,错误直接失败:
1. 加密配置缺失
创建流程直接失败,不允许明文落库
2. 解密失败
详情查询失败,模型调用失败,并记录明确错误日志
3. 历史脏数据
通过清空存量数据消除,不增加“密文/明文混读”兼容判断
4. 前端展示
前端只消费后端结果,不承担兜底脱敏职责
## 12. 测试与验收设计
### 12.1 后端验收
1. 创建个人贷款定价流程,校验数据库 `cust_name``id_num` 为密文
2. 创建企业贷款定价流程,校验数据库 `cust_name``id_num` 为密文
3. 列表查询仅支持通过 `custIsn` 命中
4. 列表返回中的 `custName` 为脱敏值
5. 详情返回中的 `custName``idNum` 为脱敏值
6. 模型输出“基本信息”中的 `custName``idNum` 返回为脱敏值
7. 模型调用链路成功,证明服务内部解密逻辑成立
8. 配置缺失时创建流程失败,确认不会明文入库
### 12.2 前端验收
1. 列表页查询项已移除客户名称,改为客户内码
2. 列表页客户名称展示为脱敏值
3. 个人详情页客户名称、证件号码展示为脱敏值
4. 企业详情页客户名称、证件号码展示为脱敏值
5. 个人模型输出“基本信息”页签中的客户名称、证件号码展示为脱敏值
6. 企业模型输出“基本信息”页签中的客户名称、证件号码展示为脱敏值
7. 创建流程、查看详情、设定执行利率等既有功能不受影响
## 13. 风险与控制
### 风险一:模型调用读取到密文
控制方式:
-`LoanPricingModelService` 调用模型前显式解密
- 用测试覆盖模型调用前数据组装逻辑
### 风险二:返回链路遗漏脱敏
控制方式:
- 统一在服务层返回前调用展示服务
- 列表 VO 与详情 VO 都纳入测试覆盖
### 风险三:历史明文和新密文混杂
控制方式:
- 实施前清空贷款定价流程历史数据
- 不保留兼容读取分支
## 14. 范围与非目标
本次包含:
- 贷款定价流程建单入库加密
- 贷款定价流程列表和详情展示脱敏
- 贷款定价流程详情页中的模型输出“基本信息”展示脱敏
- 贷款定价流程查询条件收口为客户内码
- 贷款定价流程存量数据清空处理
本次不包含:
- 模型输出表存储加密改造
- 系统其他模块的敏感字段改造
- 前后端传输层字段加密
- 密钥托管平台接入
- 基于角色的明文查看权限
## 15. 设计结论
本次采用“应用层统一 AES 加解密 + 返回前统一脱敏”的方式,对贷款定价流程中的 `custName``idNum` 完成存储加密和展示脱敏改造。
该方案满足当前客户安全要求,并保持实现路径最短、责任边界清晰、业务链路闭环完整。

View File

@@ -0,0 +1,84 @@
# 个人模型详情缺失展示字段补齐后端实施计划
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 补齐个人模型输出对象与零售模型输出表结构的新增字段,确保个人详情接口可承载并持久化最新展示字段。
**Architecture:** 后端补齐 `ModelRetailOutputFields` 实体字段,并为 `model_retail_output_fields` 表增加 5 个对应列。保持现有控制器、服务与 Mapper 链路不变,让模型返回字段按既有流程直接反序列化、入库并回查。
**Tech Stack:** Java 17、Spring Boot、MyBatis Plus、Maven、JUnit 5
---
### Task 1: 通过测试锁定缺失字段
**Files:**
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java`
- [ ] **Step 1: 编写实体字段断言测试**
新增测试,断言 `ModelRetailOutputFields` 包含以下字段:
```java
"loanRateHistory",
"minRateProduct",
"smoothRange",
"finalCalculateRate",
"referenceRate"
```
- [ ] **Step 2: 运行测试并确认先失败**
Run: `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest test`
Expected: FAIL提示缺少新增字段。
### Task 2: 补齐个人模型输出实体字段
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
- [ ] **Step 1: 在实体中新增 5 个字段**
新增以下字段:
- `loanRateHistory`
- `minRateProduct`
- `smoothRange`
- `finalCalculateRate`
- `referenceRate`
- [ ] **Step 2: 保持现有命名与注释风格一致**
新增字段使用与现有实体一致的 `private String` 定义和中文注释,不引入额外注解。
- [ ] **Step 3: 重新运行测试确认通过**
Run: `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest test`
Expected: PASS
### Task 3: 补齐零售模型输出表结构
**Files:**
- Create: `sql/add_model_retail_output_rate_fields_20260403.sql`
- Modify: `sql/model_retail.sql`
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 1: 新增数据库迁移脚本**
在迁移脚本中为 `model_retail_output_fields` 增加以下列:
- `loan_rate_history`
- `min_rate_product`
- `smooth_range`
- `final_calculate_rate`
- `reference_rate`
- [ ] **Step 2: 同步更新建表基线 SQL**
将相同列同步到仓库中的零售模型输出表建表脚本,避免新环境继续缺列。
- [ ] **Step 3: 对开发库执行迁移并验证列存在**
Run: `mysql ... < sql/add_model_retail_output_rate_fields_20260403.sql`
Expected: 迁移执行成功,`SHOW COLUMNS FROM model_retail_output_fields` 可看到新增 5 列

View File

@@ -0,0 +1,113 @@
# 个人模型详情缺失展示字段补齐设计文档
## 1. 背景
个人模型接口返回字段有更新。根据 `doc/上虞对私利率测算_上传字段与展示字段.xlsx``展示指标` sheet当前个人详情页仍缺少部分应展示字段需要补齐页面展示并保证接口链路字段完整。
## 2. 已确认范围
- 仅处理个人客户详情页
- 仅补齐 `展示指标` sheet 中当前缺失的 6 个字段
- 不调整企业客户页面
- 不新增兼容逻辑、兜底逻辑或额外展示区域
- 保持现有页面结构和分组,按最短路径补齐
## 3. 缺失字段
经核对,当前页面缺少以下字段:
- `loanTerm` 借款期限
- `loanRateHistory` 历史利率
- `minRateProduct` 产品最低利率下限
- `smoothRange` 平滑幅度
- `finalCalculateRate` 最终测算利率
- `referenceRate` 参考利率
其中:
- `loanTerm` 属于流程明细字段,来自 `LoanPricingWorkflow`
- 其余 5 个字段属于个人模型输出字段,来自 `ModelRetailOutputFields`
- 其余 5 个字段同时要求 `model_retail_output_fields` 表具备对应列,否则新流程详情无法完整落库展示
## 4. 现状分析
### 4.1 前端现状
个人详情页由两个主要区域组成:
- `PersonalWorkflowDetail.vue` 负责流程详情与左侧关键信息
- `ModelOutputDisplay.vue` 负责个人模型输出分组展示
当前页面已覆盖大部分 `展示指标` 字段,但个人详情页“业务信息”中未展示 `loanTerm`,个人模型输出“测算结果”中也未展示 5 个新增利率相关字段。
### 4.2 后端现状
- `LoanPricingWorkflow` 已包含 `loanTerm`
- `ModelRetailOutputFields` 当前未包含 `loanRateHistory``minRateProduct``smoothRange``finalCalculateRate``referenceRate`
因此前端目前无法从个人模型输出对象中读取这 5 个字段。
同时,开发库 `model_retail_output_fields` 表当前也未包含这 5 个字段列。如果只补代码而不补表结构,新的个人流程在模型结果入库时将无法完整保存这些字段。
## 5. 方案对比
### 方案一:在现有分组内补齐字段
做法:
- 在个人详情页“业务信息”区域补 `loanTerm`
- 在个人模型输出“测算结果”区域补 5 个新增利率字段
- 后端仅补 `ModelRetailOutputFields` 缺失字段定义
优点:
- 改动最小
- 不影响现有页面结构
- 与现有字段分组最贴合
缺点:
- 需要同时修改前后端
### 方案二:单独新增“利率结果扩展”分组
做法:
- 新增一个专门的 Tab 或卡片展示 5 个新增利率字段
优点:
- 新增字段集中展示
缺点:
- 页面改动更大
- 用户认知路径变化
- 不符合本次最短路径要求
## 6. 设计结论
采用方案一。
实现方式如下:
- 后端在 `ModelRetailOutputFields` 中新增 5 个字段定义,保证接口对象具备完整返回结构
- 数据库为 `model_retail_output_fields` 新增 5 个对应列,保证模型输出可正常落库
- 前端在 `PersonalWorkflowDetail.vue` 的“业务信息”中补齐 `loanTerm`
- 前端在 `ModelOutputDisplay.vue` 的个人“测算结果”中补齐 `loanRateHistory``minRateProduct``smoothRange``finalCalculateRate``referenceRate`
## 7. 验证设计
本次按最小可执行验证:
- 后端新增一个实体字段断言测试,先验证缺失字段不存在并失败,再补齐后验证通过
- 前端新增一个源码断言脚本,先验证缺失展示未实现并失败,再补齐后验证通过
- 对开发库执行表结构迁移
- 创建新的个人流程并打开详情页,确认新增字段可在真实页面展示
- 最后执行前端生产构建,确认页面代码可正常打包
## 8. 非目标
- 不调整企业详情页
- 不修改模型计算逻辑
- 不重构页面布局

View File

@@ -0,0 +1,81 @@
# 个人模型详情缺失展示字段补齐前端实施计划
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在个人详情页补齐 `展示指标` sheet 中缺失的 6 个字段展示。
**Architecture:** 前端沿用现有个人详情页结构,在流程详情的“业务信息”中补 `loanTerm`,在模型输出“测算结果”中补 5 个新增利率字段,不新增页面分组和交互。
**Tech Stack:** Vue 2、Element UI、Node.js
---
### Task 1: 通过前端断言测试锁定缺失展示
**Files:**
- Create: `ruoyi-ui/tests/retail-display-fields.test.js`
- Modify: `ruoyi-ui/package.json`
- [ ] **Step 1: 编写源码断言脚本**
断言以下展示已存在:
- `PersonalWorkflowDetail.vue` 包含 `借款期限``detailData.loanTerm`
- `ModelOutputDisplay.vue` 包含以下字段展示:
- `loanRateHistory`
- `minRateProduct`
- `smoothRange`
- `finalCalculateRate`
- `referenceRate`
- [ ] **Step 2: 运行脚本并确认先失败**
Run: `npm --prefix ruoyi-ui run test:retail-display-fields`
Expected: FAIL提示缺失展示实现。
### Task 2: 补齐个人详情页字段展示
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- Reference: `ruoyi-loan-pricing/src/main/resources/data/retail_output.json`
- [ ] **Step 1: 在业务信息中补齐借款期限**
`PersonalWorkflowDetail.vue` 的“业务信息”区域新增:
```vue
<el-descriptions-item label="借款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
```
- [ ] **Step 2: 在个人测算结果中补齐 5 个字段**
`ModelOutputDisplay.vue` 的个人“测算结果”中新增:
- 历史利率
- 产品最低利率下限
- 平滑幅度
- 最终测算利率
- 参考利率
- [ ] **Step 3: 重新运行前端断言脚本**
Run: `npm --prefix ruoyi-ui run test:retail-display-fields`
Expected: PASS
- [ ] **Step 4: 执行前端构建验证**
Run: `npm --prefix ruoyi-ui run build:prod`
Expected: 构建成功,输出包含 `Build complete.`
- [ ] **Step 5: 启动前后端并打开个人流程详情页验证**
使用浏览器打开新的个人流程详情页,确认:
- 流程详情“业务信息”出现 `借款期限`
- 模型输出“测算结果”出现并可查看以下字段
- 历史利率
- 产品最低利率下限
- 平滑幅度
- 最终测算利率
- 参考利率

View File

@@ -0,0 +1,238 @@
# 上虞个人利率测算输入参数后端 Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 补齐个人创建 DTO、流程转换和模型调用参数使个人模型请求完整覆盖 Excel 要求的输入字段。
**Architecture:** 维持现有“页面提交 -> DTO -> Workflow -> ModelInvokeDTO -> form-urlencoded 调用”的链路,只在个人创建与模型调用相关文件中补齐字段和转换规则。通过后端单元测试与真实接口联调覆盖必填、正常和分支场景。
**Tech Stack:** Spring Boot、MyBatis-Plus、Lombok、JUnit、Maven
---
## 后端模型输入参数确认
个人链路最终需要发给模型的 16 个参数如下:
- `serialNum`:服务层自动生成
- `orgCode`:服务层默认值,当前代码为 `892000`
- `runType`:服务层默认值 `1`
- `custIsn`:页面输入透传
- `custType`:个人链路固定 `个人`
- `custName`:页面输入,调用模型前解密后透传
- `idType`:页面输入透传
- `idNum`:页面输入,调用模型前解密后透传
- `guarType`:页面输入透传
- `applyAmt`:页面输入透传
- `loanPurpose`:页面输入透传
- `loanTerm`:页面输入透传
- `bizProof`:页面开关,调用模型前转 `0/1`
- `loanLoop`:页面开关,调用模型前转 `0/1`
- `collThirdParty`:页面开关,调用模型前转 `0/1`
- `collType`:页面下拉透传
调用方式确认:
- 参数载体:`ModelInvokeDTO`
- 组装方式:`BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO)`
- 请求格式:`application/x-www-form-urlencoded`
- 发送入口:`ModelService#invokeModel`
## 文件结构
- Modify: [ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java)
- 增加 `loanPurpose``loanTerm`
- Modify: [ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java)
- 将新增字段映射到 `LoanPricingWorkflow`
- Modify: [ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java)
- 增加 `loanTerm``loanLoop`
- Modify: [ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java)
- 在个人模型调用前规范化 `0/1`
- Create: [ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java)
- 覆盖字段存在与值转换
### Task 1: 为新增字段补失败测试
**Files:**
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- [ ] **Step 1: 编写字段与转换测试**
```java
@Test
void shouldContainLoanPurposeLoanTermAndLoanLoop() {
assertThat(ModelInvokeDTO.class.getDeclaredField("loanTerm")).isNotNull();
assertThat(ModelInvokeDTO.class.getDeclaredField("loanLoop")).isNotNull();
}
```
```java
@Test
void shouldConvertPersonalBooleanFlagsToZeroOne() {
// 构造个人流程对象,断言模型请求中的 bizProof/loanLoop/collThirdParty 为 1/0
}
```
- [ ] **Step 2: 运行测试并确认先失败**
Run: `mvn -pl ruoyi-loan-pricing -Dtest=LoanPricingModelServicePersonalParamsTest test`
Expected: FAIL提示字段不存在或转换逻辑未实现
- [ ] **Step 3: 保持测试只覆盖本次改动相关链路**
```java
// 仅断言 PersonalLoanPricingCreateDTO / LoanPricingConverter / ModelInvokeDTO / LoanPricingModelService
```
- [ ] **Step 4: 增加最终模型请求参数断言**
```java
// 断言 requestBody 包含 serialNum、orgCode、runType、custIsn、custType、custName、
// idType、idNum、guarType、applyAmt、loanPurpose、loanTerm、bizProof、
// loanLoop、collThirdParty、collType
```
- [ ] **Step 5: 再次运行测试确认失败原因稳定**
Run: `mvn -pl ruoyi-loan-pricing -Dtest=LoanPricingModelServicePersonalParamsTest test`
Expected: FAIL失败点与新增字段缺失一致
- [ ] **Step 6: 提交**
```bash
git add ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java
git commit -m "新增个人测算输入参数后端测试"
```
### Task 2: 补齐个人创建 DTO 与流程映射
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- [ ] **Step 1: 在个人 DTO 中增加 `loanPurpose`**
```java
@NotBlank(message = "贷款用途不能为空")
@Pattern(regexp = "^(consumer|business)$", message = "贷款用途必须是 consumer 或 business")
private String loanPurpose;
```
- [ ] **Step 2: 在个人 DTO 中增加 `loanTerm`**
```java
@NotBlank(message = "借款期限不能为空")
private String loanTerm;
```
- [ ] **Step 3: 在转换器中映射新增字段**
```java
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setLoanTerm(dto.getLoanTerm());
```
- [ ] **Step 4: 运行相关测试确认 DTO 与映射通过**
Run: `mvn -pl ruoyi-loan-pricing -Dtest=LoanPricingModelServicePersonalParamsTest test`
Expected: 仍可能 FAIL但失败点已推进到模型调用层
- [ ] **Step 5: 提交**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java
git commit -m "补齐个人测算创建参数字段"
```
### Task 3: 补齐模型调用 DTO 与个人参数规范化
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java`
- [ ] **Step 1: 在模型调用 DTO 中增加缺失字段**
```java
private String loanTerm;
private String loanLoop;
```
- [ ] **Step 2: 在个人模型调用前做 `0/1` 转换**
```java
if ("个人".equals(loanPricingWorkflow.getCustType())) {
modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof()));
modelInvokeDTO.setLoanLoop(toZeroOne(modelInvokeDTO.getLoanLoop()));
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
}
```
- [ ] **Step 3: 抽出最小辅助方法,避免散落重复逻辑**
```java
private String toZeroOne(String value) {
if ("true".equals(value) || "1".equals(value)) return "1";
if ("false".equals(value) || "0".equals(value)) return "0";
return value;
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `mvn -pl ruoyi-loan-pricing -Dtest=LoanPricingModelServicePersonalParamsTest test`
Expected: PASS
- [ ] **Step 5: 提交**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java
git commit -m "规范个人测算模型调用参数"
```
### Task 4: 后端联调与接口验证
**Files:**
- Verify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- Verify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- [ ] **Step 1: 重新编译并重启后端进程**
Run: `mvn -pl ruoyi-admin -am package -DskipTests`
Expected: 打包成功,随后重启运行中的后端服务使最新代码生效
- [ ] **Step 2: 验证正常场景**
Run: 调用 `POST /loanPricing/workflow/create/personal`
Expected:
- 返回创建成功
- 请求体包含 `loanPurpose``loanTerm`
- 模型请求中完整带出 16 个参数
- 其中 `bizProof``loanLoop``collThirdParty``0/1`
- [ ] **Step 3: 验证必填缺失场景**
Run: 缺少 `loanPurpose``loanTerm` 分别调用接口
Expected: 返回参数校验失败
- [ ] **Step 4: 验证分支场景**
Run: 以不同开关组合调用接口
Expected:
- `bizProof=true` -> 模型入参 `1`
- `bizProof=false` -> 模型入参 `0`
- `loanLoop=true/false``collThirdParty=true/false` 同理
- [ ] **Step 5: 验证结束后停止后端进程并提交**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java
git commit -m "完成个人测算输入参数后端联调"
```

View File

@@ -0,0 +1,322 @@
# 上虞个人利率测算输入参数对齐设计文档
## 1. 背景
根据 [doc/上虞利率测算接口文档.xlsx](/Users/wkc/Desktop/loan-pricing/loan-pricing/doc/上虞利率测算接口文档.xlsx) 的 `入参` sheet个人利率测算模型当前要求的输入参数为
- `serialNum`
- `orgCode`
- `runType`
- `custIsn`
- `custType`
- `custName`
- `idType`
- `idNum`
- `guarType`
- `applyAmt`
- `loanPurpose`
- `loanTerm`
- `bizProof`
- `loanLoop`
- `collThirdParty`
- `collType`
现有个人新增弹窗与模型调用链路未完全覆盖该输入集合:
- 页面缺少 `loanPurpose`
- 页面缺少 `loanTerm`
- `collType` 选项与 Excel 不一致
- 页面开关字段当前提交的是 `true/false`
- 模型调用 DTO 当前缺少 `loanTerm``loanLoop`
本次目标是按 Excel 的个人输入参数定义,对齐个人新增弹窗输入项和模型调用入参,不扩展到企业链路,不引入兜底或兼容分支。
## 2. 已确认范围
- 仅处理个人新增弹窗
- 仅处理个人创建流程到模型调用的入参链路
- 保持现有页面交互结构,不新增系统字段输入区
- `loanTerm` 使用固定年限下拉,选项按 Excel 定义
- 系统字段继续自动生成或默认赋值
- 不修改企业新增弹窗
- 不修改模型计算规则
## 3. 输入参数获取方式整理
### 3.1 系统自动带值
以下字段不放到新增弹窗中,由现有服务链路自动提供:
- `serialNum`
-`LoanPricingWorkflowServiceImpl#createLoanPricing` 按时间戳生成
- `orgCode`
-`LoanPricingWorkflowServiceImpl#createLoanPricing` 在空值时补默认值
- `runType`
-`LoanPricingWorkflowServiceImpl#createLoanPricing` 在空值时补默认值 `1`
- `custType`
-`LoanPricingConverter#toEntity(PersonalLoanPricingCreateDTO)` 固定写为 `个人`
### 3.2 用户直接输入
- 文本输入:
- `custIsn`
- `custName`
- `idNum`
- `applyAmt`
- 下拉选择:
- `idType`
- `guarType`
- `loanPurpose`
- `loanTerm`
- `collType`
- 开关选择:
- `bizProof`
- `loanLoop`
- `collThirdParty`
## 4. 现状分析
### 4.1 前端现状
[PersonalCreateDialog.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue) 当前已经提供:
- `custIsn`
- `custName`
- `idType`
- `idNum`
- `guarType`
- `applyAmt`
- `bizProof`
- `loanLoop`
- `collType`
- `collThirdParty`
当前缺失或不一致点:
- 缺少 `loanPurpose`
- 缺少 `loanTerm`
- `collType` 选项为 `一线/一类/二类`,与 Excel 的 `一类/二类/三类` 不一致
- 开关字段提交值为 `true/false`
### 4.2 后端现状
[PersonalLoanPricingCreateDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java) 当前未定义:
- `loanPurpose`
- `loanTerm`
[LoanPricingConverter.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java) 当前未把以上字段映射到 `LoanPricingWorkflow`
[ModelInvokeDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java) 当前未定义:
- `loanTerm`
- `loanLoop`
这意味着即使页面补齐字段,当前模型调用也无法完整带出 Excel 要求的个人入参。
## 5. 方案对比
### 方案一:页面补齐用户输入,系统字段继续自动带值
做法:
- 在个人新增弹窗新增 `loanPurpose` 下拉
- 在个人新增弹窗新增 `loanTerm` 固定年限下拉
- 修正 `collType` 选项
- 后端 DTO、转换器、模型调用 DTO 同步补字段
- 模型调用前统一将开关字段转换为 Excel 要求的 `0/1`
优点:
- 与现有个人/企业创建方式一致
- 改动最小
- 页面输入、流程入库、模型调用职责边界清晰
缺点:
- 需要同时修改前后端
### 方案二:前端少改,后端在模型调用前兜底补参数
做法:
- 页面只补部分字段
- 其余由后端按默认逻辑拼接模型参数
优点:
- 页面改动更少
缺点:
- 页面输入和真实模型入参不一致
- 后续排查问题时难以定位参数来源
- 不符合本次“按现有输入方式整理字段获取方式”的要求
### 方案三Excel 全量字段全部暴露到弹窗
做法:
-`serialNum``orgCode``runType``custType` 也作为页面字段给用户填写
优点:
- 页面可见字段与 Excel 完全一一对应
缺点:
- 与当前产品交互方式不一致
- 增加误填风险
- 不符合最短路径要求
## 6. 设计结论
采用方案一。
### 6.1 页面设计
个人新增弹窗保留现有分组结构,在“贷款信息”区域补齐:
- `loanPurpose`
- 下拉选项:`consumer``business`
- `loanTerm`
- 固定年限下拉
- 选项固定为 `1/2/3/4/5/6`
同时修正:
- `collType` 选项改为 `一类/二类/三类`
### 6.2 参数来源设计
- 系统带值:
- `serialNum`
- `orgCode`
- `runType`
- `custType`
- 页面透传:
- `custIsn`
- `custName`
- `idType`
- `idNum`
- `guarType`
- `applyAmt`
- `loanPurpose`
- `loanTerm`
- `collType`
- 页面开关,经模型调用层转换:
- `bizProof`
- `loanLoop`
- `collThirdParty`
### 6.3 链路设计
1. 前端提交个人创建请求时,补齐 `loanPurpose``loanTerm`
2. 后端个人创建 DTO 接收新增字段
3. 转换器将新增字段写入 `LoanPricingWorkflow`
4. 模型调用 DTO 增加 `loanPurpose``loanTerm``loanLoop`
5. `LoanPricingModelService` 在调用模型前,将个人链路中的开关字段转换为 `0/1`
6. `ModelService` 继续以 `application/x-www-form-urlencoded` 方式调用模型接口
### 6.3.1 后端模型调用输入参数确认
后端最终发给模型的个人入参,按 Excel 要求确认为以下 16 个字段:
- `serialNum`
- 来源:`LoanPricingWorkflowServiceImpl#createLoanPricing` 自动生成
- `orgCode`
- 来源:`LoanPricingWorkflowServiceImpl#createLoanPricing` 默认赋值
- 当前代码值:`892000`
- `runType`
- 来源:`LoanPricingWorkflowServiceImpl#createLoanPricing`
- 当前值:`1`
- `custIsn`
- 来源:页面输入,经个人创建 DTO 和转换器透传
- `custType`
- 来源:`LoanPricingConverter#toEntity(PersonalLoanPricingCreateDTO)`
- 当前值:固定 `个人`
- `custName`
- 来源:页面输入
- 说明:入库时加密,调用模型前解密
- `idType`
- 来源:页面输入
- `idNum`
- 来源:页面输入
- 说明:入库时加密,调用模型前解密
- `guarType`
- 来源:页面输入
- `applyAmt`
- 来源:页面输入
- `loanPurpose`
- 来源:页面输入
- 当前状态:需补齐到个人 DTO、流程实体映射和模型 DTO
- `loanTerm`
- 来源:页面输入
- 当前状态:需补齐到个人 DTO、流程实体映射和模型 DTO
- `bizProof`
- 来源:页面开关
- 模型值:调用模型前统一转换为 `0/1`
- `loanLoop`
- 来源:页面开关
- 当前状态:需补齐到模型 DTO
- 模型值:调用模型前统一转换为 `0/1`
- `collThirdParty`
- 来源:页面开关
- 模型值:调用模型前统一转换为 `0/1`
- `collType`
- 来源:页面下拉
- 模型值:按 `一类/二类/三类` 直接透传
后端调用方式确认如下:
- 参数载体:`ModelInvokeDTO`
- 参数来源:`BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO)`
- 请求构造:`ModelService#entityToMap`
- 请求格式:`application/x-www-form-urlencoded`
- 发送入口:`ModelService#invokeModel`
### 6.4 展示闭环
为保证输入项可在详情页回看,个人详情页同步补齐:
- `loanPurpose`
`loanTerm` 详情展示已存在,不需要新增区域。
## 7. 校验与错误处理
- 前端新增 `loanPurpose` 必选校验
- 前端新增 `loanTerm` 必选校验
- `loanTerm` 只能通过固定下拉选择,不提供自由输入
- 后端 DTO 对 `loanPurpose``loanTerm` 增加必填约束
- 保持现有创建失败与模型调用失败的错误提示方式
- 不新增兼容逻辑、兜底逻辑或补丁式分支
## 8. 验证设计
- 前端源码断言个人新增弹窗已出现 `loanPurpose``loanTerm`
- 前端源码断言 `loanTerm` 为固定下拉、`collType` 选项为 `一类/二类/三类`
- 后端测试或源码断言 `PersonalLoanPricingCreateDTO``LoanPricingConverter``ModelInvokeDTO` 已补齐字段
- 后端测试或日志断言调用模型前最终请求参数完整包含以上 16 个字段
- 重启后端后,覆盖以下接口验证:
- 正常场景:完整参数创建成功
- 必填缺失场景:缺少 `loanPurpose``loanTerm` 被拦截
- 分支场景:`bizProof``loanLoop``collThirdParty` 开关不同组合能正确转换为 `0/1`
- 启动前端页面并通过浏览器检查:
- 新增弹窗展示正确
- 提交流程后详情页能回显 `loanPurpose``loanTerm`
- 验证完成后停止本次启动的前后端进程
## 9. 已确认项
- `orgCode` 统一为 `892000`
- `ModelInvokeDTO` 注释已统一为 `892000`
- 数据库 `loan_pricing_workflow.org_code` 默认值已统一为 `892000`
- 存量 `loan_pricing_workflow.org_code` 数据已通过迁移脚本统一为 `892000`
## 10. 非目标
- 不调整企业新增弹窗
- 不修改企业模型调用参数
- 不修改流程列表逻辑
- 不改模型返回字段映射逻辑

View File

@@ -0,0 +1,212 @@
# 上虞个人利率测算输入参数前端 Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 按 Excel 补齐个人新增弹窗输入项,确保页面提交字段与个人模型入参要求一致。
**Architecture:** 仅修改个人创建弹窗和个人详情页,沿用当前 Element UI 表单结构,不新增页面层级。通过前端源码断言覆盖新增字段、选项和值转换,确保页面输入与现有交互方式保持一致。
**Tech Stack:** Vue 2、Element UI、RuoYi 前端请求封装、Node 源码断言脚本
---
## 文件结构
- 修改: [ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue)
- 补齐 `loanPurpose``loanTerm`
- 修正 `collType` 选项
- 调整个人开关字段提交值
- 修改: [ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue)
- 补齐 `loanPurpose` 展示
- 如有需要,扩展布尔格式化兼容 `0/1`
- 新增: [ruoyi-ui/tests/personal-create-input-params.test.js](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-ui/tests/personal-create-input-params.test.js)
- 断言新增弹窗字段、选项和值转换逻辑
- 修改: [ruoyi-ui/package.json](/Users/wkc/Desktop/loan-pricing/loan-pricing/ruoyi-ui/package.json)
- 新增针对本次改动的测试命令
### Task 1: 为个人新增弹窗补失败断言
**Files:**
- Create: `ruoyi-ui/tests/personal-create-input-params.test.js`
- Modify: `ruoyi-ui/package.json`
- [ ] **Step 1: 编写失败断言脚本**
```js
const requiredFields = ['form.loanPurpose', 'form.loanTerm']
const requiredOptions = ['value="consumer"', 'value="business"', 'label="一类"', 'label="二类"', 'label="三类"']
const requiredConversions = [
"bizProof: this.form.bizProof ? '1' : '0'",
"loanLoop: this.form.loanLoop ? '1' : '0'",
"collThirdParty: this.form.collThirdParty ? '1' : '0'"
]
```
- [ ] **Step 2: 运行断言并确认先失败**
Run: `npm --prefix ruoyi-ui run test:personal-create-input-params`
Expected: FAIL提示缺少 `loanPurpose``loanTerm` 或值转换未按 `1/0`
- [ ] **Step 3: 在 `package.json` 注册测试命令**
```json
{
"scripts": {
"test:personal-create-input-params": "node tests/personal-create-input-params.test.js"
}
}
```
- [ ] **Step 4: 再次运行断言并确认仍处于失败态**
Run: `npm --prefix ruoyi-ui run test:personal-create-input-params`
Expected: FAIL失败原因与新增字段缺失一致
- [ ] **Step 5: 提交**
```bash
git add ruoyi-ui/tests/personal-create-input-params.test.js ruoyi-ui/package.json
git commit -m "新增个人测算输入参数前端断言"
```
### Task 2: 补齐个人新增弹窗字段与选项
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- [ ] **Step 1: 增加 `loanPurpose` 表单项**
```vue
<el-form-item label="贷款用途" prop="loanPurpose">
<el-select v-model="form.loanPurpose" placeholder="请选择贷款用途" style="width: 100%">
<el-option label="消费" value="consumer" />
<el-option label="经营" value="business" />
</el-select>
</el-form-item>
```
- [ ] **Step 2: 增加 `loanTerm` 固定年限下拉**
```vue
<el-form-item label="借款期限(年)" prop="loanTerm">
<el-select v-model="form.loanTerm" placeholder="请选择借款期限" style="width: 100%">
<el-option v-for="item in ['1', '2', '3', '4', '5', '6']" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
```
- [ ] **Step 3: 修正 `collType` 选项为 Excel 定义**
```vue
<el-option label="一类" value="一类" />
<el-option label="二类" value="二类" />
<el-option label="三类" value="三类" />
```
- [ ] **Step 4: 为新增字段补表单状态与校验**
```js
form: {
loanPurpose: undefined,
loanTerm: undefined
},
rules: {
loanPurpose: [{ required: true, message: '请选择贷款用途', trigger: 'change' }],
loanTerm: [{ required: true, message: '请选择借款期限', trigger: 'change' }]
}
```
- [ ] **Step 5: 提交**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue
git commit -m "补齐个人新增弹窗输入字段"
```
### Task 3: 调整前端提交值与详情展示
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- [ ] **Step 1: 在提交逻辑中改为 `1/0` 值**
```js
const data = {
...this.form,
bizProof: this.form.bizProof ? '1' : '0',
loanLoop: this.form.loanLoop ? '1' : '0',
collThirdParty: this.form.collThirdParty ? '1' : '0'
}
```
- [ ] **Step 2: 在详情页补齐 `loanPurpose` 展示**
```vue
<el-descriptions-item label="贷款用途">{{ detailData.loanPurpose || '-' }}</el-descriptions-item>
```
- [ ] **Step 3: 兼容详情页 `0/1` 布尔展示**
```js
if (value === 'true' || value === true || value === '1' || value === 1) return '是'
if (value === 'false' || value === false || value === '0' || value === 0) return '否'
```
- [ ] **Step 4: 运行前端源码断言并确认通过**
Run: `npm --prefix ruoyi-ui run test:personal-create-input-params`
Expected: PASS输出断言通过信息
- [ ] **Step 5: 提交**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue
git commit -m "调整个人测算前端提交与展示"
```
### Task 4: 页面联调与回归验证
**Files:**
- Verify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Verify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- [ ] **Step 1: 启动前端开发服务**
Run: `npm --prefix ruoyi-ui run dev`
Expected: 成功启动并输出本地访问地址
- [ ] **Step 2: 打开流程页面验证新增弹窗**
Run: 在浏览器中进入 `/loanPricing/workflow`
Expected:
- 出现 `贷款用途`
- 出现 `借款期限(年)` 固定下拉
- `抵质押类型` 选项为 `一类/二类/三类`
- [ ] **Step 3: 结合后端联调创建个人流程**
Run: 在页面中填写完整参数并提交
Expected: 创建成功,不出现参数缺失报错
- [ ] **Step 4: 打开详情页验证回显**
Run: 打开刚创建的个人流程详情
Expected:
- 页面展示 `贷款用途`
- 页面展示 `借款期限`
- 开关字段显示为“是/否”
- [ ] **Step 5: 验证结束后停止前端进程并提交**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue ruoyi-ui/tests/personal-create-input-params.test.js ruoyi-ui/package.json
git commit -m "完成个人测算输入参数前端联调"
```

View File

@@ -0,0 +1,52 @@
# 上虞对公利率测算字段对齐后端实施计划
## 目标
- 对齐对公创建接口、模型调用入参、流程详情返回、mock 返回和 SQL 基线。
## 实施内容
- 创建请求字段改为 Excel `上传指标` 口径:
- 新增 `repayMethod`
- `isTradeConstruction` 改为 `isTradeBuildEnt`
- 移除对公创建链路中的 `isAgriGuar``isTechEnt`
- 流程主表实体补 `repayMethod`,并将 `isTradeBuildEnt` 映射到数据库列 `is_trade_construction`
- 对公模型输出实体补齐:
- `repayMethod`
- `isTradeBuildEnt`
- `loanRateHistory`
- `minRateProduct`
- `smoothRange`
- `finalCalculateRate`
- `referenceRate`
- 对公模型输出实体不再暴露:
- `isAgriGuar`
- `midEntTax`
- `cardOverdue`
- 企业模型入参统一值域:
- `isGreenLoan``isTradeBuildEnt``collThirdParty` 发送 `0/1`
- `repayMethod` 发送 `分期/不分期`
- 企业流程详情主利率改为 `finalCalculateRate`
- mock 继续保留 `data.mappingOutputFields` 包装层,只更新企业字段集合和值域
## SQL 调整
- `loan_pricing_workflow` 新增 `repay_method`
- `model_corp_output_fields` 新增:
- `repay_method`
- `is_trade_build_ent`
- `loan_rate_history`
- `min_rate_product`
- `smooth_range`
- `final_calculate_rate`
- `reference_rate`
- 已同步更新:
- `sql/loan_pricing_workflow.sql`
- `sql/model_corp.sql`
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
- `sql/2026-04-16-shangyu-corporate-alignment.sql`
## 验证
- 运行后端定向单测,确认对公字段和详情主利率断言通过
- 使用 `/login/test` 获取 token 后调用对公创建和详情接口,确认:
- 正常场景成功
- 缺少 `repayMethod` 返回校验错误
- 详情返回包含新增字段且 `loanRate = finalCalculateRate`

View File

@@ -0,0 +1,48 @@
# 上虞对公利率测算字段对齐前端实施计划
## 目标
- 对齐对公新增弹窗和企业流程详情页展示,严格跟随 Excel `上传指标``展示指标`
## 实施内容
- 对公新增弹窗调整为 Excel `上传指标`
- 新增 `repayMethod`
- `isTradeConstruction` 改为 `isTradeBuildEnt`
- 删除 `省农担担保贷款``科技型企业`
- `loanTerm` 文案改为按年
- `collType` 选项改为 `一类/二类/三类/四类`
- `isGreenLoan``isTradeBuildEnt``collThirdParty` 提交值改为 `1/0`
- 企业详情左侧关键信息:
- 标签改为 `最终测算利率`
- 读取 `corpOutput.finalCalculateRate`
- 企业流程详情业务信息:
- 新增展示 `repayMethod`
- 新增展示 `isTradeBuildEnt`
- 保留 `isGreenLoan`
- 移除不在本次口径内的企业业务展示
- 企业模型输出补齐展示:
- `repayMethod`
- `isTradeBuildEnt`
- `loanRateHistory`
- `minRateProduct`
- `smoothRange`
- `finalCalculateRate`
- `referenceRate`
- 企业模型输出移除展示:
- `isAgriGuar`
- `midEntTax`
- `cardOverdue`
## 测试脚本
- 新增:
- `ruoyi-ui/tests/corporate-create-input-params.test.js`
- `ruoyi-ui/tests/corporate-display-fields.test.js`
- 更新 `ruoyi-ui/package.json`,补充对应 npm scripts
## 验证
- `nvm use default` 后执行两个对公静态断言脚本
- 执行前端生产构建
- 启动前端页面并在浏览器中确认:
- 对公新增弹窗字段和选项正确
- 创建成功后列表刷新
- 企业详情页显示 `最终测算利率`
- 企业详情页和模型输出出现新增字段

View File

@@ -32,10 +32,12 @@
| idNum | String | 否 | 证件号码 |
| guarType | String | 是 | 担保方式,可选值: 信用/保证/抵押/质押 |
| applyAmt | String | 是 | 申请金额,单位: 元 |
| bizProof | String | | 是否有经营佐证,值: true/false |
| loanLoop | String | | 循环功能,值: true/false |
| collType | String | 否 | 抵质押类型,可选值: 一线/一类/二类 |
| collThirdParty | String | 否 | 抵质押物是否三方所有,值: true/false |
| loanPurpose | String | | 贷款用途,可选值: consumer/business |
| loanTerm | String | | 借款期限(年),固定下拉选项按模型文档配置 |
| bizProof | String | 否 | 是否有经营佐证,值: 0/1 |
| loanLoop | String | 否 | 循环功能,值: 0/1 |
| collType | String | 否 | 抵质押类型,可选值: 一类/二类/三类 |
| collThirdParty | String | 否 | 抵质押物是否三方所有,值: 0/1 |
**请求示例:**
@@ -47,10 +49,12 @@
"idNum": "110101199001011234",
"guarType": "抵押",
"applyAmt": "500000",
"bizProof": "true",
"loanLoop": "false",
"loanPurpose": "business",
"loanTerm": "3",
"bizProof": "1",
"loanLoop": "0",
"collType": "一类",
"collThirdParty": "false"
"collThirdParty": "0"
}
```
@@ -64,12 +68,14 @@
"id": 1,
"modelOutputId": 100,
"serialNum": "20250119143025123",
"orgCode": "931000",
"orgCode": "892000",
"runType": "1",
"custIsn": "CUST001",
"custType": "个人",
"guarType": "抵押",
"applyAmt": "500000",
"loanPurpose": "business",
"loanTerm": "3",
"custName": "张三",
"idType": "身份证",
"createTime": "2025-01-19 14:30:25",
@@ -136,7 +142,7 @@
"id": 2,
"modelOutputId": 101,
"serialNum": "20250119143125456",
"orgCode": "931000",
"orgCode": "892000",
"runType": "1",
"custIsn": "CORP001",
"custType": "企业",
@@ -189,7 +195,7 @@ GET /loanPricing/workflow/list?pageNum=1&pageSize=10&custName=科技
"id": 1,
"modelOutputId": 100,
"serialNum": "20250119143025123",
"orgCode": "931000",
"orgCode": "892000",
"custIsn": "CUST001",
"custType": "企业",
"guarType": "抵押",
@@ -240,7 +246,7 @@ GET /loanPricing/workflow/20250119143025123
"id": 1,
"modelOutputId": 100,
"serialNum": "20250119143025123",
"orgCode": "931000",
"orgCode": "892000",
"runType": "1",
"custIsn": "CUST001",
"custType": "企业",

View File

@@ -0,0 +1,21 @@
# AGENTS 测试步骤规范补充实施记录
## 实施时间
- 2026-03-30
## 修改内容
- 新增仓库级 `AGENTS.md` 协作规范内容
- 明确开发完成后必须执行与改动对应的验证步骤
- 明确接口开发完成后需要先重启后端进程,再进行接口调用验证
- 明确接口测试必须覆盖正常场景、参数错误场景和关键业务分支场景
- 明确前端页面开发完成后需要通过浏览器检查页面功能、交互与接口联动
- 保留测试结束后自动关闭测试进程的要求
## 文档路径
- `AGENTS.md`
- `doc/implementation-report-2026-03-30-agents-test-rules.md`
## 结论
- 已将“开发完成后的测试步骤”写入仓库级协作规范
- 新增要求能够直接约束接口开发和前端页面开发的验收动作
- 本次修改仅涉及文档规范,不涉及业务代码与接口实现

View File

@@ -0,0 +1,13 @@
# 后端端口调整为 63310 实施记录
## 修改内容
-`ruoyi-admin``dev``uat``pro` 环境 `server.port` 统一调整为 `63310`
- 将后端模型调用配置 `model.url` 同步改为 `http://localhost:63310/rate/pricing/mock/invokeModel`,避免应用内部仍回调旧端口。
- 将前端本地开发代理 `ruoyi-ui/vue.config.js` 的后端目标端口同步改为 `63310`
-`test_api` 下的 Shell 脚本与 `.http` 示例请求地址统一改为 `http://localhost:63310`
## 验证结果
- 执行 `./bin/restart_java_backend.sh restart`,构建与重启成功,启动日志显示开发环境实际监听端口为 `http-nio-63310`
- 执行 `lsof -iTCP:63310 -sTCP:LISTEN`,确认 Java 进程已监听 `63310` 端口。
- 执行 `curl -sS -X POST http://localhost:63310/login/test -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}'`,返回 `{"code":200,...}`,确认新端口可正常访问登录测试接口。
- 执行 `./bin/restart_java_backend.sh stop` 后再次检查 `./bin/restart_java_backend.sh status``lsof -iTCP:63310 -sTCP:LISTEN`,确认本次验证启动的后端进程已停止。

View File

@@ -0,0 +1,33 @@
# 贷款定价敏感字段加密后端实施记录
## 修改内容
-`ruoyi-loan-pricing` 新增 `SensitiveFieldCryptoService`,统一处理 `custName``idNum` 的 AES/ECB/PKCS5Padding + Base64 加解密。
-`ruoyi-loan-pricing` 新增 `LoanPricingSensitiveDisplayService`,统一处理个人姓名、企业名称、身份证号、统一社会信用代码的脱敏展示。
-`LoanPricingWorkflowServiceImpl` 的创建链路对 `custName``idNum` 加密后入库,并在列表、详情链路解密后做脱敏返回。
-`LoanPricingWorkflowServiceImpl` 的详情链路补充对 `ModelRetailOutputFields``ModelCorpOutputFields` 基本信息中的 `custName``idNum` 进行脱敏,避免模型输出区域继续暴露明文。
-`LoanPricingModelService` 调用模型前显式解密 `custName``idNum`,保证模型入参不接收密文;同时补齐 `ModelInvokeDTO.idNum` 字段。
- 修复模型调用后更新 `modelOutputId` 时把解密后的 `custName``idNum` 明文回写数据库的问题,改为仅更新 `modelOutputId`
-`LoanPricingWorkflowMapper.xml` 和服务查询条件中移除按 `custName` 查询,改为按 `custIsn` 查询。
- 新增 `sql/clear_loan_pricing_workflow_history.sql`,用于清理贷款定价流程及模型输出历史数据。
## 新增测试
- `SensitiveFieldCryptoServiceTest`
- `LoanPricingSensitiveDisplayServiceTest`
- `LoanPricingWorkflowServiceImplTest`
- `LoanPricingModelServiceTest`
## 验证结果
- 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。
- 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。
- 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。
- 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。
- 执行 `mvn -pl ruoyi-loan-pricing -am -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。
- 从根工程重新打包 `ruoyi-admin.jar` 后,以 `18080` 端口启动临时后端实例,并将 `model.url` 指向 `http://localhost:18080/rate/pricing/mock/invokeModel` 完成联调。
- 个人流程与企业流程创建成功,接口即时返回的 `custName``idNum` 为密文。
- 调用参数错误场景 `/loanPricing/workflow/create/personal` 且缺少 `custIsn`,接口返回 `500`,错误信息为“客户内码不能为空”。
- 调用列表接口按 `custIsn` 查询,确认个人返回 `custName``张*`,企业返回 `custName``测试****公司`
- 调用详情接口,确认流程主信息中个人返回 `张* / 1101********1234`,企业返回 `测试****公司 / 91*************00X`
- 调用详情接口,确认模型输出“基本信息”中个人返回 `张* / 3301********1234`,企业返回 `北京******公司 / 91*************XXX`
## 备注
- 联调过程中发现 `serialNum` 仍使用毫秒时间戳生成,并发创建可能触发 `uk_serial_num` 冲突;该问题为本次验证中暴露的既有风险,本次未纳入敏感字段加密方案范围内处理。

View File

@@ -0,0 +1,34 @@
# 贷款定价流程客户敏感信息加密设计实施记录
## 实施时间
- 2026-03-30
## 修改内容
- 新增贷款定价流程客户敏感信息加密改造设计文档
- 明确本次范围仅覆盖贷款定价流程主链
- 明确敏感字段限定为 `custName``idNum`
- 明确采用应用层 AES 加解密与返回前统一脱敏方案
- 明确列表查询改为仅支持客户内码 `custIsn`
- 明确存量数据处理方式为直接清空,不做迁移
- 补充模型输出“基本信息”中的 `custName``idNum` 也需纳入展示脱敏范围
## 文档路径
- `doc/2026-03-30-loan-pricing-sensitive-data-encryption-design.md`
- `doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-design.md`
## 设计结论
- `loan_pricing_workflow.cust_name``loan_pricing_workflow.id_num` 改为密文存储
- 贷款定价流程列表页、详情页仅展示脱敏值
- 贷款定价流程详情页中的模型输出“基本信息”也仅展示脱敏值
- 前端不承担加解密职责
- 模型调用前由后端服务内部解密敏感字段
## 说明
- 设计文档已按当前仓库习惯保存到 `doc/` 目录
- 仓库约束禁止启用 subagent因此本次未执行基于 subagent 的设计文档复审流程,改为人工复审
- 本次仅完成设计,不包含实施代码修改

View File

@@ -0,0 +1,15 @@
# 贷款定价敏感字段加密前端实施记录
## 修改内容
- 流程列表页查询项已从“客户名称”切换为“客户内码”,查询参数从 `queryParams.custName` 改为 `queryParams.custIsn`
- `ruoyi-ui/src/api/loanPricing/workflow.js` 保持 `params: query` 透传,不新增任何前端字段映射或加解密逻辑。
- 列表页继续直接展示后端返回的 `custName`,详情页继续直接展示后端返回的 `custName``idNum`,前端不承担脱敏算法和明文查看能力。
## 验证结果
- 执行 `rg -n 'custName|custIsn|客户名称|客户内码' ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/api/loanPricing/workflow.js`,确认列表页查询区已改为 `custIsn`,不再使用 `queryParams.custName`
- 执行 `npm --prefix ruoyi-ui run build:prod`,结果通过,最终输出包含 `Build complete.`;构建过程中仅有原有包体积告警,无新增编译错误。
- 核对 `detail.vue``PersonalWorkflowDetail.vue``CorporateWorkflowDetail.vue`,确认详情页仍直接渲染 `detailData.custName``detailData.idNum`,未新增任何前端二次脱敏或明文查看逻辑。
- 结合后端联调结果确认:后端列表接口已返回 `张*`,详情接口已返回 `张* / 1101********1234 / 测试****公司 / 91*************00X`,前端现有展示代码会直接消费这些脱敏值。
## 备注
- 浏览器侧尝试通过现有 `9527` 前端服务进入贷款定价页面时,受其固定代理目标 `http://localhost:8080` 上现有后端接口超时影响,未完成一次独立的前端页面点击链路;本次前端展示结论基于源码直渲染核对、生产构建通过以及后端真实接口返回值联调共同确认。

View File

@@ -0,0 +1,29 @@
# 贷款定价流程客户敏感信息加密计划实施记录
## 实施时间
- 2026-03-30
## 修改内容
- 新增贷款定价敏感信息加密后端实施计划
- 新增贷款定价敏感信息加密前端实施计划
- 明确后端采用统一加解密服务 + 统一展示脱敏服务的实施路径
- 明确前端仅做查询项收口和脱敏值消费,不承担加解密
- 明确测试命令、数据库清理脚本、实施记录与提交节点
- 补充模型输出“基本信息”页签中的 `custName``idNum` 也纳入脱敏验证范围
## 文档路径
- `docs/superpowers/plans/2026-03-30-loan-pricing-sensitive-data-encryption-backend-plan.md`
- `docs/superpowers/plans/2026-03-30-loan-pricing-sensitive-data-encryption-frontend-plan.md`
- `doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-plans.md`
## 计划结论
- 计划已按仓库要求拆分为后端执行文档和前端执行文档
- 后端计划覆盖密钥配置、敏感字段加解密、列表/详情脱敏、模型调用前解密和历史数据清理
- 后端计划补充模型输出“基本信息”返回值脱敏
- 前端计划覆盖查询项切换为客户内码、脱敏值展示消费、构建验证和联调验证,并显式检查模型输出“基本信息”页签
- 两份计划都采用最短路径实现,不引入明密文兼容分支
## 说明
- 已检查计划文档保存路径,执行计划保存至 `docs/superpowers/plans`
- 仓库约束禁止启用 subagent本次计划复审采用本地自检方式处理
- 当前仅完成计划文档,不包含代码实现

View File

@@ -0,0 +1,18 @@
# 密码加密传输后端实施记录
## 修改内容
-`ruoyi-framework` 新增 `PasswordTransferCryptoService`,统一处理 AES/ECB/PKCS5Padding + Base64 的密码传输解密。
-`ruoyi-admin``application.yml``application-dev.yml` 增加 `security.password-transfer.key` 配置。
-`/login``/register``/system/user/profile/updatePwd``/system/user``/system/user/resetPwd` 入口增加密码字段解密,随后继续复用原有认证、校验和 BCrypt 入库逻辑。
- 保持 `/login/test` 未改动。
## 新增测试
- `PasswordTransferCryptoServiceTest`
- `SysLoginControllerPasswordTransferTest`
- `SysRegisterControllerPasswordTransferTest`
- `SysProfileControllerPasswordTransferTest`
- `SysUserControllerPasswordTransferTest`
## 验证结果
- 执行 `mvn -pl ruoyi-admin,ruoyi-framework -am -Dtest=PasswordTransferCryptoServiceTest,SysLoginControllerPasswordTransferTest,SysRegisterControllerPasswordTransferTest,SysProfileControllerPasswordTransferTest,SysUserControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。
- 检查 `SysLoginController` 引入解密的提交 diff确认仅 `/login` 增加了解密调用,`/login/test` 无行为变化。

View File

@@ -0,0 +1,26 @@
# 系统登录与密码类接口加密传输设计实施记录
## 实施时间
- 2026-03-30
## 修改内容
- 新增系统登录与密码类接口加密传输设计文档
- 明确采用固定密钥的对称加密方案
- 明确覆盖正式密码提交接口并排除 `/login/test`
- 明确前端在 API 提交前加密、后端在控制器入口前统一解密
- 明确不采用明密文兼容处理
## 文档路径
- `docs/superpowers/specs/2026-03-30-login-password-encryption-design.md`
- `doc/implementation-report-2026-03-30-login-password-encryption-design.md`
## 设计结论
- 对正式密码提交接口启用固定密钥对称加密传输
- 保持原有请求字段名不变,仅对密码字段做加密与解密
- 解密成功后继续复用现有认证、校验与 BCrypt 入库逻辑
- `/login/test` 保持现状,不接入本次改动
## 说明
- 已按要求检查设计文档保存路径,正式设计文档保存至 `docs/superpowers/specs`
- 仓库约束禁止启用 subagent本次设计文档复审采用本地自检方式处理
- 设计确认后,下一步需要分别产出后端实施计划和前端实施计划

View File

@@ -0,0 +1,16 @@
# 密码加密传输前端实施记录
## 修改内容
- 新增 `ruoyi-ui/src/utils/passwordTransfer.js`,统一处理密码字段 AES/ECB/PKCS7 加密。
-`ruoyi-ui/package.json` 增加 `test:password-transfer` 脚本,并引入 `crypto-js` 依赖。
-`ruoyi-ui/src/api/login.js` 中为登录、注册请求只加密 `password` 字段。
-`ruoyi-ui/src/api/system/user.js` 中为个人修改密码、管理员新增用户、管理员重置密码请求加密受控密码字段。
-`ruoyi-ui/.env.development``ruoyi-ui/.env.staging``ruoyi-ui/.env.production` 增加 `VUE_APP_PASSWORD_TRANSFER_KEY` 配置。
- 页面组件保持明文表单值和原有校验逻辑不变。
## 新增验证
- 新增 `ruoyi-ui/tests/password-transfer-api.test.js`,覆盖密码加密工具、登录、注册、个人修改密码、管理员新增用户、管理员重置密码 API。
## 验证结果
- 执行 `npm run test:password-transfer`,结果通过。
- 执行 `npm run build:stage`,结果通过;仅存在既有的打包体积 warning无新增构建错误。

View File

@@ -0,0 +1,27 @@
# 系统登录与密码类接口加密传输计划实施记录
## 实施时间
- 2026-03-30
## 修改内容
- 新增密码加密传输后端实施计划
- 新增密码加密传输前端实施计划
- 明确后端以统一解密服务 + 控制器显式接入的方式实施
- 明确前端以统一加密工具 + API 层字段映射的方式实施
- 明确测试命令、实施记录与提交节点
## 文档路径
- `docs/superpowers/plans/2026-03-30-login-password-encryption-backend-plan.md`
- `docs/superpowers/plans/2026-03-30-login-password-encryption-frontend-plan.md`
- `doc/implementation-report-2026-03-30-login-password-encryption-plans.md`
## 计划结论
- 计划已按仓库要求拆分为后端执行文档和前端执行文档
- 两份计划都采用最短路径实现,不引入明密文兼容分支
- 后端计划覆盖统一解密、控制器接入和 MockMvc/单测验证
- 前端计划覆盖统一加密、API 接入、环境配置和 Node 脚本验证
## 说明
- 已按要求检查计划文档保存路径,计划保存至 `docs/superpowers/plans`
- 仓库约束禁止启用 subagent本次计划复审采用本地自检方式处理
- 当前仅完成计划文档,不包含代码实现

View File

@@ -0,0 +1,22 @@
# deploy 目录文档整理实施记录
## 修改内容
- 新增 `deploy` 目录下的本地安装手册:
- `deploy/2026-03-31-local-nginx-java-install-manual.md`
- 新增 `deploy` 目录下的独立 Nginx 配置文件:
- `deploy/nginx.conf`
- 安装手册中的 Nginx 配置说明调整为直接引用 `deploy/nginx.conf`
- 删除原 `doc/2026-03-31-local-nginx-java-install-manual.md`,避免同一手册在仓库内出现两份路径
## 路径检查
- 已确认安装手册当前保存路径为 `deploy/2026-03-31-local-nginx-java-install-manual.md`
- 已确认 Nginx 配置文件当前保存路径为 `deploy/nginx.conf`
- 已确认本次实施记录保存路径为 `doc/implementation-report-2026-03-31-deploy-folder-docs.md`
## 验证结果
- 已执行 `ls -l deploy/2026-03-31-local-nginx-java-install-manual.md deploy/nginx.conf`
- 已人工核对 `deploy/nginx.conf``bin/prod/install_env.sh` 中写入的 Nginx 配置保持一致
- 已人工核对手册中的目录、端口和脚本引用与当前交付物保持一致

View File

@@ -0,0 +1,24 @@
# 本地安装 Nginx 和 Java 手册实施记录
## 修改内容
- 新增本地安装手册 `deploy/2026-03-31-local-nginx-java-install-manual.md`
- 手册内容与当前生产安装脚本保持一致:
- Java 安装目录 `/home/webapp/env/java`
- Nginx 安装目录 `/home/webapp/env/nginx`
- 前端端口 `63311`
- 后端端口 `63310`
- 手册补充了系统依赖安装、目录初始化、Java 安装、Nginx 编译安装、配置写入、配置校验、启动与验证步骤
## 路径检查
- 已确认本次新增手册保存路径为 `deploy/2026-03-31-local-nginx-java-install-manual.md`
- 已确认本次实施记录保存路径为 `doc/implementation-report-2026-03-31-local-nginx-java-install-manual.md`
## 验证结果
- 已人工核对手册中的安装路径、端口、Nginx 配置和现有脚本 `bin/prod/install_env.sh`
- 已确认手册内容与以下脚本约定一致:
- `bin/prod/install_env.sh`
- `bin/prod/deploy_release.sh`
- `bin/prod/restart_java.sh`

View File

@@ -0,0 +1,41 @@
# 生产初始化数据库导出后端实施记录
## 本次改动
- 新增生产初始化总脚本 `sql/loan_pricing_prod_init_20260331.sql`
- 直接复用 `sql/ry_20250522.sql` 作为若依基础表和初始化数据来源
- 并入 `sql/loan_pricing_menu.sql` 中的贷款定价菜单初始化内容
- 追加 3 张贷款定价业务表结构:
- `loan_pricing_workflow`
- `model_corp_output_fields`
- `model_retail_output_fields`
## 结构来源
- 业务表最终结构以 `sql/loan_pricing_schema_20260328.sql` 为主来源
- 已核对 `loan_pricing_workflow` 的补字段和注释修正历史脚本,确认总脚本使用的是最终字段版本
## 数据范围
- 保留若依基础初始化数据
- 保留贷款定价功能菜单初始化数据:
- `sys_menu` 中的 `2000``2001``2002`
- `sys_role_menu` 中管理员角色对上述菜单的关联
- 不导出任何贷款定价业务数据
- 未写入 `loan_pricing_workflow``model_corp_output_fields``model_retail_output_fields``INSERT``DELETE` 语句
## 验证结果
- 已完成静态检查,确认 3 张业务表在总脚本中各只定义 1 次
- 已确认总脚本保留若依基础 `sys_user``sys_role``sys_menu` 初始化数据
- 已使用临时验证库导入总脚本并完成计数检查
- 导入后校验结果:
- `sys_menu` 中贷款定价菜单 `2000/2001/2002 = 3`
- `sys_role_menu` 中管理员角色菜单关联 `2000/2001/2002 = 3`
- `sys_user = 2`
- `sys_role = 2`
- `sys_menu = 88`
- `loan_pricing_workflow = 0`
- `model_corp_output_fields = 0`
- `model_retail_output_fields = 0`
- 验证结束后已删除临时验证库

View File

@@ -0,0 +1,16 @@
# 生产初始化数据库导出前端实施记录
## 范围确认
- 已根据 `docs/superpowers/specs/2026-03-31-production-db-init-export-design.md` 确认本次交付物仅为数据库初始化单文件 SQL
- 本次任务不涉及前端页面、接口契约、构建配置或部署产物调整
## 本次结论
- `ruoyi-ui` 工程不存在需要随本次任务修改的页面、接口或构建配置
- 本次前端范围为无代码改动
- 执行过程中应保持 `ruoyi-ui` 目录不变
## 验证
- 已复核本次任务提交范围,前端代码未纳入本次 SQL 导出实现

View File

@@ -0,0 +1,19 @@
# 生产初始化数据库导出计划文档实施记录
## 本次改动
- 新增设计文档 `docs/superpowers/specs/2026-03-31-production-db-init-export-design.md`
- 新增后端实施计划 `docs/superpowers/plans/2026-03-31-production-db-init-export-backend-plan.md`
- 新增前端实施计划 `docs/superpowers/plans/2026-03-31-production-db-init-export-frontend-plan.md`
## 设计结论
- 最终交付物是单一可执行 SQL 文件
- 基础部分直接复用 `sql/ry_20250522.sql`
- 业务增量仅包含 `loan_pricing_workflow``model_corp_output_fields``model_retail_output_fields` 三张表结构
- 不导出任何业务数据
## 说明
- 按仓库规范未开启 subagent
- 本次阶段仅完成设计与实施计划编写,尚未开始生成最终 SQL 文件

View File

@@ -0,0 +1,51 @@
# 生产环境安装与部署脚本实施记录
## 修改内容
- 新增生产环境安装脚本 `bin/prod/install_env.sh`
- 新增生产环境部署脚本 `bin/prod/deploy_release.sh`
- 新增生产环境 Java 管理脚本 `bin/prod/restart_java.sh`
- 两份脚本需要同步放置到生产容器 `/home/webapp` 目录,便于在目标环境直接执行
- 部署脚本改为复用独立的 Java 管理脚本完成后端启停
- 安装脚本固定将 Java 安装到 `/home/webapp/env/java`,将 Nginx 安装到 `/home/webapp/env/nginx`
- 安装脚本会创建 `/home/webapp/loan-pricing` 下的 `backend``frontend``backup``logs``run``tmp` 目录,并写入 Nginx 配置
- 部署脚本约定发布包内必须包含 1 个后端 `jar` 和 1 个 `dist.zip`
- 部署脚本在发布前会备份旧版后端 jar 与旧版前端 `dist` 目录,再完成替换、启动后端和重载 Nginx
- Nginx 前端监听端口固定为 `63311`,后端应用启动端口固定为 `63310`
## 环境勘察结论
- 已连接生产服务器 `116.62.17.81:9444` 并进入 `loan-pricing` 容器核对目录结构
- 容器内实际工作目录为 `/home/webapp`
- 已确认当前容器中存在安装包:
- `/home/webapp/openjdk-21.0.2_linux-aarch64_bin.tar.gz`
- `/home/webapp/nginx-1.20.2.tar.gz`
- 已确认当前容器尚不存在 `/home/webapp/loan-pricing`
- 已确认当前容器当前没有运行中的 Java 或 Nginx 进程
- 当前被勘察容器基础镜像为 Ubuntu但脚本已按需求改为基于 `yum` 安装系统依赖,适配正式生产环境约束
- 已确认当前容器无法直接安装原生 `yum` 包,但系统仓库提供 `dnf` 包,可通过 `dnf` 提供 `yum` 兼容执行入口
## 验证结果
- 已执行 `sh -n bin/prod/install_env.sh`
- 已执行 `sh -n bin/prod/deploy_release.sh`
- 已将两份脚本同步到生产 `loan-pricing` 容器:
- `/home/webapp/install_env.sh`
- `/home/webapp/deploy_release.sh`
- 已将 Java 管理脚本同步到生产 `loan-pricing` 容器:
- `/home/webapp/restart_java.sh`
- 已在容器内执行 `ls -l /home/webapp/install_env.sh /home/webapp/deploy_release.sh /home/webapp/restart_java.sh`,确认三份脚本均已落盘且具备可执行权限
- 已在容器内执行:
- `sh -n /home/webapp/install_env.sh`
- `sh -n /home/webapp/deploy_release.sh`
- `sh -n /home/webapp/restart_java.sh`
三份线上脚本语法校验均已通过
- 已确认 Ubuntu 24.04 仓库中 `yum` 包候选为空,`dnf` 包候选为 `4.14.0-4.1ubuntu1`
- 已在生产 `loan-pricing` 容器执行 `apt-get install -y dnf dnf-plugins-core`
- 已在生产 `loan-pricing` 容器创建 `yum` 兼容入口:
- `/usr/local/bin/yum -> /usr/bin/dnf`
- 已执行 `yum --version`,返回 `4.14.0`
- 已人工核对脚本中的关键路径、端口与部署约束:
- Java 安装目录 `/home/webapp/env/java`
- Nginx 安装目录 `/home/webapp/env/nginx`
- 项目部署目录 `/home/webapp/loan-pricing`
- 前端端口 `63311`
- 后端端口 `63310`
- 由于当前已连接勘察容器为 Ubuntu 24.04,不具备本次脚本要求的 `yum` 安装前提,因此未在该容器直接执行安装流程,仅完成语法校验与逻辑核对

View File

@@ -0,0 +1,15 @@
# AGENTS.md 双计划约束调整实施记录
## 修改内容
- 更新仓库规则文件 `AGENTS.md`
- 将“根据设计文档产出前后端项目的实施计划时,输出两份执行文档”调整为“如果是前后端开发任务,根据设计文档产出实施计划时,输出两份执行文档”
- 明确双计划要求只适用于前后端开发类任务,不再默认覆盖文档类、脚本类或其他非前后端开发任务
## 调整原因
- 用户明确要求将“双 plan”约束收窄为仅对前后端开发任务生效
- 避免后续在纯文档、纯脚本或非前后端开发任务中重复产出不必要的前后端两份计划文档
## 验证结果
- 已检查 `AGENTS.md` 中“文档”章节的规则文字已更新
- 已检查本次实施记录保存路径为 `doc/implementation-report-2026-04-01-agents-plan-rule.md`
- 本次变更仅修改规则文档,未涉及代码或脚本执行

View File

@@ -0,0 +1,18 @@
# 后端启动配置切换为 uat 实施记录
## 修改内容
-`bin/prod/deploy_from_package.sh` 的后端启动 profile 从 `pro` 调整为 `uat`
-`bin/prod/restart_java.sh` 的后端启动 profile 从 `pro` 调整为 `uat`
- 线上宿主机挂载脚本同步改为 `uat`,用于当前容器直接生效
## 原因说明
- 当前 `pro` 配置依赖的数据库地址 `64.127.23.7:3306` 从部署主机与容器内均不可达
- `uat` 配置依赖的数据库地址 `192.168.0.111:40628` 从部署主机可达,满足当前启动条件
## 验证结果
- 已验证宿主机到 `192.168.0.111:40628` 端口连通
- 已在仓库脚本中完成 `uat` 切换
- 已在宿主机挂载脚本 `/volume1/webapp/loan-pricing/deploy_from_package.sh``/volume1/webapp/restart_java.sh` 同步切换为 `uat`
- 已执行容器内命令 `/home/webapp/restart_java.sh restart`
- 已执行 `curl -I http://116.62.17.81:63310/`,返回 `HTTP/1.1 200`
- 已执行 `curl -X POST http://116.62.17.81:63311/prod-api/login/test ...`,返回 `{"code":200,...}`,确认 Nginx 反代与后端 `uat` 启动正常

View File

@@ -0,0 +1,16 @@
# Nginx 目录权限修正实施记录
## 修改内容
- 修正宿主机挂载目录 `/volume1/webapp` 的遍历权限,允许容器内 Nginx worker 访问业务目录
- 修正宿主机挂载目录 `/volume1/webapp/loan-pricing``/volume1/webapp/loan-pricing/frontend``/volume1/webapp/loan-pricing/frontend/dist` 的遍历权限
- 修正宿主机挂载目录 `/volume1/webapp/env``/volume1/webapp/env/nginx``/volume1/webapp/env/nginx/html` 的遍历权限
## 原因说明
- `loan-pricing` 容器内 Nginx worker 进程以 `nobody` 运行
- 宿主机挂载目录此前存在 `d---------` 或缺少其他用户执行权限的情况
- 结果导致容器内访问 `/home/webapp/loan-pricing/frontend/dist/index.html``/home/webapp/env/nginx/html/50x.html` 时出现 `Permission denied`
## 验证结果
- 已执行 `curl -I http://116.62.17.81:63311/`,返回 `HTTP/1.1 200 OK`
- 已执行 `curl http://116.62.17.81:63311/`,成功返回首页 HTML
- 已确认宿主机相关目录权限已调整为可供 Nginx worker 读取和遍历

View File

@@ -0,0 +1,19 @@
# Nginx Worker 用户显式配置实施记录
## 修改内容
-`deploy/nginx.conf` 中显式增加 `user nobody;`
-`bin/prod/install_env.sh` 生成的 Nginx 配置模板中显式增加 `user nobody;`
- 计划将线上实际使用的 `/volume1/webapp/env/nginx/conf/nginx.conf` 同步改为显式 `user nobody;`
## 原因说明
- 当前线上 Nginx 实际以 `nobody` worker 进程运行
- 但配置文件未显式声明 worker 用户,后续重写配置时容易与实际运行态不一致
- 显式声明 `user nobody;` 可以让配置意图与当前运行方式保持一致
## 验证结果
- 已完成仓库配置文件与安装脚本模板修改
- 已同步修改线上实际配置 `/volume1/webapp/env/nginx/conf/nginx.conf`
- 已执行 `nginx -t -c /home/webapp/env/nginx/conf/nginx.conf`,语法校验通过
- 已执行 Nginx reload容器内进程显示 `nobody` 作为 worker 用户运行
- 已执行 `curl -I http://116.62.17.81:63311/`,返回 `HTTP/1.1 200 OK`
- 已执行 `curl http://116.62.17.81:63311/prod-api/login/test`,返回状态码 `200`

View File

@@ -0,0 +1,34 @@
# 生产一键部署脚本后端实施记录
## 修改内容
- 新增生产一键部署脚本 `bin/prod/deploy_from_package.sh`
- 新增部署脚本自测文件 `bin/prod/deploy_from_package_test.sh`
- 脚本内固定 `JAVA_BIN="/home/webapp/env/java/bin/java"`
- 新增脚本同目录唯一发布 zip 校验、包内唯一 `jar` 校验
- 新增旧版后端 `jar` 时间戳备份规则
- 新增后端 PID 文件、托管进程标记、停止旧进程、启动新进程和端口监听校验逻辑
## 实现说明
- 新脚本执行目录固定为脚本所在目录,要求同目录存在:
- `backend/`
- `frontend/`
- 1 个发布 zip
- 后端目标文件固定落到 `backend/ruoyi-admin.jar`
- 旧版后端 `jar` 通过 `ruoyi-admin.jar.<时间戳>.bak` 方式原地备份
- 启动时附加 `-Dloan.pricing.home=<脚本目录>`,用于识别当前脚本托管进程
- PID 文件固定写入 `backend/backend.pid`
- 后端日志固定写入 `backend/backend-console.log`
- 端口监听检测优先使用 `ss`,当前环境没有 `ss` 时改为使用 `lsof` 完成同一条校验
## 验证结果
- 已执行 `sh -n bin/prod/deploy_from_package.sh`,语法校验通过
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测覆盖以下场景:
- 正常部署场景:
- 旧版 `jar` 被重命名为时间戳备份文件
- 新版 `jar` 落到 `backend/ruoyi-admin.jar`
- 后端 PID 文件和日志文件生成成功
- 假后端进程启动成功并监听测试端口
- 异常场景:
- 脚本同目录存在多个发布 zip 时,脚本按预期失败并输出错误信息
- 自测使用临时目录和临时假 `java` 进程,测试结束后已自动清理对应进程和目录

View File

@@ -0,0 +1,30 @@
# 生产一键部署脚本仅识别当前项目 jar 实施记录
## 问题现象
- 进程检测需要确认运行中的 `jar` 包必须是当前项目的正式后端包
## 根因分析
- 之前 `collect_backend_pids()` 使用 `ps -ef` 时,只做了“命令行包含当前项目 jar 路径”的判断
- 这种包含匹配会把以下情况误算为当前项目运行进程:
- `-jar /当前项目/backend/ruoyi-admin.jar.bak`
- 其他仅把正式 jar 路径作为前缀的命令参数
- 结果会导致脚本误报“检测到后端已在运行,请先停止旧进程”
## 修改内容
- 更新 `bin/prod/deploy_from_package.sh`
- `collect_backend_pids()` 继续使用 `ps -ef`
- 但匹配规则改为:
- 命令行中必须存在 `-jar`
-`-jar` 的下一个参数必须严格等于当前项目的 `backend/ruoyi-admin.jar`
- 同时仍要求包含当前脚本的 `-Dloan.pricing.home=<脚本目录>` 标记
- 更新 `bin/prod/deploy_from_package_test.sh`
- 新增自测场景:
-`ps -ef` 中存在 `-jar .../ruoyi-admin.jar.bak`,脚本必须忽略该进程并继续正常部署
## 验证结果
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测结果确认:
- 只有当前项目正式 `backend/ruoyi-admin.jar` 才会被识别为运行中的后端进程
- `.jar.bak` 等非正式后端包不会再误判
- 正常部署链路仍然通过

View File

@@ -0,0 +1,30 @@
# 生产一键部署脚本忽略 defunct 进程实施记录
## 问题现象
- 执行部署脚本时出现报错:
- `检测到后端已在运行,请先停止旧进程`
## 根因分析
- 当前脚本使用 `ps -ef` 收集托管后端进程
- 简化后的实现只要在 `ps -ef` 中匹配到:
- `-Dloan.pricing.home=<脚本目录>`
- `backend/ruoyi-admin.jar`
就会返回对应 PID
- 如果系统中存在已经退出但仍显示为 `<defunct>` 的历史 Java 进程,该 PID 也会被误判为“旧后端仍在运行”
- 随后 `start_backend()` 在启动前再次调用 `collect_backend_pids()`,因此会直接报“检测到后端已在运行,请先停止旧进程”
## 修改内容
- 更新 `bin/prod/deploy_from_package.sh`
-`collect_backend_pids()` 中继续使用 `ps -ef`,但显式忽略包含 `<defunct>` 的进程行
- 更新 `bin/prod/deploy_from_package_test.sh`
- 新增自测场景:
- `ps -ef` 输出中存在匹配当前脚本标记和 jar 路径的 `<defunct>` 进程
- 脚本应忽略该记录并继续正常部署
## 验证结果
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测结果确认:
- 正常部署链路通过
- 多个发布 zip 失败场景通过
- `<defunct>` 进程不会再阻塞新后端启动

View File

@@ -0,0 +1,31 @@
# 生产一键部署脚本设计文档实施记录
## 修改内容
- 新增设计文档 `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
- 设计文档明确本次交付为单脚本自包含部署方案
- 设计文档明确 Java 路径写在脚本内,发布包从脚本同目录读取
- 设计文档明确旧版后端 `jar` 与旧版前端 `dist` 使用时间戳重命名备份
- 设计文档明确后端启停逻辑、PID 管理、端口校验和失败退出规则
- 设计文档明确交付文件边界与验证范围
## 约束确认
- 已按用户确认采用“方案一:单脚本自包含部署”
- 已按用户确认后端启动参数继续沿用 `--spring.profiles.active=pro --server.port=63310`
- 已按用户确认 Java 路径直接写在脚本内
- 已按用户确认部署逻辑全部写在同一个脚本里
## 评审说明
- 仓库 `AGENTS.md` 明确要求“不开启 subagent”
- 因此本次未执行 brainstorming 技能中的 subagent 评审环节,改为人工自检设计文档是否与已确认约束一致
- 已重点核对以下内容:
- 单脚本边界是否与用户要求一致
- 备份方式是否为“重命名 + 时间戳”
- 发布源是否限定为脚本同目录 zip
- 后端端口与 profile 是否与现有生产约束一致
- 设计中未引入额外兼容、补丁或兜底方案
## 验证结果
- 已检查设计文档保存路径为 `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
- 已检查本次实施记录保存路径为 `doc/implementation-report-2026-04-01-production-one-click-deploy-design.md`
- 已人工核对设计文档中的方案对比、设计结论、执行流程、启停规则、失败处理、交付物和验证范围
- 本次变更仅新增文档,未修改脚本或代码,因此未执行运行类验证

View File

@@ -0,0 +1,33 @@
# 生产一键部署脚本前端实施记录
## 修改内容
-`bin/prod/deploy_from_package.sh` 中新增前端 `dist.zip` 唯一校验逻辑
- 新增旧版 `frontend/dist` 时间戳备份规则
- 新增新版 `frontend/dist.zip` 替换逻辑
- 新增前端静态资源解压到 `frontend/dist/` 的逻辑
- 新增 `resolve_frontend_source_dir`,支持从 `dist.zip` 解压结果中定位实际前端根目录
## 范围确认
- 本次前端交付物仅为部署脚本中的静态包部署链路
- 未修改 `ruoyi-ui` 下任何页面、接口、构建配置或打包脚本
- 如后续出现页面需求,需要回到新需求重新做设计和计划
## 实现说明
- 脚本会校验发布包中必须且只能存在 1 个 `dist.zip`
-`frontend/dist` 已存在,则原地重命名为 `dist-<时间戳>`
- 新版前端压缩包统一替换到 `frontend/dist.zip`
- 新版前端资源统一解压到 `frontend/dist/`
- 解压结果支持以下结构:
- 解压根目录直接为前端文件
- 解压后为 `dist/index.html`
- 其他情况下通过 `find index.html` 自动定位前端根目录
## 验证结果
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测覆盖以下前端链路:
- 旧版 `frontend/dist` 被重命名为时间戳备份目录
- 新版 `frontend/dist.zip` 成功替换
- 新版前端资源成功解压到 `frontend/dist/index.html`
- 解压后的页面内容与发布包内容一致
- 已执行 `git status --short ruoyi-ui`
- 已确认 `ruoyi-ui` 本次没有新增或修改的源码文件被纳入改动范围

View File

@@ -0,0 +1,32 @@
# 生产一键部署脚本 netstat 端口检测兼容实施记录
## 问题现象
- 运行 `bin/prod/deploy_from_package.sh` 时出现报错:
- `[2026-04-01 02:45:09] 缺少端口检测命令: ss 或 lsof`
## 根因分析
- 脚本启动后端前会先检查端口检测命令
- 之前的实现只支持 `ss``lsof`
- 用户实际环境中两者都不可用,因此脚本在前置校验阶段直接退出
- 当前仓库开发环境中还存在 `netstat`,说明“只支持 `ss`/`lsof`”不是部署链路本身的要求,而是脚本实现约束过窄
## 修改内容
- 更新 `bin/prod/deploy_from_package.sh`
- 将端口检测命令支持范围从:
- `ss`
- `lsof`
扩展为:
- `ss`
- `lsof`
- `netstat`
- 更新端口检测失败提示文案为“缺少端口检测命令: ss、lsof 或 netstat”
- 更新 `bin/prod/deploy_from_package_test.sh`
- 新增 `netstat` 回退场景自测,验证在 `PATH` 中无 `ss`、无 `lsof`、仅有 `netstat` 时脚本仍可正常完成部署
## 验证结果
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测结果覆盖:
- 正常部署成功
- 多个发布 zip 失败
-`netstat` 可用时,端口监听检测仍然通过

View File

@@ -0,0 +1,31 @@
# 生产一键部署脚本实施计划文档实施记录
## 修改内容
- 新增后端实施计划文档 `docs/superpowers/plans/2026-04-01-production-one-click-deploy-backend-plan.md`
- 新增前端实施计划文档 `docs/superpowers/plans/2026-04-01-production-one-click-deploy-frontend-plan.md`
- 后端计划明确单脚本实现主体由 `bin/prod/deploy_from_package.sh` 承担
- 后端计划明确发布包校验、旧版 jar 备份、PID 管理、端口等待和后端实施记录要求
- 前端计划明确本次不修改 `ruoyi-ui` 源码,只处理部署脚本中的 `dist.zip` 校验、旧版 `dist` 备份、解压与前端实施记录
## 计划拆分说明
- 已根据仓库 `AGENTS.md` 要求,按设计文档产出两份执行文档:
- 一份后端实施计划
- 一份前端实施计划
- 两份计划均基于设计文档 `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
- 后端计划负责脚本主体实现,前端计划负责前端静态包部署链路与无前端源码改动边界确认
## 评审说明
- `writing-plans` 技能要求在计划完成后走 reviewer subagent 评审环节
- 仓库 `AGENTS.md` 明确要求“不开启 subagent”
- 因此本次未开启 plan reviewer subagent改为人工自检以下内容
- 两份计划文件路径是否正确
- 后端计划是否覆盖单脚本实现、验证与实施记录
- 前端计划是否覆盖 `dist.zip``frontend/dist``ruoyi-ui` 无改动边界
- 计划中的提交命令是否使用中文提交信息
## 验证结果
- 已检查后端计划保存路径为 `docs/superpowers/plans/2026-04-01-production-one-click-deploy-backend-plan.md`
- 已检查前端计划保存路径为 `docs/superpowers/plans/2026-04-01-production-one-click-deploy-frontend-plan.md`
- 已人工核对两份计划的 Header、Goal、Architecture、Tech Stack、Task 和 Step 结构
- 已人工核对计划中引用的脚本路径、设计文档路径和实施记录路径与仓库当前目录结构一致
- 本次变更仅新增计划文档与实施记录,未执行脚本实现或运行类验证

View File

@@ -0,0 +1,29 @@
# 生产一键部署脚本改用 ps -ef 识别进程实施记录
## 修改内容
- 更新 `bin/prod/deploy_from_package.sh`
- 将后端进程识别与收集方式从 `pgrep` 改为 `ps -ef`
- 删除脚本对 `pgrep` 命令的前置依赖
- 更新 `bin/prod/deploy_from_package_test.sh`
- 新增断言,要求脚本不能再依赖 `pgrep`,并必须包含 `ps -ef` 进程识别逻辑
## 调整原因
- 用户要求使用 `ps -ef` 判断进程
- 旧实现依赖 `pgrep -f` 收集托管进程,不符合当前要求
## 实现说明
- `is_managed_backend_pid` 现在通过 `ps -ef | awk` 按 PID 读取目标进程行
- `collect_backend_pids` 现在通过 `ps -ef | awk` 同时匹配:
- `-Dloan.pricing.home=<脚本目录>`
- `backend/ruoyi-admin.jar`
- 只有同时满足托管标记和目标 jar 路径的进程才会被纳入停止范围
## 验证结果
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测结果确认:
- 脚本中已不存在 `pgrep`
- 脚本中已使用 `ps -ef`
- 正常部署链路仍然通过
- 多个发布 zip 失败场景仍然通过
- `netstat` 端口检测回退场景仍然通过

View File

@@ -0,0 +1,33 @@
# 生产一键部署脚本参考 deploy.zip 调整实施记录
## 参考压缩包
- 参考文件:`deploy/deploy.zip`
- 已核对压缩包结构:
- `deploy/ruoyi-admin.jar`
- `deploy/dist.zip`
- `__MACOSX/deploy/._ruoyi-admin.jar`
## 问题原因
- 原脚本按 `find ... -name '*.jar'` 统计后端产物
- 参考压缩包中包含 `__MACOSX/deploy/._ruoyi-admin.jar`
- 该文件会被误算成第二个 `jar`,导致脚本报错“后端 jar 数量不正确,期望 1 个,实际 2 个”
## 修改内容
- 更新 `bin/prod/deploy_from_package.sh`
- 在后端 `jar` 和前端 `dist.zip` 搜索时忽略:
- `__MACOSX` 目录下文件
- `._*` 资源分叉文件
- 更新 `bin/prod/deploy_from_package_test.sh`
- 自测发布包结构改为贴近真实 `deploy/deploy.zip`
- 外层为 `deploy/ruoyi-admin.jar`
- 外层为 `deploy/dist.zip`
-`__MACOSX` 资源文件
- 内层 `dist.zip` 也带 `dist/``__MACOSX/`
## 验证结果
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测结果确认:
- 脚本可正确识别参考压缩包结构
- `__MACOSX``._*` 不会再被误判为有效发布产物
- 正常部署链路仍然通过

View File

@@ -0,0 +1,38 @@
# 生产一键部署脚本简化实施记录
## 修改内容
- 简化 `bin/prod/deploy_from_package.sh`
- 删除端口检测逻辑,不再依赖 `ss``lsof``netstat`
- 删除前端解压兼容逻辑,不再探测多种 `dist.zip` 目录结构
- 保留并简化进程识别逻辑,直接使用 `ps -ef`
- 简化 `bin/prod/deploy_from_package_test.sh`
- 删除端口监听断言和端口检测回退场景
- 新增脚本文本断言,确认已移除端口检测和解压兼容 helper
## 当前脚本边界
- 仍然保留以下核心能力:
- 脚本同目录唯一发布 zip 校验
- 发布包内唯一 `jar` 和唯一 `dist.zip` 校验
- 旧版后端 `jar` 时间戳备份
- 旧版 `frontend/dist` 时间戳备份
- 使用 `ps -ef` 停止旧后端进程
- 替换新 `jar`
-`dist.zip` 直接解压到 `frontend/`
- 启动新的 Java 进程并写入 PID 文件
## 删除的复杂逻辑
- 不再等待端口监听成功
- 不再兼容 `ss``lsof``netstat` 三种端口检测方式
- 不再兼容 `dist.zip` 根目录、`dist/index.html` 和自动 `find index.html` 多种结构
- 当前前端解压只接受一种约定:
- `dist.zip` 解压到 `frontend/` 后必须得到 `frontend/dist/`
## 验证结果
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 自测确认:
- 脚本使用 `ps -ef`
- 脚本中已移除端口检测 helper
- 脚本中已移除前端解压兼容 helper
- 正常部署链路仍然通过
- 多个发布 zip 失败场景仍然通过

View File

@@ -0,0 +1,14 @@
# 2026-04-03 登录页默认账号密码移除实施记录
## 修改内容
- 移除登录页 `ruoyi-ui/src/views/login.vue` 中硬编码的默认账号 `admin` 与默认密码 `admin123`
- 保留 `getCookie()` 现有逻辑,确保用户勾选“记住密码”后仍可通过 cookie 回填登录信息。
- 新增前端回归脚本 `ruoyi-ui/tests/login-default-credentials.test.js`,校验默认值为空且 cookie 回填逻辑未被破坏。
## 验证记录
- 变更前执行 `node tests/login-default-credentials.test.js`,断言“登录页默认用户名应为空字符串”失败,证明问题可复现。
- 变更后再次执行 `node tests/login-default-credentials.test.js`,预期全部断言通过。
- 启动前端页面后在浏览器访问登录页,确认账号、密码输入框默认不再预填内容。
## 影响范围
- 仅影响登录页初始化展示行为,不修改登录接口、加密逻辑、验证码逻辑与记住密码逻辑。

View File

@@ -0,0 +1,24 @@
# 生产后端重启脚本实施记录
## 修改内容
- 收敛生产后端重启脚本 `bin/prod/restart_java.sh`
- 脚本固定面向已部署的 `backend/ruoyi-admin.jar` 执行启停,不再包含构建逻辑
- 后端启动 profile 固定为 `pro`
- Java 路径统一为 `/home/webapp/env/java/bin/java`,与现有生产安装脚本保持一致
- 移除 `root` 执行校验与端口监听校验,只保留 `start|stop|restart|status` 所需的最小启停逻辑
- 新增脚本自测文件 `bin/prod/restart_java_test.sh`
## 实现说明
- `start` 仅检查 Java 可执行文件、目标 jar 是否存在以及当前是否已有同脚本托管进程
- `stop` 继续基于 PID 文件和 `-Dloan.pricing.home=/home/webapp/loan-pricing` 进程标记识别并停止当前后端进程
- `restart` 按“先停后起”执行,适用于生产环境已部署 jar 的直接重启
- `status` 仅返回脚本托管进程状态,不再增加端口占用类附加判断
## 验证结果
- 已执行 `sh bin/prod/restart_java_test.sh`
- 已验证以下场景:
- 脚本固定使用 `/home/webapp/env/java`
- 脚本固定使用 `--spring.profiles.active=pro`
- 脚本不包含 `mvn``require_root``ss/lsof/netstat` 相关依赖
- `start -> status -> restart -> stop` 流程执行通过
- 自测使用临时目录中的假 `java` 进程完成,测试结束后已自动清理对应进程和临时目录

View File

@@ -0,0 +1,72 @@
# 个人模型详情缺失展示字段补齐实施记录
## 实施时间
- 2026-04-03
## 修改内容
- 补齐个人详情页“业务信息”中的 `借款期限`
- 补齐个人模型输出“测算结果”中的 5 个字段:
- `历史利率`
- `产品最低利率下限`
- `平滑幅度`
- `最终测算利率`
- `参考利率`
- 在后端 `ModelRetailOutputFields` 中新增对应 5 个字段定义
- 在零售模型 mock 数据中补齐对应 5 个字段样例值
- 新增零售模型输出表结构迁移脚本,并同步更新建表基线 SQL
- 新增后端字段断言测试与前端源码断言脚本
## 修改文件
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
- `ruoyi-loan-pricing/src/main/resources/data/retail_output.json`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java`
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- `ruoyi-ui/tests/retail-display-fields.test.js`
- `ruoyi-ui/package.json`
- `sql/add_model_retail_output_rate_fields_20260403.sql`
- `sql/model_retail.sql`
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
- `doc/2026-04-03-retail-display-fields-design.md`
- `doc/2026-04-03-retail-display-fields-backend-plan.md`
- `doc/2026-04-03-retail-display-fields-frontend-plan.md`
- `doc/implementation-report-2026-04-03-retail-display-fields.md`
## 验证方式
1. 新增后端测试,断言 `ModelRetailOutputFields` 包含 5 个新增字段,先失败后通过
2. 新增前端源码断言脚本,断言个人详情页与模型输出页已补齐字段,先失败后通过
3. 执行前端生产构建,确认页面代码可正常打包
4. 检查开发库 `model_retail_output_fields` 表结构,确认最初缺少 5 个新列
5. 执行 `sql/add_model_retail_output_rate_fields_20260403.sql` 到开发库,并再次确认 5 个新列存在
6. 重新编译并重启后端,确保新的实体字段已进入运行中的 SQL 映射
7. 创建新的个人流程 `20260403100514909`,调用详情接口确认返回以下真实值:
- `loanRateHistory = 6.40`
- `minRateProduct = 5.50`
- `smoothRange = -0.10`
- `finalCalculateRate = 6.05`
- `referenceRate = 5.95`
8. 启动前端开发服务并使用浏览器自动化打开详情页,确认:
- 页面出现 `借款期限`
- 切换到“测算结果”页签后5 个新增字段及对应值均可见
9. 验证结束后,停止本次启动的前后端进程
## 验证结果
- `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest test` 首次失败,补齐后通过
- `npm --prefix ruoyi-ui run test:retail-display-fields` 首次失败,补齐后通过
- `npm --prefix ruoyi-ui run build:prod` 成功,输出包含 `Build complete.`
- 已确认开发库 `model_retail_output_fields` 初始缺少:
- `loan_rate_history`
- `min_rate_product`
- `smooth_range`
- `final_calculate_rate`
- `reference_rate`
- 已执行迁移脚本并确认以上 5 列存在
- 已确认旧后端进程因未加载最新依赖导致 SQL 仍缺新列,重编译并重启后问题消失
- 已创建个人流程 `20260403100514909` 并通过详情接口拿到 5 个新增字段的真实值
- 已通过浏览器自动化确认个人详情页展示位与“测算结果”页签展示均正确
- 本次验证期间启动的前后端进程均已停止
## 说明
- `loanTerm` 本次仅补齐详情页展示位;个人创建表单当前无该字段录入入口,不属于本次“模型返回字段更新”范围
- 为保证新字段在新环境中也可正常落库,本次同步更新了零售模型输出表的建表基线 SQL

View File

@@ -0,0 +1,30 @@
# 2026-04-09 默认切换 Node 25 以支持 Playwright 实施记录
## 变更内容
-`nvm` 默认别名从 `14` 调整为 `25`
- 清理了本次验证过程中残留的 Playwright 浏览器安装进程
## 执行命令
- `zsh -lic 'nvm alias default 25'`
## 变更结果
- 新开的交互式 `zsh` 默认 Node 版本变为 `v25.9.0`
- 默认 npm/npx 版本变为 `11.12.1`
## 验证结果
- `zsh -lic 'node -v && npm -v && npx -v && nvm current && nvm alias default'`
- `node v25.9.0`
- `npm 11.12.1`
- `npx 11.12.1`
- `nvm current = v25.9.0`
- `default -> 25 (-> v25.9.0 *)`
- `zsh -lic '... playwright_cli.sh --help'`
- Playwright CLI 帮助输出正常
- `zsh -lic '... playwright_cli.sh --session verify-default-25 open https://example.com && snapshot && close && list'`
- 页面成功打开
- 页面标题为 `Example Domain`
- 快照成功输出
- 浏览器关闭后 `list` 返回 `(no browsers)`
## 结论
- 默认 shell 环境下已可直接使用 Playwright无需再先手动执行 `nvm use 25`

View File

@@ -0,0 +1,30 @@
# 2026-04-09 安装 Node 25 与 Node 14 实施记录
## 变更内容
- 使用 `nvm` 安装 `node v25.9.0`
- 使用 `nvm` 安装 `node v14.21.3`
- 调整 `/Users/wkc/.npmrc`,删除与 `nvm` 冲突的 `prefix=~/.npm-global`
- 保留 npm 镜像配置:`registry=https://registry.npmmirror.com`
## 处理过程
- `node 25` 通过 `nvm` 正常安装成功
- `node 14` 在 Apple Silicon 原生环境下无法直接下载 `darwin-arm64` 安装包
- 原生源码编译 `node 14` 失败,错误来自当前 macOS Command Line Tools/SDK 与旧版 `node 14` 源码不兼容
- 改为通过 Rosetta 以 `x64` 方式安装 `node 14`,安装成功
## 验证结果
- `zsh -lic 'nvm use 25 && node -v && npm -v'` 验证结果:
- `node v25.9.0`
- `npm 11.12.1`
- `zsh -lic 'nvm use 14 && node -v && npm -v'` 验证结果:
- `node v14.21.3`
- `npm 6.14.18`
- `arch -x86_64 /bin/zsh -lic 'nvm use 14 && node -v && npm -v'` 验证结果:
- `node v14.21.3`
- `npm 6.14.18`
- 新开的交互式 `zsh` 默认版本:
- `node v14.21.3`
- `npm 6.14.18`
## 备注
- `nvm` 当前默认别名已指向 `14`

View File

@@ -0,0 +1,25 @@
# 2026-04-09 Node 卸载与 nvm 安装实施记录
## 变更内容
- 卸载了 Homebrew 安装的 `node 25.8.1_1`
- 删除了本地目录 `/Users/wkc/.local/node-v14.21.3-darwin-x64`
- 更新了 `/Users/wkc/.zshrc`
- 安装了 `nvm 0.40.4`
## shell 配置调整
- 删除旧配置:`export PATH="/Users/wkc/.local/node-v14.21.3-darwin-x64/bin:$PATH"`
- 新增 `nvm` 初始化配置:
```sh
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"
```
## 验证结果
- 交互式 `zsh``nvm --version` 返回 `0.40.4`
- `node` 命令已不存在,说明当前环境中已无非 `nvm` 管理的 Node 版本
- `nvm ls` 返回 `N/A`,说明尚未通过 `nvm` 安装任何 Node 版本
## 备注
- `brew uninstall node` 过程中触发了 Homebrew 自动移除若干仅供该版本 Node 使用的依赖库

View File

@@ -0,0 +1,25 @@
# 证件输入校验移除实施记录
## 实施时间
- 2026-04-09
## 修改内容
- 移除个人新增弹窗中的证件号码格式校验
- 移除企业新增弹窗中的证件号码格式校验
- 两个新增弹窗的证件号码规则统一保留为必填校验
- 新增前端源码断言,约束后续不再恢复证件号码格式校验
## 修改文件
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- `ruoyi-ui/tests/id-number-validation-removal.test.js`
- `ruoyi-ui/package.json`
- `doc/implementation-report-2026-04-09-remove-id-number-validation.md`
## 验证方式
1. `npm --prefix ruoyi-ui run test:id-number-validation-removal`
2. `npm --prefix ruoyi-ui run build:prod`
## 说明
- 本次移除的是前端证件号码格式校验,不影响证件号码必填约束
- 后端本次未新增或调整证件号码格式校验逻辑

View File

@@ -0,0 +1,26 @@
# 上虞个人利率测算输入参数文档产出记录
## 实施时间
- 2026-04-09
## 修改内容
- 新增个人利率测算输入参数对齐设计文档
- 新增个人利率测算输入参数前端实施计划
- 新增个人利率测算输入参数后端实施计划
- 按 Excel `入参` sheet 整理个人新增弹窗字段获取方式和模型调用参数来源
- 明确 `loanTerm` 使用固定年限下拉,选项按 Excel 组织
- 明确系统字段 `serialNum``orgCode``runType``custType` 继续自动带值
- 明确个人开关字段在模型调用层转换为 `0/1`
- 补充确认后端最终发给模型的 16 个输入参数、来源、请求格式与验证口径
## 修改文件
- `doc/2026-04-09-shangyu-retail-input-params-design.md`
- `doc/2026-04-09-shangyu-retail-input-params-frontend-plan.md`
- `doc/2026-04-09-shangyu-retail-input-params-backend-plan.md`
- `doc/implementation-report-2026-04-09-shangyu-retail-input-params-plans.md`
## 说明
- 设计文档保存路径已核对为项目现有的 `doc/` 目录
- 按项目要求,本次实施计划拆分为前端与后端两份文档
- 由于仓库约束为“不开启 subagent”文档评审环节未使用子代理后续执行时将在当前会话内推进
- 本次仅产出设计与计划文档,尚未进入代码实施阶段

View File

@@ -0,0 +1,93 @@
# 上虞个人利率测算输入参数对齐实施记录
## 实施时间
- 2026-04-09
## 修改内容
- 个人新增弹窗补齐 `loanPurpose``loanTerm`
- 个人新增弹窗 `loanTerm` 固定为 `1-6`
- 个人新增弹窗 `collType` 选项统一为 `一类/二类/三类`
- 个人新增弹窗开关字段提交值由 `true/false` 调整为 `1/0`
- 个人详情页补齐 `贷款用途` 展示
- 个人与企业详情、模型输出布尔展示兼容 `1/0`
- 后端个人创建 DTO 补齐 `loanPurpose``loanTerm`
- 后端个人 DTO 到流程实体映射补齐 `loanPurpose``loanTerm`
- 后端模型调用 DTO 补齐 `loanTerm``loanLoop`
- 后端个人模型调用前统一将 `bizProof``loanLoop``collThirdParty` 规范为 `0/1`
- `orgCode` 统一为 `892000`
- `ModelInvokeDTO` 注释、接口文档、SQL 基线和迁移脚本同步统一为 `892000`
- 新增前端源码断言与后端单元测试
## 修改文件
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- `ruoyi-ui/tests/personal-create-input-params.test.js`
- `ruoyi-ui/package.json`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- `doc/api/loan-pricing-workflow-api.md`
- `sql/loan_pricing_workflow.sql`
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
- `sql/fix_comments.sql`
- `sql/fix_all_comments.sql`
- `sql/update_org_code_default_20260409.sql`
- `doc/2026-04-09-shangyu-retail-input-params-design.md`
- `doc/2026-04-09-shangyu-retail-input-params-frontend-plan.md`
- `doc/implementation-report-2026-04-09-shangyu-retail-input-params.md`
## 数据库处理
1. 执行 `sql/update_org_code_default_20260409.sql`
2.`loan_pricing_workflow.org_code` 默认值修改为 `892000`
3. 将存量 `loan_pricing_workflow.org_code``892000` 的记录统一更新为 `892000`
## 验证方式
1. 前端源码断言:
- `npm --prefix ruoyi-ui run test:personal-create-input-params`
- `npm --prefix ruoyi-ui run test:retail-display-fields`
2. 后端单元测试:
- `mvn -pl ruoyi-loan-pricing -Dtest=LoanPricingModelServiceTest,LoanPricingModelServicePersonalParamsTest test`
3. 前端构建:
- `npm --prefix ruoyi-ui run build:prod`
4. 数据库验证:
- 查询 `loan_pricing_workflow.org_code` 字段默认值
- 查询存量数据中是否仍存在非 `892000` 记录
5. 接口验证:
- `/login/test` 获取 token
- `POST /loanPricing/workflow/create/personal` 正常场景
- `POST /loanPricing/workflow/create/personal` 缺少 `loanPurpose` 场景
- `POST /loanPricing/workflow/create/personal` 分支值场景
- `GET /loanPricing/workflow/{serialNum}` 验证回显
6. 页面验证:
- 启动前端 dev server
- 使用浏览器打开流程列表页
- 校验新增弹窗下拉选项
- 页面创建个人流程并打开详情页确认回显
## 验证结果
- `npm --prefix ruoyi-ui run test:personal-create-input-params` 通过
- `npm --prefix ruoyi-ui run test:retail-display-fields` 通过
- `mvn -pl ruoyi-loan-pricing -Dtest=LoanPricingModelServiceTest,LoanPricingModelServicePersonalParamsTest test` 通过
- `npm --prefix ruoyi-ui run build:prod` 通过,输出 `Build complete.`
- 数据库验证结果:
- `loan_pricing_workflow.org_code` 默认值为 `892000`
- 存量非 `892000` 记录数为 `0`
- 接口验证结果:
- 正常场景创建成功,返回 `orgCode=892000`,并持久化 `loanPurpose``loanTerm`
- 缺少 `loanPurpose` 时返回 `贷款用途不能为空`
- 分支场景详情回显 `bizProof=0``loanLoop=1``collThirdParty=0`
- 页面验证结果:
- 新增弹窗显示 `贷款用途`
- 借款期限下拉仅包含 `1-6`
- 抵质押类型下拉为 `一类/二类/三类`
- 页面创建流程成功后,详情页展示 `贷款用途=经营``借款期限=6`
## 说明
- 浏览器验证使用系统 `Google Chrome.app`
- 本次验证期间启动的后端、前端和浏览器进程已在任务结束前关闭

View File

@@ -0,0 +1,26 @@
# 启动脚本进程判断改为 ps -ef 实施记录
## 修改内容
-`bin/prod/restart_java.sh` 中的后端进程收集逻辑由 `pgrep -f` 改为 `ps -ef | awk`
-`bin/restart_java_backend.sh` 中的后端进程收集逻辑由 `pgrep -f` 改为 `ps -ef | awk`
- 删除 `bin/restart_java_backend.sh` 中对 `pgrep` 命令的依赖校验
- 更新 `bin/prod/restart_java_test.sh`,补充 `ps -ef` / `pgrep` 约束校验,并修正测试夹具中的 JDK 目录
- 新增 `bin/restart_java_backend_test.sh`,校验本地后端重启脚本已改用 `ps -ef`
## 实现说明
- 两份脚本都只在 `ps -ef` 结果中匹配同时满足“包含脚本标记参数”和“`-jar` 指向目标 jar”这两个条件的 Java 进程
- 进程筛选时继续忽略 `<defunct>` 记录,避免误判僵尸进程
- 现有 PID 文件校验逻辑保持不变,本次只收敛“扫描当前是否已有进程”的实现方式
## 路径检查
- 已确认本次实施记录保存路径为 `doc/implementation-report-2026-04-09-start-script-ps-ef.md`
## 验证结果
- 已执行 `sh bin/prod/restart_java_test.sh`
- 已执行 `sh bin/restart_java_backend_test.sh`
- 已执行 `sh -n bin/prod/restart_java.sh && sh -n bin/restart_java_backend.sh`
- 已确认测试中拉起的假 Java 进程在脚本收尾阶段自动停止并清理

View File

@@ -0,0 +1,37 @@
# 2026-04-10 登录 Shell 默认使用 Node 25 实施记录
## 变更内容
- 保持 `nvm` 默认别名为 `25`
-`~/.zprofile` 中补充 `nvm` 初始化,并在登录 shell 启动时自动执行 `nvm use default`
## 根因分析
- `nvm alias default 25` 已经存在,但仅在交互式 shell 中可用
- `zsh -lc` 启动的是登录非交互 shell不会读取 `~/.zshrc`
- 因此这类场景下 `node``npm``npx` 未进入 PATH表现为 `npx` 启动失败
## 修改文件
- `~/.zprofile`
- `doc/implementation-report-2026-04-10-login-shell-default-node25.md`
## 验证项
- 验证登录 shell 在不手动执行 `nvm use` 的情况下可直接识别 `node`
- 验证登录 shell 在不手动执行 `nvm use` 的情况下可直接识别 `npx`
- 验证 `nvm` 默认别名仍然指向 `25`
## 执行命令
- `zsh -lc 'nvm alias default 25'`
- `zsh -lc 'echo NODE=$(node -v); echo NPM=$(npm -v); echo NPX=$(npx -v); echo NODE_PATH=$(command -v node); echo NPX_PATH=$(command -v npx); echo NVM_CURRENT=$(nvm current); echo NVM_ALIAS=$(nvm alias default | tail -n 1)'`
## 验证结果
- `nvm` 默认别名输出为 `default -> 25 (-> v25.9.0 *)`
- 登录 shell 输出 `NODE=v25.9.0`
- 登录 shell 输出 `NPM=11.12.1`
- 登录 shell 输出 `NPX=11.12.1`
- 登录 shell 输出 `NODE_PATH=/Users/wkc/.nvm/versions/node/v25.9.0/bin/node`
- 登录 shell 输出 `NPX_PATH=/Users/wkc/.nvm/versions/node/v25.9.0/bin/npx`
- 登录 shell 输出 `NVM_CURRENT=v25.9.0`
- 登录 shell 输出 `NVM_ALIAS=default -> 25 (-> v25.9.0 *)`
## 结论
- `zsh -lc` 场景下已默认切换到 Node `25.9.0`
- `npx` 在登录 shell 中已可直接使用,无需先手动执行 `nvm use 25`

View File

@@ -0,0 +1,19 @@
# 个人流程最终测算利率展示实施记录
## 修改时间
- 2026-04-11
## 修改内容
- 调整流程列表后端查询:个人客户列表“测算利率”改为读取 `model_retail_output_fields.final_calculate_rate`
- 调整个人流程详情左侧展示:标签改为“最终测算利率”,显示字段改为 `retailOutput.finalCalculateRate`
- 调整个人流程详情后端组装:`loanPricingWorkflow.loanRate` 改为取个人模型输出的 `finalCalculateRate`
## 影响范围
- 个人流程列表
- 个人流程详情左侧关键信息
- 企业流程列表与详情保持现状,不做改动
## 验证计划
- 后端单元测试验证个人详情取 `finalCalculateRate`
- 后端静态测试验证列表 SQL 的个人分支查询 `mr.final_calculate_rate`
- 前端静态测试验证个人详情展示 `finalCalculateRate`

View File

@@ -0,0 +1,17 @@
# 流程详情卡片顺序调整实施记录
## 修改时间
- 2026-04-11
## 修改内容
- 调整个人流程详情页右侧卡片顺序,将模型输出卡片移动到流程详情卡片上方
- 调整企业流程详情页右侧卡片顺序,将模型输出卡片移动到流程详情卡片上方
- 新增前端静态校验,约束个人与企业详情组件的卡片顺序
## 影响范围
- 个人流程详情页面布局
- 企业流程详情页面布局
## 验证计划
- 执行前端静态测试,确认卡片顺序断言通过
- 启动前端页面并在浏览器中检查个人、企业详情页卡片顺序

View File

@@ -0,0 +1,38 @@
# 上虞对公利率测算字段对齐实施记录
## 修改时间
- 2026-04-16
## 修改内容
- 对齐对公创建请求字段,新增 `repayMethod`,将 `isTradeConstruction` 统一为 `isTradeBuildEnt`
- 对齐企业详情返回与页面展示,左侧主利率改为 `finalCalculateRate`
- 对齐对公模型输出字段,补齐 `loanRateHistory``minRateProduct``smoothRange``finalCalculateRate``referenceRate`
- 裁剪企业模型输出和页面展示,不再暴露 `isAgriGuar``midEntTax``cardOverdue`
- 对公新增弹窗中的 `贷款期限(年)` 调整为下拉框,选项固定为 `1-10`
- 更新企业 mock 返回和 SQL 基线、迁移脚本
## 文档与脚本
- `doc/2026-04-16-shangyu-corporate-alignment-backend-plan.md`
- `doc/2026-04-16-shangyu-corporate-alignment-frontend-plan.md`
- `sql/2026-04-16-shangyu-corporate-alignment.sql`
## 验证记录
- 后端单测:
- `mvn -pl ruoyi-loan-pricing -Dtest=ModelCorpOutputFieldsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test`
- 结果13 个测试全部通过
- 前端静态断言:
- `zsh -lic 'nvm use default >/dev/null && npm --prefix ruoyi-ui run test:corporate-create-input-params'`
- `zsh -lic 'nvm use default >/dev/null && npm --prefix ruoyi-ui run test:corporate-display-fields'`
- 结果:两个脚本均通过
- 前端构建:
- `zsh -lic 'nvm use default >/dev/null && npm --prefix ruoyi-ui run build:prod'`
- 结果:构建成功,仅有体积告警
- 接口联调:
- 使用 `/login/test` 获取 token
- 验证了对公创建正常场景、缺少 `repayMethod` 的参数错误场景、`分期/不分期``1/0` 分支场景
- 详情接口确认返回新增字段,且 `loanPricingWorkflow.loanRate = modelCorpOutputFields.finalCalculateRate`
- 浏览器联调:
- 启动前端开发服务并打开流程列表
- 验证对公新增弹窗字段、选项、提交流程
- 验证创建后列表新增记录
- 验证企业详情页出现 `最终测算利率``还款方式``贸易和建筑业企业``历史利率``产品最低利率下限``平滑幅度``参考利率`

View File

@@ -0,0 +1,21 @@
# 流程详情页模型输出平铺展示实施记录
## 改动日期
- 2026-04-16
## 改动范围
- 前端:`ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- 前端测试:`ruoyi-ui/tests/model-output-flat-display.test.js`
- 前端脚本:`ruoyi-ui/package.json`
## 改动内容
- 取消流程详情页“模型输出”区域的 Tab 切换结构。
- 保留原有分组顺序与字段内容,将“基本信息”“忠诚度分析”“贡献度分析”等分组改为自上而下平铺展示。
- 按最新要求,将“测算结果”分组前移到“基本信息”下方,优先展示最终测算相关结果。
- 按最新要求,将“测算结果”中的“最终测算利率”调整到最后一行展示。
- 移除组件内仅用于 Tab 默认选中的 `activeTab` 和相关监听逻辑。
- 新增最小回归测试,校验模型输出组件不再包含 `el-tabs``el-tab-pane`,并具备平铺分组区块,同时校验“基本信息”后紧跟“测算结果”。
## 验证计划
- 使用 `nvm` 显式切换前端 Node 版本后执行 `npm run test:model-output-flat-display`
- 启动前端页面,在浏览器中打开流程详情页,确认模型输出区域已按分组平铺展示,且不再出现 Tab 切换。

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,344 @@
# Loan Pricing Sensitive Data Encryption Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让贷款定价流程在后端对 `custName``idNum` 实现密文存储,并保证列表、详情、模型输出基本信息、模型调用链路在各自边界内完成脱敏或解密。
**Architecture:** 后端在 `ruoyi-loan-pricing` 模块内新增贷款定价专用敏感字段加解密服务和展示脱敏服务,固定密钥从配置读取。`LoanPricingWorkflowServiceImpl` 在创建、列表、详情和模型输出展示链路显式接入这些服务,`LoanPricingModelService` 在调模型前显式解密,避免把密文错误透传给模型。
**Tech Stack:** Spring Boot、MyBatis Plus、JUnit 5、Mockito、Maven、JDK `javax.crypto`
---
### Task 1: 搭建敏感字段加解密与脱敏基础设施
**Files:**
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayService.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayServiceTest.java`
- Modify: `ruoyi-admin/src/main/resources/application.yml`
- [ ] **Step 1: 写加解密与脱敏失败测试**
新增两个测试文件,至少覆盖以下场景:
```java
@Test
void shouldEncryptAndDecryptCustNameAndIdNum()
{
String cipher = service.encrypt("张三");
assertNotEquals("张三", cipher);
assertEquals("张三", service.decrypt(cipher));
}
@Test
void shouldRejectBlankKeyConfiguration()
{
SensitiveFieldCryptoService service = new SensitiveFieldCryptoService("");
assertThrows(IllegalStateException.class, () -> service.encrypt("张三"));
}
```
```java
@Test
void shouldMaskPersonalNameAndIdNum()
{
assertEquals("张*", displayService.maskCustName("张三"));
assertEquals("1101********1234", displayService.maskIdNum("110101199001011234"));
}
@Test
void shouldMaskCorporateNameAndCreditCode()
{
assertEquals("测试****公司", displayService.maskCustName("测试科技有限公司"));
assertEquals("91*************00X", displayService.maskIdNum("91110000100000000X"));
}
```
- [ ] **Step 2: 运行基础测试确认当前失败**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL提示测试类或对应服务不存在。
- [ ] **Step 3: 增加配置项和最小实现**
`application.yml` 增加贷款定价敏感字段配置,例如:
```yaml
loan-pricing:
sensitive:
key: "1234567890abcdef"
```
创建加解密服务与脱敏服务,最小实现至少包含:
```java
@Service
public class SensitiveFieldCryptoService
{
public String encrypt(String plainText) { ... }
public String decrypt(String cipherText) { ... }
}
```
```java
@Service
public class LoanPricingSensitiveDisplayService
{
public String maskCustName(String custName) { ... }
public String maskIdNum(String idNum) { ... }
}
```
- [ ] **Step 4: 重新运行基础测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayService.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayServiceTest.java ruoyi-admin/src/main/resources/application.yml
git commit -m "新增贷款定价敏感字段加解密服务"
```
### Task 2: 接入流程创建与列表查询链路
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVO.java`
- Modify: `ruoyi-loan-pricing/src/main/resources/mapper/loanpricing/LoanPricingWorkflowMapper.xml`
- Modify: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: 写服务层失败测试,约束创建时入库加密、列表返回脱敏、查询按客户内码**
`LoanPricingWorkflowServiceImplTest` 增加至少 3 个用例:
```java
@Test
void shouldEncryptCustNameAndIdNumBeforeInsert() { ... }
@Test
void shouldMaskCustNameWhenReturningPagedWorkflowList() { ... }
@Test
void shouldUseCustIsnInsteadOfCustNameAsQueryCondition() { ... }
```
关键断言至少包括:
```java
verify(loanPricingWorkflowMapper).insert(argThat(entity ->
!Objects.equals(entity.getCustName(), "张三")
&& !Objects.equals(entity.getIdNum(), "110101199001011234")));
assertEquals("张*", result.getRecords().get(0).getCustName());
```
- [ ] **Step 2: 运行服务测试确认失败**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL当前创建逻辑尚未加密列表链路尚未脱敏查询条件仍包含 `custName`
- [ ] **Step 3: 在创建链路显式加密敏感字段**
`LoanPricingWorkflowServiceImpl#createLoanPricing` 中补最小实现:
```java
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum()));
loanPricingWorkflowMapper.insert(loanPricingWorkflow);
```
要求:
- 只加密 `custName``idNum`
- `custIsn` 保持原样
- 配置缺失时直接失败,不增加明文兼容分支
- [ ] **Step 4: 收口列表查询条件并补脱敏返回**
修改点至少包含:
```java
if (StringUtils.hasText(loanPricingWorkflow.getCustIsn()))
{
wrapper.like(LoanPricingWorkflow::getCustIsn, loanPricingWorkflow.getCustIsn());
}
```
```java
pageResult.getRecords().forEach(row ->
row.setCustName(loanPricingSensitiveDisplayService.maskCustName(
sensitiveFieldCryptoService.decrypt(row.getCustName()))));
```
同步从 `buildQueryWrapper``LoanPricingWorkflowMapper.xml` 删除 `custName` 过滤条件。
- [ ] **Step 5: 重新运行服务测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 6: 补充实体与 VO 边界说明性调整**
若测试或编译需要,在 `LoanPricingWorkflow``LoanPricingWorkflowListVO` 中补充本次链路使用的字段,并保持对象语义清晰;不要新增明文副本字段。
- [ ] **Step 7: 提交本任务**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVO.java ruoyi-loan-pricing/src/main/resources/mapper/loanpricing/LoanPricingWorkflowMapper.xml ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "接入流程敏感字段加密与列表脱敏"
```
### Task 3: 接入详情返回、模型输出展示与模型调用链路
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowVO.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- Modify: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: 写详情、模型输出展示与模型调用失败测试**
新增或补充以下测试场景:
```java
@Test
void shouldMaskCustNameAndIdNumWhenReturningWorkflowDetail() { ... }
```
```java
@Test
void shouldMaskCustNameAndIdNumInRetailModelOutputBasicInfo() { ... }
```
```java
@Test
void shouldMaskCustNameAndIdNumInCorporateModelOutputBasicInfo() { ... }
```
```java
@Test
void shouldDecryptCustNameAndIdNumBeforeInvokeModel() { ... }
```
关键断言至少包括:
```java
assertEquals("张*", result.getLoanPricingWorkflow().getCustName());
assertEquals("1101********1234", result.getLoanPricingWorkflow().getIdNum());
assertEquals("张*", result.getModelRetailOutputFields().getCustName());
assertEquals("1101********1234", result.getModelRetailOutputFields().getIdNum());
verify(modelService).invokeModel(argThat(dto ->
Objects.equals("张三", dto.getCustName())
&& Objects.equals("110101199001011234", dto.getIdNum())));
```
- [ ] **Step 2: 运行详情与模型测试确认失败**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL当前详情返回未完整脱敏模型输出“基本信息”仍会返回明文模型调用前也未解密。
- [ ] **Step 3: 在详情返回前显式解密再脱敏**
`selectLoanPricingBySerialNum` 中加入类似处理:
```java
String plainCustName = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName());
String plainIdNum = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum());
loanPricingWorkflow.setCustName(loanPricingSensitiveDisplayService.maskCustName(plainCustName));
loanPricingWorkflow.setIdNum(loanPricingSensitiveDisplayService.maskIdNum(plainIdNum));
```
要求:
- 对外返回对象中不保留明文
- 保持既有测算利率与执行利率逻辑不变
-`modelRetailOutputFields``modelCorpOutputFields` 非空,同样对其 `custName``idNum` 做脱敏替换
- [ ] **Step 4: 在模型调用前显式解密**
`LoanPricingModelService#invokeModelAsync` 中补最小实现:
```java
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum()));
BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO);
```
要求:
- 只解密 `custName``idNum`
- 解密失败直接中断模型调用并记录错误
- 不修改模型输出表处理逻辑
- [ ] **Step 5: 重新运行详情与模型测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 6: 运行贷款定价模块回归测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS若有失败先区分是否为既有问题再决定是否继续扩测。
- [ ] **Step 7: 提交本任务**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowVO.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "接入流程详情脱敏与模型调用解密"
```
### Task 4: 补数据库脚本与后端实施记录
**Files:**
- Create: `sql/clear_loan_pricing_workflow_history.sql`
- Create: `doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md`
- [ ] **Step 1: 新增历史数据清理脚本**
创建脚本,至少包含:
```sql
DELETE FROM model_retail_output_fields;
DELETE FROM model_corp_output_fields;
DELETE FROM loan_pricing_workflow;
```
要求:
- 删除顺序满足外键或业务依赖关系
- 只清理贷款定价流程相关数据
- [ ] **Step 2: 写后端实施记录**
实施记录至少写明:
```markdown
- 已新增贷款定价敏感字段加解密服务与展示脱敏服务
- 已在流程创建链路对 `custName``idNum` 加密后入库
- 已在详情返回与列表返回链路统一脱敏
- 已在模型调用前显式解密敏感字段
- 已新增历史数据清理脚本
```
- [ ] **Step 3: 手工验证数据库落库与返回链路**
Run: 按项目现有方式启动后端,创建一条个人流程和一条企业流程,再查询列表与详情。
Expected:
- 数据库中的 `cust_name``id_num` 不等于前端提交明文
- 列表和详情返回的 `custName``idNum` 为脱敏值
- 模型输出“基本信息”页签中的 `custName``idNum` 也为脱敏值
- [ ] **Step 4: 如果为验证启动了后端进程,结束对应进程**
Run: `ps -ef | rg 'RuoYiApplication|loan-pricing|java'`
Expected: 仅停止本次验证启动的后端进程;对非本次启动进程不做处理。
- [ ] **Step 5: 提交本任务**
```bash
git add sql/clear_loan_pricing_workflow_history.sql doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md
git commit -m "补充贷款定价敏感字段后端实施记录"
```

View File

@@ -0,0 +1,150 @@
# Loan Pricing Sensitive Data Encryption Frontend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让贷款定价流程前端只按客户内码查询,并在列表页、详情页、模型输出“基本信息”页签稳定展示后端返回的脱敏 `custName``idNum`
**Architecture:** 前端不承担任何加解密逻辑,只做查询项收口与脱敏值展示消费。列表页从按 `custName` 查询切换为按 `custIsn` 查询,详情页与 `ModelOutputDisplay.vue` 保持现有结构,继续直接渲染后端返回字段,但要联调确认模型输出“基本信息”页签不再出现敏感明文。
**Tech Stack:** Vue 2、Element UI、RuoYi 前端工程、npm
---
### Task 1: 收口流程列表页查询条件为客户内码
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- [ ] **Step 1: 核对当前查询项与请求参数**
Run: `sed -n '1,140p' ruoyi-ui/src/views/loanPricing/workflow/index.vue`
Expected: 能看到当前页面仍存在“客户名称”查询项和 `queryParams.custName`
- [ ] **Step 2: 将查询项改为客户内码**
把列表页查询区调整为类似结构:
```vue
<el-form-item label="客户内码" prop="custIsn">
<el-input
v-model="queryParams.custIsn"
placeholder="请输入客户内码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
```
同步把查询参数从:
```js
custName: undefined
```
改为:
```js
custIsn: undefined
```
- [ ] **Step 3: 核对 API 层无需额外字段转换**
检查 `ruoyi-ui/src/api/loanPricing/workflow.js`,确认 `listWorkflow(query)` 继续透传 `params: query` 即可;若代码风格需要,仅补充注释说明,不新增映射逻辑。
- [ ] **Step 4: 重新检查源码确认客户名称查询已移除**
Run: `rg -n 'custName|custIsn|客户名称|客户内码' ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/api/loanPricing/workflow.js`
Expected: 列表页查询区不再出现 `queryParams.custName`,而是使用 `custIsn`
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/api/loanPricing/workflow.js
git commit -m "调整流程列表按客户内码查询"
```
### Task 2: 固化列表页、详情页与模型输出基本信息的脱敏展示消费
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- Inspect: `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- [ ] **Step 1: 核对当前页面直接消费后端字段的位置**
Run: `rg -n 'custName|idNum' ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/views/loanPricing/workflow/detail.vue ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
Expected: 能定位列表、详情以及模型输出“基本信息”页签中所有 `custName``idNum` 的展示位置。
- [ ] **Step 2: 去掉任何可能的前端二次处理设想,只保留直接展示**
如果页面中需要加说明性注释,保持最小实现,例如:
```vue
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
```
要求:
- 不新增“查看明文”按钮
- 不新增复制原值功能
- 不在前端自行做脱敏算法
- `ModelOutputDisplay.vue` 继续直接消费后端字段,不新增本地脱敏逻辑
- [ ] **Step 3: 执行前端构建验证**
Run: `npm --prefix ruoyi-ui run build:prod`
Expected: PASS输出包含 `Build complete.`
- [ ] **Step 4: 页面联调确认脱敏展示**
Run: 按项目现有方式启动前端并进入贷款定价流程列表页、详情页。
Expected:
- 列表页客户名称显示为脱敏值
- 个人详情页客户名称、证件号码显示为脱敏值
- 企业详情页客户名称、证件号码显示为脱敏值
- 个人模型输出“基本信息”页签中的客户名称、证件号码显示为脱敏值
- 企业模型输出“基本信息”页签中的客户名称、证件号码显示为脱敏值
- [ ] **Step 5: 如果为验证启动了前端进程,结束对应进程**
Run: `ps -ef | rg 'ruoyi-ui|vue-cli-service|npm'`
Expected: 仅停止本次联调启动的前端进程;对非本次启动进程不做处理。
- [ ] **Step 6: 提交本任务**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/views/loanPricing/workflow/detail.vue ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue
git commit -m "接入流程敏感字段前端脱敏展示"
```
### Task 3: 补前端实施记录
**Files:**
- Create: `doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-frontend.md`
- [ ] **Step 1: 编写前端实施记录**
实施记录至少写明:
```markdown
- 流程列表页查询项已从客户名称切换为客户内码
- 前端不承担 `custName``idNum` 加解密逻辑
- 列表页与详情页均直接展示后端返回的脱敏值
- 模型输出“基本信息”页签也直接展示后端返回的脱敏值
- 已完成前端构建验证与页面联调
```
- [ ] **Step 2: 核对文档路径**
Run: `ls doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-frontend.md`
Expected: 文件位于仓库 `doc/` 目录,不写错到其他文档路径。
- [ ] **Step 3: 提交本任务**
```bash
git add doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-frontend.md
git commit -m "补充贷款定价敏感字段前端实施记录"
```

View File

@@ -0,0 +1,279 @@
# Backend Password Transfer Encryption Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为正式密码提交接口补上后端解密链路,让 `/login``/register``/system/user/profile/updatePwd``/system/user/resetPwd``/system/user` 在收到密文密码后先解密,再复用现有认证与 BCrypt 逻辑。
**Architecture:**`ruoyi-framework` 新增统一的密码传输解密服务,固定密钥从配置读取,负责 AES/Base64 解密和失败抛错。`ruoyi-admin` 各控制器在进入现有业务逻辑前显式调用该服务对密码字段解密,`/login/test` 保持不变。
**Tech Stack:** Spring Boot 3.5、JUnit 5、MockMvc、JDK `javax.crypto`、Maven Surefire
---
### Task 1: 搭建后端密码解密基础设施
**Files:**
- Create: `ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PasswordTransferCryptoService.java`
- Create: `ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/PasswordTransferCryptoServiceTest.java`
- Modify: `ruoyi-admin/src/main/resources/application.yml`
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- [ ] **Step 1: 写解密服务失败用例**
```java
class PasswordTransferCryptoServiceTest
{
@Test
void shouldDecryptValidCipherText()
{
String plain = service.decrypt("Base64密文");
assertEquals("admin123", plain);
}
@Test
void shouldRejectInvalidCipherText()
{
assertThrows(ServiceException.class, () -> service.decrypt("not-base64"));
}
}
```
- [ ] **Step 2: 运行测试确认当前失败**
Run: `mvn -pl ruoyi-framework -am -Dtest=PasswordTransferCryptoServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL提示测试类或 `PasswordTransferCryptoService` 不存在
- [ ] **Step 3: 补配置与最小实现**
```java
@Service
public class PasswordTransferCryptoService
{
@Value("${security.password-transfer.key}")
private String key;
public String decrypt(String cipherText)
{
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"));
return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)), StandardCharsets.UTF_8);
}
}
```
```yaml
security:
password-transfer:
key: "请替换为16位固定密钥"
```
- [ ] **Step 4: 重新运行基础测试**
Run: `mvn -pl ruoyi-framework -am -Dtest=PasswordTransferCryptoServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PasswordTransferCryptoService.java ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/PasswordTransferCryptoServiceTest.java ruoyi-admin/src/main/resources/application.yml ruoyi-admin/src/main/resources/application-dev.yml
git commit -m "新增密码传输解密服务"
```
### Task 2: 接入登录与注册接口解密
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysLoginControllerPasswordTransferTest.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysRegisterControllerPasswordTransferTest.java`
- [ ] **Step 1: 写登录与注册控制器失败测试**
```java
mockMvc.perform(post("/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"admin\",\"password\":\"cipher\",\"code\":\"1\",\"uuid\":\"u\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("cipher");
verify(loginService).login("admin", "admin123", "1", "u");
```
```java
mockMvc.perform(post("/register")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"u1\",\"password\":\"cipher\",\"code\":\"1\",\"uuid\":\"u\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("cipher");
verify(registerService).register(any(RegisterBody.class));
```
- [ ] **Step 2: 运行登录/注册测试确认失败**
Run: `mvn -pl ruoyi-admin -am -Dtest=SysLoginControllerPasswordTransferTest,SysRegisterControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL控制器尚未调用解密服务
- [ ] **Step 3: 在正式接口入口补解密**
```java
loginBody.setPassword(passwordTransferCryptoService.decrypt(loginBody.getPassword()));
```
```java
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
```
要求:
- 只改 `/login`
- 不改 `loginWithoutCaptcha`
- 解密失败直接抛错,不追加明文兼容分支
- [ ] **Step 4: 重新运行登录/注册测试**
Run: `mvn -pl ruoyi-admin -am -Dtest=SysLoginControllerPasswordTransferTest,SysRegisterControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysLoginControllerPasswordTransferTest.java ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysRegisterControllerPasswordTransferTest.java
git commit -m "接入登录注册密码解密"
```
### Task 3: 接入个人修改密码接口解密
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysProfileControllerPasswordTransferTest.java`
- [ ] **Step 1: 写修改密码失败测试**
```java
mockMvc.perform(put("/system/user/profile/updatePwd")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"oldPassword\":\"oldCipher\",\"newPassword\":\"newCipher\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("oldCipher");
verify(passwordTransferCryptoService).decrypt("newCipher");
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -pl ruoyi-admin -am -Dtest=SysProfileControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL`updatePwd` 尚未解密 `oldPassword``newPassword`
- [ ] **Step 3: 在 `updatePwd` 开头显式解密两个字段**
```java
String oldPassword = passwordTransferCryptoService.decrypt(params.get("oldPassword"));
String newPassword = passwordTransferCryptoService.decrypt(params.get("newPassword"));
```
要求:
- 仅在解密成功后继续旧密码校验
- 不处理 `confirmPassword`
- 保持原有报错文案和 BCrypt 入库逻辑
- [ ] **Step 4: 重新运行测试**
Run: `mvn -pl ruoyi-admin -am -Dtest=SysProfileControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysProfileControllerPasswordTransferTest.java
git commit -m "接入个人修改密码解密"
```
### Task 4: 接入管理员新增用户与重置密码接口解密
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysUserControllerPasswordTransferTest.java`
- [ ] **Step 1: 写新增用户与重置密码失败测试**
```java
mockMvc.perform(post("/system/user")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"userName\":\"u1\",\"nickName\":\"n1\",\"deptId\":1,\"password\":\"cipher\"}"));
verify(passwordTransferCryptoService).decrypt("cipher");
```
```java
mockMvc.perform(put("/system/user/resetPwd")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"userId\":2,\"password\":\"cipher\"}"));
verify(passwordTransferCryptoService).decrypt("cipher");
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -pl ruoyi-admin -am -Dtest=SysUserControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL新增用户和重置密码入口尚未调用解密
- [ ] **Step 3: 在 `add` 与 `resetPwd` 中先解密后继续原逻辑**
```java
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
```
要求:
- 仅对 `add``resetPwd` 补解密
- `edit` 不动
- 仍保留现有权限校验与数据范围校验
- [ ] **Step 4: 重新运行测试**
Run: `mvn -pl ruoyi-admin -am -Dtest=SysUserControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysUserControllerPasswordTransferTest.java
git commit -m "接入用户密码接口解密"
```
### Task 5: 汇总验证与后端实施记录
**Files:**
- Create: `doc/implementation-report-2026-03-30-login-password-encryption-backend.md`
- [ ] **Step 1: 运行后端全部目标测试**
Run: `mvn -pl ruoyi-admin,ruoyi-framework -am -Dtest=PasswordTransferCryptoServiceTest,SysLoginControllerPasswordTransferTest,SysRegisterControllerPasswordTransferTest,SysProfileControllerPasswordTransferTest,SysUserControllerPasswordTransferTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS所有新增后端测试通过
- [ ] **Step 2: 手工核对 `/login/test` 未被改动**
Run: `git diff -- ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java`
Expected: 仅 `/login` 增加解密调用,`loginWithoutCaptcha` 无行为变化
- [ ] **Step 3: 写后端实施记录**
```markdown
# 密码加密传输后端实施记录
- 新增密码解密服务
- 接入 5 个正式接口中的后端入口
- 保持 `/login/test` 不变
- 补充控制器与服务测试
```
- [ ] **Step 4: 再次检查文档路径与 git 状态**
Run: `git status --short`
Expected: 仅包含后端实现文件与 `doc/implementation-report-2026-03-30-login-password-encryption-backend.md`
- [ ] **Step 5: 提交本任务**
```bash
git add doc/implementation-report-2026-03-30-login-password-encryption-backend.md
git commit -m "完成密码加密传输后端实现"
```

View File

@@ -0,0 +1,258 @@
# Frontend Password Transfer Encryption Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为正式密码提交接口补上前端加密发送能力,让登录、注册、个人修改密码、管理员重置密码、管理员新增用户在请求发出前只对密码字段做 AES 加密。
**Architecture:**`ruoyi-ui` 新增统一的密码传输加密工具和字段映射辅助方法,由 API 层在提交请求前克隆并加密受控字段。页面组件继续持有明文表单值,现有表单校验和交互文案保持不变。
**Tech Stack:** Vue 2、Axios、`crypto-js`、Node 脚本测试、Vue CLI 4
---
### Task 1: 搭建前端加密工具与测试基线
**Files:**
- Modify: `ruoyi-ui/package.json`
- Modify: `ruoyi-ui/package-lock.json`
- Create: `ruoyi-ui/src/utils/passwordTransfer.js`
- Create: `ruoyi-ui/tests/password-transfer-api.test.js`
- [ ] **Step 1: 写前端失败测试脚本**
```js
const encrypted = encryptPasswordFields(
{ password: 'admin123', code: '8888' },
['password'],
'1234567890abcdef'
)
assert.notStrictEqual(encrypted.password, 'admin123')
assert.strictEqual(encrypted.code, '8888')
```
```js
const requestConfig = login('admin', 'admin123', '8888', 'uuid-1')
assert.strictEqual(requestConfig.data.password !== 'admin123', true)
```
- [ ] **Step 2: 运行测试确认当前失败**
Run: `cd ruoyi-ui && node tests/password-transfer-api.test.js`
Expected: FAIL工具文件或 API 加密行为尚不存在
- [ ] **Step 3: 新增依赖、脚本和最小工具实现**
```js
import CryptoJS from 'crypto-js'
export function encryptPasswordFields(payload, fields, key) {
const next = { ...payload }
fields.forEach((field) => {
if (next[field]) {
next[field] = CryptoJS.AES.encrypt(next[field], CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString()
}
})
return next
}
```
```json
"scripts": {
"test:password-transfer": "node tests/password-transfer-api.test.js"
}
```
- [ ] **Step 4: 重新运行前端测试**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-ui/package.json ruoyi-ui/package-lock.json ruoyi-ui/src/utils/passwordTransfer.js ruoyi-ui/tests/password-transfer-api.test.js
git commit -m "新增前端密码加密工具"
```
### Task 2: 接入登录与注册接口加密
**Files:**
- Modify: `ruoyi-ui/src/api/login.js`
- [ ] **Step 1: 扩展测试覆盖登录与注册 API**
```js
const loginConfig = login('admin', 'admin123', '8888', 'uuid-1')
assert.notStrictEqual(loginConfig.data.password, 'admin123')
assert.strictEqual(loginConfig.data.username, 'admin')
const registerConfig = register({ username: 'u1', password: 'p1', confirmPassword: 'p1', code: '8888' })
assert.notStrictEqual(registerConfig.data.password, 'p1')
assert.strictEqual(registerConfig.data.confirmPassword, 'p1')
```
- [ ] **Step 2: 运行测试确认失败**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: FAIL`login.js` 尚未对正式接口密码字段加密
- [ ] **Step 3: 在 API 层接入加密工具**
```js
const data = encryptPasswordFields({ username, password, code, uuid }, ['password'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
```
```js
const payload = encryptPasswordFields(data, ['password'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
```
要求:
- 只加密 `password`
- 保持字段名不变
- 不在页面组件中写加密逻辑
- [ ] **Step 4: 重新运行前端测试**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-ui/src/api/login.js ruoyi-ui/tests/password-transfer-api.test.js
git commit -m "接入登录注册密码加密"
```
### Task 3: 接入个人修改密码接口加密
**Files:**
- Modify: `ruoyi-ui/src/api/system/user.js`
- [ ] **Step 1: 扩展测试覆盖 `updateUserPwd`**
```js
const config = updateUserPwd('oldPwd', 'newPwd')
assert.notStrictEqual(config.data.oldPassword, 'oldPwd')
assert.notStrictEqual(config.data.newPassword, 'newPwd')
```
- [ ] **Step 2: 运行测试确认失败**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: FAIL`updateUserPwd` 仍发送明文
- [ ] **Step 3: 只在 API 层加密两个密码字段**
```js
const data = encryptPasswordFields(
{ oldPassword, newPassword },
['oldPassword', 'newPassword'],
process.env.VUE_APP_PASSWORD_TRANSFER_KEY
)
```
要求:
- 页面 `resetPwd.vue` 不改
- 继续让前端表单在明文状态下完成确认密码校验
- [ ] **Step 4: 重新运行测试**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-ui/src/api/system/user.js ruoyi-ui/tests/password-transfer-api.test.js
git commit -m "接入个人修改密码加密"
```
### Task 4: 接入管理员新增用户与重置密码接口加密
**Files:**
- Modify: `ruoyi-ui/src/api/system/user.js`
- Modify: `ruoyi-ui/.env.development`
- Modify: `ruoyi-ui/.env.staging`
- Modify: `ruoyi-ui/.env.production`
- [ ] **Step 1: 扩展测试覆盖 `addUser` 与 `resetUserPwd`**
```js
const addConfig = addUser({ userName: 'u1', password: 'initPwd', nickName: 'n1' })
assert.notStrictEqual(addConfig.data.password, 'initPwd')
const resetConfig = resetUserPwd(2, 'resetPwd')
assert.notStrictEqual(resetConfig.data.password, 'resetPwd')
```
- [ ] **Step 2: 运行测试确认失败**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: FAIL`addUser``resetUserPwd` 仍发送明文
- [ ] **Step 3: 在受控接口接入加密并补环境配置**
```js
const payload = encryptPasswordFields(data, ['password'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
```
```dotenv
VUE_APP_PASSWORD_TRANSFER_KEY=请替换为16位固定密钥
```
要求:
- 只改 `addUser``resetUserPwd`
- `updateUser` 不做密码加密处理
- 三套环境文件都补同名配置项
- [ ] **Step 4: 重新运行测试**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-ui/src/api/system/user.js ruoyi-ui/.env.development ruoyi-ui/.env.staging ruoyi-ui/.env.production ruoyi-ui/tests/password-transfer-api.test.js
git commit -m "接入用户密码接口前端加密"
```
### Task 5: 汇总验证与前端实施记录
**Files:**
- Create: `doc/implementation-report-2026-03-30-login-password-encryption-frontend.md`
- [ ] **Step 1: 运行前端目标测试**
Run: `cd ruoyi-ui && npm run test:password-transfer`
Expected: PASS受控 API 的密码字段都按预期加密
- [ ] **Step 2: 运行一次前端构建验证**
Run: `cd ruoyi-ui && npm run build:stage`
Expected: PASS新增依赖、环境变量与 API 修改不影响构建
- [ ] **Step 3: 写前端实施记录**
```markdown
# 密码加密传输前端实施记录
- 新增强制密码字段加密工具
- 登录、注册、修改密码、重置密码、新增用户在 API 层加密
- 页面表单逻辑保持不变
```
- [ ] **Step 4: 再次检查 git 状态**
Run: `git status --short`
Expected: 仅包含前端实现文件与 `doc/implementation-report-2026-03-30-login-password-encryption-frontend.md`
- [ ] **Step 5: 提交本任务**
```bash
git add doc/implementation-report-2026-03-30-login-password-encryption-frontend.md
git commit -m "完成密码加密传输前端实现"
```

View File

@@ -0,0 +1,238 @@
# Production DB Init Export Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 产出一个可直接执行的生产初始化单文件 SQL基于若依基础脚本补齐贷款定价 3 张业务表结构,且不包含任何业务数据。
**Architecture:** 直接复用 `sql/ry_20250522.sql` 作为若依基础内容来源,再从当前项目最终结构来源中抽取 `loan_pricing_workflow``model_corp_output_fields``model_retail_output_fields` 三张业务表结构,拼装为新的生产初始化总脚本。完成后通过静态检查和临时数据库导入验证,确认脚本既能完整建库建表,又不会写入业务数据。
**Tech Stack:** MySQL 5.7/8.0、SQL、shell、`mysql` 客户端、`rg`
---
### Task 1: 锁定业务表最终结构来源
**Files:**
- Inspect: `sql/loan_pricing_schema_20260328.sql`
- Inspect: `sql/loan_pricing_workflow.sql`
- Inspect: `sql/model_corp.sql`
- Inspect: `sql/model_retail.sql`
- Inspect: `sql/add_missing_fields.sql`
- Inspect: `sql/add_execute_rate_field.sql`
- Inspect: `sql/fix_comments.sql`
- Inspect: `sql/fix_comments_utf8.sql`
- Inspect: `sql/fix_all_comments.sql`
- [ ] **Step 1: 比对 3 张业务表在各 SQL 文件中的定义**
Run: `rg -n "CREATE TABLE \`loan_pricing_workflow\`|CREATE TABLE \`model_corp_output_fields\`|CREATE TABLE \`model_retail_output_fields\`|ALTER TABLE \`loan_pricing_workflow\`|ALTER TABLE loan_pricing_workflow" sql/loan_pricing_schema_20260328.sql sql/loan_pricing_workflow.sql sql/model_corp.sql sql/model_retail.sql sql/add_missing_fields.sql sql/add_execute_rate_field.sql sql/fix_comments.sql sql/fix_comments_utf8.sql sql/fix_all_comments.sql`
Expected: 能定位 3 张业务表的最终建表来源以及 `loan_pricing_workflow` 的后续字段修正脚本。
- [ ] **Step 2: 以 `loan_pricing_schema_20260328.sql` 作为最终结构主来源**
核对至少以下字段必须存在于最终结构中:
```sql
`loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限',
`is_tech_ent` varchar(10) DEFAULT NULL COMMENT '科技型企业: true/false科技型企业最多下降5BP',
`is_green_loan` varchar(10) DEFAULT NULL COMMENT '绿色贷款: true/false绿色贷款最多下降5BP',
`is_trade_construction` varchar(10) DEFAULT NULL COMMENT '贸易和建筑业企业标识: true/false抵质押类上调20BP',
`loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能',
`id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
`execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)',
```
Expected: `loan_pricing_schema_20260328.sql` 能完整反映当前 3 张业务表最终结构,不需要再从带数据文件中提取。
- [ ] **Step 3: 记录本次只导出结构、不导出业务数据的边界**
Run: `rg -n "INSERT INTO \`loan_pricing_workflow\`|INSERT INTO \`model_corp_output_fields\`|INSERT INTO \`model_retail_output_fields\`" sql/loan_pricing_required_data_20260328.sql`
Expected: 仅在历史必要数据脚本中看到业务表插数语句,作为本次明确排除项。
- [ ] **Step 4: 提交本任务**
```bash
git add docs/superpowers/plans/2026-03-31-production-db-init-export-backend-plan.md
git commit -m "补充生产初始化数据库导出后端计划"
```
### Task 2: 生成生产初始化总脚本
**Files:**
- Create: `sql/loan_pricing_prod_init_20260331.sql`
- Inspect: `sql/ry_20250522.sql`
- Inspect: `sql/loan_pricing_schema_20260328.sql`
- [ ] **Step 1: 确认目标文件尚不存在,避免覆盖已有发布资产**
Run: `test ! -f sql/loan_pricing_prod_init_20260331.sql && echo "missing"`
Expected: 输出 `missing`,说明目标文件可安全新建。
- [ ] **Step 2: 创建脚本头部说明和数据库上下文**
在新文件头部写入类似说明:
```sql
-- 说明:
-- 1. 本文件用于生产环境数据库初始化
-- 2. 基于 sql/ry_20250522.sql 追加贷款定价业务表结构生成
-- 3. 包含若依基础初始化数据,不包含任何贷款定价业务数据
```
并补齐:
```sql
CREATE DATABASE IF NOT EXISTS `loan-pricing` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `loan-pricing`;
```
- [ ] **Step 3: 追加若依基础脚本内容**
直接将 `sql/ry_20250522.sql` 的主体内容复制到目标文件数据库上下文之后,不新增、不删减其中的基础初始化数据。
- [ ] **Step 4: 追加 3 张业务表的最终 `DROP TABLE` 与 `CREATE TABLE`**
从 `sql/loan_pricing_schema_20260328.sql` 提取并追加以下 3 段:
```sql
DROP TABLE IF EXISTS `loan_pricing_workflow`;
CREATE TABLE `loan_pricing_workflow` (...);
```
```sql
DROP TABLE IF EXISTS `model_corp_output_fields`;
CREATE TABLE `model_corp_output_fields` (...);
```
```sql
DROP TABLE IF EXISTS `model_retail_output_fields`;
CREATE TABLE `model_retail_output_fields` (...);
```
要求:
- 保留最终字段定义和注释
- 不带 `AUTO_INCREMENT=13`、`AUTO_INCREMENT=7` 这类依赖现存数据的值
- 不带任何 `INSERT INTO` 业务数据
- [ ] **Step 5: 做静态完整性检查**
Run: `rg -n "loan_pricing_workflow|model_corp_output_fields|model_retail_output_fields|INSERT INTO \`loan_pricing_workflow\`|INSERT INTO \`model_corp_output_fields\`|INSERT INTO \`model_retail_output_fields\`" sql/loan_pricing_prod_init_20260331.sql`
Expected: 能看到 3 张业务表的结构定义;看不到 3 张业务表的 `INSERT INTO`。
- [ ] **Step 6: 提交本任务**
```bash
git add sql/loan_pricing_prod_init_20260331.sql
git commit -m "新增生产初始化数据库脚本"
```
### Task 3: 校验脚本范围与重复定义
**Files:**
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 1: 检查 3 张业务表在目标文件中只定义一次**
Run: `python - <<'PY'\nfrom pathlib import Path\ntext = Path('sql/loan_pricing_prod_init_20260331.sql').read_text()\nfor name in ['loan_pricing_workflow','model_corp_output_fields','model_retail_output_fields']:\n print(name, text.count(f'CREATE TABLE `{name}`'))\nPY`
Expected: 3 行输出都为 `1`。
- [ ] **Step 2: 检查目标文件未引入历史必要数据脚本中的业务插数**
Run: `rg -n "INSERT INTO \`loan_pricing_workflow\`|INSERT INTO \`model_corp_output_fields\`|INSERT INTO \`model_retail_output_fields\`|DELETE FROM \`loan_pricing_workflow\`|DELETE FROM \`model_corp_output_fields\`|DELETE FROM \`model_retail_output_fields\`" sql/loan_pricing_prod_init_20260331.sql`
Expected: 无输出。
- [ ] **Step 3: 检查目标文件仍保留若依基础初始化数据**
Run: `rg -n "insert into sys_user values|insert into sys_role values|insert into sys_menu values" sql/loan_pricing_prod_init_20260331.sql`
Expected: 能定位若依基础用户、角色、菜单初始化语句,说明基础初始化数据未被误删。
- [ ] **Step 4: 如静态检查发现重复或缺失,立即修正脚本**
只允许修正以下问题:
```sql
-- 删除重复的业务表建表段
-- 补回遗漏的 CREATE DATABASE / USE / DROP TABLE / CREATE TABLE
-- 删除误写入的业务表 DELETE / INSERT
```
- [ ] **Step 5: 提交本任务**
```bash
git add sql/loan_pricing_prod_init_20260331.sql
git commit -m "校验并修正生产初始化数据库脚本"
```
### Task 4: 用临时数据库验证脚本可执行性
**Files:**
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- Create: `doc/implementation-report-2026-03-31-production-db-init-export-backend.md`
- [ ] **Step 1: 设置临时验证库环境变量**
在执行前准备:
```bash
export DB_HOST=116.62.17.81
export DB_PORT=3307
export DB_USER=root
export DB_PASSWORD='******'
export VERIFY_DB=loan_pricing_prod_init_verify_20260331
```
要求:
- `DB_PASSWORD` 从现有环境配置读取后在当前 shell 设置
- 不把密码写回仓库文件
- [ ] **Step 2: 创建临时验证库并导入脚本**
Run:
```bash
MYSQL_PWD="$DB_PASSWORD" mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -e "DROP DATABASE IF EXISTS \`$VERIFY_DB\`; CREATE DATABASE \`$VERIFY_DB\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
perl -0pe "s/CREATE DATABASE IF NOT EXISTS `loan-pricing` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;/CREATE DATABASE IF NOT EXISTS `$VERIFY_DB` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;/; s/USE `loan-pricing`;/USE `$VERIFY_DB`;/;" sql/loan_pricing_prod_init_20260331.sql | MYSQL_PWD="$DB_PASSWORD" mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER"
```
Expected: 两条命令都成功返回,无 SQL 执行报错。
- [ ] **Step 3: 校验基础初始化数据和业务空表**
Run:
```bash
MYSQL_PWD="$DB_PASSWORD" mysql -N -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" "$VERIFY_DB" -e "SELECT COUNT(*) FROM sys_user; SELECT COUNT(*) FROM sys_role; SELECT COUNT(*) FROM sys_menu; SELECT COUNT(*) FROM loan_pricing_workflow; SELECT COUNT(*) FROM model_corp_output_fields; SELECT COUNT(*) FROM model_retail_output_fields;"
```
Expected:
- `sys_user``sys_role``sys_menu` 结果大于 0
- `loan_pricing_workflow``model_corp_output_fields``model_retail_output_fields` 结果都为 `0`
- [ ] **Step 4: 清理临时验证库**
Run: `MYSQL_PWD="$DB_PASSWORD" mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -e "DROP DATABASE IF EXISTS \`$VERIFY_DB\`;"`
Expected: PASS验证结束后临时库被删除。
- [ ] **Step 5: 编写后端实施记录**
在 `doc/implementation-report-2026-03-31-production-db-init-export-backend.md` 记录至少以下内容:
```markdown
- 新增生产初始化总脚本 `sql/loan_pricing_prod_init_20260331.sql`
- 基础部分直接复用 `sql/ry_20250522.sql`
- 新增 3 张业务表结构:`loan_pricing_workflow`、`model_corp_output_fields`、`model_retail_output_fields`
- 明确未导出任何业务数据
- 已完成静态检查和临时库导入验证
```
- [ ] **Step 6: 核对实施记录路径**
Run: `ls doc/implementation-report-2026-03-31-production-db-init-export-backend.md`
Expected: 文件存在于仓库 `doc/` 目录。
- [ ] **Step 7: 提交本任务**
```bash
git add sql/loan_pricing_prod_init_20260331.sql doc/implementation-report-2026-03-31-production-db-init-export-backend.md
git commit -m "完成生产初始化数据库脚本验证"
```

View File

@@ -0,0 +1,78 @@
# Production DB Init Export Frontend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 明确本次生产初始化数据库导出任务无前端代码改动范围,并将该结论以可追溯文档形式沉淀,避免执行阶段误改 `ruoyi-ui`
**Architecture:** 本次交付物是单文件 SQL职责只在数据库初始化层不涉及前端页面、接口契约、构建配置或部署产物调整。因此前端计划不做功能实现只负责范围确认、源码复核和实施记录留档确保“无前端改动”是被显式验证过的结论而不是口头假设。
**Tech Stack:** Vue 2、RuoYi 前端工程、shell、`rg`
---
### Task 1: 确认本次任务无前端实现范围
**Files:**
- Inspect: `docs/superpowers/specs/2026-03-31-production-db-init-export-design.md`
- Inspect: `ruoyi-ui/src`
- Inspect: `ruoyi-ui/package.json`
- [ ] **Step 1: 核对设计文档中的交付物边界**
Run: `rg -n "单一 \\.sql|生产初始化|不包含任何业务数据|业务表结构" docs/superpowers/specs/2026-03-31-production-db-init-export-design.md`
Expected: 能看到本次交付物明确限定为数据库初始化 SQL不包含前端实现要求。
- [ ] **Step 2: 检查前端目录中不存在与本次任务直接相关的待改文件**
Run: `rg -n "loan_pricing_prod_init|production db init|数据库导出|初始化脚本" ruoyi-ui/src ruoyi-ui/package.json`
Expected: 无输出,说明前端工程不存在与本次 SQL 导出任务耦合的实现点。
- [ ] **Step 3: 明确本次执行阶段不得改动 `ruoyi-ui`**
把执行约束写入实施记录草稿,至少包含:
```markdown
- 本次任务交付物为数据库初始化 SQL
- 不修改 `ruoyi-ui` 下任何源码、接口或构建配置
- 如执行中出现前端需求,应回到新需求重新做设计和计划
```
- [ ] **Step 4: 提交本任务**
```bash
git add docs/superpowers/plans/2026-03-31-production-db-init-export-frontend-plan.md
git commit -m "补充生产初始化数据库导出前端计划"
```
### Task 2: 补前端实施记录并留痕无改动结论
**Files:**
- Create: `doc/implementation-report-2026-03-31-production-db-init-export-frontend.md`
- [ ] **Step 1: 编写前端实施记录**
实施记录至少写明:
```markdown
- 已根据设计文档确认本次交付物仅为数据库初始化单文件 SQL
- 已检查 `ruoyi-ui` 工程,不存在需要随本次任务修改的页面、接口或构建配置
- 本次前端范围为无代码改动
- 执行阶段应保持 `ruoyi-ui` 目录不变
```
- [ ] **Step 2: 核对实施记录路径**
Run: `ls doc/implementation-report-2026-03-31-production-db-init-export-frontend.md`
Expected: 文件存在于仓库 `doc/` 目录。
- [ ] **Step 3: 再次确认 `ruoyi-ui` 未被纳入提交范围**
Run: `git status --short ruoyi-ui`
Expected: 无本次任务新增或修改的前端文件;如果有输出,需要先确认是否为历史遗留改动,不得误提交。
- [ ] **Step 4: 提交本任务**
```bash
git add doc/implementation-report-2026-03-31-production-db-init-export-frontend.md
git commit -m "补充生产初始化数据库导出前端实施记录"
```

View File

@@ -0,0 +1,385 @@
# Production One-Click Deploy Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 新增一份生产一键部署脚本,在脚本同目录完成发布包校验、旧版后端备份、后端进程停止、新版 `jar` 替换、后端重启和部署结果验证。
**Architecture:** 以现有 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 和 [bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh) 为参考但将解包、备份、PID 管理、进程识别、端口等待全部内联到新的单脚本 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh)。实现只覆盖单实例发布链路,不接入 Nginx、不拆独立启停脚本、不引入额外配置文件。
**Tech Stack:** POSIX shell、`unzip``find``pgrep``nohup``ss``sh -n`
---
### Task 1: 锁定脚本接口与参考实现边界
**Files:**
- Inspect: `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
- Inspect: `bin/prod/deploy_release.sh`
- Inspect: `bin/prod/restart_java.sh`
- [ ] **Step 1: 核对设计文档中的后端职责边界**
Run: `rg -n "单脚本|JAVA_BIN|backend/backend.pid|63310|loan.pricing.home|TERM|KILL" docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
Expected: 能定位单脚本、自定义 `JAVA_BIN`、PID 文件、进程标记和端口等待等核心约束。
- [ ] **Step 2: 抽取现有生产脚本中可复用的后端逻辑**
Run: `rg -n "assert_single_jar|extract_release_package|cleanup|BACKEND_JAR|collect_backend_pids|stop_backend|start_backend|BACKEND_MARKER|BACKEND_PORT" bin/prod/deploy_release.sh bin/prod/restart_java.sh`
Expected: 能定位现有发布包解压校验、PID 管理和端口等待逻辑,作为新脚本参考来源。
- [ ] **Step 3: 确认新脚本目标文件尚不存在**
Run: `test ! -f bin/prod/deploy_from_package.sh && echo "missing"`
Expected: 输出 `missing`,说明可以新增独立脚本而不覆盖现有 `/home/webapp` 发布脚本。
- [ ] **Step 4: 提交本任务**
```bash
git add docs/superpowers/plans/2026-04-01-production-one-click-deploy-backend-plan.md
git commit -m "新增生产一键部署后端计划"
```
### Task 2: 创建单脚本基础骨架与公共函数
**Files:**
- Create: `bin/prod/deploy_from_package.sh`
- [ ] **Step 1: 写入脚本头部配置和使用说明**
在 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh) 顶部写入最小骨架:
```sh
#!/bin/sh
set -eu
JAVA_BIN="/home/webapp/env/java/bin/java"
BACKEND_PORT=63310
SPRING_PROFILE="pro"
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
```
并补齐以下说明函数:
```sh
usage() {
cat <<'EOF'
用法:
./bin/prod/deploy_from_package.sh
EOF
}
```
- [ ] **Step 2: 实现脚本目录定位和路径常量**
至少补齐以下路径变量:
```sh
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
BACKEND_DIR="$SCRIPT_DIR/backend"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
BACKEND_JAR_TARGET="$BACKEND_DIR/ruoyi-admin.jar"
BACKEND_PID_FILE="$BACKEND_DIR/backend.pid"
BACKEND_LOG_FILE="$BACKEND_DIR/backend-console.log"
FRONTEND_DIST_ARCHIVE="$FRONTEND_DIR/dist.zip"
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
BACKEND_MARKER="-Dloan.pricing.home=$SCRIPT_DIR"
```
- [ ] **Step 3: 实现日志、时间戳和清理函数**
至少补齐:
```sh
timestamp() {
date "+%Y%m%d%H%M%S"
}
log_info() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
}
log_error() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
}
```
以及:
```sh
cleanup() {
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
fi
}
```
- [ ] **Step 4: 做语法校验**
Run: `sh -n bin/prod/deploy_from_package.sh`
Expected: 无输出,返回码为 0。
- [ ] **Step 5: 提交本任务**
```bash
git add bin/prod/deploy_from_package.sh
git commit -m "新增生产一键部署脚本骨架"
```
### Task 3: 实现发布包发现、解压和后端备份
**Files:**
- Modify: `bin/prod/deploy_from_package.sh`
- [ ] **Step 1: 实现目录与命令前置校验**
补齐以下校验:
```sh
require_dir() {
if [ ! -d "$1" ]; then
log_error "缺少目录: $1"
exit 1
fi
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
```
在主流程中校验:
```sh
require_dir "$BACKEND_DIR"
require_dir "$FRONTEND_DIR"
require_command unzip
require_command find
require_command pgrep
require_command ss
```
- [ ] **Step 2: 实现同目录唯一发布 zip 查找**
新增函数:
```sh
find_release_archive() {
archives=$(find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip')
count=$(printf '%s\n' "$archives" | sed '/^$/d' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "脚本同目录发布 zip 数量不正确,期望 1 个,实际 $count"
exit 1
fi
printf '%s\n' "$archives"
}
```
- [ ] **Step 3: 实现解压与唯一 `jar` 校验**
补齐:
```sh
extract_release_package() {
release_archive="$1"
release_extract_dir="$2"
mkdir -p "$release_extract_dir"
unzip -oq "$release_archive" -d "$release_extract_dir"
}
assert_single_jar() {
search_dir="$1"
count=$(find "$search_dir" -type f -name '*.jar' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "后端 jar 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name '*.jar' | head -n 1
}
```
- [ ] **Step 4: 实现旧版后端备份和新 `jar` 替换**
补齐:
```sh
backup_backend_jar() {
if [ -f "$BACKEND_JAR_TARGET" ]; then
mv "$BACKEND_JAR_TARGET" "$BACKEND_JAR_TARGET.$(timestamp).bak"
fi
}
deploy_backend_jar() {
source_jar="$1"
mv "$source_jar" "$BACKEND_JAR_TARGET"
}
```
- [ ] **Step 5: 用临时目录构造后端备份场景做静态验证**
Run:
```bash
tmpdir=$(mktemp -d)
mkdir -p "$tmpdir/backend" "$tmpdir/frontend/package"
touch "$tmpdir/backend/ruoyi-admin.jar"
touch "$tmpdir/frontend/dist.zip"
touch "$tmpdir/package/app.jar"
(cd "$tmpdir/package" && zip -q release.zip app.jar >/dev/null)
test -f "$tmpdir/backend/ruoyi-admin.jar"
rm -rf "$tmpdir"
```
Expected: 命令执行成功,用于确认计划中的文件命名和目录约定可被临时目录复现。
- [ ] **Step 6: 做语法校验**
Run: `sh -n bin/prod/deploy_from_package.sh`
Expected: 无输出,返回码为 0。
- [ ] **Step 7: 提交本任务**
```bash
git add bin/prod/deploy_from_package.sh
git commit -m "实现生产部署脚本后端发布包处理"
```
### Task 4: 实现后端进程停止、启动与端口等待
**Files:**
- Modify: `bin/prod/deploy_from_package.sh`
- [ ] **Step 1: 实现托管进程识别函数**
补齐以下函数:
```sh
is_managed_backend_pid() {
pid="$1"
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
args=$(ps -o args= -p "$pid" 2>/dev/null || true)
case "$args" in
*"$BACKEND_MARKER"*"$BACKEND_JAR_TARGET"*|*"$BACKEND_JAR_TARGET"*"$BACKEND_MARKER"*)
return 0
;;
esac
return 1
}
```
- [ ] **Step 2: 实现 PID 收集与停止流程**
补齐:
```sh
collect_backend_pids() {
pids=""
if [ -f "$BACKEND_PID_FILE" ]; then
file_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true)
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
pids="$pids $file_pid"
fi
fi
marker_pids=$(pgrep -f "$BACKEND_MARKER" 2>/dev/null || true)
for pid in $marker_pids; do
if is_managed_backend_pid "$pid"; then
pids="$pids $pid"
fi
done
printf '%s\n' "$(echo "$pids" | xargs 2>/dev/null || true)"
}
```
以及 `stop_backend()`,要求:
- 先发 `TERM`
- 等待最多 30 秒
- 超时后发 `KILL`
- 最后删除 `backend.pid`
- [ ] **Step 3: 实现启动流程**
补齐 `start_backend()`,要求:
```sh
nohup "$JAVA_BIN" $JAVA_OPTS "$BACKEND_MARKER" -jar "$BACKEND_JAR_TARGET" \
--spring.profiles.active="$SPRING_PROFILE" \
--server.port="$BACKEND_PORT" >> "$BACKEND_LOG_FILE" 2>&1 &
```
并在启动后:
- 写入 `backend/backend.pid`
- 轮询 `ss -lnt | grep ":63310 "` 最长 30 秒
- 若未监听成功则报错退出
- [ ] **Step 4: 实现主流程调用顺序**
主流程必须按以下顺序调用:
```sh
backup_backend_jar
stop_backend
deploy_backend_jar "$backend_jar_source"
start_backend
```
不得把 `stop_backend` 放到备份后面之外的位置,避免旧进程继续占用将被替换的 `jar`
- [ ] **Step 5: 做语法校验**
Run: `sh -n bin/prod/deploy_from_package.sh`
Expected: 无输出,返回码为 0。
- [ ] **Step 6: 提交本任务**
```bash
git add bin/prod/deploy_from_package.sh
git commit -m "实现生产部署脚本后端启停逻辑"
```
### Task 5: 补后端实施记录并验证关键链路
**Files:**
- Modify: `bin/prod/deploy_from_package.sh`
- Create: `doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md`
- [ ] **Step 1: 做脚本静态语法校验**
Run: `sh -n bin/prod/deploy_from_package.sh`
Expected: 无输出,返回码为 0。
- [ ] **Step 2: 检查脚本中的关键配置与标记是否落位**
Run: `rg -n "JAVA_BIN=|BACKEND_PORT=63310|SPRING_PROFILE=|BACKEND_MARKER=|backend.pid|backend-console.log|pgrep -f|ss -lnt" bin/prod/deploy_from_package.sh`
Expected: 能看到 Java 路径、端口、进程标记、PID 文件、日志文件和端口等待逻辑都已写入脚本。
- [ ] **Step 3: 编写后端实施记录**
在 [doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md](/Users/wkc/Desktop/loan-pricing/loan-pricing/doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md) 至少记录:
```markdown
- 新增脚本 `bin/prod/deploy_from_package.sh`
- 脚本内固定 `JAVA_BIN`
- 后端发布包解压与唯一 jar 校验规则
- 旧版 jar 时间戳备份规则
- PID 文件、进程标记、端口 63310 等待逻辑
- 执行的验证命令与结果
```
- [ ] **Step 4: 核对实施记录路径**
Run: `ls doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md`
Expected: 文件存在于仓库 `doc/` 目录。
- [ ] **Step 5: 提交本任务**
```bash
git add bin/prod/deploy_from_package.sh doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md
git commit -m "完成生产一键部署后端实现"
```

View File

@@ -0,0 +1,208 @@
# Production One-Click Deploy Frontend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在不修改 `ruoyi-ui` 源码的前提下,补齐生产一键部署脚本中的前端静态包替换、旧版 `dist` 备份、`dist.zip` 解压验证和实施留痕。
**Architecture:** 本次前端交付不涉及 Vue 页面、接口契约或构建配置,而是通过 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh) 管理发布目录中的 `frontend/dist.zip``frontend/dist/`。因此前端计划的重点是约束脚本里的静态包部署链路,验证旧 `dist` 目录时间戳备份、新 `dist.zip` 落盘和解压结果,并明确 `ruoyi-ui` 在本次任务中保持不变。
**Tech Stack:** shell、ZIP 静态资源包、`unzip``find``rg`
---
### Task 1: 确认本次任务无 `ruoyi-ui` 源码改动范围
**Files:**
- Inspect: `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
- Inspect: `ruoyi-ui/src`
- Inspect: `ruoyi-ui/package.json`
- [ ] **Step 1: 核对设计文档中的前端交付边界**
Run: `rg -n "frontend/dist|dist.zip|不修改|不接入 Nginx|不新增外部配置文件" docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
Expected: 能看到本次前端范围只包含发布目录中的静态包替换和解压,不涉及 `ruoyi-ui` 工程改造。
- [ ] **Step 2: 检查前端源码中不存在需要同步修改的实现点**
Run: `rg -n "deploy_from_package|dist.zip|frontend/dist|生产一键部署" ruoyi-ui/src ruoyi-ui/package.json`
Expected: 无输出,说明 `ruoyi-ui` 不依赖本次部署脚本实现。
- [ ] **Step 3: 明确执行阶段不得改动 `ruoyi-ui`**
在执行笔记中写入以下约束:
```markdown
- 本次前端交付物是发布目录中的静态包部署链路
- 不修改 `ruoyi-ui` 下任何页面、接口、构建配置或打包脚本
- 如后续出现页面需求,必须回到新需求重新设计和计划
```
- [ ] **Step 4: 提交本任务**
```bash
git add docs/superpowers/plans/2026-04-01-production-one-click-deploy-frontend-plan.md
git commit -m "新增生产一键部署前端计划"
```
### Task 2: 在部署脚本中实现前端静态包发现与备份
**Files:**
- Modify: `bin/prod/deploy_from_package.sh`
- [ ] **Step 1: 实现前端 `dist.zip` 唯一校验**
补齐函数:
```sh
assert_single_dist_zip() {
search_dir="$1"
count=$(find "$search_dir" -type f -name 'dist.zip' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "前端 dist.zip 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name 'dist.zip' | head -n 1
}
```
- [ ] **Step 2: 实现旧版前端 `dist` 时间戳备份**
补齐函数:
```sh
backup_frontend_dist() {
if [ -d "$FRONTEND_DIST_DIR" ]; then
mv "$FRONTEND_DIST_DIR" "$FRONTEND_DIR/dist-$(timestamp)"
fi
}
```
- [ ] **Step 3: 实现新前端压缩包替换**
补齐函数:
```sh
deploy_frontend_archive() {
source_dist_zip="$1"
rm -f "$FRONTEND_DIST_ARCHIVE"
mv "$source_dist_zip" "$FRONTEND_DIST_ARCHIVE"
}
```
- [ ] **Step 4: 做语法校验**
Run: `sh -n bin/prod/deploy_from_package.sh`
Expected: 无输出,返回码为 0。
- [ ] **Step 5: 提交本任务**
```bash
git add bin/prod/deploy_from_package.sh
git commit -m "实现生产部署脚本前端备份替换逻辑"
```
### Task 3: 在部署脚本中实现前端解压与结果校验
**Files:**
- Modify: `bin/prod/deploy_from_package.sh`
- [ ] **Step 1: 实现前端解压函数**
补齐函数:
```sh
deploy_frontend_dist() {
dist_unpack_dir="$WORK_DIR/frontend"
mkdir -p "$dist_unpack_dir"
unzip -oq "$FRONTEND_DIST_ARCHIVE" -d "$dist_unpack_dir"
rm -rf "$FRONTEND_DIST_DIR"
mkdir -p "$FRONTEND_DIST_DIR"
cp -a "$(resolve_frontend_source_dir "$dist_unpack_dir")"/. "$FRONTEND_DIST_DIR"/
}
```
要求同时实现 `resolve_frontend_source_dir()`,至少支持:
- 解压根目录直接包含 `index.html`
- 解压后为 `dist/index.html`
- 其他情况下用 `find` 定位第一个 `index.html`
- [ ] **Step 2: 将前端流程接入主执行顺序**
主流程至少补齐:
```sh
frontend_dist_source=$(assert_single_dist_zip "$WORK_DIR/package")
backup_frontend_dist
deploy_frontend_archive "$frontend_dist_source"
deploy_frontend_dist
```
- [ ] **Step 3: 检查脚本中已包含前端关键链路**
Run: `rg -n "assert_single_dist_zip|backup_frontend_dist|deploy_frontend_archive|deploy_frontend_dist|resolve_frontend_source_dir|frontend/dist.zip|frontend/dist" bin/prod/deploy_from_package.sh`
Expected: 能看到前端校验、备份、替换和解压函数都已落位。
- [ ] **Step 4: 做语法校验**
Run: `sh -n bin/prod/deploy_from_package.sh`
Expected: 无输出,返回码为 0。
- [ ] **Step 5: 提交本任务**
```bash
git add bin/prod/deploy_from_package.sh
git commit -m "实现生产部署脚本前端解压逻辑"
```
### Task 4: 用临时发布目录验证前端部署结果并留痕
**Files:**
- Modify: `bin/prod/deploy_from_package.sh`
- Create: `doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md`
- [ ] **Step 1: 构造临时前端发布目录做解压验证**
Run:
```bash
tmpdir=$(mktemp -d)
mkdir -p "$tmpdir/input/dist"
printf '<html>ok</html>\n' > "$tmpdir/input/dist/index.html"
(cd "$tmpdir/input" && zip -qr "$tmpdir/dist.zip" dist)
unzip -oq "$tmpdir/dist.zip" -d "$tmpdir/unpacked"
test -f "$tmpdir/unpacked/dist/index.html"
rm -rf "$tmpdir"
```
Expected: 命令执行成功,说明计划中的 `dist.zip -> frontend/dist` 解压链路可被临时目录复现。
- [ ] **Step 2: 再次确认 `ruoyi-ui` 未被纳入改动范围**
Run: `git status --short ruoyi-ui`
Expected: 无本次任务新增或修改的前端源码文件;如果有输出,必须先判断是否为历史遗留改动,不得误提交。
- [ ] **Step 3: 编写前端实施记录**
在 [doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md](/Users/wkc/Desktop/loan-pricing/loan-pricing/doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md) 至少记录:
```markdown
- 部署脚本中新增前端 dist.zip 唯一校验
- 旧版 frontend/dist 时间戳备份规则
- 新版 frontend/dist.zip 替换规则
- 前端静态资源解压到 frontend/dist 的实现方式
- 已确认 `ruoyi-ui` 本次无源码改动
- 执行的验证命令与结果
```
- [ ] **Step 4: 核对实施记录路径**
Run: `ls doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md`
Expected: 文件存在于仓库 `doc/` 目录。
- [ ] **Step 5: 提交本任务**
```bash
git add bin/prod/deploy_from_package.sh doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md
git commit -m "完成生产一键部署前端实现"
```

View File

@@ -0,0 +1,110 @@
# Personal Pricing Collateral Optional Frontend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让个人利率定价新增流程中的“抵质押类型”改为非必填,企业流程保持不变。
**Architecture:** 维持现有个人新增弹窗的数据结构、字段名称和提交报文不变,只移除前端表单对 `collType` 的必填限制。通过前端源码断言测试和页面联调共同验证“字段可空提交”的行为,避免扩大到企业流程或后端接口。
**Tech Stack:** Vue 2、Element UI、npm、Node.js 断言脚本
---
### Task 1: 固化个人新增流程“抵质押类型非必填”的前端测试
**Files:**
- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js`
- Inspect: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- [ ] **Step 1: 核对当前个人新增弹窗中的 `collType` 规则**
Run: `sed -n '150,220p' ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
Expected: 能看到 `rules.collType` 里仍有 `required: true`
- [ ] **Step 2: 先写失败断言**
`ruoyi-ui/tests/personal-create-input-params.test.js` 中增加断言,明确个人新增弹窗不应再包含:
```js
required: true, message: "请选择抵质押类型"
```
- [ ] **Step 3: 运行测试确认红灯**
Run: `npm --prefix ruoyi-ui run test:personal-create-input-params`
Expected: FAIL提示个人新增弹窗仍将抵质押类型设为必填。
### Task 2: 移除个人新增弹窗中 `collType` 的必填限制
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- [ ] **Step 1: 删除 `collType` 的必填校验**
把:
```js
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
]
```
移除,使个人表单规则中不再声明 `collType` 为必填。
- [ ] **Step 2: 保持其它字段与提交逻辑不变**
确认以下内容不改动:
```js
collType: undefined,
collThirdParty: false
```
以及提交时的:
```js
collThirdParty: this.form.collThirdParty ? '1' : '0'
```
- [ ] **Step 3: 重新运行测试确认转绿**
Run: `npm --prefix ruoyi-ui run test:personal-create-input-params`
Expected: PASS输出包含 `personal create input params assertions passed`
### Task 3: 页面联调并补实施记录
**Files:**
- Create: `docs/implementation-reports/2026-04-10-personal-pricing-collateral-optional-frontend.md`
- [ ] **Step 1: 启动前端页面**
Run: `npm --prefix ruoyi-ui run dev`
Expected: 前端本地开发服务启动成功,可访问新增个人利率定价流程页面。
- [ ] **Step 2: 浏览器确认页面行为**
联调确认:
- 个人新增弹窗“抵质押类型”字段可为空
- 不选择“抵质押类型”时其余必填项填完整仍可点击提交
- 企业新增弹窗规则不受影响
- [ ] **Step 3: 停止本次测试启动的前端进程**
Run: `ps -ef | rg 'vue-cli-service serve|npm --prefix ruoyi-ui run dev'`
Expected: 仅停止本次联调启动的前端进程。
- [ ] **Step 4: 编写实施记录**
实施记录至少包含:
```markdown
- 本次仅调整个人利率定价新增流程
- 个人新增弹窗已移除抵质押类型必填校验
- 企业新增流程未改动
- 已执行前端断言测试与页面联调验证
```
- [ ] **Step 5: 核对实施记录保存路径**
Run: `ls docs/implementation-reports/2026-04-10-personal-pricing-collateral-optional-frontend.md`
Expected: 文件位于仓库 `docs/implementation-reports` 目录。

View File

@@ -0,0 +1,27 @@
# TongWeb 接入后端实施文档
## 目标
按照 `tongweb/2026-04-16-TongWeb接入全流程通用指南.md` 在当前后端工程中接入东方通 TongWeb替换默认内嵌 Tomcat并将 license 文件随 `ruoyi-admin` 启动模块一起打包。
## 实施内容
1.`ruoyi-admin/pom.xml` 增加 TongWeb Maven 仓库。
2.`ruoyi-admin/pom.xml``ruoyi-framework``ruoyi-loan-pricing` 依赖排除 `spring-boot-starter-tomcat`
3.`ruoyi-admin/pom.xml` 引入 `com.tongweb.springboot:tongweb-spring-boot-starter-3.x:7.0.E.7`
4. 将 TongWeb license 复制到 `ruoyi-admin/src/main/resources/license.dat`,并在 `application.yml` 中配置 `server.tongweb.license.path=classpath:license.dat`
5. 增加资源存在性测试,验证 `license.dat` 可从 classpath 加载。
6. 执行后端构建、依赖树、打包产物和测试验证,确认 TongWeb 依赖与 license 已正确接入。
## 变更说明
- 当前项目基于 Spring Boot 3.5.x因此实际接入使用 `tongweb-spring-boot-starter-3.x`,版本号延续指南中的 `7.0.E.7`
- license 按本次要求保持文件名为 `license.dat`,不改名为 `Tongweb_license.dat`
- 现有 `application-dev.yml``application-pro.yml``application-uat.yml` 中的 `server.tomcat` 参数暂时保留,后续以 TongWeb 实际启动结果为准决定是否继续清理。
## 验证步骤
1. `mvn -pl ruoyi-admin -Dtest=TongWebLicenseResourceTest test`
2. `mvn -pl ruoyi-admin -am package -DskipTests`
3. `jar tf ruoyi-admin/target/ruoyi-admin.jar | rg 'license.dat|tongweb'`
4. `mvn -pl ruoyi-admin dependency:tree '-Dincludes=com.tongweb.springboot:*,com.tongweb:*,org.springframework.boot:spring-boot-starter-tomcat,org.apache.tomcat.embed:*'`

View File

@@ -0,0 +1,265 @@
# 系统登录与密码类接口加密传输设计文档
## 1. 背景
当前系统登录、注册、修改密码、重置密码、新增用户等正式接口,在请求体中直接传输明文密码。后端在收到密码后再执行现有的登录校验或 BCrypt 加密入库逻辑。
本次需求是在不改变现有业务语义的前提下,为所有正式密码提交接口增加“密码加密传输”能力,避免密码以明文形式直接出现在接口请求体中。
## 2. 已确认约束
- 采用对称加密方案
- 前端使用固定密钥加密密码字段
- 后端使用同一固定密钥解密密码字段
- 覆盖所有正式密码提交接口
- 明确不包含 `/login/test`
- 不新增兼容性或补丁性方案
- 不允许“解密失败后按明文继续处理”
- 保持最短路径实现,不改现有账号、认证、密码存储主逻辑
## 3. 接口范围
本次加密传输仅覆盖以下正式接口:
- `/login`
- `/register`
- `/system/user/profile/updatePwd`
- `/system/user/resetPwd`
- `/system/user`
各接口需要处理的密码字段如下:
- `/login``password`
- `/register``password`
- `/system/user/profile/updatePwd``oldPassword``newPassword`
- `/system/user/resetPwd``password`
- `/system/user``password`
以下接口不在本次范围内:
- `/login/test`
- 任何不提交密码字段的接口
## 4. 现状分析
### 4.1 前端现状
当前前端接口调用中,登录、注册、个人修改密码、管理员重置密码、管理员新增用户都直接提交明文密码字段。仓库中虽然已经存在 `JSEncrypt` 工具,但仅用于“记住密码”场景下 Cookie 的本地存储加密,并没有用于登录或其他正式密码接口。
### 4.2 后端现状
后端正式接口的密码处理链路如下:
- 登录接口直接读取 `LoginBody.password` 并交给认证流程
- 注册接口直接读取 `RegisterBody.password` 并执行 BCrypt 加密入库
- 修改密码接口直接读取 `oldPassword``newPassword` 并执行旧密码校验和新密码入库
- 管理员重置密码和新增用户接口直接读取 `SysUser.password` 并执行 BCrypt 加密
现有后端没有统一的密码传输解密层,因此如果直接在前端加密而后端不解密,现有校验链路会全部失效。
## 5. 方案对比
### 方案一:保留现有字段名,前端加密后提交,后端统一解密
做法:
- 保持现有请求结构不变
- 前端在 API 提交前仅加密密码字段
- 后端在控制器进入业务逻辑前,对密码字段统一解密
优点:
- 改动路径最短
- 页面、DTO、控制器入参结构基本不变
- 现有业务校验和 BCrypt 逻辑可直接复用
缺点:
- 需要明确每个接口的密码字段清单
- 前后端都要维护一份受控字段映射
### 方案二:新增专用密文字段
做法:
- 每个接口新增 `encryptedPassword``encryptedOldPassword` 等字段
- 后端只处理密文字段
优点:
- 语义清楚
- 明文和密文边界直观
缺点:
- 改动面大
- 前后端 DTO、表单、测试样例都要整体调整
- 不符合本次最短路径实现原则
### 方案三:全局请求拦截器加密 + 全局参数层解密
做法:
- 前端在 axios 拦截器中按 URL 自动加密密码字段
- 后端在过滤器或参数解析层统一自动解密
优点:
- 页面层改动最少
缺点:
- 隐式逻辑过重
- 对不同入参类型的接口可读性差
- 不利于后续定位问题
## 6. 设计结论
采用方案一。
本次仅在接口边界增加密码加密传输能力,业务层继续只处理解密后的明文密码。传输链路如下:
1. 前端表单收集用户输入的密码明文
2. API 提交前,使用固定对称密钥加密密码字段
3. 后端控制器收到请求后,先对约定密码字段解密
4. 解密成功后继续走现有业务逻辑
5. 解密失败时直接返回错误,不进入后续业务处理
`/login/test` 保持现状,不加入加密与解密逻辑。
## 7. 前端设计
### 7.1 设计目标
前端只负责“在请求发出前对密码字段加密”,不在页面组件中分散实现逻辑,也不修改表单字段命名。
### 7.2 收口位置
加密逻辑收口在 API 调用层,不放在页面组件层。
原因:
- 登录页、注册页、个人中心、用户管理都存在密码提交场景
- 若每个页面独立处理,加密逻辑容易分散和重复
- API 层更容易统一维护接口与字段映射
### 7.3 前端改动点
- 新增统一的对称加密工具
- 新增“密码字段加密”辅助方法
- 在以下接口调用前对对应字段加密:
- 登录
- 注册
- 个人修改密码
- 管理员重置密码
- 管理员新增用户
- 保持请求字段名不变
### 7.4 配置方式
前端固定密钥通过环境配置读取,不直接散落在业务代码中。
## 8. 后端设计
### 8.1 设计目标
后端只负责“在进入现有业务逻辑前将密码字段解密为明文”,不改动现有认证和密码存储主流程。
### 8.2 收口位置
解密逻辑收口在控制器入口之后、业务逻辑之前,由统一的密码解密工具完成。
原因:
- 当前接口入参类型不统一,包含 `LoginBody``RegisterBody``SysUser``Map<String, String>`
- 如果直接放到全局过滤器或参数解析层,会增加隐式复杂度
- 控制器显式调用统一解密工具,路径更短、更直观
### 8.3 后端改动点
- 新增统一的对称解密工具
- 新增面向不同入参类型的密码字段解密方法
- 在以下正式接口进入业务逻辑前显式解密:
- `SysLoginController.login`
- `SysRegisterController.register`
- `SysProfileController.updatePwd`
- `SysUserController.resetPwd`
- `SysUserController.add`
- 不改动 `SysLoginController.loginWithoutCaptcha`
### 8.4 业务链路保持不变
解密成功后继续沿用现有逻辑:
- 登录继续走认证管理器与 `SysPasswordService`
- 注册继续走 BCrypt 加密入库
- 个人修改密码继续先校验旧密码,再加密新密码入库
- 管理员重置密码和新增用户继续走 BCrypt 加密入库
## 9. 配置设计
前后端分别维护固定对称密钥配置:
- 前端从环境变量读取固定密钥
- 后端从 `application.yml` 读取固定密钥
本次设计默认前后端使用同一把固定密钥,不涉及动态下发、轮换或多套密钥管理。
## 10. 错误处理
本次只采用单一路径,不做兼容分支:
- 受控正式接口收到密码字段后,后端默认按密文处理
- 任一密码字段解密失败,接口直接返回错误
- 不允许“尝试解密失败后继续按明文处理”
- 不允许只加密部分密码字段后继续流转
修改密码接口中,`oldPassword``newPassword` 必须同时成功解密后才能进入现有校验流程。
## 11. 非目标
本次不包含以下内容:
- 不修改 `/login/test`
- 不改造密码存储方式
- 不改造现有 BCrypt 校验逻辑
- 不引入非对称加密
- 不增加密钥动态下发能力
- 不增加明密文双通道兼容逻辑
- 不修改与密码无关的请求字段
## 12. 风险与控制
主要风险如下:
1. 前后端固定密钥不一致,会导致所有正式密码接口失败
2. 某些密码字段漏加密或漏解密,会导致登录失败或入库异常
3. 个人修改密码接口包含多个密码字段,若字段映射错误,会导致旧密码校验失败
控制方式:
- 前后端统一约定固定密钥配置名称与用途
- 将密码字段清单明确写入实现计划
- 将正式接口逐一纳入测试验证
- 保持 `/login/test` 完全不接入,避免影响现有测试用途
## 13. 验证方案
实施后至少验证以下场景:
1. `/login` 提交加密后的 `password` 可以正常登录
2. `/register` 提交加密后的 `password` 可以正常注册
3. `/system/user/profile/updatePwd` 提交加密后的 `oldPassword``newPassword` 可以正常修改密码
4. `/system/user/resetPwd` 提交加密后的 `password` 可以正常重置密码
5. `/system/user` 提交加密后的 `password` 可以正常新增用户
6. 受控正式接口在密文非法时直接失败
7. `/login/test` 仍按现有方式运行,不受本次改动影响
## 14. 实施范围
- 前端:登录、注册、个人中心、用户管理相关 API 和密码加密工具
- 后端:登录、注册、个人中心、用户管理相关控制器和密码解密工具
- 配置:前端环境配置、后端应用配置
- 数据库:无表结构改动
本次属于接口边界增强,不涉及数据库结构和核心认证机制重构。

View File

@@ -0,0 +1,218 @@
# 生产初始化数据库导出设计文档
## 1. 背景
当前项目已经存在若依基础库脚本 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql),也存在历史的全量结构导出文件和“必要数据”导出文件。但本次目标不是继续导出业务数据,而是为生产部署准备一个单一、可直接执行的数据库初始化脚本。
该脚本需要满足以下要求:
- 以若依基础表和初始化数据为基线
- 在此基础上补齐贷款定价项目新增的所有业务表
- 不携带任何业务数据
- 最终导出为一个文件,生产环境可直接执行
## 2. 已确认约束
- 最终产物必须是一个单一 `.sql` 文件
- 若依基础部分直接复用 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql)
- 不从当前数据库重新抽取 `sys_*` 管理数据
- 不导出任何业务数据
- 不拆分为“基础脚本 + 增量脚本”多步执行
- 不增加兼容性、补丁性或兜底性方案
- 方案必须保持最短路径实现
## 3. 当前资产与现状
### 3.1 已有基础脚本
[sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 已包含若依基础表结构以及初始化管理数据,覆盖部门、用户、岗位、角色、菜单、字典、参数、通知、定时任务等基础能力。
### 3.2 已有历史导出文件
仓库内已有以下历史文件,可作为字段和表范围核对依据:
- [sql/loan_pricing_schema_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_schema_20260328.sql)
- [sql/loan_pricing_required_data_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_required_data_20260328.sql)
其中 `loan_pricing_required_data_20260328.sql` 带有业务数据,不符合本次生产初始化目标,因此本次不能直接复用。
### 3.3 业务增量表范围
相对于若依基础脚本,当前项目新增的业务表结构只有以下 3 张:
- `loan_pricing_workflow`
- `model_corp_output_fields`
- `model_retail_output_fields`
本次生产初始化导出只需要将这 3 张表的最终结构追加到若依基础脚本之后。
## 4. 方案对比
### 方案一:生成单一生产初始化总脚本
做法:
- 以 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 为主体
- 追加 3 张业务表的最终 `DROP TABLE``CREATE TABLE`
- 形成一个新的、可直接执行的生产初始化 SQL 文件
优点:
- 与本次“一个文件直接执行”的目标完全一致
- 路径最短,执行成本最低
- 不依赖实时连库导出,不会误带当前库中的业务数据
- 基础初始化内容与现有若依脚本保持一致,可控性高
缺点:
- 后续业务表结构变化后,需要重新生成该文件
### 方案二:保留若依基础脚本,再新增一个业务表增量脚本
做法:
- 保持 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 不变
- 额外新增一个只包含业务表结构的 SQL 文件
- 生产执行时先跑基础脚本,再跑增量脚本
优点:
- 文件职责分离较清晰
缺点:
- 不满足“导出到一个文件内”的明确要求
- 生产执行需要多步操作
- 增加人为漏执行或执行顺序错误的风险
### 方案三:从当前数据库重新导出全库,再手工删除不需要的内容
做法:
- 基于当前数据库重新导出结构与数据
- 人工删除业务数据和不需要的表内容
优点:
- 理论上能快速拿到一个初稿
缺点:
- 风险最高,容易混入业务数据、运行态数据和测试数据
- 结果依赖当前库状态,不稳定
- 不符合最短路径且可控的实现要求
## 5. 设计结论
采用方案一:生成单一生产初始化总脚本。
最终产物是一个新的 SQL 文件,放在 [sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql) 目录,用于生产数据库初始化。该文件以若依基础脚本为主体,追加 3 张业务表结构,不包含任何业务表数据。
## 6. 导出文件设计
### 6.1 文件职责
该文件只负责“生产数据库初始化”,不承担历史数据迁移、业务数据灌库或环境差异兼容职责。
### 6.2 文件内容组成
建议内容结构如下:
1. 头部说明
2. 数据库创建与切库语句
3. 若依基础表结构与初始化数据
4. 贷款定价业务表结构
5. 执行结束后的环境恢复语句
### 6.3 文件命名
建议命名为:
- `sql/loan_pricing_prod_init_20260331.sql`
命名目标是明确表达“生产初始化”用途,并带上生成日期,方便后续版本追踪。
## 7. 保留与排除范围
### 7.1 保留内容
保留内容分为两部分:
第一部分是若依基础脚本中的全部基础表与初始化数据,直接复用 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 的现有内容。
第二部分是项目业务增量表结构,仅保留如下 3 张表的结构定义:
- `loan_pricing_workflow`
- `model_corp_output_fields`
- `model_retail_output_fields`
### 7.2 排除内容
以下内容明确不进入生产初始化脚本:
- `loan_pricing_workflow` 的任何记录数据
- `model_corp_output_fields` 的任何记录数据
- `model_retail_output_fields` 的任何记录数据
- 从当前数据库重新抽取的 `sys_*` 数据
- 任何日志、运行历史、流程历史、演示数据、测试数据
## 8. 业务表结构来源
3 张业务表的结构应以当前仓库中已经确认的最终版本为准,结构来源优先按以下顺序核定:
1. [sql/loan_pricing_schema_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_schema_20260328.sql) 中对应表的最终结构
2. [sql/loan_pricing_workflow.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_workflow.sql)、[sql/model_corp.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/model_corp.sql)、[sql/model_retail.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/model_retail.sql)
3. 其后的结构修正脚本,例如字段补充、字段注释修复、执行利率字段补充等 SQL
如果存在同名表的多份定义,应以能反映当前最终字段状态的版本为准,避免把旧结构写入生产初始化脚本。
## 9. 生成方式
本次采用静态拼装方式生成,不依赖实时数据库导出。
生成步骤如下:
1. 复制 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 的基础内容
2. 从最终结构来源中提取 3 张业务表的 `DROP TABLE``CREATE TABLE`
3. 将业务表结构追加到基础内容后方
4. 在文件头部补充脚本说明,明确用途、范围和排除项
该方式可以保证结果稳定、内容可审查,并且不会因为当前数据库状态不同而发生漂移。
## 10. 验证设计
本次验证只围绕“单文件是否可用于生产初始化”展开,必须覆盖以下检查:
1. 检查脚本是否为单文件,且可独立执行
2. 检查脚本中是否完整包含若依基础内容
3. 检查脚本中是否完整包含 3 张业务表结构
4. 检查脚本中业务表不存在任何 `INSERT` 数据语句
5. 在测试库执行脚本,验证建库建表流程可跑通
6. 导入完成后核对若依基础表存在初始化数据
7. 导入完成后核对 3 张业务表为空表
## 11. 风险与控制
主要风险如下:
1. 如果业务表结构来源选择错误,可能把旧字段定义带入生产脚本
2. 如果直接复用带数据的历史导出文件,可能误将业务数据带入生产
3. 如果脚本顺序错误,可能导致执行时报表不存在或环境切换错误
控制方式如下:
- 明确以若依基础脚本为唯一基础来源
- 明确业务增量仅限 3 张表结构
- 明确禁止业务表 `INSERT` 语句进入最终脚本
- 通过测试库执行验证最终脚本的可执行性
## 12. 非目标
本次不包含以下内容:
- 不迁移任何业务数据
- 不重建当前环境中的真实用户与权限现状
- 不处理生产数据回灌
- 不新增多环境兼容脚本
- 不生成多文件执行方案

View File

@@ -0,0 +1,290 @@
# 生产一键部署脚本设计文档
## 1. 背景
当前仓库已经存在生产环境部署脚本 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 和 Java 管理脚本 [bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh)。但这套脚本依赖固定的 `/home/webapp` 目录结构、独立启停脚本以及安装好的 Nginx不符合本次“在脚本同目录直接完成发布”的目标。
本次需要设计一份新的生产一键部署脚本,直接放在发布目录内执行。该目录中同时存在:
- 部署脚本本身
- 1 个发布 zip
- `backend/` 目录
- `frontend/` 目录
发布 zip 内固定包含 1 个后端 `jar` 和 1 个前端 `dist.zip`。脚本执行时需要完成旧版本备份、新版本替换、后端重启和前端解压部署。
## 2. 已确认约束
- 交付形态必须是单脚本,自包含,不拆分独立重启脚本
- Java 可执行路径直接写在脚本内常量中,不通过命令行参数或外部配置文件传入
- 发布包从脚本同目录读取
- 发布包内必须且只能包含 1 个后端 `jar` 和 1 个前端 `dist.zip`
- 旧版后端 `jar` 与旧版前端 `dist` 目录必须通过“原地重命名 + 时间戳”的方式保留
- 后端启动参数固定沿用当前生产约束:`--spring.profiles.active=pro --server.port=63310`
- 不增加兼容性、补丁性、兜底性或回滚性方案
- 方案必须保持最短路径实现,并可完成全链路逻辑验证
## 3. 当前资产与现状
### 3.1 现有生产脚本
[bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 已具备以下能力:
- 解压发布包
- 校验包内 `jar``dist.zip`
- 备份旧版后端与前端
- 替换产物
- 调用独立 Java 管理脚本完成重启
但该脚本默认依赖 `/home/webapp` 固定目录、`restart_java.sh` 外部脚本和 Nginx 管理流程,超出了本次需求边界。
### 3.2 现有 Java 管理逻辑
[bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh) 已实现:
- 使用 PID 文件记录后端进程
- 通过 `-Dloan.pricing.home` 标记识别托管进程
- 停止时先 `TERM` 再按需 `KILL`
- 启动后等待端口 `63310` 监听成功
这部分逻辑可以作为新单脚本中的后端启停参考,但最终不再拆成第二个脚本文件。
## 4. 方案对比
### 方案一:单脚本自包含部署
做法:
- 新增一份单独的部署脚本
- 在脚本顶部写死 `JAVA_BIN`、端口、日志路径和目录约定
- 执行时自动读取脚本同目录发布包
- 将解包、备份、替换、启停全部内联到同一文件
优点:
- 与本次“一起写在一个脚本里”的目标完全一致
- 发布目录内即可直接执行,路径最短
- 不依赖独立重启脚本或外部配置文件
- 用户只需维护一个脚本文件
缺点:
- 后续如果要单独管理 `start``stop``status`,需要再扩展脚本
### 方案二:保留现有双脚本结构,只调整入口
做法:
- 保留部署脚本和重启脚本两份文件
- 让部署脚本在同目录发布模式下调用重启脚本
优点:
- 可复用现有结构,改动相对少
缺点:
- 不满足“写在一起”的明确要求
- 交付物仍有两个脚本,使用路径不够直接
### 方案三:极简覆盖式脚本
做法:
- 不维护 PID 文件
- 直接按文件名覆盖
- 使用宽泛的 Java 进程匹配方式停止旧进程
优点:
- 脚本最短
缺点:
- 容易误伤其他 Java 进程
- 状态不可控
- 不满足全链路可验证要求
## 5. 设计结论
采用方案一:单脚本自包含部署。
最终交付为 1 份新的生产一键部署脚本,负责在脚本同目录完成完整发布链路。脚本以当前仓库已有生产脚本为参考,但不保留 Nginx 管理、根目录安装和双脚本调用等超出本次范围的设计。
## 6. 脚本结构设计
### 6.1 目录约定
脚本执行目录固定为脚本所在目录。目录内约定如下:
- `deploy_release.sh` 或本次确定的新脚本文件
- 1 个发布 zip
- `backend/`
- `frontend/`
脚本不依赖仓库根目录运行,也不依赖外部工作目录传参。
### 6.2 脚本内固定配置
脚本顶部固定声明以下配置项:
- `JAVA_BIN`
- `BACKEND_PORT=63310`
- `SPRING_PROFILE=pro`
- `JAVA_OPTS`
- `BACKEND_PID_FILE=backend/backend.pid`
- `BACKEND_LOG_FILE=backend/backend-console.log`
- `BACKEND_JAR_TARGET=backend/ruoyi-admin.jar`
- `FRONTEND_DIST_DIR=frontend/dist`
- `FRONTEND_DIST_ARCHIVE=frontend/dist.zip`
其中 `JAVA_BIN` 由维护者直接修改脚本内常量,不再设计其他配置入口。
### 6.3 执行流程
脚本执行顺序固定如下:
1. 定位脚本所在目录
2. 校验 `backend/``frontend/` 目录存在,不存在则直接失败
3. 在脚本同目录查找唯一一个发布 zip
4. 解压发布 zip 到临时目录
5. 校验临时目录内必须且只能找到 1 个 `jar` 和 1 个 `dist.zip`
6.`backend/` 目录下现有 `jar` 重命名为带时间戳的备份文件
7.`frontend/dist` 重命名为 `dist-时间戳`
8. 停止当前脚本托管的旧后端进程
9. 将新 `jar` 移入 `backend/ruoyi-admin.jar`
10. 将新 `dist.zip` 移入 `frontend/dist.zip`
11. 删除当前 `frontend/dist`
12. 解压新 `frontend/dist.zip``frontend/dist/`
13. 启动新的后端 `jar`
14. 等待端口 `63310` 监听成功
15. 输出部署完成信息
## 7. 后端启停设计
### 7.1 进程识别规则
为避免误杀机器上的其他 Java 进程,脚本只管理自己启动的后端实例。
识别规则如下:
- 启动时附加标记参数:`-Dloan.pricing.home=<脚本目录>`
- 优先读取 `backend/backend.pid`
- 如果 PID 文件失效,则使用 `pgrep -f` 按以下组合特征识别:
- `-Dloan.pricing.home=<脚本目录>`
- `backend/ruoyi-admin.jar`
只有同时满足托管标记和目标 jar 特征的进程,才允许被停止。
### 7.2 停止流程
停止逻辑如下:
1. 收集当前托管进程 PID
2. 如无托管进程,则清理失效 PID 文件并直接继续部署
3. 对托管进程发送 `TERM`
4. 等待最多 30 秒
5. 若仍存在残留进程,则发送 `KILL`
6. 删除 `backend/backend.pid`
### 7.3 启动流程
启动逻辑如下:
1. 校验 `JAVA_BIN` 可执行
2. 校验 `backend/ruoyi-admin.jar` 存在
3. 使用 `nohup` 后台启动
4. 写入 `backend/backend.pid`
5. 日志输出到 `backend/backend-console.log`
6. 使用固定参数启动:
- `-Dloan.pricing.home=<脚本目录>`
- `--spring.profiles.active=pro`
- `--server.port=63310`
7. 轮询端口监听状态,最长等待 30 秒
## 8. 备份与替换设计
### 8.1 后端备份
如果 `backend/` 目录中存在旧版 `jar`,则将其原地重命名为:
- `原文件名.YYYYMMDDHHMMSS.bak`
不新增独立备份目录,避免引入额外结构。
### 8.2 前端备份
如果 `frontend/dist` 目录存在,则将其原地重命名为:
- `dist-YYYYMMDDHHMMSS`
如果 `frontend/dist.zip` 已存在,则允许被新版本覆盖,不再为历史 `dist.zip` 单独追加备份规则。本次备份目标仅聚焦于用户明确提出的旧 `jar` 和旧前端 `dist`
### 8.3 新产物替换
替换规则如下:
- 后端统一落到 `backend/ruoyi-admin.jar`
- 前端压缩包统一落到 `frontend/dist.zip`
- 前端静态资源统一解压到 `frontend/dist/`
该规则确保部署目录在每次发布后保持稳定结构,便于后续维护。
## 9. 失败处理设计
本次失败处理仅保留必要的强校验,不增加回滚或兼容分支。
以下场景直接失败退出:
- 脚本同目录下没有发布 zip
- 脚本同目录下存在多个发布 zip
- 解压后 `jar` 数量不是 1
- 解压后 `dist.zip` 数量不是 1
- `JAVA_BIN` 不可执行
-`jar` 无法写入目标位置
- `dist.zip` 解压失败
- 后端 30 秒内未监听 `63310`
脚本退出时需要清理临时解压目录,避免残留中间文件。
## 10. 交付文件设计
本次设计对应的交付物限定为以下内容:
- 1 个生产一键部署脚本
- 1 份设计文档
- 1 份实施记录
建议脚本路径为:
- [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh)
保留 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 不动,可以避免影响现有 `/home/webapp` 部署方式;新需求使用独立脚本承载,边界更清晰。
## 11. 验证设计
本次验证只覆盖该单脚本的一键部署链路,必须包含以下检查:
1. 执行 `sh -n` 校验脚本语法
2. 校验同目录只有 1 个发布 zip 时可继续执行
3. 校验没有 zip 或存在多个 zip 时直接失败
4. 校验发布包内必须正好有 1 个 `jar` 和 1 个 `dist.zip`
5. 校验旧 `jar` 被改名为带时间戳备份文件
6. 校验旧 `frontend/dist` 被改名为带时间戳目录
7. 校验新 `jar` 最终位于 `backend/ruoyi-admin.jar`
8. 校验新 `dist.zip` 最终位于 `frontend/dist.zip`
9. 校验前端资源成功解压到 `frontend/dist/`
10. 校验后端进程成功启动并监听 `63310`
11. 校验 `backend/backend.pid``backend/backend-console.log` 被正确生成
## 12. 非目标
本次明确不包含以下内容:
- 不接入 Nginx 启停或重载
- 不处理 Java 安装
- 不生成独立的后端重启脚本
- 不增加回滚脚本
- 不新增命令行参数模式
- 不新增外部配置文件
- 不兼容多实例或多应用部署场景

BIN
ruoyi-admin/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -15,6 +15,20 @@
web服务入口
</description>
<repositories>
<repository>
<id>tongweb-releases</id>
<name>TongWeb Maven Releases</name>
<url>https://mvn.elitescloud.com/nexus/repository/maven-releases/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!-- spring-boot-devtools -->
@@ -40,6 +54,12 @@
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 定时任务-->
@@ -58,6 +78,18 @@
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-loan-pricing</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.tongweb.springboot</groupId>
<artifactId>tongweb-spring-boot-starter-3.x</artifactId>
<version>7.0.E.7</version>
</dependency>
<dependency>

View File

@@ -18,6 +18,7 @@ import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
@@ -47,6 +48,9 @@ public class SysLoginController
@Autowired
private ISysConfigService configService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
/**
* 登录方法
*
@@ -57,6 +61,7 @@ public class SysLoginController
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
loginBody.setPassword(passwordTransferCryptoService.decrypt(loginBody.getPassword()));
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());

View File

@@ -23,6 +23,7 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
@@ -41,6 +42,9 @@ public class SysProfileController extends BaseController
@Autowired
private TokenService tokenService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
/**
* 个人信息
*/
@@ -92,8 +96,8 @@ public class SysProfileController extends BaseController
@PutMapping("/updatePwd")
public AjaxResult updatePwd(@RequestBody Map<String, String> params)
{
String oldPassword = params.get("oldPassword");
String newPassword = params.get("newPassword");
String oldPassword = passwordTransferCryptoService.decrypt(params.get("oldPassword"));
String newPassword = passwordTransferCryptoService.decrypt(params.get("newPassword"));
LoginUser loginUser = getLoginUser();
Long userId = loginUser.getUserId();
SysUser user = userService.selectUserById(userId);

View File

@@ -8,6 +8,7 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.RegisterBody;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.SysRegisterService;
import com.ruoyi.system.service.ISysConfigService;
@@ -25,6 +26,9 @@ public class SysRegisterController extends BaseController
@Autowired
private ISysConfigService configService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
@PostMapping("/register")
public AjaxResult register(@RequestBody RegisterBody user)
{
@@ -32,6 +36,7 @@ public class SysRegisterController extends BaseController
{
return error("当前系统没有开启注册功能!");
}
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
String msg = registerService.register(user);
return StringUtils.isEmpty(msg) ? success() : error(msg);
}

View File

@@ -27,6 +27,7 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
@@ -53,6 +54,9 @@ public class SysUserController extends BaseController
@Autowired
private ISysPostService postService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
/**
* 获取用户列表
*/
@@ -139,6 +143,7 @@ public class SysUserController extends BaseController
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setCreateBy(getUsername());
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
return toAjax(userService.insertUser(user));
}
@@ -196,6 +201,7 @@ public class SysUserController extends BaseController
{
userService.checkUserAllowed(user);
userService.checkUserDataScope(user.getUserId());
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
user.setUpdateBy(getUsername());
return toAjax(userService.resetPwd(user));

View File

@@ -1,7 +1,7 @@
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
# 服务器的HTTP端口默认为63310
port: 63310
servlet:
# 应用的访问路径
context-path: /
@@ -79,4 +79,8 @@ spring:
config:
multi-statement-allow: true
model:
url: http://localhost:8080/rate/pricing/mock/invokeModel
url: http://localhost:63310/rate/pricing/mock/invokeModel
security:
password-transfer:
key: "1234567890abcdef"

View File

@@ -0,0 +1,86 @@
# 开发环境配置
server:
# 服务器的HTTP端口默认为63310
port: 63310
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://64.127.23.7:3306/loan-pricing?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: lrdb
password: Synx2024
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
model:
url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
security:
password-transfer:
key: "1234567890abcdef"

View File

@@ -0,0 +1,86 @@
# 开发环境配置
server:
# 服务器的HTTP端口默认为63310
port: 63310
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://192.168.0.111:40628/loan-pricing?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: Kfcx@1234
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
model:
url: http://localhost:63310/rate/pricing/mock/invokeModel
security:
password-transfer:
key: "1234567890abcdef"

Some files were not shown because too many files have changed in this diff Show More