# -*- coding: utf-8 -*- """ 数据模型:Django ORM 表模型 + Pydantic 内存模型。 """ from __future__ import annotations import time import uuid from typing import Any, Dict, List, Optional from django.db import models from pydantic import BaseModel, Field from common.protocol import TaskStatus, TaskType # ══════════════════════════════════════════════════════════════ # Django ORM 模型 # ══════════════════════════════════════════════════════════════ class BossAccount(models.Model): """BOSS 账号登录状态表。""" worker_id = models.CharField(max_length=64, verbose_name="Worker 标识") browser_id = models.CharField(max_length=128, default="", verbose_name="比特浏览器窗口 ID") browser_name = models.CharField(max_length=128, default="", verbose_name="比特浏览器窗口名称(环境名)") boss_username = models.CharField(max_length=128, default="", verbose_name="BOSS 直聘用户名") boss_id = models.CharField(max_length=64, default="", blank=True, verbose_name="BOSS 直聘用户 ID") is_logged_in = models.BooleanField(default=False, verbose_name="是否已登录") current_task_id = models.CharField(max_length=32, null=True, blank=True, verbose_name="当前检测任务 ID") current_task_status = models.CharField(max_length=32, null=True, blank=True, verbose_name="当前检测任务状态") checked_at = models.DateTimeField(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 = "boss_account" unique_together = [("worker_id", "browser_id")] verbose_name = "BOSS 账号" verbose_name_plural = verbose_name def __str__(self): return f"{self.browser_name}@{self.worker_id}" class TaskLog(models.Model): """任务执行记录表。""" 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(max_length=64, default="", verbose_name="执行的 Worker") status = models.CharField(max_length=32, default="", verbose_name="最终状态") params = models.JSONField(null=True, blank=True, verbose_name="任务参数") result = models.JSONField(null=True, blank=True, verbose_name="任务结果") error = models.TextField(null=True, blank=True, verbose_name="错误信息") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: db_table = "task_log" verbose_name = "任务日志" verbose_name_plural = verbose_name def __str__(self): return f"{self.task_id} ({self.task_type})" class Task(models.Model): """ 任务表(数据库为唯一真相)。 - 所有任务的生命周期状态均保存在此表中; - 内存中不再长期保存任务状态,只作为必要的临时变量。 """ 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(max_length=64, default="", verbose_name="执行的 Worker") account_name = models.CharField(max_length=128, default="", blank=True, verbose_name="账号(环境名称)") status = models.CharField(max_length=32, default="", verbose_name="当前状态") params = models.JSONField(null=True, blank=True, verbose_name="任务参数") progress = models.TextField(null=True, blank=True, verbose_name="进度信息") result = models.JSONField(null=True, blank=True, verbose_name="任务结果") error = models.TextField(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 = "task" verbose_name = "任务" verbose_name_plural = verbose_name def __str__(self): return f"{self.task_id} ({self.task_type})" class AuthToken(models.Model): """登录 token 表:每个用户名仅保留当前有效 token。""" username = models.CharField(max_length=64, unique=True, verbose_name="用户名") token = models.CharField(max_length=64, verbose_name="当前有效 token") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: db_table = "auth_token" verbose_name = "登录 Token" verbose_name_plural = verbose_name def __str__(self): 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.""" worker_id = models.CharField(max_length=64, default="", db_index=True, verbose_name="Worker ID") account_name = models.CharField(max_length=128, default="", db_index=True, verbose_name="环境名称") browser_id = models.CharField(max_length=128, default="", blank=True, verbose_name="浏览器 ID") groups = models.JSONField(default=list, blank=True, verbose_name="筛选分组") flat_options = models.JSONField(default=list, blank=True, verbose_name="扁平筛选项") raw_payload = models.JSONField(default=dict, blank=True, verbose_name="原始筛选数据") synced_at = models.DateTimeField(auto_now=True, verbose_name="同步时间") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: db_table = "recruit_filter_snapshot" unique_together = [("worker_id", "account_name")] verbose_name = "招聘筛选快照" verbose_name_plural = verbose_name def __str__(self): return f"{self.account_name}@{self.worker_id}" class ChatScript(models.Model): """复聊话术表。""" SCRIPT_TYPE_CHOICES = [ ("first", "首次回复"), ("followup", "跟进回复"), ("wechat", "微信交换"), ("closing", "结束语"), ] position = models.CharField(max_length=64, verbose_name="岗位类型") script_type = models.CharField(max_length=32, choices=SCRIPT_TYPE_CHOICES, verbose_name="话术类型") content = models.TextField(verbose_name="话术内容") keywords = models.CharField(max_length=256, default="", blank=True, 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 = "chat_script" verbose_name = "复聊话术" verbose_name_plural = verbose_name def __str__(self): return f"{self.position} - {self.get_script_type_display()}" class ContactRecord(models.Model): """联系人记录表(招聘过程中联系过的候选人)。""" name = models.CharField(max_length=64, verbose_name="姓名") position = models.CharField(max_length=64, default="", verbose_name="岗位") contact = models.CharField(max_length=64, default="", verbose_name="联系方式") reply_status = models.CharField(max_length=32, default="未回复", verbose_name="回复状态") wechat_exchanged = models.BooleanField(default=False, verbose_name="是否交换微信") account_id = models.IntegerField(null=True, blank=True, verbose_name="关联账号 ID") worker_id = models.CharField(max_length=64, default="", verbose_name="Worker 标识") notes = models.TextField(default="", blank=True, verbose_name="备注") contacted_at = models.DateTimeField(null=True, blank=True, verbose_name="联系时间") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: db_table = "contact_record" verbose_name = "联系记录" verbose_name_plural = verbose_name ordering = ["-created_at"] def __str__(self): return f"{self.name} ({self.position})" class SystemConfig(models.Model): """系统配置键值表。""" key = models.CharField(max_length=64, unique=True, verbose_name="配置项") value = models.TextField(default="", verbose_name="配置值") description = models.CharField(max_length=256, default="", blank=True, verbose_name="描述") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") class Meta: db_table = "system_config" verbose_name = "系统配置" verbose_name_plural = verbose_name def __str__(self): 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 运行时状态与任务调度) # ══════════════════════════════════════════════════════════════ # ─── Worker ─── class BrowserProfile(BaseModel): """比特浏览器窗口信息(Worker 上报)。""" id: str name: str = "" remark: str = "" class WorkerInfo(BaseModel): """一台 Worker 的运行时信息(内存中保存)。""" worker_id: str worker_name: str = "" browsers: List[BrowserProfile] = [] online: bool = True last_heartbeat: float = Field(default_factory=time.time) connected_at: float = Field(default_factory=time.time) current_task_id: Optional[str] = None # ─── Task ─── class TaskCreate(BaseModel): """前端提交任务的请求体(也用于内部创建任务)。""" task_type: TaskType worker_id: Optional[str] = None account_name: Optional[str] = None params: Dict[str, Any] = {} class TaskInfo(BaseModel): """任务完整信息(内存中保存)。""" task_id: str = Field(default_factory=lambda: uuid.uuid4().hex[:12]) task_type: TaskType status: TaskStatus = TaskStatus.PENDING worker_id: Optional[str] = None account_name: Optional[str] = None params: Dict[str, Any] = {} progress: Optional[str] = None result: Any = None error: Optional[str] = None created_at: float = Field(default_factory=time.time) updated_at: float = Field(default_factory=time.time)