feat: 添加 Profile/Messages API 及 SSE 推送集成
- Profile API: GET/PUT /profile + PUT /profile/password - Messages API: 模型/迁移(005)/服务/路由 + 任务操作自动创建消息 - SSE 推送集成: tasks.py 中 6 个操作触发 SSE 通知 - Alembic 迁移: 004 audit_logs + 005 messages - env.py 导入所有模型确保迁移正确 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
68dac332d4
commit
ea807974cf
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
消息/通知 API
|
||||
"""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.api.deps import get_current_user
|
||||
from app.schemas.message import MessageResponse, MessageListResponse, UnreadCountResponse
|
||||
from app.services.message_service import (
|
||||
list_messages,
|
||||
get_unread_count,
|
||||
mark_as_read,
|
||||
mark_all_as_read,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/messages", tags=["消息"])
|
||||
|
||||
|
||||
@router.get("", response_model=MessageListResponse)
|
||||
async def get_messages(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
is_read: Optional[bool] = Query(None),
|
||||
type: Optional[str] = Query(None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取消息列表"""
|
||||
messages, total = await list_messages(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
is_read=is_read,
|
||||
type=type,
|
||||
)
|
||||
|
||||
return MessageListResponse(
|
||||
items=[
|
||||
MessageResponse(
|
||||
id=m.id,
|
||||
type=m.type,
|
||||
title=m.title,
|
||||
content=m.content,
|
||||
is_read=m.is_read,
|
||||
related_task_id=m.related_task_id,
|
||||
related_project_id=m.related_project_id,
|
||||
sender_name=m.sender_name,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
for m in messages
|
||||
],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/unread-count", response_model=UnreadCountResponse)
|
||||
async def get_message_unread_count(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取未读消息数"""
|
||||
count = await get_unread_count(db, current_user.id)
|
||||
return UnreadCountResponse(count=count)
|
||||
|
||||
|
||||
@router.put("/{message_id}/read")
|
||||
async def mark_message_as_read(
|
||||
message_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""标记消息已读"""
|
||||
success = await mark_as_read(db, message_id, current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="消息不存在",
|
||||
)
|
||||
await db.commit()
|
||||
return {"message": "已标记为已读"}
|
||||
|
||||
|
||||
@router.put("/read-all")
|
||||
async def mark_all_messages_as_read(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""标记所有消息已读"""
|
||||
count = await mark_all_as_read(db, current_user.id)
|
||||
await db.commit()
|
||||
return {"message": f"已标记 {count} 条消息为已读", "count": count}
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
用户资料 API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.organization import Brand, Agency, Creator
|
||||
from app.api.deps import get_current_user
|
||||
from app.services.auth import verify_password, hash_password
|
||||
from app.schemas.profile import (
|
||||
ProfileResponse,
|
||||
ProfileUpdateRequest,
|
||||
ChangePasswordRequest,
|
||||
BrandProfile,
|
||||
AgencyProfile,
|
||||
CreatorProfile,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/profile", tags=["用户资料"])
|
||||
|
||||
|
||||
def _build_profile_response(user: User, brand=None, agency=None, creator=None) -> ProfileResponse:
|
||||
"""构建资料响应"""
|
||||
resp = ProfileResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
phone=user.phone,
|
||||
name=user.name,
|
||||
avatar=user.avatar,
|
||||
role=user.role.value,
|
||||
is_verified=user.is_verified,
|
||||
created_at=user.created_at,
|
||||
)
|
||||
if brand:
|
||||
resp.brand = BrandProfile(
|
||||
id=brand.id,
|
||||
name=brand.name,
|
||||
logo=brand.logo,
|
||||
description=brand.description,
|
||||
contact_name=brand.contact_name,
|
||||
contact_phone=brand.contact_phone,
|
||||
contact_email=brand.contact_email,
|
||||
)
|
||||
if agency:
|
||||
resp.agency = AgencyProfile(
|
||||
id=agency.id,
|
||||
name=agency.name,
|
||||
logo=agency.logo,
|
||||
description=agency.description,
|
||||
contact_name=agency.contact_name,
|
||||
contact_phone=agency.contact_phone,
|
||||
contact_email=agency.contact_email,
|
||||
)
|
||||
if creator:
|
||||
resp.creator = CreatorProfile(
|
||||
id=creator.id,
|
||||
name=creator.name,
|
||||
avatar=creator.avatar,
|
||||
bio=creator.bio,
|
||||
douyin_account=creator.douyin_account,
|
||||
xiaohongshu_account=creator.xiaohongshu_account,
|
||||
bilibili_account=creator.bilibili_account,
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
async def _get_role_entity(db: AsyncSession, user: User):
|
||||
"""根据角色获取对应实体"""
|
||||
if user.role == UserRole.BRAND:
|
||||
result = await db.execute(select(Brand).where(Brand.user_id == user.id))
|
||||
return result.scalar_one_or_none(), None, None
|
||||
elif user.role == UserRole.AGENCY:
|
||||
result = await db.execute(select(Agency).where(Agency.user_id == user.id))
|
||||
return None, result.scalar_one_or_none(), None
|
||||
elif user.role == UserRole.CREATOR:
|
||||
result = await db.execute(select(Creator).where(Creator.user_id == user.id))
|
||||
return None, None, result.scalar_one_or_none()
|
||||
return None, None, None
|
||||
|
||||
|
||||
@router.get("", response_model=ProfileResponse)
|
||||
async def get_profile(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户资料"""
|
||||
brand, agency, creator = await _get_role_entity(db, current_user)
|
||||
return _build_profile_response(current_user, brand, agency, creator)
|
||||
|
||||
|
||||
@router.put("", response_model=ProfileResponse)
|
||||
async def update_profile(
|
||||
request: ProfileUpdateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新当前用户资料"""
|
||||
# 更新 User 表通用字段
|
||||
if request.name is not None:
|
||||
current_user.name = request.name
|
||||
if request.avatar is not None:
|
||||
current_user.avatar = request.avatar
|
||||
if request.phone is not None:
|
||||
current_user.phone = request.phone
|
||||
|
||||
# 更新角色表字段
|
||||
brand, agency, creator = await _get_role_entity(db, current_user)
|
||||
|
||||
if current_user.role == UserRole.BRAND and brand:
|
||||
if request.name is not None:
|
||||
brand.name = request.name
|
||||
if request.description is not None:
|
||||
brand.description = request.description
|
||||
if request.contact_name is not None:
|
||||
brand.contact_name = request.contact_name
|
||||
if request.contact_phone is not None:
|
||||
brand.contact_phone = request.contact_phone
|
||||
if request.contact_email is not None:
|
||||
brand.contact_email = request.contact_email
|
||||
|
||||
elif current_user.role == UserRole.AGENCY and agency:
|
||||
if request.name is not None:
|
||||
agency.name = request.name
|
||||
if request.description is not None:
|
||||
agency.description = request.description
|
||||
if request.contact_name is not None:
|
||||
agency.contact_name = request.contact_name
|
||||
if request.contact_phone is not None:
|
||||
agency.contact_phone = request.contact_phone
|
||||
if request.contact_email is not None:
|
||||
agency.contact_email = request.contact_email
|
||||
|
||||
elif current_user.role == UserRole.CREATOR and creator:
|
||||
if request.name is not None:
|
||||
creator.name = request.name
|
||||
if request.avatar is not None:
|
||||
creator.avatar = request.avatar
|
||||
if request.bio is not None:
|
||||
creator.bio = request.bio
|
||||
if request.douyin_account is not None:
|
||||
creator.douyin_account = request.douyin_account
|
||||
if request.xiaohongshu_account is not None:
|
||||
creator.xiaohongshu_account = request.xiaohongshu_account
|
||||
if request.bilibili_account is not None:
|
||||
creator.bilibili_account = request.bilibili_account
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 重新查询返回最新数据
|
||||
brand, agency, creator = await _get_role_entity(db, current_user)
|
||||
return _build_profile_response(current_user, brand, agency, creator)
|
||||
|
||||
|
||||
@router.put("/password")
|
||||
async def change_password(
|
||||
request: ChangePasswordRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""修改密码"""
|
||||
if not verify_password(request.old_password, current_user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="原密码不正确",
|
||||
)
|
||||
|
||||
current_user.password_hash = hash_password(request.new_password)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "密码修改成功"}
|
||||
@@ -51,6 +51,8 @@ from app.services.task_service import (
|
||||
list_pending_reviews_for_agency,
|
||||
list_pending_reviews_for_brand,
|
||||
)
|
||||
from app.api.sse import notify_new_task, notify_task_updated, notify_review_decision
|
||||
from app.services.message_service import create_message
|
||||
|
||||
router = APIRouter(prefix="/tasks", tags=["任务"])
|
||||
|
||||
@@ -172,6 +174,31 @@ async def create_new_task(
|
||||
# 重新加载关联
|
||||
task = await get_task_by_id(db, task.id)
|
||||
|
||||
# 创建消息 + SSE 通知达人有新任务
|
||||
try:
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=creator.user_id,
|
||||
type="new_task",
|
||||
title="新任务分配",
|
||||
content=f"您有新的任务「{task.name}」,来自项目「{task.project.name}」",
|
||||
related_task_id=task.id,
|
||||
related_project_id=task.project.id,
|
||||
sender_name=agency.name,
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await notify_new_task(
|
||||
task_id=task.id,
|
||||
creator_user_id=creator.user_id,
|
||||
task_name=task.name,
|
||||
project_name=task.project.name,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _task_to_response(task)
|
||||
|
||||
|
||||
@@ -367,6 +394,21 @@ async def upload_task_script(
|
||||
# 重新加载关联
|
||||
task = await get_task_by_id(db, task.id)
|
||||
|
||||
# SSE 通知代理商脚本已上传
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
agency_obj = result.scalar_one_or_none()
|
||||
if agency_obj:
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=[agency_obj.user_id],
|
||||
data={"action": "script_uploaded", "stage": task.stage.value},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _task_to_response(task)
|
||||
|
||||
|
||||
@@ -415,6 +457,21 @@ async def upload_task_video(
|
||||
# 重新加载关联
|
||||
task = await get_task_by_id(db, task.id)
|
||||
|
||||
# SSE 通知代理商视频已上传
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
agency_obj = result.scalar_one_or_none()
|
||||
if agency_obj:
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=[agency_obj.user_id],
|
||||
data={"action": "video_uploaded", "stage": task.stage.value},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _task_to_response(task)
|
||||
|
||||
|
||||
@@ -523,6 +580,41 @@ async def review_script(
|
||||
# 重新加载关联
|
||||
task = await get_task_by_id(db, task.id)
|
||||
|
||||
# 创建消息 + SSE 通知达人脚本审核结果
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
creator_obj = result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
reviewer_type = "agency" if current_user.role == UserRole.AGENCY else "brand"
|
||||
action_text = {"pass": "通过", "reject": "驳回", "force_pass": "强制通过"}.get(request.action, request.action)
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=creator_obj.user_id,
|
||||
type=request.action,
|
||||
title=f"脚本审核{action_text}",
|
||||
content=f"您的任务「{task.name}」脚本已被{action_text}" + (f",评语:{request.comment}" if request.comment else ""),
|
||||
related_task_id=task.id,
|
||||
sender_name=current_user.name,
|
||||
)
|
||||
await db.commit()
|
||||
await notify_review_decision(
|
||||
task_id=task.id,
|
||||
creator_user_id=creator_obj.user_id,
|
||||
review_type="script",
|
||||
reviewer_type=reviewer_type,
|
||||
action=request.action,
|
||||
comment=request.comment,
|
||||
)
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=[creator_obj.user_id],
|
||||
data={"action": f"script_{request.action}", "stage": task.stage.value},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _task_to_response(task)
|
||||
|
||||
|
||||
@@ -628,6 +720,41 @@ async def review_video(
|
||||
# 重新加载关联
|
||||
task = await get_task_by_id(db, task.id)
|
||||
|
||||
# 创建消息 + SSE 通知达人视频审核结果
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
creator_obj = result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
reviewer_type = "agency" if current_user.role == UserRole.AGENCY else "brand"
|
||||
action_text = {"pass": "通过", "reject": "驳回", "force_pass": "强制通过"}.get(request.action, request.action)
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=creator_obj.user_id,
|
||||
type=request.action,
|
||||
title=f"视频审核{action_text}",
|
||||
content=f"您的任务「{task.name}」视频已被{action_text}" + (f",评语:{request.comment}" if request.comment else ""),
|
||||
related_task_id=task.id,
|
||||
sender_name=current_user.name,
|
||||
)
|
||||
await db.commit()
|
||||
await notify_review_decision(
|
||||
task_id=task.id,
|
||||
creator_user_id=creator_obj.user_id,
|
||||
review_type="video",
|
||||
reviewer_type=reviewer_type,
|
||||
action=request.action,
|
||||
comment=request.comment,
|
||||
)
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=[creator_obj.user_id],
|
||||
data={"action": f"video_{request.action}", "stage": task.stage.value},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _task_to_response(task)
|
||||
|
||||
|
||||
@@ -676,6 +803,21 @@ async def submit_task_appeal(
|
||||
# 重新加载关联
|
||||
task = await get_task_by_id(db, task.id)
|
||||
|
||||
# SSE 通知代理商有新申诉
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
agency_obj = result.scalar_one_or_none()
|
||||
if agency_obj:
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=[agency_obj.user_id],
|
||||
data={"action": "appeal_submitted", "stage": task.stage.value},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _task_to_response(task)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user