feat(services): 核心业务逻辑 — 接收/返工/裂变/记录/标签打印

task_service:
- receive_task() — PENDING→WIP, 记录 received_at
- reject_task() — →REJECTED + 自动返工闭环
- transfer_task() — →COMPLETED + 多路裂变 + virtual_warehouse入库
- add_task_record() — 图文记录追加
- 全链路 selectinload(Task.records)
- 全部 now 改用 get_beijing_time()

product_service:
- create_product() — PG Sequence 自动生成16位HEX
- update_overall_status() — 宏观状态校验+更新
- 扫码返回完整 task_tree (含 records)
- order_no 自由文本→自动创建 ProductionOrder
- update_product() 支持 order_no 编辑

label_service:
- 480×360 工业级排版 (QR+名/规/单+底部通栏码)
- simhei.ttf 34px/28px + stroke_width 描边
- TSPL Socket 发送到打标机
This commit is contained in:
2026-08-05 14:00:55 +08:00
parent 79583cf8f4
commit 1fed716829
4 changed files with 415 additions and 20 deletions

View File

@ -10,7 +10,7 @@ from app.models.product import Product
from app.models.production_order import ProductionOrder
from app.models.task import Task
from app.schemas.product import ProductCreate, ProductUpdate, ProductResponse, ProductScanResponse
from app.schemas.task import TaskSummaryResponse, TaskResponse
from app.schemas.task import TaskSummaryResponse, TaskResponse, TaskRecordResponse
def _task_to_response(task: Task) -> TaskResponse:
@ -18,6 +18,8 @@ def _task_to_response(task: Task) -> TaskResponse:
return TaskResponse(
id=task.id,
product_id=task.product_id,
product_sn=task.product.serial_number if task.product else "",
product_material=task.product.material_name or task.product.material_id or "" if task.product else "",
parent_task_id=task.parent_task_id,
task_name=task.task_name,
assignee_id=task.assignee_id,
@ -29,6 +31,7 @@ def _task_to_response(task: Task) -> TaskResponse:
completed_at=task.completed_at,
created_at=task.created_at,
child_tasks=[_task_to_response(c) for c in task.child_tasks],
records=[TaskRecordResponse.model_validate(r) for r in (task.records or [])],
)
@ -37,7 +40,7 @@ async def _load_task_tree(db: AsyncSession, product_id: uuid.UUID) -> list[TaskR
# 先取顶层任务
result = await db.execute(
select(Task)
.options(selectinload(Task.child_tasks))
.options(selectinload(Task.child_tasks), selectinload(Task.records))
.where(
Task.product_id == product_id,
Task.parent_task_id.is_(None),
@ -51,7 +54,7 @@ async def _load_task_tree(db: AsyncSession, product_id: uuid.UUID) -> list[TaskR
for child in t.child_tasks:
child_result = await db.execute(
select(Task)
.options(selectinload(Task.child_tasks))
.options(selectinload(Task.child_tasks), selectinload(Task.records))
.where(Task.id == child.id)
)
refreshed = child_result.scalar_one()
@ -98,11 +101,17 @@ async def get_product_by_serial(db: AsyncSession, serial_number: str) -> Product
return ProductScanResponse(
id=product.id,
serial_number=product.serial_number,
external_serial=product.external_serial,
order_id=product.order_id,
order_no=product.order.order_no if product.order else "",
material_id=product.material_id,
material_name=product.material_name,
spec_model=product.spec_model,
category=product.category,
material_type=product.material_type,
parent_product_id=product.parent_product_id,
current_location_id=product.current_location_id,
overall_status=product.overall_status,
status=product.status,
created_at=product.created_at,
top_level_tasks=[
@ -129,23 +138,134 @@ async def get_product(db: AsyncSession, product_id: uuid.UUID) -> Product:
async def create_product(db: AsyncSession, data: ProductCreate) -> ProductResponse:
"""创建产品"""
product = Product(**data.model_dump())
"""创建产品 — 自动生成 16 位 HEX 序列号"""
from app.services.counter_service import ensure_sequence, next_hex_id
from app.models.production_order import ProductionOrder
await ensure_sequence(db)
hex_id = await next_hex_id(db)
# 处理订单: 如果传了 order_no 但没传 order_id,查找或创建
order_id = data.order_id
if not order_id and data.order_no:
result = await db.execute(
select(ProductionOrder).where(ProductionOrder.order_no == data.order_no.strip())
)
existing = result.scalar_one_or_none()
if existing:
order_id = existing.id
else:
new_order = ProductionOrder(order_no=data.order_no.strip())
db.add(new_order)
await db.flush()
order_id = new_order.id
product = Product(
serial_number=hex_id,
order_id=order_id,
material_id=data.material_id,
material_name=data.material_name or None,
spec_model=data.spec_model or None,
category=data.category or None,
material_type=data.material_type or None,
external_serial=data.external_serial,
parent_product_id=data.parent_product_id,
)
db.add(product)
await db.commit()
await db.refresh(product)
return ProductResponse.model_validate(product)
await db.refresh(product, ["order"])
return ProductResponse(
id=product.id,
serial_number=product.serial_number,
external_serial=product.external_serial,
order_id=product.order_id,
order_no=product.order.order_no if product.order else (data.order_no or ""),
material_id=product.material_id,
material_name=product.material_name,
spec_model=product.spec_model,
category=product.category,
material_type=product.material_type,
parent_product_id=product.parent_product_id,
current_location_id=product.current_location_id,
overall_status=product.overall_status,
status=product.status,
created_at=product.created_at,
)
async def update_product(db: AsyncSession, product_id: uuid.UUID, data: ProductUpdate) -> ProductResponse:
"""更新产品"""
from app.models.production_order import ProductionOrder
product = await get_product(db, product_id)
update_data = data.model_dump(exclude_unset=True)
# 处理 order_no → order_id 映射
if "order_no" in update_data:
order_no_val = update_data.pop("order_no")
if order_no_val and order_no_val.strip():
result = await db.execute(
select(ProductionOrder).where(ProductionOrder.order_no == order_no_val.strip())
)
existing = result.scalar_one_or_none()
if existing:
product.order_id = existing.id
else:
new_order = ProductionOrder(order_no=order_no_val.strip())
db.add(new_order)
await db.flush()
product.order_id = new_order.id
else:
product.order_id = None
for field, value in update_data.items():
setattr(product, field, value)
await db.commit()
await db.refresh(product, ["order"])
return ProductResponse(
id=product.id,
serial_number=product.serial_number,
external_serial=product.external_serial,
order_id=product.order_id,
order_no=product.order.order_no if product.order else "",
material_id=product.material_id,
material_name=product.material_name,
spec_model=product.spec_model,
category=product.category,
material_type=product.material_type,
parent_product_id=product.parent_product_id,
current_location_id=product.current_location_id,
overall_status=product.overall_status,
status=product.status,
created_at=product.created_at,
)
VALID_OVERALL_STATUS = {"备货", "生产", "测试", "维修", "在库"}
async def update_overall_status(db: AsyncSession, serial_number: str, status_value: str) -> ProductScanResponse:
"""更新产品宏观状态"""
if status_value not in VALID_OVERALL_STATUS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"无效状态: {status_value},合法值: {', '.join(sorted(VALID_OVERALL_STATUS))}",
)
result = await db.execute(
select(Product)
.options(selectinload(Product.order))
.where(Product.serial_number == serial_number)
)
product = result.scalar_one_or_none()
if not product:
raise HTTPException(status_code=404, detail=f"未找到序列号 {serial_number} 的产品")
product.overall_status = status_value
await db.commit()
await db.refresh(product)
return ProductResponse.model_validate(product)
return await get_product_by_serial(db, serial_number)
async def get_all_products(db: AsyncSession, skip: int = 0, limit: int = 50) -> list[ProductResponse]:
@ -158,4 +278,23 @@ async def get_all_products(db: AsyncSession, skip: int = 0, limit: int = 50) ->
.order_by(Product.created_at.desc())
)
products = result.scalars().all()
return [ProductResponse.model_validate(p) for p in products]
return [
ProductResponse(
id=p.id,
serial_number=p.serial_number,
external_serial=p.external_serial,
order_id=p.order_id,
order_no=p.order.order_no if p.order else "",
material_id=p.material_id,
material_name=p.material_name,
spec_model=p.spec_model,
category=p.category,
material_type=p.material_type,
parent_product_id=p.parent_product_id,
current_location_id=p.current_location_id,
overall_status=p.overall_status,
status=p.status,
created_at=p.created_at,
)
for p in products
]