feat: 订单模块 v0.1 实现与样板二维平面图全量采集

- 后端分层架构: api(Controller)/services(业务)/repositories(数据)/models(ORM)
- 订单模块: 创建(契约对齐目标站{contract_id,drawing_data,pic_url})、搜索、
  详情、更新、删除、状态机(意向单→正式单→生产中→完工/取消,非法迁移拒绝)、
  7节点生产进度、审计日志
- 认证模块: JWT签发(HS256/24h)、PBKDF2密码、注册登录
- 样板库API: 分类树/模板搜索/模板详情, drawing_data 与采集样本同构
- 254张样板二维平面图全部采集并静态挂载 /static/windoor-images/
- 种子脚本: 管理员+分类树+254套样板入库
- 统一响应envelope与付费墙(code=10086)/权限(code=210)语义
- 端到端冒烟测试7/7通过, 更新API.md §12自研接口规范
This commit is contained in:
HHY
2026-09-20 08:16:03 -07:00
parent 7554f2e3c7
commit d3e6de585c
288 changed files with 1835 additions and 9 deletions
@@ -0,0 +1,90 @@
"""订单业务逻辑层:状态机、进度初始化、审计日志。"""
from sqlalchemy.ext.asyncio import AsyncSession
from huamenchuang.core.deps import ModuleLockedError, NotAuthedError
from huamenchuang.models.order import DEFAULT_PROCESSES, Order, OrderProcess
from huamenchuang.models.user import User
from huamenchuang.repositories.order_repo import OrderRepository
from huamenchuang.schemas.order import OrderCreate, OrderProcessUpdate, OrderSearch, OrderUpdate
class OrderService:
def __init__(self, db: AsyncSession) -> None:
self.db = db
self.repo = OrderRepository(db)
async def create_order(self, user: User, payload: OrderCreate) -> Order:
"""创建订单:对齐目标站 {contract_id, drawing_data, pic_url} 契约。"""
order = Order(
order_no=await self.repo.next_order_no(user.company_id),
contract_id=payload.contract_id,
drawing_data=payload.drawing_data,
pic_url=payload.pic_url,
customer_id=payload.customer_id,
note=payload.note,
money=payload.money,
count=payload.count,
area=payload.area,
status=payload.status,
company_id=user.company_id,
user_id=user.id,
)
order = await self.repo.create(order)
# 初始化默认生产进度节点(直接 add,避免触发关系惰性加载)
for i, name in enumerate(DEFAULT_PROCESSES):
self.db.add(OrderProcess(order_id=order.id, name=name, status=0, sort=i))
await self.repo.add_log(order.id, user.id, "create", f"创建订单 {order.order_no}")
return order
async def search(self, user: User, q: OrderSearch) -> tuple[list[Order], int]:
return await self.repo.search(
user.company_id, page=q.page, page_size=q.page_size, keyword=q.keyword,
status=q.status, contract_id=q.contract_id,
start_date=q.start_date, end_date=q.end_date, order_by=q.order_by,
)
async def get_detail(self, user: User, order_id: int) -> Order:
order = await self.repo.get(order_id, user.company_id)
if order is None:
raise KeyError("订单不存在")
return order
async def update_order(self, user: User, order_id: int, payload: OrderUpdate) -> Order:
order = await self.get_detail(user, order_id)
data = payload.model_dump(exclude_none=True, exclude={"action", "target_status"})
for k, v in data.items():
setattr(order, k, v)
if payload.action == "change_status":
self._transition(order, payload.target_status)
await self.repo.add_log(order.id, user.id, "change_status",
f"{order.STATUS_LABELS.get(order.status)}→{order.STATUS_LABELS.get(payload.target_status)}")
else:
await self.repo.add_log(order.id, user.id, "update", f"更新字段: {', '.join(data) or '-'}")
return order
@staticmethod
def _transition(order: Order, target: int | None) -> None:
"""状态机校验:非法迁移直接拒绝。"""
if target is None:
raise ValueError("缺少 target_status")
if target not in Order.TRANSITIONS.get(order.status, set()):
raise ValueError(f"不允许的状态迁移: {order.status_label}→{order.STATUS_LABELS.get(target)}")
order.status = target
async def update_process(self, user: User, order_id: int, payload: OrderProcessUpdate) -> OrderProcess:
order = await self.get_detail(user, order_id)
proc = await self.repo.upsert_process(order.id, payload.name, payload.status)
await self.repo.add_log(order.id, user.id, "up-order-process", f"{payload.name}={payload.status}")
return proc # type: ignore[return-value]
async def list_logs(self, user: User, order_id: int) -> list:
await self.get_detail(user, order_id)
return await self.repo.list_logs(order_id)
async def delete_order(self, user: User, order_id: int) -> None:
order = await self.get_detail(user, order_id)
await self.repo.add_log(order.id, user.id, "delete", f"删除订单 {order.order_no}")
await self.repo.delete(order)
__all__ = ["OrderService", "ModuleLockedError", "NotAuthedError"]