feat: complete huihui square avatar workflows
This commit is contained in:
0
digital-avatar-app/backend/routers/__init__.py
Normal file
0
digital-avatar-app/backend/routers/__init__.py
Normal file
32
digital-avatar-app/backend/routers/authorizations.py
Normal file
32
digital-avatar-app/backend/routers/authorizations.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from models import Authorization
|
||||
from responses import ok, fail
|
||||
|
||||
router = APIRouter(tags=["授权"])
|
||||
|
||||
|
||||
@router.get("/avatar/{avatar_id}/authorizations")
|
||||
def list_auth(avatar_id: str, db: Session = Depends(get_db)):
|
||||
# demo:返回全部授权(忽略具体 avatar 绑定,便于联调)
|
||||
items = db.query(Authorization).order_by(Authorization.created_at.desc()).all()
|
||||
return ok([a.to_dict() for a in items])
|
||||
|
||||
|
||||
@router.put("/avatar/{avatar_id}/authorizations")
|
||||
def update_auth(avatar_id: str, payload: dict = Body(...), db: Session = Depends(get_db)):
|
||||
auth_id = payload.get("id")
|
||||
if not auth_id:
|
||||
return fail("缺少授权 id", 400)
|
||||
a = db.query(Authorization).filter(Authorization.id == auth_id).first()
|
||||
if not a:
|
||||
return fail("授权不存在", 404)
|
||||
if "status" in payload:
|
||||
a.status = payload["status"]
|
||||
if "permissions" in payload:
|
||||
a.permissions = payload["permissions"]
|
||||
db.commit()
|
||||
items = db.query(Authorization).order_by(Authorization.created_at.desc()).all()
|
||||
return ok([x.to_dict() for x in items])
|
||||
95
digital-avatar-app/backend/routers/avatars.py
Normal file
95
digital-avatar-app/backend/routers/avatars.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, Depends, Body, Header
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from models import Avatar, KnowledgeDoc, KnowledgeChunk, QAPair, Authorization, User
|
||||
from responses import ok, fail
|
||||
|
||||
router = APIRouter(tags=["分身"])
|
||||
|
||||
|
||||
def _resolve_user(authorization: str | None, db: Session):
|
||||
"""从 Authorization: Bearer <app_token> 解析当前登录用户"""
|
||||
if not authorization:
|
||||
return None
|
||||
token = authorization.replace("Bearer ", "", 1).replace("bearer ", "", 1).strip()
|
||||
return db.query(User).filter(User.app_token == token).first()
|
||||
|
||||
|
||||
@router.get("/avatar")
|
||||
def list_avatars(page: int = 1, limit: int = 20, authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
# 仅返回当前登录用户自己的分身;未登录返回空,避免看到种子/他人数据
|
||||
user = _resolve_user(authorization, db)
|
||||
if not user:
|
||||
return ok({"data": [], "total": 0})
|
||||
q = db.query(Avatar).filter(Avatar.owner_id == user.huihui_user_id)
|
||||
total = q.count()
|
||||
items = (
|
||||
q.order_by(Avatar.created_at.desc())
|
||||
.offset((page - 1) * limit)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return ok({"data": [a.to_dict() for a in items], "total": total})
|
||||
|
||||
|
||||
@router.get("/avatar/{avatar_id}")
|
||||
def get_avatar(avatar_id: str, db: Session = Depends(get_db)):
|
||||
a = db.query(Avatar).filter(Avatar.id == avatar_id).first()
|
||||
if not a:
|
||||
return fail("分身不存在", 404)
|
||||
return ok(a.to_dict())
|
||||
|
||||
|
||||
@router.post("/avatar")
|
||||
def create_avatar(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
user = _resolve_user(authorization, db)
|
||||
a = Avatar(
|
||||
owner_id=user.huihui_user_id if user else "",
|
||||
name=payload.get("name", "未命名分身"),
|
||||
display_name=payload.get("displayName", "") or payload.get("display_name", ""),
|
||||
description=payload.get("description", ""),
|
||||
photo_url=payload.get("photoUrl", "") or payload.get("photo_url", ""),
|
||||
emoji=payload.get("emoji", "🤖"),
|
||||
status=payload.get("status", "active"),
|
||||
token_balance=payload.get("tokenBalance", 0),
|
||||
config=payload.get("config", {}) or {},
|
||||
)
|
||||
db.add(a)
|
||||
db.commit()
|
||||
db.refresh(a)
|
||||
return ok(a.to_dict())
|
||||
|
||||
|
||||
@router.put("/avatar/{avatar_id}")
|
||||
def update_avatar(avatar_id: str, payload: dict = Body(...), db: Session = Depends(get_db)):
|
||||
a = db.query(Avatar).filter(Avatar.id == avatar_id).first()
|
||||
if not a:
|
||||
return fail("分身不存在", 404)
|
||||
mapping = {
|
||||
"displayName": "display_name",
|
||||
"photoUrl": "photo_url",
|
||||
"tokenBalance": "token_balance",
|
||||
}
|
||||
for key in ("name", "displayName", "description", "photoUrl", "emoji", "status", "tokenBalance", "config"):
|
||||
if key in payload:
|
||||
col = mapping.get(key, key)
|
||||
setattr(a, col, payload[key])
|
||||
db.commit()
|
||||
db.refresh(a)
|
||||
return ok(a.to_dict())
|
||||
|
||||
|
||||
@router.delete("/avatar/{avatar_id}")
|
||||
def delete_avatar(avatar_id: str, db: Session = Depends(get_db)):
|
||||
a = db.query(Avatar).filter(Avatar.id == avatar_id).first()
|
||||
if not a:
|
||||
return fail("分身不存在", 404)
|
||||
# 级联清理关联数据,避免孤儿记录
|
||||
db.query(KnowledgeDoc).filter(KnowledgeDoc.avatar_id == avatar_id).delete()
|
||||
db.query(KnowledgeChunk).filter(KnowledgeChunk.avatar_id == avatar_id).delete()
|
||||
db.query(QAPair).filter(QAPair.avatar_id == avatar_id).delete()
|
||||
db.query(Authorization).filter(Authorization.avatar_id == avatar_id).delete()
|
||||
db.delete(a)
|
||||
db.commit()
|
||||
return ok({"success": True})
|
||||
336
digital-avatar-app/backend/routers/huihui_auth.py
Normal file
336
digital-avatar-app/backend/routers/huihui_auth.py
Normal file
@@ -0,0 +1,336 @@
|
||||
"""
|
||||
会会短信验证码登录代理(真实开放平台对接)
|
||||
──────────────────────────────────────────────
|
||||
严格按会会开放平台 sign.js 签名范式:
|
||||
- 公共字段 appId/accessId/timestamp(12小时制hh)/signType/signVersion/accessSecret/nonce 合并业务参数
|
||||
- 过滤空值 → 字典序排序 → k=v& 拼接 → 末尾追加 accessSecret=secretKey → MD5/SHA256 大写
|
||||
- 认证服务基址 / appId / accessId / accessSecret / clientCode 走环境变量
|
||||
真实端点(来自 fat-open 网关 usercenter 的 Swagger):
|
||||
- 发送验证码:POST {BASE}{HUIHUI_SMS_SEND_PATH} 默认 /open/mobile/sms/code (query 参数)
|
||||
- 短信登录: POST {BASE}{HUIHUI_SMS_LOGIN_PATH} 默认 /open/login/token,loginType=code
|
||||
登录成功后建/链本地 users 表(按会会 userId 唯一),签发本系统 app_token 作为会话。
|
||||
无真实凭证时仍可走 DEV_MOCK 兜底联调。
|
||||
"""
|
||||
import os
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import hashlib
|
||||
import httpx
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Header
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# 会会网关按北京时间(Asia/Shanghai, UTC+8)校验时间戳,容器默认 UTC 会导致签名被拒。
|
||||
# 用固定 +8 偏移(不依赖 tzdata,slim 镜像缺 IANA 库时会抛 ZoneInfoNotFoundError)。
|
||||
_CN_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
from database import get_db
|
||||
from models import User
|
||||
from responses import ok, fail
|
||||
|
||||
router = APIRouter(tags=["会会账号"])
|
||||
|
||||
# ── 会会开放平台配置(环境变量)──
|
||||
AUTH_BASE_URL = os.getenv("HUIHUI_AUTH_BASE_URL", "https://fat-open.99hui.com/api/usercenter")
|
||||
APP_ID = os.getenv("HUIHUI_APP_ID", "")
|
||||
ACCESS_ID = os.getenv("HUIHUI_ACCESS_ID", "")
|
||||
ACCESS_SECRET = os.getenv("HUIHUI_ACCESS_SECRET", "")
|
||||
CLIENT_CODE = os.getenv("HUIHUI_CLIENT_CODE", "")
|
||||
SMS_SEND_PATH = os.getenv("HUIHUI_SMS_SEND_PATH", "/open/mobile/sms/code")
|
||||
SMS_LOGIN_PATH = os.getenv("HUIHUI_SMS_LOGIN_PATH", "/open/login/token")
|
||||
|
||||
# 临时开发态:无真实会会凭证时,本地模拟短信收发,便于端到端联调。
|
||||
DEV_MOCK = os.getenv("HUIHUI_DEV_MOCK", "false").lower() in ("1", "true", "yes")
|
||||
_mock_codes: dict[str, tuple[str, float]] = {}
|
||||
_MOCK_TTL = 300
|
||||
|
||||
|
||||
# ── 签名体系(完全对应 sign.js)──
|
||||
def _get_nonce() -> str:
|
||||
# 与会会 sign.js 一致:base36 随机串
|
||||
return "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
||||
|
||||
|
||||
def _get_timestamp() -> str:
|
||||
# 与会会 sign.js 一致:yyyyMMddHHmmss(24 小时制大写 HH)。
|
||||
# 实测:会会 common.format 走 24 小时制,用 12 小时制(%I)会被网关判"签名验证失败"。
|
||||
# 必须用北京时间,否则容器(UTC)生成的时间戳与会会网关校验窗口偏差 8h 被拒。
|
||||
return datetime.now(_CN_TZ).strftime("%Y%m%d%H%M%S")
|
||||
|
||||
|
||||
def _make_sign(params: dict, secret_key: str, sign_type: str = "MD5") -> str:
|
||||
SIGN_KEY = "signature"
|
||||
SECRET_KEY = "accessSecret"
|
||||
keys = sorted(params.keys())
|
||||
parts = []
|
||||
for k in keys:
|
||||
if k in (SIGN_KEY, SECRET_KEY):
|
||||
continue
|
||||
v = params.get(k)
|
||||
if v is None or v == "" or v == []:
|
||||
continue
|
||||
if isinstance(v, list):
|
||||
continue
|
||||
parts.append(f"{k}={v}")
|
||||
sign_str = "&".join(parts) + f"&{SECRET_KEY}={secret_key}"
|
||||
if sign_type.upper() == "SHA256":
|
||||
return hashlib.sha256(sign_str.encode("utf-8")).hexdigest().upper()
|
||||
return hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper()
|
||||
|
||||
|
||||
def _build_form(extra: dict) -> dict:
|
||||
sign_type = "MD5"
|
||||
sign_version = "1.0"
|
||||
secret = ACCESS_SECRET
|
||||
base = {
|
||||
"appId": APP_ID,
|
||||
"accessId": ACCESS_ID,
|
||||
"timestamp": _get_timestamp(),
|
||||
"signType": sign_type,
|
||||
"signVersion": sign_version,
|
||||
"accessSecret": secret,
|
||||
"nonce": _get_nonce(),
|
||||
}
|
||||
base.update(extra)
|
||||
signature = _make_sign(base, secret, sign_type) if secret else ""
|
||||
base["signature"] = signature
|
||||
base.pop("accessSecret", None) # 不发送密钥
|
||||
return base
|
||||
|
||||
|
||||
def _pick(d: dict, *keys, default=""):
|
||||
for k in keys:
|
||||
if d.get(k) not in (None, ""):
|
||||
return d[k]
|
||||
return default
|
||||
|
||||
|
||||
def _cfg_ready() -> bool:
|
||||
return bool(AUTH_BASE_URL and APP_ID and ACCESS_ID and ACCESS_SECRET)
|
||||
|
||||
|
||||
def _call_huihui(path: str, params: dict, as_query: bool = False):
|
||||
"""调用会会接口,返回 (ok: bool, payload: dict, http_status: int)"""
|
||||
url = f"{AUTH_BASE_URL}{path}"
|
||||
with httpx.Client(timeout=30, follow_redirects=True) as c:
|
||||
if as_query:
|
||||
resp = c.post(url, params=params)
|
||||
else:
|
||||
resp = c.post(url, data=params)
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
return False, {"message": f"会会返回非JSON: {resp.text[:200]}"}, resp.status_code
|
||||
# 会会统一包装 {code, message, data}
|
||||
code = data.get("code")
|
||||
if resp.status_code == 200 and code in (0, 200, "0", "200"):
|
||||
return True, data, resp.status_code
|
||||
return False, data, resp.status_code
|
||||
|
||||
|
||||
@router.post("/huihui/sms/send")
|
||||
def sms_send(body: dict = Body(...)):
|
||||
"""请求会会发送短信验证码(真实开放平台 /open/mobile/sms/code)"""
|
||||
phone = (body.get("phone") or "").strip()
|
||||
if not phone or not phone.isdigit() or len(phone) != 11:
|
||||
return fail("请输入正确的 11 位手机号", 400)
|
||||
|
||||
# 临时开发态:本地模拟发码
|
||||
if DEV_MOCK and not _cfg_ready():
|
||||
code = str(random.randint(100000, 999999))
|
||||
_mock_codes[phone] = (code, datetime.now().timestamp() + _MOCK_TTL)
|
||||
return ok({"sent": True, "devCode": code, "dev": True})
|
||||
|
||||
if not _cfg_ready():
|
||||
return fail("会会短信服务未配置(缺少 HUIHUI_APP_ID / HUIHUI_ACCESS_ID / HUIHUI_ACCESS_SECRET)", 500)
|
||||
|
||||
# /open/mobile/sms/code:mobile 与公共字段均走 query
|
||||
form = _build_form({"mobile": phone})
|
||||
ok_flag, data, status = _call_huihui(SMS_SEND_PATH, form, as_query=True)
|
||||
if not ok_flag:
|
||||
return fail(data.get("message") or f"发送失败(HTTP {status})", 502)
|
||||
return ok({"sent": True})
|
||||
|
||||
|
||||
@router.post("/huihui/sms/login")
|
||||
def sms_login(body: dict = Body(...), db: Session = Depends(get_db)):
|
||||
"""短信验证码登录:调会会 /open/login/token(loginType=code) 换取 access_token + userId,落库并签发本系统会话"""
|
||||
phone = (body.get("phone") or "").strip()
|
||||
code = (body.get("code") or "").strip()
|
||||
if not phone or not code:
|
||||
return fail("手机号或验证码缺失", 400)
|
||||
|
||||
# 临时开发态:本地校验模拟码
|
||||
if DEV_MOCK and not _cfg_ready():
|
||||
rec = _mock_codes.get(phone)
|
||||
if not rec or rec[1] < datetime.now().timestamp():
|
||||
return fail("验证码已失效,请重新获取", 401)
|
||||
if rec[0] != code:
|
||||
return fail("验证码错误", 401)
|
||||
_mock_codes.pop(phone, None)
|
||||
return _issue_session(db, phone, {
|
||||
"userId": f"dev_{phone}",
|
||||
"nickname": f"会会用户{phone[-4:]}",
|
||||
"avatarUrl": "",
|
||||
"token": f"dev_token_{phone}",
|
||||
})
|
||||
|
||||
if not _cfg_ready():
|
||||
return fail("会会登录服务未配置", 500)
|
||||
|
||||
extra = {
|
||||
"username": phone,
|
||||
"password": code,
|
||||
"loginType": "code",
|
||||
"grantType": "password",
|
||||
"isRegister": "true",
|
||||
}
|
||||
if CLIENT_CODE:
|
||||
extra["clientCode"] = CLIENT_CODE
|
||||
form = _build_form(extra)
|
||||
ok_flag, data, status = _call_huihui(SMS_LOGIN_PATH, form, as_query=False)
|
||||
if not ok_flag:
|
||||
return fail(data.get("message") or f"登录失败(HTTP {status})", 401)
|
||||
|
||||
raw = data.get("data") or {}
|
||||
user_info = raw.get("userInfo", {}) if isinstance(raw, dict) else {}
|
||||
huihui_token = (
|
||||
_pick(raw, "accessToken", "access_token", "token")
|
||||
or _pick(user_info, "accessToken", "access_token", "token")
|
||||
)
|
||||
huihui_user_id = (
|
||||
_pick(raw, "openid", "userId", "uid", "openId", "id")
|
||||
or _pick(user_info, "openid", "userId", "uid", "openId", "id")
|
||||
)
|
||||
nickname = (
|
||||
_pick(raw, "nickName", "nickname", "name", "userName")
|
||||
or _pick(user_info, "nickName", "nickname", "name", "userName")
|
||||
)
|
||||
avatar_url = (
|
||||
_pick(raw, "avatar", "avatarUrl", "headImgUrl", "headimgurl")
|
||||
or _pick(user_info, "avatar", "avatarUrl", "headImgUrl", "headimgurl")
|
||||
)
|
||||
if not huihui_user_id:
|
||||
return fail("会会未返回用户标识", 502)
|
||||
|
||||
return _issue_session(db, phone, {
|
||||
"userId": huihui_user_id,
|
||||
"nickname": nickname,
|
||||
"avatarUrl": avatar_url,
|
||||
"token": huihui_token,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/huihui/pwd/login")
|
||||
def pwd_login(body: dict = Body(...), db: Session = Depends(get_db)):
|
||||
"""账号密码登录:调会会 /open/login/token(loginType=password) 换取 access_token + userId,落库并签发本系统会话。
|
||||
与 news_service 既有对接完全一致:username=账号/手机号, password=密码, loginType=password, grantType=password, isRegister=false。
|
||||
"""
|
||||
account = (body.get("account") or "").strip()
|
||||
password = (body.get("password") or "").strip()
|
||||
if not account or not password:
|
||||
return fail("账号或密码缺失", 400)
|
||||
|
||||
if not _cfg_ready():
|
||||
return fail("会会登录服务未配置", 500)
|
||||
|
||||
extra = {
|
||||
"username": account,
|
||||
"password": password,
|
||||
"loginType": "password",
|
||||
"grantType": "password",
|
||||
"isRegister": "false",
|
||||
}
|
||||
if CLIENT_CODE:
|
||||
extra["clientCode"] = CLIENT_CODE
|
||||
form = _build_form(extra)
|
||||
ok_flag, data, status = _call_huihui(SMS_LOGIN_PATH, form, as_query=False)
|
||||
if not ok_flag:
|
||||
return fail(data.get("message") or f"登录失败(HTTP {status})", 401)
|
||||
|
||||
raw = data.get("data") or {}
|
||||
user_info = raw.get("userInfo", {}) if isinstance(raw, dict) else {}
|
||||
huihui_token = (
|
||||
_pick(raw, "accessToken", "access_token", "token")
|
||||
or _pick(user_info, "accessToken", "access_token", "token")
|
||||
)
|
||||
huihui_user_id = (
|
||||
_pick(raw, "openid", "userId", "uid", "openId", "id")
|
||||
or _pick(user_info, "openid", "userId", "uid", "openId", "id")
|
||||
)
|
||||
nickname = (
|
||||
_pick(raw, "nickName", "nickname", "name", "userName")
|
||||
or _pick(user_info, "nickName", "nickname", "name", "userName")
|
||||
)
|
||||
avatar_url = (
|
||||
_pick(raw, "avatar", "avatarUrl", "headImgUrl", "headimgurl")
|
||||
or _pick(user_info, "avatar", "avatarUrl", "headImgUrl", "headimgurl")
|
||||
)
|
||||
if not huihui_user_id:
|
||||
return fail("会会未返回用户标识", 502)
|
||||
|
||||
# 账号即手机号时记录,便于资料展示;非手机号(用户名)则不覆盖已有 phone
|
||||
phone = account if (account.isdigit() and len(account) == 11) else ""
|
||||
return _issue_session(db, phone, {
|
||||
"userId": huihui_user_id,
|
||||
"nickname": nickname,
|
||||
"avatarUrl": avatar_url,
|
||||
"token": huihui_token,
|
||||
})
|
||||
|
||||
|
||||
def _issue_session(db: Session, phone: str, info: dict):
|
||||
"""建/链本地用户并签发本系统会话 token"""
|
||||
huihui_user_id = info.get("userId", "")
|
||||
user = db.query(User).filter(User.huihui_user_id == huihui_user_id).first()
|
||||
if not user:
|
||||
user = User(huihui_user_id=huihui_user_id)
|
||||
if phone:
|
||||
user.phone = phone
|
||||
if info.get("nickname"):
|
||||
user.nickname = info["nickname"]
|
||||
if info.get("avatarUrl"):
|
||||
user.avatar_url = info["avatarUrl"]
|
||||
user.huihui_token = info.get("token", "")
|
||||
user.app_token = uuid.uuid4().hex
|
||||
user.last_login_at = datetime.now()
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return ok({
|
||||
"token": user.app_token,
|
||||
"user": user.to_dict(),
|
||||
"huihui": {
|
||||
"userId": huihui_user_id,
|
||||
"nickname": info.get("nickname", ""),
|
||||
"avatarUrl": info.get("avatarUrl", ""),
|
||||
"token": info.get("token", ""),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@router.get("/huihui/me")
|
||||
def me(authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
"""当前登录用户信息(Bearer app_token)"""
|
||||
if not authorization:
|
||||
return fail("未登录", 401)
|
||||
token = authorization.replace("Bearer ", "", 1).replace("bearer ", "", 1).strip()
|
||||
user = db.query(User).filter(User.app_token == token).first()
|
||||
if not user:
|
||||
return fail("会话无效或已过期", 401)
|
||||
return ok(user.to_dict())
|
||||
|
||||
|
||||
@router.post("/huihui/logout")
|
||||
def logout(authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
"""退出登录(作废 app_token)"""
|
||||
if authorization:
|
||||
token = authorization.replace("Bearer ", "", 1).replace("bearer ", "", 1).strip()
|
||||
user = db.query(User).filter(User.app_token == token).first()
|
||||
if user:
|
||||
user.app_token = ""
|
||||
db.commit()
|
||||
return ok({"success": True})
|
||||
35
digital-avatar-app/backend/routers/organizations.py
Normal file
35
digital-avatar-app/backend/routers/organizations.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from models import Organization
|
||||
from responses import ok, fail
|
||||
|
||||
router = APIRouter(tags=["组织"])
|
||||
|
||||
|
||||
@router.get("/organizations")
|
||||
def list_orgs(page: int = 1, limit: int = 20, db: Session = Depends(get_db)):
|
||||
q = db.query(Organization)
|
||||
total = q.count()
|
||||
items = (
|
||||
q.order_by(Organization.created_at.desc())
|
||||
.offset((page - 1) * limit)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return ok({"data": [o.to_dict() for o in items], "total": total})
|
||||
|
||||
|
||||
@router.post("/organizations")
|
||||
def create_org(payload: dict = Body(...), db: Session = Depends(get_db)):
|
||||
o = Organization(
|
||||
name=payload.get("name", "未命名组织"),
|
||||
description=payload.get("desc", "") or payload.get("description", ""),
|
||||
emoji=payload.get("emoji", "🏢"),
|
||||
org_type=payload.get("type", "") or payload.get("orgType", "team"),
|
||||
)
|
||||
db.add(o)
|
||||
db.commit()
|
||||
db.refresh(o)
|
||||
return ok(o.to_dict())
|
||||
37
digital-avatar-app/backend/routers/tokens.py
Normal file
37
digital-avatar-app/backend/routers/tokens.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from models import TokenAccount, TokenPlan
|
||||
from responses import ok, fail
|
||||
|
||||
router = APIRouter(tags=["Token"])
|
||||
|
||||
|
||||
@router.get("/token/balance")
|
||||
def balance(db: Session = Depends(get_db)):
|
||||
acc = db.query(TokenAccount).first()
|
||||
return ok({"balance": acc.balance if acc else 0})
|
||||
|
||||
|
||||
@router.get("/token/plans")
|
||||
def plans(db: Session = Depends(get_db)):
|
||||
items = db.query(TokenPlan).order_by(TokenPlan.price.asc()).all()
|
||||
return ok([p.to_dict() for p in items])
|
||||
|
||||
|
||||
@router.post("/token/charge")
|
||||
def charge(payload: dict = Body(...), db: Session = Depends(get_db)):
|
||||
plan_id = payload.get("planId")
|
||||
plan = db.query(TokenPlan).filter(TokenPlan.id == plan_id).first()
|
||||
if not plan:
|
||||
return fail("套餐不存在", 404)
|
||||
acc = db.query(TokenAccount).first()
|
||||
if not acc:
|
||||
acc = TokenAccount(balance=0)
|
||||
db.add(acc)
|
||||
db.commit()
|
||||
db.refresh(acc)
|
||||
acc.balance += plan.amount
|
||||
db.commit()
|
||||
return ok({"balance": acc.balance, "charged": plan.amount})
|
||||
Reference in New Issue
Block a user