feat: include account info in balance response

This commit is contained in:
QiuSW
2026-07-06 15:45:48 +08:00
parent c07ae8dcfc
commit a00ffd8520
5 changed files with 47 additions and 4 deletions
+13
View File
@@ -180,6 +180,7 @@ class BalanceApiTests(TestCase):
username=f"balance-user-{suffix}", username=f"balance-user-{suffix}",
email=f"balance-user-{suffix}@example.com", email=f"balance-user-{suffix}@example.com",
password="password", password="password",
first_name="主账号",
) )
self.api_key, self.raw_key = ApiKey.create_for_user(self.user, name="balance") self.api_key, self.raw_key = ApiKey.create_for_user(self.user, name="balance")
self.client = APIClient() self.client = APIClient()
@@ -225,12 +226,24 @@ class BalanceApiTests(TestCase):
self.assertEqual(response.data["user"], self.user.username) self.assertEqual(response.data["user"], self.user.username)
self.assertEqual(response.data["points_balance"], 100) self.assertEqual(response.data["points_balance"], 100)
self.assertEqual(response.data["points_balance"], ledger_sum) 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): def test_balance_returns_zero_without_creating_missing_wallet(self):
response = self.client.get(self.url, **self.auth_header()) response = self.client.get(self.url, **self.auth_header())
self.assertEqual(response.status_code, 200) 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["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()) self.assertFalse(UserWallet.objects.filter(user=self.user).exists())
def test_balance_does_not_accept_web_session_without_api_key(self): def test_balance_does_not_accept_web_session_without_api_key(self):
+7 -1
View File
@@ -109,10 +109,16 @@ class GenerateImageView(ExternalApiView):
class BalanceView(ExternalApiView): class BalanceView(ExternalApiView):
def get(self, request): def get(self, request):
balance = get_balance_snapshot(request.user) balance = get_balance_snapshot(request.user)
username = request.user.get_username()
display_name = request.user.get_full_name() or username
return Response( return Response(
{ {
"user": request.user.get_username(), "user": username,
"points_balance": balance.points_balance, "points_balance": balance.points_balance,
"account": {
"username": username,
"display_name": display_name,
},
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
+8 -2
View File
@@ -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-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` 等内部配置,且成功请求不占用生成接口限流额度。 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 ```json
{ {
"user": "demo-client", "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` ### `GET /api/v1/models`
查询当前可调用的能力别名。成功响应: 查询当前可调用的能力别名。成功响应:
File diff suppressed because one or more lines are too long
+18
View File
@@ -1282,6 +1282,24 @@
- ID 说明:T-605 已用于「免邮箱验证」,故本任务用 T-606。 - ID 说明:T-605 已用于「免邮箱验证」,故本任务用 T-606。
- 下一步:实现 T-606(路由改造 + DownloadRelease 模型/admin/迁移 + 首页/下载 SSR 模板 + 测试)。 - 下一步:实现 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「生成台」 ## 2026-07-06 决策:T-606 首页视觉方向定为 v1「生成台」
- 背景:为 T-606(公开首页 + 客户端下载)出了两版 SVG 原型,写入 `prototypes/`: - 背景:为 T-606(公开首页 + 客户端下载)出了两版 SVG 原型,写入 `prototypes/`: