51 Commits

Author SHA1 Message Date
wkc
28088d43a8 迁移项目到 RuoYi-Vue springboot2 基线 2026-04-14 15:13:51 +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
649 changed files with 83203 additions and 51315 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
}
}
}

88
.gitignore vendored
View File

@@ -1,50 +1,48 @@
######################################################################
# Build Tools
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
target/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
# IDE
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### JRebel ###
rebel.xml
### NetBeans ###
nbproject/private/
build/*
nbbuild/
dist/
nbdist/
.nb-gradle/
######################################################################
# Build Tools
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
target/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
# IDE
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### JRebel ###
rebel.xml
### NetBeans ###
nbproject/private/
build/*
nbbuild/
dist/
nbdist/
.nb-gradle/
######################################################################
# Others
.DS_Store
*.log
*.xml.versionsBackup
*.swp
!*/build/*.java
!*/build/*.html
!*/build/*.xml
logs/
!*/build/*.java
!*/build/*.html
!*/build/*.xml

View File

@@ -0,0 +1,18 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "上虞利率定价系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]
- img [ref=e18]
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

@@ -0,0 +1,20 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "上虞利率定价系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]: admin123admin
- img [ref=e11]
- generic [ref=e14]:
- generic [ref=e15]:
- textbox "密码" [ref=e16]
- img [ref=e18]
- generic [ref=e29]: 请输入您的密码
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [active] [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1,191 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- link "上虞利率定价系统" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- heading "上虞利率定价系统" [level=1] [ref=e8]
- menubar [ref=e12]:
- link "流程列表" [ref=e14] [cursor=pointer]:
- /url: /index
- menuitem "流程列表" [ref=e15]:
- img [ref=e16]
- text: 流程列表
- menuitem "系统管理 " [ref=e19]:
- generic [ref=e20] [cursor=pointer]:
- img [ref=e21]
- text: 系统管理
- generic [ref=e23]:
- text:
- generic [ref=e25]:
- generic [ref=e26]:
- generic [ref=e27]:
- img [ref=e29] [cursor=pointer]
- navigation "Breadcrumb" [ref=e31]:
- generic:
- generic [ref=e32]:
- link "首页" [ref=e33]
- text: /
- generic [ref=e379]:
- link "利率定价管理" [ref=e380]
- text: /
- link "流程列表" [ref=e382]
- generic [ref=e36]:
- img [ref=e38] [cursor=pointer]
- img [ref=e41] [cursor=pointer]
- button [ref=e44] [cursor=pointer]:
- img [ref=e45]
- button "若依" [ref=e48] [cursor=pointer]:
- img [ref=e49]
- text: 若依
- generic [ref=e50]:
- generic [ref=e53]:
- generic [ref=e54] [cursor=pointer]: 流程列表
- generic [ref=e383] [cursor=pointer]:
- text: 流程详情
- generic [ref=e384]:
- text:      
- generic [ref=e385]:
- generic [ref=e386]:
- heading "流程详情" [level=2] [ref=e387]
- button " 返回" [ref=e388] [cursor=pointer]:
- generic [ref=e389]:
- text: 返回
- generic [ref=e391]:
- generic [ref=e393]:
- generic [ref=e396]: 关键信息
- table [ref=e400]:
- rowgroup [ref=e401]:
- row "业务方流水号" [ref=e402]:
- columnheader "业务方流水号" [ref=e403]
- row "20260410150311114" [ref=e404]:
- cell "20260410150311114" [ref=e405]
- rowgroup [ref=e406]:
- row "客户名称" [ref=e407]:
- columnheader "客户名称" [ref=e408]
- row "t***" [ref=e409]:
- cell "t***" [ref=e410]
- rowgroup [ref=e411]:
- row "客户类型" [ref=e412]:
- columnheader "客户类型" [ref=e413]
- row "个人" [ref=e414]:
- cell "个人" [ref=e415]
- rowgroup [ref=e416]:
- row "申请金额" [ref=e417]:
- columnheader "申请金额" [ref=e418]
- row "1000 元" [ref=e419]:
- cell "1000 元" [ref=e420]
- rowgroup [ref=e421]:
- row "基准利率" [ref=e422]:
- columnheader "基准利率" [ref=e423]
- row "4.35 %" [ref=e424]:
- cell "4.35 %" [ref=e425]
- rowgroup [ref=e426]:
- row "浮动BP" [ref=e427]:
- columnheader "浮动BP" [ref=e428]
- row "350" [ref=e429]:
- cell "350" [ref=e430]
- rowgroup [ref=e431]:
- row "最终测算利率" [ref=e432]:
- columnheader "最终测算利率" [ref=e433]
- row "6.05 %" [ref=e434]:
- cell "6.05 %" [ref=e435]
- rowgroup [ref=e436]:
- row "执行利率" [ref=e437]:
- columnheader "执行利率" [ref=e438]
- row "% 确定" [ref=e439]:
- cell "% 确定" [ref=e440]:
- generic [ref=e441]:
- generic [ref=e442]:
- textbox "请输入执行利率" [ref=e443]
- generic [ref=e444]: "%"
- button "确定" [ref=e445] [cursor=pointer]
- generic [ref=e446]:
- generic [ref=e447]:
- generic [ref=e450]: 流程详情
- generic [ref=e451]:
- generic [ref=e452]:
- heading "基本信息" [level=4] [ref=e453]
- table [ref=e456]:
- rowgroup [ref=e457]:
- row "机构编码 892000 运行模式 1" [ref=e458]:
- rowheader "机构编码" [ref=e459]
- cell "892000" [ref=e460]
- rowheader "运行模式" [ref=e461]
- cell "1" [ref=e462]
- rowgroup [ref=e463]:
- row "客户内码 test 证件类型 身份证" [ref=e464]:
- rowheader "客户内码" [ref=e465]
- cell "test" [ref=e466]
- rowheader "证件类型" [ref=e467]
- cell "身份证" [ref=e468]
- rowgroup [ref=e469]:
- row "证件号码 ** 创建时间 2026-04-10 15:03:11" [ref=e470]:
- rowheader "证件号码" [ref=e471]
- cell "**" [ref=e472]
- rowheader "创建时间" [ref=e473]
- cell "2026-04-10 15:03:11" [ref=e474]
- rowgroup [ref=e475]:
- row "创建者 若依-admin" [ref=e476]:
- rowheader "创建者" [ref=e477]
- cell "若依-admin" [ref=e478]
- generic [ref=e479]:
- heading "业务信息" [level=4] [ref=e480]
- table [ref=e483]:
- rowgroup [ref=e484]:
- row "担保方式 信用 申请金额 1000 元" [ref=e485]:
- rowheader "担保方式" [ref=e486]
- cell "信用" [ref=e487]
- rowheader "申请金额" [ref=e488]
- cell "1000 元" [ref=e489]
- rowgroup [ref=e490]:
- row "贷款用途 消费 借款期限 1" [ref=e491]:
- rowheader "贷款用途" [ref=e492]
- cell "消费" [ref=e493]
- rowheader "借款期限" [ref=e494]
- cell "1" [ref=e495]
- rowgroup [ref=e496]:
- row "是否有经营佐证 否 循环功能 否" [ref=e497]:
- rowheader "是否有经营佐证" [ref=e498]
- cell "否" [ref=e499]
- rowheader "循环功能" [ref=e500]
- cell "否" [ref=e501]
- rowgroup [ref=e502]:
- row "抵质押类型 - 抵质押物是否三方所有 否" [ref=e503]:
- rowheader "抵质押类型" [ref=e504]
- cell "-" [ref=e505]
- rowheader "抵质押物是否三方所有" [ref=e506]
- cell "否" [ref=e507]
- generic [ref=e508]:
- generic [ref=e511]: 模型输出
- generic [ref=e513]:
- generic [ref=e515]:
- generic [ref=e517] [cursor=pointer]:
- generic [ref=e519] [cursor=pointer]:
- tablist [ref=e521]:
- tab "基本信息" [selected] [ref=e523]
- tab "忠诚度分析" [ref=e524]
- tab "贡献度分析" [ref=e525]
- tab "关联度分析" [ref=e526]
- tab "贷款特征" [ref=e527]
- tab "风险度分析" [ref=e528]
- tab "测算结果" [ref=e529]
- tabpanel "基本信息" [ref=e531]:
- table [ref=e534]:
- rowgroup [ref=e535]:
- row "客户内码 CUST20260121001 客户名称 张*" [ref=e536]:
- rowheader "客户内码" [ref=e537]
- cell "CUST20260121001" [ref=e538]
- rowheader "客户名称" [ref=e539]
- cell "张*" [ref=e540]
- rowgroup [ref=e541]:
- row "证件类型 身份证 证件号码 3301********1234" [ref=e542]:
- rowheader "证件类型" [ref=e543]
- cell "身份证" [ref=e544]
- rowheader "证件号码" [ref=e545]
- cell "3301********1234" [ref=e546]
- rowgroup [ref=e547]:
- row "基准利率 4.35 %" [ref=e548]:
- rowheader "基准利率" [ref=e549]
- cell "4.35 %" [ref=e550]
- text:

View File

@@ -0,0 +1,18 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "上虞利率定价系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]
- img [ref=e18]
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1,28 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "若依管理系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]: admin
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]: admin123
- img [ref=e18]
- generic [ref=e21]:
- generic [ref=e22]:
- textbox "验证码" [ref=e23]
- img [ref=e25]
- generic [ref=e27]:
- img
- generic [ref=e28] [cursor=pointer]:
- generic [ref=e29]:
- checkbox "记住密码"
- generic [ref=e31]: 记住密码
- button "登 录" [ref=e34] [cursor=pointer]:
- generic [ref=e35]: 登 录
- generic [ref=e36]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:
- alert [ref=e37]:
- generic [ref=e38]:
- paragraph [ref=e39]: 系统接口500异常

View File

@@ -0,0 +1,18 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "若依管理系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]: admin
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]: admin123
- img [ref=e18]
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

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

View File

@@ -1,11 +1,11 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.1</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.2</h1>
<h4 align="center">基于SpringBoot+Vue前后端分离的Java快速开发框架</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.1-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.2-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>
@@ -13,16 +13,37 @@
若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 本仓库为RuoYi-Vue的Spring Boot 2 的版本,保持同步更新。
* 前端采用Vue、Element UI。
* 后端采用Spring Boot、Spring Security、Redis & Jwt。
* 权限认证使用Jwt支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
* 高效率开发,使用代码生成器可以一键生成前后端代码。
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3),保持同步更新。
* 提供了单应用版本[RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast)Oracle版本[RuoYi-Vue-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle),保持同步更新。
* 不分离版本,请移步[RuoYi](https://gitee.com/y_project/RuoYi),微服务版本,请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
# 版本分支
RuoYi-Vue 后端项目提供 Spring Boot 2.x / 3.x / 4.x 多版本分支的并行维护。
| 名称 | 说明 | 地址 |
| :---------------- | :------------------------ | :------------------------------------------------------ |
| master 默认分支 | Spring Boot 4.x (JDK 17+) | https://gitee.com/y_project/RuoYi-Vue |
| springboot3 分支 | Spring Boot 3.x (JDK 17+) | https://gitee.com/y_project/RuoYi-Vue/tree/springboot3 |
| springboot2 分支 | Spring Boot 2.x (JDK 8+) | https://gitee.com/y_project/RuoYi-Vue/tree/springboot2 |
RuoYi-Vue 前端项目提供 Vue 2.x / 3.x / JavaScript TypeScript 版本均可混用搭配
| 项目名称 | **RuoYi-Vue** | **RuoYi-Vue3** | **RuoYi-Vue3-TypeScript** |
| :--- | :--- | :--- | :--- |
| **前端框架** | Vue 2 | Vue 3 | Vue 3 |
| **脚本语言** | JavaScript | JavaScript | TypeScript |
| **构建工具** | Vue CLI | Vite | Vite |
| **UI 组件库** | Element UI | Element Plus | Element Plus |
| **状态管理** | Vuex | Pinia | Pinia |
| **路由管理** | Vue Router 3 | Vue Router 4 | Vue Router 4 |
| **核心特点** | 1. 技术栈经典稳定<br>2. 社区资料丰富<br>3. 当前维护重心已转移 | 1. 现代前端技术栈<br>2. 开发体验与性能更优<br>3. 官方主推的活跃版本 | 1. 类型加持,减少沟通成本<br>2. 开发时有提示,效率更高<br>3. 多人协作企业级开发项目 |
| **仓库地址** | [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) | [RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3) | [RuoYi-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Vue3/tree/typescript) |
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
@@ -92,4 +113,4 @@
## 若依前后端分离交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) 点击按钮入群。
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/已满-174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [![加入QQ群](https://img.shields.io/badge/127358632-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。

View File

@@ -1,12 +1,12 @@
@echo off
echo.
echo [信息] 清理工程target生成路径。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean
@echo off
echo.
echo [信息] 清理工程target生成路径。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean
pause

View File

@@ -1,12 +1,12 @@
@echo off
echo.
echo [信息] 打包Web工程生成war/jar包文件。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean package -Dmaven.test.skip=true
@echo off
echo.
echo [信息] 打包Web工程生成war/jar包文件。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean package -Dmaven.test.skip=true
pause

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 "$@"

116
bin/prod/restart_java_test.sh Executable file
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

@@ -1,14 +1,14 @@
@echo off
echo.
echo [信息] 使用Jar命令运行Web工程。
echo.
cd %~dp0
cd ../ruoyi-admin/target
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar %JAVA_OPTS% ruoyi-admin.jar
cd bin
@echo off
echo.
echo [信息] 使用Jar命令运行Web工程。
echo.
cd %~dp0
cd ../ruoyi-admin/target
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar %JAVA_OPTS% ruoyi-admin.jar
cd bin
pause

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,43 @@
# 本地 Tomcat 与 TongWeb 打包并存后端实施计划
## 目标
- 恢复本地 `jar + 内嵌 Tomcat` 启动链路
- 保留服务器 `war + TongWeb` 部署链路
- 一次 `mvn package` 同时产出 `ruoyi-admin.jar``ruoyi-admin.war`
## 改动范围
- Maven 打包配置
- 后端启动与部署脚本
- 脚本测试
- 相关运行文档和实施记录
## 实施步骤
1. 先修改脚本测试,重新定义目标行为
- 本地测试脚本期望 `restart_java_backend.sh` 使用 `java -jar`
- 生产测试脚本继续期望 TongWeb 使用 `war`
2. 调整 Maven 打包配置
- `ruoyi-admin` 恢复主产物 `jar`
- 增加附加 `war` 产物
- 恢复本地运行所需的内嵌 Tomcat 依赖
3. 调整脚本
- 本地脚本改回管理 `ruoyi-admin.jar`
- 生产脚本继续管理 `ruoyi-admin.war`
4. 更新文档
- 更新运行说明
- 新增本次实施记录
5. 执行验证
## 验证要求
- `sh bin/restart_java_backend_test.sh`
- `sh bin/prod/restart_java_test.sh`
- `sh bin/prod/deploy_from_package_test.sh`
- `sh -n bin/restart_java_backend.sh`
- `sh -n bin/prod/restart_java.sh`
- `sh -n bin/prod/deploy_from_package.sh`
- `mvn -pl ruoyi-admin -am package -DskipTests`
- 确认 `ruoyi-admin/target/ruoyi-admin.jar`
- 确认 `ruoyi-admin/target/ruoyi-admin.war`

View File

@@ -0,0 +1,147 @@
# 本地 Tomcat 运行与 TongWeb 打包并存设计
## 背景
当前项目已经被调整为统一的 TongWeb `war` 交付模式,这会导致本地开发时也必须围绕 TongWeb 组织启动流程,不符合当前开发诉求。
本次目标是同时保留两条链路:
- 本地开发运行继续使用内嵌 Tomcat
- 打包交付继续支持服务器上的 TongWeb
并且要求一次 `mvn package` 同时产出本地运行所需的 `jar` 和服务器部署所需的 `war`
## 目标
- 保留 `IDEA``mvn spring-boot:run`、本地脚本直启后端的开发体验
- 保留面向 TongWeb 的 `war` 交付方式
- `mvn package` 后同时得到 `ruoyi-admin.jar``ruoyi-admin.war`
- 本地不强依赖安装 TongWeb
- 服务器部署脚本继续只消费 `war`
## 非目标
- 不新增第二个后端启动模块
- 不拆分额外的部署工程
- 不修改前端构建方式
- 不引入“兼容模式”“降级模式”之类额外分支逻辑
## 设计方案
### 1. 构建产物设计
`ruoyi-admin` 恢复为以 `jar` 为主产物的 Spring Boot 应用,用于本地开发运行。
在同一个 Maven 模块中补充 `war` 打包步骤,使一次 `mvn package` 后同时得到:
- `ruoyi-admin/target/ruoyi-admin.jar`
- `ruoyi-admin/target/ruoyi-admin.war`
这样本地和服务器都从同一套源码构建,但消费不同产物:
- 本地消费 `jar`
- 服务器消费 `war`
### 2. 依赖设计
为了保证本地可继续走内嵌 Tomcat
- 恢复 Web 模块中的内嵌 Tomcat 依赖链
- 保持 `spring-boot:run``java -jar` 均可正常工作
为了保证 TongWeb 外部容器部署:
- 打出的 `war` 不能把容器自身实现错误打包成部署冲突形式
- `Servlet API` 继续按外部容器提供的思路处理
本质上,本地运行和 TongWeb 部署共享同一套业务代码,但运行容器不同。
### 3. 启动脚本设计
#### 本地脚本
`bin/restart_java_backend.sh` 恢复为本地开发脚本:
- 执行 Maven 打包
- 使用 `ruoyi-admin.jar`
- 通过 `java -jar` 管理本地后端进程
这条链路不再依赖 `TONGWEB_HOME`
#### 生产脚本
以下脚本保持 TongWeb 交付模型:
- `bin/prod/restart_java.sh`
- `bin/prod/deploy_from_package.sh`
- `bin/prod/deploy_release.sh`
它们继续只处理 `ruoyi-admin.war`,不回退到 `jar`
### 4. 本地与服务器联调设计
本地开发时不要求本机安装 TongWeb。
如果需要验证 TongWeb 运行环境,只通过两种方式完成:
- 打包后部署到服务器 TongWeb 验证
- 本地系统直接调用服务器上已部署的 TongWeb 地址联调
这意味着:
- 本地开发链路只围绕 `jar + Tomcat`
- 服务器部署链路只围绕 `war + TongWeb`
## 验证方案
### 构建验证
执行:
```sh
mvn -pl ruoyi-admin -am package -DskipTests
```
确认同时存在:
- `ruoyi-admin/target/ruoyi-admin.jar`
- `ruoyi-admin/target/ruoyi-admin.war`
### 本地运行验证
执行:
```sh
sh bin/restart_java_backend.sh restart
```
确认本地以 `java -jar` 正常运行。
### TongWeb 脚本验证
执行:
```sh
sh bin/prod/restart_java_test.sh
sh bin/prod/deploy_from_package_test.sh
```
确认 TongWeb 侧仍围绕 `war` 工作。
## 影响范围
- `ruoyi-admin` Maven 打包配置
- Web 相关模块的容器依赖声明
- 本地后端脚本
- 生产 TongWeb 脚本
- 运行文档与实施记录
## 结论
本方案采用最短路径实现“双产物、双运行链路并存”:
- 本地运行继续走内嵌 Tomcat
- 服务器部署继续走 TongWeb
- 一次打包同时产出 `jar``war`
在不新增模块、不扩散复杂度的前提下,满足开发与部署两端的实际需要。

View File

@@ -0,0 +1,39 @@
# 东方通替换 Tomcat 后端实施计划
## 目标
- 将后端交付形态从内嵌 Tomcat 的 `jar` 调整为部署到东方通 TongWeb 的 `war`
- 清理当前发布链路中围绕 `java -jar` / `ruoyi-admin.jar` 的脚本约定
- 保持现有前端发布方式和 Nginx 入口不变,后端仍沿用 `63310` 作为反向代理目标端口
## 改动范围
- Maven 构建
- 调整 `ruoyi-admin` 打包类型为 `war`
- 去除模块链路中的嵌入式 Tomcat 打包依赖
- 明确 Servlet API 由外部容器提供
- 部署脚本
- 将生产部署脚本中的后端产物从 `ruoyi-admin.jar` 切换为 `ruoyi-admin.war`
- 将生产重启脚本从 Java 进程启停改为 TongWeb 容器启停与 `war` 发布
- 调整本地后端重启脚本,使其面向 TongWeb 进行构建和部署
- 运行文档
- 更新本地安装手册中的后端环境说明,改为 TongWeb
- 新增本次改动实施记录
## 实施步骤
1. 先修改现有脚本测试,明确新的 `war + TongWeb` 约束
2. 调整 Maven 配置,产出 `ruoyi-admin.war`
3. 修改生产部署脚本和本地重启脚本
4. 更新运行文档与实施记录
5. 执行脚本测试、语法校验和 Maven 打包验证
## 验证要求
- `mvn -pl ruoyi-admin -am clean package -DskipTests` 成功,且产物为 `ruoyi-admin.war`
- `sh bin/prod/restart_java_test.sh` 成功
- `sh bin/prod/deploy_from_package_test.sh` 成功
- `sh bin/restart_java_backend_test.sh` 成功
- `sh -n bin/prod/restart_java.sh`
- `sh -n bin/prod/deploy_from_package.sh`
- `sh -n bin/restart_java_backend.sh`

View File

@@ -0,0 +1,41 @@
# RuoYi-Vue springboot2 后端迁移实施计划
## 目标
以上游 `RuoYi-Vue/springboot2` 为后端框架基线,将当前项目的后端框架层整体回退并重对齐到 Spring Boot 2 / Java 8同时恢复 `ruoyi-loan-pricing` 业务模块和管理端业务接入配置。
## 范围
-`pom.xml`
- `ruoyi-admin`
- `ruoyi-common`
- `ruoyi-framework`
- `ruoyi-generator`
- `ruoyi-quartz`
- `ruoyi-system`
- `ruoyi-loan-pricing`
- `sql`
## 执行步骤
1. 备份当前后端业务模块与业务配置
2. 用上游 `springboot2` 覆盖根 POM 和基础后端模块
3. 恢复 `ruoyi-loan-pricing` 模块目录
4. 在根 POM 与 `ruoyi-admin/pom.xml` 中重新挂载 `ruoyi-loan-pricing`
5. 恢复 `ruoyi-admin/src/main/resources` 中的 `loan-pricing` 业务配置
6. 检查并修正 `ruoyi-loan-pricing` 中不兼容 Spring Boot 2 的依赖、注解和包引用
7. 校正 Mapper、资源文件和测试依赖保证模块能参与 Maven 聚合构建
8. 保留并整理业务 SQL 脚本
9. 在 Java 8 环境下执行后端编译与关键测试
## 验证要求
- `mvn -pl ruoyi-admin -am test` 至少能够完成依赖解析和关键模块测试
- `mvn -pl ruoyi-admin -am package -DskipTests` 能通过
- `ruoyi-loan-pricing` 模块可被 `ruoyi-admin` 正常引用
## 注意事项
- 不保留 Spring Boot 3 / Java 17 双配置
- 不引入兼容层或过渡层
- 若业务模块使用了 Boot 3 专属依赖,直接改为 Boot 2 可运行实现

View File

@@ -0,0 +1,37 @@
# RuoYi-Vue springboot2 前端迁移实施计划
## 目标
以上游 `RuoYi-Vue/springboot2` 为前端基线,将当前项目前端整体回退并重对齐到上游 `ruoyi-ui`,然后恢复 `loanPricing` 业务页面、接口调用、路由与相关依赖。
## 范围
- `ruoyi-ui/package.json`
- `ruoyi-ui/src`
- `ruoyi-ui/public`
- `ruoyi-ui/build`
- `ruoyi-ui/tests`
## 执行步骤
1. 备份当前 `loanPricing` 页面、接口文件、路由改动和业务测试脚本
2. 用上游 `springboot2``ruoyi-ui` 覆盖当前前端框架层
3. 恢复 `src/views/loanPricing` 页面目录
4. 恢复 `src/api/loanPricing` 接口文件
5. 将业务路由重新挂回 `src/router/index.js`
6. 恢复业务所需的前端依赖与测试脚本
7.`nvm` 切换到合适的 Node 版本后重新安装依赖
8. 执行前端构建与页面联调验证
## 验证要求
- `npm install` 成功
- `npm run build:prod` 成功
- `loanPricing` 页面路由可访问
- 页面基础交互和接口调用链路未丢失
## 注意事项
- 前端直接以 `springboot2` 上游为准,不保留当前非业务性的历史前端定制
- Node 版本必须通过 `nvm` 控制
- 测试完成后要关闭前端调试进程

View File

@@ -0,0 +1,120 @@
# RuoYi-Vue springboot2 基线迁移设计
## 1. 目标
本次迁移以上游 `https://gitee.com/y_project/RuoYi-Vue/tree/springboot2` 为唯一框架基线,先将当前仓库整体回退并重对齐到该基线,再迁回现有业务模块与业务页面,最终形成一个“框架层跟随上游、业务层保留现状”的项目结构。
本次迁移不采用兼容层、补丁层或双栈并存方案,不保留 Spring Boot 3 / Java 17 的框架实现。
## 2. 现状与目标差异
当前仓库已经是 RuoYi 多模块工程,但后端已升级到 `Spring Boot 3.5.x``Java 17`,并引入了以下业务定制:
- 后端业务模块:`ruoyi-loan-pricing`
- 前端业务页面:`ruoyi-ui/src/views/loanPricing`
- 前端业务接口:`ruoyi-ui/src/api/loanPricing`
- 管理端业务接入:`ruoyi-admin` 中的业务依赖与配置
- 业务 SQL`sql/loan_pricing_*``sql/model_*``sql/loan_pricing_menu.sql`
目标上游 `springboot2` 分支采用:
- `RuoYi-Vue 3.9.2`
- `Spring Boot 2.5.15`
- `Java 8`
- `Vue 2 + Element UI`
因此本次迁移的本质是:先将框架层彻底切回 Spring Boot 2 基线,再把利率定价业务重新挂载到新的基线上。
## 3. 迁移范围
### 3.1 框架层
以下内容以上游 `springboot2` 版本为准:
- 根目录框架文件与脚本
-`pom.xml`
- `ruoyi-admin`
- `ruoyi-common`
- `ruoyi-framework`
- `ruoyi-generator`
- `ruoyi-quartz`
- `ruoyi-system`
- `ruoyi-ui`
- 上游自带 `sql` 基础脚本
### 3.2 业务层
以下内容需要从当前仓库迁回到新基线:
- `ruoyi-loan-pricing` 全模块
- `ruoyi-admin` 中与 `loan-pricing` 相关的业务配置、业务依赖
- `ruoyi-ui/src/views/loanPricing` 页面
- `ruoyi-ui/src/api/loanPricing` 接口文件
- `ruoyi-ui/src/router/index.js` 中的业务路由
- 业务 SQL、部署脚本、项目文档
## 4. 实施策略
### 4.1 基线覆盖策略
采用“上游覆盖 + 业务回贴”模式:
1. 先备份当前业务目录与业务配置
2. 将上游 `springboot2` 内容覆盖到当前仓库
3. 恢复业务模块、业务页面、业务配置和业务 SQL
4. 处理 Spring Boot 2 下的编译和运行差异
### 4.2 后端回贴策略
后端只保留一套 Spring Boot 2 实现,不额外保留 Spring Boot 3 兼容写法。若 `ruoyi-loan-pricing` 内存在 Boot 3 / Jakarta / SpringDoc 相关依赖或 API则直接改回 Boot 2 可运行写法。
### 4.3 前端回贴策略
前端以 `springboot2` 上游 `ruoyi-ui` 为基础,回贴 `loanPricing` 页面、接口调用、路由和必要依赖;不保留与当前业务无关的历史定制。
## 5. 风险点与处理原则
### 5.1 框架依赖回退风险
风险:
- `springdoc``jakarta.*`、Boot 3 专用依赖不兼容 Boot 2
- 测试依赖、插件版本、JDK 版本需要一并回退
处理原则:
- 以 Boot 2 上游依赖为准
- 业务模块仅保留完成业务所必需的依赖
### 5.2 业务接入点遗漏风险
风险:
- `ruoyi-admin` 配置遗漏
- 前端路由、菜单、接口路径遗漏
- SQL 脚本与权限菜单脚本遗漏
处理原则:
- 逐类枚举迁移对象
- 迁移后通过编译、页面访问、接口调用进行闭环验证
## 6. 验证标准
迁移完成后至少满足以下条件:
- Maven 多模块在 Spring Boot 2 / Java 8 环境下可编译
- `ruoyi-admin` 可启动
- `ruoyi-ui` 在指定 Node 版本下可安装并构建
- `loanPricing` 页面路由可访问
- 利率定价相关接口类与 Mapper 可通过编译
- 业务 SQL 与菜单脚本仍保留在仓库内
## 7. 产出物
本次任务最终需产出:
- 本设计文档
- 后端实施计划文档
- 前端实施计划文档
- 迁移实施记录文档

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,34 @@
# 本地 Tomcat 与 TongWeb 双产物实施记录
## 本次改动
-`ruoyi-admin` 的主打包方式从 `war` 恢复为 `jar`
- 恢复 `spring-boot-maven-plugin``repackage`,保证本地可直接运行 `ruoyi-admin.jar`
-`ruoyi-admin` 中增加附加 `war` 打包步骤,使 `mvn package` 同时产出:
- `ruoyi-admin.jar`
- `ruoyi-admin.war`
-`war` 打包中排除内嵌 Tomcat 相关 jar避免 TongWeb 部署时容器冲突
-`bin/restart_java_backend.sh` 恢复为本地 `java -jar` 启动链路
- 保持 `bin/prod/restart_java.sh``bin/prod/deploy_from_package.sh` 继续消费 `ruoyi-admin.war`
- 更新 `bin/run.bat`,恢复为本地 `jar` 启动入口
- 新增设计文档 `doc/2026-04-13-local-tomcat-remote-tongweb-design.md`
- 新增实施计划 `doc/2026-04-13-local-tomcat-remote-tongweb-backend-plan.md`
## 验证结果
- 已执行 `sh bin/restart_java_backend_test.sh`
- 已执行 `sh bin/prod/restart_java_test.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 已执行 `sh -n bin/restart_java_backend.sh`
- 已执行 `sh -n bin/prod/restart_java.sh`
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `mvn -pl ruoyi-admin -am clean package -DskipTests`
- 已确认产物:
- `ruoyi-admin/target/ruoyi-admin.jar`
- `ruoyi-admin/target/ruoyi-admin.war`
## 结果说明
- 本地开发运行继续使用内嵌 Tomcat不要求本机安装 TongWeb
- 服务器部署继续使用 TongWeb只消费 `war`
- 一次打包即可同时得到本地运行产物和 TongWeb 部署产物

View File

@@ -0,0 +1,25 @@
# Tomcat 替换为东方通实施记录
## 本次改动
-`ruoyi-admin` 打包方式从 `jar` 调整为 `war`
-`ruoyi-admin` 中显式声明 `spring-boot-starter-tomcat``provided`
-`ruoyi-framework``ruoyi-loan-pricing` 中排除 `spring-boot-starter-web` 传递进来的嵌入式 Tomcat
-`ruoyi-common` 中的 `jakarta.servlet-api` 调整为 `provided`
- 删除 `application-dev.yml``application-uat.yml``application-pro.yml` 中仅对内嵌 Tomcat 生效的 `server.tomcat` 配置
-`bin/prod/restart_java.sh``java -jar` 启停改为 TongWeb 启停与 `war` 同步
-`bin/prod/deploy_from_package.sh``bin/prod/deploy_release.sh` 的后端交付物从 `ruoyi-admin.jar` 改为 `ruoyi-admin.war`
-`bin/restart_java_backend.sh` 改为本地构建 `war` 并发布到 TongWeb
- 更新 `deploy/2026-03-31-local-nginx-java-install-manual.md`,将后端运行环境说明改为 TongWeb
- 新增后端实施计划 `doc/2026-04-13-tongweb-replace-tomcat-backend-plan.md`
## 验证结果
- 已执行 `sh bin/prod/restart_java_test.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 已执行 `sh bin/restart_java_backend_test.sh`
## 说明
- 本次替换按当前项目 `Spring Boot 3 + Jakarta Servlet` 路线落地,要求实际使用的东方通版本能够承载 Jakarta 体系应用
- Nginx 入口和反向代理端口保持不变,仍通过 `63310` 转发到后端容器

View File

@@ -0,0 +1,105 @@
# RuoYi-Vue springboot2 基线迁移实施记录
## 本次完成内容
-`RuoYi-Vue/springboot2` 为基线覆盖当前仓库的框架层:
-`pom.xml`
- `ruoyi-admin`
- `ruoyi-common`
- `ruoyi-framework`
- `ruoyi-generator`
- `ruoyi-quartz`
- `ruoyi-system`
- `ruoyi-ui`
- `sql` 基础脚本
- 恢复并接回业务模块与业务页面:
- `ruoyi-loan-pricing`
- `ruoyi-ui/src/views/loanPricing`
- `ruoyi-ui/src/api/loanPricing`
- `ruoyi-ui/src/router/index.js` 中的业务路由
- `ruoyi-admin` 中的业务配置
-`ruoyi-loan-pricing` 从 Boot 3 / Java 17 写法回退到 Boot 2 / Java 8 可编译形态:
- 移除 `springdoc` 注解和依赖
-`jakarta.*` 改回 `javax.*`
-`String.repeat` 改为 Java 8 兼容实现
- 将模型调用改为模块内 `RestTemplate + form-urlencoded`
- 补回当前项目的“无 Redis”本地缓存实现
- `InMemoryCacheEntry`
- `InMemoryCacheStats`
- `InMemoryCacheStore`
- 本地缓存版 `RedisCache`
- 本地缓存版 `CacheController`
- 调整前端业务依赖与脚本:
- 恢复 `crypto-js`
- 恢复 `splitpanes`
- 恢复 `ruoyi-ui/tests` 下 4 个业务测试脚本
- 恢复项目原有部署辅助脚本:
- `bin/prod/*.sh`
- `bin/restart_java_backend*.sh`
- 修正本地启动所需配置:
- `logback.xml` 日志目录改为项目内 `logs`
- `application.yml` 补齐 `swagger``redis``mybatis`
- 增加 `ruoyi-loan-pricing` 模块挂载和依赖声明
## 验证结果
### 后端编译验证
已通过:
- `export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home && export PATH="$JAVA_HOME/bin:$PATH" && mvn -pl ruoyi-admin -am -DskipTests package`
- `export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home && export PATH="$JAVA_HOME/bin:$PATH" && mvn -pl ruoyi-admin -am install -DskipTests`
### 前端构建验证
已通过:
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm install`
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`
### 前后端联调验证
已验证:
- 前端开发服务成功启动于 `http://localhost:1024`
- 浏览器打开 `http://localhost:1024/login`,页面标题显示为“若依管理系统”
- 浏览器点击登录后成功进入 `/index`
- 页面已实际渲染“流程列表”业务页面
- 通过前端代理访问 `http://localhost:1024/dev-api/captchaImage` 返回:
- `{"msg":"操作成功","code":200,"captchaEnabled":false}`
- 源码态后端在 `Java 8 + spring-boot:run` 模式下可启动成功
- 登录接口可成功返回 token
- `POST /login`
## 联调中发现的遗留问题
浏览器进入系统后,页面出现两条后端错误提示:
1. 数据库缺少 `sys_notice_read`
2. `GET /loanPricing/workflow/list` 仍返回 `TooManyResultsException`
其中第 1 项已确认当前仓库 SQL 中已有对应建表语句:
- `sql/ry_20260330.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
说明当前代码迁移已落地,但联调数据库尚未完全补齐到 `springboot2` 基线所需表结构。
第 2 项已经定位到当前运行态仍存在列表查询返回值与 MyBatis 映射结果不一致的问题,表现为:
- `GET /loanPricing/workflow/list?pageNum=1&pageSize=10`
- 返回:`{"msg":"nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 15","code":500}`
## 结论
本次代码迁移已经完成:
- 框架层已切回 `RuoYi-Vue springboot2`
- 业务模块和业务页面已接回
- Java 8 / Spring Boot 2 / Vue 2 的编译与构建链路已打通
- 浏览器已成功进入业务页面与登录链路
当前剩余问题集中在:
- 联调数据库尚未补齐 `sys_notice_read`
- `workflow/list` 运行态查询仍需继续收口

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 "补充生产初始化数据库导出前端实施记录"
```

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