Files
junhong_cmp_fiber/docs/7月迭代/独立方案/原需求/需求14-导出功能.md
2026-07-17 16:39:41 +08:00

413 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 需求14导出功能
> 状态:原需求独立稿;最终口径以标准评审稿为准。
> 复用现有 ExportTask 体系(`tb_export_task` + Asynq不为每个业务模块新增一套导出路由。
---
## 导出模块总览
| 编号 | 模块 | 新增/修改 |
|------|------|---------|
| EXPD-001~003 | IoT卡导出 | 新增套餐名称、使用流量、剩余流量字段 |
| 6.8.2 | 代理资金概况-预充值钱包流水导出 | **全新** |
| 6.8.3 | 套餐列表导出 | **全新** |
| 6.8.4 | 退款管理退款列表导出 | **全新** |
| 6.8.5 | 换货管理导出 | **全新** |
| 6.8.6 | 代理充值导出 | **全新**(去掉"支付通道"字段) |
| 需求22 | 临期资产列表导出 | **全新**`scene=expiring_asset` |
---
## 现有导出体系说明
系统已有异步导出框架(`internal/exporter/`
- `tb_export_task` 表记录导出任务
- 导出逻辑通过 `DataSource` 接口实现,每个场景一个文件(如 `iot_card_scene.go`
- `registry.go``NewDefaultRegistry()` 统一注册所有场景
- Asynq Worker 根据任务里的 `scene` 字段,从 Registry 取对应 DataSource 执行
- 前端轮询任务状态后下载
```mermaid
sequenceDiagram
actor User as 后台用户
participant Web as 前端
participant API as ExportTask API
participant DB as PostgreSQL
participant Worker as Asynq Worker
participant Storage as 对象存储
User->>Web: 选择导出字段并确认
Web->>API: POST /api/admin/export-tasks
API->>API: 计算数据范围和字段权限交集
API->>DB: 保存查询、范围和字段快照
API-->>Web: task_id
API->>Worker: 入队导出任务
Worker->>DB: 按 scene 分片查询
Worker->>Storage: 上传导出文件
Worker->>DB: 更新进度和 download file_key
Web->>API: 轮询 GET /api/admin/export-tasks/{id}
API-->>Web: 状态、进度、download_url
```
新增导出模块需要:
1.`pkg/constants/constants.go` 新增 `ExportTaskSceneXxx` 场景常量
2.`internal/exporter/` 新建 `xxx_scene.go`,实现 `DataSource` 接口(`Scene()`/`Count()`/`Headers()`/`Fetch()`
3.`registry.go``NewDefaultRegistry()` 中注册,并更新 `IsSupportedScene()`
4. 扩展统一 `CreateExportTaskRequest.Scene` 的允许值,通过 `POST /api/admin/export-tasks` 创建任务
5. 在字段目录中注册稳定字段 key、中文表头、默认选择和所需导出权限
统一创建请求扩展为:
```go
type CreateExportTaskRequest struct {
Scene string `json:"scene" validate:"required" description:"导出场景"`
Format string `json:"format" validate:"required,oneof=xlsx csv" description:"导出格式"`
Query map[string]any `json:"query,omitempty" description:"与列表一致的筛选条件"`
Fields []string `json:"fields" validate:"required,min=1" description:"申请导出的字段key"`
}
```
新增场景:`agent_wallet_transaction``package``refund``exchange``agent_recharge``expiring_asset`。DTO description 和 `pkg/constants/` 必须同步维护。
---
## EXPD-001~003IoT卡导出字段新增
**修改文件**`internal/exporter/iot_card_scene.go`
`Headers()` 末尾追加三列,`Fetch()``Select` 追加字段,`iotCardExportRow` 追加字段:
```go
// Headers() 新增
"套餐名称", "使用流量(MB)", "剩余流量(MB)"
// baseQuery() 或 Fetch() 新增 JOIN
LEFT JOIN LATERAL (
SELECT pu.package_id, pu.data_usage_mb, pu.data_limit_mb, p.package_name
FROM tb_package_usage pu
JOIN tb_package p ON p.id = pu.package_id AND p.deleted_at IS NULL
WHERE pu.iot_card_id = c.id AND pu.status = 1
AND pu.master_usage_id IS NULL AND pu.deleted_at IS NULL
LIMIT 1
) AS pkg ON TRUE
// iotCardExportRow 新增
PackageName string `gorm:"column:package_name"`
DataUsageMB int64 `gorm:"column:data_usage_mb"`
DataLimitMB int64 `gorm:"column:data_limit_mb"`
```
剩余流量 = `DataLimitMB - DataUsageMB`(在行转换时计算)
---
## 6.8.2:代理资金概况-预充值钱包流水导出
**新增场景**`scene=agent_wallet_transaction`
支持与现有钱包流水列表相同的筛选条件,异步生成 Excel。
导出字段映射:
| 字段 | 数据来源 |
|------|---------|
| 店铺名称 | JOIN `tb_shop` |
| 交易类型 | `transaction_type`(充值/扣款/退款等,中文化) |
| 交易金额 | `amount / 100` 转元 |
| 状态 | `status` 中文化 |
| 资产类型 | `asset_type` 中文化 |
| 资产标识 | `asset_identifier` |
| 交易时间 | `created_at` |
| 交易前金额 | `balance_before / 100` |
| 交易后金额 | `balance_after / 100` |
| 购买套餐名称 | 新数据读取 `tb_order_item.package_name` 不可变快照;历史缺失时才回退 `metadata.package_name`,禁止关联当前套餐名称 |
| 操作人 | JOIN `tb_account``creator` 字段关联 `tb_account.id`,取 `username` |
| 交易 ID | `id` |
| 关联业务订单号 | `reference_id` 对应的单号JOIN 对应表) |
| 交易渠道/支付方式 | `metadata``payment_method` 字段 |
---
## 6.8.3:套餐列表导出
**新增场景**`scene=package`
支持现有套餐列表筛选条件。
导出字段(按实际列举的 25 个稳定字段 Key
```go
type PackageExportRow struct {
PackageCode string `xlsx:"套餐编码"`
PackageName string `xlsx:"套餐名称"`
SeriesName string `xlsx:"套餐系列名称"` // JOIN tb_package_series
PackageType string `xlsx:"套餐类型"` // formal/addon 中文化
DurationMonths int `xlsx:"套餐时长(月)"`
DurationDaysDesc string `xlsx:"套餐时长说明"` // 剩余天数说明
CalendarType string `xlsx:"套餐周期类型"`
DurationDays int `xlsx:"套餐天数"`
RealDataMB int64 `xlsx:"真流量额度(MB)"`
VirtualDataMB int64 `xlsx:"虚流量额度(MB)"`
EnableVirtualData string `xlsx:"是否启用虚流量"` // 是/否
VirtualRatio float64 `xlsx:"虚流量比例"`
DataResetCycle string `xlsx:"流量重置周期"`
ExpiryBase string `xlsx:"到期时间基准"`
CostPrice string `xlsx:"成本价(元)"` // 分→元
SuggestedRetailPrice string `xlsx:"建议售价(元)"`
PriceConfigStatus string `xlsx:"价格配置状态"`
Status string `xlsx:"状态"`
ShelfStatus string `xlsx:"上架状态"`
IsGift string `xlsx:"是否赠送套餐"`
CreatorID uint `xlsx:"创建人ID"`
UpdaterID uint `xlsx:"更新人ID"`
CreatedAt string `xlsx:"创建时间"`
UpdatedAt string `xlsx:"更新时间"`
DeletedAt string `xlsx:"删除时间"`
}
```
---
## 6.8.4:退款管理退款列表导出
**新增场景**`scene=refund`
导出字段(去掉“退款到账方式”,审批信息使用动态摘要,不固定具体节点):
```go
type RefundExportRow struct {
RefundNo string `xlsx:"退款单号"`
ShopName string `xlsx:"代理店铺名称"`
PaymentOrderNo string `xlsx:"关联的支付订单号"`
AssetType string `xlsx:"资产类型"`
AssetIdentifier string `xlsx:"资产标识"`
PackageName string `xlsx:"套餐名称"`
OriginalAmount string `xlsx:"原订单金额"`
ActualAmount string `xlsx:"实收金额"`
RefundableAmount string `xlsx:"可退金额"`
AppliedAmount string `xlsx:"申请退款金额"`
ActualRefundAmount string `xlsx:"实际退款金额"`
Status string `xlsx:"状态"`
RefundReason string `xlsx:"退款原因"`
Remark string `xlsx:"备注"`
ApprovalSource string `xlsx:"审批来源"`
ApprovalStatus string `xlsx:"审批状态"`
ProcessingStatus string `xlsx:"退款处理状态"`
CurrentNodeName string `xlsx:"当前审批节点"`
ApprovalRecords string `xlsx:"审批记录"`
AppliedAt string `xlsx:"退款申请时间"`
CompletedAt string `xlsx:"退款完成时间"`
SubmitterName string `xlsx:"提交人"`
VoucherURLs string `xlsx:"退款凭证"`
}
```
`ApprovalRecords` 格式示例:`业务审核:张三(通过,同意,附件2个);财务审核:李四(待处理)`。导出只记录附件数量,不导出对象存储 URL 或 file_key。`ProcessingStatus` 区分待人工退款、代理钱包回退处理中、已完成和处理失败。数据源按本批业务单的 `approval_instance_id` 批量查询审批任务、审批人和附件计数,禁止逐行查询。历史终态单据没有流程实例时,`ApprovalSource` 输出“历史审批”,审批记录仅使用原业务审批字段和审计日志,不伪造多节点记录。
---
## 6.8.5:换货管理导出
**新增场景**`scene=exchange`
```go
type ExchangeExportRow struct {
ExchangeNo string `xlsx:"换货单号"`
ExchangeType string `xlsx:"换货类型"`
ExchangeReason string `xlsx:"换货原因"`
ProblemDesc string `xlsx:"问题描述"`
OldAssetType string `xlsx:"旧资产类型"`
OldAssetIdentifier string `xlsx:"旧资产标识符"`
NewAssetIdentifier string `xlsx:"新资产标识符"`
ReceiverName string `xlsx:"收货人姓名"`
ReceiverPhone string `xlsx:"收货人电话"`
ReceiverAddress string `xlsx:"收货地址"`
ExpressCompany string `xlsx:"快递公司"`
TrackingNo string `xlsx:"快递单号"`
Status string `xlsx:"状态"`
CreatorName string `xlsx:"创建人"`
CreatedAt string `xlsx:"创建时间"`
}
```
---
## 6.8.6:代理充值导出
**新增场景**`scene=agent_recharge`
去掉"支付通道"字段(需求文档中明确去掉),保留其他字段:
```go
type AgentRechargeExportRow struct {
RechargeNo string `xlsx:"充值单号"`
ShopName string `xlsx:"店铺名称"`
RechargeType string `xlsx:"充值类型"`
RechargeAmount string `xlsx:"充值金额"`
ActualAmount string `xlsx:"实付金额"`
BalanceBefore string `xlsx:"充值前余额"`
BalanceAfter string `xlsx:"充值后余额"`
Status string `xlsx:"状态"`
PaymentMethod string `xlsx:"支付方式"`
// 去掉支付通道
OperationRemark string `xlsx:"运营备注"`
RejectReason string `xlsx:"驳回原因"`
CreatedAt string `xlsx:"创建时间"`
PaidAt string `xlsx:"支付时间"`
CompletedAt string `xlsx:"完成时间"`
SubmitterName string `xlsx:"提交人"`
ApprovalSource string `xlsx:"审批来源"`
ApprovalStatus string `xlsx:"审批状态"`
CurrentNodeName string `xlsx:"当前审批节点"`
ApprovalRecords string `xlsx:"审批记录"`
VoucherURLs string `xlsx:"支付凭证"`
Remark string `xlsx:"备注"`
}
```
充值导出的审批记录格式和批量查询规则与退款导出一致。
---
## 前端对接(通用模式)
各导出入口:对应列表页右上角"导出"按钮(与现有导出按钮样式一致)。
调用流程:
1. 点击“导出”后请求当前账号在该场景可导出的字段目录。
2. 弹框只展示后端允许的字段,默认勾选 `default_selected=true` 的字段。
3. 提交 `scene + format + query + fields``POST /api/admin/export-tasks`
4. 返回 `task_id` 后,前端轮询 `GET /api/admin/export-tasks/{id}` 直到终态。
5. `status=3` 时下载 `download_url`;失败时展示任务错误摘要,禁止自动重复创建任务。
(与现有导出体系完全一致,复用现有前端导出 Hook
---
## 导出字段权限
需求提到"不同权限显示的字段不同,角色管理中新增导出字段配置"。
评审结论:本迭代必须实现角色级导出字段配置,不作为后续预留。
该要求涉及真流量、成本价、身份材料等敏感数据,不能以“本期先全量导出”代替。字段权限必须由后端强制执行,前端复选框只负责展示。
### 1. 字段目录
每个场景在代码中注册稳定字段 key
```go
// ExportFieldDefinition 导出字段定义
type ExportFieldDefinition struct {
Key string
Header string
DefaultSelected bool
Required bool
}
```
示例:
```text
scene=package
package_code 套餐编码 默认选择
package_name 套餐名称 默认选择
real_data_mb 真流量额度 敏感字段
virtual_data_mb 虚流量额度 默认选择
cost_price 成本价 敏感字段
```
表头中文可以调整,但 `scene + field_key` 一经发布不得随意改名,否则历史角色配置会失效。
### 2. 角色字段授权表
```sql
CREATE TABLE tb_role_export_field_permission (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
scene VARCHAR(50) NOT NULL,
field_key VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
creator BIGINT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX idx_role_export_field_permission
ON tb_role_export_field_permission(role_id, scene, field_key);
```
账号拥有多个角色时,字段权限按 RBAC 常规规则取并集;超级管理员拥有全部字段。数据行范围仍使用现有数据权限快照,字段权限不能扩大店铺或企业数据范围。
### 3. 权限 API
```text
GET /api/admin/export-fields?scene=package
```
返回当前账号可选择的字段:
```json
{
"scene": "package",
"fields": [
{
"key": "package_code",
"header": "套餐编码",
"default_selected": true,
"required": true
}
]
}
```
角色管理:
```text
GET /api/admin/roles/{role_id}/export-fields
PUT /api/admin/roles/{role_id}/export-fields
```
更新请求按场景提交字段 key 列表,后端校验字段存在并记录审计日志。
### 4. 创建任务时的权限快照
创建任务时计算:
```text
resolved_fields = requested_fields ∩ role_allowed_fields ∩ scene_supported_fields
```
- 必选字段由后端自动补齐。
- 交集为空时拒绝创建任务。
- `resolved_fields` 和对应 `resolved_headers` 写入任务的 `query_json`Worker 不重新根据后来变化的角色权限扩大字段。
- Worker 只按照快照字段输出,未知字段直接失败,不允许静默回退到全量字段。
### 5. 前端角色配置
- 角色编辑页增加“导出字段权限”页签,按场景分组展示复选框。
- 普通列表的导出弹框只显示当前账号被授权的字段。
- 敏感字段可以增加“敏感”标记,但标记不能替代后端权限。
- 用户取消所有可选字段时禁用提交按钮,并提示至少选择一个字段。
---
## 查询与性能约束
- 退款和充值审批记录必须先按本批 `approval_instance_id` 批量查询,再在内存按实例分组,禁止逐行查审批任务。
- 关联业务单号、套餐名称和操作人必须使用批量 JOIN 或批量查询,禁止 N+1。
- DataSource 的 Count 和 Fetch 必须使用同一份权限过滤和查询条件。
- 动态字段不代表动态拼接不受控 SQL字段 key 必须通过服务端白名单映射到固定查询列。
---
## 发布与回滚
本需求随七月迭代停机发布:
1. 维护窗口内先执行角色字段权限表迁移,并初始化现有角色的最小可用字段集。
2. 同一发布窗口部署场景常量、DataSource、管理 API、Worker 和前端字段选择/角色配置页面。
3. 开放访问前验证普通角色、敏感字段角色和超级管理员的字段集合及实际导出文件。
4. 回滚时可关闭新增场景入口,但保留任务、文件和字段权限历史。
禁止在角色权限尚未初始化时默认放开全部敏感字段;无法解析权限时应拒绝导出并记录错误。