haha
This commit is contained in:
318
API_CONTACTS.md
Normal file
318
API_CONTACTS.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# 联系记录接口文档
|
||||
|
||||
## 1. 查询联系记录列表
|
||||
|
||||
### 接口信息
|
||||
- **URL**: `/api/contacts`
|
||||
- **方法**: `GET`
|
||||
- **认证**: 需要登录(Header 携带 Authorization)
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| page | integer | 否 | 页码,默认 1 | 1 |
|
||||
| page_size | integer | 否 | 每页数量,默认 10 | 20 |
|
||||
| search | string | 否 | 搜索关键词(姓名/岗位/联系方式) | 张三 |
|
||||
| reply_status | string | 否 | 回复状态筛选 | 已回复 |
|
||||
| wechat_exchanged | boolean | 否 | 是否交换微信 | true |
|
||||
| start_date | string | 否 | 开始日期(YYYY-MM-DD) | 2024-01-01 |
|
||||
| end_date | string | 否 | 结束日期(YYYY-MM-DD) | 2024-12-31 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
# 基础查询
|
||||
GET /api/contacts?page=1&page_size=20
|
||||
|
||||
# 关键词搜索
|
||||
GET /api/contacts?search=张三
|
||||
|
||||
# 按回复状态筛选
|
||||
GET /api/contacts?reply_status=已回复
|
||||
|
||||
# 按时间范围筛选
|
||||
GET /api/contacts?start_date=2024-01-01&end_date=2024-12-31
|
||||
|
||||
# 组合筛选
|
||||
GET /api/contacts?search=产品经理&reply_status=已回复&wechat_exchanged=true&start_date=2024-01-01&end_date=2024-12-31
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"total": 150,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "张三",
|
||||
"position": "产品经理",
|
||||
"contact": "13800138000",
|
||||
"reply_status": "已回复",
|
||||
"wechat_exchanged": true,
|
||||
"account_id": 1,
|
||||
"worker_id": "worker-001",
|
||||
"notes": "沟通顺畅,有意向",
|
||||
"contacted_at": "2024-03-01T10:30:00",
|
||||
"created_at": "2024-03-01T10:00:00"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "李四",
|
||||
"position": "前端工程师",
|
||||
"contact": "wechat:lisi123",
|
||||
"reply_status": "未回复",
|
||||
"wechat_exchanged": false,
|
||||
"account_id": 2,
|
||||
"worker_id": "worker-002",
|
||||
"notes": "",
|
||||
"contacted_at": "2024-03-02T14:20:00",
|
||||
"created_at": "2024-03-02T14:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 导出联系记录为 Excel
|
||||
|
||||
### 接口信息
|
||||
- **URL**: `/api/contacts/export`
|
||||
- **方法**: `GET`
|
||||
- **认证**: 需要登录(Header 携带 Authorization)
|
||||
- **响应类型**: `application/json`(返回下载链接)
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| search | string | 否 | 搜索关键词(姓名/岗位/联系方式) | 张三 |
|
||||
| reply_status | string | 否 | 回复状态筛选 | 已回复 |
|
||||
| wechat_exchanged | boolean | 否 | 是否交换微信 | true |
|
||||
| start_date | string | 否 | 开始日期(YYYY-MM-DD) | 2024-01-01 |
|
||||
| end_date | string | 否 | 结束日期(YYYY-MM-DD) | 2024-12-31 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
# 导出所有联系记录
|
||||
GET /api/contacts/export
|
||||
|
||||
# 导出指定时间段的记录
|
||||
GET /api/contacts/export?start_date=2024-01-01&end_date=2024-12-31
|
||||
|
||||
# 导出已回复且交换微信的记录
|
||||
GET /api/contacts/export?reply_status=已回复&wechat_exchanged=true
|
||||
|
||||
# 导出组合筛选结果
|
||||
GET /api/contacts/export?search=产品经理&start_date=2024-01-01&end_date=2024-03-31&reply_status=已回复
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"download_url": "/media/exports/contacts_export_20240304_153045_a1b2c3d4.xlsx",
|
||||
"filename": "contacts_export_20240304_153045_a1b2c3d4.xlsx",
|
||||
"total_records": 150,
|
||||
"generated_at": "2024-03-04 15:30:45"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| download_url | string | 文件下载链接(相对路径) |
|
||||
| filename | string | 生成的文件名 |
|
||||
| total_records | integer | 导出的记录总数 |
|
||||
| generated_at | string | 文件生成时间 |
|
||||
|
||||
### Excel 文件格式
|
||||
|
||||
**表头**(蓝色背景,白色粗体文字):
|
||||
|
||||
| 列名 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| ID | 记录 ID | 1 |
|
||||
| 姓名 | 联系人姓名 | 张三 |
|
||||
| 岗位 | 应聘岗位 | 产品经理 |
|
||||
| 联系方式 | 电话或微信 | 13800138000 |
|
||||
| 回复状态 | 回复状态 | 已回复 |
|
||||
| 是否交换微信 | 是/否 | 是 |
|
||||
| Worker ID | Worker 标识 | worker-001 |
|
||||
| 备注 | 备注信息 | 沟通顺畅,有意向 |
|
||||
| 联系时间 | 联系时间 | 2024-03-01 10:30:00 |
|
||||
| 创建时间 | 创建时间 | 2024-03-01 10:00:00 |
|
||||
|
||||
### 前端调用示例
|
||||
|
||||
```javascript
|
||||
// 使用 axios
|
||||
const exportContacts = async (params) => {
|
||||
try {
|
||||
const response = await axios.get('/api/contacts/export', {
|
||||
params: params,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
const { download_url, filename, total_records } = response.data.data;
|
||||
|
||||
// 方式1:直接打开下载链接
|
||||
window.open(download_url, '_blank');
|
||||
|
||||
// 方式2:使用 a 标签下载
|
||||
const link = document.createElement('a');
|
||||
link.href = download_url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 提示用户
|
||||
console.log(`成功导出 ${total_records} 条记录`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 调用示例
|
||||
exportContacts({
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-12-31',
|
||||
reply_status: '已回复'
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 使用 fetch
|
||||
const exportContacts = async (params) => {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/contacts/export?${queryString}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
const { download_url, filename, total_records } = result.data;
|
||||
|
||||
// 直接下载文件
|
||||
const link = document.createElement('a');
|
||||
link.href = download_url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
|
||||
alert(`成功导出 ${total_records} 条记录`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- Vue 3 组件示例 -->
|
||||
<template>
|
||||
<button @click="handleExport" :loading="exporting">
|
||||
导出 Excel
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const exporting = ref(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
exporting.value = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/contacts/export', {
|
||||
params: {
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-12-31'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
const { download_url, total_records } = response.data.data;
|
||||
|
||||
// 下载文件
|
||||
window.open(download_url, '_blank');
|
||||
|
||||
// 显示成功提示
|
||||
ElMessage.success(`成功导出 ${total_records} 条记录`);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('导出失败,请重试');
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未登录或 token 无效 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **时间筛选**:
|
||||
- `start_date` 从当天 00:00:00 开始
|
||||
- `end_date` 到当天 23:59:59 结束
|
||||
- 时间筛选基于 `created_at` 字段(创建时间)
|
||||
|
||||
2. **搜索功能**:
|
||||
- `search` 参数支持模糊匹配
|
||||
- 同时搜索姓名、岗位、联系方式三个字段
|
||||
|
||||
3. **导出机制**:
|
||||
- 导出接口返回下载链接,不直接返回文件流
|
||||
- 文件保存在服务器 `media/exports/` 目录
|
||||
- 文件名格式:`contacts_export_{时间戳}_{随机ID}.xlsx`
|
||||
- 导出接口不分页,会导出所有符合条件的记录
|
||||
- 建议在导出大量数据时添加时间范围限制
|
||||
|
||||
4. **文件管理**:
|
||||
- 导出的文件会保存在服务器上
|
||||
- 建议定期清理过期的导出文件
|
||||
- 可以通过定时任务删除超过一定时间的文件
|
||||
|
||||
5. **认证要求**:
|
||||
- 所有接口都需要在 Header 中携带 `Authorization` token
|
||||
- token 格式:`Bearer <token>`
|
||||
|
||||
6. **下载链接**:
|
||||
- 返回的 `download_url` 是相对路径
|
||||
- 前端需要拼接完整的服务器地址(如果跨域)
|
||||
- 或直接使用相对路径(如果同域)
|
||||
@@ -6,6 +6,7 @@ uvicorn>=0.34.0
|
||||
pydantic>=2.0.0
|
||||
PyMySQL>=1.1.0
|
||||
asgiref>=3.8.0
|
||||
openpyxl>=3.1.0
|
||||
|
||||
# ─── 客户端 GUI ───
|
||||
PyQt5>=5.15.0
|
||||
|
||||
@@ -3,16 +3,25 @@
|
||||
联系记录 API(需要登录):
|
||||
- GET /api/contacts -> 查询联系记录(支持分页、搜索)
|
||||
- GET /api/contacts/query -> 按电话号码或微信号查询(数据展示)
|
||||
- GET /api/contacts/export -> 导出联系记录为 Excel 文件(返回下载链接)
|
||||
- POST /api/contacts -> 创建联系记录
|
||||
- GET /api/contacts/{id} -> 查询单条记录
|
||||
- PUT /api/contacts/{id} -> 更新记录
|
||||
- DELETE /api/contacts/{id} -> 删除记录
|
||||
"""
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill
|
||||
|
||||
from server.core.response import api_success, api_error
|
||||
from server.models import ContactRecord
|
||||
from server.serializers import ContactRecordSerializer
|
||||
@@ -26,6 +35,8 @@ def contact_list(request):
|
||||
page_size = int(request.query_params.get("page_size", 10))
|
||||
reply_status = request.query_params.get("reply_status")
|
||||
wechat = request.query_params.get("wechat_exchanged")
|
||||
start_date = request.query_params.get("start_date")
|
||||
end_date = request.query_params.get("end_date")
|
||||
|
||||
qs = ContactRecord.objects.all()
|
||||
if search:
|
||||
@@ -38,6 +49,10 @@ def contact_list(request):
|
||||
qs = qs.filter(reply_status=reply_status)
|
||||
if wechat is not None:
|
||||
qs = qs.filter(wechat_exchanged=wechat.lower() in ("true", "1"))
|
||||
if start_date:
|
||||
qs = qs.filter(created_at__gte=start_date)
|
||||
if end_date:
|
||||
qs = qs.filter(created_at__lte=f"{end_date} 23:59:59")
|
||||
|
||||
total = qs.count()
|
||||
start = (page - 1) * page_size
|
||||
@@ -84,6 +99,105 @@ def contact_query(request):
|
||||
})
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def contact_export(request):
|
||||
"""
|
||||
导出联系记录为 Excel 文件。
|
||||
支持与列表接口相同的筛选参数:search, reply_status, wechat_exchanged, start_date, end_date
|
||||
返回下载链接而不是直接返回文件流
|
||||
"""
|
||||
search = request.query_params.get("search", "")
|
||||
reply_status = request.query_params.get("reply_status")
|
||||
wechat = request.query_params.get("wechat_exchanged")
|
||||
start_date = request.query_params.get("start_date")
|
||||
end_date = request.query_params.get("end_date")
|
||||
|
||||
# 构建查询
|
||||
qs = ContactRecord.objects.all()
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=search) |
|
||||
Q(position__icontains=search) |
|
||||
Q(contact__icontains=search)
|
||||
)
|
||||
if reply_status:
|
||||
qs = qs.filter(reply_status=reply_status)
|
||||
if wechat is not None:
|
||||
qs = qs.filter(wechat_exchanged=wechat.lower() in ("true", "1"))
|
||||
if start_date:
|
||||
qs = qs.filter(created_at__gte=start_date)
|
||||
if end_date:
|
||||
qs = qs.filter(created_at__lte=f"{end_date} 23:59:59")
|
||||
|
||||
# 创建 Excel 工作簿
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "联系记录"
|
||||
|
||||
# 设置表头样式
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF", size=11)
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
# 定义表头
|
||||
headers = ["ID", "姓名", "岗位", "联系方式", "回复状态", "是否交换微信", "Worker ID", "备注", "联系时间", "创建时间"]
|
||||
ws.append(headers)
|
||||
|
||||
# 应用表头样式
|
||||
for cell in ws[1]:
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = header_alignment
|
||||
|
||||
# 写入数据
|
||||
for record in qs.order_by("-created_at"):
|
||||
ws.append([
|
||||
record.id,
|
||||
record.name,
|
||||
record.position,
|
||||
record.contact,
|
||||
record.reply_status,
|
||||
"是" if record.wechat_exchanged else "否",
|
||||
record.worker_id,
|
||||
record.notes,
|
||||
record.contacted_at.strftime("%Y-%m-%d %H:%M:%S") if record.contacted_at else "",
|
||||
record.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
])
|
||||
|
||||
# 调整列宽
|
||||
column_widths = [8, 12, 15, 18, 12, 15, 15, 30, 20, 20]
|
||||
for i, width in enumerate(column_widths, start=1):
|
||||
ws.column_dimensions[chr(64 + i)].width = width
|
||||
|
||||
# 设置数据行样式
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
|
||||
for cell in row:
|
||||
cell.alignment = Alignment(vertical="center")
|
||||
|
||||
# 确保导出目录存在
|
||||
export_dir = settings.EXPORT_ROOT
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
|
||||
# 生成唯一文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
filename = f"contacts_export_{timestamp}_{unique_id}.xlsx"
|
||||
filepath = os.path.join(export_dir, filename)
|
||||
|
||||
# 保存文件
|
||||
wb.save(filepath)
|
||||
|
||||
# 生成下载链接
|
||||
download_url = f"{settings.MEDIA_URL}exports/{filename}"
|
||||
|
||||
return api_success({
|
||||
"download_url": download_url,
|
||||
"filename": filename,
|
||||
"total_records": qs.count(),
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
|
||||
|
||||
@api_view(["GET", "PUT", "DELETE"])
|
||||
def contact_detail(request, pk):
|
||||
try:
|
||||
|
||||
@@ -83,3 +83,13 @@ LANGUAGE_CODE = "zh-hans"
|
||||
TIME_ZONE = "Asia/Shanghai"
|
||||
USE_I18N = True
|
||||
USE_TZ = False
|
||||
|
||||
# ─── 静态文件和媒体文件 ───
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
||||
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
|
||||
# 导出文件目录
|
||||
EXPORT_ROOT = os.path.join(MEDIA_ROOT, "exports")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Django URL 路由配置。
|
||||
"""
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
from django.conf import settings
|
||||
from django.views.static import serve
|
||||
|
||||
from server.api import auth, accounts, tasks, workers, filters, scripts, contacts, stats, settings
|
||||
from server.api import auth, accounts, tasks, workers, filters, scripts, contacts, stats, settings as api_settings
|
||||
|
||||
urlpatterns = [
|
||||
# ─── 健康检查 ───
|
||||
@@ -42,6 +43,7 @@ urlpatterns = [
|
||||
# ─── 联系记录 ───
|
||||
path("api/contacts", contacts.contact_list),
|
||||
path("api/contacts/query", contacts.contact_query),
|
||||
path("api/contacts/export", contacts.contact_export),
|
||||
path("api/contacts/<int:pk>", contacts.contact_detail),
|
||||
|
||||
# ─── 统计分析 ───
|
||||
@@ -49,6 +51,9 @@ urlpatterns = [
|
||||
path("api/stats/daily", stats.stats_daily),
|
||||
|
||||
# ─── 系统配置 ───
|
||||
path("api/settings", settings.settings_list),
|
||||
path("api/settings/<str:key>", settings.settings_detail),
|
||||
path("api/settings", api_settings.settings_list),
|
||||
path("api/settings/<str:key>", api_settings.settings_detail),
|
||||
|
||||
# ─── 媒体文件服务 ───
|
||||
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user