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,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",
|
||||
]
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user