# 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)
