From a00ffd8520ae0f25397cf443591a701ec167b04c Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 6 Jul 2026 15:45:48 +0800 Subject: [PATCH] feat: include account info in balance response --- apps/api/tests.py | 13 +++++++++++++ apps/api/views.py | 8 +++++++- docs/api.md | 10 ++++++++-- docs/current-state.md | 2 +- progress.md | 18 ++++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/apps/api/tests.py b/apps/api/tests.py index 405c8e3..27837d6 100644 --- a/apps/api/tests.py +++ b/apps/api/tests.py @@ -180,6 +180,7 @@ class BalanceApiTests(TestCase): username=f"balance-user-{suffix}", email=f"balance-user-{suffix}@example.com", password="password", + first_name="主账号", ) self.api_key, self.raw_key = ApiKey.create_for_user(self.user, name="balance") self.client = APIClient() @@ -225,12 +226,24 @@ class BalanceApiTests(TestCase): self.assertEqual(response.data["user"], self.user.username) self.assertEqual(response.data["points_balance"], 100) self.assertEqual(response.data["points_balance"], ledger_sum) + self.assertEqual( + response.data["account"], + { + "username": self.user.username, + "display_name": "主账号", + }, + ) + self.assertNotIn("email", response.data["account"]) + self.assertNotIn("id", response.data["account"]) def test_balance_returns_zero_without_creating_missing_wallet(self): response = self.client.get(self.url, **self.auth_header()) self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["user"], self.user.username) self.assertEqual(response.data["points_balance"], 0) + self.assertEqual(response.data["account"]["username"], self.user.username) + self.assertEqual(response.data["account"]["display_name"], "主账号") self.assertFalse(UserWallet.objects.filter(user=self.user).exists()) def test_balance_does_not_accept_web_session_without_api_key(self): diff --git a/apps/api/views.py b/apps/api/views.py index 1ea5b3d..acdec8a 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -109,10 +109,16 @@ class GenerateImageView(ExternalApiView): class BalanceView(ExternalApiView): def get(self, request): balance = get_balance_snapshot(request.user) + username = request.user.get_username() + display_name = request.user.get_full_name() or username return Response( { - "user": request.user.get_username(), + "user": username, "points_balance": balance.points_balance, + "account": { + "username": username, + "display_name": display_name, + }, }, status=status.HTTP_200_OK, ) diff --git a/docs/api.md b/docs/api.md index 4b55b8f..1231121 100644 --- a/docs/api.md +++ b/docs/api.md @@ -21,7 +21,7 @@ T-301 已实现对外 API 鉴权基线:`apps.api.authentication.ApiKeyAuthenti T-302 已实现生成接口基线:`POST /api/v1/generate/title` 与 `POST /api/v1/generate/image` 已接入 API Key 鉴权、别名解析、计费规则、预扣点、Provider 调用、成功确认和失败退点;图片结果当前以本地 `MEDIA_ROOT` 保存并返回 `image_url`,后续可替换为对象存储。 -T-303 已实现余额查询基线:`GET /api/v1/balance` 已接入 API Key 鉴权,返回当前 `UserWallet.points_balance`;测试覆盖响应余额与 `PointsLedger.points_delta` 累加值一致的账务场景,并确认 Web session 不能调用该外部接口。 +T-303 已实现余额查询基线:`GET /api/v1/balance` 已接入 API Key 鉴权,返回当前 `UserWallet.points_balance`,并保留旧字段同时新增不含邮箱的 `account` 账号展示对象;测试覆盖响应余额与 `PointsLedger.points_delta` 累加值一致的账务场景,并确认 Web session 不能调用该外部接口。 T-601 已实现可用别名发现:`GET /api/v1/models` 已接入 API Key 鉴权,只返回当前 active 且具备对应能力的公开别名、能力、是否需要原图和点数单价;接口不调用 `AiModel.to_resolved_model()`,不解密 provider key,不返回底层 SKU、模型 URL、`api_key_encrypted`、`extra_body` 等内部配置,且成功请求不占用生成接口限流额度。 @@ -141,10 +141,16 @@ T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session + ```json { "user": "demo-client", - "points_balance": 88 + "points_balance": 88, + "account": { + "username": "demo-client", + "display_name": "主账号" + } } ``` +要点:`user` 与 `points_balance` 是旧客户端兼容字段;`account` 供桌面端测试连接时展示账号信息。`account.display_name` 取用户姓名,未设置时回退用户名;不返回邮箱、数据库用户 ID 或其他个人敏感字段。 + ### `GET /api/v1/models` 查询当前可调用的能力别名。成功响应: diff --git a/docs/current-state.md b/docs/current-state.md index 74c18e2..821c0ff 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -108,7 +108,7 @@ gunicorn config.wsgi:application --bind 127.0.0.1:8002 --workers 1 --worker-clas - `GET /records/recharge` - `GET /records/usage` -当前骨架可运行。T-002 已在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4;远程 MySQL 已完成 Django 初始迁移。T-003 已接通 django-admin,测试可创建/销毁 `test_cmhub` 测试库;当前远程 MySQL 对频繁建库/销库仍可能间歇超时,必要时用 `--keepdb` 且串行跑测试。T-004 已应用 `users.0002_alter_user_email`,`user.email` 已有唯一索引。T-101 的 AI provider 层只做 HTTP 调用与响应解析;T-102 已把 provider 运行配置接到数据库 `AiModel` / `ModelAlias`,`resolve_alias()` 每次查当前 active 配置并按 `text` / `image` 能力校验。T-103 已补 `AiConfigAuditLog`,admin 保存/删除 `AiModel` / `ModelAlias` 时记录 actor、action、target、changed_fields、changes、created_at,密钥只记录 empty/set 状态。T-104/T-105 已用临时回滚配置跑通录制标题和录制图片生成。T-201 已落地钱包、API Key、点数流水和调用记录:API Key 明文只在创建 helper 返回,库内只存 hash/prefix;CallRecord 只存 `result_ref`/`result_summary`,没有 provider raw 字段。T-202 已落地 `PricingRule` / `ExchangeRate`:计费按 `operation_type + alias + resolution` 查 active 规则,优先精确分辨率,再回退默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`;金额换点数按当前 active 汇率向下取整。T-203 已落地 `precharge_call()` / `mark_call_success()` / `refund_call_points()`:预扣锁钱包行,余额不足不写调用/流水;失败退点锁调用记录并幂等写 refund 流水。T-204 已完成复合唯一约束加固,并取得一次完整 `manage.py test` 单次全绿。T-301 已落地 `Authorization: Bearer ` 鉴权:成功后 `request.user` 为所属用户、`request.auth` 为 `ApiKey`,缺失/无效 Key 返回 401,用户或 Key 禁用返回 403,外部 API 不接受 Web session。T-302 已落地生成接口:请求别名解析后按规则计费,预扣成功才调用 Provider,成功确认调用记录,`AiProviderError` / `AiCapabilityError` 等失败路径会退点;图片结果保存到本地 media 并返回 URL。T-303 已落地余额查询接口:`GET /api/v1/balance` 继承外部 API Key 鉴权,读取 billing 余额快照并返回 `user` 与 `points_balance`,测试覆盖余额与流水累加一致。T-304 已落地充值回调:`RechargeOrder` 保存下单锁定的金额/汇率/点数,微信/支付宝回调先验签再按订单幂等入账,重复回调不重复加点,金额不一致不入账;主动查单兜底可调用 `query_and_apply_recharge_payment(order_no, query_func)` 复用同一入账路径。T-305 已落地扫码下单与轮询:用户端 session 登录后可 `POST /api/v1/recharge/create` 创建 pending 订单并拿到 mock/SDK 二维码票据,`GET /api/v1/recharge/status` 只返回本人订单并在 pending 时尝试主动查单补入账;API Key 不能调用这两个用户端接口。T-306 已落地对外 API 安全加固:`image_url` 下载在扣点前做协议白名单、公网地址校验、重定向逐跳校验和响应大小上限;DRF 全局默认不再隐式启用 Session/Basic;生成接口按 Key 限流,认证失败按 IP 限流;充值下单有单笔金额上限。T-501/T-605 已落地 allauth 注册 / 登录:`ACCOUNT_EMAIL_VERIFICATION="none"`,免邮箱验证、注册即可用,邮箱仍必填且唯一;注册成功创建 0 点钱包、不写点数流水。T-502 已落地 API Key 自助管理:`/apikeys` 登录访问,生成后完整明文只显示一次,列表只显示 prefix,不显示 hash 或历史明文;删除为吊销 `revoked`,吊销后外部 API 返回 403。T-503 已落地个人中心与记录页:`/dashboard` 展示剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 展示当前用户充值订单;`/records/usage` 展示当前用户 consume/refund 点数流水并关联调用信息;所有页面均只读且只查本人。T-504 已落地充值页:`/recharge` GET 展示余额、充值表单、当前订单和最近充值,POST 创建 pending 订单并展示二维码票据,浏览器轮询 `/api/v1/recharge/status`,paid 后刷新页面重新读取余额;页面不直接写钱包或流水。T-401 已落地运营后台完善:用户列表显示钱包余额,钱包余额只读且通过专用表单手工调点,调点必须填原因、非 0、不得扣成负数,并经 `adjust_wallet_points()` 锁钱包写 `PointsLedger(adjust)`;API Key admin 只展示 prefix 和 hash 摘要,不回显明文或完整 hash;订单、流水、调用记录继续只读并增强检索。T-402 已完成 MVP P0 验收并新增 `docs/mvp-acceptance.md`。T-403 已完成部署 / 运行文档,生产按 `deployment.md` 执行,并已补 settings 对生产静态目录、共享 cache、CSRF trusted origins、HTTPS cookie/proxy/HSTS 的环境变量支持。T-601 已落地可用别名发现:`GET /api/v1/models` 用 API Key 鉴权返回公开别名目录,`/models` 用 session 展示只读「可用模型」页;两者均不解密 provider key,不输出底层 SKU、URL、key 或 `extra_body`。T-604 已落地本地 prompt 敏感词过滤:`MODERATION_ENABLED=false` 默认 no-op,启用 `keyword` 后命中返回 `content_blocked`,并在下载 `image_url`、解析别名、计费、预扣点和上游调用前拦截。真实上游生成未执行,原因是当前环境未配置 `AI_KEY_ENCRYPTION_KEY` 且数据库没有 AiModel/ModelAlias;后续配置后可用 `import_ai_models` 导入,再通过接口跑真实标题/图片并记录图片耗时。 +当前骨架可运行。T-002 已在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4;远程 MySQL 已完成 Django 初始迁移。T-003 已接通 django-admin,测试可创建/销毁 `test_cmhub` 测试库;当前远程 MySQL 对频繁建库/销库仍可能间歇超时,必要时用 `--keepdb` 且串行跑测试。T-004 已应用 `users.0002_alter_user_email`,`user.email` 已有唯一索引。T-101 的 AI provider 层只做 HTTP 调用与响应解析;T-102 已把 provider 运行配置接到数据库 `AiModel` / `ModelAlias`,`resolve_alias()` 每次查当前 active 配置并按 `text` / `image` 能力校验。T-103 已补 `AiConfigAuditLog`,admin 保存/删除 `AiModel` / `ModelAlias` 时记录 actor、action、target、changed_fields、changes、created_at,密钥只记录 empty/set 状态。T-104/T-105 已用临时回滚配置跑通录制标题和录制图片生成。T-201 已落地钱包、API Key、点数流水和调用记录:API Key 明文只在创建 helper 返回,库内只存 hash/prefix;CallRecord 只存 `result_ref`/`result_summary`,没有 provider raw 字段。T-202 已落地 `PricingRule` / `ExchangeRate`:计费按 `operation_type + alias + resolution` 查 active 规则,优先精确分辨率,再回退默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`;金额换点数按当前 active 汇率向下取整。T-203 已落地 `precharge_call()` / `mark_call_success()` / `refund_call_points()`:预扣锁钱包行,余额不足不写调用/流水;失败退点锁调用记录并幂等写 refund 流水。T-204 已完成复合唯一约束加固,并取得一次完整 `manage.py test` 单次全绿。T-301 已落地 `Authorization: Bearer ` 鉴权:成功后 `request.user` 为所属用户、`request.auth` 为 `ApiKey`,缺失/无效 Key 返回 401,用户或 Key 禁用返回 403,外部 API 不接受 Web session。T-302 已落地生成接口:请求别名解析后按规则计费,预扣成功才调用 Provider,成功确认调用记录,`AiProviderError` / `AiCapabilityError` 等失败路径会退点;图片结果保存到本地 media 并返回 URL。T-303 已落地余额查询接口:`GET /api/v1/balance` 继承外部 API Key 鉴权,读取 billing 余额快照并返回兼容字段 `user` / `points_balance`,以及不含邮箱的 `account.username` / `account.display_name`,测试覆盖余额与流水累加一致。T-304 已落地充值回调:`RechargeOrder` 保存下单锁定的金额/汇率/点数,微信/支付宝回调先验签再按订单幂等入账,重复回调不重复加点,金额不一致不入账;主动查单兜底可调用 `query_and_apply_recharge_payment(order_no, query_func)` 复用同一入账路径。T-305 已落地扫码下单与轮询:用户端 session 登录后可 `POST /api/v1/recharge/create` 创建 pending 订单并拿到 mock/SDK 二维码票据,`GET /api/v1/recharge/status` 只返回本人订单并在 pending 时尝试主动查单补入账;API Key 不能调用这两个用户端接口。T-306 已落地对外 API 安全加固:`image_url` 下载在扣点前做协议白名单、公网地址校验、重定向逐跳校验和响应大小上限;DRF 全局默认不再隐式启用 Session/Basic;生成接口按 Key 限流,认证失败按 IP 限流;充值下单有单笔金额上限。T-501/T-605 已落地 allauth 注册 / 登录:`ACCOUNT_EMAIL_VERIFICATION="none"`,免邮箱验证、注册即可用,邮箱仍必填且唯一;注册成功创建 0 点钱包、不写点数流水。T-502 已落地 API Key 自助管理:`/apikeys` 登录访问,生成后完整明文只显示一次,列表只显示 prefix,不显示 hash 或历史明文;删除为吊销 `revoked`,吊销后外部 API 返回 403。T-503 已落地个人中心与记录页:`/dashboard` 展示剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 展示当前用户充值订单;`/records/usage` 展示当前用户 consume/refund 点数流水并关联调用信息;所有页面均只读且只查本人。T-504 已落地充值页:`/recharge` GET 展示余额、充值表单、当前订单和最近充值,POST 创建 pending 订单并展示二维码票据,浏览器轮询 `/api/v1/recharge/status`,paid 后刷新页面重新读取余额;页面不直接写钱包或流水。T-401 已落地运营后台完善:用户列表显示钱包余额,钱包余额只读且通过专用表单手工调点,调点必须填原因、非 0、不得扣成负数,并经 `adjust_wallet_points()` 锁钱包写 `PointsLedger(adjust)`;API Key admin 只展示 prefix 和 hash 摘要,不回显明文或完整 hash;订单、流水、调用记录继续只读并增强检索。T-402 已完成 MVP P0 验收并新增 `docs/mvp-acceptance.md`。T-403 已完成部署 / 运行文档,生产按 `deployment.md` 执行,并已补 settings 对生产静态目录、共享 cache、CSRF trusted origins、HTTPS cookie/proxy/HSTS 的环境变量支持。T-601 已落地可用别名发现:`GET /api/v1/models` 用 API Key 鉴权返回公开别名目录,`/models` 用 session 展示只读「可用模型」页;两者均不解密 provider key,不输出底层 SKU、URL、key 或 `extra_body`。T-604 已落地本地 prompt 敏感词过滤:`MODERATION_ENABLED=false` 默认 no-op,启用 `keyword` 后命中返回 `content_blocked`,并在下载 `image_url`、解析别名、计费、预扣点和上游调用前拦截。真实上游生成未执行,原因是当前环境未配置 `AI_KEY_ENCRYPTION_KEY` 且数据库没有 AiModel/ModelAlias;后续配置后可用 `import_ai_models` 导入,再通过接口跑真实标题/图片并记录图片耗时。 ## 开始编码前检查 diff --git a/progress.md b/progress.md index 7945ebf..5fc3eb5 100644 --- a/progress.md +++ b/progress.md @@ -1282,6 +1282,24 @@ - ID 说明:T-605 已用于「免邮箱验证」,故本任务用 T-606。 - 下一步:实现 T-606(路由改造 + DownloadRelease 模型/admin/迁移 + 首页/下载 SSR 模板 + 测试)。 +## 2026-07-06 变更:余额接口返回桌面端测试连接账号信息 + +- 背景:桌面端“测试连接”希望在余额点数外展示当前 API Key 所属账号。为兼容已有调用方,保留旧字段 `user` 与 `points_balance`,新增 `account` 对象;按决策不返回邮箱。 +- 文档先行:先更新 `docs/api.md` 的 `GET /api/v1/balance` 成功响应结构: + - `user`: 旧兼容字段,用户名。 + - `points_balance`: 当前点数余额。 + - `account.username`: 用户名。 + - `account.display_name`: 用户姓名;未设置时回退用户名。 + - 不返回 `email`、数据库用户 ID 或其他敏感字段。 +- 代码变更: + - `apps/api/views.py`:`BalanceView` 响应新增 `account`,`display_name = request.user.get_full_name() or username`。 + - `apps/api/tests.py`:更新 `BalanceApiTests`,断言旧字段保留、新字段存在,且 `account` 不含 `email` / `id`。 +- 验证: + - `py -3.12 -m py_compile apps\api\views.py apps\api\tests.py`:通过。 + - `py -3.12 manage.py check`:通过,0 issues。 + - `py -3.12 manage.py test apps.api.tests.BalanceApiTests --keepdb --noinput --verbosity 2`:通过,3 tests OK。 + - 测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。 + ## 2026-07-06 决策:T-606 首页视觉方向定为 v1「生成台」 - 背景:为 T-606(公开首页 + 客户端下载)出了两版 SVG 原型,写入 `prototypes/`: