Compare commits

..

103 Commits

Author SHA1 Message Date
luo
ac541b17af fix: some
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m35s
2026-09-17 18:43:01 +08:00
luo
68d8f769d1 fix: some
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled
2026-09-17 18:37:20 +08:00
luo
068d0c4ac2 fix: git branch
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 8m48s
2026-09-17 15:46:33 +08:00
luo
9a3a764120 fix: git branch
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 35m56s
2026-09-17 14:35:31 +08:00
luo
cea805f5d8 fix: git branch 2026-09-17 14:30:53 +08:00
luo
76d4e4ec02 fix: some 2026-09-17 12:16:20 +08:00
luo
2308d82d0f fix: 代理 2026-09-12 11:27:50 +08:00
luo
d3d257cdf8 feat: 支付商户 2026-09-10 16:50:06 +08:00
luo
ef966171b4 feat: 套餐分层级 2026-09-08 16:39:29 +08:00
luo
65cc63674e fix: 格式化时间
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m44s
2026-09-08 14:33:59 +08:00
luo
01cddd7eda fix:bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m46s
2026-09-08 10:59:38 +08:00
luo
a7b006076a fix: 字段位置交换
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 9m55s
2026-09-04 17:08:35 +08:00
luo
c36c59e268 fix: 推送到main
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 23m29s
2026-08-31 17:27:08 +08:00
luo
e56951d4b7 fix: 资产信息 绑定卡运营商显示逻辑 2026-08-31 17:10:27 +08:00
luo
b5c58d695f fix: ui 2026-08-24 16:44:08 +08:00
luo
6f6ac83b71 fix: ui 2026-08-24 16:37:21 +08:00
luo
48e4adf3ff fix: ui 2026-08-24 16:33:20 +08:00
luo
d335984c20 fix: 店铺用户名 2026-08-24 16:10:24 +08:00
luo
89e6d19d1f fix: other 2026-08-24 10:18:09 +08:00
luo
889ff3bc28 Merge branch 'develop' 2026-08-21 17:23:16 +08:00
luo
d8191f03a0 fix: 资产信息ui
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 24m28s
2026-08-21 16:56:34 +08:00
luo
4d023e7666 fix: update some files
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m35s
2026-08-19 18:14:20 +08:00
luo
fe357f56d9 fix: update some files
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 8m2s
2026-08-19 17:38:09 +08:00
luo
2cb961fd1b fix: update some files
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m20s
2026-08-19 15:53:55 +08:00
luo
4e3ea6c6d6 fix: update some files
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m7s
2026-08-19 14:18:55 +08:00
luo
2d6948010b fix: update some files
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 10m55s
2026-08-18 18:05:40 +08:00
luo
95abcb97ef fix: update some files
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled
2026-08-18 17:59:31 +08:00
luo
d9d07422a9 fix: update some files
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m44s
2026-08-18 17:36:41 +08:00
luo
93f67967c5 fix: formate code and test money
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m8s
2026-08-18 10:54:01 +08:00
luo
857d565f33 fix: 我的佣金
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m35s
2026-08-17 17:29:33 +08:00
luo
9a982e0446 fix: 我的佣金
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m0s
2026-08-15 11:03:53 +08:00
luo
737ade3a5e fix: router, 佣金
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m20s
2026-08-14 17:22:47 +08:00
luo
0d2a982f69 fix: ui
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
2026-08-13 17:29:23 +08:00
luo
3dd17e9d7b fix: ui
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled
2026-08-13 17:27:58 +08:00
luo
ca748999ec fix: 提现限制
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m39s
2026-08-13 16:51:58 +08:00
luo
e86f61f3f6 fix: 提现限制
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m39s
2026-08-13 15:21:50 +08:00
luo
10ee87bf49 fix: 提现限制
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m32s
2026-08-13 14:43:42 +08:00
luo
32635da58c fix: 提现限制
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled
2026-08-13 14:39:22 +08:00
luo
8627d203cf fix: 提现限制
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m12s
2026-08-13 14:16:12 +08:00
luo
41f3b61b62 fix: 优化外部交互弹窗
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m15s
2026-08-12 09:34:36 +08:00
luo
33485b137f fix: 优化审计
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m42s
2026-08-11 16:47:58 +08:00
luo
9599a4d52b fix: 代理系列授权和操作日志去掉
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m58s
2026-08-10 17:27:15 +08:00
luo
8650b490d7 fix: 扫码
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m24s
2026-08-10 15:57:09 +08:00
luo
373ad4c2a5 fix: 审计事件详情 2026-08-10 14:44:59 +08:00
luo
9f7f619083 feat: 审计链路
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 18s
2026-08-07 18:38:28 +08:00
luo
b8b2854aa6 feat: 审计链路
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 59s
2026-08-07 18:33:37 +08:00
luo
fa3d7088f9 fix: 店铺编辑
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m29s
2026-08-05 17:58:44 +08:00
luo
34455a9f18 fix: 退款申请
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m19s
2026-08-05 16:36:08 +08:00
luo
db9aaa7920 fix: 权限套餐列表卡片
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m38s
2026-08-05 16:12:20 +08:00
luo
1e740b1526 fix: 权限套餐列表卡片
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m44s
2026-08-05 15:55:56 +08:00
luo
81d52892f5 fix: 店铺列表代理不显示平台业务员
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m33s
2026-08-04 16:44:11 +08:00
luo
06f39815db fix: agent-fund-overview
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m28s
2026-08-04 15:12:42 +08:00
luo
83f1c88c65 fix: ui
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m36s
2026-07-31 09:38:41 +08:00
luo
055fed0cf1 fix: 修改提示
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m9s
2026-07-30 16:05:48 +08:00
luo
93ed3fbd63 fix: 代理充值扫码
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m15s
2026-07-30 09:51:04 +08:00
luo
17d2eeebc5 fix: 回调配置, 调整信用位置, 套餐
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m55s
2026-07-29 18:20:42 +08:00
luo
4394ae0f78 feat: small fix
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m12s
2026-07-29 11:36:34 +08:00
luo
827de02f2b fix: 通知
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 9m23s
2026-07-28 18:52:15 +08:00
luo
831f03c3a7 fix: 通知
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m20s
2026-07-28 18:40:54 +08:00
luo
ee266c51cc fix:通知和设备分配/回收
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m49s
2026-07-28 17:23:23 +08:00
luo
b51a3f5537 fix: 去掉通知的权限
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m51s
2026-07-28 16:08:34 +08:00
luo
261f5f2853 fix: id兼容, 通知入口
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m0s
2026-07-28 15:19:29 +08:00
luo
0e8a11430a fix: 修复角色id, 企微审批场景
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 10m25s
2026-07-28 14:27:55 +08:00
luo
2706c52a47 feat: 7月迭代
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m21s
2026-07-28 14:00:22 +08:00
luo
f0a2b84e53 feat: 产品迭代7月份
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m51s
2026-07-27 16:30:29 +08:00
luo
cc6fc9243e feat: 注意事项
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m43s
2026-07-25 14:37:27 +08:00
luo
dd6bceeeb7 feat: 系统配置
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled
2026-07-25 14:13:49 +08:00
luo
ee508c3e1e feat: 完善15- 角色默认信用与店铺实际额度管理
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m8s
2026-07-25 11:55:44 +08:00
luo
9289a6e940 feat: 完善接口19-顶部通知铃铛与站内通知中心
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m5s
2026-07-25 10:20:53 +08:00
luo
eb13763020 fix: 完善套餐分配生效条件选择
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m30s
2026-07-24 17:42:36 +08:00
luo
d1ff4d5f6c feat: 顶部通知铃铛与站内通知中心
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m7s
2026-07-24 17:12:29 +08:00
luo
4c6d871c35 feat: 退款企微审批详情与业务处理状态
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m2s
2026-07-24 16:30:58 +08:00
luo
9a84cd0d31 feat: 批量订购上传支付进度与失败明细
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 8m34s
2026-07-24 15:45:55 +08:00
luo
e043fd86d1 feat: 角色默认信用与店铺实际额度管理
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m5s
2026-07-24 12:03:04 +08:00
luo
a0c44ff538 feat: 七月迭代公共状态与异步任务交互
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m59s
2026-07-24 11:48:02 +08:00
luo
15122e225a feat: 预计最终到期时间展示新增具体
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m49s
2026-07-24 09:31:11 +08:00
luo
127fdb560b fix: 样式优化套餐分配生效条件选择
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m41s
2026-07-23 17:59:07 +08:00
luo
5e15127a89 feat: 接入新参数:套餐分配生效条件选择,资产详情前代后代换货标识
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m0s
2026-07-23 17:06:05 +08:00
luo
d7c2c146fe feat: 角色默认信用与店铺实际额度管理
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m12s
2026-07-23 14:10:48 +08:00
luo
3a819e86c9 fix: 套餐用量显示
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m52s
2026-07-23 12:02:34 +08:00
luo
e9c0e17d6a fix: 套餐用量显示
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled
2026-07-23 11:52:25 +08:00
luo
d5fd8ac564 feat: 退款充值换货列表提交人与审批摘要
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m20s
2026-07-22 18:37:41 +08:00
luo
7d3352038a feat: 换货新旧资产展示与独立搜索
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m34s
2026-07-22 16:57:17 +08:00
luo
8faa2ea2ef feat: 预计最终到期时间展示
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 8m8s
2026-07-22 16:31:01 +08:00
luo
8de4339505 feat: 卡和设备实名状态筛选
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m40s
2026-07-22 15:44:48 +08:00
luo
68aa03b538 feat: 套餐分配生效条件选择
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 9m23s
2026-07-22 14:21:15 +08:00
luo
fbfdd01eec feat: 换货退款拦截错误展示
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m41s
2026-07-22 10:14:13 +08:00
luo
5079f97326 feat: 店铺列表新增条件
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m47s
2026-07-21 18:20:35 +08:00
luo
830476d49d H5实名购买顺序与后台批量配置
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m48s
2026-07-21 16:13:40 +08:00
luo
db7b3562da fix:将文案虚流量改成流量
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m23s
2026-07-21 15:24:02 +08:00
luo
f52970ae6c fix: 资产信息流量显示
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m22s
2026-07-21 14:51:29 +08:00
luo
47a2d88c9d 复机实名校验提示适配
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m41s
2026-07-21 14:30:30 +08:00
luo
6febc66eca feat: 资产同步状态与同步轨迹入口-新
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m35s
2026-07-21 11:50:47 +08:00
luo
45d255cabf feat: 资产详情前代后代换货标识
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m17s
2026-07-21 11:13:20 +08:00
luo
eddfd157ad feat: 资产同步状态与同步轨迹入口
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m43s
2026-07-21 09:57:14 +08:00
09225dd8bc 更新 Dockerfile
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m9s
2026-07-20 15:33:35 +08:00
563804f67f 更新 docker-compose.prod.yml
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m29s
2026-07-20 14:46:19 +08:00
luo
e95dc8e4f5 feat:代理现金余额不足100元展示、店铺业务员选择展示与筛选
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m34s
2026-07-20 12:17:02 +08:00
luo
ca338dd345 fix: 测试环境显示develop分支
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m20s
2026-07-20 11:02:05 +08:00
luo
fac05db39a feat: 换货新资产归属继承提示 2026-07-20 10:50:23 +08:00
luo
2c8524485b fix: 资产信息字段显示隐藏根据角色
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m24s
2026-07-13 16:40:35 +08:00
luo
4c0207c6e6 fix: 将微信配置改成支付配置
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m41s
2026-07-10 18:09:19 +08:00
luo
5b8d6610ab fix: 将套餐系列的套餐加滚动条 2026-07-09 17:38:58 +08:00
474 changed files with 49117 additions and 5567 deletions

View File

