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')