This commit is contained in:
ddrwode
2026-03-06 10:47:46 +08:00
parent 59dd293f2d
commit 7b351039f8
14 changed files with 385 additions and 427 deletions

View File

@@ -1,52 +1,18 @@
# -*- coding: utf-8 -*-
"""
Filter APIs:
- CRUD for FilterConfig (legacy/manual configs)
- Recruit filter options (synced from site at worker startup)
"""
from rest_framework import status
from rest_framework.decorators import api_view
from server.core.response import api_error, api_success
from server.models import BossAccount, FilterConfig, RecruitFilterSnapshot
from server.models import BossAccount, RecruitFilterSnapshot
from server.serializers import (
FilterConfigSerializer,
RecruitFilterSnapshotSerializer,
)
@api_view(["GET", "POST"])
def filter_list(request):
if request.method == "GET":
qs = FilterConfig.objects.all().order_by("-updated_at")
return api_success(FilterConfigSerializer(qs, many=True).data)
ser = FilterConfigSerializer(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 filter_detail(request, pk):
try:
obj = FilterConfig.objects.get(pk=pk)
except FilterConfig.DoesNotExist:
return api_error(status.HTTP_404_NOT_FOUND, "筛选配置不存在")
if request.method == "GET":
return api_success(FilterConfigSerializer(obj).data)
if request.method == "PUT":
ser = FilterConfigSerializer(obj, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
ser.save()
return api_success(ser.data)
obj.delete()
return api_success(msg="筛选配置已删除")
def _snapshot_payload(snapshot: RecruitFilterSnapshot) -> dict:
data = RecruitFilterSnapshotSerializer(snapshot).data
return {
@@ -102,4 +68,3 @@ def recruit_filter_options(request):
}
)
return api_success(_snapshot_payload(snapshot))

View File

@@ -0,0 +1,16 @@
# Generated by Codex on 2026-03-06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("server", "0007_recruitfiltersnapshot"),
]
operations = [
migrations.DeleteModel(
name="FilterConfig",
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 6.0.2 on 2026-03-06 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('server', '0008_delete_filterconfig'),
]
operations = [
migrations.CreateModel(
name='Task',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task_id', models.CharField(max_length=32, unique=True, verbose_name='任务 ID')),
('task_type', models.CharField(max_length=64, verbose_name='任务类型')),
('worker_id', models.CharField(default='', max_length=64, verbose_name='执行的 Worker')),
('account_name', models.CharField(blank=True, default='', max_length=128, verbose_name='账号(环境名称)')),
('status', models.CharField(default='', max_length=32, verbose_name='当前状态')),
('params', models.JSONField(blank=True, null=True, verbose_name='任务参数')),
('progress', models.TextField(blank=True, null=True, verbose_name='进度信息')),
('result', models.JSONField(blank=True, null=True, verbose_name='任务结果')),
('error', models.TextField(blank=True, null=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': 'task',
},
),
]

View File

@@ -105,42 +105,6 @@ class AuthToken(models.Model):
return self.username
class FilterConfig(models.Model):
"""筛选条件配置表。"""
name = models.CharField(max_length=128, verbose_name="配置名称")
position_keywords = models.CharField(max_length=512, default="", blank=True, verbose_name="岗位关键词列表")
city = models.CharField(max_length=64, default="", blank=True, verbose_name="城市")
salary_min = models.CharField(max_length=32, default="", blank=True, verbose_name="最低薪资(K)")
salary_max = models.CharField(max_length=32, default="", blank=True, verbose_name="最高薪资(K)")
experience = models.CharField(max_length=64, default="", blank=True, verbose_name="工作经验")
education = models.CharField(max_length=32, default="不限", verbose_name="学历要求")
is_active = models.BooleanField(default=True, verbose_name="是否启用")
# 以下为兼容旧版保留字段
age_min = models.IntegerField(default=18, verbose_name="最小年龄")
age_max = models.IntegerField(default=60, verbose_name="最大年龄")
gender = models.CharField(max_length=32, default="不限", verbose_name="性别")
activity = models.CharField(max_length=32, default="不限", verbose_name="活跃度")
positions = models.JSONField(default=list, blank=True, verbose_name="期望岗位列表")
greeting_min = models.IntegerField(default=5, verbose_name="打招呼最少条数/天")
greeting_max = models.IntegerField(default=20, verbose_name="打招呼最多条数/天")
rest_minutes = models.IntegerField(default=30, verbose_name="每轮休息分钟")
collection_min = models.IntegerField(default=10, verbose_name="收藏最少个数/天")
collection_max = models.IntegerField(default=50, verbose_name="收藏最多个数/天")
message_interval = models.IntegerField(default=30, verbose_name="打招呼间隔秒")
min_amount = models.IntegerField(null=True, blank=True, verbose_name="最小金额")
max_amount = models.IntegerField(null=True, blank=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 = "filter_config"
verbose_name = "筛选配置"
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class RecruitFilterSnapshot(models.Model):
"""Per-account site filter snapshot fetched from zhipin recommend page."""

View File

@@ -5,7 +5,7 @@ DRF 序列化器。
from rest_framework import serializers
from server.models import (
BossAccount, TaskLog, FilterConfig, RecruitFilterSnapshot, ChatScript, ContactRecord, SystemConfig,
BossAccount, TaskLog, RecruitFilterSnapshot, ChatScript, ContactRecord, SystemConfig,
FollowUpConfig, FollowUpScript, FollowUpRecord
)
@@ -89,49 +89,6 @@ class LoginSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128)
# ────────────────────────── 筛选配置 ──────────────────────────
# 请求中可能出现的“金额”字段别名,统一映射为 min_amount / max_amount保证请求与响应字段名一致
FILTER_AMOUNT_ALIASES = {
"minAmount": "min_amount",
"maxAmount": "max_amount",
"最小金额": "min_amount",
"最大金额": "max_amount",
}
class FilterConfigSerializer(serializers.ModelSerializer):
"""筛选配置:列表/详情返回 name, position_keywords, city, salary_min, salary_max, experience, education, is_active 等。"""
class Meta:
model = FilterConfig
fields = "__all__"
read_only_fields = ["id", "created_at", "updated_at"]
def to_internal_value(self, data):
"""请求中兼容 minAmount/maxAmount 等别名;支持 multipart/form-dataQueryDict/多值列表)。"""
if data is None:
data = {}
else:
data = dict(data)
# multipart 解析后字段值可能是 list如 ["测试配置"]),需展平为标量
for key in list(data.keys()):
val = data[key]
if isinstance(val, (list, tuple)):
data[key] = val[0] if len(val) > 0 else ""
for alias, canonical in FILTER_AMOUNT_ALIASES.items():
if alias in data and canonical not in data:
data[canonical] = data.pop(alias)
# 请求里 is_active 可能是字符串 "true"/"false" 或单元素列表
raw = data.get("is_active")
if raw is not None:
if isinstance(raw, (list, tuple)):
raw = raw[0] if raw else ""
if isinstance(raw, str):
data["is_active"] = raw.lower() in ("true", "1", "yes", "")
return super().to_internal_value(data)
# ────────────────────────── 话术 ──────────────────────────
class RecruitFilterSnapshotSerializer(serializers.ModelSerializer):

View File

@@ -35,10 +35,8 @@ urlpatterns = [
path("api/accounts/worker/<str:worker_id>", accounts.account_list_by_worker),
path("api/accounts/<int:account_id>", accounts.account_detail),
# ─── 筛选配置 ───
path("api/filters", filters.filter_list),
# ─── 招聘筛选快照 ───
path("api/filters/options", filters.recruit_filter_options),
path("api/filters/<int:pk>", filters.filter_detail),
# ─── 话术管理 ───
path("api/scripts", scripts.script_list),