feat: add public homepage and downloads
This commit is contained in:
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
## 它做什么
|
## 它做什么
|
||||||
|
|
||||||
- 用户端(自助):注册/登录、扫码充值、查看充值记录/剩余点数/消费记录、生成与删除 API Key(注册不送免费点数)。
|
- 用户端(自助):公开首页、注册/登录、扫码充值、查看充值记录/剩余点数/消费记录、查看可用模型、生成与删除 API Key、下载桌面端(注册不送免费点数)。
|
||||||
- 对外提供 HTTP API:生成标题、生成图片(同步返回)、查询点数余额。
|
- 对外提供 HTTP API:生成标题、生成图片(同步返回)、查询点数余额、查询可用能力别名。
|
||||||
- 按「操作类型 + 能力别名(+ 可选分辨率)」计费,调用消耗不同点数;余额不足返回「点数不足,请先充值」。
|
- 按「操作类型 + 能力别名(+ 可选分辨率)」计费,调用消耗不同点数;余额不足返回「点数不足,请先充值」。
|
||||||
- **预付费点数模型**:用户在外部支付系统充值,付款成功由支付系统回调本服务,按汇率把金额转成点数存在本地;之后调用直接扣本地点数,扣费与上游解耦、低延迟。
|
- **预付费点数模型**:用户在外部支付系统充值,付款成功由支付系统回调本服务,按汇率把金额转成点数存在本地;之后调用直接扣本地点数,扣费与上游解耦、低延迟。
|
||||||
- django-admin 运营后台:管理注册用户、点数余额、计费规则、充值订单、点数流水、调用记录。
|
- 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) 第四节计费时序。
|
> ⚠️ 涉及资金/点数。改动充值、扣费、退款、对账相关代码前,先读 [`docs/05-coding-rules.md`](docs/05-coding-rules.md) 第 8 节与 [`docs/04-architecture.md`](docs/04-architecture.md) 第四节计费时序。
|
||||||
|
|
||||||
|
|||||||
+62
-1
@@ -1,3 +1,64 @@
|
|||||||
from django.contrib import admin
|
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]}..."
|
||||||
|
|||||||
@@ -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')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
+70
-2
@@ -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)
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,78 +6,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}cmhub{% endblock %}</title>
|
<title>{% block title %}cmhub{% endblock %}</title>
|
||||||
<link href="{% static 'portal/vendor/bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
<link href="{% static 'portal/vendor/bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||||
<style>
|
<link href="{% static 'portal/brand.css' %}" rel="stylesheet">
|
||||||
: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;
|
|
||||||
}
|
|
||||||
.cmhub-surface {
|
|
||||||
border: 1px solid var(--cmhub-line);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #ffffff;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.key-value {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="{% block body_class %}{% endblock %}">
|
||||||
<nav class="navbar navbar-expand">
|
<nav class="navbar navbar-expand">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand fw-semibold" href="{% url 'portal-home' %}">cmhub</a>
|
<a class="navbar-brand fw-semibold" href="{% url 'portal-home' %}">cmhub</a>
|
||||||
@@ -102,6 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% block body %}
|
||||||
<main class="cmhub-shell">
|
<main class="cmhub-shell">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -112,5 +44,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
{% extends "portal/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}cmhub AI 电商生成台{% endblock %}
|
||||||
|
{% block body_class %}home-page{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<main class="home-main">
|
||||||
|
<section class="home-hero">
|
||||||
|
<div class="home-container home-hero-grid">
|
||||||
|
<div>
|
||||||
|
<div class="home-eyebrow">AI 生成 / 点数计费 / 桌面端接入</div>
|
||||||
|
<h1>cmhub AI 电商生成台</h1>
|
||||||
|
<p class="home-lead">
|
||||||
|
把生成标题、生成主图能力封装成稳定的 HTTP API。桌面端填入 API Key 后按本地点数扣费,
|
||||||
|
余额不足直接拦截,失败自动退点。
|
||||||
|
</p>
|
||||||
|
<div class="home-actions">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a class="btn btn-primary btn-lg" href="{% url 'portal-dashboard' %}">进入控制台</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-primary btn-lg" href="{% url 'portal-signup' %}">免费注册</a>
|
||||||
|
<a class="btn btn-outline-secondary btn-lg" href="{% url 'portal-login' %}">登录</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_release and current_release.has_download %}
|
||||||
|
<a class="btn btn-outline-secondary btn-lg" href="{{ current_release.download_url }}">下载客户端</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-outline-secondary btn-lg disabled" aria-disabled="true" href="#download">客户端下载暂未发布</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="home-proof" aria-label="服务要点">
|
||||||
|
<span class="home-chip">注册即可用</span>
|
||||||
|
<span class="home-chip">按点数扣点</span>
|
||||||
|
<span class="home-chip">API Key 接入</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-stage" aria-label="生成台示意">
|
||||||
|
<div class="home-product-shot">
|
||||||
|
<div class="home-product-frame">
|
||||||
|
<div class="home-product-image">商品图</div>
|
||||||
|
<div class="home-product-copy">
|
||||||
|
轻量夹克,春秋外套,通勤穿搭,图片清晰,适合电商标题与主图优化。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="home-thread" aria-hidden="true"></div>
|
||||||
|
<div class="home-output-grid">
|
||||||
|
<div class="home-output">
|
||||||
|
<div class="home-output-title">吸睛标题</div>
|
||||||
|
<div class="home-output-text">春秋通勤轻量夹克,防风舒适,男女日常外套</div>
|
||||||
|
<span class="home-point-pill">-2 点</span>
|
||||||
|
</div>
|
||||||
|
<div class="home-output">
|
||||||
|
<div class="home-output-title">生成主图</div>
|
||||||
|
<div class="home-output-text">保留商品主体,生成干净背景和电商展示构图。</div>
|
||||||
|
<span class="home-point-pill">-12 点</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section">
|
||||||
|
<div class="home-container">
|
||||||
|
<div class="home-section-heading">
|
||||||
|
<h2 class="home-section-title">四步接入桌面端</h2>
|
||||||
|
<p class="home-section-copy">从账号到调用链路保持简单:账号、点数、API Key 和客户端配置各司其职。</p>
|
||||||
|
</div>
|
||||||
|
<div class="home-steps">
|
||||||
|
<div class="home-step">
|
||||||
|
<div class="home-step-num">01</div>
|
||||||
|
<h3>注册账号</h3>
|
||||||
|
<p>邮箱注册后直接登录,系统创建 0 点钱包,不赠点、不写虚假流水。</p>
|
||||||
|
</div>
|
||||||
|
<div class="home-step">
|
||||||
|
<div class="home-step-num">02</div>
|
||||||
|
<h3>充值点数</h3>
|
||||||
|
<p>扫码创建充值订单,到账后按汇率入账到本地点数账本。</p>
|
||||||
|
</div>
|
||||||
|
<div class="home-step">
|
||||||
|
<div class="home-step-num">03</div>
|
||||||
|
<h3>生成 API Key</h3>
|
||||||
|
<p>在控制台生成 Key,明文只显示一次,后续列表只展示 prefix。</p>
|
||||||
|
</div>
|
||||||
|
<div class="home-step">
|
||||||
|
<div class="home-step-num">04</div>
|
||||||
|
<h3>填入桌面端</h3>
|
||||||
|
<p>客户端使用能力别名调用标题或图片接口,按规则扣点。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section" id="api">
|
||||||
|
<div class="home-container">
|
||||||
|
<div class="home-section-heading">
|
||||||
|
<h2 class="home-section-title">面向电商流程的两类能力</h2>
|
||||||
|
<p class="home-section-copy">对外只暴露能力别名,不暴露底层供应商 SKU、URL、密钥或 extra_body。</p>
|
||||||
|
</div>
|
||||||
|
<div class="home-cards">
|
||||||
|
<article class="home-card">
|
||||||
|
<div class="home-card-icon">文</div>
|
||||||
|
<h3>生成标题</h3>
|
||||||
|
<p>按商品信息、提示词和能力别名生成标题。敏感词命中会在扣点和上游调用前拦截。</p>
|
||||||
|
</article>
|
||||||
|
<article class="home-card">
|
||||||
|
<div class="home-card-icon">图</div>
|
||||||
|
<h3>生成主图(改图)</h3>
|
||||||
|
<p>支持需要原图的图片能力,图片输入先做公网地址和大小校验,结果保存后返回 URL。</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-band">
|
||||||
|
<div class="home-container home-band-grid">
|
||||||
|
<div>
|
||||||
|
<h2 class="home-band-title">点数余额是唯一权威账本</h2>
|
||||||
|
<p>
|
||||||
|
充值成功后写入本地点数;每次生成先预扣,成功确认,失败退点。调用链路不实时查询支付系统。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="home-band-meter">
|
||||||
|
<strong>本地扣点</strong>
|
||||||
|
<p>余额不足返回“点数不足,请先充值”,不会继续调用上游模型。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section" id="download">
|
||||||
|
<div class="home-container">
|
||||||
|
<div class="home-section-heading">
|
||||||
|
<h2 class="home-section-title">下载桌面端</h2>
|
||||||
|
<p class="home-section-copy">下载走 HTTPS,页面展示 SHA256 供校验;安装包前期由服务器 media 目录托管,生产 Nginx 直接服务。</p>
|
||||||
|
</div>
|
||||||
|
<div class="home-download-grid">
|
||||||
|
<div class="home-download-box">
|
||||||
|
{% if current_release and current_release.has_download %}
|
||||||
|
<h3>Windows 客户端 {{ current_release.version }}</h3>
|
||||||
|
<p>当前版本已发布,可下载后填入 cmhub API Key 使用。</p>
|
||||||
|
<div class="home-actions">
|
||||||
|
<a class="btn btn-primary" href="{{ current_release.download_url }}">下载客户端</a>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a class="btn btn-outline-secondary" href="{% url 'portal-apikeys' %}">管理 API Key</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-outline-secondary" href="{% url 'portal-signup' %}">注册账号</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="home-release-meta">
|
||||||
|
<div class="home-release-row">
|
||||||
|
<span>版本</span>
|
||||||
|
<strong>{{ current_release.version }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="home-release-row">
|
||||||
|
<span>SHA256</span>
|
||||||
|
<code>{{ current_release.sha256|default:"未提供" }}</code>
|
||||||
|
</div>
|
||||||
|
{% if current_release.release_notes %}
|
||||||
|
<div class="home-release-row">
|
||||||
|
<span>说明</span>
|
||||||
|
<strong>{{ current_release.release_notes|linebreaksbr }}</strong>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="home-notice">
|
||||||
|
首期安装包可能未签名。Windows 若提示未知发布者,请先核对 SHA256,再选择“仍要运行”;正式代码签名后会更新版本说明。
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h3>客户端暂未发布</h3>
|
||||||
|
<p>后台尚未配置当前 Windows 版本。发布后这里会展示版本号、下载按钮、SHA256 和版本说明。</p>
|
||||||
|
<div class="home-actions">
|
||||||
|
<a class="btn btn-primary disabled" aria-disabled="true" href="#">暂未发布</a>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a class="btn btn-outline-secondary" href="{% url 'portal-dashboard' %}">进入控制台</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-outline-secondary" href="{% url 'portal-signup' %}">先注册账号</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="home-download-box">
|
||||||
|
<h3>接口与控制台</h3>
|
||||||
|
<div class="home-links">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a class="home-link-row" href="{% url 'portal-dashboard' %}"><span>控制台</span><span>余额与记录</span></a>
|
||||||
|
<a class="home-link-row" href="{% url 'portal-models' %}"><span>可用模型</span><span>能力别名与价格</span></a>
|
||||||
|
<a class="home-link-row" href="{% url 'portal-apikeys' %}"><span>API Key</span><span>创建与吊销</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a class="home-link-row" href="{% url 'portal-signup' %}"><span>注册</span><span>创建账号</span></a>
|
||||||
|
<a class="home-link-row" href="{% url 'portal-login' %}"><span>登录</span><span>进入控制台</span></a>
|
||||||
|
<a class="home-link-row" href="#api"><span>API 能力</span><span>标题 / 图片</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@@ -18,6 +18,7 @@ from apps.billing.models import (
|
|||||||
PricingRule,
|
PricingRule,
|
||||||
RechargeOrder,
|
RechargeOrder,
|
||||||
)
|
)
|
||||||
|
from apps.portal.models import DownloadRelease
|
||||||
from apps.users.models import ApiKey, UserWallet
|
from apps.users.models import ApiKey, UserWallet
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +111,25 @@ class PortalAccountFlowTests(TestCase):
|
|||||||
ai_model=ai_model,
|
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):
|
def assert_nav_link_active(self, response, *, href: str, label: str):
|
||||||
html = response.content.decode("utf-8")
|
html = response.content.decode("utf-8")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
@@ -211,6 +231,56 @@ class PortalAccountFlowTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
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):
|
def test_apikeys_requires_session_login(self):
|
||||||
response = self.client.get("/apikeys")
|
response = self.client.get("/apikeys")
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
from allauth.account.views import LoginView, LogoutView, SignupView
|
from allauth.account.views import LoginView, LogoutView, SignupView
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import RedirectView
|
|
||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
ApiKeyDeleteView,
|
ApiKeyDeleteView,
|
||||||
ApiKeyListCreateView,
|
ApiKeyListCreateView,
|
||||||
DashboardView,
|
DashboardView,
|
||||||
|
HomeView,
|
||||||
ModelCatalogView,
|
ModelCatalogView,
|
||||||
RechargePageView,
|
RechargePageView,
|
||||||
RechargeRecordListView,
|
RechargeRecordListView,
|
||||||
@@ -13,7 +13,7 @@ from .views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
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("signup", SignupView.as_view(), name="portal-signup"),
|
||||||
path("login", LoginView.as_view(), name="portal-login"),
|
path("login", LoginView.as_view(), name="portal-login"),
|
||||||
path("logout", LogoutView.as_view(), name="portal-logout"),
|
path("logout", LogoutView.as_view(), name="portal-logout"),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from apps.billing.services import (
|
|||||||
from apps.users.models import ApiKey
|
from apps.users.models import ApiKey
|
||||||
|
|
||||||
from .forms import ApiKeyCreateForm, RechargeCreateForm
|
from .forms import ApiKeyCreateForm, RechargeCreateForm
|
||||||
|
from .models import DownloadRelease
|
||||||
|
|
||||||
|
|
||||||
NEW_API_KEY_SESSION_KEY = "portal_new_api_key"
|
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):
|
class DashboardView(LoginRequiredMixin, TemplateView):
|
||||||
template_name = "portal/dashboard.html"
|
template_name = "portal/dashboard.html"
|
||||||
|
|
||||||
|
|||||||
@@ -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 安全加固已完成。
|
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 用户端审核优化已完成。
|
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 任务已收尾。
|
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 已完成公开首页 + 客户端下载入口。下一步需先确认业务优先级或拆新任务。
|
||||||
|
|
||||||
## 领取任务规则
|
## 领取任务规则
|
||||||
|
|
||||||
|
|||||||
+18
-3
@@ -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/`。
|
- **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。
|
||||||
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
|
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
|
||||||
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
||||||
@@ -97,6 +97,7 @@ T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前
|
|||||||
| PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel |
|
| PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel |
|
||||||
| ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),按 `currency + effective_from` 取当前有效值 |
|
| ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),按 `currency + effective_from` 取当前有效值 |
|
||||||
| SensitiveWord | 运营配置 | 本地敏感词快筛词库;word、normalized_word、category、action、is_active;T-604 MVP 仅支持 action=block |
|
| 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,
|
created_at DATETIME(6) NOT NULL,
|
||||||
updated_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/故障转移)。适配器接口需为此留口子。
|
> 可选增强(接口预留、MVP 不实现):`account_alias_permission`(按账号授权可用别名,防止调用方点用未授权/昂贵模型);别名按比例分流到多个模型(灰度/AB/故障转移)。适配器接口需为此留口子。
|
||||||
|
|
||||||
@@ -391,7 +406,7 @@ cmhub/
|
|||||||
├── config/ # Django 工程:settings / urls / wsgi
|
├── config/ # Django 工程:settings / urls / wsgi
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── users/ # 注册用户 User、UserWallet 钱包、ApiKey(模型 + admin)
|
│ ├── users/ # 注册用户 User、UserWallet 钱包、ApiKey(模型 + admin)
|
||||||
│ ├── portal/ # 用户端页面(Django 模板 SSR):注册/登录/个人中心/充值/API Key 管理
|
│ ├── portal/ # 用户端页面(Django 模板 SSR):公开首页/注册/登录/个人中心/充值/API Key/下载版本
|
||||||
│ ├── billing/ # 计费规则、汇率、充值订单、点数流水、扣点/入账逻辑(点数唯一写入方)
|
│ ├── billing/ # 计费规则、汇率、充值订单、点数流水、扣点/入账逻辑(点数唯一写入方)
|
||||||
│ ├── ai/ # 别名解析 + Provider 适配器层
|
│ ├── ai/ # 别名解析 + Provider 适配器层
|
||||||
│ │ └── providers/ # 各供应商适配器(chat/gemini/images_edits…),移植自 cmbot
|
│ │ └── providers/ # 各供应商适配器(chat/gemini/images_edits…),移植自 cmbot
|
||||||
|
|||||||
+1
-1
@@ -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-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-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-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 |
|
||||||
|
|
||||||
## 里程碑
|
## 里程碑
|
||||||
|
|
||||||
|
|||||||
@@ -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-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
|
```json
|
||||||
|
|||||||
+11
-9
File diff suppressed because one or more lines are too long
@@ -108,8 +108,8 @@
|
|||||||
- Phase 5 已完成 T-401:运营后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录;手工调点必须填写原因,并经计费层锁钱包、写 `adjust` 流水。
|
- Phase 5 已完成 T-401:运营后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录;手工调点必须填写原因,并经计费层锁钱包、写 `adjust` 流水。
|
||||||
- Phase 5 已完成 T-402:MVP P0 验收通过,注册/充值/API Key/调用/余额/记录/后台/别名映射均有测试证据,详见 `mvp-acceptance.md`。
|
- Phase 5 已完成 T-402:MVP P0 验收通过,注册/充值/API Key/调用/余额/记录/后台/别名映射均有测试证据,详见 `mvp-acceptance.md`。
|
||||||
- Phase 5 已完成 T-403:已补部署 / 运行文档,明确宝塔/Nginx/Gunicorn、生产静态与媒体文件、共享缓存限流、图片同步超时、真实商户配置和上线检查;当前免邮箱验证,邮件服务仅作为后续密码找回/通知等邮件能力配置项,详见 `deployment.md`。
|
- Phase 5 已完成 T-403:已补部署 / 运行文档,明确宝塔/Nginx/Gunicorn、生产静态与媒体文件、共享缓存限流、图片同步超时、真实商户配置和上线检查;当前免邮箱验证,邮件服务仅作为后续密码找回/通知等邮件能力配置项,详见 `deployment.md`。
|
||||||
- Phase 6 已完成 T-601 可用别名发现、T-602 后台表名/分组中文化、T-603 字段级中文化、T-604 中文敏感词本地过滤与 T-605 免邮箱验证策略落地;线上真实标题生成已恢复并验证扣点。
|
- Phase 6 已完成 T-601 可用别名发现、T-602 后台表名/分组中文化、T-603 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地与 T-606 公开首页 + 客户端下载入口;线上真实标题生成已恢复并验证扣点。
|
||||||
- 下一步是 T-606 公开首页 + 客户端下载入口,并继续补跑真实支付回调到账闭环和真实图片耗时验证。
|
- 下一步可继续补跑真实支付回调到账闭环、配置并发布客户端下载包和真实图片耗时验证。
|
||||||
|
|
||||||
---
|
---
|
||||||
*更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。*
|
*更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
| 路由 | 方法 | 职责 | 鉴权 |
|
| 路由 | 方法 | 职责 | 鉴权 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
| `/` | GET | 公开首页:项目介绍、四步上手、客户端下载入口、API/控制台入口 | 公开 |
|
||||||
| `/signup` `/login` `/logout` | GET/POST | 自助注册(**免邮箱验证、注册即可用**)/登录/登出(Django auth/allauth) | 公开 |
|
| `/signup` `/login` `/logout` | GET/POST | 自助注册(**免邮箱验证、注册即可用**)/登录/登出(Django auth/allauth) | 公开 |
|
||||||
| `/dashboard` | GET | 个人中心:剩余点数、充值总额、快捷入口 | session |
|
| `/dashboard` | GET | 个人中心:剩余点数、充值总额、快捷入口 | session |
|
||||||
| `/recharge` | GET/POST | 发起充值:选金额→展示支付二维码→轮询到账 | 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-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-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)
|
## API 路由(对外,DRF)
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ T-601 已落地 `/models`:登录用户可查看当前公开可调用别名、
|
|||||||
| 模型配置 | AiModel | 维护上游模型(url/model/api_key[加密脱敏]/api_type/capabilities/timeout) |
|
| 模型配置 | AiModel | 维护上游模型(url/model/api_key[加密脱敏]/api_type/capabilities/timeout) |
|
||||||
| 能力别名 | ModelAlias | 维护对外别名 → 具体模型的映射;换供应商在此改指向 |
|
| 能力别名 | ModelAlias | 维护对外别名 → 具体模型的映射;换供应商在此改指向 |
|
||||||
| 配置审计 | AiConfigAuditLog | 只读查看 AiModel / ModelAlias / 密钥变更:谁、何时、改了什么 |
|
| 配置审计 | AiConfigAuditLog | 只读查看 AiModel / ModelAlias / 密钥变更:谁、何时、改了什么 |
|
||||||
|
| 客户端下载版本 | DownloadRelease | 上传安装包或填写外部下载地址,标记每个平台当前版本;展示版本、SHA256 与发布说明 |
|
||||||
|
|
||||||
## 后台职责约定
|
## 后台职责约定
|
||||||
|
|
||||||
|
|||||||
+29
@@ -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。
|
- `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` 承担。
|
- 测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告;本项目邮箱唯一性由 `user.email` 承担。
|
||||||
- 下一步:按任务看板进入 T-606 公开首页 + 客户端下载入口;生产侧另需补微信真实回调到账闭环与图片生成真实耗时验证。
|
- 下一步:按任务看板进入 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 客户端安装包,或补真实图片生成耗时验证并拆成新任务。
|
||||||
|
|||||||
Reference in New Issue
Block a user