@@ -3,9 +3,12 @@ name: 构建并部署前端到测试环境
on:
push:
branches:
- main
- iteration/august
- dev
- test
# 如果不再需要 main 分支触发构建,可以把下面这行删掉;
# 如果只是想保留构建(不部署),可以留着,反正部署条件已经只认 iteration/august
- main
env:
REGISTRY: registry.boss160.cn
@@ -27,7 +30,7 @@ jobs:
- name: 设置镜像标签
id: tag
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
if [ "${{ github.ref }}" = "refs/heads/iteration/august" ]; then
echo "tag=latest" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
echo "tag=dev" >> $GITHUB_OUTPUT
@@ -51,8 +54,8 @@ jobs:
docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
docker push ${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: 部署到本地(仅 main 分支)
if: github.ref == 'refs/heads/main'
- name: 部署到本地(仅 iteration/august 分支)
if: github.ref == 'refs/heads/iteration/august'
run: |
# 确保部署目录存在
mkdir -p ${{ env.DEPLOY_DIR }}

View File

@@ -30,7 +30,7 @@ RUN pnpm run nd
# ================================
# 阶段 2: 运行阶段
# ================================
FROM --platform=linux/amd64 nginx:alpine
FROM --platform=linux/amd64 nginx:1.31.2-alpine
# 使用阿里云镜像源加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

View File

@@ -1,5 +1,4 @@
version: '3.8'
services:
web:
image: registry.boss160.cn/junhong/cmp-admin-web:latest
@@ -9,12 +8,19 @@ services:
- '3001:80'
networks:
- junhong-network
# === 以下是新增的修复内容 ===
tmpfs:
- /run:rw
- /tmp:rw
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:80/health']
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
logging:
driver: 'json-file'
options:
@@ -23,4 +29,4 @@ services:
networks:
junhong-network:
driver: bridge
driver: bridge

View File

@@ -0,0 +1,35 @@
## Context
临期结果由后端根据资产当前套餐及排队套餐计算。管理端需要在独立列表、普通资产列表、代理首页和通知中心保持相同的临期语义。本变更只覆盖管理端C 端资产和续费能力不纳入实现范围。
## Goals / Non-Goals
- Goals: 统一消费 `estimated_final_expires_at``days_until_final_expiry``expiry_level``expiry_level_name``can_renew`;提供临期查询、排序、高亮、统计、通知跳转和管理端续费入口。
- Goals: 由后端负责临期边界、预计到期结果和通知触发,前端只负责展示和路由。
- Non-Goals: 不计算套餐接续到期时间,不修改历史订单或已购套餐,不实现 C 端入口,不展示企微临期消息。
## Decisions
- Decision: 临期独立列表使用 `GET /api/admin/expiring-assets`,查询参数保持文档约定的 `asset_type``keyword``shop_id``package_id``days_min``days_max``expires_from``expires_to``page``size`
- Decision: 临期接口负责跨分页排序,返回结果按 0-3 天优先、其余按 `estimated_final_expires_at` 升序排列;前端只保留接口顺序,不改变普通资产列表原始接口排序。
- Decision: 颜色只按剩余天数映射8-15 天粉红、4-7 天紫色、0-3 天红色。已过期和不可预计记录由接口排除,前端不把它们补入临期列表。
- Decision: 续费入口由 `can_renew` 控制,具体续费动作复用现有管理端续费/充值路由和接口,不新增 C 端逻辑。
- Decision: 通知中心继续复用现有通知接口和目标跳转协议;临期通知由后端按 15 天、7 天、3 天生成,前端按通知目标跳转到临期列表或对应资产详情。
- Alternatives considered: 前端从普通资产列表筛选临期记录。Rejected because it无法保证全量、统一排序和后端预计最终到期结果的一致性。
## Risks / Trade-offs
- 风险代理首页当前概览接口未必已有临期计数字段。Mitigation在现有首页/概览响应中增加 `expiring_card_count``expiring_device_count`,避免前端分页统计。
- 风险不同资产列表的字段结构不完全一致。Mitigation在各自 API 类型中复用同一组临期字段语义,并通过统一展示格式处理空值和等级。
- 风险续费入口的现有路由可能依赖资产类型。Mitigation`asset_type``asset_id` 选择对应管理端续费入口,按钮仅在 `can_renew=true` 时展示。
## Migration Plan
1. 先扩展管理端 API 类型和服务,确认列表、首页统计和通知目标字段。
2. 上线临期列表及普通列表高亮,再接入首页统计和续费入口。
3. 最后验证通知中心过滤、已读和跳转,不修改 C 端功能。
## Open Questions
- 代理首页现有概览接口的具体路径和响应字段名称,需要以后端接口实现为准确认。
- 管理端卡/设备续费入口的现有路由是否统一,需实现时复用当前权限和路由定义。

View File

@@ -0,0 +1,35 @@
# Change: 增加管理端临期资产高亮、通知与续费入口
## Why
管理端目前缺少统一的临期资产查询入口,运营人员需要在普通资产列表中自行识别临近到期资产,且无法从临期记录快速进入续费操作。需要基于后端统一计算的预计最终到期结果,在管理端集中展示临期资产、标记普通列表并接入站内通知。
## What Changes
- 新增管理端临期资产列表,支持资产类型、关键字、店铺、套餐、剩余天数和预计到期时间筛选。
- 临期列表按 0-3 天固定置顶,其余按预计最终到期时间升序;已过期和不可预计资产不进入列表。
- 在卡列表、设备列表和资产相关列表中增加临期颜色高亮,但不改变普通列表原有排序。
- 在代理首页增加临期卡数量和临期设备数量展示,并可跳转临期列表。
- 在临期列表提供后端返回 `can_renew` 控制的续费入口,复用现有管理端续费/充值流程。
- 接入通知中心中的 15 天、7 天、3 天临期提醒及通知跳转;管理端不展示企微临期消息。
- 所有页面直接使用后端返回的预计最终到期字段和临期等级,不在前端重新计算最终到期时间。
## Impact
- Affected specs:
- `admin-expiring-assets`
- Affected code:
- `src/api/modules``src/types/api` 中的临期资产接口及类型
- 管理端临期资产列表页面和路由
- 网卡列表、设备列表及资产信息展示组件
- 代理首页统计组件
- 通知中心跳转处理
- API contracts:
- `GET /api/admin/expiring-assets`
- 现有代理首页/概览接口增加临期卡、设备数量字段,或提供等价的管理端统计响应
- 复用现有通知查询、已读和目标跳转接口
- Out of scope:
- C 端资产页临期展示
- C 端续费按钮和 C 端续费流程
- 企微临期消息展示或企微通知改造
- 前端根据套餐明细自行推导预计最终到期时间

View File

@@ -0,0 +1,111 @@
## ADDED Requirements
### Requirement: Admin Expiring Asset Query
The admin frontend SHALL provide an expiring asset list backed by `GET /api/admin/expiring-assets`. The query SHALL support `asset_type`, `keyword`, `shop_id`, `package_id`, `days_min`, `days_max`, `expires_from`, `expires_to`, `page`, and `size`.
#### Scenario: Filter expiring assets
- **WHEN** 管理端用户打开临期资产列表并提交筛选条件
- **THEN** 前端 MUST send the documented query parameters to `GET /api/admin/expiring-assets`
- **AND** 页面 MUST display the paginated response items
#### Scenario: Exclude unavailable assets
- **WHEN** 接口返回临期列表
- **THEN** 页面 MUST not add expired assets or assets without an estimable final expiry to the list on the client side
### Requirement: Expiring Asset Fields
Each expiring asset item SHALL preserve and display the backend fields `asset_type`, `asset_id`, `identifier`, `shop_name`, `package_name`, `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_level`, `expiry_level_name`, and `can_renew`. The frontend MUST use backend-provided expiry results and MUST NOT calculate the estimated final expiry from package details.
#### Scenario: Display an estimable asset
- **WHEN** 临期接口返回资产及 `estimated_final_expires_at`
- **THEN** 页面 MUST display the asset identifier, shop, package, estimated final expiry, remaining days, and backend expiry level name
#### Scenario: Preserve backend result
- **WHEN** 临期接口返回 `estimated_final_expires_at``days_until_final_expiry`
- **THEN** 前端 MUST display the returned values
- **AND** 前端 MUST NOT derive or replace them from current or queued package data
### Requirement: Expiring List Ordering And Highlighting
The expiring asset API and standalone admin list SHALL return and preserve records with 0-3 remaining days first, then order the remaining records by `estimated_final_expires_at` ascending. The list SHALL apply pink highlighting to 8-15 days, purple highlighting to 4-7 days, and red highlighting to 0-3 days.
#### Scenario: Order critical assets first
- **GIVEN** the response contains assets in multiple expiry ranges
- **WHEN** 页面渲染临期列表
- **THEN** the API response MUST place assets with 0-3 remaining days before assets with more remaining days
- **AND** the API response MUST order records in the remaining ranges by estimated final expiry ascending
- **AND** the frontend MUST preserve that cross-page ordering
#### Scenario: Highlight by remaining days
- **WHEN** 页面渲染临期资产
- **THEN** 8-15 days MUST use the pink visual treatment
- **AND** 4-7 days MUST use the purple visual treatment
- **AND** 0-3 days MUST use the red visual treatment
### Requirement: Ordinary Admin Asset List Highlighting
The admin card and device lists SHALL apply the same expiry color treatment to returned expiring asset fields without changing their existing server or client sort order.
#### Scenario: Highlight ordinary asset rows
- **WHEN** 卡列表或设备列表返回临期字段
- **THEN** 页面 MUST apply the matching expiry color treatment
- **AND** 页面 MUST preserve the list's existing ordering
### Requirement: Admin Expiring Asset Renewal Entry
The admin expiring asset list SHALL display a renewal action only when the backend item has `can_renew=true`. The action SHALL navigate to the existing management-side renewal or recharge flow for the asset type.
#### Scenario: Renewable asset
- **GIVEN** 临期资产项返回 `can_renew=true`
- **WHEN** 用户查看临期列表
- **THEN** 页面 MUST provide the management-side renewal entry
- **AND** the entry MUST target the corresponding asset and preserve asset type context
#### Scenario: Non-renewable asset
- **GIVEN** 临期资产项返回 `can_renew=false`
- **WHEN** 用户查看临期列表
- **THEN** 页面 MUST NOT display an enabled renewal action
### Requirement: Agent Dashboard Expiry Counts
The agent-facing admin home SHALL display the number of expiring cards and expiring devices using backend-provided summary fields and SHALL provide navigation to the standalone expiring asset list.
#### Scenario: Display expiry counts
- **WHEN** 代理首页概览数据返回临期卡和设备数量
- **THEN** 页面 MUST display the expiring card count and expiring device count
- **AND** 页面 MUST not calculate the counts from a paginated asset list
### Requirement: Admin Expiry Notifications
The notification center SHALL display backend-generated admin expiry reminders for 15 days, 7 days, and 3 days through the existing notification API and target navigation. The admin frontend SHALL not display WeCom expiry messages.
#### Scenario: Display expiry reminder
- **WHEN** 通知接口返回 15 天、7 天或 3 天的临期提醒
- **THEN** 通知中心 MUST classify it as a 临期提醒
- **AND** 用户点击通知后 MUST navigate to the returned management-side target
#### Scenario: Exclude WeCom expiry message
- **WHEN** 管理端加载通知中心
- **THEN** 页面 MUST not add or render a separate WeCom expiry message channel
### Requirement: Admin-Only Scope
This capability SHALL be limited to the management frontend. It SHALL NOT add or modify C-end asset expiry display, C-end renewal buttons, C-end renewal APIs, or C-end notification behavior.
#### Scenario: Keep C-end unchanged
- **WHEN** this change is implemented
- **THEN** C-end asset pages and renewal flows MUST remain outside the change scope

View File

@@ -0,0 +1,35 @@
## 1. API Contract
- [x] 1.1 增加 `GET /api/admin/expiring-assets` 的查询参数、分页响应和临期资产项类型。
- [x] 1.2 为卡列表、设备列表、资产信息和首页概览类型补充统一临期字段及临期计数字段。
- [x] 1.3 增加临期资产 API 服务方法,并确认续费入口复用现有管理端服务和权限。
## 2. Expiring Asset List
- [x] 2.1 增加或接入 `/operations/expiring-assets` 管理端路由和页面。
- [x] 2.2 实现资产类型、关键字、店铺、套餐、剩余天数和预计到期时间筛选。
- [x] 2.3 展示资产、店铺、当前套餐、预计最终到期、剩余天数和后端临期等级名称。
- [x] 2.4 实现 0-3 天置顶、其余按预计到期时间升序,并排除已过期和不可预计记录。
- [x] 2.5 按 8-15 天粉红、4-7 天紫色、0-3 天红色实现行或到期字段高亮。
- [x] 2.6 根据 `can_renew` 展示管理端续费入口,并跳转现有续费/充值流程。
## 3. Existing Admin Pages
- [x] 3.1 在网卡普通列表增加临期字段展示和颜色高亮,不改变原排序。
- [x] 3.2 在设备普通列表增加临期字段展示和颜色高亮,不改变原排序。
- [x] 3.3 在资产信息详情复用同一临期展示规则,禁止前端重新计算预计最终到期时间。
- [x] 3.4 在代理首页展示临期卡数量和临期设备数量,并支持跳转临期列表。
## 4. Notifications
- [x] 4.1 在通知中心复用临期分类,展示后端生成的 15 天、7 天、3 天临期提醒。
- [x] 4.2 使用现有通知目标跳转到临期列表或对应管理端资产详情。
- [x] 4.3 确认管理端不展示企微临期消息,不新增 C 端通知入口。
## 5. Verification
- [x] 5.1 验证临期字段直接使用后端值,前端不根据套餐明细推导最终到期时间。
- [x] 5.2 验证 0-3 天置顶、日期升序、颜色映射和普通列表原排序保持不变。
- [x] 5.3 验证 `can_renew=false` 不展示续费入口,且各资产类型使用正确管理端入口。
- [x] 5.4 验证通知中心临期分类、已读状态和跳转行为。
- [x] 5.5 运行类型检查、相关 ESLint/Stylelint、构建和 `openspec validate add-admin-expiring-assets-notifications --strict`

View File

@@ -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`

View File

@@ -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 与 PCH5 单列、大触控区、软键盘友好、地址使用级联选择器PC 使用居中卡片或两列布局且字段一致。密码输入 MUST 掩码并展示强度提示;页面 MUST NOT 展示除手机号外的完整个人敏感信息。
#### Scenario: H5 与 PC 同一表单
- **GIVEN** 用户分别在手机与 PC 打开注册页
- **WHEN** 其填写注册信息
- **THEN** 两端的字段集合 MUST 一致
- **AND** 布局 MUST 按端侧自适应
#### Scenario: 密码强度与掩码
- **GIVEN** 用户在密码输入框输入
- **WHEN** 其提交表单
- **THEN** 输入 MUST 为掩码展示
- **AND** MUST 展示密码强度提示

View File

@@ -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** 展示驳回原因、异常标记与尝试记录

View File

@@ -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 展示空态(`-` 或「未分配」)

View File

@@ -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` 的字段查询接口

View File

@@ -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`

View File

@@ -0,0 +1,33 @@
# Change: 新增资产批量实名认证策略配置
## Why
运营需要一次性为多个卡或设备配置实名认证顺序。现有后台仅支持单资产设置实名认证策略,无法满足列表多选后的批量配置需求。
## What Changes
- 在后台卡列表和设备列表分别增加“批量修改实名顺序”入口,基于当前勾选资产执行配置。
- 批量配置弹框展示已选资产数量,并提供“无需实名”“先实名后购买”“先购买后实名”三种互斥策略。
- 卡列表调用 `POST /api/admin/iot-cards/batch-update-realname-policy`;设备列表调用 `POST /api/admin/devices/batch-update-realname-policy`
- 批量请求传递 `asset_ids``realname_policy`;前端限制单次提交至多 500 条,并在超限时阻止提交并明确提示。
- 设备批量配置弹框提示“实际H5流程由设备策略决定”。
- 后端批量接口按全成全败处理;前端在失败时展示明确的后端业务错误,成功后刷新当前列表。
- 前端不根据资产类型、卡类型或其他字段自行覆盖或推导实名认证策略。
- 本提案不包含任何 H5 初始化、购买流程或 `effective_realname_policy` 的处理。
## Impact
- Affected specs:
- `iot-card-management`
- `device-management`
- Affected code:
- `src/api/modules/asset.ts` 或对应卡、设备 API 模块
- `src/types/api/asset.ts` 或对应卡、设备 API 类型
- `src/views/asset-management/iot-card-management/index.vue`
- `src/views/asset-management/device-list/index.vue`
- API contracts:
- `POST /api/admin/iot-cards/batch-update-realname-policy`
- `POST /api/admin/devices/batch-update-realname-policy`
- Out of scope:
- H5 初始化返回字段和购买流程
- 单资产实名认证策略设置接口 `PATCH /api/admin/assets/{identifier}/realname-mode`

View File

@@ -0,0 +1,54 @@
## ADDED Requirements
### Requirement: Device Batch Realname Policy Configuration
The device management list SHALL provide a `批量修改实名顺序` action for selected devices. The action SHALL submit the selected device IDs and exactly one realname policy to `POST /api/admin/devices/batch-update-realname-policy`.
#### Scenario: Open batch realname policy dialog for selected devices
- **GIVEN** 用户在设备列表勾选了一台或多台设备
- **WHEN** 用户点击“批量修改实名顺序”
- **THEN** 页面 MUST open a dialog that displays the selected device count
- **AND** 页面 MUST provide mutually exclusive options `无需实名``先实名后购买``先购买后实名`
- **AND** 页面 MUST display `实际H5流程由设备策略决定` 提示
#### Scenario: Submit selected device policy
- **GIVEN** 用户已选择一项实名认证策略
- **WHEN** 用户确认批量修改
- **THEN** 系统 MUST call `POST /api/admin/devices/batch-update-realname-policy`
- **AND** 请求 MUST contain the selected device IDs as `asset_ids`
- **AND** 请求 MUST contain the selected `realname_policy` as `none``before_order``after_order`
#### Scenario: Refresh devices after all-or-nothing success
- **WHEN** 设备批量实名认证策略接口成功返回
- **THEN** 页面 MUST close the dialog
- **AND** 页面 MUST refresh the current device list
#### Scenario: Show failed batch update reason
- **WHEN** 设备批量实名认证策略接口返回业务失败或请求失败
- **THEN** 页面 MUST display the backend business reason when provided
- **AND** 页面 MUST NOT refresh the list as a partial-success result
### Requirement: Device Batch Realname Policy Limit
The device management list MUST limit each realname policy batch submission to 500 selected devices.
#### Scenario: Prevent device batch submission over the limit
- **GIVEN** 用户在设备列表选择超过 500 台设备
- **WHEN** 用户尝试确认批量修改实名认证策略
- **THEN** 页面 MUST prevent the request from being sent
- **AND** 页面 MUST display an explicit maximum-500-items error
### Requirement: Device Batch Policy Is User Selected
The device management list MUST submit the policy explicitly selected by the user and MUST NOT infer, override, or transform it from asset type, card type, or other asset fields.
#### Scenario: Preserve selected device policy value
- **GIVEN** 用户在批量配置弹框选择任一实名认证策略
- **WHEN** 用户确认提交
- **THEN** 请求中的 `realname_policy` MUST equal the selected option

View File

@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: IoT Card Batch Realname Policy Configuration
The IoT card management list SHALL provide a `批量修改实名顺序` action for selected cards. The action SHALL submit the selected card IDs and exactly one realname policy to `POST /api/admin/iot-cards/batch-update-realname-policy`.
#### Scenario: Open batch realname policy dialog for selected cards
- **GIVEN** 用户在卡列表勾选了一张或多张卡
- **WHEN** 用户点击“批量修改实名顺序”
- **THEN** 页面 MUST open a dialog that displays the selected card count
- **AND** 页面 MUST provide mutually exclusive options `无需实名``先实名后购买``先购买后实名`
#### Scenario: Submit selected card policy
- **GIVEN** 用户已选择一项实名认证策略
- **WHEN** 用户确认批量修改
- **THEN** 系统 MUST call `POST /api/admin/iot-cards/batch-update-realname-policy`
- **AND** 请求 MUST contain the selected card IDs as `asset_ids`
- **AND** 请求 MUST contain the selected `realname_policy` as `none``before_order``after_order`
#### Scenario: Refresh cards after all-or-nothing success
- **WHEN** 卡批量实名认证策略接口成功返回
- **THEN** 页面 MUST close the dialog
- **AND** 页面 MUST refresh the current card list
#### Scenario: Show failed batch update reason
- **WHEN** 卡批量实名认证策略接口返回业务失败或请求失败
- **THEN** 页面 MUST display the backend business reason when provided
- **AND** 页面 MUST NOT refresh the list as a partial-success result
### Requirement: IoT Card Batch Realname Policy Limit
The IoT card management list MUST limit each realname policy batch submission to 500 selected cards.
#### Scenario: Prevent card batch submission over the limit
- **GIVEN** 用户在卡列表选择超过 500 张卡
- **WHEN** 用户尝试确认批量修改实名认证策略
- **THEN** 页面 MUST prevent the request from being sent
- **AND** 页面 MUST display an explicit maximum-500-items error
### Requirement: IoT Card Batch Policy Is User Selected
The IoT card management list MUST submit the policy explicitly selected by the user and MUST NOT infer, override, or transform it from asset type, card type, or other asset fields.
#### Scenario: Preserve selected card policy value
- **GIVEN** 用户在批量配置弹框选择任一实名认证策略
- **WHEN** 用户确认提交
- **THEN** 请求中的 `realname_policy` MUST equal the selected option

View File

@@ -0,0 +1,28 @@
## 1. API Contract
- [x] 1.1 定义批量实名认证策略请求类型,包含 `asset_ids:int64[]``realname_policy:none|before_order|after_order`
- [x] 1.2 接入卡批量更新接口 `POST /api/admin/iot-cards/batch-update-realname-policy`
- [x] 1.3 接入设备批量更新接口 `POST /api/admin/devices/batch-update-realname-policy`
## 2. IoT Card Batch Configuration
- [x] 2.1 在卡列表多选操作区增加“批量修改实名顺序”入口。
- [x] 2.2 弹框展示已选卡数量并提供三种互斥实名认证策略。
- [x] 2.3 超过 500 张卡时阻止提交并显示明确提示。
- [x] 2.4 成功后关闭弹框并刷新卡列表;失败时保留选择并展示后端业务原因。
## 3. Device Batch Configuration
- [x] 3.1 在设备列表多选操作区增加“批量修改实名顺序”入口。
- [x] 3.2 弹框展示已选设备数量并提供三种互斥实名认证策略。
- [x] 3.3 展示“实际H5流程由设备策略决定”提示。
- [x] 3.4 超过 500 台设备时阻止提交并显示明确提示。
- [x] 3.5 成功后关闭弹框并刷新设备列表;失败时保留选择并展示后端业务原因。
## 4. Policy Integrity and Verification
- [x] 4.1 前端不根据资产类型、卡类型或其他字段覆盖或推导提交策略。
- [ ] 4.2 验证卡和设备三种策略均按所选值提交,且单次 500 条以内成功。
- [ ] 4.3 验证超过 500 条、后端全成全败失败和网络失败均显示明确错误且不部分刷新列表。
- [ ] 4.4 验证设备弹框显示 H5 流程归属提示。
- [x] 4.5 运行相关前端校验,并执行 `openspec validate add-asset-batch-realname-policy --strict`

View File

@@ -0,0 +1,41 @@
## Context
运营需要在资产层查看当前主套餐及所有排队主套餐顺序接续后的预计最终到期时间。该结果依赖后端套餐队列、激活条件与计时规则,前端只负责展示接口结果。
## Goals / Non-Goals
- Goals:
- 在资产详情、IoT 卡列表和设备列表统一展示“预计套餐到期时间”。
- 区分可精确预计与待激活后才可计算的状态。
- 对临期资产提供基于后端剩余天数的视觉提示。
- Non-Goals:
- 不修改单个套餐明细的“到期时间”展示或套餐队列顺序。
- 不新增预计到期时间筛选、排序或前端日期计算。
- 不改变套餐续费、激活或到期规则。
## Decisions
- Decision: 统一使用以下五个后台响应字段:`estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``expiry_estimate_status_name``is_expiring`,不调用额外计算接口。
- Decision: `expiry_estimate_status` 仅使用 `exact``waiting_activation``none``invalid_data`。除 `exact` 外,`estimated_final_expires_at``days_until_final_expiry` 必须为 `null`;已过期的 `exact` 资产允许返回负数剩余天数。
- Decision: 仅当 `expiry_estimate_status=exact` 且存在 `estimated_final_expires_at` 时格式化展示日期;不可预计状态展示“待激活后起算”。
- Decision: `is_expiring=true` 是临期样式的唯一触发条件,`days_until_final_expiry` 只作为剩余天数展示或样式辅助信息,前端不自行判定临期阈值。
- Decision: 普通卡和设备列表保持既有服务端返回顺序与前端排序行为,不因临期字段重排。
- Alternatives considered: 前端根据当前套餐到期时间、排队套餐时长和计时基准计算最终日期。未采用,因为等待激活和后端队列规则会导致结果不准确。
## Risks / Trade-offs
- 后端缺少预计字段时无法显示最终日期 -> 对空值显示稳定占位,不以当前套餐日期替代。
- 未知 `expiry_estimate_status` 可能导致错误日期展示 -> 仅 `exact` 可显示日期,其他状态显示稳定占位或后端约定的不可预计提示。
- 列表增加时间列会占用宽度 -> 作为可配置动态列,沿用现有横向滚动与列选择能力。
## Migration Plan
1. 扩展资产详情及卡、设备列表类型以保留预计最终到期字段。
2. 将资产详情响应字段映射到页面状态,并在基础信息区域展示。
3. 在卡和设备列表增加预计套餐到期时间列及临期样式。
4. 验证无套餐、仅当前套餐、多个排队套餐和待激活后起算四类响应。
5. 如需回滚,移除新增展示字段和列表列;不涉及数据迁移。
## Open Questions
- 后端若返回未约定的状态值,前端不得按 `estimated_final_expires_at` 是否为空展示日期,应按不可预计状态处理。

View File

@@ -0,0 +1,40 @@
# Change: 新增资产预计套餐到期时间展示
## Why
当前页面只能在套餐明细中查看单个套餐的到期时间,运营无法快速了解当前主套餐与全部排队主套餐接续后的资产最终到期时间。前端也不能可靠地自行叠加套餐时长,尤其当套餐需要激活后才开始计时时。
## What Changes
- 在资产详情、IoT 卡列表和设备列表增加统一的 `预计套餐到期时间` 展示。
- 资产详情接口和资产列表响应支持统一的 5 个字段:`estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``expiry_estimate_status_name``is_expiring`
- `expiry_estimate_status` 使用 `exact``waiting_activation``none``invalid_data` 四种状态;非 `exact` 状态下日期和剩余天数字段必须为 `null`
-`expiry_estimate_status=exact` 时,展示后端返回的 `estimated_final_expires_at`
- 当套餐尚待激活等无法预计最终日期时,展示“待激活后起算”,不得伪造日期。
-`is_expiring=true` 时,按后端返回的剩余天数使用临期颜色提示;普通资产列表不得因临期状态改变既有排序。
- 当前套餐自身的到期时间继续仅在套餐明细中展示;前端不得叠加套餐时长计算预计最终到期时间。
- 本次仅覆盖后台资产详情、IoT 卡列表和设备列表,不处理 C 端资产信息接口或页面。
## Impact
- Affected specs:
- `asset-information`
- `iot-card-management`
- `device-management`
- Affected code:
- `src/types/api/asset.ts`
- `src/types/api/card.ts`
- `src/types/api/device.ts`
- `src/views/asset-management/asset-information/types.ts`
- `src/views/asset-management/asset-information/composables/useAssetInfo.ts`
- `src/views/asset-management/asset-information/components/BasicInfoCard.vue`
- `src/views/asset-management/iot-card-management/index.vue`
- `src/views/asset-management/device-list/index.vue`
- API contracts:
- `GET /api/admin/assets/resolve/{identifier}`
- `GET /api/admin/iot-cards/standalone`
- `GET /api/admin/devices`
- Dependencies:
- 后端在资产详情与资产列表响应中返回预计最终到期字段。
- Out of scope:
- `GET /api/c/v1/asset/info` 及 C 端资产信息页面

View File

@@ -0,0 +1,86 @@
## ADDED Requirements
### Requirement: Asset Estimated Final Expiry Contract
The admin asset information integration SHALL preserve the five backend fields `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_estimate_status`, `expiry_estimate_status_name`, and `is_expiring` returned by `GET /api/admin/assets/resolve/{identifier}`. `estimated_final_expires_at` SHALL be an RFC3339 string or `null`, `days_until_final_expiry` SHALL be an integer or `null`, and `expiry_estimate_status` SHALL be one of `exact`, `waiting_activation`, `none`, or `invalid_data`. This requirement applies only to the admin frontend and excludes C-end asset information.
#### Scenario: Preserve an exact final expiry estimate
- **GIVEN** 用户查询后台资产详情
- **WHEN** `GET /api/admin/assets/resolve/{identifier}` returns `expiry_estimate_status=exact`
- **THEN** 前端状态 MUST preserve `estimated_final_expires_at` as `string | null`
- **AND** 前端状态 MUST preserve `days_until_final_expiry` as `number | null`
- **AND** 前端状态 MUST preserve `is_expiring` as a boolean
- **AND** 前端状态 MUST preserve `expiry_estimate_status_name` as the backend-provided status name
#### Scenario: Preserve null fields for non-exact estimates
- **GIVEN** 用户查询后台资产详情
- **WHEN** `expiry_estimate_status` is `waiting_activation`, `none`, or `invalid_data`
- **THEN** `estimated_final_expires_at` MUST be preserved as `null`
- **AND** `days_until_final_expiry` MUST be preserved as `null`
- **AND** 前端 MUST NOT calculate either field
#### Scenario: Preserve an unavailable final expiry estimate
- **GIVEN** 用户查询后台资产详情
- **WHEN** 接口返回待激活或其他不可预计的 `expiry_estimate_status`
- **THEN** 前端状态 MUST preserve `expiry_estimate_status`
- **AND** 前端 MUST NOT derive `estimated_final_expires_at` from current-package or package-detail fields
- **AND** 前端 MUST NOT use `expiry_estimate_status_name` as a substitute for the status enum when choosing the display rule
### Requirement: Asset Estimated Final Expiry Display
The admin asset information view SHALL display one asset-level field labeled `预计套餐到期时间` for the current primary package and all queued primary packages, without replacing individual package expiry dates in package details.
#### Scenario: Display an exact final expiry date
- **GIVEN** 资产详情返回 `expiry_estimate_status=exact`
- **AND** `estimated_final_expires_at` has a value
- **WHEN** 页面渲染卡资产或设备资产基础信息
- **THEN** 页面 MUST display `预计套餐到期时间`
- **AND** 页面 MUST format and display `estimated_final_expires_at`
#### Scenario: Display activation-pending final expiry
- **GIVEN** 资产详情返回 `expiry_estimate_status=waiting_activation`
- **WHEN** 页面渲染卡资产或设备资产基础信息
- **THEN** `预计套餐到期时间` MUST display `待激活后起算`
- **AND** 页面 MUST NOT display a fabricated date
#### Scenario: Handle an unknown estimate status safely
- **GIVEN** 资产详情返回未约定的 `expiry_estimate_status`
- **WHEN** 页面渲染资产基础信息
- **THEN** 页面 MUST treat the estimate as unavailable
- **AND** 页面 MUST NOT display `estimated_final_expires_at` as a date
#### Scenario: Display no-package placeholder
- **GIVEN** 资产没有当前或排队主套餐
- **AND** `estimated_final_expires_at` is null or absent
- **WHEN** 页面渲染资产基础信息
- **THEN** `预计套餐到期时间` MUST display a stable placeholder
- **AND** 页面 MUST NOT substitute the current package detail expiry date
#### Scenario: Highlight backend-designated expiring asset
- **GIVEN** 资产详情返回 `is_expiring=true`
- **WHEN** 页面渲染 `预计套餐到期时间`
- **THEN** 页面 MUST apply the expiring visual treatment using `days_until_final_expiry`
- **AND** 页面 MUST NOT derive whether the asset is expiring from a locally calculated date difference
#### Scenario: Preserve package detail expiry semantics
- **GIVEN** 用户查看资产详情中的套餐明细
- **WHEN** 页面渲染单个套餐的到期时间
- **THEN** 页面 MUST continue to display that package's own expiry field in the package detail context
- **AND** 页面 MUST NOT replace it with `estimated_final_expires_at`
#### Scenario: Display no-package and invalid-data statuses
- **GIVEN** 资产详情返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **WHEN** 页面渲染资产基础信息
- **THEN** `none` MUST display a stable empty placeholder
- **AND** `invalid_data` MUST display `数据异常`
- **AND** 页面 MUST NOT display a fabricated date

View File

@@ -0,0 +1,54 @@
## ADDED Requirements
### Requirement: Device Estimated Final Expiry Display
The device management list integration SHALL preserve and display the five backend device-level estimated final package expiry fields `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_estimate_status`, `expiry_estimate_status_name`, and `is_expiring` returned in `data.items[]` by `GET /api/admin/devices`. The status SHALL be one of `exact`, `waiting_activation`, `none`, or `invalid_data`; non-`exact` records SHALL have `estimated_final_expires_at=null` and `days_until_final_expiry=null`. The frontend SHALL NOT traverse bound cards or calculate package continuation dates. This requirement applies only to the admin device list.
#### Scenario: Display exact device final expiry estimate
- **GIVEN** 设备列表接口返回 `expiry_estimate_status=exact` and `estimated_final_expires_at`
- **WHEN** 页面渲染设备列表行
- **THEN** 页面 MUST display a column labeled `预计套餐到期时间`
- **AND** 该列 MUST format and display `estimated_final_expires_at`
#### Scenario: Display activation-pending device final expiry
- **GIVEN** 设备列表接口返回 `expiry_estimate_status=waiting_activation`
- **WHEN** 页面渲染设备列表行
- **THEN** `预计套餐到期时间` MUST display `待激活后起算`
- **AND** 页面 MUST NOT traverse bound cards or display a fabricated date
#### Scenario: Display device estimate status names for non-exact states
- **GIVEN** 设备列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **WHEN** 页面渲染设备列表行
- **THEN** `none` MUST display a stable empty placeholder
- **AND** `invalid_data` MUST display `数据异常`
- **AND** 页面 MUST use the status enum to choose the display rule
#### Scenario: Preserve null fields for non-exact device estimates
- **GIVEN** 设备列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **THEN** `estimated_final_expires_at` MUST be `null`
- **AND** `days_until_final_expiry` MUST be `null`
- **AND** 页面 MUST NOT derive either field from绑定卡或套餐数据
#### Scenario: Highlight expiring device without reordering
- **GIVEN** 设备列表接口返回 `is_expiring=true` and `days_until_final_expiry`
- **WHEN** 页面渲染该设备的预计套餐到期时间
- **THEN** 页面 MUST apply the expiring visual treatment based on the backend fields
- **AND** 页面 MUST NOT change the ordinary device list sort order because of `is_expiring`
#### Scenario: Display no-package device placeholder
- **GIVEN** 设备列表记录没有可预计的最终到期时间
- **WHEN** 页面渲染预计套餐到期时间列
- **THEN** 页面 MUST display a stable placeholder
- **AND** 页面 MUST NOT use an individual package expiry as a substitute
#### Scenario: Preserve device status name without using it for business calculation
- **WHEN** 设备列表接口返回 `expiry_estimate_status_name`
- **THEN** 前端类型 MUST preserve the field
- **AND** 页面 MUST NOT calculate or replace the backend status name

View File

@@ -0,0 +1,54 @@
## ADDED Requirements
### Requirement: IoT Card Estimated Final Expiry Display
The IoT card management list integration SHALL preserve and display the five backend asset-level estimated final package expiry fields `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_estimate_status`, `expiry_estimate_status_name`, and `is_expiring` returned in `data.items[]` by `GET /api/admin/iot-cards/standalone`. The status SHALL be one of `exact`, `waiting_activation`, `none`, or `invalid_data`; non-`exact` records SHALL have `estimated_final_expires_at=null` and `days_until_final_expiry=null`. The frontend SHALL NOT calculate package continuation dates. This requirement applies only to the admin IoT card list.
#### Scenario: Display exact card final expiry estimate
- **GIVEN** 卡列表接口返回 `expiry_estimate_status=exact` and `estimated_final_expires_at`
- **WHEN** 页面渲染卡列表行
- **THEN** 页面 MUST display a column labeled `预计套餐到期时间`
- **AND** 该列 MUST format and display `estimated_final_expires_at`
#### Scenario: Display activation-pending card final expiry
- **GIVEN** 卡列表接口返回 `expiry_estimate_status=waiting_activation`
- **WHEN** 页面渲染卡列表行
- **THEN** `预计套餐到期时间` MUST display `待激活后起算`
- **AND** 页面 MUST NOT calculate or display a fabricated date
#### Scenario: Display card estimate status names for non-exact states
- **GIVEN** 卡列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **WHEN** 页面渲染卡列表行
- **THEN** `none` MUST display a stable empty placeholder
- **AND** `invalid_data` MUST display `数据异常`
- **AND** 页面 MUST use the status enum to choose the display rule
#### Scenario: Preserve null fields for non-exact card estimates
- **GIVEN** 卡列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **THEN** `estimated_final_expires_at` MUST be `null`
- **AND** `days_until_final_expiry` MUST be `null`
- **AND** 页面 MUST NOT derive either field from套餐数据
#### Scenario: Highlight expiring card without reordering
- **GIVEN** 卡列表接口返回 `is_expiring=true` and `days_until_final_expiry`
- **WHEN** 页面渲染该卡的预计套餐到期时间
- **THEN** 页面 MUST apply the expiring visual treatment based on the backend fields
- **AND** 页面 MUST NOT change the ordinary card list sort order because of `is_expiring`
#### Scenario: Display no-package card placeholder
- **GIVEN** 卡列表记录没有可预计的最终到期时间
- **WHEN** 页面渲染预计套餐到期时间列
- **THEN** 页面 MUST display a stable placeholder
- **AND** 页面 MUST NOT use an individual package expiry as a substitute
#### Scenario: Preserve card status name without using it for business calculation
- **WHEN** 卡列表接口返回 `expiry_estimate_status_name`
- **THEN** 前端类型 MUST preserve the field
- **AND** 页面 MUST NOT calculate or replace the backend status name

View File

@@ -0,0 +1,26 @@
## 1. API Contracts And Types
- [x] 1.1 扩展资产详情响应和资产信息页面状态,支持 5 个预计最终到期字段:`estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``expiry_estimate_status_name``is_expiring`
- [x] 1.2 扩展 IoT 卡和设备列表项类型,支持 5 个预计最终到期字段;不处理 C 端字段。
- [x] 1.3 校验四种状态及字段约束:`exact` 可返回日期和剩余天数,其他状态的 `estimated_final_expires_at``days_until_final_expiry` 必须为 `null`
## 2. Asset Information
- [x] 2.1 将资产解析接口返回的预计最终到期字段映射到资产详情页面状态。
- [x] 2.2 在资产详情卡和设备基础信息中展示“预计套餐到期时间”。
- [x] 2.3 按 `expiry_estimate_status` 展示:`exact` 显示日期,`waiting_activation` 显示“待激活后起算”,`none` 显示占位,`invalid_data` 显示“数据异常”。
- [x] 2.4 `is_expiring=true` 时按 `days_until_final_expiry` 应用临期样式,不根据日期或天数自行推导临期状态。
## 3. Asset Lists
- [x] 3.1 在 IoT 卡列表新增“预计套餐到期时间”可配置列,显示接口返回的预计日期或“待激活后起算”。
- [x] 3.2 在设备列表新增“预计套餐到期时间”可配置列,显示接口返回的预计日期或“待激活后起算”。
- [x] 3.3 在两个列表对 `is_expiring=true` 的预计到期时间使用临期样式,且不改变现有列表排序。
## 4. Verification
- [x] 4.1 验证无套餐时不伪造预计日期并显示稳定占位内容。
- [x] 4.2 验证仅当前套餐和存在多个排队主套餐时,资产详情和两个列表均显示后端 `estimated_final_expires_at`
- [x] 4.3 验证等待激活等不可预计状态显示“待激活后起算”,不显示计算出的日期。
- [x] 4.4 验证 `is_expiring=true` 时使用临期样式,且卡、设备列表顺序不变。
- [x] 4.5 运行相关类型检查、lint 或构建验证。

View File

@@ -0,0 +1,37 @@
## Context
卡和设备列表需要新增统一的实名状态筛选与展示能力。设备的实名状态由设备列表接口直接返回,不能通过关联卡在前端二次计算,以避免多卡设备或卡绑定关系变化时出现不一致。
## Goals / Non-Goals
- Goals:
- 支持按“全部”“已实名”“未实名”筛选卡和设备。
- 直接展示后端返回的实名状态名称。
- 在搜索、刷新、分页和导出查询中保留当前筛选条件。
- Non-Goals:
- 不新增或修改实名认证流程、策略配置或状态更新操作。
- 不在前端推导设备实名状态。
- 不变更其他资产详情页的实名状态取值规则。
## Decisions
- Decision: 使用可选数值查询参数 `real_name_status`,其中 `0` 表示未实名、`1` 表示已实名;“全部”对应不传该参数。
- Decision: 列表展示优先使用每条记录的 `real_name_status_name`,而非根据 `real_name_status` 写死文案。
- Decision: 设备列表把接口响应中的 `real_name_status``real_name_status_name` 作为唯一状态来源,不读取或遍历绑定卡数据。
- Alternatives considered: 前端将 `0``1` 映射为固定文案。未采用,因为后端已提供标准显示名称,直接使用可避免展示口径分叉。
## Risks / Trade-offs
- 后端未返回 `real_name_status_name` 时无法满足状态名称展示契约 -> 联调时校验列表响应字段,并将该字段设为必需的列表类型字段。
- 卡列表实名认证筛选依赖单卡列表接口 -> 实施时确保筛选参数发往 `GET /api/admin/iot-cards/standalone`
## Migration Plan
1. 扩展卡与设备列表 API 类型和查询参数。
2. 接入筛选控件、查询参数及状态列。
3. 验证全部、已实名、未实名筛选以及分页和重置行为。
4. 如需回滚,移除前端筛选控件、查询参数和状态列;不涉及数据迁移。
## Open Questions
- 无。

View File

@@ -0,0 +1,33 @@
# Change: 新增卡和设备实名状态筛选
## Why
运营人员需要在卡列表和设备列表中快速识别并筛选已实名或未实名的资产。当前页面未完整接入实名状态筛选和后端返回的实名状态名称,设备列表尤其不能依赖前端遍历绑定卡来推导状态。
## What Changes
- 卡列表和设备列表的筛选区新增“实名状态”,提供“全部”“已实名”“未实名”选项。
- 卡列表查询使用 `GET /api/admin/iot-cards/standalone`,设备列表查询使用 `GET /api/admin/devices`;选择状态后分别传递 `real_name_status=0|1`,未选择时不传该参数。
- 卡和设备列表项的类型契约支持 `real_name_status: int``real_name_status_name: string`
- 两个列表表格新增“实名状态”列,直接展示接口返回的 `real_name_status_name`
- 设备列表直接使用设备列表接口的实名状态字段,不遍历或关联绑定卡计算设备实名状态。
- 搜索、刷新和分页切换必须保留当前实名状态筛选;重置搜索时清空该筛选。
## Impact
- Affected specs:
- `iot-card-management`
- `device-management`
- Affected code:
- `src/api/modules/card.ts`
- `src/api/modules/device.ts`
- `src/types/api/card.ts`
- `src/types/api/device.ts`
- `src/views/asset-management/iot-card-management/index.vue`
- `src/views/asset-management/device-list/index.vue`
- API contracts:
- `GET /api/admin/iot-cards/standalone?real_name_status=0|1`
- `GET /api/admin/devices?real_name_status=0|1`
- List items return `real_name_status: int` and `real_name_status_name: string`
- Dependencies:
- 后端列表接口必须支持实名状态查询并返回实名状态名称。

View File

@@ -0,0 +1,68 @@
## ADDED Requirements
### Requirement: Device Realname Status Query Contract
The device management list integration SHALL support an optional numeric `real_name_status` parameter on `GET /api/admin/devices`. The list-item contract SHALL include `real_name_status` and `real_name_status_name` returned by the device list API.
#### Scenario: Query devices by realname status
- **GIVEN** 用户正在后台设备列表使用实名状态筛选
- **WHEN** 用户选择“已实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/devices` with `real_name_status=1`
#### Scenario: Query unverified devices
- **GIVEN** 用户正在后台设备列表使用实名状态筛选
- **WHEN** 用户选择“未实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/devices` with `real_name_status=0`
- **AND** 前端 MUST NOT 因为该值为 `0` 而省略此参数
#### Scenario: Query all devices without status restriction
- **GIVEN** 用户未选择实名状态或选择“全部”
- **WHEN** 用户查询、刷新或切换设备列表分页
- **THEN** 请求 MUST NOT 携带 `real_name_status`
#### Scenario: Receive device realname status fields
- **GIVEN** 设备列表接口返回设备记录
- **WHEN** 前端解析列表响应
- **THEN** 列表项类型 MUST 支持 `real_name_status: int`
- **AND** 列表项类型 MUST 支持 `real_name_status_name: string`
### Requirement: Device Realname Status Filter And Display
The device management page SHALL provide a `实名状态` filter with `全部``已实名``未实名` options and display the backend device realname status name in the device table.
#### Scenario: Display device realname status filter
- **GIVEN** 用户打开后台设备列表
- **WHEN** 页面渲染筛选区
- **THEN** 页面 MUST display a `实名状态` filter
- **AND** 筛选项 MUST provide `全部``已实名``未实名` options
#### Scenario: Display backend device realname status name
- **GIVEN** 设备列表接口返回某条记录的 `real_name_status_name`
- **WHEN** 页面渲染该设备的表格行
- **THEN** 页面 MUST 在“实名状态”列显示该字段值
#### Scenario: Use the device API as the status source
- **GIVEN** 设备列表接口返回设备的实名状态字段
- **WHEN** 页面渲染设备实名状态
- **THEN** 页面 MUST directly use the record's `real_name_status` and `real_name_status_name`
- **AND** 页面 MUST NOT 遍历、绑定或计算关联卡的实名状态
#### Scenario: Preserve device realname status while paginating
- **GIVEN** 用户已选择“已实名”或“未实名”并获得筛选结果
- **WHEN** 用户切换设备列表页码或每页条数
- **THEN** 后续列表请求 MUST 保留当前的 `real_name_status` 参数
#### Scenario: Reset device realname status filter
- **GIVEN** 用户已选择实名状态
- **WHEN** 用户重置设备列表搜索条件
- **THEN** 页面 MUST 清空实名状态筛选
- **AND** 后续列表请求 MUST NOT 携带 `real_name_status`

View File

@@ -0,0 +1,61 @@
## ADDED Requirements
### Requirement: IoT Card Realname Status Query Contract
The IoT card management list integration SHALL query `GET /api/admin/iot-cards/standalone` and support an optional numeric `real_name_status` parameter. The list-item contract SHALL include `real_name_status` and `real_name_status_name`.
#### Scenario: Query cards by realname status
- **GIVEN** 用户正在后台卡列表使用实名状态筛选
- **WHEN** 用户选择“已实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/iot-cards/standalone` with `real_name_status=1`
#### Scenario: Query unverified cards
- **GIVEN** 用户正在后台卡列表使用实名状态筛选
- **WHEN** 用户选择“未实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/iot-cards/standalone` with `real_name_status=0`
- **AND** 前端 MUST NOT 因为该值为 `0` 而省略此参数
#### Scenario: Query all cards without status restriction
- **GIVEN** 用户未选择实名状态或选择“全部”
- **WHEN** 用户查询、刷新或切换卡列表分页
- **THEN** 请求 MUST NOT 携带 `real_name_status`
#### Scenario: Receive card realname status fields
- **GIVEN** 卡列表接口返回资产记录
- **WHEN** 前端解析列表响应
- **THEN** 列表项类型 MUST 支持 `real_name_status: int`
- **AND** 列表项类型 MUST 支持 `real_name_status_name: string`
### Requirement: IoT Card Realname Status Filter And Display
The IoT card management page SHALL provide a `实名状态` filter with `全部``已实名``未实名` options and display the backend realname status name in the card table.
#### Scenario: Display card realname status filter
- **GIVEN** 用户打开后台卡列表
- **WHEN** 页面渲染筛选区
- **THEN** 页面 MUST display a `实名状态` filter
- **AND** 筛选项 MUST provide `全部``已实名``未实名` options
#### Scenario: Display backend card realname status name
- **GIVEN** 卡列表接口返回某条记录的 `real_name_status_name`
- **WHEN** 页面渲染该卡的表格行
- **THEN** 页面 MUST 在“实名状态”列显示该字段值
#### Scenario: Preserve card realname status while paginating
- **GIVEN** 用户已选择“已实名”或“未实名”并获得筛选结果
- **WHEN** 用户切换卡列表页码或每页条数
- **THEN** 后续列表请求 MUST 保留当前的 `real_name_status` 参数
#### Scenario: Reset card realname status filter
- **GIVEN** 用户已选择实名状态
- **WHEN** 用户重置卡列表搜索条件
- **THEN** 页面 MUST 清空实名状态筛选
- **AND** 后续列表请求 MUST NOT 携带 `real_name_status`

View File

@@ -0,0 +1,26 @@
## 1. API Contracts And Types
- [x] 1.1 扩展卡列表查询参数和列表项类型,支持可选 `real_name_status` 以及必需的 `real_name_status_name`
- [x] 1.2 扩展设备列表查询参数和列表项类型,支持可选 `real_name_status` 以及必需的 `real_name_status_name`
- [x] 1.3 将卡列表查询接入 `GET /api/admin/iot-cards/standalone` 并传递实名状态筛选参数。
## 2. Card List
- [x] 2.1 在卡列表筛选区新增“实名状态”的全部、已实名、未实名选项。
- [x] 2.2 将选中的实名状态传递给卡列表查询,并在搜索、刷新、分页、导出中保留该条件。
- [x] 2.3 在卡列表表格新增“实名状态”列,展示接口返回的 `real_name_status_name`
- [x] 2.4 重置卡列表搜索时清空实名状态筛选。
## 3. Device List
- [x] 3.1 在设备列表筛选区新增“实名状态”的全部、已实名、未实名选项。
- [x] 3.2 将选中的实名状态传递给 `GET /api/admin/devices`,并在搜索、刷新、分页、导出中保留该条件。
- [x] 3.3 在设备列表表格新增“实名状态”列,直接展示接口返回的 `real_name_status_name`,不遍历绑定卡计算状态。
- [x] 3.4 重置设备列表搜索时清空实名状态筛选。
## 4. Verification
- [ ] 4.1 验证卡列表“全部”“已实名”“未实名”分别不传、传 `1`、传 `0`,且状态列显示接口名称。
- [ ] 4.2 验证设备列表“全部”“已实名”“未实名”分别不传、传 `1`、传 `0`,且状态列直接显示设备接口名称。
- [ ] 4.3 验证两个列表在分页切换、刷新和导出时保留实名状态筛选,重置后清空该条件。
- [x] 4.4 运行相关类型检查、lint 或构建验证。

View File

@@ -0,0 +1,39 @@
# Change: 新增资产钱包自动续费配置(后台查询/保存 + 自动续费失败通知适配)
## Why
根据 `docs/产品迭代8月份/资产钱包.md`。后端测试环境(`https://cmp-api.boss160.cn``Authorization: Bearer <token>`)已提供资产钱包自动续费全局配置能力,仅有 2 个后台接口H5 侧零新增:
- 查询资产钱包自动续费配置:`GET /api/admin/asset-auto-renewal-config`
- 保存资产钱包自动续费配置:`PUT /api/admin/asset-auto-renewal-config`
后台需要提供配置页面承载总开关、适用范围、指定主套餐、到期前天数等配置;同时新增通知类型 `asset.auto_renewal.failed` 会复用既有通知接口产生新数据,需要适配展示与跳转。
## What Changes
- 新增能力 `asset-wallet-auto-renewal`
- 配置读取:`GET /api/admin/asset-auto-renewal-config`,返回唯一一份全局配置的 `enabled`+`enabled_name``scope`+`scope_name``package_ids``days_before_expiry``config_version``updater``updated_at`
- 配置保存:`PUT /api/admin/asset-auto-renewal-config`
- 请求体 `{ enabled(0/1必填), scope(all|specified必填), package_ids(uint[],仅 specified 时非空且只能选当前可售主套餐), days_before_expiry(190必填) }`
- 语义:单行配置、无新增/删除;保存即递增 `config_version`,记录操作者与前后值快照;配置变更只影响后续扫描,历史记录不重算。
- 权限:仅超级管理员与平台账号可访问;其他身份(代理/企业/个人客户)`403`,提示「无权限操作该资源或资源不存在」,无权限与不存在不区分。
- 通知适配(复用既有接口,不算新接口):
- 后台站内通知:新增通知类型 `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`),接收人是业务员/店铺账号。走既有 `GET /api/admin/notifications` + `GET /api/admin/notifications/{id}/target`;确认 `target_type``iot_card` / `device` 在导航白名单;`available=false` 时只显示正文不跳转;计入未读数。
- H5 个人客户通知:同一类型已由后端加入客户可见白名单,出现在既有 `GET /api/c/v1/notifications``unread-count`,客户侧只有文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面,本仓库无需改动。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
- 明确不做(避免前端空等):无自动续费记录页/异常记录页接口、无尝试记录查询接口、无「有余额就不停机」开关、无 H5 客户侧新接口。
## Impact
- Affected specs:
- `asset-wallet-auto-renewal` — 新增能力
- Affected code:
- `src/types/api/assetWallet.ts`(新增)
- `src/types/api/index.ts`
- `src/api/modules/assetWallet.ts`(新增)
- `src/api/modules/index.ts`
- `src/views/settings/asset-wallet-auto-renewal/index.vue`(新增)
- `src/router/routesAlias.ts`
- `src/router/routes/asyncRoutes.ts`
- `src/utils/business/notificationNavigation.ts`(确认/补充 `iot_card``device` 白名单)
- `src/components/core/layouts/art-notification/index.vue`(确认 `expiry` 分类覆盖新类型)
- `src/locales/langs/zh.json``src/locales/langs/en.json`

