This commit is contained in:
aaron 2026-05-12 22:23:59 +08:00
parent 01bc803ebb
commit ab1f0ffce4
4 changed files with 173 additions and 3 deletions

View File

@ -1,15 +1,37 @@
import asyncio import asyncio
import argparse
from app.core.database import AsyncSessionLocal, init_db 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 from app.services.cleanup_service import cleanup_expired_images
async def main() -> None: async def cleanup_command() -> None:
await init_db() await init_db()
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
count = await cleanup_expired_images(session) count = await cleanup_expired_images(session)
await session.commit()
print(f"cleaned {count} expired images") 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__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@ -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

View File

@ -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"}

View File

@ -74,6 +74,37 @@ type BaziChart = {
wuxing_balance?: Record<string, number>; wuxing_balance?: Record<string, number>;
}; };
const wuxingNames = ["木", "火", "土", "金", "水"] as const;
type WuxingName = (typeof wuxingNames)[number];
const stemWuxing: Record<string, WuxingName> = {
: "木",
: "木",
: "火",
: "火",
: "土",
: "土",
: "金",
: "金",
: "水",
: "水",
};
const branchWuxing: Record<string, WuxingName> = {
: "水",
: "土",
: "木",
: "木",
: "土",
: "火",
: "火",
: "土",
: "金",
: "金",
: "土",
: "水",
};
const defaultBaziForm: BaziForm = { const defaultBaziForm: BaziForm = {
nickname: "", nickname: "",
gender: "", gender: "",
@ -642,7 +673,7 @@ function ReportPanel({ reading, onDelete }: { reading: Reading; onDelete: () =>
function BaziChartPanel({ chart }: { chart: BaziChart | null }) { function BaziChartPanel({ chart }: { chart: BaziChart | null }) {
if (!chart) return null; if (!chart) return null;
const pillars = chart.pillars || {}; const pillars = chart.pillars || {};
const wuxing = chart.wuxing_balance || {}; const wuxing = getWuxingBalance(chart);
const pillarItems = [ const pillarItems = [
["年柱", pillars.year], ["年柱", pillars.year],
["月柱", pillars.month], ["月柱", pillars.month],
@ -674,7 +705,7 @@ function BaziChartPanel({ chart }: { chart: BaziChart | null }) {
{chart.birth_place ? <span>{chart.birth_place}</span> : null} {chart.birth_place ? <span>{chart.birth_place}</span> : null}
</div> </div>
<div className="wuxing-bars"> <div className="wuxing-bars">
{(["木", "火", "土", "金", "水"] as const).map((name) => { {wuxingNames.map((name) => {
const value = wuxing[name] || 0; const value = wuxing[name] || 0;
return ( return (
<div className="wuxing-bar" key={name}> <div className="wuxing-bar" key={name}>
@ -714,6 +745,36 @@ function getBaziChart(reading: Reading): BaziChart | null {
return chart as BaziChart; return chart as BaziChart;
} }
function getWuxingBalance(chart: BaziChart): Record<WuxingName, number> {
const existing = normalizeWuxingBalance(chart.wuxing_balance);
if (wuxingNames.some((name) => existing[name] > 0)) {
return existing;
}
return countWuxingFromPillars(chart.pillars || {});
}
function normalizeWuxingBalance(value?: Record<string, number>): Record<WuxingName, number> {
return wuxingNames.reduce(
(result, name) => {
result[name] = Number(value?.[name] || 0);
return result;
},
{ : 0, : 0, : 0, : 0, : 0 } as Record<WuxingName, number>,
);
}
function countWuxingFromPillars(pillars: Record<string, string>): Record<WuxingName, number> {
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[] }) { function SummaryList({ title, items }: { title: string; items: string[] }) {
return ( return (
<article className="summary-list"> <article className="summary-list">