diff --git a/apps/portal/admin.py b/apps/portal/admin.py index 6693535..4bd23b6 100644 --- a/apps/portal/admin.py +++ b/apps/portal/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import DownloadRelease +from .models import DownloadRelease, ImportTemplate @admin.register(DownloadRelease) @@ -64,3 +64,62 @@ class DownloadReleaseAdmin(admin.ModelAdmin): if not obj.sha256: return "-" return f"{obj.sha256[:12]}..." + + +@admin.register(ImportTemplate) +class ImportTemplateAdmin(admin.ModelAdmin): + list_display = ( + "name", + "is_current", + "download_source", + "sha256_short", + "updated_at", + ) + list_filter = ("is_current",) + search_fields = ("name", "sha256", "external_url", "file", "notes") + readonly_fields = ("created_at", "updated_at") + fieldsets = ( + ( + "模板信息", + { + "fields": ( + "name", + "is_current", + "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/0003_importtemplate.py b/apps/portal/migrations/0003_importtemplate.py new file mode 100644 index 0000000..60598b3 --- /dev/null +++ b/apps/portal/migrations/0003_importtemplate.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.15 on 2026-07-08 08:15 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('portal', '0002_downloadrelease_force_update'), + ] + + operations = [ + migrations.CreateModel( + name='ImportTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='模板名称')), + ('file', models.FileField(blank=True, upload_to='import_templates/', 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='当前模板')), + ('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={ + 'verbose_name': '导入模板', + 'verbose_name_plural': '导入模板', + 'db_table': 'import_template', + 'ordering': ('-is_current', '-created_at', '-id'), + 'indexes': [models.Index(fields=['is_current'], name='it_current_idx')], + }, + ), + ] diff --git a/apps/portal/models.py b/apps/portal/models.py index 491b290..2a3944f 100644 --- a/apps/portal/models.py +++ b/apps/portal/models.py @@ -70,3 +70,64 @@ class DownloadRelease(models.Model): platform=self.platform, is_current=True, ).exclude(pk=self.pk).update(is_current=False) + + +class ImportTemplate(models.Model): + name = models.CharField("模板名称", max_length=128) + file = models.FileField("模板文件", upload_to="import_templates/", 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) + notes = models.TextField("说明", blank=True) + created_at = models.DateTimeField("创建时间", auto_now_add=True) + updated_at = models.DateTimeField("更新时间", auto_now=True) + + class Meta: + db_table = "import_template" + verbose_name = "导入模板" + verbose_name_plural = "导入模板" + ordering = ("-is_current", "-created_at", "-id") + indexes = [ + models.Index( + fields=("is_current",), + name="it_current_idx", + ), + ] + + def __str__(self): + return self.name + + @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(is_current=True).exclude(pk=self.pk).update( + is_current=False + ) diff --git a/apps/portal/templates/portal/home.html b/apps/portal/templates/portal/home.html index 6b8f43e..86c8a34 100644 --- a/apps/portal/templates/portal/home.html +++ b/apps/portal/templates/portal/home.html @@ -26,6 +26,9 @@ {% else %} 客户端下载暂未发布 {% endif %} + {% if current_import_template_download_url %} + 下载导入模板 + {% endif %}
注册送 100 点 @@ -140,6 +143,9 @@

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

下载客户端 + {% if current_import_template_download_url %} + 下载导入模板 + {% endif %} {% if user.is_authenticated %} 管理 API Key {% else %} @@ -170,6 +176,9 @@

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

暂未发布 + {% if current_import_template_download_url %} + 下载导入模板 + {% endif %} {% if user.is_authenticated %} 进入控制台 {% else %} diff --git a/apps/portal/tests.py b/apps/portal/tests.py index 81b92b2..ac73442 100644 --- a/apps/portal/tests.py +++ b/apps/portal/tests.py @@ -3,6 +3,7 @@ from decimal import Decimal from allauth.account.models import EmailAddress from django.conf import settings +from django.contrib import admin from django.contrib.auth import get_user, get_user_model from django.contrib.staticfiles import finders from django.core import mail @@ -20,7 +21,7 @@ from apps.billing.models import ( RechargeOrder, SignupBonusGrant, ) -from apps.portal.models import DownloadRelease +from apps.portal.models import DownloadRelease, ImportTemplate from apps.users.models import ApiKey, UserWallet @@ -132,6 +133,25 @@ class PortalAccountFlowTests(TestCase): release_notes=release_notes, ) + def create_import_template( + self, + *, + name: str = "导入模板", + is_current: bool = True, + external_url: str = "https://download.example.com/import-template.xlsx", + file_name: str = "", + sha256: str = "e" * 64, + notes: str = "商品导入 Excel 模板", + ) -> ImportTemplate: + return ImportTemplate.objects.create( + name=name, + is_current=is_current, + external_url=external_url, + file=file_name, + sha256=sha256, + notes=notes, + ) + def assert_nav_link_active(self, response, *, href: str, label: str): html = response.content.decode("utf-8") self.assertIn( @@ -259,6 +279,7 @@ class PortalAccountFlowTests(TestCase): self.assertContains(response, "登录") self.assertContains(response, "客户端暂未发布") self.assertContains(response, "暂未发布") + self.assertNotContains(response, "下载导入模板") self.assertNotContains(response, "/login?next=") def test_homepage_shows_current_download_release(self): @@ -279,6 +300,63 @@ class PortalAccountFlowTests(TestCase): self.assertContains(response, "修复下载入口并补充 SHA256") self.assertContains(response, "未知发布者") + def test_homepage_shows_current_import_template_download_link(self): + self.create_download_release() + import_template = self.create_import_template( + name="蝦皮圈導入模板", + external_url="https://download.example.com/shopee-import-template.xlsx", + sha256="f" * 64, + ) + + response = self.client.get("/") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["current_import_template"], import_template) + self.assertEqual( + response.context["current_import_template_download_url"], + "https://download.example.com/shopee-import-template.xlsx", + ) + self.assertContains(response, "下载客户端") + self.assertContains(response, "下载导入模板") + self.assertContains( + response, + "https://download.example.com/shopee-import-template.xlsx", + ) + + def test_homepage_builds_absolute_import_template_file_url_without_local_path(self): + self.create_import_template( + external_url="", + file_name="import_templates/cmhub-import-template.xlsx", + ) + + response = self.client.get("/", secure=True) + + expected_url = "https://testserver/media/import_templates/cmhub-import-template.xlsx" + response_body = response.content.decode("utf-8") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.context["current_import_template_download_url"], + expected_url, + ) + self.assertContains(response, expected_url) + self.assertNotIn(str(settings.MEDIA_ROOT), response_body) + self.assertNotIn("MEDIA_ROOT", response_body) + + def test_homepage_prefers_import_template_external_url_over_file(self): + self.create_import_template( + external_url="https://cdn.example.com/current-template.xlsx", + file_name="import_templates/local-template.xlsx", + ) + + response = self.client.get("/", secure=True) + + self.assertEqual( + response.context["current_import_template_download_url"], + "https://cdn.example.com/current-template.xlsx", + ) + self.assertContains(response, "https://cdn.example.com/current-template.xlsx") + self.assertNotContains(response, "/media/import_templates/local-template.xlsx") + def test_homepage_shows_dashboard_entry_for_authenticated_user(self): user = self.create_verified_user() self.client.force_login(user) @@ -299,6 +377,35 @@ class PortalAccountFlowTests(TestCase): self.assertFalse(old_release.is_current) self.assertTrue(new_release.is_current) + def test_import_template_only_keeps_one_current(self): + old_template = self.create_import_template( + name="旧模板", + sha256="1" * 64, + ) + new_template = self.create_import_template( + name="新模板", + sha256="2" * 64, + ) + + old_template.refresh_from_db() + new_template.refresh_from_db() + + self.assertFalse(old_template.is_current) + self.assertTrue(new_template.is_current) + + def test_import_template_admin_exposes_download_fields(self): + registered_admin = admin.site._registry[ImportTemplate] + + self.assertIn("is_current", registered_admin.list_display) + self.assertIn("download_source", registered_admin.list_display) + self.assertIn("is_current", registered_admin.list_filter) + template_fields = registered_admin.fieldsets[0][1]["fields"] + download_fields = registered_admin.fieldsets[1][1]["fields"] + self.assertIn("name", template_fields) + self.assertIn("file", download_fields) + self.assertIn("external_url", download_fields) + self.assertIn("sha256", download_fields) + def test_apikeys_requires_session_login(self): response = self.client.get("/apikeys") diff --git a/apps/portal/views.py b/apps/portal/views.py index 4735ceb..0eed7f8 100644 --- a/apps/portal/views.py +++ b/apps/portal/views.py @@ -19,7 +19,7 @@ from apps.billing.services import ( from apps.users.models import ApiKey from .forms import ApiKeyCreateForm, RechargeCreateForm -from .models import DownloadRelease +from .models import DownloadRelease, ImportTemplate NEW_API_KEY_SESSION_KEY = "portal_new_api_key" @@ -98,6 +98,14 @@ def paginate_records(request, queryset): } +def build_public_download_url(request, url: str) -> str: + if not url: + return "" + if url.startswith("/"): + return request.build_absolute_uri(url) + return url + + class HomeView(TemplateView): template_name = "portal/home.html" @@ -111,6 +119,20 @@ class HomeView(TemplateView): .order_by("-created_at", "-id") .first() ) + current_import_template = ( + ImportTemplate.objects.filter(is_current=True) + .order_by("-created_at", "-id") + .first() + ) + context["current_import_template"] = current_import_template + context["current_import_template_download_url"] = ( + build_public_download_url( + self.request, + current_import_template.download_url + if current_import_template is not None + else "", + ) + ) return context diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index bc907eb..f59daa3 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「公开首页 + 客户端下载入口」、T-607「桌面端最新版本检查接口」、T-608「新用户注册赠送 100 点试用点数」与 T-609「桌面端版本检查接口增加强制更新标记」;T-610「首页导入模板下载入口」已登记为下一项 TODO。生产侧仍需补真实支付回调到账闭环、配置并发布客户端下载包和图片生成真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。 +当前项目处于:**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「公开首页 + 客户端下载入口」、T-607「桌面端最新版本检查接口」、T-608「新用户注册赠送 100 点试用点数」、T-609「桌面端版本检查接口增加强制更新标记」与 T-610「首页导入模板下载入口」。生产侧仍需补真实支付回调到账闭环、配置并发布客户端下载包和图片生成真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。 优先路径: @@ -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 已完成公开首页 + 客户端下载入口;T-607 已完成桌面端最新版本检查接口;T-608 已完成新用户注册赠送 100 点试用点数;T-609 已完成桌面端版本检查接口增加强制更新标记;T-610 已拆为「首页导入模板下载入口」,下一步可领取实现。 +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 已完成公开首页 + 客户端下载入口;T-607 已完成桌面端最新版本检查接口;T-608 已完成新用户注册赠送 100 点试用点数;T-609 已完成桌面端版本检查接口增加强制更新标记;T-610 已完成首页导入模板下载入口。 ## 领取任务规则 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index ba6ca6c..c8ba823 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/T-608 已落地 `/signup`、`/login`、`/logout` 与 `/dashboard`;注册成功后经计费层一次性发放 100 点试用点数,并写 `signup_bonus` 点数流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503/T-608 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单、注册赠点与消费/退款流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/点数记录页从固定切片改为分页。T-606 已把 `/` 改为公开首页,并新增 `DownloadRelease` 下载版本配置用于展示 Windows 客户端版本、下载地址、SHA256 与发布说明;T-607/T-609 已新增公开 JSON 版本检查接口给桌面端自动更新使用,并返回强制更新标记。T-610 目标是在公开首页下载区增加导入模板下载入口,由后台 `ImportTemplate` 配置当前模板。 +- **用户端层(Django 模板 SSR)**:公开首页、注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/点数记录)、API Key 自助管理、发起扫码充值、模型目录与客户端下载入口。入口 `apps/portal/`,公开首页匿名可访问,其他自助页面用 session 鉴权。T-501/T-608 已落地 `/signup`、`/login`、`/logout` 与 `/dashboard`;注册成功后经计费层一次性发放 100 点试用点数,并写 `signup_bonus` 点数流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503/T-608 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单、注册赠点与消费/退款流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/点数记录页从固定切片改为分页。T-606 已把 `/` 改为公开首页,并新增 `DownloadRelease` 下载版本配置用于展示 Windows 客户端版本、下载地址、SHA256 与发布说明;T-607/T-609 已新增公开 JSON 版本检查接口给桌面端自动更新使用,并返回强制更新标记。T-610 已在公开首页下载区增加导入模板下载入口,由后台 `ImportTemplate` 配置当前模板。 - **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。 - **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单、公开版本检查接口。入口 `apps/api/`。生成/余额/模型目录这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签;T-607/T-609 的客户端下载版本检查接口为公开只读例外,不需要 API Key,不读取用户、不扣点,`release.force_update` 仅表示当前发布版本是否强制升级。 - **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。 @@ -217,7 +217,7 @@ CREATE TABLE import_template ( ); ``` -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/T-609 已实现 `DownloadRelease`,admin 可上传安装包或填写 `external_url`、标记当前版本、配置是否强制更新;`external_url` 优先于 `file.url`,每平台仅一个当前版本由模型 `save()` 在事务内把同平台旧 current 置为 false。T-610 计划新增 `ImportTemplate`,admin 可上传导入模板或填写 `external_url` 并标记当前模板;`external_url` 优先于 `file.url`,当前模板互斥同样由应用层事务处理,不使用 MySQL 不支持的条件唯一约束。T-607/T-609 版本检查接口只读取 `DownloadRelease(platform, is_current=True)`;响应 URL 是外部可访问 URL,不暴露本地 `MEDIA_ROOT`,并把 `force_update` 作为公开发布元数据返回。导入模板下载不新增对外 JSON API,首页直接渲染当前模板下载链接。默认别名唯一性由 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/T-609 已实现 `DownloadRelease`,admin 可上传安装包或填写 `external_url`、标记当前版本、配置是否强制更新;`external_url` 优先于 `file.url`,每平台仅一个当前版本由模型 `save()` 在事务内把同平台旧 current 置为 false。T-610 已新增 `ImportTemplate`,admin 可上传导入模板或填写 `external_url` 并标记当前模板;`external_url` 优先于 `file.url`,当前模板互斥同样由应用层事务处理,不使用 MySQL 不支持的条件唯一约束。T-607/T-609 版本检查接口只读取 `DownloadRelease(platform, is_current=True)`;响应 URL 是外部可访问 URL,不暴露本地 `MEDIA_ROOT`,并把 `force_update` 作为公开发布元数据返回。导入模板下载不新增对外 JSON API,首页直接渲染当前模板下载链接。默认别名唯一性由 model validation、admin 与导入器保证;MySQL 不支持通用 partial unique index,若后续要强制数据库层默认别名唯一,可另评估触发器或约束表。 > 可选增强(接口预留、MVP 不实现):`account_alias_permission`(按账号授权可用别名,防止调用方点用未授权/昂贵模型);别名按比例分流到多个模型(灰度/AB/故障转移)。适配器接口需为此留口子。 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 8f30373..4fb0553 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -88,7 +88,7 @@ | T-607 | 桌面端最新版本检查接口 | T-606 | 给桌面端自动检查更新提供只读 JSON 合约。**新增路由**:`GET /api/v1/client/releases/latest?platform=windows`,公开匿名可访问,**不需要 API Key、不读取用户、不扣点、不占用生成接口限流**;只返回发布元数据,不返回本地文件系统路径、后台 ID、内部状态或任何用户数据。**请求参数**:`platform` 支持 `windows` / `macos` / `linux`,缺省按 `windows`;非法平台返回 `400 bad_request`。**响应结构**:有当前版本时返回 `{platform, release:{version, download_url, sha256, release_notes, published_at}}`;无当前版本或当前版本没有下载地址时返回 `{platform, release:null, message:"暂未发布"}` 且 HTTP 200,方便客户端安静处理。**数据来源**:复用 `DownloadRelease`,只查 `platform + is_current=True`;`download_url` 继续按 `external_url` 优先,否则由 `file.url` 生成绝对 HTTPS URL;`published_at` 可先使用 `updated_at`。**缓存与安全**:可加短 TTL 公共缓存(如 60 秒);生产下载仍走 HTTPS + SHA256 校验;不得把 `MEDIA_ROOT` 或服务器路径暴露给客户端。**含测试**:匿名无 Key 可访问;当前 release 返回完整结构和绝对下载 URL;无 current release 返回 `release:null`;非法 platform 返回 400;`external_url` 优先于 `file`;响应不含本地路径、模型/密钥/用户字段;`check`/目标测试通过并在 `../progress.md` 留证据 | DONE | | T-608 | 新用户注册赠送 100 点试用点数 | T-501, T-203, T-401 | 把 2026-07-08 的产品口径落地为账本安全实现。**业务口径**:只对 T-608 上线后的新注册用户自动发放 100 点;历史用户是否补发不在本任务内,需单独审批和批处理任务。**数据与服务**:新增 `PointsLedger.ChangeType.SIGNUP_BONUS`(展示为注册赠点)和 MySQL 兼容的数据库级幂等兜底 `SignupBonusGrant(user UNIQUE)`,不要用 partial unique / 条件唯一约束;新增 `apps.billing.services.grant_signup_bonus(user, points=100)`,在事务内锁/创建钱包、发放点数、写 `PointsLedger(signup_bonus,+100,balance_after,reason)`,重复调用或并发触发不得重复发放。**接入点**:allauth adapter 注册成功后调用 billing 服务,不得在 portal 直接写 `points_balance`;失败要清晰暴露并保持注册 / 账务一致性。**页面与文案**:公开首页、注册页、dashboard/记录页等用户端文案同步「注册送 100 点」;点数记录页能展示注册赠点正向流水;admin 可检索该流水和注册赠点记录。**防刷**:免邮箱验证策略下显式配置 `ACCOUNT_SIGNUP_RATE_LIMIT`(默认 `20/m/ip`),由 allauth signup rate limit 执行;图形验证码 / 人机验证仍作为上线前风控增强项。**测试**:注册后余额为 100 且有一条 `signup_bonus` 流水;重复调用服务不重复加点;并发触发不重复发放;现有用户不会自动补发;dashboard / 点数记录显示一致;`check`/迁移/目标测试通过并在 `../progress.md` 留证据 | DONE | | T-609 | 桌面端版本检查接口增加强制更新标记 | T-607 | 在 `GET /api/v1/client/releases/latest?platform=windows` 的当前版本响应中,为 `release` 对象新增布尔字段 `force_update`,用于桌面端判断是否必须升级。**接口兼容**:只新增字段,不删除既有 `version` / `download_url` / `sha256` / `release_notes` / `published_at` 字段;无当前版本或无下载地址时仍返回 `release:null`,不返回 `force_update`。**数据来源**:在 `DownloadRelease` 增加 `force_update` 布尔字段,默认 `false`;django-admin 可编辑并在列表展示。**示例**:`{platform:"windows", release:{version:"0.1.1", download_url:"...0.1.1.zip", release_notes:"优化了ai模块的生图的功能", force_update:true}}`。**安全**:接口仍公开匿名只读,不读取用户、不扣点、不暴露后台 ID、本地文件路径或内部状态。**文档**:同步 `api.md`、`routes.md`、`04-architecture.md`、`current-state.md`、版本接口对接文档口径。**测试**:覆盖 `force_update=true` / 默认 `false`、无 current release 不返回该字段、响应字段白名单更新、admin 字段可见;`makemigrations` / `migrate` / `check` / 目标测试通过并在 `../progress.md` 留证据 | DONE | -| T-610 | 首页导入模板下载入口 | T-606 | 在公开首页“下载客户端”按钮右侧新增“下载导入模板”链接,让用户下载桌面端导入商品数据使用的 Excel 模板。**产品口径**:模板下载是公开资源,匿名可见;不需要登录、不需要 API Key、不扣点、不占用生成接口限流。**后台配置**:新增独立 `ImportTemplate`(不要复用 `DownloadRelease`,两者生命周期不同),字段建议包含 `name`、`file`(FileField,`MEDIA_ROOT/import_templates/`)、`external_url`(可选,有则优先,后续切 CDN/对象存储)、`sha256`(可选)、`is_current`、`notes`、`created_at`、`updated_at`;admin 可上传/更新模板并标记当前模板;每次保存 current 时把其他 current 置为 false,避免使用 MySQL 不支持的条件唯一约束。**首页行为**:`HomeView` 同时读取 Windows 当前 `DownloadRelease` 和当前 `ImportTemplate`;有当前模板且有可下载地址时,在“下载客户端”右侧展示“下载导入模板”链接;无模板时不显示该链接或显示不可用提示,不能影响客户端下载入口和首页 200。**托管策略**:本地上传模板复用生产 Nginx `/media/` 静态托管,不经 Django/Gunicorn 传大文件;`external_url` 优先于 `file.url`,响应/页面不得暴露本地 `MEDIA_ROOT` 或服务器文件路径。**UI**:保持 T-606 下载区现有布局,桌面端按钮为主按钮,导入模板为次级链接/按钮;移动端需换行不重叠。**文档**:同步 `02-requirements.md`、`04-architecture.md`、`routes.md`、`api.md`、`current-state.md`。**测试**:覆盖首页有模板时展示链接且 href 为绝对/可访问 media URL,`external_url` 优先,本地路径不泄露,无模板时首页正常且不显示错误;admin 字段可见;`makemigrations` / `migrate` / `check` / 目标测试通过并在 `../progress.md` 留证据 | TODO | +| T-610 | 首页导入模板下载入口 | T-606 | 在公开首页“下载客户端”按钮右侧新增“下载导入模板”链接,让用户下载桌面端导入商品数据使用的 Excel 模板。**产品口径**:模板下载是公开资源,匿名可见;不需要登录、不需要 API Key、不扣点、不占用生成接口限流。**后台配置**:新增独立 `ImportTemplate`(不要复用 `DownloadRelease`,两者生命周期不同),字段建议包含 `name`、`file`(FileField,`MEDIA_ROOT/import_templates/`)、`external_url`(可选,有则优先,后续切 CDN/对象存储)、`sha256`(可选)、`is_current`、`notes`、`created_at`、`updated_at`;admin 可上传/更新模板并标记当前模板;每次保存 current 时把其他 current 置为 false,避免使用 MySQL 不支持的条件唯一约束。**首页行为**:`HomeView` 同时读取 Windows 当前 `DownloadRelease` 和当前 `ImportTemplate`;有当前模板且有可下载地址时,在“下载客户端”右侧展示“下载导入模板”链接;无模板时不显示该链接或显示不可用提示,不能影响客户端下载入口和首页 200。**托管策略**:本地上传模板复用生产 Nginx `/media/` 静态托管,不经 Django/Gunicorn 传大文件;`external_url` 优先于 `file.url`,响应/页面不得暴露本地 `MEDIA_ROOT` 或服务器文件路径。**UI**:保持 T-606 下载区现有布局,桌面端按钮为主按钮,导入模板为次级链接/按钮;移动端需换行不重叠。**文档**:同步 `02-requirements.md`、`04-architecture.md`、`routes.md`、`api.md`、`current-state.md`。**测试**:覆盖首页有模板时展示链接且 href 为绝对/可访问 media URL,`external_url` 优先,本地路径不泄露,无模板时首页正常且不显示错误;admin 字段可见;`makemigrations` / `migrate` / `check` / 目标测试通过并在 `../progress.md` 留证据 | DONE | ## 里程碑 diff --git a/docs/api.md b/docs/api.md index 5542719..be6d278 100644 --- a/docs/api.md +++ b/docs/api.md @@ -45,7 +45,7 @@ T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session + 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。 -T-610 目标是在公开首页下载区新增“下载导入模板”链接:模板由 django-admin 维护 `ImportTemplate` 当前记录,首页直接渲染公开下载链接;该能力不新增对外 JSON API,不需要 API Key、不读取用户、不扣点。生产本地模板文件复用 Nginx 直接服务 `MEDIA_ROOT/import_templates/`,避免文件下载占用 Gunicorn worker。 +T-610 已在公开首页下载区新增“下载导入模板”链接:模板由 django-admin 维护 `ImportTemplate` 当前记录,首页直接渲染公开下载链接;该能力不新增对外 JSON API,不需要 API Key、不读取用户、不扣点。生产本地模板文件复用 Nginx 直接服务 `MEDIA_ROOT/import_templates/`,避免文件下载占用 Gunicorn worker;`external_url` 优先于本地文件 URL。 T-607/T-609 已实现 `GET /api/v1/client/releases/latest?platform=windows`,给桌面端自动检查更新使用。该接口公开匿名可访问,不需要 API Key,不读取用户、不扣点、不占用生成接口限流;只返回 `DownloadRelease` 的公开发布元数据。T-609 起响应的 `release.force_update` 表示该版本是否必须升级,来源于后台 `DownloadRelease.force_update`,默认 `false`。 diff --git a/docs/current-state.md b/docs/current-state.md index 78663da..0508bb6 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -12,11 +12,11 @@ ## 当前快照 - 日期:2026-07-08 -- 阶段: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 公开首页 + 客户端下载入口、T-607 桌面端最新版本检查接口、T-608 新用户注册赠送 100 点试用点数与 T-609 桌面端版本检查接口增加强制更新标记已完成;T-610 首页导入模板下载入口已登记为 TODO;后续仍需处理真实支付回调到账闭环、客户端下载包发布和真实图片耗时验证 +- 阶段: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 公开首页 + 客户端下载入口、T-607 桌面端最新版本检查接口、T-608 新用户注册赠送 100 点试用点数、T-609 桌面端版本检查接口增加强制更新标记与 T-610 首页导入模板下载入口已完成;后续仍需处理真实支付回调到账闭环、客户端下载包发布和真实图片耗时验证 - 技术栈:系统 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/T-608 已接入 allauth 注册登录路径,注册成功后 adapter 调用 `grant_signup_bonus()` 经 billing 一次性发放 100 点并写 `signup_bonus` 流水,新增 `SignupBonusGrant(user UNIQUE)` 幂等标记、admin 只读检索和 `billing.0007` 迁移;T-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503/T-608 已扩展 `/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 环境变量与 `ACCOUNT_SIGNUP_RATE_LIMIT` 注册限流配置;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;T-607/T-609 已新增 `ClientLatestReleaseView` 与 `/api/v1/client/releases/latest`,公开匿名返回当前客户端版本 JSON,`release.force_update` 表示该版本是否强制升级;`portal.0002_downloadrelease_force_update` 已给 `DownloadRelease` 增加 `force_update` 字段,admin 可编辑和筛选。 +- 生产代码:已有最小 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/T-608 已接入 allauth 注册登录路径,注册成功后 adapter 调用 `grant_signup_bonus()` 经 billing 一次性发放 100 点并写 `signup_bonus` 流水,新增 `SignupBonusGrant(user UNIQUE)` 幂等标记、admin 只读检索和 `billing.0007` 迁移;T-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503/T-608 已扩展 `/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 环境变量与 `ACCOUNT_SIGNUP_RATE_LIMIT` 注册限流配置;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;T-607/T-609 已新增 `ClientLatestReleaseView` 与 `/api/v1/client/releases/latest`,公开匿名返回当前客户端版本 JSON,`release.force_update` 表示该版本是否强制升级;`portal.0002_downloadrelease_force_update` 已给 `DownloadRelease` 增加 `force_update` 字段,admin 可编辑和筛选;T-610 已新增 `ImportTemplate` 模型/admin/迁移 `portal.0003_importtemplate`,首页读取当前模板并在“下载客户端”旁展示“下载导入模板”,本地文件 URL 转为当前站点绝对 URL,`external_url` 优先。 - 用户端导航:顶部导航 active 状态已修复,`portal/base.html` 基于 `request.resolver_match.url_name` 高亮当前页面入口,并用 `aria-current="page"` 标记;「充值」不再在非充值页固定深色高亮。 -- 最新验证:T-609 桌面端版本检查接口增加强制更新标记已验证 `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\api\views.py apps\api\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 --noinput` 已应用 `portal.0002_downloadrelease_force_update`;`py -3.12 manage.py test apps.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` 重跑通过,11 tests OK;`py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_download_release_only_keeps_one_current_per_platform --keepdb --noinput --verbosity 2` 通过,2 tests OK;`.\init.ps1` 收尾通过;`git diff --check` 通过,仅 Windows CRLF 提示。测试期一次 API 目标测试并行启动时远程 MySQL `43.128.3.240` 连接超时,`Test-NetConnection 43.128.3.240 -Port 3306` 显示端口可达后单独重跑通过;仍保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 +- 最新验证:T-610 首页导入模板下载入口已验证 `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\tests.py` 通过;`py -3.12 manage.py makemigrations portal` 生成 `portal.0003_importtemplate`;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py check` 通过,0 issues;T-610 相关测试 `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_import_template_download_link apps.portal.tests.PortalAccountFlowTests.test_homepage_builds_absolute_import_template_file_url_without_local_path apps.portal.tests.PortalAccountFlowTests.test_homepage_prefers_import_template_external_url_over_file apps.portal.tests.PortalAccountFlowTests.test_import_template_only_keeps_one_current apps.portal.tests.PortalAccountFlowTests.test_import_template_admin_exposes_download_fields --keepdb --noinput --verbosity 2` 通过,6 tests OK,并在测试库应用 `portal.0003_importtemplate`;完整 `py -3.12 manage.py test apps.portal --keepdb --noinput --verbosity 1` 跑到后段时远程 MySQL `43.128.3.240` 连接被重置(2013 / WinError 10054),前 27 条已通过,后续 8 errors 为连接断开后的事务错误;`git diff --check` 通过,仅 Windows CRLF 提示。仍保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 - 测试: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-608 新用户注册赠送 100 点试用点数已验证 `py -3.12 -m py_compile config\settings.py apps\billing\models.py apps\billing\services.py apps\billing\admin.py apps\portal\adapters.py apps\portal\views.py apps\billing\tests.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 billing --noinput` 通过,No migrations to apply;注册赠点服务和并发幂等目标测试 3 tests OK;注册限流配置测试 1 test OK;portal 注册赠点、首页文案、导航、dashboard 和点数记录目标测试 6 tests OK;`SignupBonusGrant` admin 注册 smoke 1 test OK;`.\init.ps1` 收尾通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令);`git diff --check` 通过,仅 Windows CRLF 提示。测试期仍有 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告;一次 portal 目标测试启动阶段曾因远程 MySQL 连接 `43.128.3.240` 超时中断,端口探测恢复后重跑通过。 - 最新验证:T-607 桌面端最新版本检查接口已验证 `.\init.ps1` 开工前通过;`py -3.12 -m py_compile apps\api\views.py apps\api\urls.py apps\api\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.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` 通过,9 tests OK;`py -3.12 manage.py test apps.api.tests.ApiKeyAuthenticationTests apps.api.tests.BalanceApiTests apps.api.tests.ModelsCatalogApiTests apps.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` 通过,25 tests OK;`py -3.12 manage.py test apps.api --keepdb --noinput --verbosity 1` 通过,54 tests OK;`.\init.ps1` 收尾通过;`git diff --check` 通过,仅 Windows CRLF 提示。测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 @@ -54,11 +54,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-606 公开首页 + 客户端下载入口;T-607 桌面端最新版本检查接口;T-608 新用户注册赠送 100 点试用点数;T-609 桌面端版本检查接口增加强制更新标记。 +- 已完成: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-607 桌面端最新版本检查接口;T-608 新用户注册赠送 100 点试用点数;T-609 桌面端版本检查接口增加强制更新标记;T-610 首页导入模板下载入口。 - 正在进行:无。 -- 待开始:T-610 首页导入模板下载入口;之后可继续处理真实支付回调到账闭环、配置并发布客户端下载包、真实图片耗时验证,或从 Backlog 拆新任务。 +- 待开始:当前无编号 TODO;之后可继续处理真实支付回调到账闭环、配置并发布客户端下载包、真实图片耗时验证,或从 Backlog 拆新任务。 - 当前 blocker:支付商户真实密钥/证书与生产 SDK 依赖仍待提供;微信回调到账闭环仍需真实支付验收;真实 AI 标题生成已在线上跑通,图片生成真实耗时仍需补测。 -- 下一个可领取任务:T-610 首页导入模板下载入口。 +- 下一个可领取任务:当前无编号 TODO;需先从 Backlog 或业务优先级拆出下一项任务。 ## 当前可运行内容 @@ -123,8 +123,8 @@ gunicorn config.wsgi:application --bind 127.0.0.1:8002 --workers 1 --worker-clas 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-610。 -5. 下一步优先实现 T-610;之后再按业务优先级拆真实支付回调到账闭环、客户端下载包发布、真实图片耗时验证,或从 Backlog 创建新任务。 +4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务;当前无编号 TODO。 +5. 下一步按业务优先级拆真实支付回调到账闭环、客户端下载包发布、真实图片耗时验证,或从 Backlog 创建新任务。 ## 维护规则 diff --git a/docs/routes.md b/docs/routes.md index a188c8b..2952210 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -18,7 +18,7 @@ T-501/T-608 已落地 `/signup`、`/login`、`/logout` 与 `/dashboard`:注册成功后经计费层一次性发放 100 点试用点数并写 `signup_bonus` 流水,注册限流由 allauth signup rate limit 执行。T-502 已落地 `/apikeys`:登录用户只能管理自己的 Key,生成后明文只显示一次,列表只显示 prefix,删除为吊销 `revoked`。T-503/T-505/T-608 已扩展 `/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`;无当前版本时显示「暂未发布」。T-610 目标是在同一下载区“下载客户端”右侧增加“下载导入模板”,模板由后台配置当前版本,匿名可下载。 +T-606 已落地 `/` 公开首页:匿名访问返回 200,不再重定向到 `/dashboard`;匿名用户看到注册 / 登录 / 下载入口,登录用户看到「进入控制台」。首页下载区读取 `DownloadRelease(platform=windows, is_current=True)`,优先使用 `external_url`,否则使用后台上传文件的 `file.url`;无当前版本时显示「暂未发布」。T-610 已在同一下载区“下载客户端”右侧增加“下载导入模板”,模板由后台配置当前版本,匿名可下载;模板本地文件 URL 会转成当前站点绝对 URL,页面不暴露本地文件路径。 ## API 路由(对外,DRF) diff --git a/progress.md b/progress.md index dda165f..c04965f 100644 --- a/progress.md +++ b/progress.md @@ -1554,7 +1554,7 @@ - 安全:该资源公开匿名下载,不需要 API Key、不读取用户、不扣点;页面不得暴露 `MEDIA_ROOT` 或服务器本地路径;当前模板互斥用应用层事务处理,不使用 MySQL 不支持的条件唯一约束。 - 接口:不新增对外 JSON API,首页 SSR 直接渲染当前模板下载链接即可。 - 文档变更: - - `docs/06-tasks.md`:新增 T-610「首页导入模板下载入口」,状态 TODO,并增加 M9 里程碑。 + - `docs/06-tasks.md`:新增 T-610「首页导入模板下载入口」,并增加 M9 里程碑。 - `docs/02-requirements.md`:新增 P1「导入模板下载」功能行,游客权限加入公开模板下载入口。 - `docs/04-architecture.md`:新增 `ImportTemplate` 目标 schema 和首页 / admin / media 托管口径。 - `docs/routes.md`:更新 `/` 首页职责与 admin 管理项。 @@ -1562,3 +1562,26 @@ - `docs/00-ai-start-here.md`、`docs/current-state.md`:记录 T-610 为下一个可领取任务。 - 验证:纯文档修改;`git diff --check` 通过。 - 下一步:领取 T-610,先实现 `ImportTemplate` 模型/admin/迁移,再更新 `HomeView` 与首页模板,补目标测试并验证迁移、check、portal 首页测试。 + +## 2026-07-08 实施:T-610 首页导入模板下载入口 + +- 状态:DONE。 +- 代码变更: + - `apps/portal/models.py`:新增 `ImportTemplate`,字段包含 `name`、`file`、`external_url`、`sha256`、`is_current`、`notes`、`created_at`、`updated_at`;保存当前模板时在事务内把其他 current 置为 false,不使用 MySQL 条件唯一约束。 + - `apps/portal/admin.py`:注册导入模板后台,支持上传模板文件、填写外部下载地址、标记当前模板、查看 SHA256 与下载来源。 + - `apps/portal/views.py`:`HomeView` 同时读取当前 Windows 客户端版本与当前导入模板;本地模板 `file.url` 会转成当前站点绝对 URL,`external_url` 优先。 + - `apps/portal/templates/portal/home.html`:在首页 hero 与下载区的“下载客户端”旁展示“下载导入模板”;无当前模板或无下载地址时不展示,不影响首页 200。 + - `apps/portal/migrations/0003_importtemplate.py`:新增 `import_template` 表。 + - `apps/portal/tests.py`:补充首页模板链接、外部 URL 优先、本地文件绝对 URL、本地路径不泄露、当前模板互斥、admin 字段可见的目标测试。 +- 文档变更: + - `docs/06-tasks.md`:T-610 标记为 DONE。 + - `docs/00-ai-start-here.md`、`docs/current-state.md`、`docs/04-architecture.md`、`docs/routes.md`、`docs/api.md`:同步 T-610 已落地口径。 +- 验证: + - `py -3.12 manage.py makemigrations portal`:生成 `portal.0003_importtemplate`。 + - `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\tests.py`:通过。 + - `py -3.12 manage.py makemigrations --check --dry-run`:通过,No changes detected。 + - `py -3.12 manage.py check`:通过,0 issues。 + - `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_import_template_download_link apps.portal.tests.PortalAccountFlowTests.test_homepage_builds_absolute_import_template_file_url_without_local_path apps.portal.tests.PortalAccountFlowTests.test_homepage_prefers_import_template_external_url_over_file apps.portal.tests.PortalAccountFlowTests.test_import_template_only_keeps_one_current apps.portal.tests.PortalAccountFlowTests.test_import_template_admin_exposes_download_fields --keepdb --noinput --verbosity 2`:通过,6 tests OK,并在测试库应用 `portal.0003_importtemplate`。 + - `py -3.12 manage.py test apps.portal --keepdb --noinput --verbosity 1`:跑到后段时远程 MySQL `43.128.3.240` 连接被重置(2013 / WinError 10054);前 27 条已通过,后续 8 errors 为连接断开后的事务错误。 + - `git diff --check`:通过,仅 Windows CRLF 提示。 +- 下一步:提交后可部署到 185.216.248.75,执行迁移并在 django-admin 新增当前导入模板记录;配置模板后线上首页才会显示“下载导入模板”。