From 0d6b97f44d0af4d5f43d0df3c55ddfe742cd7311 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Sat, 4 Jul 2026 16:43:57 +0800 Subject: [PATCH] fix: avoid MySQL timezone dependency in billing admin --- apps/billing/admin.py | 4 ---- apps/billing/tests.py | 25 +++++++++++++++++++++++++ docs/current-state.md | 1 + progress.md | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/billing/admin.py b/apps/billing/admin.py index d6728da..a4b8e5e 100644 --- a/apps/billing/admin.py +++ b/apps/billing/admin.py @@ -31,7 +31,6 @@ class PointsLedgerAdmin(ReadOnlyLedgerAdmin): list_filter = ("change_type", "created_at") search_fields = ("user__username", "user__email", "=ref_order_id", "reason") ordering = ("-created_at", "-id") - date_hierarchy = "created_at" list_select_related = ("user", "ref_call") @@ -70,7 +69,6 @@ class ExchangeRateAdmin(admin.ModelAdmin): ordering = ("-effective_from", "-id") readonly_fields = ("created_at", "updated_at") list_editable = ("is_active",) - date_hierarchy = "effective_from" @admin.register(RechargeOrder) @@ -95,7 +93,6 @@ class RechargeOrderAdmin(ReadOnlyLedgerAdmin): "payment_txn_no", ) ordering = ("-created_at", "-id") - date_hierarchy = "created_at" list_select_related = ("user",) @@ -125,5 +122,4 @@ class CallRecordAdmin(ReadOnlyLedgerAdmin): "result_summary", ) ordering = ("-created_at", "-id") - date_hierarchy = "created_at" list_select_related = ("user", "api_key") diff --git a/apps/billing/tests.py b/apps/billing/tests.py index ef30e4d..e8d2211 100644 --- a/apps/billing/tests.py +++ b/apps/billing/tests.py @@ -10,6 +10,7 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import IntegrityError, OperationalError, connections, transaction from django.test import TestCase, TransactionTestCase, override_settings +from django.urls import reverse from django.utils import timezone from apps.ai.models import AiModel, ModelAlias @@ -49,6 +50,30 @@ from apps.users.models import ApiKey, UserWallet TEST_ENCRYPTION_KEY = Fernet.generate_key().decode("ascii") +class BillingAdminTests(TestCase): + def setUp(self): + user_model = get_user_model() + self.admin_user = user_model.objects.create_superuser( + username="billing-admin", + email="billing-admin@example.com", + password="test-password", + ) + self.client.force_login(self.admin_user) + + def test_exchange_rate_changelist_renders_when_rates_exist(self): + ExchangeRate.objects.create( + currency="CNY", + points_per_unit=Decimal("10.0000"), + effective_from=timezone.now(), + is_active=True, + ) + + response = self.client.get(reverse("admin:billing_exchangerate_changelist")) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "CNY") + + class BillingCoreModelTests(TestCase): def setUp(self): suffix = uuid.uuid4().hex[:8] diff --git a/docs/current-state.md b/docs/current-state.md index 4f92901..db21a14 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -18,6 +18,7 @@ - 用户端导航:顶部导航 active 状态已修复,`portal/base.html` 基于 `request.resolver_match.url_name` 高亮当前页面入口,并用 `aria-current="page"` 标记;「充值」不再在非充值页固定深色高亮。 - 测试:T-601 已验证:`.\init.ps1` 通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令);`py -3.12 -m py_compile apps\ai\catalog.py apps\api\views.py apps\api\urls.py apps\api\tests.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`git diff --check` 通过;`py -3.12 manage.py test apps.api.tests.ModelsCatalogApiTests apps.portal.tests.PortalAccountFlowTests.test_models_page_requires_session_login apps.portal.tests.PortalAccountFlowTests.test_models_page_lists_public_aliases_prices_and_unpriced_state --keepdb --noinput --verbosity 2` 首次运行时 3 条 API 测试已通过,portal `setUpClass` 阶段因远程 MySQL 连接 43.128.3.240 超时中断;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示 `TcpTestSucceeded=True`,单独重跑两条 portal 测试通过,2 tests OK。T-403 已验证:`./init.ps1` 开工前通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`py -3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` 找到两个本地 static 文件;`py -3.12 manage.py check --deploy` 在当前开发 `.env` 下仅报预期安全配置警告,临时注入生产型安全环境变量(含 HSTS includeSubDomains/preload)后通过,0 issues;`py -3.12 manage.py collectstatic --dry-run --noinput` 通过,预期收集 169 个 static 文件;`py -3.12 manage.py createcachetable --dry-run cmhub_cache` 通过,输出 MySQL cache 表 DDL;`py -3.12 -m compileall config` 通过;尝试 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` 时 25 条用例已通过,`GenerateApiTests` 14 条因远程 MySQL 连接重置/事务中断被记 ERROR,随后 `Test-NetConnection 43.128.3.240 -Port 3306` 端口可达,单独重跑 `py -3.12 manage.py test apps.api.tests.GenerateApiTests --noinput --keepdb --verbosity 2` 通过,14 tests OK。测试/迁移阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目用户账本邮箱唯一性由 `user.email` 唯一约束承担。 - 最新验证:T-603 字段级中文化已验证 `py -3.12 -m py_compile apps\users\models.py apps\users\admin.py apps\billing\models.py apps\billing\admin.py apps\ai\models.py apps\ai\admin.py` 通过;`py -3.12 manage.py makemigrations` 仅生成 `users.0005` / `billing.0006` / `ai.0004` 三个 `AlterField` 迁移;`py -3.12 manage.py sqlmigrate users 0005`、`billing 0006`、`ai 0004` 均为 `(no-op)`;`py -3.12 manage.py migrate` 应用成功,仅出现既有 allauth MySQL `models.W036` 警告;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py check` 通过;`git diff --check` 通过,仅 Windows CRLF 提示;`py -3.12 manage.py test --keepdb --noinput --verbosity 2` 跑 125 条,123 条通过,2 条失败,失败原因是工作区既有 `config/settings.py` 把 `ACCOUNT_EMAIL_VERIFICATION` 改为 `none`,导致 `test_signup_creates_unverified_user_wallet_with_zero_points_and_no_ledger` 收不到验证邮件、`test_unverified_email_cannot_establish_login_session` 未验证用户仍建立登录 session。 +- 线上热修验证:`/admin/billing/exchangerate/` 500 根因是 `ExchangeRateAdmin.date_hierarchy` 触发 MySQL `CONVERT_TZ`,而生产 MySQL 未加载时区表,Django 抛 `ValueError: Database returned an invalid datetime value`;已移除 `apps.billing.admin` 中 4 个 DateTime `date_hierarchy`,保留 `list_filter` 日期筛选。验证 `py -3.12 -m py_compile apps\billing\admin.py apps\billing\tests.py`、`py -3.12 manage.py check`、`py -3.12 manage.py test apps.billing.tests.BillingAdminTests.test_exchange_rate_changelist_renders_when_rates_exist --keepdb --noinput --verbosity 2` 均通过。 - 数据: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` diff --git a/progress.md b/progress.md index d69d127..c08bc98 100644 --- a/progress.md +++ b/progress.md @@ -1089,3 +1089,17 @@ - 失败用例:`test_signup_creates_unverified_user_wallet_with_zero_points_and_no_ledger`、`test_unverified_email_cannot_establish_login_session`。 - 失败原因:工作区既有 `config/settings.py` 未提交改动将 `ACCOUNT_EMAIL_VERIFICATION` 从 `mandatory` 改为 `none`,导致注册不发验证邮件、未验证用户也能建立登录 session;该文件不属于 T-603,本轮未纳入提交。 - 下一步:恢复 `ACCOUNT_EMAIL_VERIFICATION="mandatory"` 后,重跑上述失败用例和完整测试;全部通过后把 T-603 从 `BLOCKED` 改为 `DONE`。 + +## 2026-07-04 线上 admin ExchangeRate 500 热修 + +- 状态:DONE +- 现象:线上访问 `https://cm.833729.com/admin/billing/exchangerate/` 返回 500;未登录 curl 为 302,已登录 admin 页面复现 500。 +- 根因:Django admin 的 `date_hierarchy` 会对 DateTime 字段做按日期层级聚合;MySQL 后端在 `USE_TZ=True` 且 `TIME_ZONE=Asia/Shanghai` 时需要 MySQL 时区表支持 `CONVERT_TZ`。生产 MySQL 当前未加载时区表,`ExchangeRateAdmin.date_hierarchy="effective_from"` 渲染时报 `ValueError: Database returned an invalid datetime value. Are time zone definitions for your database installed?` +- 变更: + - `apps/billing/admin.py`:移除 `PointsLedgerAdmin`、`ExchangeRateAdmin`、`RechargeOrderAdmin`、`CallRecordAdmin` 的 DateTime `date_hierarchy`,避免同类页面因 MySQL 时区表缺失 500;保留 `list_filter` 日期筛选。 + - `apps/billing/tests.py`:新增 `BillingAdminTests.test_exchange_rate_changelist_renders_when_rates_exist`,覆盖有汇率数据时 admin 列表页必须 200。 +- 验证: + - `py -3.12 -m py_compile apps\billing\admin.py apps\billing\tests.py`:通过。 + - `py -3.12 manage.py check`:通过,0 issues。 + - `py -3.12 manage.py test apps.billing.tests.BillingAdminTests.test_exchange_rate_changelist_renders_when_rates_exist --keepdb --noinput --verbosity 2`:通过,1 test OK;测试阶段仍有既有 allauth MySQL `models.W036` 警告。 +- 下一步:提交并部署后,在 VPS 上重启 `cmhub-web.service`,用 Django test client 或浏览器确认 `/admin/billing/exchangerate/` 已恢复 200。