forked from erp-dev/erp
feat: settlement first api beta
This commit is contained in:
203
docs/settlement/API.md
Normal file
203
docs/settlement/API.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Settlement API 设计文档
|
||||
|
||||
## 概述
|
||||
本文档描述 settlement 模块的 API 设计,遵循项目 API 规范。
|
||||
|
||||
## API 列表
|
||||
|
||||
### 1. 开版订单统计 API
|
||||
|
||||
获取按客户分组的开版订单统计。
|
||||
|
||||
**基本信息**:
|
||||
- **URL**: `/api/v1/settlement/plate-orders/summary/`
|
||||
- **方法**: GET
|
||||
- **认证**: 需要认证
|
||||
- **权限**: 需要关联商户
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| date | string | 是 | 统计日期,格式 YYYY-MM-DD | 2026-02-08 |
|
||||
|
||||
**请求示例**:
|
||||
```http
|
||||
GET /api/v1/settlement/plate-orders/summary/?date=2026-02-08 HTTP/1.1
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"client_id": 101,
|
||||
"client_name": "客户A",
|
||||
"plate_order_count": [
|
||||
{
|
||||
"type": "首版-定位",
|
||||
"today": 26,
|
||||
"current_month": 45
|
||||
},
|
||||
{
|
||||
"type": "修改-定位",
|
||||
"today": 3,
|
||||
"current_month": 7
|
||||
},
|
||||
{
|
||||
"type": "首版-匹布",
|
||||
"today": 3,
|
||||
"current_month": 7
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"client_id": 102,
|
||||
"client_name": "客户B",
|
||||
"plate_order_count": [
|
||||
{
|
||||
"type": "首版-定位",
|
||||
"today": 10,
|
||||
"current_month": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明**:
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| data | array | 客户统计列表 |
|
||||
| data[].client_id | integer | 客户ID |
|
||||
| data[].client_name | string | 客户名称 |
|
||||
| data[].plate_order_count | array | 订单统计列表 |
|
||||
| data[].plate_order_count[].type | string | 类型组合(plate_type-production_method) |
|
||||
| data[].plate_order_count[].today | integer | 今日数量 |
|
||||
| data[].plate_order_count[].current_month | integer | 本月累计数量 |
|
||||
|
||||
**业务规则**:
|
||||
1. **商户隔离**: 只返回当前用户所属商户的数据
|
||||
2. **客户可见性**: 应用客户可见性过滤
|
||||
3. **过滤条件**:
|
||||
- `plate_type` 不为空的订单才纳入统计
|
||||
- `production_method` 不为空的订单才纳入统计
|
||||
4. **统计维度**: 按 `customer_id`、`customer_name`、`plate_type`、`production_method` 分组(同名客户不会被合并)
|
||||
5. **today**: 指定日期当天的订单数量(基于 plate_date 字段)
|
||||
6. **current_month**: 从当月1日(含)到指定日期(含)的订单数量
|
||||
7. **数据过滤**:
|
||||
- 不返回没有数据的客户
|
||||
- 客户中不显示全0数据的类型组合
|
||||
|
||||
**type 组合规则**:
|
||||
- 格式: `{plate_type}-{production_method}`
|
||||
- plate_type 可能的值: `首版`、`修改`
|
||||
- production_method 可能的值: `定位`、`匹布`
|
||||
- 组合示例: `首版-定位`、`首版-匹布`、`修改-定位`、`修改-匹布`
|
||||
|
||||
**错误响应**:
|
||||
|
||||
**400 Bad Request** - 缺少日期参数:
|
||||
```json
|
||||
{
|
||||
"error": "缺少 date 参数"
|
||||
}
|
||||
```
|
||||
|
||||
**400 Bad Request** - 日期格式错误:
|
||||
```json
|
||||
{
|
||||
"error": "日期格式错误,请使用 YYYY-MM-DD 格式"
|
||||
}
|
||||
```
|
||||
|
||||
**403 Forbidden** - 用户未关联商户:
|
||||
```json
|
||||
{
|
||||
"error": "用户未关联商户"
|
||||
}
|
||||
```
|
||||
|
||||
**403 Forbidden** - 日期不存在:
|
||||
```json
|
||||
{
|
||||
"error": "日期不存在"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现细节
|
||||
|
||||
### View 层
|
||||
- **文件**: `api_v1/views/settlement/views.py`
|
||||
- **类**: `PlateOrderSummaryView`
|
||||
- **继承**: `APIView`
|
||||
- **职责**:
|
||||
- 参数验证
|
||||
- 商户隔离
|
||||
- 调用 service 层
|
||||
- 错误处理
|
||||
|
||||
### Service 层
|
||||
- **文件**: `settlement/services.py`
|
||||
- **函数**: `get_plate_order_summary_by_customer`
|
||||
- **职责**:
|
||||
- 复杂的统计逻辑
|
||||
- 数据查询和聚合
|
||||
- 数据格式化
|
||||
|
||||
### URL 配置
|
||||
- **文件**: `api_v1/urls.py`
|
||||
- **路由**: 添加到 settlement 路由组
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### 1. 正常情况
|
||||
- 请求有效日期,返回正确数据
|
||||
- 验证商户隔离
|
||||
- 验证客户可见性过滤
|
||||
- 验证 today 和 current_month 计算正确
|
||||
|
||||
### 2. 边界情况
|
||||
- 请求当月第一天(current_month = today)
|
||||
- 请求跨月日期
|
||||
- 客户没有数据(不返回该客户)
|
||||
- 类型组合全0(不返回该类型)
|
||||
|
||||
### 3. 错误情况
|
||||
- 日期格式错误
|
||||
- 日期不存在(如 2026-02-30)
|
||||
- 用户未关联商户
|
||||
|
||||
### 4. 数据过滤
|
||||
- plate_type 为空的订单不纳入统计
|
||||
- production_method 为空的订单不纳入统计
|
||||
|
||||
---
|
||||
|
||||
## 性能考虑
|
||||
|
||||
1. **查询优化**: 使用 Django ORM 的 `annotate` 和聚合函数,避免 N+1 查询
|
||||
2. **条件聚合**: 使用 `Case`/`When` 一次查询获取 today 和 current_month
|
||||
3. **索引优化**: 确保 `plate_date`、`customer`、`plate_type`、`production_method` 字段有索引
|
||||
4. **分页**: 暂不需要分页(数据量不大)
|
||||
|
||||
### 数据库优化评估记录(PostgreSQL)
|
||||
|
||||
1. 已评估索引、视图、物化视图等数据库层优化路线
|
||||
2. 当前阶段暂不实施数据库结构优化,以保持线上稳定性
|
||||
3. 生产环境不可接受阻塞风险:后续若加索引必须使用 `CREATE INDEX CONCURRENTLY`,并采用低峰分批策略
|
||||
4. 优化上线前必须完成预发压测与 `EXPLAIN ANALYZE` 对比
|
||||
|
||||
---
|
||||
|
||||
## 后续扩展
|
||||
|
||||
1. 支持日期范围查询(start_date, end_date)
|
||||
2. 支持按商户过滤(管理员功能)
|
||||
3. 支持导出 Excel
|
||||
4. 支持缓存(Redis)
|
||||
@@ -78,6 +78,7 @@ class DailySettlementConfig(ModelBase):
|
||||
class Meta:
|
||||
verbose_name = '日结配置'
|
||||
verbose_name_plural = '日结配置'
|
||||
db_table = 'daily_settlement_config'
|
||||
```
|
||||
|
||||
**说明**:
|
||||
@@ -212,8 +213,8 @@ def run_daily_settlement(self):
|
||||
from basic_info.models import Merchant
|
||||
|
||||
# 获取所有配置了统计模块的商户
|
||||
configs = DailySettlementConfig.objects.filter(
|
||||
settlement_modules__len__gt=0
|
||||
configs = DailySettlementConfig.objects.exclude(
|
||||
settlement_modules=[]
|
||||
).select_related('merchant')
|
||||
|
||||
logger.info(
|
||||
@@ -412,7 +413,42 @@ class SettlementConfig(AppConfig):
|
||||
3. 未来如需提升性能,可以通过增加 Celery worker 数量来解决
|
||||
4. Celery 本身支持任务队列和并发控制,后续可以轻松扩展
|
||||
|
||||
## 十三、实施步骤
|
||||
## 十三、数据库优化评估(PostgreSQL)
|
||||
|
||||
### 13.1 当前结论(暂不实施)
|
||||
|
||||
已评估从数据库层面优化开版订单统计(索引、视图、物化视图),当前阶段暂不实施结构性优化,维持现有实现。
|
||||
|
||||
### 13.2 暂缓原因
|
||||
|
||||
1. 当前测试与功能已稳定,优先保证行为一致性
|
||||
2. 生产环境要求“不可接受阻塞风险”,索引变更需专项窗口与监控保障
|
||||
3. 现阶段数据规模下,统计查询尚可接受
|
||||
|
||||
### 13.3 后续可选优化路线(按优先级)
|
||||
|
||||
1. **索引优先**:为 `plate_order` 统计路径增加复合/部分索引
|
||||
2. **表达式索引**:针对 `date(plate_date)` 的筛选场景
|
||||
3. **物化视图**:当数据规模明显增大时,将日粒度聚合前置
|
||||
|
||||
### 13.4 生产安全约束
|
||||
|
||||
若后续执行索引优化,必须遵循:
|
||||
|
||||
1. 使用 PostgreSQL `CREATE INDEX CONCURRENTLY`
|
||||
2. 迁移使用 `atomic = False`
|
||||
3. 低峰分批执行,一次一个索引
|
||||
4. 全程监控 CPU/IO/WAL、慢查询与复制延迟
|
||||
|
||||
### 13.5 验证要求
|
||||
|
||||
任何数据库优化上线前,需要在预发(接近生产数据量)完成:
|
||||
|
||||
1. `EXPLAIN ANALYZE` 对比
|
||||
2. 回归测试通过(`settlement` + `api_v1.views.settlement`)
|
||||
3. 回滚脚本预演
|
||||
|
||||
## 十四、实施步骤
|
||||
|
||||
1. 创建 `settlement` 模块目录结构
|
||||
2. 实现 `models.py`(配置模型)
|
||||
@@ -427,7 +463,7 @@ class SettlementConfig(AppConfig):
|
||||
11. 编写测试用例
|
||||
12. 手动触发测试,验证结果
|
||||
|
||||
## 十四、方案优势
|
||||
## 十五、方案优势
|
||||
|
||||
1. **独立模块**:`settlement` 模块独立,职责清晰
|
||||
2. **配置驱动**:每个商户可独立配置统计模块和通知渠道
|
||||
|
||||
186
docs/settlement/Service.md
Normal file
186
docs/settlement/Service.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# Settlement Service 设计文档
|
||||
|
||||
## 概述
|
||||
本文档描述 settlement 模块的 service 层设计,遵循职责分离原则,每个函数职责单一、可测试。
|
||||
|
||||
## 函数列表
|
||||
|
||||
### 1. `get_plate_order_summary_by_customer`
|
||||
|
||||
获取按客户分组的开版订单统计。
|
||||
|
||||
**参数**:
|
||||
- `merchant_id: int` - 商户ID
|
||||
- `settlement_date: datetime.date` - 统计日期
|
||||
- `user: User | None` - 当前用户(用于客户可见性过滤)
|
||||
|
||||
**返回**:
|
||||
- `list[dict]` - 客户统计列表
|
||||
|
||||
**职责**:
|
||||
- 参数验证
|
||||
- 调用底层数据查询函数
|
||||
- 应用客户可见性过滤
|
||||
- 数据格式化
|
||||
|
||||
**参数校验规则**:
|
||||
- `merchant_id` 必须是正整数
|
||||
- `settlement_date` 必须是 `date` 或 `datetime` 类型(`datetime` 会自动转换为 `date`)
|
||||
- 参数非法时抛出 `ValueError`
|
||||
|
||||
---
|
||||
|
||||
### 2. `_get_plate_order_queryset`
|
||||
|
||||
获取开版订单的基础查询集。
|
||||
|
||||
**参数**:
|
||||
- `merchant_id: int` - 商户ID
|
||||
- `user: User | None` - 当前用户(用于客户可见性过滤)
|
||||
|
||||
**返回**:
|
||||
- `QuerySet[PlateOrder]` - 过滤后的查询集
|
||||
|
||||
**职责**:
|
||||
- 应用商户隔离
|
||||
- 应用客户可见性过滤
|
||||
- 过滤 `plate_type` 不为空的订单
|
||||
- 过滤 `production_method` 不为空的订单
|
||||
- 优化查询(select_related)
|
||||
|
||||
---
|
||||
|
||||
### 3. `_get_month_date_range`
|
||||
|
||||
获取从月初到指定日期的日期范围。
|
||||
|
||||
**参数**:
|
||||
- `settlement_date: datetime.date` - 统计日期
|
||||
|
||||
**返回**:
|
||||
- `tuple[date, date]` - (月初日期, 统计日期)
|
||||
|
||||
**职责**:
|
||||
- 计算当月第一天
|
||||
- 返回日期范围元组
|
||||
|
||||
---
|
||||
|
||||
### 4. `_aggregate_plate_orders_by_customer_and_type`
|
||||
|
||||
按客户和类型分组聚合订单数据。
|
||||
|
||||
**参数**:
|
||||
- `queryset: QuerySet[PlateOrder]` - 基础查询集
|
||||
- `settlement_date: datetime.date` - 统计日期
|
||||
|
||||
**返回**:
|
||||
- `QuerySet[PlateOrder]` - 添加了聚合标注的查询集
|
||||
|
||||
**职责**:
|
||||
- 按 customer_id、customer_name、plate_type、production_method 分组
|
||||
- 计算今日数量(条件聚合)
|
||||
- 计算本月累计数量(条件聚合)
|
||||
- 生成 type 字段(plate_type + '-' + production_method)
|
||||
|
||||
---
|
||||
|
||||
### 5. `_format_plate_order_summary`
|
||||
|
||||
格式化聚合结果为 API 返回格式。
|
||||
|
||||
**参数**:
|
||||
- `aggregated_data: QuerySet[PlateOrder]` - 聚合后的查询集
|
||||
|
||||
**返回**:
|
||||
- `list[dict]` - 格式化后的数据
|
||||
|
||||
**职责**:
|
||||
- 遍历聚合结果
|
||||
- 按客户分组
|
||||
- 返回客户ID(`client_id`)和客户名称(`client_name`)
|
||||
- 过滤全0数据
|
||||
- 生成最终的 API 返回格式
|
||||
|
||||
---
|
||||
|
||||
### 6. `_filter_zero_data`
|
||||
|
||||
过滤全0数据。
|
||||
|
||||
**参数**:
|
||||
- `plate_order_counts: list[dict]` - 订单统计列表
|
||||
|
||||
**返回**:
|
||||
- `list[dict]` - 过滤后的列表
|
||||
|
||||
**职责**:
|
||||
- 移除 today 和 current_month 都为 0 的数据
|
||||
|
||||
---
|
||||
|
||||
## 数据流程
|
||||
|
||||
```
|
||||
get_plate_order_summary_by_customer
|
||||
↓
|
||||
_get_plate_order_queryset (获取基础查询集)
|
||||
↓
|
||||
_aggregate_plate_orders_by_customer_and_type (分组聚合)
|
||||
↓
|
||||
_get_month_date_range (获取日期范围)
|
||||
↓
|
||||
_format_plate_order_summary (格式化结果)
|
||||
↓
|
||||
_filter_zero_data (过滤全0数据)
|
||||
```
|
||||
|
||||
## 过滤规则
|
||||
|
||||
1. **商户隔离**: 只查询指定商户的订单
|
||||
2. **plate_type 过滤**: `plate_type` 为空的订单不纳入统计
|
||||
3. **production_method 过滤**: `production_method` 为空的订单不纳入统计
|
||||
4. **客户可见性**: 非超级用户只能看到自己创建的客户或被授权可见的客户
|
||||
5. **全0数据过滤**: today 和 current_month 都为 0 的类型组合不返回
|
||||
6. **客户分组稳健性**: 以 customer_id 分组,避免同名客户被合并
|
||||
|
||||
## type 组合规则
|
||||
|
||||
**格式**: `{plate_type}-{production_method}`
|
||||
|
||||
**plate_type 可能的值**:
|
||||
- `首版`
|
||||
- `修改`
|
||||
|
||||
**production_method 可能的值**:
|
||||
- `定位`
|
||||
- `匹布`
|
||||
|
||||
**组合示例**:
|
||||
- `首版-定位`
|
||||
- `首版-匹布`
|
||||
- `修改-定位`
|
||||
- `修改-匹布`
|
||||
|
||||
## 测试策略
|
||||
|
||||
每个函数都应该有独立的单元测试:
|
||||
- `_get_plate_order_queryset`: 测试商户隔离、客户可见性过滤、plate_type 和 production_method 过滤
|
||||
- `_get_month_date_range`: 测试日期范围计算
|
||||
- `_aggregate_plate_orders_by_customer_and_type`: 测试分组聚合逻辑
|
||||
- `_format_plate_order_summary`: 测试数据格式化
|
||||
- `_filter_zero_data`: 测试全0数据过滤
|
||||
- `get_plate_order_summary_by_customer`: 集成测试
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. 使用 `select_related` 减少查询次数
|
||||
2. 使用 `annotate` 和聚合函数避免 N+1 查询
|
||||
3. 使用条件聚合(Case/When)一次查询获取 today 和 current_month
|
||||
|
||||
## PostgreSQL 优化评估记录
|
||||
|
||||
1. 已评估数据库层优化(索引、视图、物化视图)
|
||||
2. 当前决策:暂不实施结构性优化,优先保持线上稳定性
|
||||
3. 后续若优化,优先级为:索引 > 表达式索引 > 物化视图
|
||||
4. 生产约束:索引变更需使用并发建索引方式并在低峰执行
|
||||
Reference in New Issue
Block a user