Files
server-core/tests/test_task_claim.py
T
Pine ea904d3a50 feat(task): 完善任务系统 —— 分类字典/字段/四种接单方式/独占限额/交付时限
- Task 加 published_at/delivery_days/headcount/exclusive/publisher_id/category_id;新增 TaskCategory 分类字典表(35 类)。
- mode 归一 grab/bid/assign/recommend(旧 designated→assign、dispatch→recommend);TaskService 加 assign/recommend/select_recommend,claim 支持独占/限额、published+claimed 均可抢(多人)。
- TaskClaim 支持 source=assign/recommend、status=assigned/recommended/withdrawn + count_active_by_task;TaskCategoryRepository;Database 装配 task_categories。
- 端口:operator GET /admin/task-categories + POST tasks/{id}/assign|recommend|select;opc 同;park /api/tasks 返回 mode/deadline/delivery_days/headcount/exclusive/publisher_name。
- 迁移 0009(tasks 加列+task_categories)已应用+stamp;seed 35 分类字典 + task 新字段(含独占/推荐演示) + 补种已初始化库。
- 测试 test_task_system.py(独占/限额/指派/推荐/list_published/publish/category)+补齐 test_task_claim 语义, 15 passed。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 19:30:16 +08:00

111 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""任务中心·扫码接单:TaskService 状态机 + Repository 访问(自包含内存库)。"""
from __future__ import annotations
from contextlib import asynccontextmanager
import pytest
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.infrastructure.models import Base # noqa: F401 (注册表到 Base)
from app.infrastructure.repositories import Database
from app.services.task_service import TaskService
@asynccontextmanager
async def _db():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
Session = async_sessionmaker(engine, expire_on_commit=False)
async with Session() as session:
yield Database(session=session)
await engine.dispose()
def _grab_published(**kw):
base = dict(
task_code="TK-004-GRAB", title="短视频剪辑", mode="grab",
status="published", category="技术开发", budget_min=1500, budget_max=3000,
display_priority=30,
)
base.update(kw)
return base
@pytest.mark.asyncio
async def test_claim_marks_claimed_and_records_claim():
"""published + grab → claimedclaimed_by/claimed_at),并写 task_claims 流水。"""
async with _db() as db:
t = await db.tasks.create(_grab_published())
actor = {"id": "u_opc_1", "username": "opc01", "nickname": "阿健"}
res = await TaskService(db).claim(t["task_code"], actor, source="scan")
assert res["status"] == "claimed"
assert res["claimed_by"] == "u_opc_1"
assert res["claimed_at"]
claims = await db.task_claims.list_by_task(t["id"])
assert claims and claims[0]["claim_source"] == "scan"
@pytest.mark.asyncio
async def test_claim_disallowed_non_grab_mode():
"""bid 模式不可扫码接单。"""
async with _db() as db:
t = await db.tasks.create(_grab_published(title="UI", mode="bid"))
with pytest.raises(HTTPException):
await TaskService(db).claim(t["task_code"], {"id": "u1"}, "scan")
@pytest.mark.asyncio
async def test_claim_unknown_code_404():
async with _db() as db:
with pytest.raises(HTTPException):
await TaskService(db).claim("TK-NOT-EXIST", {"id": "u1"}, "scan")
@pytest.mark.asyncio
async def test_exclusive_grab_second_claim_rejected():
"""独占 grab:再 claim 应报错(限额语义见 test_task_system)。"""
async with _db() as db:
t = await db.tasks.create(_grab_published(exclusive=True, headcount=1))
await TaskService(db).claim(t["task_code"], {"id": "u1"}, "scan")
with pytest.raises(HTTPException):
await TaskService(db).claim(t["task_code"], {"id": "u2"}, "scan")
@pytest.mark.asyncio
async def test_unlimited_grab_allows_multiple_takers():
"""非独占、不限人数的 grab 允许多人接单。"""
async with _db() as db:
t = await db.tasks.create(_grab_published())
r1 = await TaskService(db).claim(t["task_code"], {"id": "u1"}, "scan")
r2 = await TaskService(db).claim(t["task_code"], {"id": "u2"}, "scan")
assert r1["status"] == "claimed" and r2["status"] == "claimed"
@pytest.mark.asyncio
async def test_doing_then_complete():
"""claimed → doing → completed。"""
async with _db() as db:
t = await db.tasks.create(_grab_published())
actor = {"id": "u_opc_1", "username": "opc01", "nickname": "阿健"}
await TaskService(db).claim(t["task_code"], actor, source="scan")
doing = await TaskService(db).start_doing(t["id"], actor)
assert doing["status"] == "doing" and doing["doing_at"]
done = await TaskService(db).complete(t["id"], actor)
assert done["status"] == "completed"
@pytest.mark.asyncio
async def test_list_published_returns_display_fields():
"""大屏取数:list_published 返回新字段且按 display_priority 降序。"""
async with _db() as db:
await db.tasks.create(_grab_published(task_code="TK-LOW", display_priority=1))
await db.tasks.create(_grab_published(task_code="TK-HIGH", display_priority=99))
items = await db.tasks.list_published()
codes = [t["task_code"] for t in items]
assert "TK-LOW" in codes and "TK-HIGH" in codes
assert codes.index("TK-HIGH") < codes.index("TK-LOW")
assert all("task_code" in t and "claimed_by" in t for t in items)