From ab1f0ffce44881f53b16c6637ac3a1d5ea5cf9ab Mon Sep 17 00:00:00 2001 From: aaron <> Date: Tue, 12 May 2026 22:23:59 +0800 Subject: [PATCH] 1 --- backend/app/cli.py | 24 ++++++- backend/app/services/bazi_backfill_service.py | 36 ++++++++++ backend/tests/test_bazi_backfill_service.py | 51 +++++++++++++++ web/components/PalmWebApp.tsx | 65 ++++++++++++++++++- 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 backend/app/services/bazi_backfill_service.py create mode 100644 backend/tests/test_bazi_backfill_service.py diff --git a/backend/app/cli.py b/backend/app/cli.py index cece6d5..b364462 100644 --- a/backend/app/cli.py +++ b/backend/app/cli.py @@ -1,15 +1,37 @@ import asyncio +import argparse from app.core.database import AsyncSessionLocal, init_db +from app.services.bazi_backfill_service import backfill_bazi_wuxing_balance from app.services.cleanup_service import cleanup_expired_images -async def main() -> None: +async def cleanup_command() -> None: await init_db() async with AsyncSessionLocal() as session: count = await cleanup_expired_images(session) + await session.commit() print(f"cleaned {count} expired images") +async def backfill_bazi_wuxing_command() -> None: + await init_db() + async with AsyncSessionLocal() as session: + count = await backfill_bazi_wuxing_balance(session) + await session.commit() + print(f"backfilled {count} bazi wuxing balances") + + +async def main() -> None: + parser = argparse.ArgumentParser(description="Cyber Mister maintenance commands") + parser.add_argument("command", nargs="?", default="cleanup-expired-images", choices=["cleanup-expired-images", "backfill-bazi-wuxing"]) + args = parser.parse_args() + + if args.command == "backfill-bazi-wuxing": + await backfill_bazi_wuxing_command() + else: + await cleanup_command() + + if __name__ == "__main__": asyncio.run(main()) diff --git a/backend/app/services/bazi_backfill_service.py b/backend/app/services/bazi_backfill_service.py new file mode 100644 index 0000000..c103ac6 --- /dev/null +++ b/backend/app/services/bazi_backfill_service.py @@ -0,0 +1,36 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm.attributes import flag_modified + +from app.models.reading import Reading +from app.services.bazi_calculator import BaziCalculator + + +async def backfill_bazi_wuxing_balance(db: AsyncSession) -> int: + result = await db.execute(select(Reading).where(Reading.reading_type == "bazi")) + readings = result.scalars().all() + calculator = BaziCalculator() + updated = 0 + + for reading in readings: + input_data = dict(reading.input_data or {}) + chart = input_data.get("chart") + if not isinstance(chart, dict): + continue + + pillars = chart.get("pillars") + if not isinstance(pillars, dict): + continue + + current = chart.get("wuxing_balance") + if isinstance(current, dict) and any(int(current.get(name) or 0) for name in ("木", "火", "土", "金", "水")): + continue + + chart["wuxing_balance"] = calculator._count_wuxing(pillars) + input_data["chart"] = chart + reading.input_data = input_data + flag_modified(reading, "input_data") + updated += 1 + + await db.flush() + return updated diff --git a/backend/tests/test_bazi_backfill_service.py b/backend/tests/test_bazi_backfill_service.py new file mode 100644 index 0000000..820d3c7 --- /dev/null +++ b/backend/tests/test_bazi_backfill_service.py @@ -0,0 +1,51 @@ +from uuid import uuid4 + +import pytest +from sqlalchemy import select + +from app.core.database import AsyncSessionLocal, init_db +from app.models.reading import Reading +from app.models.user import User +from app.services.bazi_backfill_service import backfill_bazi_wuxing_balance + + +@pytest.mark.asyncio +async def test_backfill_bazi_wuxing_balance_updates_missing_chart_field(): + await init_db() + user_id = str(uuid4()) + reading_id = str(uuid4()) + + async with AsyncSessionLocal() as session: + session.add(User(id=user_id, openid=f"backfill-{uuid4()}")) + session.add( + Reading( + id=reading_id, + user_id=user_id, + reading_type="bazi", + status="completed", + input_data={ + "chart": { + "pillars": { + "year": "壬申", + "month": "戊申", + "day": "丙寅", + "time": "癸巳", + } + } + }, + report_data={"overall_summary": "keep me"}, + ) + ) + await session.commit() + + async with AsyncSessionLocal() as session: + count = await backfill_bazi_wuxing_balance(session) + await session.commit() + + async with AsyncSessionLocal() as session: + result = await session.execute(select(Reading).where(Reading.id == reading_id)) + reading = result.scalar_one() + + assert count >= 1 + assert reading.input_data["chart"]["wuxing_balance"] == {"木": 1, "火": 2, "土": 1, "金": 2, "水": 2} + assert reading.report_data == {"overall_summary": "keep me"} diff --git a/web/components/PalmWebApp.tsx b/web/components/PalmWebApp.tsx index de1e14d..b5047c0 100644 --- a/web/components/PalmWebApp.tsx +++ b/web/components/PalmWebApp.tsx @@ -74,6 +74,37 @@ type BaziChart = { wuxing_balance?: Record; }; +const wuxingNames = ["木", "火", "土", "金", "水"] as const; +type WuxingName = (typeof wuxingNames)[number]; + +const stemWuxing: Record = { + 甲: "木", + 乙: "木", + 丙: "火", + 丁: "火", + 戊: "土", + 己: "土", + 庚: "金", + 辛: "金", + 壬: "水", + 癸: "水", +}; + +const branchWuxing: Record = { + 子: "水", + 丑: "土", + 寅: "木", + 卯: "木", + 辰: "土", + 巳: "火", + 午: "火", + 未: "土", + 申: "金", + 酉: "金", + 戌: "土", + 亥: "水", +}; + const defaultBaziForm: BaziForm = { nickname: "", gender: "", @@ -642,7 +673,7 @@ function ReportPanel({ reading, onDelete }: { reading: Reading; onDelete: () => function BaziChartPanel({ chart }: { chart: BaziChart | null }) { if (!chart) return null; const pillars = chart.pillars || {}; - const wuxing = chart.wuxing_balance || {}; + const wuxing = getWuxingBalance(chart); const pillarItems = [ ["年柱", pillars.year], ["月柱", pillars.month], @@ -674,7 +705,7 @@ function BaziChartPanel({ chart }: { chart: BaziChart | null }) { {chart.birth_place ? 出生地:{chart.birth_place} : null}
- {(["木", "火", "土", "金", "水"] as const).map((name) => { + {wuxingNames.map((name) => { const value = wuxing[name] || 0; return (
@@ -714,6 +745,36 @@ function getBaziChart(reading: Reading): BaziChart | null { return chart as BaziChart; } +function getWuxingBalance(chart: BaziChart): Record { + const existing = normalizeWuxingBalance(chart.wuxing_balance); + if (wuxingNames.some((name) => existing[name] > 0)) { + return existing; + } + return countWuxingFromPillars(chart.pillars || {}); +} + +function normalizeWuxingBalance(value?: Record): Record { + return wuxingNames.reduce( + (result, name) => { + result[name] = Number(value?.[name] || 0); + return result; + }, + { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 } as Record, + ); +} + +function countWuxingFromPillars(pillars: Record): Record { + const counts = normalizeWuxingBalance(); + Object.values(pillars).forEach((pillar) => { + if (!pillar || pillar === "时辰不详") return; + Array.from(pillar).forEach((char) => { + const element = stemWuxing[char] || branchWuxing[char]; + if (element) counts[element] += 1; + }); + }); + return counts; +} + function SummaryList({ title, items }: { title: string; items: string[] }) { return (