1 Commits

Author SHA1 Message Date
wkc
28088d43a8 迁移项目到 RuoYi-Vue springboot2 基线 2026-04-14 15:13:51 +08:00
650 changed files with 74521 additions and 80362 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

101
.gitignore vendored
View File

@@ -1,63 +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/
ruoyi-ui/dist.zip
????????_892.zip
*/src/test/
ruoyi-ui/tests
.playwright-cli
tongweb_63310.properties
audit.log
.DS_Store
*/.DS_Store
.codegraph/
!*/build/*.java
!*/build/*.html
!*/build/*.xml

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

30
AGENTS.md Normal file
View File

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

View File

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

BIN
bin/.DS_Store vendored

Binary file not shown.

View File

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

View File

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

0
bin/prod/restart_java_test.sh Normal file → Executable file
View File

0
bin/restart_java_backend_test.sh Normal file → Executable file
View File

View File

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

View File

@@ -1,95 +0,0 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
DATE_STAMP=$(date "+%Y%m%d")
RELEASE_ZIP="$ROOT_DIR/${DATE_STAMP}_892.zip"
BACKEND_JAR_SOURCE="$ROOT_DIR/ruoyi-admin/target/ruoyi-admin.jar"
FRONTEND_DIR="$ROOT_DIR/ruoyi-ui"
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
FRONTEND_DIST_ZIP="$FRONTEND_DIR/dist.zip"
NODE_VERSION="14"
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
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
cleanup() {
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
fi
}
build_backend() {
log_info "开始构建后端生产 jar"
(
cd "$ROOT_DIR"
mvn -pl ruoyi-admin -am clean package -DskipTests
)
if [ ! -f "$BACKEND_JAR_SOURCE" ]; then
log_error "未生成后端 jar: $BACKEND_JAR_SOURCE"
exit 1
fi
}
build_frontend() {
log_info "开始构建前端生产 dist"
ROOT_DIR="$ROOT_DIR" NODE_VERSION="$NODE_VERSION" zsh -lic 'nvm use "$NODE_VERSION" >/dev/null && npm --prefix "$ROOT_DIR/ruoyi-ui" run build:prod'
if [ ! -f "$FRONTEND_DIST_DIR/index.html" ]; then
log_error "前端生产构建失败,未找到: $FRONTEND_DIST_DIR/index.html"
exit 1
fi
rm -f "$FRONTEND_DIST_ZIP"
(
cd "$FRONTEND_DIR"
zip -qr "$FRONTEND_DIST_ZIP" dist
)
if [ ! -f "$FRONTEND_DIST_ZIP" ]; then
log_error "未生成前端压缩包: $FRONTEND_DIST_ZIP"
exit 1
fi
}
package_release() {
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/loan_pricing_release.XXXXXX")
trap cleanup EXIT INT TERM
cp "$BACKEND_JAR_SOURCE" "$WORK_DIR/ruoyi-admin.jar"
cp "$FRONTEND_DIST_ZIP" "$WORK_DIR/dist.zip"
rm -f "$RELEASE_ZIP"
(
cd "$WORK_DIR"
zip -qr "$RELEASE_ZIP" ruoyi-admin.jar dist.zip
)
log_info "上线压缩包已生成: $RELEASE_ZIP"
}
main() {
require_command mvn
require_command zsh
require_command zip
build_backend
build_frontend
package_release
}
main "$@"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -284,7 +284,6 @@ GET /loanPricing/workflow/20250119143025123
"idType": "身份证",
"idNum": "330102199001011234",
"baseLoanRate": "3.45",
"greyBlackCust": "1",
"isFirstLoan": "true",
"faithDay": "365",
"custAge": "35",
@@ -309,7 +308,7 @@ GET /loanPricing/workflow/20250119143025123
"midPerFinMan": "false",
"midPerEtc": "true",
"bpMid": "-15",
"totalBpRelevance": "-50",
"totoalBpRelevance": "-50",
"applyAmt": "500000",
"bpLoanAmount": "0",
"loanPurpose": "consumer",
@@ -325,7 +324,7 @@ GET /loanPricing/workflow/20250119143025123
"interestOverdue": "false",
"cardOverdue": "false",
"bpGreyOverdue": "0",
"totalBpRisk": "0",
"totoalBpRisk": "0",
"totalBp": "-80",
"calculateRate": "2.65"
},
@@ -364,7 +363,7 @@ GET /loanPricing/workflow/20250119143025123
"isGreenLoan": "false",
"isTechEnt": "true",
"bpEntType": "-25",
"totalBpRelevance": "-55",
"totoalBpRelevance": "-55",
"loanTerm": "36",
"bpLoanTerm": "0",
"applyAmt": "1000000",
@@ -377,7 +376,7 @@ GET /loanPricing/workflow/20250119143025123
"interestOverdue": "false",
"cardOverdue": "false",
"bpGreyOverdue": "0",
"totalBpRisk": "0",
"totoalBpRisk": "0",
"totalBp": "-85",
"calculateRate": "2.60"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
# 对公还款方式移除与抵质押字段联动实施记录
## 修改内容
- 对公新增弹窗移除 `还款方式` 输入项、初始化字段、重置字段、必填校验和提交字段。
- 对公详情页与模型输出展示移除 `还款方式`
- 对公、对私新增弹窗中,`担保方式``抵押``质押` 时才展示 `抵质押类型``抵质押物是否第三方所有`
- `抵质押类型` 根据担保方式动态切换:
- `抵押``一类``二类``三类``四类``其他`
- `质押``存单质押``其他`
- 担保方式切换时清空已选抵质押类型和第三方所有标识,隐藏抵质押字段时不向后端提交。
- 对公创建 DTO 取消 `repayMethod` 必填与枚举校验;`collType` 不再全局必填,合法值调整为 `一类/二类/三类/四类/其他/存单质押`
## 验证结果
- 前端静态测试通过:
- `npm run test:corporate-create-input-params`
- `npm run test:corporate-display-fields`
- `npm run test:personal-create-input-params`
- 后端编译与单测通过:
- `mvn -pl ruoyi-loan-pricing -am -Dtest=ModelCorpOutputFieldsTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 后端接口验证通过:
- `信用` 不传 `repayMethod`、不传抵质押字段可创建。
- `抵押``一类` 且不传 `repayMethod` 可创建。
- `质押``存单质押` 且不传 `repayMethod` 可创建。
- 缺少 `custIsn`、缺少 `guarType`、非法 `guarType` 仍返回参数错误。
- 真实前端页面验证通过:
- 对公新增弹窗不显示 `还款方式`
- 对公、对私新增弹窗在 `信用/保证` 下隐藏抵质押字段。
- 对公、对私新增弹窗在 `抵押/质押` 下显示抵质押字段,且选项分别符合规则。
- 对公详情页与模型输出区域不再显示 `还款方式`
## 说明
- 本次不删除数据库字段和实体字段,仅停止创建入口要求和页面展示,保留历史数据结构。

View File

@@ -1,41 +0,0 @@
# 个人/企业模型接口拆分实施记录
## 修改内容
- 将统一模型接口配置 `model.url` 拆分为 `model.personal-url``model.corporate-url`
- `dev``uat` 环境分别指向本地个人/企业 mock
- `http://localhost:63310/rate/pricing/mock/invokeModel/personal`
- `http://localhost:63310/rate/pricing/mock/invokeModel/corporate`
- `pro` 环境改为从 `MODEL_PERSONAL_URL``MODEL_CORPORATE_URL` 读取真实接口地址。
- `ModelService` 拆分为 `invokePersonalModel``invokeCorporateModel`,分别返回 `ModelRetailOutputFields``ModelCorpOutputFields`
- `LoanPricingModelService` 根据 `custType` 调用对应模型接口,个人只写个人模型输出表,企业只写企业模型输出表。
- mock 控制器拆分为个人、企业两个入口,不再保留统一 mock 路径作为业务调用入口。
## 字段管理
- 个人模型返回字段继续由 `ModelRetailOutputFields``model_retail_output_fields` 管理。
- 企业模型返回字段继续由 `ModelCorpOutputFields``model_corp_output_fields` 管理。
- 未新增统一返回对象,避免个人/企业字段混在同一套结构中。
## 验证记录
- 后端单测:
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,ModelRetailOutputFieldsTest,ModelCorpOutputFieldsTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:通过,`Tests run: 5, Failures: 0, Errors: 0`
- 后端打包与启动:
- `./bin/restart_java_backend.sh restart`
- 结果:打包成功,提升权限后启动成功,后端监听 `63310`
- 真实接口验证:
- `/login/test` 获取 token 成功。
- 调用 `/loanPricing/workflow/create/personal` 创建个人流程,流水号 `20260427150819677`
- 查询个人详情,返回 `modelRetailOutputFields.finalCalculateRate=6.05``modelCorpOutputFields=null`
- 调用 `/loanPricing/workflow/create/corporate` 创建企业流程,流水号 `20260427150820494`
- 查询企业详情,返回 `modelCorpOutputFields.finalCalculateRate=3.732``modelRetailOutputFields=null`
- 缺少 `custIsn` 的个人创建请求返回 `客户内码不能为空`
- 后端日志确认个人命中 `/rate/pricing/mock/invokeModel/personal`,企业命中 `/rate/pricing/mock/invokeModel/corporate`
- 测试结束后已执行 `./bin/restart_java_backend.sh stop` 停止本次启动的后端进程。
## 注意事项
- 生产环境启动前必须提供 `MODEL_PERSONAL_URL``MODEL_CORPORATE_URL`
- 本次不改前端页面和现有业务接口路径。

View File

@@ -1,54 +0,0 @@
# 2026-04-27 个人模型输出灰黑名单客户字段实施记录
## 修改内容
- 后端个人模型输出实体 `ModelRetailOutputFields` 新增 `greyBlackCust` 字段,承接个人模型返回的 `0/1` 输出值。
- 个人模型 mock 返回文件 `retail_output.json` 新增 `greyBlackCust: "1"`,用于本地模型调用链路验证。
- `model_retail_output_fields` 表结构新增 `grey_black_cust` 字段,并补充增量迁移脚本 `sql/add_model_retail_grey_black_cust_20260427.sql`
- 前端模型输出组件在个人客户“基本信息”分组中展示“灰黑名单客户”,直接展示后端返回值 `0/1`
- 接口文档示例补充 `greyBlackCust` 返回字段。
## 涉及文件
- `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/ModelOutputDisplay.vue`
- `ruoyi-ui/tests/retail-display-fields.test.js`
- `sql/model_retail.sql`
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
- `sql/add_model_retail_grey_black_cust_20260427.sql`
- `doc/api/loan-pricing-workflow-api.md`
## 数据库变更
- 已在开发库 `loan-pricing.model_retail_output_fields` 执行新增列:
```sql
ALTER TABLE model_retail_output_fields
ADD COLUMN grey_black_cust varchar(100) DEFAULT '' COMMENT '灰黑名单客户' AFTER base_loan_rate;
```
- 回查结果确认 `grey_black_cust` 字段存在。
## 验证记录
- `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 通过,确认实体字段存在,个人/企业模型调用基础链路未回归。
- `zsh -lic 'nvm use 14 >/dev/null && npm --prefix ruoyi-ui run test:retail-display-fields && npm --prefix ruoyi-ui run test:model-output-flat-display'`
- 通过,确认前端包含 `retailOutput.greyBlackCust` 且字段位于个人模型输出“基本信息”分组。
- `zsh -lic 'nvm use 14 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
- 通过,存在既有包体积 warning无编译错误。
- 后端真实接口验证:
- 重启后端后调用个人流程创建接口,业务流水号 `20260427153305173`
- 调用详情接口返回 `modelRetailOutputFields.greyBlackCust = 1`
- 数据库联表回查 `model_retail_output_fields.grey_black_cust = 1`
- browser-use 真实页面验证:
- 使用前端开发服务 `http://127.0.0.1:63311/` 打开真实流程详情页。
- 页面 `模型输出 > 基本信息` 中可见“灰黑名单客户”,展示值为 `1`
- 测试结束后已停止本次启动的后端 `63310` 与前端 `63311` 进程,并回查端口不再监听。
## 备注
- 组合执行 `LoanPricingModelServicePersonalParamsTest` 时,当前本机 JDK 21 下 Mockito inline ByteBuddy 自附加失败;该失败与本次字段改动无关。已单独执行本次直接相关的非 Mockito 失败用例并通过。

View File

@@ -1,27 +0,0 @@
# 根目录 892 上线压缩包生成脚本实施记录
## 保存路径检查
- 脚本保存路径:项目根目录 `build_release_892.sh`
- 实施记录保存路径:`doc/implementation-report-2026-04-27-root-release-package-892.md`
## 修改内容
- 新增根目录脚本 `build_release_892.sh`
- 脚本执行后自动在项目根目录生成 `YYYYMMDD_892.zip`
- 压缩包根层结构固定为:
- `ruoyi-admin.jar`
- `dist.zip`
- 后端产物来自最新执行的 `mvn -pl ruoyi-admin -am clean package -DskipTests`
- 前端产物来自 `nvm use 14` 后执行的 `npm --prefix ruoyi-ui run build:prod`
- 前端构建完成后重新生成 `ruoyi-ui/dist.zip`
- 更新 `.gitignore`,忽略根目录生成的 `????????_892.zip`
## 验证结果
- 已执行 `sh -n build_release_892.sh`,语法校验通过
- 已执行 `./build_release_892.sh`,后端 Maven 构建成功,前端生产构建成功
- 前端构建过程中仅出现原有包体积 warning 与 npm 更新检查权限提示,不影响产物生成
- 已生成根目录压缩包:`20260427_892.zip`
- 已按最新要求调整压缩包结构,根层直接放置两个文件,不再包含 `deploy/` 目录
- 已执行 `unzip -l 20260427_892.zip`,确认压缩包内容为:
- `ruoyi-admin.jar`
- `dist.zip`
- 已执行 `git check-ignore -v 20260427_892.zip ruoyi-ui/dist.zip`,确认根目录上线压缩包和前端临时压缩包均不会进入 git

View File

@@ -1,20 +0,0 @@
# 面包屑重复 key 告警处理实施记录
## 修改内容
- 修复流程列表首页进入后控制台出现 `Duplicate keys detected: '/index'` 的问题。
- 根因是当前首页实际路由为 `/index`,面包屑组件仍只按路由名 `Index` 判断首页,导致额外追加的“首页”项与“流程列表”项使用相同路径 `/index` 作为 key。
- 将面包屑首页判断补充为同时识别 `path === '/index'`,避免在首页路由重复追加“首页”项。
## 验证方式
- 启动前端开发服务后,使用真实浏览器访问 `/index`
- 检查控制台不再出现 `Duplicate keys detected: '/index'`
- 检查流程列表页面仍可正常展示。
## 验证结果
- 已使用 Node 14.21.3 启动前端开发服务并通过 Playwright 访问真实页面 `http://127.0.0.1:8080/index`
- 页面成功进入“流程列表”,面包屑仅展示“流程列表”,未再重复追加“首页”。
- 浏览器控制台统计为 `Errors: 0, Warnings: 0`,未再出现 `Duplicate keys detected: '/index'`
- 验证结束后已关闭本次启动的前端 `8080` 进程;后端 `63310` 为验证前已有进程,未做关闭处理。

View File

@@ -1,18 +0,0 @@
# 2026-04-29 业务种类与历史贷款利率设计记录
## 修改内容
- 新增设计文档 `docs/superpowers/specs/2026-04-29-business-type-history-rate-design.md`
- 明确个人和企业新增流程同时增加业务种类和历史贷款利率。
- 明确业务种类保存到流程表并在详情展示,但不进入模型入参。
- 明确历史贷款利率保存到流程表,并作为模型调用入参上传。
- 明确存量转贷时必须查询历史合同并单选一条;未选择历史合同禁止提交。
- 明确历史合同查询采用后端代理外部接口、前端列表单选回填的方案。
- 根据审查意见补充 `LoanPricingConverter` 字段映射、服务层跨字段校验、固定 mock 测试场景、直接接口测试覆盖和历史合同 URL 拼参方式。
## 验证说明
- 本次仅完成设计文档,未进入代码实现。
- 首轮设计审查发现文档存在 5 个实施风险,已按意见补充到设计文档。
- 第二轮设计审查结论为 Approved。
- 后续实施时需要按设计文档分别覆盖后端接口验证、前端交互验证和真实页面浏览器验证。

View File

@@ -1,18 +0,0 @@
# 2026-04-29 业务种类与历史贷款利率实施计划记录
## 修改内容
- 新增后端实施计划 `docs/superpowers/plans/2026-04-29-business-type-history-rate-backend-plan.md`
- 新增前端实施计划 `docs/superpowers/plans/2026-04-29-business-type-history-rate-frontend-plan.md`
- 后端计划覆盖字段、转换器、服务层校验、历史合同代理接口、mock、SQL 和后端测试。
- 前端计划覆盖历史合同查询 API、历史合同单选弹窗、个人/企业新增弹窗、详情展示、静态测试和 browser-use 真实页面验证。
- 根据计划审查意见,补充固定客户号 mock 场景 `HISTORY_EMPTY` / `HISTORY_EMPTY_RATE`,确保 browser-use 真实页面测试能稳定覆盖空列表和空历史利率。
- 根据计划审查意见,修正前端客户号选择测试命令,避免调用不存在的 npm script。
## 验证说明
- 本次仅产出实施计划,未进入代码实现。
- 真实页面测试已按用户要求明确使用 `browser-use:browser`,并禁止打开 prototype 页面。
- 首轮计划审查发现 4 个执行风险,已按意见补充和修订。
- 第二轮计划审查结论为 Approved。
- 后续实施完成后需要补充 `doc/implementation-report-2026-04-29-business-type-history-rate.md` 记录代码改动和验证结果。

View File

@@ -1,91 +0,0 @@
# 业务种类与历史贷款利率实施记录
## 后端实施
- 个人/企业利率定价创建 DTO 新增 `businessType``loanRateHistory` 字段。
- 利率定价流程实体新增 `businessType``loanRateHistory` 持久化字段。
- 模型调用 DTO 新增 `loanRateHistory` 字段,保持不新增 `businessType`
- 个人/企业创建转换器已映射业务种类和历史贷款利率。
- 流程创建服务新增业务种类校验:必填,限定 `新客``存量新增``存量转贷``存量转贷` 必须选择历史贷款合同。
- 新增历史贷款合同代理服务 `LoanRateHistoryService` 和接口 `GET /loanPricing/workflow/history-contract`
- 本地 mock 新增 `GET /rate/pricing/mock/history-contract`,覆盖正常、无历史合同、历史利率为空场景。
- 本地 mock 客户号映射新增固定测试客户号 `HISTORY_EMPTY``HISTORY_EMPTY_RATE`
- dev/uat/pro 配置新增 `loan-rate-history.url`
- SQL 迁移和初始化脚本新增 `business_type``loan_rate_history` 字段。
## 后端验证
- 首次按计划运行 `mvn -pl ruoyi-loan-pricing -am -Dtest=... test` 时,`ruoyi-common` 因未匹配测试触发 Surefire 失败;后续按本仓库多模块测试习惯补充 `-Dsurefire.failIfNoSpecifiedTests=false`
- 当前 Oracle JDK 21 环境下 Mockito inline mock maker 需要预加载 Byte Buddy agent验证命令使用 `JAVA_TOOL_OPTIONS=-javaagent:/Users/wkc/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar`
- 已执行并通过:
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest -Dsurefire.failIfNoSpecifiedTests=false test`
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test`
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- `mvn -pl ruoyi-loan-pricing -am -Dtest=HistoryLoanContractVOTest,LoanRateHistoryServiceTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanPricingWorkflowControllerCustomerMapTest,LoanRatePricingMockControllerCustomerMapTest,CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test`
## 前端实施
- `workflow.js` 新增 `queryHistoryContracts(custIsn)`,请求 `GET /loanPricing/workflow/history-contract`
- 新增共享组件 `HistoryContractSelector.vue`,按单选方式展示历史贷款合同,字段包含客户内码、历史贷款合同号、历史贷款担保方式、历史贷款产品代码、历史贷款利率、历史贷款金额、历史贷款签订时间。
- 个人/企业新增弹窗新增 `业务种类`,选项为 `新客``存量新增``存量转贷`
- 当业务种类为 `存量转贷` 时,按客户内码查询历史贷款合同并弹出单选弹窗;未选合同、无历史合同、历史贷款利率为空时禁止提交。
-`存量转贷` 创建时不提交 `loanRateHistory`
- 个人/企业详情页在业务信息中展示 `业务种类``历史贷款利率`
## 前端静态验证
- 已执行并通过:
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'`
- `zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'`
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
- `build:prod` 通过,仍存在项目原有资源体积 warning。
## 数据库变更验证
- 已按 SQL 脚本对当前开发库执行:
- `ALTER TABLE loan_pricing_workflow ADD COLUMN business_type varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER loan_purpose, ADD COLUMN loan_rate_history varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER business_type;`
- 已回查字段存在:
- `business_type varchar(20)`
- `loan_rate_history varchar(100)`
## 真实页面验证
- 后端已通过 `bin/restart_java_backend.sh restart` 重启并加载最新代码。
- 前端已通过 `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev -- --port 8080'` 启动。
- 使用 in-app browser 打开真实页面 `http://localhost:8080/index`,未使用 prototype 页面。
- 已验证个人 `存量转贷`
- 测试客户内码 `81000922431`
- 历史合同弹窗展示 7 个字段并支持单选。
- 选择合同后提交成功。
- 详情页展示 `业务种类=存量转贷``历史贷款利率=3.65`
- 已验证企业 `存量转贷`
- 测试客户内码 `81000329003`
- 历史合同弹窗展示 7 个字段并支持单选。
- 选择合同后提交成功。
- 详情页展示 `业务种类=存量转贷``历史贷款利率=3.65`
- 已验证个人 `新客`
- 测试客户内码 `81000525694`
- 不弹出历史贷款合同选择。
- 提交成功。
- 详情页展示 `业务种类=新客`,历史贷款利率为空值展示。
- 已验证企业 `存量新增`
- 测试客户内码 `81000769824`
- 不弹出历史贷款合同选择。
- 提交成功。
- 详情页展示 `业务种类=存量新增`,历史贷款利率为空值展示。
- 已验证拦截场景:
- `存量转贷` 打开历史合同弹窗后未选择合同,提示 `请选择历史贷款合同`,禁止提交。
- 固定客户号 `HISTORY_EMPTY` 映射到 `EMPTY_HISTORY`,历史合同查询为空,提示 `未查询到历史贷款合同`,提交时校验 `请选择历史贷款合同`
- 固定客户号 `HISTORY_EMPTY_RATE` 映射到 `EMPTY_RATE`,历史合同存在但历史贷款利率为空,选择时提示 `历史贷款利率不能为空`,提交时仍校验 `请选择历史贷款合同`
- 已回查数据库:
- `81000922431 / 个人 / 存量转贷 / 3.65 / 321000`
- `81000329003 / 企业 / 存量转贷 / 3.65 / 654000`
- `81000525694 / 个人 / 新客 / NULL / 321000`
- `81000769824 / 企业 / 存量新增 / NULL / 654000`
## 进程清理
- 页面验证结束后已停止本次测试启动的前端和后端进程。
- 已确认 `8080``63310` 端口无监听进程。

View File

@@ -1,15 +0,0 @@
# 2026-04-29 客户号查询选择客户内码设计记录
## 修改内容
- 新增设计文档 `docs/superpowers/specs/2026-04-29-customer-map-selection-design.md`
- 明确新增流程从“选择客户类型后直接打开新增弹窗”调整为“选择客户类型 -> 客户号查询 -> 选择客户内码 -> 打开新增弹窗”。
- 明确后端新增个人/企业客户号映射业务接口和两个 mock 接口,配置地址先指向本项目 mock。
- 明确客户号映射返回字段保持下划线命名。
- 明确新增弹窗中的客户内码和客户名称由选择结果自动带入并只读。
## 验证说明
- 本次仅完成设计文档,未进入代码实现。
- 设计文档已通过审查子代理审查,结论为 Approved无阻塞问题。
- 后续实施时需按设计文档中的后端、前端和真实页面测试范围执行验证。

View File

@@ -1,14 +0,0 @@
# 2026-04-29 客户号查询选择客户内码实施计划记录
## 修改内容
- 新增后端实施计划 `docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md`
- 新增前端实施计划 `docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md`
- 后端计划覆盖客户号映射 VO、服务、业务接口、mock 接口、profile 配置和接口验证。
- 前端计划覆盖客户号查询 API、查询选择弹窗、列表页流程串联、个人/企业新增弹窗只读带入和真实页面验证。
## 验证说明
- 本次仅完成实施计划文档,未进入代码实现。
- 计划已按已确认设计拆分为前端和后端两份执行文档。
- 实施计划已通过审查子代理审查,结论为 Approved无阻塞问题。

View File

@@ -1,60 +0,0 @@
# 2026-04-29 客户号查询选择客户内码实施记录
## 修改内容
- 后端新增个人/企业客户号映射业务接口:
- `GET /loanPricing/workflow/customer-map/personal?custId=...`
- `GET /loanPricing/workflow/customer-map/corporate?custId=...`
- 后端新增个人/企业客户号映射 mock 接口:
- `GET /rate/pricing/mock/customer-map/personal?cust_id=...`
- `GET /rate/pricing/mock/customer-map/corporate?cust_id=...`
- 配置文件新增 `customer-map` 个人/企业地址,并在 `dev``uat``pro` profile 中统一指向本项目 mock。
- 前端新增客户号查询选择弹窗。
- 客户号查询选择弹窗宽度调整为窗口宽度的 `80%`
- 客户号查询输入值在查询前去除前后空格;后端转发个人/企业客户号映射接口前同步去除前后空格,并对下游 `cust_id` 查询参数做 URI 编码,避免尾随空格进入 request target 触发 TongWeb `HTTP Status 400`
- 选中客户号查询结果后,返回参数 `cust_id` 去除前三位后自动回填到新增弹窗基础信息的 `证件号码` 字段。
- 个人/企业新增流程改为先查询客户号、选择客户内码,再打开新增弹窗。
- 新增弹窗客户内码和客户名称由选中记录自动带入并只读。
## 验证结果
- 后端针对性测试:通过。
- 命令:`mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:`Tests run: 8, Failures: 0, Errors: 0, Skipped: 0`
- 客户号空格 400 修复补充测试:通过。
- 命令:`mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:`Tests run: 6, Failures: 0, Errors: 0, Skipped: 0`
- 后端打包与启动验证:通过。
- 命令:`mvn -pl ruoyi-admin -am -DskipTests package`
- 命令:`java -jar ruoyi-admin/target/ruoyi-admin.jar --spring.profiles.active=dev`
- 结果:后端以 `dev` profile 启动成功,端口 `63310` 可用。
- 接口验证:通过。
- `GET /rate/pricing/mock/customer-map/personal?cust_id=P001` 返回随机个人客户映射列表,字段为 `cust_id``cust_isn``cust_name``faith_day``balance_avg``loan_count_his``last_loan_date`
- `GET /rate/pricing/mock/customer-map/corporate?cust_id=C001` 返回随机企业客户映射列表,字段同上。
- `GET /rate/pricing/mock/customer-map/personal?cust_id=` 返回 `客户号不能为空`
- 登录后调用 `GET /loanPricing/workflow/customer-map/personal?custId=P001``GET /loanPricing/workflow/customer-map/corporate?custId=C001` 均返回对应映射列表。
- 登录后调用 `GET /loanPricing/workflow/customer-map/personal?custId=` 返回 `客户号不能为空`
- 补充验证:以最新后端临时端口 `63311` 调用 `GET /loanPricing/workflow/customer-map/personal?custId=1w0xc20xb7%20` 返回 `code=200`,返回 `cust_id``1w0xc20xb7`;调用 `GET /loanPricing/workflow/customer-map/corporate?custId=C001%20` 返回 `code=200`,返回 `cust_id``C001`;调用 `GET /loanPricing/workflow/customer-map/personal?custId=%20` 返回 `客户号不能为空`
- 前端针对性测试:通过。
- 命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'`
- 结果:`customer map selection assertions passed``personal create input params assertions passed``corporate create input params assertions passed``workflow-index-refresh test passed`
- 补充命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js'`
- 补充结果:`customer map selection assertions passed`,覆盖客户号去前三位回填证件号码断言。
- 补充回归命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/id-number-validation-removal.test.js'`
- 补充回归结果:`customer map selection assertions passed``personal create input params assertions passed``corporate create input params assertions passed``id number validation removal assertions passed`
- 前端生产构建:通过。
- 命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
- 结果:构建成功;输出 2 个既有体积 warning。
- 真实页面验证:通过。
- 启动前端 `http://localhost:18080/`,登录后进入真实页面 `http://localhost:18080/loanPricing/workflow`
- 个人客户:点击 `新增` -> `个人客户` -> 客户号查询输入 `PTEST003` -> 返回多条客户映射 -> 选择首行 -> 打开 `新增个人利率定价流程`,客户内码自动带入 `81000450472`,客户名称自动带入 `个人客户1`,两个字段均为 `readonly`
- 企业客户:点击 `新增` -> `企业客户` -> 客户号查询输入 `CTEST001` -> 返回多条客户映射 -> 选择首行 -> 打开 `新增企业利率定价流程`,客户内码自动带入 `81000448819`,客户名称自动带入 `企业客户1`,两个字段均为 `readonly`
- 弹窗宽度补充验证:点击 `新增` -> `个人客户` 后,`客户号查询` 弹窗 DOM 样式为 `margin-top: 15vh; width: 80%;`
- 客户号空格补充验证:点击 `新增` -> `个人客户`,客户号输入 `1w0xc20xb7 ` 后查询,输入框显示为 `1w0xc20xb7`,列表正常返回客户映射,页面未出现 `400``Bad Request`
- 客户号到证件号码补充验证:点击 `新增` -> `个人客户`,客户号输入 `ABC123456789` 并选择返回行后,新增个人弹窗基础信息的 `证件号码` 自动填入 `123456789`
- 页面验证仅验证查询选择与自动回填链路,未提交新增表单,避免写入额外流程测试数据。
- 回归补充说明:
- `LoanPricingModelServiceTest` 已通过。
- 组合回归命令包含 `LoanPricingWorkflowServiceImplTest` 时,当前本机 Oracle JDK 21 环境下 Mockito inline/Byte Buddy self-attach 失败,属于既有测试环境限制,不是本次客户号映射功能断言失败。
- 进程清理:已关闭本次启动的后端和前端进程。
- 本次宽度与客户号空格补充验证结束后,`18080``63311` 无监听;`63310` 为验证前已存在的后端进程,未处理。

View File

@@ -1,27 +0,0 @@
# 历史贷款合同单选异常文本修复实施记录
## 问题
- 历史贷款合同选择弹窗的选择列出现 `{ "cus...` 这类异常文本。
- 原因是 `el-radio` 使用整行对象 `scope.row` 作为 `label`Element UI 会把对象值渲染到单选文案区域。
## 修改
- `HistoryContractSelector.vue` 将单选绑定值从整行对象调整为 `selectedContractKey`
- 新增 `contractRadioValue(row, index)` 生成稳定单选值,优先使用历史贷款合同号。
- 保留 `selectedContract` 单独保存选中行对象,提交时仍向父组件返回完整合同记录。
- 隐藏单选组件内部 label 文案,选择列只展示单选圆点,不展示对象文本。
- `business-type-history-rate.test.js` 增加断言,禁止再出现 `:label="scope.row"`
## 验证
- 已执行并通过:
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'`
- 已启动后端和前端后使用 in-app browser 真实页面验证:
- 打开 `http://localhost:8080/index`
- 新增个人客户,选择业务种类 `存量转贷`
- 历史贷款合同选择弹窗正常展示客户内码、历史贷款合同号、历史贷款利率等字段。
- 选择列文本为空,不再出现 `{ "cus...` 或行对象 JSON 文本。
- 验证结果:`hasObjectText=false`
- 验证结束后已关闭测试弹窗。
- 验证结束后已停止本次启动的前端和后端进程,并确认 `8080``63310` 端口无监听。

View File

@@ -1,17 +0,0 @@
# 实施记录 - 外部接口调用日志
## 日期
2026-04-30
## 修改内容
- 在根目录 `AGENTS.md` 的测试规范中新增外部接口调用日志要求:每次调用外部接口进行测试或联调时,必须在后端日志中完整输出请求 URL、请求参数和返回参数。
- 补齐客户映射接口、历史贷款合同接口、通用 `HttpUtils` 外部接口调用日志,输出请求 URL、请求参数和返回参数。
- 将外部接口日志调整为多行可读格式:请求 URL、请求参数、返回参数分段输出参数对象和返回对象使用 pretty JSON 展开。
- 调整单元测试,覆盖客户映射外呼日志、历史贷款合同外呼日志。
## 验证
- 已检查规则保存路径为项目根目录 `AGENTS.md`
- 已执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanRateHistoryServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,测试通过。

View File

@@ -1,34 +0,0 @@
# 上虞利率定价系统操作手册生成实施记录
## 基本信息
- 日期2026-05-09
- 任务:生成系统操作文档,覆盖主要业务流程、用户管理、部门管理,并在合适位置加入真实页面截图
- 产物:`doc/上虞利率定价系统操作手册-2026-05-09.docx`
## 实施内容
- 梳理前端路由、菜单 SQL、业务流程组件和系统管理页面确认操作手册覆盖范围。
- 临时启动前端开发服务,复用本机 63310 后端服务,通过真实浏览器获取页面截图。
- 截图覆盖登录页、流程列表、客户类型选择、客户号查询、新增个人定价流程、流程详情、用户管理、部门管理。
- 使用 `python-docx` 生成 Word 操作手册,内容包含:
- 文档说明与角色范围
- 登录与页面布局
- 利率定价主要业务流程
- 用户管理操作说明
- 部门管理操作说明
- 日常使用注意事项
## 验证情况
- 已确认前端服务可访问:`http://localhost:8080`
- 已通过 `/login/test` 获取测试登录令牌并进入真实页面截图。
- 已抽查关键截图显示正常,流程详情截图已改为从列表真实记录进入,避免使用无效流水号。
- 已生成 Word 文件:`doc/上虞利率定价系统操作手册-2026-05-09.docx`
- DOCX 渲染检查尝试使用文档技能提供的 `render_docx.py`,当前机器缺少 `soffice`,无法完成逐页 PNG 渲染;截图已内嵌在 Word 文档中。
- 已使用 macOS Quick Look 生成首屏缩略图进行抽查,封面、标题、表格和首张截图显示正常。
## 临时文件
- 截图目录、生成脚本、Playwright 临时依赖均位于 `output/` 下。
- 本次收尾时已清理 `output/`,避免误提交临时文件。

View File

@@ -1,33 +0,0 @@
# 2026-05-11 外部查询接口 GET 参数调用修复实施记录
## 实施内容
- 修复客户号查询客户内码的个人、企业两个外部接口调用方式。
- `LoanPricingCustomerMapService` 构建请求地址时先移除同名 `appCode``cust_id` 参数,再通过 GET query param 追加配置中的公共 `appCode` 和真实客户号。
- 修复历史贷款记录查询外部接口调用方式。
- `LoanRateHistoryService` 构建请求地址时先移除同名 `appCode``cust_isn` 参数,再通过 GET query param 追加配置中的公共 `appCode` 和真实客户内码。
- 调整 profile 外部地址配置。
- `application-pro.yml``application-dev.yml``application-uat.yml` 新增同一个 `loan-pricing-external.app-code` 配置项。
- 生产 profile 三条查询 URL 仅保留接口地址,不再在 URL 中写 `appCode` 或空业务参数。
- 补充服务层单元测试。
- 覆盖个人客户映射、企业客户映射、历史贷款记录三条接口最终均按 GET query param 生成公共 `appCode` 和各自业务参数。
- 测试文件位于 `*/src/test/`,按仓库 `.gitignore` 规则不纳入提交范围,仅用于本地验证。
## 涉及文件
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
- `ruoyi-admin/src/main/resources/application-pro.yml`
- `ruoyi-admin/src/main/resources/application-dev.yml`
- `ruoyi-admin/src/main/resources/application-uat.yml`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
## 验证记录
- 已执行:`mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanRateHistoryServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:通过,`Tests run: 9, Failures: 0, Errors: 0, Skipped: 0`
- 日志验证:
- 个人客户映射最终请求 URL 为 `http://mock/personal?appCode=abc&cust_id=P001`
- 企业客户映射最终请求 URL 为 `http://mock/corporate?appCode=abc&cust_id=C001`
- 历史贷款记录最终请求 URL 为 `http://mock/history?appCode=abc&cust_isn=81033011438`

View File

@@ -1,19 +0,0 @@
# 2026-05-11 上虞利率定价字段口径调整实施计划记录
## 修改内容
- 新增后端实施计划:`docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-backend-plan.md`
- 新增前端实施计划:`docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-frontend-plan.md`
- 后端计划覆盖创建 DTO、流程实体、模型入参、转换器、服务层校验、SQL schema 和后端测试。
- 前端计划覆盖个人/企业新增弹窗、业务种类选项、抵质押类型选项、`couponRate` 条件必填、静态断言、构建和 Playwright 真实页面验证。
## 范围说明
- 计划依据:`docs/superpowers/specs/2026-05-11-shangyu-pricing-field-adjustment-design.md`
- 对公 `businessType` 上传模型这一条已按用户确认从本次实施范围排除;计划只覆盖 `couponRate` 的模型入参新增。
- 本次仅产出实施计划,未进入业务代码实现。
## 待验证
- 计划需通过计划审查后再进入实施。
- 后续实现完成后需要补充 `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`,记录真实代码改动、测试命令和页面验证结果。

View File

@@ -1,76 +0,0 @@
# 上虞利率定价字段调整实施记录
## 基本信息
- 日期2026-05-11
- 范围:上虞利率定价个人/企业新增链路、服务端校验、模型入参、表结构脚本
- 目标:按已确认需求调整业务种类、抵质押类型、存单票面利率字段,以及对私新增入口字段剔除
## 修改内容
### 后端
- 个人新增 DTO
- 业务种类调整为 `新增/存量新增/存量转贷`
- 移除 `loanPurpose``bizProof` 新增入口字段。
- 新增 `couponRate`
- 企业新增 DTO
- 业务种类调整为 `新增/存量新增/存量转贷`
- 企业抵押类型调整为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- 企业质押类型调整为 `存单质押/股权质押/其他质押`
- 新增 `couponRate`
- 流程实体和模型入参:
- `LoanPricingWorkflow` 新增 `couponRate`
- `ModelInvokeDTO` 新增 `couponRate`,未增加 `businessType` 模型入参。
- 转换器:
- 个人/企业新增 DTO 均映射 `couponRate`
- 个人新增 DTO 不再映射 `loanPurpose``bizProof`
- 服务校验:
- 业务种类仅允许 `新增/存量新增/存量转贷`
-`存量转贷` 要求历史贷款合同。
- 抵押/质押时要求选择抵质押类型。
- 对私/对公按客户类型和担保方式校验各自抵质押类型。
- `质押 + 存单质押` 时要求填写 `couponRate`
- SQL
- 新增 `sql/add_coupon_rate_20260511.sql`
- 同步更新 `loan_pricing_workflow` 建表脚本中的 `coupon_rate` 字段。
### 前端
- 个人新增弹窗:
- 业务种类调整为 `新增/存量新增/存量转贷`
- 移除 `贷款用途``是否有经营佐证`
- 抵押类型调整为 `一线/一类/二类/三类`
- 质押类型调整为 `存单质押/其他质押`
- `质押 + 存单质押` 时显示并必填 `存单票面利率`
- 企业新增弹窗:
- 业务种类调整为 `新增/存量新增/存量转贷`
- 抵押类型调整为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- 质押类型调整为 `存单质押/股权质押/其他质押`
- `质押 + 存单质押` 时显示并必填 `存单票面利率`
- 共同逻辑:
-`存量转贷` 触发历史贷款合同查询。
- 非存单质押提交时清理 `couponRate`
## 验证结果
- 后端单元测试:
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果通过23 个测试全部成功。
- 前端静态断言:
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && npm --prefix ruoyi-ui run test:business-type-history-rate'`
- 结果:通过。
- 前端生产构建:
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
- 结果:构建通过,仅存在既有包体积 warning。
- 真实页面验证:
- 使用 Playwright 打开 `http://localhost:1024/index`
- 使用 `/login/test` 获取登录 token 后访问真实流程列表页面。
- 个人新增弹窗验证:已移除 `贷款用途/是否有经营佐证`;业务种类仅 `存量转贷` 触发历史利率逻辑;个人抵押/质押选项正确;`存单质押``couponRate` 显示并进入必填校验。
- 企业新增弹窗验证:抵押/质押选项正确;`存单质押``couponRate` 显示并进入必填校验;业务种类仅 `存量转贷` 触发历史利率逻辑。
- 验证后已关闭 Playwright 浏览器会话;本次未新启动前后端进程。
## 注意事项
- 控制台中的 `sockjs-node` 报错来自本地 dev-server HMR 连接内网地址失败,不影响本次页面功能验证。
- 表单校验 warning 来自验证时故意触发必填校验。

View File

@@ -1,20 +0,0 @@
# 登录页背景图替换实施记录
## 修改时间
2026-05-12
## 修改内容
- 将登录页背景资源 `ruoyi-ui/src/assets/images/login-background.jpg` 替换为上虞农商银行“心乐为新未来”宣传图。
- 保持登录页现有样式引用不变,继续由 `login.vue``.login` 背景图样式加载该资源。
## 涉及文件
- `ruoyi-ui/src/assets/images/login-background.jpg`
## 验证情况
- 已确认 `ruoyi-ui/src/assets/images/login-background.jpg` 与原始图片 SHA256 一致。
- 已使用 Node 14.21.3 执行 `npm --prefix ruoyi-ui run build:prod`,构建成功;仅存在项目原有包体积 warning。
- 已使用 browser-use 打开真实登录页 `http://localhost:9527/login`,确认新背景图已渲染,账号、密码和登录按钮显示正常。

View File

@@ -1,18 +0,0 @@
# 登录页标题文案移除实施记录
## 修改时间
2026-05-12
## 修改内容
- 移除登录框顶部标题展示,不再显示“上虞利率定价系统”。
## 涉及文件
- `ruoyi-ui/src/views/login.vue`
## 验证情况
- 已检查登录页模板,确认登录框内不再渲染标题节点。
- 已使用 browser-use 打开 `http://localhost:9527/login` 进行实际页面验证,确认页面跳转到登录页后不再出现“上虞利率定价系统”,账号和密码输入项仍正常显示。

View File

@@ -1,57 +0,0 @@
# 流程列表角色数据权限实施记录
## 修改日期
2026-05-12
## 需求范围
- 仅控制 `GET /loanPricing/workflow/list` 流程列表接口。
- 超级管理员 `user_id=1`、启用角色名为“管理员”或角色标识为 `headAdmin` 的用户可查看全部流程。
- 非管理员用户只能查看 `loan_pricing_workflow.create_by` 精确等于当前登录人 `昵称-柜员号` 的流程。
- 列表页“创建者”查询参数继续保留,但只按 `create_by` 中的柜员号部分进行模糊匹配。
## 修改内容
- `LoanPricingWorkflow` 增加非表字段 `dataScopeCreateBy`,专用于后端内部数据权限精确过滤。
- `LoanPricingWorkflowServiceImpl.selectLoanPricingPage` 增加流程列表数据权限裁剪:
- 管理员不写入 `dataScopeCreateBy`
- 非管理员写入 `dataScopeCreateBy = nickName + "-" + username`
- 前端传入 `createBy` 时仍保留原查询参数,但不能扩大非管理员可见范围。
- `LoanPricingWorkflowMapper.xml` 增加 `lpw.create_by = #{query.dataScopeCreateBy}` 精确权限条件。
- `LoanPricingWorkflowMapper.xml``createBy` 查询调整为 `SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE ...`,即只按柜员号模糊匹配。
- 补充 `LoanPricingWorkflowServiceImplTest``LoanPricingWorkflowMapperXmlTest`,覆盖管理员、业务管理员、客户经理、越权创建者查询参数和 XML 条件。
## 验证记录
- 单元测试通过:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingWorkflowMapperXmlTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 后端打包通过:
```bash
mvn -pl ruoyi-admin -am clean package -DskipTests
```
- API 验证通过:
- `admin/admin123` 查询流程列表返回全量数据,包含测试行和 `若依-admin` 历史行。
- `8929999/123456` 业务管理员查询流程列表返回全量数据。
- `8920001/123456` 客户经理查询流程列表只返回本人创建的测试行。
- 客户经理按 `createBy=8920001` 查询可返回本人测试行。
- 客户经理按昵称 `createBy=测试客户经理` 查询返回 0 条。
- 客户经理按其他柜员号 `createBy=admin` 查询返回 0 条。
- browser-use 真实页面验证通过:
- 管理员登录真实流程列表页,页面显示 `共 40 条`,可见本人测试行和 `若依-admin` 历史行。
- 客户经理登录真实流程列表页,页面只显示本人创建的测试行。
- 客户经理在“创建者”输入 `admin` 后页面显示暂无数据。
- 客户经理在“创建者”输入 `8920001` 后页面重新显示本人测试行。
## 验证数据与清理
- 因当前开发库创建流程接口依赖的模型输出表缺少 `coupon_rate` 字段,无法通过页面新增生成验证流程。
- 本次验证使用一条临时 SQL 测试数据:`serial_num = ROLE_SCOPE_20260512_001``create_by = 测试客户经理-8920001`
- 验证结束后已删除该临时数据,回查剩余数量为 0。

View File

@@ -1,32 +0,0 @@
# 贷款定价单脚本部署改造实施记录
## 保存路径检查
- 参考脚本:`/Users/wkc/Desktop/ccdi/ccdi/deploy/ccdi_function.sh`
- 新增脚本保存路径:`bin/prod/loan_pricing_function.sh`
- 实施记录保存路径:`doc/implementation-report-2026-05-13-loan-pricing-function-script.md`
## 修改内容
- 新增 `loan_pricing_function.sh`,按 `ccdi_function.sh``deploy``restart``stop` 三命令结构改造为贷款定价可用脚本。
- 按贷款定价现有生产目录约定调整:
- 后端 Jar`backend/ruoyi-admin.jar`
- 前端静态目录:`frontend/dist/`
- 日志目录:`logs/backend-console.log`
- PID 文件:`run/backend.pid`
- 临时目录:`tmp/loan-pricing-function/`
- 备份目录:`backup/YYYYMMDDHHMMSS/`
- 按贷款定价运行参数调整:
- Java 默认目录:`/home/webapp/env/java`
- 后端进程标记:`-Dloan.pricing.home=<脚本目录>`
- Spring Profile`uat`
- 后端端口:`63310`
- 上线包结构固定为根层包含:
- `ruoyi-admin.jar`
- `dist.zip`
- 前端 `dist.zip` 解压后必须包含 `dist/index.html`,部署时写入 `frontend/dist/`
- 默认保持参考脚本的启动后持续输出日志行为,并支持 `FOLLOW_LOGS=0` 供自动化验证跳过持续日志输出。
## 验证结果
- 已执行 `sh -n bin/prod/loan_pricing_function.sh`,语法校验通过。
- 已在临时目录构造 `backend/``frontend/dist/`、根层发布 zip 和假 Java 进程,验证 `deploy` 可完成备份、替换、启动和日志落盘。
- 已验证 `stop` 可停止脚本标记的后端进程并清理 PID 文件。
- 验证过程中产生的临时测试目录已删除,未新增仓库内测试文件。

View File

@@ -1,22 +0,0 @@
# 浏览器页签标题调整实施记录
## 修改时间
2026-05-15
## 修改内容
- 将前端浏览器页签标题从“上虞利率定价系统”调整为“贷款利率定价系统”。
- 同步更新开发、测试、生产环境的 `VUE_APP_TITLE` 配置,确保本地运行和打包产物标题一致。
## 涉及文件
- `ruoyi-ui/.env.development`
- `ruoyi-ui/.env.staging`
- `ruoyi-ui/.env.production`
## 验证情况
- 已通过源码检索确认 `ruoyi-ui` 中页面标题配置已统一为“贷款利率定价系统”。
- 已使用 browser-use 打开 `http://localhost:9527/login` 进行真实页面验证,浏览器标签页标题与 `document.title` 均为“贷款利率定价系统”。
- 验证时仅启动前端服务;因本地后端 `localhost:63310` 未启动,验证码接口代理返回 `ECONNREFUSED`,不影响本次页签标题验证。

View File

@@ -1,32 +0,0 @@
# 利率前端两位小数展示实施记录
## 修改时间
- 2026-05-15
## 修改范围
- `ruoyi-ui/src/utils/rate.js`
- `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
## 修改内容
- 新增 `formatRate` 前端格式化方法,统一将可解析的利率数值展示为小数点后两位。
- 利率定价流程列表的测算利率、执行利率改为通过 `formatRate` 展示。
- 个人/企业流程详情的基准利率、最终测算利率、执行利率初始展示值、历史贷款利率改为两位小数展示。
- 模型输出中的基准利率、测算利率、历史利率、产品最低利率下限、平滑幅度、参考利率、最终测算利率、派生率改为两位小数展示。
- 历史贷款合同选择弹窗和新增流程弹窗中的历史贷款利率展示改为两位小数。
## 影响说明
- 本次仅调整前端展示格式,不改后端接口、数据库字段和模型调用逻辑。
- 历史贷款利率在新增弹窗中仅格式化展示,表单内部仍保留接口返回的原始值。
## 验证
- 已执行 `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`,构建通过,仅存在资源体积 warning。
- 已启动后端 `http://localhost:63310` 和前端 `http://localhost:9527/`,通过 browser-use 打开真实页面验证。
- 流程列表接口原始返回中存在 `calculateRate = 3.932` 的数据,流程列表页面 `测算利率(%)` 展示为 `3.93`
- 流程列表页面 `执行利率(%)` 展示为 `3.88``6.18``-`,已确认非空利率均为小数点后两位。
- 流程详情页模型输出中,`finalCalculateRate = 3.732` 对应页面展示为 `3.73`,其他利率字段也按两位小数展示。

View File

@@ -1,74 +0,0 @@
# 流程列表支行管理员数据权限实施记录
## 修改日期
2026-05-18
## 需求范围
-`loan_pricing_workflow` 新增 `dept_id`,保存新创建流程的创建人机构号。
- `GET /loanPricing/workflow/list` 增加支行管理员数据权限:
- 超级管理员、角色名“管理员”或角色标识 `headAdmin` 查看全部流程。
- 角色名“支行管理员”或角色标识 `branchAdmin` 查看本人机构及下级机构创建的流程。
- 其他用户继续只查看本人 `create_by` 精确匹配的流程。
- 不回填历史流程数据,历史 `dept_id` 为空的数据不纳入支行管理员机构权限。
- 本次无前端代码改动。
## 修改内容
- 新增 `sql/add_workflow_dept_id_20260518.sql`,为流程表增加 `dept_id` 字段和 `idx_dept_id` 索引。
- 同步更新流程表建表脚本中的 `dept_id` 字段和索引定义。
- `LoanPricingWorkflow` 增加表字段 `deptId` 和列表内部权限字段 `dataScopeDeptId`
- `LoanPricingWorkflowServiceImpl.createLoanPricing` 在插入前写入当前登录人的 `deptId`
- `LoanPricingWorkflowServiceImpl.selectLoanPricingPage` 增加支行管理员分支:
- `headAdmin` 不加权限过滤。
- `branchAdmin` 写入当前登录人的 `dataScopeDeptId`
- 客户经理继续写入 `dataScopeCreateBy`
- `LoanPricingWorkflowMapper.xml` 增加基于 `lpw.dept_id``sys_dept.ancestors` 的本机构及下级机构过滤。
- 创建者查询参数仍按 `SUBSTRING_INDEX(lpw.create_by, '-', -1)` 只模糊匹配柜员号,并与数据权限条件取交集。
## 验证记录
- 单元测试通过:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingWorkflowMapperXmlTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 后端打包通过:
```bash
mvn -pl ruoyi-admin -am clean package -DskipTests
```
- 已在开发库执行:
```bash
mysql ... loan-pricing < sql/add_workflow_dept_id_20260518.sql
```
- 数据库回查确认 `loan_pricing_workflow.dept_id``idx_dept_id` 已存在。
- API 验证通过:
- `8929999/headAdmin` 查询临时流程返回 6 条,包含全部测试数据。
- `8920100/branchAdmin` 查询临时流程返回 4 条,仅包含本机构、本人和下级机构数据。
- `8920001/客户经理` 查询临时流程返回 1 条,仅包含本人创建数据。
- `8920100/branchAdmin` 使用其他支行创建者 `8920201` 作为查询条件时返回 0 条,创建者查询参数不能扩大数据权限。
- 真实创建接口验证通过:
- `8920100/branchAdmin` 调用个人流程创建接口后,新流程落库 `dept_id=101``create_by=测试支行管理员-8920100`
- browser-use 真实页面验证通过:
- 支行管理员登录真实流程列表页,页面显示 `共 4 条`
- 页面只显示 `BRANCH_SCOPE_20260518_CREATE``BRANCH_SCOPE_20260518_SELF``BRANCH_SCOPE_20260518_SAME``BRANCH_SCOPE_20260518_CHILD`
- 页面未显示其他支行 `BRANCH_SCOPE_20260518_OTHER` 和普通客户经理本人 `BRANCH_SCOPE_20260518_MANAGER`
## 测试数据保留
- 按复测要求,本次验证保留测试用户和测试流程数据,不做清理。
- 保留临时用户:
- `8920100`,昵称 `测试支行管理员`,角色 `branchAdmin`,机构 `101`
- 保留流程数据:
- `BRANCH_SCOPE_20260518_SELF`,机构 `101`,创建者 `测试支行管理员-8920100`
- `BRANCH_SCOPE_20260518_SAME`,机构 `101`,创建者 `同支行客户经理-8920101`
- `BRANCH_SCOPE_20260518_CHILD`,机构 `103`,创建者 `下级客户经理-8920103`
- `BRANCH_SCOPE_20260518_OTHER`,机构 `102`,创建者 `其他支行客户经理-8920201`
- `BRANCH_SCOPE_20260518_MANAGER`,机构 `100`,创建者 `测试客户经理-8920001`
- `BRANCH_SCOPE_20260518_CREATE`,机构 `101`,创建者 `测试支行管理员-8920100`,由真实新增接口创建。

View File

@@ -1,19 +0,0 @@
# 流程列表客户内码字段实施记录
## 修改内容
- 后端流程列表查询 `selectWorkflowPageWithRates` 增加 `lpw.cust_isn AS custIsn`,确保列表接口返回客户内码。
- 流程列表返回对象 `LoanPricingWorkflowListVO` 增加 `custIsn` 字段,承接接口返回值。
- 前端流程列表页新增“客户内码”表格列,字段绑定 `custIsn`,支持超长内容 tooltip 展示。
## 影响范围
- 仅影响利率定价流程列表 `/loanPricing/workflow/list` 的返回字段和页面展示。
- 不修改新增流程、详情页、筛选条件和数据库结构。
## 验证记录
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowListVOTest -Dsurefire.failIfNoSpecifiedTests=false test` 通过。
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && node tests/customer-map-selection.test.js` 通过。
- 使用真实后端接口 `/loanPricing/workflow/list?pageNum=1&pageSize=3` 验证返回 `custIsn`,前三条返回值为 `81000529053``81000791269``81000769824`
- 使用 browser-use 打开真实流程列表页 `http://localhost:1024/index`,确认表头包含“客户内码”,前三条列表行客户内码展示为 `81000529053``81000791269``81000769824`

View File

@@ -1,22 +0,0 @@
# 流程列表最终测算利率展示实施记录
## 修改内容
- 流程列表联表查询中,企业客户分支由 `model_corp_output_fields.calculate_rate` 改为 `model_corp_output_fields.final_calculate_rate`
- 个人客户分支保持读取 `model_retail_output_fields.final_calculate_rate`
- 前端流程列表列名由“测算利率(%)”调整为“最终测算利率(%)”,继续复用列表接口字段 `calculateRate` 展示,避免扩大接口字段变更范围。
## 影响范围
- 仅影响利率定价流程列表 `/loanPricing/workflow/list` 的利率来源和列名展示。
- 不修改详情页、新增流程、执行利率和数据库结构。
## 验证记录
- 已更新 `LoanPricingWorkflowMapperXmlTest`,约束个人、企业流程列表均取 `final_calculate_rate`
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowMapperXmlTest,LoanPricingWorkflowListVOTest -Dsurefire.failIfNoSpecifiedTests=false test` 通过。
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && node tests/customer-map-selection.test.js` 通过。
- 使用 Node 静态断言确认流程列表列名为“最终测算利率(%)”,并且不再展示旧列名“测算利率(%)”。
- `mvn -pl ruoyi-admin -am clean package -DskipTests` 打包通过,并重启本地 63310 后端。
- 使用真实后端接口 `/loanPricing/workflow/list?pageNum=1&pageSize=3` 验证返回值,前三条 `calculateRate` 分别为 `3.732``6.05``3.732`
- 使用 browser-use 打开真实流程列表页 `http://localhost:1024/index`,确认列名为“最终测算利率(%)”,前三条页面展示值为 `3.73``6.05``3.73`,旧列名未出现。

View File

@@ -1,28 +0,0 @@
# 流程列表列宽连续展示实施记录
## 修改时间
- 2026-05-18
## 修改范围
- `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
## 修改内容
- 将流程列表表格字段改为最小列宽布局,宽屏下自动铺满容器,窄屏下通过表格横向滚动查看。
- 加宽“业务方流水号”“客户内码”“客户名称”“创建者”等关键字段最小列宽,避免当前业务数据被省略或换行截断。
- 将关键字段列的 `show-overflow-tooltip` 省略展示方式改为最小列宽完整展示,并覆盖 Element UI 默认省略号样式。
- 将“操作”列固定在右侧,并通过最小列宽布局让主体列自动铺满,避免固定列与主体列之间出现中间空白断开。
## 影响说明
- 本次仅调整流程列表前端表格展示,不修改接口、后端查询逻辑、数据库结构和权限逻辑。
- 字段内容较长时优先按最小列宽完整展示,页面可通过横向滚动查看完整列表。
## 验证
- 已执行 `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`,前端生产构建通过,仅存在既有资源体积 warning。
- 已复用本机前端 `http://localhost:1024/` 与后端 `http://localhost:63310` 进行真实页面验证。
- 已通过 browser-use 打开真实流程列表页 `http://localhost:1024/loanPricing/workflow` 验证页面标题为“贷款利率定价系统”,页面非空且无浏览器 error/warn 日志。
- 在真实页面验证流程列表:
-`1707x517` 视口下,表格主体 `scrollWidth=2020``clientWidth=1607`,确认窄屏仍可横向滚动。
-`2167x542` 视口下,表格主体 `scrollWidth=2067``clientWidth=2067`,确认宽屏下表格自动铺满容器。
- “操作”列固定在表格右侧,固定列宽度约 `112px`
- 固定“操作”列左侧间距 `gapBeforeFixed=0`,右侧尾部间距 `trailingGapAfterFixed=0`,未出现中间或尾部大块空白。
- “业务方流水号”“客户内码”“客户名称”“创建者”单元格样式为 `text-overflow: clip``overflow: hidden``white-space: nowrap`,当前列表关键字段 `BRANCH_SCOPE_20260518_MANAGER``测试支行管理员-8920100` 等完整展示且未使用省略号截断。

View File

@@ -1,50 +0,0 @@
# sys_user_role 角色关系 SQL 生成记录
## 修改日期
2026-05-20
## 需求范围
- 根据 `/Users/wkc/Downloads/892.xlsx``Sheet1` 生成 `sys_user_role` 插入语句。
- Excel 字段使用规则:`柜员号` 匹配 `sys_user.user_name` 后取 `sys_user.user_id``类型` 匹配 `sys_role.role_name` 后取 `sys_role.role_id`
- 未将 Excel 柜员号直接写入 `sys_user_role.user_id`
- 本次只生成 SQL 文件,未执行写入数据库。
## 输出文件
- `sql/insert_sys_user_role_892_20260519.sql`
## 数据统计
- Excel 有效数据行257 行。
- 管理员4 行。
- 支行管理员37 行。
- 客户经理213 行。
- Excel 中 3 个重复柜员号同时出现客户经理和支行管理员,本次按支行管理员优先处理,不插入客户经理角色。
## 重复柜员号
- `8922557`: 裘朝山 / 892220 / 客户经理 / Excel 第 26 行、裘朝山 / 892220 / 支行管理员 / Excel 第 117 行
- `8922667`: 徐华源 / 892080 / 客户经理 / Excel 第 66 行、徐华源 / 892080 / 支行管理员 / Excel 第 121 行
- `8923504`: 陈俊杰 / 892170 / 客户经理 / Excel 第 28 行、陈俊杰 / 892170 / 支行管理员 / Excel 第 118 行
- 处理规则:以上重复柜员号只保留在支行管理员插入语句中。
## 数据库映射确认
- 已查询开发库 `sys_role`,当前有效角色为:
- `管理员` -> `role_id=100`, `role_key=headAdmin`
- `客户经理` -> `role_id=101`, `role_key=common`
- `支行管理员` -> `role_id=102`, `role_key=branchAdmin`
- SQL 文件不硬编码 `role_id`,执行时按 `sys_role.role_name` 动态匹配,避免不同环境角色自增 ID 不一致。
- SQL 使用 `INSERT IGNORE`,重复执行不会重复写入相同 `(user_id, role_id)`
- SQL 不创建临时表,已按 `管理员``支行管理员``客户经理` 拆成三条独立插入语句,便于分角色执行和核对影响范围。
## 验证记录
- 已读取 Excel 并校验 `类型` 只包含 `管理员``支行管理员``客户经理`
- 已确认 `sys_user_role` 表结构为 `user_id``role_id` 联合主键。
- 已对开发库做不写入目标表的干跑校验257 行角色均可匹配,当前库 Excel 柜员号匹配到的有效用户数为 0。
- 批量执行该 SQL 前需先导入 Excel 对应用户,否则三条插入语句因匹配不到 `sys_user.user_name` 不会写入关系。
- 已按要求移除临时表建表、临时表插入和临时表删除语句。
- 已在事务内执行 SQL 脚本并 `ROLLBACK`,确认三条插入语句语法通过且未落库。

View File

@@ -1,38 +0,0 @@
# 对公余值覆盖字段实施记录
## 背景
对公新增流程需要在“贷款信息”分组补充“余值覆盖”开关,字段名为 `resCover`,提交到后端和模型调用时使用 `0/1` 值。
## 修改内容
1. 前端企业新增弹窗 `CorporateCreateDialog.vue`
- 在“贷款信息”分组新增“余值覆盖”开关。
- 表单默认值为 `false`
- 提交时转换为 `resCover: '1'``resCover: '0'`
2. 后端字段链路
- `CorporateLoanPricingCreateDTO` 增加 `resCover`
- `LoanPricingWorkflow` 增加 `resCover`,对应数据库字段 `res_cover`
- `LoanPricingConverter` 将 DTO 字段写入流程实体。
- `ModelInvokeDTO` 增加 `resCover`
- `LoanPricingModelService` 在企业模型调用前将 `resCover` 归一化为 `0/1`
3. 页面展示
- 企业流程详情页展示“余值覆盖”。
4. SQL
- 新增 `sql/add_res_cover_20260522.sql`
- 同步更新 `sql/loan_pricing_workflow.sql``sql/loan_pricing_schema_20260328.sql``sql/loan_pricing_prod_init_20260331.sql`
## 验证结果
- 已执行前端字段静态断言:`npm run test:corporate-create-input-params`,通过。
- 已执行前端详情字段静态断言:`npm run test:corporate-display-fields`,通过。
- 已执行后端单测:`mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`,通过。
- 已执行后端打包:`mvn -pl ruoyi-admin -am -DskipTests package`,通过。
- 已执行开发库 SQL`sql/add_res_cover_20260522.sql`,回查 `loan_pricing_workflow.res_cover``varchar(10)`
- 已启动真实前端页面,通过浏览器打开企业新增弹窗,确认“贷款信息”分组中展示“余值覆盖”开关,且位置在“企业标识”分组之前。
- 已用临时后端端口发起企业创建接口,响应中 `resCover``1`,后端外部模型调用日志确认请求参数包含 `"resCover":"1"`
- 接口验证产生的测试流程数据已清理,回查 `cust_isn = 'RES_COVER_TEST_20260522'` 的流程记录数为 `0`
- 测试时启动的前端和临时后端进程已关闭。

View File

@@ -1,14 +0,0 @@
# 用户管理新增弹窗宽度前端实施记录
## 修改内容
- 将用户管理“添加或修改用户配置对话框”的宽度从 `600px` 调整为页面宽度的 `80%`
- 修改范围仅限前端页面 `ruoyi-ui/src/views/system/user/index.vue`,不涉及后端接口、数据结构或权限逻辑。
## 验证记录
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && npm run build:prod`
- 结果:通过;仅保留项目既有的 webpack 体积提示。
- 真实页面验证:
- 前端地址:`http://localhost:1024/system/user`
- 登录用户:`admin`
- 点击用户管理页面“新增”后打开 `添加用户` 弹窗。
- 浏览器视口宽度为 `1067px`,弹窗实际宽度为 `853.33px`,宽度比例为 `0.7998`,符合页面宽度 `80%` 的要求。

View File

@@ -1,26 +0,0 @@
# 流程列表编辑功能后端实施记录
## 修改内容
- 在利率定价流程接口新增编辑查询接口:`GET /loanPricing/workflow/{serialNum}/edit`
- 新增个人流程编辑接口:`PUT /loanPricing/workflow/{serialNum}/personal`
- 新增企业流程编辑接口:`PUT /loanPricing/workflow/{serialNum}/corporate`
- 编辑接口按当前登录用户的 `昵称-柜员号` 校验创建者,只允许流程创建者编辑。
- 编辑时保持原业务方流水号、客户类型、创建者、创建时间和创建人部门,只覆盖表单字段。
- 编辑保存后重新调用模型服务;已有模型输出记录时覆盖原模型输出,并保持流程关联。
## 验证记录
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:通过。
- 覆盖:创建者编辑、非创建者拒绝、客户类型不匹配拒绝、编辑数据解密返回、重新测算覆盖模型结果。
- `mvn -pl ruoyi-loan-pricing -am test`
- 结果:通过。
- 覆盖:利率定价模块现有单测和本次新增单测。
- `mvn -pl ruoyi-admin -am -DskipTests package`
- 结果:通过,重新打包 `ruoyi-admin/target/ruoyi-admin.jar` 用于真实接口验证。
- 真实接口验证:
- 创建临时个人流程 `20260525110739953`,创建者为 `若依-admin`
- 创建者调用 `GET /loanPricing/workflow/20260525110739953/edit` 成功返回原始客户名称 `编辑测试客户` 和原始证件号 `330103199901019999`
- 创建者通过页面编辑提交后,编辑详情接口返回 `applyAmt=120000`,并保持原 `serialNum``custType``createBy``createTime``deptId`
- 非创建者 `8929999` 调用编辑详情接口返回 `只有创建者可以编辑该流程`
- 非创建者 `8929999` 调用个人更新接口返回 `只有创建者可以编辑该流程`
- 验证完成后已按精确流水号删除临时流程和关联 `model_retail_output_fields` 记录,清理后计数均为 0。

View File

@@ -1,24 +0,0 @@
# 流程列表编辑功能前端实施记录
## 修改内容
- 在流程列表操作列新增“编辑”按钮。
- 编辑按钮只在 `row.createBy` 等于当前登录用户 `nickName-name` 时展示。
- 点击编辑后直接查询流程编辑数据,并按客户类型打开个人或企业弹窗,不再进入客户类型选择和客户号选择流程。
- 个人和企业新增弹窗复用为新增/编辑双模式:
- 新增模式继续调用原新增接口。
- 编辑模式显示编辑标题、回显原始数据,并调用对应更新接口。
- 编辑回显时跳过担保方式和抵质押类型监听中的清空逻辑,避免抵质押字段被初始化过程误清除。
## 验证记录
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && node tests/workflow-index-refresh.test.js && node tests/personal-create-input-params.test.js && node tests/corporate-create-input-params.test.js`
- 结果:通过。
- 覆盖:操作列编辑按钮、创建者展示控制、编辑数据查询、个人/企业弹窗编辑模式和更新接口调用。
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && npm run build:prod`
- 结果:通过;仅保留项目既有的 webpack 体积提示。
- 真实页面验证:
- 前端地址:`http://localhost:1024/loanPricing/workflow`
- 创建者 `admin` 登录后,临时流水 `20260525110739953` 操作列显示“查看”和“编辑”。
- 点击“编辑”直接打开 `编辑个人利率定价流程` 弹窗,回显客户内码 `EDITTEST20260525`、客户名称 `编辑测试客户`、证件号 `330103199901019999`、担保方式 `信用`、申请金额 `100000`、借款期限 `3`、业务种类 `新增`
- 将申请金额改为 `120000` 后提交,页面提示“编辑成功”,列表刷新后该流水申请金额变为 `120000`
- 非创建者 `8929999` 登录后,同一流水仍可查看,但操作列只显示“查看”,不显示“编辑”。
- 浏览器控制台无相关 error仅出现登录和表单过程中的 `async-validator` 校验 warning。

View File

@@ -1,28 +0,0 @@
# 取消贷款定价页面脱敏展示后端实施记录
## 需求范围
- 取消贷款定价流程页面展示层的客户名称、证件号码脱敏效果。
- 列表、详情、模型输出基本信息返回完整展示值。
- 保留流程表客户名称、证件号码存储加密,以及创建、编辑、模型调用链路中的加解密逻辑。
## 实施内容
- 调整 `LoanPricingWorkflowServiceImpl` 分页列表返回逻辑,客户名称解密后直接返回。
- 调整 `LoanPricingWorkflowServiceImpl` 详情返回逻辑,流程主信息中的客户名称、证件号码解密后直接返回。
- 取消个人、企业模型输出基本信息中的客户名称、证件号码返回前脱敏处理。
- 删除不再使用的贷款定价专用展示脱敏服务及对应单元测试。
- 更新工作流服务单元测试,验证列表、详情、个人模型输出、企业模型输出均返回完整展示值。
## 未改动内容
- 未修改数据库表结构和数据迁移脚本。
- 未修改 `SensitiveFieldCryptoService` 存储加解密逻辑。
- 未修改登录、密码传输加密逻辑。
- 未修改通用 `@Sensitive` 脱敏机制。
- 未修改前端业务代码。
## 验证计划
- 执行 `LoanPricingWorkflowServiceImplTest`,确认返回展示值与原加解密边界符合预期。
- 启动真实前后端页面,进入贷款定价流程列表与详情页,确认页面展示完整客户名称和证件号码。

View File

@@ -1,18 +0,0 @@
--------客户号与客户内码映射接口
----对私
http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ind_idmapno?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_id=
---案例1对1 101330419198206033217 101330419197511072429
---案例1对n 101330682197911073012 10133062319810217642X
----对公
http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ent_idmapno?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_id=
---案例1对1 20291330600146150140Y 20291330600146150466L
---案例1对n 202913306047458026221 2029133060475302009XU
-----历史合同查询接口
http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_isn=
----1对1 81033011438 81035265634
----1对n 82002469287 82000882275

View File

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

View File

