Compare commits
26 Commits
7c563b3315
...
codex/migr
| Author | SHA1 | Date | |
|---|---|---|---|
| 28088d43a8 | |||
| f1e4b26800 | |||
| 235672304a | |||
| ec4a7c09db | |||
| 5839a76f87 | |||
| fa0b446699 | |||
| f001047d0c | |||
| 09707d312e | |||
| 8e6eb5b382 | |||
| 62784ee81a | |||
| 1e9340bbda | |||
| 541969a837 | |||
| 351fae8cd3 | |||
| 54eabaebd8 | |||
| f874e2d942 | |||
| 9b35d04e50 | |||
| 3a8f37f547 | |||
| f8b2bf2afc | |||
| 3ce3c438a9 | |||
| db5735897d | |||
| 99cdaacf10 | |||
| 0f9c9b30cd | |||
| 14e72f0e5e | |||
| 1c2171ba24 | |||
| d96b8a8740 | |||
| 4db4f542a5 |
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": []
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
88
.gitignore
vendored
@@ -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
|
||||
|
||||
18
.playwright-cli/page-2026-04-11T07-16-36-054Z.yml
Normal file
18
.playwright-cli/page-2026-04-11T07-16-36-054Z.yml
Normal 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:
|
||||
20
.playwright-cli/page-2026-04-11T07-17-14-786Z.yml
Normal file
20
.playwright-cli/page-2026-04-11T07-17-14-786Z.yml
Normal 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:
|
||||
1
.playwright-cli/page-2026-04-11T07-18-09-957Z.yml
Normal file
1
.playwright-cli/page-2026-04-11T07-18-09-957Z.yml
Normal file
@@ -0,0 +1 @@
|
||||
- generic [ref=e2]:
|
||||
191
.playwright-cli/page-2026-04-11T07-18-43-284Z.yml
Normal file
191
.playwright-cli/page-2026-04-11T07-18-43-284Z.yml
Normal 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:
|
||||
18
.playwright-cli/page-2026-04-11T07-25-40-868Z.yml
Normal file
18
.playwright-cli/page-2026-04-11T07-25-40-868Z.yml
Normal 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:
|
||||
1
.playwright-cli/page-2026-04-11T07-25-55-827Z.yml
Normal file
1
.playwright-cli/page-2026-04-11T07-25-55-827Z.yml
Normal file
@@ -0,0 +1 @@
|
||||
- generic [ref=e2]:
|
||||
1
.playwright-cli/page-2026-04-11T07-26-20-300Z.yml
Normal file
1
.playwright-cli/page-2026-04-11T07-26-20-300Z.yml
Normal file
@@ -0,0 +1 @@
|
||||
- generic [ref=e2]:
|
||||
1
.playwright-cli/page-2026-04-11T07-26-34-973Z.yml
Normal file
1
.playwright-cli/page-2026-04-11T07-26-34-973Z.yml
Normal file
@@ -0,0 +1 @@
|
||||
- generic [ref=e2]:
|
||||
28
.playwright-cli/page-2026-04-14T05-22-21-308Z.yml
Normal file
28
.playwright-cli/page-2026-04-14T05-22-21-308Z.yml
Normal 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异常
|
||||
18
.playwright-cli/page-2026-04-14T05-39-30-657Z.yml
Normal file
18
.playwright-cli/page-2026-04-14T05-39-30-657Z.yml
Normal 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:
|
||||
@@ -8,7 +8,7 @@
|
||||
- 不开启subagent
|
||||
|
||||
## 文档
|
||||
- 根据设计文档产出前后端项目的实施计划时,输出两份执行文档,一份为后端的实施计划,一份为前端的实施计划
|
||||
- 如果是前后端开发任务,根据设计文档产出实施计划时,输出两份执行文档,一份为后端的实施计划,一份为前端的实施计划
|
||||
- 每一次改动都需要留下实施文档,记录修改的内容
|
||||
- 每次写设计文档的时候,都要检查一下保存路径是否正确
|
||||
|
||||
|
||||
33
README.md
33
README.md
@@ -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)
|
||||
|
||||
# 版本分支
|
||||
|
||||
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群: [](https://jq.qq.com/?_wv=1027&k=5bVB1og) [](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [](https://jq.qq.com/?_wv=1027&k=51G72yr) [](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [](https://jq.qq.com/?_wv=1027&k=SpyH2875) [](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) 点击按钮入群。
|
||||
QQ群: [](https://jq.qq.com/?_wv=1027&k=5bVB1og) [](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [](https://jq.qq.com/?_wv=1027&k=51G72yr) [](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [](https://jq.qq.com/?_wv=1027&k=SpyH2875) [](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。
|
||||
@@ -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
|
||||
@@ -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
257
bin/prod/deploy_from_package.sh
Executable 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 "$@"
|
||||
262
bin/prod/deploy_from_package_test.sh
Executable file
262
bin/prod/deploy_from_package_test.sh
Executable 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
245
bin/prod/deploy_release.sh
Executable 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
244
bin/prod/install_env.sh
Executable 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
208
bin/prod/restart_java.sh
Executable 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
116
bin/prod/restart_java_test.sh
Executable 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 "$@"
|
||||
@@ -83,7 +83,16 @@ collect_pids() {
|
||||
fi
|
||||
fi
|
||||
|
||||
marker_pids=$(pgrep -f "$APP_MARKER" 2>/dev/null || true)
|
||||
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
|
||||
@@ -225,7 +234,6 @@ restart_action() {
|
||||
main() {
|
||||
ensure_command mvn
|
||||
ensure_command lsof
|
||||
ensure_command pgrep
|
||||
ensure_command ps
|
||||
ensure_command tail
|
||||
|
||||
|
||||
42
bin/restart_java_backend_test.sh
Executable file
42
bin/restart_java_backend_test.sh
Executable 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 "$@"
|
||||
26
bin/run.bat
26
bin/run.bat
@@ -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
|
||||
190
deploy/2026-03-31-local-nginx-java-install-manual.md
Normal file
190
deploy/2026-03-31-local-nginx-java-install-manual.md
Normal 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
BIN
deploy/deploy.zip
Normal file
Binary file not shown.
45
deploy/nginx.conf
Normal file
45
deploy/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
84
doc/2026-04-03-retail-display-fields-backend-plan.md
Normal file
84
doc/2026-04-03-retail-display-fields-backend-plan.md
Normal 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 列
|
||||
113
doc/2026-04-03-retail-display-fields-design.md
Normal file
113
doc/2026-04-03-retail-display-fields-design.md
Normal 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. 非目标
|
||||
|
||||
- 不调整企业详情页
|
||||
- 不修改模型计算逻辑
|
||||
- 不重构页面布局
|
||||
81
doc/2026-04-03-retail-display-fields-frontend-plan.md
Normal file
81
doc/2026-04-03-retail-display-fields-frontend-plan.md
Normal 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: 启动前后端并打开个人流程详情页验证**
|
||||
|
||||
使用浏览器打开新的个人流程详情页,确认:
|
||||
|
||||
- 流程详情“业务信息”出现 `借款期限`
|
||||
- 模型输出“测算结果”出现并可查看以下字段
|
||||
- 历史利率
|
||||
- 产品最低利率下限
|
||||
- 平滑幅度
|
||||
- 最终测算利率
|
||||
- 参考利率
|
||||
238
doc/2026-04-09-shangyu-retail-input-params-backend-plan.md
Normal file
238
doc/2026-04-09-shangyu-retail-input-params-backend-plan.md
Normal 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 "完成个人测算输入参数后端联调"
|
||||
```
|
||||
322
doc/2026-04-09-shangyu-retail-input-params-design.md
Normal file
322
doc/2026-04-09-shangyu-retail-input-params-design.md
Normal 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. 非目标
|
||||
|
||||
- 不调整企业新增弹窗
|
||||
- 不修改企业模型调用参数
|
||||
- 不修改流程列表逻辑
|
||||
- 不改模型返回字段映射逻辑
|
||||
212
doc/2026-04-09-shangyu-retail-input-params-frontend-plan.md
Normal file
212
doc/2026-04-09-shangyu-retail-input-params-frontend-plan.md
Normal 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 "完成个人测算输入参数前端联调"
|
||||
```
|
||||
43
doc/2026-04-13-local-tomcat-remote-tongweb-backend-plan.md
Normal file
43
doc/2026-04-13-local-tomcat-remote-tongweb-backend-plan.md
Normal 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`
|
||||
147
doc/2026-04-13-local-tomcat-remote-tongweb-design.md
Normal file
147
doc/2026-04-13-local-tomcat-remote-tongweb-design.md
Normal 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`
|
||||
|
||||
在不新增模块、不扩散复杂度的前提下,满足开发与部署两端的实际需要。
|
||||
39
doc/2026-04-13-tongweb-replace-tomcat-backend-plan.md
Normal file
39
doc/2026-04-13-tongweb-replace-tomcat-backend-plan.md
Normal 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`
|
||||
41
doc/2026-04-14-ruoyi-vue-springboot2-backend-plan.md
Normal file
41
doc/2026-04-14-ruoyi-vue-springboot2-backend-plan.md
Normal 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 可运行实现
|
||||
37
doc/2026-04-14-ruoyi-vue-springboot2-frontend-plan.md
Normal file
37
doc/2026-04-14-ruoyi-vue-springboot2-frontend-plan.md
Normal 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` 控制
|
||||
- 测试完成后要关闭前端调试进程
|
||||
120
doc/2026-04-14-ruoyi-vue-springboot2-migration-design.md
Normal file
120
doc/2026-04-14-ruoyi-vue-springboot2-migration-design.md
Normal 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. 产出物
|
||||
|
||||
本次任务最终需产出:
|
||||
|
||||
- 本设计文档
|
||||
- 后端实施计划文档
|
||||
- 前端实施计划文档
|
||||
- 迁移实施记录文档
|
||||
@@ -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": "企业",
|
||||
|
||||
22
doc/implementation-report-2026-03-31-deploy-folder-docs.md
Normal file
22
doc/implementation-report-2026-03-31-deploy-folder-docs.md
Normal 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 配置保持一致
|
||||
- 已人工核对手册中的目录、端口和脚本引用与当前交付物保持一致
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
- 验证结束后已删除临时验证库
|
||||
@@ -0,0 +1,16 @@
|
||||
# 生产初始化数据库导出前端实施记录
|
||||
|
||||
## 范围确认
|
||||
|
||||
- 已根据 `docs/superpowers/specs/2026-03-31-production-db-init-export-design.md` 确认本次交付物仅为数据库初始化单文件 SQL
|
||||
- 本次任务不涉及前端页面、接口契约、构建配置或部署产物调整
|
||||
|
||||
## 本次结论
|
||||
|
||||
- `ruoyi-ui` 工程不存在需要随本次任务修改的页面、接口或构建配置
|
||||
- 本次前端范围为无代码改动
|
||||
- 执行过程中应保持 `ruoyi-ui` 目录不变
|
||||
|
||||
## 验证
|
||||
|
||||
- 已复核本次任务提交范围,前端代码未纳入本次 SQL 导出实现
|
||||
@@ -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 文件
|
||||
@@ -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` 安装前提,因此未在该容器直接执行安装流程,仅完成语法校验与逻辑核对
|
||||
15
doc/implementation-report-2026-04-01-agents-plan-rule.md
Normal file
15
doc/implementation-report-2026-04-01-agents-plan-rule.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# AGENTS.md 双计划约束调整实施记录
|
||||
|
||||
## 修改内容
|
||||
- 更新仓库规则文件 `AGENTS.md`
|
||||
- 将“根据设计文档产出前后端项目的实施计划时,输出两份执行文档”调整为“如果是前后端开发任务,根据设计文档产出实施计划时,输出两份执行文档”
|
||||
- 明确双计划要求只适用于前后端开发类任务,不再默认覆盖文档类、脚本类或其他非前后端开发任务
|
||||
|
||||
## 调整原因
|
||||
- 用户明确要求将“双 plan”约束收窄为仅对前后端开发任务生效
|
||||
- 避免后续在纯文档、纯脚本或非前后端开发任务中重复产出不必要的前后端两份计划文档
|
||||
|
||||
## 验证结果
|
||||
- 已检查 `AGENTS.md` 中“文档”章节的规则文字已更新
|
||||
- 已检查本次实施记录保存路径为 `doc/implementation-report-2026-04-01-agents-plan-rule.md`
|
||||
- 本次变更仅修改规则文档,未涉及代码或脚本执行
|
||||
@@ -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` 启动正常
|
||||
@@ -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 读取和遍历
|
||||
19
doc/implementation-report-2026-04-01-nginx-worker-user.md
Normal file
19
doc/implementation-report-2026-04-01-nginx-worker-user.md
Normal 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`
|
||||
@@ -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` 进程,测试结束后已自动清理对应进程和目录
|
||||
@@ -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` 等非正式后端包不会再误判
|
||||
- 正常部署链路仍然通过
|
||||
@@ -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>` 进程不会再阻塞新后端启动
|
||||
@@ -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`
|
||||
- 已人工核对设计文档中的方案对比、设计结论、执行流程、启停规则、失败处理、交付物和验证范围
|
||||
- 本次变更仅新增文档,未修改脚本或代码,因此未执行运行类验证
|
||||
@@ -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` 本次没有新增或修改的源码文件被纳入改动范围
|
||||
@@ -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` 可用时,端口监听检测仍然通过
|
||||
@@ -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 结构
|
||||
- 已人工核对计划中引用的脚本路径、设计文档路径和实施记录路径与仓库当前目录结构一致
|
||||
- 本次变更仅新增计划文档与实施记录,未执行脚本实现或运行类验证
|
||||
@@ -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` 端口检测回退场景仍然通过
|
||||
@@ -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` 和 `._*` 不会再被误判为有效发布产物
|
||||
- 正常部署链路仍然通过
|
||||
@@ -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 失败场景仍然通过
|
||||
@@ -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`,预期全部断言通过。
|
||||
- 启动前端页面后在浏览器访问登录页,确认账号、密码输入框默认不再预填内容。
|
||||
|
||||
## 影响范围
|
||||
- 仅影响登录页初始化展示行为,不修改登录接口、加密逻辑、验证码逻辑与记住密码逻辑。
|
||||
@@ -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` 进程完成,测试结束后已自动清理对应进程和临时目录
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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 使用的依赖库
|
||||
@@ -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`
|
||||
|
||||
## 说明
|
||||
- 本次移除的是前端证件号码格式校验,不影响证件号码必填约束
|
||||
- 后端本次未新增或调整证件号码格式校验逻辑
|
||||
@@ -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”,文档评审环节未使用子代理,后续执行时将在当前会话内推进
|
||||
- 本次仅产出设计与计划文档,尚未进入代码实施阶段
|
||||
@@ -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`
|
||||
- 本次验证期间启动的后端、前端和浏览器进程已在任务结束前关闭
|
||||
26
doc/implementation-report-2026-04-09-start-script-ps-ef.md
Normal file
26
doc/implementation-report-2026-04-09-start-script-ps-ef.md
Normal 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 进程在脚本收尾阶段自动停止并清理
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -0,0 +1,17 @@
|
||||
# 流程详情卡片顺序调整实施记录
|
||||
|
||||
## 修改时间
|
||||
- 2026-04-11
|
||||
|
||||
## 修改内容
|
||||
- 调整个人流程详情页右侧卡片顺序,将模型输出卡片移动到流程详情卡片上方
|
||||
- 调整企业流程详情页右侧卡片顺序,将模型输出卡片移动到流程详情卡片上方
|
||||
- 新增前端静态校验,约束个人与企业详情组件的卡片顺序
|
||||
|
||||
## 影响范围
|
||||
- 个人流程详情页面布局
|
||||
- 企业流程详情页面布局
|
||||
|
||||
## 验证计划
|
||||
- 执行前端静态测试,确认卡片顺序断言通过
|
||||
- 启动前端页面并在浏览器中检查个人、企业详情页卡片顺序
|
||||
@@ -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 部署产物
|
||||
@@ -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` 转发到后端容器
|
||||
@@ -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` 运行态查询仍需继续收口
|
||||
BIN
doc/~$上虞利率测算接口文档.xlsx
Normal file
BIN
doc/~$上虞利率测算接口文档.xlsx
Normal file
Binary file not shown.
BIN
doc/上虞利率测算接口文档.xlsx
Normal file
BIN
doc/上虞利率测算接口文档.xlsx
Normal file
Binary file not shown.
BIN
doc/上虞对私利率测算_上传字段与展示字段.xlsx
Normal file
BIN
doc/上虞对私利率测算_上传字段与展示字段.xlsx
Normal file
Binary file not shown.
@@ -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 "完成生产初始化数据库脚本验证"
|
||||
```
|
||||
@@ -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 "补充生产初始化数据库导出前端实施记录"
|
||||
```
|
||||
@@ -0,0 +1,385 @@
|
||||
# Production One-Click Deploy Backend Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 新增一份生产一键部署脚本,在脚本同目录完成发布包校验、旧版后端备份、后端进程停止、新版 `jar` 替换、后端重启和部署结果验证。
|
||||
|
||||
**Architecture:** 以现有 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 和 [bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh) 为参考,但将解包、备份、PID 管理、进程识别、端口等待全部内联到新的单脚本 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh)。实现只覆盖单实例发布链路,不接入 Nginx、不拆独立启停脚本、不引入额外配置文件。
|
||||
|
||||
**Tech Stack:** POSIX shell、`unzip`、`find`、`pgrep`、`nohup`、`ss`、`sh -n`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 锁定脚本接口与参考实现边界
|
||||
|
||||
**Files:**
|
||||
- Inspect: `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
|
||||
- Inspect: `bin/prod/deploy_release.sh`
|
||||
- Inspect: `bin/prod/restart_java.sh`
|
||||
|
||||
- [ ] **Step 1: 核对设计文档中的后端职责边界**
|
||||
|
||||
Run: `rg -n "单脚本|JAVA_BIN|backend/backend.pid|63310|loan.pricing.home|TERM|KILL" docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
|
||||
Expected: 能定位单脚本、自定义 `JAVA_BIN`、PID 文件、进程标记和端口等待等核心约束。
|
||||
|
||||
- [ ] **Step 2: 抽取现有生产脚本中可复用的后端逻辑**
|
||||
|
||||
Run: `rg -n "assert_single_jar|extract_release_package|cleanup|BACKEND_JAR|collect_backend_pids|stop_backend|start_backend|BACKEND_MARKER|BACKEND_PORT" bin/prod/deploy_release.sh bin/prod/restart_java.sh`
|
||||
Expected: 能定位现有发布包解压校验、PID 管理和端口等待逻辑,作为新脚本参考来源。
|
||||
|
||||
- [ ] **Step 3: 确认新脚本目标文件尚不存在**
|
||||
|
||||
Run: `test ! -f bin/prod/deploy_from_package.sh && echo "missing"`
|
||||
Expected: 输出 `missing`,说明可以新增独立脚本而不覆盖现有 `/home/webapp` 发布脚本。
|
||||
|
||||
- [ ] **Step 4: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/plans/2026-04-01-production-one-click-deploy-backend-plan.md
|
||||
git commit -m "新增生产一键部署后端计划"
|
||||
```
|
||||
|
||||
### Task 2: 创建单脚本基础骨架与公共函数
|
||||
|
||||
**Files:**
|
||||
- Create: `bin/prod/deploy_from_package.sh`
|
||||
|
||||
- [ ] **Step 1: 写入脚本头部配置和使用说明**
|
||||
|
||||
在 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh) 顶部写入最小骨架:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
JAVA_BIN="/home/webapp/env/java/bin/java"
|
||||
BACKEND_PORT=63310
|
||||
SPRING_PROFILE="pro"
|
||||
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
|
||||
```
|
||||
|
||||
并补齐以下说明函数:
|
||||
|
||||
```sh
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./bin/prod/deploy_from_package.sh
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现脚本目录定位和路径常量**
|
||||
|
||||
至少补齐以下路径变量:
|
||||
|
||||
```sh
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
BACKEND_DIR="$SCRIPT_DIR/backend"
|
||||
FRONTEND_DIR="$SCRIPT_DIR/frontend"
|
||||
BACKEND_JAR_TARGET="$BACKEND_DIR/ruoyi-admin.jar"
|
||||
BACKEND_PID_FILE="$BACKEND_DIR/backend.pid"
|
||||
BACKEND_LOG_FILE="$BACKEND_DIR/backend-console.log"
|
||||
FRONTEND_DIST_ARCHIVE="$FRONTEND_DIR/dist.zip"
|
||||
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
|
||||
BACKEND_MARKER="-Dloan.pricing.home=$SCRIPT_DIR"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现日志、时间戳和清理函数**
|
||||
|
||||
至少补齐:
|
||||
|
||||
```sh
|
||||
timestamp() {
|
||||
date "+%Y%m%d%H%M%S"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
|
||||
}
|
||||
```
|
||||
|
||||
以及:
|
||||
|
||||
```sh
|
||||
cleanup() {
|
||||
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 做语法校验**
|
||||
|
||||
Run: `sh -n bin/prod/deploy_from_package.sh`
|
||||
Expected: 无输出,返回码为 0。
|
||||
|
||||
- [ ] **Step 5: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add bin/prod/deploy_from_package.sh
|
||||
git commit -m "新增生产一键部署脚本骨架"
|
||||
```
|
||||
|
||||
### Task 3: 实现发布包发现、解压和后端备份
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/prod/deploy_from_package.sh`
|
||||
|
||||
- [ ] **Step 1: 实现目录与命令前置校验**
|
||||
|
||||
补齐以下校验:
|
||||
|
||||
```sh
|
||||
require_dir() {
|
||||
if [ ! -d "$1" ]; then
|
||||
log_error "缺少目录: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
log_error "缺少命令: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
在主流程中校验:
|
||||
|
||||
```sh
|
||||
require_dir "$BACKEND_DIR"
|
||||
require_dir "$FRONTEND_DIR"
|
||||
require_command unzip
|
||||
require_command find
|
||||
require_command pgrep
|
||||
require_command ss
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现同目录唯一发布 zip 查找**
|
||||
|
||||
新增函数:
|
||||
|
||||
```sh
|
||||
find_release_archive() {
|
||||
archives=$(find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip')
|
||||
count=$(printf '%s\n' "$archives" | sed '/^$/d' | wc -l | tr -d ' ')
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "脚本同目录发布 zip 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$archives"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现解压与唯一 `jar` 校验**
|
||||
|
||||
补齐:
|
||||
|
||||
```sh
|
||||
extract_release_package() {
|
||||
release_archive="$1"
|
||||
release_extract_dir="$2"
|
||||
mkdir -p "$release_extract_dir"
|
||||
unzip -oq "$release_archive" -d "$release_extract_dir"
|
||||
}
|
||||
|
||||
assert_single_jar() {
|
||||
search_dir="$1"
|
||||
count=$(find "$search_dir" -type f -name '*.jar' | wc -l | tr -d ' ')
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "后端 jar 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
find "$search_dir" -type f -name '*.jar' | head -n 1
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 实现旧版后端备份和新 `jar` 替换**
|
||||
|
||||
补齐:
|
||||
|
||||
```sh
|
||||
backup_backend_jar() {
|
||||
if [ -f "$BACKEND_JAR_TARGET" ]; then
|
||||
mv "$BACKEND_JAR_TARGET" "$BACKEND_JAR_TARGET.$(timestamp).bak"
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_backend_jar() {
|
||||
source_jar="$1"
|
||||
mv "$source_jar" "$BACKEND_JAR_TARGET"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 用临时目录构造后端备份场景做静态验证**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
tmpdir=$(mktemp -d)
|
||||
mkdir -p "$tmpdir/backend" "$tmpdir/frontend/package"
|
||||
touch "$tmpdir/backend/ruoyi-admin.jar"
|
||||
touch "$tmpdir/frontend/dist.zip"
|
||||
touch "$tmpdir/package/app.jar"
|
||||
(cd "$tmpdir/package" && zip -q release.zip app.jar >/dev/null)
|
||||
test -f "$tmpdir/backend/ruoyi-admin.jar"
|
||||
rm -rf "$tmpdir"
|
||||
```
|
||||
|
||||
Expected: 命令执行成功,用于确认计划中的文件命名和目录约定可被临时目录复现。
|
||||
|
||||
- [ ] **Step 6: 做语法校验**
|
||||
|
||||
Run: `sh -n bin/prod/deploy_from_package.sh`
|
||||
Expected: 无输出,返回码为 0。
|
||||
|
||||
- [ ] **Step 7: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add bin/prod/deploy_from_package.sh
|
||||
git commit -m "实现生产部署脚本后端发布包处理"
|
||||
```
|
||||
|
||||
### Task 4: 实现后端进程停止、启动与端口等待
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/prod/deploy_from_package.sh`
|
||||
|
||||
- [ ] **Step 1: 实现托管进程识别函数**
|
||||
|
||||
补齐以下函数:
|
||||
|
||||
```sh
|
||||
is_managed_backend_pid() {
|
||||
pid="$1"
|
||||
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
args=$(ps -o args= -p "$pid" 2>/dev/null || true)
|
||||
case "$args" in
|
||||
*"$BACKEND_MARKER"*"$BACKEND_JAR_TARGET"*|*"$BACKEND_JAR_TARGET"*"$BACKEND_MARKER"*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现 PID 收集与停止流程**
|
||||
|
||||
补齐:
|
||||
|
||||
```sh
|
||||
collect_backend_pids() {
|
||||
pids=""
|
||||
if [ -f "$BACKEND_PID_FILE" ]; then
|
||||
file_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true)
|
||||
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
|
||||
pids="$pids $file_pid"
|
||||
fi
|
||||
fi
|
||||
marker_pids=$(pgrep -f "$BACKEND_MARKER" 2>/dev/null || true)
|
||||
for pid in $marker_pids; do
|
||||
if is_managed_backend_pid "$pid"; then
|
||||
pids="$pids $pid"
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "$(echo "$pids" | xargs 2>/dev/null || true)"
|
||||
}
|
||||
```
|
||||
|
||||
以及 `stop_backend()`,要求:
|
||||
|
||||
- 先发 `TERM`
|
||||
- 等待最多 30 秒
|
||||
- 超时后发 `KILL`
|
||||
- 最后删除 `backend.pid`
|
||||
|
||||
- [ ] **Step 3: 实现启动流程**
|
||||
|
||||
补齐 `start_backend()`,要求:
|
||||
|
||||
```sh
|
||||
nohup "$JAVA_BIN" $JAVA_OPTS "$BACKEND_MARKER" -jar "$BACKEND_JAR_TARGET" \
|
||||
--spring.profiles.active="$SPRING_PROFILE" \
|
||||
--server.port="$BACKEND_PORT" >> "$BACKEND_LOG_FILE" 2>&1 &
|
||||
```
|
||||
|
||||
并在启动后:
|
||||
|
||||
- 写入 `backend/backend.pid`
|
||||
- 轮询 `ss -lnt | grep ":63310 "` 最长 30 秒
|
||||
- 若未监听成功则报错退出
|
||||
|
||||
- [ ] **Step 4: 实现主流程调用顺序**
|
||||
|
||||
主流程必须按以下顺序调用:
|
||||
|
||||
```sh
|
||||
backup_backend_jar
|
||||
stop_backend
|
||||
deploy_backend_jar "$backend_jar_source"
|
||||
start_backend
|
||||
```
|
||||
|
||||
不得把 `stop_backend` 放到备份后面之外的位置,避免旧进程继续占用将被替换的 `jar`。
|
||||
|
||||
- [ ] **Step 5: 做语法校验**
|
||||
|
||||
Run: `sh -n bin/prod/deploy_from_package.sh`
|
||||
Expected: 无输出,返回码为 0。
|
||||
|
||||
- [ ] **Step 6: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add bin/prod/deploy_from_package.sh
|
||||
git commit -m "实现生产部署脚本后端启停逻辑"
|
||||
```
|
||||
|
||||
### Task 5: 补后端实施记录并验证关键链路
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/prod/deploy_from_package.sh`
|
||||
- Create: `doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md`
|
||||
|
||||
- [ ] **Step 1: 做脚本静态语法校验**
|
||||
|
||||
Run: `sh -n bin/prod/deploy_from_package.sh`
|
||||
Expected: 无输出,返回码为 0。
|
||||
|
||||
- [ ] **Step 2: 检查脚本中的关键配置与标记是否落位**
|
||||
|
||||
Run: `rg -n "JAVA_BIN=|BACKEND_PORT=63310|SPRING_PROFILE=|BACKEND_MARKER=|backend.pid|backend-console.log|pgrep -f|ss -lnt" bin/prod/deploy_from_package.sh`
|
||||
Expected: 能看到 Java 路径、端口、进程标记、PID 文件、日志文件和端口等待逻辑都已写入脚本。
|
||||
|
||||
- [ ] **Step 3: 编写后端实施记录**
|
||||
|
||||
在 [doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md](/Users/wkc/Desktop/loan-pricing/loan-pricing/doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md) 至少记录:
|
||||
|
||||
```markdown
|
||||
- 新增脚本 `bin/prod/deploy_from_package.sh`
|
||||
- 脚本内固定 `JAVA_BIN`
|
||||
- 后端发布包解压与唯一 jar 校验规则
|
||||
- 旧版 jar 时间戳备份规则
|
||||
- PID 文件、进程标记、端口 63310 等待逻辑
|
||||
- 执行的验证命令与结果
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 核对实施记录路径**
|
||||
|
||||
Run: `ls doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md`
|
||||
Expected: 文件存在于仓库 `doc/` 目录。
|
||||
|
||||
- [ ] **Step 5: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add bin/prod/deploy_from_package.sh doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md
|
||||
git commit -m "完成生产一键部署后端实现"
|
||||
```
|
||||
@@ -0,0 +1,208 @@
|
||||
# Production One-Click Deploy Frontend Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在不修改 `ruoyi-ui` 源码的前提下,补齐生产一键部署脚本中的前端静态包替换、旧版 `dist` 备份、`dist.zip` 解压验证和实施留痕。
|
||||
|
||||
**Architecture:** 本次前端交付不涉及 Vue 页面、接口契约或构建配置,而是通过 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh) 管理发布目录中的 `frontend/dist.zip` 和 `frontend/dist/`。因此前端计划的重点是约束脚本里的静态包部署链路,验证旧 `dist` 目录时间戳备份、新 `dist.zip` 落盘和解压结果,并明确 `ruoyi-ui` 在本次任务中保持不变。
|
||||
|
||||
**Tech Stack:** shell、ZIP 静态资源包、`unzip`、`find`、`rg`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 确认本次任务无 `ruoyi-ui` 源码改动范围
|
||||
|
||||
**Files:**
|
||||
- Inspect: `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
|
||||
- Inspect: `ruoyi-ui/src`
|
||||
- Inspect: `ruoyi-ui/package.json`
|
||||
|
||||
- [ ] **Step 1: 核对设计文档中的前端交付边界**
|
||||
|
||||
Run: `rg -n "frontend/dist|dist.zip|不修改|不接入 Nginx|不新增外部配置文件" docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md`
|
||||
Expected: 能看到本次前端范围只包含发布目录中的静态包替换和解压,不涉及 `ruoyi-ui` 工程改造。
|
||||
|
||||
- [ ] **Step 2: 检查前端源码中不存在需要同步修改的实现点**
|
||||
|
||||
Run: `rg -n "deploy_from_package|dist.zip|frontend/dist|生产一键部署" ruoyi-ui/src ruoyi-ui/package.json`
|
||||
Expected: 无输出,说明 `ruoyi-ui` 不依赖本次部署脚本实现。
|
||||
|
||||
- [ ] **Step 3: 明确执行阶段不得改动 `ruoyi-ui`**
|
||||
|
||||
在执行笔记中写入以下约束:
|
||||
|
||||
```markdown
|
||||
- 本次前端交付物是发布目录中的静态包部署链路
|
||||
- 不修改 `ruoyi-ui` 下任何页面、接口、构建配置或打包脚本
|
||||
- 如后续出现页面需求,必须回到新需求重新设计和计划
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/plans/2026-04-01-production-one-click-deploy-frontend-plan.md
|
||||
git commit -m "新增生产一键部署前端计划"
|
||||
```
|
||||
|
||||
### Task 2: 在部署脚本中实现前端静态包发现与备份
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/prod/deploy_from_package.sh`
|
||||
|
||||
- [ ] **Step 1: 实现前端 `dist.zip` 唯一校验**
|
||||
|
||||
补齐函数:
|
||||
|
||||
```sh
|
||||
assert_single_dist_zip() {
|
||||
search_dir="$1"
|
||||
count=$(find "$search_dir" -type f -name 'dist.zip' | wc -l | tr -d ' ')
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "前端 dist.zip 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
find "$search_dir" -type f -name 'dist.zip' | head -n 1
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现旧版前端 `dist` 时间戳备份**
|
||||
|
||||
补齐函数:
|
||||
|
||||
```sh
|
||||
backup_frontend_dist() {
|
||||
if [ -d "$FRONTEND_DIST_DIR" ]; then
|
||||
mv "$FRONTEND_DIST_DIR" "$FRONTEND_DIR/dist-$(timestamp)"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现新前端压缩包替换**
|
||||
|
||||
补齐函数:
|
||||
|
||||
```sh
|
||||
deploy_frontend_archive() {
|
||||
source_dist_zip="$1"
|
||||
rm -f "$FRONTEND_DIST_ARCHIVE"
|
||||
mv "$source_dist_zip" "$FRONTEND_DIST_ARCHIVE"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 做语法校验**
|
||||
|
||||
Run: `sh -n bin/prod/deploy_from_package.sh`
|
||||
Expected: 无输出,返回码为 0。
|
||||
|
||||
- [ ] **Step 5: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add bin/prod/deploy_from_package.sh
|
||||
git commit -m "实现生产部署脚本前端备份替换逻辑"
|
||||
```
|
||||
|
||||
### Task 3: 在部署脚本中实现前端解压与结果校验
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/prod/deploy_from_package.sh`
|
||||
|
||||
- [ ] **Step 1: 实现前端解压函数**
|
||||
|
||||
补齐函数:
|
||||
|
||||
```sh
|
||||
deploy_frontend_dist() {
|
||||
dist_unpack_dir="$WORK_DIR/frontend"
|
||||
mkdir -p "$dist_unpack_dir"
|
||||
unzip -oq "$FRONTEND_DIST_ARCHIVE" -d "$dist_unpack_dir"
|
||||
rm -rf "$FRONTEND_DIST_DIR"
|
||||
mkdir -p "$FRONTEND_DIST_DIR"
|
||||
cp -a "$(resolve_frontend_source_dir "$dist_unpack_dir")"/. "$FRONTEND_DIST_DIR"/
|
||||
}
|
||||
```
|
||||
|
||||
要求同时实现 `resolve_frontend_source_dir()`,至少支持:
|
||||
|
||||
- 解压根目录直接包含 `index.html`
|
||||
- 解压后为 `dist/index.html`
|
||||
- 其他情况下用 `find` 定位第一个 `index.html`
|
||||
|
||||
- [ ] **Step 2: 将前端流程接入主执行顺序**
|
||||
|
||||
主流程至少补齐:
|
||||
|
||||
```sh
|
||||
frontend_dist_source=$(assert_single_dist_zip "$WORK_DIR/package")
|
||||
backup_frontend_dist
|
||||
deploy_frontend_archive "$frontend_dist_source"
|
||||
deploy_frontend_dist
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 检查脚本中已包含前端关键链路**
|
||||
|
||||
Run: `rg -n "assert_single_dist_zip|backup_frontend_dist|deploy_frontend_archive|deploy_frontend_dist|resolve_frontend_source_dir|frontend/dist.zip|frontend/dist" bin/prod/deploy_from_package.sh`
|
||||
Expected: 能看到前端校验、备份、替换和解压函数都已落位。
|
||||
|
||||
- [ ] **Step 4: 做语法校验**
|
||||
|
||||
Run: `sh -n bin/prod/deploy_from_package.sh`
|
||||
Expected: 无输出,返回码为 0。
|
||||
|
||||
- [ ] **Step 5: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add bin/prod/deploy_from_package.sh
|
||||
git commit -m "实现生产部署脚本前端解压逻辑"
|
||||
```
|
||||
|
||||
### Task 4: 用临时发布目录验证前端部署结果并留痕
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/prod/deploy_from_package.sh`
|
||||
- Create: `doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md`
|
||||
|
||||
- [ ] **Step 1: 构造临时前端发布目录做解压验证**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
tmpdir=$(mktemp -d)
|
||||
mkdir -p "$tmpdir/input/dist"
|
||||
printf '<html>ok</html>\n' > "$tmpdir/input/dist/index.html"
|
||||
(cd "$tmpdir/input" && zip -qr "$tmpdir/dist.zip" dist)
|
||||
unzip -oq "$tmpdir/dist.zip" -d "$tmpdir/unpacked"
|
||||
test -f "$tmpdir/unpacked/dist/index.html"
|
||||
rm -rf "$tmpdir"
|
||||
```
|
||||
|
||||
Expected: 命令执行成功,说明计划中的 `dist.zip -> frontend/dist` 解压链路可被临时目录复现。
|
||||
|
||||
- [ ] **Step 2: 再次确认 `ruoyi-ui` 未被纳入改动范围**
|
||||
|
||||
Run: `git status --short ruoyi-ui`
|
||||
Expected: 无本次任务新增或修改的前端源码文件;如果有输出,必须先判断是否为历史遗留改动,不得误提交。
|
||||
|
||||
- [ ] **Step 3: 编写前端实施记录**
|
||||
|
||||
在 [doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md](/Users/wkc/Desktop/loan-pricing/loan-pricing/doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md) 至少记录:
|
||||
|
||||
```markdown
|
||||
- 部署脚本中新增前端 dist.zip 唯一校验
|
||||
- 旧版 frontend/dist 时间戳备份规则
|
||||
- 新版 frontend/dist.zip 替换规则
|
||||
- 前端静态资源解压到 frontend/dist 的实现方式
|
||||
- 已确认 `ruoyi-ui` 本次无源码改动
|
||||
- 执行的验证命令与结果
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 核对实施记录路径**
|
||||
|
||||
Run: `ls doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md`
|
||||
Expected: 文件存在于仓库 `doc/` 目录。
|
||||
|
||||
- [ ] **Step 5: 提交本任务**
|
||||
|
||||
```bash
|
||||
git add bin/prod/deploy_from_package.sh doc/implementation-report-2026-04-01-production-one-click-deploy-frontend.md
|
||||
git commit -m "完成生产一键部署前端实现"
|
||||
```
|
||||
@@ -0,0 +1,110 @@
|
||||
# Personal Pricing Collateral Optional Frontend Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 让个人利率定价新增流程中的“抵质押类型”改为非必填,企业流程保持不变。
|
||||
|
||||
**Architecture:** 维持现有个人新增弹窗的数据结构、字段名称和提交报文不变,只移除前端表单对 `collType` 的必填限制。通过前端源码断言测试和页面联调共同验证“字段可空提交”的行为,避免扩大到企业流程或后端接口。
|
||||
|
||||
**Tech Stack:** Vue 2、Element UI、npm、Node.js 断言脚本
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 固化个人新增流程“抵质押类型非必填”的前端测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js`
|
||||
- Inspect: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
|
||||
- [ ] **Step 1: 核对当前个人新增弹窗中的 `collType` 规则**
|
||||
|
||||
Run: `sed -n '150,220p' ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
Expected: 能看到 `rules.collType` 里仍有 `required: true`。
|
||||
|
||||
- [ ] **Step 2: 先写失败断言**
|
||||
|
||||
在 `ruoyi-ui/tests/personal-create-input-params.test.js` 中增加断言,明确个人新增弹窗不应再包含:
|
||||
|
||||
```js
|
||||
required: true, message: "请选择抵质押类型"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行测试确认红灯**
|
||||
|
||||
Run: `npm --prefix ruoyi-ui run test:personal-create-input-params`
|
||||
Expected: FAIL,提示个人新增弹窗仍将抵质押类型设为必填。
|
||||
|
||||
### Task 2: 移除个人新增弹窗中 `collType` 的必填限制
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
|
||||
- [ ] **Step 1: 删除 `collType` 的必填校验**
|
||||
|
||||
把:
|
||||
|
||||
```js
|
||||
collType: [
|
||||
{required: true, message: "请选择抵质押类型", trigger: "change"}
|
||||
]
|
||||
```
|
||||
|
||||
移除,使个人表单规则中不再声明 `collType` 为必填。
|
||||
|
||||
- [ ] **Step 2: 保持其它字段与提交逻辑不变**
|
||||
|
||||
确认以下内容不改动:
|
||||
|
||||
```js
|
||||
collType: undefined,
|
||||
collThirdParty: false
|
||||
```
|
||||
|
||||
以及提交时的:
|
||||
|
||||
```js
|
||||
collThirdParty: this.form.collThirdParty ? '1' : '0'
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 重新运行测试确认转绿**
|
||||
|
||||
Run: `npm --prefix ruoyi-ui run test:personal-create-input-params`
|
||||
Expected: PASS,输出包含 `personal create input params assertions passed`。
|
||||
|
||||
### Task 3: 页面联调并补实施记录
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/implementation-reports/2026-04-10-personal-pricing-collateral-optional-frontend.md`
|
||||
|
||||
- [ ] **Step 1: 启动前端页面**
|
||||
|
||||
Run: `npm --prefix ruoyi-ui run dev`
|
||||
Expected: 前端本地开发服务启动成功,可访问新增个人利率定价流程页面。
|
||||
|
||||
- [ ] **Step 2: 浏览器确认页面行为**
|
||||
|
||||
联调确认:
|
||||
- 个人新增弹窗“抵质押类型”字段可为空
|
||||
- 不选择“抵质押类型”时其余必填项填完整仍可点击提交
|
||||
- 企业新增弹窗规则不受影响
|
||||
|
||||
- [ ] **Step 3: 停止本次测试启动的前端进程**
|
||||
|
||||
Run: `ps -ef | rg 'vue-cli-service serve|npm --prefix ruoyi-ui run dev'`
|
||||
Expected: 仅停止本次联调启动的前端进程。
|
||||
|
||||
- [ ] **Step 4: 编写实施记录**
|
||||
|
||||
实施记录至少包含:
|
||||
|
||||
```markdown
|
||||
- 本次仅调整个人利率定价新增流程
|
||||
- 个人新增弹窗已移除抵质押类型必填校验
|
||||
- 企业新增流程未改动
|
||||
- 已执行前端断言测试与页面联调验证
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 核对实施记录保存路径**
|
||||
|
||||
Run: `ls docs/implementation-reports/2026-04-10-personal-pricing-collateral-optional-frontend.md`
|
||||
Expected: 文件位于仓库 `docs/implementation-reports` 目录。
|
||||
@@ -0,0 +1,218 @@
|
||||
# 生产初始化数据库导出设计文档
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前项目已经存在若依基础库脚本 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql),也存在历史的全量结构导出文件和“必要数据”导出文件。但本次目标不是继续导出业务数据,而是为生产部署准备一个单一、可直接执行的数据库初始化脚本。
|
||||
|
||||
该脚本需要满足以下要求:
|
||||
|
||||
- 以若依基础表和初始化数据为基线
|
||||
- 在此基础上补齐贷款定价项目新增的所有业务表
|
||||
- 不携带任何业务数据
|
||||
- 最终导出为一个文件,生产环境可直接执行
|
||||
|
||||
## 2. 已确认约束
|
||||
|
||||
- 最终产物必须是一个单一 `.sql` 文件
|
||||
- 若依基础部分直接复用 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql)
|
||||
- 不从当前数据库重新抽取 `sys_*` 管理数据
|
||||
- 不导出任何业务数据
|
||||
- 不拆分为“基础脚本 + 增量脚本”多步执行
|
||||
- 不增加兼容性、补丁性或兜底性方案
|
||||
- 方案必须保持最短路径实现
|
||||
|
||||
## 3. 当前资产与现状
|
||||
|
||||
### 3.1 已有基础脚本
|
||||
|
||||
[sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 已包含若依基础表结构以及初始化管理数据,覆盖部门、用户、岗位、角色、菜单、字典、参数、通知、定时任务等基础能力。
|
||||
|
||||
### 3.2 已有历史导出文件
|
||||
|
||||
仓库内已有以下历史文件,可作为字段和表范围核对依据:
|
||||
|
||||
- [sql/loan_pricing_schema_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_schema_20260328.sql)
|
||||
- [sql/loan_pricing_required_data_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_required_data_20260328.sql)
|
||||
|
||||
其中 `loan_pricing_required_data_20260328.sql` 带有业务数据,不符合本次生产初始化目标,因此本次不能直接复用。
|
||||
|
||||
### 3.3 业务增量表范围
|
||||
|
||||
相对于若依基础脚本,当前项目新增的业务表结构只有以下 3 张:
|
||||
|
||||
- `loan_pricing_workflow`
|
||||
- `model_corp_output_fields`
|
||||
- `model_retail_output_fields`
|
||||
|
||||
本次生产初始化导出只需要将这 3 张表的最终结构追加到若依基础脚本之后。
|
||||
|
||||
## 4. 方案对比
|
||||
|
||||
### 方案一:生成单一生产初始化总脚本
|
||||
|
||||
做法:
|
||||
|
||||
- 以 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 为主体
|
||||
- 追加 3 张业务表的最终 `DROP TABLE` 与 `CREATE TABLE`
|
||||
- 形成一个新的、可直接执行的生产初始化 SQL 文件
|
||||
|
||||
优点:
|
||||
|
||||
- 与本次“一个文件直接执行”的目标完全一致
|
||||
- 路径最短,执行成本最低
|
||||
- 不依赖实时连库导出,不会误带当前库中的业务数据
|
||||
- 基础初始化内容与现有若依脚本保持一致,可控性高
|
||||
|
||||
缺点:
|
||||
|
||||
- 后续业务表结构变化后,需要重新生成该文件
|
||||
|
||||
### 方案二:保留若依基础脚本,再新增一个业务表增量脚本
|
||||
|
||||
做法:
|
||||
|
||||
- 保持 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 不变
|
||||
- 额外新增一个只包含业务表结构的 SQL 文件
|
||||
- 生产执行时先跑基础脚本,再跑增量脚本
|
||||
|
||||
优点:
|
||||
|
||||
- 文件职责分离较清晰
|
||||
|
||||
缺点:
|
||||
|
||||
- 不满足“导出到一个文件内”的明确要求
|
||||
- 生产执行需要多步操作
|
||||
- 增加人为漏执行或执行顺序错误的风险
|
||||
|
||||
### 方案三:从当前数据库重新导出全库,再手工删除不需要的内容
|
||||
|
||||
做法:
|
||||
|
||||
- 基于当前数据库重新导出结构与数据
|
||||
- 人工删除业务数据和不需要的表内容
|
||||
|
||||
优点:
|
||||
|
||||
- 理论上能快速拿到一个初稿
|
||||
|
||||
缺点:
|
||||
|
||||
- 风险最高,容易混入业务数据、运行态数据和测试数据
|
||||
- 结果依赖当前库状态,不稳定
|
||||
- 不符合最短路径且可控的实现要求
|
||||
|
||||
## 5. 设计结论
|
||||
|
||||
采用方案一:生成单一生产初始化总脚本。
|
||||
|
||||
最终产物是一个新的 SQL 文件,放在 [sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql) 目录,用于生产数据库初始化。该文件以若依基础脚本为主体,追加 3 张业务表结构,不包含任何业务表数据。
|
||||
|
||||
## 6. 导出文件设计
|
||||
|
||||
### 6.1 文件职责
|
||||
|
||||
该文件只负责“生产数据库初始化”,不承担历史数据迁移、业务数据灌库或环境差异兼容职责。
|
||||
|
||||
### 6.2 文件内容组成
|
||||
|
||||
建议内容结构如下:
|
||||
|
||||
1. 头部说明
|
||||
2. 数据库创建与切库语句
|
||||
3. 若依基础表结构与初始化数据
|
||||
4. 贷款定价业务表结构
|
||||
5. 执行结束后的环境恢复语句
|
||||
|
||||
### 6.3 文件命名
|
||||
|
||||
建议命名为:
|
||||
|
||||
- `sql/loan_pricing_prod_init_20260331.sql`
|
||||
|
||||
命名目标是明确表达“生产初始化”用途,并带上生成日期,方便后续版本追踪。
|
||||
|
||||
## 7. 保留与排除范围
|
||||
|
||||
### 7.1 保留内容
|
||||
|
||||
保留内容分为两部分:
|
||||
|
||||
第一部分是若依基础脚本中的全部基础表与初始化数据,直接复用 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 的现有内容。
|
||||
|
||||
第二部分是项目业务增量表结构,仅保留如下 3 张表的结构定义:
|
||||
|
||||
- `loan_pricing_workflow`
|
||||
- `model_corp_output_fields`
|
||||
- `model_retail_output_fields`
|
||||
|
||||
### 7.2 排除内容
|
||||
|
||||
以下内容明确不进入生产初始化脚本:
|
||||
|
||||
- `loan_pricing_workflow` 的任何记录数据
|
||||
- `model_corp_output_fields` 的任何记录数据
|
||||
- `model_retail_output_fields` 的任何记录数据
|
||||
- 从当前数据库重新抽取的 `sys_*` 数据
|
||||
- 任何日志、运行历史、流程历史、演示数据、测试数据
|
||||
|
||||
## 8. 业务表结构来源
|
||||
|
||||
3 张业务表的结构应以当前仓库中已经确认的最终版本为准,结构来源优先按以下顺序核定:
|
||||
|
||||
1. [sql/loan_pricing_schema_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_schema_20260328.sql) 中对应表的最终结构
|
||||
2. [sql/loan_pricing_workflow.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/loan_pricing_workflow.sql)、[sql/model_corp.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/model_corp.sql)、[sql/model_retail.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/model_retail.sql)
|
||||
3. 其后的结构修正脚本,例如字段补充、字段注释修复、执行利率字段补充等 SQL
|
||||
|
||||
如果存在同名表的多份定义,应以能反映当前最终字段状态的版本为准,避免把旧结构写入生产初始化脚本。
|
||||
|
||||
## 9. 生成方式
|
||||
|
||||
本次采用静态拼装方式生成,不依赖实时数据库导出。
|
||||
|
||||
生成步骤如下:
|
||||
|
||||
1. 复制 [sql/ry_20250522.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing/sql/ry_20250522.sql) 的基础内容
|
||||
2. 从最终结构来源中提取 3 张业务表的 `DROP TABLE` 与 `CREATE TABLE`
|
||||
3. 将业务表结构追加到基础内容后方
|
||||
4. 在文件头部补充脚本说明,明确用途、范围和排除项
|
||||
|
||||
该方式可以保证结果稳定、内容可审查,并且不会因为当前数据库状态不同而发生漂移。
|
||||
|
||||
## 10. 验证设计
|
||||
|
||||
本次验证只围绕“单文件是否可用于生产初始化”展开,必须覆盖以下检查:
|
||||
|
||||
1. 检查脚本是否为单文件,且可独立执行
|
||||
2. 检查脚本中是否完整包含若依基础内容
|
||||
3. 检查脚本中是否完整包含 3 张业务表结构
|
||||
4. 检查脚本中业务表不存在任何 `INSERT` 数据语句
|
||||
5. 在测试库执行脚本,验证建库建表流程可跑通
|
||||
6. 导入完成后核对若依基础表存在初始化数据
|
||||
7. 导入完成后核对 3 张业务表为空表
|
||||
|
||||
## 11. 风险与控制
|
||||
|
||||
主要风险如下:
|
||||
|
||||
1. 如果业务表结构来源选择错误,可能把旧字段定义带入生产脚本
|
||||
2. 如果直接复用带数据的历史导出文件,可能误将业务数据带入生产
|
||||
3. 如果脚本顺序错误,可能导致执行时报表不存在或环境切换错误
|
||||
|
||||
控制方式如下:
|
||||
|
||||
- 明确以若依基础脚本为唯一基础来源
|
||||
- 明确业务增量仅限 3 张表结构
|
||||
- 明确禁止业务表 `INSERT` 语句进入最终脚本
|
||||
- 通过测试库执行验证最终脚本的可执行性
|
||||
|
||||
## 12. 非目标
|
||||
|
||||
本次不包含以下内容:
|
||||
|
||||
- 不迁移任何业务数据
|
||||
- 不重建当前环境中的真实用户与权限现状
|
||||
- 不处理生产数据回灌
|
||||
- 不新增多环境兼容脚本
|
||||
- 不生成多文件执行方案
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
# 生产一键部署脚本设计文档
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前仓库已经存在生产环境部署脚本 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 和 Java 管理脚本 [bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh)。但这套脚本依赖固定的 `/home/webapp` 目录结构、独立启停脚本以及安装好的 Nginx,不符合本次“在脚本同目录直接完成发布”的目标。
|
||||
|
||||
本次需要设计一份新的生产一键部署脚本,直接放在发布目录内执行。该目录中同时存在:
|
||||
|
||||
- 部署脚本本身
|
||||
- 1 个发布 zip
|
||||
- `backend/` 目录
|
||||
- `frontend/` 目录
|
||||
|
||||
发布 zip 内固定包含 1 个后端 `jar` 和 1 个前端 `dist.zip`。脚本执行时需要完成旧版本备份、新版本替换、后端重启和前端解压部署。
|
||||
|
||||
## 2. 已确认约束
|
||||
|
||||
- 交付形态必须是单脚本,自包含,不拆分独立重启脚本
|
||||
- Java 可执行路径直接写在脚本内常量中,不通过命令行参数或外部配置文件传入
|
||||
- 发布包从脚本同目录读取
|
||||
- 发布包内必须且只能包含 1 个后端 `jar` 和 1 个前端 `dist.zip`
|
||||
- 旧版后端 `jar` 与旧版前端 `dist` 目录必须通过“原地重命名 + 时间戳”的方式保留
|
||||
- 后端启动参数固定沿用当前生产约束:`--spring.profiles.active=pro --server.port=63310`
|
||||
- 不增加兼容性、补丁性、兜底性或回滚性方案
|
||||
- 方案必须保持最短路径实现,并可完成全链路逻辑验证
|
||||
|
||||
## 3. 当前资产与现状
|
||||
|
||||
### 3.1 现有生产脚本
|
||||
|
||||
[bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 已具备以下能力:
|
||||
|
||||
- 解压发布包
|
||||
- 校验包内 `jar` 和 `dist.zip`
|
||||
- 备份旧版后端与前端
|
||||
- 替换产物
|
||||
- 调用独立 Java 管理脚本完成重启
|
||||
|
||||
但该脚本默认依赖 `/home/webapp` 固定目录、`restart_java.sh` 外部脚本和 Nginx 管理流程,超出了本次需求边界。
|
||||
|
||||
### 3.2 现有 Java 管理逻辑
|
||||
|
||||
[bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh) 已实现:
|
||||
|
||||
- 使用 PID 文件记录后端进程
|
||||
- 通过 `-Dloan.pricing.home` 标记识别托管进程
|
||||
- 停止时先 `TERM` 再按需 `KILL`
|
||||
- 启动后等待端口 `63310` 监听成功
|
||||
|
||||
这部分逻辑可以作为新单脚本中的后端启停参考,但最终不再拆成第二个脚本文件。
|
||||
|
||||
## 4. 方案对比
|
||||
|
||||
### 方案一:单脚本自包含部署
|
||||
|
||||
做法:
|
||||
|
||||
- 新增一份单独的部署脚本
|
||||
- 在脚本顶部写死 `JAVA_BIN`、端口、日志路径和目录约定
|
||||
- 执行时自动读取脚本同目录发布包
|
||||
- 将解包、备份、替换、启停全部内联到同一文件
|
||||
|
||||
优点:
|
||||
|
||||
- 与本次“一起写在一个脚本里”的目标完全一致
|
||||
- 发布目录内即可直接执行,路径最短
|
||||
- 不依赖独立重启脚本或外部配置文件
|
||||
- 用户只需维护一个脚本文件
|
||||
|
||||
缺点:
|
||||
|
||||
- 后续如果要单独管理 `start`、`stop`、`status`,需要再扩展脚本
|
||||
|
||||
### 方案二:保留现有双脚本结构,只调整入口
|
||||
|
||||
做法:
|
||||
|
||||
- 保留部署脚本和重启脚本两份文件
|
||||
- 让部署脚本在同目录发布模式下调用重启脚本
|
||||
|
||||
优点:
|
||||
|
||||
- 可复用现有结构,改动相对少
|
||||
|
||||
缺点:
|
||||
|
||||
- 不满足“写在一起”的明确要求
|
||||
- 交付物仍有两个脚本,使用路径不够直接
|
||||
|
||||
### 方案三:极简覆盖式脚本
|
||||
|
||||
做法:
|
||||
|
||||
- 不维护 PID 文件
|
||||
- 直接按文件名覆盖
|
||||
- 使用宽泛的 Java 进程匹配方式停止旧进程
|
||||
|
||||
优点:
|
||||
|
||||
- 脚本最短
|
||||
|
||||
缺点:
|
||||
|
||||
- 容易误伤其他 Java 进程
|
||||
- 状态不可控
|
||||
- 不满足全链路可验证要求
|
||||
|
||||
## 5. 设计结论
|
||||
|
||||
采用方案一:单脚本自包含部署。
|
||||
|
||||
最终交付为 1 份新的生产一键部署脚本,负责在脚本同目录完成完整发布链路。脚本以当前仓库已有生产脚本为参考,但不保留 Nginx 管理、根目录安装和双脚本调用等超出本次范围的设计。
|
||||
|
||||
## 6. 脚本结构设计
|
||||
|
||||
### 6.1 目录约定
|
||||
|
||||
脚本执行目录固定为脚本所在目录。目录内约定如下:
|
||||
|
||||
- `deploy_release.sh` 或本次确定的新脚本文件
|
||||
- 1 个发布 zip
|
||||
- `backend/`
|
||||
- `frontend/`
|
||||
|
||||
脚本不依赖仓库根目录运行,也不依赖外部工作目录传参。
|
||||
|
||||
### 6.2 脚本内固定配置
|
||||
|
||||
脚本顶部固定声明以下配置项:
|
||||
|
||||
- `JAVA_BIN`
|
||||
- `BACKEND_PORT=63310`
|
||||
- `SPRING_PROFILE=pro`
|
||||
- `JAVA_OPTS`
|
||||
- `BACKEND_PID_FILE=backend/backend.pid`
|
||||
- `BACKEND_LOG_FILE=backend/backend-console.log`
|
||||
- `BACKEND_JAR_TARGET=backend/ruoyi-admin.jar`
|
||||
- `FRONTEND_DIST_DIR=frontend/dist`
|
||||
- `FRONTEND_DIST_ARCHIVE=frontend/dist.zip`
|
||||
|
||||
其中 `JAVA_BIN` 由维护者直接修改脚本内常量,不再设计其他配置入口。
|
||||
|
||||
### 6.3 执行流程
|
||||
|
||||
脚本执行顺序固定如下:
|
||||
|
||||
1. 定位脚本所在目录
|
||||
2. 校验 `backend/` 与 `frontend/` 目录存在,不存在则直接失败
|
||||
3. 在脚本同目录查找唯一一个发布 zip
|
||||
4. 解压发布 zip 到临时目录
|
||||
5. 校验临时目录内必须且只能找到 1 个 `jar` 和 1 个 `dist.zip`
|
||||
6. 将 `backend/` 目录下现有 `jar` 重命名为带时间戳的备份文件
|
||||
7. 将 `frontend/dist` 重命名为 `dist-时间戳`
|
||||
8. 停止当前脚本托管的旧后端进程
|
||||
9. 将新 `jar` 移入 `backend/ruoyi-admin.jar`
|
||||
10. 将新 `dist.zip` 移入 `frontend/dist.zip`
|
||||
11. 删除当前 `frontend/dist`
|
||||
12. 解压新 `frontend/dist.zip` 到 `frontend/dist/`
|
||||
13. 启动新的后端 `jar`
|
||||
14. 等待端口 `63310` 监听成功
|
||||
15. 输出部署完成信息
|
||||
|
||||
## 7. 后端启停设计
|
||||
|
||||
### 7.1 进程识别规则
|
||||
|
||||
为避免误杀机器上的其他 Java 进程,脚本只管理自己启动的后端实例。
|
||||
|
||||
识别规则如下:
|
||||
|
||||
- 启动时附加标记参数:`-Dloan.pricing.home=<脚本目录>`
|
||||
- 优先读取 `backend/backend.pid`
|
||||
- 如果 PID 文件失效,则使用 `pgrep -f` 按以下组合特征识别:
|
||||
- `-Dloan.pricing.home=<脚本目录>`
|
||||
- `backend/ruoyi-admin.jar`
|
||||
|
||||
只有同时满足托管标记和目标 jar 特征的进程,才允许被停止。
|
||||
|
||||
### 7.2 停止流程
|
||||
|
||||
停止逻辑如下:
|
||||
|
||||
1. 收集当前托管进程 PID
|
||||
2. 如无托管进程,则清理失效 PID 文件并直接继续部署
|
||||
3. 对托管进程发送 `TERM`
|
||||
4. 等待最多 30 秒
|
||||
5. 若仍存在残留进程,则发送 `KILL`
|
||||
6. 删除 `backend/backend.pid`
|
||||
|
||||
### 7.3 启动流程
|
||||
|
||||
启动逻辑如下:
|
||||
|
||||
1. 校验 `JAVA_BIN` 可执行
|
||||
2. 校验 `backend/ruoyi-admin.jar` 存在
|
||||
3. 使用 `nohup` 后台启动
|
||||
4. 写入 `backend/backend.pid`
|
||||
5. 日志输出到 `backend/backend-console.log`
|
||||
6. 使用固定参数启动:
|
||||
- `-Dloan.pricing.home=<脚本目录>`
|
||||
- `--spring.profiles.active=pro`
|
||||
- `--server.port=63310`
|
||||
7. 轮询端口监听状态,最长等待 30 秒
|
||||
|
||||
## 8. 备份与替换设计
|
||||
|
||||
### 8.1 后端备份
|
||||
|
||||
如果 `backend/` 目录中存在旧版 `jar`,则将其原地重命名为:
|
||||
|
||||
- `原文件名.YYYYMMDDHHMMSS.bak`
|
||||
|
||||
不新增独立备份目录,避免引入额外结构。
|
||||
|
||||
### 8.2 前端备份
|
||||
|
||||
如果 `frontend/dist` 目录存在,则将其原地重命名为:
|
||||
|
||||
- `dist-YYYYMMDDHHMMSS`
|
||||
|
||||
如果 `frontend/dist.zip` 已存在,则允许被新版本覆盖,不再为历史 `dist.zip` 单独追加备份规则。本次备份目标仅聚焦于用户明确提出的旧 `jar` 和旧前端 `dist`。
|
||||
|
||||
### 8.3 新产物替换
|
||||
|
||||
替换规则如下:
|
||||
|
||||
- 后端统一落到 `backend/ruoyi-admin.jar`
|
||||
- 前端压缩包统一落到 `frontend/dist.zip`
|
||||
- 前端静态资源统一解压到 `frontend/dist/`
|
||||
|
||||
该规则确保部署目录在每次发布后保持稳定结构,便于后续维护。
|
||||
|
||||
## 9. 失败处理设计
|
||||
|
||||
本次失败处理仅保留必要的强校验,不增加回滚或兼容分支。
|
||||
|
||||
以下场景直接失败退出:
|
||||
|
||||
- 脚本同目录下没有发布 zip
|
||||
- 脚本同目录下存在多个发布 zip
|
||||
- 解压后 `jar` 数量不是 1
|
||||
- 解压后 `dist.zip` 数量不是 1
|
||||
- `JAVA_BIN` 不可执行
|
||||
- 新 `jar` 无法写入目标位置
|
||||
- `dist.zip` 解压失败
|
||||
- 后端 30 秒内未监听 `63310`
|
||||
|
||||
脚本退出时需要清理临时解压目录,避免残留中间文件。
|
||||
|
||||
## 10. 交付文件设计
|
||||
|
||||
本次设计对应的交付物限定为以下内容:
|
||||
|
||||
- 1 个生产一键部署脚本
|
||||
- 1 份设计文档
|
||||
- 1 份实施记录
|
||||
|
||||
建议脚本路径为:
|
||||
|
||||
- [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh)
|
||||
|
||||
保留 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 不动,可以避免影响现有 `/home/webapp` 部署方式;新需求使用独立脚本承载,边界更清晰。
|
||||
|
||||
## 11. 验证设计
|
||||
|
||||
本次验证只覆盖该单脚本的一键部署链路,必须包含以下检查:
|
||||
|
||||
1. 执行 `sh -n` 校验脚本语法
|
||||
2. 校验同目录只有 1 个发布 zip 时可继续执行
|
||||
3. 校验没有 zip 或存在多个 zip 时直接失败
|
||||
4. 校验发布包内必须正好有 1 个 `jar` 和 1 个 `dist.zip`
|
||||
5. 校验旧 `jar` 被改名为带时间戳备份文件
|
||||
6. 校验旧 `frontend/dist` 被改名为带时间戳目录
|
||||
7. 校验新 `jar` 最终位于 `backend/ruoyi-admin.jar`
|
||||
8. 校验新 `dist.zip` 最终位于 `frontend/dist.zip`
|
||||
9. 校验前端资源成功解压到 `frontend/dist/`
|
||||
10. 校验后端进程成功启动并监听 `63310`
|
||||
11. 校验 `backend/backend.pid` 和 `backend/backend-console.log` 被正确生成
|
||||
|
||||
## 12. 非目标
|
||||
|
||||
本次明确不包含以下内容:
|
||||
|
||||
- 不接入 Nginx 启停或重载
|
||||
- 不处理 Java 安装
|
||||
- 不生成独立的后端重启脚本
|
||||
- 不增加回滚脚本
|
||||
- 不新增命令行参数模式
|
||||
- 不新增外部配置文件
|
||||
- 不兼容多实例或多应用部署场景
|
||||
513
pom.xml
513
pom.xml
@@ -1,191 +1,218 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi</artifactId>
|
||||
<version>3.9.1</version>
|
||||
|
||||
<name>ruoyi</name>
|
||||
<url>http://www.ruoyi.vip</url>
|
||||
<description>若依管理系统</description>
|
||||
|
||||
<properties>
|
||||
<ruoyi.version>3.9.1</ruoyi.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>17</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<mybatis-spring-boot.version>3.0.5</mybatis-spring-boot.version>
|
||||
<druid.version>1.2.27</druid.version>
|
||||
<yauaa.version>7.32.0</yauaa.version>
|
||||
<swagger.version>3.0.0</swagger.version>
|
||||
<kaptcha.version>2.3.3</kaptcha.version>
|
||||
<pagehelper.boot.version>2.1.1</pagehelper.boot.version>
|
||||
<fastjson.version>2.0.60</fastjson.version>
|
||||
<oshi.version>6.9.1</oshi.version>
|
||||
<commons.io.version>2.21.0</commons.io.version>
|
||||
<poi.version>4.1.2</poi.version>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi</artifactId>
|
||||
<version>3.9.2</version>
|
||||
|
||||
<name>ruoyi</name>
|
||||
<url>http://www.ruoyi.vip</url>
|
||||
<description>若依管理系统</description>
|
||||
|
||||
<properties>
|
||||
<ruoyi.version>3.9.2</ruoyi.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<spring-boot.version>2.5.15</spring-boot.version>
|
||||
<druid.version>1.2.28</druid.version>
|
||||
<yauaa.version>7.32.0</yauaa.version>
|
||||
<swagger.version>3.0.0</swagger.version>
|
||||
<kaptcha.version>2.3.3</kaptcha.version>
|
||||
<pagehelper.boot.version>1.4.7</pagehelper.boot.version>
|
||||
<fastjson.version>2.0.61</fastjson.version>
|
||||
<oshi.version>6.10.0</oshi.version>
|
||||
<commons.io.version>2.21.0</commons.io.version>
|
||||
<poi.version>4.1.2</poi.version>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<jwt.version>0.9.1</jwt.version>
|
||||
<quartz.version>2.5.2</quartz.version>
|
||||
<mysql.version>8.2.0</mysql.version>
|
||||
<jaxb-api.version>2.3.1</jaxb-api.version>
|
||||
<jakarta.version>6.0.0</jakarta.version>
|
||||
<springdoc.version>2.8.14</springdoc.version>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
<!-- SpringBoot的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>3.5.8</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里数据库连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 解析客户端操作系统、浏览器等 -->
|
||||
<dependency>
|
||||
<groupId>nl.basjes.parse.useragent</groupId>
|
||||
<artifactId>yauaa</artifactId>
|
||||
<version>${yauaa.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- pagehelper 分页插件 -->
|
||||
<dependency>
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
<version>${pagehelper.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>${mybatis-spring-boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>${mysql.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb-api.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>${jakarta.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 获取系统信息 -->
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>${oshi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- spring-doc -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- io常用工具类 -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons.io.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- excel工具 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- velocity代码生成使用模板 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
<version>${velocity.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 定时任务 -->
|
||||
<dependency>
|
||||
<groupId>org.quartz-scheduler</groupId>
|
||||
<artifactId>quartz</artifactId>
|
||||
<version>${quartz.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里JSON解析器 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Token生成与解析-->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>pro.fessional</groupId>
|
||||
<artifactId>kaptcha</artifactId>
|
||||
<version>${kaptcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 定时任务-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-quartz</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 代码生成-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-generator</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 核心模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
<jsqlparser.version>4.5</jsqlparser.version>
|
||||
<!-- override dependency version -->
|
||||
<tomcat.version>9.0.112</tomcat.version>
|
||||
<logback.version>1.2.13</logback.version>
|
||||
<spring-security.version>5.7.14</spring-security.version>
|
||||
<spring-framework.version>5.3.39</spring-framework.version>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
<!-- 覆盖SpringFramework的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-framework-bom</artifactId>
|
||||
<version>${spring-framework.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 覆盖SpringSecurity的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-bom</artifactId>
|
||||
<version>${spring-security.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringBoot的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 覆盖logback的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 覆盖tomcat的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
<version>${tomcat.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-el</artifactId>
|
||||
<version>${tomcat.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-websocket</artifactId>
|
||||
<version>${tomcat.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里数据库连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 解析客户端操作系统、浏览器等 -->
|
||||
<dependency>
|
||||
<groupId>nl.basjes.parse.useragent</groupId>
|
||||
<artifactId>yauaa</artifactId>
|
||||
<version>${yauaa.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- pagehelper 分页插件 -->
|
||||
<dependency>
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
<version>${pagehelper.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 获取系统信息 -->
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>${oshi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger3依赖 -->
|
||||
<dependency>
|
||||
<groupId>io.springfox</groupId>
|
||||
<artifactId>springfox-boot-starter</artifactId>
|
||||
<version>${swagger.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.swagger</groupId>
|
||||
<artifactId>swagger-models</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- io常用工具类 -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons.io.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- excel工具 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- velocity代码生成使用模板 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
<version>${velocity.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里JSON解析器 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Token生成与解析-->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>pro.fessional</groupId>
|
||||
<artifactId>kaptcha</artifactId>
|
||||
<version>${kaptcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 定时任务-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-quartz</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 代码生成-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-generator</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 核心模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
@@ -200,64 +227,70 @@
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
<artifactId>jsqlparser</artifactId>
|
||||
<version>${jsqlparser.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<modules>
|
||||
<module>ruoyi-admin</module>
|
||||
<module>ruoyi-framework</module>
|
||||
<module>ruoyi-system</module>
|
||||
|
||||
<modules>
|
||||
<module>ruoyi-admin</module>
|
||||
<module>ruoyi-framework</module>
|
||||
<module>ruoyi-system</module>
|
||||
<module>ruoyi-quartz</module>
|
||||
<module>ruoyi-generator</module>
|
||||
<module>ruoyi-common</module>
|
||||
<module>ruoyi-loan-pricing</module>
|
||||
</modules>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<parameters>true</parameters>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,53 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>ruoyi</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>3.9.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
<artifactId>ruoyi-admin</artifactId>
|
||||
|
||||
<description>
|
||||
web服务入口
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- spring-boot-devtools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional> <!-- 表示依赖不会传递 -->
|
||||
</dependency>
|
||||
|
||||
<!-- spring-doc -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Mysql驱动包 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 核心模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 定时任务-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-quartz</artifactId>
|
||||
</dependency>
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>ruoyi</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>3.9.2</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
<artifactId>ruoyi-admin</artifactId>
|
||||
|
||||
<description>
|
||||
web服务入口
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- spring-boot-devtools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional> <!-- 表示依赖不会传递 -->
|
||||
</dependency>
|
||||
|
||||
<!-- swagger3-->
|
||||
<dependency>
|
||||
<groupId>io.springfox</groupId>
|
||||
<artifactId>springfox-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
|
||||
<dependency>
|
||||
<groupId>io.swagger</groupId>
|
||||
<artifactId>swagger-models</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Mysql驱动包 -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 核心模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 定时任务-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-quartz</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 代码生成-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
@@ -60,42 +67,36 @@
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>3.5.4</version>
|
||||
<configuration>
|
||||
<addResources>true</addResources>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||
<warName>${project.artifactId}</warName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
</build>
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>2.5.15</version>
|
||||
<configuration>
|
||||
<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||
<warName>${project.artifactId}</warName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
package com.ruoyi;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
|
||||
/**
|
||||
* 启动程序
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
|
||||
public class RuoYiApplication
|
||||
{
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// System.setProperty("spring.devtools.restart.enabled", "false");
|
||||
SpringApplication.run(RuoYiApplication.class, args);
|
||||
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" +
|
||||
" .-------. ____ __ \n" +
|
||||
" | _ _ \\ \\ \\ / / \n" +
|
||||
" | ( ' ) | \\ _. / ' \n" +
|
||||
" |(_ o _) / _( )_ .' \n" +
|
||||
" | (_,_).' __ ___(_ o _)' \n" +
|
||||
" | |\\ \\ | || |(_,_)' \n" +
|
||||
" | | \\ `' /| `-' / \n" +
|
||||
" | | \\ / \\ / \n" +
|
||||
" ''-' `'-' `-..-' ");
|
||||
}
|
||||
}
|
||||
package com.ruoyi;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
|
||||
/**
|
||||
* 启动程序
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
|
||||
public class RuoYiApplication
|
||||
{
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// System.setProperty("spring.devtools.restart.enabled", "false");
|
||||
SpringApplication.run(RuoYiApplication.class, args);
|
||||
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" +
|
||||
" .-------. ____ __ \n" +
|
||||
" | _ _ \\ \\ \\ / / \n" +
|
||||
" | ( ' ) | \\ _. / ' \n" +
|
||||
" |(_ o _) / _( )_ .' \n" +
|
||||
" | (_,_).' __ ___(_ o _)' \n" +
|
||||
" | |\\ \\ | || |(_,_)' \n" +
|
||||
" | | \\ `' /| `-' / \n" +
|
||||
" | | \\ / \\ / \n" +
|
||||
" ''-' `'-' `-..-' ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.ruoyi;
|
||||
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
|
||||
/**
|
||||
* web容器中进行部署
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class RuoYiServletInitializer extends SpringBootServletInitializer
|
||||
{
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
|
||||
{
|
||||
return application.sources(RuoYiApplication.class);
|
||||
}
|
||||
}
|
||||
package com.ruoyi;
|
||||
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
|
||||
/**
|
||||
* web容器中进行部署
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class RuoYiServletInitializer extends SpringBootServletInitializer
|
||||
{
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
|
||||
{
|
||||
return application.sources(RuoYiApplication.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
package com.ruoyi.web.controller.common;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import jakarta.annotation.Resource;
|
||||
import javax.imageio.ImageIO;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.FastByteArrayOutputStream;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.google.code.kaptcha.Producer;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.sign.Base64;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
|
||||
/**
|
||||
* 验证码操作处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
public class CaptchaController
|
||||
{
|
||||
@Resource(name = "captchaProducer")
|
||||
private Producer captchaProducer;
|
||||
|
||||
@Resource(name = "captchaProducerMath")
|
||||
private Producer captchaProducerMath;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
@GetMapping("/captchaImage")
|
||||
public AjaxResult getCode(HttpServletResponse response) throws IOException
|
||||
{
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
boolean captchaEnabled = configService.selectCaptchaEnabled();
|
||||
ajax.put("captchaEnabled", captchaEnabled);
|
||||
if (!captchaEnabled)
|
||||
{
|
||||
return ajax;
|
||||
}
|
||||
|
||||
// 保存验证码信息
|
||||
String uuid = IdUtils.simpleUUID();
|
||||
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
|
||||
String capStr = null, code = null;
|
||||
BufferedImage image = null;
|
||||
|
||||
// 生成验证码
|
||||
String captchaType = RuoYiConfig.getCaptchaType();
|
||||
if ("math".equals(captchaType))
|
||||
{
|
||||
String capText = captchaProducerMath.createText();
|
||||
capStr = capText.substring(0, capText.lastIndexOf("@"));
|
||||
code = capText.substring(capText.lastIndexOf("@") + 1);
|
||||
image = captchaProducerMath.createImage(capStr);
|
||||
}
|
||||
else if ("char".equals(captchaType))
|
||||
{
|
||||
capStr = code = captchaProducer.createText();
|
||||
image = captchaProducer.createImage(capStr);
|
||||
}
|
||||
|
||||
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
|
||||
// 转换流信息写出
|
||||
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
|
||||
try
|
||||
{
|
||||
ImageIO.write(image, "jpg", os);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
ajax.put("uuid", uuid);
|
||||
ajax.put("img", Base64.encode(os.toByteArray()));
|
||||
return ajax;
|
||||
}
|
||||
}
|
||||
package com.ruoyi.web.controller.common;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Resource;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.FastByteArrayOutputStream;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.google.code.kaptcha.Producer;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.sign.Base64;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
|
||||
/**
|
||||
* 验证码操作处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
public class CaptchaController
|
||||
{
|
||||
@Resource(name = "captchaProducer")
|
||||
private Producer captchaProducer;
|
||||
|
||||
@Resource(name = "captchaProducerMath")
|
||||
private Producer captchaProducerMath;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
@GetMapping("/captchaImage")
|
||||
public AjaxResult getCode(HttpServletResponse response) throws IOException
|
||||
{
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
boolean captchaEnabled = configService.selectCaptchaEnabled();
|
||||
ajax.put("captchaEnabled", captchaEnabled);
|
||||
if (!captchaEnabled)
|
||||
{
|
||||
return ajax;
|
||||
}
|
||||
|
||||
// 保存验证码信息
|
||||
String uuid = IdUtils.simpleUUID();
|
||||
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
|
||||
String capStr = null, code = null;
|
||||
BufferedImage image = null;
|
||||
|
||||
// 生成验证码
|
||||
String captchaType = RuoYiConfig.getCaptchaType();
|
||||
if ("math".equals(captchaType))
|
||||
{
|
||||
String capText = captchaProducerMath.createText();
|
||||
capStr = capText.substring(0, capText.lastIndexOf("@"));
|
||||
code = capText.substring(capText.lastIndexOf("@") + 1);
|
||||
image = captchaProducerMath.createImage(capStr);
|
||||
}
|
||||
else if ("char".equals(captchaType))
|
||||
{
|
||||
capStr = code = captchaProducer.createText();
|
||||
image = captchaProducer.createImage(capStr);
|
||||
}
|
||||
|
||||
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
|
||||
// 转换流信息写出
|
||||
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
|
||||
try
|
||||
{
|
||||
ImageIO.write(image, "jpg", os);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
ajax.put("uuid", uuid);
|
||||
ajax.put("img", Base64.encode(os.toByteArray()));
|
||||
return ajax;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,162 +1,162 @@
|
||||
package com.ruoyi.web.controller.common;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.framework.config.ServerConfig;
|
||||
|
||||
/**
|
||||
* 通用请求处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/common")
|
||||
public class CommonController
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
|
||||
|
||||
@Autowired
|
||||
private ServerConfig serverConfig;
|
||||
|
||||
private static final String FILE_DELIMITER = ",";
|
||||
|
||||
/**
|
||||
* 通用下载请求
|
||||
*
|
||||
* @param fileName 文件名称
|
||||
* @param delete 是否删除
|
||||
*/
|
||||
@GetMapping("/download")
|
||||
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!FileUtils.checkAllowDownload(fileName))
|
||||
{
|
||||
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
|
||||
}
|
||||
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
|
||||
String filePath = RuoYiConfig.getDownloadPath() + fileName;
|
||||
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
FileUtils.setAttachmentResponseHeader(response, realFileName);
|
||||
FileUtils.writeBytes(filePath, response.getOutputStream());
|
||||
if (delete)
|
||||
{
|
||||
FileUtils.deleteFile(filePath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("下载文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用上传请求(单个)
|
||||
*/
|
||||
@PostMapping("/upload")
|
||||
public AjaxResult uploadFile(MultipartFile file) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
// 上传文件路径
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
// 上传并返回新文件名称
|
||||
String fileName = FileUploadUtils.upload(filePath, file);
|
||||
String url = serverConfig.getUrl() + fileName;
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("url", url);
|
||||
ajax.put("fileName", fileName);
|
||||
ajax.put("newFileName", FileUtils.getName(fileName));
|
||||
ajax.put("originalFilename", file.getOriginalFilename());
|
||||
return ajax;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用上传请求(多个)
|
||||
*/
|
||||
@PostMapping("/uploads")
|
||||
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
// 上传文件路径
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
List<String> urls = new ArrayList<String>();
|
||||
List<String> fileNames = new ArrayList<String>();
|
||||
List<String> newFileNames = new ArrayList<String>();
|
||||
List<String> originalFilenames = new ArrayList<String>();
|
||||
for (MultipartFile file : files)
|
||||
{
|
||||
// 上传并返回新文件名称
|
||||
String fileName = FileUploadUtils.upload(filePath, file);
|
||||
String url = serverConfig.getUrl() + fileName;
|
||||
urls.add(url);
|
||||
fileNames.add(fileName);
|
||||
newFileNames.add(FileUtils.getName(fileName));
|
||||
originalFilenames.add(file.getOriginalFilename());
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("urls", StringUtils.join(urls, FILE_DELIMITER));
|
||||
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMITER));
|
||||
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMITER));
|
||||
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMITER));
|
||||
return ajax;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地资源通用下载
|
||||
*/
|
||||
@GetMapping("/download/resource")
|
||||
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
|
||||
throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!FileUtils.checkAllowDownload(resource))
|
||||
{
|
||||
throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
|
||||
}
|
||||
// 本地资源路径
|
||||
String localPath = RuoYiConfig.getProfile();
|
||||
// 数据库资源地址
|
||||
String downloadPath = localPath + FileUtils.stripPrefix(resource);
|
||||
// 下载名称
|
||||
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
FileUtils.setAttachmentResponseHeader(response, downloadName);
|
||||
FileUtils.writeBytes(downloadPath, response.getOutputStream());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("下载文件失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.ruoyi.web.controller.common;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.framework.config.ServerConfig;
|
||||
|
||||
/**
|
||||
* 通用请求处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/common")
|
||||
public class CommonController
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
|
||||
|
||||
@Autowired
|
||||
private ServerConfig serverConfig;
|
||||
|
||||
private static final String FILE_DELIMITER = ",";
|
||||
|
||||
/**
|
||||
* 通用下载请求
|
||||
*
|
||||
* @param fileName 文件名称
|
||||
* @param delete 是否删除
|
||||
*/
|
||||
@GetMapping("/download")
|
||||
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!FileUtils.checkAllowDownload(fileName))
|
||||
{
|
||||
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
|
||||
}
|
||||
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
|
||||
String filePath = RuoYiConfig.getDownloadPath() + fileName;
|
||||
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
FileUtils.setAttachmentResponseHeader(response, realFileName);
|
||||
FileUtils.writeBytes(filePath, response.getOutputStream());
|
||||
if (delete)
|
||||
{
|
||||
FileUtils.deleteFile(filePath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("下载文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用上传请求(单个)
|
||||
*/
|
||||
@PostMapping("/upload")
|
||||
public AjaxResult uploadFile(MultipartFile file) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
// 上传文件路径
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
// 上传并返回新文件名称
|
||||
String fileName = FileUploadUtils.upload(filePath, file);
|
||||
String url = serverConfig.getUrl() + fileName;
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("url", url);
|
||||
ajax.put("fileName", fileName);
|
||||
ajax.put("newFileName", FileUtils.getName(fileName));
|
||||
ajax.put("originalFilename", file.getOriginalFilename());
|
||||
return ajax;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用上传请求(多个)
|
||||
*/
|
||||
@PostMapping("/uploads")
|
||||
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
// 上传文件路径
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
List<String> urls = new ArrayList<String>();
|
||||
List<String> fileNames = new ArrayList<String>();
|
||||
List<String> newFileNames = new ArrayList<String>();
|
||||
List<String> originalFilenames = new ArrayList<String>();
|
||||
for (MultipartFile file : files)
|
||||
{
|
||||
// 上传并返回新文件名称
|
||||
String fileName = FileUploadUtils.upload(filePath, file);
|
||||
String url = serverConfig.getUrl() + fileName;
|
||||
urls.add(url);
|
||||
fileNames.add(fileName);
|
||||
newFileNames.add(FileUtils.getName(fileName));
|
||||
originalFilenames.add(file.getOriginalFilename());
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("urls", StringUtils.join(urls, FILE_DELIMITER));
|
||||
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMITER));
|
||||
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMITER));
|
||||
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMITER));
|
||||
return ajax;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地资源通用下载
|
||||
*/
|
||||
@GetMapping("/download/resource")
|
||||
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
|
||||
throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!FileUtils.checkAllowDownload(resource))
|
||||
{
|
||||
throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
|
||||
}
|
||||
// 本地资源路径
|
||||
String localPath = RuoYiConfig.getProfile();
|
||||
// 数据库资源地址
|
||||
String downloadPath = localPath + FileUtils.stripPrefix(resource);
|
||||
// 下载名称
|
||||
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
FileUtils.setAttachmentResponseHeader(response, downloadName);
|
||||
FileUtils.writeBytes(downloadPath, response.getOutputStream());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("下载文件失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.SysCache;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@@ -15,33 +22,24 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 缓存监控
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/cache")
|
||||
public class CacheController
|
||||
{
|
||||
private final RedisCache redisCache;
|
||||
|
||||
private final static List<SysCache> caches = new ArrayList<SysCache>();
|
||||
private static final List<SysCache> CACHES = new ArrayList<SysCache>();
|
||||
|
||||
static
|
||||
{
|
||||
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
|
||||
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
|
||||
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
|
||||
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
|
||||
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
|
||||
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
|
||||
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
|
||||
CACHES.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
|
||||
CACHES.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
|
||||
CACHES.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
|
||||
CACHES.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
|
||||
CACHES.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
|
||||
CACHES.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
|
||||
CACHES.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
|
||||
}
|
||||
|
||||
public CacheController(RedisCache redisCache)
|
||||
@@ -55,23 +53,23 @@ public class CacheController
|
||||
{
|
||||
InMemoryCacheStats stats = redisCache.getCacheStats();
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("cache_type", stats.cacheType());
|
||||
info.put("cache_mode", stats.mode());
|
||||
info.put("key_size", stats.keySize());
|
||||
info.put("hit_count", stats.hitCount());
|
||||
info.put("miss_count", stats.missCount());
|
||||
info.put("expired_count", stats.expiredCount());
|
||||
info.put("write_count", stats.writeCount());
|
||||
info.put("cache_type", stats.getCacheType());
|
||||
info.put("cache_mode", stats.getMode());
|
||||
info.put("key_size", stats.getKeySize());
|
||||
info.put("hit_count", stats.getHitCount());
|
||||
info.put("miss_count", stats.getMissCount());
|
||||
info.put("expired_count", stats.getExpiredCount());
|
||||
info.put("write_count", stats.getWriteCount());
|
||||
|
||||
Map<String, Object> result = new HashMap<>(3);
|
||||
result.put("info", info);
|
||||
result.put("dbSize", stats.keySize());
|
||||
result.put("dbSize", stats.getKeySize());
|
||||
|
||||
List<Map<String, String>> pieList = new ArrayList<>();
|
||||
pieList.add(statEntry("hit_count", stats.hitCount()));
|
||||
pieList.add(statEntry("miss_count", stats.missCount()));
|
||||
pieList.add(statEntry("expired_count", stats.expiredCount()));
|
||||
pieList.add(statEntry("write_count", stats.writeCount()));
|
||||
pieList.add(statEntry("hit_count", stats.getHitCount()));
|
||||
pieList.add(statEntry("miss_count", stats.getMissCount()));
|
||||
pieList.add(statEntry("expired_count", stats.getExpiredCount()));
|
||||
pieList.add(statEntry("write_count", stats.getWriteCount()));
|
||||
result.put("commandStats", pieList);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
@@ -80,7 +78,7 @@ public class CacheController
|
||||
@GetMapping("/getNames")
|
||||
public AjaxResult cache()
|
||||
{
|
||||
return AjaxResult.success(caches);
|
||||
return AjaxResult.success(CACHES);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@@ -139,9 +137,9 @@ public class CacheController
|
||||
{
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
if (cacheValue instanceof String stringValue)
|
||||
if (cacheValue instanceof String)
|
||||
{
|
||||
return stringValue;
|
||||
return (String) cacheValue;
|
||||
}
|
||||
return JSON.toJSONString(cacheValue);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.framework.web.domain.Server;
|
||||
|
||||
/**
|
||||
* 服务器监控
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/server")
|
||||
public class ServerController
|
||||
{
|
||||
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||
@GetMapping()
|
||||
public AjaxResult getInfo() throws Exception
|
||||
{
|
||||
Server server = new Server();
|
||||
server.copyTo();
|
||||
return AjaxResult.success(server);
|
||||
}
|
||||
}
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.framework.web.domain.Server;
|
||||
|
||||
/**
|
||||
* 服务器监控
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/server")
|
||||
public class ServerController
|
||||
{
|
||||
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||
@GetMapping()
|
||||
public AjaxResult getInfo() throws Exception
|
||||
{
|
||||
Server server = new Server();
|
||||
server.copyTo();
|
||||
return AjaxResult.success(server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import java.util.List;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.framework.web.service.SysPasswordService;
|
||||
import com.ruoyi.system.domain.SysLogininfor;
|
||||
import com.ruoyi.system.service.ISysLogininforService;
|
||||
|
||||
/**
|
||||
* 系统访问记录
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/logininfor")
|
||||
public class SysLogininforController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISysLogininforService logininforService;
|
||||
|
||||
@Autowired
|
||||
private SysPasswordService passwordService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysLogininfor logininfor)
|
||||
{
|
||||
startPage();
|
||||
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@Log(title = "登录日志", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, SysLogininfor logininfor)
|
||||
{
|
||||
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
|
||||
ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
|
||||
util.exportExcel(response, list, "登录日志");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
|
||||
@Log(title = "登录日志", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{infoIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] infoIds)
|
||||
{
|
||||
return toAjax(logininforService.deleteLogininforByIds(infoIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
|
||||
@Log(title = "登录日志", businessType = BusinessType.CLEAN)
|
||||
@DeleteMapping("/clean")
|
||||
public AjaxResult clean()
|
||||
{
|
||||
logininforService.cleanLogininfor();
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
|
||||
@Log(title = "账户解锁", businessType = BusinessType.OTHER)
|
||||
@GetMapping("/unlock/{userName}")
|
||||
public AjaxResult unlock(@PathVariable("userName") String userName)
|
||||
{
|
||||
passwordService.clearLoginRecordCache(userName);
|
||||
return success();
|
||||
}
|
||||
}
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.framework.web.service.SysPasswordService;
|
||||
import com.ruoyi.system.domain.SysLogininfor;
|
||||
import com.ruoyi.system.service.ISysLogininforService;
|
||||
|
||||
/**
|
||||
* 系统访问记录
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/logininfor")
|
||||
public class SysLogininforController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISysLogininforService logininforService;
|
||||
|
||||
@Autowired
|
||||
private SysPasswordService passwordService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysLogininfor logininfor)
|
||||
{
|
||||
startPage();
|
||||
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@Log(title = "登录日志", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, SysLogininfor logininfor)
|
||||
{
|
||||
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
|
||||
ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
|
||||
util.exportExcel(response, list, "登录日志");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
|
||||
@Log(title = "登录日志", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{infoIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] infoIds)
|
||||
{
|
||||
return toAjax(logininforService.deleteLogininforByIds(infoIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
|
||||
@Log(title = "登录日志", businessType = BusinessType.CLEAN)
|
||||
@DeleteMapping("/clean")
|
||||
public AjaxResult clean()
|
||||
{
|
||||
logininforService.cleanLogininfor();
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
|
||||
@Log(title = "账户解锁", businessType = BusinessType.OTHER)
|
||||
@GetMapping("/unlock/{userName}")
|
||||
public AjaxResult unlock(@PathVariable("userName") String userName)
|
||||
{
|
||||
passwordService.clearLoginRecordCache(userName);
|
||||
return success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import java.util.List;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.system.domain.SysOperLog;
|
||||
import com.ruoyi.system.service.ISysOperLogService;
|
||||
|
||||
/**
|
||||
* 操作日志记录
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/operlog")
|
||||
public class SysOperlogController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISysOperLogService operLogService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysOperLog operLog)
|
||||
{
|
||||
startPage();
|
||||
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@Log(title = "操作日志", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, SysOperLog operLog)
|
||||
{
|
||||
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
|
||||
ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
|
||||
util.exportExcel(response, list, "操作日志");
|
||||
}
|
||||
|
||||
@Log(title = "操作日志", businessType = BusinessType.DELETE)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
|
||||
@DeleteMapping("/{operIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] operIds)
|
||||
{
|
||||
return toAjax(operLogService.deleteOperLogByIds(operIds));
|
||||
}
|
||||
|
||||
@Log(title = "操作日志", businessType = BusinessType.CLEAN)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
|
||||
@DeleteMapping("/clean")
|
||||
public AjaxResult clean()
|
||||
{
|
||||
operLogService.cleanOperLog();
|
||||
return success();
|
||||
}
|
||||
}
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.system.domain.SysOperLog;
|
||||
import com.ruoyi.system.service.ISysOperLogService;
|
||||
|
||||
/**
|
||||
* 操作日志记录
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/operlog")
|
||||
public class SysOperlogController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISysOperLogService operLogService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysOperLog operLog)
|
||||
{
|
||||
startPage();
|
||||
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@Log(title = "操作日志", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, SysOperLog operLog)
|
||||
{
|
||||
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
|
||||
ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
|
||||
util.exportExcel(response, list, "操作日志");
|
||||
}
|
||||
|
||||
@Log(title = "操作日志", businessType = BusinessType.DELETE)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
|
||||
@DeleteMapping("/{operIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] operIds)
|
||||
{
|
||||
return toAjax(operLogService.deleteOperLogByIds(operIds));
|
||||
}
|
||||
|
||||
@Log(title = "操作日志", businessType = BusinessType.CLEAN)
|
||||
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
|
||||
@DeleteMapping("/clean")
|
||||
public AjaxResult clean()
|
||||
{
|
||||
operLogService.cleanOperLog();
|
||||
return success();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user