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
+17
View File
@@ -0,0 +1,17 @@
"""ORM 模型统一导出。"""
from huamenchuang.models.contract import Contract, Customer
from huamenchuang.models.order import DEFAULT_PROCESSES, Order, OrderLog, OrderProcess
from huamenchuang.models.user import User
from huamenchuang.models.windoor_template import WindoorCatelog, WindoorTemplate
__all__ = [
"Contract",
"Customer",
"DEFAULT_PROCESSES",
"Order",
"OrderLog",
"OrderProcess",
"User",
"WindoorCatelog",
"WindoorTemplate",
]
+55
View File
@@ -0,0 +1,55 @@
"""客户、合同模型(订单的上游实体)。"""
import datetime
from sqlalchemy import DateTime, ForeignKey, Numeric, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from huamenchuang.core.database import Base
class Customer(Base):
__tablename__ = "customers"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(128))
phone: Mapped[str] = mapped_column(String(32), default="")
address: Mapped[str] = mapped_column(String(255), default="")
note: Mapped[str] = mapped_column(Text, default="")
company_id: Mapped[int] = mapped_column(default=1, index=True)
salesmen: Mapped[str | None] = mapped_column(String(64), nullable=True)
discount: Mapped[float | None] = mapped_column(Numeric(6, 3), nullable=True)
balance: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
updated_at: Mapped[datetime.datetime] = mapped_column(
DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
)
class Contract(Base):
"""合同/意向单:目标站契约 status="意向单" 起步,oids[] 记录关联订单。"""
__tablename__ = "contracts"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
contract_no: Mapped[str] = mapped_column(String(32), unique=True) # 如 FG20260920001
customer_id: Mapped[int | None] = mapped_column(ForeignKey("customers.id"), nullable=True, index=True)
customer_name: Mapped[str] = mapped_column(String(128), default="")
phone: Mapped[str] = mapped_column(String(32), default="")
address: Mapped[str] = mapped_column(String(255), default="")
area: Mapped[float] = mapped_column(default=0) # ㎡
count: Mapped[int] = mapped_column(default=0)
money: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
# 0=意向单 1=正式单(已签) 2=已完成 3=已作废
status: Mapped[int] = mapped_column(default=0, index=True)
sign_status: Mapped[int] = mapped_column(default=0)
sign_picture_url: Mapped[str | None] = mapped_column(String(255), nullable=True)
discount: Mapped[float] = mapped_column(Numeric(6, 3), default=1)
note: Mapped[str] = mapped_column(Text, default="")
company_id: Mapped[int] = mapped_column(default=1, index=True)
user_id: Mapped[int] = mapped_column(default=0, index=True)
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
updated_at: Mapped[datetime.datetime] = mapped_column(
DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
)
customer: Mapped[Customer | None] = relationship(lazy="joined")
+103
View File
@@ -0,0 +1,103 @@
"""订单模块模型:订单本体、生产进度、操作日志。"""
import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from huamenchuang.core.database import Base
if TYPE_CHECKING:
from huamenchuang.models.contract import Contract
class Order(Base):
"""订单:由合同 + 画布 drawing_data 创建(对齐目标站创建契约)。"""
__tablename__ = "orders"
# 状态机(自研设计,语义对齐目标站"意向单/正式单"流转)
STATUS_INTENTION = 0 # 意向单
STATUS_FORMAL = 1 # 正式单
STATUS_PRODUCING = 2 # 生产中
STATUS_FINISHED = 3 # 已完工
STATUS_CANCELED = 4 # 已取消
STATUS_LABELS = {0: "意向单", 1: "正式单", 2: "生产中", 3: "已完工", 4: "已取消"}
# 允许的状态迁移
TRANSITIONS: dict[int, set[int]] = {
0: {1, 4},
1: {2, 4},
2: {3},
3: set(),
4: set(),
}
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_no: Mapped[str] = mapped_column(String(32), unique=True) # DO+YYYYMMDD+4位流水
contract_id: Mapped[int | None] = mapped_column(
ForeignKey("contracts.id"), nullable=True, index=True
)
drawing_data: Mapped[str] = mapped_column(Text, default="") # shapeManager 序列化 JSON
pic_url: Mapped[str] = mapped_column(String(255), default="") # 平面图/缩略图
# 冗余客户信息(下单快照,客户资料变更不影响订单)
customer_id: Mapped[int | None] = mapped_column(ForeignKey("customers.id"), nullable=True)
customer_name: Mapped[str] = mapped_column(String(128), default="")
phone: Mapped[str] = mapped_column(String(32), default="")
address: Mapped[str] = mapped_column(String(255), default="")
area: Mapped[float] = mapped_column(Numeric(10, 3), default=0)
count: Mapped[int] = mapped_column(Integer, default=0)
money: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
status: Mapped[int] = mapped_column(Integer, default=0, index=True)
# 拆单/切割状态(对齐目标站 cut_status)
cut_status: Mapped[int] = mapped_column(Integer, default=0)
sort: Mapped[int] = mapped_column(Integer, default=0)
note: Mapped[str] = mapped_column(Text, default="")
company_id: Mapped[int] = mapped_column(default=1, index=True)
user_id: Mapped[int] = mapped_column(default=0, index=True)
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
updated_at: Mapped[datetime.datetime] = mapped_column(
DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
)
contract: Mapped["Contract | None"] = relationship("Contract", lazy="joined")
processes: Mapped[list["OrderProcess"]] = relationship(
back_populates="order", cascade="all, delete-orphan", order_by="OrderProcess.sort"
)
@property
def status_label(self) -> str:
return self.STATUS_LABELS.get(self.status, "未知")
class OrderProcess(Base):
"""订单生产进度节点(下料→组装→玻璃→打胶→五金→质检→完工)。"""
__tablename__ = "order_processes"
__table_args__ = (UniqueConstraint("order_id", "name", name="uq_order_process"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
name: Mapped[str] = mapped_column(String(32))
status: Mapped[int] = mapped_column(Integer, default=0) # 0未开始 1进行中 2完成
finish_time: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
order: Mapped[Order] = relationship(back_populates="processes")
DEFAULT_PROCESSES = ["下料", "组装", "玻璃", "打胶", "五金", "质检", "完工"]
class OrderLog(Base):
"""订单操作日志(对齐目标站 order_up_logs)。"""
__tablename__ = "order_logs"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
user_id: Mapped[int] = mapped_column(default=0)
action: Mapped[str] = mapped_column(String(32)) # create/update_status/delete_process...
detail: Mapped[str] = mapped_column(String(255), default="")
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
+26
View File
@@ -0,0 +1,26 @@
"""用户模型。"""
import datetime
from sqlalchemy import DateTime, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from huamenchuang.core.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
nickname: Mapped[str] = mapped_column(String(64), default="")
phone_number: Mapped[str] = mapped_column(String(20), unique=True)
password_hash: Mapped[str] = mapped_column(String(128), default="")
company_id: Mapped[int] = mapped_column(default=1, index=True)
email: Mapped[str | None] = mapped_column(String(128), nullable=True)
language: Mapped[str] = mapped_column(String(16), default="zh-CN")
# 套餐到期时间,空=试用
try_expiration_time: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True)
note: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
updated_at: Mapped[datetime.datetime] = mapped_column(
DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
)
@@ -0,0 +1,44 @@
"""样板库模型(drawing_data 与目标站 254 套样本同构)。"""
import datetime
from sqlalchemy import DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from huamenchuang.core.database import Base
class WindoorTemplate(Base):
"""官方/企业窗型样板。drawing_data 为 shapeManager 序列化 JSON。"""
__tablename__ = "windoor_templates"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(128), default="")
drawing_data: Mapped[str] = mapped_column(Text) # 原样保留编辑器 JSON 字符串
pic_url: Mapped[str] = mapped_column(String(255), default="")
catelog_id: Mapped[int] = mapped_column(Integer, default=0, index=True)
note: Mapped[str] = mapped_column(String(255), default="")
times: Mapped[int] = mapped_column(Integer, default=0) # 引用次数
# 尺寸限制(毫米),用于编辑器校验
low_width: Mapped[int] = mapped_column(Integer, default=0)
high_width: Mapped[int] = mapped_column(Integer, default=0)
low_height: Mapped[int] = mapped_column(Integer, default=0)
high_height: Mapped[int] = mapped_column(Integer, default=0)
is_default: Mapped[int] = mapped_column(Integer, default=1) # 1=官方样板
company_id: Mapped[int] = mapped_column(default=1, index=True)
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
updated_at: Mapped[datetime.datetime] = mapped_column(
DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
)
class WindoorCatelog(Base):
"""样板分类树(推拉门{平开,推拉,复古门,肯德基门,中式门窗,转角窗})。"""
__tablename__ = "windoor_catelogs"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(64))
fid: Mapped[int] = mapped_column(Integer, default=0, index=True)
note: Mapped[str] = mapped_column(String(255), default="")
company_id: Mapped[int] = mapped_column(default=1, index=True)