@@ -1,742 +0,0 @@
# Business Type History Rate Backend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not enable subagents or `using-superpowers` during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add backend support for business type persistence, historical loan-rate lookup, and historical loan-rate model input for both personal and corporate workflow creation.
**Architecture:** Keep the existing workflow creation API shape and extend the request DTOs, entity, converter, service validation, SQL schema, and model DTO. Add one backend proxy service/controller endpoint for historical contracts, following the existing customer-map proxy pattern, plus a mock endpoint for local development and browser-use testing.
**Tech Stack:** Spring Boot, RuoYi, MyBatis Plus, Lombok, JUnit 5, Mockito, Spring `MockRestServiceServer`, MySQL schema SQL.
---
## File Structure
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
- Add `businessType` and `loanRateHistory`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java`
- Add the same fields.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Add persisted fields `businessType` and `loanRateHistory`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- Add `loanRateHistory` only.
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVO.java`
- Represent external historical contract rows with underscore JSON names.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- Map `businessType` and `loanRateHistory` for personal and corporate create DTOs.
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
- Proxy external historical contract lookup.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- Add `GET /loanPricing/workflow/history-contract`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Add service-layer cross-field validation before insert/model invocation.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
- Add mock historical contract endpoint with fixed normal, empty, and empty-rate scenarios.
- Add fixed customer-map mock customer IDs that return `cust_isn=EMPTY_HISTORY` and `cust_isn=EMPTY_RATE` for browser-use testing.
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- Add local mock `loan-rate-history.url`.
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
- Add local mock `loan-rate-history.url`.
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
- Add real external `loan-rate-history.url` without `cust_isn=`.
- Create: `sql/add_business_type_history_rate_20260429.sql`
- Migration for existing databases.
- Modify: `sql/loan_pricing_workflow.sql`
- Update standalone workflow schema.
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Update bundled schema.
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- Update production init schema.
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
## Task 1: Add Backend Field Contracts
**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/domain/dto/CorporateLoanPricingCreateDTO.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVO.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- [ ] **Step 1: Write failing field tests**
Add to `LoanPricingModelServicePersonalParamsTest`:
```java
@Test
void shouldContainBusinessTypeAndLoanRateHistoryInCreateDtosAndWorkflow() throws NoSuchFieldException {
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("businessType"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanRateHistory"));
assertNotNull(CorporateLoanPricingCreateDTO.class.getDeclaredField("businessType"));
assertNotNull(CorporateLoanPricingCreateDTO.class.getDeclaredField("loanRateHistory"));
assertNotNull(LoanPricingWorkflow.class.getDeclaredField("businessType"));
assertNotNull(LoanPricingWorkflow.class.getDeclaredField("loanRateHistory"));
}
@Test
void shouldContainLoanRateHistoryButNotBusinessTypeInModelInvokeDto() throws NoSuchFieldException {
assertNotNull(ModelInvokeDTO.class.getDeclaredField("loanRateHistory"));
assertThrows(NoSuchFieldException.class, () -> ModelInvokeDTO.class.getDeclaredField("businessType"));
}
```
Create `HistoryLoanContractVOTest`:
```java
package com.ruoyi.loanpricing.domain.vo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
class HistoryLoanContractVOTest {
@Test
void shouldDeserializeUnderscoreFields() throws Exception {
String json = "{\"cust_isn\":\"81033011438\",\"loan_contract_history\":\"HT001\",\"guar_type_history\":\"信用\",\"product_code_history\":\"P001\",\"loan_rate_history\":\"3.65\",\"loan_amount_history\":\"100000\",\"loan_sign_date_history\":\"2025-01-01\"}";
HistoryLoanContractVO vo = new ObjectMapper().readValue(json, HistoryLoanContractVO.class);
assertNotNull(vo.getCustIsn());
assertNotNull(vo.getLoanContractHistory());
assertNotNull(vo.getLoanRateHistory());
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest test
```
Expected: FAIL because the fields and VO do not exist.
- [ ] **Step 3: Add DTO/entity/model fields**
Add to both create DTOs:
```java
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新客", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新客|存量新增|存量转贷)$", message = "业务种类必须是:新客、存量新增、存量转贷之一")
private String businessType;
@Schema(description = "历史贷款利率", example = "3.65")
private String loanRateHistory;
```
Add to `LoanPricingWorkflow`:
```java
/** 业务种类: 新客/存量新增/存量转贷 */
private String businessType;
/** 历史贷款利率 */
private String loanRateHistory;
```
Add only this field to `ModelInvokeDTO`:
```java
/**
* 历史贷款利率
*/
private String loanRateHistory;
```
- [ ] **Step 4: Create `HistoryLoanContractVO`**
```java
package com.ruoyi.loanpricing.domain.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import lombok.Data;
@Data
public class HistoryLoanContractVO implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("cust_isn")
private String custIsn;
@JsonProperty("loan_contract_history")
private String loanContractHistory;
@JsonProperty("guar_type_history")
private String guarTypeHistory;
@JsonProperty("product_code_history")
private String productCodeHistory;
@JsonProperty("loan_rate_history")
private String loanRateHistory;
@JsonProperty("loan_amount_history")
private String loanAmountHistory;
@JsonProperty("loan_sign_date_history")
private String loanSignDateHistory;
}
```
- [ ] **Step 5: Run field tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest test
```
Expected: PASS.
- [ ] **Step 6: Commit**
```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/domain/dto/CorporateLoanPricingCreateDTO.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVO.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java
git commit -m "新增业务种类与历史利率后端字段"
```
## Task 2: Map and Validate Workflow Creation
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: Write failing converter tests**
Add:
```java
@Test
void shouldMapBusinessTypeAndLoanRateHistoryFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setGuarType("信用");
dto.setApplyAmt("100000");
dto.setBusinessType("存量转贷");
dto.setLoanRateHistory("3.65");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("存量转贷", workflow.getBusinessType());
assertEquals("3.65", workflow.getLoanRateHistory());
}
```
Add a similar corporate DTO test in `LoanPricingModelServiceTest` or a new converter-focused test.
- [ ] **Step 2: Write failing service validation tests**
Add to `LoanPricingWorkflowServiceImplTest`:
```java
@Test
void shouldRejectMissingBusinessTypeBeforeInsert() {
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType(null);
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("业务种类不能为空", ex.getMessage());
verify(loanPricingWorkflowMapper, never()).insert(any());
}
@Test
void shouldRejectInvalidBusinessTypeBeforeInsert() {
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType("其他");
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("业务种类必须是:新客、存量新增、存量转贷之一", ex.getMessage());
}
@Test
void shouldRejectStockTransferWithoutLoanRateHistory() {
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory(" ");
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("请选择历史贷款合同", ex.getMessage());
}
```
Add helper:
```java
private LoanPricingWorkflow validWorkflow() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustIsn("81033011438");
workflow.setCustType("个人");
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setBusinessType("新客");
return workflow;
}
```
- [ ] **Step 3: Run tests to verify they fail**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test
```
Expected: FAIL because mapping and validation do not exist.
- [ ] **Step 4: Implement converter mapping**
Add to both `toEntity(...)` methods:
```java
entity.setBusinessType(dto.getBusinessType());
entity.setLoanRateHistory(dto.getLoanRateHistory());
```
- [ ] **Step 5: Implement service validation**
In `LoanPricingWorkflowServiceImpl`, before defaults/encryption/insert in `createLoanPricing(...)`, call:
```java
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
```
Add:
```java
private void validateBusinessTypeAndHistoryRate(LoanPricingWorkflow workflow) {
if (!StringUtils.hasText(workflow.getBusinessType())) {
throw new ServiceException("业务种类不能为空");
}
if (!"新客".equals(workflow.getBusinessType())
&& !"存量新增".equals(workflow.getBusinessType())
&& !"存量转贷".equals(workflow.getBusinessType())) {
throw new ServiceException("业务种类必须是:新客、存量新增、存量转贷之一");
}
if ("存量转贷".equals(workflow.getBusinessType())
&& !StringUtils.hasText(workflow.getLoanRateHistory())) {
throw new ServiceException("请选择历史贷款合同");
}
}
```
Import `com.ruoyi.common.exception.ServiceException`.
- [ ] **Step 6: Run validation tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test
```
Expected: PASS.
- [ ] **Step 7: Commit**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "校验并映射业务种类历史利率"
```
## Task 3: Add Historical Contract Proxy and Mock
**Files:**
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
- [ ] **Step 1: Write failing service tests**
Create tests following `LoanPricingCustomerMapServiceTest`:
```java
@Test
void shouldQueryHistoryContractsWithCustIsnParam() {
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
LoanRateHistoryService service = new LoanRateHistoryService(restTemplate, "http://mock/history?appCode=abc");
server.expect(requestTo("http://mock/history?appCode=abc&cust_isn=81033011438"))
.andRespond(withSuccess("{\"code\":200,\"data\":[{\"cust_isn\":\"81033011438\",\"loan_contract_history\":\"HT001\",\"loan_rate_history\":\"3.65\"}]}", MediaType.APPLICATION_JSON));
List<HistoryLoanContractVO> result = service.query(" 81033011438 ");
assertEquals(1, result.size());
assertEquals("3.65", result.get(0).getLoanRateHistory());
server.verify();
}
@Test
void shouldRejectBlankCustIsn() {
LoanRateHistoryService service = new LoanRateHistoryService(new RestTemplate(), "http://mock/history");
ServiceException ex = assertThrows(ServiceException.class, () -> service.query(" "));
assertEquals("客户内码不能为空", ex.getMessage());
}
```
- [ ] **Step 2: Write failing controller/mock tests**
Mock endpoint tests must cover:
- Normal `cust_isn=81033011438` returns at least one row with `loan_rate_history`.
- Fixed empty scenario, for example `cust_isn=EMPTY_HISTORY`, returns `data: []`.
- Fixed empty-rate scenario, for example `cust_isn=EMPTY_RATE`, returns a row whose `loan_rate_history` is empty.
Customer-map mock tests must also cover browser-use fixed customer numbers:
- `cust_id=HISTORY_EMPTY` returns exactly one customer-map row with `cust_isn=EMPTY_HISTORY`.
- `cust_id=HISTORY_EMPTY_RATE` returns exactly one customer-map row with `cust_isn=EMPTY_RATE`.
Controller test must verify `/loanPricing/workflow/history-contract?custIsn=81033011438` delegates to `LoanRateHistoryService.query("81033011438")`.
- [ ] **Step 3: Run tests to verify they fail**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest test
```
Expected: FAIL because service/controller/mock do not exist.
- [ ] **Step 4: Implement `LoanRateHistoryService`**
```java
@Service
public class LoanRateHistoryService {
private final RestTemplate restTemplate;
@Value("${loan-rate-history.url}")
private String historyUrl;
public LoanRateHistoryService() {
this(new RestTemplate());
}
LoanRateHistoryService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
LoanRateHistoryService(RestTemplate restTemplate, String historyUrl) {
this.restTemplate = restTemplate;
this.historyUrl = historyUrl;
}
public List<HistoryLoanContractVO> query(String custIsn) {
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
if (!StringUtils.hasText(normalizedCustIsn)) {
throw new ServiceException("客户内码不能为空");
}
URI uri = UriComponentsBuilder.fromHttpUrl(historyUrl)
.queryParam("cust_isn", normalizedCustIsn)
.build()
.encode()
.toUri();
HistoryLoanContractResponse response = restTemplate.getForObject(uri, HistoryLoanContractResponse.class);
if (response == null) {
throw new ServiceException("历史贷款合同接口无返回");
}
if (response.getCode() != null && response.getCode() != 200) {
throw new ServiceException(StringUtils.hasText(response.getMsg()) ? response.getMsg() : "历史贷款合同查询失败");
}
return response.getData() == null ? Collections.emptyList() : response.getData();
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
static class HistoryLoanContractResponse {
private Integer code;
private String msg;
private List<HistoryLoanContractVO> data;
}
}
```
- [ ] **Step 5: Add controller endpoint**
Inject `LoanRateHistoryService` into `LoanPricingWorkflowController` and add:
```java
@Operation(summary = "查询历史贷款合同")
@GetMapping("/history-contract")
public AjaxResult queryHistoryContract(@RequestParam("custIsn") String custIsn) {
return success(loanRateHistoryService.query(custIsn));
}
```
- [ ] **Step 6: Add mock endpoint**
Add to `LoanRatePricingMockController`:
```java
@Anonymous
@Operation(summary = "模拟历史贷款合同查询")
@GetMapping("/history-contract")
public AjaxResult queryHistoryContract(@RequestParam("cust_isn") String custIsn) {
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
if (!StringUtils.hasText(normalizedCustIsn)) {
throw new ServiceException("客户内码不能为空");
}
if ("EMPTY_HISTORY".equals(normalizedCustIsn)) {
return success(Collections.emptyList());
}
List<HistoryLoanContractVO> records = new ArrayList<>();
HistoryLoanContractVO record = new HistoryLoanContractVO();
record.setCustIsn(normalizedCustIsn);
record.setLoanContractHistory("HT" + normalizedCustIsn);
record.setGuarTypeHistory("信用");
record.setProductCodeHistory("P001");
record.setLoanRateHistory("EMPTY_RATE".equals(normalizedCustIsn) ? "" : "3.65");
record.setLoanAmountHistory("100000");
record.setLoanSignDateHistory(LocalDate.now().minusMonths(6).toString());
records.add(record);
return success(records);
}
```
- [ ] **Step 7: Add fixed customer-map mock scenarios**
Before the random-record loop in `randomCustomerMapRecords`, add:
```java
if ("HISTORY_EMPTY".equals(normalizedCustId)) {
CustomerMapRecordVO record = new CustomerMapRecordVO();
record.setCustId(normalizedCustId);
record.setCustIsn("EMPTY_HISTORY");
record.setCustName(namePrefix + "空历史合同");
record.setFaithDay("0");
record.setBalanceAvg("0");
record.setLoanCountHis("0");
record.setLastLoanDate("");
return Collections.singletonList(record);
}
if ("HISTORY_EMPTY_RATE".equals(normalizedCustId)) {
CustomerMapRecordVO record = new CustomerMapRecordVO();
record.setCustId(normalizedCustId);
record.setCustIsn("EMPTY_RATE");
record.setCustName(namePrefix + "空历史利率");
record.setFaithDay("30");
record.setBalanceAvg("10000");
record.setLoanCountHis("1");
record.setLastLoanDate(LocalDate.now().minusMonths(3).toString());
return Collections.singletonList(record);
}
```
Import `java.util.Collections` if needed.
- [ ] **Step 8: Add profile config**
Use mock URLs in dev/uat:
```yaml
loan-rate-history:
url: http://localhost:63310/rate/pricing/mock/history-contract
```
Use real URL in pro without `cust_isn=`:
```yaml
loan-rate-history:
url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07
```
- [ ] **Step 9: Run proxy/mock tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerCustomerMapTest test
```
Expected: PASS.
- [ ] **Step 10: Commit**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java \
ruoyi-admin/src/main/resources/application-dev.yml \
ruoyi-admin/src/main/resources/application-uat.yml \
ruoyi-admin/src/main/resources/application-pro.yml \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java
git commit -m "新增历史贷款合同查询接口"
```
## Task 4: Persist SQL Schema Changes
**Files:**
- Create: `sql/add_business_type_history_rate_20260429.sql`
- Modify: `sql/loan_pricing_workflow.sql`
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 1: Write migration SQL**
Create:
```sql
-- 为利率定价流程添加业务种类和历史贷款利率字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER `loan_purpose`,
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER `business_type`;
```
- [ ] **Step 2: Update schema SQL files**
In each `loan_pricing_workflow` create-table definition, add:
```sql
`business_type` varchar(20) DEFAULT NULL COMMENT '业务种类',
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率',
```
Place them after `loan_purpose`.
- [ ] **Step 3: Verify SQL text**
Run:
```bash
rg -n "business_type|loan_rate_history" sql/add_business_type_history_rate_20260429.sql sql/loan_pricing_workflow.sql sql/loan_pricing_schema_20260328.sql sql/loan_pricing_prod_init_20260331.sql
```
Expected: each file contains both fields in the workflow table context.
- [ ] **Step 4: Commit**
```bash
git add sql/add_business_type_history_rate_20260429.sql \
sql/loan_pricing_workflow.sql \
sql/loan_pricing_schema_20260328.sql \
sql/loan_pricing_prod_init_20260331.sql
git commit -m "新增业务种类历史利率数据库字段"
```
## Task 5: Verify Model Input Chain
**Files:**
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- [ ] **Step 1: Add personal model assertion**
In `shouldInvokePersonalModelWithExpectedParams`, set:
```java
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory("3.65");
```
Add to the `argThat`:
```java
&& Objects.equals("3.65", dto.getLoanRateHistory())
```
- [ ] **Step 2: Add corporate model assertion**
Add or update a corporate model invocation test in `LoanPricingModelServiceTest`:
```java
workflow.setCustType("企业");
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory("3.75");
```
Verify:
```java
verify(modelService).invokeCorporateModel(argThat((ModelInvokeDTO dto) ->
Objects.equals("3.75", dto.getLoanRateHistory())));
```
- [ ] **Step 3: Run model tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest test
```
Expected: PASS and no `businessType` field exists in `ModelInvokeDTO`.
- [ ] **Step 4: Commit**
```bash
git add ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java
git commit -m "验证历史利率模型入参链路"
```
## Task 6: Backend Final Verification
**Files:**
- No new files unless a test failure requires a focused fix.
- [ ] **Step 1: Run focused backend test suite**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=HistoryLoanContractVOTest,LoanRateHistoryServiceTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest test
```
Expected: PASS.
- [ ] **Step 2: Run existing related customer-map tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanPricingWorkflowControllerCustomerMapTest,LoanRatePricingMockControllerCustomerMapTest,CustomerMapRecordVOTest test
```
Expected: PASS, confirming the new proxy pattern did not break customer-map behavior.
- [ ] **Step 3: Record backend implementation notes**
Append backend verification notes to a new or existing implementation report, for example:
```text
doc/implementation-report-2026-04-29-business-type-history-rate.md
```
Include commands run and whether browser-use verification remains for the frontend plan.
- [ ] **Step 4: Commit**
```bash
git add doc/implementation-report-2026-04-29-business-type-history-rate.md
git commit -m "记录业务种类历史利率后端验证"
```

View File

@@ -1,732 +0,0 @@
# Business Type History Rate Frontend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not enable subagents or `using-superpowers` during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add business type selection and historical loan-contract single-selection to the personal and corporate workflow creation UI, then verify the real page with browser-use.
**Architecture:** Reuse the current customer-map driven creation flow. Add one shared `HistoryContractSelector` component, one API function, fields and validation in both create dialogs, and detail display in both detail components. Final browser verification must use `browser-use:browser` with the in-app browser, not Playwright CLI or prototype pages.
**Tech Stack:** Vue 2, Element UI, RuoYi request wrapper, Node static tests, nvm Node 14.21.3, browser-use `iab` runtime for real page testing.
---
## File Structure
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Add `queryHistoryContracts(custIsn)`.
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
- Shared modal for historical contract query results and single selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Add business type, history-rate display, query trigger, selection handling, and submit validation.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Same behavior for corporate creation.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Show business type and historical loan rate.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- Show business type and historical loan rate.
- Modify: `ruoyi-ui/package.json`
- Add `test:business-type-history-rate`.
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`
- Static coverage for API, selector, create dialogs, validation, clearing, and detail display.
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`
- Add frontend and browser-use verification notes after execution.
## Task 1: Add Frontend API and Static Test Skeleton
**Files:**
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Modify: `ruoyi-ui/package.json`
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Write failing API assertions**
Create `ruoyi-ui/tests/business-type-history-rate.test.js`:
```js
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const workflowApi = read('src/api/loanPricing/workflow.js')
assert(
workflowApi.includes('export function queryHistoryContracts') &&
workflowApi.includes("url: '/loanPricing/workflow/history-contract'") &&
workflowApi.includes('params: { custIsn: custIsn }'),
'缺少历史贷款合同查询 API'
)
console.log('business type history rate assertions passed')
```
Add package script:
```json
"test:business-type-history-rate": "node tests/business-type-history-rate.test.js"
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL because the API function does not exist.
- [ ] **Step 3: Implement API function**
Append to `workflow.js`:
```js
// 查询历史贷款合同
export function queryHistoryContracts(custIsn) {
return request({
url: '/loanPricing/workflow/history-contract',
method: 'get',
params: { custIsn: custIsn }
})
}
```
- [ ] **Step 4: Run API test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/api/loanPricing/workflow.js ruoyi-ui/package.json ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "新增历史贷款合同前端接口"
```
## Task 2: Create Historical Contract Selector
**Files:**
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Extend failing selector assertions**
Add:
```js
const historySelector = read('src/views/loanPricing/workflow/components/HistoryContractSelector.vue')
assert(
historySelector.includes('title="历史贷款合同选择"') &&
historySelector.includes('width="80%"') &&
historySelector.includes(':data="contracts"'),
'历史贷款合同选择弹窗缺少标题、宽度或表格数据'
)
;[
'cust_isn',
'loan_contract_history',
'guar_type_history',
'product_code_history',
'loan_rate_history',
'loan_amount_history',
'loan_sign_date_history'
].forEach((field) => {
assert(historySelector.includes(`prop="${field}"`) || historySelector.includes(`row.${field}`), `历史合同弹窗缺少字段 ${field}`)
})
assert(
historySelector.includes('type="radio"') &&
historySelector.includes('selectedContract') &&
historySelector.includes("this.$emit('select', this.selectedContract)") &&
historySelector.includes('请选择历史贷款合同'),
'历史合同弹窗缺少单选、确定选择或未选提示'
)
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL because the component does not exist.
- [ ] **Step 3: Implement `HistoryContractSelector.vue`**
Use a small presentational component. It receives already-loaded `contracts` and does not call the API itself.
```vue
<template>
<el-dialog title="历史贷款合同选择" :visible.sync="dialogVisible" width="80%" append-to-body
:close-on-click-modal="false" @close="handleClose">
<el-table :data="contracts" v-loading="loading" @row-click="handleRowClick">
<el-table-column label="选择" align="center" width="70">
<template slot-scope="scope">
<el-radio v-model="selectedContract" :label="scope.row">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="历史贷款合同号" prop="loan_contract_history" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="历史贷款担保方式" prop="guar_type_history" align="center"/>
<el-table-column label="历史贷款产品代码" prop="product_code_history" align="center"/>
<el-table-column label="历史贷款利率" prop="loan_rate_history" align="center"/>
<el-table-column label="历史贷款金额" prop="loan_amount_history" align="center"/>
<el-table-column label="历史贷款签订时间" prop="loan_sign_date_history" align="center" width="150"/>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmSelect"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</div>
</el-dialog>
</template>
```
Script:
```js
export default {
name: "HistoryContractSelector",
props: {
visible: { type: Boolean, default: false },
contracts: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }
},
data() {
return { selectedContract: null }
},
computed: {
dialogVisible: {
get() { return this.visible },
set(val) { this.$emit('update:visible', val) }
}
},
methods: {
handleRowClick(row) {
this.selectedContract = row
},
confirmSelect() {
if (!this.selectedContract) {
this.$modal.msgWarning("请选择历史贷款合同")
return
}
if (!this.selectedContract.loan_rate_history) {
this.$modal.msgWarning("历史贷款利率不能为空")
return
}
this.$emit('select', this.selectedContract)
this.dialogVisible = false
},
handleClose() {
this.selectedContract = null
}
}
}
```
- [ ] **Step 4: Run selector test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS for selector assertions.
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "新增历史贷款合同选择弹窗"
```
## Task 3: Add Personal Create Dialog Behavior
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Extend failing personal dialog assertions**
Add:
```js
const personalCreate = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
assert(
personalCreate.includes('label="业务种类"') &&
personalCreate.includes('v-model="form.businessType"') &&
personalCreate.includes('新客') &&
personalCreate.includes('存量新增') &&
personalCreate.includes('存量转贷'),
'个人新增弹窗缺少业务种类选择'
)
assert(
personalCreate.includes('label="历史贷款利率"') &&
personalCreate.includes('v-model="form.loanRateHistory"') &&
personalCreate.includes(':readonly="true"'),
'个人新增弹窗缺少只读历史贷款利率'
)
assert(
personalCreate.includes('queryHistoryContracts') &&
personalCreate.includes('HistoryContractSelector') &&
personalCreate.includes('handleBusinessTypeChange') &&
personalCreate.includes('handleHistoryContractSelect'),
'个人新增弹窗缺少历史合同查询和选择逻辑'
)
assert(
personalCreate.includes('请选择历史贷款合同') &&
personalCreate.includes('未查询到历史贷款合同') &&
personalCreate.includes('delete data.loanRateHistory'),
'个人新增弹窗缺少存量转贷拦截、空列表提示或非存量转贷清理'
)
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL.
- [ ] **Step 3: Implement personal form fields**
In the loan information area, add:
```vue
<el-row>
<el-col :span="12">
<el-form-item label="业务种类" prop="businessType">
<el-select v-model="form.businessType" placeholder="请选择业务种类" style="width: 100%" @change="handleBusinessTypeChange">
<el-option label="新客" value="新客"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="isStockTransfer">
<el-form-item label="历史贷款利率" prop="loanRateHistory">
<el-input v-model="form.loanRateHistory" placeholder="请选择历史贷款合同" :readonly="true"/>
</el-form-item>
</el-col>
</el-row>
```
Add selector under the form:
```vue
<history-contract-selector
:visible.sync="showHistorySelector"
:contracts="historyContracts"
:loading="historyLoading"
@select="handleHistoryContractSelect"
/>
```
- [ ] **Step 4: Implement personal script**
Import:
```js
import {createPersonalWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
import HistoryContractSelector from "./HistoryContractSelector"
```
Register component, then add state:
```js
businessTypeOptions: ['新客', '存量新增', '存量转贷'],
showHistorySelector: false,
historyLoading: false,
historyContracts: [],
selectedHistoryContract: null,
```
Add form fields in initial data and `reset()`:
```js
businessType: undefined,
loanRateHistory: undefined,
```
Add rules:
```js
businessType: [
{required: true, message: "请选择业务种类", trigger: "change"}
],
loanRateHistory: [
{required: true, message: "请选择历史贷款合同", trigger: "change"}
]
```
Add computed:
```js
isStockTransfer() {
return this.form.businessType === '存量转贷'
}
```
Add methods:
```js
handleBusinessTypeChange(value) {
this.clearHistoryContract()
if (value === '存量转贷') {
this.queryHistoryContracts()
}
},
queryHistoryContracts() {
if (!this.form.custIsn) {
this.$modal.msgWarning("客户内码不能为空")
return
}
this.historyLoading = true
queryHistoryContracts(this.form.custIsn).then(response => {
this.historyContracts = response.data || []
if (this.historyContracts.length === 0) {
this.$modal.msgWarning("未查询到历史贷款合同")
return
}
this.showHistorySelector = true
}).finally(() => {
this.historyLoading = false
})
},
handleHistoryContractSelect(row) {
if (!row.loan_rate_history) {
this.$modal.msgWarning("历史贷款利率不能为空")
return
}
this.selectedHistoryContract = row
this.form.loanRateHistory = row.loan_rate_history
},
clearHistoryContract() {
this.selectedHistoryContract = null
this.form.loanRateHistory = undefined
this.historyContracts = []
}
```
In `submitForm`, before setting `submitting`:
```js
if (this.isStockTransfer && !this.form.loanRateHistory) {
this.$modal.msgWarning("请选择历史贷款合同")
return
}
```
When building `data`, remove historical rate for non-stock-transfer:
```js
if (!this.isStockTransfer) {
delete data.loanRateHistory
}
```
- [ ] **Step 5: Run personal static test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS for personal assertions.
- [ ] **Step 6: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "个人新增支持业务种类和历史利率"
```
## Task 4: Add Corporate Create Dialog Behavior
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Extend failing corporate assertions**
Mirror the personal assertions for `CorporateCreateDialog.vue`, replacing assertion messages with “企业新增弹窗...”.
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL.
- [ ] **Step 3: Implement corporate fields and selector**
Apply the same form, selector, state, rules, computed, and methods from Task 3, but keep existing corporate-specific fields and submission conversion intact.
- [ ] **Step 4: Keep collateral and green/trade behavior untouched**
When editing submit payload, keep:
```js
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
```
Do not reintroduce `repayMethod` UI.
- [ ] **Step 5: Run corporate static test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "企业新增支持业务种类和历史利率"
```
## Task 5: Add Detail Display
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Add failing detail assertions**
```js
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const corporateDetail = read('src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue')
;[
['个人详情', personalDetail],
['企业详情', corporateDetail]
].forEach(([name, source]) => {
assert(source.includes('label="业务种类"') && source.includes('detailData.businessType'), `${name} 缺少业务种类展示`)
assert(source.includes('label="历史贷款利率"') && source.includes('detailData.loanRateHistory'), `${name} 缺少历史贷款利率展示`)
})
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL.
- [ ] **Step 3: Add personal detail fields**
In personal “业务信息” descriptions, add:
```vue
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ detailData.loanRateHistory || '-' }}</el-descriptions-item>
```
- [ ] **Step 4: Add corporate detail fields**
Add the same two fields to corporate “业务信息” descriptions.
- [ ] **Step 5: Run detail test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue \
ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "详情展示业务种类和历史利率"
```
## Task 6: Frontend Static Verification
**Files:**
- No new files unless tests expose a focused defect.
- [ ] **Step 1: Run new focused test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 2: Run related existing frontend tests**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
```
Expected: PASS.
- [ ] **Step 3: Build frontend**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```
Expected: build succeeds.
- [ ] **Step 4: Commit only if fixes were required**
```bash
git status --short
```
Expected: no unexpected frontend changes beyond this plan.
## Task 7: Real Page Verification With browser-use
**Files:**
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`
- [ ] **Step 1: Start backend with latest code**
Start or restart the backend on port `63310` using the repo's existing scripts or Maven command. Confirm the backend has loaded the latest backend code before browser testing.
Example:
```bash
bin/restart_java_backend_test.sh
```
Expected: backend listens on `63310`.
- [ ] **Step 2: Start frontend with Node 14**
Use nvm-managed Node:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev -- --port 8080'
```
Expected: frontend dev server is available at `http://localhost:8080`.
- [ ] **Step 3: Initialize browser-use**
Use the Node REPL `js` tool and the in-app browser backend. First browser cell:
```js
if (!globalThis.agent) {
const { setupAtlasRuntime } = await import("/Users/wkc/.codex/plugins/cache/openai-bundled/browser-use/0.1.0-alpha1/scripts/browser-client.mjs");
const backend = "iab";
await setupAtlasRuntime({ globals: globalThis, backend });
}
await agent.browser.nameSession("🔎 利率定价历史利率测试");
if (typeof tab === "undefined") {
globalThis.tab = await agent.browser.tabs.new();
}
await tab.goto("http://localhost:8080");
await tab.playwright.waitForLoadState({ state: "domcontentloaded", timeoutMs: 10000 });
console.log(await tab.title());
```
- [ ] **Step 4: Log in on the real page**
Use the real login page, not a prototype. If the default test login path is available, use the project-supported login route. Otherwise log in through the visible login form with the configured test account.
After login, verify that the actual workflow list is visible.
- [ ] **Step 5: Verify personal new customer path**
In the browser:
1. Open 利率定价流程 list.
2. Click 新增.
3. Select 个人客户.
4. Query a customer number and choose a customer internal code.
5. In the personal create dialog, choose 业务种类 = 新客.
6. Confirm no historical contract selector appears.
7. Fill required fields and submit.
8. Open detail and verify 业务种类 displays 新客 and 历史贷款利率 is empty/`-`.
- [ ] **Step 6: Verify personal existing-new path**
Repeat personal creation with 业务种类 = 存量新增. Confirm no historical contract selector appears, submit succeeds, and detail displays 存量新增.
- [ ] **Step 7: Verify personal stock-transfer path**
Repeat personal creation with 业务种类 = 存量转贷. Confirm:
1. Historical contract selector opens.
2. All 7 columns are visible.
3. A single radio selection is possible.
4. Selecting a row fills 历史贷款利率.
5. Submit succeeds.
6. Detail displays 业务种类 = 存量转贷 and the chosen 历史贷款利率.
- [ ] **Step 8: Verify corporate three paths**
Repeat Steps 5-7 for 企业客户: 新客、存量新增、存量转贷.
- [ ] **Step 9: Verify blocked stock-transfer submission**
Use a stock-transfer flow and attempt to submit without selecting a historical contract. Confirm the page blocks submission and shows “请选择历史贷款合同”.
- [ ] **Step 10: Verify empty history scenario**
Use the fixed customer-map mock customer number `HISTORY_EMPTY`. In the customer number selector, query `HISTORY_EMPTY`, select the returned row with `cust_isn=EMPTY_HISTORY`, then choose 业务种类 = 存量转贷. Confirm “未查询到历史贷款合同” appears and submission remains blocked.
- [ ] **Step 11: Verify empty historical-rate scenario**
Use the fixed customer-map mock customer number `HISTORY_EMPTY_RATE`. In the customer number selector, query `HISTORY_EMPTY_RATE`, select the returned row with `cust_isn=EMPTY_RATE`, then choose 业务种类 = 存量转贷. Confirm the history selector opens with a row whose `loan_rate_history` is empty; selecting it and clicking 确定 must show “历史贷款利率不能为空”, must not fill the create form's historical loan-rate field, and submission must remain blocked.
- [ ] **Step 12: Capture evidence**
Capture screenshots or DOM snapshots for:
- Personal stock-transfer selector with all 7 columns.
- Corporate stock-transfer selector with all 7 columns.
- Detail page showing 业务种类 and 历史贷款利率.
- Blocked submit warning.
- Empty-history warning from `HISTORY_EMPTY`.
- Empty-rate warning from `HISTORY_EMPTY_RATE`.
Use browser-use screenshots through `await display(await tab.playwright.screenshot({ fullPage: false }))` when visual confirmation matters.
- [ ] **Step 13: Stop test processes**
Stop only the backend/frontend processes started for this test. Do not kill unrelated user processes.
- [ ] **Step 14: Record verification**
Update:
```text
doc/implementation-report-2026-04-29-business-type-history-rate.md
```
Include:
- Static test commands and results.
- Backend/frontend server commands.
- browser-use URL and scenarios covered.
- Confirmation that test processes were stopped.
- [ ] **Step 15: Commit verification notes**
```bash
git add doc/implementation-report-2026-04-29-business-type-history-rate.md
git commit -m "记录业务种类历史利率页面验证"
```

View File

@@ -1,678 +0,0 @@
# Customer Map Selection Backend Implementation Plan
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.
**Goal:** Add backend customer-id-to-customer-internal-code query APIs for personal and corporate workflow creation, backed by local mock interfaces and profile configuration.
**Architecture:** Keep workflow creation APIs unchanged. Add a small customer-map service that reads separate personal/corporate URLs from configuration, calls them with GET `cust_id`, and returns mapping records with underscore JSON field names through the existing RuoYi `AjaxResult` response convention. Add mock endpoints under the existing mock controller so all active profiles can point to local mock URLs first.
**Tech Stack:** Spring Boot, RuoYi `AjaxResult`, Spring `RestTemplate`, Jackson `@JsonProperty`, JUnit 5, Mockito, Maven.
---
## File Structure
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java`
- Holds one customer mapping record.
- Uses Java camelCase fields internally and `@JsonProperty` to serialize/deserialize underscore field names.
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
- Owns config URL selection, GET forwarding, parameter validation, and response parsing.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- Adds authenticated business endpoints used by the frontend.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
- Adds anonymous local mock endpoints.
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- Adds `customer-map` mock URL config.
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
- Adds `customer-map` mock URL config.
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
- Adds `customer-map` mock URL config for the production profile, currently pointing to local mock as requested.
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java`
- Verifies JSON field names stay underscored.
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
- Verifies personal/corporate URL routing, `cust_id` forwarding, response parsing, and missing customer-id errors.
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
- Verifies mock endpoints return one or more underscore-field records.
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java`
- Verifies business controller methods delegate to the service and preserve the result records.
## Task 1: Add Customer Map Record VO
**Files:**
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java`
- [ ] **Step 1: Write the failing serialization test**
Create `CustomerMapRecordVOTest`:
```java
package com.ruoyi.loanpricing.domain.vo;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
class CustomerMapRecordVOTest
{
@Test
void shouldSerializeCustomerMapFieldsWithUnderscoreNames() throws Exception
{
CustomerMapRecordVO record = new CustomerMapRecordVO();
record.setCustId("101330419198206033217");
record.setCustIsn("81033011438");
record.setCustName("张三");
record.setFaithDay("20");
record.setBalanceAvg("300000");
record.setLoanCountHis("2");
record.setLastLoanDate("2025-12-01");
String json = new ObjectMapper().writeValueAsString(record);
assertTrue(json.contains("\"cust_id\""));
assertTrue(json.contains("\"cust_isn\""));
assertTrue(json.contains("\"cust_name\""));
assertTrue(json.contains("\"faith_day\""));
assertTrue(json.contains("\"balance_avg\""));
assertTrue(json.contains("\"loan_count_his\""));
assertTrue(json.contains("\"last_loan_date\""));
}
}
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: FAIL because `CustomerMapRecordVO` does not exist.
- [ ] **Step 3: Implement the VO**
Create `CustomerMapRecordVO.java`:
```java
package com.ruoyi.loanpricing.domain.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import lombok.Data;
@Data
public class CustomerMapRecordVO implements Serializable
{
private static final long serialVersionUID = 1L;
@JsonProperty("cust_id")
private String custId;
@JsonProperty("cust_isn")
private String custIsn;
@JsonProperty("cust_name")
private String custName;
@JsonProperty("faith_day")
private String faithDay;
@JsonProperty("balance_avg")
private String balanceAvg;
@JsonProperty("loan_count_his")
private String loanCountHis;
@JsonProperty("last_loan_date")
private String lastLoanDate;
}
```
- [ ] **Step 4: Run the test to verify it passes**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
## Task 2: Add Customer Map Service
**Files:**
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
- [ ] **Step 1: Write service tests first**
Create tests that use `MockRestServiceServer` and the package-private test constructor:
```java
package com.ruoyi.loanpricing.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import org.springframework.test.web.client.MockRestServiceServer;
class LoanPricingCustomerMapServiceTest
{
@Test
void shouldQueryPersonalCustomerMapWithCustIdParam()
{
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
restTemplate,
"http://mock/personal",
"http://mock/corporate");
server.expect(requestTo("http://mock/personal?cust_id=P001"))
.andRespond(withSuccess("{\"code\":200,\"msg\":\"操作成功\",\"data\":[{\"cust_id\":\"P001\",\"cust_isn\":\"81033011438\",\"cust_name\":\"张三\"}]}",
MediaType.APPLICATION_JSON));
List<CustomerMapRecordVO> result = service.queryPersonal("P001");
assertEquals(1, result.size());
assertEquals("81033011438", result.get(0).getCustIsn());
assertEquals("张三", result.get(0).getCustName());
server.verify();
}
@Test
void shouldQueryCorporateCustomerMapWithCustIdParam()
{
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
restTemplate,
"http://mock/personal",
"http://mock/corporate");
server.expect(requestTo("http://mock/corporate?cust_id=C001"))
.andRespond(withSuccess("{\"code\":200,\"data\":[{\"cust_id\":\"C001\",\"cust_isn\":\"82002469287\",\"cust_name\":\"测试企业\"}]}",
MediaType.APPLICATION_JSON));
List<CustomerMapRecordVO> result = service.queryCorporate("C001");
assertEquals("82002469287", result.get(0).getCustIsn());
assertEquals("测试企业", result.get(0).getCustName());
server.verify();
}
@Test
void shouldRejectBlankCustId()
{
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
new RestTemplate(),
"http://mock/personal",
"http://mock/corporate");
ServiceException ex = assertThrows(ServiceException.class, () -> service.queryPersonal(" "));
assertEquals("客户号不能为空", ex.getMessage());
}
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: FAIL because `LoanPricingCustomerMapService` does not exist.
- [ ] **Step 3: Implement the service**
Create the service with a default Spring constructor and a package-private test constructor:
```java
package com.ruoyi.loanpricing.service;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Service
public class LoanPricingCustomerMapService
{
private final RestTemplate restTemplate;
@Value("${customer-map.personal-url}")
private String personalUrl;
@Value("${customer-map.corporate-url}")
private String corporateUrl;
public LoanPricingCustomerMapService()
{
this(new RestTemplate());
}
LoanPricingCustomerMapService(RestTemplate restTemplate)
{
this.restTemplate = restTemplate;
}
LoanPricingCustomerMapService(RestTemplate restTemplate, String personalUrl, String corporateUrl)
{
this.restTemplate = restTemplate;
this.personalUrl = personalUrl;
this.corporateUrl = corporateUrl;
}
public List<CustomerMapRecordVO> queryPersonal(String custId)
{
return query(personalUrl, custId);
}
public List<CustomerMapRecordVO> queryCorporate(String custId)
{
return query(corporateUrl, custId);
}
private List<CustomerMapRecordVO> query(String url, String custId)
{
if (!StringUtils.hasText(custId))
{
throw new ServiceException("客户号不能为空");
}
URI uri = UriComponentsBuilder.fromHttpUrl(url)
.queryParam("cust_id", custId)
.build()
.toUri();
CustomerMapResponse response = restTemplate.getForObject(uri, CustomerMapResponse.class);
if (response == null)
{
throw new ServiceException("客户号映射接口无返回");
}
if (response.getCode() != null && response.getCode() != 200)
{
throw new ServiceException(StringUtils.hasText(response.getMsg()) ? response.getMsg() : "客户号映射查询失败");
}
return response.getData() == null ? Collections.emptyList() : response.getData();
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
static class CustomerMapResponse
{
private Integer code;
private String msg;
private List<CustomerMapRecordVO> data;
}
}
```
- [ ] **Step 4: Run service tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
## Task 3: Add Mock Endpoints
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
- [ ] **Step 1: Write controller tests for mock endpoints**
```java
package com.ruoyi.loanpricing.controller;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import java.util.List;
import org.junit.jupiter.api.Test;
class LoanRatePricingMockControllerCustomerMapTest
{
@Test
void shouldReturnPersonalCustomerMapRecords() throws Exception
{
LoanRatePricingMockController controller = new LoanRatePricingMockController();
AjaxResult result = controller.queryPersonalCustomerMap("P001");
List<?> rows = (List<?>) result.get("data");
assertFalse(rows.isEmpty());
String json = new ObjectMapper().writeValueAsString(rows.get(0));
assertTrue(json.contains("\"cust_id\""));
assertTrue(json.contains("\"cust_isn\""));
assertTrue(json.contains("\"cust_name\""));
}
@Test
void shouldReturnCorporateCustomerMapRecords() throws Exception
{
LoanRatePricingMockController controller = new LoanRatePricingMockController();
AjaxResult result = controller.queryCorporateCustomerMap("C001");
List<?> rows = (List<?>) result.get("data");
assertFalse(rows.isEmpty());
String json = new ObjectMapper().writeValueAsString(rows.get(0));
assertTrue(json.contains("\"cust_id\""));
assertTrue(json.contains("\"loan_count_his\""));
assertTrue(json.contains("\"last_loan_date\""));
}
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: FAIL because the mock methods do not exist.
- [ ] **Step 3: Implement mock methods**
Modify `LoanRatePricingMockController`:
```java
@Anonymous
@Operation(summary = "模拟个人客户号映射查询")
@GetMapping("/customer-map/personal")
public AjaxResult queryPersonalCustomerMap(@RequestParam("cust_id") String custId)
{
return success(randomCustomerMapRecords(custId, "个人客户"));
}
@Anonymous
@Operation(summary = "模拟企业客户号映射查询")
@GetMapping("/customer-map/corporate")
public AjaxResult queryCorporateCustomerMap(@RequestParam("cust_id") String custId)
{
return success(randomCustomerMapRecords(custId, "企业客户"));
}
```
Add a private helper in the same controller:
```java
private List<CustomerMapRecordVO> randomCustomerMapRecords(String custId, String namePrefix)
{
if (!StringUtils.hasText(custId))
{
throw new ServiceException("客户号不能为空");
}
int count = ThreadLocalRandom.current().nextInt(1, 4);
List<CustomerMapRecordVO> records = new ArrayList<>();
for (int i = 1; i <= count; i++)
{
CustomerMapRecordVO record = new CustomerMapRecordVO();
record.setCustId(custId);
record.setCustIsn(String.valueOf(81000000000L + ThreadLocalRandom.current().nextInt(1000000)));
record.setCustName(namePrefix + i);
record.setFaithDay(String.valueOf(ThreadLocalRandom.current().nextInt(1, 365)));
record.setBalanceAvg(String.valueOf(ThreadLocalRandom.current().nextInt(10000, 900000)));
record.setLoanCountHis(String.valueOf(ThreadLocalRandom.current().nextInt(0, 10)));
record.setLastLoanDate(LocalDate.now().minusDays(ThreadLocalRandom.current().nextInt(1, 800)).toString());
records.add(record);
}
return records;
}
```
Required imports:
```java
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
```
- [ ] **Step 4: Run mock controller tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
## Task 4: Add Business Endpoints
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java`
- [ ] **Step 1: Write controller delegation tests**
```java
package com.ruoyi.loanpricing.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
import java.lang.reflect.Field;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
class LoanPricingWorkflowControllerCustomerMapTest
{
@Test
void shouldReturnPersonalCustomerMapFromService() throws Exception
{
LoanPricingCustomerMapService service = Mockito.mock(LoanPricingCustomerMapService.class);
CustomerMapRecordVO row = new CustomerMapRecordVO();
row.setCustIsn("81033011438");
when(service.queryPersonal("P001")).thenReturn(List.of(row));
LoanPricingWorkflowController controller = new LoanPricingWorkflowController();
setField(controller, "customerMapService", service);
AjaxResult result = controller.queryPersonalCustomerMap("P001");
List<?> rows = (List<?>) result.get("data");
assertEquals(1, rows.size());
}
@Test
void shouldReturnCorporateCustomerMapFromService() throws Exception
{
LoanPricingCustomerMapService service = Mockito.mock(LoanPricingCustomerMapService.class);
CustomerMapRecordVO row = new CustomerMapRecordVO();
row.setCustIsn("82002469287");
when(service.queryCorporate("C001")).thenReturn(List.of(row));
LoanPricingWorkflowController controller = new LoanPricingWorkflowController();
setField(controller, "customerMapService", service);
AjaxResult result = controller.queryCorporateCustomerMap("C001");
List<?> rows = (List<?>) result.get("data");
assertEquals(1, rows.size());
}
private static void setField(Object target, String fieldName, Object value) throws Exception
{
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: FAIL because controller methods and field do not exist.
- [ ] **Step 3: Implement controller methods**
In `LoanPricingWorkflowController`, add:
```java
@Autowired
private LoanPricingCustomerMapService customerMapService;
@Operation(summary = "查询个人客户号映射")
@GetMapping("/customer-map/personal")
public AjaxResult queryPersonalCustomerMap(@RequestParam("custId") String custId)
{
return success(customerMapService.queryPersonal(custId));
}
@Operation(summary = "查询企业客户号映射")
@GetMapping("/customer-map/corporate")
public AjaxResult queryCorporateCustomerMap(@RequestParam("custId") String custId)
{
return success(customerMapService.queryCorporate(custId));
}
```
Required import:
```java
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
```
- [ ] **Step 4: Run controller tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
## Task 5: Add Profile Configuration
**Files:**
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
- [ ] **Step 1: Add `customer-map` to every active profile**
Place this block next to the existing `model:` block in each profile file:
```yaml
customer-map:
personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal
corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate
```
Keep the existing `model:` URLs unchanged.
- [ ] **Step 2: Verify config keys exist**
Run:
```bash
rg -n "customer-map:|personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal|corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate" ruoyi-admin/src/main/resources/application-dev.yml ruoyi-admin/src/main/resources/application-uat.yml ruoyi-admin/src/main/resources/application-pro.yml
```
Expected: each profile contains one `customer-map` block with both mock URLs.
## Task 6: Backend Verification
**Files:**
- Verify only, no new files.
- [ ] **Step 1: Run focused backend tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 2: Run affected existing model/workflow tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 3: Manual API verification after backend restart**
Restart backend so config and routes are active, then call:
```bash
curl -sS 'http://localhost:63310/rate/pricing/mock/customer-map/personal?cust_id=P001'
curl -sS 'http://localhost:63310/rate/pricing/mock/customer-map/corporate?cust_id=C001'
TOKEN=$(curl -sS -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' 'http://localhost:63310/login/test' | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
curl -sS -H "Authorization: Bearer ${TOKEN}" 'http://localhost:63310/loanPricing/workflow/customer-map/personal?custId=P001'
curl -sS -H "Authorization: Bearer ${TOKEN}" 'http://localhost:63310/loanPricing/workflow/customer-map/corporate?custId=C001'
```
Expected: each successful response uses the RuoYi response wrapper and `data` contains one or more records with underscore fields. If the local admin password differs, replace `admin/admin123` with a valid local test account before calling the authenticated workflow endpoints.
- [ ] **Step 4: Commit backend work**
Use a Chinese commit message and do not include unrelated dirty files:
```bash
git status --short
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java \
ruoyi-admin/src/main/resources/application-dev.yml \
ruoyi-admin/src/main/resources/application-uat.yml \
ruoyi-admin/src/main/resources/application-pro.yml
git commit -m "新增客户号映射后端接口"
```
Do not commit temporary curl output, screenshots, or generated test data.

View File

@@ -1,662 +0,0 @@
# Customer Map Selection Frontend Implementation Plan
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.
**Goal:** Add the frontend customer-id query and customer-internal-code selection step before opening personal or corporate workflow creation dialogs.
**Architecture:** Keep the existing list page and personal/corporate creation dialogs. Insert a shared customer-map selector dialog between customer-type selection and workflow creation, then pass the selected underscore-field record into the existing create dialogs. Existing workflow create APIs stay unchanged and still receive `custIsn` and `custName` in camelCase.
**Tech Stack:** Vue 2, Element UI, existing RuoYi request wrapper, Node static assertion tests, nvm-managed frontend runtime, Playwright/browser verification after implementation.
---
## File Structure
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Adds personal/corporate customer-map query functions.
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
- Owns customer-id input, query action, result table, loading state, and row selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Opens customer-map selector after customer type selection and then opens the correct create dialog after row selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Accepts selected customer-map record, fills `custIsn` and `custName`, and makes both fields read-only.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Same selected-record behavior for corporate creation.
- Modify: `ruoyi-ui/package.json`
- Adds a focused test script for the new customer-map selection checks.
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
- Static test coverage for API paths, selector fields, parent wiring, selected underscore fields, and read-only create dialog inputs.
## Task 1: Add Frontend API Methods
**Files:**
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
- Modify: `ruoyi-ui/package.json`
- [ ] **Step 1: Write failing API assertions**
Create the first version of `ruoyi-ui/tests/customer-map-selection.test.js`:
```js
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const workflowApi = read('src/api/loanPricing/workflow.js')
assert(
workflowApi.includes('export function queryPersonalCustomerMap') &&
workflowApi.includes("url: '/loanPricing/workflow/customer-map/personal'") &&
workflowApi.includes('params: { custId: custId }'),
'缺少个人客户号映射查询 API'
)
assert(
workflowApi.includes('export function queryCorporateCustomerMap') &&
workflowApi.includes("url: '/loanPricing/workflow/customer-map/corporate'") &&
workflowApi.includes('params: { custId: custId }'),
'缺少企业客户号映射查询 API'
)
console.log('customer map selection assertions passed')
```
Add a script in `ruoyi-ui/package.json`:
```json
"test:customer-map-selection": "node tests/customer-map-selection.test.js"
```
- [ ] **Step 2: Run the test to verify it fails**
Run with the project Node version:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because the API methods do not exist.
- [ ] **Step 3: Implement the API methods**
Append to `workflow.js`:
```js
// 查询个人客户号映射
export function queryPersonalCustomerMap(custId) {
return request({
url: '/loanPricing/workflow/customer-map/personal',
method: 'get',
params: { custId: custId }
})
}
// 查询企业客户号映射
export function queryCorporateCustomerMap(custId) {
return request({
url: '/loanPricing/workflow/customer-map/corporate',
method: 'get',
params: { custId: custId }
})
}
```
- [ ] **Step 4: Run the API test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS for API assertions.
## Task 2: Create Customer Map Selector Dialog
**Files:**
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
- [ ] **Step 1: Extend the static test for selector requirements**
Add these assertions to `customer-map-selection.test.js`:
```js
const selector = read('src/views/loanPricing/workflow/components/CustomerMapSelector.vue')
assert(
selector.includes('title="客户号查询"') &&
selector.includes('v-model="queryForm.custId"') &&
selector.includes('handleQuery'),
'客户号查询弹窗缺少客户号输入或查询动作'
)
assert(
selector.includes("queryPersonalCustomerMap") &&
selector.includes("queryCorporateCustomerMap") &&
selector.includes("this.customerType === 'personal'"),
'客户号查询弹窗未按客户类型调用个人/企业接口'
)
;['cust_id', 'cust_isn', 'cust_name', 'faith_day', 'balance_avg', 'loan_count_his', 'last_loan_date'].forEach((field) => {
assert(selector.includes(`prop="${field}"`) || selector.includes(`row.${field}`), `查询结果表格缺少字段 ${field}`)
})
assert(
selector.includes("this.$emit('select', row)") &&
selector.includes('未查询到客户信息') &&
selector.includes('请输入客户号'),
'客户号查询弹窗缺少选择事件或关键提示'
)
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because `CustomerMapSelector.vue` does not exist.
- [ ] **Step 3: Implement `CustomerMapSelector.vue`**
Create the component:
```vue
<template>
<el-dialog title="客户号查询" :visible.sync="dialogVisible" width="900px" append-to-body
:close-on-click-modal="false" @close="handleClose">
<el-form :model="queryForm" inline size="small">
<el-form-item label="客户号">
<el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="customerList">
<el-table-column label="客户号" prop="cust_id" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="客户名称" prop="cust_name" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="用信天数" prop="faith_day" align="center"/>
<el-table-column label="存款年日均" prop="balance_avg" align="center"/>
<el-table-column label="历史贷款次数" prop="loan_count_his" align="center"/>
<el-table-column label="上次贷款日期" prop="last_loan_date" align="center" width="130"/>
<el-table-column label="操作" align="center" width="90">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="handleSelect(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script>
import {queryPersonalCustomerMap, queryCorporateCustomerMap} from "@/api/loanPricing/workflow"
export default {
name: "CustomerMapSelector",
props: {
visible: {
type: Boolean,
default: false
},
customerType: {
type: String,
default: undefined
}
},
data() {
return {
loading: false,
queryForm: {
custId: undefined
},
customerList: []
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
handleQuery() {
if (!this.queryForm.custId) {
this.$modal.msgWarning("请输入客户号")
return
}
this.loading = true
const request = this.customerType === 'personal'
? queryPersonalCustomerMap
: queryCorporateCustomerMap
request(this.queryForm.custId).then(response => {
this.customerList = response.data || []
if (this.customerList.length === 0) {
this.$modal.msgWarning("未查询到客户信息")
}
}).finally(() => {
this.loading = false
})
},
handleSelect(row) {
this.$emit('select', row)
this.dialogVisible = false
},
handleClose() {
this.queryForm.custId = undefined
this.customerList = []
this.loading = false
}
}
}
</script>
```
- [ ] **Step 4: Run the selector test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS through selector assertions.
## Task 3: Wire Selector Into Workflow List Page
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
- [ ] **Step 1: Extend the test for parent wiring**
Add assertions:
```js
const workflowIndex = read('src/views/loanPricing/workflow/index.vue')
assert(
workflowIndex.includes('CustomerMapSelector') &&
workflowIndex.includes('<customer-map-selector') &&
workflowIndex.includes(':customer-type="selectedCustomerType"') &&
workflowIndex.includes('@select="handleCustomerMapSelect"'),
'流程列表页未接入客户号查询选择弹窗'
)
assert(
workflowIndex.includes('selectedCustomerType') &&
workflowIndex.includes('selectedCustomerMap') &&
workflowIndex.includes('showCustomerMapSelector'),
'流程列表页缺少客户类型、客户映射选择状态'
)
assert(
workflowIndex.includes("this.selectedCustomerType = type") &&
workflowIndex.includes('this.showCustomerMapSelector = true') &&
!workflowIndex.includes("if (type === 'personal') {\\n this.showPersonalDialog = true"),
'选择客户类型后应先打开客户号查询弹窗,而不是直接打开新增弹窗'
)
assert(
workflowIndex.includes('handleCustomerMapSelect') &&
workflowIndex.includes('this.selectedCustomerMap = row') &&
workflowIndex.includes('this.showPersonalDialog = true') &&
workflowIndex.includes('this.showCorporateDialog = true'),
'选择客户内码后未打开对应新增弹窗'
)
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because the list page does not yet use the selector.
- [ ] **Step 3: Update `index.vue` template and script**
Add component usage after the customer type selector:
```vue
<customer-map-selector
:visible.sync="showCustomerMapSelector"
:customer-type="selectedCustomerType"
@select="handleCustomerMapSelect"
/>
```
Pass the selected record into both create dialogs:
```vue
<personal-create-dialog
:visible.sync="showPersonalDialog"
:customer-map="selectedCustomerMap"
@success="handleCreateSuccess"
/>
<corporate-create-dialog
:visible.sync="showCorporateDialog"
:customer-map="selectedCustomerMap"
@success="handleCreateSuccess"
/>
```
Import and register the selector:
```js
import CustomerMapSelector from "./components/CustomerMapSelector"
```
Add state:
```js
showCustomerMapSelector: false,
selectedCustomerType: undefined,
selectedCustomerMap: null,
```
Change `handleSelectType`:
```js
handleSelectType(type) {
this.selectedCustomerType = type
this.selectedCustomerMap = null
this.showCustomerMapSelector = true
}
```
Add selected-row handler:
```js
handleCustomerMapSelect(row) {
this.selectedCustomerMap = row
if (this.selectedCustomerType === 'personal') {
this.showPersonalDialog = true
} else if (this.selectedCustomerType === 'corporate') {
this.showCorporateDialog = true
}
}
```
Clear selected state when the create flow completes:
```js
handleCreateSuccess() {
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
this.getList()
}
```
Add watchers for dialog close so cancellation also clears the selected record:
```js
watch: {
showPersonalDialog(val) {
if (!val && !this.showCorporateDialog) {
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
}
},
showCorporateDialog(val) {
if (!val && !this.showPersonalDialog) {
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
}
}
}
```
- [ ] **Step 4: Run the parent wiring test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS through parent wiring assertions.
## Task 4: Make Create Dialog Customer Fields Read-Only and Auto-Filled
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
- [ ] **Step 1: Extend test for create dialog behavior**
Add assertions:
```js
const personalCreateDialog = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
const corporateCreateDialog = read('src/views/loanPricing/workflow/components/CorporateCreateDialog.vue')
;[
['个人新增弹窗', personalCreateDialog],
['企业新增弹窗', corporateCreateDialog]
].forEach(([name, source]) => {
assert(source.includes('customerMap'), `${name} 缺少 customerMap 入参`)
assert(source.includes(':readonly="true"') || source.includes('readonly'), `${name} 客户内码和客户名称应只读`)
assert(source.includes('this.customerMap.cust_isn'), `${name} 未从 cust_isn 自动带入客户内码`)
assert(source.includes('this.customerMap.cust_name'), `${name} 未从 cust_name 自动带入客户名称`)
assert(source.includes('clearValidate'), `${name} 应清空校验而不是用 resetFields 覆盖已选客户`)
assert(!source.includes('this.resetForm("form")'), `${name} 不应在 reset() 中调用 resetForm("form") 覆盖只读客户字段`)
})
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because dialogs do not yet consume `customerMap`.
- [ ] **Step 3: Update personal create dialog**
Add prop:
```js
customerMap: {
type: Object,
default: null
}
```
Make customer fields read-only:
```vue
<el-input v-model="form.custIsn" placeholder="请选择客户内码" :readonly="true"/>
<el-input v-model="form.custName" placeholder="请选择客户名称" :readonly="true"/>
```
In `reset()`, initialize from selected row and do not call `this.resetForm("form")`. The existing RuoYi helper delegates to Element UI `resetFields()`, which can restore stale initial values and overwrite the selected read-only customer fields. After assigning the new form object, only clear validation:
```js
reset() {
this.form = {
orgCode: '892000',
runType: '1',
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
custName: this.customerMap ? this.customerMap.cust_name : undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
}
this.submitting = false
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
}
```
Keep existing required validation for `custIsn` and `custName`.
- [ ] **Step 4: Update corporate create dialog**
Apply the same prop, read-only inputs, and `reset()` initialization to `CorporateCreateDialog.vue`. Do not call `this.resetForm("form")` in the corporate dialog reset path; assign the new form object with `custIsn` and `custName` from `customerMap`, then use `this.$refs.form.clearValidate()` inside `$nextTick()`.
- [ ] **Step 5: Run the create dialog test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS through create dialog assertions.
## Task 5: Frontend Verification
**Files:**
- Verify only, no new source files.
- [ ] **Step 1: Run focused customer-map test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS.
- [ ] **Step 2: Run affected existing frontend tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
```
Expected: PASS.
- [ ] **Step 3: Build frontend**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```
Expected: build succeeds.
- [ ] **Step 4: Browser verification after backend and frontend are running**
Use the real application page, not a prototype page:
1. Open the workflow list page.
2. Click “新增”.
3. Select “个人客户”.
4. Confirm the customer-id query dialog opens.
5. Query any customer number.
6. Select one result row.
7. Confirm the personal create dialog opens and `客户内码` / `客户名称` are auto-filled and read-only.
8. Fill the remaining required fields and submit.
9. Repeat the same flow for “企业客户”.
10. Close and reopen the create flow at least once with a second selected row, and confirm the new row's `cust_isn` / `cust_name` replace the previous values in the create dialog.
11. Confirm both created records appear in the list or detail page.
Expected: both personal and corporate flows pass through customer-map selection before creation.
- [ ] **Step 5: Cleanup test processes**
Stop any backend or frontend processes started for verification before ending the task.
- [ ] **Step 6: Commit frontend work**
Use a Chinese commit message and avoid unrelated dirty files:
```bash
git status --short
git add ruoyi-ui/src/api/loanPricing/workflow.js \
ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue \
ruoyi-ui/src/views/loanPricing/workflow/index.vue \
ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
ruoyi-ui/package.json \
ruoyi-ui/tests/customer-map-selection.test.js
git commit -m "新增客户号查询选择前端流程"
```
Do not commit screenshots, browser traces, temporary spreadsheets, or generated test data.
## Task 6: Final End-to-End Verification and Implementation Record
**Files:**
- Create: `doc/implementation-report-2026-04-29-customer-map-selection.md`
- [ ] **Step 1: Run backend and frontend verification together**
After backend and frontend implementation are both complete:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
```
Expected: all tests pass.
- [ ] **Step 2: Complete browser verification**
Run the browser flow from Task 5 for both personal and corporate customers. Confirm the backend API response wrapper is still the existing RuoYi wrapper while records inside `data` keep underscore fields.
- [ ] **Step 3: Write implementation report**
Create `doc/implementation-report-2026-04-29-customer-map-selection.md` with:
```markdown
# 2026-04-29 客户号查询选择客户内码实施记录
## 修改内容
- 后端新增个人/企业客户号映射业务接口。
- 后端新增个人/企业客户号映射 mock 接口。
- 配置文件新增 `customer-map` 个人/企业地址并指向本项目 mock。
- 前端新增客户号查询选择弹窗。
- 个人/企业新增流程改为先查询客户号、选择客户内码,再打开新增弹窗。
- 新增弹窗客户内码和客户名称由选中记录自动带入并只读。
## 验证结果
- 后端测试:填写实际执行命令和通过/失败结果。
- 前端测试:填写实际执行命令和通过/失败结果。
- 真实页面验证:填写个人、企业两条浏览器验证流程和结果。
- 进程清理:填写本次启动的前后端进程是否已关闭。
```
- [ ] **Step 4: Commit implementation report**
```bash
git add doc/implementation-report-2026-04-29-customer-map-selection.md
git commit -m "补充客户号映射选择实施记录"
```

View File

@@ -1,647 +0,0 @@
# Shangyu Pricing Field Adjustment Backend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not invoke `using-superpowers` or subagents during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Update the backend create-flow contract so `businessType`, `couponRate`, personal removed fields, and customer-type-specific collateral options match the approved Shangyu pricing spec.
**Architecture:** Keep the existing workflow creation API and entity flow. Add `couponRate` to the DTO/entity/model path, tighten service-layer validation in `LoanPricingWorkflowServiceImpl`, remove personal create dependencies on `loanPurpose` and `bizProof`, and update SQL schema files in place.
**Tech Stack:** Spring Boot, RuoYi, MyBatis Plus, Lombok, Jakarta Validation, JUnit 5, Mockito, MySQL SQL files.
---
## File Structure
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
- Change `businessType` values to `新增/存量新增/存量转贷`.
- Remove personal create DTO dependency on `loanPurpose` and `bizProof`.
- Add `couponRate`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java`
- Change `businessType` values to `新增/存量新增/存量转贷`.
- Expand `collType` values for corporate mortgage and pledge paths.
- Add `couponRate`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Update `businessType` comment and add persisted `couponRate`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- Add `couponRate` only. Do not add `businessType`; the user excluded that model-input branch from this scope.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- Map `couponRate`.
- Stop mapping removed personal fields from the personal create DTO.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Update business type validation.
- Add collateral-option validation by `custType + guarType`.
- Add required `couponRate` validation for `质押 + 存单质押`.
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- Add validation coverage and update existing create tests to set valid `businessType`.
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Remove old personal `loanPurpose` DTO expectations so Maven test compilation remains valid after DTO removal.
- Keep `loanTerm/loanLoop` model assertions.
- Create Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java`
- Confirm personal removed fields no longer drive conversion and `couponRate` maps.
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- Confirm `couponRate` is copied into `ModelInvokeDTO` via the model invocation path.
- Create: `sql/add_coupon_rate_20260511.sql`
- Migration for existing databases.
- Modify: `sql/loan_pricing_workflow.sql`
- Add `coupon_rate`.
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Add `coupon_rate` to `loan_pricing_workflow`.
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- Add `coupon_rate` to production init schema.
## Task 1: Add Failing Backend Contract Tests
**Files:**
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Create Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- [ ] **Step 1: Update existing create tests with valid business type**
In `LoanPricingWorkflowServiceImplTest`, every test that calls `createLoanPricing` must set:
```java
workflow.setBusinessType("新增");
```
For tests using mortgage or pledge, also set an allowed `collType` for the matching `custType`.
- [ ] **Step 2: Update old personal parameter tests**
In `LoanPricingModelServicePersonalParamsTest`, replace the old DTO field test:
Add the static import if it is not present:
```java
import static org.junit.jupiter.api.Assertions.assertNull;
```
```java
@Test
void shouldRemoveLoanPurposeAndKeepLoanTermInPersonalCreateDto() throws NoSuchFieldException {
assertThrows(NoSuchFieldException.class,
() -> PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm"));
}
```
Replace the old converter test that called `dto.setLoanPurpose(...)`:
```java
@Test
void shouldMapLoanTermWithoutLoanPurposeFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setCustName("张三");
dto.setGuarType("信用");
dto.setApplyAmt("100000");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertNull(workflow.getLoanPurpose());
assertNull(workflow.getBizProof());
assertEquals("3", workflow.getLoanTerm());
}
```
In `shouldInvokePersonalModelWithExpectedParams`, remove:
```java
workflow.setLoanPurpose("business");
workflow.setBizProof("true");
```
and remove these argument matcher clauses:
```java
&& Objects.equals("business", dto.getLoanPurpose())
&& Objects.equals("1", dto.getBizProof())
```
Keep the existing `loanTerm`, `loanLoop`, `collThirdParty`, and `collType` assertions.
- [ ] **Step 3: Add failing business type and collateral validation tests**
Add tests like:
```java
@Test
void shouldRejectOldBusinessType() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setBusinessType("新客");
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("业务种类必须是:新增、存量新增、存量转贷之一", ex.getMessage());
}
@Test
void shouldRequireHistoryRateForStockTransfer() {
LoanPricingWorkflow workflow = validCorporateWorkflow();
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory(null);
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("请选择历史贷款合同", ex.getMessage());
}
@Test
void shouldRequireCouponRateForCertificatePledge() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setGuarType("质押");
workflow.setCollType("存单质押");
workflow.setCouponRate(null);
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("存单票面利率不能为空", ex.getMessage());
}
```
Add helper methods:
```java
private LoanPricingWorkflow validPersonalWorkflow() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustType("个人");
workflow.setCustIsn("P001");
workflow.setCustName("张三");
workflow.setIdNum("330102199001011234");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setBusinessType("新增");
return workflow;
}
private LoanPricingWorkflow validCorporateWorkflow() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustType("企业");
workflow.setCustIsn("C001");
workflow.setCustName("测试企业");
workflow.setIdNum("91330100MA0000000X");
workflow.setGuarType("信用");
workflow.setApplyAmt("1000000");
workflow.setBusinessType("新增");
return workflow;
}
```
- [ ] **Step 4: Add failing allowed-option tests**
Add focused tests:
```java
@Test
void shouldAllowPersonalMortgageLineType() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setGuarType("抵押");
workflow.setCollType("一线");
when(sensitiveFieldCryptoService.encrypt(any())).thenAnswer(invocation -> invocation.getArgument(0));
loanPricingWorkflowService.createLoanPricing(workflow);
verify(loanPricingWorkflowMapper).insert(any());
}
@Test
void shouldRejectCorporateMortgageTypeForPersonalMortgage() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setGuarType("抵押");
workflow.setCollType("排污权抵押");
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("个人抵押抵质押类型必须是:一线、一类、二类、三类之一", ex.getMessage());
}
@Test
void shouldAllowCorporatePledgeEquityType() {
LoanPricingWorkflow workflow = validCorporateWorkflow();
workflow.setGuarType("质押");
workflow.setCollType("股权质押");
when(sensitiveFieldCryptoService.encrypt(any())).thenAnswer(invocation -> invocation.getArgument(0));
loanPricingWorkflowService.createLoanPricing(workflow);
verify(loanPricingWorkflowMapper).insert(any());
}
```
- [ ] **Step 5: Add failing converter test**
Create `LoanPricingConverterTest`:
```java
package com.ruoyi.loanpricing.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import org.junit.jupiter.api.Test;
class LoanPricingConverterTest {
@Test
void shouldMapCouponRateForPersonalWorkflow() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("P001");
dto.setGuarType("质押");
dto.setApplyAmt("100000");
dto.setBusinessType("新增");
dto.setCouponRate("2.15");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("2.15", workflow.getCouponRate());
assertNull(workflow.getLoanPurpose());
assertNull(workflow.getBizProof());
}
@Test
void shouldMapCouponRateForCorporateWorkflow() {
CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO();
dto.setCustIsn("C001");
dto.setGuarType("质押");
dto.setApplyAmt("1000000");
dto.setBusinessType("新增");
dto.setCouponRate("2.35");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("2.35", workflow.getCouponRate());
}
}
```
- [ ] **Step 6: Add failing model DTO test**
In `LoanPricingModelServiceTest`, add a test or extend the existing argument-captor test to assert:
```java
assertEquals("2.15", capturedModelInvokeDto.getCouponRate());
```
The workflow used by that test must set:
```java
loanPricingWorkflow.setCouponRate("2.15");
loanPricingWorkflow.setBusinessType("新增");
```
- [ ] **Step 7: Run backend tests and confirm failure**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: FAIL because `couponRate` and new validation are not implemented yet.
## Task 2: Implement DTO, Entity, Converter, and Model DTO Fields
**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/domain/dto/CorporateLoanPricingCreateDTO.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- [ ] **Step 1: Update personal create DTO**
In `PersonalLoanPricingCreateDTO`:
- Remove the `loanPurpose` field and its validation annotations.
- Remove the `bizProof` field.
- Change `businessType` annotations to:
```java
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "新增", allowableValues = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
private String businessType;
```
- Add:
```java
@Schema(description = "存单票面利率", example = "2.15")
private String couponRate;
```
- [ ] **Step 2: Update corporate create DTO**
In `CorporateLoanPricingCreateDTO`:
```java
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "新增", allowableValues = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
private String businessType;
```
Change `collType` annotation to include the combined corporate option set:
```java
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"})
@Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型不符合当前客户类型和担保方式")
private String collType;
```
Add:
```java
@Schema(description = "存单票面利率", example = "2.35")
private String couponRate;
```
- [ ] **Step 3: Update workflow entity**
In `LoanPricingWorkflow`:
```java
/** 业务种类: 新增/存量新增/存量转贷 */
private String businessType;
/** 存单票面利率 */
private String couponRate;
```
Keep `loanPurpose` and `bizProof` on the entity for historical rows and existing schema compatibility; they are no longer populated by personal create DTO conversion.
- [ ] **Step 4: Update model DTO**
In `ModelInvokeDTO`, add:
```java
/**
* 存单票面利率
*/
private String couponRate;
```
Do not add `businessType` in this task.
- [ ] **Step 5: Update converter**
In personal conversion, remove:
```java
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setBizProof(dto.getBizProof());
```
Add for both personal and corporate conversion:
```java
entity.setCouponRate(dto.getCouponRate());
```
- [ ] **Step 6: Run targeted converter and model tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: personal parameter, converter, and model field tests PASS; service validation tests may still fail until Task 3.
- [ ] **Step 7: Commit field and converter changes**
```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/domain/dto/CorporateLoanPricingCreateDTO.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.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/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java
git commit -m "调整上虞利率定价后端字段"
```
## Task 3: Implement Service-Layer Validation
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: Replace business type validation**
Update `validateBusinessTypeAndHistoryRate`:
```java
private void validateBusinessTypeAndHistoryRate(LoanPricingWorkflow workflow) {
if (!StringUtils.hasText(workflow.getBusinessType())) {
throw new ServiceException("业务种类不能为空");
}
if (!isOneOf(workflow.getBusinessType(), "新增", "存量新增", "存量转贷")) {
throw new ServiceException("业务种类必须是:新增、存量新增、存量转贷之一");
}
if ("存量转贷".equals(workflow.getBusinessType())
&& !StringUtils.hasText(workflow.getLoanRateHistory())) {
throw new ServiceException("请选择历史贷款合同");
}
}
```
- [ ] **Step 2: Add collateral and coupon validators**
Add calls before insert:
```java
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
validateCollateralType(loanPricingWorkflow);
validateCouponRate(loanPricingWorkflow);
```
Add helper methods:
```java
private void validateCouponRate(LoanPricingWorkflow workflow) {
if ("质押".equals(workflow.getGuarType())
&& "存单质押".equals(workflow.getCollType())
&& !StringUtils.hasText(workflow.getCouponRate())) {
throw new ServiceException("存单票面利率不能为空");
}
}
private void validateCollateralType(LoanPricingWorkflow workflow) {
if (!"抵押".equals(workflow.getGuarType()) && !"质押".equals(workflow.getGuarType())) {
return;
}
if (!StringUtils.hasText(workflow.getCollType())) {
throw new ServiceException("请选择抵质押类型");
}
if ("个人".equals(workflow.getCustType()) && "抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "一线", "一类", "二类", "三类")) {
throw new ServiceException("个人抵押抵质押类型必须是:一线、一类、二类、三类之一");
}
if ("个人".equals(workflow.getCustType()) && "质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "其他质押")) {
throw new ServiceException("个人质押抵质押类型必须是:存单质押、其他质押之一");
}
if ("企业".equals(workflow.getCustType()) && "抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押")) {
throw new ServiceException("企业抵押抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押之一");
}
if ("企业".equals(workflow.getCustType()) && "质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "股权质押", "其他质押")) {
throw new ServiceException("企业质押抵质押类型必须是:存单质押、股权质押、其他质押之一");
}
}
private boolean isOneOf(String value, String... allowedValues) {
for (String allowedValue : allowedValues) {
if (allowedValue.equals(value)) {
return true;
}
}
return false;
}
```
- [ ] **Step 3: Run service tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 4: Run backend targeted suite**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 5: Commit validation changes**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "增加上虞利率定价创建校验"
```
## Task 4: Update SQL Schema Files
**Files:**
- Create: `sql/add_coupon_rate_20260511.sql`
- Modify: `sql/loan_pricing_workflow.sql`
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 1: Create migration SQL**
Create `sql/add_coupon_rate_20260511.sql`:
```sql
-- 上虞利率定价存单票面利率字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`;
```
- [ ] **Step 2: Update standalone workflow schema**
In `sql/loan_pricing_workflow.sql`, add after `loan_rate_history`:
```sql
`coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率',
```
- [ ] **Step 3: Update bundled schema files**
Apply the same column to the `loan_pricing_workflow` table definitions in:
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 4: Verify SQL references**
Run:
```bash
rg -n "coupon_rate|存单票面利率" sql
```
Expected: `coupon_rate` appears in the migration and all three schema/init files.
- [ ] **Step 5: Commit SQL changes**
```bash
git add sql/add_coupon_rate_20260511.sql \
sql/loan_pricing_workflow.sql \
sql/loan_pricing_schema_20260328.sql \
sql/loan_pricing_prod_init_20260331.sql
git commit -m "新增存单票面利率数据库字段"
```
## Task 5: Backend Verification and Record
**Files:**
- Create or Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`
- [ ] **Step 1: Run backend verification**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 2: Check staged-independent status**
Run:
```bash
git status --short
```
Expected: only intended backend and SQL files are modified or staged for this implementation slice. Existing unrelated files such as `AGENTS.md`, `.DS_Store`, and operation-manual docs must stay out of these commits.
- [ ] **Step 3: Update implementation record**
Add backend notes to `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`:
```markdown
## 后端实现
- 调整个人/企业创建 DTO 的业务种类口径为 `新增/存量新增/存量转贷`
- 新增 `couponRate` 存单票面利率字段,并贯通流程保存和模型入参。
- 取消对私创建链路对 `loanPurpose``bizProof` 的依赖。
- 增加服务层校验,覆盖业务种类、历史贷款利率、抵质押类型和存单票面利率。
- 新增 `coupon_rate` 数据库字段及 schema/init SQL。
## 后端验证
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
```
- [ ] **Step 4: Commit backend record**
```bash
git add doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md
git commit -m "记录上虞字段调整后端实现"
```

View File

@@ -1,485 +0,0 @@
# Shangyu Pricing Field Adjustment Frontend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not invoke `using-superpowers` or subagents during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Update the personal and corporate workflow create dialogs so the displayed fields, dynamic options, required `couponRate`, and submitted payload match the approved Shangyu pricing spec.
**Architecture:** Keep the existing Vue 2 create-dialog components and static Node assertion tests. Update each dialog in place, using computed properties for customer-type-specific collateral options and the `质押 + 存单质押` coupon-rate condition; verify with static tests, production build, and a real Playwright browser check.
**Tech Stack:** Vue 2, Element UI, RuoYi request wrapper, Node static tests, npm scripts, nvm-controlled frontend runtime, Playwright real browser verification.
---
## File Structure
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Change `businessType` options.
- Remove `loanPurpose` and `bizProof` UI, form fields, rules, reset values, and submit payload handling.
- Change personal `collateralTypeOptions`.
- Add `couponRate` conditional field and submit cleanup.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Change `businessType` options.
- Change corporate `collateralTypeOptions`.
- Add `couponRate` conditional field and submit cleanup.
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- Update business-type assertions from `新客` to `新增`.
- Add `couponRate` assertions.
- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js`
- Assert personal removed fields are absent.
- Assert personal collateral options match the new spec.
- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js`
- Assert corporate collateral options match the new spec.
- Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`
- Add frontend implementation and Playwright verification notes after execution.
## Task 1: Update Frontend Static Assertions First
**Files:**
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js`
- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js`
- [ ] **Step 1: Update business-type assertions**
In `business-type-history-rate.test.js`, replace old assertions for `新客` with:
```js
;[
['个人新增弹窗', personalCreate],
['企业新增弹窗', corporateCreate]
].forEach(([name, source]) => {
assert(source.includes('label="业务种类"'), `${name} 缺少业务种类`)
assert(source.includes('value="新增"'), `${name} 缺少新增选项`)
assert(source.includes('value="存量新增"'), `${name} 缺少存量新增选项`)
assert(source.includes('value="存量转贷"'), `${name} 缺少存量转贷选项`)
assert(!source.includes('value="新客"'), `${name} 仍保留旧业务种类 新客`)
assert(source.includes("if (value === '存量转贷')"), `${name} 未保持仅存量转贷查询历史合同`)
})
```
- [ ] **Step 2: Add coupon-rate assertions**
In the same test file, add:
```js
;[
['个人新增弹窗', personalCreate],
['企业新增弹窗', corporateCreate]
].forEach(([name, source]) => {
assert(source.includes('label="存单票面利率"'), `${name} 缺少存单票面利率字段`)
assert(source.includes('prop="couponRate"'), `${name} 缺少 couponRate prop`)
assert(source.includes('isCertificatePledge'), `${name} 缺少存单质押条件计算`)
assert(source.includes("this.form.guarType === '质押'") && source.includes("this.form.collType === '存单质押'"), `${name} couponRate 条件不正确`)
assert(source.includes('delete data.couponRate'), `${name} 未在非适用条件删除 couponRate`)
assert(source.includes('存单票面利率不能为空'), `${name} 缺少 couponRate 必填提示`)
})
```
This shared `business-type-history-rate.test.js` is expected to remain failing until both personal and corporate dialogs are updated. Do not use it as a passing checkpoint after only the personal dialog is changed.
- [ ] **Step 3: Update personal-field assertions**
In `personal-create-input-params.test.js`, replace old required assertions for `loanPurpose` and `bizProof` with absence checks:
```js
assert(
!personalCreateDialog.includes('label="贷款用途"') &&
!personalCreateDialog.includes('prop="loanPurpose"') &&
!personalCreateDialog.includes('loanPurpose:'),
'个人新增弹窗不应继续保留贷款用途字段'
)
assert(
!personalCreateDialog.includes('label="是否有经营佐证"') &&
!personalCreateDialog.includes('prop="bizProof"') &&
!personalCreateDialog.includes('bizProof:'),
'个人新增弹窗不应继续保留是否有经营佐证字段'
)
```
Update personal collateral assertion:
```js
assert(
personalCreateDialog.includes("return ['一线', '一类', '二类', '三类']") &&
personalCreateDialog.includes("return ['存单质押', '其他质押']"),
'个人新增弹窗抵质押类型选项未按新口径动态切换'
)
```
- [ ] **Step 4: Update corporate collateral assertions**
In `corporate-create-input-params.test.js`, replace old collateral assertion with:
```js
assert(
corporateCreateDialog.includes("return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']") &&
corporateCreateDialog.includes("return ['存单质押', '股权质押', '其他质押']"),
'企业新增弹窗抵质押类型选项未按新口径动态切换'
)
```
- [ ] **Step 5: Run frontend static tests and confirm failure**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
```
Expected: FAIL because the UI is not updated yet.
## Task 2: Update Personal Create Dialog
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- [ ] **Step 1: Change business type options**
Replace the first option:
```vue
<el-option label="新增" value="新增"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
```
- [ ] **Step 2: Remove personal fields from template**
Delete the `贷款用途` form item and its surrounding column. Delete the `是否有经营佐证` form item. Keep `借款期限(年)` and `循环功能`.
If a row becomes single-column, leave it as a normal `el-row` with one `el-col :span="12"`; do not restructure the whole dialog.
- [ ] **Step 3: Remove personal fields from data, rules, and submit payload**
Remove these from `form` initial state and `reset()`:
```js
loanPurpose: undefined,
bizProof: false,
```
Remove `loanPurpose` from `rules`.
Remove this submit conversion:
```js
bizProof: this.form.bizProof ? '1' : '0',
```
Keep:
```js
loanLoop: this.form.loanLoop ? '1' : '0'
```
- [ ] **Step 4: Update personal collateral options**
Change `collateralTypeOptions()` to:
```js
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一线', '一类', '二类', '三类']
}
if (this.form.guarType === '质押') {
return ['存单质押', '其他质押']
}
return []
}
```
- [ ] **Step 5: Add coupon-rate field**
Add under the collateral row:
```vue
<el-col :span="12" v-if="isCertificatePledge">
<el-form-item label="存单票面利率" prop="couponRate">
<el-input v-model="form.couponRate" placeholder="请输入存单票面利率"/>
</el-form-item>
</el-col>
```
If the row already has two columns, put this field in the same `抵质押信息` area and keep the existing `900px` dialog.
- [ ] **Step 6: Add computed condition and validator**
Add:
```js
isCertificatePledge() {
return this.form.guarType === '质押' && this.form.collType === '存单质押'
}
```
Add a validator in `data()`:
```js
const validateCouponRate = (rule, value, callback) => {
if (this.isCertificatePledge && !value) {
callback(new Error('存单票面利率不能为空'))
return
}
callback()
}
```
Add rule:
```js
couponRate: [
{validator: validateCouponRate, trigger: "blur"}
]
```
- [ ] **Step 7: Add coupon-rate state cleanup**
Add `couponRate: undefined` to `form` initial state and `reset()`.
Add watcher:
```js
'form.collType'() {
this.resetCouponRateIfNotCertificatePledge()
}
```
Add method:
```js
resetCouponRateIfNotCertificatePledge() {
if (!this.isCertificatePledge) {
this.form.couponRate = undefined
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['couponRate'])
}
})
}
}
```
Call it at the end of `resetCollateralFields()`.
- [ ] **Step 8: Add submit cleanup and front-end guard**
In `submitForm`, before `this.submitting = true`, add:
```js
if (this.isCertificatePledge && !this.form.couponRate) {
this.$modal.msgWarning("存单票面利率不能为空")
return
}
```
After collateral handling:
```js
if (!this.isCertificatePledge) {
delete data.couponRate
}
```
- [ ] **Step 9: Run personal static tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params'
```
Expected: PASS. The shared `test:business-type-history-rate` is intentionally not run here because it also asserts corporate changes that are implemented in Task 3.
- [ ] **Step 10: Commit personal frontend changes**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
ruoyi-ui/tests/personal-create-input-params.test.js
git commit -m "调整上虞对私新增字段口径"
```
## Task 3: Update Corporate Create Dialog
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Change business type options**
Use:
```vue
<el-option label="新增" value="新增"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
```
- [ ] **Step 2: Update corporate collateral options**
Change `collateralTypeOptions()` to:
```js
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']
}
if (this.form.guarType === '质押') {
return ['存单质押', '股权质押', '其他质押']
}
return []
}
```
- [ ] **Step 3: Add coupon-rate field and state**
Mirror the personal dialog implementation:
```vue
<el-col :span="12" v-if="isCertificatePledge">
<el-form-item label="存单票面利率" prop="couponRate">
<el-input v-model="form.couponRate" placeholder="请输入存单票面利率"/>
</el-form-item>
</el-col>
```
Add:
```js
couponRate: undefined
```
to initial `form` and `reset()`.
- [ ] **Step 4: Add computed condition, validator, watcher, and submit cleanup**
Use the same names as personal dialog:
```js
isCertificatePledge() {
return this.form.guarType === '质押' && this.form.collType === '存单质押'
}
```
Use the same `validateCouponRate`, `couponRate` rule, `form.collType` watcher, `resetCouponRateIfNotCertificatePledge`, front-end guard, and:
```js
if (!this.isCertificatePledge) {
delete data.couponRate
}
```
- [ ] **Step 5: Run corporate static tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:corporate-create-input-params && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 6: Commit corporate frontend changes**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
ruoyi-ui/tests/corporate-create-input-params.test.js \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "调整上虞对公新增字段口径"
```
## Task 4: Frontend Build and Real Page Verification
**Files:**
- Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`
- [ ] **Step 1: Run all related static tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
```
Expected: PASS.
- [ ] **Step 2: Run production build**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```
Expected: PASS and `ruoyi-ui/dist` generated.
- [ ] **Step 3: Start the local app stack**
Use the repository's existing startup scripts if available. If a frontend dev server is needed, run it with Node controlled by `nvm`:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev'
```
Record the PID and URL. If the backend also needs restart, use the existing repository backend restart script rather than inventing a new start path.
- [ ] **Step 4: Use Playwright on the real page**
Open the actual local workflow page, not a prototype. Verify:
- Personal create dialog:
- `业务种类` shows `新增/存量新增/存量转贷`.
- `新客` is absent.
- `贷款用途` is absent.
- `是否有经营佐证` is absent.
- `抵押` shows `一线/一类/二类/三类`.
- `质押 + 存单质押` shows `存单票面利率`.
- Leaving `存单票面利率` empty blocks submit.
- Corporate create dialog:
- `业务种类` shows `新增/存量新增/存量转贷`.
- `抵押` shows `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`.
- `质押` shows `存单质押/股权质押/其他质押`.
- `质押 + 存单质押` shows `存单票面利率`.
- Leaving `存单票面利率` empty blocks submit.
- History-rate behavior:
- `存量转贷` triggers historical-contract query.
- `新增` and `存量新增` do not trigger historical-contract query.
- [ ] **Step 5: Stop test processes**
Stop any frontend/backend processes started in this task. Verify with:
```bash
ps -ef | rg 'vue-cli-service|ruoyi-admin|RuoYiApplication'
```
Expected: no leftover processes from this test run.
- [ ] **Step 6: Update implementation record**
Append:
```markdown
## 前端实现
- 调整个人/企业新增弹窗业务种类选项为 `新增/存量新增/存量转贷`
- 个人新增弹窗剔除 `loanPurpose``bizProof`
- 按客户类型和担保方式调整抵质押类型选项。
- 新增 `质押 + 存单质押` 下的 `couponRate` 存单票面利率字段、必填校验和提交清理。
## 前端验证
- `npm --prefix ruoyi-ui run test:business-type-history-rate`
- `npm --prefix ruoyi-ui run test:personal-create-input-params`
- `npm --prefix ruoyi-ui run test:corporate-create-input-params`
- `npm --prefix ruoyi-ui run build:prod`
- Playwright 真实页面验证:通过
```
- [ ] **Step 7: Commit frontend verification record**
```bash
git add doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md
git commit -m "记录上虞字段调整前端验证"
```

