# Customer Map Selection Frontend Implementation Plan

> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.

**Goal:** Add the frontend customer-id query and customer-internal-code selection step before opening personal or corporate workflow creation dialogs.

**Architecture:** Keep the existing list page and personal/corporate creation dialogs. Insert a shared customer-map selector dialog between customer-type selection and workflow creation, then pass the selected underscore-field record into the existing create dialogs. Existing workflow create APIs stay unchanged and still receive `custIsn` and `custName` in camelCase.

**Tech Stack:** Vue 2, Element UI, existing RuoYi request wrapper, Node static assertion tests, nvm-managed frontend runtime, Playwright/browser verification after implementation.

---

## File Structure

- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
  - Adds personal/corporate customer-map query functions.
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
  - Owns customer-id input, query action, result table, loading state, and row selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
  - Opens customer-map selector after customer type selection and then opens the correct create dialog after row selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
  - Accepts selected customer-map record, fills `custIsn` and `custName`, and makes both fields read-only.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
  - Same selected-record behavior for corporate creation.
- Modify: `ruoyi-ui/package.json`
  - Adds a focused test script for the new customer-map selection checks.
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
  - Static test coverage for API paths, selector fields, parent wiring, selected underscore fields, and read-only create dialog inputs.

## Task 1: Add Frontend API Methods

**Files:**
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
- Modify: `ruoyi-ui/package.json`

- [ ] **Step 1: Write failing API assertions**

Create the first version of `ruoyi-ui/tests/customer-map-selection.test.js`:

```js
const fs = require('fs')
const path = require('path')
const assert = require('assert')

function read(relativePath) {
  return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}

const workflowApi = read('src/api/loanPricing/workflow.js')

assert(
  workflowApi.includes('export function queryPersonalCustomerMap') &&
    workflowApi.includes("url: '/loanPricing/workflow/customer-map/personal'") &&
    workflowApi.includes('params: { custId: custId }'),
  '缺少个人客户号映射查询 API'
)

assert(
  workflowApi.includes('export function queryCorporateCustomerMap') &&
    workflowApi.includes("url: '/loanPricing/workflow/customer-map/corporate'") &&
    workflowApi.includes('params: { custId: custId }'),
  '缺少企业客户号映射查询 API'
)

console.log('customer map selection assertions passed')
```

Add a script in `ruoyi-ui/package.json`:

```json
"test:customer-map-selection": "node tests/customer-map-selection.test.js"
```

- [ ] **Step 2: Run the test to verify it fails**

Run with the project Node version:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: FAIL because the API methods do not exist.

- [ ] **Step 3: Implement the API methods**

Append to `workflow.js`:

```js
// 查询个人客户号映射
export function queryPersonalCustomerMap(custId) {
  return request({
    url: '/loanPricing/workflow/customer-map/personal',
    method: 'get',
    params: { custId: custId }
  })
}

// 查询企业客户号映射
export function queryCorporateCustomerMap(custId) {
  return request({
    url: '/loanPricing/workflow/customer-map/corporate',
    method: 'get',
    params: { custId: custId }
  })
}
```

- [ ] **Step 4: Run the API test**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: PASS for API assertions.

## Task 2: Create Customer Map Selector Dialog

**Files:**
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`

- [ ] **Step 1: Extend the static test for selector requirements**

Add these assertions to `customer-map-selection.test.js`:

```js
const selector = read('src/views/loanPricing/workflow/components/CustomerMapSelector.vue')

assert(
  selector.includes('title="客户号查询"') &&
    selector.includes('v-model="queryForm.custId"') &&
    selector.includes('handleQuery'),
  '客户号查询弹窗缺少客户号输入或查询动作'
)

assert(
  selector.includes("queryPersonalCustomerMap") &&
    selector.includes("queryCorporateCustomerMap") &&
    selector.includes("this.customerType === 'personal'"),
  '客户号查询弹窗未按客户类型调用个人/企业接口'
)

;['cust_id', 'cust_isn', 'cust_name', 'faith_day', 'balance_avg', 'loan_count_his', 'last_loan_date'].forEach((field) => {
  assert(selector.includes(`prop="${field}"`) || selector.includes(`row.${field}`), `查询结果表格缺少字段 ${field}`)
})

assert(
  selector.includes("this.$emit('select', row)") &&
    selector.includes('未查询到客户信息') &&
    selector.includes('请输入客户号'),
  '客户号查询弹窗缺少选择事件或关键提示'
)
```

- [ ] **Step 2: Run the test to verify it fails**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: FAIL because `CustomerMapSelector.vue` does not exist.

- [ ] **Step 3: Implement `CustomerMapSelector.vue`**

Create the component:

```vue
<template>
  <el-dialog title="客户号查询" :visible.sync="dialogVisible" width="900px" append-to-body
             :close-on-click-modal="false" @close="handleClose">
    <el-form :model="queryForm" inline size="small">
      <el-form-item label="客户号">
        <el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>
      </el-form-item>
    </el-form>

    <el-table v-loading="loading" :data="customerList">
      <el-table-column label="客户号" prop="cust_id" align="center" :show-overflow-tooltip="true"/>
      <el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
      <el-table-column label="客户名称" prop="cust_name" align="center" :show-overflow-tooltip="true"/>
      <el-table-column label="用信天数" prop="faith_day" align="center"/>
      <el-table-column label="存款年日均" prop="balance_avg" align="center"/>
      <el-table-column label="历史贷款次数" prop="loan_count_his" align="center"/>
      <el-table-column label="上次贷款日期" prop="last_loan_date" align="center" width="130"/>
      <el-table-column label="操作" align="center" width="90">
        <template slot-scope="scope">
          <el-button type="text" size="mini" @click="handleSelect(scope.row)">选择</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-dialog>
</template>

<script>
import {queryPersonalCustomerMap, queryCorporateCustomerMap} from "@/api/loanPricing/workflow"

export default {
  name: "CustomerMapSelector",
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    customerType: {
      type: String,
      default: undefined
    }
  },
  data() {
    return {
      loading: false,
      queryForm: {
        custId: undefined
      },
      customerList: []
    }
  },
  computed: {
    dialogVisible: {
      get() {
        return this.visible
      },
      set(val) {
        this.$emit('update:visible', val)
      }
    }
  },
  methods: {
    handleQuery() {
      if (!this.queryForm.custId) {
        this.$modal.msgWarning("请输入客户号")
        return
      }
      this.loading = true
      const request = this.customerType === 'personal'
        ? queryPersonalCustomerMap
        : queryCorporateCustomerMap
      request(this.queryForm.custId).then(response => {
        this.customerList = response.data || []
        if (this.customerList.length === 0) {
          this.$modal.msgWarning("未查询到客户信息")
        }
      }).finally(() => {
        this.loading = false
      })
    },
    handleSelect(row) {
      this.$emit('select', row)
      this.dialogVisible = false
    },
    handleClose() {
      this.queryForm.custId = undefined
      this.customerList = []
      this.loading = false
    }
  }
}
</script>
```

- [ ] **Step 4: Run the selector test**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: PASS through selector assertions.

## Task 3: Wire Selector Into Workflow List Page

**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`

- [ ] **Step 1: Extend the test for parent wiring**

Add assertions:

```js
const workflowIndex = read('src/views/loanPricing/workflow/index.vue')

assert(
  workflowIndex.includes('CustomerMapSelector') &&
    workflowIndex.includes('<customer-map-selector') &&
    workflowIndex.includes(':customer-type="selectedCustomerType"') &&
    workflowIndex.includes('@select="handleCustomerMapSelect"'),
  '流程列表页未接入客户号查询选择弹窗'
)

assert(
  workflowIndex.includes('selectedCustomerType') &&
    workflowIndex.includes('selectedCustomerMap') &&
    workflowIndex.includes('showCustomerMapSelector'),
  '流程列表页缺少客户类型、客户映射选择状态'
)

assert(
  workflowIndex.includes("this.selectedCustomerType = type") &&
    workflowIndex.includes('this.showCustomerMapSelector = true') &&
    !workflowIndex.includes("if (type === 'personal') {\\n        this.showPersonalDialog = true"),
  '选择客户类型后应先打开客户号查询弹窗，而不是直接打开新增弹窗'
)

assert(
  workflowIndex.includes('handleCustomerMapSelect') &&
    workflowIndex.includes('this.selectedCustomerMap = row') &&
    workflowIndex.includes('this.showPersonalDialog = true') &&
    workflowIndex.includes('this.showCorporateDialog = true'),
  '选择客户内码后未打开对应新增弹窗'
)
```

- [ ] **Step 2: Run the test to verify it fails**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: FAIL because the list page does not yet use the selector.

- [ ] **Step 3: Update `index.vue` template and script**

Add component usage after the customer type selector:

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

Pass the selected record into both create dialogs:

```vue
<personal-create-dialog
  :visible.sync="showPersonalDialog"
  :customer-map="selectedCustomerMap"
  @success="handleCreateSuccess"
/>

<corporate-create-dialog
  :visible.sync="showCorporateDialog"
  :customer-map="selectedCustomerMap"
  @success="handleCreateSuccess"
/>
```

Import and register the selector:

```js
import CustomerMapSelector from "./components/CustomerMapSelector"
```

Add state:

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

Change `handleSelectType`:

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

Add selected-row handler:

```js
handleCustomerMapSelect(row) {
  this.selectedCustomerMap = row
  if (this.selectedCustomerType === 'personal') {
    this.showPersonalDialog = true
  } else if (this.selectedCustomerType === 'corporate') {
    this.showCorporateDialog = true
  }
}
```

Clear selected state when the create flow completes:

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

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

```js
watch: {
  showPersonalDialog(val) {
    if (!val && !this.showCorporateDialog) {
      this.selectedCustomerMap = null
      this.selectedCustomerType = undefined
    }
  },
  showCorporateDialog(val) {
    if (!val && !this.showPersonalDialog) {
      this.selectedCustomerMap = null
      this.selectedCustomerType = undefined
    }
  }
}
```

- [ ] **Step 4: Run the parent wiring test**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: PASS through parent wiring assertions.

## Task 4: Make Create Dialog Customer Fields Read-Only and Auto-Filled

**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`

- [ ] **Step 1: Extend test for create dialog behavior**

Add assertions:

```js
const personalCreateDialog = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
const corporateCreateDialog = read('src/views/loanPricing/workflow/components/CorporateCreateDialog.vue')

;[
  ['个人新增弹窗', personalCreateDialog],
  ['企业新增弹窗', corporateCreateDialog]
].forEach(([name, source]) => {
  assert(source.includes('customerMap'), `${name} 缺少 customerMap 入参`)
  assert(source.includes(':readonly="true"') || source.includes('readonly'), `${name} 客户内码和客户名称应只读`)
  assert(source.includes('this.customerMap.cust_isn'), `${name} 未从 cust_isn 自动带入客户内码`)
  assert(source.includes('this.customerMap.cust_name'), `${name} 未从 cust_name 自动带入客户名称`)
  assert(source.includes('clearValidate'), `${name} 应清空校验而不是用 resetFields 覆盖已选客户`)
  assert(!source.includes('this.resetForm("form")'), `${name} 不应在 reset() 中调用 resetForm("form") 覆盖只读客户字段`)
})
```

- [ ] **Step 2: Run the test to verify it fails**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: FAIL because dialogs do not yet consume `customerMap`.

- [ ] **Step 3: Update personal create dialog**

Add prop:

```js
customerMap: {
  type: Object,
  default: null
}
```

Make customer fields read-only:

```vue
<el-input v-model="form.custIsn" placeholder="请选择客户内码" :readonly="true"/>
<el-input v-model="form.custName" placeholder="请选择客户名称" :readonly="true"/>
```

In `reset()`, initialize from selected row and do not call `this.resetForm("form")`. The existing RuoYi helper delegates to Element UI `resetFields()`, which can restore stale initial values and overwrite the selected read-only customer fields. After assigning the new form object, only clear validation:

```js
reset() {
  this.form = {
    orgCode: '892000',
    runType: '1',
    custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
    custName: this.customerMap ? this.customerMap.cust_name : undefined,
    idType: undefined,
    idNum: undefined,
    guarType: undefined,
    applyAmt: undefined,
    loanPurpose: undefined,
    loanTerm: undefined,
    bizProof: false,
    loanLoop: false,
    collType: undefined,
    collThirdParty: false
  }
  this.submitting = false
  this.$nextTick(() => {
    if (this.$refs.form) {
      this.$refs.form.clearValidate()
    }
  })
}
```

Keep existing required validation for `custIsn` and `custName`.

- [ ] **Step 4: Update corporate create dialog**

Apply the same prop, read-only inputs, and `reset()` initialization to `CorporateCreateDialog.vue`. Do not call `this.resetForm("form")` in the corporate dialog reset path; assign the new form object with `custIsn` and `custName` from `customerMap`, then use `this.$refs.form.clearValidate()` inside `$nextTick()`.

- [ ] **Step 5: Run the create dialog test**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: PASS through create dialog assertions.

## Task 5: Frontend Verification

**Files:**
- Verify only, no new source files.

- [ ] **Step 1: Run focused customer-map test**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```

