1
This commit is contained in:
parent
01bc803ebb
commit
ab1f0ffce4
@ -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())
|
||||||
|
|||||||
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>;
|
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">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user