first commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# 新增model需要在这里导入
|
||||
from .admin import *
|
||||
from .automation import *
|
||||
from .weixin import *
|
||||
from .msg import *
|
||||
@@ -0,0 +1,297 @@
|
||||
from tortoise import fields
|
||||
from tortoise import fields as Fields
|
||||
|
||||
from app.schemas.menus import MenuType
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import MethodType
|
||||
|
||||
|
||||
class User(BaseModel, TimestampMixin):
|
||||
username = fields.CharField(max_length=20, unique=True, description="用户名称", index=True)
|
||||
alias = fields.CharField(max_length=30, null=True, description="姓名", index=True)
|
||||
email = fields.CharField(max_length=255, unique=True, description="邮箱", index=True)
|
||||
phone = fields.CharField(max_length=20, null=True, description="电话", index=True)
|
||||
password = fields.CharField(max_length=128, null=True, description="密码")
|
||||
is_active = fields.BooleanField(default=True, description="是否激活", index=True)
|
||||
is_superuser = fields.BooleanField(default=False, description="是否为超级管理员", index=True)
|
||||
last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True)
|
||||
roles = fields.ManyToManyField("models.Role", related_name="user_roles")
|
||||
dept_id = fields.IntField(null=True, description="部门ID", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "user"
|
||||
|
||||
|
||||
class Role(BaseModel, TimestampMixin):
|
||||
name = fields.CharField(max_length=20, unique=True, description="角色名称", index=True)
|
||||
desc = fields.CharField(max_length=500, null=True, description="角色描述")
|
||||
menus = fields.ManyToManyField("models.Menu", related_name="role_menus")
|
||||
apis = fields.ManyToManyField("models.Api", related_name="role_apis")
|
||||
|
||||
class Meta:
|
||||
table = "role"
|
||||
|
||||
|
||||
class Api(BaseModel, TimestampMixin):
|
||||
path = fields.CharField(max_length=100, description="API路径", index=True)
|
||||
method = fields.CharEnumField(MethodType, description="请求方法", index=True)
|
||||
summary = fields.CharField(max_length=500, description="请求简介", index=True)
|
||||
tags = fields.CharField(max_length=100, description="API标签", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "api"
|
||||
|
||||
|
||||
class Menu(BaseModel, TimestampMixin):
|
||||
name = fields.CharField(max_length=20, description="菜单名称", index=True)
|
||||
remark = fields.JSONField(null=True, description="保留字段")
|
||||
menu_type = fields.CharEnumField(MenuType, null=True, description="菜单类型")
|
||||
icon = fields.CharField(max_length=100, null=True, description="菜单图标")
|
||||
path = fields.CharField(max_length=100, description="菜单路径", index=True)
|
||||
order = fields.IntField(default=0, description="排序", index=True)
|
||||
parent_id = fields.IntField(default=0, description="父菜单ID", index=True)
|
||||
is_hidden = fields.BooleanField(default=False, description="是否隐藏")
|
||||
component = fields.CharField(max_length=100, description="组件")
|
||||
keepalive = fields.BooleanField(default=True, description="存活")
|
||||
redirect = fields.CharField(max_length=100, null=True, description="重定向")
|
||||
|
||||
class Meta:
|
||||
table = "menu"
|
||||
|
||||
|
||||
class Dept(BaseModel, TimestampMixin):
|
||||
name = fields.CharField(max_length=20, unique=True, description="部门名称", index=True)
|
||||
desc = fields.CharField(max_length=500, null=True, description="备注")
|
||||
is_deleted = fields.BooleanField(default=False, description="软删除标记", index=True)
|
||||
order = fields.IntField(default=0, description="排序", index=True)
|
||||
parent_id = fields.IntField(default=0, max_length=10, description="父部门ID", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "dept"
|
||||
|
||||
|
||||
class DeptClosure(BaseModel, TimestampMixin):
|
||||
ancestor = fields.IntField(description="父代", index=True)
|
||||
descendant = fields.IntField(description="子代", index=True)
|
||||
level = fields.IntField(default=0, description="深度", index=True)
|
||||
|
||||
|
||||
class AuditLog(BaseModel, TimestampMixin):
|
||||
user_id = fields.IntField(description="用户ID", index=True)
|
||||
username = fields.CharField(max_length=64, default="", description="用户名称", index=True)
|
||||
module = fields.CharField(max_length=64, default="", description="功能模块", index=True)
|
||||
summary = fields.CharField(max_length=128, default="", description="请求描述", index=True)
|
||||
method = fields.CharField(max_length=10, default="", description="请求方法", index=True)
|
||||
path = fields.CharField(max_length=255, default="", description="请求路径", index=True)
|
||||
status = fields.IntField(default=-1, description="状态码", index=True)
|
||||
response_time = fields.IntField(default=0, description="响应时间(单位ms)", index=True)
|
||||
request_args = fields.JSONField(null=True, description="请求参数")
|
||||
response_body = fields.JSONField(null=True, description="返回数据")
|
||||
|
||||
|
||||
class Codegen(BaseModel, TimestampMixin):
|
||||
"""
|
||||
{
|
||||
"name": "user",
|
||||
"author": "Ly997",
|
||||
"description": "系统用户管理",
|
||||
"version": "1.0.0",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "Long",
|
||||
"primary_key": true,
|
||||
"description": "用户ID",
|
||||
"required": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"type": "String",
|
||||
"length": 50,
|
||||
"description": "用户名",
|
||||
"required": true,
|
||||
"unique": true,
|
||||
"validations": [
|
||||
{
|
||||
"type": "min_length",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"type": "max_length",
|
||||
"value": 20
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "String",
|
||||
"description": "电子邮箱",
|
||||
"required": true,
|
||||
"validations": [
|
||||
{
|
||||
"type": "email"
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "password_hash",
|
||||
"type": "String",
|
||||
"description": "密码哈希",
|
||||
"required": true,
|
||||
"secret": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "is_active",
|
||||
"type": "Boolean",
|
||||
"description": "是否激活",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"type": "DateTime",
|
||||
"description": "创建时间",
|
||||
"auto_now_add": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"type": "DateTime",
|
||||
"description": "更新时间",
|
||||
"auto_now": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"name": "roles",
|
||||
"type": "many-to-many",
|
||||
"target": "role",
|
||||
"through": "user_roles",
|
||||
"description": "用户角色关联"
|
||||
},
|
||||
{
|
||||
"name": "posts",
|
||||
"type": "one-to-many",
|
||||
"target": "post",
|
||||
"description": "用户发表的文章"
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"operations": [
|
||||
"create",
|
||||
"read",
|
||||
"update",
|
||||
"delete",
|
||||
"list"
|
||||
],
|
||||
"base_path": "/users",
|
||||
"auth_required": true,
|
||||
"permissions": {
|
||||
"create": [
|
||||
"admin",
|
||||
"manager"
|
||||
],
|
||||
"delete": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"label": "用户管理",
|
||||
"icon": "user",
|
||||
"order": 10,
|
||||
"submenu": [
|
||||
{
|
||||
"label": "用户列表",
|
||||
"path": "/users",
|
||||
"icon": "list"
|
||||
},
|
||||
{
|
||||
"label": "角色管理",
|
||||
"path": "/roles"
|
||||
}
|
||||
]
|
||||
}
|
||||
} """
|
||||
name = Fields.CharField(max_length=64, description="名称", index=True)
|
||||
author = Fields.CharField(max_length=64, description="作者", index=True)
|
||||
description = Fields.CharField(max_length=500, description="描述")
|
||||
version = Fields.CharField(max_length=64, description="版本", index=True)
|
||||
fields = Fields.JSONField(description="字段")
|
||||
relations = Fields.JSONField(description="关系")
|
||||
api = Fields.JSONField(description="API", null=True)
|
||||
menu = Fields.JSONField(description="菜单", null=True)
|
||||
|
||||
class Meta:
|
||||
table = "codegen"
|
||||
|
||||
async def dumps(self, output_type='pydantic'):
|
||||
from app.schemas.codegen import BaseCodegen
|
||||
|
||||
obj_dict = await self.to_dict()
|
||||
# print('type(obj_dict["fields"])', type(obj_dict["fields"]))
|
||||
# print('type(obj_dict["relations"])', type(obj_dict["relations"]))
|
||||
# print('type(obj_dict["api"])', type(obj_dict["api"]))
|
||||
# obj_dict['fields'] = json.loads(obj_dict['fields'])
|
||||
# obj_dict['relations'] = json.loads(obj_dict['relations'])
|
||||
# obj_dict['api'] = json.loads(obj_dict['api'])
|
||||
# obj_dict['menu'] = json.loads(obj_dict['menu'])
|
||||
|
||||
if output_type == 'pydantic': return BaseCodegen(**obj_dict)
|
||||
else : return obj_dict
|
||||
|
||||
|
||||
class Datasource(BaseModel, TimestampMixin):
|
||||
tableName = Fields.CharField(max_length=32, description="名称", index=True)
|
||||
tableComment = Fields.CharField(max_length=64, description="注释", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "datasource"
|
||||
@@ -0,0 +1,119 @@
|
||||
from tortoise import fields
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import TaskStatus, TaskType, ActionType, TaskStatus, ScenarioScope
|
||||
|
||||
|
||||
class Scenario(BaseModel, TimestampMixin):
|
||||
title = fields.CharField(max_length=255, description="场景标题", index=True)
|
||||
trigger = fields.JSONField(description="触发条件(事件/时间)")
|
||||
actions = fields.JSONField(default=list, description="执行动作模板列表")
|
||||
visible = fields.BooleanField(default=True, description="是否可见", index=True)
|
||||
notes = fields.TextField(null=True, description="备注")
|
||||
is_global = fields.BooleanField(default=True, description="是否全局场景", index=True)
|
||||
owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID(非全局时有效)", index=True)
|
||||
enabled = fields.BooleanField(default=True, description="是否启用", index=True)
|
||||
scope = fields.CharEnumField(ScenarioScope, default=ScenarioScope.ALL, description="作用角色范围", index=True)
|
||||
due_days = fields.IntField(null=True, description="截止时间(天)")
|
||||
|
||||
class Meta:
|
||||
table = "automation_scenario"
|
||||
indexes = [
|
||||
["is_global", "enabled"],
|
||||
["owner_user_id", "enabled"],
|
||||
]
|
||||
|
||||
"""
|
||||
CREATE TABLE automation_scenario_trigger_index (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
scenario_id BIGINT NOT NULL,
|
||||
event_name VARCHAR(64) NOT NULL COMMENT '监听的事件名,如 group_chat_updated',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_global TINYINT(1) NOT NULL DEFAULT 1,
|
||||
owner_user_id VARCHAR(64) DEFAULT NULL,
|
||||
scope VARCHAR(8) NOT NULL DEFAULT 'all',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_event_enabled (event_name, enabled),
|
||||
KEY idx_scenario_id (scenario_id),
|
||||
CONSTRAINT fk_scenario_def FOREIGN KEY (scenario_id) REFERENCES automation_scenario(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
"""
|
||||
class ScenarioTriggerIndex(BaseModel, TimestampMixin):
|
||||
scenario_id = fields.BigIntField(description="场景ID")
|
||||
type = fields.CharEnumField(TaskType, default=TaskType.EVENT, description="事件类型", index=True)
|
||||
event_name = fields.CharField(max_length=64, description="监听的事件名,如 group_chat_updated", index=True)
|
||||
enabled = fields.BooleanField(default=True, description="是否启用", index=True)
|
||||
is_global = fields.BooleanField(default=True, description="是否全局场景", index=True)
|
||||
owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID(非全局时有效)", index=True)
|
||||
scope = fields.CharEnumField(ScenarioScope, default=ScenarioScope.ALL, description="作用角色范围")
|
||||
|
||||
class Meta:
|
||||
table = "automation_scenario_trigger_index"
|
||||
|
||||
|
||||
class Task(BaseModel, TimestampMixin):
|
||||
title = fields.CharField(max_length=255, description="待办标题", index=True)
|
||||
event_data = fields.JSONField(default=dict, description="事件数据")
|
||||
notes = fields.TextField(null=True, description="备注")
|
||||
reason = fields.TextField(null=True, description="触发原因")
|
||||
|
||||
completed_at = fields.DatetimeField(null=True, description="完成时间")
|
||||
|
||||
# 关联字段
|
||||
source_scenario = fields.ForeignKeyField(
|
||||
"models.Scenario",
|
||||
related_name="generated_tasks",
|
||||
null=True,
|
||||
on_delete=fields.SET_NULL,
|
||||
description="来源场景"
|
||||
)
|
||||
|
||||
related_order_id = fields.CharField(max_length=64, null=True, description="关联订单ID", index=True)
|
||||
owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID", index=True)
|
||||
|
||||
# 分配与状态
|
||||
assignee_user_id = fields.CharField(max_length=64, null=True, description="指派人用户ID", index=True)
|
||||
assignee_username = fields.CharField(max_length=64, null=True, description="指派人用户名", index=True)
|
||||
status = fields.CharEnumField(TaskStatus, default=TaskStatus.PENDING, description="任务状态", index=True)
|
||||
|
||||
# 自动化控制
|
||||
due_at = fields.DatetimeField(null=True, description="截止时间")
|
||||
auto_closeable = fields.BooleanField(default=True, description="是否允许自动消除")
|
||||
closed_by = fields.CharField(max_length=32, null=True, description="关闭方式: manual/auto/expired", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "automation_task"
|
||||
indexes = [
|
||||
["assignee_user_id", "status"],
|
||||
]
|
||||
|
||||
|
||||
class Action(BaseModel, TimestampMixin):
|
||||
type = fields.CharEnumField(ActionType, null=True, description="操作类型", index=True)
|
||||
detail = fields.JSONField(default={}, description="操作详情")
|
||||
done = fields.BooleanField(default=False, description="是否已完成", index=True)
|
||||
done_at = fields.DatetimeField(null=True, description="完成时间")
|
||||
result = fields.JSONField(default={}, description="操作结果")
|
||||
userid = fields.CharField(max_length=64, null=True, description="操作用户ID", index=True)
|
||||
username = fields.CharField(max_length=64, null=True, description="操作用户名称", index=True)
|
||||
notes = fields.TextField(null=True, description="操作备注")
|
||||
|
||||
# 硬外键:必须属于一个 Task
|
||||
task = fields.ForeignKeyField(
|
||||
"models.Task",
|
||||
related_name="actions",
|
||||
null=True, # ← 允许为空
|
||||
on_delete=fields.SET_NULL, # ← 注意:CASCADE 不能和 null=True 同时用于 SET_NULL
|
||||
description="所属任务"
|
||||
)
|
||||
|
||||
# 可选:记录来自哪个动作模板(场景中的定义)
|
||||
action_template_id = fields.CharField(max_length=64, null=True, description="操作模板ID", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "automation_action"
|
||||
indexes = [
|
||||
["task_id", "done"], # 优化查询未完成动作
|
||||
["type", "done"],
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from tortoise import fields, models
|
||||
|
||||
from app.settings import settings
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
id = fields.BigIntField(pk=True, index=True)
|
||||
|
||||
async def to_dict(self, m2m: bool = False, exclude_fields: list[str] | None = None):
|
||||
if exclude_fields is None:
|
||||
exclude_fields = []
|
||||
|
||||
d = {}
|
||||
for field in self._meta.db_fields:
|
||||
if field not in exclude_fields:
|
||||
value = getattr(self, field)
|
||||
if isinstance(value, datetime):
|
||||
value = value.strftime(settings.DATETIME_FORMAT)
|
||||
d[field] = value
|
||||
|
||||
if m2m:
|
||||
tasks = [
|
||||
self.__fetch_m2m_field(field, exclude_fields)
|
||||
for field in self._meta.m2m_fields
|
||||
if field not in exclude_fields
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
for field, values in results:
|
||||
d[field] = values
|
||||
|
||||
return d
|
||||
|
||||
async def __fetch_m2m_field(self, field, exclude_fields):
|
||||
values = await getattr(self, field).all().values()
|
||||
formatted_values = []
|
||||
|
||||
for value in values:
|
||||
formatted_value = {}
|
||||
for k, v in value.items():
|
||||
if k not in exclude_fields:
|
||||
if isinstance(v, datetime):
|
||||
formatted_value[k] = v.strftime(settings.DATETIME_FORMAT)
|
||||
else:
|
||||
formatted_value[k] = v
|
||||
formatted_values.append(formatted_value)
|
||||
|
||||
return field, formatted_values
|
||||
|
||||
@classmethod
|
||||
def get_all_keys(cls, exclude_fields: list[str] | None = None):
|
||||
if exclude_fields is None:
|
||||
exclude_fields = []
|
||||
return [field for field in cls._meta.fields_map.keys() if field not in exclude_fields]
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class UUIDModel:
|
||||
uuid = fields.UUIDField(unique=True, pk=False, index=True)
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at = fields.DatetimeField(auto_now_add=True, index=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True, index=True)
|
||||
@@ -0,0 +1,64 @@
|
||||
from enum import Enum, StrEnum
|
||||
|
||||
|
||||
class EnumBase(Enum):
|
||||
@classmethod
|
||||
def get_member_values(cls):
|
||||
return [item.value for item in cls._member_map_.values()]
|
||||
|
||||
@classmethod
|
||||
def get_member_names(cls):
|
||||
return [name for name in cls._member_names_]
|
||||
|
||||
|
||||
class MethodType(StrEnum):
|
||||
GET = "GET"
|
||||
POST = "POST"
|
||||
PUT = "PUT"
|
||||
DELETE = "DELETE"
|
||||
PATCH = "PATCH"
|
||||
|
||||
class TaskStatus(Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
EXPIRED = "expired"
|
||||
|
||||
class TaskType(Enum):
|
||||
DECODE_ORDER = "decode_order"
|
||||
EVENT = "event"
|
||||
TIMER = "timer"
|
||||
|
||||
class ScenarioScope(str, Enum):
|
||||
ALL = "all"
|
||||
PERSONAL = "personal"
|
||||
KEFU = "kefu" # 接待员
|
||||
FOLLOW = "follow" # 跟单员
|
||||
DESIGNER = "designer" # 设计师
|
||||
|
||||
class RoleType(StrEnum):
|
||||
BUYER = "buyer"
|
||||
DESIGER = "desiger"
|
||||
FOLLOWER = "follower"
|
||||
ADMIN = "admin"
|
||||
KEFU = "taobao_kefu"
|
||||
|
||||
class MsgType(StrEnum):
|
||||
NEW_ORDER = "new_order"
|
||||
COMMENT = "comment"
|
||||
SYSTEM = "system"
|
||||
NOTIFY = "notify"
|
||||
|
||||
class ActionType(StrEnum):
|
||||
CREATE_GROUP = "create_group"
|
||||
VIEW_ERP_LOG = "view_erp_log"
|
||||
BIND_USER = "bind_user"
|
||||
BIND_ORDER = "bind_order"
|
||||
WRITE_REMARK = "write_remark"
|
||||
SEND_WECHAT_NOTIFY = "send_notify"
|
||||
# 清理群聊
|
||||
CLEAN_GROUP = "clean_group"
|
||||
# 设置群管理员
|
||||
SET_GROUP_ADMIN = "set_group_admin"
|
||||
@@ -0,0 +1,72 @@
|
||||
from tortoise import fields
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import MsgType, ActionType
|
||||
|
||||
class Msg(BaseModel, TimestampMixin):
|
||||
hash_id = fields.CharField(max_length=64, null=True, description="消息哈希ID", index=True, unique=True)
|
||||
title = fields.CharField(max_length=64, null=True, description="消息标题", index=True)
|
||||
content = fields.TextField(null=True, description="消息内容")
|
||||
detail = fields.JSONField(default={}, description="消息详情")
|
||||
is_send = fields.BooleanField(default=False, description="是否已发送", index=True)
|
||||
send_at = fields.DatetimeField(null=True, description="发送时间")
|
||||
is_read = fields.BooleanField(default=False, description="是否已读", index=True)
|
||||
read_at = fields.DatetimeField(null=True, description="已读时间")
|
||||
is_delete = fields.BooleanField(default=False, description="是否已删除", index=True)
|
||||
delete_at = fields.DatetimeField(null=True, description="删除时间")
|
||||
type = fields.CharEnumField(MsgType, null=True, description="消息类型", index=True)
|
||||
owner_id = fields.CharField(max_length=64, null=True, description="消息所有者ID", index=True)
|
||||
owner_name = fields.CharField(max_length=64, null=True, description="消息所有者名称", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "system_msg"
|
||||
|
||||
|
||||
class Follow(BaseModel, TimestampMixin):
|
||||
order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True)
|
||||
order_title = fields.TextField(null=True, description="订单标题")
|
||||
order_pic = fields.CharField(max_length=256, null=True, description="订单图片")
|
||||
pay_time = fields.DatetimeField(null=True, description="支付时间", index=True)
|
||||
pay_price = fields.FloatField(null=True, description="支付金额", index=True)
|
||||
remark = fields.TextField(null=True, description="erp额外备注")
|
||||
|
||||
customer_id = fields.CharField(max_length=64, null=True, description="客户ID", index=True)
|
||||
customer_name = fields.CharField(max_length=64, null=True, description="客户名称", index=True)
|
||||
customer_taobao_id = fields.CharField(max_length=64, null=True, description="客户淘宝ID", index=True)
|
||||
|
||||
staff_id = fields.CharField(max_length=64, null=True, description="企微员工ID", index=True)
|
||||
staff_name = fields.CharField(max_length=64, null=True, description="企微员工名称", index=True)
|
||||
staff_time = fields.DatetimeField(null=True, description="添加时间", index=True)
|
||||
|
||||
designer_id = fields.CharField(max_length=64, null=True, description="设计师ID", index=True)
|
||||
designer_name = fields.CharField(max_length=64, null=True, description="设计师名称", index=True)
|
||||
|
||||
shop_name = fields.CharField(max_length=64, null=True, description="店铺名称", index=True)
|
||||
kefu_id = fields.CharField(max_length=64, null=True, description="淘宝客服ID", index=True)
|
||||
kefu_name = fields.CharField(max_length=64, null=True, description="淘宝客服名称", index=True)
|
||||
|
||||
is_delete = fields.BooleanField(default=False, description="是否已删除", index=True)
|
||||
|
||||
feishu_record_id = fields.CharField(max_length=64, null=True, description="飞书记录ID", index=True)
|
||||
is_add_to_feishu = fields.BooleanField(default=False, description="是否已添加到飞书", index=True)
|
||||
is_update_to_feishu = fields.BooleanField(default=False, description="是否已更新到飞书", index=True)
|
||||
is_delete_from_feishu = fields.BooleanField(default=False, description="是否已从飞书删除", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "system_follow"
|
||||
|
||||
def to_feishu_record(self, fields: dict=None):
|
||||
fields = fields or {}
|
||||
return {
|
||||
"record_id": self.feishu_record_id,
|
||||
"fields": {
|
||||
"付款时间": self.pay_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"店铺名": self.shop_name,
|
||||
"成交客服": self.kefu_name,
|
||||
"旺旺id": self.customer_name,
|
||||
"添加企微客服": self.staff_name,
|
||||
"订单编号": self.order_id,
|
||||
"设计师": self.designer_name,
|
||||
**fields
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
from tortoise import fields
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import RoleType
|
||||
|
||||
class WeixinUser(BaseModel, TimestampMixin):
|
||||
# 自动成为订单关联方
|
||||
auto_relate = fields.BooleanField(default=False, description="是否自动成为订单关联方")
|
||||
userid = fields.CharField(max_length=64, description="用户微信id", index=True) # acctid
|
||||
corp_id = fields.CharField(max_length=64, description="企业微信id", default="1970325009092879", index=True)
|
||||
vid = fields.CharField(max_length=64, description="用户微信id", default='', index=True)
|
||||
wx_id_hash = fields.CharField(max_length=64, null=True, description="微信id hash")
|
||||
is_quit = fields.BooleanField(default=False, description="是否离职")
|
||||
has_external_user_permit = fields.BooleanField(default=False, description="是否具有对外联系权限")
|
||||
wx_nick_name = fields.CharField(max_length=64, null=True, description="微信昵称")
|
||||
account = fields.CharField(max_length=64, null=True, description="账号")
|
||||
|
||||
position = fields.CharField(max_length=64, null=True, description="职位")
|
||||
role = fields.CharEnumField(RoleType, null=True, description="用户角色", index=True)
|
||||
depart_ids = fields.JSONField(default=[], description="部门ID列表")
|
||||
|
||||
username = fields.CharField(max_length=64, null=True, description="用户微信名称", index=True)
|
||||
english_name = fields.CharField(max_length=64, null=True, description="英文名")
|
||||
name = fields.CharField(max_length=64, null=True, description="用户名称", index=True)
|
||||
realname = fields.CharField(max_length=64, null=True, description="用户真实名字", index=True)
|
||||
alias = fields.CharField(max_length=30, null=True, description="别名")
|
||||
avatar = fields.CharField(max_length=512, null=True, description="头像")
|
||||
mobile = fields.CharField(max_length=11, null=True, description="手机号")
|
||||
email = fields.CharField(max_length=64, null=True, description="邮箱")
|
||||
gender = fields.IntField(null=True, description="性别")
|
||||
|
||||
erp_id = fields.IntField(null=True, description="ERP中的用户ID", index=True)
|
||||
erp_name = fields.CharField(max_length=64, null=True, description="ERP中的用户名称")
|
||||
crm_id = fields.IntField(null=True, description="星云有客中的用户ID", index=True)
|
||||
crm_name = fields.CharField(max_length=64, null=True, description="星云有客中的用户名称")
|
||||
|
||||
def to_dict(self, exclude_fields=None, include_sensitive=False):
|
||||
"""
|
||||
自定义字典转换行为
|
||||
|
||||
Args:
|
||||
exclude_fields: 要排除的字段列表
|
||||
include_sensitive: 是否包含敏感信息(如手机号、邮箱等)
|
||||
|
||||
Returns:
|
||||
dict: 转换后的字典
|
||||
"""
|
||||
exclude_fields = exclude_fields or ['staff_memberships']
|
||||
data = {}
|
||||
|
||||
# 获取所有字段名
|
||||
model_fields = self._meta.fields_map.keys()
|
||||
|
||||
for field_name in model_fields:
|
||||
if field_name in exclude_fields:
|
||||
continue
|
||||
|
||||
# 如果不包含敏感信息,则跳过敏感字段
|
||||
if not include_sensitive and field_name in ['mobile', 'email']:
|
||||
continue
|
||||
|
||||
value = getattr(self, field_name)
|
||||
|
||||
# 处理枚举字段
|
||||
if field_name == 'role' and value is not None:
|
||||
data[field_name] = value.value if hasattr(value, 'value') else value
|
||||
# 处理布尔字段的默认值显示
|
||||
elif field_name == 'is_quit':
|
||||
data[field_name] = bool(value) if value is not None else False
|
||||
elif field_name == 'has_external_user_permit':
|
||||
data[field_name] = bool(value) if value is not None else False
|
||||
else:
|
||||
data[field_name] = value
|
||||
|
||||
return data
|
||||
|
||||
def to_public_dict(self):
|
||||
"""
|
||||
返回公开信息的字典(不包含敏感信息)
|
||||
"""
|
||||
sensitive_fields = ['mobile', 'email']
|
||||
exclude_fields = ['wx_id_hash'] # 可能还有其他不想暴露的字段
|
||||
all_exclude = sensitive_fields + exclude_fields
|
||||
|
||||
return self.to_dict(exclude_fields=all_exclude, include_sensitive=False)
|
||||
|
||||
def to_detail_dict(self):
|
||||
"""
|
||||
返回详细信息的字典(包含所有信息)
|
||||
"""
|
||||
return self.to_dict(include_sensitive=True)
|
||||
|
||||
def to_safe_dict(self, visible_fields=None):
|
||||
"""
|
||||
返回指定字段的安全字典
|
||||
|
||||
Args:
|
||||
visible_fields: 指定要包含的字段列表,如果为None则使用默认安全字段
|
||||
"""
|
||||
if visible_fields is None:
|
||||
# 默认的安全字段(不包含敏感信息)
|
||||
visible_fields = [
|
||||
'userid', 'vid', 'wx_nick_name', 'username', 'name',
|
||||
'realname', 'alias', 'avatar', 'position', 'role',
|
||||
'depart_ids', 'erpid', 'crmid', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
data = {}
|
||||
for field_name in visible_fields:
|
||||
if hasattr(self, field_name):
|
||||
value = getattr(self, field_name)
|
||||
if field_name == 'role' and value is not None:
|
||||
data[field_name] = value.value if hasattr(value, 'value') else value
|
||||
else:
|
||||
data[field_name] = value
|
||||
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
table = "weixin_user"
|
||||
|
||||
class WeixinGroupChat(BaseModel, TimestampMixin):
|
||||
chat_id = fields.CharField(max_length=64, description="群聊ID", index=True)
|
||||
xingyun_chat_id = fields.IntField(null=True, description="星云中的群聊ID", index=True)
|
||||
name = fields.CharField(max_length=64, null=True, description="群聊名称")
|
||||
create_time = fields.IntField(description="创建时间")
|
||||
admin_list = fields.JSONField(default=[], description="所有管理员")
|
||||
owner = fields.CharField(max_length=64, null=True, description="群主")
|
||||
member_version = fields.CharField(max_length=64, null=True, description="群成员版本")
|
||||
external_user_count = fields.IntField(null=True, description="外部群成员数量")
|
||||
external_member_list = fields.JSONField(default=[], description="外部群成员列表")
|
||||
internal_member_count = fields.IntField(null=True, description="内部群成员数量")
|
||||
internal_member_list = fields.JSONField(default=[], description="内部群成员列表")
|
||||
avatars = fields.JSONField(default=[], description="群成员头像")
|
||||
|
||||
class Meta:
|
||||
table = "weixin_group_chat"
|
||||
|
||||
class WeixinCustomer(BaseModel, TimestampMixin):
|
||||
order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True)
|
||||
shop_name = fields.CharField(max_length=64, null=True, description="店铺名称")
|
||||
weixin_id = fields.CharField(max_length=64, null=True, description="微信体系中的id", index=True)
|
||||
weixin_name = fields.CharField(max_length=64, null=True, description="微信体系中的用户名")
|
||||
weixin_unionid = fields.CharField(max_length=64, null=True, description="微信体系中的unionid", index=True)
|
||||
weixin_avatar = fields.CharField(max_length=512, null=True, description="微信头像")
|
||||
xingyun_id = fields.IntField(null=True, description="星云有客中的用户ID", index=True)
|
||||
xingyun_sex = fields.IntField(null=True, description="星云有客中的用户性别", index=True)
|
||||
xingyun_name = fields.CharField(max_length=64, null=True, description="星云有客中的用户名")
|
||||
xingyun_avatar = fields.CharField(max_length=512, null=True, description="星云有客头像")
|
||||
xingyun_tags = fields.JSONField(default=[], null=True, description="星云用户标签")
|
||||
xingyun_external_userid = fields.CharField(max_length=64, null=True, description="星云有客中的外部用户ID")
|
||||
xingyun_sync = fields.BooleanField(default=False, description="是否已同步到星云", index=True)
|
||||
# erp_id = fields.IntField(null=True, description="ERP中的客户ID", index=True)
|
||||
# erp_name = fields.CharField(max_length=64, null=True, description="ERP中的客户名称")
|
||||
# erp_avatar = fields.CharField(max_length=512, null=True, description="ERP头像")
|
||||
taobao_id = fields.CharField(max_length=64, null=True, description="淘宝中的用户ID", index=True)
|
||||
taobao_name = fields.CharField(max_length=64, null=True, description="淘宝中的用户名")
|
||||
taobao_avatar = fields.CharField(max_length=512, null=True, description="淘宝头像")
|
||||
need_confirm = fields.BooleanField(null=True, description="是否需要确认")
|
||||
extra = fields.JSONField(default={}, description="额外信息")
|
||||
|
||||
# 使用 through 指向自定义中间模型
|
||||
groups = fields.ManyToManyField(
|
||||
"models.WeixinGroupChat",
|
||||
through="customer_group", # 必须是中间模型的 table 名(或模型名)
|
||||
related_name="customers"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def orm_format(cls, data: dict):
|
||||
# 将不在字段中的字段保存到extra字段中
|
||||
all_fields = set(cls._meta.fields_map.keys())
|
||||
|
||||
extra = {}
|
||||
data_in = {"extra": extra}
|
||||
for k, v in data.copy().items():
|
||||
if k not in all_fields:
|
||||
extra[k] = v
|
||||
else:
|
||||
data_in[k] = v
|
||||
|
||||
id = data_in.pop('id', None)
|
||||
if id: data_in['platform_id'] = id
|
||||
return data_in
|
||||
|
||||
def to_dict(self, *args, **kwargs):
|
||||
# 类似于WeixinUser参数
|
||||
return {
|
||||
"id": self.id,
|
||||
"userid": self.weixin_id,
|
||||
"name": self.taobao_name,
|
||||
"avatar": self.xingyun_avatar,
|
||||
"weixin_name": self.weixin_name,
|
||||
"role": 'buyer',
|
||||
"is_customer": True,
|
||||
}
|
||||
# data = await super().to_dict(*args, **kwargs)
|
||||
# data['id'] = data.pop('platform_id', None)
|
||||
# data.update(data.pop('extra', {}))
|
||||
# return data
|
||||
|
||||
def dump_dict(self):
|
||||
all_fields = set(self._meta.fields_map.keys())
|
||||
data = {}
|
||||
for k, v in self.__dict__.items():
|
||||
if k in all_fields:
|
||||
data[k] = v
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_bind(cls, data: dict):
|
||||
# 将不在字段中的字段保存到extra字段中
|
||||
data_in = cls.orm_format(data)
|
||||
return cls(**data_in)
|
||||
|
||||
class Meta:
|
||||
table = "weixin_customer"
|
||||
|
||||
# === 中间模型:CustomerGroup ===
|
||||
class CustomerGroup(BaseModel, TimestampMixin):
|
||||
"""顾客与群聊的关联关系(带额外信息)"""
|
||||
|
||||
# 外键指向顾客
|
||||
customer = fields.ForeignKeyField(
|
||||
"models.WeixinCustomer",
|
||||
related_name="group_memberships" # 从 Customer 反向查关系
|
||||
)
|
||||
|
||||
# 外键指向群聊
|
||||
group = fields.ForeignKeyField(
|
||||
"models.WeixinGroupChat",
|
||||
related_name="customer_memberships" # 从 Group 反向查关系
|
||||
)
|
||||
|
||||
# 外键指向群聊
|
||||
staff = fields.ForeignKeyField(
|
||||
"models.WeixinUser",
|
||||
related_name="staff_memberships" # 从 User 反向查关系
|
||||
)
|
||||
|
||||
# 额外字段
|
||||
join_time = fields.DatetimeField(auto_now_add=True, description="入群时间", index=True)
|
||||
role = fields.CharField(
|
||||
max_length=32,
|
||||
default="member",
|
||||
description="群内角色:member / admin / owner",
|
||||
index=True
|
||||
)
|
||||
|
||||
staff_userid = fields.CharField(max_length=64, null=True, description="客服ID", index=True)
|
||||
customer_userid = fields.CharField(max_length=64, null=True, description="顾客的ID", index=True)
|
||||
group_chatid = fields.CharField(max_length=64, null=True, description="群聊ID", index=True)
|
||||
order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True)
|
||||
shop_name = fields.CharField(max_length=64, null=True, description="店铺名称", index=True)
|
||||
remark = fields.CharField(max_length=255, null=True, description="备注")
|
||||
|
||||
class Meta:
|
||||
table = "customer_group"
|
||||
# 确保同一个顾客不能重复加入同一个群
|
||||
unique_together = ("customer", "group")
|
||||
Reference in New Issue
Block a user