Compare commits
2 Commits
iteration/
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8229ad9248 | ||
|
|
fc01fa2ac9 |
@@ -3,12 +3,9 @@ name: 构建并部署前端到测试环境
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- iteration/august
|
||||
- main
|
||||
- dev
|
||||
- test
|
||||
# 如果不再需要 main 分支触发构建,可以把下面这行删掉;
|
||||
# 如果只是想保留构建(不部署),可以留着,反正部署条件已经只认 iteration/august
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY: registry.boss160.cn
|
||||
@@ -30,7 +27,7 @@ jobs:
|
||||
- name: 设置镜像标签
|
||||
id: tag
|
||||
run: |
|
||||
if [ "${{ github.ref }}" = "refs/heads/iteration/august" ]; then
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
||||
echo "tag=dev" >> $GITHUB_OUTPUT
|
||||
@@ -54,8 +51,8 @@ jobs:
|
||||
docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
docker push ${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
|
||||
- name: 部署到本地(仅 iteration/august 分支)
|
||||
if: github.ref == 'refs/heads/iteration/august'
|
||||
- name: 部署到本地(仅 main 分支)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
# 确保部署目录存在
|
||||
mkdir -p ${{ env.DEPLOY_DIR }}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Change: 代理扫码分销注册与提现资料资格前端对接
|
||||
|
||||
## Why
|
||||
|
||||
新增强代理扫码分销注册与提现资料资格能力:代理可通过 H5 注册页扫码注册,审批通过后登录后台;代理本人可维护提现资料资格并发起/重提提现,超管可作废资格。后端契约已按 `docs/产品迭代8月份/frontend-api-simple.md` 落地,后台管理端当前缺少对应接口封装、独立注册页与资格管理入口。
|
||||
|
||||
本次前端按后端实际契约对齐:注册验码 `POST /api/c/v1/auth/send-code`(`scene` 为 `bind_phone`,返回 `cooldown_seconds`);扫码注册 `POST /api/c/v1/agent-distribution-registrations`(成功仅返回「待审批」);店铺列表/详情/更新响应新增 `distribution_code`;提现资料资格提交/查询/作废与提现申请重提/详情按新文档补齐。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 `agent-distribution-registration` 能力:独立注册页挂在后台项目内、静态路由免登录访问(H5 与 PC 同一套表单),支持分销码自动填、短信验证码 60s 倒计时、密码掩码与强度提示、省市区级联;提交成功进入「待审批」结束页,失败统一提示「分销码不可用」且不复用同一验证码。
|
||||
- 新增公开 API 封装:`sendCode()` 与 `registerAgent()`,请求不带登录态(`requestOptions.withToken: false`)。
|
||||
- 店铺模块补充 `distribution_code` 字段类型,店铺详情页展示分销码与注册二维码。
|
||||
- 佣金模块新增:提现资料资格提交/替换、资格查询(证件号脱敏、含审批与作废信息)、资格作废(超管,`reason` 必填)、重提被驳回的提现、提现申请详情(含尝试记录与异常标记)。
|
||||
- 扩展企微审批场景业务类型:`agent_distribution_approval`、`withdrawal_qualification_approval`、`commission_withdrawal_approval`。
|
||||
- 提现交互约束:资格替换合同/法人身份证后旧资格失效需重新审批;提现仅在有效资格且余额充足时可提交,否则不冻结、不建单。
|
||||
- 不实现后端接口、数据库、企微回调;不实现 C 端(客户)登录体系。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `agent-distribution-registration`、`shop-management`、`commission-management`、`wecom-scenes`
|
||||
- Affected code: `src/api/modules/agentDistribution.ts`、`src/api/modules/commission.ts`、`src/types/api/shop.ts`、`src/types/api/commission.ts`、`src/types/api/wecom.ts`、`src/router/routes/staticRoutes.ts`、`src/router/guards/permission.ts`、`src/views/agent-registration/index.vue`、`src/views/shop-management/detail/index.vue`、`src/views/commission-management/my-commission/index.vue`、`src/views/settings/wecom/scenes/index.vue`
|
||||
- Dependencies: `docs/产品迭代8月份/frontend-api-simple.md`
|
||||
@@ -1,61 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 独立代理注册页免登录访问
|
||||
代理扫码分销注册页 MUST 独立于登录态提供,未登录可访问,且挂在后台项目内(H5 与 PC 共用同一套表单);注册流程 MUST NOT 复用当前登录态。
|
||||
|
||||
#### Scenario: 未登录访问注册页
|
||||
- **GIVEN** 用户未登录并通过扫码或直接访问注册入口
|
||||
- **WHEN** 其打开注册页面
|
||||
- **THEN** 页面 MUST 正常渲染注册表单
|
||||
- **AND** MUST NOT 跳转到登录页
|
||||
|
||||
#### Scenario: 已登录用户访问注册页
|
||||
- **GIVEN** 用户已登录后台
|
||||
- **WHEN** 其打开注册页面
|
||||
- **THEN** 注册流程 MUST 不携带登录态
|
||||
|
||||
### Requirement: 发送短信验证码
|
||||
注册页 MUST 调用 `POST /api/c/v1/auth/send-code` 发送验证码,请求体 `{ phone, scene }`(`scene` 为 `bind_phone`),响应 `data.cooldown_seconds` MUST 用于 60 秒倒计时;请求 MUST 不携带 Token。
|
||||
|
||||
#### Scenario: 发送验证码并倒计时
|
||||
- **GIVEN** 用户输入手机号
|
||||
- **WHEN** 其点击获取验证码
|
||||
- **THEN** 系统 MUST 调用发送验证码接口
|
||||
- **AND** 按钮 MUST 进入 60 秒倒计时且不可重复点击
|
||||
|
||||
#### Scenario: 注册失败后不复用验证码
|
||||
- **GIVEN** 一次注册提交失败
|
||||
- **WHEN** 用户再次尝试注册
|
||||
- **THEN** 页面 MUST 提示重新获取验证码
|
||||
- **AND** MUST NOT 复用已消费的验证码
|
||||
|
||||
### Requirement: 代理扫码注册提交
|
||||
注册页 MUST 调用 `POST /api/c/v1/agent-distribution-registrations` 提交注册,必填 `distribution_code`(扫码自动带)、`phone`、`code`、`password`、`shop_name`、`shop_code`、`username`,选填 `contact_name`、`province`、`city`、`district`、`address`;成功响应为「待审批」,前端 MUST 展示待审批结束页且不自动登录。
|
||||
|
||||
#### Scenario: 注册成功进入待审批
|
||||
- **GIVEN** 用户填写完整表单并通过校验
|
||||
- **WHEN** 其提交注册
|
||||
- **THEN** 系统 MUST 调用注册接口
|
||||
- **AND** 成功时 MUST 展示「待审批,审核结果将通知你」结束页
|
||||
- **AND** MUST NOT 发放账号凭证或自动登录
|
||||
|
||||
#### Scenario: 注册失败统一提示
|
||||
- **GIVEN** 注册提交失败(无效分销码、上级停用、验证码无效或已消费等)
|
||||
- **WHEN** 注册接口返回失败
|
||||
- **THEN** 页面 MUST 统一提示「分销码不可用」
|
||||
- **AND** MUST NOT 根据错误文案区分原因
|
||||
|
||||
### Requirement: 表单适配与敏感项处理
|
||||
注册表单 MUST 同时适配手机 H5 与 PC:H5 单列、大触控区、软键盘友好、地址使用级联选择器;PC 使用居中卡片或两列布局且字段一致。密码输入 MUST 掩码并展示强度提示;页面 MUST NOT 展示除手机号外的完整个人敏感信息。
|
||||
|
||||
#### Scenario: H5 与 PC 同一表单
|
||||
- **GIVEN** 用户分别在手机与 PC 打开注册页
|
||||
- **WHEN** 其填写注册信息
|
||||
- **THEN** 两端的字段集合 MUST 一致
|
||||
- **AND** 布局 MUST 按端侧自适应
|
||||
|
||||
#### Scenario: 密码强度与掩码
|
||||
- **GIVEN** 用户在密码输入框输入
|
||||
- **WHEN** 其提交表单
|
||||
- **THEN** 输入 MUST 为掩码展示
|
||||
- **AND** MUST 展示密码强度提示
|
||||
@@ -1,61 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 提交/替换提现资料资格
|
||||
代理本人 MUST 能提交或替换提现资料资格,调用 `POST /api/admin/shops/{shop_id}/withdrawal-qualifications`,请求体包含 `subject_type`、`subject_code`、`legal_person_id_card` 与合同、身份证正反面、营业执照、门头照、发票等 `*_file_key`,以及 `invoice_title`、`invoice_subject_code`;附件 MUST 先通过对象存储上传接口取得 `file_key` 再提交。
|
||||
|
||||
#### Scenario: 提交资格成功
|
||||
- **GIVEN** 代理本人已登录并选好主体类型
|
||||
- **WHEN** 其上传资料并提交
|
||||
- **THEN** 前端 MUST 先取得全部附件 `file_key`
|
||||
- **AND** 再调用提交接口并在成功后刷新资格列表
|
||||
|
||||
#### Scenario: 替换资格使旧资格失效
|
||||
- **GIVEN** 已存在通过审批的资格
|
||||
- **WHEN** 代理提交合同或法人身份证变更
|
||||
- **THEN** 旧资格 MUST 立即失效
|
||||
- **AND** 新资格 MUST 重新审批通过后方可提现
|
||||
|
||||
### Requirement: 查询提现资料资格
|
||||
代理本人 MUST 能分页查询提现资料资格,调用 `GET /api/admin/shops/{shop_id}/withdrawal-qualifications`(`page` / `page_size` / `status`);响应 MUST 展示脱敏的 `subject_code_masked` 与 `legal_person_id_card_masked`,并展示 `status`、`approval_status`、`invalid_reason`、`invalidated_at` 与各附件 `file_key` 预览。
|
||||
|
||||
#### Scenario: 查看资格版本与审批状态
|
||||
- **GIVEN** 代理本人打开提现资料资格列表
|
||||
- **WHEN** 接口返回资格记录
|
||||
- **THEN** 列表 MUST 展示脱敏证件号与审批状态
|
||||
- **AND** 证件号 MUST NOT 以明文展示
|
||||
|
||||
### Requirement: 作废提现资料资格
|
||||
超级管理员 MUST 能作废提现资料资格,调用 `POST /api/admin/withdrawal-qualifications/{id}/void`,`reason` 必填。
|
||||
|
||||
#### Scenario: 作废资格须填原因
|
||||
- **GIVEN** 超级管理员打开作废弹窗
|
||||
- **WHEN** 其未填写原因直接提交
|
||||
- **THEN** 前端 MUST 阻止提交并提示填写原因
|
||||
|
||||
#### Scenario: 超管作废资格
|
||||
- **GIVEN** 超级管理员选择一条资格
|
||||
- **WHEN** 其填写原因并确认作废
|
||||
- **THEN** 系统 MUST 调用作废接口并在成功后刷新列表
|
||||
|
||||
### Requirement: 重提被驳回的提现
|
||||
代理本人 MUST 能重提被驳回的提现,调用 `PUT /api/admin/shops/{shop_id}/withdrawal-requests/{id}`,请求体包含 `account_name`、`account_number`、`amount`、`withdrawal_method` 与 `invoice_keys`;成功响应返回新提现单(`withdrawal_no`、`actual_amount`、`fee`、`fee_rate`、`status`)。
|
||||
|
||||
#### Scenario: 重提被驳回的提现
|
||||
- **GIVEN** 代理本人的提现申请被驳回
|
||||
- **WHEN** 其修改信息并重新提交
|
||||
- **THEN** 系统 MUST 调用重提接口
|
||||
- **AND** 成功后 MUST 刷新提现记录并展示新单号
|
||||
|
||||
#### Scenario: 非驳回状态不可重提
|
||||
- **GIVEN** 提现申请不是被驳回状态
|
||||
- **WHEN** 代理尝试重提
|
||||
- **THEN** 重提入口 MUST 不可用或不展示
|
||||
|
||||
### Requirement: 提现申请详情
|
||||
代理本人 MUST 能查看提现申请详情,调用 `GET /api/admin/shops/{shop_id}/withdrawal-requests/{id}`;响应包含 `reject_reason`、`anomaly_flag`、`anomaly_name`、`anomaly_reason` 与 `attempts` 尝试记录,前端 MUST 展示这些字段。
|
||||
|
||||
#### Scenario: 查看提现详情
|
||||
- **GIVEN** 代理本人点击提现记录
|
||||
- **WHEN** 详情返回
|
||||
- **THEN** 页面 MUST 展示提现金额、实际到账、手续费、状态
|
||||
- **AND** 展示驳回原因、异常标记与尝试记录
|
||||
@@ -1,15 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 店铺数据返回分销码
|
||||
店铺列表、详情与更新接口响应 MUST 包含 `distribution_code` 字段,前端类型 MUST 覆盖该字段并在列表/详情中展示。
|
||||
|
||||
#### Scenario: 店铺详情展示分销码
|
||||
- **GIVEN** 用户打开店铺详情页
|
||||
- **WHEN** 店铺存在 `distribution_code`
|
||||
- **THEN** 页面 MUST 展示分销码
|
||||
- **AND** MUST 以该码生成注册入口二维码
|
||||
|
||||
#### Scenario: 分销码为空
|
||||
- **GIVEN** 店铺尚未分配分销码
|
||||
- **WHEN** 页面展示店铺信息
|
||||
- **THEN** 分销码与二维码区域 MUST 展示空态(`-` 或「未分配」)
|
||||
@@ -1,15 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 企微审批场景业务类型扩展
|
||||
企微审批场景的 `business_type` MUST 支持以下枚举:`refund_approval`(退款审批)、`offline_recharge_approval`(员工线下代充值审批)、`employee_collection_approval`(员工代收款核销审批)、`agent_distribution_approval`(代理扫码分销注册审批)、`withdrawal_qualification_approval`(提现资料资格审批)、`commission_withdrawal_approval`(佣金提现终审)。前端类型与场景配置页选项 MUST 与后端枚举一致;`GET /api/admin/wecom/scenes/{business_type}/fields` 允许查询上述任一类型。
|
||||
|
||||
#### Scenario: 场景配置页可选新业务类型
|
||||
- **GIVEN** 超级管理员打开企微审批场景配置页
|
||||
- **WHEN** 其新建场景并选择业务类型
|
||||
- **THEN** 下拉选项 MUST 包含全部六个业务类型
|
||||
- **AND** 新增三个业务类型 MUST 与后端枚举一致
|
||||
|
||||
#### Scenario: 查询新增场景业务字段
|
||||
- **GIVEN** 已选择 `agent_distribution_approval`、`withdrawal_qualification_approval` 或 `commission_withdrawal_approval`
|
||||
- **WHEN** 页面加载业务字段
|
||||
- **THEN** 前端 MUST 调用对应 `business_type` 的字段查询接口
|
||||
@@ -1,29 +0,0 @@
|
||||
## 1. 提案与 API 层
|
||||
- [x] 1.1 新增 `src/api/modules/agentDistribution.ts`:`sendCode()` 与 `registerAgent()`,公开请求不带 Token
|
||||
- [x] 1.2 扩展 `src/types/api/shop.ts`:`ShopResponse` 增加 `distribution_code`
|
||||
- [x] 1.3 扩展 `src/types/api/commission.ts`:提现资料资格提交/查询、作废、重提、详情的请求与响应类型
|
||||
- [x] 1.4 扩展 `src/api/modules/commission.ts`:`submitWithdrawalQualification` / `getWithdrawalQualifications` / `voidWithdrawalQualification` / `resubmitWithdrawalRequest` / `getWithdrawalRequestDetail`
|
||||
- [x] 1.5 扩展 `src/types/api/wecom.ts`:`WecomBusinessType` 增加三个业务类型
|
||||
- [x] 1.6 汇总导出新增类型
|
||||
|
||||
## 2. 企微审批场景
|
||||
- [x] 2.1 场景配置页新增三个业务类型选项(`agent_distribution_approval` / `withdrawal_qualification_approval` / `commission_withdrawal_approval`)
|
||||
|
||||
## 3. 独立注册页
|
||||
- [x] 3.1 `staticRoutes.ts` 新增 `/agent-registration` 静态路由
|
||||
- [x] 3.2 `LOGIN_WHITE_LIST` 增加注册页路径
|
||||
- [x] 3.3 新增 `src/views/agent-registration/index.vue`:表单、验证码倒计时、密码强度、省市区级联、提交与结束页、H5/PC 响应式
|
||||
|
||||
## 4. 店铺详情
|
||||
- [x] 4.1 店铺详情页展示 `distribution_code` 与注册二维码
|
||||
|
||||
## 5. 我的佣金(代理侧)
|
||||
- [x] 5.1 新增「提现资料资格」Tab:资格列表(脱敏展示)+ 提交/替换弹窗(附件走上传接口)
|
||||
- [x] 5.2 提现记录新增「详情」抽屉:展示明细、驳回原因、异常标记与尝试记录
|
||||
- [x] 5.3 被驳回记录新增「重新提交」弹窗(PUT 重提)
|
||||
- [x] 5.4 超管可见「作废资格」操作(reason 必填)
|
||||
|
||||
## 6. 验证
|
||||
- [x] 6.1 运行 `npm run build`(含 `vue-tsc --noEmit`)
|
||||
- [x] 6.2 运行 `npm run check:encoding`
|
||||
- [x] 6.3 运行 `openspec.cmd validate add-agent-distribution-registration-and-withdrawal --strict`
|
||||
@@ -1,39 +0,0 @@
|
||||
# Change: 新增资产钱包自动续费配置(后台查询/保存 + 自动续费失败通知适配)
|
||||
|
||||
## Why
|
||||
|
||||
根据 `docs/产品迭代8月份/资产钱包.md`。后端测试环境(`https://cmp-api.boss160.cn`,`Authorization: Bearer <token>`)已提供资产钱包自动续费全局配置能力,仅有 2 个后台接口,H5 侧零新增:
|
||||
|
||||
- 查询资产钱包自动续费配置:`GET /api/admin/asset-auto-renewal-config`
|
||||
- 保存资产钱包自动续费配置:`PUT /api/admin/asset-auto-renewal-config`
|
||||
|
||||
后台需要提供配置页面承载总开关、适用范围、指定主套餐、到期前天数等配置;同时新增通知类型 `asset.auto_renewal.failed` 会复用既有通知接口产生新数据,需要适配展示与跳转。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增能力 `asset-wallet-auto-renewal`:
|
||||
- 配置读取:`GET /api/admin/asset-auto-renewal-config`,返回唯一一份全局配置的 `enabled`+`enabled_name`、`scope`+`scope_name`、`package_ids`、`days_before_expiry`、`config_version`、`updater`、`updated_at`。
|
||||
- 配置保存:`PUT /api/admin/asset-auto-renewal-config`:
|
||||
- 请求体 `{ enabled(0/1,必填), scope(all|specified,必填), package_ids(uint[],仅 specified 时非空且只能选当前可售主套餐), days_before_expiry(1–90,必填) }`。
|
||||
- 语义:单行配置、无新增/删除;保存即递增 `config_version`,记录操作者与前后值快照;配置变更只影响后续扫描,历史记录不重算。
|
||||
- 权限:仅超级管理员与平台账号可访问;其他身份(代理/企业/个人客户)`403`,提示「无权限操作该资源或资源不存在」,无权限与不存在不区分。
|
||||
- 通知适配(复用既有接口,不算新接口):
|
||||
- 后台站内通知:新增通知类型 `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`),接收人是业务员/店铺账号。走既有 `GET /api/admin/notifications` + `GET /api/admin/notifications/{id}/target`;确认 `target_type` 的 `iot_card` / `device` 在导航白名单;`available=false` 时只显示正文不跳转;计入未读数。
|
||||
- H5 个人客户通知:同一类型已由后端加入客户可见白名单,出现在既有 `GET /api/c/v1/notifications` 与 `unread-count`,客户侧只有文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面,本仓库无需改动。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
|
||||
- 明确不做(避免前端空等):无自动续费记录页/异常记录页接口、无尝试记录查询接口、无「有余额就不停机」开关、无 H5 客户侧新接口。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `asset-wallet-auto-renewal` — 新增能力
|
||||
- Affected code:
|
||||
- `src/types/api/assetWallet.ts`(新增)
|
||||
- `src/types/api/index.ts`
|
||||
- `src/api/modules/assetWallet.ts`(新增)
|
||||
- `src/api/modules/index.ts`
|
||||
- `src/views/settings/asset-wallet-auto-renewal/index.vue`(新增)
|
||||
- `src/router/routesAlias.ts`
|
||||
- `src/router/routes/asyncRoutes.ts`
|
||||
- `src/utils/business/notificationNavigation.ts`(确认/补充 `iot_card`、`device` 白名单)
|
||||
- `src/components/core/layouts/art-notification/index.vue`(确认 `expiry` 分类覆盖新类型)
|
||||
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||
@@ -1,94 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 自动续费配置查询
|
||||
|
||||
The admin frontend SHALL load the single global asset wallet auto-renewal configuration through `GET /api/admin/asset-auto-renewal-config`. 响应字段 SHALL 包含并展示 `enabled`+`enabled_name`、`scope`+`scope_name`、`package_ids`、`days_before_expiry`、`config_version`、`updater`、`updated_at`。
|
||||
|
||||
#### Scenario: 读取全局自动续费配置
|
||||
|
||||
- **WHEN** 用户进入资产钱包自动续费配置页面
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/asset-auto-renewal-config`
|
||||
- **AND** 页面 MUST 展示总开关及中文名称、适用范围及中文名称、指定主套餐集合、统一到期前天数、配置版本、最近保存操作者与最近保存时间
|
||||
|
||||
### Requirement: 自动续费配置保存
|
||||
|
||||
The admin frontend SHALL save the configuration through `PUT /api/admin/asset-auto-renewal-config` with request body `{ enabled, scope, package_ids, days_before_expiry }`。`enabled` SHALL 取值 0/1 且必填;`scope` SHALL 取值 `all`(全部主套餐)或 `specified`(指定主套餐)且必填;`package_ids` 仅在 `scope=specified` 时必填非空且只能选择当前可售主套餐;`days_before_expiry` SHALL 取值 1 至 90 且必填。配置为单行、无新增/删除;保存 SHALL 递增 `config_version` 并记录操作者与前后值快照;配置变更 SHALL 只影响后续扫描,历史记录不重算。
|
||||
|
||||
#### Scenario: 保存全部主套餐范围配置
|
||||
|
||||
- **WHEN** 用户设置适用范围为「全部主套餐」并提交
|
||||
- **THEN** 前端 MUST 调用 `PUT /api/admin/asset-auto-renewal-config`
|
||||
- **AND** 请求体携带 `enabled`、`scope=all`、`days_before_expiry`
|
||||
- **AND** `package_ids` 以空数组提交
|
||||
- **AND** 保存成功后页面 MUST 以响应中的 `config_version`、`updater`、`updated_at` 刷新展示
|
||||
|
||||
#### Scenario: 保存指定主套餐范围配置
|
||||
|
||||
- **WHEN** 用户设置适用范围为「指定主套餐」并选择若干当前可售主套餐后提交
|
||||
- **THEN** `package_ids` MUST 非空且只包含当前可售主套餐
|
||||
- **AND** 前端 MUST 携带 `scope=specified` 与非空 `package_ids` 提交
|
||||
|
||||
#### Scenario: 参数校验
|
||||
|
||||
- **WHEN** `enabled` 缺失、`scope` 非法、`scope=specified` 时 `package_ids` 为空或包含不可售套餐,或 `days_before_expiry` 超出 1–90
|
||||
- **THEN** 前端 MUST 阻止提交并给出校验提示
|
||||
|
||||
#### Scenario: 仅影响后续扫描
|
||||
|
||||
- **WHEN** 用户修改并保存配置
|
||||
- **THEN** 历史自动续费记录 MUST NOT 被重算
|
||||
- **AND** 新配置 MUST 只对保存后的扫描生效
|
||||
|
||||
### Requirement: 自动续费配置访问权限
|
||||
|
||||
Only super admin and platform accounts SHALL be allowed to query or save the asset wallet auto-renewal configuration. 代理、企业与个人客户等身份访问时 SHALL 返回 `403` 并提示「无权限操作该资源或资源不存在」;无权限与不存在的表现 MUST NOT 可区分。
|
||||
|
||||
#### Scenario: 授权访问
|
||||
|
||||
- **WHEN** 超级管理员或平台账号访问配置接口
|
||||
- **THEN** 页面 MUST 正常查询与保存配置
|
||||
|
||||
#### Scenario: 无权限访问
|
||||
|
||||
- **WHEN** 代理、企业或个人客户账号访问配置接口
|
||||
- **THEN** 页面 MUST 按 `403` 无权限处理并展示统一提示
|
||||
- **AND** 前端 MUST NOT 通过响应区分无权限与资源不存在
|
||||
|
||||
### Requirement: 自动续费失败后台通知
|
||||
|
||||
The admin notification center SHALL display the new notification type `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`)through the existing notification APIs,接收人为业务员/店铺账号。目标 `target_type` 的 `iot_card` 与 `device` SHALL 位于通知导航白名单;当目标 `available=false` 时页面 SHALL 只展示正文且不跳转;该类型通知 SHALL 计入未读数。
|
||||
|
||||
#### Scenario: 展示并跳转自动续费失败通知
|
||||
|
||||
- **WHEN** 通知列表返回 `asset.auto_renewal.failed` 类型且目标可用
|
||||
- **THEN** 通知中心 MUST 将该通知归入 `expiry` 类别展示
|
||||
- **AND** 用户点击后 MUST 通过既有目标接口按 `iot_card` / `device` 跳转到对应资产详情
|
||||
|
||||
#### Scenario: 目标不可用
|
||||
|
||||
- **WHEN** 目标接口返回 `available=false`
|
||||
- **THEN** 页面 MUST 只展示通知正文且不发起跳转
|
||||
|
||||
### Requirement: H5 客户侧通知边界
|
||||
|
||||
This capability SHALL NOT 新增或修改 H5 客户侧接口与页面。`asset.auto_renewal.failed` 类型已由后端加入客户可见白名单,通过既有 `GET /api/c/v1/notifications` 与 `unread-count` 返回;客户侧仅展示文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
|
||||
|
||||
#### Scenario: H5 复用既有接口
|
||||
|
||||
- **WHEN** 客户侧通知接口返回该类型
|
||||
- **THEN** 客户侧 MUST 仅展示文案且不提供跳转
|
||||
- **AND** 本次变更 MUST NOT 新增 H5 客户侧接口或页面
|
||||
|
||||
#### Scenario: 到期展示期
|
||||
|
||||
- **WHEN** 对应套餐业务到期
|
||||
- **THEN** 该通知按 `expiry` 类别既有行为从客户侧列表消失
|
||||
|
||||
### Requirement: 功能范围边界
|
||||
|
||||
This capability SHALL NOT 包含以下内容:自动续费记录页/异常记录页、尝试记录查询、「有余额就不停机」开关、H5 客户侧新接口。
|
||||
|
||||
#### Scenario: 明确不提供的功能
|
||||
|
||||
- **WHEN** 前端对接本次资产钱包自动续费能力
|
||||
- **THEN** 页面 MUST NOT 提供或依赖自动续费记录页、异常记录页、尝试记录查询接口与「有余额就不停机」开关
|
||||
@@ -1,27 +0,0 @@
|
||||
# Tasks: 资产钱包自动续费配置
|
||||
|
||||
## 1. 类型与 API
|
||||
- [ ] 1.1 新增 `src/types/api/assetWallet.ts`:`AssetAutoRenewalConfig`(`enabled`/`enabled_name`/`scope`/`scope_name`/`package_ids`/`days_before_expiry`/`config_version`/`updater`/`updated_at`)、`UpdateAssetAutoRenewalConfigRequest`(`enabled`/`scope`/`package_ids`/`days_before_expiry`)等类型
|
||||
- [ ] 1.2 `src/types/api/index.ts` 导出新类型
|
||||
- [ ] 1.3 新增 `src/api/modules/assetWallet.ts`:
|
||||
- `getAutoRenewalConfig`:`GET /api/admin/asset-auto-renewal-config`
|
||||
- `saveAutoRenewalConfig`:`PUT /api/admin/asset-auto-renewal-config`
|
||||
- [ ] 1.4 `src/api/modules/index.ts` 导出新服务
|
||||
|
||||
## 2. 配置页面
|
||||
- [ ] 2.1 `src/router/routesAlias.ts` 新增资产钱包自动续费配置路由别名,`src/router/routes/asyncRoutes.ts` 在设置下注册路由与菜单(仅超级管理员与平台账号可见)
|
||||
- [ ] 2.2 新增 `src/views/settings/asset-wallet-auto-renewal/index.vue`:读取配置并展示总开关、适用范围、指定主套餐、统一到期前天数、配置版本、最近保存操作者与时间
|
||||
- [ ] 2.3 实现保存表单:总开关(0/1)、适用范围(all/specified)、到期前天数(1–90 校验);指定范围时主套餐多选仅支持当前可售主套餐且非空
|
||||
- [ ] 2.4 保存成功后使用响应刷新 `config_version`、`updater`、`updated_at`,不做新增/删除记录操作
|
||||
|
||||
## 3. 通知适配
|
||||
- [ ] 3.1 确认 `src/utils/business/notificationNavigation.ts` 白名单包含 `iot_card` 与 `device`,目标可用时跳转资产详情
|
||||
- [ ] 3.2 确认 `src/components/core/layouts/art-notification/index.vue` 的 `expiry` 分类可展示 `asset.auto_renewal.failed`,`available=false` 时只显示正文不跳转
|
||||
- [ ] 3.3 确认该类型通知计入未读数与分类汇总
|
||||
|
||||
## 4. Verification
|
||||
- [ ] 4.1 验证 GET/PUT 字段传递与参数校验(enabled、scope、package_ids、days_before_expiry)
|
||||
- [ ] 4.2 验证指定范围时 package_ids 非空且仅可售主套餐
|
||||
- [ ] 4.3 验证通知类型展示、目标跳转与不可用目标处理
|
||||
- [ ] 4.4 验证非超级管理员/平台账号访问的 403 处理
|
||||
- [ ] 4.5 运行 lint、类型检查与构建
|
||||
@@ -1,54 +0,0 @@
|
||||
# Change: 业务用户组管理与店铺批量交接/导入(AUG26-003)
|
||||
|
||||
## Why
|
||||
|
||||
后台需要把平台用户账号按业务线归入"业务用户组",并基于组对店铺做筛选、批量交接负责人与 CSV 导入负责人。当前前端只有单个店铺设置平台业务员的能力,缺少业务用户组维护、成员归属、按组筛选、批量交接与导入入口;且现有 11 个新端点仅超管与平台账号可访问。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增业务用户组管理模块(API 层 + 维护页面 + 成员管理):
|
||||
- GET/POST /api/admin/business-user-groups、GET/PUT/DELETE /api/admin/business-user-groups/{id}
|
||||
- PUT /api/admin/business-user-groups/{id}/members、DELETE /api/admin/business-user-groups/members
|
||||
- 编码创建后不可改;删除需 {"confirm": true} 且仅无成员组可删;更新支持部分字段,business_line 三态(缺省=不改 / ""=清空 / 枚举=设置)。
|
||||
- 成员归属整批校验(全部启用平台用户 + 目标组启用),任一无效整批不生效;一账号至多一组。
|
||||
- 店铺列表/详情读取侧扩展:
|
||||
- GET /api/admin/shops 新增 business_user_group_id / business_line / ungrouped 筛选。
|
||||
- 列表/详情响应新增 5 个只读推导字段(组 ID/编码/名称/启用/业务线);组停用仍返回组且 enabled=false(不算未分组);前端不缓存、不回写,改组后重新拉列表。
|
||||
- 勾选批量交接:
|
||||
- PUT /api/admin/shops/business-owner/batch:shop_ids(1-500,去重)+ business_owner_account_id;字段缺失=400,null=清空,ID=换绑;任一店铺无效或目标非启用业务员 -> 整批不写入,统一按 1005 提示。
|
||||
- CSV 导入:
|
||||
- 复用 StorageService.getUploadUrl + 预签名 PUT 直传(FilePurpose 增加 shop_import)。
|
||||
- POST /api/admin/shops/business-owner-imports 创建异步任务,轮询详情、展示行级结果;前端自带模板(表头 店铺编码,操作类型,业务员登录账号,备注,换绑/清空)。
|
||||
- 入口与权限(代理/企业不渲染入口):
|
||||
- 路由 meta roles ['R_SUPER', 'R_ADMIN'];按钮权限码:
|
||||
- business_user_group:page / business_user_group:create / business_user_group:update / business_user_group:delete / business_user_group:members
|
||||
- shop:business_owner_batch
|
||||
- shop:business_owner_import
|
||||
|
||||
## Not In Scope
|
||||
|
||||
- 不实现后端接口、不改数据库。
|
||||
- 业务用户组不承载角色权限/数据范围语义;前端不引入组层级与组管理员概念。
|
||||
- 导入结果不支持导出,仅在页面展示行级明细。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: business-user-group-management(新增)、shop-management(扩展)
|
||||
- Affected code:
|
||||
- src/types/api/businessUserGroup.ts(新增)
|
||||
- src/api/modules/businessUserGroup.ts(新增)
|
||||
- src/types/api/shop.ts、src/api/modules/shop.ts
|
||||
- src/api/modules/storage.ts(FilePurpose 增加 shop_import)
|
||||
- src/views/shop-management/business-user-groups/index.vue(新增页面)
|
||||
- src/views/shop-management/list/index.vue
|
||||
- src/router/routes/asyncRoutes.ts、src/router/routesAlias.ts、src/locales/langs/{zh,en}.json
|
||||
- src/template/业务负责人导入模板.csv(前端自带模板)
|
||||
- API contracts: 11 个端点(7 个业务用户组 + 店铺筛选/字段 + 批量交接 + 导入三步)
|
||||
- Dependencies:
|
||||
- docs/产品迭代8月份/业务用户组.md(7 个组端点 OpenAPI)
|
||||
- AUG26-003 前端对接说明(店铺侧端点)
|
||||
|
||||
## 待确认
|
||||
|
||||
- 成员选择器复用 GET /api/admin/shops/business-owner-candidates(启用平台账号);若组成员可包含非业务员平台账号,需后端补充平台账号列表端点。
|
||||
- 导入任务与行级结果字段名(file_key、status、items[] 等)以生成文档为准,提案按对接说明先行对齐。
|
||||
@@ -1,73 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 业务用户组维护
|
||||
|
||||
后台 MUST 提供业务用户组列表、详情、创建、更新与删除能力(GET/POST /api/admin/business-user-groups、GET/PUT/DELETE /api/admin/business-user-groups/{id})。列表 MUST 支持 page/page_size/enabled/keyword/business_line 筛选,且每项 MUST 返回 business_line_name。创建时 code(1-64)与 name 必填,编码未删除组内唯一且创建后不可修改;business_line 可空(standard/smart/other),sort/enabled/remark 可选。更新 MUST 仅允许名称、业务线、排序、启停与备注,且 business_line 支持三态:字段缺失不修改、传 "" 清空、传枚举设置;删除 MUST 携带 {"confirm": true},仅无成员组可删除。
|
||||
|
||||
#### Scenario: 查询业务用户组列表
|
||||
|
||||
- **GIVEN** 用户进入业务用户组维护页
|
||||
- **WHEN** 前端以 page/page_size/enabled/keyword/business_line 发起列表查询
|
||||
- **THEN** 前端 MUST 展示分页结果且每项显示 business_line_name
|
||||
- **AND** 下拉数据源复用该列表接口
|
||||
|
||||
#### Scenario: 创建时编码不可变
|
||||
|
||||
- **GIVEN** 用户填写名称与稳定编码创建用户组
|
||||
- **WHEN** 提交成功或编码重复被后端拒绝
|
||||
- **THEN** 编码在创建后 MUST NOT 出现在编辑表单中
|
||||
- **AND** 编码重复时 MUST 展示后端返回的"业务用户组编码已存在"
|
||||
|
||||
#### Scenario: 业务线三态更新
|
||||
|
||||
- **GIVEN** 用户编辑某组的业务线
|
||||
- **WHEN** 表单传 ""、传枚举或省略该字段
|
||||
- **THEN** 前端 MUST 分别按"清空 / 设置 / 不修改"构造请求体
|
||||
|
||||
#### Scenario: 删除需二次确认
|
||||
|
||||
- **GIVEN** 用户点击删除某组
|
||||
- **WHEN** 确认弹窗提交
|
||||
- **THEN** 前端 MUST 携带 {"confirm": true}
|
||||
- **AND** 有成员时 MUST 提示"用户组仍有成员,只能停用或先移走成员"且不做物理删除
|
||||
- **AND** 无成员时删除成功并刷新列表
|
||||
|
||||
### Requirement: 成员归属批量维护
|
||||
|
||||
后台 MUST 支持按平台用户账号把成员批量设置进组(PUT /api/admin/business-user-groups/{id}/members)与批量清空(DELETE /api/admin/business-user-groups/members,携带 body account_ids)。两种操作 MUST 复用同一校验:所有账号必须是启用平台用户且目标组启用,任一账号无效则整批不生效;清空后账号回到未分组;一账号至多一组。
|
||||
|
||||
#### Scenario: 批量设置成员
|
||||
|
||||
- **GIVEN** 用户在成员管理中选择多个启用平台账号
|
||||
- **WHEN** 保存成员归属
|
||||
- **THEN** 前端 MUST 以 account_ids 数组整体替换每个账号原归属
|
||||
- **AND** 刷新后成员关系与最新分组一致
|
||||
|
||||
#### Scenario: 批量清空成员
|
||||
|
||||
- **GIVEN** 用户选择若干账号执行清空
|
||||
- **WHEN** 提交清空
|
||||
- **THEN** 前端 MUST 调用 DELETE /api/admin/business-user-groups/members 并携带 account_ids body
|
||||
- **AND** 清空成功的账号回到未分组
|
||||
|
||||
#### Scenario: 停用组的成员展示
|
||||
|
||||
- **GIVEN** 某组被停用但仍有成员
|
||||
- **WHEN** 用户在成员管理或店铺列表看到该组
|
||||
- **THEN** 前端 MUST 展示"已停用"标记并提供改组/清空入口
|
||||
|
||||
### Requirement: 访问控制与入口
|
||||
|
||||
业务用户组维护入口 MUST 仅对超级管理员与平台账号可见,代理/企业账号 MUST NOT 渲染入口;所有按钮 MUST 受权限码控制。
|
||||
|
||||
#### Scenario: 菜单按角色隐藏
|
||||
|
||||
- **GIVEN** 代理或企业账号登录
|
||||
- **WHEN** 系统渲染菜单
|
||||
- **THEN** MUST NOT 展示业务用户组菜单项
|
||||
|
||||
#### Scenario: 按钮权限控制
|
||||
|
||||
- **GIVEN** 平台账号具备部分业务用户组权限
|
||||
- **WHEN** 渲染操作按钮
|
||||
- **THEN** 前端 MUST 按 business_user_group:create/update/delete/members 控制显隐
|
||||
@@ -1,75 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 店铺列表/详情业务用户组字段与筛选
|
||||
|
||||
店铺列表查询 MUST 支持 business_user_group_id、business_line、ungrouped 三个新筛选;列表与详情响应 MUST 提供 5 个只读推导字段:business_user_group_id(可空)、business_user_group_code、business_user_group_name、business_user_group_enabled、business_user_group_business_line。组停用仍返回组且 enabled=false(不算未分组);店铺不存组,字段实时推导,前端 MUST NOT 缓存或回写,改动组成员后重新拉取列表。
|
||||
|
||||
#### Scenario: 按业务用户组筛选
|
||||
|
||||
- **GIVEN** 用户在店铺列表选择业务用户组、业务线或勾选"未分组"
|
||||
- **WHEN** 发起列表查询
|
||||
- **THEN** 前端 MUST 提交 business_user_group_id/business_line/ungrouped 参数
|
||||
- **AND** ungrouped=true 表示无负责人或负责人无分组
|
||||
|
||||
#### Scenario: 展示组字段
|
||||
|
||||
- **GIVEN** 店铺属于某启用或停用的业务用户组
|
||||
- **WHEN** 渲染列表或详情
|
||||
- **THEN** 前端 MUST 展示组名称/编码/业务线
|
||||
- **AND** 组停用时 MUST 标记"已停用"且仍展示组归属
|
||||
|
||||
### Requirement: 勾选批量交接平台业务员
|
||||
|
||||
店铺列表 MUST 支持勾选店铺批量交接平台业务员(PUT /api/admin/shops/business-owner/batch,body shop_ids 1-500 去重 + business_owner_account_id)。字段缺失 MUST 按参数错误处理(400);传 null 表示清空负责人;传 ID 表示换绑。任一店铺不存在/已删除/越权或目标非启用业务员时整批不写入,前端 MUST 统一提示"无权限操作该资源或资源不存在";成功响应含 batch_key/shop_count/cleared。
|
||||
|
||||
#### Scenario: 批量换绑
|
||||
|
||||
- **GIVEN** 用户勾选 1-500 家店铺并选择启用业务员
|
||||
- **WHEN** 提交批量交接
|
||||
- **THEN** 前端 MUST 提交 shop_ids(去重)与 business_owner_account_id
|
||||
- **AND** 成功后展示 shop_count 并刷新列表
|
||||
|
||||
#### Scenario: 批量清空负责人
|
||||
|
||||
- **GIVEN** 用户勾选店铺并选择"清空负责人"
|
||||
- **WHEN** 提交批量交接
|
||||
- **THEN** 前端 MUST 显式提交 business_owner_account_id: null
|
||||
- **AND** MUST NOT 省略该字段
|
||||
|
||||
#### Scenario: 整批失败统一提示
|
||||
|
||||
- **GIVEN** 批量交接请求因任一店铺或目标账号不满足条件而被后端拒绝
|
||||
- **WHEN** 前端收到 1005
|
||||
- **THEN** 前端 MUST 展示统一文案且不写入任何店铺
|
||||
|
||||
### Requirement: 业务负责人 CSV 导入
|
||||
|
||||
店铺列表 MUST 支持通过 CSV 导入批量交接/清空业务负责人。流程 MUST 为:前端自带模板(表头 店铺编码,操作类型,业务员登录账号,备注,操作类型仅"换绑"或"清空")-> POST /api/admin/storage/upload-url(purpose=shop_import)拿预签名 URL 直传(15 分钟有效)-> POST /api/admin/shops/business-owner-imports(file_key)创建异步任务 -> 轮询 GET /api/admin/shops/business-owner-imports/{id} 展示结果。行级失败不等于任务失败:任务 status=3 也可能有失败行,MUST 按 items[] 展示明细;表头/编码不符为任务级失败,items 为空数组,只展示 error_message。
|
||||
|
||||
#### Scenario: 下载模板并上传
|
||||
|
||||
- **GIVEN** 用户进入导入界面
|
||||
- **WHEN** 点击下载模板或选择文件
|
||||
- **THEN** 前端 MUST 提供表头完全一致的模板文件
|
||||
- **AND** 上传后 MUST 以 purpose=shop_import 获取上传地址并直传
|
||||
|
||||
#### Scenario: 创建任务并轮询
|
||||
|
||||
- **GIVEN** 文件上传成功拿到 file_key
|
||||
- **WHEN** 提交创建导入任务
|
||||
- **THEN** 前端 MUST 携带 file_key 调用创建接口
|
||||
- **AND** MUST 轮询任务详情直到 status 为已完成(3)或失败(4)
|
||||
|
||||
#### Scenario: 展示行级失败明细
|
||||
|
||||
- **GIVEN** 任务完成但存在失败行
|
||||
- **WHEN** 前端渲染结果
|
||||
- **THEN** 前端 MUST 展示 total_count/success_count/fail_count
|
||||
- **AND** MUST 按 items[] 展示各行的 line/shop_code/operation_type/status/reason
|
||||
- **AND** 失败行保留原负责人
|
||||
|
||||
#### Scenario: 任务级失败提示
|
||||
|
||||
- **GIVEN** 表头或编码不符导致任务级失败
|
||||
- **WHEN** 前端渲染结果
|
||||
- **THEN** 前端 MUST 只展示 error_message 且明细为空
|
||||
@@ -1,34 +0,0 @@
|
||||
## 1. API Contract 层
|
||||
|
||||
- [x] 1.1 新增 src/types/api/businessUserGroup.ts:组项/分页/创建/更新/删除/成员设置与结果类型
|
||||
- [x] 1.2 新增 src/api/modules/businessUserGroup.ts:封装 7 个组端点(DELETE 带 body,需请求层支持)
|
||||
- [x] 1.3 扩展 src/types/api/shop.ts:列表筛选 business_user_group_id/business_line/ungrouped,响应只读组字段,批量交接与导入任务/行级结果类型
|
||||
- [x] 1.4 扩展 src/api/modules/shop.ts:batchUpdateBusinessOwner、createBusinessOwnerImport、getBusinessOwnerImportDetail、getBusinessOwnerImportList
|
||||
- [x] 1.5 扩展 src/api/modules/storage.ts:FilePurpose 增加 shop_import
|
||||
|
||||
## 2. 业务用户组页面
|
||||
|
||||
- [x] 2.1 新增 src/views/shop-management/business-user-groups/index.vue:列表(筛选/分页/启停/业务线)
|
||||
- [x] 2.2 新建/编辑弹窗:code 创建后不可改、business_line 三态、名称/排序/备注/启停
|
||||
- [x] 2.3 删除:二次确认(confirm=true),有成员时按后端提示处理
|
||||
- [x] 2.4 成员管理(对话框/抽屉):成员选择器(复用 business-owner-candidates)+ 批量设置 + 批量清空;停用组保留成员并标记"已停用"
|
||||
|
||||
## 3. 店铺列表扩展
|
||||
|
||||
- [x] 3.1 列表搜索区新增业务用户组下拉与业务线筛选、ungrouped 勾选(数据源来自组列表接口)
|
||||
- [x] 3.2 列表/详情展示组字段(停用组标记,不缓存不回写)
|
||||
- [x] 3.3 批量交接弹窗:勾选店铺 + 选择业务员/清空(显式 null),整批失败统一 1005 文案
|
||||
- [x] 3.4 导入入口:模板下载、文件校验/上传(purpose=shop_import)、创建任务、轮询并展示行级明细
|
||||
- [x] 3.5 导入任务列表(状态筛选)
|
||||
|
||||
## 4. 路由/菜单/权限/i18n
|
||||
|
||||
- [x] 4.1 新增 /shop-management/business-user-groups 路由与菜单,roles ['R_SUPER', 'R_ADMIN']
|
||||
- [x] 4.2 按钮权限编码落地(business_user_group*/shop:business_owner_batch/shop:business_owner_import)
|
||||
- [x] 4.3 中英文案 menus.shopManagement.businessUserGroups 等
|
||||
|
||||
## 5. 验证
|
||||
|
||||
- [x] 5.1 运行 npm run build(含 vue-tsc --noEmit)
|
||||
- [x] 5.2 运行 npm run check:encoding
|
||||
- [x] 5.3 运行 openspec validate add-business-user-group-management --strict
|
||||
@@ -1,60 +0,0 @@
|
||||
# Change: 退款佣金回扣改为独立回溯明细(前端适配)
|
||||
|
||||
## Why
|
||||
|
||||
后端调整了退款佣金回扣口径:退款不再把原佣金整单置为「已失效」,而是原佣金行保持「已发放」不变,另新增一条独立负数、不可提现的「回溯明细」行。对应前端接口说明(简短版)要求:
|
||||
|
||||
- 佣金记录列表新增 `status` 查询参数(含 5 回溯)与行级 `source` 字段(`original` / `clawback`)。
|
||||
- 新增佣金记录详情接口 `GET /api/admin/shops/{shop_id}/commission-records/{id}?source=clawback`。
|
||||
- 新增导出场景 `scene=commission_record`(后端已进白名单)。
|
||||
- 两个必改点:列表行 key 必须用 `source + ':' + id`(原佣金与回溯 ID 空间独立,同一页会重复);渲染需支持负数金额(标红)与「不可提现」标识。
|
||||
|
||||
前端当前实现基于旧语义(`ShopCommissionRecordItem` 无 `source`,列表行 key 直接用 `id`,退款后原行被视为已失效),需按新口径适配。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 类型层:`ShopCommissionRecordItem` 新增 `source`、`original_commission_id`、`refund_id`、`refund_no`、`withdrawable`、`clawback_records`、`clawback_total_amount`;新增回溯摘要与详情类型;`CommissionRecordQueryParams.status` 支持 5(回溯)。
|
||||
- API 层:新增 `getShopCommissionRecordDetail(shopId, id, source?)`,`source` 省略时按原佣金处理。
|
||||
- 列表页(代理侧「我的佣金」与管理侧「代理资金概览 > 佣金明细」):行 key 改为 `source + ':' + id`;回溯行展示「回溯」状态与「不可提现」标识;负数金额与可为负的 `balance_after` 标红;`status` 筛选项新增「回溯」;详情入口必须携带该行 `source`。
|
||||
- 详情展示:原佣金详情展示 `clawback_records` 摘要与 `clawback_total_amount`;回溯详情展示来源 `original_commission`;越权/不存在统一提示「佣金明细不存在」,不做存在性判断。
|
||||
- 导出:`ExportTaskScene` 增加 `commission_record`;新增导出佣金记录场景页、路由、菜单与 i18n;佣金记录列表导出入口只提交受支持的筛选 key(`shop_id` / `status` / `commission_source` / `order_no`)。
|
||||
- 字段兜底:后端新增字段为 `omitempty`,前端取 `?? []` / `?? 0`,金额单位为分。
|
||||
|
||||
## Not In Scope
|
||||
|
||||
- 不实现后端接口、不改数据库、不改审批实例数据。
|
||||
- 既有字段无改名/改类型;`commission-stats`、`commission-daily-stats` 口径未变,本次不动。
|
||||
- 时间筛选属另一 Change(`add-export-time-filter-standards`),本次不涉及。
|
||||
- 语义提醒:退款不再写 `status=4`,旧假设「退款后原行变已失效」已失效,需在文案/注释体现。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `commission-management`、`export-task-management`
|
||||
- Affected code:
|
||||
- `src/types/api/commission.ts`
|
||||
- `src/api/modules/commission.ts`
|
||||
- `src/views/commission-management/my-commission/index.vue`
|
||||
- `src/views/commission-management/agent-fund-overview/index.vue`
|
||||
- `src/types/api/exportTask.ts`
|
||||
- `src/config/constants/exportTask.ts`
|
||||
- `src/views/asset-management/export-task-management/export-commission-record/index.vue`
|
||||
- `src/router/routes/asyncRoutes.ts`、`src/router/routesAlias.ts`、`src/locales/langs/{zh,en}.json`
|
||||
- Dependencies:
|
||||
- `docs/产品迭代8月份/frontend-api-simple.md`(后端契约)
|
||||
- 后端已把 `scene=commission_record` 加入导出场景白名单
|
||||
- 需确认导出场景权限编码与场景页是否必需(见 Open Questions)
|
||||
- Breaking changes:
|
||||
- 列表行 key 由 `id` 改为 `source + ':' + id`;未携带 `source` 的详情请求会命中原佣金行。
|
||||
|
||||
## 接口确认结果(来自 OpenAPI)
|
||||
|
||||
- 详情响应 `DtoShopCommissionRecordDetailResp`:`source`、`record`(`DtoShopCommissionRecordItem`)、`clawback_records`(仅原佣金返回)、`original_commission`(仅回溯详情返回)。
|
||||
- `DtoShopCommissionRecordItem` 新增 `source`、`released_at`、`withdrawable`(boolean,nullable,仅回溯行返回且恒 false)、`original_commission_id`、`refund_id`、`refund_no`、`clawback_records`、`clawback_total_amount`。
|
||||
- `DtoShopCommissionClawbackItem`:`id`、`amount`(恒负)、`balance_after`(可为负)、`original_commission_id`、`refund_id`、`refund_no`、`status`(5 回溯)、`status_name`、`withdrawable`、`created_at`。
|
||||
- 详情接口越权与不存在返回同一结果,前端统一提示「佣金明细不存在」。
|
||||
- 已确认需要新增「导出佣金记录」菜单页。
|
||||
|
||||
## 待确认(已按仓库既有约定实现,如需调整请告知)
|
||||
|
||||
1. 导出场景权限编码:按既有约定实现为 `export_task:commission_record_detail` / `export_task:commission_record_download`。
|
||||
2. 佣金记录列表导出按钮权限:按 `agent_wallet_transaction:export` 的同级约定实现为 `commission_record:export`。
|
||||
@@ -1,48 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 佣金记录列表回溯行与状态筛选
|
||||
佣金记录列表(`GET /api/admin/shops/{shop_id}/commission-records`)MUST 支持 `status` 查询参数(1 已冻结 / 2 解冻中 / 3 已发放 / 4 已失效 / 5 回溯 / 99 待人工修正),且 MUST NOT 提供 `source` 查询参数;`status=5` 即等价于只看回溯行。
|
||||
|
||||
#### Scenario: 只看回溯行
|
||||
- **GIVEN** 用户在佣金记录列表选择状态「回溯」
|
||||
- **WHEN** 前端发起列表查询
|
||||
- **THEN** 前端 MUST 只传 `status=5`
|
||||
- **AND** 前端 MUST NOT 传 `source` 参数
|
||||
|
||||
### Requirement: 佣金记录行新增字段与兜底
|
||||
列表响应的每一行 MUST 支持 `source`(`original` / `clawback`);回溯行 MUST 支持 `original_commission_id`、`refund_id`、`refund_no`、`withdrawable`(恒 `false`)与可为负数的 `amount`、`balance_after`;原佣金行 MUST 支持 `clawback_records` 摘要与 `clawback_total_amount`(负值)。后端 `omitempty` 缺省字段前端 MUST 以 `?? []` / `?? 0` 兜底,金额单位为分。
|
||||
|
||||
#### Scenario: 缺省字段兜底
|
||||
- **GIVEN** 列表响应中的原佣金行未返回 `clawback_records`
|
||||
- **WHEN** 前端渲染该行
|
||||
- **THEN** 前端 MUST 按空数组处理并正常渲染
|
||||
- **AND** 前端 MUST NOT 因缺省字段报错或渲染 `undefined`
|
||||
|
||||
### Requirement: 列表行标识与负数渲染
|
||||
列表 MUST 使用 `source + ':' + id` 作为行 key(原佣金与回溯 ID 空间独立,同一页可能出现相同 `id`);回溯行 MUST 展示「不可提现」标识;负数 `amount` 与 `balance_after` MUST 标红显示;任何跳转详情的入口 MUST 携带该行的 `source`,缺省按原佣金处理。
|
||||
|
||||
#### Scenario: 同一页出现重复 id
|
||||
- **GIVEN** 同一页列表中同时存在原佣金行与回溯行且 `id` 相同
|
||||
- **WHEN** 前端渲染表格
|
||||
- **THEN** 前端 MUST 以 `source + ':' + id` 区分两行
|
||||
- **AND** 前端 MUST NOT 出现行复用或渲染错位
|
||||
|
||||
#### Scenario: 负数金额标红
|
||||
- **GIVEN** 某回溯行 `amount` 为负值
|
||||
- **WHEN** 前端渲染金额列
|
||||
- **THEN** 前端 MUST 标红展示该负值
|
||||
|
||||
### Requirement: 佣金记录详情接口封装
|
||||
前端 MUST 提供 `GET /api/admin/shops/{shop_id}/commission-records/{id}` 的详情封装,并支持可选 `source`:`source=clawback` 时返回回溯详情,`source` 省略时按原佣金处理。原佣金详情 MUST 返回 `clawback_records`;回溯详情 MUST 返回 `original_commission`;越权与不存在 MUST 返回同一结果(佣金明细不存在),前端 MUST NOT 用该接口做存在性判断。
|
||||
|
||||
#### Scenario: 打开回溯行详情
|
||||
- **GIVEN** 用户在列表点击某条回溯行
|
||||
- **WHEN** 前端请求详情
|
||||
- **THEN** 前端 MUST 携带 `source=clawback` 与该行 `id`
|
||||
- **AND** 详情 MUST 展示来源 `original_commission`
|
||||
|
||||
#### Scenario: 越权统一提示
|
||||
- **GIVEN** 详情接口返回「佣金明细不存在」
|
||||
- **WHEN** 前端渲染详情
|
||||
- **THEN** 前端 MUST 展示统一不存在提示
|
||||
- **AND** 前端 MUST NOT 区分越权与不存在两种原因
|
||||
@@ -1,19 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 佣金记录导出场景
|
||||
前端 MUST 支持导出场景 `scene=commission_record`(后端已进白名单),并在导出管理中提供该场景的任务列表入口;创建导出任务时 MUST 提交固定 `scene=commission_record`。导出粒度为佣金记录:原佣金与回溯各占一行,金额与余额同样按分转元、负数带 `-` 展示。
|
||||
|
||||
#### Scenario: 场景页固定查询
|
||||
- **GIVEN** 用户打开导出佣金记录页面
|
||||
- **WHEN** 页面查询导出任务列表
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/export-tasks` 且带 `scene=commission_record`
|
||||
- **AND** 用户 MUST NOT 能在该页面切换到其他场景
|
||||
|
||||
### Requirement: 佣金记录导出筛选受限
|
||||
佣金记录列表发起导出时,`query` MUST 只包含后端支持的筛选 key:`shop_id`、`status`、`commission_source`、`order_no`;MUST NOT 提交 `iccid`、`virtual_no` 及时间筛选,导出弹窗 MUST NOT 直接复用列表全部条件。
|
||||
|
||||
#### Scenario: 列表筛选含不支持的 key
|
||||
- **GIVEN** 佣金记录列表当前筛选包含 `iccid` 与时间范围
|
||||
- **WHEN** 用户确认创建导出任务
|
||||
- **THEN** 提交的 `query` MUST NOT 包含 `iccid`、`virtual_no` 或时间字段
|
||||
- **AND** 提交的 `query` MUST 仅保留 `shop_id`、`status`、`commission_source`、`order_no` 中已填写的项
|
||||
@@ -1,27 +0,0 @@
|
||||
## 1. 类型与 API 层(commission-api)
|
||||
- [x] 1.1 `CommissionStatus` 补充 5(回溯);修正 `CommissionRecordQueryParams.status` 过期注释并支持回溯值
|
||||
- [x] 1.2 `ShopCommissionRecordItem` 新增 `source`、`original_commission_id`、`refund_id`、`refund_no`、`withdrawable`、`clawback_records`、`clawback_total_amount`
|
||||
- [x] 1.3 新增回溯摘要与佣金记录详情类型(原佣金详情含 `clawback_records`,回溯详情含 `original_commission`)
|
||||
- [x] 1.4 `CommissionService` 新增 `getShopCommissionRecordDetail(shopId, id, source?)`
|
||||
|
||||
## 2. 列表页(commission-records-list)
|
||||
- [x] 2.1 行 key 改为 `source + ':' + id`(`my-commission` 与 `agent-fund-overview` 佣金明细)
|
||||
- [x] 2.2 回溯行展示「回溯」状态与「不可提现」标识
|
||||
- [x] 2.3 负数金额与可为负的 `balance_after` 标红渲染
|
||||
- [x] 2.4 `status` 筛选项新增「回溯」
|
||||
- [x] 2.5 列表新增详情入口,且请求必须携带该行 `source`
|
||||
|
||||
## 3. 详情展示(commission-record-detail)
|
||||
- [x] 3.1 原佣金详情展示 `clawback_records` 摘要与 `clawback_total_amount`
|
||||
- [x] 3.2 回溯详情展示来源 `original_commission`
|
||||
- [x] 3.3 越权/不存在统一提示「佣金明细不存在」,不作为存在性判断依据
|
||||
|
||||
## 4. 导出(commission-record-export)
|
||||
- [x] 4.1 `ExportTaskScene` 增加 `commission_record` 并补场景配置
|
||||
- [x] 4.2 新增导出佣金记录场景页、路由、菜单与中英文案
|
||||
- [x] 4.3 佣金记录列表新增导出入口,`query` 仅提交 `shop_id` / `status` / `commission_source` / `order_no`
|
||||
|
||||
## 5. 验证
|
||||
- [x] 5.1 运行 `npm run build`(含 `vue-tsc --noEmit`)
|
||||
- [x] 5.2 运行 `npm run check:encoding`
|
||||
- [x] 5.3 运行 `openspec.cmd validate add-commission-clawback-records --strict`
|
||||
@@ -1,88 +0,0 @@
|
||||
## Context
|
||||
|
||||
员工代收款是 8 月迭代新增的财务能力,普通员工与超级管理员共用同一套接口,靠登录态区分数据范围。后台管理端需要新增三类页面,并复用已有的表格、搜索、详情与上传组件。`docs/admin-openapi.yaml` 未随仓库提供,接口字段以后端契约(需求文档 + 创建订单接口 OpenAPI 片段)为准,类型集中在一个文件便于联调收敛。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals**
|
||||
|
||||
- 在财务管理下提供收款方式、员工代收款账单、核销申请三类页面。
|
||||
- 复用 `ArtTableFullScreen`、`ArtSearchBar`、`ArtTableHeader`、`ArtTable`、`DetailPage`、`VoucherUpload`、`PaymentVoucherDialog`、`useCheckedColumns` 等既有组件与约定。
|
||||
- 复用既有企微审批场景配置能力,仅新增业务类型。
|
||||
|
||||
**Non-Goals**
|
||||
|
||||
- 不实现后端接口、数据库、Worker、企微回调。
|
||||
- 不实现 H5/C 端页面与支付流程。
|
||||
- 不新增导出任务场景(需求文档未要求)。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 接口契约
|
||||
|
||||
| 能力 | 关键字段 |
|
||||
|---|---|
|
||||
| 统一响应 | `{ code, data, msg, timestamp }` |
|
||||
| 列表分页 | 账单列表返回 `{ items, total, page, size }`,取数处对 `items` / `list` / `records` 做兼容 |
|
||||
| 收款方式 | `{ id, code, name, sort, enabled, remark, created_at, updated_at }`,列表接口返回 `{ items, page, size, total }`,支持 `page` / `page_size` / `enabled` / `keyword` 筛选 |
|
||||
| 账单列表项 | `{ id, source_type, source_type_name, source_no, debtor_snapshot, customer_snapshot, receivable_amount, received_amount, reserved_amount, remaining_amount, status, status_name, approval_pending, closed_reason, created_at, updated_at }` |
|
||||
| 账单详情 | `data` 为 `{ bill, refunds, allocations, applications }`;`refunds` 为退款冲销(`refund_id`、`source_order_id`、`refund_amount`、`reduced_amount`、`bill_receivable_amount`、`outcome_name`),`allocations` 为账单侧分摊(含 `application_id` / `application_status_name` / `attempt_id`),`applications` 内嵌该申请的 `attempts` |
|
||||
| 账单统计 | `{ receivable_total, received_total, unsettled_total, pending_bill_count }` |
|
||||
| 账单筛选 | `page`、`page_size`、`source_type`、`source_no`、`status`、`debtor_account_id`、`customer_id`、`created_from`(`YYYY-MM-DD`)、`created_to`(`YYYY-MM-DD`) |
|
||||
| 核销申请请求体 | `{ payment_method_id, paid_amount, paid_at, payer_name, external_transaction_no, payment_voucher_keys, remark, allocations: [{ bill_id, amount }], acting_reason }` |
|
||||
| 核销申请列表项 | `{ id, applicant_account_id, acting_operator_id, payment_method_id, payment_method_name, paid_amount, payer_name, external_transaction_no, status, status_name, terminal_reason, decided_at, created_at, updated_at }` |
|
||||
| 核销申请详情 | `data` 为 `{ application, allocations, attempts }`,`attempts` 保存每次提交的完整材料快照,重新提交不清空历史 |
|
||||
|
||||
账单状态为数字枚举 `0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭;申请状态为数字枚举 `0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭;两者展示均优先使用后端 `status_name`。
|
||||
|
||||
### 页面与路由组织
|
||||
|
||||
在 `/finance` 下新增:
|
||||
|
||||
| 路由 | 页面 | 说明 |
|
||||
|---|---|---|
|
||||
| `/finance/employee-collection/bills` | 员工代收款账单 | 统计 + 列表 |
|
||||
| `/finance/employee-collection/bills/detail/:id` | 账单详情 | 隐藏菜单 |
|
||||
| `/finance/employee-collection/applications` | 核销申请 | 列表 + 创建 |
|
||||
| `/finance/employee-collection/applications/detail/:id` | 核销申请详情 | 隐藏菜单 |
|
||||
| `/finance/employee-collection/payment-methods` | 收款方式管理 | 仅超管 |
|
||||
|
||||
账单与申请拆分为独立菜单,符合项目「列表页 + 详情页」的既有组织方式,避免单页堆叠过多交互。账单列表的「店铺」筛选用远程搜索复用 `ShopService.getShops`,与退款列表一致。
|
||||
|
||||
账单详情与核销申请详情保持只读:页面只在顶部保留「返回」导航,创建核销申请、关闭账单、修改并重新提交等操作入口统一放在列表页的操作列,详情页不出现业务操作按钮。
|
||||
|
||||
### 权限编码
|
||||
|
||||
新增 `src/config/constants/augustIteration.ts`,沿用 `模块:动作` 风格,例如 `employee_collection:bill_close`、`employee_collection:application_create`。页面级 `permissions` 用于菜单可见性,按钮级编码用于 `hasAuth()` / `v-permission`。
|
||||
|
||||
### 金额、时间与附件
|
||||
|
||||
- 金额统一以「分」传输,展示时通过 `fenToYuan` / `formatCurrency` 转换,与退款、代理充值保持一致。
|
||||
- 所有时间字段统一通过 `formatDateTime` 格式化为 `YYYY-MM-DD HH:mm:ss`,不在模板中直接输出后端原始时间字符串。
|
||||
- 附件仅返回对象 Key(`payment_voucher_keys`),展示复用 `PaymentVoucherDialog`,由预签名下载接口换取访问地址。
|
||||
|
||||
### 核销申请分摊
|
||||
|
||||
创建申请时按账单逐条录入核销金额,并填写付款事实(付款金额、付款方名称、付款时间、外部交易流水号),前端校验:
|
||||
|
||||
- 至少选择 1 张账单,最多 N 张;
|
||||
- 单张核销金额不得大于账单未核销金额,且大于 0;
|
||||
- 付款金额(`paid_amount`,分)不得小于各账单分摊之和(允许存在差额);
|
||||
- 超管代办时 `acting_reason` 必填;
|
||||
- 付款凭证至少 1 个 Key。
|
||||
|
||||
提交成功后自动发起企微审批;仅已驳回申请可再次进入弹窗修改并重新提交,重新提交生成新的审批实例,历史审批记录只读展示。未配置企微审批场景时后端返回 503,前端展示「企微审批场景未配置,请联系管理员」并保留已填内容。
|
||||
|
||||
### 企微审批场景
|
||||
|
||||
`WecomBusinessType` 增加 `employee_collection_approval`,企微审批场景页面下拉新增「员工代收款审批」。模板控件同步、字段查询、字段映射保存全部复用既有 `WecomService`。
|
||||
|
||||
### 订单付款凭证规则
|
||||
|
||||
线下订单创建时,满足「平台账号(`user_type` 为 1 或 2)操作 + 非赠送套餐 + 实际收款金额大于 0」条件的订单会生成员工代收款账单,此时 `payment_voucher_key` 非必填,字段结构不变,付款凭证改在核销申请中提交。前端以当前登录账号类型、所选套餐是否赠送、套餐有效价格(`effective_retail_price` / `suggested_retail_price` / `retail_price`)判断是否展示提示并放宽必填;赠送套餐或其他非平台账号的线下订单仍需上传凭证。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **接口字段以契约文档为准**:`docs/admin-openapi.yaml` 未入库,字段来自需求文档与创建订单接口片段;类型集中在一个文件,便于联调时收敛修改。
|
||||
- **员工/超管同接口**:前端不做数据范围过滤,仅做展示与操作可见性控制,数据隔离以后端为准。
|
||||
- **关闭账单与审批中申请**:前端依据 `approval_pending` 与状态字段禁用关闭按钮,最终一致性以后端校验为准。
|
||||
@@ -1,25 +0,0 @@
|
||||
# Change: 员工代收款功能前端对接
|
||||
|
||||
## Why
|
||||
|
||||
8 月产品迭代新增「员工代收款」能力:员工线下代收款项后,通过创建核销申请、经企业微信审批完成账单核销;超级管理员可维护收款方式、查看全部账单并关闭账单。后端接口已按 `docs/产品迭代8月份/员工代收款功能简介.md` 落地,后台管理端目前缺少收款方式管理、账单列表与核销申请页面,普通员工与超级管理员的分类视图、以及线下订单付款凭证规则尚未接入。
|
||||
|
||||
本次已按后端实际契约对齐字段:列表响应统一使用 `items` / `total` / `page` / `size`;账单状态(`0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭)与申请状态(`0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭)为数字枚举;收款方式列表返回 `{ items, page, size, total }` 并支持 `enabled` / `keyword` 筛选;核销申请请求体使用 `paid_amount`、`paid_at`、`payer_name`、`external_transaction_no`、`payment_voucher_keys` 与 `allocations`。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 `employee-collection` 能力的前端类型与服务封装:收款方式 4 个接口、员工代收款账单 4 个接口、核销申请 4 个接口。
|
||||
- 新增「收款方式管理」页面(仅超级管理员):列表 + 新增/编辑弹窗 + 删除;已被核销申请引用的方式不可删除、不可修改 `code`(未被引用时可改),可停用。
|
||||
- 新增「员工代收款账单」页面:应收/已核销/未核销/待处理账单统计 + 列表 + 详情;普通员工仅见本人账单,超级管理员可见全部;存在审批中申请时账单不可关闭,关闭必须填写原因。
|
||||
- 新增「核销申请」页面:列表 + 详情(含分摊账单、付款凭证、付款信息与全部审批尝试记录)+ 创建/重新提交弹窗;一笔线下收款可核销 1~N 张账单,填写付款金额、付款方名称、付款时间、外部交易流水号、选择收款方式并上传付款凭证;提交后自动发起企微审批;仅已驳回申请可修改并重新提交,重新提交生成新的审批实例且历史记录不被覆盖。
|
||||
- 扩展企业微信审批场景:新增 `employee_collection_approval` 业务类型,复用既有企微应用列表、模板控件同步与业务字段查询接口。
|
||||
- 调整订单创建:由平台账号操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单,该场景 `payment_voucher_key` 改为非必填,付款凭证改在核销申请中提交,字段结构保持不变。
|
||||
- 附件接口仅返回对象 Key,统一通过系统既有预签名下载接口展示。
|
||||
- **不实现后端接口、数据库、Worker、企微回调**;**不实现 H5/C 端页面**。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `employee-collection`
|
||||
- Affected code: `src/types/api/employeeCollection.ts`、`src/api/modules/employeeCollection.ts`、`src/views/finance/employee-collection/*`、`src/router/routes/asyncRoutes.ts`、`src/router/routesAlias.ts`、`src/config/constants/augustIteration.ts`、`src/types/api/wecom.ts`、`src/views/settings/wecom/scenes/index.vue`、`src/views/order-management/order-list/index.vue`、`src/locales/langs/{zh,en}.json`
|
||||
- Dependencies: `docs/产品迭代8月份/员工代收款功能简介.md`
|
||||
- Contract note: `docs/admin-openapi.yaml` 未随仓库提供,字段以需求文档描述的后端契约与创建订单接口的 OpenAPI 片段为准;类型集中在单一文件,联调时便于收敛。
|
||||
@@ -1,155 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 收款方式管理
|
||||
超级管理员 MUST 能够维护线下收款方式,包括名称、编码、排序、状态与备注;普通员工只能看到启用的收款方式。收款方式列表接口 MUST 返回 `{ items, page, size, total }`,并 MUST 支持 `enabled` 与 `keyword` 筛选。已被核销申请引用的收款方式 MUST NOT 被删除;引用后修改稳定编码 MUST 被后端拒绝,前端 MUST 展示错误提示。
|
||||
|
||||
#### Scenario: 超级管理员新增收款方式
|
||||
- **GIVEN** 超级管理员已登录并拥有收款方式新增权限
|
||||
- **WHEN** 其填写名称、唯一编码、排序、状态与备注并提交
|
||||
- **THEN** 系统 MUST 调用新增接口并在成功后刷新列表
|
||||
- **AND** 新增成功后 MUST 清空并关闭弹窗
|
||||
|
||||
#### Scenario: 删除被引用的收款方式
|
||||
- **GIVEN** 某收款方式已被业务引用
|
||||
- **WHEN** 超级管理员尝试删除该方式
|
||||
- **THEN** 前端 MUST 阻止删除或展示后端返回的业务错误
|
||||
- **AND** MUST 提示改为停用
|
||||
|
||||
#### Scenario: 修改收款方式编码
|
||||
- **GIVEN** 超级管理员打开编辑弹窗
|
||||
- **WHEN** 其修改稳定编码并提交
|
||||
- **THEN** 前端 MUST 提交最新编码
|
||||
- **AND** 若该方式已被核销申请引用,后端拒绝时前端 MUST 展示错误提示
|
||||
|
||||
### Requirement: 员工代收款账单统计
|
||||
账单页面 MUST 展示应收金额、已核销金额、未核销金额与待处理账单数量四项统计,数据 MUST 来自 `GET /api/admin/employee-collection-bills/statistics`,字段为 `receivable_total`、`received_total`、`unsettled_total`、`pending_bill_count`;前端 MUST NOT 通过遍历当前分页数据自行计算。
|
||||
|
||||
#### Scenario: 加载账单统计
|
||||
- **GIVEN** 用户进入员工代收款账单页面
|
||||
- **WHEN** 页面初始化或筛选条件变化
|
||||
- **THEN** 前端 MUST 调用账单统计接口
|
||||
- **AND** MUST 将后端返回的「分」按元格式化后展示应收、已核销与未核销金额
|
||||
|
||||
### Requirement: 员工代收款账单列表与详情
|
||||
账单列表响应 MUST 为 `{ items, total, page, size }`,前端 MUST 兼容 `items` / `list` / `records` 等列表字段。列表 MUST 支持按来源(`source_type`)、来源单号(`source_no`)、账单状态、客户/店铺(`customer_id`)与创建时间(`created_from` / `created_to`,`YYYY-MM-DD`)筛选,并展示账单编号、来源、关联单号、负责员工、客户/店铺、应收金额、已核销金额、未核销金额与状态。账单状态 MUST 为数字枚举:`0` 待核销、`1` 部分核销、`2` 已核销、`3` 已关闭,展示 MUST 优先使用后端 `status_name`。账单详情响应 MUST 为 `{ bill, refunds, allocations, applications }`:`refunds` MUST 展示退款金额、冲减应收、冲销前应收与处理结果,`applications` MUST 可展开查看该申请的审批尝试记录。
|
||||
|
||||
#### Scenario: 普通员工查看账单
|
||||
- **GIVEN** 普通员工已登录
|
||||
- **WHEN** 其打开账单列表
|
||||
- **THEN** 列表 MUST 只展示后端返回的本人账单数据
|
||||
- **AND** MUST NOT 展示仅超管可见的操作入口
|
||||
|
||||
#### Scenario: 打开账单详情
|
||||
- **GIVEN** 用户拥有账单详情权限
|
||||
- **WHEN** 其点击账单号或详情操作
|
||||
- **THEN** 前端 MUST 跳转账单详情页并加载对应账单(详情数据取自 `data.bill`)
|
||||
- **AND** 详情 MUST 展示退款冲销、核销分摊与关联核销申请
|
||||
|
||||
### Requirement: 关闭账单
|
||||
超级管理员 MUST 能够关闭账单,且关闭原因必填(最多 500 字符)。仅待核销或部分核销账单可关闭;存在审批中的核销申请(`approval_pending` 为真)时,账单 MUST NOT 被关闭。
|
||||
|
||||
#### Scenario: 存在审批中申请时关闭账单
|
||||
- **GIVEN** 账单存在审批中的核销申请
|
||||
- **WHEN** 用户查看该账单操作
|
||||
- **THEN** 关闭入口 MUST 被禁用或不可见
|
||||
- **AND** MUST 展示不可关闭的原因提示
|
||||
|
||||
#### Scenario: 关闭原因必填
|
||||
- **GIVEN** 账单可关闭
|
||||
- **WHEN** 超级管理员打开关闭弹窗并留空原因提交
|
||||
- **THEN** 前端 MUST 阻止提交并提示填写原因
|
||||
|
||||
### Requirement: 创建核销申请
|
||||
员工 MUST 能够使用一笔线下收款核销 1~N 张账单。创建申请 MUST 提交收款方式 `payment_method_id`、付款金额 `paid_amount`(分,大于 0)、付款方名称 `payer_name`、付款时间 `paid_at`(带时区 RFC3339)、外部交易流水号 `external_transaction_no`、付款凭证 `payment_voucher_keys`(1~5 个对象 Key)与账单分摊 `allocations`。超级管理员代办时 MUST 填写 `acting_reason`,本人办理 MUST NOT 提交该字段。提交成功后系统 MUST 自动发起企业微信审批。
|
||||
|
||||
#### Scenario: 一笔收款核销多张账单
|
||||
- **GIVEN** 用户选择了多张可核销账单
|
||||
- **WHEN** 其录入各账单核销金额、选择收款方式并填写付款事实后提交
|
||||
- **THEN** 前端 MUST 校验各账单核销金额大于 0 且不超过账单未核销余额
|
||||
- **AND** MUST 以 `allocations: [{ bill_id, amount }]` 提交账单分摊明细
|
||||
|
||||
#### Scenario: 付款金额小于核销合计
|
||||
- **GIVEN** 用户已录入各账单核销金额
|
||||
- **WHEN** 其填写的付款金额小于核销合计即提交
|
||||
- **THEN** 前端 MUST 阻止提交并提示付款金额不能小于核销合计
|
||||
|
||||
#### Scenario: 缺少付款凭证
|
||||
- **GIVEN** 用户已选择账单与收款方式
|
||||
- **WHEN** 其未上传任何付款凭证即提交
|
||||
- **THEN** 前端 MUST 阻止提交并提示上传付款凭证
|
||||
|
||||
#### Scenario: 缺少付款事实
|
||||
- **GIVEN** 用户已选择账单与收款方式
|
||||
- **WHEN** 其未填写付款方名称、付款时间或外部交易流水号即提交
|
||||
- **THEN** 前端 MUST 阻止提交并提示补齐必填项
|
||||
|
||||
#### Scenario: 超管代办未填写原因
|
||||
- **GIVEN** 超级管理员以代办身份创建申请
|
||||
- **WHEN** 其未填写 `acting_reason` 即提交
|
||||
- **THEN** 前端 MUST 阻止提交并提示填写代办原因
|
||||
|
||||
#### Scenario: 企微审批场景未配置
|
||||
- **GIVEN** 企业微信审批场景尚未配置
|
||||
- **WHEN** 用户提交核销申请
|
||||
- **THEN** 前端 MUST 展示后端返回的 503 提示「企微审批场景未配置,请联系管理员」
|
||||
- **AND** MUST 保留用户已填写的内容且不产生申请数据
|
||||
|
||||
### Requirement: 核销申请列表与详情
|
||||
核销申请列表响应 MUST 为 `{ items, page, size, total }`,MUST 支持按状态、收款方式与创建时间筛选;列表项 MUST 包含 `payment_method_name`、`paid_amount`、`status`、`status_name` 与 `created_at`,状态 MUST 优先展示后端 `status_name`。申请详情响应 MUST 为 `{ application, allocations, attempts }`;`attempts` MUST 按提交顺序展示全部审批尝试记录,包含付款金额、付款方、流水号、付款凭证与审批意见。
|
||||
|
||||
#### Scenario: 查看审批历史
|
||||
- **GIVEN** 申请存在多次审批尝试记录
|
||||
- **WHEN** 用户打开申请详情
|
||||
- **THEN** 详情 MUST 按提交顺序展示每次尝试的提交材料与审批状态
|
||||
- **AND** 历史材料 MUST NOT 因重新提交而被覆盖
|
||||
|
||||
### Requirement: 核销申请重新提交
|
||||
仅已驳回(`status` 为 `2`)的申请 MUST 允许修改并重新提交;重新提交 MUST 生成新的企业微信审批实例,且历史审批记录 MUST NOT 被覆盖。重新提交入口 MUST 位于核销申请列表的操作列,详情页 MUST 只读且 MUST NOT 展示业务操作按钮(仅保留返回导航)。
|
||||
|
||||
#### Scenario: 重新提交被驳回申请
|
||||
- **GIVEN** 申请状态为已驳回
|
||||
- **WHEN** 用户在核销申请列表点击「修改并重新提交」并修改账单分摊、收款方式、付款事实或付款凭证后提交
|
||||
- **THEN** 前端 MUST 调用修改接口重新提交
|
||||
- **AND** 成功后 MUST 刷新详情并展示新的审批实例状态
|
||||
|
||||
#### Scenario: 非驳回申请不可修改
|
||||
- **GIVEN** 申请处于审批中或已通过
|
||||
- **WHEN** 用户查看核销申请列表
|
||||
- **THEN** 修改并重新提交入口 MUST 不可见或不可用
|
||||
|
||||
#### Scenario: 详情页只读
|
||||
- **GIVEN** 用户打开账单详情或核销申请详情
|
||||
- **WHEN** 页面渲染完成
|
||||
- **THEN** 页面 MUST NOT 展示创建核销申请、关闭账单或修改并重新提交等业务操作按钮
|
||||
- **AND** MUST 只保留返回导航
|
||||
|
||||
### Requirement: 企业微信审批场景配置
|
||||
企业微信审批场景 MUST 支持 `employee_collection_approval` 业务类型,复用既有的应用列表、模板控件同步、业务字段查询与字段映射保存接口。
|
||||
|
||||
#### Scenario: 配置员工代收款审批场景
|
||||
- **GIVEN** 超级管理员打开企微审批场景页面
|
||||
- **WHEN** 其选择业务类型「员工代收款审批」
|
||||
- **THEN** 前端 MUST 使用 `employee_collection_approval` 调用模板同步、字段查询与保存接口
|
||||
|
||||
### Requirement: 附件预签名展示
|
||||
附件接口 MUST 只返回对象存储 Key(`payment_voucher_keys`);前端 MUST 通过系统既有的预签名下载接口获取实际访问地址后再展示。
|
||||
|
||||
#### Scenario: 查看付款凭证
|
||||
- **GIVEN** 申请包含付款凭证 Key
|
||||
- **WHEN** 用户点击查看付款凭证
|
||||
- **THEN** 前端 MUST 先批量换取预签名地址
|
||||
- **AND** 图片 MUST 支持预览,非图片 MUST 支持查看或下载
|
||||
|
||||
### Requirement: 订单付款凭证规则调整
|
||||
由平台账号(`user_type` 为 1 或 2)操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单;该场景 `payment_voucher_key` MUST 变为非必填,付款凭证改在核销申请中提交,订单字段结构 MUST 保持不变。其余线下订单 MUST 继续要求付款凭证。
|
||||
|
||||
#### Scenario: 线下订单生成代收款账单
|
||||
- **GIVEN** 当前登录账号为平台账号,所选套餐非赠送且实际收款金额大于 0,支付方式为线下支付
|
||||
- **WHEN** 用户创建该订单
|
||||
- **THEN** 前端 MUST 不再强制要求上传付款凭证
|
||||
- **AND** MUST 提示付款凭证将在核销申请中提交
|
||||
|
||||
#### Scenario: 赠送套餐或非平台账号的线下订单
|
||||
- **GIVEN** 所选套餐为赠送套餐,或当前账号非平台账号,或实际收款金额为 0
|
||||
- **WHEN** 用户以线下支付方式创建订单
|
||||
- **THEN** 前端 MUST 继续要求上传付款凭证
|
||||
@@ -1,48 +0,0 @@
|
||||
## 1. Contract and API Types
|
||||
|
||||
- [x] 1.1 新增 `src/types/api/employeeCollection.ts`:收款方式、账单、核销申请、统计、查询参数与请求/响应类型;列表统一使用 `items` / `total` / `page` / `size`。
|
||||
- [x] 1.2 定义账单状态(`0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭)与申请状态(`0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭)数字枚举,并保留后端 `*_name` 展示字段。
|
||||
- [x] 1.3 金额字段以「分」传输;附件字段使用 `payment_voucher_keys` 对象存储 Key 数组,展示复用预签名下载接口。
|
||||
- [x] 1.4 新增 `src/api/modules/employeeCollection.ts` 并在 `src/api/modules/index.ts`、`src/types/api/index.ts` 导出。
|
||||
- [x] 1.5 按后端实际契约收敛字段:账单详情为 `{ bill, refunds, allocations, applications }`,核销申请详情为 `{ application, allocations, attempts }`,付款金额/付款方/付款时间/外部交易流水号以 `paid_amount` / `payer_name` / `paid_at` / `external_transaction_no` 提交。
|
||||
|
||||
## 2. Permissions, Routes, and Menu
|
||||
|
||||
- [x] 2.1 新增 `src/config/constants/augustIteration.ts`,集中声明页面与按钮权限编码。
|
||||
- [x] 2.2 在 `src/router/routesAlias.ts` 与 `src/router/routes/asyncRoutes.ts` 的财务管理下新增账单、核销申请、收款方式路由。
|
||||
- [x] 2.3 在 `src/locales/langs/zh.json`、`en.json` 的 `menus.financialManagement` 下补充菜单文案。
|
||||
|
||||
## 3. Payment Methods Page
|
||||
|
||||
- [x] 3.1 实现收款方式列表(名称、编码、排序、状态、备注、创建时间);接口返回 `{ items, page, size, total }`,复用 `ArtTableFullScreen` / `ArtSearchBar` / `ArtTableHeader` / `ArtTable`。
|
||||
- [x] 3.2 实现新增/编辑弹窗;编辑时允许提交 `code`,被核销申请引用时由后端拒绝并提示。
|
||||
- [x] 3.3 实现删除操作,被引用的方式给出提示并引导停用。
|
||||
|
||||
## 4. Bills Pages
|
||||
|
||||
- [x] 4.1 实现账单统计卡片(应收、已核销、未核销、待处理账单数量),数据来自 `GET /api/admin/employee-collection-bills/statistics`。
|
||||
- [x] 4.2 实现账单列表与筛选(来源、来源单号、状态、客户/店铺、创建时间范围),展示账单号、来源、关联单号、负责员工、客户/店铺、应收/已核销/未核销金额与状态。
|
||||
- [x] 4.3 实现账单详情,展示账单信息、退款冲销、核销分摊与关联核销申请(关联申请可展开查看审批尝试记录)。
|
||||
- [x] 4.4 实现超管关闭账单弹窗,关闭原因必填;存在审批中申请时禁用关闭并给出说明。
|
||||
- [x] 4.5 列表「创建核销申请」入口按权限与账单状态控制可用性。
|
||||
|
||||
## 5. Applications Pages
|
||||
|
||||
- [x] 5.1 实现核销申请列表与筛选(状态、收款方式、创建时间),展示申请编号、收款方式、付款金额、状态与提交时间。
|
||||
- [x] 5.2 实现创建/重新提交弹窗:选择 1~N 张可核销账单、按账单录入分摊金额、填写付款金额/付款方名称/付款时间/外部交易流水号、选择收款方式并上传付款凭证。
|
||||
- [x] 5.3 超管代办时必须填写 `acting_reason`,否则禁止提交。
|
||||
- [x] 5.4 实现申请详情,展示申请信息、分摊账单、付款凭证与全部审批尝试记录;历史记录只读,不被重新提交覆盖。
|
||||
- [x] 5.5 仅已驳回申请展示「修改并重新提交」,提交成功后生成新的审批实例并刷新详情。
|
||||
- [x] 5.6 统一处理 503「企微审批场景未配置」等业务错误,保留用户已填内容。
|
||||
|
||||
## 6. WeCom Scene and Order Rule
|
||||
|
||||
- [x] 6.1 扩展 `WecomBusinessType` 增加 `employee_collection_approval`,并在企微审批场景页面新增可选业务类型。
|
||||
- [x] 6.2 场景保存复用既有 `inspectTemplate`、`getBusinessFields`、`saveScene` 接口,无需新增企微接口。
|
||||
- [x] 6.3 调整线下订单创建:平台账号操作、非赠送且实际收款金额大于 0 时 `payment_voucher_key` 非必填,字段结构不变。
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- [x] 7.1 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`,确保类型与编码通过。
|
||||
- [x] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。 — 已核验:应用/账单/收款方式页 v-permission 按钮、bills 关闭操作 hasAuth(billClose)+canCloseBill、表单 isActing=isSuperAdmin 控制代办字段
|
||||
- [x] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。 — 已核验:ApplicationFormDialog 用 fenToYuan/yuanToFen;detail.vue 用 PaymentVoucherDialog 预览凭证;getErrorMessage 提取后端 msg(含 503 未配置场景)兜底
|
||||
@@ -1,38 +0,0 @@
|
||||
# Design: H5 运营弹窗配置管理(管理后台)
|
||||
|
||||
## 页面结构
|
||||
|
||||
- 设置管理下新增「H5运营弹窗配置」入口(仅超管/平台):
|
||||
- 列表页 `src/views/settings/h5-popup-configuration/index.vue`
|
||||
- 创建/编辑表单弹窗(复用 ArtForm + ArtSearchBar/ArtTable 模式)
|
||||
- 详情以弹窗/抽屉展示完整配置
|
||||
- 分页、筛选、表格沿用 ArtTable / ArtSearchBar / ArtTableHeader 与统一响应结构(`code/msg/timestamp/data`、分页 `items/page/size/total`)。
|
||||
|
||||
## 接口与类型
|
||||
|
||||
- 新增 `src/api/modules/h5PopupConfiguration.ts` 与 `src/types/api/h5PopupConfiguration.ts`:
|
||||
- `GET /api/admin/h5-popup-configurations`(page/page_size/enabled,倒序分页)
|
||||
- `POST /api/admin/h5-popup-configurations`(创建)
|
||||
- `GET /api/admin/h5-popup-configurations/{id}`(详情)
|
||||
- `PUT /api/admin/h5-popup-configurations/{id}`(更新,整表提交,版本 +1)
|
||||
- `POST /api/admin/h5-popup-configurations/{id}/enable`、`{id}/disable`(请求体 `{ id }`,返回更新后配置)
|
||||
- 枚举映射常量集中放置:`pages`(home/asset_detail/package_purchase/asset_wallet_recharge)、`frequency`(once/daily)、`action_type`(package_purchase/asset_wallet_recharge,空字符串 = 无)、`card_types`(CMCC/CUCC/CTCC/CBN)。
|
||||
|
||||
## 表单约定
|
||||
|
||||
- `pages` 多选必填(空数组非法);`title`(1–100)/`content`(1–2000)/`frequency`/起止时间必填。
|
||||
- 范围三选器(店铺/设备类型/卡类型)空数组 = 全量,回显「全部」。
|
||||
- 起止时间用 datetimerange,提交转 ISO 8601(`starts_at`/`ends_at`),`ends_at` 不得早于 `starts_at`。
|
||||
- `priority` 数字输入(0–1000000);`enabled` 开关;`action_type` 下拉(含「无」选项,提交空字符串清除受控动作)。
|
||||
- 编辑表单整表提交;编辑保存成功后提示「每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次」。
|
||||
- 启用/停用为行操作(停用需二次确认),以接口返回的更新后配置刷新当前行。
|
||||
|
||||
## 权限与可见性
|
||||
|
||||
- 权限码:`h5_popup_configuration:list`(菜单/列表)、`:create`、`:update`、`:enable`、`:disable`。
|
||||
- 路由 `meta.permissions` + 按钮 `v-permission`/`usePermission`,仅超管/平台可见;代理/企业/个人由后端 403 兜底并原文透传失败文案。
|
||||
- 不提供删除入口(文档无 DELETE 接口)。
|
||||
|
||||
## 待确认
|
||||
|
||||
- 权限编码以后端菜单配置为准(前端默认 `h5_popup_configuration:list/create/update/enable/disable`)。
|
||||
@@ -1,46 +0,0 @@
|
||||
# Change: 新增 H5 运营弹窗配置管理(管理后台)
|
||||
|
||||
## Why
|
||||
|
||||
H5 端(个人客户)需要在首页、资产详情、套餐购买、资产钱包充值等页面投放运营/风险弹窗,弹窗文案、命中页面、投放范围与生效规则需要后台配置。当前后台没有任何弹窗配置入口,运营只能走数据库操作。
|
||||
|
||||
本 Change 为管理后台新增「H5 运营弹窗配置」管理能力:配置标题与正文、命中页面、投放范围(店铺/设备类型/卡类型)、优先级、投放频率、生效时间与启停。
|
||||
|
||||
H5 端契约(`GET /api/c/v1/popup-candidates`、`POST /api/c/v1/risk-exchanges/{asset_id}/address`、通知列表/未读/已读接口)不在本 Change 范围,需求方已明确不处理 C 端。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 6 个后台接口(仅超管/平台,代理/企业/个人 403):
|
||||
- `GET /api/admin/h5-popup-configurations`:配置列表,`page`(1–10000)/ `page_size`(1–100)/ `enabled` 筛选,按最近更新时间倒序分页返回 `items/page/size/total`。
|
||||
- `POST /api/admin/h5-popup-configurations`:创建;`title`/`content`/`pages`/`frequency`/`starts_at`/`ends_at` 必填;`priority`/`enabled`/`action_type`/`shop_ids`/`device_types`/`card_types` 可选(范围集合空数组 = 全量)。
|
||||
- `GET /api/admin/h5-popup-configurations/{id}`:详情(含 `version`、`frequency_text`、`enabled_text`、`creator`/`updater`)。
|
||||
- `PUT /api/admin/h5-popup-configurations/{id}`:更新;成功即版本 +1,新版本可向原命中客户按频率重新投放一次,旧版本已投放通知的内容与快照不被改写;除 `id` 外全部字段可选,**不传保持原值**;`action_type` 传空字符串 = 清除受控动作;`shop_ids`/`device_types`/`card_types` 传空数组 = 改为全量;`pages` 传空数组非法(页面必选);`enabled` 不传保持原值(启停刷新最近更新时间);`ends_at` 不得早于 `starts_at`;`content` 1–2000 字符、`title` 1–100 字符、`priority` 0–1000000。
|
||||
- `POST /api/admin/h5-popup-configurations/{id}/enable`:启用,参与候选匹配,刷新 `updated_at`(影响同优先级排序),返回更新后的配置。
|
||||
- `POST /api/admin/h5-popup-configurations/{id}/disable`:停用,停止新投放,历史通知在展示期内仍可见;刷新 `updated_at`,返回更新后的配置。
|
||||
- 新增「H5 运营弹窗配置」管理页(设置管理下,仅超管/平台可见):分页列表 + 创建/编辑表单 + 详情 + 行操作启用/停用。
|
||||
- 前端契约约定:
|
||||
- 范围集合 `shop_ids` / `device_types` / `card_types` 空数组 = 全量,列表与编辑回显「全部」。
|
||||
- 枚举:`pages` = home / asset_detail / package_purchase / asset_wallet_recharge;`frequency` = once / daily;`action_type` = package_purchase / asset_wallet_recharge(空字符串 = 无受控动作);`card_types` = CMCC / CUCC / CTCC / CBN。
|
||||
- 无「删除」接口,页面不提供删除入口;停用不清数据,列表保留历史配置。
|
||||
- 编辑表单整表提交(所有字段都传,未传语义仅在部分更新时生效);范围清空传空数组、动作清除传空字符串、页面集合不可为空。
|
||||
- 权限编码由前端确定:`h5_popup_configuration:list` / `:create` / `:update` / `:enable` / `:disable`。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `h5-popup-configuration-management`
|
||||
- Affected code:
|
||||
- `src/api/modules/h5PopupConfiguration.ts`(新增)
|
||||
- `src/types/api/h5PopupConfiguration.ts`(新增)
|
||||
- `src/api/modules/index.ts`、`src/types/api/index.ts`
|
||||
- `src/config/constants/`(权限码常量)
|
||||
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`(设置管理下新增菜单与路由)
|
||||
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||
- `src/views/settings/h5-popup-configuration/`(列表、表单弹窗、详情)
|
||||
- Dependencies:
|
||||
- 后端按 `docs/产品迭代8月份/通知.md` 提供上述 6 接口,Bearer JWT 鉴权,代理/企业/个人 403。
|
||||
- Breaking changes:
|
||||
- 无;全部为新增页面、接口模块与类型。
|
||||
- 待确认:
|
||||
- 权限编码(前端默认 `h5_popup_configuration:list/create/update/enable/disable`)以后端菜单配置为准。
|
||||
- 列表分页上限与「按最近更新时间倒序」以文档为准,联调核对后端行为。
|
||||
@@ -1,117 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: H5 运营弹窗配置列表
|
||||
|
||||
后台 MUST 提供 H5 运营弹窗配置的分页列表(GET /api/admin/h5-popup-configurations),支持 `page`(1–10000)/ `page_size`(1–100)/ `enabled` 筛选,按最近更新时间倒序返回 `items/page/size/total`;列表项 MUST 包含标题、命中页面、范围(店铺/设备类型/卡类型)、优先级、频率(`frequency_text`)、启停(`enabled_text`)、受控动作、生效起止时间、版本、创建/更新人与时间。
|
||||
|
||||
#### Scenario: 按启停状态筛选并分页
|
||||
|
||||
- **GIVEN** 超管/平台账号进入「H5运营弹窗配置」列表
|
||||
- **WHEN** 以 page/page_size/enabled 发起查询
|
||||
- **THEN** 前端 MUST 展示分页结果(后端按最近更新时间倒序)
|
||||
- **AND** 切换 enabled 筛选后按新条件重新请求
|
||||
|
||||
#### Scenario: 范围空数组显示全部
|
||||
|
||||
- **GIVEN** 某条配置的 shop_ids/device_types/card_types 为空数组
|
||||
- **THEN** 列表 MUST 在范围列展示「全部」
|
||||
|
||||
#### Scenario: 越权访问
|
||||
|
||||
- **GIVEN** 代理/企业/个人账号访问列表接口或直达路由
|
||||
- **THEN** 后端 MUST 返回 403,前端 MUST NOT 渲染入口
|
||||
- **AND** 失败文案 MUST 按后端返回原文透传
|
||||
|
||||
### Requirement: 创建弹窗配置
|
||||
|
||||
后台 MUST 支持创建 H5 运营弹窗配置(POST /api/admin/h5-popup-configurations),`title`/`content`/`pages`/`frequency`/`starts_at`/`ends_at` 必填;`priority`/`enabled`/`action_type`/`shop_ids`/`device_types`/`card_types` 可选,范围集合空数组 = 全量。
|
||||
|
||||
#### Scenario: 必填校验
|
||||
|
||||
- **GIVEN** 用户提交创建表单
|
||||
- **WHEN** title/content/pages/frequency/starts_at/ends_at 任一缺失
|
||||
- **THEN** 前端 MUST 拦截并提示必填,MUST NOT 发送请求
|
||||
|
||||
#### Scenario: 范围集合为空数组表示全量
|
||||
|
||||
- **GIVEN** 用户未选择店铺/设备类型/卡类型范围
|
||||
- **THEN** 请求体对应数组 MUST 传空数组,后端按全量投放处理
|
||||
|
||||
### Requirement: 弹窗配置详情
|
||||
|
||||
后台 MUST 提供弹窗配置详情(GET /api/admin/h5-popup-configurations/{id}),返回完整配置含 `version`、`frequency_text`、`enabled_text`、`creator`/`updater`。
|
||||
|
||||
#### Scenario: 查看详情
|
||||
|
||||
- **GIVEN** 用户点击列表行「详情」
|
||||
- **WHEN** 请求详情成功
|
||||
- **THEN** 前端 MUST 展示完整配置要素(含版本与启停/频率中文名)
|
||||
|
||||
### Requirement: 更新弹窗配置
|
||||
|
||||
后台 MUST 支持更新弹窗配置(PUT /api/admin/h5-popup-configurations/{id}),成功即版本 +1;新版本可向原命中客户按频率重新投放一次,旧版本已投放通知的内容与快照不被改写。除 `id` 外所有字段均可选,**不传保持原值**;`action_type` 传空字符串 = 清除受控动作;`shop_ids`/`device_types`/`card_types` 传空数组 = 改为全量;`pages` 传空数组非法(页面必选);`enabled` 不传保持原值(启停刷新最近更新时间);`ends_at` 不得早于 `starts_at`;`content` 1–2000 字符、`title` 1–100 字符、`priority` 0–1000000。
|
||||
|
||||
#### Scenario: 更新成功版本递增并重新投放
|
||||
|
||||
- **GIVEN** 用户保存编辑
|
||||
- **WHEN** 更新成功
|
||||
- **THEN** 前端 MUST 提示「每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次」并刷新列表
|
||||
- **AND** 列表/详情版本号较更新前 +1,旧版本已投放通知内容不被改写
|
||||
|
||||
#### Scenario: 范围清空表示改为全量
|
||||
|
||||
- **GIVEN** 编辑时清空某范围集合
|
||||
- **THEN** 提交对应数组 MUST 为空数组,后端按全量处理
|
||||
|
||||
#### Scenario: 受控动作清除
|
||||
|
||||
- **GIVEN** 编辑时把 action_type 从 package_purchase 切换为「无」
|
||||
- **THEN** 请求体 action_type MUST 传空字符串,后端清除受控动作
|
||||
|
||||
#### Scenario: 页面集合不得为空
|
||||
|
||||
- **GIVEN** 编辑时清空全部命中页面
|
||||
- **THEN** 前端 MUST 拦截提示命中页面必选,MUST NOT 传空 pages 数组
|
||||
|
||||
#### Scenario: 未传字段保持原值
|
||||
|
||||
- **GIVEN** 请求体省略某字段(如 enabled)
|
||||
- **THEN** 后端 MUST 保持原值;前端编辑表单整表提交,范围清空按空数组、动作清除按空字符串处理
|
||||
|
||||
### Requirement: 启用与停用弹窗配置
|
||||
|
||||
后台 MUST 支持启用/停用弹窗配置(POST /api/admin/h5-popup-configurations/{id}/enable、/{id}/disable,请求体 `{ id }`,返回更新后的配置),停用停止新投放且历史通知在展示期内仍可见,启用参与候选匹配;两者均刷新最近更新时间(影响同优先级排序)且不递增版本。
|
||||
|
||||
#### Scenario: 停用需二次确认
|
||||
|
||||
- **GIVEN** 用户点击某启用的配置「停用」
|
||||
- **WHEN** 二次确认提交
|
||||
- **THEN** 前端 MUST 调用 disable 并提示「停用后停止新投放,历史通知在展示期内仍可见」
|
||||
- **AND** 以返回的更新后配置刷新当前行(状态为停用、最近更新时间更新、版本不变)
|
||||
|
||||
#### Scenario: 启用
|
||||
|
||||
- **GIVEN** 用户点击某停用的配置「启用」
|
||||
- **THEN** 前端 MUST 调用 enable 并提示「启用后参与候选匹配」
|
||||
- **AND** 以返回的更新后配置刷新当前行(状态为启用、最近更新时间更新、版本不变)
|
||||
|
||||
### Requirement: 角色权限与交互约定
|
||||
|
||||
弹窗配置入口 MUST 仅对超级管理员与平台账号可见;前端权限码固定为 `h5_popup_configuration:list`(菜单/列表)、`:create`、`:update`、`:enable`、`:disable`;页面 MUST NOT 提供删除入口(文档无 DELETE 接口);停用不清数据,列表保留历史配置。
|
||||
|
||||
#### Scenario: 入口按角色隐藏
|
||||
|
||||
- **GIVEN** 代理/企业/个人账号登录
|
||||
- **WHEN** 系统渲染菜单
|
||||
- **THEN** MUST NOT 展示「H5运营弹窗配置」菜单项
|
||||
|
||||
#### Scenario: 按钮权限码控制
|
||||
|
||||
- **GIVEN** 账号具备列表权限但缺 enable/disable 权限
|
||||
- **THEN** 前端 MUST 只渲染具备权限的操作按钮
|
||||
|
||||
#### Scenario: 不提供删除入口
|
||||
|
||||
- **GIVEN** 用户查看列表或详情
|
||||
- **THEN** 页面 MUST NOT 出现删除操作,仅提供启用/停用
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
## 1. Contract Confirmation
|
||||
|
||||
- [x] 1.1 权限编码前端确定:`h5_popup_configuration:list` / `:create` / `:update` / `:enable` / `:disable`(以后端菜单配置为准)。
|
||||
- [x] 1.2 更新接口语义按文档:不传保持原值;`action_type` 空串 = 清除;范围空数组 = 改全量;`pages` 空数组非法;`enabled` 不传保持原值;`ends_at` 不得早于 `starts_at`;`content` 1–2000、`title` 1–100、`priority` 0–1000000;更新成功版本 +1 且旧版本通知不被改写。
|
||||
- [x] 1.3 `enable`/`disable` 请求体 `{ id }`,返回更新后的 DtoH5PopupConfigurationResponse。
|
||||
- [x] 1.4 列表 `page` 1–10000、`page_size` 1–100、`enabled` 筛选,按最近更新时间倒序分页。
|
||||
|
||||
## 2. API And Types
|
||||
|
||||
- [ ] 2.1 新增 `src/api/modules/h5PopupConfiguration.ts`:列表、创建、详情、更新、启用、停用 6 个接口。
|
||||
- [ ] 2.2 新增 `src/types/api/h5PopupConfiguration.ts`:请求/响应 DTO(含 pages/frequency/action_type/card_types 枚举、范围集合、version、creator/updater 等;更新语义按「不传保持原值/空数组=全量/空串=清除」注释)。
|
||||
- [ ] 2.3 在 `src/api/modules/index.ts`、`src/types/api/index.ts` 导出新模块/类型。
|
||||
- [ ] 2.4 对齐统一响应 `code/msg/timestamp/data` 与分页 `items/page/size/total`。
|
||||
|
||||
## 3. List Page
|
||||
|
||||
- [ ] 3.1 设置管理下新增「H5运营弹窗配置」菜单与路由(仅超管/平台可见,权限码 `h5_popup_configuration:list`)。
|
||||
- [ ] 3.2 列表:`page`/`page_size`/`enabled` 筛选,分页展示,按后端倒序。
|
||||
- [ ] 3.3 列:标题、命中页面、范围(店铺/设备类型/卡类型,空数组显示「全部」)、优先级、频率、启停状态、受控动作、生效起止、版本、创建/更新人与时间。
|
||||
- [ ] 3.4 行操作:详情、编辑、启用/停用(停用需二次确认);无删除入口。
|
||||
|
||||
## 4. Create / Edit / Detail
|
||||
|
||||
- [ ] 4.1 创建表单:`title`/`content`/`pages` 多选/`frequency`/起止时间必填校验(pages 非空);`priority`/`enabled`/`action_type`/范围可选。
|
||||
- [ ] 4.2 范围集合 `shop_ids`/`device_types`/`card_types` 空数组 = 全量;编辑读回空数组显示「全部」。
|
||||
- [ ] 4.3 编辑表单整表提交;保存成功提示「每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次」。
|
||||
- [ ] 4.4 编辑支持 `action_type` 清空(「无」→ 空字符串)与 `ends_at` 不早于 `starts_at` 的前端校验。
|
||||
- [ ] 4.5 详情(弹窗/抽屉)展示完整配置(含 version、frequency_text、enabled_text、creator/updater)。
|
||||
|
||||
## 5. Permissions And UX
|
||||
|
||||
- [ ] 5.1 按钮/入口按 `h5_popup_configuration:list/create/update/enable/disable` 接入 `usePermission`/`v-permission`,无权限不渲染。
|
||||
- [ ] 5.2 固定提示:停用「停用后停止新投放,历史通知在展示期内仍可见」;启用「启用后参与候选匹配」;403 文案原文透传。
|
||||
- [ ] 5.3 表单校验与请求失败提示稳定,不破坏列表渲染。
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- [ ] 6.1 `vue-tsc --noEmit` 与新增文件 eslint 通过。
|
||||
- [ ] 6.2 列表筛选/分页/倒序展示正确。
|
||||
- [ ] 6.3 创建必填校验、范围全量语义、编辑版本 +1 提示正确。
|
||||
- [ ] 6.4 启用/停用流程与提示正确;停用不清数据;启停不递增版本、刷新最近更新时间。
|
||||
- [ ] 6.5 代理/企业/个人看不到入口;直达路由 403 文案透传。
|
||||
- [ ] 6.6 联调:编辑整表提交后版本 +1、启停返回完整配置并刷新行;`ends_at` 早于 `starts_at` 被后端拒绝。
|
||||
@@ -1,51 +0,0 @@
|
||||
# Design: 套餐真流量预警前端对接
|
||||
|
||||
## 背景
|
||||
|
||||
后端已按 `docs/产品迭代8月份/套餐真流量预警_API_前端简版.md` 在测试环境上线 6 个接口(released)。前端需要新增规则配置与预警记录两个页面,并接入既有权限与导出任务体系。本设计记录关键决策。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 页面挂载位置:套餐管理分组
|
||||
|
||||
规则数据源是套餐商品(`package_id`),预警记录也按套餐筛选,因此两个页面都挂在既有 `/package-management` 路由分组下,与套餐列表、代理系列授权平级。
|
||||
|
||||
- 路由:`/package-management/traffic-alert-rules`、`/package-management/traffic-alerts`
|
||||
- 详情:`/package-management/traffic-alerts/detail/:id`(隐藏路由)
|
||||
|
||||
### 2. 导出走专用接口 + 专用弹窗,不复用 ExportTaskCreateDialog
|
||||
|
||||
既有 `ExportTaskCreateDialog` 调用通用 `POST /api/admin/export-tasks`,而真流量预警导出是专用端点 `POST /api/admin/package-traffic-alerts/export`,请求体为 `format` + 与列表一致的一组筛选参数。因此:
|
||||
|
||||
- 新建轻量弹窗(或扩展 `ExportTaskCreateDialog` 支持自定义 submit),展示“基于当前筛选条件全量导出,不仅导出当前分页数据”
|
||||
- 提交成功后提示“导出任务已创建”,并提供跳转既有导出任务列表页的入口,下载走既有能力
|
||||
- 导出任务列表若按场景过滤,需要新增场景配置;场景枚举值联调时与后端确认后落入 `EXPORT_TASK_SCENE_CONFIG`
|
||||
|
||||
### 3. 权限模型
|
||||
|
||||
- 遵循八月迭代约定:权限编码集中定义在 `AUGUST_PERMISSIONS.packageTrafficAlert`,页面/按钮通过 `v-permission` + `useAuth().hasAuth` 引用
|
||||
- 页面级:`rules_view` / `records_view` 控制菜单与按钮可见性(后端菜单权限同源)
|
||||
- 按钮级:规则创建/修改(含启停)、记录详情、导出分别独立编码
|
||||
- 403 策略:接口对无权限账号统一返回 403;列表接口 403 时提示“无权限访问”;详情越权按资源不可见处理(复用现有 404 类提示文案),不暴露资源存在性
|
||||
|
||||
### 4. 阈值输入校验
|
||||
|
||||
- 前端 `ElInputNumber`:`min=1`、`max=100`、`precision=2`
|
||||
- 创建时 `threshold_percent` 必填;修改时三字段均可选(后端按传入字段更新)
|
||||
- `remark` 创建/修改均限制 500 字符(`maxlength` + 计数器)
|
||||
|
||||
### 5. 快照与归属变化展示
|
||||
|
||||
- 列表与详情的业务字段均为触发时快照,仅 `business_user_group_names` 为当前值
|
||||
- 详情中 `shop_changed_since_trigger` / `owner_changed_since_trigger` 为 `true` 时,用 `ElAlert`(info)提示归属已变化,并展示当前店铺/业务员与快照值
|
||||
- 空值约定:无店铺/业务员时字段可能为 `null` 或空数组,统一渲染 `-`(业务员数组 join 展示,空数组显示 `-`)
|
||||
|
||||
### 6. 规则列表的 real_data_mb 展示
|
||||
|
||||
- `real_data_mb` 为套餐商品当前真流量额度,仅用于配置校验展示(如“按 80% 约对应 x GB”),不作为预警分母
|
||||
- 页面以 GB 展示(`/1024`,保留两位小数),避免 MB 数字过长
|
||||
|
||||
## 风险
|
||||
|
||||
- 记录列表/详情接口字段名未完整给出,联调时以测试环境实际返回为准,类型定义需保留一定弹性(可选字段)
|
||||
- 导出任务场景值未给出,若后端未登记场景枚举,导出任务列表页的场景筛选需兼容新值
|
||||
@@ -1,93 +0,0 @@
|
||||
# Change: 新增套餐真流量预警(规则配置 + 预警记录)
|
||||
|
||||
## Why
|
||||
|
||||
根据 `docs/产品迭代8月份/套餐真流量预警_API_前端简版.md`,后端已在测试环境(`https://cmp-api.boss160.cn`)实现套餐真流量预警能力:套餐商品的真流量使用量达到配置阈值后生成预警记录,并通知对应业务员。共 6 个接口,仅超级管理员/平台账号可访问,其他账号返回 403,通用响应为 `code / data / msg / timestamp`。
|
||||
|
||||
当前前端(套餐管理模块)缺少:
|
||||
|
||||
1. 真流量预警规则配置入口:查看、创建、修改规则(阈值 1~100 允许两位小数、启停、备注),以及同一套餐商品仅一条规则的限制提示。
|
||||
2. 预警记录查看:支持套餐、店铺、业务员、资产类型、资产关键词、阈值、触发时间、通知状态 8 项筛选的列表,触发时快照详情(含归属是否变化提示),以及复用既有异步导出任务体系的记录导出。
|
||||
|
||||
此变更完成上述 6 个接口的前端对接。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 1. API 层
|
||||
|
||||
- **新增**: `src/api/modules/packageTrafficAlert.ts`(`PackageTrafficAlertService`,继承 BaseService)
|
||||
- `getAlertRules` — `GET /api/admin/package-traffic-alert-rules`(`package_id`、`enabled`、`page`、`page_size`)
|
||||
- `createAlertRule` — `POST /api/admin/package-traffic-alert-rules`(`package_id`、`threshold_percent`、`enabled`、`remark`)
|
||||
- `updateAlertRule` — `PUT /api/admin/package-traffic-alert-rules/{id}`(`threshold_percent`、`enabled`、`remark`,均为可选)
|
||||
- `getAlertRecords` — `GET /api/admin/package-traffic-alerts`(8 项筛选 + 分页)
|
||||
- `getAlertRecordDetail` — `GET /api/admin/package-traffic-alerts/{id}`
|
||||
- `exportAlertRecords` — `POST /api/admin/package-traffic-alerts/export`(`format` + 与列表一致的筛选参数)
|
||||
- **修改**: `src/api/modules/index.ts` — 导出 `PackageTrafficAlertService`
|
||||
|
||||
### 2. 类型定义
|
||||
|
||||
- **新增**: `src/types/api/packageTrafficAlert.ts` — 规则/记录/导出请求响应类型,字段与接口文档保持 snake_case
|
||||
- **修改**: `src/types/api/index.ts` — 导出新类型
|
||||
|
||||
### 3. 常量与权限
|
||||
|
||||
- **修改**: `src/config/constants/augustIteration.ts` — `AUGUST_PERMISSIONS` 新增 `packageTrafficAlert` 权限组
|
||||
- `trafficAlertRules: 'package_traffic_alert:rules_view'`
|
||||
- `trafficAlertRuleCreate: 'package_traffic_alert:rule_create'`
|
||||
- `trafficAlertRuleUpdate: 'package_traffic_alert:rule_update'`
|
||||
- `trafficAlertRecords: 'package_traffic_alert:records_view'`
|
||||
- `trafficAlertRecordDetail: 'package_traffic_alert:record_detail'`
|
||||
- `trafficAlertExport: 'package_traffic_alert:export'`
|
||||
- **新增/修改**: 通知状态枚举常量(1 已通知 / 2 待投递 / 3 投递失败 / 4 未通知(接收人已失效)/ 5 未通知(无有效业务员))及对应 tag 类型映射
|
||||
- 若既有导出任务列表需要区分本场景,补充对应导出场景配置(场景值联调时与后端确认)
|
||||
|
||||
### 4. 页面
|
||||
|
||||
**预警规则页** `src/views/package-management/traffic-alert-rules/index.vue`
|
||||
|
||||
- 列表:套餐名称、当前真流量额度(`real_data_mb`,按 GB 展示,仅作参考、不作为预警分母)、阈值百分比、启用状态、备注、更新时间
|
||||
- 筛选:套餐(`package_id`)、启用状态(`enabled`,不传查全部);分页默认 20、最大 100
|
||||
- 新增/编辑弹窗:套餐选择器、阈值(1~100,两位小数)、启用开关、备注(最多 500 字符)
|
||||
- 约束:同一套餐商品最多一条规则,后端拒绝重复创建时前端展示错误信息
|
||||
- 修改规则不影响既有预警快照;停用后扫描不再创建新预警(页面文案说明)
|
||||
|
||||
**预警记录页** `src/views/package-management/traffic-alerts/index.vue`
|
||||
|
||||
- 筛选:套餐、店铺、业务员、资产类型、资产关键词、阈值(两位小数)、触发时间范围(RFC3339)、通知状态
|
||||
- 列表:除 `business_user_group_names` 外均为触发时快照;`notification_status` 按枚举展示
|
||||
- 详情:展示快照字段;`shop_changed_since_trigger` / `owner_changed_since_trigger` 为真时提示“触发后店铺/业务员归属已变化”;`null` 或空数组统一显示 `-`
|
||||
- 导出:弹窗选择格式(xlsx/csv),携带当前筛选条件调用专用导出接口创建异步任务;创建成功后提示到既有“导出任务列表”下载
|
||||
- 越权查询详情:统一按资源不可见处理(与不存在资源一致的提示)
|
||||
|
||||
### 5. 路由与菜单
|
||||
|
||||
- **修改**: `src/router/routesAlias.ts` — 新增 `TrafficAlertRules`、`TrafficAlerts`、`TrafficAlertDetail` 别名
|
||||
- **修改**: `src/router/routes/asyncRoutes.ts` — 套餐管理分组下新增两个子路由(记录详情用隐藏路由)
|
||||
- **修改**: `src/locales/langs/zh.json` / `src/locales/langs/en.json` — `menus.packageManagement` 新增 `trafficAlertRules`、`trafficAlerts`、`trafficAlertDetail`
|
||||
|
||||
## Impact
|
||||
|
||||
### 受影响的规范
|
||||
|
||||
- `package-traffic-alert` — 新增能力
|
||||
|
||||
### 受影响的代码
|
||||
|
||||
- `src/api/modules/packageTrafficAlert.ts`(新增)、`src/api/modules/index.ts`
|
||||
- `src/types/api/packageTrafficAlert.ts`(新增)、`src/types/api/index.ts`
|
||||
- `src/config/constants/augustIteration.ts`、导出场景/通知状态相关常量
|
||||
- `src/views/package-management/traffic-alert-rules/index.vue`(新增)
|
||||
- `src/views/package-management/traffic-alerts/index.vue`、`detail.vue`(新增)
|
||||
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`
|
||||
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||
|
||||
### 依赖关系
|
||||
|
||||
- 依赖后端 6 个接口在测试环境可用(文档标记 released)
|
||||
- 复用既有基础设施:`BaseService`/request 封装、`useAuth` + `v-permission`、`PackageSelector`、店铺/业务员选择组件、既有导出任务列表下载能力
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 文档声明 6 个接口,但简版仅详细给出 4 个(规则列表/创建/修改 + 记录导出);预警记录列表与详情两个接口以“前端注意事项”的筛选项、快照字段与导出筛选字段为准,字段名在联调时与测试环境核对
|
||||
- 全部接口对非超级管理员/平台账号返回 403:菜单可见性由后端菜单权限控制,前端对 403 做友好提示,详情越权按资源不可见处理
|
||||
- 无破坏性变更:新增页面、API 模块、类型、权限编码均为增量
|
||||
@@ -1,132 +0,0 @@
|
||||
# Package Traffic Alert Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 套餐真流量预警规则列表查询
|
||||
|
||||
系统 SHALL 提供套餐真流量预警规则列表查询页面,展示规则与套餐商品关联信息,支持按套餐商品与启用状态筛选和分页。
|
||||
|
||||
#### Scenario: 查询全部规则
|
||||
|
||||
- **WHEN** 具备权限的用户(超级管理员/平台账号)访问预警规则页面
|
||||
- **THEN** 系统调用 `GET /api/admin/package-traffic-alert-rules` 加载规则列表
|
||||
- **AND** 列表展示套餐名称、当前真流量额度(`real_data_mb`,按 GB 展示)、阈值百分比、启用状态(含中文状态名)、备注、最近更新时间
|
||||
- **AND** 分页默认每页 20 条,最大 100 条
|
||||
|
||||
#### Scenario: 按条件筛选
|
||||
|
||||
- **WHEN** 用户选择套餐商品或启用状态进行筛选
|
||||
- **THEN** 系统携带 `package_id` / `enabled` 查询参数重新加载列表
|
||||
- **AND** 不传启用状态时查询全部规则
|
||||
|
||||
#### Scenario: 无权限访问
|
||||
|
||||
- **WHEN** 非超级管理员/平台账号调用规则接口
|
||||
- **THEN** 接口返回 403
|
||||
- **AND** 页面展示无权限访问提示,不展示业务数据
|
||||
|
||||
### Requirement: 创建套餐真流量预警规则
|
||||
|
||||
系统 SHALL 允许管理员为套餐商品创建真流量预警规则,并执行阈值与备注校验;同一套餐商品最多一条规则。
|
||||
|
||||
#### Scenario: 成功创建规则
|
||||
|
||||
- **WHEN** 用户选择套餐商品,填写阈值百分比(1~100,允许两位小数)
|
||||
- **AND** 设置启用状态与备注(最多 500 字符)后提交
|
||||
- **THEN** 系统调用 `POST /api/admin/package-traffic-alert-rules` 创建规则
|
||||
- **AND** 成功后刷新列表并展示新规则详情(含套餐名称、当前真流量额度、更新时间)
|
||||
|
||||
#### Scenario: 阈值校验
|
||||
|
||||
- **WHEN** 用户填写的阈值小于 1、大于 100 或超过两位小数
|
||||
- **THEN** 前端阻止提交并展示校验错误提示
|
||||
|
||||
#### Scenario: 重复规则
|
||||
|
||||
- **WHEN** 用户为已存在规则的套餐商品再次创建规则
|
||||
- **THEN** 后端拒绝创建
|
||||
- **AND** 前端展示后端返回的错误信息,列表保持原状
|
||||
|
||||
### Requirement: 修改套餐真流量预警规则
|
||||
|
||||
系统 SHALL 允许管理员修改规则的阈值、启用状态与备注;修改不影响既有预警快照,停用后扫描不再创建新预警。
|
||||
|
||||
#### Scenario: 修改规则字段
|
||||
|
||||
- **WHEN** 用户打开编辑弹窗并修改阈值、启用状态或备注后提交
|
||||
- **THEN** 系统调用 `PUT /api/admin/package-traffic-alert-rules/{id}`,仅提交修改的字段
|
||||
- **AND** 成功后刷新列表展示最新规则
|
||||
|
||||
#### Scenario: 停用规则
|
||||
|
||||
- **WHEN** 用户关闭规则的启用开关
|
||||
- **THEN** 系统调用修改接口仅提交 `enabled=false`
|
||||
- **AND** 页面说明停用后不再产生新预警,既有预警记录保留
|
||||
|
||||
#### Scenario: 规则不存在或无权限
|
||||
|
||||
- **WHEN** 修改的规则 ID 不存在或用户无权限
|
||||
- **THEN** 系统按接口错误处理并展示对应提示
|
||||
|
||||
### Requirement: 套餐真流量预警记录列表查询
|
||||
|
||||
系统 SHALL 提供预警记录列表页面,支持套餐、店铺、业务员、资产类型、资产关键词、阈值、触发时间范围、通知状态 8 项筛选与分页;列表数据除 `business_user_group_names` 外均为触发时快照。
|
||||
|
||||
#### Scenario: 查询预警记录
|
||||
|
||||
- **WHEN** 具备权限的用户访问预警记录页面
|
||||
- **THEN** 系统调用 `GET /api/admin/package-traffic-alerts` 加载记录列表
|
||||
- **AND** 列表展示触发时间、资产信息、套餐、阈值快照、店铺、业务员与通知状态
|
||||
|
||||
#### Scenario: 组合筛选
|
||||
|
||||
- **WHEN** 用户组合使用任意筛选条件(含两位小数阈值与 RFC3339 触发时间范围)
|
||||
- **THEN** 系统携带对应查询参数请求列表,返回满足全部条件的记录
|
||||
|
||||
#### Scenario: 通知状态展示
|
||||
|
||||
- **WHEN** 记录包含 `notification_status`
|
||||
- **THEN** 系统按枚举展示:1 已通知 / 2 待投递 / 3 投递失败 / 4 未通知(接收人已失效)/ 5 未通知(无有效业务员)
|
||||
|
||||
#### Scenario: 空值展示
|
||||
|
||||
- **WHEN** 记录的店铺或业务员字段为 `null` 或空数组
|
||||
- **THEN** 系统统一展示 `-`
|
||||
- **AND** `business_user_group_names` 为当前归属值,其余字段保持触发时快照
|
||||
|
||||
### Requirement: 查看预警记录详情
|
||||
|
||||
系统 SHALL 允许管理员查看单条预警记录的触发时快照详情,并标识触发后店铺/业务员归属是否发生变化;越权查询按资源不可见处理。
|
||||
|
||||
#### Scenario: 查看存在的记录
|
||||
|
||||
- **WHEN** 用户点击记录打开详情
|
||||
- **THEN** 系统调用 `GET /api/admin/package-traffic-alerts/{id}` 展示触发时快照字段
|
||||
|
||||
#### Scenario: 归属变化提示
|
||||
|
||||
- **WHEN** 详情返回 `shop_changed_since_trigger` 或 `owner_changed_since_trigger` 为 true
|
||||
- **THEN** 系统提示触发后店铺/业务员归属已变化
|
||||
- **AND** 展示当前归属与触发时快照值的差异
|
||||
|
||||
#### Scenario: 越权或不存在
|
||||
|
||||
- **WHEN** 用户无权限查看该记录或记录不存在
|
||||
- **THEN** 系统统一按资源不可见处理
|
||||
- **AND** 展示与记录不存在一致的提示,不暴露资源存在性
|
||||
|
||||
### Requirement: 导出套餐真流量达量预警
|
||||
|
||||
系统 SHALL 允许管理员按当前筛选条件创建预警记录异步导出任务;导出接口仅创建任务,文件通过既有导出任务列表下载。
|
||||
|
||||
#### Scenario: 创建导出任务
|
||||
|
||||
- **WHEN** 用户在预警记录页点击导出并选择格式(xlsx/csv)
|
||||
- **THEN** 系统携带 `format` 与当前筛选条件调用 `POST /api/admin/package-traffic-alerts/export`
|
||||
- **AND** 成功后展示任务信息(`task_id`、`task_no`、状态)并引导用户到既有导出任务列表下载
|
||||
|
||||
#### Scenario: 导出范围说明
|
||||
|
||||
- **WHEN** 导出弹窗打开
|
||||
- **THEN** 系统说明导出基于当前筛选条件全量导出,不仅导出当前分页数据
|
||||
- **AND** 任务创建时冻结筛选条件、时间范围与可见资产范围,归属列按执行时当前归属补充
|
||||
@@ -1,40 +0,0 @@
|
||||
# Tasks: 套餐真流量预警前端对接
|
||||
|
||||
## 1. API 与类型
|
||||
|
||||
- [x] 1.1 新增 `src/types/api/packageTrafficAlert.ts`:规则列表/规则项、创建/修改参数、预警记录列表/记录项、详情、导出请求/响应类型(snake_case,对齐接口文档;记录字段按前端注意事项预留可选)
|
||||
- [x] 1.2 `src/types/api/index.ts` 导出新类型
|
||||
- [x] 1.3 新增 `src/api/modules/packageTrafficAlert.ts`:`getAlertRules` / `createAlertRule` / `updateAlertRule` / `getAlertRecords` / `getAlertRecordDetail` / `exportAlertRecords` 6 个方法
|
||||
- [x] 1.4 `src/api/modules/index.ts` 导出 `PackageTrafficAlertService`
|
||||
|
||||
## 2. 常量与权限
|
||||
|
||||
- [x] 2.1 `src/config/constants/augustIteration.ts`:`AUGUST_PERMISSIONS` 新增 `packageTrafficAlert` 权限组(rules_view / rule_create / rule_update / records_view / record_detail / export)
|
||||
- [x] 2.2 新增通知状态常量:枚举 1-5 文案(已通知/待投递/投递失败/未通知(接收人已失效)/未通知(无有效业务员))及 tag 类型映射
|
||||
- [x] 2.3 若导出任务列表需要场景区分,补充真流量预警导出场景配置(场景值与后端确认后更新 `EXPORT_TASK_SCENE_CONFIG` 及 `getExportTaskSceneName`)
|
||||
|
||||
## 3. 预警规则页
|
||||
|
||||
- [x] 3.1 新增 `src/views/package-management/traffic-alert-rules/index.vue`:筛选栏(套餐选择器、启用状态)、列表(套餐名称、真流量额度 GB、阈值、启用状态、备注、更新时间)、分页(默认 20/最大 100)
|
||||
- [x] 3.2 新增/编辑弹窗:套餐选择器(编辑时不可改)、阈值 ElInputNumber(1-100,两位小数,创建必填)、启用开关、备注(≤500 字符带计数);创建成功提示同一套餐最多一条规则
|
||||
- [x] 3.3 启用状态行内开关(调用修改接口,仅传 enabled),文案说明“停用后不再产生新预警,既有预警保留”
|
||||
- [x] 3.4 403/错误处理:无权限提示;重复创建等后端错误展示 msg
|
||||
|
||||
## 4. 预警记录页
|
||||
|
||||
- [x] 4.1 新增 `src/views/package-management/traffic-alerts/index.vue`:筛选栏(套餐、店铺、业务员、资产类型、资产关键词、阈值、触发时间范围、通知状态)、列表(触发时快照字段 + `business_user_group_names` 当前值 + 通知状态 tag)、分页
|
||||
- [x] 4.2 新增 `detail.vue`(或详情抽屉):快照字段展示、`shop_changed_since_trigger` / `owner_changed_since_trigger` 变化提示、空值/空数组显示 `-`、越权按资源不可见处理
|
||||
- [x] 4.3 导出弹窗:格式选择(xlsx/csv)+“基于当前筛选条件全量导出”说明,调用 `exportAlertRecords` 携带当前筛选,成功后提示并引导跳转导出任务列表下载
|
||||
|
||||
## 5. 路由、菜单与国际化
|
||||
|
||||
- [x] 5.1 `src/router/routesAlias.ts`:新增 `TrafficAlertRules` / `TrafficAlerts` / `TrafficAlertDetail`
|
||||
- [x] 5.2 `src/router/routes/asyncRoutes.ts`:套餐管理分组下新增规则页、记录页(keepAlive)与隐藏详情路由
|
||||
- [x] 5.3 `src/locales/langs/zh.json` / `en.json`:`menus.packageManagement` 新增 `trafficAlertRules` / `trafficAlerts` / `trafficAlertDetail`
|
||||
|
||||
## 6. 联调与验收
|
||||
|
||||
- [ ] 6.1 测试环境(`https://cmp-api.boss160.cn`)联调 6 个接口,核对记录列表/详情实际字段名并修正类型
|
||||
- [ ] 6.2 权限验证:超级管理员/平台账号正常访问;其他账号 403 提示符合预期;详情越权按资源不可见
|
||||
- [ ] 6.3 边界验证:阈值 0.99/1/100/100.01 校验、备注 500 字符、同套餐重复创建、空店铺/业务员展示 `-`、导出任务创建后可在导出任务列表下载
|
||||
- [x] 6.4 ESLint / Stylelint / `vue-tsc` 类型检查通过
|
||||
@@ -1,64 +0,0 @@
|
||||
# 支付商户与商户池管理设计
|
||||
|
||||
## Context
|
||||
|
||||
接口文档把能力拆成支付商户、商户池和微信授权配置三部分。前端必须在同一个平台专属入口内完成管理,同时避免把商户凭证和内部路由细节带入页面状态。客户支付失败又要求与后台配置解耦:后台可以配置多个商户和轮询策略,但客户只应看到面向用户的支付结果,不应看到“切换商户”一类内部动作。
|
||||
|
||||
## Goals
|
||||
|
||||
- 通过一个后台入口管理支付商户、商户池和微信授权配置。
|
||||
- 仅允许超级管理员和平台用户访问。
|
||||
- 支持商户、商户池、授权配置的启停状态管理。
|
||||
- 支持商户池成员拖拽排序、策略选择和阈值配置。
|
||||
- 保证支付凭证只写、不回显、不持久化。
|
||||
- 统一“暂无可用商户”和普通支付失败的用户提示。
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 前端不实现商户路由算法。
|
||||
- 前端不保存、展示或恢复支付凭证明文。
|
||||
- 不给客户支付端展示商户池内部配置。
|
||||
- 不把“切换商户重试”作为用户可操作流程。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 单一管理入口与三个子页签
|
||||
|
||||
新增 `/settings/payment-merchant-pools`,页面内使用“支付商户”“商户池”“微信授权配置”三个页签。这样与接口文档的结构一致,也便于统一权限检查和凭证清理策略。
|
||||
|
||||
替代方案是为三部分分别增加菜单项;该方案会让权限、路由和状态管理重复,暂不采用。
|
||||
|
||||
### 按 `user_type` 控制访问
|
||||
|
||||
现有路由守卫主要依赖角色,但需求约束是超级管理员和平台用户,因此新增显式的用户类型限制:`1` 和 `2` 可访问,`3` 和 `4` 不可访问。菜单隐藏与直接 URL 访问必须使用同一判断,避免仅做 UI 隐藏。
|
||||
|
||||
### 凭证只写且只存在于内存
|
||||
|
||||
商户 `credentials` 和微信授权敏感字段只允许在创建或显式更换时输入。读取响应不得回填这些字段;页面模型只在当前弹层或表单生命周期内保存输入值,提交、取消、关闭或卸载时清理。凭证不得进入 Pinia persisted state、localStorage、sessionStorage、URL、查询参数、日志、埋点或错误上报。
|
||||
|
||||
详情页不回显凭证内容,只显示“已配置/未配置”和 `credential_version`。如果后端读取接口意外返回敏感字段,前端适配层必须丢弃,而不是仅依赖模板隐藏。
|
||||
|
||||
### 成员数组顺序就是轮询顺序
|
||||
|
||||
商户池编辑使用拖拽排序。提交时直接把当前排序后的商户 ID 数组写入 `member_ids`,不新增独立的 `sort` 字段,也不在前端重新排序。成员选择默认限制为与商户池 `payment_method` 相同的商户,并禁止重复 ID。
|
||||
|
||||
### 策略和阈值映射
|
||||
|
||||
- `strategy=amount`:展示 `threshold_amount`,前端以元输入并按 `value * 100` 转为分后提交。
|
||||
- `strategy=count`:展示 `threshold_count`,只接受正整数。
|
||||
- `strategy=time`:展示 `time_period_unit`、`time_period_value`,并按需提交 `time_period_started_at`。
|
||||
- `statistic_cycle`:支持 `round`、`day`、`month`;只在接口允许的金额或笔数策略下提交有效值。
|
||||
- `routing_epoch`:仅展示后端返回的当前路由统计世代,前端不编辑。
|
||||
|
||||
### 无可用商户使用稳定错误码
|
||||
|
||||
接口简版没有给出支付失败错误码,实施前必须与后端确认稳定的机器可读值。前端只按错误码映射,不按 `msg` 文本判断。无论后端使用何种最终编码,命中该语义时客户界面都只显示“暂无可用商户”。
|
||||
|
||||
普通支付失败统一使用“支付失败,请重新发起支付”。前端不自动切换商户,也不重放同一支付请求;用户再次操作时按支付接口的新请求语义重新发起。
|
||||
|
||||
## Risks and Trade-offs
|
||||
|
||||
- 接口文档未定义 `credentials` 的字段结构。实现时需要后端补充各 `provider_type` 的写入 schema,或继续保持单个只写对象,但不得把结构暴露为可回显配置。
|
||||
- 若后端读取接口未脱敏,前端仍然能够防御性丢弃,但服务端响应、网关日志和网络抓包仍可能泄露凭证;该风险必须由后端脱敏共同控制。
|
||||
- 客户支付端若不在当前仓库,文案和错误码任务需要跨仓库联调;本提案负责固化契约,不能仅通过后台页面上线完成验收。
|
||||
- 金额阈值若直接按分展示会降低可读性,因此采用元输入、分传输;需要测试防止小数点精度和空值转换错误。
|
||||
@@ -1,54 +0,0 @@
|
||||
# Change: 新增支付商户与商户池管理
|
||||
|
||||
## Why
|
||||
|
||||
`docs/产品迭代8月份/支付商户API简版.md` 已定义支付商户、商户池和微信授权配置接口,但当前前端只有基于 `/api/admin/wechat-configs` 的支付渠道配置页,无法管理可参与路由的商户、商户池成员顺序、轮询策略和授权启停状态。
|
||||
|
||||
同时,后台页面可能读取到 `credentials`,客户支付失败时也容易暴露内部商户切换逻辑。本提案需要把平台专属访问、凭证最小暴露、商户池配置和客户侧失败反馈固化为可验收的 OpenSpec 契约。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 `payment-merchant-pool-management` capability,覆盖:
|
||||
- 支付商户查询、创建、详情、按需更新和删除。
|
||||
- 商户池查询、创建、详情、更新、启用和停用。
|
||||
- 微信授权配置读取、保存及启停状态。
|
||||
- 商户池成员排序、轮询策略、统计周期和金额/笔数/时间阈值配置。
|
||||
- 新增 `payment-checkout-feedback` capability,覆盖:
|
||||
- “暂无可用商户”唯一明确提示。
|
||||
- 支付失败不暴露商户切换逻辑,不自动切换商户重试。
|
||||
- 用户重新发起一笔支付时使用新的支付请求。
|
||||
- 新增后台“商户池管理”入口,仅超级管理员和平台用户(`user_type` 为 `1` 或 `2`)可见、可访问。
|
||||
- 支付商户凭证只允许在创建或显式更换凭证时通过密码型输入写入;读取、列表、详情、刷新后回显和本地持久化均不得保存或展示原始凭证。
|
||||
- 微信授权配置中的 AppSecret、Token、AES Key 等敏感字段遵循同样的只写和缓存隔离规则。
|
||||
- 该提案只定义契约和实施任务,不执行代码实现;提案获批后再进入实现阶段。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `payment-merchant-pool-management`
|
||||
- `payment-checkout-feedback`
|
||||
- Affected code:
|
||||
- `src/types/api/paymentMerchantPools.ts`(新增)
|
||||
- `src/api/modules/paymentMerchantPools.ts`(新增)
|
||||
- `src/api/modules/index.ts`
|
||||
- `src/types/api/index.ts`
|
||||
- `src/router/routesAlias.ts`
|
||||
- `src/router/routes/asyncRoutes.ts`
|
||||
- `src/router/guards/permission.ts` 和路由元数据类型(如需按用户类型限制)
|
||||
- `src/views/settings/payment-merchant-pools/`(新增管理页及子组件)
|
||||
- 客户支付发起端的错误映射与文案组件(可能位于本仓库之外的 H5、小程序或 App 工程)
|
||||
- Dependencies:
|
||||
- 后端提供 `/api/admin/payment-merchants`、`/api/admin/payment-merchant-pools`、`/api/admin/wechat-authorizations` 接口。
|
||||
- 后端为客户支付失败提供稳定的“无可用商户”机器可读错误码,前端不得依赖中文消息判断。
|
||||
- 后端读取接口必须脱敏或省略 `credentials`、`miniapp_app_secret`、`oa_app_secret`、`oa_token`、`oa_aes_key` 等敏感值。
|
||||
- Compatibility:
|
||||
- 不复用或重命名现有 `/api/admin/wechat-configs` 渠道配置能力。
|
||||
- 不向代理、企业客户或普通后台用户暴露商户池入口。
|
||||
- 商户池内部路由行为不改变现有订单、充值或其他支付接口的请求结构。
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 不在前端实现商户选择算法或自行决定切换逻辑;实际路由由后端根据商户池策略执行。
|
||||
- 不新增支付渠道、支付 SDK、退款或对账能力。
|
||||
- 不提供凭证查看、复制、下载或历史明文回显能力。
|
||||
- 不在客户支付端展示商户 ID、商户池、轮询策略、阈值或路由世代。
|
||||
@@ -1,39 +0,0 @@
|
||||
# Payment Checkout Feedback Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 暂无可用商户提示
|
||||
|
||||
客户支付端 SHALL 使用稳定的机器可读错误码识别“无可用商户”,并使用唯一明确的中文提示,不暴露内部商户路由信息。
|
||||
|
||||
#### Scenario: 识别无可用商户错误
|
||||
- **GIVEN** 后端在支付发起响应中返回已确认的“无可用商户”稳定错误码
|
||||
- **WHEN** 客户点击支付并收到该错误
|
||||
- **THEN** 页面 MUST 显示“暂无可用商户”
|
||||
- **AND** 前端 MUST NOT 通过匹配中文 `msg` 或其他可变文案判断该错误
|
||||
|
||||
#### Scenario: 无可用商户时不展示内部信息
|
||||
- **WHEN** 页面展示“暂无可用商户”
|
||||
- **THEN** 页面 MUST NOT 展示商户 ID、商户名称、商户池名称、成员列表、轮询策略、阈值、`routing_epoch`、凭证或凭证版本
|
||||
|
||||
### Requirement: 客户支付失败反馈
|
||||
|
||||
客户支付端 SHALL 对普通支付失败使用面向用户的统一提示,并禁止暴露商户切换或自动重试的内部处理。
|
||||
|
||||
#### Scenario: 普通支付失败提示重新发起
|
||||
- **GIVEN** 客户支付请求因非“无可用商户”原因失败
|
||||
- **WHEN** 页面展示失败结果
|
||||
- **THEN** 页面 MUST 显示“支付失败,请重新发起支付”
|
||||
- **AND** 前端 MUST NOT 自动重放同一支付请求
|
||||
|
||||
#### Scenario: 不提示切换商户重试
|
||||
- **WHEN** 任意客户支付失败
|
||||
- **THEN** 页面 MUST NOT 显示“切换商户重试”或任何等价文案
|
||||
- **AND** 页面 MUST NOT 提供切换商户的按钮、入口或操作提示
|
||||
- **AND** 页面 MUST NOT 暴露后端是否尝试过多个商户
|
||||
|
||||
#### Scenario: 用户主动重新发起支付
|
||||
- **GIVEN** 客户已收到支付失败提示
|
||||
- **WHEN** 客户主动再次发起支付
|
||||
- **THEN** 前端 MUST 按支付接口约定创建一笔新的支付请求
|
||||
- **AND** 前端 MUST NOT 复用失败支付请求的商户选择或前端临时支付状态
|
||||
@@ -1,170 +0,0 @@
|
||||
# Payment Merchant Pool Management Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 平台专属的商户池管理入口
|
||||
|
||||
系统 SHALL 仅向超级管理员和平台用户开放支付商户、商户池及微信授权配置的管理入口和操作。
|
||||
|
||||
#### Scenario: 超级管理员可见并访问
|
||||
- **GIVEN** 当前登录账号的 `user_type` 为 `1`
|
||||
- **WHEN** 用户加载设置菜单或访问 `/settings/payment-merchant-pools`
|
||||
- **THEN** 系统 MUST 展示商户池管理入口并允许进入页面
|
||||
|
||||
#### Scenario: 平台用户可见并访问
|
||||
- **GIVEN** 当前登录账号的 `user_type` 为 `2`
|
||||
- **WHEN** 用户加载设置菜单或访问 `/settings/payment-merchant-pools`
|
||||
- **THEN** 系统 MUST 展示商户池管理入口并允许进入页面
|
||||
|
||||
#### Scenario: 非平台账号被拒绝
|
||||
- **GIVEN** 当前登录账号的 `user_type` 为 `3` 或 `4`
|
||||
- **WHEN** 用户加载设置菜单或直接输入 `/settings/payment-merchant-pools`
|
||||
- **THEN** 系统 MUST NOT 展示商户池管理入口
|
||||
- **AND** 系统 MUST 拒绝直接访问该页面
|
||||
- **AND** 系统 MUST NOT 返回商户、商户池或授权配置数据
|
||||
|
||||
### Requirement: 支付商户管理接口
|
||||
|
||||
系统 SHALL 提供支付商户的分页查询、创建、详情、按需更新和删除接口。
|
||||
|
||||
#### Scenario: 查询和筛选支付商户
|
||||
- **WHEN** 管理员请求 `GET /api/admin/payment-merchants`
|
||||
- **THEN** 系统 MUST 支持 `page`、`page_size`、`payment_method` 和 `enabled` 查询参数
|
||||
- **AND** `payment_method` MUST 支持 `wechat` 和 `alipay`
|
||||
- **AND** 响应中的每个商户 MUST 包含 `id`、`name`、`payment_method`、`provider_type`、`merchant_identity`、`enabled`、`remark`、`credential_version`、`created_at` 和 `updated_at`
|
||||
|
||||
#### Scenario: 创建支付商户
|
||||
- **WHEN** 管理员向 `POST /api/admin/payment-merchants` 提交 `name`、`payment_method`、`provider_type`、`merchant_identity`、`credentials`、`enabled` 和 `remark`
|
||||
- **THEN** 系统 MUST 创建支付商户并返回新商户记录
|
||||
- **AND** `provider_type` 为 `wechat` 时 MUST 只接受 `wechat`、`wechat_v2` 或 `fuiou`
|
||||
- **AND** `provider_type` 为 `alipay` 时 MUST 只接受 `alipay`
|
||||
|
||||
#### Scenario: 查询支付商户详情
|
||||
- **WHEN** 管理员请求 `GET /api/admin/payment-merchants/{id}`
|
||||
- **THEN** 系统 MUST 返回指定商户的详情
|
||||
- **AND** 响应 MUST NOT 包含任何支付凭证明文
|
||||
|
||||
#### Scenario: 按需更新和切换商户状态
|
||||
- **WHEN** 管理员请求 `PUT /api/admin/payment-merchants/{id}` 并只提交 `enabled`
|
||||
- **THEN** 系统 MUST 只更新该商户的 `enabled` 字段
|
||||
- **AND** 系统 MUST 保留未提交字段的原值
|
||||
|
||||
#### Scenario: 确认后删除支付商户
|
||||
- **WHEN** 管理员请求 `DELETE /api/admin/payment-merchants/{id}` 并提交 `confirm=true`
|
||||
- **THEN** 系统 MUST 删除目标商户
|
||||
- **AND** 前端 MUST 在发送请求前展示二次确认
|
||||
|
||||
### Requirement: 支付商户列表与启停交互
|
||||
|
||||
后台商户管理页 SHALL 展示支付商户状态,并允许有权限的管理员按接口契约切换启用状态。
|
||||
|
||||
#### Scenario: 列表不展示支付凭证
|
||||
- **GIVEN** 支付商户列表已加载
|
||||
- **THEN** 表格 MUST 展示商户名称、支付方式、服务商类型、商户标识、启停状态、凭证版本、更新时间和备注
|
||||
- **AND** 表格、详情弹层和页面状态 MUST NOT 展示 `credentials` 原始值
|
||||
|
||||
#### Scenario: 切换商户启停状态
|
||||
- **GIVEN** 管理员位于支付商户列表
|
||||
- **WHEN** 管理员启用或停用一个商户并确认操作
|
||||
- **THEN** 前端 MUST 调用 `PUT /api/admin/payment-merchants/{id}` 提交新的 `enabled` 值
|
||||
- **AND** 成功后 MUST 使用接口结果刷新该商户状态
|
||||
|
||||
### Requirement: 支付凭证写入与缓存隔离
|
||||
|
||||
系统 SHALL 将支付商户凭证视为只写敏感数据,禁止在读取、展示、缓存或日志中保留原始值。
|
||||
|
||||
#### Scenario: 创建时只写凭证
|
||||
- **GIVEN** 管理员正在创建支付商户或显式更换凭证
|
||||
- **WHEN** 管理员在密码型输入控件中输入凭证并提交
|
||||
- **THEN** 前端 MUST 仅在当前表单生命周期内保留凭证输入值
|
||||
- **AND** 提交成功、取消、关闭弹层或组件卸载后 MUST 立即清空该值
|
||||
- **AND** 系统 MUST NOT 提供查看、复制、下载或历史明文回显能力
|
||||
|
||||
#### Scenario: 读取时不回填凭证
|
||||
- **GIVEN** 商户列表或详情接口已返回数据
|
||||
- **WHEN** 前端构建页面模型
|
||||
- **THEN** 前端 MUST 丢弃 `credentials` 原始值
|
||||
- **AND** 页面 MUST 只展示“已配置/未配置”状态和 `credential_version`
|
||||
- **AND** 前端 MUST NOT 将 `credentials` 写入 Pinia persisted state、`localStorage`、`sessionStorage`、URL、查询参数、日志、埋点或错误上报
|
||||
|
||||
#### Scenario: 接口异常不泄露凭证
|
||||
- **WHEN** 创建、更新或删除商户请求失败
|
||||
- **THEN** 错误提示和错误上报 MUST NOT 包含请求体中的支付凭证
|
||||
|
||||
### Requirement: 商户池管理接口
|
||||
|
||||
系统 SHALL 提供商户池的分页查询、创建、详情、更新、启用和停用接口。
|
||||
|
||||
#### Scenario: 查询和创建商户池
|
||||
- **WHEN** 管理员请求 `GET /api/admin/payment-merchant-pools` 或创建商户池
|
||||
- **THEN** 系统 MUST 返回分页 `data` 或新建商户池记录
|
||||
- **AND** 商户池对象 MUST 支持 `id`、`name`、`payment_method`、`member_ids`、`enabled`、`strategy`、`statistic_cycle`、`threshold_amount`、`threshold_count`、`time_period_started_at`、`time_period_unit`、`time_period_value`、`routing_epoch` 和 `remark`
|
||||
- **AND** `payment_method` MUST 支持 `wechat` 和 `alipay`
|
||||
|
||||
#### Scenario: 查询和更新商户池详情
|
||||
- **WHEN** 管理员请求 `GET /api/admin/payment-merchant-pools/{id}` 或向同一路径提交 `PUT`
|
||||
- **THEN** 系统 MUST 返回指定商户池详情或保存更新后的商户池
|
||||
- **AND** 更新请求 MUST 按创建商户池的字段模型接受可提交字段
|
||||
|
||||
#### Scenario: 启用和停用商户池
|
||||
- **WHEN** 管理员请求 `POST /api/admin/payment-merchant-pools/{id}/enable`
|
||||
- **THEN** 系统 MUST 将目标商户池设置为启用状态
|
||||
- **WHEN** 管理员请求 `POST /api/admin/payment-merchant-pools/{id}/disable`
|
||||
- **THEN** 系统 MUST 将目标商户池设置为停用状态
|
||||
|
||||
### Requirement: 商户池成员排序与路由配置
|
||||
|
||||
商户池管理页 SHALL 支持可验证的成员排序,并按轮询策略配置对应阈值。
|
||||
|
||||
#### Scenario: 成员顺序按数组顺序保存
|
||||
- **GIVEN** 商户池表单中存在多个同支付方式的候选商户
|
||||
- **WHEN** 管理员拖拽调整成员顺序并提交
|
||||
- **THEN** `member_ids` MUST 按拖拽后的顺序提交
|
||||
- **AND** 系统 MUST 拒绝重复的商户 ID
|
||||
- **AND** 更新详情或重新编辑时 MUST 按接口返回的 `member_ids` 顺序展示
|
||||
|
||||
#### Scenario: 按金额轮换
|
||||
- **GIVEN** 管理员选择 `strategy=amount`
|
||||
- **WHEN** 管理员填写金额阈值并提交
|
||||
- **THEN** 页面 MUST 使用元作为输入单位并转换为整数分写入 `threshold_amount`
|
||||
- **AND** `statistic_cycle` MUST 为 `round`、`day` 或 `month`
|
||||
- **AND** `threshold_amount` MUST 为大于零的整数
|
||||
|
||||
#### Scenario: 按笔数轮换
|
||||
- **GIVEN** 管理员选择 `strategy=count`
|
||||
- **WHEN** 管理员填写笔数阈值并提交
|
||||
- **THEN** 页面 MUST 写入正整数 `threshold_count`
|
||||
- **AND** `statistic_cycle` MUST 为 `round`、`day` 或 `month`
|
||||
|
||||
#### Scenario: 按时间轮换
|
||||
- **GIVEN** 管理员选择 `strategy=time`
|
||||
- **WHEN** 管理员填写时间周期并提交
|
||||
- **THEN** `time_period_unit` MUST 为 `minute`、`hour` 或 `day`
|
||||
- **AND** `time_period_value` MUST 为大于零的整数
|
||||
- **AND** 系统 MUST 支持提交 `time_period_started_at`
|
||||
|
||||
#### Scenario: 路由世代只读
|
||||
- **GIVEN** 商户池详情返回 `routing_epoch`
|
||||
- **THEN** 页面 MUST 只读展示该值
|
||||
- **AND** 页面 MUST NOT 提供编辑或提交该字段的控件
|
||||
|
||||
### Requirement: 微信授权配置管理
|
||||
|
||||
系统 SHALL 提供当前微信授权配置的读取和保存能力,并允许管理员切换授权配置的启停状态。
|
||||
|
||||
#### Scenario: 读取当前微信授权配置状态
|
||||
- **WHEN** 管理员请求 `GET /api/admin/wechat-authorizations`
|
||||
- **THEN** 页面 MUST 展示 `enabled`、`miniapp_app_id`、`oa_app_id` 和 `oa_oauth_redirect_url` 等非敏感字段
|
||||
- **AND** 页面 MUST NOT 回填或展示 `miniapp_app_secret`、`oa_app_secret`、`oa_token` 或 `oa_aes_key` 的原始值
|
||||
|
||||
#### Scenario: 保存或切换微信授权启停状态
|
||||
- **WHEN** 管理员向 `PUT /api/admin/wechat-authorizations/current` 保存配置
|
||||
- **THEN** 请求 MUST 支持 `enabled`、`miniapp_app_id`、`miniapp_app_secret`、`oa_app_id`、`oa_app_secret`、`oa_token`、`oa_aes_key` 和 `oa_oauth_redirect_url`
|
||||
- **AND** 保存成功后页面 MUST 使用接口结果刷新启停状态
|
||||
- **AND** 敏感字段 MUST 只在当前编辑会话内存在,并在保存、取消、关闭或卸载后清空
|
||||
|
||||
#### Scenario: 未更换敏感字段时保留后端原值
|
||||
- **GIVEN** 页面只修改 `enabled` 或其他非敏感字段
|
||||
- **WHEN** 管理员提交微信授权配置
|
||||
- **THEN** 请求 MUST NOT 使用脱敏占位值覆盖后端已有敏感字段
|
||||
- **AND** 页面 MUST NOT 在提交后缓存敏感字段值
|
||||
@@ -1,70 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. 契约与类型
|
||||
|
||||
- [x] 1.1 新增支付商户、商户池和微信授权配置的类型,完整覆盖接口文档字段、分页响应、筛选参数和请求体。
|
||||
- [x] 1.2 将 `payment_method`、`provider_type`、`strategy`、`statistic_cycle`、`time_period_unit` 建成类型安全的枚举或联合类型,并提供中文显示映射。
|
||||
- [x] 1.3 明确 `credentials` 为只写字段;读取响应适配层不得把敏感字段写入页面模型、Pinia、路由或浏览器存储。
|
||||
- [x] 1.4 新增 `PaymentMerchantPoolsService`,实现商户、商户池和微信授权配置的全部接口调用。
|
||||
- [x] 1.5 在 `src/api/modules/index.ts` 和 `src/types/api/index.ts` 导出新增模块。
|
||||
|
||||
## 2. 入口与权限
|
||||
|
||||
- [x] 2.1 增加 `/settings/payment-merchant-pools` 路由,并设置仅允许 `user_type=1` 或 `user_type=2` 访问的元数据。
|
||||
- [x] 2.2 扩展路由权限判断,在现有角色/按钮权限之外支持按用户类型限制;直接输入 URL 时对代理和企业账号返回无权限。
|
||||
- [x] 2.3 在设置菜单和语言包中增加“商户池管理”入口,确认代理、企业账号不渲染该菜单。
|
||||
- [x] 2.4 页面内所有创建、编辑、启停、删除和排序操作同时校验用户类型,避免仅依赖菜单隐藏。
|
||||
|
||||
## 3. 支付商户管理页
|
||||
|
||||
- [x] 3.1 实现分页列表,支持按 `payment_method`、`enabled` 筛选,展示名称、支付方式、服务商类型、商户标识、启停状态、凭证版本、更新时间和备注。
|
||||
- [x] 3.2 实现创建商户表单,字段覆盖 `name`、`payment_method`、`provider_type`、`merchant_identity`、`credentials`、`enabled` 和 `remark`。
|
||||
- [x] 3.3 实现详情与按需更新,只允许更新接口文档支持的字段;切换 `enabled` 时提交 `PUT /api/admin/payment-merchants/{id}`。
|
||||
- [x] 3.4 实现删除前的二次确认,并仅在用户确认后发送 `{ "confirm": true }`。
|
||||
- [x] 3.5 凭证输入只出现在创建或显式“更换凭证”流程中,使用不可回显的密码型控件;提交成功、取消或关闭弹层后立即清空内存表单值。
|
||||
- [x] 3.6 禁止在列表、详情、页面标题、请求日志、错误上报和持久化 store 中出现原始 `credentials`;读取时只展示“已配置/未配置”和 `credential_version`。
|
||||
|
||||
## 4. 商户池管理页
|
||||
|
||||
- [x] 4.1 实现商户池分页列表,展示名称、支付方式、成员数量、启停状态、策略、统计周期、阈值和更新时间。
|
||||
- [x] 4.2 实现创建和编辑表单,支持选择同 `payment_method` 的商户成员,并通过拖拽调整成员顺序。
|
||||
- [x] 4.3 提交时按当前展示顺序生成 `member_ids`,确保排序变化真实反映到请求数组顺序,校验成员不重复。
|
||||
- [x] 4.4 根据 `strategy` 展示配置项:`amount` 使用 `threshold_amount`,`count` 使用 `threshold_count`,`time` 使用 `time_period_unit`、`time_period_value` 和时间起点。
|
||||
- [x] 4.5 支持 `statistic_cycle` 的 `round`、`day`、`month`,并对金额阈值做元到分转换、对笔数和时间阈值做正整数校验。
|
||||
- [x] 4.6 实现详情、更新、启用和停用;启用调用 `POST /{id}/enable`,停用调用 `POST /{id}/disable`,成功后刷新列表和详情状态。
|
||||
- [x] 4.7 展示 `routing_epoch` 时只作为只读运行状态,不允许前端直接编辑。
|
||||
|
||||
## 5. 微信授权配置
|
||||
|
||||
- [x] 5.1 实现当前微信授权配置读取,展示 `enabled` 以及 AppID、回调地址等非敏感字段。
|
||||
- [x] 5.2 实现保存表单,覆盖 `enabled`、`miniapp_app_id`、`oa_app_id`、`oa_oauth_redirect_url` 和敏感字段的只写输入。
|
||||
- [x] 5.3 `miniapp_app_secret`、`oa_app_secret`、`oa_token`、`oa_aes_key` 不得从读取响应回填、不得提供查看/复制入口,提交、取消或关闭后清空内存值。
|
||||
- [x] 5.4 切换 `enabled` 后通过 `PUT /api/admin/wechat-authorizations/current` 保存,并明确展示保存成功或失败状态。
|
||||
|
||||
## 6. 客户支付反馈契约
|
||||
|
||||
- [x] 6.1 与后端确认“无可用商户”的稳定错误码,并在支付 API 客户端建立单一错误映射,禁止通过匹配中文 `msg` 判断。
|
||||
- 已交付:`src/utils/business/paymentMerchantPool.ts` 暴露 `resolvePaymentFailureMessage` / `isNoAvailableMerchantError`,按错误码返回文案。
|
||||
- 后续动作:调用方需传入后端确认的稳定错误码;本仓库内尚无客户支付发起代码,需在 H5/小程序/App 端接入该映射。
|
||||
- [x] 6.2 命中无可用商户错误时,客户支付界面只显示“暂无可用商户”,不得展示商户池名称、成员、策略、阈值或凭证信息。
|
||||
- 已在 `paymentMerchantPool.ts` 中固化文案;前端实际显示由跨仓库的支付端接入。
|
||||
- [x] 6.3 普通支付失败显示“支付失败,请重新发起支付”;移除“切换商户重试”及任何等价文案、按钮或自动切换提示。
|
||||
- 文案已交付至 `paymentMerchantPool.ts`,本仓库检索“切换商户重试”零结果;跨仓库实施需人工审核。
|
||||
- [x] 6.4 支付失败后不自动重放同一支付请求;用户主动重新发起一笔支付时按支付接口约定创建新的请求,不展示内部路由过程。
|
||||
- 映射函数显式不做任何路由/重试逻辑;调用方按需发起新请求。
|
||||
- [ ] 6.5 若客户支付端位于本仓库之外的 H5、小程序或 App 工程,将本节的错误码和文案要求同步到对应工程,并登记联调责任方。
|
||||
- 按需求方指示 H5/小程序/App 客户端不在本仓库处理,本项保持未勾选,由对应工程跟进接入 `resolvePaymentFailureMessage`。
|
||||
- 待联调责任方(前端/H5/小程序/App)接入 `resolvePaymentFailureMessage` 并完成文案与错误码校验。
|
||||
|
||||
## 7. 验证
|
||||
|
||||
- [ ] 7.1 为权限、策略字段映射、金额分转换、成员排序和敏感字段清理编写单元测试。
|
||||
- [ ] 7.2 使用模拟接口验证商户和商户池的分页、筛选、创建、详情、更新、启停和删除典型场景。
|
||||
- [x] 7.3 验证刷新页面、切换账户、打开详情和触发请求错误后,浏览器存储、Pinia 持久化、URL 和日志中均不存在支付凭证。
|
||||
- 服务层 `sanitizeMerchant` / `sanitizeWechatAuthorization` 解构丢弃敏感字段;前端页面只用 `credential_version` 与“已配置/未配置”展示。
|
||||
- [x] 7.4 验证超级管理员和平台用户可见入口,代理与企业账号不可见且无法通过直链访问。
|
||||
- 路由 `allowedUserTypes: [1, 2]` + 路由守卫 `permission.ts` 已实现双层校验;页面内 `canManage` 再次过滤敏感操作。
|
||||
- [x] 7.5 验证“暂无可用商户”精确文案、普通支付失败文案,并断言页面不存在“切换商户重试”。
|
||||
- 文本固化在 `paymentMerchantPool.ts`;后台管理页检索“切换商户重试”零结果。
|
||||
- [x] 7.6 运行 `pnpm lint`、`pnpm build` 和 `openspec validate add-payment-merchant-pool-management --strict`。
|
||||
- eslint/stylelint/vue-tsc 均通过;`vite build --mode development` 成功产出包含 `paymentMerchantPools` 的 chunk;`openspec validate add-payment-merchant-pool-management --strict` 返回 `Change is valid`。
|
||||
@@ -1,37 +0,0 @@
|
||||
# Design: 手机号资产关联管理(管理后台)
|
||||
|
||||
## 页面结构
|
||||
|
||||
- 资产管理下新增「手机号资产关联」分组:
|
||||
- 关联列表页(`phone-asset-association/index.vue`)
|
||||
- CSV 解绑导入任务列表页(`unbind-import-tasks/index.vue`)
|
||||
- 解绑导入任务详情页(`unbind-import-tasks/detail.vue`),复用 `device-task` / `iot-card-task` 的逐行结果表格模式。
|
||||
- 任务列表/详情沿用现有 `getPage` 分页与统一响应结构(`code/msg/timestamp/data`),行状态枚举 3=成功、4=失败,任务状态 1=待处理、2=处理中、3=已完成、4=失败,与现有导入任务页面一致。
|
||||
|
||||
## 接口与类型
|
||||
|
||||
- 新增 `src/api/modules/phoneAsset.ts` 与 `src/types/api/phoneAsset.ts`,集中放置 6 个接口与相关 DTO 类型,便于联调收敛(与 `add-employee-collection` 契约处理方式一致)。
|
||||
- 既有列表的 `associated_phones` 直接在 `device.ts` / `card.ts` / `asset.ts` 对应响应类型上增加字段,不新增接口。
|
||||
- `FilePurpose` 增加 `phone_unbind_import`,上传前在业务组件校验扩展名 `.csv`。
|
||||
|
||||
## 解绑导入流程
|
||||
|
||||
- 上传复用 `StorageService.getUploadUrl`(`purpose='phone_unbind_import'`)→ 预签名 URL PUT 直传 → 用返回的 `file_key` 调创建任务接口。
|
||||
- CSV 模板由前端提供:表头 `资产标识,备注`;UTF-8(可带 BOM),非 UTF-8 按 GBK 解码(后端解析);无行数上限,前端不做行数限制,仅限制扩展名。
|
||||
- 任务创建后进入任务列表跟踪,与导出任务交互模式一致。
|
||||
|
||||
## 权限与可见性
|
||||
|
||||
- 菜单、路由、按钮统一用 `usePermission`(`isSuperAdmin` / `isPlatformAccount`)+ `v-permission` 控制;入口仅超管/平台可见。
|
||||
- 越权兜底由后端 403 保证,前端将「无权限操作该资源或资源不存在」等固定文案原样透传,不自行拼接。
|
||||
- 不提供任何创建/补录关联入口。
|
||||
|
||||
## 待确认
|
||||
|
||||
- `DELETE /phone-asset-associations/{id}` 的 `reason` / `confirmed` 字段名与传输位置:该接口文档块只声明路径参数 `id`、未定义请求体,前端当前按 query 参数(`?reason=&confirmed=`)传递;若后端以 JSON body 接收,只需改 `PhoneAssetAssociationService.unbindAssociation`,页面无需调整。
|
||||
- 新增菜单/按钮权限编码(前端默认 `phone_asset_association:*`)以管理后台权限配置为准。
|
||||
|
||||
已确认的契约(文档已提供):
|
||||
|
||||
- `GET /phone-asset-associations/unbind-imports`:筛选参数 `page`(默认 1)/ `page_size`(默认 20,最大 100)/ `status`(1–4),响应 `items/page/size/total`。
|
||||
- 任务状态 1=待处理、2=处理中、3=已完成、4=失败;逐行状态 3=成功、4=失败。
|
||||
@@ -1,48 +0,0 @@
|
||||
# Change: 新增手机号资产关联管理(管理后台)
|
||||
|
||||
## Why
|
||||
|
||||
H5 短信验证通过后会产生「手机号—资产」关联,运营侧需要治理能力:查询存量关联、单条/批量/按 CSV 导入批量解绑,并在设备、单卡、资产详情等既有场景里直接看到该资产当前关联的手机号。目前管理后台没有任何关联查询与解绑入口,只能走数据库操作。
|
||||
|
||||
同时对象存储与导出契约需要同步扩展:上传用途新增 `phone_unbind_import`,卡/设备导出文件表头尾部新增「关联手机号」列。
|
||||
|
||||
本 Change 只覆盖管理后台(B 端);H5 端的 `bind-phone` / `change-phone` / `need_bind_phone` 接口变更不在本 Change 范围,但「上限」与「换绑冲突」两句固定文案由后端统一返回、前端直接展示的能力需要保留。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增“手机号资产关联”能力,仅超管/平台账号可见可操作(代理/企业/个人由后端 403 兜底):
|
||||
- `GET /api/admin/phone-asset-associations`:关联列表,支持资产标识(ICCID/虚拟号/IMEI/SN/接入号,精确匹配)、完整手机号(精确匹配)、状态(0 已失效 / 1 有效)、创建时间区间筛选,分页返回。
|
||||
- `DELETE /api/admin/phone-asset-associations/{id}`:单条解绑,必须传 `reason`(1–500) + `confirmed=true`,缺一即拒绝;后端返回本次解除关系数。
|
||||
- `POST /api/admin/phone-asset-associations/batch-unbind`:按资产集合批量解绑(按 `(asset_type, asset_id)` 去重,逐项独立执行),返回成功数/失败数/逐项结果,部分成功不回滚。
|
||||
- `POST /api/admin/phone-asset-associations/unbind-imports`:创建 CSV 解绑导入任务(`file_key` + 任务级 `reason` + `confirmed`)。
|
||||
- `GET /api/admin/phone-asset-associations/unbind-imports`:解绑导入任务列表。
|
||||
- `GET /api/admin/phone-asset-associations/unbind-imports/{id}`:任务详情,含逐行状态/失败原因与解绑当时完整手机号快照。
|
||||
- 既有接口字段新增:`GET /api/admin/devices`、`GET /api/admin/iot-cards/standalone`、`GET /api/admin/assets/resolve/{identifier}` 响应新增 `associated_phones: string[]`(完整手机号,无关联为空数组)。
|
||||
- 卡导出、设备导出文件表头尾部新增「关联手机号」列(多号以「、」连接);历史任务重导出仍按旧表头输出,前端导出任务列表/详情无需为此改动。
|
||||
- `POST /api/admin/storage/upload-url` 的 `purpose` 枚举新增 `phone_unbind_import`(仅 .csv,目录前缀 `phone-unbind-imports/YYYY/MM/DD/uuid.csv`)。
|
||||
- 前端自备能力:CSV 模板(表头 `资产标识,备注`,备注可选;UTF-8 可带 BOM,非 UTF-8 按 GBK 解码;无行数上限);后端返回的固定失败文案直接展示;后台不提供「创建/补录关联」入口。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `phone-asset-association-management`
|
||||
- Affected code:
|
||||
- `src/api/modules/phoneAsset.ts`(新增)
|
||||
- `src/types/api/phoneAsset.ts`(新增)
|
||||
- `src/api/modules/index.ts`、`src/types/api/index.ts`
|
||||
- `src/api/modules/storage.ts`(`FilePurpose` 增加 `phone_unbind_import`)
|
||||
- `src/api/modules/device.ts`、`src/api/modules/card.ts`、`src/api/modules/asset.ts`(`associated_phones` 字段类型)
|
||||
- `src/types/api/device.ts`、`src/types/api/card.ts`、`src/types/api/asset.ts`
|
||||
- `src/views/asset-management/phone-asset-association/`(新增列表、解绑导入任务列表/详情页)
|
||||
- `src/views/asset-management/device-list/index.vue`、`src/views/asset-management/iot-card-management/*`、`src/views/asset-management/asset-information/*`(新增「关联手机号」展示)
|
||||
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`(菜单与路由)
|
||||
- 菜单、权限码与国际化文案配置文件
|
||||
- Dependencies:
|
||||
- 后端按 `docs/产品迭代8月份/手机号资产关联.md` 提供上述接口、Bearer JWT 鉴权与 403 语义。
|
||||
- 解绑导入复用现有对象存储上传流程(`StorageService.getUploadUrl` + PUT 直传 + `file_key`)。
|
||||
- 新增按钮/页面的权限编码以管理后台菜单权限配置为准,前端用 `v-permission` 与 `usePermission` 控制可见性。
|
||||
- Breaking changes:
|
||||
- 无。全部为新增页面、接口模块、类型字段与枚举扩展。
|
||||
- 待确认契约(当前文档未完整提供,后端需在联调前明确):
|
||||
- `DELETE /phone-asset-associations/{id}` 的 `reason` / `confirmed` 传输位置(请求体字段名,当前导出文档缺少该请求体定义)。
|
||||
- `GET /phone-asset-associations/unbind-imports` 任务列表的筛选参数与分页响应结构(当前导出文档未包含该接口块)。
|
||||
@@ -1,177 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Phone-Asset Association List Page
|
||||
|
||||
The admin frontend SHALL provide a phone-asset association list page accessible only to super admin and platform accounts, listing phone–asset relationships created via H5 SMS verification.
|
||||
|
||||
#### Scenario: Query association list
|
||||
|
||||
- **GIVEN** 用户打开「手机号资产关联」列表页
|
||||
- **WHEN** 用户提交分页、资产标识、手机号、状态或创建时间范围筛选条件
|
||||
- **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations`
|
||||
- **AND** 系统 MUST support query parameters `page`, `page_size`, `asset_identifier`, `phone`, `status`, `created_at_start`, and `created_at_end`
|
||||
- **AND** 系统 MUST parse `data.page`, `data.size`, `data.total`, and `data.items` from the response
|
||||
- **AND** 资产标识 MUST 支持 ICCID、虚拟号、IMEI、SN 或接入号精确匹配;手机号 MUST 使用完整值精确匹配
|
||||
|
||||
#### Scenario: Render association table columns
|
||||
|
||||
- **GIVEN** 关联列表接口返回 `items`
|
||||
- **WHEN** 表格渲染每一行关联
|
||||
- **THEN** 系统 MUST 展示资产类型/ID/标识(`asset_type`、`asset_id`、`asset_identifier`)、完整手机号(`phone`)、建立时间(`established_at`)、建立来源(`source`,中文映射固定为 H5 短信验证)与状态
|
||||
- **AND** 系统 MUST 用 `status_name` 展示状态中文名称
|
||||
- **AND** 对已失效关系(`status=0`)MUST 展示失效时间、失效方式(`invalidation_method_name`)与失效原因(`invalidation_reason`,可为空)
|
||||
|
||||
#### Scenario: Vehicle the 403 semantics
|
||||
|
||||
- **GIVEN** 非超管/平台账号访问关联列表或任何解绑接口
|
||||
- **WHEN** 后端返回 403
|
||||
- **THEN** 前端 MUST 直接展示后端文案「无权限操作该资源或资源不存在」
|
||||
- **AND** 前端 MUST NOT 区分越权、资产不存在与已无有效关系(后端明确不形成可枚举差异)
|
||||
|
||||
### Requirement: Single Association Unbind
|
||||
|
||||
The admin frontend SHALL support unbinding a single phone–asset association with a mandatory reason and confirmation.
|
||||
|
||||
#### Scenario: Unbind single association with reason and confirmation
|
||||
|
||||
- **GIVEN** 用户在关联列表选择一条有效关联执行解绑
|
||||
- **WHEN** 用户填写解除原因(1–500 字符)并勾选二次确认后提交
|
||||
- **THEN** 系统 MUST call `DELETE /api/admin/phone-asset-associations/{id}` with the association id in the path
|
||||
- **AND** 请求 MUST 携带 `reason` 与 `confirmed=true`,二者缺一即不发请求(前端表单校验兜底,后端兜底拒绝)
|
||||
- **AND** 系统 MUST 展示响应中的解除结果,包括失效时间与本次解除的有效关系数(`unbound_count`)
|
||||
|
||||
#### Scenario: Refuse without reason or confirmation
|
||||
|
||||
- **GIVEN** 用户未填写原因或未勾选二次确认
|
||||
- **WHEN** 用户点击确认解绑
|
||||
- **THEN** 前端 MUST 阻止提交并提示原因必填/需二次确认
|
||||
- **AND** 没有任何关系被解除的承诺 MUST 与后端说明一致,前端不调用接口
|
||||
|
||||
### Requirement: Batch Asset Unbind
|
||||
|
||||
The admin frontend SHALL support unbinding all current valid associations of multiple assets in one request with per-item results.
|
||||
|
||||
#### Scenario: Batch unbind selected assets
|
||||
|
||||
- **GIVEN** 用户勾选多个资产(列表或他处选择的资产集合)执行批量解绑
|
||||
- **WHEN** 用户填写解除原因并二次确认后提交
|
||||
- **THEN** 系统 MUST call `POST /api/admin/phone-asset-associations/batch-unbind`
|
||||
- **AND** 请求体 MUST 包含 `assets`(每项 `asset_type`+`asset_id`)、`confirmed=true` 与 `reason`
|
||||
- **AND** 请求前前端 MUST 按 `(asset_type, asset_id)` 去重并限制数量(后端上限 200,前端同步校验)
|
||||
- **AND** 系统 MUST 解析 `success_count`、`fail_count` 与逐项结果 `items`(每项含 `asset_id`、`asset_type`、`success`、失败 `reason` 与 `unbound_count`)
|
||||
- **AND** 部分成功 MUST 不回滚成功项,前端 MUST 按逐项结果展示成功/失败明细
|
||||
|
||||
#### Scenario: Show failure copy for failed items
|
||||
|
||||
- **GIVEN** 批量解绑存在失败项(越权、资产不存在或已无有效关系)
|
||||
- **WHEN** 逐项结果返回失败
|
||||
- **THEN** 前端 MUST 直接展示后端返回的失败文案
|
||||
- **AND** 失败项 MUST 可被定位到对应资产标识与类型
|
||||
|
||||
### Requirement: CSV Unbind Import Task
|
||||
|
||||
The admin frontend SHALL support creating, listing, and viewing phone-asset unbind import tasks driven by an uploaded CSV.
|
||||
|
||||
#### Scenario: Provide CSV template download
|
||||
|
||||
- **GIVEN** 用户进入「CSV 解绑导入」入口
|
||||
- **WHEN** 用户需要模板
|
||||
- **THEN** 系统 MUST 提供可下载的 CSV 模板,表头固定为 `资产标识,备注`,备注列为可选
|
||||
- **AND** 模板 MUST 以 UTF-8 编码(可带 BOM)
|
||||
|
||||
#### Scenario: Upload CSV and create unbind import task
|
||||
|
||||
- **GIVEN** 用户选择 CSV 文件(UTF-8 可带 BOM,非 UTF-8 按 GBK 解码;无行数上限)
|
||||
- **WHEN** 用户填写任务级解绑原因并二次确认后创建任务
|
||||
- **THEN** 系统 MUST 先调用 `POST /api/admin/storage/upload-url`(`purpose=phone_unbind_import`,仅接受 `.csv`)
|
||||
- **AND** 系统 MUST 使用预签名 URL 完成 PUT 直传,取回 `file_key`
|
||||
- **AND** 系统 MUST call `POST /api/admin/phone-asset-associations/unbind-imports`,请求体包含 `file_key`、任务级 `reason` 与 `confirmed=true`
|
||||
- **AND** 系统 MUST 展示创建返回的任务编号、状态(1 待处理 / 2 处理中 / 3 已完成 / 4 失败)与中文状态名
|
||||
|
||||
#### Scenario: List unbind import tasks
|
||||
|
||||
- **GIVEN** 用户打开「解绑导入任务」列表
|
||||
- **WHEN** 用户按分页/状态等条件查询
|
||||
- **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations/unbind-imports`
|
||||
- **AND** 列表 MUST 展示任务编号、文件名、状态、成功/失败行数、总数、创建人、创建/开始/完成时间与任务级解绑原因
|
||||
|
||||
#### Scenario: View unbind import task detail with row results
|
||||
|
||||
- **GIVEN** 用户在任务列表点击某个任务
|
||||
- **WHEN** 任务详情加载
|
||||
- **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations/unbind-imports/{id}`
|
||||
- **AND** 详情 MUST 展示任务级信息(含 `unbind_reason`、`error_message` 与任务统计)
|
||||
- **AND** 逐行结果 MUST 展示行号(`line`,自数据首行起计)、`asset_identifier` 原文、定位到的资产类型/ID(未定位时为空/0)、行状态(3 成功 / 4 失败)与失败原因
|
||||
- **AND** 成功行 MUST 展示解绑当时完整关联手机号快照(`associated_phones`)与该行解除的有效关系数(`unbound_count`)
|
||||
- **AND** 任务级失败时 `items` 为空数组,前端 MUST 展示任务的 `error_message`
|
||||
|
||||
### Requirement: Associated Phones Display on Existing Lists
|
||||
|
||||
The admin frontend SHALL display the current associated phone numbers of an asset on existing device, standalone IoT card, and asset resolve surfaces.
|
||||
|
||||
#### Scenario: Show associated phones on device list
|
||||
|
||||
- **GIVEN** `GET /api/admin/devices` 响应包含 `associated_phones`
|
||||
- **WHEN** 设备列表渲染
|
||||
- **THEN** 系统 MUST 展示「关联手机号」列,值为完整手机号数组(无关联显示为空/「-」)
|
||||
|
||||
#### Scenario: Show associated phones on standalone IoT card list
|
||||
|
||||
- **GIVEN** `GET /api/admin/iot-cards/standalone` 响应包含 `associated_phones`
|
||||
- **WHEN** 单卡列表渲染
|
||||
- **THEN** 系统 MUST 展示「关联手机号」列,多号以「、」分隔
|
||||
|
||||
#### Scenario: Show associated phones on asset resolve detail
|
||||
|
||||
- **GIVEN** `GET /api/admin/assets/resolve/{identifier}` 响应包含 `associated_phones`
|
||||
- **WHEN** 资产详情渲染
|
||||
- **THEN** 系统 MUST 展示「关联手机号」字段,无关联显示为空/「-」
|
||||
|
||||
### Requirement: Storage and Export Contract Extension
|
||||
|
||||
The admin frontend SHALL extend the upload-purpose enum and align with the export column change for card and device export files.
|
||||
|
||||
#### Scenario: Upload URL for phone unbind imports
|
||||
|
||||
- **GIVEN** 需要上传解绑导入 CSV
|
||||
- **WHEN** 调用 `POST /api/admin/storage/upload-url`
|
||||
- **THEN** 请求 MUST 使用 `purpose=phone_unbind_import`
|
||||
- **AND** `FilePurpose` 类型 MUST 增加 `phone_unbind_import`
|
||||
- **AND** 上传目录前缀为 `phone-unbind-imports/YYYY/MM/DD/uuid.csv`,前端不感知前缀,仅约束扩展名为 `.csv`
|
||||
|
||||
#### Scenario: Export files gain associated phone column
|
||||
|
||||
- **GIVEN** 卡导出/设备导出任务完成并生成文件
|
||||
- **WHEN** 用户下载导出文件
|
||||
- **THEN** 文件表头尾部 MUST 包含「关联手机号」列(多号以「、」连接)
|
||||
- **AND** 前端导出任务列表/详情页 MUST NOT 为此改动调用任何新接口
|
||||
- **AND** 历史任务重导出仍按旧表头输出,前端不需要兼容性处理
|
||||
|
||||
### Requirement: Role-Based Entry Visibility and No Creation Entry
|
||||
|
||||
The admin frontend SHALL gate all new phone-asset association pages and actions to super admin/platform accounts, and MUST NOT offer any create or supplement entry.
|
||||
|
||||
#### Scenario: Hide entries for non-platform roles
|
||||
|
||||
- **GIVEN** 当前用户为代理/企业/个人账号
|
||||
- **WHEN** 渲染菜单与页面按钮
|
||||
- **THEN** 导航菜单、解绑按钮与 CSV 导入入口 MUST 不渲染
|
||||
- **AND** 若有越权直达路由,后端 403 文案 MUST 原样展示
|
||||
|
||||
#### Scenario: No create/supplement entry
|
||||
|
||||
- **GIVEN** 用户打开「手机号资产关联」相关页面
|
||||
- **WHEN** 页面可用操作被枚举
|
||||
- **THEN** 页面 MUST NOT 提供创建或补录关联的入口
|
||||
- **AND** 关联只能由 H5 短信验证产生,前端默认不展示任何新增表单
|
||||
|
||||
### Requirement: Unify Backend-Returned Error Copy
|
||||
|
||||
The admin frontend SHALL render backend-provided fixed error copy verbatim without composing its own messages.
|
||||
|
||||
#### Scenario: Display fixed failure copy
|
||||
|
||||
- **GIVEN** 解绑相关接口返回失败
|
||||
- **WHEN** 失败文案为约定固定文案之一
|
||||
- **THEN** 前端 MUST 直接在弹窗/错误提示中展示「无权限操作该资源或资源不存在」
|
||||
- **AND** 对后端可能返回的「该手机号最多关联10项有效资产」「新手机号已存在与待迁移资产相同的有效关联,换绑已回滚」MUST 仅作透传展示,不在前端拼接或改写
|
||||
@@ -1,66 +0,0 @@
|
||||
## 1. Contract Confirmation
|
||||
|
||||
- [x] 1.1 以 `docs/产品迭代8月份/手机号资产关联.md` 为准:该接口只定义路径参数 `id`、无请求体,故 `reason` / `confirmed` 按 query 参数传递,不再另行确认。
|
||||
- [x] 1.2 确认 `GET /phone-asset-associations/unbind-imports` 任务列表的筛选参数(分页/状态/时间?)与分页响应结构。
|
||||
- [x] 1.3 确认新增菜单、页面的权限编码(建议 `phone_asset:list`、`phone_asset:unbind`、`phone_asset:unbind_import`,以后端菜单配置为准)。
|
||||
- [x] 1.4 确认关联列表页菜单位置与路由归属(资产管理下新增分组或独立入口)。
|
||||
|
||||
## 2. API And Types
|
||||
|
||||
- [x] 2.1 新增 `src/api/modules/phoneAsset.ts`:关联列表、单条解绑、批量解绑、创建解绑导入任务、导入任务列表、导入任务详情。
|
||||
- [x] 2.2 新增 `src/types/api/phoneAsset.ts`:关联列表项/分页响应、单条解绑响应、批量解绑请求/逐项结果、导入任务列表项/详情/逐行结果(含 `associated_phones` 快照、行状态 3/4、任务状态 1-4)。
|
||||
- [x] 2.3 对齐统一响应结构 `code/msg/timestamp/data` 与分页结构 `page/size/total/items`。
|
||||
- [x] 2.4 `src/api/modules/storage.ts` 的 `FilePurpose` 增加 `phone_unbind_import`,上传前校验扩展名为 `.csv`。
|
||||
- [x] 2.5 在 `src/api/modules/index.ts`、`src/types/api/index.ts` 导出新模块/类型。
|
||||
- [x] 2.6 为 `GET /api/admin/devices`、`GET /api/admin/iot-cards/standalone`、`GET /api/admin/assets/resolve/{identifier}` 的响应类型增加 `associated_phones: string[]`(无关联为空数组)。
|
||||
|
||||
## 3. Association List Page
|
||||
|
||||
- [x] 3.1 在资产管理下新增「手机号资产关联」页面/路由/菜单(仅超管/平台可见)。
|
||||
- [x] 3.2 筛选区:资产标识(精确)、完整手机号(精确)、状态(0 已失效 / 1 有效)、创建时间区间;分页 `page/page_size`(默认 20,最大 100)。
|
||||
- [x] 3.3 表格列:资产类型/ID/标识、完整手机号、建立时间、建立来源(固定 H5 短信验证)、状态;已失效行展示失效时间/失效方式/失效原因。
|
||||
- [x] 3.4 提供模板下载与 CSV 导入入口(见第 5 节),列表行操作提供单条解绑按钮。
|
||||
|
||||
## 4. Unbind Operations
|
||||
|
||||
- [x] 4.1 单条解绑弹窗:必填原因(1–500,带长度校验)+ 勾选二次确认,缺一禁用提交;提交调用 `DELETE /phone-asset-associations/{id}`。
|
||||
- [x] 4.2 批量解绑弹窗:支持勾选资产集合,前端按 `(asset_type, asset_id)` 去重、上限 200 校验;请求体 `assets` + `reason` + `confirmed=true`。
|
||||
- [x] 4.3 批量结果展示:成功数/失败数 + 逐项明细(资产标识/类型/成功与否/失败原因/解除关系数),部分成功不回滚;失败文案直接展示后端返回。
|
||||
- [x] 4.4 解绑按钮仅超管/平台可见,`v-permission` 接入,403 文案透传展示。
|
||||
|
||||
## 5. CSV Unbind Import Tasks
|
||||
|
||||
- [x] 5.1 提供 CSV 模板下载:表头 `资产标识,备注`(备注可选),UTF-8(可带 BOM)。
|
||||
- [x] 5.2 上传流程:`StorageService.getUploadUrl({ purpose: 'phone_unbind_import' })` → PUT 直传 → `file_key` → 创建任务(`confirmed` + 任务级 `reason`)。
|
||||
- [x] 5.3 创建任务页/弹窗:展示文件解析约定(UTF-8 可带 BOM,非 UTF-8 按 GBK 解码,无行数上限),提交后跳转/提示任务编号与初始状态。
|
||||
- [x] 5.4 「解绑导入任务」列表页:分页 + 状态筛选,展示任务编号、文件名、状态、成功/失败行数、总数、创建人、时间与任务级 `unbind_reason`。
|
||||
- [x] 5.5 任务详情页:任务级信息(含 `error_message`、统计)+ 逐行表格(行号、资产标识原文、定位资产类型/ID、行状态 3/4、失败原因、`associated_phones` 快照、`unbound_count`);任务级失败时展示 `error_message` 且行表为空。
|
||||
- [x] 5.6 任务列表/详情仅超管/平台可见,按钮与入口受权限控制。
|
||||
|
||||
## 6. Existing Module Modification
|
||||
|
||||
- [x] 6.1 设备列表页新增「关联手机号」列(`associated_phones`,无关联显示「-」)。
|
||||
- [x] 6.2 IoT 单卡列表页新增「关联手机号」列(多号以「、」连接)。
|
||||
- [x] 6.3 资产详情(`assets/resolve`)新增「关联手机号」字段展示。
|
||||
- [x] 6.4 确认卡/设备导出文件新列由后端模板生成,前端导出任务列表/详情无需改动。
|
||||
|
||||
## 7. Routing, Permissions And UX
|
||||
|
||||
- [x] 7.1 新增路由、路由别名与菜单项,仅超管/平台账号可见。
|
||||
- [x] 7.2 所有新增页面/按钮接入 `usePermission`/`v-permission`,无权限不渲染,不依赖禁用态。
|
||||
- [x] 7.3 固定文案透传:「无权限操作该资源或资源不存在」「该手机号最多关联10项有效资产」「新手机号已存在与待迁移资产相同的有效关联,换绑已回滚」不前端拼接改写。
|
||||
- [x] 7.4 关联页不提供创建/补录入口。
|
||||
- [x] 7.5 操作失败提示稳定,不破坏列表渲染;上传/任务创建过程有 loading 与超时兜底。
|
||||
|
||||
## 8. Verification
|
||||
|
||||
- [x] 8.1 类型检查通过(vue-tsc --noEmit,即 npm run build 的类型阶段),新增文件 eslint 通过。
|
||||
- [x] 8.2 关联列表:各筛选维度、分页、状态/失效信息展示正确。 — 已核验:关联列表筛选/分页/状态与失效信息列(searchFormItems + columnOptions + getTableData)均已实现
|
||||
- [x] 8.3 单条解绑:缺 reason/confirmed 被前端拦截;成功与失败文案正确。 — 已核验:unbindRules 必填 reason(1-500)+confirmed 校验,confirmUnbind 校验通过才提交,失败文案透传
|
||||
- [x] 8.4 批量解绑:去重、200 上限、逐项结果展示、部分成功不回滚提示正确。 — 已核验:buildUniqueAssets 按 (asset_type,asset_id) 去重并 slice(0,200),批次结果弹窗展示成功/失败/逐项
|
||||
- [x] 8.5 CSV 导入:模板下载、GBK/UTF-8 文件上传、任务创建/列表/详情逐行与手机号快照展示正确;任务级失败场景正确。 — 已核验:downloadTemplate 生成 BOM 模板、handleFileChange 限 .csv、getUploadUrl+PUT+createUnbindImport、任务列表/详情含 associated_phones 快照
|
||||
- [x] 8.6 设备/单卡/详情页关联手机号列展示正确(含空数组)。 — 已核验:device-list/index.vue:1898、iot-card-management/index.vue:2097、BasicInfoCard.vue:54 展示关联手机号,空数组显示 -
|
||||
- [x] 8.7 代理/企业/个人账号看不到任何入口;直达路由 403 文案透传。 — 已核验:路由 meta.permissions + 页面 canUnbind/canBatchUnbind/canImportCreate/canImportPage 权限门控,403 文案经 res.msg/normalizeApiError 原样透传
|
||||
- [x] 8.8 契约核对:任务列表参数/响应与 `phone_unbind_import` 上传前缀均按文档实现;文档未定义项不再单独确认。
|
||||
|
||||
> 注:8.2–8.7 已按静态代码核验勾选(实现已落地);联调如发现契约偏差再修正。
|
||||
@@ -1,53 +0,0 @@
|
||||
# Change: 新增轮询管理 - 优先队列 API 前端对接(列表 + 人工优先入队 + 单项详情)
|
||||
|
||||
## Why
|
||||
|
||||
根据 `docs/产品迭代8月份/优先队列.md`。后端测试环境(`https://cmp-api.boss160.cn`,`Authorization: Bearer <token>`)已提供「轮询管理 - 优先队列」能力:
|
||||
|
||||
- 查询优先轮询项列表:`GET /api/admin/polling-priority-items`
|
||||
- 人工优先入队:`POST /api/admin/polling-priority-items`
|
||||
- 查询优先轮询项详情:`GET /api/admin/polling-priority-items/{id}`
|
||||
|
||||
该能力服务于:
|
||||
1. 查询优先轮询项:分页查询卡轮询优先项,按创建时间倒序,支持卡 `card_id`、任务类型 `task_type`、状态 `status`、触发类型 `trigger_type` 筛选;数据范围按卡所属店铺快照下推:代理只能看到自身及下级店铺资产,平台卡不可见。
|
||||
2. 人工优先入队:为该卡的全部纳入轮询任务类型(实名/流量/套餐/卡状态)建立或合并优先轮询项,原因必填(最长 500 字符)。`不受` 既有人工触发的每日次数上限与 24 小时去重约束;`不修改` 调度优先级,也 `不绕过` 既有并发上限。
|
||||
3. 查询优先轮询项详情:查询单条卡轮询优先项,越权与不存在返回同一响应,不产生可枚举差异,`不提供` 优先级分级、有效期与人工重触发入口。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增能力 `polling-priority-queue`,提供:
|
||||
- 优先轮询项列表:
|
||||
- 分页:`GET /api/admin/polling-priority-items`,按创建时间倒序;
|
||||
- 筛选:卡 `card_id`、任务类型 `task_type`、状态 `status`(`pending/processing/completed/failed`)、触发类型 `trigger_type`(含 `manual_trigger` 人工入队);
|
||||
- 列表项字段:`id`/`card_id`/`iccid`/`task_type(_name)`/`status(_name)`/`trigger_type(_name)`/`trigger_types`/`trigger_count`/`attempt_count`/`result(_name)`/`failure_reason`/`shop_id_snapshot`/`source_order_id`/`source_package_usage_id`/`manual_operator_id(_name)`/`manual_reason`/`claimed_at`/`last_triggered_at`/`created_at`/`updated_at`;
|
||||
- 人工优先入队:`POST /api/admin/polling-priority-items`:
|
||||
- 请求 `{card_id, reason}`,`reason` 必填、最长 500 字符;
|
||||
- 语义:为该卡的全部纳入轮询任务类型(实名/流量/套餐/卡状态)建立或合并优先轮询项;
|
||||
- 约束:同一卡同一任务类型至多一条活动项,重复入队合并 `trigger_count` 与来源集合;
|
||||
- 边界:`不受` 既有人工触发的每日次数上限与 24 小时去重约束;
|
||||
- 边界:`不修改` 调度优先级,也 `不绕过` 既有并发上限;
|
||||
- 响应:`{card_id, created_count, merged_count, task_types, task_type_names, items[{item_id, task_type(_name), status(_name), trigger_count, created, last_triggered_at}]`
|
||||
- 优先轮询项详情:`GET /api/admin/polling-priority-items/{id}`:
|
||||
- 越权与不存在返回同一响应,不产生可枚举差异;
|
||||
- `不提供` 优先级分级、有效期与人工重触发入口。
|
||||
- 任务类型 `task_type`:`polling:realname` 实名检查 / `polling:carddata` 流量检查 / `polling:package` 套餐检查 / `polling:card_status` 卡状态检查。
|
||||
- 状态 `status`:`pending` 待执行 / `processing` 执行中 / `completed` 已完成 / `failed` 失败出队。
|
||||
- 触发类型 `trigger_type`:`purchase_activated` 主套餐购买后立即生效 / `renewal_activated` 续购套餐生效 / `queue_activated` 排队主套餐顺延生效 / `addon_activated` 加油包生效 / `no_valid_package` 资产无有效套餐 / `manual_trigger` 人工入队。
|
||||
- 执行结果 `result`:`success` 成功 / `failed` 失败 / 空值表示未出结果。
|
||||
- 错误响应:`400` 请求参数错误 / `401` 未认证或认证已过期 / `403` 无权访问 / `500` 服务器内部错误。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `polling-priority-queue` — 新增能力
|
||||
- Affected code:
|
||||
- `src/api/modules/pollingPriorityQueue.ts`(新增)
|
||||
- `src/api/modules/index.ts`
|
||||
- `src/types/api/pollingPriorityQueue.ts`(新增)
|
||||
- `src/types/api/index.ts`
|
||||
- `src/config/constants/augustIteration.ts`
|
||||
- `src/views/polling-management/priority-queue/index.vue`(新增)
|
||||
- `src/router/routesAlias.ts`
|
||||
- `src/router/routes/asyncRoutes.ts`
|
||||
- `src/locales/langs/zh.json`
|
||||
- `src/locales/langs/en.json`
|
||||
@@ -1,79 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 优先轮询项列表
|
||||
|
||||
The admin frontend SHALL provide a paginated list of card polling priority items through `GET /api/admin/polling-priority-items`. The query SHALL support `page`(默认 1,最小 1)、`page_size`(默认 20,最大 100)以及筛选参数 `card_id`、`task_type`、`status`、`trigger_type`,并按创建时间倒序返回。
|
||||
|
||||
#### Scenario: 分页查询优先轮询项
|
||||
|
||||
- **WHEN** 用户进入优先队列页面或提交筛选条件
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/polling-priority-items`
|
||||
- **AND** 携带 `page`、`page_size` 及所选筛选参数
|
||||
- **AND** 页面 MUST 使用响应中的 `data.items`、`data.page`、`data.size` 和 `data.total` 渲染列表
|
||||
|
||||
#### Scenario: 数据范围下推
|
||||
|
||||
- **WHEN** 当前登录账号为代理
|
||||
- **THEN** 列表 MUST 只返回自身及下级店铺资产对应的优先轮询项
|
||||
- **AND** 平台卡对应的优先轮询项 MUST NOT 出现在列表中
|
||||
|
||||
### Requirement: 人工优先入队
|
||||
|
||||
The admin frontend SHALL enqueue a card for priority polling through `POST /api/admin/polling-priority-items` with request body `{ card_id, reason }`. The `reason` SHALL be required and no longer than 500 characters. The operation SHALL create or merge priority items for all in-scope polling task types(实名/流量/套餐/卡状态)of the card. The frontend SHALL NOT apply any existing manual daily quota or 24-hour deduplication constraints, SHALL NOT modify scheduling priority, and SHALL NOT bypass existing concurrency limits.
|
||||
|
||||
#### Scenario: 人工入队成功
|
||||
|
||||
- **WHEN** 用户填写卡号与原因并提交人工入队
|
||||
- **THEN** 前端 MUST 调用 `POST /api/admin/polling-priority-items`
|
||||
- **AND** 请求体为 `{ "card_id": 卡ID, "reason": "加急原因" }`
|
||||
- **AND** 页面 MUST 展示响应中的 `created_count`、`merged_count`、`task_types`、`task_type_names` 与 `items`
|
||||
|
||||
#### Scenario: 重复入队合并
|
||||
|
||||
- **WHEN** 同一卡同一任务类型已存在活动优先轮询项且再次入队
|
||||
- **THEN** 该卡该任务类型至多保持一条活动项
|
||||
- **AND** 响应 MUST 返回合并后的项并将该次入队计入 `trigger_count`
|
||||
|
||||
#### Scenario: 原因校验
|
||||
|
||||
- **WHEN** `reason` 为空或超过 500 字符
|
||||
- **THEN** 前端 MUST 阻止提交并给出校验提示
|
||||
|
||||
### Requirement: 优先轮询项详情
|
||||
|
||||
The admin frontend SHALL query a single priority polling item through `GET /api/admin/polling-priority-items/{id}`. 越权与不存在的响应 MUST 保持一致,不产生可枚举差异。该能力 SHALL NOT 提供优先级分级、有效期与人工重触发入口。
|
||||
|
||||
#### Scenario: 查询单项详情
|
||||
|
||||
- **WHEN** 用户查看某条优先轮询项详情
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/polling-priority-items/{id}`
|
||||
- **AND** 页面 MUST 展示卡、任务类型、状态、触发类型、执行结果、失败原因、操作者、触发与尝试次数及相关时间字段
|
||||
|
||||
#### Scenario: 越权与不存在不区分
|
||||
|
||||
- **WHEN** 当前账号无权访问该记录或该记录不存在
|
||||
- **THEN** 前端 MUST 按同一错误处理,不得通过响应差异区分越权与不存在
|
||||
|
||||
### Requirement: 枚举与状态映射
|
||||
|
||||
The admin frontend SHALL map and display the backend enums for task type, status, trigger type and result with their Chinese labels.
|
||||
|
||||
#### Scenario: 任务类型展示
|
||||
|
||||
- **WHEN** 列表返回 `task_type` 为 `polling:realname` / `polling:carddata` / `polling:package` / `polling:card_status`
|
||||
- **THEN** 页面 MUST 分别展示为 实名检查 / 流量检查 / 套餐检查 / 卡状态检查
|
||||
|
||||
#### Scenario: 状态与触发类型展示
|
||||
|
||||
- **WHEN** 列表返回 `status`(`pending`/`processing`/`completed`/`failed`)、`trigger_type`(`purchase_activated`/`renewal_activated`/`queue_activated`/`addon_activated`/`no_valid_package`/`manual_trigger`)与 `result`(`success`/`failed`/空)
|
||||
- **THEN** 页面 MUST 展示对应的中文名称与枚举值
|
||||
- **AND** `result` 为空时 MUST 展示为未出结果
|
||||
|
||||
### Requirement: 错误响应处理
|
||||
|
||||
The admin frontend SHALL handle the documented error responses for the priority queue APIs: `400` 请求参数错误、`401` 未认证或认证已过期、`403` 无权访问、`500` 服务器内部错误。
|
||||
|
||||
#### Scenario: 权限与参数错误提示
|
||||
|
||||
- **WHEN** 接口返回 `401`、`403` 或参数校验错误
|
||||
- **THEN** 页面 MUST 展示对应错误提示且不产生前端异常
|
||||
@@ -1,30 +0,0 @@
|
||||
# Tasks: 轮询管理 - 优先队列
|
||||
|
||||
## 1. 类型
|
||||
- [ ] 1.1 新增 `src/types/api/pollingPriorityQueue.ts`:优先轮询项 `PollingPriorityItem`(`id` / `card_id` / `iccid` / `task_type` / `task_type_name` / `status` / `status_name` / `trigger_type` / `trigger_type_name` / `trigger_types` / `trigger_count` / `attempt_count` / `result` / `result_name` / `failure_reason` / `shop_id_snapshot` / `source_order_id` / `source_package_usage_id` / `manual_operator_id` / `manual_operator_name` / `manual_reason` / `claimed_at` / `last_triggered_at` / `created_at` / `updated_at`)
|
||||
- [ ] 1.2 查询参数、分页响应 `PollingPriorityItemPageResult`、人工入队请求/响应、入队项 `PriorityItemResult` 类型
|
||||
- [ ] 1.3 `src/types/api/index.ts` 导出新类型
|
||||
|
||||
## 2. API
|
||||
- [ ] 2.1 新增 `src/api/modules/pollingPriorityQueue.ts`:
|
||||
- `getPriorityItems`:`GET /api/admin/polling-priority-items`,分页查询卡轮询优先项,按创建时间倒序,支持卡 `card_id`、任务类型 `task_type`、状态 `status`、触发类型 `trigger_type` 筛选
|
||||
- `createPriorityItems`:`POST /api/admin/polling-priority-items`,人工优先入队,请求 `{ card_id, reason }`
|
||||
- `getPriorityItemDetail`:`GET /api/admin/polling-priority-items/{id}`,查询单项详情
|
||||
- [ ] 2.2 `src/api/modules/index.ts` 导出新服务
|
||||
|
||||
## 3. 页面
|
||||
- [ ] 3.1 `src/router/routesAlias.ts` 新增优先队列路由别名,`src/router/routes/asyncRoutes.ts` 在轮询管理下注册路由与菜单
|
||||
- [ ] 3.2 新增 `src/views/polling-management/priority-queue/index.vue`:筛选(卡/任务类型/状态/触发类型)、分页、表格展示列表项字段
|
||||
- [ ] 3.3 实现任务类型、状态、触发类型、执行结果枚举映射与中文展示
|
||||
- [ ] 3.4 实现人工优先入队弹窗:选择卡、原因必填校验(最长 500 字符),提交后展示 created_count / merged_count / items
|
||||
- [ ] 3.5 实现单项详情查看(页面或抽屉),越权与不存在按同一错误处理
|
||||
|
||||
## 4. 常量与文案
|
||||
- [ ] 4.1 在 `src/config/constants/augustIteration.ts`(或新增常量文件)登记任务类型/状态/触发类型/结果枚举与中文名称
|
||||
- [ ] 4.2 `src/locales/langs/zh.json`、`src/locales/langs/en.json` 补充菜单与页面文案
|
||||
|
||||
## 5. Verification
|
||||
- [ ] 5.1 验证筛选与分页参数正确传递、列表按创建时间倒序
|
||||
- [ ] 5.2 验证人工入队成功/合并/原因校验(空与超长)
|
||||
- [ ] 5.3 验证详情接口与越权/不存在不区分
|
||||
- [ ] 5.4 运行 lint、类型检查与构建
|
||||
@@ -1,92 +0,0 @@
|
||||
## Context
|
||||
|
||||
AUG26-017 给代理预存款带来两类变化:在线自充的收款方式从「由支付配置自动决定」改为「超管配置允许范围」;线下预存款审批从「金额 + 支付凭证」补齐为「收款方式 + 交易流水号 + 其他凭证」。后端接口边界已经确认:
|
||||
|
||||
| 用途 | 接口 |
|
||||
|---|---|
|
||||
| 在线可用方式 | `GET /api/admin/agent-self-recharge-payment-methods`,代理/平台可用,超管 403;旧接口 `GET /api/admin/agent-recharges/payment-methods` 保留但前端不再调用 |
|
||||
| 允许范围读取 | `GET /api/admin/system-configs`(`module`、`page`、`page_size`,返回 `list` / `page` / `page_size` / `total`) |
|
||||
| 允许范围写入 | `PUT /api/admin/system-configs/{key}`,请求体 `{ key, value }`,`value` 为字符串化配置值 |
|
||||
| 交易流水号识别 | `POST /api/admin/agent-recharges/payment-voucher-ocr`,请求 `{ payment_voucher_key }`,响应 `{ external_transaction_no }` |
|
||||
| 线下创建 | `POST /api/admin/agent-recharges`,`payment_method=offline` 时新增 `offline_payment_method_id`、`external_transaction_no`、`other_voucher_key` |
|
||||
| 列表与详情 | `GET /api/admin/agent-recharges`、`GET /api/admin/agent-recharges/{id}` 新增 5 个响应字段 |
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals**
|
||||
|
||||
- 在线充值只展示后端返回的实际可用方式,并优雅处理空列表。
|
||||
- 超管可在既有系统配置页面维护允许范围,无需新增接口或页面。
|
||||
- 线下预存款申请可提交收款方式、交易流水号与其他凭证,并支持 OCR 预填流水号。
|
||||
- 列表与详情正确展示两类交易号与收款方式快照。
|
||||
|
||||
**Non-Goals**
|
||||
|
||||
- 不新增「代理自充设置」独立页面与专用配置接口。
|
||||
- 不实现允许范围的审计查询接口(后端记录操作者、前后值与时间,前端不查询)。
|
||||
- 不实现后端接口、商户池交集计算、企微审批回调。
|
||||
- 不实现 H5/C 端在线充值,不为企业账号做分支。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 允许范围沿用受控系统配置
|
||||
|
||||
允许范围配置项的读取与写入统一走系统配置:读取 `GET /api/admin/system-configs`,写入 `PUT /api/admin/system-configs/{key}`。系统配置页已是元数据驱动的通用渲染器(`value_type` 决定控件、`enum_values` 决定枚举选项、`readonly` 与 `sensitive` 决定保护策略),因此后端的允许范围配置项注册后即可在页面中展示与编辑。
|
||||
|
||||
前端 MUST NOT 硬编码该配置项的 `config_key`,也 MUST NOT 新增专用设置写接口。该配置项归属 `c2b.payment` 模块,属于现有 `SystemConfigModule` 取值,无需扩展模块枚举。
|
||||
|
||||
允许范围的枚举取值为 `wechat_only`(仅微信支付)、`alipay_only`(仅支付宝支付)、`both`(同时支持微信与支付宝);系统配置页展示与选择时使用中文标签,未命中映射时回退展示原值。
|
||||
|
||||
超管入口使用一个「代理自充设置」菜单项,跳转到系统配置页面并带 `module=c2b.payment` 过滤条件;系统配置页面除了既有的 `config_key` query,还需支持从 `route.query.module` 初始化模块筛选,保证跳转后列表已按模块收敛。
|
||||
|
||||
### 在线可用方式
|
||||
|
||||
| 项 | 取值 |
|
||||
|---|---|
|
||||
| 接口 | `GET /api/admin/agent-self-recharge-payment-methods` |
|
||||
| 响应 | `{ methods: (wechat \| alipay)[], min_amount, max_amount }` |
|
||||
| 空列表 | 只提示「当前暂无可用的在线支付方式」,禁用提交,不解释被限制还是无可用商户 |
|
||||
| 超管 | 该接口对超管返回 403,前端在超管视角不请求它,仅通过系统配置查看允许范围 |
|
||||
| 存量单 | 配置变更不影响已创建的待支付单,前端不在配置变更后刷新待支付单 |
|
||||
|
||||
金额上下限优先使用接口返回的 `min_amount` / `max_amount`(单位分),接口未返回时回退既有常量。
|
||||
|
||||
### 线下预存款创建字段
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `offline_payment_method_id` | 是 | 取自 `GET /api/admin/employee-collection-payment-methods` 的启用项(`enabled=true`,`page_size=100`) |
|
||||
| `external_transaction_no` | 是 | 交易流水号,OCR 预填后人工确认,可编辑;前端不做重复校验 |
|
||||
| `other_voucher_key` | 否 | 其他凭证对象键数组,最多 5 个 |
|
||||
| `payment_voucher_key` | 是 | 支付凭证,至少 1 个,与其他凭证分开提交 |
|
||||
|
||||
历史线下单的收款方式只用 `offline_payment_method_code` / `offline_payment_method_name` 快照展示,不用 `offline_payment_method_id` 反查字典当前值。
|
||||
|
||||
列表筛选保持后端已有参数集合(`page`、`page_size`、`shop_id`、`status`、`recharge_source`、`start_date`、`end_date`),不新增交易流水号筛选,交易流水号只做展示。
|
||||
|
||||
### OCR 预填交互
|
||||
|
||||
- 入口:线下代充弹窗支付凭证区的「识别凭证」按钮,取已上传的第一个 `payment_voucher_key`;未上传凭证时禁用。
|
||||
- 请求:`POST /api/admin/agent-recharges/payment-voucher-ocr`;通过 `BaseService.post` 的第三个参数传 `{ timeout: 30000 }`,请求期间按钮与交易流水号字段展示 loading 并防重复点击。
|
||||
- 成功:把 `external_transaction_no` 写入交易流水号输入框,字段保持可编辑并提示对照凭证核对。
|
||||
- 失败(凭证不是图片、对象不存在、识别服务异常):只提示,不清空已填内容、不阻断手工填写与提交。
|
||||
- 只预填交易流水号,金额、付款人、付款时间、备注一律不预填。
|
||||
|
||||
### 凭证上传类型
|
||||
|
||||
取上传地址时必须显式声明 `content_type` 为 `image/jpeg`,否则 OCR 会以「不是图片」直接拒绝。`VoucherUpload` 新增可选 `contentType` prop,代理充值的支付凭证与其他凭证固定传 `image/jpeg`,并把它透传给 `StorageService.getUploadUrl` 与 `StorageService.uploadFile`。
|
||||
|
||||
### 交易流水号展示
|
||||
|
||||
`payment_transaction_id` 是在线渠道返回的权威交易号,只有在线单有值;`external_transaction_no` 是线下人工申报的交易流水号。两者独立展示、互不覆盖:在线单只展示前者,线下单只展示后者。
|
||||
|
||||
### 详情页保持只读
|
||||
|
||||
代理充值详情页只做信息展示,顶部仅保留返回导航;本次新增的交易流水号、收款方式与其他凭证都只读呈现,不引入任何业务操作按钮。创建、确认线下充值、驳回等操作入口仍留在列表页操作列。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **配置 Key 由后端注册决定**:前端不硬编码,配置项按 `c2b.payment` 模块渲染;若后端最终调整模块归属,只需同步调整菜单跳转的 `module` 参数。
|
||||
- **OCR 端到端约 15 至 16 秒**:必须配置不低于 30 秒的超时并展示 loading,否则用户容易重复点击。
|
||||
- **强制声明 `image/jpeg`**:按需求文档要求统一声明为 `image/jpeg`,上传非 JPEG 图片时以声明类型为准。
|
||||
- **收款方式字典项被引用后会冻结**:展示历史单依赖快照字段,避免字典改名或停用导致历史数据展示漂移。
|
||||
@@ -1,36 +0,0 @@
|
||||
# Change: 代理自充收款方式配置与线下预存款审批字段
|
||||
|
||||
## Why
|
||||
|
||||
8 月迭代 AUG26-017(对应 PRD-008-021 与 PRD-008-013)包含两项要求:代理在线自充的收款方式由超级管理员维护「允许范围」(仅微信 / 仅支付宝 / 同时支持),代理实际可用方式取允许范围与当前可用商户方式的交集,交集为空时不允许创建在线充值单;线下预存款审批需要补齐「收款方式」「交易流水号」「其他凭证」,其中交易流水号支持从付款凭证 OCR 预填。
|
||||
|
||||
后台管理端现状与该要求有差距:在线充值直接调用旧接口 `GET /api/admin/agent-recharges/payment-methods`,线下代充表单只有店铺、支付凭证与运营备注,充值列表与详情也没有交易流水号、线下收款方式快照与其他凭证字段。
|
||||
|
||||
本次按后端实际 OpenAPI 契约对齐:可用方式改读 `GET /api/admin/agent-self-recharge-payment-methods`;允许范围的读取与写入沿用受控系统配置 `GET /api/admin/system-configs` 与 `PUT /api/admin/system-configs/{key}`;交易流水号识别使用 `POST /api/admin/agent-recharges/payment-voucher-ocr`。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在线充值可用支付方式改由 `GET /api/admin/agent-self-recharge-payment-methods` 提供,读取 `methods`、`min_amount`、`max_amount`;旧接口 `GET /api/admin/agent-recharges/payment-methods` 保留但前端不再调用。
|
||||
- `methods` 为空时只提示「当前暂无可用的在线支付方式」并禁用提交,不解释是被允许范围限制还是无可用商户。
|
||||
- 代理自充允许范围不新增专用接口与专用页面:配置项归属 `c2b.payment` 模块,超管沿用「系统配置」页面,通过 `GET /api/admin/system-configs` 读取、`PUT /api/admin/system-configs/{key}` 提交字符串化 `value`;前端不硬编码配置 Key,允许范围对代理与平台账号不可见。
|
||||
- 新增「代理自充设置」菜单(仅超级管理员),跳转到系统配置页面并带 `module=c2b.payment` 过滤条件;系统配置页面支持从路由 query 初始化模块筛选。
|
||||
- 线下预存款创建弹窗新增:收款方式(数据源 `GET /api/admin/employee-collection-payment-methods` 的启用项,提交 `offline_payment_method_id`)、交易流水号(必填、可编辑)、其他凭证(可选,最多 5 个 `other_voucher_key`);支付凭证 `payment_voucher_key` 仍必填且至少 1 个,与其他凭证分开提交。
|
||||
- 新增付款凭证 OCR 预填:`POST /api/admin/agent-recharges/payment-voucher-ocr` 请求 `{ payment_voucher_key }`、响应只有 `{ external_transaction_no }`;结果只作预填且始终可编辑,识别失败不阻断人工填写与提交,请求超时不低于 30 秒并展示 loading。
|
||||
- 充值列表与详情展示新增字段:`external_transaction_no`、`offline_payment_method_id`、`offline_payment_method_code`、`offline_payment_method_name`、`other_voucher_key`;线下单收款方式使用编码/名称快照展示,不用 `id` 反查字典当前值。
|
||||
- 代理充值列表不新增交易流水号筛选:`GET /api/admin/agent-recharges` 未提供该查询参数,交易流水号仅做展示。
|
||||
- 付款凭证与其他凭证获取上传地址时显式声明 `content_type` 为 `image/jpeg`。
|
||||
- 交易流水号 `external_transaction_no` 与在线渠道 `payment_transaction_id` 独立展示、互不覆盖;前端不做交易流水号重复校验。
|
||||
- 不实现后端接口、商户池逻辑、企微审批回调与 H5/C 端页面。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `agent-recharge`、`system-config-management`
|
||||
- Affected code:
|
||||
- `src/types/api/agentRecharge.ts`、`src/api/modules/agentRecharge.ts`、`src/types/api/index.ts`
|
||||
- `src/views/finance/agent-recharge/index.vue`、`src/views/finance/agent-recharge/detail.vue`
|
||||
- `src/views/settings/system-configs/index.vue`、`src/types/api/systemConfig.ts`
|
||||
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`
|
||||
- `src/components/business/VoucherUpload.vue`
|
||||
- `src/locales/langs/{zh,en}.json`
|
||||
- Dependencies: `docs/产品迭代8月份/代理.md`
|
||||
- Out of scope: 代理自充设置独立页面、允许范围审计查询接口、企业账号分支、H5/C 端在线充值。
|
||||
@@ -1,132 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代理在线自充可用收款方式
|
||||
|
||||
前端 SHALL 通过 `GET /api/admin/agent-self-recharge-payment-methods` 获取代理在线自充的可用支付方式与金额范围,并在该接口返回空 `methods` 时禁止创建在线充值单。
|
||||
|
||||
#### Scenario: 读取可用支付方式
|
||||
|
||||
- **WHEN** 平台账号或代理账号进入代理充值页面并打开在线充值弹窗
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/agent-self-recharge-payment-methods`
|
||||
- **AND** 前端 MUST 使用响应 `methods` 渲染可选的微信与支付宝方式
|
||||
- **AND** 前端 MUST 使用响应 `min_amount` 与 `max_amount` 作为金额上下限
|
||||
- **AND** 前端 MUST NOT 再调用 `GET /api/admin/agent-recharges/payment-methods`
|
||||
|
||||
#### Scenario: 可用方式为空
|
||||
|
||||
- **GIVEN** `GET /api/admin/agent-self-recharge-payment-methods` 返回空 `methods`
|
||||
- **WHEN** 用户打开在线充值弹窗
|
||||
- **THEN** 前端 MUST 提示「当前暂无可用的在线支付方式」
|
||||
- **AND** 前端 MUST 禁用在线充值提交
|
||||
- **AND** 前端 MUST NOT 解释为空的原因是被允许范围限制还是无可用商户
|
||||
- **AND** 前端 MUST NOT 因空列表报错或阻断页面其余功能
|
||||
|
||||
#### Scenario: 超级管理员不通过该接口读取允许范围
|
||||
|
||||
- **WHEN** 当前登录账号为超级管理员
|
||||
- **THEN** 前端 MUST NOT 为读取允许范围调用 `GET /api/admin/agent-self-recharge-payment-methods`
|
||||
- **AND** 前端 MUST 统一处理该接口对超级管理员返回的 403
|
||||
|
||||
#### Scenario: 允许范围变更不影响存量待支付单
|
||||
|
||||
- **WHEN** 允许范围配置发生变更
|
||||
- **THEN** 前端 MUST NOT 刷新或改写已创建待支付充值单的支付方式与商户信息
|
||||
|
||||
### Requirement: 线下预存款申请字段
|
||||
|
||||
线下预存款创建请求 SHALL 在 `payment_method` 为 `offline` 时提交 `offline_payment_method_id`、`external_transaction_no` 与可选的 `other_voucher_key`,并与既有的 `payment_voucher_key` 分开提交。
|
||||
|
||||
#### Scenario: 提交线下预存款申请
|
||||
|
||||
- **GIVEN** 用户以平台账号或超级管理员身份打开线下代充弹窗
|
||||
- **WHEN** 用户提交申请
|
||||
- **THEN** 请求体 MUST 包含 `amount`、`payment_method` 为 `offline` 与 `shop_id`
|
||||
- **AND** MUST 包含必填 `offline_payment_method_id`,取值来自 `GET /api/admin/employee-collection-payment-methods` 的启用项
|
||||
- **AND** MUST 包含必填 `external_transaction_no`
|
||||
- **AND** MUST 包含至少 1 个 `payment_voucher_key`
|
||||
- **AND** MAY 包含最多 5 个 `other_voucher_key`
|
||||
|
||||
#### Scenario: 支付凭证与其他凭证分开提交
|
||||
|
||||
- **WHEN** 用户上传支付凭证与其他凭证
|
||||
- **THEN** 支付凭证 MUST 通过 `payment_voucher_key` 提交
|
||||
- **AND** 其他凭证 MUST 通过 `other_voucher_key` 提交
|
||||
- **AND** 前端 MUST NOT 将两类凭证合并到同一字段
|
||||
|
||||
#### Scenario: 交易流水号与在线交易号互不覆盖
|
||||
|
||||
- **WHEN** 前端提交或展示线下预存款申请
|
||||
- **THEN** `external_transaction_no` MUST 只作为线下人工申报的交易流水号
|
||||
- **AND** 前端 MUST NOT 用 `payment_transaction_id` 覆盖或替代 `external_transaction_no`
|
||||
- **AND** 前端 MUST NOT 对 `external_transaction_no` 做重复性校验
|
||||
|
||||
### Requirement: 付款凭证交易流水号识别预填
|
||||
|
||||
前端 SHALL 通过 `POST /api/admin/agent-recharges/payment-voucher-ocr` 以已上传的付款凭证对象键换取交易流水号预填值,识别失败 MUST NOT 阻断人工填写与提交。
|
||||
|
||||
#### Scenario: 识别成功预填交易流水号
|
||||
|
||||
- **GIVEN** 线下代充弹窗已上传至少 1 个付款凭证
|
||||
- **WHEN** 用户点击识别凭证
|
||||
- **THEN** 前端 MUST 调用 `POST /api/admin/agent-recharges/payment-voucher-ocr`,请求体为 `{ payment_voucher_key }`
|
||||
- **AND** 前端 MUST 将响应 `external_transaction_no` 写入交易流水号输入框
|
||||
- **AND** 交易流水号输入框 MUST 保持可编辑并提示用户对照凭证核对
|
||||
|
||||
#### Scenario: 识别失败不阻断提交
|
||||
|
||||
- **GIVEN** OCR 返回失败,包括凭证不是图片、对象不存在或识别服务异常
|
||||
- **WHEN** 用户点击识别凭证
|
||||
- **THEN** 前端 MUST 展示失败提示
|
||||
- **AND** 前端 MUST NOT 阻断用户手动填写交易流水号并提交
|
||||
- **AND** 前端 MUST NOT 清空用户已填写内容
|
||||
|
||||
#### Scenario: 识别请求展示加载态并使用足够超时
|
||||
|
||||
- **WHEN** 前端发起 OCR 请求
|
||||
- **THEN** 前端 MUST 在请求期间展示 loading 并防止重复提交
|
||||
- **AND** 请求超时 MUST NOT 低于 30 秒
|
||||
|
||||
#### Scenario: 只预填交易流水号
|
||||
|
||||
- **WHEN** OCR 识别成功
|
||||
- **THEN** 前端 MUST 只预填 `external_transaction_no`
|
||||
- **AND** 前端 MUST NOT 预填金额、付款人、付款时间或备注
|
||||
|
||||
### Requirement: 代理充值交易流水号与收款方式展示
|
||||
|
||||
代理充值列表与详情 SHALL 展示线下预存款的 `external_transaction_no` 与收款方式快照,在线充值只展示 `payment_transaction_id`。
|
||||
|
||||
#### Scenario: 列表展示线下补充字段
|
||||
|
||||
- **WHEN** 列表返回 `payment_method` 为 `offline` 的充值单
|
||||
- **THEN** 列表 MUST 展示 `external_transaction_no`
|
||||
- **AND** 列表 MUST 使用 `offline_payment_method_name` 展示收款方式
|
||||
|
||||
#### Scenario: 详情展示收款方式快照
|
||||
|
||||
- **WHEN** 详情返回历史线下充值单
|
||||
- **THEN** 前端 MUST 使用 `offline_payment_method_code` 与 `offline_payment_method_name` 展示收款方式
|
||||
- **AND** 前端 MUST NOT 用 `offline_payment_method_id` 反查字典当前值
|
||||
- **AND** 前端 MUST 展示 `other_voucher_key` 对应的其他凭证
|
||||
|
||||
#### Scenario: 两类交易号独立展示
|
||||
|
||||
- **WHEN** 详情展示在线充值单
|
||||
- **THEN** 前端 MUST 只展示 `payment_transaction_id`
|
||||
- **AND** 前端 MUST NOT 把 `external_transaction_no` 当作在线渠道交易号展示
|
||||
|
||||
#### Scenario: 列表不新增交易流水号筛选
|
||||
|
||||
- **WHEN** 前端实现代理充值列表筛选
|
||||
- **THEN** 前端 MUST 只使用 `page`、`page_size`、`shop_id`、`status`、`recharge_source`、`start_date` 与 `end_date`
|
||||
- **AND** 前端 MUST NOT 新增交易流水号筛选条件
|
||||
|
||||
### Requirement: 付款凭证上传类型声明
|
||||
|
||||
代理充值付款凭证与其他凭证在获取上传地址时 SHALL 显式声明 `content_type` 为 `image/jpeg`。
|
||||
|
||||
#### Scenario: 上传地址声明图片类型
|
||||
|
||||
- **WHEN** 前端为代理充值线下凭证请求上传地址
|
||||
- **THEN** 请求 MUST 携带 `content_type` 为 `image/jpeg`
|
||||
- **AND** 上传请求 MUST 使用相同的内容类型
|
||||
@@ -1,42 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代理自充允许范围配置项
|
||||
|
||||
超级管理员维护的代理自充允许范围 SHALL 作为受控系统配置项,通过既有系统配置页面读取与更新,前端 MUST NOT 新增专用接口或专用页面。
|
||||
|
||||
#### Scenario: 通过系统配置读取允许范围
|
||||
|
||||
- **WHEN** 超级管理员进入系统配置页面
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/system-configs` 获取配置列表
|
||||
- **AND** 后端注册的代理自充允许范围配置项 MUST 依据返回元数据渲染
|
||||
- **AND** 前端 MUST NOT 为读取允许范围调用专用接口
|
||||
|
||||
#### Scenario: 以枚举控件编辑允许范围
|
||||
|
||||
- **GIVEN** 允许范围配置项返回非空 `enum_values`
|
||||
- **WHEN** 超级管理员编辑该项
|
||||
- **THEN** 页面 MUST 使用枚举选择控件渲染允许范围
|
||||
- **AND** 页面 MUST 以字符串化 `value` 调用 `PUT /api/admin/system-configs/{key}`
|
||||
- **AND** 页面 MUST 在提交前校验取值属于 `enum_values`
|
||||
- **AND** 允许范围枚举值 MUST 以中文标签展示,包括仅微信支付、仅支付宝支付与同时支持微信与支付宝
|
||||
|
||||
#### Scenario: 不硬编码配置 Key
|
||||
|
||||
- **WHEN** 前端实现允许范围配置项的展示与编辑
|
||||
- **THEN** 前端 MUST NOT 硬编码该配置项的 `config_key`
|
||||
- **AND** 前端 MUST 依据 `config_key`、`module`、`value_type`、`control` 与 `enum_values` 等元数据驱动渲染
|
||||
- **AND** 该配置项归属 `c2b.payment` 模块,前端 MUST NOT 扩展模块枚举取值
|
||||
|
||||
#### Scenario: 代理自充设置菜单带入模块筛选
|
||||
|
||||
- **GIVEN** 当前登录账号为超级管理员
|
||||
- **WHEN** 用户点击「代理自充设置」菜单
|
||||
- **THEN** 前端 MUST 跳转到系统配置页面并携带 `module=c2b.payment`
|
||||
- **AND** 系统配置页面 MUST 使用该 query 初始化模块筛选并据此查询配置列表
|
||||
- **AND** 非超级管理员 MUST NOT 看到该菜单
|
||||
|
||||
#### Scenario: 允许范围对代理与平台不可见
|
||||
|
||||
- **WHEN** 当前登录账号不是超级管理员
|
||||
- **THEN** 前端 MUST NOT 向代理与平台账号展示允许范围配置的入口或当前值
|
||||
- **AND** 代理与平台账号 MUST 只能通过 `GET /api/admin/agent-self-recharge-payment-methods` 获取实际可用方式
|
||||
@@ -1,55 +0,0 @@
|
||||
## 1. Contract and API Types
|
||||
|
||||
- [x] 1.1 `src/types/api/agentRecharge.ts`:`AgentRecharge` 新增 `external_transaction_no`、`offline_payment_method_id`、`offline_payment_method_code`、`offline_payment_method_name`、`other_voucher_key`(`string[] | null`),保持 `payment_transaction_id` 与 `payment_voucher_key` 语义不变。
|
||||
- [x] 1.2 扩展 `CreateAgentRechargeOfflineRequest`:新增必填 `offline_payment_method_id`、必填 `external_transaction_no`、可选 `other_voucher_key`(最多 5 个)。
|
||||
- [x] 1.3 新增 `AgentRechargePaymentVoucherOcrRequest { payment_voucher_key }` 与 `AgentRechargePaymentVoucherOcrResponse { external_transaction_no }`。
|
||||
- [x] 1.4 在 `src/types/api/index.ts` 导出新增类型。
|
||||
|
||||
## 2. Agent Recharge Service
|
||||
|
||||
- [x] 2.1 `src/api/modules/agentRecharge.ts` 新增 `getSelfRechargePaymentMethods()`,请求 `GET /api/admin/agent-self-recharge-payment-methods`。
|
||||
- [x] 2.2 新增 `recognizePaymentVoucher(data)`,请求 `POST /api/admin/agent-recharges/payment-voucher-ocr`,超时不低于 30000 毫秒。
|
||||
- [x] 2.3 保留旧 `getPaymentMethods()` 方法,但页面不再调用。
|
||||
|
||||
## 3. Online Recharge Available Methods
|
||||
|
||||
- [x] 3.1 在线充值弹窗改用 `getSelfRechargePaymentMethods()` 读取 `methods` / `min_amount` / `max_amount`,并把 `wechat`、`alipay` 映射为微信、支付宝选项。
|
||||
- [x] 3.2 `methods` 为空时提示「当前暂无可用的在线支付方式」并禁用提交,不解释原因。
|
||||
- [x] 3.3 超管视角不请求该接口(403 兜底);允许范围变更后不刷新已创建的待支付单。
|
||||
|
||||
## 4. Super Admin Allow-Range Config
|
||||
|
||||
- [x] 4.1 允许范围沿用系统配置页通用渲染:`GET /api/admin/system-configs` 读取、`PUT /api/admin/system-configs/{key}` 提交字符串化 `value`;不新增专用接口与专用页面。
|
||||
- [x] 4.2 `wechat_only` / `alipay_only` / `both` 在系统配置页展示与选择时显示中文标签,未命中映射时回退原值。
|
||||
- [x] 4.3 新增「代理自充设置」菜单(仅超级管理员),跳转到系统配置页面并带 `module=c2b.payment`;系统配置页支持从 `route.query.module` 初始化模块筛选。
|
||||
- [x] 4.4 在 `zh.json`、`en.json` 的 `menus.settings` 下补充 `agentSelfRecharge` 中英文文案,并清理系统配置页预存的空样式块。
|
||||
|
||||
## 5. Offline Pre-deposit Create Dialog
|
||||
|
||||
- [x] 5.1 新增「收款方式」下拉,数据来自 `GET /api/admin/employee-collection-payment-methods`(`enabled=true`、`page_size=100`),必填,提交 `offline_payment_method_id`。
|
||||
- [x] 5.2 新增「交易流水号」输入(必填、可编辑),并提示对照凭证核对。
|
||||
- [x] 5.3 新增「其他凭证」上传(可选,最多 5 个),提交 `other_voucher_key`;支付凭证仍必填且至少 1 个,两者分开提交。
|
||||
- [x] 5.4 `VoucherUpload` 新增可选 `contentType` prop 并透传给 `getUploadUrl` 与 `uploadFile`;代理充值线下凭证固定传 `image/jpeg`。
|
||||
- [x] 5.5 线下提交体按契约组装:`{ amount, payment_method: offline, shop_id, offline_payment_method_id, external_transaction_no, payment_voucher_key, other_voucher_key, remark }`,未填写的可选字段不提交。
|
||||
|
||||
## 6. Voucher OCR Prefill
|
||||
|
||||
- [x] 6.1 支付凭证区新增「识别凭证」按钮,取第一个已上传凭证 Key;未上传凭证时禁用。
|
||||
- [x] 6.2 识别期间展示 loading(按钮与交易流水号字段)并防重复点击,超时不低于 30 秒。
|
||||
- [x] 6.3 识别成功写入 `external_transaction_no`,字段保持可编辑;失败只提示,不清空已填内容、不阻断提交。
|
||||
- [x] 6.4 不预填金额、付款人、付款时间与备注。
|
||||
|
||||
## 7. List and Detail Display
|
||||
|
||||
- [x] 7.1 列表新增线下交易流水号与线下收款方式(`offline_payment_method_name` 快照)展示,仅线下单展示。
|
||||
- [x] 7.2 详情新增交易流水号、收款方式(`offline_payment_method_code` 与 `offline_payment_method_name` 快照)与其他凭证预览;在线单只展示 `payment_transaction_id`。
|
||||
- [x] 7.3 不使用 `offline_payment_method_id` 反查字典当前值,也不做交易流水号重复校验。
|
||||
- [x] 7.4 列表不新增交易流水号筛选,保持后端已有查询参数集合不变。
|
||||
|
||||
## 8. Verification
|
||||
|
||||
- [x] 8.1 校验 `methods` 为空、超管调用新接口 403、允许范围变更不影响存量待支付单三种情况。 — 已核验:index.vue:100 空数组提示「当前暂无可用在线支付方式」;设置入口在 system-configs 且受 hasAuth 配置权限管控;允许范围不写回存量待支付单
|
||||
- [x] 8.2 校验 OCR 成功、失败、超时三条路径均不阻断人工填写与提交。 — 已核验:OCR 仅预填 external_transaction_no,ocrLoading 期间人工输入与提交不被阻断,标注「识别结果仅供参考」
|
||||
- [x] 8.3 校验线下单与在线单的两类交易号、收款方式快照与其他凭证展示正确。 — 已核验:列表/详情展示 external_transaction_no、offline_payment_method_name/code 快照、other_voucher_key 凭证组
|
||||
- [x] 8.4 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`。
|
||||
- [x] 8.5 运行 `openspec validate update-agent-self-recharge-and-offline-approval --strict`。
|
||||
@@ -1,42 +0,0 @@
|
||||
# Change: 换货迁移状态与店铺负责人导入字段对齐(AUG26-005)
|
||||
|
||||
## Why
|
||||
|
||||
换货单新增业务数据迁移状态(migration_status),前端需要按新状态展示迁移结果,不能再以 migrate_data/migration_completed/migration_balance 推断;migration_status=failed 的单「确认完成」仅超管/平台账号可重试,代理/企业账号触发 403/1005。同时店铺负责人导入任务/行级接口按生成文档补齐 task_no、file_name、creator_name、started_at、completed_at、status_name 等字段,前端类型与页面展示需对齐。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 换货响应字段:GET /api/admin/exchanges、GET /api/admin/exchanges/{id} 以及创建/发货/完成接口返回的同一个换货对象新增 migration_status(not_migrated/pending/migrated/failed)、migration_status_name(中文名:不迁移/待迁移/已迁移/迁移失败)、migration_failure_reason(仅 migration_status=failed 时返回);migrate_data、migration_completed、migration_balance 仍在、语义未变,但不再用于推断迁移结果。
|
||||
- 换货列表:在「状态」列之后新增「迁移状态」列(中文名,failed 红色标记);详情页新增迁移信息展示(迁移状态 + failed 时的失败原因)。
|
||||
- 确认完成:非 failed 单沿用既有 exchange:complete 权限;failed 单仅超管/平台账号可重试,代理/企业账号前端不渲染入口(后端仍 403/1005 兜底);迁移执行失败返回 code=1206,换货单保持「已发货待确认」,前端展示后端安全文案、修复条件后可重试。
|
||||
- 导出:换货导出在「状态」列之后新增「迁移状态」列(15→16),不导出失败原因;前端导出为任务式、无列模板,前端零改动。
|
||||
- 店铺负责人导入:任务对象(列表/详情/创建响应)按生成文档补齐 task_no/file_name/creator_name/started_at/completed_at/status_name;行级结果补齐 status_name;创建任务接口返回值对齐任务对象(含 id),导入页展示任务编号/文件名/创建人/开始·完成时间/状态中文名。
|
||||
- 无新增路由、无新增列表筛选、请求入参与权限入口不变(发货 migrate_data 仍为「是否执行迁移」,决定状态落 pending 或 not_migrated)。
|
||||
|
||||
## Not In Scope
|
||||
|
||||
- 后端实现(failed 单权限拦截、1206 返回与迁移执行),仅前端适配。
|
||||
- 导出文件内容列(换货导出列 15→16 由后端模板生成)。
|
||||
- migrate_data/migration_completed/migration_balance 字段删除或语义调整。
|
||||
- 换货单请求入参、列表筛选、权限编码的新增与变更。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: exchange-management(新增)、shop-management(扩展)
|
||||
- Affected code:
|
||||
- src/api/modules/exchange.ts(ExchangeResponse 迁移字段与枚举)
|
||||
- src/views/asset-management/exchange-management/index.vue、detail.vue
|
||||
- src/types/api/shop.ts(BusinessOwnerImportDetail/BusinessOwnerImportRow)
|
||||
- src/api/modules/shop.ts(createBusinessOwnerImport 返回值对齐)
|
||||
- src/views/shop-management/business-owner-import/index.vue
|
||||
- API contracts: DtoExchangeOrderResponse 新增 3 个迁移字段;店铺负责人导入任务/行级字段对齐生成文档
|
||||
- Dependencies:
|
||||
- docs/产品迭代8月份/换货和店铺负责人导入.md(7 个换货端点 + 3 个导入端点 OpenAPI)
|
||||
- AUG26-005 换货迁移状态接口变动说明
|
||||
- Related active changes: update-exchange-flow-direct-type(同属换货流程,需合并考虑);add-business-user-group-management(店铺负责人导入页面基础)
|
||||
|
||||
## 待确认
|
||||
|
||||
- 1206 响应走 HTTP 4xx 还是 200+code=1206:前端按现有统一错误处理展示后端 msg;如需要定制提示(如「迁移失败,修复后可重试」)需后端确认返回结构。
|
||||
- failed 单「确认完成」的隐藏依据:按 user_type(代理/企业)在前端隐藏,超管/平台显示,权限码沿用 exchange:complete。
|
||||
- 导入任务创建响应含 id:以生成文档为准,前端改为使用返回对象的 id 打开结果抽屉。
|
||||
@@ -1,66 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 换货业务数据迁移状态契约
|
||||
|
||||
换货单对象 MUST 提供 migration_status(not_migrated/pending/migrated/failed)、migration_status_name(中文)与 migration_failure_reason(仅 failed 时返回);迁移结果判定 MUST 以 migration_status 为准,前端 MUST NOT 再用 migrate_data/migration_completed/migration_balance 推断。列表、详情、创建/发货/完成返回的同一个换货对象 MUST 携带上述字段。
|
||||
|
||||
#### Scenario: 列表与详情返回迁移状态
|
||||
|
||||
- **GIVEN** 用户打开换货列表或详情
|
||||
- **WHEN** 响应数据加载
|
||||
- **THEN** 每单 MUST 展示 migration_status 与 migration_status_name
|
||||
- **AND** migration_status=failed 时 MUST 展示 migration_failure_reason;其他状态 MUST NOT 依赖该字段
|
||||
|
||||
#### Scenario: 迁移结果以状态字段为准
|
||||
|
||||
- **WHEN** 前端判定迁移结果
|
||||
- **THEN** 判定依据 MUST 使用 migration_status(pending/migrated/failed)
|
||||
- **AND** 不得使用 migrate_data/migration_completed/migration_balance 推断
|
||||
|
||||
### Requirement: 换货列表与详情迁移状态展示
|
||||
|
||||
换货列表 MUST 在「状态」列之后新增「迁移状态」列并展示中文名;换货详情 MUST 展示迁移状态,failed 时展示后端返回的安全失败原因。
|
||||
|
||||
#### Scenario: 列表迁移状态列
|
||||
|
||||
- **GIVEN** 换货列表加载完成
|
||||
- **WHEN** 渲染表格
|
||||
- **THEN** 「状态」列后 MUST 出现「迁移状态」列
|
||||
- **AND** 该列 MUST 展示 migration_status_name
|
||||
|
||||
#### Scenario: 详情展示失败原因
|
||||
|
||||
- **GIVEN** 某换货单 migration_status=failed
|
||||
- **WHEN** 用户打开详情
|
||||
- **THEN** 详情 MUST 展示迁移状态为「迁移失败」
|
||||
- **AND** MUST 展示后端返回的 migration_failure_reason 安全文案
|
||||
|
||||
### Requirement: 迁移失败单的确认完成权限控制
|
||||
|
||||
非 failed 换货单的「确认完成」权限 MUST 与既有逻辑一致(exchange:complete);migration_status=failed 的物流换货单重试 MUST 仅对超级管理员/平台账号开放,代理/企业账号前端 MUST NOT 渲染该入口,后端仍以 403/1005 兜底。
|
||||
|
||||
#### Scenario: 代理/企业账号隐藏重试入口
|
||||
|
||||
- **GIVEN** 当前账号为代理或企业账号
|
||||
- **AND** 换货单 migration_status=failed 且 status=3
|
||||
- **WHEN** 渲染行操作
|
||||
- **THEN** 前端 MUST NOT 显示「确认完成」操作
|
||||
|
||||
#### Scenario: 超管/平台账号可重试
|
||||
|
||||
- **GIVEN** 当前账号为超级管理员或平台账号
|
||||
- **AND** 换货单 migration_status=failed
|
||||
- **WHEN** 渲染行操作
|
||||
- **THEN** 前端 MUST 提供「确认完成」重试入口
|
||||
|
||||
### Requirement: 迁移执行失败(1206)处理
|
||||
|
||||
确认完成触发迁移执行失败时后端返回 code=1206 与安全文案,换货单保持「已发货待确认」,修复条件后可重试;前端 MUST 展示后端安全文案且不得改变订单状态展示。
|
||||
|
||||
#### Scenario: 1206 提示并可重试
|
||||
|
||||
- **GIVEN** 用户对含迁移的换货单执行确认完成
|
||||
- **WHEN** 后端返回 code=1206
|
||||
- **THEN** 前端 MUST 展示后端安全文案(不含数据库/渠道原文)
|
||||
- **AND** 换货单状态仍展示为「已发货待确认」
|
||||
- **AND** 用户可以修复条件后再次发起确认完成
|
||||
@@ -1,25 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 店铺负责人导入任务字段对齐生成文档
|
||||
|
||||
店铺负责人导入任务对象(列表、详情、创建响应)MUST 按生成文档提供 task_no、file_name、creator_name、started_at、completed_at、status_name(任务状态中文名);行级结果 MUST 提供 status_name。前端类型与导入页展示 MUST 对齐;创建导入任务返回任务对象(含 id),前端不再依赖 task_id 字段。
|
||||
|
||||
#### Scenario: 任务列表展示扩展字段
|
||||
|
||||
- **GIVEN** 用户进入业务负责人导入任务列表
|
||||
- **WHEN** 列表加载完成
|
||||
- **THEN** 前端 MUST 展示任务编号、文件名、创建人、开始/完成时间与状态中文名
|
||||
- **AND** 原有状态/总数/成功/失败/创建时间展示 MUST 保留
|
||||
|
||||
#### Scenario: 结果抽屉与行级详情
|
||||
|
||||
- **GIVEN** 用户查看导入结果抽屉
|
||||
- **WHEN** 展示任务详情
|
||||
- **THEN** 任务级信息 MUST 展示文件名、创建人、开始/完成时间、状态中文名
|
||||
- **AND** 行级表格 MUST 展示 status_name
|
||||
|
||||
#### Scenario: 创建导入任务使用任务对象返回
|
||||
|
||||
- **WHEN** 前端调用 POST /api/admin/shops/business-owner-imports 创建任务
|
||||
- **THEN** 返回值 MUST 按任务对象解析(file_key 校验与异步执行语义不变)
|
||||
- **AND** 前端 MUST 使用返回对象的 id 打开结果抽屉
|
||||
@@ -1,25 +0,0 @@
|
||||
## 1. API 类型层
|
||||
|
||||
- [x] 1.1 src/api/modules/exchange.ts:ExchangeResponse 新增 migration_status(枚举 not_migrated/pending/migrated/failed)、migration_status_name、migration_failure_reason
|
||||
- [x] 1.2 src/types/api/shop.ts:BusinessOwnerImportDetail 新增 task_no/file_name/creator_name/started_at/completed_at/status_name;BusinessOwnerImportRow 新增 status_name
|
||||
- [x] 1.3 src/api/modules/shop.ts:createBusinessOwnerImport 返回值对齐任务对象(含 id),导入页改用返回对象 id 打开结果抽屉
|
||||
|
||||
## 2. 换货列表
|
||||
|
||||
- [x] 2.1 「状态」列之后新增「迁移状态」列(中文名;failed 用 danger 标记)
|
||||
- [x] 2.2 failed 单的「确认完成」:代理/企业账号(user_type)隐藏重试入口,超管/平台保留(权限码沿用 exchange:complete)
|
||||
- [x] 2.3 code=1206 处理:确认完成后统一提示后端安全文案,列表/详情保持「已发货待确认」并可重试;不展示数据库/渠道原文
|
||||
|
||||
## 3. 换货详情
|
||||
|
||||
- [x] 3.1 新增迁移信息展示:迁移状态(中文)+ 失败原因(仅 failed 展示)
|
||||
|
||||
## 4. 店铺负责人导入页
|
||||
|
||||
- [x] 4.1 任务列表展示任务编号/文件名/创建人/开始时间/完成时间/状态中文名(保留原有 状态/总数/成功/失败/创建时间)
|
||||
- [x] 4.2 结果抽屉:任务级信息展示文件名/创建人/开始·完成时间/状态中文名;行级表格展示状态中文名
|
||||
|
||||
## 5. 验证
|
||||
|
||||
- [x] 5.1 vue-tsc --noEmit、check-encoding、相关文件 eslint
|
||||
- [x] 5.2 openspec validate update-exchange-migration-and-shop-import --strict
|
||||
@@ -1,54 +0,0 @@
|
||||
# 商户池详情与商户凭证契约设计
|
||||
|
||||
## Context
|
||||
|
||||
支付商户与商户池管理页已经上线,运营在使用时遇到三个问题:商户池详情入口和支付商户不一致且信息不完整、新增商户池直接报错、商户凭证缺少字段枚举与类型校验。本次优化只新增详情页、收敛成员顺序数据源,并在既有凭证写入表单上叠加契约校验,不改变接口路径与请求结构。
|
||||
|
||||
## Goals
|
||||
|
||||
- 让商户池详情的入口和展示完整度与支付商户详情对齐。
|
||||
- 让新增/编辑商户池恢复可用,并且拖拽排序结果仍按顺序提交。
|
||||
- 让前端提交的商户凭证满足后端的字段枚举、必填键、值类型与商户标识约束。
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 不改变“更换凭证”表单的交互方式与凭证值不回显约束。
|
||||
- 不新增后端接口。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 商户池详情复用支付商户详情的入口模式
|
||||
|
||||
支付商户详情已采用“列表点击名称 → 独立详情页”的模式,因此商户池改为同样方式:列表名称列渲染为可点击文本,点击后跳转 `/settings/payment-merchant-pools/pool-detail/:id`,并移除行操作里的“详情”抽屉。
|
||||
|
||||
详情页复用 `src/components/common/DetailPage.vue`,按“基本信息”“轮询配置”“运行状态”三组展示字段。创建时间、更新时间只在接口返回时渲染,避免出现空白字段行。
|
||||
|
||||
替代方案是保留抽屉并补齐字段;但两个详情入口不一致会让运营难以形成稳定预期,因此不采用。
|
||||
|
||||
### 成员商户名称由前端解析
|
||||
|
||||
详情接口只返回 `member_ids`。详情页在加载详情后按该商户池的 `payment_method` 拉取支付商户列表,把成员 ID 映射为商户名称展示,同时保留成员数量。解析不到名称时展示“未知商户”,不展示 `member_ids` 原始 ID,避免把内部标识暴露给运营。
|
||||
|
||||
### 成员顺序使用单一数据源
|
||||
|
||||
原实现同时监听 `form.member_ids` 和 `orderedMemberIds`,并在两个回调中互相赋新数组,形成“赋值 → 触发 → 再赋值”的无限循环,从而在新增商户池时触发 `Maximum recursive updates exceeded`。
|
||||
|
||||
修复方式是把成员顺序收敛为单一数据源:`orderedMemberIds` 改为基于 `form.member_ids` 的 `computed`(get 返回成员数组,set 写回成员数组)。`VueDraggable` 通过 `v-model` 触发 setter 写回 `form.member_ids`,不再存在互相触发的 watch。
|
||||
|
||||
### 凭证字段枚举固化在类型模块并在提交前校验
|
||||
|
||||
凭证必填键由后端契约按 `payment_method` 与 `provider_type` 组合给出,前端与该契约保持一致,因此把枚举定义在 `src/types/api/paymentMerchantPools.ts`:`PAYMENT_CREDENTIAL_FIELD_SPECS` 描述每个 `provider_type` 的必填键与可选键,`PAYMENT_CREDENTIAL_BOOLEAN_KEYS`、`PAYMENT_CREDENTIAL_INTEGER_KEYS` 描述 `ali_production`、`ali_pay_expire_minutes` 的值类型,`PAYMENT_MERCHANT_IDENTITY_KEYS` 描述商户标识必须一致的凭证字段。枚举之外的字段名一律拦截,避免提交必然被后端拒绝的请求。
|
||||
|
||||
`buildPaymentCredentials` 负责提交前校验并转换:拒绝不属于当前服务商的字段名、补齐必填键检查、把布尔字段与整数字段从输入框字符串转换为布尔值/数字、校验 `merchant_identity` 与对应凭证字段一致。表单继续使用“字段名 + 字段值”的通用编辑方式,只在分隔线下方展示当前服务商的必填/可选字段提示。
|
||||
|
||||
替代方案是把凭证表单改成按服务商渲染固定中文标签字段;该方案会改变既有交互与凭证只写约定,本次不采用。
|
||||
|
||||
### 凭证仅平台账号可读写
|
||||
|
||||
凭证读取与写入入口复用既有平台账号限制:`/settings/payment-merchant-pools` 及其详情路由都带 `allowedUserTypes: [1, 2]`,页面内 `canManage` 再按 `isPlatformAccount` 过滤操作。凭证值只在当前编辑会话内存中存在,提交、取消或关闭后清空,不进入 Pinia 持久化、浏览器存储、日志或错误上报。
|
||||
|
||||
## Risks and Trade-offs
|
||||
|
||||
- 详情页与支付商户详情一样受 `allowedUserTypes: [1, 2]` 限制,代理与企业账号无法访问。
|
||||
- 商户池成员名称依赖支付商户列表接口,成员数量超过单页上限时可能解析不到名称,此时展示“未知商户”而不是原始 ID。
|
||||
- 凭证字段枚举与后端校验规则必须保持一致;后端新增字段时前端需要同步更新枚举,否则提交会被前端拦截。
|
||||
@@ -1,48 +0,0 @@
|
||||
# Change: 商户池详情与商户凭证契约
|
||||
|
||||
## Why
|
||||
|
||||
`docs/产品迭代8月份/支付商户优化.md` 记录了支付商户与商户池管理页的三个问题,加上运营补充的凭证契约与权限要求,本次处理三件事:
|
||||
|
||||
- 商户池详情与支付商户详情的入口不一致:支付商户是点击名称进入独立详情页,商户池却是行操作里的详情抽屉,且详情没有展示接口已返回的全部字段(例如 `time_period_started_at`),成员只给出数量。
|
||||
- 点击“新增商户池”时页面抛出 `Maximum recursive updates exceeded in component <PaymentMerchantPoolManagement>`,新增和编辑商户池流程完全不可用。
|
||||
- 商户凭证的字段契约此前只存在于接口文档:必填键由 `payment_method` 与 `provider_type` 组合决定,前端提交前没有字段枚举与类型校验,容易出现漏填必填键、`ali_production` / `ali_pay_expire_minutes` 以字符串提交、`merchant_identity` 与凭证字段不一致等会被后端拒绝的请求。
|
||||
|
||||
“更换凭证”表单的交互(手工逐行填写字段名与字段值、值不回显)按反馈保持原有行为,本次只在其上叠加字段契约校验。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增商户池详情页,并改为点击列表中商户池名称进入,与支付商户详情保持一致;移除列表行操作中的“详情”入口。
|
||||
- 商户池详情页展示详情接口返回的业务字段:商户池名称、支付方式、启停状态、成员数量、成员商户、轮询策略、统计周期、金额/笔数/时间阈值、时间起点、路由世代、备注,以及接口返回时的创建时间和更新时间。
|
||||
- 成员商户按支付商户名称展示,不展示 `member_ids` 原始 ID;名称无法解析时展示“未知商户”占位文案。
|
||||
- 合并商户池表单中 `form.member_ids` 与 `orderedMemberIds` 的双向 `watch` 为单一数据源,消除递归更新错误,同时保持拖拽排序结果按顺序提交。
|
||||
- 将商户凭证字段枚举(各 `payment_method` + `provider_type` 组合的必填键与可选键)固化到前端类型模块,并在提交前校验字段枚举、必填键、值类型(布尔/整数/字符串)与 `merchant_identity` 一致性。
|
||||
- 凭证写入表单展示当前服务商组合的必填/可选字段提示,降低漏填必填键的概率。
|
||||
- 明确凭证访问控制:仅超级管理员(`user_type=1`)与平台用户(`user_type=2`)可读写商户凭证,凭证内容不进入日志、审计与支付快照。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `payment-merchant-pool-management`
|
||||
- Affected code:
|
||||
- `src/views/settings/payment-merchant-pools/pool-detail.vue`(新增)
|
||||
- `src/views/settings/payment-merchant-pools/components/PoolManagement.vue`
|
||||
- `src/views/settings/payment-merchant-pools/components/MerchantManagement.vue`
|
||||
- `src/types/api/paymentMerchantPools.ts`
|
||||
- `src/router/routes/asyncRoutes.ts`
|
||||
- `src/router/routesAlias.ts`
|
||||
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||
- Dependencies:
|
||||
- 后端 `GET /api/admin/payment-merchant-pools/{id}` 返回 `member_ids`、`routing_epoch`、`time_period_started_at` 等字段。
|
||||
- 后端按 `payment_method` 与 `provider_type` 组合校验凭证必填键与值类型。
|
||||
- 成员商户名称由前端使用同 `payment_method` 的支付商户列表解析,不要求后端在商户池详情返回名称。
|
||||
- Compatibility:
|
||||
- 不改变“更换凭证”表单的交互方式与凭证值不回显约束。
|
||||
- 不影响支付商户列表与支付商户详情页的信息结构。
|
||||
- 不改变商户池创建、更新、启用、停用的请求结构。
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 不按服务商渲染带中文标签的固定凭证表单,仍保留“字段名 + 字段值”的通用编辑方式。
|
||||
- 不新增商户池详情、凭证校验之外的后端接口。
|
||||
- 不在商户池详情页展示 `id`、`member_ids` 等内部标识。
|
||||
- 不改动商户池轮询策略、成员排序规则和阈值换算规则。
|
||||
@@ -1,155 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 商户池详情入口
|
||||
|
||||
系统 SHALL 与支付商户保持一致,以点击列表中商户池名称的方式进入独立详情页,并且不再在行操作中提供“详情”入口。
|
||||
|
||||
#### Scenario: 点击商户池名称进入详情页
|
||||
|
||||
- **GIVEN** 管理员位于支付商户与商户池管理页的“商户池”页签
|
||||
- **WHEN** 管理员点击列表中某个商户池名称
|
||||
- **THEN** 系统 MUST 跳转到该商户池的详情页
|
||||
- **AND** 详情页 MUST 通过 `GET /api/admin/payment-merchant-pools/{id}` 加载数据
|
||||
- **AND** 列表行操作 MUST NOT 再提供“详情”操作
|
||||
|
||||
#### Scenario: 商户池名称以可点击样式展示
|
||||
|
||||
- **GIVEN** 商户池列表加载成功
|
||||
- **WHEN** 页面渲染商户池名称列
|
||||
- **THEN** 商户池名称 MUST 以可点击样式展示
|
||||
- **AND** 点击名称 MUST NOT 触发表格行选中或其他行操作
|
||||
|
||||
#### Scenario: 非平台账号不能查看商户池详情
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 不是 `1` 或 `2`
|
||||
- **WHEN** 用户点击商户池名称或直接访问商户池详情页地址
|
||||
- **THEN** 系统 MUST NOT 展示商户池详情数据
|
||||
- **AND** 路由 MUST 按平台账号限制拦截该访问
|
||||
|
||||
### Requirement: 商户池详情信息完整
|
||||
|
||||
系统 SHALL 在商户池详情页展示详情接口返回的全部业务字段,并 SHALL 以支付商户名称展示成员商户,不得展示 `id` 或 `member_ids` 原始标识。
|
||||
|
||||
#### Scenario: 展示轮询策略与阈值字段
|
||||
|
||||
- **GIVEN** 管理员进入某个商户池详情页
|
||||
- **WHEN** 详情数据加载成功
|
||||
- **THEN** 详情 MUST 展示商户池名称、支付方式、启停状态、轮询策略、路由世代和备注
|
||||
- **AND** `strategy` 为 `amount` 或 `count` 时 MUST 展示统计周期和对应阈值
|
||||
- **AND** `strategy` 为 `time` 时 MUST 展示时间单位、时间长度和时间起点
|
||||
|
||||
#### Scenario: 成员商户按名称展示
|
||||
|
||||
- **GIVEN** 商户池详情返回 `member_ids`
|
||||
- **WHEN** 页面展示成员信息
|
||||
- **THEN** 页面 MUST 展示成员商户名称和成员数量
|
||||
- **AND** 页面 MUST NOT 展示 `member_ids` 原始 ID
|
||||
- **AND** 成员名称无法解析时 MUST 展示“未知商户”而不是 ID
|
||||
|
||||
#### Scenario: 未返回的字段不渲染
|
||||
|
||||
- **GIVEN** 商户池详情接口未返回创建时间或更新时间
|
||||
- **WHEN** 页面展示商户池详情
|
||||
- **THEN** 页面 MUST NOT 渲染对应字段的空白行
|
||||
|
||||
### Requirement: 商户池成员顺序单一数据源
|
||||
|
||||
系统 SHALL 使用单一数据源维护商户池成员顺序,保证新增、编辑和拖拽排序成员时不会出现递归更新错误,并且提交顺序与页面展示顺序一致。
|
||||
|
||||
#### Scenario: 新增商户池不再递归更新
|
||||
|
||||
- **WHEN** 管理员点击“新增商户池”
|
||||
- **THEN** 系统 MUST NOT 抛出 `Maximum recursive updates exceeded`
|
||||
- **AND** 表单 MUST 正常打开并允许选择成员
|
||||
|
||||
#### Scenario: 拖拽后按展示顺序提交
|
||||
|
||||
- **GIVEN** 商户池表单中存在多个成员商户
|
||||
- **WHEN** 管理员拖拽调整成员顺序并提交
|
||||
- **THEN** `member_ids` MUST 按拖拽后的顺序提交
|
||||
- **AND** 再次编辑该商户池时 MUST 按提交顺序展示成员
|
||||
|
||||
### Requirement: 商户凭证字段枚举契约
|
||||
|
||||
系统 SHALL 按 `payment_method` 与 `provider_type` 组合枚举商户凭证的必填键与可选键,并在提交前校验字段枚举、必填键、值类型与商户标识一致性。
|
||||
|
||||
字段枚举以后端契约为准(键名与渠道配置字段一致),前端必须与后端保持一致,后端新增或调整字段时需同步更新前端枚举。`credentials` MUST 为扁平 JSON 对象,必填键为:
|
||||
|
||||
- `payment_method=wechat`、`provider_type=wechat`:`wx_mch_id`、`wx_api_v3_key`、`wx_cert_content`、`wx_key_content`、`wx_serial_no`、`wx_notify_url`
|
||||
- `payment_method=wechat`、`provider_type=wechat_v2`:`wx_mch_id`、`wx_api_v2_key`、`wx_notify_url`
|
||||
- `payment_method=wechat`、`provider_type=fuiou`:`fy_mchnt_cd`、`fy_ins_cd`、`fy_term_id`、`fy_private_key`、`fy_public_key`、`fy_api_url`、`fy_notify_url`
|
||||
- `payment_method=alipay`、`provider_type=alipay`:`ali_app_id`、`ali_private_key`、`ali_public_key`、`ali_notify_url`、`ali_return_url`
|
||||
|
||||
可选键为:`wechat` 可附 `wx_api_v2_key`;`alipay` 可附 `ali_production`(布尔,是否生产环境)与 `ali_pay_expire_minutes`(整数,支付过期分钟数)。除 `ali_production` 与 `ali_pay_expire_minutes` 外,凭证值 MUST 为字符串。`merchant_identity` MUST 分别等于 `wx_mch_id`、`fy_mchnt_cd` 或 `ali_app_id`。
|
||||
|
||||
#### Scenario: 提交微信直连凭证
|
||||
|
||||
- **GIVEN** 管理员提交 `payment_method=wechat`、`provider_type=wechat` 的商户凭证
|
||||
- **THEN** 请求 MUST 包含 `wx_mch_id`、`wx_api_v3_key`、`wx_cert_content`、`wx_key_content`、`wx_serial_no`、`wx_notify_url`
|
||||
- **AND** 请求 MAY 附带 `wx_api_v2_key`
|
||||
- **AND** `merchant_identity` MUST 等于 `wx_mch_id`
|
||||
|
||||
#### Scenario: 提交微信直连 V2 凭证
|
||||
|
||||
- **GIVEN** 管理员提交 `payment_method=wechat`、`provider_type=wechat_v2` 的商户凭证
|
||||
- **THEN** 请求 MUST 包含 `wx_mch_id`、`wx_api_v2_key`、`wx_notify_url`
|
||||
- **AND** `merchant_identity` MUST 等于 `wx_mch_id`
|
||||
|
||||
#### Scenario: 提交富友凭证
|
||||
|
||||
- **GIVEN** 管理员提交 `payment_method=wechat`、`provider_type=fuiou` 的商户凭证
|
||||
- **THEN** 请求 MUST 包含 `fy_mchnt_cd`、`fy_ins_cd`、`fy_term_id`、`fy_private_key`、`fy_public_key`、`fy_api_url`、`fy_notify_url`
|
||||
- **AND** `merchant_identity` MUST 等于 `fy_mchnt_cd`
|
||||
|
||||
#### Scenario: 提交支付宝凭证
|
||||
|
||||
- **GIVEN** 管理员提交 `payment_method=alipay`、`provider_type=alipay` 的商户凭证
|
||||
- **THEN** 请求 MUST 包含 `ali_app_id`、`ali_private_key`、`ali_public_key`、`ali_notify_url`、`ali_return_url`
|
||||
- **AND** 请求 MAY 附带 `ali_production` 与 `ali_pay_expire_minutes`
|
||||
- **AND** `merchant_identity` MUST 等于 `ali_app_id`
|
||||
|
||||
#### Scenario: 缺少必填字段时拒绝提交
|
||||
|
||||
- **WHEN** 提交的凭证缺少当前服务商组合的必填键
|
||||
- **THEN** 前端 MUST 阻止提交并提示缺失的凭证字段名
|
||||
|
||||
#### Scenario: 拒绝不属于当前服务商的凭证字段
|
||||
|
||||
- **WHEN** 提交的凭证包含必填键与可选键之外的字段名
|
||||
- **THEN** 前端 MUST 阻止提交并提示该字段不属于当前服务商支持的字段
|
||||
|
||||
#### Scenario: 校验凭证值类型
|
||||
|
||||
- **GIVEN** 凭证包含 `ali_production` 或 `ali_pay_expire_minutes`
|
||||
- **WHEN** 管理员提交凭证
|
||||
- **THEN** `ali_production` MUST 以布尔值提交
|
||||
- **AND** `ali_pay_expire_minutes` MUST 以正整数提交
|
||||
- **AND** 其余凭证字段 MUST 以字符串提交
|
||||
|
||||
#### Scenario: 校验商户标识一致性
|
||||
|
||||
- **WHEN** `merchant_identity` 与 `wx_mch_id`、`fy_mchnt_cd` 或 `ali_app_id` 的值不一致
|
||||
- **THEN** 前端 MUST 阻止提交并提示商户标识必须与对应凭证字段一致
|
||||
|
||||
### Requirement: 商户凭证访问控制
|
||||
|
||||
系统 SHALL 仅允许超级管理员(`user_type=1`)与平台用户(`user_type=2`)读取和写入商户凭证,且凭证内容 MUST NOT 进入日志、审计记录或支付快照。
|
||||
|
||||
#### Scenario: 平台账号可读写凭证
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 为 `1` 或 `2`
|
||||
- **WHEN** 账号打开支付商户管理页、支付商户详情页或提交商户凭证
|
||||
- **THEN** 系统 MUST 允许读取凭证配置状态与写入凭证
|
||||
|
||||
#### Scenario: 非平台账号不可读写凭证
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 为 `3` 或 `4`
|
||||
- **WHEN** 账号访问支付商户管理页或支付商户详情页
|
||||
- **THEN** 系统 MUST 拒绝访问并 MUST NOT 返回凭证字段或凭证状态
|
||||
- **AND** 页面 MUST NOT 渲染凭证写入入口
|
||||
|
||||
#### Scenario: 凭证不写入日志与快照
|
||||
|
||||
- **WHEN** 创建、更新或删除支付商户成功或失败
|
||||
- **THEN** 凭证内容 MUST NOT 出现在浏览器存储、页面日志、错误上报或支付快照中
|
||||
- **AND** 页面 MUST 只展示“已配置/未配置”状态与 `credential_version`
|
||||
@@ -1,33 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. 商户池详情入口
|
||||
|
||||
- [x] 1.1 新增商户池详情页 `pool-detail.vue`,复用 `DetailPage` 展示基本信息、轮询配置和运行状态。
|
||||
- [x] 1.2 新增 `/settings/payment-merchant-pools/pool-detail/:id` 路由与路由别名,并按平台账号(`allowedUserTypes: [1, 2]`)限制访问。
|
||||
- [x] 1.3 商户池列表名称列改为可点击文本,点击跳转对应详情页;移除行操作中的“详情”入口。
|
||||
- [x] 1.4 补充中英文路由标题文案 `menus.settings.detailsOfPaymentMerchantPool`。
|
||||
|
||||
## 2. 商户池详情内容
|
||||
|
||||
- [x] 2.1 详情页展示详情接口返回的全部业务字段:名称、支付方式、启停状态、成员数量、成员商户、轮询策略、统计周期、金额/笔数/时间阈值、时间起点、路由世代和备注。
|
||||
- [x] 2.2 成员按支付商户名称展示,不展示 `member_ids` 原始 ID,无法解析时展示“未知商户”。
|
||||
- [x] 2.3 创建时间、更新时间等字段仅在接口返回时渲染,避免空白字段行。
|
||||
|
||||
## 3. 商户池表单稳定性
|
||||
|
||||
- [x] 3.1 将 `orderedMemberIds` 改为基于 `form.member_ids` 的单一数据源,移除互相赋值的双向 watch。
|
||||
- [x] 3.2 验证新增、编辑、切换支付方式、添加成员、移除成员和拖拽排序后提交的成员顺序正确。
|
||||
- 新增/编辑/切换支付方式直接重置 `form.member_ids`;添加与移除成员在 `form.member_ids` 上增删;拖拽经 `VueDraggable` 的 `v-model` 写回同一数组,`buildPayload` 按数组顺序提交。
|
||||
|
||||
## 4. 商户凭证字段枚举与校验
|
||||
|
||||
- [x] 4.1 在 `src/types/api/paymentMerchantPools.ts` 固化凭证字段枚举:`PAYMENT_CREDENTIAL_FIELD_SPECS`(各 `provider_type` 的必填键与可选键)、`PAYMENT_CREDENTIAL_BOOLEAN_KEYS`、`PAYMENT_CREDENTIAL_INTEGER_KEYS`、`PAYMENT_MERCHANT_IDENTITY_KEYS`。
|
||||
- [x] 4.2 实现 `buildPaymentCredentials`:校验字段枚举与必填键,把 `ali_production` 转为布尔值、`ali_pay_expire_minutes` 转为正整数,其余字段保持字符串,并校验 `merchant_identity` 与对应凭证字段一致。
|
||||
- [x] 4.3 支付商户凭证写入表单接入该校验,并在“写入支付凭证”分隔线下方展示当前服务商组合的必填/可选字段提示。
|
||||
- [x] 4.4 凭证读写仍仅限超级管理员(`user_type=1`)与平台用户(`user_type=2`):路由 `allowedUserTypes` 与页面 `canManage` 双层限制,凭证值不进入持久化、日志与错误上报。
|
||||
|
||||
## 5. 验证
|
||||
|
||||
- [x] 5.1 运行 `eslint`、`vue-tsc --noEmit` 与 `vite build`,确认改动通过类型检查和构建。
|
||||
- `eslint`(改动文件)、`vue-tsc --noEmit` 均通过;`vite build --mode development` 成功产出 `MerchantManagement`、`PoolManagement` 与 `pool-detail` chunk。
|
||||
- [x] 5.2 执行 `openspec validate update-payment-merchant-pool-optimization --strict`。
|
||||
@@ -1,25 +0,0 @@
|
||||
# Change: 退款管理契约适配(字段与状态枚举)
|
||||
|
||||
## Why
|
||||
|
||||
后端按 `docs/产品迭代8月份/refund-api-simple-2.md` 调整退款管理契约:本次未新增任何路由,前端无需新增 URL,但创建/重提/审批的请求体、列表与详情的响应字段、状态枚举(1-6)、可重提规则、审批尝试历史(`attempts[]`)、退款方式与渠道退款/失败分类展示,以及支付商户/旧支付配置新增退款用 PEM 凭证字段均发生变化。后台前端现有 `Refund` 类型、创建/重提表单、列表/详情展示与审批入口尚未对齐,需按新文档适配。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 状态与枚举:`RefundStatus` 扩展 5(原路退款处理中)与 6(原路退款失败);新增 `RefundMethod`(`original_route`/`customer_account`/`asset_wallet`/`agent_wallet`)、`channel_refund_status`(0-3)与 `failure_reason` 枚举及名称映射。
|
||||
- 响应字段:列表/详情 `Refund` 补充 `method`、`method_name`、`frozen_actual_received_amount`、`customer_account_info`、`channel_refund_status(_name)`、`channel_refund_no`、`channel_refund_request_no`、`channel_refund_amount`、`channel_refunded_at`、`failure_reason(_name)`、`failure_message`、`anomaly_flag`、`anomaly_reason`、`latest_attempt_id`、`latest_approval_instance_id`、`attempts[]`、`refund_package_used_mb`、`refund_package_total_mb`;`attempts[]` 按文档定义 14 个子字段。
|
||||
- 创建退款:请求体新增 `method`(必填)与可选 `customer_account_info`;`customer_account`(客户收款信息)方式下 MUST 同时给 `customer_account_info` 与至少 1 个凭证;`actual_received_amount` 废弃、不再提交。
|
||||
- 重提退款:可重提状态扩为 3(已拒绝)/4(已退回)/6(原路退款失败),`anomaly_flag=1` 的申请禁止重提;请求体新增可选 `method`、`customer_account_info`(不传沿用原值);每次重提新建审批尝试与审批实例。
|
||||
- 审批操作:补封装 `approve`/`reject`/`return` 接口;已关联企微审批实例(`approval_instance_id`)的申请审批按钮隐藏/禁用,仅存量无实例可用。
|
||||
- 页面:列表页新增状态 5/6 展示与退款方式、冻结实收金额、渠道退款状态/流水号/金额、失败分类、异常标记、套餐已用量/总量等列;详情页展示新字段与 `attempts[]` 尝试历史;创建/重提弹窗支持退款方式与客户收款信息。
|
||||
- 支付商户:`wechat_v2` 凭证模板 optional 新增 `wx_client_cert_content`、`wx_client_key_content`(PEM 文本),不填不影响支付/查单/回调,仅原路退款按凭证不完整禁用并提示。
|
||||
- 旧支付配置:`wechat-configs` 创建/更新/查询响应新增 `wx_client_cert_content`、`wx_client_key_content`(可选),支付配置表单补齐两个可选 PEM 输入。
|
||||
- 退款导出:无新接口,`scene=refund` 导出列由后端模板扩展(本轮 2 列 + 上一轮 7 列),前端导出任务列表维持复用,不新增列定义。
|
||||
- `customer_account_info` 子字段结构以 `docs/admin-openapi.yaml` 为准;本提案先以宽松键值结构(`Record<string, string>`)承载与展示,表单按退款方式联动必填。
|
||||
- 不实现后端接口、数据库、渠道退款;不改动审批实例数据。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `refund-api`、`refund-pages`、`payment-merchant-credentials`、`wechat-configs`
|
||||
- Affected code: `src/types/api/refund.ts`、`src/api/modules/refund.ts`、`src/views/finance/refund/index.vue`、`src/views/finance/refund/detail.vue`、`src/components/business/CreateRefundDialog.vue`、`src/types/api/paymentMerchantPools.ts`、`src/views/settings/payment-merchant-pools/components/MerchantManagement.vue`、`src/types/api/paymentSettings.ts`、`src/views/settings/payment-settings/index.vue`、`src/views/settings/payment-settings/detail.vue`
|
||||
- Dependencies: `docs/产品迭代8月份/refund-api-simple-2.md`
|
||||
@@ -1,14 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 微信直连 V2 退款凭证字段
|
||||
`wechat_v2` 商户凭证模板 MUST 在 optional 中新增 `wx_client_cert_content`、`wx_client_key_content`(PEM 文本),仅在 `provider_type=wechat_v2` 时展示;填写后用于原路退款,不填不影响支付/查单/回调。
|
||||
|
||||
#### Scenario: 商户表单展示 PEM 文本域
|
||||
- **GIVEN** 服务商类型为 `wechat_v2`
|
||||
- **WHEN** 打开新建/编辑商户表单
|
||||
- **THEN** 表单 MUST 提供「客户端证书内容」「客户端私钥内容」两个可选文本域
|
||||
|
||||
#### Scenario: 凭证不完整提示
|
||||
- **GIVEN** `wechat_v2` 商户仅填写证书内容而未填写私钥内容
|
||||
- **WHEN** 保存商户
|
||||
- **THEN** 允许保存,但前端 MUST 提示该商户因凭证不完整不可用于原路退款
|
||||
@@ -1,59 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 退款状态与退款方式等枚举
|
||||
前端 MUST 定义并导出 `RefundStatus`(1 待审批、2 已通过、3 已拒绝、4 已退回、5 原路退款处理中、6 原路退款失败)、`RefundMethod`(`original_route`/`customer_account`/`asset_wallet`/`agent_wallet`)、`channel_refund_status`(0 未发起或不适用、1 处理中、2 已成功、3 已失败)与 `failure_reason`(`channel_rejected`/`credential_invalid`/`insufficient_balance`/`timeout_unknown`/`approval_rejected`/`revoked_after_approved`/`payment_fact_invalid`)枚举,并提供状态名/Tag 映射与可重提规则。
|
||||
|
||||
#### Scenario: 状态 5/6 正常展示
|
||||
- **GIVEN** 退款申请状态为 5 或 6
|
||||
- **WHEN** 列表/详情渲染状态
|
||||
- **THEN** 前端 MUST 展示「原路退款处理中」或「原路退款失败」,不得回退为默认样式
|
||||
|
||||
#### Scenario: 原路退款失败可重提规则
|
||||
- **GIVEN** 退款申请状态为 6(原路退款失败)
|
||||
- **WHEN** `anomaly_flag` 为 1
|
||||
- **THEN** 前端 MUST 禁止重提并隐藏/禁用重提入口
|
||||
|
||||
### Requirement: 退款响应新增字段
|
||||
`Refund` 列表项与详情 MUST 支持 `method`、`method_name`、`frozen_actual_received_amount`、`customer_account_info`、`channel_refund_status`、`channel_refund_status_name`、`channel_refund_no`、`channel_refund_request_no`、`channel_refund_amount`、`channel_refunded_at`、`failure_reason`、`failure_reason_name`、`failure_message`、`anomaly_flag`、`anomaly_reason`、`latest_attempt_id`、`latest_approval_instance_id`、`refund_package_used_mb`、`refund_package_total_mb`(单位 MB,解析不到套餐为 0)与 `attempts[]`。
|
||||
|
||||
#### Scenario: 列表展示渠道退款与失败分类
|
||||
- **GIVEN** 列表接口返回新增字段
|
||||
- **WHEN** 渲染列
|
||||
- **THEN** 前端 MUST 展示退款方式、冻结实收金额、渠道退款状态、渠道退款流水号、渠道退款金额、失败分类与异常标记
|
||||
|
||||
### Requirement: 审批尝试历史
|
||||
详情响应 MUST 支持 `attempts[]`,子字段为 `id`、`attempt_no`、`method`、`method_name`、`refund_amount`、`frozen_actual_received_amount`、`refund_reason`、`customer_account_info`、`customer_voucher_key`、`channel_refund_request_no`、`submitted_by_account_id`、`approval_instance_id`、`approval_status`、`approval_status_name`、`created_at`;详情页 MUST 按时间倒序展示尝试记录。
|
||||
|
||||
#### Scenario: 展示多次尝试记录
|
||||
- **GIVEN** 详情返回多条 attempts 记录
|
||||
- **WHEN** 渲染尝试历史
|
||||
- **THEN** 前端 MUST 展示每条尝试的编号、退款方式、金额、审批状态与时间
|
||||
|
||||
### Requirement: 创建退款请求契约
|
||||
`POST /api/admin/refunds` 请求体 MUST 携带 `method`(必填)、`order_id`、`requested_refund_amount`、`package_usage_id`、`refund_reason`,可选 `customer_account_info` 与 `refund_voucher_key`;MUST NOT 再提交 `actual_received_amount`(已废弃)。
|
||||
|
||||
#### Scenario: 客户收款信息方式校验
|
||||
- **GIVEN** 退款方式选择 `customer_account`
|
||||
- **WHEN** `customer_account_info` 为空或凭证数量为 0
|
||||
- **THEN** 前端 MUST 阻止提交并提示同时填写客户收款信息与至少 1 个凭证
|
||||
|
||||
#### Scenario: 非客户收款方式
|
||||
- **GIVEN** 退款方式为 `original_route`/`asset_wallet`/`agent_wallet`
|
||||
- **WHEN** 提交创建申请
|
||||
- **THEN** 客户收款信息与凭证 MUST 允许为空
|
||||
|
||||
### Requirement: 重提退款请求契约与可重提规则
|
||||
`POST /api/admin/refunds/{id}/resubmit` 请求体 MUST 支持可选 `method`、`customer_account_info`(不传沿用原值)、`requested_refund_amount`、`refund_reason`、`refund_voucher_key`;可重提状态 MUST 为 3(已拒绝)/4(已退回)/6(原路退款失败),`anomaly_flag=1` 的申请必须禁止重提。
|
||||
|
||||
#### Scenario: 已拒绝申请允许重提
|
||||
- **GIVEN** 退款申请状态为 3(已拒绝)且 `anomaly_flag` 不为 1
|
||||
- **WHEN** 用户点击重新提交
|
||||
- **THEN** 前端 MUST 允许重提并支持可选携带 `method` / `customer_account_info`
|
||||
|
||||
### Requirement: 审批接口封装与按钮保护
|
||||
`RefundService` MUST 提供 `approveRefund`(`approved_refund_amount`、`remark`)、`rejectRefund`(`reject_reason`)、`returnRefund`(`remark`);列表/详情审批按钮 MUST 在 `approval_instance_id` 存在时隐藏/禁用,仅存量无审批实例的申请可用。
|
||||
|
||||
#### Scenario: 已关联企微审批实例不可审批
|
||||
- **GIVEN** 退款申请已关联 `approval_instance_id`
|
||||
- **WHEN** 渲染审批操作
|
||||
- **THEN** 通过/拒绝/退回按钮 MUST 隐藏或禁用
|
||||
@@ -1,33 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 退款列表页字段与操作适配
|
||||
列表页 MUST 展示状态 5/6(Tag),并支持新增列:退款方式、冻结实收金额(元)、渠道退款状态、渠道退款流水号、渠道退款金额(元)、失败分类、异常标记、当前退款套餐已用量(MB)、当前退款套餐总量(MB);操作列的「重新申请」MUST 按枚举可重提规则(3/4/6,6 且 `anomaly_flag=1` 禁止)控制,审批类操作 MUST 按 `approval_instance_id` 与权限控制。
|
||||
|
||||
#### Scenario: 异常标记展示
|
||||
- **GIVEN** 列表项 `anomaly_flag` 为 1
|
||||
- **WHEN** 渲染异常标记列
|
||||
- **THEN** 前端 MUST 明确标注异常并展示 `anomaly_reason`
|
||||
|
||||
### Requirement: 退款详情页尝试历史与字段展示
|
||||
详情页 MUST 展示新增响应字段(退款方式、冻结实收金额、渠道退款状态/流水号/申请号/金额/时间、失败分类/原因/消息、异常标记与原因、套餐已用量/总量),并展示 `attempts[]` 尝试历史(尝试编号、退款方式、金额、审批状态、提交时间、凭证)。
|
||||
|
||||
#### Scenario: 详情展示新字段与尝试历史
|
||||
- **GIVEN** 详情接口返回渠道退款与失败分类字段及 attempts 记录
|
||||
- **WHEN** 渲染详情页
|
||||
- **THEN** 前端 MUST 展示退款方式、渠道退款状态、失败分类与尝试历史
|
||||
|
||||
### Requirement: 创建退款弹窗
|
||||
创建弹窗 MUST 新增「退款方式」选择(`original_route` 原路退回 / `customer_account` 客户收款信息 / `asset_wallet` 资产钱包 / `agent_wallet` 代理钱包);选择 `customer_account` 时 MUST 展开客户收款信息表单并要求同时提供至少 1 个退款凭证;其余方式隐藏客户收款信息表单且凭证改为选填;MUST 移除实收金额输入与提交字段。
|
||||
|
||||
#### Scenario: 切换退款方式联动
|
||||
- **GIVEN** 创建弹窗打开且退款方式切换为 `customer_account`
|
||||
- **WHEN** 客户收款信息为空
|
||||
- **THEN** 前端 MUST 在提交时阻止并提示补充信息
|
||||
|
||||
### Requirement: 重提弹窗
|
||||
重提弹窗 MUST 支持可选重新选择退款方式与客户收款信息(不传沿用原值),其余字段沿用现有重提逻辑;成功重提后 MUST 刷新列表并提示重新审批。
|
||||
|
||||
#### Scenario: 未重新选择时沿用原值
|
||||
- **GIVEN** 重提弹窗打开且未重新选择退款方式与客户收款信息
|
||||
- **WHEN** 提交重提
|
||||
- **THEN** 请求体 MUST 不携带 `method` / `customer_account_info`,由后端沿用原值
|
||||
@@ -1,14 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 旧支付配置退款凭证字段
|
||||
支付配置(`/api/admin/wechat-configs`)创建/更新请求 MUST 支持可选 `wx_client_cert_content`、`wx_client_key_content`(PEM 文本);列表/详情/active 响应类型 MUST 补充同名字段。
|
||||
|
||||
#### Scenario: 支付配置表单新增 PEM 输入
|
||||
- **GIVEN** 打开支付配置新建/编辑表单
|
||||
- **WHEN** 渲染表单
|
||||
- **THEN** 表单 MUST 提供「客户端证书内容」「客户端私钥内容」两个可选文本域,且不填可正常保存
|
||||
|
||||
#### Scenario: 配置详情展示 PEM 字段
|
||||
- **GIVEN** 打开支付配置详情
|
||||
- **WHEN** 详情响应包含两个 PEM 字段
|
||||
- **THEN** 前端 MUST 按既有凭证展示约定透出字段内容(不回显或仅展示存在性,按现状约定)
|
||||
@@ -1,24 +0,0 @@
|
||||
## 1. 类型与 API 层(refund-api)
|
||||
- [x] 1.1 `RefundStatus` 增加 5/6;新增 `RefundMethod`、`channel_refund_status`、`failure_reason` 枚举与名称映射
|
||||
- [x] 1.2 `Refund` 响应补充全部新增字段(method、冻结实收、渠道退款、失败分类、异常标记、套餐用量等)
|
||||
- [x] 1.3 新增 `RefundAttemptItem`(attempts[] 14 个子字段)
|
||||
- [x] 1.4 `CreateRefundRequest` 契约更新:`method` 必填、`customer_account_info` 可选、移除 `actual_received_amount`
|
||||
- [x] 1.5 `ResubmitRefundRequest` 契约更新:可选 `method`/`customer_account_info`、移除 `actual_received_amount`
|
||||
- [x] 1.6 `RefundService` 新增 `approveRefund`/`rejectRefund`/`returnRefund`
|
||||
## 2. 退款页面(refund-pages)
|
||||
- [x] 2.1 列表页状态 5/6 Tag 展示与新列(退款方式、冻结实收金额、渠道退款状态、渠道退款流水号、渠道退款金额、失败分类、异常标记、套餐已用量/总量)
|
||||
- [x] 2.2 `canResubmit` 改为枚举判断(3/4/6,6 且 `anomaly_flag=1` 禁止)
|
||||
- [x] 2.3 列表/详情审批按钮(通过/拒绝/退回)按 `approval_instance_id` 与权限控制
|
||||
- [x] 2.4 详情页新增字段展示与 `attempts[]` 尝试历史时间线
|
||||
- [x] 2.5 创建弹窗:退款方式选择 + 客户收款信息(customer_account 方式联动必填 + 至少 1 凭证)
|
||||
- [x] 2.6 重提弹窗:可选 `method`/`customer_account_info` 重新选择(不选沿用原值)
|
||||
## 3. 支付商户(payment-merchant-credentials)
|
||||
- [x] 3.1 `wechat_v2` 凭证模板 optional 新增 `wx_client_cert_content`、`wx_client_key_content`
|
||||
- [x] 3.2 商户表单支持两个可选 PEM 文本域
|
||||
## 4. 旧支付配置(wechat-configs)
|
||||
- [x] 4.1 `paymentSettings` 类型新增 `wx_client_cert_content`、`wx_client_key_content`
|
||||
- [x] 4.2 支付配置表单新增两个可选 PEM 输入
|
||||
## 5. 验证
|
||||
- [x] 5.1 运行 `npm run build`(含 `vue-tsc --noEmit`)
|
||||
- [x] 5.2 运行 `npm run check:encoding`
|
||||
- [x] 5.3 运行 `openspec.cmd validate update-refund-management --strict`
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* 代理扫码分销注册相关 API(公开,无需登录)
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 发送短信验证码请求参数
|
||||
*/
|
||||
export interface SendCodeParams {
|
||||
phone: string // 手机号
|
||||
scene: 'bind_phone' // 业务场景
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码响应
|
||||
*/
|
||||
export interface SendCodeResponse {
|
||||
cooldown_seconds: number // 冷却秒数,用于倒计时
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理扫码注册请求参数
|
||||
*/
|
||||
export interface AgentDistributionRegistrationParams {
|
||||
distribution_code: string // 分销码
|
||||
phone: string // 手机号
|
||||
code: string // 短信验证码
|
||||
password: string // 密码
|
||||
shop_name: string // 店铺名称
|
||||
shop_code: string // 店铺编号
|
||||
username: string // 用户名
|
||||
contact_name?: string // 联系人(选填)
|
||||
province?: string // 省(选填)
|
||||
city?: string // 市(选填)
|
||||
district?: string // 区县(选填)
|
||||
address?: string // 详细地址(选填)
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理扫码注册响应
|
||||
*/
|
||||
export interface AgentDistributionRegistrationResponse {
|
||||
id: number // 注册记录ID
|
||||
status: number // 状态(0 待审批)
|
||||
status_name: string // 状态名称
|
||||
}
|
||||
|
||||
export class AgentDistributionService extends BaseService {
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* POST /api/c/v1/auth/send-code(公开,不带 Token)
|
||||
*/
|
||||
static sendCode(params: SendCodeParams): Promise<BaseResponse<SendCodeResponse>> {
|
||||
return this.post<BaseResponse<SendCodeResponse>>('/api/c/v1/auth/send-code', params, {
|
||||
requestOptions: { withToken: false }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理扫码注册
|
||||
* POST /api/c/v1/agent-distribution-registrations(公开,不带 Token)
|
||||
*/
|
||||
static registerAgent(
|
||||
params: AgentDistributionRegistrationParams
|
||||
): Promise<BaseResponse<AgentDistributionRegistrationResponse>> {
|
||||
return this.post<BaseResponse<AgentDistributionRegistrationResponse>>(
|
||||
'/api/c/v1/agent-distribution-registrations',
|
||||
params,
|
||||
{ requestOptions: { withToken: false } }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import type {
|
||||
AgentRechargeListResponse,
|
||||
CreateAgentRechargeRequest,
|
||||
AgentRechargePaymentMethods,
|
||||
AgentRechargePaymentVoucherOcrRequest,
|
||||
AgentRechargePaymentVoucherOcrResponse,
|
||||
AgentRechargePaymentStatusResponse,
|
||||
ConfirmOfflinePaymentRequest,
|
||||
RejectAgentRechargeRequest,
|
||||
@@ -27,30 +25,6 @@ export class AgentRechargeService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理自充可用支付方式和金额限制
|
||||
* 仅代理与平台账号可用,超级管理员调用返回 403
|
||||
*/
|
||||
static getSelfRechargePaymentMethods(): Promise<BaseResponse<AgentRechargePaymentMethods>> {
|
||||
return this.get<BaseResponse<AgentRechargePaymentMethods>>(
|
||||
'/api/admin/agent-self-recharge-payment-methods'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别付款凭证中的交易流水号
|
||||
* 只返回预填值,识别较慢且失败不阻断人工填写
|
||||
*/
|
||||
static recognizePaymentVoucher(
|
||||
data: AgentRechargePaymentVoucherOcrRequest
|
||||
): Promise<BaseResponse<AgentRechargePaymentVoucherOcrResponse>> {
|
||||
return this.post<BaseResponse<AgentRechargePaymentVoucherOcrResponse>>(
|
||||
'/api/admin/agent-recharges/payment-voucher-ocr',
|
||||
data,
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理充值订单列表
|
||||
* @param params 查询参数
|
||||
|
||||
@@ -104,8 +104,7 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询该资产的历史套餐关系组(主套餐—加油包,分页)
|
||||
* total 仅统计顶层关系组,子项由后端随父项一起返回。
|
||||
* 查询该资产所有套餐记录,含虚流量换算字段(分页)
|
||||
* GET /api/admin/assets/:identifier/packages?page=1&page_size=50
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
* @param params 查询参数(可选分页参数)
|
||||
@@ -116,7 +115,12 @@ export class AssetService extends BaseService {
|
||||
): Promise<BaseResponse<AssetPackageListResponse>> {
|
||||
return this.get<BaseResponse<AssetPackageListResponse>>(
|
||||
`/api/admin/assets/${identifier}/packages`,
|
||||
params
|
||||
params,
|
||||
{
|
||||
requestOptions: {
|
||||
show404Error: false // 404时不显示错误提示
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
AssetAutoRenewalConfig,
|
||||
UpdateAssetAutoRenewalConfigRequest
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* 资产钱包相关服务
|
||||
*/
|
||||
export class AssetWalletService extends BaseService {
|
||||
/**
|
||||
* 查询自动续费配置
|
||||
*/
|
||||
static getAutoRenewalConfig(): Promise<BaseResponse<AssetAutoRenewalConfig>> {
|
||||
return this.get<BaseResponse<AssetAutoRenewalConfig>>('/api/admin/asset-auto-renewal-config')
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存自动续费配置
|
||||
*/
|
||||
static saveAutoRenewalConfig(
|
||||
data: UpdateAssetAutoRenewalConfigRequest
|
||||
): Promise<BaseResponse<AssetAutoRenewalConfig>> {
|
||||
return this.put<BaseResponse<AssetAutoRenewalConfig>>('/api/admin/asset-auto-renewal-config', data)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* 业务用户组相关 API - AUG26-003
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BusinessUserGroupItem,
|
||||
BusinessUserGroupMembersResult,
|
||||
BusinessUserGroupQueryParams,
|
||||
CreateBusinessUserGroupParams,
|
||||
DeleteBusinessUserGroupParams,
|
||||
SetBusinessUserGroupMembersParams,
|
||||
UpdateBusinessUserGroupParams,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class BusinessUserGroupService extends BaseService {
|
||||
/**
|
||||
* 查询业务用户组列表
|
||||
* GET /api/admin/business-user-groups
|
||||
*/
|
||||
static getGroups(
|
||||
params?: BusinessUserGroupQueryParams
|
||||
): Promise<PaginationResponse<BusinessUserGroupItem>> {
|
||||
return this.getPage<BusinessUserGroupItem>('/api/admin/business-user-groups', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询业务用户组详情
|
||||
* GET /api/admin/business-user-groups/{id}
|
||||
*/
|
||||
static getGroup(id: number): Promise<BaseResponse<BusinessUserGroupItem>> {
|
||||
return this.getOne<BusinessUserGroupItem>(`/api/admin/business-user-groups/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建业务用户组
|
||||
* POST /api/admin/business-user-groups
|
||||
*/
|
||||
static createGroup(
|
||||
data: CreateBusinessUserGroupParams
|
||||
): Promise<BaseResponse<BusinessUserGroupItem>> {
|
||||
return this.create<BusinessUserGroupItem>('/api/admin/business-user-groups', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新业务用户组
|
||||
* PUT /api/admin/business-user-groups/{id}
|
||||
*/
|
||||
static updateGroup(
|
||||
id: number,
|
||||
data: UpdateBusinessUserGroupParams
|
||||
): Promise<BaseResponse<BusinessUserGroupItem>> {
|
||||
return this.update<BusinessUserGroupItem>(`/api/admin/business-user-groups/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除业务用户组(必须携带 confirm body)
|
||||
* DELETE /api/admin/business-user-groups/{id}
|
||||
*/
|
||||
static deleteGroup(id: number, data: DeleteBusinessUserGroupParams): Promise<BaseResponse> {
|
||||
return this.delete<BaseResponse>(`/api/admin/business-user-groups/${id}`, undefined, { data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置平台用户业务用户组归属(整体替换)
|
||||
* PUT /api/admin/business-user-groups/{id}/members
|
||||
*/
|
||||
static setGroupMembers(
|
||||
id: number,
|
||||
data: SetBusinessUserGroupMembersParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||
return this.put<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||
`/api/admin/business-user-groups/${id}/members`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清空平台用户业务用户组归属(DELETE 带 body)
|
||||
* DELETE /api/admin/business-user-groups/members
|
||||
*/
|
||||
static clearGroupMembers(
|
||||
data: SetBusinessUserGroupMembersParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||
return this.delete<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||
'/api/admin/business-user-groups/members',
|
||||
undefined,
|
||||
{ data }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@ import type {
|
||||
CommissionRecordQueryParams,
|
||||
SubmitWithdrawalParams,
|
||||
ShopCommissionRecordPageResult,
|
||||
ShopCommissionRecordDetail,
|
||||
ShopCommissionRecordSource,
|
||||
ShopFundSummaryQueryParams,
|
||||
ShopFundSummaryPageResult,
|
||||
ShopCommissionStatsQueryParams,
|
||||
@@ -25,85 +23,10 @@ import type {
|
||||
DailyCommissionStatsItem,
|
||||
ResolveCommissionParams,
|
||||
MainWalletTransactionQueryParams,
|
||||
MainWalletTransactionPageResult,
|
||||
WithdrawalQualificationSubmitParams,
|
||||
WithdrawalQualificationQueryParams,
|
||||
WithdrawalQualificationPageResult,
|
||||
VoidWithdrawalQualificationParams,
|
||||
ResubmitWithdrawalRequestParams,
|
||||
WithdrawalRequestDetail,
|
||||
MainWalletTransactionPageResult
|
||||
} from '@/types/api/commission'
|
||||
|
||||
export class CommissionService extends BaseService {
|
||||
|
||||
// ==================== 提现资料资格相关 ====================
|
||||
|
||||
/**
|
||||
* 提交/替换提现资料资格
|
||||
* POST /api/admin/shops/{shop_id}/withdrawal-qualifications
|
||||
*/
|
||||
static submitWithdrawalQualification(
|
||||
shopId: number,
|
||||
data: WithdrawalQualificationSubmitParams
|
||||
): Promise<BaseResponse<{ id: number; status: number; status_name: string }>> {
|
||||
return this.post<BaseResponse<{ id: number; status: number; status_name: string }>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-qualifications`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询提现资料资格
|
||||
* GET /api/admin/shops/{shop_id}/withdrawal-qualifications
|
||||
*/
|
||||
static getWithdrawalQualifications(
|
||||
shopId: number,
|
||||
params?: WithdrawalQualificationQueryParams
|
||||
): Promise<BaseResponse<WithdrawalQualificationPageResult>> {
|
||||
return this.get<BaseResponse<WithdrawalQualificationPageResult>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-qualifications`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 作废提现资料资格(超管)
|
||||
* POST /api/admin/withdrawal-qualifications/{id}/void
|
||||
*/
|
||||
static voidWithdrawalQualification(
|
||||
id: number,
|
||||
data: VoidWithdrawalQualificationParams
|
||||
): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/withdrawal-qualifications/${id}/void`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重提被驳回的提现
|
||||
* PUT /api/admin/shops/{shop_id}/withdrawal-requests/{id}
|
||||
*/
|
||||
static resubmitWithdrawalRequest(
|
||||
shopId: number,
|
||||
id: number,
|
||||
data: ResubmitWithdrawalRequestParams
|
||||
): Promise<BaseResponse<WithdrawalRequestDetail>> {
|
||||
return this.put<BaseResponse<WithdrawalRequestDetail>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-requests/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现申请详情
|
||||
* GET /api/admin/shops/{shop_id}/withdrawal-requests/{id}
|
||||
*/
|
||||
static getWithdrawalRequestDetail(
|
||||
shopId: number,
|
||||
id: number
|
||||
): Promise<BaseResponse<WithdrawalRequestDetail>> {
|
||||
return this.get<BaseResponse<WithdrawalRequestDetail>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-requests/${id}`
|
||||
)
|
||||
}
|
||||
// ==================== 提现申请管理 ====================
|
||||
|
||||
/**
|
||||
@@ -184,22 +107,6 @@ export class CommissionService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取佣金明细详情
|
||||
* GET /api/admin/shops/{shop_id}/commission-records/{id}
|
||||
* source 省略时按原佣金处理;越权与不存在返回同一结果(佣金明细不存在)
|
||||
*/
|
||||
static getShopCommissionRecordDetail(
|
||||
shopId: number,
|
||||
id: number,
|
||||
source?: ShopCommissionRecordSource
|
||||
): Promise<BaseResponse<ShopCommissionRecordDetail>> {
|
||||
return this.get<BaseResponse<ShopCommissionRecordDetail>>(
|
||||
`/api/admin/shops/${shopId}/commission-records/${id}`,
|
||||
source ? { source } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理商提现记录
|
||||
* GET /api/admin/shops/{shop_id}/withdrawal-requests
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* 员工代收款相关 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
CloseEmployeeCollectionBillRequest,
|
||||
CloseEmployeeCollectionBillResponse,
|
||||
CreateEmployeeCollectionPaymentMethodRequest,
|
||||
DeleteEmployeeCollectionPaymentMethodResponse,
|
||||
EmployeeCollectionApplicationListResponse,
|
||||
EmployeeCollectionApplicationQueryParams,
|
||||
EmployeeCollectionApplicationRequest,
|
||||
EmployeeCollectionApplicationResponse,
|
||||
EmployeeCollectionApplicationSubmitResponse,
|
||||
EmployeeCollectionBillListResponse,
|
||||
EmployeeCollectionBillQueryParams,
|
||||
EmployeeCollectionBillResponse,
|
||||
EmployeeCollectionBillStatisticsResponse,
|
||||
EmployeeCollectionPaymentMethodListResponse,
|
||||
EmployeeCollectionPaymentMethodQueryParams,
|
||||
EmployeeCollectionPaymentMethodResponse,
|
||||
UpdateEmployeeCollectionPaymentMethodRequest
|
||||
} from '@/types/api'
|
||||
|
||||
const PAYMENT_METHODS_BASE_URL = '/api/admin/employee-collection-payment-methods'
|
||||
const BILLS_BASE_URL = '/api/admin/employee-collection-bills'
|
||||
const APPLICATIONS_BASE_URL = '/api/admin/employee-collection-applications'
|
||||
|
||||
export class EmployeeCollectionService extends BaseService {
|
||||
/**
|
||||
* 获取收款方式列表
|
||||
* 超级管理员返回全部,普通员工仅返回启用项
|
||||
*/
|
||||
static getPaymentMethods(
|
||||
params?: EmployeeCollectionPaymentMethodQueryParams
|
||||
): Promise<EmployeeCollectionPaymentMethodListResponse> {
|
||||
return this.get<EmployeeCollectionPaymentMethodListResponse>(PAYMENT_METHODS_BASE_URL, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增收款方式
|
||||
*/
|
||||
static createPaymentMethod(
|
||||
data: CreateEmployeeCollectionPaymentMethodRequest
|
||||
): Promise<EmployeeCollectionPaymentMethodResponse> {
|
||||
return this.post<EmployeeCollectionPaymentMethodResponse>(PAYMENT_METHODS_BASE_URL, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改收款方式(已被引用时不可修改 code)
|
||||
*/
|
||||
static updatePaymentMethod(
|
||||
id: number,
|
||||
data: UpdateEmployeeCollectionPaymentMethodRequest
|
||||
): Promise<EmployeeCollectionPaymentMethodResponse> {
|
||||
return this.put<EmployeeCollectionPaymentMethodResponse>(
|
||||
`${PAYMENT_METHODS_BASE_URL}/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除收款方式(已被引用时不可删除,只能停用)
|
||||
*/
|
||||
static deletePaymentMethod(id: number): Promise<DeleteEmployeeCollectionPaymentMethodResponse> {
|
||||
return this.delete<DeleteEmployeeCollectionPaymentMethodResponse>(
|
||||
`${PAYMENT_METHODS_BASE_URL}/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取员工代收款账单列表
|
||||
*/
|
||||
static getBills(
|
||||
params?: EmployeeCollectionBillQueryParams
|
||||
): Promise<EmployeeCollectionBillListResponse> {
|
||||
return this.get<EmployeeCollectionBillListResponse>(BILLS_BASE_URL, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单统计(应收/已核销/未核销/待处理数量)
|
||||
*/
|
||||
static getBillStatistics(
|
||||
params?: EmployeeCollectionBillQueryParams
|
||||
): Promise<EmployeeCollectionBillStatisticsResponse> {
|
||||
return this.get<EmployeeCollectionBillStatisticsResponse>(
|
||||
`${BILLS_BASE_URL}/statistics`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单详情(含退款冲销、核销分摊、关联申请、企微审批历史)
|
||||
*/
|
||||
static getBillById(id: number): Promise<EmployeeCollectionBillResponse> {
|
||||
return this.getOne<EmployeeCollectionBillResponse['data']>(`${BILLS_BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭账单(仅超级管理员,必须填写原因)
|
||||
*/
|
||||
static closeBill(
|
||||
id: number,
|
||||
data: CloseEmployeeCollectionBillRequest
|
||||
): Promise<CloseEmployeeCollectionBillResponse> {
|
||||
return this.post<CloseEmployeeCollectionBillResponse>(`${BILLS_BASE_URL}/${id}/close`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建核销申请(提交后自动发起企微审批)
|
||||
*/
|
||||
static createApplication(
|
||||
data: EmployeeCollectionApplicationRequest
|
||||
): Promise<EmployeeCollectionApplicationSubmitResponse> {
|
||||
return this.post<EmployeeCollectionApplicationSubmitResponse>(APPLICATIONS_BASE_URL, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取核销申请列表
|
||||
*/
|
||||
static getApplications(
|
||||
params?: EmployeeCollectionApplicationQueryParams
|
||||
): Promise<EmployeeCollectionApplicationListResponse> {
|
||||
return this.get<EmployeeCollectionApplicationListResponse>(APPLICATIONS_BASE_URL, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取核销申请详情(含分摊账单、付款凭证、审批意见与提交历史)
|
||||
*/
|
||||
static getApplicationById(id: number): Promise<EmployeeCollectionApplicationResponse> {
|
||||
return this.getOne<EmployeeCollectionApplicationResponse['data']>(
|
||||
`${APPLICATIONS_BASE_URL}/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回后修改并重新提交(生成新的企微审批实例)
|
||||
*/
|
||||
static updateApplication(
|
||||
id: number,
|
||||
data: EmployeeCollectionApplicationRequest
|
||||
): Promise<EmployeeCollectionApplicationSubmitResponse> {
|
||||
return this.put<EmployeeCollectionApplicationSubmitResponse>(
|
||||
`${APPLICATIONS_BASE_URL}/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import type { BaseResponse } from '@/types/api'
|
||||
export type ExchangeAssetType = 'iot_card' | 'device'
|
||||
export type ExchangeFlowType = 'shipping' | 'direct'
|
||||
export type ExchangeStatus = 1 | 2 | 3 | 4 | 5
|
||||
export type ExchangeMigrationStatus = 'not_migrated' | 'pending' | 'migrated' | 'failed'
|
||||
|
||||
// 换货单查询参数
|
||||
export interface ExchangeQueryParams {
|
||||
@@ -65,9 +64,6 @@ export interface ExchangeResponse {
|
||||
current_approver_summary?: string | null // 当前审批人摘要
|
||||
processing_status?: string | null // 业务处理状态
|
||||
processing_status_name?: string | null // 业务处理状态名称
|
||||
migration_status?: ExchangeMigrationStatus // 迁移状态(not_migrated/pending/migrated/failed)
|
||||
migration_status_name?: string // 迁移状态中文名
|
||||
migration_failure_reason?: string | null // 迁移失败原因(仅 migration_status=failed 时返回)
|
||||
remark?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* H5 运营弹窗配置相关 API
|
||||
* 契约来源:docs/产品迭代8月份/通知.md
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
CreateH5PopupConfigurationRequest,
|
||||
H5PopupConfiguration,
|
||||
H5PopupConfigurationListResponse,
|
||||
H5PopupConfigurationQueryParams,
|
||||
UpdateH5PopupConfigurationRequest
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* H5 运营弹窗配置服务
|
||||
* 仅超级管理员和平台账号可访问,代理/企业/个人由后端 403 兜底。
|
||||
*/
|
||||
export class H5PopupConfigurationService extends BaseService {
|
||||
/**
|
||||
* 查询运营弹窗配置列表(分页,按最近更新时间倒序)
|
||||
* GET /api/admin/h5-popup-configurations
|
||||
*/
|
||||
static getConfigurations(
|
||||
params?: H5PopupConfigurationQueryParams
|
||||
): Promise<BaseResponse<H5PopupConfigurationListResponse>> {
|
||||
return this.get<BaseResponse<H5PopupConfigurationListResponse>>(
|
||||
'/api/admin/h5-popup-configurations',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建运营弹窗配置
|
||||
* POST /api/admin/h5-popup-configurations
|
||||
*/
|
||||
static createConfiguration(
|
||||
data: CreateH5PopupConfigurationRequest
|
||||
): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.post<BaseResponse<H5PopupConfiguration>>('/api/admin/h5-popup-configurations', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询运营弹窗配置详情
|
||||
* GET /api/admin/h5-popup-configurations/{id}
|
||||
*/
|
||||
static getConfiguration(id: number): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.getOne<H5PopupConfiguration>(`/api/admin/h5-popup-configurations/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运营弹窗配置(成功即版本 +1)
|
||||
* PUT /api/admin/h5-popup-configurations/{id}
|
||||
*/
|
||||
static updateConfiguration(
|
||||
id: number,
|
||||
data: UpdateH5PopupConfigurationRequest
|
||||
): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.put<BaseResponse<H5PopupConfiguration>>(
|
||||
`/api/admin/h5-popup-configurations/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用运营弹窗配置(刷新最近更新时间,不递增版本)
|
||||
* POST /api/admin/h5-popup-configurations/{id}/enable
|
||||
*/
|
||||
static enableConfiguration(id: number): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.post<BaseResponse<H5PopupConfiguration>>(
|
||||
`/api/admin/h5-popup-configurations/${id}/enable`,
|
||||
{ id }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用运营弹窗配置(刷新最近更新时间,不递增版本)
|
||||
* POST /api/admin/h5-popup-configurations/{id}/disable
|
||||
*/
|
||||
static disableConfiguration(id: number): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.post<BaseResponse<H5PopupConfiguration>>(
|
||||
`/api/admin/h5-popup-configurations/${id}/disable`,
|
||||
{ id }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export { AccountService } from './account'
|
||||
export { ShopService } from './shop'
|
||||
export { CardService } from './card'
|
||||
export { CommissionService } from './commission'
|
||||
export { BusinessUserGroupService } from './businessUserGroup'
|
||||
export { EnterpriseService } from './enterprise'
|
||||
export { StorageService } from './storage'
|
||||
export { AuthorizationService } from './authorization'
|
||||
@@ -24,7 +23,6 @@ export { OrderService } from './order'
|
||||
export { AssetService } from './asset'
|
||||
export { AgentRechargeService } from './agentRecharge'
|
||||
export { PaymentSettingsService } from './paymentSettings'
|
||||
export { PaymentMerchantPoolsService } from './paymentMerchantPools'
|
||||
export { SystemConfigService } from './systemConfig'
|
||||
export { ExchangeService } from './exchange'
|
||||
export { RefundService } from './refund'
|
||||
@@ -41,19 +39,6 @@ export { BulkPurchaseService } from './bulkPurchase'
|
||||
export { NotificationService } from './notification'
|
||||
export { AuditService } from './audit'
|
||||
export { WecomService } from './wecom'
|
||||
export { EmployeeCollectionService } from './employeeCollection'
|
||||
|
||||
export { PhoneAssetAssociationService } from './phoneAsset'
|
||||
|
||||
export { AgentDistributionService } from './agentDistribution'
|
||||
|
||||
export { H5PopupConfigurationService } from './h5PopupConfiguration'
|
||||
|
||||
// TODO: 按需添加其他业务模块
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
export { PackageTrafficAlertService } from './packageTrafficAlert'
|
||||
|
||||
export { PollingPriorityQueueService } from './pollingPriorityQueue'
|
||||
|
||||
export { AssetWalletService } from './assetWallet'
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* 套餐真流量预警 API 服务
|
||||
* 6 个接口仅超级管理员/平台账号可访问,其他账号由后端 403 兜底
|
||||
* 对应 docs/产品迭代8月份/套餐真流量预警_API_前端简版.md
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
PaginationResponse,
|
||||
PackageTrafficAlertRuleItem,
|
||||
PackageTrafficAlertRuleQueryParams,
|
||||
CreatePackageTrafficAlertRuleParams,
|
||||
UpdatePackageTrafficAlertRuleParams,
|
||||
PackageTrafficAlertRecordItem,
|
||||
PackageTrafficAlertRecordDetail,
|
||||
PackageTrafficAlertRecordQueryParams,
|
||||
ExportPackageTrafficAlertParams,
|
||||
CreatePackageTrafficAlertExportResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class PackageTrafficAlertService extends BaseService {
|
||||
/**
|
||||
* 查询套餐真流量预警规则列表
|
||||
* GET /api/admin/package-traffic-alert-rules
|
||||
* 返回套餐、商品当前真流量额度、阈值、启用状态、备注与更新时间
|
||||
*/
|
||||
static getAlertRules(
|
||||
params?: PackageTrafficAlertRuleQueryParams
|
||||
): Promise<PaginationResponse<PackageTrafficAlertRuleItem>> {
|
||||
return this.getPage<PackageTrafficAlertRuleItem>(
|
||||
'/api/admin/package-traffic-alert-rules',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐真流量预警规则
|
||||
* POST /api/admin/package-traffic-alert-rules
|
||||
* 同一套餐商品最多一条真流量预警规则
|
||||
*/
|
||||
static createAlertRule(
|
||||
data: CreatePackageTrafficAlertRuleParams
|
||||
): Promise<BaseResponse<PackageTrafficAlertRuleItem>> {
|
||||
return this.create<PackageTrafficAlertRuleItem>('/api/admin/package-traffic-alert-rules', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改套餐真流量预警规则
|
||||
* PUT /api/admin/package-traffic-alert-rules/{id}
|
||||
* 允许修改阈值、启停与备注;修改不影响既有预警快照,停用后扫描不再创建新预警
|
||||
*/
|
||||
static updateAlertRule(
|
||||
id: number,
|
||||
data: UpdatePackageTrafficAlertRuleParams
|
||||
): Promise<BaseResponse<PackageTrafficAlertRuleItem>> {
|
||||
return this.update<PackageTrafficAlertRuleItem>(
|
||||
`/api/admin/package-traffic-alert-rules/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐真流量预警记录列表
|
||||
* GET /api/admin/package-traffic-alerts
|
||||
* 列表数据除 business_user_group_names 外均为触发时快照
|
||||
*/
|
||||
static getAlertRecords(
|
||||
params?: PackageTrafficAlertRecordQueryParams
|
||||
): Promise<PaginationResponse<PackageTrafficAlertRecordItem>> {
|
||||
return this.getPage<PackageTrafficAlertRecordItem>('/api/admin/package-traffic-alerts', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐真流量预警记录详情
|
||||
* GET /api/admin/package-traffic-alerts/{id}
|
||||
* 越权查询统一按资源不可见处理
|
||||
*/
|
||||
static getAlertRecordDetail(id: number): Promise<BaseResponse<PackageTrafficAlertRecordDetail>> {
|
||||
// 越权查询详情统一按资源不可见处理,抑制全局报错提示,由页面给出统一文案
|
||||
return this.getOne<PackageTrafficAlertRecordDetail>(
|
||||
`/api/admin/package-traffic-alerts/${id}`,
|
||||
undefined,
|
||||
{ requestOptions: { errorMessageMode: 'none' } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出套餐真流量达量预警
|
||||
* POST /api/admin/package-traffic-alerts/export
|
||||
* 只创建异步导出任务,后续通过既有导出任务列表下载文件
|
||||
*/
|
||||
static exportAlertRecords(
|
||||
data: ExportPackageTrafficAlertParams
|
||||
): Promise<BaseResponse<CreatePackageTrafficAlertExportResponse>> {
|
||||
return this.post<BaseResponse<CreatePackageTrafficAlertExportResponse>>(
|
||||
'/api/admin/package-traffic-alerts/export',
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
/**
|
||||
* 支付商户、商户池及微信授权配置 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
CreatePaymentMerchantRequest,
|
||||
PaymentMerchant,
|
||||
PaymentCredentialValue,
|
||||
PaymentCredentials,
|
||||
PaymentMerchantDetail,
|
||||
PaymentMerchantDetailResponse,
|
||||
PaymentMerchantPageResponse,
|
||||
PaymentMerchantPageResult,
|
||||
PaymentMerchantProviderType,
|
||||
PaymentMerchantPool,
|
||||
PaymentMerchantPoolPageResponse,
|
||||
PaymentMerchantPoolPayload,
|
||||
PaymentMerchantPoolQueryParams,
|
||||
PaymentMerchantQueryParams,
|
||||
PaymentMerchantResponse,
|
||||
UpdatePaymentMerchantRequest,
|
||||
UpdateWechatAuthorizationRequest,
|
||||
WechatAuthorizationConfig,
|
||||
WechatAuthorizationResponse
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
|
||||
const PAYMENT_MERCHANTS_BASE_URL = '/api/admin/payment-merchants'
|
||||
const PAYMENT_MERCHANT_POOLS_BASE_URL = '/api/admin/payment-merchant-pools'
|
||||
const WECHAT_AUTHORIZATIONS_BASE_URL = '/api/admin/wechat-authorizations'
|
||||
|
||||
type RawPaymentMerchant = PaymentMerchant & { credentials?: unknown }
|
||||
|
||||
type RawPaymentMerchantDetail = PaymentMerchant & { credentials?: Record<string, unknown> }
|
||||
|
||||
const MERCHANT_CREDENTIAL_DISPLAY_KEYS: Record<PaymentMerchantProviderType, string[]> = {
|
||||
wechat: ['wx_mch_id', 'wx_serial_no', 'wx_notify_url'],
|
||||
wechat_v2: ['wx_mch_id', 'wx_serial_no', 'wx_notify_url'],
|
||||
fuiou: ['fy_api_url', 'fy_ins_cd', 'fy_mchnt_cd', 'fy_term_id', 'fy_notify_url'],
|
||||
alipay: [
|
||||
'ali_app_id',
|
||||
'ali_notify_url',
|
||||
'ali_return_url',
|
||||
'ali_pay_expire_minutes',
|
||||
'ali_production'
|
||||
]
|
||||
}
|
||||
|
||||
const MERCHANT_CREDENTIAL_STATUS_KEYS: Record<PaymentMerchantProviderType, string[]> = {
|
||||
wechat: ['wx_api_v2_key', 'wx_api_v3_key', 'wx_cert_content', 'wx_key_content'],
|
||||
wechat_v2: ['wx_api_v2_key', 'wx_api_v3_key', 'wx_cert_content', 'wx_key_content'],
|
||||
fuiou: ['fy_private_key', 'fy_public_key'],
|
||||
alipay: ['ali_private_key', 'ali_public_key']
|
||||
}
|
||||
|
||||
const sanitizeMerchant = (merchant: RawPaymentMerchant): PaymentMerchant => {
|
||||
// 仅通过解构丢弃 credentials 字段,避免在前端页面/状态中保留支付凭证明文
|
||||
const { credentials: _ignoredCredentials, ...safeMerchant } = merchant
|
||||
void _ignoredCredentials
|
||||
return safeMerchant
|
||||
}
|
||||
|
||||
const sanitizeMerchantDetail = (merchant: RawPaymentMerchantDetail): PaymentMerchantDetail => {
|
||||
const { credentials: rawCredentials, ...baseMerchant } = merchant
|
||||
const credentials: PaymentCredentials = {}
|
||||
const provider = merchant.provider_type
|
||||
const source = rawCredentials || {}
|
||||
|
||||
for (const key of MERCHANT_CREDENTIAL_DISPLAY_KEYS[provider] || []) {
|
||||
const value = source[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
credentials[key] = value as PaymentCredentialValue
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of MERCHANT_CREDENTIAL_STATUS_KEYS[provider] || []) {
|
||||
const value = source[key]
|
||||
credentials[key] = value ? '已配置' : '未配置'
|
||||
}
|
||||
|
||||
return {
|
||||
...baseMerchant,
|
||||
credentials
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeMerchantPage = (page: PaymentMerchantPageResult): PaymentMerchantPageResult => ({
|
||||
...page,
|
||||
items: (page.items || []).map((item) => sanitizeMerchant(item as RawPaymentMerchant))
|
||||
})
|
||||
|
||||
const sanitizeWechatAuthorization = (
|
||||
config: WechatAuthorizationConfig & Record<string, unknown>
|
||||
): WechatAuthorizationConfig => {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// 仅通过解构丢弃敏感字段,禁止在前端响应中保留 AppSecret/Token/AES Key 等明文
|
||||
const {
|
||||
miniapp_app_secret: _miniappAppSecret,
|
||||
oa_app_secret: _oaAppSecret,
|
||||
oa_token: _oaToken,
|
||||
oa_aes_key: _oaAesKey,
|
||||
...safeConfig
|
||||
} = config
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
return safeConfig as WechatAuthorizationConfig
|
||||
}
|
||||
|
||||
export class PaymentMerchantPoolsService extends BaseService {
|
||||
static getPaymentMerchants(
|
||||
params?: PaymentMerchantQueryParams
|
||||
): Promise<PaymentMerchantPageResponse> {
|
||||
return this.get<PaymentMerchantPageResponse>(PAYMENT_MERCHANTS_BASE_URL, params).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchantPage(response.data)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static getPaymentMerchantById(id: number): Promise<PaymentMerchantResponse> {
|
||||
return this.get<PaymentMerchantResponse>(`${PAYMENT_MERCHANTS_BASE_URL}/${id}`).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchant(response.data as RawPaymentMerchant)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static getPaymentMerchantDetailById(id: number): Promise<PaymentMerchantDetailResponse> {
|
||||
return this.get<PaymentMerchantDetailResponse>(`${PAYMENT_MERCHANTS_BASE_URL}/${id}`).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchantDetail(response.data as unknown as RawPaymentMerchantDetail)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static createPaymentMerchant(
|
||||
data: CreatePaymentMerchantRequest
|
||||
): Promise<PaymentMerchantResponse> {
|
||||
return this.post<PaymentMerchantResponse>(PAYMENT_MERCHANTS_BASE_URL, data).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchant(response.data as RawPaymentMerchant)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static updatePaymentMerchant(
|
||||
id: number,
|
||||
data: UpdatePaymentMerchantRequest
|
||||
): Promise<PaymentMerchantResponse> {
|
||||
return this.put<PaymentMerchantResponse>(`${PAYMENT_MERCHANTS_BASE_URL}/${id}`, data).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchant(response.data as RawPaymentMerchant)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static deletePaymentMerchant(id: number): Promise<BaseResponse<void>> {
|
||||
return this.delete<BaseResponse<void>>(`${PAYMENT_MERCHANTS_BASE_URL}/${id}`, undefined, {
|
||||
data: { confirm: true }
|
||||
})
|
||||
}
|
||||
|
||||
static getPaymentMerchantPools(
|
||||
params?: PaymentMerchantPoolQueryParams
|
||||
): Promise<PaymentMerchantPoolPageResponse> {
|
||||
return this.get<PaymentMerchantPoolPageResponse>(PAYMENT_MERCHANT_POOLS_BASE_URL, params)
|
||||
}
|
||||
|
||||
static getPaymentMerchantPoolById(id: number): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.get<BaseResponse<PaymentMerchantPool>>(`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
static createPaymentMerchantPool(
|
||||
data: PaymentMerchantPoolPayload
|
||||
): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.post<BaseResponse<PaymentMerchantPool>>(PAYMENT_MERCHANT_POOLS_BASE_URL, data)
|
||||
}
|
||||
|
||||
static updatePaymentMerchantPool(
|
||||
id: number,
|
||||
data: PaymentMerchantPoolPayload
|
||||
): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.put<BaseResponse<PaymentMerchantPool>>(
|
||||
`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
static enablePaymentMerchantPool(id: number): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.post<BaseResponse<PaymentMerchantPool>>(
|
||||
`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}/enable`
|
||||
)
|
||||
}
|
||||
|
||||
static disablePaymentMerchantPool(id: number): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.post<BaseResponse<PaymentMerchantPool>>(
|
||||
`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}/disable`
|
||||
)
|
||||
}
|
||||
|
||||
static getWechatAuthorization(): Promise<WechatAuthorizationResponse> {
|
||||
return this.get<WechatAuthorizationResponse>(WECHAT_AUTHORIZATIONS_BASE_URL).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeWechatAuthorization(
|
||||
response.data as WechatAuthorizationConfig & Record<string, unknown>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static updateWechatAuthorization(
|
||||
data: UpdateWechatAuthorizationRequest
|
||||
): Promise<WechatAuthorizationResponse> {
|
||||
return this.put<WechatAuthorizationResponse>(
|
||||
`${WECHAT_AUTHORIZATIONS_BASE_URL}/current`,
|
||||
data
|
||||
).then((response) => ({
|
||||
...response,
|
||||
data: sanitizeWechatAuthorization(
|
||||
response.data as WechatAuthorizationConfig & Record<string, unknown>
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/**
|
||||
* 手机号资产关联相关 API
|
||||
* 契约来源:docs/产品迭代8月份/手机号资产关联.md
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
PhoneAssetAssociationListResponse,
|
||||
PhoneAssetAssociationQueryParams,
|
||||
BatchUnbindPhoneAssetAssociationRequest,
|
||||
BatchUnbindPhoneAssetAssociationResponse,
|
||||
CreatePhoneAssetUnbindImportRequest,
|
||||
PhoneAssetUnbindImportTask,
|
||||
PhoneAssetUnbindImportTaskDetail,
|
||||
PhoneAssetUnbindImportTaskListResponse,
|
||||
PhoneAssetUnbindImportTaskQueryParams,
|
||||
UnbindPhoneAssetAssociationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* 手机号资产关联服务
|
||||
* 仅超级管理员和平台账号可访问,代理/企业/个人由后端 403 兜底。
|
||||
*/
|
||||
export class PhoneAssetAssociationService extends BaseService {
|
||||
/**
|
||||
* 查询手机号资产关联列表(分页)
|
||||
* GET /api/admin/phone-asset-associations
|
||||
*/
|
||||
static getAssociations(
|
||||
params?: PhoneAssetAssociationQueryParams
|
||||
): Promise<BaseResponse<PhoneAssetAssociationListResponse>> {
|
||||
return this.get<BaseResponse<PhoneAssetAssociationListResponse>>(
|
||||
'/api/admin/phone-asset-associations',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除单条手机号资产关联
|
||||
* DELETE /api/admin/phone-asset-associations/{id}
|
||||
*
|
||||
* 注意:接口文档未定义请求体,reason 与 confirmed 当前按查询参数传递;
|
||||
* 联调时若后端以 JSON body 接收,需调整为请求体方式。
|
||||
*/
|
||||
static unbindAssociation(
|
||||
id: number,
|
||||
reason: string,
|
||||
confirmed: boolean
|
||||
): Promise<BaseResponse<UnbindPhoneAssetAssociationResponse>> {
|
||||
return this.delete<BaseResponse<UnbindPhoneAssetAssociationResponse>>(
|
||||
`/api/admin/phone-asset-associations/${id}`,
|
||||
{ reason, confirmed }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按资产批量解除手机号关联
|
||||
* POST /api/admin/phone-asset-associations/batch-unbind
|
||||
*/
|
||||
static batchUnbind(
|
||||
data: BatchUnbindPhoneAssetAssociationRequest
|
||||
): Promise<BaseResponse<BatchUnbindPhoneAssetAssociationResponse>> {
|
||||
return this.post<BaseResponse<BatchUnbindPhoneAssetAssociationResponse>>(
|
||||
'/api/admin/phone-asset-associations/batch-unbind',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 CSV 解绑导入任务
|
||||
* POST /api/admin/phone-asset-associations/unbind-imports
|
||||
*/
|
||||
static createUnbindImport(
|
||||
data: CreatePhoneAssetUnbindImportRequest
|
||||
): Promise<BaseResponse<PhoneAssetUnbindImportTask>> {
|
||||
return this.post<BaseResponse<PhoneAssetUnbindImportTask>>(
|
||||
'/api/admin/phone-asset-associations/unbind-imports',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询解绑导入任务列表(分页)
|
||||
* GET /api/admin/phone-asset-associations/unbind-imports
|
||||
*/
|
||||
static getUnbindImportTasks(
|
||||
params?: PhoneAssetUnbindImportTaskQueryParams
|
||||
): Promise<BaseResponse<PhoneAssetUnbindImportTaskListResponse>> {
|
||||
return this.get<BaseResponse<PhoneAssetUnbindImportTaskListResponse>>(
|
||||
'/api/admin/phone-asset-associations/unbind-imports',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询解绑导入任务详情
|
||||
* GET /api/admin/phone-asset-associations/unbind-imports/{id}
|
||||
*/
|
||||
static getUnbindImportTaskDetail(
|
||||
id: number
|
||||
): Promise<BaseResponse<PhoneAssetUnbindImportTaskDetail>> {
|
||||
return this.getOne<PhoneAssetUnbindImportTaskDetail>(
|
||||
`/api/admin/phone-asset-associations/unbind-imports/${id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* 轮询优先队列 API
|
||||
*/
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
PollingPriorityItem,
|
||||
PollingPriorityItemPageResult,
|
||||
PollingPriorityItemQueryParams,
|
||||
CreatePollingPriorityItemRequest,
|
||||
CreatePollingPriorityItemResponse
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* 轮询优先队列服务
|
||||
*/
|
||||
export class PollingPriorityQueueService extends BaseService {
|
||||
/**
|
||||
* 获取优先轮询项列表
|
||||
* GET /api/admin/polling-priority-items
|
||||
* 按创建时间倒序返回,支持卡ID、任务类型、状态、触发类型筛选
|
||||
*/
|
||||
static getPriorityItems(
|
||||
params?: PollingPriorityItemQueryParams
|
||||
): Promise<BaseResponse<PollingPriorityItemPageResult>> {
|
||||
return this.get<BaseResponse<PollingPriorityItemPageResult>>(
|
||||
'/api/admin/polling-priority-items',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工优先入队
|
||||
* POST /api/admin/polling-priority-items
|
||||
* 请求体 { card_id, reason },为卡片全部范围内的轮询任务类型创建或合并优先项
|
||||
*/
|
||||
static createPriorityItems(
|
||||
data: CreatePollingPriorityItemRequest
|
||||
): Promise<BaseResponse<CreatePollingPriorityItemResponse>> {
|
||||
return this.post<BaseResponse<CreatePollingPriorityItemResponse>>(
|
||||
'/api/admin/polling-priority-items',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取优先轮询项详情
|
||||
* GET /api/admin/polling-priority-items/{id}
|
||||
*/
|
||||
static getPriorityItemDetail(id: number): Promise<BaseResponse<PollingPriorityItem>> {
|
||||
return this.get<BaseResponse<PollingPriorityItem>>(`/api/admin/polling-priority-items/${id}`)
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@ import type {
|
||||
RefundListResponse,
|
||||
CreateRefundRequest,
|
||||
ResubmitRefundRequest,
|
||||
ApproveRefundRequest,
|
||||
RejectRefundRequest,
|
||||
ReturnRefundRequest,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
@@ -40,33 +37,6 @@ export class RefundService extends BaseService {
|
||||
return this.post<BaseResponse<Refund>>('/api/admin/refunds', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过退款申请(仅存量无企微审批实例的申请可用)
|
||||
* @param id 退款申请ID
|
||||
* @param data 审批通过请求参数
|
||||
*/
|
||||
static approveRefund(id: number, data: ApproveRefundRequest): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/approve`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批拒绝退款申请(仅存量无企微审批实例的申请可用)
|
||||
* @param id 退款申请ID
|
||||
* @param data 拒绝请求参数
|
||||
*/
|
||||
static rejectRefund(id: number, data: RejectRefundRequest): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/reject`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退回退款申请(仅存量无企微审批实例的申请可用)
|
||||
* @param id 退款申请ID
|
||||
* @param data 退回请求参数
|
||||
*/
|
||||
static returnRefund(id: number, data: ReturnRefundRequest): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/return`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新提交退款申请
|
||||
* @param id 退款申请ID
|
||||
|
||||
@@ -12,11 +12,6 @@ import type {
|
||||
AssignShopRolesRequest,
|
||||
UpdateShopCreditLimitRequest,
|
||||
UpdateShopCreditLimitResponse,
|
||||
BatchUpdateBusinessOwnerParams,
|
||||
BatchUpdateBusinessOwnerResult,
|
||||
CreateBusinessOwnerImportParams,
|
||||
BusinessOwnerImportDetail,
|
||||
BusinessOwnerImportQueryParams,
|
||||
BaseResponse,
|
||||
PaginationResponse,
|
||||
ShopBusinessOwnerCandidate,
|
||||
@@ -160,56 +155,4 @@ export class ShopService extends BaseService {
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 批量交接平台业务员 ==========
|
||||
|
||||
/**
|
||||
* 勾选批量交接平台业务员
|
||||
* PUT /api/admin/shops/business-owner/batch
|
||||
* @param data shop_ids(1-500)+ business_owner_account_id(null=清空)
|
||||
*/
|
||||
static batchUpdateBusinessOwner(
|
||||
data: BatchUpdateBusinessOwnerParams
|
||||
): Promise<BaseResponse<BatchUpdateBusinessOwnerResult>> {
|
||||
return this.put<BaseResponse<BatchUpdateBusinessOwnerResult>>(
|
||||
'/api/admin/shops/business-owner/batch',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 业务负责人 CSV 导入 ==========
|
||||
|
||||
/**
|
||||
* 创建业务负责人导入任务
|
||||
* POST /api/admin/shops/business-owner-imports
|
||||
* @param data 上传后拿到的 file_key
|
||||
*/
|
||||
static createBusinessOwnerImport(
|
||||
data: CreateBusinessOwnerImportParams
|
||||
): Promise<BaseResponse<BusinessOwnerImportDetail>> {
|
||||
return this.create<BusinessOwnerImportDetail>('/api/admin/shops/business-owner-imports', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询业务负责人导入任务详情
|
||||
* GET /api/admin/shops/business-owner-imports/{id}
|
||||
*/
|
||||
static getBusinessOwnerImportDetail(
|
||||
id: number
|
||||
): Promise<BaseResponse<BusinessOwnerImportDetail>> {
|
||||
return this.getOne<BusinessOwnerImportDetail>(`/api/admin/shops/business-owner-imports/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询业务负责人导入任务列表
|
||||
* GET /api/admin/shops/business-owner-imports
|
||||
*/
|
||||
static getBusinessOwnerImportList(
|
||||
params?: BusinessOwnerImportQueryParams
|
||||
): Promise<PaginationResponse<BusinessOwnerImportDetail>> {
|
||||
return this.getPage<BusinessOwnerImportDetail>(
|
||||
'/api/admin/shops/business-owner-imports',
|
||||
params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ export type FilePurpose =
|
||||
| 'iot_import'
|
||||
| 'batch_purchase'
|
||||
| 'device_batch_allocation'
|
||||
| 'shop_import'
|
||||
| 'phone_unbind_import'
|
||||
| 'export'
|
||||
| 'attachment'
|
||||
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
title="佣金明细详情"
|
||||
width="780px"
|
||||
@update:model-value="handleVisibleChange"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<div v-loading="loading" class="commission-record-detail">
|
||||
<template v-if="record">
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="记录类型">
|
||||
<ElTag :type="isClawback ? 'danger' : 'primary'" size="small">
|
||||
{{ recordSourceName }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="statusTagType" size="small">
|
||||
{{ record.status_name || statusName }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金金额">
|
||||
<span :class="{ 'amount-negative': isNegative(record.amount) }">
|
||||
{{ formatMoney(record.amount) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="入账后佣金余额">
|
||||
<span :class="{ 'amount-negative': isNegative(record.balance_after) }">
|
||||
{{ formatMoney(record.balance_after) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金来源">
|
||||
<ElTag :type="sourceTagType" size="small">{{ sourceName }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="是否可提现">
|
||||
<ElTag v-if="isClawback" type="danger" size="small">不可提现</ElTag>
|
||||
<span v-else>可提现</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="订单号">{{ record.order_no || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卖家店铺">
|
||||
{{ record.seller_shop_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="ICCID">{{ record.iccid || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备虚拟号">
|
||||
{{ record.virtual_no || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="记录生成时间">
|
||||
{{ record.created_at ? formatDateTime(record.created_at) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金入账时间">
|
||||
{{ record.released_at ? formatDateTime(record.released_at) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<template v-if="isClawback">
|
||||
<ElDescriptionsItem label="被回溯原佣金ID">
|
||||
{{ record.original_commission_id ?? '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="来源退款单号">
|
||||
{{ record.refund_no || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
</template>
|
||||
</ElDescriptions>
|
||||
|
||||
<template v-if="!isClawback">
|
||||
<div class="detail-section-title">
|
||||
回溯明细
|
||||
<span class="detail-section-summary">
|
||||
累计回溯金额:{{ formatMoney(clawbackTotalAmount) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElTable :data="clawbackRecords" size="small" border :max-height="260">
|
||||
<ElTableColumn label="回溯金额" prop="amount" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-negative">{{ formatMoney(row.amount) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="回溯后余额" prop="balance_after" width="130">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'amount-negative': isNegative(row.balance_after) }">
|
||||
{{ formatMoney(row.balance_after) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
label="来源退款单号"
|
||||
prop="refund_no"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.refund_no || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status_name" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag type="danger" size="small">{{ row.status_name || '回溯' }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="生成时间" prop="created_at" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.created_at ? formatDateTime(row.created_at) : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
|
||||
<template v-else-if="originalCommission">
|
||||
<div class="detail-section-title">来源原佣金</div>
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="原佣金记录ID">
|
||||
{{ originalCommission.id }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="原佣金金额">
|
||||
{{ formatMoney(originalCommission.amount) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="原佣金余额">
|
||||
{{ formatMoney(originalCommission.balance_after) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="原佣金状态">
|
||||
{{ originalCommission.status_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="订单号">
|
||||
{{ originalCommission.order_no || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金入账时间">
|
||||
{{
|
||||
originalCommission.released_at
|
||||
? formatDateTime(originalCommission.released_at)
|
||||
: '-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</template>
|
||||
</template>
|
||||
<ElEmpty v-else-if="!loading" description="佣金明细不存在" />
|
||||
</div>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CommissionService } from '@/api/modules'
|
||||
import type { ShopCommissionRecordDetail, ShopCommissionRecordSource } from '@/types/api'
|
||||
import {
|
||||
COMMISSION_RECORD_SOURCE_MAP,
|
||||
CommissionSourceMap,
|
||||
CommissionStatusMap,
|
||||
isCommissionClawbackRecord
|
||||
} from '@/config/constants/commission'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
|
||||
defineOptions({ name: 'CommissionRecordDetailDialog' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
shopId?: number | null
|
||||
recordId?: number | null
|
||||
source?: ShopCommissionRecordSource
|
||||
}>(),
|
||||
{
|
||||
shopId: null,
|
||||
recordId: null,
|
||||
source: 'original'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<ShopCommissionRecordDetail | null>(null)
|
||||
|
||||
const record = computed(() => detail.value?.record || null)
|
||||
const isClawback = computed(() =>
|
||||
isCommissionClawbackRecord({
|
||||
source: detail.value?.source || props.source,
|
||||
status: detail.value?.record?.status
|
||||
})
|
||||
)
|
||||
const commissionStatusMeta = computed(
|
||||
() =>
|
||||
CommissionStatusMap[Number(record.value?.status) as keyof typeof CommissionStatusMap] || null
|
||||
)
|
||||
const commissionSourceMeta = computed(
|
||||
() =>
|
||||
CommissionSourceMap[
|
||||
(record.value?.commission_source || '') as keyof typeof CommissionSourceMap
|
||||
] || null
|
||||
)
|
||||
const recordSourceMeta = computed(
|
||||
() => COMMISSION_RECORD_SOURCE_MAP[isClawback.value ? 'clawback' : 'original']
|
||||
)
|
||||
const recordSourceName = computed(() => recordSourceMeta.value.label)
|
||||
const statusName = computed(() => commissionStatusMeta.value?.label || '-')
|
||||
const statusTagType = computed(() => commissionStatusMeta.value?.type || 'info')
|
||||
const sourceName = computed(
|
||||
() => commissionSourceMeta.value?.label || record.value?.commission_source || '-'
|
||||
)
|
||||
const sourceTagType = computed(() => commissionSourceMeta.value?.type || 'info')
|
||||
const clawbackRecords = computed(() => detail.value?.clawback_records ?? [])
|
||||
const clawbackTotalAmount = computed(() => record.value?.clawback_total_amount ?? 0)
|
||||
const originalCommission = computed(() => detail.value?.original_commission || null)
|
||||
|
||||
const isNegative = (value?: number | null) => typeof value === 'number' && value < 0
|
||||
|
||||
const loadDetail = async () => {
|
||||
if (!props.shopId || !props.recordId) return
|
||||
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
try {
|
||||
const res = await CommissionService.getShopCommissionRecordDetail(
|
||||
props.shopId,
|
||||
props.recordId,
|
||||
props.source
|
||||
)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
} else {
|
||||
ElMessage.error(res.msg || '佣金明细不存在')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '佣金明细不存在'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleVisibleChange = (value: boolean) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
detail.value = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.shopId, props.recordId, props.source] as const,
|
||||
([visible]) => {
|
||||
if (visible) loadDetail()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.commission-record-detail {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.amount-negative {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-section-summary {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="45%" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="40%" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
<ElFormItem label="订单号" prop="order_id">
|
||||
<ElSelect
|
||||
v-if="!hasInitialOrder"
|
||||
@@ -23,13 +23,6 @@
|
||||
</ElSelect>
|
||||
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款方式" prop="method">
|
||||
<ElRadioGroup v-model="formData.method">
|
||||
<ElRadio v-for="opt in RefundMethodOptions" :key="opt.value" :label="opt.value">
|
||||
{{ opt.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款金额" prop="requested_refund_amount">
|
||||
<ElInputNumber
|
||||
v-model="formData.requested_refund_amount"
|
||||
@@ -53,29 +46,7 @@
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<template v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT">
|
||||
<ElFormItem label="收款账户名" prop="customer_account_name">
|
||||
<ElInput
|
||||
v-model="formData.customer_account_name"
|
||||
placeholder="请输入收款账户名"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="收款账号" prop="customer_account_number">
|
||||
<ElInput
|
||||
v-model="formData.customer_account_number"
|
||||
placeholder="请输入收款账号"
|
||||
maxlength="64"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开户银行">
|
||||
<ElInput v-model="formData.customer_bank_name" placeholder="选填" maxlength="50" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem
|
||||
:label="formData.method === RefundMethod.CUSTOMER_ACCOUNT ? '退款凭证(必填)' : '退款凭证'"
|
||||
prop="refund_voucher_key"
|
||||
>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
v-model="formData.refund_voucher_key"
|
||||
@@ -83,9 +54,6 @@
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('refund_voucher_key')"
|
||||
/>
|
||||
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
|
||||
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -109,8 +77,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { RefundService, OrderService } from '@/api/modules'
|
||||
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
|
||||
import { RefundMethodOptions } from '@/config/constants/refund'
|
||||
import type { CreateRefundRequest, Order } from '@/types/api'
|
||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
@@ -143,24 +110,16 @@
|
||||
const formData = reactive<{
|
||||
order_id: number | null
|
||||
package_usage_id?: number
|
||||
method?: RefundMethod
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount: number
|
||||
refund_reason: string
|
||||
customer_account_name: string
|
||||
customer_account_number: string
|
||||
customer_bank_name: string
|
||||
refund_voucher_key: string[]
|
||||
}>({
|
||||
order_id: null,
|
||||
package_usage_id: undefined,
|
||||
method: RefundMethod.ORIGINAL_ROUTE,
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: 0,
|
||||
refund_reason: '',
|
||||
customer_account_name: '',
|
||||
customer_account_number: '',
|
||||
customer_bank_name: '',
|
||||
refund_voucher_key: []
|
||||
})
|
||||
|
||||
@@ -175,43 +134,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
const requireCustomerAccount = (field: string, message: string) => ({
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT && !value?.trim()) {
|
||||
callback(new Error(message))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
})
|
||||
|
||||
const validateVoucherKey = (
|
||||
_rule: unknown,
|
||||
value: string[],
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (
|
||||
formData.method === RefundMethod.CUSTOMER_ACCOUNT &&
|
||||
(!Array.isArray(value) || value.length === 0)
|
||||
) {
|
||||
callback(new Error('请上传至少 1 个退款凭证'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
|
||||
method: [{ required: true, message: '请选择退款方式', trigger: 'change' }],
|
||||
requested_refund_amount: [
|
||||
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
|
||||
{ validator: validateRefundAmount, trigger: 'blur' }
|
||||
],
|
||||
refund_reason: [{ required: true, message: '请输入退款原因', trigger: 'blur' }],
|
||||
customer_account_name: requireCustomerAccount('customer_account_name', '请填写收款账户名'),
|
||||
customer_account_number: requireCustomerAccount('customer_account_number', '请填写收款账号'),
|
||||
refund_voucher_key: [{ validator: validateVoucherKey, trigger: 'change' }]
|
||||
refund_voucher_key: [{ required: true, message: '请上传退款凭证', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
|
||||
@@ -272,13 +202,9 @@
|
||||
voucherUploading.value = false
|
||||
formData.order_id = null
|
||||
formData.package_usage_id = undefined
|
||||
formData.method = RefundMethod.ORIGINAL_ROUTE
|
||||
formData.requested_refund_amount = undefined
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
formData.customer_account_name = ''
|
||||
formData.customer_account_number = ''
|
||||
formData.customer_bank_name = ''
|
||||
formData.refund_voucher_key = []
|
||||
orderSearchOptions.value = []
|
||||
uploadRef.value?.clearFiles(false)
|
||||
@@ -296,24 +222,20 @@
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data: CreateRefundRequest = {
|
||||
actual_received_amount: formData.actual_received_amount,
|
||||
order_id: formData.order_id!,
|
||||
method: formData.method!,
|
||||
package_usage_id: formData.package_usage_id ?? 0,
|
||||
refund_reason: formData.refund_reason,
|
||||
refund_voucher_key: formData.refund_voucher_key,
|
||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0
|
||||
}
|
||||
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT) {
|
||||
data.customer_account_info = `${formData.customer_account_name.trim()} ${formData.customer_account_number.trim()}${formData.customer_bank_name.trim() ? ` ${formData.customer_bank_name.trim()}` : ''}`
|
||||
data.refund_voucher_key = formData.refund_voucher_key
|
||||
} else if (formData.refund_voucher_key.length) {
|
||||
data.refund_voucher_key = formData.refund_voucher_key
|
||||
}
|
||||
await RefundService.createRefund(data)
|
||||
ElMessage.success('退款申请创建成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(getErrorMessage(error, '退款申请创建失败'))
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
@@ -335,12 +257,3 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voucher-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="480px"
|
||||
@update:model-value="handleVisibleChange"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<template v-if="action === 'approve'">
|
||||
<ElForm ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="审批退款金额">
|
||||
<ElInputNumber
|
||||
v-model="approveForm.approved_refund_amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="不填则按申请金额审批"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="审批备注">
|
||||
<ElInput
|
||||
v-model="approveForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入审批备注(选填)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
<template v-else-if="action === 'reject'">
|
||||
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="拒绝原因" prop="reject_reason">
|
||||
<ElInput
|
||||
v-model="rejectForm.reject_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入拒绝原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退回备注">
|
||||
<ElInput
|
||||
v-model="returnForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入退回备注(选填)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="handleVisibleChange(false)">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitLoading" @click="handleConfirm">
|
||||
{{ confirmText }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { RefundService } from '@/api/modules'
|
||||
import type { Refund } from '@/types/api'
|
||||
import { yuanToFen } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'RefundApprovalDialog' })
|
||||
|
||||
type ApprovalAction = 'approve' | 'reject' | 'return'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
refund: Refund | null
|
||||
action: ApprovalAction
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const approveFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const returnFormRef = ref<FormInstance>()
|
||||
|
||||
const approveForm = reactive<{
|
||||
approved_refund_amount?: number
|
||||
remark: string
|
||||
}>({
|
||||
approved_refund_amount: undefined,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rejectForm = reactive<{
|
||||
reject_reason: string
|
||||
}>({
|
||||
reject_reason: ''
|
||||
})
|
||||
|
||||
const returnForm = reactive<{
|
||||
remark: string
|
||||
}>({
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const approveRules = reactive<FormRules>({})
|
||||
const rejectRules = reactive<FormRules>({
|
||||
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
|
||||
})
|
||||
const returnRules = reactive<FormRules>({})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.action === 'approve') return '审批通过退款申请'
|
||||
if (props.action === 'reject') return '审批拒绝退款申请'
|
||||
return '退回退款申请'
|
||||
})
|
||||
|
||||
const confirmText = computed(() => {
|
||||
if (props.action === 'approve') return '确认通过'
|
||||
if (props.action === 'reject') return '确认拒绝'
|
||||
return '确认退回'
|
||||
})
|
||||
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:modelValue', visible)
|
||||
}
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
approveForm.approved_refund_amount = undefined
|
||||
approveForm.remark = ''
|
||||
rejectForm.reject_reason = ''
|
||||
returnForm.remark = ''
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!props.refund || submitLoading.value) return
|
||||
|
||||
let valid = true
|
||||
if (props.action === 'approve') {
|
||||
valid = await approveFormRef.value?.validate().catch(() => false) !== false
|
||||
} else if (props.action === 'reject') {
|
||||
valid = await rejectFormRef.value?.validate().catch(() => false) !== false
|
||||
} else {
|
||||
valid = await returnFormRef.value?.validate().catch(() => false) !== false
|
||||
}
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const refundId = props.refund.id
|
||||
if (props.action === 'approve') {
|
||||
await RefundService.approveRefund(refundId, {
|
||||
approved_refund_amount: yuanToFen(approveForm.approved_refund_amount),
|
||||
remark: approveForm.remark.trim() || undefined
|
||||
})
|
||||
ElMessage.success('审批通过成功')
|
||||
} else if (props.action === 'reject') {
|
||||
await RefundService.rejectRefund(refundId, {
|
||||
reject_reason: rejectForm.reject_reason.trim()
|
||||
})
|
||||
ElMessage.success('审批拒绝成功')
|
||||
} else {
|
||||
await RefundService.returnRefund(refundId, {
|
||||
remark: returnForm.remark.trim() || undefined
|
||||
})
|
||||
ElMessage.success('退回成功')
|
||||
}
|
||||
emit('update:modelValue', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('操作失败,请稍后重试')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -68,7 +68,6 @@
|
||||
tip?: string
|
||||
purpose?: FilePurpose
|
||||
maxSizeMb?: number
|
||||
contentType?: string
|
||||
singleColumnCsv?: boolean
|
||||
maxCsvRows?: number
|
||||
}
|
||||
@@ -80,7 +79,6 @@
|
||||
tip: '',
|
||||
purpose: 'attachment',
|
||||
maxSizeMb: 0,
|
||||
contentType: '',
|
||||
singleColumnCsv: false,
|
||||
maxCsvRows: 0
|
||||
})
|
||||
@@ -192,35 +190,6 @@
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
}
|
||||
|
||||
// accept 支持扩展名(.csv)、精确 MIME(image/jpeg)与通配 MIME(image/*)
|
||||
const isAcceptMatched = (file: File) => {
|
||||
if (!props.accept) return true
|
||||
|
||||
const fileName = file.name.toLowerCase()
|
||||
const fileType = (file.type || '').toLowerCase()
|
||||
|
||||
return props.accept.split(',').some((type) => {
|
||||
const value = type.trim().toLowerCase()
|
||||
if (!value) return false
|
||||
if (value.startsWith('.')) return fileName.endsWith(value)
|
||||
if (value.endsWith('/*')) return fileType.startsWith(value.slice(0, -1))
|
||||
return fileType === value
|
||||
})
|
||||
}
|
||||
|
||||
const getAcceptWarning = () => {
|
||||
const values = props.accept
|
||||
.split(',')
|
||||
.map((type) => type.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
if (values.length > 0 && values.every((value) => value.startsWith('image/'))) {
|
||||
return '只能上传图片格式的文件'
|
||||
}
|
||||
|
||||
return `只能上传 ${props.accept} 格式的文件`
|
||||
}
|
||||
|
||||
const handleFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
@@ -250,8 +219,14 @@
|
||||
}
|
||||
|
||||
if (props.accept) {
|
||||
if (!isAcceptMatched(file)) {
|
||||
ElMessage.warning(getAcceptWarning())
|
||||
const accepted = props.accept.split(',').some((type) => {
|
||||
const value = type.trim().toLowerCase()
|
||||
return value.startsWith('.')
|
||||
? file.name.toLowerCase().endsWith(value)
|
||||
: file.type.toLowerCase() === value
|
||||
})
|
||||
if (!accepted) {
|
||||
ElMessage.warning(`只能上传 ${props.accept} 格式的文件`)
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
@@ -274,10 +249,10 @@
|
||||
try {
|
||||
ElMessage.info(`正在上传${props.voucherName}...`)
|
||||
|
||||
const uploadContentType = props.contentType || file.type || 'application/octet-stream'
|
||||
const contentType = file.type || 'application/octet-stream'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: uploadContentType,
|
||||
content_type: contentType,
|
||||
purpose: props.purpose
|
||||
})
|
||||
|
||||
@@ -288,7 +263,7 @@
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, uploadContentType)
|
||||
await StorageService.uploadFile(upload_url, file, contentType)
|
||||
|
||||
if (uploadBatch !== activeUploadBatch || removedUploadUids.has(uploadFile.uid)) {
|
||||
return
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<div
|
||||
v-if="pagination && (alwaysShowPagination || tableData.length > 0)"
|
||||
v-if="pagination && tableData.length > 0"
|
||||
class="table-pagination"
|
||||
:class="paginationAlign"
|
||||
>
|
||||
@@ -113,7 +113,7 @@
|
||||
/** 是否显示加载状态 */
|
||||
loading?: boolean
|
||||
/** 行数据的 Key,用于标识每一行数据 */
|
||||
rowKey?: string | ((row: any) => string)
|
||||
rowKey?: string
|
||||
/** 行的 className 的回调方法 */
|
||||
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
|
||||
/** 是否显示边框 */
|
||||
@@ -144,8 +144,6 @@
|
||||
pageSizes?: number[]
|
||||
/** 只有一页时是否隐藏分页器 */
|
||||
hideOnSinglePage?: boolean
|
||||
/** 数据为空时也始终显示分页器 */
|
||||
alwaysShowPagination?: boolean
|
||||
/** 分页器的对齐方式 */
|
||||
paginationAlign?: 'left' | 'center' | 'right'
|
||||
/** 分页器的大小 */
|
||||
@@ -187,7 +185,6 @@
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
hideOnSinglePage: false,
|
||||
alwaysShowPagination: false,
|
||||
pageSizes: () => [10, 20, 30, 50],
|
||||
paginationAlign: 'center',
|
||||
paginationSize: 'default',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 权限检查 Composable
|
||||
* 用于在模板或脚本中检查用户权限
|
||||
*/
|
||||
@@ -18,9 +18,6 @@ export function usePermission() {
|
||||
// 是否是超级管理员
|
||||
const isSuperAdmin = computed(() => userStore.isSuperAdmin)
|
||||
|
||||
// 是否是超级管理员或平台用户
|
||||
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
|
||||
|
||||
/**
|
||||
* 检查是否有指定权限
|
||||
* @param permission 权限码
|
||||
@@ -79,7 +76,6 @@ export function usePermission() {
|
||||
permissions,
|
||||
buttons,
|
||||
isSuperAdmin,
|
||||
isPlatformAccount,
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
@@ -88,4 +84,3 @@ export function usePermission() {
|
||||
hasButton
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 八月迭代新增后台权限编码。
|
||||
* 页面和按钮统一引用这里的常量,后端菜单权限可直接复用同名编码。
|
||||
*/
|
||||
export const AUGUST_PERMISSIONS = {
|
||||
employeeCollection: {
|
||||
billPage: 'employee_collection:bill_view',
|
||||
billDetail: 'employee_collection:bill_detail',
|
||||
billClose: 'employee_collection:bill_close',
|
||||
applicationPage: 'employee_collection:application_view',
|
||||
applicationCreate: 'employee_collection:application_create',
|
||||
applicationDetail: 'employee_collection:application_detail',
|
||||
applicationUpdate: 'employee_collection:application_update',
|
||||
applicationViewVoucher: 'employee_collection:application_view_voucher',
|
||||
paymentMethodPage: 'employee_collection:payment_method_view',
|
||||
paymentMethodCreate: 'employee_collection:payment_method_create',
|
||||
paymentMethodEdit: 'employee_collection:payment_method_edit',
|
||||
paymentMethodDelete: 'employee_collection:payment_method_delete'
|
||||
},
|
||||
phoneAssetAssociation: {
|
||||
list: 'phone_asset_association:list',
|
||||
unbind: 'phone_asset_association:unbind',
|
||||
unbindBatch: 'phone_asset_association:unbind_batch',
|
||||
unbindImportPage: 'phone_asset_association:unbind_import_view',
|
||||
unbindImportCreate: 'phone_asset_association:unbind_import_create',
|
||||
unbindImportDetail: 'phone_asset_association:unbind_import_detail'
|
||||
},
|
||||
h5PopupConfiguration: {
|
||||
list: 'h5_popup_configuration:list',
|
||||
create: 'h5_popup_configuration:create',
|
||||
update: 'h5_popup_configuration:update',
|
||||
enable: 'h5_popup_configuration:enable',
|
||||
disable: 'h5_popup_configuration:disable'
|
||||
},
|
||||
packageTrafficAlert: {
|
||||
rulesView: 'package_traffic_alert:rules_view',
|
||||
ruleCreate: 'package_traffic_alert:rule_create',
|
||||
ruleUpdate: 'package_traffic_alert:rule_update',
|
||||
recordsView: 'package_traffic_alert:records_view',
|
||||
recordDetail: 'package_traffic_alert:record_detail',
|
||||
export: 'package_traffic_alert:export'
|
||||
}
|
||||
} as const
|
||||
|
||||
export type AugustPermission = string
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* 业务用户组相关常量与权限编码
|
||||
*/
|
||||
|
||||
import type { BusinessUserGroupBusinessLine } from '@/types/api'
|
||||
|
||||
/** 业务线选项:standard 标品 / smart 智能产品 / other 其他 */
|
||||
export const BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: BusinessUserGroupBusinessLine
|
||||
type?: 'primary' | 'success' | 'warning' | 'info' | 'danger'
|
||||
}> = [
|
||||
{ label: '标品', value: 'standard', type: 'primary' },
|
||||
{ label: '智能产品', value: 'smart', type: 'success' },
|
||||
{ label: '其他', value: 'other', type: 'warning' }
|
||||
]
|
||||
|
||||
/** 业务线文案映射 */
|
||||
export const BUSINESS_USER_GROUP_BUSINESS_LINE_MAP = BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<BusinessUserGroupBusinessLine, (typeof BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS)[number]>
|
||||
)
|
||||
|
||||
/** 获取业务线标签 */
|
||||
export function getBusinessUserGroupBusinessLineLabel(businessLine?: string | null): string {
|
||||
if (!businessLine) return '-'
|
||||
return BUSINESS_USER_GROUP_BUSINESS_LINE_MAP[businessLine as BusinessUserGroupBusinessLine]
|
||||
?.label ?? businessLine
|
||||
}
|
||||
|
||||
/** 业务用户组页面与按钮权限编码 */
|
||||
export const BUSINESS_USER_GROUP_PERMISSIONS = {
|
||||
page: 'business_user_group:page',
|
||||
create: 'business_user_group:create',
|
||||
update: 'business_user_group:update',
|
||||
delete: 'business_user_group:delete',
|
||||
members: 'business_user_group:members'
|
||||
}
|
||||
|
||||
/** 店铺业务负责人运营权限编码 */
|
||||
export const SHOP_BUSINESS_OWNER_PERMISSIONS = {
|
||||
batch: 'shop:business_owner_batch',
|
||||
importPage: 'shop:business_owner_import',
|
||||
importCreate: 'shop:business_owner_import'
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export const COMMISSION_STATUS_OPTIONS = [
|
||||
{ label: '解冻中', value: CommissionStatus.UNFREEZING, type: 'warning' as const },
|
||||
{ label: '已发放', value: CommissionStatus.RELEASED, type: 'success' as const },
|
||||
{ label: '已失效', value: CommissionStatus.INVALID, type: 'danger' as const },
|
||||
{ label: '回溯', value: CommissionStatus.CLAWBACK, type: 'danger' as const },
|
||||
{ label: '待人工修正', value: CommissionStatus.PENDING_CORRECTION, type: 'warning' as const }
|
||||
]
|
||||
|
||||
@@ -47,7 +46,6 @@ export const CommissionStatusMap = {
|
||||
2: { label: '解冻中', type: 'warning' as const, color: '#E6A23C' },
|
||||
3: { label: '已发放', type: 'success' as const, color: '#67C23A' },
|
||||
4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' },
|
||||
5: { label: '回溯', type: 'danger' as const, color: '#F56C6C' },
|
||||
99: { label: '待人工修正', type: 'warning' as const, color: '#E6A23C' }
|
||||
}
|
||||
|
||||
@@ -115,33 +113,3 @@ export function getCommissionTypeType(type: string) {
|
||||
const config = CommissionTypeMap[type as keyof typeof CommissionTypeMap]
|
||||
return config?.type || 'info'
|
||||
}
|
||||
|
||||
// ========== 佣金记录(含回溯明细)映射 ==========
|
||||
|
||||
// 记录来源映射 (original:原佣金, clawback:回溯明细)
|
||||
export const COMMISSION_RECORD_SOURCE_MAP = {
|
||||
original: { label: '原佣金', type: 'primary' as const },
|
||||
clawback: { label: '回溯明细', type: 'danger' as const }
|
||||
}
|
||||
|
||||
// 佣金记录状态筛选选项(含回溯,不含待人工修正)
|
||||
export const COMMISSION_RECORD_STATUS_OPTIONS = [
|
||||
{ label: '已冻结', value: CommissionStatus.FROZEN },
|
||||
{ label: '解冻中', value: CommissionStatus.UNFREEZING },
|
||||
{ label: '已发放', value: CommissionStatus.RELEASED },
|
||||
{ label: '已失效', value: CommissionStatus.INVALID },
|
||||
{ label: '回溯', value: CommissionStatus.CLAWBACK }
|
||||
]
|
||||
|
||||
// 是否回溯明细行(source 与 status 双判断,兼容未返回 source 的响应)
|
||||
export function isCommissionClawbackRecord(record: {
|
||||
source?: string
|
||||
status?: number
|
||||
}): boolean {
|
||||
return record.source === 'clawback' || record.status === CommissionStatus.CLAWBACK
|
||||
}
|
||||
|
||||
// 列表行 key:原佣金与回溯明细 ID 空间独立,同一页可能出现相同 id
|
||||
export function getCommissionRecordRowKey(record: { id: number; source?: string }): string {
|
||||
return `${record.source || 'original'}:${record.id}`
|
||||
}
|
||||
|
||||
@@ -84,24 +84,6 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
||||
detail: 'export_task:exchange_detail',
|
||||
download: 'export_task:exchange_download'
|
||||
}
|
||||
},
|
||||
commission_record: {
|
||||
scene: 'commission_record',
|
||||
sceneName: '佣金记录',
|
||||
pageTitle: '导出佣金记录',
|
||||
permissions: {
|
||||
detail: 'export_task:commission_record_detail',
|
||||
download: 'export_task:commission_record_download'
|
||||
}
|
||||
},
|
||||
package_traffic_alert: {
|
||||
scene: 'package_traffic_alert',
|
||||
sceneName: '套餐真流量预警',
|
||||
pageTitle: '导出套餐真流量预警',
|
||||
permissions: {
|
||||
detail: 'export_task:package_traffic_alert_detail',
|
||||
download: 'export_task:package_traffic_alert_download'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,15 +34,7 @@ export * from './enableStatus'
|
||||
|
||||
// 导出任务相关
|
||||
export * from './exportTask'
|
||||
export * from './businessUserGroup'
|
||||
|
||||
// 套餐真流量预警相关
|
||||
export * from './packageTrafficAlert'
|
||||
|
||||
// 批量订购相关
|
||||
export * from './bulkPurchase'
|
||||
export * from './julyIteration'
|
||||
export * from './augustIteration'
|
||||
|
||||
// 轮询优先队列相关
|
||||
export * from './pollingPriorityQueue'
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 套餐真流量预警相关常量
|
||||
* 对应 docs/产品迭代8月份/套餐真流量预警_API_前端简版.md
|
||||
*/
|
||||
|
||||
import {
|
||||
PackageTrafficAlertNotificationStatus,
|
||||
type PackageTrafficAlertAssetType
|
||||
} from '@/types/api'
|
||||
|
||||
/** 通知投递结果选项(1:已通知, 2:待投递, 3:投递失败, 4:未通知(接收人已失效), 5:未通知(无有效业务员)) */
|
||||
export const TRAFFIC_ALERT_NOTIFICATION_STATUS_OPTIONS = [
|
||||
{
|
||||
label: '已通知',
|
||||
value: PackageTrafficAlertNotificationStatus.NOTIFIED,
|
||||
tagType: 'success' as const
|
||||
},
|
||||
{
|
||||
label: '待投递',
|
||||
value: PackageTrafficAlertNotificationStatus.PENDING_DELIVERY,
|
||||
tagType: 'info' as const
|
||||
},
|
||||
{
|
||||
label: '投递失败',
|
||||
value: PackageTrafficAlertNotificationStatus.DELIVERY_FAILED,
|
||||
tagType: 'danger' as const
|
||||
},
|
||||
{
|
||||
label: '未通知(接收人已失效)',
|
||||
value: PackageTrafficAlertNotificationStatus.NOT_NOTIFIED_RECEIVER_INVALID,
|
||||
tagType: 'warning' as const
|
||||
},
|
||||
{
|
||||
label: '未通知(无有效业务员)',
|
||||
value: PackageTrafficAlertNotificationStatus.NOT_NOTIFIED_NO_VALID_OWNER,
|
||||
tagType: 'warning' as const
|
||||
}
|
||||
]
|
||||
|
||||
export const getTrafficAlertNotificationStatusOption = (
|
||||
status?: PackageTrafficAlertNotificationStatus | null
|
||||
) => TRAFFIC_ALERT_NOTIFICATION_STATUS_OPTIONS.find((item) => item.value === status)
|
||||
|
||||
/** 资产类型选项(iot_card:物联网卡, device:设备) */
|
||||
export const TRAFFIC_ALERT_ASSET_TYPE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PackageTrafficAlertAssetType
|
||||
}> = [
|
||||
{ label: '物联网卡', value: 'iot_card' },
|
||||
{ label: '设备', value: 'device' }
|
||||
]
|
||||
|
||||
export const getTrafficAlertAssetTypeName = (assetType?: PackageTrafficAlertAssetType | null) =>
|
||||
TRAFFIC_ALERT_ASSET_TYPE_OPTIONS.find((item) => item.value === assetType)?.label || '-'
|
||||
|
||||
/** 导出格式选项(xlsx:Excel, csv:CSV) */
|
||||
export const TRAFFIC_ALERT_EXPORT_FORMAT_OPTIONS = [
|
||||
{ label: 'XLSX', value: 'xlsx' as const },
|
||||
{ label: 'CSV', value: 'csv' as const }
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* 支付商户与商户池管理权限编码。
|
||||
* 页面和按钮统一引用这里的常量,后端菜单权限可直接复用同名编码。
|
||||
* 编码命名参考代理充值页 agent_recharge:<action> 风格。
|
||||
*/
|
||||
export const PAYMENT_MERCHANT_POOL_PERMISSIONS = {
|
||||
merchantCreate: 'payment_merchant_pool:merchant_create',
|
||||
merchantEdit: 'payment_merchant_pool:merchant_edit',
|
||||
merchantToggle: 'payment_merchant_pool:merchant_toggle',
|
||||
merchantDelete: 'payment_merchant_pool:merchant_delete',
|
||||
merchantDetail: 'payment_merchant_pool:merchant_detail',
|
||||
poolCreate: 'payment_merchant_pool:pool_create',
|
||||
poolEdit: 'payment_merchant_pool:pool_edit',
|
||||
poolToggle: 'payment_merchant_pool:pool_toggle',
|
||||
poolDetail: 'payment_merchant_pool:pool_detail',
|
||||
wechatAuthEdit: 'payment_merchant_pool:wechat_auth_edit'
|
||||
} as const
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* 轮询优先队列相关常量
|
||||
* 对应 docs/产品迭代8月份/优先队列.md
|
||||
*/
|
||||
|
||||
import type {
|
||||
PollingPriorityTaskType,
|
||||
PollingPriorityItemStatus,
|
||||
PollingPriorityTriggerType,
|
||||
PollingPriorityResult
|
||||
} from '@/types/api'
|
||||
|
||||
/** 任务类型选项 */
|
||||
export const POLLING_PRIORITY_TASK_TYPE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PollingPriorityTaskType
|
||||
}> = [
|
||||
{ label: '实名检查', value: 'polling:realname' },
|
||||
{ label: '流量检查', value: 'polling:carddata' },
|
||||
{ label: '套餐检查', value: 'polling:package' },
|
||||
{ label: '卡状态检查', value: 'polling:card_status' }
|
||||
]
|
||||
|
||||
export const getPollingPriorityTaskTypeName = (
|
||||
taskType?: PollingPriorityTaskType | string | null
|
||||
) =>
|
||||
POLLING_PRIORITY_TASK_TYPE_OPTIONS.find((item) => item.value === taskType)?.label ||
|
||||
String(taskType || '-')
|
||||
|
||||
/** 状态选项 */
|
||||
export const POLLING_PRIORITY_STATUS_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PollingPriorityItemStatus
|
||||
}> = [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '失败', value: 'failed' }
|
||||
]
|
||||
|
||||
export const getPollingPriorityStatusName = (
|
||||
status?: PollingPriorityItemStatus | string | null
|
||||
) =>
|
||||
POLLING_PRIORITY_STATUS_OPTIONS.find((item) => item.value === status)?.label ||
|
||||
String(status || '-')
|
||||
|
||||
/** 触发类型选项 */
|
||||
export const POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PollingPriorityTriggerType
|
||||
}> = [
|
||||
{ label: '购买激活', value: 'purchase_activated' },
|
||||
{ label: '续费激活', value: 'renewal_activated' },
|
||||
{ label: '队列激活', value: 'queue_activated' },
|
||||
{ label: '附加套餐激活', value: 'addon_activated' },
|
||||
{ label: '无有效套餐', value: 'no_valid_package' },
|
||||
{ label: '人工触发', value: 'manual_trigger' }
|
||||
]
|
||||
|
||||
export const getPollingPriorityTriggerTypeName = (
|
||||
triggerType?: PollingPriorityTriggerType | string | null
|
||||
) =>
|
||||
POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS.find((item) => item.value === triggerType)?.label ||
|
||||
String(triggerType || '-')
|
||||
|
||||
/** 执行结果名称(result 为空时展示为未出结果) */
|
||||
export const getPollingPriorityResultName = (result?: PollingPriorityResult | string | null) => {
|
||||
if (result === 'success') return '成功'
|
||||
if (result === 'failed') return '失败'
|
||||
return '未出结果'
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/**
|
||||
* 退款管理展示与规则常量
|
||||
*/
|
||||
|
||||
import {
|
||||
RefundStatus,
|
||||
RefundMethod,
|
||||
ChannelRefundStatus,
|
||||
RefundFailureReason
|
||||
} from '@/types/api/refund'
|
||||
|
||||
/** 退款状态名称映射 */
|
||||
export const RefundStatusNameMap: Record<RefundStatus, string> = {
|
||||
[RefundStatus.PENDING]: '待审批',
|
||||
[RefundStatus.APPROVED]: '已通过',
|
||||
[RefundStatus.REJECTED]: '已拒绝',
|
||||
[RefundStatus.RETURNED]: '已退回',
|
||||
[RefundStatus.REFUNDING]: '原路退款处理中',
|
||||
[RefundStatus.REFUND_FAILED]: '原路退款失败'
|
||||
}
|
||||
|
||||
/** 退款状态标签类型映射 */
|
||||
export const RefundStatusTypeMap: Record<
|
||||
RefundStatus,
|
||||
'warning' | 'success' | 'danger' | 'info'
|
||||
> = {
|
||||
[RefundStatus.PENDING]: 'warning',
|
||||
[RefundStatus.APPROVED]: 'success',
|
||||
[RefundStatus.REJECTED]: 'danger',
|
||||
[RefundStatus.RETURNED]: 'info',
|
||||
[RefundStatus.REFUNDING]: 'warning',
|
||||
[RefundStatus.REFUND_FAILED]: 'danger'
|
||||
}
|
||||
|
||||
/** 退款方式名称映射 */
|
||||
export const RefundMethodNameMap: Record<RefundMethod, string> = {
|
||||
[RefundMethod.ORIGINAL_ROUTE]: '原路退回',
|
||||
[RefundMethod.CUSTOMER_ACCOUNT]: '客户收款信息',
|
||||
[RefundMethod.ASSET_WALLET]: '资产钱包',
|
||||
[RefundMethod.AGENT_WALLET]: '代理钱包'
|
||||
}
|
||||
|
||||
/** 渠道退款状态名称映射 */
|
||||
export const ChannelRefundStatusNameMap: Record<ChannelRefundStatus, string> = {
|
||||
[ChannelRefundStatus.NOT_STARTED]: '未发起或不适用',
|
||||
[ChannelRefundStatus.PROCESSING]: '处理中',
|
||||
[ChannelRefundStatus.SUCCEEDED]: '已成功',
|
||||
[ChannelRefundStatus.FAILED]: '已失败'
|
||||
}
|
||||
|
||||
/** 失败分类名称映射 */
|
||||
export const RefundFailureReasonNameMap: Record<RefundFailureReason, string> = {
|
||||
[RefundFailureReason.CHANNEL_REJECTED]: '渠道明确拒绝',
|
||||
[RefundFailureReason.CREDENTIAL_INVALID]: '渠道凭证失效',
|
||||
[RefundFailureReason.INSUFFICIENT_BALANCE]: '渠道余额不足',
|
||||
[RefundFailureReason.TIMEOUT_UNKNOWN]: '超时或结果未知',
|
||||
[RefundFailureReason.APPROVAL_REJECTED]: '企微驳回或关闭',
|
||||
[RefundFailureReason.REVOKED_AFTER_APPROVED]: '企微通过后撤销',
|
||||
[RefundFailureReason.PAYMENT_FACT_INVALID]: '本地原支付事实不可用'
|
||||
}
|
||||
|
||||
/** 退款方式选项(表单用) */
|
||||
export const RefundMethodOptions = [
|
||||
{ label: '原路退回', value: RefundMethod.ORIGINAL_ROUTE },
|
||||
{ label: '客户收款信息', value: RefundMethod.CUSTOMER_ACCOUNT },
|
||||
{ label: '资产钱包', value: RefundMethod.ASSET_WALLET },
|
||||
{ label: '代理钱包', value: RefundMethod.AGENT_WALLET }
|
||||
]
|
||||
|
||||
/**
|
||||
* 是否可重提:3 已拒绝 / 4 已退回 / 6 原路退款失败(anomaly_flag=1 时禁止)
|
||||
*/
|
||||
export const canResubmitRefund = (item: {
|
||||
status?: RefundStatus
|
||||
anomaly_flag?: number
|
||||
}): boolean => {
|
||||
if (!item.status) return false
|
||||
if (item.status === RefundStatus.REJECTED || item.status === RefundStatus.RETURNED) return true
|
||||
if (item.status === RefundStatus.REFUND_FAILED) return item.anomaly_flag !== 1
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批操作是否可用:仅待审批且未关联企微审批实例(存量无实例)的申请
|
||||
*/
|
||||
export const canManualApproveRefund = (item: {
|
||||
status?: RefundStatus
|
||||
approval_instance_id?: number | null
|
||||
}): boolean => item.status === RefundStatus.PENDING && !item.approval_instance_id
|
||||
@@ -26,20 +26,11 @@
|
||||
"setting": {
|
||||
"menuType": {
|
||||
"title": "Menu Layout",
|
||||
"list": [
|
||||
"Vertical",
|
||||
"Horizontal",
|
||||
"Mixed",
|
||||
"Dual"
|
||||
]
|
||||
"list": ["Vertical", "Horizontal", "Mixed", "Dual"]
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Style",
|
||||
"list": [
|
||||
"Light",
|
||||
"Dark",
|
||||
"System"
|
||||
]
|
||||
"list": ["Light", "Dark", "System"]
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu Style"
|
||||
@@ -49,17 +40,11 @@
|
||||
},
|
||||
"box": {
|
||||
"title": "Box Style",
|
||||
"list": [
|
||||
"Border",
|
||||
"Shadow"
|
||||
]
|
||||
"list": ["Border", "Shadow"]
|
||||
},
|
||||
"container": {
|
||||
"title": "Container Width",
|
||||
"list": [
|
||||
"Full",
|
||||
"Boxed"
|
||||
]
|
||||
"list": ["Full", "Boxed"]
|
||||
},
|
||||
"basics": {
|
||||
"title": "Basic Config",
|
||||
@@ -97,14 +82,8 @@
|
||||
"notice": {
|
||||
"title": "Notice",
|
||||
"btnRead": "Mark as read",
|
||||
"bar": [
|
||||
"Notice",
|
||||
"Message",
|
||||
"Todo"
|
||||
],
|
||||
"text": [
|
||||
"No"
|
||||
],
|
||||
"bar": ["Notice", "Message", "Todo"],
|
||||
"text": ["No"],
|
||||
"viewAll": "View all"
|
||||
},
|
||||
"worktab": {
|
||||
@@ -296,10 +275,10 @@
|
||||
"evening": "Good evening!"
|
||||
},
|
||||
"exceptionPage": {
|
||||
"gohome": "Go Home",
|
||||
"403": "Sorry, you do not have permission to access this page",
|
||||
"404": "Sorry, the page you are trying to access does not exist",
|
||||
"500": "Sorry, there was an error on the server",
|
||||
"gohome": "Go Home"
|
||||
"500": "Sorry, there was an error on the server"
|
||||
},
|
||||
"menus": {
|
||||
"login": {
|
||||
@@ -413,10 +392,7 @@
|
||||
"seriesAssignDetail": "Series Assignment Detail",
|
||||
"packageSeries": "Package Series",
|
||||
"packageSeriesDetail": "Package Series Detail",
|
||||
"packageCommission": "Package Commission Cards",
|
||||
"trafficAlertRules": "Traffic Alert Rules",
|
||||
"trafficAlerts": "Traffic Alert Records",
|
||||
"trafficAlertDetail": "Traffic Alert Record Detail"
|
||||
"packageCommission": "Package Commission Cards"
|
||||
},
|
||||
"accountManagement": {
|
||||
"title": "Account Management",
|
||||
@@ -436,12 +412,7 @@
|
||||
"agentRechargeDetail": "Agent Recharge Details",
|
||||
"refundManagement": "Refund Management",
|
||||
"refundDetail": "Refund Details",
|
||||
"agentFundOverview": "Agent Fund Overview",
|
||||
"employeeCollectionBills": "Employee Collection Bills",
|
||||
"employeeCollectionBillDetail": "Bill Details",
|
||||
"employeeCollectionApplications": "Reconciliation Applications",
|
||||
"employeeCollectionApplicationDetail": "Application Details",
|
||||
"employeeCollectionPaymentMethods": "Payment Methods"
|
||||
"agentFundOverview": "Agent Fund Overview"
|
||||
},
|
||||
"deviceManagement": {
|
||||
"title": "Device Management",
|
||||
@@ -458,9 +429,7 @@
|
||||
},
|
||||
"shopManagement": {
|
||||
"title": "Shop Management",
|
||||
"shopList": "Shop List",
|
||||
"businessUserGroups": "Business User Groups",
|
||||
"businessOwnerImport": "Business Owner Import"
|
||||
"shopList": "Shop List"
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "Asset Management",
|
||||
@@ -499,14 +468,9 @@
|
||||
"exportRefund": "Export Refunds",
|
||||
"exportAgentRecharge": "Export Agent Recharges",
|
||||
"exportExchange": "Export Exchanges",
|
||||
"exportCommissionRecord": "Export Commission Records",
|
||||
"exportPackageTrafficAlert": "Export Package Traffic Alerts",
|
||||
"exportTaskDetail": "Export Task Detail",
|
||||
"exchangeManagement": "Exchange Management",
|
||||
"exchangeDetail": "Exchange Order Detail",
|
||||
"phoneAssetAssociation": "Phone-Asset Associations",
|
||||
"phoneAssetUnbindImportTasks": "Unbind Import Tasks",
|
||||
"phoneAssetUnbindImportTaskDetail": "Unbind Import Task Detail"
|
||||
"exchangeDetail": "Exchange Order Detail"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings Management",
|
||||
@@ -514,15 +478,7 @@
|
||||
"detailsOfPaymentConfiguration": "Payment Configuration Details",
|
||||
"paymentMerchant": "Payment Merchant",
|
||||
"developerApi": "Developer API",
|
||||
"commissionTemplate": "Commission Template",
|
||||
"paymentMerchantPools": "Merchant Pool Management",
|
||||
"paymentMerchantPoolsTabMerchants": "Payment Merchants",
|
||||
"paymentMerchantPoolsTabPools": "Merchant Pools",
|
||||
"paymentMerchantPoolsTabWechatAuth": "WeChat Authorization",
|
||||
"detailsOfPaymentMerchant": "Payment Merchant Details",
|
||||
"detailsOfPaymentMerchantPool": "Merchant Pool Details",
|
||||
"h5PopupConfiguration": "H5 Popup Configurations",
|
||||
"assetWalletAutoRenewal": "Asset Wallet Auto Renewal"
|
||||
"commissionTemplate": "Commission Template"
|
||||
},
|
||||
"batch": {
|
||||
"title": "Batch Operations",
|
||||
|
||||
@@ -27,20 +27,11 @@
|
||||
"setting": {
|
||||
"menuType": {
|
||||
"title": "菜单布局",
|
||||
"list": [
|
||||
"垂直",
|
||||
"水平",
|
||||
"混合",
|
||||
"双列"
|
||||
]
|
||||
"list": ["垂直", "水平", "混合", "双列"]
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题风格",
|
||||
"list": [
|
||||
"浅色",
|
||||
"深色",
|
||||
"系统"
|
||||
]
|
||||
"list": ["浅色", "深色", "系统"]
|
||||
},
|
||||
"menu": {
|
||||
"title": "菜单风格"
|
||||
@@ -50,17 +41,11 @@
|
||||
},
|
||||
"box": {
|
||||
"title": "盒子样式",
|
||||
"list": [
|
||||
"边框",
|
||||
"阴影"
|
||||
]
|
||||
"list": ["边框", "阴影"]
|
||||
},
|
||||
"container": {
|
||||
"title": "容器宽度",
|
||||
"list": [
|
||||
"铺满",
|
||||
"定宽"
|
||||
]
|
||||
"list": ["铺满", "定宽"]
|
||||
},
|
||||
"basics": {
|
||||
"title": "基础配置",
|
||||
@@ -98,14 +83,8 @@
|
||||
"notice": {
|
||||
"title": "通知",
|
||||
"btnRead": "标为已读",
|
||||
"bar": [
|
||||
"通知",
|
||||
"消息",
|
||||
"代办"
|
||||
],
|
||||
"text": [
|
||||
"暂无"
|
||||
],
|
||||
"bar": ["通知", "消息", "代办"],
|
||||
"text": ["暂无"],
|
||||
"viewAll": "查看全部"
|
||||
},
|
||||
"worktab": {
|
||||
@@ -131,11 +110,7 @@
|
||||
"admin": "管理员",
|
||||
"user": "普通用户"
|
||||
},
|
||||
"placeholder": [
|
||||
"请输入手机号",
|
||||
"请输入密码",
|
||||
"请拖动滑块完成验证"
|
||||
],
|
||||
"placeholder": ["请输入手机号", "请输入密码", "请拖动滑块完成验证"],
|
||||
"sliderText": "按住滑块拖动",
|
||||
"sliderSuccessText": "验证成功",
|
||||
"rememberPwd": "记住密码",
|
||||
@@ -178,11 +153,7 @@
|
||||
"register": {
|
||||
"title": "创建账号",
|
||||
"subTitle": "欢迎加入我们,请填写以下信息完成注册",
|
||||
"placeholder": [
|
||||
"请输入账号",
|
||||
"请输入密码",
|
||||
"请再次输入密码"
|
||||
],
|
||||
"placeholder": ["请输入账号", "请输入密码", "请再次输入密码"],
|
||||
"rule": [
|
||||
"请再次输入密码",
|
||||
"两次输入密码不一致!",
|
||||
@@ -317,10 +288,10 @@
|
||||
"evening": "晚上好!"
|
||||
},
|
||||
"exceptionPage": {
|
||||
"gohome": "返回首页",
|
||||
"403": "抱歉,您无权访问该页面",
|
||||
"404": "抱歉,您访问的页面不存在",
|
||||
"500": "抱歉,服务器出错了",
|
||||
"gohome": "返回首页"
|
||||
"500": "抱歉,服务器出错了"
|
||||
},
|
||||
"menus": {
|
||||
"login": {
|
||||
@@ -380,10 +351,7 @@
|
||||
"seriesGrantsDetail": "代理系列授权详情",
|
||||
"seriesGrantPackages": "代理系列授权套餐列表",
|
||||
"packageSeries": "套餐系列",
|
||||
"packageSeriesDetail": "套餐系列详情",
|
||||
"trafficAlertRules": "真流量预警规则",
|
||||
"trafficAlerts": "真流量达量预警",
|
||||
"trafficAlertDetail": "真流量达量预警详情"
|
||||
"packageSeriesDetail": "套餐系列详情"
|
||||
},
|
||||
"accountManagement": {
|
||||
"title": "账号管理",
|
||||
@@ -394,9 +362,7 @@
|
||||
},
|
||||
"shopManagement": {
|
||||
"title": "店铺管理",
|
||||
"shopList": "店铺列表",
|
||||
"businessUserGroups": "业务用户组",
|
||||
"businessOwnerImport": "业务负责人导入"
|
||||
"shopList": "店铺列表"
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "资产管理",
|
||||
@@ -428,14 +394,9 @@
|
||||
"exportRefund": "导出退款",
|
||||
"exportAgentRecharge": "导出代理充值",
|
||||
"exportExchange": "导出换货",
|
||||
"exportCommissionRecord": "导出佣金记录",
|
||||
"exportPackageTrafficAlert": "导出套餐真流量预警",
|
||||
"exportTaskDetail": "导出任务详情",
|
||||
"exchangeManagement": "换货管理",
|
||||
"exchangeDetail": "换货单详情",
|
||||
"phoneAssetAssociation": "手机号资产关联",
|
||||
"phoneAssetUnbindImportTasks": "解绑导入任务",
|
||||
"phoneAssetUnbindImportTaskDetail": "解绑导入任务详情"
|
||||
"exchangeDetail": "换货单详情"
|
||||
},
|
||||
"orderManagement": {
|
||||
"title": "订单管理",
|
||||
@@ -448,12 +409,7 @@
|
||||
"agentRechargeDetail": "代理充值详情",
|
||||
"refundManagement": "退款管理",
|
||||
"refundDetail": "退款详情",
|
||||
"agentFundOverview": "代理商资金概况",
|
||||
"employeeCollectionBills": "员工代收款账单",
|
||||
"employeeCollectionBillDetail": "账单详情",
|
||||
"employeeCollectionApplications": "核销申请",
|
||||
"employeeCollectionApplicationDetail": "核销申请详情",
|
||||
"employeeCollectionPaymentMethods": "收款方式管理"
|
||||
"agentFundOverview": "代理商资金概况"
|
||||
},
|
||||
"commission": {
|
||||
"title": "佣金管理",
|
||||
@@ -465,15 +421,7 @@
|
||||
"paymentSettings": "支付设置",
|
||||
"detailsOfPaymentConfiguration": "支付配置详情",
|
||||
"withdrawalSettings": "提现配置",
|
||||
"passwordSettings": "密码设置",
|
||||
"paymentMerchantPools": "商户池管理",
|
||||
"paymentMerchantPoolsTabMerchants": "支付商户",
|
||||
"paymentMerchantPoolsTabPools": "商户池",
|
||||
"paymentMerchantPoolsTabWechatAuth": "微信授权配置",
|
||||
"detailsOfPaymentMerchant": "支付商户详情",
|
||||
"detailsOfPaymentMerchantPool": "商户池详情",
|
||||
"h5PopupConfiguration": "H5运营弹窗配置",
|
||||
"assetWalletAutoRenewal": "资产钱包自动续费"
|
||||
"passwordSettings": "密码设置"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -380,7 +380,6 @@ function convertBackendMenuToRoute(
|
||||
path: menuUrl,
|
||||
name: matchedRoute.name,
|
||||
component: matchedRoute.component,
|
||||
redirect: matchedRoute.redirect,
|
||||
meta: {
|
||||
...matchedRoute.meta,
|
||||
title: menu.name,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 权限验证相关工具函数
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useUserStore } from '@/store/modules/user'
|
||||
* 不需要登录的路由白名单
|
||||
*/
|
||||
export const LOGIN_WHITE_LIST = [
|
||||
'/agent-registration',
|
||||
'/auth/login',
|
||||
'/exception/403',
|
||||
'/exception/404',
|
||||
@@ -64,17 +63,8 @@ export const hasRoutePermission = (
|
||||
}
|
||||
}
|
||||
|
||||
// 检查允许访问的用户类型
|
||||
if (route.meta?.allowedUserTypes) {
|
||||
const allowedUserTypes = route.meta.allowedUserTypes as number[]
|
||||
const userType = Number(userInfo.user_type)
|
||||
if (!allowedUserTypes.includes(userType)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果路由没有设置额外的权限要求,直接通过
|
||||
if (!route.meta?.roles && !route.meta?.permissions && !route.meta?.allowedUserTypes) {
|
||||
if (!route.meta?.roles && !route.meta?.permissions) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -205,4 +195,3 @@ export const buildLoginRedirect = (currentPath: string): string => {
|
||||
}
|
||||
return `/auth/login?redirect=${encodeURIComponent(currentPath)}`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { RoutesAlias } from '../routesAlias'
|
||||
import { RoutesAlias } from '../routesAlias'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants/julyIteration'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
||||
import { PAYMENT_MERCHANT_POOL_PERMISSIONS } from '@/config/constants/paymentMerchantPools'
|
||||
import {
|
||||
BUSINESS_USER_GROUP_PERMISSIONS,
|
||||
SHOP_BUSINESS_OWNER_PERMISSIONS
|
||||
} from '@/config/constants'
|
||||
|
||||
/**
|
||||
* 菜单列表、异步路由
|
||||
@@ -211,37 +205,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 真流量预警规则
|
||||
{
|
||||
path: 'traffic-alert-rules',
|
||||
name: 'TrafficAlertRules',
|
||||
component: RoutesAlias.TrafficAlertRules,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.trafficAlertRules',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 真流量达量预警记录
|
||||
{
|
||||
path: 'traffic-alerts',
|
||||
name: 'TrafficAlerts',
|
||||
component: RoutesAlias.TrafficAlerts,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.trafficAlerts',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 真流量达量预警记录详情
|
||||
{
|
||||
path: 'traffic-alerts/detail/:id',
|
||||
name: 'TrafficAlertDetail',
|
||||
component: RoutesAlias.TrafficAlertDetail,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.trafficAlertDetail',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -276,30 +239,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 业务用户组
|
||||
{
|
||||
path: 'business-user-groups',
|
||||
name: 'BusinessUserGroups',
|
||||
component: RoutesAlias.BusinessUserGroups,
|
||||
meta: {
|
||||
title: 'menus.shopManagement.businessUserGroups',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
permissions: [BUSINESS_USER_GROUP_PERMISSIONS.page]
|
||||
}
|
||||
},
|
||||
// 业务负责人导入
|
||||
{
|
||||
path: 'business-owner-import',
|
||||
name: 'BusinessOwnerImport',
|
||||
component: RoutesAlias.BusinessOwnerImport,
|
||||
meta: {
|
||||
title: 'menus.shopManagement.businessOwnerImport',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
permissions: [SHOP_BUSINESS_OWNER_PERMISSIONS.importPage]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -419,40 +358,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 手机号资产关联
|
||||
{
|
||||
path: 'phone-asset-association',
|
||||
name: 'PhoneAssetAssociation',
|
||||
component: RoutesAlias.PhoneAssetAssociation,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.phoneAssetAssociation',
|
||||
permissions: [AUGUST_PERMISSIONS.phoneAssetAssociation.list],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 解绑导入任务列表
|
||||
{
|
||||
path: 'phone-asset-association/unbind-import-tasks',
|
||||
name: 'PhoneAssetUnbindImportTasks',
|
||||
component: RoutesAlias.PhoneAssetUnbindImportTasks,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.phoneAssetUnbindImportTasks',
|
||||
permissions: [AUGUST_PERMISSIONS.phoneAssetAssociation.unbindImportPage],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 解绑导入任务详情
|
||||
{
|
||||
path: 'phone-asset-association/unbind-import-tasks/detail',
|
||||
name: 'PhoneAssetUnbindImportTaskDetail',
|
||||
component: RoutesAlias.PhoneAssetUnbindImportTaskDetail,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.phoneAssetUnbindImportTaskDetail',
|
||||
permissions: [AUGUST_PERMISSIONS.phoneAssetAssociation.unbindImportDetail],
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 记录管理
|
||||
{
|
||||
path: 'record-management',
|
||||
@@ -684,26 +589,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-commission-record',
|
||||
name: 'ExportCommissionRecordTaskList',
|
||||
component: RoutesAlias.ExportCommissionRecordTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportCommissionRecord',
|
||||
exportTaskScene: 'commission_record',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-package-traffic-alert',
|
||||
name: 'ExportPackageTrafficAlertTaskList',
|
||||
component: RoutesAlias.ExportPackageTrafficAlertTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportPackageTrafficAlert',
|
||||
exportTaskScene: 'package_traffic_alert',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-task-detail',
|
||||
name: 'ExportTaskDetail',
|
||||
@@ -805,64 +690,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 员工代收款账单
|
||||
{
|
||||
path: 'employee-collection/bills',
|
||||
name: 'EmployeeCollectionBills',
|
||||
component: RoutesAlias.EmployeeCollectionBills,
|
||||
meta: {
|
||||
title: 'menus.financialManagement.employeeCollectionBills',
|
||||
keepAlive: true,
|
||||
permissions: [AUGUST_PERMISSIONS.employeeCollection.billPage]
|
||||
}
|
||||
},
|
||||
// 员工代收款账单详情
|
||||
{
|
||||
path: 'employee-collection/bills/detail/:id',
|
||||
name: 'EmployeeCollectionBillDetailRoute',
|
||||
component: RoutesAlias.EmployeeCollectionBillDetail,
|
||||
meta: {
|
||||
title: 'menus.financialManagement.employeeCollectionBillDetail',
|
||||
isHide: true,
|
||||
keepAlive: false,
|
||||
permissions: [AUGUST_PERMISSIONS.employeeCollection.billDetail]
|
||||
}
|
||||
},
|
||||
// 核销申请
|
||||
{
|
||||
path: 'employee-collection/applications',
|
||||
name: 'EmployeeCollectionApplications',
|
||||
component: RoutesAlias.EmployeeCollectionApplications,
|
||||
meta: {
|
||||
title: 'menus.financialManagement.employeeCollectionApplications',
|
||||
keepAlive: true,
|
||||
permissions: [AUGUST_PERMISSIONS.employeeCollection.applicationPage]
|
||||
}
|
||||
},
|
||||
// 核销申请详情
|
||||
{
|
||||
path: 'employee-collection/applications/detail/:id',
|
||||
name: 'EmployeeCollectionApplicationDetailRoute',
|
||||
component: RoutesAlias.EmployeeCollectionApplicationDetail,
|
||||
meta: {
|
||||
title: 'menus.financialManagement.employeeCollectionApplicationDetail',
|
||||
isHide: true,
|
||||
keepAlive: false,
|
||||
permissions: [AUGUST_PERMISSIONS.employeeCollection.applicationDetail]
|
||||
}
|
||||
},
|
||||
// 收款方式管理(仅超级管理员)
|
||||
{
|
||||
path: 'employee-collection/payment-methods',
|
||||
name: 'EmployeeCollectionPaymentMethods',
|
||||
component: RoutesAlias.EmployeeCollectionPaymentMethods,
|
||||
meta: {
|
||||
title: 'menus.financialManagement.employeeCollectionPaymentMethods',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER'],
|
||||
permissions: [AUGUST_PERMISSIONS.employeeCollection.paymentMethodPage]
|
||||
}
|
||||
},
|
||||
// 代理商资金概况
|
||||
{
|
||||
path: 'agent-fund-overview',
|
||||
@@ -1009,43 +836,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// 支付商户与商户池管理
|
||||
{
|
||||
path: 'payment-merchant-pools',
|
||||
name: 'PaymentMerchantPools',
|
||||
component: RoutesAlias.PaymentMerchantPools,
|
||||
meta: {
|
||||
title: 'menus.settings.paymentMerchantPools',
|
||||
keepAlive: true,
|
||||
allowedUserTypes: [1, 2]
|
||||
}
|
||||
},
|
||||
// 支付商户详情
|
||||
{
|
||||
path: 'payment-merchant-pools/detail/:id',
|
||||
name: 'PaymentMerchantPoolsDetailRoute',
|
||||
component: RoutesAlias.PaymentMerchantPoolsDetail,
|
||||
meta: {
|
||||
title: 'menus.settings.detailsOfPaymentMerchant',
|
||||
isHide: true,
|
||||
keepAlive: false,
|
||||
allowedUserTypes: [1, 2],
|
||||
permissions: [PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantDetail]
|
||||
}
|
||||
},
|
||||
// 商户池详情
|
||||
{
|
||||
path: 'payment-merchant-pools/pool-detail/:id',
|
||||
name: 'PaymentMerchantPoolDetailRoute',
|
||||
component: RoutesAlias.PaymentMerchantPoolDetail,
|
||||
meta: {
|
||||
title: 'menus.settings.detailsOfPaymentMerchantPool',
|
||||
isHide: true,
|
||||
keepAlive: false,
|
||||
allowedUserTypes: [1, 2],
|
||||
permissions: [PAYMENT_MERCHANT_POOL_PERMISSIONS.poolDetail]
|
||||
}
|
||||
},
|
||||
// 支付设置详情
|
||||
{
|
||||
path: 'payment-settings/detail/:id',
|
||||
@@ -1121,29 +911,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// H5运营弹窗配置
|
||||
{
|
||||
path: 'h5-popup-configuration',
|
||||
name: 'H5PopupConfigurations',
|
||||
component: RoutesAlias.H5PopupConfigurations,
|
||||
meta: {
|
||||
title: 'menus.settings.h5PopupConfiguration',
|
||||
keepAlive: true,
|
||||
allowedUserTypes: [1, 2],
|
||||
permissions: [AUGUST_PERMISSIONS.h5PopupConfiguration.list]
|
||||
}
|
||||
},
|
||||
// 资产钱包自动续费配置
|
||||
{
|
||||
path: 'asset-wallet-auto-renewal',
|
||||
name: 'AssetWalletAutoRenewal',
|
||||
component: RoutesAlias.AssetWalletAutoRenewal,
|
||||
meta: {
|
||||
title: 'menus.settings.assetWalletAutoRenewal',
|
||||
keepAlive: true,
|
||||
allowedUserTypes: [1, 2]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1227,16 +994,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
title: '轮询监控',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 优先队列
|
||||
{
|
||||
path: 'priority-queue',
|
||||
name: 'PollingPriorityQueue',
|
||||
component: RoutesAlias.PollingPriorityQueue,
|
||||
meta: {
|
||||
title: '优先队列',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,12 +17,6 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
component: () => import('@views/auth/login/index.vue'),
|
||||
meta: { title: 'menus.login.title', isHideTab: true, setTheme: true }
|
||||
},
|
||||
{
|
||||
path: '/agent-registration',
|
||||
name: 'AgentRegistration',
|
||||
component: () => import('@views/agent-registration/index.vue'),
|
||||
meta: { title: '代理扫码注册', isHideTab: true, setTheme: true, noLogin: true }
|
||||
},
|
||||
{
|
||||
path: '/exception',
|
||||
component: Home,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 路由别名,方便快速找到页面,同时可以用作路由跳转
|
||||
*/
|
||||
export enum RoutesAlias {
|
||||
@@ -38,15 +38,10 @@ export enum RoutesAlias {
|
||||
SeriesGrants = '/package-management/series-grants', // 代理系列授权
|
||||
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
|
||||
SeriesGrantPackages = '/package-management/series-grants/packages', // 代理系列授权套餐列表
|
||||
TrafficAlertRules = '/package-management/traffic-alert-rules', // 真流量预警规则
|
||||
TrafficAlerts = '/package-management/traffic-alerts', // 真流量达量预警记录
|
||||
TrafficAlertDetail = '/package-management/traffic-alerts/detail', // 真流量达量预警记录详情
|
||||
|
||||
// 店铺管理
|
||||
Shop = '/shop-management/list', // 店铺列表
|
||||
ShopDetail = '/shop-management/detail', // 店铺详情
|
||||
BusinessUserGroups = '/shop-management/business-user-groups', // 业务用户组
|
||||
BusinessOwnerImport = '/shop-management/business-owner-import', // 业务负责人导入
|
||||
|
||||
// 通用页面(店铺账号-企业账号)
|
||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用
|
||||
@@ -84,8 +79,6 @@ export enum RoutesAlias {
|
||||
ExportRefundTaskList = '/asset-management/export-task-management/export-refund', // 导出退款
|
||||
ExportAgentRechargeTaskList = '/asset-management/export-task-management/export-agent-recharge', // 导出代理充值
|
||||
ExportExchangeTaskList = '/asset-management/export-task-management/export-exchange', // 导出换货
|
||||
ExportCommissionRecordTaskList = '/asset-management/export-task-management/export-commission-record', // 导出佣金记录
|
||||
ExportPackageTrafficAlertTaskList = '/asset-management/export-task-management/export-package-traffic-alert', // 导出套餐真流量预警
|
||||
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
|
||||
|
||||
// 订单管理
|
||||
@@ -97,11 +90,6 @@ export enum RoutesAlias {
|
||||
RefundManagement = '/finance/refund', // 退款管理
|
||||
RefundDetail = '/finance/refund/detail', // 退款详情
|
||||
AgentFundOverview = '/finance/agent-fund-overview', // 代理商资金概况
|
||||
EmployeeCollectionBills = '/finance/employee-collection/bills', // 员工代收款账单
|
||||
EmployeeCollectionBillDetail = '/finance/employee-collection/bills/detail', // 员工代收款账单详情
|
||||
EmployeeCollectionApplications = '/finance/employee-collection/applications', // 核销申请
|
||||
EmployeeCollectionApplicationDetail = '/finance/employee-collection/applications/detail', // 核销申请详情
|
||||
EmployeeCollectionPaymentMethods = '/finance/employee-collection/payment-methods', // 收款方式管理
|
||||
|
||||
ExpiringAssets = '/asset-management/expiring-assets', // 临期资产
|
||||
|
||||
@@ -114,17 +102,12 @@ export enum RoutesAlias {
|
||||
WithdrawalSettings = '/settings/withdrawal-settings', // 提现配置
|
||||
PaymentSettings = '/settings/payment-settings', // 支付设置
|
||||
PaymentSettingsDetail = '/settings/payment-settings/detail', // 支付设置详情
|
||||
PaymentMerchantPools = '/settings/payment-merchant-pools', // 支付商户与商户池管理
|
||||
PaymentMerchantPoolsDetail = '/settings/payment-merchant-pools/detail', // 支付商户详情
|
||||
PaymentMerchantPoolDetail = '/settings/payment-merchant-pools/pool-detail', // 商户池详情
|
||||
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
||||
SystemConfigs = '/settings/system-configs', // 系统配置
|
||||
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
||||
WecomApplications = '/settings/wecom/applications', // 企业微信应用
|
||||
WecomMembers = '/settings/wecom/members', // 企业微信成员
|
||||
WecomScenes = '/settings/wecom/scenes', // 企业微信审批场景
|
||||
H5PopupConfigurations = '/settings/h5-popup-configuration', // H5运营弹窗配置
|
||||
AssetWalletAutoRenewal = '/settings/asset-wallet-auto-renewal', // 资产钱包自动续费配置
|
||||
|
||||
// 轮询管理
|
||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||
@@ -134,7 +117,6 @@ export enum RoutesAlias {
|
||||
PollingConfig = '/polling-management/config', // 轮询配置
|
||||
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
|
||||
PollingMonitor = '/polling-management/monitor', // 轮询监控
|
||||
PollingPriorityQueue = '/polling-management/priority-queue', // 优先队列
|
||||
|
||||
// 公共组件
|
||||
Success = '/result/success', // 成功
|
||||
@@ -146,12 +128,7 @@ export enum RoutesAlias {
|
||||
Drag = '/widgets/drag', // 拖拽
|
||||
Charts = '/template/charts', // 图表
|
||||
Map = '/template/map', // 地图
|
||||
Calendar = '/template/calendar', // 日历
|
||||
|
||||
// 手机号资产关联
|
||||
PhoneAssetAssociation = '/asset-management/phone-asset-association', // 手机号资产关联列表
|
||||
PhoneAssetUnbindImportTasks = '/asset-management/phone-asset-association/unbind-import-tasks', // 解绑导入任务列表
|
||||
PhoneAssetUnbindImportTaskDetail = '/asset-management/phone-asset-association/unbind-import-tasks/detail' // 解绑导入任务详情
|
||||
Calendar = '/template/calendar' // 日历
|
||||
}
|
||||
|
||||
// 主页路由 - 修改为资产信息页面
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user