feat: 后台订单按平台、代理商、在线支付拆分列表与导出
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m34s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m34s
- 新增 GET /orders/platform、/orders/agent、/orders/online,类别由路径固定,参数、响应、权限同原订单列表 - 新增导出场景 order_platform、order_agent、order_online,筛选与表头同 order - 类别按钱从哪来划分:客户端下单为在线支付;后台代理钱包扣款(含平台代扣)为代理商;其余后台订单为平台 - 列表与导出共用 internal/query/ordercategory 的类别条件 - 企业账号访问订单列表、详情、购买预检改为无权限,修复其可看到全部订单的问题 - 原订单列表与 order 导出场景保留不变 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
242
docs/engineering/后台订单按类别拆分列表技术方案.md
Normal file
242
docs/engineering/后台订单按类别拆分列表技术方案.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 后台订单按类别拆分列表技术方案
|
||||
|
||||
最后更新:2026-10-26
|
||||
状态:技术方案(未实现)。已定事项与后续事项见第六部分。
|
||||
|
||||
文档结构:
|
||||
- 第一部分:现状逻辑(As-Is)
|
||||
- 第二部分:老卡管对照
|
||||
- 第三部分:方案(To-Be)
|
||||
- 第四部分:改动范围
|
||||
- 第五部分:验证
|
||||
- 第六部分:已定事项与后续事项
|
||||
|
||||
---
|
||||
|
||||
## 第一部分:现状逻辑(As-Is)
|
||||
|
||||
### 1. 接口
|
||||
|
||||
| 接口 | 实现 | 说明 |
|
||||
|---|---|---|
|
||||
| `GET /api/admin/orders` | `OrderHandler.List`(`internal/handler/admin/order.go:90`) | 订单列表,平台、代理商、在线支付三类订单混在一起返回 |
|
||||
| `GET /api/admin/orders/:id` | `OrderHandler.Get` | 订单详情 |
|
||||
| `POST /api/admin/orders` | `OrderHandler.Create` | 后台下单 |
|
||||
| `POST /api/admin/orders/:id/cancel` | `OrderHandler.Cancel` | 取消订单,仅代理账号 |
|
||||
| `POST /api/admin/orders/purchase-check` | `OrderHandler.PurchaseCheck` | 套餐购买预检 |
|
||||
| `POST /api/admin/export-tasks`(`scene=order`) | `OrderDataSource`(`internal/exporter/order_scene.go`) | 订单导出,与列表一样三类混在一起 |
|
||||
|
||||
路由在 `internal/routes/order.go`,`GET /orders` 在第 22 行,`GET /orders/:id` 在第 30 行。
|
||||
|
||||
### 2. 列表的权限与数据范围
|
||||
|
||||
两层条件,全部在服务端决定,前端不能改:
|
||||
|
||||
1. Handler 层:代理账号固定买家条件 `buyer_type = agent AND buyer_id = 当前店铺`;平台账号与超管不加买家条件。
|
||||
2. Store 层:`OrderStore.List`(`internal/store/postgres/order_store.go:85`)叠加 `applyOrderScope`(第 200 行)。代理账号限定为买家、销售店铺或操作者属于本店及下级店铺;平台账号与超管不限。
|
||||
|
||||
后台认证 `AdminAuth` 允许超管、平台、代理、企业四类账号访问。
|
||||
|
||||
订单导出(`OrderDataSource.applyFilters`,`order_scene.go:130`)复刻了上面两层条件:创建导出任务时冻结账号类型、店铺范围(`ScopeShopIDs`)与发起店铺(`CreatorShopID`),执行时按冻结值过滤;代理账号同样固定"买家是本店"。导出任务只允许超管、平台、代理创建(`internal/service/export_task/service.go`)。
|
||||
|
||||
导出场景的接入点:
|
||||
- 场景常量:`pkg/constants/constants.go:400` `ExportTaskSceneOrder = "order"`。
|
||||
- 注册:`exporter.NewDefaultRegistry`(`internal/exporter/registry.go`)按数据源的 `Scene()` 建表,创建任务时用 `IsSupported` 校验场景。
|
||||
- 时间筛选:`timeFilterScenes`(`internal/exporter/time_filters.go`)登记了 `order`,创建任务时按统一格式冻结 `start_time`/`end_time`。
|
||||
|
||||
### 3. 订单的来源与支付方式
|
||||
|
||||
| 字段 | 取值 | 说明 |
|
||||
|---|---|---|
|
||||
| `source` | `admin` 后台 / `client` 客户端 | 非空,默认 `admin`(`pkg/constants/order_source.go`) |
|
||||
| `payment_method` | `wallet` / `offline` / `wechat` / `alipay` | 后台下单只支持 `wallet` 与 `offline` |
|
||||
| `buyer_type` | `agent` / `personal` | 买家类型 |
|
||||
| `operator_account_type`、`operator_type` | `platform` / `agent` / `personal_customer` / 空 | 操作账号类型;历史订单有空值 |
|
||||
|
||||
### 4. 各下单场景
|
||||
|
||||
后台下单 `CreateAdminOrder`(`internal/service/order/service.go:214`):
|
||||
|
||||
| 场景 | 操作账号 | 支付方式 | 买家 | 扣谁的钱 | 退款方式 |
|
||||
|---|---|---|---|---|---|
|
||||
| 平台自营 | 平台 | 线下 | 个人(id 0) | 不扣钱包,线下收款 | 退到客户账户 |
|
||||
| 赠送 | 平台 | 线下 | 个人(id 0) | 不收钱 | 退到客户账户 |
|
||||
| 平台代购 | 平台 | 线下 | 资源所属代理 | 不扣钱包,线下收款 | 退到客户账户 |
|
||||
| 平台代扣代理钱包 | 平台 | 钱包 | 资源所属代理 | 该代理主钱包 | 退回代理钱包 |
|
||||
| 代理自购 | 代理 | 钱包 | 本代理 | 本代理主钱包 | 退回代理钱包 |
|
||||
| 代理给下级购买 | 代理 | 钱包 | 下级代理 | 本代理(操作方)主钱包 | 退回代理钱包 |
|
||||
| 代理 OpenAPI 下单 | 代理 | 钱包 | 同代理后台下单 | 代理主钱包 | 退回代理钱包 |
|
||||
| 资产套餐批量订购 | 同发起账号 | 同上 | 同上 | 同上 | 同上 |
|
||||
|
||||
客户端下单(`source = client`):个人客户在 H5 下单,用微信、支付宝或卡钱包支付,含待支付订单;自动续费(`internal/application/assetautorenewal/renew.go`)、自动购买(`internal/task/auto_purchase.go`)也记为客户端订单。
|
||||
|
||||
平台代扣代理钱包与代理自购的扣款钱包、买家、成本价、订单角色(都是 `self_purchase`)、退款方式、佣金基准完全相同,只有操作人字段不同。
|
||||
|
||||
### 5. 测试环境数据分布(165 笔)
|
||||
|
||||
| 来源 | 支付方式 | 场景 | 笔数 |
|
||||
|---|---|---|---|
|
||||
| client | 微信、支付宝、卡钱包、空(待支付) | 个人客户在线下单 | 53 |
|
||||
| admin | wallet | 代理操作 | 21 |
|
||||
| admin | wallet | 平台代扣代理钱包 | 31 |
|
||||
| admin | offline | 平台代购 | 41 |
|
||||
| admin | offline | 平台自营、赠送等(买家个人) | 19 |
|
||||
|
||||
生产分布未查到(查询时数据库连接工具断开),实施前需补查,见第五部分。
|
||||
|
||||
### 6. 现状问题
|
||||
|
||||
- 运营无法按"平台订单 / 代理商订单 / 在线支付订单"分页面查看,只能靠支付方式、订单角色间接筛,而且这两个条件都区分不出"平台代扣代理钱包"和"平台代购"。
|
||||
|
||||
---
|
||||
|
||||
## 第二部分:老卡管对照
|
||||
|
||||
源码 `kyhl-src`(`apps/kyhl-crm`)中三类订单是三张表、三个页面:
|
||||
|
||||
| 老页面 | 表 | 内容 | 谁能看 |
|
||||
|---|---|---|---|
|
||||
| 微信订单(`agentWeixinOrder/list`) | `tbl_agent_weixin_order` | C 端公众号订单:微信支付、余额支付(卡钱包)、自动续费 | 平台全部;代理看自己与下级卡上的订单 |
|
||||
| 代理商订单(平台 `agentOrder/syslist`,代理 `agentOrder/list`) | `tbl_agent_order` | 代理账号在后台给卡加套餐,从代理预存款余额扣款(`CardViewComp.agentRecharge`) | 平台全部;代理只看自己的 |
|
||||
| 平台加套餐 | 不生成订单 | 平台账号给卡加套餐只写套餐记录(`addCardOrBatchImportPackage`) | — |
|
||||
|
||||
结论:老系统判断是否为代理商订单,看的是"是否扣代理余额",不看操作人。新系统的平台线下订单(自营、赠送、代购)在老系统里没有订单记录。
|
||||
|
||||
附:老系统首页统计的"平台订单 / 代理商订单"按收款公众号划分(`weixin_agent_id` 是否为空),与列表页口径不同,不作为本方案的依据。
|
||||
|
||||
---
|
||||
|
||||
## 第三部分:方案(To-Be)
|
||||
|
||||
### 1. 接口
|
||||
|
||||
保留 `GET /api/admin/orders`,参数、响应、行为均不变。新增三个列表接口:
|
||||
|
||||
| 接口 | 类别 |
|
||||
|---|---|
|
||||
| `GET /api/admin/orders/platform` | 平台订单 |
|
||||
| `GET /api/admin/orders/agent` | 代理商订单 |
|
||||
| `GET /api/admin/orders/online` | 在线支付订单 |
|
||||
|
||||
- 类别由路径固定,不接收类型参数。
|
||||
- 查询参数、分页、响应结构与原列表相同:入参 `dto.OrderListRequest`,出参 `dto.OrderListResponse`。
|
||||
- 认证、用户类型准入、数据范围与原列表相同。
|
||||
- 订单导出同样分三类,见本部分第 6 节。
|
||||
- 订单详情、取消、购买预检、后台下单不变。
|
||||
|
||||
### 2. 类别划分:按钱从哪来,不按操作人
|
||||
|
||||
| 类别 | 条件 | 包含 |
|
||||
|---|---|---|
|
||||
| 在线支付订单 | `source = 'client'` | 个人客户微信、支付宝、卡钱包支付,自动续费,自动购买,待支付 |
|
||||
| 代理商订单 | `source = 'admin' AND payment_method = 'wallet'` | 代理自购、代理给下级购买、平台代扣代理钱包、代理 OpenAPI 下单 |
|
||||
| 平台订单 | `source = 'admin' AND payment_method <> 'wallet'`(含空) | 平台自营、赠送、平台代购 |
|
||||
|
||||
- `source` 非空且只有两个取值,后台订单按是否用代理钱包支付二分,所以三类互斥且覆盖全部订单。相同条件下,三个新接口的总数之和等于原接口的总数。
|
||||
- 不走代理钱包的后台订单一律不进代理商订单接口。平台代购的买家虽然记为代理,但按线下收款、退款走客户账户,归平台订单;后续如有要求再调整。
|
||||
- 不依赖操作人字段,历史订单操作人为空也不影响分类。
|
||||
|
||||
放弃的方案:按操作账号类型划分(`operator_account_type`,为空回退 `operator_type`)。这样会把平台代扣代理钱包归到平台订单,与资金归属和老卡管口径都不一致,还要兜底历史空值。
|
||||
|
||||
### 3. 实现
|
||||
|
||||
1. 常量:在 `pkg/constants/order_category.go` 定义订单类别 `platform` / `agent` / `online`。
|
||||
2. Store:`OrderStore.List` 支持内部过滤键 `order_category`,类别条件集中在一个函数里,三个接口与导出共用(见第 6 节)。
|
||||
3. Service:`order.Service.List` 增加订单类别入参并透传给 Store;原调用传空类别,行为不变。
|
||||
4. Handler:把现有 `List` 的参数解析、时间范围解析、代理买家条件提取为内部方法 `list(c, category)`。原 `List` 以空类别调用,新增 `ListPlatform`、`ListAgent`、`ListOnline` 分别以固定类别调用。权限逻辑只有一份。
|
||||
5. 路由:三个静态路径必须注册在 `GET /orders/:id`(`internal/routes/order.go:30`)之前,否则会被当作订单 ID 解析,返回"无效的订单ID"。
|
||||
|
||||
类别不进 DTO,所以请求里即使带 `order_category` 等参数也不会被解析。
|
||||
|
||||
### 4. 代理账号看到的结果
|
||||
|
||||
权限不变,代理账号在新接口看到的是原接口结果按类别切开的部分:
|
||||
|
||||
| 接口 | 代理账号能看到 |
|
||||
|---|---|
|
||||
| 平台订单 | 平台为本店名下的卡代购的订单 |
|
||||
| 代理商订单 | 买家是本店的钱包订单:本店自购、平台代扣本店钱包、上级给本店购买 |
|
||||
| 在线支付订单 | 始终为空:原接口对代理只返回买家是本店的订单,客户端订单的买家是个人客户 |
|
||||
|
||||
代理在"给下级购买"时,订单买家是下级,原接口对该代理不可见,新接口也不可见。
|
||||
|
||||
### 4.1 企业账号不能访问订单接口(修复现有问题)
|
||||
|
||||
现状:后台认证允许企业账号,原列表对非代理账号不加买家条件,店铺范围对企业账号也不生效,接口权限中间件未接入路由,所以企业账号调用 `GET /orders` 与 `GET /orders/:id` 会看到全部订单。下单已拒绝企业账号,取消只允许代理,导出任务不允许企业账号创建。
|
||||
|
||||
做法:`OrderHandler` 增加企业账号校验,原列表、三个新列表、订单详情、购买预检对企业账号返回无权限。校验放在 Handler,不放 Store 的 `applyOrderScope`,因为后者被资产订单查询等其他场景共用。企业账号查看自己资产的订单继续用 `GET /assets/{identifier}/orders`(按资产数据范围校验),不受影响。
|
||||
|
||||
这是本方案中唯一的破坏性变更,只影响企业账号。
|
||||
|
||||
### 5. 性能
|
||||
|
||||
类别条件只用 `source` 与 `payment_method`,不加专用索引。实施后在测试环境看执行计划;生产订单量明显增长时,再评估 `(source, payment_method, created_at)` 组合索引。
|
||||
|
||||
### 6. 导出分三类
|
||||
|
||||
保留 `scene=order`,行为不变。新增三个导出场景,与三个列表接口一一对应:
|
||||
|
||||
| 场景 | 对应列表 |
|
||||
|---|---|
|
||||
| `order_platform` | `GET /orders/platform` |
|
||||
| `order_agent` | `GET /orders/agent` |
|
||||
| `order_online` | `GET /orders/online` |
|
||||
|
||||
- 与列表一致:类别由场景决定,`filters` 里不加类别参数;筛选条件、表头、列内容、数据范围与 `scene=order` 完全相同,只多一个固定的类别条件。
|
||||
- 用新场景而不是给 `order` 加参数,原因是导出本来就靠 `scene` 区分数据,和列表"路径决定类别"是同一个思路,前端每个页面各传自己的场景即可。
|
||||
|
||||
实现:
|
||||
|
||||
1. 场景常量:在 `pkg/constants` 新增 `ExportTaskSceneOrderPlatform`、`ExportTaskSceneOrderAgent`、`ExportTaskSceneOrderOnline`。
|
||||
2. 数据源:`OrderDataSource` 增加类别字段,`NewOrderDataSource(db, category)`;`Scene()` 按类别返回场景,类别为空时返回 `order`;`applyFilters` 在现有条件后叠加类别条件。`Count`、`Fetch` 共用 `applyFilters`,条数与内容自动一致。
|
||||
3. 类别条件只写一份:放在 `internal/query/ordercategory`,按表别名生成条件(导出查询用别名 `o`,Store 无别名),列表与导出共用,保证口径一致。与不轮询清单共用 `internal/query/pollingpause` 的做法一致。
|
||||
4. 注册:`NewDefaultRegistry` 再注册三个类别的 `OrderDataSource`;`timeFilterScenes` 加入三个新场景,时间格式校验与 `order` 相同;`IsSupportedScene` 同步加入。
|
||||
5. 导出任务创建接口的场景说明(`internal/routes/export_task.go`)补充三个新场景。
|
||||
|
||||
---
|
||||
|
||||
## 第四部分:改动范围
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `pkg/constants`(订单相关常量文件) | 新增订单类别常量 |
|
||||
| `internal/store/postgres/order_store.go` | `List` 支持 `order_category` |
|
||||
| `internal/service/order/service.go` | `List` 透传订单类别 |
|
||||
| `internal/handler/admin/order.go` | 提取内部列表方法,新增三个 Handler;企业账号校验 |
|
||||
| `internal/routes/order.go` | 注册三个路由,写明类别口径和"参数、响应、权限同原订单列表" |
|
||||
| `pkg/constants/constants.go` | 新增三个订单导出场景常量 |
|
||||
| `internal/exporter/order_scene.go` | 数据源支持类别,叠加类别条件 |
|
||||
| `internal/exporter/registry.go`、`internal/exporter/time_filters.go` | 注册三个新场景,纳入时间筛选 |
|
||||
| `internal/routes/export_task.go` | 场景列表说明补充三个新场景 |
|
||||
| OpenAPI 文档、前端接口变更说明 | 补充三个列表接口与三个导出场景 |
|
||||
|
||||
- 不新增字段,不需要迁移,不改历史数据。
|
||||
- 原导出场景 `order` 不变;不改订单详情、取消、下单。
|
||||
- 无自动化测试(项目无 `*_test.go`),在测试环境验证。
|
||||
|
||||
---
|
||||
|
||||
## 第五部分:验证(测试环境)
|
||||
|
||||
1. 平台账号,相同条件调用三个新接口与原接口:三个新接口的总数之和等于原接口总数;抽查平台代扣代理钱包在代理商订单、平台代购在平台订单、自动续费在在线支付订单。
|
||||
2. 平台账号,叠加支付状态、时间、资产标识、订单号等原有条件,结果正确;原接口结果与上线前一致。
|
||||
3. 代理账号,三个新接口的结果都是原接口结果的子集,总数之和等于原接口;在线支付订单为空。
|
||||
4. `GET /orders/:id` 仍正常;`/orders/platform` 等路径不会被当作订单 ID。
|
||||
4.1 企业账号调用原列表、三个新列表、订单详情、购买预检返回无权限;`GET /assets/{identifier}/orders` 查询本企业资产不受影响。
|
||||
5. 导出:平台账号与代理账号分别用三个新场景导出,条数与对应列表接口的总数一致,表头与 `scene=order` 相同;`scene=order` 导出结果与上线前一致;新场景传旧时间格式时与 `order` 一样被拒绝。
|
||||
6. 上线前补查生产分布:按 `source`、`payment_method` 统计,确认没有 `source` 为其他值的订单、后台订单只有钱包和线下两种支付方式。
|
||||
|
||||
---
|
||||
|
||||
## 第六部分:已定事项与后续事项
|
||||
|
||||
已定:
|
||||
- 原接口保留,新接口不传类型参数;超管、平台、代理的权限与数据范围不变。
|
||||
- 企业账号不能访问 `/orders` 下的列表、详情、购买预检,修复其可看到全部订单的问题。
|
||||
- 按钱从哪来划分:代理钱包扣款的后台订单(含平台代扣)归代理商订单;不走代理钱包的后台订单归平台订单。
|
||||
- 导出也分三类:新增 `order_platform`、`order_agent`、`order_online` 三个场景,原 `order` 场景保留。
|
||||
|
||||
后续(有要求再做):
|
||||
- 平台代购是否改归代理商订单。
|
||||
- 代理账号是否需要在在线支付订单接口看到自己资产上的个人客户订单(属于权限调整)。
|
||||
- 在线支付订单是否增加老卡管微信订单页的"订单来源、收款方"筛选。
|
||||
Reference in New Issue
Block a user