forked from erp-dev/erp
fea: business completed
This commit is contained in:
267
docs/api_v2_wecom_binduser.md
Normal file
267
docs/api_v2_wecom_binduser.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# 企业微信用户绑定 API
|
||||
|
||||
## 接口说明
|
||||
|
||||
供企业微信服务端程序调用,将企业微信 OAuth2 获取的 `user_id` 与本系统的员工(Employee)绑定。调用方需提供本系统的用户名和密码进行身份验证,验证通过后完成绑定。
|
||||
|
||||
## 接口信息
|
||||
|
||||
- **URL**: `/api/v2/wecom/binduser/`
|
||||
- **方法**: `POST`
|
||||
- **Content-Type**: `application/json`
|
||||
- **认证方式**: 固定密钥,通过 `Authorization` 请求头传入(不使用 Bearer 前缀)
|
||||
|
||||
## 认证
|
||||
|
||||
请求必须在 `Authorization` 头中携带预分配的访问密钥(由系统管理员提供)。
|
||||
|
||||
```
|
||||
Authorization: your-access-key
|
||||
```
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| wecom_user_id | string | 是 | 企业微信 OAuth2 返回的 user_id,最长 128 字符 |
|
||||
| username | string | 是 | 本系统用户名 |
|
||||
| password | string | 是 | 本系统用户密码 |
|
||||
| force_update | boolean | 否 | 如果该员工已绑定其他企业微信用户,是否强制覆盖。默认 `false` |
|
||||
|
||||
## 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST "https://your-domain/api/v2/wecom/binduser/" \
|
||||
-H "Authorization: your-access-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"wecom_user_id": "zhangsan",
|
||||
"username": "zhangsan",
|
||||
"password": "mypassword123"
|
||||
}'
|
||||
```
|
||||
|
||||
## 响应说明
|
||||
|
||||
### 绑定成功(200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "绑定成功",
|
||||
"employee_id": 42,
|
||||
"employee_name": "张三",
|
||||
"wecom_user_id": "zhangsan"
|
||||
}
|
||||
```
|
||||
|
||||
### 用户名或密码错误(401 Unauthorized)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "用户名或密码错误"
|
||||
}
|
||||
```
|
||||
|
||||
### 用户已被禁用(403 Forbidden)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "该用户已被禁用"
|
||||
}
|
||||
```
|
||||
|
||||
### 用户未关联员工(404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "该用户未关联任何员工记录"
|
||||
}
|
||||
```
|
||||
|
||||
### 企业微信用户已绑定到其他员工(409 Conflict)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "该企业微信用户已绑定到其他员工: 李四 (ID: 15)"
|
||||
}
|
||||
```
|
||||
|
||||
### 员工已绑定其他企业微信用户(409 Conflict)
|
||||
|
||||
当该员工已有不同的 `wecom_user_id` 且未传 `force_update=true` 时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "该员工已绑定其他企业微信用户",
|
||||
"current_wecom_user_id": "old_wecom_id",
|
||||
"hint": "如需覆盖,请传入 force_update=true"
|
||||
}
|
||||
```
|
||||
|
||||
### 认证失败(401 Unauthorized)
|
||||
|
||||
Authorization 头缺失或密钥错误:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "AGENT_ACCESS_KEY 无效"
|
||||
}
|
||||
```
|
||||
|
||||
### 参数验证失败(400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"wecom_user_id": ["该字段是必填项。"],
|
||||
"username": ["该字段是必填项。"],
|
||||
"password": ["该字段是必填项。"]
|
||||
}
|
||||
```
|
||||
|
||||
## 业务规则
|
||||
|
||||
1. **一对一约束**:一个企业微信 user_id 只能绑定一个员工,一个员工只能绑定一个企业微信 user_id。
|
||||
2. **幂等性**:重复绑定相同的 wecom_user_id 到同一个员工不会报错,返回 200。
|
||||
3. **冲突处理**:
|
||||
- 如果 `wecom_user_id` 已被其他员工绑定 → 拒绝(409)
|
||||
- 如果该员工已绑定不同的 `wecom_user_id` 且 `force_update=false` → 拒绝(409)
|
||||
- 如果该员工已绑定不同的 `wecom_user_id` 且 `force_update=true` → 覆盖绑定
|
||||
4. **身份验证**:通过 Django 内置认证机制验证用户名和密码,验证通过后通过 `User → Employee` 关系找到对应员工。
|
||||
|
||||
## 接入流程
|
||||
|
||||
```
|
||||
企业微信用户 → OAuth2 授权 → 获取 wecom user_id
|
||||
↓
|
||||
调用本接口(传入 wecom_user_id + 本系统 username/password)
|
||||
↓
|
||||
验证凭据 → 查找 Employee → 写入 wecom_user_id
|
||||
↓
|
||||
绑定完成,后续可通过 wecom_user_id 识别用户身份
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 密码通过 HTTPS 传输,请确保生产环境使用 TLS。
|
||||
2. 该接口不限制调用频率,但建议调用方做适当限流。
|
||||
3. 绑定成功后,后续业务可通过 `Employee.wecom_user_id` 字段查找对应员工。
|
||||
|
||||
---
|
||||
|
||||
# 企业微信快捷登录 API
|
||||
|
||||
## 接口说明
|
||||
|
||||
供企业微信服务端程序调用,通过已绑定的 `wecom_user_id` 快捷获取本系统的 JWT token,无需再次输入用户名密码。
|
||||
|
||||
前提条件:该 `wecom_user_id` 必须已通过绑定接口(`/api/v2/wecom/binduser/`)完成绑定。
|
||||
|
||||
## 接口信息
|
||||
|
||||
- **URL**: `/api/v2/wecom/login/`
|
||||
- **方法**: `POST`
|
||||
- **Content-Type**: `application/json`
|
||||
- **认证方式**: 固定密钥,通过 `Authorization` 请求头传入(与绑定接口相同)
|
||||
|
||||
## 认证
|
||||
|
||||
```
|
||||
Authorization: your-access-key
|
||||
```
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| wecom_user_id | string | 是 | 企业微信 OAuth2 返回的 user_id |
|
||||
|
||||
## 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST "https://your-domain/api/v2/wecom/login/" \
|
||||
-H "Authorization: your-access-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"wecom_user_id": "zhangsan"
|
||||
}'
|
||||
```
|
||||
|
||||
## 响应说明
|
||||
|
||||
### 登录成功(200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"employee_id": 42,
|
||||
"employee_name": "张三",
|
||||
"username": "zhangsan"
|
||||
}
|
||||
```
|
||||
|
||||
返回的 `access` token 可直接用于后续业务 API 的 `Authorization: Bearer <token>` 认证。
|
||||
|
||||
### 企业微信用户未绑定(404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "该企业微信用户未绑定本系统员工"
|
||||
}
|
||||
```
|
||||
|
||||
### 员工未关联系统用户(404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "该员工未关联系统用户"
|
||||
}
|
||||
```
|
||||
|
||||
### 用户已被禁用(403 Forbidden)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "该用户已被禁用"
|
||||
}
|
||||
```
|
||||
|
||||
### 认证失败(401 Unauthorized)
|
||||
|
||||
Authorization 头缺失或密钥错误:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "AGENT_ACCESS_KEY 无效"
|
||||
}
|
||||
```
|
||||
|
||||
### 参数验证失败(400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"wecom_user_id": ["该字段是必填项。"]
|
||||
}
|
||||
```
|
||||
|
||||
## 业务规则
|
||||
|
||||
1. 该接口仅对已完成绑定的 `wecom_user_id` 有效。
|
||||
2. 每次调用都会生成新的 JWT token 对(access + refresh)。
|
||||
3. access token 有效期为 7 天(与系统 JWT 配置一致)。
|
||||
4. 安全边界依赖 `AGENT_ACCESS_KEY`,请妥善保管该密钥。
|
||||
|
||||
## 典型使用流程
|
||||
|
||||
```
|
||||
用户首次使用:
|
||||
企业微信 OAuth2 → 获取 wecom_user_id
|
||||
→ 调用 /api/v2/wecom/binduser/(传 wecom_user_id + username + password)
|
||||
→ 绑定完成
|
||||
|
||||
用户后续登录:
|
||||
企业微信 OAuth2 → 获取 wecom_user_id
|
||||
→ 调用 /api/v2/wecom/login/(仅传 wecom_user_id)
|
||||
→ 获取 JWT token
|
||||
→ 使用 token 访问业务 API
|
||||
```
|
||||
201
docs/statements_pagination_performance_evaluation.md
Normal file
201
docs/statements_pagination_performance_evaluation.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# 客户对账单 API 分页与性能评估
|
||||
|
||||
本文档评估当前 `/api/v1/customers/<id>/statements/` 接口的分页能力与性能瓶颈,供前后端共同讨论优化方案。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前现状
|
||||
|
||||
### 接口行为
|
||||
|
||||
- 每次请求返回**该客户的全部对账记录**(销售单 + 退货单 + 收款单 + 外部业务依据)
|
||||
- 无分页参数,无日期过滤
|
||||
- 排序固定:`occurred_at → recorded_at → source_id` 倒序
|
||||
- 每条记录包含 `cumulative_amount`(滚动累计)和 `arrears_amount`(实时欠款)
|
||||
|
||||
### 数据规模参考
|
||||
|
||||
| 客户 | 销售单 | 退货单 | 收款单 | 总记录数 | 响应体积(估算) |
|
||||
|------|--------|--------|--------|----------|-----------------|
|
||||
| 木棉 | ~200 | 1 | ~50 | ~250 | ~100KB |
|
||||
| 腾飞纺织 | ~1,500 | 9 | ~92 | ~1,600 | ~800KB |
|
||||
| 张晓鹏 | ~3,100 | 6 | ~219 | ~3,350 | ~2MB |
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么当前无法直接分页
|
||||
|
||||
核心原因:**滚动累计字段(`cumulative_amount` / `arrears_amount`)依赖前序所有记录的计算结果。**
|
||||
|
||||
```
|
||||
结欠[n] = 结欠[n-1] + 本行应收 - 本行已收
|
||||
```
|
||||
|
||||
要展示第 N 页的 `arrears_amount`,必须先计算前 N-1 页所有记录的累加值。这意味着:
|
||||
- 不能简单用 DB 的 `OFFSET/LIMIT`
|
||||
- 不能跳页
|
||||
- 后端必须从第一条开始逐条计算
|
||||
|
||||
---
|
||||
|
||||
## 3. 性能瓶颈分析
|
||||
|
||||
每次 API 调用的执行路径:
|
||||
|
||||
```
|
||||
1. 4 次 DB 查询(sales / returns / receipts / external_statements)
|
||||
└── 每次都 prefetch_related('items__product') 加载全部明细
|
||||
2. Python 内存中合并 + 排序全部记录
|
||||
3. 逐条遍历计算 running totals
|
||||
4. 全量序列化为 JSON(含 items 明细数组)
|
||||
5. HTTP 响应传输
|
||||
```
|
||||
|
||||
对于张晓鹏(3350 条记录):
|
||||
- DB 查询:~200ms(4 次查询 + prefetch)
|
||||
- Python 排序 + 计算:~50ms
|
||||
- JSON 序列化:~300ms
|
||||
- 网络传输(2MB):取决于带宽
|
||||
|
||||
**`StatementRecordView`(单条查询)更严重**:为了查 1 条记录,构建了完整对账单再过滤。
|
||||
|
||||
---
|
||||
|
||||
## 4. 优化方案对比
|
||||
|
||||
### 方案 A:游标分页(推荐短期方案)
|
||||
|
||||
**原理**:前端传 `page_size` + 上一页最后一条的 `cumulative_amount` 作为 cursor,后端从 cursor 继续累加。
|
||||
|
||||
```
|
||||
GET /customers/6/statements/?page_size=50
|
||||
GET /customers/6/statements/?page_size=50&cursor=eyJjdW11bGF0aXZlIjoiMzE4OTEuMDAiLCJsYXN0X2lkIjoxODE0MX0=
|
||||
```
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 减少响应体积 | 后端仍需全量查询(但只序列化一页) |
|
||||
| 前端按需加载 | 不能跳页,只能顺序翻页 |
|
||||
| 向后兼容(不传参数 = 全量) | 需要前端配合改造 |
|
||||
|
||||
**后端改动**:中等。排序 + running total 计算后,只返回 cursor 之后的 N 条。
|
||||
|
||||
---
|
||||
|
||||
### 方案 B:日期范围过滤
|
||||
|
||||
**原理**:前端传 `date_from` / `date_to`,后端只查询范围内的记录。
|
||||
|
||||
```
|
||||
GET /customers/6/statements/?date_from=2026-01-01&date_to=2026-05-18
|
||||
```
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 实现简单 | running total 仍需从历史第一条开始算 |
|
||||
| 减少返回数据量 | 或者放弃 running total 的准确性 |
|
||||
| 前端可做"按年/按月"切换 | |
|
||||
|
||||
**后端改动**:低。在 DB 查询加 `occurred_at` 过滤即可。但 `cumulative_amount` 需要决定:
|
||||
- 选项 1:从第一条算到 date_to(准确但慢)
|
||||
- 选项 2:只在返回范围内累计(快但不连续)
|
||||
|
||||
---
|
||||
|
||||
### 方案 C:延迟加载 items 明细
|
||||
|
||||
**原理**:默认不返回 `items` 数组,前端需要时单独请求。
|
||||
|
||||
```
|
||||
GET /customers/6/statements/ → 不含 items
|
||||
GET /customers/6/statements/?include_items=true → 含 items(当前行为)
|
||||
GET /statements/record/?...&include_items=true → 单条含 items
|
||||
```
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 响应体积减少 50%+ | 前端需要额外请求获取明细 |
|
||||
| 后端改动极小 | 如果前端表格需要展开明细,交互变复杂 |
|
||||
| 完全向后兼容 | |
|
||||
|
||||
**后端改动**:极低。序列化时根据参数决定是否包含 items。
|
||||
|
||||
---
|
||||
|
||||
### 方案 D:预计算快照(长期方案)
|
||||
|
||||
**原理**:同步时预计算每条记录的 `cumulative_amount` / `arrears_amount` 存入 DB,查询时直接分页。
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 真正的 DB 级分页 | 需要新表或新字段 |
|
||||
| 查询性能最优 | 数据一致性维护复杂(任何单据变动需重算) |
|
||||
| 支持跳页 | 开发成本高 |
|
||||
|
||||
**后端改动**:高。需要设计快照表 + 触发重算机制。
|
||||
|
||||
---
|
||||
|
||||
### 方案 E:响应缓存
|
||||
|
||||
**原理**:对账单结果缓存 N 秒(或按数据版本号缓存),重复请求直接返回。
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 零前端改动 | 数据有延迟(缓存过期前看不到最新) |
|
||||
| 后端改动极小 | 大客户首次请求仍然慢 |
|
||||
| 对高频刷新场景效果显著 | |
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐实施路径
|
||||
|
||||
```
|
||||
第一步(立即可做):方案 C — 默认不返回 items,减少 50%+ 响应体积
|
||||
第二步(短期):方案 B — 加日期范围过滤,前端做"按月/按年"切换
|
||||
第三步(中期):方案 A — 游标分页,前端改为滚动加载
|
||||
第四步(按需):方案 E — 缓存,应对高频刷新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 需要前端确认的问题
|
||||
|
||||
1. **对账单表格是否需要一次展示全部记录?** 还是可以接受分页/滚动加载?
|
||||
2. **items 明细是否默认展示?** 还是用户点击展开时才加载?
|
||||
3. **是否需要"按月/按年"切换?** 如果需要,running total 是否可以只在当前范围内累计?
|
||||
4. **`cumulative_amount` / `arrears_amount` 是否是必须字段?** 如果前端不使用这两个字段,分页就变得简单很多。
|
||||
5. **导出 Excel 功能是否需要全量数据?** 如果需要,导出可以走单独的异步接口。
|
||||
|
||||
---
|
||||
|
||||
## 7. 当前接口响应结构(供参考)
|
||||
|
||||
```json
|
||||
{
|
||||
"counterparty": 6,
|
||||
"counterparty_name": "张晓鹏",
|
||||
"records": [
|
||||
{
|
||||
"source_type": "external_sales_order",
|
||||
"source_label": "外部销售单",
|
||||
"source_id": 18141,
|
||||
"occurred_at": "2026-05-18",
|
||||
"recorded_at": "2026-05-18T21:45:03Z",
|
||||
"status": 2,
|
||||
"status_label": "已同步",
|
||||
"positive_amount": "2436.00",
|
||||
"negative_amount": "0.00",
|
||||
"cumulative_amount": "31891.00",
|
||||
"current_balance": "780454.00",
|
||||
"arrears_amount": "748563.00",
|
||||
"remarks": "...",
|
||||
"items": [ ... ]
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"positive_total": "26981509.80",
|
||||
"negative_total": "26201055.80"
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user