diff --git a/VERSION b/VERSION index f9d405b..efe97d8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1,2 @@ -VERSION=1.2.14 +VERSION=1.2.18 +# 2026-03-27 寻号功能基础版:寻配号发布、自动配号匹配、我的寻号列表编辑 diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index 360b5bf..602723a 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -57,14 +57,10 @@ def decode_access_token(token: str) -> Optional[dict]: def get_current_user( credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), db: Session = Depends(lambda: SessionLocal()) -) -> User: - """获取当前用户""" +) -> Optional[User]: + """获取当前用户(可返回None)""" if not credentials: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="未提供认证信息", - headers={"WWW-Authenticate": "Bearer"}, - ) + return None token = credentials.credentials payload = decode_access_token(token) @@ -86,10 +82,6 @@ def get_current_user( user = db.query(User).filter(User.f99_90_id == user_id).first() if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="用户不存在", - headers={"WWW-Authenticate": "Bearer"}, - ) + return None return user diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py index fd741e0..bf48d73 100644 --- a/backend/app/routers/collections.py +++ b/backend/app/routers/collections.py @@ -105,6 +105,7 @@ def get_next_code( @router.get("") def get_collections( + id: str = Query(None, description="filter by collection id"), category: Optional[str] = None, status: Optional[str] = None, search: Optional[str] = None, @@ -143,6 +144,10 @@ def get_collections( if user_id: query = query.filter(Collection.f99_91_user_id == user_id) + # 按ID精确筛选 + if id: + query = query.filter(Collection.f99_90_id == id) + if category: query = query.filter(Collection.f01_03_category == category) if status: diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py index 1b56533..ef65024 100644 --- a/backend/app/routers/information.py +++ b/backend/app/routers/information.py @@ -69,6 +69,8 @@ class InformationResponse(BaseModel): collection_category: Optional[str] = None collection_version: Optional[str] = None collection_number: Optional[str] = None + # 匹配数量(我的藏品中满足条件的数量) + matched_count: Optional[int] = 0 class Config: from_attributes = True @@ -81,9 +83,10 @@ def get_information_list( status: str = Query("active", description="状态: active/closed/expired"), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """获取资讯列表""" + """获取资讯列表(公开,无需登录)""" query = db.query(Information).options( joinedload(Information.user), joinedload(Information.collection) @@ -102,6 +105,11 @@ def get_information_list( # 转换结果 result = [] for item in items: + # 计算匹配数量(仅对seek类型,且用户登录时) + matched_count = 0 + if item.info_type == 'seek' and item.expect_number and current_user: + matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number) + result.append(InformationResponse( id=item.id, user_id=item.user_id, @@ -127,11 +135,98 @@ def get_information_list( collection_category=item.collection.f01_03_category if item.collection else None, collection_version=item.collection.f02_11_version if item.collection else None, collection_number=item.collection.f02_10_prefix_serial if item.collection else None, + matched_count=matched_count, )) return result +def match_collections_count(db: Session, user_id: str, expect_number: str) -> int: + """根据号码特征计算匹配藏品数量""" + if not expect_number or len(expect_number) != 10: + return 0 + + # 固定前缀 + if not expect_number.startswith('J0'): + return 0 + + pattern = expect_number[2:] # 后8位 + if not pattern: + return 0 + + # 获取用户所有藏品 + collections = db.query(Collection).filter( + Collection.f99_91_user_id == user_id, + Collection.f01_04_status == "in_collection" + ).all() + + count = 0 + for c in collections: + number = c.f02_10_prefix_serial or '' + # 去掉J0前缀后取前8位 + if len(number) >= 10 and number.startswith('J0'): + col_pattern = number[2:10] + if match_pattern(col_pattern, pattern): + count += 1 + elif len(number) >= 8: + col_pattern = number[:8] + if match_pattern(col_pattern, pattern): + count += 1 + + return count + + +def match_pattern(col_number: str, pattern: str) -> bool: + """匹配号码特征模式""" + # X = 任意数字 + # A = 非4 + # B = 非47 + # C = 非347 + # D = 非247 + # E = 非2347 + # F = 非23457 + # G = 非123457 + + col_num = col_number[2:] if col_number.startswith('J0') else col_number # 去掉J0前缀 + + for i, p in enumerate(pattern): + if i >= len(col_num): + return False + + c = col_num[i] + + if p == 'X': + if not c.isdigit(): + return False + elif p == 'A': + if c == '4': + return False + elif p == 'B': + if c in '47': + return False + elif p == 'C': + if c in '347': + return False + elif p == 'D': + if c in '247': + return False + elif p == 'E': + if c in '2347': + return False + elif p == 'F': + if c in '23457': + return False + elif p == 'G': + if c in '123457': + return False + else: + # 数字或字母必须完全匹配 + if p != c: + return False + + return True + + # 获取单条资讯 @router.get("/{info_id}", response_model=InformationResponse) def get_information( @@ -333,10 +428,13 @@ def delete_information( @router.get("/seek/match") def get_seek_match( info_id: str, - current_user: User = Depends(get_current_user), + current_user: Optional[User] = Depends(get_current_user), db: Session = Depends(get_db) ): """获取符合条件的我的藏品推荐""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + info = db.query(Information).filter( Information.id == info_id, Information.info_type == "seek" @@ -345,45 +443,108 @@ def get_seek_match( if not info: raise HTTPException(status_code=404, detail="寻配号信息不存在") - # 查询当前用户的藏品,匹配条件 - query = db.query(Collection).filter( + # 获取用户所有藏品 + collections = db.query(Collection).filter( Collection.f99_91_user_id == current_user.f99_90_id, Collection.f01_04_status == "in_collection" - ) + ).all() - if info.expect_category: - query = query.filter(Collection.f01_03_category == info.expect_category) - if info.expect_version: - query = query.filter(Collection.f02_11_version == info.expect_version) - if info.expect_packaging: - query = query.filter(Collection.f02_12_packaging == info.expect_packaging) - if info.expect_number: - query = query.filter(Collection.f02_10_prefix_serial.contains(info.expect_number)) - if info.expect_price_min: - query = query.filter(Collection.f05_40_cost_price >= info.expect_price_min) - if info.expect_price_max: - query = query.filter(Collection.f05_40_cost_price <= info.expect_price_max) + # 去掉版别筛选,因为藏品分类和发布需求的版别不同 + # if info.expect_category: + # collections = [c for c in collections if c.f01_03_category == info.expect_category] - matched_collections = query.all() + # 按号码特征模式匹配 + matched = [] + if info.expect_number and len(info.expect_number) == 10: + pattern = info.expect_number[2:] # 后8位 + for c in collections: + number = c.f02_10_prefix_serial or '' + # 去掉J0前缀后取前8位 + if len(number) >= 10 and number.startswith('J0'): + col_pattern = number[2:10] # 取J0后面的8位 + if match_pattern(col_pattern, pattern): + matched.append(c) + elif len(number) >= 8: + col_pattern = number[:8] # 取前8位 + if match_pattern(col_pattern, pattern): + matched.append(c) + else: + matched = collections return { "info_id": info_id, - "matched_count": len(matched_collections), + "matched_count": len(matched), "collections": [ { "id": c.f99_90_id, + "code": c.f01_02_code or '', "name": c.f01_01_name, + "number": c.f02_10_prefix_serial, + "status": c.f01_04_status, "category": c.f01_03_category, "version": c.f02_11_version, "packaging": c.f02_12_packaging, - "number": c.f02_10_prefix_serial, "cost_price": c.f05_40_cost_price, } - for c in matched_collections + for c in matched ] } +# 我的寻号列表 +@router.get("/my-seeks") +def get_my_seeks( + current_user: Optional[User] = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取当前用户发布的所有寻号信息""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + items = db.query(Information).filter( + Information.user_id == current_user.f99_90_id, + Information.info_type == "seek", + Information.status == "active" + ).order_by(Information.created_at.desc()).all() + + result = [] + for item in items: + # 计算匹配数量 + matched_count = 0 + if item.expect_number: + matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number) + + result.append(InformationResponse( + id=item.id, + user_id=item.user_id, + info_type=item.info_type, + title=item.title, + content=item.content, + collection_id=item.collection_id, + expect_category=item.expect_category, + expect_version=item.expect_version, + expect_packaging=item.expect_packaging, + expect_number=item.expect_number, + expect_price_min=item.expect_price_min, + expect_price_max=item.expect_price_max, + deal_price=item.deal_price, + deal_date=item.deal_date, + status=item.status, + view_count=item.view_count, + contact_count=item.contact_count, + created_at=item.created_at, + user_name=item.user.f01_01_name if item.user else None, + user_avatar=item.user.avatar if item.user else None, + collection_name=item.collection.f01_01_name if item.collection else None, + collection_category=item.collection.f01_03_category if item.collection else None, + collection_version=item.collection.f02_11_version if item.collection else None, + collection_number=item.collection.f02_10_prefix_serial if item.collection else None, + matched_count=matched_count, + )) + + return result + + # 成交数据统计 @router.get("/deal/stats") def get_deal_stats( diff --git a/frontend/index.html b/frontend/index.html index 7cebb1a..9190919 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ -