diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/ai/__init__.py b/apps/ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/ai/admin.py b/apps/ai/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/ai/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/ai/apps.py b/apps/ai/apps.py new file mode 100644 index 0000000..4fc2e53 --- /dev/null +++ b/apps/ai/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.ai' diff --git a/apps/ai/migrations/__init__.py b/apps/ai/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/ai/models.py b/apps/ai/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/apps/ai/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/apps/ai/tests.py b/apps/ai/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/ai/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/ai/views.py b/apps/ai/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/ai/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/api/__init__.py b/apps/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/admin.py b/apps/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/api/apps.py b/apps/api/apps.py new file mode 100644 index 0000000..ae75201 --- /dev/null +++ b/apps/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.api' diff --git a/apps/api/migrations/__init__.py b/apps/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/models.py b/apps/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/apps/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/apps/api/tests.py b/apps/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/api/views.py b/apps/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/billing/__init__.py b/apps/billing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/billing/admin.py b/apps/billing/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/billing/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/billing/apps.py b/apps/billing/apps.py new file mode 100644 index 0000000..21971f8 --- /dev/null +++ b/apps/billing/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BillingConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.billing' diff --git a/apps/billing/migrations/__init__.py b/apps/billing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/billing/models.py b/apps/billing/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/apps/billing/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/apps/billing/tests.py b/apps/billing/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/billing/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/billing/views.py b/apps/billing/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/billing/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/portal/__init__.py b/apps/portal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/portal/admin.py b/apps/portal/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/portal/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/portal/apps.py b/apps/portal/apps.py new file mode 100644 index 0000000..59d451d --- /dev/null +++ b/apps/portal/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PortalConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.portal' diff --git a/apps/portal/migrations/__init__.py b/apps/portal/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/portal/models.py b/apps/portal/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/apps/portal/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/apps/portal/tests.py b/apps/portal/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/portal/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/portal/views.py b/apps/portal/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/portal/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/users/__init__.py b/apps/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/users/admin.py b/apps/users/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/users/apps.py b/apps/users/apps.py new file mode 100644 index 0000000..2bb189c --- /dev/null +++ b/apps/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.users' diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py new file mode 100644 index 0000000..aa7f42b --- /dev/null +++ b/apps/users/migrations/0001_initial.py @@ -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()), + ], + ), + ] diff --git a/apps/users/migrations/__init__.py b/apps/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/users/models.py b/apps/users/models.py new file mode 100644 index 0000000..d11efab --- /dev/null +++ b/apps/users/models.py @@ -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" diff --git a/apps/users/tests.py b/apps/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/users/views.py b/apps/users/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/users/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/config/__init__.py b/config/__init__.py index e69de29..9c0f756 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -0,0 +1,3 @@ +import pymysql + +pymysql.install_as_MySQLdb() diff --git a/config/settings.py b/config/settings.py index 01929ea..3d313cd 100644 --- a/config/settings.py +++ b/config/settings.py @@ -20,6 +20,17 @@ 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/ +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: value = os.environ.get(name) 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()] +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. 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 = [ 'rest_framework', + 'apps.users', + 'apps.portal', + 'apps.billing', + 'apps.ai', + 'apps.api', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -82,14 +107,24 @@ TEMPLATES = [ WSGI_APPLICATION = 'config.wsgi.application' +AUTH_USER_MODEL = 'users.User' + # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.mysql', + '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 # 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 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 832e930..49ac07f 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -23,7 +23,7 @@ | 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-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 | ## Phase 1 · 最高风险验证(AI 调用 + 可插拔供应商) diff --git a/docs/current-state.md b/docs/current-state.md index 18f08c9..8b3cadd 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -11,11 +11,11 @@ ## 当前快照 -- 日期:2026-07-01 -- 阶段:Phase 0 地基,T-001 已完成;下一步 T-002 -- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + django-admin;用户端(模板 SSR/Bootstrap/allauth) 与 MySQL 8.4 接入在后续任务落地;详见 `03-tech-stack.md` -- 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/` -- 测试:Django test runner 可运行,当前 0 tests +- 日期:2026-07-02 +- 阶段:Phase 0 地基,T-002 已完成;下一步 T-003 +- 技术栈:系统 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/`;T-002 已创建 `apps/users|portal|billing|ai|api` +- 测试:`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`) - 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh` - 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test` @@ -33,7 +33,7 @@ | `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令 | | `requirements.txt` | 已有 | Django 5.2 / DRF 3.16 依赖 | | `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 创建 | | `tests/` | 待建 | 随各任务补充 | @@ -43,7 +43,8 @@ - 已完成: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 ``` -当前骨架可运行。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`。 2. 读 `docs/00-ai-start-here.md`。 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`。 ## 维护规则 diff --git a/progress.md b/progress.md index 365a6ab..7976c9a 100644 --- a/progress.md +++ b/progress.md @@ -173,3 +173,25 @@ - 阻塞:无。 - 决策:T-001 不接入 MySQL、不创建自定义 User、不创建 apps 目录;这些按任务边界留给 T-002。T-001 阶段暂保留 Django 默认 SQLite 配置用于框架启动检查,T-002 必须在首次迁移前改为自定义 User + MySQL 8.4 配置。 - 下一步:领取 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 与最小测试。 diff --git a/requirements.txt b/requirements.txt index d8da618..3ba75d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Django>=5.2,<5.3 djangorestframework>=3.16,<3.17 +PyMySQL>=1.1,<1.2