diff --git a/README.md b/README.md index 16bbe12..2018cc3 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ ## 它做什么 -- 用户端(自助):注册/登录、扫码充值、查看充值记录/剩余点数/消费记录、生成与删除 API Key(注册不送免费点数)。 -- 对外提供 HTTP API:生成标题、生成图片(同步返回)、查询点数余额。 +- 用户端(自助):公开首页、注册/登录、扫码充值、查看充值记录/剩余点数/消费记录、查看可用模型、生成与删除 API Key、下载桌面端(注册不送免费点数)。 +- 对外提供 HTTP API:生成标题、生成图片(同步返回)、查询点数余额、查询可用能力别名。 - 按「操作类型 + 能力别名(+ 可选分辨率)」计费,调用消耗不同点数;余额不足返回「点数不足,请先充值」。 - **预付费点数模型**:用户在外部支付系统充值,付款成功由支付系统回调本服务,按汇率把金额转成点数存在本地;之后调用直接扣本地点数,扣费与上游解耦、低延迟。 - django-admin 运营后台:管理注册用户、点数余额、计费规则、充值订单、点数流水、调用记录。 @@ -25,7 +25,7 @@ Python 3.12 / Django 5.2 LTS + DRF / django-admin / 用户端 Django 模板 SSR ## 当前状态 -Phase 2 计费核心已完成,Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端已完成 T-501~T-505,Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档,Phase 6 已完成 T-601 可用别名发现、T-602 django-admin 分组/表名中文化与 T-605 免邮箱验证策略落地;T-603 字段级中文化代码和 no-op 迁移已完成,待稳定 MySQL 测试库补跑完整验证后收尾。用户可通过 allauth 自助注册(免邮箱验证、注册即可用,邮箱仍必填且唯一)、登录、登出,扫码充值并轮询到账,生成 / 删除(吊销)API Key,查看余额、充值总额、分页充值记录、分页消费记录与可用模型;运营可在 django-admin 检索用户、钱包、API Key、计费规则、汇率、充值订单、点数流水和调用记录,并通过计费层带原因手工调点;生产部署按 `docs/deployment.md` 执行。生产侧仍需配置真实支付、AI 模型和图片真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。详见 [`docs/current-state.md`](docs/current-state.md)。 +Phase 2 计费核心已完成,Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端已完成 T-501~T-505,Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档,Phase 6 已完成 T-601 可用别名发现、T-602/T-603 django-admin 中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地与 T-606 公开首页 + 客户端下载入口。用户可通过公开首页进入注册、登录和下载入口;登录后可扫码充值并轮询到账,生成 / 删除(吊销)API Key,查看余额、充值总额、分页充值记录、分页消费记录与可用模型;运营可在 django-admin 检索用户、钱包、API Key、计费规则、汇率、充值订单、点数流水、调用记录和客户端发布版本,并通过计费层带原因手工调点;生产部署按 `docs/deployment.md` 执行。生产侧仍需补真实支付回调到账闭环、配置并发布客户端下载包和图片真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。详见 [`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) 第四节计费时序。 diff --git a/apps/portal/admin.py b/apps/portal/admin.py index 8c38f3f..b534bae 100644 --- a/apps/portal/admin.py +++ b/apps/portal/admin.py @@ -1,3 +1,64 @@ from django.contrib import admin -# Register your models here. +from .models import DownloadRelease + + +@admin.register(DownloadRelease) +class DownloadReleaseAdmin(admin.ModelAdmin): + list_display = ( + "platform", + "version", + "is_current", + "download_source", + "sha256_short", + "updated_at", + ) + list_filter = ("platform", "is_current") + search_fields = ("version", "sha256", "external_url", "file") + readonly_fields = ("created_at", "updated_at") + fieldsets = ( + ( + "版本信息", + { + "fields": ( + "platform", + "version", + "is_current", + "release_notes", + ) + }, + ), + ( + "下载配置", + { + "fields": ( + "file", + "external_url", + "sha256", + ) + }, + ), + ( + "时间", + { + "fields": ( + "created_at", + "updated_at", + ) + }, + ), + ) + + @admin.display(description="下载来源") + def download_source(self, obj): + if obj.external_url: + return "外部链接" + if obj.file: + return "本地文件" + return "未配置" + + @admin.display(description="SHA256") + def sha256_short(self, obj): + if not obj.sha256: + return "-" + return f"{obj.sha256[:12]}..." diff --git a/apps/portal/migrations/0001_initial.py b/apps/portal/migrations/0001_initial.py new file mode 100644 index 0000000..a36e83d --- /dev/null +++ b/apps/portal/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.15 on 2026-07-06 09:48 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='DownloadRelease', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('platform', models.CharField(choices=[('windows', 'Windows'), ('macos', 'macOS'), ('linux', 'Linux')], max_length=32, verbose_name='平台')), + ('version', models.CharField(max_length=64, verbose_name='版本')), + ('file', models.FileField(blank=True, upload_to='downloads/', verbose_name='安装包文件')), + ('external_url', models.URLField(blank=True, verbose_name='外部下载地址')), + ('sha256', models.CharField(blank=True, max_length=64, validators=[django.core.validators.RegexValidator(message='SHA256 必须是 64 位十六进制字符。', regex='^[A-Fa-f0-9]{64}$')], verbose_name='SHA256')), + ('is_current', models.BooleanField(default=False, verbose_name='当前版本')), + ('release_notes', models.TextField(blank=True, verbose_name='发布说明')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'download_release', + 'verbose_name': '客户端下载版本', + 'verbose_name_plural': '客户端下载版本', + 'ordering': ('platform', '-is_current', '-created_at', '-id'), + 'indexes': [models.Index(fields=['platform', 'is_current'], name='dl_platform_current_idx')], + }, + ), + ] diff --git a/apps/portal/models.py b/apps/portal/models.py index 71a8362..a2b50f2 100644 --- a/apps/portal/models.py +++ b/apps/portal/models.py @@ -1,3 +1,71 @@ -from django.db import models +from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator +from django.db import models, transaction -# Create your models here. + +class DownloadRelease(models.Model): + class Platform(models.TextChoices): + WINDOWS = "windows", "Windows" + MACOS = "macos", "macOS" + LINUX = "linux", "Linux" + + platform = models.CharField("平台", max_length=32, choices=Platform.choices) + version = models.CharField("版本", max_length=64) + file = models.FileField("安装包文件", upload_to="downloads/", blank=True) + external_url = models.URLField("外部下载地址", blank=True) + sha256 = models.CharField( + "SHA256", + max_length=64, + blank=True, + validators=[ + RegexValidator( + regex=r"^[A-Fa-f0-9]{64}$", + message="SHA256 必须是 64 位十六进制字符。", + ) + ], + ) + is_current = models.BooleanField("当前版本", default=False) + release_notes = models.TextField("发布说明", blank=True) + created_at = models.DateTimeField("创建时间", auto_now_add=True) + updated_at = models.DateTimeField("更新时间", auto_now=True) + + class Meta: + db_table = "download_release" + verbose_name = "客户端下载版本" + verbose_name_plural = "客户端下载版本" + ordering = ("platform", "-is_current", "-created_at", "-id") + indexes = [ + models.Index( + fields=("platform", "is_current"), + name="dl_platform_current_idx", + ), + ] + + def __str__(self): + return f"{self.get_platform_display()} {self.version}" + + @property + def download_url(self) -> str: + if self.external_url: + return self.external_url + if self.file: + return self.file.url + return "" + + @property + def has_download(self) -> bool: + return bool(self.download_url) + + def clean(self): + super().clean() + if not self.file and not self.external_url: + raise ValidationError("安装包文件和外部下载地址至少填写一个。") + + def save(self, *args, **kwargs): + with transaction.atomic(): + super().save(*args, **kwargs) + if self.is_current: + type(self).objects.filter( + platform=self.platform, + is_current=True, + ).exclude(pk=self.pk).update(is_current=False) diff --git a/apps/portal/static/portal/brand.css b/apps/portal/static/portal/brand.css new file mode 100644 index 0000000..0a8717f --- /dev/null +++ b/apps/portal/static/portal/brand.css @@ -0,0 +1,634 @@ +:root { + --cm-paper: #F5F7FB; + --cm-card: #FFFFFF; + --cm-ink-strong: #161C2B; + --cm-ink: #1A2030; + --cm-ink-soft: #5A6478; + --cm-faint: #8A93A6; + --cm-line: #DCE1EC; + --cm-indigo: #5B4BEF; + --cm-indigo-deep: #3A2FB0; + --cm-amber: #E8A02D; + --cm-amber-ink: #C9821A; + --cm-mint: #3FCB7E; + --cm-chip-indigo: #ECEBFF; + --cm-chip-amber: #FBEFD9; + --cm-band-1: #1B1740; + --cm-band-2: #2A1E5E; + --cm-radius: 8px; + --cm-radius-lg: 8px; + --cm-font-cjk: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif; + --cm-font-latin: "Space Grotesk", system-ui, sans-serif; + --cm-font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; + --cm-btn: linear-gradient(#6455F2, #4335CF); + --cm-thread: linear-gradient(#5B4BEF, #E8A02D); + --cm-shadow: 0 18px 42px rgba(22, 28, 43, 0.09); + --cmhub-bg: var(--cm-paper); + --cmhub-ink: var(--cm-ink); + --cmhub-line: var(--cm-line); + --cmhub-accent: var(--cm-indigo); + --bs-body-font-family: var(--cm-font-cjk); + --bs-body-color: var(--cm-ink); + --bs-body-bg: var(--cm-paper); + --bs-border-color: var(--cm-line); + --bs-link-color: var(--cm-indigo); + --bs-link-hover-color: var(--cm-indigo-deep); +} + +body { + background: var(--cm-paper); + color: var(--cm-ink); +} + +.navbar { + border-bottom: 1px solid var(--cm-line); + background: rgba(255, 255, 255, 0.92); +} + +.navbar-brand { + align-items: center; + color: var(--cm-ink-strong); + display: inline-flex; + gap: 10px; +} + +.navbar-brand::before { + background: + linear-gradient(to top, var(--cm-indigo) 46%, transparent 46%) 0 100% / 5px 18px no-repeat, + linear-gradient(to top, var(--cm-indigo) 68%, transparent 68%) 9px 100% / 5px 18px no-repeat, + linear-gradient(to top, var(--cm-amber) 92%, transparent 92%) 18px 100% / 5px 18px no-repeat; + border: 1px solid var(--cm-line); + border-radius: 7px; + content: ""; + height: 28px; + width: 32px; +} + +.cmhub-shell { + margin: 0 auto; + max-width: 1080px; + padding: 40px 16px; +} + +.cmhub-panel { + background: var(--cm-card); + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + box-shadow: var(--cm-shadow); + margin: 0 auto; + max-width: 440px; + padding: 28px; +} + +.cmhub-panel form p { + margin-bottom: 16px; +} + +.cmhub-panel label { + color: var(--cm-ink-strong); + display: block; + font-weight: 600; + margin-bottom: 6px; +} + +.cmhub-panel input { + border: 1px solid #B9C2CF; + border-radius: var(--cm-radius); + min-height: 40px; + padding: 8px 10px; + width: 100%; +} + +.cmhub-panel .errorlist { + color: #B42318; + margin: 0 0 12px; + padding-left: 18px; +} + +.btn-primary { + --bs-btn-bg: var(--cm-indigo); + --bs-btn-border-color: var(--cm-indigo); + --bs-btn-hover-bg: var(--cm-indigo-deep); + --bs-btn-hover-border-color: var(--cm-indigo-deep); + --bs-btn-active-bg: var(--cm-indigo-deep); + --bs-btn-active-border-color: var(--cm-indigo-deep); + background-image: var(--cm-btn); +} + +.btn-outline-secondary { + --bs-btn-color: var(--cm-ink); + --bs-btn-border-color: var(--cm-line); + --bs-btn-hover-bg: var(--cm-chip-indigo); + --bs-btn-hover-border-color: var(--cm-indigo); + --bs-btn-hover-color: var(--cm-indigo-deep); +} + +.metric, +.cmhub-surface { + background: var(--cm-card); + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + box-shadow: 0 10px 30px rgba(22, 28, 43, 0.05); +} + +.metric { + padding: 18px; +} + +.metric .display-6, +.metric .h5, +.cmhub-surface h2 { + color: var(--cm-ink-strong); +} + +.metric .display-6 { + color: var(--cm-amber-ink); + font-family: var(--cm-font-mono); +} + +.cmhub-surface { + padding: 20px; +} + +.key-value, +code, +.font-monospace { + font-family: var(--cm-font-mono); + word-break: break-all; +} + +.table { + --bs-table-bg: transparent; + --bs-table-color: var(--cm-ink); + --bs-table-border-color: var(--cm-line); +} + +.badge.text-bg-secondary { + background-color: var(--cm-ink-soft) !important; +} + +.badge.text-bg-warning { + background-color: var(--cm-chip-amber) !important; + color: var(--cm-amber-ink) !important; +} + +.badge.text-bg-success { + background-color: var(--cm-mint) !important; + color: #0E3B24 !important; +} + +.home-page .navbar { + position: sticky; + top: 0; + z-index: 10; +} + +.home-main { + overflow: hidden; +} + +.home-container { + margin: 0 auto; + max-width: 1180px; + padding: 0 20px; +} + +.home-hero { + min-height: calc(100vh - 56px); + padding: 64px 0 34px; +} + +.home-hero-grid { + align-items: center; + display: grid; + gap: 42px; + grid-template-columns: minmax(0, 0.96fr) minmax(360px, 1.04fr); +} + +.home-eyebrow { + color: var(--cm-indigo); + font-size: 13px; + font-weight: 700; + letter-spacing: 0; + margin-bottom: 14px; +} + +.home-hero h1, +.home-section-title, +.home-band-title { + color: var(--cm-ink-strong); + font-family: var(--cm-font-latin), var(--cm-font-cjk); + font-weight: 700; + letter-spacing: 0; +} + +.home-hero h1 { + font-size: clamp(2.35rem, 5vw, 3.55rem); + line-height: 1.05; + margin-bottom: 20px; +} + +.home-lead { + color: var(--cm-ink-soft); + font-size: 1.08rem; + line-height: 1.75; + max-width: 620px; +} + +.home-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 28px; +} + +.home-proof { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 24px; +} + +.home-chip { + align-items: center; + background: var(--cm-card); + border: 1px solid var(--cm-line); + border-radius: 999px; + color: var(--cm-ink-soft); + display: inline-flex; + font-size: 0.88rem; + gap: 8px; + min-height: 34px; + padding: 6px 12px; +} + +.home-chip::before { + background: var(--cm-mint); + border-radius: 999px; + content: ""; + height: 8px; + width: 8px; +} + +.home-stage { + background: var(--cm-card); + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius-lg); + box-shadow: var(--cm-shadow); + padding: 18px; +} + +.home-product-shot { + background: + linear-gradient(135deg, rgba(91, 75, 239, 0.08), rgba(232, 160, 45, 0.16)), + var(--cm-paper); + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + min-height: 160px; + padding: 18px; +} + +.home-product-frame { + align-items: center; + background: #FBFCFF; + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + display: flex; + gap: 14px; + min-height: 120px; + padding: 14px; +} + +.home-product-image { + align-items: center; + background: linear-gradient(145deg, #E8EBF7, #FFFFFF); + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + color: var(--cm-indigo); + display: flex; + flex: 0 0 108px; + font-weight: 700; + height: 92px; + justify-content: center; +} + +.home-product-copy { + color: var(--cm-ink-soft); + font-size: 0.9rem; + line-height: 1.6; +} + +.home-thread { + background: var(--cm-thread); + border-radius: 999px; + height: 56px; + margin: 16px auto; + position: relative; + width: 4px; +} + +.home-thread::before, +.home-thread::after { + background: var(--cm-card); + border: 2px solid var(--cm-line); + border-radius: 999px; + content: ""; + height: 12px; + left: -4px; + position: absolute; + width: 12px; +} + +.home-thread::before { + top: -2px; +} + +.home-thread::after { + bottom: -2px; +} + +.home-output-grid { + display: grid; + gap: 12px; + grid-template-columns: 1fr 1fr; +} + +.home-output { + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + padding: 14px; +} + +.home-output-title { + color: var(--cm-ink-strong); + font-weight: 700; + margin-bottom: 10px; +} + +.home-output-text { + color: var(--cm-ink-soft); + font-size: 0.92rem; + line-height: 1.6; +} + +.home-point-pill { + background: var(--cm-chip-amber); + border-radius: 999px; + color: var(--cm-amber-ink); + display: inline-flex; + font-family: var(--cm-font-mono); + font-size: 0.84rem; + font-weight: 700; + margin-top: 12px; + padding: 5px 10px; +} + +.home-section { + padding: 70px 0; +} + +.home-section-heading { + display: flex; + justify-content: space-between; + align-items: end; + gap: 24px; + margin-bottom: 28px; +} + +.home-section-title { + font-size: clamp(1.9rem, 4vw, 2.4rem); + line-height: 1.15; + margin: 0; +} + +.home-section-copy { + color: var(--cm-ink-soft); + line-height: 1.7; + margin: 0; + max-width: 520px; +} + +.home-steps, +.home-cards { + display: grid; + gap: 16px; +} + +.home-steps { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.home-cards { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.home-step, +.home-card, +.home-download-box { + background: var(--cm-card); + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + padding: 22px; +} + +.home-step-num { + color: var(--cm-indigo); + font-family: var(--cm-font-mono); + font-size: 0.86rem; + font-weight: 700; + margin-bottom: 18px; +} + +.home-step h3, +.home-card h3, +.home-download-box h3 { + color: var(--cm-ink-strong); + font-size: 1.12rem; + font-weight: 700; + margin-bottom: 10px; +} + +.home-step p, +.home-card p, +.home-download-box p { + color: var(--cm-ink-soft); + line-height: 1.7; + margin-bottom: 0; +} + +.home-card { + min-height: 210px; +} + +.home-card-icon { + align-items: center; + background: var(--cm-chip-indigo); + border-radius: var(--cm-radius); + color: var(--cm-indigo-deep); + display: inline-flex; + font-weight: 700; + height: 44px; + justify-content: center; + margin-bottom: 22px; + width: 44px; +} + +.home-band { + background: linear-gradient(135deg, var(--cm-band-1), var(--cm-band-2)); + color: #FFFFFF; + padding: 62px 0; +} + +.home-band-grid { + align-items: center; + display: grid; + gap: 28px; + grid-template-columns: 1.1fr 0.9fr; +} + +.home-band-title { + color: #FFFFFF; + font-size: clamp(1.9rem, 4vw, 2.35rem); + line-height: 1.18; + margin-bottom: 14px; +} + +.home-band p { + color: rgba(255, 255, 255, 0.78); + line-height: 1.75; + margin-bottom: 0; +} + +.home-band-meter { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: var(--cm-radius); + padding: 22px; +} + +.home-band-meter strong { + color: var(--cm-amber); + display: block; + font-family: var(--cm-font-mono); + font-size: 2.2rem; + line-height: 1; + margin-bottom: 8px; +} + +.home-download-grid { + display: grid; + gap: 20px; + grid-template-columns: minmax(0, 1fr) minmax(320px, 0.74fr); +} + +.home-release-meta { + display: grid; + gap: 12px; + margin-top: 18px; +} + +.home-release-row { + align-items: start; + display: grid; + gap: 10px; + grid-template-columns: 96px minmax(0, 1fr); +} + +.home-release-row span { + color: var(--cm-faint); +} + +.home-release-row strong, +.home-release-row code { + color: var(--cm-ink); + min-width: 0; +} + +.home-notice { + background: var(--cm-chip-amber); + border: 1px solid rgba(232, 160, 45, 0.34); + border-radius: var(--cm-radius); + color: #6F4708; + line-height: 1.7; + margin-top: 18px; + padding: 14px 16px; +} + +.home-links { + display: grid; + gap: 12px; +} + +.home-link-row { + align-items: center; + border: 1px solid var(--cm-line); + border-radius: var(--cm-radius); + color: var(--cm-ink); + display: flex; + justify-content: space-between; + min-height: 54px; + padding: 12px 14px; + text-decoration: none; +} + +.home-link-row:hover { + background: var(--cm-chip-indigo); + color: var(--cm-indigo-deep); +} + +@media (max-width: 991.98px) { + .home-hero-grid, + .home-band-grid, + .home-download-grid { + grid-template-columns: 1fr; + } + + .home-steps { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 575.98px) { + .cmhub-shell { + padding: 28px 12px; + } + + .home-container { + padding: 0 14px; + } + + .home-hero { + min-height: auto; + padding: 44px 0 28px; + } + + .home-hero-grid, + .home-output-grid, + .home-steps, + .home-cards { + grid-template-columns: 1fr; + } + + .home-section-heading { + align-items: start; + flex-direction: column; + } + + .home-actions .btn { + width: 100%; + } + + .home-product-frame, + .home-release-row { + grid-template-columns: 1fr; + } + + .home-product-frame { + align-items: stretch; + display: grid; + } + + .home-product-image { + width: 100%; + } +} diff --git a/apps/portal/templates/portal/base.html b/apps/portal/templates/portal/base.html index 9b28dab..24e844f 100644 --- a/apps/portal/templates/portal/base.html +++ b/apps/portal/templates/portal/base.html @@ -6,78 +6,9 @@ {% block title %}cmhub{% endblock %} - + - + + {% block body %}
{% if messages %}
@@ -112,5 +44,6 @@ {% endif %} {% block content %}{% endblock %}
+ {% endblock %} diff --git a/apps/portal/templates/portal/home.html b/apps/portal/templates/portal/home.html new file mode 100644 index 0000000..97845c5 --- /dev/null +++ b/apps/portal/templates/portal/home.html @@ -0,0 +1,199 @@ +{% extends "portal/base.html" %} + +{% block title %}cmhub AI 电商生成台{% endblock %} +{% block body_class %}home-page{% endblock %} + +{% block body %} +
+
+
+
+
AI 生成 / 点数计费 / 桌面端接入
+

cmhub AI 电商生成台

+

+ 把生成标题、生成主图能力封装成稳定的 HTTP API。桌面端填入 API Key 后按本地点数扣费, + 余额不足直接拦截,失败自动退点。 +

+
+ {% if user.is_authenticated %} + 进入控制台 + {% else %} + 免费注册 + 登录 + {% endif %} + {% if current_release and current_release.has_download %} + 下载客户端 + {% else %} + 客户端下载暂未发布 + {% endif %} +
+
+ 注册即可用 + 按点数扣点 + API Key 接入 +
+
+ +
+
+
+
商品图
+
+ 轻量夹克,春秋外套,通勤穿搭,图片清晰,适合电商标题与主图优化。 +
+
+
+ +
+
+
吸睛标题
+
春秋通勤轻量夹克,防风舒适,男女日常外套
+ -2 点 +
+
+
生成主图
+
保留商品主体,生成干净背景和电商展示构图。
+ -12 点 +
+
+
+
+
+ +
+
+
+

四步接入桌面端

+

从账号到调用链路保持简单:账号、点数、API Key 和客户端配置各司其职。

+
+
+
+
01
+

注册账号

+

邮箱注册后直接登录,系统创建 0 点钱包,不赠点、不写虚假流水。

+
+
+
02
+

充值点数

+

扫码创建充值订单,到账后按汇率入账到本地点数账本。

+
+
+
03
+

生成 API Key

+

在控制台生成 Key,明文只显示一次,后续列表只展示 prefix。

+
+
+
04
+

填入桌面端

+

客户端使用能力别名调用标题或图片接口,按规则扣点。

+
+
+
+
+ +
+
+
+

面向电商流程的两类能力

+

对外只暴露能力别名,不暴露底层供应商 SKU、URL、密钥或 extra_body。

+
+
+
+
文
+

生成标题

+

按商品信息、提示词和能力别名生成标题。敏感词命中会在扣点和上游调用前拦截。

+
+
+
图
+

生成主图(改图)

+

支持需要原图的图片能力,图片输入先做公网地址和大小校验,结果保存后返回 URL。

+
+
+
+
+ +
+
+
+

点数余额是唯一权威账本

+

+ 充值成功后写入本地点数;每次生成先预扣,成功确认,失败退点。调用链路不实时查询支付系统。 +

+
+
+ 本地扣点 +

余额不足返回“点数不足,请先充值”,不会继续调用上游模型。

+
+
+
+ +
+
+
+

下载桌面端

+

下载走 HTTPS,页面展示 SHA256 供校验;安装包前期由服务器 media 目录托管,生产 Nginx 直接服务。

+
+
+
+ {% if current_release and current_release.has_download %} +

Windows 客户端 {{ current_release.version }}

+

当前版本已发布,可下载后填入 cmhub API Key 使用。

+
+ 下载客户端 + {% if user.is_authenticated %} + 管理 API Key + {% else %} + 注册账号 + {% endif %} +
+
+
+ 版本 + {{ current_release.version }} +
+
+ SHA256 + {{ current_release.sha256|default:"未提供" }} +
+ {% if current_release.release_notes %} +
+ 说明 + {{ current_release.release_notes|linebreaksbr }} +
+ {% endif %} +
+
+ 首期安装包可能未签名。Windows 若提示未知发布者,请先核对 SHA256,再选择“仍要运行”;正式代码签名后会更新版本说明。 +
+ {% else %} +

客户端暂未发布

+

后台尚未配置当前 Windows 版本。发布后这里会展示版本号、下载按钮、SHA256 和版本说明。

+
+ 暂未发布 + {% if user.is_authenticated %} + 进入控制台 + {% else %} + 先注册账号 + {% endif %} +
+ {% endif %} +
+
+

接口与控制台

+ +
+
+
+
+
+{% endblock %} diff --git a/apps/portal/tests.py b/apps/portal/tests.py index 9e29790..37b7378 100644 --- a/apps/portal/tests.py +++ b/apps/portal/tests.py @@ -18,6 +18,7 @@ from apps.billing.models import ( PricingRule, RechargeOrder, ) +from apps.portal.models import DownloadRelease from apps.users.models import ApiKey, UserWallet @@ -110,6 +111,25 @@ class PortalAccountFlowTests(TestCase): ai_model=ai_model, ) + def create_download_release( + self, + *, + version: str = "1.0.0", + platform: str = DownloadRelease.Platform.WINDOWS, + is_current: bool = True, + external_url: str = "https://download.example.com/cmhub-desktop.exe", + sha256: str = "a" * 64, + release_notes: str = "首版 Windows 客户端", + ) -> DownloadRelease: + return DownloadRelease.objects.create( + platform=platform, + version=version, + is_current=is_current, + external_url=external_url, + sha256=sha256, + release_notes=release_notes, + ) + def assert_nav_link_active(self, response, *, href: str, label: str): html = response.content.decode("utf-8") self.assertIn( @@ -211,6 +231,56 @@ class PortalAccountFlowTests(TestCase): self.assertEqual(response.status_code, 403) + def test_homepage_is_public_and_shows_anonymous_onboarding_without_release(self): + response = self.client.get("/") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.resolver_match.url_name, "portal-home") + self.assertContains(response, "cmhub AI 电商生成台") + self.assertContains(response, "免费注册") + self.assertContains(response, "登录") + self.assertContains(response, "客户端暂未发布") + self.assertContains(response, "暂未发布") + self.assertNotContains(response, "/login?next=") + + def test_homepage_shows_current_download_release(self): + release = self.create_download_release( + version="1.2.3", + external_url="https://download.example.com/cmhub-1.2.3.exe", + sha256="b" * 64, + release_notes="修复下载入口并补充 SHA256", + ) + + response = self.client.get("/") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["current_release"], release) + self.assertContains(response, "Windows 客户端 1.2.3") + self.assertContains(response, "https://download.example.com/cmhub-1.2.3.exe") + self.assertContains(response, "b" * 64) + self.assertContains(response, "修复下载入口并补充 SHA256") + self.assertContains(response, "未知发布者") + + def test_homepage_shows_dashboard_entry_for_authenticated_user(self): + user = self.create_verified_user() + self.client.force_login(user) + + response = self.client.get("/") + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "进入控制台") + self.assertNotContains(response, "免费注册") + + def test_download_release_only_keeps_one_current_per_platform(self): + old_release = self.create_download_release(version="1.0.0", sha256="c" * 64) + new_release = self.create_download_release(version="1.1.0", sha256="d" * 64) + + old_release.refresh_from_db() + new_release.refresh_from_db() + + self.assertFalse(old_release.is_current) + self.assertTrue(new_release.is_current) + def test_apikeys_requires_session_login(self): response = self.client.get("/apikeys") diff --git a/apps/portal/urls.py b/apps/portal/urls.py index d347caa..ed35bf3 100644 --- a/apps/portal/urls.py +++ b/apps/portal/urls.py @@ -1,11 +1,11 @@ from allauth.account.views import LoginView, LogoutView, SignupView from django.urls import path -from django.views.generic import RedirectView from .views import ( ApiKeyDeleteView, ApiKeyListCreateView, DashboardView, + HomeView, ModelCatalogView, RechargePageView, RechargeRecordListView, @@ -13,7 +13,7 @@ from .views import ( ) urlpatterns = [ - path("", RedirectView.as_view(pattern_name="portal-dashboard", permanent=False), name="portal-home"), + path("", HomeView.as_view(), 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"), diff --git a/apps/portal/views.py b/apps/portal/views.py index 1524476..1ab751f 100644 --- a/apps/portal/views.py +++ b/apps/portal/views.py @@ -19,6 +19,7 @@ from apps.billing.services import ( from apps.users.models import ApiKey from .forms import ApiKeyCreateForm, RechargeCreateForm +from .models import DownloadRelease NEW_API_KEY_SESSION_KEY = "portal_new_api_key" @@ -88,6 +89,22 @@ def paginate_records(request, queryset): } +class HomeView(TemplateView): + template_name = "portal/home.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["current_release"] = ( + DownloadRelease.objects.filter( + platform=DownloadRelease.Platform.WINDOWS, + is_current=True, + ) + .order_by("-created_at", "-id") + .first() + ) + return context + + class DashboardView(LoginRequiredMixin, TemplateView): template_name = "portal/dashboard.html" diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 0543873..15645a1 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -38,7 +38,7 @@ ## 当前阶段 -当前项目处于:**Phase 6 增强任务推进期**。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 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化;Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;Phase 6 已完成 T-601「可用别名发现」、T-602「django-admin 中文化第 1-3 层」、T-603「django-admin 字段级中文化」、T-604「中文敏感词本地过滤」与 T-605「免邮箱验证策略落地」。下一步按看板进入 T-606「公开首页 + 客户端下载入口」。生产侧仍需补真实支付回调到账闭环和图片生成真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。 +当前项目处于:**Phase 6 增强任务推进期**。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 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化;Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;Phase 6 已完成 T-601「可用别名发现」、T-602「django-admin 中文化第 1-3 层」、T-603「django-admin 字段级中文化」、T-604「中文敏感词本地过滤」、T-605「免邮箱验证策略落地」与 T-606「公开首页 + 客户端下载入口」。生产侧仍需补真实支付回调到账闭环、配置并发布客户端下载包和图片生成真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。 优先路径: @@ -48,7 +48,7 @@ 4. Phase 3:对外 API 与充值 —— T-301 Key 鉴权、T-302 生成接口、T-303 余额查询、T-304 充值回调、T-305 扫码下单与轮询、T-306 安全加固已完成。 5. Phase 4:用户端(Django 模板 SSR)—— T-501 注册登录、T-502 API Key 管理、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成。 6. Phase 5:后台与发布 —— T-401 运营后台完善、T-402 完整验收 MVP、T-403 部署 / 运行文档已完成;计划内 MVP 任务已收尾。 -7. Phase 6:增强(MVP 后)—— T-601 可用别名发现已完成,实现 `/api/v1/models` 与 portal 只读「可用模型」页;T-602 已完成 django-admin 分组/表名中文化;T-603 已完成字段级中文标签代码与 no-op 迁移并人工确认 admin 字段中文化;T-604 已完成中文敏感词本地过滤;T-605 已完成免邮箱验证策略落地;下一步是 T-606 公开首页 + 客户端下载入口。 +7. Phase 6:增强(MVP 后)—— T-601 可用别名发现已完成,实现 `/api/v1/models` 与 portal 只读「可用模型」页;T-602 已完成 django-admin 分组/表名中文化;T-603 已完成字段级中文标签代码与 no-op 迁移并人工确认 admin 字段中文化;T-604 已完成中文敏感词本地过滤;T-605 已完成免邮箱验证策略落地;T-606 已完成公开首页 + 客户端下载入口。下一步需先确认业务优先级或拆新任务。 ## 领取任务规则 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index b4aa34d..0659faa 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -18,7 +18,7 @@ 组件落位: -- **用户端层(Django 模板 SSR)**:注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值。入口 `apps/portal/`,用 session 鉴权。T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`;注册成功通过 allauth adapter 创建 0 点 `UserWallet`,不写赠点流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单与消费流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/消费记录页从固定切片改为分页。 +- **用户端层(Django 模板 SSR)**:公开首页、注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值、模型目录与客户端下载入口。入口 `apps/portal/`,公开首页匿名可访问,其他自助页面用 session 鉴权。T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`;注册成功通过 allauth adapter 创建 0 点 `UserWallet`,不写赠点流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单与消费流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/消费记录页从固定切片改为分页。T-606 已把 `/` 改为公开首页,并新增 `DownloadRelease` 下载版本配置用于展示 Windows 客户端版本、下载地址、SHA256 与发布说明。 - **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。 - **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。 - **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。 @@ -97,6 +97,7 @@ T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前 | PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel | | ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),按 `currency + effective_from` 取当前有效值 | | SensitiveWord | 运营配置 | 本地敏感词快筛词库;word、normalized_word、category、action、is_active;T-604 MVP 仅支持 action=block | +| DownloadRelease | 运营配置 | 客户端下载版本;platform、version、file、external_url、sha256、is_current、release_notes、created_at、updated_at;每平台当前版本由应用层事务保存时互斥 | 关键事实: @@ -184,9 +185,23 @@ CREATE TABLE exchange_rate ( created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NOT NULL ); + +-- 客户端下载版本(首页读取当前版本;安装包生产由 Nginx 直接服务 media) +CREATE TABLE download_release ( + id BIGINT PRIMARY KEY, + platform VARCHAR(32) NOT NULL, -- windows / macos / linux + version VARCHAR(64) NOT NULL, + file VARCHAR(100) NOT NULL DEFAULT '', -- MEDIA_ROOT/downloads/... + external_url VARCHAR(200) NOT NULL DEFAULT '', -- 有则优先,后续切 CDN/对象存储 + sha256 VARCHAR(64) NOT NULL DEFAULT '', + is_current BOOLEAN NOT NULL DEFAULT FALSE, + release_notes LONGTEXT NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL +); ``` -T-102 已实现 `AiModel` / `ModelAlias` 的 Django models、admin、迁移与别名解析。T-103 已补 `AiConfigAuditLog`,admin 里保存/删除模型配置或能力别名时自动写审计日志。T-202 已实现 `PricingRule` / `ExchangeRate` 与 `apps.billing.pricing` 计算函数:定价按 `operation_type + alias + resolution` 查 active 规则,优先 exact resolution,再回退到空 resolution 默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`。T-203 已实现 `apps.billing.services`:`precharge_call()` 锁 `UserWallet` 行预扣并写 pending 调用与 consume 流水;`mark_call_success()` 确认成功不再改余额;`refund_call_points()` 锁调用记录并幂等退点,写 refund 流水。T-401 已在同一计费层新增 `adjust_wallet_points()`,供 admin 手工调点使用。默认别名唯一性由 model validation、admin 与导入器保证;MySQL 不支持通用 partial unique index,若后续要强制数据库层默认别名唯一,可另评估触发器或约束表。 +T-102 已实现 `AiModel` / `ModelAlias` 的 Django models、admin、迁移与别名解析。T-103 已补 `AiConfigAuditLog`,admin 里保存/删除模型配置或能力别名时自动写审计日志。T-202 已实现 `PricingRule` / `ExchangeRate` 与 `apps.billing.pricing` 计算函数:定价按 `operation_type + alias + resolution` 查 active 规则,优先 exact resolution,再回退到空 resolution 默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`。T-203 已实现 `apps.billing.services`:`precharge_call()` 锁 `UserWallet` 行预扣并写 pending 调用与 consume 流水;`mark_call_success()` 确认成功不再改余额;`refund_call_points()` 锁调用记录并幂等退点,写 refund 流水。T-401 已在同一计费层新增 `adjust_wallet_points()`,供 admin 手工调点使用。T-606 已实现 `DownloadRelease`,admin 可上传安装包或填写 `external_url` 并标记当前版本;`external_url` 优先于 `file.url`,每平台仅一个当前版本由模型 `save()` 在事务内把同平台旧 current 置为 false。默认别名唯一性由 model validation、admin 与导入器保证;MySQL 不支持通用 partial unique index,若后续要强制数据库层默认别名唯一,可另评估触发器或约束表。 > 可选增强(接口预留、MVP 不实现):`account_alias_permission`(按账号授权可用别名,防止调用方点用未授权/昂贵模型);别名按比例分流到多个模型(灰度/AB/故障转移)。适配器接口需为此留口子。 @@ -391,7 +406,7 @@ cmhub/ ├── config/ # Django 工程:settings / urls / wsgi ├── apps/ │ ├── users/ # 注册用户 User、UserWallet 钱包、ApiKey(模型 + admin) -│ ├── portal/ # 用户端页面(Django 模板 SSR):注册/登录/个人中心/充值/API Key 管理 +│ ├── portal/ # 用户端页面(Django 模板 SSR):公开首页/注册/登录/个人中心/充值/API Key/下载版本 │ ├── billing/ # 计费规则、汇率、充值订单、点数流水、扣点/入账逻辑(点数唯一写入方) │ ├── ai/ # 别名解析 + Provider 适配器层 │ │ └── providers/ # 各供应商适配器(chat/gemini/images_edits…),移植自 cmbot diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 8b0c0a2..daa4016 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -84,7 +84,7 @@ | T-603 | django-admin 中文化(第 4 层·字段级) | T-602 | 给各模型**字段**加中文 `verbose_name`,使 admin 列表/编辑页的字段标签显示中文,在 T-602(1-3 层)之上做。范围覆盖 `users`/`billing`/`ai`(及 portal 相关)模型的业务字段,例如 `points_balance`→点数余额、`key_prefix`→Key 前缀、`points_delta`→点数变动、`balance_after`→变动后余额、`amount_money`→金额、`exchange_rate`→汇率、`points_granted`→到账点数、`api_type`/`capabilities`/`is_active`/`created_at` 等;`admin.py` 里自定义显示方法(`@admin.display(description=...)`)同步中文。**仅显示层**:`verbose_name` 只改展示,**不改字段名、不改代码引用、英文字段名保持不变**;产出的是 `AlterField`(metadata-only,**无 DB schema 变更**)迁移。验收:`makemigrations`(仅 AlterField,无 schema 变更)→ `migrate` → `check` 0 issues → `test` 全绿 → `/admin/` 字段标签目视中文,并在 `../progress.md` 留证据。已人工确认 admin 字段中文化,目标后台回归测试通过 | DONE | | T-605 | 落实「免邮箱验证」策略 | T-501 | 把 `ACCOUNT_EMAIL_VERIFICATION="none"`(注册即可用、不发验证邮件、邮箱仍必填且唯一)作为**既定策略**清理落地:① `config/settings.py` 把 `ACCOUNT_EMAIL_VERIFICATION = "none"#"mandatory"` 改为干净的 `"none"`(去行内注释),并把免验证下无实际意义的 `ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION` 设为 `False`;② 更新 `apps/portal/tests.py` 里假设 mandatory 的 2 条测试(signup 不再依赖验证邮件、未验证也可直接登录),改为断言「注册后可直接登录」;③ 同步全项目文档口径(`02-requirements`/`05-coding-rules`/`api`/`04-architecture`/`03-tech-stack`/`routes`/`env`/`deployment`/`00-ai-start-here` 中「邮箱验证」→「免邮箱验证,邮箱仍唯一」)。验收:`check` 通过,`apps.portal` 26 tests OK;全量 `manage.py test` 已尝试,跑到 95/131 后因远程 MySQL 连接超时失败(WinError 10051/10060),非本任务断言失败,详见 `../progress.md` | DONE | | T-604 | 中文敏感词本地过滤(本地 keyword provider) | T-302, T-401 | 按 [`moderation.md`](moderation.md) 实施。**范围收紧**:T-604 只做输入 prompt 的本地敏感词快筛,不做云内容安全、不做输出审核、不做图片审核。**关键时序**:serializer 后先审 prompt,命中即 `400 content_blocked`;不得先下载 `image_url`,不得预扣点,不写 `CallRecord` / `PointsLedger`,不调上游。**核心实现**:新增 `apps.moderation`、`SensitiveWord` 模型/admin/迁移、keyword provider、归一化管线、Aho-Corasick matcher;`ahocorapy` 作为候选依赖,编码前必须验证 PyPI 可用性和 API 形状。**缓存**:matcher 进程内缓存,词库变更用共享 cache 版本号失效,不能只靠 `post_save` signal;生产依赖共享 cache。**配置**:`MODERATION_ENABLED=false` 默认 no-op;启用时 `MODERATION_PROVIDER=keyword`;MVP `SensitiveWord.action` 只支持 `block`。**验收**:`check`/`test` 全绿,覆盖 no-op、命中拦截且不扣点/不建记录/不调上游/不下载图片、归一化防绕过、词库变更后 matcher 重建;真实词库数据不进仓库,`__pycache__` 不进 Git | DONE | -| T-606 | 公开首页 + 客户端下载入口 | T-501 | 给网站补「前门」并提供桌面端下载。**路由改造**:`/` 从「重定向到 `/dashboard`」改为**公开首页**(匿名可访问、不跳登录);已登录用户显示「进入控制台」,匿名显示「注册/登录 + 下载客户端」。**首页内容**(SSR 模板):项目一句话介绍(生成标题/图片、按点数计费)+ 三步上手(注册→充值→建 API Key→桌面端填 Key)+ 下载入口 + 文档链接。**`DownloadRelease` 模型**:`platform`(如 windows)、`version`、`file`(FileField,**前期存 `MEDIA_ROOT`、服务器托管**) + `external_url`(URLField 可选,**后续切对象存储/CDN 用,有则优先**)、`sha256`、`is_current`(每平台仅一个当前版本)、`release_notes`、时间戳;admin 可上传安装包并标记当前版本;迁移。**下载区块**:展示当前 release 的版本、下载按钮、**SHA256 校验值**、可选 release notes;无 current release 时优雅提示「暂未发布」。**托管策略**:**前期安装包放本服务器**(生产由 **Nginx 直接服务 media/下载文件、不走 Django**,与 T-403 static/media serving 一致,大文件不占 gunicorn worker);`external_url` 预留,后续切对象存储只改后台链接不改代码。**安全**:下载走 HTTPS,页面展示 SHA256 供校验;**代码签名**作为决策登记——未签名 Windows 安装包会被 SmartScreen 拦「未知发布者」、macOS 被 Gatekeeper 拦,首页先给「如何忽略警告」说明,正式签名后续补(挂 Backlog / deployment)。**含测试**:`/` 匿名 200 不跳登录、下载区展示当前 release、无 current release 优雅处理、已登录用户显示「进入控制台」。**视觉原型(已定 v1)**:按 `prototypes/cmhub-homepage-v1.svg` 落地——「生成台」方向:靛蓝=生成 / 琥珀=点数;Hero 为「商品图 + 一句话 → 吸睛标题 + 生成主图 + 点数计量」转化图;四步上手 01–04、两张能力卡、深色计费 band、下载区(版本/SHA256/未签名提示);配色、间距、结构照此原型转成 Django 模板(Bootstrap + 本地 static)。**视觉一致性(brand token)**:按 [`brand.md`](brand.md) 抽出共享 CSS 变量(如 `apps/portal/static/portal/brand.css`),**首页与现有 portal 页面(dashboard/记录/充值/API Key)一起套用同一套 token**(把配色/字体变量灌进现有 Bootstrap,各页只引用变量不散写 hex),确保落地页与登录后控制台风格一致——**不是只做漂亮首页**。验收 `check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | TODO | +| T-606 | 公开首页 + 客户端下载入口 | T-501 | 给网站补「前门」并提供桌面端下载。**路由改造**:`/` 从「重定向到 `/dashboard`」改为**公开首页**(匿名可访问、不跳登录);已登录用户显示「进入控制台」,匿名显示「注册/登录 + 下载客户端」。**首页内容**(SSR 模板):项目一句话介绍(生成标题/图片、按点数计费)+ 三步上手(注册→充值→建 API Key→桌面端填 Key)+ 下载入口 + 文档链接。**`DownloadRelease` 模型**:`platform`(如 windows)、`version`、`file`(FileField,**前期存 `MEDIA_ROOT`、服务器托管**) + `external_url`(URLField 可选,**后续切对象存储/CDN 用,有则优先**)、`sha256`、`is_current`(每平台仅一个当前版本)、`release_notes`、时间戳;admin 可上传安装包并标记当前版本;迁移。**下载区块**:展示当前 release 的版本、下载按钮、**SHA256 校验值**、可选 release notes;无 current release 时优雅提示「暂未发布」。**托管策略**:**前期安装包放本服务器**(生产由 **Nginx 直接服务 media/下载文件、不走 Django**,与 T-403 static/media serving 一致,大文件不占 gunicorn worker);`external_url` 预留,后续切对象存储只改后台链接不改代码。**安全**:下载走 HTTPS,页面展示 SHA256 供校验;**代码签名**作为决策登记——未签名 Windows 安装包会被 SmartScreen 拦「未知发布者」、macOS 被 Gatekeeper 拦,首页先给「如何忽略警告」说明,正式签名后续补(挂 Backlog / deployment)。**含测试**:`/` 匿名 200 不跳登录、下载区展示当前 release、无 current release 优雅处理、已登录用户显示「进入控制台」。**视觉原型(已定 v1)**:按 `prototypes/cmhub-homepage-v1.svg` 落地——「生成台」方向:靛蓝=生成 / 琥珀=点数;Hero 为「商品图 + 一句话 → 吸睛标题 + 生成主图 + 点数计量」转化图;四步上手 01–04、两张能力卡、深色计费 band、下载区(版本/SHA256/未签名提示);配色、间距、结构照此原型转成 Django 模板(Bootstrap + 本地 static)。**视觉一致性(brand token)**:按 [`brand.md`](brand.md) 抽出共享 CSS 变量(如 `apps/portal/static/portal/brand.css`),**首页与现有 portal 页面(dashboard/记录/充值/API Key)一起套用同一套 token**(把配色/字体变量灌进现有 Bootstrap,各页只引用变量不散写 hex),确保落地页与登录后控制台风格一致——**不是只做漂亮首页**。验收 `check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | DONE | ## 里程碑 diff --git a/docs/api.md b/docs/api.md index 6b90add..f640033 100644 --- a/docs/api.md +++ b/docs/api.md @@ -41,6 +41,8 @@ T-503/T-505 已实现用户端个人中心 / 记录页基线:`/dashboard` 走 T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session + CSRF,GET 展示余额、充值表单、当前订单和最近充值;POST 创建 pending 充值订单并用本地 static 自托管的 qrcode.js 展示支付二维码票据;浏览器轮询 `GET /api/v1/recharge/status`,订单 paid 后刷新页面重新读取余额。页面不直接加点,到账仍以支付回调或主动查单入账后的本地订单状态为准。若当前 `PAYMENT_CALLBACK_MODE=mock`,页面必须明确提示二维码为测试票据、不能用微信/支付宝真实支付,避免用户把 mock 二维码当成生产收款码。当前因支付宝可信 IP 未配置,用户端 `/recharge` 表单暂只开放微信支付,底层 JSON API / 回调 / SDK 路径仍保留支付宝。 +T-606 已实现公开首页与客户端下载入口:`GET /` 匿名返回 200,不再重定向到 `/dashboard`;匿名用户看到注册 / 登录 / 下载入口,登录用户看到「进入控制台」。下载区读取 `DownloadRelease(platform=windows, is_current=True)`,展示版本、下载按钮、SHA256 和发布说明;`external_url` 优先于后台上传文件的 `file.url`。`DownloadRelease` 不新增对外 JSON API,只由 SSR 首页和 django-admin 使用;生产本地安装包由 Nginx 直接服务 `MEDIA_ROOT/downloads/`,避免大文件占用 Gunicorn worker。 + 通用错误响应: ```json diff --git a/docs/current-state.md b/docs/current-state.md index df6b625..bb7ba93 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -12,11 +12,12 @@ ## 当前快照 - 日期:2026-07-06 -- 阶段:Phase 6 增强(MVP 后);Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成,Phase 5 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档已完成,Phase 6 T-601 可用别名发现、T-602 django-admin 中文化第 1-3 层、T-603 django-admin 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地已完成;下一步可进入 T-606 公开首页 + 客户端下载入口 +- 阶段:Phase 6 增强(MVP 后);Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成,Phase 5 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档已完成,Phase 6 T-601 可用别名发现、T-602 django-admin 中文化第 1-3 层、T-603 django-admin 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地、T-606 公开首页 + 客户端下载入口已完成;后续可继续处理真实支付回调到账闭环、客户端下载包发布或真实图片耗时验证 - 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + django-allauth 65.18.0 + PyMySQL 1.1.3 + cryptography 49.0.0 + requests 2.34.2 + ahocorapy 1.6.2 + wechatpayv3 2.0.2 + python-alipay-sdk 3.4.0 + django-admin;MySQL 8.4 已接入 settings,并支持 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT`;用户端已用 Django 模板 SSR + Bootstrap + allauth 落地注册登录;生产部署口径为 VPS / 宝塔 + Nginx + Gunicorn(gthread) + systemd;详见 `03-tech-stack.md` 与 `deployment.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-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503 已扩展 `/dashboard` 为个人中心汇总,并新增 `/records/recharge` 充值记录与 `/records/usage` 消费记录,只读展示当前用户数据;T-504 已新增 `/recharge` 页面、`RechargeCreateForm`、充值导航入口和轮询脚本,页面创建 pending 订单、展示二维码票据、轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额;T-505 已把 Bootstrap 5 CSS 与 qrcode.js vendoring 到 `apps/portal/static/portal/vendor/`,页面不再依赖 jsdelivr,并把充值记录 / 消费记录改为 Django `Paginator` 分页;T-401 已新增 `adjust_wallet_points()` 手工调点服务、钱包 admin 专用调点表单与模板,后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录,流水/订单/调用记录保持只读;T-402 已新增 `docs/mvp-acceptance.md`,按 P0 验收矩阵记录 MVP 完整验收结论、测试证据和已知限制;T-403 已新增 `docs/deployment.md` 与 `requirements-production.txt`,并在 settings 中补齐 `STATIC_ROOT`、`CSRF_TRUSTED_ORIGINS`、共享 `CACHES`、HTTPS cookie、proxy SSL 与 HSTS 环境变量支持;T-601 已新增 `apps.ai.catalog.get_public_model_catalog()`、`GET /api/v1/models` 与 portal `/models` 只读页面,只展示 active 可调用别名、能力、是否需要原图和点数单价,不解密 provider key,不暴露底层 SKU / URL / key / `extra_body`;T-604 已新增 `apps.moderation`、`SensitiveWord` 模型/admin/迁移、keyword provider、归一化管线和共享 cache 版本失效,生成接口已改为 prompt 先审再读取图片/计费/扣点/调上游 +- 生产代码:已有最小 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-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503 已扩展 `/dashboard` 为个人中心汇总,并新增 `/records/recharge` 充值记录与 `/records/usage` 消费记录,只读展示当前用户数据;T-504 已新增 `/recharge` 页面、`RechargeCreateForm`、充值导航入口和轮询脚本,页面创建 pending 订单、展示二维码票据、轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额;T-505 已把 Bootstrap 5 CSS 与 qrcode.js vendoring 到 `apps/portal/static/portal/vendor/`,页面不再依赖 jsdelivr,并把充值记录 / 消费记录改为 Django `Paginator` 分页;T-401 已新增 `adjust_wallet_points()` 手工调点服务、钱包 admin 专用调点表单与模板,后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录,流水/订单/调用记录保持只读;T-402 已新增 `docs/mvp-acceptance.md`,按 P0 验收矩阵记录 MVP 完整验收结论、测试证据和已知限制;T-403 已新增 `docs/deployment.md` 与 `requirements-production.txt`,并在 settings 中补齐 `STATIC_ROOT`、`CSRF_TRUSTED_ORIGINS`、共享 `CACHES`、HTTPS cookie、proxy SSL 与 HSTS 环境变量支持;T-601 已新增 `apps.ai.catalog.get_public_model_catalog()`、`GET /api/v1/models` 与 portal `/models` 只读页面,只展示 active 可调用别名、能力、是否需要原图和点数单价,不解密 provider key,不暴露底层 SKU / URL / key / `extra_body`;T-604 已新增 `apps.moderation`、`SensitiveWord` 模型/admin/迁移、keyword provider、归一化管线和共享 cache 版本失效,生成接口已改为 prompt 先审再读取图片/计费/扣点/调上游;T-606 已新增公开首页 `/`、`DownloadRelease` 模型/admin/迁移、首页 SSR 模板、共享 `portal/brand.css`,并把现有 portal 页面套入同一套品牌 token。 - 用户端导航:顶部导航 active 状态已修复,`portal/base.html` 基于 `request.resolver_match.url_name` 高亮当前页面入口,并用 `aria-current="page"` 标记;「充值」不再在非充值页固定深色高亮。 - 测试:T-601 已验证:`.\init.ps1` 通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令);`py -3.12 -m py_compile apps\ai\catalog.py apps\api\views.py apps\api\urls.py apps\api\tests.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`git diff --check` 通过;`py -3.12 manage.py test apps.api.tests.ModelsCatalogApiTests apps.portal.tests.PortalAccountFlowTests.test_models_page_requires_session_login apps.portal.tests.PortalAccountFlowTests.test_models_page_lists_public_aliases_prices_and_unpriced_state --keepdb --noinput --verbosity 2` 首次运行时 3 条 API 测试已通过,portal `setUpClass` 阶段因远程 MySQL 连接 43.128.3.240 超时中断;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示 `TcpTestSucceeded=True`,单独重跑两条 portal 测试通过,2 tests OK。T-403 已验证:`./init.ps1` 开工前通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`py -3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` 找到两个本地 static 文件;`py -3.12 manage.py check --deploy` 在当前开发 `.env` 下仅报预期安全配置警告,临时注入生产型安全环境变量(含 HSTS includeSubDomains/preload)后通过,0 issues;`py -3.12 manage.py collectstatic --dry-run --noinput` 通过,预期收集 169 个 static 文件;`py -3.12 manage.py createcachetable --dry-run cmhub_cache` 通过,输出 MySQL cache 表 DDL;`py -3.12 -m compileall config` 通过;尝试 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` 时 25 条用例已通过,`GenerateApiTests` 14 条因远程 MySQL 连接重置/事务中断被记 ERROR,随后 `Test-NetConnection 43.128.3.240 -Port 3306` 端口可达,单独重跑 `py -3.12 manage.py test apps.api.tests.GenerateApiTests --noinput --keepdb --verbosity 2` 通过,14 tests OK。测试/迁移阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目用户账本邮箱唯一性由 `user.email` 唯一约束承担。 +- 最新验证:T-606 公开首页 + 客户端下载入口已验证 `.\init.ps1` 通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令);`py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py migrate portal` 已应用 `portal.0001_initial` 到当前数据库;`py -3.12 manage.py shell -c "... Client().get('/') ..."` 返回 `STATUS=200`、首页标识和下载区标识均存在;`py -3.12 manage.py findstatic portal/brand.css portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` 均找到;`py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_dashboard_entry_for_authenticated_user apps.portal.tests.PortalAccountFlowTests.test_download_release_only_keeps_one_current_per_platform apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --noinput --verbosity 2` 通过,5 tests OK。`py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests --keepdb --noinput --verbosity 2` 已重跑,因远程 MySQL `43.128.3.240` 在测试库迁移阶段连接超时(2003 / WinError 10060)中断,不是断言失败;数据库恢复后需补跑 portal 整组。 - 最新验证:T-605 免邮箱验证策略已验证 `py -3.12 -m py_compile config\settings.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;更新后的注册 / 登录目标测试 3 tests OK;`py -3.12 manage.py test apps.portal --keepdb --noinput --verbosity 2` 通过,26 tests OK;`git diff --check` 通过,仅 Windows CRLF 提示。全量 `py -3.12 manage.py test --keepdb --noinput --verbosity 2` 已尝试,发现 131 tests,跑到 95 tests 后远程 MySQL `43.128.3.240` 出现连接超时 / WinError 10051,最终 22 errors;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示 `TcpTestSucceeded=True`,单独复跑失败起点的 billing 分组时 17 条业务用例 OK,并发扣点用例本体打印 `ok` 后在测试库 flush 阶段再次因 MySQL 连接超时 / WinError 10060 记 ERROR。结论:T-605 相关测试已通过,全量单次绿受远程 MySQL 稳定性阻塞。 - 最新验证:T-604 中文敏感词本地过滤已验证 `py -3.12 -m py_compile config\settings.py apps\api\generation.py apps\api\tests.py apps\moderation\apps.py apps\moderation\models.py apps\moderation\normalization.py apps\moderation\policy.py apps\moderation\services.py apps\moderation\signals.py apps\moderation\versioning.py apps\moderation\providers\base.py apps\moderation\providers\keyword.py apps\moderation\tests.py` 通过;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py test apps.moderation --keepdb --noinput --verbosity 2` 通过,7 tests OK;新增生成接口目标测试 2 tests OK;`py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 2` 通过,16 tests OK。测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 - 最新验证:T-603 django-admin 字段级中文化已收尾:人工确认 `/admin/` 字段标签中文化;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py test apps.users.tests apps.billing.tests.BillingAdminTests apps.ai.tests.AiModelAdminTests apps.ai.tests.AiConfigAuditAdminTests --keepdb --noinput --verbosity 2` 通过,15 tests OK。测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 @@ -42,7 +43,7 @@ | `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令,并校验解释器版本 `>=3.12,<3.14` | | `requirements.txt` / `requirements-production.txt` / `pyproject.toml` | 已有 | `requirements.txt` 管本地/基础运行依赖;`requirements-production.txt` 追加 Linux 生产 Gunicorn;`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/moderation) | 已有 | 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-401 已在 `apps/billing/services.py` 落 `adjust_wallet_points()`,在 `apps/users/admin.py` 与 `apps/users/templates/admin/users/userwallet/adjust_points.html` 落钱包手工调点入口;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;T-502 已在 portal 落 `/apikeys`、API Key 创建表单、列表模板与删除(吊销)动作;T-503 已在 portal 落个人中心汇总、充值记录和消费记录页;T-504 已在 portal 落 `/recharge` 充值页、充值表单、二维码票据展示和状态轮询;T-505 已在 portal 落本地 vendor static 与记录分页;T-601 已在 `apps/ai/catalog.py` 落公开模型目录,在 api 落 `/api/v1/models`,在 portal 落 `/models` 页面和导航入口;T-604 已在 moderation 落 `SensitiveWord`、keyword matcher、归一化、共享 cache 版本失效与 admin | +| `apps/`(users/portal/billing/ai/api/moderation) | 已有 | 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-401 已在 `apps/billing/services.py` 落 `adjust_wallet_points()`,在 `apps/users/admin.py` 与 `apps/users/templates/admin/users/userwallet/adjust_points.html` 落钱包手工调点入口;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;T-502 已在 portal 落 `/apikeys`、API Key 创建表单、列表模板与删除(吊销)动作;T-503 已在 portal 落个人中心汇总、充值记录和消费记录页;T-504 已在 portal 落 `/recharge` 充值页、充值表单、二维码票据展示和状态轮询;T-505 已在 portal 落本地 vendor static 与记录分页;T-601 已在 `apps/ai/catalog.py` 落公开模型目录,在 api 落 `/api/v1/models`,在 portal 落 `/models` 页面和导航入口;T-604 已在 moderation 落 `SensitiveWord`、keyword matcher、归一化、共享 cache 版本失效与 admin;T-606 已在 portal 落公开首页、`DownloadRelease`、下载版本 admin、`brand.css` 与首页模板 | | `manage.py` | 已有 | T-001 创建 | | `tests/` | 待建 | 随各任务补充 | @@ -50,11 +51,11 @@ 任务状态以 [`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-501 注册 / 登录(allauth);T-502 API Key 自助管理页;T-503 个人中心 / 记录页;T-504 充值页(扫码 + 轮询到账);T-505 Phase 4 用户端审核优化;T-401 运营后台完善;T-402 完整验收 MVP;T-403 部署 / 运行文档;T-601 可用别名发现;T-602 django-admin 中文化(第 1-3 层);T-603 django-admin 中文化(第 4 层·字段级);T-604 中文敏感词本地过滤;T-605 免邮箱验证策略落地。 +- 已完成: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);T-502 API Key 自助管理页;T-503 个人中心 / 记录页;T-504 充值页(扫码 + 轮询到账);T-505 Phase 4 用户端审核优化;T-401 运营后台完善;T-402 完整验收 MVP;T-403 部署 / 运行文档;T-601 可用别名发现;T-602 django-admin 中文化(第 1-3 层);T-603 django-admin 中文化(第 4 层·字段级);T-604 中文敏感词本地过滤;T-605 免邮箱验证策略落地;T-606 公开首页 + 客户端下载入口。 - 正在进行:无。 -- 待开始:T-606 公开首页 + 客户端下载入口;之后可继续处理真实支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。 +- 待开始:看板当前无新的编号任务;之后可继续处理真实支付回调到账闭环、配置并发布客户端下载包、真实图片耗时验证,或从 Backlog 拆新任务。 - 当前 blocker:支付商户真实密钥/证书与生产 SDK 依赖仍待提供;微信回调到账闭环仍需真实支付验收;真实 AI 标题生成已在线上跑通,图片生成真实耗时仍需补测。 -- 下一个可领取任务:T-606 公开首页 + 客户端下载入口。 +- 下一个可领取任务:看板当前无新的编号任务,需先确认下一项业务优先级或新增任务。 ## 当前可运行内容 @@ -99,6 +100,7 @@ gunicorn config.wsgi:application --bind 127.0.0.1:8002 --workers 1 --worker-clas 当前用户端页面: +- `GET /` - `GET/POST /signup` - `GET/POST /login` - `GET/POST /logout` @@ -110,15 +112,15 @@ gunicorn config.wsgi:application --bind 127.0.0.1:8002 --workers 1 --worker-clas - `GET /records/recharge` - `GET /records/usage` -当前骨架可运行。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 ` 鉴权:成功后 `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`,以及不含邮箱的 `account.username` / `account.display_name`,测试覆盖余额与流水累加一致。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/T-605 已落地 allauth 注册 / 登录:`ACCOUNT_EMAIL_VERIFICATION="none"`,免邮箱验证、注册即可用,邮箱仍必填且唯一;注册成功创建 0 点钱包、不写点数流水。T-502 已落地 API Key 自助管理:`/apikeys` 登录访问,生成后完整明文只显示一次,列表只显示 prefix,不显示 hash 或历史明文;删除为吊销 `revoked`,吊销后外部 API 返回 403。T-503 已落地个人中心与记录页:`/dashboard` 展示剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 展示当前用户充值订单;`/records/usage` 展示当前用户 consume/refund 点数流水并关联调用信息;所有页面均只读且只查本人。T-504 已落地充值页:`/recharge` GET 展示余额、充值表单、当前订单和最近充值,POST 创建 pending 订单并展示二维码票据,浏览器轮询 `/api/v1/recharge/status`,paid 后刷新页面重新读取余额;页面不直接写钱包或流水。T-401 已落地运营后台完善:用户列表显示钱包余额,钱包余额只读且通过专用表单手工调点,调点必须填原因、非 0、不得扣成负数,并经 `adjust_wallet_points()` 锁钱包写 `PointsLedger(adjust)`;API Key admin 只展示 prefix 和 hash 摘要,不回显明文或完整 hash;订单、流水、调用记录继续只读并增强检索。T-402 已完成 MVP P0 验收并新增 `docs/mvp-acceptance.md`。T-403 已完成部署 / 运行文档,生产按 `deployment.md` 执行,并已补 settings 对生产静态目录、共享 cache、CSRF trusted origins、HTTPS cookie/proxy/HSTS 的环境变量支持。T-601 已落地可用别名发现:`GET /api/v1/models` 用 API Key 鉴权返回公开别名目录,`/models` 用 session 展示只读「可用模型」页;两者均不解密 provider key,不输出底层 SKU、URL、key 或 `extra_body`。T-603 已完成 django-admin 字段级中文化并人工确认字段标签中文。T-604 已落地本地 prompt 敏感词过滤:`MODERATION_ENABLED=false` 默认 no-op,启用 `keyword` 后命中返回 `content_blocked`,并在下载 `image_url`、解析别名、计费、预扣点和上游调用前拦截。真实标题上游生成已在线上跑通并验证扣点;图片生成真实耗时仍需补测。 +当前骨架可运行。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 ` 鉴权:成功后 `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`,以及不含邮箱的 `account.username` / `account.display_name`,测试覆盖余额与流水累加一致。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/T-605 已落地 allauth 注册 / 登录:`ACCOUNT_EMAIL_VERIFICATION="none"`,免邮箱验证、注册即可用,邮箱仍必填且唯一;注册成功创建 0 点钱包、不写点数流水。T-502 已落地 API Key 自助管理:`/apikeys` 登录访问,生成后完整明文只显示一次,列表只显示 prefix,不显示 hash 或历史明文;删除为吊销 `revoked`,吊销后外部 API 返回 403。T-503 已落地个人中心与记录页:`/dashboard` 展示剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 展示当前用户充值订单;`/records/usage` 展示当前用户 consume/refund 点数流水并关联调用信息;所有页面均只读且只查本人。T-504 已落地充值页:`/recharge` GET 展示余额、充值表单、当前订单和最近充值,POST 创建 pending 订单并展示二维码票据,浏览器轮询 `/api/v1/recharge/status`,paid 后刷新页面重新读取余额;页面不直接写钱包或流水。T-401 已落地运营后台完善:用户列表显示钱包余额,钱包余额只读且通过专用表单手工调点,调点必须填原因、非 0、不得扣成负数,并经 `adjust_wallet_points()` 锁钱包写 `PointsLedger(adjust)`;API Key admin 只展示 prefix 和 hash 摘要,不回显明文或完整 hash;订单、流水、调用记录继续只读并增强检索。T-402 已完成 MVP P0 验收并新增 `docs/mvp-acceptance.md`。T-403 已完成部署 / 运行文档,生产按 `deployment.md` 执行,并已补 settings 对生产静态目录、共享 cache、CSRF trusted origins、HTTPS cookie/proxy/HSTS 的环境变量支持。T-601 已落地可用别名发现:`GET /api/v1/models` 用 API Key 鉴权返回公开别名目录,`/models` 用 session 展示只读「可用模型」页;两者均不解密 provider key,不输出底层 SKU、URL、key 或 `extra_body`。T-603 已完成 django-admin 字段级中文化并人工确认字段标签中文。T-604 已落地本地 prompt 敏感词过滤:`MODERATION_ENABLED=false` 默认 no-op,启用 `keyword` 后命中返回 `content_blocked`,并在下载 `image_url`、解析别名、计费、预扣点和上游调用前拦截。T-606 已落地公开首页:`GET /` 匿名 200、不跳登录;下载区读取 Windows 当前 `DownloadRelease`,优先 `external_url`,无当前版本显示「暂未发布」;现有 portal 页面已共用 `brand.css` 品牌 token。真实标题上游生成已在线上跑通并验证扣点;图片生成真实耗时仍需补测。 ## 开始编码前检查 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-606 为 `TODO`。 -5. 当前下一个任务是 T-606:公开首页 + 客户端下载入口。 +4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务;T-606 已完成,看板当前无新的编号任务。 +5. 下一步需先确认业务优先级,或把真实支付回调到账闭环、客户端下载包发布、真实图片耗时验证拆成新任务。 ## 维护规则 diff --git a/docs/project-brief.md b/docs/project-brief.md index 2e538bc..e5d805e 100644 --- a/docs/project-brief.md +++ b/docs/project-brief.md @@ -108,8 +108,8 @@ - Phase 5 已完成 T-401:运营后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录;手工调点必须填写原因,并经计费层锁钱包、写 `adjust` 流水。 - Phase 5 已完成 T-402:MVP P0 验收通过,注册/充值/API Key/调用/余额/记录/后台/别名映射均有测试证据,详见 `mvp-acceptance.md`。 - Phase 5 已完成 T-403:已补部署 / 运行文档,明确宝塔/Nginx/Gunicorn、生产静态与媒体文件、共享缓存限流、图片同步超时、真实商户配置和上线检查;当前免邮箱验证,邮件服务仅作为后续密码找回/通知等邮件能力配置项,详见 `deployment.md`。 -- Phase 6 已完成 T-601 可用别名发现、T-602 后台表名/分组中文化、T-603 字段级中文化、T-604 中文敏感词本地过滤与 T-605 免邮箱验证策略落地;线上真实标题生成已恢复并验证扣点。 -- 下一步是 T-606 公开首页 + 客户端下载入口,并继续补跑真实支付回调到账闭环和真实图片耗时验证。 +- Phase 6 已完成 T-601 可用别名发现、T-602 后台表名/分组中文化、T-603 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地与 T-606 公开首页 + 客户端下载入口;线上真实标题生成已恢复并验证扣点。 +- 下一步可继续补跑真实支付回调到账闭环、配置并发布客户端下载包和真实图片耗时验证。 --- *更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。* diff --git a/docs/routes.md b/docs/routes.md index e3ae7d8..21c954f 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -7,6 +7,7 @@ | 路由 | 方法 | 职责 | 鉴权 | | --- | --- | --- | --- | +| `/` | GET | 公开首页:项目介绍、四步上手、客户端下载入口、API/控制台入口 | 公开 | | `/signup` `/login` `/logout` | GET/POST | 自助注册(**免邮箱验证、注册即可用**)/登录/登出(Django auth/allauth) | 公开 | | `/dashboard` | GET | 个人中心:剩余点数、充值总额、快捷入口 | session | | `/recharge` | GET/POST | 发起充值:选金额→展示支付二维码→轮询到账 | session | @@ -17,6 +18,7 @@ T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`。T-502 已落地 `/apikeys`:登录用户只能管理自己的 Key,生成后明文只显示一次,列表只显示 prefix,删除为吊销 `revoked`。T-503/T-505 已扩展 `/dashboard` 为个人中心汇总,并落地 `/records/recharge` 与 `/records/usage`:充值总额按已支付订单统计,入账 / 消费 / 退款点数按 `PointsLedger` 统计,记录页只查询当前登录用户数据并分页展示。T-504/T-505 已落地 `/recharge`:登录用户可选择金额和支付方式创建 pending 充值订单,页面用本地 static 自托管 qrcode.js 展示二维码票据并轮询 `/api/v1/recharge/status`,到账后刷新余额。 T-601 已落地 `/models`:登录用户可查看当前公开可调用别名、能力、是否需要原图和点数单价;页面不展示底层 SKU、模型 URL、provider key、`api_key_encrypted` 或 `extra_body`。 +T-606 已落地 `/` 公开首页:匿名访问返回 200,不再重定向到 `/dashboard`;匿名用户看到注册 / 登录 / 下载入口,登录用户看到「进入控制台」。首页下载区读取 `DownloadRelease(platform=windows, is_current=True)`,优先使用 `external_url`,否则使用后台上传文件的 `file.url`;无当前版本时显示「暂未发布」。 ## API 路由(对外,DRF) @@ -48,6 +50,7 @@ T-601 已落地 `/models`:登录用户可查看当前公开可调用别名、 | 模型配置 | AiModel | 维护上游模型(url/model/api_key[加密脱敏]/api_type/capabilities/timeout) | | 能力别名 | ModelAlias | 维护对外别名 → 具体模型的映射;换供应商在此改指向 | | 配置审计 | AiConfigAuditLog | 只读查看 AiModel / ModelAlias / 密钥变更:谁、何时、改了什么 | +| 客户端下载版本 | DownloadRelease | 上传安装包或填写外部下载地址,标记每个平台当前版本;展示版本、SHA256 与发布说明 | ## 后台职责约定 diff --git a/progress.md b/progress.md index 296364e..3492d0b 100644 --- a/progress.md +++ b/progress.md @@ -1371,3 +1371,32 @@ - `py -3.12 manage.py test apps.users.tests apps.billing.tests.BillingAdminTests apps.ai.tests.AiModelAdminTests apps.ai.tests.AiConfigAuditAdminTests --keepdb --noinput --verbosity 2`:通过,15 tests OK。 - 测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告;本项目邮箱唯一性由 `user.email` 承担。 - 下一步:按任务看板进入 T-606 公开首页 + 客户端下载入口;生产侧另需补微信真实回调到账闭环与图片生成真实耗时验证。 + +## 2026-07-06 T-606 公开首页 + 客户端下载入口 + +- 状态:DONE。 +- 代码变更: + - `apps/portal/models.py` / `apps/portal/migrations/0001_initial.py`:新增 `DownloadRelease`,字段包含 `platform`、`version`、`file`、`external_url`、`sha256`、`is_current`、`release_notes` 与时间戳;表名 `download_release`;`external_url` 优先于 `file.url`;保存时在事务内把同平台其他 current 置为 false,避免使用 MySQL 不支持的条件唯一约束。 + - `apps/portal/admin.py`:注册客户端下载版本 admin,支持上传本地安装包或填写外部链接,列表展示平台、版本、当前状态、下载来源和 SHA256 摘要。 + - `apps/portal/views.py` / `urls.py`:`/` 从重定向 `/dashboard` 改为 `HomeView` 公开首页;首页读取 Windows 当前发布版本。 + - `apps/portal/templates/portal/home.html`:按 v1「生成台」方向落地首页,包含 Hero、四步上手、标题/主图能力卡、点数计费 band、客户端下载区和控制台/API 入口;无 current release 时显示「暂未发布」。 + - `apps/portal/static/portal/brand.css` / `base.html`:抽共享品牌 token,首页与现有 dashboard / 记录 / 充值 / API Key / 可用模型页面共用同一套颜色、字体、按钮和面板样式;保留现有导航按钮 HTML,避免破坏 active 状态断言。 + - `apps/portal/tests.py`:新增 `/` 匿名 200、不跳登录、下载当前版本展示、无当前版本提示、登录用户显示「进入控制台」、每平台只保留一个 current release 等测试。 +- 文档变更: + - `docs/06-tasks.md`:T-606 标为 DONE。 + - `docs/routes.md`:新增 `/` 公开首页和 `DownloadRelease` admin 职责。 + - `docs/04-architecture.md` / `docs/api.md`:同步 `DownloadRelease` schema、首页/下载入口行为和生产媒体文件由 Nginx 直接服务口径。 + - `docs/current-state.md`、`docs/00-ai-start-here.md`、`docs/project-brief.md`、`README.md`:同步 Phase 6 当前状态,移除“下一步 T-606”旧口径。 +- 验证: + - `.\init.ps1`:开工前通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。 + - `py -3.12 manage.py makemigrations portal`:生成 `apps/portal/migrations/0001_initial.py`。 + - `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py`:通过。 + - `py -3.12 manage.py check`:通过,0 issues。 + - `py -3.12 manage.py makemigrations --check --dry-run`:通过,No changes detected。 + - `py -3.12 manage.py migrate portal`:通过,`portal.0001_initial` 已应用到当前数据库;仅保留 allauth 既有 `models.W036` 警告。 + - `py -3.12 manage.py shell -c "... Client().get('/') ..."`:通过,返回 `STATUS=200`,首页标识 `cmhub AI 电商生成台` 与下载区标识均存在。 + - `py -3.12 manage.py findstatic portal/brand.css portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1`:通过,均找到本地 static 文件。 + - `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_dashboard_entry_for_authenticated_user apps.portal.tests.PortalAccountFlowTests.test_download_release_only_keeps_one_current_per_platform apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --noinput --verbosity 2`:重建测试库后通过,5 tests OK;测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 + - `.\init.ps1`:收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。 + - `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests --keepdb --noinput --verbosity 2`:已尝试,因远程 MySQL `43.128.3.240` 在测试库迁移阶段连接超时(2003 / WinError 10060)中断,不是断言失败;数据库恢复后需补跑 portal 整组。 +- 下一步:看板当前无新的编号任务;建议优先确认真实支付回调到账闭环、上传/发布 Windows 客户端安装包,或补真实图片生成耗时验证并拆成新任务。