d851b54755
1. 运行迁移脚本:同步 2 条园区管理员 + 1 条企业管理员到 organization_members 2. 新增 get_user_organizations / get_user_primary_org 辅助函数 3. 改造各路由优先从 organization_members 查询组织: - rbac_org.py: /org/me 返回所有绑定组织 - rbac_portals.py: referrals 和 carrier_park 优先用 organization_members - rbac_hall.py: 发布岗位优先用 organization_members 获取企业 4. users.org_id 保留作回退,不删除字段,风险可控
1032 lines
47 KiB
Python
1032 lines
47 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""接口层 · 大厅(任务/人才/岗位/服务)+ 社区(线上/实体载体)。
|
||
|
||
设计:design/云超服平台/大厅与社区升级设计方案.md §4。
|
||
- 公共读:游客可访问(optional_current_user),登录后附 can_accept / 已报名标记。
|
||
- 写接口:按 capabilities 叠加制角色判定(opc_member 基础能力 + enterprise/carrier/operator 叠加)。
|
||
- 审核类写接口(任务/岗位/服务上下架)走 operator 能力;admin 端管理页在 rbac_admin 扩展。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||
|
||
from sqlalchemy import select
|
||
|
||
from ..dependencies import Database, get_current_user, get_db, get_user_primary_org, optional_current_user
|
||
from ..schemas.opc import TaskClaimRequest
|
||
from ...infrastructure.models import ParkTenant
|
||
from ...infrastructure.repositories import utcnow_iso
|
||
from ...rbac import write_audit
|
||
|
||
router = APIRouter(prefix="/hall", tags=["hall"])
|
||
|
||
|
||
def require_cap(*caps: str):
|
||
"""按叠加制能力集合判定(capabilities:opc_member/enterprise/carrier/operator)。"""
|
||
from fastapi import Depends as _Depends
|
||
|
||
def dep(user: dict = _Depends(get_current_user)) -> dict:
|
||
if not (set(caps) & set(user.get("capabilities") or [])):
|
||
raise HTTPException(status_code=403, detail="无权限执行该操作")
|
||
return user
|
||
return dep
|
||
|
||
|
||
def _publish_status(user: dict) -> str:
|
||
"""operator 直接发布,其余角色进审核(pending → 运营端通过后 published)。"""
|
||
return "published" if "operator" in (user.get("capabilities") or []) else "pending"
|
||
|
||
|
||
# ===========================================================================
|
||
# 大厅 Hub
|
||
# ===========================================================================
|
||
|
||
@router.get("/overview", summary="大厅总览(四厅统计)")
|
||
async def hall_overview(db: Database = Depends(get_db)):
|
||
tasks = [t for t in await db.tasks.list_published() if t.get("status") == "published" and t.get("c_visible")]
|
||
return {
|
||
"tasks": len(tasks),
|
||
"grab": sum(1 for t in tasks if t.get("mode") == "grab"),
|
||
"bid": sum(1 for t in tasks if t.get("mode") == "bid"),
|
||
"register": sum(1 for t in tasks if t.get("mode") == "register"),
|
||
"talents": len(await db.hall_talents.list()),
|
||
"jobs": len(await db.hall_jobs.list(status="published")),
|
||
"services": len(await db.hall_services.list(status="published")),
|
||
}
|
||
|
||
|
||
# ===========================================================================
|
||
# 任务大厅
|
||
# ===========================================================================
|
||
|
||
@router.get("/tasks", summary="任务大厅列表(公开;可按模式/分类/关键字筛选)")
|
||
async def hall_tasks(
|
||
mode: str = "",
|
||
category: str = "",
|
||
q: str = "",
|
||
db: Database = Depends(get_db),
|
||
user: dict | None = Depends(optional_current_user),
|
||
):
|
||
from ...services.task_service import TaskService
|
||
|
||
items = [t for t in await db.tasks.list_published()
|
||
if t.get("status") == "published" and t.get("c_visible")]
|
||
if mode:
|
||
items = [t for t in items if t.get("mode") == mode]
|
||
if category:
|
||
items = [t for t in items if t.get("category") == category]
|
||
if q:
|
||
kw = q.lower()
|
||
items = [t for t in items
|
||
if kw in (t.get("title") or "").lower() or kw in (t.get("tags") or "").lower()
|
||
or kw in (t.get("publisher_name") or "").lower()]
|
||
items.sort(key=lambda t: -(t.get("display_priority") or 0))
|
||
if user:
|
||
svc = TaskService(db)
|
||
for it in items:
|
||
acc = await svc.can_accept(it, user)
|
||
it["can_accept"] = acc["ok"]
|
||
it["accept_reasons"] = acc["reasons"]
|
||
return {"items": items}
|
||
|
||
|
||
# 字面路径必须在参数化路径之前注册,否则 /tasks/mine 会被 /tasks/{task_id} 抢先匹配
|
||
@router.get("/tasks/mine", summary="我发布的任务(含接单/投标数)")
|
||
async def hall_my_tasks(db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
items = [t for t in await db.tasks.list(status="") if t.get("publisher_id") == user["id"]]
|
||
out = []
|
||
for t in items:
|
||
claims = await db.task_claims.list_by_task(t["id"])
|
||
bids = await db.bids.list_for_task(t["id"])
|
||
out.append({**t,
|
||
"claimCount": sum(1 for c in claims if c.get("status") in ("claimed", "doing", "assigned")),
|
||
"registerCount": sum(1 for c in claims if c.get("claim_source") == "register" and c.get("status") == "registered"),
|
||
"bidCount": sum(1 for b in bids if b.get("status") == "submitted")})
|
||
return {"items": out}
|
||
|
||
|
||
@router.get("/tasks/{task_id}", summary="任务详情(公开;登录附资格与我的接单/投标/报名状态)")
|
||
async def hall_task_detail(
|
||
task_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict | None = Depends(optional_current_user),
|
||
):
|
||
from ...services.task_service import TaskService
|
||
|
||
item = await db.tasks.get(task_id)
|
||
if item is None or item.get("status") not in ("published", "claimed", "doing") \
|
||
or not item.get("c_visible"):
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
if user:
|
||
svc = TaskService(db)
|
||
acc = await svc.can_accept(item, user)
|
||
item["can_accept"] = acc["ok"]
|
||
item["accept_reasons"] = acc["reasons"]
|
||
claims = await db.task_claims.list_by_task(task_id)
|
||
mine = [c for c in claims if c.get("claimer_user_id") == user["id"]]
|
||
item["myClaim"] = mine[0] if mine else None
|
||
item["myBid"] = next(
|
||
(b for b in await db.bids.list_for_task(task_id) if b.get("opc_id") == user["id"]), None)
|
||
if item.get("mode") == "register":
|
||
item["registerCount"] = sum(
|
||
1 for c in claims if c.get("claim_source") == "register" and c.get("status") == "registered")
|
||
return item
|
||
|
||
|
||
@router.post("/tasks", summary="发布任务(登录用户;operator 直接发布,其余进审核)")
|
||
async def hall_publish_task(
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
title = str(body.get("title", "")).strip()
|
||
if not title:
|
||
raise HTTPException(status_code=400, detail="请填写任务标题")
|
||
status = _publish_status(user)
|
||
caps = user.get("capabilities") or []
|
||
task = await db.tasks.create({
|
||
"title": title,
|
||
"category": str(body.get("category", "")).strip(),
|
||
"sub_category": str(body.get("subCategory", "") or body.get("sub_category", "")).strip(),
|
||
"description": str(body.get("description", "")),
|
||
"tags": str(body.get("tags", "")),
|
||
"mode": str(body.get("mode", "grab")).strip() or "grab",
|
||
"budget_min": int(body.get("budgetMin") or body.get("budget_min") or 0),
|
||
"budget_max": int(body.get("budgetMax") or body.get("budget_max") or 0),
|
||
"deadline": str(body.get("deadline", "")),
|
||
"delivery_days": int(body.get("deliveryDays") or body.get("delivery_days") or 0),
|
||
"headcount": int(body.get("headcount") or 0),
|
||
"register_quota": int(body.get("registerQuota") or body.get("register_quota") or 0),
|
||
"bid_quota": int(body.get("bidQuota") or body.get("bid_quota") or 0),
|
||
"win_quota": int(body.get("winQuota") or body.get("win_quota") or 0),
|
||
"eligibility": json.dumps(body.get("eligibility") or {}, ensure_ascii=False),
|
||
"status": status,
|
||
"published_at": utcnow_iso() if status == "published" else "",
|
||
"publisher_id": user.get("id"),
|
||
"publisher_name": user.get("nickname") or user.get("username", ""),
|
||
"publisher_role": "enterprise" if "enterprise" in caps else ("carrier" if "carrier" in caps else ("operator" if "operator" in caps else "opc")),
|
||
"c_visible": True,
|
||
})
|
||
await write_audit(db, action="hall.task.publish", resource="task", resource_id=task["id"],
|
||
detail=title, user=user, request=request)
|
||
# 任务发布即建任务群(fire-and-forget,IM 服务不可用不阻断)
|
||
try:
|
||
import asyncio
|
||
|
||
from ...im import client as im_client
|
||
|
||
asyncio.create_task(im_client.sync_task_group(task["id"], title))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
return {"ok": True, "id": task["id"], "status": status, "item": task}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/register", summary="报名任务(register 模式;先到先得,达上限关闭)")
|
||
async def hall_register_task(
|
||
task_id: str,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
actor: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
from ...services.task_service import TaskService
|
||
|
||
claim = await TaskService(db).register(task_id, actor)
|
||
await write_audit(db, action="hall.task.register", resource="task", resource_id=task_id,
|
||
detail=actor.get("username"), user=actor, request=request)
|
||
return {"ok": True, "claim": claim}
|
||
|
||
|
||
@router.get("/tasks/{task_id}/claims", summary="任务报名/接单列表(发包方或运营端可见)")
|
||
async def hall_task_claims(
|
||
task_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
task = await db.tasks.get(task_id)
|
||
if task is None:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
caps = user.get("capabilities") or []
|
||
is_publisher = task.get("publisher_id") == user.get("id")
|
||
if not (is_publisher or "operator" in caps or "carrier" in caps or "enterprise" in caps):
|
||
raise HTTPException(status_code=403, detail="仅发包方或运营端可查看")
|
||
claims = await db.task_claims.list_by_task(task_id)
|
||
bids = await db.bids.list_for_task(task_id)
|
||
return {"claims": claims, "bids": bids}
|
||
|
||
|
||
# ===========================================================================
|
||
# 人才大厅
|
||
# ===========================================================================
|
||
|
||
@router.get("/talents", summary="人才大厅列表(OPC + 求职者)")
|
||
async def hall_talents(
|
||
type: str = "",
|
||
region: str = "",
|
||
field: str = "",
|
||
q: str = "",
|
||
db: Database = Depends(get_db),
|
||
):
|
||
items = await db.hall_talents.list(talent_type=type, region=region, q=q, field=field)
|
||
return {"items": items}
|
||
|
||
|
||
@router.get("/talents/{user_id}", summary="人才详情")
|
||
async def hall_talent_detail(user_id: str, db: Database = Depends(get_db)):
|
||
profile = await db.hall_talents.get(user_id)
|
||
if profile is None or not profile.get("published"):
|
||
raise HTTPException(status_code=404, detail="人才档案不存在")
|
||
full = await db.users.get_by_id(user_id) or {}
|
||
out = dict(profile)
|
||
# 隐私:contact_ok=False 不出联系方式;OPC 附信用分与认证状态
|
||
out["creditScore"] = (full.get("opc_credit_score") if profile.get("contactOk") else None)
|
||
out["opcCertified"] = bool(full.get("opc_certified") or full.get("opc_cert"))
|
||
out["services"] = await db.hall_services.list(status="published", opc_id=user_id)
|
||
return out
|
||
|
||
|
||
@router.put("/talents/me", summary="维护我的人才档案(OPC 与求职者共用)")
|
||
async def hall_upsert_talent(
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
patch = {
|
||
"talentType": str(body.get("talentType", "opc")).strip() or "opc",
|
||
"displayName": str(body.get("displayName", "")).strip() or user.get("nickname") or user.get("username", ""),
|
||
"avatar": str(body.get("avatar", "") or user.get("avatar", "")),
|
||
"headline": str(body.get("headline", "")).strip(),
|
||
"bio": str(body.get("bio", "")),
|
||
"fields": [str(x) for x in (body.get("fields") or []) if str(x).strip()],
|
||
"skills": [str(x) for x in (body.get("skills") or []) if str(x).strip()],
|
||
"workYears": int(body.get("workYears") or 0),
|
||
"education": str(body.get("education", "")),
|
||
"region": str(body.get("region", "")),
|
||
"contactOk": bool(body.get("contactOk", True)),
|
||
}
|
||
profile = await db.hall_talents.upsert(user["id"], patch)
|
||
await write_audit(db, action="hall.talent.upsert", resource="talent_profile",
|
||
resource_id=user["id"], user=user, request=request)
|
||
return {"ok": True, "profile": profile}
|
||
|
||
|
||
# ===========================================================================
|
||
# 岗位大厅
|
||
# ===========================================================================
|
||
|
||
@router.get("/jobs", summary="岗位大厅列表(published;enterprise 可见自己全部)")
|
||
async def hall_jobs(
|
||
category: str = "",
|
||
work_type: str = "",
|
||
q: str = "",
|
||
db: Database = Depends(get_db),
|
||
user: dict | None = Depends(optional_current_user),
|
||
):
|
||
if user and "enterprise" in (user.get("capabilities") or []):
|
||
mine = await db.hall_jobs.list(status="", publisher_id=user["id"])
|
||
pub = await db.hall_jobs.list(status="published", category=category, work_type=work_type, q=q)
|
||
seen = {i["id"] for i in pub}
|
||
return {"items": pub + [i for i in mine if i["id"] not in seen]}
|
||
return {"items": await db.hall_jobs.list(status="published", category=category,
|
||
work_type=work_type, q=q)}
|
||
|
||
|
||
# 字面路径必须在参数化路径之前注册
|
||
@router.get("/jobs/mine", summary="我发布的岗位(含投递数)")
|
||
async def hall_my_jobs(db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
items = await db.hall_jobs.list(status="", publisher_id=user["id"])
|
||
out = []
|
||
for j in items:
|
||
apps = await db.hall_job_apps.list_for_job(j["id"])
|
||
out.append({**j, "applicationCount": len(apps),
|
||
"newApplicationCount": sum(1 for a in apps if a.get("status") == "submitted")})
|
||
return {"items": out}
|
||
|
||
|
||
@router.get("/jobs/{job_id}", summary="岗位详情")
|
||
async def hall_job_detail(job_id: str, db: Database = Depends(get_db)):
|
||
item = await db.hall_jobs.get(job_id)
|
||
if item is None or item.get("status") not in ("published", "closed"):
|
||
raise HTTPException(status_code=404, detail="岗位不存在")
|
||
return item
|
||
|
||
|
||
@router.post("/jobs", summary="发布岗位(enterprise/carrier/operator)")
|
||
async def hall_publish_job(
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("enterprise", "carrier", "operator")),
|
||
):
|
||
title = str(body.get("title", "")).strip()
|
||
if not title:
|
||
raise HTTPException(status_code=400, detail="请填写岗位名称")
|
||
status = _publish_status(user)
|
||
# 优先从 organization_members 获取企业组织,回退 user.org_id
|
||
ent_org = await get_user_primary_org(db, user, org_type="enterprise")
|
||
company_id = ent_org["org_id"] if ent_org else user.get("org_id")
|
||
job = await db.hall_jobs.create(
|
||
company_id=company_id or None,
|
||
company_name=str(body.get("companyName", "")).strip() or user.get("company", ""),
|
||
title=title,
|
||
category=str(body.get("category", "")).strip(),
|
||
description=str(body.get("description", "")),
|
||
requirement=str(body.get("requirement", "")),
|
||
tags=str(body.get("tags", "")),
|
||
salary_min=int(body.get("salaryMin") or 0),
|
||
salary_max=int(body.get("salaryMax") or 0),
|
||
location=str(body.get("location", "")),
|
||
work_type=str(body.get("workType", "fulltime")) or "fulltime",
|
||
headcount=int(body.get("headcount") or 1),
|
||
contact=str(body.get("contact", "")),
|
||
publisher_id=user.get("id"),
|
||
status=status,
|
||
published_at=utcnow_iso() if status == "published" else "",
|
||
)
|
||
await write_audit(db, action="hall.job.publish", resource="job", resource_id=job["id"],
|
||
detail=title, user=user, request=request)
|
||
return {"ok": True, "id": job["id"], "status": status}
|
||
|
||
|
||
@router.post("/jobs/{job_id}/apply", summary="投递简历(opc_member 及以上)")
|
||
async def hall_apply_job(
|
||
job_id: str,
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
job = await db.hall_jobs.get(job_id)
|
||
if job is None or job.get("status") != "published":
|
||
raise HTTPException(status_code=404, detail="岗位不存在或未发布")
|
||
existing = [a for a in await db.hall_job_apps.list_for_job(job_id) if a.get("userId") == user["id"]]
|
||
if existing:
|
||
raise HTTPException(status_code=400, detail="你已投递该岗位")
|
||
app_ = await db.hall_job_apps.create(
|
||
job_id, user["id"], user.get("nickname") or user.get("username", ""),
|
||
contact=str(body.get("contact", "")), note=str(body.get("note", "")))
|
||
await write_audit(db, action="hall.job.apply", resource="job", resource_id=job_id,
|
||
detail=user.get("username"), user=user, request=request)
|
||
return {"ok": True, "id": app_["id"]}
|
||
|
||
|
||
@router.get("/jobs/{job_id}/applications", summary="岗位投递列表(发布者/企业/运营端)")
|
||
async def hall_job_applications(
|
||
job_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("enterprise", "operator", "carrier")),
|
||
):
|
||
job = await db.hall_jobs.get(job_id)
|
||
if job is None:
|
||
raise HTTPException(status_code=404, detail="岗位不存在")
|
||
if job.get("publisherId") != user.get("id") and "operator" not in (user.get("capabilities") or []):
|
||
raise HTTPException(status_code=403, detail="仅岗位发布方或运营端可查看")
|
||
return {"items": await db.hall_job_apps.list_for_job(job_id)}
|
||
|
||
|
||
# ===========================================================================
|
||
# 服务大厅(OPC 个人服务)
|
||
# ===========================================================================
|
||
|
||
@router.get("/services", summary="服务大厅列表(published;可按 OPC 过滤)")
|
||
async def hall_services(
|
||
category: str = "",
|
||
q: str = "",
|
||
opc_id: str = "",
|
||
db: Database = Depends(get_db),
|
||
):
|
||
return {"items": await db.hall_services.list(status="published", category=category,
|
||
q=q, opc_id=opc_id)}
|
||
|
||
|
||
@router.get("/services/mine", summary="我的服务(发布者视角,含审核状态)")
|
||
async def hall_my_services(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
return {"items": await db.hall_services.list(status="", opc_id=user["id"])}
|
||
|
||
|
||
@router.get("/services/{service_id}", summary="服务详情(含作品集与 OPC 档案)")
|
||
async def hall_service_detail(service_id: str, db: Database = Depends(get_db)):
|
||
item = await db.hall_services.get(service_id)
|
||
if item is None or item.get("status") != "published":
|
||
raise HTTPException(status_code=404, detail="服务不存在")
|
||
if item.get("opcId"):
|
||
item["talent"] = await db.hall_talents.get(item["opcId"])
|
||
return item
|
||
|
||
|
||
@router.post("/services", summary="发布服务(opc_member;分类限服务类;operator 直接上架)")
|
||
async def hall_publish_service(
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
title = str(body.get("title", "")).strip()
|
||
if not title:
|
||
raise HTTPException(status_code=400, detail="请填写服务名称")
|
||
status = _publish_status(user)
|
||
svc = await db.hall_services.create(
|
||
opc_id=user["id"],
|
||
opc_name=user.get("nickname") or user.get("username", ""),
|
||
title=title,
|
||
category=str(body.get("category", "")).strip(),
|
||
description=str(body.get("description", "")),
|
||
cover=str(body.get("cover", "")),
|
||
gallery=[str(x) for x in (body.get("gallery") or []) if str(x).strip()],
|
||
price=int(body.get("price") or 0),
|
||
delivery_days=int(body.get("deliveryDays") or 0),
|
||
tags=str(body.get("tags", "")),
|
||
status=status,
|
||
)
|
||
await write_audit(db, action="hall.service.publish", resource="opc_service",
|
||
resource_id=svc["id"], detail=title, user=user, request=request)
|
||
return {"ok": True, "id": svc["id"], "status": status}
|
||
|
||
|
||
@router.put("/services/{service_id}", summary="编辑/上下架我的服务(发布者或运营端)")
|
||
async def hall_update_service(
|
||
service_id: str,
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
svc = await db.hall_services.get(service_id)
|
||
if svc is None:
|
||
raise HTTPException(status_code=404, detail="服务不存在")
|
||
caps = user.get("capabilities") or []
|
||
if svc.get("opcId") != user.get("id") and "operator" not in caps:
|
||
raise HTTPException(status_code=403, detail="仅服务发布者或运营端可编辑")
|
||
allowed = ("title", "category", "description", "cover", "price", "delivery_days", "tags", "status")
|
||
patch = {}
|
||
for k in allowed:
|
||
camel = "".join(p.capitalize() for p in k.split("_"))
|
||
if k in body:
|
||
patch[k] = body[k]
|
||
elif camel in body:
|
||
patch[k] = body[camel]
|
||
if "status" in patch and patch["status"] == "published" and "operator" not in caps:
|
||
patch["status"] = "pending" # 编辑后回审核
|
||
updated = await db.hall_services.update(service_id, patch)
|
||
await write_audit(db, action="hall.service.update", resource="opc_service",
|
||
resource_id=service_id, user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/services/{service_id}/inquiry", summary="服务咨询/下单意向")
|
||
async def hall_service_inquiry(
|
||
service_id: str,
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
svc = await db.hall_services.get(service_id)
|
||
if svc is None or svc.get("status") != "published":
|
||
raise HTTPException(status_code=404, detail="服务不存在")
|
||
inq = await db.hall_inquiries.create(
|
||
service_id, user["id"], user.get("nickname") or user.get("username", ""),
|
||
contact=str(body.get("contact", "")), note=str(body.get("note", "")))
|
||
await write_audit(db, action="hall.service.inquiry", resource="opc_service",
|
||
resource_id=service_id, user=user, request=request)
|
||
return {"ok": True, "id": inq["id"]}
|
||
|
||
|
||
# ===========================================================================
|
||
# 社区(线上帖子 + 实体载体)
|
||
# ===========================================================================
|
||
|
||
community_router = APIRouter(prefix="/community", tags=["community"])
|
||
|
||
|
||
@community_router.get("/overview", summary="社区 Hub(最新帖子 + 载体推荐)")
|
||
async def community_overview(db: Database = Depends(get_db)):
|
||
posts = (await db.community_posts.list(status="published"))[:5]
|
||
tenants = (await db.session.execute(
|
||
select(ParkTenant).where(ParkTenant.status == "active").limit(5))).scalars().all()
|
||
carriers = [{"id": t.id, "name": t.name, "city": t.city, "cover": t.cover, "introHtml": t.intro_html[:120]}
|
||
for t in tenants]
|
||
return {"posts": posts, "carriers": carriers,
|
||
"topics": ["交流", "求助", "资源", "经验", "公告"]}
|
||
|
||
|
||
@community_router.get("/posts", summary="帖子流(published;按话题/关键字筛选)")
|
||
async def community_posts(topic: str = "", q: str = "", db: Database = Depends(get_db)):
|
||
return {"items": await db.community_posts.list(status="published", topic=topic, q=q)}
|
||
|
||
|
||
@community_router.get("/posts/mine", summary="我的帖子(含审核状态)")
|
||
async def community_my_posts(db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user)):
|
||
return {"items": await db.community_posts.list(status="", author_id=user["id"])}
|
||
|
||
|
||
@community_router.get("/posts/{post_id}", summary="帖子详情(正文 + 评论树)")
|
||
async def community_post_detail(post_id: str, db: Database = Depends(get_db)):
|
||
post = await db.community_posts.get(post_id)
|
||
if post is None or post.get("status") != "published":
|
||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||
comments = await db.community_comments.list_for_post(post_id)
|
||
return {"post": post, "comments": comments}
|
||
|
||
|
||
@community_router.post("/posts", summary="发帖(登录用户;admin 可后台治理)")
|
||
async def community_create_post(
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
title = str(body.get("title", "")).strip()
|
||
content = str(body.get("content", "")).strip()
|
||
if not title or not content:
|
||
raise HTTPException(status_code=400, detail="请填写标题与正文")
|
||
post = await db.community_posts.create(
|
||
author_id=user["id"],
|
||
author_name=user.get("nickname") or user.get("username", ""),
|
||
avatar=user.get("avatar", ""),
|
||
topic=str(body.get("topic", "交流")).strip() or "交流",
|
||
title=title, content=content,
|
||
images=[str(x) for x in (body.get("images") or []) if str(x).strip()],
|
||
status="published",
|
||
)
|
||
await write_audit(db, action="community.post.create", resource="community_post",
|
||
resource_id=post["id"], user=user, request=request)
|
||
return {"ok": True, "id": post["id"], "item": post}
|
||
|
||
|
||
@community_router.post("/posts/{post_id}/like", summary="点赞/取消点赞帖子")
|
||
async def community_like_post(
|
||
post_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
post = await db.community_posts.get(post_id)
|
||
if post is None:
|
||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||
if await db.community_likes.has("post", post_id, user["id"]):
|
||
await db.community_likes.remove("post", post_id, user["id"])
|
||
await db.community_posts.inc_likes(post_id, -1)
|
||
return {"ok": True, "liked": False}
|
||
await db.community_likes.add("post", post_id, user["id"])
|
||
await db.community_posts.inc_likes(post_id, 1)
|
||
return {"ok": True, "liked": True}
|
||
|
||
|
||
@community_router.post("/posts/{post_id}/comments", summary="评论帖子(支持楼中楼)")
|
||
async def community_comment_post(
|
||
post_id: str,
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
post = await db.community_posts.get(post_id)
|
||
if post is None or post.get("status") != "published":
|
||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||
content = str(body.get("content", "")).strip()
|
||
if not content:
|
||
raise HTTPException(status_code=400, detail="请填写评论内容")
|
||
cmt = await db.community_comments.create(
|
||
post_id, user["id"], user.get("nickname") or user.get("username", ""),
|
||
content, parent_id=str(body.get("parentId", "")))
|
||
await db.community_posts.inc_comments(post_id, 1)
|
||
await write_audit(db, action="community.comment", resource="community_post",
|
||
resource_id=post_id, user=user, request=request)
|
||
return {"ok": True, "comment": cmt}
|
||
|
||
|
||
@community_router.get("/carriers", summary="实体社区载体列表(云南省内创业载体,公开)")
|
||
async def community_carriers(region: str = "", q: str = "", db: Database = Depends(get_db)):
|
||
from sqlalchemy import select
|
||
from ...infrastructure.models import ParkCompany, ParkTenant
|
||
|
||
rows = (await db.session.execute(
|
||
select(ParkTenant).where(ParkTenant.status == "active"))).scalars().all()
|
||
items = []
|
||
for t in rows:
|
||
if region and (t.city or "") != region:
|
||
continue
|
||
if q and q.lower() not in (t.name or "").lower():
|
||
continue
|
||
companies = (await db.session.execute(
|
||
select(ParkCompany).where(ParkCompany.tenant_id == t.id))).scalars().all()
|
||
items.append({
|
||
"id": t.id, "name": t.name, "city": t.city, "cover": t.cover,
|
||
"address": t.address, "contact": t.contact, "tags": t.tags,
|
||
"areaM2": t.area_m2, "stats": t.stats_json,
|
||
"companyCount": len(companies),
|
||
})
|
||
return {"items": items}
|
||
|
||
|
||
@community_router.get("/carriers/{carrier_id}", summary="载体详情(实体社区)")
|
||
async def community_carrier_detail(carrier_id: str, db: Database = Depends(get_db)):
|
||
from sqlalchemy import select
|
||
from ...infrastructure.models import ParkCompany, ParkTenant
|
||
|
||
t = await db.session.get(ParkTenant, carrier_id)
|
||
if t is None or t.status != "active":
|
||
raise HTTPException(status_code=404, detail="载体不存在")
|
||
companies = (await db.session.execute(
|
||
select(ParkCompany).where(ParkCompany.tenant_id == t.id)
|
||
.order_by(ParkCompany.created_at.desc()))).scalars().all()
|
||
return {
|
||
"id": t.id, "name": t.name, "city": t.city, "cover": t.cover,
|
||
"lng": t.lng, "lat": t.lat, "address": t.address, "contact": t.contact,
|
||
"tags": t.tags, "areaM2": t.area_m2,
|
||
"introHtml": t.intro_html, "policyHtml": t.policy_html, "admissionHtml": t.admission_html,
|
||
"services": json.loads(t.services_json or "[]"),
|
||
"stats": t.stats_json,
|
||
"companies": [{"id": c.id, "name": c.name, "industry": c.industry, "bio": c.bio}
|
||
for c in companies[:60]],
|
||
"companyCount": len(companies),
|
||
"followerCount": await db.community_follows.count("carrier", t.id),
|
||
}
|
||
|
||
|
||
@community_router.post("/carriers/{carrier_id}/follow", summary="关注/取消关注载体")
|
||
async def community_follow_carrier(
|
||
carrier_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
t = await db.session.get(ParkTenant, carrier_id)
|
||
if t is None:
|
||
raise HTTPException(status_code=404, detail="载体不存在")
|
||
res = await db.community_follows.toggle(user["id"], "carrier", carrier_id)
|
||
return {"ok": True, **res}
|
||
|
||
|
||
@community_router.post("/carriers/{carrier_id}/join", summary="申请入驻载体(复用入驻审核流)")
|
||
async def community_join_carrier(
|
||
carrier_id: str,
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
from ...infrastructure.models import ParkTenant
|
||
|
||
t = await db.session.get(ParkTenant, carrier_id)
|
||
if t is None or t.status != "active":
|
||
raise HTTPException(status_code=404, detail="载体不存在")
|
||
adm = await db.park_admissions.create(
|
||
tenant_id=t.id, tenant_name=t.name, user_id=user["id"],
|
||
username=user.get("username", ""),
|
||
applicant_name=str(body.get("name", "")).strip() or user.get("nickname", ""),
|
||
contact_phone=str(body.get("phone", "")).strip(),
|
||
form_json=json.dumps(body.get("form") or {}, ensure_ascii=False),
|
||
source="community")
|
||
await write_audit(db, action="community.carrier.join", resource="park_admission",
|
||
resource_id=adm["id"], user=user, request=request)
|
||
return {"ok": True, "id": adm["id"], "status": adm["status"]}
|
||
|
||
|
||
# ===========================================================================
|
||
# 服务订单(预约/下单 → 接单 → 交付 → 完成,0036)
|
||
# ===========================================================================
|
||
|
||
|
||
async def _send_user_sms(db, user_id: str, scene: str, params: dict[str, str]) -> None:
|
||
"""向指定用户手机号发送场景短信;无手机号/发送失败均不影响主业务。"""
|
||
if not user_id:
|
||
return
|
||
try:
|
||
u = await db.users.get_by_id(user_id)
|
||
except Exception:
|
||
u = None
|
||
phone = str((u or {}).get("phone") or "").strip()
|
||
if not phone:
|
||
return
|
||
try:
|
||
from ...services import sms
|
||
cfg = await sms.load_sms_config(db)
|
||
await sms.send_template(cfg, scene, phone, params)
|
||
except Exception as exc: # 短信失败不阻断业务
|
||
print(f"[sms] {scene} -> {phone} 发送失败(已忽略): {exc}")
|
||
|
||
|
||
@router.post("/services/{service_id}/order", summary="预约/下单服务(buyer)")
|
||
async def hall_order_service(
|
||
service_id: str,
|
||
body: dict,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
svc = await db.hall_services.get(service_id)
|
||
if svc is None or svc.get("status") != "published":
|
||
raise HTTPException(status_code=404, detail="服务不存在")
|
||
if svc.get("opcId") == user.get("id"):
|
||
raise HTTPException(status_code=400, detail="不能下单自己的服务")
|
||
contact = str(body.get("contact", "")).strip()
|
||
if not contact:
|
||
raise HTTPException(status_code=400, detail="请填写联系方式")
|
||
order = await db.service_orders.create(
|
||
service_id=service_id, opc_id=svc.get("opcId"), buyer_id=user["id"],
|
||
buyer_name=user.get("nickname") or user.get("username", ""),
|
||
contact=contact, schedule_at=str(body.get("scheduleAt", "")).strip(),
|
||
note=str(body.get("note", "")), price=int(body.get("price") or 0))
|
||
await write_audit(db, action="hall.service.order", resource="service_order",
|
||
resource_id=order["id"], user=user, request=request)
|
||
# 服务预约短信 → 服务方(appointment 场景,模板变量见 system_configs)
|
||
await _send_user_sms(db, svc.get("opcId"), "appointment", {
|
||
"service_name": svc.get("title") or "",
|
||
"schedule_at": str(body.get("scheduleAt", "") or ""),
|
||
"contact": contact,
|
||
})
|
||
return {"ok": True, "id": order["id"], "status": order["status"], "item": order}
|
||
|
||
|
||
@router.get("/orders/bought", summary="我下单的服务订单(buyer)")
|
||
async def hall_orders_bought(db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
return {"items": await db.service_orders.list(buyer_id=user["id"])}
|
||
|
||
|
||
@router.get("/orders/selling", summary="我收到的服务订单(服务方)")
|
||
async def hall_orders_selling(db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
return {"items": await db.service_orders.list(opc_id=user["id"])}
|
||
|
||
|
||
@router.post("/orders/{order_id}/{action}", summary="订单流转(accept/reject/start/deliver/complete/cancel)")
|
||
async def hall_order_transition(
|
||
order_id: str,
|
||
action: str,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
order = await db.service_orders.get(order_id)
|
||
if order is None:
|
||
raise HTTPException(status_code=404, detail="订单不存在")
|
||
is_seller = order.get("opcId") == user.get("id")
|
||
is_buyer = order.get("buyerId") == user.get("id")
|
||
is_operator = "operator" in (user.get("capabilities") or [])
|
||
if not (is_seller or is_buyer or is_operator):
|
||
raise HTTPException(status_code=403, detail="无权操作该订单")
|
||
# 权限约束:accept/reject/start/deliver 仅服务方(或运营);cancel 买卖双方皆可;complete 仅买方
|
||
seller_only = {"accept", "reject", "start", "deliver"}
|
||
buyer_only = {"complete"}
|
||
if action in seller_only and not (is_seller or is_operator):
|
||
raise HTTPException(status_code=403, detail="仅服务方可操作")
|
||
if action in buyer_only and not (is_buyer or is_operator):
|
||
raise HTTPException(status_code=403, detail="仅买方可确认完成")
|
||
if action == "cancel" and not (is_seller or is_buyer or is_operator):
|
||
raise HTTPException(status_code=403, detail="无权取消")
|
||
try:
|
||
updated = await db.service_orders.transition(order_id, action)
|
||
except KeyError:
|
||
raise HTTPException(status_code=404, detail="订单不存在")
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=400, detail=str(exc))
|
||
await write_audit(db, action=f"hall.order.{action}", resource="service_order",
|
||
resource_id=order_id, user=user, request=request)
|
||
# 订单状态变动短信 → 交易对手方(order_status 场景)
|
||
status_map = {
|
||
"accept": "已接单", "reject": "已拒绝", "start": "进行中",
|
||
"deliver": "已交付", "complete": "已完成", "cancel": "已取消",
|
||
}
|
||
target = order.get("buyerId") if is_seller else order.get("opcId")
|
||
if action in ("complete", "cancel"):
|
||
target = order.get("opcId") if is_buyer else order.get("buyerId")
|
||
await _send_user_sms(db, target, "order_status", {
|
||
"order_no": order.get("id") or "",
|
||
"status": status_map.get(action, action),
|
||
})
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
# ===========================================================================
|
||
# 发包方任务操作(定标 / 指派 / 报名选定 / 取消,0036)
|
||
# ===========================================================================
|
||
|
||
def _ensure_task_owner(task: dict, user: dict) -> None:
|
||
caps = user.get("capabilities") or []
|
||
if task.get("publisher_id") != user.get("id") and "operator" not in caps:
|
||
raise HTTPException(status_code=403, detail="仅发包方或运营端可操作")
|
||
|
||
|
||
@router.post("/tasks/{task_id}/win", summary="评标定标(发包方/运营端;bid → win,任务 → in_progress)")
|
||
async def hall_task_win(task_id: str, body: dict, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
task = await db.tasks.get(task_id)
|
||
if task is None:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
_ensure_task_owner(task, user)
|
||
updated = await TaskService(db).win_bid(task_id, str(body.get("bidId", "")))
|
||
await write_audit(db, action="hall.task.win", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/assign", summary="指派接单人(发包方/运营端;assign 模式)")
|
||
async def hall_task_assign(task_id: str, body: dict, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
task = await db.tasks.get(task_id)
|
||
if task is None:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
_ensure_task_owner(task, user)
|
||
updated = await TaskService(db).assign(task_id, str(body.get("userId", "")), user)
|
||
await write_audit(db, action="hall.task.assign", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/select", summary="报名选定(发包方/运营端;register 模式)")
|
||
async def hall_task_select(task_id: str, body: dict, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
task = await db.tasks.get(task_id)
|
||
if task is None:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
_ensure_task_owner(task, user)
|
||
updated = await TaskService(db).register_select(task_id, str(body.get("claimId", "")), user)
|
||
await write_audit(db, action="hall.task.select", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/cancel", summary="取消任务(发包方/运营端;未接单可取消)")
|
||
async def hall_task_cancel(task_id: str, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
task = await db.tasks.get(task_id)
|
||
if task is None:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
_ensure_task_owner(task, user)
|
||
if task.get("status") != "published":
|
||
raise HTTPException(status_code=400, detail="任务已被接单,不可取消")
|
||
updated = await db.tasks.set_status(task_id, "cancelled")
|
||
await write_audit(db, action="hall.task.cancel", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
# ── 任务统一合并到大厅:OPC 接单侧操作(原 /opc/tasks/*,2026-09 增量合并) ──
|
||
|
||
@router.post("/tasks/{task_id}/grab", summary="抢单(grab 模式,先到先得)")
|
||
async def hall_task_grab(task_id: str, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
|
||
updated = await TaskService(db).grab(task_id, user)
|
||
await write_audit(db, action="hall.task.grab", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/grab-by-code", summary="扫码/按短码接单")
|
||
async def hall_task_grab_by_code(
|
||
req: TaskClaimRequest,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member")),
|
||
):
|
||
from ...services.task_service import TaskService
|
||
|
||
updated = await TaskService(db).claim(
|
||
(req.task_code or "").strip(), user, source="scan",
|
||
)
|
||
await write_audit(db, action="hall.task.claim", resource="task",
|
||
resource_id=updated["id"], user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/bid", summary="投标(bid 模式,报价+方案)")
|
||
async def hall_task_bid(task_id: str, body: dict, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
|
||
bid = await TaskService(db).bid(task_id, user,
|
||
int(body.get("quote") or 0),
|
||
str(body.get("plan") or ""))
|
||
await write_audit(db, action="hall.task.bid", resource="bid", resource_id=bid["id"],
|
||
user=user, request=request)
|
||
return {"ok": True, "item": bid}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/doing", summary="开始做单")
|
||
async def hall_task_doing(task_id: str, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
|
||
updated = await TaskService(db).start_doing(task_id, user)
|
||
await write_audit(db, action="hall.task.doing", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/complete", summary="完成做单")
|
||
async def hall_task_complete(task_id: str, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
|
||
updated = await TaskService(db).complete(task_id, user)
|
||
await write_audit(db, action="hall.task.complete", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/deliver", summary="交付成果")
|
||
async def hall_task_deliver(task_id: str, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
|
||
updated = await TaskService(db).deliver(task_id, user)
|
||
await write_audit(db, action="hall.task.deliver", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "item": updated}
|
||
|
||
|
||
@router.post("/tasks/{task_id}/recommend", summary="推荐候选人(发包方/运营端)")
|
||
async def hall_task_recommend(task_id: str, body: dict, request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
from ...services.task_service import TaskService
|
||
|
||
task = await db.tasks.get(task_id)
|
||
if task is None:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
_ensure_task_owner(task, user)
|
||
records = await TaskService(db).recommend(task_id, body.get("candidates") or [], user)
|
||
await write_audit(db, action="hall.task.recommend", resource="task", resource_id=task_id,
|
||
user=user, request=request)
|
||
return {"ok": True, "items": records}
|
||
|
||
|
||
# ===========================================================================
|
||
# 站内会话(点对点私信,0041)
|
||
# ===========================================================================
|
||
|
||
dm_router = APIRouter(prefix="/dm", tags=["dm"])
|
||
|
||
|
||
@dm_router.get("/conversations", summary="我的会话列表(按对方聚合,含未读数)")
|
||
async def dm_conversations(db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
convs = await db.dm.conversations(user["id"])
|
||
# 补对方展示信息
|
||
out = []
|
||
for c in convs:
|
||
peer = await db.users.get_by_id(c["peerId"]) or {}
|
||
talent = await db.hall_talents.get(c["peerId"])
|
||
out.append({**c, "peerName": peer.get("nickname") or peer.get("username", ""),
|
||
"peerAvatar": peer.get("avatar", ""),
|
||
"peerHeadline": (talent or {}).get("headline", "")})
|
||
return {"items": out}
|
||
|
||
|
||
@dm_router.get("/messages", summary="与某人的消息记录(进入会话即标记已读)")
|
||
async def dm_messages(peer: str, db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
items = await db.dm.thread(user["id"], peer)
|
||
await db.dm.mark_read(user["id"], peer)
|
||
peer = await db.users.get_by_id(peer) or {}
|
||
return {"items": items, "peerName": peer.get("nickname") or peer.get("username", ""),
|
||
"peerAvatar": peer.get("avatar", "")}
|
||
|
||
|
||
@dm_router.post("/messages", summary="发送私信")
|
||
async def dm_send(body: dict, request: Request, db: Database = Depends(get_db),
|
||
user: dict = Depends(require_cap("opc_member"))):
|
||
peer = str(body.get("peerId", "")).strip()
|
||
content = str(body.get("content", "")).strip()
|
||
if not peer or not content:
|
||
raise HTTPException(status_code=400, detail="请填写接收人与内容")
|
||
if peer == user.get("id"):
|
||
raise HTTPException(status_code=400, detail="不能给自己发消息")
|
||
target = await db.users.get_by_id(peer)
|
||
if target is None:
|
||
raise HTTPException(status_code=404, detail="接收人不存在")
|
||
msg = await db.dm.send(user["id"], peer, content)
|
||
await write_audit(db, action="dm.send", resource="dm_message", resource_id=msg["id"],
|
||
detail=f"to={peer}", user=user, request=request)
|
||
return {"ok": True, "message": msg}
|
||
|
||
|
||
# ===========================================================================
|
||
# 我发布的(发布者视角,0041)
|
||
# ===========================================================================
|
||
# 注:/tasks/mine 和 /jobs/mine 已移至参数化路由之前注册,避免被 {task_id}/{job_id} 抢先匹配
|