Compare commits
80 Commits
main
...
iteration/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dec2b42f8 | ||
|
|
0d094a0537 | ||
|
|
c907d6e21f | ||
|
|
227dd63bb8 | ||
|
|
8fe65ca66d | ||
|
|
8b01169219 | ||
|
|
3411b66c71 | ||
|
|
984db9889c | ||
|
|
49a4d32eab | ||
|
|
d24506ab54 | ||
|
|
8569255f75 | ||
|
|
2f4eed917e | ||
|
|
1a1ec485a5 | ||
|
|
e41adb4505 | ||
|
|
2c8de04041 | ||
|
|
98a4be8719 | ||
|
|
f5730089a1 | ||
|
|
f2614d8efb | ||
|
|
06684e1fc0 | ||
|
|
049f8257fb | ||
|
|
ecce4f6e7b | ||
|
|
c706f543a4 | ||
|
|
d82e716d33 | ||
|
|
22e796ba23 | ||
|
|
6dedd9f852 | ||
|
|
7f3ff10fda | ||
|
|
eeeab03295 | ||
|
|
0d77a7d55b | ||
|
|
16effb69f3 | ||
|
|
c31183205d | ||
|
|
b2a76adc9a | ||
|
|
52268859aa | ||
|
|
f1006187f6 | ||
|
|
944f17ec3f | ||
|
|
633c88d0aa | ||
|
|
f61a3f84b6 | ||
|
|
327516dd40 | ||
|
|
2f23aeaaba | ||
|
|
2c64957e55 | ||
|
|
1a5f3d1e7d | ||
|
|
d02ce01f6a | ||
|
|
aec2a27a65 | ||
|
|
f896cad34f | ||
|
|
7a74bec337 | ||
|
|
5364916b1d | ||
|
|
6655638116 | ||
|
|
e79d599da1 | ||
|
|
015f990fb6 | ||
|
|
063337e6a3 | ||
|
|
c26d36154c | ||
|
|
669610a84a | ||
|
|
a26d7b4284 | ||
|
|
10fccc4def | ||
|
|
0a28b7d8ef | ||
|
|
c136f68ab8 | ||
|
|
c5d7a260d0 | ||
|
|
ffba959d45 | ||
|
|
9a6761e01d | ||
|
|
2c00341a7c | ||
| 5c1dc59579 | |||
| d52219b5c8 | |||
| bce5d6f270 | |||
| 42e57cf698 | |||
| 55eec78a90 | |||
| cb1561a317 | |||
|
|
94764b0e0a | ||
|
|
a6ce24f2f2 | ||
|
|
085a83d4e4 | ||
|
|
e37d4902ca | ||
|
|
08889d2e42 | ||
|
|
5e7f5eaba4 | ||
|
|
ac541b17af | ||
|
|
68d8f769d1 | ||
|
|
068d0c4ac2 | ||
|
|
9a3a764120 | ||
|
|
cea805f5d8 | ||
|
|
76d4e4ec02 | ||
|
|
2308d82d0f | ||
|
|
d3d257cdf8 | ||
|
|
ef966171b4 |
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.husky
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
openspec
|
||||||
|
.codex
|
||||||
|
.claude
|
||||||
|
.agents
|
||||||
@@ -3,9 +3,12 @@ name: 构建并部署前端到测试环境
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- iteration/8-11
|
||||||
- dev
|
- dev
|
||||||
- test
|
- test
|
||||||
|
# 如果不再需要 main 分支触发构建,可以把下面这行删掉;
|
||||||
|
# 如果只是想保留构建(不部署),可以留着,反正部署条件已经只认 iteration/8-11
|
||||||
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: registry.boss160.cn
|
REGISTRY: registry.boss160.cn
|
||||||
@@ -13,7 +16,7 @@ env:
|
|||||||
DEPLOY_DIR: /opt/junhong_cmp_web
|
DEPLOY_DIR: /opt/junhong_cmp_web
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -24,67 +27,49 @@ jobs:
|
|||||||
git clone https://git.boss160.cn/luo/one-pipe-system.git .
|
git clone https://git.boss160.cn/luo/one-pipe-system.git .
|
||||||
git checkout ${{ github.sha }}
|
git checkout ${{ github.sha }}
|
||||||
|
|
||||||
- name: 设置镜像标签
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
|
||||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
|
||||||
echo "tag=dev" >> $GITHUB_OUTPUT
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/test" ]; then
|
|
||||||
echo "tag=test" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "tag=unknown" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 登录 Docker Registry
|
- name: 登录 Docker Registry
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${{ env.REGISTRY }}" --username "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${{ env.REGISTRY }}" --username "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
- name: 构建前端镜像
|
- name: 校验镜像存在
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} .
|
if ! docker manifest inspect "${{ env.IMAGE_NAME }}:${{ github.sha }}" >/dev/null 2>&1; then
|
||||||
docker tag ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} ${{ env.IMAGE_NAME }}:${{ github.sha }}
|
echo "镜像不存在:${{ env.IMAGE_NAME }}:${{ github.sha }}"
|
||||||
|
echo "请先在开发机完成 pre-push 镜像构建和推送"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: 推送镜像到 Registry
|
- name: 部署到测试环境
|
||||||
|
if: github.ref == 'refs/heads/iteration/8-11'
|
||||||
run: |
|
run: |
|
||||||
docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
mkdir -p "${{ env.DEPLOY_DIR }}"
|
||||||
docker push ${{ env.IMAGE_NAME }}:${{ github.sha }}
|
cp -v docker-compose.prod.yml "${{ env.DEPLOY_DIR }}/"
|
||||||
|
cd "${{ env.DEPLOY_DIR }}"
|
||||||
|
|
||||||
- name: 部署到本地(仅 main 分支)
|
IMAGE_TAG=${{ github.sha }} docker compose -f docker-compose.prod.yml pull
|
||||||
if: github.ref == 'refs/heads/main'
|
IMAGE_TAG=${{ github.sha }} docker compose -f docker-compose.prod.yml up -d
|
||||||
run: |
|
|
||||||
# 确保部署目录存在
|
|
||||||
mkdir -p ${{ env.DEPLOY_DIR }}
|
|
||||||
|
|
||||||
echo "当前工作目录: $(pwd)"
|
echo "等待服务健康检查通过..."
|
||||||
echo "当前目录内容:"
|
for i in $(seq 1 30); do
|
||||||
ls -la
|
WEB_HEALTH=$(docker inspect junhong-cmp-web --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' 2>/dev/null || true)
|
||||||
|
if [ "$WEB_HEALTH" = "healthy" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" -eq 30 ]; then
|
||||||
|
echo "前端服务在等待窗口内未恢复健康"
|
||||||
|
docker compose -f docker-compose.prod.yml logs web --tail=100
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
# 更新 docker-compose.prod.yml
|
IMAGE_TAG=${{ github.sha }} docker compose -f docker-compose.prod.yml ps
|
||||||
echo "更新部署配置文件..."
|
|
||||||
cp -v docker-compose.prod.yml ${{ env.DEPLOY_DIR }}/
|
|
||||||
|
|
||||||
cd ${{ env.DEPLOY_DIR }}
|
|
||||||
|
|
||||||
echo "拉取最新镜像..."
|
|
||||||
docker compose -f docker-compose.prod.yml pull
|
|
||||||
|
|
||||||
echo "重启服务..."
|
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
|
|
||||||
echo "等待服务启动..."
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
echo "部署完成!"
|
|
||||||
docker compose -f docker-compose.prod.yml ps
|
|
||||||
|
|
||||||
- name: 构建结果通知
|
- name: 构建结果通知
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ job.status }}" = "success" ]; then
|
if [ "${{ job.status }}" = "success" ]; then
|
||||||
echo "构建成功: ${{ steps.tag.outputs.tag }}"
|
echo "部署成功: ${{ github.sha }}"
|
||||||
echo "镜像标签: ${{ github.sha }}"
|
|
||||||
else
|
else
|
||||||
echo "构建失败"
|
echo "部署失败"
|
||||||
fi
|
fi
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -2,6 +2,13 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
docs
|
docs
|
||||||
|
!docs/
|
||||||
|
docs/*
|
||||||
|
!docs/权限.md
|
||||||
|
!docs/api/
|
||||||
|
docs/api/*
|
||||||
|
!docs/api/package-silence.openapi.json
|
||||||
|
!docs/2026-10-26-轮询账户限流-后台接入交付.md
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
.cursorrules
|
.cursorrules
|
||||||
|
|||||||
1
.husky/pre-push
Executable file
1
.husky/pre-push
Executable file
@@ -0,0 +1 @@
|
|||||||
|
node scripts/deploy-frontend.mjs --pre-push
|
||||||
11
Dockerfile
11
Dockerfile
@@ -6,15 +6,16 @@ FROM --platform=linux/amd64 node:20-alpine AS builder
|
|||||||
# 使用阿里云镜像源加速
|
# 使用阿里云镜像源加速
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# 安装 pnpm
|
# 清除宿主机注入的失效代理,再安装 pnpm
|
||||||
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
|
RUN npm config delete proxy && npm config delete https-proxy && npm install --global pnpm@10.15.0 --registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
# 设置 npm 镜像源
|
|
||||||
RUN pnpm config set registry https://registry.npmmirror.com
|
RUN pnpm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 限制 Node 堆,防止前端构建耗尽宿主机内存
|
||||||
|
ARG NODE_OPTIONS=--max-old-space-size=1536
|
||||||
|
ENV NODE_OPTIONS=${NODE_OPTIONS}
|
||||||
|
|
||||||
# 复制 package.json 和 pnpm-lock.yaml(利用 Docker 缓存)
|
# 复制 package.json 和 pnpm-lock.yaml(利用 Docker 缓存)
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
version: '3.8'
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
image: registry.boss160.cn/junhong/cmp-admin-web:latest
|
image: registry.boss160.cn/junhong/cmp-admin-web:${IMAGE_TAG:-latest}
|
||||||
container_name: junhong-cmp-web
|
container_name: junhong-cmp-web
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '3001:80'
|
- '3001:80'
|
||||||
networks:
|
networks:
|
||||||
- junhong-network
|
- junhong-network
|
||||||
|
|
||||||
# === 以下是新增的修复内容 ===
|
# === 以下是新增的修复内容 ===
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /run:rw
|
- /run:rw
|
||||||
- /tmp:rw
|
- /tmp:rw
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:80/health']
|
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:80/health']
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 5s
|
start_period: 5s
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
driver: 'json-file'
|
driver: 'json-file'
|
||||||
options:
|
options:
|
||||||
@@ -29,4 +28,4 @@ services:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
junhong-network:
|
junhong-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
128
docs/2026-10-26-轮询账户限流-后台接入交付.md
Normal file
128
docs/2026-10-26-轮询账户限流-后台接入交付.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# 轮询账户限流:后台接入交付
|
||||||
|
|
||||||
|
本次仅修改本后台前端,未修改独立 C 端、未推送 Git 或部署生产、未配置数据库菜单。真实接口权限和调度行为仍需测试环境联调。本地写操作全部通过 mock 验证。
|
||||||
|
|
||||||
|
## 后台配置与使用
|
||||||
|
|
||||||
|
新增菜单、隐藏详情、按钮编码和平台账号限制见 [权限增量清单](./权限.md#轮询管理运营商账户限流与调度2026-10-26-增量)。原有网关、轮询配置预览/统计、停复机记录 URL 和权限保留。仅超级管理员的维护项不列入普通配置清单。
|
||||||
|
|
||||||
|
UI 沿用店铺列表表头、可选列、分页和店铺详情返回/标题/两列布局;窄屏详情单列。时间统一店铺格式,运营商账户按名称远程搜索,搜索条件 >=4 才出现展开。
|
||||||
|
|
||||||
|
新增配置字段现可保存,旧文档“仅预览”已失效。四项间隔:空继承、0 禁止且不再继承、正数执行。编辑清空使用 clear_fields,保存 warnings 单独展示;不再发送保护期间隔。
|
||||||
|
|
||||||
|
## C 端需要同步哪些
|
||||||
|
|
||||||
|
**必须修改(文档明确要求)**
|
||||||
|
|
||||||
|
1. C 端资产详情调用 `GET /api/c/v1/asset/info` 后,不再展示保护期状态(`device_protect_status`),包括标签、说明、倒计时和相关状态分支(如有)。
|
||||||
|
2. 废弃字段可能仍返回固定 `none`,也可能缺失;兼容解析,但不能再依赖它决定页面展示或是否允许操作。不要把 `none` 当成新的异常状态。
|
||||||
|
|
||||||
|
**随新后端回归验证(不是要求 C 端增加后台功能)**
|
||||||
|
|
||||||
|
- 订单/套餐生效、有效期变化后,重新读取资产实际 `enable_polling`;后台可能自动恢复轮询,不要仅凭旧前端缓存维持“已关闭”。
|
||||||
|
- 指令队列启用时,业务受理不等于上游已完成停复机。若 C 端有即时完成提示,改为区分“已受理/处理中”和实际状态,并在合理刷新后确认。
|
||||||
|
- 资产详情读入口的请求检查、延迟确认及冷却由后端场景配置控制,C 端不要自行复制调度逻辑或高频循环触发。
|
||||||
|
|
||||||
|
C 端不需要新增本次后台菜单、告警/调度参数/导入接口;本仓库没有修改任何 C 端代码。若 C 端本来没有保护期 UI 或逻辑,第一项只需核查删除残留引用。
|
||||||
|
|
||||||
|
## 接口覆盖
|
||||||
|
|
||||||
|
文档 46 个新增操作中,已有 GatewayService 17 个、PollingConfigService 7 个,本次 PollingSchedulerService 补 22 个。自动测试逐条解析原文附录的 46 个方法/路径,与实际服务调用对照,不以数量代替路径验收。
|
||||||
|
|
||||||
|
下表路径省略 `/api/admin`。精确 URL、Query/Body 测试位于 `gateway.test.mjs`、`pollingConfigExtensions.test.mjs`、`pollingScheduler.test.mjs`。
|
||||||
|
|
||||||
|
| HTTP 操作 | 服务方法 | 后台入口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `DELETE /gateway-error-classifications/{id}` | `GatewayService.deleteRule` | 上游错误分类 |
|
||||||
|
| `DELETE /polling-carrier-params/{carrier_id}` | `PollingSchedulerService.resetCarrierParam` | 账户参数维护 |
|
||||||
|
| `DELETE /polling-rate-limit-groups/{id}` | `GatewayService.deleteGroup` | 网关限流组维护 |
|
||||||
|
| `GET /card-polling-pauses` | `PollingSchedulerService.getPauses` | 不轮询清单 |
|
||||||
|
| `GET /card-stop-resume-records` | `GatewayService.getRecords` | 停复机记录 |
|
||||||
|
| `GET /gateway-error-classifications` | `GatewayService.getRules` | 上游错误分类 |
|
||||||
|
| `GET /iot-cards/{id}/polling-state` | `PollingSchedulerService.getCardState` | 单卡/设备绑定卡/告警详情 |
|
||||||
|
| `GET /iot-cards/{id}/stop-resume-records` | `GatewayService.getCardRecords` | 单卡/告警详情 |
|
||||||
|
| `GET /polling-carrier-params` | `PollingSchedulerService.getCarrierParams` | 运营商账户调度参数 |
|
||||||
|
| `GET /polling-carrier-params/{carrier_id}` | `PollingSchedulerService.getCarrierParam` | 账户参数详情 |
|
||||||
|
| `GET /polling-check-scenes` | `PollingSchedulerService.getScenes` | 请求检查场景 |
|
||||||
|
| `GET /polling-configs/match-preview` | `PollingConfigService.previewMatch` | 配置匹配预览 |
|
||||||
|
| `GET /polling-configs/recalculate-due/{task_id}` | `PollingConfigService.getRecalculateTask` | 重算进度 |
|
||||||
|
| `GET /polling-failure-alerts` | `PollingSchedulerService.getAlerts` | 告警普通/停机被拒 |
|
||||||
|
| `GET /polling-failure-alerts/summary` | `PollingSchedulerService.getAlertSummary` | 告警账户汇总 |
|
||||||
|
| `GET /polling-failure-alerts/{id}` | `PollingSchedulerService.getAlert` | 告警详情 |
|
||||||
|
| `GET /polling-options` | `GatewayService.getOptions` | 网关各页枚举 |
|
||||||
|
| `GET /polling-rate-limit-groups` | `GatewayService.getGroups` | 网关限流组 |
|
||||||
|
| `GET /polling-rate-limit-groups/{id}` | `GatewayService.getGroup` | 网关限流组详情 |
|
||||||
|
| `GET /polling-stats/carrier-queues` | `PollingSchedulerService.getQueues` | 账户队列监控 |
|
||||||
|
| `GET /polling-stats/carriers` | `GatewayService.getCarrierStats` | 网关汇总/限流模式 |
|
||||||
|
| `GET /polling-stats/carriers/{carrier_id}/trend` | `GatewayService.getCarrierTrend` | 账户调用趋势 |
|
||||||
|
| `GET /polling-stats/pre-reset` | `PollingConfigService.getPreResetStats` | 重置日前统计 |
|
||||||
|
| `GET /polling-stats/tiers` | `PollingConfigService.getTierStats` | 分层统计 |
|
||||||
|
| `GET /polling-status-imports` | `PollingSchedulerService.getImports` | 导入任务列表 |
|
||||||
|
| `GET /polling-status-imports/{id}` | `PollingSchedulerService.getImport` | 导入逐行结果 |
|
||||||
|
| `GET /polling-status/impact` | `PollingSchedulerService.getImpact` | 单项/批量/导入确认 |
|
||||||
|
| `GET /stop-resume-rule/dry-run` | `PollingSchedulerService.dryRun` | 规则演练(只读) |
|
||||||
|
| `POST /devices/batch-update-polling-status` | `PollingSchedulerService.toggleDevices` | 设备批量/清单恢复 |
|
||||||
|
| `POST /gateway-error-classifications` | `GatewayService.createRule` | 上游错误分类 |
|
||||||
|
| `POST /gateway-error-classifications/test` | `GatewayService.testRule` | 错误分类测试 |
|
||||||
|
| `POST /iot-cards/batch-update-polling-status` | `PollingSchedulerService.toggleCards` | 卡批量/清单恢复 |
|
||||||
|
| `POST /polling-configs/match-preview` | `PollingConfigService.previewDraft` | 配置草稿预览 |
|
||||||
|
| `POST /polling-configs/recalculate-due` | `PollingConfigService.recalculateDue` | 重算到期 |
|
||||||
|
| `POST /polling-failure-alerts/close-polling` | `PollingSchedulerService.closeAlertPolling` | 告警批量关闭卡轮询 |
|
||||||
|
| `POST /polling-rate-limit-groups` | `GatewayService.createGroup` | 网关限流组维护 |
|
||||||
|
| `POST /polling-status-imports` | `PollingSchedulerService.createImport` | 创建导入 |
|
||||||
|
| `POST /stop-resume-rule/dry-run/export` | `PollingSchedulerService.exportDryRun` | 演练导出 |
|
||||||
|
| `PUT /gateway-error-classifications/{id}` | `GatewayService.updateRule` | 上游错误分类 |
|
||||||
|
| `PUT /gateway-error-classifications/{id}/status` | `GatewayService.setRuleStatus` | 上游错误分类 |
|
||||||
|
| `PUT /polling-carrier-params/{carrier_id}` | `PollingSchedulerService.saveCarrierParam` | 账户参数维护 |
|
||||||
|
| `PUT /polling-check-scenes/{scene}` | `PollingSchedulerService.saveScene` | 请求检查场景维护 |
|
||||||
|
| `PUT /polling-configs/priorities` | `PollingConfigService.updatePriorities` | 整体优先级(拖动/上移下移) |
|
||||||
|
| `PUT /polling-failure-alerts/{id}/status` | `PollingSchedulerService.handleAlert` | 告警处理 |
|
||||||
|
| `PUT /polling-rate-limit-groups/{id}` | `GatewayService.updateGroup` | 网关限流组维护 |
|
||||||
|
| `PUT /polling-rate-limit-groups/{id}/status` | `GatewayService.setGroupStatus` | 网关限流组维护 |
|
||||||
|
|
||||||
|
### 16 个既有接口改动的处理
|
||||||
|
|
||||||
|
| 既有接口 | 接入结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| GET /polling-configs | 列表新增资产/套餐条件、重置日前窗口和间隔列 |
|
||||||
|
| GET /polling-configs/:id | 编辑读入全部新增字段,废弃保护间隔不再参与 |
|
||||||
|
| POST /polling-configs | 白名单保存新增匹配及窗口字段、三态间隔,展示 warnings |
|
||||||
|
| PUT /polling-configs/:id | clear_fields 显式清空,0 保留,展示 warnings |
|
||||||
|
| GET /assets/resolve/:identifier | 不再映射废弃保护期展示;其余资产身份不变 |
|
||||||
|
| GET /assets/:identifier/realtime-status | 不再更新保护期 UI 字段 |
|
||||||
|
| POST /assets/:identifier/refresh | 沿用刷新读取,界面无保护期展示 |
|
||||||
|
| GET /api/c/v1/asset/info | 独立 C 端同步项,本仓库不改 |
|
||||||
|
| PUT /carriers/:id | 0→1 首次不带确认;展示后端实际影响后确认重提 |
|
||||||
|
| GET /system-configs | 新增 polling 模块,复用 string/int/bool/json 编辑与校验 |
|
||||||
|
| GET /iot-cards/standalone | 三态是否轮询筛选,保留 false |
|
||||||
|
| GET /devices | 新增筛选与是否轮询列 |
|
||||||
|
| PATCH /assets/:identifier/polling-status | 先读 impact 确认,拒绝/取消恢复 UI,不提供强制参数 |
|
||||||
|
| POST /storage/upload-url | purpose=polling_status_import;校验 key 前缀后上传,成功才建任务 |
|
||||||
|
| POST /export-tasks | 三个新增场景;卡/设备保留 enable_polling 筛选 |
|
||||||
|
| GET /export-tasks | 三个场景列表、共用详情与下载权限;历史文件不做客户端改写 |
|
||||||
|
|
||||||
|
`PUT /system-configs/:key` 继续走原编辑能力,新增键来自服务器;本次不会自动修改 scheduler、rate_limit_mode、queue_enabled。
|
||||||
|
|
||||||
|
## 验证与限制
|
||||||
|
|
||||||
|
本地复现:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$tests = @(rg --files scripts/tests -g '*.test.mjs')
|
||||||
|
node --import ./scripts/tests/register-esbuild.mjs --test $tests
|
||||||
|
.\node_modules\.bin\vue-tsc.cmd --noEmit
|
||||||
|
node scripts/tests/verifyBuild.mjs
|
||||||
|
node scripts/tests/verifyBuild.mjs --serve --scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
最后一项在 `http://127.0.0.1:5190` 启动本地 mock,所有 /api 被拦截;可使用 local-scheduler-platform/admin/agent/enterprise 模拟 token。该结果不等于线上后端已联调。
|
||||||
|
|
||||||
|
已覆盖:46 路径映射、22 新方法请求结构、null/0/false、清空与废弃字段、权限/身份、500 条和 CSV 50000 行边界、引号换行、部分成功、影响说明失败/取消、上传失败、迟到请求/账号范围切换/卸载、模板编译。旧实名管控回归改成后端实际文案确认。
|
||||||
|
|
||||||
|
生产构建的既有循环依赖、常量 v-model 和大 chunk 警告仍存在;未在本次跨范围重构。后端若将实名确认提示升级为独立错误码,可替换当前的语义识别,避免依赖提示中的“实名/确认”关键词。当前未知网络错误或不相关错误不会触发确认重试。
|
||||||
|
|
||||||
|
2026-10-26 最终回归:全量 191 项通过(本次调度接入新增 17 项,保留并升级原有回归)。类型检查、生产构建、针对性 ESLint / Prettier / 新增组件样式检查、全仓 UTF-8 编码检查通过。
|
||||||
|
|
||||||
|
浏览器使用本地 mock 验证了平台账号入口、特殊告警视图、批量 impact 确认与四类逐项结果、CSV 上传建任务/详情、演练汇总与明细、时间格式、网关 80% 成功率及 p95 超桶显示;检查了浅/深色与 375/390/768/1024/1440 宽度布局。开发热更新仍可能遇到项目既有 API 循环依赖,刷新后进行验收,不把热更新错误当作真实接口结果。
|
||||||
|
|
||||||
|
真实后端账号、数据范围、错误码、异步终态和导出文件内容需由部署后的联调确认;本次未进行线上写入、Git 推送或部署。
|
||||||
4390
docs/api/package-silence.openapi.json
Normal file
4390
docs/api/package-silence.openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
437
docs/权限.md
Normal file
437
docs/权限.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 权限清单
|
||||||
|
|
||||||
|
## 套餐管理:真流量预警规则
|
||||||
|
|
||||||
|
页面 URL:`/package-management/traffic-alert-rules`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| -------- | ------------- | ------------------------------------ |
|
||||||
|
| 页面查看 | `rulesView` | `package_traffic_alert:rules_view` |
|
||||||
|
| 新增规则 | `ruleCreate` | `package_traffic_alert:rule_create` |
|
||||||
|
| 编辑规则 | `ruleUpdate` | `package_traffic_alert:rule_update` |
|
||||||
|
| 启用规则 | `ruleEnable` | `package_traffic_alert:rule_enable` |
|
||||||
|
| 停用规则 | `ruleDisable` | `package_traffic_alert:rule_disable` |
|
||||||
|
|
||||||
|
启用和停用不得复用编辑权限。
|
||||||
|
|
||||||
|
## 套餐管理:真流量达量预警
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 预警列表 | `/package-management/traffic-alerts` | `recordsView` | `package_traffic_alert:records_view` |
|
||||||
|
| 点击套餐名称进入预警详情 | `/package-management/traffic-alerts/detail/:id` | `recordDetail` | `package_traffic_alert:record_detail` |
|
||||||
|
| 导出预警记录 | — | `export` | `package_traffic_alert:export` |
|
||||||
|
| 导出任务页面 | `/asset-management/export-task-management/export-package-traffic-alert` | `page` | `export_task:package_traffic_alert_page` |
|
||||||
|
|
||||||
|
## 店铺管理:店铺列表
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 店铺列表 | `/shop-management/list` | `listView` | `shop:list_view` |
|
||||||
|
| 店铺详情 | `/shop-management/detail/:id` | `detailView` | `shop:detail_view` |
|
||||||
|
| 新增店铺 | — | — | `shop:add` |
|
||||||
|
| 批量交接 | — | `batch` | `shop:business_owner_batch` |
|
||||||
|
| 导入业务员 | — | `importPage` | `shop:business_owner_import_view` |
|
||||||
|
| 查看店铺详情 | — | — | `shop:detail` |
|
||||||
|
| 切换店铺状态 | — | — | `shop:modify_status` |
|
||||||
|
| 账号列表 | — | — | `shop:look_customer` |
|
||||||
|
| 默认角色 | — | — | `shop:default_role` |
|
||||||
|
| 调整实际信用额度 | — | — | `shop:credit_limit_manage` |
|
||||||
|
| 编辑店铺 | — | — | `shop:edit` |
|
||||||
|
| 删除店铺 | — | — | `shop:delete` |
|
||||||
|
| 审计记录 | — | — | `audit:shop_entry` / `audit:agent_resource_activity` |
|
||||||
|
| 资金链路 | — | — | `audit:finance_timeline` |
|
||||||
|
|
||||||
|
## 店铺管理:业务用户组
|
||||||
|
|
||||||
|
页面 URL:`/shop-management/business-user-groups`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| -------- | --------- | ----------------------------- |
|
||||||
|
| 页面查看 | `page` | `business_user_group:page` |
|
||||||
|
| 新增 | `create` | `business_user_group:create` |
|
||||||
|
| 编辑 | `update` | `business_user_group:update` |
|
||||||
|
| 删除 | `delete` | `business_user_group:delete` |
|
||||||
|
| 成员管理 | `members` | `business_user_group:members` |
|
||||||
|
|
||||||
|
## 店铺管理:批量换绑业务员
|
||||||
|
|
||||||
|
页面 URL:`/shop-management/business-owner-import`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| ---------------------------- | -------------- | ----------------------------------- |
|
||||||
|
| 页面查看 | `importPage` | `shop:business_owner_import_view` |
|
||||||
|
| 创建导入任务 | `importCreate` | `shop:business_owner_import_create` |
|
||||||
|
| 详情按钮 | `importDetail` | `shop:business_owner_import_detail` |
|
||||||
|
| 详情页面(当前为列表页抽屉) | `importDetail` | `shop:business_owner_import_detail` |
|
||||||
|
|
||||||
|
## 店铺管理:代理注册申请
|
||||||
|
|
||||||
|
列表不显示操作列,点击“申请店铺”名称进入详情页。
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 申请列表 | `/shop-management/agent-distribution-registrations` | `list` | `agent_distribution_registration:list` |
|
||||||
|
| 申请详情 | `/shop-management/agent-distribution-registrations/detail/:id` | `detail` | `agent_distribution_registration:detail` |
|
||||||
|
| 恢复审批 | — | `recoverApproval` | `agent_distribution_registration:recover_approval` |
|
||||||
|
|
||||||
|
## 资产管理:手机号资产关联
|
||||||
|
|
||||||
|
页面 URL:`/asset-management/phone-asset-association`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| ---------------- | -------------------- | ---------------------------------------------- |
|
||||||
|
| 页面查看 | `list` | `phone_asset_association:list` |
|
||||||
|
| 单条解绑 | `unbind` | `phone_asset_association:unbind` |
|
||||||
|
| 批量解绑 | `unbindBatch` | `phone_asset_association:unbind_batch` |
|
||||||
|
| 解绑导入页面 | `unbindImportPage` | `phone_asset_association:unbind_import_view` |
|
||||||
|
| 创建解绑导入任务 | `unbindImportCreate` | `phone_asset_association:unbind_import_create` |
|
||||||
|
|
||||||
|
## 资产管理:解绑导入任务
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 任务列表 | `/asset-management/task-management/phone-asset-unbind-import-tasks` | `unbindImportPage` | `phone_asset_association:unbind_import_page` |
|
||||||
|
| 任务详情 | `/asset-management/task-management/phone-asset-unbind-import-tasks/detail` | `unbindImportDetail` | `phone_asset_association:unbind_import_detail` |
|
||||||
|
| 详情按钮 | — | `unbindImportDetail` | `asset_association:unbind_import_detail` |
|
||||||
|
|
||||||
|
## 资产管理:临期资产
|
||||||
|
|
||||||
|
页面 URL:`/asset-management/expiring-assets`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| -------- | -------- | ----------------------- |
|
||||||
|
| 页面查看 | `page` | `expiring_asset:page` |
|
||||||
|
| 导出 | `export` | `expiring_asset:export` |
|
||||||
|
|
||||||
|
## 资产管理:导出任务页面
|
||||||
|
|
||||||
|
详情按钮为导出任务列表中的任务编号点击入口,权限编码按导出场景区分;详情页面统一使用 `id` 查询参数。
|
||||||
|
|
||||||
|
| 导出场景 | 列表页面 URL | 页面权限编码 | 详情按钮权限编码 | 详情页面 URL |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 设备 | `/asset-management/export-task-management/export-device` | `export_task:device_page` | `export_task:device_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| IoT 卡 | `/asset-management/export-task-management/export-iot-card` | `export_task:iot_card_page` | `export_task:iot_card_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 订单 | `/asset-management/export-task-management/export-order` | `export_task:order_page` | `export_task:order_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 套餐 | `/asset-management/export-task-management/export-package` | `export_task:package_page` | `export_task:package_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 代理主钱包流水 | `/asset-management/export-task-management/export-agent-wallet-transaction` | `export_task:agent_wallet_transaction_page` | `export_task:agent_wallet_transaction_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 代理充值 | `/asset-management/export-task-management/export-agent-recharge` | `export_task:agent_recharge_page` | `export_task:agent_recharge_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 退款 | `/asset-management/export-task-management/export-refund` | `export_task:refund_page` | `export_task:refund_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 换货 | `/asset-management/export-task-management/export-exchange` | `export_task:exchange_page` | `export_task:exchange_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 佣金明细 | `/asset-management/export-task-management/export-commission-record` | `export_task:commission_record_page` | `export_task:commission_record_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 套餐真流量达量预警 | `/asset-management/export-task-management/export-package-traffic-alert` | `export_task:package_traffic_alert_page` | `export_task:package_traffic_alert_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 临期资产 | `/asset-management/export-task-management/export-expiring-asset` | `export_task:expiring_asset_page` | `export_task:expiring_asset_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 设备激活报表 | `/asset-management/export-task-management/export-operations-activation` | `export_task:operations_activation_page` | `export_task:operations_activation_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
| 套餐续费报表 | `/asset-management/export-task-management/export-operations-renewal` | `export_task:operations_renewal_page` | `export_task:operations_renewal_detail` | `/asset-management/export-task-management/export-task-detail?id=:id` |
|
||||||
|
|
||||||
|
## 财务管理:退款管理
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --------- | ---------------------------- | ----------------- | ------------------------- |
|
||||||
|
| 退款列表 | `/finance/refund` | `page` | `refund:page` |
|
||||||
|
| 退款详情 | `/finance/refund/detail/:id` | `detailPage` | `refund:detail_page` |
|
||||||
|
| 恢复审批 | — | `recoverApproval` | `refund:recover_approval` |
|
||||||
|
|
||||||
|
## 财务管理:员工代收款账单
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 账单列表 | `/finance/employee-collection/bills` | `billPage` | `employee_collection:bill_view` |
|
||||||
|
| 账单详情 | `/finance/employee-collection/bills/detail/:id` | `billDetail` | `employee_collection:bill_detail` |
|
||||||
|
| 创建核销申请 | — | `applicationCreate` | `employee_collection:application_create` |
|
||||||
|
| 关闭账单 | — | `billClose` | `employee_collection:bill_close` |
|
||||||
|
|
||||||
|
## 财务管理:核销申请
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 申请列表 | `/finance/employee-collection/applications` | `applicationPage` | `employee_collection:application_view` |
|
||||||
|
| 申请详情 | `/finance/employee-collection/applications/detail/:id` | `applicationDetail` | `employee_collection:application_detail` |
|
||||||
|
| 修改并重新提交 | — | `applicationUpdate` | `employee_collection:application_update` |
|
||||||
|
| 查看凭证 | — | `applicationViewVoucher` | `employee_collection:application_view_voucher` |
|
||||||
|
| 恢复审批 | — | `applicationRecoverApproval` | `employee_collection:application_recover_approval` |
|
||||||
|
|
||||||
|
## 财务管理:收款方式管理
|
||||||
|
|
||||||
|
页面 URL:`/finance/employee-collection/payment-methods`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| -------- | --------------------- | ------------------------------------------- |
|
||||||
|
| 页面查看 | `paymentMethodPage` | `employee_collection:payment_method_view` |
|
||||||
|
| 新增 | `paymentMethodCreate` | `employee_collection:payment_method_create` |
|
||||||
|
| 编辑 | `paymentMethodEdit` | `employee_collection:payment_method_edit` |
|
||||||
|
| 删除 | `paymentMethodDelete` | `employee_collection:payment_method_delete` |
|
||||||
|
|
||||||
|
## 财务管理:代理商资金概况
|
||||||
|
|
||||||
|
页面 URL:`/finance/agent-fund-overview`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| ------------ | ------------------ | --------------------------------------- |
|
||||||
|
| 页面查看 | `page` | `agent_fund_overview:page` |
|
||||||
|
| 佣金明细导出 | `commissionExport` | `agent_fund_overview:commission_export` |
|
||||||
|
|
||||||
|
## 设置:商户池管理
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 管理页面 | `/settings/payment-merchant-pools` | `page` | `payment_merchant_pool:page` |
|
||||||
|
| 商户详情 | `/settings/payment-merchant-pools/detail/:id` | `merchantDetail` | `payment_merchant_pool:merchant_detail` |
|
||||||
|
| 商户池详情 | `/settings/payment-merchant-pools/pool-detail/:id` | `poolDetail` | `payment_merchant_pool:pool_detail` |
|
||||||
|
| 新增/编辑/启停/删除商户 | — | — | `payment_merchant_pool:merchant_create` / `merchant_edit` / `merchant_toggle` / `merchant_delete` |
|
||||||
|
| 新增/编辑/启停商户池 | — | — | `payment_merchant_pool:pool_create` / `pool_edit` / `pool_toggle` |
|
||||||
|
| 保存微信授权配置 | — | `wechatAuthEdit` | `payment_merchant_pool:wechat_auth_edit` |
|
||||||
|
|
||||||
|
## 设置:H5运营弹窗配置
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 配置列表 | `/settings/h5-popup-configuration` | `list` | `h5_popup_configuration:list` |
|
||||||
|
| 配置详情 | `/settings/h5-popup-configuration/detail/:id` | `detail` | `h5_popup_configuration:detail` |
|
||||||
|
| 新增/编辑/启用/停用 | — | — | `h5_popup_configuration:create` / `update` / `enable` / `disable` |
|
||||||
|
|
||||||
|
## 设置:资产钱包自动续费
|
||||||
|
|
||||||
|
| 页面/操作 | URL | 权限名称 | 权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 页面查看 | `/settings/asset-wallet-auto-renewal` | `page` | `asset_wallet_auto_renewal:page` |
|
||||||
|
| 保存配置 | — | `update` | `asset_wallet_auto_renewal:update` |
|
||||||
|
|
||||||
|
## 轮询管理:优先队列
|
||||||
|
|
||||||
|
页面 URL:`/polling-management/priority-queue`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| ------------ | -------- | ------------------------------- |
|
||||||
|
| 页面查看 | `page` | `polling_priority_queue:page` |
|
||||||
|
| 人工优先入队 | `create` | `polling_priority_queue:create` |
|
||||||
|
| 查看详情 | `detail` | `polling_priority_queue:detail` |
|
||||||
|
|
||||||
|
## 运营报表:设备激活报表
|
||||||
|
|
||||||
|
页面 URL:`/operations-reports/activation`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| -------- | ------------------ | -------------------------------------- |
|
||||||
|
| 页面查看 | `activationPage` | `operations_reports:activation_page` |
|
||||||
|
| 导出 | `activationExport` | `operations_reports:activation_export` |
|
||||||
|
|
||||||
|
## 运营报表:套餐续费报表
|
||||||
|
|
||||||
|
页面 URL:`/operations-reports/renewal`
|
||||||
|
|
||||||
|
| 类型 | 权限名称 | 权限编码 |
|
||||||
|
| -------- | --------------- | ----------------------------------- |
|
||||||
|
| 页面查看 | `renewalPage` | `operations_reports:renewal_page` |
|
||||||
|
| 导出 | `renewalExport` | `operations_reports:renewal_export` |
|
||||||
|
|
||||||
|
## 通用:导出任务详情
|
||||||
|
|
||||||
|
页面 URL:`/asset-management/export-task-management/export-task-detail`
|
||||||
|
|
||||||
|
- 页面权限:`export_task:detail_page`
|
||||||
|
- 各场景详情权限:`export_task:<scene>_detail`
|
||||||
|
- 各场景下载权限:`export_task:<scene>_download`
|
||||||
|
|
||||||
|
所有权限编码必须保持唯一;页面 URL 权限与按钮权限应分别定义。
|
||||||
|
|
||||||
|
## 轮询管理:网关限流
|
||||||
|
|
||||||
|
以下仅列普通账号需要配置的项。父菜单为已有「轮询管理」`/polling-management`,无需新增一级菜单。
|
||||||
|
|
||||||
|
| 菜单名称 | URL | 父菜单 | 菜单权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 停复机记录 | `/polling-management/stop-resume-records` | 轮询管理 | `card_stop_resume_record:page` |
|
||||||
|
| 上游错误分类 | `/polling-management/error-classifications` | 轮询管理 | `gateway_error_classification:page` |
|
||||||
|
| 网关限流组 | `/polling-management/rate-limit-groups` | 轮询管理 | `gateway_rate_limit_group:page` |
|
||||||
|
| 网关调用统计 | `/polling-management/gateway-stats` | 轮询管理 | `gateway_call_stats:page` |
|
||||||
|
| 网关限流组详情(隐藏) | `/polling-management/rate-limit-groups/detail/:id` | 轮询管理 | `gateway_rate_limit_group:detail_page` |
|
||||||
|
|
||||||
|
详情为轮询管理下的同级隐藏路由,不挂在列表组件下。`:id` 保留路由参数形式;普通账号需同时获得该隐藏菜单与「查看详情」按钮授权。
|
||||||
|
|
||||||
|
| 按钮名称 | 所属菜单/位置 | 按钮权限编码 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 测试分类 / 草稿测试 | 上游错误分类 | `gateway_error_classification:test` |
|
||||||
|
| 查看详情 | 网关限流组 | `gateway_rate_limit_group:detail` |
|
||||||
|
| 查询趋势 | 网关调用统计 | `gateway_call_stats:trend` |
|
||||||
|
| 停复机记录 | 资产管理 → 单卡信息,卡信息头部 | `iot_info:stop_resume_records` |
|
||||||
|
| 停复机记录 | 资产管理 → 单卡信息,设备绑定卡操作区 | `device_info:stop_resume_records` |
|
||||||
|
|
||||||
|
查询、重置、分页与刷新使用页面权限,无需另配按钮。运营商名称搜索下拉需要当前账号能读取 `/api/admin/carriers`;这些前端编码不替代后端独立鉴权。
|
||||||
|
|
||||||
|
## 轮询管理:轮询配置预览与统计
|
||||||
|
|
||||||
|
本次只需新增一个菜单;已有「轮询配置」`/polling-management/config` 保留,不改旧 CRUD 权限。以下仅列普通账号需要配置的项。
|
||||||
|
|
||||||
|
| 菜单名称 | URL | 父菜单 | 菜单权限编码 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 轮询配置统计 | `/polling-management/config-stats` | 轮询管理 `/polling-management` | `polling_config_stats:page` |
|
||||||
|
|
||||||
|
| 按钮名称 | 所属菜单 | 按钮权限编码 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 匹配预览 / 草稿预览 / 预览未保存修改 | 轮询配置 `/polling-management/config` | `polling_config:match_preview` |
|
||||||
|
| 查询重算进度 | 轮询配置 `/polling-management/config` | `polling_config:recalculate_progress` |
|
||||||
|
| 查看重置日前加密查流量效果统计 | 轮询配置统计 | `polling_config_stats:pre_reset` |
|
||||||
|
| 查看分层轮询统计 | 轮询配置统计 | `polling_config_stats:tiers` |
|
||||||
|
|
||||||
|
统计菜单权限和区域权限需同时授权;若仅授权其中一个统计区域,另一区域不会请求接口。进度查询权限只允许查询已有任务,不授予创建任务的能力。上述编码是前端配置约定,后端独立鉴权。
|
||||||
|
|
||||||
|
### 接入说明
|
||||||
|
|
||||||
|
- 当前配置预览使用 GET;草稿预览使用 POST,并在 Query 和 Body 同时传入同一 ICCID。多条草稿可覆盖已有配置或以 id=0 新增,预览不调用持久化 CRUD。
|
||||||
|
- 草稿中实名、流量、套餐、卡状态检查区分继承(null)、禁止且不再继承(0)、指定秒数(正数)。保护期已下线,表单和请求均移除。重置日前加密需明确开启,窗口和窗口内流量间隔成对设置。
|
||||||
|
- 以 2026-10-26 完整接口文档为准:资产状态、套餐状态、重置日前窗口字段现已支持创建/编辑保存;编辑清空通过 `clear_fields`,保存成功展示 `warnings`。
|
||||||
|
- 时间使用与店铺列表相同的 `formatDateTime`。运营商账户按名称远程搜索,仍提交数字 ID;搜索条件 >=4 时才显示展开按钮。
|
||||||
|
- 任务进度每 3 秒查询一次,终态、失败、关闭、路由离开和卸载后停止;保留真实 task_id、任务状态及 string 类型的响应账户 ID。
|
||||||
|
- 统计周期为 null 时显示「无周期数据」,真实 0 保留。分层统计标明 5 分钟缓存,未匹配配置的在库卡和已销售无套餐卡突出告警;刷新不宣称清除服务器缓存。
|
||||||
|
|
||||||
|
### 本地验证
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
node --test scripts/tests/pollingConfigExtensions.test.mjs
|
||||||
|
.\node_modules\.bin\vue-tsc.cmd --noEmit
|
||||||
|
node scripts/tests/verifyBuild.mjs
|
||||||
|
node scripts/tests/verifyBuild.mjs --serve --polling
|
||||||
|
```
|
||||||
|
|
||||||
|
最后一条命令启动 `http://127.0.0.1:5190` 的本地模拟接口页面;所有 `/api` 请求被拦截,不会对真实后端保存配置或创建任务。当前未进行真实后端联调、菜单数据写入、Git 推送或生产部署。
|
||||||
|
|
||||||
|
2026-10-26 验证:新增轮询配置测试 20 项、全量回归 174 项通过;类型、生产构建、针对性 lint / 格式 / 编码检查通过。浏览器验证了当前预览、草稿控件、异步进度、空周期/零值统计、普通账号入口及小屏布局。生产构建仍有项目既有循环依赖、其他页面常量绑定与大 chunk 警告。
|
||||||
|
|
||||||
|
## 轮询管理:运营商账户限流与调度(2026-10-26 增量)
|
||||||
|
|
||||||
|
本节在前两次配置的基础上新增,旧菜单和旧权限不必重建。仅列非超级管理员专属的菜单/按钮;标注“平台”的项只允许 user_type=1/2,权限码不能突破身份限制。前端编码是本项目约定,后端仍需独立授权。
|
||||||
|
|
||||||
|
### 新增可见菜单
|
||||||
|
|
||||||
|
| 菜单名称 | URL | 父菜单 | 菜单权限 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 运营商账户调度参数 | `/polling-management/carrier-params` | 轮询管理 `/polling-management` | `polling_carrier_params:page` |
|
||||||
|
| 请求检查场景 | `/polling-management/check-scenes` | 轮询管理 | `polling_check_scene:page` |
|
||||||
|
| 失败告警 | `/polling-management/failure-alerts` | 轮询管理 | `polling_failure_alert:page` |
|
||||||
|
| 账户队列监控 | `/polling-management/carrier-queues` | 轮询管理 | `polling_carrier_queue:page` |
|
||||||
|
| 停复机规则演练(平台) | `/polling-management/stop-resume-dry-run` | 轮询管理 | `stop_resume_dry_run:page` |
|
||||||
|
| 不轮询清单(平台) | `/polling-management/polling-pauses` | 轮询管理 | `polling_pause:page` |
|
||||||
|
| 轮询开关导入(平台) | `/polling-management/polling-status-imports` | 轮询管理 | `polling_status_import:page` |
|
||||||
|
| 导出不轮询清单(平台) | `/asset-management/export-task-management/export-polling-pause` | 导出管理 `/asset-management/export-task-management` | `export_task:polling_pause_page` |
|
||||||
|
| 导出停机被拒告警(平台) | `/asset-management/export-task-management/export-polling-stop-refused-alert` | 导出管理 | `export_task:polling_stop_refused_alert_page` |
|
||||||
|
| 导出停复机规则演练(平台) | `/asset-management/export-task-management/export-stop-resume-rule-dry-run` | 导出管理 | `export_task:stop_resume_rule_dry_run_page` |
|
||||||
|
|
||||||
|
### 新增隐藏详情菜单
|
||||||
|
|
||||||
|
这些是独立页面,不是按钮编码,需配置隐藏路由;URL 参数保留,不替换为具体 ID。
|
||||||
|
|
||||||
|
| 菜单名称 | URL | 父菜单 | 菜单权限 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 运营商账户调度参数详情 | `/polling-management/carrier-params/detail/:carrier_id` | 轮询管理 | `polling_carrier_params:detail_page` |
|
||||||
|
| 失败告警详情 | `/polling-management/failure-alerts/detail/:id` | 轮询管理 | `polling_failure_alert:detail_page` |
|
||||||
|
| 轮询开关导入详情(平台) | `/polling-management/polling-status-imports/detail/:id` | 轮询管理 | `polling_status_import:detail_page` |
|
||||||
|
|
||||||
|
三个导出场景共用已有隐藏详情 `/asset-management/export-task-management/export-task-detail?id=任务ID`,沿用 `export_task:detail_page`,不再新增详情路由。
|
||||||
|
|
||||||
|
### 新增按钮权限编码
|
||||||
|
|
||||||
|
| 所属页面 | 按钮/能力 | 编码 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 运营商账户调度参数 | 查看详情 | `polling_carrier_params:detail` |
|
||||||
|
| 失败告警 | 查看详情 | `polling_failure_alert:detail` |
|
||||||
|
| 失败告警 | 账户汇总 | `polling_failure_alert:summary` |
|
||||||
|
| 失败告警 | 标记已处理(仅未处理状态) | `polling_failure_alert:handle` |
|
||||||
|
| 失败告警 | 批量关闭卡轮询(平台) | `polling_failure_alert:close_polling` |
|
||||||
|
| 失败告警 | 导出停机被拒(平台) | `polling_failure_alert:export` |
|
||||||
|
| 停复机规则演练 | 导出演练明细(平台) | `stop_resume_dry_run:export` |
|
||||||
|
| 不轮询清单 | 批量开启轮询(平台) | `polling_pause:restore` |
|
||||||
|
| 不轮询清单 | 创建导出任务(平台) | `polling_pause:export` |
|
||||||
|
| 轮询开关导入 | 上传并创建任务(平台) | `polling_status_import:create` |
|
||||||
|
| 轮询开关导入 | 查看任务与逐行结果(平台) | `polling_status_import:detail` |
|
||||||
|
| IoT 卡列表 | 批量开关轮询(平台/代理) | `iot_card:batch_polling` |
|
||||||
|
| 设备列表 | 批量开关轮询(平台/代理) | `device:batch_polling` |
|
||||||
|
| 单卡资产详情 / 卡级告警详情 | 查询单卡轮询状态 | `iot_info:polling_state` |
|
||||||
|
| 设备资产详情的绑定卡 | 查询该绑定卡轮询状态 | `device_info:polling_state` |
|
||||||
|
| 导出不轮询清单 | 查看详情 / 下载 | `export_task:polling_pause_detail` / `export_task:polling_pause_download` |
|
||||||
|
| 导出停机被拒告警 | 查看详情 / 下载 | `export_task:polling_stop_refused_alert_detail` / `export_task:polling_stop_refused_alert_download` |
|
||||||
|
| 导出停复机规则演练 | 查看详情 / 下载 | `export_task:stop_resume_rule_dry_run_detail` / `export_task:stop_resume_rule_dry_run_download` |
|
||||||
|
|
||||||
|
“详情”按钮及对应隐藏详情菜单需同时授权。告警详情内查看停复机记录复用已有 `iot_info:stop_resume_records`;资产单项轮询开关复用 `iot_info:auto_polling` / `device_info:auto_polling`,无需新编码。请求检查场景、队列监控无新增普通写按钮。系统配置沿用旧菜单与权限,仅新增 polling 模块选项。
|
||||||
|
|
||||||
|
### 操作边界
|
||||||
|
|
||||||
|
- 批量只提交当前勾选 ID,最多 500 条,不扩展成全量筛选;原因必填,先读 impact 再确认。四类逐项结果完整保留,部分成功不回滚。
|
||||||
|
- 企业账号不开放批量;代理资产范围由后端校验。风险停机、已销户不提供“强制开启”。
|
||||||
|
- 账户级告警 card_id=0 不请求单卡接口、不允许勾选关闭卡轮询;停机被拒视图隐藏并剔除不支持的筛选。
|
||||||
|
- 导入仅 CSV,首行为表头,首列保留文本,最多 50000 数据行;上传成功后才创建任务。任务终态/请求失败停止自动刷新。
|
||||||
|
- 演练仅判断不执行;明细 result 不改变汇总,导出也不带 result;演练允许每页 500,其他列表最大 100。
|
||||||
|
- 调度、限流、指令队列开关不会因本次前端接入自动修改。旧并发配置仅 legacy 模式有效。
|
||||||
|
|
||||||
|
C 端同步项和接口覆盖/验证见 [本次接入交付说明](./2026-10-26-轮询账户限流-后台接入交付.md)。
|
||||||
|
|
||||||
|
本次验证:191 项回归通过;类型、生产构建、代码/样式/格式与 UTF-8 检查通过。仅本地 mock 验收,没有写入菜单数据、推送或部署;生产构建仍有项目既有循环依赖与大 chunk 警告。
|
||||||
|
|
||||||
|
## 订单分类与沉默期(2026-10-28)
|
||||||
|
|
||||||
|
后台实现已按 Apifox 定义补齐。以下为实际路由和前端权限约定;后端仍需创建真实菜单/按钮记录并独立校验权限与数据范围。未推送、部署或写入服务器菜单,真实支付/审批/退款/导出需测试环境联调。准确范围见 [执行记录](../openspec/changes/add-order-segmentation-and-package-silence/implementation.md)。
|
||||||
|
|
||||||
|
### 新增可见菜单
|
||||||
|
|
||||||
|
| 菜单名称 | URL | 父菜单 | 菜单权限 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 平台订单 | `/order-management/platform-orders` | 订单管理 `/order-management` | `orders:platform_page` |
|
||||||
|
| 代理商订单 | `/order-management/agent-orders` | 订单管理 | `orders:agent_page` |
|
||||||
|
| 在线支付订单 | `/order-management/online-orders` | 订单管理 | `orders:online_page` |
|
||||||
|
| 延期订单 | `/package-management/silence-orders` | 套餐管理 `/package-management` | `package_silence:orders_page` |
|
||||||
|
| 延期批次与异常 | `/package-management/silence-batches` | 套餐管理 | `package_silence:batches_page` |
|
||||||
|
| 沉默期临期/过期套餐 | `/package-management/silence-usages` | 套餐管理 | `package_silence:usages_page` |
|
||||||
|
| 延期政策 | `/package-management/silence-policies` | 套餐管理 | `package_silence:policies_page`(并需 `package_silence:policy`) |
|
||||||
|
| 导出平台订单 | `/asset-management/export-task-management/export-order-platform` | 导出管理 `/asset-management/export-task-management` | `export_task:order_platform_page` |
|
||||||
|
| 导出代理商订单 | `/asset-management/export-task-management/export-order-agent` | 导出管理 | `export_task:order_agent_page` |
|
||||||
|
| 导出在线支付订单 | `/asset-management/export-task-management/export-order-online` | 导出管理 | `export_task:order_online_page` |
|
||||||
|
| 导出延期订单 | `/asset-management/export-task-management/export-silence-order` | 导出管理 | `export_task:package_silence_order_page` |
|
||||||
|
| 导出沉默期套餐 | `/asset-management/export-task-management/export-silence-usage` | 导出管理 | `export_task:package_silence_usage_page` |
|
||||||
|
|
||||||
|
代理账号仅可配置平台/代理商普通订单及其导出;在线普通订单及导出只允许 user_type=1/2。企业不得配置普通订单菜单。旧普通订单列表/详情保留。沉默期模块没有额外虚构身份范围,按后端真实授权及数据范围工作。
|
||||||
|
|
||||||
|
### 新增隐藏详情菜单
|
||||||
|
|
||||||
|
| 菜单名称 | URL | 父菜单 | 菜单权限 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 延期订单详情 | `/package-management/silence-orders/detail/:id` | 套餐管理 | `package_silence:order_detail_page` |
|
||||||
|
| 延期批次详情 | `/package-management/silence-batches/detail/:id` | 套餐管理 | `package_silence:batch_detail_page` |
|
||||||
|
|
||||||
|
直接访问详情同时要求对应列表页权限、隐藏详情菜单权限和下表详情按钮权限;不能只配隐藏详情菜单。导出详情沿用已有隐藏 `export_task:detail_page` 与已有导出详情 URL。仅超管专属配置不列入普通菜单清单。
|
||||||
|
|
||||||
|
### 新增及沿用按钮权限
|
||||||
|
|
||||||
|
| 操作 | 权限编码 |
|
||||||
|
| --- | --- |
|
||||||
|
| 下单时调整本次沉默期(两个单项购买入口) | `package_silence:override` |
|
||||||
|
| 扫码付费延期、单项特殊延期 | `package_silence:extend` |
|
||||||
|
| 批量特殊延期(含 CSV 导入) | 同时需要 `package_silence:extend`、`package_silence:extend_batch` |
|
||||||
|
| 登记线下付费延期 | `package_silence:offline` |
|
||||||
|
| 异常明细重试/关闭 | `package_silence:exception` |
|
||||||
|
| 政策查询/新增/编辑/启停/匹配 | `package_silence:policy` |
|
||||||
|
| 查看延期订单详情 | `package_silence:order_detail` |
|
||||||
|
| 查看延期批次详情 | `package_silence:batch_detail` |
|
||||||
|
| 创建延期订单导出 | `package_silence:order_export` |
|
||||||
|
| 创建沉默期套餐清单导出 | `package_silence:usage_export` |
|
||||||
|
| 创建普通订单/查看详情/导出 | 沿用 `orders:add` / `orders:view_detail` / `orders:export` |
|
||||||
|
| 平台普通订单导出详情/下载 | `export_task:order_platform_detail` / `export_task:order_platform_download` |
|
||||||
|
| 代理商普通订单导出详情/下载 | `export_task:order_agent_detail` / `export_task:order_agent_download` |
|
||||||
|
| 在线普通订单导出详情/下载 | `export_task:order_online_detail` / `export_task:order_online_download` |
|
||||||
|
| 延期订单导出详情/下载 | `export_task:package_silence_order_detail` / `export_task:package_silence_order_download` |
|
||||||
|
| 沉默期套餐导出详情/下载 | `export_task:package_silence_usage_detail` / `export_task:package_silence_usage_download` |
|
||||||
|
|
||||||
|
沉默期流水、首次购买记录没有在后端文档声明独立按钮权限,继承所属资产/列表页的读取范围,不虚构按钮编码。角色配置继续使用后端真实权限树,需后端提供权限记录和 ID,前端不伪造记录。
|
||||||
|
|
||||||
|
### 操作边界与验证
|
||||||
|
|
||||||
|
- 默认商品沉默期 1–90 天,购买覆盖天数仅最小 1;付费延期天数/金额由资格和试算决定,不复制算法。
|
||||||
|
- 特殊延期 CSV 为前端解析:表头 `package_usage_id,days`,每行两个整数,转换为文档 items[];不假设后端导入文件格式。
|
||||||
|
- 政策免审批完整枚举未在 OpenAPI 给出;已有值原样保留,提供明确的 offline_paid/batch,其他值需填写后端精确编码。政策接口没有 DELETE,不提供删除。
|
||||||
|
- 二维码/线下同一快照重试复用幂等键;特殊请求结果不明时提示核对批次并阻止重复提交。待审批、已支付和已完成严格区分。
|
||||||
|
- 异常关闭必须填写原因并确认,提示在线订单可能原路退款;不直接修改套餐截止时间。
|
||||||
|
- 企业资产五项限制保持;首次购买为只读事实,不回填绑定卡或换货资产;对象缺失隐藏沉默期入口。
|
||||||
|
- 236 项测试、类型与生产构建通过;仅本地 mock 验收,没有真实支付、审批、退款、开关切换、导出、推送或部署。
|
||||||
|
|
||||||
|
### C 端同步(本仓库未修改)
|
||||||
|
|
||||||
|
独立 C 端需接入可选 silence、状态/格式化截止日及 extendable 入口;`/api/c/v1/package-silence` 下的资格、报价、自助下单/qr_content、订单状态刷新、我的延期订单/详情、流水。对象缺失或总开关关闭时隐藏入口;金额单位分、结果取后端,不能复制计费/审批算法,不调用后台 URL。资产归属由后端校验。
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Change: 代理扫码分销注册与提现资料资格前端对接
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
新增强代理扫码分销注册与提现资料资格能力:代理可通过 H5 注册页扫码注册,审批通过后登录后台;代理本人可维护提现资料资格并发起/重提提现,超管可作废资格。后端契约已按 `docs/产品迭代8月份/frontend-api-simple.md` 落地,后台管理端当前缺少对应接口封装、独立注册页与资格管理入口。
|
||||||
|
|
||||||
|
本次前端按后端实际契约对齐:注册验码 `POST /api/c/v1/auth/send-code`(`scene` 为 `bind_phone`,返回 `cooldown_seconds`);扫码注册 `POST /api/c/v1/agent-distribution-registrations`(成功仅返回「待审批」);店铺列表/详情/更新响应新增 `distribution_code`;提现资料资格提交/查询/作废与提现申请重提/详情按新文档补齐。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 `agent-distribution-registration` 能力:独立注册页挂在后台项目内、静态路由免登录访问(H5 与 PC 同一套表单),支持分销码自动填、短信验证码 60s 倒计时、密码掩码与强度提示、省市区级联;提交成功进入「待审批」结束页,失败统一提示「分销码不可用」且不复用同一验证码。
|
||||||
|
- 新增公开 API 封装:`sendCode()` 与 `registerAgent()`,请求不带登录态(`requestOptions.withToken: false`)。
|
||||||
|
- 店铺模块补充 `distribution_code` 字段类型,店铺详情页展示分销码与注册二维码。
|
||||||
|
- 佣金模块新增:提现资料资格提交/替换、资格查询(证件号脱敏、含审批与作废信息)、资格作废(超管,`reason` 必填)、重提被驳回的提现、提现申请详情(含尝试记录与异常标记)。
|
||||||
|
- 扩展企微审批场景业务类型:`agent_distribution_approval`、`withdrawal_qualification_approval`、`commission_withdrawal_approval`。
|
||||||
|
- 提现交互约束:资格替换合同/法人身份证后旧资格失效需重新审批;提现仅在有效资格且余额充足时可提交,否则不冻结、不建单。
|
||||||
|
- 不实现后端接口、数据库、企微回调;不实现 C 端(客户)登录体系。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `agent-distribution-registration`、`shop-management`、`commission-management`、`wecom-scenes`
|
||||||
|
- Affected code: `src/api/modules/agentDistribution.ts`、`src/api/modules/commission.ts`、`src/types/api/shop.ts`、`src/types/api/commission.ts`、`src/types/api/wecom.ts`、`src/router/routes/staticRoutes.ts`、`src/router/guards/permission.ts`、`src/views/agent-registration/index.vue`、`src/views/shop-management/detail/index.vue`、`src/views/commission-management/my-commission/index.vue`、`src/views/settings/wecom/scenes/index.vue`
|
||||||
|
- Dependencies: `docs/产品迭代8月份/frontend-api-simple.md`
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 独立代理注册页免登录访问
|
||||||
|
代理扫码分销注册页 MUST 独立于登录态提供,未登录可访问,且挂在后台项目内(H5 与 PC 共用同一套表单);注册流程 MUST NOT 复用当前登录态。
|
||||||
|
|
||||||
|
#### Scenario: 未登录访问注册页
|
||||||
|
- **GIVEN** 用户未登录并通过扫码或直接访问注册入口
|
||||||
|
- **WHEN** 其打开注册页面
|
||||||
|
- **THEN** 页面 MUST 正常渲染注册表单
|
||||||
|
- **AND** MUST NOT 跳转到登录页
|
||||||
|
|
||||||
|
#### Scenario: 已登录用户访问注册页
|
||||||
|
- **GIVEN** 用户已登录后台
|
||||||
|
- **WHEN** 其打开注册页面
|
||||||
|
- **THEN** 注册流程 MUST 不携带登录态
|
||||||
|
|
||||||
|
### Requirement: 发送短信验证码
|
||||||
|
注册页 MUST 调用 `POST /api/c/v1/auth/send-code` 发送验证码,请求体 `{ phone, scene }`(`scene` 为 `bind_phone`),响应 `data.cooldown_seconds` MUST 用于 60 秒倒计时;请求 MUST 不携带 Token。
|
||||||
|
|
||||||
|
#### Scenario: 发送验证码并倒计时
|
||||||
|
- **GIVEN** 用户输入手机号
|
||||||
|
- **WHEN** 其点击获取验证码
|
||||||
|
- **THEN** 系统 MUST 调用发送验证码接口
|
||||||
|
- **AND** 按钮 MUST 进入 60 秒倒计时且不可重复点击
|
||||||
|
|
||||||
|
#### Scenario: 注册失败后不复用验证码
|
||||||
|
- **GIVEN** 一次注册提交失败
|
||||||
|
- **WHEN** 用户再次尝试注册
|
||||||
|
- **THEN** 页面 MUST 提示重新获取验证码
|
||||||
|
- **AND** MUST NOT 复用已消费的验证码
|
||||||
|
|
||||||
|
### Requirement: 代理扫码注册提交
|
||||||
|
注册页 MUST 调用 `POST /api/c/v1/agent-distribution-registrations` 提交注册,必填 `distribution_code`(扫码自动带)、`phone`、`code`、`password`、`shop_name`、`shop_code`、`username`,选填 `contact_name`、`province`、`city`、`district`、`address`;成功响应为「待审批」,前端 MUST 展示待审批结束页且不自动登录。
|
||||||
|
|
||||||
|
#### Scenario: 注册成功进入待审批
|
||||||
|
- **GIVEN** 用户填写完整表单并通过校验
|
||||||
|
- **WHEN** 其提交注册
|
||||||
|
- **THEN** 系统 MUST 调用注册接口
|
||||||
|
- **AND** 成功时 MUST 展示「待审批,审核结果将通知你」结束页
|
||||||
|
- **AND** MUST NOT 发放账号凭证或自动登录
|
||||||
|
|
||||||
|
#### Scenario: 注册失败统一提示
|
||||||
|
- **GIVEN** 注册提交失败(无效分销码、上级停用、验证码无效或已消费等)
|
||||||
|
- **WHEN** 注册接口返回失败
|
||||||
|
- **THEN** 页面 MUST 统一提示「分销码不可用」
|
||||||
|
- **AND** MUST NOT 根据错误文案区分原因
|
||||||
|
|
||||||
|
### Requirement: 表单适配与敏感项处理
|
||||||
|
注册表单 MUST 同时适配手机 H5 与 PC:H5 单列、大触控区、软键盘友好、地址使用级联选择器;PC 使用居中卡片或两列布局且字段一致。密码输入 MUST 掩码并展示强度提示;页面 MUST NOT 展示除手机号外的完整个人敏感信息。
|
||||||
|
|
||||||
|
#### Scenario: H5 与 PC 同一表单
|
||||||
|
- **GIVEN** 用户分别在手机与 PC 打开注册页
|
||||||
|
- **WHEN** 其填写注册信息
|
||||||
|
- **THEN** 两端的字段集合 MUST 一致
|
||||||
|
- **AND** 布局 MUST 按端侧自适应
|
||||||
|
|
||||||
|
#### Scenario: 密码强度与掩码
|
||||||
|
- **GIVEN** 用户在密码输入框输入
|
||||||
|
- **WHEN** 其提交表单
|
||||||
|
- **THEN** 输入 MUST 为掩码展示
|
||||||
|
- **AND** MUST 展示密码强度提示
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 提交/替换提现资料资格
|
||||||
|
代理本人 MUST 能提交或替换提现资料资格,调用 `POST /api/admin/shops/{shop_id}/withdrawal-qualifications`,请求体包含 `subject_type`、`subject_code`、`legal_person_id_card` 与合同、身份证正反面、营业执照、门头照、发票等 `*_file_key`,以及 `invoice_title`、`invoice_subject_code`;附件 MUST 先通过对象存储上传接口取得 `file_key` 再提交。
|
||||||
|
|
||||||
|
#### Scenario: 提交资格成功
|
||||||
|
- **GIVEN** 代理本人已登录并选好主体类型
|
||||||
|
- **WHEN** 其上传资料并提交
|
||||||
|
- **THEN** 前端 MUST 先取得全部附件 `file_key`
|
||||||
|
- **AND** 再调用提交接口并在成功后刷新资格列表
|
||||||
|
|
||||||
|
#### Scenario: 替换资格使旧资格失效
|
||||||
|
- **GIVEN** 已存在通过审批的资格
|
||||||
|
- **WHEN** 代理提交合同或法人身份证变更
|
||||||
|
- **THEN** 旧资格 MUST 立即失效
|
||||||
|
- **AND** 新资格 MUST 重新审批通过后方可提现
|
||||||
|
|
||||||
|
### Requirement: 查询提现资料资格
|
||||||
|
代理本人 MUST 能分页查询提现资料资格,调用 `GET /api/admin/shops/{shop_id}/withdrawal-qualifications`(`page` / `page_size` / `status`);响应 MUST 展示脱敏的 `subject_code_masked` 与 `legal_person_id_card_masked`,并展示 `status`、`approval_status`、`invalid_reason`、`invalidated_at` 与各附件 `file_key` 预览。
|
||||||
|
|
||||||
|
#### Scenario: 查看资格版本与审批状态
|
||||||
|
- **GIVEN** 代理本人打开提现资料资格列表
|
||||||
|
- **WHEN** 接口返回资格记录
|
||||||
|
- **THEN** 列表 MUST 展示脱敏证件号与审批状态
|
||||||
|
- **AND** 证件号 MUST NOT 以明文展示
|
||||||
|
|
||||||
|
### Requirement: 作废提现资料资格
|
||||||
|
超级管理员 MUST 能作废提现资料资格,调用 `POST /api/admin/withdrawal-qualifications/{id}/void`,`reason` 必填。
|
||||||
|
|
||||||
|
#### Scenario: 作废资格须填原因
|
||||||
|
- **GIVEN** 超级管理员打开作废弹窗
|
||||||
|
- **WHEN** 其未填写原因直接提交
|
||||||
|
- **THEN** 前端 MUST 阻止提交并提示填写原因
|
||||||
|
|
||||||
|
#### Scenario: 超管作废资格
|
||||||
|
- **GIVEN** 超级管理员选择一条资格
|
||||||
|
- **WHEN** 其填写原因并确认作废
|
||||||
|
- **THEN** 系统 MUST 调用作废接口并在成功后刷新列表
|
||||||
|
|
||||||
|
### Requirement: 重提被驳回的提现
|
||||||
|
代理本人 MUST 能重提被驳回的提现,调用 `PUT /api/admin/shops/{shop_id}/withdrawal-requests/{id}`,请求体包含 `account_name`、`account_number`、`amount`、`withdrawal_method` 与 `invoice_keys`;成功响应返回新提现单(`withdrawal_no`、`actual_amount`、`fee`、`fee_rate`、`status`)。
|
||||||
|
|
||||||
|
#### Scenario: 重提被驳回的提现
|
||||||
|
- **GIVEN** 代理本人的提现申请被驳回
|
||||||
|
- **WHEN** 其修改信息并重新提交
|
||||||
|
- **THEN** 系统 MUST 调用重提接口
|
||||||
|
- **AND** 成功后 MUST 刷新提现记录并展示新单号
|
||||||
|
|
||||||
|
#### Scenario: 非驳回状态不可重提
|
||||||
|
- **GIVEN** 提现申请不是被驳回状态
|
||||||
|
- **WHEN** 代理尝试重提
|
||||||
|
- **THEN** 重提入口 MUST 不可用或不展示
|
||||||
|
|
||||||
|
### Requirement: 提现申请详情
|
||||||
|
代理本人 MUST 能查看提现申请详情,调用 `GET /api/admin/shops/{shop_id}/withdrawal-requests/{id}`;响应包含 `reject_reason`、`anomaly_flag`、`anomaly_name`、`anomaly_reason` 与 `attempts` 尝试记录,前端 MUST 展示这些字段。
|
||||||
|
|
||||||
|
#### Scenario: 查看提现详情
|
||||||
|
- **GIVEN** 代理本人点击提现记录
|
||||||
|
- **WHEN** 详情返回
|
||||||
|
- **THEN** 页面 MUST 展示提现金额、实际到账、手续费、状态
|
||||||
|
- **AND** 展示驳回原因、异常标记与尝试记录
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 店铺数据返回分销码
|
||||||
|
店铺列表、详情与更新接口响应 MUST 包含 `distribution_code` 字段,前端类型 MUST 覆盖该字段并在列表/详情中展示。
|
||||||
|
|
||||||
|
#### Scenario: 店铺详情展示分销码
|
||||||
|
- **GIVEN** 用户打开店铺详情页
|
||||||
|
- **WHEN** 店铺存在 `distribution_code`
|
||||||
|
- **THEN** 页面 MUST 展示分销码
|
||||||
|
- **AND** MUST 以该码生成注册入口二维码
|
||||||
|
|
||||||
|
#### Scenario: 分销码为空
|
||||||
|
- **GIVEN** 店铺尚未分配分销码
|
||||||
|
- **WHEN** 页面展示店铺信息
|
||||||
|
- **THEN** 分销码与二维码区域 MUST 展示空态(`-` 或「未分配」)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 企微审批场景业务类型扩展
|
||||||
|
企微审批场景的 `business_type` MUST 支持以下枚举:`refund_approval`(退款审批)、`offline_recharge_approval`(员工线下代充值审批)、`employee_collection_approval`(员工代收款核销审批)、`agent_distribution_approval`(代理扫码分销注册审批)、`withdrawal_qualification_approval`(提现资料资格审批)、`commission_withdrawal_approval`(佣金提现终审)。前端类型与场景配置页选项 MUST 与后端枚举一致;`GET /api/admin/wecom/scenes/{business_type}/fields` 允许查询上述任一类型。
|
||||||
|
|
||||||
|
#### Scenario: 场景配置页可选新业务类型
|
||||||
|
- **GIVEN** 超级管理员打开企微审批场景配置页
|
||||||
|
- **WHEN** 其新建场景并选择业务类型
|
||||||
|
- **THEN** 下拉选项 MUST 包含全部六个业务类型
|
||||||
|
- **AND** 新增三个业务类型 MUST 与后端枚举一致
|
||||||
|
|
||||||
|
#### Scenario: 查询新增场景业务字段
|
||||||
|
- **GIVEN** 已选择 `agent_distribution_approval`、`withdrawal_qualification_approval` 或 `commission_withdrawal_approval`
|
||||||
|
- **WHEN** 页面加载业务字段
|
||||||
|
- **THEN** 前端 MUST 调用对应 `business_type` 的字段查询接口
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## 1. 提案与 API 层
|
||||||
|
- [x] 1.1 新增 `src/api/modules/agentDistribution.ts`:`sendCode()` 与 `registerAgent()`,公开请求不带 Token
|
||||||
|
- [x] 1.2 扩展 `src/types/api/shop.ts`:`ShopResponse` 增加 `distribution_code`
|
||||||
|
- [x] 1.3 扩展 `src/types/api/commission.ts`:提现资料资格提交/查询、作废、重提、详情的请求与响应类型
|
||||||
|
- [x] 1.4 扩展 `src/api/modules/commission.ts`:`submitWithdrawalQualification` / `getWithdrawalQualifications` / `voidWithdrawalQualification` / `resubmitWithdrawalRequest` / `getWithdrawalRequestDetail`
|
||||||
|
- [x] 1.5 扩展 `src/types/api/wecom.ts`:`WecomBusinessType` 增加三个业务类型
|
||||||
|
- [x] 1.6 汇总导出新增类型
|
||||||
|
|
||||||
|
## 2. 企微审批场景
|
||||||
|
- [x] 2.1 场景配置页新增三个业务类型选项(`agent_distribution_approval` / `withdrawal_qualification_approval` / `commission_withdrawal_approval`)
|
||||||
|
|
||||||
|
## 3. 独立注册页
|
||||||
|
- [x] 3.1 `staticRoutes.ts` 新增 `/agent-registration` 静态路由
|
||||||
|
- [x] 3.2 `LOGIN_WHITE_LIST` 增加注册页路径
|
||||||
|
- [x] 3.3 新增 `src/views/agent-registration/index.vue`:表单、验证码倒计时、密码强度、省市区级联、提交与结束页、H5/PC 响应式
|
||||||
|
|
||||||
|
## 4. 店铺详情
|
||||||
|
- [x] 4.1 店铺详情页展示 `distribution_code` 与注册二维码
|
||||||
|
|
||||||
|
## 5. 我的佣金(代理侧)
|
||||||
|
- [x] 5.1 新增「提现资料资格」Tab:资格列表(脱敏展示)+ 提交/替换弹窗(附件走上传接口)
|
||||||
|
- [x] 5.2 提现记录新增「详情」抽屉:展示明细、驳回原因、异常标记与尝试记录
|
||||||
|
- [x] 5.3 被驳回记录新增「重新提交」弹窗(PUT 重提)
|
||||||
|
- [x] 5.4 超管可见「作废资格」操作(reason 必填)
|
||||||
|
|
||||||
|
## 6. 验证
|
||||||
|
- [x] 6.1 运行 `npm run build`(含 `vue-tsc --noEmit`)
|
||||||
|
- [x] 6.2 运行 `npm run check:encoding`
|
||||||
|
- [x] 6.3 运行 `openspec.cmd validate add-agent-distribution-registration-and-withdrawal --strict`
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
代理注册申请由公开注册接口创建,后台需要按数据范围查询申请并展示审批提交状态。恢复审批只恢复原企业微信审批提交,不创建新的注册申请或审批实例。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 使用列表页 + 独立详情页,详情页 URL 为 `/shop-management/agent-distribution-registrations/detail/:id`。
|
||||||
|
- 列表和详情均使用后端返回的 `status`、`approval_status`、`submission_status` 和名称字段,不在前端推导状态。
|
||||||
|
- 恢复按钮仅由后端返回的 `recoverable` 控制,并使用独立 `agent_distribution_registration:recover_approval` 权限。
|
||||||
|
- 详情响应直接消费后端完整手机号和注册地址,不展示密码或内部审批字段。
|
||||||
|
|
||||||
|
## Risks / Mitigations
|
||||||
|
|
||||||
|
- 状态可能在列表与详情之间发生变化 → 详情和恢复成功/失败后重新加载后端数据。
|
||||||
|
- 恢复请求并发冲突 → 后端返回错误时提示并刷新详情,不做乐观更新。
|
||||||
|
- 手机号属于敏感业务数据 → 页面仅受注册申请列表/详情权限和后端数据范围控制。
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Change: 增加代理注册申请后台管理
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
后台目前无法查看代理扫码注册申请,也无法处理企业微信审批提交失败或结果未知的注册申请。根据 `docs/产品迭代8月份/代理注册管理.md`,需要提供申请列表、详情和恢复原审批提交入口。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增后台代理注册申请列表页,支持状态、申请/决定时间、手机号、关键字、直接上级店铺和提交异常筛选。
|
||||||
|
- 新增注册申请详情页,展示完整手机号、注册地址、申请店铺、分销码、业务状态、审批状态和企微提交状态。
|
||||||
|
- 当 `recoverable = true` 时显示“恢复审批”,调用 `POST /api/admin/agent-distribution-registrations/{id}/recover-approval`,成功后刷新详情。
|
||||||
|
- 新增代理注册申请 API、类型、路由和权限编码。
|
||||||
|
- 权限编码:列表 `agent_distribution_registration:list`、详情 `agent_distribution_registration:detail`、恢复审批 `agent_distribution_registration:recover_approval`。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `agent-distribution-registration`
|
||||||
|
- Affected code:
|
||||||
|
- `src/api/modules/agentDistribution.ts`
|
||||||
|
- `src/types/api/agentDistribution.ts`
|
||||||
|
- `src/config/constants/augustIteration.ts`
|
||||||
|
- `src/router/routesAlias.ts`
|
||||||
|
- `src/router/routes/asyncRoutes.ts`
|
||||||
|
- `src/views/shop-management/agent-distribution-registrations/index.vue`
|
||||||
|
- `src/views/shop-management/agent-distribution-registrations/detail.vue`
|
||||||
|
- `src/locales/langs/zh.json`
|
||||||
|
- `docs/权限.md`
|
||||||
|
- Out of scope:
|
||||||
|
- C 端注册流程和代理登录流程。
|
||||||
|
- 后端审批、数据库和企业微信回调实现。
|
||||||
|
- 修改注册申请业务资料或审批结果。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Agent Registration Application List
|
||||||
|
|
||||||
|
The admin frontend SHALL provide a paginated agent registration application list with the documented filters and backend response statuses.
|
||||||
|
|
||||||
|
#### Scenario: Filter registration applications
|
||||||
|
|
||||||
|
- **WHEN** an authorized user selects status, time, phone, keyword, parent shop, or submission anomaly filters
|
||||||
|
- **THEN** the frontend MUST send those filters to `GET /api/admin/agent-distribution-registrations`
|
||||||
|
- **AND** the list MUST display business, approval, and submission status names from the response
|
||||||
|
|
||||||
|
### Requirement: Agent Registration Application Detail
|
||||||
|
|
||||||
|
The admin frontend SHALL provide a detail page for `GET /api/admin/agent-distribution-registrations/{id}` and display the complete registration information returned by the backend.
|
||||||
|
|
||||||
|
#### Scenario: Open registration detail
|
||||||
|
|
||||||
|
- **WHEN** an authorized user opens a registration application
|
||||||
|
- **THEN** the frontend MUST request the application detail by ID
|
||||||
|
- **AND** MUST display the phone, address, shop, distribution code, business status, approval status, and submission status
|
||||||
|
- **AND** MUST NOT display password or internal approval fields not present in the response
|
||||||
|
|
||||||
|
### Requirement: Recover Agent Registration Approval
|
||||||
|
|
||||||
|
The frontend SHALL expose `POST /api/admin/agent-distribution-registrations/{id}/recover-approval` only when `recoverable = true` and the user has the recovery permission.
|
||||||
|
|
||||||
|
#### Scenario: Recover approval
|
||||||
|
|
||||||
|
- **GIVEN** a registration response has `recoverable = true`
|
||||||
|
- **WHEN** the user confirms “恢复审批”
|
||||||
|
- **THEN** the frontend MUST call the recovery endpoint with only the registration ID
|
||||||
|
- **AND** MUST refresh the detail after success or a state conflict
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## 1. API And Types
|
||||||
|
|
||||||
|
- [x] 1.1 增加代理注册申请列表、详情和恢复审批 TypeScript 类型。
|
||||||
|
- [x] 1.2 扩展 `AgentDistributionService`:列表、详情、恢复审批接口。
|
||||||
|
- [x] 1.3 增加唯一权限常量 `list`、`detail`、`recoverApproval`。
|
||||||
|
|
||||||
|
## 2. Pages And Routes
|
||||||
|
|
||||||
|
- [x] 2.1 新增注册申请列表页和筛选条件。
|
||||||
|
- [x] 2.2 新增详情页和审批/提交状态展示。
|
||||||
|
- [x] 2.3 接入“恢复审批”按钮、确认、错误提示和刷新。
|
||||||
|
- [x] 2.4 增加店铺管理下的列表/详情路由与中英文菜单文案。
|
||||||
|
|
||||||
|
## 3. Documentation And Verification
|
||||||
|
|
||||||
|
- [x] 3.1 将页面、详情和恢复审批权限写入 `docs/权限.md`。
|
||||||
|
- [x] 3.2 运行类型检查、lint、编码检查、构建和 OpenSpec 校验。
|
||||||
@@ -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(1–90,必填) }`。
|
||||||
|
- 语义:单行配置、无新增/删除;保存即递增 `config_version`,记录操作者与前后值快照;配置变更只影响后续扫描,历史记录不重算。
|
||||||
|
- 权限:仅超级管理员与平台账号可访问;其他身份(代理/企业/个人客户)`403`,提示「无权限操作该资源或资源不存在」,无权限与不存在不区分。
|
||||||
|
- 通知适配(复用既有接口,不算新接口):
|
||||||
|
- 后台站内通知:新增通知类型 `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`),接收人是业务员/店铺账号。走既有 `GET /api/admin/notifications` + `GET /api/admin/notifications/{id}/target`;确认 `target_type` 的 `iot_card` / `device` 在导航白名单;`available=false` 时只显示正文不跳转;计入未读数。
|
||||||
|
- H5 个人客户通知:同一类型已由后端加入客户可见白名单,出现在既有 `GET /api/c/v1/notifications` 与 `unread-count`,客户侧只有文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面,本仓库无需改动。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
|
||||||
|
- 明确不做(避免前端空等):无自动续费记录页/异常记录页接口、无尝试记录查询接口、无「有余额就不停机」开关、无 H5 客户侧新接口。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs:
|
||||||
|
- `asset-wallet-auto-renewal` — 新增能力
|
||||||
|
- Affected code:
|
||||||
|
- `src/types/api/assetWallet.ts`(新增)
|
||||||
|
- `src/types/api/index.ts`
|
||||||
|
- `src/api/modules/assetWallet.ts`(新增)
|
||||||
|
- `src/api/modules/index.ts`
|
||||||
|
- `src/views/settings/asset-wallet-auto-renewal/index.vue`(新增)
|
||||||
|
- `src/router/routesAlias.ts`
|
||||||
|
- `src/router/routes/asyncRoutes.ts`
|
||||||
|
- `src/utils/business/notificationNavigation.ts`(确认/补充 `iot_card`、`device` 白名单)
|
||||||
|
- `src/components/core/layouts/art-notification/index.vue`(确认 `expiry` 分类覆盖新类型)
|
||||||
|
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||||
@@ -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` 超出 1–90
|
||||||
|
- **THEN** 前端 MUST 阻止提交并给出校验提示
|
||||||
|
|
||||||
|
#### Scenario: 仅影响后续扫描
|
||||||
|
|
||||||
|
- **WHEN** 用户修改并保存配置
|
||||||
|
- **THEN** 历史自动续费记录 MUST NOT 被重算
|
||||||
|
- **AND** 新配置 MUST 只对保存后的扫描生效
|
||||||
|
|
||||||
|
### Requirement: 自动续费配置访问权限
|
||||||
|
|
||||||
|
Only super admin and platform accounts SHALL be allowed to query or save the asset wallet auto-renewal configuration. 代理、企业与个人客户等身份访问时 SHALL 返回 `403` 并提示「无权限操作该资源或资源不存在」;无权限与不存在的表现 MUST NOT 可区分。
|
||||||
|
|
||||||
|
#### Scenario: 授权访问
|
||||||
|
|
||||||
|
- **WHEN** 超级管理员或平台账号访问配置接口
|
||||||
|
- **THEN** 页面 MUST 正常查询与保存配置
|
||||||
|
|
||||||
|
#### Scenario: 无权限访问
|
||||||
|
|
||||||
|
- **WHEN** 代理、企业或个人客户账号访问配置接口
|
||||||
|
- **THEN** 页面 MUST 按 `403` 无权限处理并展示统一提示
|
||||||
|
- **AND** 前端 MUST NOT 通过响应区分无权限与资源不存在
|
||||||
|
|
||||||
|
### Requirement: 自动续费失败后台通知
|
||||||
|
|
||||||
|
The admin notification center SHALL display the new notification type `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`)through the existing notification APIs,接收人为业务员/店铺账号。目标 `target_type` 的 `iot_card` 与 `device` SHALL 位于通知导航白名单;当目标 `available=false` 时页面 SHALL 只展示正文且不跳转;该类型通知 SHALL 计入未读数。
|
||||||
|
|
||||||
|
#### Scenario: 展示并跳转自动续费失败通知
|
||||||
|
|
||||||
|
- **WHEN** 通知列表返回 `asset.auto_renewal.failed` 类型且目标可用
|
||||||
|
- **THEN** 通知中心 MUST 将该通知归入 `expiry` 类别展示
|
||||||
|
- **AND** 用户点击后 MUST 通过既有目标接口按 `iot_card` / `device` 跳转到对应资产详情
|
||||||
|
|
||||||
|
#### Scenario: 目标不可用
|
||||||
|
|
||||||
|
- **WHEN** 目标接口返回 `available=false`
|
||||||
|
- **THEN** 页面 MUST 只展示通知正文且不发起跳转
|
||||||
|
|
||||||
|
### Requirement: H5 客户侧通知边界
|
||||||
|
|
||||||
|
This capability SHALL NOT 新增或修改 H5 客户侧接口与页面。`asset.auto_renewal.failed` 类型已由后端加入客户可见白名单,通过既有 `GET /api/c/v1/notifications` 与 `unread-count` 返回;客户侧仅展示文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
|
||||||
|
|
||||||
|
#### Scenario: H5 复用既有接口
|
||||||
|
|
||||||
|
- **WHEN** 客户侧通知接口返回该类型
|
||||||
|
- **THEN** 客户侧 MUST 仅展示文案且不提供跳转
|
||||||
|
- **AND** 本次变更 MUST NOT 新增 H5 客户侧接口或页面
|
||||||
|
|
||||||
|
#### Scenario: 到期展示期
|
||||||
|
|
||||||
|
- **WHEN** 对应套餐业务到期
|
||||||
|
- **THEN** 该通知按 `expiry` 类别既有行为从客户侧列表消失
|
||||||
|
|
||||||
|
### Requirement: 功能范围边界
|
||||||
|
|
||||||
|
This capability SHALL NOT 包含以下内容:自动续费记录页/异常记录页、尝试记录查询、「有余额就不停机」开关、H5 客户侧新接口。
|
||||||
|
|
||||||
|
#### Scenario: 明确不提供的功能
|
||||||
|
|
||||||
|
- **WHEN** 前端对接本次资产钱包自动续费能力
|
||||||
|
- **THEN** 页面 MUST NOT 提供或依赖自动续费记录页、异常记录页、尝试记录查询接口与「有余额就不停机」开关
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Tasks: 资产钱包自动续费配置
|
||||||
|
|
||||||
|
## 1. 类型与 API
|
||||||
|
- [x] 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`)等类型
|
||||||
|
- [x] 1.2 `src/types/api/index.ts` 导出新类型
|
||||||
|
- [x] 1.3 新增 `src/api/modules/assetWallet.ts`:
|
||||||
|
- `getAutoRenewalConfig`:`GET /api/admin/asset-auto-renewal-config`
|
||||||
|
- `saveAutoRenewalConfig`:`PUT /api/admin/asset-auto-renewal-config`
|
||||||
|
- [x] 1.4 `src/api/modules/index.ts` 导出新服务
|
||||||
|
|
||||||
|
## 2. 配置页面
|
||||||
|
- [x] 2.1 `src/router/routesAlias.ts` 新增资产钱包自动续费配置路由别名,`src/router/routes/asyncRoutes.ts` 在设置下注册路由与菜单(仅超级管理员与平台账号可见)
|
||||||
|
- [x] 2.2 新增 `src/views/settings/asset-wallet-auto-renewal/index.vue`:读取配置并展示总开关、适用范围、指定主套餐、统一到期前天数、配置版本、最近保存操作者与时间(含 `enabled_name`/`scope_name`)
|
||||||
|
- [x] 2.3 实现保存表单:总开关(0/1)、适用范围(all/specified)、到期前天数(1–90 校验);指定范围时主套餐多选仅支持当前可售主套餐且非空
|
||||||
|
- [x] 2.4 保存成功后使用响应刷新 `config_version`、`updater`、`updated_at`,不做新增/删除记录操作
|
||||||
|
|
||||||
|
## 3. 通知适配
|
||||||
|
- [x] 3.1 确认 `src/utils/business/notificationNavigation.ts` 白名单包含 `iot_card` 与 `device`,目标可用时跳转资产详情
|
||||||
|
- [x] 3.2 确认 `src/components/core/layouts/art-notification/index.vue` 的 `expiry` 分类可展示 `asset.auto_renewal.failed`,`available=false` 时只显示正文不跳转
|
||||||
|
- [x] 3.3 确认该类型通知计入未读数与分类汇总
|
||||||
|
|
||||||
|
## 4. Verification
|
||||||
|
- [x] 4.1 验证 GET/PUT 字段传递与参数校验(enabled、scope、package_ids、days_before_expiry)
|
||||||
|
- [x] 4.2 验证指定范围时 package_ids 非空且仅可售主套餐
|
||||||
|
- [x] 4.3 验证通知类型展示、目标跳转与不可用目标处理
|
||||||
|
- [x] 4.4 验证非超级管理员/平台账号访问的 403 处理
|
||||||
|
- [x] 4.5 运行 lint、类型检查与构建
|
||||||
@@ -20,7 +20,7 @@ The admin frontend SHALL provide a bulk purchase form that binds one uploaded ba
|
|||||||
|
|
||||||
#### Scenario: Hide voucher upload for wallet payment
|
#### Scenario: Hide voucher upload for wallet payment
|
||||||
|
|
||||||
- **GIVEN** 用户选择代理钱包支付
|
- **GIVEN** 用户选择代理预存款支付
|
||||||
- **WHEN** 页面渲染或提交批量订购表单
|
- **WHEN** 页面渲染或提交批量订购表单
|
||||||
- **THEN** 页面 MUST NOT submit a voucher file
|
- **THEN** 页面 MUST NOT submit a voucher file
|
||||||
- **AND** 页面 MUST clear or ignore a voucher selected before switching to wallet payment
|
- **AND** 页面 MUST clear or ignore a voucher selected before switching to wallet payment
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
- [x] 2.1 新增批量订购入口和表单,支持选择代理商、支付方式和 Excel 文件。
|
- [x] 2.1 新增批量订购入口和表单,支持选择代理商、支付方式和 Excel 文件。
|
||||||
- [x] 2.2 提供前端静态 Excel 模板下载,并限制上传文件类型和必要的文件状态。
|
- [x] 2.2 提供前端静态 Excel 模板下载,并限制上传文件类型和必要的文件状态。
|
||||||
- [x] 2.3 线下支付时显示整批支付凭证上传,钱包支付时隐藏或清理凭证字段。
|
- [x] 2.3 线下支付时显示整批支付凭证上传,预存款支付时隐藏或清理凭证字段。
|
||||||
- [x] 2.4 复用订单列表已有 `VoucherUpload` 组件展示凭证上传进度,上传未完成时禁止创建任务。
|
- [x] 2.4 复用订单列表已有 `VoucherUpload` 组件展示凭证上传进度,上传未完成时禁止创建任务。
|
||||||
- [x] 2.5 创建请求携带 `shop_id`、`payment_method`、`file`、必要的 `voucher_file` 和 `request_id`。
|
- [x] 2.5 创建请求携带 `shop_id`、`payment_method`、`file`、必要的 `voucher_file` 和 `request_id`。
|
||||||
- [x] 2.6 防止重复点击和重复提交,创建成功后保存 `task_id` 并进入任务详情。
|
- [x] 2.6 防止重复点击和重复提交,创建成功后保存 `task_id` 并进入任务详情。
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Change: 业务用户组管理与店铺批量交接/导入(AUG26-003)
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
后台需要把平台用户账号按业务线归入"业务用户组",并基于组对店铺做筛选、批量交接负责人与 CSV 导入负责人。当前前端只有单个店铺设置平台业务员的能力,缺少业务用户组维护、成员归属、按组筛选、批量交接与导入入口;且现有 11 个新端点仅超管与平台账号可访问。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增业务用户组管理模块(API 层 + 维护页面 + 成员管理):
|
||||||
|
- GET/POST /api/admin/business-user-groups、GET/PUT/DELETE /api/admin/business-user-groups/{id}
|
||||||
|
- PUT /api/admin/business-user-groups/{id}/members、DELETE /api/admin/business-user-groups/members
|
||||||
|
- 编码创建后不可改;删除需 {"confirm": true} 且仅无成员组可删;更新支持部分字段,business_line 三态(缺省=不改 / ""=清空 / 枚举=设置)。
|
||||||
|
- 成员归属整批校验(全部启用平台用户 + 目标组启用),任一无效整批不生效;一账号至多一组。
|
||||||
|
- 店铺列表/详情读取侧扩展:
|
||||||
|
- GET /api/admin/shops 新增 business_user_group_id / business_line / ungrouped 筛选。
|
||||||
|
- 列表/详情响应新增 5 个只读推导字段(组 ID/编码/名称/启用/业务线);组停用仍返回组且 enabled=false(不算未分组);前端不缓存、不回写,改组后重新拉列表。
|
||||||
|
- 勾选批量交接:
|
||||||
|
- PUT /api/admin/shops/business-owner/batch:shop_ids(1-500,去重)+ business_owner_account_id;字段缺失=400,null=清空,ID=换绑;任一店铺无效或目标非启用业务员 -> 整批不写入,统一按 1005 提示。
|
||||||
|
- CSV 导入:
|
||||||
|
- 复用 StorageService.getUploadUrl + 预签名 PUT 直传(FilePurpose 增加 shop_import)。
|
||||||
|
- POST /api/admin/shops/business-owner-imports 创建异步任务,轮询详情、展示行级结果;前端自带模板(表头 店铺编码,操作类型,业务员登录账号,备注,换绑/清空)。
|
||||||
|
- 入口与权限(代理/企业不渲染入口):
|
||||||
|
- 路由 meta roles ['R_SUPER', 'R_ADMIN'];按钮权限码:
|
||||||
|
- business_user_group:page / business_user_group:create / business_user_group:update / business_user_group:delete / business_user_group:members
|
||||||
|
- shop:business_owner_batch
|
||||||
|
- shop:business_owner_import
|
||||||
|
|
||||||
|
## Not In Scope
|
||||||
|
|
||||||
|
- 不实现后端接口、不改数据库。
|
||||||
|
- 业务用户组不承载角色权限/数据范围语义;前端不引入组层级与组管理员概念。
|
||||||
|
- 导入结果不支持导出,仅在页面展示行级明细。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: business-user-group-management(新增)、shop-management(扩展)
|
||||||
|
- Affected code:
|
||||||
|
- src/types/api/businessUserGroup.ts(新增)
|
||||||
|
- src/api/modules/businessUserGroup.ts(新增)
|
||||||
|
- src/types/api/shop.ts、src/api/modules/shop.ts
|
||||||
|
- src/api/modules/storage.ts(FilePurpose 增加 shop_import)
|
||||||
|
- src/views/shop-management/business-user-groups/index.vue(新增页面)
|
||||||
|
- src/views/shop-management/list/index.vue
|
||||||
|
- src/router/routes/asyncRoutes.ts、src/router/routesAlias.ts、src/locales/langs/{zh,en}.json
|
||||||
|
- src/template/批量换绑业务员模板.csv(前端自带模板)
|
||||||
|
- API contracts: 11 个端点(7 个业务用户组 + 店铺筛选/字段 + 批量交接 + 导入三步)
|
||||||
|
- Dependencies:
|
||||||
|
- docs/产品迭代8月份/业务用户组.md(7 个组端点 OpenAPI)
|
||||||
|
- AUG26-003 前端对接说明(店铺侧端点)
|
||||||
|
|
||||||
|
## 待确认
|
||||||
|
|
||||||
|
- 成员选择器复用 GET /api/admin/shops/business-owner-candidates(启用平台账号);若组成员可包含非业务员平台账号,需后端补充平台账号列表端点。
|
||||||
|
- 导入任务与行级结果字段名(file_key、status、items[] 等)以生成文档为准,提案按对接说明先行对齐。
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 业务用户组维护
|
||||||
|
|
||||||
|
后台 MUST 提供业务用户组列表、详情、创建、更新与删除能力(GET/POST /api/admin/business-user-groups、GET/PUT/DELETE /api/admin/business-user-groups/{id})。列表 MUST 支持 page/page_size/enabled/keyword/business_line 筛选,且每项 MUST 返回 business_line_name。创建时 code(1-64)与 name 必填,编码未删除组内唯一且创建后不可修改;business_line 可空(standard/smart/other),sort/enabled/remark 可选。更新 MUST 仅允许名称、业务线、排序、启停与备注,且 business_line 支持三态:字段缺失不修改、传 "" 清空、传枚举设置;删除 MUST 携带 {"confirm": true},仅无成员组可删除。
|
||||||
|
|
||||||
|
#### Scenario: 查询业务用户组列表
|
||||||
|
|
||||||
|
- **GIVEN** 用户进入业务用户组维护页
|
||||||
|
- **WHEN** 前端以 page/page_size/enabled/keyword/business_line 发起列表查询
|
||||||
|
- **THEN** 前端 MUST 展示分页结果且每项显示 business_line_name
|
||||||
|
- **AND** 下拉数据源复用该列表接口
|
||||||
|
|
||||||
|
#### Scenario: 创建时编码不可变
|
||||||
|
|
||||||
|
- **GIVEN** 用户填写名称与稳定编码创建用户组
|
||||||
|
- **WHEN** 提交成功或编码重复被后端拒绝
|
||||||
|
- **THEN** 编码在创建后 MUST NOT 出现在编辑表单中
|
||||||
|
- **AND** 编码重复时 MUST 展示后端返回的"业务用户组编码已存在"
|
||||||
|
|
||||||
|
#### Scenario: 业务线三态更新
|
||||||
|
|
||||||
|
- **GIVEN** 用户编辑某组的业务线
|
||||||
|
- **WHEN** 表单传 ""、传枚举或省略该字段
|
||||||
|
- **THEN** 前端 MUST 分别按"清空 / 设置 / 不修改"构造请求体
|
||||||
|
|
||||||
|
#### Scenario: 删除需二次确认
|
||||||
|
|
||||||
|
- **GIVEN** 用户点击删除某组
|
||||||
|
- **WHEN** 确认弹窗提交
|
||||||
|
- **THEN** 前端 MUST 携带 {"confirm": true}
|
||||||
|
- **AND** 有成员时 MUST 提示"用户组仍有成员,只能停用或先移走成员"且不做物理删除
|
||||||
|
- **AND** 无成员时删除成功并刷新列表
|
||||||
|
|
||||||
|
### Requirement: 成员归属批量维护
|
||||||
|
|
||||||
|
后台 MUST 支持按平台用户账号把成员批量设置进组(PUT /api/admin/business-user-groups/{id}/members)与批量清空(DELETE /api/admin/business-user-groups/members,携带 body account_ids)。两种操作 MUST 复用同一校验:所有账号必须是启用平台用户且目标组启用,任一账号无效则整批不生效;清空后账号回到未分组;一账号至多一组。
|
||||||
|
|
||||||
|
#### Scenario: 批量设置成员
|
||||||
|
|
||||||
|
- **GIVEN** 用户在成员管理中选择多个启用平台账号
|
||||||
|
- **WHEN** 保存成员归属
|
||||||
|
- **THEN** 前端 MUST 以 account_ids 数组整体替换每个账号原归属
|
||||||
|
- **AND** 刷新后成员关系与最新分组一致
|
||||||
|
|
||||||
|
#### Scenario: 批量清空成员
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择若干账号执行清空
|
||||||
|
- **WHEN** 提交清空
|
||||||
|
- **THEN** 前端 MUST 调用 DELETE /api/admin/business-user-groups/members 并携带 account_ids body
|
||||||
|
- **AND** 清空成功的账号回到未分组
|
||||||
|
|
||||||
|
#### Scenario: 停用组的成员展示
|
||||||
|
|
||||||
|
- **GIVEN** 某组被停用但仍有成员
|
||||||
|
- **WHEN** 用户在成员管理或店铺列表看到该组
|
||||||
|
- **THEN** 前端 MUST 展示"已停用"标记并提供改组/清空入口
|
||||||
|
|
||||||
|
### Requirement: 访问控制与入口
|
||||||
|
|
||||||
|
业务用户组维护入口 MUST 仅对超级管理员与平台账号可见,代理/企业账号 MUST NOT 渲染入口;所有按钮 MUST 受权限码控制。
|
||||||
|
|
||||||
|
#### Scenario: 菜单按角色隐藏
|
||||||
|
|
||||||
|
- **GIVEN** 代理或企业账号登录
|
||||||
|
- **WHEN** 系统渲染菜单
|
||||||
|
- **THEN** MUST NOT 展示业务用户组菜单项
|
||||||
|
|
||||||
|
#### Scenario: 按钮权限控制
|
||||||
|
|
||||||
|
- **GIVEN** 平台账号具备部分业务用户组权限
|
||||||
|
- **WHEN** 渲染操作按钮
|
||||||
|
- **THEN** 前端 MUST 按 business_user_group:create/update/delete/members 控制显隐
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 店铺列表/详情业务用户组字段与筛选
|
||||||
|
|
||||||
|
店铺列表查询 MUST 支持 business_user_group_id、business_line、ungrouped 三个新筛选;列表与详情响应 MUST 提供 5 个只读推导字段:business_user_group_id(可空)、business_user_group_code、business_user_group_name、business_user_group_enabled、business_user_group_business_line。组停用仍返回组且 enabled=false(不算未分组);店铺不存组,字段实时推导,前端 MUST NOT 缓存或回写,改动组成员后重新拉取列表。
|
||||||
|
|
||||||
|
#### Scenario: 按业务用户组筛选
|
||||||
|
|
||||||
|
- **GIVEN** 用户在店铺列表选择业务用户组、业务线或勾选"未分组"
|
||||||
|
- **WHEN** 发起列表查询
|
||||||
|
- **THEN** 前端 MUST 提交 business_user_group_id/business_line/ungrouped 参数
|
||||||
|
- **AND** ungrouped=true 表示无负责人或负责人无分组
|
||||||
|
|
||||||
|
#### Scenario: 展示组字段
|
||||||
|
|
||||||
|
- **GIVEN** 店铺属于某启用或停用的业务用户组
|
||||||
|
- **WHEN** 渲染列表或详情
|
||||||
|
- **THEN** 前端 MUST 展示组名称/编码/业务线
|
||||||
|
- **AND** 组停用时 MUST 标记"已停用"且仍展示组归属
|
||||||
|
|
||||||
|
### Requirement: 勾选批量交接平台业务员
|
||||||
|
|
||||||
|
店铺列表 MUST 支持勾选店铺批量交接平台业务员(PUT /api/admin/shops/business-owner/batch,body shop_ids 1-500 去重 + business_owner_account_id)。字段缺失 MUST 按参数错误处理(400);传 null 表示清空负责人;传 ID 表示换绑。任一店铺不存在/已删除/越权或目标非启用业务员时整批不写入,前端 MUST 统一提示"无权限操作该资源或资源不存在";成功响应含 batch_key/shop_count/cleared。
|
||||||
|
|
||||||
|
#### Scenario: 批量换绑
|
||||||
|
|
||||||
|
- **GIVEN** 用户勾选 1-500 家店铺并选择启用业务员
|
||||||
|
- **WHEN** 提交批量交接
|
||||||
|
- **THEN** 前端 MUST 提交 shop_ids(去重)与 business_owner_account_id
|
||||||
|
- **AND** 成功后展示 shop_count 并刷新列表
|
||||||
|
|
||||||
|
#### Scenario: 批量清空负责人
|
||||||
|
|
||||||
|
- **GIVEN** 用户勾选店铺并选择"清空负责人"
|
||||||
|
- **WHEN** 提交批量交接
|
||||||
|
- **THEN** 前端 MUST 显式提交 business_owner_account_id: null
|
||||||
|
- **AND** MUST NOT 省略该字段
|
||||||
|
|
||||||
|
#### Scenario: 整批失败统一提示
|
||||||
|
|
||||||
|
- **GIVEN** 批量交接请求因任一店铺或目标账号不满足条件而被后端拒绝
|
||||||
|
- **WHEN** 前端收到 1005
|
||||||
|
- **THEN** 前端 MUST 展示统一文案且不写入任何店铺
|
||||||
|
|
||||||
|
### Requirement: 业务负责人 CSV 导入
|
||||||
|
|
||||||
|
店铺列表 MUST 支持通过 CSV 导入批量交接/清空业务负责人。流程 MUST 为:前端自带模板(表头 店铺编码,操作类型,业务员登录账号,备注,操作类型仅"换绑"或"清空")-> POST /api/admin/storage/upload-url(purpose=shop_import)拿预签名 URL 直传(15 分钟有效)-> POST /api/admin/shops/business-owner-imports(file_key)创建异步任务 -> 轮询 GET /api/admin/shops/business-owner-imports/{id} 展示结果。行级失败不等于任务失败:任务 status=3 也可能有失败行,MUST 按 items[] 展示明细;表头/编码不符为任务级失败,items 为空数组,只展示 error_message。
|
||||||
|
|
||||||
|
#### Scenario: 下载模板并上传
|
||||||
|
|
||||||
|
- **GIVEN** 用户进入导入界面
|
||||||
|
- **WHEN** 点击下载模板或选择文件
|
||||||
|
- **THEN** 前端 MUST 提供表头完全一致的模板文件
|
||||||
|
- **AND** 上传后 MUST 以 purpose=shop_import 获取上传地址并直传
|
||||||
|
|
||||||
|
#### Scenario: 创建任务并轮询
|
||||||
|
|
||||||
|
- **GIVEN** 文件上传成功拿到 file_key
|
||||||
|
- **WHEN** 提交创建导入任务
|
||||||
|
- **THEN** 前端 MUST 携带 file_key 调用创建接口
|
||||||
|
- **AND** MUST 轮询任务详情直到 status 为已完成(3)或失败(4)
|
||||||
|
|
||||||
|
#### Scenario: 展示行级失败明细
|
||||||
|
|
||||||
|
- **GIVEN** 任务完成但存在失败行
|
||||||
|
- **WHEN** 前端渲染结果
|
||||||
|
- **THEN** 前端 MUST 展示 total_count/success_count/fail_count
|
||||||
|
- **AND** MUST 按 items[] 展示各行的 line/shop_code/operation_type/status/reason
|
||||||
|
- **AND** 失败行保留原负责人
|
||||||
|
|
||||||
|
#### Scenario: 任务级失败提示
|
||||||
|
|
||||||
|
- **GIVEN** 表头或编码不符导致任务级失败
|
||||||
|
- **WHEN** 前端渲染结果
|
||||||
|
- **THEN** 前端 MUST 只展示 error_message 且明细为空
|
||||||
34
openspec/changes/add-business-user-group-management/tasks.md
Normal file
34
openspec/changes/add-business-user-group-management/tasks.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## 1. API Contract 层
|
||||||
|
|
||||||
|
- [x] 1.1 新增 src/types/api/businessUserGroup.ts:组项/分页/创建/更新/删除/成员设置与结果类型
|
||||||
|
- [x] 1.2 新增 src/api/modules/businessUserGroup.ts:封装 7 个组端点(DELETE 带 body,需请求层支持)
|
||||||
|
- [x] 1.3 扩展 src/types/api/shop.ts:列表筛选 business_user_group_id/business_line/ungrouped,响应只读组字段,批量交接与导入任务/行级结果类型
|
||||||
|
- [x] 1.4 扩展 src/api/modules/shop.ts:batchUpdateBusinessOwner、createBusinessOwnerImport、getBusinessOwnerImportDetail、getBusinessOwnerImportList
|
||||||
|
- [x] 1.5 扩展 src/api/modules/storage.ts:FilePurpose 增加 shop_import
|
||||||
|
|
||||||
|
## 2. 业务用户组页面
|
||||||
|
|
||||||
|
- [x] 2.1 新增 src/views/shop-management/business-user-groups/index.vue:列表(筛选/分页/启停/业务线)
|
||||||
|
- [x] 2.2 新建/编辑弹窗:code 创建后不可改、business_line 三态、名称/排序/备注/启停
|
||||||
|
- [x] 2.3 删除:二次确认(confirm=true),有成员时按后端提示处理
|
||||||
|
- [x] 2.4 成员管理(对话框/抽屉):成员选择器(复用 business-owner-candidates)+ 批量设置 + 批量清空;停用组保留成员并标记"已停用"
|
||||||
|
|
||||||
|
## 3. 店铺列表扩展
|
||||||
|
|
||||||
|
- [x] 3.1 列表搜索区新增业务用户组下拉与业务线筛选、ungrouped 勾选(数据源来自组列表接口)
|
||||||
|
- [x] 3.2 列表/详情展示组字段(停用组标记,不缓存不回写)
|
||||||
|
- [x] 3.3 批量交接弹窗:勾选店铺 + 选择业务员/清空(显式 null),整批失败统一 1005 文案
|
||||||
|
- [x] 3.4 导入入口:模板下载、文件校验/上传(purpose=shop_import)、创建任务、轮询并展示行级明细
|
||||||
|
- [x] 3.5 导入任务列表(状态筛选)
|
||||||
|
|
||||||
|
## 4. 路由/菜单/权限/i18n
|
||||||
|
|
||||||
|
- [x] 4.1 新增 /shop-management/business-user-groups 路由与菜单,roles ['R_SUPER', 'R_ADMIN']
|
||||||
|
- [x] 4.2 按钮权限编码落地(business_user_group*/shop:business_owner_batch/shop:business_owner_import)
|
||||||
|
- [x] 4.3 中英文案 menus.shopManagement.businessUserGroups 等
|
||||||
|
|
||||||
|
## 5. 验证
|
||||||
|
|
||||||
|
- [x] 5.1 运行 npm run build(含 vue-tsc --noEmit)
|
||||||
|
- [x] 5.2 运行 npm run check:encoding
|
||||||
|
- [x] 5.3 运行 openspec validate add-business-user-group-management --strict
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
现有前端没有运营商通道阈值命中记录页面或对应 API 类型。后端已提供只读分页查询接口,页面需忠实呈现不可变的命中时快照和周期恢复状态。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
- Goals: 提供受限于平台账号的筛选、分页和证据查询;不丢失空值的历史语义。
|
||||||
|
- Non-Goals: 修改通道阈值、发起停复机、导出记录、查看卡片详情,或查询上游响应。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Decision: 在系统管理路径下新增独立隐藏查询页,由运营商管理页提供入口;路由使用 `allowedUserTypes: [1, 2]`,页面自身也在请求前检查平台账号。
|
||||||
|
- Rationale: 接口只允许超级管理员和平台账号。前端隐藏/保护页面入口,后端仍是最终授权及卡数据范围过滤边界。
|
||||||
|
- Decision: 精确 ID 筛选使用数值输入,计费周期起点使用日期时间选择器,判定时间使用日期时间范围选择器;时间序列化为带时区 RFC3339 秒级字符串。
|
||||||
|
- Rationale: 与接口的精确匹配和闭区间语义一致,避免按日期截断或丢失时区。
|
||||||
|
- Decision: 页面仅展示响应快照,不再查询通道配置或根据当前阈值重算历史值。
|
||||||
|
- Rationale: 命中值冻结于判定时;历史缺失值必须保留接口规定的空值含义。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 查询页不依赖后端菜单节点;前端入口仅对平台账号显示,页面路由限制用户类型,API 仍执行最终授权和数据范围过滤。
|
||||||
|
- 表格包含多列证据字段,采用现有可横向滚动的管理台表格以保持信息完整。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Change: 新增运营商通道阈值命中记录查询
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
达量停机与周期恢复需要可追溯的数字证据。运营人员需要查询每次停机判定时冻结的累计流量、实际阈值、计费周期、触发来源,以及周期解锁和复机结果;通道阈值后续变更不能影响历史记录。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 在运营商管理页提供“通道阈值命中记录”入口,打开独立只读查询页并接入 `GET /api/admin/carrier-traffic-threshold-hits`。
|
||||||
|
- 支持按运营商通道 ID、IoT 卡 ID、计费周期起点精确匹配,并按判定时间闭区间筛选。
|
||||||
|
- 展示命中时的流量和阈值快照、判定/触发信息、锁状态、解锁时间、复机结果及安全失败原因。
|
||||||
|
- 仅超级管理员与平台账号可访问;数据范围过滤及越权资源不可区分由后端强制执行,前端不替代后端授权。
|
||||||
|
- 对接口定义的空历史值使用明确的“无”或“未处理/历史记录”展示,不发起额外的通道阈值或上游接口查询。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `carrier-traffic-threshold-hits`
|
||||||
|
- Affected code:
|
||||||
|
- `src/api/modules/carrierTrafficThresholdHit.ts`
|
||||||
|
- `src/types/api/carrierTrafficThresholdHit.ts`
|
||||||
|
- `src/views/system/carrier-traffic-threshold-hits/index.vue`
|
||||||
|
- `src/router/routesAlias.ts`, `src/router/routes/staticRoutes.ts`, `src/views/system/carrier-management/index.vue`
|
||||||
|
- `src/locales/langs/zh.json`, `src/locales/langs/en.json`
|
||||||
|
- Breaking changes: 无;仅新增只读查询接口消费与页面入口。
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Carrier Traffic Threshold Hit Record Query
|
||||||
|
|
||||||
|
The admin frontend SHALL provide a read-only paginated view of carrier traffic threshold hit records using `GET /api/admin/carrier-traffic-threshold-hits`.
|
||||||
|
|
||||||
|
#### Scenario: Restrict page access to platform accounts
|
||||||
|
|
||||||
|
- **GIVEN** a user is not a super administrator or platform account
|
||||||
|
- **WHEN** the user navigates to the carrier traffic threshold hit page
|
||||||
|
- **THEN** the frontend MUST deny access and MUST NOT issue the hit-record query
|
||||||
|
- **AND** backend authorization and card data-scope filtering MUST remain authoritative
|
||||||
|
|
||||||
|
#### Scenario: Filter hit records
|
||||||
|
|
||||||
|
- **GIVEN** an authorized platform user opens the hit-record page
|
||||||
|
- **WHEN** the user searches by optional carrier ID, card ID, billing period start, or judged-at time range
|
||||||
|
- **THEN** the frontend MUST send `carrier_id`, `card_id`, `period_start`, `start_time`, and `end_time` only when provided
|
||||||
|
- **AND** period and judged-at timestamps MUST be timezone-qualified RFC3339 values with second precision
|
||||||
|
- **AND** `start_time` and `end_time` MUST represent the API's inclusive time interval
|
||||||
|
|
||||||
|
#### Scenario: Display immutable hit evidence and recovery result
|
||||||
|
|
||||||
|
- **GIVEN** the query returns threshold-hit records
|
||||||
|
- **WHEN** the frontend renders the table
|
||||||
|
- **THEN** it MUST display record/card/carrier IDs, hit traffic, frozen threshold value and unit, billing period start, judged time, trigger source, lock status, unlock time, resume result, failure reason, and creation time
|
||||||
|
- **AND** it MUST NOT recompute hit traffic or threshold from current channel configuration
|
||||||
|
- **AND** it MUST display nullable historical traffic/threshold values as “无”, blank resume results as “未处理或历史记录”, and missing unlock/failure details as “无”
|
||||||
|
|
||||||
|
#### Scenario: Preserve server ordering and pagination
|
||||||
|
|
||||||
|
- **GIVEN** the user queries hit records
|
||||||
|
- **WHEN** the frontend loads or changes the page
|
||||||
|
- **THEN** it MUST send `page` and `page_size` (never exceeding 100)
|
||||||
|
- **AND** it MUST render the server-provided page, size, total, and item order without client-side re-sorting
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
## 1. API Contract
|
||||||
|
|
||||||
|
- [x] 1.1 增加命中记录查询参数、记录项及分页响应类型。
|
||||||
|
- [x] 1.2 增加 `GET /api/admin/carrier-traffic-threshold-hits` 服务方法并统一导出。
|
||||||
|
|
||||||
|
## 2. Admin UI
|
||||||
|
|
||||||
|
- [x] 2.1 新增超级管理员/平台账号可访问的隐藏路由、运营商管理页入口及中英文标题。
|
||||||
|
- [x] 2.2 实现筛选、分页和命中/周期恢复快照表格,按接口约定展示空值。
|
||||||
|
- [x] 2.3 运行 OpenSpec 校验、ESLint 与生产构建。
|
||||||
60
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
60
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Change: 退款佣金回扣改为独立回溯明细(前端适配)
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
后端调整了退款佣金回扣口径:退款不再把原佣金整单置为「已失效」,而是原佣金行保持「已发放」不变,另新增一条独立负数、不可提现的「回溯明细」行。对应前端接口说明(简短版)要求:
|
||||||
|
|
||||||
|
- 佣金记录列表新增 `status` 查询参数(含 5 回溯)与行级 `source` 字段(`original` / `clawback`)。
|
||||||
|
- 新增佣金记录详情接口 `GET /api/admin/shops/{shop_id}/commission-records/{id}?source=clawback`。
|
||||||
|
- 新增导出场景 `scene=commission_record`(后端已进白名单)。
|
||||||
|
- 两个必改点:列表行 key 必须用 `source + ':' + id`(原佣金与回溯 ID 空间独立,同一页会重复);渲染需支持负数金额(标红)与「不可提现」标识。
|
||||||
|
|
||||||
|
前端当前实现基于旧语义(`ShopCommissionRecordItem` 无 `source`,列表行 key 直接用 `id`,退款后原行被视为已失效),需按新口径适配。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 类型层:`ShopCommissionRecordItem` 新增 `source`、`original_commission_id`、`refund_id`、`refund_no`、`withdrawable`、`clawback_records`、`clawback_total_amount`;新增回溯摘要与详情类型;`CommissionRecordQueryParams.status` 支持 5(回溯)。
|
||||||
|
- API 层:新增 `getShopCommissionRecordDetail(shopId, id, source?)`,`source` 省略时按原佣金处理。
|
||||||
|
- 列表页(代理侧「我的佣金」与管理侧「代理资金概览 > 佣金明细」):行 key 改为 `source + ':' + id`;回溯行展示「回溯」状态与「不可提现」标识;负数金额与可为负的 `balance_after` 标红;`status` 筛选项新增「回溯」;详情入口必须携带该行 `source`。
|
||||||
|
- 详情展示:原佣金详情展示 `clawback_records` 摘要与 `clawback_total_amount`;回溯详情展示来源 `original_commission`;越权/不存在统一提示「佣金明细不存在」,不做存在性判断。
|
||||||
|
- 导出:`ExportTaskScene` 增加 `commission_record`;新增导出佣金记录场景页、路由、菜单与 i18n;佣金记录列表导出入口只提交受支持的筛选 key(`shop_id` / `status` / `commission_source` / `order_no`)。
|
||||||
|
- 字段兜底:后端新增字段为 `omitempty`,前端取 `?? []` / `?? 0`,金额单位为分。
|
||||||
|
|
||||||
|
## Not In Scope
|
||||||
|
|
||||||
|
- 不实现后端接口、不改数据库、不改审批实例数据。
|
||||||
|
- 既有字段无改名/改类型;`commission-stats`、`commission-daily-stats` 口径未变,本次不动。
|
||||||
|
- 时间筛选属另一 Change(`add-export-time-filter-standards`),本次不涉及。
|
||||||
|
- 语义提醒:退款不再写 `status=4`,旧假设「退款后原行变已失效」已失效,需在文案/注释体现。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `commission-management`、`export-task-management`
|
||||||
|
- Affected code:
|
||||||
|
- `src/types/api/commission.ts`
|
||||||
|
- `src/api/modules/commission.ts`
|
||||||
|
- `src/views/commission-management/my-commission/index.vue`
|
||||||
|
- `src/views/commission-management/agent-fund-overview/index.vue`
|
||||||
|
- `src/types/api/exportTask.ts`
|
||||||
|
- `src/config/constants/exportTask.ts`
|
||||||
|
- `src/views/asset-management/export-task-management/export-commission-record/index.vue`
|
||||||
|
- `src/router/routes/asyncRoutes.ts`、`src/router/routesAlias.ts`、`src/locales/langs/{zh,en}.json`
|
||||||
|
- Dependencies:
|
||||||
|
- `docs/产品迭代8月份/frontend-api-simple.md`(后端契约)
|
||||||
|
- 后端已把 `scene=commission_record` 加入导出场景白名单
|
||||||
|
- 需确认导出场景权限编码与场景页是否必需(见 Open Questions)
|
||||||
|
- Breaking changes:
|
||||||
|
- 列表行 key 由 `id` 改为 `source + ':' + id`;未携带 `source` 的详情请求会命中原佣金行。
|
||||||
|
|
||||||
|
## 接口确认结果(来自 OpenAPI)
|
||||||
|
|
||||||
|
- 详情响应 `DtoShopCommissionRecordDetailResp`:`source`、`record`(`DtoShopCommissionRecordItem`)、`clawback_records`(仅原佣金返回)、`original_commission`(仅回溯详情返回)。
|
||||||
|
- `DtoShopCommissionRecordItem` 新增 `source`、`released_at`、`withdrawable`(boolean,nullable,仅回溯行返回且恒 false)、`original_commission_id`、`refund_id`、`refund_no`、`clawback_records`、`clawback_total_amount`。
|
||||||
|
- `DtoShopCommissionClawbackItem`:`id`、`amount`(恒负)、`balance_after`(可为负)、`original_commission_id`、`refund_id`、`refund_no`、`status`(5 回溯)、`status_name`、`withdrawable`、`created_at`。
|
||||||
|
- 详情接口越权与不存在返回同一结果,前端统一提示「佣金明细不存在」。
|
||||||
|
- 已确认需要新增「导出佣金记录」菜单页。
|
||||||
|
|
||||||
|
## 待确认(已按仓库既有约定实现,如需调整请告知)
|
||||||
|
|
||||||
|
1. 导出场景权限编码:按既有约定实现为 `export_task:commission_record_detail` / `export_task:commission_record_download`。
|
||||||
|
2. 佣金记录列表导出按钮权限:按 `agent_wallet_transaction:export` 的同级约定实现为 `commission_record:export`。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 佣金记录列表回溯行与状态筛选
|
||||||
|
佣金记录列表(`GET /api/admin/shops/{shop_id}/commission-records`)MUST 支持 `status` 查询参数(1 已冻结 / 2 解冻中 / 3 已发放 / 4 已失效 / 5 回溯 / 99 待人工修正),且 MUST NOT 提供 `source` 查询参数;`status=5` 即等价于只看回溯行。
|
||||||
|
|
||||||
|
#### Scenario: 只看回溯行
|
||||||
|
- **GIVEN** 用户在佣金记录列表选择状态「回溯」
|
||||||
|
- **WHEN** 前端发起列表查询
|
||||||
|
- **THEN** 前端 MUST 只传 `status=5`
|
||||||
|
- **AND** 前端 MUST NOT 传 `source` 参数
|
||||||
|
|
||||||
|
### Requirement: 佣金记录行新增字段与兜底
|
||||||
|
列表响应的每一行 MUST 支持 `source`(`original` / `clawback`);回溯行 MUST 支持 `original_commission_id`、`refund_id`、`refund_no`、`withdrawable`(恒 `false`)与可为负数的 `amount`、`balance_after`;原佣金行 MUST 支持 `clawback_records` 摘要与 `clawback_total_amount`(负值)。后端 `omitempty` 缺省字段前端 MUST 以 `?? []` / `?? 0` 兜底,金额单位为分。
|
||||||
|
|
||||||
|
#### Scenario: 缺省字段兜底
|
||||||
|
- **GIVEN** 列表响应中的原佣金行未返回 `clawback_records`
|
||||||
|
- **WHEN** 前端渲染该行
|
||||||
|
- **THEN** 前端 MUST 按空数组处理并正常渲染
|
||||||
|
- **AND** 前端 MUST NOT 因缺省字段报错或渲染 `undefined`
|
||||||
|
|
||||||
|
### Requirement: 列表行标识与负数渲染
|
||||||
|
列表 MUST 使用 `source + ':' + id` 作为行 key(原佣金与回溯 ID 空间独立,同一页可能出现相同 `id`);回溯行 MUST 展示「不可提现」标识;负数 `amount` 与 `balance_after` MUST 标红显示;任何跳转详情的入口 MUST 携带该行的 `source`,缺省按原佣金处理。
|
||||||
|
|
||||||
|
#### Scenario: 同一页出现重复 id
|
||||||
|
- **GIVEN** 同一页列表中同时存在原佣金行与回溯行且 `id` 相同
|
||||||
|
- **WHEN** 前端渲染表格
|
||||||
|
- **THEN** 前端 MUST 以 `source + ':' + id` 区分两行
|
||||||
|
- **AND** 前端 MUST NOT 出现行复用或渲染错位
|
||||||
|
|
||||||
|
#### Scenario: 负数金额标红
|
||||||
|
- **GIVEN** 某回溯行 `amount` 为负值
|
||||||
|
- **WHEN** 前端渲染金额列
|
||||||
|
- **THEN** 前端 MUST 标红展示该负值
|
||||||
|
|
||||||
|
### Requirement: 佣金记录详情接口封装
|
||||||
|
前端 MUST 提供 `GET /api/admin/shops/{shop_id}/commission-records/{id}` 的详情封装,并支持可选 `source`:`source=clawback` 时返回回溯详情,`source` 省略时按原佣金处理。原佣金详情 MUST 返回 `clawback_records`;回溯详情 MUST 返回 `original_commission`;越权与不存在 MUST 返回同一结果(佣金明细不存在),前端 MUST NOT 用该接口做存在性判断。
|
||||||
|
|
||||||
|
#### Scenario: 打开回溯行详情
|
||||||
|
- **GIVEN** 用户在列表点击某条回溯行
|
||||||
|
- **WHEN** 前端请求详情
|
||||||
|
- **THEN** 前端 MUST 携带 `source=clawback` 与该行 `id`
|
||||||
|
- **AND** 详情 MUST 展示来源 `original_commission`
|
||||||
|
|
||||||
|
#### Scenario: 越权统一提示
|
||||||
|
- **GIVEN** 详情接口返回「佣金明细不存在」
|
||||||
|
- **WHEN** 前端渲染详情
|
||||||
|
- **THEN** 前端 MUST 展示统一不存在提示
|
||||||
|
- **AND** 前端 MUST NOT 区分越权与不存在两种原因
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 佣金记录导出场景
|
||||||
|
前端 MUST 支持导出场景 `scene=commission_record`(后端已进白名单),并在导出管理中提供该场景的任务列表入口;创建导出任务时 MUST 提交固定 `scene=commission_record`。导出粒度为佣金记录:原佣金与回溯各占一行,金额与余额同样按分转元、负数带 `-` 展示。
|
||||||
|
|
||||||
|
#### Scenario: 场景页固定查询
|
||||||
|
- **GIVEN** 用户打开导出佣金记录页面
|
||||||
|
- **WHEN** 页面查询导出任务列表
|
||||||
|
- **THEN** 前端 MUST 调用 `GET /api/admin/export-tasks` 且带 `scene=commission_record`
|
||||||
|
- **AND** 用户 MUST NOT 能在该页面切换到其他场景
|
||||||
|
|
||||||
|
### Requirement: 佣金记录导出筛选受限
|
||||||
|
佣金记录列表发起导出时,`query` MUST 只包含后端支持的筛选 key:`shop_id`、`status`、`commission_source`、`order_no`;MUST NOT 提交 `iccid`、`virtual_no` 及时间筛选,导出弹窗 MUST NOT 直接复用列表全部条件。
|
||||||
|
|
||||||
|
#### Scenario: 列表筛选含不支持的 key
|
||||||
|
- **GIVEN** 佣金记录列表当前筛选包含 `iccid` 与时间范围
|
||||||
|
- **WHEN** 用户确认创建导出任务
|
||||||
|
- **THEN** 提交的 `query` MUST NOT 包含 `iccid`、`virtual_no` 或时间字段
|
||||||
|
- **AND** 提交的 `query` MUST 仅保留 `shop_id`、`status`、`commission_source`、`order_no` 中已填写的项
|
||||||
27
openspec/changes/add-commission-clawback-records/tasks.md
Normal file
27
openspec/changes/add-commission-clawback-records/tasks.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## 1. 类型与 API 层(commission-api)
|
||||||
|
- [x] 1.1 `CommissionStatus` 补充 5(回溯);修正 `CommissionRecordQueryParams.status` 过期注释并支持回溯值
|
||||||
|
- [x] 1.2 `ShopCommissionRecordItem` 新增 `source`、`original_commission_id`、`refund_id`、`refund_no`、`withdrawable`、`clawback_records`、`clawback_total_amount`
|
||||||
|
- [x] 1.3 新增回溯摘要与佣金记录详情类型(原佣金详情含 `clawback_records`,回溯详情含 `original_commission`)
|
||||||
|
- [x] 1.4 `CommissionService` 新增 `getShopCommissionRecordDetail(shopId, id, source?)`
|
||||||
|
|
||||||
|
## 2. 列表页(commission-records-list)
|
||||||
|
- [x] 2.1 行 key 改为 `source + ':' + id`(`my-commission` 与 `agent-fund-overview` 佣金明细)
|
||||||
|
- [x] 2.2 回溯行展示「回溯」状态与「不可提现」标识
|
||||||
|
- [x] 2.3 负数金额与可为负的 `balance_after` 标红渲染
|
||||||
|
- [x] 2.4 `status` 筛选项新增「回溯」
|
||||||
|
- [x] 2.5 列表新增详情入口,且请求必须携带该行 `source`
|
||||||
|
|
||||||
|
## 3. 详情展示(commission-record-detail)
|
||||||
|
- [x] 3.1 原佣金详情展示 `clawback_records` 摘要与 `clawback_total_amount`
|
||||||
|
- [x] 3.2 回溯详情展示来源 `original_commission`
|
||||||
|
- [x] 3.3 越权/不存在统一提示「佣金明细不存在」,不作为存在性判断依据
|
||||||
|
|
||||||
|
## 4. 导出(commission-record-export)
|
||||||
|
- [x] 4.1 `ExportTaskScene` 增加 `commission_record` 并补场景配置
|
||||||
|
- [x] 4.2 新增导出佣金记录场景页、路由、菜单与中英文案
|
||||||
|
- [x] 4.3 佣金记录列表新增导出入口,`query` 仅提交 `shop_id` / `status` / `commission_source` / `order_no`
|
||||||
|
|
||||||
|
## 5. 验证
|
||||||
|
- [x] 5.1 运行 `npm run build`(含 `vue-tsc --noEmit`)
|
||||||
|
- [x] 5.2 运行 `npm run check:encoding`
|
||||||
|
- [x] 5.3 运行 `openspec.cmd validate add-commission-clawback-records --strict`
|
||||||
@@ -24,7 +24,7 @@ The system SHALL provide a paginated list of all commission withdrawal requests
|
|||||||
|
|
||||||
### Requirement: Withdrawal Request Approval
|
### Requirement: Withdrawal Request Approval
|
||||||
|
|
||||||
The system SHALL allow administrators to approve withdrawal requests using the approval API contract. The administrator SHALL enter the remark manually, and the system SHALL submit the request's account name, account number, amount, and withdrawal method from the selected list item together with the manual payment type.
|
The system SHALL allow administrators to approve withdrawal requests.
|
||||||
|
|
||||||
#### Scenario: Approve withdrawal request
|
#### Scenario: Approve withdrawal request
|
||||||
|
|
||||||
@@ -32,13 +32,6 @@ The system SHALL allow administrators to approve withdrawal requests using the a
|
|||||||
- **THEN** the system marks the request as approved
|
- **THEN** the system marks the request as approved
|
||||||
- **AND** updates the processor information and timestamp
|
- **AND** updates the processor information and timestamp
|
||||||
|
|
||||||
#### Scenario: Submit approval details
|
|
||||||
|
|
||||||
- **WHEN** administrator confirms approval after optionally entering a remark
|
|
||||||
- **THEN** the system submits the selected request's account name, account number, amount, and withdrawal method with `payment_type` set to `manual`
|
|
||||||
- **AND** submits the manually entered remark
|
|
||||||
- **AND** refreshes the list after a successful response
|
|
||||||
|
|
||||||
### Requirement: Withdrawal Request Rejection
|
### Requirement: Withdrawal Request Rejection
|
||||||
|
|
||||||
The system SHALL allow administrators to reject withdrawal requests with a reason.
|
The system SHALL allow administrators to reject withdrawal requests with a reason.
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
- [x] 5.6 Implement approve action with confirmation dialog
|
- [x] 5.6 Implement approve action with confirmation dialog
|
||||||
- [x] 5.7 Implement reject action with reason input dialog
|
- [x] 5.7 Implement reject action with reason input dialog
|
||||||
- [x] 5.8 Add status badges using unified status components
|
- [x] 5.8 Add status badges using unified status components
|
||||||
- [x] 5.9 Submit approval request details from the selected list item and collect a manual remark
|
|
||||||
|
|
||||||
## 6. Withdrawal Settings Module
|
## 6. Withdrawal Settings Module
|
||||||
|
|
||||||
|
|||||||
88
openspec/changes/add-employee-collection/design.md
Normal file
88
openspec/changes/add-employee-collection/design.md
Normal 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` 与状态字段禁用关闭按钮,最终一致性以后端校验为准。
|
||||||
25
openspec/changes/add-employee-collection/proposal.md
Normal file
25
openspec/changes/add-employee-collection/proposal.md
Normal 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`(未被引用时可改),可停用。
|
||||||
|
- 新增「员工代收款账单」页面:应收/已核销/未核销/待处理账单统计 + 列表 + 详情;普通员工仅见本人账单,超级管理员可见全部;存在审批中申请时账单不可关闭,关闭必须填写原因。
|
||||||
|
- 新增「核销申请」页面:列表 + 详情(含分摊账单、付款凭证、付款信息与全部审批尝试记录)+ 创建/重新提交弹窗;一笔线下收款可核销 1~N 张账单,填写付款金额、付款方名称、付款时间、外部交易流水号、选择收款方式并上传付款凭证;提交后自动发起企微审批;仅已驳回申请可修改并重新提交,重新提交生成新的审批实例且历史记录不被覆盖。
|
||||||
|
- 扩展企业微信审批场景:新增 `employee_collection_approval` 业务类型,复用既有企微应用列表、模板控件同步与业务字段查询接口。
|
||||||
|
- 调整订单创建:由平台账号操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单,该场景 `payment_voucher_key` 改为非必填,付款凭证改在核销申请中提交,字段结构保持不变。
|
||||||
|
- 附件接口仅返回对象 Key,统一通过系统既有预签名下载接口展示。
|
||||||
|
- **不实现后端接口、数据库、Worker、企微回调**;**不实现 H5/C 端页面**。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `employee-collection`
|
||||||
|
- Affected code: `src/types/api/employeeCollection.ts`、`src/api/modules/employeeCollection.ts`、`src/views/finance/employee-collection/*`、`src/router/routes/asyncRoutes.ts`、`src/router/routesAlias.ts`、`src/config/constants/augustIteration.ts`、`src/types/api/wecom.ts`、`src/views/settings/wecom/scenes/index.vue`、`src/views/order-management/order-list/index.vue`、`src/locales/langs/{zh,en}.json`
|
||||||
|
- Dependencies: `docs/产品迭代8月份/员工代收款功能简介.md`
|
||||||
|
- Contract note: `docs/admin-openapi.yaml` 未随仓库提供,字段以需求文档描述的后端契约与创建订单接口的 OpenAPI 片段为准;类型集中在单一文件,联调时便于收敛。
|
||||||
@@ -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 能够使用一笔线下收款核销 1~N 张账单。创建申请 MUST 提交收款方式 `payment_method_id`、付款金额 `paid_amount`(分,大于 0)、付款方名称 `payer_name`、付款时间 `paid_at`(带时区 RFC3339)、外部交易流水号 `external_transaction_no`、付款凭证 `payment_voucher_keys`(1~5 个对象 Key)与账单分摊 `allocations`。超级管理员代办时 MUST 填写 `acting_reason`,本人办理 MUST NOT 提交该字段。提交成功后系统 MUST 自动发起企业微信审批。
|
||||||
|
|
||||||
|
#### Scenario: 一笔收款核销多张账单
|
||||||
|
- **GIVEN** 用户选择了多张可核销账单
|
||||||
|
- **WHEN** 其录入各账单核销金额、选择收款方式并填写付款事实后提交
|
||||||
|
- **THEN** 前端 MUST 校验各账单核销金额大于 0 且不超过账单未核销余额
|
||||||
|
- **AND** MUST 以 `allocations: [{ bill_id, amount }]` 提交账单分摊明细
|
||||||
|
|
||||||
|
#### Scenario: 付款金额小于核销合计
|
||||||
|
- **GIVEN** 用户已录入各账单核销金额
|
||||||
|
- **WHEN** 其填写的付款金额小于核销合计即提交
|
||||||
|
- **THEN** 前端 MUST 阻止提交并提示付款金额不能小于核销合计
|
||||||
|
|
||||||
|
#### Scenario: 缺少付款凭证
|
||||||
|
- **GIVEN** 用户已选择账单与收款方式
|
||||||
|
- **WHEN** 其未上传任何付款凭证即提交
|
||||||
|
- **THEN** 前端 MUST 阻止提交并提示上传付款凭证
|
||||||
|
|
||||||
|
#### Scenario: 缺少付款事实
|
||||||
|
- **GIVEN** 用户已选择账单与收款方式
|
||||||
|
- **WHEN** 其未填写付款方名称、付款时间或外部交易流水号即提交
|
||||||
|
- **THEN** 前端 MUST 阻止提交并提示补齐必填项
|
||||||
|
|
||||||
|
#### Scenario: 超管代办未填写原因
|
||||||
|
- **GIVEN** 超级管理员以代办身份创建申请
|
||||||
|
- **WHEN** 其未填写 `acting_reason` 即提交
|
||||||
|
- **THEN** 前端 MUST 阻止提交并提示填写代办原因
|
||||||
|
|
||||||
|
#### Scenario: 企微审批场景未配置
|
||||||
|
- **GIVEN** 企业微信审批场景尚未配置
|
||||||
|
- **WHEN** 用户提交核销申请
|
||||||
|
- **THEN** 前端 MUST 展示后端返回的 503 提示「企微审批场景未配置,请联系管理员」
|
||||||
|
- **AND** MUST 保留用户已填写的内容且不产生申请数据
|
||||||
|
|
||||||
|
### Requirement: 核销申请列表与详情
|
||||||
|
核销申请列表响应 MUST 为 `{ items, page, size, total }`,MUST 支持按状态、收款方式与创建时间筛选;列表项 MUST 包含 `payment_method_name`、`paid_amount`、`status`、`status_name` 与 `created_at`,状态 MUST 优先展示后端 `status_name`。申请详情响应 MUST 为 `{ application, allocations, attempts }`;`attempts` MUST 按提交顺序展示全部审批尝试记录,包含付款金额、付款方、流水号、付款凭证与审批意见。
|
||||||
|
|
||||||
|
#### Scenario: 查看审批历史
|
||||||
|
- **GIVEN** 申请存在多次审批尝试记录
|
||||||
|
- **WHEN** 用户打开申请详情
|
||||||
|
- **THEN** 详情 MUST 按提交顺序展示每次尝试的提交材料与审批状态
|
||||||
|
- **AND** 历史材料 MUST NOT 因重新提交而被覆盖
|
||||||
|
|
||||||
|
### Requirement: 核销申请重新提交
|
||||||
|
仅已驳回(`status` 为 `2`)的申请 MUST 允许修改并重新提交;重新提交 MUST 生成新的企业微信审批实例,且历史审批记录 MUST NOT 被覆盖。重新提交入口 MUST 位于核销申请列表的操作列,详情页 MUST 只读且 MUST NOT 展示业务操作按钮(仅保留返回导航)。
|
||||||
|
|
||||||
|
#### Scenario: 重新提交被驳回申请
|
||||||
|
- **GIVEN** 申请状态为已驳回
|
||||||
|
- **WHEN** 用户在核销申请列表点击「修改并重新提交」并修改账单分摊、收款方式、付款事实或付款凭证后提交
|
||||||
|
- **THEN** 前端 MUST 调用修改接口重新提交
|
||||||
|
- **AND** 成功后 MUST 刷新详情并展示新的审批实例状态
|
||||||
|
|
||||||
|
#### Scenario: 非驳回申请不可修改
|
||||||
|
- **GIVEN** 申请处于审批中或已通过
|
||||||
|
- **WHEN** 用户查看核销申请列表
|
||||||
|
- **THEN** 修改并重新提交入口 MUST 不可见或不可用
|
||||||
|
|
||||||
|
#### Scenario: 详情页只读
|
||||||
|
- **GIVEN** 用户打开账单详情或核销申请详情
|
||||||
|
- **WHEN** 页面渲染完成
|
||||||
|
- **THEN** 页面 MUST NOT 展示创建核销申请、关闭账单或修改并重新提交等业务操作按钮
|
||||||
|
- **AND** MUST 只保留返回导航
|
||||||
|
|
||||||
|
### Requirement: 企业微信审批场景配置
|
||||||
|
企业微信审批场景 MUST 支持 `employee_collection_approval` 业务类型,复用既有的应用列表、模板控件同步、业务字段查询与字段映射保存接口。
|
||||||
|
|
||||||
|
#### Scenario: 配置员工代收款审批场景
|
||||||
|
- **GIVEN** 超级管理员打开企微审批场景页面
|
||||||
|
- **WHEN** 其选择业务类型「员工代收款审批」
|
||||||
|
- **THEN** 前端 MUST 使用 `employee_collection_approval` 调用模板同步、字段查询与保存接口
|
||||||
|
|
||||||
|
### Requirement: 附件预签名展示
|
||||||
|
附件接口 MUST 只返回对象存储 Key(`payment_voucher_keys`);前端 MUST 通过系统既有的预签名下载接口获取实际访问地址后再展示。
|
||||||
|
|
||||||
|
#### Scenario: 查看付款凭证
|
||||||
|
- **GIVEN** 申请包含付款凭证 Key
|
||||||
|
- **WHEN** 用户点击查看付款凭证
|
||||||
|
- **THEN** 前端 MUST 先批量换取预签名地址
|
||||||
|
- **AND** 图片 MUST 支持预览,非图片 MUST 支持查看或下载
|
||||||
|
|
||||||
|
### Requirement: 订单付款凭证规则调整
|
||||||
|
由平台账号(`user_type` 为 1 或 2)操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单;该场景 `payment_voucher_key` MUST 变为非必填,付款凭证改在核销申请中提交,订单字段结构 MUST 保持不变。其余线下订单 MUST 继续要求付款凭证。
|
||||||
|
|
||||||
|
#### Scenario: 线下订单生成代收款账单
|
||||||
|
- **GIVEN** 当前登录账号为平台账号,所选套餐非赠送且实际收款金额大于 0,支付方式为线下支付
|
||||||
|
- **WHEN** 用户创建该订单
|
||||||
|
- **THEN** 前端 MUST 不再强制要求上传付款凭证
|
||||||
|
- **AND** MUST 提示付款凭证将在核销申请中提交
|
||||||
|
|
||||||
|
#### Scenario: 赠送套餐或非平台账号的线下订单
|
||||||
|
- **GIVEN** 所选套餐为赠送套餐,或当前账号非平台账号,或实际收款金额为 0
|
||||||
|
- **WHEN** 用户以线下支付方式创建订单
|
||||||
|
- **THEN** 前端 MUST 继续要求上传付款凭证
|
||||||
48
openspec/changes/add-employee-collection/tasks.md
Normal file
48
openspec/changes/add-employee-collection/tasks.md
Normal 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 实现创建/重新提交弹窗:选择 1~N 张可核销账单、按账单录入分摊金额、填写付款金额/付款方名称/付款时间/外部交易流水号、选择收款方式并上传付款凭证。
|
||||||
|
- [x] 5.3 超管代办时必须填写 `acting_reason`,否则禁止提交。
|
||||||
|
- [x] 5.4 实现申请详情,展示申请信息、分摊账单、付款凭证与全部审批尝试记录;历史记录只读,不被重新提交覆盖。
|
||||||
|
- [x] 5.5 仅已驳回申请展示「修改并重新提交」,提交成功后生成新的审批实例并刷新详情。
|
||||||
|
- [x] 5.6 统一处理 503「企微审批场景未配置」等业务错误,保留用户已填内容。
|
||||||
|
|
||||||
|
## 6. WeCom Scene and Order Rule
|
||||||
|
|
||||||
|
- [x] 6.1 扩展 `WecomBusinessType` 增加 `employee_collection_approval`,并在企微审批场景页面新增可选业务类型。
|
||||||
|
- [x] 6.2 场景保存复用既有 `inspectTemplate`、`getBusinessFields`、`saveScene` 接口,无需新增企微接口。
|
||||||
|
- [x] 6.3 调整线下订单创建:平台账号操作、非赠送且实际收款金额大于 0 时 `payment_voucher_key` 非必填,字段结构不变。
|
||||||
|
|
||||||
|
## 7. Verification
|
||||||
|
|
||||||
|
- [x] 7.1 运行 `npm run build`(含 `vue-tsc --noEmit`)与 `npm run check:encoding`,确保类型与编码通过。
|
||||||
|
- [x] 7.2 校验普通员工与超管的菜单、列与操作可见性差异。 — 已核验:应用/账单/收款方式页 v-permission 按钮、bills 关闭操作 hasAuth(billClose)+canCloseBill、表单 isActing=isSuperAdmin 控制代办字段
|
||||||
|
- [x] 7.3 校验金额分/元转换、附件预签名展示与 503 错误兜底。 — 已核验:ApplicationFormDialog 用 fenToYuan/yuanToFen;detail.vue 用 PaymentVoucherDialog 预览凭证;getErrorMessage 提取后端 msg(含 503 未配置场景)兜底
|
||||||
41
openspec/changes/add-gateway-rate-limit-management/design.md
Normal file
41
openspec/changes/add-gateway-rate-limit-management/design.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
项目使用 Vue 3、TypeScript、Element Plus 和 BaseService。后端控制菜单模式下,超级管理员直接使用全部静态路由,普通用户使用授权菜单;按钮控制复用现有 `useAuth`,身份复用 `userStore.isSuperAdmin`。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
- Goals: 完整接入文档声明的 API、可操作的管理页面、可靠表单校验、清晰权限配置交付。
|
||||||
|
- Non-Goals: 不实现后端限流,不改变旧轮询/停复机行为,不部署,不猜测统计指标字段。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 四页统一归属「轮询管理」,不新增一级菜单:
|
||||||
|
|
||||||
|
| 页面 | URL | 父菜单 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 停复机记录 | `/polling-management/stop-resume-records` | 轮询管理 `/polling-management` |
|
||||||
|
| 上游错误分类 | `/polling-management/error-classifications` | 轮询管理 `/polling-management` |
|
||||||
|
| 网关限流组 | `/polling-management/rate-limit-groups` | 轮询管理 `/polling-management` |
|
||||||
|
| 网关调用统计 | `/polling-management/gateway-stats` | 轮询管理 `/polling-management` |
|
||||||
|
| 网关限流组详情(隐藏路由) | `/polling-management/rate-limit-groups/detail/:id` | 轮询管理 `/polling-management` |
|
||||||
|
|
||||||
|
- 页面权限分别为 `card_stop_resume_record:page`、`gateway_error_classification:page`、`gateway_rate_limit_group:page`、`gateway_call_stats:page`,隐藏详情路由使用 `gateway_rate_limit_group:detail_page`。普通操作权限为 `gateway_error_classification:test`、`gateway_rate_limit_group:detail`、`gateway_call_stats:trend`;单卡入口按资产类型使用 `iot_info:stop_resume_records` / `device_info:stop_resume_records`。这些是前端配置约定,不声称来自后端接口文档;详情页与按钮使用独立编码,避免菜单与按钮编码冲突。
|
||||||
|
- 超级管理员维护入口不定义供普通账号分配的增删改权限;UI 与事件处理双重身份检查,后端继续独立鉴权。
|
||||||
|
- 提取专用新模块承载网关契约,避免覆盖旧 `/api/cards/{iccid}/suspend-resume-records` 接口及其不同 DTO。
|
||||||
|
- 枚举请求失败显式展示错误和重试,不把不可用枚举误报为空;未知值在只读展示中保留原值。
|
||||||
|
- 时间统一转换 RFC3339;全局记录查询必须有完整区间,单卡可使用后端默认;分页最大 100。
|
||||||
|
- 统计内部字段使用 `unknown`/记录值承载,只展示实际返回字段,空值不转成业务零;不从不明确字段推导图表或指标。
|
||||||
|
- 界面复用项目现有表格、搜索区、分页、弹窗、主题变量及状态标签;参考 UI/UX skill 的加载反馈、可访问标签、键盘焦点与响应式原则,不更换全站字体、颜色或新增表单依赖。
|
||||||
|
- 用户补充要求:列表严格复用店铺列表的 ArtSearchBar / ArtTableHeader / ArtTable / ArtTableFullScreen;详情使用店铺详情的返回、标题和双列描述表。时间展示复用同一个 `formatDateTime`,不改变请求与数据源;展开按钮仅在搜索条件数 >= 4 时出现。
|
||||||
|
- 运营商账户一律名称搜索下拉,复用 `CarrierService.getCarriers({ page: 1, page_size: 20, carrier_name })`,保留原账户名称回显,选中后仍提交 ID;趋势额外保留「无法解析账户的卡」的值 0。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 统计契约缺字段 → 接口和数据可查看,但正式业务趋势图需后端样例或完整 DTO 后再确定。
|
||||||
|
- 前端权限编码未在 API 文档定义 → 交付明确为菜单按钮配置编码,不替代后端鉴权。
|
||||||
|
- 31 天日期窗口和不同时区 → 按实际时间戳差值校验,测试边界与非法值。
|
||||||
|
- 用户已有未提交改动 → 不碰部署文件及 package 配置,不引入新依赖。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
服务契约测试覆盖所有 17 个接口;业务测试覆盖时间范围、QPS、优先级操作、超级管理员门禁、枚举值和统计未知字段。执行 `vue-tsc --noEmit`、Vite 构建、针对性 lint/格式/编码检查。没有真实后端响应时,明确区分本地 mock 验证与线上联调。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Change: 网关限流管理前端接入
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
`docs/网关限流.md` 新增 17 个管理端接口,当前前端缺少网关限流组、上游错误分类、全局停复机记录及按运营商统计的入口。需要完整接入已定义契约,并在交付时提供普通账号需要配置的菜单与按钮权限,不列出仅超级管理员的操作。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 在现有「轮询管理」下新增「停复机记录」「上游错误分类」「网关限流组」「网关调用统计」四个页面。
|
||||||
|
- 接入全部 17 个接口及类型;枚举通过 `GET /api/admin/polling-options` 获取,不把可配置枚举仅写死在前端。
|
||||||
|
- 停复机记录支持文档内筛选、分页、必填时间范围和 31 天跨度校验;单卡信息及设备绑定卡提供单卡历史入口,使用卡 ID 请求新接口。
|
||||||
|
- 分类规则支持分页筛选、测试现有规则和草稿;限流组支持分页筛选和详情。新建、修改、启停、删除均仅向超级管理员开放,并在操作函数中再次检查身份。
|
||||||
|
- 限流组表单校验 QPS、预留额度与高优先级操作的关系,删除和启停提供确认与防重复提交。
|
||||||
|
- 网关统计支持账户汇总及账户趋势查询,校验各接口时间参数。
|
||||||
|
- 接入路由别名、静态路由和现有后端菜单模式;使用 `xxx:xxx` 格式前端权限常量控制普通用户操作。
|
||||||
|
- 新增契约、参数校验、权限分支和交互回归测试,运行类型检查、构建及编码检查。
|
||||||
|
- 完工后输出菜单配置清单(名称、URL、父菜单)与按钮权限编码;清单仅保留非超级管理员专属项。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `gateway-rate-limit-management`(新增)。当前 `openspec list --specs` 无已归档规范;现有 `add-polling-priority-queue` 已完成,与本次新增页面并列,不修改其行为。
|
||||||
|
- Affected code: `src/types/api/`、`src/api/modules/`、`src/config/constants/`、`src/views/polling-management/`、`src/views/asset-management/asset-information/`、`src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`、`scripts/tests/`。
|
||||||
|
- 不修改网关后端限流算法、既有轮询调度、停复机执行动作;不自动创建数据库菜单,不推送 Git,不执行生产部署。
|
||||||
|
- 保留用户在 `.gitea/workflows/deploy.yaml` 与 `package.json` 的现有修改。
|
||||||
|
|
||||||
|
## Contract boundary
|
||||||
|
|
||||||
|
文档未提供统计接口 `carriers[]`、`points[]` 内部字段,仓库当前也没有 `docs/admin-openapi.yaml` 可补全。实现不得假定次数、失败率、时间戳等字段:先按实际响应提供可查看的动态字段表格/原始数据,并清楚提示契约未展开;有真实字段样例后才绘制业务趋势图。不得用虚构字段或零值冒充统计结果。
|
||||||
|
|
||||||
|
权限编码是前端配置约定,并非接口文档声明的后端权限;实际授权仍由后端控制。仅超级管理员限制必须依据现有 `userStore.isSuperAdmin`,不能通过配置普通按钮权限绕过。
|
||||||
|
|
||||||
|
用户于 2026-10-24 回复「直接执行」,已批准按此范围实施。
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 停复机记录查询
|
||||||
|
|
||||||
|
The frontend SHALL provide global stop/resume records and single-card history using the two documented admin APIs. Global queries MUST include RFC3339 `start_time` and `end_time` with an ordered range no greater than 31 days, all documented optional filters and pagination of at most 100 rows. Single-card history MUST use the card ID rather than ICCID or device ID.
|
||||||
|
|
||||||
|
#### Scenario: 查询全局记录
|
||||||
|
|
||||||
|
- **WHEN** 用户进入页面或修改筛选、页码
|
||||||
|
- **THEN** 前端 MUST 请求 `/api/admin/card-stop-resume-records`,展示记录字段及 `items/page/size/total`
|
||||||
|
- **AND** 不合法或缺失的时间区间 MUST 阻止请求并显示明确提示。
|
||||||
|
|
||||||
|
#### Scenario: 查看单卡历史
|
||||||
|
|
||||||
|
- **WHEN** 获授权用户从单卡或设备绑定卡打开历史
|
||||||
|
- **THEN** 前端 MUST 请求 `/api/admin/iot-cards/{id}/stop-resume-records`
|
||||||
|
- **AND** 无卡 ID 或无入口权限时 MUST 不请求接口,切换卡后不得残留另一张卡的结果。
|
||||||
|
|
||||||
|
### Requirement: 上游错误分类规则
|
||||||
|
|
||||||
|
The frontend SHALL implement rule listing, creation, modification, status change, deletion and non-persistent testing according to the documented fields. Rule maintenance MUST be restricted to super administrators. Testing SHALL support the current rule set and an optional valid draft, and display `matched/rule_id/category/category_name/keyword/maybe_accepted`.
|
||||||
|
|
||||||
|
#### Scenario: 普通账号测试分类
|
||||||
|
|
||||||
|
- **WHEN** 具有测试权限的用户提交操作与上游原始信息
|
||||||
|
- **THEN** 前端 MUST 调用 `/api/admin/gateway-error-classifications/test` 并展示实际命中结果
|
||||||
|
- **AND** 空信息或超过 1000 字符 MUST 不得提交,不通过增删改接口测试草稿。
|
||||||
|
|
||||||
|
#### Scenario: 超级管理员维护
|
||||||
|
|
||||||
|
- **WHEN** 超级管理员提交合法规则或确认启停、删除
|
||||||
|
- **THEN** 前端 MUST 使用对应 CRUD 或 status 接口,成功后刷新列表
|
||||||
|
- **AND** 普通账号 MUST 不显示维护入口且无法从处理函数发起维护请求。
|
||||||
|
|
||||||
|
### Requirement: 网关限流组
|
||||||
|
|
||||||
|
The frontend SHALL implement group listing, details, creation, modification, status change and deletion, including carrier/global and status filters. Maintenance MUST be restricted to super administrators. `max_qps` MUST be an integer in 1–10000, `reserved_qps` MUST be a nonnegative integer less than `max_qps`, operations MUST be nonempty, high-priority operations MUST be a subset and MUST be nonempty when reserved QPS is positive.
|
||||||
|
|
||||||
|
#### Scenario: 维护限流组
|
||||||
|
|
||||||
|
- **WHEN** 超级管理员提交限流组
|
||||||
|
- **THEN** 前端 MUST 校验文档的必填、长度、范围及操作关联约束后调用 POST 或 PUT
|
||||||
|
- **AND** 全局组 MUST 使用空账户值,不得错误提交为具体账户。
|
||||||
|
|
||||||
|
#### Scenario: 查询限流组
|
||||||
|
|
||||||
|
- **WHEN** 获授权用户查询列表或详情
|
||||||
|
- **THEN** 前端 MUST 分别调用 `/api/admin/polling-rate-limit-groups` 或 `/{id}` 并展示真实字段
|
||||||
|
- **AND** 普通账号 MUST 不能通过详情页进入维护操作。
|
||||||
|
|
||||||
|
### Requirement: 动态枚举与统计
|
||||||
|
|
||||||
|
The frontend SHALL load all seven enum collections from `/api/admin/polling-options`, and SHALL implement carrier summary and trend requests with documented parameter bounds. Undefined inner fields in `carriers` and `points` MUST be presented transparently from actual response data without invented metrics, silently substituted zeros or assumed chart series.
|
||||||
|
|
||||||
|
#### Scenario: 加载枚举
|
||||||
|
|
||||||
|
- **WHEN** 页面需要筛选、编辑或显示网关枚举
|
||||||
|
- **THEN** 前端 MUST 使用接口的 label/value,失败时提供错误反馈及重试
|
||||||
|
- **AND** 未知只读枚举值 MUST 保留原始值,不被显示为另一个已知状态。
|
||||||
|
|
||||||
|
#### Scenario: 查询汇总与趋势
|
||||||
|
|
||||||
|
- **WHEN** 用户选择合法的统计区间、运营商和聚合粒度
|
||||||
|
- **THEN** 前端 MUST 请求对应 `/api/admin/polling-stats/carriers` 或 `/{carrier_id}/trend`
|
||||||
|
- **AND** 汇总 minutes 为 1–60,趋势 minutes 不超过 10080,粒度为有效正整数,账户 0 MUST 可用
|
||||||
|
- **AND** 契约缺失的内部字段 MUST 以实际字段或原始数据展示并说明限制。
|
||||||
|
|
||||||
|
### Requirement: 菜单权限与交付
|
||||||
|
|
||||||
|
The frontend SHALL register four pages under existing polling management and honor the existing static/superadmin and backend-menu routing modes. Normal actions MUST use explicit `xxx:xxx` permissions. The handoff SHALL list menu name, URL, parent menu and button permission codes only for non-superadmin-exclusive configuration.
|
||||||
|
|
||||||
|
#### Scenario: 普通账号访问
|
||||||
|
|
||||||
|
- **WHEN** 普通账号获得相应菜单和按钮权限
|
||||||
|
- **THEN** 对应页面和普通查询、测试操作 MUST 可用
|
||||||
|
- **AND** 未授权按钮和请求 MUST 不可用,超级管理员专属维护能力 MUST 不被普通按钮权限开启。
|
||||||
|
|
||||||
|
#### Scenario: 交付配置清单
|
||||||
|
|
||||||
|
- **WHEN** 实现和验证结束
|
||||||
|
- **THEN** 交付 MUST 列出实际实现的普通菜单与按钮权限,不列出仅超级管理员专属配置
|
||||||
|
- **AND** MUST 披露未进行真实后端联调或统计字段不完整等验证限制。
|
||||||
|
|
||||||
|
### Requirement: 店铺页面一致性
|
||||||
|
|
||||||
|
The frontend SHALL reuse the shop list search, table header, column settings, fullscreen and pagination components, and the shop detail back button, title and responsive description layout. All displayed business timestamps MUST use the same `formatDateTime` formatter as the shop list. Search expansion MUST be enabled if and only if there are at least four search fields. Carrier selection MUST be a remote dropdown searched by carrier name using `/api/admin/carriers?page=1&page_size=20&carrier_name=...` while requests keep numeric IDs.
|
||||||
|
|
||||||
|
#### Scenario: 展开与时间显示
|
||||||
|
|
||||||
|
- **WHEN** 页面有少于 4 个搜索条件
|
||||||
|
- **THEN** `show-expand` MUST 为 false;4 个及以上 MUST 为 true
|
||||||
|
- **AND** 已知列表、详情及统计数据中的业务时间 MUST 展示为店铺列表相同格式,不直接显示 RFC3339 字符串。
|
||||||
|
|
||||||
|
#### Scenario: 按名称搜索运营商
|
||||||
|
|
||||||
|
- **WHEN** 用户在运营商账户下拉输入名称
|
||||||
|
- **THEN** 前端 MUST 请求 carriers 列表并携带 page=1、page_size=20、carrier_name
|
||||||
|
- **AND** 编辑回显 MUST 保留原账户名称,筛选/保存 MUST 提交所选 numeric carrier_id,趋势未解析账户 MUST 仍可选择值 0。
|
||||||
29
openspec/changes/add-gateway-rate-limit-management/tasks.md
Normal file
29
openspec/changes/add-gateway-rate-limit-management/tasks.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## 1. 契约和公共能力
|
||||||
|
|
||||||
|
- [x] 1.1 确认提案并记录审批。
|
||||||
|
- [x] 1.2 新增全部接口请求、响应类型和服务方法,统一导出,复用现有认证及错误处理。
|
||||||
|
- [x] 1.3 接入动态枚举、权限常量、分页与日期/限流参数校验工具。
|
||||||
|
|
||||||
|
## 2. 页面与入口
|
||||||
|
|
||||||
|
- [x] 2.1 新增停复机记录列表,支持全部筛选字段、分页、默认最近时间段和 31 天校验。
|
||||||
|
- [x] 2.2 单卡及设备绑定卡新增停复机历史入口,使用真实卡 ID,处理权限及卡切换状态。
|
||||||
|
- [x] 2.3 新增上游错误分类页,支持列表筛选、超级管理员维护及现有规则/草稿测试结果展示。
|
||||||
|
- [x] 2.4 新增网关限流组页,支持列表、详情、超级管理员维护及关联字段校验。
|
||||||
|
- [x] 2.5 新增网关调用统计页,支持汇总、趋势参数和未定义内部字段的透明展示。
|
||||||
|
- [x] 2.6 注册路由别名、四个菜单路由及隐藏详情路由、页面权限;复用现有后端菜单解析模式。
|
||||||
|
|
||||||
|
## 3. 验证与交付
|
||||||
|
|
||||||
|
- [x] 3.1 覆盖全部服务 URL、HTTP 方法、参数和响应的契约测试。
|
||||||
|
- [x] 3.2 覆盖时间、QPS、高优先级操作子集、权限和动态枚举回归测试。
|
||||||
|
- [x] 3.3 检查加载、错误、空数据、确认、重复提交和响应式显示。
|
||||||
|
- [x] 3.4 执行类型检查、构建、相关回归测试、格式与编码检查,记录实际结果。
|
||||||
|
- [x] 3.5 交付菜单名称/URL/父菜单和按钮权限编码,仅列非超级管理员专属配置。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 全量 Node 回归 153/153 通过,其中网关新增 23 项;`vue-tsc --noEmit`、生产构建、针对性 ESLint/Stylelint、格式和 UTF-8 检查通过。
|
||||||
|
- 本地 mock 浏览器验证:列表与详情、规则测试、按运营商名称搜索、账户 0 趋势、普通账号无维护按钮及授权隐藏详情路由;375px / 768px / 1024px / 1440px 检查与局部截图复核。
|
||||||
|
- 菜单与按钮清单写入 `docs/权限.md`,本地详细接入说明位于 `docs/网关限流-前端接入说明.md`(仓库默认忽略其他 docs 文件)。
|
||||||
|
- 未进行真实后端联调、Git 推送或生产部署;统计内部字段缺失时按实际响应展示。构建仍有项目既有常量绑定、循环依赖和大 chunk 警告。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Design: H5 运营弹窗配置管理(管理后台)
|
||||||
|
|
||||||
|
## 页面结构
|
||||||
|
|
||||||
|
- 设置管理下新增「H5运营弹窗配置」入口(仅超管/平台):
|
||||||
|
- 列表页 `src/views/settings/h5-popup-configuration/index.vue`
|
||||||
|
- 创建/编辑表单弹窗(复用 ArtForm + ArtSearchBar/ArtTable 模式)
|
||||||
|
- 详情以弹窗/抽屉展示完整配置
|
||||||
|
- 分页、筛选、表格沿用 ArtTable / ArtSearchBar / ArtTableHeader 与统一响应结构(`code/msg/timestamp/data`、分页 `items/page/size/total`)。
|
||||||
|
|
||||||
|
## 接口与类型
|
||||||
|
|
||||||
|
- 新增 `src/api/modules/h5PopupConfiguration.ts` 与 `src/types/api/h5PopupConfiguration.ts`:
|
||||||
|
- `GET /api/admin/h5-popup-configurations`(page/page_size/enabled,倒序分页)
|
||||||
|
- `POST /api/admin/h5-popup-configurations`(创建)
|
||||||
|
- `GET /api/admin/h5-popup-configurations/{id}`(详情)
|
||||||
|
- `PUT /api/admin/h5-popup-configurations/{id}`(更新,整表提交,版本 +1)
|
||||||
|
- `POST /api/admin/h5-popup-configurations/{id}/enable`、`{id}/disable`(请求体 `{ id }`,返回更新后配置)
|
||||||
|
- 枚举映射常量集中放置:`pages`(home/asset_detail/package_purchase/asset_wallet_recharge)、`frequency`(once/daily)、`action_type`(package_purchase/asset_wallet_recharge,空字符串 = 无)、`card_types`(CMCC/CUCC/CTCC/CBN)。
|
||||||
|
|
||||||
|
## 表单约定
|
||||||
|
|
||||||
|
- `pages` 多选必填(空数组非法);`title`(1–100)/`content`(1–2000)/`frequency`/起止时间必填。
|
||||||
|
- 范围三选器(店铺/设备类型/卡类型)空数组 = 全量,回显「全部」。
|
||||||
|
- 起止时间用 datetimerange,提交转 ISO 8601(`starts_at`/`ends_at`),`ends_at` 不得早于 `starts_at`。
|
||||||
|
- `priority` 数字输入(0–1000000);`enabled` 开关;`action_type` 下拉(含「无」选项,提交空字符串清除受控动作)。
|
||||||
|
- 编辑表单整表提交;编辑保存成功后提示「每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次」。
|
||||||
|
- 启用/停用为行操作(停用需二次确认),以接口返回的更新后配置刷新当前行。
|
||||||
|
|
||||||
|
## 权限与可见性
|
||||||
|
|
||||||
|
- 权限码:`h5_popup_configuration:list`(菜单/列表)、`:create`、`:update`、`:enable`、`:disable`。
|
||||||
|
- 路由 `meta.permissions` + 按钮 `v-permission`/`usePermission`,仅超管/平台可见;代理/企业/个人由后端 403 兜底并原文透传失败文案。
|
||||||
|
- 不提供删除入口(文档无 DELETE 接口)。
|
||||||
|
|
||||||
|
## 待确认
|
||||||
|
|
||||||
|
- 权限编码以后端菜单配置为准(前端默认 `h5_popup_configuration:list/create/update/enable/disable`)。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Change: 新增 H5 运营弹窗配置管理(管理后台)
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
H5 端(个人客户)需要在首页、资产详情、套餐购买、资产钱包充值等页面投放运营/风险弹窗,弹窗文案、命中页面、投放范围与生效规则需要后台配置。当前后台没有任何弹窗配置入口,运营只能走数据库操作。
|
||||||
|
|
||||||
|
本 Change 为管理后台新增「H5 运营弹窗配置」管理能力:配置标题与正文、命中页面、投放范围(店铺/设备类型/卡类型)、优先级、投放频率、生效时间与启停。
|
||||||
|
|
||||||
|
H5 端契约(`GET /api/c/v1/popup-candidates`、`POST /api/c/v1/risk-exchanges/{asset_id}/address`、通知列表/未读/已读接口)不在本 Change 范围,需求方已明确不处理 C 端。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 6 个后台接口(仅超管/平台,代理/企业/个人 403):
|
||||||
|
- `GET /api/admin/h5-popup-configurations`:配置列表,`page`(1–10000)/ `page_size`(1–100)/ `enabled` 筛选,按最近更新时间倒序分页返回 `items/page/size/total`。
|
||||||
|
- `POST /api/admin/h5-popup-configurations`:创建;`title`/`content`/`pages`/`frequency`/`starts_at`/`ends_at` 必填;`priority`/`enabled`/`action_type`/`shop_ids`/`device_types`/`card_types` 可选(范围集合空数组 = 全量)。
|
||||||
|
- `GET /api/admin/h5-popup-configurations/{id}`:详情(含 `version`、`frequency_text`、`enabled_text`、`creator`/`updater`)。
|
||||||
|
- `PUT /api/admin/h5-popup-configurations/{id}`:更新;成功即版本 +1,新版本可向原命中客户按频率重新投放一次,旧版本已投放通知的内容与快照不被改写;除 `id` 外全部字段可选,**不传保持原值**;`action_type` 传空字符串 = 清除受控动作;`shop_ids`/`device_types`/`card_types` 传空数组 = 改为全量;`pages` 传空数组非法(页面必选);`enabled` 不传保持原值(启停刷新最近更新时间);`ends_at` 不得早于 `starts_at`;`content` 1–2000 字符、`title` 1–100 字符、`priority` 0–1000000。
|
||||||
|
- `POST /api/admin/h5-popup-configurations/{id}/enable`:启用,参与候选匹配,刷新 `updated_at`(影响同优先级排序),返回更新后的配置。
|
||||||
|
- `POST /api/admin/h5-popup-configurations/{id}/disable`:停用,停止新投放,历史通知在展示期内仍可见;刷新 `updated_at`,返回更新后的配置。
|
||||||
|
- 新增「H5 运营弹窗配置」管理页(设置管理下,仅超管/平台可见):分页列表 + 创建/编辑表单 + 详情 + 行操作启用/停用。
|
||||||
|
- 前端契约约定:
|
||||||
|
- 范围集合 `shop_ids` / `device_types` / `card_types` 空数组 = 全量,列表与编辑回显「全部」。
|
||||||
|
- 枚举:`pages` = home / asset_detail / package_purchase / asset_wallet_recharge;`frequency` = once / daily;`action_type` = package_purchase / asset_wallet_recharge(空字符串 = 无受控动作);`card_types` = CMCC / CUCC / CTCC / CBN。
|
||||||
|
- 无「删除」接口,页面不提供删除入口;停用不清数据,列表保留历史配置。
|
||||||
|
- 编辑表单整表提交(所有字段都传,未传语义仅在部分更新时生效);范围清空传空数组、动作清除传空字符串、页面集合不可为空。
|
||||||
|
- 权限编码由前端确定:`h5_popup_configuration:list` / `:create` / `:update` / `:enable` / `:disable`。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs:
|
||||||
|
- `h5-popup-configuration-management`
|
||||||
|
- Affected code:
|
||||||
|
- `src/api/modules/h5PopupConfiguration.ts`(新增)
|
||||||
|
- `src/types/api/h5PopupConfiguration.ts`(新增)
|
||||||
|
- `src/api/modules/index.ts`、`src/types/api/index.ts`
|
||||||
|
- `src/config/constants/`(权限码常量)
|
||||||
|
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`(设置管理下新增菜单与路由)
|
||||||
|
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||||
|
- `src/views/settings/h5-popup-configuration/`(列表、表单弹窗、详情)
|
||||||
|
- Dependencies:
|
||||||
|
- 后端按 `docs/产品迭代8月份/通知.md` 提供上述 6 接口,Bearer JWT 鉴权,代理/企业/个人 403。
|
||||||
|
- Breaking changes:
|
||||||
|
- 无;全部为新增页面、接口模块与类型。
|
||||||
|
- 待确认:
|
||||||
|
- 权限编码(前端默认 `h5_popup_configuration:list/create/update/enable/disable`)以后端菜单配置为准。
|
||||||
|
- 列表分页上限与「按最近更新时间倒序」以文档为准,联调核对后端行为。
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: H5 运营弹窗配置列表
|
||||||
|
|
||||||
|
后台 MUST 提供 H5 运营弹窗配置的分页列表(GET /api/admin/h5-popup-configurations),支持 `page`(1–10000)/ `page_size`(1–100)/ `enabled` 筛选,按最近更新时间倒序返回 `items/page/size/total`;列表项 MUST 包含标题、命中页面、范围(店铺/设备类型/卡类型)、优先级、频率(`frequency_text`)、启停(`enabled_text`)、受控动作、生效起止时间、版本、创建/更新人与时间。
|
||||||
|
|
||||||
|
#### Scenario: 按启停状态筛选并分页
|
||||||
|
|
||||||
|
- **GIVEN** 超管/平台账号进入「H5运营弹窗配置」列表
|
||||||
|
- **WHEN** 以 page/page_size/enabled 发起查询
|
||||||
|
- **THEN** 前端 MUST 展示分页结果(后端按最近更新时间倒序)
|
||||||
|
- **AND** 切换 enabled 筛选后按新条件重新请求
|
||||||
|
|
||||||
|
#### Scenario: 范围空数组显示全部
|
||||||
|
|
||||||
|
- **GIVEN** 某条配置的 shop_ids/device_types/card_types 为空数组
|
||||||
|
- **THEN** 列表 MUST 在范围列展示「全部」
|
||||||
|
|
||||||
|
#### Scenario: 越权访问
|
||||||
|
|
||||||
|
- **GIVEN** 代理/企业/个人账号访问列表接口或直达路由
|
||||||
|
- **THEN** 后端 MUST 返回 403,前端 MUST NOT 渲染入口
|
||||||
|
- **AND** 失败文案 MUST 按后端返回原文透传
|
||||||
|
|
||||||
|
### Requirement: 创建弹窗配置
|
||||||
|
|
||||||
|
后台 MUST 支持创建 H5 运营弹窗配置(POST /api/admin/h5-popup-configurations),`title`/`content`/`pages`/`frequency`/`starts_at`/`ends_at` 必填;`priority`/`enabled`/`action_type`/`shop_ids`/`device_types`/`card_types` 可选,范围集合空数组 = 全量。
|
||||||
|
|
||||||
|
#### Scenario: 必填校验
|
||||||
|
|
||||||
|
- **GIVEN** 用户提交创建表单
|
||||||
|
- **WHEN** title/content/pages/frequency/starts_at/ends_at 任一缺失
|
||||||
|
- **THEN** 前端 MUST 拦截并提示必填,MUST NOT 发送请求
|
||||||
|
|
||||||
|
#### Scenario: 范围集合为空数组表示全量
|
||||||
|
|
||||||
|
- **GIVEN** 用户未选择店铺/设备类型/卡类型范围
|
||||||
|
- **THEN** 请求体对应数组 MUST 传空数组,后端按全量投放处理
|
||||||
|
|
||||||
|
### Requirement: 弹窗配置详情
|
||||||
|
|
||||||
|
后台 MUST 提供弹窗配置详情(GET /api/admin/h5-popup-configurations/{id}),返回完整配置含 `version`、`frequency_text`、`enabled_text`、`creator`/`updater`。
|
||||||
|
|
||||||
|
#### Scenario: 查看详情
|
||||||
|
|
||||||
|
- **GIVEN** 用户点击列表行「详情」
|
||||||
|
- **WHEN** 请求详情成功
|
||||||
|
- **THEN** 前端 MUST 展示完整配置要素(含版本与启停/频率中文名)
|
||||||
|
|
||||||
|
### Requirement: 更新弹窗配置
|
||||||
|
|
||||||
|
后台 MUST 支持更新弹窗配置(PUT /api/admin/h5-popup-configurations/{id}),成功即版本 +1;新版本可向原命中客户按频率重新投放一次,旧版本已投放通知的内容与快照不被改写。除 `id` 外所有字段均可选,**不传保持原值**;`action_type` 传空字符串 = 清除受控动作;`shop_ids`/`device_types`/`card_types` 传空数组 = 改为全量;`pages` 传空数组非法(页面必选);`enabled` 不传保持原值(启停刷新最近更新时间);`ends_at` 不得早于 `starts_at`;`content` 1–2000 字符、`title` 1–100 字符、`priority` 0–1000000。
|
||||||
|
|
||||||
|
#### Scenario: 更新成功版本递增并重新投放
|
||||||
|
|
||||||
|
- **GIVEN** 用户保存编辑
|
||||||
|
- **WHEN** 更新成功
|
||||||
|
- **THEN** 前端 MUST 提示「每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次」并刷新列表
|
||||||
|
- **AND** 列表/详情版本号较更新前 +1,旧版本已投放通知内容不被改写
|
||||||
|
|
||||||
|
#### Scenario: 范围清空表示改为全量
|
||||||
|
|
||||||
|
- **GIVEN** 编辑时清空某范围集合
|
||||||
|
- **THEN** 提交对应数组 MUST 为空数组,后端按全量处理
|
||||||
|
|
||||||
|
#### Scenario: 受控动作清除
|
||||||
|
|
||||||
|
- **GIVEN** 编辑时把 action_type 从 package_purchase 切换为「无」
|
||||||
|
- **THEN** 请求体 action_type MUST 传空字符串,后端清除受控动作
|
||||||
|
|
||||||
|
#### Scenario: 页面集合不得为空
|
||||||
|
|
||||||
|
- **GIVEN** 编辑时清空全部命中页面
|
||||||
|
- **THEN** 前端 MUST 拦截提示命中页面必选,MUST NOT 传空 pages 数组
|
||||||
|
|
||||||
|
#### Scenario: 未传字段保持原值
|
||||||
|
|
||||||
|
- **GIVEN** 请求体省略某字段(如 enabled)
|
||||||
|
- **THEN** 后端 MUST 保持原值;前端编辑表单整表提交,范围清空按空数组、动作清除按空字符串处理
|
||||||
|
|
||||||
|
### Requirement: 启用与停用弹窗配置
|
||||||
|
|
||||||
|
后台 MUST 支持启用/停用弹窗配置(POST /api/admin/h5-popup-configurations/{id}/enable、/{id}/disable,请求体 `{ id }`,返回更新后的配置),停用停止新投放且历史通知在展示期内仍可见,启用参与候选匹配;两者均刷新最近更新时间(影响同优先级排序)且不递增版本。
|
||||||
|
|
||||||
|
#### Scenario: 停用需二次确认
|
||||||
|
|
||||||
|
- **GIVEN** 用户点击某启用的配置「停用」
|
||||||
|
- **WHEN** 二次确认提交
|
||||||
|
- **THEN** 前端 MUST 调用 disable 并提示「停用后停止新投放,历史通知在展示期内仍可见」
|
||||||
|
- **AND** 以返回的更新后配置刷新当前行(状态为停用、最近更新时间更新、版本不变)
|
||||||
|
|
||||||
|
#### Scenario: 启用
|
||||||
|
|
||||||
|
- **GIVEN** 用户点击某停用的配置「启用」
|
||||||
|
- **THEN** 前端 MUST 调用 enable 并提示「启用后参与候选匹配」
|
||||||
|
- **AND** 以返回的更新后配置刷新当前行(状态为启用、最近更新时间更新、版本不变)
|
||||||
|
|
||||||
|
### Requirement: 角色权限与交互约定
|
||||||
|
|
||||||
|
弹窗配置入口 MUST 仅对超级管理员与平台账号可见;前端权限码固定为 `h5_popup_configuration:list`(菜单/列表)、`:create`、`:update`、`:enable`、`:disable`;页面 MUST NOT 提供删除入口(文档无 DELETE 接口);停用不清数据,列表保留历史配置。
|
||||||
|
|
||||||
|
#### Scenario: 入口按角色隐藏
|
||||||
|
|
||||||
|
- **GIVEN** 代理/企业/个人账号登录
|
||||||
|
- **WHEN** 系统渲染菜单
|
||||||
|
- **THEN** MUST NOT 展示「H5运营弹窗配置」菜单项
|
||||||
|
|
||||||
|
#### Scenario: 按钮权限码控制
|
||||||
|
|
||||||
|
- **GIVEN** 账号具备列表权限但缺 enable/disable 权限
|
||||||
|
- **THEN** 前端 MUST 只渲染具备权限的操作按钮
|
||||||
|
|
||||||
|
#### Scenario: 不提供删除入口
|
||||||
|
|
||||||
|
- **GIVEN** 用户查看列表或详情
|
||||||
|
- **THEN** 页面 MUST NOT 出现删除操作,仅提供启用/停用
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
## 1. Contract Confirmation
|
||||||
|
|
||||||
|
- [x] 1.1 权限编码前端确定:`h5_popup_configuration:list` / `:create` / `:update` / `:enable` / `:disable`(以后端菜单配置为准)。
|
||||||
|
- [x] 1.2 更新接口语义按文档:不传保持原值;`action_type` 空串 = 清除;范围空数组 = 改全量;`pages` 空数组非法;`enabled` 不传保持原值;`ends_at` 不得早于 `starts_at`;`content` 1–2000、`title` 1–100、`priority` 0–1000000;更新成功版本 +1 且旧版本通知不被改写。
|
||||||
|
- [x] 1.3 `enable`/`disable` 请求体 `{ id }`,返回更新后的 DtoH5PopupConfigurationResponse。
|
||||||
|
- [x] 1.4 列表 `page` 1–10000、`page_size` 1–100、`enabled` 筛选,按最近更新时间倒序分页。
|
||||||
|
|
||||||
|
## 2. API And Types
|
||||||
|
|
||||||
|
- [ ] 2.1 新增 `src/api/modules/h5PopupConfiguration.ts`:列表、创建、详情、更新、启用、停用 6 个接口。
|
||||||
|
- [ ] 2.2 新增 `src/types/api/h5PopupConfiguration.ts`:请求/响应 DTO(含 pages/frequency/action_type/card_types 枚举、范围集合、version、creator/updater 等;更新语义按「不传保持原值/空数组=全量/空串=清除」注释)。
|
||||||
|
- [ ] 2.3 在 `src/api/modules/index.ts`、`src/types/api/index.ts` 导出新模块/类型。
|
||||||
|
- [ ] 2.4 对齐统一响应 `code/msg/timestamp/data` 与分页 `items/page/size/total`。
|
||||||
|
|
||||||
|
## 3. List Page
|
||||||
|
|
||||||
|
- [ ] 3.1 设置管理下新增「H5运营弹窗配置」菜单与路由(仅超管/平台可见,权限码 `h5_popup_configuration:list`)。
|
||||||
|
- [ ] 3.2 列表:`page`/`page_size`/`enabled` 筛选,分页展示,按后端倒序。
|
||||||
|
- [ ] 3.3 列:标题、命中页面、范围(店铺/设备类型/卡类型,空数组显示「全部」)、优先级、频率、启停状态、受控动作、生效起止、版本、创建/更新人与时间。
|
||||||
|
- [ ] 3.4 行操作:详情、编辑、启用/停用(停用需二次确认);无删除入口。
|
||||||
|
|
||||||
|
## 4. Create / Edit / Detail
|
||||||
|
|
||||||
|
- [ ] 4.1 创建表单:`title`/`content`/`pages` 多选/`frequency`/起止时间必填校验(pages 非空);`priority`/`enabled`/`action_type`/范围可选。
|
||||||
|
- [ ] 4.2 范围集合 `shop_ids`/`device_types`/`card_types` 空数组 = 全量;编辑读回空数组显示「全部」。
|
||||||
|
- [ ] 4.3 编辑表单整表提交;保存成功提示「每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次」。
|
||||||
|
- [ ] 4.4 编辑支持 `action_type` 清空(「无」→ 空字符串)与 `ends_at` 不早于 `starts_at` 的前端校验。
|
||||||
|
- [ ] 4.5 详情(弹窗/抽屉)展示完整配置(含 version、frequency_text、enabled_text、creator/updater)。
|
||||||
|
|
||||||
|
## 5. Permissions And UX
|
||||||
|
|
||||||
|
- [ ] 5.1 按钮/入口按 `h5_popup_configuration:list/create/update/enable/disable` 接入 `usePermission`/`v-permission`,无权限不渲染。
|
||||||
|
- [ ] 5.2 固定提示:停用「停用后停止新投放,历史通知在展示期内仍可见」;启用「启用后参与候选匹配」;403 文案原文透传。
|
||||||
|
- [ ] 5.3 表单校验与请求失败提示稳定,不破坏列表渲染。
|
||||||
|
|
||||||
|
## 6. Verification
|
||||||
|
|
||||||
|
- [ ] 6.1 `vue-tsc --noEmit` 与新增文件 eslint 通过。
|
||||||
|
- [ ] 6.2 列表筛选/分页/倒序展示正确。
|
||||||
|
- [ ] 6.3 创建必填校验、范围全量语义、编辑版本 +1 提示正确。
|
||||||
|
- [ ] 6.4 启用/停用流程与提示正确;停用不清数据;启停不递增版本、刷新最近更新时间。
|
||||||
|
- [ ] 6.5 代理/企业/个人看不到入口;直达路由 403 文案透传。
|
||||||
|
- [ ] 6.6 联调:编辑整表提交后版本 +1、启停返回完整配置并刷新行;`ends_at` 早于 `starts_at` 被后端拒绝。
|
||||||
23
openspec/changes/add-iot-card-recall-line-input/proposal.md
Normal file
23
openspec/changes/add-iot-card-recall-line-input/proposal.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Change: 批量回收支持一行一个 ICCID
|
||||||
|
|
||||||
|
用户已确认并完成实施;验证结果见 `tasks.md`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
IoT 卡管理的批量回收目前只能从表格勾选 ICCID、输入号段范围或使用筛选条件。已有卡号清单的用户需要直接粘贴每行一个 ICCID,并复用现有 ICCID 列表的回收接口。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 在“批量回收”弹窗的选卡方式中增加“一行一个ICCID”,选择后显示带标签和输入提示的多行文本框,沿用现有 Element Plus 表单样式。
|
||||||
|
- 按换行拆分输入,去除每行首尾空白并忽略空行;ICCID 始终按字符串处理,不作数字转换。不增加额外的卡号长度或格式限制。
|
||||||
|
- 提交时仍调用 `POST /api/admin/iot-cards/standalone/recall`,使用 `selection_type: "list"` 和 `iccids: string[]`,不向接口发送新的选卡方式或文本框字段。
|
||||||
|
- 没有有效输入时展示校验提示,不提交请求;切换方式后按当前方式构建参数,关闭再打开时清空手工输入。
|
||||||
|
- 取消批量回收入口必须勾选表格的限制。有勾选时仍默认“ICCID列表”,无勾选时默认“一行一个ICCID”;原有列表模式仍校验必须有勾选项。
|
||||||
|
- 保持号段范围、筛选条件、权限、备注、结果展示和刷新逻辑不变;不修改其他批量操作。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `iot-card-management`
|
||||||
|
- Affected code: `src/views/asset-management/iot-card-management/index.vue`
|
||||||
|
- Backend impact: 无;复用 `CardService.recallStandaloneCards` 及其现有列表请求结构。
|
||||||
|
- Related changes: `update-batch-range-count-hints-formatting`(保留其号段数量提示)。
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: IoT Card Batch Recall Line Input
|
||||||
|
|
||||||
|
The IoT card batch recall dialog SHALL offer “一行一个ICCID” as an additional frontend-only selection mode alongside the existing ICCID list, range and filter modes.
|
||||||
|
|
||||||
|
The new mode MUST show a labeled multiline input with a hint to enter one ICCID per line. It MUST split LF and CRLF line endings, trim each line, ignore empty lines and preserve each ICCID as a string without numeric conversion or additional length or format restrictions.
|
||||||
|
|
||||||
|
Submitting this mode MUST call the existing `POST /api/admin/iot-cards/standalone/recall` endpoint with `selection_type: "list"`, the entered values in `iccids`, and the existing optional remark. It MUST NOT include selected table rows, range fields, filter fields or frontend-only fields in that request.
|
||||||
|
|
||||||
|
#### Scenario: Submit a pasted ICCID list
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择“一行一个ICCID”并输入多个 ICCID,行首尾存在空白且内容中包含空行
|
||||||
|
- **WHEN** 用户点击“确认回收”
|
||||||
|
- **THEN** 系统 MUST 去除首尾空白和空行,将剩余各行作为字符串放入 `iccids`
|
||||||
|
- **AND** 系统 MUST 使用原有列表模式的接口和 `selection_type: "list"` 提交
|
||||||
|
- **AND** 系统 MUST 沿用现有成功数、失败项及列表刷新流程
|
||||||
|
|
||||||
|
#### Scenario: Reject empty manual input
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择“一行一个ICCID”
|
||||||
|
- **WHEN** 输入为空或仅包含空白和空行并尝试提交
|
||||||
|
- **THEN** 系统 MUST 显示请输入 ICCID 的校验提示
|
||||||
|
- **AND** 系统 MUST NOT 发送回收请求
|
||||||
|
|
||||||
|
#### Scenario: Keep each selection mode isolated
|
||||||
|
|
||||||
|
- **GIVEN** 用户在逐行输入与现有列表、号段或筛选方式之间切换
|
||||||
|
- **WHEN** 用户提交当前选择方式
|
||||||
|
- **THEN** 系统 MUST 仅根据当前方式构建请求参数
|
||||||
|
- **AND** 原有列表、号段和筛选方式 MUST 保持既有行为及号段数量提示
|
||||||
|
|
||||||
|
### Requirement: IoT Card Batch Recall Manual Input Entry
|
||||||
|
|
||||||
|
The batch recall action SHALL remain permission-controlled and be available without selected table rows so users can enter ICCIDs directly. When opening the dialog, it MUST default to the existing list mode if rows are selected and to line input mode otherwise. The existing list mode MUST still reject submission without selected cards. Manual input MUST be cleared when the dialog is reopened.
|
||||||
|
|
||||||
|
#### Scenario: Open batch recall without selected rows
|
||||||
|
|
||||||
|
- **GIVEN** 用户拥有 `iot_card:batch_recycle` 权限且未勾选表格行
|
||||||
|
- **WHEN** 用户点击“批量回收”
|
||||||
|
- **THEN** 系统 MUST 打开弹窗并默认选择“一行一个ICCID”
|
||||||
|
- **AND** 用户 MUST 能直接粘贴卡号,而无需先勾选无关卡片
|
||||||
|
|
||||||
|
#### Scenario: Preserve the selected-row workflow
|
||||||
|
|
||||||
|
- **GIVEN** 用户已在表格勾选卡片
|
||||||
|
- **WHEN** 用户打开“批量回收”弹窗
|
||||||
|
- **THEN** 系统 MUST 默认选择现有“ICCID列表”方式
|
||||||
|
- **AND** 用户按列表方式提交时 MUST 继续使用已勾选卡片的 ICCID
|
||||||
|
|
||||||
|
#### Scenario: Clear manual input when reopening
|
||||||
|
|
||||||
|
- **GIVEN** 用户在“一行一个ICCID”方式下输入过卡号并关闭弹窗
|
||||||
|
- **WHEN** 用户再次打开“批量回收”弹窗
|
||||||
|
- **THEN** 手工输入框 MUST 为空,不得沿用上一次回收的输入
|
||||||
31
openspec/changes/add-iot-card-recall-line-input/tasks.md
Normal file
31
openspec/changes/add-iot-card-recall-line-input/tasks.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## 1. Specification
|
||||||
|
|
||||||
|
- [x] 1.1 核对批量回收入口、选卡方式和现有列表模式接口。
|
||||||
|
- [x] 1.2 明确逐行输入、空值校验、接口复用和既有模式兼容场景。
|
||||||
|
- [x] 1.3 获得用户对提案的确认后开始实施(用户回复“执行”)。
|
||||||
|
|
||||||
|
## 2. Implementation
|
||||||
|
|
||||||
|
- [x] 2.1 新增仅用于前端的逐行输入选项和多行文本框,保留后端选卡方式枚举。
|
||||||
|
- [x] 2.2 实现按行解析、非空校验及 `selection_type: "list"` 的请求参数构建。
|
||||||
|
- [x] 2.3 调整无勾选时的入口和默认方式,并确保弹窗重开清空输入。
|
||||||
|
- [x] 2.4 保持既有列表、号段、筛选、权限及操作结果流程不变。
|
||||||
|
|
||||||
|
## 3. Validation
|
||||||
|
|
||||||
|
- [x] 3.1 运行 `openspec validate add-iot-card-recall-line-input --strict`(通过)。
|
||||||
|
- [x] 3.2 检查多行输入(LF / CRLF)、首尾空白、空行、全空输入和长 ICCID 字符串处理。
|
||||||
|
- [x] 3.3 检查模式切换、无勾选打开、有勾选默认列表、关闭重开及接口参数隔离。
|
||||||
|
- [x] 3.4 运行相关文件代码检查和项目类型检查,记录结果。
|
||||||
|
|
||||||
|
## 4. Verification results
|
||||||
|
|
||||||
|
- `node --test scripts/tests/iotCardRecallLineInput.test.mjs`:11 项通过;执行真实 Vue 组件 setup、校验规则和提交函数,并使用 mock 请求验证原有回收 API 的路径及参数,无真实回收操作。
|
||||||
|
- `node --import ./scripts/tests/register-esbuild.mjs --test scripts/tests/*.test.mjs`:项目全部 141 项测试通过(PowerShell 中先枚举测试文件再传入)。
|
||||||
|
- `pnpm exec eslint src/views/asset-management/iot-card-management/index.vue scripts/tests/iotCardRecallLineInput.test.mjs`:通过。
|
||||||
|
- `pnpm exec prettier --check src/views/asset-management/iot-card-management/index.vue scripts/tests/iotCardRecallLineInput.test.mjs`:通过。
|
||||||
|
- `node scripts/check-encoding.mjs src/views/asset-management/iot-card-management/index.vue scripts/tests/iotCardRecallLineInput.test.mjs`:通过。
|
||||||
|
- `pnpm exec vue-tsc --noEmit`:通过。
|
||||||
|
- `node scripts/tests/verifyBuild.mjs`:生产构建通过,输出到临时目录并保留生成声明文件;其他页面仍有 `searchForm` 常量赋值、循环依赖及大 chunk 警告,本次未修改这些页面。
|
||||||
|
- `openspec validate add-iot-card-recall-line-input --strict`、`git diff --check`:通过。
|
||||||
|
- 验证范围:本地自动化及构建检查,未执行真实后端回收或浏览器人工验收。工作区另有 `src/types/components.d.ts` 的生成变动,本次未回退或覆盖。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Design: 新增运营报表模块并迁移时间筛选参数
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- 接口文档 `docs/产品迭代8月份/前端接口简版汇总.md` 只给出指标中文名,未给出 JSON 字段名;`默认模块.openapi.json` 不包含 `operations-reports`。
|
||||||
|
- 项目已有 ECharts 封装(`useChart` / `ArtLineChart` / `ArtBarChart`)与导出任务轮询(`useAsyncTaskPolling` + `ExportTaskService`),应复用而非新建。
|
||||||
|
- 其余列表的时间参数迁移属于参数重命名,不改变页面交互。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
- Goals: 报表页可用、参数迁移后筛选生效、导出复用既有异步任务链路。
|
||||||
|
- Non-Goals: 不改动后端、不新增后端字段定义;不重构既有导出任务中心。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 指标字段以「防御性可选类型 + 集中列配置」实现
|
||||||
|
|
||||||
|
由于接口未提供字段名,`operationsReport.ts` 的指标字段统一声明为可选,并保留 `[key: string]: unknown` 索引签名;所有列定义集中在 `src/views/operations-reports/constants.ts`。后端字段名如与推断不同,只需改一处常量即可,不改模板与请求逻辑。
|
||||||
|
|
||||||
|
- 比率/卡均类指标:`number | null`,`null` 渲染为 `-`,否则固定两位小数。
|
||||||
|
- 汇总响应的 `totals` 用于「全部」分组与合计行;`has_snapshot=false` 时展示空态。
|
||||||
|
|
||||||
|
### 趋势图使用项目既有 ECharts 体系
|
||||||
|
|
||||||
|
`OperationsTrendChart.vue` 基于 `useChart` 初始化,props 接收 `categories` 与多 `series`(含 `type`/`yAxisIndex` 以支持比率走次坐标轴),`watch` 深度监听后 `setOption`,卸载时 dispose。
|
||||||
|
|
||||||
|
### 导出复用异步任务轮询
|
||||||
|
|
||||||
|
导出接口仅返回任务信息(`task_id/task_no/status/...`)。页面调用 `useAsyncTaskPolling`(`fetchTask` = `ExportTaskService.getExportTaskDetail`)轮询,终态后提供下载;403 透传为无权限提示。每个页面使用独立 `storageKey` 避免互相串扰。
|
||||||
|
|
||||||
|
### 路由与访问控制
|
||||||
|
|
||||||
|
在 `asyncRoutes` 新增顶层 `/operations-reports`(`component: RoutesAlias.Home`)与两个子路由;`meta.allowedUserTypes` 限制为超管/平台(沿用现有权限门禁机制)。菜单标题使用中文字面量,与现有静态业务页保持一致。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 指标字段名未经后端确认 → 通过集中常量降低风险,联调时单点修正。
|
||||||
|
- 报表页可能返回大量行 → 首版不做虚拟滚动,依赖后端分页/汇总;后续按需优化。
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Change: 新增运营报表模块并迁移时间筛选参数
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
后端新增「运营报表」接口(设备激活汇总/趋势/导出、套餐续费汇总/趋势/导出),前端目前既没有对应 API 封装也没有任何页面。同时,多个列表接口的时间筛选参数统一变更为 `start_time`/`end_time`(RFC3339 带时区、闭区间),旧参数(`created_at_start/end`、`expires_from/to`、`start_date/end_date`)将返回 `1001`,前端必须同步迁移,否则相关页面筛选会失效。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 `OperationsReportsService` API 模块及对应类型定义,封装 4 个查询接口与 2 个导出接口。
|
||||||
|
- 新增顶层菜单「运营报表」,含「设备激活报表」「套餐续费报表」两个子页;每页提供时间/分组筛选、汇总表格、趋势图与异步导出。
|
||||||
|
- 运营报表仅超级管理员/平台账号可访问,其他账号返回 `403`。
|
||||||
|
- 换货单列表、资产分配记录、代理充值订单、临期资产列表时间筛选参数迁移为 `start_time`/`end_time`。
|
||||||
|
- 新增临期资产导出接口 `POST /api/admin/expiring-assets/export`(复用列表筛选参数 + `format: 'xlsx'`,异步任务)。
|
||||||
|
- 汇总比率、卡均保留两位小数,分母为零或无快照时显示 `-`;无快照时展示空态。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `operations-reports`(新增)、`time-filter-conventions`(新增)
|
||||||
|
- Affected code:
|
||||||
|
- `src/api/modules/operationsReports.ts`(新增)、`src/api/modules/index.ts`
|
||||||
|
- `src/types/api/operationsReport.ts`(新增)、`src/types/api/index.ts`
|
||||||
|
- `src/views/operations-reports/activation/index.vue`、`src/views/operations-reports/renewal/index.vue`、`src/views/operations-reports/components/OperationsTrendChart.vue`、`src/views/operations-reports/constants.ts`(新增)
|
||||||
|
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`
|
||||||
|
- `src/api/modules/exchange.ts`、`src/views/asset-management/exchange-management/index.vue`
|
||||||
|
- `src/types/api/card.ts`、`src/views/asset-management/record-management/asset-assign/index.vue`
|
||||||
|
- `src/types/api/agentRecharge.ts`、`src/views/finance/agent-recharge/index.vue`
|
||||||
|
- `src/types/api/asset.ts`、`src/api/modules/asset.ts`、`src/views/asset-management/expiring-assets/index.vue`
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 运营报表 API 服务
|
||||||
|
|
||||||
|
系统 SHALL 提供 `OperationsReportsService` 封装运营报表 6 个接口:
|
||||||
|
|
||||||
|
- 查询设备激活汇总 `GET /api/admin/operations-reports/activation-summary`
|
||||||
|
- 查询设备激活趋势 `GET /api/admin/operations-reports/activation-trend`
|
||||||
|
- 导出设备激活汇总 `POST /api/admin/operations-reports/activation-summary/export`
|
||||||
|
- 查询套餐续费汇总 `GET /api/admin/operations-reports/package-renewal-summary`
|
||||||
|
- 查询套餐续费趋势 `GET /api/admin/operations-reports/package-renewal-trend`
|
||||||
|
- 导出套餐续费汇总 `POST /api/admin/operations-reports/package-renewal-summary/export`
|
||||||
|
|
||||||
|
查询参数 SHALL 使用 `start_time`/`end_time`(RFC3339 秒级带时区)与 `group_by`;趋势查询额外支持 `granularity`(`day|month`,默认 `day`)。导出请求 SHALL 携带 `format`(`xlsx`)。
|
||||||
|
|
||||||
|
#### Scenario: 查询设备激活汇总
|
||||||
|
|
||||||
|
- **WHEN** 调用 `OperationsReportsService.getActivationSummary({ start_time, end_time, group_by })`
|
||||||
|
- **THEN** 返回包含 `group_by`、`group_name`、`has_snapshot`、`snapshot_dates`、`items`、`totals` 的响应
|
||||||
|
|
||||||
|
#### Scenario: 导出设备激活汇总
|
||||||
|
|
||||||
|
- **WHEN** 调用 `OperationsReportsService.exportActivationSummary({ format: 'xlsx', start_time, end_time, group_by })`
|
||||||
|
- **THEN** 返回包含 `task_id`、`task_no`、`status`、`status_name`、`message` 的异步任务信息
|
||||||
|
|
||||||
|
### Requirement: 运营报表页面
|
||||||
|
|
||||||
|
系统 SHALL 提供顶层菜单「运营报表」及两个子页「设备激活报表」「套餐续费报表」。每页 SHALL 包含时间范围与分组筛选、汇总表格、趋势图和导出按钮。
|
||||||
|
|
||||||
|
#### Scenario: 查询汇总并渲染表格
|
||||||
|
|
||||||
|
- **WHEN** 用户选择时间范围与分组后点击查询
|
||||||
|
- **THEN** 请求对应汇总接口并以表格展示各分组指标与合计行
|
||||||
|
|
||||||
|
#### Scenario: 渲染趋势图
|
||||||
|
|
||||||
|
- **WHEN** 汇总数据加载成功
|
||||||
|
- **THEN** 趋势图按 `granularity`(日/月)以时间点为横轴展示关键指标曲线
|
||||||
|
|
||||||
|
#### Scenario: 导出报表
|
||||||
|
|
||||||
|
- **WHEN** 用户点击导出并成功创建异步任务
|
||||||
|
- **THEN** 前端轮询任务状态,任务完成后可下载文件
|
||||||
|
|
||||||
|
### Requirement: 运营报表访问控制与空态
|
||||||
|
|
||||||
|
运营报表 SHALL 仅对超级管理员/平台账号开放,其他账号访问 SHALL 返回 `403`。比率与卡均指标 SHALL 保留两位小数;分母为零时返回 `null`,页面显示 `-`。无快照时 `has_snapshot=false`、`totals=null`、`items=[]`,页面 SHALL 展示空态而非报错。
|
||||||
|
|
||||||
|
#### Scenario: 无快照
|
||||||
|
|
||||||
|
- **WHEN** 汇总响应 `has_snapshot` 为 `false`
|
||||||
|
- **THEN** 页面展示空态
|
||||||
|
|
||||||
|
#### Scenario: 比率分母为零
|
||||||
|
|
||||||
|
- **WHEN** `activation_rate` 或 `renewal_rate` 为 `null`
|
||||||
|
- **THEN** 表格对应单元格显示 `-`
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 列表时间筛选参数统一
|
||||||
|
|
||||||
|
以下列表接口的时间筛选参数 SHALL 统一为 `start_time`/`end_time`(RFC3339 秒级、闭区间),前端不得再发送旧参数:
|
||||||
|
|
||||||
|
- 换货单列表 `GET /api/admin/exchanges`(由 `created_at_start`/`created_at_end` 迁移)
|
||||||
|
- 资产分配记录 `GET /api/admin/asset-allocation-records`(由 `created_at_start`/`created_at_end` 迁移)
|
||||||
|
- 代理充值订单 `GET /api/admin/agent-recharges`(由 `start_date`/`end_date` 迁移,取消按日自动补齐首尾秒)
|
||||||
|
- 临期资产列表 `GET /api/admin/expiring-assets`(由 `expires_from`/`expires_to` 迁移)
|
||||||
|
|
||||||
|
#### Scenario: 换货单列表时间筛选
|
||||||
|
|
||||||
|
- **WHEN** 用户选择创建时间范围并搜索
|
||||||
|
- **THEN** 请求参数为 `start_time`/`end_time`,且不包含 `created_at_start`/`created_at_end`
|
||||||
|
|
||||||
|
#### Scenario: 代理充值订单时间筛选
|
||||||
|
|
||||||
|
- **WHEN** 用户选择时间范围并搜索或导出
|
||||||
|
- **THEN** 请求参数为 `start_time`/`end_time`,且不包含 `start_date`/`end_date`
|
||||||
|
|
||||||
|
#### Scenario: 临期资产列表时间筛选
|
||||||
|
|
||||||
|
- **WHEN** 用户选择预计到期时间范围并搜索
|
||||||
|
- **THEN** 请求参数为 `start_time`/`end_time`,且不包含 `expires_from`/`expires_to`
|
||||||
|
|
||||||
|
### Requirement: 临期资产导出
|
||||||
|
|
||||||
|
系统 SHALL 提供 `POST /api/admin/expiring-assets/export`,复用临期资产列表筛选参数并携带 `format: 'xlsx'`,返回异步导出任务信息;前端 SHALL 通过现有导出任务接口轮询并在完成后下载。
|
||||||
|
|
||||||
|
#### Scenario: 导出临期资产
|
||||||
|
|
||||||
|
- **WHEN** 用户在临期资产页面点击导出
|
||||||
|
- **THEN** 创建异步导出任务并轮询状态,完成后可下载 xlsx 文件
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
## 1. 运营报表 API 与类型
|
||||||
|
|
||||||
|
- [x] 1.1 新增 `src/types/api/operationsReport.ts`(查询、汇总、趋势、导出任务类型)
|
||||||
|
- [x] 1.2 在 `src/types/api/index.ts` 导出新类型
|
||||||
|
- [x] 1.3 新增 `src/api/modules/operationsReports.ts`(6 个接口方法)
|
||||||
|
- [x] 1.4 在 `src/api/modules/index.ts` 注册 `OperationsReportsService`
|
||||||
|
|
||||||
|
## 2. 运营报表页面
|
||||||
|
|
||||||
|
- [x] 2.1 新增 `src/views/operations-reports/constants.ts`(分组选项与指标列配置)
|
||||||
|
- [x] 2.2 新增 `src/views/operations-reports/components/OperationsTrendChart.vue`(多系列趋势图)
|
||||||
|
- [x] 2.3 新增「设备激活报表」页面(筛选 + 汇总表格 + 趋势图 + 导出)
|
||||||
|
- [x] 2.4 新增「套餐续费报表」页面(筛选 + 汇总表格 + 趋势图 + 导出)
|
||||||
|
|
||||||
|
## 3. 路由与菜单
|
||||||
|
|
||||||
|
- [x] 3.1 `routesAlias.ts` 新增两个页面别名
|
||||||
|
- [x] 3.2 `asyncRoutes.ts` 新增顶层「运营报表」菜单(含权限限制)
|
||||||
|
|
||||||
|
## 4. 时间筛选参数迁移
|
||||||
|
|
||||||
|
- [x] 4.1 换货单列表 `created_at_start/end` -> `start_time/end_time`(类型 + 页面)
|
||||||
|
- [x] 4.2 资产分配记录 `created_at_start/end` -> `start_time/end_time`(类型 + 页面)
|
||||||
|
- [x] 4.3 代理充值订单 `start_date/end_date` -> `start_time/end_time`(类型 + 页面,含导出参数)
|
||||||
|
- [x] 4.4 临期资产列表 `expires_from/to` -> `start_time/end_time`(类型 + 页面)
|
||||||
|
|
||||||
|
## 5. 临期资产导出
|
||||||
|
|
||||||
|
- [x] 5.1 `AssetService.exportExpiringAssets`(POST /api/admin/expiring-assets/export)
|
||||||
|
- [x] 5.2 临期资产页面新增「导出」按钮(筛选参数 + `format: 'xlsx'` + 异步任务轮询下载)
|
||||||
|
|
||||||
|
## 6. 验证
|
||||||
|
|
||||||
|
- [x] 6.1 运行 `eslint`、`stylelint`、`vue-tsc --noEmit`、`vite build --mode production` 均通过
|
||||||
|
- [x] 6.2 运行 `openspec validate add-operations-reports-and-time-filters --strict` 通过
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Vue 3 / Element Plus 后台已有订单列表与详情、两处单项购买、套餐创建/分配/系列授权、资产当前/历史套餐、统一导出、企微审批与角色权限树。本次复用现有能力,延续店铺列表/详情标准、时间格式、条件 >=4 才展开和响应式。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 原订单列表接收受控分类 props,三个薄路由入口使用独立名称缓存,按分类选择服务与导出场景,不在 query 注入 category。旧列表继续 all;企业路由门禁与请求/处理函数都限制,代理 online 严格禁止,后端继续鉴权。
|
||||||
|
- 企业操作限制使用公共账号类型判断;不能仅改一个 v-if:覆盖基础信息、绑定卡、当前套餐、历史套餐及父页处理函数,保留 start/stop、历史与只读数据。
|
||||||
|
- 套餐商品默认天数、单次购买覆盖天数、延期天数分别校验,不能共享错误的 90 天上限;null/空与 0 区分;加油包白名单剔除沉默期字段。原实名生效条件保存为只读基线,禁止转换为购买生效。
|
||||||
|
- 下单前冻结目标、表单字段和幂等标识;权限取消或资产/套餐切换不发送旧覆盖值。后端决定实际是否适用及是否审批,不靠前端成功提示推定立即生效。
|
||||||
|
- silence 可选且不可从其他套餐复用。当前/历史套餐分别展示各自响应;后端不返回对象时不猜测未启用与不适用的区别,不请求资格接口。
|
||||||
|
- 首次购买显示使用 formatDateTime;查询与导出共享同一范围转换,ICCID/IMEI 保持字符串。世代与结束原因由服务端返回,记录 DTO 不明时仅完成字段/筛选,记录弹窗保持待补契约。
|
||||||
|
- 使用现有预签名上传、凭证、二维码与审批组件,不引入依赖。延期付款金额必须取 quote,不能在浏览器自行按 30 天计费;未知的金额单位与 evidence_refs 结构是实现前置条件。
|
||||||
|
- 对文档只列 URL、未列 DTO 的延期页面,先列覆盖清单与契约缺口,不擅自制作可写空壳或推断财务模型。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
2026-10-28 契约补齐:保存 Apifox 的 21 个后台操作为 `docs/api/package-silence.openapi.json`;全部金额为整数分,前端元输入精确转换。首次购买记录使用 `/api/admin/assets/{asset_type}/{id}/first-purchase-records`,流水/首次购买记录为数组;政策列表也为数组但支持 page/page_size,因此显示上一页/下一页而不伪造总数。政策适用系列/套餐使用既有名称搜索接口。
|
||||||
|
|
||||||
|
特殊延期 CSV 在前端按 `package_usage_id,days` 两列解析,再以明确的 items[] 提交,不猜后端 import_file_key 的未说明文件格式。二维码/线下订单同一快照重试复用 request_id;特殊接口不定义幂等字段,结果不确定时阻止重复提交并提示核对批次。免审批场景保留真实返回的字符串,提供文档明确的 offline_paid/batch;其他编码需后端提供,不猜完整枚举。
|
||||||
|
|
||||||
|
先记录当前 198 项基线与 dirty worktree;新增精确请求/角色/字段校验及组件行为测试,覆盖企业 5 项操作但保留停复机、三分类服务、导出 false/0、套餐天数边界、购买覆盖与原因、无权限不发字段、加油包剔除、首次购买时间与空值、未知错误和 0 元退款。运行全量 Node 测试、vue-tsc、受影响文件 lint/format/encoding、生产构建与针对性本地 mock;不把不完整 DTO 的自造响应作为后端验收。
|
||||||
|
|
||||||
|
## Rollout and risks
|
||||||
|
|
||||||
|
无数据库迁移,不自动开沉默期总开关,旧订单路由/导出与现有菜单配置保留。新菜单是否可见仍需后端菜单数据授权;清单在实施后按实际 URL/权限交付。真实支付与审批、资产范围和文件内容需测试环境联调。C 端为独立工程,仅提供同步清单。
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 执行记录:订单分类与沉默期
|
||||||
|
|
||||||
|
## 当前结论
|
||||||
|
|
||||||
|
本后台代码已按汇总文档及用户授权的 Apifox 定义补齐,原先因缺少 DTO 挂起的延期办理、政策、批次异常和记录弹窗已实现。未修改独立 C 端,未推送、部署或调用真实业务写入;不能把本地 mock 验证等同于线上验收。
|
||||||
|
|
||||||
|
## 契约来源与差异
|
||||||
|
|
||||||
|
21 个后台操作读取自用户授权的 Apifox 分享文档,留存无密码快照 [package-silence.openapi.json](../../../docs/api/package-silence.openapi.json)。本会话通过官方 Apifox MCP Server 0.0.18 的 --oas 模式验证文档读取,不代表直接实时连接整个私有项目;线上变更后需重新读取快照。
|
||||||
|
|
||||||
|
- 所有延期金额、单价均为整数分;元输入做精确整数转换,不能使用浮点金额直接提交。
|
||||||
|
- 首次购买记录真实路径为 `GET /api/admin/assets/{asset_type}/{id}/first-purchase-records`,不是汇总里带 package-silence 前缀的路径。
|
||||||
|
- 首次购买记录、沉默期流水为数组;政策列表也是数组且有 page/page_size,因此政策页不伪造 total。
|
||||||
|
- 更新政策与启停请求含 id;特殊收款时间是 offline_paid_at,线下订单是 paid_at。
|
||||||
|
- 两个沉默期导出场景依据 orders/usages 各自文档启用,filters 与列表同范围;通用导出接口对 query 的类型为字符串值,沿用 JSON 字符串 filters。之前三类普通订单仍保留汇总明确要求的对象 filters。
|
||||||
|
|
||||||
|
## 实现覆盖
|
||||||
|
|
||||||
|
| 范围 | 已实现 |
|
||||||
|
| --- | --- |
|
||||||
|
| 分类订单 | 平台/代理/在线三页、独立精确路径与 scene、旧列表详情及历史导出保留;代理禁止 online、企业禁止普通订单 |
|
||||||
|
| 企业资产限制 | 有效期、已用量、停用、轮询、实名状态五类入口和处理函数限制;授权资产停复机保持;未找到仍为普通未找到 |
|
||||||
|
| 套餐与购买 | 商品默认 1–90 天、加油包剔除、生效条件限制;单项/批量/系列分配写前校验;两处下单覆盖无 90 天上限并有权限及原因校验 |
|
||||||
|
| 资产沉默期 | 按可选 silence 展示真实状态/截止/累计;真实 usage ID 打开延期与流水;对象缺失隐藏 |
|
||||||
|
| 扫码延期 | 资格、后端报价、wechat/alipay 二维码、确认后创建、订单刷新;终态/错误/关闭停止刷新;不复制计价算法 |
|
||||||
|
| 线下登记 | 实收元转分、与报价一致、收款方式字典、交易单号、RFC3339 付款时间、1–5 张凭证、申请依据;待审批不显示已生效 |
|
||||||
|
| 特殊与批量 | 五类特殊延期、原因/依据、优惠价收款;CSV 两列转 items[],校验表头/整数/重复 ID;批量需 extend 和 extend_batch;展示真实批次与错误行 |
|
||||||
|
| 订单与批次 | 列表/详情/筛选、订单导出;批次详情逐行异常;重试/关闭需原因及确认,关闭提示可能原路退款 |
|
||||||
|
| 临期与过期 | expiring/expired/all 视图、默认 7 天、资产/店铺/套餐/截止范围及导出;延期与流水入口 |
|
||||||
|
| 延期政策 | 列表、详情加载、新增/全量编辑、启停、匹配、重叠错误;系列/套餐名称搜索;无 DELETE 不提供删除 |
|
||||||
|
| 首次购买 | 卡/设备时间列、RFC3339 筛选/导出、资产详情;列表记录入口和只读世代历史,展示来源/订单/结束原因;不伪造绑定卡或换货时间 |
|
||||||
|
| 配置与兼容 | 专用总开关/precheck/failed_items、通用配置只读、企微审批类型、六项真实权限识别、1260–1275 提示及零元退款 |
|
||||||
|
|
||||||
|
同一二维码/线下请求快照重试使用相同 request_id,不自动重发写请求。特殊延期没有文档定义的幂等参数,传输结果不确定时阻止再次提交并提示先核对批次;不得直接修改截止时间替代异常处理。
|
||||||
|
|
||||||
|
CSV 为本地解析的 `package_usage_id,days` 两列格式,经文档 items[] 提交,未猜测后端 import_file_key 的文件格式。政策免审批场景完整枚举未在 OpenAPI 列出:提供明确的 offline_paid、batch,并保留服务端现有值;其他编码由后端提供后填写,不根据中文自行造值。批次及明细未知状态原样显示,不推断“已完成”。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
当前全量 **236 项测试通过**(保留原 198 项,上一阶段 18 项,本阶段 20 项)。覆盖 21 个精确 HTTP 操作、金额精度与 0、CSV、报价失效、权限撤销/确认取消、幂等重试、迟到响应、线下凭证与审批、特殊结果不确定防重复、政策完整提交、异常 ID/原因、导出白名单与模板编译。
|
||||||
|
|
||||||
|
类型检查、生产构建、受影响代码 ESLint/Prettier/Stylelint、UTF-8 检查通过。生产构建仍有项目既有常量 v-model、API 循环依赖及大 chunk 警告,未在本轮扩大重构。
|
||||||
|
|
||||||
|
本地浏览器使用 `127.0.0.1:5190` 全 /api 拦截 mock,核验临期状态与格式化截止时间、31 天后端试算为 60 天、提交确认及订单刷新、政策编辑与 390px 窄屏、批次异常原因确认。上一阶段三类订单/角色门禁/套餐配置/总开关 mock 检查仍保留。没有请求真实支付或审批接口。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$tests = @(rg --files scripts/tests -g '*.test.mjs')
|
||||||
|
node --import ./scripts/tests/register-esbuild.mjs --test --test-reporter=spec $tests
|
||||||
|
.\node_modules\.bin\vue-tsc.cmd --noEmit
|
||||||
|
node scripts/tests/verifyBuild.mjs
|
||||||
|
node scripts/tests/verifyBuild.mjs --serve --order-silence
|
||||||
|
openspec validate add-order-segmentation-and-package-silence --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## 菜单、权限和 C 端
|
||||||
|
|
||||||
|
完整菜单/URL/父菜单与普通按钮编码见 [权限清单](../../../docs/权限.md#订单分类与沉默期2026-10-28)。不列仅超管专属设置,不写入服务器菜单或伪造权限 ID。
|
||||||
|
|
||||||
|
C 端需要另行接入:可选 silence 状态/截止与 extendable 入口;资格、后端报价、自助订单/qr_content、订单状态刷新、我的延期订单和流水;总开关关闭或对象缺失隐藏入口;金额为分、不能复制计费/审批规则。对应 `/api/c/v1/package-silence`,不复用后台 URL;资产归属鉴权交给后端。本仓库未修改 C 端代码。
|
||||||
|
|
||||||
|
真实测试环境仍需验收:数据范围与权限、实际商户支付及查单、企微通过/驳回、异常关闭退款、上传文件和导出下载内容。全部检查只证明前端实现及本地契约行为,不证明后端已部署兼容。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Change: 订单分类拆页与套餐沉默期后台接入
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
依据 `docs/2026-10-27-订单拆分至沉默期-前端修改汇总(1).md`,后台尚未接入订单分类列表、企业账号操作限制、套餐沉默期及延期、首次购买套餐时间。需要保留既有订单详情、付款/审批、套餐分配与资产功能,并区分前端实现和真实后端联调。
|
||||||
|
|
||||||
|
用户于 2026-10-28 明确要求“根据文档来写提案,然后执行”,本提案完成严格校验后按该授权实施。延续只修改本后台仓库的范围,不修改独立 H5/C 端,不推送或部署。工作区已有四个轮询修复文件必须保留。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 订单管理新增平台、代理商、在线支付三页,复用原列表及详情、筛选、创建能力;各自请求 `/orders/platform|agent|online`,不提交类别参数,导出分别使用 `order_platform|order_agent|order_online`。保留旧列表及历史 `order` 导出。代理隐藏在线支付页,企业隐藏订单菜单并阻止页面请求;不改变后端数据范围。
|
||||||
|
- 企业账号隐藏并在处理函数阻止资产改有效期、改已用量、停用、开关轮询、改实名状态五项操作,保留授权资产的停复机及读取。未找到不改写为无权限。
|
||||||
|
- 正式套餐新增默认沉默期(1–90 天)与付费延期开关;实名即生效必填,新建默认 30 天;购买即生效可选;加油包不显示、不提交。新增未配置筛选与导出参数、详情展示;明确生效条件不可从实名改成购买,购买改实名需同请求带天数。
|
||||||
|
- 后台两处单项下单入口增加有权限才可填写的 `silence_period_days`、`silence_reason`;购买覆盖天数仅最小 1、不套用套餐配置 90 天上限,天数存在时原因必填。保留后端审批结果,不将待审批说成已延期。
|
||||||
|
- 资产当前套餐、历史套餐按可选 `silence` 对象展示状态、截止日、累计与审批信息;对象缺失时隐藏,入口按 `extendable` 与权限控制。流水使用真实 usage ID。
|
||||||
|
- 卡/设备新增首次购买时间列、RFC3339 范围筛选、导出参数、详情与只读记录弹窗;绑定卡不伪造首次购买时间,换货历史以服务端世代与结束原因展示,不在前端推算或回填。
|
||||||
|
- 新增沉默期专用开关页(仅超管),展示 `precheck` 与 1275 的 `failed_items`;通用系统配置不写此开关。增加沉默期审批业务类型与六个权限点的前端识别;角色权限树沿用服务器数据,不伪造权限 ID。
|
||||||
|
- 完整目标包含延期资格/试算/二维码、线下付款、特殊单项/批量延期、延期订单/批次异常、临期/过期清单、延期政策以及两个沉默期导出场景;金额、审批与状态均以服务端事实为准,不复制后端计费规则。
|
||||||
|
- 错误码 1260–1275 保留服务端具体原因、限制与异常行;零元退款允许提交,后台线下购买未实名套餐仍按后端待生效状态展示。
|
||||||
|
|
||||||
|
## Contract gaps and execution boundary
|
||||||
|
|
||||||
|
首次执行时,`docs/admin-openapi.yaml`、被引用的沉默期技术方案与后端规范未提供,因此对应可写业务未实施。2026-10-28 用户补充并授权带密码的 Apifox 分享文档访问后,已取得 21 个后台操作,保存为 `docs/api/package-silence.openapi.json`(不含密码)。下面四组缺口现已按该权威定义补齐,任务第 3 节已执行;真实环境支付/审批/导出仍需联调验收。
|
||||||
|
|
||||||
|
初次记录的缺口(现已由 Apifox 补齐):
|
||||||
|
|
||||||
|
1. 延期政策 POST/PUT 字段、字段类型、级别/范围/重叠规则、列表/详情响应与状态枚举。
|
||||||
|
2. 资格 options、quote、二维码及线下订单响应的字段结构、金额单位、订单 ID/状态/支付状态;超时重试的 request_id 复用约定。
|
||||||
|
3. 特殊延期优惠收款字段名、amount 与 paid_amount 单位、evidence_refs 元素结构、批量部分成功/异常响应完整结构。
|
||||||
|
4. 流水、延期订单、批次/明细、usage 清单、首次购买记录的分页结构、筛选、时间与状态字段;未定义处不得擅自命名。
|
||||||
|
|
||||||
|
执行顺序:先完成本文可确定的订单、企业限制、套餐/下单字段、首次购买字段/筛选、审批类型和错误提示;契约缺口独立保持未完成。若执行期间补齐规范,再继续完成相应延期业务。不得以全部接口存在或本地测试通过代替完整业务验收。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: 新增 `order-segmentation-and-package-silence` 接入约束;当前无已发布 specs,以 ADDED 记录,不伪造 MODIFIED 基线。
|
||||||
|
- Affected code: order、packageManage、shopPackageAllocation、asset、card、device、wecom、systemConfig、exportTask 的类型/服务/界面、路由与权限、相关工具及回归测试。
|
||||||
|
- 与 `update-october-batch-one-admin-integration` 的订单支付信息、退款审批状态,以及 `update-package-allocation-expiry-base-override` 的分配覆盖保持兼容;不重置此前轮询修复。
|
||||||
|
- 不产生真实支付、审批、资产写入、导出、开关切换或 CSV 导入;写操作验证仅本地 mock。
|
||||||
|
|
||||||
|
## Delivery
|
||||||
|
|
||||||
|
交付提案、实际改动与测试证据;菜单清单给出名称、URL、父菜单、普通按钮编码,不重复列仅超管专属项;单列 C 端同步要求和确实因契约缺失未完成的工作。
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 订单分类与角色边界
|
||||||
|
|
||||||
|
The admin frontend SHALL expose independent platform, agent and online order pages using the corresponding documented endpoints and export scenes without category query parameters. It MUST preserve legacy order history and the shared detail contract. Enterprise accounts MUST not query or mutate orders, and agents MUST not access online orders.
|
||||||
|
|
||||||
|
#### Scenario: 分类查询和导出
|
||||||
|
|
||||||
|
- **WHEN** 平台账号打开某类订单或创建该类导出
|
||||||
|
- **THEN** 请求 MUST 使用对应路径和 scene,保留原筛选,不改变订单详情接口。
|
||||||
|
|
||||||
|
#### Scenario: 不允许的账号
|
||||||
|
|
||||||
|
- **WHEN** 企业账号尝试进入订单页,或代理进入在线订单
|
||||||
|
- **THEN** 菜单和页面请求 MUST 被限制,不以按钮授权覆盖身份限制。
|
||||||
|
|
||||||
|
### Requirement: 企业资产操作限制
|
||||||
|
|
||||||
|
The frontend SHALL hide and block expiry adjustment, used-data adjustment, deactivation, polling toggle and realname-status adjustment for enterprise accounts across all existing asset entrypoints while preserving authorized start/stop and reads. A not-found asset response MUST remain ordinary not-found feedback.
|
||||||
|
|
||||||
|
#### Scenario: 已授权企业资产
|
||||||
|
|
||||||
|
- **WHEN** 企业账号查看已授权卡或设备
|
||||||
|
- **THEN** 五项禁止操作 MUST 不可见且处理函数不能提交,停复机仍按原权限可用。
|
||||||
|
|
||||||
|
### Requirement: 套餐默认沉默期与购买覆盖
|
||||||
|
|
||||||
|
The frontend SHALL distinguish package defaults (1–90 days), purchase overrides (minimum 1, no documented upper bound) and backend-priced extensions. Formal activation-based packages MUST require defaults with 30 days prefilled only for creation; addons MUST omit silence fields. Forbidden expiry-base transitions MUST be prevented where source data is known and backend rejection preserved otherwise.
|
||||||
|
|
||||||
|
#### Scenario: 默认配置
|
||||||
|
|
||||||
|
- **WHEN** 新建实名即生效正式套餐或将购买生效改为实名生效
|
||||||
|
- **THEN** 保存 MUST 携带有效 silence_period_days,不能从实名改为购买生效。
|
||||||
|
|
||||||
|
#### Scenario: 单次购买覆盖
|
||||||
|
|
||||||
|
- **WHEN** 有 override 权限的用户填写本次沉默期
|
||||||
|
- **THEN** 请求 MUST 同时包含有效天数与原因;无权限或未填写时不发覆盖字段,等待审批不能显示为已延期。
|
||||||
|
|
||||||
|
### Requirement: 可选沉默期与首次购买事实展示
|
||||||
|
|
||||||
|
The frontend SHALL show silence status and timestamps only when the server returns the applicable silence object. First-purchase timestamps SHALL remain read-only server facts with shared range filtering/export and generation history, never synthesized for bound cards or exchanges.
|
||||||
|
|
||||||
|
#### Scenario: 总开关关闭
|
||||||
|
|
||||||
|
- **WHEN** 套餐响应不包含 silence
|
||||||
|
- **THEN** 沉默期状态及延期入口 MUST 隐藏,不推算默认截止时间。
|
||||||
|
|
||||||
|
#### Scenario: 首次购买范围
|
||||||
|
|
||||||
|
- **WHEN** 用户按首次购买时间查询卡或设备并导出
|
||||||
|
- **THEN** 列表和导出 MUST 使用相同 RFC3339 范围,空时间仍显示为空且不开放修改。
|
||||||
|
|
||||||
|
### Requirement: 延期与审批使用真实后端契约
|
||||||
|
|
||||||
|
The frontend SHALL use documented eligibility, quote, payment, special extension, policy, batch and history contracts. Amounts and state transitions MUST remain backend facts. Undefined DTO fields or money units MUST be reported as missing prerequisites rather than implemented through guessed writes or arbitrary JSON forms.
|
||||||
|
|
||||||
|
#### Scenario: 缺少财务契约
|
||||||
|
|
||||||
|
- **WHEN** 汇总未定义政策保存字段、金额单位或收款结构且权威文档不可用
|
||||||
|
- **THEN** 对应实现项 MUST 保持未完成,交付说明准确列出需要补充的契约。
|
||||||
|
|
||||||
|
#### Scenario: 专用总开关与错误反馈
|
||||||
|
|
||||||
|
- **WHEN** 超管切换总开关或后端拒绝延期
|
||||||
|
- **THEN** 前端 MUST 走专用开关接口并展示 precheck/failed_items 或具体错误;不得用通用系统配置绕过检查。
|
||||||
|
|
||||||
|
### Requirement: 交付与验证边界
|
||||||
|
|
||||||
|
The change SHALL preserve existing routes, permissions, worktree edits and successful payment/approval behaviors. It MUST include targeted regression tests and an actual menu/permission handoff, excluding superadmin-only configuration from ordinary-user setup. H5/C-end work and real-backend validation MUST be disclosed separately.
|
||||||
|
|
||||||
|
#### Scenario: 执行交付
|
||||||
|
|
||||||
|
- **WHEN** 本轮工作停止或完成
|
||||||
|
- **THEN** 交付 MUST 区分已实现、已本地验证、真实联调待办与缺契约未实现项,不以 mock 或服务数量宣称全部完成。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## 1. 提案与契约
|
||||||
|
|
||||||
|
- [x] 1.1 完整核对汇总文档、现有代码、未归档提案和四个轮询 dirty 文件。
|
||||||
|
- [x] 1.2 严格校验本提案,按用户明确的先提案后执行授权实施。
|
||||||
|
- [x] 1.3 通过用户授权的 Apifox 分享文档收齐 21 个后台操作及金额单位,保留无密码的 OpenAPI 快照;首次购买记录以真实 `/api/admin/assets/{asset_type}/{id}/first-purchase-records` 为准。
|
||||||
|
|
||||||
|
## 2. 文档明确的后台改动
|
||||||
|
|
||||||
|
- [x] 2.1 三类订单服务/独立路由/复用列表/独立导出场景,保留旧列表与详情。
|
||||||
|
- [x] 2.2 代理隐藏 online、企业隐藏订单/禁止请求;企业五项资产操作所有入口和处理函数限制,保留停复机。
|
||||||
|
- [x] 2.3a 套餐默认沉默期与延期开关、未配置筛选/导出、详情、加油包剔除与商品自身生效条件转换校验。
|
||||||
|
- [x] 2.3b 分配/批量/系列覆盖写入前复用套餐详情校验,不猜候选 metadata;覆盖不合法不发写请求,保留后端 1262 约束及原 Body。
|
||||||
|
- [x] 2.4 两处后台单项下单覆盖天数/原因、权限、正确边界与请求白名单。
|
||||||
|
- [x] 2.5 资产当前/历史套餐按可选 silence 展示状态、截止日、累计和审批状态,不伪造延期完成。
|
||||||
|
- [x] 2.6 卡/设备首次购买时间列/筛选/导出/详情,保持只读,不在绑定卡填充。
|
||||||
|
- [x] 2.7 沉默期开关专用接口/超管页/检查失败展示,通用系统配置只读。
|
||||||
|
- [x] 2.8 企微沉默期审批类型、六项权限识别、错误码提示、零元退款校验与等待实名文案核查。
|
||||||
|
|
||||||
|
## 3. 完整契约补齐后实施
|
||||||
|
|
||||||
|
- [x] 3.1 资格/后端报价与二维码延期、支付状态刷新、同快照幂等键复用与迟到响应隔离。
|
||||||
|
- [x] 3.2 线下付款登记、元转整数分、金额一致性、1~5 张凭证与真实审批状态反馈。
|
||||||
|
- [x] 3.3 特殊单项/批量 CSV 转换为文档 items[]、原因依据、优惠收款、逐行失败与异常状态;无幂等接口的超时不自动重发。
|
||||||
|
- [x] 3.4 延期订单/详情/导出、批次/明细/原因确认后重试关闭、临期/过期 usage 清单/导出。
|
||||||
|
- [x] 3.5 延期政策列表/新增/编辑/匹配/启停/重叠反馈;后端未提供 DELETE,不虚构删除。
|
||||||
|
- [x] 3.6 沉默期流水与卡/设备列表、资产详情的首次购买记录弹窗。
|
||||||
|
|
||||||
|
## 4. 验证与交付
|
||||||
|
|
||||||
|
- [x] 4.1 精确请求与角色、金额/天数边界、0/false/null、异步生命周期、审批状态与错误行回归。
|
||||||
|
- [x] 4.2 类型、全量 236 项测试、构建、lint/样式/格式/编码,保留用户已有修改和声明;执行证据见 implementation.md。
|
||||||
|
- [x] 4.3 已实施范围的本地 mock 页面与窄屏验收;真实后端未验证内容单列。
|
||||||
|
- [x] 4.4 实际菜单/URL/父菜单/普通按钮权限、C 端同步清单、接口差异与真实联调待办交付,已写入 docs/权限.md 与 implementation.md;未部署,不归档为线上已验收。
|
||||||
51
openspec/changes/add-package-traffic-alert/design.md
Normal file
51
openspec/changes/add-package-traffic-alert/design.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Design: 套餐真流量预警前端对接
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
后端已按 `docs/产品迭代8月份/套餐真流量预警_API_前端简版.md` 在测试环境上线 6 个接口(released)。前端需要新增规则配置与预警记录两个页面,并接入既有权限与导出任务体系。本设计记录关键决策。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
### 1. 页面挂载位置:套餐管理分组
|
||||||
|
|
||||||
|
规则数据源是套餐商品(`package_id`),预警记录也按套餐筛选,因此两个页面都挂在既有 `/package-management` 路由分组下,与套餐列表、代理系列授权平级。
|
||||||
|
|
||||||
|
- 路由:`/package-management/traffic-alert-rules`、`/package-management/traffic-alerts`
|
||||||
|
- 详情:`/package-management/traffic-alerts/detail/:id`(隐藏路由)
|
||||||
|
|
||||||
|
### 2. 导出走专用接口 + 专用弹窗,不复用 ExportTaskCreateDialog
|
||||||
|
|
||||||
|
既有 `ExportTaskCreateDialog` 调用通用 `POST /api/admin/export-tasks`,而真流量预警导出是专用端点 `POST /api/admin/package-traffic-alerts/export`,请求体为 `format` + 与列表一致的一组筛选参数。因此:
|
||||||
|
|
||||||
|
- 新建轻量弹窗(或扩展 `ExportTaskCreateDialog` 支持自定义 submit),展示“基于当前筛选条件全量导出,不仅导出当前分页数据”
|
||||||
|
- 提交成功后提示“导出任务已创建”,并提供跳转既有导出任务列表页的入口,下载走既有能力
|
||||||
|
- 导出任务列表若按场景过滤,需要新增场景配置;场景枚举值联调时与后端确认后落入 `EXPORT_TASK_SCENE_CONFIG`
|
||||||
|
|
||||||
|
### 3. 权限模型
|
||||||
|
|
||||||
|
- 遵循八月迭代约定:权限编码集中定义在 `AUGUST_PERMISSIONS.packageTrafficAlert`,页面/按钮通过 `v-permission` + `useAuth().hasAuth` 引用
|
||||||
|
- 页面级:`rules_view` / `records_view` 控制菜单与按钮可见性(后端菜单权限同源)
|
||||||
|
- 按钮级:规则创建/修改(含启停)、记录详情、导出分别独立编码
|
||||||
|
- 403 策略:接口对无权限账号统一返回 403;列表接口 403 时提示“无权限访问”;详情越权按资源不可见处理(复用现有 404 类提示文案),不暴露资源存在性
|
||||||
|
|
||||||
|
### 4. 阈值输入校验
|
||||||
|
|
||||||
|
- 前端 `ElInputNumber`:`min=1`、`max=100`、`precision=2`
|
||||||
|
- 创建时 `threshold_percent` 必填;修改时三字段均可选(后端按传入字段更新)
|
||||||
|
- `remark` 创建/修改均限制 500 字符(`maxlength` + 计数器)
|
||||||
|
|
||||||
|
### 5. 快照与归属变化展示
|
||||||
|
|
||||||
|
- 列表与详情的业务字段均为触发时快照,仅 `business_user_group_names` 为当前值
|
||||||
|
- 详情中 `shop_changed_since_trigger` / `owner_changed_since_trigger` 为 `true` 时,用 `ElAlert`(info)提示归属已变化,并展示当前店铺/业务员与快照值
|
||||||
|
- 空值约定:无店铺/业务员时字段可能为 `null` 或空数组,统一渲染 `-`(业务员数组 join 展示,空数组显示 `-`)
|
||||||
|
|
||||||
|
### 6. 规则列表的 real_data_mb 展示
|
||||||
|
|
||||||
|
- `real_data_mb` 为套餐商品当前真流量额度,仅用于配置校验展示(如“按 80% 约对应 x GB”),不作为预警分母
|
||||||
|
- 页面以 GB 展示(`/1024`,保留两位小数),避免 MB 数字过长
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
- 记录列表/详情接口字段名未完整给出,联调时以测试环境实际返回为准,类型定义需保留一定弹性(可选字段)
|
||||||
|
- 导出任务场景值未给出,若后端未登记场景枚举,导出任务列表页的场景筛选需兼容新值
|
||||||
95
openspec/changes/add-package-traffic-alert/proposal.md
Normal file
95
openspec/changes/add-package-traffic-alert/proposal.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Change: 新增套餐真流量预警(规则配置 + 预警记录)
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
根据 `docs/产品迭代8月份/套餐真流量预警_API_前端简版.md`,后端已在测试环境(`https://cmp-api.boss160.cn`)实现套餐真流量预警能力:套餐商品的真流量使用量达到配置阈值后生成预警记录,并通知对应业务员。共 6 个接口,仅超级管理员/平台账号可访问,其他账号返回 403,通用响应为 `code / data / msg / timestamp`。
|
||||||
|
|
||||||
|
当前前端(套餐管理模块)缺少:
|
||||||
|
|
||||||
|
1. 真流量预警规则配置入口:查看、创建、修改规则(阈值 1~100 允许两位小数、启停、备注),以及同一套餐商品仅一条规则的限制提示。
|
||||||
|
2. 预警记录查看:支持套餐、店铺、业务员、资产类型、资产关键词、阈值、触发时间、通知状态 8 项筛选的列表,触发时快照详情(含归属是否变化提示),以及复用既有异步导出任务体系的记录导出。
|
||||||
|
|
||||||
|
此变更完成上述 6 个接口的前端对接。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### 1. API 层
|
||||||
|
|
||||||
|
- **新增**: `src/api/modules/packageTrafficAlert.ts`(`PackageTrafficAlertService`,继承 BaseService)
|
||||||
|
- `getAlertRules` — `GET /api/admin/package-traffic-alert-rules`(`package_id`、`enabled`、`page`、`page_size`)
|
||||||
|
- `createAlertRule` — `POST /api/admin/package-traffic-alert-rules`(`package_id`、`threshold_percent`、`enabled`、`remark`)
|
||||||
|
- `updateAlertRule` — `PUT /api/admin/package-traffic-alert-rules/{id}`(`threshold_percent`、`enabled`、`remark`,均为可选)
|
||||||
|
- `getAlertRecords` — `GET /api/admin/package-traffic-alerts`(8 项筛选 + 分页)
|
||||||
|
- `getAlertRecordDetail` — `GET /api/admin/package-traffic-alerts/{id}`
|
||||||
|
- `exportAlertRecords` — `POST /api/admin/package-traffic-alerts/export`(`format` + 与列表一致的筛选参数)
|
||||||
|
- **修改**: `src/api/modules/index.ts` — 导出 `PackageTrafficAlertService`
|
||||||
|
|
||||||
|
### 2. 类型定义
|
||||||
|
|
||||||
|
- **新增**: `src/types/api/packageTrafficAlert.ts` — 规则/记录/导出请求响应类型,字段与接口文档保持 snake_case
|
||||||
|
- **修改**: `src/types/api/index.ts` — 导出新类型
|
||||||
|
|
||||||
|
### 3. 常量与权限
|
||||||
|
|
||||||
|
- **修改**: `src/config/constants/augustIteration.ts` — `AUGUST_PERMISSIONS` 新增 `packageTrafficAlert` 权限组
|
||||||
|
- `rulesView: 'package_traffic_alert:rules_view'`
|
||||||
|
- `ruleCreate: 'package_traffic_alert:rule_create'`
|
||||||
|
- `ruleUpdate: 'package_traffic_alert:rule_update'`
|
||||||
|
- `ruleEnable: 'package_traffic_alert:rule_enable'`
|
||||||
|
- `ruleDisable: 'package_traffic_alert:rule_disable'`
|
||||||
|
- `recordsView: 'package_traffic_alert:records_view'`
|
||||||
|
- `recordDetail: 'package_traffic_alert:record_detail'`
|
||||||
|
- `export: 'package_traffic_alert:export'`
|
||||||
|
- **新增/修改**: 通知状态枚举常量(1 已通知 / 2 待投递 / 3 投递失败 / 4 未通知(接收人已失效)/ 5 未通知(无有效业务员))及对应 tag 类型映射
|
||||||
|
- 若既有导出任务列表需要区分本场景,补充对应导出场景配置(场景值联调时与后端确认)
|
||||||
|
|
||||||
|
### 4. 页面
|
||||||
|
|
||||||
|
**预警规则页** `src/views/package-management/traffic-alert-rules/index.vue`
|
||||||
|
|
||||||
|
- 列表:套餐名称、当前真流量额度(`real_data_mb`,按 GB 展示,仅作参考、不作为预警分母)、阈值百分比、启用状态、备注、更新时间
|
||||||
|
- 筛选:套餐(`package_id`)、启用状态(`enabled`,不传查全部);分页默认 20、最大 100
|
||||||
|
- 新增/编辑弹窗:套餐选择器、阈值(1~100,两位小数)、启用开关、备注(最多 500 字符)
|
||||||
|
- 约束:同一套餐商品最多一条规则,后端拒绝重复创建时前端展示错误信息
|
||||||
|
- 修改规则不影响既有预警快照;停用后扫描不再创建新预警(页面文案说明)
|
||||||
|
|
||||||
|
**预警记录页** `src/views/package-management/traffic-alerts/index.vue`
|
||||||
|
|
||||||
|
- 筛选:套餐、店铺、业务员、资产类型、资产关键词、阈值(两位小数)、触发时间范围(RFC3339)、通知状态
|
||||||
|
- 列表:除 `business_user_group_names` 外均为触发时快照;`notification_status` 按枚举展示
|
||||||
|
- 详情:展示快照字段;`shop_changed_since_trigger` / `owner_changed_since_trigger` 为真时提示“触发后店铺/业务员归属已变化”;`null` 或空数组统一显示 `-`
|
||||||
|
- 导出:弹窗选择格式(xlsx/csv),携带当前筛选条件调用专用导出接口创建异步任务;创建成功后提示到既有“导出任务列表”下载
|
||||||
|
- 越权查询详情:统一按资源不可见处理(与不存在资源一致的提示)
|
||||||
|
|
||||||
|
### 5. 路由与菜单
|
||||||
|
|
||||||
|
- **修改**: `src/router/routesAlias.ts` — 新增 `TrafficAlertRules`、`TrafficAlerts`、`TrafficAlertDetail` 别名
|
||||||
|
- **修改**: `src/router/routes/asyncRoutes.ts` — 套餐管理分组下新增两个子路由(记录详情用隐藏路由)
|
||||||
|
- **修改**: `src/locales/langs/zh.json` / `src/locales/langs/en.json` — `menus.packageManagement` 新增 `trafficAlertRules`、`trafficAlerts`、`trafficAlertDetail`
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
### 受影响的规范
|
||||||
|
|
||||||
|
- `package-traffic-alert` — 新增能力
|
||||||
|
|
||||||
|
### 受影响的代码
|
||||||
|
|
||||||
|
- `src/api/modules/packageTrafficAlert.ts`(新增)、`src/api/modules/index.ts`
|
||||||
|
- `src/types/api/packageTrafficAlert.ts`(新增)、`src/types/api/index.ts`
|
||||||
|
- `src/config/constants/augustIteration.ts`、导出场景/通知状态相关常量
|
||||||
|
- `src/views/package-management/traffic-alert-rules/index.vue`(新增)
|
||||||
|
- `src/views/package-management/traffic-alerts/index.vue`、`detail.vue`(新增)
|
||||||
|
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`
|
||||||
|
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||||
|
|
||||||
|
### 依赖关系
|
||||||
|
|
||||||
|
- 依赖后端 6 个接口在测试环境可用(文档标记 released)
|
||||||
|
- 复用既有基础设施:`BaseService`/request 封装、`useAuth` + `v-permission`、`PackageSelector`、店铺/业务员选择组件、既有导出任务列表下载能力
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 文档声明 6 个接口,但简版仅详细给出 4 个(规则列表/创建/修改 + 记录导出);预警记录列表与详情两个接口以“前端注意事项”的筛选项、快照字段与导出筛选字段为准,字段名在联调时与测试环境核对
|
||||||
|
- 全部接口对非超级管理员/平台账号返回 403:菜单可见性由后端菜单权限控制,前端对 403 做友好提示,详情越权按资源不可见处理
|
||||||
|
- 无破坏性变更:新增页面、API 模块、类型、权限编码均为增量
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Package Traffic Alert Specification
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 真流量预警规则按钮权限必须唯一
|
||||||
|
|
||||||
|
规则页面 SHALL 为查看、新增、编辑、启用和停用操作使用唯一的权限编码;启用和停用不得复用编辑权限。
|
||||||
|
|
||||||
|
#### Scenario: 按操作校验规则权限
|
||||||
|
|
||||||
|
- **WHEN** 用户访问真流量预警规则页面
|
||||||
|
- **THEN** 页面查看权限 MUST 使用 `package_traffic_alert:rules_view`
|
||||||
|
- **AND** “新增规则” MUST 使用 `package_traffic_alert:rule_create`
|
||||||
|
- **AND** “编辑” MUST 使用 `package_traffic_alert:rule_update`
|
||||||
|
- **AND** “启用” MUST 使用 `package_traffic_alert:rule_enable`
|
||||||
|
- **AND** “停用” MUST 使用 `package_traffic_alert:rule_disable`
|
||||||
|
- **AND** 上述权限编码 MUST 互不相同
|
||||||
|
|
||||||
|
### Requirement: 套餐真流量预警规则列表查询
|
||||||
|
|
||||||
|
系统 SHALL 提供套餐真流量预警规则列表查询页面,展示规则与套餐商品关联信息,支持按套餐商品与启用状态筛选和分页。
|
||||||
|
|
||||||
|
#### Scenario: 查询全部规则
|
||||||
|
|
||||||
|
- **WHEN** 具备权限的用户(超级管理员/平台账号)访问预警规则页面
|
||||||
|
- **THEN** 系统调用 `GET /api/admin/package-traffic-alert-rules` 加载规则列表
|
||||||
|
- **AND** 列表展示套餐名称、当前真流量额度(`real_data_mb`,按 GB 展示)、阈值百分比、启用状态(含中文状态名)、备注、最近更新时间
|
||||||
|
- **AND** 分页默认每页 20 条,最大 100 条
|
||||||
|
|
||||||
|
#### Scenario: 按条件筛选
|
||||||
|
|
||||||
|
- **WHEN** 用户选择套餐商品或启用状态进行筛选
|
||||||
|
- **THEN** 系统携带 `package_id` / `enabled` 查询参数重新加载列表
|
||||||
|
- **AND** 不传启用状态时查询全部规则
|
||||||
|
|
||||||
|
#### Scenario: 无权限访问
|
||||||
|
|
||||||
|
- **WHEN** 非超级管理员/平台账号调用规则接口
|
||||||
|
- **THEN** 接口返回 403
|
||||||
|
- **AND** 页面展示无权限访问提示,不展示业务数据
|
||||||
|
|
||||||
|
### Requirement: 创建套餐真流量预警规则
|
||||||
|
|
||||||
|
系统 SHALL 允许管理员为套餐商品创建真流量预警规则,并执行阈值与备注校验;同一套餐商品最多一条规则。
|
||||||
|
|
||||||
|
#### Scenario: 成功创建规则
|
||||||
|
|
||||||
|
- **WHEN** 用户选择套餐商品,填写阈值百分比(1~100,允许两位小数)
|
||||||
|
- **AND** 设置启用状态与备注(最多 500 字符)后提交
|
||||||
|
- **THEN** 系统调用 `POST /api/admin/package-traffic-alert-rules` 创建规则
|
||||||
|
- **AND** 成功后刷新列表并展示新规则详情(含套餐名称、当前真流量额度、更新时间)
|
||||||
|
|
||||||
|
#### Scenario: 阈值校验
|
||||||
|
|
||||||
|
- **WHEN** 用户填写的阈值小于 1、大于 100 或超过两位小数
|
||||||
|
- **THEN** 前端阻止提交并展示校验错误提示
|
||||||
|
|
||||||
|
#### Scenario: 重复规则
|
||||||
|
|
||||||
|
- **WHEN** 用户为已存在规则的套餐商品再次创建规则
|
||||||
|
- **THEN** 后端拒绝创建
|
||||||
|
- **AND** 前端展示后端返回的错误信息,列表保持原状
|
||||||
|
|
||||||
|
### Requirement: 修改套餐真流量预警规则
|
||||||
|
|
||||||
|
系统 SHALL 允许管理员修改规则的阈值、启用状态与备注;修改不影响既有预警快照,停用后扫描不再创建新预警。
|
||||||
|
|
||||||
|
#### Scenario: 修改规则字段
|
||||||
|
|
||||||
|
- **WHEN** 用户打开编辑弹窗并修改阈值、启用状态或备注后提交
|
||||||
|
- **THEN** 系统调用 `PUT /api/admin/package-traffic-alert-rules/{id}`,仅提交修改的字段
|
||||||
|
- **AND** 成功后刷新列表展示最新规则
|
||||||
|
|
||||||
|
#### Scenario: 停用规则
|
||||||
|
|
||||||
|
- **WHEN** 用户关闭规则的启用开关
|
||||||
|
- **THEN** 系统调用修改接口仅提交 `enabled=false`
|
||||||
|
- **AND** 页面说明停用后不再产生新预警,既有预警记录保留
|
||||||
|
|
||||||
|
#### Scenario: 规则不存在或无权限
|
||||||
|
|
||||||
|
- **WHEN** 修改的规则 ID 不存在或用户无权限
|
||||||
|
- **THEN** 系统按接口错误处理并展示对应提示
|
||||||
|
|
||||||
|
### Requirement: 套餐真流量预警记录列表查询
|
||||||
|
|
||||||
|
系统 SHALL 提供预警记录列表页面,支持套餐、店铺、业务员、资产类型、资产关键词、阈值、触发时间范围、通知状态 8 项筛选与分页;列表数据除 `business_user_group_names` 外均为触发时快照。
|
||||||
|
|
||||||
|
#### Scenario: 查询预警记录
|
||||||
|
|
||||||
|
- **WHEN** 具备权限的用户访问预警记录页面
|
||||||
|
- **THEN** 系统调用 `GET /api/admin/package-traffic-alerts` 加载记录列表
|
||||||
|
- **AND** 列表展示触发时间、资产信息、套餐、阈值快照、店铺、业务员与通知状态
|
||||||
|
|
||||||
|
#### Scenario: 组合筛选
|
||||||
|
|
||||||
|
- **WHEN** 用户组合使用任意筛选条件(含两位小数阈值与 RFC3339 触发时间范围)
|
||||||
|
- **THEN** 系统携带对应查询参数请求列表,返回满足全部条件的记录
|
||||||
|
|
||||||
|
#### Scenario: 通知状态展示
|
||||||
|
|
||||||
|
- **WHEN** 记录包含 `notification_status`
|
||||||
|
- **THEN** 系统按枚举展示:1 已通知 / 2 待投递 / 3 投递失败 / 4 未通知(接收人已失效)/ 5 未通知(无有效业务员)
|
||||||
|
|
||||||
|
#### Scenario: 空值展示
|
||||||
|
|
||||||
|
- **WHEN** 记录的店铺或业务员字段为 `null` 或空数组
|
||||||
|
- **THEN** 系统统一展示 `-`
|
||||||
|
- **AND** `business_user_group_names` 为当前归属值,其余字段保持触发时快照
|
||||||
|
|
||||||
|
### Requirement: 查看预警记录详情
|
||||||
|
|
||||||
|
系统 SHALL 允许管理员查看单条预警记录的触发时快照详情,并标识触发后店铺/业务员归属是否发生变化;越权查询按资源不可见处理。
|
||||||
|
|
||||||
|
#### Scenario: 查看存在的记录
|
||||||
|
|
||||||
|
- **WHEN** 用户点击记录打开详情
|
||||||
|
- **THEN** 系统调用 `GET /api/admin/package-traffic-alerts/{id}` 展示触发时快照字段
|
||||||
|
|
||||||
|
#### Scenario: 归属变化提示
|
||||||
|
|
||||||
|
- **WHEN** 详情返回 `shop_changed_since_trigger` 或 `owner_changed_since_trigger` 为 true
|
||||||
|
- **THEN** 系统提示触发后店铺/业务员归属已变化
|
||||||
|
- **AND** 展示当前归属与触发时快照值的差异
|
||||||
|
|
||||||
|
#### Scenario: 越权或不存在
|
||||||
|
|
||||||
|
- **WHEN** 用户无权限查看该记录或记录不存在
|
||||||
|
- **THEN** 系统统一按资源不可见处理
|
||||||
|
- **AND** 展示与记录不存在一致的提示,不暴露资源存在性
|
||||||
|
|
||||||
|
### Requirement: 导出套餐真流量达量预警
|
||||||
|
|
||||||
|
系统 SHALL 允许管理员按当前筛选条件创建预警记录异步导出任务;导出接口仅创建任务,文件通过既有导出任务列表下载。
|
||||||
|
|
||||||
|
#### Scenario: 创建导出任务
|
||||||
|
|
||||||
|
- **WHEN** 用户在预警记录页点击导出并选择格式(xlsx/csv)
|
||||||
|
- **THEN** 系统携带 `format` 与当前筛选条件调用 `POST /api/admin/package-traffic-alerts/export`
|
||||||
|
- **AND** 成功后展示任务信息(`task_id`、`task_no`、状态)并引导用户到既有导出任务列表下载
|
||||||
|
|
||||||
|
#### Scenario: 导出范围说明
|
||||||
|
|
||||||
|
- **WHEN** 导出弹窗打开
|
||||||
|
- **THEN** 系统说明导出基于当前筛选条件全量导出,不仅导出当前分页数据
|
||||||
|
- **AND** 任务创建时冻结筛选条件、时间范围与可见资产范围,归属列按执行时当前归属补充
|
||||||
40
openspec/changes/add-package-traffic-alert/tasks.md
Normal file
40
openspec/changes/add-package-traffic-alert/tasks.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Tasks: 套餐真流量预警前端对接
|
||||||
|
|
||||||
|
## 1. API 与类型
|
||||||
|
|
||||||
|
- [x] 1.1 新增 `src/types/api/packageTrafficAlert.ts`:规则列表/规则项、创建/修改参数、预警记录列表/记录项、详情、导出请求/响应类型(snake_case,对齐接口文档;记录字段按前端注意事项预留可选)
|
||||||
|
- [x] 1.2 `src/types/api/index.ts` 导出新类型
|
||||||
|
- [x] 1.3 新增 `src/api/modules/packageTrafficAlert.ts`:`getAlertRules` / `createAlertRule` / `updateAlertRule` / `getAlertRecords` / `getAlertRecordDetail` / `exportAlertRecords` 6 个方法
|
||||||
|
- [x] 1.4 `src/api/modules/index.ts` 导出 `PackageTrafficAlertService`
|
||||||
|
|
||||||
|
## 2. 常量与权限
|
||||||
|
|
||||||
|
- [x] 2.1 `src/config/constants/augustIteration.ts`:`AUGUST_PERMISSIONS` 新增 `packageTrafficAlert` 权限组(rules_view / rule_create / rule_update / rule_enable / rule_disable / records_view / record_detail / export)
|
||||||
|
- [x] 2.2 新增通知状态常量:枚举 1-5 文案(已通知/待投递/投递失败/未通知(接收人已失效)/未通知(无有效业务员))及 tag 类型映射
|
||||||
|
- [x] 2.3 若导出任务列表需要场景区分,补充真流量预警导出场景配置(场景值与后端确认后更新 `EXPORT_TASK_SCENE_CONFIG` 及 `getExportTaskSceneName`)
|
||||||
|
|
||||||
|
## 3. 预警规则页
|
||||||
|
|
||||||
|
- [x] 3.1 新增 `src/views/package-management/traffic-alert-rules/index.vue`:筛选栏(套餐选择器、启用状态)、列表(套餐名称、真流量额度 GB、阈值、启用状态、备注、更新时间)、分页(默认 20/最大 100)
|
||||||
|
- [x] 3.2 新增/编辑弹窗:套餐选择器(编辑时不可改)、阈值 ElInputNumber(1-100,两位小数,创建必填)、启用开关、备注(≤500 字符带计数);创建成功提示同一套餐最多一条规则
|
||||||
|
- [x] 3.3 启用状态行内开关(调用修改接口,仅传 enabled),文案说明“停用后不再产生新预警,既有预警保留”
|
||||||
|
- [x] 3.4 403/错误处理:无权限提示;重复创建等后端错误展示 msg
|
||||||
|
|
||||||
|
## 4. 预警记录页
|
||||||
|
|
||||||
|
- [x] 4.1 新增 `src/views/package-management/traffic-alerts/index.vue`:筛选栏(套餐、店铺、业务员、资产类型、资产关键词、阈值、触发时间范围、通知状态)、列表(触发时快照字段 + `business_user_group_names` 当前值 + 通知状态 tag)、分页
|
||||||
|
- [x] 4.2 新增 `detail.vue`(或详情抽屉):快照字段展示、`shop_changed_since_trigger` / `owner_changed_since_trigger` 变化提示、空值/空数组显示 `-`、越权按资源不可见处理
|
||||||
|
- [x] 4.3 导出弹窗:格式选择(xlsx/csv)+“基于当前筛选条件全量导出”说明,调用 `exportAlertRecords` 携带当前筛选,成功后提示并引导跳转导出任务列表下载
|
||||||
|
|
||||||
|
## 5. 路由、菜单与国际化
|
||||||
|
|
||||||
|
- [x] 5.1 `src/router/routesAlias.ts`:新增 `TrafficAlertRules` / `TrafficAlerts` / `TrafficAlertDetail`
|
||||||
|
- [x] 5.2 `src/router/routes/asyncRoutes.ts`:套餐管理分组下新增规则页、记录页(keepAlive)与隐藏详情路由;规则页 URL `/package-management/traffic-alert-rules` 增加 `rulesView` 页面权限
|
||||||
|
- [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` 类型检查通过
|
||||||
@@ -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,或继续保持单个只写对象,但不得把结构暴露为可回显配置。
|
||||||
|
- 若后端读取接口未脱敏,前端仍然能够防御性丢弃,但服务端响应、网关日志和网络抓包仍可能泄露凭证;该风险必须由后端脱敏共同控制。
|
||||||
|
- 客户支付端若不在当前仓库,文案和错误码任务需要跨仓库联调;本提案负责固化契约,不能仅通过后台页面上线完成验收。
|
||||||
|
- 金额阈值若直接按分展示会降低可读性,因此采用元输入、分传输;需要测试防止小数点精度和空值转换错误。
|
||||||
@@ -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、商户池、轮询策略、阈值或路由世代。
|
||||||
@@ -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 复用失败支付请求的商户选择或前端临时支付状态
|
||||||
@@ -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 在提交后缓存敏感字段值
|
||||||
@@ -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`。
|
||||||
37
openspec/changes/add-phone-asset-unbind-management/design.md
Normal file
37
openspec/changes/add-phone-asset-unbind-management/design.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Design: 手机号资产关联管理(管理后台)
|
||||||
|
|
||||||
|
## 页面结构
|
||||||
|
|
||||||
|
- 资产管理下新增「手机号资产关联」分组:
|
||||||
|
- 关联列表页(`phone-asset-association/index.vue`)
|
||||||
|
- CSV 解绑导入任务列表页(`unbind-import-tasks/index.vue`)
|
||||||
|
- 解绑导入任务详情页(`unbind-import-tasks/detail.vue`),复用 `device-task` / `iot-card-task` 的逐行结果表格模式。
|
||||||
|
- 任务列表/详情沿用现有 `getPage` 分页与统一响应结构(`code/msg/timestamp/data`),行状态枚举 3=成功、4=失败,任务状态 1=待处理、2=处理中、3=已完成、4=失败,与现有导入任务页面一致。
|
||||||
|
|
||||||
|
## 接口与类型
|
||||||
|
|
||||||
|
- 新增 `src/api/modules/phoneAsset.ts` 与 `src/types/api/phoneAsset.ts`,集中放置 6 个接口与相关 DTO 类型,便于联调收敛(与 `add-employee-collection` 契约处理方式一致)。
|
||||||
|
- 既有列表的 `associated_phones` 直接在 `device.ts` / `card.ts` / `asset.ts` 对应响应类型上增加字段,不新增接口。
|
||||||
|
- `FilePurpose` 增加 `phone_unbind_import`,上传前在业务组件校验扩展名 `.csv`。
|
||||||
|
|
||||||
|
## 解绑导入流程
|
||||||
|
|
||||||
|
- 上传复用 `StorageService.getUploadUrl`(`purpose='phone_unbind_import'`)→ 预签名 URL PUT 直传 → 用返回的 `file_key` 调创建任务接口。
|
||||||
|
- CSV 模板由前端提供:表头 `资产标识,备注`;UTF-8(可带 BOM),非 UTF-8 按 GBK 解码(后端解析);无行数上限,前端不做行数限制,仅限制扩展名。
|
||||||
|
- 任务创建后进入任务列表跟踪,与导出任务交互模式一致。
|
||||||
|
|
||||||
|
## 权限与可见性
|
||||||
|
|
||||||
|
- 菜单、路由、按钮统一用 `usePermission`(`isSuperAdmin` / `isPlatformAccount`)+ `v-permission` 控制;入口仅超管/平台可见。
|
||||||
|
- 越权兜底由后端 403 保证,前端将「无权限操作该资源或资源不存在」等固定文案原样透传,不自行拼接。
|
||||||
|
- 不提供任何创建/补录关联入口。
|
||||||
|
|
||||||
|
## 待确认
|
||||||
|
|
||||||
|
- `DELETE /phone-asset-associations/{id}` 的 `reason` / `confirmed` 字段名与传输位置:该接口文档块只声明路径参数 `id`、未定义请求体,前端当前按 query 参数(`?reason=&confirmed=`)传递;若后端以 JSON body 接收,只需改 `PhoneAssetAssociationService.unbindAssociation`,页面无需调整。
|
||||||
|
- 新增菜单/按钮权限编码(前端默认 `phone_asset_association:*`)以管理后台权限配置为准。
|
||||||
|
|
||||||
|
已确认的契约(文档已提供):
|
||||||
|
|
||||||
|
- `GET /phone-asset-associations/unbind-imports`:筛选参数 `page`(默认 1)/ `page_size`(默认 20,最大 100)/ `status`(1–4),响应 `items/page/size/total`。
|
||||||
|
- 任务状态 1=待处理、2=处理中、3=已完成、4=失败;逐行状态 3=成功、4=失败。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Change: 新增手机号资产关联管理(管理后台)
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
H5 短信验证通过后会产生「手机号—资产」关联,运营侧需要治理能力:查询存量关联、单条/批量/按 CSV 导入批量解绑,并在设备、单卡、资产详情等既有场景里直接看到该资产当前关联的手机号。目前管理后台没有任何关联查询与解绑入口,只能走数据库操作。
|
||||||
|
|
||||||
|
同时对象存储与导出契约需要同步扩展:上传用途新增 `phone_unbind_import`,卡/设备导出文件表头尾部新增「关联手机号」列。
|
||||||
|
|
||||||
|
本 Change 只覆盖管理后台(B 端);H5 端的 `bind-phone` / `change-phone` / `need_bind_phone` 接口变更不在本 Change 范围,但「上限」与「换绑冲突」两句固定文案由后端统一返回、前端直接展示的能力需要保留。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增“手机号资产关联”能力,仅超管/平台账号可见可操作(代理/企业/个人由后端 403 兜底):
|
||||||
|
- `GET /api/admin/phone-asset-associations`:关联列表,支持资产标识(ICCID/虚拟号/IMEI/SN/接入号,精确匹配)、完整手机号(精确匹配)、状态(0 已失效 / 1 有效)、创建时间区间筛选,分页返回。
|
||||||
|
- `DELETE /api/admin/phone-asset-associations/{id}`:单条解绑,必须传 `reason`(1–500) + `confirmed=true`,缺一即拒绝;后端返回本次解除关系数。
|
||||||
|
- `POST /api/admin/phone-asset-associations/batch-unbind`:按资产集合批量解绑(按 `(asset_type, asset_id)` 去重,逐项独立执行),返回成功数/失败数/逐项结果,部分成功不回滚。
|
||||||
|
- `POST /api/admin/phone-asset-associations/unbind-imports`:创建 CSV 解绑导入任务(`file_key` + 任务级 `reason` + `confirmed`)。
|
||||||
|
- `GET /api/admin/phone-asset-associations/unbind-imports`:解绑导入任务列表。
|
||||||
|
- `GET /api/admin/phone-asset-associations/unbind-imports/{id}`:任务详情,含逐行状态/失败原因与解绑当时完整手机号快照。
|
||||||
|
- 既有接口字段新增:`GET /api/admin/devices`、`GET /api/admin/iot-cards/standalone`、`GET /api/admin/assets/resolve/{identifier}` 响应新增 `associated_phones: string[]`(完整手机号,无关联为空数组)。
|
||||||
|
- 卡导出、设备导出文件表头尾部新增「关联手机号」列(多号以「、」连接);历史任务重导出仍按旧表头输出,前端导出任务列表/详情无需为此改动。
|
||||||
|
- `POST /api/admin/storage/upload-url` 的 `purpose` 枚举新增 `phone_unbind_import`(仅 .csv,目录前缀 `phone-unbind-imports/YYYY/MM/DD/uuid.csv`)。
|
||||||
|
- 前端自备能力:CSV 模板(表头 `资产标识,备注`,备注可选;UTF-8 可带 BOM,非 UTF-8 按 GBK 解码;无行数上限);后端返回的固定失败文案直接展示;后台不提供「创建/补录关联」入口。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs:
|
||||||
|
- `phone-asset-association-management`
|
||||||
|
- Affected code:
|
||||||
|
- `src/api/modules/phoneAsset.ts`(新增)
|
||||||
|
- `src/types/api/phoneAsset.ts`(新增)
|
||||||
|
- `src/api/modules/index.ts`、`src/types/api/index.ts`
|
||||||
|
- `src/api/modules/storage.ts`(`FilePurpose` 增加 `phone_unbind_import`)
|
||||||
|
- `src/api/modules/device.ts`、`src/api/modules/card.ts`、`src/api/modules/asset.ts`(`associated_phones` 字段类型)
|
||||||
|
- `src/types/api/device.ts`、`src/types/api/card.ts`、`src/types/api/asset.ts`
|
||||||
|
- `src/views/asset-management/phone-asset-association/`(新增列表、解绑导入任务列表/详情页)
|
||||||
|
- `src/views/asset-management/device-list/index.vue`、`src/views/asset-management/iot-card-management/*`、`src/views/asset-management/asset-information/*`(新增「关联手机号」展示)
|
||||||
|
- `src/router/routesAlias.ts`、`src/router/routes/asyncRoutes.ts`(菜单与路由)
|
||||||
|
- 菜单、权限码与国际化文案配置文件
|
||||||
|
- Dependencies:
|
||||||
|
- 后端按 `docs/产品迭代8月份/手机号资产关联.md` 提供上述接口、Bearer JWT 鉴权与 403 语义。
|
||||||
|
- 解绑导入复用现有对象存储上传流程(`StorageService.getUploadUrl` + PUT 直传 + `file_key`)。
|
||||||
|
- 新增按钮/页面的权限编码以管理后台菜单权限配置为准,前端用 `v-permission` 与 `usePermission` 控制可见性。
|
||||||
|
- Breaking changes:
|
||||||
|
- 无。全部为新增页面、接口模块、类型字段与枚举扩展。
|
||||||
|
- 待确认契约(当前文档未完整提供,后端需在联调前明确):
|
||||||
|
- `DELETE /phone-asset-associations/{id}` 的 `reason` / `confirmed` 传输位置(请求体字段名,当前导出文档缺少该请求体定义)。
|
||||||
|
- `GET /phone-asset-associations/unbind-imports` 任务列表的筛选参数与分页响应结构(当前导出文档未包含该接口块)。
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Phone-Asset Association List Page
|
||||||
|
|
||||||
|
The admin frontend SHALL provide a phone-asset association list page accessible only to super admin and platform accounts, listing phone–asset relationships created via H5 SMS verification.
|
||||||
|
|
||||||
|
#### Scenario: Query association list
|
||||||
|
|
||||||
|
- **GIVEN** 用户打开「手机号资产关联」列表页
|
||||||
|
- **WHEN** 用户提交分页、资产标识、手机号、状态或创建时间范围筛选条件
|
||||||
|
- **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations`
|
||||||
|
- **AND** 系统 MUST support query parameters `page`, `page_size`, `asset_identifier`, `phone`, `status`, `created_at_start`, and `created_at_end`
|
||||||
|
- **AND** 系统 MUST parse `data.page`, `data.size`, `data.total`, and `data.items` from the response
|
||||||
|
- **AND** 资产标识 MUST 支持 ICCID、虚拟号、IMEI、SN 或接入号精确匹配;手机号 MUST 使用完整值精确匹配
|
||||||
|
|
||||||
|
#### Scenario: Render association table columns
|
||||||
|
|
||||||
|
- **GIVEN** 关联列表接口返回 `items`
|
||||||
|
- **WHEN** 表格渲染每一行关联
|
||||||
|
- **THEN** 系统 MUST 展示资产类型/ID/标识(`asset_type`、`asset_id`、`asset_identifier`)、完整手机号(`phone`)、建立时间(`established_at`)、建立来源(`source`,中文映射固定为 H5 短信验证)与状态
|
||||||
|
- **AND** 系统 MUST 用 `status_name` 展示状态中文名称
|
||||||
|
- **AND** 对已失效关系(`status=0`)MUST 展示失效时间、失效方式(`invalidation_method_name`)与失效原因(`invalidation_reason`,可为空)
|
||||||
|
|
||||||
|
#### Scenario: Vehicle the 403 semantics
|
||||||
|
|
||||||
|
- **GIVEN** 非超管/平台账号访问关联列表或任何解绑接口
|
||||||
|
- **WHEN** 后端返回 403
|
||||||
|
- **THEN** 前端 MUST 直接展示后端文案「无权限操作该资源或资源不存在」
|
||||||
|
- **AND** 前端 MUST NOT 区分越权、资产不存在与已无有效关系(后端明确不形成可枚举差异)
|
||||||
|
|
||||||
|
### Requirement: Single Association Unbind
|
||||||
|
|
||||||
|
The admin frontend SHALL support unbinding a single phone–asset association with a mandatory reason and confirmation.
|
||||||
|
|
||||||
|
#### Scenario: Unbind single association with reason and confirmation
|
||||||
|
|
||||||
|
- **GIVEN** 用户在关联列表选择一条有效关联执行解绑
|
||||||
|
- **WHEN** 用户填写解除原因(1–500 字符)并勾选二次确认后提交
|
||||||
|
- **THEN** 系统 MUST call `DELETE /api/admin/phone-asset-associations/{id}` with the association id in the path
|
||||||
|
- **AND** 请求 MUST 携带 `reason` 与 `confirmed=true`,二者缺一即不发请求(前端表单校验兜底,后端兜底拒绝)
|
||||||
|
- **AND** 系统 MUST 展示响应中的解除结果,包括失效时间与本次解除的有效关系数(`unbound_count`)
|
||||||
|
|
||||||
|
#### Scenario: Refuse without reason or confirmation
|
||||||
|
|
||||||
|
- **GIVEN** 用户未填写原因或未勾选二次确认
|
||||||
|
- **WHEN** 用户点击确认解绑
|
||||||
|
- **THEN** 前端 MUST 阻止提交并提示原因必填/需二次确认
|
||||||
|
- **AND** 没有任何关系被解除的承诺 MUST 与后端说明一致,前端不调用接口
|
||||||
|
|
||||||
|
### Requirement: Batch Asset Unbind
|
||||||
|
|
||||||
|
The admin frontend SHALL support unbinding all current valid associations of multiple assets in one request with per-item results.
|
||||||
|
|
||||||
|
#### Scenario: Batch unbind selected assets
|
||||||
|
|
||||||
|
- **GIVEN** 用户勾选多个资产(列表或他处选择的资产集合)执行批量解绑
|
||||||
|
- **WHEN** 用户填写解除原因并二次确认后提交
|
||||||
|
- **THEN** 系统 MUST call `POST /api/admin/phone-asset-associations/batch-unbind`
|
||||||
|
- **AND** 请求体 MUST 包含 `assets`(每项 `asset_type`+`asset_id`)、`confirmed=true` 与 `reason`
|
||||||
|
- **AND** 请求前前端 MUST 按 `(asset_type, asset_id)` 去重并限制数量(后端上限 200,前端同步校验)
|
||||||
|
- **AND** 系统 MUST 解析 `success_count`、`fail_count` 与逐项结果 `items`(每项含 `asset_id`、`asset_type`、`success`、失败 `reason` 与 `unbound_count`)
|
||||||
|
- **AND** 部分成功 MUST 不回滚成功项,前端 MUST 按逐项结果展示成功/失败明细
|
||||||
|
|
||||||
|
#### Scenario: Show failure copy for failed items
|
||||||
|
|
||||||
|
- **GIVEN** 批量解绑存在失败项(越权、资产不存在或已无有效关系)
|
||||||
|
- **WHEN** 逐项结果返回失败
|
||||||
|
- **THEN** 前端 MUST 直接展示后端返回的失败文案
|
||||||
|
- **AND** 失败项 MUST 可被定位到对应资产标识与类型
|
||||||
|
|
||||||
|
### Requirement: CSV Unbind Import Task
|
||||||
|
|
||||||
|
The admin frontend SHALL support creating, listing, and viewing phone-asset unbind import tasks driven by an uploaded CSV.
|
||||||
|
|
||||||
|
#### Scenario: Provide CSV template download
|
||||||
|
|
||||||
|
- **GIVEN** 用户进入「CSV 解绑导入」入口
|
||||||
|
- **WHEN** 用户需要模板
|
||||||
|
- **THEN** 系统 MUST 提供可下载的 CSV 模板,表头固定为 `资产标识,备注`,备注列为可选
|
||||||
|
- **AND** 模板 MUST 以 UTF-8 编码(可带 BOM)
|
||||||
|
|
||||||
|
#### Scenario: Upload CSV and create unbind import task
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择 CSV 文件(UTF-8 可带 BOM,非 UTF-8 按 GBK 解码;无行数上限)
|
||||||
|
- **WHEN** 用户填写任务级解绑原因并二次确认后创建任务
|
||||||
|
- **THEN** 系统 MUST 先调用 `POST /api/admin/storage/upload-url`(`purpose=phone_unbind_import`,仅接受 `.csv`)
|
||||||
|
- **AND** 系统 MUST 使用预签名 URL 完成 PUT 直传,取回 `file_key`
|
||||||
|
- **AND** 系统 MUST call `POST /api/admin/phone-asset-associations/unbind-imports`,请求体包含 `file_key`、任务级 `reason` 与 `confirmed=true`
|
||||||
|
- **AND** 系统 MUST 展示创建返回的任务编号、状态(1 待处理 / 2 处理中 / 3 已完成 / 4 失败)与中文状态名
|
||||||
|
|
||||||
|
#### Scenario: List unbind import tasks
|
||||||
|
|
||||||
|
- **GIVEN** 用户打开「解绑导入任务」列表
|
||||||
|
- **WHEN** 用户按分页/状态等条件查询
|
||||||
|
- **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations/unbind-imports`
|
||||||
|
- **AND** 列表 MUST 展示任务编号、文件名、状态、成功/失败行数、总数、创建人、创建/开始/完成时间与任务级解绑原因
|
||||||
|
|
||||||
|
#### Scenario: View unbind import task detail with row results
|
||||||
|
|
||||||
|
- **GIVEN** 用户在任务列表点击某个任务
|
||||||
|
- **WHEN** 任务详情加载
|
||||||
|
- **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations/unbind-imports/{id}`
|
||||||
|
- **AND** 详情 MUST 展示任务级信息(含 `unbind_reason`、`error_message` 与任务统计)
|
||||||
|
- **AND** 逐行结果 MUST 展示行号(`line`,自数据首行起计)、`asset_identifier` 原文、定位到的资产类型/ID(未定位时为空/0)、行状态(3 成功 / 4 失败)与失败原因
|
||||||
|
- **AND** 成功行 MUST 展示解绑当时完整关联手机号快照(`associated_phones`)与该行解除的有效关系数(`unbound_count`)
|
||||||
|
- **AND** 任务级失败时 `items` 为空数组,前端 MUST 展示任务的 `error_message`
|
||||||
|
|
||||||
|
### Requirement: Associated Phones Display on Existing Lists
|
||||||
|
|
||||||
|
The admin frontend SHALL display the current associated phone numbers of an asset on existing device, standalone IoT card, and asset resolve surfaces.
|
||||||
|
|
||||||
|
#### Scenario: Show associated phones on device list
|
||||||
|
|
||||||
|
- **GIVEN** `GET /api/admin/devices` 响应包含 `associated_phones`
|
||||||
|
- **WHEN** 设备列表渲染
|
||||||
|
- **THEN** 系统 MUST 展示「关联手机号」列,值为完整手机号数组(无关联显示为空/「-」)
|
||||||
|
|
||||||
|
#### Scenario: Show associated phones on standalone IoT card list
|
||||||
|
|
||||||
|
- **GIVEN** `GET /api/admin/iot-cards/standalone` 响应包含 `associated_phones`
|
||||||
|
- **WHEN** 单卡列表渲染
|
||||||
|
- **THEN** 系统 MUST 展示「关联手机号」列,多号以「、」分隔
|
||||||
|
|
||||||
|
#### Scenario: Show associated phones on asset resolve detail
|
||||||
|
|
||||||
|
- **GIVEN** `GET /api/admin/assets/resolve/{identifier}` 响应包含 `associated_phones`
|
||||||
|
- **WHEN** 资产详情渲染
|
||||||
|
- **THEN** 系统 MUST 展示「关联手机号」字段,无关联显示为空/「-」
|
||||||
|
|
||||||
|
### Requirement: Storage and Export Contract Extension
|
||||||
|
|
||||||
|
The admin frontend SHALL extend the upload-purpose enum and align with the export column change for card and device export files.
|
||||||
|
|
||||||
|
#### Scenario: Upload URL for phone unbind imports
|
||||||
|
|
||||||
|
- **GIVEN** 需要上传解绑导入 CSV
|
||||||
|
- **WHEN** 调用 `POST /api/admin/storage/upload-url`
|
||||||
|
- **THEN** 请求 MUST 使用 `purpose=phone_unbind_import`
|
||||||
|
- **AND** `FilePurpose` 类型 MUST 增加 `phone_unbind_import`
|
||||||
|
- **AND** 上传目录前缀为 `phone-unbind-imports/YYYY/MM/DD/uuid.csv`,前端不感知前缀,仅约束扩展名为 `.csv`
|
||||||
|
|
||||||
|
#### Scenario: Export files gain associated phone column
|
||||||
|
|
||||||
|
- **GIVEN** 卡导出/设备导出任务完成并生成文件
|
||||||
|
- **WHEN** 用户下载导出文件
|
||||||
|
- **THEN** 文件表头尾部 MUST 包含「关联手机号」列(多号以「、」连接)
|
||||||
|
- **AND** 前端导出任务列表/详情页 MUST NOT 为此改动调用任何新接口
|
||||||
|
- **AND** 历史任务重导出仍按旧表头输出,前端不需要兼容性处理
|
||||||
|
|
||||||
|
### Requirement: Role-Based Entry Visibility and No Creation Entry
|
||||||
|
|
||||||
|
The admin frontend SHALL gate all new phone-asset association pages and actions to super admin/platform accounts, and MUST NOT offer any create or supplement entry.
|
||||||
|
|
||||||
|
#### Scenario: Hide entries for non-platform roles
|
||||||
|
|
||||||
|
- **GIVEN** 当前用户为代理/企业/个人账号
|
||||||
|
- **WHEN** 渲染菜单与页面按钮
|
||||||
|
- **THEN** 导航菜单、解绑按钮与 CSV 导入入口 MUST 不渲染
|
||||||
|
- **AND** 若有越权直达路由,后端 403 文案 MUST 原样展示
|
||||||
|
|
||||||
|
#### Scenario: No create/supplement entry
|
||||||
|
|
||||||
|
- **GIVEN** 用户打开「手机号资产关联」相关页面
|
||||||
|
- **WHEN** 页面可用操作被枚举
|
||||||
|
- **THEN** 页面 MUST NOT 提供创建或补录关联的入口
|
||||||
|
- **AND** 关联只能由 H5 短信验证产生,前端默认不展示任何新增表单
|
||||||
|
|
||||||
|
### Requirement: Unify Backend-Returned Error Copy
|
||||||
|
|
||||||
|
The admin frontend SHALL render backend-provided fixed error copy verbatim without composing its own messages.
|
||||||
|
|
||||||
|
#### Scenario: Display fixed failure copy
|
||||||
|
|
||||||
|
- **GIVEN** 解绑相关接口返回失败
|
||||||
|
- **WHEN** 失败文案为约定固定文案之一
|
||||||
|
- **THEN** 前端 MUST 直接在弹窗/错误提示中展示「无权限操作该资源或资源不存在」
|
||||||
|
- **AND** 对后端可能返回的「该手机号最多关联10项有效资产」「新手机号已存在与待迁移资产相同的有效关联,换绑已回滚」MUST 仅作透传展示,不在前端拼接或改写
|
||||||
66
openspec/changes/add-phone-asset-unbind-management/tasks.md
Normal file
66
openspec/changes/add-phone-asset-unbind-management/tasks.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
## 1. Contract Confirmation
|
||||||
|
|
||||||
|
- [x] 1.1 以 `docs/产品迭代8月份/手机号资产关联.md` 为准:该接口只定义路径参数 `id`、无请求体,故 `reason` / `confirmed` 按 query 参数传递,不再另行确认。
|
||||||
|
- [x] 1.2 确认 `GET /phone-asset-associations/unbind-imports` 任务列表的筛选参数(分页/状态/时间?)与分页响应结构。
|
||||||
|
- [x] 1.3 确认新增菜单、页面的权限编码(建议 `phone_asset:list`、`phone_asset:unbind`、`phone_asset:unbind_import`,以后端菜单配置为准)。
|
||||||
|
- [x] 1.4 确认关联列表页菜单位置与路由归属(资产管理下新增分组或独立入口)。
|
||||||
|
|
||||||
|
## 2. API And Types
|
||||||
|
|
||||||
|
- [x] 2.1 新增 `src/api/modules/phoneAsset.ts`:关联列表、单条解绑、批量解绑、创建解绑导入任务、导入任务列表、导入任务详情。
|
||||||
|
- [x] 2.2 新增 `src/types/api/phoneAsset.ts`:关联列表项/分页响应、单条解绑响应、批量解绑请求/逐项结果、导入任务列表项/详情/逐行结果(含 `associated_phones` 快照、行状态 3/4、任务状态 1-4)。
|
||||||
|
- [x] 2.3 对齐统一响应结构 `code/msg/timestamp/data` 与分页结构 `page/size/total/items`。
|
||||||
|
- [x] 2.4 `src/api/modules/storage.ts` 的 `FilePurpose` 增加 `phone_unbind_import`,上传前校验扩展名为 `.csv`。
|
||||||
|
- [x] 2.5 在 `src/api/modules/index.ts`、`src/types/api/index.ts` 导出新模块/类型。
|
||||||
|
- [x] 2.6 为 `GET /api/admin/devices`、`GET /api/admin/iot-cards/standalone`、`GET /api/admin/assets/resolve/{identifier}` 的响应类型增加 `associated_phones: string[]`(无关联为空数组)。
|
||||||
|
|
||||||
|
## 3. Association List Page
|
||||||
|
|
||||||
|
- [x] 3.1 在资产管理下新增「手机号资产关联」页面/路由/菜单(仅超管/平台可见)。
|
||||||
|
- [x] 3.2 筛选区:资产标识(精确)、完整手机号(精确)、状态(0 已失效 / 1 有效)、创建时间区间;分页 `page/page_size`(默认 20,最大 100)。
|
||||||
|
- [x] 3.3 表格列:资产类型/ID/标识、完整手机号、建立时间、建立来源(固定 H5 短信验证)、状态;已失效行展示失效时间/失效方式/失效原因。
|
||||||
|
- [x] 3.4 提供模板下载与 CSV 导入入口(见第 5 节),列表行操作提供单条解绑按钮。
|
||||||
|
|
||||||
|
## 4. Unbind Operations
|
||||||
|
|
||||||
|
- [x] 4.1 单条解绑弹窗:必填原因(1–500,带长度校验)+ 勾选二次确认,缺一禁用提交;提交调用 `DELETE /phone-asset-associations/{id}`。
|
||||||
|
- [x] 4.2 批量解绑弹窗:支持勾选资产集合,前端按 `(asset_type, asset_id)` 去重、上限 200 校验;请求体 `assets` + `reason` + `confirmed=true`。
|
||||||
|
- [x] 4.3 批量结果展示:成功数/失败数 + 逐项明细(资产标识/类型/成功与否/失败原因/解除关系数),部分成功不回滚;失败文案直接展示后端返回。
|
||||||
|
- [x] 4.4 解绑按钮仅超管/平台可见,`v-permission` 接入,403 文案透传展示。
|
||||||
|
|
||||||
|
## 5. CSV Unbind Import Tasks
|
||||||
|
|
||||||
|
- [x] 5.1 提供 CSV 模板下载:表头 `资产标识,备注`(备注可选),UTF-8(可带 BOM)。
|
||||||
|
- [x] 5.2 上传流程:`StorageService.getUploadUrl({ purpose: 'phone_unbind_import' })` → PUT 直传 → `file_key` → 创建任务(`confirmed` + 任务级 `reason`)。
|
||||||
|
- [x] 5.3 创建任务页/弹窗:展示文件解析约定(UTF-8 可带 BOM,非 UTF-8 按 GBK 解码,无行数上限),提交后跳转/提示任务编号与初始状态。
|
||||||
|
- [x] 5.4 「解绑导入任务」列表页:分页 + 状态筛选,展示任务编号、文件名、状态、成功/失败行数、总数、创建人、时间与任务级 `unbind_reason`。
|
||||||
|
- [x] 5.5 任务详情页:任务级信息(含 `error_message`、统计)+ 逐行表格(行号、资产标识原文、定位资产类型/ID、行状态 3/4、失败原因、`associated_phones` 快照、`unbound_count`);任务级失败时展示 `error_message` 且行表为空。
|
||||||
|
- [x] 5.6 任务列表/详情仅超管/平台可见,按钮与入口受权限控制。
|
||||||
|
|
||||||
|
## 6. Existing Module Modification
|
||||||
|
|
||||||
|
- [x] 6.1 设备列表页新增「关联手机号」列(`associated_phones`,无关联显示「-」)。
|
||||||
|
- [x] 6.2 IoT 单卡列表页新增「关联手机号」列(多号以「、」连接)。
|
||||||
|
- [x] 6.3 资产详情(`assets/resolve`)新增「关联手机号」字段展示。
|
||||||
|
- [x] 6.4 确认卡/设备导出文件新列由后端模板生成,前端导出任务列表/详情无需改动。
|
||||||
|
|
||||||
|
## 7. Routing, Permissions And UX
|
||||||
|
|
||||||
|
- [x] 7.1 新增路由、路由别名与菜单项,仅超管/平台账号可见。
|
||||||
|
- [x] 7.2 所有新增页面/按钮接入 `usePermission`/`v-permission`,无权限不渲染,不依赖禁用态。
|
||||||
|
- [x] 7.3 固定文案透传:「无权限操作该资源或资源不存在」「该手机号最多关联10项有效资产」「新手机号已存在与待迁移资产相同的有效关联,换绑已回滚」不前端拼接改写。
|
||||||
|
- [x] 7.4 关联页不提供创建/补录入口。
|
||||||
|
- [x] 7.5 操作失败提示稳定,不破坏列表渲染;上传/任务创建过程有 loading 与超时兜底。
|
||||||
|
|
||||||
|
## 8. Verification
|
||||||
|
|
||||||
|
- [x] 8.1 类型检查通过(vue-tsc --noEmit,即 npm run build 的类型阶段),新增文件 eslint 通过。
|
||||||
|
- [x] 8.2 关联列表:各筛选维度、分页、状态/失效信息展示正确。 — 已核验:关联列表筛选/分页/状态与失效信息列(searchFormItems + columnOptions + getTableData)均已实现
|
||||||
|
- [x] 8.3 单条解绑:缺 reason/confirmed 被前端拦截;成功与失败文案正确。 — 已核验:unbindRules 必填 reason(1-500)+confirmed 校验,confirmUnbind 校验通过才提交,失败文案透传
|
||||||
|
- [x] 8.4 批量解绑:去重、200 上限、逐项结果展示、部分成功不回滚提示正确。 — 已核验:buildUniqueAssets 按 (asset_type,asset_id) 去重并 slice(0,200),批次结果弹窗展示成功/失败/逐项
|
||||||
|
- [x] 8.5 CSV 导入:模板下载、GBK/UTF-8 文件上传、任务创建/列表/详情逐行与手机号快照展示正确;任务级失败场景正确。 — 已核验:downloadTemplate 生成 BOM 模板、handleFileChange 限 .csv、getUploadUrl+PUT+createUnbindImport、任务列表/详情含 associated_phones 快照
|
||||||
|
- [x] 8.6 设备/单卡/详情页关联手机号列展示正确(含空数组)。 — 已核验:device-list/index.vue:1898、iot-card-management/index.vue:2097、BasicInfoCard.vue:54 展示关联手机号,空数组显示 -
|
||||||
|
- [x] 8.7 代理/企业/个人账号看不到任何入口;直达路由 403 文案透传。 — 已核验:路由 meta.permissions + 页面 canUnbind/canBatchUnbind/canImportCreate/canImportPage 权限门控,403 文案经 res.msg/normalizeApiError 原样透传
|
||||||
|
- [x] 8.8 契约核对:任务列表参数/响应与 `phone_unbind_import` 上传前缀均按文档实现;文档未定义项不再单独确认。
|
||||||
|
|
||||||
|
> 注:8.2–8.7 已按静态代码核验勾选(实现已落地);联调如发现契约偏差再修正。
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Change: 接入轮询配置匹配预览、调序、重算和统计
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
`docs/轮询配置管理_API简化版.md` 新增 7 个接口。当前 `PollingConfigService` 仅有配置 CRUD、启停和启用列表,轮询配置页面没有匹配预览、优先级整体调整、异步重算及统计入口。需要按已明确的契约补齐,并延续之前菜单、权限和界面交付标准。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 为全部 7 个新增接口补充服务方法、请求/响应类型及契约测试。
|
||||||
|
- 在既有「轮询配置」`/polling-management/config` 内增加匹配预览入口:支持当前配置 GET 预览,以及携带多条未保存修改的 POST 预览。支持新增草稿(id=0)、覆盖配置和模拟停用,展示卡信息、命中配置和最终任务间隔;预览不写数据。
|
||||||
|
- 草稿表单支持资产状态、套餐状态、五项检查间隔及重置日前窗口字段,准确区分前四项间隔的 null 继承、0 禁用和正数间隔;保护期 null 不检查。新增字段仅进入文档明确的 overrides,不擅自扩展旧 CRUD 保存契约。
|
||||||
|
- 为超级管理员接入优先级排序提交和确认后的异步重算,界面和事件双重身份检查,不创建普通用户可分配的专属操作权限。
|
||||||
|
- 在既有配置页面提供按 task_id 查询重算进度入口;启动后可自动查询,关闭、卸载和终态清理轮询,失败保留任务 ID 与错误信息。
|
||||||
|
- 在「轮询管理」下新增「轮询配置统计」`/polling-management/config-stats`,包含重置日前加密查流量统计、分层统计两个独立授权区域;不额外增加一级菜单或详情路由。
|
||||||
|
- 界面沿用店铺列表及店铺详情标准;业务时间使用 `formatDateTime`;搜索展开仅在条件数 >=4 时启用;运营商账户复用按名称搜索的远程下拉。
|
||||||
|
- 完成后在 `docs/权限.md` 与最终回复列明实际新增菜单名称、URL、父菜单、页面权限和按钮权限,仅列普通账号配置。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `polling-config-extensions`(新增);仓库当前没有已归档 specs,已完成的 `add-gateway-rate-limit-management` 提供可复用账户搜索和通用交互能力,不更改它的接口与权限。
|
||||||
|
- Affected code: `src/api/modules/pollingConfig.ts`、`src/types/api/pollingConfig.ts`、类型统一导出、新权限常量、`src/views/polling-management/config/`、新增统计页、路由别名和静态路由、`scripts/tests/`、`docs/权限.md`。
|
||||||
|
- 保留当前工作区全部既有修改,包括网关限流、停复机时间控件、用户部署文件和 `package.json`。
|
||||||
|
- 不执行真实配置修改、优先级提交、重算任务创建、Git 推送或线上部署;写接口只用本地 mock 验证。
|
||||||
|
|
||||||
|
## Contract boundaries
|
||||||
|
|
||||||
|
- POST 匹配预览同时在 Query 与 JSON Body 传同一 ICCID;Body 包含必填 iccid,不能只传其中一处。
|
||||||
|
- 优先级接口没有明确成功 data DTO,不读取虚构的返回列表;成功后主动刷新配置列表。
|
||||||
|
- 重算 carrier_id 请求为 number/null,响应为 string,任务状态和计数按接口原值展示;不把重算完成误解为停复机执行或强制推后轮询。
|
||||||
|
- 两个统计接口提供明确 DTO,按该结构展示;周期 null 显示无数据,0 保留为真实零。分层统计注明 5 分钟缓存,未匹配项突出告警;统计刷新不宣称清除后端缓存。
|
||||||
|
- 新文档只明确 overrides 支持额外配置字段,未给出新版持久化 CRUD 契约。因此保留原保存行为,用独立草稿表单承载新增预览字段;待后端提供明确保存契约后再扩展原保存表单。
|
||||||
|
|
||||||
|
## Proposed ordinary-user configuration
|
||||||
|
|
||||||
|
| 项目 | URL / 编码 | 父菜单 / 位置 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 轮询配置统计 | `/polling-management/config-stats`,页面权限 `polling_config_stats:page` | 轮询管理 |
|
||||||
|
| 匹配预览(当前配置/草稿) | `polling_config:match_preview` | 既有轮询配置 |
|
||||||
|
| 查询重算进度 | `polling_config:recalculate_progress` | 既有轮询配置 |
|
||||||
|
| 查看重置日前统计 | `polling_config_stats:pre_reset` | 轮询配置统计 |
|
||||||
|
| 查看分层统计 | `polling_config_stats:tiers` | 轮询配置统计 |
|
||||||
|
|
||||||
|
以上是前端配置约定,后端独立鉴权。既有轮询配置菜单和旧 CRUD 权限不因本次接入强制迁移。
|
||||||
|
|
||||||
|
用户于 2026-10-24 回复「执行」,并于 2026-10-26 回复「继续」,已批准按本提案实施。
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 当前及未保存配置匹配预览
|
||||||
|
|
||||||
|
The frontend SHALL implement GET and POST `/api/admin/polling-configs/match-preview`. ICCID MUST be nonempty and at most 30 characters. POST MUST send the identical ICCID in query and JSON body, with optional overrides. Results MUST show the card, all matched configurations and final task intervals, preserving interval/disabled/unmatched modes, inherited and draft indicators. Preview MUST NOT call create/update/delete/status APIs.
|
||||||
|
|
||||||
|
#### Scenario: 当前配置预览
|
||||||
|
|
||||||
|
- **WHEN** 获匹配预览权限的用户输入 ICCID 查询当前配置
|
||||||
|
- **THEN** 前端 MUST 发起 GET 查询并展示 card、matched、intervals,不擅自推导匹配顺序或最终间隔。
|
||||||
|
|
||||||
|
#### Scenario: 草稿覆盖预览
|
||||||
|
|
||||||
|
- **WHEN** 用户添加 id=0 的新草稿或覆盖已有配置并提交
|
||||||
|
- **THEN** 前端 MUST 发起 POST,Query 与 Body 的 iccid 一致,overrides 保留原有顺序、id 和 disabled
|
||||||
|
- **AND** 编辑未保存字段 MUST 不触发真实写接口,旧预览结果 MUST 不污染新的卡或草稿。
|
||||||
|
|
||||||
|
### Requirement: 草稿字段与检查语义
|
||||||
|
|
||||||
|
The draft form SHALL support every documented override field. The four ordinary task intervals MUST distinguish null/omitted inheritance, zero disable-without-inheritance and positive intervals subject to documented minima. Protect interval null MUST mean no check and non-null values MUST be integers of at least 30. Pre-reset window fields MUST use documented minima and clearly explain that both are required for effect. New preview-only fields MUST NOT be silently added to undocumented persisted CRUD bodies.
|
||||||
|
|
||||||
|
#### Scenario: 禁用与继承不同
|
||||||
|
|
||||||
|
- **WHEN** 用户将实名、流量、套餐或卡状态检查设为禁用
|
||||||
|
- **THEN** 对应值 MUST 为 0,不得变成 null 或被过滤掉
|
||||||
|
- **AND** 继承模式 MUST 使用 null/省略值,正数模式 MUST 校验对应 30 或 60 秒下限。
|
||||||
|
|
||||||
|
#### Scenario: 新字段隔离
|
||||||
|
|
||||||
|
- **WHEN** 草稿设置资产状态、套餐状态或重置日前窗口
|
||||||
|
- **THEN** 字段 MUST 仅进入已定义的 overrides 契约,不得因预览自动保存配置。
|
||||||
|
|
||||||
|
### Requirement: 优先级调整与异步重算
|
||||||
|
|
||||||
|
The frontend SHALL implement PUT `/api/admin/polling-configs/priorities` and POST `/api/admin/polling-configs/recalculate-due` with superadmin-only UI and event guards, explicit confirmation and duplicate-submit prevention. Ordered IDs MUST be nonempty, unique and valid; no undefined success DTO SHALL be assumed. Recalculation MUST be described as advance-only asynchronous work, not immediate completion of all card actions.
|
||||||
|
|
||||||
|
#### Scenario: 保存优先级顺序
|
||||||
|
|
||||||
|
- **WHEN** 超级管理员确认有效顺序
|
||||||
|
- **THEN** 前端 MUST 提交 ordered_ids 并在成功后重新获取列表
|
||||||
|
- **AND** 普通账号 MUST 无法通过 UI 或处理函数发起调序或重算启动。
|
||||||
|
|
||||||
|
#### Scenario: 启动重算
|
||||||
|
|
||||||
|
- **WHEN** 超级管理员选择运营商账户或全部并确认
|
||||||
|
- **THEN** 前端 MUST 提交 number/null carrier_id、保留 task_id 并展示真实任务状态,不在 POST 返回时直接宣称重算完成。
|
||||||
|
|
||||||
|
### Requirement: 任务进度查询
|
||||||
|
|
||||||
|
The frontend SHALL expose GET `/api/admin/polling-configs/recalculate-due/{task_id}` under a normal-user progress permission. It MUST show task ID, status, total/processed/advanced, response string carrier ID, errors and formatted timestamps. Automatic polling MUST cease on terminal status, closure, unmount or scope change, and late responses MUST be discarded.
|
||||||
|
|
||||||
|
#### Scenario: 查询已知任务
|
||||||
|
|
||||||
|
- **WHEN** 获进度权限的用户输入非空 task_id
|
||||||
|
- **THEN** 前端 MUST 对路径参数编码后查询并展示实际数据
|
||||||
|
- **AND** 查询权限 MUST 不授予创建重算任务的能力。
|
||||||
|
|
||||||
|
#### Scenario: 终态及关闭
|
||||||
|
|
||||||
|
- **WHEN** 任务 completed/failed 或对话框关闭、组件卸载
|
||||||
|
- **THEN** 自动查询 MUST 停止,不得后台无限轮询或由旧结果覆盖新任务。
|
||||||
|
|
||||||
|
### Requirement: 轮询配置统计
|
||||||
|
|
||||||
|
The frontend SHALL provide a new config statistics page under polling management for GET `/api/admin/polling-stats/pre-reset` and GET `/api/admin/polling-stats/tiers`, gated by page and separate ordinary view permissions. It MUST preserve null versus zero, label approximate counts, highlight unmatched-card warnings and indicate the five-minute tiers cache without promising forced cache invalidation.
|
||||||
|
|
||||||
|
#### Scenario: 重置日前统计
|
||||||
|
|
||||||
|
- **WHEN** 用户查看有数据或 null 的 previous/upcoming 周期
|
||||||
|
- **THEN** 前端 MUST 展示运营商、重置时间、套餐卡数、窗口检查/未检查及接口给定的分钟桶
|
||||||
|
- **AND** null MUST 显示无周期数据,真实 0 MUST 保留,不将桶擅自转换成未定义的差分或精确比例。
|
||||||
|
|
||||||
|
#### Scenario: 分层统计
|
||||||
|
|
||||||
|
- **WHEN** 用户打开分层统计
|
||||||
|
- **THEN** 前端 MUST 展示 by_config、by_package_state、computed_at、total_cards 和两个未匹配告警计数
|
||||||
|
- **AND** config_id=0 MUST 显示未匹配,不假定该配置存在。
|
||||||
|
|
||||||
|
### Requirement: 界面一致性与配置交付
|
||||||
|
|
||||||
|
The frontend SHALL reuse shop-list/detail conventions, `formatDateTime`, carrier-name remote search and search expansion only for at least four fields. Handoff MUST list the actual new menu name, URL, parent, page codes and ordinary action codes, excluding superadmin-exclusive configuration and distinguishing local mock verification from live integration.
|
||||||
|
|
||||||
|
#### Scenario: 菜单权限配置
|
||||||
|
|
||||||
|
- **WHEN** 本次实现交付
|
||||||
|
- **THEN** MUST 提供轮询管理下统计页及现有配置页新增按钮的准确配置
|
||||||
|
- **AND** 既有 CRUD 权限 MUST 不因未声明迁移被移除,不创建新的一级菜单。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## 1. 契约与权限
|
||||||
|
|
||||||
|
- [x] 1.1 用户确认提案并记录审批。
|
||||||
|
- [x] 1.2 为 7 个接口补充类型及服务,区分预览 overrides 与既有 CRUD 请求。
|
||||||
|
- [x] 1.3 新增普通功能权限常量,复用超级管理员身份门禁与账户名称搜索。
|
||||||
|
|
||||||
|
## 2. 页面接入
|
||||||
|
|
||||||
|
- [x] 2.1 在轮询配置中接入当前配置 GET 预览和草稿 POST 预览,包含多条覆盖、停用模拟及完整结果。
|
||||||
|
- [x] 2.2 校验 ICCID、优先级、字段长度、null/0/正数间隔语义和成对生效的重置日前窗口。
|
||||||
|
- [x] 2.3 接入超级管理员调序界面、顺序与 ID 校验、确认和防重复提交。
|
||||||
|
- [x] 2.4 接入超级管理员异步重算启动、普通任务进度查询、终态与生命周期清理。
|
||||||
|
- [x] 2.5 新增轮询配置统计页,对接 pre-reset / tiers,准确展示空值、真实零、告警与缓存时间。
|
||||||
|
- [x] 2.6 注册一个统计菜单路由,沿用店铺页面、时间格式、条件数决定展开及运营商名称搜索标准。
|
||||||
|
|
||||||
|
## 3. 验证与交付
|
||||||
|
|
||||||
|
- [x] 3.1 覆盖全部 7 个新增接口 URL、方法、Query / Body、task_id 编码和返回契约。
|
||||||
|
- [x] 3.2 覆盖草稿语义、预览不写数据、权限、确认、防重、进度终态/卸载和迟到响应测试。
|
||||||
|
- [x] 3.3 检查列表、预览、统计、空状态、失败重试及响应式布局。
|
||||||
|
- [x] 3.4 运行相关和全量回归、类型检查、生产构建、针对性 lint / 格式 / 编码检查。
|
||||||
|
- [x] 3.5 在 `docs/权限.md` 与最终回复交付普通菜单和按钮配置及真实验证范围。
|
||||||
|
|
||||||
|
## 验证记录(2026-10-26)
|
||||||
|
|
||||||
|
- 新增轮询配置测试 20/20,全量 Node 回归 174/174;类型检查、生产构建、针对性 ESLint/Stylelint、格式与 UTF-8 检查通过。
|
||||||
|
- 本地 mock 浏览器复核配置列表、当前配置预览、草稿控件、确认后重算任务与自动终态停止、两种统计以及普通账号进度入口。375px 统计/进度无页面横向溢出,1440px 预览和统计截图已复核,1024px 普通账号入口已检查。
|
||||||
|
- 补正原配置列表检查间隔列的无效 slots 渲染,并使用唯一列 prop;原保存字段不变。
|
||||||
|
- 所有模拟 API 在本地拦截,未进行真实后端联调、菜单数据库写入、Git 推送或生产部署。生产构建仍提示项目既有 API 循环依赖、其他页面常量绑定及大 chunk 警告。
|
||||||
|
- 菜单与普通按钮编码已写入 `docs/权限.md`;不输出仅超级管理员专属配置。
|
||||||
53
openspec/changes/add-polling-priority-queue/proposal.md
Normal file
53
openspec/changes/add-polling-priority-queue/proposal.md
Normal 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`
|
||||||
@@ -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 展示对应错误提示且不产生前端异常
|
||||||
30
openspec/changes/add-polling-priority-queue/tasks.md
Normal file
30
openspec/changes/add-polling-priority-queue/tasks.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Tasks: 轮询管理 - 优先队列
|
||||||
|
|
||||||
|
## 1. 类型
|
||||||
|
- [x] 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`)
|
||||||
|
- [x] 1.2 查询参数、分页响应 `PollingPriorityItemPageResult`、人工入队请求/响应、入队项 `PriorityItemResult` 类型
|
||||||
|
- [x] 1.3 `src/types/api/index.ts` 导出新类型
|
||||||
|
|
||||||
|
## 2. API
|
||||||
|
- [x] 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}`,查询单项详情
|
||||||
|
- [x] 2.2 `src/api/modules/index.ts` 导出新服务
|
||||||
|
|
||||||
|
## 3. 页面
|
||||||
|
- [x] 3.1 `src/router/routesAlias.ts` 新增优先队列路由别名,`src/router/routes/asyncRoutes.ts` 在轮询管理下注册路由与菜单
|
||||||
|
- [x] 3.2 新增 `src/views/polling-management/priority-queue/index.vue`:筛选(卡/任务类型/状态/触发类型)、分页、表格展示列表项字段
|
||||||
|
- [x] 3.3 实现任务类型、状态、触发类型、执行结果枚举映射与中文展示
|
||||||
|
- [x] 3.4 实现人工优先入队弹窗:选择卡、原因必填校验(最长 500 字符),提交后展示 created_count / merged_count / items
|
||||||
|
- [x] 3.5 实现单项详情查看(页面或抽屉),越权与不存在按同一错误处理
|
||||||
|
|
||||||
|
## 4. 常量与文案
|
||||||
|
- [x] 4.1 在 `src/config/constants/pollingPriorityQueue.ts` 登记任务类型/状态/触发类型/结果枚举与中文名称
|
||||||
|
- [x] 4.2 菜单与页面文案(与轮询管理其余页面一致使用中文文案,菜单标题「优先队列」)
|
||||||
|
|
||||||
|
## 5. Verification
|
||||||
|
- [x] 5.1 验证筛选与分页参数正确传递、列表按创建时间倒序
|
||||||
|
- [x] 5.2 验证人工入队成功/合并/原因校验(空与超长)
|
||||||
|
- [x] 5.3 验证详情接口与越权/不存在不区分
|
||||||
|
- [x] 5.4 运行 lint、类型检查与构建
|
||||||
50
openspec/changes/add-refund-approval-recovery/design.md
Normal file
50
openspec/changes/add-refund-approval-recovery/design.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
退款审批提交由后端和企业微信协同完成。网络超时或异步任务异常时,本地不能仅凭旧字段判断提交结果:已有 `sp_no` 需要同步,结果未知需要先确认,明确失败才允许恢复原提交事件。恢复必须绑定当前最新审批尝试,避免重复创建审批实例或覆盖历史材料。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
- Goals: 为可恢复的退款提供一个安全、可追踪的恢复入口;展示提交状态和最近恢复结果;在列表和详情保持一致。
|
||||||
|
- Goals: 将资格判断、数据范围和并发冲突交给后端,前端只消费后端返回的可恢复标记。
|
||||||
|
- Non-Goals: 不实现审批引擎、企业微信调用、自动重试调度或新的审批流程。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Decision: 新增独立的 `recoverApproval` API,而不是复用历史 `triggerApproval`。
|
||||||
|
- Rationale: `trigger-approval` 面向没有审批状态的历史记录;恢复接口面向已有审批尝试的同步/结果确认/原事件恢复,两者的幂等语义和审计含义不同。
|
||||||
|
- Decision: 以 `approval_recoverable` 作为按钮展示条件,后端作为最终授权和状态校验方。
|
||||||
|
- Rationale: 前端无法可靠重建“结果未知”和“明确失败”等状态,且状态可能在页面打开后发生变化。
|
||||||
|
- Decision: 恢复接口只接收路径中的退款 ID,响应返回完整退款详情。
|
||||||
|
- Rationale: 防止客户端篡改审批实例、材料或单号;一次响应即可刷新列表/详情所需字段。
|
||||||
|
- Decision: 将 `submission_status`、失败摘要和最近恢复时间作为普通退款响应字段展示。
|
||||||
|
- Rationale: 列表需要快速筛选/判断,详情需要审计和排障;字段由后端脱敏并保持一致。
|
||||||
|
|
||||||
|
## State and Concurrency
|
||||||
|
|
||||||
|
1. 若最新尝试已有 `sp_no`,后端执行原审批单同步。
|
||||||
|
2. 若提交结果未知,后端先确认结果;确认成功或已存在时不得再次提交。
|
||||||
|
3. 若结果明确提交失败,后端恢复原提交事件;不得创建新的尝试或审批实例。
|
||||||
|
4. 若资格在请求期间失效,接口返回状态冲突;前端提示固定文案并刷新详情/列表。
|
||||||
|
5. 后端应保证重复请求幂等,前端按钮在请求期间置为 loading,避免重复点击。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- Risk: 列表数据在恢复完成前已过期。
|
||||||
|
- Mitigation: 成功和状态冲突都强制重新请求后端;不在前端乐观修改审批状态。
|
||||||
|
- Risk: 失败摘要包含凭证或个人信息。
|
||||||
|
- Mitigation: 仅展示后端提供的脱敏 `approval_failure_summary`,前端不拼接原始异常。
|
||||||
|
- Risk: 新入口与旧历史补发入口混淆。
|
||||||
|
- Mitigation: 使用“恢复审批”文案和独立 API,保留两种入口各自的资格条件与说明。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. 扩展退款响应类型与 API service。
|
||||||
|
2. 在列表和详情接入恢复按钮、确认/加载/错误处理和刷新逻辑。
|
||||||
|
3. 增加 API、资格显示、冲突刷新和字段展示测试。
|
||||||
|
4. 与后端联调已有单号、未知结果、明确失败和不可恢复四类状态。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- `submission_status` 的完整枚举值及名称由后端提供,前端是否需要筛选取决于最终列表需求。
|
||||||
|
- 状态冲突错误是否有稳定错误码;在未确定前以前端按错误消息或统一冲突码兼容处理。
|
||||||
|
|
||||||
37
openspec/changes/add-refund-approval-recovery/proposal.md
Normal file
37
openspec/changes/add-refund-approval-recovery/proposal.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Change: 增加退款原审批恢复
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
退款审批提交可能出现企业微信单号已生成但本地结果未知、原提交事件明确失败,或需要重新同步已有审批单的情况。当前退款页面只有历史记录的“补发审批”能力,无法安全地恢复同一审批尝试;运营人员也缺少提交状态、失败摘要和最近恢复时间,难以判断是否可以处理。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 `POST /api/admin/refunds/{id}/recover-approval`,无请求体,调用后恢复原审批提交事件或确认未知结果。
|
||||||
|
- 新增 `RefundService.recoverApproval(id)`,成功响应解析为更新后的退款详情。
|
||||||
|
- 扩展退款列表和详情模型,支持 `submission_status`、`submission_status_name`、`approval_recoverable`、`approval_failure_summary`、`approval_last_recovery_at`。
|
||||||
|
- 在退款列表和详情中,仅当后端返回 `approval_recoverable = true` 且当前账号具备退款管理权限及数据范围时显示“恢复审批”。
|
||||||
|
- 恢复成功后刷新当前列表/详情;遇到状态冲突时提示“当前退款审批状态不允许恢复”并重新加载后端状态。
|
||||||
|
- 恢复动作沿用退款管理权限和数据范围,不新增审批实例、审批尝试、审批材料、审批实例 ID 或企业微信单号输入,也不替代现有历史审批“补发”能力。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs:
|
||||||
|
- `refund-management`
|
||||||
|
- Affected code:
|
||||||
|
- `src/api/modules/refund.ts`
|
||||||
|
- `src/types/api/refund.ts`
|
||||||
|
- `src/views/finance/refund/index.vue`
|
||||||
|
- `src/views/finance/refund/detail.vue`
|
||||||
|
- API contract:
|
||||||
|
- `POST /api/admin/refunds/{id}/recover-approval`
|
||||||
|
- `GET /api/admin/refunds`
|
||||||
|
- `GET /api/admin/refunds/{id}`
|
||||||
|
- Dependencies:
|
||||||
|
- 后端返回 `approval_recoverable` 的最终资格判断,并在服务端保证幂等、并发状态校验和数据范围校验。
|
||||||
|
- 后端成功响应包含完整退款详情,失败响应遵循现有统一错误结构。
|
||||||
|
- Out of scope:
|
||||||
|
- 创建新的审批尝试或审批实例。
|
||||||
|
- 前端直接调用企业微信、传入审批实例 ID、审批材料或 `sp_no`。
|
||||||
|
- 修改退款金额、退款方式、审批结果或渠道退款流程。
|
||||||
|
- 批量恢复、自动轮询恢复或替代 `trigger-approval` 历史审批补发。
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Refund Approval Recovery API
|
||||||
|
|
||||||
|
The refund service SHALL expose `POST /api/admin/refunds/{id}/recover-approval` with no request body. The operation SHALL use the existing refund-management permission and data scope, and SHALL return the updated complete refund record on success.
|
||||||
|
|
||||||
|
#### Scenario: Recover an existing approval submission
|
||||||
|
|
||||||
|
- **GIVEN** a refund record is marked `approval_recoverable = true`
|
||||||
|
- **WHEN** the client posts to `/api/admin/refunds/{id}/recover-approval`
|
||||||
|
- **THEN** the request MUST contain only the refund ID path parameter and no body
|
||||||
|
- **AND** a successful response MUST contain the updated refund detail, including the latest submission and approval fields
|
||||||
|
|
||||||
|
#### Scenario: Reject unauthorized or out-of-scope recovery
|
||||||
|
|
||||||
|
- **GIVEN** the current account lacks refund-management permission or data scope for the refund
|
||||||
|
- **WHEN** the client calls the recovery endpoint
|
||||||
|
- **THEN** the service MUST reject the request using the existing authentication/authorization error contract
|
||||||
|
|
||||||
|
### Requirement: Recovery Preserves the Original Approval Attempt
|
||||||
|
|
||||||
|
The recovery operation SHALL synchronize an existing approval identified by `sp_no`, confirm an unknown submission result before retrying, or restore the original submission event after an explicit submission failure. It MUST NOT create a new approval attempt or approval instance.
|
||||||
|
|
||||||
|
#### Scenario: Existing WeCom approval number
|
||||||
|
|
||||||
|
- **GIVEN** the latest approval attempt already has an `sp_no`
|
||||||
|
- **WHEN** recovery is requested
|
||||||
|
- **THEN** the service MUST trigger synchronization of that original approval
|
||||||
|
- **AND** MUST preserve the existing approval attempt and instance
|
||||||
|
|
||||||
|
#### Scenario: Unknown submission result
|
||||||
|
|
||||||
|
- **GIVEN** the submission result is unknown
|
||||||
|
- **WHEN** recovery is requested
|
||||||
|
- **THEN** the service MUST confirm the submission result before any resubmission
|
||||||
|
- **AND** MUST NOT create a duplicate approval when the original submission is found
|
||||||
|
|
||||||
|
#### Scenario: Explicit submission failure
|
||||||
|
|
||||||
|
- **GIVEN** the original submission is explicitly known to have failed
|
||||||
|
- **WHEN** recovery is requested
|
||||||
|
- **THEN** the service MUST restore the original submission event
|
||||||
|
- **AND** MUST NOT create a new approval attempt or approval instance
|
||||||
|
|
||||||
|
### Requirement: Refund Approval Recovery Fields
|
||||||
|
|
||||||
|
Refund list and detail responses SHALL expose `submission_status`, `submission_status_name`, `approval_recoverable`, `approval_failure_summary`, and `approval_last_recovery_at`. The failure summary MUST be a redacted diagnostic value supplied by the backend.
|
||||||
|
|
||||||
|
#### Scenario: Display recovery metadata
|
||||||
|
|
||||||
|
- **GIVEN** a refund response contains recovery metadata
|
||||||
|
- **WHEN** the list or detail page renders the record
|
||||||
|
- **THEN** it MUST display the submission status name, redacted failure summary when present, and the latest recovery time
|
||||||
|
- **AND** it MUST use `approval_recoverable` from the response without deriving eligibility from stale local fields
|
||||||
|
|
||||||
|
### Requirement: Refund Approval Recovery Entry
|
||||||
|
|
||||||
|
The refund list and detail pages SHALL show a “恢复审批” action only when the backend returns `approval_recoverable = true` and the existing refund-management permission and data scope allow access.
|
||||||
|
|
||||||
|
#### Scenario: Show recovery action for an eligible record
|
||||||
|
|
||||||
|
- **GIVEN** `approval_recoverable` is `true`
|
||||||
|
- **AND** the current account can access the refund under existing permission and data scope
|
||||||
|
- **WHEN** the page renders the record
|
||||||
|
- **THEN** the page MUST show the “恢复审批” action
|
||||||
|
|
||||||
|
#### Scenario: Hide recovery action for an ineligible record
|
||||||
|
|
||||||
|
- **GIVEN** `approval_recoverable` is `false`, absent, or the account cannot access the refund
|
||||||
|
- **WHEN** the page renders the record
|
||||||
|
- **THEN** the page MUST NOT show the “恢复审批” action
|
||||||
|
|
||||||
|
#### Scenario: Recovery succeeds
|
||||||
|
|
||||||
|
- **GIVEN** the user confirms recovery for an eligible record
|
||||||
|
- **WHEN** the recovery endpoint succeeds
|
||||||
|
- **THEN** the page MUST show a success message and reload the list or detail from the backend
|
||||||
|
- **AND** the refreshed record MUST show the latest submission and approval state
|
||||||
|
|
||||||
|
#### Scenario: Recovery conflicts with a concurrent state change
|
||||||
|
|
||||||
|
- **GIVEN** the record becomes non-recoverable before the request is processed
|
||||||
|
- **WHEN** the endpoint returns a status conflict
|
||||||
|
- **THEN** the page MUST show “当前退款审批状态不允许恢复”
|
||||||
|
- **AND** MUST reload the current record instead of applying an optimistic state
|
||||||
|
|
||||||
21
openspec/changes/add-refund-approval-recovery/tasks.md
Normal file
21
openspec/changes/add-refund-approval-recovery/tasks.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## 1. Contract And Types
|
||||||
|
|
||||||
|
- [x] 1.1 在 `Refund` 列表/详情类型中增加 `submission_status`、`submission_status_name`、`approval_recoverable`、`approval_failure_summary`、`approval_last_recovery_at`。
|
||||||
|
- [x] 1.2 在 `RefundService` 增加 `recoverApproval(id)`,调用无请求体的 `POST /api/admin/refunds/{id}/recover-approval` 并返回完整 `Refund`。
|
||||||
|
- [x] 1.3 补充 API 注释和统一错误响应处理,明确不传审批实例 ID、材料或企业微信单号。
|
||||||
|
|
||||||
|
## 2. Refund List And Detail
|
||||||
|
|
||||||
|
- [x] 2.1 列表展示提交状态名称、脱敏失败摘要和最近恢复时间(按页面现有字段展示约定接入)。
|
||||||
|
- [x] 2.2 仅在 `approval_recoverable === true` 且通过现有退款管理权限/数据范围校验时显示“恢复审批”操作。
|
||||||
|
- [x] 2.3 恢复操作增加确认和请求中 loading,成功后刷新列表并展示最新响应数据。
|
||||||
|
- [x] 2.4 详情页展示上述字段和“恢复审批”操作;成功后刷新详情。
|
||||||
|
- [x] 2.5 处理状态冲突:提示“当前退款审批状态不允许恢复”,并刷新当前数据;其他后端错误沿用统一错误提示。
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [x] 3.1 核对已有 `sp_no`、提交结果未知、明确提交失败和不可恢复记录的入口展示与刷新行为。
|
||||||
|
- [x] 3.2 核对无退款权限、越权数据和 `approval_recoverable = false` 时不显示按钮。
|
||||||
|
- [x] 3.3 核对重复点击被 loading 阻止,且请求体为空、不携带审批材料或实例 ID。
|
||||||
|
- [x] 3.4 运行相关类型检查、lint、编码检查和构建。
|
||||||
|
- [x] 3.5 运行 `openspec.cmd validate add-refund-approval-recovery --strict`。
|
||||||
33
openspec/changes/add-self-service-password-change/design.md
Normal file
33
openspec/changes/add-self-service-password-change/design.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
头像菜单位于 `src/components/core/layouts/art-header-bar/index.vue`,当前“个人中心”通过路由跳转,“锁定屏幕”通过事件打开。`AuthService.changePassword` 已封装 `PUT /api/auth/password`,但当前 TypeScript 参数 `ChangePasswordParams` 使用 `oldPassword`、`newPassword`、`confirmPassword`,与 OpenAPI 的 `old_password`、`new_password` 不一致。HTTP 层只序列化 JSON,不会将 camelCase 自动转换成 snake_case。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
- Goals: 提供头像菜单入口和简单的自助修改密码流程;遵循给定 OpenAPI 请求契约;不增加权限编码或权限判断。
|
||||||
|
- Non-Goals: 修改后端接口、增加路由、改变锁屏行为、增加权限管理配置。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Decision: 使用头像菜单中的对话框承载表单,不为此功能新增页面路由。入口依次位于个人中心、修改密码、锁定屏幕。
|
||||||
|
- Decision: 表单收集当前密码、新密码和确认密码;确认密码仅用于客户端一致性校验,提交时仅构造 `{ old_password, new_password }`。
|
||||||
|
- Decision: 将 API DTO 类型调整为精确表达 OpenAPI 的 snake_case 字段,避免依赖 HTTP 层不存在的字段转换。
|
||||||
|
- Decision: 所有已登录用户均可见并可操作该入口,不使用 RBAC、按钮权限或菜单权限配置进行隐藏/拦截;API 请求仍携带现有 Bearer Token。
|
||||||
|
- Decision: 接口成功时清空敏感表单数据、提示成功,再调用 `userStore.clearLocalState()` 清除旧 token 并跳转登录页;接口失败时展示错误反馈,不记录密码值。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 当前类型与接口字段不一致,直接复用旧 camelCase 参数会发送错误字段;以 DTO 对齐及请求验证覆盖此风险。
|
||||||
|
- 密码策略细节未在给定 OpenAPI 中定义;本次不自行新增密码复杂度规则,沿用后端校验结果并反馈错误。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
无需数据迁移或后端变更。前端发布后,已登录用户可从头像菜单使用入口;回滚时可移除菜单项及对话框实现。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- None.
|
||||||
|
|
||||||
|
## Session Handling
|
||||||
|
|
||||||
|
After a successful password change, clear the current client authentication state with `userStore.clearLocalState()` and redirect to the login route. Do not attempt the logout API after changing the password because the old token may already be invalid.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Change: 添加自助修改密码功能
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
当前头像菜单提供个人中心和锁定屏幕入口,但用户无法从该处修改自己的登录密码。认证服务已提供 `PUT /api/auth/password`,需要补齐可发现的前端入口和修改流程。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 在头像菜单的“个人中心”和“锁定屏幕”之间增加“修改密码”入口。
|
||||||
|
- 点击后打开修改密码表单,提交当前密码和新密码至 `PUT /api/auth/password`。
|
||||||
|
- 按 OpenAPI 使用 `old_password`、`new_password` 请求字段;确认密码(如表单提供)只在前端校验,不发送给接口。
|
||||||
|
- 修改密码入口面向所有已登录用户,不新增权限编码、路由权限或按钮权限。
|
||||||
|
- 修改成功后清理本地认证状态并跳转登录页,要求用户使用新密码重新登录。
|
||||||
|
- 为新增文案提供中英文国际化。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `admin-authentication`
|
||||||
|
- Affected code: `src/components/core/layouts/art-header-bar/index.vue`、`src/components/core/layouts/art-header-bar/ChangePasswordDialog.vue`、`src/components/core/layouts/art-header-bar/style.scss`、`src/api/modules/auth.ts`、`src/types/api/auth.ts`、`src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||||
|
- API: 已有 `PUT /api/auth/password`;请求体按 OpenAPI 发送 `old_password` 和 `new_password`,使用当前登录 Bearer Token。
|
||||||
|
- Authorization: 不新增前端权限配置;接口仍按认证 API 的 Bearer 认证要求调用。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Self-Service Password Change Entry
|
||||||
|
|
||||||
|
The authenticated admin interface SHALL provide a password-change entry in the avatar menu between “个人中心” and “锁定屏幕”. The entry SHALL be available to every authenticated user without a frontend route, button, or menu permission check.
|
||||||
|
|
||||||
|
#### Scenario: User opens password-change form from avatar menu
|
||||||
|
|
||||||
|
- **GIVEN** the user is authenticated in the admin interface
|
||||||
|
- **WHEN** the user opens the avatar menu
|
||||||
|
- **THEN** the menu shows “个人中心”, “修改密码”, and “锁定屏幕” in that order
|
||||||
|
- **AND** selecting “修改密码” opens the password-change form
|
||||||
|
- **AND** the entry is not hidden or disabled based on a permission code
|
||||||
|
|
||||||
|
### Requirement: Change Current User Password
|
||||||
|
|
||||||
|
The password-change form SHALL collect the current password and new password, and SHALL submit them through `PUT /api/auth/password` using the current Bearer token. The request body MUST contain exactly the API fields `old_password` and `new_password`; any confirmation value used for client-side validation MUST NOT be sent. Password values MUST NOT be written to application logs.
|
||||||
|
|
||||||
|
#### Scenario: Password change succeeds
|
||||||
|
|
||||||
|
- **GIVEN** the user has entered a current password and matching new-password values
|
||||||
|
- **WHEN** the user submits the form and the API returns success
|
||||||
|
- **THEN** the frontend sends `old_password` and `new_password` to `PUT /api/auth/password`
|
||||||
|
- **AND** the frontend displays success feedback and clears the password form values
|
||||||
|
- **AND** the frontend clears the local authentication state and redirects the user to the login page
|
||||||
|
- **AND** the user must authenticate again with the new password
|
||||||
|
|
||||||
|
#### Scenario: Password change is rejected
|
||||||
|
|
||||||
|
- **GIVEN** the user submits the password-change form
|
||||||
|
- **WHEN** the API rejects the request
|
||||||
|
- **THEN** the frontend displays error feedback and does not report success
|
||||||
|
- **AND** the frontend does not log any password value
|
||||||
13
openspec/changes/add-self-service-password-change/tasks.md
Normal file
13
openspec/changes/add-self-service-password-change/tasks.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
## 1. 头像菜单与修改密码表单
|
||||||
|
|
||||||
|
- [x] 1.1 在头像菜单“个人中心”和“锁定屏幕”之间添加“修改密码”入口,不设置权限判断。
|
||||||
|
- [x] 1.2 实现修改密码表单,收集当前密码、新密码及确认密码,并在前端校验必填项与两次新密码一致。
|
||||||
|
- [x] 1.3 调用认证 API;请求仅包含 `old_password` 和 `new_password`,成功后清空表单并提示结果,失败时显示错误反馈。
|
||||||
|
- [x] 1.4 添加中英文菜单、表单、校验及结果提示文案。
|
||||||
|
|
||||||
|
## 2. API 契约与验证
|
||||||
|
|
||||||
|
- [x] 2.1 对齐 `ChangePasswordParams` 与 `AuthService.changePassword` 的请求 DTO,确保网络请求字段为 OpenAPI 定义的 snake_case,且不发送确认密码。
|
||||||
|
- [x] 2.2 验证菜单顺序、表单校验、成功与失败反馈,以及入口不依赖任何权限编码。
|
||||||
|
- [x] 2.3 运行相关类型检查和构建,处理本次改动引入的问题。
|
||||||
|
- [x] 2.4 密码修改成功后清理本地认证状态并跳转登录页。
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Change: 新增当前用户修改密码弹窗
|
|
||||||
|
|
||||||
状态:用户已确认执行(2026-10-09),按本提案实施。
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
当前顶部用户菜单只有“个人中心”和“锁定屏幕”,缺少当前登录用户修改密码的入口。统一认证接口已有 `PUT /api/auth/password`,但前端参数类型仍使用不符合接口约定的驼峰字段。
|
|
||||||
|
|
||||||
## What Changes
|
|
||||||
|
|
||||||
- 在顶部用户菜单“个人中心”和“锁定屏幕”之间新增“修改密码”,点击后打开弹窗,并关闭用户菜单浮层。
|
|
||||||
- 弹窗包含旧密码、新密码、确认新密码;输入隐藏并允许切换可见,新密码及确认新密码长度为 8–32 位,确认密码必须一致。旧密码必填,不套用新密码长度限制,以兼容已有密码。
|
|
||||||
- 复用 Element Plus 表单、弹窗及主题,提供字段校验、取消、确认、提交中状态和重复提交保护,兼容窄屏及键盘操作。
|
|
||||||
- 复用 `AuthService.changePassword`,修正参数类型,仅发送 `old_password` 和 `new_password`;确认密码不发送给后端,Bearer 认证沿用现有请求层。
|
|
||||||
- 成功后提示并关闭弹窗、清空密码和旧的记住密码凭证;不主动退出登录。失败时保持弹窗,由现有请求层展示后端错误,401 沿用现有认证过期处理。
|
|
||||||
- 补齐中文及英文菜单与表单文案,以及参数映射、密码长度边界、密码一致性、成功/失败与重复提交验证。
|
|
||||||
|
|
||||||
## Impact
|
|
||||||
|
|
||||||
- Affected specs: `user-auth`(当前无已归档的能力规范,本提案新增增量规范)。
|
|
||||||
- Affected code: `src/components/core/layouts/art-header-bar/index.vue`、新增修改密码弹窗组件、`src/types/api/auth.ts`、`src/api/modules/auth.ts`、`src/locales/langs/zh.json`、`src/locales/langs/en.json`。
|
|
||||||
- 不涉及管理员给其他账号重置密码,不修改锁屏密码,不引入新依赖,不调用测试环境真实密码修改接口。
|
|
||||||
- 本次另行完成的资产实名时间修正属于现有展示问题修复:仅指定三个 `real_name_at` 展示位置在现有逻辑基础上加 8 小时,不属于本提案的审批范围。
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
审批后验证 7、8、32、33 位新密码边界、确认密码不一致、旧密码为空、成功及失败响应、重复点击,以及菜单位置与重新打开后的密码清空。运行相关测试、类型检查和受影响文件的代码检查。
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
## ADDED Requirements
|
|
||||||
|
|
||||||
### Requirement: Self-service password menu entry
|
|
||||||
|
|
||||||
系统 SHALL 在顶部用户菜单“个人中心”和“锁定屏幕”之间提供“修改密码”入口,点击后显示当前登录用户的修改密码弹窗,不跳转页面。
|
|
||||||
|
|
||||||
#### Scenario: 打开修改密码弹窗
|
|
||||||
|
|
||||||
- **WHEN** 当前登录用户点击顶部菜单“修改密码”
|
|
||||||
- **THEN** 系统 MUST 关闭用户菜单浮层并显示修改密码弹窗
|
|
||||||
- **AND** 弹窗 MUST 包含旧密码、新密码、确认新密码、取消及确认操作
|
|
||||||
- **AND** 输入 MUST 默认隐藏密码并支持切换可见
|
|
||||||
|
|
||||||
### Requirement: Password form validation
|
|
||||||
|
|
||||||
系统 SHALL 校验旧密码必填、新密码及确认新密码长度为 8–32 位且两次新密码一致,在校验通过前不得发送修改密码请求。系统 MUST NOT 自动裁剪或修改用户输入的密码,也不得对旧密码新增长度限制。
|
|
||||||
|
|
||||||
#### Scenario: 密码长度边界
|
|
||||||
|
|
||||||
- **WHEN** 新密码或确认新密码长度为 7 或 33 位
|
|
||||||
- **THEN** 表单 MUST 提示长度要求并阻止提交
|
|
||||||
- **AND** 长度为 8 或 32 位且其他校验通过时 MUST 允许提交
|
|
||||||
|
|
||||||
#### Scenario: 必填及一致性校验
|
|
||||||
|
|
||||||
- **WHEN** 任一密码字段为空,或者两次新密码不一致
|
|
||||||
- **THEN** 表单 MUST 展示对应字段错误并阻止提交
|
|
||||||
|
|
||||||
### Requirement: Unified authentication password contract
|
|
||||||
|
|
||||||
系统 SHALL 通过现有统一认证请求层调用 `PUT /api/auth/password`,请求体 MUST 仅包含 `old_password` 和 `new_password`,Bearer 认证 MUST 沿用当前登录会话,确认密码 MUST NOT 发送到服务端。
|
|
||||||
|
|
||||||
#### Scenario: 提交有效表单
|
|
||||||
|
|
||||||
- **WHEN** 用户提交通过校验的密码表单
|
|
||||||
- **THEN** 系统 MUST 发送旧密码至 `old_password`、新密码至 `new_password`
|
|
||||||
- **AND** 请求方法 MUST 为 PUT,请求路径 MUST 为 `/api/auth/password`
|
|
||||||
- **AND** 请求 MUST 沿用现有请求层的 Bearer 认证
|
|
||||||
|
|
||||||
### Requirement: Password submission lifecycle
|
|
||||||
|
|
||||||
系统 SHALL 在提交期间提供加载状态并阻止重复提交;成功后提示、关闭弹窗并清空敏感字段及记住密码凭证;普通失败时保留弹窗并展示后端错误;认证过期 MUST 沿用现有请求层处理。成功后系统 MUST NOT 主动退出登录或改变锁屏密码。
|
|
||||||
|
|
||||||
#### Scenario: 重复点击提交
|
|
||||||
|
|
||||||
- **WHEN** 修改密码请求尚未完成时用户再次确认
|
|
||||||
- **THEN** 系统 MUST 保持加载状态且不得发送重复请求
|
|
||||||
|
|
||||||
#### Scenario: 修改成功
|
|
||||||
|
|
||||||
- **WHEN** 修改密码请求成功
|
|
||||||
- **THEN** 系统 MUST 提示成功并关闭弹窗
|
|
||||||
- **AND** 系统 MUST 清空全部密码字段及保存的旧登录凭证
|
|
||||||
- **AND** 系统 MUST 保留当前登录状态,不修改锁屏密码
|
|
||||||
|
|
||||||
#### Scenario: 修改失败
|
|
||||||
|
|
||||||
- **WHEN** 接口返回普通错误,如旧密码错误或服务器错误
|
|
||||||
- **THEN** 系统 MUST 通过现有请求层展示错误并保留弹窗
|
|
||||||
- **AND** 系统 MUST 解除提交中状态以允许用户更正后重试
|
|
||||||
|
|
||||||
#### Scenario: 取消后重新打开
|
|
||||||
|
|
||||||
- **WHEN** 用户取消或关闭弹窗后重新打开
|
|
||||||
- **THEN** 所有密码字段及校验状态 MUST 为空
|
|
||||||
|
|
||||||
### Requirement: Password dialog interface consistency
|
|
||||||
|
|
||||||
修改密码弹窗 SHALL 复用现有 Element Plus 组件和主题,支持中英文、键盘操作及窄屏展示,密码输入 MUST 具备可识别标签和可见校验反馈。
|
|
||||||
|
|
||||||
#### Scenario: 窄屏及键盘操作
|
|
||||||
|
|
||||||
- **WHEN** 用户在窄屏设备或通过键盘打开弹窗
|
|
||||||
- **THEN** 弹窗 MUST 不产生水平溢出,且可访问全部输入、取消及确认操作
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
## 1. 审批
|
|
||||||
|
|
||||||
- [x] 1.1 用户确认修改密码弹窗提案后开始实现。
|
|
||||||
|
|
||||||
## 2. 实现
|
|
||||||
|
|
||||||
- [x] 2.1 修正 `ChangePasswordParams`,仅包含 `old_password` 和 `new_password`,复用 `PUT /api/auth/password`。
|
|
||||||
- [x] 2.2 新增修改密码弹窗:旧密码必填、新密码及确认密码 8–32 位、两次新密码一致,支持密码显隐。
|
|
||||||
- [x] 2.3 加入加载状态、重复提交保护、成功提示及关闭、关闭后清空密码;成功时清除旧的记住密码凭证。
|
|
||||||
- [x] 2.4 在“个人中心”和“锁定屏幕”之间增加入口,关闭菜单浮层后显示弹窗,补充中英文文案。
|
|
||||||
|
|
||||||
## 3. 验证
|
|
||||||
|
|
||||||
- [x] 3.1 验证密码长度 7、8、32、33 位边界、必填项和确认密码不一致。
|
|
||||||
- [x] 3.2 验证请求方法、路径、Bearer 认证及下划线参数,确认请求不含确认密码。
|
|
||||||
- [x] 3.3 验证成功、失败、认证过期、重复提交和重新打开弹窗的状态清理。
|
|
||||||
- [x] 3.4 运行相关测试、类型检查及受影响文件代码检查,检查菜单顺序、键盘操作和窄屏布局。
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-11-07
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
- **后端(8-11,不改)**:接口和字段见 `junhong_cmp_fiber/docs/2026-11-06-测试环境8-11新功能前端对接与权限现状.md` 第 2.9、2.11、2.12 节,以 8-11 生成的 `admin-openapi.yaml` 为准。
|
||||||
|
- **账号类型限制**(后端只按账号类型校验,没有对应权限码):
|
||||||
|
- 记录查询接口不传资产标识时查全部,仅平台账号可用。
|
||||||
|
- 资产详情的采集记录、冲正查询与提交,企业账号无权调用。
|
||||||
|
- 退款后处理人工重试、尝试记录查询,仅平台账号(含超级管理员)可用。
|
||||||
|
- 人工重试只接受 `asset_post_status = 3`(需人工处理)且资产后处理未完成的退款单。
|
||||||
|
- **管理后台现状(`iteration/8-11`)**:
|
||||||
|
- 菜单为静态路由(`src/router/routes/asyncRoutes.ts`),用 `meta.permissions` 过滤;按钮用 `useAuth().hasAuth()`;超级管理员不受限。
|
||||||
|
- 列表页通用组件:`ArtSearchBar`、`ArtTableHeader`、`ArtTableFullScreen`。
|
||||||
|
- 可复用的模式是 `components/business/gateway/StopResumeRecords.vue`:同一个组件既作为独立页(`views/polling-management/stop-resume-records`),也嵌在资产详情里(传入卡 ID)。
|
||||||
|
- 资产详情:
|
||||||
|
- `views/asset-management/asset-information` 用 `BasicInfoCard` 触发停复机记录、`PackageListCard` 触发日流量详单(`DailyRecordsDialog`)。
|
||||||
|
- 同步按钮走 `useAssetInfo.refreshAsset`,目前忽略响应体。
|
||||||
|
- 退款:`views/finance/refund/index.vue`、`detail.vue`,接口在 `api/modules/refund.ts`,类型在 `types/api/refund.ts`。
|
||||||
|
- **约束**:基于 `iteration/8-11`,只部署测试环境;不改停复机逻辑;网关只读开关保持开启。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 运营能在后台查清楚"一张卡的流量读数从采集到入账"的全过程,并能人工冲正。
|
||||||
|
- 退款后处理卡住时,运营能看到原因并人工重试。
|
||||||
|
- 新页面和按钮有明确的权限码,可以分配给非超级管理员角色。
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 后端接口或权限校验改动。
|
||||||
|
- 采集记录导出、归档文件下载(后端未提供)。
|
||||||
|
- 生产发布与生产权限数据。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1 记录列表做成可复用组件,页面只是壳
|
||||||
|
|
||||||
|
参照 `StopResumeRecords.vue`,在 `src/components/business/traffic-records/` 下新建组件:
|
||||||
|
|
||||||
|
- `TrafficCollectionRecords.vue`:成功记录;传 `failures` 时切到失败记录。
|
||||||
|
- `CardDailyUsages.vue`
|
||||||
|
- `PackageUsageDailyRecords.vue`
|
||||||
|
- `PackageTrafficAdjustments.vue`
|
||||||
|
- `RefundPostProcessAttempts.vue`
|
||||||
|
|
||||||
|
每个组件接受可选的固定筛选项,例如 `identifier`、`packageUsageId`、`refundId`。独立页面不传,资产详情和退款详情传入,复用同一套列表与原始数据展开。
|
||||||
|
|
||||||
|
**备选**:页面与弹窗各写一套。**拒绝原因**:字段多(约 30 个),两套容易不一致。
|
||||||
|
|
||||||
|
### D2 菜单位置
|
||||||
|
|
||||||
|
| 页面 | 菜单位置 |
|
||||||
|
|---|---|
|
||||||
|
| 流量采集成功记录、流量采集失败记录、卡日流量、套餐日流量详单、套餐流量冲正流水 | 资产管理 → 记录管理(与"资产分配""授权记录"并列) |
|
||||||
|
| 退款后处理尝试记录 | 财务管理(紧跟"退款管理") |
|
||||||
|
|
||||||
|
路由别名在 `routesAlias.ts` 注册,页面目录分别为 `views/asset-management/record-management/<name>/index.vue` 与 `views/finance/refund-post-attempts/index.vue`。
|
||||||
|
|
||||||
|
### D3 权限码
|
||||||
|
|
||||||
|
后端没有这些权限码,前端新增常量文件 `src/config/constants/trafficRecords.ts`:
|
||||||
|
|
||||||
|
| 用途 | 权限码 |
|
||||||
|
|---|---|
|
||||||
|
| 采集成功记录页 | `traffic_collection_record:page` |
|
||||||
|
| 采集失败记录页 | `traffic_collection_failure:page` |
|
||||||
|
| 卡日流量页 | `card_daily_usage:page` |
|
||||||
|
| 套餐日流量详单页 | `package_usage_daily_record:page` |
|
||||||
|
| 冲正流水页 | `package_traffic_adjustment:page` |
|
||||||
|
| 退款后处理尝试记录页 | `refund_post_process_attempt:page` |
|
||||||
|
| 资产详情 · 采集记录(卡 / 设备绑定卡) | `iot_info:traffic_collections`、`device_info:traffic_collections` |
|
||||||
|
| 资产详情 · 冲正记录 | `package_usage:traffic_adjustments` |
|
||||||
|
| 资产详情 · 人工冲正 | `package_usage:traffic_adjust` |
|
||||||
|
| 退款 · 人工重试 | `refund:asset_post_retry` |
|
||||||
|
|
||||||
|
- 菜单用 `meta.permissions`,按钮用 `hasAuth`。
|
||||||
|
- 前端还要按账号类型隐藏后端会拒绝的入口:企业账号不显示资产详情的采集记录和冲正;非平台账号不显示人工重试和尝试记录页。
|
||||||
|
- 清单交后端写入测试库 `tb_permission` 并挂到对应菜单。权限码命名最终以写入测试库的数据为准。
|
||||||
|
|
||||||
|
### D4 时间范围与默认值
|
||||||
|
|
||||||
|
- 所有列表默认最近 1 天(日期类默认最近 7 天)。
|
||||||
|
- 选择器限制跨度 ≤ 31 天,提交前再校验一次。
|
||||||
|
- 时间参数按上海时间 `YYYY-MM-DD HH:mm:ss` 传,后端同时接受 RFC3339。
|
||||||
|
- 结束时间按"不含"处理,日期类按"含首尾"处理,筛选栏旁用文字说明。
|
||||||
|
|
||||||
|
### D5 枚举中文名
|
||||||
|
|
||||||
|
在 `src/config/constants/trafficRecords.ts` 统一维护以下中文名,未知值原样显示:
|
||||||
|
|
||||||
|
| 枚举 | 中文名 |
|
||||||
|
|---|---|
|
||||||
|
| `decision` | written 已写入、baseline 建立基线、queue_not_full 采集队列未满、slope_negative 读数回落、out_of_order 采集乱序、settled_existing 已结算 |
|
||||||
|
| `result` | ok 正常、failed 失败、rate_limited 限流、invalid_payload 响应无效、no_reading 无读数、invalid_reading 读数无效 |
|
||||||
|
| `source` | polling 轮询、manual_sync 人工同步、business_event 业务事件 |
|
||||||
|
| 退款后处理 `step` | load 加载、invalidate 失效套餐、activate 激活、check_main 检查主套餐、stop 停机、mark 标记完成、done 完成 |
|
||||||
|
| 退款后处理 `trigger` | outbox 自动、manual 人工 |
|
||||||
|
| `asset_post_status` | 0 未开始、1 待重试、2 已完成、3 需人工处理 |
|
||||||
|
|
||||||
|
### D6 原始数据展示
|
||||||
|
|
||||||
|
- 表格行展开后,用只读 JSON 块(`<pre>` 格式化)展示 `queue_points`、`raw_request`、`raw_response`。
|
||||||
|
- `raw_response` 为空、`raw_response_text` 有值时,显示文本并标注"已截断"。
|
||||||
|
- 每块提供"复制"按钮,方便贴给网关方排查。
|
||||||
|
|
||||||
|
### D7 人工冲正交互
|
||||||
|
|
||||||
|
- 表单只让运营填正整数加方向,由前端换算 `delta_mb` 的正负,避免运营手输负号出错。
|
||||||
|
- 确认框展示当前已用量(取套餐行的已用量)、调整量与预计结果。
|
||||||
|
- 退回不能使结果小于 0。
|
||||||
|
- 成功后刷新套餐列表,并展示返回的 `before_usage_mb → after_usage_mb`。
|
||||||
|
- 提示文案说明"提交后系统会按规则重新判断停复机",前端不调用任何停复机接口。
|
||||||
|
|
||||||
|
### D8 人工重试交互
|
||||||
|
|
||||||
|
- 按钮只在"需人工处理 + 平台账号 + 有权限"时出现。
|
||||||
|
- 成功时接口返回最新退款详情,直接替换当前行或详情数据。
|
||||||
|
- 失败时显示后端 `msg`,并重新拉取该退款单,更新尝试次数和最后错误。
|
||||||
|
|
||||||
|
### D9 同步结果(可选)
|
||||||
|
|
||||||
|
- `AssetService.refreshAsset` 的响应类型增加 `traffic_refresh`。
|
||||||
|
- `useAssetInfo.refreshAsset` 在成功提示里附加一行入账结果,或用 `ElNotification` 展示。
|
||||||
|
- 不改同步按钮的限流和刷新逻辑。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **[权限码未入库,非超级管理员看不到新功能]** → 测试先用超级管理员验证。按 D3 清单补测试库后,再用普通运营账号验证。
|
||||||
|
- **[人工冲正、人工重试会触发后端停复机判断]** → 测试环境资产已全部在网关只读表,停复机调用会被拦截,不会改上游卡状态。验证只看停复机指令和拦截日志,不看上游卡状态。
|
||||||
|
- **[采集记录字段多,表格过宽]** → 默认只显示关键列,其余列通过 `ArtTableHeader` 列设置打开;原始数据放展开行。
|
||||||
|
- **[采集记录只保留当月加最近 2 个整月,资产详情采集记录保留 90 天]** → 筛选栏说明保留期,超出保留期查询为空属正常。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. 在 `iteration/8-11` 实现。本地 `build:development`(接测试后端)通过类型检查与构建。
|
||||||
|
2. 后端在测试库补 D3 权限码,并分配给测试角色。
|
||||||
|
3. 推送 `iteration/8-11` 部署测试环境。管理后台 CI 要求推送前在本机构建镜像并推送到 `registry.boss160.cn`,需先 `docker login`;部署时间由你安排。
|
||||||
|
4. 用超级管理员和普通运营账号分别验证。
|
||||||
|
5. 回退:回滚对应提交重新部署;新增页面不影响现有页面。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- 权限码命名(D3)是否沿用此清单,还是由后端统一定名后回填前端常量?默认先用此清单。
|
||||||
|
- 记录查询菜单是否放在"资产管理 → 记录管理",还是放到"轮询管理"下与停复机记录并列?默认放"记录管理"。
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
后端 8-11 迭代在 11-05 新增了流量采集记录分表与记录查询接口、套餐流量人工冲正、退款资产后处理逐次尝试与重试上限,10-29 起资产刷新接口还会返回本次读数是否入账(`traffic_refresh`)。管理后台目前都没有接上:运营看不到采集成功/失败记录和卡日流量,无法人工冲正套餐流量;退款后处理失败到达重试上限后,后台既看不到状态也不能人工重试。依据 `junhong_cmp_fiber/docs/2026-11-06-测试环境8-11新功能前端对接与权限现状.md` 第 2.9、2.11、2.12、5.2 节补齐管理后台。
|
||||||
|
|
||||||
|
本变更基于 `iteration/8-11` 分支,只部署测试环境。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 6 个记录查询列表页:运营商流量采集成功记录、采集失败记录、卡日流量、套餐日流量详单、套餐流量冲正流水、退款资产后处理尝试记录。
|
||||||
|
- 资产详情新增"采集记录"(按卡,可包含失败记录)。
|
||||||
|
- 资产详情套餐列表新增"冲正记录"查看与"人工冲正"(正数补扣、负数退回,原因必填)。
|
||||||
|
- 退款列表、详情展示资产后处理状态、已尝试次数、最后错误、下次自动重试时间;列表支持按后处理状态筛选;"需人工处理"的退款单提供"人工重试"按钮与尝试记录入口。
|
||||||
|
- 可选:资产"同步"成功后展示本次读数、读数结果、是否写入、写入判断、增量。
|
||||||
|
- 新增上述页面与按钮的前端权限码常量,并整理清单供测试库补权限数据。
|
||||||
|
- 不改停复机逻辑:人工冲正、人工重试会由后端按规则触发停复机判断,前端只调用接口;测试环境资产已全部在网关只读表中,不会真实停复机。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `traffic-record-query`:采集成功/失败记录、卡日流量、套餐日流量详单、冲正流水 4 类全局查询页,以及资产详情中的卡采集记录。
|
||||||
|
- `package-traffic-adjustment`:资产详情中按套餐查看冲正流水与提交人工冲正。
|
||||||
|
- `refund-asset-post-processing`:退款后处理状态展示与筛选、人工重试、后处理尝试记录查询页。
|
||||||
|
- `asset-refresh-traffic-result`:资产同步后展示 `traffic_refresh` 入账结果(可选)。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
(无。本仓库 `openspec/specs/` 下没有已有规格。)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- 接口模块:新增 `src/api/modules/trafficRecords.ts`;`refund.ts` 增加人工重试与尝试记录;`asset.ts` 增加采集记录、冲正查询与提交,`AssetRefreshResponse` 类型增加 `traffic_refresh`。
|
||||||
|
- 类型:`src/types/api/refund.ts` 的 `Refund` 与 `RefundQueryParams` 增加 `asset_post_*` 字段;新增记录查询类型。
|
||||||
|
- 页面:新增记录查询页(`src/views/asset-management/record-management/*`、`src/views/finance/refund-post-attempts`);修改 `src/views/finance/refund/index.vue`、`detail.vue`、`src/views/asset-management/asset-information`(采集记录、冲正)。
|
||||||
|
- 路由与权限:`src/router/routes/asyncRoutes.ts`、`routesAlias.ts`,新增权限常量文件;菜单与按钮权限码需在测试库 `tb_permission` 补数据,否则只有超级管理员可见。
|
||||||
|
- 后端接口(8-11 已提供,不改):`/api/admin/traffic-collections`、`/traffic-collection-failures`、`/card-daily-usages`、`/package-usage-daily-records`、`/package-traffic-adjustments`、`/refund-post-process-attempts`、`/assets/{identifier}/traffic-collections`、`/assets/{identifier}/packages/{package_usage_id}/traffic-adjustments`(GET/POST)、`POST /refunds/{id}/asset-post-processing/retry`、`POST /assets/{identifier}/refresh`。
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 同步后展示流量入账结果
|
||||||
|
|
||||||
|
资产详情点击"同步"成功后,若响应包含 `traffic_refresh`,系统 SHALL 在成功提示中或提示下方展示本次流量入账结果:读数结果(中文名称)、本次运营商读数 `reading_mb`(为空时显示"未返回读数")、是否写入 `written`、写入判断 `decision`(中文名称)、本次增量 `delta_mb`。响应不含 `traffic_refresh` 时 MUST 保持现有"刷新成功"提示不变。本需求为可选项,不实现不影响现有功能。
|
||||||
|
|
||||||
|
#### Scenario: 读数已写入
|
||||||
|
- **WHEN** 同步返回 `traffic_refresh = { result: ok, reading_mb: 1200, written: true, decision: written, delta_mb: 15 }`
|
||||||
|
- **THEN** 页面显示"本次读数 1200 MB,已写入,增量 15 MB"
|
||||||
|
|
||||||
|
#### Scenario: 读数未写入
|
||||||
|
- **WHEN** 同步返回 `written = false`、`decision = queue_not_full`
|
||||||
|
- **THEN** 页面显示"本次读数未写入:采集队列未满"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 查看套餐冲正流水
|
||||||
|
|
||||||
|
资产详情套餐列表中每笔套餐 SHALL 提供"冲正记录"入口,弹窗调用 `GET /api/admin/assets/{identifier}/packages/{package_usage_id}/traffic-adjustments` 分页展示该套餐的冲正流水:冲正时间、调整量、调整前后已用量、调整前后套餐状态、原因、操作人、关联流量事件 ID 与采集 ID。企业账号 MUST NOT 显示该入口。
|
||||||
|
|
||||||
|
#### Scenario: 查看冲正记录
|
||||||
|
- **WHEN** 运营在资产详情某笔套餐上点击"冲正记录"
|
||||||
|
- **THEN** 弹窗按时间倒序显示该套餐的冲正流水,无记录时显示空状态
|
||||||
|
|
||||||
|
### Requirement: 提交人工冲正
|
||||||
|
|
||||||
|
有冲正按钮权限的账号 SHALL 能在资产详情套餐上提交人工冲正,调用 `POST /api/admin/assets/{identifier}/packages/{package_usage_id}/traffic-adjustments`。表单字段:调整方向(补扣 / 退回)、调整量 MB(正整数,提交时按方向转为正数或负数 `delta_mb`)、原因(必填,去首尾空格后不能为空)、关联采集 ID 与关联流量事件 ID(选填)。提交前 SHALL 二次确认,确认框展示当前已用量、调整量与预计调整后已用量,并提示"提交后系统会按规则重新判断停复机"。退回量大于当前已用量时 SHALL 提示调整后不能小于 0 并阻止提交。提交中 MUST 防重复。成功后 SHALL 提示调整前后已用量,并刷新套餐列表与冲正记录。
|
||||||
|
|
||||||
|
#### Scenario: 补扣
|
||||||
|
- **WHEN** 运营对已用 500 MB 的套餐选择补扣 100 MB、填写原因并确认
|
||||||
|
- **THEN** 系统提交 `delta_mb = 100`,成功后提示已用量 500 MB → 600 MB,套餐列表刷新
|
||||||
|
|
||||||
|
#### Scenario: 退回超过已用量
|
||||||
|
- **WHEN** 运营对已用 50 MB 的套餐选择退回 80 MB
|
||||||
|
- **THEN** 页面提示调整后已用量不能小于 0,不提交
|
||||||
|
|
||||||
|
#### Scenario: 原因为空
|
||||||
|
- **WHEN** 运营未填写原因直接提交
|
||||||
|
- **THEN** 表单提示原因必填,不发请求
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 退款后处理状态展示
|
||||||
|
|
||||||
|
退款列表与退款详情 SHALL 展示资产后处理状态 `asset_post_status`(0 未开始、1 待重试、2 已完成、3 需人工处理),列表用标签展示,"需人工处理"用警示色。详情 SHALL 另外展示已尝试次数 `asset_post_attempts`(注明自动重试上限 10 次)、最后一次错误 `asset_post_last_error`、下次自动重试时间 `asset_post_next_at`(仅待重试时有值)。字段缺失时显示"-",不影响现有展示。
|
||||||
|
|
||||||
|
#### Scenario: 待重试
|
||||||
|
- **WHEN** 一笔退款 `asset_post_status = 1`、已尝试 3 次、下次重试时间为 10:30
|
||||||
|
- **THEN** 详情显示"待重试"、已尝试 3/10 次、最后错误与下次重试时间 10:30
|
||||||
|
|
||||||
|
### Requirement: 按后处理状态筛选
|
||||||
|
|
||||||
|
退款列表 SHALL 增加"资产后处理状态"筛选项,选中后请求带 `asset_post_status`;清空时不传该参数。
|
||||||
|
|
||||||
|
#### Scenario: 筛选需人工处理
|
||||||
|
- **WHEN** 运营选择"需人工处理"并查询
|
||||||
|
- **THEN** 请求带 `asset_post_status = 3`,列表只显示需人工处理的退款单
|
||||||
|
|
||||||
|
### Requirement: 人工重试后处理
|
||||||
|
|
||||||
|
退款单 `asset_post_status = 3` 且当前账号为平台账号或超级管理员、并有人工重试按钮权限时,退款列表操作列与详情页 SHALL 显示"人工重试"按钮;其他状态或账号类型 MUST NOT 显示。点击后 SHALL 二次确认,提示"重试会重新执行退款后的资产处理(可能包括停机),确认继续?"。调用 `POST /api/admin/refunds/{id}/asset-post-processing/retry`,提交中 MUST 防重复。成功时 SHALL 用返回的最新退款详情刷新当前行或详情;失败时 SHALL 显示后端错误信息(如"资产后处理仍未完成:…"),并重新加载该退款单以展示最新尝试次数与错误。
|
||||||
|
|
||||||
|
#### Scenario: 重试成功
|
||||||
|
- **WHEN** 运营对需人工处理的退款单点击"人工重试"并确认,后端返回成功
|
||||||
|
- **THEN** 页面提示重试成功,该退款单后处理状态显示"已完成",按钮消失
|
||||||
|
|
||||||
|
#### Scenario: 重试仍失败
|
||||||
|
- **WHEN** 后端返回"资产后处理仍未完成:停机失败"
|
||||||
|
- **THEN** 页面显示该错误,重新加载后仍显示"需人工处理"和最新错误
|
||||||
|
|
||||||
|
#### Scenario: 非需人工处理
|
||||||
|
- **WHEN** 退款单后处理状态为"待重试"
|
||||||
|
- **THEN** 不显示"人工重试"按钮
|
||||||
|
|
||||||
|
### Requirement: 后处理尝试记录查询
|
||||||
|
|
||||||
|
系统 SHALL 提供"退款后处理尝试记录"列表页,调用 `GET /api/admin/refund-post-process-attempts`,仅平台账号与超级管理员可用。筛选条件:时间范围(必填,跨度不超过 31 天)、退款单 ID 或单号、资产标识、处理步骤 `step`、结果 `result`、错误码、触发方式 `trigger`(自动 / 人工)。列表 SHALL 展示记录时间、退款单号、资产类型与 ICCID、第几次尝试、步骤、结果、错误码与错误信息、触发方式、耗时;步骤、结果、触发方式显示中文名称。退款详情 SHALL 提供"后处理尝试记录"入口,打开该列表并自动带上退款单 ID。
|
||||||
|
|
||||||
|
#### Scenario: 从退款详情查看
|
||||||
|
- **WHEN** 运营在退款详情点击"后处理尝试记录"
|
||||||
|
- **THEN** 打开尝试记录列表,已按该退款单 ID 筛选,时间范围为最近 31 天
|
||||||
|
|
||||||
|
#### Scenario: 时间范围必填
|
||||||
|
- **WHEN** 运营清空时间范围后查询
|
||||||
|
- **THEN** 页面提示请选择时间范围,不发请求
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 采集成功记录查询
|
||||||
|
|
||||||
|
系统 SHALL 提供"流量采集成功记录"列表页,调用 `GET /api/admin/traffic-collections`。筛选条件:资产标识、时间范围(`start_time`~`end_time`,结束时间不含)、运营商账户、写入判断 `decision`、来源 `source`、场景 `scene`、是否有增量 `has_delta`、是否触发重置日防旧值 `stale_guarded`。列表 SHALL 展示采集时间、ICCID 或卡 ID、运营商账户、来源、场景、运营商原始读数、本月累计、本次增量、写入判断、防旧值、网关 code 与 trace_id、网关尝试次数,并可展开查看采集队列 `queue_points`、原始请求 `raw_request`、原始响应 `raw_response` / `raw_response_text`(截断时标注)。写入判断、来源、读数结果 SHALL 显示中文名称。分页每页默认 20、最大 100。
|
||||||
|
|
||||||
|
#### Scenario: 默认查询
|
||||||
|
- **WHEN** 运营打开采集成功记录页
|
||||||
|
- **THEN** 默认时间范围为最近 1 天,按采集时间倒序展示第一页
|
||||||
|
|
||||||
|
#### Scenario: 查看原始数据
|
||||||
|
- **WHEN** 运营展开一条 `raw_truncated = true` 的记录
|
||||||
|
- **THEN** 页面显示 `raw_response_text` 并标注"原始响应已截断"
|
||||||
|
|
||||||
|
### Requirement: 采集失败记录查询
|
||||||
|
|
||||||
|
系统 SHALL 提供"流量采集失败记录"列表页,调用 `GET /api/admin/traffic-collection-failures`。筛选条件:资产标识、时间范围、运营商账户、失败类型 `result`、错误类别 `error_category`、上游返回码 `provider_code`、来源、场景。列表 SHALL 展示采集时间、ICCID、运营商账户、失败类型、错误类别、HTTP 状态、上游返回码与信息、网关 code/msg/trace_id、耗时、尝试次数,并可展开原始请求与响应。
|
||||||
|
|
||||||
|
#### Scenario: 按失败类型筛选
|
||||||
|
- **WHEN** 运营选择失败类型"限流"并查询
|
||||||
|
- **THEN** 列表只显示 `result = rate_limited` 的记录
|
||||||
|
|
||||||
|
### Requirement: 卡日流量查询
|
||||||
|
|
||||||
|
系统 SHALL 提供"卡日流量"列表页,调用 `GET /api/admin/card-daily-usages`。筛选条件:资产标识、日期范围(`start_date`~`end_date`,含首尾)、运营商账户、当日用量下限 `min_usage_mb`。列表 SHALL 展示日期、ICCID、运营商账户、当日用量 `settled_mb`、当日结束时本月累计、最后一次运营商原始读数、当天写入次数 `obs_count`、是否触发防旧值、更新时间。
|
||||||
|
|
||||||
|
#### Scenario: 排查异常大流量
|
||||||
|
- **WHEN** 运营输入当日用量下限 1024 MB 并查询
|
||||||
|
- **THEN** 列表只显示当日用量不低于 1024 MB 的卡日记录
|
||||||
|
|
||||||
|
### Requirement: 套餐日流量详单查询
|
||||||
|
|
||||||
|
系统 SHALL 提供"套餐日流量详单"列表页,调用 `GET /api/admin/package-usage-daily-records`。筛选条件:资产标识、日期范围、套餐使用记录 ID、套餐 ID、运营商账户。列表 SHALL 展示日期、套餐使用记录 ID、套餐 ID、卡 ID 或设备 ID、当日流量、截止当日累计、更新时间。
|
||||||
|
|
||||||
|
#### Scenario: 按套餐使用记录查询
|
||||||
|
- **WHEN** 运营输入套餐使用记录 ID 并查询
|
||||||
|
- **THEN** 列表只显示该套餐的日流量
|
||||||
|
|
||||||
|
### Requirement: 冲正流水查询
|
||||||
|
|
||||||
|
系统 SHALL 提供"套餐流量冲正流水"列表页,调用 `GET /api/admin/package-traffic-adjustments`。筛选条件:资产标识、时间范围、套餐使用记录 ID、操作人账号 ID、调整方向 `direction`(补扣 / 退回)。列表 SHALL 展示冲正时间、套餐使用记录 ID、卡 ID 或设备 ID、调整量(正数补扣、负数退回)、调整前后已用量、调整前后套餐状态、原因、操作人、关联流量事件 ID 与采集 ID。
|
||||||
|
|
||||||
|
#### Scenario: 只看退回
|
||||||
|
- **WHEN** 运营选择调整方向"退回"
|
||||||
|
- **THEN** 列表只显示调整量为负数的流水
|
||||||
|
|
||||||
|
### Requirement: 查询范围与账号限制
|
||||||
|
|
||||||
|
所有记录查询页的时间或日期跨度 MUST NOT 超过 31 天,超出时 SHALL 在提交前提示并阻止查询。不填资产标识查询全部只允许平台账号与超级管理员:其他账号类型 SHALL 要求填写资产标识后才能查询。后端返回无权限时 SHALL 显示后端提示。
|
||||||
|
|
||||||
|
#### Scenario: 跨度超限
|
||||||
|
- **WHEN** 运营选择 2026-10-01 至 2026-11-06 并查询
|
||||||
|
- **THEN** 页面提示时间跨度不能超过 31 天,不发请求
|
||||||
|
|
||||||
|
#### Scenario: 代理账号不填资产
|
||||||
|
- **WHEN** 代理账号不填资产标识直接查询
|
||||||
|
- **THEN** 页面提示请先输入资产标识
|
||||||
|
|
||||||
|
### Requirement: 资产详情采集记录
|
||||||
|
|
||||||
|
资产详情 SHALL 为卡(含设备绑定的卡)提供"采集记录"入口,弹窗调用 `GET /api/admin/assets/{identifier}/traffic-collections`,默认最近 1 天、每页 50、不含失败记录;可勾选"包含失败记录"(`include_failures = true`),并可按写入判断、来源、场景筛选。列表 SHALL 用 `record_kind` 区分成功与失败记录,成功记录展示读数、累计、增量、写入判断,失败记录展示失败类型、错误类别与上游返回;均可展开原始数据。企业账号 MUST NOT 显示该入口。
|
||||||
|
|
||||||
|
#### Scenario: 包含失败记录
|
||||||
|
- **WHEN** 运营在卡详情打开采集记录并勾选"包含失败记录"
|
||||||
|
- **THEN** 列表同时显示成功与失败记录,失败记录有明显标识
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user