feat: finish phase 4 portal review fixes
This commit is contained in:
@@ -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-504:用户可通过 allauth 自助注册、邮箱验证、登录、登出,扫码充值并轮询到账,生成 / 删除(吊销)API Key,并查看余额、充值总额、充值记录与消费记录。Phase 4 审核新增 T-505,下一步先处理用户端审核优化,再进入 T-401 运营后台完善。详见 [`docs/current-state.md`](docs/current-state.md)。
|
||||
Phase 2 计费核心已完成,Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端已完成 T-501~T-505:用户可通过 allauth 自助注册、邮箱验证、登录、登出,扫码充值并轮询到账,生成 / 删除(吊销)API Key,查看余额、充值总额、分页充值记录与分页消费记录;用户端静态资源已改为本地自托管。下一步进入 T-401 运营后台完善。详见 [`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) 第四节计费时序。
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2024 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
File diff suppressed because one or more lines are too long
+10
@@ -0,0 +1,10 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Ryan Day
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
+2835
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
<!doctype html>
|
||||
{% load static %}
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}cmhub{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="{% static 'portal/vendor/bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--cmhub-bg: #f6f7f9;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{% if page_obj and records_total_count %}
|
||||
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap mt-3">
|
||||
<div class="text-secondary small">
|
||||
共 {{ records_total_count }} 条,每页 {{ records_page_size }} 条,当前第 {{ page_obj.number }} / {{ paginator.num_pages }} 页
|
||||
</div>
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="记录分页">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item{% if not page_obj.has_previous %} disabled{% endif %}">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="page-link">上一页</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="page-item{% if not page_obj.has_next %} disabled{% endif %}">
|
||||
{% if page_obj.has_next %}
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="page-link">下一页</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "portal/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}充值 - cmhub{% endblock %}
|
||||
|
||||
@@ -159,7 +160,7 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js"></script>
|
||||
<script src="{% static 'portal/vendor/qrcode/qrcode.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const panel = document.querySelector("[data-recharge-order]");
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "portal/includes/pagination.html" %}
|
||||
{% else %}
|
||||
<div class="text-secondary">暂无充值记录</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "portal/includes/pagination.html" %}
|
||||
{% else %}
|
||||
<div class="text-secondary">暂无消费记录</div>
|
||||
{% endif %}
|
||||
|
||||
+98
-1
@@ -3,6 +3,7 @@ from decimal import Decimal
|
||||
|
||||
from allauth.account.models import EmailAddress
|
||||
from django.contrib.auth import get_user, get_user_model
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core import mail
|
||||
from django.core.cache import cache
|
||||
from django.test import Client, TestCase, override_settings
|
||||
@@ -377,6 +378,43 @@ class PortalAccountFlowTests(TestCase):
|
||||
self.assertContains(response, "R-OWN-PENDING-503")
|
||||
self.assertNotContains(response, "R-OTHER-RECHARGE-503")
|
||||
|
||||
def test_recharge_records_paginate_more_than_page_size(self):
|
||||
user = self.create_verified_user()
|
||||
other_user = self.create_verified_user()
|
||||
self.create_recharge_order(
|
||||
other_user,
|
||||
order_no="R-PAGE-OTHER-505",
|
||||
amount="99.00",
|
||||
points=990,
|
||||
)
|
||||
for index in range(55):
|
||||
self.create_recharge_order(
|
||||
user,
|
||||
order_no=f"R-PAGE-{index:03d}",
|
||||
amount="1.00",
|
||||
points=10,
|
||||
)
|
||||
self.client.force_login(user)
|
||||
|
||||
first_page = self.client.get("/records/recharge")
|
||||
second_page = self.client.get("/records/recharge?page=2")
|
||||
|
||||
self.assertEqual(first_page.status_code, 200)
|
||||
self.assertEqual(first_page.context["records_total_count"], 55)
|
||||
self.assertEqual(first_page.context["page_obj"].number, 1)
|
||||
self.assertEqual(len(first_page.context["recharge_orders"]), 50)
|
||||
self.assertContains(first_page, "共 55 条,每页 50 条")
|
||||
self.assertContains(first_page, "R-PAGE-054")
|
||||
self.assertNotContains(first_page, "R-PAGE-000")
|
||||
self.assertNotContains(first_page, "R-PAGE-OTHER-505")
|
||||
|
||||
self.assertEqual(second_page.status_code, 200)
|
||||
self.assertEqual(second_page.context["page_obj"].number, 2)
|
||||
self.assertEqual(len(second_page.context["recharge_orders"]), 5)
|
||||
self.assertContains(second_page, "R-PAGE-000")
|
||||
self.assertNotContains(second_page, "R-PAGE-054")
|
||||
self.assertNotContains(second_page, "R-PAGE-OTHER-505")
|
||||
|
||||
def test_usage_records_require_login_and_only_show_current_user_usage(self):
|
||||
user = self.create_verified_user()
|
||||
other_user = self.create_verified_user()
|
||||
@@ -435,6 +473,59 @@ class PortalAccountFlowTests(TestCase):
|
||||
self.assertNotContains(response, "other-alias")
|
||||
self.assertNotContains(response, other_key.key_prefix)
|
||||
|
||||
def test_usage_records_paginate_more_than_page_size(self):
|
||||
user = self.create_verified_user()
|
||||
other_user = self.create_verified_user()
|
||||
api_key, _raw_key = ApiKey.create_for_user(user, name="desktop")
|
||||
other_key, _other_raw_key = ApiKey.create_for_user(other_user, name="other")
|
||||
other_call = self.create_call_record(
|
||||
other_user,
|
||||
other_key,
|
||||
alias="other-page-alias",
|
||||
points_cost=99,
|
||||
)
|
||||
PointsLedger.objects.create(
|
||||
user=other_user,
|
||||
change_type=PointsLedger.ChangeType.CONSUME,
|
||||
points_delta=-99,
|
||||
balance_after=0,
|
||||
ref_call=other_call,
|
||||
)
|
||||
for index in range(55):
|
||||
call = self.create_call_record(
|
||||
user,
|
||||
api_key,
|
||||
alias=f"title-page-{index:03d}",
|
||||
points_cost=1,
|
||||
)
|
||||
PointsLedger.objects.create(
|
||||
user=user,
|
||||
change_type=PointsLedger.ChangeType.CONSUME,
|
||||
points_delta=-1,
|
||||
balance_after=100 - index,
|
||||
ref_call=call,
|
||||
)
|
||||
self.client.force_login(user)
|
||||
|
||||
first_page = self.client.get("/records/usage")
|
||||
second_page = self.client.get("/records/usage?page=2")
|
||||
|
||||
self.assertEqual(first_page.status_code, 200)
|
||||
self.assertEqual(first_page.context["records_total_count"], 55)
|
||||
self.assertEqual(first_page.context["page_obj"].number, 1)
|
||||
self.assertEqual(len(first_page.context["usage_entries"]), 50)
|
||||
self.assertContains(first_page, "共 55 条,每页 50 条")
|
||||
self.assertContains(first_page, "title-page-054")
|
||||
self.assertNotContains(first_page, "title-page-000")
|
||||
self.assertNotContains(first_page, "other-page-alias")
|
||||
|
||||
self.assertEqual(second_page.status_code, 200)
|
||||
self.assertEqual(second_page.context["page_obj"].number, 2)
|
||||
self.assertEqual(len(second_page.context["usage_entries"]), 5)
|
||||
self.assertContains(second_page, "title-page-000")
|
||||
self.assertNotContains(second_page, "title-page-054")
|
||||
self.assertNotContains(second_page, "other-page-alias")
|
||||
|
||||
def test_recharge_page_requires_login_and_shows_form(self):
|
||||
user = self.create_verified_user()
|
||||
UserWallet.objects.create(user=user, points_balance=25)
|
||||
@@ -450,8 +541,14 @@ class PortalAccountFlowTests(TestCase):
|
||||
self.assertEqual(response.context["balance"].points_balance, 25)
|
||||
self.assertContains(response, "充值金额")
|
||||
self.assertContains(response, "创建订单")
|
||||
self.assertContains(response, "qrcode@1.5.4")
|
||||
self.assertContains(response, "portal/vendor/bootstrap/bootstrap.min.css")
|
||||
self.assertContains(response, "portal/vendor/qrcode/qrcode.js")
|
||||
self.assertContains(response, "QRCode.toCanvas")
|
||||
self.assertNotContains(response, "cdn.jsdelivr.net")
|
||||
self.assertIsNotNone(
|
||||
finders.find("portal/vendor/bootstrap/bootstrap.min.css")
|
||||
)
|
||||
self.assertIsNotNone(finders.find("portal/vendor/qrcode/qrcode.js"))
|
||||
|
||||
def test_recharge_page_post_creates_pending_order_without_crediting_wallet_or_ledger(self):
|
||||
user = self.create_verified_user()
|
||||
|
||||
+19
-2
@@ -1,5 +1,6 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Sum
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
@@ -74,6 +75,18 @@ def get_usage_ledger_entries_for_user(user):
|
||||
)
|
||||
|
||||
|
||||
def paginate_records(request, queryset):
|
||||
paginator = Paginator(queryset, RECORDS_PAGE_SIZE)
|
||||
page_obj = paginator.get_page(request.GET.get("page"))
|
||||
return {
|
||||
"paginator": paginator,
|
||||
"page_obj": page_obj,
|
||||
"is_paginated": page_obj.has_other_pages(),
|
||||
"records_total_count": paginator.count,
|
||||
"records_page_size": RECORDS_PAGE_SIZE,
|
||||
}
|
||||
|
||||
|
||||
class DashboardView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "portal/dashboard.html"
|
||||
|
||||
@@ -175,8 +188,10 @@ class RechargeRecordListView(LoginRequiredMixin, TemplateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
orders = get_recharge_orders_for_user(self.request.user)
|
||||
pagination = paginate_records(self.request, orders)
|
||||
context.update(get_portal_account_summary(self.request.user))
|
||||
context["recharge_orders"] = orders[:RECORDS_PAGE_SIZE]
|
||||
context.update(pagination)
|
||||
context["recharge_orders"] = pagination["page_obj"].object_list
|
||||
return context
|
||||
|
||||
|
||||
@@ -186,6 +201,8 @@ class UsageRecordListView(LoginRequiredMixin, TemplateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
entries = get_usage_ledger_entries_for_user(self.request.user)
|
||||
pagination = paginate_records(self.request, entries)
|
||||
context.update(get_portal_account_summary(self.request.user))
|
||||
context["usage_entries"] = entries[:RECORDS_PAGE_SIZE]
|
||||
context.update(pagination)
|
||||
context["usage_entries"] = pagination["page_obj"].object_list
|
||||
return context
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
## 当前阶段
|
||||
|
||||
当前项目处于:**Phase 4 审核优化收尾**。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 充值页。Phase 4 审核新增 T-505,下一步先处理用户端审核优化,再进入 T-401 运营后台完善。
|
||||
当前项目处于:**Phase 5 后台与发布**。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 用户端审核优化。下一步进入 T-401 运营后台完善。
|
||||
|
||||
优先路径:
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
2. Phase 1:最高风险功能原型 —— T-101/T-102/T-103/T-104/T-105 已完成 provider 层、模型配置表、别名解析、配置审计、录制标题/图片 smoke 与审核修补;真实图片同步耗时待配置 Fernet 主密钥、AiModel/ModelAlias 与真实上游后在 T-302/T-403 前补测。
|
||||
3. Phase 2:计费核心 —— T-201/T-202/T-203 已完成 UserWallet/ApiKey/PointsLedger/CallRecord、计费规则、汇率、计费计算、并发安全扣点与失败退点。
|
||||
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-505 完成后进入 T-401 运营后台完善,随后做完整验收与部署 / 运行文档。
|
||||
5. Phase 4:用户端(Django 模板 SSR)—— T-501 注册登录、T-502 API Key 管理、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成。
|
||||
6. Phase 5:后台与发布 —— 下一步 T-401 运营后台完善,随后做完整验收与部署 / 运行文档。
|
||||
|
||||
## 领取任务规则
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| Web 框架 | Django 5.2 LTS | 已定 | 自带 ORM、迁移、admin,适合「API + 运营后台」;选 LTS 维护到 2028,安全更新窗口最长。**禁用已 EOL 的 4.0/4.1**(无安全补丁,资金服务不可用) |
|
||||
| API 框架 | Django REST Framework (DRF) | 已定 | 鉴权、序列化、参数校验、限流现成 |
|
||||
| 运营后台 | django-admin | 已定 | 近零代码即得用户/点数/记录的增删改查与检索,省 80% 后台工作量 |
|
||||
| 用户端 | Django 模板 SSR + Bootstrap 5 + django-allauth | 已定 | T-501 已落地自助注册/登录/登出、邮箱验证与最小 dashboard;T-502 已落地 API Key 自助生成 / 删除页;T-503 已落地个人中心、充值记录和消费记录页;T-504 已落地充值页(创建订单、展示二维码票据、轮询到账后刷新);MVP 先用 Django form + Bootstrap 模板,不为简单表单引入 crispy-forms |
|
||||
| 用户端 | Django 模板 SSR + Bootstrap 5 + django-allauth | 已定 | T-501 已落地自助注册/登录/登出、邮箱验证与最小 dashboard;T-502 已落地 API Key 自助生成 / 删除页;T-503 已落地个人中心、充值记录和消费记录页;T-504 已落地充值页(创建订单、展示二维码票据、轮询到账后刷新);T-505 已把 Bootstrap 与 qrcode.js 改为本地 static 自托管,并给记录页加分页;MVP 先用 Django form + Bootstrap 模板,不为简单表单引入 crispy-forms |
|
||||
| 后台美化 | django-unfold 或 simpleui | 待定 | 仅外观,MVP 可先用原生 admin,后期按需引入 |
|
||||
| AI 上游对接 | **Provider 适配器层**(按 `api_type` 注册)+ **能力别名** 映射 + `requests` HTTP 客户端 | 已定 | 对外只暴露 `generate text/image` 两接口与别名;换供应商改后台映射,不动对外契约。移植 `cmbot` 的调用逻辑到各适配器。当前 3 模型机制不同:文本 chat、`nano-banana2` chat 多模态返图、`gpt-image-2` images/edits 改图(详见 `04` 3.1) |
|
||||
| 供应商密钥存储 | 应用层 Fernet 加密(`cryptography`) | 已定 | `AiModel.api_key_encrypted` 加密入库、admin 写入型字段不回显;加密主密钥 `AI_KEY_ENCRYPTION_KEY` 走环境变量,配置清单见 `env.md` |
|
||||
@@ -41,7 +41,7 @@
|
||||
- **同步可用的前提是「超时链路 + worker 容量」配对**,否则会「小量正常、上量假死」:① 每模型 `timeout_seconds` 设有限值(`ai_models.json` 现为 `0`,迁入须改);② Gunicorn `--timeout` 与网关 `proxy_read_timeout` 按最慢图片放大(别用默认 30s / 60s);③ worker/线程数按**峰值总并发**预留。详见 [架构设计](04-architecture.md) 第五节结论。
|
||||
- **何时转 V2 异步**:worker 被长连接占满拖慢快接口/后台、接入方总并发明显上涨、或需要「关窗重连/任务持久化」体验——在此之前保持同步。
|
||||
- **数据库:MySQL 8.4 LTS,cmhub 专用独立实例**:① 部署环境的 VPS 已装 MySQL 5.7 供其他服务用,但 5.7 跑不了 Django 5.2(需 ≥8.0.11)、已 EOL、且不支持 CHECK 约束,故**不复用**它;② 机器内存宽裕(`available` 7.4G),给 cmhub **单开一个 MySQL 8.4 LTS 实例**(独立端口/容器),与已有 5.7 完全隔离、互不影响;③ 选 8.4 LTS 取长维护窗口 + 完整 CHECK 约束(CHECK 需 MySQL ≥8.0.16 才真正生效);④ 强制 InnoDB + utf8mb4(5.7/老配置默认非 utf8mb4,prompt 的 emoji/生僻字会写失败);⑤ MySQL 默认隔离级别 REPEATABLE READ(不同于 PostgreSQL 的 READ COMMITTED),`select_for_update` 扣点仍安全,但计费实现按此语义验证;⑥ 开发环境同用 MySQL,不要用 SQLite——SQLite 会静默忽略 `FOR UPDATE`,并发扣点逻辑测不出来;⑦ 不在代码里写死只适配某一种库的 SQL。
|
||||
- **用户端用 Django 模板 SSR 单体,不引前端框架**:需求含终端用户自助(注册/充值/API Key/记录),选 Django 模板 + Bootstrap + allauth 与后端同工程单体部署,复用 Django auth/session,开发部署最快、最契合单机 MVP;代价是交互不如 SPA,可后续加 HTMX。T-501 已接入 django-allauth 65.18.0,使用邮箱验证、session 与 CSRF;注册成功只创建 0 点 `UserWallet`,不写赠点流水。T-502 已用 Django Form + Bootstrap 模板落地 `/apikeys`,生成 Key 后明文只显示一次,删除写为 `revoked`。T-503 已用只读 Django TemplateView 落地 dashboard 汇总、充值记录和消费记录。T-504 已用 Django Form + Bootstrap 模板落地 `/recharge`:页面 POST 创建 pending 充值订单,展示支付二维码票据,并用浏览器轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额。放弃 Vue/React 前后端分离(两套项目/部署,与单体 MVP 调性冲突)。用户模型:`User`(auth) 持登录态、`UserWallet` 持点数(扣点锁 wallet、与 auth 解耦)、`ApiKey`(User 1:N,哈希存储)。注册不送免费点数。
|
||||
- **用户端用 Django 模板 SSR 单体,不引前端框架**:需求含终端用户自助(注册/充值/API Key/记录),选 Django 模板 + Bootstrap + allauth 与后端同工程单体部署,复用 Django auth/session,开发部署最快、最契合单机 MVP;代价是交互不如 SPA,可后续加 HTMX。T-501 已接入 django-allauth 65.18.0,使用邮箱验证、session 与 CSRF;注册成功只创建 0 点 `UserWallet`,不写赠点流水。T-502 已用 Django Form + Bootstrap 模板落地 `/apikeys`,生成 Key 后明文只显示一次,删除写为 `revoked`。T-503 已用只读 Django TemplateView 落地 dashboard 汇总、充值记录和消费记录。T-504 已用 Django Form + Bootstrap 模板落地 `/recharge`:页面 POST 创建 pending 充值订单,展示支付二维码票据,并用浏览器轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额。T-505 已把 Bootstrap 5 CSS 与 qrcode.js vendoring 到 `apps/portal/static/portal/vendor/`,页面不再依赖 jsdelivr,充值/消费记录页改为 Django `Paginator` 分页。放弃 Vue/React 前后端分离(两套项目/部署,与单体 MVP 调性冲突)。用户模型:`User`(auth) 持登录态、`UserWallet` 持点数(扣点锁 wallet、与 auth 解耦)、`ApiKey`(User 1:N,哈希存储)。注册不送免费点数。
|
||||
|
||||
## 三、构建与运行命令
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
组件落位:
|
||||
|
||||
- **用户端层(Django 模板 SSR)**:注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值。入口 `apps/portal/`,用 session 鉴权。T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`;注册成功通过 allauth adapter 创建 0 点 `UserWallet`,不写赠点流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单与消费流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。
|
||||
- **用户端层(Django 模板 SSR)**:注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值。入口 `apps/portal/`,用 session 鉴权。T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`;注册成功通过 allauth adapter 创建 0 点 `UserWallet`,不写赠点流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单与消费流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/消费记录页从固定切片改为分页。
|
||||
- **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。
|
||||
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
|
||||
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
||||
@@ -46,7 +46,7 @@ T-304 已实现 `/api/v1/recharge/callback/wechat` 与 `/api/v1/recharge/callbac
|
||||
|
||||
T-305 已实现 `/api/v1/recharge/create` 与 `/api/v1/recharge/status`:两个端点走 `SessionAuthentication + IsAuthenticated`,属于用户端 session + CSRF 流程,不接受 API Key;创建订单时调用 `apps.billing.services.create_recharge_order()` 锁定当前汇率和预计到账点数,再经 `apps.billing.payment_gateways.create_payment_order()` 获取微信 native `code_url` 或支付宝当面付 `qr_code`;状态查询只允许订单所属用户访问,并在 pending 时尝试 `query_payment_order()` 主动查单补入账,查单不可用时保持 pending 等回调。
|
||||
|
||||
T-504 已实现用户端 `/recharge` 页面:GET 展示当前余额、充值表单、当前订单和最近充值;POST 经 `RechargeCreateForm` 校验金额与支付方式后复用 `create_recharge_order()` 创建 pending 订单并重定向到当前订单页,避免刷新重复下单;页面用 qrcode.js 渲染 `code_url`,同时保留可复制支付票据兜底;浏览器每秒轮询 `/api/v1/recharge/status`,订单 paid 后刷新页面重新读取余额。页面不直接写 `UserWallet.points_balance` 或 `PointsLedger`。
|
||||
T-504 已实现用户端 `/recharge` 页面:GET 展示当前余额、充值表单、当前订单和最近充值;POST 经 `RechargeCreateForm` 校验金额与支付方式后复用 `create_recharge_order()` 创建 pending 订单并重定向到当前订单页,避免刷新重复下单;页面用本地 static 自托管的 qrcode.js 渲染 `code_url`,同时保留可复制支付票据兜底;浏览器每秒轮询 `/api/v1/recharge/status`,订单 paid 后刷新页面重新读取余额。页面不直接写 `UserWallet.points_balance` 或 `PointsLedger`。
|
||||
|
||||
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` 控制单笔上限。
|
||||
|
||||
|
||||
+7
-3
@@ -65,15 +65,15 @@
|
||||
| T-502 | API Key 自助管理页 | T-501, T-301 | 登录用户生成/删除 Key;明文只显示一次、库内只存哈希;列表只显示 prefix;删除即吊销,吊销后该 Key 调用 403(无效/不存在 Key 仍为 401) | DONE |
|
||||
| T-503 | 个人中心 / 记录页 | T-501, T-203 | 剩余点数、充值总额、充值记录、消费(调用)记录;数据与流水一致;仅见本人 | DONE |
|
||||
| T-504 | 充值页(扫码 + 轮询到账) | T-501, T-305 | 发起充值→展示二维码→轮询订单状态→到账后余额刷新;到账以回调为权威 | DONE |
|
||||
| T-505 | Phase 4 用户端审核优化 | T-504 | 按 [`phase-4-review.md`](phase-4-review.md) 处理 **P2**(P2-1 Bootstrap/qrcode 自托管或加 SRI+crossorigin,断网 jsdelivr 后充值页仍可用;P2-2 记录页真正分页或明确截断提示,含 >50 条测试);P3 登记到 T-401/T-403 或 Backlog。**无 P1,但按任务看板领取规则应先完成 T-505,再进入 T-401**;`check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | TODO |
|
||||
| T-505 | Phase 4 用户端审核优化 | T-504 | 按 [`phase-4-review.md`](phase-4-review.md) 处理 **P2**(P2-1 Bootstrap/qrcode 自托管或加 SRI+crossorigin,断网 jsdelivr 后充值页仍可用;P2-2 记录页真正分页或明确截断提示,含 >50 条测试);P3 登记到 T-401/T-403 或 Backlog。**无 P1,但按任务看板领取规则应先完成 T-505,再进入 T-401**;`check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | DONE |
|
||||
|
||||
## Phase 5 · 后台与发布
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-401 | 运营后台完善 | T-302, T-304 | admin 可管理用户/钱包/ApiKey(脱敏)、配规则/汇率、检索充值订单/流水/调用记录;手工调点带原因并经计费层写流水 | TODO |
|
||||
| T-402 | 完整验收 MVP | T-401, T-504 | `02-requirements.md` 的 P0 验收全部通过(含用户端注册/充值/API Key/记录) | TODO |
|
||||
| T-403 | 部署 / 运行文档 | T-402 | 新环境可按文档运行;明确图片接口超时配置;上线前必须引用一次真实图片生成耗时来设置 Gunicorn `--timeout`、Nginx `proxy_read_timeout`、客户端 read timeout;若尚无真实耗时,部署文档必须显式标注图片同步风险未退,不得声称已验证;**DRF 限流依赖共享缓存**:多 Gunicorn worker 下默认 `LocMemCache` 是每进程的,会让 `generate`/`api_auth_failure` 限流按 worker 各算一份(实际速率≈worker 数×配置值),部署必须配置 Redis/Memcached 等共享 `CACHES` 后端并在文档说明,否则限流形同虚设 | TODO |
|
||||
| T-402 | 完整验收 MVP | T-401, T-505 | `02-requirements.md` 的 P0 验收全部通过(含用户端注册/充值/API Key/记录) | TODO |
|
||||
| T-403 | 部署 / 运行文档 | T-402 | 新环境可按文档运行;明确图片接口超时配置;上线前必须引用一次真实图片生成耗时来设置 Gunicorn `--timeout`、Nginx `proxy_read_timeout`、客户端 read timeout;若尚无真实耗时,部署文档必须显式标注图片同步风险未退,不得声称已验证;生产必须配置真实 `DJANGO_EMAIL_BACKEND` / `DJANGO_DEFAULT_FROM_EMAIL`,否则 allauth 邮箱验证无法发信、用户无法完成登录;**生产静态文件 serving**:T-505 已把 Bootstrap/qrcode 自托管到 `apps/portal/static/`,但 `settings` 目前只有 `STATIC_URL`,`DEBUG=False` 下 Django 不发静态文件——必须配置 `STATIC_ROOT` + `collectstatic` + WhiteNoise 或 Nginx 托管 `/static/`,否则用户端 CSS 错版、充值二维码 404(P2-1 的国内可用性目标在生产失效);**DRF 限流依赖共享缓存**:多 Gunicorn worker 下默认 `LocMemCache` 是每进程的,会让 `generate`/`api_auth_failure` 限流按 worker 各算一份(实际速率≈worker 数×配置值),部署必须配置 Redis/Memcached 等共享 `CACHES` 后端并在文档说明,否则限流形同虚设 | TODO |
|
||||
|
||||
## 里程碑
|
||||
|
||||
@@ -91,4 +91,8 @@
|
||||
- 注册赠点 / 试用额度(需邮箱/图形验证码 + 限流防薅羊毛)。
|
||||
- 用量统计报表。
|
||||
- 退款对账自动化、API Key 轮换、Provider key 轮换(MultiFernet / 双 key 迁移)、限流细化。
|
||||
- 用户端 API Key 数量治理:限制每用户 active Key 数量(如 10 个)、提供轮换与过期策略,避免无限建 Key。
|
||||
- 充值下单风控:限制每用户 pending 订单数量或创建频率,避免反复点击刷出大量未支付订单并放大支付网关调用。
|
||||
- 用户端登录与凭证安全复核:评估 `ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION=True` 是否适合生产;评估新 API Key 明文短暂进入 session 的替代方案。
|
||||
- allauth `account.EmailAddress` 的 MySQL `models.W036` 条件唯一约束警告为已知 cosmetic 警告;邮箱唯一性由 `user.email` 约束承担,后续升级 allauth / 数据库兼容性时复核。
|
||||
- **SSRF DNS rebinding 加固(`image_url` 下载)**:当前 `download_image_input` 校验时 `getaddrinfo` 解析一次、`requests.get` 又独立解析一次,两次之间存在 TOCTOU(恶意 DNS 可先返公网 IP 过校验、再返内网 IP 供实际请求)。彻底堵需把连接**钉死到已校验的 IP**(自定义 `HTTPAdapter`/`connection` 连已验证 IP + 保留 Host 头做 TLS/路由),对每次重定向同样处理。属安全深水区,规模化/公网大流量前处理;上线前安全检查应至少标注此残留。
|
||||
|
||||
+2
-2
@@ -33,9 +33,9 @@ T-501 已实现用户端注册 / 登录基线:`/signup` `/login` `/logout` 走
|
||||
|
||||
T-502 已实现用户端 API Key 自助管理基线:`/apikeys` 走 Django session + CSRF;登录用户可生成和删除自己的 Key,生成后的明文只在重定向后的首个页面显示一次,库内只保存 `key_hash` 与 `key_prefix`。用户端“删除”落库为 `revoked`,保留历史记录关联;吊销后的 Key 调用生成 / 余额接口返回 `403 account_disabled`,缺失、无效或不存在的 Key 仍返回 `401 unauthorized`。
|
||||
|
||||
T-503 已实现用户端个人中心 / 记录页基线:`/dashboard` 走 Django session,展示当前用户剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 只展示当前登录用户的充值订单;`/records/usage` 只展示当前登录用户的 `consume` / `refund` 点数流水并关联调用信息。页面只读,不写 `UserWallet.points_balance`。
|
||||
T-503/T-505 已实现用户端个人中心 / 记录页基线:`/dashboard` 走 Django session,展示当前用户剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 分页展示当前登录用户的充值订单;`/records/usage` 分页展示当前登录用户的 `consume` / `refund` 点数流水并关联调用信息。页面只读,不写 `UserWallet.points_balance`。
|
||||
|
||||
T-504 已实现用户端充值页基线:`/recharge` 走 Django session + CSRF,GET 展示余额、充值表单、当前订单和最近充值;POST 创建 pending 充值订单并展示支付二维码票据;浏览器轮询 `GET /api/v1/recharge/status`,订单 paid 后刷新页面重新读取余额。页面不直接加点,到账仍以支付回调或主动查单入账后的本地订单状态为准。
|
||||
T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session + CSRF,GET 展示余额、充值表单、当前订单和最近充值;POST 创建 pending 充值订单并用本地 static 自托管的 qrcode.js 展示支付二维码票据;浏览器轮询 `GET /api/v1/recharge/status`,订单 paid 后刷新页面重新读取余额。页面不直接加点,到账仍以支付回调或主动查单入账后的本地订单状态为准。
|
||||
|
||||
通用错误响应:
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
## 当前快照
|
||||
|
||||
- 日期:2026-07-03
|
||||
- 阶段:Phase 4 审核优化收尾;Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页与 T-504 充值页已完成,Phase 4 审核新增 T-505,下一步 T-505 用户端审核优化,完成后进入 T-401 运营后台完善
|
||||
- 阶段:Phase 5 后台与发布;Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成,下一步 T-401 运营后台完善
|
||||
- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + django-allauth 65.18.0 + PyMySQL 1.1.3 + cryptography 46.0.7 + requests 2.34.2 + django-admin;MySQL 8.4 已接入 settings,并支持 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT`;用户端已用 Django 模板 SSR + Bootstrap + allauth 落地注册登录;详见 `03-tech-stack.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 已接入 allauth,新增 portal 路由、注册适配器、登录/注册/登出模板和最小 dashboard,注册成功创建 0 点钱包且不写赠点流水;T-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503 已扩展 `/dashboard` 为个人中心汇总,并新增 `/records/recharge` 充值记录与 `/records/usage` 消费记录,只读展示当前用户数据;T-504 已新增 `/recharge` 页面、`RechargeCreateForm`、充值导航入口和轮询脚本,页面创建 pending 订单、展示二维码票据、轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额
|
||||
- 测试:T-504 已验证:`./init.ps1` 开工前通过;`py -3.12 -m py_compile apps\portal\forms.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` 无变化;`py -3.12 -m compileall apps config` 通过;`py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` 通过(19 tests OK);`py -3.12 manage.py test apps.users apps.portal apps.api --noinput --keepdb --verbosity 2` 通过(60 tests OK);`./init.ps1` 收尾通过;`git diff --check` 通过(仅 Windows CRLF 提示)。测试/迁移阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目用户账本唯一性由 `user.email` 唯一约束承担。
|
||||
- 生产代码:已有最小 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 已接入 allauth,新增 portal 路由、注册适配器、登录/注册/登出模板和最小 dashboard,注册成功创建 0 点钱包且不写赠点流水;T-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503 已扩展 `/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-505 已验证:`./init.ps1` 开工前通过;`py -3.12 -m py_compile apps\portal\views.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过;`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 makemigrations --check --dry-run` 无变化;`py -3.12 -m compileall apps config` 通过;`rg -n "cdn\.jsdelivr|jsdelivr|qrcode@1\.5\.4" apps\portal\templates apps\portal\static` 无命中;`py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` 通过(21 tests OK,含 >50 条分页测试);新增分页测试单跑通过(2 tests OK);`./init.ps1` 收尾通过。曾尝试 `py -3.12 manage.py test apps.users apps.portal apps.api --noinput --keepdb --verbosity 2` 与 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2`,但远程 MySQL 在运行中多次出现 `OperationalError 2003/2013`、`Packet sequence number wrong` 或连接重置,未取得单次全绿;已通过的用例无断言失败。测试/迁移阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目用户账本唯一性由 `user.email` 唯一约束承担。
|
||||
- 数据:AI 上游调用与模型配置参考 `D:\chengma\cmbot`(`src/services/ai_text_service.py`、`ai_image_service.py`、`config/ai_models.json`);真实 `ai_models.json` 不提交,需通过 `import_ai_models` 命令加密导入
|
||||
- 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh`
|
||||
- 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test`
|
||||
@@ -33,7 +33,7 @@
|
||||
| `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令,并校验解释器版本 `>=3.12,<3.14` |
|
||||
| `requirements.txt` / `pyproject.toml` | 已有 | `requirements.txt` 管运行依赖;`pyproject.toml` 落地 `requires-python`;T-101 新增 `requests`;T-102 使用既有 `cryptography` 做 Fernet 加密;T-501 新增 `django-allauth` |
|
||||
| `config/`(Django 工程) | 已有 | T-001 创建,含 settings / urls / wsgi / asgi |
|
||||
| `apps/`(users/portal/billing/ai/api) | 已有 | T-002 创建;`apps/users` 已定义自定义 `User`;T-003 已注册 admin 与 admin smoke test;T-004 已给 `User.email` 加唯一约束;T-101 已新增 `apps/ai/providers`;T-102 已新增 `apps/ai/security.py`、`aliases.py`、`importers.py`、management command 与 `ai.0001_initial` 迁移;T-103 已新增 `apps/ai/audit.py` 与 `ai.0002_aiconfigauditlog` 迁移;T-104/T-105 已新增 `smoke_ai_generation` 录制 title/image smoke 命令;T-201 已在 users 落 `UserWallet` / `ApiKey`,在 billing 落 `PointsLedger` / `CallRecord`;T-202 已在 billing 落 `PricingRule` / `ExchangeRate` 与 `pricing.py`;T-203/T-303/T-304/T-305 已在 `apps/billing/services.py` 落扣点/退点、余额快照、充值入账与充值下单;T-304/T-305 已在 `apps/billing/payment_gateways.py` 落回调验签、mock 下单与 SDK 入口;T-301~T-306 已在 api 落鉴权、生成接口编排、序列化器、图片存储、余额查询、充值回调、充值下单/状态查询、`image_url` SSRF 防护、生成/认证限流与统一 429 错误响应;T-501 已在 portal 落 allauth 注册/登录/登出路由、模板、adapter 与 dashboard;T-502 已在 portal 落 `/apikeys`、API Key 创建表单、列表模板与删除(吊销)动作;T-503 已在 portal 落个人中心汇总、充值记录和消费记录页;T-504 已在 portal 落 `/recharge` 充值页、充值表单、二维码票据展示和状态轮询 |
|
||||
| `apps/`(users/portal/billing/ai/api) | 已有 | T-002 创建;`apps/users` 已定义自定义 `User`;T-003 已注册 admin 与 admin smoke test;T-004 已给 `User.email` 加唯一约束;T-101 已新增 `apps/ai/providers`;T-102 已新增 `apps/ai/security.py`、`aliases.py`、`importers.py`、management command 与 `ai.0001_initial` 迁移;T-103 已新增 `apps/ai/audit.py` 与 `ai.0002_aiconfigauditlog` 迁移;T-104/T-105 已新增 `smoke_ai_generation` 录制 title/image smoke 命令;T-201 已在 users 落 `UserWallet` / `ApiKey`,在 billing 落 `PointsLedger` / `CallRecord`;T-202 已在 billing 落 `PricingRule` / `ExchangeRate` 与 `pricing.py`;T-203/T-303/T-304/T-305 已在 `apps/billing/services.py` 落扣点/退点、余额快照、充值入账与充值下单;T-304/T-305 已在 `apps/billing/payment_gateways.py` 落回调验签、mock 下单与 SDK 入口;T-301~T-306 已在 api 落鉴权、生成接口编排、序列化器、图片存储、余额查询、充值回调、充值下单/状态查询、`image_url` SSRF 防护、生成/认证限流与统一 429 错误响应;T-501 已在 portal 落 allauth 注册/登录/登出路由、模板、adapter 与 dashboard;T-502 已在 portal 落 `/apikeys`、API Key 创建表单、列表模板与删除(吊销)动作;T-503 已在 portal 落个人中心汇总、充值记录和消费记录页;T-504 已在 portal 落 `/recharge` 充值页、充值表单、二维码票据展示和状态轮询;T-505 已在 portal 落本地 vendor static 与记录分页 |
|
||||
| `manage.py` | 已有 | T-001 创建 |
|
||||
| `tests/` | 待建 | 随各任务补充 |
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
|
||||
任务状态以 [`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-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 用户端审核优化。
|
||||
- 正在进行:无。
|
||||
- 当前 blocker:远程 MySQL 连接当前不稳定或很慢;支付商户真实密钥/证书与生产 SDK 依赖仍待提供;真实 AI 上游 smoke 仍需配置密钥和模型数据后执行。
|
||||
- 下一个可领取任务:**T-505 Phase 4 用户端审核优化**;完成后进入 **T-401 运营后台完善**。
|
||||
- 下一个可领取任务:**T-401 运营后台完善**。
|
||||
|
||||
## 当前可运行内容
|
||||
|
||||
@@ -97,7 +97,7 @@ python3.12 manage.py smoke_ai_generation image --recorded
|
||||
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-505)。
|
||||
4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务(当前为 T-401)。
|
||||
5. 将该任务状态改为 `DOING`。
|
||||
|
||||
## 维护规则
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# cmhub 项目介绍(给管理层)
|
||||
|
||||
> 面向决策与汇报的项目概览。技术细节见同目录架构与需求文档。
|
||||
> 日期:2026-07-03 | 阶段:Phase 4 审核优化收尾(T-505 待处理)
|
||||
> 日期:2026-07-03 | 阶段:Phase 5 后台与发布(T-505 已完成)
|
||||
|
||||
## 一句话概括
|
||||
|
||||
@@ -104,8 +104,8 @@
|
||||
- Phase 1 已完成 T-101~T-104:AI Provider 适配器、AiModel/ModelAlias、Fernet 加密密钥存储、别名解析、配置审计与录制标题生成 smoke 已落地。
|
||||
- Phase 2 计费核心已完成:PricingRule / ExchangeRate、计费计算、并发安全扣点与失败退点已落地,并覆盖并发测试。
|
||||
- Phase 3 对外 API 与充值已完成到 T-306:API Key 鉴权、生成/余额接口、扫码充值下单与轮询、支付回调幂等入账、`image_url` SSRF 防护、生成/认证限流和充值金额上限已落地。
|
||||
- Phase 4 已完成 T-501~T-504:用户端注册 / 登录(allauth)、邮箱验证、登出、API Key 自助管理、个人中心汇总、充值记录、消费记录和充值页已落地;用户可创建充值订单、查看二维码票据并轮询到账;Key 明文只显示一次,库内只保留 hash 和 prefix,删除即吊销;记录页仅见本人数据。
|
||||
- 下一步先做 T-505:处理 Phase 4 用户端审核优化(静态资源自托管或 SRI、记录页分页或明确截断提示),完成后再进入 T-401 运营后台完善,补齐用户/钱包/API Key/计费规则/充值订单/流水/调用记录的运营检索与手工调点能力。
|
||||
- Phase 4 已完成 T-501~T-505:用户端注册 / 登录(allauth)、邮箱验证、登出、API Key 自助管理、个人中心汇总、充值记录、消费记录和充值页已落地;用户可创建充值订单、查看二维码票据并轮询到账;Key 明文只显示一次,库内只保留 hash 和 prefix,删除即吊销;记录页仅见本人数据并支持分页;Bootstrap 与 qrcode.js 已改为本地 static 自托管。
|
||||
- 下一步是 T-401:完善运营后台,补齐用户/钱包/API Key/计费规则/充值订单/流水/调用记录的运营检索与手工调点能力。
|
||||
|
||||
---
|
||||
*更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# cmhub · 一页汇报版
|
||||
|
||||
> 自助用户端 + 计费型 AI 能力网关 + 运营后台 | 2026-07-03 | Phase 4 审核优化收尾
|
||||
> 自助用户端 + 计费型 AI 能力网关 + 运营后台 | 2026-07-03 | Phase 5 后台与发布
|
||||
|
||||
## 电梯陈述(30 秒)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
## 进度
|
||||
|
||||
M1 骨架已完成;M2 已通过录制生成 smoke 验证 AI 调用链路;M3 计费、对外 API、充值闭环与上线前安全加固已完成到 T-306;M4 用户端已完成注册 / 登录(T-501)、API Key 管理页(T-502)、个人中心 / 记录页(T-503)和充值页(T-504)。下一步先做 Phase 4 审核优化 T-505,随后进入运营后台完善。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线。
|
||||
M1 骨架已完成;M2 已通过录制生成 smoke 验证 AI 调用链路;M3 计费、对外 API、充值闭环与上线前安全加固已完成到 T-306;M4 用户端已完成注册 / 登录(T-501)、API Key 管理页(T-502)、个人中心 / 记录页(T-503)、充值页(T-504)和审核优化(T-505)。下一步做运营后台完善。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线。
|
||||
|
||||
---
|
||||
*详见 `project-brief.md`(完整介绍)。*
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
| `/records/usage` | GET | 点数使用(消费/调用)记录 | session |
|
||||
| `/apikeys` | GET/POST | API Key 管理:列表 / 生成 / 删除(删除即吊销,明文只显示一次) | session |
|
||||
|
||||
T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`。T-502 已落地 `/apikeys`:登录用户只能管理自己的 Key,生成后明文只显示一次,列表只显示 prefix,删除为吊销 `revoked`。T-503 已扩展 `/dashboard` 为个人中心汇总,并落地 `/records/recharge` 与 `/records/usage`:充值总额按已支付订单统计,入账 / 消费 / 退款点数按 `PointsLedger` 统计,记录页只查询当前登录用户数据。T-504 已落地 `/recharge`:登录用户可选择金额和支付方式创建 pending 充值订单,页面展示二维码票据并轮询 `/api/v1/recharge/status`,到账后刷新余额。
|
||||
T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`。T-502 已落地 `/apikeys`:登录用户只能管理自己的 Key,生成后明文只显示一次,列表只显示 prefix,删除为吊销 `revoked`。T-503/T-505 已扩展 `/dashboard` 为个人中心汇总,并落地 `/records/recharge` 与 `/records/usage`:充值总额按已支付订单统计,入账 / 消费 / 退款点数按 `PointsLedger` 统计,记录页只查询当前登录用户数据并分页展示。T-504/T-505 已落地 `/recharge`:登录用户可选择金额和支付方式创建 pending 充值订单,页面用本地 static 自托管 qrcode.js 展示二维码票据并轮询 `/api/v1/recharge/status`,到账后刷新余额。
|
||||
|
||||
## API 路由(对外,DRF)
|
||||
|
||||
|
||||
+26
@@ -845,3 +845,29 @@
|
||||
- 变更:统一新增 T-505 后的下一步口径:因任务看板规则要求领取第一个 `TODO` 且依赖均 `DONE` 的任务,T-505 作为 Phase 4 用户端审核优化先于 T-401 执行;同步更新 `README.md`、`docs/00-ai-start-here.md`、`docs/current-state.md`、`docs/project-brief.md`、`docs/project-onepager.md`、`docs/06-tasks.md`、`docs/phase-4-review.md` 与本执行流水。
|
||||
- 验证:文档口径修正,无代码变更;执行 `git diff --check`。
|
||||
- 下一步:领取 T-505 Phase 4 用户端审核优化。
|
||||
|
||||
## 2026-07-03 T-505 Phase 4 用户端审核优化
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:
|
||||
- `apps/portal/static/portal/vendor/`:新增本地自托管 `bootstrap/bootstrap.min.css` 与 `qrcode/qrcode.js`,并随附许可证文件;`base.html` / `recharge.html` 改用 `{% static %}`,移除 jsdelivr 运行时依赖。
|
||||
- `apps/portal/views.py`:新增 `paginate_records()`,充值记录与消费记录从固定 `[:50]` 切片改为 Django `Paginator` 分页。
|
||||
- `apps/portal/templates/portal/includes/pagination.html`:新增分页片段;充值记录 / 消费记录页展示总数、每页数量、当前页和上一页 / 下一页。
|
||||
- `apps/portal/tests.py`:新增本地静态资源断言、无 CDN 断言,以及充值记录 / 消费记录各 >50 条分页测试。
|
||||
- `docs/06-tasks.md`:T-505 标记 DONE;P3 后续项登记到 T-403 或 Backlog:生产邮件配置、API Key 数量治理、充值下单频控、邮箱确认自动登录 / raw key session 策略复核、allauth W036 已知警告。
|
||||
- 同步更新 `README.md`、`docs/00-ai-start-here.md`、`docs/03-tech-stack.md`、`docs/04-architecture.md`、`docs/api.md`、`docs/routes.md`、`docs/current-state.md`、`docs/project-brief.md`、`docs/project-onepager.md`。
|
||||
- 验证:
|
||||
- `./init.ps1`:开工前通过;收尾通过。
|
||||
- `py -3.12 -m py_compile apps\portal\views.py apps\portal\tests.py`:通过。
|
||||
- `py -3.12 manage.py check`:通过,0 issues。
|
||||
- `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 makemigrations --check --dry-run`:通过,No changes detected。
|
||||
- `py -3.12 -m compileall apps config`:通过。
|
||||
- `rg -n "cdn\.jsdelivr|jsdelivr|qrcode@1\.5\.4" apps\portal\templates apps\portal\static`:无命中。
|
||||
- `py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2`:通过,21 tests OK。
|
||||
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_recharge_records_paginate_more_than_page_size apps.portal.tests.PortalAccountFlowTests.test_usage_records_paginate_more_than_page_size --noinput --keepdb --verbosity 2`:通过,2 tests OK。
|
||||
- 尝试 `py -3.12 manage.py test apps.users apps.portal apps.api --noinput --keepdb --verbosity 2`:未取得单次全绿;第一次在 `setUpClass` 前远程 MySQL 超时,第二次跑过 users + portal + 多数 API 后 MySQL 出现 `OperationalError 2003/2013` / 连接重置,已通过用例无断言失败。
|
||||
- 尝试 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2`:未取得单次全绿;36 个 API 测试已运行,多数通过,`BalanceApiTests.setUpClass` 因远程 MySQL `Packet sequence number wrong` 中断。`Test-NetConnection 43.128.3.240 -Port 3306` 后续显示端口可达,判断为远程 MySQL 间歇稳定性问题。
|
||||
- 阻塞:T-505 功能无阻塞;完整大套件单次全绿仍受远程 MySQL 连接稳定性影响。
|
||||
- 决策:T-505 仅处理审核报告 P2;P3 不在本轮实现,已登记到 T-403 或 Backlog。
|
||||
- 下一步:领取 T-401 运营后台完善。
|
||||
|
||||
Reference in New Issue
Block a user