docs: 阶段4钱包管理设计文档

main
halo 2026-07-09 21:59:19 +08:00
parent 95d14db49b
commit be913bd9b0
1 changed files with 227 additions and 0 deletions

View File

@ -0,0 +1,227 @@
# 阶段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企业客户列表查询能力新建一个只读用途的企业客户选择弹窗,选中后回填 `enterpriseId/enterpriseName/businessLicense/certificateNumber/certificateEffectiveDate/certificateExpiryDate/certificateAddress`)、经营范围(文本,可编辑)、职业(文本,缺口5同上)、手机号码(文本,11位校验)、绑定银行账号/开户行号/开户行全称(文本输入)。`certificateType` 固定传 `BUSINESS_LICENSE`,不展示选择框
2. **法人代表信息**:客户名称(点击"选择"打开 `PersonalCustomerPicker`,选中后回填 `legalPersonName`,并需要额外一次个人客户 `detail` 查询补全 `certificateNumber`/`certificateExpiryDate` 映射为 `legalPersonIdType/legalPersonIdNo/legalPersonOpto`——因列表接口字段不含证件到期日,与阶段3股东高管信息同样的字段补全问题)
3. **实际控制人信息**:默认与"法人代表信息"相同(选中法人代表后自动回填,`controllerName/controllerIdType/controllerIdNo/controllerOpto` = 法人代表对应字段),同时提供"重新选择"按钮可打开 `PersonalCustomerPicker` 单独指定
4. **受益人信息**(单条,非列表,与需求文档"受益人信息"字段表结构一致,虽然后端是数组 `enterpriseAccountOpenApplicationBeneficiaryBtoList`,前端仅收集一条,提交时包裹为长度1的数组):客户名称(默认同法人代表,可重新选择)、证件号(`beneIdNo`)、证件地址(`beneAddress`)、证件到期日(`beneOpto`)、性别(`beneSex`,由选中客户 `GenderEnum` 映射为 `'0'`男/`'1'`女)、国籍(`beneNationality`,固定文本"中国",不可编辑)、是否股东(`actCtrl`,单选"是"/"否",默认"是")、持股比例(`actHdRat`,数字输入,单位%,0-100)
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/beneIdType/beneIdNo/beneOpto/beneSex/beneNationality/actCtrl/actHdRat`,其余27个字段不提交(不传等同后端存空值,若后端有强制校验以实测调整)
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 生成实施计划。