1
This commit is contained in:
parent
01bc803ebb
commit
ab1f0ffce4
@ -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())
|
||||
|
||||
36
backend/app/services/bazi_backfill_service.py
Normal file
36
backend/app/services/bazi_backfill_service.py
Normal 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
|
||||
51
backend/tests/test_bazi_backfill_service.py
Normal file
51
backend/tests/test_bazi_backfill_service.py
Normal 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"}
|
||||
@ -74,6 +74,37 @@ type BaziChart = {
|
||||
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 = {
|
||||
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 ? <span>出生地:{chart.birth_place}</span> : null}
|
||||
</div>
|
||||
<div className="wuxing-bars">
|
||||
{(["木", "火", "土", "金", "水"] as const).map((name) => {
|
||||
{wuxingNames.map((name) => {
|
||||
const value = wuxing[name] || 0;
|
||||
return (
|
||||
<div className="wuxing-bar" key={name}>
|
||||
@ -714,6 +745,36 @@ function getBaziChart(reading: Reading): BaziChart | null {
|
||||
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[] }) {
|
||||
return (
|
||||
<article className="summary-list">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user