forked from erp-dev/erp
feat: product quick api and param create api
This commit is contained in:
499
api_v1/views/products/API.md
Normal file
499
api_v1/views/products/API.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# Products API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
Products API 提供轻量级的产品查询接口,专门为前端下拉框和自动完成功能设计。
|
||||
|
||||
**特点**:
|
||||
- 只返回 `id` 和 `name` 字段,减少数据传输
|
||||
- 支持分页(LimitOffset)
|
||||
- 支持按名称模糊搜索
|
||||
- 需要 JWT 认证
|
||||
|
||||
**基础路径**: `/api/v1/products/quick/`
|
||||
|
||||
---
|
||||
|
||||
## 认证
|
||||
|
||||
所有接口都需要 JWT 认证。在请求头中添加:
|
||||
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口详情
|
||||
|
||||
### 获取产品列表
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
GET /api/v1/products/quick/
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| limit | integer | 否 | 返回结果数量,不提供则返回所有 |
|
||||
| offset | integer | 否 | 偏移量,默认 0 |
|
||||
| search | string | 否 | 按产品名称模糊搜索(不区分大小写) |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```bash
|
||||
# 获取所有产品
|
||||
curl -X GET "http://localhost:8000/api/v1/products/quick/" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# 分页获取(前10条)
|
||||
curl -X GET "http://localhost:8000/api/v1/products/quick/?limit=10&offset=0" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# 按名称搜索
|
||||
curl -X GET "http://localhost:8000/api/v1/products/quick/?search=布料" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# 搜索 + 分页
|
||||
curl -X GET "http://localhost:8000/api/v1/products/quick/?search=布料&limit=5&offset=0" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
**成功响应 - 不分页** (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 5,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "纯棉布料"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "涤纶布料"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "丝绸布料"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "麻布"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "牛仔布"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应 - 带分页** (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 50,
|
||||
"next": "http://localhost:8000/api/v1/products/quick/?limit=10&offset=10",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "纯棉布料"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "涤纶布料"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 401 Unauthorized | 未认证或 Token 无效 |
|
||||
| 403 Forbidden | 无权限访问 |
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 前端下拉框
|
||||
|
||||
```javascript
|
||||
// React 示例
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function ProductSelect() {
|
||||
const [products, setProducts] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProducts = async () => {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/v1/products/quick/?limit=100', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
setProducts(data.results);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<select>
|
||||
{products.map(product => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 自动完成(Autocomplete)
|
||||
|
||||
```javascript
|
||||
// Vue 3 + Element Plus 示例
|
||||
<template>
|
||||
<el-autocomplete
|
||||
v-model="searchText"
|
||||
:fetch-suggestions="queryProducts"
|
||||
placeholder="搜索产品"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const searchText = ref('');
|
||||
|
||||
const queryProducts = async (queryString, cb) => {
|
||||
const url = queryString
|
||||
? `/api/v1/products/quick/?search=${encodeURIComponent(queryString)}&limit=10`
|
||||
: '/api/v1/products/quick/?limit=10';
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 转换为 Element Plus 需要的格式
|
||||
const suggestions = data.results.map(p => ({
|
||||
value: p.name,
|
||||
id: p.id
|
||||
}));
|
||||
|
||||
cb(suggestions);
|
||||
};
|
||||
|
||||
const handleSelect = (item) => {
|
||||
console.log('选中产品:', item.id, item.value);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. 搜索框实时搜索
|
||||
|
||||
```javascript
|
||||
// Vanilla JavaScript 示例
|
||||
const searchInput = document.getElementById('product-search');
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
|
||||
let debounceTimer;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(debounceTimer);
|
||||
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const searchText = e.target.value.trim();
|
||||
|
||||
if (searchText.length < 2) {
|
||||
resultsContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/v1/products/quick/?search=${encodeURIComponent(searchText)}&limit=10`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 渲染搜索结果
|
||||
resultsContainer.innerHTML = data.results.map(product => `
|
||||
<div class="search-result" data-id="${product.id}">
|
||||
${product.name}
|
||||
</div>
|
||||
`).join('');
|
||||
}, 300); // 300ms 防抖
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Python 使用示例
|
||||
|
||||
### 基础查询
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
TOKEN = "your_jwt_token"
|
||||
headers = {"Authorization": f"Bearer {TOKEN}"}
|
||||
|
||||
# 获取所有产品
|
||||
response = requests.get(f"{BASE_URL}/api/v1/products/quick/", headers=headers)
|
||||
products = response.json()
|
||||
|
||||
print(f"共有 {products['count']} 个产品:")
|
||||
for product in products['results']:
|
||||
print(f" - {product['id']}: {product['name']}")
|
||||
```
|
||||
|
||||
### 分页查询
|
||||
|
||||
```python
|
||||
def get_all_products(token):
|
||||
"""获取所有产品(自动处理分页)"""
|
||||
BASE_URL = "http://localhost:8000"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
all_products = []
|
||||
offset = 0
|
||||
limit = 50
|
||||
|
||||
while True:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/products/quick/?limit={limit}&offset={offset}",
|
||||
headers=headers
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
all_products.extend(data['results'])
|
||||
|
||||
if data['next'] is None:
|
||||
break
|
||||
|
||||
offset += limit
|
||||
|
||||
return all_products
|
||||
|
||||
# 使用
|
||||
products = get_all_products(token)
|
||||
print(f"总共获取了 {len(products)} 个产品")
|
||||
```
|
||||
|
||||
### 搜索查询
|
||||
|
||||
```python
|
||||
def search_products(token, query, limit=10):
|
||||
"""搜索产品"""
|
||||
BASE_URL = "http://localhost:8000"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/products/quick/?search={query}&limit={limit}",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return response.json()['results']
|
||||
|
||||
# 使用
|
||||
results = search_products(token, "布料", limit=5)
|
||||
for product in results:
|
||||
print(f"{product['id']}: {product['name']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 前端缓存
|
||||
|
||||
```javascript
|
||||
// 使用本地缓存避免重复请求
|
||||
class ProductCache {
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
this.cacheTimeout = 5 * 60 * 1000; // 5分钟
|
||||
}
|
||||
|
||||
async getProducts(search = '', limit = 100) {
|
||||
const cacheKey = `${search}:${limit}`;
|
||||
const cached = this.cache.get(cacheKey);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const url = search
|
||||
? `/api/v1/products/quick/?search=${encodeURIComponent(search)}&limit=${limit}`
|
||||
: `/api/v1/products/quick/?limit=${limit}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
this.cache.set(cacheKey, {
|
||||
data: data.results,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return data.results;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const productCache = new ProductCache();
|
||||
```
|
||||
|
||||
### 防抖搜索
|
||||
|
||||
```javascript
|
||||
// 防抖函数避免频繁请求
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
const searchProducts = debounce(async (query) => {
|
||||
const response = await fetch(
|
||||
`/api/v1/products/quick/?search=${encodeURIComponent(query)}&limit=10`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
updateSearchResults(data.results);
|
||||
}, 300);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么只返回 id 和 name?
|
||||
|
||||
**A**: 这是一个轻量级接口,专门为下拉框和自动完成设计。只返回必要的字段可以:
|
||||
- 减少数据传输量
|
||||
- 提高响应速度
|
||||
- 降低服务器负载
|
||||
|
||||
如果需要完整的产品信息,请使用完整的产品 API。
|
||||
|
||||
### Q: 搜索是否区分大小写?
|
||||
|
||||
**A**: 不区分。搜索使用 `icontains` 查询,对大小写不敏感。
|
||||
|
||||
### Q: 最多能返回多少条数据?
|
||||
|
||||
**A**: 建议使用 `limit` 参数限制返回数量。对于下拉框,建议 `limit=100`;对于自动完成,建议 `limit=10`。
|
||||
|
||||
### Q: 如何实现"加载更多"功能?
|
||||
|
||||
**A**: 使用 `offset` 参数进行分页。每次增加 `offset` 的值来获取下一页数据。
|
||||
|
||||
```javascript
|
||||
let offset = 0;
|
||||
const limit = 20;
|
||||
|
||||
async function loadMore() {
|
||||
const response = await fetch(
|
||||
`/api/v1/products/quick/?limit=${limit}&offset=${offset}`,
|
||||
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
appendProducts(data.results);
|
||||
offset += limit;
|
||||
|
||||
// 检查是否还有更多数据
|
||||
return data.next !== null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试
|
||||
|
||||
完整的测试套件位于 `api_v1/views/products/test_products_api.py`。
|
||||
|
||||
运行测试:
|
||||
|
||||
```bash
|
||||
# 运行所有产品 API 测试
|
||||
python manage.py test api_v1.views.products
|
||||
|
||||
# 运行特定测试
|
||||
python manage.py test api_v1.views.products.test_products_api.ProductQuickAPITestCase.test_search_products_by_name
|
||||
```
|
||||
|
||||
测试覆盖:
|
||||
- ✅ 基础列表查询(带/不带分页)
|
||||
- ✅ 名称模糊搜索
|
||||
- ✅ 搜索 + 分页组合
|
||||
- ✅ 大小写不敏感搜索
|
||||
- ✅ 空结果处理
|
||||
- ✅ 认证检查
|
||||
- ✅ 响应格式验证
|
||||
- ✅ 边界情况测试
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2025-11-18
|
||||
|
||||
- 初始版本发布
|
||||
- 支持产品列表查询(仅 id 和 name)
|
||||
- 支持分页(LimitOffset)
|
||||
- 支持按名称模糊搜索
|
||||
- 完整的测试覆盖
|
||||
|
||||
---
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [Basic Info Models](../../basic_info/models.py) - Product 模型定义
|
||||
- [API 路由配置](../../urls.py)
|
||||
- [测试文件](./test_products_api.py)
|
||||
Reference in New Issue
Block a user