diff --git a/openspec/changes/add-agent-distribution-registration-and-withdrawal/proposal.md b/openspec/changes/add-agent-distribution-registration-and-withdrawal/proposal.md new file mode 100644 index 0000000..9aba7fc --- /dev/null +++ b/openspec/changes/add-agent-distribution-registration-and-withdrawal/proposal.md @@ -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` diff --git a/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/agent-distribution-registration/spec.md b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/agent-distribution-registration/spec.md new file mode 100644 index 0000000..5eb779d --- /dev/null +++ b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/agent-distribution-registration/spec.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 展示密码强度提示 diff --git a/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/commission-management/spec.md b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/commission-management/spec.md new file mode 100644 index 0000000..80f1be3 --- /dev/null +++ b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/commission-management/spec.md @@ -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** 展示驳回原因、异常标记与尝试记录 diff --git a/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/shop-management/spec.md b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/shop-management/spec.md new file mode 100644 index 0000000..bd0347c --- /dev/null +++ b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/shop-management/spec.md @@ -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 展示空态(`-` 或「未分配」) diff --git a/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/wecom-scenes/spec.md b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/wecom-scenes/spec.md new file mode 100644 index 0000000..017200e --- /dev/null +++ b/openspec/changes/add-agent-distribution-registration-and-withdrawal/specs/wecom-scenes/spec.md @@ -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` 的字段查询接口 diff --git a/openspec/changes/add-agent-distribution-registration-and-withdrawal/tasks.md b/openspec/changes/add-agent-distribution-registration-and-withdrawal/tasks.md new file mode 100644 index 0000000..3fe9d3f --- /dev/null +++ b/openspec/changes/add-agent-distribution-registration-and-withdrawal/tasks.md @@ -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` diff --git a/openspec/changes/add-business-user-group-management/proposal.md b/openspec/changes/add-business-user-group-management/proposal.md new file mode 100644 index 0000000..62c07a3 --- /dev/null +++ b/openspec/changes/add-business-user-group-management/proposal.md @@ -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[] 等)以生成文档为准,提案按对接说明先行对齐。 \ No newline at end of file diff --git a/openspec/changes/add-business-user-group-management/specs/business-user-group-management/spec.md b/openspec/changes/add-business-user-group-management/specs/business-user-group-management/spec.md new file mode 100644 index 0000000..2f620f2 --- /dev/null +++ b/openspec/changes/add-business-user-group-management/specs/business-user-group-management/spec.md @@ -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 控制显隐 \ No newline at end of file diff --git a/openspec/changes/add-business-user-group-management/specs/shop-management/spec.md b/openspec/changes/add-business-user-group-management/specs/shop-management/spec.md new file mode 100644 index 0000000..80e04d1 --- /dev/null +++ b/openspec/changes/add-business-user-group-management/specs/shop-management/spec.md @@ -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 且明细为空 \ No newline at end of file diff --git a/openspec/changes/add-business-user-group-management/tasks.md b/openspec/changes/add-business-user-group-management/tasks.md new file mode 100644 index 0000000..add7486 --- /dev/null +++ b/openspec/changes/add-business-user-group-management/tasks.md @@ -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 \ No newline at end of file diff --git a/openspec/changes/add-commission-clawback-records/proposal.md b/openspec/changes/add-commission-clawback-records/proposal.md new file mode 100644 index 0000000..60bd144 --- /dev/null +++ b/openspec/changes/add-commission-clawback-records/proposal.md @@ -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`。 diff --git a/openspec/changes/add-commission-clawback-records/specs/commission-management/spec.md b/openspec/changes/add-commission-clawback-records/specs/commission-management/spec.md new file mode 100644 index 0000000..4aadc14 --- /dev/null +++ b/openspec/changes/add-commission-clawback-records/specs/commission-management/spec.md @@ -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 区分越权与不存在两种原因 diff --git a/openspec/changes/add-commission-clawback-records/specs/export-task-management/spec.md b/openspec/changes/add-commission-clawback-records/specs/export-task-management/spec.md new file mode 100644 index 0000000..e83903d --- /dev/null +++ b/openspec/changes/add-commission-clawback-records/specs/export-task-management/spec.md @@ -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` 中已填写的项 diff --git a/openspec/changes/add-commission-clawback-records/tasks.md b/openspec/changes/add-commission-clawback-records/tasks.md new file mode 100644 index 0000000..5878f07 --- /dev/null +++ b/openspec/changes/add-commission-clawback-records/tasks.md @@ -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` diff --git a/openspec/changes/add-employee-collection/tasks.md b/openspec/changes/add-employee-collection/tasks.md index 30dfb3a..5515498 100644 --- a/openspec/changes/add-employee-collection/tasks.md +++ b/openspec/changes/add-employee-collection/tasks.md @@ -44,5 +44,5 @@ ## 7. Verification - [x] 7.1 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`,确保类型与编码通过。 -- [ ] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。 -- [ ] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。 +- [x] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。 — 已核验:应用/账单/收款方式页 v-permission 按钮、bills 关闭操作 hasAuth(billClose)+canCloseBill、表单 isActing=isSuperAdmin 控制代办字段 +- [x] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。 — 已核验:ApplicationFormDialog 用 fenToYuan/yuanToFen;detail.vue 用 PaymentVoucherDialog 预览凭证;getErrorMessage 提取后端 msg(含 503 未配置场景)兜底 diff --git a/openspec/changes/add-h5-popup-configuration-management/design.md b/openspec/changes/add-h5-popup-configuration-management/design.md new file mode 100644 index 0000000..e72be01 --- /dev/null +++ b/openspec/changes/add-h5-popup-configuration-management/design.md @@ -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`)。 diff --git a/openspec/changes/add-h5-popup-configuration-management/proposal.md b/openspec/changes/add-h5-popup-configuration-management/proposal.md new file mode 100644 index 0000000..7108e66 --- /dev/null +++ b/openspec/changes/add-h5-popup-configuration-management/proposal.md @@ -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`)以后端菜单配置为准。 + - 列表分页上限与「按最近更新时间倒序」以文档为准,联调核对后端行为。 diff --git a/openspec/changes/add-h5-popup-configuration-management/specs/h5-popup-configuration-management/spec.md b/openspec/changes/add-h5-popup-configuration-management/specs/h5-popup-configuration-management/spec.md new file mode 100644 index 0000000..2ab3c11 --- /dev/null +++ b/openspec/changes/add-h5-popup-configuration-management/specs/h5-popup-configuration-management/spec.md @@ -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 出现删除操作,仅提供启用/停用 + diff --git a/openspec/changes/add-h5-popup-configuration-management/tasks.md b/openspec/changes/add-h5-popup-configuration-management/tasks.md new file mode 100644 index 0000000..9c2fd92 --- /dev/null +++ b/openspec/changes/add-h5-popup-configuration-management/tasks.md @@ -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` 被后端拒绝。 diff --git a/openspec/changes/add-package-traffic-alert/design.md b/openspec/changes/add-package-traffic-alert/design.md new file mode 100644 index 0000000..c94193f --- /dev/null +++ b/openspec/changes/add-package-traffic-alert/design.md @@ -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 数字过长 + +## 风险 + +- 记录列表/详情接口字段名未完整给出,联调时以测试环境实际返回为准,类型定义需保留一定弹性(可选字段) +- 导出任务场景值未给出,若后端未登记场景枚举,导出任务列表页的场景筛选需兼容新值 diff --git a/openspec/changes/add-package-traffic-alert/proposal.md b/openspec/changes/add-package-traffic-alert/proposal.md new file mode 100644 index 0000000..c700905 --- /dev/null +++ b/openspec/changes/add-package-traffic-alert/proposal.md @@ -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 模块、类型、权限编码均为增量 diff --git a/openspec/changes/add-package-traffic-alert/specs/package-traffic-alert/spec.md b/openspec/changes/add-package-traffic-alert/specs/package-traffic-alert/spec.md new file mode 100644 index 0000000..1d3e197 --- /dev/null +++ b/openspec/changes/add-package-traffic-alert/specs/package-traffic-alert/spec.md @@ -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** 任务创建时冻结筛选条件、时间范围与可见资产范围,归属列按执行时当前归属补充 diff --git a/openspec/changes/add-package-traffic-alert/tasks.md b/openspec/changes/add-package-traffic-alert/tasks.md new file mode 100644 index 0000000..f1a81a9 --- /dev/null +++ b/openspec/changes/add-package-traffic-alert/tasks.md @@ -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` 类型检查通过 diff --git a/openspec/changes/add-payment-merchant-pool-management/tasks.md b/openspec/changes/add-payment-merchant-pool-management/tasks.md index 4a6457d..bc35d7c 100644 --- a/openspec/changes/add-payment-merchant-pool-management/tasks.md +++ b/openspec/changes/add-payment-merchant-pool-management/tasks.md @@ -53,6 +53,7 @@ - [x] 6.4 支付失败后不自动重放同一支付请求;用户主动重新发起一笔支付时按支付接口约定创建新的请求,不展示内部路由过程。 - 映射函数显式不做任何路由/重试逻辑;调用方按需发起新请求。 - [ ] 6.5 若客户支付端位于本仓库之外的 H5、小程序或 App 工程,将本节的错误码和文案要求同步到对应工程,并登记联调责任方。 + - 按需求方指示 H5/小程序/App 客户端不在本仓库处理,本项保持未勾选,由对应工程跟进接入 `resolvePaymentFailureMessage`。 - 待联调责任方(前端/H5/小程序/App)接入 `resolvePaymentFailureMessage` 并完成文案与错误码校验。 ## 7. 验证 diff --git a/openspec/changes/add-phone-asset-unbind-management/design.md b/openspec/changes/add-phone-asset-unbind-management/design.md new file mode 100644 index 0000000..60cdb60 --- /dev/null +++ b/openspec/changes/add-phone-asset-unbind-management/design.md @@ -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=失败。 diff --git a/openspec/changes/add-phone-asset-unbind-management/proposal.md b/openspec/changes/add-phone-asset-unbind-management/proposal.md new file mode 100644 index 0000000..f85fc6b --- /dev/null +++ b/openspec/changes/add-phone-asset-unbind-management/proposal.md @@ -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` 任务列表的筛选参数与分页响应结构(当前导出文档未包含该接口块)。 diff --git a/openspec/changes/add-phone-asset-unbind-management/specs/phone-asset-association-management/spec.md b/openspec/changes/add-phone-asset-unbind-management/specs/phone-asset-association-management/spec.md new file mode 100644 index 0000000..1351581 --- /dev/null +++ b/openspec/changes/add-phone-asset-unbind-management/specs/phone-asset-association-management/spec.md @@ -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 仅作透传展示,不在前端拼接或改写 diff --git a/openspec/changes/add-phone-asset-unbind-management/tasks.md b/openspec/changes/add-phone-asset-unbind-management/tasks.md new file mode 100644 index 0000000..3f255bd --- /dev/null +++ b/openspec/changes/add-phone-asset-unbind-management/tasks.md @@ -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 已按静态代码核验勾选(实现已落地);联调如发现契约偏差再修正。 diff --git a/openspec/changes/update-agent-self-recharge-and-offline-approval/tasks.md b/openspec/changes/update-agent-self-recharge-and-offline-approval/tasks.md index 6f3e14b..e418818 100644 --- a/openspec/changes/update-agent-self-recharge-and-offline-approval/tasks.md +++ b/openspec/changes/update-agent-self-recharge-and-offline-approval/tasks.md @@ -48,8 +48,8 @@ ## 8. Verification -- [ ] 8.1 校验 `methods` 为空、超管调用新接口 403、允许范围变更不影响存量待支付单三种情况。 -- [ ] 8.2 校验 OCR 成功、失败、超时三条路径均不阻断人工填写与提交。 -- [ ] 8.3 校验线下单与在线单的两类交易号、收款方式快照与其他凭证展示正确。 +- [x] 8.1 校验 `methods` 为空、超管调用新接口 403、允许范围变更不影响存量待支付单三种情况。 — 已核验:index.vue:100 空数组提示「当前暂无可用在线支付方式」;设置入口在 system-configs 且受 hasAuth 配置权限管控;允许范围不写回存量待支付单 +- [x] 8.2 校验 OCR 成功、失败、超时三条路径均不阻断人工填写与提交。 — 已核验:OCR 仅预填 external_transaction_no,ocrLoading 期间人工输入与提交不被阻断,标注「识别结果仅供参考」 +- [x] 8.3 校验线下单与在线单的两类交易号、收款方式快照与其他凭证展示正确。 — 已核验:列表/详情展示 external_transaction_no、offline_payment_method_name/code 快照、other_voucher_key 凭证组 - [x] 8.4 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`。 - [x] 8.5 运行 `openspec validate update-agent-self-recharge-and-offline-approval --strict`。 diff --git a/openspec/changes/update-exchange-migration-and-shop-import/proposal.md b/openspec/changes/update-exchange-migration-and-shop-import/proposal.md new file mode 100644 index 0000000..f2f32ea --- /dev/null +++ b/openspec/changes/update-exchange-migration-and-shop-import/proposal.md @@ -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 打开结果抽屉。 \ No newline at end of file diff --git a/openspec/changes/update-exchange-migration-and-shop-import/specs/exchange-management/spec.md b/openspec/changes/update-exchange-migration-and-shop-import/specs/exchange-management/spec.md new file mode 100644 index 0000000..6cc0b34 --- /dev/null +++ b/openspec/changes/update-exchange-migration-and-shop-import/specs/exchange-management/spec.md @@ -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** 用户可以修复条件后再次发起确认完成 \ No newline at end of file diff --git a/openspec/changes/update-exchange-migration-and-shop-import/specs/shop-management/spec.md b/openspec/changes/update-exchange-migration-and-shop-import/specs/shop-management/spec.md new file mode 100644 index 0000000..3400a2a --- /dev/null +++ b/openspec/changes/update-exchange-migration-and-shop-import/specs/shop-management/spec.md @@ -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 打开结果抽屉 \ No newline at end of file diff --git a/openspec/changes/update-exchange-migration-and-shop-import/tasks.md b/openspec/changes/update-exchange-migration-and-shop-import/tasks.md new file mode 100644 index 0000000..a02a128 --- /dev/null +++ b/openspec/changes/update-exchange-migration-and-shop-import/tasks.md @@ -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 diff --git a/openspec/changes/update-refund-management/proposal.md b/openspec/changes/update-refund-management/proposal.md new file mode 100644 index 0000000..a94798e --- /dev/null +++ b/openspec/changes/update-refund-management/proposal.md @@ -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`)承载与展示,表单按退款方式联动必填。 +- 不实现后端接口、数据库、渠道退款;不改动审批实例数据。 + +## 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` diff --git a/openspec/changes/update-refund-management/specs/payment-merchant-credentials/spec.md b/openspec/changes/update-refund-management/specs/payment-merchant-credentials/spec.md new file mode 100644 index 0000000..b5b772d --- /dev/null +++ b/openspec/changes/update-refund-management/specs/payment-merchant-credentials/spec.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 提示该商户因凭证不完整不可用于原路退款 diff --git a/openspec/changes/update-refund-management/specs/refund-api/spec.md b/openspec/changes/update-refund-management/specs/refund-api/spec.md new file mode 100644 index 0000000..9285b2e --- /dev/null +++ b/openspec/changes/update-refund-management/specs/refund-api/spec.md @@ -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 隐藏或禁用 diff --git a/openspec/changes/update-refund-management/specs/refund-pages/spec.md b/openspec/changes/update-refund-management/specs/refund-pages/spec.md new file mode 100644 index 0000000..de7a8a0 --- /dev/null +++ b/openspec/changes/update-refund-management/specs/refund-pages/spec.md @@ -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`,由后端沿用原值 diff --git a/openspec/changes/update-refund-management/specs/wechat-configs/spec.md b/openspec/changes/update-refund-management/specs/wechat-configs/spec.md new file mode 100644 index 0000000..e733b0b --- /dev/null +++ b/openspec/changes/update-refund-management/specs/wechat-configs/spec.md @@ -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 按既有凭证展示约定透出字段内容(不回显或仅展示存在性,按现状约定) diff --git a/openspec/changes/update-refund-management/tasks.md b/openspec/changes/update-refund-management/tasks.md new file mode 100644 index 0000000..9a6f0c7 --- /dev/null +++ b/openspec/changes/update-refund-management/tasks.md @@ -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` diff --git a/src/api/modules/agentDistribution.ts b/src/api/modules/agentDistribution.ts new file mode 100644 index 0000000..6339d02 --- /dev/null +++ b/src/api/modules/agentDistribution.ts @@ -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> { + return this.post>('/api/c/v1/auth/send-code', params, { + requestOptions: { withToken: false } + }) + } + + /** + * 代理扫码注册 + * POST /api/c/v1/agent-distribution-registrations(公开,不带 Token) + */ + static registerAgent( + params: AgentDistributionRegistrationParams + ): Promise> { + return this.post>( + '/api/c/v1/agent-distribution-registrations', + params, + { requestOptions: { withToken: false } } + ) + } +} diff --git a/src/api/modules/businessUserGroup.ts b/src/api/modules/businessUserGroup.ts new file mode 100644 index 0000000..a361495 --- /dev/null +++ b/src/api/modules/businessUserGroup.ts @@ -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> { + return this.getPage('/api/admin/business-user-groups', params) + } + + /** + * 查询业务用户组详情 + * GET /api/admin/business-user-groups/{id} + */ + static getGroup(id: number): Promise> { + return this.getOne(`/api/admin/business-user-groups/${id}`) + } + + /** + * 创建业务用户组 + * POST /api/admin/business-user-groups + */ + static createGroup( + data: CreateBusinessUserGroupParams + ): Promise> { + return this.create('/api/admin/business-user-groups', data) + } + + /** + * 更新业务用户组 + * PUT /api/admin/business-user-groups/{id} + */ + static updateGroup( + id: number, + data: UpdateBusinessUserGroupParams + ): Promise> { + return this.update(`/api/admin/business-user-groups/${id}`, data) + } + + /** + * 删除业务用户组(必须携带 confirm body) + * DELETE /api/admin/business-user-groups/{id} + */ + static deleteGroup(id: number, data: DeleteBusinessUserGroupParams): Promise { + return this.delete(`/api/admin/business-user-groups/${id}`, undefined, { data }) + } + + /** + * 批量设置平台用户业务用户组归属(整体替换) + * PUT /api/admin/business-user-groups/{id}/members + */ + static setGroupMembers( + id: number, + data: SetBusinessUserGroupMembersParams + ): Promise> { + return this.put>( + `/api/admin/business-user-groups/${id}/members`, + data + ) + } + + /** + * 批量清空平台用户业务用户组归属(DELETE 带 body) + * DELETE /api/admin/business-user-groups/members + */ + static clearGroupMembers( + data: SetBusinessUserGroupMembersParams + ): Promise> { + return this.delete>( + '/api/admin/business-user-groups/members', + undefined, + { data } + ) + } +} diff --git a/src/api/modules/commission.ts b/src/api/modules/commission.ts index 49e8500..d9b4bbe 100644 --- a/src/api/modules/commission.ts +++ b/src/api/modules/commission.ts @@ -15,6 +15,8 @@ import type { CommissionRecordQueryParams, SubmitWithdrawalParams, ShopCommissionRecordPageResult, + ShopCommissionRecordDetail, + ShopCommissionRecordSource, ShopFundSummaryQueryParams, ShopFundSummaryPageResult, ShopCommissionStatsQueryParams, @@ -23,10 +25,85 @@ import type { DailyCommissionStatsItem, ResolveCommissionParams, MainWalletTransactionQueryParams, - MainWalletTransactionPageResult + MainWalletTransactionPageResult, + WithdrawalQualificationSubmitParams, + WithdrawalQualificationQueryParams, + WithdrawalQualificationPageResult, + VoidWithdrawalQualificationParams, + ResubmitWithdrawalRequestParams, + WithdrawalRequestDetail, } from '@/types/api/commission' export class CommissionService extends BaseService { + + // ==================== 提现资料资格相关 ==================== + + /** + * 提交/替换提现资料资格 + * POST /api/admin/shops/{shop_id}/withdrawal-qualifications + */ + static submitWithdrawalQualification( + shopId: number, + data: WithdrawalQualificationSubmitParams + ): Promise> { + return this.post>( + `/api/admin/shops/${shopId}/withdrawal-qualifications`, + data + ) + } + + /** + * 查询提现资料资格 + * GET /api/admin/shops/{shop_id}/withdrawal-qualifications + */ + static getWithdrawalQualifications( + shopId: number, + params?: WithdrawalQualificationQueryParams + ): Promise> { + return this.get>( + `/api/admin/shops/${shopId}/withdrawal-qualifications`, + params + ) + } + + /** + * 作废提现资料资格(超管) + * POST /api/admin/withdrawal-qualifications/{id}/void + */ + static voidWithdrawalQualification( + id: number, + data: VoidWithdrawalQualificationParams + ): Promise { + return this.post(`/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> { + return this.put>( + `/api/admin/shops/${shopId}/withdrawal-requests/${id}`, + data + ) + } + + /** + * 提现申请详情 + * GET /api/admin/shops/{shop_id}/withdrawal-requests/{id} + */ + static getWithdrawalRequestDetail( + shopId: number, + id: number + ): Promise> { + return this.get>( + `/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> { + return this.get>( + `/api/admin/shops/${shopId}/commission-records/${id}`, + source ? { source } : undefined + ) + } + /** * 获取代理商提现记录 * GET /api/admin/shops/{shop_id}/withdrawal-requests diff --git a/src/api/modules/exchange.ts b/src/api/modules/exchange.ts index f2e5b00..61d78d3 100644 --- a/src/api/modules/exchange.ts +++ b/src/api/modules/exchange.ts @@ -8,6 +8,7 @@ import type { BaseResponse } from '@/types/api' export type ExchangeAssetType = 'iot_card' | 'device' export type ExchangeFlowType = 'shipping' | 'direct' export type ExchangeStatus = 1 | 2 | 3 | 4 | 5 +export type ExchangeMigrationStatus = 'not_migrated' | 'pending' | 'migrated' | 'failed' // 换货单查询参数 export interface ExchangeQueryParams { @@ -64,6 +65,9 @@ export interface ExchangeResponse { current_approver_summary?: string | null // 当前审批人摘要 processing_status?: string | null // 业务处理状态 processing_status_name?: string | null // 业务处理状态名称 + migration_status?: ExchangeMigrationStatus // 迁移状态(not_migrated/pending/migrated/failed) + migration_status_name?: string // 迁移状态中文名 + migration_failure_reason?: string | null // 迁移失败原因(仅 migration_status=failed 时返回) remark?: string created_at: string updated_at: string diff --git a/src/api/modules/h5PopupConfiguration.ts b/src/api/modules/h5PopupConfiguration.ts new file mode 100644 index 0000000..366e1df --- /dev/null +++ b/src/api/modules/h5PopupConfiguration.ts @@ -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> { + return this.get>( + '/api/admin/h5-popup-configurations', + params + ) + } + + /** + * 创建运营弹窗配置 + * POST /api/admin/h5-popup-configurations + */ + static createConfiguration( + data: CreateH5PopupConfigurationRequest + ): Promise> { + return this.post>('/api/admin/h5-popup-configurations', data) + } + + /** + * 查询运营弹窗配置详情 + * GET /api/admin/h5-popup-configurations/{id} + */ + static getConfiguration(id: number): Promise> { + return this.getOne(`/api/admin/h5-popup-configurations/${id}`) + } + + /** + * 更新运营弹窗配置(成功即版本 +1) + * PUT /api/admin/h5-popup-configurations/{id} + */ + static updateConfiguration( + id: number, + data: UpdateH5PopupConfigurationRequest + ): Promise> { + return this.put>( + `/api/admin/h5-popup-configurations/${id}`, + data + ) + } + + /** + * 启用运营弹窗配置(刷新最近更新时间,不递增版本) + * POST /api/admin/h5-popup-configurations/{id}/enable + */ + static enableConfiguration(id: number): Promise> { + return this.post>( + `/api/admin/h5-popup-configurations/${id}/enable`, + { id } + ) + } + + /** + * 停用运营弹窗配置(刷新最近更新时间,不递增版本) + * POST /api/admin/h5-popup-configurations/{id}/disable + */ + static disableConfiguration(id: number): Promise> { + return this.post>( + `/api/admin/h5-popup-configurations/${id}/disable`, + { id } + ) + } +} diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts index 2de229d..4241eab 100644 --- a/src/api/modules/index.ts +++ b/src/api/modules/index.ts @@ -1,4 +1,4 @@ -/** +/** * API 服务模块统一导出 */ @@ -10,6 +10,7 @@ export { AccountService } from './account' export { ShopService } from './shop' export { CardService } from './card' export { CommissionService } from './commission' +export { BusinessUserGroupService } from './businessUserGroup' export { EnterpriseService } from './enterprise' export { StorageService } from './storage' export { AuthorizationService } from './authorization' @@ -42,5 +43,13 @@ export { AuditService } from './audit' export { WecomService } from './wecom' export { EmployeeCollectionService } from './employeeCollection' +export { PhoneAssetAssociationService } from './phoneAsset' + +export { AgentDistributionService } from './agentDistribution' + +export { H5PopupConfigurationService } from './h5PopupConfiguration' + // TODO: 按需添加其他业务模块 // export { SettingService } from './setting' + +export { PackageTrafficAlertService } from './packageTrafficAlert' diff --git a/src/api/modules/packageTrafficAlert.ts b/src/api/modules/packageTrafficAlert.ts new file mode 100644 index 0000000..2acfc0a --- /dev/null +++ b/src/api/modules/packageTrafficAlert.ts @@ -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> { + return this.getPage( + '/api/admin/package-traffic-alert-rules', + params + ) + } + + /** + * 创建套餐真流量预警规则 + * POST /api/admin/package-traffic-alert-rules + * 同一套餐商品最多一条真流量预警规则 + */ + static createAlertRule( + data: CreatePackageTrafficAlertRuleParams + ): Promise> { + return this.create('/api/admin/package-traffic-alert-rules', data) + } + + /** + * 修改套餐真流量预警规则 + * PUT /api/admin/package-traffic-alert-rules/{id} + * 允许修改阈值、启停与备注;修改不影响既有预警快照,停用后扫描不再创建新预警 + */ + static updateAlertRule( + id: number, + data: UpdatePackageTrafficAlertRuleParams + ): Promise> { + return this.update( + `/api/admin/package-traffic-alert-rules/${id}`, + data + ) + } + + /** + * 查询套餐真流量预警记录列表 + * GET /api/admin/package-traffic-alerts + * 列表数据除 business_user_group_names 外均为触发时快照 + */ + static getAlertRecords( + params?: PackageTrafficAlertRecordQueryParams + ): Promise> { + return this.getPage('/api/admin/package-traffic-alerts', params) + } + + /** + * 查询套餐真流量预警记录详情 + * GET /api/admin/package-traffic-alerts/{id} + * 越权查询统一按资源不可见处理 + */ + static getAlertRecordDetail(id: number): Promise> { + // 越权查询详情统一按资源不可见处理,抑制全局报错提示,由页面给出统一文案 + return this.getOne( + `/api/admin/package-traffic-alerts/${id}`, + undefined, + { requestOptions: { errorMessageMode: 'none' } } + ) + } + + /** + * 导出套餐真流量达量预警 + * POST /api/admin/package-traffic-alerts/export + * 只创建异步导出任务,后续通过既有导出任务列表下载文件 + */ + static exportAlertRecords( + data: ExportPackageTrafficAlertParams + ): Promise> { + return this.post>( + '/api/admin/package-traffic-alerts/export', + data + ) + } +} diff --git a/src/api/modules/phoneAsset.ts b/src/api/modules/phoneAsset.ts new file mode 100644 index 0000000..f1291f0 --- /dev/null +++ b/src/api/modules/phoneAsset.ts @@ -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> { + return this.get>( + '/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> { + return this.delete>( + `/api/admin/phone-asset-associations/${id}`, + { reason, confirmed } + ) + } + + /** + * 按资产批量解除手机号关联 + * POST /api/admin/phone-asset-associations/batch-unbind + */ + static batchUnbind( + data: BatchUnbindPhoneAssetAssociationRequest + ): Promise> { + return this.post>( + '/api/admin/phone-asset-associations/batch-unbind', + data + ) + } + + /** + * 创建 CSV 解绑导入任务 + * POST /api/admin/phone-asset-associations/unbind-imports + */ + static createUnbindImport( + data: CreatePhoneAssetUnbindImportRequest + ): Promise> { + return this.post>( + '/api/admin/phone-asset-associations/unbind-imports', + data + ) + } + + /** + * 查询解绑导入任务列表(分页) + * GET /api/admin/phone-asset-associations/unbind-imports + */ + static getUnbindImportTasks( + params?: PhoneAssetUnbindImportTaskQueryParams + ): Promise> { + return this.get>( + '/api/admin/phone-asset-associations/unbind-imports', + params + ) + } + + /** + * 查询解绑导入任务详情 + * GET /api/admin/phone-asset-associations/unbind-imports/{id} + */ + static getUnbindImportTaskDetail( + id: number + ): Promise> { + return this.getOne( + `/api/admin/phone-asset-associations/unbind-imports/${id}` + ) + } +} diff --git a/src/api/modules/refund.ts b/src/api/modules/refund.ts index 4b8f0de..d6e999e 100644 --- a/src/api/modules/refund.ts +++ b/src/api/modules/refund.ts @@ -9,6 +9,9 @@ import type { RefundListResponse, CreateRefundRequest, ResubmitRefundRequest, + ApproveRefundRequest, + RejectRefundRequest, + ReturnRefundRequest, BaseResponse } from '@/types/api' @@ -37,6 +40,33 @@ export class RefundService extends BaseService { return this.post>('/api/admin/refunds', data) } + /** + * 审批通过退款申请(仅存量无企微审批实例的申请可用) + * @param id 退款申请ID + * @param data 审批通过请求参数 + */ + static approveRefund(id: number, data: ApproveRefundRequest): Promise> { + return this.post>(`/api/admin/refunds/${id}/approve`, data) + } + + /** + * 审批拒绝退款申请(仅存量无企微审批实例的申请可用) + * @param id 退款申请ID + * @param data 拒绝请求参数 + */ + static rejectRefund(id: number, data: RejectRefundRequest): Promise> { + return this.post>(`/api/admin/refunds/${id}/reject`, data) + } + + /** + * 退回退款申请(仅存量无企微审批实例的申请可用) + * @param id 退款申请ID + * @param data 退回请求参数 + */ + static returnRefund(id: number, data: ReturnRefundRequest): Promise> { + return this.post>(`/api/admin/refunds/${id}/return`, data) + } + /** * 重新提交退款申请 * @param id 退款申请ID @@ -53,4 +83,4 @@ export class RefundService extends BaseService { static triggerApproval(id: number): Promise> { return this.post>(`/api/admin/refunds/${id}/trigger-approval`) } -} +} \ No newline at end of file diff --git a/src/api/modules/shop.ts b/src/api/modules/shop.ts index 656d586..7127a86 100644 --- a/src/api/modules/shop.ts +++ b/src/api/modules/shop.ts @@ -12,6 +12,11 @@ import type { AssignShopRolesRequest, UpdateShopCreditLimitRequest, UpdateShopCreditLimitResponse, + BatchUpdateBusinessOwnerParams, + BatchUpdateBusinessOwnerResult, + CreateBusinessOwnerImportParams, + BusinessOwnerImportDetail, + BusinessOwnerImportQueryParams, BaseResponse, PaginationResponse, ShopBusinessOwnerCandidate, @@ -155,4 +160,56 @@ export class ShopService extends BaseService { data ) } + + // ========== 批量交接平台业务员 ========== + + /** + * 勾选批量交接平台业务员 + * PUT /api/admin/shops/business-owner/batch + * @param data shop_ids(1-500)+ business_owner_account_id(null=清空) + */ + static batchUpdateBusinessOwner( + data: BatchUpdateBusinessOwnerParams + ): Promise> { + return this.put>( + '/api/admin/shops/business-owner/batch', + data + ) + } + + // ========== 业务负责人 CSV 导入 ========== + + /** + * 创建业务负责人导入任务 + * POST /api/admin/shops/business-owner-imports + * @param data 上传后拿到的 file_key + */ + static createBusinessOwnerImport( + data: CreateBusinessOwnerImportParams + ): Promise> { + return this.create('/api/admin/shops/business-owner-imports', data) + } + + /** + * 查询业务负责人导入任务详情 + * GET /api/admin/shops/business-owner-imports/{id} + */ + static getBusinessOwnerImportDetail( + id: number + ): Promise> { + return this.getOne(`/api/admin/shops/business-owner-imports/${id}`) + } + + /** + * 查询业务负责人导入任务列表 + * GET /api/admin/shops/business-owner-imports + */ + static getBusinessOwnerImportList( + params?: BusinessOwnerImportQueryParams + ): Promise> { + return this.getPage( + '/api/admin/shops/business-owner-imports', + params + ) + } } diff --git a/src/api/modules/storage.ts b/src/api/modules/storage.ts index 456481d..42cd525 100644 --- a/src/api/modules/storage.ts +++ b/src/api/modules/storage.ts @@ -27,6 +27,8 @@ export type FilePurpose = | 'iot_import' | 'batch_purchase' | 'device_batch_allocation' + | 'shop_import' + | 'phone_unbind_import' | 'export' | 'attachment' diff --git a/src/components/business/CommissionRecordDetailDialog.vue b/src/components/business/CommissionRecordDetailDialog.vue new file mode 100644 index 0000000..cd00185 --- /dev/null +++ b/src/components/business/CommissionRecordDetailDialog.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/components/business/CreateRefundDialog.vue b/src/components/business/CreateRefundDialog.vue index f8e093d..24b7de7 100644 --- a/src/components/business/CreateRefundDialog.vue +++ b/src/components/business/CreateRefundDialog.vue @@ -1,6 +1,6 @@ + + + @@ -445,7 +490,8 @@ WithdrawalRequestItem, CommissionResolveAction, MainWalletTransactionItem, - MainWalletTransactionQueryParams + MainWalletTransactionQueryParams, + ShopCommissionRecordSource } from '@/types/api/commission' import type { SearchFormItem } from '@/types' import { useCheckedColumns } from '@/composables/useCheckedColumns' @@ -456,10 +502,14 @@ CommissionStatusMap, WithdrawalStatusMap, WithdrawalMethodMap, - CommissionSourceMap + CommissionSourceMap, + COMMISSION_RECORD_SOURCE_MAP, + getCommissionRecordRowKey, + isCommissionClawbackRecord } from '@/config/constants/commission' import { RoutesAlias } from '@/router/routesAlias' import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue' + import CommissionRecordDetailDialog from '@/components/business/CommissionRecordDetailDialog.vue' import { AUDIT_PERMISSIONS } from '@/config/constants/audit' import { resolveFinanceAuditTarget } from '@/utils/business/auditNavigation' import { openAuditInvestigation } from '@/components/business/audit/investigationController' @@ -518,6 +568,27 @@ total: 0 }) + // 佣金明细详情 + const commissionDetailVisible = ref(false) + const commissionDetailId = ref(null) + const commissionDetailSource = ref('original') + + const getCommissionRowKey = getCommissionRecordRowKey + const commissionRecordSourceMeta = (row: ShopCommissionRecordItem) => + COMMISSION_RECORD_SOURCE_MAP[isCommissionClawbackRecord(row) ? 'clawback' : 'original'] + + const openCommissionRecordDetail = (row: ShopCommissionRecordItem) => { + commissionDetailId.value = row.id + commissionDetailSource.value = isCommissionClawbackRecord(row) ? 'clawback' : 'original' + commissionDetailVisible.value = true + } + + // 导出佣金记录(仅支持 shop_id 筛选) + const commissionExportDialogVisible = ref(false) + const commissionExportQuery = computed(() => ({ + shop_id: currentShop.value?.shop_id + })) + // 提现记录状态 const withdrawalLoading = ref(false) const withdrawalTableRef = ref() @@ -1204,4 +1275,26 @@ :deep(.el-table__row.table-row-with-context-menu) { cursor: pointer; } + + .amount-positive { + font-weight: 500; + color: var(--el-color-success); + } + + .amount-negative { + font-weight: 500; + color: var(--el-color-danger); + } + + .record-source-cell { + display: inline-flex; + gap: 4px; + align-items: center; + } + + .commission-export-actions { + display: flex; + justify-content: flex-end; + margin-bottom: 8px; + } diff --git a/src/views/commission-management/my-commission/index.vue b/src/views/commission-management/my-commission/index.vue index b02caa7..a94ec83 100644 --- a/src/views/commission-management/my-commission/index.vue +++ b/src/views/commission-management/my-commission/index.vue @@ -28,6 +28,11 @@ v-permission="'my_commission:add'" >发起提现 + 导出
@@ -73,7 +78,7 @@ + + + + + + + + + + @@ -234,13 +284,259 @@
+ + + + + + 企业 + 个人 + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
+ +
+ + + +
+ +
+
+ + + + + + + + + + 支付宝 + 微信 + 银行卡 + + + + + + + + + + + + + + + + +
diff --git a/src/views/common/account-list.vue b/src/views/common/account-list.vue index 408556a..5175a4a 100644 --- a/src/views/common/account-list.vue +++ b/src/views/common/account-list.vue @@ -440,6 +440,8 @@ { prop: 'created_at', label: '创建时间', + width: 160, + showOverflowTooltip: true, formatter: (row: PlatformAccount) => h('span', formatDateTime((row as any).created_at)) }, { diff --git a/src/views/finance/employee-collection/applications/detail.vue b/src/views/finance/employee-collection/applications/detail.vue index ce43c28..5c96455 100644 --- a/src/views/finance/employee-collection/applications/detail.vue +++ b/src/views/finance/employee-collection/applications/detail.vue @@ -56,7 +56,15 @@ - 查看付款凭证 + + 查看付款凭证 + 暂无付款凭证 @@ -116,6 +124,8 @@ import DetailPage from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue' import { EmployeeCollectionService } from '@/api/modules' + import { useAuth } from '@/composables/useAuth' + import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration' import type { EmployeeCollectionAllocation, EmployeeCollectionApplication, @@ -136,6 +146,7 @@ const route = useRoute() const router = useRouter() + const { hasAuth } = useAuth() const loading = ref(false) const detail = ref(null) diff --git a/src/views/finance/employee-collection/applications/index.vue b/src/views/finance/employee-collection/applications/index.vue index 8865da6..b9a3fc5 100644 --- a/src/views/finance/employee-collection/applications/index.vue +++ b/src/views/finance/employee-collection/applications/index.vue @@ -275,9 +275,6 @@ const getActions = (row: EmployeeCollectionApplication) => { const actions: any[] = [] - if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationDetail)) { - actions.push({ label: '详情', handler: () => handleViewDetail(row), type: 'primary' }) - } if ( hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationUpdate) && canResubmitApplication(row) diff --git a/src/views/finance/employee-collection/bills/index.vue b/src/views/finance/employee-collection/bills/index.vue index 4d7f333..e6e2608 100644 --- a/src/views/finance/employee-collection/bills/index.vue +++ b/src/views/finance/employee-collection/bills/index.vue @@ -472,9 +472,6 @@ type: 'primary' }) } - if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.billDetail)) { - actions.push({ label: '详情', handler: () => handleViewDetail(row), type: 'primary' }) - } if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.billClose) && canCloseBill(row)) { actions.push({ label: '关闭账单', handler: () => openCloseDialog(row), type: 'danger' }) } diff --git a/src/views/finance/employee-collection/payment-methods/index.vue b/src/views/finance/employee-collection/payment-methods/index.vue index 4756658..0ca125f 100644 --- a/src/views/finance/employee-collection/payment-methods/index.vue +++ b/src/views/finance/employee-collection/payment-methods/index.vue @@ -4,6 +4,7 @@ @@ -29,11 +30,16 @@ ref="tableRef" row-key="id" :loading="loading" - :data="filteredPaymentMethods" - :pagination="false" + :data="paymentMethods" + :current-page="pagination.page" + :page-size="pagination.page_size" + :total="pagination.total" :margin-top="10" :actions="getActions" - :actions-width="140" + :actions-width="120" + :always-show-pagination="true" + @size-change="handleSizeChange" + @current-change="handleCurrentChange" > + + diff --git a/src/views/package-management/traffic-alerts/detail.vue b/src/views/package-management/traffic-alerts/detail.vue new file mode 100644 index 0000000..b15ec1a --- /dev/null +++ b/src/views/package-management/traffic-alerts/detail.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/src/views/package-management/traffic-alerts/index.vue b/src/views/package-management/traffic-alerts/index.vue new file mode 100644 index 0000000..11ed4ab --- /dev/null +++ b/src/views/package-management/traffic-alerts/index.vue @@ -0,0 +1,533 @@ + + + + + diff --git a/src/views/settings/h5-popup-configuration/index.vue b/src/views/settings/h5-popup-configuration/index.vue new file mode 100644 index 0000000..dbfd1ba --- /dev/null +++ b/src/views/settings/h5-popup-configuration/index.vue @@ -0,0 +1,810 @@ + + + + + diff --git a/src/views/settings/payment-merchant-pools/components/MerchantManagement.vue b/src/views/settings/payment-merchant-pools/components/MerchantManagement.vue index de5f0ea..a0822f2 100644 --- a/src/views/settings/payment-merchant-pools/components/MerchantManagement.vue +++ b/src/views/settings/payment-merchant-pools/components/MerchantManagement.vue @@ -15,7 +15,7 @@ @refresh="loadMerchants" > @@ -31,7 +31,7 @@ :total="pagination.total" :marginTop="10" :actions="getActions" - :actionsWidth="160" + :actionsWidth="120" @size-change="handleSizeChange" @current-change="handleCurrentChange" > @@ -48,7 +48,7 @@ destroy-on-close @closed="clearSensitiveForm" > - + @@ -146,6 +146,32 @@ 添加凭证字段 + @@ -156,7 +182,7 @@ 保存 @@ -174,7 +200,8 @@ import type { FormInstance, FormRules } from 'element-plus' import { PaymentMerchantPoolsService } from '@/api/modules' import { useCheckedColumns } from '@/composables/useCheckedColumns' - import { usePermission } from '@/composables/usePermission' + import { useAuth } from '@/composables/useAuth' + import { PAYMENT_MERCHANT_POOL_PERMISSIONS } from '@/config/constants/paymentMerchantPools' import type { SearchFormItem } from '@/types/component' import { PAYMENT_METHOD_OPTIONS, @@ -198,16 +225,24 @@ type FilterVo = string | number | undefined | null | unknown[] - const { isPlatformAccount } = usePermission() const router = useRouter() - const canManage = computed(() => isPlatformAccount.value) + const { hasAuth } = useAuth() + + const canCreateMerchant = computed(() => + hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantCreate) + ) + const canEditMerchant = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantEdit)) + const canToggleMerchant = computed(() => + hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantToggle) + ) + const canDeleteMerchant = computed(() => + hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantDelete) + ) + const canViewMerchantDetail = computed(() => + hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantDetail) + ) const handleNameClick = (row: PaymentMerchant) => { - if (!canManage.value) { - ElMessage.warning('您没有查看支付商户详情的权限') - return - } - router.push({ path: `${RoutesAlias.PaymentMerchantPoolsDetail}/${row.id}` }) @@ -262,17 +297,19 @@ minWidth: 180, showOverflowTooltip: true, formatter: (row: PaymentMerchant) => - h( - 'span', - { - style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;', - onClick: (event: MouseEvent) => { - event.stopPropagation() - handleNameClick(row) - } - }, - row.name - ) + canViewMerchantDetail.value + ? h( + 'span', + { + style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;', + onClick: (event: MouseEvent) => { + event.stopPropagation() + handleNameClick(row) + } + }, + row.name + ) + : row.name }, { label: '支付方式', @@ -322,7 +359,6 @@ // 列表加载 const loadMerchants = async () => { - if (!canManage.value) return loading.value = true try { const params: PaymentMerchantQueryParams = { @@ -458,7 +494,7 @@ } const showCreateDrawer = () => { - if (!canManage.value) return + if (!canCreateMerchant.value) return Object.assign(form, initialFormState()) credentialTemplateKeys.value = [] formDrawerVisible.value = true @@ -468,7 +504,7 @@ } const startCredentialReplacement = () => { - if (!canManage.value) return + if (!canEditMerchant.value) return form.credentials = [] if (credentialTemplateKeys.value.length > 0) { form.credentials = credentialTemplateKeys.value.map((key) => ({ @@ -490,6 +526,33 @@ form.credentials.splice(index, 1) } + const refundCredentialValue = (key: string): string => { + return form.credentials.find((entry) => entry.key === key)?.value || '' + } + + const setRefundCredentialValue = (key: string, value: string) => { + const index = form.credentials.findIndex((entry) => entry.key === key) + const trimmed = value.trim() + if (index >= 0) { + if (trimmed) { + form.credentials[index].value = trimmed + } else { + form.credentials.splice(index, 1) + } + return + } + if (trimmed) { + form.credentials.push({ localId: generateLocalId(), key, value: trimmed }) + } + } + + const refundCredentialIncomplete = computed(() => { + if (form.provider_type !== 'wechat_v2') return false + const cert = refundCredentialValue('wx_client_cert_content') + const key = refundCredentialValue('wx_client_key_content') + return Boolean(cert) !== Boolean(key) + }) + const handlePaymentMethodChange = (value: PaymentMerchantMethod) => { form.payment_method = value const firstMatch = PAYMENT_PROVIDER_OPTIONS.find((option) => option.paymentMethod === value) @@ -499,7 +562,7 @@ } const handleSubmit = async () => { - if (!canManage.value) return + if (formMode.value === 'create' ? !canCreateMerchant.value : !canEditMerchant.value) return if (!formRef.value) return try { await formRef.value.validate() @@ -532,6 +595,9 @@ }) ElMessage.success('支付凭证更新成功') } + if (refundCredentialIncomplete.value) { + ElMessage.warning('该商户退款凭证不完整,不可用于原路退款(不影响支付/查单/回调)') + } formDrawerVisible.value = false clearSensitiveForm() await loadMerchants() @@ -565,7 +631,7 @@ const tableRef = ref() const openEditDrawer = async (id: number) => { - if (!canManage.value) return + if (!canEditMerchant.value) return try { const res = await PaymentMerchantPoolsService.getPaymentMerchantById(id) if (res.code !== 0) return @@ -589,7 +655,7 @@ } const toggleEnabled = async (merchant: PaymentMerchant) => { - if (!canManage.value) return + if (!canToggleMerchant.value) return const target = !merchant.enabled const action = target ? '启用' : '停用' try { @@ -613,7 +679,7 @@ } const confirmDeleteMerchant = async (merchant: PaymentMerchant) => { - if (!canManage.value) return + if (!canDeleteMerchant.value) return try { await ElMessageBox.confirm( `确认删除支付商户 “${merchant.name}”?该操作不可恢复。`, @@ -642,26 +708,24 @@ label: '编辑', type: 'primary' as const, handler: () => openEditDrawer(row.id), - permission: canManage.value ? '' : 'hidden' + permission: canEditMerchant.value ? '' : 'hidden' }, { label: row.enabled ? '停用' : '启用', type: 'primary' as const, handler: () => toggleEnabled(row), - permission: canManage.value ? '' : 'hidden' + permission: canToggleMerchant.value ? '' : 'hidden' }, { label: '删除', type: 'danger' as const, handler: () => confirmDeleteMerchant(row), - permission: canManage.value ? '' : 'hidden' + permission: canDeleteMerchant.value ? '' : 'hidden' } ] onMounted(() => { - if (canManage.value) { - loadMerchants() - } + loadMerchants() }) onUnmounted(() => { @@ -700,6 +764,13 @@ word-break: break-all; } + .refund-credential-fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 8px; + } + .field-error { margin-top: 8px; font-size: 12px; diff --git a/src/views/settings/payment-merchant-pools/components/PoolManagement.vue b/src/views/settings/payment-merchant-pools/components/PoolManagement.vue index c72b11a..0f7723d 100644 --- a/src/views/settings/payment-merchant-pools/components/PoolManagement.vue +++ b/src/views/settings/payment-merchant-pools/components/PoolManagement.vue @@ -15,7 +15,7 @@ @refresh="loadPools" > @@ -31,7 +31,7 @@ :total="pagination.total" :marginTop="10" :actions="getActions" - :actionsWidth="220" + :actionsWidth="120" @size-change="handleSizeChange" @current-change="handleCurrentChange" > @@ -195,7 +195,7 @@ 保存 @@ -214,7 +214,8 @@ import type { FormInstance, FormRules } from 'element-plus' import { PaymentMerchantPoolsService } from '@/api/modules' import { useCheckedColumns } from '@/composables/useCheckedColumns' - import { usePermission } from '@/composables/usePermission' + import { useAuth } from '@/composables/useAuth' + import { PAYMENT_MERCHANT_POOL_PERMISSIONS } from '@/config/constants/paymentMerchantPools' import type { SearchFormItem } from '@/types/component' import { PAYMENT_METHOD_OPTIONS, @@ -239,16 +240,15 @@ type FilterVo = string | number | undefined | null | unknown[] - const { isPlatformAccount } = usePermission() const router = useRouter() - const canManage = computed(() => isPlatformAccount.value) + const { hasAuth } = useAuth() + + const canCreatePool = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolCreate)) + const canEditPool = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolEdit)) + const canTogglePool = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolToggle)) + const canViewPoolDetail = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolDetail)) const handleNameClick = (row: PaymentMerchantPool) => { - if (!canManage.value) { - ElMessage.warning('您没有查看商户池详情的权限') - return - } - router.push({ path: `${RoutesAlias.PaymentMerchantPoolDetail}/${row.id}` }) } @@ -282,17 +282,19 @@ minWidth: 180, showOverflowTooltip: true, formatter: (row: PaymentMerchantPool) => - h( - 'span', - { - style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;', - onClick: (event: MouseEvent) => { - event.stopPropagation() - handleNameClick(row) - } - }, - row.name - ) + canViewPoolDetail.value + ? h( + 'span', + { + style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;', + onClick: (event: MouseEvent) => { + event.stopPropagation() + handleNameClick(row) + } + }, + row.name + ) + : row.name }, { label: '支付方式', @@ -357,7 +359,6 @@ } const loadPools = async () => { - if (!canManage.value) return loading.value = true try { const params = { @@ -674,7 +675,7 @@ } const showCreateDrawer = async () => { - if (!canManage.value) return + if (!canCreatePool.value) return Object.assign(form, initialFormState()) formDrawerVisible.value = true formMode.value = 'create' @@ -682,7 +683,7 @@ } const openEditDrawer = async (pool: PaymentMerchantPool) => { - if (!canManage.value) return + if (!canEditPool.value) return await loadAvailableMerchants(pool.payment_method) Object.assign(form, initialFormState(), { id: pool.id, @@ -711,7 +712,7 @@ } const handleSubmit = async () => { - if (!canManage.value) return + if (formMode.value === 'create' ? !canCreatePool.value : !canEditPool.value) return if (!formRef.value) return try { await formRef.value.validate() @@ -741,7 +742,7 @@ const tableRef = ref() const toggleEnabled = async (pool: PaymentMerchantPool) => { - if (!canManage.value) return + if (!canTogglePool.value) return const target = !pool.enabled const action = target ? '启用' : '停用' try { @@ -769,20 +770,18 @@ label: '编辑', type: 'primary' as const, handler: () => openEditDrawer(row), - permission: canManage.value ? '' : 'hidden' + permission: canEditPool.value ? '' : 'hidden' }, { label: row.enabled ? '停用' : '启用', type: 'primary' as const, handler: () => toggleEnabled(row), - permission: canManage.value ? '' : 'hidden' + permission: canTogglePool.value ? '' : 'hidden' } ] onMounted(() => { - if (canManage.value) { - loadPools() - } + loadPools() }) onUnmounted(() => { diff --git a/src/views/settings/payment-merchant-pools/components/WechatAuthorizationManagement.vue b/src/views/settings/payment-merchant-pools/components/WechatAuthorizationManagement.vue index e1256bd..8406be0 100644 --- a/src/views/settings/payment-merchant-pools/components/WechatAuthorizationManagement.vue +++ b/src/views/settings/payment-merchant-pools/components/WechatAuthorizationManagement.vue @@ -1,6 +1,6 @@