feat: add apps and custom user

This commit is contained in:
QiuSW
2026-07-02 09:07:15 +08:00
parent 1cd7f0819d
commit af0302c3f6
43 changed files with 237 additions and 14 deletions
+1
View File
@@ -0,0 +1 @@
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.ai'
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.api'
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BillingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.billing'
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class PortalConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.portal'
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.users'
+45
View File
@@ -0,0 +1,45 @@
# Generated by Django 5.2.15 on 2026-07-02 00:52
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, verbose_name='email address')),
('payment_user_id', models.CharField(blank=True, help_text='External payment system user identifier, if available.', max_length=128)),
('status', models.CharField(choices=[('active', 'Active'), ('disabled', 'Disabled')], default='active', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'db_table': 'user',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
View File
+28
View File
@@ -0,0 +1,28 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
class Status(models.TextChoices):
ACTIVE = "active", "Active"
DISABLED = "disabled", "Disabled"
email = models.EmailField("email address")
payment_user_id = models.CharField(
max_length=128,
blank=True,
help_text="External payment system user identifier, if available.",
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.ACTIVE,
)
created_at = models.DateTimeField(auto_now_add=True)
@property
def is_business_active(self) -> bool:
return self.status == self.Status.ACTIVE and self.is_active
class Meta:
db_table = "user"
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
+3
View File
@@ -0,0 +1,3 @@
import pymysql
pymysql.install_as_MySQLdb()
+39 -4
View File
@@ -20,6 +20,17 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
def load_dotenv(path: Path) -> None:
if not path.exists():
return
for raw_line in path.read_text(encoding="utf-8-sig").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
def env_bool(name: str, default: bool = False) -> bool: def env_bool(name: str, default: bool = False) -> bool:
value = os.environ.get(name) value = os.environ.get(name)
if value is None: if value is None:
@@ -32,6 +43,15 @@ def env_list(name: str, default: str = "") -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()] return [item.strip() for item in value.split(",") if item.strip()]
def env_int(name: str, default: int) -> int:
value = os.environ.get(name)
if not value:
return default
return int(value)
load_dotenv(BASE_DIR / ".env")
# SECURITY WARNING: keep the secret key used in production secret. # SECURITY WARNING: keep the secret key used in production secret.
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "django-insecure-dev-only-change-me") SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "django-insecure-dev-only-change-me")
@@ -45,6 +65,11 @@ ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost")
INSTALLED_APPS = [ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'apps.users',
'apps.portal',
'apps.billing',
'apps.ai',
'apps.api',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -82,14 +107,24 @@ TEMPLATES = [
WSGI_APPLICATION = 'config.wsgi.application' WSGI_APPLICATION = 'config.wsgi.application'
AUTH_USER_MODEL = 'users.User'
# Database # Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases # https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.mysql',
'NAME': BASE_DIR / 'db.sqlite3', 'HOST': os.environ.get('MYSQL_HOST', '127.0.0.1'),
'PORT': env_int('MYSQL_PORT', 3306),
'NAME': os.environ.get('MYSQL_DATABASE', 'cmhub'),
'USER': os.environ.get('MYSQL_USER', ''),
'PASSWORD': os.environ.get('MYSQL_PASSWORD', ''),
'OPTIONS': {
'charset': os.environ.get('MYSQL_CHARSET', 'utf8mb4'),
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
} }
} }
@@ -116,9 +151,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/ # https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'UTC' TIME_ZONE = os.environ.get('DJANGO_TIME_ZONE', 'Asia/Shanghai')
USE_I18N = True USE_I18N = True
+1 -1
View File
@@ -23,7 +23,7 @@
| ID | 任务 | 依赖 | 验收要点 | 状态 | | ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| T-001 | 初始化 Django + DRF 项目骨架 | - | `manage.py` 可运行;`runserver` 起得来;用真实命令替换 `init.sh`/`init.ps1` 与 `00-ai-start-here.md`/`03-tech-stack.md`/`current-state.md` 的占位命令 | DONE | | T-001 | 初始化 Django + DRF 项目骨架 | - | `manage.py` 可运行;`runserver` 起得来;用真实命令替换 `init.sh`/`init.ps1` 与 `00-ai-start-here.md`/`03-tech-stack.md`/`current-state.md` 的占位命令 | DONE |
| T-002 | 建立 apps 目录、自定义 User 与配置 | T-001 | 按 `04-architecture.md` 建 `apps/users|portal|billing|ai|api`;**首次迁移前定义自定义 `User` 模型(设 `AUTH_USER_MODEL`)**;settings 用环境变量读密钥、配 MySQL(utf8mb4),无明文密钥 | TODO | | T-002 | 建立 apps 目录、自定义 User 与配置 | T-001 | 按 `04-architecture.md` 建 `apps/users|portal|billing|ai|api`;**首次迁移前定义自定义 `User` 模型(设 `AUTH_USER_MODEL`)**;settings 用环境变量读密钥、配 MySQL(utf8mb4),无明文密钥 | DONE |
| T-003 | 接通 django-admin 与最小测试 | T-001 | `createsuperuser` 后能登录 `/admin/`;`manage.py test` 可运行(至少 1 条占位测试通过) | TODO | | T-003 | 接通 django-admin 与最小测试 | T-001 | `createsuperuser` 后能登录 `/admin/`;`manage.py test` 可运行(至少 1 条占位测试通过) | TODO |
## Phase 1 · 最高风险验证(AI 调用 + 可插拔供应商) ## Phase 1 · 最高风险验证(AI 调用 + 可插拔供应商)
+10 -9
View File
@@ -11,11 +11,11 @@
## 当前快照 ## 当前快照
- 日期:2026-07-01 - 日期:2026-07-02
- 阶段:Phase 0 地基,T-001 已完成;下一步 T-002 - 阶段:Phase 0 地基,T-002 已完成;下一步 T-003
- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + django-admin;用户端(模板 SSR/Bootstrap/allauth) 与 MySQL 8.4 接入在后续任务落地;详见 `03-tech-stack.md` - 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + PyMySQL 1.1.3 + django-admin;MySQL 8.4 已接入 settings;用户端(模板 SSR/Bootstrap/allauth) 后续任务落地;详见 `03-tech-stack.md`
- 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/` - 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/`;T-002 已创建 `apps/users|portal|billing|ai|api`
- 测试:Django test runner 可运行,当前 0 tests - 测试:`manage.py check` / `manage.py test` 通过;MySQL 迁移已成功
- 数据:AI 上游调用与模型配置参考 `D:\chengma\cmbot`(`src/services/ai_text_service.py`、`ai_image_service.py`、`config/ai_models.json`) - 数据:AI 上游调用与模型配置参考 `D:\chengma\cmbot`(`src/services/ai_text_service.py`、`ai_image_service.py`、`config/ai_models.json`)
- 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh` - 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh`
- 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test` - 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test`
@@ -33,7 +33,7 @@
| `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令 | | `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令 |
| `requirements.txt` | 已有 | Django 5.2 / DRF 3.16 依赖 | | `requirements.txt` | 已有 | Django 5.2 / DRF 3.16 依赖 |
| `config/`(Django 工程) | 已有 | T-001 创建,含 settings / urls / wsgi / asgi | | `config/`(Django 工程) | 已有 | T-001 创建,含 settings / urls / wsgi / asgi |
| `apps/`(users/portal/billing/ai/api) | 待建 | T-002 创建(含自定义 User,首迁移前定) | | `apps/`(users/portal/billing/ai/api) | 已有 | T-002 创建;`apps/users` 已定义自定义 `User` |
| `manage.py` | 已有 | T-001 创建 | | `manage.py` | 已有 | T-001 创建 |
| `tests/` | 待建 | 随各任务补充 | | `tests/` | 待建 | 随各任务补充 |
@@ -43,7 +43,8 @@
- 已完成:T-001 初始化 Django + DRF 项目骨架。 - 已完成:T-001 初始化 Django + DRF 项目骨架。
- 正在进行:无。 - 正在进行:无。
- 下一个可领取任务:**T-002 建立 apps 目录、自定义 User 与配置**。 - 当前 blocker:无。
- 下一个可领取任务:**T-003 接通 django-admin 与最小测试**。
## 当前可运行内容 ## 当前可运行内容
@@ -61,14 +62,14 @@ python3.12 manage.py test
python3.12 manage.py runserver python3.12 manage.py runserver
``` ```
当前骨架可运行。T-001 暂保留 Django 默认 SQLite 配置用于框架启动检查;T-002 必须在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4。 当前骨架可运行。T-002 已在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4;远程 MySQL 已完成 Django 初始迁移。
## 开始编码前检查 ## 开始编码前检查
1. 读仓库级 `AGENTS.md` / `CLAUDE.md`。 1. 读仓库级 `AGENTS.md` / `CLAUDE.md`。
2. 读 `docs/00-ai-start-here.md`。 2. 读 `docs/00-ai-start-here.md`。
3. 读 `docs/05-coding-rules.md`(尤其第 8 节资金安全)。 3. 读 `docs/05-coding-rules.md`(尤其第 8 节资金安全)。
4. 在 `docs/06-tasks.md` 取第一个 `TODO` 且依赖均 `DONE` 的任务(当前为 T-002)。 4. 在 `docs/06-tasks.md` 取第一个 `TODO` 且依赖均 `DONE` 的任务(当前为 T-003)。
5. 将该任务状态改为 `DOING`。 5. 将该任务状态改为 `DOING`。
## 维护规则 ## 维护规则
+22
View File
@@ -173,3 +173,25 @@
- 阻塞:无。 - 阻塞:无。
- 决策:T-001 不接入 MySQL、不创建自定义 User、不创建 apps 目录;这些按任务边界留给 T-002。T-001 阶段暂保留 Django 默认 SQLite 配置用于框架启动检查,T-002 必须在首次迁移前改为自定义 User + MySQL 8.4 配置。 - 决策:T-001 不接入 MySQL、不创建自定义 User、不创建 apps 目录;这些按任务边界留给 T-002。T-001 阶段暂保留 Django 默认 SQLite 配置用于框架启动检查,T-002 必须在首次迁移前改为自定义 User + MySQL 8.4 配置。
- 下一步:领取 T-002 建立 apps 目录、自定义 User 与配置。 - 下一步:领取 T-002 建立 apps 目录、自定义 User 与配置。
## 2026-07-02 T-002 建立 apps 目录、自定义 User 与配置
- 状态:DONE
- 变更:
- 创建 `apps/` 包与 `apps/users|portal|billing|ai|api` 五个 Django app。
- `apps/users.models.User` 继承 `AbstractUser`,新增 `payment_user_id`、`status`、`created_at`,邮箱改为必填,`db_table="user"`;`settings.AUTH_USER_MODEL="users.User"` 已设置。
- `settings.py` 增加根目录 `.env` 读取,数据库从 SQLite 切换到 MySQL,配置 `utf8mb4` 与严格 SQL 模式。
- 采用 `PyMySQL` 作为 MySQL 驱动,更新 `requirements.txt` 与 `config/__init__.py`。
- 生成 `apps/users/migrations/0001_initial.py`。
- 验证:
- `py -3.12 -m pip install -r requirements.txt`:通过,安装 PyMySQL 1.1.3。
- `py -3.12 manage.py check`:通过,0 issues。
- `py -3.12 manage.py makemigrations users`:通过,生成 `0001_initial.py`。
- `py -3.12 manage.py migrate`:首次失败,MySQL 返回 `ALTER command denied to user 'test'@'61.141.174.182' for table 'django_content_type'`;用户在宝塔/MySQL 授权后,清理失败迁移留下的空表并重新执行,最终通过。
- `SHOW GRANTS FOR CURRENT_USER()`:授权后显示 `GRANT ALL PRIVILEGES ON cmhub.* TO test@%`。
- `py -3.12 manage.py check`:通过,0 issues。
- `py -3.12 manage.py test`:通过,当前 0 tests。
- MySQL 验证:存在自定义 `user` 表,含 `payment_user_id` / `status` / `created_at`;未创建默认 `auth_user` 表;`django_migrations` 共 19 条。
- 阻塞:已解除。曾因 MySQL 用户缺少 `ALTER/INDEX/DROP` 等迁移权限受阻。
- 决策:使用 PyMySQL 作为 MySQL 驱动;T-002 只落 apps、自定义 User、settings 与 MySQL 初始迁移,不提前实现 UserWallet/ApiKey 等 T-201 内容。
- 下一步:领取 T-003 接通 django-admin 与最小测试。
+1
View File
@@ -1,2 +1,3 @@
Django>=5.2,<5.3 Django>=5.2,<5.3
djangorestframework>=3.16,<3.17 djangorestframework>=3.16,<3.17
PyMySQL>=1.1,<1.2