forked from erp-dev/erp
10 KiB
10 KiB
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 | 否 | 按产品名称模糊搜索(不区分大小写) |
请求示例
# 获取所有产品
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)
{
"count": 5,
"results": [
{
"id": 1,
"name": "纯棉布料"
},
{
"id": 2,
"name": "涤纶布料"
},
{
"id": 3,
"name": "丝绸布料"
},
{
"id": 4,
"name": "麻布"
},
{
"id": 5,
"name": "牛仔布"
}
]
}
成功响应 - 带分页 (200 OK)
{
"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 | 无权限访问 |
{
"detail": "Authentication credentials were not provided."
}
使用场景
1. 前端下拉框
// 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)
// 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. 搜索框实时搜索
// 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 使用示例
基础查询
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']}")
分页查询
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)} 个产品")
搜索查询
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']}")
性能优化建议
前端缓存
// 使用本地缓存避免重复请求
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();
防抖搜索
// 防抖函数避免频繁请求
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 的值来获取下一页数据。
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。
运行测试:
# 运行所有产品 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 - Product 模型定义
- API 路由配置
- 测试文件