补充流程列表测算利率联表查询

This commit is contained in:
wkc
2026-03-28 12:13:48 +08:00
parent 750af8c07e
commit 39e2177280
16 changed files with 431 additions and 9 deletions

View File

@@ -0,0 +1,41 @@
# 流程列表测算利率展示后端实施记录
## 实施时间
- 2026-03-28
## 修改内容
- 新增流程列表专用返回对象 `LoanPricingWorkflowListVO`
- 将流程列表分页返回从 `LoanPricingWorkflow` 调整为列表专用 VO
- 在 Mapper 中新增联表分页方法 `selectWorkflowPageWithRates`
- 新增 `LoanPricingWorkflowMapper.xml`,通过联表 SQL 一次返回 `calculateRate``executeRate`
- 保留现有详情页测算利率兼容逻辑,不回退工作区中已有的详情链路调整
## 关键链路
- 主表:`loan_pricing_workflow`
- 个人客户测算利率来源:`model_retail_output_fields.calculate_rate`
- 企业客户测算利率来源:`model_corp_output_fields.calculate_rate`
- 统一返回字段:`calculateRate`
## 修改文件
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVO.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/mapper/LoanPricingWorkflowMapper.java`
- `ruoyi-loan-pricing/src/main/resources/mapper/loanpricing/LoanPricingWorkflowMapper.xml`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVOTest.java`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
## 验证结果
- 已执行 `mvn -pl ruoyi-loan-pricing -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=LoanPricingWorkflowServiceImplTest test`
- 结果为 `Tests run: 3, Failures: 0, Errors: 0, Skipped: 0`
- 已执行 `mvn -pl ruoyi-loan-pricing -am -Dsurefire.failIfNoSpecifiedTests=false test`
- 模块验证结果为 `Tests run: 4, Failures: 0, Errors: 0, Skipped: 0`
- 已确认列表分页链路改为返回 `LoanPricingWorkflowListVO`
- 已确认服务层会透传 `calculateRate`
## 说明
- 本次未修改数据库表结构,也未将测算利率回写到 `loan_pricing_workflow`
- 单独执行 `-pl ruoyi-loan-pricing` 时会命中旧的上游构件,因此测试命令需带 `-am`
- 本次未为验证额外启动新的后端进程
- 本次未执行真实后端启动后的接口联调,请以后端模块测试结果作为本次主要验证依据

View File

@@ -0,0 +1,30 @@
# 流程列表测算利率展示前端实施记录
## 实施时间
- 2026-03-28
## 修改内容
- 在流程列表页新增“测算利率(%)”列
- 新增列绑定后端返回字段 `calculateRate`
- 保持“执行利率(%)”列继续绑定 `executeRate`
- 保持“测算利率(%)”列位于“执行利率(%)”列之前
## 修改文件
- `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- `doc/implementation-report-2026-03-28-workflow-calculate-rate-list-frontend.md`
## 验证方式
1. 通过源码检查确认“测算利率(%)”列已新增
2. 通过源码检查确认“测算利率(%)”列位于“执行利率(%)”之前
3. 执行前端生产构建验证页面代码可正常打包
## 验证结果
- 已新增“测算利率(%)”列,绑定字段为 `calculateRate`
- 已保留“执行利率(%)”列,绑定字段为 `executeRate`
- 已确认“测算利率(%)”列位于“执行利率(%)”列之前
- 已执行 `npm --prefix ruoyi-ui run build:prod`,构建成功,输出包含 `Build complete.`
- 本次构建过程中仅出现项目原有的打包体积 warning未出现新的构建错误
## 说明
- 本次只调整流程列表页,不改详情页展示逻辑
- 本次未为验证额外启动新的前端进程

View File

@@ -0,0 +1,37 @@
# 流程详情测算利率改为模型输出表取数实施记录
## 实施时间
- 2026-03-28
## 问题说明
- 流程详情接口返回的 `loanPricingWorkflow.loanRate` 仍保留流程主表中的值
- 当模型输出表中的 `calculateRate` 与流程主表中的 `loanRate` 不一致时,详情链路无法保证“测算利率”按模型输出表口径返回
## 本次修改
-`LoanPricingWorkflowServiceImpl#selectLoanPricingBySerialNum` 中补充详情组装逻辑
- 个人客户详情查询时,将 `model_retail_output_fields.calculate_rate` 回填到 `loanPricingWorkflow.loanRate`
- 企业客户详情查询时,将 `model_corp_output_fields.calculate_rate` 回填到 `loanPricingWorkflow.loanRate`
- 新增服务层单元测试,覆盖个人、企业两条详情查询分支
-`ruoyi-loan-pricing` 模块补充测试依赖 `spring-boot-starter-test`
## 影响范围
- 仅影响流程详情接口 `/loanPricing/workflow/{serialNum}` 的返回值组装
- 不修改数据库表结构
- 不修改模型输出表写入逻辑
- 不修改流程列表接口
## 验证方式
1. 新增 `LoanPricingWorkflowServiceImplTest`
2. 先执行失败用例,确认详情返回的 `loanRate` 未按模型输出表取值
3. 修复详情组装逻辑后重新执行测试
## 验证结果
- 执行命令:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 结果2 个测试全部通过
## 备注
- 验证时发现仅编译 `ruoyi-loan-pricing` 模块会引用到本地旧版 `ruoyi-common` 依赖,需使用 `-am` 让依赖模块一并参与构建
- 本次未启动新的前后端进程