View File

@@ -1,284 +0,0 @@
# 业务种类与历史贷款利率设计
## 背景
利率定价流程新增时,当前页面已经支持先选择客户类型,再通过客户号查询选择客户内码,最后打开个人或企业新增弹窗。新需求要求在新增流程时增加两个字段:
- 业务种类
- 历史贷款利率
字段和接口规则以 `doc/上虞_客户内码客户_历史利率_映射表.xlsx` 的“历史贷款合同查询选择”sheet 以及 `doc/利率定价接口.txt` 的历史合同查询接口为准。
## 已确认规则
- 个人客户和企业客户新增流程同时增加业务种类和历史贷款利率。
- 业务种类选项为:新客、存量新增、存量转贷。
- 业务种类需要保存到流程表,并在详情页展示。
- 业务种类只用于前端判断是否触发历史合同查询,不作为模型入参上传。
- 仅当业务种类选择“存量转贷”时,查询历史贷款合同。
- 存量转贷必须选择一条历史合同记录;未查询到历史合同、未选择历史合同或历史贷款利率为空时,禁止提交。
- 历史贷款利率需要保存到流程表。
- 发起利率定价的模型调用入参需要新增历史贷款利率字段。
- 历史合同查询列表展示 Excel 中“历史贷款合同查询选择”sheet 的全部返回字段,并由用户单选一条。
## 推荐方案
采用后端代理历史合同查询、前端弹窗单选的方案。
前端负责业务种类交互、触发查询、展示历史合同列表、单选回填历史贷款利率和提交前拦截。后端负责代理外部历史合同接口、统一错误处理、保存新增字段,以及在模型调用中传递历史贷款利率。
不采用前端直接调用外部网关地址的方案,避免把外部地址和 `appCode` 暴露到前端,也避免错误处理分散。
不采用进入新增弹窗前先查询历史合同的方案,因为只有存量转贷需要历史合同,前置查询会让新客和存量新增多走不必要步骤。
## 数据流
新增流程保持现有主路径:
1. 列表页点击新增。
2. 选择个人客户或企业客户。
3. 客户号查询并选择客户内码。
4. 打开个人或企业新增弹窗。
5. 在贷款信息中选择业务种类。
6. 若业务种类为新客或存量新增,直接填写其他字段并提交。
7. 若业务种类为存量转贷,前端用当前客户内码查询历史合同。
8. 前端弹出历史贷款合同选择列表。
9. 用户单选一条历史合同。
10. 前端回填该行历史贷款利率。
11. 提交创建接口。
12. 后端保存业务种类和历史贷款利率。
13. 后端调用模型时只把历史贷款利率带入模型请求。
## 历史合同查询接口设计
新增后端业务接口:
```text
GET /loanPricing/workflow/history-contract?custIsn=...
```
接口职责:
1. 校验 `custIsn` 非空。
2. 读取历史合同外部接口配置。
3. 使用 GET 调用外部接口,请求参数名为 `cust_isn`
4. 解析外部接口返回列表。
5. 返回给前端的字段保持下划线命名。
新增配置:
```yaml
loan-rate-history:
url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07
```
配置值只保存接口基址和固定 `appCode`,不包含 `cust_isn=`。代码统一通过 `UriComponentsBuilder.queryParam("cust_isn", custIsn)` 追加客户内码,避免重复参数或拼接错误。
开发和测试环境可指向本项目 mock 接口;生产配置使用真实外部接口地址。
## 历史合同返回字段
新增 `HistoryLoanContractVO`JSON 字段按 Excel 保持下划线命名:
| 字段 | 名称 | 用途 |
| --- | --- | --- |
| `cust_isn` | 客户内码 | 前端展示 |
| `loan_contract_history` | 历史贷款合同号 | 前端展示 |
| `guar_type_history` | 历史贷款担保方式 | 前端展示 |
| `product_code_history` | 历史贷款产品代码 | 前端展示 |
| `loan_rate_history` | 历史贷款利率 | 前端展示,用户选择后作为后续参数 |
| `loan_amount_history` | 历史贷款金额 | 前端展示 |
| `loan_sign_date_history` | 历史贷款签订时间 | 前端展示 |
## 创建接口和数据模型
个人创建 DTO `PersonalLoanPricingCreateDTO` 新增:
- `businessType`
- `loanRateHistory`
企业创建 DTO `CorporateLoanPricingCreateDTO` 新增:
- `businessType`
- `loanRateHistory`
校验规则:
- `businessType` 必填。
- `businessType` 只能是新客、存量新增、存量转贷。
- `businessType=存量转贷` 时,`loanRateHistory` 必填。
- `businessType` 为新客或存量新增时,不要求 `loanRateHistory`
流程实体 `LoanPricingWorkflow` 新增:
- `businessType`
- `loanRateHistory`
转换器 `LoanPricingConverter` 需要同步更新:
- `toEntity(PersonalLoanPricingCreateDTO dto)` 映射 `businessType``loanRateHistory`
- `toEntity(CorporateLoanPricingCreateDTO dto)` 映射 `businessType``loanRateHistory`
这两个字段必须先写入 `LoanPricingWorkflow`,后续详情展示和模型入参才会闭环。
数据库 `loan_pricing_workflow` 新增:
```sql
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER `loan_purpose`,
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER `business_type`;
```
需要同步更新独立 schema SQL 和生产初始化 SQL 中的 `loan_pricing_workflow` 表结构。
跨字段校验放在创建服务层统一处理,不只依赖 DTO 注解:
- 创建个人流程前校验业务种类。
- 创建企业流程前校验业务种类。
- `businessType` 必须是新客、存量新增、存量转贷之一。
- `businessType=存量转贷` 时,`loanRateHistory` 必须非空。
- 校验通过后才允许入库和触发模型调用。
这样可以拦截绕过前端直接调用创建接口的请求。
## 模型入参
模型入参 `ModelInvokeDTO` 新增:
- `loanRateHistory`
`LoanPricingModelService` 仍按现有方式从 `LoanPricingWorkflow` 复制属性到 `ModelInvokeDTO`。由于 `LoanPricingWorkflow``ModelInvokeDTO` 字段同名,历史贷款利率会进入模型请求。
`businessType` 不添加到 `ModelInvokeDTO`,不进入模型请求。
`ModelService` 仍使用现有 `application/x-www-form-urlencoded` 请求方式,不调整模型调用协议。
## 前端交互
个人和企业新增弹窗都在“贷款信息”区域增加:
- 业务种类:下拉框,选项为新客、存量新增、存量转贷。
- 历史贷款利率:只读输入框,仅当业务种类为存量转贷时展示或启用。
业务种类选择逻辑:
- 选择新客:清空已选历史合同和历史贷款利率。
- 选择存量新增:清空已选历史合同和历史贷款利率。
- 选择存量转贷:使用当前 `custIsn` 查询历史贷款合同,并打开历史合同选择弹窗。
历史贷款合同选择弹窗:
- 宽度沿用客户号查询弹窗的 80%。
- 表格首列为单选框。
- 表格展示客户内码、历史贷款合同号、历史贷款担保方式、历史贷款产品代码、历史贷款利率、历史贷款金额、历史贷款签订时间。
- 用户选择一条记录后点击确定。
- 确定后关闭弹窗,并将该行 `loan_rate_history` 回填到新增表单的 `loanRateHistory`
提交逻辑:
- 业务种类始终必填。
- 存量转贷时必须已选择历史合同。
- 存量转贷时 `loanRateHistory` 为空则禁止提交。
- 新客和存量新增提交时不需要历史贷款利率。
## 详情展示
个人详情和企业详情都需要展示:
- 业务种类
- 历史贷款利率
展示位置放在现有贷款信息或基础信息区域,字段值来自 `loanPricingWorkflow.businessType``loanPricingWorkflow.loanRateHistory`
若历史贷款利率为空,详情页按现有详情页空值展示方式显示。
## 异常处理
- 前端业务种类为空:提示“请选择业务种类”。
- 存量转贷未选择历史合同:提示“请选择历史贷款合同”,禁止提交。
- 存量转贷历史合同返回空列表:提示“未查询到历史贷款合同”,禁止提交。
- 历史合同查询失败:展示后端返回错误,保留当前新增弹窗,不提交。
- 后端历史合同接口 `custIsn` 为空:返回“客户内码不能为空”。
- 外部历史合同接口无返回或返回错误:后端抛出业务异常。
- 创建接口收到存量转贷但历史贷款利率为空:后端返回业务错误。
## Mock 接口
为本地开发和真实页面测试新增历史合同 mock
```text
GET /rate/pricing/mock/history-contract?cust_isn=...
```
mock 接口返回 1 条或多条历史合同记录,字段使用历史合同返回字段中的下划线名称。
mock 需要提供固定测试场景:
- 常规 `cust_isn` 返回 1 条或多条带 `loan_rate_history` 的历史合同。
- 固定 `cust_isn` 返回空列表,用于验证“未查询到历史贷款合同”。
- 固定 `cust_isn` 返回记录但 `loan_rate_history` 为空,用于验证前端和后端禁止提交。
开发和测试 profile 的 `loan-rate-history.url` 可配置为本项目 mock 地址。
## 测试方案
### 后端验证
- 历史合同查询接口正常返回下划线字段。
- 历史合同查询接口缺少 `custIsn` 时返回错误。
- 历史合同外部接口返回空列表时,前端可收到空列表并拦截提交。
- 个人创建接口 `businessType=存量转贷` 且缺少 `loanRateHistory` 时返回错误。
- 企业创建接口 `businessType=存量转贷` 且缺少 `loanRateHistory` 时返回错误。
- 个人创建接口 `businessType` 缺失时返回错误。
- 企业创建接口 `businessType` 缺失时返回错误。
- 个人创建接口 `businessType` 非法值时返回错误。
- 企业创建接口 `businessType` 非法值时返回错误。
- 个人创建接口 `businessType=新客` 且无 `loanRateHistory` 时正常保存。
- 企业创建接口 `businessType=新客` 且无 `loanRateHistory` 时正常保存。
- 个人创建接口 `businessType=存量新增` 且无 `loanRateHistory` 时正常保存。
- 企业创建接口 `businessType=存量新增` 且无 `loanRateHistory` 时正常保存。
- 个人创建接口 `businessType=存量转贷``loanRateHistory` 为空字符串时返回错误。
- 企业创建接口 `businessType=存量转贷``loanRateHistory` 为空字符串时返回错误。
- 个人创建接口带业务种类和历史贷款利率时正常保存。
- 企业创建接口带业务种类和历史贷款利率时正常保存。
- 模型调用请求包含 `loanRateHistory`
- 模型调用请求不包含 `businessType`
### 前端验证
- 个人新增弹窗展示业务种类和历史贷款利率。
- 企业新增弹窗展示业务种类和历史贷款利率。
- 业务种类为新客时不触发历史合同查询。
- 业务种类为存量新增时不触发历史合同查询。
- 业务种类为存量转贷时触发历史合同查询。
- 历史合同弹窗展示全部 7 个返回字段。
- 历史合同弹窗只允许单选一条。
- 选择历史合同后回填历史贷款利率。
- 历史合同返回空列表时提示“未查询到历史贷款合同”,并禁止提交。
- 历史合同返回记录但 `loan_rate_history` 为空时,不能把空值作为有效选择提交。
- 业务种类从存量转贷切换到新客或存量新增时,清空历史贷款利率。
- 存量转贷未选择历史合同时禁止提交。
### 真实页面验证
按照项目规则,页面功能开发完成后必须启动前端页面,并用浏览器打开真实利率定价流程页面验证,禁止打开 prototype 页面。
真实页面至少覆盖:
1. 个人新客新增并提交。
2. 个人存量新增新增并提交。
3. 个人存量转贷查询历史合同、单选、回填历史贷款利率、提交、详情展示。
4. 企业新客新增并提交。
5. 企业存量新增新增并提交。
6. 企业存量转贷查询历史合同、单选、回填历史贷款利率、提交、详情展示。
7. 存量转贷不选择历史合同直接提交时被拦截。
测试结束后关闭本次测试启动的前端和后端进程。
## 不在本次范围
- 不修改客户号与客户内码映射流程。
- 不改变模型调用协议。
- 不把业务种类作为模型入参。
- 不新增历史合同落库明细表。
- 不保存完整历史合同记录,只保存用户选择后的历史贷款利率。

