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:
@@ -0,0 +1 @@
|
||||
"""华门窗包。"""
|
||||
@@ -0,0 +1,49 @@
|
||||
"""认证路由:登录/注册/当前用户。"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok
|
||||
from huamenchuang.schemas.auth import LoginRequest
|
||||
from huamenchuang.services.auth_service import AuthService
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(body: LoginRequest, db: DbSession):
|
||||
"""账号密码登录,签发 JWT。"""
|
||||
try:
|
||||
result = await AuthService(db).login(body.phone_number, body.password)
|
||||
except Exception:
|
||||
return fail("登录失败,请稍后再试")
|
||||
if result is None:
|
||||
return fail("手机号或密码错误")
|
||||
token, hours, user = result
|
||||
return ok(data={
|
||||
"token": token,
|
||||
"expires_hours": hours,
|
||||
"user": {"id": user.id, "nickname": user.nickname, "phone_number": user.phone_number,
|
||||
"company_id": user.company_id},
|
||||
}, message="登录成功")
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register(body: LoginRequest, db: DbSession):
|
||||
"""注册并直接返回 token。"""
|
||||
service = AuthService(db)
|
||||
try:
|
||||
user = await service.register(body.phone_number, body.password)
|
||||
except ValueError as e:
|
||||
return fail(str(e))
|
||||
from huamenchuang.core.security import create_token
|
||||
return ok(data={"token": create_token(user.id), "user_id": user.id}, message="注册成功")
|
||||
|
||||
|
||||
@router.get("/current-user")
|
||||
async def current_user(user: CurrentUser):
|
||||
"""当前登录用户信息。"""
|
||||
return ok(data={
|
||||
"id": user.id, "nickname": user.nickname, "phone_number": user.phone_number,
|
||||
"company_id": user.company_id, "language": user.language,
|
||||
"try_expiration_time": str(user.try_expiration_time) if user.try_expiration_time else None,
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
"""订单路由(Controller 层,只做参数编排,业务在 Service)。"""
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession, require_module
|
||||
from huamenchuang.core.response import fail, ok, paginated
|
||||
from huamenchuang.schemas.order import OrderCreate, OrderProcessUpdate, OrderSearch, OrderUpdate
|
||||
from huamenchuang.services.order_service import OrderService
|
||||
|
||||
router = APIRouter(prefix="/orders", tags=["订单"],
|
||||
dependencies=[Depends(require_module("order"))])
|
||||
|
||||
|
||||
def _to_out(order) -> dict:
|
||||
return {
|
||||
"id": order.id,
|
||||
"order_no": order.order_no,
|
||||
"contract_id": order.contract_id,
|
||||
"pic_url": order.pic_url,
|
||||
"customer_id": order.customer_id,
|
||||
"customer_name": order.customer_name,
|
||||
"phone": order.phone,
|
||||
"address": order.address,
|
||||
"area": float(order.area),
|
||||
"count": order.count,
|
||||
"money": float(order.money),
|
||||
"status": order.status,
|
||||
"status_label": order.status_label,
|
||||
"cut_status": order.cut_status,
|
||||
"sort": order.sort,
|
||||
"note": order.note,
|
||||
"created_at": str(order.created_at),
|
||||
"updated_at": str(order.updated_at),
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_order(body: OrderCreate, user: CurrentUser, db: DbSession):
|
||||
"""创建订单。契约对齐目标站:{contract_id, drawing_data, pic_url}。"""
|
||||
try:
|
||||
order = await OrderService(db).create_order(user, body)
|
||||
except Exception:
|
||||
return fail("创建订单失败")
|
||||
return ok(data=_to_out(order), message="创建成功")
|
||||
|
||||
|
||||
@router.post("/search")
|
||||
async def search_orders(body: OrderSearch, user: CurrentUser, db: DbSession):
|
||||
"""订单搜索(POST 搜索语义对齐目标站)。"""
|
||||
items, total = await OrderService(db).search(user, body)
|
||||
return ok(data=paginated([_to_out(o) for o in items], body.page, body.page_size, total))
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_orders(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15):
|
||||
"""订单分页列表(GET 简化入口)。"""
|
||||
items, total = await OrderService(db).search(
|
||||
user, OrderSearch(page=page, page_size=page_size))
|
||||
return ok(data=paginated([_to_out(o) for o in items], page, page_size, total))
|
||||
|
||||
|
||||
@router.get("/{order_id}")
|
||||
async def order_detail(order_id: int, user: CurrentUser, db: DbSession):
|
||||
"""订单详情:含 drawing_data 与生产进度。"""
|
||||
try:
|
||||
order = await OrderService(db).get_detail(user, order_id)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
data = _to_out(order)
|
||||
data["drawing_data"] = order.drawing_data
|
||||
data["processes"] = [
|
||||
{"id": p.id, "name": p.name, "status": p.status,
|
||||
"finish_time": str(p.finish_time) if p.finish_time else None}
|
||||
for p in order.processes
|
||||
]
|
||||
return ok(data=data)
|
||||
|
||||
|
||||
@router.put("/{order_id}")
|
||||
async def update_order(order_id: int, body: OrderUpdate, user: CurrentUser, db: DbSession):
|
||||
"""更新订单基础字段;action=change_status 时执行状态迁移。"""
|
||||
try:
|
||||
order = await OrderService(db).update_order(user, order_id, body)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
except ValueError as e:
|
||||
return fail(str(e))
|
||||
return ok(data=_to_out(order), message="更新成功")
|
||||
|
||||
|
||||
@router.delete("/{order_id}")
|
||||
async def delete_order(order_id: int, user: CurrentUser, db: DbSession):
|
||||
"""删除订单(软动作:记日志后物理删除)。"""
|
||||
try:
|
||||
await OrderService(db).delete_order(user, order_id)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
return ok(message="删除成功")
|
||||
|
||||
|
||||
@router.post("/{order_id}/process")
|
||||
async def update_process(order_id: int, body: OrderProcessUpdate, user: CurrentUser, db: DbSession):
|
||||
"""更新生产进度节点。"""
|
||||
try:
|
||||
proc = await OrderService(db).update_process(user, order_id, body)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
return ok(data={"id": proc.id, "name": proc.name, "status": proc.status,
|
||||
"finish_time": str(proc.finish_time) if proc.finish_time else None},
|
||||
message="操作成功")
|
||||
|
||||
|
||||
@router.get("/{order_id}/logs")
|
||||
async def order_logs(order_id: int, user: CurrentUser, db: DbSession):
|
||||
"""订单操作日志(对齐 order_up_logs 语义)。"""
|
||||
try:
|
||||
logs = await OrderService(db).list_logs(user, order_id)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
return ok(data=[{"id": g.id, "action": g.action, "detail": g.detail,
|
||||
"created_at": str(g.created_at)} for g in logs])
|
||||
@@ -0,0 +1,9 @@
|
||||
"""路由汇总。"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from huamenchuang.api import auth, orders, windoor
|
||||
|
||||
api_router = APIRouter(prefix="/api")
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(orders.router)
|
||||
api_router.include_router(windoor.router)
|
||||
@@ -0,0 +1,66 @@
|
||||
"""样板库路由:分类树、模板搜索、模板详情(含 drawing_data)。"""
|
||||
from fastapi import APIRouter, Query
|
||||
from sqlalchemy import select
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok
|
||||
from huamenchuang.models.windoor_template import WindoorCatelog, WindoorTemplate
|
||||
|
||||
router = APIRouter(prefix="/windoor", tags=["样板库"])
|
||||
|
||||
|
||||
@router.get("/catelogs")
|
||||
async def catelogs(db: DbSession, user: CurrentUser):
|
||||
"""分类树(与目标站 default_windoor_catelog 同构)。"""
|
||||
stmt = select(WindoorCatelog).where(WindoorCatelog.company_id == user.company_id)
|
||||
nodes = list((await db.execute(stmt)).scalars())
|
||||
by_fid: dict[int, list] = {}
|
||||
for n in nodes:
|
||||
by_fid.setdefault(n.fid, []).append(n)
|
||||
|
||||
def build(fid: int) -> list[dict]:
|
||||
return [
|
||||
{"id": n.id, "name": n.name, "fid": n.fid, "note": n.note, "children": build(n.id)}
|
||||
for n in by_fid.get(fid, [])
|
||||
]
|
||||
|
||||
return ok(data=build(0))
|
||||
|
||||
|
||||
@router.get("/templates")
|
||||
async def search_templates(
|
||||
db: DbSession, user: CurrentUser,
|
||||
name: str = Query(default="", max_length=64),
|
||||
catelog_id: int | None = None,
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=50, ge=1, le=200),
|
||||
):
|
||||
"""模板搜索(列表不含大字段,含平面图地址)。"""
|
||||
conds = [WindoorTemplate.company_id == user.company_id]
|
||||
if name:
|
||||
conds.append(WindoorTemplate.name.like(f"%{name}%"))
|
||||
if catelog_id is not None:
|
||||
conds.append(WindoorTemplate.catelog_id == catelog_id)
|
||||
stmt = (select(WindoorTemplate).where(*conds)
|
||||
.order_by(WindoorTemplate.id).offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=[{
|
||||
"id": t.id, "name": t.name, "pic_url": t.pic_url, "catelog_id": t.catelog_id,
|
||||
"note": t.note, "times": t.times,
|
||||
"low_width": t.low_width, "high_width": t.high_width,
|
||||
"low_height": t.low_height, "high_height": t.high_height,
|
||||
} for t in items])
|
||||
|
||||
|
||||
@router.get("/templates/{template_id}")
|
||||
async def template_detail(template_id: int, db: DbSession, user: CurrentUser):
|
||||
"""模板详情:返回 drawing_data(画布可直接 openFile 渲染)。"""
|
||||
t = await db.get(WindoorTemplate, template_id)
|
||||
if t is None or t.company_id != user.company_id:
|
||||
return fail("模板不存在")
|
||||
return ok(data={
|
||||
"id": t.id, "name": t.name, "drawing_data": t.drawing_data, "pic_url": t.pic_url,
|
||||
"catelog_id": t.catelog_id, "note": t.note,
|
||||
"low_width": t.low_width, "high_width": t.high_width,
|
||||
"low_height": t.low_height, "high_height": t.high_height,
|
||||
})
|
||||
Reference in New Issue
Block a user