View File

@@ -0,0 +1,22 @@
# 流程列表执行利率文案调整实施记录
## 本次改动
- 将流程列表中的列头文案由“贷款利率(%)”调整为“执行利率(%)”
- 保持字段绑定 `loanRate`、列表接口和后端数据结构不变,仅调整前端展示文案
## 修改文件
- `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
## 执行方式
1. 定位贷款定价流程列表页中的利率列定义
2. 将列表列头文案从“贷款利率(%)”替换为“执行利率(%)”
3. 通过源码断言和前端构建验证改动结果
## 验证目标
- 流程列表页不再出现“贷款利率(%)”列头
- 流程列表页展示“执行利率(%)”列头
- 前端项目可正常构建

View File

@@ -0,0 +1,33 @@
# 流程详情返回后列表未刷新前端实施记录
## 实施时间
- 2026-03-28
## 问题说明
- 流程列表页 `workflow/index.vue` 仅在 `created()` 中调用 `getList()`
- 该页面在布局层通过 `keep-alive` 缓存
- 从流程详情页返回时,列表页实例会被重新激活而不是重新创建,因此不会自动刷新
## 本次修改
- 为流程列表页增加 `activated()` 生命周期
- 页面从详情页返回并重新激活时,重新执行 `getList()`
- 新增一个无需额外测试框架的 Node 校验脚本,验证列表页激活时会调用 `getList()`
## 影响范围
- 仅影响前端流程列表页返回时的刷新行为
- 不修改详情页路由
- 不修改后端接口和查询参数
## 验证方式
1. 先运行前端校验脚本,确认修复前组件缺少 `activated()`,测试失败
2. 补充 `activated()` 后再次运行校验脚本
## 验证结果
- 执行命令:
```bash
node ruoyi-ui/tests/workflow-index-refresh.test.js
```
- 结果:校验通过
## 备注
- 本次未启动新的前后端进程

View File

@@ -41,6 +41,12 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -12,6 +12,7 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
import io.swagger.v3.oas.annotations.Operation;
@@ -68,8 +69,8 @@ public class LoanPricingWorkflowController extends BaseController
public TableDataInfo list(LoanPricingWorkflow loanPricingWorkflow)
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<LoanPricingWorkflow> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
IPage<LoanPricingWorkflow> result = loanPricingWorkflowService.selectLoanPricingPage(page, loanPricingWorkflow);
Page<LoanPricingWorkflowListVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(page, loanPricingWorkflow);
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(200);
rspData.setMsg("查询成功");

View File

@@ -0,0 +1,27 @@
package com.ruoyi.loanpricing.domain.vo;
import lombok.Data;
import java.util.Date;
@Data
public class LoanPricingWorkflowListVO
{
private String serialNum;
private String custName;
private String custType;
private String guarType;
private String applyAmt;
private String calculateRate;
private String executeRate;
private Date createTime;
private String createBy;
}

View File

@@ -1,7 +1,11 @@
package com.ruoyi.loanpricing.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import org.apache.ibatis.annotations.Param;
/**
* 利率定价流程Mapper接口
@@ -11,5 +15,6 @@ import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
*/
public interface LoanPricingWorkflowMapper extends BaseMapper<LoanPricingWorkflow>
{
IPage<LoanPricingWorkflowListVO> selectWorkflowPageWithRates(Page<?> page,
@Param("query") LoanPricingWorkflow query);
}

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import java.util.List;
@@ -48,7 +49,7 @@ public interface ILoanPricingWorkflowService
* @param loanPricingWorkflow 利率定价流程信息
* @return 分页结果
*/
public IPage<LoanPricingWorkflow> selectLoanPricingPage(Page<LoanPricingWorkflow> page, LoanPricingWorkflow loanPricingWorkflow);
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow);
/**
* 查询利率定价流程详情

View File

@@ -8,6 +8,7 @@ import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
@@ -126,12 +127,9 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
* @return 利率定价流程
*/
@Override
public IPage<LoanPricingWorkflow> selectLoanPricingPage(Page<LoanPricingWorkflow> page, LoanPricingWorkflow loanPricingWorkflow)
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow)
{
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = buildQueryWrapper(loanPricingWorkflow);
// 按更新时间倒序
wrapper.orderByDesc(LoanPricingWorkflow::getUpdateTime);
return loanPricingWorkflowMapper.selectPage(page, wrapper);
return loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, loanPricingWorkflow);
}
/**
@@ -153,10 +151,18 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
if (Objects.nonNull(loanPricingWorkflow.getModelOutputId())){
if (loanPricingWorkflow.getCustType().equals("个人")){
ModelRetailOutputFields modelRetailOutputFields = modelRetailOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
if (Objects.nonNull(modelRetailOutputFields))
{
loanPricingWorkflow.setLoanRate(modelRetailOutputFields.getCalculateRate());
}
loanPricingWorkflowVO.setModelRetailOutputFields(modelRetailOutputFields);
}
if (loanPricingWorkflow.getCustType().equals("企业")){
ModelCorpOutputFields modelCorpOutputFields = modelCorpOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
if (Objects.nonNull(modelCorpOutputFields))
{
loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getCalculateRate());
}
loanPricingWorkflowVO.setModelCorpOutputFields(modelCorpOutputFields);
}
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper">
<select id="selectWorkflowPageWithRates" resultType="com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO">
SELECT
lpw.serial_num AS serialNum,
lpw.cust_name AS custName,
lpw.cust_type AS custType,
lpw.guar_type AS guarType,
lpw.apply_amt AS applyAmt,
CASE
WHEN lpw.cust_type = '个人' THEN mr.calculate_rate
WHEN lpw.cust_type = '企业' THEN mc.calculate_rate
ELSE NULL
END AS calculateRate,
lpw.execute_rate AS executeRate,
lpw.create_time AS createTime,
lpw.create_by AS createBy
FROM loan_pricing_workflow lpw
LEFT JOIN model_retail_output_fields mr ON lpw.model_output_id = mr.id
LEFT JOIN model_corp_output_fields mc ON lpw.model_output_id = mc.id
<where>
<if test="query != null and query.createBy != null and query.createBy != ''">
AND lpw.create_by LIKE CONCAT('%', #{query.createBy}, '%')
</if>
<if test="query != null and query.custName != null and query.custName != ''">
AND lpw.cust_name LIKE CONCAT('%', #{query.custName}, '%')
</if>
<if test="query != null and query.orgCode != null and query.orgCode != ''">
AND lpw.org_code LIKE CONCAT('%', #{query.orgCode}, '%')
</if>
</where>
ORDER BY lpw.update_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,19 @@
package com.ruoyi.loanpricing.domain.vo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class LoanPricingWorkflowListVOTest
{
@Test
void shouldExposeCalculateRateAndExecuteRateFields()
{
LoanPricingWorkflowListVO vo = new LoanPricingWorkflowListVO();
vo.setCalculateRate("6.15");
vo.setExecuteRate("5.80");
assertEquals("6.15", vo.getCalculateRate());
assertEquals("5.80", vo.getExecuteRate());
}
}

View File

@@ -0,0 +1,99 @@
package com.ruoyi.loanpricing.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import com.ruoyi.loanpricing.service.LoanPricingModelService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class LoanPricingWorkflowServiceImplTest
{
@Mock
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
@Mock
private LoanPricingModelService loanPricingModelService;
@Mock
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
@Mock
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@InjectMocks
private LoanPricingWorkflowServiceImpl loanPricingWorkflowService;
@Test
void shouldReturnPagedWorkflowListWithCalculateRate()
{
LoanPricingWorkflowListVO row = new LoanPricingWorkflowListVO();
row.setCalculateRate("6.15");
Page<LoanPricingWorkflowListVO> pageResult = new Page<>(1, 10);
pageResult.setRecords(java.util.List.of(row));
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(pageResult);
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
assertEquals("6.15", result.getRecords().get(0).getCalculateRate());
}
@Test
void shouldUseRetailModelOutputCalculateRateForWorkflowDetail()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
workflow.setCustType("个人");
workflow.setModelOutputId(11L);
workflow.setLoanRate("4.35");
ModelRetailOutputFields retailOutputFields = new ModelRetailOutputFields();
retailOutputFields.setCalculateRate("6.15");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(modelRetailOutputFieldsMapper.selectById(11L)).thenReturn(retailOutputFields);
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("P20260328001");
assertEquals("6.15", result.getLoanPricingWorkflow().getLoanRate());
assertEquals("6.15", result.getModelRetailOutputFields().getCalculateRate());
}
@Test
void shouldUseCorporateModelOutputCalculateRateForWorkflowDetail()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("C20260328001");
workflow.setCustType("企业");
workflow.setModelOutputId(22L);
workflow.setLoanRate("3.80");
ModelCorpOutputFields corpOutputFields = new ModelCorpOutputFields();
corpOutputFields.setCalculateRate("3.932");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(modelCorpOutputFieldsMapper.selectById(22L)).thenReturn(corpOutputFields);
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("C20260328001");
assertEquals("3.932", result.getLoanPricingWorkflow().getLoanRate());
assertEquals("3.932", result.getModelCorpOutputFields().getCalculateRate());
}
}

View File

@@ -50,6 +50,7 @@
<el-table-column label="客户类型" align="center" prop="custType" width="100" />
<el-table-column label="担保方式" align="center" prop="guarType" width="100" />
<el-table-column label="申请金额(元)" align="center" prop="applyAmt" width="120" />
<el-table-column label="测算利率(%)" align="center" prop="calculateRate" width="100" />
<el-table-column label="执行利率(%)" align="center" prop="executeRate" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
@@ -130,6 +131,9 @@ export default {
created() {
this.getList()
},
activated() {
this.getList()
},
methods: {
/** 查询利率定价流程列表 */
getList() {

View File

@@ -0,0 +1,52 @@
const assert = require('assert')
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function loadComponentOptions(filePath) {
const source = fs.readFileSync(filePath, 'utf8')
const scriptMatch = source.match(/<script>([\s\S]*?)<\/script>/)
if (!scriptMatch) {
throw new Error('未找到组件脚本内容')
}
const importNames = []
const importPattern = /^import\s+([A-Za-z0-9_]+)\s+from\s+.*$/gm
let importMatch = importPattern.exec(scriptMatch[1])
while (importMatch) {
importNames.push(importMatch[1])
importMatch = importPattern.exec(scriptMatch[1])
}
const stubImports = importNames.map(name => `const ${name} = {};`).join('\n')
const transformed = `${stubImports}\n${scriptMatch[1]}`
.replace(/^import .*$/gm, '')
.replace(/export default/, 'module.exports =')
const sandbox = {
module: { exports: {} },
exports: {},
require,
console
}
vm.runInNewContext(transformed, sandbox, { filename: filePath })
return sandbox.module.exports
}
const filePath = path.resolve(__dirname, '../src/views/loanPricing/workflow/index.vue')
const component = loadComponentOptions(filePath)
assert.strictEqual(typeof component.activated, 'function', '流程列表页应在激活时刷新数据')
let refreshCount = 0
component.activated.call({
getList() {
refreshCount += 1
}
})
assert.strictEqual(refreshCount, 1, '流程列表页激活时应调用一次 getList')
console.log('workflow-index-refresh test passed')