commit 3a7b41ebce59cde0598525314a7bbc56c0fd75a0 Author: pony <1356137040@qq.com> Date: Mon Nov 17 18:22:56 2025 +0800 代码上传 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2a4543 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.c +*.pyc +*.pyd +dist +build +.pypirc \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f974d6a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 yang kunhao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..6b9bb88 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,14 @@ +# 保留安装和说明文件 +include README.md +include LICENSE +include requirements.txt + +# 包含二进制文件 +recursive-include hertz_studio_django_xxx *.py +recursive-include hertz_studio_django_xxx/migrations *.py + + + +# 排除源代码和缓存(但已通过include保留了必要的.py文件) +global-exclude *.pyc +global-exclude __pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/hertz_studio_django_utils/README.md b/hertz_studio_django_utils/README.md new file mode 100644 index 0000000..220d700 --- /dev/null +++ b/hertz_studio_django_utils/README.md @@ -0,0 +1,438 @@ +# Hertz Studio Django Utils 模块 + +## 概述 + +Hertz Studio Django Utils 是一个功能丰富的工具类模块,为 Hertz Server Django 项目提供核心的工具函数、响应格式、验证器和加密服务。该模块采用模块化设计,便于维护和扩展。 + +## 功能特性 + +- 🔐 **加密工具**: 提供多种加密算法和密码哈希功能 +- 📧 **邮件服务**: 统一的邮件发送接口和验证码邮件模板 +- 📋 **响应格式**: 标准化的API响应格式和错误处理 +- ✅ **数据验证**: 邮箱、密码、手机号等格式验证 +- 🧩 **模块化设计**: 各功能模块独立,便于按需使用 + +## 模块结构 + +``` +hertz_studio_django_utils/ +├── __init__.py # 模块初始化 +├── crypto/ # 加密工具 +│ ├── __init__.py +│ ├── encryption_utils.py # 通用加密工具 +│ └── password_hashers.py # 密码哈希器 +├── email/ # 邮件服务 +│ ├── __init__.py +│ └── email_service.py # 邮件发送服务 +├── responses/ # 响应格式 +│ ├── __init__.py +│ └── HertzResponse.py # 统一响应类 +└── validators/ # 验证器 + ├── __init__.py + ├── email_validator.py # 邮箱验证器 + ├── password_validator.py # 密码验证器 + └── phone_validator.py # 手机号验证器 +``` + +## 核心类说明 + +### 1. EncryptionUtils 加密工具类 + +提供多种加密算法和工具函数: + +```python +from hertz_studio_django_utils.crypto import EncryptionUtils + +# MD5哈希 +hash_value = EncryptionUtils.md5_hash("password", "salt") + +# SHA256哈希 +hash_value = EncryptionUtils.sha256_hash("password", "salt") + +# 数据加密解密 +encrypted = EncryptionUtils.encrypt_data("敏感数据", "password") +decrypted = EncryptionUtils.decrypt_data(encrypted, "password") + +# 生成随机盐值 +salt = EncryptionUtils.generate_salt(32) +``` + +### 2. MD5PasswordHasher MD5密码哈希器 + +兼容旧系统的MD5密码加密: + +```python +from hertz_studio_django_utils.crypto import MD5PasswordHasher + +hasher = MD5PasswordHasher() +encoded_password = hasher.encode("password", "salt") +is_valid = hasher.verify("password", encoded_password) +``` + +### 3. EmailService 邮件服务类 + +提供邮件发送功能: + +```python +from hertz_studio_django_utils.email import EmailService + +# 发送普通邮件 +success = EmailService.send_email( + recipient_email="user@example.com", + subject="邮件主题", + html_content="

HTML内容

", + text_content="纯文本内容" +) + +# 发送验证码邮件 +success = EmailService.send_verification_code( + recipient_email="user@example.com", + recipient_name="用户名", + verification_code="123456", + code_type="register" +) +``` + +### 4. HertzResponse 统一响应类 + +标准化的API响应格式: + +```python +from hertz_studio_django_utils.responses import HertzResponse + +# 成功响应 +return HertzResponse.success(data={"user": user_data}, message="操作成功") + +# 失败响应 +return HertzResponse.fail(message="操作失败", data={"error": "详情"}) + +# 错误响应 +return HertzResponse.error(message="系统错误", error=str(e)) + +# 验证错误 +return HertzResponse.validation_error(message="参数错误", errors=serializer.errors) + +# 自定义响应 +return HertzResponse.custom( + success=True, + message="自定义消息", + data={"custom": "data"}, + code=200 +) +``` + +### 5. 验证器类 + +提供数据格式验证功能: + +```python +from hertz_studio_django_utils.validators import ( + EmailValidator, PasswordValidator, PhoneValidator +) + +# 邮箱验证 +is_valid, message = EmailValidator.validate_email("test@example.com") +normalized_email = EmailValidator.normalize_email(" Test@Example.COM ") + +# 密码强度验证 +is_valid, errors = PasswordValidator.validate_password_strength("Password123!") +score = PasswordValidator.calculate_password_score("Password123!") +level = PasswordValidator.get_password_strength_level("Password123!") + +# 手机号验证 +is_valid = PhoneValidator.is_valid_china_mobile("13800138000") +is_valid, message = PhoneValidator.validate_china_mobile("13800138000") +carrier = PhoneValidator.get_mobile_carrier("13800138000") +``` + +## 安装和配置 + +### 1. 依赖安装 + +确保已安装以下依赖: + +```bash +pip install Django>=5.2.6 +pip install cryptography>=41.0.0 +``` + +### 2. 配置邮件服务 + +在 `settings.py` 中配置邮件服务: + +```python +# 邮件配置 +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.example.com' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = 'your-email@example.com' +EMAIL_HOST_PASSWORD = 'your-password' +DEFAULT_FROM_EMAIL = 'noreply@example.com' +``` + +### 3. 配置密码哈希器 + +在 `settings.py` 中配置密码哈希器: + +```python +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', + 'hertz_studio_django_utils.crypto.password_hashers.MD5PasswordHasher', # MD5兼容 +] +``` + +## 快速开始 + +### 1. 导入模块 + +```python +# 导入整个工具模块 +from hertz_studio_django_utils import ( + EncryptionUtils, + MD5PasswordHasher, + EmailService, + HertzResponse, + EmailValidator, + PasswordValidator, + PhoneValidator +) + +# 或按需导入特定功能 +from hertz_studio_django_utils.responses import HertzResponse +from hertz_studio_django_utils.email import EmailService +``` + +### 2. 使用示例 + +```python +# 在Django视图中使用统一响应 +from rest_framework.decorators import api_view +from hertz_studio_django_utils.responses import HertzResponse + +@api_view(['GET']) +def user_profile(request): + try: + user_data = {"username": "testuser", "email": "test@example.com"} + return HertzResponse.success(data=user_data, message="获取用户信息成功") + except Exception as e: + return HertzResponse.error(message="获取用户信息失败", error=str(e)) + +# 发送验证码邮件 +from hertz_studio_django_utils.email import EmailService + +success = EmailService.send_verification_code( + recipient_email="user@example.com", + recipient_name="张三", + verification_code="654321", + code_type="register" +) + +if success: + print("验证码邮件发送成功") +else: + print("验证码邮件发送失败") +``` + +## API接口 + +### 加密工具 API + +| 方法 | 描述 | 参数 | 返回值 | +|------|------|------|--------| +| `md5_hash(data, salt)` | MD5哈希加密 | data: str, salt: str | str | +| `sha256_hash(data, salt)` | SHA256哈希加密 | data: str, salt: str | str | +| `encrypt_data(data, password)` | 加密数据 | data: str, password: str | Optional[str] | +| `decrypt_data(encrypted_data, password)` | 解密数据 | encrypted_data: str, password: str | Optional[str] | +| `generate_salt(length)` | 生成随机盐值 | length: int | str | + +### 邮件服务 API + +| 方法 | 描述 | 参数 | 返回值 | +|------|------|------|--------| +| `send_email()` | 发送邮件 | recipient_email, subject, html_content, text_content, from_email | bool | +| `send_verification_code()` | 发送验证码邮件 | recipient_email, recipient_name, verification_code, code_type | bool | + +### 响应格式 API + +| 方法 | 描述 | 参数 | 返回值 | +|------|------|------|--------| +| `success()` | 成功响应 | data, message, code | JsonResponse | +| `fail()` | 失败响应 | message, data, code | JsonResponse | +| `error()` | 错误响应 | message, error, code | JsonResponse | +| `unauthorized()` | 未授权响应 | message, code | JsonResponse | +| `validation_error()` | 验证错误响应 | message, errors, code | JsonResponse | +| `custom()` | 自定义响应 | success, message, data, code, **kwargs | JsonResponse | + +### 验证器 API + +| 类 | 方法 | 描述 | +|----|------|------| +| `EmailValidator` | `is_valid_email()` | 验证邮箱格式 | +| | `validate_email()` | 验证邮箱并返回详细信息 | +| | `normalize_email()` | 标准化邮箱地址 | +| `PasswordValidator` | `validate_password_strength()` | 验证密码强度 | +| | `calculate_password_score()` | 计算密码强度分数 | +| | `get_password_strength_level()` | 获取密码强度等级 | +| `PhoneValidator` | `is_valid_china_mobile()` | 验证中国大陆手机号 | +| | `validate_china_mobile()` | 验证手机号并返回详细信息 | +| | `normalize_phone()` | 标准化手机号 | + +## 配置参数 + +### 邮件服务配置 + +| 参数 | 默认值 | 描述 | +|------|--------|------| +| `EMAIL_HOST` | - | SMTP服务器地址 | +| `EMAIL_PORT` | 587 | SMTP端口 | +| `EMAIL_USE_TLS` | True | 使用TLS加密 | +| `EMAIL_HOST_USER` | - | SMTP用户名 | +| `EMAIL_HOST_PASSWORD` | - | SMTP密码 | +| `DEFAULT_FROM_EMAIL` | - | 默认发件人邮箱 | + +### 密码验证配置 + +| 参数 | 默认值 | 描述 | +|------|--------|------| +| `min_length` | 8 | 密码最小长度 | +| `max_length` | 128 | 密码最大长度 | + +## 高级用法 + +### 自定义邮件模板 + +```python +from hertz_studio_django_utils.email import EmailService + +# 自定义邮件内容 +custom_html = """ + + +

自定义邮件

+

您好 {name},这是一封自定义邮件。

