From b71564e2bf9ceb31df1f624a817ee2297f9f9256 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 13 Jul 2026 17:04:59 +0800 Subject: [PATCH] feat: add client release size bytes --- README.md | 2 +- apps/api/tests.py | 20 ++++++++++++++- apps/api/views.py | 1 + apps/portal/admin.py | 2 ++ .../0004_downloadrelease_size_bytes.py | 25 +++++++++++++++++++ apps/portal/models.py | 9 ++++++- docs/02-requirements.md | 2 +- docs/04-architecture.md | 11 ++++---- docs/06-tasks.md | 3 +++ docs/api.md | 7 +++--- docs/current-state.md | 4 ++- docs/routes.md | 4 +-- progress.md | 21 ++++++++++++++++ 13 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 apps/portal/migrations/0004_downloadrelease_size_bytes.py diff --git a/README.md b/README.md index f3fdfdf..a8ad608 100644 --- a/README.md +++ b/README.md @@ -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/T-603 django-admin 中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地、T-606 公开首页 + 客户端下载入口、T-607 桌面端最新版本检查接口、T-608 新用户注册赠送 100 点试用点数、T-609 桌面端版本强制更新标记、T-610 首页导入模板下载入口、T-611 用户端品牌名统一为“虾皮圈”、T-612 生图同步接口止血、T-613 抽生成核心 service、T-614 生图异步任务化接口与 T-615 旧同步生图接口遥测 / 弃用口径。用户可通过公开首页进入注册、登录、下载客户端和下载导入模板;新用户注册后经计费层自动获得 100 点并写注册赠点流水;登录后可扫码充值并轮询到账,生成 / 删除(吊销)API Key,查看余额、充值总额、分页充值记录、分页点数记录与可用模型;桌面端可匿名请求最新客户端版本 JSON,并读取 `release.force_update` 判断是否必须升级;新版桌面端可用异步生图提交 / 轮询接口,旧同步生图接口继续兼容并写结构化用量日志;运营可在 django-admin 检索用户、钱包、API Key、计费规则、汇率、充值订单、点数流水、注册赠点记录、调用记录、图片生成任务、客户端发布版本和导入模板,并通过计费层带原因手工调点。生产侧仍需补真实支付回调到账闭环,并按日志观察旧同步生图接口迁移进度。详见 [`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 公开首页 + 客户端下载入口、T-607 桌面端最新版本检查接口、T-608 新用户注册赠送 100 点试用点数、T-609 桌面端版本强制更新标记、T-610 首页导入模板下载入口、T-611 用户端品牌名统一为“虾皮圈”、T-612 生图同步接口止血、T-613 抽生成核心 service、T-614 生图异步任务化接口、T-615 旧同步生图接口遥测 / 弃用口径、T-616 生图失败自动重试 2 次与 T-617 桌面端版本文件大小字段。用户可通过公开首页进入注册、登录、下载客户端和下载导入模板;新用户注册后经计费层自动获得 100 点并写注册赠点流水;登录后可扫码充值并轮询到账,生成 / 删除(吊销)API Key,查看余额、充值总额、分页充值记录、分页点数记录与可用模型;桌面端可匿名请求最新客户端版本 JSON,并读取 `release.force_update` 判断是否必须升级、读取 `release.size_bytes` 校验安装包大小;新版桌面端可用异步生图提交 / 轮询接口,旧同步生图接口继续兼容并写结构化用量日志;运营可在 django-admin 检索用户、钱包、API Key、计费规则、汇率、充值订单、点数流水、注册赠点记录、调用记录、图片生成任务、客户端发布版本和导入模板,并通过计费层带原因手工调点。生产侧仍需补真实支付回调到账闭环,并按日志观察旧同步生图接口迁移进度。详见 [`docs/current-state.md`](docs/current-state.md)。 > ⚠️ 涉及资金/点数。改动充值、扣费、退款、对账相关代码前,先读 [`docs/05-coding-rules.md`](docs/05-coding-rules.md) 第 8 节与 [`docs/04-architecture.md`](docs/04-architecture.md) 第四节计费时序。 diff --git a/apps/api/tests.py b/apps/api/tests.py index e1195e9..4f19d80 100644 --- a/apps/api/tests.py +++ b/apps/api/tests.py @@ -442,6 +442,7 @@ class ClientLatestReleaseApiTests(TestCase): sha256: str = "a" * 64, release_notes: str = "首版 Windows 客户端", force_update: bool = False, + size_bytes: int | None = None, ) -> DownloadRelease: return DownloadRelease.objects.create( platform=platform, @@ -452,6 +453,7 @@ class ClientLatestReleaseApiTests(TestCase): sha256=sha256, release_notes=release_notes, force_update=force_update, + size_bytes=size_bytes, ) def test_latest_release_is_public_without_api_key_and_returns_current_release(self): @@ -460,6 +462,7 @@ class ClientLatestReleaseApiTests(TestCase): external_url="https://download.example.com/cmhub-1.2.3.exe", sha256="b" * 64, release_notes="修复下载入口并补充 SHA256", + size_bytes=18_765_432, ) response = self.client.get(self.url) @@ -475,11 +478,13 @@ class ClientLatestReleaseApiTests(TestCase): "sha256", "release_notes", "force_update", + "size_bytes", "published_at", }, ) self.assertEqual(response.data["release"]["version"], "1.2.3") self.assertFalse(response.data["release"]["force_update"]) + self.assertEqual(response.data["release"]["size_bytes"], 18_765_432) self.assertEqual( response.data["release"]["download_url"], "https://download.example.com/cmhub-1.2.3.exe", @@ -517,6 +522,14 @@ class ClientLatestReleaseApiTests(TestCase): ) self.assertTrue(response.data["release"]["force_update"]) + def test_latest_release_returns_null_size_bytes_when_not_configured(self): + self.create_release(size_bytes=None) + + response = self.client.get(self.url) + + self.assertEqual(response.status_code, 200) + self.assertIsNone(response.data["release"]["size_bytes"]) + def test_latest_release_ignores_web_session_and_does_not_return_user_data(self): user = get_user_model().objects.create_user( username="release-session-user", @@ -590,6 +603,7 @@ class ClientLatestReleaseApiTests(TestCase): self.assertIsNone(response.data["release"]) self.assertEqual(response.data["message"], "暂未发布") self.assertNotIn("force_update", response.data) + self.assertNotIn("size_bytes", response.data) def test_latest_release_rejects_invalid_platform(self): response = self.client.get(f"{self.url}?platform=android") @@ -630,6 +644,7 @@ class ClientLatestReleaseApiTests(TestCase): "sha256", "release_notes", "force_update", + "size_bytes", "published_at", }, ) @@ -649,13 +664,16 @@ class ClientLatestReleaseApiTests(TestCase): ): self.assertNotIn(forbidden, response_body) - def test_download_release_admin_exposes_force_update(self): + def test_download_release_admin_exposes_release_metadata_fields(self): registered_admin = admin.site._registry[DownloadRelease] self.assertIn("force_update", registered_admin.list_display) self.assertIn("force_update", registered_admin.list_filter) + self.assertIn("size_bytes", registered_admin.list_display) version_fields = registered_admin.fieldsets[0][1]["fields"] self.assertIn("force_update", version_fields) + download_fields = registered_admin.fieldsets[1][1]["fields"] + self.assertIn("size_bytes", download_fields) @override_settings( diff --git a/apps/api/views.py b/apps/api/views.py index 91669ae..4c67169 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -299,6 +299,7 @@ class ClientLatestReleaseView(APIView): "sha256": release.sha256, "release_notes": release.release_notes, "force_update": release.force_update, + "size_bytes": release.size_bytes, "published_at": timezone.localtime(release.updated_at).isoformat(), }, }, diff --git a/apps/portal/admin.py b/apps/portal/admin.py index 4bd23b6..a82b83d 100644 --- a/apps/portal/admin.py +++ b/apps/portal/admin.py @@ -11,6 +11,7 @@ class DownloadReleaseAdmin(admin.ModelAdmin): "is_current", "force_update", "download_source", + "size_bytes", "sha256_short", "updated_at", ) @@ -36,6 +37,7 @@ class DownloadReleaseAdmin(admin.ModelAdmin): "fields": ( "file", "external_url", + "size_bytes", "sha256", ) }, diff --git a/apps/portal/migrations/0004_downloadrelease_size_bytes.py b/apps/portal/migrations/0004_downloadrelease_size_bytes.py new file mode 100644 index 0000000..abd93f6 --- /dev/null +++ b/apps/portal/migrations/0004_downloadrelease_size_bytes.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.15 on 2026-07-13 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portal", "0003_importtemplate"), + ] + + operations = [ + migrations.AddField( + model_name="downloadrelease", + name="size_bytes", + field=models.PositiveBigIntegerField( + blank=True, + help_text="客户端下载文件大小,单位字节;桌面端用于下载后校验。", + null=True, + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="文件大小字节数", + ), + ), + ] diff --git a/apps/portal/models.py b/apps/portal/models.py index 2a3944f..d62ca81 100644 --- a/apps/portal/models.py +++ b/apps/portal/models.py @@ -1,5 +1,5 @@ from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator +from django.core.validators import MinValueValidator, RegexValidator from django.db import models, transaction @@ -13,6 +13,13 @@ class DownloadRelease(models.Model): version = models.CharField("版本", max_length=64) file = models.FileField("安装包文件", upload_to="downloads/", blank=True) external_url = models.URLField("外部下载地址", blank=True) + size_bytes = models.PositiveBigIntegerField( + "文件大小字节数", + null=True, + blank=True, + validators=[MinValueValidator(1)], + help_text="客户端下载文件大小,单位字节;桌面端用于下载后校验。", + ) sha256 = models.CharField( "SHA256", max_length=64, diff --git a/docs/02-requirements.md b/docs/02-requirements.md index 3d21c99..141a5e2 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -37,7 +37,7 @@ | 点数流水 | 每次点数变动(充值/消费/退款/运营调整)落库,可对账 | P0 | | 运营后台 | django-admin 管理用户、点数、计费规则、充值订单、流水、调用记录 | P0 | | 公开首页 + 客户端下载 | 匿名访客打开域名看到项目介绍与上手引导(注册→充值→建 Key→下载客户端)、并能下载桌面端安装包(含版本/SHA256 校验);前期安装包托管在本服务器,后续可切对象存储/CDN | P1 | -| 桌面端版本检查 API | 桌面端可匿名请求最新版本 JSON,拿到版本号、下载地址、SHA256 和发布说明;无当前版本时返回“暂未发布”而不是报错 | P1 | +| 桌面端版本检查 API | 桌面端可匿名请求最新版本 JSON,拿到版本号、下载地址、文件大小字节数、SHA256、强制更新标记和发布说明;无当前版本时返回“暂未发布”而不是报错 | P1 | | 导入模板下载 | 匿名访客可在公开首页“下载客户端”旁边下载导入商品数据用的 Excel 模板;运营可在后台更新当前模板文件 | P1 | | 用户端品牌展示 | 终端用户可见的用户端页面统一展示产品名“虾皮圈”;内部工程名、API 路径、环境变量和文档中的服务代号 `cmhub` 不随本项改名 | P1 | diff --git a/docs/04-architecture.md b/docs/04-architecture.md index ed877a3..c935f62 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -18,9 +18,9 @@ 组件落位: -- **用户端层(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/T-617 已新增公开 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` 仅表示当前发布版本是否强制升级。 +- **API 层(DRF)**:对外生成接口、异步图片任务接口、余额查询、支付回调接收、扫码下单、公开版本检查接口。入口 `apps/api/`。生成/余额/模型目录这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签;T-607/T-609/T-617 的客户端下载版本检查接口为公开只读例外,不需要 API Key,不读取用户、不扣点,`release.force_update` 仅表示当前发布版本是否强制升级,`release.size_bytes` 仅表示下载文件大小字节数。 - **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。 - **AI 调用层(Provider Adapter 架构)**:对外只暴露稳定能力,内部用「能力别名 → 具体供应商适配器」解耦。入口 `apps/ai/`,适配器在 `apps/ai/providers/`。 - **内容安全层(本地敏感词 / 后续云审核)**:入口 `apps/moderation/`。T-604 只做 prompt 文本本地敏感词快筛,命中在扣点和调上游前返回 `content_blocked`;云内容安全、图片审核和输出审核保留扩展点,不在 T-604 范围。 @@ -55,7 +55,7 @@ T-305 已实现 `/api/v1/recharge/create` 与 `/api/v1/recharge/status`:两个 T-504 已实现用户端 `/recharge` 页面:GET 展示当前余额、充值表单、当前订单和最近充值;POST 经 `RechargeCreateForm` 校验金额与支付方式后复用 `create_recharge_order()` 创建 pending 订单并重定向到当前订单页,避免刷新重复下单;页面用本地 static 自托管的 qrcode.js 渲染 `code_url`,同时保留可复制支付票据兜底;浏览器每秒轮询 `/api/v1/recharge/status`,订单 paid 后刷新页面重新读取余额。页面不直接写 `UserWallet.points_balance` 或 `PointsLedger`。当 `PAYMENT_CALLBACK_MODE=mock` 时,页面必须醒目提示二维码为测试票据,不能用于微信/支付宝真实支付。当前因支付宝可信 IP 未配置,用户端表单暂只开放微信支付,底层支付宝 API / 回调 / SDK 路径保留。 -T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前校验 `image_url` 协议与解析后的 IP,只允许公网 `http` / `https`,拒绝私有、回环、链路本地、保留、组播、未指定地址;重定向由服务端手动跟随并逐跳重新校验,响应按 `IMAGE_URL_MAX_BYTES` 流式限长读取。`REST_FRAMEWORK` 全局默认认证为空、默认权限为 `IsAuthenticated`,外部 API 和用户端 session API 必须显式声明认证类;生成接口挂 `GenerateRateThrottle`,认证失败挂 IP 限流;充值下单通过 `RECHARGE_MAX_AMOUNT_CNY` 控制单笔上限。T-607/T-609 版本检查接口已显式使用 `AllowAny` / 空认证,且只返回公开发布元数据和强制更新标记。 +T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前校验 `image_url` 协议与解析后的 IP,只允许公网 `http` / `https`,拒绝私有、回环、链路本地、保留、组播、未指定地址;重定向由服务端手动跟随并逐跳重新校验,响应按 `IMAGE_URL_MAX_BYTES` 流式限长读取。`REST_FRAMEWORK` 全局默认认证为空、默认权限为 `IsAuthenticated`,外部 API 和用户端 session API 必须显式声明认证类;生成接口挂 `GenerateRateThrottle`,认证失败挂 IP 限流;充值下单通过 `RECHARGE_MAX_AMOUNT_CNY` 控制单笔上限。T-607/T-609/T-617 版本检查接口已显式使用 `AllowAny` / 空认证,且只返回公开发布元数据、强制更新标记和文件大小字节数。 **计费层(`apps/billing`)** @@ -104,7 +104,7 @@ T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前 | PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel | | ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),按 `currency + effective_from` 取当前有效值 | | SensitiveWord | 运营配置 | 本地敏感词快筛词库;word、normalized_word、category、action、is_active;T-604 MVP 仅支持 action=block | -| DownloadRelease | 运营配置 | 客户端下载版本;platform、version、file、external_url、sha256、is_current、force_update、release_notes、created_at、updated_at;每平台当前版本由应用层事务保存时互斥 | +| DownloadRelease | 运营配置 | 客户端下载版本;platform、version、file、external_url、size_bytes、sha256、is_current、force_update、release_notes、created_at、updated_at;每平台当前版本由应用层事务保存时互斥 | | ImportTemplate | 运营配置 | 导入商品数据模板;name、file、external_url、sha256、is_current、notes、created_at、updated_at;当前模板由应用层事务保存时互斥 | 关键事实: @@ -201,6 +201,7 @@ CREATE TABLE download_release ( version VARCHAR(64) NOT NULL, file VARCHAR(100) NOT NULL DEFAULT '', -- MEDIA_ROOT/downloads/... external_url VARCHAR(200) NOT NULL DEFAULT '', -- 有则优先,后续切 CDN/对象存储 + size_bytes BIGINT UNSIGNED NULL, -- 安装包文件大小字节数,客户端校验用 sha256 VARCHAR(64) NOT NULL DEFAULT '', is_current BOOLEAN NOT NULL DEFAULT FALSE, force_update BOOLEAN NOT NULL DEFAULT FALSE, -- 当前版本是否强制升级 @@ -223,7 +224,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/T-617 已实现 `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/T-617 版本检查接口只读取 `DownloadRelease(platform, is_current=True)`;响应 URL 是外部可访问 URL,不暴露本地 `MEDIA_ROOT`,并把 `force_update` 和 `size_bytes` 作为公开发布元数据返回。导入模板下载不新增对外 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 3d1b1e6..1d90690 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -96,6 +96,8 @@ | T-615 | 旧同步生图接口用量遥测 + 弃用口径 | T-614 | 给旧同步接口装可观测、定弃用退出条件,避免永久双维护。**最小遥测**:旧 `POST /api/v1/generate/image` 和新 `/tasks` 路径都写结构化日志 / 计数,至少含 route_type(sync/async)、api_key 前缀或 ID、user_id、client version(若请求头提供,如 `X-Client-Version`)、alias、status、latency_ms、error_code;日志不得包含 API Key 明文、prompt 全文、图片 base64 或 provider raw。**查看方式**:先用日志查询即可;若要 admin 报表需单独评估数据量和索引。**弃用口径**:在 `deployment.md`/`04-architecture.md` 记录迁移计划:新版桌面端默认走异步接口 → 观察旧路调用量和错误率 → 旧路调用归零或低于阈值一段时间 → 宣布 deprecate → 另立任务下线;旧同步接口下线前必须保留成功响应兼容。**验收**:能按 client version / api_key 看到旧路与新路用量;弃用条件成文;不泄露敏感数据;`check`/目标测试/`init` 通过并在 `../progress.md` 留证据 | DONE | | T-616 | 生图失败自动重试 2 次 | T-614, T-615, T-203 | 给异步生图 worker 增加临时性上游失败自动重试,降低 `api.vectorengine.ai` 偶发 `Read timed out` 对用户的失败率。**业务口径**:“2 次重试”定义为第 1 次正常执行 + 失败后最多再重试 2 次,即最多 3 次上游调用;提交阶段仍只预扣一次。**只重试临时性错误**:`upstream_timeout`、网络连接错误、上游 502/503/504 等;不重试 `content_blocked`、`insufficient_points`、`no_pricing_rule`、模型/别名配置不可用、参数错误、Provider 配置错误等确定性错误。**数据结构**:复用现有 `attempt_count` 表示已开始执行次数;新增 `next_attempt_at`(允许下次抢占时间)和必要的 metadata-only admin 展示;如需记录最后一次错误,继续使用 `error_code` / `error_message`,不得保存 provider raw。**状态机**:`queued -> running(attempt_count+1)`;若成功则 `succeeded` 并确认已预扣调用;若可重试失败且 `attempt_count < 3`,任务回到 `queued`、设置 `next_attempt_at=now+backoff`、保留 `CallRecord.pending`、不退点;若不可重试或已到最终次数,才 `failed` 并调用计费层幂等退点。**worker 抢任务**:只抢 `status=queued` 且 `next_attempt_at IS NULL OR next_attempt_at <= now` 的任务;避免 66 worker 立即反复打爆上游。**配置**:新增 `IMAGE_TASK_MAX_RETRIES=2`、`IMAGE_TASK_RETRY_BACKOFF_SECONDS=10,30`(或等价配置),同步 `.env.example`、`env.md`、`deployment.md`;生产要提醒最坏耗时约 `上游硬截止 * 3 + backoff`,桌面端轮询超时需覆盖。**接口兼容**:轮询响应可新增可选字段 `attempt_count`、`max_attempts`、`next_attempt_at`,但不得删除既有字段;旧客户端忽略新字段仍可工作。**日志**:扩展 `event=image_task_processed`,包含 `attempt`、`max_attempts`、`retrying`、`next_attempt_at`、`duration_ms`、`error_code`,不得记录 prompt、base64、provider raw 或密钥。**账务验收**:第 1/2 次 `upstream_timeout` 回到 queued 且不退点;第 3 次成功只扣一次并成功确认;连续 3 次失败只退一次;非重试错误立即失败并只退一次;幂等提交仍返回同一 task,不重复预扣;reaper 与迟到 worker 不得把已最终失败/已退款任务改回成功。**测试**:覆盖上述账务、状态机、backoff 抢占、轮询新增字段兼容、worker 日志字段和敏感信息不泄露;`makemigrations` / `migrate` / `check` / 目标测试 / `init` 通过并在 `../progress.md` 留证据 | DONE | +| T-617 | 桌面端版本检查接口增加文件大小字段 | T-607, T-609 | 在 `GET /api/v1/client/releases/latest?platform=windows` 的当前版本响应中,为 `release` 对象新增 `size_bytes` 字段,用于桌面端下载后比对文件大小。**接口兼容**:只新增字段,不删除既有 `version` / `download_url` / `sha256` / `release_notes` / `force_update` / `published_at`;无当前版本或无下载地址时仍返回 `release:null`,不返回顶层 `size_bytes`。**数据来源**:`DownloadRelease` 新增 `size_bytes` 可空正整数字段,单位字节,django-admin 可填写并在列表展示;老数据为空时接口返回 `null`,客户端只在值为正整数时做大小校验。**安全**:接口仍公开匿名只读,不读取用户、不扣点、不暴露后台 ID、本地文件路径或内部状态。**文档**:同步 `api.md`、`routes.md`、`04-architecture.md`、`02-requirements.md`、`current-state.md`。**测试**:覆盖有值返回整数、未配置返回 `null`、未发布响应不返回顶层字段、响应字段白名单更新、admin 字段可见;`makemigrations --check` / `check` / 目标测试通过并在 `../progress.md` 留证据 | DONE | + ## 里程碑 - M1:Django + admin 骨架可运行、自定义 User 就位(T-003)。 @@ -110,6 +112,7 @@ - M10:用户端品牌名统一为“虾皮圈”(T-611)。 - M11:生图异步任务化(先止血、后共享 core、再新老接口共存并观察旧路用量,T-612~T-615)。 - M12:生图失败自动重试(T-616)。 +- M13:桌面端版本文件大小校验元数据(T-617)。 ## 待办池(Backlog) diff --git a/docs/api.md b/docs/api.md index 9745306..d8b55b3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -51,7 +51,7 @@ T-606 已实现公开首页与客户端下载入口:`GET /` 匿名返回 200 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`。 +T-607/T-609/T-617 已实现 `GET /api/v1/client/releases/latest?platform=windows`,给桌面端自动检查更新使用。该接口公开匿名可访问,不需要 API Key,不读取用户、不扣点、不占用生成接口限流;只返回 `DownloadRelease` 的公开发布元数据。`release.force_update` 表示该版本是否必须升级,来源于后台 `DownloadRelease.force_update`,默认 `false`;`release.size_bytes` 表示安装包文件大小字节数,来源于后台 `DownloadRelease.size_bytes`,用于桌面端下载后校验文件大小。 通用错误响应: @@ -122,6 +122,7 @@ GET /api/v1/client/releases/latest?platform=windows "sha256": "64位sha256", "release_notes": "优化了ai模块的生图的功能", "force_update": true, + "size_bytes": 45678901, "published_at": "2026-07-06T18:00:00+08:00" } } @@ -137,9 +138,9 @@ GET /api/v1/client/releases/latest?platform=windows } ``` -字段说明:`force_update` 为布尔值,表示桌面端是否必须升级到该版本;未勾选时返回 `false`。客户端可先做版本号比较,再按 `force_update=true` 阻止继续使用旧版本或进入强制升级流程。 +字段说明:`force_update` 为布尔值,表示桌面端是否必须升级到该版本;未勾选时返回 `false`。`size_bytes` 为整数或 `null`,单位字节;后台未填写时返回 `null`,客户端应只在该值为正整数时校验下载后的本地文件大小。客户端可先做版本号比较,再按 `force_update=true` 阻止继续使用旧版本或进入强制升级流程,下载完成后同时校验 `size_bytes` 和 `sha256`。 -要点:接口只查 `DownloadRelease(platform, is_current=True)`;`download_url` 优先使用 `external_url`,否则用 `file.url` 生成绝对 HTTPS URL;`published_at` MVP 可使用 `DownloadRelease.updated_at`;`force_update` 使用后台发布版本上的布尔配置,默认 `false`。响应不得包含本地 `MEDIA_ROOT`、文件系统路径、后台 ID、`is_current`、用户信息、API Key、模型配置或任何密钥字段。成功和“暂未发布”均返回 HTTP 200,方便桌面端静默检查;非法平台返回 `400 bad_request`。无当前版本或当前版本没有下载地址时返回 `release:null`,不返回独立的 `force_update` 顶层字段。 +要点:接口只查 `DownloadRelease(platform, is_current=True)`;`download_url` 优先使用 `external_url`,否则用 `file.url` 生成绝对 HTTPS URL;`published_at` MVP 可使用 `DownloadRelease.updated_at`;`force_update` 使用后台发布版本上的布尔配置,默认 `false`;`size_bytes` 使用后台填写的安装包字节数,可为空。响应不得包含本地 `MEDIA_ROOT`、文件系统路径、后台 ID、`is_current`、用户信息、API Key、模型配置或任何密钥字段。成功和“暂未发布”均返回 HTTP 200,方便桌面端静默检查;非法平台返回 `400 bad_request`。无当前版本或当前版本没有下载地址时返回 `release:null`,不返回独立的 `force_update` / `size_bytes` 顶层字段。 ### `POST /api/v1/generate/title` diff --git a/docs/current-state.md b/docs/current-state.md index 7afd134..fee1b15 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -12,10 +12,12 @@ ## 当前快照 - 日期:2026-07-09 -- 阶段: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 首页导入模板下载入口、T-611 用户端品牌名统一为虾皮圈、T-612 生图同步接口止血、T-613 抽生成核心 service、T-614 生图异步任务化接口、T-615 旧同步生图接口遥测 / 弃用口径与 T-616 生图失败自动重试 2 次已完成;后续仍需处理真实支付回调到账闭环、客户端下载包发布和生产侧旧同步接口用量观察 +- 阶段: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 首页导入模板下载入口、T-611 用户端品牌名统一为虾皮圈、T-612 生图同步接口止血、T-613 抽生成核心 service、T-614 生图异步任务化接口、T-615 旧同步生图接口遥测 / 弃用口径、T-616 生图失败自动重试 2 次与 T-617 桌面端版本文件大小字段已完成;后续仍需处理真实支付回调到账闭环、客户端下载包发布和生产侧旧同步接口用量观察 - 技术栈:系统 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 可编辑和筛选;T-610 已新增 `ImportTemplate` 模型/admin/迁移 `portal.0003_importtemplate`,首页读取当前模板并在“下载客户端”旁展示“下载导入模板”,本地文件 URL 转为当前站点绝对 URL,`external_url` 优先;T-611 已把用户端 portal 可见品牌名统一为“虾皮圈”,包括页面标题、顶部导航、首页 H1、用户端“虾皮圈 API Key”文案和 allauth 邮件模板;T-612 已新增 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS`,生图 Provider 上游请求和上游返回图片 URL 下载会按 `min(AiModel.timeout_seconds 或分辨率默认值, 硬截止)` 控制读取超时,超时返回 `upstream_timeout` 并走既有失败退点路径;T-613 已把旧同步生成链路抽成 `GenerationInput`、`prepare_generation()`、`precharge_generation()`、`execute_precharged_generation()` 与 `GenerationResult`,旧 view 只负责 serializer 和异常转 HTTP,后续异步 worker 可复用已预扣执行 / 确认 / 退点阶段。 - T-614/T-616 生产代码补充:已新增 `ImageGenerationTask` 与 `api.0001_initial`,新增 `POST /api/v1/generate/image/tasks`、`GET /api/v1/generate/image/tasks/{task_id}`、`apps.api.image_tasks` 任务服务、`run_image_tasks` management command 和只读 admin;异步提交支持 `Idempotency-Key` 去重 / 冲突检测,worker 使用 DB 任务表、租约、心跳与 reaper,成功返回 cmhub 托管 URL;T-616 已通过 `api.0002_imagegenerationtask_next_attempt_at_and_more` 增加 `next_attempt_at` 与 `(status, next_attempt_at)` 索引,临时性 `upstream_timeout` / `upstream_error` 默认最多重试 2 次,重试期间不退点,最终失败 / 僵任务才走计费层幂等退款。 +- T-617 生产代码补充:`DownloadRelease` 已新增 `size_bytes` 可空正整数字段和迁移 `portal.0004_downloadrelease_size_bytes`,django-admin 可填写并在列表展示;`GET /api/v1/client/releases/latest` 的 `release` 对象新增 `size_bytes`,有值返回整数,未配置返回 `null`,未发布时仍只返回 `release:null`。 +- 最新验证:T-617 桌面端版本文件大小字段已验证 `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.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` 通过,12 tests OK,覆盖 `size_bytes` 有值、未配置为 `null`、未发布不返回顶层字段、响应字段白名单和 admin 字段可见;`py -3.12 manage.py migrate portal --noinput` 120 秒超时,但 `py -3.12 manage.py showmigrations portal` 确认 `portal.0004_downloadrelease_size_bytes` 已应用,且无残留 migrate 进程。测试期仍保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 - T-614 实现取舍补充:worker 执行前会基于任务快照复跑 `prepare_generation()`,即复审 prompt 并重解析别名 / Provider / 定价;账务使用已预扣 `CallRecord.points_cost`,不会重复扣点。短队列下这是偏安全取舍,词库变更后排队任务仍可被拦截并退款;若后续队列积压或频繁切换模型,应单独做提交时模型配置快照。异步 submit 对 `image_base64` 只解码并保存输入文件,桌面端主链路推荐继续使用;`image_url` 会在 submit 阶段完成 SSRF 校验、远程下载和大小限制,可能阻塞提交请求,属于边缘兼容路径。 - T-615 生产代码补充:已新增 `apps.api.telemetry`,旧同步 `POST /api/v1/generate/image` 与新异步提交 `POST /api/v1/generate/image/tasks` 都写 `cmhub.api.generation_usage` 结构化日志事件 `generation_route_usage`;字段白名单为 `route_type`、API Key ID/前缀、`user_id`、`X-Client-Version`、别名、状态、耗时、错误码和 HTTP 状态,不记录 API Key 明文、prompt、图片 base64 或 provider raw。 - 异步生图 worker 排障补充:`run_image_tasks` 每处理一个任务会输出 `event=image_task_processed task_id=... status=... alias=... attempt=... max_attempts=... retrying=... next_attempt_at=... duration_ms=...`,重试中 / 失败 / 过期任务额外输出 `error_code`;日志不包含 prompt、`image_base64`、provider raw 或密钥。生产扩容 worker 应按 2、4、8、16 逐级观察队列长度、耗时、错误码、MySQL 连接数和上游失败率,不建议直接扩到 100。 diff --git a/docs/routes.md b/docs/routes.md index 6498ab6..9b3480b 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -36,7 +36,7 @@ T-606 已落地 `/` 公开首页:匿名访问返回 200,不再重定向到 ` | `/api/v1/recharge/callback/wechat` | POST | 微信 V3 异步回调 | 验签(`@csrf_exempt`) | | `/api/v1/recharge/callback/alipay` | POST | 支付宝异步回调 | 验签(`@csrf_exempt`) | -T-607/T-609 已落地 `/api/v1/client/releases/latest`:公开匿名可访问,不需要 API Key,不读取用户账本,只返回当前 `DownloadRelease` 的版本、下载 URL、SHA256、发布说明、强制更新标记和发布时间;无当前版本返回 `release:null`。 +T-607/T-609/T-617 已落地 `/api/v1/client/releases/latest`:公开匿名可访问,不需要 API Key,不读取用户账本,只返回当前 `DownloadRelease` 的版本、下载 URL、SHA256、发布说明、强制更新标记、文件大小字节数和发布时间;无当前版本返回 `release:null`。 T-614 已落地 `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks/{task_id}`:新版桌面端可先提交异步生图任务,再轮询状态;旧 `/api/v1/generate/image` 同步接口继续保留。异步查询只允许同一用户访问自己的任务,跨用户按不存在处理。 ## 运营后台(django-admin,`/admin/`) @@ -57,7 +57,7 @@ T-614 已落地 `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks | 模型配置 | AiModel | 维护上游模型(url/model/api_key[加密脱敏]/api_type/capabilities/timeout) | | 能力别名 | ModelAlias | 维护对外别名 → 具体模型的映射;换供应商在此改指向 | | 配置审计 | AiConfigAuditLog | 只读查看 AiModel / ModelAlias / 密钥变更:谁、何时、改了什么 | -| 客户端下载版本 | DownloadRelease | 上传安装包或填写外部下载地址,标记每个平台当前版本与是否强制更新;展示版本、SHA256、强制更新标记与发布说明 | +| 客户端下载版本 | DownloadRelease | 上传安装包或填写外部下载地址,标记每个平台当前版本、是否强制更新和文件大小字节数;展示版本、SHA256、强制更新标记与发布说明 | | 导入模板 | ImportTemplate | 上传导入模板或填写外部下载地址,标记当前模板;展示模板名称、SHA256 与说明 | ## 后台职责约定 diff --git a/progress.md b/progress.md index c82e05e..c8a1634 100644 --- a/progress.md +++ b/progress.md @@ -1840,3 +1840,24 @@ - 决策: - T-616 第一版不重试敏感词、余额不足、未定价、模型 / 别名不可用、参数错误和 Provider 能力错误;这些属于确定性失败,立即失败并退款或在提交阶段不扣点。 - 重试期间不展示 `error` 给轮询客户端,客户端只需看到 `queued + next_attempt_at` 后继续轮询;最终 `failed` 才表示已退款。 + +## 2026-07-13 实施:T-617 桌面端版本检查接口增加文件大小字段 + +- 状态:DONE。 +- 背景:桌面端请求 `GET /api/v1/client/releases/latest?platform=windows` 后,需要拿到安装包文件大小,下载完成后与本地文件大小比对,辅助发现下载不完整或文件异常。 +- 代码变更: + - `apps/portal/models.py` / `apps/portal/migrations/0004_downloadrelease_size_bytes.py`:`DownloadRelease` 新增 `size_bytes` 可空正整数字段,单位字节,老发布记录可保持空值。 + - `apps/portal/admin.py`:客户端下载版本 admin 的下载配置区新增 `size_bytes`,列表页同步展示,便于运营填写和核对。 + - `apps/api/views.py`:`GET /api/v1/client/releases/latest` 的 `release` 对象新增 `size_bytes`;有值返回整数,未配置返回 `null`;无当前版本或无下载地址时仍返回 `release:null`。 + - `apps/api/tests.py`:补充版本接口字段白名单、有值返回、空值返回、未发布不返回顶层字段和 admin 字段可见测试。 +- 文档变更: + - `docs/api.md`:更新响应示例、字段说明和客户端校验建议。 + - `docs/04-architecture.md`:同步 `DownloadRelease.size_bytes` schema、公开版本检查接口元数据口径。 + - `docs/02-requirements.md` / `docs/routes.md` / `docs/06-tasks.md` / `docs/current-state.md` / `README.md`:同步 T-617 状态和桌面端文件大小校验口径。 +- 验证: + - `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.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2`:通过,12 tests OK。 + - `py -3.12 manage.py migrate portal --noinput`:命令 120 秒超时,但随后 `py -3.12 manage.py showmigrations portal` 显示 `portal.0004_downloadrelease_size_bytes` 已应用;未发现残留 `manage.py migrate` 进程。 +- 测试环境现象: + - 测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。