feat: add portal allauth account flow
This commit is contained in:
@@ -3,6 +3,8 @@ DJANGO_SECRET_KEY=change-me-generate-a-random-secret
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,testserver
|
||||
DJANGO_TIME_ZONE=Asia/Shanghai
|
||||
DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||
DJANGO_DEFAULT_FROM_EMAIL=noreply@cmhub.local
|
||||
|
||||
# MySQL 8.4
|
||||
MYSQL_HOST=127.0.0.1
|
||||
|
||||
@@ -25,7 +25,7 @@ Python 3.12 / Django 5.2 LTS + DRF / django-admin / 用户端 Django 模板 SSR
|
||||
|
||||
## 当前状态
|
||||
|
||||
Phase 2 计费核心已完成,Phase 3 对外 API 与充值已完成到 T-306:对外 API 可用 `Authorization: Bearer <API_KEY>` 调用生成能力或查询点数余额;用户端 session 流程可创建充值订单、获取二维码票据并轮询订单状态,充值回调按订单幂等入账;`image_url` 已做 SSRF 防护,生成/认证失败有限流,充值下单有单笔金额上限。下一步进入 Phase 4 用户端,先做 T-501 注册 / 登录(allauth)。详见 [`docs/current-state.md`](docs/current-state.md)。
|
||||
Phase 2 计费核心已完成,Phase 3 对外 API 与充值已完成到 T-306,Phase 4 已完成 T-501 注册 / 登录:用户端可通过 allauth 自助注册、邮箱验证、登录、登出,注册后自动创建 0 点钱包且不写赠点流水。下一步做 T-502 API Key 自助管理页。详见 [`docs/current-state.md`](docs/current-state.md)。
|
||||
|
||||
> ⚠️ 涉及资金/点数。改动充值、扣费、退款、对账相关代码前,先读 [`docs/05-coding-rules.md`](docs/05-coding-rules.md) 第 8 节与 [`docs/04-architecture.md`](docs/04-architecture.md) 第四节计费时序。
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
|
||||
from apps.users.models import UserWallet
|
||||
|
||||
|
||||
class CmhubAccountAdapter(DefaultAccountAdapter):
|
||||
def save_user(self, request, user, form, commit=True):
|
||||
with transaction.atomic():
|
||||
saved_user = super().save_user(request, user, form, commit=commit)
|
||||
if commit:
|
||||
UserWallet.objects.get_or_create(
|
||||
user=saved_user,
|
||||
defaults={"points_balance": 0},
|
||||
)
|
||||
return saved_user
|
||||
@@ -0,0 +1,7 @@
|
||||
你好,
|
||||
|
||||
请打开下面的链接完成 cmhub 邮箱验证:
|
||||
|
||||
{{ activate_url }}
|
||||
|
||||
如果这不是你的操作,可以忽略本邮件。
|
||||
@@ -0,0 +1 @@
|
||||
[cmhub] 验证你的邮箱
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}登录 - cmhub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cmhub-panel">
|
||||
<h1 class="h4 mb-3">登录</h1>
|
||||
<form method="post" action="{% url 'portal-login' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{{ form.as_p }}
|
||||
{% if redirect_field_value %}
|
||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}">
|
||||
{% endif %}
|
||||
<button class="btn btn-primary w-100" type="submit">登录</button>
|
||||
</form>
|
||||
<div class="mt-3 text-secondary">
|
||||
没有账号?<a href="{% url 'portal-signup' %}">注册</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}退出 - cmhub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cmhub-panel">
|
||||
<h1 class="h4 mb-3">退出登录</h1>
|
||||
<form method="post" action="{% url 'portal-logout' %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-primary w-100" type="submit">退出</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}注册 - cmhub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cmhub-panel">
|
||||
<h1 class="h4 mb-3">注册</h1>
|
||||
<form method="post" action="{% url 'portal-signup' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{{ form.as_p }}
|
||||
{% if redirect_field_value %}
|
||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}">
|
||||
{% endif %}
|
||||
<button class="btn btn-primary w-100" type="submit">注册</button>
|
||||
</form>
|
||||
<div class="mt-3 text-secondary">
|
||||
已有账号?<a href="{% url 'portal-login' %}">登录</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}验证邮箱 - cmhub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cmhub-panel">
|
||||
<h1 class="h4 mb-3">验证邮箱</h1>
|
||||
<p class="mb-0 text-secondary">验证邮件已发送,请先完成邮箱验证再登录。</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,99 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}cmhub{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--cmhub-bg: #f6f7f9;
|
||||
--cmhub-ink: #17202a;
|
||||
--cmhub-line: #d8dee6;
|
||||
--cmhub-accent: #0f766e;
|
||||
}
|
||||
body {
|
||||
background: var(--cmhub-bg);
|
||||
color: var(--cmhub-ink);
|
||||
}
|
||||
.navbar {
|
||||
border-bottom: 1px solid var(--cmhub-line);
|
||||
background: #ffffff;
|
||||
}
|
||||
.cmhub-shell {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 16px;
|
||||
}
|
||||
.cmhub-panel {
|
||||
max-width: 440px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--cmhub-line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 28px;
|
||||
}
|
||||
.cmhub-panel form p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.cmhub-panel label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cmhub-panel input {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
border: 1px solid #b9c2cf;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.cmhub-panel .errorlist {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 18px;
|
||||
color: #b42318;
|
||||
}
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--cmhub-accent);
|
||||
--bs-btn-border-color: var(--cmhub-accent);
|
||||
--bs-btn-hover-bg: #0b5f59;
|
||||
--bs-btn-hover-border-color: #0b5f59;
|
||||
}
|
||||
.metric {
|
||||
border: 1px solid var(--cmhub-line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold" href="{% url 'portal-home' %}">cmhub</a>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-dashboard' %}">控制台</a>
|
||||
<form method="post" action="{% url 'portal-logout' %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">退出</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-login' %}">登录</a>
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'portal-signup' %}">注册</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="cmhub-shell">
|
||||
{% if messages %}
|
||||
<div class="mb-3">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags|default:'info' }} mb-2">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}控制台 - cmhub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex flex-column gap-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">控制台</h1>
|
||||
<div class="text-secondary">{{ user.email }}</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="metric">
|
||||
<div class="text-secondary small">剩余点数</div>
|
||||
<div class="display-6 fw-semibold">{{ balance.points_balance }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+113
-2
@@ -1,3 +1,114 @@
|
||||
from django.test import TestCase
|
||||
import uuid
|
||||
|
||||
# Create your tests here.
|
||||
from allauth.account.models import EmailAddress
|
||||
from django.contrib.auth import get_user, get_user_model
|
||||
from django.core import mail
|
||||
from django.core.cache import cache
|
||||
from django.test import Client, TestCase, override_settings
|
||||
|
||||
from apps.billing.models import PointsLedger
|
||||
from apps.users.models import UserWallet
|
||||
|
||||
|
||||
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
||||
class PortalAccountFlowTests(TestCase):
|
||||
password = "Strong-pass-123"
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
|
||||
def create_verified_user(self):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
user = get_user_model().objects.create_user(
|
||||
username=f"portal-user-{suffix}",
|
||||
email=f"portal-user-{suffix}@example.com",
|
||||
password=self.password,
|
||||
)
|
||||
EmailAddress.objects.create(
|
||||
user=user,
|
||||
email=user.email,
|
||||
verified=True,
|
||||
primary=True,
|
||||
)
|
||||
return user
|
||||
|
||||
def test_signup_creates_unverified_user_wallet_with_zero_points_and_no_ledger(self):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
email = f"signup-{suffix}@example.com"
|
||||
|
||||
response = self.client.post(
|
||||
"/signup",
|
||||
{
|
||||
"username": f"signup-{suffix}",
|
||||
"email": email,
|
||||
"password1": self.password,
|
||||
"password2": self.password,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
user = get_user_model().objects.get(email=email)
|
||||
email_address = EmailAddress.objects.get(user=user, email=email)
|
||||
wallet = UserWallet.objects.get(user=user)
|
||||
self.assertFalse(email_address.verified)
|
||||
self.assertTrue(email_address.primary)
|
||||
self.assertEqual(wallet.points_balance, 0)
|
||||
self.assertFalse(PointsLedger.objects.filter(user=user).exists())
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
|
||||
def test_unverified_email_cannot_establish_login_session(self):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
user = get_user_model().objects.create_user(
|
||||
username=f"unverified-{suffix}",
|
||||
email=f"unverified-{suffix}@example.com",
|
||||
password=self.password,
|
||||
)
|
||||
EmailAddress.objects.create(
|
||||
user=user,
|
||||
email=user.email,
|
||||
verified=False,
|
||||
primary=True,
|
||||
)
|
||||
|
||||
self.client.post("/login", {"login": user.email, "password": self.password})
|
||||
|
||||
self.assertFalse(get_user(self.client).is_authenticated)
|
||||
|
||||
def test_verified_user_can_login_view_dashboard_and_logout(self):
|
||||
user = self.create_verified_user()
|
||||
UserWallet.objects.create(user=user, points_balance=42)
|
||||
|
||||
login_response = self.client.post(
|
||||
"/login",
|
||||
{"login": user.email, "password": self.password},
|
||||
)
|
||||
|
||||
self.assertEqual(login_response.status_code, 302)
|
||||
self.assertEqual(login_response["Location"], "/dashboard")
|
||||
self.assertTrue(get_user(self.client).is_authenticated)
|
||||
|
||||
dashboard_response = self.client.get("/dashboard")
|
||||
self.assertEqual(dashboard_response.status_code, 200)
|
||||
self.assertContains(dashboard_response, user.email)
|
||||
self.assertContains(dashboard_response, "42")
|
||||
|
||||
logout_response = self.client.post("/logout")
|
||||
self.assertEqual(logout_response.status_code, 302)
|
||||
self.assertEqual(logout_response["Location"], "/login")
|
||||
self.assertFalse(get_user(self.client).is_authenticated)
|
||||
|
||||
def test_dashboard_requires_session_login(self):
|
||||
response = self.client.get("/dashboard")
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response["Location"].startswith("/login?next="))
|
||||
|
||||
def test_login_post_is_csrf_protected(self):
|
||||
csrf_client = Client(enforce_csrf_checks=True)
|
||||
|
||||
response = csrf_client.post(
|
||||
"/login",
|
||||
{"login": "nobody@example.com", "password": "password"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from allauth.account.views import LoginView, LogoutView, SignupView
|
||||
from django.urls import path
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from .views import DashboardView
|
||||
|
||||
urlpatterns = [
|
||||
path("", RedirectView.as_view(pattern_name="portal-dashboard", permanent=False), name="portal-home"),
|
||||
path("signup", SignupView.as_view(), name="portal-signup"),
|
||||
path("login", LoginView.as_view(), name="portal-login"),
|
||||
path("logout", LogoutView.as_view(), name="portal-logout"),
|
||||
path("dashboard", DashboardView.as_view(), name="portal-dashboard"),
|
||||
]
|
||||
+12
-2
@@ -1,3 +1,13 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
# Create your views here.
|
||||
from apps.billing.services import get_balance_snapshot
|
||||
|
||||
|
||||
class DashboardView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "portal/dashboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["balance"] = get_balance_snapshot(self.request.user)
|
||||
return context
|
||||
|
||||
+27
-1
@@ -76,6 +76,11 @@ PAYMENT_MOCK_CALLBACK_SECRET = os.environ.get(
|
||||
"cmhub-dev-mock-callback-secret" if DEBUG else "",
|
||||
)
|
||||
PAYMENT_QR_EXPIRES_MINUTES = env_int("PAYMENT_QR_EXPIRES_MINUTES", 10)
|
||||
EMAIL_BACKEND = os.environ.get(
|
||||
"DJANGO_EMAIL_BACKEND",
|
||||
"django.core.mail.backends.console.EmailBackend",
|
||||
)
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("DJANGO_DEFAULT_FROM_EMAIL", "noreply@cmhub.local")
|
||||
WECHAT_PAY_APPID = os.environ.get("WECHAT_PAY_APPID", "")
|
||||
WECHAT_PAY_MCHID = os.environ.get("WECHAT_PAY_MCHID", "")
|
||||
WECHAT_PAY_API_V3_KEY = os.environ.get("WECHAT_PAY_API_V3_KEY", "")
|
||||
@@ -108,6 +113,8 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'rest_framework',
|
||||
'apps.users',
|
||||
'apps.portal',
|
||||
@@ -135,6 +142,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'allauth.account.middleware.AccountMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
@@ -143,7 +151,7 @@ ROOT_URLCONF = 'config.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': [BASE_DIR / 'apps' / 'portal' / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@@ -158,6 +166,24 @@ TEMPLATES = [
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend',
|
||||
]
|
||||
|
||||
LOGIN_URL = "/login"
|
||||
LOGIN_REDIRECT_URL = "/dashboard"
|
||||
LOGOUT_REDIRECT_URL = "/login"
|
||||
|
||||
ACCOUNT_ADAPTER = "apps.portal.adapters.CmhubAccountAdapter"
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_UNIQUE_EMAIL = True
|
||||
ACCOUNT_LOGIN_METHODS = {"email", "username"}
|
||||
ACCOUNT_SIGNUP_FIELDS = ["username*", "email*", "password1*", "password2*"]
|
||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "/login"
|
||||
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = "/dashboard"
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = "[cmhub] "
|
||||
|
||||
|
||||
# Database
|
||||
|
||||
@@ -20,6 +20,8 @@ from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path('', include('apps.portal.urls')),
|
||||
path('accounts/', include('allauth.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('apps.api.urls')),
|
||||
]
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
## 当前阶段
|
||||
|
||||
当前项目处于:**Phase 4 用户端起步**。Phase 2 计费核心已完成到 T-204;Phase 3 已完成 T-301 API Key 鉴权、T-302 生成标题 / 图片接口、T-303 余额查询接口、T-304 充值回调、T-305 扫码充值下单 + 轮询与 T-306 对外 API 安全加固。下一步进入 T-501 注册 / 登录(allauth)。
|
||||
当前项目处于:**Phase 4 用户端**。Phase 2 计费核心已完成到 T-204;Phase 3 已完成 T-301 API Key 鉴权、T-302 生成标题 / 图片接口、T-303 余额查询接口、T-304 充值回调、T-305 扫码充值下单 + 轮询与 T-306 对外 API 安全加固;Phase 4 已完成 T-501 注册 / 登录(allauth)。下一步进入 T-502 API Key 自助管理页。
|
||||
|
||||
优先路径:
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
2. Phase 1:最高风险功能原型 —— T-101/T-102/T-103/T-104/T-105 已完成 provider 层、模型配置表、别名解析、配置审计、录制标题/图片 smoke 与审核修补;真实图片同步耗时待配置 Fernet 主密钥、AiModel/ModelAlias 与真实上游后在 T-302/T-403 前补测。
|
||||
3. Phase 2:计费核心 —— T-201/T-202/T-203 已完成 UserWallet/ApiKey/PointsLedger/CallRecord、计费规则、汇率、计费计算、并发安全扣点与失败退点。
|
||||
4. Phase 3:对外 API 与充值 —— T-301 Key 鉴权、T-302 生成接口、T-303 余额查询、T-304 充值回调、T-305 扫码下单与轮询、T-306 安全加固已完成。
|
||||
5. Phase 4:用户端(Django 模板 SSR)—— 下一步 T-501 注册登录,然后 API Key 管理、个人中心/记录页、充值页。
|
||||
5. Phase 4:用户端(Django 模板 SSR)—— T-501 注册登录已完成,下一步 T-502 API Key 管理,然后个人中心/记录页、充值页。
|
||||
6. Phase 5:后台与发布 —— 运营后台完善、完整验收、部署 / 运行文档。
|
||||
|
||||
## 领取任务规则
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| Web 框架 | Django 5.2 LTS | 已定 | 自带 ORM、迁移、admin,适合「API + 运营后台」;选 LTS 维护到 2028,安全更新窗口最长。**禁用已 EOL 的 4.0/4.1**(无安全补丁,资金服务不可用) |
|
||||
| API 框架 | Django REST Framework (DRF) | 已定 | 鉴权、序列化、参数校验、限流现成 |
|
||||
| 运营后台 | django-admin | 已定 | 近零代码即得用户/点数/记录的增删改查与检索,省 80% 后台工作量 |
|
||||
| 用户端 | Django 模板 SSR + Bootstrap 5 + django-allauth + crispy-forms | 已定 | 自助注册/登录/充值/API Key 管理/记录页;allauth 出注册登录邮箱验证,crispy + 现成 Bootstrap 模板出页面,单体不引前端框架 |
|
||||
| 用户端 | Django 模板 SSR + Bootstrap 5 + django-allauth | 已定 | T-501 已落地自助注册/登录/登出、邮箱验证与最小 dashboard;MVP 先用 Django form + Bootstrap 模板,不为简单表单引入 crispy-forms |
|
||||
| 后台美化 | django-unfold 或 simpleui | 待定 | 仅外观,MVP 可先用原生 admin,后期按需引入 |
|
||||
| AI 上游对接 | **Provider 适配器层**(按 `api_type` 注册)+ **能力别名** 映射 + `requests` HTTP 客户端 | 已定 | 对外只暴露 `generate text/image` 两接口与别名;换供应商改后台映射,不动对外契约。移植 `cmbot` 的调用逻辑到各适配器。当前 3 模型机制不同:文本 chat、`nano-banana2` chat 多模态返图、`gpt-image-2` images/edits 改图(详见 `04` 3.1) |
|
||||
| 供应商密钥存储 | 应用层 Fernet 加密(`cryptography`) | 已定 | `AiModel.api_key_encrypted` 加密入库、admin 写入型字段不回显;加密主密钥 `AI_KEY_ENCRYPTION_KEY` 走环境变量,配置清单见 `env.md` |
|
||||
@@ -20,7 +20,7 @@
|
||||
| 数据库 | MySQL 8.4 LTS(cmhub 专用独立实例) | 已定 | 满足 Django 5.2 的 MySQL ≥8.0.11;引擎 InnoDB + 字符集 utf8mb4;行锁 `select_for_update` / 条件更新保并发扣点。**不复用 VPS 已有的 MySQL 5.7**(跑不了 Django 5.2、无 CHECK 约束)。开发亦用 MySQL,勿用 SQLite(不支持 `select_for_update`) |
|
||||
| MySQL 驱动 | PyMySQL + cryptography | 已定 | PyMySQL 负责 Django 连接 MySQL;MySQL 8 默认 `caching_sha2_password` 认证需要 `cryptography` 支持;客户端连接/读/写超时通过 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT` 配置 |
|
||||
| 对外鉴权 | API Key(DRF 自定义 Authentication,哈希存储比对) | 已定 | 用户自助生成 Key;**API 只认 Key、不挂 SessionAuthentication**,防浏览器 cookie 绕过计费 |
|
||||
| 用户端鉴权 | Django Session(+ allauth 注册登录邮箱验证) | 已定 | 用户端页面与 `recharge/create` 走 session + CSRF;后台账号也用 Session 登录 |
|
||||
| 用户端鉴权 | Django Session(+ allauth 注册登录邮箱验证) | 已定 | T-501 已落地 `/signup` `/login` `/logout` 与 `/dashboard`;用户端页面与 `recharge/create` 走 session + CSRF;后台账号也用 Session 登录 |
|
||||
| 充值对接 | 自助扫码:微信 V3 native + 支付宝当面付;下单取二维码 + 服务端回调(验签 + 幂等) | 已定 | T-304/T-305 已落地回调、扫码下单、状态轮询、HMAC mock 联调与 SDK 模式入口;生产需安装并配置 `wechatpayv3` / `python-alipay-sdk` 与真实商户密钥/证书 |
|
||||
| 生成返回方式 | 同步 HTTP(无任务队列) | 已定 | MVP 简化;图片接口需调大网关/服务超时 |
|
||||
| 任务队列 | 暂不引入(Celery/RQ) | 待定 | V2 异步化时再评估 |
|
||||
@@ -41,7 +41,7 @@
|
||||
- **同步可用的前提是「超时链路 + worker 容量」配对**,否则会「小量正常、上量假死」:① 每模型 `timeout_seconds` 设有限值(`ai_models.json` 现为 `0`,迁入须改);② Gunicorn `--timeout` 与网关 `proxy_read_timeout` 按最慢图片放大(别用默认 30s / 60s);③ worker/线程数按**峰值总并发**预留。详见 [架构设计](04-architecture.md) 第五节结论。
|
||||
- **何时转 V2 异步**:worker 被长连接占满拖慢快接口/后台、接入方总并发明显上涨、或需要「关窗重连/任务持久化」体验——在此之前保持同步。
|
||||
- **数据库:MySQL 8.4 LTS,cmhub 专用独立实例**:① 部署环境的 VPS 已装 MySQL 5.7 供其他服务用,但 5.7 跑不了 Django 5.2(需 ≥8.0.11)、已 EOL、且不支持 CHECK 约束,故**不复用**它;② 机器内存宽裕(`available` 7.4G),给 cmhub **单开一个 MySQL 8.4 LTS 实例**(独立端口/容器),与已有 5.7 完全隔离、互不影响;③ 选 8.4 LTS 取长维护窗口 + 完整 CHECK 约束(CHECK 需 MySQL ≥8.0.16 才真正生效);④ 强制 InnoDB + utf8mb4(5.7/老配置默认非 utf8mb4,prompt 的 emoji/生僻字会写失败);⑤ MySQL 默认隔离级别 REPEATABLE READ(不同于 PostgreSQL 的 READ COMMITTED),`select_for_update` 扣点仍安全,但计费实现按此语义验证;⑥ 开发环境同用 MySQL,不要用 SQLite——SQLite 会静默忽略 `FOR UPDATE`,并发扣点逻辑测不出来;⑦ 不在代码里写死只适配某一种库的 SQL。
|
||||
- **用户端用 Django 模板 SSR 单体,不引前端框架**:需求含终端用户自助(注册/充值/API Key/记录),选 Django 模板 + Bootstrap + allauth 与后端同工程单体部署,复用 Django auth/session,开发部署最快、最契合单机 MVP;代价是交互不如 SPA,可后续加 HTMX。放弃 Vue/React 前后端分离(两套项目/部署,与单体 MVP 调性冲突)。用户模型:`User`(auth) 持登录态、`UserWallet` 持点数(扣点锁 wallet、与 auth 解耦)、`ApiKey`(User 1:N,哈希存储)。注册不送免费点数。
|
||||
- **用户端用 Django 模板 SSR 单体,不引前端框架**:需求含终端用户自助(注册/充值/API Key/记录),选 Django 模板 + Bootstrap + allauth 与后端同工程单体部署,复用 Django auth/session,开发部署最快、最契合单机 MVP;代价是交互不如 SPA,可后续加 HTMX。T-501 已接入 django-allauth 65.18.0,使用邮箱验证、session 与 CSRF;注册成功只创建 0 点 `UserWallet`,不写赠点流水。放弃 Vue/React 前后端分离(两套项目/部署,与单体 MVP 调性冲突)。用户模型:`User`(auth) 持登录态、`UserWallet` 持点数(扣点锁 wallet、与 auth 解耦)、`ApiKey`(User 1:N,哈希存储)。注册不送免费点数。
|
||||
|
||||
## 三、构建与运行命令
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
组件落位:
|
||||
|
||||
- **用户端层(Django 模板 SSR)**:注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值。入口 `apps/portal/`,用 session 鉴权。
|
||||
- **用户端层(Django 模板 SSR)**:注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值。入口 `apps/portal/`,用 session 鉴权。T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`;注册成功通过 allauth adapter 创建 0 点 `UserWallet`,不写赠点流水。
|
||||
- **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。
|
||||
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
|
||||
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
||||
@@ -272,7 +272,7 @@ CREATE TABLE call_record (
|
||||
- `payment_user_id`、`payment_txn_no` 为对账预留,字段先建。
|
||||
- `recharge_order.exchange_rate` 与 `points_granted` 在下单时写入,状态为 `pending` 时也必须有值;支付回调金额必须与订单金额一致,入账时不得按新的汇率重算。
|
||||
- `call_record.status` 状态机为 `pending -> success / failed`。上游失败退点后仍保持 `failed`,退款流水通过 `points_ledger(change_type=refund, ref_call_id=call_record.id)` 关联,不单独增加 `refunded` 状态,避免调用结果与账务动作混在一个字段里。
|
||||
- T-201 已落地 `UserWallet` / `ApiKey` 于 `apps.users`,`PointsLedger` / `CallRecord` 于 `apps.billing`;T-203 已落地扣点/退点服务;T-304 已落地 `RechargeOrder`、回调幂等入账服务和 `points_ledger(ref_order_id, change_type)` 复合唯一约束,`ref_order_id` 当前仍为数值引用 `RechargeOrder.id`;T-305 已落地 `create_recharge_order()`,负责创建 pending 订单、锁定汇率/点数并回填二维码票据。
|
||||
- T-201 已落地 `UserWallet` / `ApiKey` 于 `apps.users`,`PointsLedger` / `CallRecord` 于 `apps.billing`;T-203 已落地扣点/退点服务;T-304 已落地 `RechargeOrder`、回调幂等入账服务和 `points_ledger(ref_order_id, change_type)` 复合唯一约束,`ref_order_id` 当前仍为数值引用 `RechargeOrder.id`;T-305 已落地 `create_recharge_order()`,负责创建 pending 订单、锁定汇率/点数并回填二维码票据;T-501 已保证 allauth 自助注册路径创建 `UserWallet(points_balance=0)`,且不写 `PointsLedger`,避免把注册初始化误记为赠点或充值。
|
||||
|
||||
## 四、计费时序(核心,务必照此实现)
|
||||
|
||||
@@ -365,7 +365,7 @@ CREATE TABLE call_record (
|
||||
2. 移植并跑通一次 AI 调用(标题 / 图片)原型(Phase 1)。
|
||||
3. 计费:点数扣减(并发安全)+ 计费规则 + 调用记录(Phase 2,T-201~T-203 已完成)。
|
||||
4. 对外 API 鉴权 + 余额查询 + 充值下单/回调 + 安全加固(Phase 3,已完成到 T-306)。
|
||||
5. 用户端注册登录、API Key 管理、个人中心、充值页(Phase 4)。
|
||||
5. 用户端注册登录、API Key 管理、个人中心、充值页(Phase 4;T-501 已完成注册登录,下一步 T-502 API Key 管理)。
|
||||
6. 运营后台完善、完整验收、部署(Phase 5)。
|
||||
|
||||
## 七、项目结构建议
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-501 | 注册 / 登录(allauth) | T-201 | 自助注册(邮箱验证)、登录、登出;注册后钱包点数为 0(不送点数);session + CSRF | TODO |
|
||||
| T-501 | 注册 / 登录(allauth) | T-201 | 自助注册(邮箱验证)、登录、登出;注册后钱包点数为 0(不送点数);session + CSRF | DONE |
|
||||
| T-502 | API Key 自助管理页 | T-501, T-301 | 登录用户生成/删除 Key;明文只显示一次、库存哈希;列表只显示 prefix;删除后该 Key 调用 401 | TODO |
|
||||
| T-503 | 个人中心 / 记录页 | T-501, T-203 | 剩余点数、充值总额、充值记录、消费(调用)记录;数据与流水一致;仅见本人 | TODO |
|
||||
| T-504 | 充值页(扫码 + 轮询到账) | T-501, T-305 | 发起充值→展示二维码→轮询订单状态→到账后余额刷新;到账以回调为权威 | TODO |
|
||||
|
||||
@@ -29,6 +29,8 @@ T-305 已实现扫码充值下单与轮询基线:`POST /api/v1/recharge/create
|
||||
|
||||
T-306 已实现对外 API 安全加固:`image_url` 下载只允许 `http` / `https` 公网地址,拒绝私有/回环/链路本地/保留等地址,重定向后重新校验并限制响应大小;全局 DRF 默认认证为空且默认权限为 `IsAuthenticated`,外部 API 与用户端 API 必须显式 opt-in 认证类;生成接口按 API Key / 用户限流,认证失败按 IP 限流;充值创建有 `RECHARGE_MAX_AMOUNT_CNY` 单笔上限。
|
||||
|
||||
T-501 已实现用户端注册 / 登录基线:`/signup` `/login` `/logout` 走 django-allauth + Django session + CSRF;注册邮箱必须验证,注册成功创建 0 点 `UserWallet`,不创建赠点流水。对外 API 仍只认 API Key,不接受 Web session。
|
||||
|
||||
通用错误响应:
|
||||
|
||||
```json
|
||||
|
||||
+18
-11
@@ -12,15 +12,15 @@
|
||||
## 当前快照
|
||||
|
||||
- 日期:2026-07-03
|
||||
- 阶段:Phase 4 用户端起步;Phase 3 对外 API 与充值已完成到 T-306,下一步 T-501 注册 / 登录(allauth)
|
||||
- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + PyMySQL 1.1.3 + cryptography 46.0.7 + requests 2.34.2 + django-admin;MySQL 8.4 已接入 settings,并支持 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT`;用户端(模板 SSR/Bootstrap/allauth) 后续任务落地;详见 `03-tech-stack.md`
|
||||
- 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/`;T-002 已创建 `apps/users|portal|billing|ai|api`;T-003 已把自定义 `User` 注册进 django-admin;T-004 已完成 email 唯一性、init 版本断言、app 顺序、`.env.example` 与 `pyproject.toml`;T-101 已新增 `apps/ai/providers/`(Provider 接口、注册表、chat/gemini/images/images_edits 适配器);T-102 已新增 `AiModel` / `ModelAlias`、Fernet 加密密钥存储、别名解析、admin 配置页、`import_ai_models` 导入命令;T-103 已新增 `AiConfigAuditLog` 审计表、admin 只读页面和后台保存/删除审计 hook;T-104/T-105 已完成录制 title/image smoke 与审核修补;T-201 已新增 `UserWallet` / `ApiKey`、`PointsLedger` / `CallRecord`、对应 admin 与迁移;T-202 已新增 `PricingRule` / `ExchangeRate`、`apps.billing.pricing` 计费计算函数、admin 配置页与迁移;T-203 已新增 `apps.billing.services`,实现并发安全预扣、成功确认与幂等失败退点;T-204 已新增 `billing.0003_pointsledger_unique_ledger_change_type_per_call`,用 MySQL 可落地的 `ref_call + change_type` 复合唯一约束兜底防重复 refund;T-301 已新增 `apps.api.authentication.ApiKeyAuthentication` 与 `ExternalApiView`;T-302 已新增生成接口编排、序列化器、图片本地存储和 `/api/v1/generate/title|image` 路由;T-303 已新增 `apps.billing.services.get_balance_snapshot()` 与 `/api/v1/balance` 余额查询接口;T-304 已新增 `RechargeOrder`、充值回调验签适配器、幂等入账服务、微信/支付宝回调路由与迁移 `billing.0004_rechargeorder_and_more`;T-305 已新增 `create_recharge_order()`、微信/支付宝扫码下单 mock/SDK 入口、`/api/v1/recharge/create` 与 `/api/v1/recharge/status`;T-306 已新增 `apps.api.throttles`、`apps.api.exceptions`、`REST_FRAMEWORK` 安全默认认证、生成/认证失败限流、`image_url` SSRF 防护与响应大小上限、充值单笔金额上限
|
||||
- 测试:T-306 已验证:`py -3.12 -m py_compile config/settings.py apps/api/exceptions.py apps/api/generation.py apps/api/authentication.py apps/api/views.py apps/api/serializers.py apps/api/throttles.py apps/api/tests.py`、`py -3.12 manage.py check`、`py -3.12 manage.py makemigrations --check --dry-run`、`py -3.12 -m compileall apps config`、`./init.ps1` 均通过;`py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` 通过(39 tests OK,含 SSRF、限流、充值上限);认证/生成限流单点补测通过(2 tests OK)。`py -3.12 manage.py test apps.billing apps.api --noinput --keepdb --verbosity 2` 未作为绿灯:62 tests 跑到末尾,功能用例输出均为 ok,最后远程 MySQL 在并发扣点测试的 flush/重连阶段 reset 连接;随后 `$env:MYSQL_CONNECT_TIMEOUT='90'; py -3.12 manage.py test apps.billing.tests.ConcurrentDebitTests --noinput --keepdb --verbosity 2` 通过(1 test OK)。
|
||||
- 阶段:Phase 4 用户端;Phase 3 对外 API 与充值已完成到 T-306,T-501 注册 / 登录(allauth)已完成,下一步 T-502 API Key 自助管理页
|
||||
- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + django-allauth 65.18.0 + PyMySQL 1.1.3 + cryptography 46.0.7 + requests 2.34.2 + django-admin;MySQL 8.4 已接入 settings,并支持 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT`;用户端已用 Django 模板 SSR + Bootstrap + allauth 落地注册登录;详见 `03-tech-stack.md`
|
||||
- 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/`;T-002 已创建 `apps/users|portal|billing|ai|api`;T-003 已把自定义 `User` 注册进 django-admin;T-004 已完成 email 唯一性、init 版本断言、app 顺序、`.env.example` 与 `pyproject.toml`;T-101 已新增 `apps/ai/providers/`(Provider 接口、注册表、chat/gemini/images/images_edits 适配器);T-102 已新增 `AiModel` / `ModelAlias`、Fernet 加密密钥存储、别名解析、admin 配置页、`import_ai_models` 导入命令;T-103 已新增 `AiConfigAuditLog` 审计表、admin 只读页面和后台保存/删除审计 hook;T-104/T-105 已完成录制 title/image smoke 与审核修补;T-201 已新增 `UserWallet` / `ApiKey`、`PointsLedger` / `CallRecord`、对应 admin 与迁移;T-202 已新增 `PricingRule` / `ExchangeRate`、`apps.billing.pricing` 计费计算函数、admin 配置页与迁移;T-203 已新增 `apps.billing.services`,实现并发安全预扣、成功确认与幂等失败退点;T-204 已新增 `billing.0003_pointsledger_unique_ledger_change_type_per_call`,用 MySQL 可落地的 `ref_call + change_type` 复合唯一约束兜底防重复 refund;T-301 已新增 `apps.api.authentication.ApiKeyAuthentication` 与 `ExternalApiView`;T-302 已新增生成接口编排、序列化器、图片本地存储和 `/api/v1/generate/title|image` 路由;T-303 已新增 `apps.billing.services.get_balance_snapshot()` 与 `/api/v1/balance` 余额查询接口;T-304 已新增 `RechargeOrder`、充值回调验签适配器、幂等入账服务、微信/支付宝回调路由与迁移 `billing.0004_rechargeorder_and_more`;T-305 已新增 `create_recharge_order()`、微信/支付宝扫码下单 mock/SDK 入口、`/api/v1/recharge/create` 与 `/api/v1/recharge/status`;T-306 已新增 `apps.api.throttles`、`apps.api.exceptions`、`REST_FRAMEWORK` 安全默认认证、生成/认证失败限流、`image_url` SSRF 防护与响应大小上限、充值单笔金额上限;T-501 已接入 allauth,新增 portal 路由、注册适配器、登录/注册/登出模板和最小 dashboard,注册成功创建 0 点钱包且不写赠点流水
|
||||
- 测试:T-501 已验证:`py -3.12 -m pip install -r requirements.txt` 安装 django-allauth 65.18.0;`py -3.12 manage.py check`、`py -3.12 -m py_compile config\settings.py config\urls.py apps\portal\adapters.py apps\portal\urls.py apps\portal\views.py apps\portal\tests.py`、`py -3.12 manage.py makemigrations --check --dry-run`、`py -3.12 manage.py migrate --noinput`、`py -3.12 -m compileall apps config` 均通过;`py -3.12 manage.py shell -c "from django.test import Client; r=Client().get('/login'); print(r.status_code)"` 返回 200,且命中项目内 portal 模板;`py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` 通过(5 tests OK);`py -3.12 manage.py test apps.users apps.portal --noinput --keepdb --verbosity 2` 通过(7 tests OK);首次完整 `py -3.12 manage.py test --noinput --keepdb --verbosity 2` 在 6 分钟工具超时内无结果,随后设置 `$env:MYSQL_CONNECT_TIMEOUT='90'; $env:MYSQL_READ_TIMEOUT='180'; $env:MYSQL_WRITE_TIMEOUT='180'` 后完整套件通过(95 tests OK,234.743s);修正模板优先级后再次完整重跑通过(95 tests OK,301.722s)。测试/迁移阶段有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目用户账本唯一性仍由 `user.email` 唯一约束承担。
|
||||
- 数据:AI 上游调用与模型配置参考 `D:\chengma\cmbot`(`src/services/ai_text_service.py`、`ai_image_service.py`、`config/ai_models.json`);真实 `ai_models.json` 不提交,需通过 `import_ai_models` 命令加密导入
|
||||
- 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh`
|
||||
- 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test`
|
||||
- 设计基线:**自助用户端 + 对外 API + 运营后台**三合一单体;用户模型 `User`(auth)/`UserWallet`(点数,锁 wallet 扣点)/`ApiKey`(1:N,哈希存储);对外两接口 + **能力别名 + Provider 适配器**(可插拔供应商);自助扫码充值;注册不送点数。详见 `04-architecture.md` 与 2026-06-29 / 2026-07-01 的 `progress.md` 决策
|
||||
- 配置基线:运行环境变量集中见 `docs/env.md`;真实密钥/支付凭证不得写入代码或文档样例。充值订单在创建时锁定汇率与预计点数,回调入账使用订单值,不按新汇率重算。支付回调与下单由 `PAYMENT_CALLBACK_MODE` 控制:本地/测试可用 HMAC `mock`,生产应为 `sdk`;二维码本地有效期提示由 `PAYMENT_QR_EXPIRES_MINUTES` 控制。T-306 起对外 API 安全配置包含 `API_GENERATE_THROTTLE_RATE`、`API_AUTH_FAILURE_THROTTLE_RATE`、`IMAGE_URL_MAX_BYTES`、`IMAGE_URL_MAX_REDIRECTS`、`IMAGE_URL_CONNECT_TIMEOUT_SECONDS`、`IMAGE_URL_READ_TIMEOUT_SECONDS`、`RECHARGE_MAX_AMOUNT_CNY`。
|
||||
- 配置基线:运行环境变量集中见 `docs/env.md`;真实密钥/支付凭证不得写入代码或文档样例。allauth 邮箱验证发信由 `DJANGO_EMAIL_BACKEND` / `DJANGO_DEFAULT_FROM_EMAIL` 控制,本地默认 console backend,生产需配置真实邮件服务。充值订单在创建时锁定汇率与预计点数,回调入账使用订单值,不按新汇率重算。支付回调与下单由 `PAYMENT_CALLBACK_MODE` 控制:本地/测试可用 HMAC `mock`,生产应为 `sdk`;二维码本地有效期提示由 `PAYMENT_QR_EXPIRES_MINUTES` 控制。T-306 起对外 API 安全配置包含 `API_GENERATE_THROTTLE_RATE`、`API_AUTH_FAILURE_THROTTLE_RATE`、`IMAGE_URL_MAX_BYTES`、`IMAGE_URL_MAX_REDIRECTS`、`IMAGE_URL_CONNECT_TIMEOUT_SECONDS`、`IMAGE_URL_READ_TIMEOUT_SECONDS`、`RECHARGE_MAX_AMOUNT_CNY`。
|
||||
- 当前 blocker:T-306 上线前 SSRF 风险已修复;远程 MySQL 连接仍可能很慢或间歇超时,完整测试需预留较长时间并优先用 `--keepdb` 串行跑。微信/支付宝真实商户密钥/证书与生产 SDK 依赖仍待提供/安装;真实 AI 上游 smoke 需要先配置 `AI_KEY_ENCRYPTION_KEY` 并导入 AiModel/ModelAlias。图片同步真实耗时风险仍未退,已登记到 T-403。
|
||||
|
||||
## 当前目录要点
|
||||
@@ -31,9 +31,9 @@
|
||||
| `AGENTS.md` / `CLAUDE.md` | 已有 | 仓库级入口 |
|
||||
| `progress.md` | 已有 | 执行流水,已记录多轮文档决策;后续任务继续追加 |
|
||||
| `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令,并校验解释器版本 `>=3.12,<3.14` |
|
||||
| `requirements.txt` / `pyproject.toml` | 已有 | `requirements.txt` 管运行依赖;`pyproject.toml` 落地 `requires-python`;T-101 新增 `requests`;T-102 使用既有 `cryptography` 做 Fernet 加密 |
|
||||
| `requirements.txt` / `pyproject.toml` | 已有 | `requirements.txt` 管运行依赖;`pyproject.toml` 落地 `requires-python`;T-101 新增 `requests`;T-102 使用既有 `cryptography` 做 Fernet 加密;T-501 新增 `django-allauth` |
|
||||
| `config/`(Django 工程) | 已有 | T-001 创建,含 settings / urls / wsgi / asgi |
|
||||
| `apps/`(users/portal/billing/ai/api) | 已有 | T-002 创建;`apps/users` 已定义自定义 `User`;T-003 已注册 admin 与 admin smoke test;T-004 已给 `User.email` 加唯一约束;T-101 已新增 `apps/ai/providers`;T-102 已新增 `apps/ai/security.py`、`aliases.py`、`importers.py`、management command 与 `ai.0001_initial` 迁移;T-103 已新增 `apps/ai/audit.py` 与 `ai.0002_aiconfigauditlog` 迁移;T-104/T-105 已新增 `smoke_ai_generation` 录制 title/image smoke 命令;T-201 已在 users 落 `UserWallet` / `ApiKey`,在 billing 落 `PointsLedger` / `CallRecord`;T-202 已在 billing 落 `PricingRule` / `ExchangeRate` 与 `pricing.py`;T-203/T-303/T-304/T-305 已在 `apps/billing/services.py` 落扣点/退点、余额快照、充值入账与充值下单;T-304/T-305 已在 `apps/billing/payment_gateways.py` 落回调验签、mock 下单与 SDK 入口;T-301~T-306 已在 api 落鉴权、生成接口编排、序列化器、图片存储、余额查询、充值回调、充值下单/状态查询、`image_url` SSRF 防护、生成/认证限流与统一 429 错误响应 |
|
||||
| `apps/`(users/portal/billing/ai/api) | 已有 | T-002 创建;`apps/users` 已定义自定义 `User`;T-003 已注册 admin 与 admin smoke test;T-004 已给 `User.email` 加唯一约束;T-101 已新增 `apps/ai/providers`;T-102 已新增 `apps/ai/security.py`、`aliases.py`、`importers.py`、management command 与 `ai.0001_initial` 迁移;T-103 已新增 `apps/ai/audit.py` 与 `ai.0002_aiconfigauditlog` 迁移;T-104/T-105 已新增 `smoke_ai_generation` 录制 title/image smoke 命令;T-201 已在 users 落 `UserWallet` / `ApiKey`,在 billing 落 `PointsLedger` / `CallRecord`;T-202 已在 billing 落 `PricingRule` / `ExchangeRate` 与 `pricing.py`;T-203/T-303/T-304/T-305 已在 `apps/billing/services.py` 落扣点/退点、余额快照、充值入账与充值下单;T-304/T-305 已在 `apps/billing/payment_gateways.py` 落回调验签、mock 下单与 SDK 入口;T-301~T-306 已在 api 落鉴权、生成接口编排、序列化器、图片存储、余额查询、充值回调、充值下单/状态查询、`image_url` SSRF 防护、生成/认证限流与统一 429 错误响应;T-501 已在 portal 落 allauth 注册/登录/登出路由、模板、adapter 与 dashboard |
|
||||
| `manage.py` | 已有 | T-001 创建 |
|
||||
| `tests/` | 待建 | 随各任务补充 |
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
|
||||
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史执行记录见 [`../progress.md`](../progress.md)。
|
||||
|
||||
- 已完成:T-001 初始化 Django + DRF 项目骨架;T-002 建立 apps 目录、自定义 User 与配置;T-003 接通 django-admin 与最小测试;T-004 Phase 0 骨架审核修补;T-101 Provider 适配器层 + 移植 cmbot 调用;T-102 AiModel + ModelAlias 模型 + 别名解析;T-103 配置变更审计;T-104 跑通一次录制标题生成;T-105 Phase 1 AI 层审核修补;T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型;T-202 PricingRule / ExchangeRate 模型 + 计费计算;T-203 并发安全扣点 / 退点;T-204 Phase 2 计费核心审核加固;T-301 API Key 鉴权;T-302 生成标题 / 图片接口;T-303 余额查询接口;T-304 充值回调;T-305 扫码充值下单 + 轮询;T-306 Phase 3 对外 API 安全加固。
|
||||
- 已完成:T-001 初始化 Django + DRF 项目骨架;T-002 建立 apps 目录、自定义 User 与配置;T-003 接通 django-admin 与最小测试;T-004 Phase 0 骨架审核修补;T-101 Provider 适配器层 + 移植 cmbot 调用;T-102 AiModel + ModelAlias 模型 + 别名解析;T-103 配置变更审计;T-104 跑通一次录制标题生成;T-105 Phase 1 AI 层审核修补;T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型;T-202 PricingRule / ExchangeRate 模型 + 计费计算;T-203 并发安全扣点 / 退点;T-204 Phase 2 计费核心审核加固;T-301 API Key 鉴权;T-302 生成标题 / 图片接口;T-303 余额查询接口;T-304 充值回调;T-305 扫码充值下单 + 轮询;T-306 Phase 3 对外 API 安全加固;T-501 注册 / 登录(allauth)。
|
||||
- 正在进行:无。
|
||||
- 当前 blocker:远程 MySQL 连接当前不稳定或很慢;支付商户真实密钥/证书与生产 SDK 依赖仍待提供;真实 AI 上游 smoke 仍需配置密钥和模型数据后执行。
|
||||
- 下一个可领取任务:**T-501 注册 / 登录(allauth)**。
|
||||
- 下一个可领取任务:**T-502 API Key 自助管理页**。
|
||||
|
||||
## 当前可运行内容
|
||||
|
||||
@@ -78,14 +78,21 @@ python3.12 manage.py smoke_ai_generation image --recorded
|
||||
- `POST /api/v1/recharge/callback/wechat`
|
||||
- `POST /api/v1/recharge/callback/alipay`
|
||||
|
||||
当前骨架可运行。T-002 已在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4;远程 MySQL 已完成 Django 初始迁移。T-003 已接通 django-admin,测试可创建/销毁 `test_cmhub` 测试库;当前远程 MySQL 对频繁建库/销库仍可能间歇超时,必要时用 `--keepdb` 且串行跑测试。T-004 已应用 `users.0002_alter_user_email`,`user.email` 已有唯一索引。T-101 的 AI provider 层只做 HTTP 调用与响应解析;T-102 已把 provider 运行配置接到数据库 `AiModel` / `ModelAlias`,`resolve_alias()` 每次查当前 active 配置并按 `text` / `image` 能力校验。T-103 已补 `AiConfigAuditLog`,admin 保存/删除 `AiModel` / `ModelAlias` 时记录 actor、action、target、changed_fields、changes、created_at,密钥只记录 empty/set 状态。T-104/T-105 已用临时回滚配置跑通录制标题和录制图片生成。T-201 已落地钱包、API Key、点数流水和调用记录:API Key 明文只在创建 helper 返回,库内只存 hash/prefix;CallRecord 只存 `result_ref`/`result_summary`,没有 provider raw 字段。T-202 已落地 `PricingRule` / `ExchangeRate`:计费按 `operation_type + alias + resolution` 查 active 规则,优先精确分辨率,再回退默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`;金额换点数按当前 active 汇率向下取整。T-203 已落地 `precharge_call()` / `mark_call_success()` / `refund_call_points()`:预扣锁钱包行,余额不足不写调用/流水;失败退点锁调用记录并幂等写 refund 流水。T-204 已完成复合唯一约束加固,并取得一次完整 `manage.py test` 单次全绿。T-301 已落地 `Authorization: Bearer <API_KEY>` 鉴权:成功后 `request.user` 为所属用户、`request.auth` 为 `ApiKey`,缺失/无效 Key 返回 401,用户或 Key 禁用返回 403,外部 API 不接受 Web session。T-302 已落地生成接口:请求别名解析后按规则计费,预扣成功才调用 Provider,成功确认调用记录,`AiProviderError` / `AiCapabilityError` 等失败路径会退点;图片结果保存到本地 media 并返回 URL。T-303 已落地余额查询接口:`GET /api/v1/balance` 继承外部 API Key 鉴权,读取 billing 余额快照并返回 `user` 与 `points_balance`,测试覆盖余额与流水累加一致。T-304 已落地充值回调:`RechargeOrder` 保存下单锁定的金额/汇率/点数,微信/支付宝回调先验签再按订单幂等入账,重复回调不重复加点,金额不一致不入账;主动查单兜底可调用 `query_and_apply_recharge_payment(order_no, query_func)` 复用同一入账路径。T-305 已落地扫码下单与轮询:用户端 session 登录后可 `POST /api/v1/recharge/create` 创建 pending 订单并拿到 mock/SDK 二维码票据,`GET /api/v1/recharge/status` 只返回本人订单并在 pending 时尝试主动查单补入账;API Key 不能调用这两个用户端接口。T-306 已落地对外 API 安全加固:`image_url` 下载在扣点前做协议白名单、公网地址校验、重定向逐跳校验和响应大小上限;DRF 全局默认不再隐式启用 Session/Basic;生成接口按 Key 限流,认证失败按 IP 限流;充值下单有单笔金额上限。真实上游生成未执行,原因是当前环境未配置 `AI_KEY_ENCRYPTION_KEY` 且数据库没有 AiModel/ModelAlias;后续配置后可用 `import_ai_models` 导入,再通过接口跑真实标题/图片。
|
||||
当前用户端页面:
|
||||
|
||||
- `GET/POST /signup`
|
||||
- `GET/POST /login`
|
||||
- `GET/POST /logout`
|
||||
- `GET /dashboard`
|
||||
|
||||
当前骨架可运行。T-002 已在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4;远程 MySQL 已完成 Django 初始迁移。T-003 已接通 django-admin,测试可创建/销毁 `test_cmhub` 测试库;当前远程 MySQL 对频繁建库/销库仍可能间歇超时,必要时用 `--keepdb` 且串行跑测试。T-004 已应用 `users.0002_alter_user_email`,`user.email` 已有唯一索引。T-101 的 AI provider 层只做 HTTP 调用与响应解析;T-102 已把 provider 运行配置接到数据库 `AiModel` / `ModelAlias`,`resolve_alias()` 每次查当前 active 配置并按 `text` / `image` 能力校验。T-103 已补 `AiConfigAuditLog`,admin 保存/删除 `AiModel` / `ModelAlias` 时记录 actor、action、target、changed_fields、changes、created_at,密钥只记录 empty/set 状态。T-104/T-105 已用临时回滚配置跑通录制标题和录制图片生成。T-201 已落地钱包、API Key、点数流水和调用记录:API Key 明文只在创建 helper 返回,库内只存 hash/prefix;CallRecord 只存 `result_ref`/`result_summary`,没有 provider raw 字段。T-202 已落地 `PricingRule` / `ExchangeRate`:计费按 `operation_type + alias + resolution` 查 active 规则,优先精确分辨率,再回退默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`;金额换点数按当前 active 汇率向下取整。T-203 已落地 `precharge_call()` / `mark_call_success()` / `refund_call_points()`:预扣锁钱包行,余额不足不写调用/流水;失败退点锁调用记录并幂等写 refund 流水。T-204 已完成复合唯一约束加固,并取得一次完整 `manage.py test` 单次全绿。T-301 已落地 `Authorization: Bearer <API_KEY>` 鉴权:成功后 `request.user` 为所属用户、`request.auth` 为 `ApiKey`,缺失/无效 Key 返回 401,用户或 Key 禁用返回 403,外部 API 不接受 Web session。T-302 已落地生成接口:请求别名解析后按规则计费,预扣成功才调用 Provider,成功确认调用记录,`AiProviderError` / `AiCapabilityError` 等失败路径会退点;图片结果保存到本地 media 并返回 URL。T-303 已落地余额查询接口:`GET /api/v1/balance` 继承外部 API Key 鉴权,读取 billing 余额快照并返回 `user` 与 `points_balance`,测试覆盖余额与流水累加一致。T-304 已落地充值回调:`RechargeOrder` 保存下单锁定的金额/汇率/点数,微信/支付宝回调先验签再按订单幂等入账,重复回调不重复加点,金额不一致不入账;主动查单兜底可调用 `query_and_apply_recharge_payment(order_no, query_func)` 复用同一入账路径。T-305 已落地扫码下单与轮询:用户端 session 登录后可 `POST /api/v1/recharge/create` 创建 pending 订单并拿到 mock/SDK 二维码票据,`GET /api/v1/recharge/status` 只返回本人订单并在 pending 时尝试主动查单补入账;API Key 不能调用这两个用户端接口。T-306 已落地对外 API 安全加固:`image_url` 下载在扣点前做协议白名单、公网地址校验、重定向逐跳校验和响应大小上限;DRF 全局默认不再隐式启用 Session/Basic;生成接口按 Key 限流,认证失败按 IP 限流;充值下单有单笔金额上限。T-501 已落地 allauth 注册 / 登录:`ACCOUNT_EMAIL_VERIFICATION="mandatory"`,注册成功创建 0 点钱包、不写点数流水;未验证邮箱不能建立登录 session;`/dashboard` 作为登录落点只展示当前点数,完整个人中心留给 T-503。真实上游生成未执行,原因是当前环境未配置 `AI_KEY_ENCRYPTION_KEY` 且数据库没有 AiModel/ModelAlias;后续配置后可用 `import_ai_models` 导入,再通过接口跑真实标题/图片。
|
||||
|
||||
## 开始编码前检查
|
||||
|
||||
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-501)。
|
||||
4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务(当前为 T-502)。
|
||||
5. 将该任务状态改为 `DOING`。
|
||||
|
||||
## 维护规则
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
| `DJANGO_ALLOWED_HOSTS` | 是 | `cmhub.example.com,127.0.0.1` | 逗号分隔 |
|
||||
| `DJANGO_CSRF_TRUSTED_ORIGINS` | 生产是 | `https://cmhub.example.com` | 用户端表单、admin、充值页需要 |
|
||||
| `DJANGO_TIME_ZONE` | 否 | `Asia/Shanghai` | 默认按中国业务时区 |
|
||||
| `DJANGO_EMAIL_BACKEND` | 否 | `django.core.mail.backends.console.EmailBackend` | allauth 注册邮箱验证发信后端;生产应改为真实 SMTP / 邮件服务 |
|
||||
| `DJANGO_DEFAULT_FROM_EMAIL` | 生产是 | `noreply@cmhub.example.com` | allauth 邮件默认发件人 |
|
||||
|
||||
## 三、数据库配置
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
### P3 · 登记 / 后续任务(不在 T-204 硬性范围)
|
||||
|
||||
- **手工调点服务未实现**(§8「运营手工调整必须填原因」):模型层已备好(`PointsLedger` ADJUST 类型的 `clean()` 强制要求 `reason`),wallet admin 已锁死不可直接改余额(安全的中间态);但还缺一个 billing 层 `adjust_points()`(锁 wallet + 写 adjust 流水 + reason)供后台用。→ 登记 **T-401**。
|
||||
- **wallet 在扣点热路径 `get_or_create`**:`precharge_call` 用 `select_for_update().get_or_create(user=...)`,会在「钱包缺失」时静默建零钱包再报余额不足,掩盖异常。应保证注册(T-501)时建钱包;billing 层遇缺失可考虑显式报错。→ 登记 **T-501**。
|
||||
- **wallet 在扣点热路径 `get_or_create`**:`precharge_call` 用 `select_for_update().get_or_create(user=...)`,会在「钱包缺失」时静默建零钱包再报余额不足,掩盖异常。T-501 已保证 allauth 自助注册路径创建 0 点钱包;billing 层遇缺失是否改为显式报错仍可在后续计费加固中评估。
|
||||
- **锁顺序约定**:`refund_call_points` 锁序为 (CallRecord → wallet),`precharge_call` 只锁 wallet。当前无跨序死锁;但将来若新增「先锁 wallet 再锁 CallRecord」路径需警惕死锁。→ 作为约定登记。
|
||||
- **只追加靠 admin + 无 UPDATE 代码保证**:`PointsLedger`/`CallRecord` 的不可改目前由 admin 只读 + 服务层不写 update 保证,DB 层无触发器阻止 UPDATE/DELETE。Django 层已足够,登记备忘即可。
|
||||
- **`OperationType` 双份枚举**:`apps.ai.ModelAlias.OperationType` 与 `apps.billing.CallRecord.OperationType` 各定义一份 title/image,语义重复。极小的 DRY 隐患,后续可抽公共枚举。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# cmhub 项目介绍(给管理层)
|
||||
|
||||
> 面向决策与汇报的项目概览。技术细节见同目录架构与需求文档。
|
||||
> 日期:2026-07-03 | 阶段:Phase 4 起步(T-501 注册 / 登录)
|
||||
> 日期:2026-07-03 | 阶段:Phase 4 用户端(T-501 已完成,下一步 T-502)
|
||||
|
||||
## 一句话概括
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
- Phase 1 已完成 T-101~T-104:AI Provider 适配器、AiModel/ModelAlias、Fernet 加密密钥存储、别名解析、配置审计与录制标题生成 smoke 已落地。
|
||||
- Phase 2 计费核心已完成:PricingRule / ExchangeRate、计费计算、并发安全扣点与失败退点已落地,并覆盖并发测试。
|
||||
- Phase 3 对外 API 与充值已完成到 T-306:API Key 鉴权、生成/余额接口、扫码充值下单与轮询、支付回调幂等入账、`image_url` SSRF 防护、生成/认证限流和充值金额上限已落地。
|
||||
- 下一步是 T-501:实现用户端注册 / 登录(allauth),注册后钱包点数为 0,不送免费点数。
|
||||
- Phase 4 已完成 T-501:用户端注册 / 登录(allauth)、邮箱验证、登出、最小 dashboard 与注册后 0 点钱包已落地。
|
||||
- 下一步是 T-502:实现 API Key 自助管理页,生成时明文只显示一次,库存只保留哈希和 prefix。
|
||||
|
||||
---
|
||||
*更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# cmhub · 一页汇报版
|
||||
|
||||
> 自助用户端 + 计费型 AI 能力网关 + 运营后台 | 2026-07-03 | Phase 4 起步
|
||||
> 自助用户端 + 计费型 AI 能力网关 + 运营后台 | 2026-07-03 | Phase 4 用户端
|
||||
|
||||
## 电梯陈述(30 秒)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
## 进度
|
||||
|
||||
M1 骨架已完成;M2 已通过录制生成 smoke 验证 AI 调用链路;M3 计费、对外 API、充值闭环与上线前安全加固已完成到 T-306。下一步进入 Phase 4 用户端,先做注册 / 登录,再做 API Key 管理、个人中心和充值页。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线。
|
||||
M1 骨架已完成;M2 已通过录制生成 smoke 验证 AI 调用链路;M3 计费、对外 API、充值闭环与上线前安全加固已完成到 T-306;Phase 4 已完成注册 / 登录(T-501)。下一步做 API Key 管理页,再做个人中心和充值页。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线。
|
||||
|
||||
---
|
||||
*详见 `project-brief.md`(完整介绍)。*
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
| `/records/usage` | GET | 点数使用(消费/调用)记录 | session |
|
||||
| `/apikeys` | GET/POST | API Key 管理:列表 / 生成 / 删除(明文只显示一次) | session |
|
||||
|
||||
T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`。`/dashboard` 当前只作为登录后的落点展示剩余点数;充值汇总、充值记录与消费记录留给 T-503。
|
||||
|
||||
## API 路由(对外,DRF)
|
||||
|
||||
| 路由 | 方法 | 职责 | 鉴权 |
|
||||
|
||||
+25
@@ -728,3 +728,28 @@
|
||||
- `git diff --check`:通过,仅 Windows CRLF 提示。
|
||||
- 阻塞:T-306 功能无阻塞;完整大套件仍受远程 MySQL 连接稳定性影响。生产真实支付仍需微信 / 支付宝商户密钥、证书与 SDK 配置;真实 AI 上游 smoke 仍需配置 `AI_KEY_ENCRYPTION_KEY` 并导入 AiModel/ModelAlias。
|
||||
- 下一步:领取 T-501 注册 / 登录(allauth)。
|
||||
|
||||
## 2026-07-03 T-501 注册 / 登录(allauth)
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:
|
||||
- `requirements.txt`:新增 `django-allauth>=65.18,<66`,本机安装到 65.18.0。
|
||||
- `config/settings.py` / `config/urls.py`:接入 `allauth` / `allauth.account`、allauth authentication backend、`AccountMiddleware`、邮箱验证 mandatory、登录/登出跳转、邮件 backend 配置、项目模板优先级、`accounts/` 路由与 portal 页面路由。
|
||||
- `apps/portal/`:新增 `CmhubAccountAdapter`,allauth 注册成功后创建 `UserWallet(points_balance=0)`,不写赠点流水;新增 `/signup`、`/login`、`/logout`、`/dashboard` 路由,最小 dashboard 只读显示点数余额;新增 Bootstrap 模板与中文验证邮件模板。
|
||||
- `apps/portal/tests.py`:覆盖注册创建未验证邮箱与 0 点钱包、无赠点流水、未验证邮箱不能登录、已验证用户登录/dashboard/登出、dashboard session 保护、登录 POST CSRF 保护。
|
||||
- 同步更新 `.env.example`、`README.md`、`docs/00-ai-start-here.md`、`docs/03-tech-stack.md`、`docs/04-architecture.md`、`docs/api.md`、`docs/routes.md`、`docs/env.md`、`docs/06-tasks.md`、`docs/current-state.md`、`docs/project-brief.md`、`docs/project-onepager.md`、`docs/phase-2-review.md`。
|
||||
- 验证:
|
||||
- `./init.ps1`:开工前通过,依赖同步与 `manage.py check` 正常。
|
||||
- `py -3.12 -m pip install -r requirements.txt`:通过,安装 `django-allauth 65.18.0`。
|
||||
- `py -3.12 manage.py check`:通过,0 issues。
|
||||
- `py -3.12 -m py_compile config\settings.py config\urls.py apps\portal\adapters.py apps\portal\urls.py apps\portal\views.py apps\portal\tests.py`:通过。
|
||||
- `py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2`:通过,5 tests OK。
|
||||
- `py -3.12 manage.py makemigrations --check --dry-run`:通过,No changes detected。
|
||||
- `py -3.12 manage.py migrate --noinput`:通过,应用 `account.0001` ~ `account.0009`;MySQL 对 allauth `account.EmailAddress` 条件唯一约束提示 `models.W036`,第三方表的条件约束不会创建。
|
||||
- `py -3.12 manage.py test apps.users apps.portal --noinput --keepdb --verbosity 2`:通过,7 tests OK。
|
||||
- `py -3.12 -m compileall apps config`:通过。
|
||||
- `py -3.12 manage.py shell -c "from django.test import Client; r=Client().get('/login'); print(r.status_code); print(r.content[:120])"`:通过,`/login` 返回 200;修正模板优先级后确认返回项目内 `portal/base.html` 模板。
|
||||
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2`:首次在 6 分钟工具超时内无结果;随后设置 `$env:MYSQL_CONNECT_TIMEOUT='90'`、`$env:MYSQL_READ_TIMEOUT='180'`、`$env:MYSQL_WRITE_TIMEOUT='180'` 后重跑通过,95 tests OK,234.743s;修正模板优先级后再次完整重跑通过,95 tests OK,301.722s。
|
||||
- 阻塞:T-501 功能无阻塞。生产邮箱验证需要配置真实邮件服务;当前默认 console backend 只适合本地开发。allauth 的 `EmailAddress` 条件唯一约束在 MySQL 上有第三方模型警告,但本项目账本唯一性由 `user.email` 唯一约束承担。
|
||||
- 决策:不引入 crispy-forms;T-501 只需要简单 allauth 表单,MVP 先用 Django form + Bootstrap 模板。注册路径只初始化 0 点钱包,不创建 `PointsLedger`,避免被误解为注册赠点。
|
||||
- 下一步:领取 T-502 API Key 自助管理页。
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
Django>=5.2,<5.3
|
||||
djangorestframework>=3.16,<3.17
|
||||
django-allauth>=65.18,<66
|
||||
PyMySQL>=1.1,<1.2
|
||||
cryptography>=42,<47
|
||||
requests>=2.32,<3
|
||||
|
||||
Reference in New Issue
Block a user