+ + +""".format(name="张三") + +success = EmailService.send_email( + recipient_email="user@example.com", + subject="自定义邮件", + html_content=custom_html +) +``` + +### 扩展响应格式 + +```python +from hertz_studio_django_utils.responses import HertzResponse + +# 扩展自定义响应 +class CustomResponse(HertzResponse): + @staticmethod + def paginated(data, total, page, page_size, message="查询成功"): + """分页响应""" + pagination = { + "total": total, + "page": page, + "page_size": page_size, + "total_pages": (total + page_size - 1) // page_size + } + return HertzResponse.success( + data={"items": data, "pagination": pagination}, + message=message + ) + +# 使用扩展响应 +return CustomResponse.paginated( + data=users, + total=100, + page=1, + page_size=20, + message="用户列表查询成功" +) +``` + +## 测试 + +### 单元测试示例 + +```python +from django.test import TestCase +from hertz_studio_django_utils.validators import EmailValidator + +class EmailValidatorTest(TestCase): + def test_valid_email(self): + """测试有效邮箱地址""" + is_valid, message = EmailValidator.validate_email("test@example.com") + self.assertTrue(is_valid) + self.assertEqual(message, "邮箱地址格式正确") + + def test_invalid_email(self): + """测试无效邮箱地址""" + is_valid, message = EmailValidator.validate_email("invalid-email") + self.assertFalse(is_valid) + self.assertEqual(message, "邮箱地址格式不正确") +``` + +### 运行测试 + +```bash +python manage.py test hertz_studio_django_utils.tests +``` + +## 安全考虑 + +1. **密码安全**: 使用强密码哈希算法,避免明文存储密码 +2. **加密安全**: 使用安全的加密算法和随机盐值 +3. **输入验证**: 对所有输入数据进行严格验证 +4. **错误处理**: 避免泄露敏感错误信息 +5. **邮件安全**: 使用TLS加密邮件传输 + +## 常见问题 + +### Q: 邮件发送失败怎么办? +A: 检查邮件配置是否正确,包括SMTP服务器、端口、用户名和密码。 + +### Q: MD5密码哈希是否安全? +A: MD5被认为是不安全的哈希算法,仅用于兼容旧系统。新系统应使用更安全的算法如bcrypt或Argon2。 + +### Q: 如何自定义响应格式? +A: 可以继承 `HertzResponse` 类并添加自定义方法。 + +### Q: 验证器是否支持国际手机号? +A: 目前主要支持中国大陆手机号验证,国际手机号验证功能有限。 + +## 更新日志 + +### v1.0.0 (2024-01-01) +- 初始版本发布 +- 包含加密工具、邮件服务、响应格式、验证器等核心功能 + +## 🔗 相关链接 + +- [🏠 返回主项目](../README.md) - Hertz Server Django 主项目 +- [🔐 认证授权模块](../hertz_studio_django_auth/README.md) - 用户管理和权限控制 +- [🖼️ 验证码模块](../hertz_studio_django_captcha/README.md) - 图片和邮箱验证码功能 +- [📋 代码风格指南](../CODING_STYLE_GUIDE.md) - 开发规范和最佳实践 +- [Django 官方文档](https://docs.djangoproject.com/) +- [Django REST Framework 文档](https://www.django-rest-framework.org/) + +## 贡献指南 + +1. Fork 本项目 +2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 打开 Pull Request + +## 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 + +## 支持 + +如有问题或建议,请提交 [Issue](https://github.com/your-org/hertz-server-django/issues) 或联系开发团队。 \ No newline at end of file diff --git a/hertz_studio_django_utils/__init__.py b/hertz_studio_django_utils/__init__.py new file mode 100644 index 0000000..e5b8fd2 --- /dev/null +++ b/hertz_studio_django_utils/__init__.py @@ -0,0 +1 @@ +# Hertz Server Django Utils Package \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/__init__.py b/hertz_studio_django_utils/code_generator/__init__.py new file mode 100644 index 0000000..7021b65 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/__init__.py @@ -0,0 +1,39 @@ +""" +Django代码生成器模块 + +该模块提供了Django应用代码自动生成功能,包括: +- 模型(ORM)代码生成 +- 序列化器代码生成 +- 视图(CRUD)代码生成 +- URL路由代码生成 + +使用示例: + from hertz_studio_django_utils.code_generator import DjangoCodeGenerator + + generator = DjangoCodeGenerator() + generator.generate_full_module( + model_name='User', + fields=[ + {'name': 'username', 'type': 'CharField', 'max_length': 150}, + {'name': 'email', 'type': 'EmailField'} + ] + ) +""" + +from .base_generator import BaseGenerator +from .django_code_generator import DjangoCodeGenerator +from .model_generator import ModelGenerator +from .serializer_generator import SerializerGenerator +from .view_generator import ViewGenerator +from .url_generator import URLGenerator +from .menu_generator import MenuGenerator + +__all__ = [ + 'BaseGenerator', + 'DjangoCodeGenerator', + 'ModelGenerator', + 'SerializerGenerator', + 'ViewGenerator', + 'URLGenerator', + 'MenuGenerator' +] \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/app_generator.py b/hertz_studio_django_utils/code_generator/app_generator.py new file mode 100644 index 0000000..d5edb84 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/app_generator.py @@ -0,0 +1,286 @@ +""" +Django应用代码生成器 + +该模块负责根据配置生成完整的Django应用代码 +""" + +import os +from typing import Dict, List, Any, Optional +from .base_generator import BaseGenerator +from .model_generator import ModelGenerator +from .serializer_generator import SerializerGenerator +from .view_generator import ViewGenerator +from .url_generator import URLGenerator + + +class AppGenerator(BaseGenerator): + """Django应用代码生成器""" + + def __init__(self): + """初始化应用生成器""" + super().__init__() + self.model_generator = ModelGenerator() + self.serializer_generator = SerializerGenerator() + self.view_generator = ViewGenerator() + self.url_generator = URLGenerator() + + def generate(self, app_name: str = None, models: List[Dict[str, Any]] = None, **kwargs) -> Dict[str, str]: + """ + 生成完整的Django应用代码 + + Args: + app_name: 应用名称 + models: 模型配置列表 + **kwargs: 其他参数 + + Returns: + Dict[str, str]: 生成的文件代码映射 + """ + # 处理参数 + if app_name is None: + app_name = kwargs.get('app_name', 'default_app') + if models is None: + models = kwargs.get('models', []) + + api_version = kwargs.get('api_version', 'v1') + include_admin = kwargs.get('include_admin', True) + include_tests = kwargs.get('include_tests', True) + + generated_files = {} + + # 生成应用配置文件 + generated_files['apps.py'] = self.generate_apps_config(app_name) + + # 生成__init__.py文件 + generated_files['__init__.py'] = self.generate_init_file(app_name) + + # 生成模型、序列化器、视图和URL + all_models = [] + for model_config in models: + model_name = model_config.get('name', 'DefaultModel') + fields = model_config.get('fields', []) + operations = model_config.get('operations', ['list', 'create', 'retrieve', 'update', 'delete']) + + # 生成模型代码 + model_code = self.model_generator.generate( + model_name=model_name, + fields=fields, + **model_config + ) + + # 生成序列化器代码 + serializer_code = self.serializer_generator.generate( + model_name=model_name, + fields=fields + ) + + # 生成视图代码 + view_code = self.view_generator.generate( + model_name=model_name, + operations=operations + ) + + # 生成URL代码 + url_code = self.url_generator.generate( + model_name=model_name, + operations=operations, + app_name=app_name + ) + + # 添加到生成的文件中 + generated_files[f'models/{model_name.lower()}_models.py'] = model_code + generated_files[f'serializers/{model_name.lower()}_serializers.py'] = serializer_code + generated_files[f'views/{model_name.lower()}_views.py'] = view_code + generated_files[f'urls/{model_name.lower()}_urls.py'] = url_code + + all_models.append(model_name) + + # 生成主要文件 + generated_files['models.py'] = self.generate_models_init(all_models) + generated_files['serializers/__init__.py'] = self.generate_serializers_init(all_models) + generated_files['views/__init__.py'] = self.generate_views_init(all_models) + generated_files['urls.py'] = self.generate_main_urls(app_name, all_models, api_version) + + # 生成管理后台文件 + if include_admin: + generated_files['admin.py'] = self.generate_admin_config(all_models) + + # 生成测试文件 + if include_tests: + generated_files['tests.py'] = self.generate_tests(all_models) + + return generated_files + + def generate_apps_config(self, app_name: str) -> str: + """生成应用配置文件""" + context = { + 'app_name': app_name, + 'app_label': app_name.replace('hertz_studio_django_', ''), + 'verbose_name': app_name.replace('_', ' ').title() + } + return self.render_template('django/apps.mako', context) + + def generate_init_file(self, app_name: str) -> str: + """生成__init__.py文件""" + return f'"""\n{app_name} Django应用\n"""\n\ndefault_app_config = \'{app_name}.apps.{app_name.title().replace("_", "")}Config\'\n' + + def generate_models_init(self, models: List[str]) -> str: + """生成models.py主文件""" + imports = [] + for model in models: + imports.append(f'from .models.{model.lower()}_models import {model}') + + context = { + 'imports': imports, + 'models': models + } + return self.render_template('django/models_init.mako', context) + + def generate_serializers_init(self, models: List[str]) -> str: + """生成serializers/__init__.py文件""" + imports = [] + for model in models: + imports.append(f'from .{model.lower()}_serializers import {model}Serializer') + + return '\n'.join(imports) + '\n' + + def generate_views_init(self, models: List[str]) -> str: + """生成views/__init__.py文件""" + imports = [] + for model in models: + imports.append(f'from .{model.lower()}_views import {model}ViewSet') + + return '\n'.join(imports) + '\n' + + def generate_main_urls(self, app_name: str, models: List[str], api_version: str) -> str: + """生成主URL配置文件""" + context = { + 'app_name': app_name, + 'models': models, + 'api_version': api_version, + 'url_includes': [f'path(\'{model.lower()}/\', include(\'{app_name}.urls.{model.lower()}_urls\'))' for model in models] + } + return self.render_template('django/main_urls.mako', context) + + def generate_admin_config(self, models: List[str]) -> str: + """生成管理后台配置""" + context = { + 'models': models + } + return self.render_template('django/admin.mako', context) + + def generate_tests(self, models: List[str]) -> str: + """生成测试文件""" + context = { + 'models': models + } + return self.render_template('django/tests.mako', context) + + def generate_full_app( + self, + app_name: str, + models: List[Dict[str, Any]], + output_dir: str = None, + **kwargs + ) -> Dict[str, str]: + """ + 生成完整的Django应用并写入文件 + + Args: + app_name: 应用名称 + models: 模型配置列表 + output_dir: 输出目录 + **kwargs: 其他参数 + + Returns: + Dict[str, str]: 生成的文件路径映射 + """ + generated_files = self.generate(app_name, models, **kwargs) + + if output_dir: + file_paths = {} + for file_name, content in generated_files.items(): + file_path = os.path.join(output_dir, app_name, file_name) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + file_paths[file_name] = file_path + + return file_paths + + return generated_files + + def generate_django_project( + self, + project_name: str, + apps: List[Dict[str, Any]], + output_dir: str = None, + **kwargs + ) -> Dict[str, str]: + """ + 生成完整的Django项目 + + Args: + project_name: 项目名称 + apps: 应用配置列表 + output_dir: 输出目录 + **kwargs: 其他参数 + + Returns: + Dict[str, str]: 生成的文件路径映射 + """ + generated_files = {} + + # 生成项目配置文件 + generated_files['manage.py'] = self.generate_manage_py(project_name) + generated_files[f'{project_name}/settings.py'] = self.generate_settings(project_name, apps) + generated_files[f'{project_name}/urls.py'] = self.generate_project_urls(project_name, apps) + generated_files[f'{project_name}/wsgi.py'] = self.generate_wsgi(project_name) + generated_files[f'{project_name}/asgi.py'] = self.generate_asgi(project_name) + generated_files[f'{project_name}/__init__.py'] = '' + + # 生成每个应用 + for app_config in apps: + app_name = app_config.get('name', 'default_app') + models = app_config.get('models', []) + + app_files = self.generate(app_name, models, **app_config) + for file_name, content in app_files.items(): + generated_files[f'{app_name}/{file_name}'] = content + + return generated_files + + def generate_manage_py(self, project_name: str) -> str: + """生成manage.py文件""" + context = {'project_name': project_name} + return self.render_template('django/manage.mako', context) + + def generate_settings(self, project_name: str, apps: List[Dict[str, Any]]) -> str: + """生成settings.py文件""" + app_names = [app.get('name', 'default_app') for app in apps] + context = { + 'project_name': project_name, + 'apps': app_names + } + return self.render_template('django/settings.mako', context) + + def generate_project_urls(self, project_name: str, apps: List[Dict[str, Any]]) -> str: + """生成项目主URL配置""" + app_names = [app.get('name', 'default_app') for app in apps] + context = { + 'project_name': project_name, + 'apps': app_names + } + return self.render_template('django/project_urls.mako', context) + + def generate_wsgi(self, project_name: str) -> str: + """生成WSGI配置""" + context = {'project_name': project_name} + return self.render_template('django/wsgi.mako', context) + + def generate_asgi(self, project_name: str) -> str: + """生成ASGI配置""" + context = {'project_name': project_name} + return self.render_template('django/asgi.mako', context) \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/base_generator.py b/hertz_studio_django_utils/code_generator/base_generator.py new file mode 100644 index 0000000..8458a28 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/base_generator.py @@ -0,0 +1,262 @@ +""" +基础代码生成器类 + +提供代码生成的基础功能和通用方法 +""" + +import os +import re +from typing import Dict, List, Any, Optional +from abc import ABC, abstractmethod +from .template_engine import TemplateEngine + + +class BaseGenerator(ABC): + """ + 基础代码生成器抽象类 + """ + + def __init__(self, template_dir: str = None): + """ + 初始化基础生成器 + + Args: + template_dir: 模板目录路径 + """ + self.template_vars = {} + self.template_engine = TemplateEngine(template_dir) + + def render_template(self, template_name: str, context: Dict[str, Any] = None) -> str: + """ + 渲染模板 + + Args: + template_name: 模板文件名 + context: 模板上下文变量 + + Returns: + str: 渲染后的代码字符串 + """ + if context is None: + context = {} + + # 合并模板变量和上下文 + merged_context = {**self.template_vars, **context} + + return self.template_engine.render_template(template_name, merged_context) + + def create_template_context(self, **kwargs) -> Dict[str, Any]: + """ + 创建模板上下文 + + Args: + **kwargs: 上下文变量 + + Returns: + Dict[str, Any]: 模板上下文 + """ + context = {**self.template_vars, **kwargs} + + # 添加常用的辅助函数到上下文 + context.update({ + 'snake_to_camel': self.snake_to_camel, + 'camel_to_snake': self.camel_to_snake, + 'format_field_name': self.format_field_name, + 'format_class_name': self.format_class_name, + 'format_verbose_name': self.format_verbose_name, + 'get_django_field_type': self.get_django_field_type, + }) + + return context + + @abstractmethod + def generate(self, **kwargs) -> str: + """ + 生成代码的抽象方法 + + Returns: + str: 生成的代码字符串 + """ + pass + + def set_template_var(self, key: str, value: Any) -> None: + """ + 设置模板变量 + + Args: + key: 变量名 + value: 变量值 + """ + self.template_vars[key] = value + + def get_template_var(self, key: str, default: Any = None) -> Any: + """ + 获取模板变量 + + Args: + key: 变量名 + default: 默认值 + + Returns: + Any: 变量值 + """ + return self.template_vars.get(key, default) + + def snake_to_camel(self, snake_str: str) -> str: + """ + 将蛇形命名转换为驼峰命名 + + Args: + snake_str: 蛇形命名字符串 + + Returns: + str: 驼峰命名字符串 + """ + components = snake_str.split('_') + return ''.join(word.capitalize() for word in components) + + def camel_to_snake(self, camel_str: str) -> str: + """ + 将驼峰命名转换为蛇形命名 + + Args: + camel_str: 驼峰命名字符串 + + Returns: + str: 蛇形命名字符串 + """ + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel_str) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + def format_field_name(self, field_name: str) -> str: + """ + 格式化字段名称 + + Args: + field_name: 原始字段名 + + Returns: + str: 格式化后的字段名 + """ + return field_name.lower().replace(' ', '_').replace('-', '_') + + def format_class_name(self, name: str) -> str: + """ + 格式化类名 + + Args: + name: 原始名称 + + Returns: + str: 格式化后的类名 + """ + return self.snake_to_camel(self.format_field_name(name)) + + def format_verbose_name(self, field_name: str) -> str: + """ + 格式化verbose_name + + Args: + field_name: 字段名 + + Returns: + str: 格式化后的verbose_name + """ + return field_name.replace('_', ' ').title() + + def generate_imports(self, imports: List[str]) -> str: + """ + 生成导入语句 + + Args: + imports: 导入列表 + + Returns: + str: 导入语句字符串 + """ + if not imports: + return '' + + return '\n'.join(imports) + '\n\n' + + def indent_code(self, code: str, indent_level: int = 1) -> str: + """ + 为代码添加缩进 + + Args: + code: 代码字符串 + indent_level: 缩进级别 + + Returns: + str: 缩进后的代码 + """ + indent = ' ' * indent_level + lines = code.split('\n') + return '\n'.join(indent + line if line.strip() else line for line in lines) + + def write_to_file(self, file_path: str, content: str) -> bool: + """ + 将内容写入文件 + + Args: + file_path: 文件路径 + content: 文件内容 + + Returns: + bool: 是否写入成功 + """ + try: + # 确保目录存在 + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + return True + except Exception as e: + print(f"写入文件失败: {e}") + return False + + def validate_field_config(self, field_config: Dict[str, Any]) -> bool: + """ + 验证字段配置 + + Args: + field_config: 字段配置字典 + + Returns: + bool: 配置是否有效 + """ + required_keys = ['name', 'type'] + return all(key in field_config for key in required_keys) + + def get_django_field_type(self, field_type: str) -> str: + """ + 获取Django字段类型 + + Args: + field_type: 字段类型 + + Returns: + str: Django字段类型 + """ + field_mapping = { + 'string': 'CharField', + 'text': 'TextField', + 'integer': 'IntegerField', + 'float': 'FloatField', + 'decimal': 'DecimalField', + 'boolean': 'BooleanField', + 'date': 'DateField', + 'datetime': 'DateTimeField', + 'time': 'TimeField', + 'email': 'EmailField', + 'url': 'URLField', + 'file': 'FileField', + 'image': 'ImageField', + 'json': 'JSONField', + 'uuid': 'UUIDField', + 'foreign_key': 'ForeignKey', + 'many_to_many': 'ManyToManyField', + 'one_to_one': 'OneToOneField' + } + return field_mapping.get(field_type.lower(), 'CharField') \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/copy/hertz_server_django/README.md b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/README.md new file mode 100644 index 0000000..8ad7a0a --- /dev/null +++ b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/README.md @@ -0,0 +1,401 @@ +# Hertz Server Django 主项目配置 + +## 📋 项目概述 + +Hertz Server Django 是一个基于 Django 和 Django REST Framework 构建的现代化后端服务框架,提供认证授权、验证码、工具类等核心功能模块。主项目配置模块负责整个项目的全局配置、路由管理和基础设置。 + +## ✨ 核心特性 + +- **模块化架构**: 采用微服务架构设计,各功能模块独立开发部署 +- **RESTful API**: 基于DRF构建的标准REST API接口 +- **OpenAPI 3.0文档**: 自动生成的API文档,支持Swagger UI和ReDoc +- **多数据库支持**: 支持SQLite和MySQL,可配置Redis作为缓存和会话存储 +- **跨域支持**: 内置CORS跨域请求处理 +- **WebSocket支持**: 基于Channels的实时通信能力 +- **环境配置**: 使用python-decouple进行环境变量管理 + +## 📁 项目结构 + +``` +hertz_server_django/ # 项目根目录 +├── hertz_server_django/ # 主项目配置模块 +│ ├── __init__.py # 包初始化文件 +│ ├── settings.py # 项目全局配置 +│ ├── urls.py # 主URL路由配置 +│ ├── asgi.py # ASGI应用配置 +│ ├── wsgi.py # WSGI应用配置 +│ └── views.py # 根视图函数 +├── hertz_demo/ # 演示模块 +├── hertz_studio_django_captcha/ # 验证码模块 +├── hertz_studio_django_auth/ # 认证授权模块 +├── hertz_studio_django_utils/ # 工具类模块 +├── manage.py # Django管理脚本 +├── requirements.txt # 项目依赖 +├── .env # 环境变量配置 +└── data/ # 数据目录(SQLite数据库等) +``` + +## ⚙️ 核心配置文件 + +### settings.py - 项目全局配置 + +#### 基础配置 +```python +# 项目根目录 +BASE_DIR = Path(__file__).resolve().parent.parent + +# 安全密钥(从环境变量读取) +SECRET_KEY = config('SECRET_KEY', default='django-insecure-...') + +# 调试模式 +DEBUG = config('DEBUG', default=True, cast=bool) + +# 允许的主机 +ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1') +``` + +#### 应用配置 +```python +INSTALLED_APPS = [ + # Django核心应用 + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # 第三方应用 + 'rest_framework', # Django REST Framework + 'corsheaders', # CORS跨域支持 + 'channels', # WebSocket支持 + 'drf_spectacular', # OpenAPI文档生成 + + # 本地应用模块 + 'hertz_demo', # 演示模块 + 'hertz_studio_django_captcha', # 验证码模块 + 'hertz_studio_django_auth', # 认证授权模块 +] +``` + +#### 数据库配置 +```python +# 数据库切换配置 +USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool) + +if USE_REDIS_AS_DB: + # 开发环境使用SQLite + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'data/db.sqlite3', + } + } +else: + # 生产环境使用MySQL + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': config('DB_NAME', default='hertz_server'), + 'USER': config('DB_USER', default='root'), + 'PASSWORD': config('DB_PASSWORD', default='root'), + 'HOST': config('DB_HOST', default='localhost'), + 'PORT': config('DB_PORT', default='3306'), + 'OPTIONS': {'charset': 'utf8mb4'}, + } + } +``` + +#### Redis缓存配置 +```python +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/0'), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } +} +``` + +#### DRF配置 +```python +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_AUTHENTICATION_CLASSES': [], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ], +} +``` + +#### OpenAPI文档配置 +```python +SPECTACULAR_SETTINGS = { + 'TITLE': 'Hertz Server API', + 'DESCRIPTION': 'API documentation for Hertz Server Django project', + 'VERSION': '1.0.0', + 'SERVE_INCLUDE_SCHEMA': False, + 'COMPONENT_SPLIT_REQUEST': True, + 'SCHEMA_PATH_PREFIX': '/api/', +} +``` + +### urls.py - 主路由配置 + +```python +from django.urls import path, include +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from . import views + +urlpatterns = [ + # 首页路由 + path('', views.index, name='index'), + + # Hertz Captcha路由 + path('api/captcha/', include('hertz_studio_django_captcha.urls')), + + # Hertz Auth路由 + path('api/', include('hertz_studio_django_auth.urls')), + + # Demo应用路由 + path('', include('hertz_demo.urls')), + + # API文档 + path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), + path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), +] +``` + +## 🚀 快速开始 + +### 1. 环境准备 + +```bash +# 创建虚拟环境 +python -m venv venv + +# 激活虚拟环境 +# Windows: +venv\Scripts\activate +# Linux/Mac: +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 2. 环境配置 + +创建 `.env` 文件: + +```ini +# 基础配置 +DEBUG=True +SECRET_KEY=your-secret-key-here +ALLOWED_HOSTS=localhost,127.0.0.1 + +# 数据库配置 +USE_REDIS_AS_DB=True +REDIS_URL=redis://127.0.0.1:6379/0 + +# MySQL配置(如果USE_REDIS_AS_DB=False) +DB_NAME=hertz_server +DB_USER=root +DB_PASSWORD=root +DB_HOST=localhost +DB_PORT=3306 +``` + +### 3. 数据库初始化 + +```bash +# 创建数据库迁移 +python manage.py makemigrations + +# 应用数据库迁移 +python manage.py migrate + +# 创建超级用户(可选) +python manage.py createsuperuser +``` + +### 4. 启动开发服务器 + +```bash +# 启动Django开发服务器 +python manage.py runserver + +# 访问应用 +# 首页: http://localhost:8000/ +# API文档: http://localhost:8000/api/docs/ +# 演示页面: http://localhost:8000/demo/captcha/ +``` + +## 🔧 配置详解 + +### 环境变量管理 + +项目使用 `python-decouple` 进行环境变量管理,支持: +- 从 `.env` 文件读取配置 +- 类型转换和默认值设置 +- 开发和生产环境分离 + +### 数据库配置策略 + +**开发环境**: 使用SQLite + Redis缓存 +- 快速启动和开发测试 +- 数据存储在SQLite文件 +- 会话和缓存使用Redis + +**生产环境**: 使用MySQL + Redis缓存 +- 高性能数据库支持 +- 数据持久化存储 +- Redis用于缓存和会话 + +### 中间件配置 + +```python +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', # CORS处理 + 'django.middleware.security.SecurityMiddleware', # 安全中间件 + 'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理 + 'django.middleware.common.CommonMiddleware', # 通用处理 + 'django.middleware.csrf.CsrfViewMiddleware', # CSRF保护 + 'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证 + 'hertz_studio_django_auth.utils.middleware.AuthMiddleware', # 自定义认证 + 'django.contrib.messages.middleware.MessageMiddleware', # 消息框架 + 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持保护 +] +``` + +## 🌐 API文档访问 + +项目提供完整的OpenAPI 3.0文档: + +- **Swagger UI**: http://localhost:8000/api/docs/ +- **ReDoc**: http://localhost:8000/api/redoc/ +- **Schema JSON**: http://localhost:8000/api/schema/ + +## 🚢 部署配置 + +### 生产环境部署 + +1. **环境变量配置**: +```ini +DEBUG=False +SECRET_KEY=your-production-secret-key +ALLOWED_HOSTS=your-domain.com,api.your-domain.com +USE_REDIS_AS_DB=False +``` + +2. **静态文件收集**: +```bash +python manage.py collectstatic +``` + +3. **WSGI部署**: +```python +# 使用Gunicorn + Nginx +# gunicorn hertz_server_django.wsgi:application +``` + +4. **ASGI部署**: +```python +# 使用Daphne + Nginx +# daphne hertz_server_django.asgi:application +``` + +## 🔒 安全配置 + +### 生产环境安全设置 + +```python +# 强制HTTPS +SECURE_SSL_REDIRECT = True +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + +# CSRF保护 +CSRF_COOKIE_SECURE = True +CSRF_COOKIE_HTTPONLY = True + +# 会话安全 +SESSION_COOKIE_SECURE = True +SESSION_COOKIE_HTTPONLY = True + +# 安全头部 +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = 'DENY' +``` + +## 📊 性能优化 + +### 缓存策略 + +```python +# 数据库查询缓存 +from django.core.cache import cache + +# 使用缓存 +cache.set('key', 'value', timeout=300) +value = cache.get('key') +``` + +### 数据库优化 + +```python +# 使用select_related减少查询 +users = User.objects.select_related('profile').filter(is_active=True) + +# 使用prefetch_related优化多对多关系 +users = User.objects.prefetch_related('groups', 'permissions') +``` + +## 🐛 故障排除 + +### 常见问题 + +1. **Redis连接失败**: 检查Redis服务是否启动,配置是否正确 +2. **数据库迁移错误**: 删除数据库文件重新迁移,或检查MySQL连接 +3. **静态文件404**: 运行 `python manage.py collectstatic` +4. **CORS问题**: 检查CORS配置和中间件顺序 + +### 日志配置 + +```python +# settings.py 中添加日志配置 +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} +``` + +## 🔗 相关链接 + +- [🎮 演示模块](../hertz_demo/README.md) - 功能演示和测试页面 +- [🔐 认证授权模块](../hertz_studio_django_auth/README.md) - 用户管理和权限控制 +- [📸 验证码模块](../hertz_studio_django_captcha/README.md) - 验证码生成和验证 +- [🛠️ 工具类模块](../hertz_studio_django_utils/README.md) - 加密、邮件和验证工具 +- [🐍 Django文档](https://docs.djangoproject.com/) - Django官方文档 +- [🔌 DRF文档](https://www.django-rest-framework.org/) - Django REST Framework文档 + +--- + +💡 **提示**: 此配置模块是整个项目的核心,负责协调各功能模块的协同工作。生产部署前请务必检查所有安全配置。 \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/copy/hertz_server_django/__init__.py b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hertz_studio_django_utils/code_generator/copy/hertz_server_django/asgi.py b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/asgi.py new file mode 100644 index 0000000..fb05dfc --- /dev/null +++ b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/asgi.py @@ -0,0 +1,35 @@ +""" +ASGI config for hertz_server_django project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os +from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack +from channels.security.websocket import AllowedHostsOriginValidator + +from hertz_demo import routing + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings') + +# Initialize Django ASGI application early to ensure the AppRegistry +# is populated before importing code that may import ORM models. +django_asgi_app = get_asgi_application() + +# Import websocket routing + +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack( + URLRouter( + routing.websocket_urlpatterns + ) + ) + ), +}) diff --git a/hertz_studio_django_utils/code_generator/copy/hertz_server_django/settings.py b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/settings.py new file mode 100644 index 0000000..83f18d7 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/settings.py @@ -0,0 +1,335 @@ +""" +Django settings for hertz_server_django project. + +Generated by 'django-admin startproject' using Django 5.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +import os +from pathlib import Path +from decouple import config + +# 修复DRF的ip_address_validators函数 +def fix_drf_ip_validators(): + """ + 修复DRF的ip_address_validators函数返回值问题 + """ + try: + from rest_framework import fields + + # 保存原始函数 + original_ip_address_validators = fields.ip_address_validators + + def fixed_ip_address_validators(protocol, unpack_ipv4): + """ + 修复后的ip_address_validators函数,确保返回两个值 + """ + validators = original_ip_address_validators(protocol, unpack_ipv4) + # 如果只返回了validators,添加默认的error_message + if isinstance(validators, list): + return validators, 'Enter a valid IP address.' + else: + # 如果已经返回了两个值,直接返回 + return validators + + # 应用猴子补丁 + fields.ip_address_validators = fixed_ip_address_validators + + except ImportError: + # 如果DRF未安装,忽略错误 + pass + +# 应用修复 +fix_drf_ip_validators() + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY', default='django-insecure-0a1bx*8!97l^4z#ml#ufn_*9ut*)zlso$*k-g^h&(2=p@^51md') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = config('DEBUG', default=True, cast=bool) + +ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')]) + +# Database switch configuration +USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool) + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # Third party apps + 'rest_framework', + 'corsheaders', + 'channels', + 'drf_spectacular', + + # Local apps + 'hertz_demo', + 'hertz_studio_django_captcha', + 'hertz_studio_django_auth', # 权限管理系统 + 'hertz_studio_django_notice', # 通知公告模块 + 'hertz_studio_django_ai', + 'hertz_studio_django_system_monitor', # 系统监测模块 + 'hertz_studio_django_log', # 日志管理模块 + 'hertz_studio_django_wiki', # 知识管理模块 + +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'hertz_studio_django_auth.utils.middleware.AuthMiddleware', # 权限认证中间件 + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'hertz_server_django.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'] + , + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'hertz_server_django.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +if USE_REDIS_AS_DB: + # Redis as primary database (for caching and session storage) + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'data/db.sqlite3', + } + } + + # Use Redis for sessions + SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + SESSION_CACHE_ALIAS = 'default' + +else: + # MySQL database configuration + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': config('DB_NAME', default='hertz_server'), + 'USER': config('DB_USER', default='root'), + 'PASSWORD': config('DB_PASSWORD', default='root'), + 'HOST': config('DB_HOST', default='localhost'), + 'PORT': config('DB_PORT', default='3306'), + 'OPTIONS': { + 'charset': 'utf8mb4', + }, + } + } + +# Redis +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/0'), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' +STATICFILES_DIRS = [ + BASE_DIR / 'static', +] + +# Media files (User uploaded files) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Django REST Framework configuration +# 使用自定义AuthMiddleware进行认证,不使用DRF的认证和权限系统 +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_AUTHENTICATION_CLASSES': [], # 不使用DRF认证类 + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', # 所有接口默认允许访问,由AuthMiddleware控制权限 + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ], +} + +# Spectacular (OpenAPI 3.0) configuration +SPECTACULAR_SETTINGS = { + 'TITLE': 'Hertz Server API', + 'DESCRIPTION': 'API documentation for Hertz Server Django project', + 'VERSION': '1.0.0', + 'SERVE_INCLUDE_SCHEMA': False, + 'COMPONENT_SPLIT_REQUEST': True, + 'SCHEMA_PATH_PREFIX': '/api/', +} + +# CORS configuration +CORS_ALLOWED_ORIGINS = config( + 'CORS_ALLOWED_ORIGINS', + default='http://localhost:3000,http://127.0.0.1:3000', + cast=lambda v: [s.strip() for s in v.split(',')] +) + +CORS_ALLOW_CREDENTIALS = True + +CORS_ALLOW_ALL_ORIGINS = config('CORS_ALLOW_ALL_ORIGINS', default=False, cast=bool) + +# Captcha settings +CAPTCHA_IMAGE_SIZE = ( + config('CAPTCHA_IMAGE_SIZE_WIDTH', default=120, cast=int), + config('CAPTCHA_IMAGE_SIZE_HEIGHT', default=50, cast=int) +) +CAPTCHA_LENGTH = config('CAPTCHA_LENGTH', default=4, cast=int) +CAPTCHA_TIMEOUT = config('CAPTCHA_TIMEOUT', default=5, cast=int) # minutes +CAPTCHA_FONT_SIZE = config('CAPTCHA_FONT_SIZE', default=40, cast=int) +CAPTCHA_BACKGROUND_COLOR = config('CAPTCHA_BACKGROUND_COLOR', default='#ffffff') +CAPTCHA_FOREGROUND_COLOR = config('CAPTCHA_FOREGROUND_COLOR', default='#000000') +# 验证码词典文件路径 +CAPTCHA_WORDS_DICTIONARY = str(BASE_DIR / 'captcha_words.txt') +# 验证码挑战函数配置 +CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # 默认使用随机字符 +# 数学验证码配置 +CAPTCHA_MATH_CHALLENGE_OPERATOR = '+-*' +# 验证码噪声和过滤器 +CAPTCHA_NOISE_FUNCTIONS = ( + 'captcha.helpers.noise_arcs', + 'captcha.helpers.noise_dots', +) +CAPTCHA_FILTER_FUNCTIONS = ( + 'captcha.helpers.post_smooth', +) + +# Email configuration +EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend') +EMAIL_HOST = config('EMAIL_HOST', default='smtp.qq.com') +EMAIL_PORT = config('EMAIL_PORT', default=465, cast=int) +EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=True, cast=bool) +EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool) +EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='563161210@qq.com') +EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='') +DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='563161210@qq.com') + +# Channels configuration for WebSocket support +ASGI_APPLICATION = 'hertz_server_django.asgi.application' + +# Channel layers configuration +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [config('REDIS_URL', default='redis://127.0.0.1:6379/2')], + }, + }, +} + +# 自定义用户模型 +AUTH_USER_MODEL = 'hertz_studio_django_auth.HertzUser' + +# JWT配置 +JWT_SECRET_KEY = config('JWT_SECRET_KEY', default=SECRET_KEY) +JWT_ALGORITHM = 'HS256' +JWT_ACCESS_TOKEN_LIFETIME = config('JWT_ACCESS_TOKEN_LIFETIME', default=60 * 60 * 24, cast=int) # 24小时 +JWT_REFRESH_TOKEN_LIFETIME = config('JWT_REFRESH_TOKEN_LIFETIME', default=60 * 60 * 24 * 7, cast=int) # 7天 + +# 权限系统配置 +HERTZ_AUTH_SETTINGS = { + 'SUPER_ADMIN_PERMISSIONS': ['*'], # 超级管理员拥有所有权限 + 'DEFAULT_PERMISSIONS': [], # 默认权限 +} + +# AuthMiddleware配置 - 不需要登录验证的URL模式(支持正则表达式) +NO_AUTH_PATTERNS = config( + 'NO_AUTH_PATTERNS', + default=r'^/api/auth/login/?$,^/api/auth/register/?$,^/api/auth/email/code/?$,^/api/auth/send-email-code/?$,^/api/auth/password/reset/?$,^/api/captcha/.*$,^/api/docs/.*$,^/api/redoc/.*$,^/api/schema/.*$,^/admin/.*$,^/static/.*$,^/media/.*$,^/demo/.*$,^/websocket/.*$,^/api/system/.*$', + cast=lambda v: [s.strip() for s in v.split(',')] +) + +# 密码加密配置 +PASSWORD_HASHERS = [ + 'hertz_studio_django_utils.crypto.MD5PasswordHasher', # 使用MD5加密 + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', +] diff --git a/hertz_studio_django_utils/code_generator/copy/hertz_server_django/urls.py b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/urls.py new file mode 100644 index 0000000..ae143a3 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/urls.py @@ -0,0 +1,62 @@ +""" +URL configuration for hertz_server_django project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from . import views + +urlpatterns = [ + # 首页路由 + path('', views.index, name='index'), + + # Hertz Captcha routes + path('api/captcha/', include('hertz_studio_django_captcha.urls')), + + # Hertz Auth routes + path('api/', include('hertz_studio_django_auth.urls')), + + # Demo app routes + path('', include('hertz_demo.urls')), + + # Hertz AI routes + path('api/ai/', include('hertz_studio_django_ai.urls')), + + # Hertz System Monitor routes + path('api/system/', include('hertz_studio_django_system_monitor.urls')), + + # Hertz System Notification routes + path('api/notice/', include('hertz_studio_django_notice.urls')), + + # Hertz Log routes + path('api/log/', include('hertz_studio_django_log.urls')), + + # Hertz Wiki routes + path('api/wiki/', include('hertz_studio_django_wiki.urls')), + + + + # OpenAPI documentation + path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), + path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), +] + +# 在开发环境下提供媒体文件服务 +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0]) diff --git a/hertz_studio_django_utils/code_generator/copy/hertz_server_django/views.py b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/views.py new file mode 100644 index 0000000..5d1f7e8 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/views.py @@ -0,0 +1,13 @@ +from django.contrib.auth.models import Permission +from django.shortcuts import render + +from hertz_studio_django_auth.utils.decorators import no_login_required + + +@no_login_required +def index(request): + """ + 系统首页视图 + 展示系统的基础介绍和功能特性 + """ + return render(request, 'index.html') \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/copy/hertz_server_django/wsgi.py b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/wsgi.py new file mode 100644 index 0000000..1e9fdf9 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/copy/hertz_server_django/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for hertz_server_django project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings') + +application = get_wsgi_application() diff --git a/hertz_studio_django_utils/code_generator/django_code_generator.py b/hertz_studio_django_utils/code_generator/django_code_generator.py new file mode 100644 index 0000000..dea34f3 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/django_code_generator.py @@ -0,0 +1,687 @@ +""" +Django代码生成器主入口类 + +该模块提供了一个统一的Django代码生成器入口, +整合了模型、序列化器、视图、URL路由等所有生成器功能。 + +使用示例: + generator = DjangoCodeGenerator() + + # 生成完整的CRUD模块 + generator.generate_full_module( + model_name='User', + fields=[ + {'name': 'username', 'type': 'CharField', 'max_length': 150}, + {'name': 'email', 'type': 'EmailField'}, + {'name': 'phone', 'type': 'CharField', 'max_length': 20} + ], + output_dir='./generated_code' + ) +""" + +import os +from typing import Dict, List, Optional, Any +from .base_generator import BaseGenerator +from .model_generator import ModelGenerator +from .serializer_generator import SerializerGenerator +from .view_generator import ViewGenerator +from .url_generator import URLGenerator +from .yaml_parser import YAMLParser + + +class DjangoCodeGenerator(BaseGenerator): + """Django代码生成器主入口类""" + + def __init__(self): + """初始化Django代码生成器""" + super().__init__() + self.model_generator = ModelGenerator() + self.serializer_generator = SerializerGenerator() + self.view_generator = ViewGenerator() + self.url_generator = URLGenerator() + self.yaml_parser = YAMLParser() + + def generate(self, **kwargs) -> str: + """ + 实现抽象方法generate + + Returns: + str: 生成的代码字符串 + """ + return "Django代码生成器" + + def generate_full_module( + self, + model_name: str, + fields: List[Dict[str, Any]], + output_dir: str = './generated_code', + app_name: str = None, + operations: List[str] = None, + permissions: List[str] = None, + validators: Optional[Dict[str, str]] = None, + table_name: str = None, + verbose_name: str = None, + ordering: List[str] = None + ) -> Dict[str, str]: + """ + 生成完整的Django模块代码(模型、序列化器、视图、URL) + + Args: + model_name: 模型名称 + fields: 字段配置列表 + output_dir: 输出目录 + app_name: 应用名称 + operations: 支持的操作列表 + permissions: 权限装饰器列表 + validators: 字段验证器映射 + table_name: 数据库表名 + verbose_name: 模型显示名称 + ordering: 默认排序字段 + + Returns: + Dict[str, str]: 生成的代码文件映射 + """ + if operations is None: + operations = ['create', 'read', 'update', 'delete', 'list'] + + if not app_name: + app_name = self.to_snake_case(model_name) + + generated_files = {} + + # 创建输出目录 + os.makedirs(output_dir, exist_ok=True) + + # 生成模型代码 + model_code = self.generate_model( + model_name=model_name, + fields=fields, + table_name=table_name, + verbose_name=verbose_name, + ordering=ordering + ) + generated_files['models.py'] = model_code + + # 生成序列化器代码 + serializer_code = self.generate_serializers( + model_name=model_name, + fields=[field['name'] for field in fields], + validators=validators + ) + generated_files['serializers.py'] = serializer_code + + # 生成视图代码 + view_code = self.generate_views( + model_name=model_name, + operations=operations, + permissions=permissions + ) + generated_files['views.py'] = view_code + + # 生成URL路由代码 + url_code = self.generate_urls( + model_name=model_name, + operations=operations, + app_name=app_name + ) + generated_files['urls.py'] = url_code + + # 写入文件 + for filename, code in generated_files.items(): + file_path = os.path.join(output_dir, filename) + self.write_to_file(file_path, code) + + return generated_files + + def generate_model( + self, + model_name: str, + fields: List[Dict[str, Any]], + table_name: str = None, + verbose_name: str = None, + ordering: List[str] = None, + status_choices: List[tuple] = None + ) -> str: + """ + 生成模型代码 + + Args: + model_name: 模型名称 + fields: 字段配置列表 + table_name: 数据库表名 + verbose_name: 模型显示名称 + ordering: 默认排序字段 + status_choices: 状态选择项 + + Returns: + str: 生成的模型代码 + """ + return self.model_generator.generate( + model_name=model_name, + fields=fields, + table_name=table_name, + verbose_name=verbose_name, + ordering=ordering, + status_choices=status_choices + ) + + def generate_serializers( + self, + model_name: str, + fields: List[str], + validators: Optional[Dict[str, str]] = None, + create_fields: Optional[List[str]] = None, + update_fields: Optional[List[str]] = None, + list_fields: Optional[List[str]] = None + ) -> str: + """ + 生成序列化器代码 + + Args: + model_name: 模型名称 + fields: 字段列表 + validators: 字段验证器映射 + create_fields: 创建时使用的字段 + update_fields: 更新时使用的字段 + list_fields: 列表时显示的字段 + + Returns: + str: 生成的序列化器代码 + """ + return self.serializer_generator.generate( + model_name=model_name, + fields=fields, + create_fields=create_fields, + update_fields=update_fields, + list_fields=list_fields, + validators=validators + ) + + def generate_views( + self, + model_name: str, + operations: List[str] = None, + permissions: List[str] = None, + filters: Optional[List[str]] = None, + ordering: Optional[List[str]] = None, + search_fields: Optional[List[str]] = None, + pagination: bool = True + ) -> str: + """ + 生成视图代码 + + Args: + model_name: 模型名称 + operations: 支持的操作列表 + permissions: 权限装饰器列表 + filters: 过滤字段列表 + ordering: 排序字段列表 + search_fields: 搜索字段列表 + pagination: 是否启用分页 + + Returns: + str: 生成的视图代码 + """ + if operations is None: + operations = ['create', 'read', 'update', 'delete', 'list'] + + return self.view_generator.generate( + model_name=model_name, + operations=operations, + permissions=permissions, + filters=filters, + ordering=ordering, + search_fields=search_fields, + pagination=pagination + ) + + def generate_urls( + self, + model_name: str, + operations: List[str] = None, + prefix: str = '', + app_name: str = '', + namespace: str = '' + ) -> str: + """ + 生成URL路由代码 + + Args: + model_name: 模型名称 + operations: 支持的操作列表 + prefix: URL前缀 + app_name: 应用名称 + namespace: 命名空间 + + Returns: + str: 生成的URL路由代码 + """ + if operations is None: + operations = ['create', 'read', 'update', 'delete', 'list'] + + return self.url_generator.generate( + model_name=model_name, + operations=operations, + prefix=prefix, + app_name=app_name, + namespace=namespace + ) + + def generate_api_module( + self, + model_name: str, + fields: List[Dict[str, Any]], + output_dir: str = './generated_api', + version: str = 'v1', + permissions: List[str] = None + ) -> Dict[str, str]: + """ + 生成RESTful API模块代码 + + Args: + model_name: 模型名称 + fields: 字段配置列表 + output_dir: 输出目录 + version: API版本 + permissions: 权限装饰器列表 + + Returns: + Dict[str, str]: 生成的代码文件映射 + """ + generated_files = {} + + # 创建输出目录 + os.makedirs(output_dir, exist_ok=True) + + # 生成模型代码 + model_code = self.generate_model( + model_name=model_name, + fields=fields + ) + generated_files['models.py'] = model_code + + # 生成序列化器代码 + serializer_code = self.generate_serializers( + model_name=model_name, + fields=[field['name'] for field in fields] + ) + generated_files['serializers.py'] = serializer_code + + # 生成API视图代码 + view_code = self.generate_views( + model_name=model_name, + permissions=permissions + ) + generated_files['views.py'] = view_code + + # 生成API URL路由代码 + url_code = self.url_generator.generate_api_urls( + model_name=model_name, + version=version + ) + generated_files['urls.py'] = url_code + + # 写入文件 + for filename, code in generated_files.items(): + file_path = os.path.join(output_dir, filename) + self.write_to_file(file_path, code) + + return generated_files + + def generate_nested_module( + self, + parent_model: str, + child_model: str, + parent_fields: List[Dict[str, Any]], + child_fields: List[Dict[str, Any]], + output_dir: str = './generated_nested', + relationship_type: str = 'foreign_key' + ) -> Dict[str, str]: + """ + 生成嵌套资源模块代码 + + Args: + parent_model: 父模型名称 + child_model: 子模型名称 + parent_fields: 父模型字段配置 + child_fields: 子模型字段配置 + output_dir: 输出目录 + relationship_type: 关系类型 ('foreign_key', 'one_to_one', 'many_to_many') + + Returns: + Dict[str, str]: 生成的代码文件映射 + """ + generated_files = {} + + # 创建输出目录 + os.makedirs(output_dir, exist_ok=True) + + # 在子模型字段中添加父模型关系 + if relationship_type == 'foreign_key': + child_fields.append({ + 'name': self.to_snake_case(parent_model), + 'type': 'ForeignKey', + 'to': parent_model, + 'on_delete': 'models.CASCADE', + 'verbose_name': f'{parent_model}', + 'help_text': f'关联的{parent_model}' + }) + + # 生成父模型代码 + parent_model_code = self.generate_model( + model_name=parent_model, + fields=parent_fields + ) + generated_files[f'{self.to_snake_case(parent_model)}_models.py'] = parent_model_code + + # 生成子模型代码 + child_model_code = self.generate_model( + model_name=child_model, + fields=child_fields + ) + generated_files[f'{self.to_snake_case(child_model)}_models.py'] = child_model_code + + # 生成嵌套URL路由 + nested_url_code = self.url_generator.generate_nested_urls( + parent_model=parent_model, + child_model=child_model + ) + generated_files['nested_urls.py'] = nested_url_code + + # 写入文件 + for filename, code in generated_files.items(): + file_path = os.path.join(output_dir, filename) + self.write_to_file(file_path, code) + + return generated_files + + def generate_app_structure( + self, + app_name: str, + models: List[Dict[str, Any]], + output_dir: str = None + ) -> Dict[str, str]: + """ + 生成完整的Django应用结构 + + Args: + app_name: 应用名称 + models: 模型配置列表 + output_dir: 输出目录 + + Returns: + Dict[str, str]: 生成的代码文件映射 + """ + if not output_dir: + output_dir = f'./generated_{app_name}' + + generated_files = {} + + # 创建应用目录结构 + app_dir = os.path.join(output_dir, app_name) + os.makedirs(app_dir, exist_ok=True) + os.makedirs(os.path.join(app_dir, 'serializers'), exist_ok=True) + os.makedirs(os.path.join(app_dir, 'views'), exist_ok=True) + os.makedirs(os.path.join(app_dir, 'urls'), exist_ok=True) + + # 生成应用配置文件 + apps_code = self._generate_apps_config(app_name) + generated_files['apps.py'] = apps_code + + # 生成__init__.py文件 + generated_files['__init__.py'] = '' + generated_files['serializers/__init__.py'] = '' + generated_files['views/__init__.py'] = '' + generated_files['urls/__init__.py'] = '' + + # 为每个模型生成代码 + all_models_code = [] + all_serializers_code = [] + all_views_code = [] + all_urls_code = [] + + for model_config in models: + model_name = model_config['name'] + fields = model_config['fields'] + + # 生成模型代码 + model_code = self.generate_model( + model_name=model_name, + fields=fields, + verbose_name=model_config.get('verbose_name', model_name), + table_name=model_config.get('table_name'), + ordering=model_config.get('ordering', ['-created_at']) + ) + all_models_code.append(model_code) + + # 生成序列化器代码 + serializer_code = self.generate_serializers( + model_name=model_name, + fields=[field['name'] for field in fields], + validators=model_config.get('validators', {}) + ) + all_serializers_code.append(serializer_code) + + # 生成视图代码 + view_code = self.generate_views( + model_name=model_name, + operations=model_config.get('operations', ['create', 'read', 'update', 'delete', 'list']), + permissions=model_config.get('permissions', []) + ) + all_views_code.append(view_code) + + # 生成URL代码 + url_code = self.generate_urls( + model_name=model_name, + app_name=app_name, + operations=model_config.get('operations', ['create', 'read', 'update', 'delete', 'list']) + ) + all_urls_code.append(url_code) + + # 合并所有代码 + generated_files['models.py'] = '\n\n'.join(all_models_code) + generated_files['serializers.py'] = '\n\n'.join(all_serializers_code) + generated_files['views.py'] = '\n\n'.join(all_views_code) + generated_files['urls.py'] = '\n\n'.join(all_urls_code) + + # 写入文件 + for filename, code in generated_files.items(): + file_path = os.path.join(app_dir, filename) + # 确保目录存在 + os.makedirs(os.path.dirname(file_path), exist_ok=True) + self.write_to_file(file_path, code) + + return generated_files + + def _generate_apps_config(self, app_name: str) -> str: + """生成Django应用配置代码""" + class_name = self.snake_to_camel(app_name) + 'Config' + + code_parts = [ + 'from django.apps import AppConfig', + '', + f'class {class_name}(AppConfig):', + ' """', + f' {app_name}应用配置', + ' """', + " default_auto_field = 'django.db.models.BigAutoField'", + f" name = '{app_name}'", + f" verbose_name = '{app_name.title()}'" + ] + + return '\n'.join(code_parts) + + def generate_from_yaml_file(self, yaml_file_path: str) -> Dict[str, str]: + """ + 从YAML配置文件生成Django应用代码 + + Args: + yaml_file_path: YAML配置文件路径 + + Returns: + Dict[str, str]: 生成的文件路径和内容映射 + + Raises: + FileNotFoundError: 当YAML文件不存在时 + ValueError: 当配置验证失败时 + """ + # 解析YAML配置 + config = self.yaml_parser.parse_yaml_file(yaml_file_path) + + # 从配置生成代码 + return self.generate_from_yaml_config(config) + + def generate_from_yaml_config(self, config: Dict[str, Any]) -> Dict[str, str]: + """ + 从YAML配置字典生成Django应用代码 + + Args: + config: YAML配置字典 + + Returns: + Dict[str, str]: 生成的文件路径和内容映射 + """ + app_name = config['app_name'] + models = config['models'] + output_dir = config.get('output_dir', '.') # 默认输出到当前目录 + + all_generated_files = {} + + # 为每个模型生成代码 + if isinstance(models, dict): + # 如果models是字典格式 + models_items = models.items() + else: + # 如果models是列表格式,转换为字典格式 + models_items = [(model['name'], model) for model in models] + + for model_name, model_config in models_items: + # 获取模型字段配置 + fields = model_config.get('fields', []) + operations = model_config.get('operations', ['create', 'get', 'update', 'delete', 'list']) + permissions = model_config.get('permissions', {}) + api_config = model_config.get('api', {}) + + # 转换字段格式 + processed_fields = {} + for field in fields: + field_name = field['name'] + processed_fields[field_name] = { + 'type': field['type'], + 'verbose_name': field.get('verbose_name', field_name), + 'help_text': field.get('help_text', f'{field_name}字段'), + } + + # 添加其他字段属性 + for attr in ['max_length', 'null', 'blank', 'default', 'unique', 'choices']: + if attr in field: + processed_fields[field_name][attr] = field[attr] + + # 创建应用目录 + app_dir = os.path.join(output_dir, app_name) + print(f"📁 创建应用目录: {app_dir}") + os.makedirs(app_dir, exist_ok=True) + + # 创建migrations子目录 + migrations_dir = os.path.join(app_dir, 'migrations') + print(f"📁 创建子目录: {migrations_dir}") + os.makedirs(migrations_dir, exist_ok=True) + # 创建migrations/__init__.py文件 + migrations_init_file = os.path.join(migrations_dir, '__init__.py') + with open(migrations_init_file, 'w', encoding='utf-8') as f: + f.write('') + all_generated_files[migrations_init_file] = '' + + # 生成模型代码 + print(f"📝 生成模型代码...") + model_code = self.model_generator.generate(model_name, processed_fields) + model_file = os.path.join(app_dir, 'models.py') + with open(model_file, 'w', encoding='utf-8') as f: + f.write(model_code) + all_generated_files[model_file] = model_code + print(f"✅ 已生成: {model_file}") + + # 生成序列化器代码 + print(f"📝 生成序列化器代码...") + serializer_code = self.serializer_generator.generate(model_name, processed_fields) + serializer_file = os.path.join(app_dir, 'serializers.py') + with open(serializer_file, 'w', encoding='utf-8') as f: + f.write(serializer_code) + all_generated_files[serializer_file] = serializer_code + print(f"✅ 已生成: {serializer_file}") + + # 生成视图代码 + print(f"📝 生成视图代码...") + view_code = self.view_generator.generate(model_name, operations) + view_file = os.path.join(app_dir, 'views.py') + with open(view_file, 'w', encoding='utf-8') as f: + f.write(view_code) + all_generated_files[view_file] = view_code + print(f"✅ 已生成: {view_file}") + + # 生成URL代码 + print(f"📝 生成URL代码...") + url_code = self.url_generator.generate(model_name, operations) + url_file = os.path.join(app_dir, 'urls.py') + with open(url_file, 'w', encoding='utf-8') as f: + f.write(url_code) + all_generated_files[url_file] = url_code + print(f"✅ 已生成: {url_file}") + + # 生成应用配置文件 + print(f"📝 生成应用配置文件...") + apps_code = self._generate_apps_config(app_name) + apps_file = os.path.join(app_dir, 'apps.py') + with open(apps_file, 'w', encoding='utf-8') as f: + f.write(apps_code) + all_generated_files[apps_file] = apps_code + print(f"✅ 已生成: {apps_file}") + + # 生成__init__.py文件 + init_file = os.path.join(app_dir, '__init__.py') + with open(init_file, 'w', encoding='utf-8') as f: + f.write('') + all_generated_files[init_file] = '' + print(f"✅ 已生成: {init_file}") + + # 生成migrations的__init__.py + migrations_init = os.path.join(app_dir, 'migrations', '__init__.py') + with open(migrations_init, 'w', encoding='utf-8') as f: + f.write('') + all_generated_files[migrations_init] = '' + print(f"✅ 已生成: {migrations_init}") + + print(f"\n🎉 Django应用 '{app_name}' 生成完成!") + print(f"📁 输出目录: {os.path.abspath(output_dir)}") + print(f"📄 生成文件数量: {len(all_generated_files)}") + + return all_generated_files + + def generate_yaml_template(self, output_path: str = 'app_template.yaml') -> str: + """ + 生成YAML配置文件模板 + + Args: + output_path: 输出文件路径 + + Returns: + str: 生成的模板内容 + """ + return self.yaml_parser.generate_yaml_template(output_path) + + def validate_yaml_config(self, yaml_file_path: str) -> bool: + """ + 验证YAML配置文件格式 + + Args: + yaml_file_path: YAML配置文件路径 + + Returns: + bool: 验证是否通过 + """ + try: + self.yaml_parser.parse_yaml_file(yaml_file_path) + return True + except Exception as e: + print(f"YAML配置验证失败: {e}") + return False \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/menu_generator.py b/hertz_studio_django_utils/code_generator/menu_generator.py new file mode 100644 index 0000000..b746ad3 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/menu_generator.py @@ -0,0 +1,434 @@ +#!/usr/bin/env python +""" +菜单权限生成器 +用于自动生成菜单配置和权限同步功能 +""" + +from typing import Dict, List, Optional, Any +import os +from pathlib import Path + + +class MenuGenerator: + """ + 菜单权限生成器 + + 用于根据模型和操作自动生成菜单配置,包括: + 1. 菜单项配置 + 2. 权限配置 + 3. 菜单层级结构 + """ + + def __init__(self): + """初始化菜单生成器""" + self.menu_types = { + 'directory': 1, # 目录 + 'menu': 2, # 菜单 + 'button': 3 # 按钮 + } + + # 标准操作映射 - 与视图生成器保持一致 + self.operation_mapping = { + 'list': {'name': '查询', 'permission': 'list'}, + 'create': {'name': '新增', 'permission': 'add'}, + 'retrieve': {'name': '详情', 'permission': 'list'}, # 统一使用list权限 + 'update': {'name': '修改', 'permission': 'edit'}, + 'delete': {'name': '删除', 'permission': 'remove'}, + 'export': {'name': '导出', 'permission': 'export'}, + 'import': {'name': '导入', 'permission': 'import'}, + } + + def generate_menu_config( + self, + module_name: str, + model_name: str, + operations: List[str], + parent_code: Optional[str] = None, + menu_prefix: str = 'system', + sort_order: int = 1, + icon: Optional[str] = None, + component_path: Optional[str] = None + ) -> List[Dict[str, Any]]: + """ + 生成菜单配置 + + Args: + module_name: 模块名称(中文) + model_name: 模型名称(英文,snake_case) + operations: 操作列表 + parent_code: 父级菜单代码 + menu_prefix: 菜单前缀 + sort_order: 排序 + icon: 图标 + component_path: 组件路径 + + Returns: + List[Dict]: 菜单配置列表 + """ + menus = [] + + # 生成主菜单 + main_menu_code = f"{menu_prefix}:{model_name}" + main_menu = { + 'menu_name': module_name, + 'menu_code': main_menu_code, + 'menu_type': self.menu_types['menu'], + 'path': f"/{menu_prefix}/{model_name}", + 'component': component_path or f"{menu_prefix}/{model_name}/index", + 'icon': icon or model_name, + 'permission': f"{main_menu_code}:list", + 'sort_order': sort_order, + 'parent_code': parent_code + } + menus.append(main_menu) + + # 生成操作按钮 + for i, operation in enumerate(operations, 1): + if operation in self.operation_mapping: + op_config = self.operation_mapping[operation] + button_menu = { + 'menu_name': f"{module_name}{op_config['name']}", + 'menu_code': f"{main_menu_code}:{op_config['permission']}", + 'menu_type': self.menu_types['button'], + 'permission': f"{main_menu_code}:{op_config['permission']}", + 'sort_order': i, + 'parent_code': main_menu_code + } + menus.append(button_menu) + + return menus + + def generate_directory_config( + self, + directory_name: str, + directory_code: str, + path: str, + icon: str = 'folder', + sort_order: int = 1 + ) -> Dict[str, Any]: + """ + 生成目录配置 + + Args: + directory_name: 目录名称 + directory_code: 目录代码 + path: 路径 + icon: 图标 + sort_order: 排序 + + Returns: + Dict: 目录配置 + """ + return { + 'menu_name': directory_name, + 'menu_code': directory_code, + 'menu_type': self.menu_types['directory'], + 'path': path, + 'icon': icon, + 'sort_order': sort_order, + 'parent_code': None + } + + def generate_custom_menu_config( + self, + menu_name: str, + menu_code: str, + operations: List[Dict[str, str]], + parent_code: Optional[str] = None, + path: Optional[str] = None, + component: Optional[str] = None, + icon: Optional[str] = None, + sort_order: int = 1 + ) -> List[Dict[str, Any]]: + """ + 生成自定义菜单配置 + + Args: + menu_name: 菜单名称 + menu_code: 菜单代码 + operations: 自定义操作列表 [{'name': '操作名', 'permission': '权限码'}] + parent_code: 父级菜单代码 + path: 路径 + component: 组件 + icon: 图标 + sort_order: 排序 + + Returns: + List[Dict]: 菜单配置列表 + """ + menus = [] + + # 生成主菜单 + main_menu = { + 'menu_name': menu_name, + 'menu_code': menu_code, + 'menu_type': self.menu_types['menu'], + 'sort_order': sort_order, + 'parent_code': parent_code + } + + if path: + main_menu['path'] = path + if component: + main_menu['component'] = component + if icon: + main_menu['icon'] = icon + if operations: + main_menu['permission'] = f"{menu_code}:{operations[0]['permission']}" + + menus.append(main_menu) + + # 生成操作按钮 + for i, operation in enumerate(operations, 1): + button_menu = { + 'menu_name': f"{menu_name}{operation['name']}", + 'menu_code': f"{menu_code}:{operation['permission']}", + 'menu_type': self.menu_types['button'], + 'permission': f"{menu_code}:{operation['permission']}", + 'sort_order': i, + 'parent_code': menu_code + } + menus.append(button_menu) + + return menus + + def update_menus_config_file( + self, + new_menus: List[Dict[str, Any]], + config_file_path: str = None + ) -> bool: + """ + 更新菜单配置文件 + + Args: + new_menus: 新的菜单配置列表 + config_file_path: 配置文件路径 + + Returns: + bool: 更新是否成功 + """ + if not config_file_path: + config_file_path = os.path.join( + Path(__file__).parent.parent, + 'config', + 'menus_config.py' + ) + + try: + # 读取现有配置 + with open(config_file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 查找现有菜单列表的结束位置 + import_end = content.find(']') + if import_end == -1: + return False + + # 生成新菜单配置代码 + new_menu_code = self._generate_menu_code(new_menus) + + # 在现有菜单列表末尾添加新菜单(在最后一个 ] 之前) + updated_content = ( + content[:import_end] + + ',\n\n # 新增菜单配置\n' + + new_menu_code + + content[import_end:] + ) + + # 写入更新后的配置 + with open(config_file_path, 'w', encoding='utf-8') as f: + f.write(updated_content) + + return True + + except Exception as e: + print(f"更新菜单配置文件失败: {e}") + return False + + def _generate_menu_code(self, menus: List[Dict[str, Any]]) -> str: + """ + 生成菜单配置代码 + + Args: + menus: 菜单配置列表 + + Returns: + str: 菜单配置代码 + """ + code_lines = [] + + for menu in menus: + code_lines.append(" {") + for key, value in menu.items(): + if isinstance(value, str): + code_lines.append(f" '{key}': '{value}',") + elif value is None: + code_lines.append(f" '{key}': None,") + else: + code_lines.append(f" '{key}': {value},") + code_lines.append(" }") + + return '\n'.join(code_lines) + + def generate_permission_sync_code( + self, + menu_configs: List[Dict[str, Any]] + ) -> str: + """ + 生成权限同步代码 + + Args: + menu_configs: 菜单配置列表 + + Returns: + str: 权限同步代码 + """ + sync_code = ''' +def sync_new_menus(): + """ + 同步新增菜单到数据库 + """ + from hertz_studio_django_auth.models import HertzMenu + from django.db import transaction + + new_menus = [ +''' + + # 添加菜单配置 + sync_code += self._generate_menu_code(menu_configs) + + sync_code += ''' + ] + + print("正在同步新增菜单...") + + with transaction.atomic(): + created_menus = {} + + # 按层级创建菜单 + for menu_data in new_menus: + parent_code = menu_data.pop('parent_code', None) + parent_id = None + + if parent_code and parent_code in created_menus: + parent_id = created_menus[parent_code] + + menu, created = HertzMenu.objects.get_or_create( + menu_code=menu_data['menu_code'], + defaults={ + **menu_data, + 'parent_id': parent_id + } + ) + + created_menus[menu.menu_code] = menu + + if created: + print(f"菜单创建成功: {menu.menu_name}") + else: + print(f"菜单已存在: {menu.menu_name}") + + print("菜单同步完成") +''' + + return sync_code + + def create_menu_sync_script( + self, + menu_configs: List[Dict[str, Any]], + script_path: str = None + ) -> bool: + """ + 创建菜单同步脚本 + + Args: + menu_configs: 菜单配置列表 + script_path: 脚本路径 + + Returns: + bool: 创建是否成功 + """ + if not script_path: + script_path = os.path.join( + Path(__file__).parent.parent.parent, + 'sync_menus.py' + ) + + try: + script_content = f'''#!/usr/bin/env python +""" +菜单同步脚本 +自动生成的菜单权限同步脚本 +""" + +import os +import sys +import django + +# 添加项目路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# 设置Django环境 +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings') +django.setup() + +{self.generate_permission_sync_code(menu_configs)} + +if __name__ == "__main__": + sync_new_menus() +''' + + with open(script_path, 'w', encoding='utf-8') as f: + f.write(script_content) + + return True + + except Exception as e: + print(f"创建菜单同步脚本失败: {e}") + return False + + +# 使用示例 +def example_usage(): + """使用示例""" + generator = MenuGenerator() + + # 生成标准CRUD菜单 + crud_menus = generator.generate_menu_config( + module_name="产品管理", + model_name="product", + operations=['list', 'create', 'update', 'delete'], + parent_code="system", + sort_order=5, + icon="product" + ) + + # 生成自定义菜单 + custom_menus = generator.generate_custom_menu_config( + menu_name="报表管理", + menu_code="system:report", + operations=[ + {'name': '查看', 'permission': 'view'}, + {'name': '导出', 'permission': 'export'}, + {'name': '打印', 'permission': 'print'} + ], + parent_code="system", + path="/system/report", + component="system/report/index", + icon="report", + sort_order=8 + ) + + # 更新配置文件 + all_menus = crud_menus + custom_menus + generator.update_menus_config_file(all_menus) + + # 创建同步脚本 + generator.create_menu_sync_script(all_menus) + + print("菜单配置生成完成") + + +if __name__ == "__main__": + example_usage() \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/model_generator.py b/hertz_studio_django_utils/code_generator/model_generator.py new file mode 100644 index 0000000..dba7965 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/model_generator.py @@ -0,0 +1,262 @@ +""" +Django模型代码生成器 + +该模块负责根据配置生成Django模型代码 +""" + +import os +from typing import Dict, List, Any, Optional +from .base_generator import BaseGenerator + + +class ModelGenerator(BaseGenerator): + """Django模型代码生成器""" + + def __init__(self): + """初始化模型生成器""" + super().__init__() + + def generate(self, model_name: str = None, fields: List[Dict[str, Any]] = None, **kwargs) -> str: + """ + 生成Django模型代码 + + Args: + model_name: 模型名称 + fields: 字段列表 + **kwargs: 其他参数 + + Returns: + str: 生成的模型代码 + """ + # 处理参数,支持位置参数和关键字参数 + if model_name is None: + model_name = kwargs.get('model_name', 'DefaultModel') + if fields is None: + fields = kwargs.get('fields', []) + + table_name = kwargs.get('table_name') + verbose_name = kwargs.get('verbose_name') + verbose_name_plural = kwargs.get('verbose_name_plural') + ordering = kwargs.get('ordering', ['-created_at']) + status_choices = kwargs.get('status_choices', [ + ('active', '激活'), + ('inactive', '未激活'), + ('deleted', '已删除') + ]) + + # 确保verbose_name不为None + if verbose_name is None: + verbose_name = model_name + + # 确保verbose_name_plural不为None + if verbose_name_plural is None: + verbose_name_plural = verbose_name + '列表' + + # 处理字段列表,确保每个字段都有必要的属性 + processed_fields = [] + for field in fields: + if isinstance(field, dict): + # 字段已经在YAML解析器中处理过类型转换,这里不再重复处理 + processed_fields.append(field) + elif isinstance(field, str): + # 如果字段是字符串,转换为字典格式 + processed_fields.append({ + 'name': field, + 'type': 'CharField', # 直接使用Django字段类型 + 'django_field_type': 'CharField', + 'max_length': 100, + 'verbose_name': field, + 'help_text': f'{field}字段' + }) + + # 添加默认的时间戳字段(如果不存在) + has_created_at = any(field.get('name') == 'created_at' for field in processed_fields) + has_updated_at = any(field.get('name') == 'updated_at' for field in processed_fields) + + if not has_created_at: + processed_fields.append({ + 'name': 'created_at', + 'type': 'DateTimeField', + 'auto_now_add': True, + 'verbose_name': '创建时间', + 'help_text': '记录创建时间' + }) + + if not has_updated_at: + processed_fields.append({ + 'name': 'updated_at', + 'type': 'DateTimeField', + 'auto_now': True, + 'verbose_name': '更新时间', + 'help_text': '记录最后更新时间' + }) + + # 准备模板上下文 + context = { + 'model_name': model_name, + 'fields': processed_fields, + 'table_name': table_name or self.camel_to_snake(model_name), + 'verbose_name': verbose_name, + 'verbose_name_plural': verbose_name_plural, + 'ordering': ordering, + 'status_choices': status_choices, + 'has_status_field': any(field.get('name') == 'status' for field in processed_fields) + } + + # 渲染模板 + return self.render_template('django/models.mako', context) + + def generate_model_with_relationships( + self, + model_name: str, + fields: List[Dict[str, Any]], + relationships: List[Dict[str, Any]] = None, + **kwargs + ) -> str: + """ + 生成包含关系字段的模型代码 + + Args: + model_name: 模型名称 + fields: 字段配置列表 + relationships: 关系字段配置列表 + + Returns: + str: 生成的模型代码 + """ + if relationships: + # 将关系字段添加到字段列表中 + for rel in relationships: + fields.append(rel) + + return self.generate( + model_name=model_name, + fields=fields, + **kwargs + ) + + def generate_abstract_model( + self, + model_name: str, + fields: List[Dict[str, Any]], + **kwargs + ) -> str: + """ + 生成抽象模型代码 + + Args: + model_name: 模型名称 + fields: 字段配置列表 + + Returns: + str: 生成的抽象模型代码 + """ + kwargs['abstract'] = True + return self.generate( + model_name=model_name, + fields=fields, + **kwargs + ) + + def generate_proxy_model( + self, + model_name: str, + base_model: str, + **kwargs + ) -> str: + """ + 生成代理模型代码 + + Args: + model_name: 模型名称 + base_model: 基础模型名称 + + Returns: + str: 生成的代理模型代码 + """ + kwargs['proxy'] = True + kwargs['base_model'] = base_model + return self.generate( + model_name=model_name, + fields=[], + **kwargs + ) + + def validate_field_config(self, field_config: Dict[str, Any]) -> bool: + """ + 验证字段配置 + + Args: + field_config: 字段配置字典 + + Returns: + bool: 验证是否通过 + """ + required_keys = ['name', 'type'] + for key in required_keys: + if key not in field_config: + return False + + # 验证字段类型 + valid_types = { + 'CharField', 'TextField', 'IntegerField', 'FloatField', + 'DecimalField', 'BooleanField', 'DateField', 'DateTimeField', + 'EmailField', 'URLField', 'ImageField', 'FileField', + 'ForeignKey', 'ManyToManyField', 'OneToOneField', 'JSONField', + 'SlugField', 'PositiveIntegerField', 'BigIntegerField', + 'SmallIntegerField', 'UUIDField', 'TimeField' + } + + if field_config['type'] not in valid_types: + return False + + return True + + def add_timestamp_fields(self, fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + 添加时间戳字段 + + Args: + fields: 原字段列表 + + Returns: + List[Dict[str, Any]]: 包含时间戳字段的字段列表 + """ + timestamp_fields = [ + { + 'name': 'created_at', + 'type': 'DateTimeField', + 'auto_now_add': True, + 'verbose_name': '创建时间', + 'help_text': '记录创建时间' + }, + { + 'name': 'updated_at', + 'type': 'DateTimeField', + 'auto_now': True, + 'verbose_name': '更新时间', + 'help_text': '记录最后更新时间' + } + ] + + return fields + timestamp_fields + + def add_status_field(self, fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + 添加状态字段 + + Args: + fields: 原字段列表 + + Returns: + List[Dict[str, Any]]: 包含状态字段的字段列表 + """ + status_field = { + 'name': 'status', + 'type': 'IntegerField', + 'choices': 'StatusChoices.choices', + 'default': 'StatusChoices.ENABLED', + 'verbose_name': '状态' + } + + return fields + [status_field] \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/serializer_generator.py b/hertz_studio_django_utils/code_generator/serializer_generator.py new file mode 100644 index 0000000..bc1e9b9 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/serializer_generator.py @@ -0,0 +1,336 @@ +""" +Django序列化器代码生成器 + +该模块负责根据配置生成Django REST Framework序列化器代码 +""" + +import os +from typing import Dict, List, Any, Optional +from .base_generator import BaseGenerator + + +class SerializerGenerator(BaseGenerator): + """Django序列化器代码生成器""" + + def __init__(self): + """初始化序列化器生成器""" + super().__init__() + + def generate(self, model_name: str = None, fields: List[Any] = None, **kwargs) -> str: + """ + 生成Django序列化器代码 + + Args: + model_name: 模型名称 + fields: 字段列表(可以是字符串列表或字典列表) + **kwargs: 其他参数 + + Returns: + str: 生成的序列化器代码 + """ + # 处理参数,支持位置参数和关键字参数 + if model_name is None: + model_name = kwargs.get('model_name', 'DefaultModel') + if fields is None: + fields = kwargs.get('fields', []) + + # 处理字段列表,统一转换为字典格式 + processed_fields = [] + + for field in fields: + if isinstance(field, str): + # 字符串字段转换为字典格式 + field_config = { + 'name': field, + 'type': 'CharField', + 'create': True, + 'update': True, + 'list': True, + 'required': True, + 'read_only': False + } + processed_fields.append(field_config) + elif isinstance(field, dict): + # 字典字段,确保有必要的属性 + field_config = { + 'name': field.get('name', 'unknown_field'), + 'type': field.get('type', 'CharField'), + 'create': field.get('create', True), + 'update': field.get('update', True), + 'list': field.get('list', True), + 'required': field.get('required', True), + 'read_only': field.get('read_only', False) + } + processed_fields.append(field_config) + + # 添加默认字段(如果不存在) + field_names = [f['name'] for f in processed_fields] + + if 'id' not in field_names: + processed_fields.insert(0, { + 'name': 'id', + 'type': 'IntegerField', + 'create': False, + 'update': False, + 'list': True, + 'required': False, + 'read_only': True + }) + + if 'created_at' not in field_names: + processed_fields.append({ + 'name': 'created_at', + 'type': 'DateTimeField', + 'create': False, + 'update': False, + 'list': True, + 'required': False, + 'read_only': True + }) + + if 'updated_at' not in field_names: + processed_fields.append({ + 'name': 'updated_at', + 'type': 'DateTimeField', + 'create': False, + 'update': False, + 'list': True, + 'required': False, + 'read_only': True + }) + + # 生成字段列表 + create_fields_list = [f['name'] for f in processed_fields if f.get('create', False)] + update_fields_list = [f['name'] for f in processed_fields if f.get('update', False)] + list_fields_list = [f['name'] for f in processed_fields if f.get('list', False)] + + # 准备模板上下文 + context = { + 'model_name': model_name, + 'model_name_lower': model_name.lower(), + 'fields': processed_fields, + 'create_fields_list': create_fields_list, + 'update_fields_list': update_fields_list, + 'list_fields_list': list_fields_list, + 'has_create_serializer': bool(create_fields_list), + 'has_update_serializer': bool(update_fields_list), + 'has_list_serializer': bool(list_fields_list) + } + + # 渲染模板 + return self.render_template('django/serializers.mako', context) + + def generate_crud_serializers( + self, + model_name: str, + fields: List[str], + create_fields: Optional[List[str]] = None, + update_fields: Optional[List[str]] = None, + list_fields: Optional[List[str]] = None, + validators: Optional[Dict[str, str]] = None + ) -> str: + """ + 生成CRUD序列化器代码 + + Args: + model_name: 模型名称 + fields: 字段列表 + create_fields: 创建时使用的字段 + update_fields: 更新时使用的字段 + list_fields: 列表时显示的字段 + validators: 字段验证器映射 + + Returns: + str: 生成的序列化器代码 + """ + # 默认字段配置 + if create_fields is None: + create_fields = [f for f in fields if f not in ['id', 'created_at', 'updated_at']] + if update_fields is None: + update_fields = [f for f in fields if f not in ['id', 'created_at', 'updated_at']] + if list_fields is None: + list_fields = fields + + # 构建字段配置 + field_configs = [] + for field_name in fields: + field_config = { + 'name': field_name, + 'create': field_name in create_fields, + 'update': field_name in update_fields, + 'list': field_name in list_fields, + 'validators': validators.get(field_name, {}) if validators else {} + } + field_configs.append(field_config) + + return self.generate( + model_name=model_name, + fields=field_configs + ) + + def generate_nested_serializer( + self, + model_name: str, + fields: List[str], + nested_fields: Dict[str, Dict[str, Any]] + ) -> str: + """ + 生成嵌套序列化器代码 + + Args: + model_name: 模型名称 + fields: 字段列表 + nested_fields: 嵌套字段配置 + + Returns: + str: 生成的嵌套序列化器代码 + """ + # 构建字段配置,包含嵌套字段信息 + field_configs = [] + for field_name in fields: + field_config = { + 'name': field_name, + 'nested': field_name in nested_fields, + 'nested_config': nested_fields.get(field_name, {}) + } + field_configs.append(field_config) + + return self.generate( + model_name=model_name, + fields=field_configs, + has_nested=True + ) + + def generate_read_only_serializer( + self, + model_name: str, + fields: List[str] + ) -> str: + """ + 生成只读序列化器代码 + + Args: + model_name: 模型名称 + fields: 字段列表 + + Returns: + str: 生成的只读序列化器代码 + """ + field_configs = [] + for field_name in fields: + field_config = { + 'name': field_name, + 'read_only': True + } + field_configs.append(field_config) + + return self.generate( + model_name=model_name, + fields=field_configs, + read_only=True + ) + + def generate_create_serializer( + self, + model_name: str, + fields: List[str], + validators: Optional[Dict[str, str]] = None + ) -> str: + """ + 生成创建序列化器代码 + + Args: + model_name: 模型名称 + fields: 字段列表 + validators: 字段验证器映射 + + Returns: + str: 生成的创建序列化器代码 + """ + field_configs = [] + for field_name in fields: + field_config = { + 'name': field_name, + 'validators': validators.get(field_name, {}) if validators else {} + } + field_configs.append(field_config) + + return self.generate( + model_name=model_name, + fields=field_configs, + serializer_type='create' + ) + + def generate_update_serializer( + self, + model_name: str, + fields: List[str], + validators: Optional[Dict[str, str]] = None + ) -> str: + """ + 生成更新序列化器代码 + + Args: + model_name: 模型名称 + fields: 字段列表 + validators: 字段验证器映射 + + Returns: + str: 生成的更新序列化器代码 + """ + field_configs = [] + for field_name in fields: + field_config = { + 'name': field_name, + 'validators': validators.get(field_name, {}) if validators else {} + } + field_configs.append(field_config) + + return self.generate( + model_name=model_name, + fields=field_configs, + serializer_type='update' + ) + + def add_custom_validation( + self, + serializer_code: str, + field_name: str, + validation_code: str + ) -> str: + """ + 添加自定义验证方法 + + Args: + serializer_code: 原序列化器代码 + field_name: 字段名称 + validation_code: 验证代码 + + Returns: + str: 包含自定义验证的序列化器代码 + """ + validation_method = f""" + def validate_{field_name}(self, value): + \"\"\" + 验证{field_name}字段 + \"\"\" + {validation_code} + return value +""" + + # 在类定义结束前插入验证方法 + lines = serializer_code.split('\n') + insert_index = -1 + for i, line in enumerate(lines): + if line.strip().startswith('class ') and 'Serializer' in line: + # 找到类定义结束的位置 + for j in range(i + 1, len(lines)): + if lines[j].strip() and not lines[j].startswith(' '): + insert_index = j + break + break + + if insert_index > 0: + lines.insert(insert_index, validation_method) + + return '\n'.join(lines) \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/template_engine.py b/hertz_studio_django_utils/code_generator/template_engine.py new file mode 100644 index 0000000..f6bf33e --- /dev/null +++ b/hertz_studio_django_utils/code_generator/template_engine.py @@ -0,0 +1,248 @@ +""" +模板引擎类 + +基于Mako模板引擎的代码生成模板管理器 +""" + +import os +from typing import Dict, Any, Optional +from mako.template import Template +from mako.lookup import TemplateLookup +from mako.exceptions import TemplateLookupException, CompileException + + +class TemplateEngine: + """ + 模板引擎类 + + 负责管理和渲染Mako模板 + """ + + def __init__(self, template_dir: str = None): + """ + 初始化模板引擎 + + Args: + template_dir: 模板目录路径 + """ + if template_dir is None: + # 默认模板目录 + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_dir = os.path.join(current_dir, 'templates') + + self.template_dir = template_dir + self.lookup = TemplateLookup( + directories=[template_dir], + module_directory=os.path.join(template_dir, '.mako_modules'), + input_encoding='utf-8', + output_encoding='utf-8', + encoding_errors='replace' + ) + + def render_template(self, template_name: str, context: Dict[str, Any]) -> str: + """ + 渲染模板 + + Args: + template_name: 模板文件名 + context: 模板上下文变量 + + Returns: + str: 渲染后的代码字符串 + + Raises: + TemplateLookupException: 模板不存在 + CompileException: 模板编译错误 + """ + try: + template = self.lookup.get_template(template_name) + rendered = template.render(**context) + # 确保返回字符串而不是bytes + if isinstance(rendered, bytes): + return rendered.decode('utf-8') + return rendered + except TemplateLookupException as e: + raise TemplateLookupException(f"模板 '{template_name}' 不存在: {str(e)}") + except CompileException as e: + raise CompileException(f"模板 '{template_name}' 编译错误: {str(e)}") + except Exception as e: + raise Exception(f"渲染模板 '{template_name}' 时发生错误: {str(e)}") + + def render_django_model(self, context: Dict[str, Any]) -> str: + """ + 渲染Django模型模板 + + Args: + context: 模板上下文 + + Returns: + str: 生成的模型代码 + """ + return self.render_template('django/models.mako', context) + + def render_django_serializer(self, context: Dict[str, Any]) -> str: + """ + 渲染Django序列化器模板 + + Args: + context: 模板上下文 + + Returns: + str: 生成的序列化器代码 + """ + return self.render_template('django/serializers.mako', context) + + def render_django_view(self, context: Dict[str, Any]) -> str: + """ + 渲染Django视图模板 + + Args: + context: 模板上下文 + + Returns: + str: 生成的视图代码 + """ + return self.render_template('django/views.mako', context) + + def render_django_url(self, context: Dict[str, Any]) -> str: + """ + 渲染Django URL配置模板 + + Args: + context: 模板上下文 + + Returns: + str: 生成的URL配置代码 + """ + return self.render_template('django/urls.mako', context) + + def render_django_app(self, context: Dict[str, Any]) -> str: + """ + 渲染Django应用配置模板 + + Args: + context: 模板上下文 + + Returns: + str: 生成的应用配置代码 + """ + return self.render_template('django/apps.mako', context) + + def get_available_templates(self) -> list: + """ + 获取可用的模板列表 + + Returns: + list: 模板文件列表 + """ + templates = [] + for root, dirs, files in os.walk(self.template_dir): + for file in files: + if file.endswith('.mako'): + rel_path = os.path.relpath(os.path.join(root, file), self.template_dir) + templates.append(rel_path.replace('\\', '/')) + return templates + + def template_exists(self, template_name: str) -> bool: + """ + 检查模板是否存在 + + Args: + template_name: 模板名称 + + Returns: + bool: 模板是否存在 + """ + try: + self.lookup.get_template(template_name) + return True + except TemplateLookupException: + return False + + def validate_template(self, template_name: str) -> tuple: + """ + 验证模板语法 + + Args: + template_name: 模板名称 + + Returns: + tuple: (是否有效, 错误信息) + """ + try: + template = self.lookup.get_template(template_name) + # 尝试编译模板 + template.code + return True, None + except CompileException as e: + return False, f"模板编译错误: {str(e)}" + except TemplateLookupException as e: + return False, f"模板不存在: {str(e)}" + except Exception as e: + return False, f"未知错误: {str(e)}" + + def create_context(self, **kwargs) -> Dict[str, Any]: + """ + 创建模板上下文 + + Args: + **kwargs: 上下文变量 + + Returns: + Dict[str, Any]: 模板上下文 + """ + return kwargs + + def add_template_directory(self, directory: str) -> None: + """ + 添加模板目录 + + Args: + directory: 模板目录路径 + """ + if os.path.exists(directory): + self.lookup.directories.append(directory) + else: + raise FileNotFoundError(f"模板目录不存在: {directory}") + + def clear_cache(self) -> None: + """ + 清除模板缓存 + """ + self.lookup._collection.clear() + + def get_template_info(self, template_name: str) -> Dict[str, Any]: + """ + 获取模板信息 + + Args: + template_name: 模板名称 + + Returns: + Dict[str, Any]: 模板信息 + """ + try: + template = self.lookup.get_template(template_name) + template_path = template.filename + + info = { + 'name': template_name, + 'path': template_path, + 'exists': True, + 'size': os.path.getsize(template_path) if template_path else 0, + 'modified_time': os.path.getmtime(template_path) if template_path else None + } + + # 验证模板 + is_valid, error = self.validate_template(template_name) + info['valid'] = is_valid + info['error'] = error + + return info + except TemplateLookupException: + return { + 'name': template_name, + 'exists': False, + 'valid': False, + 'error': '模板不存在' + } \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/models.mako.py b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/models.mako.py new file mode 100644 index 0000000..10688af --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/models.mako.py @@ -0,0 +1,225 @@ +# -*- coding:utf-8 -*- +from mako import runtime, filters, cache +UNDEFINED = runtime.UNDEFINED +STOP_RENDERING = runtime.STOP_RENDERING +__M_dict_builtin = dict +__M_locals_builtin = locals +_magic_number = 10 +_modified_time = 1760426258.0375054 +_enable_loop = True +_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/models.mako' +_template_uri = 'django/models.mako' +_source_encoding = 'utf-8' +_exports = [] + + + +from datetime import datetime + + +def render_body(context,**pageargs): + __M_caller = context.caller_stack._push_frame() + try: + __M_locals = __M_dict_builtin(pageargs=pageargs) + any = context.get('any', UNDEFINED) + chr = context.get('chr', UNDEFINED) + ordering = context.get('ordering', UNDEFINED) + fields = context.get('fields', UNDEFINED) + verbose_name = context.get('verbose_name', UNDEFINED) + model_name = context.get('model_name', UNDEFINED) + str = context.get('str', UNDEFINED) + table_name = context.get('table_name', UNDEFINED) + status_choices = context.get('status_choices', UNDEFINED) + tuple = context.get('tuple', UNDEFINED) + isinstance = context.get('isinstance', UNDEFINED) + len = context.get('len', UNDEFINED) + __M_writer = context.writer() + __M_writer('"""\r\nDjango模型模板\r\n"""\r\n') + __M_writer('\r\n') + +# 导入必要的模块 + imports = [] + if any(field.get('type') == 'ForeignKey' for field in fields): + imports.append('from django.contrib.auth import get_user_model') + if any(field.get('type') in ['DateTimeField', 'DateField', 'TimeField'] for field in fields): + imports.append('from django.utils import timezone') + if any(field.get('choices') for field in fields): + imports.append('from django.core.validators import validate_email') + + # 生成状态选择项 + status_choices_code = "" + if status_choices: + choices_list = [] + for choice in status_choices: + if isinstance(choice, tuple) and len(choice) == 2: + choices_list.append(f" ('{choice[0]}', '{choice[1]}')") + else: + choices_list.append(f" ('{choice}', '{choice}')") + status_choices_code = f""" STATUS_CHOICES = [ +{',{}'.format(chr(10)).join(choices_list)}, + ]""" + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['field','choice','choices_list','imports','status_choices_code'] if __M_key in __M_locals_builtin_stored])) + __M_writer('from django.db import models\r\n') + for import_line in imports: + __M_writer(str(import_line)) + __M_writer('\r\n') + __M_writer('\r\nclass ') + __M_writer(str(model_name)) + __M_writer('(models.Model):\r\n """\r\n ') + __M_writer(str(verbose_name or model_name)) + __M_writer('模型\r\n') + if table_name: + __M_writer(' \r\n 数据表名: ') + __M_writer(str(table_name)) + __M_writer('\r\n') + __M_writer(' 创建时间: ') + __M_writer(str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + __M_writer('\r\n """\r\n') + if status_choices_code: + __M_writer(str(status_choices_code)) + __M_writer('\r\n\r\n') + for field in fields: + + field_name = field['name'] + # 使用转换后的Django字段类型 + field_type = field.get('type', 'CharField') + field_options = [] + + # 处理字段选项 + if field.get('verbose_name'): + field_options.append(f"verbose_name='{field['verbose_name']}'") + if field.get('help_text'): + field_options.append(f"help_text='{field['help_text']}'") + if field.get('max_length'): + field_options.append(f"max_length={field['max_length']}") + if field.get('null'): + field_options.append(f"null={field['null']}") + if field.get('blank'): + field_options.append(f"blank={field['blank']}") + if field.get('default') is not None: + if isinstance(field['default'], str): + field_options.append(f"default='{field['default']}'") + else: + field_options.append(f"default={field['default']}") + if field.get('unique'): + field_options.append(f"unique={field['unique']}") + if field.get('db_index'): + field_options.append(f"db_index={field['db_index']}") + + # 处理特殊字段类型 + if field_type == 'ForeignKey': + if field.get('to'): + field_options.insert(0, f"'{field['to']}'") + else: + field_options.insert(0, "get_user_model()") + if field.get('on_delete'): + field_options.append(f"on_delete=models.{field['on_delete']}") + else: + field_options.append("on_delete=models.CASCADE") + elif field_type == 'ManyToManyField': + if field.get('to'): + field_options.insert(0, f"'{field['to']}'") + elif field_type == 'OneToOneField': + if field.get('to'): + field_options.insert(0, f"'{field['to']}'") + if field.get('on_delete'): + field_options.append(f"on_delete=models.{field['on_delete']}") + else: + field_options.append("on_delete=models.CASCADE") + + # 处理选择项 + if field.get('choices'): + if field_name == 'status' and status_choices: + field_options.append("choices=STATUS_CHOICES") + else: + choices_list = [] + for choice in field['choices']: + if isinstance(choice, tuple) and len(choice) == 2: + choices_list.append(f"('{choice[0]}', '{choice[1]}')") + else: + choices_list.append(f"('{choice}', '{choice}')") + field_options.append(f"choices=[{', '.join(choices_list)}]") + + # 处理特殊字段类型的额外参数 + if field_type == 'DecimalField': + if not any('max_digits' in opt for opt in field_options): + field_options.append('max_digits=10') + if not any('decimal_places' in opt for opt in field_options): + field_options.append('decimal_places=2') + elif field_type == 'DateTimeField': + if field_name == 'created_at' and not any('auto_now_add' in opt for opt in field_options): + field_options.append('auto_now_add=True') + elif field_name == 'updated_at' and not any('auto_now' in opt for opt in field_options): + field_options.append('auto_now=True') + + options_str = ', '.join(field_options) if field_options else '' + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['field_options','options_str','choice','choices_list','opt','field_name','field_type'] if __M_key in __M_locals_builtin_stored])) + __M_writer('\r\n ') + __M_writer(str(field_name)) + __M_writer(' = models.') + __M_writer(str(field_type)) + __M_writer('(') + __M_writer(str(options_str)) + __M_writer(')\r\n') + __M_writer('\r\n class Meta:\r\n') + if table_name: + __M_writer(" db_table = '") + __M_writer(str(table_name)) + __M_writer("'\r\n") + __M_writer(" verbose_name = '") + __M_writer(str(verbose_name or model_name)) + __M_writer("'\r\n verbose_name_plural = '") + __M_writer(str(verbose_name or model_name)) + __M_writer("'\r\n") + if ordering: + __M_writer(' ordering = [') + __M_writer(str(', '.join([f"'{field}'" for field in ordering]))) + __M_writer(']\r\n') + else: + __M_writer(" ordering = ['-created_at']\r\n") + __M_writer('\r\n def __str__(self):\r\n """字符串表示"""\r\n') + if fields: + __M_writer(' ') + + # 寻找合适的字段作为字符串表示 + str_field = None + for field in fields: + if field['name'] in ['name', 'title', 'username', 'email']: + str_field = field['name'] + break + if not str_field: + # 如果没有找到合适的字段,使用第一个字符串字段 + for field in fields: + if field['type'] in ['CharField', 'TextField', 'EmailField']: + str_field = field['name'] + break + if not str_field: + str_field = 'id' + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['field','str_field'] if __M_key in __M_locals_builtin_stored])) + __M_writer('\r\n return str(self.') + __M_writer(str(str_field)) + __M_writer(')\r\n') + else: + __M_writer(' return f"') + __M_writer(str(model_name)) + __M_writer('({self.id})"\r\n') + __M_writer('\r\n def save(self, *args, **kwargs):\r\n """保存方法"""\r\n super().save(*args, **kwargs)\r\n\r\n @classmethod\r\n def get_by_id(cls, obj_id):\r\n """根据ID获取对象"""\r\n try:\r\n return cls.objects.get(id=obj_id)\r\n except cls.DoesNotExist:\r\n return None') + return '' + finally: + context.caller_stack._pop_frame() + + +""" +__M_BEGIN_METADATA +{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/models.mako", "uri": "django/models.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "37": 1, "38": 6, "39": 7, "40": 8, "41": 9, "42": 10, "43": 11, "44": 12, "45": 13, "46": 14, "47": 15, "48": 16, "49": 17, "50": 18, "51": 19, "52": 20, "53": 21, "54": 22, "55": 23, "56": 24, "57": 25, "58": 26, "59": 27, "60": 28, "61": 29, "62": 30, "65": 30, "66": 31, "67": 32, "68": 32, "69": 34, "70": 35, "71": 35, "72": 37, "73": 37, "74": 38, "75": 39, "76": 40, "77": 40, "78": 42, "79": 42, "80": 42, "81": 44, "82": 45, "83": 45, "84": 48, "85": 49, "86": 50, "87": 51, "88": 52, "89": 53, "90": 54, "91": 55, "92": 56, "93": 57, "94": 58, "95": 59, "96": 60, "97": 61, "98": 62, "99": 63, "100": 64, "101": 65, "102": 66, "103": 67, "104": 68, "105": 69, "106": 70, "107": 71, "108": 72, "109": 73, "110": 74, "111": 75, "112": 76, "113": 77, "114": 78, "115": 79, "116": 80, "117": 81, "118": 82, "119": 83, "120": 84, "121": 85, "122": 86, "123": 87, "124": 88, "125": 89, "126": 90, "127": 91, "128": 92, "129": 93, "130": 94, "131": 95, "132": 96, "133": 97, "134": 98, "135": 99, "136": 100, "137": 101, "138": 102, "139": 103, "140": 104, "141": 105, "142": 106, "143": 107, "144": 108, "145": 109, "146": 110, "147": 111, "148": 112, "149": 113, "150": 114, "151": 115, "152": 116, "153": 117, "154": 118, "155": 119, "156": 120, "157": 121, "158": 122, "159": 123, "160": 124, "163": 123, "164": 124, "165": 124, "166": 124, "167": 124, "168": 124, "169": 124, "170": 126, "171": 128, "172": 129, "173": 129, "174": 129, "175": 131, "176": 131, "177": 131, "178": 132, "179": 132, "180": 133, "181": 134, "182": 134, "183": 134, "184": 135, "185": 136, "186": 138, "187": 141, "188": 142, "189": 142, "190": 143, "191": 144, "192": 145, "193": 146, "194": 147, "195": 148, "196": 149, "197": 150, "198": 151, "199": 152, "200": 153, "201": 154, "202": 155, "203": 156, "204": 157, "205": 158, "208": 157, "209": 158, "210": 158, "211": 159, "212": 160, "213": 160, "214": 160, "215": 162, "221": 215}} +__M_END_METADATA +""" diff --git a/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/serializers.mako.py b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/serializers.mako.py new file mode 100644 index 0000000..38e5534 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/serializers.mako.py @@ -0,0 +1,173 @@ +# -*- coding:utf-8 -*- +from mako import runtime, filters, cache +UNDEFINED = runtime.UNDEFINED +STOP_RENDERING = runtime.STOP_RENDERING +__M_dict_builtin = dict +__M_locals_builtin = locals +_magic_number = 10 +_modified_time = 1760424185.601884 +_enable_loop = True +_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/serializers.mako' +_template_uri = 'django/serializers.mako' +_source_encoding = 'utf-8' +_exports = [] + + + +from datetime import datetime + + +def render_body(context,**pageargs): + __M_caller = context.caller_stack._push_frame() + try: + __M_locals = __M_dict_builtin(pageargs=pageargs) + model_name = context.get('model_name', UNDEFINED) + fields = context.get('fields', UNDEFINED) + __M_writer = context.writer() + __M_writer('"""\nDjango序列化器模板\n"""\n') + __M_writer('\n') + +# 生成字段列表 + all_fields = [] + create_fields_list = [] + update_fields_list = [] + list_fields_list = [] + + # 处理字段列表 + for field in fields: + field_name = field['name'] + all_fields.append(field_name) + + # 排除自动生成的字段 + if field_name not in ['id', 'created_at', 'updated_at']: + create_fields_list.append(field_name) + update_fields_list.append(field_name) + + list_fields_list.append(field_name) + + # 添加默认字段 + if 'id' not in all_fields: + all_fields.insert(0, 'id') + if 'created_at' not in all_fields: + all_fields.append('created_at') + if 'updated_at' not in all_fields: + all_fields.append('updated_at') + + # 如果没有指定列表字段,使用所有字段 + if not list_fields_list: + list_fields_list = all_fields + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['all_fields','field_name','update_fields_list','field','create_fields_list','list_fields_list'] if __M_key in __M_locals_builtin_stored])) + __M_writer('\nfrom rest_framework import serializers\nfrom .models import ') + __M_writer(str(model_name)) + __M_writer('\n\n\nclass ') + __M_writer(str(model_name)) + __M_writer('Serializer(serializers.ModelSerializer):\n """\n ') + __M_writer(str(model_name)) + __M_writer('序列化器\n \n 用于') + __M_writer(str(model_name)) + __M_writer('模型的序列化和反序列化\n 创建时间: ') + __M_writer(str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + __M_writer('\n """\n \n class Meta:\n model = ') + __M_writer(str(model_name)) + __M_writer('\n fields = [') + __M_writer(str(', '.join([f"'{field}'" for field in all_fields]))) + __M_writer("]\n read_only_fields = ['id', 'created_at', 'updated_at']\n \n") + for field in fields: + pass + if field.get('validators'): + __M_writer(' def validate_') + __M_writer(str(field['name'])) + __M_writer('(self, value):\n """\n 验证') + __M_writer(str(field['name'])) + __M_writer('字段\n """\n') + for validator_name, validator_rule in field['validators'].items(): + __M_writer(' # ') + __M_writer(str(validator_name)) + __M_writer('验证: ') + __M_writer(str(validator_rule)) + __M_writer('\n') + __M_writer(' return value\n \n') + __M_writer(' def validate(self, attrs):\n """\n 对象级别的验证\n """\n # 在这里添加跨字段验证逻辑\n return attrs\n \n def create(self, validated_data):\n """\n 创建') + __M_writer(str(model_name)) + __M_writer('实例\n """\n return ') + __M_writer(str(model_name)) + __M_writer('.objects.create(**validated_data)\n \n def update(self, instance, validated_data):\n """\n 更新') + __M_writer(str(model_name)) + __M_writer('实例\n """\n for attr, value in validated_data.items():\n setattr(instance, attr, value)\n instance.save()\n return instance\n\n\nclass ') + __M_writer(str(model_name)) + __M_writer('CreateSerializer(serializers.ModelSerializer):\n """\n ') + __M_writer(str(model_name)) + __M_writer('创建序列化器\n \n 用于创建') + __M_writer(str(model_name)) + __M_writer('实例\n """\n \n class Meta:\n model = ') + __M_writer(str(model_name)) + __M_writer('\n fields = [') + __M_writer(str(', '.join([f"'{field}'" for field in create_fields_list]))) + __M_writer(']\n \n') + for field in fields: + pass + if field['name'] in create_fields_list and field.get('validators'): + __M_writer(' def validate_') + __M_writer(str(field['name'])) + __M_writer('(self, value):\n """\n 验证') + __M_writer(str(field['name'])) + __M_writer('字段\n """\n') + for validator_name, validator_rule in field['validators'].items(): + __M_writer(' # ') + __M_writer(str(validator_name)) + __M_writer('验证: ') + __M_writer(str(validator_rule)) + __M_writer('\n') + __M_writer(' return value\n \n') + __M_writer('\n\nclass ') + __M_writer(str(model_name)) + __M_writer('UpdateSerializer(serializers.ModelSerializer):\n """\n ') + __M_writer(str(model_name)) + __M_writer('更新序列化器\n \n 用于更新') + __M_writer(str(model_name)) + __M_writer('实例\n """\n \n class Meta:\n model = ') + __M_writer(str(model_name)) + __M_writer('\n fields = [') + __M_writer(str(', '.join([f"'{field}'" for field in update_fields_list]))) + __M_writer(']\n \n') + for field in fields: + pass + if field['name'] in update_fields_list and field.get('validators'): + __M_writer(' def validate_') + __M_writer(str(field['name'])) + __M_writer('(self, value):\n """\n 验证') + __M_writer(str(field['name'])) + __M_writer('字段\n """\n') + for validator_name, validator_rule in field['validators'].items(): + __M_writer(' # ') + __M_writer(str(validator_name)) + __M_writer('验证: ') + __M_writer(str(validator_rule)) + __M_writer('\n') + __M_writer(' return value\n \n') + __M_writer('\n\nclass ') + __M_writer(str(model_name)) + __M_writer('ListSerializer(serializers.ModelSerializer):\n """\n ') + __M_writer(str(model_name)) + __M_writer('列表序列化器\n \n 用于列表显示') + __M_writer(str(model_name)) + __M_writer('实例\n """\n \n class Meta:\n model = ') + __M_writer(str(model_name)) + __M_writer('\n fields = [') + __M_writer(str(', '.join([f"'{field}'" for field in list_fields_list]))) + __M_writer(']\n read_only_fields = [') + __M_writer(str(', '.join([f"'{field}'" for field in list_fields_list]))) + __M_writer(']') + return '' + finally: + context.caller_stack._pop_frame() + + +""" +__M_BEGIN_METADATA +{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/serializers.mako", "uri": "django/serializers.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "27": 1, "28": 6, "29": 7, "30": 8, "31": 9, "32": 10, "33": 11, "34": 12, "35": 13, "36": 14, "37": 15, "38": 16, "39": 17, "40": 18, "41": 19, "42": 20, "43": 21, "44": 22, "45": 23, "46": 24, "47": 25, "48": 26, "49": 27, "50": 28, "51": 29, "52": 30, "53": 31, "54": 32, "55": 33, "56": 34, "57": 35, "58": 36, "59": 37, "60": 38, "63": 37, "64": 39, "65": 39, "66": 42, "67": 42, "68": 44, "69": 44, "70": 46, "71": 46, "72": 47, "73": 47, "74": 51, "75": 51, "76": 52, "77": 52, "78": 55, "80": 56, "81": 57, "82": 57, "83": 57, "84": 59, "85": 59, "86": 61, "87": 62, "88": 62, "89": 62, "90": 62, "91": 62, "92": 64, "93": 68, "94": 77, "95": 77, "96": 79, "97": 79, "98": 83, "99": 83, "100": 91, "101": 91, "102": 93, "103": 93, "104": 95, "105": 95, "106": 99, "107": 99, "108": 100, "109": 100, "110": 102, "112": 103, "113": 104, "114": 104, "115": 104, "116": 106, "117": 106, "118": 108, "119": 109, "120": 109, "121": 109, "122": 109, "123": 109, "124": 111, "125": 115, "126": 117, "127": 117, "128": 119, "129": 119, "130": 121, "131": 121, "132": 125, "133": 125, "134": 126, "135": 126, "136": 128, "138": 129, "139": 130, "140": 130, "141": 130, "142": 132, "143": 132, "144": 134, "145": 135, "146": 135, "147": 135, "148": 135, "149": 135, "150": 137, "151": 141, "152": 143, "153": 143, "154": 145, "155": 145, "156": 147, "157": 147, "158": 151, "159": 151, "160": 152, "161": 152, "162": 153, "163": 153, "169": 163}} +__M_END_METADATA +""" diff --git a/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/urls.mako.py b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/urls.mako.py new file mode 100644 index 0000000..d0386e5 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/urls.mako.py @@ -0,0 +1,129 @@ +# -*- coding:utf-8 -*- +from mako import runtime, filters, cache +UNDEFINED = runtime.UNDEFINED +STOP_RENDERING = runtime.STOP_RENDERING +__M_dict_builtin = dict +__M_locals_builtin = locals +_magic_number = 10 +_modified_time = 1760498624.445131 +_enable_loop = True +_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/urls.mako' +_template_uri = 'django/urls.mako' +_source_encoding = 'utf-8' +_exports = [] + + + +from datetime import datetime + + +def render_body(context,**pageargs): + __M_caller = context.caller_stack._push_frame() + try: + __M_locals = __M_dict_builtin(pageargs=pageargs) + app_name = context.get('app_name', UNDEFINED) + operations = context.get('operations', UNDEFINED) + model_name = context.get('model_name', UNDEFINED) + prefix = context.get('prefix', UNDEFINED) + __M_writer = context.writer() + __M_writer('"""\nDjango URL配置模板\n"""\n') + __M_writer('\n') + +# 生成操作列表 + operations_list = operations or ['create', 'get', 'update', 'delete', 'list'] + snake_model_name = model_name.lower() + resource_name = snake_model_name + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['operations_list','resource_name','snake_model_name'] if __M_key in __M_locals_builtin_stored])) + __M_writer("\nfrom django.urls import path\nfrom . import views\n\napp_name = '") + __M_writer(str(app_name or snake_model_name)) + __M_writer("'\n\nurlpatterns = [\n") + if 'create' in operations_list: + __M_writer(' # 创建') + __M_writer(str(model_name)) + __M_writer("\n path('', views.create_") + __M_writer(str(snake_model_name)) + __M_writer(", name='create_") + __M_writer(str(snake_model_name)) + __M_writer("'),\n") + __M_writer(' \n') + if 'list' in operations_list: + __M_writer(' # 获取') + __M_writer(str(model_name)) + __M_writer("列表\n path('list/', views.list_") + __M_writer(str(snake_model_name)) + __M_writer(", name='list_") + __M_writer(str(snake_model_name)) + __M_writer("'),\n") + __M_writer(' \n') + if 'get' in operations_list: + __M_writer(' # 获取') + __M_writer(str(model_name)) + __M_writer("详情\n path('/', views.get_") + __M_writer(str(snake_model_name)) + __M_writer(", name='get_") + __M_writer(str(snake_model_name)) + __M_writer("'),\n") + __M_writer(' \n') + if 'update' in operations_list: + __M_writer(' # 更新') + __M_writer(str(model_name)) + __M_writer("\n path('/update/', views.update_") + __M_writer(str(snake_model_name)) + __M_writer(", name='update_") + __M_writer(str(snake_model_name)) + __M_writer("'),\n") + __M_writer(' \n') + if 'delete' in operations_list: + __M_writer(' # 删除') + __M_writer(str(model_name)) + __M_writer("\n path('/delete/', views.delete_") + __M_writer(str(snake_model_name)) + __M_writer(", name='delete_") + __M_writer(str(snake_model_name)) + __M_writer("'),\n") + __M_writer(']\n\n# RESTful风格的URL配置(可选)\nrestful_urlpatterns = [\n') + if 'create' in operations_list or 'list' in operations_list: + __M_writer(' # POST: 创建') + __M_writer(str(model_name)) + __M_writer(', GET: 获取') + __M_writer(str(model_name)) + __M_writer("列表\n path('") + __M_writer(str(prefix)) + __M_writer(str(resource_name)) + __M_writer("/', views.") + __M_writer(str('create_' + snake_model_name if 'create' in operations_list else 'list_' + snake_model_name)) + __M_writer(", name='") + __M_writer(str(snake_model_name)) + __M_writer("_collection'),\n") + __M_writer(' \n') + if 'get' in operations_list or 'update' in operations_list or 'delete' in operations_list: + __M_writer(" # GET: 获取详情, PUT/PATCH: 更新, DELETE: 删除\n path('") + __M_writer(str(prefix)) + __M_writer(str(resource_name)) + __M_writer('//', views.update_") + __M_writer(str(snake_model_name)) + __M_writer(", name='") + __M_writer(str(snake_model_name)) + __M_writer("_detail'),\n") + __M_writer(']') + return '' + finally: + context.caller_stack._pop_frame() + + +""" +__M_BEGIN_METADATA +{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/urls.mako", "uri": "django/urls.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "29": 1, "30": 6, "31": 7, "32": 8, "33": 9, "34": 10, "35": 11, "36": 12, "37": 13, "40": 12, "41": 16, "42": 16, "43": 19, "44": 20, "45": 20, "46": 20, "47": 21, "48": 21, "49": 21, "50": 21, "51": 23, "52": 24, "53": 25, "54": 25, "55": 25, "56": 26, "57": 26, "58": 26, "59": 26, "60": 28, "61": 29, "62": 30, "63": 30, "64": 30, "65": 31, "66": 31, "67": 31, "68": 31, "69": 31, "70": 31, "71": 33, "72": 34, "73": 35, "74": 35, "75": 35, "76": 36, "77": 36, "78": 36, "79": 36, "80": 36, "81": 36, "82": 38, "83": 39, "84": 40, "85": 40, "86": 40, "87": 41, "88": 41, "89": 41, "90": 41, "91": 41, "92": 41, "93": 43, "94": 47, "95": 48, "96": 48, "97": 48, "98": 48, "99": 48, "100": 49, "101": 49, "102": 49, "103": 49, "104": 49, "105": 49, "106": 49, "107": 51, "108": 52, "109": 53, "110": 54, "111": 54, "112": 54, "113": 54, "114": 54, "115": 54, "116": 54, "117": 54, "118": 54, "119": 56, "125": 119}} +__M_END_METADATA +""" diff --git a/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/views.mako.py b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/views.mako.py new file mode 100644 index 0000000..c359948 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/.mako_modules/django/views.mako.py @@ -0,0 +1,328 @@ +# -*- coding:utf-8 -*- +from mako import runtime, filters, cache +UNDEFINED = runtime.UNDEFINED +STOP_RENDERING = runtime.STOP_RENDERING +__M_dict_builtin = dict +__M_locals_builtin = locals +_magic_number = 10 +_modified_time = 1760498624.4341366 +_enable_loop = True +_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/views.mako' +_template_uri = 'django/views.mako' +_source_encoding = 'utf-8' +_exports = [] + + + +from datetime import datetime + + +def render_body(context,**pageargs): + __M_caller = context.caller_stack._push_frame() + try: + __M_locals = __M_dict_builtin(pageargs=pageargs) + operations = context.get('operations', UNDEFINED) + pagination = context.get('pagination', UNDEFINED) + isinstance = context.get('isinstance', UNDEFINED) + permissions = context.get('permissions', UNDEFINED) + dict = context.get('dict', UNDEFINED) + ordering = context.get('ordering', UNDEFINED) + model_name = context.get('model_name', UNDEFINED) + search_fields = context.get('search_fields', UNDEFINED) + __M_writer = context.writer() + __M_writer('"""\nDjango视图模板\n"""\n') + __M_writer('\n') + +# 生成操作列表 + operations_list = operations or ['create', 'get', 'update', 'delete', 'list'] + permissions_list = permissions or [] + snake_model_name = model_name.lower() + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['operations_list','snake_model_name','permissions_list'] if __M_key in __M_locals_builtin_stored])) + __M_writer('\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\nfrom rest_framework import status\nfrom drf_spectacular.utils import extend_schema, OpenApiResponse\nfrom django.core.paginator import Paginator\nfrom django.db.models import Q\n\nfrom .models import ') + __M_writer(str(model_name)) + __M_writer('\nfrom .serializers import (\n ') + __M_writer(str(model_name)) + __M_writer('Serializer,\n ') + __M_writer(str(model_name)) + __M_writer('CreateSerializer,\n ') + __M_writer(str(model_name)) + __M_writer('UpdateSerializer,\n ') + __M_writer(str(model_name)) + __M_writer('ListSerializer\n)\nfrom hertz_studio_django_utils.responses import HertzResponse\n') + if permissions_list: + __M_writer('from hertz_studio_django_auth.utils.decorators import login_required, permission_required\n') + __M_writer('\n\n') + if 'create' in operations_list: + __M_writer("@extend_schema(\n operation_id='create_") + __M_writer(str(snake_model_name)) + __M_writer("',\n summary='创建") + __M_writer(str(model_name)) + __M_writer("',\n description='创建新的") + __M_writer(str(model_name)) + __M_writer("实例',\n request=") + __M_writer(str(model_name)) + __M_writer('CreateSerializer,\n responses={\n 201: OpenApiResponse(response=') + __M_writer(str(model_name)) + __M_writer("Serializer, description='创建成功'),\n 400: OpenApiResponse(description='参数错误'),\n },\n tags=['") + __M_writer(str(model_name)) + __M_writer("']\n)\n@api_view(['POST'])\n") + if 'create' in permissions_list: + + # 获取权限代码,如果permissions_list是字典,则获取对应的权限代码 + if isinstance(permissions_list, dict) and 'create' in permissions_list: + permission_code = permissions_list['create'] + else: + permission_code = f'{snake_model_name}:create' + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored])) + __M_writer("\n@permission_required('") + __M_writer(str(permission_code)) + __M_writer("')\n") + __M_writer('def create_') + __M_writer(str(snake_model_name)) + __M_writer('(request):\n """\n 创建') + __M_writer(str(model_name)) + __M_writer('\n \n 创建时间: ') + __M_writer(str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + __M_writer('\n """\n try:\n serializer = ') + __M_writer(str(model_name)) + __M_writer('CreateSerializer(data=request.data)\n if serializer.is_valid():\n instance = serializer.save()\n response_serializer = ') + __M_writer(str(model_name)) + __M_writer("Serializer(instance)\n return HertzResponse.success(\n data=response_serializer.data,\n message='") + __M_writer(str(model_name)) + __M_writer("创建成功'\n )\n return HertzResponse.validation_error(\n message='参数验证失败',\n errors=serializer.errors\n )\n except Exception as e:\n return HertzResponse.error(\n message='创建") + __M_writer(str(model_name)) + __M_writer("失败',\n error=str(e)\n )\n\n\n") + if 'get' in operations_list or 'retrieve' in operations_list: + __M_writer("@extend_schema(\n operation_id='get_") + __M_writer(str(snake_model_name)) + __M_writer("',\n summary='获取") + __M_writer(str(model_name)) + __M_writer("详情',\n description='根据ID获取") + __M_writer(str(model_name)) + __M_writer("详情',\n responses={\n 200: OpenApiResponse(response=") + __M_writer(str(model_name)) + __M_writer("Serializer, description='获取成功'),\n 404: OpenApiResponse(description='") + __M_writer(str(model_name)) + __M_writer("不存在'),\n },\n tags=['") + __M_writer(str(model_name)) + __M_writer("']\n)\n@api_view(['GET'])\n") + if 'get' in permissions_list or 'retrieve' in permissions_list: + + # 获取权限代码 + if isinstance(permissions_list, dict): + permission_code = permissions_list.get('get') or permissions_list.get('retrieve') or f'{snake_model_name}:query' + else: + permission_code = f'{snake_model_name}:query' + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored])) + __M_writer("\n@permission_required('") + __M_writer(str(permission_code)) + __M_writer("')\n") + __M_writer('def get_') + __M_writer(str(snake_model_name)) + __M_writer('(request, ') + __M_writer(str(snake_model_name)) + __M_writer('_id):\n """\n 获取') + __M_writer(str(model_name)) + __M_writer('详情\n \n Args:\n ') + __M_writer(str(snake_model_name)) + __M_writer('_id: ') + __M_writer(str(model_name)) + __M_writer('ID\n """\n try:\n instance = ') + __M_writer(str(model_name)) + __M_writer('.get_by_id(') + __M_writer(str(snake_model_name)) + __M_writer("_id)\n if not instance:\n return HertzResponse.not_found(message='") + __M_writer(str(model_name)) + __M_writer("不存在')\n \n serializer = ") + __M_writer(str(model_name)) + __M_writer("Serializer(instance)\n return HertzResponse.success(\n data=serializer.data,\n message='获取") + __M_writer(str(model_name)) + __M_writer("详情成功'\n )\n except Exception as e:\n return HertzResponse.error(\n message='获取") + __M_writer(str(model_name)) + __M_writer("详情失败',\n error=str(e)\n )\n\n\n") + if 'update' in operations_list: + __M_writer("@extend_schema(\n operation_id='update_") + __M_writer(str(snake_model_name)) + __M_writer("',\n summary='更新") + __M_writer(str(model_name)) + __M_writer("',\n description='根据ID更新") + __M_writer(str(model_name)) + __M_writer("信息',\n request=") + __M_writer(str(model_name)) + __M_writer('UpdateSerializer,\n responses={\n 200: OpenApiResponse(response=') + __M_writer(str(model_name)) + __M_writer("Serializer, description='更新成功'),\n 404: OpenApiResponse(description='") + __M_writer(str(model_name)) + __M_writer("不存在'),\n 400: OpenApiResponse(description='参数错误'),\n },\n tags=['") + __M_writer(str(model_name)) + __M_writer("']\n)\n@api_view(['PUT', 'PATCH'])\n") + if 'update' in permissions_list: + + # 获取权限代码 + if isinstance(permissions_list, dict) and 'update' in permissions_list: + permission_code = permissions_list['update'] + else: + permission_code = f'{snake_model_name}:update' + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored])) + __M_writer("\n@permission_required('") + __M_writer(str(permission_code)) + __M_writer("')\n") + __M_writer('def update_') + __M_writer(str(snake_model_name)) + __M_writer('(request, ') + __M_writer(str(snake_model_name)) + __M_writer('_id):\n """\n 更新') + __M_writer(str(model_name)) + __M_writer('\n \n Args:\n ') + __M_writer(str(snake_model_name)) + __M_writer('_id: ') + __M_writer(str(model_name)) + __M_writer('ID\n """\n try:\n instance = ') + __M_writer(str(model_name)) + __M_writer('.get_by_id(') + __M_writer(str(snake_model_name)) + __M_writer("_id)\n if not instance:\n return HertzResponse.not_found(message='") + __M_writer(str(model_name)) + __M_writer("不存在')\n \n partial = request.method == 'PATCH'\n serializer = ") + __M_writer(str(model_name)) + __M_writer('UpdateSerializer(\n instance, \n data=request.data, \n partial=partial\n )\n \n if serializer.is_valid():\n updated_instance = serializer.save()\n response_serializer = ') + __M_writer(str(model_name)) + __M_writer("Serializer(updated_instance)\n return HertzResponse.success(\n data=response_serializer.data,\n message='") + __M_writer(str(model_name)) + __M_writer("更新成功'\n )\n return HertzResponse.validation_error(\n message='参数验证失败',\n errors=serializer.errors\n )\n except Exception as e:\n return HertzResponse.error(\n message='更新") + __M_writer(str(model_name)) + __M_writer("失败',\n error=str(e)\n )\n\n\n") + if 'delete' in operations_list: + __M_writer("@extend_schema(\n operation_id='delete_") + __M_writer(str(snake_model_name)) + __M_writer("',\n summary='删除") + __M_writer(str(model_name)) + __M_writer("',\n description='根据ID删除") + __M_writer(str(model_name)) + __M_writer("',\n responses={\n 200: OpenApiResponse(description='删除成功'),\n 404: OpenApiResponse(description='") + __M_writer(str(model_name)) + __M_writer("不存在'),\n },\n tags=['") + __M_writer(str(model_name)) + __M_writer("']\n)\n@api_view(['DELETE'])\n") + if 'delete' in permissions_list: + + # 获取权限代码 + if isinstance(permissions_list, dict) and 'delete' in permissions_list: + permission_code = permissions_list['delete'] + else: + permission_code = f'{snake_model_name}:delete' + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored])) + __M_writer("\n@permission_required('") + __M_writer(str(permission_code)) + __M_writer("')\n") + __M_writer('def delete_') + __M_writer(str(snake_model_name)) + __M_writer('(request, ') + __M_writer(str(snake_model_name)) + __M_writer('_id):\n """\n 删除') + __M_writer(str(model_name)) + __M_writer('\n \n Args:\n ') + __M_writer(str(snake_model_name)) + __M_writer('_id: ') + __M_writer(str(model_name)) + __M_writer('ID\n """\n try:\n instance = ') + __M_writer(str(model_name)) + __M_writer('.get_by_id(') + __M_writer(str(snake_model_name)) + __M_writer("_id)\n if not instance:\n return HertzResponse.not_found(message='") + __M_writer(str(model_name)) + __M_writer("不存在')\n \n instance.delete()\n return HertzResponse.success(message='") + __M_writer(str(model_name)) + __M_writer("删除成功')\n except Exception as e:\n return HertzResponse.error(\n message='删除") + __M_writer(str(model_name)) + __M_writer("失败',\n error=str(e)\n )\n\n\n") + if 'list' in operations_list: + __M_writer("@extend_schema(\n operation_id='list_") + __M_writer(str(snake_model_name)) + __M_writer("',\n summary='获取") + __M_writer(str(model_name)) + __M_writer("列表',\n description='分页获取") + __M_writer(str(model_name)) + __M_writer("列表',\n responses={\n 200: OpenApiResponse(response=") + __M_writer(str(model_name)) + __M_writer("ListSerializer, description='获取成功'),\n },\n tags=['") + __M_writer(str(model_name)) + __M_writer("']\n)\n@api_view(['GET'])\n") + if 'list' in permissions_list: + + # 获取权限代码 + if isinstance(permissions_list, dict) and 'list' in permissions_list: + permission_code = permissions_list['list'] + else: + permission_code = f'{snake_model_name}:list' + + + __M_locals_builtin_stored = __M_locals_builtin() + __M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored])) + __M_writer("\n@permission_required('") + __M_writer(str(permission_code)) + __M_writer("')\n") + __M_writer('def list_') + __M_writer(str(snake_model_name)) + __M_writer('(request):\n """\n 获取') + __M_writer(str(model_name)) + __M_writer('列表\n \n 支持分页、搜索和排序\n """\n try:\n queryset = ') + __M_writer(str(model_name)) + __M_writer(".objects.all()\n \n # 搜索功能\n search = request.GET.get('search', '')\n if search:\n") + if search_fields: + __M_writer(' search_q = Q()\n') + for field in search_fields: + __M_writer(' search_q |= Q(') + __M_writer(str(field)) + __M_writer('__icontains=search)\n') + __M_writer(' queryset = queryset.filter(search_q)\n') + else: + __M_writer(' # 默认搜索字段,可根据需要调整\n queryset = queryset.filter(\n Q(id__icontains=search)\n )\n') + __M_writer(" \n # 排序功能\n ordering = request.GET.get('ordering', '-created_at')\n") + if ordering: + __M_writer(' valid_orderings = [') + __M_writer(str(', '.join([f"'{field}'" for field in ordering] + [f"'-{field}'" for field in ordering]))) + __M_writer(']\n') + else: + __M_writer(" valid_orderings = ['created_at', '-created_at', 'updated_at', '-updated_at']\n") + __M_writer(' if ordering in valid_orderings:\n queryset = queryset.order_by(ordering)\n \n # 分页功能\n') + if pagination: + __M_writer(" page = int(request.GET.get('page', 1))\n page_size = int(request.GET.get('page_size', 20))\n paginator = Paginator(queryset, page_size)\n page_obj = paginator.get_page(page)\n \n serializer = ") + __M_writer(str(model_name)) + __M_writer("ListSerializer(page_obj.object_list, many=True)\n \n return HertzResponse.success(\n data={\n 'results': serializer.data,\n 'pagination': {\n 'page': page,\n 'page_size': page_size,\n 'total_pages': paginator.num_pages,\n 'total_count': paginator.count,\n 'has_next': page_obj.has_next(),\n 'has_previous': page_obj.has_previous(),\n }\n },\n message='获取") + __M_writer(str(model_name)) + __M_writer("列表成功'\n )\n") + else: + __M_writer(' serializer = ') + __M_writer(str(model_name)) + __M_writer("ListSerializer(queryset, many=True)\n return HertzResponse.success(\n data=serializer.data,\n message='获取") + __M_writer(str(model_name)) + __M_writer("列表成功'\n )\n") + __M_writer(" except Exception as e:\n return HertzResponse.error(\n message='获取") + __M_writer(str(model_name)) + __M_writer("列表失败',\n error=str(e)\n )\n\n\n") + return '' + finally: + context.caller_stack._pop_frame() + + +""" +__M_BEGIN_METADATA +{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/views.mako", "uri": "django/views.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "33": 1, "34": 6, "35": 7, "36": 8, "37": 9, "38": 10, "39": 11, "40": 12, "41": 13, "44": 12, "45": 20, "46": 20, "47": 22, "48": 22, "49": 23, "50": 23, "51": 24, "52": 24, "53": 25, "54": 25, "55": 28, "56": 29, "57": 31, "58": 33, "59": 34, "60": 35, "61": 35, "62": 36, "63": 36, "64": 37, "65": 37, "66": 38, "67": 38, "68": 40, "69": 40, "70": 43, "71": 43, "72": 46, "73": 47, "74": 48, "75": 49, "76": 50, "77": 51, "78": 52, "79": 53, "80": 54, "83": 53, "84": 54, "85": 54, "86": 56, "87": 56, "88": 56, "89": 58, "90": 58, "91": 60, "92": 60, "93": 63, "94": 63, "95": 66, "96": 66, "97": 69, "98": 69, "99": 77, "100": 77, "101": 83, "102": 84, "103": 85, "104": 85, "105": 86, "106": 86, "107": 87, "108": 87, "109": 89, "110": 89, "111": 90, "112": 90, "113": 92, "114": 92, "115": 95, "116": 96, "117": 97, "118": 98, "119": 99, "120": 100, "121": 101, "122": 102, "123": 103, "126": 102, "127": 103, "128": 103, "129": 105, "130": 105, "131": 105, "132": 105, "133": 105, "134": 107, "135": 107, "136": 110, "137": 110, "138": 110, "139": 110, "140": 113, "141": 113, "142": 113, "143": 113, "144": 115, "145": 115, "146": 117, "147": 117, "148": 120, "149": 120, "150": 124, "151": 124, "152": 130, "153": 131, "154": 132, "155": 132, "156": 133, "157": 133, "158": 134, "159": 134, "160": 135, "161": 135, "162": 137, "163": 137, "164": 138, "165": 138, "166": 141, "167": 141, "168": 144, "169": 145, "170": 146, "171": 147, "172": 148, "173": 149, "174": 150, "175": 151, "176": 152, "179": 151, "180": 152, "181": 152, "182": 154, "183": 154, "184": 154, "185": 154, "186": 154, "187": 156, "188": 156, "189": 159, "190": 159, "191": 159, "192": 159, "193": 162, "194": 162, "195": 162, "196": 162, "197": 164, "198": 164, "199": 167, "200": 167, "201": 175, "202": 175, "203": 178, "204": 178, "205": 186, "206": 186, "207": 192, "208": 193, "209": 194, "210": 194, "211": 195, "212": 195, "213": 196, "214": 196, "215": 199, "216": 199, "217": 201, "218": 201, "219": 204, "220": 205, "221": 206, "222": 207, "223": 208, "224": 209, "225": 210, "226": 211, "227": 212, "230": 211, "231": 212, "232": 212, "233": 214, "234": 214, "235": 214, "236": 214, "237": 214, "238": 216, "239": 216, "240": 219, "241": 219, "242": 219, "243": 219, "244": 222, "245": 222, "246": 222, "247": 222, "248": 224, "249": 224, "250": 227, "251": 227, "252": 230, "253": 230, "254": 236, "255": 237, "256": 238, "257": 238, "258": 239, "259": 239, "260": 240, "261": 240, "262": 242, "263": 242, "264": 244, "265": 244, "266": 247, "267": 248, "268": 249, "269": 250, "270": 251, "271": 252, "272": 253, "273": 254, "274": 255, "277": 254, "278": 255, "279": 255, "280": 257, "281": 257, "282": 257, "283": 259, "284": 259, "285": 264, "286": 264, "287": 269, "288": 270, "289": 271, "290": 272, "291": 272, "292": 272, "293": 274, "294": 275, "295": 276, "296": 281, "297": 284, "298": 285, "299": 285, "300": 285, "301": 286, "302": 287, "303": 289, "304": 293, "305": 294, "306": 299, "307": 299, "308": 313, "309": 313, "310": 315, "311": 316, "312": 316, "313": 316, "314": 319, "315": 319, "316": 322, "317": 324, "318": 324, "324": 318}} +__M_END_METADATA +""" diff --git a/hertz_studio_django_utils/code_generator/templates/django/apps.mako b/hertz_studio_django_utils/code_generator/templates/django/apps.mako new file mode 100644 index 0000000..e477f20 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/django/apps.mako @@ -0,0 +1,23 @@ +""" +Django应用配置模板 +""" +<%! +from datetime import datetime +%> +<% +# 生成应用配置类名 +app_class_name = ''.join(word.capitalize() for word in app_name.split('_')) + 'Config' +%> +from django.apps import AppConfig + + +class ${app_class_name}(AppConfig): + """ + ${app_name}应用配置 + + 创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + """ + default_auto_field = 'django.db.models.BigAutoField' + name = '${app_name}' + verbose_name = '${verbose_name or app_name}' + diff --git a/hertz_studio_django_utils/code_generator/templates/django/models.mako b/hertz_studio_django_utils/code_generator/templates/django/models.mako new file mode 100644 index 0000000..5dc0847 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/django/models.mako @@ -0,0 +1,173 @@ +""" +Django模型模板 +""" +<%! +from datetime import datetime +%> +<% +# 导入必要的模块 +imports = [] +if any(field.get('type') == 'ForeignKey' for field in fields): + imports.append('from django.contrib.auth import get_user_model') +if any(field.get('type') in ['DateTimeField', 'DateField', 'TimeField'] for field in fields): + imports.append('from django.utils import timezone') +if any(field.get('choices') for field in fields): + imports.append('from django.core.validators import validate_email') + +# 生成状态选择项 +status_choices_code = "" +if status_choices: + choices_list = [] + for choice in status_choices: + if isinstance(choice, tuple) and len(choice) == 2: + choices_list.append(f" ('{choice[0]}', '{choice[1]}')") + else: + choices_list.append(f" ('{choice}', '{choice}')") + status_choices_code = f""" STATUS_CHOICES = [ +{',{}'.format(chr(10)).join(choices_list)}, + ]""" +%>\ +from django.db import models +% for import_line in imports: +${import_line} +% endfor + +class ${model_name}(models.Model): + """ + ${verbose_name or model_name}模型 + % if table_name: + + 数据表名: ${table_name} + % endif + 创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + """ +% if status_choices_code: +${status_choices_code} + +% endif +% for field in fields: +<% +field_name = field['name'] +# 使用转换后的Django字段类型 +field_type = field.get('type', 'CharField') +field_options = [] + +# 处理字段选项 +if field.get('verbose_name'): + field_options.append(f"verbose_name='{field['verbose_name']}'") +if field.get('help_text'): + field_options.append(f"help_text='{field['help_text']}'") +if field.get('max_length'): + field_options.append(f"max_length={field['max_length']}") +if field.get('null'): + field_options.append(f"null={field['null']}") +if field.get('blank'): + field_options.append(f"blank={field['blank']}") +if field.get('default') is not None: + if isinstance(field['default'], str): + field_options.append(f"default='{field['default']}'") + else: + field_options.append(f"default={field['default']}") +if field.get('unique'): + field_options.append(f"unique={field['unique']}") +if field.get('db_index'): + field_options.append(f"db_index={field['db_index']}") + +# 处理特殊字段类型 +if field_type == 'ForeignKey': + if field.get('to'): + field_options.insert(0, f"'{field['to']}'") + else: + field_options.insert(0, "get_user_model()") + if field.get('on_delete'): + field_options.append(f"on_delete=models.{field['on_delete']}") + else: + field_options.append("on_delete=models.CASCADE") +elif field_type == 'ManyToManyField': + if field.get('to'): + field_options.insert(0, f"'{field['to']}'") +elif field_type == 'OneToOneField': + if field.get('to'): + field_options.insert(0, f"'{field['to']}'") + if field.get('on_delete'): + field_options.append(f"on_delete=models.{field['on_delete']}") + else: + field_options.append("on_delete=models.CASCADE") + +# 处理选择项 +if field.get('choices'): + if field_name == 'status' and status_choices: + field_options.append("choices=STATUS_CHOICES") + else: + choices_list = [] + for choice in field['choices']: + if isinstance(choice, tuple) and len(choice) == 2: + choices_list.append(f"('{choice[0]}', '{choice[1]}')") + else: + choices_list.append(f"('{choice}', '{choice}')") + field_options.append(f"choices=[{', '.join(choices_list)}]") + +# 处理特殊字段类型的额外参数 +if field_type == 'DecimalField': + if not any('max_digits' in opt for opt in field_options): + field_options.append('max_digits=10') + if not any('decimal_places' in opt for opt in field_options): + field_options.append('decimal_places=2') +elif field_type == 'DateTimeField': + if field_name == 'created_at' and not any('auto_now_add' in opt for opt in field_options): + field_options.append('auto_now_add=True') + elif field_name == 'updated_at' and not any('auto_now' in opt for opt in field_options): + field_options.append('auto_now=True') + +options_str = ', '.join(field_options) if field_options else '' +%> + ${field_name} = models.${field_type}(${options_str}) +% endfor + + class Meta: + % if table_name: + db_table = '${table_name}' + % endif + verbose_name = '${verbose_name or model_name}' + verbose_name_plural = '${verbose_name or model_name}' + % if ordering: + ordering = [${', '.join([f"'{field}'" for field in ordering])}] + % else: + ordering = ['-created_at'] + % endif + + def __str__(self): + """字符串表示""" + % if fields: + <% + # 寻找合适的字段作为字符串表示 + str_field = None + for field in fields: + if field['name'] in ['name', 'title', 'username', 'email']: + str_field = field['name'] + break + if not str_field: + # 如果没有找到合适的字段,使用第一个字符串字段 + for field in fields: + if field['type'] in ['CharField', 'TextField', 'EmailField']: + str_field = field['name'] + break + if not str_field: + str_field = 'id' + %> + return str(self.${str_field}) + % else: + return f"${model_name}({self.id})" + % endif + + def save(self, *args, **kwargs): + """保存方法""" + super().save(*args, **kwargs) + + @classmethod + def get_by_id(cls, obj_id): + """根据ID获取对象""" + try: + return cls.objects.get(id=obj_id) + except cls.DoesNotExist: + return None \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/templates/django/project_urls.mako b/hertz_studio_django_utils/code_generator/templates/django/project_urls.mako new file mode 100644 index 0000000..671840f --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/django/project_urls.mako @@ -0,0 +1,62 @@ +""" +Django主项目URL配置模板 +用于自动添加新app的URL路由 +""" +<%! +from datetime import datetime +%> +<% +# 获取现有的URL配置 +existing_urls = url_patterns or [] +new_app_config = { + 'app_name': app_name, + 'url_path': url_path or f'api/{app_name.replace("hertz_studio_django_", "").replace("_", "/")}/', + 'comment': comment or f'{verbose_name or app_name} routes' +} + +# 检查URL是否已存在 +url_exists = any(config.get('app_name') == new_app_config['app_name'] for config in existing_urls) +if not url_exists: + existing_urls.append(new_app_config) +%> +""" +URL configuration for hertz_server_django project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from . import views + +urlpatterns = [ + # 首页路由 + path('', views.index, name='index'), + +% for url_config in existing_urls: + # ${url_config['comment']} + path('${url_config['url_path']}', include('${url_config['app_name']}.urls')), + +% endfor + # API documentation routes + path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), + path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), +] + +# 在开发环境下提供媒体文件服务 +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0]) \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/templates/django/serializers.mako b/hertz_studio_django_utils/code_generator/templates/django/serializers.mako new file mode 100644 index 0000000..c42733e --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/django/serializers.mako @@ -0,0 +1,153 @@ +""" +Django序列化器模板 +""" +<%! +from datetime import datetime +%> +<% +# 生成字段列表 +all_fields = [] +create_fields_list = [] +update_fields_list = [] +list_fields_list = [] + +# 处理字段列表 +for field in fields: + field_name = field['name'] + all_fields.append(field_name) + + # 排除自动生成的字段 + if field_name not in ['id', 'created_at', 'updated_at']: + create_fields_list.append(field_name) + update_fields_list.append(field_name) + + list_fields_list.append(field_name) + +# 添加默认字段 +if 'id' not in all_fields: + all_fields.insert(0, 'id') +if 'created_at' not in all_fields: + all_fields.append('created_at') +if 'updated_at' not in all_fields: + all_fields.append('updated_at') + +# 如果没有指定列表字段,使用所有字段 +if not list_fields_list: + list_fields_list = all_fields +%> +from rest_framework import serializers +from .models import ${model_name} + + +class ${model_name}Serializer(serializers.ModelSerializer): + """ + ${model_name}序列化器 + + 用于${model_name}模型的序列化和反序列化 + 创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + """ + + class Meta: + model = ${model_name} + fields = [${', '.join([f"'{field}'" for field in all_fields])}] + read_only_fields = ['id', 'created_at', 'updated_at'] + + % for field in fields: + % if field.get('validators'): + def validate_${field['name']}(self, value): + """ + 验证${field['name']}字段 + """ + % for validator_name, validator_rule in field['validators'].items(): + # ${validator_name}验证: ${validator_rule} + % endfor + return value + + % endif + % endfor + def validate(self, attrs): + """ + 对象级别的验证 + """ + # 在这里添加跨字段验证逻辑 + return attrs + + def create(self, validated_data): + """ + 创建${model_name}实例 + """ + return ${model_name}.objects.create(**validated_data) + + def update(self, instance, validated_data): + """ + 更新${model_name}实例 + """ + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + + +class ${model_name}CreateSerializer(serializers.ModelSerializer): + """ + ${model_name}创建序列化器 + + 用于创建${model_name}实例 + """ + + class Meta: + model = ${model_name} + fields = [${', '.join([f"'{field}'" for field in create_fields_list])}] + + % for field in fields: + % if field['name'] in create_fields_list and field.get('validators'): + def validate_${field['name']}(self, value): + """ + 验证${field['name']}字段 + """ + % for validator_name, validator_rule in field['validators'].items(): + # ${validator_name}验证: ${validator_rule} + % endfor + return value + + % endif + % endfor + + +class ${model_name}UpdateSerializer(serializers.ModelSerializer): + """ + ${model_name}更新序列化器 + + 用于更新${model_name}实例 + """ + + class Meta: + model = ${model_name} + fields = [${', '.join([f"'{field}'" for field in update_fields_list])}] + + % for field in fields: + % if field['name'] in update_fields_list and field.get('validators'): + def validate_${field['name']}(self, value): + """ + 验证${field['name']}字段 + """ + % for validator_name, validator_rule in field['validators'].items(): + # ${validator_name}验证: ${validator_rule} + % endfor + return value + + % endif + % endfor + + +class ${model_name}ListSerializer(serializers.ModelSerializer): + """ + ${model_name}列表序列化器 + + 用于列表显示${model_name}实例 + """ + + class Meta: + model = ${model_name} + fields = [${', '.join([f"'{field}'" for field in list_fields_list])}] + read_only_fields = [${', '.join([f"'{field}'" for field in list_fields_list])}] \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/templates/django/settings.mako b/hertz_studio_django_utils/code_generator/templates/django/settings.mako new file mode 100644 index 0000000..13c12e4 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/django/settings.mako @@ -0,0 +1,345 @@ +""" +Django settings.py 配置模板 +用于自动添加新app到INSTALLED_APPS +""" +<%! +from datetime import datetime +%> +<% +# 获取现有的INSTALLED_APPS列表 +existing_apps = installed_apps or [] +new_app = app_name + +# 检查app是否已存在 +if new_app not in existing_apps: + existing_apps.append(new_app) +%> +""" +Django settings for hertz_server_django project. + +Generated by 'django-admin startproject' using Django 5.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +import os +from pathlib import Path +from decouple import config + +# 修复DRF的ip_address_validators函数 +def fix_drf_ip_validators(): + """ + 修复DRF的ip_address_validators函数返回值问题 + """ + try: + from rest_framework import fields + + # 保存原始函数 + original_ip_address_validators = fields.ip_address_validators + + def fixed_ip_address_validators(protocol, unpack_ipv4): + """ + 修复后的ip_address_validators函数,确保返回两个值 + """ + validators = original_ip_address_validators(protocol, unpack_ipv4) + # 如果只返回了validators,添加默认的error_message + if isinstance(validators, list): + return validators, 'Enter a valid IP address.' + else: + # 如果已经返回了两个值,直接返回 + return validators + + # 应用猴子补丁 + fields.ip_address_validators = fixed_ip_address_validators + + except ImportError: + # 如果DRF未安装,忽略错误 + pass + +# 应用修复 +fix_drf_ip_validators() + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY', default='django-insecure-0a1bx*8!97l^4z#ml#ufn_*9ut*)zlso$*k-g^h&(2=p@^51md') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = config('DEBUG', default=True, cast=bool) + +ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')]) + +# Database switch configuration +USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool) + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # Third party apps + 'rest_framework', + 'corsheaders', + 'channels', + 'drf_spectacular', + + # Local apps +% for app in existing_apps: + '${app}', +% endfor +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'hertz_studio_django_auth.utils.middleware.AuthMiddleware', # 权限认证中间件 + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'hertz_server_django.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'] + , + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'hertz_server_django.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +if USE_REDIS_AS_DB: + # Redis as primary database (for caching and session storage) + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'data/db.sqlite3', + } + } + + # Use Redis for sessions + SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + SESSION_CACHE_ALIAS = 'default' + +else: + # MySQL database configuration + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': config('DB_NAME', default='hertz_server'), + 'USER': config('DB_USER', default='root'), + 'PASSWORD': config('DB_PASSWORD', default='root'), + 'HOST': config('DB_HOST', default='localhost'), + 'PORT': config('DB_PORT', default='3306'), + 'OPTIONS': { + 'charset': 'utf8mb4', + }, + } + } + +# Redis +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/0'), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' +STATICFILES_DIRS = [ + BASE_DIR / 'static', +] + +# Media files (User uploaded files) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Django REST Framework configuration +# 使用自定义AuthMiddleware进行认证,不使用DRF的认证和权限系统 +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_AUTHENTICATION_CLASSES': [], # 不使用DRF认证类 + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', # 所有接口默认允许访问,由AuthMiddleware控制权限 + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ], +} + +# Spectacular (OpenAPI 3.0) configuration +SPECTACULAR_SETTINGS = { + 'TITLE': 'Hertz Server API', + 'DESCRIPTION': 'API documentation for Hertz Server Django project', + 'VERSION': '1.0.0', + 'SERVE_INCLUDE_SCHEMA': False, + 'COMPONENT_SPLIT_REQUEST': True, + 'SCHEMA_PATH_PREFIX': '/api/', +} + +# CORS configuration +CORS_ALLOWED_ORIGINS = config( + 'CORS_ALLOWED_ORIGINS', + default='http://localhost:3000,http://127.0.0.1:3000', + cast=lambda v: [s.strip() for s in v.split(',')] +) + +CORS_ALLOW_CREDENTIALS = True + +CORS_ALLOW_ALL_ORIGINS = config('CORS_ALLOW_ALL_ORIGINS', default=False, cast=bool) + +# Captcha settings +CAPTCHA_IMAGE_SIZE = ( + config('CAPTCHA_IMAGE_SIZE_WIDTH', default=120, cast=int), + config('CAPTCHA_IMAGE_SIZE_HEIGHT', default=50, cast=int) +) +CAPTCHA_LENGTH = config('CAPTCHA_LENGTH', default=4, cast=int) +CAPTCHA_TIMEOUT = config('CAPTCHA_TIMEOUT', default=5, cast=int) # minutes +CAPTCHA_FONT_SIZE = config('CAPTCHA_FONT_SIZE', default=40, cast=int) +CAPTCHA_BACKGROUND_COLOR = config('CAPTCHA_BACKGROUND_COLOR', default='#ffffff') +CAPTCHA_FOREGROUND_COLOR = config('CAPTCHA_FOREGROUND_COLOR', default='#000000') +# 验证码词典文件路径 +CAPTCHA_WORDS_DICTIONARY = str(BASE_DIR / 'captcha_words.txt') +# 验证码挑战函数配置 +CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # 默认使用随机字符 +# 数学验证码配置 +CAPTCHA_MATH_CHALLENGE_OPERATOR = '+-*' +# 验证码噪声和过滤器 +CAPTCHA_NOISE_FUNCTIONS = ( + 'captcha.helpers.noise_arcs', + 'captcha.helpers.noise_dots', +) +CAPTCHA_FILTER_FUNCTIONS = ( + 'captcha.helpers.post_smooth', +) + +# Email configuration +EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend') +EMAIL_HOST = config('EMAIL_HOST', default='smtp.qq.com') +EMAIL_PORT = config('EMAIL_PORT', default=465, cast=int) +EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=True, cast=bool) +EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool) +EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='563161210@qq.com') +EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='') +DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='563161210@qq.com') + +# Channels configuration for WebSocket support +ASGI_APPLICATION = 'hertz_server_django.asgi.application' + +# Channel layers configuration +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [config('REDIS_URL', default='redis://127.0.0.1:6379/2')], + }, + }, +} + +# 自定义用户模型 +AUTH_USER_MODEL = 'hertz_studio_django_auth.HertzUser' + +# JWT配置 +JWT_SECRET_KEY = config('JWT_SECRET_KEY', default=SECRET_KEY) +JWT_ALGORITHM = 'HS256' +JWT_ACCESS_TOKEN_LIFETIME = config('JWT_ACCESS_TOKEN_LIFETIME', default=60 * 60 * 24, cast=int) # 24小时 +JWT_REFRESH_TOKEN_LIFETIME = config('JWT_REFRESH_TOKEN_LIFETIME', default=60 * 60 * 24 * 7, cast=int) # 7天 + +# 权限系统配置 +HERTZ_AUTH_SETTINGS = { + 'SUPER_ADMIN_PERMISSIONS': ['*'], # 超级管理员拥有所有权限 + 'DEFAULT_PERMISSIONS': [], # 默认权限 +} + +# AuthMiddleware配置 - 不需要登录验证的URL模式(支持正则表达式) +NO_AUTH_PATTERNS = config( + 'NO_AUTH_PATTERNS', + default=r'^/api/auth/login/?$,^/api/auth/register/?$,^/api/auth/email/code/?$,^/api/auth/send-email-code/?$,^/api/auth/password/reset/?$,^/api/captcha/.*$,^/api/docs/.*$,^/api/redoc/.*$,^/api/schema/.*$,^/admin/.*$,^/static/.*$,^/media/.*$,^/demo/.*$,^/websocket/.*$,^/api/system/.*$', + cast=lambda v: [s.strip() for s in v.split(',')] +) + +# 密码加密配置 +PASSWORD_HASHERS = [ + 'hertz_studio_django_utils.crypto.MD5PasswordHasher', # 使用MD5加密 + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', +] \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/templates/django/urls.mako b/hertz_studio_django_utils/code_generator/templates/django/urls.mako new file mode 100644 index 0000000..94cfcd3 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/django/urls.mako @@ -0,0 +1,56 @@ +""" +Django URL配置模板 +""" +<%! +from datetime import datetime +%> +<% +# 生成操作列表 +operations_list = operations or ['create', 'get', 'update', 'delete', 'list'] +snake_model_name = model_name.lower() +resource_name = snake_model_name +%> +from django.urls import path +from . import views + +app_name = '${app_name or snake_model_name}' + +urlpatterns = [ + % if 'create' in operations_list: + # 创建${model_name} + path('', views.create_${snake_model_name}, name='create_${snake_model_name}'), + % endif + + % if 'list' in operations_list: + # 获取${model_name}列表 + path('list/', views.list_${snake_model_name}, name='list_${snake_model_name}'), + % endif + + % if 'get' in operations_list: + # 获取${model_name}详情 + path('/', views.get_${snake_model_name}, name='get_${snake_model_name}'), + % endif + + % if 'update' in operations_list: + # 更新${model_name} + path('/update/', views.update_${snake_model_name}, name='update_${snake_model_name}'), + % endif + + % if 'delete' in operations_list: + # 删除${model_name} + path('/delete/', views.delete_${snake_model_name}, name='delete_${snake_model_name}'), + % endif +] + +# RESTful风格的URL配置(可选) +restful_urlpatterns = [ + % if 'create' in operations_list or 'list' in operations_list: + # POST: 创建${model_name}, GET: 获取${model_name}列表 + path('${prefix}${resource_name}/', views.${'create_' + snake_model_name if 'create' in operations_list else 'list_' + snake_model_name}, name='${snake_model_name}_collection'), + % endif + + % if 'get' in operations_list or 'update' in operations_list or 'delete' in operations_list: + # GET: 获取详情, PUT/PATCH: 更新, DELETE: 删除 + path('${prefix}${resource_name}//', views.update_${snake_model_name}, name='${snake_model_name}_detail'), + % endif +] \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/templates/django/views.mako b/hertz_studio_django_utils/code_generator/templates/django/views.mako new file mode 100644 index 0000000..b1a3163 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/templates/django/views.mako @@ -0,0 +1,329 @@ +""" +Django视图模板 +""" +<%! +from datetime import datetime +%> +<% +# 生成操作列表 +operations_list = operations or ['create', 'get', 'update', 'delete', 'list'] +permissions_list = permissions or [] +snake_model_name = model_name.lower() +%> +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework import status +from drf_spectacular.utils import extend_schema, OpenApiResponse +from django.core.paginator import Paginator +from django.db.models import Q + +from .models import ${model_name} +from .serializers import ( + ${model_name}Serializer, + ${model_name}CreateSerializer, + ${model_name}UpdateSerializer, + ${model_name}ListSerializer +) +from hertz_studio_django_utils.responses import HertzResponse +% if permissions_list: +from hertz_studio_django_auth.utils.decorators import login_required, permission_required +% endif + + +% if 'create' in operations_list: +@extend_schema( + operation_id='create_${snake_model_name}', + summary='创建${model_name}', + description='创建新的${model_name}实例', + request=${model_name}CreateSerializer, + responses={ + 201: OpenApiResponse(response=${model_name}Serializer, description='创建成功'), + 400: OpenApiResponse(description='参数错误'), + }, + tags=['${model_name}'] +) +@api_view(['POST']) +% if 'create' in permissions_list: +<% + # 获取权限代码,如果permissions_list是字典,则获取对应的权限代码 + if isinstance(permissions_list, dict) and 'create' in permissions_list: + permission_code = permissions_list['create'] + else: + permission_code = f'{snake_model_name}:create' +%> +@permission_required('${permission_code}') +% endif +def create_${snake_model_name}(request): + """ + 创建${model_name} + + 创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + """ + try: + serializer = ${model_name}CreateSerializer(data=request.data) + if serializer.is_valid(): + instance = serializer.save() + response_serializer = ${model_name}Serializer(instance) + return HertzResponse.success( + data=response_serializer.data, + message='${model_name}创建成功' + ) + return HertzResponse.validation_error( + message='参数验证失败', + errors=serializer.errors + ) + except Exception as e: + return HertzResponse.error( + message='创建${model_name}失败', + error=str(e) + ) + + +% endif +% if 'get' in operations_list or 'retrieve' in operations_list: +@extend_schema( + operation_id='get_${snake_model_name}', + summary='获取${model_name}详情', + description='根据ID获取${model_name}详情', + responses={ + 200: OpenApiResponse(response=${model_name}Serializer, description='获取成功'), + 404: OpenApiResponse(description='${model_name}不存在'), + }, + tags=['${model_name}'] +) +@api_view(['GET']) +% if 'get' in permissions_list or 'retrieve' in permissions_list: +<% + # 获取权限代码 + if isinstance(permissions_list, dict): + permission_code = permissions_list.get('get') or permissions_list.get('retrieve') or f'{snake_model_name}:query' + else: + permission_code = f'{snake_model_name}:query' +%> +@permission_required('${permission_code}') +% endif +def get_${snake_model_name}(request, ${snake_model_name}_id): + """ + 获取${model_name}详情 + + Args: + ${snake_model_name}_id: ${model_name}ID + """ + try: + instance = ${model_name}.get_by_id(${snake_model_name}_id) + if not instance: + return HertzResponse.not_found(message='${model_name}不存在') + + serializer = ${model_name}Serializer(instance) + return HertzResponse.success( + data=serializer.data, + message='获取${model_name}详情成功' + ) + except Exception as e: + return HertzResponse.error( + message='获取${model_name}详情失败', + error=str(e) + ) + + +% endif +% if 'update' in operations_list: +@extend_schema( + operation_id='update_${snake_model_name}', + summary='更新${model_name}', + description='根据ID更新${model_name}信息', + request=${model_name}UpdateSerializer, + responses={ + 200: OpenApiResponse(response=${model_name}Serializer, description='更新成功'), + 404: OpenApiResponse(description='${model_name}不存在'), + 400: OpenApiResponse(description='参数错误'), + }, + tags=['${model_name}'] +) +@api_view(['PUT', 'PATCH']) +% if 'update' in permissions_list: +<% + # 获取权限代码 + if isinstance(permissions_list, dict) and 'update' in permissions_list: + permission_code = permissions_list['update'] + else: + permission_code = f'{snake_model_name}:update' +%> +@permission_required('${permission_code}') +% endif +def update_${snake_model_name}(request, ${snake_model_name}_id): + """ + 更新${model_name} + + Args: + ${snake_model_name}_id: ${model_name}ID + """ + try: + instance = ${model_name}.get_by_id(${snake_model_name}_id) + if not instance: + return HertzResponse.not_found(message='${model_name}不存在') + + partial = request.method == 'PATCH' + serializer = ${model_name}UpdateSerializer( + instance, + data=request.data, + partial=partial + ) + + if serializer.is_valid(): + updated_instance = serializer.save() + response_serializer = ${model_name}Serializer(updated_instance) + return HertzResponse.success( + data=response_serializer.data, + message='${model_name}更新成功' + ) + return HertzResponse.validation_error( + message='参数验证失败', + errors=serializer.errors + ) + except Exception as e: + return HertzResponse.error( + message='更新${model_name}失败', + error=str(e) + ) + + +% endif +% if 'delete' in operations_list: +@extend_schema( + operation_id='delete_${snake_model_name}', + summary='删除${model_name}', + description='根据ID删除${model_name}', + responses={ + 200: OpenApiResponse(description='删除成功'), + 404: OpenApiResponse(description='${model_name}不存在'), + }, + tags=['${model_name}'] +) +@api_view(['DELETE']) +% if 'delete' in permissions_list: +<% + # 获取权限代码 + if isinstance(permissions_list, dict) and 'delete' in permissions_list: + permission_code = permissions_list['delete'] + else: + permission_code = f'{snake_model_name}:delete' +%> +@permission_required('${permission_code}') +% endif +def delete_${snake_model_name}(request, ${snake_model_name}_id): + """ + 删除${model_name} + + Args: + ${snake_model_name}_id: ${model_name}ID + """ + try: + instance = ${model_name}.get_by_id(${snake_model_name}_id) + if not instance: + return HertzResponse.not_found(message='${model_name}不存在') + + instance.delete() + return HertzResponse.success(message='${model_name}删除成功') + except Exception as e: + return HertzResponse.error( + message='删除${model_name}失败', + error=str(e) + ) + + +% endif +% if 'list' in operations_list: +@extend_schema( + operation_id='list_${snake_model_name}', + summary='获取${model_name}列表', + description='分页获取${model_name}列表', + responses={ + 200: OpenApiResponse(response=${model_name}ListSerializer, description='获取成功'), + }, + tags=['${model_name}'] +) +@api_view(['GET']) +% if 'list' in permissions_list: +<% + # 获取权限代码 + if isinstance(permissions_list, dict) and 'list' in permissions_list: + permission_code = permissions_list['list'] + else: + permission_code = f'{snake_model_name}:list' +%> +@permission_required('${permission_code}') +% endif +def list_${snake_model_name}(request): + """ + 获取${model_name}列表 + + 支持分页、搜索和排序 + """ + try: + queryset = ${model_name}.objects.all() + + # 搜索功能 + search = request.GET.get('search', '') + if search: + % if search_fields: + search_q = Q() + % for field in search_fields: + search_q |= Q(${field}__icontains=search) + % endfor + queryset = queryset.filter(search_q) + % else: + # 默认搜索字段,可根据需要调整 + queryset = queryset.filter( + Q(id__icontains=search) + ) + % endif + + # 排序功能 + ordering = request.GET.get('ordering', '-created_at') + % if ordering: + valid_orderings = [${', '.join([f"'{field}'" for field in ordering] + [f"'-{field}'" for field in ordering])}] + % else: + valid_orderings = ['created_at', '-created_at', 'updated_at', '-updated_at'] + % endif + if ordering in valid_orderings: + queryset = queryset.order_by(ordering) + + # 分页功能 + % if pagination: + page = int(request.GET.get('page', 1)) + page_size = int(request.GET.get('page_size', 20)) + paginator = Paginator(queryset, page_size) + page_obj = paginator.get_page(page) + + serializer = ${model_name}ListSerializer(page_obj.object_list, many=True) + + return HertzResponse.success( + data={ + 'results': serializer.data, + 'pagination': { + 'page': page, + 'page_size': page_size, + 'total_pages': paginator.num_pages, + 'total_count': paginator.count, + 'has_next': page_obj.has_next(), + 'has_previous': page_obj.has_previous(), + } + }, + message='获取${model_name}列表成功' + ) + % else: + serializer = ${model_name}ListSerializer(queryset, many=True) + return HertzResponse.success( + data=serializer.data, + message='获取${model_name}列表成功' + ) + % endif + except Exception as e: + return HertzResponse.error( + message='获取${model_name}列表失败', + error=str(e) + ) + + +% endif \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/test/apps.py b/hertz_studio_django_utils/code_generator/test/apps.py new file mode 100644 index 0000000..f6a17c8 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/test/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + +class hertz_studio_django_testConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'hertz_studio_django_test' + verbose_name = '测试应用' \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/test/models.py b/hertz_studio_django_utils/code_generator/test/models.py new file mode 100644 index 0000000..86bba74 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/test/models.py @@ -0,0 +1,30 @@ +from django.db import models +from hertz_studio_django_auth.models import HertzUser +import os + +class Testmodels(models.Model): + """ + 测试模型 + """ + # 测试字段 + test_field = models.CharField(max_length=100, verbose_name='测试字段') + name = models.CharField(max_length=50, unique=True, verbose_name='名称') + test_path = models.CharField(max_length=100, verbose_name='测试路径') + parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父项') + is_active = models.BooleanField(default=True, verbose_name='是否激活') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_at = models.DatetimeField(auto_now=True, verbose_name='更新时间') + + + class Meta: + db_table = test1 + verbose_name = '测试模型' + verbose_name_plural = '测试模型' + + def __str__(self): + return self.name + + def get_full_path(self): + if self.parent: + return f'{self.parent.get_full_path()} > {self.name}' + return self.name \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/test/serializers.py b/hertz_studio_django_utils/code_generator/test/serializers.py new file mode 100644 index 0000000..77f5dce --- /dev/null +++ b/hertz_studio_django_utils/code_generator/test/serializers.py @@ -0,0 +1,24 @@ +from rest_framework import serializers +from django.utils import timezone +from models import * + +class TestSerializer(serializers.ModelSerializer): + """ + 测试序列化器 + """ + parent_name = serializers.CharField(source='parent.name', read_only=True) + children_count = serializers.SerializerMethodField() + articales_count = serializers.SerializerMethodField() + full_path = serializers.CharField(source='get_full_path',read_only=True) + + class Meta: + model = Testmodels + fields=[ + 'name','parent','is_active','full_path','created_at','updated_at' + ] + read_only_fields = ['id', 'created_at', 'updated_at'] + + + def get_children_count(self, obj): + """获取子分类数量""" + return obj.children.filter(is_active=True).count() diff --git a/hertz_studio_django_utils/code_generator/test/urls.py b/hertz_studio_django_utils/code_generator/test/urls.py new file mode 100644 index 0000000..eb1c99c --- /dev/null +++ b/hertz_studio_django_utils/code_generator/test/urls.py @@ -0,0 +1,6 @@ +from django.urls import path, include +from views import * + +urlpatterns = [ + path('test/', test, name='test'), +] \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/test/views.py b/hertz_studio_django_utils/code_generator/test/views.py new file mode 100644 index 0000000..e69de29 diff --git a/hertz_studio_django_utils/code_generator/url_generator.py b/hertz_studio_django_utils/code_generator/url_generator.py new file mode 100644 index 0000000..7597cf3 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/url_generator.py @@ -0,0 +1,350 @@ +""" +Django URL配置代码生成器 + +该模块负责根据配置生成Django URL路由代码 +""" + +import os +from typing import Dict, List, Any, Optional +from .base_generator import BaseGenerator + + +class URLGenerator(BaseGenerator): + """Django URL配置代码生成器""" + + def __init__(self): + """初始化URL生成器""" + super().__init__() + + def generate(self, model_name: str = None, operations: List[str] = None, app_name: str = None, **kwargs) -> str: + """ + 生成Django URL配置代码 + + Args: + model_name: 模型名称 + operations: 支持的操作列表 + app_name: 应用名称 + **kwargs: 其他参数 + + Returns: + str: 生成的URL配置代码 + """ + # 处理参数,支持位置参数和关键字参数 + if model_name is None: + model_name = kwargs.get('model_name', 'DefaultModel') + if operations is None: + operations = kwargs.get('operations', ['list', 'create', 'retrieve', 'update', 'delete']) + if app_name is None: + app_name = kwargs.get('app_name', 'default_app') + + api_version = kwargs.get('api_version', 'v1') + prefix = kwargs.get('prefix', '') + namespace = kwargs.get('namespace', app_name) + + # 确保operations是列表 + if isinstance(operations, str): + operations = [operations] + + # 准备模板上下文 + context = { + 'app_name': app_name, + 'model_name': model_name, + 'model_name_lower': model_name.lower(), + 'snake_model_name': model_name.lower(), # 添加snake_model_name变量 + 'operations': operations, + 'api_version': api_version, + 'prefix': prefix, + 'namespace': namespace, + 'has_create': 'create' in operations, + 'has_list': 'list' in operations, + 'has_retrieve': 'retrieve' in operations, + 'has_update': 'update' in operations, + 'has_delete': 'delete' in operations, + 'url_patterns': [] + } + + # 生成URL模式 + for operation in operations: + if operation == 'list': + context['url_patterns'].append({ + 'pattern': f'{model_name.lower()}/', + 'view': f'list_{model_name.lower()}', + 'name': f'{model_name.lower()}_list' + }) + elif operation == 'create': + context['url_patterns'].append({ + 'pattern': f'{model_name.lower()}/create/', + 'view': f'create_{model_name.lower()}', + 'name': f'{model_name.lower()}_create' + }) + elif operation == 'retrieve' or operation == 'get': + context['url_patterns'].append({ + 'pattern': f'{model_name.lower()}//', + 'view': f'get_{model_name.lower()}', + 'name': f'{model_name.lower()}_detail' + }) + elif operation == 'update': + context['url_patterns'].append({ + 'pattern': f'{model_name.lower()}//update/', + 'view': f'update_{model_name.lower()}', + 'name': f'{model_name.lower()}_update' + }) + elif operation == 'delete': + context['url_patterns'].append({ + 'pattern': f'{model_name.lower()}//delete/', + 'view': f'delete_{model_name.lower()}', + 'name': f'{model_name.lower()}_delete' + }) + + # 渲染模板 + return self.render_template('django/urls.mako', context) + + def generate_rest_urls( + self, + app_name: str, + model_name: str, + viewset_name: Optional[str] = None, + api_version: str = 'v1', + namespace: Optional[str] = None + ) -> str: + """ + 生成REST风格的URL配置 + + Args: + app_name: 应用名称 + model_name: 模型名称 + viewset_name: ViewSet名称 + api_version: API版本 + namespace: 命名空间 + + Returns: + str: 生成的REST URL配置代码 + """ + if viewset_name is None: + viewset_name = f'{model_name}ViewSet' + + context = { + 'app_name': app_name, + 'model_name': model_name, + 'viewset_name': viewset_name, + 'api_version': api_version, + 'namespace': namespace, + 'url_type': 'rest' + } + + return self.render_template('django/urls.mako', context) + + def generate_router_urls( + self, + app_name: str, + viewsets: List[Dict[str, str]], + api_version: str = 'v1', + router_type: str = 'DefaultRouter' + ) -> str: + """ + 生成使用Router的URL配置 + + Args: + app_name: 应用名称 + viewsets: ViewSet配置列表 + api_version: API版本 + router_type: Router类型 + + Returns: + str: 生成的Router URL配置代码 + """ + context = { + 'app_name': app_name, + 'viewsets': viewsets, + 'api_version': api_version, + 'router_type': router_type, + 'url_type': 'router' + } + + return self.render_template('django/urls.mako', context) + + def generate_function_based_urls( + self, + app_name: str, + views: List[Dict[str, Any]], + api_version: str = 'v1' + ) -> str: + """ + 生成基于函数视图的URL配置 + + Args: + app_name: 应用名称 + views: 视图配置列表 + api_version: API版本 + + Returns: + str: 生成的函数视图URL配置代码 + """ + context = { + 'app_name': app_name, + 'views': views, + 'api_version': api_version, + 'url_type': 'function_based' + } + + return self.render_template('django/urls.mako', context) + + def generate_nested_urls( + self, + app_name: str, + parent_model: str, + child_model: str, + operations: List[str], + api_version: str = 'v1' + ) -> str: + """ + 生成嵌套资源的URL配置 + + Args: + app_name: 应用名称 + parent_model: 父模型名称 + child_model: 子模型名称 + operations: 支持的操作列表 + api_version: API版本 + + Returns: + str: 生成的嵌套URL配置代码 + """ + context = { + 'app_name': app_name, + 'parent_model': parent_model, + 'child_model': child_model, + 'operations': operations, + 'api_version': api_version, + 'url_type': 'nested' + } + + return self.render_template('django/urls.mako', context) + + def generate_custom_action_urls( + self, + app_name: str, + model_name: str, + actions: List[Dict[str, Any]], + api_version: str = 'v1' + ) -> str: + """ + 生成自定义动作的URL配置 + + Args: + app_name: 应用名称 + model_name: 模型名称 + actions: 自定义动作配置列表 + api_version: API版本 + + Returns: + str: 生成的自定义动作URL配置代码 + """ + context = { + 'app_name': app_name, + 'model_name': model_name, + 'actions': actions, + 'api_version': api_version, + 'url_type': 'custom_actions' + } + + return self.render_template('django/urls.mako', context) + + def generate_app_urls( + self, + app_name: str, + models: List[str], + api_version: str = 'v1', + include_admin: bool = False + ) -> str: + """ + 生成应用级别的URL配置 + + Args: + app_name: 应用名称 + models: 模型名称列表 + api_version: API版本 + include_admin: 是否包含管理后台URL + + Returns: + str: 生成的应用URL配置代码 + """ + context = { + 'app_name': app_name, + 'models': models, + 'api_version': api_version, + 'include_admin': include_admin, + 'url_type': 'app_level' + } + + return self.render_template('django/urls.mako', context) + + def add_url_pattern( + self, + url_code: str, + pattern: str, + view: str, + name: str, + methods: Optional[List[str]] = None + ) -> str: + """ + 添加URL模式 + + Args: + url_code: 原URL配置代码 + pattern: URL模式 + view: 视图名称 + name: URL名称 + methods: HTTP方法列表 + + Returns: + str: 包含新URL模式的配置代码 + """ + if methods: + method_str = f", methods={methods}" + else: + method_str = "" + + new_pattern = f" path('{pattern}', {view}, name='{name}'{method_str})," + + # 在urlpatterns列表中添加新模式 + lines = url_code.split('\n') + for i, line in enumerate(lines): + if 'urlpatterns = [' in line: + # 找到列表结束位置 + for j in range(i + 1, len(lines)): + if lines[j].strip() == ']': + lines.insert(j, new_pattern) + break + break + + return '\n'.join(lines) + + def generate_api_documentation_urls( + self, + app_name: str, + api_version: str = 'v1', + include_swagger: bool = True, + include_redoc: bool = True + ) -> str: + """ + 生成API文档的URL配置 + + Args: + app_name: 应用名称 + api_version: API版本 + include_swagger: 是否包含Swagger UI + include_redoc: 是否包含ReDoc + + Returns: + str: 生成的API文档URL配置代码 + """ + context = { + 'app_name': app_name, + 'api_version': api_version, + 'include_swagger': include_swagger, + 'include_redoc': include_redoc, + 'url_type': 'api_docs' + } + + return self.render_template('django/urls.mako', context) \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/view_generator.py b/hertz_studio_django_utils/code_generator/view_generator.py new file mode 100644 index 0000000..c509408 --- /dev/null +++ b/hertz_studio_django_utils/code_generator/view_generator.py @@ -0,0 +1,416 @@ +""" +Django视图代码生成器 + +该模块负责根据配置生成Django REST Framework视图代码 +""" + +import os +from typing import Dict, List, Any, Optional +from .base_generator import BaseGenerator + + +class ViewGenerator(BaseGenerator): + """Django视图代码生成器""" + + def __init__(self): + """初始化视图生成器""" + super().__init__() + + def generate(self, model_name: str = None, operations: List[str] = None, **kwargs) -> str: + """ + 生成Django视图代码 + + Args: + model_name: 模型名称 + operations: 支持的操作列表 + **kwargs: 其他参数 + + Returns: + str: 生成的视图代码 + """ + # 处理参数,支持位置参数和关键字参数 + if model_name is None: + model_name = kwargs.get('model_name', 'DefaultModel') + if operations is None: + operations = kwargs.get('operations', ['list', 'create', 'retrieve', 'update', 'delete']) + + permissions = kwargs.get('permissions', {}) + authentication = kwargs.get('authentication', ['IsAuthenticated']) + pagination = kwargs.get('pagination', True) + filters = kwargs.get('filters', {}) + permission_type = kwargs.get('permission_type', 'standard') # 'standard' 或 'system' + + # 确保operations是列表 + if isinstance(operations, str): + operations = [operations] + + # 自动生成权限配置 + if not permissions and permission_type: + if permission_type == 'system': + permissions = self.generate_system_permission_config(model_name, operations) + else: + permissions = self.generate_permission_config(model_name, operations) + + # 准备模板上下文 + context = { + 'model_name': model_name, + 'model_name_lower': model_name.lower(), + 'operations': operations, + 'permissions': permissions, + 'permissions_list': permissions, # 为了兼容模板 + 'authentication': authentication, + 'pagination': pagination, + 'filters': filters, + 'has_create': 'create' in operations, + 'has_list': 'list' in operations, + 'has_retrieve': 'retrieve' in operations, + 'has_update': 'update' in operations, + 'has_delete': 'delete' in operations, + 'viewset_name': f'{model_name}ViewSet', + 'serializer_name': f'{model_name}Serializer', + 'queryset_name': f'{model_name}.objects.all()', + 'view_classes': [] + } + + # 生成视图类列表 + for operation in operations: + if operation == 'list': + context['view_classes'].append({ + 'name': f'{model_name}ListView', + 'base_class': 'ListAPIView', + 'operation': 'list' + }) + elif operation == 'create': + context['view_classes'].append({ + 'name': f'{model_name}CreateView', + 'base_class': 'CreateAPIView', + 'operation': 'create' + }) + elif operation == 'retrieve' or operation == 'get': + context['view_classes'].append({ + 'name': f'{model_name}DetailView', + 'base_class': 'RetrieveAPIView', + 'operation': 'retrieve' + }) + elif operation == 'update': + context['view_classes'].append({ + 'name': f'{model_name}UpdateView', + 'base_class': 'UpdateAPIView', + 'operation': 'update' + }) + elif operation == 'delete': + context['view_classes'].append({ + 'name': f'{model_name}DeleteView', + 'base_class': 'DestroyAPIView', + 'operation': 'delete' + }) + + # 渲染模板 + return self.render_template('django/views.mako', context) + + def generate_api_view( + self, + model_name: str, + view_name: str, + http_methods: List[str], + permissions: Optional[List[str]] = None, + authentication: Optional[List[str]] = None + ) -> str: + """ + 生成API视图代码 + + Args: + model_name: 模型名称 + view_name: 视图名称 + http_methods: HTTP方法列表 + permissions: 权限列表 + authentication: 认证方式列表 + + Returns: + str: 生成的API视图代码 + """ + context = { + 'model_name': model_name, + 'view_name': view_name, + 'http_methods': http_methods, + 'permissions': permissions or [], + 'authentication': authentication or [], + 'view_type': 'api_view' + } + + return self.render_template('django/views.mako', context) + + def generate_viewset( + self, + model_name: str, + viewset_type: str = 'ModelViewSet', + operations: Optional[List[str]] = None, + permissions: Optional[List[str]] = None, + filters: Optional[Dict[str, Any]] = None + ) -> str: + """ + 生成ViewSet代码 + + Args: + model_name: 模型名称 + viewset_type: ViewSet类型 + operations: 支持的操作列表 + permissions: 权限列表 + filters: 过滤器配置 + + Returns: + str: 生成的ViewSet代码 + """ + if operations is None: + operations = ['list', 'create', 'retrieve', 'update', 'destroy'] + + context = { + 'model_name': model_name, + 'viewset_type': viewset_type, + 'operations': operations, + 'permissions': permissions or [], + 'filters': filters or {}, + 'view_type': 'viewset' + } + + return self.render_template('django/views.mako', context) + + def generate_generic_view( + self, + model_name: str, + view_type: str, + permissions: Optional[List[str]] = None, + filters: Optional[Dict[str, Any]] = None + ) -> str: + """ + 生成通用视图代码 + + Args: + model_name: 模型名称 + view_type: 视图类型 (ListAPIView, CreateAPIView等) + permissions: 权限列表 + filters: 过滤器配置 + + Returns: + str: 生成的通用视图代码 + """ + context = { + 'model_name': model_name, + 'view_type': view_type, + 'permissions': permissions or [], + 'filters': filters or {}, + 'generic_view': True + } + + return self.render_template('django/views.mako', context) + + def generate_crud_views( + self, + model_name: str, + operations: List[str], + permissions: Optional[Dict[str, List[str]]] = None, + pagination: bool = True, + filters: Optional[Dict[str, Any]] = None + ) -> str: + """ + 生成CRUD视图代码 + + Args: + model_name: 模型名称 + operations: 支持的操作列表 + permissions: 每个操作的权限配置 + pagination: 是否启用分页 + filters: 过滤器配置 + + Returns: + str: 生成的CRUD视图代码 + """ + context = { + 'model_name': model_name, + 'operations': operations, + 'permissions': permissions or {}, + 'pagination': pagination, + 'filters': filters or {}, + 'crud_views': True + } + + return self.render_template('django/views.mako', context) + + def generate_custom_action( + self, + action_name: str, + http_methods: List[str], + detail: bool = False, + permissions: Optional[List[str]] = None, + serializer_class: Optional[str] = None + ) -> str: + """ + 生成自定义动作代码 + + Args: + action_name: 动作名称 + http_methods: HTTP方法列表 + detail: 是否为详情动作 + permissions: 权限列表 + serializer_class: 序列化器类名 + + Returns: + str: 生成的自定义动作代码 + """ + context = { + 'action_name': action_name, + 'http_methods': http_methods, + 'detail': detail, + 'permissions': permissions or [], + 'serializer_class': serializer_class, + 'custom_action': True + } + + return self.render_template('django/views.mako', context) + + def add_permission_decorator( + self, + view_code: str, + permissions: List[str] + ) -> str: + """ + 添加权限装饰器 + + Args: + view_code: 原视图代码 + permissions: 权限列表 + + Returns: + str: 包含权限装饰器的视图代码 + """ + decorators = [] + for permission in permissions: + if permission == 'login_required': + decorators.append('@login_required') + elif permission == 'no_login_required': + decorators.append('@no_login_required') + else: + decorators.append(f'@permission_required("{permission}")') + + # 在函数定义前添加装饰器 + lines = view_code.split('\n') + for i, line in enumerate(lines): + if line.strip().startswith('def ') or line.strip().startswith('class '): + # 在函数或类定义前插入装饰器 + for j, decorator in enumerate(decorators): + lines.insert(i + j, decorator) + break + + return '\n'.join(lines) + + def generate_permission_config( + self, + model_name: str, + operations: List[str], + permission_prefix: str = None + ) -> Dict[str, str]: + """ + 生成权限配置字典 + + Args: + model_name: 模型名称 + operations: 操作列表 + permission_prefix: 权限前缀,默认使用模型名称小写 + + Returns: + Dict[str, str]: 权限配置字典 + """ + if permission_prefix is None: + permission_prefix = model_name.lower() + + # 统一权限映射规则,与菜单生成器保持一致 + permission_mapping = { + 'list': f'{permission_prefix}:list', + 'create': f'{permission_prefix}:add', # 改为add,与菜单一致 + 'retrieve': f'{permission_prefix}:list', # 改为list,与菜单一致 + 'get': f'{permission_prefix}:list', # 改为list,与菜单一致 + 'update': f'{permission_prefix}:edit', # 改为edit,与菜单一致 + 'delete': f'{permission_prefix}:remove', # 改为remove,与菜单一致 + 'partial_update': f'{permission_prefix}:edit' # 改为edit,与菜单一致 + } + + return {op: permission_mapping.get(op, f'{permission_prefix}:{op}') for op in operations} + + def generate_system_permission_config( + self, + model_name: str, + operations: List[str], + module_prefix: str = 'system' + ) -> Dict[str, str]: + """ + 生成系统级权限配置字典 + + Args: + model_name: 模型名称 + operations: 操作列表 + module_prefix: 模块前缀,默认为'system' + + Returns: + Dict[str, str]: 系统级权限配置字典 + """ + model_lower = model_name.lower() + + permission_mapping = { + 'list': f'{module_prefix}:{model_lower}:list', + 'create': f'{model_lower}:create', + 'retrieve': f'{module_prefix}:{model_lower}:query', + 'get': f'{module_prefix}:{model_lower}:query', + 'update': f'{model_lower}:update', + 'delete': f'{model_lower}:delete', + 'partial_update': f'{model_lower}:update' + } + + return {op: permission_mapping.get(op, f'{module_prefix}:{model_lower}:{op}') for op in operations} + + def add_swagger_documentation( + self, + view_code: str, + operation_id: str, + summary: str, + description: str, + tags: List[str], + request_schema: Optional[str] = None, + response_schema: Optional[str] = None + ) -> str: + """ + 添加Swagger文档注解 + + Args: + view_code: 原视图代码 + operation_id: 操作ID + summary: 摘要 + description: 描述 + tags: 标签列表 + request_schema: 请求模式 + response_schema: 响应模式 + + Returns: + str: 包含Swagger文档的视图代码 + """ + swagger_decorator = f"""@extend_schema( + operation_id='{operation_id}', + summary='{summary}', + description='{description}', + tags={tags}""" + + if request_schema: + swagger_decorator += f",\n request={request_schema}" + + if response_schema: + swagger_decorator += f",\n responses={{200: {response_schema}}}" + + swagger_decorator += "\n)" + + # 在函数定义前添加装饰器 + lines = view_code.split('\n') + for i, line in enumerate(lines): + if line.strip().startswith('def '): + lines.insert(i, swagger_decorator) + break + + return '\n'.join(lines) \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/yaml_generator_cli.py b/hertz_studio_django_utils/code_generator/yaml_generator_cli.py new file mode 100644 index 0000000..28a00fc --- /dev/null +++ b/hertz_studio_django_utils/code_generator/yaml_generator_cli.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +""" +YAML Django代码生成器命令行工具 + +该脚本提供命令行接口,用于从YAML配置文件生成Django应用代码。 + +使用示例: + python yaml_generator_cli.py generate app_config.yaml + python yaml_generator_cli.py template --output my_app.yaml + python yaml_generator_cli.py validate app_config.yaml +""" + +import argparse +import sys +import os +from pathlib import Path +from django_code_generator import DjangoCodeGenerator + + +def generate_from_yaml(yaml_file_path: str, output_dir: str = None) -> bool: + """ + 从YAML文件生成Django应用代码 + + Args: + yaml_file_path: YAML配置文件路径 + output_dir: 输出目录(可选) + + Returns: + bool: 生成是否成功 + """ + try: + generator = DjangoCodeGenerator() + + # 解析YAML配置 + config = generator.yaml_parser.parse_yaml_file(yaml_file_path) + + # 如果指定了输出目录,覆盖配置中的设置 + if output_dir: + config['output_dir'] = output_dir + + print(f"正在从 {yaml_file_path} 生成Django应用代码...") + print(f"应用名称: {config['app_name']}") + print(f"输出目录: {config['output_dir']}") + print(f"模型数量: {len(config['models'])}") + + # 生成代码 + generated_files = generator.generate_from_yaml_config(config) + + print(f"\n✅ 成功生成 {len(generated_files)} 个文件:") + for file_path in sorted(generated_files.keys()): + print(f" 📄 {file_path}") + + print(f"\n🎉 Django应用 '{config['app_name']}' 生成完成!") + print(f"📁 输出目录: {os.path.abspath(config['output_dir'])}") + + return True + + except FileNotFoundError as e: + print(f"❌ 错误: {e}") + return False + except Exception as e: + print(f"❌ 生成失败: {e}") + return False + + +def generate_template(output_path: str = 'app_template.yaml') -> bool: + """ + 生成YAML配置文件模板 + + Args: + output_path: 输出文件路径 + + Returns: + bool: 生成是否成功 + """ + try: + generator = DjangoCodeGenerator() + template_content = generator.generate_yaml_template(output_path) + + print(f"✅ YAML配置模板已生成: {os.path.abspath(output_path)}") + print("\n📝 模板内容预览:") + print("-" * 50) + # 显示前20行 + lines = template_content.split('\n') + for i, line in enumerate(lines[:20]): + print(f"{i+1:2d}: {line}") + if len(lines) > 20: + print(f"... (还有 {len(lines) - 20} 行)") + print("-" * 50) + + return True + + except Exception as e: + print(f"❌ 模板生成失败: {e}") + return False + + +def validate_yaml(yaml_file_path: str) -> bool: + """ + 验证YAML配置文件 + + Args: + yaml_file_path: YAML配置文件路径 + + Returns: + bool: 验证是否通过 + """ + try: + generator = DjangoCodeGenerator() + config = generator.yaml_parser.parse_yaml_file(yaml_file_path) + + print(f"✅ YAML配置文件验证通过: {yaml_file_path}") + print(f"📋 配置摘要:") + print(f" 应用名称: {config['app_name']}") + print(f" 版本: {config.get('version', 'N/A')}") + print(f" 描述: {config.get('description', 'N/A')}") + print(f" 作者: {config.get('author', 'N/A')}") + print(f" 输出目录: {config['output_dir']}") + print(f" 模型数量: {len(config['models'])}") + + print(f"\n📊 模型详情:") + for i, model in enumerate(config['models'], 1): + print(f" {i}. {model['name']} ({len(model['fields'])} 个字段)") + operations = model.get('operations', []) + if operations: + print(f" 操作: {', '.join(operations)}") + + return True + + except Exception as e: + print(f"❌ YAML配置验证失败: {e}") + return False + + +def main(): + """主函数""" + parser = argparse.ArgumentParser( + description='YAML Django代码生成器', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +使用示例: + %(prog)s generate app_config.yaml # 从YAML生成代码 + %(prog)s generate app_config.yaml -o ./my_app # 指定输出目录 + %(prog)s template # 生成默认模板 + %(prog)s template -o my_template.yaml # 生成自定义模板 + %(prog)s validate app_config.yaml # 验证YAML配置 + """ + ) + + subparsers = parser.add_subparsers(dest='command', help='可用命令') + + # generate 子命令 + generate_parser = subparsers.add_parser('generate', help='从YAML文件生成Django应用代码') + generate_parser.add_argument('yaml_file', help='YAML配置文件路径') + generate_parser.add_argument('-o', '--output', help='输出目录') + + # template 子命令 + template_parser = subparsers.add_parser('template', help='生成YAML配置文件模板') + template_parser.add_argument('-o', '--output', default='app_template.yaml', help='输出文件路径') + + # validate 子命令 + validate_parser = subparsers.add_parser('validate', help='验证YAML配置文件') + validate_parser.add_argument('yaml_file', help='YAML配置文件路径') + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return 1 + + success = False + + if args.command == 'generate': + success = generate_from_yaml(args.yaml_file, args.output) + elif args.command == 'template': + success = generate_template(args.output) + elif args.command == 'validate': + success = validate_yaml(args.yaml_file) + + return 0 if success else 1 + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/hertz_studio_django_utils/code_generator/yaml_parser.py b/hertz_studio_django_utils/code_generator/yaml_parser.py new file mode 100644 index 0000000..6795c6f --- /dev/null +++ b/hertz_studio_django_utils/code_generator/yaml_parser.py @@ -0,0 +1,372 @@ +""" +YAML配置文件解析器 + +该模块负责解析YAML配置文件,将其转换为Django代码生成器可以使用的数据结构。 + +使用示例: + parser = YAMLParser() + config = parser.parse_yaml_file('app_config.yaml') + generator = DjangoCodeGenerator() + generator.generate_from_yaml_config(config) +""" + +import yaml +import os +from typing import Dict, List, Any, Optional +from pathlib import Path + + +class YAMLParser: + """YAML配置文件解析器""" + + def __init__(self): + """初始化YAML解析器""" + # 支持Django字段类型 + self.supported_field_types = { + 'CharField', 'TextField', 'IntegerField', 'FloatField', + 'DecimalField', 'BooleanField', 'DateField', 'DateTimeField', + 'EmailField', 'URLField', 'ImageField', 'FileField', + 'ForeignKey', 'ManyToManyField', 'OneToOneField', 'JSONField', + 'SlugField', 'PositiveIntegerField', 'BigIntegerField', + 'SmallIntegerField', 'UUIDField', 'TimeField', + 'GenericIPAddressField', 'BinaryField', 'DurationField', + # 支持通用字段类型 + 'string', 'text', 'integer', 'float', 'decimal', 'boolean', + 'date', 'datetime', 'email', 'url', 'image', 'file', 'json', + 'slug', 'uuid', 'time', 'ip', 'binary', 'duration' + } + + # 通用字段类型到Django字段类型的映射 + self.field_type_mapping = { + 'string': 'CharField', + 'text': 'TextField', + 'integer': 'IntegerField', + 'float': 'FloatField', + 'decimal': 'DecimalField', + 'boolean': 'BooleanField', + 'date': 'DateField', + 'datetime': 'DateTimeField', + 'email': 'EmailField', + 'url': 'URLField', + 'image': 'ImageField', + 'file': 'FileField', + 'json': 'JSONField', + 'slug': 'SlugField', + 'uuid': 'UUIDField', + 'time': 'TimeField', + 'ip': 'GenericIPAddressField', + 'binary': 'BinaryField', + 'duration': 'DurationField' + } + + self.supported_operations = { + 'create', 'read', 'update', 'delete', 'list', 'search', 'filter', 'get', 'retrieve' + } + + def parse_yaml_file(self, yaml_file_path: str) -> Dict[str, Any]: + """ + 解析YAML配置文件 + + Args: + yaml_file_path: YAML文件路径 + + Returns: + Dict[str, Any]: 解析后的配置字典 + + Raises: + FileNotFoundError: 当文件不存在时 + yaml.YAMLError: 当YAML格式错误时 + ValueError: 当配置验证失败时 + """ + if not os.path.exists(yaml_file_path): + raise FileNotFoundError(f"YAML配置文件不存在: {yaml_file_path}") + + try: + with open(yaml_file_path, 'r', encoding='utf-8') as file: + config = yaml.safe_load(file) + except yaml.YAMLError as e: + raise yaml.YAMLError(f"YAML文件格式错误: {e}") + + # 验证配置结构 + validated_config = self._validate_config(config) + return validated_config + + def _validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """ + 验证YAML配置结构 + + Args: + config: 原始配置字典 + + Returns: + Dict[str, Any]: 验证后的配置字典 + + Raises: + ValueError: 当配置验证失败时 + """ + if not isinstance(config, dict): + raise ValueError("YAML配置必须是字典格式") + + # 验证必需的顶级字段 + required_fields = ['app_name', 'models'] + for field in required_fields: + if field not in config: + raise ValueError(f"缺少必需字段: {field}") + + # 验证应用名称 + app_name = config['app_name'] + if not isinstance(app_name, str) or not app_name.strip(): + raise ValueError("app_name必须是非空字符串") + + # 验证模型配置 + models = config['models'] + if isinstance(models, dict): + # 如果models是字典格式,转换为列表格式 + models_list = [] + for model_name, model_config in models.items(): + model_config['name'] = model_name + models_list.append(model_config) + models = models_list + elif not isinstance(models, list) or len(models) == 0: + raise ValueError("models必须是非空列表或字典") + + validated_models = [] + for i, model in enumerate(models): + validated_model = self._validate_model_config(model, i) + validated_models.append(validated_model) + + # 构建验证后的配置 + validated_config = { + 'app_name': app_name.strip(), + 'models': validated_models, + 'output_dir': config.get('output_dir', './generated_code'), + 'version': config.get('version', '1.0.0'), + 'description': config.get('description', ''), + 'author': config.get('author', ''), + 'global_settings': config.get('global_settings', {}) + } + + return validated_config + + def _validate_model_config(self, model: Dict[str, Any], index: int) -> Dict[str, Any]: + """ + 验证单个模型配置 + + Args: + model: 模型配置字典 + index: 模型在列表中的索引 + + Returns: + Dict[str, Any]: 验证后的模型配置 + + Raises: + ValueError: 当模型配置验证失败时 + """ + if not isinstance(model, dict): + raise ValueError(f"模型配置[{index}]必须是字典格式") + + # 验证必需字段 + if 'name' not in model: + raise ValueError(f"模型配置[{index}]缺少必需字段: name") + + if 'fields' not in model: + raise ValueError(f"模型配置[{index}]缺少必需字段: fields") + + model_name = model['name'] + if not isinstance(model_name, str) or not model_name.strip(): + raise ValueError(f"模型配置[{index}]的name必须是非空字符串") + + # 验证字段配置 + fields = model['fields'] + if not isinstance(fields, list) or len(fields) == 0: + raise ValueError(f"模型{model_name}的fields必须是非空列表") + + validated_fields = [] + for j, field in enumerate(fields): + validated_field = self._validate_field_config(field, model_name, j) + validated_fields.append(validated_field) + + # 验证操作配置 + operations = model.get('operations', ['create', 'read', 'update', 'delete', 'list']) + if not isinstance(operations, list): + raise ValueError(f"模型{model_name}的operations必须是列表") + + for op in operations: + if op not in self.supported_operations: + raise ValueError(f"模型{model_name}包含不支持的操作: {op}") + + # 构建验证后的模型配置 + validated_model = { + 'name': model_name.strip(), + 'fields': validated_fields, + 'operations': operations, + 'table_name': model.get('table_name'), + 'verbose_name': model.get('verbose_name', model_name), + 'verbose_name_plural': model.get('verbose_name_plural'), + 'ordering': model.get('ordering', []), + 'permissions': model.get('permissions', []), + 'validators': model.get('validators', {}), + 'meta_options': model.get('meta_options', {}), + 'admin_config': model.get('admin_config', {}), + 'api_config': model.get('api_config', {}) + } + + return validated_model + + def _validate_field_config(self, field: Dict[str, Any], model_name: str, index: int) -> Dict[str, Any]: + """ + 验证单个字段配置 + + Args: + field: 字段配置字典 + model_name: 所属模型名称 + index: 字段在列表中的索引 + + Returns: + Dict[str, Any]: 验证后的字段配置 + + Raises: + ValueError: 当字段配置验证失败时 + """ + if not isinstance(field, dict): + raise ValueError(f"模型{model_name}的字段配置[{index}]必须是字典格式") + + # 验证必需字段 + if 'name' not in field: + raise ValueError(f"模型{model_name}的字段配置[{index}]缺少必需字段: name") + + if 'type' not in field: + raise ValueError(f"模型{model_name}的字段配置[{index}]缺少必需字段: type") + + field_name = field['name'] + field_type = field['type'] + + if not isinstance(field_name, str) or not field_name.strip(): + raise ValueError(f"模型{model_name}的字段配置[{index}]的name必须是非空字符串") + + # 检查字段类型是否支持(包括通用类型和Django类型) + if field_type not in self.supported_field_types and field_type not in self.field_type_mapping: + raise ValueError(f"模型{model_name}的字段{field_name}使用了不支持的类型: {field_type}") + + # 如果是通用类型,转换为Django字段类型 + django_field_type = self.field_type_mapping.get(field_type, field_type) + # 移除调试输出 + print(f"字段类型转换: {field_type} -> {django_field_type}") + + # 构建验证后的字段配置 + validated_field = { + 'name': field['name'], + 'type': django_field_type, # 使用转换后的Django字段类型 + 'verbose_name': field.get('verbose_name', field['name']), + 'help_text': field.get('help_text', f"{field['name']}字段"), + 'required': field.get('required', False), + 'null': not field.get('required', False), + 'blank': not field.get('required', False) + } + + # 添加其他字段属性 + for key in ['max_length', 'default', 'unique', 'choices', 'max_digits', 'decimal_places', 'upload_to']: + if key in field: + validated_field[key] = field[key] + + # 移除None值 + validated_field = {k: v for k, v in validated_field.items() if v is not None} + + return validated_field + + def generate_yaml_template(self, output_path: str = 'app_template.yaml') -> str: + """ + 生成YAML配置文件模板 + + Args: + output_path: 输出文件路径 + + Returns: + str: 生成的模板内容 + """ + template = { + 'app_name': 'hertz_studio_django_example', + 'version': '1.0.0', + 'description': 'Django应用示例', + 'author': 'Hertz Studio', + 'output_dir': './generated_code', + 'global_settings': { + 'use_uuid_primary_key': False, + 'add_created_updated_fields': True, + 'add_status_field': True, + 'default_permissions': ['add', 'change', 'delete', 'view'] + }, + 'models': [ + { + 'name': 'User', + 'verbose_name': '用户', + 'verbose_name_plural': '用户列表', + 'table_name': 'example_user', + 'ordering': ['-created_at'], + 'fields': [ + { + 'name': 'username', + 'type': 'CharField', + 'max_length': 150, + 'unique': True, + 'verbose_name': '用户名', + 'help_text': '用户登录名' + }, + { + 'name': 'email', + 'type': 'EmailField', + 'unique': True, + 'verbose_name': '邮箱', + 'help_text': '用户邮箱地址' + }, + { + 'name': 'phone', + 'type': 'CharField', + 'max_length': 20, + 'blank': True, + 'null': True, + 'verbose_name': '手机号', + 'help_text': '用户手机号码' + }, + { + 'name': 'avatar', + 'type': 'ImageField', + 'upload_to': 'avatars/', + 'blank': True, + 'null': True, + 'verbose_name': '头像', + 'help_text': '用户头像图片' + }, + { + 'name': 'is_active', + 'type': 'BooleanField', + 'default': True, + 'verbose_name': '是否激活', + 'help_text': '用户账户是否激活' + } + ], + 'operations': ['create', 'read', 'update', 'delete', 'list', 'search'], + 'permissions': ['add_user', 'change_user', 'delete_user', 'view_user'], + 'validators': { + 'username': 'validate_username', + 'email': 'validate_email' + }, + 'admin_config': { + 'list_display': ['username', 'email', 'is_active', 'created_at'], + 'list_filter': ['is_active', 'created_at'], + 'search_fields': ['username', 'email'] + }, + 'api_config': { + 'pagination': True, + 'filters': ['is_active'], + 'search_fields': ['username', 'email'], + 'ordering_fields': ['username', 'created_at'] + } + } + ] + } + + with open(output_path, 'w', encoding='utf-8') as file: + yaml.dump(template, file, default_flow_style=False, allow_unicode=True, indent=2) + + return yaml.dump(template, default_flow_style=False, allow_unicode=True, indent=2) \ No newline at end of file diff --git a/hertz_studio_django_utils/config/__init__.py b/hertz_studio_django_utils/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hertz_studio_django_utils/config/departments_config.py b/hertz_studio_django_utils/config/departments_config.py new file mode 100644 index 0000000..f6258fb --- /dev/null +++ b/hertz_studio_django_utils/config/departments_config.py @@ -0,0 +1,24 @@ +departments = [ + { + 'dept_name': 'Hertz科技', + 'dept_code': 'hertz_tech', + 'leader': '超级管理员', + 'email': 'admin@hertz.com', + 'sort_order': 1, + 'parent_id': None + }, + { + 'dept_name': '技术部', + 'dept_code': 'tech_dept', + 'leader': '技术总监', + 'sort_order': 1, + 'parent_code': 'hertz_tech' + }, + { + 'dept_name': '运营部', + 'dept_code': 'ops_dept', + 'leader': '运营总监', + 'sort_order': 2, + 'parent_code': 'hertz_tech' + } +] \ No newline at end of file diff --git a/hertz_studio_django_utils/config/menus_config.py b/hertz_studio_django_utils/config/menus_config.py new file mode 100644 index 0000000..0e3fc80 --- /dev/null +++ b/hertz_studio_django_utils/config/menus_config.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python +""" +菜单配置文件 +包含系统所有菜单配置和动态菜单生成功能 +""" + +from typing import List, Dict, Any, Optional + + +def add_new_menus(new_menus: List[Dict[str, Any]]) -> None: + """ + 动态添加新菜单到配置中 + + Args: + new_menus: 新菜单配置列表 + """ + global menus + menus.extend(new_menus) + print(f"已添加 {len(new_menus)} 个新菜单到配置中") + + +def get_menus_by_parent(parent_code: Optional[str] = None) -> List[Dict[str, Any]]: + """ + 根据父级代码获取菜单列表 + + Args: + parent_code: 父级菜单代码,None表示获取顶级菜单 + + Returns: + List[Dict]: 菜单列表 + """ + return [menu for menu in menus if menu.get('parent_code') == parent_code] + + +def get_menu_by_code(menu_code: str) -> Optional[Dict[str, Any]]: + """ + 根据菜单代码获取菜单配置 + + Args: + menu_code: 菜单代码 + + Returns: + Optional[Dict]: 菜单配置,未找到返回None + """ + for menu in menus: + if menu.get('menu_code') == menu_code: + return menu + return None + + +def generate_menu_permissions() -> List[str]: + """ + 生成所有菜单权限列表 + + Returns: + List[str]: 权限代码列表 + """ + permissions = [] + for menu in menus: + if menu.get('permission'): + permissions.append(menu['permission']) + return permissions + + +menus = [ + # 系统管理目录 + { + 'menu_name': '系统管理', + 'menu_code': 'system', + 'menu_type': 1, # 目录 + 'path': '/system', + 'icon': 'system', + 'sort_order': 1, + 'parent_code': None + }, + + # 用户管理菜单 + { + 'menu_name': '用户管理', + 'menu_code': 'system:user', + 'menu_type': 2, # 菜单 + 'path': '/system/user', + 'component': 'system/user/index', + 'icon': 'user', + 'permission': 'system:user:list', + 'sort_order': 1, + 'parent_code': 'system' + }, + { + 'menu_name': '用户查询', + 'menu_code': 'system:user:query', + 'menu_type': 3, # 按钮 + 'permission': 'system:user:query', + 'sort_order': 1, + 'parent_code': 'system:user' + }, + { + 'menu_name': '用户新增', + 'menu_code': 'system:user:add', + 'menu_type': 3, + 'permission': 'system:user:add', + 'sort_order': 2, + 'parent_code': 'system:user' + }, + { + 'menu_name': '用户修改', + 'menu_code': 'system:user:edit', + 'menu_type': 3, + 'permission': 'system:user:edit', + 'sort_order': 3, + 'parent_code': 'system:user' + }, + { + 'menu_name': '用户删除', + 'menu_code': 'system:user:remove', + 'menu_type': 3, + 'permission': 'system:user:remove', + 'sort_order': 4, + 'parent_code': 'system:user' + }, + { + 'menu_name': '分配角色', + 'menu_code': 'system:user:role', + 'menu_type': 3, + 'permission': 'system:user:role', + 'sort_order': 5, + 'parent_code': 'system:user' + }, + + # 角色管理菜单 + { + 'menu_name': '角色管理', + 'menu_code': 'system:role', + 'menu_type': 2, + 'path': '/system/role', + 'component': 'system/role/index', + 'icon': 'role', + 'permission': 'system:role:list', + 'sort_order': 2, + 'parent_code': 'system' + }, + { + 'menu_name': '角色查询', + 'menu_code': 'system:role:query', + 'menu_type': 3, + 'permission': 'system:role:query', + 'sort_order': 1, + 'parent_code': 'system:role' + }, + { + 'menu_name': '角色新增', + 'menu_code': 'system:role:add', + 'menu_type': 3, + 'permission': 'system:role:add', + 'sort_order': 2, + 'parent_code': 'system:role' + }, + { + 'menu_name': '角色修改', + 'menu_code': 'system:role:edit', + 'menu_type': 3, + 'permission': 'system:role:edit', + 'sort_order': 3, + 'parent_code': 'system:role' + }, + { + 'menu_name': '角色删除', + 'menu_code': 'system:role:remove', + 'menu_type': 3, + 'permission': 'system:role:remove', + 'sort_order': 4, + 'parent_code': 'system:role' + }, + { + 'menu_name': '分配权限', + 'menu_code': 'system:role:menu', + 'menu_type': 3, + 'permission': 'system:role:menu', + 'sort_order': 5, + 'parent_code': 'system:role' + }, + + # 菜单管理菜单 + { + 'menu_name': '菜单管理', + 'menu_code': 'system:menu', + 'menu_type': 2, + 'path': '/system/menu', + 'component': 'system/menu/index', + 'icon': 'menu', + 'permission': 'system:menu:list', + 'sort_order': 3, + 'parent_code': 'system' + }, + { + 'menu_name': '菜单查询', + 'menu_code': 'system:menu:query', + 'menu_type': 3, + 'permission': 'system:menu:query', + 'sort_order': 1, + 'parent_code': 'system:menu' + }, + { + 'menu_name': '菜单新增', + 'menu_code': 'system:menu:add', + 'menu_type': 3, + 'permission': 'system:menu:add', + 'sort_order': 2, + 'parent_code': 'system:menu' + }, + { + 'menu_name': '菜单修改', + 'menu_code': 'system:menu:edit', + 'menu_type': 3, + 'permission': 'system:menu:edit', + 'sort_order': 3, + 'parent_code': 'system:menu' + }, + { + 'menu_name': '菜单删除', + 'menu_code': 'system:menu:remove', + 'menu_type': 3, + 'permission': 'system:menu:remove', + 'sort_order': 4, + 'parent_code': 'system:menu' + }, + + # 部门管理菜单 + { + 'menu_name': '部门管理', + 'menu_code': 'system:dept', + 'menu_type': 2, + 'path': '/system/dept', + 'component': 'system/dept/index', + 'icon': 'dept', + 'permission': 'system:dept:list', + 'sort_order': 4, + 'parent_code': 'system' + }, + { + 'menu_name': '部门查询', + 'menu_code': 'system:dept:query', + 'menu_type': 3, + 'permission': 'system:dept:query', + 'sort_order': 1, + 'parent_code': 'system:dept' + }, + { + 'menu_name': '部门新增', + 'menu_code': 'system:dept:add', + 'menu_type': 3, + 'permission': 'system:dept:add', + 'sort_order': 2, + 'parent_code': 'system:dept' + }, + { + 'menu_name': '部门修改', + 'menu_code': 'system:dept:edit', + 'menu_type': 3, + 'permission': 'system:dept:edit', + 'sort_order': 3, + 'parent_code': 'system:dept' + }, + { + 'menu_name': '部门删除', + 'menu_code': 'system:dept:remove', + 'menu_type': 3, + 'permission': 'system:dept:remove', + 'sort_order': 4, + 'parent_code': 'system:dept' + }, + + # ==================== 工作室模块 ==================== + # 工作室目录 + { + 'menu_name': '工作室', + 'menu_code': 'studio', + 'menu_type': 1, # 目录 + 'path': '/studio', + 'icon': 'appstore', + 'sort_order': 10, + 'parent_code': None + }, + + # ==================== 通知公告模块 ==================== + # 通知公告菜单 + { + 'menu_name': '通知公告', + 'menu_code': 'studio:notice', + 'menu_type': 2, # 菜单 + 'path': '/studio/notice', + 'component': 'studio/notice/index', + 'icon': 'notice', + 'permission': 'studio:notice:list', + 'sort_order': 1, + 'parent_code': 'studio' + }, + { + 'menu_name': '通知查询', + 'menu_code': 'studio:notice:query', + 'menu_type': 3, # 按钮 + 'permission': 'studio:notice:query', + 'sort_order': 1, + 'parent_code': 'studio:notice' + }, + { + 'menu_name': '通知新增', + 'menu_code': 'studio:notice:add', + 'menu_type': 3, + 'permission': 'studio:notice:add', + 'sort_order': 2, + 'parent_code': 'studio:notice' + }, + { + 'menu_name': '通知修改', + 'menu_code': 'studio:notice:edit', + 'menu_type': 3, + 'permission': 'studio:notice:edit', + 'sort_order': 3, + 'parent_code': 'studio:notice' + }, + { + 'menu_name': '通知删除', + 'menu_code': 'studio:notice:remove', + 'menu_type': 3, + 'permission': 'studio:notice:remove', + 'sort_order': 4, + 'parent_code': 'studio:notice' + }, + + # ==================== AI对话模块 ==================== + # AI对话菜单 + { + 'menu_name': 'AI对话', + 'menu_code': 'studio:ai', + 'menu_type': 2, # 菜单 + 'path': '/studio/ai', + 'component': 'studio/ai/index', + 'icon': 'robot', + 'permission': 'studio:ai:list', + 'sort_order': 2, + 'parent_code': 'studio' + }, + { + 'menu_name': 'AI查询', + 'menu_code': 'studio:ai:query', + 'menu_type': 3, # 按钮 + 'permission': 'studio:ai:query', + 'sort_order': 1, + 'parent_code': 'studio:ai' + }, + { + 'menu_name': 'AI新增', + 'menu_code': 'studio:ai:add', + 'menu_type': 3, + 'permission': 'studio:ai:add', + 'sort_order': 2, + 'parent_code': 'studio:ai' + }, + { + 'menu_name': 'AI修改', + 'menu_code': 'studio:ai:edit', + 'menu_type': 3, + 'permission': 'studio:ai:edit', + 'sort_order': 3, + 'parent_code': 'studio:ai' + }, + { + 'menu_name': 'AI删除', + 'menu_code': 'studio:ai:remove', + 'menu_type': 3, + 'permission': 'studio:ai:remove', + 'sort_order': 4, + 'parent_code': 'studio:ai' + }, + + # ==================== 系统监控模块 ==================== + # 系统监控菜单 + { + 'menu_name': '系统监控', + 'menu_code': 'studio:system_monitor', + 'menu_type': 2, # 菜单 + 'path': '/studio/monitor', + 'component': 'studio/system_monitor/index', + 'icon': 'monitor', + 'permission': 'studio:system_monitor:list', + 'sort_order': 3, + 'parent_code': 'studio' + }, + { + 'menu_name': '监控查询', + 'menu_code': 'studio:system_monitor:query', + 'menu_type': 3, # 按钮 + 'permission': 'studio:system_monitor:query', + 'sort_order': 1, + 'parent_code': 'studio:system_monitor' + }, + { + 'menu_name': '监控新增', + 'menu_code': 'studio:system_monitor:add', + 'menu_type': 3, + 'permission': 'studio:system_monitor:add', + 'sort_order': 2, + 'parent_code': 'studio:system_monitor' + }, + { + 'menu_name': '监控修改', + 'menu_code': 'studio:system_monitor:edit', + 'menu_type': 3, + 'permission': 'studio:system_monitor:edit', + 'sort_order': 3, + 'parent_code': 'studio:system_monitor' + }, + { + 'menu_name': '监控删除', + 'menu_code': 'studio:system_monitor:remove', + 'menu_type': 3, + 'permission': 'studio:system_monitor:remove', + 'sort_order': 4, + 'parent_code': 'studio:system_monitor' + }, + + # 知识管理菜单 + { + 'menu_name': '知识管理', + 'menu_code': 'system:wiki', + 'menu_type': 2, # 菜单 + 'path': '/system/wiki', + 'component': 'system/wiki/index', + 'icon': 'book', + 'permission': 'system:wiki:list', + 'sort_order': 6, + 'parent_code': 'system' + }, + + # 知识分类管理 + { + 'menu_name': '知识分类', + 'menu_code': 'system:wiki:category', + 'menu_type': 2, # 菜单 + 'path': '/system/wiki/category', + 'component': 'system/wiki/category/index', + 'icon': 'folder', + 'permission': 'system:wiki:category:list', + 'sort_order': 1, + 'parent_code': 'system:wiki' + }, + { + 'menu_name': '分类查询', + 'menu_code': 'system:wiki:category:query', + 'menu_type': 3, # 按钮 + 'permission': 'system:wiki:category:query', + 'sort_order': 1, + 'parent_code': 'system:wiki:category' + }, + { + 'menu_name': '分类新增', + 'menu_code': 'system:wiki:category:add', + 'menu_type': 3, + 'permission': 'system:wiki:category:add', + 'sort_order': 2, + 'parent_code': 'system:wiki:category' + }, + { + 'menu_name': '分类修改', + 'menu_code': 'system:wiki:category:edit', + 'menu_type': 3, + 'permission': 'system:wiki:category:update', + 'sort_order': 3, + 'parent_code': 'system:wiki:category' + }, + { + 'menu_name': '分类删除', + 'menu_code': 'system:wiki:category:remove', + 'menu_type': 3, + 'permission': 'system:wiki:category:remove', + 'sort_order': 4, + 'parent_code': 'system:wiki:category' + }, + + # 知识文章管理 + { + 'menu_name': '知识文章', + 'menu_code': 'system:wiki:article', + 'menu_type': 2, # 菜单 + 'path': '/system/wiki/article', + 'component': 'system/wiki/article/index', + 'icon': 'file-text', + 'permission': 'system:wiki:article:list', + 'sort_order': 2, + 'parent_code': 'system:wiki' + }, + { + 'menu_name': '文章查询', + 'menu_code': 'system:wiki:article:query', + 'menu_type': 3, # 按钮 + 'permission': 'system:wiki:article:query', + 'sort_order': 1, + 'parent_code': 'system:wiki:article' + }, + { + 'menu_name': '文章新增', + 'menu_code': 'system:wiki:article:add', + 'menu_type': 3, + 'permission': 'system:wiki:article:add', + 'sort_order': 2, + 'parent_code': 'system:wiki:article' + }, + { + 'menu_name': '文章修改', + 'menu_code': 'system:wiki:article:edit', + 'menu_type': 3, + 'permission': 'system:wiki:article:edit', + 'sort_order': 3, + 'parent_code': 'system:wiki:article' + }, + { + 'menu_name': '文章删除', + 'menu_code': 'system:wiki:article:remove', + 'menu_type': 3, + 'permission': 'system:wiki:article:remove', + 'sort_order': 4, + 'parent_code': 'system:wiki:article' + }, + { + 'menu_name': '文章发布', + 'menu_code': 'system:wiki:article:publish', + 'menu_type': 3, + 'permission': 'system:wiki:article:publish', + 'sort_order': 5, + 'parent_code': 'system:wiki:article' + }, + + # 日志管理菜单 + { + 'menu_name': '日志管理', + 'menu_code': 'system:log', + 'menu_type': 2, # 菜单 + 'path': '/system/log', + 'component': 'system/log/index', + 'icon': 'log', + 'permission': 'log.view_operationlog', + 'sort_order': 7, + 'parent_code': 'system' + }, + { + 'menu_name': '日志查询', + 'menu_code': 'system:log:query', + 'menu_type': 3, # 按钮 + 'permission': 'log.view_operationlog', + 'sort_order': 1, + 'parent_code': 'system:log' + }, + { + 'menu_name': '日志详情', + 'menu_code': 'system:log:detail', + 'menu_type': 3, + 'permission': 'log.view_operationlog', + 'sort_order': 2, + 'parent_code': 'system:log' + }, + + # YOLO古建筑识别模块 + { + 'menu_name': 'YOLO识别', + 'menu_code': 'studio:yolo', + 'menu_type': 2, # 菜单 + 'path': '/studio/yolo', + 'component': 'studio/yolo/index', + 'icon': 'camera', + 'permission': 'studio:yolo:list', + 'sort_order': 4, + 'parent_code': 'studio' + }, + { + 'menu_name': '图像识别', + 'menu_code': 'studio:yolo:recognition', + 'menu_type': 3, # 按钮 + 'permission': 'studio:yolo:recognition', + 'sort_order': 1, + 'parent_code': 'studio:yolo' + }, + { + 'menu_name': '识别历史', + 'menu_code': 'studio:yolo:history', + 'menu_type': 3, + 'permission': 'studio:yolo:history', + 'sort_order': 2, + 'parent_code': 'studio:yolo' + }, + { + 'menu_name': '问答记录', + 'menu_code': 'studio:yolo:question', + 'menu_type': 3, + 'permission': 'studio:yolo:question', + 'sort_order': 3, + 'parent_code': 'studio:yolo' + }, + { + 'menu_name': '记录收藏', + 'menu_code': 'studio:yolo:favorite', + 'menu_type': 3, + 'permission': 'studio:yolo:favorite', + 'sort_order': 4, + 'parent_code': 'studio:yolo' + }, + { + 'menu_name': '记录删除', + 'menu_code': 'studio:yolo:delete', + 'menu_type': 3, + 'permission': 'studio:yolo:delete', + 'sort_order': 5, + 'parent_code': 'studio:yolo' + }, + { + 'menu_name': '统计信息', + 'menu_code': 'studio:yolo:statistics', + 'menu_type': 3, + 'permission': 'studio:yolo:statistics', + 'sort_order': 6, + 'parent_code': 'studio:yolo' + } +] + + diff --git a/hertz_studio_django_utils/config/roles_config.py b/hertz_studio_django_utils/config/roles_config.py new file mode 100644 index 0000000..d4aa37c --- /dev/null +++ b/hertz_studio_django_utils/config/roles_config.py @@ -0,0 +1,20 @@ +roles = [ + { + 'role_name': '超级管理员', + 'role_code': 'super_admin', + 'description': '系统超级管理员,拥有所有权限', + 'sort_order': 1 + }, + { + 'role_name': '系统管理员', + 'role_code': 'system_admin', + 'description': '系统管理员,拥有系统管理权限', + 'sort_order': 2 + }, + { + 'role_name': '普通用户', + 'role_code': 'normal_user', + 'description': '普通用户,基础权限', + 'sort_order': 3 + } +] \ No newline at end of file diff --git a/hertz_studio_django_utils/crypto/__init__.py b/hertz_studio_django_utils/crypto/__init__.py new file mode 100644 index 0000000..75b3047 --- /dev/null +++ b/hertz_studio_django_utils/crypto/__init__.py @@ -0,0 +1,4 @@ +from .encryption_utils import EncryptionUtils +from .password_hashers import MD5PasswordHasher + +__all__ = ['EncryptionUtils', 'MD5PasswordHasher'] \ No newline at end of file diff --git a/hertz_studio_django_utils/crypto/encryption_utils.py b/hertz_studio_django_utils/crypto/encryption_utils.py new file mode 100644 index 0000000..e06d5e6 --- /dev/null +++ b/hertz_studio_django_utils/crypto/encryption_utils.py @@ -0,0 +1,160 @@ +import base64 +import hashlib +import secrets +from typing import Optional +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +class EncryptionUtils: + """ + 加密工具类 + 提供各种加密解密功能 + """ + + @staticmethod + def generate_salt(length: int = 32) -> str: + """ + 生成随机盐值 + + Args: + length: 盐值长度 + + Returns: + str: Base64编码的盐值 + """ + salt = secrets.token_bytes(length) + return base64.b64encode(salt).decode('utf-8') + + @staticmethod + def md5_hash(data: str, salt: str = '') -> str: + """ + MD5哈希加密 + + Args: + data: 待加密数据 + salt: 盐值 + + Returns: + str: MD5哈希值 + """ + combined = data + salt + md5_hash = hashlib.md5(combined.encode('utf-8')) + return md5_hash.hexdigest() + + @staticmethod + def sha256_hash(data: str, salt: str = '') -> str: + """ + SHA256哈希加密 + + Args: + data: 待加密数据 + salt: 盐值 + + Returns: + str: SHA256哈希值 + """ + combined = data + salt + sha256_hash = hashlib.sha256(combined.encode('utf-8')) + return sha256_hash.hexdigest() + + @staticmethod + def generate_key_from_password(password: str, salt: bytes) -> bytes: + """ + 从密码生成加密密钥 + + Args: + password: 密码 + salt: 盐值 + + Returns: + bytes: 加密密钥 + """ + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + ) + key = base64.urlsafe_b64encode(kdf.derive(password.encode())) + return key + + @staticmethod + def encrypt_data(data: str, password: str) -> Optional[str]: + """ + 使用密码加密数据 + + Args: + data: 待加密数据 + password: 加密密码 + + Returns: + Optional[str]: 加密后的数据(Base64编码),失败返回None + """ + try: + # 生成随机盐值 + salt = secrets.token_bytes(16) + + # 从密码生成密钥 + key = EncryptionUtils.generate_key_from_password(password, salt) + + # 创建Fernet实例 + fernet = Fernet(key) + + # 加密数据 + encrypted_data = fernet.encrypt(data.encode('utf-8')) + + # 将盐值和加密数据组合 + combined = salt + encrypted_data + + # 返回Base64编码的结果 + return base64.b64encode(combined).decode('utf-8') + + except Exception: + return None + + @staticmethod + def decrypt_data(encrypted_data: str, password: str) -> Optional[str]: + """ + 使用密码解密数据 + + Args: + encrypted_data: 加密后的数据(Base64编码) + password: 解密密码 + + Returns: + Optional[str]: 解密后的数据,失败返回None + """ + try: + # Base64解码 + combined = base64.b64decode(encrypted_data.encode('utf-8')) + + # 分离盐值和加密数据 + salt = combined[:16] + encrypted_bytes = combined[16:] + + # 从密码生成密钥 + key = EncryptionUtils.generate_key_from_password(password, salt) + + # 创建Fernet实例 + fernet = Fernet(key) + + # 解密数据 + decrypted_data = fernet.decrypt(encrypted_bytes) + + return decrypted_data.decode('utf-8') + + except Exception: + return None + + @staticmethod + def generate_random_key() -> str: + """ + 生成随机加密密钥 + + Returns: + str: Base64编码的随机密钥 + """ + key = Fernet.generate_key() + return key.decode('utf-8') \ No newline at end of file diff --git a/hertz_studio_django_utils/crypto/password_hashers.py b/hertz_studio_django_utils/crypto/password_hashers.py new file mode 100644 index 0000000..f8693f0 --- /dev/null +++ b/hertz_studio_django_utils/crypto/password_hashers.py @@ -0,0 +1,79 @@ +import hashlib +from django.contrib.auth.hashers import BasePasswordHasher +from django.utils.crypto import constant_time_compare + + +class MD5PasswordHasher(BasePasswordHasher): + """ + MD5密码哈希器 + 用于兼容旧系统的MD5密码加密 + """ + algorithm = 'md5' + library = 'hashlib' + + def encode(self, password, salt): + """ + 编码密码 + + Args: + password: 原始密码 + salt: 盐值 + + Returns: + str: 编码后的密码 + """ + hash_obj = hashlib.md5((salt + password).encode('utf-8')) + hash_value = hash_obj.hexdigest() + return f'{self.algorithm}${salt}${hash_value}' + + def verify(self, password, encoded): + """ + 验证密码 + + Args: + password: 原始密码 + encoded: 编码后的密码 + + Returns: + bool: 验证结果 + """ + algorithm, salt, hash_value = encoded.split('$', 2) + assert algorithm == self.algorithm + encoded_2 = self.encode(password, salt) + return constant_time_compare(encoded, encoded_2) + + def safe_summary(self, encoded): + """ + 返回密码的安全摘要信息 + + Args: + encoded: 编码后的密码 + + Returns: + dict: 摘要信息 + """ + algorithm, salt, hash_value = encoded.split('$', 2) + assert algorithm == self.algorithm + return { + 'algorithm': algorithm, + 'salt': salt[:6] + '...', + 'hash': hash_value[:6] + '...', + } + + def harden_runtime(self, password, encoded): + """ + 硬化运行时间(MD5不需要) + """ + pass + + def must_update(self, encoded): + """ + 检查是否需要更新密码编码 + + Args: + encoded: 编码后的密码 + + Returns: + bool: 是否需要更新 + """ + return False \ No newline at end of file diff --git a/hertz_studio_django_utils/email/__init__.py b/hertz_studio_django_utils/email/__init__.py new file mode 100644 index 0000000..a87a0cb --- /dev/null +++ b/hertz_studio_django_utils/email/__init__.py @@ -0,0 +1,3 @@ +from .email_service import EmailService + +__all__ = ['EmailService'] \ No newline at end of file diff --git a/hertz_studio_django_utils/email/email_service.py b/hertz_studio_django_utils/email/email_service.py new file mode 100644 index 0000000..6638a83 --- /dev/null +++ b/hertz_studio_django_utils/email/email_service.py @@ -0,0 +1,174 @@ +from django.core.mail import EmailMultiAlternatives +from django.conf import settings +from django.utils.html import strip_tags +from typing import Optional, Dict, Any +import logging + +logger = logging.getLogger(__name__) + + +class EmailService: + """ + 邮件发送服务类 + 提供统一的邮件发送接口 + """ + + @staticmethod + def send_email( + recipient_email: str, + subject: str, + html_content: str, + text_content: Optional[str] = None, + from_email: Optional[str] = None + ) -> bool: + """ + 发送邮件 + + Args: + recipient_email: 收件人邮箱 + subject: 邮件主题 + html_content: HTML内容 + text_content: 纯文本内容(可选) + from_email: 发件人邮箱(可选) + + Returns: + bool: 发送是否成功 + """ + try: + # 检查邮件配置 + if not settings.EMAIL_HOST_USER or not settings.EMAIL_HOST_PASSWORD: + logger.warning("邮件配置不完整,使用控制台输出模式") + logger.info(f"收件人: {recipient_email}") + logger.info(f"主题: {subject}") + logger.info(f"内容: {text_content or strip_tags(html_content)[:200]}...") + return True + + # 如果没有提供纯文本内容,从HTML中提取 + if not text_content: + text_content = strip_tags(html_content) + + # 创建邮件 + email = EmailMultiAlternatives( + subject=subject, + body=text_content, + from_email=from_email or settings.DEFAULT_FROM_EMAIL, + to=[recipient_email] + ) + + # 添加HTML内容 + email.attach_alternative(html_content, "text/html") + + # 发送邮件 + email.send() + logger.info(f"邮件发送成功: {recipient_email}") + return True + + except Exception as e: + logger.error(f"邮件发送失败: {str(e)}") + return False + + @staticmethod + def send_verification_code( + recipient_email: str, + recipient_name: str, + verification_code: str, + code_type: str = 'register' + ) -> bool: + """ + 发送验证码邮件 + + Args: + recipient_email: 收件人邮箱 + recipient_name: 收件人姓名 + verification_code: 验证码 + code_type: 验证码类型(register/reset_password) + + Returns: + bool: 发送是否成功 + """ + # 根据类型生成邮件内容 + email_content = EmailService._generate_verification_email_content( + recipient_name, verification_code, code_type + ) + + return EmailService.send_email( + recipient_email=recipient_email, + subject=email_content['subject'], + html_content=email_content['html_content'], + text_content=email_content['text_content'] + ) + + @staticmethod + def _generate_verification_email_content( + recipient_name: str, + verification_code: str, + code_type: str + ) -> Dict[str, str]: + """ + 生成验证码邮件内容 + + Args: + recipient_name: 收件人姓名 + verification_code: 验证码 + code_type: 验证码类型 + + Returns: + Dict[str, str]: 包含subject, html_content, text_content的字典 + """ + if code_type == 'register': + subject = '🔐 注册验证码 - Hertz Server Django' + title = '注册验证码' + description = '感谢您注册 Hertz Server Django!请使用以下验证码完成注册:' + elif code_type == 'reset_password': + subject = '🔐 密码重置验证码 - Hertz Server Django' + title = '密码重置验证码' + description = '您正在重置密码,请使用以下验证码完成操作:' + else: + subject = '🔐 邮箱验证码 - Hertz Server Django' + title = '邮箱验证码' + description = '请使用以下验证码完成验证:' + + html_content = f''' + + +
+
+

🔐 {title}

+
+
+

亲爱的 {recipient_name}

+

{description}

+
+
+ {verification_code} +
+
+

验证码有效期为5分钟,请尽快使用。如果您没有进行此操作,请忽略此邮件。

+
+

此致
Hertz Server Django 团队

+
+
+ + + ''' + + text_content = f''' + {title} + + 亲爱的 {recipient_name}, + + {description} + + 验证码:{verification_code} + + 验证码有效期为5分钟,请尽快使用。如果您没有进行此操作,请忽略此邮件。 + + 此致 + Hertz Server Django 团队 + ''' + + return { + 'subject': subject, + 'html_content': html_content, + 'text_content': text_content + } \ No newline at end of file diff --git a/hertz_studio_django_utils/log/__init__.py b/hertz_studio_django_utils/log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hertz_studio_django_utils/log/log_decorator.py b/hertz_studio_django_utils/log/log_decorator.py new file mode 100644 index 0000000..e4cc781 --- /dev/null +++ b/hertz_studio_django_utils/log/log_decorator.py @@ -0,0 +1,368 @@ +from functools import wraps +from django.contrib.auth.models import User +from django.http import JsonResponse +import json +import logging + +# 设置日志记录器 +logger = logging.getLogger(__name__) + +def operation_log(action_type, module, description=None, target_model=None): + """ + 操作日志装饰器 + + Args: + action_type (str): 操作类型,如 'create', 'update', 'delete' 等 + module (str): 操作模块,如 '用户管理', '通知管理' 等 + description (str, optional): 操作描述,如果不提供则自动生成 + target_model (str, optional): 目标模型名称 + + Usage: + @operation_log('create', '用户管理', '创建新用户', 'User') + def create_user(request): + # 视图函数逻辑 + pass + """ + def decorator(view_func): + @wraps(view_func) + def wrapper(*args, **kwargs): + # 检查是否是类视图方法(第一个参数是self,第二个是request) + if len(args) >= 2 and hasattr(args[0], '__class__') and hasattr(args[1], 'META'): + # 类视图方法 + self_instance = args[0] + request = args[1] + print(f"\n=== 类视图装饰器被调用 ===") + print(f"类名: {self_instance.__class__.__name__}") + print(f"函数名: {view_func.__name__}") + elif len(args) >= 1 and hasattr(args[0], 'META'): + # 函数视图 + request = args[0] + print(f"\n=== 函数视图装饰器被调用 ===") + print(f"函数名: {view_func.__name__}") + else: + # 无法识别的调用方式,直接执行原函数 + return view_func(*args, **kwargs) + + print(f"操作类型: {action_type}") + print(f"模块: {module}") + print(f"请求方法: {request.method}") + print(f"请求路径: {request.path}") + print(f"=== 装饰器调用信息结束 ===\n") + # 延迟导入避免循环导入 + from hertz_studio_django_log.models import OperationLog + + # 获取用户信息 + user = None + if hasattr(request, 'user') and request.user.is_authenticated: + user = request.user + + # 获取请求信息 + ip_address = get_client_ip(request) + user_agent = request.META.get('HTTP_USER_AGENT', '')[:500] # 限制长度 + + # 获取请求数据 + request_data = get_request_data(request) + + response = None + response_status = 200 + target_id = None + + try: + # 执行原始视图函数 + if len(args) >= 2 and hasattr(args[0], '__class__') and hasattr(args[1], 'META'): + # 类视图方法,去掉self参数 + response = view_func(args[0], request, *args[2:], **kwargs) + else: + # 函数视图 + response = view_func(request, *args[1:], **kwargs) + + # 获取响应状态码 + if hasattr(response, 'status_code'): + response_status = response.status_code + + # 尝试获取目标ID + target_id = extract_target_id(response, kwargs) + + except Exception as e: + response_status = 500 + logger.error(f"视图函数执行错误: {str(e)}") + # 打印详细的错误信息到控制台 + import traceback + print(f"\n=== 装饰器捕获到异常 ===") + print(f"异常类型: {type(e).__name__}") + print(f"异常信息: {str(e)}") + print(f"异常堆栈:") + traceback.print_exc() + print(f"=== 装饰器异常信息结束 ===\n") + # 重新抛出异常,让Django处理 + raise + + # 异步记录操作日志 + try: + print(f"\n=== 开始记录操作日志 ===") + print(f"用户: {user}") + print(f"操作类型: {action_type}") + print(f"模块: {module}") + print(f"描述: {description or f'{action_type}操作 - {module}'}") + print(f"目标模型: {target_model}") + print(f"目标ID: {target_id}") + print(f"IP地址: {ip_address}") + print(f"请求数据: {request_data}") + print(f"响应状态: {response_status}") + + log_operation( + user=user, + action_type=action_type, + module=module, + description=description or f"{action_type}操作 - {module}", + target_model=target_model, + target_id=target_id, + ip_address=ip_address, + user_agent=user_agent, + request_data=request_data, + response_status=response_status + ) + print(f"操作日志记录成功") + print(f"=== 操作日志记录结束 ===\n") + except Exception as log_error: + # 日志记录失败不应该影响正常业务 + print(f"\n=== 操作日志记录失败 ===") + print(f"错误类型: {type(log_error).__name__}") + print(f"错误信息: {str(log_error)}") + import traceback + traceback.print_exc() + print(f"=== 操作日志记录失败信息结束 ===\n") + logger.error(f"操作日志记录失败: {log_error}") + + return response + + return wrapper + return decorator + +def get_client_ip(request): + """ + 获取客户端真实IP地址 + """ + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + else: + ip = request.META.get('REMOTE_ADDR') + return ip + +def get_request_data(request): + """ + 安全地获取请求数据 + """ + request_data = {} + try: + if request.method == 'GET': + request_data = dict(request.GET) + elif request.method == 'POST': + content_type = request.META.get('CONTENT_TYPE', '') + if 'application/json' in content_type: + if hasattr(request, 'body') and request.body: + request_data = json.loads(request.body.decode('utf-8')) + else: + request_data = dict(request.POST) + elif request.method in ['PUT', 'PATCH', 'DELETE']: + if hasattr(request, 'body') and request.body: + request_data = json.loads(request.body.decode('utf-8')) + except (json.JSONDecodeError, UnicodeDecodeError, AttributeError) as e: + logger.warning(f"解析请求数据失败: {str(e)}") + request_data = {} + + # 过滤敏感信息 + sensitive_fields = ['password', 'token', 'secret', 'key', 'csrf_token'] + if isinstance(request_data, dict): + for field in sensitive_fields: + if field in request_data: + request_data[field] = '***' + + return request_data + +def extract_target_id(response, kwargs): + """ + 从响应或URL参数中提取目标ID + """ + target_id = None + + # 从响应中获取ID + try: + if hasattr(response, 'data') and isinstance(response.data, dict): + if 'id' in response.data: + target_id = response.data['id'] + elif 'data' in response.data and isinstance(response.data['data'], dict): + if 'id' in response.data['data']: + target_id = response.data['data']['id'] + except (AttributeError, TypeError): + pass + + # 从URL参数中获取ID + if not target_id and kwargs: + for key, value in kwargs.items(): + if key.endswith('_id') or key == 'pk' or key == 'id': + try: + target_id = int(value) + break + except (ValueError, TypeError): + pass + + return target_id + +def log_operation(user, action_type, module, description, target_model=None, + target_id=None, ip_address=None, user_agent=None, + request_data=None, response_status=None): + """ + 记录操作日志 + """ + from hertz_studio_django_log.models import OperationLog + + # 如果用户未登录,记录为匿名用户操作(可选择是否记录) + if user is None: + # 对于某些重要操作,即使是匿名用户也要记录 + # 可以通过配置决定是否记录匿名用户操作 + logger.info(f"记录匿名用户的操作日志: {action_type} - {module} - {description}") + # 为匿名用户创建一个临时用户对象或跳过用户字段 + # 这里我们选择跳过匿名用户的日志记录,保持原有逻辑 + # 如果需要记录匿名用户操作,可以注释掉下面的return语句 + return + + # 限制数据长度避免数据库错误 + if user_agent and len(user_agent) > 500: + user_agent = user_agent[:500] + + if description and len(description) > 255: + description = description[:255] + + # 限制请求数据大小 + if request_data and len(str(request_data)) > 5000: + request_data = {'message': '请求数据过大,已省略'} + + # 使用模型的create_log方法来创建日志 + OperationLog.create_log( + user=user, + action_type=action_type, + module=module, + description=description, + target_model=target_model, + target_id=target_id, + ip_address=ip_address, + user_agent=user_agent, + request_data=request_data, + response_status=response_status + ) + +def auto_log(action_type, module=None): + """ + 自动日志装饰器,根据视图类名和方法名自动推断模块和描述 + + Args: + action_type (str): 操作类型 + module (str, optional): 模块名称,如果不提供则从视图类名推断 + """ + def decorator(view_func): + @wraps(view_func) + def wrapper(*args, **kwargs): + # 延迟导入避免循环导入 + from hertz_studio_django_log.models import OperationLog + + # 获取request对象 + request = None + if args and hasattr(args[0], '__class__') and hasattr(args[0], '__module__'): + # 这是类方法,request是第二个参数 + request = args[1] if len(args) > 1 else None + view_instance = args[0] + class_name = view_instance.__class__.__name__ + else: + # 这是函数视图,request是第一个参数 + request = args[0] if args else None + class_name = view_func.__name__ + + # 检查request对象是否有效 + if not request or not hasattr(request, 'META'): + return view_func(*args, **kwargs) + + # 获取用户信息 + user = None + if hasattr(request, 'user') and request.user.is_authenticated: + user = request.user + + # 自动推断模块名称 + auto_module = module + if not auto_module: + if 'User' in class_name: + auto_module = '用户管理' + elif 'Notification' in class_name: + auto_module = '通知管理' + elif 'Config' in class_name: + auto_module = '系统配置' + elif 'File' in class_name: + auto_module = '文件管理' + elif 'AI' in class_name or 'Chat' in class_name: + auto_module = 'AI助手' + elif 'Wiki' in class_name: + auto_module = '知识管理' + else: + auto_module = '系统管理' + + # 自动生成描述 + action_map = { + 'create': '创建', + 'update': '更新', + 'delete': '删除', + 'view': '查看', + 'list': '列表查看', + 'login': '登录', + 'logout': '登出' + } + auto_description = f"{action_map.get(action_type, action_type)} - {auto_module}" + + # 获取请求信息 + ip_address = get_client_ip(request) + user_agent = request.META.get('HTTP_USER_AGENT', '')[:500] + request_data = get_request_data(request) + + response = None + response_status = 200 + target_id = None + + try: + # 执行原始视图函数 + response = view_func(*args, **kwargs) + + # 获取响应状态码 + if hasattr(response, 'status_code'): + response_status = response.status_code + + # 尝试获取目标ID + target_id = extract_target_id(response, kwargs) + + except Exception as e: + response_status = 500 + logger.error(f"视图函数执行错误: {str(e)}") + # 重新抛出异常,让Django处理 + raise + + # 异步记录操作日志 + try: + log_operation( + user=user, + action_type=action_type, + module=auto_module, + description=auto_description, + target_model=auto_module, + target_id=target_id, + ip_address=ip_address, + user_agent=user_agent, + request_data=request_data, + response_status=response_status + ) + except Exception as log_error: + # 日志记录失败不应该影响正常业务 + logger.error(f"操作日志记录失败: {log_error}") + + return response + + return wrapper + return decorator \ No newline at end of file diff --git a/hertz_studio_django_utils/ollama/__init__.py b/hertz_studio_django_utils/ollama/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hertz_studio_django_utils/ollama/ollama_client.py b/hertz_studio_django_utils/ollama/ollama_client.py new file mode 100644 index 0000000..a8a4585 --- /dev/null +++ b/hertz_studio_django_utils/ollama/ollama_client.py @@ -0,0 +1,207 @@ +import json +import asyncio +import aiohttp +import requests +from typing import List, Dict, Any, AsyncGenerator, Optional +from django.conf import settings + + +class OllamaClient: + """ + Ollama API客户端 + 用于与Ollama服务通信,获取LLM响应 + """ + + def __init__(self, base_url: str = None): + """ + 初始化Ollama客户端 + + Args: + base_url: Ollama服务地址,默认从settings中获取 + """ + self.base_url = base_url or getattr(settings, "OLLAMA_BASE_URL", "http://localhost:11434") + self.generate_url = f"{self.base_url}/api/generate" + self.chat_url = f"{self.base_url}/api/chat" + + async def generate_stream( + self, + model: str, + prompt: str, + system_prompt: Optional[str] = None, + temperature: float = 0.7, + top_p: float = 0.9, + top_k: int = 40, + max_tokens: int = 2048 + ) -> AsyncGenerator[str, None]: + """ + 生成文本流 + + Args: + model: 模型名称,如deepseek-r1:1.5b + prompt: 用户输入的提示 + system_prompt: 系统提示 + temperature: 温度参数 + top_p: top-p采样参数 + top_k: top-k采样参数 + max_tokens: 最大生成token数 + + Yields: + 生成的文本片段 + """ + payload = { + "model": model, + "prompt": prompt, + "stream": True, + "options": { + "temperature": temperature, + "top_p": top_p, + "top_k": top_k, + "num_predict": max_tokens + } + } + + if system_prompt: + payload["system"] = system_prompt + + async with aiohttp.ClientSession() as session: + async with session.post(self.generate_url, json=payload) as response: + if response.status != 200: + error_text = await response.text() + raise Exception(f"Ollama API error: {response.status} - {error_text}") + + # 流式读取响应 + async for line in response.content: + if not line: + continue + + try: + data = json.loads(line) + if "response" in data: + yield data["response"] + + # 如果接收到完成标志,退出 + if data.get("done", False): + break + except json.JSONDecodeError: + continue + + async def chat_stream( + self, + model: str, + messages: List[Dict[str, str]], + temperature: float = 0.7, + top_p: float = 0.9, + top_k: int = 40, + max_tokens: int = 2048 + ) -> AsyncGenerator[str, None]: + """ + 流式聊天接口 + + Args: + model: 模型名称 + messages: 消息历史,格式为[{"role": "user", "content": "..."}, ...] + temperature: 温度参数 + top_p: top-p采样参数 + top_k: top-k采样参数 + max_tokens: 最大生成token数 + + Yields: + 生成的文本片段 + """ + payload = { + "model": model, + "messages": messages, + "stream": True, + "options": { + "temperature": temperature, + "top_p": top_p, + "top_k": top_k, + "num_predict": max_tokens + } + } + + async with aiohttp.ClientSession() as session: + async with session.post(self.chat_url, json=payload) as response: + if response.status != 200: + error_text = await response.text() + raise Exception(f"Ollama API error: {response.status} - {error_text}") + + # 流式读取响应 + async for line in response.content: + if not line: + continue + + try: + data = json.loads(line) + if "message" in data and "content" in data["message"]: + yield data["message"]["content"] + + # 如果接收到完成标志,退出 + if data.get("done", False): + break + except json.JSONDecodeError: + continue + + async def check_model_availability(self, model: str) -> bool: + """ + 检查模型是否可用 + + Args: + model: 模型名称 + + Returns: + 模型是否可用 + """ + async with aiohttp.ClientSession() as session: + async with session.get(f"{self.base_url}/api/tags") as response: + if response.status != 200: + return False + + data = await response.json() + models = data.get("models", []) + return any(m["name"] == model for m in models) + + def chat_completion( + self, + model: str, + messages: List[Dict[str, str]], + temperature: float = 0.7, + top_p: float = 0.9, + top_k: int = 40, + max_tokens: int = 2048 + ) -> str: + """ + 同步聊天接口,用于REST API + + Args: + model: 模型名称 + messages: 消息历史,格式为[{"role": "user", "content": "..."}, ...] + temperature: 温度参数 + top_p: top-p采样参数 + top_k: top-k采样参数 + max_tokens: 最大生成token数 + + Returns: + 生成的完整回复文本 + """ + payload = { + "model": model, + "messages": messages, + "stream": False, # 非流式 + "options": { + "temperature": temperature, + "top_p": top_p, + "top_k": top_k, + "num_predict": max_tokens + } + } + + response = requests.post(self.chat_url, json=payload) + if response.status_code != 200: + raise Exception(f"Ollama API error: {response.status_code} - {response.text}") + + data = response.json() + if "message" in data and "content" in data["message"]: + return data["message"]["content"] + + raise Exception("Unexpected response format from Ollama API") \ No newline at end of file diff --git a/hertz_studio_django_utils/responses/HertzResponse.py b/hertz_studio_django_utils/responses/HertzResponse.py new file mode 100644 index 0000000..0b82654 --- /dev/null +++ b/hertz_studio_django_utils/responses/HertzResponse.py @@ -0,0 +1,185 @@ +from django.http import JsonResponse +from typing import Any, Dict, Optional + + +class HertzResponse: + """ + Hertz统一响应类 + 提供标准化的API响应格式 + """ + + @staticmethod + def success(data: Any = None, message: str = "操作成功", code: int = 200) -> JsonResponse: + """ + 成功响应 + + Args: + data: 响应数据 + message: 响应消息 + code: HTTP状态码 + + Returns: + JsonResponse: 标准化的成功响应 + """ + response_data = { + 'success': True, + 'code': code, + 'message': message, + 'data': data + } + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) + + @staticmethod + def fail(message: str = "操作失败", data: Any = None, code: int = 400) -> JsonResponse: + """ + 失败响应(业务逻辑失败) + + Args: + message: 失败消息 + data: 响应数据 + code: HTTP状态码 + + Returns: + JsonResponse: 标准化的失败响应 + """ + response_data = { + 'success': False, + 'code': code, + 'message': message, + 'data': data + } + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) + + @staticmethod + def error(message: str = "系统错误", error: str = None, code: int = 500) -> JsonResponse: + """ + 错误响应(系统错误) + + Args: + message: 错误消息 + error: 详细错误信息 + code: HTTP状态码 + + Returns: + JsonResponse: 标准化的错误响应 + """ + response_data = { + 'success': False, + 'code': code, + 'message': message + } + + if error: + response_data['error'] = error + + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) + + @staticmethod + def unauthorized(message: str = "未授权访问", code: int = 401) -> JsonResponse: + """ + 未授权响应 + + Args: + message: 响应消息 + code: HTTP状态码 + + Returns: + JsonResponse: 标准化的未授权响应 + """ + response_data = { + 'success': False, + 'code': code, + 'message': message + } + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) + + @staticmethod + def forbidden(message: str = "禁止访问", code: int = 403) -> JsonResponse: + """ + 禁止访问响应 + + Args: + message: 响应消息 + code: HTTP状态码 + + Returns: + JsonResponse: 标准化的禁止访问响应 + """ + response_data = { + 'success': False, + 'code': code, + 'message': message + } + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) + + @staticmethod + def not_found(message: str = "资源未找到", code: int = 404) -> JsonResponse: + """ + 资源未找到响应 + + Args: + message: 响应消息 + code: HTTP状态码 + + Returns: + JsonResponse: 标准化的资源未找到响应 + """ + response_data = { + 'success': False, + 'code': code, + 'message': message + } + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) + + @staticmethod + def validation_error(message: str = "参数验证失败", errors: Dict = None, code: int = 422) -> JsonResponse: + """ + 参数验证错误响应 + + Args: + message: 响应消息 + errors: 验证错误详情 + code: HTTP状态码 + + Returns: + JsonResponse: 标准化的参数验证错误响应 + """ + response_data = { + 'success': False, + 'code': code, + 'message': message + } + + if errors: + response_data['errors'] = errors + + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) + + @staticmethod + def custom(success: bool, message: str, data: Any = None, code: int = 200, **kwargs) -> JsonResponse: + """ + 自定义响应 + + Args: + success: 是否成功 + message: 响应消息 + data: 响应数据 + code: HTTP状态码 + **kwargs: 其他自定义字段 + + Returns: + JsonResponse: 自定义响应 + """ + response_data = { + 'success': success, + 'code': code, + 'message': message + } + + if data is not None: + response_data['data'] = data + + # 添加其他自定义字段 + response_data.update(kwargs) + + return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False}) \ No newline at end of file diff --git a/hertz_studio_django_utils/responses/__init__.py b/hertz_studio_django_utils/responses/__init__.py new file mode 100644 index 0000000..9b3b80e --- /dev/null +++ b/hertz_studio_django_utils/responses/__init__.py @@ -0,0 +1,3 @@ +from .HertzResponse import HertzResponse + +__all__ = ['HertzResponse'] \ No newline at end of file diff --git a/hertz_studio_django_utils/validators/__init__.py b/hertz_studio_django_utils/validators/__init__.py new file mode 100644 index 0000000..6a254ad --- /dev/null +++ b/hertz_studio_django_utils/validators/__init__.py @@ -0,0 +1,5 @@ +from .email_validator import EmailValidator +from .phone_validator import PhoneValidator +from .password_validator import PasswordValidator + +__all__ = ['EmailValidator', 'PhoneValidator', 'PasswordValidator'] \ No newline at end of file diff --git a/hertz_studio_django_utils/validators/email_validator.py b/hertz_studio_django_utils/validators/email_validator.py new file mode 100644 index 0000000..b4687ec --- /dev/null +++ b/hertz_studio_django_utils/validators/email_validator.py @@ -0,0 +1,117 @@ +import re +from typing import Tuple + + +class EmailValidator: + """ + 邮箱验证器 + 提供邮箱格式验证功能 + """ + + # 邮箱正则表达式 + EMAIL_PATTERN = re.compile( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + ) + + @staticmethod + def is_valid_email(email: str) -> bool: + """ + 验证邮箱格式是否正确 + + Args: + email: 邮箱地址 + + Returns: + bool: 邮箱格式是否正确 + """ + if not email or not isinstance(email, str): + return False + + return bool(EmailValidator.EMAIL_PATTERN.match(email.strip())) + + @staticmethod + def validate_email(email: str) -> Tuple[bool, str]: + """ + 验证邮箱并返回详细信息 + + Args: + email: 邮箱地址 + + Returns: + Tuple[bool, str]: (是否有效, 提示信息) + """ + if not email: + return False, "邮箱地址不能为空" + + if not isinstance(email, str): + return False, "邮箱地址必须是字符串" + + email = email.strip() + + if len(email) == 0: + return False, "邮箱地址不能为空" + + if len(email) > 254: + return False, "邮箱地址长度不能超过254个字符" + + if not EmailValidator.EMAIL_PATTERN.match(email): + return False, "邮箱地址格式不正确" + + # 检查本地部分长度(@符号前的部分) + local_part = email.split('@')[0] + if len(local_part) > 64: + return False, "邮箱用户名部分长度不能超过64个字符" + + return True, "邮箱地址格式正确" + + @staticmethod + def normalize_email(email: str) -> str: + """ + 标准化邮箱地址 + + Args: + email: 邮箱地址 + + Returns: + str: 标准化后的邮箱地址 + """ + if not email or not isinstance(email, str): + return '' + + # 去除首尾空格并转换为小写 + return email.strip().lower() + + @staticmethod + def get_email_domain(email: str) -> str: + """ + 获取邮箱域名 + + Args: + email: 邮箱地址 + + Returns: + str: 邮箱域名 + """ + if not EmailValidator.is_valid_email(email): + return '' + + return email.split('@')[1].lower() + + @staticmethod + def is_common_email_provider(email: str) -> bool: + """ + 检查是否为常见邮箱服务商 + + Args: + email: 邮箱地址 + + Returns: + bool: 是否为常见邮箱服务商 + """ + common_providers = { + 'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', + '163.com', '126.com', 'qq.com', 'sina.com', 'sohu.com' + } + + domain = EmailValidator.get_email_domain(email) + return domain in common_providers \ No newline at end of file diff --git a/hertz_studio_django_utils/validators/password_validator.py b/hertz_studio_django_utils/validators/password_validator.py new file mode 100644 index 0000000..8b2a392 --- /dev/null +++ b/hertz_studio_django_utils/validators/password_validator.py @@ -0,0 +1,229 @@ +import re +from typing import Tuple, List + + +class PasswordValidator: + """ + 密码验证器 + 提供密码强度验证功能 + """ + + """ + 生产环境 + """ + # @staticmethod + # def validate_password_strength(password: str, min_length: int = 8, max_length: int = 128) -> Tuple[bool, List[str]]: + # """ + # 验证密码强度 + # + # Args: + # password: 密码 + # min_length: 最小长度 + # max_length: 最大长度 + # + # Returns: + # Tuple[bool, List[str]]: (是否通过验证, 错误信息列表) + # """ + # errors = [] + # + # if not password: + # errors.append("密码不能为空") + # return False, errors + # + # if not isinstance(password, str): + # errors.append("密码必须是字符串") + # return False, errors + # + # # 检查长度 + # if len(password) < min_length: + # errors.append(f"密码长度至少{min_length}位") + # + # if len(password) > max_length: + # errors.append(f"密码长度不能超过{max_length}位") + # + # # 检查是否包含数字 + # if not re.search(r'\d', password): + # errors.append("密码必须包含至少一个数字") + # + # # 检查是否包含小写字母 + # if not re.search(r'[a-z]', password): + # errors.append("密码必须包含至少一个小写字母") + # + # # 检查是否包含大写字母 + # if not re.search(r'[A-Z]', password): + # errors.append("密码必须包含至少一个大写字母") + # + # return len(errors) == 0, errors + + + + + """ + 开发环境 + """ + # 默认使用下面开发环境,在生产环境中请使用上面的校验规则! + @staticmethod + def validate_password_strength(password: str, + min_length: int = 6, + max_length: int = 128) -> Tuple[bool, List[str]]: + """ + 验证密码强度(仅检查长度,不少于 6 位即可) + + Args: + password: 密码 + min_length: 最小长度(默认 6) + max_length: 最大长度(默认 128) + + Returns: + Tuple[bool, List[str]]: (是否通过验证, 错误信息列表) + """ + errors = [] + + if not password: + errors.append("密码不能为空") + return False, errors + + if not isinstance(password, str): + errors.append("密码必须是字符串") + return False, errors + + # 仅长度检查 + if len(password) < min_length: + errors.append(f"密码长度至少{min_length}位") + + if len(password) > max_length: + errors.append(f"密码长度不能超过{max_length}位") + + return len(errors) == 0, errors + + + @staticmethod + def validate_simple_password(password: str, min_length: int = 6, max_length: int = 128) -> Tuple[bool, str]: + """ + 简单密码验证(只检查长度和基本字符) + + Args: + password: 密码 + min_length: 最小长度 + max_length: 最大长度 + + Returns: + Tuple[bool, str]: (是否通过验证, 错误信息) + """ + if not password: + return False, "密码不能为空" + + if not isinstance(password, str): + return False, "密码必须是字符串" + + if len(password) < min_length: + return False, f"密码长度至少{min_length}位" + + if len(password) > max_length: + return False, f"密码长度不能超过{max_length}位" + + # 检查是否包含数字或字母 + if not re.search(r'[a-zA-Z0-9]', password): + return False, "密码必须包含字母或数字" + + return True, "密码格式正确" + + @staticmethod + def check_common_passwords(password: str) -> bool: + """ + 检查是否为常见弱密码 + + Args: + password: 密码 + + Returns: + bool: 是否为常见弱密码 + """ + common_passwords = { + '123456', 'password', '123456789', '12345678', '12345', + '1234567', '1234567890', 'qwerty', 'abc123', '111111', + '123123123', 'admin', 'letmein', 'welcome', 'monkey', + '1234', 'dragon', 'pass', 'master', 'hello', + 'freedom', 'whatever', 'qazwsx', 'trustno1', 'jordan23' + } + + return password.lower() in common_passwords + + @staticmethod + def calculate_password_score(password: str) -> int: + """ + 计算密码强度分数(0-100) + + Args: + password: 密码 + + Returns: + int: 密码强度分数 + """ + if not password: + return 0 + + score = 0 + + # 长度分数(最多30分) + length_score = min(len(password) * 2, 30) + score += length_score + + # 字符类型分数 + if re.search(r'[a-z]', password): # 小写字母 + score += 10 + + if re.search(r'[A-Z]', password): # 大写字母 + score += 10 + + if re.search(r'\d', password): # 数字 + score += 10 + + if re.search(r'[!@#$%^&*(),.?":{}|<>]', password): # 特殊字符 + score += 15 + + # 字符多样性分数 + unique_chars = len(set(password)) + diversity_score = min(unique_chars * 2, 25) + score += diversity_score + + # 扣分项 + if PasswordValidator.check_common_passwords(password): + score -= 30 + + # 重复字符扣分 + if re.search(r'(.)\1{2,}', password): # 连续3个相同字符 + score -= 10 + + # 连续数字或字母扣分 + if re.search(r'(012|123|234|345|456|567|678|789|890)', password): + score -= 5 + + if re.search(r'(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)', password.lower()): + score -= 5 + + return max(0, min(100, score)) + + @staticmethod + def get_password_strength_level(password: str) -> str: + """ + 获取密码强度等级 + + Args: + password: 密码 + + Returns: + str: 密码强度等级 + """ + score = PasswordValidator.calculate_password_score(password) + + if score >= 80: + return "很强" + elif score >= 60: + return "强" + elif score >= 40: + return "中等" + elif score >= 20: + return "弱" + else: + return "很弱" \ No newline at end of file diff --git a/hertz_studio_django_utils/validators/phone_validator.py b/hertz_studio_django_utils/validators/phone_validator.py new file mode 100644 index 0000000..44b496d --- /dev/null +++ b/hertz_studio_django_utils/validators/phone_validator.py @@ -0,0 +1,178 @@ +import re +from typing import Tuple + + +class PhoneValidator: + """ + 手机号验证器 + 提供手机号格式验证功能 + """ + + # 中国大陆手机号正则表达式 + CHINA_MOBILE_PATTERN = re.compile( + r'^1[3-9]\d{9}$' + ) + + # 国际手机号正则表达式(简化版) + INTERNATIONAL_PATTERN = re.compile( + r'^\+?[1-9]\d{1,14}$' + ) + + @staticmethod + def is_valid_china_mobile(phone: str) -> bool: + """ + 验证中国大陆手机号格式是否正确 + + Args: + phone: 手机号 + + Returns: + bool: 手机号格式是否正确 + """ + if not phone or not isinstance(phone, str): + return False + + # 去除空格和连字符 + phone = phone.replace(' ', '').replace('-', '') + + return bool(PhoneValidator.CHINA_MOBILE_PATTERN.match(phone)) + + @staticmethod + def is_valid_international_phone(phone: str) -> bool: + """ + 验证国际手机号格式是否正确 + + Args: + phone: 手机号 + + Returns: + bool: 手机号格式是否正确 + """ + if not phone or not isinstance(phone, str): + return False + + # 去除空格和连字符 + phone = phone.replace(' ', '').replace('-', '') + + return bool(PhoneValidator.INTERNATIONAL_PATTERN.match(phone)) + + @staticmethod + def is_valid_phone(phone: str) -> bool: + """ + 验证手机号格式是否正确(默认使用中国大陆手机号验证) + + Args: + phone: 手机号 + + Returns: + bool: 手机号格式是否正确 + """ + return PhoneValidator.is_valid_china_mobile(phone) + + @staticmethod + def validate_china_mobile(phone: str) -> Tuple[bool, str]: + """ + 验证中国大陆手机号并返回详细信息 + + Args: + phone: 手机号 + + Returns: + Tuple[bool, str]: (是否有效, 提示信息) + """ + if not phone: + return False, "手机号不能为空" + + if not isinstance(phone, str): + return False, "手机号必须是字符串" + + # 去除空格和连字符 + phone = phone.replace(' ', '').replace('-', '') + + if len(phone) == 0: + return False, "手机号不能为空" + + if len(phone) != 11: + return False, "手机号长度必须为11位" + + if not phone.isdigit(): + return False, "手机号只能包含数字" + + if not phone.startswith('1'): + return False, "手机号必须以1开头" + + if phone[1] not in '3456789': + return False, "手机号第二位必须是3-9之间的数字" + + return True, "手机号格式正确" + + @staticmethod + def normalize_phone(phone: str) -> str: + """ + 标准化手机号 + + Args: + phone: 手机号 + + Returns: + str: 标准化后的手机号 + """ + if not phone or not isinstance(phone, str): + return '' + + # 去除空格、连字符和括号 + phone = phone.replace(' ', '').replace('-', '').replace('(', '').replace(')', '') + + # 如果是中国大陆手机号且以+86开头,去除+86 + if phone.startswith('+86') and len(phone) == 14: + phone = phone[3:] + elif phone.startswith('86') and len(phone) == 13: + phone = phone[2:] + + return phone + + @staticmethod + def get_mobile_carrier(phone: str) -> str: + """ + 获取手机号运营商(仅支持中国大陆) + + Args: + phone: 手机号 + + Returns: + str: 运营商名称 + """ + if not PhoneValidator.is_valid_china_mobile(phone): + return '未知' + + phone = PhoneValidator.normalize_phone(phone) + prefix = phone[:3] + + # 中国移动 + china_mobile_prefixes = { + '134', '135', '136', '137', '138', '139', + '147', '150', '151', '152', '157', '158', '159', + '172', '178', '182', '183', '184', '187', '188', + '195', '197', '198' + } + + # 中国联通 + china_unicom_prefixes = { + '130', '131', '132', '145', '155', '156', + '166', '171', '175', '176', '185', '186', '196' + } + + # 中国电信 + china_telecom_prefixes = { + '133', '149', '153', '173', '174', '177', + '180', '181', '189', '191', '193', '199' + } + + if prefix in china_mobile_prefixes: + return '中国移动' + elif prefix in china_unicom_prefixes: + return '中国联通' + elif prefix in china_telecom_prefixes: + return '中国电信' + else: + return '未知运营商' \ No newline at end of file diff --git a/hertz_studio_django_utils/yolo/__init__.py b/hertz_studio_django_utils/yolo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hertz_studio_django_utils/yolo/video_converter.py b/hertz_studio_django_utils/yolo/video_converter.py new file mode 100644 index 0000000..b120c56 --- /dev/null +++ b/hertz_studio_django_utils/yolo/video_converter.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +视频转换工具类 +用于将视频转换为H.264编码的MP4格式,确保浏览器兼容性 +""" + +import os +import subprocess +import json +import logging +from pathlib import Path +from typing import Optional, Dict, Any + +logger = logging.getLogger(__name__) + +class VideoConverter: + """视频格式转换工具类""" + + def __init__(self): + """初始化视频转换器""" + self.ffmpeg_available = self._check_ffmpeg() + + def _check_ffmpeg(self) -> bool: + """ + 检查FFmpeg是否可用 + + Returns: + bool: FFmpeg是否可用 + """ + try: + result = subprocess.run( + ['ffmpeg', '-version'], + capture_output=True, + text=True, + timeout=10 + ) + return result.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + logger.warning("FFmpeg未安装或不可用") + return False + + def get_video_info(self, video_path: str) -> Optional[Dict[str, Any]]: + """ + 获取视频信息 + + Args: + video_path: 视频文件路径 + + Returns: + Dict: 视频信息字典,包含编码格式、分辨率、时长等 + """ + if not self.ffmpeg_available: + logger.error("FFmpeg不可用,无法获取视频信息") + return None + + try: + cmd = [ + 'ffprobe', '-v', 'quiet', '-print_format', 'json', + '-show_format', '-show_streams', video_path + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + logger.error(f"获取视频信息失败: {result.stderr}") + return None + + info = json.loads(result.stdout) + + # 查找视频流 + video_stream = None + for stream in info.get('streams', []): + if stream.get('codec_type') == 'video': + video_stream = stream + break + + if not video_stream: + logger.error("未找到视频流") + return None + + return { + 'codec': video_stream.get('codec_name', 'unknown'), + 'width': video_stream.get('width', 0), + 'height': video_stream.get('height', 0), + 'duration': float(info.get('format', {}).get('duration', 0)), + 'size': os.path.getsize(video_path) if os.path.exists(video_path) else 0 + } + + except Exception as e: + logger.error(f"获取视频信息时出错: {str(e)}") + return None + + def is_h264_compatible(self, video_path: str) -> bool: + """ + 检查视频是否已经是H.264编码 + + Args: + video_path: 视频文件路径 + + Returns: + bool: 是否为H.264编码 + """ + video_info = self.get_video_info(video_path) + if not video_info: + return False + + return video_info.get('codec', '').lower() == 'h264' + + def convert_to_h264(self, input_path: str, output_path: Optional[str] = None, + quality: str = 'medium', overwrite: bool = True) -> Optional[str]: + """ + 将视频转换为H.264编码的MP4格式 + + Args: + input_path: 输入视频文件路径 + output_path: 输出视频文件路径(可选) + quality: 质量设置 ('high', 'medium', 'low') + overwrite: 是否覆盖已存在的文件 + + Returns: + str: 转换后的文件路径,失败返回None + """ + if not self.ffmpeg_available: + logger.error("FFmpeg不可用,无法进行视频转换") + return None + + input_path = Path(input_path) + + if not input_path.exists(): + logger.error(f"输入文件不存在: {input_path}") + return None + + # 检查是否已经是H.264格式 + if self.is_h264_compatible(str(input_path)): + logger.info(f"视频已经是H.264格式: {input_path}") + return str(input_path) + + # 生成输出文件路径 + if output_path is None: + output_path = input_path.parent / f"{input_path.stem}_h264.mp4" + else: + output_path = Path(output_path) + + # 检查输出文件是否已存在 + if output_path.exists() and not overwrite: + logger.info(f"输出文件已存在: {output_path}") + return str(output_path) + + # 设置质量参数 + quality_settings = { + 'high': {'crf': '18', 'preset': 'slow'}, + 'medium': {'crf': '23', 'preset': 'medium'}, + 'low': {'crf': '28', 'preset': 'fast'} + } + + settings = quality_settings.get(quality, quality_settings['medium']) + + # 构建FFmpeg命令 + cmd = [ + 'ffmpeg', + '-i', str(input_path), + '-c:v', 'libx264', # 使用H.264编码器 + '-crf', settings['crf'], # 质量设置 + '-preset', settings['preset'], # 编码速度预设 + '-c:a', 'aac', # 音频编码器 + '-b:a', '128k', # 音频比特率 + '-movflags', '+faststart', # 优化网络播放 + '-y' if overwrite else '-n', # 覆盖或跳过已存在文件 + str(output_path) + ] + + try: + logger.info(f"开始转换视频: {input_path} -> {output_path}") + + # 执行转换 + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300 # 5分钟超时 + ) + + if result.returncode == 0: + if output_path.exists(): + logger.info(f"视频转换成功: {output_path}") + + # 验证转换结果 + if self.is_h264_compatible(str(output_path)): + return str(output_path) + else: + logger.error("转换完成但格式验证失败") + return None + else: + logger.error("转换完成但输出文件未生成") + return None + else: + logger.error(f"视频转换失败: {result.stderr}") + return None + + except subprocess.TimeoutExpired: + logger.error("视频转换超时") + return None + except Exception as e: + logger.error(f"视频转换过程中出错: {str(e)}") + return None + + def ensure_h264_format(self, video_path: str, quality: str = 'medium') -> str: + """ + 确保视频为H.264格式,如果不是则自动转换 + + Args: + video_path: 视频文件路径 + quality: 转换质量设置 + + Returns: + str: H.264格式的视频文件路径 + """ + if self.is_h264_compatible(video_path): + return video_path + + converted_path = self.convert_to_h264(video_path, quality=quality) + return converted_path if converted_path else video_path + + def get_conversion_status(self) -> Dict[str, Any]: + """ + 获取转换器状态信息 + + Returns: + Dict: 状态信息 + """ + return { + 'ffmpeg_available': self.ffmpeg_available, + 'supported_formats': ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv'] if self.ffmpeg_available else [], + 'output_format': 'H.264 MP4' + } + +# 创建全局实例 +video_converter = VideoConverter() \ No newline at end of file diff --git a/license_verifier.py b/license_verifier.py new file mode 100644 index 0000000..e10451e --- /dev/null +++ b/license_verifier.py @@ -0,0 +1,60 @@ +import platform +import uuid +import hashlib +import requests + + +# 常量配置 +ACTIVATE_URL = "http://activate.hzsystems.cn/api/activate_machine" +PACKAGE_NAME = "hertz_studio_django_captcha" + + +def get_machine_id() -> str: + """生成机器码。 + + 根据当前系统信息(平台、架构、MAC地址)生成唯一机器码, + 使用 SHA256 取前16位并转大写,前缀为 HERTZ_STUDIO_。 + """ + system_info = f"{platform.platform()}-{platform.machine()}-{uuid.getnode()}" + return 'HERTZ_STUDIO_' + hashlib.sha256(system_info.encode()).hexdigest()[:16].upper() + + +def request_verify_machine_code(package_name: str, machine_code: str): + """请求后端校验机器码。 + + 向授权服务器提交包名和机器码,返回 JSON 响应; + 如果网络异常,返回 None。 + """ + try: + resp = requests.post( + ACTIVATE_URL, + json={"package_name": package_name, "machine_code": machine_code}, + timeout=10, + ) + return resp.json() + except requests.RequestException as e: + print(f"机器码验证请求失败: {e}") + return None + + +def verify_machine_license() -> None: + """运行时验证授权。 + + 在 Django 应用启动时执行授权验证:打印提示、生成机器码并请求校验; + 校验失败则抛出 RuntimeError 阻止应用启动。 + """ + machine_id = get_machine_id() + print(f"您的机器码: {machine_id}, 当前包名: {PACKAGE_NAME}") + print("请将此机器码发送给作者进行注册。") + + resp = request_verify_machine_code(PACKAGE_NAME, machine_id) + if resp and resp.get("success") is True: + print("=" * 60) + print("机器码验证成功!") + print("=" * 60) + return + + print("=" * 60) + print("机器码验证失败!请联系作者获取运行权限。") + print("=" * 60) + raise RuntimeError("Hertz Xxxxx license verification failed") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..995a69d --- /dev/null +++ b/setup.py @@ -0,0 +1,107 @@ +from setuptools import setup, find_packages +import sys +import platform +import uuid +import hashlib +import requests + + +package_name = 'hertz_studio_django_xxx' + +# 读取README文件内容 +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +# 读取requirements文件 +def read_requirements(): + with open("requirements.txt", "r", encoding="utf-8") as f: + return [line.strip() for line in f if line.strip() and not line.startswith("#")] + +# 机器码验证请求 +def request_verify_machine_code(package_name, machine_code): + """请求验证机器码""" + url = "http://activate.hzsystems.cn/api/activate_machine" + data = {"package_name": package_name, "machine_code": machine_code} + try: + response = requests.post(url, json=data, timeout=10) + return response.json() + except requests.RequestException as e: + print(f"机器码验证请求失败: {e}") + return None + +# 机器码验证功能 +def verify_machine_license(): + """验证机器码""" + print("\n" + "="*60) + print("欢迎使用 Hertz System xxx!") + print("="*60) + print("本软件需要机器码验证才能安装。") + print("请联系作者获取安装权限:hertz studio(563161210@qq.com)") + print("="*60) + + # 获取系统信息 + system_info = f"{platform.platform()}-{platform.machine()}-{uuid.getnode()}" + machine_id = 'HERTZ_STUDIO_'+hashlib.sha256(system_info.encode()).hexdigest()[:16].upper() + + print(f"您的机器码: {machine_id},当前安装的包名: {package_name}") + print("请将此机器码发送给作者进行注册。") + + # 请求验证机器码 + response = request_verify_machine_code(package_name, machine_id) + if response.get('success') == True: + print("=" * 60) + print("机器码验证成功!") + print("=" * 60) + else: + print("=" * 60) + print("机器码验证失败!请联系作者获取安装权限。") + print("=" * 60) + sys.exit(1) + + +# 在安装前验证机器码 +if 'install' in sys.argv or 'bdist_wheel' in sys.argv or 'sdist' in sys.argv: + verify_machine_license() + +setup( + name=package_name, # PyPI上的包名 + version="1.0.1", # 版本号 + author="yang kunhao", # 作者名 + author_email="563161210@qq.com", # 作者邮箱 + description="一个功能强大的Django验证码应用", # 简短描述 + long_description=long_description, + long_description_content_type="text/markdown", + url="http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx", # 项目地址 + packages=find_packages(include=[f"{package_name}*"]), # 自动发现并同时打包两个包 + include_package_data=True, # 包含MANIFEST.in中定义的文件 + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Framework :: Django", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + license="MIT", # 使用 SPDX 许可证标识符 + python_requires=">=3.10", # Python版本要求 + install_requires=read_requirements(), # 依赖包 + keywords="django xxx verification security", # 关键词 + project_urls={ + "Bug Reports": "http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx/issues", + "Source": "http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx", + "Documentation": "http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx#readme", + }, +) diff --git a/使用手册.md b/使用手册.md new file mode 100644 index 0000000..2567038 --- /dev/null +++ b/使用手册.md @@ -0,0 +1,114 @@ +# APP打包步骤 + + + +## 一、复制APP + +将要打包的APP复制粘贴到根目录 + + + +## 二、编辑APP下面的apps.py文件 + +在apps.py文件中添加该函数: + +```python +def ready(self): + """Django 应用启动时的钩子。 + 在此执行运行时授权校验,未授权则抛出异常阻止应用启动。 + """ + try: + from .license_verifier import verify_machine_license + verify_machine_license() + except Exception: + # 直接再抛出,让 Django 启动失败并展示错误信息 + raise +``` + + + +## 三、迁移license + +1. 将根目录下面的license_verifier.py复制粘贴到要打包的APP下 + +2. 修改license_verifier.py文件 + + 将下面的PACKAGE_NAME的值改为要把包的app名称 + + ``` + PACKAGE_NAME = "hertz_studio_django_xxxxx" + ``` + + 修改该脚本最后一行代码,将Xxxxx改为打包的app简称: + + ``` + raise RuntimeError("Hertz Xxxxx license verification failed") + ``` + + + +## 四、修改MANIFEST.in文件 + +将下面的hertz_studio_django_xxx改为打包的app名称 + +``` +# 包含二进制文件 +recursive-include hertz_studio_django_xxx *.py +recursive-include hertz_studio_django_xxx/migrations *.py +``` + + + +## 五、修改根目录下面的setup.py文件 + +- 修改成要打包的app名称(将hertz_studio_django_xxx改为打包的app名称) + +``` +package_name = 'hertz_studio_django_xxx' +``` + +- 将XXX改为app简称 + +``` +print("欢迎使用 Hertz System xxx!") +``` + +- 更改下载的项目地址(将hertz_studio_django_xxx改为打包的app名称) + +``` +url="http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx", # 项目地址 +``` + +- 更改关键词(将XXX改为app简称) + +``` +keywords="django XXXX verification security", # 关键词 +``` + +- 修改project_urls(将xxx改为app简称) + +``` +project_urls={ + + "Bug Reports": "http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx/issues", + + "Source": "http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx", + + "Documentation": "http://hzgit.hzsystems.cn/hertz_studio_django/hertz_studio_django_xxx#readme", + +} +``` + + + + + +## 六、打包 + +完成上述的修改后,在终端的项目输入以下命令并回车进行打包 + +``` +python setup.py sdist +``` + +运行完成后会在dist目录下生成一个打包过后的文件