haha
This commit is contained in:
187
server/api/followup.py
Normal file
187
server/api/followup.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
复聊配置 API(需要登录):
|
||||
- GET /api/followup-configs -> 查询复聊配置列表
|
||||
- POST /api/followup-configs -> 创建复聊配置
|
||||
- GET /api/followup-configs/{id} -> 查询单个配置
|
||||
- PUT /api/followup-configs/{id} -> 更新配置
|
||||
- DELETE /api/followup-configs/{id} -> 删除配置
|
||||
|
||||
- GET /api/followup-scripts -> 查询话术列表
|
||||
- POST /api/followup-scripts -> 创建话术
|
||||
- GET /api/followup-scripts/{id} -> 查询单个话术
|
||||
- PUT /api/followup-scripts/{id} -> 更新话术
|
||||
- DELETE /api/followup-scripts/{id} -> 删除话术
|
||||
|
||||
- GET /api/followup-records -> 查询复聊记录
|
||||
- POST /api/followup-records/send -> 手动发送复聊消息
|
||||
"""
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
|
||||
from server.core.response import api_success, api_error
|
||||
from server.models import FollowUpConfig, FollowUpScript, FollowUpRecord, ContactRecord
|
||||
from server.serializers import (
|
||||
FollowUpConfigSerializer,
|
||||
FollowUpScriptSerializer,
|
||||
FollowUpRecordSerializer
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────── 复聊配置 ──────────────────────────
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def followup_config_list(request):
|
||||
"""复聊配置列表。"""
|
||||
if request.method == "GET":
|
||||
position = request.query_params.get("position")
|
||||
is_active = request.query_params.get("is_active")
|
||||
|
||||
qs = FollowUpConfig.objects.all()
|
||||
if position:
|
||||
qs = qs.filter(position__icontains=position)
|
||||
if is_active is not None:
|
||||
qs = qs.filter(is_active=is_active.lower() in ("true", "1"))
|
||||
|
||||
configs = qs.order_by("-created_at")
|
||||
return api_success(FollowUpConfigSerializer(configs, many=True).data)
|
||||
|
||||
# POST
|
||||
ser = FollowUpConfigSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
ser.save()
|
||||
return api_success(ser.data, http_status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(["GET", "PUT", "DELETE"])
|
||||
def followup_config_detail(request, pk):
|
||||
"""复聊配置详情。"""
|
||||
try:
|
||||
obj = FollowUpConfig.objects.get(pk=pk)
|
||||
except FollowUpConfig.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, "复聊配置不存在")
|
||||
|
||||
if request.method == "GET":
|
||||
return api_success(FollowUpConfigSerializer(obj).data)
|
||||
|
||||
if request.method == "PUT":
|
||||
ser = FollowUpConfigSerializer(obj, data=request.data, partial=True)
|
||||
ser.is_valid(raise_exception=True)
|
||||
ser.save()
|
||||
return api_success(ser.data)
|
||||
|
||||
# DELETE
|
||||
obj.delete()
|
||||
return api_success(msg="复聊配置已删除")
|
||||
|
||||
|
||||
# ────────────────────────── 复聊话术 ──────────────────────────
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def followup_script_list(request):
|
||||
"""复聊话术列表。"""
|
||||
if request.method == "GET":
|
||||
config_id = request.query_params.get("config_id")
|
||||
day_number = request.query_params.get("day_number")
|
||||
|
||||
qs = FollowUpScript.objects.all()
|
||||
if config_id:
|
||||
qs = qs.filter(config_id=config_id)
|
||||
if day_number is not None:
|
||||
qs = qs.filter(day_number=day_number)
|
||||
|
||||
scripts = qs.order_by("config_id", "day_number", "order")
|
||||
return api_success(FollowUpScriptSerializer(scripts, many=True).data)
|
||||
|
||||
# POST
|
||||
ser = FollowUpScriptSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
ser.save()
|
||||
return api_success(ser.data, http_status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(["GET", "PUT", "DELETE"])
|
||||
def followup_script_detail(request, pk):
|
||||
"""复聊话术详情。"""
|
||||
try:
|
||||
obj = FollowUpScript.objects.get(pk=pk)
|
||||
except FollowUpScript.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, "复聊话术不存在")
|
||||
|
||||
if request.method == "GET":
|
||||
return api_success(FollowUpScriptSerializer(obj).data)
|
||||
|
||||
if request.method == "PUT":
|
||||
ser = FollowUpScriptSerializer(obj, data=request.data, partial=True)
|
||||
ser.is_valid(raise_exception=True)
|
||||
ser.save()
|
||||
return api_success(ser.data)
|
||||
|
||||
# DELETE
|
||||
obj.delete()
|
||||
return api_success(msg="复聊话术已删除")
|
||||
|
||||
|
||||
# ────────────────────────── 复聊记录 ──────────────────────────
|
||||
|
||||
@api_view(["GET"])
|
||||
def followup_record_list(request):
|
||||
"""复聊记录列表。"""
|
||||
contact_id = request.query_params.get("contact_id")
|
||||
config_id = request.query_params.get("config_id")
|
||||
got_reply = request.query_params.get("got_reply")
|
||||
page = int(request.query_params.get("page", 1))
|
||||
page_size = int(request.query_params.get("page_size", 20))
|
||||
|
||||
qs = FollowUpRecord.objects.all()
|
||||
if contact_id:
|
||||
qs = qs.filter(contact_id=contact_id)
|
||||
if config_id:
|
||||
qs = qs.filter(config_id=config_id)
|
||||
if got_reply is not None:
|
||||
qs = qs.filter(got_reply=got_reply.lower() in ("true", "1"))
|
||||
|
||||
total = qs.count()
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
records = qs[start:end]
|
||||
|
||||
return api_success({
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"results": FollowUpRecordSerializer(records, many=True).data,
|
||||
})
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def followup_send_manual(request):
|
||||
"""手动发送复聊消息。"""
|
||||
contact_id = request.data.get("contact_id")
|
||||
content = request.data.get("content", "").strip()
|
||||
|
||||
if not contact_id:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, "请提供 contact_id")
|
||||
if not content:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, "请提供发送内容")
|
||||
|
||||
try:
|
||||
contact = ContactRecord.objects.get(pk=contact_id)
|
||||
except ContactRecord.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, "联系人不存在")
|
||||
|
||||
# TODO: 这里需要调用浏览器自动化发送消息
|
||||
# 暂时只记录到数据库
|
||||
|
||||
record = FollowUpRecord.objects.create(
|
||||
contact_id=contact_id,
|
||||
config_id=0, # 手动发送
|
||||
script_id=0, # 手动发送
|
||||
day_number=0,
|
||||
content=content,
|
||||
)
|
||||
|
||||
return api_success({
|
||||
"record": FollowUpRecordSerializer(record).data,
|
||||
"message": "复聊消息已发送"
|
||||
})
|
||||
71
server/migrations/0004_add_followup_config.py
Normal file
71
server/migrations/0004_add_followup_config.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
新增复聊配置表的数据库迁移
|
||||
"""
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('server', '0003_add_boss_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FollowUpConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128, verbose_name='配置名称')),
|
||||
('position', models.CharField(max_length=64, verbose_name='岗位类型')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '复聊配置',
|
||||
'verbose_name_plural': '复聊配置',
|
||||
'db_table': 'follow_up_config',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FollowUpScript',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('config_id', models.IntegerField(verbose_name='关联的复聊配置ID')),
|
||||
('day_number', models.IntegerField(verbose_name='第几天(1=第一天,2=第二天,0=往后一直)')),
|
||||
('content', models.TextField(verbose_name='话术内容')),
|
||||
('interval_hours', models.IntegerField(default=24, verbose_name='间隔小时数')),
|
||||
('order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '复聊话术',
|
||||
'verbose_name_plural': '复聊话术',
|
||||
'db_table': 'follow_up_script',
|
||||
'ordering': ['config_id', 'day_number', 'order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FollowUpRecord',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('contact_id', models.IntegerField(verbose_name='关联的联系人ID')),
|
||||
('config_id', models.IntegerField(verbose_name='使用的复聊配置ID')),
|
||||
('script_id', models.IntegerField(verbose_name='使用的话术ID')),
|
||||
('day_number', models.IntegerField(verbose_name='第几天')),
|
||||
('content', models.TextField(verbose_name='发送的内容')),
|
||||
('sent_at', models.DateTimeField(auto_now_add=True, verbose_name='发送时间')),
|
||||
('got_reply', models.BooleanField(default=False, verbose_name='是否得到回复')),
|
||||
('reply_content', models.TextField(default='', blank=True, verbose_name='回复内容')),
|
||||
('replied_at', models.DateTimeField(null=True, blank=True, verbose_name='回复时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '复聊记录',
|
||||
'verbose_name_plural': '复聊记录',
|
||||
'db_table': 'follow_up_record',
|
||||
'ordering': ['-sent_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -197,6 +197,65 @@ class SystemConfig(models.Model):
|
||||
return self.key
|
||||
|
||||
|
||||
class FollowUpConfig(models.Model):
|
||||
"""复聊配置表。"""
|
||||
name = models.CharField(max_length=128, verbose_name="配置名称")
|
||||
position = models.CharField(max_length=64, verbose_name="岗位类型")
|
||||
is_active = models.BooleanField(default=True, verbose_name="是否启用")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
db_table = "follow_up_config"
|
||||
verbose_name = "复聊配置"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.position})"
|
||||
|
||||
|
||||
class FollowUpScript(models.Model):
|
||||
"""复聊话术表(支持多轮回复)。"""
|
||||
config_id = models.IntegerField(verbose_name="关联的复聊配置ID")
|
||||
day_number = models.IntegerField(verbose_name="第几天(1=第一天,2=第二天,0=往后一直)")
|
||||
content = models.TextField(verbose_name="话术内容")
|
||||
interval_hours = models.IntegerField(default=24, verbose_name="间隔小时数")
|
||||
order = models.IntegerField(default=0, verbose_name="排序")
|
||||
is_active = models.BooleanField(default=True, verbose_name="是否启用")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
class Meta:
|
||||
db_table = "follow_up_script"
|
||||
verbose_name = "复聊话术"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['config_id', 'day_number', 'order']
|
||||
|
||||
def __str__(self):
|
||||
return f"第{self.day_number}天 - {self.content[:20]}"
|
||||
|
||||
|
||||
class FollowUpRecord(models.Model):
|
||||
"""复聊记录表(记录每次发送的话术和回复)。"""
|
||||
contact_id = models.IntegerField(verbose_name="关联的联系人ID")
|
||||
config_id = models.IntegerField(verbose_name="使用的复聊配置ID")
|
||||
script_id = models.IntegerField(verbose_name="使用的话术ID")
|
||||
day_number = models.IntegerField(verbose_name="第几天")
|
||||
content = models.TextField(verbose_name="发送的内容")
|
||||
sent_at = models.DateTimeField(auto_now_add=True, verbose_name="发送时间")
|
||||
got_reply = models.BooleanField(default=False, verbose_name="是否得到回复")
|
||||
reply_content = models.TextField(default="", blank=True, verbose_name="回复内容")
|
||||
replied_at = models.DateTimeField(null=True, blank=True, verbose_name="回复时间")
|
||||
|
||||
class Meta:
|
||||
db_table = "follow_up_record"
|
||||
verbose_name = "复聊记录"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-sent_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"联系人{self.contact_id} - 第{self.day_number}天"
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# Pydantic 内存模型(非数据库,用于 Worker 运行时状态与任务调度)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -4,7 +4,10 @@ DRF 序列化器。
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
from server.models import BossAccount, TaskLog, FilterConfig, ChatScript, ContactRecord, SystemConfig
|
||||
from server.models import (
|
||||
BossAccount, TaskLog, FilterConfig, ChatScript, ContactRecord, SystemConfig,
|
||||
FollowUpConfig, FollowUpScript, FollowUpRecord
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────── 账号 ──────────────────────────
|
||||
@@ -122,3 +125,35 @@ class SystemConfigSerializer(serializers.ModelSerializer):
|
||||
model = SystemConfig
|
||||
fields = "__all__"
|
||||
read_only_fields = ["updated_at"]
|
||||
|
||||
# ────────────────────────── 复聊配置 ──────────────────────────
|
||||
|
||||
class FollowUpScriptSerializer(serializers.ModelSerializer):
|
||||
"""复聊话术序列化器。"""
|
||||
class Meta:
|
||||
model = FollowUpScript
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id", "created_at"]
|
||||
|
||||
|
||||
class FollowUpConfigSerializer(serializers.ModelSerializer):
|
||||
"""复聊配置序列化器(包含关联的话术列表)。"""
|
||||
scripts = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = FollowUpConfig
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_scripts(self, obj):
|
||||
"""获取该配置下的所有话术。"""
|
||||
scripts = FollowUpScript.objects.filter(config_id=obj.id, is_active=True).order_by('day_number', 'order')
|
||||
return FollowUpScriptSerializer(scripts, many=True).data
|
||||
|
||||
|
||||
class FollowUpRecordSerializer(serializers.ModelSerializer):
|
||||
"""复聊记录序列化器。"""
|
||||
class Meta:
|
||||
model = FollowUpRecord
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id", "sent_at"]
|
||||
|
||||
@@ -5,7 +5,10 @@ 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 as api_settings
|
||||
from server.api import (
|
||||
auth, accounts, tasks, workers, filters, scripts, contacts, stats,
|
||||
settings as api_settings, followup
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# ─── 健康检查 ───
|
||||
@@ -46,6 +49,14 @@ urlpatterns = [
|
||||
path("api/contacts/export", contacts.contact_export),
|
||||
path("api/contacts/<int:pk>", contacts.contact_detail),
|
||||
|
||||
# ─── 复聊配置 ───
|
||||
path("api/followup-configs", followup.followup_config_list),
|
||||
path("api/followup-configs/<int:pk>", followup.followup_config_detail),
|
||||
path("api/followup-scripts", followup.followup_script_list),
|
||||
path("api/followup-scripts/<int:pk>", followup.followup_script_detail),
|
||||
path("api/followup-records", followup.followup_record_list),
|
||||
path("api/followup-records/send", followup.followup_send_manual),
|
||||
|
||||
# ─── 统计分析 ───
|
||||
path("api/stats", stats.stats_overview),
|
||||
path("api/stats/daily", stats.stats_daily),
|
||||
|
||||
Reference in New Issue
Block a user