View File

@@ -0,0 +1,94 @@
## ADDED Requirements
### Requirement: 自动续费配置查询
The admin frontend SHALL load the single global asset wallet auto-renewal configuration through `GET /api/admin/asset-auto-renewal-config`. 响应字段 SHALL 包含并展示 `enabled`+`enabled_name``scope`+`scope_name``package_ids``days_before_expiry``config_version``updater``updated_at`
#### Scenario: 读取全局自动续费配置
- **WHEN** 用户进入资产钱包自动续费配置页面
- **THEN** 前端 MUST 调用 `GET /api/admin/asset-auto-renewal-config`
- **AND** 页面 MUST 展示总开关及中文名称、适用范围及中文名称、指定主套餐集合、统一到期前天数、配置版本、最近保存操作者与最近保存时间
### Requirement: 自动续费配置保存
The admin frontend SHALL save the configuration through `PUT /api/admin/asset-auto-renewal-config` with request body `{ enabled, scope, package_ids, days_before_expiry }``enabled` SHALL 取值 0/1 且必填;`scope` SHALL 取值 `all`(全部主套餐)或 `specified`(指定主套餐)且必填;`package_ids` 仅在 `scope=specified` 时必填非空且只能选择当前可售主套餐;`days_before_expiry` SHALL 取值 1 至 90 且必填。配置为单行、无新增/删除;保存 SHALL 递增 `config_version` 并记录操作者与前后值快照;配置变更 SHALL 只影响后续扫描,历史记录不重算。
#### Scenario: 保存全部主套餐范围配置
- **WHEN** 用户设置适用范围为「全部主套餐」并提交
- **THEN** 前端 MUST 调用 `PUT /api/admin/asset-auto-renewal-config`
- **AND** 请求体携带 `enabled``scope=all``days_before_expiry`
- **AND** `package_ids` 以空数组提交
- **AND** 保存成功后页面 MUST 以响应中的 `config_version``updater``updated_at` 刷新展示
#### Scenario: 保存指定主套餐范围配置
- **WHEN** 用户设置适用范围为「指定主套餐」并选择若干当前可售主套餐后提交
- **THEN** `package_ids` MUST 非空且只包含当前可售主套餐
- **AND** 前端 MUST 携带 `scope=specified` 与非空 `package_ids` 提交
#### Scenario: 参数校验
- **WHEN** `enabled` 缺失、`scope` 非法、`scope=specified``package_ids` 为空或包含不可售套餐,或 `days_before_expiry` 超出 190
- **THEN** 前端 MUST 阻止提交并给出校验提示
#### Scenario: 仅影响后续扫描
- **WHEN** 用户修改并保存配置
- **THEN** 历史自动续费记录 MUST NOT 被重算
- **AND** 新配置 MUST 只对保存后的扫描生效
### Requirement: 自动续费配置访问权限
Only super admin and platform accounts SHALL be allowed to query or save the asset wallet auto-renewal configuration. 代理、企业与个人客户等身份访问时 SHALL 返回 `403` 并提示「无权限操作该资源或资源不存在」;无权限与不存在的表现 MUST NOT 可区分。
#### Scenario: 授权访问
- **WHEN** 超级管理员或平台账号访问配置接口
- **THEN** 页面 MUST 正常查询与保存配置
#### Scenario: 无权限访问
- **WHEN** 代理、企业或个人客户账号访问配置接口
- **THEN** 页面 MUST 按 `403` 无权限处理并展示统一提示
- **AND** 前端 MUST NOT 通过响应区分无权限与资源不存在
### Requirement: 自动续费失败后台通知
The admin notification center SHALL display the new notification type `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`through the existing notification APIs接收人为业务员/店铺账号。目标 `target_type``iot_card``device` SHALL 位于通知导航白名单;当目标 `available=false` 时页面 SHALL 只展示正文且不跳转;该类型通知 SHALL 计入未读数。
#### Scenario: 展示并跳转自动续费失败通知
- **WHEN** 通知列表返回 `asset.auto_renewal.failed` 类型且目标可用
- **THEN** 通知中心 MUST 将该通知归入 `expiry` 类别展示
- **AND** 用户点击后 MUST 通过既有目标接口按 `iot_card` / `device` 跳转到对应资产详情
#### Scenario: 目标不可用
- **WHEN** 目标接口返回 `available=false`
- **THEN** 页面 MUST 只展示通知正文且不发起跳转
### Requirement: H5 客户侧通知边界
This capability SHALL NOT 新增或修改 H5 客户侧接口与页面。`asset.auto_renewal.failed` 类型已由后端加入客户可见白名单,通过既有 `GET /api/c/v1/notifications``unread-count` 返回;客户侧仅展示文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
#### Scenario: H5 复用既有接口
- **WHEN** 客户侧通知接口返回该类型
- **THEN** 客户侧 MUST 仅展示文案且不提供跳转
- **AND** 本次变更 MUST NOT 新增 H5 客户侧接口或页面
#### Scenario: 到期展示期
- **WHEN** 对应套餐业务到期
- **THEN** 该通知按 `expiry` 类别既有行为从客户侧列表消失
### Requirement: 功能范围边界
This capability SHALL NOT 包含以下内容:自动续费记录页/异常记录页、尝试记录查询、「有余额就不停机」开关、H5 客户侧新接口。
#### Scenario: 明确不提供的功能
- **WHEN** 前端对接本次资产钱包自动续费能力
- **THEN** 页面 MUST NOT 提供或依赖自动续费记录页、异常记录页、尝试记录查询接口与「有余额就不停机」开关

View File

@@ -0,0 +1,27 @@
# Tasks: 资产钱包自动续费配置
## 1. 类型与 API
- [ ] 1.1 新增 `src/types/api/assetWallet.ts``AssetAutoRenewalConfig``enabled`/`enabled_name`/`scope`/`scope_name`/`package_ids`/`days_before_expiry`/`config_version`/`updater`/`updated_at`)、`UpdateAssetAutoRenewalConfigRequest``enabled`/`scope`/`package_ids`/`days_before_expiry`)等类型
- [ ] 1.2 `src/types/api/index.ts` 导出新类型
- [ ] 1.3 新增 `src/api/modules/assetWallet.ts`
- `getAutoRenewalConfig``GET /api/admin/asset-auto-renewal-config`
- `saveAutoRenewalConfig``PUT /api/admin/asset-auto-renewal-config`
- [ ] 1.4 `src/api/modules/index.ts` 导出新服务
## 2. 配置页面
- [ ] 2.1 `src/router/routesAlias.ts` 新增资产钱包自动续费配置路由别名,`src/router/routes/asyncRoutes.ts` 在设置下注册路由与菜单(仅超级管理员与平台账号可见)
- [ ] 2.2 新增 `src/views/settings/asset-wallet-auto-renewal/index.vue`:读取配置并展示总开关、适用范围、指定主套餐、统一到期前天数、配置版本、最近保存操作者与时间
- [ ] 2.3 实现保存表单总开关0/1、适用范围all/specified、到期前天数190 校验);指定范围时主套餐多选仅支持当前可售主套餐且非空
- [ ] 2.4 保存成功后使用响应刷新 `config_version``updater``updated_at`,不做新增/删除记录操作
## 3. 通知适配
- [ ] 3.1 确认 `src/utils/business/notificationNavigation.ts` 白名单包含 `iot_card``device`,目标可用时跳转资产详情
- [ ] 3.2 确认 `src/components/core/layouts/art-notification/index.vue``expiry` 分类可展示 `asset.auto_renewal.failed``available=false` 时只显示正文不跳转
- [ ] 3.3 确认该类型通知计入未读数与分类汇总
## 4. Verification
- [ ] 4.1 验证 GET/PUT 字段传递与参数校验enabled、scope、package_ids、days_before_expiry
- [ ] 4.2 验证指定范围时 package_ids 非空且仅可售主套餐
- [ ] 4.3 验证通知类型展示、目标跳转与不可用目标处理
- [ ] 4.4 验证非超级管理员/平台账号访问的 403 处理
- [ ] 4.5 运行 lint、类型检查与构建

View File

@@ -0,0 +1,53 @@
## Context
本次变更横跨平台、代理和企业三类主体以及资产、订单、退款、钱包、通知等多个业务页面。OpenAPI 将关联关系明确建模为 `investigation_refs``linkage` 和稳定资源引用,并区分平台内部 ID 与代理/企业可见的业务标识。前端必须保持这些边界,不能为了补齐跳转而从名称、时间或摘要推断关联关系。
仓库目前没有已归档的基线 specs但存在通知中心和旧资产操作日志的活跃变更。本提案建立独立的审计链路能力通过明确的依赖点与它们集成不复制或覆盖其已有要求。
## Goals / Non-Goals
- Goals: 按 OpenAPI 实现 16 个只读接口的类型、查询、页面和跨视角跳转契约。
- Goals: 为平台、代理和企业提供符合各自权限与数据投影的业务审计入口。
- Goals: 统一分页、RFC3339 时间范围、枚举展示、在线保留窗口和错误处理。
- Non-Goals: 不修改后端接口、数据库、日志采集或保留策略。
- Non-Goals: 不提供恢复、重试、修改、删除、处置、封禁、任意全文搜索或导出。
- Non-Goals: 不替换现有资产操作日志组件,也不修改 C/H5 端。
## Decisions
- Decision: 新增独立审计 API 模块和共享类型按平台审计、主体活动、资金、风险、Integration、链路时间线分组暴露查询函数统一保留服务端响应包装和 nullable 字段。
- Decision: 平台业务入口使用响应中的内部稳定 ID。卡、设备、店铺、企业、订单和退款分别使用对应 `resource_type``response.data.id`;钱包通过资金时间线的 `wallet_id` 进入。
- Decision: 代理与企业入口使用响应中的稳定业务标识。卡使用 `iccid`,设备使用 `virtual_no`;代理的分配、换货、店铺和企业使用接口文档指定的业务编号。缺少稳定标识时隐藏入口。
- Decision: 前端只转发后端返回的调查引用。事件的 `event_id``actor_ref``resource_refs``request_id``correlation_id``integration_refs`,以及 Integration 的 `linkage`/`resource` 是跨视角导航的唯一数据来源。
- Decision: `request_id` 可以来自调查引用、Integration、或用户从 Access Log 明确粘贴;`correlation_id``integration_id` 只能来自接口返回的稳定字段。前端不使用 UUID 或其他方式生成这些 ID。
- Decision: 对不存在、为空或 `fidelity=false` 的引用隐藏对应跳转,禁止按名称、时间、摘要或相邻记录猜测缺失关系。
- Decision: 资源搜索严格使用 `resource_type + keyword` 精确查询,并把选中项的 `resource_type/resource_id` 原样传给资源时间线。`historical=true` 只作为历史快照命中提示。
- Decision: 列表和时间线统一消费 `page``page_size``total` 并保留 `retention` 语义。时间筛选发送含时区的 RFC3339 值,`created_to` 按接口定义为不包含该时刻;页面不展示全局在线窗口提示,也不提供归档查询假入口。
- Decision: 所有枚举筛选提交稳定 code并优先展示后端 `*_name``action`、Integration `operation` 等开放编码必须直接来自响应,不能从中文文案反推。
- Decision: 资金金额按分展示转换,权威性只认 `amount_authority.authoritative=true` 指向的业务字段;前端不得合并冲突金额或自行计算余额。
- Decision: 代理与企业活动共用展示外壳但使用独立 API。企业页面只展示主体安全投影不引入平台操作者、风险、内部原因或 before/after 字段。
- Decision: 通知目标仅在 `available=true``target_type=integration_log``target_key` 非空时跳转,并将 `target_key` 原样作为 Integration 详情的 `integration_id`。其他情况保留通知正文并提示目标不可用。
- Decision: 权限以最终菜单/按钮权限编码和服务端鉴权共同控制。平台、代理、企业不展示不属于其主体的审计入口,`401/403` 继续走共享认证与无权处理。
## Risks / Trade-offs
- 最终菜单与按钮权限编码尚未体现在 OpenAPI 中;实施前必须确认,否则只能依赖服务端 `403`,无法做到准确的入口隐藏。
- 审计事件和 Integration 模型字段较多;通过共享详情/时间线组件和严格类型避免各业务页重复实现,但不抽象为可以执行任意端点的动态页面。
- 在线保留窗口外的数据不可由这些接口查询;空状态使用“当前查询无记录”的中性表达,避免将空数据误报为无历史事件。
- 活跃通知中心变更也会修改通知导航;实施时需在其最新状态上追加 `integration_log` 规则,避免覆盖既有白名单映射。
- 旧资产操作日志与新审计中心含义相邻但数据源不同;两个入口并存并清晰命名,避免误将旧日志能力删除。
## Migration Plan
1. 确认平台、代理、企业的菜单和按钮权限编码,以及审计中心路由归属。
2. 建立 OpenAPI 对齐的类型、API 模块和共享只读展示组件。
3. 按平台审计、资金、风险、Integration、主体活动顺序接入页面。
4. 在业务页面与通知导航中增加稳定引用入口。
5. 完成角色、数据隔离、空引用、保留窗口、错误和跨视角跳转验收。
6. 若上线异常,隐藏新增路由与入口;现有业务接口和旧操作日志不受影响。
## Open Questions
- 平台审计中心、风险中心、Integration、代理资源活动和企业资源活动的最终菜单及按钮权限编码分别是什么
- 审计中心是作为一级菜单,还是归入现有系统管理/运维菜单?
- OpenAPI 未包含 `GET /api/admin/notifications/{id}/target` 的完整响应 schema是否继续以活跃通知中心变更定义的 `available/target_type/target_id/target_key` 为最终契约?

View File

@@ -0,0 +1,38 @@
# Change: 新增审计链路前端接入能力
## Why
八月迭代新增了审计事件、请求与业务链路、资金调查、风险调查、外部集成交互以及代理/企业资源活动共 16 个只读接口。当前前端尚无统一的审计调查页面、接口类型和业务入口,无法可靠消费后端返回的稳定调查引用,也容易错误地由名称、时间或页面数据自行拼接链路标识。
## What Changes
- 新增平台审计事件列表、详情、操作者时间线、资源精确搜索和资源时间线能力。
- 新增请求链路与业务关联链路时间线,并只使用接口返回或用户从 Access Log 粘贴的稳定 `request_id``correlation_id`
- 新增资金调查时间线、风险总览与明细、Integration 总览/列表/详情页面。
- 新增代理和企业资源活动入口,并按主体支持范围使用稳定业务标识和后端安全投影。
- 在卡、设备、店铺、企业、订单、退款和钱包相关列表/详情页增加角色匹配的「审计记录」入口。
- 接入通知目标到 Integration 详情的受控跳转:仅接受可用的 `integration_log` 目标。
- 统一使用后端返回的名称、状态、保留窗口语义和调查引用;禁止前端猜测或生成 `request_id``correlation_id``integration_id`
- 所有新增能力均为只读调查能力,不增加恢复、修改、删除、处置、封禁或导出操作。
## Impact
- Affected specs: `audit-chain-frontend-integration`
- Affected code:
- `src/api/modules/` 下新增审计接口模块
- `src/types/api/` 下新增审计接口类型
- `src/views/` 下审计中心、风险中心、Integration 和时间线页面
- 卡、设备、店铺、企业、订单、退款、钱包相关列表/详情页
- 路由、菜单、权限控制及通知目标导航
- API contracts:
- `GET /api/admin/audit/*`
- `GET /api/admin/agent/resource-activities/{resource_type}/{identifier}`
- `GET /api/admin/enterprise/resource-activities/{resource_type}/{identifier}`
- `GET /api/admin/notifications/{id}/target`
- Dependencies:
- 与活跃变更 `update-admin-notification-center-api` 的受控通知目标协议保持一致
- 与活跃变更 `update-admin-asset-device-signal-and-audit-logs` 的旧资产操作日志展示并存;本变更不替换旧日志接口
- Source of truth:
- `docs/产品迭代8月份/审计链路接口变更整理文档.md`
- `docs/产品迭代8月份/默认模块.openapi.json`
- Breaking changes: 无;现有业务接口路径保持不变

View File

@@ -0,0 +1,207 @@
## ADDED Requirements
### Requirement: Platform Audit Event Investigation
The admin frontend SHALL provide platform audit event list and detail views using `GET /api/admin/audit/events` and `GET /api/admin/audit/events/{event_id}`. It SHALL preserve the documented event, actor, scope, resource, result, risk, request and investigation-reference fields, use stable codes for filters, and prefer backend display names.
#### Scenario: Filter and inspect audit events
- **WHEN** an authorized platform user filters audit events by documented time, action, category, actor, source, result, risk, scope, resource or linkage parameters
- **THEN** the frontend MUST send the documented parameter names and stable code values
- **AND** it MUST render the paginated `items`, `total`, `page`, `page_size` and `retention` response
#### Scenario: Open an event detail
- **WHEN** the user opens an event returned by the list or another investigation reference
- **THEN** the frontend MUST pass its `event_id` unchanged to the detail endpoint
- **AND** it MUST display backend snapshots and names without replacing historical values with current account or resource names
### Requirement: Stable Investigation Reference Navigation
The frontend SHALL navigate between audit views only through stable identifiers returned by the APIs. `investigation_refs.actor_ref`, `resource_refs`, `request_id`, `correlation_id`, `integration_refs`, Integration `linkage`, and stable Integration `resource` references SHALL be the authoritative navigation sources.
#### Scenario: Navigate through a returned reference
- **WHEN** an audit or Integration response contains a non-empty supported investigation reference
- **THEN** the frontend MUST pass the returned identifier and type unchanged to the corresponding actor, resource, request, correlation or Integration view
#### Scenario: A reference is unavailable or unreliable
- **WHEN** the required reference is empty, absent, unsupported, or its linkage `fidelity` indicates that a relationship is unavailable
- **THEN** the frontend MUST hide or disable the related navigation action
- **AND** it MUST NOT infer a relationship from names, timestamps, summaries, adjacent rows or resource text
#### Scenario: Link identifiers are not generated by the frontend
- **WHEN** the frontend needs a `correlation_id` or `integration_id`
- **THEN** it MUST use a value returned by an API
- **AND** it MUST NOT generate, concatenate or guess the identifier
### Requirement: Actor, Resource, and Link Timelines
The admin frontend SHALL provide actor-event, resource-event, request-link and correlation-link timelines through the documented endpoints. It SHALL support an explicitly pasted Access Log `request_id`, while all other navigation values SHALL come from returned stable references.
#### Scenario: Inspect actor behavior
- **WHEN** a user follows `actor_ref.kind/id` or opens an account by its stable ID
- **THEN** the frontend MUST call `/api/admin/audit/actors/{kind}/{id}/events`
- **AND** action and resource filters MUST use stable values returned by audit data
#### Scenario: Select an exact resource result
- **WHEN** a user searches with a supported `resource_type` and exact `keyword` and selects a result
- **THEN** the frontend MUST pass `items[].resource_type/resource_id` unchanged to the resource timeline
- **AND** it MUST identify `historical=true` as a historical-snapshot match
#### Scenario: Query a request copied from Access Log
- **WHEN** a platform user explicitly pastes a request ID from Access Log
- **THEN** the frontend MAY query `/api/admin/audit/requests/{request_id}/timeline` with that exact value
- **AND** it MUST NOT claim that the endpoint scans Access Log or archived object storage
### Requirement: Retention, Pagination, and Time Semantics
All audit list and timeline views SHALL use the documented pagination and `retention` contract. Time filters SHALL be RFC3339 timestamps with timezone information, and `created_to` SHALL be treated as an exclusive upper bound.
#### Scenario: Respect the online retention window without a global prompt
- **WHEN** a response contains `retention.online_from`, `archived_before` and `timezone`
- **THEN** the frontend MUST preserve the retention semantics in its data contract without displaying a global online-window banner
- **AND** it MUST NOT interpret an empty online result as proof that no historical event exists
#### Scenario: Page through a timeline
- **WHEN** the user changes page or page size
- **THEN** the frontend MUST use `page` and `page_size`, respect the maximum page size of 100, and preserve the active filters
### Requirement: Finance Investigation Timeline
The admin frontend SHALL query `GET /api/admin/audit/finance/timeline` with any documented stable finance condition, including shop, wallet, order, payment, refund, recharge, approval, third-party trade, actor or correlation identifiers. Monetary values SHALL remain integer fen in application data, and authority SHALL follow `amount_authority`.
#### Scenario: Open finance history from a business record
- **WHEN** a user opens finance history from an order, refund, recharge, wallet or shop with a stable backend ID
- **THEN** the frontend MUST send the matching documented query parameter
- **AND** it MUST allow the server to complete related facts instead of assembling a local timeline
#### Scenario: Display an authoritative amount
- **WHEN** a finance node returns an amount and `amount_authority.authoritative=true`
- **THEN** the frontend MUST treat the referenced table and field as authoritative
- **AND** it MUST only convert integer fen for presentation and MUST NOT reconcile conflicting facts locally
### Requirement: Risk Investigation
The admin frontend SHALL provide risk overview and event-detail views using `/api/admin/audit/risks/overview` and `/api/admin/audit/risks/events`. Filters SHALL use codes returned in overview collections, and the selected time range SHALL not exceed 31 days.
#### Scenario: Drill down from a risk summary
- **WHEN** a user selects a risk, result, action, source or signal represented by the overview
- **THEN** the frontend MUST use the returned stable code for the supported event-list filter
- **AND** backend `name` values MUST be used only for display
#### Scenario: Select a range longer than 31 days
- **WHEN** the user attempts to query more than 31 days
- **THEN** the frontend MUST prevent submission and explain the maximum range
### Requirement: External Integration Investigation
The admin frontend SHALL provide Integration overview, list and detail views through the documented endpoints. It SHALL preserve provider, direction, operation, raw result, derived result category, duration, state-change, trigger, resource, linkage, attempt, content-summary, fidelity and retention fields.
#### Scenario: Apply overview filters to the list
- **WHEN** a user selects an overview provider, direction or result
- **THEN** the frontend MUST map `providers[].code` to `provider`, `directions[].code` to `direction`, `results[].code` to `result`, and `results[].category` to `result_category`
- **AND** it MUST use `name` only as display text
#### Scenario: Display Integration trend categories
- **WHEN** the overview returns trend points
- **THEN** the frontend MUST preserve the five categories `processing`, `succeeded`, `indeterminate`, `failed` and `not_sent`
- **AND** it MUST use only the documented `hour` or `day` bucket
#### Scenario: Inspect an Integration detail
- **WHEN** a user opens an `integration_id` returned by a list, event reference or notification target
- **THEN** the frontend MUST query the detail with that value unchanged
- **AND** it MUST present the capability as read-only without recovery, modification, deletion or export actions
### Requirement: Role-Specific Resource Activity
The frontend SHALL use the agent and enterprise resource-activity endpoints according to the authenticated subject. Agent activity SHALL support `iot_card`, `device`, `asset_allocation_record`, `exchange_order`, `shop` and `enterprise`; enterprise activity SHALL support only authorized `iot_card` and `device` resources.
#### Scenario: Open an agent resource activity
- **WHEN** an agent opens activity for a supported resource
- **THEN** the frontend MUST use ICCID for a card, VirtualNo for a device, or the documented stable business number for another supported resource
- **AND** it MUST NOT send or infer the agent identity or shop scope as query data
#### Scenario: Open enterprise asset activity
- **WHEN** an enterprise user opens an authorized card or device activity
- **THEN** the frontend MUST use ICCID or VirtualNo with the enterprise endpoint
- **AND** it MUST render only the subject-safe projection without platform actor, risk, internal reason or before/after fields
#### Scenario: A stable subject identifier is missing
- **WHEN** a business response does not contain the required ICCID, VirtualNo or stable business number
- **THEN** the frontend MUST hide the activity entry
- **AND** it MUST NOT substitute a display name or internal identifier intended for another subject
### Requirement: Business Audit Entries
The frontend SHALL add role-appropriate audit entries to card, device, shop, enterprise, order, refund and wallet list/detail contexts without changing existing business API URLs. Platform entries SHALL use backend internal IDs; agent and enterprise entries SHALL use their documented stable business identifiers.
#### Scenario: Open platform asset or business history
- **WHEN** a platform user opens audit history from a card, device, shop, enterprise, order or refund record
- **THEN** the frontend MUST use the corresponding `resource_type` and backend `response.data.id` with the resource timeline
#### Scenario: Open wallet or transaction history
- **WHEN** a user opens audit history for a wallet, order or refund with a stable ID
- **THEN** the frontend MUST offer the applicable finance timeline query
- **AND** the resource timeline MAY also be offered only when a stable resource reference is available
#### Scenario: Preserve existing operation logs
- **WHEN** new audit entries are introduced on asset pages
- **THEN** existing asset operation-log features MUST remain available
- **AND** the UI MUST distinguish the existing operation logs from the new cross-system audit investigation
### Requirement: Controlled Notification Integration Target
The frontend SHALL resolve a notification through `GET /api/admin/notifications/{id}/target` before opening an Integration detail. It SHALL open the detail only when the target is available, has `target_type=integration_log`, and contains a non-empty `target_key`.
#### Scenario: Open an Integration notification
- **WHEN** target resolution returns `available=true`, `target_type=integration_log`, and a non-empty `target_key`
- **THEN** the frontend MUST pass `target_key` unchanged as the Integration `integration_id`
- **AND** it MUST open the controlled internal Integration detail route
#### Scenario: Notification target cannot be used
- **WHEN** the target is unavailable, has another type, lacks `target_key`, or maps to no registered route
- **THEN** the frontend MUST not navigate or execute an arbitrary URL
- **AND** it MUST preserve the notification content and show a safe unavailable-target message
### Requirement: Audit Authorization and Read-Only Boundary
The frontend SHALL enforce the final platform, agent and enterprise route and action permissions, while preserving backend authentication, authorization and data-isolation enforcement. All capabilities in this change SHALL remain read-only.
#### Scenario: A subject lacks audit permission
- **WHEN** the current subject lacks the configured permission for an audit page or business entry
- **THEN** the frontend MUST hide or disable that page or entry and MUST NOT call the endpoint as a fallback
#### Scenario: An audit request is rejected
- **WHEN** an endpoint returns `400`, `401`, `403` or `500`
- **THEN** the frontend MUST use shared validation, authentication, authorization and server-error handling
- **AND** it MUST preserve the current investigation state when retrying would be unsafe or misleading
#### Scenario: A user inspects an audit record
- **WHEN** any audit, risk, finance, Integration or subject-activity view is displayed
- **THEN** the UI MUST NOT provide mutation, recovery, deletion, risk-disposition, blocking or export actions

View File

@@ -0,0 +1,44 @@
## 1. Contract and Shared Infrastructure
- [x] 1.1 根据 `默认模块.openapi.json` 建立审计事件、调查引用、资源、保留窗口、链路节点、资金节点、风险和 Integration 的 TypeScript 类型。
- [x] 1.2 新增平台审计、主体资源活动、资金、风险、Integration 和链路时间线 API 方法,保持文档参数名、枚举和 nullable 语义。
- [x] 1.3 建立共享的分页、RFC3339 时间范围、保留窗口、枚举名称和 API 错误展示能力。
- [x] 1.4 建立只读事件详情、资源摘要、调查引用和时间线节点组件,不提供写操作或导出能力。
## 2. Platform Audit Center
- [x] 2.1 实现审计事件列表及文档定义的时间、动作、类别、操作者、来源、结果、风险、范围、资源和链路筛选。
- [x] 2.2 实现事件详情,展示事件、操作者、资源快照、结果、风险和元数据,并按非空 `investigation_refs` 提供跳转。
- [x] 2.3 实现操作者行为时间线,使用 `actor_ref.kind/id` 和稳定 code 筛选。
- [x] 2.4 实现资源精确搜索及资源时间线,原样使用选中项的 `resource_type/resource_id` 并提示历史快照命中。
- [x] 2.5 实现请求和业务关联时间线,支持从返回引用进入,以及明确粘贴 Access Log `request_id` 的查询入口。
## 3. Specialized Investigations
- [x] 3.1 实现资金调查时间线和全部文档筛选条件,以分为数据单位并展示金额权威来源。
- [x] 3.2 实现风险总览和风险事件明细,按总览返回的稳定 code 回填筛选,并限制最长 31 天时间范围。
- [x] 3.3 实现 Integration 总览、列表和详情,覆盖 provider、direction、result、result_category、趋势、尝试、内容摘要、保真度和关联字段。
- [x] 3.4 实现各调查视角之间基于 `investigation_refs``linkage` 和稳定资源引用的受控跳转。
## 4. Subject Activity and Business Entries
- [x] 4.1 实现代理资源活动页面,支持卡、设备、资产分配记录、换货单、店铺和企业的稳定业务标识。
- [x] 4.2 实现企业资源活动页面,仅支持当前授权卡和设备并只渲染主体安全投影。
- [x] 4.3 在卡、设备、店铺、企业、订单、退款和钱包列表/详情增加角色匹配的「审计记录」入口。
- [x] 4.4 缺少内部 ID、ICCID、VirtualNo 或业务编号时隐藏入口,不使用页面文本或其他字段推断。
- [x] 4.5 保留现有资产操作日志入口,并通过命名与说明区分旧操作日志和新审计调查能力。
## 5. Notification, Routes, and Permissions
- [x] 5.1 增加审计中心、风险中心、Integration 和共享时间线的路由与菜单配置。
- [x] 5.2 接入最终确认的平台、代理、企业菜单/按钮权限编码,隐藏越权页面和业务入口。
- [x] 5.3 在通知目标导航中仅对可用 `integration_log` 目标开放 Integration 详情,并原样使用 `target_key`
- [x] 5.4 对未知、不可用或缺失目标保留安全提示,不执行任意 URL 或推测跳转。
## 6. Verification
- [ ] 6.1 为 API 参数序列化、枚举、nullable 字段、分页和 RFC3339 时间范围增加单元测试。
- [ ] 6.2 为稳定引用跳转、空引用隐藏、`fidelity=false`、通知 Integration 目标和禁止生成链路 ID 增加测试。
- [ ] 6.3 验证平台、代理、企业角色可见性、服务端数据隔离、企业安全投影以及 `401/403/400/500` 处理。
- [ ] 6.4 验证保留窗口、空数据、最长 31 天风险查询、资金金额单位和 Integration 五类趋势状态。
- [ ] 6.5 完成 16 个接口的联调回归,并确认现有业务 URL、通知中心和旧资产操作日志未被破坏。

View File

@@ -0,0 +1,65 @@
## Context
批量订购包含文件上传、异步任务处理、任务恢复和逐行失败查看四个阶段。任务创建需要绑定一个代理商和一种支付方式,线下支付还需要上传整批凭证;任务处理过程中可能出现部分成功和钱包余额不足,前端必须展示后端结果而不能把整批操作当成原子事务。
## Goals / Non-Goals
- Goals: 提供批量订单 Excel 上传、支付方式选择、任务进度展示、任务恢复和逐行失败明细。
- Goals: 使用 `request_id` 防止重复提交,并按任务 ID 查询服务端真实状态。
- Goals: 复用公共异步任务的五态语义和终态规则。
- Non-Goals: 不在前端解析 Excel 业务行、不执行订单创建、不计算订单金额、不回滚已成功订单。
- Non-Goals: 不改造现有单笔订单创建和单笔支付凭证上传流程。
- Non-Goals: 不新增后端模板下载接口。
## Decisions
- Decision: 批量订购页面使用独立任务视图,订单列表只提供入口,不把逐行结果嵌入订单列表表格。
- Rationale: 批量任务有独立生命周期和大量逐行结果,独立页面更适合恢复、轮询和分页查看。
- Decision: `payment_method` 在创建表单中为单选值,并在请求中对整批固定;线下支付时才允许提交 `voucher_file`
- Rationale: 产品明确一个批次不能混合支付方式,前端应避免生成含混请求。
- Decision: 使用前端静态 Excel 模板资源。
- Rationale: 产品明确模板下载不依赖后端动态生成,减少接口依赖。
- Decision: 使用 `request_id` 作为客户端幂等键,并在创建前生成一次、提交重试时复用同一值。
- Rationale: 防止网络重试或重复点击创建多个相同批次。
- Decision: 部分成功、钱包余额不足和逐行业务失败均由后端任务结果表达;前端只展示计数和失败明细,不自行推断或回滚。
- Rationale: 钱包扣款和订单事务边界属于后端职责,前端不能可靠重建。
## Data Model
- Create request: `shop_id``payment_method``file`、可选 `voucher_file``request_id`
- Task identity: `task_id``task_no`
- Task status: `1=待处理``2=处理中``3=已完成``4=已失败``5=已取消`;部分成功仍属于已完成终态。
- Task summary: status, total count, success count, failed count, amount summary, timestamps and safe error summary when returned.
- Item result: row number, asset identifier, package code, row status and safe error reason.
- Item query: task ID, page, size and optional row status filter.
## Risks / Trade-offs
- Risk: 后端金额字段名称或单位未在产品文档中明确。
- Mitigation: API 类型和页面以接口实际返回字段为准,统一标注金额单位;实施前补齐字段契约,前端不自行计算汇总。
- Risk: 文件或凭证上传成功但任务创建请求失败,可能留下孤立对象。
- Mitigation: 创建失败时保留用户选择和错误提示;对象清理策略由后端存储生命周期或接口约定处理。
- Risk: 任务详情恢复时任务已过期、删除或用户失去权限。
- Mitigation: 按页面权限和接口错误处理展示对应状态,不重复创建原任务。
## Migration Plan
1. 确认批量订购任务摘要、金额和逐行结果字段契约。
2. 新增 API、类型、权限和静态模板资源。
3. 实现批量订购入口、文件上传、线下凭证上传和幂等提交。
4. 实现任务详情、公共状态展示、轮询和 `task_id` 恢复。
5. 实现逐行结果分页、状态筛选和失败明细展示。
6. 验证钱包余额不足、部分成功、重复提交、刷新恢复和权限组合。
## Open Questions
- 批量订购任务摘要中的金额字段名称、金额单位和金额分类需要以后端接口文档确认。
- 失败明细中的资产字段是统一 `asset_identifier`,还是按资产类型返回 `iccid` / `virtual_no`,需要以后端确认。
- `voucher_file` 是单文件、文件数组还是已上传对象存储 key需要与 multipart 接口契约确认。
- 批量订购入口的最终路由、菜单名称和权限编码需要产品/后端确认。

