# purchase - 认购群API路由 # Version: 0.0.5 (2026-04-29) # 认购群、成员、藏品管理 from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from pydantic import BaseModel from typing import Optional, List from datetime import datetime from app.core.database import get_db from app.core.auth import get_current_user from app.models.models import User from app.models.purchase import PurchaseGroup, PurchaseMember, PurchaseCollection router = APIRouter(prefix="/api/purchase", tags=["认购"]) # ============ Schema ============ class PurchaseGroupCreate(BaseModel): name: str description: str total_members: int total_collections: int cycle_months: int start_date: str class PurchaseGroupResponse(BaseModel): cycle_months: Optional[int] = None start_date: Optional[datetime] = None id: str group_code: str name: str description: Optional[str] creator_id: str creator_name: Optional[str] status: str total_members: int total_pending: int total_collections: int total_confirmed: int total_amount: float avg_price: Optional[float] = None created_at: Optional[datetime] class PurchaseCollectionCreate(BaseModel): collection_name: Optional[str] = None collection_code: Optional[str] = None number: Optional[str] = None collection_code: Optional[str] = None grading: Optional[str] = None boss_name: Optional[str] = None price: Optional[float] = None source: Optional[str] = None paid: Optional[bool] = False class PurchaseCollectionUpdate(BaseModel): collection_name: Optional[str] = None number: Optional[str] = None grading: Optional[str] = None boss_name: Optional[str] = None price: Optional[float] = None source: Optional[str] = None paid: Optional[bool] = None status: Optional[str] = None class PurchaseCollectionResponse(BaseModel): id: str group_id: str user_id: str user_name: Optional[str] collection_name: Optional[str] collection_code: Optional[str] number: Optional[str] = None collection_code: Optional[str] = None grading: Optional[str] = None boss_name: Optional[str] = None price: Optional[float] = None source: Optional[str] = None paid: bool status: str submit_date: Optional[datetime] submitted_at: Optional[datetime] class MemberResponse(BaseModel): id: str user_id: str user_name: Optional[str] user_avatar: Optional[str] role: str status: str joined_at: Optional[datetime] # ============ 工具函数 ============ def generate_group_code(db: Session) -> str: """生成群编码:年+序号,如 26001""" year = datetime.now().year % 100 # 26 # 查找当前年份最大的编号 last = db.query(PurchaseGroup).filter( PurchaseGroup.group_code.like(f"{year}%") ).order_by(PurchaseGroup.group_code.desc()).first() if last and last.group_code: try: num = int(last.group_code) + 1 except: num = 1 else: num = 1 return f"{year}{num:03d}" # 26001 # ============ 认购群API ============ @router.get("/groups", response_model=List[PurchaseGroupResponse]) def get_groups( status: str = Query("active", description="过滤状态"), db: Session = Depends(get_db) ): """获取所有认购群列表""" query = db.query(PurchaseGroup) if status: query = query.filter(PurchaseGroup.status == status) groups = query.order_by(PurchaseGroup.created_at.desc()).all() return groups @router.get("/my-groups", response_model=List[PurchaseGroupResponse]) def get_my_groups( current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """获取我参与的认购群""" if not current_user: return [] member_groups = db.query(PurchaseMember).filter( PurchaseMember.user_id == current_user.f99_90_id, PurchaseMember.status == "approved" ).all() group_ids = [m.group_id for m in member_groups] if not group_ids: return [] groups = db.query(PurchaseGroup).filter( PurchaseGroup.id.in_(group_ids) ).all() return groups @router.get("/groups/{group_id}/pending-members", response_model=List[MemberResponse]) def get_pending_members( group_id: str, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """获取待审批成员列表""" group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") if group.creator_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="只有群主可以查看") members = db.query(PurchaseMember).filter( PurchaseMember.group_id == group_id, PurchaseMember.status == "pending" ).order_by(PurchaseMember.joined_at.asc()).all() return members @router.post("/groups/{group_id}/members/{member_id}/approve") def approve_member( group_id: str, member_id: str, action: str = Query(..., description="approve/reject"), current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """审批或拒绝成员""" group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") if group.creator_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="只有群主可以审批") member = db.query(PurchaseMember).filter( PurchaseMember.id == member_id, PurchaseMember.group_id == group_id ).first() if not member: raise HTTPException(status_code=404, detail="成员不存在") if action == "approve": member.status = "approved" group.total_members += 1 group.total_pending -= 1 db.commit() return {"message": "已批准加入"} elif action == "reject": member.status = "rejected" group.total_pending -= 1 db.commit() return {"message": "已拒绝加入"} else: raise HTTPException(status_code=400, detail="无效操作") @router.post("/groups", response_model=PurchaseGroupResponse) def create_group( data: PurchaseGroupCreate, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """创建认购群""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") # 普通用户最多创建3个认购群 if current_user.role not in ["admin", "superadmin"]: existing_count = db.query(PurchaseGroup).filter( PurchaseGroup.creator_id == current_user.f99_90_id ).count() if existing_count >= 3: raise HTTPException(status_code=400, detail="普通用户最多创建3个认购群") group_code = generate_group_code(db) # 处理日期 start_date = None if data.start_date: try: start_date = datetime.fromisoformat(data.start_date.replace('Z', '+08:00')) except: start_date = datetime.now() group = PurchaseGroup( group_code=group_code, name=data.name, description=data.description, creator_id=current_user.f99_90_id, creator_name=current_user.f01_01_name, status="active", total_members=data.total_members, total_collections=data.total_collections, cycle_months=data.cycle_months, start_date=start_date ) db.add(group) db.commit() member = PurchaseMember( group_id=group.id, user_id=current_user.f99_90_id, user_name=current_user.f01_01_name, user_avatar=current_user.avatar, role="creator", status="approved" ) db.add(member) group.total_members = 1 db.commit() db.refresh(group) return group @router.get("/groups/{group_id}", response_model=PurchaseGroupResponse) def get_group(group_id: str, db: Session = Depends(get_db)): """获取认购群详情""" group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") return group @router.post("/groups/{group_id}/join") def join_group( group_id: str, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """申请加入认购群""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") if group.status != "active": raise HTTPException(status_code=400, detail="该认购群已结束") existing = db.query(PurchaseMember).filter( PurchaseMember.group_id == group_id, PurchaseMember.user_id == current_user.f99_90_id ).first() if existing: if existing.status == "approved": raise HTTPException(status_code=400, detail="您已加入该认购群") elif existing.status == "pending": raise HTTPException(status_code=400, detail="您已申请加入,等待审批") elif existing.status == "rejected": existing.status = "pending" existing.joined_at = datetime.now() group.total_pending += 1 db.commit() return {"message": "已重新申请,等待审批"} member = PurchaseMember( group_id=group_id, user_id=current_user.f99_90_id, user_name=current_user.f01_01_name, user_avatar=current_user.avatar, role="member", status="pending" ) db.add(member) group.total_pending += 1 db.commit() return {"message": "申请成功,等待群主审批"} @router.get("/groups/{group_id}/members", response_model=List[MemberResponse]) def get_group_members( group_id: str, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """获取认购群成员列表""" members = db.query(PurchaseMember).filter( PurchaseMember.group_id == group_id, PurchaseMember.status == "approved" ).order_by(PurchaseMember.joined_at.asc()).all() return [ { "id": m.id, "user_id": m.user_id, "user_name": m.user_name, "user_avatar": m.user_avatar, "role": m.role, "status": m.status, "joined_at": m.joined_at } for m in members ] @router.get("/groups/{group_id}/collections", response_model=List[PurchaseCollectionResponse]) def get_group_collections(group_id: str, db: Session = Depends(get_db)): """获取认购群藏品列表""" collections = db.query(PurchaseCollection).filter( PurchaseCollection.group_id == group_id ).order_by(PurchaseCollection.submitted_at.desc()).all() return [ { "id": c.id, "group_id": c.group_id, "user_id": c.user_id, "user_name": c.user_name, "collection_name": c.collection_name, "collection_code": c.collection_code, "number": c.number, "grading": c.grading, "boss_name": c.boss_name, "price": float(c.price), "source": c.source, "paid": c.paid, "status": c.status, "submit_date": c.submit_date, "submitted_at": c.submitted_at } for c in collections ] @router.post("/groups/{group_id}/collections", response_model=PurchaseCollectionResponse) def add_collection( group_id: str, data: PurchaseCollectionCreate, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """提交认购藏品""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") # All fields now optional - frontend will send what it has # No validation needed as all fields are Optional in schema group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") member = db.query(PurchaseMember).filter( PurchaseMember.group_id == group_id, PurchaseMember.user_id == current_user.f99_90_id, PurchaseMember.status == "approved" ).first() if not member: raise HTTPException(status_code=400, detail="请先加入该认购群并通过审批") max_code = db.query(PurchaseCollection).filter( PurchaseCollection.group_id == group_id ).order_by(PurchaseCollection.collection_code.desc()).first() if max_code and max_code.collection_code: try: next_num = int(max_code.collection_code) + 1 except: next_num = 1 else: next_num = 1 collection_code = f"{next_num:03d}" collection = PurchaseCollection( group_id=group_id, user_id=current_user.f99_90_id, user_name=current_user.f01_01_name, collection_name=data.collection_name, collection_code=collection_code, number=data.number, grading=data.grading, boss_name=data.boss_name, price=data.price, source=data.source, paid=data.paid, status="pending" ) db.add(collection) group.total_collections += 1 from decimal import Decimal group.total_amount += Decimal(str(data.price)) if group.total_collections > 0: group.avg_price = group.total_amount / group.total_collections db.commit() db.refresh(collection) return collection @router.put("/groups/{group_id}/collections/{collection_id}", response_model=PurchaseCollectionResponse) def update_collection( group_id: str, collection_id: str, data: PurchaseCollectionUpdate, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """编辑认购藏品 (仅群主可操作)""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") if group.creator_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="只有群主可以编辑") collection = db.query(PurchaseCollection).filter( PurchaseCollection.id == collection_id, PurchaseCollection.group_id == group_id ).first() if not collection: raise HTTPException(status_code=404, detail="藏品不存在") old_price = float(collection.price) # 更新字段 if data.collection_name is not None: collection.collection_name = data.collection_name if data.number is not None: collection.number = data.number if data.grading is not None: collection.grading = data.grading if data.boss_name is not None: collection.boss_name = data.boss_name if data.source is not None: collection.source = data.source if data.paid is not None: collection.paid = data.paid # 处理状态变化 new_status = data.status if new_status and new_status != collection.status: if new_status == "confirmed" and collection.status != "confirmed": group.total_confirmed += 1 elif collection.status == "confirmed" and new_status != "confirmed": group.total_confirmed -= 1 collection.status = new_status # 处理价格变化 if data.price is not None and data.price != old_price: from decimal import Decimal group.total_amount = group.total_amount - Decimal(str(old_price)) + Decimal(str(data.price)) if group.total_collections > 0: group.avg_price = group.total_amount / group.total_collections db.commit() db.refresh(collection) return collection @router.post("/groups/{group_id}/close") def close_group( group_id: str, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """结束认购群""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") if group.creator_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="只有群主可以结束认购群") group.status = "closed" db.commit() return {"message": "认购群已结束"} @router.delete("/groups/{group_id}") def delete_group( group_id: str, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """删除认购群 (仅管理员可操作)""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") # 管理员权限检查 if current_user.role not in ["admin", "superadmin"]: raise HTTPException(status_code=403, detail="只有管理员可以删除认购群") group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") # 删除相关成员和藏品 db.query(PurchaseMember).filter(PurchaseMember.group_id == group_id).delete() db.query(PurchaseCollection).filter(PurchaseCollection.group_id == group_id).delete() db.delete(group) db.commit() return {"message": "认购群已删除"} class MemberUpdate(BaseModel): wechat_name: Optional[str] = None contact: Optional[str] = None purchase_count: Optional[int] = None paid_amount: Optional[float] = None notes: Optional[str] = None class GroupUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None cycle_months: Optional[int] = None start_date: Optional[str] = None # 更新成员信息 @router.put("/groups/{group_id}/members/{member_id}") def update_member( group_id: str, member_id: str, data: MemberUpdate, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """更新成员信息 (成员自己或群主可编辑)""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") member = db.query(PurchaseMember).filter( PurchaseMember.id == member_id, PurchaseMember.group_id == group_id ).first() if not member: raise HTTPException(status_code=404, detail="成员不存在") # 只有成员自己或群主可以修改 if member.user_id != current_user.f99_90_id and group.creator_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="无权修改") if data.wechat_name is not None: member.wechat_name = data.wechat_name if data.contact is not None: member.contact = data.contact if data.purchase_count is not None: member.purchase_count = data.purchase_count if data.paid_amount is not None: from decimal import Decimal member.paid_amount = Decimal(str(data.paid_amount)) if data.notes is not None: member.notes = data.notes db.commit() db.refresh(member) return {"message": "更新成功", "member_id": member.id} # 更新群信息 @router.put("/groups/{group_id}", response_model=PurchaseGroupResponse) def update_group( group_id: str, data: GroupUpdate, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """更新群信息 (仅群主可编辑)""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="认购群不存在") if group.creator_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="只有群主可以修改") if data.name is not None: group.name = data.name if data.description is not None: group.description = data.description if data.cycle_months is not None: group.cycle_months = data.cycle_months if data.start_date is not None: try: group.start_date = datetime.fromisoformat(data.start_date.replace('Z', '+08:00')) except: pass db.commit() db.refresh(group) return group