diff --git a/backend/app/main.py b/backend/app/main.py index 0587121..6738fd6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -19,6 +19,7 @@ from app.routers import information as information_router from app.routers import yichens as yichens_router from app.routers import seek as seek_router from app.routers import deal as deal_router +from app.routers import purchase as purchase_router # 版本信息 - 从 config/VERSION 文件读取 def get_version(): @@ -88,6 +89,7 @@ app.include_router(information_router.router) app.include_router(yichens_router.router) # 一尘看板 app.include_router(seek_router.router) # 寻配号 app.include_router(deal_router.router) # 成交行情 +app.include_router(purchase_router.router) # 认购群 @app.get("/") diff --git a/backend/app/models/purchase.py b/backend/app/models/purchase.py new file mode 100644 index 0000000..ef9d392 --- /dev/null +++ b/backend/app/models/purchase.py @@ -0,0 +1,55 @@ +# purchase - 认购群模型 +# Version: 0.0.1 (2026-04-24) +# 认购群、成员、藏品管理 + +from sqlalchemy import Column, String, Text, Integer, DECIMAL, DateTime, ForeignKey +from sqlalchemy.sql import func +from app.core.database import Base +import uuid + +def generate_uuid(): + return str(uuid.uuid4()) + +class PurchaseGroup(Base): + """认购群表""" + __tablename__ = "purchase_group" + + id = Column(String(36), primary_key=True, default=generate_uuid) + name = Column(String(100), nullable=False) # 认购群名称 + description = Column(Text, nullable=True) # 描述 + creator_id = Column(String(36), nullable=False) # 创建者ID + creator_name = Column(String(50), nullable=True) # 创建者名字 + status = Column(String(20), default="active") # active/closed + total_members = Column(Integer, default=0) # 总参与人数 + total_collections = Column(Integer, default=0) # 总藏品数 + total_amount = Column(DECIMAL(12,2), default=0) # 总金额 + avg_price = Column(DECIMAL(10,2), default=0) # 均价 + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + +class PurchaseMember(Base): + """认购成员表""" + __tablename__ = "purchase_member" + + id = Column(String(36), primary_key=True, default=generate_uuid) + group_id = Column(String(36), ForeignKey("purchase_group.id"), nullable=False) + user_id = Column(String(36), nullable=False) + user_name = Column(String(50), nullable=True) + user_avatar = Column(String(255), nullable=True) + role = Column(String(20), default="member") # creator/admin/member + joined_at = Column(DateTime(timezone=True), server_default=func.now()) + +class PurchaseCollection(Base): + """认购藏品表""" + __tablename__ = "purchase_collection" + + id = Column(String(36), primary_key=True, default=generate_uuid) + group_id = Column(String(36), ForeignKey("purchase_group.id"), nullable=False) + user_id = Column(String(36), nullable=False) + user_name = Column(String(50), nullable=True) + collection_name = Column(String(100), nullable=True) # 藏品名称 + collection_code = Column(String(50), nullable=True) # 藏品编号 + number = Column(String(50), nullable=True) # 冠字号 + price = Column(DECIMAL(10,2), nullable=False) # 认购价格 + status = Column(String(20), default="pending") # pending/confirmed + submitted_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file diff --git a/backend/app/routers/purchase.py b/backend/app/routers/purchase.py new file mode 100644 index 0000000..1663f27 --- /dev/null +++ b/backend/app/routers/purchase.py @@ -0,0 +1,276 @@ +# purchase - 认购群API路由 +# Version: 0.0.1 (2026-04-24) +# 认购群、成员、藏品管理 + +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: Optional[str] = None + +class PurchaseGroupResponse(BaseModel): + id: str + name: str + description: Optional[str] + creator_id: str + creator_name: Optional[str] + status: str + total_members: int + total_collections: int + total_amount: float + avg_price: float + created_at: Optional[datetime] + +class PurchaseCollectionCreate(BaseModel): + collection_name: str + collection_code: Optional[str] = None + number: Optional[str] = None + price: float + +# ============ 认购群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) +): + """获取我参与的认购群""" + member_groups = db.query(PurchaseMember).filter( + PurchaseMember.user_id == current_user.f99_90_id + ).all() + + group_ids = [m.group_id for m in member_groups] + groups = db.query(PurchaseGroup).filter( + PurchaseGroup.id.in_(group_ids) + ).all() + + return groups + +@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="请先登录") + + group = PurchaseGroup( + name=data.name, + description=data.description, + creator_id=current_user.f99_90_id, + creator_name=current_user.f01_01_name, + status="active" + ) + 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" + ) + db.add(member) + 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: + raise HTTPException(status_code=400, detail="您已加入该认购群") + + # 加入 + 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" + ) + db.add(member) + + # 更新群成员数 + group.total_members += 1 + db.commit() + + return {"message": "加入成功"} + +@router.get("/groups/{group_id}/members") +def get_group_members( + group_id: str, + db: Session = Depends(get_db) +): + """获取认购群成员列表""" + members = db.query(PurchaseMember).filter( + PurchaseMember.group_id == group_id + ).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, + "joined_at": m.joined_at + } + for m in members + ] + +@router.get("/groups/{group_id}/collections") +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, + "user_id": c.user_id, + "user_name": c.user_name, + "collection_name": c.collection_name, + "collection_code": c.collection_code, + "number": c.number, + "price": float(c.price), + "status": c.status, + "submitted_at": c.submitted_at + } + for c in collections + ] + +@router.post("/groups/{group_id}/collections") +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="请先登录") + + # 检查群是否存在 + 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 + ).first() + + if not member: + raise HTTPException(status_code=400, detail="请先加入该认购群") + + # 添加藏品 + 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=data.collection_code, + number=data.number, + price=data.price + ) + db.add(collection) + + # 更新群统计 + group.total_collections += 1 + group.total_amount += float(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": "认购群已结束"} \ No newline at end of file diff --git a/config/VERSION.json b/config/VERSION.json index d27821e..a263464 100644 --- a/config/VERSION.json +++ b/config/VERSION.json @@ -1,18 +1,18 @@ { - "version": "0.0.8", + "version": "0.0.9", "updated": "2026-04-24", "modules": { "frontend": { - "version": "0.0.8", + "version": "0.0.9", "pages": { "Add": "0.0.1", "Admin": "0.0.1", "Detail": "0.0.1", "Edit": "0.0.1", - "Home": "0.0.1", + "Home": "0.0.4", "List": "0.0.1", "Login": "0.0.1", - "News": "0.0.3", + "News": "0.0.8", "News_YichensBoard": "0.0.1", "Settings": "0.0.1", "Stats": "0.0.1", @@ -23,7 +23,7 @@ } }, "backend": { - "version": "0.0.8", + "version": "0.0.9", "routers": { "auth": "0.0.1", "collections": "0.0.1", @@ -32,7 +32,7 @@ "news": "0.0.1", "ocr": "0.0.1", "operations": "0.0.1", - "seek": "0.0.4", + "seek": "0.0.9", "users": "0.0.1", "yichens": "0.0.1" }, @@ -44,5 +44,12 @@ "utils": "0.0.1" } } + }, + "changelog": { + "0.0.9": [ + "Home.jsx: 修复寻配号数据统计API", + "News.jsx: 寻配号功能稳定版(自有匹配+网络匹配缓存)", + "seek.py: 路由优化,留言功能迁移,缓存机制" + ] } } \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 0bc01c7..2ba9e20 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ -