fix: some
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
# 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`
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
## 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 展示密码强度提示
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
## 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** 展示驳回原因、异常标记与尝试记录
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 店铺数据返回分销码
|
||||||
|
店铺列表、详情与更新接口响应 MUST 包含 `distribution_code` 字段,前端类型 MUST 覆盖该字段并在列表/详情中展示。
|
||||||
|
|
||||||
|
#### Scenario: 店铺详情展示分销码
|
||||||
|
- **GIVEN** 用户打开店铺详情页
|
||||||
|
- **WHEN** 店铺存在 `distribution_code`
|
||||||
|
- **THEN** 页面 MUST 展示分销码
|
||||||
|
- **AND** MUST 以该码生成注册入口二维码
|
||||||
|
|
||||||
|
#### Scenario: 分销码为空
|
||||||
|
- **GIVEN** 店铺尚未分配分销码
|
||||||
|
- **WHEN** 页面展示店铺信息
|
||||||
|
- **THEN** 分销码与二维码区域 MUST 展示空态(`-` 或「未分配」)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## 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` 的字段查询接口
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## 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`
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# 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[] 等)以生成文档为准,提案按对接说明先行对齐。
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
## 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 控制显隐
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
## 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 且明细为空
|
||||||
34
openspec/changes/add-business-user-group-management/tasks.md
Normal file
34
openspec/changes/add-business-user-group-management/tasks.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## 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
|
||||||
60
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
60
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# 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`。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## 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 区分越权与不存在两种原因
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## 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` 中已填写的项
|
||||||
27
openspec/changes/add-commission-clawback-records/tasks.md
Normal file
27
openspec/changes/add-commission-clawback-records/tasks.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## 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`
|
||||||
@@ -44,5 +44,5 @@
|
|||||||
## 7. Verification
|
## 7. Verification
|
||||||
|
|
||||||
- [x] 7.1 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`,确保类型与编码通过。
|
- [x] 7.1 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`,确保类型与编码通过。
|
||||||
- [ ] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。
|
- [x] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。 — 已核验:应用/账单/收款方式页 v-permission 按钮、bills 关闭操作 hasAuth(billClose)+canCloseBill、表单 isActing=isSuperAdmin 控制代办字段
|
||||||
- [ ] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。
|
- [x] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。 — 已核验:ApplicationFormDialog 用 fenToYuan/yuanToFen;detail.vue 用 PaymentVoucherDialog 预览凭证;getErrorMessage 提取后端 msg(含 503 未配置场景)兜底
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 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`)。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 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`)以后端菜单配置为准。
|
||||||
|
- 列表分页上限与「按最近更新时间倒序」以文档为准,联调核对后端行为。
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
## 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 出现删除操作,仅提供启用/停用
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
## 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` 被后端拒绝。
|
||||||
51
openspec/changes/add-package-traffic-alert/design.md
Normal file
51
openspec/changes/add-package-traffic-alert/design.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 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 数字过长
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
- 记录列表/详情接口字段名未完整给出,联调时以测试环境实际返回为准,类型定义需保留一定弹性(可选字段)
|
||||||
|
- 导出任务场景值未给出,若后端未登记场景枚举,导出任务列表页的场景筛选需兼容新值
|
||||||
93
openspec/changes/add-package-traffic-alert/proposal.md
Normal file
93
openspec/changes/add-package-traffic-alert/proposal.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 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 模块、类型、权限编码均为增量
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 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** 任务创建时冻结筛选条件、时间范围与可见资产范围,归属列按执行时当前归属补充
|
||||||
40
openspec/changes/add-package-traffic-alert/tasks.md
Normal file
40
openspec/changes/add-package-traffic-alert/tasks.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 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` 类型检查通过
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
- [x] 6.4 支付失败后不自动重放同一支付请求;用户主动重新发起一笔支付时按支付接口约定创建新的请求,不展示内部路由过程。
|
- [x] 6.4 支付失败后不自动重放同一支付请求;用户主动重新发起一笔支付时按支付接口约定创建新的请求,不展示内部路由过程。
|
||||||
- 映射函数显式不做任何路由/重试逻辑;调用方按需发起新请求。
|
- 映射函数显式不做任何路由/重试逻辑;调用方按需发起新请求。
|
||||||
- [ ] 6.5 若客户支付端位于本仓库之外的 H5、小程序或 App 工程,将本节的错误码和文案要求同步到对应工程,并登记联调责任方。
|
- [ ] 6.5 若客户支付端位于本仓库之外的 H5、小程序或 App 工程,将本节的错误码和文案要求同步到对应工程,并登记联调责任方。
|
||||||
|
- 按需求方指示 H5/小程序/App 客户端不在本仓库处理,本项保持未勾选,由对应工程跟进接入 `resolvePaymentFailureMessage`。
|
||||||
- 待联调责任方(前端/H5/小程序/App)接入 `resolvePaymentFailureMessage` 并完成文案与错误码校验。
|
- 待联调责任方(前端/H5/小程序/App)接入 `resolvePaymentFailureMessage` 并完成文案与错误码校验。
|
||||||
|
|
||||||
## 7. 验证
|
## 7. 验证
|
||||||
|
|||||||
37
openspec/changes/add-phone-asset-unbind-management/design.md
Normal file
37
openspec/changes/add-phone-asset-unbind-management/design.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 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=失败。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 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` 任务列表的筛选参数与分页响应结构(当前导出文档未包含该接口块)。
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
## 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 仅作透传展示,不在前端拼接或改写
|
||||||
66
openspec/changes/add-phone-asset-unbind-management/tasks.md
Normal file
66
openspec/changes/add-phone-asset-unbind-management/tasks.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
## 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 已按静态代码核验勾选(实现已落地);联调如发现契约偏差再修正。
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
|
|
||||||
## 8. Verification
|
## 8. Verification
|
||||||
|
|
||||||
- [ ] 8.1 校验 `methods` 为空、超管调用新接口 403、允许范围变更不影响存量待支付单三种情况。
|
- [x] 8.1 校验 `methods` 为空、超管调用新接口 403、允许范围变更不影响存量待支付单三种情况。 — 已核验:index.vue:100 空数组提示「当前暂无可用在线支付方式」;设置入口在 system-configs 且受 hasAuth 配置权限管控;允许范围不写回存量待支付单
|
||||||
- [ ] 8.2 校验 OCR 成功、失败、超时三条路径均不阻断人工填写与提交。
|
- [x] 8.2 校验 OCR 成功、失败、超时三条路径均不阻断人工填写与提交。 — 已核验:OCR 仅预填 external_transaction_no,ocrLoading 期间人工输入与提交不被阻断,标注「识别结果仅供参考」
|
||||||
- [ ] 8.3 校验线下单与在线单的两类交易号、收款方式快照与其他凭证展示正确。
|
- [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.4 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`。
|
||||||
- [x] 8.5 运行 `openspec validate update-agent-self-recharge-and-offline-approval --strict`。
|
- [x] 8.5 运行 `openspec validate update-agent-self-recharge-and-offline-approval --strict`。
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 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 打开结果抽屉。
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
## 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** 用户可以修复条件后再次发起确认完成
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## 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 打开结果抽屉
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## 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
|
||||||
25
openspec/changes/update-refund-management/proposal.md
Normal file
25
openspec/changes/update-refund-management/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 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`
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## 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 提示该商户因凭证不完整不可用于原路退款
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
## 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 隐藏或禁用
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## 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`,由后端沿用原值
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## 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 按既有凭证展示约定透出字段内容(不回显或仅展示存在性,按现状约定)
|
||||||
24
openspec/changes/update-refund-management/tasks.md
Normal file
24
openspec/changes/update-refund-management/tasks.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## 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`
|
||||||
74
src/api/modules/agentDistribution.ts
Normal file
74
src/api/modules/agentDistribution.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 代理扫码分销注册相关 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 } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/api/modules/businessUserGroup.ts
Normal file
93
src/api/modules/businessUserGroup.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* 业务用户组相关 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,6 +15,8 @@ import type {
|
|||||||
CommissionRecordQueryParams,
|
CommissionRecordQueryParams,
|
||||||
SubmitWithdrawalParams,
|
SubmitWithdrawalParams,
|
||||||
ShopCommissionRecordPageResult,
|
ShopCommissionRecordPageResult,
|
||||||
|
ShopCommissionRecordDetail,
|
||||||
|
ShopCommissionRecordSource,
|
||||||
ShopFundSummaryQueryParams,
|
ShopFundSummaryQueryParams,
|
||||||
ShopFundSummaryPageResult,
|
ShopFundSummaryPageResult,
|
||||||
ShopCommissionStatsQueryParams,
|
ShopCommissionStatsQueryParams,
|
||||||
@@ -23,10 +25,85 @@ import type {
|
|||||||
DailyCommissionStatsItem,
|
DailyCommissionStatsItem,
|
||||||
ResolveCommissionParams,
|
ResolveCommissionParams,
|
||||||
MainWalletTransactionQueryParams,
|
MainWalletTransactionQueryParams,
|
||||||
MainWalletTransactionPageResult
|
MainWalletTransactionPageResult,
|
||||||
|
WithdrawalQualificationSubmitParams,
|
||||||
|
WithdrawalQualificationQueryParams,
|
||||||
|
WithdrawalQualificationPageResult,
|
||||||
|
VoidWithdrawalQualificationParams,
|
||||||
|
ResubmitWithdrawalRequestParams,
|
||||||
|
WithdrawalRequestDetail,
|
||||||
} from '@/types/api/commission'
|
} from '@/types/api/commission'
|
||||||
|
|
||||||
export class CommissionService extends BaseService {
|
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}`
|
||||||
|
)
|
||||||
|
}
|
||||||
// ==================== 提现申请管理 ====================
|
// ==================== 提现申请管理 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +184,22 @@ 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
|
* GET /api/admin/shops/{shop_id}/withdrawal-requests
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { BaseResponse } from '@/types/api'
|
|||||||
export type ExchangeAssetType = 'iot_card' | 'device'
|
export type ExchangeAssetType = 'iot_card' | 'device'
|
||||||
export type ExchangeFlowType = 'shipping' | 'direct'
|
export type ExchangeFlowType = 'shipping' | 'direct'
|
||||||
export type ExchangeStatus = 1 | 2 | 3 | 4 | 5
|
export type ExchangeStatus = 1 | 2 | 3 | 4 | 5
|
||||||
|
export type ExchangeMigrationStatus = 'not_migrated' | 'pending' | 'migrated' | 'failed'
|
||||||
|
|
||||||
// 换货单查询参数
|
// 换货单查询参数
|
||||||
export interface ExchangeQueryParams {
|
export interface ExchangeQueryParams {
|
||||||
@@ -64,6 +65,9 @@ export interface ExchangeResponse {
|
|||||||
current_approver_summary?: string | null // 当前审批人摘要
|
current_approver_summary?: string | null // 当前审批人摘要
|
||||||
processing_status?: string | null // 业务处理状态
|
processing_status?: string | null // 业务处理状态
|
||||||
processing_status_name?: 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
|
remark?: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
|||||||
87
src/api/modules/h5PopupConfiguration.ts
Normal file
87
src/api/modules/h5PopupConfiguration.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/**
|
/**
|
||||||
* API 服务模块统一导出
|
* API 服务模块统一导出
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ export { AccountService } from './account'
|
|||||||
export { ShopService } from './shop'
|
export { ShopService } from './shop'
|
||||||
export { CardService } from './card'
|
export { CardService } from './card'
|
||||||
export { CommissionService } from './commission'
|
export { CommissionService } from './commission'
|
||||||
|
export { BusinessUserGroupService } from './businessUserGroup'
|
||||||
export { EnterpriseService } from './enterprise'
|
export { EnterpriseService } from './enterprise'
|
||||||
export { StorageService } from './storage'
|
export { StorageService } from './storage'
|
||||||
export { AuthorizationService } from './authorization'
|
export { AuthorizationService } from './authorization'
|
||||||
@@ -42,5 +43,13 @@ export { AuditService } from './audit'
|
|||||||
export { WecomService } from './wecom'
|
export { WecomService } from './wecom'
|
||||||
export { EmployeeCollectionService } from './employeeCollection'
|
export { EmployeeCollectionService } from './employeeCollection'
|
||||||
|
|
||||||
|
export { PhoneAssetAssociationService } from './phoneAsset'
|
||||||
|
|
||||||
|
export { AgentDistributionService } from './agentDistribution'
|
||||||
|
|
||||||
|
export { H5PopupConfigurationService } from './h5PopupConfiguration'
|
||||||
|
|
||||||
// TODO: 按需添加其他业务模块
|
// TODO: 按需添加其他业务模块
|
||||||
// export { SettingService } from './setting'
|
// export { SettingService } from './setting'
|
||||||
|
|
||||||
|
export { PackageTrafficAlertService } from './packageTrafficAlert'
|
||||||
|
|||||||
101
src/api/modules/packageTrafficAlert.ts
Normal file
101
src/api/modules/packageTrafficAlert.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* 套餐真流量预警 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/api/modules/phoneAsset.ts
Normal file
107
src/api/modules/phoneAsset.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* 手机号资产关联相关 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}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ import type {
|
|||||||
RefundListResponse,
|
RefundListResponse,
|
||||||
CreateRefundRequest,
|
CreateRefundRequest,
|
||||||
ResubmitRefundRequest,
|
ResubmitRefundRequest,
|
||||||
|
ApproveRefundRequest,
|
||||||
|
RejectRefundRequest,
|
||||||
|
ReturnRefundRequest,
|
||||||
BaseResponse
|
BaseResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
@@ -37,6 +40,33 @@ export class RefundService extends BaseService {
|
|||||||
return this.post<BaseResponse<Refund>>('/api/admin/refunds', data)
|
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
|
* @param id 退款申请ID
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import type {
|
|||||||
AssignShopRolesRequest,
|
AssignShopRolesRequest,
|
||||||
UpdateShopCreditLimitRequest,
|
UpdateShopCreditLimitRequest,
|
||||||
UpdateShopCreditLimitResponse,
|
UpdateShopCreditLimitResponse,
|
||||||
|
BatchUpdateBusinessOwnerParams,
|
||||||
|
BatchUpdateBusinessOwnerResult,
|
||||||
|
CreateBusinessOwnerImportParams,
|
||||||
|
BusinessOwnerImportDetail,
|
||||||
|
BusinessOwnerImportQueryParams,
|
||||||
BaseResponse,
|
BaseResponse,
|
||||||
PaginationResponse,
|
PaginationResponse,
|
||||||
ShopBusinessOwnerCandidate,
|
ShopBusinessOwnerCandidate,
|
||||||
@@ -155,4 +160,56 @@ export class ShopService extends BaseService {
|
|||||||
data
|
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,6 +27,8 @@ export type FilePurpose =
|
|||||||
| 'iot_import'
|
| 'iot_import'
|
||||||
| 'batch_purchase'
|
| 'batch_purchase'
|
||||||
| 'device_batch_allocation'
|
| 'device_batch_allocation'
|
||||||
|
| 'shop_import'
|
||||||
|
| 'phone_unbind_import'
|
||||||
| 'export'
|
| 'export'
|
||||||
| 'attachment'
|
| 'attachment'
|
||||||
|
|
||||||
|
|||||||
269
src/components/business/CommissionRecordDetailDialog.vue
Normal file
269
src/components/business/CommissionRecordDetailDialog.vue
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<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>
|
<template>
|
||||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="40%" @closed="handleDialogClosed">
|
<ElDialog v-model="dialogVisible" title="创建退款申请" width="45%" @closed="handleDialogClosed">
|
||||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||||
<ElFormItem label="订单号" prop="order_id">
|
<ElFormItem label="订单号" prop="order_id">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-if="!hasInitialOrder"
|
v-if="!hasInitialOrder"
|
||||||
@@ -23,6 +23,13 @@
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
||||||
</ElFormItem>
|
</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">
|
<ElFormItem label="退款金额" prop="requested_refund_amount">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="formData.requested_refund_amount"
|
v-model="formData.requested_refund_amount"
|
||||||
@@ -46,7 +53,21 @@
|
|||||||
placeholder="请输入退款原因"
|
placeholder="请输入退款原因"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
<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"
|
||||||
|
>
|
||||||
<VoucherUpload
|
<VoucherUpload
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
v-model="formData.refund_voucher_key"
|
v-model="formData.refund_voucher_key"
|
||||||
@@ -54,6 +75,9 @@
|
|||||||
@uploading-change="voucherUploading = $event"
|
@uploading-change="voucherUploading = $event"
|
||||||
@change="formRef?.validateField('refund_voucher_key')"
|
@change="formRef?.validateField('refund_voucher_key')"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
|
||||||
|
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -77,7 +101,8 @@
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import { RefundService, OrderService } from '@/api/modules'
|
import { RefundService, OrderService } from '@/api/modules'
|
||||||
import type { CreateRefundRequest, Order } from '@/types/api'
|
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
|
||||||
|
import { RefundMethodOptions } from '@/config/constants/refund'
|
||||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||||
import { getErrorMessage } from '@/utils/business'
|
import { getErrorMessage } from '@/utils/business'
|
||||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||||
@@ -110,16 +135,24 @@
|
|||||||
const formData = reactive<{
|
const formData = reactive<{
|
||||||
order_id: number | null
|
order_id: number | null
|
||||||
package_usage_id?: number
|
package_usage_id?: number
|
||||||
|
method?: RefundMethod
|
||||||
requested_refund_amount?: number
|
requested_refund_amount?: number
|
||||||
actual_received_amount: number
|
actual_received_amount: number
|
||||||
refund_reason: string
|
refund_reason: string
|
||||||
|
customer_account_name: string
|
||||||
|
customer_account_number: string
|
||||||
|
customer_bank_name: string
|
||||||
refund_voucher_key: string[]
|
refund_voucher_key: string[]
|
||||||
}>({
|
}>({
|
||||||
order_id: null,
|
order_id: null,
|
||||||
package_usage_id: undefined,
|
package_usage_id: undefined,
|
||||||
|
method: RefundMethod.ORIGINAL_ROUTE,
|
||||||
requested_refund_amount: undefined,
|
requested_refund_amount: undefined,
|
||||||
actual_received_amount: 0,
|
actual_received_amount: 0,
|
||||||
refund_reason: '',
|
refund_reason: '',
|
||||||
|
customer_account_name: '',
|
||||||
|
customer_account_number: '',
|
||||||
|
customer_bank_name: '',
|
||||||
refund_voucher_key: []
|
refund_voucher_key: []
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -134,14 +167,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>({
|
const formRules = reactive<FormRules>({
|
||||||
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
|
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
|
||||||
|
method: [{ required: true, message: '请选择退款方式', trigger: 'change' }],
|
||||||
requested_refund_amount: [
|
requested_refund_amount: [
|
||||||
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
|
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
|
||||||
{ validator: validateRefundAmount, trigger: 'blur' }
|
{ validator: validateRefundAmount, trigger: 'blur' }
|
||||||
],
|
],
|
||||||
refund_reason: [{ required: true, message: '请输入退款原因', trigger: 'blur' }],
|
refund_reason: [{ required: true, message: '请输入退款原因', trigger: 'blur' }],
|
||||||
refund_voucher_key: [{ required: true, message: '请上传退款凭证', trigger: 'change' }]
|
customer_account_name: requireCustomerAccount('customer_account_name', '请填写收款账户名'),
|
||||||
|
customer_account_number: requireCustomerAccount('customer_account_number', '请填写收款账号'),
|
||||||
|
refund_voucher_key: [{ validator: validateVoucherKey, trigger: 'change' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
|
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
|
||||||
@@ -202,9 +264,13 @@
|
|||||||
voucherUploading.value = false
|
voucherUploading.value = false
|
||||||
formData.order_id = null
|
formData.order_id = null
|
||||||
formData.package_usage_id = undefined
|
formData.package_usage_id = undefined
|
||||||
|
formData.method = RefundMethod.ORIGINAL_ROUTE
|
||||||
formData.requested_refund_amount = undefined
|
formData.requested_refund_amount = undefined
|
||||||
formData.actual_received_amount = 0
|
formData.actual_received_amount = 0
|
||||||
formData.refund_reason = ''
|
formData.refund_reason = ''
|
||||||
|
formData.customer_account_name = ''
|
||||||
|
formData.customer_account_number = ''
|
||||||
|
formData.customer_bank_name = ''
|
||||||
formData.refund_voucher_key = []
|
formData.refund_voucher_key = []
|
||||||
orderSearchOptions.value = []
|
orderSearchOptions.value = []
|
||||||
uploadRef.value?.clearFiles(false)
|
uploadRef.value?.clearFiles(false)
|
||||||
@@ -222,20 +288,30 @@
|
|||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data: CreateRefundRequest = {
|
const data: CreateRefundRequest = {
|
||||||
actual_received_amount: formData.actual_received_amount,
|
|
||||||
order_id: formData.order_id!,
|
order_id: formData.order_id!,
|
||||||
|
method: formData.method!,
|
||||||
package_usage_id: formData.package_usage_id ?? 0,
|
package_usage_id: formData.package_usage_id ?? 0,
|
||||||
refund_reason: formData.refund_reason,
|
refund_reason: formData.refund_reason,
|
||||||
refund_voucher_key: formData.refund_voucher_key,
|
|
||||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0
|
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0
|
||||||
}
|
}
|
||||||
|
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT) {
|
||||||
|
data.customer_account_info = {
|
||||||
|
account_name: formData.customer_account_name.trim(),
|
||||||
|
account_number: formData.customer_account_number.trim(),
|
||||||
|
...(formData.customer_bank_name.trim()
|
||||||
|
? { bank_name: 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)
|
await RefundService.createRefund(data)
|
||||||
ElMessage.success('退款申请创建成功')
|
ElMessage.success('退款申请创建成功')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
emit('success')
|
emit('success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
ElMessage.error(getErrorMessage(error, '退款申请创建失败'))
|
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -257,3 +333,12 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.voucher-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
201
src/components/business/RefundApprovalDialog.vue
Normal file
201
src/components/business/RefundApprovalDialog.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<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>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div
|
<div
|
||||||
v-if="pagination && tableData.length > 0"
|
v-if="pagination && (alwaysShowPagination || tableData.length > 0)"
|
||||||
class="table-pagination"
|
class="table-pagination"
|
||||||
:class="paginationAlign"
|
:class="paginationAlign"
|
||||||
>
|
>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
/** 是否显示加载状态 */
|
/** 是否显示加载状态 */
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
/** 行数据的 Key,用于标识每一行数据 */
|
/** 行数据的 Key,用于标识每一行数据 */
|
||||||
rowKey?: string
|
rowKey?: string | ((row: any) => string)
|
||||||
/** 行的 className 的回调方法 */
|
/** 行的 className 的回调方法 */
|
||||||
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
|
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
|
||||||
/** 是否显示边框 */
|
/** 是否显示边框 */
|
||||||
@@ -144,6 +144,8 @@
|
|||||||
pageSizes?: number[]
|
pageSizes?: number[]
|
||||||
/** 只有一页时是否隐藏分页器 */
|
/** 只有一页时是否隐藏分页器 */
|
||||||
hideOnSinglePage?: boolean
|
hideOnSinglePage?: boolean
|
||||||
|
/** 数据为空时也始终显示分页器 */
|
||||||
|
alwaysShowPagination?: boolean
|
||||||
/** 分页器的对齐方式 */
|
/** 分页器的对齐方式 */
|
||||||
paginationAlign?: 'left' | 'center' | 'right'
|
paginationAlign?: 'left' | 'center' | 'right'
|
||||||
/** 分页器的大小 */
|
/** 分页器的大小 */
|
||||||
@@ -185,6 +187,7 @@
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
hideOnSinglePage: false,
|
hideOnSinglePage: false,
|
||||||
|
alwaysShowPagination: false,
|
||||||
pageSizes: () => [10, 20, 30, 50],
|
pageSizes: () => [10, 20, 30, 50],
|
||||||
paginationAlign: 'center',
|
paginationAlign: 'center',
|
||||||
paginationSize: 'default',
|
paginationSize: 'default',
|
||||||
|
|||||||
@@ -11,10 +11,34 @@ export const AUGUST_PERMISSIONS = {
|
|||||||
applicationCreate: 'employee_collection:application_create',
|
applicationCreate: 'employee_collection:application_create',
|
||||||
applicationDetail: 'employee_collection:application_detail',
|
applicationDetail: 'employee_collection:application_detail',
|
||||||
applicationUpdate: 'employee_collection:application_update',
|
applicationUpdate: 'employee_collection:application_update',
|
||||||
|
applicationViewVoucher: 'employee_collection:application_view_voucher',
|
||||||
paymentMethodPage: 'employee_collection:payment_method_view',
|
paymentMethodPage: 'employee_collection:payment_method_view',
|
||||||
paymentMethodCreate: 'employee_collection:payment_method_create',
|
paymentMethodCreate: 'employee_collection:payment_method_create',
|
||||||
paymentMethodEdit: 'employee_collection:payment_method_edit',
|
paymentMethodEdit: 'employee_collection:payment_method_edit',
|
||||||
paymentMethodDelete: 'employee_collection:payment_method_delete'
|
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
|
} as const
|
||||||
|
|
||||||
|
|||||||
48
src/config/constants/businessUserGroup.ts
Normal file
48
src/config/constants/businessUserGroup.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 业务用户组相关常量与权限编码
|
||||||
|
*/
|
||||||
|
|
||||||
|
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,6 +10,7 @@ export const COMMISSION_STATUS_OPTIONS = [
|
|||||||
{ label: '解冻中', value: CommissionStatus.UNFREEZING, type: 'warning' as const },
|
{ label: '解冻中', value: CommissionStatus.UNFREEZING, type: 'warning' as const },
|
||||||
{ label: '已发放', value: CommissionStatus.RELEASED, type: 'success' as const },
|
{ label: '已发放', value: CommissionStatus.RELEASED, type: 'success' as const },
|
||||||
{ label: '已失效', value: CommissionStatus.INVALID, type: 'danger' 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 }
|
{ label: '待人工修正', value: CommissionStatus.PENDING_CORRECTION, type: 'warning' as const }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ export const CommissionStatusMap = {
|
|||||||
2: { label: '解冻中', type: 'warning' as const, color: '#E6A23C' },
|
2: { label: '解冻中', type: 'warning' as const, color: '#E6A23C' },
|
||||||
3: { label: '已发放', type: 'success' as const, color: '#67C23A' },
|
3: { label: '已发放', type: 'success' as const, color: '#67C23A' },
|
||||||
4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' },
|
4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' },
|
||||||
|
5: { label: '回溯', type: 'danger' as const, color: '#F56C6C' },
|
||||||
99: { label: '待人工修正', type: 'warning' as const, color: '#E6A23C' }
|
99: { label: '待人工修正', type: 'warning' as const, color: '#E6A23C' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,3 +115,33 @@ export function getCommissionTypeType(type: string) {
|
|||||||
const config = CommissionTypeMap[type as keyof typeof CommissionTypeMap]
|
const config = CommissionTypeMap[type as keyof typeof CommissionTypeMap]
|
||||||
return config?.type || 'info'
|
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,6 +84,24 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
detail: 'export_task:exchange_detail',
|
detail: 'export_task:exchange_detail',
|
||||||
download: 'export_task:exchange_download'
|
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,7 +34,12 @@ export * from './enableStatus'
|
|||||||
|
|
||||||
// 导出任务相关
|
// 导出任务相关
|
||||||
export * from './exportTask'
|
export * from './exportTask'
|
||||||
|
export * from './businessUserGroup'
|
||||||
|
|
||||||
|
// 套餐真流量预警相关
|
||||||
|
export * from './packageTrafficAlert'
|
||||||
|
|
||||||
// 批量订购相关
|
// 批量订购相关
|
||||||
export * from './bulkPurchase'
|
export * from './bulkPurchase'
|
||||||
export * from './julyIteration'
|
export * from './julyIteration'
|
||||||
|
export * from './augustIteration'
|
||||||
|
|||||||
60
src/config/constants/packageTrafficAlert.ts
Normal file
60
src/config/constants/packageTrafficAlert.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 套餐真流量预警相关常量
|
||||||
|
* 对应 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 }
|
||||||
|
]
|
||||||
17
src/config/constants/paymentMerchantPools.ts
Normal file
17
src/config/constants/paymentMerchantPools.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 支付商户与商户池管理权限编码。
|
||||||
|
* 页面和按钮统一引用这里的常量,后端菜单权限可直接复用同名编码。
|
||||||
|
* 编码命名参考代理充值页 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
|
||||||
89
src/config/constants/refund.ts
Normal file
89
src/config/constants/refund.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* 退款管理展示与规则常量
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
@@ -413,7 +413,10 @@
|
|||||||
"seriesAssignDetail": "Series Assignment Detail",
|
"seriesAssignDetail": "Series Assignment Detail",
|
||||||
"packageSeries": "Package Series",
|
"packageSeries": "Package Series",
|
||||||
"packageSeriesDetail": "Package Series Detail",
|
"packageSeriesDetail": "Package Series Detail",
|
||||||
"packageCommission": "Package Commission Cards"
|
"packageCommission": "Package Commission Cards",
|
||||||
|
"trafficAlertRules": "Traffic Alert Rules",
|
||||||
|
"trafficAlerts": "Traffic Alert Records",
|
||||||
|
"trafficAlertDetail": "Traffic Alert Record Detail"
|
||||||
},
|
},
|
||||||
"accountManagement": {
|
"accountManagement": {
|
||||||
"title": "Account Management",
|
"title": "Account Management",
|
||||||
@@ -455,7 +458,9 @@
|
|||||||
},
|
},
|
||||||
"shopManagement": {
|
"shopManagement": {
|
||||||
"title": "Shop Management",
|
"title": "Shop Management",
|
||||||
"shopList": "Shop List"
|
"shopList": "Shop List",
|
||||||
|
"businessUserGroups": "Business User Groups",
|
||||||
|
"businessOwnerImport": "Business Owner Import"
|
||||||
},
|
},
|
||||||
"assetManagement": {
|
"assetManagement": {
|
||||||
"title": "Asset Management",
|
"title": "Asset Management",
|
||||||
@@ -494,14 +499,18 @@
|
|||||||
"exportRefund": "Export Refunds",
|
"exportRefund": "Export Refunds",
|
||||||
"exportAgentRecharge": "Export Agent Recharges",
|
"exportAgentRecharge": "Export Agent Recharges",
|
||||||
"exportExchange": "Export Exchanges",
|
"exportExchange": "Export Exchanges",
|
||||||
|
"exportCommissionRecord": "Export Commission Records",
|
||||||
|
"exportPackageTrafficAlert": "Export Package Traffic Alerts",
|
||||||
"exportTaskDetail": "Export Task Detail",
|
"exportTaskDetail": "Export Task Detail",
|
||||||
"exchangeManagement": "Exchange Management",
|
"exchangeManagement": "Exchange Management",
|
||||||
"exchangeDetail": "Exchange Order Detail"
|
"exchangeDetail": "Exchange Order Detail",
|
||||||
|
"phoneAssetAssociation": "Phone-Asset Associations",
|
||||||
|
"phoneAssetUnbindImportTasks": "Unbind Import Tasks",
|
||||||
|
"phoneAssetUnbindImportTaskDetail": "Unbind Import Task Detail"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings Management",
|
"title": "Settings Management",
|
||||||
"paymentSettings": "Payment Settings",
|
"paymentSettings": "Payment Settings",
|
||||||
"agentSelfRecharge": "Agent Self-Recharge Settings",
|
|
||||||
"detailsOfPaymentConfiguration": "Payment Configuration Details",
|
"detailsOfPaymentConfiguration": "Payment Configuration Details",
|
||||||
"paymentMerchant": "Payment Merchant",
|
"paymentMerchant": "Payment Merchant",
|
||||||
"developerApi": "Developer API",
|
"developerApi": "Developer API",
|
||||||
@@ -509,7 +518,10 @@
|
|||||||
"paymentMerchantPools": "Merchant Pool Management",
|
"paymentMerchantPools": "Merchant Pool Management",
|
||||||
"paymentMerchantPoolsTabMerchants": "Payment Merchants",
|
"paymentMerchantPoolsTabMerchants": "Payment Merchants",
|
||||||
"paymentMerchantPoolsTabPools": "Merchant Pools",
|
"paymentMerchantPoolsTabPools": "Merchant Pools",
|
||||||
"paymentMerchantPoolsTabWechatAuth": "WeChat Authorization"
|
"paymentMerchantPoolsTabWechatAuth": "WeChat Authorization",
|
||||||
|
"detailsOfPaymentMerchant": "Payment Merchant Details",
|
||||||
|
"detailsOfPaymentMerchantPool": "Merchant Pool Details",
|
||||||
|
"h5PopupConfiguration": "H5 Popup Configurations"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"title": "Batch Operations",
|
"title": "Batch Operations",
|
||||||
|
|||||||
@@ -380,7 +380,10 @@
|
|||||||
"seriesGrantsDetail": "代理系列授权详情",
|
"seriesGrantsDetail": "代理系列授权详情",
|
||||||
"seriesGrantPackages": "代理系列授权套餐列表",
|
"seriesGrantPackages": "代理系列授权套餐列表",
|
||||||
"packageSeries": "套餐系列",
|
"packageSeries": "套餐系列",
|
||||||
"packageSeriesDetail": "套餐系列详情"
|
"packageSeriesDetail": "套餐系列详情",
|
||||||
|
"trafficAlertRules": "真流量预警规则",
|
||||||
|
"trafficAlerts": "真流量达量预警",
|
||||||
|
"trafficAlertDetail": "真流量达量预警详情"
|
||||||
},
|
},
|
||||||
"accountManagement": {
|
"accountManagement": {
|
||||||
"title": "账号管理",
|
"title": "账号管理",
|
||||||
@@ -391,7 +394,9 @@
|
|||||||
},
|
},
|
||||||
"shopManagement": {
|
"shopManagement": {
|
||||||
"title": "店铺管理",
|
"title": "店铺管理",
|
||||||
"shopList": "店铺列表"
|
"shopList": "店铺列表",
|
||||||
|
"businessUserGroups": "业务用户组",
|
||||||
|
"businessOwnerImport": "业务负责人导入"
|
||||||
},
|
},
|
||||||
"assetManagement": {
|
"assetManagement": {
|
||||||
"title": "资产管理",
|
"title": "资产管理",
|
||||||
@@ -423,9 +428,14 @@
|
|||||||
"exportRefund": "导出退款",
|
"exportRefund": "导出退款",
|
||||||
"exportAgentRecharge": "导出代理充值",
|
"exportAgentRecharge": "导出代理充值",
|
||||||
"exportExchange": "导出换货",
|
"exportExchange": "导出换货",
|
||||||
|
"exportCommissionRecord": "导出佣金记录",
|
||||||
|
"exportPackageTrafficAlert": "导出套餐真流量预警",
|
||||||
"exportTaskDetail": "导出任务详情",
|
"exportTaskDetail": "导出任务详情",
|
||||||
"exchangeManagement": "换货管理",
|
"exchangeManagement": "换货管理",
|
||||||
"exchangeDetail": "换货单详情"
|
"exchangeDetail": "换货单详情",
|
||||||
|
"phoneAssetAssociation": "手机号资产关联",
|
||||||
|
"phoneAssetUnbindImportTasks": "解绑导入任务",
|
||||||
|
"phoneAssetUnbindImportTaskDetail": "解绑导入任务详情"
|
||||||
},
|
},
|
||||||
"orderManagement": {
|
"orderManagement": {
|
||||||
"title": "订单管理",
|
"title": "订单管理",
|
||||||
@@ -453,14 +463,16 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"title": "设置管理",
|
"title": "设置管理",
|
||||||
"paymentSettings": "支付设置",
|
"paymentSettings": "支付设置",
|
||||||
"agentSelfRecharge": "代理自充设置",
|
|
||||||
"detailsOfPaymentConfiguration": "支付配置详情",
|
"detailsOfPaymentConfiguration": "支付配置详情",
|
||||||
"withdrawalSettings": "提现配置",
|
"withdrawalSettings": "提现配置",
|
||||||
"passwordSettings": "密码设置",
|
"passwordSettings": "密码设置",
|
||||||
"paymentMerchantPools": "商户池管理",
|
"paymentMerchantPools": "商户池管理",
|
||||||
"paymentMerchantPoolsTabMerchants": "支付商户",
|
"paymentMerchantPoolsTabMerchants": "支付商户",
|
||||||
"paymentMerchantPoolsTabPools": "商户池",
|
"paymentMerchantPoolsTabPools": "商户池",
|
||||||
"paymentMerchantPoolsTabWechatAuth": "微信授权配置"
|
"paymentMerchantPoolsTabWechatAuth": "微信授权配置",
|
||||||
|
"detailsOfPaymentMerchant": "支付商户详情",
|
||||||
|
"detailsOfPaymentMerchantPool": "商户池详情",
|
||||||
|
"h5PopupConfiguration": "H5运营弹窗配置"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ function convertBackendMenuToRoute(
|
|||||||
path: menuUrl,
|
path: menuUrl,
|
||||||
name: matchedRoute.name,
|
name: matchedRoute.name,
|
||||||
component: matchedRoute.component,
|
component: matchedRoute.component,
|
||||||
|
redirect: matchedRoute.redirect,
|
||||||
meta: {
|
meta: {
|
||||||
...matchedRoute.meta,
|
...matchedRoute.meta,
|
||||||
title: menu.name,
|
title: menu.name,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useUserStore } from '@/store/modules/user'
|
|||||||
* 不需要登录的路由白名单
|
* 不需要登录的路由白名单
|
||||||
*/
|
*/
|
||||||
export const LOGIN_WHITE_LIST = [
|
export const LOGIN_WHITE_LIST = [
|
||||||
|
'/agent-registration',
|
||||||
'/auth/login',
|
'/auth/login',
|
||||||
'/exception/403',
|
'/exception/403',
|
||||||
'/exception/404',
|
'/exception/404',
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
|||||||
import { JULY_PERMISSIONS } from '@/config/constants/julyIteration'
|
import { JULY_PERMISSIONS } from '@/config/constants/julyIteration'
|
||||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
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'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 菜单列表、异步路由
|
* 菜单列表、异步路由
|
||||||
@@ -206,6 +211,37 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
isHide: true,
|
isHide: true,
|
||||||
keepAlive: false
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -240,6 +276,30 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
isHide: true,
|
isHide: true,
|
||||||
keepAlive: false
|
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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -359,6 +419,40 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: false
|
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',
|
path: 'record-management',
|
||||||
@@ -590,6 +684,26 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
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',
|
path: 'export-task-detail',
|
||||||
name: 'ExportTaskDetail',
|
name: 'ExportTaskDetail',
|
||||||
@@ -915,7 +1029,8 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
title: 'menus.settings.detailsOfPaymentMerchant',
|
title: 'menus.settings.detailsOfPaymentMerchant',
|
||||||
isHide: true,
|
isHide: true,
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
allowedUserTypes: [1, 2]
|
allowedUserTypes: [1, 2],
|
||||||
|
permissions: [PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantDetail]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 商户池详情
|
// 商户池详情
|
||||||
@@ -927,7 +1042,8 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
title: 'menus.settings.detailsOfPaymentMerchantPool',
|
title: 'menus.settings.detailsOfPaymentMerchantPool',
|
||||||
isHide: true,
|
isHide: true,
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
allowedUserTypes: [1, 2]
|
allowedUserTypes: [1, 2],
|
||||||
|
permissions: [PAYMENT_MERCHANT_POOL_PERMISSIONS.poolDetail]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 支付设置详情
|
// 支付设置详情
|
||||||
@@ -963,17 +1079,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
roles: ['R_SUPER', 'R_ADMIN']
|
roles: ['R_SUPER', 'R_ADMIN']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 代理自充设置(跳转系统配置并带上模块筛选)
|
|
||||||
{
|
|
||||||
path: 'agent-self-recharge',
|
|
||||||
name: 'AgentSelfRechargeSettings',
|
|
||||||
redirect: { path: RoutesAlias.SystemConfigs, query: { module: 'c2b.payment' } },
|
|
||||||
meta: {
|
|
||||||
title: 'menus.settings.agentSelfRecharge',
|
|
||||||
keepAlive: false,
|
|
||||||
roles: ['R_SUPER']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'wecom',
|
path: 'wecom',
|
||||||
name: 'WecomSettings',
|
name: 'WecomSettings',
|
||||||
@@ -1016,6 +1121,18 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
roles: ['R_SUPER', 'R_ADMIN']
|
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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
component: () => import('@views/auth/login/index.vue'),
|
component: () => import('@views/auth/login/index.vue'),
|
||||||
meta: { title: 'menus.login.title', isHideTab: true, setTheme: true }
|
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',
|
path: '/exception',
|
||||||
component: Home,
|
component: Home,
|
||||||
|
|||||||
@@ -38,10 +38,15 @@ export enum RoutesAlias {
|
|||||||
SeriesGrants = '/package-management/series-grants', // 代理系列授权
|
SeriesGrants = '/package-management/series-grants', // 代理系列授权
|
||||||
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
|
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
|
||||||
SeriesGrantPackages = '/package-management/series-grants/packages', // 代理系列授权套餐列表
|
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', // 店铺列表
|
Shop = '/shop-management/list', // 店铺列表
|
||||||
ShopDetail = '/shop-management/detail', // 店铺详情
|
ShopDetail = '/shop-management/detail', // 店铺详情
|
||||||
|
BusinessUserGroups = '/shop-management/business-user-groups', // 业务用户组
|
||||||
|
BusinessOwnerImport = '/shop-management/business-owner-import', // 业务负责人导入
|
||||||
|
|
||||||
// 通用页面(店铺账号-企业账号)
|
// 通用页面(店铺账号-企业账号)
|
||||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用
|
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用
|
||||||
@@ -79,6 +84,8 @@ export enum RoutesAlias {
|
|||||||
ExportRefundTaskList = '/asset-management/export-task-management/export-refund', // 导出退款
|
ExportRefundTaskList = '/asset-management/export-task-management/export-refund', // 导出退款
|
||||||
ExportAgentRechargeTaskList = '/asset-management/export-task-management/export-agent-recharge', // 导出代理充值
|
ExportAgentRechargeTaskList = '/asset-management/export-task-management/export-agent-recharge', // 导出代理充值
|
||||||
ExportExchangeTaskList = '/asset-management/export-task-management/export-exchange', // 导出换货
|
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', // 导出任务详情
|
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
|
||||||
|
|
||||||
// 订单管理
|
// 订单管理
|
||||||
@@ -112,11 +119,11 @@ export enum RoutesAlias {
|
|||||||
PaymentMerchantPoolDetail = '/settings/payment-merchant-pools/pool-detail', // 商户池详情
|
PaymentMerchantPoolDetail = '/settings/payment-merchant-pools/pool-detail', // 商户池详情
|
||||||
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
||||||
SystemConfigs = '/settings/system-configs', // 系统配置
|
SystemConfigs = '/settings/system-configs', // 系统配置
|
||||||
AgentSelfRechargeSettings = '/settings/agent-self-recharge', // 代理自充设置
|
|
||||||
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
||||||
WecomApplications = '/settings/wecom/applications', // 企业微信应用
|
WecomApplications = '/settings/wecom/applications', // 企业微信应用
|
||||||
WecomMembers = '/settings/wecom/members', // 企业微信成员
|
WecomMembers = '/settings/wecom/members', // 企业微信成员
|
||||||
WecomScenes = '/settings/wecom/scenes', // 企业微信审批场景
|
WecomScenes = '/settings/wecom/scenes', // 企业微信审批场景
|
||||||
|
H5PopupConfigurations = '/settings/h5-popup-configuration', // H5运营弹窗配置
|
||||||
|
|
||||||
// 轮询管理
|
// 轮询管理
|
||||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||||
@@ -137,7 +144,12 @@ export enum RoutesAlias {
|
|||||||
Drag = '/widgets/drag', // 拖拽
|
Drag = '/widgets/drag', // 拖拽
|
||||||
Charts = '/template/charts', // 图表
|
Charts = '/template/charts', // 图表
|
||||||
Map = '/template/map', // 地图
|
Map = '/template/map', // 地图
|
||||||
Calendar = '/template/calendar' // 日历
|
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' // 解绑导入任务详情
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主页路由 - 修改为资产信息页面
|
// 主页路由 - 修改为资产信息页面
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AppRouteRecord } from '@/types/router'
|
|||||||
* @returns 处理后的路由配置
|
* @returns 处理后的路由配置
|
||||||
*/
|
*/
|
||||||
export const menuDataToRouter = (route: AppRouteRecord, parentPath = ''): AppRouteRecord => {
|
export const menuDataToRouter = (route: AppRouteRecord, parentPath = ''): AppRouteRecord => {
|
||||||
const { id, name, component, meta, children } = route
|
const { id, name, component, redirect, meta, children } = route
|
||||||
|
|
||||||
const fullPath = buildRoutePath(route, parentPath)
|
const fullPath = buildRoutePath(route, parentPath)
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export const menuDataToRouter = (route: AppRouteRecord, parentPath = ''): AppRou
|
|||||||
name,
|
name,
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
component,
|
component,
|
||||||
|
redirect,
|
||||||
meta,
|
meta,
|
||||||
children: processChildren(children || [], fullPath)
|
children: processChildren(children || [], fullPath)
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/template/业务负责人导入模板.csv
Normal file
1
src/template/业务负责人导入模板.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
店铺编码,操作类型,业务员登录账号,备注
|
||||||
|
@@ -159,6 +159,7 @@ export interface AssetExchangeTrace {
|
|||||||
export interface AssetResolveResponse {
|
export interface AssetResolveResponse {
|
||||||
asset_type: AssetType // 资产类型:card 或 device
|
asset_type: AssetType // 资产类型:card 或 device
|
||||||
asset_id: number // 数据库 ID
|
asset_id: number // 数据库 ID
|
||||||
|
associated_phones?: string[] // 当前关联手机号列表(完整手机号,无关联时为空数组)
|
||||||
identifier?: string // 原样回传本次查询所用的标识符
|
identifier?: string // 原样回传本次查询所用的标识符
|
||||||
virtual_no: string // 虚拟号
|
virtual_no: string // 虚拟号
|
||||||
status: number // 资产状态
|
status: number // 资产状态
|
||||||
|
|||||||
74
src/types/api/businessUserGroup.ts
Normal file
74
src/types/api/businessUserGroup.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 业务用户组相关类型定义 - 匹配后端 AUG26-003 接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PaginationParams } from './common'
|
||||||
|
|
||||||
|
/** 业务线枚举:standard 标品 / smart 智能产品 / other 其他 */
|
||||||
|
export type BusinessUserGroupBusinessLine = 'standard' | 'smart' | 'other'
|
||||||
|
|
||||||
|
/** 业务用户组响应实体 */
|
||||||
|
export interface BusinessUserGroupItem {
|
||||||
|
id: number // 业务用户组ID
|
||||||
|
code: string // 业务用户组稳定编码,创建后不可修改
|
||||||
|
name: string // 业务用户组名称
|
||||||
|
business_line: string // 所属业务线,空字符串表示未设置
|
||||||
|
business_line_name: string // 所属业务线中文名称,未设置时为空字符串
|
||||||
|
enabled: boolean // 是否启用;停用后不得新增成员,也不得作为批量目标
|
||||||
|
sort: number // 排序值
|
||||||
|
remark: string // 备注
|
||||||
|
created_at: string // 创建时间
|
||||||
|
updated_at: string // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务用户组列表查询参数 */
|
||||||
|
export interface BusinessUserGroupQueryParams extends PaginationParams {
|
||||||
|
page?: number // 页码,默认 1
|
||||||
|
page_size?: number // 每页数量,默认 20,最大 100
|
||||||
|
enabled?: boolean // 按启用状态过滤;不传返回全部
|
||||||
|
keyword?: string // 按稳定编码或名称模糊搜索,最多 100 字符
|
||||||
|
business_line?: BusinessUserGroupBusinessLine | '' // 按所属业务线过滤
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务用户组分页响应 */
|
||||||
|
export interface BusinessUserGroupPageResult {
|
||||||
|
items: BusinessUserGroupItem[] | null
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
total?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建业务用户组请求 */
|
||||||
|
export interface CreateBusinessUserGroupParams {
|
||||||
|
code: string // 稳定编码,1-64 字符,未删除组内唯一,创建后不可修改
|
||||||
|
name: string // 名称,1-100 字符
|
||||||
|
business_line?: BusinessUserGroupBusinessLine | '' // 所属业务线,留空表示不设置
|
||||||
|
enabled?: boolean // 是否启用,默认启用
|
||||||
|
sort?: number // 排序值,非负整数,默认 0
|
||||||
|
remark?: string // 备注,最多 500 字符
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新业务用户组请求(部分更新,全部可选) */
|
||||||
|
export interface UpdateBusinessUserGroupParams {
|
||||||
|
name?: string // 名称,1-100 字符
|
||||||
|
business_line?: BusinessUserGroupBusinessLine | '' | null // 三态:缺省不改 / "" 清空 / 枚举设置
|
||||||
|
enabled?: boolean // 是否启用
|
||||||
|
sort?: number // 排序值,非负整数
|
||||||
|
remark?: string // 备注,最多 500 字符
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除业务用户组请求 */
|
||||||
|
export interface DeleteBusinessUserGroupParams {
|
||||||
|
confirm: boolean // 确认删除,必须为 true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量设置/清空成员请求 */
|
||||||
|
export interface SetBusinessUserGroupMembersParams {
|
||||||
|
account_ids: number[] // 平台用户账号ID列表,至少一个;每个账号必须是启用平台用户
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量设置/清空成员结果 */
|
||||||
|
export interface BusinessUserGroupMembersResult {
|
||||||
|
group_id: number // 目标业务用户组ID;清空操作为 0
|
||||||
|
account_ids: number[] | null // 本次成功维护归属的平台用户账号ID列表
|
||||||
|
}
|
||||||
@@ -384,6 +384,7 @@ export interface StandaloneIotCard {
|
|||||||
gateway_card_imei?: string // 网关侧返回的卡 IMEI(可空)
|
gateway_card_imei?: string // 网关侧返回的卡 IMEI(可空)
|
||||||
id: number // 卡ID
|
id: number // 卡ID
|
||||||
iccid: string // ICCID
|
iccid: string // ICCID
|
||||||
|
associated_phones?: string[] // 当前关联手机号列表(完整手机号,无关联时为空数组)
|
||||||
imsi?: string // IMSI (可选)
|
imsi?: string // IMSI (可选)
|
||||||
msisdn?: string // 卡接入号 (可选)
|
msisdn?: string // 卡接入号 (可选)
|
||||||
virtual_no?: string // 卡虚拟号(可空)
|
virtual_no?: string // 卡虚拟号(可空)
|
||||||
|
|||||||
@@ -4,12 +4,131 @@
|
|||||||
|
|
||||||
import { PaginationParams } from '@/types'
|
import { PaginationParams } from '@/types'
|
||||||
|
|
||||||
|
// ==================== 提现资料资格相关 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交/替换提现资料资格请求参数
|
||||||
|
*/
|
||||||
|
export interface WithdrawalQualificationSubmitParams {
|
||||||
|
subject_type: string // 主体类型(enterprise 等)
|
||||||
|
subject_code: string // 主体证件号
|
||||||
|
legal_person_id_card: string // 法人身份证号
|
||||||
|
contract_file_key: string // 合同附件 Key
|
||||||
|
id_card_front_file_key: string // 身份证正面 Key
|
||||||
|
id_card_back_file_key: string // 身份证背面 Key
|
||||||
|
business_license_file_key?: string // 营业执照 Key
|
||||||
|
shop_front_file_key?: string // 门头照 Key
|
||||||
|
invoice_file_key?: string // 发票附件 Key
|
||||||
|
invoice_title?: string // 发票抬头
|
||||||
|
invoice_subject_code?: string // 发票主体证件号
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现资料资格项
|
||||||
|
*/
|
||||||
|
export interface WithdrawalQualificationItem {
|
||||||
|
id: number // 资格ID
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
shop_name: string // 店铺名称
|
||||||
|
subject_type: string // 主体类型
|
||||||
|
subject_type_name: string // 主体类型名称
|
||||||
|
subject_code_masked: string // 脱敏后主体证件号
|
||||||
|
legal_person_id_card_masked: string // 脱敏后法人身份证号
|
||||||
|
status: number // 状态(0 待审批等)
|
||||||
|
status_name: string // 状态名称
|
||||||
|
approval_instance_id?: number // 审批实例ID
|
||||||
|
approval_status?: number // 审批状态
|
||||||
|
approval_status_name?: string // 审批状态名称
|
||||||
|
contract_file_key?: string // 合同附件 Key
|
||||||
|
id_card_front_file_key?: string // 身份证正面 Key
|
||||||
|
id_card_back_file_key?: string // 身份证背面 Key
|
||||||
|
business_license_file_key?: string // 营业执照 Key
|
||||||
|
shop_front_file_key?: string // 门头照 Key
|
||||||
|
invoice_file_key?: string // 发票附件 Key
|
||||||
|
invoice_title?: string // 发票抬头
|
||||||
|
invalid_reason?: string // 作废原因
|
||||||
|
invalidated_at?: string | null // 作废时间
|
||||||
|
created_at: string // 创建时间
|
||||||
|
updated_at: string // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现资料资格查询参数
|
||||||
|
*/
|
||||||
|
export interface WithdrawalQualificationQueryParams extends PaginationParams {
|
||||||
|
status?: number // 状态筛选
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现资料资格分页结果
|
||||||
|
*/
|
||||||
|
export interface WithdrawalQualificationPageResult {
|
||||||
|
items: WithdrawalQualificationItem[] | null
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作废提现资料资格请求参数
|
||||||
|
*/
|
||||||
|
export interface VoidWithdrawalQualificationParams {
|
||||||
|
reason: string // 作废原因(必填)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重提被驳回的提现请求参数
|
||||||
|
*/
|
||||||
|
export interface ResubmitWithdrawalRequestParams {
|
||||||
|
account_name: string // 收款账户名
|
||||||
|
account_number: string // 收款账号
|
||||||
|
amount: number // 提现金额(分)
|
||||||
|
withdrawal_method: WithdrawalMethod // 提现方式
|
||||||
|
invoice_keys?: string[] // 发票附件 Key 列表
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现申请尝试记录项
|
||||||
|
*/
|
||||||
|
export interface WithdrawalAttemptItem {
|
||||||
|
attempt: number // 第几次尝试
|
||||||
|
status: number // 状态
|
||||||
|
status_name: string // 状态名称
|
||||||
|
reject_reason?: string // 驳回原因
|
||||||
|
created_at: string // 尝试时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现申请详情
|
||||||
|
*/
|
||||||
|
export interface WithdrawalRequestDetail {
|
||||||
|
id: number // 提现申请ID
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
shop_name: string // 店铺名称
|
||||||
|
withdrawal_no: string // 提现单号
|
||||||
|
withdrawal_method: WithdrawalMethod // 提现方式
|
||||||
|
account_name: string // 收款账户名
|
||||||
|
account_number: string // 收款账号
|
||||||
|
amount: number // 提现金额(分)
|
||||||
|
actual_amount: number // 实际到账(分)
|
||||||
|
fee: number // 手续费(分)
|
||||||
|
fee_rate: number // 手续费率(基点,100=1%)
|
||||||
|
status: number // 状态
|
||||||
|
status_name: string // 状态名称
|
||||||
|
reject_reason?: string // 驳回原因
|
||||||
|
anomaly_flag: number // 异常标记
|
||||||
|
anomaly_name: string // 异常名称
|
||||||
|
anomaly_reason?: string // 异常原因
|
||||||
|
attempts?: WithdrawalAttemptItem[] // 尝试记录
|
||||||
|
}
|
||||||
|
|
||||||
// 佣金状态
|
// 佣金状态
|
||||||
export enum CommissionStatus {
|
export enum CommissionStatus {
|
||||||
FROZEN = 1, // 已冻结
|
FROZEN = 1, // 已冻结
|
||||||
UNFREEZING = 2, // 解冻中
|
UNFREEZING = 2, // 解冻中
|
||||||
RELEASED = 3, // 已发放
|
RELEASED = 3, // 已发放
|
||||||
INVALID = 4, // 已失效
|
INVALID = 4, // 已失效
|
||||||
|
CLAWBACK = 5, // 回溯
|
||||||
PENDING_CORRECTION = 99 // 待人工修正
|
PENDING_CORRECTION = 99 // 待人工修正
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +265,7 @@ export interface MyCommissionRecordItem {
|
|||||||
export interface CommissionRecordQueryParams extends PaginationParams {
|
export interface CommissionRecordQueryParams extends PaginationParams {
|
||||||
page_size?: number // 每页数量
|
page_size?: number // 每页数量
|
||||||
commission_source?: string // 佣金来源(cost_diff:成本差价,one_time:一次性佣金)
|
commission_source?: string // 佣金来源(cost_diff:成本差价,one_time:一次性佣金)
|
||||||
status?: number // 状态(1已入账,2已失效)
|
status?: number // 状态(1已冻结,2解冻中,3已发放,4已失效,5回溯,99待人工修正)
|
||||||
start_time?: string // 开始时间
|
start_time?: string // 开始时间
|
||||||
end_time?: string // 结束时间
|
end_time?: string // 结束时间
|
||||||
iccid?: string // ICCID(模糊查询)
|
iccid?: string // ICCID(模糊查询)
|
||||||
@@ -189,7 +308,7 @@ export interface ShopCommissionRecordItem {
|
|||||||
amount: number // 佣金金额(分)
|
amount: number // 佣金金额(分)
|
||||||
balance_after: number // 入账后佣金余额(分)
|
balance_after: number // 入账后佣金余额(分)
|
||||||
commission_source: string // 佣金来源(cost_diff:成本差价,one_time:一次性佣金)
|
commission_source: string // 佣金来源(cost_diff:成本差价,one_time:一次性佣金)
|
||||||
status: number // 状态(1已冻结, 2解冻中, 3已发放, 4已失效)
|
status: number // 状态(1已冻结, 2解冻中, 3已发放, 4已失效, 5回溯, 99待人工修正)
|
||||||
status_name: string // 状态名称
|
status_name: string // 状态名称
|
||||||
order_id?: number // 订单ID
|
order_id?: number // 订单ID
|
||||||
order_no?: string // 订单号
|
order_no?: string // 订单号
|
||||||
@@ -199,6 +318,45 @@ export interface ShopCommissionRecordItem {
|
|||||||
created_at: string // 佣金入账时间
|
created_at: string // 佣金入账时间
|
||||||
seller_shop_id?: number // 卖家店铺ID
|
seller_shop_id?: number // 卖家店铺ID
|
||||||
seller_shop_name?: string // 卖家店铺名称
|
seller_shop_name?: string // 卖家店铺名称
|
||||||
|
source?: ShopCommissionRecordSource // 记录来源(original:原佣金, clawback:回溯明细)
|
||||||
|
released_at?: string // 佣金入账时间,回溯明细为空
|
||||||
|
withdrawable?: boolean | null // 是否可提现,仅回溯明细返回且恒为 false
|
||||||
|
original_commission_id?: number // 被回溯的原佣金记录ID,仅回溯明细返回
|
||||||
|
refund_id?: number // 来源退款申请ID,仅回溯明细返回
|
||||||
|
refund_no?: string // 来源退款单号,仅回溯明细返回
|
||||||
|
clawback_records?: ShopCommissionClawbackItem[] | null // 该原佣金已生成的全部回溯明细,仅原佣金返回
|
||||||
|
clawback_total_amount?: number | null // 该原佣金累计回溯金额(分,负值),仅原佣金返回
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 佣金记录来源
|
||||||
|
*/
|
||||||
|
export type ShopCommissionRecordSource = 'original' | 'clawback'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 佣金回溯明细项(原佣金详情中返回的回溯摘要,或回溯明细行)
|
||||||
|
*/
|
||||||
|
export interface ShopCommissionClawbackItem {
|
||||||
|
id: number // 回溯明细ID
|
||||||
|
amount: number // 回溯金额(分),恒为负数
|
||||||
|
balance_after: number // 回溯后佣金余额(分),可为负数
|
||||||
|
original_commission_id: number // 被回溯的原佣金记录ID
|
||||||
|
refund_id?: number // 来源退款申请ID
|
||||||
|
refund_no?: string // 来源退款单号
|
||||||
|
status: number // 状态(5:回溯)
|
||||||
|
status_name?: string // 状态名称
|
||||||
|
withdrawable?: boolean | null // 是否可提现,回溯明细恒为不可提现
|
||||||
|
created_at: string // 生成时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 佣金明细详情响应
|
||||||
|
*/
|
||||||
|
export interface ShopCommissionRecordDetail {
|
||||||
|
source?: ShopCommissionRecordSource // 记录来源(original:原佣金, clawback:回溯明细)
|
||||||
|
record: ShopCommissionRecordItem // 当前记录
|
||||||
|
clawback_records?: ShopCommissionClawbackItem[] | null // 该原佣金已生成的全部回溯明细,仅原佣金详情返回
|
||||||
|
original_commission?: ShopCommissionRecordItem | null // 回溯明细对应的原佣金,仅回溯详情返回
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type SwitchMode = '0' | '1' // 0=自动, 1=手动
|
|||||||
export interface Device {
|
export interface Device {
|
||||||
id: number // 设备ID
|
id: number // 设备ID
|
||||||
virtual_no: string // 虚拟号(原 device_no)
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
|
associated_phones?: string[] // 当前关联手机号列表(完整手机号,无关联时为空数组)
|
||||||
device_name: string // 设备名称
|
device_name: string // 设备名称
|
||||||
device_model: string // 设备型号
|
device_model: string // 设备型号
|
||||||
device_type: string // 设备类型
|
device_type: string // 设备类型
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export type ExportTaskScene =
|
|||||||
| 'agent_recharge'
|
| 'agent_recharge'
|
||||||
| 'refund'
|
| 'refund'
|
||||||
| 'exchange'
|
| 'exchange'
|
||||||
|
| 'commission_record'
|
||||||
|
| 'package_traffic_alert'
|
||||||
|
|
||||||
export type ExportTaskFormat = 'xlsx' | 'csv'
|
export type ExportTaskFormat = 'xlsx' | 'csv'
|
||||||
|
|
||||||
|
|||||||
113
src/types/api/h5PopupConfiguration.ts
Normal file
113
src/types/api/h5PopupConfiguration.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* H5 运营弹窗配置相关类型
|
||||||
|
* 契约来源:docs/产品迭代8月份/通知.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 命中页面集合 (home:首页, asset_detail:资产详情, package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值)
|
||||||
|
export type H5PopupPage = 'home' | 'asset_detail' | 'package_purchase' | 'asset_wallet_recharge'
|
||||||
|
|
||||||
|
// 投放频率 (once:每客户每配置版本仅一次, daily:每客户每配置版本每个上海自然日一次)
|
||||||
|
export type H5PopupFrequency = 'once' | 'daily'
|
||||||
|
|
||||||
|
// 受控动作 (package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值);空字符串表示无受控动作
|
||||||
|
export type H5PopupActionType = 'package_purchase' | 'asset_wallet_recharge' | ''
|
||||||
|
|
||||||
|
// 卡类型范围 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||||
|
export type H5PopupCardType = 'CMCC' | 'CUCC' | 'CTCC' | 'CBN'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5 运营弹窗配置(列表项 / 详情 / 创建与更新响应)
|
||||||
|
*/
|
||||||
|
export interface H5PopupConfiguration {
|
||||||
|
id: number // 配置ID
|
||||||
|
title: string // 弹窗标题
|
||||||
|
content: string // 弹窗正文
|
||||||
|
pages: H5PopupPage[] | null // 命中页面集合;空数组等价于非法(页面必选)
|
||||||
|
priority: number // 优先级,数值越大越优先
|
||||||
|
frequency: H5PopupFrequency // 投放频率
|
||||||
|
frequency_text: string // 投放频率名称(中文)
|
||||||
|
enabled: boolean // 是否启用;停用后停止新投放,历史通知在展示期内仍可见
|
||||||
|
enabled_text: string // 启停状态名称(中文)
|
||||||
|
action_type: H5PopupActionType // 受控动作;空值表示无受控动作
|
||||||
|
shop_ids: number[] | null // 店铺范围;空数组表示全量
|
||||||
|
device_types: string[] | null // 设备类型范围;空数组表示全量
|
||||||
|
card_types: H5PopupCardType[] | null // 卡类型范围;空数组表示全量
|
||||||
|
starts_at: string // 生效开始时间(ISO 8601)
|
||||||
|
ends_at: string // 生效结束时间(ISO 8601)
|
||||||
|
version: number // 配置版本,每次更新递增;版本参与频率去重,旧版本通知保留原快照
|
||||||
|
creator: number // 创建人账号ID
|
||||||
|
updater: number // 最近更新人账号ID
|
||||||
|
created_at: string // 创建时间(ISO 8601)
|
||||||
|
updated_at: string // 最近更新时间(ISO 8601);启停同样刷新该时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表查询参数
|
||||||
|
* GET /api/admin/h5-popup-configurations
|
||||||
|
*/
|
||||||
|
export interface H5PopupConfigurationQueryParams {
|
||||||
|
page?: number // 页码,默认 1(1~10000)
|
||||||
|
page_size?: number // 每页数量,默认 20,最大 100
|
||||||
|
enabled?: boolean | null // 是否启用筛选
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表分页响应
|
||||||
|
*/
|
||||||
|
export interface H5PopupConfigurationListResponse {
|
||||||
|
items: H5PopupConfiguration[] | null
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建请求
|
||||||
|
* POST /api/admin/h5-popup-configurations
|
||||||
|
* title/content/pages/frequency/starts_at/ends_at 必填;
|
||||||
|
* shop_ids/device_types/card_types 空数组 = 全量。
|
||||||
|
*/
|
||||||
|
export interface CreateH5PopupConfigurationRequest {
|
||||||
|
title: string // 弹窗标题,1~100 字符
|
||||||
|
content: string // 弹窗正文,1~2000 字符,不接受 HTML、URL 或前端路由
|
||||||
|
pages: H5PopupPage[] // 命中页面集合(非空)
|
||||||
|
priority?: number // 优先级,数值越大越优先,0~1000000
|
||||||
|
frequency: H5PopupFrequency // 投放频率
|
||||||
|
starts_at: string // 生效开始时间(ISO 8601)
|
||||||
|
ends_at: string // 生效结束时间(ISO 8601),不得早于开始时间
|
||||||
|
enabled?: boolean // 是否启用,默认由后端决定
|
||||||
|
action_type?: H5PopupActionType // 受控动作
|
||||||
|
shop_ids?: number[] // 店铺范围,空数组 = 全量
|
||||||
|
device_types?: string[] // 设备类型范围,空数组 = 全量
|
||||||
|
card_types?: H5PopupCardType[] // 卡类型范围,空数组 = 全量
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新请求
|
||||||
|
* PUT /api/admin/h5-popup-configurations/{id}
|
||||||
|
* 除 id 外全部可选,不传保持原值;action_type 空字符串 = 清除受控动作;
|
||||||
|
* shop_ids/device_types/card_types 空数组 = 改为全量;pages 空数组非法。
|
||||||
|
*/
|
||||||
|
export interface UpdateH5PopupConfigurationRequest {
|
||||||
|
id: number // 运营弹窗配置ID
|
||||||
|
title?: string // 不传保持原值,1~100 字符
|
||||||
|
content?: string // 不传保持原值,1~2000 字符
|
||||||
|
pages?: H5PopupPage[] // 不传保持原值;传空数组等价于非法(页面必选)
|
||||||
|
priority?: number // 不传保持原值,0~1000000
|
||||||
|
frequency?: H5PopupFrequency // 不传保持原值
|
||||||
|
starts_at?: string // 不传保持原值(ISO 8601)
|
||||||
|
ends_at?: string // 不传保持原值(ISO 8601),不得早于开始时间
|
||||||
|
enabled?: boolean // 不传保持原值;启停会刷新最近更新时间并影响同优先级排序
|
||||||
|
action_type?: H5PopupActionType | null // 不传保持原值;传空字符串 = 清除受控动作
|
||||||
|
shop_ids?: number[] | null // 不传保持原值;传空数组 = 改为全量
|
||||||
|
device_types?: string[] | null // 不传保持原值;传空数组 = 改为全量
|
||||||
|
card_types?: H5PopupCardType[] | null // 不传保持原值;传空数组 = 改为全量
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/停用请求体
|
||||||
|
* POST /api/admin/h5-popup-configurations/{id}/enable、/{id}/disable
|
||||||
|
*/
|
||||||
|
export interface H5PopupConfigurationIDParams {
|
||||||
|
id: number // 运营弹窗配置ID
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/**
|
/**
|
||||||
* API 类型统一导出
|
* API 类型统一导出
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ export * from './account'
|
|||||||
|
|
||||||
// 店铺相关
|
// 店铺相关
|
||||||
export * from './shop'
|
export * from './shop'
|
||||||
|
export * from './businessUserGroup'
|
||||||
|
|
||||||
// 网卡相关
|
// 网卡相关
|
||||||
export * from './card'
|
export * from './card'
|
||||||
@@ -137,3 +138,12 @@ export * from './wecom'
|
|||||||
|
|
||||||
// 员工代收款相关
|
// 员工代收款相关
|
||||||
export * from './employeeCollection'
|
export * from './employeeCollection'
|
||||||
|
|
||||||
|
// 手机号资产关联相关
|
||||||
|
export * from './phoneAsset'
|
||||||
|
|
||||||
|
// H5 运营弹窗配置相关
|
||||||
|
export * from './h5PopupConfiguration'
|
||||||
|
|
||||||
|
// 套餐真流量预警相关
|
||||||
|
export * from './packageTrafficAlert'
|
||||||
|
|||||||
137
src/types/api/packageTrafficAlert.ts
Normal file
137
src/types/api/packageTrafficAlert.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* 套餐真流量预警相关类型
|
||||||
|
* 对应 docs/产品迭代8月份/套餐真流量预警_API_前端简版.md
|
||||||
|
* 6 个接口仅超级管理员/平台账号可访问,其他账号后端返回 403
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BaseResponse, PaginationParams, PaginationResponse } from './common'
|
||||||
|
|
||||||
|
// ========== 预警规则 ==========
|
||||||
|
|
||||||
|
/** 预警规则列表查询参数 */
|
||||||
|
export interface PackageTrafficAlertRuleQueryParams extends PaginationParams {
|
||||||
|
package_id?: number // 按套餐商品ID过滤
|
||||||
|
enabled?: boolean // 按启用状态过滤;不传时查询全部
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预警规则项 */
|
||||||
|
export interface PackageTrafficAlertRuleItem {
|
||||||
|
id: number // 预警规则ID
|
||||||
|
package_id: number // 套餐商品ID
|
||||||
|
package_name: string // 套餐名称
|
||||||
|
real_data_mb: number // 套餐商品当前真流量额度(MB),仅用于配置校验展示,不作为预警分母
|
||||||
|
threshold_percent: number // 真流量预警阈值百分比(1~100,允许两位小数)
|
||||||
|
enabled: boolean // 是否启用
|
||||||
|
enabled_name: string // 启用状态名称(中文)
|
||||||
|
remark: string // 备注
|
||||||
|
updated_at: string // 最近更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建预警规则参数 */
|
||||||
|
export interface CreatePackageTrafficAlertRuleParams {
|
||||||
|
package_id: number // 套餐商品ID;必须存在且真流量额度大于零
|
||||||
|
threshold_percent: number // 真流量预警阈值百分比,取值 1 至 100,允许两位小数
|
||||||
|
enabled?: boolean // 是否启用(默认 true);停用后扫描不再创建新预警
|
||||||
|
remark?: string // 备注,最多 500 字符
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改预警规则参数(允许修改阈值、启停与备注;修改不影响既有预警快照) */
|
||||||
|
export interface UpdatePackageTrafficAlertRuleParams {
|
||||||
|
threshold_percent?: number // 新的真流量预警阈值百分比,取值 1 至 100,允许两位小数
|
||||||
|
enabled?: boolean // 是否启用;停用后扫描不再创建新预警,既有预警保留
|
||||||
|
remark?: string // 备注,最多 500 字符
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 预警记录 ==========
|
||||||
|
|
||||||
|
/** 资产类型(iot_card:物联网卡, device:设备) */
|
||||||
|
export type PackageTrafficAlertAssetType = 'iot_card' | 'device'
|
||||||
|
|
||||||
|
/** 通知投递结果 */
|
||||||
|
export enum PackageTrafficAlertNotificationStatus {
|
||||||
|
/** 已通知 */
|
||||||
|
NOTIFIED = 1,
|
||||||
|
/** 待投递 */
|
||||||
|
PENDING_DELIVERY = 2,
|
||||||
|
/** 投递失败 */
|
||||||
|
DELIVERY_FAILED = 3,
|
||||||
|
/** 未通知(接收人已失效) */
|
||||||
|
NOT_NOTIFIED_RECEIVER_INVALID = 4,
|
||||||
|
/** 未通知(无有效业务员) */
|
||||||
|
NOT_NOTIFIED_NO_VALID_OWNER = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预警记录列表查询参数(与导出筛选一致) */
|
||||||
|
export interface PackageTrafficAlertRecordQueryParams extends PaginationParams {
|
||||||
|
package_id?: number // 按阈值来源套餐商品ID过滤
|
||||||
|
shop_id?: number // 按触发时所属店铺ID过滤
|
||||||
|
business_owner_account_id?: number // 按触发时店铺业务员账号ID过滤
|
||||||
|
asset_type?: PackageTrafficAlertAssetType // 资产类型 (iot_card:物联网卡, device:设备)
|
||||||
|
asset_identifier?: string // 资产或卡标识关键词,匹配资产标识、卡标识与对应标识符快照
|
||||||
|
threshold_percent?: number // 按触发阈值快照精确过滤,允许两位小数
|
||||||
|
start_time?: string // 触发时间起始(RFC3339,含该时刻)
|
||||||
|
end_time?: string // 触发时间截止(RFC3339,含该时刻)
|
||||||
|
notification_status?: PackageTrafficAlertNotificationStatus // 通知投递结果过滤 (1-5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预警记录项
|
||||||
|
* 除 business_user_group_names(当前值)外,其余字段均为触发时快照
|
||||||
|
*/
|
||||||
|
export interface PackageTrafficAlertRecordItem {
|
||||||
|
id: number // 预警记录ID
|
||||||
|
package_id?: number // 阈值来源套餐商品ID
|
||||||
|
package_name?: string | null // 套餐名称快照
|
||||||
|
shop_id?: number | null // 触发时所属店铺ID
|
||||||
|
shop_name?: string | null // 触发时所属店铺名称
|
||||||
|
business_owner_account_id?: number | null // 触发时店铺业务员账号ID
|
||||||
|
business_owner_username?: string | null // 触发时店铺业务员账号名
|
||||||
|
business_user_group_names?: string[] // 当前业务用户组名称(非触发时快照,可为空数组)
|
||||||
|
asset_type?: PackageTrafficAlertAssetType | null // 资产类型快照
|
||||||
|
asset_identifier?: string | null // 资产或卡标识快照
|
||||||
|
threshold_percent?: number // 触发阈值快照
|
||||||
|
triggered_at?: string // 触发时间
|
||||||
|
notification_status?: PackageTrafficAlertNotificationStatus // 通知投递结果
|
||||||
|
notification_status_name?: string // 通知投递结果名称
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预警记录详情(在列表项基础上标识触发后归属是否变化) */
|
||||||
|
export interface PackageTrafficAlertRecordDetail extends PackageTrafficAlertRecordItem {
|
||||||
|
shop_changed_since_trigger?: boolean // 触发后店铺归属是否变化
|
||||||
|
owner_changed_since_trigger?: boolean // 触发后业务员归属是否变化
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 导出 ==========
|
||||||
|
|
||||||
|
/** 创建预警记录导出任务参数(复用既有异步导出任务) */
|
||||||
|
export interface ExportPackageTrafficAlertParams {
|
||||||
|
format: 'xlsx' | 'csv' // 导出格式 (xlsx:Excel, csv:CSV)
|
||||||
|
package_id?: number
|
||||||
|
shop_id?: number
|
||||||
|
business_owner_account_id?: number
|
||||||
|
asset_type?: PackageTrafficAlertAssetType
|
||||||
|
asset_identifier?: string // 最多 100 字符
|
||||||
|
threshold_percent?: number
|
||||||
|
start_time?: string
|
||||||
|
end_time?: string
|
||||||
|
notification_status?: PackageTrafficAlertNotificationStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建预警记录导出任务响应 */
|
||||||
|
export interface CreatePackageTrafficAlertExportResponse {
|
||||||
|
task_id: number // 导出任务ID
|
||||||
|
task_no: string // 任务编号
|
||||||
|
status: number // 任务状态 (1:待处理, 2:处理中, 3:已完成, 4:已失败, 5:已取消)
|
||||||
|
status_name: string // 任务状态名称(中文)
|
||||||
|
message: string // 提示信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 响应类型 ==========
|
||||||
|
|
||||||
|
export type PackageTrafficAlertRuleListResponse = PaginationResponse<PackageTrafficAlertRuleItem>
|
||||||
|
export type PackageTrafficAlertRuleResponse = BaseResponse<PackageTrafficAlertRuleItem>
|
||||||
|
export type PackageTrafficAlertRecordListResponse =
|
||||||
|
PaginationResponse<PackageTrafficAlertRecordItem>
|
||||||
|
export type PackageTrafficAlertRecordDetailResponse = BaseResponse<PackageTrafficAlertRecordDetail>
|
||||||
|
export type CreatePackageTrafficAlertExportApiResponse =
|
||||||
|
BaseResponse<CreatePackageTrafficAlertExportResponse>
|
||||||
@@ -241,7 +241,7 @@ export const PAYMENT_CREDENTIAL_FIELD_SPECS: Record<
|
|||||||
},
|
},
|
||||||
wechat_v2: {
|
wechat_v2: {
|
||||||
required: ['wx_mch_id', 'wx_api_v2_key', 'wx_notify_url'],
|
required: ['wx_mch_id', 'wx_api_v2_key', 'wx_notify_url'],
|
||||||
optional: []
|
optional: ['wx_client_cert_content', 'wx_client_key_content']
|
||||||
},
|
},
|
||||||
fuiou: {
|
fuiou: {
|
||||||
required: [
|
required: [
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface WechatPayConfigFields {
|
|||||||
wx_serial_no: string
|
wx_serial_no: string
|
||||||
wx_cert_content: string
|
wx_cert_content: string
|
||||||
wx_key_content: string
|
wx_key_content: string
|
||||||
|
wx_client_cert_content: string
|
||||||
|
wx_client_key_content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AlipayConfigFields {
|
interface AlipayConfigFields {
|
||||||
|
|||||||
192
src/types/api/phoneAsset.ts
Normal file
192
src/types/api/phoneAsset.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* 手机号资产关联相关类型
|
||||||
|
* 契约来源:docs/产品迭代8月份/手机号资产关联.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 资产类型
|
||||||
|
export type PhoneAssetType = 'iot_card' | 'device'
|
||||||
|
|
||||||
|
// 关联状态 (0:已失效, 1:有效)
|
||||||
|
export type PhoneAssetAssociationStatus = 0 | 1
|
||||||
|
|
||||||
|
// 失效方式
|
||||||
|
export type PhoneAssetInvalidationMethod = 'backend_single' | 'backend_batch' | 'csv_import'
|
||||||
|
|
||||||
|
// 建立来源(固定为 H5 短信验证)
|
||||||
|
export type PhoneAssetSource = 'h5_sms_verification'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号资产关联列表项
|
||||||
|
* GET /api/admin/phone-asset-associations
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetAssociation {
|
||||||
|
id: number // 关联关系ID
|
||||||
|
asset_id: number // 资产ID
|
||||||
|
asset_identifier: string // 资产当前标识(卡为 ICCID,设备为虚拟号)
|
||||||
|
asset_type: PhoneAssetType // 资产类型
|
||||||
|
phone: string // 关联手机号(完整值)
|
||||||
|
established_at: string // 建立时间(短信验证通过时间)
|
||||||
|
source: PhoneAssetSource // 建立来源,固定为 h5_sms_verification
|
||||||
|
status: PhoneAssetAssociationStatus // 关联状态 (0:已失效, 1:有效)
|
||||||
|
status_name: string // 关联状态中文名称
|
||||||
|
invalidated_at: string | null // 失效时间;有效关系为空
|
||||||
|
invalidation_method: PhoneAssetInvalidationMethod | null // 失效方式;有效关系为空
|
||||||
|
invalidation_method_name: string // 失效方式中文名称;有效关系为空
|
||||||
|
invalidation_reason: string // 失效原因;有效关系为空
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联列表查询参数
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetAssociationQueryParams {
|
||||||
|
page?: number // 页码,默认 1
|
||||||
|
page_size?: number // 每页数量,默认 20,最大 100
|
||||||
|
asset_identifier?: string // 资产标识(ICCID、虚拟号、IMEI、SN 或接入号,精确匹配)
|
||||||
|
phone?: string // 手机号(完整值精确匹配)
|
||||||
|
status?: PhoneAssetAssociationStatus | null // 关联状态 (0:已失效, 1:有效)
|
||||||
|
created_at_start?: string // 关联创建时间起(含)
|
||||||
|
created_at_end?: string // 关联创建时间止(含)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联列表分页响应
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetAssociationListResponse {
|
||||||
|
items: PhoneAssetAssociation[] | null
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单条解绑响应
|
||||||
|
* DELETE /api/admin/phone-asset-associations/{id}
|
||||||
|
*/
|
||||||
|
export interface UnbindPhoneAssetAssociationResponse {
|
||||||
|
asset_id: number // 资产ID
|
||||||
|
asset_type: PhoneAssetType // 资产类型
|
||||||
|
id: number // 关联关系ID
|
||||||
|
invalidated_at: string // 失效时间
|
||||||
|
unbound_count: number // 本次解除的有效关系数,成功时为 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量解绑资产项
|
||||||
|
*/
|
||||||
|
export interface BatchUnbindAssetItem {
|
||||||
|
asset_id: number // 资产ID
|
||||||
|
asset_type: PhoneAssetType // 资产类型 (iot_card:物联网卡, device:设备)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量解绑请求
|
||||||
|
* POST /api/admin/phone-asset-associations/batch-unbind
|
||||||
|
*/
|
||||||
|
export interface BatchUnbindPhoneAssetAssociationRequest {
|
||||||
|
assets: BatchUnbindAssetItem[] // 待解除的资产集合,按 (资产类型, 资产ID) 去重后执行
|
||||||
|
confirmed: boolean // 二次确认,必须为 true
|
||||||
|
reason: string // 解除原因(1~500 字符)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量解绑逐项结果
|
||||||
|
*/
|
||||||
|
export interface BatchUnbindAssetResult {
|
||||||
|
asset_id: number // 资产ID
|
||||||
|
asset_type: PhoneAssetType // 资产类型
|
||||||
|
success: boolean // 该项是否解除成功
|
||||||
|
reason: string // 该项失败原因,成功时为空
|
||||||
|
unbound_count: number // 该项解除的有效关系数
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量解绑响应
|
||||||
|
*/
|
||||||
|
export interface BatchUnbindPhoneAssetAssociationResponse {
|
||||||
|
success_count: number // 解除成功的资产项数
|
||||||
|
fail_count: number // 解除失败的资产项数
|
||||||
|
items: BatchUnbindAssetResult[] | null // 逐项结果,顺序与请求去重后的资产集合一致
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑导入任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)
|
||||||
|
*/
|
||||||
|
export type PhoneAssetUnbindImportTaskStatus = 1 | 2 | 3 | 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建解绑导入任务请求
|
||||||
|
* POST /api/admin/phone-asset-associations/unbind-imports
|
||||||
|
*/
|
||||||
|
export interface CreatePhoneAssetUnbindImportRequest {
|
||||||
|
file_key: string // CSV 对象存储 Key(phone-unbind-imports/ 开头且扩展名为 .csv)
|
||||||
|
reason: string // 任务级解绑原因(1~500 字符)
|
||||||
|
confirmed: boolean // 二次确认,必须为 true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑导入任务(列表项 / 创建响应)
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetUnbindImportTask {
|
||||||
|
id: number // 导入任务ID
|
||||||
|
task_no: string // 导入任务编号
|
||||||
|
file_name: string // 上传的源 CSV 文件名
|
||||||
|
status: PhoneAssetUnbindImportTaskStatus // 任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)
|
||||||
|
status_name: string // 任务状态中文名称
|
||||||
|
total_count: number // 数据行总数;任务级失败时为 0
|
||||||
|
success_count: number // 成功行数
|
||||||
|
fail_count: number // 失败行数
|
||||||
|
unbind_reason: string // 任务级解绑原因
|
||||||
|
error_message: string // 任务级失败原因;与行级失败原因分开记录
|
||||||
|
creator_name: string // 任务创建人名称快照
|
||||||
|
created_at: string // 创建时间
|
||||||
|
started_at: string // 开始处理时间
|
||||||
|
completed_at: string // 完成时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑导入任务列表分页响应
|
||||||
|
* GET /api/admin/phone-asset-associations/unbind-imports
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetUnbindImportTaskListResponse {
|
||||||
|
items: PhoneAssetUnbindImportTask[] | null
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑导入任务列表查询参数
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetUnbindImportTaskQueryParams {
|
||||||
|
page?: number // 页码,默认 1
|
||||||
|
page_size?: number // 每页数量,默认 20,最大 100
|
||||||
|
status?: PhoneAssetUnbindImportTaskStatus | null // 任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逐行状态 (3:成功, 4:失败)
|
||||||
|
*/
|
||||||
|
export type PhoneAssetUnbindImportItemStatus = 3 | 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑导入任务逐行结果
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetUnbindImportItem {
|
||||||
|
line: number // 行号,自数据首行起计(表头不计入)
|
||||||
|
asset_identifier: string // 导入文件中的资产标识原文
|
||||||
|
asset_id: number // 资产ID;未定位到资产时为 0
|
||||||
|
asset_type: PhoneAssetType | '' // 资产类型;未定位到资产时为空
|
||||||
|
status: PhoneAssetUnbindImportItemStatus // 行状态 (3:成功, 4:失败)
|
||||||
|
status_name: string // 行状态中文名称
|
||||||
|
reason: string // 失败原因;成功行为空
|
||||||
|
associated_phones: string[] | null // 解绑当时的完整关联手机号快照;失败行为空数组
|
||||||
|
unbound_count: number // 该行解除的有效关系数
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑导入任务详情
|
||||||
|
* GET /api/admin/phone-asset-associations/unbind-imports/{id}
|
||||||
|
*/
|
||||||
|
export interface PhoneAssetUnbindImportTaskDetail extends PhoneAssetUnbindImportTask {
|
||||||
|
items: PhoneAssetUnbindImportItem[] | null // 逐行结果明细;任务级失败时为空数组
|
||||||
|
}
|
||||||
@@ -7,7 +7,36 @@ export enum RefundStatus {
|
|||||||
PENDING = 1, // 待审批
|
PENDING = 1, // 待审批
|
||||||
APPROVED = 2, // 已通过
|
APPROVED = 2, // 已通过
|
||||||
REJECTED = 3, // 已拒绝
|
REJECTED = 3, // 已拒绝
|
||||||
RETURNED = 4 // 已退回
|
RETURNED = 4, // 已退回
|
||||||
|
REFUNDING = 5, // 原路退款处理中
|
||||||
|
REFUND_FAILED = 6 // 原路退款失败
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退款方式
|
||||||
|
export enum RefundMethod {
|
||||||
|
ORIGINAL_ROUTE = 'original_route', // 原路退回
|
||||||
|
CUSTOMER_ACCOUNT = 'customer_account', // 客户收款信息
|
||||||
|
ASSET_WALLET = 'asset_wallet', // 资产钱包
|
||||||
|
AGENT_WALLET = 'agent_wallet' // 代理钱包
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渠道退款状态
|
||||||
|
export enum ChannelRefundStatus {
|
||||||
|
NOT_STARTED = 0, // 未发起或不适用
|
||||||
|
PROCESSING = 1, // 处理中
|
||||||
|
SUCCEEDED = 2, // 已成功
|
||||||
|
FAILED = 3 // 已失败
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失败原因分类
|
||||||
|
export enum RefundFailureReason {
|
||||||
|
CHANNEL_REJECTED = 'channel_rejected', // 渠道明确拒绝
|
||||||
|
CREDENTIAL_INVALID = 'credential_invalid', // 渠道凭证失效
|
||||||
|
INSUFFICIENT_BALANCE = 'insufficient_balance', // 渠道余额不足
|
||||||
|
TIMEOUT_UNKNOWN = 'timeout_unknown', // 超时或结果未知
|
||||||
|
APPROVAL_REJECTED = 'approval_rejected', // 企微驳回或关闭
|
||||||
|
REVOKED_AFTER_APPROVED = 'revoked_after_approved', // 企微通过后撤销
|
||||||
|
PAYMENT_FACT_INVALID = 'payment_fact_invalid' // 本地原支付事实不可用
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundAttachment {
|
export interface RefundAttachment {
|
||||||
@@ -42,6 +71,25 @@ export interface RefundApproval {
|
|||||||
business_process_result?: unknown
|
business_process_result?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 退款申请审批尝试历史项
|
||||||
|
export interface RefundAttemptItem {
|
||||||
|
id: number // 尝试记录ID
|
||||||
|
attempt_no: number // 第几次尝试
|
||||||
|
method?: RefundMethod | null // 退款方式
|
||||||
|
method_name?: string | null // 退款方式名称
|
||||||
|
refund_amount?: number | null // 退款金额(分)
|
||||||
|
frozen_actual_received_amount?: number | null // 冻结实收金额(分)
|
||||||
|
refund_reason?: string | null // 退款原因
|
||||||
|
customer_account_info?: Record<string, string> | null // 客户收款信息
|
||||||
|
customer_voucher_key?: string[] | string | null // 客户凭证键
|
||||||
|
channel_refund_request_no?: string | null // 渠道退款申请号
|
||||||
|
submitted_by_account_id?: number | null // 提交人账号ID
|
||||||
|
approval_instance_id?: number | null // 审批实例ID
|
||||||
|
approval_status?: number | null // 审批状态
|
||||||
|
approval_status_name?: string | null // 审批状态名称
|
||||||
|
created_at: string // 创建时间
|
||||||
|
}
|
||||||
|
|
||||||
// 退款申请
|
// 退款申请
|
||||||
export interface Refund {
|
export interface Refund {
|
||||||
id: number
|
id: number
|
||||||
@@ -55,9 +103,29 @@ export interface Refund {
|
|||||||
asset_type: string // 资产类型
|
asset_type: string // 资产类型
|
||||||
requested_refund_amount?: number | null // 申请退款金额(分)
|
requested_refund_amount?: number | null // 申请退款金额(分)
|
||||||
approved_refund_amount?: number | null // 实际退款金额(分)
|
approved_refund_amount?: number | null // 实际退款金额(分)
|
||||||
actual_received_amount?: number | null // 实收金额(分)
|
actual_received_amount?: number | null // 实收金额(分,历史数据保留)
|
||||||
status: RefundStatus
|
status: RefundStatus
|
||||||
status_name?: string | null
|
status_name?: string | null
|
||||||
|
method?: RefundMethod | null // 退款方式
|
||||||
|
method_name?: string | null // 退款方式名称
|
||||||
|
frozen_actual_received_amount?: number | null // 冻结实收金额(分)
|
||||||
|
customer_account_info?: Record<string, string> | null // 客户收款信息
|
||||||
|
channel_refund_status?: ChannelRefundStatus | null // 渠道退款状态
|
||||||
|
channel_refund_status_name?: string | null // 渠道退款状态名称
|
||||||
|
channel_refund_no?: string | null // 渠道退款单号
|
||||||
|
channel_refund_request_no?: string | null // 渠道退款申请号
|
||||||
|
channel_refund_amount?: number | null // 渠道退款金额(分)
|
||||||
|
channel_refunded_at?: string | null // 渠道退款时间
|
||||||
|
failure_reason?: RefundFailureReason | null // 失败分类
|
||||||
|
failure_reason_name?: string | null // 失败分类名称
|
||||||
|
failure_message?: string | null // 失败消息
|
||||||
|
anomaly_flag?: number // 异常标记(0 正常、1 异常)
|
||||||
|
anomaly_reason?: string | null // 异常原因
|
||||||
|
latest_attempt_id?: number | null // 最新尝试记录ID
|
||||||
|
latest_approval_instance_id?: number | null // 最新审批实例ID
|
||||||
|
attempts?: RefundAttemptItem[] // 审批尝试历史
|
||||||
|
refund_package_used_mb?: number // 当前退款套餐已用量(MB)
|
||||||
|
refund_package_total_mb?: number // 当前退款套餐总量(MB)
|
||||||
refund_reason: string
|
refund_reason: string
|
||||||
refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串
|
refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||||
attachments?: RefundAttachment[]
|
attachments?: RefundAttachment[]
|
||||||
@@ -110,17 +178,36 @@ export interface RefundListResponse {
|
|||||||
// 创建退款申请请求
|
// 创建退款申请请求
|
||||||
export interface CreateRefundRequest {
|
export interface CreateRefundRequest {
|
||||||
order_id: number
|
order_id: number
|
||||||
|
method: RefundMethod // 退款方式(必填)
|
||||||
requested_refund_amount: number // 申请退款金额(分)
|
requested_refund_amount: number // 申请退款金额(分)
|
||||||
actual_received_amount: number // 实收金额(分)
|
|
||||||
package_usage_id: number // 关联套餐使用记录 ID,无关联记录时传 0
|
package_usage_id: number // 关联套餐使用记录 ID,无关联记录时传 0
|
||||||
refund_reason: string
|
refund_reason: string
|
||||||
refund_voucher_key: string[]
|
customer_account_info?: Record<string, string> | null // 客户收款信息(customer_account 方式必填)
|
||||||
|
refund_voucher_key?: string[] | null // 退款凭证(customer_account 方式必填至少 1 个)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新提交退款申请请求
|
// 重新提交退款申请请求
|
||||||
export interface ResubmitRefundRequest {
|
export interface ResubmitRefundRequest {
|
||||||
|
method?: RefundMethod | null // 退款方式(不传沿用原值)
|
||||||
requested_refund_amount?: number
|
requested_refund_amount?: number
|
||||||
actual_received_amount?: number
|
|
||||||
attachments?: RefundAttachment[]
|
|
||||||
refund_reason?: string
|
refund_reason?: string
|
||||||
|
customer_account_info?: Record<string, string> | null // 客户收款信息(不传沿用原值)
|
||||||
|
refund_voucher_key?: string[] | null
|
||||||
|
attachments?: RefundAttachment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批通过退款申请请求
|
||||||
|
export interface ApproveRefundRequest {
|
||||||
|
approved_refund_amount?: number | null // 审批退款金额(分)
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批拒绝退款申请请求
|
||||||
|
export interface RejectRefundRequest {
|
||||||
|
reject_reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退回退款申请请求
|
||||||
|
export interface ReturnRefundRequest {
|
||||||
|
remark?: string
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,13 @@ export interface ShopResponse {
|
|||||||
business_owner_username: string // 平台业务员账号名
|
business_owner_username: string // 平台业务员账号名
|
||||||
business_owner_phone_summary: string // 平台业务员手机号摘要
|
business_owner_phone_summary: string // 平台业务员手机号摘要
|
||||||
business_owner_available: boolean // 是否仍可用于通知接收
|
business_owner_available: boolean // 是否仍可用于通知接收
|
||||||
|
distribution_code?: string // 分销码(用于生成注册二维码)
|
||||||
client_login_disabled: boolean // 是否禁止 C 端新登录
|
client_login_disabled: boolean // 是否禁止 C 端新登录
|
||||||
|
business_user_group_id: number | null // 业务用户组ID(实时推导,可空)
|
||||||
|
business_user_group_code: string // 业务用户组编码
|
||||||
|
business_user_group_name: string // 业务用户组名称
|
||||||
|
business_user_group_enabled: boolean // 业务用户组是否启用;停用仍返回组且 enabled=false
|
||||||
|
business_user_group_business_line: string // 业务用户组业务线
|
||||||
}
|
}
|
||||||
|
|
||||||
// 店铺列表查询参数
|
// 店铺列表查询参数
|
||||||
@@ -41,6 +47,9 @@ export interface ShopQueryParams extends PaginationParams {
|
|||||||
contact_phone?: string // 联系电话精确查询
|
contact_phone?: string // 联系电话精确查询
|
||||||
business_owner_account_id?: number | null // 平台业务员账号ID
|
business_owner_account_id?: number | null // 平台业务员账号ID
|
||||||
client_login_disabled?: boolean
|
client_login_disabled?: boolean
|
||||||
|
business_user_group_id?: number | null // 按业务用户组ID筛选
|
||||||
|
business_line?: string | null // 按业务线筛选(standard/smart/other)
|
||||||
|
ungrouped?: boolean // true=无负责人或负责人无分组
|
||||||
page?: number // 页码
|
page?: number // 页码
|
||||||
page_size?: number // 每页数量
|
page_size?: number // 每页数量
|
||||||
}
|
}
|
||||||
@@ -139,3 +148,69 @@ export interface UpdateShopCreditLimitResponse {
|
|||||||
version: number // 更新后的钱包版本
|
version: number // 更新后的钱包版本
|
||||||
wallet_id: number // 主钱包 ID
|
wallet_id: number // 主钱包 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 批量交接平台业务员 ==========
|
||||||
|
|
||||||
|
/** 批量交接平台业务员请求 */
|
||||||
|
export interface BatchUpdateBusinessOwnerParams {
|
||||||
|
shop_ids: number[] // 店铺ID列表,1-500,自动去重
|
||||||
|
business_owner_account_id: number | null // 显式必传:null=清空负责人,ID=换绑
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量交接平台业务员响应 */
|
||||||
|
export interface BatchUpdateBusinessOwnerResult {
|
||||||
|
batch_key: string // 批次标识
|
||||||
|
shop_count: number // 处理店铺数
|
||||||
|
cleared: number // 清空负责人的店铺数
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 业务负责人 CSV 导入 ==========
|
||||||
|
|
||||||
|
/** 业务负责人导入任务行级结果 */
|
||||||
|
export interface BusinessOwnerImportRow {
|
||||||
|
line: number // 行号,从数据首行 1 开始
|
||||||
|
shop_code: string // 店铺编码
|
||||||
|
operation_type: string // 操作类型:换绑 / 清空
|
||||||
|
status: number // 3=成功 4=失败
|
||||||
|
status_name?: string // 状态中文名(成功/失败)
|
||||||
|
reason: string // 失败原因
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务负责人导入任务详情 */
|
||||||
|
export interface BusinessOwnerImportDetail {
|
||||||
|
id: number // 导入任务ID
|
||||||
|
task_no?: string // 任务编号
|
||||||
|
file_name?: string // 文件名
|
||||||
|
creator_name?: string // 创建人
|
||||||
|
status: number // 1=待处理 2=处理中 3=已完成 4=失败
|
||||||
|
status_name?: string // 状态中文名(待处理/处理中/已完成/失败)
|
||||||
|
started_at?: string // 开始时间
|
||||||
|
completed_at?: string // 完成时间
|
||||||
|
total_count?: number
|
||||||
|
success_count?: number
|
||||||
|
fail_count?: number
|
||||||
|
error_message?: string | null // 任务级失败原因
|
||||||
|
items?: BusinessOwnerImportRow[] | null // 行级结果
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建业务负责人导入任务请求 */
|
||||||
|
export interface CreateBusinessOwnerImportParams {
|
||||||
|
file_key: string // 由 /storage/upload-url(purpose=shop_import)获取
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务负责人导入任务列表查询参数 */
|
||||||
|
export interface BusinessOwnerImportQueryParams {
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
status?: number // 按状态筛选
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务负责人导入任务列表响应 */
|
||||||
|
export interface BusinessOwnerImportPageResult {
|
||||||
|
items: BusinessOwnerImportDetail[] | null
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
total?: number
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export type WecomBusinessType =
|
|||||||
| 'refund_approval'
|
| 'refund_approval'
|
||||||
| 'offline_recharge_approval'
|
| 'offline_recharge_approval'
|
||||||
| 'employee_collection_approval'
|
| 'employee_collection_approval'
|
||||||
|
| 'agent_distribution_approval'
|
||||||
|
| 'withdrawal_qualification_approval'
|
||||||
|
| 'commission_withdrawal_approval'
|
||||||
|
|
||||||
export interface WecomApplication {
|
export interface WecomApplication {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
3
src/types/components.d.ts
vendored
3
src/types/components.d.ts
vendored
@@ -86,6 +86,7 @@ declare module 'vue' {
|
|||||||
CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default']
|
CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default']
|
||||||
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
||||||
CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default']
|
CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default']
|
||||||
|
CommissionRecordDetailDialog: typeof import('./../components/business/CommissionRecordDetailDialog.vue')['default']
|
||||||
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
||||||
CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default']
|
CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default']
|
||||||
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
|
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
|
||||||
@@ -126,6 +127,7 @@ declare module 'vue' {
|
|||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElResult: typeof import('element-plus/es')['ElResult']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
@@ -152,6 +154,7 @@ declare module 'vue' {
|
|||||||
PackageSelector: typeof import('./../components/business/PackageSelector.vue')['default']
|
PackageSelector: typeof import('./../components/business/PackageSelector.vue')['default']
|
||||||
PaymentVoucherDialog: typeof import('./../components/business/PaymentVoucherDialog.vue')['default']
|
PaymentVoucherDialog: typeof import('./../components/business/PaymentVoucherDialog.vue')['default']
|
||||||
RealnamePolicyDialog: typeof import('./../components/device/RealnamePolicyDialog.vue')['default']
|
RealnamePolicyDialog: typeof import('./../components/device/RealnamePolicyDialog.vue')['default']
|
||||||
|
RefundApprovalDialog: typeof import('./../components/business/RefundApprovalDialog.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default']
|
SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default']
|
||||||
|
|||||||
@@ -616,7 +616,8 @@
|
|||||||
{
|
{
|
||||||
prop: 'shop_name',
|
prop: 'shop_name',
|
||||||
label: '店铺名称',
|
label: '店铺名称',
|
||||||
minWidth: 150,
|
minWidth: 160,
|
||||||
|
showOverflowTooltip: true,
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
return row.shop_name || '-'
|
return row.shop_name || '-'
|
||||||
}
|
}
|
||||||
|
|||||||
425
src/views/agent-registration/index.vue
Normal file
425
src/views/agent-registration/index.vue
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
<template>
|
||||||
|
<div class="agent-registration-page">
|
||||||
|
<div class="register-card">
|
||||||
|
<div class="register-header">
|
||||||
|
<h1 class="register-title">代理扫码注册</h1>
|
||||||
|
<p class="register-subtitle">填写资料提交后,审核结果将通过通知告知</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="submitted" class="register-result">
|
||||||
|
<ElResult icon="success" title="提交成功" sub-title="待审批,审核结果将通知你">
|
||||||
|
<template #extra>
|
||||||
|
<ElButton type="primary" @click="handleFillAgain">重新填写</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElResult>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElForm
|
||||||
|
v-else
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
size="large"
|
||||||
|
class="register-form"
|
||||||
|
>
|
||||||
|
<div class="form-grid">
|
||||||
|
<ElFormItem label="分销码" prop="distribution_code">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.distribution_code"
|
||||||
|
:disabled="lockedDistributionCode"
|
||||||
|
placeholder="请输入分销码"
|
||||||
|
maxlength="64"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="手机号" prop="phone">
|
||||||
|
<ElInput v-model="form.phone" placeholder="请输入手机号" maxlength="11" clearable />
|
||||||
|
</ElFormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem label="短信验证码" prop="code">
|
||||||
|
<div class="code-row">
|
||||||
|
<ElInput v-model="form.code" placeholder="请输入短信验证码" maxlength="6" />
|
||||||
|
<ElButton :disabled="!canSendCode" :loading="sending" @click="handleSendCode">
|
||||||
|
{{ countdownText }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="密码" prop="password">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入密码(8-32 位)"
|
||||||
|
maxlength="32"
|
||||||
|
/>
|
||||||
|
<div class="strength-bar" :class="strengthClass">
|
||||||
|
<i v-for="n in 3" :key="n" :class="{ active: passwordStrength >= n }" />
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<ElFormItem label="店铺名称" prop="shop_name">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.shop_name"
|
||||||
|
:disabled="lockedShopName"
|
||||||
|
placeholder="请输入店铺名称"
|
||||||
|
maxlength="50"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="店铺编号" prop="shop_code">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.shop_code"
|
||||||
|
:disabled="lockedShopCode"
|
||||||
|
placeholder="请输入店铺编号"
|
||||||
|
maxlength="50"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem label="用户名" prop="username">
|
||||||
|
<ElInput v-model="form.username" placeholder="请输入登录用户名" maxlength="50" clearable />
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<ElFormItem label="联系人" prop="contact_name">
|
||||||
|
<ElInput v-model="form.contact_name" placeholder="选填" maxlength="50" clearable />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="省市区">
|
||||||
|
<ElCascader
|
||||||
|
v-model="form.region"
|
||||||
|
:options="regionOptions"
|
||||||
|
placeholder="请选择省市区"
|
||||||
|
clearable
|
||||||
|
class="region-cascader"
|
||||||
|
@change="handleRegionChange"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem label="详细地址" prop="address">
|
||||||
|
<ElInput v-model="form.address" placeholder="选填,请输入详细地址" maxlength="200" clearable />
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="submit-btn"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
提交注册
|
||||||
|
</ElButton>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { CascaderOption, CascaderValue, FormInstance, FormRules } from 'element-plus'
|
||||||
|
import {
|
||||||
|
AgentDistributionService,
|
||||||
|
type AgentDistributionRegistrationParams
|
||||||
|
} from '@/api/modules/agentDistribution'
|
||||||
|
import { regionData } from '@/utils/constants/regionData'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const regionOptions = regionData as CascaderOption[]
|
||||||
|
|
||||||
|
interface RegisterForm {
|
||||||
|
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
|
||||||
|
region: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<RegisterForm>({
|
||||||
|
distribution_code: (route.query.distribution_code as string) || '',
|
||||||
|
phone: '',
|
||||||
|
code: '',
|
||||||
|
password: '',
|
||||||
|
shop_name: (route.query.shop_name as string) || '',
|
||||||
|
shop_code: (route.query.shop_code as string) || '',
|
||||||
|
username: '',
|
||||||
|
contact_name: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
district: '',
|
||||||
|
address: '',
|
||||||
|
region: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const submitted = ref(false)
|
||||||
|
const sending = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const countdown = ref(0)
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
distribution_code: [{ required: true, message: '请输入分销码', trigger: 'blur' }],
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
code: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 8, max: 32, message: '密码长度为 8-32 位', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
shop_name: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }],
|
||||||
|
shop_code: [{ required: true, message: '请输入店铺编号', trigger: 'blur' }],
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockedDistributionCode = computed(() => !!route.query.distribution_code)
|
||||||
|
const lockedShopName = computed(() => !!route.query.shop_name)
|
||||||
|
const lockedShopCode = computed(() => !!route.query.shop_code)
|
||||||
|
|
||||||
|
const canSendCode = computed(() => countdown.value <= 0 && !sending.value)
|
||||||
|
|
||||||
|
const countdownText = computed(() =>
|
||||||
|
countdown.value > 0 ? `${countdown.value}s 后重发` : '获取验证码'
|
||||||
|
)
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value -= 1
|
||||||
|
if (countdown.value <= 0 && timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
|
||||||
|
ElMessage.warning('请先输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sending.value = true
|
||||||
|
try {
|
||||||
|
const res = await AgentDistributionService.sendCode({
|
||||||
|
phone: form.phone,
|
||||||
|
scene: 'bind_phone'
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('验证码已发送')
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 错误提示由请求层统一处理
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordStrength = computed(() => {
|
||||||
|
const pwd = form.password
|
||||||
|
if (!pwd) return 0
|
||||||
|
let score = 0
|
||||||
|
if (pwd.length >= 8) score += 1
|
||||||
|
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score += 1
|
||||||
|
if (/\d/.test(pwd)) score += 1
|
||||||
|
if (/[^A-Za-z0-9]/.test(pwd)) score += 1
|
||||||
|
if (score >= 4) return 3
|
||||||
|
if (score >= 2) return 2
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const strengthClass = computed(() => {
|
||||||
|
const map = ['', 'is-weak', 'is-medium', 'is-strong']
|
||||||
|
return map[passwordStrength.value] || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRegionChange = (value: CascaderValue | null | undefined) => {
|
||||||
|
const parts = Array.isArray(value) ? value.map((item) => String(item)) : []
|
||||||
|
form.province = parts[0] || ''
|
||||||
|
form.city = parts[1] || ''
|
||||||
|
form.district = parts[2] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
formRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const params: AgentDistributionRegistrationParams = {
|
||||||
|
distribution_code: form.distribution_code.trim(),
|
||||||
|
phone: form.phone.trim(),
|
||||||
|
code: form.code.trim(),
|
||||||
|
password: form.password,
|
||||||
|
shop_name: form.shop_name.trim(),
|
||||||
|
shop_code: form.shop_code.trim(),
|
||||||
|
username: form.username.trim()
|
||||||
|
}
|
||||||
|
if (form.contact_name.trim()) params.contact_name = form.contact_name.trim()
|
||||||
|
if (form.province) params.province = form.province
|
||||||
|
if (form.city) params.city = form.city
|
||||||
|
if (form.district) params.district = form.district
|
||||||
|
if (form.address.trim()) params.address = form.address.trim()
|
||||||
|
|
||||||
|
const res = await AgentDistributionService.registerAgent(params)
|
||||||
|
if (res.code === 0) {
|
||||||
|
submitted.value = true
|
||||||
|
} else {
|
||||||
|
ElMessage.error('分销码不可用')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('分销码不可用')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFillAgain = () => {
|
||||||
|
submitted.value = false
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
form.distribution_code = (route.query.distribution_code as string) || form.distribution_code
|
||||||
|
form.shop_name = (route.query.shop_name as string) || form.shop_name
|
||||||
|
form.shop_code = (route.query.shop_code as string) || form.shop_code
|
||||||
|
form.code = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.agent-registration-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 48px 16px 64px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(160deg, #eef3fb 0%, #f7f9fd 40%, #eef3fb 100%);
|
||||||
|
|
||||||
|
.register-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
padding: 36px 36px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 30px rgb(93 135 255 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.register-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-subtitle {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--el-border-color-lighter);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-strong i.active {
|
||||||
|
background: var(--el-color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-weak i.active {
|
||||||
|
background: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-cascader {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-result {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 24px 12px 40px;
|
||||||
|
|
||||||
|
.register-card {
|
||||||
|
padding: 24px 16px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-row .el-button {
|
||||||
|
width: 112px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -46,6 +46,12 @@
|
|||||||
direction="vertical"
|
direction="vertical"
|
||||||
border
|
border
|
||||||
size="large"
|
size="large"
|
||||||
|
>
|
||||||
|
<!-- 关联手机号(通用) -->
|
||||||
|
<ElDescriptionsItem
|
||||||
|
label="关联手机号"
|
||||||
|
label-class-name="asset-label asset-label--associated-phones"
|
||||||
|
>{{ (cardInfo?.associated_phones || []).join('、') || '-' }}</ElDescriptionsItem
|
||||||
>
|
>
|
||||||
<!-- 卡专属字段 -->
|
<!-- 卡专属字段 -->
|
||||||
<template v-if="cardInfo?.asset_type === 'card'">
|
<template v-if="cardInfo?.asset_type === 'card'">
|
||||||
@@ -698,6 +704,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AssetInfo {
|
interface AssetInfo {
|
||||||
|
associated_phones?: string[]
|
||||||
asset_id: number
|
asset_id: number
|
||||||
asset_type: 'card' | 'device'
|
asset_type: 'card' | 'device'
|
||||||
identifier: string
|
identifier: string
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export function useAssetInfo() {
|
|||||||
// 卡专属字段 (asset_type === 'card')
|
// 卡专属字段 (asset_type === 'card')
|
||||||
iccid: data.iccid || '',
|
iccid: data.iccid || '',
|
||||||
msisdn: data.msisdn || '',
|
msisdn: data.msisdn || '',
|
||||||
|
associated_phones: data.associated_phones ?? [],
|
||||||
imsi: data.imsi || '',
|
imsi: data.imsi || '',
|
||||||
carrier_id: data.carrier_id,
|
carrier_id: data.carrier_id,
|
||||||
carrier_type: data.carrier_type || '',
|
carrier_type: data.carrier_type || '',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface AssetInfo {
|
|||||||
iccid?: string
|
iccid?: string
|
||||||
imei?: string
|
imei?: string
|
||||||
msisdn?: string
|
msisdn?: string
|
||||||
|
associated_phones?: string[] // 当前关联手机号(完整值)
|
||||||
carrier_type?: string
|
carrier_type?: string
|
||||||
carrier_name?: string
|
carrier_name?: string
|
||||||
real_name_status?: number
|
real_name_status?: number
|
||||||
|
|||||||
@@ -1617,6 +1617,7 @@
|
|||||||
{ label: '设备号', prop: 'virtual_no' },
|
{ label: '设备号', prop: 'virtual_no' },
|
||||||
{ label: 'IMEI', prop: 'imei' },
|
{ label: 'IMEI', prop: 'imei' },
|
||||||
{ label: '设备名称', prop: 'device_name' },
|
{ label: '设备名称', prop: 'device_name' },
|
||||||
|
{ label: '关联手机号', prop: 'associated_phones' },
|
||||||
{ label: '店铺名称', prop: 'shop_name' },
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
{ label: '套餐系列', prop: 'series_name' },
|
{ label: '套餐系列', prop: 'series_name' },
|
||||||
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
|
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
|
||||||
@@ -1893,10 +1894,19 @@
|
|||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
minWidth: 150
|
minWidth: 150
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'associated_phones',
|
||||||
|
label: '关联手机号',
|
||||||
|
minWidth: 160,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: Device) =>
|
||||||
|
row.associated_phones?.length ? row.associated_phones.join('、') : '-'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'shop_name',
|
prop: 'shop_name',
|
||||||
label: '店铺名称',
|
label: '店铺名称',
|
||||||
minWidth: 120
|
minWidth: 160,
|
||||||
|
showOverflowTooltip: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'series_name',
|
prop: 'series_name',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import DetailPage from '@/components/common/DetailPage.vue'
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
import type { DetailField, DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ExchangeDetail' })
|
defineOptions({ name: 'ExchangeDetail' })
|
||||||
|
|
||||||
@@ -62,6 +62,16 @@
|
|||||||
return types[status] || 'info'
|
return types[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMigrationStatusType = (migrationStatus?: string): TagProps['type'] => {
|
||||||
|
const types: Record<string, TagProps['type']> = {
|
||||||
|
not_migrated: 'info',
|
||||||
|
pending: 'warning',
|
||||||
|
migrated: 'success',
|
||||||
|
failed: 'danger'
|
||||||
|
}
|
||||||
|
return types[migrationStatus || ''] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
// 根据流程类型动态生成详情页配置
|
// 根据流程类型动态生成详情页配置
|
||||||
const detailSections = computed<DetailSection[]>(() => {
|
const detailSections = computed<DetailSection[]>(() => {
|
||||||
if (!exchangeDetail.value) return []
|
if (!exchangeDetail.value) return []
|
||||||
@@ -70,9 +80,7 @@
|
|||||||
const sections: DetailSection[] = []
|
const sections: DetailSection[] = []
|
||||||
|
|
||||||
// 基本信息
|
// 基本信息
|
||||||
sections.push({
|
const basicFields: DetailField[] = [
|
||||||
title: '基本信息',
|
|
||||||
fields: [
|
|
||||||
{ label: '换货单号', prop: 'exchange_no' },
|
{ label: '换货单号', prop: 'exchange_no' },
|
||||||
{
|
{
|
||||||
label: '状态',
|
label: '状态',
|
||||||
@@ -112,9 +120,28 @@
|
|||||||
label: '继承归属店铺',
|
label: '继承归属店铺',
|
||||||
formatter: (_, data) =>
|
formatter: (_, data) =>
|
||||||
formatExchangeShopName(data.inherited_shop_name, data.inherited_shop_id)
|
formatExchangeShopName(data.inherited_shop_name, data.inherited_shop_id)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '迁移状态',
|
||||||
|
render: (data) =>
|
||||||
|
data.migration_status
|
||||||
|
? h(
|
||||||
|
ElTag,
|
||||||
|
{ type: getMigrationStatusType(data.migration_status) },
|
||||||
|
() => data.migration_status_name || data.migration_status
|
||||||
|
)
|
||||||
|
: h('span', '--')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
if (exchangeDetail.value.migration_status === 'failed') {
|
||||||
|
basicFields.push({
|
||||||
|
label: '迁移失败原因',
|
||||||
|
prop: 'migration_failure_reason',
|
||||||
|
formatter: (value) => value || '--',
|
||||||
|
fullWidth: true
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
sections.push({ title: '基本信息', fields: basicFields })
|
||||||
|
|
||||||
// 收货信息(仅物流换货显示)
|
// 收货信息(仅物流换货显示)
|
||||||
if (flowType === 'shipping') {
|
if (flowType === 'shipping') {
|
||||||
|
|||||||
@@ -987,6 +987,19 @@
|
|||||||
formatter: (row: ExchangeResponse) =>
|
formatter: (row: ExchangeResponse) =>
|
||||||
h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '--')
|
h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '--')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'migration_status',
|
||||||
|
label: '迁移状态',
|
||||||
|
width: 130,
|
||||||
|
formatter: (row: ExchangeResponse) =>
|
||||||
|
row.migration_status
|
||||||
|
? h(
|
||||||
|
ElTag,
|
||||||
|
{ type: getMigrationStatusType(row.migration_status) },
|
||||||
|
() => row.migration_status_name || '--'
|
||||||
|
)
|
||||||
|
: '--'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'created_at',
|
prop: 'created_at',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
@@ -1006,6 +1019,18 @@
|
|||||||
return types[status] || 'info'
|
return types[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMigrationStatusType = (
|
||||||
|
migrationStatus?: string
|
||||||
|
): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
|
||||||
|
const types: Record<string, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
|
||||||
|
not_migrated: 'info',
|
||||||
|
pending: 'warning',
|
||||||
|
migrated: 'success',
|
||||||
|
failed: 'danger'
|
||||||
|
}
|
||||||
|
return types[migrationStatus || ''] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
const getAssetTypeName = (assetType?: string | null) => {
|
const getAssetTypeName = (assetType?: string | null) => {
|
||||||
if (assetType === 'iot_card') return '物联网卡'
|
if (assetType === 'iot_card') return '物联网卡'
|
||||||
if (assetType === 'device') return '设备'
|
if (assetType === 'device') return '设备'
|
||||||
@@ -1638,7 +1663,9 @@
|
|||||||
|
|
||||||
// 状态3(已发货待确认):确认完成
|
// 状态3(已发货待确认):确认完成
|
||||||
if (status === 3) {
|
if (status === 3) {
|
||||||
if (hasAuth('exchange:complete')) {
|
// migration_status=failed 的重试仅超管/平台账号可操作,代理/企业不渲染入口(后端以 403/1005 兜底)
|
||||||
|
const canRetryFailed = row.migration_status !== 'failed' || userType === 1 || userType === 2
|
||||||
|
if (hasAuth('exchange:complete') && canRetryFailed) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: '确认完成',
|
label: '确认完成',
|
||||||
handler: () => handleCompleteExchange(row),
|
handler: () => handleCompleteExchange(row),
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<ExportTaskList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExportTaskList from '../export-task-list/index.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ExportCommissionRecordTaskList' })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<ExportTaskList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExportTaskList from '../export-task-list/index.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ExportPackageTrafficAlertTaskList' })
|
||||||
|
</script>
|
||||||
@@ -1946,6 +1946,7 @@
|
|||||||
{ label: 'ICCID', prop: 'iccid' },
|
{ label: 'ICCID', prop: 'iccid' },
|
||||||
{ label: 'IMEI', prop: 'gateway_card_imei' },
|
{ label: 'IMEI', prop: 'gateway_card_imei' },
|
||||||
{ label: 'MSISDN', prop: 'msisdn' },
|
{ label: 'MSISDN', prop: 'msisdn' },
|
||||||
|
{ label: '关联手机号', prop: 'associated_phones' },
|
||||||
{ label: '卡虚拟号', prop: 'virtual_no' },
|
{ label: '卡虚拟号', prop: 'virtual_no' },
|
||||||
{ label: '运营商', prop: 'carrier_name' },
|
{ label: '运营商', prop: 'carrier_name' },
|
||||||
{ label: '卡业务类型', prop: 'card_category' },
|
{ label: '卡业务类型', prop: 'card_category' },
|
||||||
@@ -2092,6 +2093,14 @@
|
|||||||
label: 'MSISDN',
|
label: 'MSISDN',
|
||||||
width: 160
|
width: 160
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'associated_phones',
|
||||||
|
label: '关联手机号',
|
||||||
|
minWidth: 150,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: StandaloneIotCard) =>
|
||||||
|
row.associated_phones?.length ? row.associated_phones.join('、') : '-'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'virtual_no',
|
prop: 'virtual_no',
|
||||||
label: '卡虚拟号',
|
label: '卡虚拟号',
|
||||||
|
|||||||
750
src/views/asset-management/phone-asset-association/index.vue
Normal file
750
src/views/asset-management/phone-asset-association/index.vue
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
<template>
|
||||||
|
<ArtTableFullScreen>
|
||||||
|
<div class="phone-asset-association-page" id="table-full-screen">
|
||||||
|
<ArtSearchBar
|
||||||
|
v-model:filter="searchForm"
|
||||||
|
:items="searchFormItems"
|
||||||
|
label-width="110"
|
||||||
|
:show-expand="false"
|
||||||
|
@reset="handleReset"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElCard shadow="never" class="art-table-card">
|
||||||
|
<ArtTableHeader
|
||||||
|
:columnList="columnOptions"
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
@refresh="handleRefresh"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<ElButton
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:disabled="selectedRows.length === 0"
|
||||||
|
@click="openBatchUnbindDialog"
|
||||||
|
v-if="canBatchUnbind"
|
||||||
|
>
|
||||||
|
批量解绑
|
||||||
|
</ElButton>
|
||||||
|
<ElButton type="primary" plain @click="openImportDialog" v-if="canImportCreate">
|
||||||
|
CSV解绑导入
|
||||||
|
</ElButton>
|
||||||
|
<ElButton @click="goUnbindImportTasks" v-if="canImportPage">解绑导入任务</ElButton>
|
||||||
|
</template>
|
||||||
|
</ArtTableHeader>
|
||||||
|
|
||||||
|
<ArtTable
|
||||||
|
:loading="loading"
|
||||||
|
:data="associationList"
|
||||||
|
:currentPage="pagination.page"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:marginTop="10"
|
||||||
|
:actions="getActions"
|
||||||
|
:actionsWidth="100"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn type="selection" width="55" />
|
||||||
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 单条解绑 -->
|
||||||
|
<ElDialog
|
||||||
|
v-model="unbindDialogVisible"
|
||||||
|
title="解除手机号资产关联"
|
||||||
|
width="480px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<ElForm ref="unbindFormRef" :model="unbindForm" :rules="unbindRules" label-width="90px">
|
||||||
|
<ElFormItem label="资产标识">
|
||||||
|
<span>{{ unbindForm.asset_identifier || '-' }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="手机号">
|
||||||
|
<span>{{ unbindForm.phone || '-' }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="解除原因" prop="reason">
|
||||||
|
<ElInput
|
||||||
|
v-model="unbindForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请填写解除原因(1~500 字符)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="二次确认" prop="confirmed">
|
||||||
|
<ElCheckbox v-model="unbindForm.confirmed">我已确认解除该关联关系</ElCheckbox>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="unbindDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="danger" :loading="unbindLoading" @click="confirmUnbind">确认解绑</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 批量解绑 -->
|
||||||
|
<ElDialog
|
||||||
|
v-model="batchUnbindDialogVisible"
|
||||||
|
title="批量解除手机号资产关联"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<ElAlert
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
:title="`已选择 ${selectedRows.length} 项资产(按资产类型与ID去重后执行,上限 200)`"
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
/>
|
||||||
|
<ElForm ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="90px">
|
||||||
|
<ElFormItem label="解除原因" prop="reason">
|
||||||
|
<ElInput
|
||||||
|
v-model="batchForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请填写解除原因(1~500 字符)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="二次确认" prop="confirmed">
|
||||||
|
<ElCheckbox v-model="batchForm.confirmed">我已确认解除上述全部关联关系</ElCheckbox>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="batchUnbindDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="danger" :loading="batchUnbindLoading" @click="confirmBatchUnbind">
|
||||||
|
确认批量解绑
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 批量解绑结果 -->
|
||||||
|
<ElDialog v-model="batchResultVisible" title="批量解绑结果" width="560px">
|
||||||
|
<div class="batch-result-summary">
|
||||||
|
<span>成功 {{ batchResult?.success_count ?? 0 }} 项</span>
|
||||||
|
<span class="text-danger">失败 {{ batchResult?.fail_count ?? 0 }} 项</span>
|
||||||
|
</div>
|
||||||
|
<ElTable :data="batchResult?.items || []" max-height="320" size="small" border>
|
||||||
|
<ElTableColumn prop="asset_type" label="资产类型" width="110" />
|
||||||
|
<ElTableColumn prop="asset_id" label="资产ID" width="90" />
|
||||||
|
<ElTableColumn prop="success" label="结果" width="80" />
|
||||||
|
<ElTableColumn prop="unbound_count" label="解除关系数" width="100" />
|
||||||
|
<ElTableColumn prop="reason" label="失败原因" show-overflow-tooltip />
|
||||||
|
</ElTable>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton type="primary" @click="batchResultVisible = false">关闭</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- CSV 解绑导入 -->
|
||||||
|
<ElDialog
|
||||||
|
v-model="importDialogVisible"
|
||||||
|
title="CSV 解绑导入"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="handleImportDialogClosed"
|
||||||
|
>
|
||||||
|
<div class="import-tips">
|
||||||
|
<span>模板表头:</span>
|
||||||
|
<ElButton link type="primary" @click="downloadTemplate">下载模板</ElButton>
|
||||||
|
<div class="import-tips-detail">
|
||||||
|
资产标识,备注(备注可选);支持 UTF-8(可带 BOM),非 UTF-8 按 GBK 解码;无行数上限。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElForm ref="importFormRef" :model="importForm" :rules="importRules" label-width="90px">
|
||||||
|
<ElFormItem label="CSV 文件" prop="file">
|
||||||
|
<ElUpload
|
||||||
|
ref="uploadRef"
|
||||||
|
drag
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
accept=".csv"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-exceed="handleFileExceed"
|
||||||
|
>
|
||||||
|
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||||
|
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择 .csv 文件</em></div>
|
||||||
|
</ElUpload>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="解除原因" prop="reason">
|
||||||
|
<ElInput
|
||||||
|
v-model="importForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请填写任务级解除原因(1~500 字符)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="二次确认" prop="confirmed">
|
||||||
|
<ElCheckbox v-model="importForm.confirmed">我已确认按模板批量解除关联关系</ElCheckbox>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" :loading="importLoading" @click="confirmImport"
|
||||||
|
>创建导入任务</ElButton
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</ArtTableFullScreen>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
ElMessage,
|
||||||
|
ElTag,
|
||||||
|
FormInstance,
|
||||||
|
FormRules,
|
||||||
|
UploadFile,
|
||||||
|
UploadInstance
|
||||||
|
} from 'element-plus'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
import { PhoneAssetAssociationService, StorageService } from '@/api/modules'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||||
|
import type { SearchFormItem } from '@/types'
|
||||||
|
import type {
|
||||||
|
BatchUnbindAssetItem,
|
||||||
|
BatchUnbindPhoneAssetAssociationResponse,
|
||||||
|
PhoneAssetAssociation,
|
||||||
|
PhoneAssetAssociationStatus
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
|
defineOptions({ name: 'PhoneAssetAssociation' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const perms = AUGUST_PERMISSIONS.phoneAssetAssociation
|
||||||
|
|
||||||
|
const canBatchUnbind = computed(() => hasAuth(perms.unbindBatch))
|
||||||
|
const canImportCreate = computed(() => hasAuth(perms.unbindImportCreate))
|
||||||
|
const canImportPage = computed(() => hasAuth(perms.unbindImportPage))
|
||||||
|
const canUnbind = computed(() => hasAuth(perms.unbind))
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const associationList = ref<PhoneAssetAssociation[]>([])
|
||||||
|
const selectedRows = ref<PhoneAssetAssociation[]>([])
|
||||||
|
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||||
|
|
||||||
|
const initialSearchState = {
|
||||||
|
asset_identifier: '',
|
||||||
|
phone: '',
|
||||||
|
status: undefined as PhoneAssetAssociationStatus | undefined,
|
||||||
|
dateRange: [] as string[],
|
||||||
|
created_at_start: '',
|
||||||
|
created_at_end: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchForm = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '有效', value: 1 as const },
|
||||||
|
{ label: '已失效', value: 0 as const }
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchFormItems: SearchFormItem[] = [
|
||||||
|
{
|
||||||
|
label: '资产标识',
|
||||||
|
prop: 'asset_identifier',
|
||||||
|
type: 'input',
|
||||||
|
config: { clearable: true, placeholder: 'ICCID / 虚拟号 / IMEI / SN / 接入号(精确匹配)' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '手机号',
|
||||||
|
prop: 'phone',
|
||||||
|
type: 'input',
|
||||||
|
config: { clearable: true, placeholder: '完整手机号(精确匹配)' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关联状态',
|
||||||
|
prop: 'status',
|
||||||
|
type: 'select',
|
||||||
|
config: { clearable: true, placeholder: '全部' },
|
||||||
|
options: () => statusOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
prop: 'dateRange',
|
||||||
|
type: 'datetimerange',
|
||||||
|
config: {
|
||||||
|
type: 'datetimerange',
|
||||||
|
rangeSeparator: '至',
|
||||||
|
startPlaceholder: '开始时间',
|
||||||
|
endPlaceholder: '结束时间',
|
||||||
|
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const columnOptions = [
|
||||||
|
{ label: '资产类型', prop: 'asset_type' },
|
||||||
|
{ label: '资产标识', prop: 'asset_identifier' },
|
||||||
|
{ label: '手机号', prop: 'phone' },
|
||||||
|
{ label: '建立时间', prop: 'established_at' },
|
||||||
|
{ label: '来源', prop: 'source' },
|
||||||
|
{ label: '状态', prop: 'status' },
|
||||||
|
{ label: '失效时间', prop: 'invalidated_at' },
|
||||||
|
{ label: '失效方式', prop: 'invalidation_method_name' },
|
||||||
|
{ label: '失效原因', prop: 'invalidation_reason' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
|
{
|
||||||
|
prop: 'asset_type',
|
||||||
|
label: '资产类型',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: PhoneAssetAssociation) =>
|
||||||
|
row.asset_type === 'iot_card' ? '物联网卡' : '设备'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'asset_identifier',
|
||||||
|
label: '资产标识',
|
||||||
|
minWidth: 200,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: PhoneAssetAssociation) =>
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||||
|
onClick: () => goToAsset(row)
|
||||||
|
},
|
||||||
|
row.asset_identifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ prop: 'phone', label: '手机号', width: 140 },
|
||||||
|
{
|
||||||
|
prop: 'established_at',
|
||||||
|
label: '建立时间',
|
||||||
|
minWidth: 170,
|
||||||
|
formatter: (row: PhoneAssetAssociation) => formatDateTime(row.established_at) || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'source',
|
||||||
|
label: '来源',
|
||||||
|
width: 120,
|
||||||
|
formatter: () => 'H5 短信验证'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '状态',
|
||||||
|
width: 90,
|
||||||
|
formatter: (row: PhoneAssetAssociation) =>
|
||||||
|
h(
|
||||||
|
ElTag,
|
||||||
|
{ type: row.status === 1 ? 'success' : 'info' },
|
||||||
|
() => row.status_name || (row.status === 1 ? '有效' : '已失效')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'invalidated_at',
|
||||||
|
label: '失效时间',
|
||||||
|
minWidth: 170,
|
||||||
|
formatter: (row: PhoneAssetAssociation) => formatDateTime(row.invalidated_at) || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'invalidation_method_name',
|
||||||
|
label: '失效方式',
|
||||||
|
width: 130,
|
||||||
|
formatter: (row: PhoneAssetAssociation) => row.invalidation_method_name || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'invalidation_reason',
|
||||||
|
label: '失效原因',
|
||||||
|
minWidth: 160,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: PhoneAssetAssociation) => row.invalidation_reason || '-'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// ========== 列表数据 ==========
|
||||||
|
|
||||||
|
const syncDateRange = () => {
|
||||||
|
if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
|
||||||
|
searchForm.created_at_start = searchForm.dateRange[0] || ''
|
||||||
|
searchForm.created_at_end = searchForm.dateRange[1] || ''
|
||||||
|
} else {
|
||||||
|
searchForm.created_at_start = ''
|
||||||
|
searchForm.created_at_end = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toRfc3339 = (date: string, endOfDay = false) => {
|
||||||
|
if (!date) return undefined
|
||||||
|
if (date.includes('T')) return date
|
||||||
|
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}+08:00`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTableData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
syncDateRange()
|
||||||
|
const res = await PhoneAssetAssociationService.getAssociations({
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
asset_identifier: searchForm.asset_identifier || undefined,
|
||||||
|
phone: searchForm.phone || undefined,
|
||||||
|
status: searchForm.status,
|
||||||
|
created_at_start: toRfc3339(searchForm.created_at_start),
|
||||||
|
created_at_end: toRfc3339(searchForm.created_at_end, true)
|
||||||
|
})
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
associationList.value = res.data.items || []
|
||||||
|
pagination.total = res.data.total || 0
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '获取手机号资产关联列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取手机号资产关联列表失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message || '获取手机号资产关联列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
Object.assign(searchForm, { ...initialSearchState })
|
||||||
|
pagination.page = 1
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = (pageSize: number) => {
|
||||||
|
pagination.pageSize = pageSize
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (page: number) => {
|
||||||
|
pagination.page = page
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectionChange = (rows: PhoneAssetAssociation[]) => {
|
||||||
|
selectedRows.value = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToAsset = (row: PhoneAssetAssociation) => {
|
||||||
|
router.push({
|
||||||
|
path: RoutesAlias.AssetInformation,
|
||||||
|
query:
|
||||||
|
row.asset_type === 'iot_card'
|
||||||
|
? { iccid: row.asset_identifier }
|
||||||
|
: { virtual_no: row.asset_identifier }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActions = (row: PhoneAssetAssociation) => {
|
||||||
|
const actions: any[] = []
|
||||||
|
if (row.status === 1 && canUnbind.value) {
|
||||||
|
actions.push({ label: '解绑', handler: () => openUnbindDialog(row), type: 'danger' })
|
||||||
|
}
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 单条解绑 ==========
|
||||||
|
|
||||||
|
const unbindDialogVisible = ref(false)
|
||||||
|
const unbindLoading = ref(false)
|
||||||
|
const unbindFormRef = ref<FormInstance>()
|
||||||
|
const unbindForm = reactive<{
|
||||||
|
id: number
|
||||||
|
asset_identifier: string
|
||||||
|
phone: string
|
||||||
|
reason: string
|
||||||
|
confirmed: boolean
|
||||||
|
}>({
|
||||||
|
id: 0,
|
||||||
|
asset_identifier: '',
|
||||||
|
phone: '',
|
||||||
|
reason: '',
|
||||||
|
confirmed: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const unbindRules: FormRules = {
|
||||||
|
reason: [
|
||||||
|
{ required: true, message: '请填写解除原因', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmed: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value: boolean, callback) => {
|
||||||
|
if (!value) callback(new Error('请先勾选二次确认'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openUnbindDialog = (row: PhoneAssetAssociation) => {
|
||||||
|
unbindForm.id = row.id
|
||||||
|
unbindForm.asset_identifier = row.asset_identifier
|
||||||
|
unbindForm.phone = row.phone
|
||||||
|
unbindForm.reason = ''
|
||||||
|
unbindForm.confirmed = false
|
||||||
|
unbindDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUnbind = async () => {
|
||||||
|
if (!(await unbindFormRef.value?.validate().catch(() => false))) return
|
||||||
|
unbindLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await PhoneAssetAssociationService.unbindAssociation(
|
||||||
|
unbindForm.id,
|
||||||
|
unbindForm.reason.trim(),
|
||||||
|
unbindForm.confirmed
|
||||||
|
)
|
||||||
|
if (res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '解除关联失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success('解除关联成功')
|
||||||
|
unbindDialogVisible.value = false
|
||||||
|
getTableData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解除关联失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message || '解除关联失败')
|
||||||
|
} finally {
|
||||||
|
unbindLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 批量解绑 ==========
|
||||||
|
|
||||||
|
const batchUnbindDialogVisible = ref(false)
|
||||||
|
const batchUnbindLoading = ref(false)
|
||||||
|
const batchFormRef = ref<FormInstance>()
|
||||||
|
const batchForm = reactive({ reason: '', confirmed: false, assets: [] as BatchUnbindAssetItem[] })
|
||||||
|
const batchResult = ref<BatchUnbindPhoneAssetAssociationResponse | null>(null)
|
||||||
|
const batchResultVisible = ref(false)
|
||||||
|
|
||||||
|
const batchRules: FormRules = {
|
||||||
|
reason: [
|
||||||
|
{ required: true, message: '请填写解除原因', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmed: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value: boolean, callback) => {
|
||||||
|
if (!value) callback(new Error('请先勾选二次确认'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildUniqueAssets = (rows: PhoneAssetAssociation[]): BatchUnbindAssetItem[] => {
|
||||||
|
const map = new Map<string, BatchUnbindAssetItem>()
|
||||||
|
rows.forEach((row) => {
|
||||||
|
if (row.status !== 1) return
|
||||||
|
const key = `${row.asset_type}:${row.asset_id}`
|
||||||
|
if (!map.has(key)) map.set(key, { asset_type: row.asset_type, asset_id: row.asset_id })
|
||||||
|
})
|
||||||
|
return Array.from(map.values()).slice(0, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openBatchUnbindDialog = () => {
|
||||||
|
batchForm.assets = buildUniqueAssets(selectedRows.value)
|
||||||
|
if (batchForm.assets.length === 0) {
|
||||||
|
ElMessage.warning('请选择有效关联的资产')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
batchForm.reason = ''
|
||||||
|
batchForm.confirmed = false
|
||||||
|
batchResult.value = null
|
||||||
|
batchUnbindDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmBatchUnbind = async () => {
|
||||||
|
if (!(await batchFormRef.value?.validate().catch(() => false))) return
|
||||||
|
batchUnbindLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await PhoneAssetAssociationService.batchUnbind({
|
||||||
|
assets: batchForm.assets,
|
||||||
|
reason: batchForm.reason.trim(),
|
||||||
|
confirmed: batchForm.confirmed
|
||||||
|
})
|
||||||
|
if (res.code !== 0 || !res.data) {
|
||||||
|
ElMessage.error(res.msg || '批量解绑失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
batchResult.value = res.data
|
||||||
|
batchUnbindDialogVisible.value = false
|
||||||
|
batchResultVisible.value = true
|
||||||
|
getTableData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量解绑失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message || '批量解绑失败')
|
||||||
|
} finally {
|
||||||
|
batchUnbindLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== CSV 解绑导入 ==========
|
||||||
|
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const importLoading = ref(false)
|
||||||
|
const uploadRef = ref<UploadInstance>()
|
||||||
|
const importFormRef = ref<FormInstance>()
|
||||||
|
const importForm = reactive({
|
||||||
|
file: null as File | null,
|
||||||
|
reason: '',
|
||||||
|
confirmed: false
|
||||||
|
})
|
||||||
|
const importRules: FormRules = {
|
||||||
|
file: [
|
||||||
|
{
|
||||||
|
validator: (_rule, _value, callback) => {
|
||||||
|
if (!importForm.file) callback(new Error('请选择 CSV 文件'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
reason: [
|
||||||
|
{ required: true, message: '请填写任务级解除原因', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmed: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value: boolean, callback) => {
|
||||||
|
if (!value) callback(new Error('请先勾选二次确认'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openImportDialog = () => {
|
||||||
|
importForm.file = null
|
||||||
|
importForm.reason = ''
|
||||||
|
importForm.confirmed = false
|
||||||
|
importDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportDialogClosed = () => {
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (file: UploadFile) => {
|
||||||
|
const rawFile = file.raw
|
||||||
|
if (!rawFile) return
|
||||||
|
if (!rawFile.name.toLowerCase().endsWith('.csv')) {
|
||||||
|
ElMessage.error('解绑导入仅支持 .csv 文件')
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
importForm.file = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
importForm.file = rawFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileExceed = () => {
|
||||||
|
ElMessage.warning('仅支持上传一个 CSV 文件')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadTemplate = () => {
|
||||||
|
const content = '\uFEFF资产标识,备注\n'
|
||||||
|
const blob = new Blob([content], { type: 'text/csv;charset=utf-8' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = '手机号资产解绑导入模板.csv'
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goUnbindImportTasks = () => {
|
||||||
|
router.push(RoutesAlias.PhoneAssetUnbindImportTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmImport = async () => {
|
||||||
|
if (!(await importFormRef.value?.validate().catch(() => false))) return
|
||||||
|
const file = importForm.file
|
||||||
|
if (!file) return
|
||||||
|
importLoading.value = true
|
||||||
|
try {
|
||||||
|
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||||
|
file_name: file.name,
|
||||||
|
content_type: 'text/csv',
|
||||||
|
purpose: 'phone_unbind_import'
|
||||||
|
})
|
||||||
|
if (uploadUrlRes.code !== 0 || !uploadUrlRes.data) {
|
||||||
|
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { upload_url, file_key } = uploadUrlRes.data
|
||||||
|
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||||||
|
|
||||||
|
const res = await PhoneAssetAssociationService.createUnbindImport({
|
||||||
|
file_key,
|
||||||
|
reason: importForm.reason.trim(),
|
||||||
|
confirmed: importForm.confirmed
|
||||||
|
})
|
||||||
|
if (res.code !== 0 || !res.data) {
|
||||||
|
ElMessage.error(res.msg || '创建解绑导入任务失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success(`解绑导入任务已创建!任务编号:${res.data.task_no}`)
|
||||||
|
importDialogVisible.value = false
|
||||||
|
goUnbindImportTasks()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建解绑导入任务失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message || '创建解绑导入任务失败')
|
||||||
|
} finally {
|
||||||
|
importLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getTableData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.phone-asset-association-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-result-summary {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-tips {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
|
||||||
|
.import-tips-detail {
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
<template>
|
||||||
|
<ArtTableFullScreen>
|
||||||
|
<div class="unbind-import-task-detail-page" id="table-full-screen">
|
||||||
|
<ElCard shadow="never" class="art-table-card">
|
||||||
|
<div class="detail-header">
|
||||||
|
<ElButton @click="goBack">
|
||||||
|
<template #icon>
|
||||||
|
<ElIcon><ArrowLeft /></ElIcon>
|
||||||
|
</template>
|
||||||
|
返回
|
||||||
|
</ElButton>
|
||||||
|
<h2 class="detail-title">解绑导入任务详情</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading && !taskDetail" class="detail-loading">
|
||||||
|
<ElIcon class="is-loading" :size="36">
|
||||||
|
<Loading />
|
||||||
|
</ElIcon>
|
||||||
|
<div>加载中...</div>
|
||||||
|
</div>
|
||||||
|
<template v-else-if="taskDetail">
|
||||||
|
<DetailPage :sections="detailSections" :data="taskDetail" />
|
||||||
|
|
||||||
|
<div class="row-table-title">逐行结果</div>
|
||||||
|
<ElTable :data="taskDetail.items || []" border stripe size="default" max-height="460">
|
||||||
|
<ElTableColumn prop="line" label="行号" width="80" />
|
||||||
|
<ElTableColumn
|
||||||
|
prop="asset_identifier"
|
||||||
|
label="资产标识(原文)"
|
||||||
|
min-width="180"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<ElTableColumn prop="asset_type" label="资产类型" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.asset_type ? (row.asset_type === 'iot_card' ? '物联网卡' : '设备') : '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="asset_id" label="资产ID" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.asset_id ? row.asset_id : '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="status" label="行状态" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElTag :type="row.status === 3 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status_name || (row.status === 3 ? '成功' : '失败') }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="associated_phones"
|
||||||
|
label="解绑时关联手机号"
|
||||||
|
min-width="200"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ (row.associated_phones || []).join('、') || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="unbound_count" label="解除关系数" width="100" />
|
||||||
|
<ElTableColumn prop="reason" label="失败原因" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.reason || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</template>
|
||||||
|
<ElEmpty v-else description="暂无详情" />
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</ArtTableFullScreen>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElButton, ElCard, ElEmpty, ElIcon, ElMessage, ElTag } from 'element-plus'
|
||||||
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
|
import { PhoneAssetAssociationService } from '@/api/modules'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||||
|
import type {
|
||||||
|
PhoneAssetUnbindImportTaskDetail,
|
||||||
|
PhoneAssetUnbindImportTaskStatus
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
|
defineOptions({ name: 'PhoneAssetUnbindImportTaskDetail' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
|
||||||
|
const taskId = computed(() => Number(route.query.id || route.params.id || 0))
|
||||||
|
const loading = ref(false)
|
||||||
|
const taskDetail = ref<PhoneAssetUnbindImportTaskDetail | null>(null)
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
const getStatusTagType = (status?: PhoneAssetUnbindImportTaskStatus) => {
|
||||||
|
const map: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||||
|
1: 'info',
|
||||||
|
2: 'warning',
|
||||||
|
3: 'success',
|
||||||
|
4: 'danger'
|
||||||
|
}
|
||||||
|
return status ? map[status] || 'info' : 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
if (!taskId.value) {
|
||||||
|
ElMessage.error('缺少任务ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await PhoneAssetAssociationService.getUnbindImportTaskDetail(taskId.value)
|
||||||
|
if (res.code === 403) {
|
||||||
|
ElMessage.error(res.msg || '无权限查看该任务详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res.code !== 0 || !res.data) {
|
||||||
|
ElMessage.error(res.msg || '获取解绑导入任务详情失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskDetail.value = res.data
|
||||||
|
startPollingIfNeeded()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取解绑导入任务详情失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message || '获取解绑导入任务详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPollingIfNeeded = () => {
|
||||||
|
const status = taskDetail.value?.status
|
||||||
|
if (status === 1 || status === 2) {
|
||||||
|
if (!pollTimer) {
|
||||||
|
pollTimer = setInterval(fetchDetail, 5000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailSections = computed((): DetailSection[] => {
|
||||||
|
const data = taskDetail.value
|
||||||
|
if (!data) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '任务基本信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||||
|
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||||
|
{
|
||||||
|
label: '任务状态',
|
||||||
|
render: (task: PhoneAssetUnbindImportTaskDetail) =>
|
||||||
|
h(ElTag, { type: getStatusTagType(task.status) }, () => task.status_name || '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '任务级解绑原因',
|
||||||
|
prop: 'unbind_reason',
|
||||||
|
fullWidth: true,
|
||||||
|
formatter: (value: string) => value || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'creator_name',
|
||||||
|
formatter: (value: string) => value || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
prop: 'created_at',
|
||||||
|
formatter: (value: string) => formatDateTime(value) || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '开始时间',
|
||||||
|
prop: 'started_at',
|
||||||
|
formatter: (value: string) => formatDateTime(value) || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '完成时间',
|
||||||
|
prop: 'completed_at',
|
||||||
|
formatter: (value: string) => formatDateTime(value) || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '任务级错误',
|
||||||
|
prop: 'error_message',
|
||||||
|
fullWidth: true,
|
||||||
|
formatter: (value: string) => value || '-'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '处理统计',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: '数据行总数',
|
||||||
|
prop: 'total_count',
|
||||||
|
formatter: (value: number) => String(value ?? 0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '成功行数',
|
||||||
|
prop: 'success_count',
|
||||||
|
formatter: (value: number) => String(value ?? 0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '失败行数',
|
||||||
|
prop: 'fail_count',
|
||||||
|
formatter: (value: number) => String(value ?? 0)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push(RoutesAlias.PhoneAssetUnbindImportTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!hasAuth(AUGUST_PERMISSIONS.phoneAssetAssociation.unbindImportDetail)) {
|
||||||
|
ElMessage.warning('您没有查看解绑导入任务详情的权限')
|
||||||
|
goBack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchDetail()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.unbind-import-task-detail-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 60px 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-table-title {
|
||||||
|
margin: 20px 0 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user