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