Expected: PASS.

- [ ] **Step 2: Run affected existing frontend tests**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
```

Expected: PASS.

- [ ] **Step 3: Build frontend**

Run:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```

Expected: build succeeds.

- [ ] **Step 4: Browser verification after backend and frontend are running**

Use the real application page, not a prototype page:

1. Open the workflow list page.
2. Click “新增”.
3. Select “个人客户”.
4. Confirm the customer-id query dialog opens.
5. Query any customer number.
6. Select one result row.
7. Confirm the personal create dialog opens and `客户内码` / `客户名称` are auto-filled and read-only.
8. Fill the remaining required fields and submit.
9. Repeat the same flow for “企业客户”.
10. Close and reopen the create flow at least once with a second selected row, and confirm the new row's `cust_isn` / `cust_name` replace the previous values in the create dialog.
11. Confirm both created records appear in the list or detail page.

Expected: both personal and corporate flows pass through customer-map selection before creation.

- [ ] **Step 5: Cleanup test processes**

Stop any backend or frontend processes started for verification before ending the task.

- [ ] **Step 6: Commit frontend work**

Use a Chinese commit message and avoid unrelated dirty files:

```bash
git status --short
git add ruoyi-ui/src/api/loanPricing/workflow.js \
  ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue \
  ruoyi-ui/src/views/loanPricing/workflow/index.vue \
  ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
  ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
  ruoyi-ui/package.json \
  ruoyi-ui/tests/customer-map-selection.test.js
git commit -m "新增客户号查询选择前端流程"
```

Do not commit screenshots, browser traces, temporary spreadsheets, or generated test data.

## Task 6: Final End-to-End Verification and Implementation Record

**Files:**
- Create: `doc/implementation-report-2026-04-29-customer-map-selection.md`

- [ ] **Step 1: Run backend and frontend verification together**

After backend and frontend implementation are both complete:

```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
```

Expected: all tests pass.

- [ ] **Step 2: Complete browser verification**

Run the browser flow from Task 5 for both personal and corporate customers. Confirm the backend API response wrapper is still the existing RuoYi wrapper while records inside `data` keep underscore fields.

- [ ] **Step 3: Write implementation report**

Create `doc/implementation-report-2026-04-29-customer-map-selection.md` with:

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

## 修改内容

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

## 验证结果

- 后端测试：填写实际执行命令和通过/失败结果。
- 前端测试：填写实际执行命令和通过/失败结果。
- 真实页面验证：填写个人、企业两条浏览器验证流程和结果。
- 进程清理：填写本次启动的前后端进程是否已关闭。
```

- [ ] **Step 4: Commit implementation report**

```bash
git add doc/implementation-report-2026-04-29-customer-map-selection.md
git commit -m "补充客户号映射选择实施记录"
```
