From 998f0b3c48bbfb1a6f55b3b914a56090eb403215 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Mon, 25 May 2026 16:04:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B5=81=E7=A8=8B=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=BC=96=E8=BE=91=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...report-2026-05-25-workflow-edit-backend.md | 26 +++ ...eport-2026-05-25-workflow-edit-frontend.md | 24 +++ .../LoanPricingWorkflowController.java | 40 ++++ .../service/ILoanPricingWorkflowService.java | 26 +++ .../service/LoanPricingModelService.java | 50 +++-- .../impl/LoanPricingWorkflowServiceImpl.java | 120 ++++++++++- .../service/LoanPricingModelServiceTest.java | 35 ++- .../LoanPricingWorkflowServiceImplTest.java | 201 +++++++++++++++++- ruoyi-ui/src/api/loanPricing/workflow.js | 26 +++ .../components/CorporateCreateDialog.vue | 87 ++++++-- .../components/PersonalCreateDialog.vue | 77 +++++-- .../src/views/loanPricing/workflow/index.vue | 45 +++- .../personal-create-input-params.test.js | 9 + ruoyi-ui/tests/workflow-index-refresh.test.js | 30 ++- 14 files changed, 746 insertions(+), 50 deletions(-) create mode 100644 doc/implementation-report-2026-05-25-workflow-edit-backend.md create mode 100644 doc/implementation-report-2026-05-25-workflow-edit-frontend.md diff --git a/doc/implementation-report-2026-05-25-workflow-edit-backend.md b/doc/implementation-report-2026-05-25-workflow-edit-backend.md new file mode 100644 index 0000000..9469cbc --- /dev/null +++ b/doc/implementation-report-2026-05-25-workflow-edit-backend.md @@ -0,0 +1,26 @@ +# 流程列表编辑功能后端实施记录 + +## 修改内容 +- 在利率定价流程接口新增编辑查询接口:`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。 diff --git a/doc/implementation-report-2026-05-25-workflow-edit-frontend.md b/doc/implementation-report-2026-05-25-workflow-edit-frontend.md new file mode 100644 index 0000000..c33a80a --- /dev/null +++ b/doc/implementation-report-2026-05-25-workflow-edit-frontend.md @@ -0,0 +1,24 @@ +# 流程列表编辑功能前端实施记录 + +## 修改内容 +- 在流程列表操作列新增“编辑”按钮。 +- 编辑按钮只在 `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。 diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java index c8a03dd..347890a 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java @@ -69,6 +69,46 @@ public class LoanPricingWorkflowController extends BaseController return success(result); } + /** + * 查询利率定价流程编辑数据 + */ + @Operation(summary = "查询利率定价流程编辑数据") + @GetMapping("/{serialNum}/edit") + public AjaxResult getEditInfo( + @Parameter(description = "业务方流水号") + @PathVariable("serialNum") String serialNum) + { + return success(loanPricingWorkflowService.selectEditableLoanPricingBySerialNum(serialNum)); + } + + /** + * 编辑个人客户利率定价流程 + */ + @Operation(summary = "编辑个人客户利率定价流程") + @Log(title = "个人客户利率定价流程", businessType = BusinessType.UPDATE) + @PutMapping("/{serialNum}/personal") + public AjaxResult updatePersonal( + @Parameter(description = "业务方流水号") + @PathVariable("serialNum") String serialNum, + @Validated @RequestBody PersonalLoanPricingCreateDTO dto) + { + return success(loanPricingWorkflowService.updatePersonalLoanPricing(serialNum, dto)); + } + + /** + * 编辑企业客户利率定价流程 + */ + @Operation(summary = "编辑企业客户利率定价流程") + @Log(title = "企业客户利率定价流程", businessType = BusinessType.UPDATE) + @PutMapping("/{serialNum}/corporate") + public AjaxResult updateCorporate( + @Parameter(description = "业务方流水号") + @PathVariable("serialNum") String serialNum, + @Validated @RequestBody CorporateLoanPricingCreateDTO dto) + { + return success(loanPricingWorkflowService.updateCorporateLoanPricing(serialNum, dto)); + } + /** * 查询个人客户号映射 */ diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java index 0f8cda2..8abbd7a 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java @@ -34,6 +34,32 @@ public interface ILoanPricingWorkflowService */ public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto); + /** + * 查询利率定价流程编辑数据 + * + * @param serialNum 业务方流水号 + * @return 编辑用流程数据 + */ + public LoanPricingWorkflow selectEditableLoanPricingBySerialNum(String serialNum); + + /** + * 编辑个人客户利率定价流程 + * + * @param serialNum 业务方流水号 + * @param dto 个人客户编辑DTO + * @return 更新后的流程数据 + */ + public LoanPricingWorkflow updatePersonalLoanPricing(String serialNum, PersonalLoanPricingCreateDTO dto); + + /** + * 编辑企业客户利率定价流程 + * + * @param serialNum 业务方流水号 + * @param dto 企业客户编辑DTO + * @return 更新后的流程数据 + */ + public LoanPricingWorkflow updateCorporateLoanPricing(String serialNum, CorporateLoanPricingCreateDTO dto); + /** * 查询利率定价流程列表 * diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java index 6a588b6..460f1e8 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java @@ -40,6 +40,14 @@ public class LoanPricingModelService { private SensitiveFieldCryptoService sensitiveFieldCryptoService; public void invokeModelAsync(Long workflowId) { + invokeModelAndSave(workflowId, false); + } + + public void reinvokeModelAndOverwrite(Long workflowId) { + invokeModelAndSave(workflowId, true); + } + + private void invokeModelAndSave(Long workflowId, boolean overwriteModelOutput) { LoanPricingWorkflow loanPricingWorkflow = loanPricingWorkflowMapper.selectById(workflowId); if (Objects.isNull(loanPricingWorkflow)){ log.error("未找到对应的流程信息,未调用模型服务"); @@ -68,26 +76,43 @@ public class LoanPricingModelService { if (loanPricingWorkflow.getCustType().equals("个人")){ // 个人模型 ModelRetailOutputFields modelRetailOutputFields = modelService.invokePersonalModel(modelInvokeDTO); - modelRetailOutputFieldsMapper.insert(modelRetailOutputFields); + if (overwriteModelOutput && Objects.nonNull(loanPricingWorkflow.getModelOutputId())) + { + modelRetailOutputFields.setId(loanPricingWorkflow.getModelOutputId()); + modelRetailOutputFieldsMapper.updateById(modelRetailOutputFields); + } + else + { + modelRetailOutputFieldsMapper.insert(modelRetailOutputFields); + } log.info("个人模型调用成功"); - LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow(); - workflowToUpdate.setId(loanPricingWorkflow.getId()); - workflowToUpdate.setModelOutputId(modelRetailOutputFields.getId()); - loanPricingWorkflowMapper.updateById(workflowToUpdate); - log.info("更新流程信息成功"); + updateWorkflowModelOutputId(loanPricingWorkflow, modelRetailOutputFields.getId()); }else if (loanPricingWorkflow.getCustType().equals("企业")){ // 企业模型 ModelCorpOutputFields modelCorpOutputFields = modelService.invokeCorporateModel(modelInvokeDTO); - modelCorpOutputFieldsMapper.insert(modelCorpOutputFields); + if (overwriteModelOutput && Objects.nonNull(loanPricingWorkflow.getModelOutputId())) + { + modelCorpOutputFields.setId(loanPricingWorkflow.getModelOutputId()); + modelCorpOutputFieldsMapper.updateById(modelCorpOutputFields); + } + else + { + modelCorpOutputFieldsMapper.insert(modelCorpOutputFields); + } log.info("企业模型调用成功"); - LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow(); - workflowToUpdate.setId(loanPricingWorkflow.getId()); - workflowToUpdate.setModelOutputId(modelCorpOutputFields.getId()); - loanPricingWorkflowMapper.updateById(workflowToUpdate); - log.info("更新流程信息成功"); + updateWorkflowModelOutputId(loanPricingWorkflow, modelCorpOutputFields.getId()); } } + private void updateWorkflowModelOutputId(LoanPricingWorkflow loanPricingWorkflow, Long modelOutputId) + { + LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow(); + workflowToUpdate.setId(loanPricingWorkflow.getId()); + workflowToUpdate.setModelOutputId(modelOutputId); + loanPricingWorkflowMapper.updateById(workflowToUpdate); + log.info("更新流程信息成功"); + } + private void normalizePersonalModelInvokeDTO(ModelInvokeDTO modelInvokeDTO) { modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof())); @@ -98,6 +123,7 @@ public class LoanPricingModelService { private void normalizeCorporateModelInvokeDTO(ModelInvokeDTO modelInvokeDTO) { modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty())); + modelInvokeDTO.setResCover(toZeroOne(modelInvokeDTO.getResCover())); modelInvokeDTO.setIsGreenLoan(toZeroOne(modelInvokeDTO.getIsGreenLoan())); modelInvokeDTO.setIsTradeBuildEnt(toZeroOne(modelInvokeDTO.getIsTradeBuildEnt())); } diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java index cc355db..4697432 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java @@ -1,6 +1,7 @@ package com.ruoyi.loanpricing.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.constant.UserConstants; @@ -47,6 +48,10 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi private static final String WORKFLOW_ADMIN_ROLE_KEY = "headAdmin"; + private static final String WORKFLOW_BRANCH_ADMIN_ROLE_NAME = "支行管理员"; + + private static final String WORKFLOW_BRANCH_ADMIN_ROLE_KEY = "branchAdmin"; + @Resource private LoanPricingWorkflowMapper loanPricingWorkflowMapper; @@ -93,6 +98,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi loanPricingWorkflow.setRunType("1"); } + loanPricingWorkflow.setDeptId(SecurityUtils.getDeptId()); loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName())); loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum())); loanPricingWorkflowMapper.insert(loanPricingWorkflow); @@ -193,6 +199,97 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi return createLoanPricing(entity); } + @Override + public LoanPricingWorkflow selectEditableLoanPricingBySerialNum(String serialNum) + { + LoanPricingWorkflow workflow = selectWorkflowBySerialNum(serialNum); + assertCurrentUserIsCreator(workflow); + workflow.setCustName(sensitiveFieldCryptoService.decrypt(workflow.getCustName())); + workflow.setIdNum(sensitiveFieldCryptoService.decrypt(workflow.getIdNum())); + return workflow; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public LoanPricingWorkflow updatePersonalLoanPricing(String serialNum, PersonalLoanPricingCreateDTO dto) + { + LoanPricingWorkflow editingWorkflow = LoanPricingConverter.toEntity(dto); + return updateLoanPricing(serialNum, editingWorkflow, "个人"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public LoanPricingWorkflow updateCorporateLoanPricing(String serialNum, CorporateLoanPricingCreateDTO dto) + { + LoanPricingWorkflow editingWorkflow = LoanPricingConverter.toEntity(dto); + return updateLoanPricing(serialNum, editingWorkflow, "企业"); + } + + private LoanPricingWorkflow updateLoanPricing(String serialNum, LoanPricingWorkflow editingWorkflow, String expectedCustType) + { + LoanPricingWorkflow existingWorkflow = selectWorkflowBySerialNum(serialNum); + assertCurrentUserIsCreator(existingWorkflow); + if (!expectedCustType.equals(existingWorkflow.getCustType())) + { + throw new ServiceException("客户类型不匹配,不能编辑该流程"); + } + + editingWorkflow.setId(existingWorkflow.getId()); + editingWorkflow.setSerialNum(existingWorkflow.getSerialNum()); + editingWorkflow.setCustType(existingWorkflow.getCustType()); + editingWorkflow.setModelOutputId(existingWorkflow.getModelOutputId()); + validateBusinessTypeAndHistoryRate(editingWorkflow); + validateCollateralTypeAndCouponRate(editingWorkflow); + + String encryptedCustName = sensitiveFieldCryptoService.encrypt(editingWorkflow.getCustName()); + String encryptedIdNum = sensitiveFieldCryptoService.encrypt(editingWorkflow.getIdNum()); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(LoanPricingWorkflow::getId, existingWorkflow.getId()) + .set(LoanPricingWorkflow::getCustIsn, editingWorkflow.getCustIsn()) + .set(LoanPricingWorkflow::getCustName, encryptedCustName) + .set(LoanPricingWorkflow::getIdType, editingWorkflow.getIdType()) + .set(LoanPricingWorkflow::getIdNum, encryptedIdNum) + .set(LoanPricingWorkflow::getGuarType, editingWorkflow.getGuarType()) + .set(LoanPricingWorkflow::getApplyAmt, editingWorkflow.getApplyAmt()) + .set(LoanPricingWorkflow::getBusinessType, editingWorkflow.getBusinessType()) + .set(LoanPricingWorkflow::getLoanRateHistory, editingWorkflow.getLoanRateHistory()) + .set(LoanPricingWorkflow::getCouponRate, editingWorkflow.getCouponRate()) + .set(LoanPricingWorkflow::getLoanTerm, editingWorkflow.getLoanTerm()) + .set(LoanPricingWorkflow::getCollType, editingWorkflow.getCollType()) + .set(LoanPricingWorkflow::getCollThirdParty, editingWorkflow.getCollThirdParty()) + .set(LoanPricingWorkflow::getLoanLoop, editingWorkflow.getLoanLoop()) + .set(LoanPricingWorkflow::getResCover, editingWorkflow.getResCover()) + .set(LoanPricingWorkflow::getRepayMethod, editingWorkflow.getRepayMethod()) + .set(LoanPricingWorkflow::getIsGreenLoan, editingWorkflow.getIsGreenLoan()) + .set(LoanPricingWorkflow::getIsTradeBuildEnt, editingWorkflow.getIsTradeBuildEnt()) + .set(LoanPricingWorkflow::getUpdateBy, buildCurrentCreateBy(SecurityUtils.getLoginUser())) + .set(LoanPricingWorkflow::getUpdateTime, new Date()); + loanPricingWorkflowMapper.update(null, updateWrapper); + loanPricingModelService.reinvokeModelAndOverwrite(existingWorkflow.getId()); + return selectEditableLoanPricingBySerialNum(serialNum); + } + + private LoanPricingWorkflow selectWorkflowBySerialNum(String serialNum) + { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(LoanPricingWorkflow::getSerialNum, serialNum); + LoanPricingWorkflow workflow = loanPricingWorkflowMapper.selectOne(wrapper); + if (workflow == null) + { + throw new ServiceException("记录不存在"); + } + return workflow; + } + + private void assertCurrentUserIsCreator(LoanPricingWorkflow workflow) + { + String currentCreateBy = buildCurrentCreateBy(SecurityUtils.getLoginUser()); + if (!currentCreateBy.equals(workflow.getCreateBy())) + { + throw new ServiceException("只有创建者可以编辑该流程"); + } + } + /** * 查询利率定价流程列表 * @@ -306,7 +403,15 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi { LoanPricingWorkflow scopedQuery = query == null ? new LoanPricingWorkflow() : query; LoginUser loginUser = SecurityUtils.getLoginUser(); - if (!canViewAllWorkflows(loginUser)) + if (canViewAllWorkflows(loginUser)) + { + return scopedQuery; + } + if (canViewBranchWorkflows(loginUser)) + { + scopedQuery.setDataScopeDeptId(loginUser.getDeptId() == null ? -1L : loginUser.getDeptId()); + } + else { scopedQuery.setDataScopeCreateBy(buildCurrentCreateBy(loginUser)); } @@ -331,6 +436,19 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi || WORKFLOW_ADMIN_ROLE_KEY.equals(role.getRoleKey()))); } + private boolean canViewBranchWorkflows(LoginUser loginUser) + { + List roles = loginUser.getUser().getRoles(); + if (roles == null) + { + return false; + } + return roles.stream().anyMatch(role -> role != null + && UserConstants.ROLE_NORMAL.equals(role.getStatus()) + && (WORKFLOW_BRANCH_ADMIN_ROLE_NAME.equals(role.getRoleName()) + || WORKFLOW_BRANCH_ADMIN_ROLE_KEY.equals(role.getRoleKey()))); + } + private String buildCurrentCreateBy(LoginUser loginUser) { SysUser user = loginUser.getUser(); diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java index 5a2dfa4..8c9bc33 100644 --- a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java +++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java @@ -57,6 +57,7 @@ class LoanPricingModelServiceTest workflow.setCustName("cipher-name"); workflow.setIdNum("cipher-id"); workflow.setRepayMethod("分期"); + workflow.setResCover("true"); workflow.setIsGreenLoan("true"); workflow.setIsTradeBuildEnt("false"); workflow.setCollThirdParty("true"); @@ -67,6 +68,7 @@ class LoanPricingModelServiceTest ModelInvokeDTO request = context.modelService.corporateRequest; assertEquals("分期", request.getRepayMethod()); + assertEquals("1", request.getResCover()); assertEquals("1", request.getIsGreenLoan()); assertEquals("0", request.getIsTradeBuildEnt()); assertEquals("1", request.getCollThirdParty()); @@ -77,6 +79,24 @@ class LoanPricingModelServiceTest assertEquals(1, context.corpInsertCount.get()); } + @Test + void shouldOverwriteExistingCorporateModelOutputWhenReinvokingModel() throws Exception + { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setId(4L); + workflow.setModelOutputId(88L); + workflow.setCustType("企业"); + workflow.setCustName("cipher-name"); + workflow.setIdNum("cipher-id"); + TestContext context = createContext(workflow); + + context.service.reinvokeModelAndOverwrite(4L); + + assertEquals(1, context.modelService.corporateCalls.get()); + assertEquals(0, context.corpInsertCount.get()); + assertEquals(1, context.corpUpdateCount.get()); + } + private static LoanPricingWorkflow personalWorkflow(Long id) { LoanPricingWorkflow workflow = new LoanPricingWorkflow(); @@ -96,15 +116,17 @@ class LoanPricingModelServiceTest context.workflow = workflow; context.updatedWorkflow = new AtomicReference<>(); context.retailInsertCount = new AtomicInteger(); + context.retailUpdateCount = new AtomicInteger(); context.corpInsertCount = new AtomicInteger(); + context.corpUpdateCount = new AtomicInteger(); setField(context.service, "modelService", context.modelService); setField(context.service, "loanPricingWorkflowMapper", workflowMapper(context.workflow, context.updatedWorkflow)); setField(context.service, "modelRetailOutputFieldsMapper", - insertCountingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount)); + savingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount, context.retailUpdateCount)); setField(context.service, "modelCorpOutputFieldsMapper", - insertCountingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount)); + savingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount, context.corpUpdateCount)); setField(context.service, "sensitiveFieldCryptoService", new TestSensitiveFieldCryptoService()); return context; } @@ -126,7 +148,7 @@ class LoanPricingModelServiceTest }); } - private static T insertCountingMapper(Class mapperClass, AtomicInteger insertCount) + private static T savingMapper(Class mapperClass, AtomicInteger insertCount, AtomicInteger updateCount) { return proxy(mapperClass, (proxy, method, args) -> { if ("insert".equals(method.getName())) @@ -134,6 +156,11 @@ class LoanPricingModelServiceTest insertCount.incrementAndGet(); return 1; } + if ("updateById".equals(method.getName())) + { + updateCount.incrementAndGet(); + return 1; + } return defaultValue(method); }); } @@ -172,7 +199,9 @@ class LoanPricingModelServiceTest private LoanPricingWorkflow workflow; private AtomicReference updatedWorkflow; private AtomicInteger retailInsertCount; + private AtomicInteger retailUpdateCount; private AtomicInteger corpInsertCount; + private AtomicInteger corpUpdateCount; } private static class CapturingModelService extends ModelService diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java index fa68147..2a80446 100644 --- a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java +++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.when; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -100,6 +101,7 @@ class LoanPricingWorkflowServiceImplTest verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) -> Objects.equals("cipher-name", entity.getCustName()) && Objects.equals("cipher-id", entity.getIdNum()) + && Objects.equals(100L, entity.getDeptId()) && Objects.equals("CUST001", entity.getCustIsn()))); } @@ -148,6 +150,7 @@ class LoanPricingWorkflowServiceImplTest verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture()); assertNull(queryCaptor.getValue().getDataScopeCreateBy()); + assertNull(queryCaptor.getValue().getDataScopeDeptId()); } @Test @@ -162,6 +165,40 @@ class LoanPricingWorkflowServiceImplTest verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture()); assertNull(queryCaptor.getValue().getDataScopeCreateBy()); + assertNull(queryCaptor.getValue().getDataScopeDeptId()); + } + + @Test + void shouldSetCurrentDeptForBranchAdminWhenReturningPagedWorkflowList() + { + setLoginUser(102L, 101L, "8920100", "测试支行管理员", role(102L, "支行管理员", "branchAdmin")); + when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult()); + + loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow()); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class); + verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture()); + + assertEquals(101L, queryCaptor.getValue().getDataScopeDeptId()); + assertNull(queryCaptor.getValue().getDataScopeCreateBy()); + } + + @Test + void shouldKeepBranchAdminWithinDeptDataScopeWhenCreateByQueryIsProvided() + { + setLoginUser(102L, 101L, "8920100", "测试支行管理员", role(102L, "支行管理员", "branchAdmin")); + LoanPricingWorkflow query = new LoanPricingWorkflow(); + query.setCreateBy("8920001"); + when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult()); + + loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), query); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class); + verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture()); + + assertEquals("8920001", queryCaptor.getValue().getCreateBy()); + assertEquals(101L, queryCaptor.getValue().getDataScopeDeptId()); + assertNull(queryCaptor.getValue().getDataScopeCreateBy()); } @Test @@ -193,6 +230,7 @@ class LoanPricingWorkflowServiceImplTest assertEquals("若依-admin", queryCaptor.getValue().getCreateBy()); assertEquals("测试客户经理-8920001", queryCaptor.getValue().getDataScopeCreateBy()); + assertNull(queryCaptor.getValue().getDataScopeDeptId()); } @Test @@ -336,15 +374,151 @@ class LoanPricingWorkflowServiceImplTest dto.setLoanTerm("3"); dto.setCollType("存单质押"); dto.setCouponRate("2.35"); + dto.setResCover("1"); loanPricingWorkflowService.createCorporateLoanPricing(dto); verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) -> Objects.equals("企业", entity.getCustType()) && Objects.equals("新增", entity.getBusinessType()) + && Objects.equals("1", entity.getResCover()) && Objects.equals("2.35", entity.getCouponRate()))); } + @Test + void shouldReturnEditableWorkflowWithPlainSensitiveFieldsForCreator() + { + LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin"); + workflow.setCustName("cipher-name"); + workflow.setIdNum("cipher-id"); + + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三"); + when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234"); + + LoanPricingWorkflow result = loanPricingWorkflowService.selectEditableLoanPricingBySerialNum("P20260525001"); + + assertEquals("张三", result.getCustName()); + assertEquals("110101199001011234", result.getIdNum()); + } + + @Test + void shouldRejectEditableWorkflowWhenCurrentUserIsNotCreator() + { + LoanPricingWorkflow workflow = editableWorkflow("个人", "其他用户-8920001"); + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.selectEditableLoanPricingBySerialNum("P20260525001")); + + assertEquals("只有创建者可以编辑该流程", exception.getMessage()); + } + + @Test + void shouldUpdatePersonalWorkflowAndReinvokeModelForCreator() + { + LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin"); + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + when(sensitiveFieldCryptoService.encrypt("张三")).thenReturn("cipher-name-new"); + when(sensitiveFieldCryptoService.encrypt("110101199001019999")).thenReturn("cipher-id-new"); + when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三"); + when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001019999"); + + PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO(); + dto.setCustIsn("CUST001"); + dto.setCustName("张三"); + dto.setIdType("身份证"); + dto.setIdNum("110101199001019999"); + dto.setGuarType("信用"); + dto.setApplyAmt("200000"); + dto.setBusinessType("新增"); + dto.setLoanTerm("3"); + dto.setLoanLoop("1"); + + loanPricingWorkflowService.updatePersonalLoanPricing("P20260525001", dto); + + ArgumentCaptor updateWrapperCaptor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class); + verify(loanPricingWorkflowMapper).update(any(), updateWrapperCaptor.capture()); + assertTrue(updateWrapperCaptor.getValue().getSqlSet().contains("apply_amt")); + verify(loanPricingModelService).reinvokeModelAndOverwrite(101L); + } + + @Test + void shouldUpdateCorporateWorkflowAndReinvokeModelForCreator() + { + LoanPricingWorkflow workflow = editableWorkflow("企业", "若依-admin"); + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + when(sensitiveFieldCryptoService.encrypt("测试科技有限公司")).thenReturn("cipher-corp-name"); + when(sensitiveFieldCryptoService.encrypt("91110000100000000X")).thenReturn("cipher-corp-id"); + when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("测试科技有限公司"); + when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91110000100000000X"); + + CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO(); + dto.setCustIsn("CORP001"); + dto.setCustName("测试科技有限公司"); + dto.setIdType("统一社会信用代码"); + dto.setIdNum("91110000100000000X"); + dto.setGuarType("质押"); + dto.setApplyAmt("300000"); + dto.setBusinessType("新增"); + dto.setLoanTerm("5"); + dto.setCollType("存单质押"); + dto.setCollThirdParty("1"); + dto.setCouponRate("2.35"); + dto.setResCover("1"); + dto.setIsGreenLoan("1"); + dto.setIsTradeBuildEnt("0"); + + loanPricingWorkflowService.updateCorporateLoanPricing("C20260525001", dto); + + ArgumentCaptor updateWrapperCaptor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class); + verify(loanPricingWorkflowMapper).update(any(), updateWrapperCaptor.capture()); + String sqlSet = updateWrapperCaptor.getValue().getSqlSet(); + assertTrue(sqlSet.contains("res_cover"), sqlSet); + assertTrue(sqlSet.contains("is_green_loan"), sqlSet); + assertTrue(sqlSet.contains("is_trade_construction"), sqlSet); + assertTrue(sqlSet.contains("coll_third_party"), sqlSet); + verify(loanPricingModelService).reinvokeModelAndOverwrite(101L); + } + + @Test + void shouldRejectUpdateWhenCustomerTypeDoesNotMatch() + { + LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin"); + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + + CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO(); + dto.setCustIsn("CORP001"); + dto.setGuarType("信用"); + dto.setApplyAmt("300000"); + dto.setBusinessType("新增"); + dto.setLoanTerm("5"); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.updateCorporateLoanPricing("P20260525001", dto)); + + assertEquals("客户类型不匹配,不能编辑该流程", exception.getMessage()); + } + + @Test + void shouldRejectUpdateWhenCurrentUserIsNotCreator() + { + LoanPricingWorkflow workflow = editableWorkflow("个人", "其他用户-8920001"); + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + + PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO(); + dto.setCustIsn("CUST001"); + dto.setGuarType("信用"); + dto.setApplyAmt("200000"); + dto.setBusinessType("新增"); + dto.setLoanTerm("3"); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.updatePersonalLoanPricing("P20260525001", dto)); + + assertEquals("只有创建者可以编辑该流程", exception.getMessage()); + } + @Test void shouldUseRetailModelOutputFinalCalculateRateForWorkflowDetail() { @@ -479,6 +653,25 @@ class LoanPricingWorkflowServiceImplTest return workflow; } + private LoanPricingWorkflow editableWorkflow(String custType, String createBy) + { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setId(101L); + workflow.setSerialNum("个人".equals(custType) ? "P20260525001" : "C20260525001"); + workflow.setModelOutputId(201L); + workflow.setCustIsn("CUST001"); + workflow.setCustType(custType); + workflow.setCustName("cipher-name"); + workflow.setIdType("身份证"); + workflow.setIdNum("cipher-id"); + workflow.setGuarType("信用"); + workflow.setApplyAmt("100000"); + workflow.setBusinessType("新增"); + workflow.setLoanTerm("3"); + workflow.setCreateBy(createBy); + return workflow; + } + private Page emptyPageResult() { Page pageResult = new Page<>(1, 10); @@ -487,13 +680,19 @@ class LoanPricingWorkflowServiceImplTest } private void setLoginUser(Long userId, String username, String nickName, SysRole... roles) + { + setLoginUser(userId, 100L, username, nickName, roles); + } + + private void setLoginUser(Long userId, Long deptId, String username, String nickName, SysRole... roles) { SysUser user = new SysUser(); user.setUserId(userId); + user.setDeptId(deptId); user.setUserName(username); user.setNickName(nickName); user.setRoles(java.util.Arrays.asList(roles)); - LoginUser loginUser = new LoginUser(userId, 100L, user, Collections.emptySet()); + LoginUser loginUser = new LoginUser(userId, deptId, user, Collections.emptySet()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(authentication); diff --git a/ruoyi-ui/src/api/loanPricing/workflow.js b/ruoyi-ui/src/api/loanPricing/workflow.js index 602346f..1798a43 100644 --- a/ruoyi-ui/src/api/loanPricing/workflow.js +++ b/ruoyi-ui/src/api/loanPricing/workflow.js @@ -17,6 +17,14 @@ export function getWorkflow(serialNum) { }) } +// 查询利率定价流程编辑数据 +export function getWorkflowEdit(serialNum) { + return request({ + url: '/loanPricing/workflow/' + serialNum + '/edit', + method: 'get' + }) +} + // 创建个人客户利率定价流程 export function createPersonalWorkflow(data) { return request({ @@ -26,6 +34,15 @@ export function createPersonalWorkflow(data) { }) } +// 编辑个人客户利率定价流程 +export function updatePersonalWorkflow(serialNum, data) { + return request({ + url: '/loanPricing/workflow/' + serialNum + '/personal', + method: 'put', + data: data + }) +} + // 创建企业客户利率定价流程 export function createCorporateWorkflow(data) { return request({ @@ -35,6 +52,15 @@ export function createCorporateWorkflow(data) { }) } +// 编辑企业客户利率定价流程 +export function updateCorporateWorkflow(serialNum, data) { + return request({ + url: '/loanPricing/workflow/' + serialNum + '/corporate', + method: 'put', + data: data + }) +} + // 查询个人客户号映射 export function queryPersonalCustomerMap(custId) { return request({ diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue index 9911a83..cb9dc2a 100644 --- a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue +++ b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue @@ -1,5 +1,5 @@