feat: v0.9 样板数据完整性验证/解析加固/报表统计
- 双源确认样板库完整性: search接口与画图编辑器for_ht目录均为254条, 0遗漏 - 全量体检: 6套样板含7个畸形段(缺ps/pe)导致解析报错 前后端重放/算料统一加固过滤, 254样板0异常/1046窗格 - 报表统计模块(/chart): 近N天订单数金额按日柱图+8项总量+状态分布 - 样板库官方/我的样板双Tab(is_default过滤) - 使用指南接入帮助文章接口(种子8篇), seed分类树幂等修复 - 全接口回归18/18通过
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""报表统计路由(对齐原站 /chart 报表)。"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import ok
|
||||
from huamenchuang.models.business import AfterSale, MeasureOrder, Quotation
|
||||
from huamenchuang.models.contract import Contract, Customer
|
||||
from huamenchuang.models.order import Order
|
||||
|
||||
router = APIRouter(prefix="/reports", tags=["报表"])
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def summary(user: CurrentUser, db: DbSession, days: int = 14):
|
||||
"""经营汇总:近 N 天订单数/金额按日 + 各实体总数。"""
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
cid = user.company_id
|
||||
|
||||
# 按日订单数与金额
|
||||
day_rows = (await db.execute(
|
||||
select(func.date(Order.created_at), func.count(), func.sum(Order.money))
|
||||
.where(Order.company_id == cid, Order.created_at >= since)
|
||||
.group_by(func.date(Order.created_at))
|
||||
)).all()
|
||||
by_day = [{"date": str(d), "count": c, "money": float(m or 0)} for d, c, m in day_rows]
|
||||
|
||||
async def count(model, extra=None):
|
||||
conds = [model.company_id == cid]
|
||||
if extra is not None:
|
||||
conds.append(extra)
|
||||
return (await db.execute(select(func.count()).select_from(model).where(*conds))).scalar_one()
|
||||
|
||||
totals = {
|
||||
"orders": await count(Order),
|
||||
"intentions": await count(Order, Order.status == 0),
|
||||
"finished": await count(Order, Order.status == 3),
|
||||
"quotations": await count(Quotation),
|
||||
"customers": await count(Customer),
|
||||
"contracts": await count(Contract),
|
||||
"measures": await count(MeasureOrder),
|
||||
"after_sales": await count(AfterSale, AfterSale.status != 2),
|
||||
}
|
||||
|
||||
# 订单状态分布
|
||||
status_rows = (await db.execute(
|
||||
select(Order.status, func.count()).where(Order.company_id == cid).group_by(Order.status)
|
||||
)).all()
|
||||
status_dist = [{"status": s, "count": c} for s, c in status_rows]
|
||||
|
||||
return ok(data={"by_day": by_day, "totals": totals, "status_dist": status_dist, "days": days})
|
||||
@@ -10,6 +10,7 @@ from huamenchuang.api import (
|
||||
orders,
|
||||
platform,
|
||||
quote,
|
||||
reports,
|
||||
warehouse,
|
||||
windoor,
|
||||
)
|
||||
@@ -25,3 +26,5 @@ api_router.include_router(quote.router)
|
||||
api_router.include_router(warehouse.router)
|
||||
api_router.include_router(business.router)
|
||||
api_router.include_router(platform.router)
|
||||
api_router.include_router(reports.router)
|
||||
api_router.include_router(platform.router)
|
||||
|
||||
@@ -32,6 +32,7 @@ async def search_templates(
|
||||
db: DbSession, user: CurrentUser,
|
||||
name: str = Query(default="", max_length=64),
|
||||
catelog_id: int | None = None,
|
||||
is_default: int | None = Query(default=None, description="1官方样板 0我的样板"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=50, ge=1, le=200),
|
||||
):
|
||||
@@ -41,6 +42,8 @@ async def search_templates(
|
||||
conds.append(WindoorTemplate.name.like(f"%{name}%"))
|
||||
if catelog_id is not None:
|
||||
conds.append(WindoorTemplate.catelog_id == catelog_id)
|
||||
if is_default is not None:
|
||||
conds.append(WindoorTemplate.is_default == is_default)
|
||||
stmt = (select(WindoorTemplate).where(*conds)
|
||||
.order_by(WindoorTemplate.id).offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
|
||||
@@ -90,13 +90,26 @@ def _split_poly(region: list[dict], axis: str, pos: float) -> tuple[list[dict],
|
||||
return to_segs(poly_a), to_segs(poly_b)
|
||||
|
||||
|
||||
def _clean_region(region: list) -> list[dict]:
|
||||
"""过滤畸形段(缺 ps/pe 或类型不对),保证重放/渲染不抛错。"""
|
||||
out = []
|
||||
for s in region:
|
||||
if not isinstance(s, dict):
|
||||
continue
|
||||
ps, pe = s.get("ps"), s.get("pe")
|
||||
if isinstance(ps, dict) and isinstance(pe, dict) and "x" in ps and "y" in ps and "x" in pe and "y" in pe:
|
||||
out.append({"ps": dict(ps), "pe": dict(pe)})
|
||||
return out
|
||||
|
||||
|
||||
def replay_shape(shape: dict) -> list[list[dict]]:
|
||||
"""分割树重放: 基底多边形 + mm.sp[](polyId.idx 指向作用区域) → 最终窗格列表。
|
||||
|
||||
与前端 replayShape 同语义: idx 越界取 0; 退化/非凸跳过。
|
||||
与前端 replayShape 同语义: idx 越界取 0; 退化/非凸跳过; 畸形段过滤。
|
||||
"""
|
||||
base = shape.get("polygon", {}).get("polygon", []) or []
|
||||
regions = [[{"ps": dict(s["ps"]), "pe": dict(s["pe"])} for s in r] for r in base]
|
||||
regions = [_clean_region(r) for r in base]
|
||||
regions = [r for r in regions if r]
|
||||
if not regions:
|
||||
return regions
|
||||
for sp in (shape.get("mm") or {}).get("sp", []) or []:
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""种子文章(帮助中心内容,自撰)。"""
|
||||
|
||||
ARTICLES = [
|
||||
("常见问题解答", "如何更改标线尺寸?", "在画布编辑模式中,直接修改顶部 W/H 输入框,或拖拽外框右缘/下缘/右下角手柄;所有尺寸标注会自动跟随几何更新。"),
|
||||
("常见问题解答", "如何绘制横竖中梃?", "进入编辑模式后点击「+竖梃」或「+横梃」,再点击目标玻璃区域即可完成分割;点击已有中梃可选中并拖动调整位置,「删除梃」可移除。"),
|
||||
("常见问题解答", "如何把样板改成自己的?", "样板库 → 点击样板 → 编辑分格/尺寸 → 「另存为我的样板」,原图板不受影响。"),
|
||||
("常见问题解答", "报价是如何计算的?", "报价 = 外框 + 中梃(按实际跨度)+ 扇框 + 玻璃面积 + 五金(按扇数),价格取自产品管理中的价格本。"),
|
||||
("功能教程", "订单全流程", "合同管理创建意向单 → 转正式单 → 下订单 → 订单详情中推进生产进度(下料/组装/玻璃/打胶/五金/质检/完工)→ 打印中心输出生产单。"),
|
||||
("功能教程", "优化下料使用", "录入订单的型材需求与仓库原料,系统按首递减算法输出切割组合、余料与利用率;也可在「下料方案」按合同一键汇总。"),
|
||||
("功能教程", "3D 展示与效果图", "「效果图渲染」可在场景中预览窗型并调整墙面/玻璃颜色;「3D 展示」提供可旋转的立体视图。"),
|
||||
("功能教程", "分享订单给客户", "订单管理 → 「分享」→ 复制链接发给客户,客户免登录即可查看订单摘要与图纸。"),
|
||||
]
|
||||
@@ -27,9 +27,9 @@ async def seed() -> None:
|
||||
# 2) 分类树(来自 default_windoor_catelog.json,取目标站原始 id 便于对照)
|
||||
cat_file = DATA / "api-samples" / "default_windoor_catelog.json"
|
||||
if cat_file.exists():
|
||||
from sqlalchemy import select as _sel
|
||||
cats = json.load(open(cat_file, encoding="utf-8"))["data"]
|
||||
existing = {c.id for c in (await db.execute(
|
||||
__import__("sqlalchemy").select(WindoorCatelog.id))).scalars()}
|
||||
existing = set((await db.execute(_sel(WindoorCatelog.id))).scalars())
|
||||
for top in cats:
|
||||
if top["id"] not in existing:
|
||||
db.add(WindoorCatelog(id=top["id"], name=top["name"], fid=0, company_id=1))
|
||||
@@ -71,6 +71,17 @@ async def seed() -> None:
|
||||
await db.commit()
|
||||
print("种子数据完成")
|
||||
|
||||
# 4) 帮助文章(幂等:按标题去重)
|
||||
async with SessionLocal() as db:
|
||||
from huamenchuang.models.extend import Article
|
||||
from huamenchuang.scripts.article_seed_data import ARTICLES
|
||||
existing = {t for (t,) in (await db.execute(select(Article.title))).all()}
|
||||
for i, (cat, title, content) in enumerate(ARTICLES):
|
||||
if title not in existing:
|
||||
db.add(Article(title=title, category=cat, content=content, sort=i))
|
||||
await db.commit()
|
||||
print("帮助文章就绪")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
|
||||
Reference in New Issue
Block a user