diff --git a/backend/app/api/endpoints/avatars.py b/backend/app/api/endpoints/avatars.py index bb38988..496544f 100644 --- a/backend/app/api/endpoints/avatars.py +++ b/backend/app/api/endpoints/avatars.py @@ -1,8 +1,14 @@ """数字分身管理 API 端点""" -from fastapi import APIRouter, Query, HTTPException +from typing import Optional +import mimetypes +import uuid +from fastapi import APIRouter, Query, HTTPException, UploadFile, File +from fastapi.responses import JSONResponse from app.schemas import ApiResponse from app.services.avatar_service import avatar_service, get_session, is_available +from app.core.database import AsyncSessionLocal +from app.core.config import settings router = APIRouter() @@ -69,3 +75,127 @@ def update_avatar_status(avatar_id: str, body: dict): return ApiResponse(data=data, message=msg) finally: db.close() + + +async def _upload_to_filecenter(file_bytes: bytes, filename: str) -> str: + """利用会会平台 filecenter 上传头像,返回 URL""" + import httpx + import hashlib + import random + from datetime import datetime + + async with AsyncSessionLocal() as db: + # 读取平台配置 + from sqlalchemy import select + from app.models import SystemConfig + result = await db.execute(select(SystemConfig)) + configs = {row.config_key: row.config_value for row in result.scalars().all()} + + cfg = { + "appId": configs.get("platform_app_id", ""), + "accessId": configs.get("platform_access_id", ""), + "accessSecret": configs.get("platform_access_secret", ""), + } + biz_url = configs.get("news_platform_base_url", "http://192.168.1.200:63120") + + if not cfg["accessSecret"]: + raise RuntimeError("平台 accessSecret 未配置") + + # 构建签名参数 + nonce = str(random.random())[2:][: random.randint(8, 12)] + timestamp = datetime.now().strftime("%Y%m%d%I%M%S") + sign_params = { + "appId": cfg["appId"], + "accessId": cfg["accessId"], + "timestamp": timestamp, + "nonce": nonce, + "module": "userInfo", + "service": "kccloud", + } + + # 计算签名 + keys = sorted(sign_params.keys()) + sign_parts = [] + for k in keys: + v = sign_params.get(k) + if v and v != "" and v != []: + sign_parts.append(f"{k}={v}") + sign_str = "&".join(sign_parts) + f"&accessSecret={cfg['accessSecret']}" + signature = hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper() + + # 构建 filecenter URL + filecenter_url = biz_url.replace("/huihuibusiness", "/filecenter") + if "/api/" in filecenter_url: + filecenter_url = filecenter_url.split("/api/", 1)[0] + "/api/filecenter" + else: + filecenter_url = filecenter_url.rstrip("/") + "/filecenter" + + # 确定 MIME 类型 + mime = mimetypes.guess_type(filename)[0] or "image/jpeg" + files = {"file": (filename, file_bytes, mime)} + + # 发送请求 + async with httpx.AsyncClient(timeout=30) as client: + r = await client.post( + f"{filecenter_url}/fileUpload", + files=files, + data={**sign_params, "signature": signature}, + ) + d = r.json() + if d.get("code") in [0, 200]: + url = d.get("data") or d.get("url") or "" + if isinstance(url, dict): + url = url.get("url") or url.get("path") or "" + if not url: + raise RuntimeError(f"filecenter 返回空 URL: {d}") + return url + raise RuntimeError(f"filecenter 上传失败: {d.get('message', '未知错误')}") + + +@router.post("/{avatar_id}/upload-photo") +async def upload_avatar_photo( + avatar_id: str, + file: UploadFile = File(...), +): + """上传数字分身头像(通过会会平台 filecenter)""" + if not is_available(): + raise HTTPException(status_code=503, detail="数字分身数据库尚未初始化") + + # 验证文件 + if not file.content_type or not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="仅支持图片文件") + + file_bytes = await file.read() + if len(file_bytes) > 5 * 1024 * 1024: + raise HTTPException(status_code=400, detail="头像文件不能超过5MB") + + # 确保文件扩展名正确 + filename = file.filename or "avatar.jpg" + ext = filename.split(".")[-1].lower() if "." in filename else "jpg" + if ext not in ("jpg", "jpeg", "png", "gif", "webp"): + filename = f"avatar.{ext}" + + try: + photo_url = await _upload_to_filecenter(file_bytes, filename) + except Exception as e: + raise HTTPException(status_code=500, detail=f"头像上传失败: {str(e)}") + + # 更新数据库 + db = get_session() + try: + data = avatar_service.get_avatar(db, avatar_id) + if not data: + raise HTTPException(status_code=404, detail="数字分身不存在") + + from sqlalchemy import text + db.execute( + text("UPDATE avatars SET photo_url = :url, updated_at = datetime('now') WHERE id = :id"), + {"url": photo_url, "id": avatar_id}, + ) + db.commit() + + # 刷新数据 + updated = avatar_service.get_avatar(db, avatar_id) + return ApiResponse(data=updated, message="头像上传成功") + finally: + db.close() diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 2d5dc51..7dbffa3 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -90,6 +90,9 @@ export const tailLogFile = (filename, lines = 100) => request.get(`/logs/files/$ export const getAvatars = (params) => request.get('/avatars', { params }) export const getAvatarDetail = (id) => request.get(`/avatars/${id}`) export const updateAvatarStatus = (id, status) => request.put(`/avatars/${id}/status`, { status }) +export const uploadAvatarPhoto = (id, formData) => request.post(`/avatars/${id}/upload-photo`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } +}) export default request export const uploadAvatar = (userId, formData) => request.post(`/users/${userId}/upload-avatar`, formData, { headers: { "Content-Type": "multipart/form-data" } }) diff --git a/frontend/src/views/Avatars.vue b/frontend/src/views/Avatars.vue index fc2fd00..d43c8a5 100644 --- a/frontend/src/views/Avatars.vue +++ b/frontend/src/views/Avatars.vue @@ -76,9 +76,14 @@ - + @@ -195,7 +200,7 @@