feat:报名

This commit is contained in:
Pine
2026-09-09 14:23:26 +08:00
parent 8276bef63b
commit d5b322dc22
9 changed files with 723 additions and 16 deletions
+48 -4
View File
@@ -21,6 +21,47 @@ router = APIRouter(tags=["ecosystem"])
_ANY = ("opc_member", "carrier", "operator")
def _normalize_activity_ext(payload: dict) -> dict:
"""运营方/成员端发布活动:校验并规范化报名体系升级字段(JSON 列)。"""
from ..training import main as training_main
if payload.get("form_fields_json") is not None:
try:
raw = json.loads(payload["form_fields_json"]) if isinstance(payload["form_fields_json"], str) else payload["form_fields_json"]
except (TypeError, ValueError):
raise HTTPException(status_code=400, detail="form_fields_json 不是合法 JSON")
fields, err = training_main.normalize_form_fields(raw)
if err:
raise HTTPException(status_code=400, detail=err)
payload["form_fields_json"] = json.dumps(fields, ensure_ascii=False)
if payload.get("activity_admins_json") is not None:
try:
raw = json.loads(payload["activity_admins_json"]) if isinstance(payload["activity_admins_json"], str) else payload["activity_admins_json"]
except (TypeError, ValueError):
raise HTTPException(status_code=400, detail="activity_admins_json 不是合法 JSON")
admins, err = training_main.normalize_activity_admins(raw)
if err:
raise HTTPException(status_code=400, detail=err)
payload["activity_admins_json"] = json.dumps(admins, ensure_ascii=False)
if payload.get("secret_json") is not None:
try:
raw = json.loads(payload["secret_json"]) if isinstance(payload["secret_json"], str) else payload["secret_json"]
except (TypeError, ValueError):
raise HTTPException(status_code=400, detail="secret_json 不是合法 JSON")
payload["secret_json"] = json.dumps(training_main.normalize_secret(raw), ensure_ascii=False)
if payload.get("remind_before_min") is not None:
remind = int(payload["remind_before_min"] or 0)
if remind < 0 or remind > 10080:
raise HTTPException(status_code=400, detail="提醒提前量须在 0~10080 分钟之间")
payload["remind_before_min"] = remind
if payload.get("signup_deadline") is not None:
dl = str(payload["signup_deadline"] or "").strip()
if dl:
from ..training.main import _parse_ms
if _parse_ms(dl) <= 0:
raise HTTPException(status_code=400, detail="报名截止时间格式不正确")
return payload
# ── 通知中心(实时通知体系)──────────────────────────────────────────────
@router.get("/notifications", summary="我的通知(分页/分类/已读筛选)")
async def list_notifications(
@@ -280,6 +321,7 @@ class BookingCreateRequest(BaseModel):
name: str = ""
contact: str = ""
want: str = ""
form_data: dict | None = None # 自定义报名表单值 {f_id: value}
@router.get("/activities", summary="活动列表(OPC 用户)")
@@ -315,7 +357,7 @@ async def create_activity_public(
db: Database = Depends(get_db),
actor: dict = Depends(require_roles(*_ANY)),
):
payload = req.model_dump(exclude_none=True)
payload = _normalize_activity_ext(req.model_dump(exclude_none=True))
payload["publisher_id"] = actor["id"]
payload["publisher_name"] = actor.get("username") or actor.get("nickname") or ""
payload["review_status"] = "approved" # 演示环境直接通过;如需审核可改为 pending
@@ -336,6 +378,9 @@ async def register_activity_public(
booking = training.register_event(event_id, actor["id"], actor.get("username") or "", req.model_dump(exclude_none=True))
if booking is None:
raise HTTPException(status_code=404, detail="活动不存在")
if booking.get("error"):
err = booking["error"]
raise HTTPException(status_code=400, detail=err)
await write_audit(db, action="booking.create", resource="booking", resource_id=booking["booking"]["id"],
detail=event_id, user=actor, request=request)
return booking
@@ -349,9 +394,8 @@ async def update_activity_owned(
db: Database = Depends(get_db),
actor: dict = Depends(require_roles(*_ANY)),
):
activity = training.update_activity_owned(
event_id, req.model_dump(exclude_unset=True, exclude_none=True), actor["id"],
)
payload = _normalize_activity_ext(req.model_dump(exclude_unset=True, exclude_none=True))
activity = training.update_activity_owned(event_id, payload, actor["id"])
if activity is None:
raise HTTPException(status_code=404, detail="活动不存在或无权修改")
await write_audit(db, action="activity.update", resource="activity", resource_id=event_id,
+13
View File
@@ -1108,6 +1108,19 @@ async def update_booking(
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:booking.manage")),
):
# 审核动作:走训练域统一审核入口(状态联动 + 审核记录 + 短信通知,幂等)
if req.audit_status in ("approved", "rejected"):
from ..training import main as training_main
payload = {"user_id": actor.get("id", ""), "username": actor.get("username", ""),
"sub": actor.get("id", "")}
bk = await training_main._apply_booking_audit(
booking_id, req.audit_status, req.audit_comment or req.note or "", payload, dict(actor))
if bk is None:
raise HTTPException(status_code=404, detail="Booking not found")
await write_audit(db, action="booking.review", resource="booking", resource_id=booking_id,
detail=f"decision={req.audit_status} comment={req.audit_comment or ''}",
user=actor, request=request)
return training.pack_booking(bk)
booking = training.update_booking(booking_id, req.model_dump(exclude_none=True))
if booking is None:
raise HTTPException(status_code=404, detail="Booking not found")
+16
View File
@@ -243,6 +243,12 @@ class ActivityCreateRequest(BaseModel):
notice: str | None = None # 活动注意事项
external_url: str | None = None
signup_mode: str | None = None # mini|external
# ── 报名体系升级 ──
form_fields_json: str | None = None # 报名表单字段定义(JSON 数组)
activity_admins_json: str | None = None # 活动管理员(JSON 数组,手机号绑定)
secret_json: str | None = None # 审核通过后可见信息(JSON 对象)
signup_deadline: str | None = None # 报名截止(ISO;空=不限制)
remind_before_min: int | None = None # 活动前提醒提前分钟数(0=关闭)
class ActivityStatusRequest(BaseModel):
@@ -265,12 +271,22 @@ class ActivityUpdateRequest(BaseModel):
duration_min: int | None = None
capacity: int | None = None
price_fen: int | None = None # 定价(分),0=免费
# ── 报名体系升级 ──
form_fields_json: str | None = None
activity_admins_json: str | None = None
secret_json: str | None = None
signup_deadline: str | None = None
remind_before_min: int | None = None
class BookingUpdateRequest(BaseModel):
status: str | None = None
audit_status: str | None = None
note: str | None = None
# ── 报名体系升级 ──
audit_comment: str | None = None # 审核备注(与小程序 /review 口径一致)
contact: str | None = None
name: str | None = None
class TestCreateRequest(BaseModel):