61c572a19a
- 任务统一合并到 /hall:新增 grab/grab-by-code/bid/doing/complete/deliver/recommend 操作端点(走 TaskService 真实逻辑) - 删除 rbac_opc 全部旧任务端点(广场/详情/grab/bid/doing/complete/assign/recommend/select/deliver),/opc/tasks 返回 404 - /opc/my-tasks 改真实聚合(task_claims 承接 + bids 投标,附任务信息),不再读 opc_tasks 演示表 - dashboard_service.opc_dashboard 改 task_claims 统计 - 删除 opc_tasks 演示表(models/seed/repositories) - BidRepository 新增 list_by_opc - 短信生态(上一轮):sms.py 阿里云签名、seed 7 项配置、send-code scene、rbac_hall 预约/订单短信
512 lines
22 KiB
Python
512 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""OPC 超级个体端端点(工作台聚合数据)。
|
||
|
||
所有数据均由服务端统一总库提供,前端不落地任何演示数据。
|
||
当前角色要求 opc_member;后续各页面端点在此扩展。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||
from pydantic import BaseModel
|
||
|
||
from ..dependencies import get_db, optional_current_user
|
||
from ..schemas.opc import ProfileUpdate, FinanceRecordCreate
|
||
from ...rbac import require_roles, write_audit
|
||
from ...infrastructure.repositories import Database, new_id, utcnow_iso
|
||
from ...infrastructure.models import FinanceRecord, ContentComment
|
||
from ... import config
|
||
from ...services import compute_client, compute_catalog
|
||
|
||
router = APIRouter(prefix="/opc", tags=["opc"])
|
||
|
||
|
||
@router.get("/dashboard", summary="OPC 工作台总览")
|
||
async def opc_dashboard(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
"""返回当前 OPC 用户的工作台聚合数据(统计/进行中任务/智能体建议/月度收入/最新消息)。"""
|
||
from ...services.dashboard_service import DashboardService
|
||
|
||
return await DashboardService(db).opc_dashboard(user["id"])
|
||
|
||
|
||
# ── OPC 子页面(全部由服务端提供标准数据)─────────────────────────────────
|
||
|
||
@router.get("/agent", summary="OPC 智能体助手")
|
||
async def opc_agent(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
agents = await db.agents.get_by_user(user["id"], port=user.get("port") or "opc")
|
||
default = next((a for a in agents if a.get("id", "").startswith("pine_agents_official_001")), None)
|
||
return {
|
||
"agent": {
|
||
"name": (default or {}).get("name", "小云"),
|
||
"desc": (default or {}).get("description", "您的智能体助手,可处理创业、政策、财税等事务"),
|
||
"port": user.get("port") or "opc",
|
||
},
|
||
"quick_actions": [
|
||
{"title": "查询政策", "desc": "为您匹配最新创业政策"},
|
||
{"title": "发布能力", "desc": "快速发布您的服务能力"},
|
||
{"title": "财务助手", "desc": "查看收入与支出"},
|
||
],
|
||
"chat_path": "/chat",
|
||
}
|
||
|
||
|
||
@router.get("/my-tasks", summary="我的任务(真实接单/投标聚合)")
|
||
async def opc_my_tasks(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
"""聚合该用户真实承接的任务(task_claims)与投标(bids),附任务信息。
|
||
|
||
2026-09 增量合并:不再返回 opc_tasks 演示看板,改为真实任务系统数据。
|
||
"""
|
||
claims = await db.task_claims.list_by_claimer(user["id"])
|
||
bids = await db.bids.list_by_opc(user["id"])
|
||
task_cache: dict[str, dict] = {}
|
||
for c in claims + bids:
|
||
tid = c.get("task_id") or c.get("taskId") or ""
|
||
if tid and tid not in task_cache:
|
||
t = await db.tasks.get(tid)
|
||
if t:
|
||
task_cache[tid] = t
|
||
items = []
|
||
for c in claims:
|
||
t = task_cache.get(c.get("task_id") or "")
|
||
if not t:
|
||
continue
|
||
items.append({
|
||
"id": t["id"], "claimId": c["id"], "title": t.get("title", ""),
|
||
"taskCode": t.get("task_code", ""), "mode": t.get("mode", ""),
|
||
"status": t.get("status", ""), "claimSource": c.get("claim_source", ""),
|
||
"claimStatus": c.get("status", ""),
|
||
"budgetMin": t.get("budget_min", 0), "budgetMax": t.get("budget_max", 0),
|
||
"deadline": t.get("deadline", ""), "createdAt": t.get("created_at", ""),
|
||
})
|
||
for b in bids:
|
||
t = task_cache.get(b.get("task_id") or "")
|
||
if not t:
|
||
continue
|
||
items.append({
|
||
"id": t["id"], "bidId": b["id"], "title": t.get("title", ""),
|
||
"taskCode": t.get("task_code", ""), "mode": t.get("mode", ""),
|
||
"status": t.get("status", ""), "claimSource": "bid",
|
||
"claimStatus": b.get("status", ""), "quote": b.get("quote", 0),
|
||
"plan": b.get("plan", ""),
|
||
"budgetMin": t.get("budget_min", 0), "budgetMax": t.get("budget_max", 0),
|
||
"deadline": t.get("deadline", ""), "createdAt": t.get("created_at", ""),
|
||
})
|
||
return {"items": items}
|
||
|
||
|
||
@router.get("/services", summary="服务市场(在营服务商)")
|
||
async def opc_services(
|
||
db: Database = Depends(get_db),
|
||
_u: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
return {"items": await db.providers.list(status="active")}
|
||
|
||
|
||
@router.get("/policy", summary="政策列表(已发布政策)")
|
||
async def opc_policy(
|
||
db: Database = Depends(get_db),
|
||
_u: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
return {"items": await db.content.list(ctype="policy", status="published", scheduled_ready=True)}
|
||
|
||
|
||
def _resolve_content_media(item: dict) -> dict:
|
||
"""C 端出口:媒体字段(对象路径)统一补全为可访问 URL(CDN 鉴权直链 / 站点绝对路径)。"""
|
||
from ...infrastructure.oss import resolve_url
|
||
for k in ("publisher_avatar", "cover", "cover_small", "video", "video_cover"):
|
||
item[k] = resolve_url(item.get(k, ""))
|
||
item["images"] = [resolve_url(u) for u in (item.get("images") or [])]
|
||
return item
|
||
|
||
|
||
@router.get("/content", summary="资讯中心(按类别,已发布)")
|
||
async def opc_content(
|
||
type: str = "news",
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""C 端资讯:type ∈ policy/news/skill/dynamic,仅 published + 公开(is_public)。公开浏览。"""
|
||
items = await db.content.list(ctype=type, status="published", public_only=True, scheduled_ready=True)
|
||
return {"items": [_resolve_content_media(it) for it in items]}
|
||
|
||
|
||
@router.get("/content/{content_id}", summary="资讯详情")
|
||
async def opc_content_detail(
|
||
content_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict | None = Depends(optional_current_user),
|
||
):
|
||
item = await db.content.get(content_id)
|
||
if item is None or item.get("status") != "published" or not item.get("is_public"):
|
||
raise HTTPException(status_code=404, detail="内容不存在")
|
||
# 阅读数自增(每次详情浏览 +1)
|
||
if item.get("status") == "published":
|
||
await db.content.incr_read_count(content_id)
|
||
item["read_count"] = (item.get("read_count") or 0) + 1
|
||
item = _resolve_content_media(item)
|
||
# 当前登录用户是否已点赞(未登录 false)
|
||
item["liked_by_me"] = await db.content.is_liked(content_id, (user or {}).get("id", ""))
|
||
return item
|
||
|
||
|
||
@router.post("/content/{content_id}/like", summary="点赞/取消点赞(登录,一人一赞)")
|
||
async def opc_content_like(
|
||
content_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
item = await db.content.get(content_id)
|
||
if item is None:
|
||
raise HTTPException(status_code=404, detail="内容不存在")
|
||
return await db.content.toggle_like(content_id, user["id"])
|
||
|
||
|
||
@router.post("/content/{content_id}/share", summary="分享计数 +1(可重复分享)")
|
||
async def opc_content_share(
|
||
content_id: str,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""分享落库:web 复制链接 / 小程序转发成功后调用,share_count +1。公开计数。"""
|
||
item = await db.content.get(content_id)
|
||
if item is None or item.get("status") != "published" or not item.get("is_public"):
|
||
raise HTTPException(status_code=404, detail="内容不存在")
|
||
return await db.content.add_share(content_id)
|
||
|
||
|
||
@router.get("/content/{content_id}/comments", summary="资讯留言列表(微信式嵌套)")
|
||
async def opc_content_comments(
|
||
content_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict | None = Depends(optional_current_user),
|
||
):
|
||
from ...infrastructure.oss import resolve_url
|
||
uid = (user or {}).get("id", "")
|
||
items = await db.content.content_comments(content_id, nested=True)
|
||
for c in items:
|
||
c["avatar"] = resolve_url(c.get("avatar", ""))
|
||
c["liked_by_me"] = await db.content.is_comment_liked(c["id"], uid) if uid else False
|
||
for rp in c.get("replies", []):
|
||
rp["avatar"] = resolve_url(rp.get("avatar", ""))
|
||
rp["liked_by_me"] = await db.content.is_comment_liked(rp["id"], uid) if uid else False
|
||
return {"items": items}
|
||
|
||
|
||
class ContentCommentBody(BaseModel):
|
||
content: str
|
||
reply_to: str = "" # 回复目标评论 id(空=顶层留言)
|
||
|
||
|
||
@router.post("/content/{content_id}/comments", summary="发表留言/回复")
|
||
async def opc_content_add_comment(
|
||
content_id: str,
|
||
body: ContentCommentBody,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
if not body.content.strip():
|
||
raise HTTPException(status_code=400, detail="留言不能为空")
|
||
item = await db.content.get(content_id)
|
||
if item is None or item.get("status") != "published":
|
||
raise HTTPException(status_code=404, detail="内容不存在")
|
||
# 取当前用户资料(昵称/头像)用于留言展示
|
||
me = await db.users.get_by_id(user["id"]) or {}
|
||
actor = {"id": user["id"], "username": user.get("username", ""),
|
||
"nickname": (me.get("nickname") or user.get("username", "")),
|
||
"avatar": me.get("avatar", "")}
|
||
comment = await db.content.add_content_comment(content_id, actor, body.content.strip(),
|
||
reply_to=body.reply_to.strip())
|
||
if comment:
|
||
from ...infrastructure.oss import resolve_url
|
||
comment["avatar"] = resolve_url(comment.get("avatar", ""))
|
||
return {"ok": True, "comment": comment}
|
||
|
||
|
||
@router.post("/comments/{comment_id}/like", summary="评论点赞/取消(登录,一人一赞)")
|
||
async def opc_comment_like(
|
||
comment_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
c = await db.session.get(ContentComment, comment_id)
|
||
if c is None:
|
||
raise HTTPException(status_code=404, detail="留言不存在")
|
||
return await db.content.toggle_comment_like(comment_id, user["id"])
|
||
|
||
|
||
@router.get("/finance", summary="财务流水")
|
||
async def opc_finance(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
uid = user["id"]
|
||
return {
|
||
"records": await db.finance.list_by_user(uid),
|
||
"total_income": await db.finance.total_income(uid),
|
||
}
|
||
|
||
|
||
@router.get("/messages", summary="站内消息")
|
||
async def opc_messages(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
return {"items": await db.messages.list_by_user(user["id"])}
|
||
|
||
|
||
@router.get("/profile", summary="个人资料")
|
||
async def opc_profile(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
profile = await db.users.to_profile(user)
|
||
profile["opc_profile"] = await db.opc_profiles.get(user["id"])
|
||
return profile
|
||
|
||
|
||
|
||
@router.put("/profile", summary="OPC 基础资料编辑")
|
||
async def opc_update_profile(
|
||
req: ProfileUpdate,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
actor: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
profile_updates = {k: v for k, v in req.model_dump(exclude_none=True).items()
|
||
if k in ("nickname", "account", "company", "room")}
|
||
if profile_updates:
|
||
await db.users.update_profile(actor["id"], profile_updates)
|
||
if req.credit_score is not None:
|
||
await db.opc_profiles.upsert(actor["id"], req.credit_score)
|
||
await write_audit(db, action="profile.update", resource="user", resource_id=actor["id"],
|
||
user=actor, request=request)
|
||
fresh_user = await db.users.get_by_id(actor["id"])
|
||
return await opc_profile(db, fresh_user)
|
||
|
||
|
||
@router.post("/finance/record", summary="OPC 记一笔收支")
|
||
async def opc_add_finance(
|
||
req: FinanceRecordCreate,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
actor: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
from ...services.finance_service import FinanceService
|
||
|
||
record = await FinanceService(db).add_record(
|
||
user_id=actor["id"], category=req.category, amount=req.amount,
|
||
date=req.date or utcnow_iso()[:10], note=req.note,
|
||
)
|
||
await write_audit(db, action="finance.add", resource="finance", resource_id=record["id"],
|
||
user=actor, request=request)
|
||
return record
|
||
|
||
|
||
@router.get("/tax", summary="OPC 税务申报")
|
||
async def opc_tax(
|
||
db: Database = Depends(get_db),
|
||
_u: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
return await db.portal_pages.get("opc", "tax") or {"items": []}
|
||
|
||
|
||
@router.get("/affairs", summary="OPC 工商/政务办事")
|
||
async def opc_affairs(
|
||
db: Database = Depends(get_db),
|
||
_u: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
return await db.portal_pages.get("opc", "affairs") or {"items": []}
|
||
|
||
|
||
# ── 算力中心(用户端,供应商自动对接 PineAgents,令牌自服务)─────────────────
|
||
@router.get("/compute/base", summary="算力基础 API 地址")
|
||
async def opc_compute_base(
|
||
request: Request,
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
"""用户接入地址:经 server-core /v1 中继(OpenAI 兼容)。优先用公网配置,回落请求 host。"""
|
||
base = (config.COMPUTE_PUBLIC_BASE or str(request.base_url)).rstrip("/")
|
||
# 反代/配置落成 http 时对外一律升级 https(本地回环除外),否则客户端 https 页面/证书校验不可用
|
||
if "://" in base:
|
||
scheme, rest = base.split("://", 1)
|
||
host = rest.split("/", 1)[0]
|
||
if scheme == "http" and not (host.startswith(("localhost", "127.0.0.1", "192.168.", "10.", "172."))):
|
||
base = f"https://{rest}"
|
||
return {
|
||
"base_url": f"{base}/v1",
|
||
"relay_path": "/v1/chat/completions",
|
||
"models_path": "/v1/models",
|
||
"username": user.get("username"),
|
||
"hint": "将 base_url 与下方令牌填入 OpenAI 兼容客户端(如 base_url/api_key)即可调用本平台模型",
|
||
}
|
||
|
||
|
||
@router.get("/compute/models", summary="可用模型(算力中心,admin 新增)")
|
||
async def opc_compute_models(_u: dict = Depends(require_roles("opc_member"))):
|
||
return {"items": await compute_catalog.models()}
|
||
|
||
|
||
@router.get("/compute/prices", summary="模型价目(admin 模型)")
|
||
async def opc_compute_prices(_u: dict = Depends(require_roles("opc_member"))):
|
||
return {"items": await compute_catalog.prices()}
|
||
|
||
|
||
@router.get("/compute/usage", summary="我的用量(本月按模型)")
|
||
async def opc_compute_usage(user: dict = Depends(require_roles("opc_member"))):
|
||
try:
|
||
data = await compute_client.user_usage(user.get("username"))
|
||
except compute_client.ComputeError as exc:
|
||
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
|
||
return data
|
||
|
||
|
||
@router.get("/compute/balance", summary="我的算力余额")
|
||
async def opc_compute_balance(user: dict = Depends(require_roles("opc_member"))):
|
||
try:
|
||
data = await compute_client.user_balance(user.get("username"))
|
||
except compute_client.ComputeError as exc:
|
||
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
|
||
return data
|
||
|
||
|
||
@router.get("/compute/tokens", summary="我的算力令牌")
|
||
async def opc_compute_tokens(user: dict = Depends(require_roles("opc_member"))):
|
||
"""列出当前用户的引擎消费令牌。key 统一补 sk- 前缀(OpenAI 客户端口径)。"""
|
||
try:
|
||
data = await compute_client.list_user_tokens(user.get("username"))
|
||
except compute_client.ComputeError as exc:
|
||
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
|
||
items = data if isinstance(data, list) else data.get("items", [])
|
||
out = []
|
||
for it in items or []:
|
||
it = dict(it)
|
||
key = str(it.get("key") or it.get("token") or "")
|
||
if key and not key.startswith("sk-"):
|
||
it["key"] = f"sk-{key}"
|
||
out.append(it)
|
||
return {"items": out}
|
||
|
||
|
||
@router.post("/compute/tokens", summary="创建算力令牌")
|
||
async def opc_compute_tokens_create(user: dict = Depends(require_roles("opc_member"))):
|
||
"""为当前用户签发一枚消费令牌(供应商自动对接 PineAgents)。"""
|
||
try:
|
||
token = await compute_client.issue_user_token(user.get("username"), name="OPC 端令牌")
|
||
except compute_client.ComputeError as exc:
|
||
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
|
||
token = dict(token or {})
|
||
key = str(token.get("key") or token.get("token") or "")
|
||
if key and not key.startswith("sk-"):
|
||
token["key"] = f"sk-{key}"
|
||
return token
|
||
|
||
|
||
@router.delete("/compute/tokens/{token_id}", summary="删除算力令牌")
|
||
async def opc_compute_tokens_delete(token_id: int, user: dict = Depends(require_roles("opc_member"))):
|
||
try:
|
||
await compute_client.delete_token(token_id)
|
||
except compute_client.ComputeError as exc:
|
||
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
|
||
return {"ok": True, "token_id": token_id}
|
||
|
||
|
||
# ── OPC 认证 ─────────────────────────────────────────────────────────────
|
||
|
||
@router.post("/certification/apply", summary="提交 OPC 认证申请")
|
||
async def opc_cert_apply(
|
||
req: OpcCertApplyRequest,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
"""当前 OPC 提交认证资料 → 状态置 pending,待运营方审核。"""
|
||
me = await db.users.get_by_id(user["id"])
|
||
if me is None:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
if me.get("certification_status") == "certified":
|
||
raise HTTPException(status_code=400, detail="当前账号已是认证OPC,无需重复申请")
|
||
if me.get("certification_status") == "pending":
|
||
raise HTTPException(status_code=400, detail="认证申请审核中,请勿重复提交")
|
||
applied = await db.opc_certifications.by_user(user["id"])
|
||
if applied and applied.get("status") in ("pending", "reviewing"):
|
||
raise HTTPException(status_code=400, detail="已有待审核的认证申请,请勿重复提交")
|
||
cert = await db.opc_certifications.create({
|
||
"real_name": req.real_name, "gender": req.gender, "address": req.address,
|
||
"industry": req.industry, "ability": req.ability, "phone": req.phone,
|
||
"docs_json": req.docs_json or "{}",
|
||
}, user["id"], me.get("username", ""))
|
||
await write_audit(
|
||
action="opc.cert_apply", resource="opc_certification", resource_id=cert["id"],
|
||
detail=f"real_name={req.real_name} industry={req.industry}",
|
||
user=user, request=request,
|
||
)
|
||
return {"ok": True, "certification": cert}
|
||
|
||
|
||
@router.get("/certification/mine", summary="我的 OPC 认证状态")
|
||
async def opc_cert_mine(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
me = await db.users.get_by_id(user["id"])
|
||
cert = await db.opc_certifications.by_user(user["id"])
|
||
return {
|
||
"certification_status": (me or {}).get("certification_status", "uncertified"),
|
||
"certification": cert,
|
||
}
|
||
|
||
|
||
# ── OPC 转园 ─────────────────────────────────────────────────────────────
|
||
|
||
@router.post("/park-transfer/apply", summary="发起 OPC 转园申请")
|
||
async def opc_park_transfer_apply(
|
||
req: ParkTransferApplyRequest,
|
||
request: Request,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
"""园区 OPC 申请转入目标园区(原园区自动取当前 park 归属,经园区/平台审核)。"""
|
||
me = await db.users.get_by_id(user["id"])
|
||
if me is None:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
if me.get("affiliation") != "park" or not me.get("park_id"):
|
||
raise HTTPException(status_code=400, detail="仅园区 OPC 可发起转园(需先归属园区)")
|
||
if not req.to_park_id:
|
||
raise HTTPException(status_code=400, detail="请选择目标园区")
|
||
if req.to_park_id == me.get("park_id"):
|
||
raise HTTPException(status_code=400, detail="目标园区与当前园区相同")
|
||
existing = await db.park_transfers.by_user(user["id"])
|
||
if existing and existing.get("status") in ("pending", "reviewing"):
|
||
raise HTTPException(status_code=400, detail="已有待审核的转园申请,请勿重复提交")
|
||
# 两级审核状态机:转出园区无管理员时第一级自动通过,直接进第二级
|
||
from ...domain import park_transfer as pt_rules
|
||
from app.park import tenants as tnt
|
||
from_tenant = await tnt.get_tenant(me.get("park_id", ""))
|
||
transfer = await db.park_transfers.create({
|
||
"to_park_id": req.to_park_id, "to_park_name": req.to_park_name,
|
||
"reason": req.reason,
|
||
"from_park_id": me.get("park_id", ""), "from_park_name": me.get("park_name", ""),
|
||
"stage": pt_rules.initial_stage(from_tenant),
|
||
"initiator_role": "user", "target_kind": "user",
|
||
}, user["id"], me.get("username", ""))
|
||
await write_audit(
|
||
action="opc.park_transfer_apply", resource="park_transfer", resource_id=transfer["id"],
|
||
detail=f"from={me.get('park_name','')} to={req.to_park_name}",
|
||
user=user, request=request,
|
||
)
|
||
return {"ok": True, "transfer": transfer}
|
||
|
||
|
||
@router.get("/park-transfer/mine", summary="我的转园申请")
|
||
async def opc_park_transfer_mine(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member")),
|
||
):
|
||
return {"transfer": await db.park_transfers.by_user(user["id"])}
|