first commit

This commit is contained in:
2026-06-25 17:41:06 +08:00
commit b2f23a933b
370 changed files with 30526 additions and 0 deletions
+272
View File
@@ -0,0 +1,272 @@
import os
import zipfile
from io import BytesIO
from pathlib import Path
from mako.lookup import TemplateLookup
import black
class CodeGenerator:
def __init__(self, template_root='templates'):
self.template_root = template_root
self.lookup = TemplateLookup(directories=[template_root], input_encoding='utf-8')
def generate_files(self, entity_config, prj_template_dir, params, language='python'):
"""
生成所有匹配的模板文件
:param entity_config: 实体配置
:param prj_template_dir: 项目对应的文件路径
:param params: 生成参数
:return: 生成的文件字典 {相对路径: 文件内容}
"""
entity_config = type("EntityConfig", (object,), entity_config)
generated_files = []
template_dir = os.path.join(self.template_root, prj_template_dir)
# 遍历模板目录
for root, _, files in os.walk(template_dir):
for file in files:
if not file.endswith('.mako'): continue
template_path = os.path.join(root, file)
relative_path = os.path.relpath(template_path, self.template_root)
# 渲染模板
template = self.lookup.get_template(relative_path)
content = template.render(entity=entity_config, params=params, generator=self)
if relative_path.endswith('.py.mako'):
content = self.post_process(content)
# 计算输出路径
output_path = self._get_output_path(relative_path, entity_config, params)
# generated_files[output_path] = content
generated_files.append(dict(
path=output_path,
content=content
))
return generated_files
def post_process(self, content):
try:
# 尝试使用 Black 格式化代码
formatted_code = black.format_str(content, mode=black.Mode(line_length=100))
return formatted_code
except Exception as e:
print(f"Black 格式化失败: {e}")
return content
def _get_output_path(self, template_path, entity_config, params):
"""
根据模板路径计算输出路径
:param template_path: 模板相对路径
:return: 输出文件相对路径(无.mako后缀)
"""
# 移除.mako后缀
if template_path.endswith('.mako'):
output_path = template_path[:-5]
else:
output_path = template_path
# 替换实体名称占位符
entity_name = entity_config.name
output_path = output_path.replace('Entity', entity_name.capitalize())
output_path = output_path.replace('entity', entity_name.lower())
return output_path
def _process_relations(self, entity):
"""处理实体关系并返回处理后的结果"""
if not hasattr(entity, 'relations') or not entity.relations:
return []
processed = []
for rel in entity.relations:
# 确保关系有必要的字段
rel.setdefault('through', None)
rel.setdefault('description', '')
# 计算反向引用名称
rel['reverse_name'] = self._get_reverse_relation_name(entity.name, rel)
processed.append(rel)
return processed
def _get_reverse_relation_name(self, entity_name, relation):
"""生成反向引用名称"""
if relation['type'] == 'one-to-many':
return entity_name.lower()
elif relation['type'] == 'many-to-one':
return relation['name']
elif relation['type'] == 'many-to-many':
return f"{entity_name.lower()}s"
return None
def create_zip(self, generated_files):
"""
将生成的文件打包为zip
:param generated_files: {路径: 内容} 字典
:return: zip文件字节流
"""
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for item in generated_files:
path, content = item.get('path'), item.get('content')
zip_file.writestr(path, content)
zip_buffer.seek(0)
return zip_buffer
def create_local_code(self, generated_files, output_dir):
save_dir = Path(output_dir)
for item in generated_files:
# 确保输出目录存在
path, content = item.get('path'), item.get('content')
output_file = save_dir / path
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, 'w', encoding='utf-8', newline='') as f:
f.write(content)
if __name__ == "__main__":
# 使用示例
generator = CodeGenerator()
default_frontend_config = {
"editable": True,
"listable": True,
"detailable": True,
"sortable": True,
"filterable": True,
"filter_operator": 'equal', # equal | contains 默认过滤操作符为包含
"display_type": 'text', # 默认显示类型为文本
}
entity_config = {
"name": "user", # 实体名称(小写)
"author": "Ly997", # 实体名称(小写)
"description": "系统用户管理", # 实体描述
"version": "1.0.0", # 版本号
"fields": [
{
"name": "id",
"type": "Long",
"primary_key": True,
"description": "用户ID",
"required": True
},
{
"name": "username",
"type": "String",
"length": 50,
"description": "用户名",
"required": True,
"unique": True,
"validations": [
{"type": "min_length", "value": 4},
{"type": "max_length", "value": 20}
]
},
{
"name": "email",
"type": "String",
"description": "电子邮箱",
"required": True,
"validations": [
{"type": "email"}
]
},
{
"name": "password_hash",
"type": "String",
"description": "密码哈希",
"required": True,
"secret": True # 标记为敏感字段
},
{
"name": "is_active",
"type": "Boolean",
"description": "是否激活",
"default": True
},
{
"name": "created_at",
"type": "DateTime",
"description": "创建时间",
"auto_now_add": True
},
{
"name": "updated_at",
"type": "DateTime",
"description": "更新时间",
"auto_now": True
}
],
"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",
}
]
}
}
params = {
"package": "com.example",
"author": "John Doe"
}
# 在代码中合并默认配置和字段的前端配置
for field in entity_config["fields"]:
frontend = {**default_frontend_config, **field}
field.update(frontend)
import json
with open('tmp.json', 'w', encoding='utf-8') as f:
json.dump(entity_config, f, ensure_ascii=False, indent=4)
# 生成所有文件
generated_files = generator.generate_files(entity_config, 'relation-demo', params)
# generated_files = generator.generate_files(entity_config, 'vue-fastapi-admin', params)
generator.create_local_code(generated_files, 'tmp')
# 打包为zip
zip_data = generator.create_zip(generated_files)
file_path = './tmp.zip'
with open(file_path, 'wb') as f:
f.write(zip_data.getvalue())
print(f"文件已保存到 {file_path}")
# 在Flask中返回zip文件示例
# return send_file(zip_data, mimetype='application/zip', as_attachment=True, download_name='generated_code.zip')
+133
View File
@@ -0,0 +1,133 @@
import random
import string
from hashlib import md5
from typing import Any, Dict, List, Union, Optional, get_origin, get_args, Type
from pydantic import BaseModel
def gen_random_str(length=10, prefix="", suffix=""):
"""
生成随机字符串,默认长度为10,可添加前缀和后缀
"""
random_str = prefix + ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + suffix
return md5(random_str.encode()).hexdigest()
def split_generator(iterable, chunk_size=50):
"""
将可迭代对象拆分为多个子列表
"""
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == chunk_size:
yield chunk
chunk = []
if chunk:
yield chunk
async def async_split_generator(iterable, chunk_size=50):
"""
将可迭代对象拆分为多个子列表
"""
chunk = []
async for item in iterable:
chunk.append(item)
if len(chunk) == chunk_size:
yield chunk
chunk = []
if chunk:
yield chunk
async def async_generator_to_list(iterable):
"""
将可迭代对象转换为列表
"""
return [item async for item in iterable]
TYPE_MAPPING = {
"str": "string",
"int": "number",
"float": "number",
"bool": "boolean",
"list": "array",
"dict": "object",
"NoneType": "null", # 注意:None 的类型名是 'NoneType'
}
def transform_pydantic_to_list(
cls: Type[BaseModel],
prefix: str = "" # 新增:当前路径前缀
) -> List[Dict[str, Any]]:
if not issubclass(cls, BaseModel):
raise TypeError(f"{cls} is not a Pydantic BaseModel")
result = []
exclude_filter = getattr(cls, "__exclude_filter__", [])
meta_mapping = getattr(cls, "__meta_mapping__", {})
for field_name, field_info in cls.model_fields.items():
if field_name in exclude_filter:
continue
# 构建当前字段的完整路径
current_path = f"{prefix}.{field_name}" if prefix else field_name
field_meta = meta_mapping.get(field_name, {}) or {}
raw_type = field_info.annotation
children = []
display_type_name = "any"
origin = get_origin(raw_type)
args = get_args(raw_type)
# 处理 Optional[T](即 Union[T, None]
if origin is Union and type(None) in args:
# 提取非 None 的类型
non_none_types = [arg for arg in args if arg is not type(None)]
if len(non_none_types) == 1:
inner_type = non_none_types[0]
else:
inner_type = raw_type # 多类型 Union,暂不深入处理
else:
inner_type = raw_type
# 判断是否为 List[BaseModel]
list_origin = get_origin(inner_type)
if list_origin is list:
item_type = get_args(inner_type)[0] if get_args(inner_type) else Any
if isinstance(item_type, type) and issubclass(item_type, BaseModel):
children = transform_pydantic_to_list(item_type, prefix=current_path)
display_type_name = "list"
elif isinstance(inner_type, type) and issubclass(inner_type, BaseModel):
children = transform_pydantic_to_list(inner_type, prefix=current_path)
display_type_name = "object"
else:
# 基础类型:获取类型名
if inner_type is type(None):
display_type_name = "NoneType"
elif hasattr(inner_type, '__name__'):
display_type_name = inner_type.__name__
else:
display_type_name = str(inner_type)
frontend_type = TYPE_MAPPING.get(display_type_name, "string")
item = {
"value": current_path, # ✅ 使用层级路径
"label": field_info.description or field_name,
"type": frontend_type,
**field_meta,
}
if children:
item["children"] = children
result.append(item)
return result
if __name__ == "__main__":
print(gen_random_str())
print(gen_random_str())
print(gen_random_str())
print(gen_random_str())
+134
View File
@@ -0,0 +1,134 @@
import pymysql
class DatabaseInfo:
def __init__(self, host, user, port, password, database):
self.host = host
self.port = port
self.user = user
self.password = password
self.database = database
self.connection = None
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
def connect(self):
try:
self.connection = pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database
)
except pymysql.Error as e:
print(f"数据库连接失败: {e}")
def disconnect(self):
if self.connection:
self.connection.close()
def get_all_tables(self):
if not self.connection:
self.connect()
try:
with self.connection.cursor() as cursor:
cursor.execute("SHOW TABLE STATUS")
tables = cursor.fetchall()
table_info_list = []
for table in tables:
table_info = {
"tableName": table[0],
"tableComment": table[17] # 表注释信息在第 19 列(索引为 18)
}
table_info_list.append(table_info)
return table_info_list
except pymysql.Error as e:
print(f"获取表名失败: {e}")
return []
def get_table_info(self, table_name):
if not self.connection:
self.connect()
try:
with self.connection.cursor() as cursor:
cursor.execute("SHOW TABLE STATUS LIKE %s", (table_name,))
table = cursor.fetchone()
table_info = {
"tableName": table[0],
"tableComment": table[17] # 表注释信息在第 19 列(索引为 18)
}
return table_info
except pymysql.Error as e:
print(f"获取表名失败: {e}")
return {}
def parse_type_and_validation(self, column_type):
# 简单的类型解析,实际可能需要更复杂的逻辑
if "int" in column_type.lower():
return dict(type="int", validation="int")
elif "varchar" in column_type.lower():
number = column_type.split("(")[1].split(")")[0].strip()
return dict(type="str", validations=[
dict(type="str", min=1, max=int(number))
])
# elif "datetime" in column_type.lower():
# return dict(type="str", validations=[
# dict(type="str", format="%Y-%m-%d %H:%M:%S")
# ])
else:
return dict(type=column_type)
def get_table_structure(self, table_name):
if not self.connection:
self.connect()
try:
table_info = self.get_table_info(table_name)
with self.connection.cursor() as cursor:
# 使用 SHOW FULL COLUMNS FROM 获取包含注释的列信息
cursor.execute(f"SHOW FULL COLUMNS FROM {table_name}")
columns = cursor.fetchall()
structure = []
for column in columns:
column_info = {
"name": column[0],
"type": column[1],
"required": column[2] is not None,
"primary_key": column[4] == 'PRI',
# "default": column[4],
"description": column[8] # 注释信息在第 9 列(索引为 8
}
column_info.update(self.parse_type_and_validation(column_info["type"])) # 添加解析后的类型和验证信息
structure.append(column_info)
table_info['fields'] = structure
return table_info
except pymysql.Error as e:
print(f"获取表 {table_name} 结构失败: {e}")
return {'fields': []}
if __name__ == "__main__":
# 请根据实际情况修改数据库连接信息
db_info = DatabaseInfo(
host="lt.330770.xyz",
port=3307,
user="root",
password="rap_sky",
database="rpa"
)
tables = db_info.get_all_tables()
print("所有表名:")
for table in tables:
print(table)
tableName = table.get('TableName')
structure = db_info.get_table_structure(tableName)
print(f"{tableName} 的结构信息:")
for column in structure:
print(column)
db_info.disconnect()
+237
View File
@@ -0,0 +1,237 @@
from typing import Dict, List, Callable, Any
from fastapi import BackgroundTasks
import asyncio
import logging
from enum import Enum
import time
logger = logging.getLogger(__name__)
class EventPriority(Enum):
HIGH = 1
MEDIUM = 2
LOW = 3
class EventType:
# 同步星云联系人信息
SYNC_XINGYUN_CONTACT_INFO = "sync_xingyun_contact_info"
# 绑定订单给用户
BIND_ORDER_FOR_USER = "bind_order_for_user"
# 打开个人聊天会话
OPEN_PERSONAL_CHAT = "open_personal_chat"
# 群聊变更
GROUP_CHAT_UPDATED = "group_chat_updated"
# 检查创建群聊
CHECK_CREATE_GROUP_CHAT = "check_create_group_chat"
# 客服领单
CUSTOMER_SERVICE_ASSIGN_ORDER = "customer_service_assign_order"
# 分配设计师
DESIGNER_ASSIGN_ORDER = "designer_assign_order"
# 客户复购
OLD_CUSTOMER_REPEAT_PURCHASE = "old_customer_repeat_purchase"
# 客户退货
CUSTOMER_RETURN_ORDER = "customer_return_order"
# 设计稿上传
DESIGNER_UPLOAD_DESIGN = "designer_upload_design"
# 客户在新店铺下单
CUSTOMER_ORDER_IN_NEW_SHOP = "customer_order_in_new_shop"
EventList = [
{
"value": EventType.SYNC_XINGYUN_CONTACT_INFO,
"label": "同步星云联系人信息",
"description": "当星云联系人信息发生变化时触发",
"automation_event": False
},
{
"value": EventType.BIND_ORDER_FOR_USER,
"label": "绑定订单",
"description": "当用户创建订单时触发",
"automation_event": True
},
{
"value": EventType.OPEN_PERSONAL_CHAT,
"label": "打开个人聊天会话",
"description": "当用户创建个人聊天时触发",
"automation_event": False
},
{
"value": EventType.GROUP_CHAT_UPDATED,
"label": "群聊变更",
"description": "当群聊信息发生变化时触发",
"automation_event": True
},
{
"value": EventType.CHECK_CREATE_GROUP_CHAT,
"label": "群聊创建",
"description": "当用户请求创建群聊时触发",
"automation_event": True
},
{
"value": EventType.CUSTOMER_SERVICE_ASSIGN_ORDER,
"label": "客服领单",
"description": "当客服领单时触发",
"automation_event": True
},
{
"value": EventType.DESIGNER_ASSIGN_ORDER,
"label": "分配设计师",
"description": "当分配设计师时触发",
"automation_event": True
},
{
"value": EventType.OLD_CUSTOMER_REPEAT_PURCHASE,
"label": "客户复购",
"description": "当老客复购时触发",
"automation_event": True
},
{
"value": EventType.CUSTOMER_RETURN_ORDER,
"label": "客户退货",
"description": "当客户退货时触发",
"automation_event": True
},
{
"value": EventType.DESIGNER_UPLOAD_DESIGN,
"label": "设计稿上传",
"description": "当设计师上传设计稿时触发",
"automation_event": True
},
{
"value": EventType.CUSTOMER_ORDER_IN_NEW_SHOP,
"label": "客户在新店铺下单",
"description": "当客户在新店铺下单时触发",
"automation_event": True
},
]
class NonBlockingEventManager:
def __init__(self):
self.handlers: Dict[str, List[Callable]] = {}
self.priority_handlers: Dict[EventPriority, List[Callable]] = {
EventPriority.HIGH: [],
EventPriority.MEDIUM: [],
EventPriority.LOW: []
}
self.pretasks: List[Callable] = []
self.automation_event_handlers: Dict[str, Any] = []
def subscribe(self, event_type: str, handler: Callable, priority: EventPriority = EventPriority.MEDIUM, input_model: Any = None):
if event_type not in self.handlers:
self.handlers[event_type] = []
for event in EventList:
if event["value"] == event_type:
event["field_tree"] = input_model
self.automation_event_handlers.append(event)
logger.info(f"Subscribe {event_type} with priority {priority}")
# print(f"Subscribe {event_type} with priority {priority}", flush=True)
self.handlers[event_type].append(handler)
def subscribe_priority(self, handler: Callable, priority: EventPriority = EventPriority.MEDIUM):
"""订阅所有事件类型的处理器,按优先级"""
self.priority_handlers[priority].append(handler)
async def publish_async(self, event_type: str, data: Any, background_tasks: BackgroundTasks):
"""异步发布事件,不阻塞主线程"""
# 添加到后台任务,立即返回
background_tasks.add_task(
self._process_event_async,
event_type,
data
)
def register_pretask(self, pretask: Callable):
"""注册预任务"""
self.pretasks.append(pretask)
async def trigger_event(self, event_type: str, event_data: Any):
"""解析事件数据"""
for pretask in self.pretasks:
await pretask(event_type, event_data)
async def _process_event_async(self, event_type: str, data: Any):
"""异步处理事件"""
start_time = time.time()
# 触发事件系统
try:
await self.trigger_event(event_type, data)
except Exception as e:
logger.exception(f'处理事件失败,{e}')
# 处理优先级处理器(高优先级先执行)
for priority in [EventPriority.HIGH, EventPriority.MEDIUM, EventPriority.LOW]:
priority_tasks = []
for handler in self.priority_handlers[priority]:
try:
if asyncio.iscoroutinefunction(handler):
task = handler(event_type, data)
else:
task = asyncio.to_thread(handler, event_type, data)
priority_tasks.append(task)
except Exception as e:
logger.error(f"Error in priority handler: {e}")
if priority_tasks:
await asyncio.gather(*priority_tasks, return_exceptions=True)
# 处理特定事件类型的处理器
if event_type in self.handlers:
specific_tasks = []
for handler in self.handlers[event_type]:
try:
if asyncio.iscoroutinefunction(handler):
task = handler(event_type, data)
else:
task = asyncio.to_thread(handler, event_type, data)
specific_tasks.append(task)
except Exception as e:
logging.error(f"Error in specific handler: {e}")
if specific_tasks:
await asyncio.gather(*specific_tasks, return_exceptions=True)
logging.info(f"Event {event_type} processed in {time.time() - start_time:.2f}s")
# 创建全局事件管理器
event_manager = NonBlockingEventManager()
# # 定义事件处理器
# async def log_event_handler(event_type: str, data: Any):
# await asyncio.sleep(0.1) # 模拟异步日志记录
# logging.info(f"Logged event: {event_type}")
# def update_cache_handler( Any):
# import time
# time.sleep(0.5) # 模拟缓存更新
# logging.info(f"Cache updated with: {data}")
# # 注册处理器
# event_manager.subscribe_priority(log_event_handler, EventPriority.HIGH)
# event_manager.subscribe("order_created", update_cache_handler, EventPriority.MEDIUM)
# @app.post("/orders")
# async def create_order_complete(order_ dict, background_tasks: BackgroundTasks):
# # 执行主要业务逻辑
# order_id = f"order_{hash(str(order_data))}"
# order_data["order_id"] = order_id
# # 保存到数据库
# # await save_order_to_db(order_data)
# # 立即返回响应
# response = {"order_id": order_id, "status": "created"}
# # 异步发布事件,不阻塞响应
# await event_manager.publish_async("order_created", order_data, background_tasks)
# return response
# # 启动时配置
# @app.on_event("startup")
# async def startup_event():
# logging.basicConfig(level=logging.INFO)
+34
View File
@@ -0,0 +1,34 @@
# excel_utils.py
import pandas as pd
from typing import Dict, List, Tuple
def get_sheets_and_headers(
file_path: str,
header: int = 0,
skiprows: int = 0,
**kwargs
) -> Dict[str, List[str]]:
"""
读取 Excel 文件,返回每个 sheet 的名称及其表头(列名)。
参数:
file_path (str): Excel 文件路径(支持 .xlsx / .xls
header (int): 表头所在行索引(默认 0,即第一行)
skiprows (int): 跳过的行数(在表头之前)
**kwargs: 透传给 pd.read_excel 的其他参数(如 engine 等)
返回:
Dict[str, List[str]]: {sheet_name: [col1, col2, ...]}
"""
# 使用 nrows=0 只读取表头,不加载数据,性能高
all_sheets = pd.read_excel(
file_path,
sheet_name=None, # 读取所有 sheet
header=header,
skiprows=skiprows,
nrows=0, # ⚡ 关键:只读表头
**kwargs
)
return {name: df.columns.tolist() for name, df in all_sheets.items()}
+10
View File
@@ -0,0 +1,10 @@
import jwt
from app.schemas.login import JWTPayload
from app.settings.config import settings
def create_access_token(*, data: JWTPayload):
payload = data.model_dump().copy()
encoded_jwt = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt
+16
View File
@@ -0,0 +1,16 @@
from passlib import pwd
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def generate_password() -> str:
return pwd.genword()
Submodule app/utils/some_sdk added at 2e8b24e312