SFT/docs/superpowers/specs/2026-07-09-phase4-wallet-ma...

228 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 阶段4设计:钱包管理(账户列表 + 个人开户 + 企业开户 + 账户主页/交易/提现/保证金/回单对账单)
- 状态: 用户已授权自主推进,批量决策已确认,自查通过后直接进入 writing-plans
- 上级文档: `2026-07-08-fryl-frontend-overall-design.md`(阶段4=钱包管理);阶段3设计文档 `2026-07-09-phase3-customer-management-design.md` 作为格式与既有约定参照
- 支撑材料: `凡荣e链前端业务需求说明书.md` 2.6.12.6.10(约12351814行)、`swagger_project_scfs_2026-07-09_14-15-23.json`(`/api/wallet-management/personal-opening/*`、`/api/wallet-management/enterprise-open-application/*`、`/api/wallet-management/enterprise-bank-card/*`、`/api/wallet-account/*`)
- 已确认的跨阶段批量决策(与本阶段相关部分):企业开户采用"完整申请单流程"(`enterprise-open-application/create+list+detail+update`,不用一步直提 `/api/enterprise-account-open/create`);移动端确认页(`personal-opening/mobile/*`、`enterprise-open-application/confirm`)不属于本项目范围;渠道编号来源=表单内"所属渠道"选择框(复用阶段3 `ChannelSelect.vue`)
## 1. 范围
**包含**(对应需求文档 2.6.12.6.10):
- 2.6.1 账户列表页(查询+开户入口+开分户/销户/变更/手机号变更/绑卡/查看/余额同步,单选)
- 2.6.2 个人账户开立(选择个人客户→录入补充信息→提交草稿→展示申请编号,轮询开户进度)
- 2.6.3 企业账户开立(基础信息+法人代表+实际控制人+受益人信息+影像资料,提交为"开户申请",支持列表查询/编辑/查看)
- 2.6.4 账户主页面(选中账户的多子账户余额卡片+交易明细列表)
- 2.6.5 交易明细查询(时间范围+分页)
- 2.6.6 主账户提现(金额+短信验证码)
- 2.6.7 回单下载(单笔/批量)
- 2.6.8 对账单下载(时间范围)
- 2.6.9 保证金缴纳
- 2.6.10 保证金释放
- 绑卡管理(个人 `wallet-account/bind-card*`、企业 `enterprise-bank-card/*`,含发送验证码环节)
**不包含(超出后端能力或明确排除,不问用户,列入第7节能力缺口)**:
- 移动端确认相关页面(`personal-opening/mobile/draft-detail`、`mobile/submit`、`enterprise-open-application/confirm`)——批量决策3已定,客户手机端流程范围外,本阶段前端只做到"提交草稿/申请后展示申请编号+轮询状态"
- 企业开户"负责人信息""经办人信息"(`leaderName/leaderIdType/leaderIdNo/leaderMobile/leaderOpto`、`operatorName/operatorIdType/operatorIdNo/operatorMobile/operatorOpto`)——需求文档 2.6.3.4 字段表未列出这两组信息,但后端 `CreateEnterpriseAccountOpenBto`/`UpdateEnterpriseAccountOpenApplicationBto` 中存在对应字段;按"需求文档未要求即不做 UI 采集"原则不在表单展示,提交时不传(见第7节缺口1)
- 企业开户受益人信息的完整反洗钱字段集(`EnterpriseAccountOpenApplicationBeneficiaryBto` 35个字段,如受益所有权形成/终止日期、25%以上收益权/表决权、实际控制形式、高级管理人员职位等)——需求文档 2.6.3.4 仅要求8个字段(客户名称/证件号/证件地址/证件到期日/性别/国籍/是否股东/持股比例),按需求文档裁剪,不采集剩余27个字段(见第7节缺口2)
- 账户列表页"渠道""机构""冻结账户余额"三列——后端 `WalletAccountVo`/`WalletAccountDetailVo` 均无对应字段(见第7节缺口3),不展示,不编造数据
- 账户列表查询条件"所属机构""渠道""客户类型"——`wallet-account/page` 请求体仅支持 `accountName/accountNo/accountType(账户类型A1-A7非客户类型个人/企业)`,与需求文档索引要素不匹配(见第7节缺口3),查询表单按后端实际能力裁剪
- 个人开户"绑定银行账号校验(卡信息查询接口223405)""职业三级码值菜单"——swagger 中未检索到对应查询接口,按纯文本输入实现,不做后端联动校验(见第7节缺口4、5)
- 保证金缴纳/释放的短信验证码步骤——`MarginOperationRequestEo` 请求体不含 `channelNo`/`verifyCode` 字段,与需求文档描述的"发送验证码→校验"流程不符,按接口实际字段实现(不采集验证码,仅金额+摘要),见第7节缺口6
## 2. 接口契约速查
### 2.1 账户列表与账户主页(`/api/wallet-account/*`)
| 接口 | Body | 说明 |
| --- | --- | --- |
| `POST .../page` | `{page,pageSize,accountName,accountNo,accountType}` | 响应 `{total,page,pageSize,records: WalletAccountVo[]}`;`WalletAccountVo` 字段:`id,customerId,accountNo,accountType(A1/A2/A3/A6/A7),accountName,accountRelation('0'主/'1'从),customerNo,accountStatus(NORMAL/FROZEN/CLOSED/LOCKED),mainAccountNo,openDate` |
| `POST .../detail` | `{id}` | 响应 `WalletAccountDetailVo{mainAccountNo,mainBalance,a2AccountNo,a2Balance,a6AccountNo,a6Balance,a7AccountNo,a7Balance}` |
| `POST .../close` | `{id}` | 销户,响应 `data` 无固定类型,按"成功即刷新列表"处理 |
| `POST .../open-sub-account` | `{accountNo}`(主账户账号) | 开分户(A7),同上处理响应 |
| `POST .../update` | `{accountNo,mobile,bankCardNumber,bankNo,bankName}`(仅 `accountNo` 必填) | 账户信息变更(与"手机号变更"分属两个不同接口,`change-mobile` 专用于变更手机号) |
| `POST .../change-mobile` | `{accountNo,newMobile}`(均必填) | 手机号变更 |
| `POST .../sync-balance` | `{id}` | 余额同步,响应 `data` 无固定类型,成功后前端重新调用一次 `detail` 刷新展示 |
| `POST .../transaction-detail` | `{accountNo,channelNo,startDate,endDate,page(默认0),rows(默认10000)}`(`accountNo/channelNo/startDate/endDate` 必填) | 响应 `TransactionDetailVo{accountNo,accountName,curBalance,availBalance,withdrawBalance,preBalance,detailList: TradeDetailItemVo[]}`;`TradeDetailItemVo{id,accountDate,tradeTime,detailType,transType,transAmount,balance,oppAccountNo,oppAccountName,remark}` |
| `POST .../withdraw` | `{channelNo, withdrawRequestEo: WithdrawRequestEo}` | `WithdrawRequestEo{accountNo,accountName,primaryAccount(银行卡号),amount,verifyCode(前端调用发送验证码接口获取),transSummary,receiveUrl,transFee,feeAccountNo,feeAccountName}`,响应 `WithdrawResultVo{success,recode,recodeInfo,accountNo,accountName,serialNo,transAmount}` |
| `POST .../margin-pay` | `MarginOperationRequestEo{accountNo,accountName,tradeAmount,remark}`(**无 channelNo/verifyCode**,见缺口6) | 保证金缴纳,响应 `MarginResultVo{success,recode,recodeInfo,accountNoBzj,accountNameBzj}` |
| `POST .../margin-release` | 同上 `MarginOperationRequestEo` | 保证金释放,响应同 `MarginResultVo` |
| `POST .../download-statement` | `{accountNo,channelNo,startDate,endDate}`(均必填) | 响应 `StatementDownloadVo{fileData(base64 PDF)}` |
| `POST .../download-receipt` | `{accountNo,channelNo,originalSerialNo}`(均必填,**仅支持单笔**,见缺口7) | 响应 `ReceiptDownloadVo{fileData(base64 PDF)}` |
| `POST .../bind-card-list` | `{accountNo,channelNo}`(均必填) | 响应 `BindCardListVo{accountNo,accountName,idType,idNo,curBalance,availBalance,withdrawBalance,preBalance,detailList: BindCardDetailVo[]{mobile,primaryAccount}}` |
| `POST .../bind-card` | `BindCardRequestEo{accountNo,idNo,mobile,bankCardNumber,bankNo,bankName,setDefault,verifyCode,channelNo}` | 响应 `ActionResultVo{success,message}` |
| `POST .../unbind-card` | `UnbindCardRequestEo{accountNo,bankCardNumber,channelNo}` | 响应 `ActionResultVo`;后端注明"最后一张绑卡不可删除" |
### 2.2 个人开户(`/api/wallet-management/personal-opening/*`)
| 接口 | Body | 说明 |
| --- | --- | --- |
| `POST .../query-customer` | `{organizationId,customerCode,customerName,certificateNumber,page,pageSize}`(`page/pageSize` 必填) | 响应 `records: CustomerListPageVo[]`(字段同 `IndividualCustomerDetailVo`:`id,customerCode,customerName,certificateType,certificateNumber,mobilePhone,organizationId,gender,birthDate,ethnicity,certificateEffectiveDate,certificateExpiryDate,issuingAuthority,certificateAddress,occupation`) |
| `POST .../create-draft` | 必填 `accountProperty,bankCardNumber,certificateAddress,certificateNumber,certificateType,channelNo,customerId,customerName,issuingAuthority,mobilePhone,occupation`;选填 `certificateEffectiveDate,certificateExpiryDate,gender,bankNo,bankName,accountClass,signNo,ethnic,jobNote,openLongitude,openLatitude,openIp,fileList: AccountOpenFileItemEo[]{fileNo,fileType,filePage}` | 响应 `CreateDraftResultVo{applicationNo}` |
| `POST .../query-progress` | `{applicationNo}` | 响应 `ApplicationProgressVo{applicationNo,applicationStatus(DRAFT/PENDING_ACCOUNT_OPEN/CONFIRMED/OPENED/APPLY_FAILED),failReason,mainAccountNo,customerNo,createdAt,updatedAt}` |
`accountProperty` 固定传 `"1"`(个人);`fileList` 的 `fileType` 取值:`01`身份证正面、`02`身份证反面、`04`人脸照片(范围外,不采集)、`13`开户协议(范围外,不采集),本阶段仅上传 `01`/`02`。
### 2.3 企业开户申请(`/api/wallet-management/enterprise-open-application/*`、`enterprise-bank-card/*`)
| 接口 | Body | 说明 |
| --- | --- | --- |
| `POST .../list` | `FindEnterpriseOpenApplicationListQto{applicationNo,enterpriseNameLike,applicationStatus,startDate,endDate,page,pageSize}` | 响应 `records: EnterpriseAccountOpenApplicationListVo[]{applicationNo,enterpriseName,businessLicense,applicationStatus,channelNo,confirmTime,mainAccountNo,customerNo,failReason,createdAt,updatedAt}` |
| `POST .../create` | `CreateEnterpriseAccountOpenBto`(见下) | 响应 `CreateDraftResultVo{applicationNo}` |
| `POST .../detail` | `{applicationNo}` | 响应 `EnterpriseAccountOpenApplicationDetailVo`(字段同 Create Bto,另加 `frchainSerialNo,openSerialNo,fileList: EnterpriseAccountOpenApplicationFileVo[],beneficiaryList: EnterpriseAccountOpenApplicationBeneficiaryVo[]`) |
| `POST .../update` | `UpdateEnterpriseAccountOpenApplicationBto`(同 Create Bto 字段,`applicationNo`→`id`) | 响应 `data` boolean。**与阶段3"股东高管信息仅新增可写"不同,企业开户申请的编辑接口支持完整字段回写(含受益人列表),不存在该限制** |
| `POST enterprise-bank-card/list` | `{accountNo,channelNo}`(均必填) | 响应 `BindCardListVo`(结构同2.1个人绑卡列表) |
| `POST enterprise-bank-card/bind` | 必填 `accountNo,accountProperty,bankName,bankNo,channelNo,idNo,mobile,primaryAccount,setDefault,verifyCode` | 企业账户绑卡 |
| `POST enterprise-bank-card/unbind` | 必填 `accountNo,channelNo,primaryAccount` | 企业账户解绑 |
`CreateEnterpriseAccountOpenBto` 完整字段(本阶段实际采集/传递的子集见第4.3节,未采集字段不传):`applicationNo(update时对应id),organizationId,enterpriseId,enterpriseName,businessLicense,certificateType(固定BUSINESS_LICENSE),certificateNumber,certificateEffectiveDate,certificateExpiryDate,certificateAddress,issuingAuthority,signNo,mobilePhone,businessScope,industry,orgcodes,ratcodes,bankCardNumber,bankNo,bankName,legalPersonName,legalPersonIdType,legalPersonIdNo,legalPersonOpto,controllerName,controllerIdType,controllerIdNo,controllerOpto,leaderName/leaderIdType/leaderIdNo/leaderMobile/leaderOpto(不采集),operatorName/operatorIdType/operatorIdNo/operatorMobile/operatorOpto(不采集),applicationStatus,channelNo,openLongitude,openDimensions,openIp,businessLicenseFileNo,creatorId,enterpriseAccountOpenApplicationFileBtoList: [{fileNo,fileType('12'营业执照/'13'开户协议/'01'法人身份证正面/'02'法人身份证反面),filePage}],enterpriseAccountOpenApplicationBeneficiaryBtoList: [{beneName,beneIdType,beneIdNo,beneOpto,beneSex('0'男/'1'女),beneNationality,actCtrl(是否股东,'是'/'否'),actHdRat(持股比例),...(27个反洗钱扩展字段不采集)}]`。
删除接口:未检索到企业开户申请删除接口,列表页不提供删除入口。
### 2.4 枚举取值(均从 swagger 摘取)
- `AccountTypeEnum`: `A1`主账户 / `A2`电商A2账户 / `A3`电商A3账户 / `A6`保证金账户 / `A7`分户账户
- `AccountStatusEnum`: `NORMAL`正常 / `FROZEN`冻结 / `CLOSED`销户 / `LOCKED`锁定
- `OpenApplicationStatusEnum`(个人开户进度): `DRAFT`待确认 / `PENDING_ACCOUNT_OPEN`待调用开户接口 / `CONFIRMED`已确认 / `OPENED`已开立 / `APPLY_FAILED`申请失败
- `EnterpriseOpenStatusEnum`(企业开户申请状态): `DRAFT`待确认 / `CONFIRMED`已确认 / `SUBMITTED`已开立 / `APPLY_FAILED`申请失败
- `CertificateTypeEnum`/`GenderEnum`:复用阶段3已确认取值(`ID_CARD/PASSPORT/OTHER/BUSINESS_LICENSE`、`MALE/FEMALE/UNKNOWN`)
## 3. 页面架构
```
src/views/wallet/
account/
AccountList.vue # 2.6.1 账户列表页(ProTable,单选,:row-selection="{type:'radio'}")
AccountDetail.vue # 2.6.4/2.6.5 账户主页(余额卡片+交易明细+提现/缴纳/释放弹窗入口)
personal-open/
PersonalOpenForm.vue # 2.6.2 个人开户表单(独立路由页)
enterprise-open/
EnterpriseOpenList.vue # 2.6.3 企业开户申请列表(ProTable,查询/新增/编辑/查看,无删除)
EnterpriseOpenForm.vue # 2.6.3 企业开户申请表单(独立路由页,create/edit/detail三态)
src/components/
WalletCustomerPicker.vue # 钱包开户专用客户选择弹窗(个人:query-customer;企业:复用阶段3 fetchEnterpriseCustomerListApi)
BindCardModal.vue # 绑卡管理弹窗(列表+新增绑定+解绑,个人/企业通过 props 区分调用的 API 组)
WithdrawModal.vue # 提现弹窗(金额+发送验证码+验证码输入)
MarginModal.vue # 保证金缴纳/释放弹窗(金额输入,无验证码步骤,见缺口6)
src/api/wallet.js # 本阶段全部接口封装
```
复用阶段3既有组件:`ChannelSelect.vue`(所属渠道选择)、`PersonalCustomerPicker.vue`(企业开户表单的"法人代表/实际控制人/受益人"客户名称选择,复用其选择个人客户的能力)、`IdCardUpload.vue`(企业开户"法人身份证正/反面"上传)、`LicenseUpload.vue`(企业开户"营业执照"上传)。
路由沿用阶段3例外方案:列表页(`AccountList`、`EnterpriseOpenList`)走菜单驱动路由;表单页(`PersonalOpenForm`、`EnterpriseOpenForm`)、账户主页(`AccountDetail`)作为 `extraChildRoutes` 非菜单子路由注册,通过 `router.push` + query 参数跳转:
```
'/wallet/account': 'wallet/account/AccountList' # 菜单
'/wallet/account/detail': 'wallet/account/AccountDetail' # extraChildRoutes,?accountNo=xxx
'/wallet/personal-open': 'wallet/personal-open/PersonalOpenForm' # extraChildRoutes(从账户列表"开户"弹窗跳入)
'/wallet/enterprise-open': 'wallet/enterprise-open/EnterpriseOpenList' # 菜单
'/wallet/enterprise-open/create': 'wallet/enterprise-open/EnterpriseOpenForm' # extraChildRoutes
'/wallet/enterprise-open/edit': 'wallet/enterprise-open/EnterpriseOpenForm' # extraChildRoutes
'/wallet/enterprise-open/detail': 'wallet/enterprise-open/EnterpriseOpenForm' # extraChildRoutes
```
菜单节点仅需配置"钱包管理 > 账户列表""钱包管理 > 企业开户申请"两项(后端权限管理侧配置),个人开户无独立菜单入口(需求文档 2.6.1.2:"点击开户,弹出'个人开户'、'企业开户'"选择框,由账户列表页内部弹窗触发跳转,不是独立菜单)。
## 4. 关键交互设计
### 4.1 账户列表页(`AccountList.vue`)
- 查询表单**按后端实际能力裁剪**为:账户名称(`accountName`,模糊)、账号(`accountNo`,精确)——不提供需求文档描述的"所属机构""渠道""客户类型"下拉(见第7节缺口3)
- 列表列裁剪为:序号、账户名称、账号、账户类型(`accountType` 用 `StatusTag` 展示 A1/A2/A3/A6/A7 中文名)、主/从标识(`accountRelation`)、状态(`accountStatus`)——不展示"活期账户余额""冻结账户余额""保证金账户余额""渠道""机构"列
- **余额展示折中方案**:对当前页每一条 `accountRelation==='0'`(主账户)的记录,前端并发调用一次 `wallet-account/detail`,将返回的 `mainBalance`/`a6Balance` 追加展示在该行"活期余额""保证金余额"两个补充列(从账户行留空,不重复查询);任一 detail 请求失败不阻塞整表渲染,失败单元格显示 `--`
- 单选(`row-selection: {type:'radio'}`),选中后启用:开分户(仅当选中行 `accountType==='A1'`)、销户、变更、手机号变更、绑卡、查看、余额同步;未选中全部禁用(除"开户"始终可用)
- 点击"开户"→ `<a-dropdown>``<a-modal>` 二选一("个人开户"跳 `/wallet/personal-open`;"企业开户"跳 `/wallet/enterprise-open/create`)
- 点击账户名称/"查看"→ `router.push('/wallet/account/detail?accountNo=xxx&id=xxx')`
- "变更"弹窗:复用 `wallet-account/update` 字段(手机号/绑定银行卡号/开户行号/开户行名称);"手机号变更"弹窗单独走 `change-mobile`(需求文档将两者列为并列按钮,后端也是两个独立接口,不合并)
- "余额同步"点击后调用 `sync-balance`,成功后重新拉取当前行 `detail` 刷新余额补充列,不整表重新查询
### 4.2 个人开户表单(`PersonalOpenForm.vue`)
- 所属渠道:`ChannelSelect.vue`
- 账户名称:点击"选择客户"打开 `WalletCustomerPicker`(个人模式,调用 `query-customer`),选中后回填 `customerId/customerName/certificateType/certificateNumber/certificateEffectiveDate/certificateExpiryDate/issuingAuthority/gender/certificateAddress/occupation`(字段只读展示,与需求文档"选择个人客户后反显字段信息"一致)
- 手机号码:文本输入,11位数字校验(复用 `phoneValidatorRule`)
- 绑定银行账号:文本输入(缺口4:无卡号查询校验接口,仅做非空+纯数字长度校验)
- 职业:文本输入(缺口5:无职业三级码值菜单接口,退化为文本框,提交时按选中客户 `occupation` 字段预填,可编辑)
- 证件照片正/反面:复用 `IdCardUpload.vue`(`side='front'/'back'`),上传成功后取 `fileNo` 组装进 `fileList`(`fileType='01'/'02'`)
- 提交:组装 `create-draft` 请求体(`accountProperty` 固定 `"1"`),成功后展示 `applicationNo`,弹出"申请已提交,等待客户手机扫码确认"提示框,提供"刷新状态"按钮调用 `query-progress` 轮询展示 `applicationStatus`(不做自动定时轮询,避免无限请求,由用户手动点击刷新,与阶段3"移动端后续流程范围外"的既定处理方式一致)
- 提交按钮 loading 态防重复点击(全局交互规则)
### 4.3 企业开户表单(`EnterpriseOpenForm.vue`)
四个信息区,均按需求文档 2.6.3.4 字段表实现(不使用后端 Bto 的全量字段):
1. **基础信息**:所属渠道(`ChannelSelect`)、账户名称(点击"选择企业客户"→复用阶段3企业客户列表查询能力新建一个只读用途的企业客户选择弹窗,选中后额外调用一次 `enterprise-customer/detail` 补全字段,回填 `enterpriseId/enterpriseName/businessLicense/certificateNumber/certificateEffectiveDate/certificateExpiryDate/certificateAddress`)、经营范围(文本,可编辑)、所属行业(`industry`,文本输入——**订正**:此前版本误写为"职业",`CreateEnterpriseAccountOpenBto` 并无 `occupation` 字段,企业场景对应字段应为 `industry`)、手机号码(文本,11位校验)、绑定银行账号/开户行号/开户行全称(文本输入)。`certificateType` 固定传 `BUSINESS_LICENSE`,不展示选择框
2. **法人代表信息**:客户名称(点击"选择"打开 `PersonalCustomerPicker`,选中后回填 `legalPersonName`,并需要额外一次个人客户 `detail` 查询补全 `certificateType`/`certificateNumber`/`certificateExpiryDate` 映射为 `legalPersonIdType/legalPersonIdNo/legalPersonOpto`——因列表接口字段不含证件到期日,与阶段3股东高管信息同样的字段补全问题)
3. **实际控制人信息**:默认与"法人代表信息"相同(选中法人代表后自动回填,`controllerName/controllerIdType/controllerIdNo/controllerOpto` = 法人代表对应字段),同时提供"重新选择"按钮可打开 `PersonalCustomerPicker` 单独指定
4. **受益人信息**(单条,非列表,虽然后端是数组 `enterpriseAccountOpenApplicationBeneficiaryBtoList`,前端仅收集一条,提交时包裹为长度1的数组):客户名称(默认同法人代表,可重新选择,选中后额外查一次个人客户 `detail`)、证件号(`beneIdNo`,取自 `certificateNumber`)、证件地址(`beneAddress`,取自 `certificateAddress`——**核实**:经重新核对 swagger `EnterpriseAccountOpenApplicationBeneficiaryBto` 原始定义,`beneAddress` 字段确实存在,此前一版"订正"误判为不存在并删除该字段,现予以恢复)、证件到期日(`beneOpto`,取自 `certificateExpiryDate`)、性别(`beneSex`,由选中客户 `GenderEnum` 映射为 `'0'`男/`'1'`女)、国籍(`beneNationality`,固定文本"中国",不可编辑)、是否股东(`actCtrl`,单选"是"/"否",默认"是")、持股比例(`actHdRat`,数字输入,单位%,0-100)。`beneIdType`(证件类型)不作为独立 UI 字段展示,提交时取自选中客户的 `certificateType`(与法人代表/实际控制人区块"不单独展示证件类型选择框"的处理方式一致)
5. **影像信息**:营业执照(`LicenseUpload`→`fileType='12'`)、法人身份证正/反面(`IdCardUpload`→`fileType='01'/'02'`),三者组装进 `enterpriseAccountOpenApplicationFileBtoList`
提交:`create` 时不传 `applicationNo`;`update` 时用路由 query 的 `applicationNo` 映射到 `id`。**编辑模式下四个信息区均可编辑**(与阶段3"股东高管信息仅新增可写"不同,已在2.3节注明,`UpdateEnterpriseAccountOpenApplicationBto` 支持全量回写)。查看模式(`detail`)全部字段 `disabled`,并展示 `applicationStatus`/`failReason`/`mainAccountNo`/`customerNo`(开立成功后回填)。
### 4.4 企业开户申请列表(`EnterpriseOpenList.vue`)
- 查询条件:申请编号、企业名称(模糊)、申请状态(`EnterpriseOpenStatusEnum` 下拉)、创建时间范围(`startDate`/`endDate`)
- 列表列:申请编号、企业名称、营业执照号、状态(`StatusTag`)、渠道编号、主账户账号、失败原因、创建时间
- 操作列:查看(→ detail 路由)、编辑(仅 `applicationStatus==='DRAFT'` 时可编辑,其余状态按钮禁用,理由:已确认/已开立的申请不应再改动基础信息,需求文档虽未明确限制但业务合理性决策,如实标注为前端侧约束,非后端强制)
- 无删除入口(第2.3节已注明无删除接口)
### 4.5 账户主页(`AccountDetail.vue`)——2.6.4/2.6.5
- 页面载入:用路由 query 的 `id``wallet-account/detail` 展示 `mainBalance/a2Balance/a6Balance/a7Balance` 四张余额卡片(无数据的子账户卡片不展示,如 `a2AccountNo` 为空则不渲染 A2 卡片)
- 主账户卡片提供"提现"按钮(`WithdrawModal`);保证金账户卡片提供"缴纳"/"释放"按钮(`MarginModal`,`mode='pay'|'release'`)
- 日期选择器:当月/上月/近三月快捷按钮 + 自定义起止日期,默认"今天"(`startDate=endDate=今天`),变更后调用 `transaction-detail` 刷新列表(该接口一次性返回全部匹配记录,`rows` 默认传大值如 `1000`,前端本地做客户端分页展示,因为响应结构 `detailList` 是数组而非标准 `{total,records}` 分页结构,不复用 `ProTable` 的服务端分页模式,改用普通 `<a-table>` + 前端 `pagination` 配置)
- 交易明细列:序号、系统流水号(`id`)、记账日期(`accountDate`)、交易类型(`transType`)、对方户名(`oppAccountName`)、对方账号(`oppAccountNo`)、交易金额(`transAmount`,按 `detailType` 或金额符号用红/绿色区分借贷,若字段本身不含正负号标识则按 `detailType` 字符串包含"借/贷"关键字判断,具体规则以实测返回值为准,先按金额字符串是否带负号兜底)、账户余额(`balance`)、交易时间(`tradeTime`)、摘要(`remark`)
- 排序:前端按 `accountDate` 倒序、同日期按 `id` 升序本地排序(接口未声明排序保证,前端兜底排序,与需求文档规则2.6.4.5一致)
- "回单"按钮:未勾选任何流水→按当前查询时间范围批量调用(受限于接口只支持单笔 `originalSerialNo`,批量场景前端循环逐笔调用 `download-receipt` 后打包为多个下载动作,不做后端不支持的"批量合并PDF/ZIP",见缺口7,提示文案调整为"将依次下载 N 笔回单");勾选一笔或多笔→逐笔调用
- "对账单"按钮:调用 `download-statement`,base64 解码为 Blob 触发浏览器下载,文件名 `对账单_${accountName}_${startDate}_${endDate}.pdf`
- 资金操作(提现/缴纳/释放)成功后:自动重新调用 `detail` + `transaction-detail` 刷新余额与列表(需求文档2.6.4.5规则)
### 4.6 提现/保证金弹窗
- `WithdrawModal`:金额输入(校验 `0 < amount <= mainBalance`)、绑定银行卡号(从 `detail`/`bind-card-list` 取默认卡自动填充只读展示)、"发送验证码"按钮调用通用 `/api/auth/send-sms-code`(`businessType` 采用约定值 `WALLET_WITHDRAW`,swagger 未定义该字段合法枚举,属工程假设,见缺口8)、验证码输入框、确定按钮组装 `WithdrawRequestEo` 提交
- `MarginModal`:金额输入(校验上限:`pay` 模式不超过主账户可用余额,`release` 模式不超过保证金账户余额,两个上限值均来自 `AccountDetail` 页面已加载的 `mainBalance`/`a6Balance`)+ 摘要(可选文本),**不提供验证码输入框**(因 `MarginOperationRequestEo``verifyCode` 字段,提供了也无法真正传给后端校验,不做误导性 UI;已记入缺口6并同步登记 `缺失的后端接口.md`)
### 4.7 绑卡管理弹窗(`BindCardModal.vue`)
- Props:`mode='personal'|'enterprise'`,内部按 mode 切换调用 `wallet-account/bind-card-list``enterprise-bank-card/list` 等对应三个接口
- 列表展示已绑卡片(`detailList: [{mobile,primaryAccount}]`);"新增绑卡"表单:银行卡号/开户行号/开户行名称/手机号/证件号(企业模式另需 `accountProperty`)+ "发送验证码"(`businessType` 约定值 `WALLET_BIND_CARD`,同缺口8)+验证码+是否默认;"解绑"按钮对每张已绑卡单独触发(最后一张不可解绑,提交后端会报错,前端不做客户端拦截,直接展示后端返回的 message)
## 5. 表单校验补充
不新增 `validators.js` 规则(手机号/金额校验复用现有 `phoneValidatorRule`/AntD 内置 `number` 类型规则),金额上限比较用组件内联 `validator` 函数(参考 `MarginModal`/`WithdrawModal` 各自的 `amount` 字段规则,依据当前已加载的余额上限动态生成,不适合提取为全局 validators)。
## 6. 业务规则映射
| 需求规则 | 落地方式 |
| --- | --- |
| 账户列表不支持多选,每次只能选中一条 | `row-selection: {type: 'radio'}` |
| 开分户仅主账户(A1)支持 | 选中行 `accountType!=='A1'` 时"开分户"按钮禁用 |
| 状态为"销户"禁用除查看外所有操作 | 选中行 `accountStatus==='CLOSED'` 时除"查看"外按钮全部禁用 |
| 状态为"冻结"禁用交易类操作 | 选中行 `accountStatus==='FROZEN'` 时"提现/缴纳/释放"入口禁用(这两个操作在账户主页而非列表页,列表页选中冻结账户后仍可"查看"进入主页,主页内按钮再次按状态禁用) |
| 提现/绑卡验证码有效期5分钟 | 前端不做本地倒计时强制失效校验(接口报错即视为失效,展示 message),"发送验证码"按钮点击后60秒内禁用防重复发送(常见反截流交互,需求未明确但属合理默认) |
| 操作频率限制(每分钟最多3次提现) | 前端不做本地计数拦截,依赖后端报错 message 展示,遵循"不做后端未明确的客户端强校验"原则 |
| 并发控制(同一账户同时只能一笔资金操作) | 前端每个弹窗提交时按钮 loading 禁用重复点击;跨弹窗并发依赖后端报错处理 |
| 对账单默认当前自然月 | 时间选择器初始值改为"当月"(1日至今日),而非需求2.6.4"默认今天"——2.6.4/2.6.8 两节对"默认时间范围"的描述不一致(前者说"今天",后者对账单场景说"默认当前自然月"),按场景区分:账户主页交易明细默认"今天"(遵循2.6.4.5),对账单下载弹窗单独的时间选择默认"当月"(遵循2.6.8.5),两处不共用同一个日期状态 |
## 7. 已识别的后端能力缺口(登记入 `缺失的后端接口.md`)
1. **企业开户 Bto/Vo 含"负责人信息""经办人信息"两组字段,但需求文档字段表未要求**——不采集,提交时不传;若后端将其中某字段标记为服务端必填,以实测报错为准调整(当前 swagger 未见 `required` 数组包含这两组字段)
2. **企业开户"受益人信息"后端支持35+反洗钱合规字段,需求文档仅要求8个**——按需求文档裁剪,只采集/回填 `beneName/beneIdNo/beneAddress/beneOpto/beneSex/beneNationality/actCtrl/actHdRat`(对应客户名称/证件号/证件地址/证件到期日/性别/国籍/是否股东/持股比例),另 `beneIdType` 不作为独立 UI 字段但提交时随选中客户 `certificateType` 一并回填(与法人代表/实际控制人一致的隐式字段处理方式),其余 26+ 个反洗钱扩展字段(受益所有权形成/终止日期、25%以上收益权/表决权、实际控制形式、高级管理人员职位、`beneBirthday`/`beneMobile` 等)不提交
3. **账户列表页字段/查询条件与需求文档不完全匹配**——`WalletAccountVo`/`WalletAccountDetailVo` 均无"渠道""机构""冻结账户余额"字段,`wallet-account/page` 查询体不支持按"所属机构""渠道""客户类型(个人/企业)"过滤(仅支持账户名称/账号/账户类型A1-A7);列表页按后端实际返回字段裁剪展示列与查询条件
4. **个人/企业开户"绑定银行账号"无卡号查询校验接口**——需求文档提及"卡信息查询接口223405"用于校验卡号正确性,swagger 中未检索到对应管理端接口(`wallet-account`/`wallet-management` 分组下均无 `query-bank-card`/`verify-card` 类路径),前端仅做格式校验,不做后端联动查卡校验
5. **"职业"字段无三级码值菜单查询接口**——需求文档要求"职业表菜单选择,选择至三级码值名称",swagger 未检索到职业字典接口,退化为文本输入
6. **保证金缴纳/释放请求体 `MarginOperationRequestEo` 缺失 `channelNo`/`verifyCode` 字段**——需求文档2.6.9/2.6.10均描述"发送验证码→校验"两步流程,且接口 `description` 也提及"验证码由前端自行调用发送验证码接口获取",但实际 schema 字段没有承载验证码的位置,前后端契约不一致;前端按 schema 实际字段实现(仅金额+摘要),不采集/不校验验证码,不做误导性 UI(与"不做后端未明确的强校验/不Mock接口"原则一致)。**与之对比**,提现接口 `WithdrawRequestEo` 正常含 `verifyCode` 字段,行为不对称,建议后端后续为 `MarginOperationRequestEo` 补齐这两个字段
7. **回单下载接口仅支持单笔(`originalSerialNo` 单个字符串),不支持批量/流水号数组**——需求文档2.6.7描述"批量下载:不勾选任何流水,点击回单按钮…返回一个PDF文件,内含多页"及"多笔/批量下载:返回一个zip",与实际接口签名(单笔单流水号,响应仅一个PDF)不符;前端改为"未勾选=按当前查询范围内所有可见流水逐笔循环调用并逐个触发下载"的兜底方案,不生成合并PDF/ZIP,已在4.5节标注
8. **`/api/auth/send-sms-code``businessType` 字段无枚举约束文档**——该接口描述提到用于"手机号注册""找回密码(FIND_PASSWORD)"场景,钱包提现/绑卡场景复用该接口时 `businessType` 取值(`WALLET_WITHDRAW`/`WALLET_BIND_CARD`)属前端工程假设,非后端文档确认值,若后端有白名单限制会导致发送验证码报错,以实测报错信息为准调整取值
9. **无企业开户"开分户""销户"专属接口区分**——企业主账户开立后从账户(A2/A3/A7)是否需要类似个人开户"主账户开立成功后从账户随之开立"的行为,swagger 未见企业专属"query-progress"等价接口,企业开户进度查询复用 `enterprise-open-application/detail``applicationStatus` 字段代替(已在4.3节体现,未单独建 query-progress 接口调用)
10. **无独立"企业客户选择"轻量查询接口专供开户场景**——个人开户有专属 `personal-opening/query-customer`,企业开户基础信息的"企业客户选择"沿用阶段3 `enterprise-customer/page` 接口(非钱包模块专属,行为应一致,只是接口分组不同,风险较低,仅记录以备后续核实)
## 8. 自查(占位符/矛盾/歧义/范围)
- 全文无 "TBD"/"待定" 类占位符;标注"以实测为准"的均为基于当前 swagger 文档信息不足的合理工程假设,已在第7节逐项列出
- 范围与总体设计文档阶段4定义(钱包管理)一致,未扩展至授信/商户/订单模块
- 第3节路由方案沿用阶段3"菜单驱动+extraChildRoutes非菜单子路由"的既定例外模式,无新增技术方案分歧
- 与批量决策结果核对:企业开户采用完整申请单流程(2.3/4.3节);移动端确认页面排除(第1节"不包含");渠道编号来源=`ChannelSelect`表单内选择(4.2/4.3节),与批量决策结论一致,无冲突
- 4.6/7节已如实记录"保证金验证码"需求与接口的不一致,未强行编造字段或Mock校验逻辑,符合项目"不Mock接口"硬性约束
设计自查通过,直接进入 writing-plans 生成实施计划。