View File

@@ -0,0 +1,38 @@
# Change: 新增批量订购任务与逐行结果
## Why
运营需要为同一个代理商批量导入套餐订单,并统一指定整批支付方式。当前订单页面只支持单笔创建,无法上传批量订单文件、跟踪处理进度或定位逐行失败原因,批量操作也无法安全处理线下支付凭证和钱包余额不足场景。
## What Changes
- 新增批量订购套餐入口,支持选择代理商、整批支付方式和上传 Excel 文件。
- 支持线下支付时上传整批支付凭证;一个批次禁止混合支付方式。
- 模板下载使用前端静态文件,不依赖后端模板接口。
- 新增批量订购任务 API提交 multipart 字段 `shop_id``payment_method``file``voucher_file``request_id`
- 创建成功后展示任务号、任务状态、总数、成功数、失败数和金额汇总。
- 支持按 `task_id` 恢复任务详情,并轮询进行中的任务。
- 新增逐行结果查询,支持分页和状态筛选。
- 失败明细展示行号、资产、套餐编码和错误原因。
- 部分成功作为任务终态;钱包余额不足只影响对应行或后续行,不回滚已经成功的订单。
- 接入现有 RBAC 权限体系,批量订购创建、任务详情、逐行结果和模板下载权限独立控制。
## Impact
- Affected specs:
- `bulk-purchase-task`
- `order-management`
- `async-task-interaction`(复用现有公共异步任务状态与恢复规则)
- Affected code:
- `src/api/modules/bulkPurchase.ts`
- `src/types/api/bulkPurchase.ts`
- `src/views/order-management/bulk-purchase/index.vue`
- `src/components/business/BulkPurchaseCreateDialog.vue`(如采用独立弹窗)
- `src/views/order-management/order-list/index.vue`(批量入口)
- 静态 Excel 模板资源、路由、菜单、权限和国际化配置
- Dependencies:
- 后端提供三个批量订购接口及 multipart 鉴权契约。
- 对象存储上传能力支持批量订单文件和线下支付凭证上传。
- 需要确认批量订购任务的完整响应字段和并发轮询策略与公共异步任务规范一致。
- Breaking changes:
- 无。新增 API、页面、权限和任务能力不修改单笔订单创建接口。

View File

@@ -0,0 +1,133 @@
## ADDED Requirements
### Requirement: Bulk Purchase Creation Form
The admin frontend SHALL provide a bulk purchase form that binds one uploaded batch to one shop and one payment method.
#### Scenario: Select batch purchase inputs
- **GIVEN** 用户打开批量订购入口
- **WHEN** 用户填写批量订购表单
- **THEN** 页面 MUST require a target shop, one payment method, and an Excel file
- **AND** 页面 MUST NOT provide a way to mix payment methods within the same batch
#### Scenario: Show voucher upload for offline payment
- **GIVEN** 用户选择线下支付
- **WHEN** 页面渲染批量订购表单
- **THEN** 页面 MUST show the batch voucher upload control
- **AND** 页面 MUST require a completed voucher upload before task creation
#### Scenario: Hide voucher upload for wallet payment
- **GIVEN** 用户选择代理钱包支付
- **WHEN** 页面渲染或提交批量订购表单
- **THEN** 页面 MUST NOT submit a voucher file
- **AND** 页面 MUST clear or ignore a voucher selected before switching to wallet payment
### Requirement: Bulk Purchase File Upload And Creation
The admin frontend SHALL create a bulk purchase task through `POST /api/admin/bulk-purchases` using multipart form data.
#### Scenario: Create bulk purchase task
- **GIVEN** 用户已选择代理商、支付方式并完成文件上传
- **WHEN** 用户确认创建批量订购任务
- **THEN** 系统 MUST submit multipart fields `shop_id`, `payment_method`, `file`, and `request_id`
- **AND** 系统 MUST submit `voucher_file` when the payment method is offline
- **AND** 系统 MUST save the returned `task_id` and `task_no`
#### Scenario: Prevent creation during upload
- **GIVEN** Excel 文件或线下支付凭证仍在上传
- **WHEN** 用户点击创建任务
- **THEN** 页面 MUST prevent task creation
- **AND** 页面 MUST show the upload-in-progress state until all required uploads finish
#### Scenario: Download static template
- **GIVEN** 用户打开批量订购表单
- **WHEN** 用户点击模板下载
- **THEN** 页面 MUST download the packaged frontend static Excel template
- **AND** 页面 MUST NOT require a template-generation API request
### Requirement: Bulk Purchase Task Summary And Status
The admin frontend SHALL display the server-provided bulk purchase task summary and use the shared five-state async task semantics.
#### Scenario: Display task summary
- **GIVEN** 批量订购任务创建成功或详情接口返回任务
- **WHEN** 页面展示任务详情
- **THEN** 页面 MUST display task ID or task number, status, total count, success count, failed count, and returned amount summaries
- **AND** 页面 MUST use server-provided values without recalculating amounts or counts from the uploaded file
#### Scenario: Treat partial success as terminal completion
- **GIVEN** 任务已处理完成且同时存在成功行和失败行
- **WHEN** 页面展示任务状态
- **THEN** 页面 MUST display the task as a completed terminal task
- **AND** 页面 MUST display success and failed counts separately
- **AND** 页面 MUST NOT introduce a separate partial-success status
#### Scenario: Handle wallet insufficiency without rollback
- **GIVEN** 代理钱包余额不足导致部分或后续行无法创建订单
- **WHEN** 页面展示任务结果
- **THEN** 页面 MUST show the affected rows as failed with their returned reasons
- **AND** 页面 MUST preserve and display rows that were already successful
- **AND** 页面 MUST NOT attempt a frontend rollback
### Requirement: Bulk Purchase Task Recovery And Polling
The admin frontend SHALL recover a bulk purchase task by `task_id` and poll its summary while it is active.
#### Scenario: Query task summary
- **GIVEN** 用户需要加载批量订购任务
- **WHEN** 页面查询任务摘要
- **THEN** 系统 MUST call `GET /api/admin/bulk-purchases/{task_id}`
- **AND** 页面 MUST update the task summary from the response
#### Scenario: Recover task after refresh
- **GIVEN** 页面存在已保存的 active `task_id`
- **WHEN** 用户刷新页面或重新进入批量订购任务页
- **THEN** 页面 MUST query the existing task by `task_id`
- **AND** 页面 MUST NOT create another bulk purchase task
#### Scenario: Stop polling at terminal state
- **GIVEN** 批量订购任务状态为已完成、已失败或已取消
- **WHEN** 页面收到任务摘要
- **THEN** 页面 MUST stop automatic polling
- **AND** 页面 MUST retain the terminal summary and failure counts for viewing
### Requirement: Bulk Purchase Item Results
The admin frontend SHALL provide paginated and status-filterable row results for a bulk purchase task.
#### Scenario: Query item results
- **GIVEN** 用户打开批量订购任务结果
- **WHEN** 页面加载逐行结果
- **THEN** 系统 MUST call `GET /api/admin/bulk-purchases/{task_id}/items`
- **AND** 系统 MUST support `page`, `size`, and optional `status` query parameters
#### Scenario: Display failed item details
- **GIVEN** 逐行结果接口返回失败行
- **WHEN** 页面渲染结果表格
- **THEN** 页面 MUST display row number, asset identifier, package code, and safe error reason
- **AND** 页面 MUST NOT expose raw backend stack traces or technical error details
### Requirement: Bulk Purchase Permissions
The admin frontend SHALL gate bulk purchase creation, task viewing, item-result viewing, and template download with explicit permissions.
#### Scenario: Hide unauthorized bulk purchase operations
- **GIVEN** 当前用户缺少某项批量订购权限
- **WHEN** 页面渲染对应入口或操作
- **THEN** 页面 MUST NOT display or enable that operation
- **AND** direct API failure MUST NOT be treated as permission to continue

View File

@@ -0,0 +1,46 @@
## 1. Contract And Types
- [ ] 1.1 确认 `POST /api/admin/bulk-purchases` 的 multipart 字段类型、文件字段格式和响应结构。
- [ ] 1.2 确认 `GET /api/admin/bulk-purchases/{task_id}` 的任务摘要字段、状态值、金额字段和错误字段。
- [ ] 1.3 确认 `GET /api/admin/bulk-purchases/{task_id}/items` 的逐行字段、分页结构和状态筛选参数。
- [x] 1.4 新增批量订购 API service、请求类型、任务摘要类型和逐行结果类型。
- [x] 1.5 明确 `request_id` 的生成、保存和重复提交响应处理规则。
## 2. Upload And Create
- [x] 2.1 新增批量订购入口和表单,支持选择代理商、支付方式和 Excel 文件。
- [x] 2.2 提供前端静态 Excel 模板下载,并限制上传文件类型和必要的文件状态。
- [x] 2.3 线下支付时显示整批支付凭证上传,钱包支付时隐藏或清理凭证字段。
- [x] 2.4 复用订单列表已有 `VoucherUpload` 组件展示凭证上传进度,上传未完成时禁止创建任务。
- [x] 2.5 创建请求携带 `shop_id``payment_method``file`、必要的 `voucher_file``request_id`
- [x] 2.6 防止重复点击和重复提交,创建成功后保存 `task_id` 并进入任务详情。
## 3. Task Progress And Recovery
- [x] 3.1 展示任务号、状态、总数、成功数、失败数、金额汇总和安全错误摘要。
- [x] 3.2 按公共异步任务规则轮询待处理和处理中的任务,并在终态停止。
- [x] 3.3 页面刷新或通过任务 ID 重新进入时恢复任务详情,不重复创建批次。
- [x] 3.4 将部分成功展示为已完成终态,并同时展示成功数和失败数。
- [x] 3.5 展示钱包余额不足导致的逐行或后续行失败,不回滚已成功订单。
## 4. Item Results
- [x] 4.1 新增逐行结果表格,展示行号、资产、套餐编码、状态和错误原因。
- [x] 4.2 支持按逐行状态筛选和分页查询。
- [x] 4.3 失败原因优先展示后端安全错误文案,不直接展示底层技术错误。
- [x] 4.4 任务详情和逐行结果保持当前任务 ID、筛选条件和分页状态。
## 5. Permissions And Verification
- [x] 5.1 为批量订购创建、任务详情、逐行结果和模板下载配置独立权限。
- [ ] 5.2 验证一个批次只能提交一种支付方式,线下支付缺少凭证时不能提交。
- [ ] 5.3 验证重复提交复用 `request_id`,不会创建重复任务。
- [ ] 5.4 验证空文件、上传失败、任务失败、部分成功和钱包余额不足场景。
- [ ] 5.5 验证页面刷新恢复、任务终态停止轮询和逐行失败筛选。
- [x] 5.6 运行 `openspec validate add-bulk-purchase-upload-task --strict`、类型检查、lint 和构建。
## Implementation Notes
- 后端接口文档未随仓库提供1.1、1.2、1.3 仍需联调确认精确金额字段、逐行字段和 multipart 文件格式;当前类型使用可选兼容字段。
- 5.2 至 5.5 需要接入真实后端后进行手工回归,当前已完成前端校验、权限、幂等键、恢复、轮询和筛选逻辑。
- 支付凭证复用 `src/components/business/VoucherUpload.vue`,提交 `voucher_file` 时使用该组件上传后返回的文件 key。

View File

@@ -0,0 +1,41 @@
## Context
三个后台业务列表都需要展示相同的审批摘要,但各自保留既有退款、充值或换货操作。审批来源和业务处理状态均由后端列表接口返回,前端不请求单条审批详情来填充表格。
## Goals / Non-Goals
- Goals:
- 统一展示提交人、审批状态、当前审批人摘要和业务处理状态。
- 正确区分无审批、历史本地审批和企微审批。
- 在不增加逐行请求的前提下支持长摘要完整查看。
- Non-Goals:
- 不创建、修改或撤回审批流程。
- 不增加历史本地审批操作按钮。
- 不以 `approval_status` 推断 `processing_status`,或反向推断。
## Decisions
- Decision: 三个列表项模型复用相同名称和语义的审批摘要字段,字段由各自的列表接口直接返回。
- Decision: `approval_source=none` 时审批状态与当前审批人摘要均显示 `-``legacy` 时审批状态固定显示“历史审批”,当前审批人摘要显示 `-``wecom` 时直接展示 `approval_status_name``current_approver_summary`
- Decision: 审批人摘要仅负责展示,使用表格溢出省略与 tooltip 呈现完整文本。
- Decision: 业务处理状态单独读取 `processing_status_name`,不与审批状态混合或映射。
- Decision: 列表分页和刷新仅调用现有列表 API禁止为每条记录请求审批详情。
- Alternatives considered: 从单条详情或企微审批 API 批量补齐摘要。未采用,因为会引入 N+1 请求并与列表响应已提供的摘要字段重复。
## Risks / Trade-offs
- 后端遗漏摘要字段时信息不可用 -> 统一显示稳定占位,不影响原有列表和业务操作。
- 审批状态名称可能为空 -> 企微来源显示稳定占位,不自行翻译状态码。
- 审批人摘要长度不受控 -> 表格列使用溢出省略和 hover 完整文本。
## Migration Plan
1. 扩展三个列表项类型以保留审批摘要字段。
2. 在三个列表中添加四个展示列并遵循审批来源规则。
3. 验证 `none``legacy``wecom` 和处理状态为空的响应。
4. 验证刷新、分页未出现逐行审批详情请求。
5. 如需回滚,移除列表列与附加类型字段;不涉及数据迁移。
## Open Questions
- 无。

View File

@@ -0,0 +1,36 @@
# Change: 新增业务列表提交人与审批摘要
## Why
退款、代理充值和换货列表当前无法直接显示提交人、企微审批进度及业务处理进度。运营人员需要进入详情或依赖额外沟通才能判断记录由谁发起、审批进行到哪一步以及后续业务是否完成。
## What Changes
- 在退款列表、代理充值列表和换货列表增加“提交人”“审批状态”“当前审批人摘要”“业务处理状态”四列。
- 三个列表接口项统一支持 `submitter_name``approval_source``approval_status``approval_status_name``current_approver_summary``processing_status``processing_status_name`
- `approval_source=none` 时审批状态和当前审批人摘要显示 `-`
- `approval_source=legacy` 时审批状态显示“历史审批”,作为只读历史信息,不新增审批操作按钮。
- `approval_source=wecom` 时显示后端返回的企微审批状态及当前审批人摘要;超长审批人摘要使用省略显示并在悬浮时展示完整文本。
- 业务处理状态直接展示后端 `processing_status_name`,与审批状态分列展示。
- 列表仅使用列表响应中的审批摘要字段,翻页和刷新时不得为每行额外请求审批详情。
## Impact
- Affected specs:
- `order-management`
- `agent-recharge`
- `exchange-management`
- Affected code:
- `src/types/api/refund.ts`
- `src/types/api/agentRecharge.ts`
- `src/api/modules/exchange.ts`
- `src/views/finance/refund/index.vue`
- `src/views/finance/agent-recharge/index.vue`
- `src/views/asset-management/exchange-management/index.vue`
- API contracts:
- `GET /api/admin/refunds`
- `GET /api/admin/agent-recharges`
- `GET /api/admin/exchanges`
- Out of scope:
- 审批发起、撤回、审批操作或企微审批详情页。
- 前端根据业务状态或审批步骤推导审批结果和业务处理状态。

View File

@@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Agent Recharge List Approval Summary Contract
The `GET /api/admin/agent-recharges` list-item contract SHALL support `submitter_name`, `approval_source`, `approval_status`, `approval_status_name`, `current_approver_summary`, `processing_status`, and `processing_status_name`.
#### Scenario: Receive agent recharge approval summary fields
- **GIVEN** 后台代理充值列表接口返回充值记录
- **WHEN** 前端解析列表响应
- **THEN** 代理充值列表项类型 MUST preserve all approval and processing summary fields
- **AND** 页面 MUST NOT request an individual approval-detail API to populate the row
### Requirement: Agent Recharge List Approval And Processing Display
The agent recharge list SHALL display `提交人`, `审批状态`, `当前审批人摘要`, and `业务处理状态` as distinct columns based on backend summary fields.
#### Scenario: Display recharge with no approval source
- **GIVEN** 代理充值记录的 `approval_source=none`
- **WHEN** 页面渲染代理充值列表行
- **THEN** `审批状态` MUST display `-`
- **AND** `当前审批人摘要` MUST display `-`
#### Scenario: Display legacy recharge approval read-only
- **GIVEN** 代理充值记录的 `approval_source=legacy`
- **WHEN** 页面渲染代理充值列表行
- **THEN** `审批状态` MUST display `历史审批`
- **AND** 页面 MUST NOT add an approval operation button for that historical approval
#### Scenario: Display WeCom recharge approval summary
- **GIVEN** 代理充值记录的 `approval_source=wecom`
- **WHEN** 页面渲染代理充值列表行
- **THEN** `审批状态` MUST display backend `approval_status_name`
- **AND** `当前审批人摘要` MUST display backend `current_approver_summary`
- **AND** `业务处理状态` MUST independently display backend `processing_status_name`
#### Scenario: View long recharge approver summary
- **GIVEN** 代理充值记录的 `current_approver_summary` exceeds its table cell width
- **WHEN** 页面渲染当前审批人摘要列
- **THEN** 摘要 MUST be visually truncated in the cell
- **AND** 用户 MUST be able to view the complete backend text on hover
#### Scenario: Paginate recharges without per-row approval requests
- **WHEN** 用户切换代理充值列表页码或刷新列表
- **THEN** 页面 MUST use the agent recharge list API response for approval summaries
- **AND** 页面 MUST NOT issue approval-detail requests per recharge row

View File

@@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Exchange List Approval Summary Contract
The `GET /api/admin/exchanges` list-item contract SHALL support `submitter_name`, `approval_source`, `approval_status`, `approval_status_name`, `current_approver_summary`, `processing_status`, and `processing_status_name`.
#### Scenario: Receive exchange approval summary fields
- **GIVEN** 后台换货列表接口返回换货记录
- **WHEN** 前端解析列表响应
- **THEN** 换货列表项类型 MUST preserve all approval and processing summary fields
- **AND** 页面 MUST NOT request an individual approval-detail API to populate the row
### Requirement: Exchange List Approval And Processing Display
The exchange management list SHALL display `提交人`, `审批状态`, `当前审批人摘要`, and `业务处理状态` as distinct columns based on backend summary fields.
#### Scenario: Display exchange with no approval source
- **GIVEN** 换货记录的 `approval_source=none`
- **WHEN** 页面渲染换货列表行
- **THEN** `审批状态` MUST display `-`
- **AND** `当前审批人摘要` MUST display `-`
#### Scenario: Display legacy exchange approval read-only
- **GIVEN** 换货记录的 `approval_source=legacy`
- **WHEN** 页面渲染换货列表行
- **THEN** `审批状态` MUST display `历史审批`
- **AND** 页面 MUST NOT add an approval operation button for that historical approval
#### Scenario: Display WeCom exchange approval summary
- **GIVEN** 换货记录的 `approval_source=wecom`
- **WHEN** 页面渲染换货列表行
- **THEN** `审批状态` MUST display backend `approval_status_name`
- **AND** `当前审批人摘要` MUST display backend `current_approver_summary`
- **AND** `业务处理状态` MUST independently display backend `processing_status_name`
#### Scenario: View long exchange approver summary
- **GIVEN** 换货记录的 `current_approver_summary` exceeds its table cell width
- **WHEN** 页面渲染当前审批人摘要列
- **THEN** 摘要 MUST be visually truncated in the cell
- **AND** 用户 MUST be able to view the complete backend text on hover
#### Scenario: Paginate exchanges without per-row approval requests
- **WHEN** 用户切换换货列表页码或刷新列表
- **THEN** 页面 MUST use the exchange list API response for approval summaries
- **AND** 页面 MUST NOT issue approval-detail requests per exchange row

View File

@@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Refund List Approval Summary Contract
The `GET /api/admin/refunds` list-item contract SHALL support `submitter_name`, `approval_source`, `approval_status`, `approval_status_name`, `current_approver_summary`, `processing_status`, and `processing_status_name`.
#### Scenario: Receive refund approval summary fields
- **GIVEN** 后台退款列表接口返回退款记录
- **WHEN** 前端解析列表响应
- **THEN** 退款列表项类型 MUST preserve all approval and processing summary fields
- **AND** 页面 MUST NOT request an individual approval-detail API to populate the row
### Requirement: Refund List Approval And Processing Display
The refund management list SHALL display `提交人`, `审批状态`, `当前审批人摘要`, and `业务处理状态` as distinct columns based on backend summary fields.
#### Scenario: Display refund with no approval source
- **GIVEN** 退款记录的 `approval_source=none`
- **WHEN** 页面渲染退款列表行
- **THEN** `审批状态` MUST display `-`
- **AND** `当前审批人摘要` MUST display `-`
#### Scenario: Display legacy refund approval read-only
- **GIVEN** 退款记录的 `approval_source=legacy`
- **WHEN** 页面渲染退款列表行
- **THEN** `审批状态` MUST display `历史审批`
- **AND** 页面 MUST NOT add an approval operation button for that historical approval
#### Scenario: Display WeCom refund approval summary
- **GIVEN** 退款记录的 `approval_source=wecom`
- **WHEN** 页面渲染退款列表行
- **THEN** `审批状态` MUST display backend `approval_status_name`
- **AND** `当前审批人摘要` MUST display backend `current_approver_summary`
- **AND** `业务处理状态` MUST independently display backend `processing_status_name`
#### Scenario: View long refund approver summary
- **GIVEN** 退款记录的 `current_approver_summary` exceeds its table cell width
- **WHEN** 页面渲染当前审批人摘要列
- **THEN** 摘要 MUST be visually truncated in the cell
- **AND** 用户 MUST be able to view the complete backend text on hover
#### Scenario: Paginate refunds without per-row approval requests
- **WHEN** 用户切换退款列表页码或刷新列表
- **THEN** 页面 MUST use the refund list API response for approval summaries
- **AND** 页面 MUST NOT issue approval-detail requests per refund row

View File

@@ -0,0 +1,33 @@
## 1. List Contracts
- [x] 1.1 扩展退款、代理充值和换货列表项类型,支持 `submitter_name``approval_source``approval_status``approval_status_name``current_approver_summary``processing_status``processing_status_name`
- [x] 1.2 确认三个列表 API 使用列表响应直接提供上述字段,不新增逐条审批详情查询。
## 2. Shared Summary Behavior
- [x] 2.1 实现审批来源展示规则:`none` 显示 `-``legacy` 显示“历史审批”且只读,`wecom` 显示后端审批状态名称。
- [x] 2.2 将当前审批人摘要限制为单行省略,并提供完整文本悬浮提示。
- [x] 2.3 将业务处理状态独立显示为后端 `processing_status_name`,缺失时显示稳定占位内容。
## 3. Refund List
- [x] 3.1 在退款列表增加提交人、审批状态、当前审批人摘要、业务处理状态列。
- [x] 3.2 保留退款既有审批与业务操作,不为历史审批记录增加新操作按钮。
## 4. Agent Recharge List
- [x] 4.1 在代理充值列表增加提交人、审批状态、当前审批人摘要、业务处理状态列。
- [x] 4.2 保留代理充值既有确认支付、拒绝等操作,不为历史审批记录增加新操作按钮。
## 5. Exchange List
- [x] 5.1 在换货列表增加提交人、审批状态、当前审批人摘要、业务处理状态列。
- [x] 5.2 保留换货既有流程和资产筛选逻辑,不为历史审批记录增加新操作按钮。
## 6. Verification
- [x] 6.1 验证三类列表在 `approval_source=none` 时审批状态和当前审批人摘要均显示 `-`
- [x] 6.2 验证 `approval_source=legacy` 时显示“历史审批”且没有新增审批操作,`wecom` 时显示后端审批状态和当前审批人摘要。
- [x] 6.3 验证长当前审批人摘要会省略显示并能通过悬浮查看完整文本。
- [x] 6.4 验证三个列表的业务处理状态独立显示,分页和刷新不触发逐行审批详情请求。
- [x] 6.5 运行相关类型检查、lint 或构建验证。

View File

@@ -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/batchshop_ids1-500去重+ business_owner_account_id字段缺失=400null=清空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.tsFilePurpose 增加 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月份/业务用户组.md7 个组端点 OpenAPI
- AUG26-003 前端对接说明(店铺侧端点)
## 待确认
- 成员选择器复用 GET /api/admin/shops/business-owner-candidates启用平台账号若组成员可包含非业务员平台账号需后端补充平台账号列表端点。
- 导入任务与行级结果字段名file_key、status、items[] 等)以生成文档为准,提案按对接说明先行对齐。

View File

@@ -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。创建时 code1-64与 name 必填编码未删除组内唯一且创建后不可修改business_line 可空standard/smart/othersort/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 控制显隐

View File

@@ -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/batchbody 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-urlpurpose=shop_import拿预签名 URL 直传15 分钟有效)-> POST /api/admin/shops/business-owner-importsfile_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 且明细为空

View File

@@ -0,0 +1,34 @@
## 1. API Contract 层
- [x] 1.1 新增 src/types/api/businessUserGroup.ts组项/分页/创建/更新/删除/成员设置与结果类型
- [x] 1.2 新增 src/api/modules/businessUserGroup.ts封装 7 个组端点DELETE 带 body需请求层支持
- [x] 1.3 扩展 src/types/api/shop.ts列表筛选 business_user_group_id/business_line/ungrouped响应只读组字段批量交接与导入任务/行级结果类型
- [x] 1.4 扩展 src/api/modules/shop.tsbatchUpdateBusinessOwner、createBusinessOwnerImport、getBusinessOwnerImportDetail、getBusinessOwnerImportList
- [x] 1.5 扩展 src/api/modules/storage.tsFilePurpose 增加 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

View File

@@ -0,0 +1,60 @@
# Change: 退款佣金回扣改为独立回溯明细(前端适配)
## Why
后端调整了退款佣金回扣口径:退款不再把原佣金整单置为「已失效」,而是原佣金行保持「已发放」不变,另新增一条独立负数、不可提现的「回溯明细」行。对应前端接口说明(简短版)要求:
- 佣金记录列表新增 `status` 查询参数(含 5 回溯)与行级 `source` 字段(`original` / `clawback`)。
- 新增佣金记录详情接口 `GET /api/admin/shops/{shop_id}/commission-records/{id}?source=clawback`
- 新增导出场景 `scene=commission_record`(后端已进白名单)。
- 两个必改点:列表行 key 必须用 `source + ':' + id`(原佣金与回溯 ID 空间独立,同一页会重复);渲染需支持负数金额(标红)与「不可提现」标识。
前端当前实现基于旧语义(`ShopCommissionRecordItem``source`,列表行 key 直接用 `id`,退款后原行被视为已失效),需按新口径适配。
## What Changes
- 类型层:`ShopCommissionRecordItem` 新增 `source``original_commission_id``refund_id``refund_no``withdrawable``clawback_records``clawback_total_amount`;新增回溯摘要与详情类型;`CommissionRecordQueryParams.status` 支持 5回溯
- API 层:新增 `getShopCommissionRecordDetail(shopId, id, source?)``source` 省略时按原佣金处理。
- 列表页(代理侧「我的佣金」与管理侧「代理资金概览 > 佣金明细」):行 key 改为 `source + ':' + id`;回溯行展示「回溯」状态与「不可提现」标识;负数金额与可为负的 `balance_after` 标红;`status` 筛选项新增「回溯」;详情入口必须携带该行 `source`
- 详情展示:原佣金详情展示 `clawback_records` 摘要与 `clawback_total_amount`;回溯详情展示来源 `original_commission`;越权/不存在统一提示「佣金明细不存在」,不做存在性判断。
- 导出:`ExportTaskScene` 增加 `commission_record`;新增导出佣金记录场景页、路由、菜单与 i18n佣金记录列表导出入口只提交受支持的筛选 key`shop_id` / `status` / `commission_source` / `order_no`)。
- 字段兜底:后端新增字段为 `omitempty`,前端取 `?? []` / `?? 0`,金额单位为分。
## Not In Scope
- 不实现后端接口、不改数据库、不改审批实例数据。
- 既有字段无改名/改类型;`commission-stats``commission-daily-stats` 口径未变,本次不动。
- 时间筛选属另一 Change`add-export-time-filter-standards`),本次不涉及。
- 语义提醒:退款不再写 `status=4`,旧假设「退款后原行变已失效」已失效,需在文案/注释体现。
## Impact
- Affected specs: `commission-management``export-task-management`
- Affected code:
- `src/types/api/commission.ts`
- `src/api/modules/commission.ts`
- `src/views/commission-management/my-commission/index.vue`
- `src/views/commission-management/agent-fund-overview/index.vue`
- `src/types/api/exportTask.ts`
- `src/config/constants/exportTask.ts`
- `src/views/asset-management/export-task-management/export-commission-record/index.vue`
- `src/router/routes/asyncRoutes.ts``src/router/routesAlias.ts``src/locales/langs/{zh,en}.json`
- Dependencies:
- `docs/产品迭代8月份/frontend-api-simple.md`(后端契约)
- 后端已把 `scene=commission_record` 加入导出场景白名单
- 需确认导出场景权限编码与场景页是否必需(见 Open Questions
- Breaking changes:
- 列表行 key 由 `id` 改为 `source + ':' + id`;未携带 `source` 的详情请求会命中原佣金行。
## 接口确认结果(来自 OpenAPI
- 详情响应 `DtoShopCommissionRecordDetailResp``source``record``DtoShopCommissionRecordItem`)、`clawback_records`(仅原佣金返回)、`original_commission`(仅回溯详情返回)。
- `DtoShopCommissionRecordItem` 新增 `source``released_at``withdrawable`booleannullable仅回溯行返回且恒 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`

View File

@@ -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 区分越权与不存在两种原因

View File

@@ -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` 中已填写的项

View File

