代码上传
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
*.c
|
||||||
|
*.pyc
|
||||||
|
*.pyd
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.pypirc
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||||
14
MANIFEST.in
Normal file
14
MANIFEST.in
Normal file
@@ -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__
|
||||||
438
hertz_studio_django_utils/README.md
Normal file
438
hertz_studio_django_utils/README.md
Normal file
@@ -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="<h1>HTML内容</h1>",
|
||||||
|
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 = """
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>自定义邮件</h1>
|
||||||
|
<p>您好 {name},这是一封自定义邮件。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""".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) 或联系开发团队。
|
||||||
1
hertz_studio_django_utils/__init__.py
Normal file
1
hertz_studio_django_utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Hertz Server Django Utils Package
|
||||||
39
hertz_studio_django_utils/code_generator/__init__.py
Normal file
39
hertz_studio_django_utils/code_generator/__init__.py
Normal file
@@ -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'
|
||||||
|
]
|
||||||
286
hertz_studio_django_utils/code_generator/app_generator.py
Normal file
286
hertz_studio_django_utils/code_generator/app_generator.py
Normal file
@@ -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)
|
||||||
262
hertz_studio_django_utils/code_generator/base_generator.py
Normal file
262
hertz_studio_django_utils/code_generator/base_generator.py
Normal file
@@ -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')
|
||||||
@@ -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文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
💡 **提示**: 此配置模块是整个项目的核心,负责协调各功能模块的协同工作。生产部署前请务必检查所有安全配置。
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
||||||
@@ -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',
|
||||||
|
]
|
||||||
@@ -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])
|
||||||
@@ -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')
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
434
hertz_studio_django_utils/code_generator/menu_generator.py
Normal file
434
hertz_studio_django_utils/code_generator/menu_generator.py
Normal file
@@ -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()
|
||||||
262
hertz_studio_django_utils/code_generator/model_generator.py
Normal file
262
hertz_studio_django_utils/code_generator/model_generator.py
Normal file
@@ -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]
|
||||||
336
hertz_studio_django_utils/code_generator/serializer_generator.py
Normal file
336
hertz_studio_django_utils/code_generator/serializer_generator.py
Normal file
@@ -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)
|
||||||
248
hertz_studio_django_utils/code_generator/template_engine.py
Normal file
248
hertz_studio_django_utils/code_generator/template_engine.py
Normal file
@@ -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': '模板不存在'
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
"""
|
||||||
@@ -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
|
||||||
|
"""
|
||||||
@@ -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('<int:")
|
||||||
|
__M_writer(str(snake_model_name))
|
||||||
|
__M_writer("_id>/', 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('<int:")
|
||||||
|
__M_writer(str(snake_model_name))
|
||||||
|
__M_writer("_id>/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('<int:")
|
||||||
|
__M_writer(str(snake_model_name))
|
||||||
|
__M_writer("_id>/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('/<int:')
|
||||||
|
__M_writer(str(snake_model_name))
|
||||||
|
__M_writer("_id>/', 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
|
||||||
|
"""
|
||||||
@@ -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
|
||||||
|
"""
|
||||||
@@ -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}'
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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])
|
||||||
@@ -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])}]
|
||||||
@@ -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',
|
||||||
|
]
|
||||||
@@ -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('<int:${snake_model_name}_id>/', views.get_${snake_model_name}, name='get_${snake_model_name}'),
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if 'update' in operations_list:
|
||||||
|
# 更新${model_name}
|
||||||
|
path('<int:${snake_model_name}_id>/update/', views.update_${snake_model_name}, name='update_${snake_model_name}'),
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if 'delete' in operations_list:
|
||||||
|
# 删除${model_name}
|
||||||
|
path('<int:${snake_model_name}_id>/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}/<int:${snake_model_name}_id>/', views.update_${snake_model_name}, name='${snake_model_name}_detail'),
|
||||||
|
% endif
|
||||||
|
]
|
||||||
@@ -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
|
||||||
6
hertz_studio_django_utils/code_generator/test/apps.py
Normal file
6
hertz_studio_django_utils/code_generator/test/apps.py
Normal file
@@ -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 = '测试应用'
|
||||||
30
hertz_studio_django_utils/code_generator/test/models.py
Normal file
30
hertz_studio_django_utils/code_generator/test/models.py
Normal file
@@ -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
|
||||||
24
hertz_studio_django_utils/code_generator/test/serializers.py
Normal file
24
hertz_studio_django_utils/code_generator/test/serializers.py
Normal file
@@ -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()
|
||||||
6
hertz_studio_django_utils/code_generator/test/urls.py
Normal file
6
hertz_studio_django_utils/code_generator/test/urls.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from views import *
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('test/', test, name='test'),
|
||||||
|
]
|
||||||
350
hertz_studio_django_utils/code_generator/url_generator.py
Normal file
350
hertz_studio_django_utils/code_generator/url_generator.py
Normal file
@@ -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()}/<int:pk>/',
|
||||||
|
'view': f'get_{model_name.lower()}',
|
||||||
|
'name': f'{model_name.lower()}_detail'
|
||||||
|
})
|
||||||
|
elif operation == 'update':
|
||||||
|
context['url_patterns'].append({
|
||||||
|
'pattern': f'{model_name.lower()}/<int:pk>/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()}/<int:pk>/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)
|
||||||
416
hertz_studio_django_utils/code_generator/view_generator.py
Normal file
416
hertz_studio_django_utils/code_generator/view_generator.py
Normal file
@@ -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)
|
||||||
184
hertz_studio_django_utils/code_generator/yaml_generator_cli.py
Normal file
184
hertz_studio_django_utils/code_generator/yaml_generator_cli.py
Normal file
@@ -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())
|
||||||
372
hertz_studio_django_utils/code_generator/yaml_parser.py
Normal file
372
hertz_studio_django_utils/code_generator/yaml_parser.py
Normal file
@@ -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)
|
||||||
0
hertz_studio_django_utils/config/__init__.py
Normal file
0
hertz_studio_django_utils/config/__init__.py
Normal file
24
hertz_studio_django_utils/config/departments_config.py
Normal file
24
hertz_studio_django_utils/config/departments_config.py
Normal file
@@ -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'
|
||||||
|
}
|
||||||
|
]
|
||||||
625
hertz_studio_django_utils/config/menus_config.py
Normal file
625
hertz_studio_django_utils/config/menus_config.py
Normal file
@@ -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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
20
hertz_studio_django_utils/config/roles_config.py
Normal file
20
hertz_studio_django_utils/config/roles_config.py
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
4
hertz_studio_django_utils/crypto/__init__.py
Normal file
4
hertz_studio_django_utils/crypto/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .encryption_utils import EncryptionUtils
|
||||||
|
from .password_hashers import MD5PasswordHasher
|
||||||
|
|
||||||
|
__all__ = ['EncryptionUtils', 'MD5PasswordHasher']
|
||||||
160
hertz_studio_django_utils/crypto/encryption_utils.py
Normal file
160
hertz_studio_django_utils/crypto/encryption_utils.py
Normal file
@@ -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')
|
||||||
79
hertz_studio_django_utils/crypto/password_hashers.py
Normal file
79
hertz_studio_django_utils/crypto/password_hashers.py
Normal file
@@ -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
|
||||||
3
hertz_studio_django_utils/email/__init__.py
Normal file
3
hertz_studio_django_utils/email/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .email_service import EmailService
|
||||||
|
|
||||||
|
__all__ = ['EmailService']
|
||||||
174
hertz_studio_django_utils/email/email_service.py
Normal file
174
hertz_studio_django_utils/email/email_service.py
Normal file
@@ -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'''
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<div style="background: #28a745; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||||
|
<h1 style="margin: 0; font-size: 24px;">🔐 {title}</h1>
|
||||||
|
</div>
|
||||||
|
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||||
|
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||||
|
<p>{description}</p>
|
||||||
|
<div style="text-align: center; margin: 40px 0;">
|
||||||
|
<div style="background: #f8f9fa; border: 2px dashed #28a745; padding: 20px; border-radius: 10px; display: inline-block;">
|
||||||
|
<span style="font-size: 32px; font-weight: bold; color: #28a745; letter-spacing: 5px;">{verification_code}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="color: #666; font-size: 14px;">验证码有效期为5分钟,请尽快使用。如果您没有进行此操作,请忽略此邮件。</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
|
||||||
|
text_content = f'''
|
||||||
|
{title}
|
||||||
|
|
||||||
|
亲爱的 {recipient_name},
|
||||||
|
|
||||||
|
{description}
|
||||||
|
|
||||||
|
验证码:{verification_code}
|
||||||
|
|
||||||
|
验证码有效期为5分钟,请尽快使用。如果您没有进行此操作,请忽略此邮件。
|
||||||
|
|
||||||
|
此致
|
||||||
|
Hertz Server Django 团队
|
||||||
|
'''
|
||||||
|
|
||||||
|
return {
|
||||||
|
'subject': subject,
|
||||||
|
'html_content': html_content,
|
||||||
|
'text_content': text_content
|
||||||
|
}
|
||||||
0
hertz_studio_django_utils/log/__init__.py
Normal file
0
hertz_studio_django_utils/log/__init__.py
Normal file
368
hertz_studio_django_utils/log/log_decorator.py
Normal file
368
hertz_studio_django_utils/log/log_decorator.py
Normal file
@@ -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
|
||||||
0
hertz_studio_django_utils/ollama/__init__.py
Normal file
0
hertz_studio_django_utils/ollama/__init__.py
Normal file
207
hertz_studio_django_utils/ollama/ollama_client.py
Normal file
207
hertz_studio_django_utils/ollama/ollama_client.py
Normal file
@@ -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")
|
||||||
185
hertz_studio_django_utils/responses/HertzResponse.py
Normal file
185
hertz_studio_django_utils/responses/HertzResponse.py
Normal file
@@ -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})
|
||||||
3
hertz_studio_django_utils/responses/__init__.py
Normal file
3
hertz_studio_django_utils/responses/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .HertzResponse import HertzResponse
|
||||||
|
|
||||||
|
__all__ = ['HertzResponse']
|
||||||
5
hertz_studio_django_utils/validators/__init__.py
Normal file
5
hertz_studio_django_utils/validators/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .email_validator import EmailValidator
|
||||||
|
from .phone_validator import PhoneValidator
|
||||||
|
from .password_validator import PasswordValidator
|
||||||
|
|
||||||
|
__all__ = ['EmailValidator', 'PhoneValidator', 'PasswordValidator']
|
||||||
117
hertz_studio_django_utils/validators/email_validator.py
Normal file
117
hertz_studio_django_utils/validators/email_validator.py
Normal file
@@ -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
|
||||||
229
hertz_studio_django_utils/validators/password_validator.py
Normal file
229
hertz_studio_django_utils/validators/password_validator.py
Normal file
@@ -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 "很弱"
|
||||||
178
hertz_studio_django_utils/validators/phone_validator.py
Normal file
178
hertz_studio_django_utils/validators/phone_validator.py
Normal file
@@ -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 '未知运营商'
|
||||||
0
hertz_studio_django_utils/yolo/__init__.py
Normal file
0
hertz_studio_django_utils/yolo/__init__.py
Normal file
244
hertz_studio_django_utils/yolo/video_converter.py
Normal file
244
hertz_studio_django_utils/yolo/video_converter.py
Normal file
@@ -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()
|
||||||
60
license_verifier.py
Normal file
60
license_verifier.py
Normal file
@@ -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")
|
||||||
0
requirements.txt
Normal file
0
requirements.txt
Normal file
107
setup.py
Normal file
107
setup.py
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
)
|
||||||
114
使用手册.md
Normal file
114
使用手册.md
Normal file
@@ -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目录下生成一个打包过后的文件
|
||||||
Reference in New Issue
Block a user