View File

@@ -1,164 +0,0 @@
# 客户号查询选择客户内码设计
## 背景
在利率定价流程新增时,现有页面流程为:
1. 列表页点击“新增”。
2. 选择个人客户或企业客户。
3. 直接打开对应新增弹窗。
4. 用户手工输入客户内码和客户名称。
新需求要求在选择个人或企业后,先根据客户号调用映射接口查询客户信息,由用户从返回结果中选择一条客户内码,再自动带入新增弹窗。映射接口请求和返回字段以 `doc/上虞_客户内码客户_历史利率_映射表.xlsx` 的“客户号与客户内码映射”sheet 为准。
本次设计只覆盖客户号映射查询、选择和自动带入,不调整模型测算接口调用逻辑,不新增数据库字段,不改变现有流程创建接口的请求结构。
## 已确认规则
- 个人和企业分别有客户号映射接口地址。
- 当前先在本项目内新增两个 mock 接口,不直接调用生产网关。
- 配置文件新增客户号映射接口地址,当前配置指向本项目 mock 接口。
- mock 接口随机返回 1 条或多条客户映射数据。
- 后端业务接口返回给前端的字段保持下划线命名。
- 用户选择客户映射记录后,新增弹窗自动填入客户内码和客户名称。
- 新增弹窗中的客户内码和客户名称改为只读。
- 其余新增流程字段和提交接口保持现状。
## 接口字段
客户号映射请求参数:
| 字段 | 名称 |
| --- | --- |
| `cust_id` | 客户号 |
客户号映射返回字段:
| 字段 | 名称 | 用途 |
| --- | --- | --- |
| `cust_id` | 客户号 | 前端展示 |
| `cust_isn` | 客户内码 | 用户选择后作为流程创建入参 `custIsn` |
| `cust_name` | 客户名称 | 用户选择后作为流程创建入参 `custName` |
| `faith_day` | 用信天数 | 前端展示,辅助判断 |
| `balance_avg` | 存款年日均 | 前端展示,辅助判断 |
| `loan_count_his` | 历史贷款次数 | 前端展示,辅助判断 |
| `last_loan_date` | 上次贷款日期 | 前端展示,辅助判断 |
## 后端设计
新增客户号映射查询业务接口:
- `GET /loanPricing/workflow/customer-map/personal?custId=...`
- `GET /loanPricing/workflow/customer-map/corporate?custId=...`
业务接口职责:
1. 校验 `custId` 非空。
2. 根据个人或企业读取对应配置地址。
3. 使用 GET 调用配置地址,请求 mock 时传参名为 `cust_id`
4. 解析 mock 返回结果。
5. 返回给前端的客户映射数据字段保持下划线,不转 camelCase。
新增 mock 接口:
- `GET /rate/pricing/mock/customer-map/personal?cust_id=...`
- `GET /rate/pricing/mock/customer-map/corporate?cust_id=...`
mock 接口职责:
1. 校验 `cust_id` 非空。
2. 随机生成 1 条或多条客户映射记录。
3. 返回字段使用 `cust_id``cust_isn``cust_name``faith_day``balance_avg``loan_count_his``last_loan_date`
新增配置节点:
```yaml
customer-map:
personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal
corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate
```
配置节点与现有 `model.personal-url``model.corporate-url` 分开,避免客户号映射接口和模型测算接口混用。需要启动的 profile 均应包含该配置,当前统一指向本项目 mock 地址。
## 前端设计
新增一个客户号查询选择弹窗组件,个人和企业共用,通过 `customerType` 区分调用个人或企业查询接口。
弹窗内容:
- 输入框:客户号。
- 按钮:查询。
- 表格列:客户号、客户内码、客户名称、用信天数、存款年日均、历史贷款次数、上次贷款日期。
- 操作列:选择。
新增流程:
1. 列表页点击“新增”。
2. 打开现有客户类型选择弹窗。
3. 用户选择个人客户或企业客户。
4. 打开客户号查询选择弹窗。
5. 用户输入客户号并点击查询。
6. 前端调用对应业务接口。
7. 用户从结果表格选择一条客户映射记录。
8. 前端关闭查询选择弹窗,打开对应新增弹窗。
9. 新增弹窗将 `cust_isn` 带入 `custIsn`,将 `cust_name` 带入 `custName`
10. 新增弹窗中客户内码和客户名称只读。
11. 用户继续填写其余字段并提交。
个人和企业新增弹窗需要增加一个入参,用于接收选中的客户映射记录。弹窗重置时清空本次客户选择,下一次新增必须重新查询选择。
现有创建接口保持不变:
- `POST /loanPricing/workflow/create/personal`
- `POST /loanPricing/workflow/create/corporate`
提交时仍只提交现有流程创建字段,不提交 `faith_day``balance_avg``loan_count_his``last_loan_date`
## 异常处理
- 客户号为空:前端拦截,提示“请输入客户号”。
- 后端业务接口收到空 `custId`:返回参数错误。
- mock 接口收到空 `cust_id`:返回参数错误。
- 映射接口调用失败:后端返回错误,前端展示接口错误提示。
- 映射接口返回空列表:前端提示“未查询到客户信息”。
- 用户没有选择客户映射记录:不打开新增弹窗。
- 新增弹窗中的客户内码、客户名称虽然只读,但保留必填校验,防止异常状态提交空值。
## 测试方案
后端验证:
- 个人客户映射查询接口正常返回下划线字段。
- 企业客户映射查询接口正常返回下划线字段。
- 缺少 `custId` 时返回参数错误。
- mock 个人接口返回随机客户映射列表。
- mock 企业接口返回随机客户映射列表。
- 配置读取个人和企业映射地址,不复用模型测算地址。
前端验证:
- 客户号为空时点击查询,提示“请输入客户号”。
- 个人客户类型下调用个人映射查询接口。
- 企业客户类型下调用企业映射查询接口。
- 查询结果表格展示全部返回字段。
- 选择一条结果后,个人新增弹窗自动带入 `custIsn``custName`,且两项只读。
- 选择一条结果后,企业新增弹窗自动带入 `custIsn``custName`,且两项只读。
- 关闭新增弹窗后再次新增,需要重新查询客户号。
真实页面验证:
1. 启动后端进程,确保最新代码生效。
2. 启动前端页面。
3. 使用浏览器进入利率定价流程列表。
4. 跑通个人客户新增:新增 -> 选择个人 -> 查询客户号 -> 选择客户内码 -> 新增弹窗只读带入 -> 填写其余字段 -> 提交。
5. 跑通企业客户新增:新增 -> 选择企业 -> 查询客户号 -> 选择客户内码 -> 新增弹窗只读带入 -> 填写其余字段 -> 提交。
6. 验证列表或详情中新增流程存在。
7. 测试结束后关闭本次启动的前后端进程。
## 不在本次范围
- 不新增数据库字段。
- 不修改现有流程创建接口请求结构。
- 不把 `faith_day``balance_avg``loan_count_his``last_loan_date` 写入流程表。
- 不调整模型测算个人/企业接口。
- 不直接调用生产网关地址。

View File

@@ -1,231 +0,0 @@
# 上虞利率定价字段口径调整设计
## 背景
当前上虞利率定价新增流程已经具备个人、企业两个新增弹窗,并已有业务种类、历史贷款利率和历史贷款合同选择链路。本次需求是在现有链路上调整字段口径:
- 前端通用业务种类改为 `新增/存量新增/存量转贷`,仅 `存量转贷` 查询历史贷款合同。
- `质押 + 存单质押` 时新增 `couponRate` 存单票面利率,客户经理填写后上传。
- 上虞对私剔除 `loanPurpose` 贷款用途和 `bizProof` 是否有经营佐证。
- 上虞对私、对公按客户类型和担保方式分别调整 `collType` 可选值。
用户已确认本次按页面、创建接口、服务校验、流程保存、模型调用参数全链路同步调整。用户也确认 `couponRate``质押 + 存单质押` 时必须填写。对公 `businessType` 上传模型接口这一条用户已明确要求忽略,因此本设计不新增、不验证 `businessType` 模型入参链路;本次模型入参调整只覆盖 `couponRate`
## 范围
### 本次包含
- 调整个人和企业新增弹窗的 `businessType` 选项。
- 保持 `businessType=存量转贷` 才查询历史贷款合同。
- 新增 `couponRate` 条件展示、必填校验、提交和模型入参传递。
- 移除个人新增弹窗中的 `loanPurpose``bizProof`
- 后端取消个人创建 DTO 对 `loanPurpose``bizProof` 的必填依赖。
- 按个人/企业和抵押/质押分别校验 `collType` 可选值。
- 新增或更新数据库字段定义,使 `couponRate` 能保存到流程表。
- 更新直接相关的前端静态断言和后端单元测试。
### 本次不包含
- 不重做历史贷款合同查询接口。
- 不改历史贷款合同选择弹窗结构。
- 不新增字典配置或字段配置化能力。
- 不处理旧历史数据回填。
- 不新增或验证对公 `businessType` 上传模型逻辑;该条按用户确认忽略。
## 推荐方案
采用全链路最短路径同步方案。
页面负责字段展示、条件清空和提交前整理。后端负责创建 DTO 接收、统一业务校验、流程实体保存和模型入参传递。SQL 同步补充 `coupon_rate` 字段,保证新环境和已有环境都能落库。
不采用只改前端的方式,因为直接调用创建接口会绕过 `couponRate` 必填和 `collType` 口径校验。不采用配置化字段规则,因为本次口径明确,配置化会超出需求范围。
## 前端设计
### 通用业务种类
个人新增弹窗和企业新增弹窗的 `businessType` 选项统一为:
- `新增`
- `存量新增`
- `存量转贷`
交互规则:
- 选择 `存量转贷` 时,沿用现有客户内码查询历史贷款合同逻辑。
- 选择 `新增``存量新增` 时,清空历史合同选择和 `loanRateHistory`
-`存量转贷` 提交前不上传 `loanRateHistory`
### 存单票面利率
个人和企业新增弹窗都新增 `couponRate` 输入框,字段名称展示为 `存单票面利率`
展示和提交规则:
- 仅当 `guarType=质押``collType=存单质押` 时展示。
- 展示时必填。
- 切换担保方式或抵质押类型后,如果不再满足 `质押 + 存单质押`,清空 `couponRate`
- 不满足条件时提交前不上传 `couponRate`
### 上虞对私字段调整
个人新增弹窗移除:
- `贷款用途 loanPurpose`
- `是否有经营佐证 bizProof`
个人提交数据不再包含这两个字段。个人的 `loanLoop``collThirdParty` 等既有字段保持现有处理方式。
个人 `collType` 选项:
- `guarType=抵押``一线/一类/二类/三类`
- `guarType=质押``存单质押/其他质押`
### 上虞对公字段调整
企业 `collType` 选项:
- `guarType=抵押``一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- `guarType=质押``存单质押/股权质押/其他质押`
企业其他既有上传字段保持现状。
## 后端设计
### DTO 和实体
`PersonalLoanPricingCreateDTO`
- `businessType` 允许值改为 `新增/存量新增/存量转贷`
- 移除 `loanPurpose` 必填和枚举校验。
- 移除 `bizProof` 业务依赖。
- 新增 `couponRate`
`CorporateLoanPricingCreateDTO`
- `businessType` 允许值改为 `新增/存量新增/存量转贷`
- 新增 `couponRate`
- `collType` 允许值覆盖对公抵押和质押新口径。
`LoanPricingWorkflow`
- `businessType` 注释更新为 `新增/存量新增/存量转贷`
- 新增 `couponRate` 字段,对应数据库 `coupon_rate`
- 个人剔除字段不再作为本次创建链路必需字段。
`ModelInvokeDTO`
- 新增 `couponRate`,用于模型调用参数。
- 不新增或调整 `businessType` 模型入参;该条按用户确认不纳入本次范围。
`LoanPricingConverter`
- 个人和企业创建 DTO 都映射 `couponRate`
- 个人转换不再依赖 `loanPurpose``bizProof`
### 服务层校验
创建流程统一校验放在 `LoanPricingWorkflowServiceImpl`,确保绕过前端直接调用接口也会被拦截。
校验规则:
- `businessType` 必填,只允许 `新增/存量新增/存量转贷`
- `businessType=存量转贷` 时,`loanRateHistory` 必填。
- `guarType=质押``collType=存单质押` 时,`couponRate` 必填。
- `custType=个人``guarType=抵押` 时,`collType` 只允许 `一线/一类/二类/三类`
- `custType=个人``guarType=质押` 时,`collType` 只允许 `存单质押/其他质押`
- `custType=企业``guarType=抵押` 时,`collType` 只允许 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- `custType=企业``guarType=质押` 时,`collType` 只允许 `存单质押/股权质押/其他质押`
当担保方式不是抵押或质押时,不要求 `collType``couponRate`
## 数据库设计
流程表新增字段:
```sql
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`;
```
需要同步更新:
- 当前迁移 SQL。
- `sql/loan_pricing_workflow.sql`
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
已有历史数据不回填,`coupon_rate` 允许为空。
## 数据流
1. 用户进入利率定价列表,选择新增。
2. 用户选择个人或企业客户,并通过客户号映射选择客户内码。
3. 打开对应新增弹窗。
4. 用户选择业务种类。
5. 若业务种类为 `存量转贷`,前端使用客户内码查询历史贷款合同并回填 `loanRateHistory`
6. 用户选择担保方式和抵质押类型。
7. 若选择 `质押 + 存单质押`,前端展示并要求填写 `couponRate`
8. 前端提交前清理不适用字段。
9. 后端创建接口接收 DTO。
10. 转换器映射为 `LoanPricingWorkflow`
11. 服务层做业务种类、历史利率、抵质押类型和 `couponRate` 校验。
12. 流程表保存。
13. 模型调用前复制流程字段到 `ModelInvokeDTO`,带出 `couponRate``businessType` 模型入参不在本次设计范围内。
## 错误处理
- 业务种类为空:提示 `请选择业务种类` 或后端返回 `业务种类不能为空`
- 业务种类为旧值或非法值:后端返回 `业务种类必须是:新增、存量新增、存量转贷之一`
- `存量转贷` 未选择历史贷款合同:提示或返回 `请选择历史贷款合同`
- `质押 + 存单质押` 未填写 `couponRate`:提示或返回 `存单票面利率不能为空`
- `collType` 不符合当前客户类型和担保方式:后端返回对应可选值错误。
## 测试设计
### 前端静态断言
更新或新增前端测试脚本,覆盖:
- 个人和企业新增弹窗都包含 `新增/存量新增/存量转贷`
- 个人和企业新增弹窗不再包含旧选项 `新客`
- 个人新增弹窗不再展示 `贷款用途``是否有经营佐证`
- 个人抵押选项为 `一线/一类/二类/三类`
- 个人质押选项包含 `存单质押/其他质押`
- 企业抵押选项为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- 企业质押选项为 `存单质押/股权质押/其他质押`
- `couponRate` 仅在 `质押 + 存单质押` 条件下展示、必填、提交。
- 非适用条件提交前删除 `couponRate`
### 后端单元测试
更新 `LoanPricingWorkflowServiceImplTest` 或新增直接相关测试,覆盖:
- `businessType=新增` 通过。
- `businessType=新客` 拒绝。
- `businessType=存量转贷` 且缺少 `loanRateHistory` 拒绝。
- `guarType=质押``collType=存单质押` 且缺少 `couponRate` 拒绝。
- 个人抵押允许 `一线/一类/二类/三类`,拒绝对公专属抵押值。
- 企业抵押允许 `排污权抵押/设备等其他不动产抵押`
- 企业质押允许 `股权质押/其他质押`
### 实际页面验证
实现完成后按项目规则验证:
- 使用 `nvm` 控制前端 Node 版本。
- 启动前端页面并用 Playwright 打开真实页面,不使用 prototype。
- 验证个人新增弹窗字段移除、业务种类选项、抵押/质押选项和 `couponRate` 条件展示。
- 验证企业新增弹窗业务种类选项、抵押/质押选项和 `couponRate` 条件展示。
- 验证 `存量转贷` 仍会触发历史合同查询,`新增/存量新增` 不触发。
- 测试结束关闭本次启动的前后端进程。
## 验收标准
- 页面字段和选项与本设计一致。
- 创建接口不能接受旧业务种类 `新客`
- `质押 + 存单质押` 未填 `couponRate` 时前后端都不能提交成功。
- 不适用 `couponRate` 时不会上传该字段。
- `couponRate` 能保存到流程表并进入模型调用参数。
- 对私不再要求或上传 `loanPurpose``bizProof`
- 前端静态断言、后端单元测试和真实页面验证通过。

513
pom.xml
View File

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

BIN
ruoyi-admin/.DS_Store vendored

Binary file not shown.

View File

@@ -1,73 +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>
<repositories>
<repository>
<id>tongweb-releases</id>
<name>TongWeb Maven Releases</name>
<url>https://mvn.elitescloud.com/nexus/repository/maven-releases/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!-- spring-boot-devtools -->
<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>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</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>
@@ -78,56 +65,38 @@
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-loan-pricing</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.tongweb.springboot</groupId>
<artifactId>tongweb-spring-boot-starter-3.x</artifactId>
<version>7.0.E.7</version>
</dependency>
<dependency>
<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>

View File

@@ -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" +
" ''-' `'-' `-..-' ");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -1,83 +1,83 @@
package com.ruoyi.web.controller.monitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysUserOnline;
import com.ruoyi.system.service.ISysUserOnlineService;
/**
* 在线用户监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/online")
public class SysUserOnlineController extends BaseController
{
@Autowired
private ISysUserOnlineService userOnlineService;
@Autowired
private RedisCache redisCache;
@PreAuthorize("@ss.hasPermi('monitor:online:list')")
@GetMapping("/list")
public TableDataInfo list(String ipaddr, String userName)
{
Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
for (String key : keys)
{
LoginUser user = redisCache.getCacheObject(key);
if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
{
userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
}
else if (StringUtils.isNotEmpty(ipaddr))
{
userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
}
else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
{
userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
}
else
{
userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
}
}
Collections.reverse(userOnlineList);
userOnlineList.removeAll(Collections.singleton(null));
return getDataTable(userOnlineList);
}
/**
* 强退用户
*/
@PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
@Log(title = "在线用户", businessType = BusinessType.FORCE)
@DeleteMapping("/{tokenId}")
public AjaxResult forceLogout(@PathVariable String tokenId)
{
redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
return success();
}
}
package com.ruoyi.web.controller.monitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysUserOnline;
import com.ruoyi.system.service.ISysUserOnlineService;
/**
* 在线用户监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/online")
public class SysUserOnlineController extends BaseController
{
@Autowired
private ISysUserOnlineService userOnlineService;
@Autowired
private RedisCache redisCache;
@PreAuthorize("@ss.hasPermi('monitor:online:list')")
@GetMapping("/list")
public TableDataInfo list(String ipaddr, String userName)
{
Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
for (String key : keys)
{
LoginUser user = redisCache.getCacheObject(key);
if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
{
userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
}
else if (StringUtils.isNotEmpty(ipaddr))
{
userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
}
else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
{
userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
}
else
{
userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
}
}
Collections.reverse(userOnlineList);
userOnlineList.removeAll(Collections.singleton(null));
return getDataTable(userOnlineList);
}
/**
* 强退用户
*/
@PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
@Log(title = "在线用户", businessType = BusinessType.FORCE)
@DeleteMapping("/{tokenId}")
public AjaxResult forceLogout(@PathVariable String tokenId)
{
redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
return success();
}
}

View File

@@ -1,133 +1,133 @@
package com.ruoyi.web.controller.system;
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.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.SysConfig;
import com.ruoyi.system.service.ISysConfigService;
/**
* 参数配置 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/config")
public class SysConfigController extends BaseController
{
@Autowired
private ISysConfigService configService;
/**
* 获取参数配置列表
*/
@PreAuthorize("@ss.hasPermi('system:config:list')")
@GetMapping("/list")
public TableDataInfo list(SysConfig config)
{
startPage();
List<SysConfig> list = configService.selectConfigList(config);
return getDataTable(list);
}
@Log(title = "参数管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:config:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysConfig config)
{
List<SysConfig> list = configService.selectConfigList(config);
ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
util.exportExcel(response, list, "参数数据");
}
/**
* 根据参数编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:config:query')")
@GetMapping(value = "/{configId}")
public AjaxResult getInfo(@PathVariable Long configId)
{
return success(configService.selectConfigById(configId));
}
/**
* 根据参数键名查询参数值
*/
@GetMapping(value = "/configKey/{configKey}")
public AjaxResult getConfigKey(@PathVariable String configKey)
{
return success(configService.selectConfigByKey(configKey));
}
/**
* 新增参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:add')")
@Log(title = "参数管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setCreateBy(getUsername());
return toAjax(configService.insertConfig(config));
}
/**
* 修改参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:edit')")
@Log(title = "参数管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setUpdateBy(getUsername());
return toAjax(configService.updateConfig(config));
}
/**
* 删除参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{configIds}")
public AjaxResult remove(@PathVariable Long[] configIds)
{
configService.deleteConfigByIds(configIds);
return success();
}
/**
* 刷新参数缓存
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
configService.resetConfigCache();
return success();
}
}
package com.ruoyi.web.controller.system;
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.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.SysConfig;
import com.ruoyi.system.service.ISysConfigService;
/**
* 参数配置 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/config")
public class SysConfigController extends BaseController
{
@Autowired
private ISysConfigService configService;
/**
* 获取参数配置列表
*/
@PreAuthorize("@ss.hasPermi('system:config:list')")
@GetMapping("/list")
public TableDataInfo list(SysConfig config)
{
startPage();
List<SysConfig> list = configService.selectConfigList(config);
return getDataTable(list);
}
@Log(title = "参数管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:config:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysConfig config)
{
List<SysConfig> list = configService.selectConfigList(config);
ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
util.exportExcel(response, list, "参数数据");
}
/**
* 根据参数编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:config:query')")
@GetMapping(value = "/{configId}")
public AjaxResult getInfo(@PathVariable Long configId)
{
return success(configService.selectConfigById(configId));
}
/**
* 根据参数键名查询参数值
*/
@GetMapping(value = "/configKey/{configKey}")
public AjaxResult getConfigKey(@PathVariable String configKey)
{
return success(configService.selectConfigByKey(configKey));
}
/**
* 新增参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:add')")
@Log(title = "参数管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setCreateBy(getUsername());
return toAjax(configService.insertConfig(config));
}
/**
* 修改参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:edit')")
@Log(title = "参数管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setUpdateBy(getUsername());
return toAjax(configService.updateConfig(config));
}
/**
* 删除参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{configIds}")
public AjaxResult remove(@PathVariable Long[] configIds)
{
configService.deleteConfigByIds(configIds);
return success();
}
/**
* 刷新参数缓存
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
configService.resetConfigCache();
return success();
}
}

View File

@@ -1,132 +1,147 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* 部门信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dept")
public class SysDeptController extends BaseController
{
@Autowired
private ISysDeptService deptService;
/**
* 获取部门列表
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept)
{
List<SysDept> depts = deptService.selectDeptList(dept);
return success(depts);
}
/**
* 查询部门列表(排除节点)
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list/exclude/{deptId}")
public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
{
List<SysDept> depts = deptService.selectDeptList(new SysDept());
depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
return success(depts);
}
/**
* 根据部门编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:dept:query')")
@GetMapping(value = "/{deptId}")
public AjaxResult getInfo(@PathVariable Long deptId)
{
deptService.checkDeptDataScope(deptId);
return success(deptService.selectDeptById(deptId));
}
/**
* 新增部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:add')")
@Log(title = "部门管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDept dept)
{
if (!deptService.checkDeptNameUnique(dept))
{
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
dept.setCreateBy(getUsername());
return toAjax(deptService.insertDept(dept));
}
/**
* 修改部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "部门管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDept dept)
{
Long deptId = dept.getDeptId();
deptService.checkDeptDataScope(deptId);
if (!deptService.checkDeptNameUnique(dept))
{
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
else if (dept.getParentId().equals(deptId))
{
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
}
else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
{
return error("该部门包含未停用的子部门!");
}
dept.setUpdateBy(getUsername());
return toAjax(deptService.updateDept(dept));
}
/**
* 删除部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:remove')")
@Log(title = "部门管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{deptId}")
public AjaxResult remove(@PathVariable Long deptId)
{
if (deptService.hasChildByDeptId(deptId))
{
return warn("存在下级部门,不允许删除");
}
if (deptService.checkDeptExistUser(deptId))
{
return warn("部门存在用户,不允许删除");
}
deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId));
}
}
package com.ruoyi.web.controller.system;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* 部门信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dept")
public class SysDeptController extends BaseController
{
@Autowired
private ISysDeptService deptService;
/**
* 获取部门列表
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept)
{
List<SysDept> depts = deptService.selectDeptList(dept);
return success(depts);
}
/**
* 查询部门列表(排除节点)
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list/exclude/{deptId}")
public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
{
List<SysDept> depts = deptService.selectDeptList(new SysDept());
depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
return success(depts);
}
/**
* 根据部门编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:dept:query')")
@GetMapping(value = "/{deptId}")
public AjaxResult getInfo(@PathVariable Long deptId)
{
deptService.checkDeptDataScope(deptId);
return success(deptService.selectDeptById(deptId));
}
/**
* 新增部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:add')")
@Log(title = "部门管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDept dept)
{
if (!deptService.checkDeptNameUnique(dept))
{
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
dept.setCreateBy(getUsername());
return toAjax(deptService.insertDept(dept));
}
/**
* 修改部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "部门管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDept dept)
{
Long deptId = dept.getDeptId();
deptService.checkDeptDataScope(deptId);
if (!deptService.checkDeptNameUnique(dept))
{
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
else if (dept.getParentId().equals(deptId))
{
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
}
else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
{
return error("该部门包含未停用的子部门!");
}
dept.setUpdateBy(getUsername());
return toAjax(deptService.updateDept(dept));
}
/**
* 保存部门排序
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "保存部门排序", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public AjaxResult updateSort(@RequestBody Map<String, String> params)
{
String[] deptIds = params.get("deptIds").split(",");
String[] orderNums = params.get("orderNums").split(",");
deptService.updateDeptSort(deptIds, orderNums);
return success();
}
/**
* 删除部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:remove')")
@Log(title = "部门管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{deptId}")
public AjaxResult remove(@PathVariable Long deptId)
{
if (deptService.hasChildByDeptId(deptId))
{
return warn("存在下级部门,不允许删除");
}
if (deptService.checkDeptExistUser(deptId))
{
return warn("部门存在用户,不允许删除");
}
deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId));
}
}

View File

@@ -1,121 +1,121 @@
package com.ruoyi.web.controller.system;
import java.util.ArrayList;
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.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.domain.entity.SysDictData;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictDataService;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/data")
public class SysDictDataController extends BaseController
{
@Autowired
private ISysDictDataService dictDataService;
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictData dictData)
{
startPage();
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
return getDataTable(list);
}
@Log(title = "字典数据", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictData dictData)
{
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
util.exportExcel(response, list, "字典数据");
}
/**
* 查询字典数据详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictCode}")
public AjaxResult getInfo(@PathVariable Long dictCode)
{
return success(dictDataService.selectDictDataById(dictCode));
}
/**
* 根据字典类型查询字典数据信息
*/
@GetMapping(value = "/type/{dictType}")
public AjaxResult dictType(@PathVariable String dictType)
{
List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
if (StringUtils.isNull(data))
{
data = new ArrayList<SysDictData>();
}
return success(data);
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典数据", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictData dict)
{
dict.setCreateBy(getUsername());
return toAjax(dictDataService.insertDictData(dict));
}
/**
* 修改保存字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典数据", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictData dict)
{
dict.setUpdateBy(getUsername());
return toAjax(dictDataService.updateDictData(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictCodes}")
public AjaxResult remove(@PathVariable Long[] dictCodes)
{
dictDataService.deleteDictDataByIds(dictCodes);
return success();
}
}
package com.ruoyi.web.controller.system;
import java.util.ArrayList;
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.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.domain.entity.SysDictData;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictDataService;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/data")
public class SysDictDataController extends BaseController
{
@Autowired
private ISysDictDataService dictDataService;
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictData dictData)
{
startPage();
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
return getDataTable(list);
}
@Log(title = "字典数据", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictData dictData)
{
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
util.exportExcel(response, list, "字典数据");
}
/**
* 查询字典数据详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictCode}")
public AjaxResult getInfo(@PathVariable Long dictCode)
{
return success(dictDataService.selectDictDataById(dictCode));
}
/**
* 根据字典类型查询字典数据信息
*/
@GetMapping(value = "/type/{dictType}")
public AjaxResult dictType(@PathVariable String dictType)
{
List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
if (StringUtils.isNull(data))
{
data = new ArrayList<SysDictData>();
}
return success(data);
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典数据", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictData dict)
{
dict.setCreateBy(getUsername());
return toAjax(dictDataService.insertDictData(dict));
}
/**
* 修改保存字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典数据", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictData dict)
{
dict.setUpdateBy(getUsername());
return toAjax(dictDataService.updateDictData(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictCodes}")
public AjaxResult remove(@PathVariable Long[] dictCodes)
{
dictDataService.deleteDictDataByIds(dictCodes);
return success();
}
}

View File

@@ -1,131 +1,131 @@
package com.ruoyi.web.controller.system;
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.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.domain.entity.SysDictType;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/type")
public class SysDictTypeController extends BaseController
{
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictType dictType)
{
startPage();
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
return getDataTable(list);
}
@Log(title = "字典类型", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictType dictType)
{
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
util.exportExcel(response, list, "字典类型");
}
/**
* 查询字典类型详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictId}")
public AjaxResult getInfo(@PathVariable Long dictId)
{
return success(dictTypeService.selectDictTypeById(dictId));
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典类型", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setCreateBy(getUsername());
return toAjax(dictTypeService.insertDictType(dict));
}
/**
* 修改字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典类型", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setUpdateBy(getUsername());
return toAjax(dictTypeService.updateDictType(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictIds}")
public AjaxResult remove(@PathVariable Long[] dictIds)
{
dictTypeService.deleteDictTypeByIds(dictIds);
return success();
}
/**
* 刷新字典缓存
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
dictTypeService.resetDictCache();
return success();
}
/**
* 获取字典选择框列表
*/
@GetMapping("/optionselect")
public AjaxResult optionselect()
{
List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
return success(dictTypes);
}
}
package com.ruoyi.web.controller.system;
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.validation.annotation.Validated;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.domain.entity.SysDictType;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/type")
public class SysDictTypeController extends BaseController
{
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictType dictType)
{
startPage();
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
return getDataTable(list);
}
@Log(title = "字典类型", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictType dictType)
{
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
util.exportExcel(response, list, "字典类型");
}
/**
* 查询字典类型详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictId}")
public AjaxResult getInfo(@PathVariable Long dictId)
{
return success(dictTypeService.selectDictTypeById(dictId));
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典类型", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setCreateBy(getUsername());
return toAjax(dictTypeService.insertDictType(dict));
}
/**
* 修改字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典类型", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setUpdateBy(getUsername());
return toAjax(dictTypeService.updateDictType(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictIds}")
public AjaxResult remove(@PathVariable Long[] dictIds)
{
dictTypeService.deleteDictTypeByIds(dictIds);
return success();
}
/**
* 刷新字典缓存
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
dictTypeService.resetDictCache();
return success();
}
/**
* 获取字典选择框列表
*/
@GetMapping("/optionselect")
public AjaxResult optionselect()
{
List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
return success(dictTypes);
}
}

View File

@@ -1,29 +1,64 @@
package com.ruoyi.web.controller.system;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.StringUtils;
/**
* 首页
*
* @author ruoyi
*/
@RestController
public class SysIndexController
{
/** 系统基础配置 */
@Autowired
private RuoYiConfig ruoyiConfig;
/**
* 访问首页,提示语
*/
@RequestMapping("/")
public String index()
{
return StringUtils.format("欢迎使用{}后台管理框架当前版本v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
}
}
package com.ruoyi.web.controller.system;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;
/**
* 首页
*
* @author ruoyi
*/
@RestController
public class SysIndexController
{
/** 系统基础配置 */
@Autowired
private RuoYiConfig ruoyiConfig;
@Autowired
private ISysUserService userService;
/**
* 访问首页,提示语
*/
@RequestMapping("/")
public String index()
{
return StringUtils.format("欢迎使用{}后台管理框架当前版本v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
}
/**
* 解锁屏幕
*/
@PostMapping("/unlockscreen")
public AjaxResult unlockScreen(@RequestBody Map<String, String> body)
{
String password = body.get("password");
if (StringUtils.isEmpty(password))
{
return AjaxResult.error("密码不能为空");
}
String username = SecurityUtils.getUsername();
SysUser user = userService.selectUserByUserName(username);
if (user == null)
{
return AjaxResult.error("服务器超时,请重新登录");
}
if (!SecurityUtils.matchesPassword(password, user.getPassword()))
{
return AjaxResult.error("密码错误,请重新输入");
}
return AjaxResult.success("解锁成功");
}
}

View File

@@ -1,152 +1,131 @@
package com.ruoyi.web.controller.system;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysMenuService;
/**
* 登录验证
*
* @author ruoyi
*/
@RestController
public class SysLoginController
{
@Autowired
private SysLoginService loginService;
@Autowired
private ISysMenuService menuService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private TokenService tokenService;
@Autowired
private ISysConfigService configService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
loginBody.setPassword(passwordTransferCryptoService.decrypt(loginBody.getPassword()));
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 登录方法
* 该方法处理用户登录请求,无需验证码
* @param loginBody 登录信息,包含用户名和密码
* @return 结果
*/
@PostMapping("/login/test")
public AjaxResult loginWithoutCaptcha (@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.loginWithoutCaptcha(loginBody.getUsername(), loginBody.getPassword());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
LoginUser loginUser = SecurityUtils.getLoginUser();
SysUser user = loginUser.getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
if (!loginUser.getPermissions().equals(permissions))
{
loginUser.setPermissions(permissions);
tokenService.refreshToken(loginUser);
}
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
return ajax;
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
// 检查初始密码是否提醒修改
public boolean initPasswordIsModify(Date pwdUpdateDate)
{
Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify"));
return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null;
}
// 检查密码是否过期
public boolean passwordIsExpiration(Date pwdUpdateDate)
{
Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays"));
if (passwordValidateDays != null && passwordValidateDays > 0)
{
if (StringUtils.isNull(pwdUpdateDate))
{
// 如果从未修改过初始密码,直接提醒过期
return true;
}
Date nowDate = DateUtils.getNowDate();
return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays;
}
return false;
}
}
package com.ruoyi.web.controller.system;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysMenuService;
/**
* 登录验证
*
* @author ruoyi
*/
@RestController
public class SysLoginController
{
@Autowired
private SysLoginService loginService;
@Autowired
private ISysMenuService menuService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private TokenService tokenService;
@Autowired
private ISysConfigService configService;
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
LoginUser loginUser = SecurityUtils.getLoginUser();
SysUser user = loginUser.getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
if (!loginUser.getPermissions().equals(permissions))
{
loginUser.setPermissions(permissions);
tokenService.refreshToken(loginUser);
}
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
return ajax;
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
// 检查初始密码是否提醒修改
public boolean initPasswordIsModify(Date pwdUpdateDate)
{
Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify"));
return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null;
}
// 检查密码是否过期
public boolean passwordIsExpiration(Date pwdUpdateDate)
{
Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays"));
if (passwordValidateDays != null && passwordValidateDays > 0)
{
if (StringUtils.isNull(pwdUpdateDate))
{
// 如果从未修改过初始密码,直接提醒过期
return true;
}
Date nowDate = DateUtils.getNowDate();
return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays;
}
return false;
}
}

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