This commit is contained in:
ddrwode
2026-03-04 14:11:12 +08:00
parent 6780f184c2
commit 3303a980f6
5 changed files with 453 additions and 5 deletions

318
API_CONTACTS.md Normal file
View 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` 是相对路径
- 前端需要拼接完整的服务器地址(如果跨域)
- 或直接使用相对路径(如果同域)

View File

@@ -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

View File

@@ -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:

View File

@@ -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")

View File

@@ -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}),
]