2026-04-26 21:13:00 +08:00
|
|
|
|
# purchase - 认购群API路由
|
2026-05-02 13:50:21 +08:00
|
|
|
|
# Version: 0.3.0 (2026-05-02)
|
|
|
|
|
|
# 新增:管理员审批功能
|
2026-04-26 21:13:00 +08:00
|
|
|
|
# 认购群、成员、藏品管理
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-30 21:36:02 +08:00
|
|
|
|
description: str
|
|
|
|
|
|
total_members: int
|
|
|
|
|
|
total_collections: int
|
|
|
|
|
|
cycle_months: int
|
|
|
|
|
|
start_date: str
|
2026-04-26 21:13:00 +08:00
|
|
|
|
|
|
|
|
|
|
class PurchaseGroupResponse(BaseModel):
|
2026-04-29 20:48:41 +08:00
|
|
|
|
cycle_months: Optional[int] = None
|
|
|
|
|
|
start_date: Optional[datetime] = None
|
2026-04-26 21:13:00 +08:00
|
|
|
|
id: str
|
2026-04-29 20:48:41 +08:00
|
|
|
|
group_code: str
|
2026-04-26 21:13:00 +08:00
|
|
|
|
name: str
|
|
|
|
|
|
description: Optional[str]
|
|
|
|
|
|
creator_id: str
|
|
|
|
|
|
creator_name: Optional[str]
|
|
|
|
|
|
status: str
|
|
|
|
|
|
total_members: int
|
2026-04-29 20:48:41 +08:00
|
|
|
|
total_pending: int
|
2026-04-26 21:13:00 +08:00
|
|
|
|
total_collections: int
|
2026-04-29 20:48:41 +08:00
|
|
|
|
total_confirmed: int
|
2026-04-26 21:13:00 +08:00
|
|
|
|
total_amount: float
|
2026-04-30 00:39:55 +08:00
|
|
|
|
avg_price: Optional[float] = None
|
2026-04-26 21:13:00 +08:00
|
|
|
|
created_at: Optional[datetime]
|
|
|
|
|
|
|
|
|
|
|
|
class PurchaseCollectionCreate(BaseModel):
|
2026-04-30 00:39:55 +08:00
|
|
|
|
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
|
2026-04-29 20:48:41 +08:00
|
|
|
|
|
|
|
|
|
|
class PurchaseCollectionUpdate(BaseModel):
|
|
|
|
|
|
collection_name: Optional[str] = None
|
2026-04-26 21:13:00 +08:00
|
|
|
|
number: Optional[str] = None
|
2026-04-29 20:48:41 +08:00
|
|
|
|
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]
|
2026-04-30 00:39:55 +08:00
|
|
|
|
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
|
2026-04-29 20:48:41 +08:00
|
|
|
|
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]
|
2026-05-01 10:00:17 +08:00
|
|
|
|
wechat_name: Optional[str] = None
|
|
|
|
|
|
contact: Optional[str] = None
|
|
|
|
|
|
purchase_count: Optional[int] = 0
|
|
|
|
|
|
paid_amount: Optional[float] = 0
|
|
|
|
|
|
notes: Optional[str] = None
|
2026-04-29 20:48:41 +08:00
|
|
|
|
|
|
|
|
|
|
# ============ 工具函数 ============
|
|
|
|
|
|
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
|
2026-04-26 21:13:00 +08:00
|
|
|
|
|
|
|
|
|
|
# ============ 认购群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)
|
|
|
|
|
|
):
|
2026-05-01 10:00:17 +08:00
|
|
|
|
"""获取我参与的认购群(仅已批准的)"""
|
2026-04-30 00:39:55 +08:00
|
|
|
|
if not current_user:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
2026-04-26 21:13:00 +08:00
|
|
|
|
member_groups = db.query(PurchaseMember).filter(
|
2026-04-29 20:48:41 +08:00
|
|
|
|
PurchaseMember.user_id == current_user.f99_90_id,
|
|
|
|
|
|
PurchaseMember.status == "approved"
|
2026-04-26 21:13:00 +08:00
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
|
|
group_ids = [m.group_id for m in member_groups]
|
2026-04-30 00:39:55 +08:00
|
|
|
|
if not group_ids:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
2026-04-26 21:13:00 +08:00
|
|
|
|
groups = db.query(PurchaseGroup).filter(
|
|
|
|
|
|
PurchaseGroup.id.in_(group_ids)
|
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
|
|
return groups
|
|
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
@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="无效操作")
|
|
|
|
|
|
|
2026-04-26 21:13:00 +08:00
|
|
|
|
@router.post("/groups", response_model=PurchaseGroupResponse)
|
|
|
|
|
|
def create_group(
|
|
|
|
|
|
data: PurchaseGroupCreate,
|
|
|
|
|
|
current_user = Depends(get_current_user),
|
|
|
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
|
):
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"""创建认购群"""
|
2026-04-26 21:13:00 +08:00
|
|
|
|
if not current_user:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="请先登录")
|
|
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
# 普通用户最多创建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()
|
|
|
|
|
|
|
2026-05-02 13:50:21 +08:00
|
|
|
|
# 管理员直接创建通过审批
|
|
|
|
|
|
if current_user.role in ["admin", "superadmin"]:
|
|
|
|
|
|
initial_status = "active"
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 普通用户创建需要审批
|
|
|
|
|
|
initial_status = "pending"
|
|
|
|
|
|
|
2026-04-26 21:13:00 +08:00
|
|
|
|
group = PurchaseGroup(
|
2026-04-29 20:48:41 +08:00
|
|
|
|
group_code=group_code,
|
2026-04-26 21:13:00 +08:00
|
|
|
|
name=data.name,
|
|
|
|
|
|
description=data.description,
|
|
|
|
|
|
creator_id=current_user.f99_90_id,
|
|
|
|
|
|
creator_name=current_user.f01_01_name,
|
2026-05-02 13:50:21 +08:00
|
|
|
|
status=initial_status,
|
2026-04-30 21:36:02 +08:00
|
|
|
|
total_members=data.total_members,
|
|
|
|
|
|
total_collections=data.total_collections,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
cycle_months=data.cycle_months,
|
|
|
|
|
|
start_date=start_date
|
2026-04-26 21:13:00 +08:00
|
|
|
|
)
|
|
|
|
|
|
db.add(group)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
2026-05-02 13:50:21 +08:00
|
|
|
|
# 创建者直接成为成员
|
2026-04-26 21:13:00 +08:00
|
|
|
|
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,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
role="creator",
|
|
|
|
|
|
status="approved"
|
2026-04-26 21:13:00 +08:00
|
|
|
|
)
|
|
|
|
|
|
db.add(member)
|
2026-04-29 20:48:41 +08:00
|
|
|
|
group.total_members = 1
|
2026-04-26 21:13:00 +08:00
|
|
|
|
db.commit()
|
|
|
|
|
|
db.refresh(group)
|
|
|
|
|
|
|
|
|
|
|
|
return group
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/groups/{group_id}", response_model=PurchaseGroupResponse)
|
2026-04-29 20:48:41 +08:00
|
|
|
|
def get_group(group_id: str, db: Session = Depends(get_db)):
|
2026-04-26 21:13:00 +08:00
|
|
|
|
"""获取认购群详情"""
|
|
|
|
|
|
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)
|
|
|
|
|
|
):
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"""申请加入认购群"""
|
2026-04-26 21:13:00 +08:00
|
|
|
|
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="认购群不存在")
|
|
|
|
|
|
|
2026-05-02 13:50:21 +08:00
|
|
|
|
if group.status == "pending":
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="该认购群正在等待审批,请耐心等待")
|
|
|
|
|
|
|
|
|
|
|
|
if group.status == "rejected":
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="该认购群申请已被拒绝")
|
|
|
|
|
|
|
2026-04-26 21:13:00 +08:00
|
|
|
|
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:
|
2026-04-29 20:48:41 +08:00
|
|
|
|
if existing.status == "approved":
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="您已加入该认购群")
|
2026-05-01 10:00:17 +08:00
|
|
|
|
else:
|
|
|
|
|
|
# pending/rejected/其他状态:重新激活为pending
|
2026-04-29 20:48:41 +08:00
|
|
|
|
existing.status = "pending"
|
|
|
|
|
|
existing.joined_at = datetime.now()
|
2026-05-01 10:00:17 +08:00
|
|
|
|
existing.user_name = current_user.f01_01_name
|
2026-04-29 20:48:41 +08:00
|
|
|
|
group.total_pending += 1
|
|
|
|
|
|
db.commit()
|
2026-05-01 10:00:17 +08:00
|
|
|
|
return {"message": "申请成功,等待审批"}
|
2026-04-26 21:13:00 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
role="member",
|
|
|
|
|
|
status="pending"
|
2026-04-26 21:13:00 +08:00
|
|
|
|
)
|
|
|
|
|
|
db.add(member)
|
2026-04-29 20:48:41 +08:00
|
|
|
|
group.total_pending += 1
|
2026-04-26 21:13:00 +08:00
|
|
|
|
db.commit()
|
|
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
return {"message": "申请成功,等待群主审批"}
|
2026-04-26 21:13:00 +08:00
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
@router.get("/groups/{group_id}/members", response_model=List[MemberResponse])
|
2026-04-26 21:13:00 +08:00
|
|
|
|
def get_group_members(
|
|
|
|
|
|
group_id: str,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
current_user = Depends(get_current_user),
|
2026-04-26 21:13:00 +08:00
|
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取认购群成员列表"""
|
|
|
|
|
|
members = db.query(PurchaseMember).filter(
|
2026-04-29 20:48:41 +08:00
|
|
|
|
PurchaseMember.group_id == group_id,
|
|
|
|
|
|
PurchaseMember.status == "approved"
|
2026-04-26 21:13:00 +08:00
|
|
|
|
).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,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"status": m.status,
|
2026-05-01 10:00:17 +08:00
|
|
|
|
"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
|
2026-04-26 21:13:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
for m in members
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
@router.get("/groups/{group_id}/collections", response_model=List[PurchaseCollectionResponse])
|
|
|
|
|
|
def get_group_collections(group_id: str, db: Session = Depends(get_db)):
|
2026-04-26 21:13:00 +08:00
|
|
|
|
"""获取认购群藏品列表"""
|
|
|
|
|
|
collections = db.query(PurchaseCollection).filter(
|
|
|
|
|
|
PurchaseCollection.group_id == group_id
|
|
|
|
|
|
).order_by(PurchaseCollection.submitted_at.desc()).all()
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": c.id,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"group_id": c.group_id,
|
2026-04-26 21:13:00 +08:00
|
|
|
|
"user_id": c.user_id,
|
|
|
|
|
|
"user_name": c.user_name,
|
|
|
|
|
|
"collection_name": c.collection_name,
|
|
|
|
|
|
"collection_code": c.collection_code,
|
|
|
|
|
|
"number": c.number,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"grading": c.grading,
|
|
|
|
|
|
"boss_name": c.boss_name,
|
2026-04-26 21:13:00 +08:00
|
|
|
|
"price": float(c.price),
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"source": c.source,
|
|
|
|
|
|
"paid": c.paid,
|
2026-04-26 21:13:00 +08:00
|
|
|
|
"status": c.status,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"submit_date": c.submit_date,
|
2026-04-26 21:13:00 +08:00
|
|
|
|
"submitted_at": c.submitted_at
|
|
|
|
|
|
}
|
|
|
|
|
|
for c in collections
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
@router.post("/groups/{group_id}/collections", response_model=PurchaseCollectionResponse)
|
2026-04-26 21:13:00 +08:00
|
|
|
|
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="请先登录")
|
|
|
|
|
|
|
2026-04-30 00:39:55 +08:00
|
|
|
|
# All fields now optional - frontend will send what it has
|
|
|
|
|
|
# No validation needed as all fields are Optional in schema
|
2026-04-29 20:48:41 +08:00
|
|
|
|
|
2026-04-26 21:13:00 +08:00
|
|
|
|
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,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
PurchaseMember.user_id == current_user.f99_90_id,
|
|
|
|
|
|
PurchaseMember.status == "approved"
|
2026-04-26 21:13:00 +08:00
|
|
|
|
).first()
|
|
|
|
|
|
|
|
|
|
|
|
if not member:
|
2026-04-29 20:48:41 +08:00
|
|
|
|
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}"
|
2026-04-26 21:13:00 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
collection_code=collection_code,
|
2026-04-26 21:13:00 +08:00
|
|
|
|
number=data.number,
|
2026-04-29 20:48:41 +08:00
|
|
|
|
grading=data.grading,
|
|
|
|
|
|
boss_name=data.boss_name,
|
|
|
|
|
|
price=data.price,
|
|
|
|
|
|
source=data.source,
|
|
|
|
|
|
paid=data.paid,
|
|
|
|
|
|
status="pending"
|
2026-04-26 21:13:00 +08:00
|
|
|
|
)
|
|
|
|
|
|
db.add(collection)
|
|
|
|
|
|
|
|
|
|
|
|
group.total_collections += 1
|
2026-04-29 20:48:41 +08:00
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
group.total_amount += Decimal(str(data.price))
|
2026-04-26 21:13:00 +08:00
|
|
|
|
if group.total_collections > 0:
|
|
|
|
|
|
group.avg_price = group.total_amount / group.total_collections
|
|
|
|
|
|
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.refresh(collection)
|
|
|
|
|
|
|
|
|
|
|
|
return collection
|
|
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
@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
|
|
|
|
|
|
|
2026-04-26 21:13:00 +08:00
|
|
|
|
@router.post("/groups/{group_id}/close")
|
|
|
|
|
|
def close_group(
|
|
|
|
|
|
group_id: str,
|
|
|
|
|
|
current_user = Depends(get_current_user),
|
|
|
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
|
):
|
2026-04-29 20:48:41 +08:00
|
|
|
|
"""结束认购群"""
|
2026-04-26 21:13:00 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-04-29 20:48:41 +08:00
|
|
|
|
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):
|
2026-05-01 10:00:17 +08:00
|
|
|
|
user_name: Optional[str] = None
|
2026-04-29 20:48:41 +08:00
|
|
|
|
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="无权修改")
|
|
|
|
|
|
|
2026-05-01 10:00:17 +08:00
|
|
|
|
if data.user_name is not None:
|
|
|
|
|
|
member.user_name = data.user_name
|
2026-04-29 20:48:41 +08:00
|
|
|
|
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
|
2026-05-02 13:50:21 +08:00
|
|
|
|
|
|
|
|
|
|
# ============ 管理员审批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
|