feat: 前后端全量开发 v0.2

后端:
- 客户模块 CRUD+搜索 (api/services/repositories 分层)
- 合同模块: FG单号规则/意向单转正式单/订单创建自动快照客户信息
- 优化下料: FFD首递减装箱算法(利用率统计/余料/未排入清单)
前端 (Vue3+TS+Element Plus):
- 7页面: 登录/工作台/订单中心/合同/客户/样板库/优化下料
- 自研二维窗型画布渲染器: 解析drawing_data渲染玻璃区域/外框/
  扇开向标记/尺寸标注, 支持缩放平移复位
- 样板库与订单联动(用此样板下单自动带图纸)
- axios统一envelope拦截/登录守卫/付费墙提示
工程:
- SPA history 回退路由, 构建产物挂载后端
- 全接口回归 6/6 通过
This commit is contained in:
HHY
2026-09-20 08:46:04 -07:00
parent d3e6de585c
commit f7a555dd6c
35 changed files with 3702 additions and 10 deletions
@@ -0,0 +1,44 @@
"""合同业务逻辑层:意向单→正式单流转。"""
from sqlalchemy.ext.asyncio import AsyncSession
from huamenchuang.models.contract import Contract
from huamenchuang.models.user import User
from huamenchuang.repositories.contract_repo import ContractRepository
class ContractService:
def __init__(self, db: AsyncSession) -> None:
self.repo = ContractRepository(db)
async def create(self, user: User, data: dict) -> Contract:
contract = await self.repo.create(Contract(
company_id=user.company_id,
user_id=user.id,
contract_no=await self.repo.next_contract_no(user.company_id),
**data,
))
return contract
async def update(self, user: User, contract_id: int, data: dict) -> Contract:
contract = await self.repo.get(contract_id, user.company_id)
if contract is None:
raise KeyError("合同不存在")
for k, v in data.items():
setattr(contract, k, v)
return contract
async def convert_to_formal(self, user: User, contract_id: int) -> Contract:
"""意向单转正式单(对齐目标站 convert-contract 语义)。"""
contract = await self.repo.get(contract_id, user.company_id)
if contract is None:
raise KeyError("合同不存在")
if contract.status != 0:
raise ValueError("仅意向单可转正式单")
contract.status = 1
return contract
async def delete(self, user: User, contract_id: int) -> None:
contract = await self.repo.get(contract_id, user.company_id)
if contract is None:
raise KeyError("合同不存在")
await self.repo.delete(contract)
@@ -0,0 +1,28 @@
"""客户业务逻辑层。"""
from sqlalchemy.ext.asyncio import AsyncSession
from huamenchuang.models.contract import Customer
from huamenchuang.models.user import User
from huamenchuang.repositories.customer_repo import CustomerRepository
class CustomerService:
def __init__(self, db: AsyncSession) -> None:
self.repo = CustomerRepository(db)
async def create(self, user: User, data: dict) -> Customer:
return await self.repo.create(Customer(company_id=user.company_id, **data))
async def update(self, user: User, customer_id: int, data: dict) -> Customer:
customer = await self.repo.get(customer_id, user.company_id)
if customer is None:
raise KeyError("客户不存在")
for k, v in data.items():
setattr(customer, k, v)
return customer
async def delete(self, user: User, customer_id: int) -> None:
customer = await self.repo.get(customer_id, user.company_id)
if customer is None:
raise KeyError("客户不存在")
await self.repo.delete(customer)
@@ -0,0 +1,104 @@
"""一维优化下料服务:首递减(FFD)装箱算法,输出切割方案与利用率。
对齐目标站 optimize1d 语义:输入订单型材需求 + 库存原料,输出每根原料的切割组合。
"""
from dataclasses import dataclass, field
@dataclass
class BarDemand:
"""一段型材需求。"""
length: int # 需求长度(mm)
qty: int # 数量
label: str = "" # 用途标签
@dataclass
class StockBar:
"""库存原料。"""
length: int # 原料长度(mm)
qty: int # 库存根数
label: str = ""
@dataclass
class CutPiece:
length: int
label: str = ""
@dataclass
class CutPlan:
"""一根原料的切割方案。"""
stock_length: int
stock_label: str
pieces: list[CutPiece] = field(default_factory=list)
waste: int = 0 # 余料浪费
# 锯口损耗(mm),每次切割损耗一段
KERF_DEFAULT = 5
def optimize_1d(demands: list[BarDemand], stocks: list[StockBar],
kerf: int = KERF_DEFAULT) -> dict:
"""首递减装箱:长件优先装入可容纳的原料,输出方案与统计。"""
# 展开需求并降序
pieces: list[CutPiece] = []
for d in demands:
pieces.extend(CutPiece(length=d.length, label=d.label) for _ in range(max(0, d.qty)))
pieces.sort(key=lambda p: p.length, reverse=True)
# 展开库存(可切割长度 = 原料长)
avail: list[dict] = []
for s in stocks:
for _ in range(max(0, s.qty)):
avail.append({"length": s.length, "label": s.label, "pieces": [], "used": 0})
unplaced: list[CutPiece] = []
for piece in pieces:
# 找第一根装得下的原料(剩余空间 >= 件长 + 锯口)
placed = False
for bar in avail:
remain = bar["length"] - bar["used"]
cost = piece.length + (kerf if bar["pieces"] else 0)
if remain >= cost:
bar["pieces"].append(piece)
bar["used"] += cost
placed = True
break
if not placed:
unplaced.append(piece)
plans = [
CutPlan(
stock_length=b["length"],
stock_label=b["label"],
pieces=b["pieces"],
waste=b["length"] - b["used"],
)
for b in avail if b["pieces"]
]
total_demand = sum(p.length for p in pieces)
total_stock = sum(p.stock_length for p in plans)
return {
"plans": [
{
"stock_length": p.stock_length,
"stock_label": p.stock_label,
"pieces": [{"length": c.length, "label": c.label} for c in p.pieces],
"waste": p.waste,
}
for p in plans
],
"unplaced": [{"length": c.length, "label": c.label} for c in unplaced],
"stats": {
"total_pieces": len(pieces),
"placed_pieces": len(pieces) - len(unplaced),
"stock_bars_used": len(plans),
"total_demand_mm": total_demand,
"total_stock_mm": total_stock,
"utilization": round(total_demand / total_stock, 4) if total_stock else 0,
"kerf_mm": kerf,
},
}
+15 -2
View File
@@ -14,13 +14,26 @@ class OrderService:
self.repo = OrderRepository(db)
async def create_order(self, user: User, payload: OrderCreate) -> Order:
"""创建订单:对齐目标站 {contract_id, drawing_data, pic_url} 契约。"""
"""创建订单:对齐目标站 {contract_id, drawing_data, pic_url} 契约。
指定 contract_id 时自动快照合同客户信息。"""
customer_id = payload.customer_id
customer_name = phone = address = ""
if payload.contract_id:
from huamenchuang.repositories.contract_repo import ContractRepository
contract = await ContractRepository(self.db).get(payload.contract_id, user.company_id)
if contract is None:
raise KeyError("关联合同不存在")
customer_id = contract.customer_id
customer_name, phone, address = contract.customer_name, contract.phone, contract.address
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,
customer_id=customer_id,
customer_name=customer_name,
phone=phone,
address=address,
note=payload.note,
money=payload.money,
count=payload.count,