feat: P0-3 打印模板体系(区块化模板CRUD+打印中心重构)

This commit is contained in:
HHY
2026-09-25 03:13:38 -07:00
parent 3d0f43d567
commit 87f9869829
8 changed files with 360 additions and 28 deletions
+92
View File
@@ -0,0 +1,92 @@
"""打印模板路由:订单打印模板 CRUD(对齐目标站 order_format 语义)。"""
import json
from fastapi import APIRouter
from pydantic import BaseModel, Field
from sqlalchemy import select
from huamenchuang.core.deps import CurrentUser, DbSession
from huamenchuang.core.response import fail, ok
from huamenchuang.models.extend import OrderFormat
router = APIRouter(prefix="/order-formats", tags=["打印模板"])
BLOCK_TYPES = ("title", "info", "image", "table", "sign")
TABLE_KINDS = ("process", "bom")
class FormatBody(BaseModel):
name: str = Field(min_length=1, max_length=64)
order_type: int = Field(default=0, ge=0, le=1)
blocks: list[dict] = Field(default_factory=list, description="模板区块列表")
is_default: int = Field(default=0, ge=0, le=1)
def _validate_blocks(blocks: list[dict]) -> str | None:
for b in blocks:
if b.get("type") not in BLOCK_TYPES:
return f"不支持的区块类型: {b.get('type')}"
if b.get("type") == "table" and b.get("kind") not in TABLE_KINDS:
return "table 区块需要 kind: process/bom"
return None
def _to_out(f: OrderFormat) -> dict:
return {"id": f.id, "name": f.name, "order_type": f.order_type,
"blocks": json.loads(f.blocks or "[]"), "is_default": f.is_default, "sort": f.sort}
@router.get("")
async def list_formats(user: CurrentUser, db: DbSession):
stmt = (select(OrderFormat).where(OrderFormat.company_id == user.company_id)
.order_by(OrderFormat.sort, OrderFormat.id))
rows = list((await db.execute(stmt)).scalars())
return ok(data=[_to_out(r) for r in rows])
@router.post("")
async def create_format(body: FormatBody, user: CurrentUser, db: DbSession):
err = _validate_blocks(body.blocks)
if err:
return fail(err)
fmt = OrderFormat(company_id=user.company_id,
name=body.name, order_type=body.order_type,
blocks=json.dumps(body.blocks, ensure_ascii=False),
is_default=body.is_default)
if body.is_default: # 唯一默认
for other in (await db.execute(select(OrderFormat).where(
OrderFormat.company_id == user.company_id))).scalars():
other.is_default = 0
db.add(fmt)
await db.flush()
return ok(data=_to_out(fmt), message="模板已创建")
@router.put("/{format_id}")
async def update_format(format_id: int, body: FormatBody, user: CurrentUser, db: DbSession):
fmt = await db.get(OrderFormat, format_id)
if fmt is None or fmt.company_id != user.company_id:
return fail("模板不存在")
err = _validate_blocks(body.blocks)
if err:
return fail(err)
fmt.name = body.name
fmt.order_type = body.order_type
fmt.blocks = json.dumps(body.blocks, ensure_ascii=False)
if body.is_default:
for other in (await db.execute(select(OrderFormat).where(
OrderFormat.company_id == user.company_id))).scalars():
other.is_default = 0
fmt.is_default = 1
else:
fmt.is_default = 0
return ok(data=_to_out(fmt), message="模板已更新")
@router.delete("/{format_id}")
async def delete_format(format_id: int, user: CurrentUser, db: DbSession):
fmt = await db.get(OrderFormat, format_id)
if fmt is None or fmt.company_id != user.company_id:
return fail("模板不存在")
await db.delete(fmt)
return ok(message="模板已删除")
+2
View File
@@ -7,6 +7,7 @@ from huamenchuang.api import (
contracts,
customers,
optimize,
order_format,
orders,
platform,
quote,
@@ -25,6 +26,7 @@ api_router.include_router(windoor.router)
api_router.include_router(optimize.router)
api_router.include_router(quote.router)
api_router.include_router(script.router)
api_router.include_router(order_format.router)
api_router.include_router(warehouse.router)
api_router.include_router(business.router)
api_router.include_router(platform.router)
+2
View File
@@ -19,6 +19,7 @@ from huamenchuang.models.extend import (
Article,
CompanyParam,
Device,
OrderFormat,
PayOrder,
SmsCode,
)
@@ -37,6 +38,7 @@ __all__ = [
"Device",
"MeasureOrder",
"Order",
"OrderFormat",
"OrderLog",
"OrderProcess",
"PayOrder",
+16
View File
@@ -68,6 +68,22 @@ class Article(Base):
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
class OrderFormat(Base):
"""订单打印模板(对齐目标站 order_format:可自定义模板区块与默认模板)。"""
__tablename__ = "order_formats"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
company_id: Mapped[int] = mapped_column(default=1, index=True)
name: Mapped[str] = mapped_column(String(64))
order_type: Mapped[int] = mapped_column(default=0) # 0门窗订单 1阳光房订单
# 模板区块 JSON: [{type: title|info|image|table|sign, text?, fields?, kind?}]
blocks: Mapped[str] = mapped_column(Text, default="[]")
is_default: Mapped[int] = mapped_column(default=0)
sort: Mapped[int] = mapped_column(default=0)
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
class SmsCode(Base):
"""短信验证码(provider 适配层:未配置短信签名时走 mock 通道)。"""
+29
View File
@@ -117,6 +117,35 @@ async def seed() -> None:
await db.commit()
print("价格脚本就绪")
# 7) 打印模板(幂等:按名称去重)
async with SessionLocal() as db:
from huamenchuang.models.extend import OrderFormat
FORMATS = [
("标准生产订单", [
{"type": "title", "text": "生产订单"},
{"type": "info", "fields": ["order_no", "customer_name", "phone", "address", "count", "money"]},
{"type": "image"},
{"type": "table", "kind": "process"},
{"type": "sign"},
], 1),
("生产订单+BOM下料单", [
{"type": "title", "text": "生产订单"},
{"type": "info", "fields": ["order_no", "customer_name", "phone", "address",
"count", "area", "money", "created_at"]},
{"type": "image"},
{"type": "table", "kind": "bom"},
{"type": "table", "kind": "process"},
{"type": "sign"},
], 0),
]
existing = {n for (n,) in (await db.execute(select(OrderFormat.name))).all()}
for i, (name, blocks, is_default) in enumerate(FORMATS):
if name not in existing:
db.add(OrderFormat(name=name, order_type=0, sort=i, is_default=is_default,
blocks=__import__("json").dumps(blocks, ensure_ascii=False)))
await db.commit()
print("打印模板就绪")
if __name__ == "__main__":
asyncio.run(seed())