diff --git a/bin/prod/restart_java.sh b/bin/prod/restart_java.sh
index e891d91..accedc1 100755
--- a/bin/prod/restart_java.sh
+++ b/bin/prod/restart_java.sh
@@ -5,7 +5,7 @@ set -eu
WEBAPP_ROOT="/home/webapp"
ENV_ROOT="$WEBAPP_ROOT/env"
APP_ROOT="$WEBAPP_ROOT/loan-pricing"
-JAVA_HOME="$ENV_ROOT/jdk"
+JAVA_HOME="$ENV_ROOT/java"
BACKEND_DIR="$APP_ROOT/backend"
LOG_DIR="$APP_ROOT/logs"
RUN_DIR="$APP_ROOT/run"
@@ -37,13 +37,6 @@ usage() {
EOF
}
-require_root() {
- if [ "$(id -u)" -ne 0 ]; then
- log_error "请使用 root 用户执行脚本"
- exit 1
- fi
-}
-
ensure_runtime_dirs() {
mkdir -p "$BACKEND_DIR" "$LOG_DIR" "$RUN_DIR"
}
@@ -133,8 +126,10 @@ stop_backend() {
}
start_backend() {
+ ensure_runtime_dirs
+
if [ ! -x "$JAVA_HOME/bin/java" ]; then
- log_error "未检测到 Java,可先执行 /home/webapp/install_env.sh"
+ log_error "未检测到可执行 Java: $JAVA_HOME/bin/java"
exit 1
fi
@@ -164,19 +159,7 @@ start_backend() {
exit 1
fi
- wait_seconds=0
- while [ "$wait_seconds" -lt 30 ]; do
- if ss -lnt 2>/dev/null | grep -q ":$BACKEND_PORT "; then
- log_info "后端已监听端口: $BACKEND_PORT"
- return 0
- fi
-
- sleep 1
- wait_seconds=$((wait_seconds + 1))
- done
-
- log_error "后端未在预期时间内监听端口 $BACKEND_PORT"
- exit 1
+ log_info "后端已启动,PID: $backend_pid"
}
status_backend() {
@@ -186,11 +169,6 @@ status_backend() {
return 0
fi
- if ss -lnt 2>/dev/null | grep -q ":$BACKEND_PORT "; then
- log_info "未识别到脚本托管进程,但端口 $BACKEND_PORT 已被占用"
- return 0
- fi
-
log_info "后端未运行"
}
diff --git a/bin/prod/restart_java_test.sh b/bin/prod/restart_java_test.sh
new file mode 100644
index 0000000..4e1a119
--- /dev/null
+++ b/bin/prod/restart_java_test.sh
@@ -0,0 +1,114 @@
+#!/bin/sh
+
+set -eu
+
+ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
+SCRIPT_UNDER_TEST="$ROOT_DIR/bin/prod/restart_java.sh"
+
+fail() {
+ printf 'FAIL: %s\n' "$1" >&2
+ exit 1
+}
+
+assert_grep() {
+ pattern="$1"
+ target="$2"
+ if ! grep -Eq -- "$pattern" "$target"; then
+ fail "expected pattern [$pattern] in $target"
+ fi
+}
+
+assert_not_grep() {
+ pattern="$1"
+ target="$2"
+ if grep -Eq -- "$pattern" "$target"; then
+ fail "did not expect pattern [$pattern] in $target"
+ fi
+}
+
+create_fake_java() {
+ fake_java="$1"
+
+ cat > "$fake_java" <<'EOF'
+#!/bin/sh
+set -eu
+
+while :; do
+ sleep 1
+done
+EOF
+
+ chmod +x "$fake_java"
+}
+
+prepare_script_env() {
+ work_dir="$1"
+
+ mkdir -p "$work_dir/env/java/bin" "$work_dir/loan-pricing/backend" "$work_dir/loan-pricing/logs" "$work_dir/loan-pricing/run"
+ create_fake_java "$work_dir/env/java/bin/java"
+ printf 'fake-jar\n' > "$work_dir/loan-pricing/backend/ruoyi-admin.jar"
+ cp "$SCRIPT_UNDER_TEST" "$work_dir/restart_java.sh"
+ perl -0pi -e "s#WEBAPP_ROOT=\"/home/webapp\"#WEBAPP_ROOT=\"$work_dir\"#" "$work_dir/restart_java.sh"
+ chmod +x "$work_dir/restart_java.sh"
+}
+
+cleanup_work_dir() {
+ work_dir="$1"
+
+ if [ -f "$work_dir/loan-pricing/run/backend.pid" ]; then
+ backend_pid=$(cat "$work_dir/loan-pricing/run/backend.pid" 2>/dev/null || true)
+ if [ -n "${backend_pid:-}" ]; then
+ kill "$backend_pid" 2>/dev/null || true
+ wait "$backend_pid" 2>/dev/null || true
+ fi
+ fi
+
+ rm -rf "$work_dir"
+}
+
+test_script_contract() {
+ assert_grep 'JAVA_HOME="\$ENV_ROOT/java"' "$SCRIPT_UNDER_TEST"
+ assert_grep '--spring\.profiles\.active=pro' "$SCRIPT_UNDER_TEST"
+ assert_not_grep 'mvn' "$SCRIPT_UNDER_TEST"
+ assert_not_grep 'require_root' "$SCRIPT_UNDER_TEST"
+ assert_not_grep '\b(ss|lsof|netstat)\b' "$SCRIPT_UNDER_TEST"
+}
+
+test_restart_flow() {
+ work_dir=$(mktemp -d)
+ trap 'cleanup_work_dir "$work_dir"' EXIT INT TERM
+
+ prepare_script_env "$work_dir"
+
+ "$work_dir/restart_java.sh" start
+ if [ ! -f "$work_dir/loan-pricing/run/backend.pid" ]; then
+ fail "expected backend pid file after start"
+ fi
+
+ backend_pid=$(cat "$work_dir/loan-pricing/run/backend.pid")
+ kill -0 "$backend_pid" 2>/dev/null || fail "expected backend process to be running after start"
+
+ status_output=$("$work_dir/restart_java.sh" status 2>&1 || true)
+ printf '%s\n' "$status_output" | grep -q '后端正在运行' || fail "expected status output to show running"
+
+ "$work_dir/restart_java.sh" restart
+ restarted_pid=$(cat "$work_dir/loan-pricing/run/backend.pid")
+ kill -0 "$restarted_pid" 2>/dev/null || fail "expected backend process to be running after restart"
+
+ "$work_dir/restart_java.sh" stop
+ if [ -f "$work_dir/loan-pricing/run/backend.pid" ]; then
+ fail "expected backend pid file to be removed after stop"
+ fi
+
+ trap - EXIT INT TERM
+ cleanup_work_dir "$work_dir"
+}
+
+main() {
+ [ -f "$SCRIPT_UNDER_TEST" ] || fail "script under test not found: $SCRIPT_UNDER_TEST"
+ test_script_contract
+ test_restart_flow
+ printf 'PASS: restart_java tests\n'
+}
+
+main "$@"
diff --git a/doc/2026-04-03-retail-display-fields-backend-plan.md b/doc/2026-04-03-retail-display-fields-backend-plan.md
new file mode 100644
index 0000000..3cc3ccb
--- /dev/null
+++ b/doc/2026-04-03-retail-display-fields-backend-plan.md
@@ -0,0 +1,84 @@
+# 个人模型详情缺失展示字段补齐后端实施计划
+
+> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 补齐个人模型输出对象与零售模型输出表结构的新增字段,确保个人详情接口可承载并持久化最新展示字段。
+
+**Architecture:** 后端补齐 `ModelRetailOutputFields` 实体字段,并为 `model_retail_output_fields` 表增加 5 个对应列。保持现有控制器、服务与 Mapper 链路不变,让模型返回字段按既有流程直接反序列化、入库并回查。
+
+**Tech Stack:** Java 17、Spring Boot、MyBatis Plus、Maven、JUnit 5
+
+---
+
+### Task 1: 通过测试锁定缺失字段
+
+**Files:**
+- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java`
+
+- [ ] **Step 1: 编写实体字段断言测试**
+
+新增测试,断言 `ModelRetailOutputFields` 包含以下字段:
+
+```java
+"loanRateHistory",
+"minRateProduct",
+"smoothRange",
+"finalCalculateRate",
+"referenceRate"
+```
+
+- [ ] **Step 2: 运行测试并确认先失败**
+
+Run: `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest test`
+Expected: FAIL,提示缺少新增字段。
+
+### Task 2: 补齐个人模型输出实体字段
+
+**Files:**
+- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
+
+- [ ] **Step 1: 在实体中新增 5 个字段**
+
+新增以下字段:
+
+- `loanRateHistory`
+- `minRateProduct`
+- `smoothRange`
+- `finalCalculateRate`
+- `referenceRate`
+
+- [ ] **Step 2: 保持现有命名与注释风格一致**
+
+新增字段使用与现有实体一致的 `private String` 定义和中文注释,不引入额外注解。
+
+- [ ] **Step 3: 重新运行测试确认通过**
+
+Run: `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest test`
+Expected: PASS
+
+### Task 3: 补齐零售模型输出表结构
+
+**Files:**
+- Create: `sql/add_model_retail_output_rate_fields_20260403.sql`
+- Modify: `sql/model_retail.sql`
+- Modify: `sql/loan_pricing_schema_20260328.sql`
+- Modify: `sql/loan_pricing_prod_init_20260331.sql`
+
+- [ ] **Step 1: 新增数据库迁移脚本**
+
+在迁移脚本中为 `model_retail_output_fields` 增加以下列:
+
+- `loan_rate_history`
+- `min_rate_product`
+- `smooth_range`
+- `final_calculate_rate`
+- `reference_rate`
+
+- [ ] **Step 2: 同步更新建表基线 SQL**
+
+将相同列同步到仓库中的零售模型输出表建表脚本,避免新环境继续缺列。
+
+- [ ] **Step 3: 对开发库执行迁移并验证列存在**
+
+Run: `mysql ... < sql/add_model_retail_output_rate_fields_20260403.sql`
+Expected: 迁移执行成功,`SHOW COLUMNS FROM model_retail_output_fields` 可看到新增 5 列
diff --git a/doc/2026-04-03-retail-display-fields-design.md b/doc/2026-04-03-retail-display-fields-design.md
new file mode 100644
index 0000000..ad53cf2
--- /dev/null
+++ b/doc/2026-04-03-retail-display-fields-design.md
@@ -0,0 +1,113 @@
+# 个人模型详情缺失展示字段补齐设计文档
+
+## 1. 背景
+
+个人模型接口返回字段有更新。根据 `doc/上虞对私利率测算_上传字段与展示字段.xlsx` 的 `展示指标` sheet,当前个人详情页仍缺少部分应展示字段,需要补齐页面展示并保证接口链路字段完整。
+
+## 2. 已确认范围
+
+- 仅处理个人客户详情页
+- 仅补齐 `展示指标` sheet 中当前缺失的 6 个字段
+- 不调整企业客户页面
+- 不新增兼容逻辑、兜底逻辑或额外展示区域
+- 保持现有页面结构和分组,按最短路径补齐
+
+## 3. 缺失字段
+
+经核对,当前页面缺少以下字段:
+
+- `loanTerm` 借款期限
+- `loanRateHistory` 历史利率
+- `minRateProduct` 产品最低利率下限
+- `smoothRange` 平滑幅度
+- `finalCalculateRate` 最终测算利率
+- `referenceRate` 参考利率
+
+其中:
+
+- `loanTerm` 属于流程明细字段,来自 `LoanPricingWorkflow`
+- 其余 5 个字段属于个人模型输出字段,来自 `ModelRetailOutputFields`
+- 其余 5 个字段同时要求 `model_retail_output_fields` 表具备对应列,否则新流程详情无法完整落库展示
+
+## 4. 现状分析
+
+### 4.1 前端现状
+
+个人详情页由两个主要区域组成:
+
+- `PersonalWorkflowDetail.vue` 负责流程详情与左侧关键信息
+- `ModelOutputDisplay.vue` 负责个人模型输出分组展示
+
+当前页面已覆盖大部分 `展示指标` 字段,但个人详情页“业务信息”中未展示 `loanTerm`,个人模型输出“测算结果”中也未展示 5 个新增利率相关字段。
+
+### 4.2 后端现状
+
+- `LoanPricingWorkflow` 已包含 `loanTerm`
+- `ModelRetailOutputFields` 当前未包含 `loanRateHistory`、`minRateProduct`、`smoothRange`、`finalCalculateRate`、`referenceRate`
+
+因此前端目前无法从个人模型输出对象中读取这 5 个字段。
+
+同时,开发库 `model_retail_output_fields` 表当前也未包含这 5 个字段列。如果只补代码而不补表结构,新的个人流程在模型结果入库时将无法完整保存这些字段。
+
+## 5. 方案对比
+
+### 方案一:在现有分组内补齐字段
+
+做法:
+
+- 在个人详情页“业务信息”区域补 `loanTerm`
+- 在个人模型输出“测算结果”区域补 5 个新增利率字段
+- 后端仅补 `ModelRetailOutputFields` 缺失字段定义
+
+优点:
+
+- 改动最小
+- 不影响现有页面结构
+- 与现有字段分组最贴合
+
+缺点:
+
+- 需要同时修改前后端
+
+### 方案二:单独新增“利率结果扩展”分组
+
+做法:
+
+- 新增一个专门的 Tab 或卡片展示 5 个新增利率字段
+
+优点:
+
+- 新增字段集中展示
+
+缺点:
+
+- 页面改动更大
+- 用户认知路径变化
+- 不符合本次最短路径要求
+
+## 6. 设计结论
+
+采用方案一。
+
+实现方式如下:
+
+- 后端在 `ModelRetailOutputFields` 中新增 5 个字段定义,保证接口对象具备完整返回结构
+- 数据库为 `model_retail_output_fields` 新增 5 个对应列,保证模型输出可正常落库
+- 前端在 `PersonalWorkflowDetail.vue` 的“业务信息”中补齐 `loanTerm`
+- 前端在 `ModelOutputDisplay.vue` 的个人“测算结果”中补齐 `loanRateHistory`、`minRateProduct`、`smoothRange`、`finalCalculateRate`、`referenceRate`
+
+## 7. 验证设计
+
+本次按最小可执行验证:
+
+- 后端新增一个实体字段断言测试,先验证缺失字段不存在并失败,再补齐后验证通过
+- 前端新增一个源码断言脚本,先验证缺失展示未实现并失败,再补齐后验证通过
+- 对开发库执行表结构迁移
+- 创建新的个人流程并打开详情页,确认新增字段可在真实页面展示
+- 最后执行前端生产构建,确认页面代码可正常打包
+
+## 8. 非目标
+
+- 不调整企业详情页
+- 不修改模型计算逻辑
+- 不重构页面布局
diff --git a/doc/2026-04-03-retail-display-fields-frontend-plan.md b/doc/2026-04-03-retail-display-fields-frontend-plan.md
new file mode 100644
index 0000000..451a7b4
--- /dev/null
+++ b/doc/2026-04-03-retail-display-fields-frontend-plan.md
@@ -0,0 +1,81 @@
+# 个人模型详情缺失展示字段补齐前端实施计划
+
+> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在个人详情页补齐 `展示指标` sheet 中缺失的 6 个字段展示。
+
+**Architecture:** 前端沿用现有个人详情页结构,在流程详情的“业务信息”中补 `loanTerm`,在模型输出“测算结果”中补 5 个新增利率字段,不新增页面分组和交互。
+
+**Tech Stack:** Vue 2、Element UI、Node.js
+
+---
+
+### Task 1: 通过前端断言测试锁定缺失展示
+
+**Files:**
+- Create: `ruoyi-ui/tests/retail-display-fields.test.js`
+- Modify: `ruoyi-ui/package.json`
+
+- [ ] **Step 1: 编写源码断言脚本**
+
+断言以下展示已存在:
+
+- `PersonalWorkflowDetail.vue` 包含 `借款期限` 和 `detailData.loanTerm`
+- `ModelOutputDisplay.vue` 包含以下字段展示:
+ - `loanRateHistory`
+ - `minRateProduct`
+ - `smoothRange`
+ - `finalCalculateRate`
+ - `referenceRate`
+
+- [ ] **Step 2: 运行脚本并确认先失败**
+
+Run: `npm --prefix ruoyi-ui run test:retail-display-fields`
+Expected: FAIL,提示缺失展示实现。
+
+### Task 2: 补齐个人详情页字段展示
+
+**Files:**
+- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
+- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
+- Reference: `ruoyi-loan-pricing/src/main/resources/data/retail_output.json`
+
+- [ ] **Step 1: 在业务信息中补齐借款期限**
+
+在 `PersonalWorkflowDetail.vue` 的“业务信息”区域新增:
+
+```vue
+{{ detailData.loanTerm || '-' }}
+```
+
+- [ ] **Step 2: 在个人测算结果中补齐 5 个字段**
+
+在 `ModelOutputDisplay.vue` 的个人“测算结果”中新增:
+
+- 历史利率
+- 产品最低利率下限
+- 平滑幅度
+- 最终测算利率
+- 参考利率
+
+- [ ] **Step 3: 重新运行前端断言脚本**
+
+Run: `npm --prefix ruoyi-ui run test:retail-display-fields`
+Expected: PASS
+
+- [ ] **Step 4: 执行前端构建验证**
+
+Run: `npm --prefix ruoyi-ui run build:prod`
+Expected: 构建成功,输出包含 `Build complete.`
+
+- [ ] **Step 5: 启动前后端并打开个人流程详情页验证**
+
+使用浏览器打开新的个人流程详情页,确认:
+
+- 流程详情“业务信息”出现 `借款期限`
+- 模型输出“测算结果”出现并可查看以下字段
+ - 历史利率
+ - 产品最低利率下限
+ - 平滑幅度
+ - 最终测算利率
+ - 参考利率
diff --git a/doc/implementation-report-2026-04-03-production-backend-restart-script.md b/doc/implementation-report-2026-04-03-production-backend-restart-script.md
new file mode 100644
index 0000000..1d90c6b
--- /dev/null
+++ b/doc/implementation-report-2026-04-03-production-backend-restart-script.md
@@ -0,0 +1,24 @@
+# 生产后端重启脚本实施记录
+
+## 修改内容
+- 收敛生产后端重启脚本 `bin/prod/restart_java.sh`
+- 脚本固定面向已部署的 `backend/ruoyi-admin.jar` 执行启停,不再包含构建逻辑
+- 后端启动 profile 固定为 `pro`
+- Java 路径统一为 `/home/webapp/env/java/bin/java`,与现有生产安装脚本保持一致
+- 移除 `root` 执行校验与端口监听校验,只保留 `start|stop|restart|status` 所需的最小启停逻辑
+- 新增脚本自测文件 `bin/prod/restart_java_test.sh`
+
+## 实现说明
+- `start` 仅检查 Java 可执行文件、目标 jar 是否存在以及当前是否已有同脚本托管进程
+- `stop` 继续基于 PID 文件和 `-Dloan.pricing.home=/home/webapp/loan-pricing` 进程标记识别并停止当前后端进程
+- `restart` 按“先停后起”执行,适用于生产环境已部署 jar 的直接重启
+- `status` 仅返回脚本托管进程状态,不再增加端口占用类附加判断
+
+## 验证结果
+- 已执行 `sh bin/prod/restart_java_test.sh`
+- 已验证以下场景:
+ - 脚本固定使用 `/home/webapp/env/java`
+ - 脚本固定使用 `--spring.profiles.active=pro`
+ - 脚本不包含 `mvn`、`require_root`、`ss/lsof/netstat` 相关依赖
+ - `start -> status -> restart -> stop` 流程执行通过
+- 自测使用临时目录中的假 `java` 进程完成,测试结束后已自动清理对应进程和临时目录
diff --git a/doc/implementation-report-2026-04-03-retail-display-fields.md b/doc/implementation-report-2026-04-03-retail-display-fields.md
new file mode 100644
index 0000000..2a879bb
--- /dev/null
+++ b/doc/implementation-report-2026-04-03-retail-display-fields.md
@@ -0,0 +1,72 @@
+# 个人模型详情缺失展示字段补齐实施记录
+
+## 实施时间
+- 2026-04-03
+
+## 修改内容
+- 补齐个人详情页“业务信息”中的 `借款期限`
+- 补齐个人模型输出“测算结果”中的 5 个字段:
+ - `历史利率`
+ - `产品最低利率下限`
+ - `平滑幅度`
+ - `最终测算利率`
+ - `参考利率`
+- 在后端 `ModelRetailOutputFields` 中新增对应 5 个字段定义
+- 在零售模型 mock 数据中补齐对应 5 个字段样例值
+- 新增零售模型输出表结构迁移脚本,并同步更新建表基线 SQL
+- 新增后端字段断言测试与前端源码断言脚本
+
+## 修改文件
+- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
+- `ruoyi-loan-pricing/src/main/resources/data/retail_output.json`
+- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java`
+- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
+- `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
+- `ruoyi-ui/tests/retail-display-fields.test.js`
+- `ruoyi-ui/package.json`
+- `sql/add_model_retail_output_rate_fields_20260403.sql`
+- `sql/model_retail.sql`
+- `sql/loan_pricing_schema_20260328.sql`
+- `sql/loan_pricing_prod_init_20260331.sql`
+- `doc/2026-04-03-retail-display-fields-design.md`
+- `doc/2026-04-03-retail-display-fields-backend-plan.md`
+- `doc/2026-04-03-retail-display-fields-frontend-plan.md`
+- `doc/implementation-report-2026-04-03-retail-display-fields.md`
+
+## 验证方式
+1. 新增后端测试,断言 `ModelRetailOutputFields` 包含 5 个新增字段,先失败后通过
+2. 新增前端源码断言脚本,断言个人详情页与模型输出页已补齐字段,先失败后通过
+3. 执行前端生产构建,确认页面代码可正常打包
+4. 检查开发库 `model_retail_output_fields` 表结构,确认最初缺少 5 个新列
+5. 执行 `sql/add_model_retail_output_rate_fields_20260403.sql` 到开发库,并再次确认 5 个新列存在
+6. 重新编译并重启后端,确保新的实体字段已进入运行中的 SQL 映射
+7. 创建新的个人流程 `20260403100514909`,调用详情接口确认返回以下真实值:
+ - `loanRateHistory = 6.40`
+ - `minRateProduct = 5.50`
+ - `smoothRange = -0.10`
+ - `finalCalculateRate = 6.05`
+ - `referenceRate = 5.95`
+8. 启动前端开发服务并使用浏览器自动化打开详情页,确认:
+ - 页面出现 `借款期限`
+ - 切换到“测算结果”页签后,5 个新增字段及对应值均可见
+9. 验证结束后,停止本次启动的前后端进程
+
+## 验证结果
+- `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest test` 首次失败,补齐后通过
+- `npm --prefix ruoyi-ui run test:retail-display-fields` 首次失败,补齐后通过
+- `npm --prefix ruoyi-ui run build:prod` 成功,输出包含 `Build complete.`
+- 已确认开发库 `model_retail_output_fields` 初始缺少:
+ - `loan_rate_history`
+ - `min_rate_product`
+ - `smooth_range`
+ - `final_calculate_rate`
+ - `reference_rate`
+- 已执行迁移脚本并确认以上 5 列存在
+- 已确认旧后端进程因未加载最新依赖导致 SQL 仍缺新列,重编译并重启后问题消失
+- 已创建个人流程 `20260403100514909` 并通过详情接口拿到 5 个新增字段的真实值
+- 已通过浏览器自动化确认个人详情页展示位与“测算结果”页签展示均正确
+- 本次验证期间启动的前后端进程均已停止
+
+## 说明
+- `loanTerm` 本次仅补齐详情页展示位;个人创建表单当前无该字段录入入口,不属于本次“模型返回字段更新”范围
+- 为保证新字段在新环境中也可正常落库,本次同步更新了零售模型输出表的建表基线 SQL
diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java
index 3eb775e..ee72e3a 100644
--- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java
+++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java
@@ -170,6 +170,21 @@ public class ModelRetailOutputFields {
// 测算利率
private String calculateRate;
+ // 历史利率
+ private String loanRateHistory;
+
+ // 产品最低利率下限
+ private String minRateProduct;
+
+ // 平滑幅度
+ private String smoothRange;
+
+ // 最终测算利率
+ private String finalCalculateRate;
+
+ // 参考利率
+ private String referenceRate;
+
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}
diff --git a/ruoyi-loan-pricing/src/main/resources/data/retail_output.json b/ruoyi-loan-pricing/src/main/resources/data/retail_output.json
index b01601b..38777c1 100644
--- a/ruoyi-loan-pricing/src/main/resources/data/retail_output.json
+++ b/ruoyi-loan-pricing/src/main/resources/data/retail_output.json
@@ -52,7 +52,12 @@
"bpGreyOverdue": "98",
"totoalBpRisk": "95",
"totalBp": "350",
- "calculateRate": "6.15"
+ "calculateRate": "6.15",
+ "loanRateHistory": "6.40",
+ "minRateProduct": "5.50",
+ "smoothRange": "-0.10",
+ "finalCalculateRate": "6.05",
+ "referenceRate": "5.95"
},
"extensionMap": {},
"reasonMessage": "Running successfully",
@@ -65,4 +70,4 @@
"workflowVersion": 14,
"callTime": 1736405548630,
"status": 1
-}
\ No newline at end of file
+}
diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java
new file mode 100644
index 0000000..bdd49ee
--- /dev/null
+++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java
@@ -0,0 +1,26 @@
+package com.ruoyi.loanpricing.domain.entity;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.Test;
+
+class ModelRetailOutputFieldsTest
+{
+ @Test
+ void shouldContainLatestRetailDisplayRateFields()
+ {
+ Set fieldNames = Arrays.stream(ModelRetailOutputFields.class.getDeclaredFields())
+ .map(Field::getName)
+ .collect(Collectors.toSet());
+
+ assertTrue(fieldNames.contains("loanRateHistory"), "缺少字段 loanRateHistory");
+ assertTrue(fieldNames.contains("minRateProduct"), "缺少字段 minRateProduct");
+ assertTrue(fieldNames.contains("smoothRange"), "缺少字段 smoothRange");
+ assertTrue(fieldNames.contains("finalCalculateRate"), "缺少字段 finalCalculateRate");
+ assertTrue(fieldNames.contains("referenceRate"), "缺少字段 referenceRate");
+ }
+}
diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json
index d728724..0897d9c 100644
--- a/ruoyi-ui/package.json
+++ b/ruoyi-ui/package.json
@@ -9,7 +9,8 @@
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
- "test:password-transfer": "node tests/password-transfer-api.test.js"
+ "test:password-transfer": "node tests/password-transfer-api.test.js",
+ "test:retail-display-fields": "node tests/retail-display-fields.test.js"
},
"keywords": [
"vue",
diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue
index 97c06f5..cf1b660 100644
--- a/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue
+++ b/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue
@@ -93,6 +93,11 @@
{{ retailOutput.totalBp || '-' }}
{{ retailOutput.calculateRate || '-' }} %
+ {{ retailOutput.loanRateHistory || '-' }}
+ {{ retailOutput.minRateProduct || '-' }}
+ {{ retailOutput.smoothRange || '-' }}
+ {{ retailOutput.finalCalculateRate || '-' }} %
+ {{ retailOutput.referenceRate || '-' }} %
diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue
index 6365313..55145d3 100644
--- a/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue
+++ b/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue
@@ -72,6 +72,7 @@
{{ detailData.guarType }}
{{ detailData.applyAmt }} 元
+ {{ detailData.loanTerm || '-' }}
{{
formatBoolean(detailData.bizProof)
}}
diff --git a/ruoyi-ui/tests/retail-display-fields.test.js b/ruoyi-ui/tests/retail-display-fields.test.js
new file mode 100644
index 0000000..530cb85
--- /dev/null
+++ b/ruoyi-ui/tests/retail-display-fields.test.js
@@ -0,0 +1,29 @@
+const fs = require('fs')
+const path = require('path')
+const assert = require('assert')
+
+function read(relativePath) {
+ return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
+}
+
+const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
+const modelOutput = read('src/views/loanPricing/workflow/components/ModelOutputDisplay.vue')
+
+assert(
+ personalDetail.includes('label="借款期限"') && personalDetail.includes('detailData.loanTerm'),
+ '个人详情页缺少借款期限展示'
+)
+
+const requiredRetailFields = [
+ 'retailOutput.loanRateHistory',
+ 'retailOutput.minRateProduct',
+ 'retailOutput.smoothRange',
+ 'retailOutput.finalCalculateRate',
+ 'retailOutput.referenceRate'
+]
+
+requiredRetailFields.forEach((field) => {
+ assert(modelOutput.includes(field), `模型输出缺少字段展示: ${field}`)
+})
+
+console.log('retail display fields assertions passed')
diff --git a/sql/add_model_retail_output_rate_fields_20260403.sql b/sql/add_model_retail_output_rate_fields_20260403.sql
new file mode 100644
index 0000000..287bcc1
--- /dev/null
+++ b/sql/add_model_retail_output_rate_fields_20260403.sql
@@ -0,0 +1,6 @@
+ALTER TABLE `model_retail_output_fields`
+ ADD COLUMN `loan_rate_history` varchar(100) DEFAULT '' COMMENT '历史利率' AFTER `calculate_rate`,
+ ADD COLUMN `min_rate_product` varchar(100) DEFAULT '' COMMENT '产品最低利率下限' AFTER `loan_rate_history`,
+ ADD COLUMN `smooth_range` varchar(100) DEFAULT '' COMMENT '平滑幅度' AFTER `min_rate_product`,
+ ADD COLUMN `final_calculate_rate` varchar(100) DEFAULT '' COMMENT '最终测算利率' AFTER `smooth_range`,
+ ADD COLUMN `reference_rate` varchar(100) DEFAULT '' COMMENT '参考利率' AFTER `final_calculate_rate`;
diff --git a/sql/loan_pricing_prod_init_20260331.sql b/sql/loan_pricing_prod_init_20260331.sql
index 3adef4c..e091b48 100644
--- a/sql/loan_pricing_prod_init_20260331.sql
+++ b/sql/loan_pricing_prod_init_20260331.sql
@@ -908,6 +908,11 @@ CREATE TABLE `model_retail_output_fields` (
`totoal_bp_risk` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'TOTAL_BP_风险度',
`total_bp` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '浮动BP',
`calculate_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '测算利率',
+ `loan_rate_history` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '历史利率',
+ `min_rate_product` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '产品最低利率下限',
+ `smooth_range` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '平滑幅度',
+ `final_calculate_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '最终测算利率',
+ `reference_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '参考利率',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='零售模型输出字段表';
diff --git a/sql/loan_pricing_schema_20260328.sql b/sql/loan_pricing_schema_20260328.sql
index 040e818..7366339 100644
--- a/sql/loan_pricing_schema_20260328.sql
+++ b/sql/loan_pricing_schema_20260328.sql
@@ -490,6 +490,11 @@ CREATE TABLE `model_retail_output_fields` (
`totoal_bp_risk` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'TOTAL_BP_风险度',
`total_bp` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '浮动BP',
`calculate_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '测算利率',
+ `loan_rate_history` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '历史利率',
+ `min_rate_product` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '产品最低利率下限',
+ `smooth_range` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '平滑幅度',
+ `final_calculate_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '最终测算利率',
+ `reference_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '参考利率',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='零售模型输出字段表';
diff --git a/sql/model_retail.sql b/sql/model_retail.sql
index 950eab4..e79a8ff 100644
--- a/sql/model_retail.sql
+++ b/sql/model_retail.sql
@@ -106,9 +106,19 @@ CREATE TABLE IF NOT EXISTS model_retail_output_fields (
total_bp VARCHAR(100) DEFAULT '' COMMENT '浮动BP',
-- 测算利率(百分比,如6.15)
calculate_rate VARCHAR(100) DEFAULT '' COMMENT '测算利率',
+ -- 历史利率
+ loan_rate_history VARCHAR(100) DEFAULT '' COMMENT '历史利率',
+ -- 产品最低利率下限
+ min_rate_product VARCHAR(100) DEFAULT '' COMMENT '产品最低利率下限',
+ -- 平滑幅度
+ smooth_range VARCHAR(100) DEFAULT '' COMMENT '平滑幅度',
+ -- 最终测算利率
+ final_calculate_rate VARCHAR(100) DEFAULT '' COMMENT '最终测算利率',
+ -- 参考利率
+ reference_rate VARCHAR(100) DEFAULT '' COMMENT '参考利率',
-- 创建时间(审计字段)
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-- 主键约束
PRIMARY KEY (id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='零售模型输出字段表';
\ No newline at end of file
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='零售模型输出字段表';