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,47 @@
|
||||
"""认证业务逻辑。"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from huamenchuang.core.security import create_token
|
||||
from huamenchuang.models.user import User
|
||||
from huamenchuang.repositories.user_repo import UserRepository
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""PBKDF2-SHA256(标准库实现,无额外依赖)。"""
|
||||
salt = os.urandom(16)
|
||||
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100_000)
|
||||
return salt.hex() + "$" + dk.hex()
|
||||
|
||||
|
||||
def verify_password(password: str, stored: str) -> bool:
|
||||
try:
|
||||
salt_hex, dk_hex = stored.split("$", 1)
|
||||
except ValueError:
|
||||
return False
|
||||
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), bytes.fromhex(salt_hex), 100_000)
|
||||
return hmac.compare_digest(dk.hex(), dk_hex)
|
||||
|
||||
|
||||
class AuthService:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.repo = UserRepository(db)
|
||||
|
||||
async def login(self, phone_number: str, password: str) -> tuple[str, int, User] | None:
|
||||
user = await self.repo.get_by_phone(phone_number)
|
||||
if user is None or not verify_password(password, user.password_hash):
|
||||
return None
|
||||
return create_token(user.id), 24, user
|
||||
|
||||
async def register(self, phone_number: str, password: str, nickname: str = "") -> User:
|
||||
if await self.repo.get_by_phone(phone_number):
|
||||
raise ValueError("手机号已注册")
|
||||
user = await self.repo.create(User(
|
||||
phone_number=phone_number,
|
||||
password_hash=hash_password(password),
|
||||
nickname=nickname or f"用户{phone_number[-4:]}",
|
||||
))
|
||||
return user
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user