jiachenlong/backend/app/routers/purchase.py

760 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# purchase - 认购群API路由
# Version: 0.3.0 (2026-05-02)
# 新增:管理员审批功能
# 认购群、成员、藏品管理
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]
wechat_name: Optional[str] = None
contact: Optional[str] = None
purchase_count: Optional[int] = 0
paid_amount: Optional[float] = 0
notes: Optional[str] = None
# ============ 工具函数 ============
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()
# 管理员直接创建通过审批
if current_user.role in ["admin", "superadmin"]:
initial_status = "active"
else:
# 普通用户创建需要审批
initial_status = "pending"
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=initial_status,
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 == "pending":
raise HTTPException(status_code=400, detail="该认购群正在等待审批,请耐心等待")
if group.status == "rejected":
raise HTTPException(status_code=400, 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="您已加入该认购群")
else:
# pending/rejected/其他状态重新激活为pending
existing.status = "pending"
existing.joined_at = datetime.now()
existing.user_name = current_user.f01_01_name
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,
"wechat_name": m.wechat_name,
"contact": m.contact,
"purchase_count": m.purchase_count or 0,
"paid_amount": float(m.paid_amount or 0),
"notes": m.notes
}
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):
user_name: Optional[str] = None
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.user_name is not None:
member.user_name = data.user_name
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
# ============ 管理员审批API ============
class GroupApprovalResponse(BaseModel):
id: str
group_code: str
name: str
description: Optional[str]
creator_id: str
creator_name: Optional[str]
status: str
total_members: int
total_collections: int
created_at: Optional[datetime]
@router.get("/pending-approval", response_model=List[GroupApprovalResponse])
def get_pending_approval_groups(
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="只有管理员可以查看待审批列表")
groups = db.query(PurchaseGroup).filter(
PurchaseGroup.status == "pending"
).order_by(PurchaseGroup.created_at.asc()).all()
return groups
@router.post("/groups/{group_id}/approve")
def approve_group(
group_id: str,
action: str = Query(..., description="approve/reject"),
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="认购群不存在")
if group.status != "pending":
raise HTTPException(status_code=400, detail="该认购群不在待审批状态")
if action == "approve":
group.status = "active"
group.approved_by = current_user.f99_90_id
group.approved_at = datetime.now()
db.commit()
return {"message": "已批准该认购群", "status": "active"}
elif action == "reject":
group.status = "rejected"
group.approved_by = current_user.f99_90_id
group.approved_at = datetime.now()
db.commit()
return {"message": "已拒绝该认购群", "status": "rejected"}
else:
raise HTTPException(status_code=400, detail="无效操作")
@router.get("/my-pending-groups", response_model=List[PurchaseGroupResponse])
def get_my_pending_groups(
current_user = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取我创建的待审批认购群"""
if not current_user:
return []
groups = db.query(PurchaseGroup).filter(
PurchaseGroup.creator_id == current_user.f99_90_id,
PurchaseGroup.status == "pending"
).all()
return groups