Files
loan-pricing/docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md

21 KiB

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:

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:

"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:

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:

// 查询个人客户号映射
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:

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:

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:

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:

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

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:

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:

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:

<customer-map-selector
  :visible.sync="showCustomerMapSelector"
  :customer-type="selectedCustomerType"
  @select="handleCustomerMapSelect"
/>

Pass the selected record into both create dialogs:

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

import CustomerMapSelector from "./components/CustomerMapSelector"

Add state:

showCustomerMapSelector: false,
selectedCustomerType: undefined,
selectedCustomerMap: null,

Change handleSelectType:

handleSelectType(type) {
  this.selectedCustomerType = type
  this.selectedCustomerMap = null
  this.showCustomerMapSelector = true
}

Add selected-row handler:

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:

handleCreateSuccess() {
  this.selectedCustomerMap = null
  this.selectedCustomerType = undefined
  this.getList()
}

Add watchers for dialog close so cancellation also clears the selected record:

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:

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:

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:

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:

customerMap: {
  type: Object,
  default: null
}

Make customer fields read-only:

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

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:

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:

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:

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:

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:

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:

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:

# 2026-04-29 客户号查询选择客户内码实施记录

## 修改内容

- 后端新增个人/企业客户号映射业务接口。
- 后端新增个人/企业客户号映射 mock 接口。
- 配置文件新增 `customer-map` 个人/企业地址并指向本项目 mock。
- 前端新增客户号查询选择弹窗。
- 个人/企业新增流程改为先查询客户号、选择客户内码,再打开新增弹窗。
- 新增弹窗客户内码和客户名称由选中记录自动带入并只读。

## 验证结果

- 后端测试:填写实际执行命令和通过/失败结果。
- 前端测试:填写实际执行命令和通过/失败结果。
- 真实页面验证:填写个人、企业两条浏览器验证流程和结果。
- 进程清理:填写本次启动的前后端进程是否已关闭。
  • Step 4: Commit implementation report
git add doc/implementation-report-2026-04-29-customer-map-selection.md
git commit -m "补充客户号映射选择实施记录"