@@ -0,0 +1,27 @@
## 1. 类型与 API 层commission-api
- [x] 1.1 `CommissionStatus` 补充 5回溯修正 `CommissionRecordQueryParams.status` 过期注释并支持回溯值
- [x] 1.2 `ShopCommissionRecordItem` 新增 `source``original_commission_id``refund_id``refund_no``withdrawable``clawback_records``clawback_total_amount`
- [x] 1.3 新增回溯摘要与佣金记录详情类型(原佣金详情含 `clawback_records`,回溯详情含 `original_commission`
- [x] 1.4 `CommissionService` 新增 `getShopCommissionRecordDetail(shopId, id, source?)`
## 2. 列表页commission-records-list
- [x] 2.1 行 key 改为 `source + ':' + id``my-commission``agent-fund-overview` 佣金明细)
- [x] 2.2 回溯行展示「回溯」状态与「不可提现」标识
- [x] 2.3 负数金额与可为负的 `balance_after` 标红渲染
- [x] 2.4 `status` 筛选项新增「回溯」
- [x] 2.5 列表新增详情入口,且请求必须携带该行 `source`
## 3. 详情展示commission-record-detail
- [x] 3.1 原佣金详情展示 `clawback_records` 摘要与 `clawback_total_amount`
- [x] 3.2 回溯详情展示来源 `original_commission`
- [x] 3.3 越权/不存在统一提示「佣金明细不存在」,不作为存在性判断依据
## 4. 导出commission-record-export
- [x] 4.1 `ExportTaskScene` 增加 `commission_record` 并补场景配置
- [x] 4.2 新增导出佣金记录场景页、路由、菜单与中英文案
- [x] 4.3 佣金记录列表新增导出入口,`query` 仅提交 `shop_id` / `status` / `commission_source` / `order_no`
## 5. 验证
- [x] 5.1 运行 `npm run build`(含 `vue-tsc --noEmit`
- [x] 5.2 运行 `npm run check:encoding`
- [x] 5.3 运行 `openspec.cmd validate add-commission-clawback-records --strict`

View File

@@ -0,0 +1,88 @@
## Context
员工代收款是 8 月迭代新增的财务能力,普通员工与超级管理员共用同一套接口,靠登录态区分数据范围。后台管理端需要新增三类页面,并复用已有的表格、搜索、详情与上传组件。`docs/admin-openapi.yaml` 未随仓库提供,接口字段以后端契约(需求文档 + 创建订单接口 OpenAPI 片段)为准,类型集中在一个文件便于联调收敛。
## Goals / Non-Goals
**Goals**
- 在财务管理下提供收款方式、员工代收款账单、核销申请三类页面。
- 复用 `ArtTableFullScreen``ArtSearchBar``ArtTableHeader``ArtTable``DetailPage``VoucherUpload``PaymentVoucherDialog``useCheckedColumns` 等既有组件与约定。
- 复用既有企微审批场景配置能力,仅新增业务类型。
**Non-Goals**
- 不实现后端接口、数据库、Worker、企微回调。
- 不实现 H5/C 端页面与支付流程。
- 不新增导出任务场景(需求文档未要求)。
## Decisions
### 接口契约
| 能力 | 关键字段 |
|---|---|
| 统一响应 | `{ code, data, msg, timestamp }` |
| 列表分页 | 账单列表返回 `{ items, total, page, size }`,取数处对 `items` / `list` / `records` 做兼容 |
| 收款方式 | `{ id, code, name, sort, enabled, remark, created_at, updated_at }`,列表接口返回 `{ items, page, size, total }`,支持 `page` / `page_size` / `enabled` / `keyword` 筛选 |
| 账单列表项 | `{ id, source_type, source_type_name, source_no, debtor_snapshot, customer_snapshot, receivable_amount, received_amount, reserved_amount, remaining_amount, status, status_name, approval_pending, closed_reason, created_at, updated_at }` |
| 账单详情 | `data``{ bill, refunds, allocations, applications }``refunds` 为退款冲销(`refund_id``source_order_id``refund_amount``reduced_amount``bill_receivable_amount``outcome_name``allocations` 为账单侧分摊(含 `application_id` / `application_status_name` / `attempt_id``applications` 内嵌该申请的 `attempts` |
| 账单统计 | `{ receivable_total, received_total, unsettled_total, pending_bill_count }` |
| 账单筛选 | `page``page_size``source_type``source_no``status``debtor_account_id``customer_id``created_from``YYYY-MM-DD`)、`created_to``YYYY-MM-DD` |
| 核销申请请求体 | `{ payment_method_id, paid_amount, paid_at, payer_name, external_transaction_no, payment_voucher_keys, remark, allocations: [{ bill_id, amount }], acting_reason }` |
| 核销申请列表项 | `{ id, applicant_account_id, acting_operator_id, payment_method_id, payment_method_name, paid_amount, payer_name, external_transaction_no, status, status_name, terminal_reason, decided_at, created_at, updated_at }` |
| 核销申请详情 | `data``{ application, allocations, attempts }``attempts` 保存每次提交的完整材料快照,重新提交不清空历史 |
账单状态为数字枚举 `0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭;申请状态为数字枚举 `0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭;两者展示均优先使用后端 `status_name`
### 页面与路由组织
`/finance` 下新增:
| 路由 | 页面 | 说明 |
|---|---|---|
| `/finance/employee-collection/bills` | 员工代收款账单 | 统计 + 列表 |
| `/finance/employee-collection/bills/detail/:id` | 账单详情 | 隐藏菜单 |
| `/finance/employee-collection/applications` | 核销申请 | 列表 + 创建 |
| `/finance/employee-collection/applications/detail/:id` | 核销申请详情 | 隐藏菜单 |
| `/finance/employee-collection/payment-methods` | 收款方式管理 | 仅超管 |
账单与申请拆分为独立菜单,符合项目「列表页 + 详情页」的既有组织方式,避免单页堆叠过多交互。账单列表的「店铺」筛选用远程搜索复用 `ShopService.getShops`,与退款列表一致。
账单详情与核销申请详情保持只读:页面只在顶部保留「返回」导航,创建核销申请、关闭账单、修改并重新提交等操作入口统一放在列表页的操作列,详情页不出现业务操作按钮。
### 权限编码
新增 `src/config/constants/augustIteration.ts`,沿用 `模块:动作` 风格,例如 `employee_collection:bill_close``employee_collection:application_create`。页面级 `permissions` 用于菜单可见性,按钮级编码用于 `hasAuth()` / `v-permission`
### 金额、时间与附件
- 金额统一以「分」传输,展示时通过 `fenToYuan` / `formatCurrency` 转换,与退款、代理充值保持一致。
- 所有时间字段统一通过 `formatDateTime` 格式化为 `YYYY-MM-DD HH:mm:ss`,不在模板中直接输出后端原始时间字符串。
- 附件仅返回对象 Key`payment_voucher_keys`),展示复用 `PaymentVoucherDialog`,由预签名下载接口换取访问地址。
### 核销申请分摊
创建申请时按账单逐条录入核销金额,并填写付款事实(付款金额、付款方名称、付款时间、外部交易流水号),前端校验:
- 至少选择 1 张账单,最多 N 张;
- 单张核销金额不得大于账单未核销金额,且大于 0
- 付款金额(`paid_amount`,分)不得小于各账单分摊之和(允许存在差额);
- 超管代办时 `acting_reason` 必填;
- 付款凭证至少 1 个 Key。
提交成功后自动发起企微审批;仅已驳回申请可再次进入弹窗修改并重新提交,重新提交生成新的审批实例,历史审批记录只读展示。未配置企微审批场景时后端返回 503前端展示「企微审批场景未配置请联系管理员」并保留已填内容。
### 企微审批场景
`WecomBusinessType` 增加 `employee_collection_approval`,企微审批场景页面下拉新增「员工代收款审批」。模板控件同步、字段查询、字段映射保存全部复用既有 `WecomService`
### 订单付款凭证规则
线下订单创建时,满足「平台账号(`user_type` 为 1 或 2操作 + 非赠送套餐 + 实际收款金额大于 0」条件的订单会生成员工代收款账单此时 `payment_voucher_key` 非必填,字段结构不变,付款凭证改在核销申请中提交。前端以当前登录账号类型、所选套餐是否赠送、套餐有效价格(`effective_retail_price` / `suggested_retail_price` / `retail_price`)判断是否展示提示并放宽必填;赠送套餐或其他非平台账号的线下订单仍需上传凭证。
## Risks / Trade-offs
- **接口字段以契约文档为准**`docs/admin-openapi.yaml` 未入库,字段来自需求文档与创建订单接口片段;类型集中在一个文件,便于联调时收敛修改。
- **员工/超管同接口**:前端不做数据范围过滤,仅做展示与操作可见性控制,数据隔离以后端为准。
- **关闭账单与审批中申请**:前端依据 `approval_pending` 与状态字段禁用关闭按钮,最终一致性以后端校验为准。

View File

@@ -0,0 +1,25 @@
# Change: 员工代收款功能前端对接
## Why
8 月产品迭代新增「员工代收款」能力:员工线下代收款项后,通过创建核销申请、经企业微信审批完成账单核销;超级管理员可维护收款方式、查看全部账单并关闭账单。后端接口已按 `docs/产品迭代8月份/员工代收款功能简介.md` 落地,后台管理端目前缺少收款方式管理、账单列表与核销申请页面,普通员工与超级管理员的分类视图、以及线下订单付款凭证规则尚未接入。
本次已按后端实际契约对齐字段:列表响应统一使用 `items` / `total` / `page` / `size`;账单状态(`0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭)与申请状态(`0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭)为数字枚举;收款方式列表返回 `{ items, page, size, total }` 并支持 `enabled` / `keyword` 筛选;核销申请请求体使用 `paid_amount``paid_at``payer_name``external_transaction_no``payment_voucher_keys``allocations`
## What Changes
- 新增 `employee-collection` 能力的前端类型与服务封装:收款方式 4 个接口、员工代收款账单 4 个接口、核销申请 4 个接口。
- 新增「收款方式管理」页面(仅超级管理员):列表 + 新增/编辑弹窗 + 删除;已被核销申请引用的方式不可删除、不可修改 `code`(未被引用时可改),可停用。
- 新增「员工代收款账单」页面:应收/已核销/未核销/待处理账单统计 + 列表 + 详情;普通员工仅见本人账单,超级管理员可见全部;存在审批中申请时账单不可关闭,关闭必须填写原因。
- 新增「核销申请」页面:列表 + 详情(含分摊账单、付款凭证、付款信息与全部审批尝试记录)+ 创建/重新提交弹窗;一笔线下收款可核销 1N 张账单,填写付款金额、付款方名称、付款时间、外部交易流水号、选择收款方式并上传付款凭证;提交后自动发起企微审批;仅已驳回申请可修改并重新提交,重新提交生成新的审批实例且历史记录不被覆盖。
- 扩展企业微信审批场景:新增 `employee_collection_approval` 业务类型,复用既有企微应用列表、模板控件同步与业务字段查询接口。
- 调整订单创建:由平台账号操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单,该场景 `payment_voucher_key` 改为非必填,付款凭证改在核销申请中提交,字段结构保持不变。
- 附件接口仅返回对象 Key统一通过系统既有预签名下载接口展示。
- **不实现后端接口、数据库、Worker、企微回调****不实现 H5/C 端页面**。
## Impact
- Affected specs: `employee-collection`
- Affected code: `src/types/api/employeeCollection.ts``src/api/modules/employeeCollection.ts``src/views/finance/employee-collection/*``src/router/routes/asyncRoutes.ts``src/router/routesAlias.ts``src/config/constants/augustIteration.ts``src/types/api/wecom.ts``src/views/settings/wecom/scenes/index.vue``src/views/order-management/order-list/index.vue``src/locales/langs/{zh,en}.json`
- Dependencies: `docs/产品迭代8月份/员工代收款功能简介.md`
- Contract note: `docs/admin-openapi.yaml` 未随仓库提供,字段以需求文档描述的后端契约与创建订单接口的 OpenAPI 片段为准;类型集中在单一文件,联调时便于收敛。

View File

@@ -0,0 +1,155 @@
## ADDED Requirements
### Requirement: 收款方式管理
超级管理员 MUST 能够维护线下收款方式,包括名称、编码、排序、状态与备注;普通员工只能看到启用的收款方式。收款方式列表接口 MUST 返回 `{ items, page, size, total }`,并 MUST 支持 `enabled``keyword` 筛选。已被核销申请引用的收款方式 MUST NOT 被删除;引用后修改稳定编码 MUST 被后端拒绝,前端 MUST 展示错误提示。
#### Scenario: 超级管理员新增收款方式
- **GIVEN** 超级管理员已登录并拥有收款方式新增权限
- **WHEN** 其填写名称、唯一编码、排序、状态与备注并提交
- **THEN** 系统 MUST 调用新增接口并在成功后刷新列表
- **AND** 新增成功后 MUST 清空并关闭弹窗
#### Scenario: 删除被引用的收款方式
- **GIVEN** 某收款方式已被业务引用
- **WHEN** 超级管理员尝试删除该方式
- **THEN** 前端 MUST 阻止删除或展示后端返回的业务错误
- **AND** MUST 提示改为停用
#### Scenario: 修改收款方式编码
- **GIVEN** 超级管理员打开编辑弹窗
- **WHEN** 其修改稳定编码并提交
- **THEN** 前端 MUST 提交最新编码
- **AND** 若该方式已被核销申请引用,后端拒绝时前端 MUST 展示错误提示
### Requirement: 员工代收款账单统计
账单页面 MUST 展示应收金额、已核销金额、未核销金额与待处理账单数量四项统计,数据 MUST 来自 `GET /api/admin/employee-collection-bills/statistics`,字段为 `receivable_total``received_total``unsettled_total``pending_bill_count`;前端 MUST NOT 通过遍历当前分页数据自行计算。
#### Scenario: 加载账单统计
- **GIVEN** 用户进入员工代收款账单页面
- **WHEN** 页面初始化或筛选条件变化
- **THEN** 前端 MUST 调用账单统计接口
- **AND** MUST 将后端返回的「分」按元格式化后展示应收、已核销与未核销金额
### Requirement: 员工代收款账单列表与详情
账单列表响应 MUST 为 `{ items, total, page, size }`,前端 MUST 兼容 `items` / `list` / `records` 等列表字段。列表 MUST 支持按来源(`source_type`)、来源单号(`source_no`)、账单状态、客户/店铺(`customer_id`)与创建时间(`created_from` / `created_to``YYYY-MM-DD`)筛选,并展示账单编号、来源、关联单号、负责员工、客户/店铺、应收金额、已核销金额、未核销金额与状态。账单状态 MUST 为数字枚举:`0` 待核销、`1` 部分核销、`2` 已核销、`3` 已关闭,展示 MUST 优先使用后端 `status_name`。账单详情响应 MUST 为 `{ bill, refunds, allocations, applications }``refunds` MUST 展示退款金额、冲减应收、冲销前应收与处理结果,`applications` MUST 可展开查看该申请的审批尝试记录。
#### Scenario: 普通员工查看账单
- **GIVEN** 普通员工已登录
- **WHEN** 其打开账单列表
- **THEN** 列表 MUST 只展示后端返回的本人账单数据
- **AND** MUST NOT 展示仅超管可见的操作入口
#### Scenario: 打开账单详情
- **GIVEN** 用户拥有账单详情权限
- **WHEN** 其点击账单号或详情操作
- **THEN** 前端 MUST 跳转账单详情页并加载对应账单(详情数据取自 `data.bill`
- **AND** 详情 MUST 展示退款冲销、核销分摊与关联核销申请
### Requirement: 关闭账单
超级管理员 MUST 能够关闭账单,且关闭原因必填(最多 500 字符)。仅待核销或部分核销账单可关闭;存在审批中的核销申请(`approval_pending` 为真)时,账单 MUST NOT 被关闭。
#### Scenario: 存在审批中申请时关闭账单
- **GIVEN** 账单存在审批中的核销申请
- **WHEN** 用户查看该账单操作
- **THEN** 关闭入口 MUST 被禁用或不可见
- **AND** MUST 展示不可关闭的原因提示
#### Scenario: 关闭原因必填
- **GIVEN** 账单可关闭
- **WHEN** 超级管理员打开关闭弹窗并留空原因提交
- **THEN** 前端 MUST 阻止提交并提示填写原因
### Requirement: 创建核销申请
员工 MUST 能够使用一笔线下收款核销 1N 张账单。创建申请 MUST 提交收款方式 `payment_method_id`、付款金额 `paid_amount`(分,大于 0、付款方名称 `payer_name`、付款时间 `paid_at`(带时区 RFC3339、外部交易流水号 `external_transaction_no`、付款凭证 `payment_voucher_keys`15 个对象 Key与账单分摊 `allocations`。超级管理员代办时 MUST 填写 `acting_reason`,本人办理 MUST NOT 提交该字段。提交成功后系统 MUST 自动发起企业微信审批。
#### Scenario: 一笔收款核销多张账单
- **GIVEN** 用户选择了多张可核销账单
- **WHEN** 其录入各账单核销金额、选择收款方式并填写付款事实后提交
- **THEN** 前端 MUST 校验各账单核销金额大于 0 且不超过账单未核销余额
- **AND** MUST 以 `allocations: [{ bill_id, amount }]` 提交账单分摊明细
#### Scenario: 付款金额小于核销合计
- **GIVEN** 用户已录入各账单核销金额
- **WHEN** 其填写的付款金额小于核销合计即提交
- **THEN** 前端 MUST 阻止提交并提示付款金额不能小于核销合计
#### Scenario: 缺少付款凭证
- **GIVEN** 用户已选择账单与收款方式
- **WHEN** 其未上传任何付款凭证即提交
- **THEN** 前端 MUST 阻止提交并提示上传付款凭证
#### Scenario: 缺少付款事实
- **GIVEN** 用户已选择账单与收款方式
- **WHEN** 其未填写付款方名称、付款时间或外部交易流水号即提交
- **THEN** 前端 MUST 阻止提交并提示补齐必填项
#### Scenario: 超管代办未填写原因
- **GIVEN** 超级管理员以代办身份创建申请
- **WHEN** 其未填写 `acting_reason` 即提交
- **THEN** 前端 MUST 阻止提交并提示填写代办原因
#### Scenario: 企微审批场景未配置
- **GIVEN** 企业微信审批场景尚未配置
- **WHEN** 用户提交核销申请
- **THEN** 前端 MUST 展示后端返回的 503 提示「企微审批场景未配置,请联系管理员」
- **AND** MUST 保留用户已填写的内容且不产生申请数据
### Requirement: 核销申请列表与详情
核销申请列表响应 MUST 为 `{ items, page, size, total }`MUST 支持按状态、收款方式与创建时间筛选;列表项 MUST 包含 `payment_method_name``paid_amount``status``status_name``created_at`,状态 MUST 优先展示后端 `status_name`。申请详情响应 MUST 为 `{ application, allocations, attempts }``attempts` MUST 按提交顺序展示全部审批尝试记录,包含付款金额、付款方、流水号、付款凭证与审批意见。
#### Scenario: 查看审批历史
- **GIVEN** 申请存在多次审批尝试记录
- **WHEN** 用户打开申请详情
- **THEN** 详情 MUST 按提交顺序展示每次尝试的提交材料与审批状态
- **AND** 历史材料 MUST NOT 因重新提交而被覆盖
### Requirement: 核销申请重新提交
仅已驳回(`status``2`)的申请 MUST 允许修改并重新提交;重新提交 MUST 生成新的企业微信审批实例,且历史审批记录 MUST NOT 被覆盖。重新提交入口 MUST 位于核销申请列表的操作列,详情页 MUST 只读且 MUST NOT 展示业务操作按钮(仅保留返回导航)。
#### Scenario: 重新提交被驳回申请
- **GIVEN** 申请状态为已驳回
- **WHEN** 用户在核销申请列表点击「修改并重新提交」并修改账单分摊、收款方式、付款事实或付款凭证后提交
- **THEN** 前端 MUST 调用修改接口重新提交
- **AND** 成功后 MUST 刷新详情并展示新的审批实例状态
#### Scenario: 非驳回申请不可修改
- **GIVEN** 申请处于审批中或已通过
- **WHEN** 用户查看核销申请列表
- **THEN** 修改并重新提交入口 MUST 不可见或不可用
#### Scenario: 详情页只读
- **GIVEN** 用户打开账单详情或核销申请详情
- **WHEN** 页面渲染完成
- **THEN** 页面 MUST NOT 展示创建核销申请、关闭账单或修改并重新提交等业务操作按钮
- **AND** MUST 只保留返回导航
### Requirement: 企业微信审批场景配置
企业微信审批场景 MUST 支持 `employee_collection_approval` 业务类型,复用既有的应用列表、模板控件同步、业务字段查询与字段映射保存接口。
#### Scenario: 配置员工代收款审批场景
- **GIVEN** 超级管理员打开企微审批场景页面
- **WHEN** 其选择业务类型「员工代收款审批」
- **THEN** 前端 MUST 使用 `employee_collection_approval` 调用模板同步、字段查询与保存接口
### Requirement: 附件预签名展示
附件接口 MUST 只返回对象存储 Key`payment_voucher_keys`);前端 MUST 通过系统既有的预签名下载接口获取实际访问地址后再展示。
#### Scenario: 查看付款凭证
- **GIVEN** 申请包含付款凭证 Key
- **WHEN** 用户点击查看付款凭证
- **THEN** 前端 MUST 先批量换取预签名地址
- **AND** 图片 MUST 支持预览,非图片 MUST 支持查看或下载
### Requirement: 订单付款凭证规则调整
由平台账号(`user_type` 为 1 或 2操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单;该场景 `payment_voucher_key` MUST 变为非必填,付款凭证改在核销申请中提交,订单字段结构 MUST 保持不变。其余线下订单 MUST 继续要求付款凭证。
#### Scenario: 线下订单生成代收款账单
- **GIVEN** 当前登录账号为平台账号,所选套餐非赠送且实际收款金额大于 0支付方式为线下支付
- **WHEN** 用户创建该订单
- **THEN** 前端 MUST 不再强制要求上传付款凭证
- **AND** MUST 提示付款凭证将在核销申请中提交
#### Scenario: 赠送套餐或非平台账号的线下订单
- **GIVEN** 所选套餐为赠送套餐,或当前账号非平台账号,或实际收款金额为 0
- **WHEN** 用户以线下支付方式创建订单
- **THEN** 前端 MUST 继续要求上传付款凭证

View File

@@ -0,0 +1,48 @@
## 1. Contract and API Types
- [x] 1.1 新增 `src/types/api/employeeCollection.ts`:收款方式、账单、核销申请、统计、查询参数与请求/响应类型;列表统一使用 `items` / `total` / `page` / `size`
- [x] 1.2 定义账单状态(`0` 待核销 / `1` 部分核销 / `2` 已核销 / `3` 已关闭)与申请状态(`0` 审批中 / `1` 已通过 / `2` 已驳回 / `3` 已撤销或已关闭)数字枚举,并保留后端 `*_name` 展示字段。
- [x] 1.3 金额字段以「分」传输;附件字段使用 `payment_voucher_keys` 对象存储 Key 数组,展示复用预签名下载接口。
- [x] 1.4 新增 `src/api/modules/employeeCollection.ts` 并在 `src/api/modules/index.ts``src/types/api/index.ts` 导出。
- [x] 1.5 按后端实际契约收敛字段:账单详情为 `{ bill, refunds, allocations, applications }`,核销申请详情为 `{ application, allocations, attempts }`,付款金额/付款方/付款时间/外部交易流水号以 `paid_amount` / `payer_name` / `paid_at` / `external_transaction_no` 提交。
## 2. Permissions, Routes, and Menu
- [x] 2.1 新增 `src/config/constants/augustIteration.ts`,集中声明页面与按钮权限编码。
- [x] 2.2 在 `src/router/routesAlias.ts``src/router/routes/asyncRoutes.ts` 的财务管理下新增账单、核销申请、收款方式路由。
- [x] 2.3 在 `src/locales/langs/zh.json``en.json``menus.financialManagement` 下补充菜单文案。
## 3. Payment Methods Page
- [x] 3.1 实现收款方式列表(名称、编码、排序、状态、备注、创建时间);接口返回 `{ items, page, size, total }`,复用 `ArtTableFullScreen` / `ArtSearchBar` / `ArtTableHeader` / `ArtTable`
- [x] 3.2 实现新增/编辑弹窗;编辑时允许提交 `code`,被核销申请引用时由后端拒绝并提示。
- [x] 3.3 实现删除操作,被引用的方式给出提示并引导停用。
## 4. Bills Pages
- [x] 4.1 实现账单统计卡片(应收、已核销、未核销、待处理账单数量),数据来自 `GET /api/admin/employee-collection-bills/statistics`
- [x] 4.2 实现账单列表与筛选(来源、来源单号、状态、客户/店铺、创建时间范围),展示账单号、来源、关联单号、负责员工、客户/店铺、应收/已核销/未核销金额与状态。
- [x] 4.3 实现账单详情,展示账单信息、退款冲销、核销分摊与关联核销申请(关联申请可展开查看审批尝试记录)。
- [x] 4.4 实现超管关闭账单弹窗,关闭原因必填;存在审批中申请时禁用关闭并给出说明。
- [x] 4.5 列表「创建核销申请」入口按权限与账单状态控制可用性。
## 5. Applications Pages
- [x] 5.1 实现核销申请列表与筛选(状态、收款方式、创建时间),展示申请编号、收款方式、付款金额、状态与提交时间。
- [x] 5.2 实现创建/重新提交弹窗:选择 1N 张可核销账单、按账单录入分摊金额、填写付款金额/付款方名称/付款时间/外部交易流水号、选择收款方式并上传付款凭证。
- [x] 5.3 超管代办时必须填写 `acting_reason`,否则禁止提交。
- [x] 5.4 实现申请详情,展示申请信息、分摊账单、付款凭证与全部审批尝试记录;历史记录只读,不被重新提交覆盖。
- [x] 5.5 仅已驳回申请展示「修改并重新提交」,提交成功后生成新的审批实例并刷新详情。
- [x] 5.6 统一处理 503「企微审批场景未配置」等业务错误保留用户已填内容。
## 6. WeCom Scene and Order Rule
- [x] 6.1 扩展 `WecomBusinessType` 增加 `employee_collection_approval`,并在企微审批场景页面新增可选业务类型。
- [x] 6.2 场景保存复用既有 `inspectTemplate``getBusinessFields``saveScene` 接口,无需新增企微接口。
- [x] 6.3 调整线下订单创建:平台账号操作、非赠送且实际收款金额大于 0 时 `payment_voucher_key` 非必填,字段结构不变。
## 7. Verification
- [x] 7.1 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`,确保类型与编码通过。
- [x] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。 — 已核验:应用/账单/收款方式页 v-permission 按钮、bills 关闭操作 hasAuth(billClose)+canCloseBill、表单 isActing=isSuperAdmin 控制代办字段
- [x] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。 — 已核验ApplicationFormDialog 用 fenToYuan/yuanToFendetail.vue 用 PaymentVoucherDialog 预览凭证getErrorMessage 提取后端 msg含 503 未配置场景)兜底

View File

@@ -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`1100/`content`12000/`frequency`/起止时间必填。
- 范围三选器(店铺/设备类型/卡类型)空数组 = 全量,回显「全部」。
- 起止时间用 datetimerange提交转 ISO 8601`starts_at`/`ends_at``ends_at` 不得早于 `starts_at`
- `priority` 数字输入01000000`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`)。

View File

@@ -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`110000/ `page_size`1100/ `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` 12000 字符、`title` 1100 字符、`priority` 01000000。
- `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`)以后端菜单配置为准。
- 列表分页上限与「按最近更新时间倒序」以文档为准,联调核对后端行为。

View File

@@ -0,0 +1,117 @@
## ADDED Requirements
### Requirement: H5 运营弹窗配置列表
后台 MUST 提供 H5 运营弹窗配置的分页列表GET /api/admin/h5-popup-configurations支持 `page`110000/ `page_size`1100/ `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` 12000 字符、`title` 1100 字符、`priority` 01000000。
#### 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 出现删除操作,仅提供启用/停用

View File

@@ -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` 12000、`title` 1100、`priority` 01000000更新成功版本 +1 且旧版本通知不被改写。
- [x] 1.3 `enable`/`disable` 请求体 `{ id }`,返回更新后的 DtoH5PopupConfigurationResponse。
- [x] 1.4 列表 `page` 110000、`page_size` 1100、`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` 被后端拒绝。

View File

@@ -0,0 +1,50 @@
## Context
退款和代理充值已经接入企微审批,但部分历史记录在审批流程上线前创建,列表中的 `approval_status` 为空。这些记录需要由运营人员手动触发一次审批补发,才能进入企微审批链路。
## Goals / Non-Goals
- Goals: 提供代理充值和退款的历史审批补发入口。
- Goals: 通过路径参数指定目标记录,并复用后端返回的完整记录与审批状态。
- Goals: 由明确的状态规则控制入口展示,后端做最终资格校验。
- Non-Goals: 不在前端实现审批提交、审批回调或审批引擎。
- Non-Goals: 不修改历史记录的业务字段、金额或支付/退款结果。
- Non-Goals: 不提供批量自动补发。
## Decisions
- Decision: 两个模块分别新增 `triggerApproval(id)` 服务方法,返回完整业务记录并保留审批字段。
- Rationale: 两个接口契约一致,成功后页面需要立即展示最新审批状态,完整记录可直接用于刷新。
- Decision: 代理充值仅在 `approval_status` 为空且 `status` 不属于已完成、已驳回、已关闭时显示补发入口;退款仅在 `status=待审批``approval_status` 为空时显示补发入口。
- Rationale: 与业务规则一致,避免对已进入审批或已终结的记录重复补发;后端仍做最终资格校验。
- Decision: 引入独立按钮权限 `agent_recharge:trigger_approval``refund:trigger_approval`
- Rationale: 补发审批是财务相关敏感操作,需与现有确认支付、拒绝、重新申请权限区分。
- Decision: 补发审批与现有“确认支付/拒绝”和“重新申请”操作共存。
- Rationale: 它们承担不同职责;补发审批只是把历史记录推进企微审批,不改变后续人工确认或重新申请的流程。
## Risks / Trade-offs
- Risk: 代理充值中 `status=已支付``已退款` 等非终态记录是否允许补发,需要后端最终确认。
- Mitigation: 前端按约定的审批状态与状态排除规则展示入口,后端对不允许的记录返回错误并稳定展示。
- Risk: 接口可能对部分状态返回拒绝。
- Mitigation: 前端处理后端错误信息,不将失败记录标记为已补发。
- Risk: 重复点击可能触发多次审批提交。
- Mitigation: 提交期间锁定按钮并禁用重复触发;后端应保证幂等或返回明确的已存在审批提示。
## Migration Plan
1. 确认两个 `trigger-approval` 接口的响应字段与现有 `AgentRecharge``Refund` 类型一致。
2. 新增服务方法和按钮权限。
3. 在列表接入补发审批入口及资格判断。
4. 联调补发成功、接口拒绝、权限缺失、审批状态为空与状态不符合条件等场景。
5. 验证与现有确认支付、拒绝、重新申请操作不冲突。
## Open Questions
- 后端对可补发记录的最终状态校验以及幂等性需确认。
- 两个接口是否都需要独立权限码,还是复用现有审批/财务权限,需与后端权限配置对齐。

View File

@@ -0,0 +1,39 @@
# Change: 补发历史线下代理充值审批与补发历史退款审批
## Why
历史线下代理充值记录和历史退款申请在企微审批流程上线前创建,列表中这些记录的审批状态为空,运营人员无法为它们补发审批流程。需要为这两类业务提供“补发审批”入口,调用后端触发审批接口,将历史记录纳入企微审批。
## What Changes
-`AgentRechargeService` 新增 `triggerApproval(id)`,调用 `POST /api/admin/agent-recharges/{id}/trigger-approval`
-`RefundService` 新增 `triggerApproval(id)`,调用 `POST /api/admin/refunds/{id}/trigger-approval`
- 在代理充值列表为 `approval_status` 为空且 `status` 不属于已完成、已驳回、已关闭的充值记录增加“补发审批”操作。
- 在退款列表为 `status=待审批``approval_status` 为空的退款申请增加“补发审批”操作。
- 引入权限 `agent_recharge:trigger_approval``refund:trigger_approval`,仅对有权限的平台账号展示操作。
- 补发成功后刷新列表并展示返回的最新审批状态;失败时展示后端错误信息。
- 不改变现有“确认支付”“拒绝”“重新申请”等操作的资格和职责。
## Impact
- Affected specs:
- `agent-recharge`
- `refund-management`
- Affected code:
- `src/api/modules/agentRecharge.ts`
- `src/api/modules/refund.ts`
- `src/types/api/agentRecharge.ts`
- `src/types/api/refund.ts`
- `src/views/finance/agent-recharge/agentRechargeActions.ts`
- `src/views/finance/agent-recharge/index.vue`
- `src/views/finance/refund/index.vue`
- API contracts:
- `POST /api/admin/agent-recharges/{id}/trigger-approval`
- `POST /api/admin/refunds/{id}/trigger-approval`
- Dependencies:
- 后端按文档返回完整业务记录及当前审批状态。
- 后端负责校验记录是否可补发审批,前端仅控制入口展示并处理后端拒绝。
- Out of scope:
- 企微审批的发起、撤回、通过、驳回或删除动作本身。
- 修改历史记录的业务字段、金额或支付/退款结果。
- 自动判断并批量补发历史审批。

View File

@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: Agent Recharge Historical Approval Resend API
The agent recharge service SHALL expose a historical approval resend operation through `POST /api/admin/agent-recharges/{id}/trigger-approval`.
#### Scenario: Resend approval for a historical offline recharge
- **GIVEN** 一个需要补发审批的历史线下代理充值记录 `id`
- **WHEN** 前端调用 `POST /api/admin/agent-recharges/{id}/trigger-approval`
- **THEN** 请求 MUST 在路径参数中携带该充值记录 `id`
- **AND** 成功响应 MUST 被解析为完整的 `AgentRecharge` 记录,并保留 `approval_provider``approval_instance_id``approval_status``approval_status_name` 字段
### Requirement: Agent Recharge Historical Approval Resend Entry
The agent recharge list page SHALL provide a `补发审批` action only for recharge records whose `approval_status` is empty and whose business `status` is not completed, rejected, or closed, and SHALL gate it by permission.
#### Scenario: Show resend action for an eligible recharge
- **GIVEN** 平台账号拥有 `agent_recharge:trigger_approval` 权限
- **AND** 充值记录的 `approval_status` 为空(`null``undefined`
- **AND** 充值记录的 `status` 不属于已完成3、已驳回6、已关闭4
- **WHEN** 页面渲染代理充值列表
- **THEN** 页面 MUST 为该记录显示“补发审批”操作
#### Scenario: Hide resend action when approval status is present
- **GIVEN** 充值记录的 `approval_status` 不为空
- **WHEN** 页面渲染该记录
- **THEN** 页面 MUST NOT 显示“补发审批”操作
#### Scenario: Hide resend action for terminal recharge statuses
- **GIVEN** 充值记录的 `status` 为已完成3、已驳回6或已关闭4
- **WHEN** 页面渲染该记录
- **THEN** 页面 MUST NOT 显示“补发审批”操作
#### Scenario: Hide resend action without permission
- **GIVEN** 当前账号不拥有 `agent_recharge:trigger_approval` 权限
- **WHEN** 页面渲染代理充值记录
- **THEN** 页面 MUST NOT 显示“补发审批”操作
#### Scenario: Resend approval succeeds
- **GIVEN** 用户对符合条件的充值记录点击“补发审批”
- **WHEN** 接口返回 `code=0`
- **THEN** 页面 MUST 显示成功提示并刷新列表
- **AND** 刷新后的记录 MUST 展示接口返回的最新审批状态
#### Scenario: Resend approval fails
- **GIVEN** 接口返回非零 `code` 或请求失败
- **WHEN** 用户触发“补发审批”
- **THEN** 页面 MUST 展示后端返回的错误信息
- **AND** 页面 MUST NOT 将记录标记为已补发审批

View File

@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: Refund Historical Approval Resend API
The refund service SHALL expose a historical approval resend operation through `POST /api/admin/refunds/{id}/trigger-approval`.
#### Scenario: Resend approval for a historical refund
- **GIVEN** 一个需要补发审批的历史退款申请 `id`
- **WHEN** 前端调用 `POST /api/admin/refunds/{id}/trigger-approval`
- **THEN** 请求 MUST 在路径参数中携带该退款申请 `id`
- **AND** 成功响应 MUST 被解析为完整的 `Refund` 记录,并保留 `approval_provider``approval_instance_id``approval_status``approval_status_name` 字段
### Requirement: Refund Historical Approval Resend Entry
The refund list page SHALL provide a `补发审批` action only for refund records whose `status` is pending approval and whose `approval_status` is empty, and SHALL gate it by permission.
#### Scenario: Show resend action for an eligible refund
- **GIVEN** 平台账号拥有 `refund:trigger_approval` 权限
- **AND** 退款申请的 `status` 为待审批1
- **AND** 退款申请的 `approval_status` 为空(`null``undefined`
- **WHEN** 页面渲染退款列表
- **THEN** 页面 MUST 为该记录显示“补发审批”操作
#### Scenario: Hide resend action when refund status is not pending
- **GIVEN** 退款申请的 `status` 不为待审批1
- **WHEN** 页面渲染该记录
- **THEN** 页面 MUST NOT 显示“补发审批”操作
#### Scenario: Hide resend action when approval status is present
- **GIVEN** 退款申请的 `approval_status` 不为空
- **WHEN** 页面渲染该记录
- **THEN** 页面 MUST NOT 显示“补发审批”操作
#### Scenario: Hide resend action without permission
- **GIVEN** 当前账号不拥有 `refund:trigger_approval` 权限
- **WHEN** 页面渲染退款记录
- **THEN** 页面 MUST NOT 显示“补发审批”操作
#### Scenario: Resend approval succeeds
- **GIVEN** 用户对符合条件的退款申请点击“补发审批”
- **WHEN** 接口返回 `code=0`
- **THEN** 页面 MUST 显示成功提示并刷新列表
- **AND** 刷新后的记录 MUST 展示接口返回的最新审批状态
#### Scenario: Resend approval fails
- **GIVEN** 接口返回非零 `code` 或请求失败
- **WHEN** 用户触发“补发审批”
- **THEN** 页面 MUST 展示后端返回的错误信息
- **AND** 页面 MUST NOT 将记录标记为已补发审批

View File

@@ -0,0 +1,23 @@
## 1. 类型与 API 契约
- [x] 1.1 在 `src/api/modules/agentRecharge.ts` 新增 `triggerApproval(id)` 方法
- [x] 1.2 在 `src/api/modules/refund.ts` 新增 `triggerApproval(id)` 方法
- [x] 1.3 确认 `AgentRecharge``Refund` 类型包含 `approval_provider``approval_source``approval_instance_id``approval_status``approval_status_name`
## 2. 代理充值补发审批入口
- [x] 2.1 在 `agentRechargeActions.ts` 增加“补发审批”动作及资格判断
- [x] 2.2 在代理充值列表接入权限 `agent_recharge:trigger_approval` 与成功/失败处理
- [x] 2.3 补发成功后刷新列表并展示最新审批状态
## 3. 退款补发审批入口
- [x] 3.1 在退款列表 `getActions` 增加“补发审批”动作及资格判断
- [x] 3.2 接入权限 `refund:trigger_approval` 与成功/失败处理
- [x] 3.3 补发成功后刷新列表并展示最新审批状态
## 4. 校验与验证
- [x] 4.1 运行 `openspec validate add-historical-approval-resend --strict`
- [x] 4.2 运行 ESLint、类型检查并修复
- [ ] 4.3 手工验证有/无审批实例、线上/线下充值、权限开关等场景

View File

@@ -0,0 +1,70 @@
## Context
系统已经存在 `ArtNotification` 顶部通知组件,但组件中的通知、消息和待办列表都是静态数据,顶部通知按钮处于注释状态。新的通知能力需要同时服务顶部快速查看和完整通知中心,并确保用户在任一入口执行已读操作后未读数与列表状态一致。
## Goals / Non-Goals
- Goals: 提供余额预警、临期提醒、审批结果和系统告警统一的站内通知入口。
- Goals: 在顶部快速查看最近 10 条通知,并提供进入通知中心的入口。
- Goals: 支持通知分类、类型、严重级别和已读状态筛选。
- Goals: 将通知点击导航限制在前端认可的业务目标内,禁止任意 URL 跳转。
- Goals: 单条已读、全部已读、抽屉和通知中心共享同一份未读状态。
- Non-Goals: 不实现 C 端通知页面。
- Non-Goals: 不实现推送通道或服务端通知生成规则。
- Non-Goals: 不让前端根据通知正文推断跳转地址。
## Decisions
- Decision: 通知铃铛放在顶部全局导航的设置按钮和用户头像菜单附近。
- Rationale: 该区域已承载全局设置和用户级入口,适合放置跨页面可访问的通知入口,也符合产品指定位置。
- Decision: 顶部抽屉只加载最近 10 条,完整通知中心使用独立路由 `/notifications`
- Rationale: 顶部入口保持轻量,筛选和完整历史查询放在独立页面,避免挤占全局导航空间。
- Decision: 未读数使用独立接口获取,单条通知和全部已读成功后立即更新本地共享状态,并以接口返回值为最终结果。
- Rationale: 未读数是全局状态,不能依赖当前抽屉或列表的局部数量推导。
- Decision: 点击通知先调用单条已读接口,再调用目标接口,根据返回的受控 route name/route params 跳转。
- Rationale: 已读状态必须在导航前落库,目标由后端业务引用解析,前端不执行通知携带的任意 URL。
- Decision: 未知 `ref_type` 或目标接口无可用目标时只展示通知正文和状态,不跳转。
- Rationale: 保证安全,同时让无法关联页面的系统通知仍然可读。
- Decision: `unread-summary` 为顶部抽屉分类提供数据,`notifications` 为通知中心筛选列表提供数据。
- Rationale: 顶部快速查看与完整列表的数据量和筛选职责不同,避免顶部加载完整历史数据。
## Data Contract
- Notification item: id, title, content, category, type, severity, read status, created time and optional `ref_type`/reference ID.
- Unread count: non-negative integer; frontend formats it as `0`, `1`-`99` or `99+`.
- Unread summary: category counts and recent notification items, limited to the latest 10 items for the drawer.
- Notification list: paginated items plus total count, with category, type, severity and read-state filters.
- Target response: controlled internal route information, such as route name/path and route params; no arbitrary executable URL.
## Risks / Trade-offs
- Risk: 后端通知字段或枚举名称与文档不一致。
- Mitigation: 在任务阶段先确认字段契约,类型层保留稳定的可选字段和未知值占位展示。
- Risk: 用户在多个标签页同时读通知,单页本地未读数短暂不一致。
- Mitigation: 操作成功后刷新未读数和当前列表;跨标签实时同步不作为本期强制目标。
- Risk: 目标记录已删除或用户权限发生变化。
- Mitigation: 目标接口返回不可跳转时只展示正文,并处理权限/不存在状态,不回退到任意 URL。
## Migration Plan
1. 确认通知列表、摘要、未读数和目标接口字段及枚举。
2. 新增通知 API service、类型和共享状态。
3. 将顶部通知按钮放入设置/头像区域,接入未读数和最近通知抽屉。
4. 新增 `/notifications` 路由及通知中心页面。
5. 实现筛选、单条已读、全部已读和受控目标跳转。
6. 删除或替换 `ArtNotification` 中的静态 mock 数据,并验证四类通知展示一致。
## Open Questions
- 通知中心是否需要分页参数名称 `page/page_size`,以及默认每页数量需要后端确认。
- `category``type``severity``read_status` 的枚举值及展示名称需要后端确认。
- 目标接口返回 route name、path 还是 route key + params需要与路由菜单契约确认。
- 未读数是否需要页面进入时定时刷新或仅在打开抽屉/完成操作时刷新,本期默认按接口调用时机刷新。
- 通知中心菜单权限和按钮权限编码需要后端权限表确认。

View File

@@ -0,0 +1,46 @@
# Change: 顶部通知铃铛与站内通知中心
## Why
当前顶部通知组件仍使用硬编码 mock 数据,通知按钮也未启用,运营人员无法统一查看余额预警、临期提醒、审批结果和系统告警。需要建立真实的站内通知数据链路,并在全局导航和通知中心之间保持未读状态同步。
## What Changes
- 在顶部全局导航的设置按钮与用户头像入口区域增加通知铃铛。
- 铃铛展示未读数量,统一格式为 `0``1``99``99+`
- 点击铃铛展示最近 10 条通知抽屉,支持全部、审批、临期、同步/系统分类。
- 抽屉提供进入 `/notifications` 通知中心的入口。
- 新增通知中心页面,支持分类、通知类型、严重级别、已读状态筛选和全部已读。
- 点击通知时先调用标记已读接口,再根据受控 `ref_type` 获取目标并跳转;未知目标只展示通知正文,不执行任意 URL 跳转。
- 接入未读数、未读摘要、通知列表、单条已读和全部已读接口。
- 替换现有 `ArtNotification` 硬编码 mock 数据实现,并复用现有顶部布局和设置/头像区域的视觉位置。
## Impact
- Affected specs:
- `notification-center`
- Affected code:
- `src/components/core/layouts/art-header-bar/index.vue`
- `src/components/core/layouts/art-notification/index.vue`
- `src/components/core/layouts/art-notification/style.scss`
- `src/views/notifications/index.vue`
- `src/api/modules/notification.ts`
- `src/types/api/notification.ts`
- 路由、菜单和通知相关权限配置
- API contracts:
- `GET /api/admin/notifications/unread-count`
- `GET /api/admin/notifications/unread-summary`
- `GET /api/admin/notifications`
- `PUT /api/admin/notifications/{id}/read`
- `PUT /api/admin/notifications/read-all`
- `GET /api/admin/notifications/{id}/target`
- Dependencies:
- 后端按当前用户返回通知数据、未读统计和受控跳转目标。
- 后端明确通知分类、类型、严重级别、已读状态和 `ref_type` 枚举。
- 目标接口不得返回可被前端直接执行的任意外部 URL。
- Out of scope:
- C 端 `/api/c/v1/notifications` 接口和页面。
- 前端创建、编辑或删除通知。
- 浏览器推送、WebSocket 或轮询之外的实时推送机制。
- Breaking changes:
- 现有 `ArtNotification` mock 通知数据和无效的空操作按钮将被真实通知数据及接口行为替换。

View File

@@ -0,0 +1,81 @@
## ADDED Requirements
### Requirement: Global Notification Bell
The admin frontend SHALL provide a global notification bell in the top navigation near the settings and user avatar entries.
#### Scenario: Display unread count
- **GIVEN** 用户已登录后台
- **WHEN** 顶部导航加载未读通知数量
- **THEN** 铃铛 MUST call `GET /api/admin/notifications/unread-count`
- **AND** 数量 MUST display as `0`, `1` through `99`, or `99+`
#### Scenario: Open recent notification drawer
- **WHEN** 用户点击顶部通知铃铛
- **THEN** 页面 MUST display the latest 10 notifications from the unread summary contract
- **AND** 抽屉 MUST provide 全部、审批、临期、同步/系统分类
- **AND** 抽屉 MUST provide an entry to `/notifications`
### Requirement: Notification Center Filtering
The admin frontend SHALL provide a `/notifications` notification center with server-backed filtering.
#### Scenario: Filter notifications
- **WHEN** 用户打开通知中心或调整筛选条件
- **THEN** 页面 MUST support category, notification type, severity and read-state filters
- **AND** 页面 MUST load results from `GET /api/admin/notifications`
- **AND** 页面 MUST display notification title, content, category, severity, read state and created time
#### Scenario: Mark all notifications read
- **WHEN** 用户点击全部已读
- **THEN** 页面 MUST call `PUT /api/admin/notifications/read-all`
- **AND** 页面 MUST refresh the unread count and visible notification read states after success
### Requirement: Notification Read State Synchronization
The notification drawer and notification center SHALL keep read state and unread count synchronized with the backend.
#### Scenario: Mark a notification read
- **WHEN** 用户点击一条未读通知
- **THEN** 页面 MUST call `PUT /api/admin/notifications/{id}/read` before navigation
- **AND** 页面 MUST refresh or update the shared unread count after the read request succeeds
#### Scenario: Preserve read state after refresh
- **GIVEN** 用户已完成单条已读或全部已读操作
- **WHEN** 用户关闭抽屉、切换页面或刷新通知中心
- **THEN** 页面 MUST use the latest backend response rather than restoring stale local unread state
### Requirement: Controlled Notification Navigation
The admin frontend SHALL navigate from notifications only through controlled internal targets.
#### Scenario: Navigate to a valid notification target
- **GIVEN** 通知包含受支持的 `ref_type` 或业务引用
- **WHEN** 单条已读成功后页面请求 `GET /api/admin/notifications/{id}/target`
- **THEN** 页面 MUST navigate only to a target recognized by the frontend route table
- **AND** 页面 MUST preserve the target route parameters returned by the backend
#### Scenario: Handle an unknown target safely
- **GIVEN** 通知的 `ref_type` 未知、目标已删除或目标无权限
- **WHEN** 页面处理通知点击
- **THEN** 页面 MUST not navigate to an arbitrary URL
- **AND** 页面 MUST still display the notification content and a stable unavailable-target state
### Requirement: Consistent Notification Categories
The notification drawer and notification center SHALL present balance warnings, expiry reminders, approval results and system alerts through the same notification data contract.
#### Scenario: Display supported notification types
- **GIVEN** 后端返回余额预警、临期提醒、审批结果或系统告警
- **WHEN** 用户查看抽屉或通知中心
- **THEN** 页面 MUST display each item using its backend category, type and severity
- **AND** 页面 MUST not replace the item with hardcoded mock notification content

View File

@@ -0,0 +1,39 @@
## 1. Contract And Types
- [x] 1.1 按产品文档落地通知项、未读统计、未读摘要、分页列表和受控目标响应字段。
- [x] 1.2 按产品文档落地分类、通知类型、严重级别、已读状态和 `ref_type` 兼容类型。
- [x] 1.3 新增通知中心菜单权限 `notifications:view`、单条已读权限 `notifications:read`、全部已读权限 `notifications:read_all`
- [x] 1.4 新增 `notification.ts` API service覆盖未读数、摘要、列表、单条已读、全部已读和目标接口。
- [x] 1.5 新增通知实体、筛选参数、分页响应和目标响应类型。
## 2. Global Header And Drawer
- [x] 2.1 在顶部设置按钮与用户头像入口区域增加通知铃铛。
- [x] 2.2 实现未读数量格式化为 `0``1``99``99+`
- [x] 2.3 顶部抽屉加载最近 10 条通知,并按全部、审批、临期、同步/系统分类展示。
- [x] 2.4 替换 `ArtNotification` 静态 mock 数据,接入真实摘要接口。
- [x] 2.5 提供进入 `/notifications` 通知中心的入口,并保证桌面端和移动端布局可用。
## 3. Notification Center
- [x] 3.1 新增 `/notifications` 路由和通知中心页面。
- [x] 3.2 支持分类、通知类型、严重级别和已读状态筛选。
- [x] 3.3 展示通知标题、正文、严重级别、分类、时间和已读状态。
- [x] 3.4 实现全部已读,并在成功后同步未读数和列表状态。
- [x] 3.5 处理空列表、加载失败、权限失败和未知枚举值的稳定展示。
## 4. Read And Controlled Navigation
- [x] 4.1 点击通知时先调用 `PUT /api/admin/notifications/{id}/read`
- [x] 4.2 已读成功后调用 `GET /api/admin/notifications/{id}/target` 获取受控目标。
- [x] 4.3 仅允许跳转到后端返回且前端路由表认可的内部目标。
- [x] 4.4 未知 `ref_type`、目标不存在或无权限时只展示正文,不执行任意 URL 跳转。
- [x] 4.5 抽屉和通知中心的已读操作后刷新共享未读数。
## 5. Verification
- [x] 5.1 验证余额、临期、审批和系统通知在抽屉及通知中心展示一致。
- [x] 5.2 验证未读数在单条已读、全部已读、抽屉关闭和页面刷新后与接口结果一致。
- [x] 5.3 验证通知点击不会跳转到任意外部或未注册 URL。
- [x] 5.4 验证未知目标通知仍可查看正文且不会阻塞其他通知操作。
- [x] 5.5 运行类型检查、lint、相关测试和构建。

View File

@@ -0,0 +1,17 @@
# Change: Add purchased package names to the order list
## Why
Order list records already include their purchased line items, but operators cannot identify the purchased packages without opening the order detail. Showing these names next to the order number makes multi-package orders immediately understandable.
## What Changes
- Place the existing `资产标识符` column immediately after `订单编号`, followed by a `购买套餐名称` column in the order list.
- Render `items[].package_name` in the API-returned order, joining multiple item names with the Chinese enumeration separator `、`.
- Show a placeholder when an order has no returned items or no usable package name.
## Impact
- Affected specs: `order-list-purchased-package-names`
- Affected code: `src/views/order-management/order-list/index.vue`
- Dependencies: `GET /api/admin/orders` returns `items` with each item's `package_name`.

View File

@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Order list purchased package name display
The order list SHALL display `资产标识符` immediately after `订单编号`, followed by a `购买套餐名称` column. The package-name column MUST render package names from the current row's `items` in their API-returned order and MUST join multiple names with `、`.
#### Scenario: Order contains multiple purchased packages
- **GIVEN** an order response whose `items` contains package names `正式套餐` and `加油包33`
- **WHEN** the order is rendered in the order list
- **THEN** `资产标识符` MUST be positioned between `订单编号` and `购买套餐名称`
- **AND** the `购买套餐名称` column MUST display `正式套餐、加油包33`
- **AND** the order-number column MUST retain its existing navigation behavior
#### Scenario: Order has no available package names
- **GIVEN** an order response with `items=null`, an empty item list, or item records without usable names
- **WHEN** the order is rendered in the order list
- **THEN** the `购买套餐名称` column MUST display the standard placeholder

View File

@@ -0,0 +1,10 @@
## 1. Implementation
- [x] 1.1 Place `资产标识符` after order number and add the purchased-package-name column immediately after it in the order-list column chooser.
- [x] 1.2 Render package names from `Order.items` in API-returned order, separating multiple values with `、`.
- [x] 1.3 Render a placeholder for null, empty, or unnamed order items without altering the order-number link behavior.
## 2. Verification
- [x] 2.1 Verify a single item renders its package name and multiple items render `套餐A、套餐B`.
- [x] 2.2 Run targeted format, type, and lint checks.

View File

@@ -0,0 +1,51 @@
# Design: 套餐真流量预警前端对接
## 背景
后端已按 `docs/产品迭代8月份/套餐真流量预警_API_前端简版.md` 在测试环境上线 6 个接口released。前端需要新增规则配置与预警记录两个页面并接入既有权限与导出任务体系。本设计记录关键决策。
## 决策
### 1. 页面挂载位置:套餐管理分组
规则数据源是套餐商品(`package_id`),预警记录也按套餐筛选,因此两个页面都挂在既有 `/package-management` 路由分组下,与套餐列表、代理系列授权平级。
- 路由:`/package-management/traffic-alert-rules``/package-management/traffic-alerts`
- 详情:`/package-management/traffic-alerts/detail/:id`(隐藏路由)
### 2. 导出走专用接口 + 专用弹窗,不复用 ExportTaskCreateDialog
既有 `ExportTaskCreateDialog` 调用通用 `POST /api/admin/export-tasks`,而真流量预警导出是专用端点 `POST /api/admin/package-traffic-alerts/export`,请求体为 `format` + 与列表一致的一组筛选参数。因此:
- 新建轻量弹窗(或扩展 `ExportTaskCreateDialog` 支持自定义 submit展示“基于当前筛选条件全量导出不仅导出当前分页数据”
- 提交成功后提示“导出任务已创建”,并提供跳转既有导出任务列表页的入口,下载走既有能力
- 导出任务列表若按场景过滤,需要新增场景配置;场景枚举值联调时与后端确认后落入 `EXPORT_TASK_SCENE_CONFIG`
### 3. 权限模型
- 遵循八月迭代约定:权限编码集中定义在 `AUGUST_PERMISSIONS.packageTrafficAlert`,页面/按钮通过 `v-permission` + `useAuth().hasAuth` 引用
- 页面级:`rules_view` / `records_view` 控制菜单与按钮可见性(后端菜单权限同源)
- 按钮级:规则创建/修改(含启停)、记录详情、导出分别独立编码
- 403 策略:接口对无权限账号统一返回 403列表接口 403 时提示“无权限访问”;详情越权按资源不可见处理(复用现有 404 类提示文案),不暴露资源存在性
### 4. 阈值输入校验
- 前端 `ElInputNumber``min=1``max=100``precision=2`
- 创建时 `threshold_percent` 必填;修改时三字段均可选(后端按传入字段更新)
- `remark` 创建/修改均限制 500 字符(`maxlength` + 计数器)
### 5. 快照与归属变化展示
- 列表与详情的业务字段均为触发时快照,仅 `business_user_group_names` 为当前值
- 详情中 `shop_changed_since_trigger` / `owner_changed_since_trigger``true` 时,用 `ElAlert`info提示归属已变化并展示当前店铺/业务员与快照值
- 空值约定:无店铺/业务员时字段可能为 `null` 或空数组,统一渲染 `-`(业务员数组 join 展示,空数组显示 `-`
### 6. 规则列表的 real_data_mb 展示
- `real_data_mb` 为套餐商品当前真流量额度,仅用于配置校验展示(如“按 80% 约对应 x GB”不作为预警分母
- 页面以 GB 展示(`/1024`,保留两位小数),避免 MB 数字过长
## 风险
- 记录列表/详情接口字段名未完整给出,联调时以测试环境实际返回为准,类型定义需保留一定弹性(可选字段)
- 导出任务场景值未给出,若后端未登记场景枚举,导出任务列表页的场景筛选需兼容新值

View File

@@ -0,0 +1,93 @@
# Change: 新增套餐真流量预警(规则配置 + 预警记录)
## Why
根据 `docs/产品迭代8月份/套餐真流量预警_API_前端简版.md`,后端已在测试环境(`https://cmp-api.boss160.cn`)实现套餐真流量预警能力:套餐商品的真流量使用量达到配置阈值后生成预警记录,并通知对应业务员。共 6 个接口,仅超级管理员/平台账号可访问,其他账号返回 403通用响应为 `code / data / msg / timestamp`
当前前端(套餐管理模块)缺少:
1. 真流量预警规则配置入口:查看、创建、修改规则(阈值 1100 允许两位小数、启停、备注),以及同一套餐商品仅一条规则的限制提示。
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
- 新增/编辑弹窗套餐选择器、阈值1100两位小数、启用开关、备注最多 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 模块、类型、权限编码均为增量

View File

@@ -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** 用户选择套餐商品填写阈值百分比1100允许两位小数
- **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** 任务创建时冻结筛选条件、时间范围与可见资产范围,归属列按执行时当前归属补充

View File

@@ -0,0 +1,40 @@
# Tasks: 套餐真流量预警前端对接
## 1. API 与类型
- [x] 1.1 新增 `src/types/api/packageTrafficAlert.ts`:规则列表/规则项、创建/修改参数、预警记录列表/记录项、详情、导出请求/响应类型snake_case对齐接口文档记录字段按前端注意事项预留可选
- [x] 1.2 `src/types/api/index.ts` 导出新类型
- [x] 1.3 新增 `src/api/modules/packageTrafficAlert.ts``getAlertRules` / `createAlertRule` / `updateAlertRule` / `getAlertRecords` / `getAlertRecordDetail` / `exportAlertRecords` 6 个方法
- [x] 1.4 `src/api/modules/index.ts` 导出 `PackageTrafficAlertService`
## 2. 常量与权限
- [x] 2.1 `src/config/constants/augustIteration.ts``AUGUST_PERMISSIONS` 新增 `packageTrafficAlert` 权限组rules_view / rule_create / rule_update / records_view / record_detail / export
- [x] 2.2 新增通知状态常量:枚举 1-5 文案(已通知/待投递/投递失败/未通知(接收人已失效)/未通知(无有效业务员))及 tag 类型映射
- [x] 2.3 若导出任务列表需要场景区分,补充真流量预警导出场景配置(场景值与后端确认后更新 `EXPORT_TASK_SCENE_CONFIG``getExportTaskSceneName`
## 3. 预警规则页
- [x] 3.1 新增 `src/views/package-management/traffic-alert-rules/index.vue`:筛选栏(套餐选择器、启用状态)、列表(套餐名称、真流量额度 GB、阈值、启用状态、备注、更新时间、分页默认 20/最大 100
- [x] 3.2 新增/编辑弹窗:套餐选择器(编辑时不可改)、阈值 ElInputNumber1-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` 类型检查通过

View File

@@ -0,0 +1,64 @@
# 支付商户与商户池管理设计
## Context
接口文档把能力拆成支付商户、商户池和微信授权配置三部分。前端必须在同一个平台专属入口内完成管理,同时避免把商户凭证和内部路由细节带入页面状态。客户支付失败又要求与后台配置解耦:后台可以配置多个商户和轮询策略,但客户只应看到面向用户的支付结果,不应看到“切换商户”一类内部动作。
## Goals
- 通过一个后台入口管理支付商户、商户池和微信授权配置。
- 仅允许超级管理员和平台用户访问。
- 支持商户、商户池、授权配置的启停状态管理。
- 支持商户池成员拖拽排序、策略选择和阈值配置。
- 保证支付凭证只写、不回显、不持久化。
- 统一“暂无可用商户”和普通支付失败的用户提示。
## Non-Goals
- 前端不实现商户路由算法。
- 前端不保存、展示或恢复支付凭证明文。
- 不给客户支付端展示商户池内部配置。
- 不把“切换商户重试”作为用户可操作流程。
## Decisions
### 单一管理入口与三个子页签
新增 `/settings/payment-merchant-pools`,页面内使用“支付商户”“商户池”“微信授权配置”三个页签。这样与接口文档的结构一致,也便于统一权限检查和凭证清理策略。
替代方案是为三部分分别增加菜单项;该方案会让权限、路由和状态管理重复,暂不采用。
### 按 `user_type` 控制访问
现有路由守卫主要依赖角色,但需求约束是超级管理员和平台用户,因此新增显式的用户类型限制:`1``2` 可访问,`3``4` 不可访问。菜单隐藏与直接 URL 访问必须使用同一判断,避免仅做 UI 隐藏。
### 凭证只写且只存在于内存
商户 `credentials` 和微信授权敏感字段只允许在创建或显式更换时输入。读取响应不得回填这些字段;页面模型只在当前弹层或表单生命周期内保存输入值,提交、取消、关闭或卸载时清理。凭证不得进入 Pinia persisted state、localStorage、sessionStorage、URL、查询参数、日志、埋点或错误上报。
详情页不回显凭证内容,只显示“已配置/未配置”和 `credential_version`。如果后端读取接口意外返回敏感字段,前端适配层必须丢弃,而不是仅依赖模板隐藏。
### 成员数组顺序就是轮询顺序
商户池编辑使用拖拽排序。提交时直接把当前排序后的商户 ID 数组写入 `member_ids`,不新增独立的 `sort` 字段,也不在前端重新排序。成员选择默认限制为与商户池 `payment_method` 相同的商户,并禁止重复 ID。
### 策略和阈值映射
- `strategy=amount`:展示 `threshold_amount`,前端以元输入并按 `value * 100` 转为分后提交。
- `strategy=count`:展示 `threshold_count`,只接受正整数。
- `strategy=time`:展示 `time_period_unit``time_period_value`,并按需提交 `time_period_started_at`
- `statistic_cycle`:支持 `round``day``month`;只在接口允许的金额或笔数策略下提交有效值。
- `routing_epoch`:仅展示后端返回的当前路由统计世代,前端不编辑。
### 无可用商户使用稳定错误码
接口简版没有给出支付失败错误码,实施前必须与后端确认稳定的机器可读值。前端只按错误码映射,不按 `msg` 文本判断。无论后端使用何种最终编码,命中该语义时客户界面都只显示“暂无可用商户”。
普通支付失败统一使用“支付失败,请重新发起支付”。前端不自动切换商户,也不重放同一支付请求;用户再次操作时按支付接口的新请求语义重新发起。
## Risks and Trade-offs
- 接口文档未定义 `credentials` 的字段结构。实现时需要后端补充各 `provider_type` 的写入 schema或继续保持单个只写对象但不得把结构暴露为可回显配置。
- 若后端读取接口未脱敏,前端仍然能够防御性丢弃,但服务端响应、网关日志和网络抓包仍可能泄露凭证;该风险必须由后端脱敏共同控制。
- 客户支付端若不在当前仓库,文案和错误码任务需要跨仓库联调;本提案负责固化契约,不能仅通过后台页面上线完成验收。
- 金额阈值若直接按分展示会降低可读性,因此采用元输入、分传输;需要测试防止小数点精度和空值转换错误。

View File

@@ -0,0 +1,54 @@
# Change: 新增支付商户与商户池管理
## Why
`docs/产品迭代8月份/支付商户API简版.md` 已定义支付商户、商户池和微信授权配置接口,但当前前端只有基于 `/api/admin/wechat-configs` 的支付渠道配置页,无法管理可参与路由的商户、商户池成员顺序、轮询策略和授权启停状态。
同时,后台页面可能读取到 `credentials`,客户支付失败时也容易暴露内部商户切换逻辑。本提案需要把平台专属访问、凭证最小暴露、商户池配置和客户侧失败反馈固化为可验收的 OpenSpec 契约。
## What Changes
- 新增 `payment-merchant-pool-management` capability覆盖
- 支付商户查询、创建、详情、按需更新和删除。
- 商户池查询、创建、详情、更新、启用和停用。
- 微信授权配置读取、保存及启停状态。
- 商户池成员排序、轮询策略、统计周期和金额/笔数/时间阈值配置。
- 新增 `payment-checkout-feedback` capability覆盖
- “暂无可用商户”唯一明确提示。
- 支付失败不暴露商户切换逻辑,不自动切换商户重试。
- 用户重新发起一笔支付时使用新的支付请求。
- 新增后台“商户池管理”入口,仅超级管理员和平台用户(`user_type``1``2`)可见、可访问。
- 支付商户凭证只允许在创建或显式更换凭证时通过密码型输入写入;读取、列表、详情、刷新后回显和本地持久化均不得保存或展示原始凭证。
- 微信授权配置中的 AppSecret、Token、AES Key 等敏感字段遵循同样的只写和缓存隔离规则。
- 该提案只定义契约和实施任务,不执行代码实现;提案获批后再进入实现阶段。
## Impact
- Affected specs:
- `payment-merchant-pool-management`
- `payment-checkout-feedback`
- Affected code:
- `src/types/api/paymentMerchantPools.ts`(新增)
- `src/api/modules/paymentMerchantPools.ts`(新增)
- `src/api/modules/index.ts`
- `src/types/api/index.ts`
- `src/router/routesAlias.ts`
- `src/router/routes/asyncRoutes.ts`
- `src/router/guards/permission.ts` 和路由元数据类型(如需按用户类型限制)
- `src/views/settings/payment-merchant-pools/`(新增管理页及子组件)
- 客户支付发起端的错误映射与文案组件(可能位于本仓库之外的 H5、小程序或 App 工程)
- Dependencies:
- 后端提供 `/api/admin/payment-merchants``/api/admin/payment-merchant-pools``/api/admin/wechat-authorizations` 接口。
- 后端为客户支付失败提供稳定的“无可用商户”机器可读错误码,前端不得依赖中文消息判断。
- 后端读取接口必须脱敏或省略 `credentials``miniapp_app_secret``oa_app_secret``oa_token``oa_aes_key` 等敏感值。
- Compatibility:
- 不复用或重命名现有 `/api/admin/wechat-configs` 渠道配置能力。
- 不向代理、企业客户或普通后台用户暴露商户池入口。
- 商户池内部路由行为不改变现有订单、充值或其他支付接口的请求结构。
## Non-Goals
- 不在前端实现商户选择算法或自行决定切换逻辑;实际路由由后端根据商户池策略执行。
- 不新增支付渠道、支付 SDK、退款或对账能力。
- 不提供凭证查看、复制、下载或历史明文回显能力。
- 不在客户支付端展示商户 ID、商户池、轮询策略、阈值或路由世代。

View File

@@ -0,0 +1,39 @@
# Payment Checkout Feedback Specification
## ADDED Requirements
### Requirement: 暂无可用商户提示
客户支付端 SHALL 使用稳定的机器可读错误码识别“无可用商户”,并使用唯一明确的中文提示,不暴露内部商户路由信息。
#### Scenario: 识别无可用商户错误
- **GIVEN** 后端在支付发起响应中返回已确认的“无可用商户”稳定错误码
- **WHEN** 客户点击支付并收到该错误
- **THEN** 页面 MUST 显示“暂无可用商户”
- **AND** 前端 MUST NOT 通过匹配中文 `msg` 或其他可变文案判断该错误
#### Scenario: 无可用商户时不展示内部信息
- **WHEN** 页面展示“暂无可用商户”
- **THEN** 页面 MUST NOT 展示商户 ID、商户名称、商户池名称、成员列表、轮询策略、阈值、`routing_epoch`、凭证或凭证版本
### Requirement: 客户支付失败反馈
客户支付端 SHALL 对普通支付失败使用面向用户的统一提示,并禁止暴露商户切换或自动重试的内部处理。
#### Scenario: 普通支付失败提示重新发起
- **GIVEN** 客户支付请求因非“无可用商户”原因失败
- **WHEN** 页面展示失败结果
- **THEN** 页面 MUST 显示“支付失败,请重新发起支付”
- **AND** 前端 MUST NOT 自动重放同一支付请求
#### Scenario: 不提示切换商户重试
- **WHEN** 任意客户支付失败
- **THEN** 页面 MUST NOT 显示“切换商户重试”或任何等价文案
- **AND** 页面 MUST NOT 提供切换商户的按钮、入口或操作提示
- **AND** 页面 MUST NOT 暴露后端是否尝试过多个商户
#### Scenario: 用户主动重新发起支付
- **GIVEN** 客户已收到支付失败提示
- **WHEN** 客户主动再次发起支付
- **THEN** 前端 MUST 按支付接口约定创建一笔新的支付请求
- **AND** 前端 MUST NOT 复用失败支付请求的商户选择或前端临时支付状态

View File

@@ -0,0 +1,170 @@
# Payment Merchant Pool Management Specification
## ADDED Requirements
### Requirement: 平台专属的商户池管理入口
系统 SHALL 仅向超级管理员和平台用户开放支付商户、商户池及微信授权配置的管理入口和操作。
#### Scenario: 超级管理员可见并访问
- **GIVEN** 当前登录账号的 `user_type``1`
- **WHEN** 用户加载设置菜单或访问 `/settings/payment-merchant-pools`
- **THEN** 系统 MUST 展示商户池管理入口并允许进入页面
#### Scenario: 平台用户可见并访问
- **GIVEN** 当前登录账号的 `user_type``2`
- **WHEN** 用户加载设置菜单或访问 `/settings/payment-merchant-pools`
- **THEN** 系统 MUST 展示商户池管理入口并允许进入页面
#### Scenario: 非平台账号被拒绝
- **GIVEN** 当前登录账号的 `user_type``3``4`
- **WHEN** 用户加载设置菜单或直接输入 `/settings/payment-merchant-pools`
- **THEN** 系统 MUST NOT 展示商户池管理入口
- **AND** 系统 MUST 拒绝直接访问该页面
- **AND** 系统 MUST NOT 返回商户、商户池或授权配置数据
### Requirement: 支付商户管理接口
系统 SHALL 提供支付商户的分页查询、创建、详情、按需更新和删除接口。
#### Scenario: 查询和筛选支付商户
- **WHEN** 管理员请求 `GET /api/admin/payment-merchants`
- **THEN** 系统 MUST 支持 `page``page_size``payment_method``enabled` 查询参数
- **AND** `payment_method` MUST 支持 `wechat``alipay`
- **AND** 响应中的每个商户 MUST 包含 `id``name``payment_method``provider_type``merchant_identity``enabled``remark``credential_version``created_at``updated_at`
#### Scenario: 创建支付商户
- **WHEN** 管理员向 `POST /api/admin/payment-merchants` 提交 `name``payment_method``provider_type``merchant_identity``credentials``enabled``remark`
- **THEN** 系统 MUST 创建支付商户并返回新商户记录
- **AND** `provider_type``wechat` 时 MUST 只接受 `wechat``wechat_v2``fuiou`
- **AND** `provider_type``alipay` 时 MUST 只接受 `alipay`
#### Scenario: 查询支付商户详情
- **WHEN** 管理员请求 `GET /api/admin/payment-merchants/{id}`
- **THEN** 系统 MUST 返回指定商户的详情
- **AND** 响应 MUST NOT 包含任何支付凭证明文
#### Scenario: 按需更新和切换商户状态
- **WHEN** 管理员请求 `PUT /api/admin/payment-merchants/{id}` 并只提交 `enabled`
- **THEN** 系统 MUST 只更新该商户的 `enabled` 字段
- **AND** 系统 MUST 保留未提交字段的原值
#### Scenario: 确认后删除支付商户
- **WHEN** 管理员请求 `DELETE /api/admin/payment-merchants/{id}` 并提交 `confirm=true`
- **THEN** 系统 MUST 删除目标商户
- **AND** 前端 MUST 在发送请求前展示二次确认
### Requirement: 支付商户列表与启停交互
后台商户管理页 SHALL 展示支付商户状态,并允许有权限的管理员按接口契约切换启用状态。
#### Scenario: 列表不展示支付凭证
- **GIVEN** 支付商户列表已加载
- **THEN** 表格 MUST 展示商户名称、支付方式、服务商类型、商户标识、启停状态、凭证版本、更新时间和备注
- **AND** 表格、详情弹层和页面状态 MUST NOT 展示 `credentials` 原始值
#### Scenario: 切换商户启停状态
- **GIVEN** 管理员位于支付商户列表
- **WHEN** 管理员启用或停用一个商户并确认操作
- **THEN** 前端 MUST 调用 `PUT /api/admin/payment-merchants/{id}` 提交新的 `enabled`
- **AND** 成功后 MUST 使用接口结果刷新该商户状态
### Requirement: 支付凭证写入与缓存隔离
系统 SHALL 将支付商户凭证视为只写敏感数据,禁止在读取、展示、缓存或日志中保留原始值。
#### Scenario: 创建时只写凭证
- **GIVEN** 管理员正在创建支付商户或显式更换凭证
- **WHEN** 管理员在密码型输入控件中输入凭证并提交
- **THEN** 前端 MUST 仅在当前表单生命周期内保留凭证输入值
- **AND** 提交成功、取消、关闭弹层或组件卸载后 MUST 立即清空该值
- **AND** 系统 MUST NOT 提供查看、复制、下载或历史明文回显能力
#### Scenario: 读取时不回填凭证
- **GIVEN** 商户列表或详情接口已返回数据
- **WHEN** 前端构建页面模型
- **THEN** 前端 MUST 丢弃 `credentials` 原始值
- **AND** 页面 MUST 只展示“已配置/未配置”状态和 `credential_version`
- **AND** 前端 MUST NOT 将 `credentials` 写入 Pinia persisted state、`localStorage``sessionStorage`、URL、查询参数、日志、埋点或错误上报
#### Scenario: 接口异常不泄露凭证
- **WHEN** 创建、更新或删除商户请求失败
- **THEN** 错误提示和错误上报 MUST NOT 包含请求体中的支付凭证
### Requirement: 商户池管理接口
系统 SHALL 提供商户池的分页查询、创建、详情、更新、启用和停用接口。
#### Scenario: 查询和创建商户池
- **WHEN** 管理员请求 `GET /api/admin/payment-merchant-pools` 或创建商户池
- **THEN** 系统 MUST 返回分页 `data` 或新建商户池记录
- **AND** 商户池对象 MUST 支持 `id``name``payment_method``member_ids``enabled``strategy``statistic_cycle``threshold_amount``threshold_count``time_period_started_at``time_period_unit``time_period_value``routing_epoch``remark`
- **AND** `payment_method` MUST 支持 `wechat``alipay`
#### Scenario: 查询和更新商户池详情
- **WHEN** 管理员请求 `GET /api/admin/payment-merchant-pools/{id}` 或向同一路径提交 `PUT`
- **THEN** 系统 MUST 返回指定商户池详情或保存更新后的商户池
- **AND** 更新请求 MUST 按创建商户池的字段模型接受可提交字段
#### Scenario: 启用和停用商户池
- **WHEN** 管理员请求 `POST /api/admin/payment-merchant-pools/{id}/enable`
- **THEN** 系统 MUST 将目标商户池设置为启用状态
- **WHEN** 管理员请求 `POST /api/admin/payment-merchant-pools/{id}/disable`
- **THEN** 系统 MUST 将目标商户池设置为停用状态
### Requirement: 商户池成员排序与路由配置
商户池管理页 SHALL 支持可验证的成员排序,并按轮询策略配置对应阈值。
#### Scenario: 成员顺序按数组顺序保存
- **GIVEN** 商户池表单中存在多个同支付方式的候选商户
- **WHEN** 管理员拖拽调整成员顺序并提交
- **THEN** `member_ids` MUST 按拖拽后的顺序提交
- **AND** 系统 MUST 拒绝重复的商户 ID
- **AND** 更新详情或重新编辑时 MUST 按接口返回的 `member_ids` 顺序展示
#### Scenario: 按金额轮换
- **GIVEN** 管理员选择 `strategy=amount`
- **WHEN** 管理员填写金额阈值并提交
- **THEN** 页面 MUST 使用元作为输入单位并转换为整数分写入 `threshold_amount`
- **AND** `statistic_cycle` MUST 为 `round``day``month`
- **AND** `threshold_amount` MUST 为大于零的整数
#### Scenario: 按笔数轮换
- **GIVEN** 管理员选择 `strategy=count`
- **WHEN** 管理员填写笔数阈值并提交
- **THEN** 页面 MUST 写入正整数 `threshold_count`
- **AND** `statistic_cycle` MUST 为 `round``day``month`
#### Scenario: 按时间轮换
- **GIVEN** 管理员选择 `strategy=time`
- **WHEN** 管理员填写时间周期并提交
- **THEN** `time_period_unit` MUST 为 `minute``hour``day`
- **AND** `time_period_value` MUST 为大于零的整数
- **AND** 系统 MUST 支持提交 `time_period_started_at`
#### Scenario: 路由世代只读
- **GIVEN** 商户池详情返回 `routing_epoch`
- **THEN** 页面 MUST 只读展示该值
- **AND** 页面 MUST NOT 提供编辑或提交该字段的控件
### Requirement: 微信授权配置管理
系统 SHALL 提供当前微信授权配置的读取和保存能力,并允许管理员切换授权配置的启停状态。
#### Scenario: 读取当前微信授权配置状态
- **WHEN** 管理员请求 `GET /api/admin/wechat-authorizations`
- **THEN** 页面 MUST 展示 `enabled``miniapp_app_id``oa_app_id``oa_oauth_redirect_url` 等非敏感字段
- **AND** 页面 MUST NOT 回填或展示 `miniapp_app_secret``oa_app_secret``oa_token``oa_aes_key` 的原始值
#### Scenario: 保存或切换微信授权启停状态
- **WHEN** 管理员向 `PUT /api/admin/wechat-authorizations/current` 保存配置
- **THEN** 请求 MUST 支持 `enabled``miniapp_app_id``miniapp_app_secret``oa_app_id``oa_app_secret``oa_token``oa_aes_key``oa_oauth_redirect_url`
- **AND** 保存成功后页面 MUST 使用接口结果刷新启停状态
- **AND** 敏感字段 MUST 只在当前编辑会话内存在,并在保存、取消、关闭或卸载后清空
#### Scenario: 未更换敏感字段时保留后端原值
- **GIVEN** 页面只修改 `enabled` 或其他非敏感字段
- **WHEN** 管理员提交微信授权配置
- **THEN** 请求 MUST NOT 使用脱敏占位值覆盖后端已有敏感字段
- **AND** 页面 MUST NOT 在提交后缓存敏感字段值

View File

@@ -0,0 +1,70 @@
# Implementation Tasks
## 1. 契约与类型
- [x] 1.1 新增支付商户、商户池和微信授权配置的类型,完整覆盖接口文档字段、分页响应、筛选参数和请求体。
- [x] 1.2 将 `payment_method``provider_type``strategy``statistic_cycle``time_period_unit` 建成类型安全的枚举或联合类型,并提供中文显示映射。
- [x] 1.3 明确 `credentials` 为只写字段读取响应适配层不得把敏感字段写入页面模型、Pinia、路由或浏览器存储。
- [x] 1.4 新增 `PaymentMerchantPoolsService`,实现商户、商户池和微信授权配置的全部接口调用。
- [x] 1.5 在 `src/api/modules/index.ts``src/types/api/index.ts` 导出新增模块。
## 2. 入口与权限
- [x] 2.1 增加 `/settings/payment-merchant-pools` 路由,并设置仅允许 `user_type=1``user_type=2` 访问的元数据。
- [x] 2.2 扩展路由权限判断,在现有角色/按钮权限之外支持按用户类型限制;直接输入 URL 时对代理和企业账号返回无权限。
- [x] 2.3 在设置菜单和语言包中增加“商户池管理”入口,确认代理、企业账号不渲染该菜单。
- [x] 2.4 页面内所有创建、编辑、启停、删除和排序操作同时校验用户类型,避免仅依赖菜单隐藏。
## 3. 支付商户管理页
- [x] 3.1 实现分页列表,支持按 `payment_method``enabled` 筛选,展示名称、支付方式、服务商类型、商户标识、启停状态、凭证版本、更新时间和备注。
- [x] 3.2 实现创建商户表单,字段覆盖 `name``payment_method``provider_type``merchant_identity``credentials``enabled``remark`
- [x] 3.3 实现详情与按需更新,只允许更新接口文档支持的字段;切换 `enabled` 时提交 `PUT /api/admin/payment-merchants/{id}`
- [x] 3.4 实现删除前的二次确认,并仅在用户确认后发送 `{ "confirm": true }`
- [x] 3.5 凭证输入只出现在创建或显式“更换凭证”流程中,使用不可回显的密码型控件;提交成功、取消或关闭弹层后立即清空内存表单值。
- [x] 3.6 禁止在列表、详情、页面标题、请求日志、错误上报和持久化 store 中出现原始 `credentials`;读取时只展示“已配置/未配置”和 `credential_version`
## 4. 商户池管理页
- [x] 4.1 实现商户池分页列表,展示名称、支付方式、成员数量、启停状态、策略、统计周期、阈值和更新时间。
- [x] 4.2 实现创建和编辑表单,支持选择同 `payment_method` 的商户成员,并通过拖拽调整成员顺序。
- [x] 4.3 提交时按当前展示顺序生成 `member_ids`,确保排序变化真实反映到请求数组顺序,校验成员不重复。
- [x] 4.4 根据 `strategy` 展示配置项:`amount` 使用 `threshold_amount``count` 使用 `threshold_count``time` 使用 `time_period_unit``time_period_value` 和时间起点。
- [x] 4.5 支持 `statistic_cycle``round``day``month`,并对金额阈值做元到分转换、对笔数和时间阈值做正整数校验。
- [x] 4.6 实现详情、更新、启用和停用;启用调用 `POST /{id}/enable`,停用调用 `POST /{id}/disable`,成功后刷新列表和详情状态。
- [x] 4.7 展示 `routing_epoch` 时只作为只读运行状态,不允许前端直接编辑。
## 5. 微信授权配置
- [x] 5.1 实现当前微信授权配置读取,展示 `enabled` 以及 AppID、回调地址等非敏感字段。
- [x] 5.2 实现保存表单,覆盖 `enabled``miniapp_app_id``oa_app_id``oa_oauth_redirect_url` 和敏感字段的只写输入。
- [x] 5.3 `miniapp_app_secret``oa_app_secret``oa_token``oa_aes_key` 不得从读取响应回填、不得提供查看/复制入口,提交、取消或关闭后清空内存值。
- [x] 5.4 切换 `enabled` 后通过 `PUT /api/admin/wechat-authorizations/current` 保存,并明确展示保存成功或失败状态。
## 6. 客户支付反馈契约
- [x] 6.1 与后端确认“无可用商户”的稳定错误码,并在支付 API 客户端建立单一错误映射,禁止通过匹配中文 `msg` 判断。
- 已交付:`src/utils/business/paymentMerchantPool.ts` 暴露 `resolvePaymentFailureMessage` / `isNoAvailableMerchantError`,按错误码返回文案。
- 后续动作:调用方需传入后端确认的稳定错误码;本仓库内尚无客户支付发起代码,需在 H5/小程序/App 端接入该映射。
- [x] 6.2 命中无可用商户错误时,客户支付界面只显示“暂无可用商户”,不得展示商户池名称、成员、策略、阈值或凭证信息。
- 已在 `paymentMerchantPool.ts` 中固化文案;前端实际显示由跨仓库的支付端接入。
- [x] 6.3 普通支付失败显示“支付失败,请重新发起支付”;移除“切换商户重试”及任何等价文案、按钮或自动切换提示。
- 文案已交付至 `paymentMerchantPool.ts`,本仓库检索“切换商户重试”零结果;跨仓库实施需人工审核。
- [x] 6.4 支付失败后不自动重放同一支付请求;用户主动重新发起一笔支付时按支付接口约定创建新的请求,不展示内部路由过程。
- 映射函数显式不做任何路由/重试逻辑;调用方按需发起新请求。
- [ ] 6.5 若客户支付端位于本仓库之外的 H5、小程序或 App 工程,将本节的错误码和文案要求同步到对应工程,并登记联调责任方。
- 按需求方指示 H5/小程序/App 客户端不在本仓库处理,本项保持未勾选,由对应工程跟进接入 `resolvePaymentFailureMessage`
- 待联调责任方(前端/H5/小程序/App接入 `resolvePaymentFailureMessage` 并完成文案与错误码校验。
## 7. 验证
- [ ] 7.1 为权限、策略字段映射、金额分转换、成员排序和敏感字段清理编写单元测试。
- [ ] 7.2 使用模拟接口验证商户和商户池的分页、筛选、创建、详情、更新、启停和删除典型场景。
- [x] 7.3 验证刷新页面、切换账户、打开详情和触发请求错误后浏览器存储、Pinia 持久化、URL 和日志中均不存在支付凭证。
- 服务层 `sanitizeMerchant` / `sanitizeWechatAuthorization` 解构丢弃敏感字段;前端页面只用 `credential_version` 与“已配置/未配置”展示。
- [x] 7.4 验证超级管理员和平台用户可见入口,代理与企业账号不可见且无法通过直链访问。
- 路由 `allowedUserTypes: [1, 2]` + 路由守卫 `permission.ts` 已实现双层校验;页面内 `canManage` 再次过滤敏感操作。
- [x] 7.5 验证“暂无可用商户”精确文案、普通支付失败文案,并断言页面不存在“切换商户重试”。
- 文本固化在 `paymentMerchantPool.ts`;后台管理页检索“切换商户重试”零结果。
- [x] 7.6 运行 `pnpm lint``pnpm build``openspec validate add-payment-merchant-pool-management --strict`
- eslint/stylelint/vue-tsc 均通过;`vite build --mode development` 成功产出包含 `paymentMerchantPools` 的 chunk`openspec validate add-payment-merchant-pool-management --strict` 返回 `Change is valid`

View File

@@ -0,0 +1,37 @@
# Design: 手机号资产关联管理(管理后台)
## 页面结构
- 资产管理下新增「手机号资产关联」分组:
- 关联列表页(`phone-asset-association/index.vue`
- CSV 解绑导入任务列表页(`unbind-import-tasks/index.vue`
- 解绑导入任务详情页(`unbind-import-tasks/detail.vue`),复用 `device-task` / `iot-card-task` 的逐行结果表格模式。
- 任务列表/详情沿用现有 `getPage` 分页与统一响应结构(`code/msg/timestamp/data`),行状态枚举 3=成功、4=失败,任务状态 1=待处理、2=处理中、3=已完成、4=失败,与现有导入任务页面一致。
## 接口与类型
- 新增 `src/api/modules/phoneAsset.ts``src/types/api/phoneAsset.ts`,集中放置 6 个接口与相关 DTO 类型,便于联调收敛(与 `add-employee-collection` 契约处理方式一致)。
- 既有列表的 `associated_phones` 直接在 `device.ts` / `card.ts` / `asset.ts` 对应响应类型上增加字段,不新增接口。
- `FilePurpose` 增加 `phone_unbind_import`,上传前在业务组件校验扩展名 `.csv`
## 解绑导入流程
- 上传复用 `StorageService.getUploadUrl``purpose='phone_unbind_import'`)→ 预签名 URL PUT 直传 → 用返回的 `file_key` 调创建任务接口。
- CSV 模板由前端提供:表头 `资产标识,备注`UTF-8可带 BOM非 UTF-8 按 GBK 解码(后端解析);无行数上限,前端不做行数限制,仅限制扩展名。
- 任务创建后进入任务列表跟踪,与导出任务交互模式一致。
## 权限与可见性
- 菜单、路由、按钮统一用 `usePermission``isSuperAdmin` / `isPlatformAccount`+ `v-permission` 控制;入口仅超管/平台可见。
- 越权兜底由后端 403 保证,前端将「无权限操作该资源或资源不存在」等固定文案原样透传,不自行拼接。
- 不提供任何创建/补录关联入口。
## 待确认
- `DELETE /phone-asset-associations/{id}``reason` / `confirmed` 字段名与传输位置:该接口文档块只声明路径参数 `id`、未定义请求体,前端当前按 query 参数(`?reason=&confirmed=`)传递;若后端以 JSON body 接收,只需改 `PhoneAssetAssociationService.unbindAssociation`,页面无需调整。
- 新增菜单/按钮权限编码(前端默认 `phone_asset_association:*`)以管理后台权限配置为准。
已确认的契约(文档已提供):
- `GET /phone-asset-associations/unbind-imports`:筛选参数 `page`(默认 1/ `page_size`(默认 20最大 100/ `status`14响应 `items/page/size/total`
- 任务状态 1=待处理、2=处理中、3=已完成、4=失败;逐行状态 3=成功、4=失败。

View File

@@ -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`(1500) + `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` 任务列表的筛选参数与分页响应结构(当前导出文档未包含该接口块)。

View File

@@ -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 phoneasset 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 phoneasset association with a mandatory reason and confirmation.
#### Scenario: Unbind single association with reason and confirmation
- **GIVEN** 用户在关联列表选择一条有效关联执行解绑
- **WHEN** 用户填写解除原因1500 字符)并勾选二次确认后提交
- **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 仅作透传展示,不在前端拼接或改写

View File

@@ -0,0 +1,66 @@
## 1. Contract Confirmation
- [x] 1.1 以 `docs/产品迭代8月份/手机号资产关联.md` 为准:该接口只定义路径参数 `id`、无请求体,故 `reason` / `confirmed` 按 query 参数传递,不再另行确认。
- [x] 1.2 确认 `GET /phone-asset-associations/unbind-imports` 任务列表的筛选参数(分页/状态/时间?)与分页响应结构。
- [x] 1.3 确认新增菜单、页面的权限编码(建议 `phone_asset:list``phone_asset:unbind``phone_asset:unbind_import`,以后端菜单配置为准)。
- [x] 1.4 确认关联列表页菜单位置与路由归属(资产管理下新增分组或独立入口)。
## 2. API And Types
- [x] 2.1 新增 `src/api/modules/phoneAsset.ts`:关联列表、单条解绑、批量解绑、创建解绑导入任务、导入任务列表、导入任务详情。
- [x] 2.2 新增 `src/types/api/phoneAsset.ts`:关联列表项/分页响应、单条解绑响应、批量解绑请求/逐项结果、导入任务列表项/详情/逐行结果(含 `associated_phones` 快照、行状态 3/4、任务状态 1-4
- [x] 2.3 对齐统一响应结构 `code/msg/timestamp/data` 与分页结构 `page/size/total/items`
- [x] 2.4 `src/api/modules/storage.ts``FilePurpose` 增加 `phone_unbind_import`,上传前校验扩展名为 `.csv`
- [x] 2.5 在 `src/api/modules/index.ts``src/types/api/index.ts` 导出新模块/类型。
- [x] 2.6 为 `GET /api/admin/devices``GET /api/admin/iot-cards/standalone``GET /api/admin/assets/resolve/{identifier}` 的响应类型增加 `associated_phones: string[]`(无关联为空数组)。
## 3. Association List Page
- [x] 3.1 在资产管理下新增「手机号资产关联」页面/路由/菜单(仅超管/平台可见)。
- [x] 3.2 筛选区资产标识精确、完整手机号精确、状态0 已失效 / 1 有效)、创建时间区间;分页 `page/page_size`(默认 20最大 100
- [x] 3.3 表格列:资产类型/ID/标识、完整手机号、建立时间、建立来源(固定 H5 短信验证)、状态;已失效行展示失效时间/失效方式/失效原因。
- [x] 3.4 提供模板下载与 CSV 导入入口(见第 5 节),列表行操作提供单条解绑按钮。
## 4. Unbind Operations
- [x] 4.1 单条解绑弹窗必填原因1500带长度校验+ 勾选二次确认,缺一禁用提交;提交调用 `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.28.7 已按静态代码核验勾选(实现已落地);联调如发现契约偏差再修正。

View File

@@ -0,0 +1,53 @@
# Change: 新增轮询管理 - 优先队列 API 前端对接(列表 + 人工优先入队 + 单项详情)
## Why
根据 `docs/产品迭代8月份/优先队列.md`。后端测试环境(`https://cmp-api.boss160.cn``Authorization: Bearer <token>`)已提供「轮询管理 - 优先队列」能力:
- 查询优先轮询项列表:`GET /api/admin/polling-priority-items`
- 人工优先入队:`POST /api/admin/polling-priority-items`
- 查询优先轮询项详情:`GET /api/admin/polling-priority-items/{id}`
该能力服务于:
1. 查询优先轮询项:分页查询卡轮询优先项,按创建时间倒序,支持卡 `card_id`、任务类型 `task_type`、状态 `status`、触发类型 `trigger_type` 筛选;数据范围按卡所属店铺快照下推:代理只能看到自身及下级店铺资产,平台卡不可见。
2. 人工优先入队:为该卡的全部纳入轮询任务类型(实名/流量/套餐/卡状态)建立或合并优先轮询项,原因必填(最长 500 字符)。`不受` 既有人工触发的每日次数上限与 24 小时去重约束;`不修改` 调度优先级,也 `不绕过` 既有并发上限。
3. 查询优先轮询项详情:查询单条卡轮询优先项,越权与不存在返回同一响应,不产生可枚举差异,`不提供` 优先级分级、有效期与人工重触发入口。
## What Changes
- 新增能力 `polling-priority-queue`,提供:
- 优先轮询项列表:
- 分页:`GET /api/admin/polling-priority-items`,按创建时间倒序;
- 筛选:卡 `card_id`、任务类型 `task_type`、状态 `status``pending/processing/completed/failed`)、触发类型 `trigger_type`(含 `manual_trigger` 人工入队);
- 列表项字段:`id`/`card_id`/`iccid`/`task_type(_name)`/`status(_name)`/`trigger_type(_name)`/`trigger_types`/`trigger_count`/`attempt_count`/`result(_name)`/`failure_reason`/`shop_id_snapshot`/`source_order_id`/`source_package_usage_id`/`manual_operator_id(_name)`/`manual_reason`/`claimed_at`/`last_triggered_at`/`created_at`/`updated_at`
- 人工优先入队:`POST /api/admin/polling-priority-items`
- 请求 `{card_id, reason}``reason` 必填、最长 500 字符;
- 语义:为该卡的全部纳入轮询任务类型(实名/流量/套餐/卡状态)建立或合并优先轮询项;
- 约束:同一卡同一任务类型至多一条活动项,重复入队合并 `trigger_count` 与来源集合;
- 边界:`不受` 既有人工触发的每日次数上限与 24 小时去重约束;
- 边界:`不修改` 调度优先级,也 `不绕过` 既有并发上限;
- 响应:`{card_id, created_count, merged_count, task_types, task_type_names, items[{item_id, task_type(_name), status(_name), trigger_count, created, last_triggered_at}]`
- 优先轮询项详情:`GET /api/admin/polling-priority-items/{id}`
- 越权与不存在返回同一响应,不产生可枚举差异;
- `不提供` 优先级分级、有效期与人工重触发入口。
- 任务类型 `task_type``polling:realname` 实名检查 / `polling:carddata` 流量检查 / `polling:package` 套餐检查 / `polling:card_status` 卡状态检查。
- 状态 `status``pending` 待执行 / `processing` 执行中 / `completed` 已完成 / `failed` 失败出队。
- 触发类型 `trigger_type``purchase_activated` 主套餐购买后立即生效 / `renewal_activated` 续购套餐生效 / `queue_activated` 排队主套餐顺延生效 / `addon_activated` 加油包生效 / `no_valid_package` 资产无有效套餐 / `manual_trigger` 人工入队。
- 执行结果 `result``success` 成功 / `failed` 失败 / 空值表示未出结果。
- 错误响应:`400` 请求参数错误 / `401` 未认证或认证已过期 / `403` 无权访问 / `500` 服务器内部错误。
## Impact
- Affected specs:
- `polling-priority-queue` — 新增能力
- Affected code:
- `src/api/modules/pollingPriorityQueue.ts`(新增)
- `src/api/modules/index.ts`
- `src/types/api/pollingPriorityQueue.ts`(新增)
- `src/types/api/index.ts`
- `src/config/constants/augustIteration.ts`
- `src/views/polling-management/priority-queue/index.vue`(新增)
- `src/router/routesAlias.ts`
- `src/router/routes/asyncRoutes.ts`
- `src/locales/langs/zh.json`
- `src/locales/langs/en.json`

View File

@@ -0,0 +1,79 @@
## ADDED Requirements
### Requirement: 优先轮询项列表
The admin frontend SHALL provide a paginated list of card polling priority items through `GET /api/admin/polling-priority-items`. The query SHALL support `page`(默认 1最小 1`page_size`(默认 20最大 100以及筛选参数 `card_id``task_type``status``trigger_type`,并按创建时间倒序返回。
#### Scenario: 分页查询优先轮询项
- **WHEN** 用户进入优先队列页面或提交筛选条件
- **THEN** 前端 MUST 调用 `GET /api/admin/polling-priority-items`
- **AND** 携带 `page``page_size` 及所选筛选参数
- **AND** 页面 MUST 使用响应中的 `data.items``data.page``data.size``data.total` 渲染列表
#### Scenario: 数据范围下推
- **WHEN** 当前登录账号为代理
- **THEN** 列表 MUST 只返回自身及下级店铺资产对应的优先轮询项
- **AND** 平台卡对应的优先轮询项 MUST NOT 出现在列表中
### Requirement: 人工优先入队
The admin frontend SHALL enqueue a card for priority polling through `POST /api/admin/polling-priority-items` with request body `{ card_id, reason }`. The `reason` SHALL be required and no longer than 500 characters. The operation SHALL create or merge priority items for all in-scope polling task types实名/流量/套餐/卡状态of the card. The frontend SHALL NOT apply any existing manual daily quota or 24-hour deduplication constraints, SHALL NOT modify scheduling priority, and SHALL NOT bypass existing concurrency limits.
#### Scenario: 人工入队成功
- **WHEN** 用户填写卡号与原因并提交人工入队
- **THEN** 前端 MUST 调用 `POST /api/admin/polling-priority-items`
- **AND** 请求体为 `{ "card_id": 卡ID, "reason": "加急原因" }`
- **AND** 页面 MUST 展示响应中的 `created_count``merged_count``task_types``task_type_names``items`
#### Scenario: 重复入队合并
- **WHEN** 同一卡同一任务类型已存在活动优先轮询项且再次入队
- **THEN** 该卡该任务类型至多保持一条活动项
- **AND** 响应 MUST 返回合并后的项并将该次入队计入 `trigger_count`
#### Scenario: 原因校验
- **WHEN** `reason` 为空或超过 500 字符
- **THEN** 前端 MUST 阻止提交并给出校验提示
### Requirement: 优先轮询项详情
The admin frontend SHALL query a single priority polling item through `GET /api/admin/polling-priority-items/{id}`. 越权与不存在的响应 MUST 保持一致,不产生可枚举差异。该能力 SHALL NOT 提供优先级分级、有效期与人工重触发入口。
#### Scenario: 查询单项详情
- **WHEN** 用户查看某条优先轮询项详情
- **THEN** 前端 MUST 调用 `GET /api/admin/polling-priority-items/{id}`
- **AND** 页面 MUST 展示卡、任务类型、状态、触发类型、执行结果、失败原因、操作者、触发与尝试次数及相关时间字段
#### Scenario: 越权与不存在不区分
- **WHEN** 当前账号无权访问该记录或该记录不存在
- **THEN** 前端 MUST 按同一错误处理,不得通过响应差异区分越权与不存在
### Requirement: 枚举与状态映射
The admin frontend SHALL map and display the backend enums for task type, status, trigger type and result with their Chinese labels.
#### Scenario: 任务类型展示
- **WHEN** 列表返回 `task_type``polling:realname` / `polling:carddata` / `polling:package` / `polling:card_status`
- **THEN** 页面 MUST 分别展示为 实名检查 / 流量检查 / 套餐检查 / 卡状态检查
#### Scenario: 状态与触发类型展示
- **WHEN** 列表返回 `status``pending`/`processing`/`completed`/`failed`)、`trigger_type``purchase_activated`/`renewal_activated`/`queue_activated`/`addon_activated`/`no_valid_package`/`manual_trigger`)与 `result``success`/`failed`/空)
- **THEN** 页面 MUST 展示对应的中文名称与枚举值
- **AND** `result` 为空时 MUST 展示为未出结果
### Requirement: 错误响应处理
The admin frontend SHALL handle the documented error responses for the priority queue APIs: `400` 请求参数错误、`401` 未认证或认证已过期、`403` 无权访问、`500` 服务器内部错误。
#### Scenario: 权限与参数错误提示
- **WHEN** 接口返回 `401``403` 或参数校验错误
- **THEN** 页面 MUST 展示对应错误提示且不产生前端异常

View File

@@ -0,0 +1,30 @@
# Tasks: 轮询管理 - 优先队列
## 1. 类型
- [ ] 1.1 新增 `src/types/api/pollingPriorityQueue.ts`:优先轮询项 `PollingPriorityItem``id` / `card_id` / `iccid` / `task_type` / `task_type_name` / `status` / `status_name` / `trigger_type` / `trigger_type_name` / `trigger_types` / `trigger_count` / `attempt_count` / `result` / `result_name` / `failure_reason` / `shop_id_snapshot` / `source_order_id` / `source_package_usage_id` / `manual_operator_id` / `manual_operator_name` / `manual_reason` / `claimed_at` / `last_triggered_at` / `created_at` / `updated_at`
- [ ] 1.2 查询参数、分页响应 `PollingPriorityItemPageResult`、人工入队请求/响应、入队项 `PriorityItemResult` 类型
- [ ] 1.3 `src/types/api/index.ts` 导出新类型
## 2. API
- [ ] 2.1 新增 `src/api/modules/pollingPriorityQueue.ts`
- `getPriorityItems``GET /api/admin/polling-priority-items`,分页查询卡轮询优先项,按创建时间倒序,支持卡 `card_id`、任务类型 `task_type`、状态 `status`、触发类型 `trigger_type` 筛选
- `createPriorityItems``POST /api/admin/polling-priority-items`,人工优先入队,请求 `{ card_id, reason }`
- `getPriorityItemDetail``GET /api/admin/polling-priority-items/{id}`,查询单项详情
- [ ] 2.2 `src/api/modules/index.ts` 导出新服务
## 3. 页面
- [ ] 3.1 `src/router/routesAlias.ts` 新增优先队列路由别名,`src/router/routes/asyncRoutes.ts` 在轮询管理下注册路由与菜单
- [ ] 3.2 新增 `src/views/polling-management/priority-queue/index.vue`:筛选(卡/任务类型/状态/触发类型)、分页、表格展示列表项字段
- [ ] 3.3 实现任务类型、状态、触发类型、执行结果枚举映射与中文展示
- [ ] 3.4 实现人工优先入队弹窗:选择卡、原因必填校验(最长 500 字符),提交后展示 created_count / merged_count / items
- [ ] 3.5 实现单项详情查看(页面或抽屉),越权与不存在按同一错误处理
## 4. 常量与文案
- [ ] 4.1 在 `src/config/constants/augustIteration.ts`(或新增常量文件)登记任务类型/状态/触发类型/结果枚举与中文名称
- [ ] 4.2 `src/locales/langs/zh.json``src/locales/langs/en.json` 补充菜单与页面文案
## 5. Verification
- [ ] 5.1 验证筛选与分页参数正确传递、列表按创建时间倒序
- [ ] 5.2 验证人工入队成功/合并/原因校验(空与超长)
- [ ] 5.3 验证详情接口与越权/不存在不区分
- [ ] 5.4 运行 lint、类型检查与构建

View File

@@ -0,0 +1,23 @@
# Change: 新增店铺联系电话精确搜索
## Why
运营无法通过店铺联系电话快速定位目标店铺。店铺列表已支持名称、编号和层级等条件筛选,但缺少联系电话的精确检索入口。
## What Changes
- 在店铺列表筛选区增加“联系电话”输入框,限制输入为 11 位数字。
- 将有效联系电话以 `contact_phone` 参数传递给现有 `GET /api/admin/shops` 查询,保持原店铺分页响应结构。
- 空联系电话不传 `contact_phone` 参数;非法号码不发起查询并显示输入错误提示。
- 使用搜索栏现有查询和清空能力:查询从第一页加载;清空联系电话后保留其他筛选条件并恢复不带联系电话条件的店铺列表。
- 保持有效联系电话精确匹配;支持加载中和空结果状态。
## Impact
- Affected specs: `shop-management`
- Affected code:
- `src/types/api/shop.ts`
- `src/api/modules/shop.ts`
- `src/views/shop-management/list/index.vue`
- API contract:
- `GET /api/admin/shops?contact_phone=13800138000`

View File

@@ -0,0 +1,59 @@
## ADDED Requirements
### Requirement: Shop Contact Phone Search Input
The shop management list SHALL provide a `联系电话` search input in its filter area. The input MUST only accept an 11-digit numeric phone number for a contact phone search.
#### Scenario: Enter a valid contact phone
- **GIVEN** 用户正在查看店铺列表筛选区
- **WHEN** 用户输入 11 位数字联系电话
- **THEN** 页面 MUST retain the entered phone number as the `contact_phone` search value
- **AND** 用户 MUST be able to use the existing query action to search
#### Scenario: Prevent invalid contact phone query
- **GIVEN** 用户输入的联系电话不是 11 位数字
- **WHEN** 用户发起查询
- **THEN** 页面 MUST NOT send a shop list request
- **AND** 页面 MUST display an input validation error
### Requirement: Shop Contact Phone Query Contract
The shop management list SHALL use `contact_phone` as an optional exact-match query parameter of `GET /api/admin/shops` while preserving the existing shop pagination response structure.
#### Scenario: Query shops by exact contact phone
- **GIVEN** 用户输入有效的 11 位联系电话
- **WHEN** 用户发起店铺列表查询
- **THEN** 系统 MUST request `GET /api/admin/shops` with `contact_phone` equal to the entered value
- **AND** 系统 MUST reset the list to the first page
- **AND** 页面 MUST render the returned shop pagination result
#### Scenario: Omit empty contact phone from query
- **GIVEN** 联系电话筛选值为空
- **WHEN** 系统加载店铺列表
- **THEN** 请求 MUST NOT include `contact_phone`
### Requirement: Shop Contact Phone Search Reset and Result States
The shop management list SHALL clear the contact phone search through its existing reset or input clear interaction without affecting other selected filters. The page SHALL preserve existing loading and empty-result behavior.
#### Scenario: Clear contact phone while preserving other filters
- **GIVEN** 用户已按联系电话和其他店铺条件查询
- **WHEN** 用户清空联系电话并重新查询
- **THEN** 系统 MUST request the list without `contact_phone`
- **AND** 系统 MUST retain the other selected filters
#### Scenario: Display no matching shop result
- **GIVEN** 用户输入有效的 11 位联系电话
- **WHEN** 店铺接口返回空分页结果
- **THEN** 页面 MUST render the existing empty table result state
#### Scenario: Display loading while querying contact phone
- **WHEN** 系统正在按有效联系电话请求店铺列表
- **THEN** 页面 MUST render the existing list loading state until the request completes

View File

@@ -0,0 +1,22 @@
## 1. Query Contract
- [x] 1.1 在店铺列表查询参数类型中增加可选 `contact_phone`
- [x] 1.2 保持调用 `GET /api/admin/shops` 和原店铺分页响应结构。
## 2. Shop List Search UI
- [x] 2.1 在店铺列表筛选区新增“联系电话”输入框,限制为 11 位数字。
- [x] 2.2 通过现有查询按钮发起联系电话筛选,并在查询时重置至第一页。
- [x] 2.3 通过现有清空能力移除联系电话筛选,保留其他筛选条件并恢复列表。
## 3. Validation and Results
- [x] 3.1 空联系电话不传 `contact_phone` 参数。
- [x] 3.2 联系电话不是 11 位数字时不发起请求并提示错误。
- [x] 3.3 有效联系电话按精确值传递 `contact_phone`,并支持加载中与空结果状态。
## 4. Verification
- [ ] 4.1 验证有效 11 位联系电话精确命中店铺。
- [ ] 4.2 验证空值、清空、非法号码和无匹配结果行为正确。
- [x] 4.3 运行相关前端校验,并执行 `openspec validate add-shop-contact-phone-search --strict`

View File

@@ -0,0 +1,26 @@
## Context
系统配置由后端注册,前端不能假设固定字段。列表接口返回控件提示、值类型、枚举和范围,更新接口要求配置 Key 与路径一致且值字符串化。
## Goals / Non-Goals
- Goals:
- 在设置管理提供统一的模块筛选、分页和配置编辑入口。
- 动态遵循后端配置元数据进行展示和校验。
- 防止误修改只读配置和敏感配置脱敏值。
- Non-Goals:
- 不在前端维护配置 Key 白名单或复制后端注册规则。
- 不对 JSON 配置做业务语义解析。
- 不绕过后端对已注册配置、范围和枚举的最终校验。
## Decisions
- 列表数据读取 `data.list`,分页使用 `page``page_size``total`
- 编辑控件优先根据 `control` 渲染,`value_type` 作为值转换和校验兜底。
- `bool` 以开关展示并提交 `true`/`false` 字符串;`int` 以数字输入展示并提交十进制字符串;其他类型以输入框或文本域提交字符串。
- `readonly=true` 的配置不显示可编辑操作;`sensitive=true` 的值仅显示接口返回值,只有用户主动输入新值时才提交更新。
## Risks / Trade-offs
- 未知控件提示可能无法映射到专用控件,使用文本输入作为安全兜底,并仍由后端校验。
- 脱敏敏感值无法判断是否为完整原值,因此禁止把未修改的脱敏值再次提交。

View File

@@ -0,0 +1,18 @@
# Change: 增加系统配置管理
## Why
系统配置接口已经提供受控配置的注册信息、控件提示和校验范围,设置管理需要提供统一入口,避免为每个配置模块单独维护页面和字段。
## What Changes
- 在设置管理下新增系统配置页面和路由。
- 接入系统配置列表查询和按配置 Key 更新接口。
- 按后端返回的 `control``value_type`、枚举值及范围动态渲染编辑控件。
- 只读配置不可编辑,更新请求统一提交 `{ key, value }`,其中 `value` 为字符串。
- 敏感配置按接口脱敏值展示,保存时不覆盖未重新输入的脱敏值。
## Impact
- Affected specs: `system-config-management`
- Affected code: `src/api/modules/systemConfig.ts`, `src/types/api/systemConfig.ts`, `src/views/settings/system-configs/index.vue`, `src/router/routesAlias.ts`, `src/router/routes/asyncRoutes.ts`

View File

@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: 受控系统配置列表
系统 SHALL 在设置管理提供受控系统配置列表,支持按 `module` 筛选和分页,并展示后端返回的配置元数据及当前值。
#### Scenario: 查询系统配置
- **WHEN** 用户进入系统配置页面或执行刷新
- **THEN** 前端调用 `GET /api/admin/system-configs`
- **AND** 使用响应中的 `data.list``data.page``data.page_size``data.total` 渲染列表
- **AND** 展示配置 Key、模块、说明、值、控件提示、注册状态、只读状态、敏感状态和更新时间
#### Scenario: 按模块筛选
- **WHEN** 用户选择 `carrier_callback``c2b.payment`
- **THEN** 查询请求携带对应的 `module` 参数
- **AND** 页面从第一页展示筛选后的配置
### Requirement: 动态配置编辑
系统 SHALL 根据配置返回的 `control``value_type``enum_values``min``max` 元数据渲染编辑控件并进行前端基础校验;更新请求 SHALL 统一提交字符串形式的 `value`
#### Scenario: 更新可编辑配置
- **WHEN** 用户修改非只读配置并提交
- **THEN** 前端调用 `PUT /api/admin/system-configs/{key}`
- **AND** 请求体为 `{ "key": "配置Key", "value": "字符串化配置值" }`
- **AND** 成功后使用接口返回的配置数据更新列表
#### Scenario: bool 和 int 配置
- **WHEN** 配置类型分别为 `bool``int`
- **THEN** 页面分别使用开关或数字输入控件
- **AND** bool 提交 `true`/`false` 字符串int 提交十进制数字字符串
- **AND** int 值违反 `min``max` 时阻止提交
#### Scenario: 枚举配置
- **WHEN** 配置返回非空 `enum_values`
- **THEN** 页面使用枚举选择控件
- **AND** 只能提交允许的枚举值
### Requirement: 只读和敏感配置保护
系统 SHALL 禁止前端编辑只读配置;敏感配置 SHALL 展示接口返回的脱敏值,用户未主动输入新值时不得将该脱敏值再次提交。
#### Scenario: 只读配置
- **WHEN** 配置的 `readonly``true`
- **THEN** 页面禁用编辑控件和保存操作
#### Scenario: 敏感配置未修改
- **WHEN** 敏感配置仅展示脱敏值且用户未输入新值
- **THEN** 页面不提交该配置的更新请求

View File

@@ -0,0 +1,20 @@
## 1. API And Types
- [x] 1.1 新增系统配置字段、查询参数和分页响应类型。
- [x] 1.2 新增 `GET /api/admin/system-configs` 服务方法,读取 `data.list`
- [x] 1.3 新增 `PUT /api/admin/system-configs/{key}` 服务方法,提交 `{ key, value }`
## 2. Settings Management Page
- [x] 2.1 在设置管理下新增系统配置路由和菜单入口。
- [x] 2.2 实现模块筛选、分页、刷新和配置列表展示。
- [x] 2.3 根据配置元数据动态渲染开关、数字、枚举、文本和 JSON 控件。
- [x] 2.4 实现只读配置禁用编辑、敏感值脱敏保护和字符串化提交。
- [x] 2.5 展示配置注册状态、更新时间、范围和校验提示。
## 3. Verification
- [x] 3.1 验证两个模块筛选和分页参数正确传递。
- [x] 3.2 验证 bool、int、enum、string 和 json 配置的展示及提交值。
- [x] 3.3 验证只读配置不能提交,敏感配置未修改时不会回传脱敏值。
- [x] 3.4 运行类型检查、lint 和构建。

View File

@@ -0,0 +1,52 @@
## Context
审计调查横跨平台、代理和企业三种身份,以及事件、资源、请求、业务链路、资金和外部交互等多种事实视角。后端通过内部资源 ID、主体安全 identifier、`investigation_refs`、Integration `linkage``fidelity` 明确表达可导航关系;前端的职责是原样传递这些稳定引用,而不是补全关系。
现有实现已经形成 `AuditInvestigationDrawer``AuditResourceSearchDialog` 和全局控制器,但业务页面仍需逐一核对入口覆盖、角色映射和降级规则。本设计将导航判断集中到共享目标解析层,展示仍由只读组件负责。
## Goals / Non-Goals
- Goals: 用一个类型安全的目标模型表达所有允许的审计调查入口。
- Goals: 完成文档中资产、组织、账号、交易、资金和调查节点的导航矩阵。
- Goals: 保证平台、代理和企业仅调用各自允许的接口,并隐藏缺少稳定参数的入口。
- Goals: 使用弹窗/右侧抽屉承载续查,锁定底层滚动并支持分页与空状态。
- Non-Goals: 不修改后端链路生成、数据库、留存或授权规则。
- Non-Goals: 不提供恢复、重试、处置、封禁、修改、删除或导出。
- Non-Goals: 不把旧 operation log 合并进新审计链路。
- Non-Goals: 不从 Access Log、历史队列、自由文本或时间邻近关系自动发现链路。
## Decisions
- Decision: 使用判别联合类型描述 `event/actor/resource/request/correlation/finance/integration/agent/enterprise` 调查目标;调用方只能提交该目标模型,不能提交任意 URL。
- Decision: 使用全局调查宿主承载共享抽屉和精确资源弹窗。业务页面操作只负责构造目标,不各自维护弹窗、加载和滚动状态。
- Decision: 独立的审计中心列表与详情可以保留路由;跨视角时间线和关联详情使用抽屉。移除旧时间线路由、别名和所有指向它的跳转。
- Decision: 平台资源入口优先使用业务响应中的内部稳定 ID。只有调用上下文没有资源 ID、但明确持有 `iot_card/device/shop/order/refund` 的业务 Key 时,才进入精确资源搜索。
- Decision: 精确资源搜索不展示 `resource_id` 或身份快照中的内部关联 ID。候选项只展示按资源类型白名单选择的业务字段选中后在内部原样使用返回的 `resource_type/resource_id`
- Decision: 代理使用 ICCID、VirtualNo、分配单号、换货单号、店铺编号或企业编号企业只使用当前授权卡的 ICCID 或设备 VirtualNo。主体身份和范围不作为前端参数。
- Decision: `investigation_refs` 是来源详情续查的权威来源。字段缺失、目标与当前详情相同或 `fidelity=false` 时隐藏对应操作;共享调查抽屉内部保持只读,不重复嵌套一组调查按钮。
- Decision: 请求和业务链路 ID 默认不展示原值;仅在明确的超级管理员开发调查上下文中允许复制真实 request ID且不得由前端生成。
- Decision: 资金目标只提交一个已知稳定条件,其他订单、支付、退款、充值和钱包关联由服务端解析;金额始终按分保存在应用数据中。
- Decision: `401/403`、授权撤销或主体资源不存在时显示中性“活动不可用”,不回退平台接口、精确资源搜索或旧 operation log。
- Decision: 所有抽屉使用 `append-to-body`、modal 和 scroll lock关闭后销毁内容分页切换保留目标和筛选切换目标时清空旧数据。
## Risks / Trade-offs
- 业务页面数量多,容易出现入口漏接或使用错误字段。通过集中目标解析函数和表驱动测试覆盖文档矩阵。
- 全局抽屉减少重复代码,但必须防止上一目标的数据闪现。打开新目标前先清空状态,并以当前请求标识忽略过期响应。
- 精确资源搜索结果来自当前表或历史快照;仅展示白名单字段会减少调试信息,但可以避免泄露内部 ID 和不稳定结构。
- 本变更与 `add-audit-chain-frontend-integration` 修改同一组件目录。实施前必须以其最新状态为基线,避免覆盖已完成的 UI 调整。
## Migration Plan
1. 固化共享调查目标类型、控制器和角色解析函数,并为现有入口提供兼容适配。
2. 将仍指向旧时间线路由的入口切换为全局调查宿主,确认无引用后删除旧页面、路由和别名。
3. 按文档矩阵依次接入资产组织页、账号交易页、资金页和调查节点续查。
4. 收紧精确搜索触发条件、主体视角和空标识降级规则。
5. 完成角色矩阵、参数原样传递、禁止推断、弹窗滚动和过期请求测试。
6. 若出现回归,可按业务模块隐藏新入口;不恢复任意 URL 跳转或 ID 推断逻辑。
## Open Questions
- 账号、代理充值、资产分配、换货、店铺资金概况等尚未接入页面的最终按钮权限是否沿用资源时间线/资金时间线权限,还是增加业务入口专用权限?
- 超级管理员是否需要一个显式的 Access Log request ID 粘贴入口;若不需要,前端应完全移除手工 request 查询。
- `historical=true` 在产品要求隐藏“匹配来源”后,是否仍需用非字段式的警告图标提示资源可能已删除?

View File

@@ -0,0 +1,41 @@
## 实施差异核对
### 已接入矩阵
| 来源 | 平台视角 | 代理视角 | 企业视角 | 稳定字段 |
| --- | --- | --- | --- | --- |
| IoT 卡列表/统一资产详情 | 资源时间线 | 主体活动 | 主体活动 | `id` / `iccid` |
| 设备列表/统一资产详情 | 资源时间线 | 主体活动 | 主体活动 | `id` / `virtual_no` |
| 设备绑定卡 | 卡资源时间线、绑定资源时间线 | 卡主体活动 | 卡主体活动 | `card_id` / `iccid` |
| 资产分配列表/详情 | 分配记录、资产资源时间线 | 分配活动 | 不提供 | `id``asset_id` / `allocation_no` |
| 换货列表/详情 | 换货记录、旧/新资产时间线 | 换货活动 | 不提供 | `id`、资产 ID / `exchange_no` |
| 店铺列表 | 店铺资源、店铺资金链路 | 店铺活动 | 不提供 | `id` / `shop_no` |
| 企业列表 | 企业资源 | 企业活动 | 不提供 | `id` / `enterprise_no` |
| 账号列表 | 账号资源 | 不提供 | 不提供 | `id` |
| 订单列表/详情 | 订单资源、订单资金链路 | 不提供 | 不提供 | `id``order_id` |
| 退款列表/详情 | 退款资源、退款资金链路、审批资源 | 不提供 | 不提供 | `id``refund_id``approval_instance_id` |
| 代理充值列表/详情 | 充值资源、充值资金链路、审批资源 | 不提供 | 不提供 | `id``recharge_id``approval_instance_id` |
| 资产钱包 | 钱包资金链路、资产资源 | 不提供 | 不提供 | `wallet_id``asset_type + asset_id` |
| 店铺资金概况 | 店铺资金链路 | 不提供 | 不提供 | `shop_id` |
所有入口先独立校验自身权限和完整参数;一个入口缺字段不会隐藏同一行的其他有效入口。平台资源只使用非零内部 ID代理和企业活动只使用文档规定的业务标识。
### 共享调查与降级
- 事件、操作者、资源、请求、业务链路、资金、Integration、代理活动和企业活动均由全局调查宿主打开。
- 精确注册资源只在调查引用缺少 `resource_id`、但包含受支持类型及业务 Key 时出现;业务列表已有内部 ID 时不再提供搜索按钮。
- 精确搜索限制为 `iot_card/device/shop/order/refund`,候选项不显示 Registry ID 或快照内部关联 ID。
- 目标切换会清空旧内容并忽略过期响应;关闭会销毁内容,抽屉和弹窗锁定底层滚动。
- 缺字段、空白值、零值、不支持的主体资源类型和撤销授权均不推断或回退到其他视角。
### 受响应字段限制的入口
- 钱包流水响应只有 `asset_identifier`,没有完整的 `asset_type + asset_id`,因此不从流水行推断资产审计目标。
- 当前路由中没有独立的账号详情和店铺详情页面,对应入口保留在列表。
- 风险/链路/资金节点抽屉内部不增加继续调查按钮:产品要求移除弹窗内无意义按钮;事件详情仍使用权威 `investigation_refs` 提供跨视角入口。
### 路由核对
- 独立的审计事件和外部交互列表/详情路由保留。
- 操作者、资源、请求、业务链路、资金及主体活动不再使用独立时间线路由或任意 URL 跳转。
- 审计中心顶级菜单排列在财务管理之后。

View File

@@ -0,0 +1,40 @@
# Change: 对齐审计跨视角调查与前端导航契约
## Why
现有审计中心已经接入事件、资源、链路、资金、风险和外部交互等只读查询,但业务页面入口、调查节点续查、平台/代理/企业视角以及缺失标识时的降级行为仍缺少统一约束。若各页面继续自行拼装目标参数,容易把内部 ID、业务 Key、主体安全标识或链路 ID 混用,并产生越权回退、错误串链和重复页面。
本变更以《跨视角调查与前端导航契约.md》为导航事实来源在现有 `add-audit-chain-frontend-integration` 能力之上建立统一的跨视角导航规范和复用组件。
## What Changes
- 建立统一的审计调查目标模型和导航控制器,覆盖事件详情、操作者行为、资源审计、请求链路、业务链路、资金链路、外部交互以及代理/企业资源活动。
- 调查时间线、链路和外部交互续查统一使用可复用抽屉或弹窗,不再新增独立时间线跳转页面;同一目标不重复打开自身详情。
- 按“内部稳定 ID → 主体安全业务标识 → 返回的调查引用 → 精确资源搜索 → 隐藏入口”的顺序解析入口,禁止从名称、摘要、时间、编号前缀或邻近记录推断关联。
- 对齐资产、组织、账号、订单、退款、充值、钱包和资金页面的逐行入口矩阵,并按平台、代理、企业身份选择不同接口和参数。
- 平台仅在缺少内部资源 ID、但持有允许搜索的 Registry Key 时调用精确资源搜索;零命中或多命中均保留用户选择,不自动猜测。
- 所有弹窗和抽屉只显示业务可读字段,内部资源 ID、request/correlation 等技术标识仅作为查询参数使用,除明确的开发调查场景外不直接展示。
- 统一处理空引用、`fidelity=false`、历史快照、授权撤销、归档窗口和接口拒绝,不回退旧日志、平台接口或其他主体接口。
- 补充跨页面入口矩阵、参数映射、角色隔离和禁止推断的自动化测试。
## Impact
- Affected specs: `audit-cross-view-navigation`
- Depends on: `add-audit-chain-frontend-integration`
- Affected code:
- `src/components/business/audit/` 调查抽屉、精确资源弹窗和全局导航控制器
- `src/utils/business/auditNavigation.ts` 角色与目标解析
- 审计事件、风险、外部交互页面的调查入口
- 卡、设备、设备卡槽、统一资产、分配、换货、店铺、企业、账号、订单、退款、充值、钱包及资金列表/详情
- 路由、权限和通知目标导航
- API contracts:
- `GET /api/admin/audit/*`
- `GET /api/admin/agent/resource-activities/{resource_type}/{identifier}`
- `GET /api/admin/enterprise/resource-activities/{resource_type}/{identifier}`
- 现有业务列表与详情接口返回的稳定字段
- Source of truth:
- `docs/产品迭代8月份/跨视角调查与前端导航契约.md`
- `docs/产品迭代8月份/默认模块.openapi.json`
- Breaking changes:
- 移除旧的独立审计时间线路由及其导航方式,统一改为抽屉/弹窗调查。
- 入口缺少契约要求的稳定标识时将被隐藏,不再容忍前端推断或降级调用其他视角接口。

View File

@@ -0,0 +1,130 @@
## ADDED Requirements
### Requirement: Shared Cross-View Investigation Surface
The frontend SHALL open actor, resource, request, correlation, finance, Integration, agent-activity and enterprise-activity investigations through reusable modal or drawer components. It SHALL NOT require a standalone timeline route for these cross-view investigations.
#### Scenario: Open an investigation from a business row
- **WHEN** an authorized user selects an audit or activity action from a supported business row
- **THEN** the frontend MUST construct a typed investigation target from the row's documented stable fields
- **AND** it MUST open the shared investigation surface without navigating to a standalone timeline page
#### Scenario: Replace an open investigation target
- **WHEN** the user opens a second investigation after another target was loaded
- **THEN** the shared surface MUST clear stale content before loading the new target
- **AND** a late response from the previous target MUST NOT replace the current result
### Requirement: Deterministic Investigation Target Resolution
The frontend SHALL resolve investigation targets in this order: a platform internal stable ID for a platform resource timeline, a documented subject-safe business identifier for agent or enterprise activity, an explicit API investigation reference, an exact Registry resource-key search when no internal resource ID exists, or no entry. It SHALL NOT infer a target from display text or proximity.
#### Scenario: A platform resource ID is available
- **WHEN** a platform business response contains the documented non-zero internal resource ID
- **THEN** the frontend MUST call the resource timeline with the mapped resource type and that ID
- **AND** it MUST NOT search by the display name or business Key to replace the available ID
#### Scenario: Only a supported Registry Key is available
- **WHEN** a platform investigation reference contains a supported resource type and business Key but no resource ID
- **THEN** the frontend MUST use the exact resource search endpoint
- **AND** zero or multiple matches MUST remain unresolved until the user makes an explicit choice
#### Scenario: No stable target exists
- **WHEN** the required internal ID, subject identifier or explicit investigation reference is missing
- **THEN** the frontend MUST hide or disable the entry
- **AND** it MUST NOT infer a relationship from a name, summary, timestamp, identifier prefix, adjacent record or current-account profile
### Requirement: Exact Registered Resource Selection
The frontend SHALL support exact Registry lookup only for `iot_card`, `device`, `shop`, `order` and `refund`. It SHALL pass the selected candidate's returned `resource_type/resource_id` unchanged to the resource timeline while keeping internal identifiers out of the presentation.
#### Scenario: Present an exact resource candidate
- **WHEN** exact lookup returns a candidate with `identity_snapshot`
- **THEN** the frontend MUST display only a resource-type-specific whitelist of business-readable fields
- **AND** it MUST NOT display `resource_id` or snapshot fields that are internal IDs or internal foreign keys
#### Scenario: Select a historical candidate
- **WHEN** a user selects a candidate with `historical=true` and a stable returned resource ID
- **THEN** the frontend MUST use the returned type and ID without modification
- **AND** it MUST NOT claim that the resource still exists in the current business table
### Requirement: Role-Isolated Resource Activity Navigation
The frontend SHALL choose platform, agent or enterprise resource investigation APIs solely from the authenticated subject and the documented source-page fields. Agent and enterprise navigation SHALL NOT fall back to platform audit or Registry search.
#### Scenario: Open agent activity
- **WHEN** an agent opens a supported card, device, allocation, exchange, shop or enterprise activity
- **THEN** the frontend MUST use the documented ICCID, VirtualNo or business number with the agent activity endpoint
- **AND** it MUST NOT send agent identity or shop scope as proof of authorization
#### Scenario: Open enterprise activity
- **WHEN** an enterprise user opens activity for a currently authorized card or device
- **THEN** the frontend MUST use ICCID or VirtualNo with the enterprise activity endpoint
- **AND** it MUST NOT use the enterprise route parameter or a platform internal ID as authorization evidence
#### Scenario: Subject activity becomes unavailable
- **WHEN** authorization is revoked, the resource is out of scope, or the subject endpoint rejects access
- **THEN** the frontend MUST present an activity-unavailable state
- **AND** it MUST NOT retry through a platform endpoint, Registry search or legacy operation log
### Requirement: Business and Finance Entry Matrix
The frontend SHALL implement the documented row-level navigation matrix for asset, organization, account, order, refund, recharge, wallet and fund-summary contexts. Each action SHALL be independently visible only when its complete stable parameter set and required permission are available.
#### Scenario: Open resource and finance investigations independently
- **WHEN** a business record contains both a resource ID and a stable finance condition
- **THEN** the frontend MUST offer the applicable resource audit and finance investigation actions independently
- **AND** failure or absence of one parameter set MUST NOT suppress the other valid action
#### Scenario: Let the server resolve finance relationships
- **WHEN** the frontend opens finance history from an order, refund, recharge, wallet or shop
- **THEN** it MUST submit only the stable condition explicitly returned by the source business API
- **AND** it MUST NOT derive or supplement payment, refund, wallet, approval or third-party identifiers from text or number prefixes
### Requirement: Investigation Reference Continuation
The frontend SHALL treat `investigation_refs` and documented Integration references on supported source details as the authoritative sources for continuing an investigation. It SHALL expose only actions whose complete required reference is present and reliable. Shared investigation drawers SHALL remain read-only and SHALL NOT repeat nested investigation action groups inside their result nodes.
#### Scenario: Continue from a complete reference
- **WHEN** a supported source detail contains a complete actor, resource, request, correlation or Integration reference
- **THEN** the frontend MUST pass its type and identifier unchanged to the matching shared investigation surface
#### Scenario: Display a result inside the shared drawer
- **WHEN** a shared investigation drawer renders an event, resource, link, finance or Integration result
- **THEN** it MUST present that result as read-only evidence without nested investigation action buttons
- **AND** it MUST preserve documented source and reference-only boundaries in the displayed facts
#### Scenario: Suppress unavailable or self-referential navigation
- **WHEN** a reference is incomplete, its fidelity is false, or its event target is the detail already being displayed
- **THEN** the frontend MUST hide the corresponding action
- **AND** it MUST NOT create a substitute identifier or reopen the same detail
### Requirement: Read-Only Presentation and Scroll Isolation
All shared investigation surfaces SHALL remain read-only, hide non-business internal identifiers by default, and isolate modal scrolling from the underlying page.
#### Scenario: Display an investigation surface
- **WHEN** a modal or drawer is open
- **THEN** it MUST use a modal overlay, append to the document body, and lock underlying-page scrolling
- **AND** it MUST remain usable within its own scroll container at supported viewport sizes
#### Scenario: Inspect a read-only result
- **WHEN** audit, activity, finance, link or Integration data is displayed
- **THEN** the UI MUST NOT expose mutation, recovery, retry, deletion, disposition, blocking or export actions
- **AND** raw internal IDs MUST be retained only as navigation parameters unless an explicitly authorized developer-investigation workflow requires display

Some files were not shown because too many files have changed in this diff Show More