Compare commits

..

No commits in common. "new" and "main" have entirely different histories.
new ... main

49 changed files with 4281 additions and 6018 deletions

View File

@ -6,9 +6,9 @@
| 项目 | 内容 | | 项目 | 内容 |
|------|------| |------|------|
| **版本** | v0.0.7 | | **版本** | v1.2.38 |
| **代号** | 寻配号重构版本 | | **代号** | 最终优化版本 |
| **发布日期** | 2026-04-24 | | **发布日期** | 2026-03-31 |
## 技术栈 ## 技术栈

View File

@ -1 +0,0 @@
1.2.98

View File

@ -1,48 +0,0 @@
{
"version": "1.2.98",
"updated": "2026-04-19",
"modules": {
"frontend": {
"version": "1.2.98",
"pages": {
"Add": "1.2.90",
"Admin": "1.2.98",
"Detail": "1.2.90",
"Edit": "1.2.90",
"Home": "1.2.95",
"List": "1.2.93",
"Login": "1.2.85",
"News": "1.2.80",
"News_YichensBoard": "1.2.75",
"Settings": "1.2.75",
"Stats": "1.2.70",
"YichensBoard": "1.2.70"
},
"config": {
"version": "1.2.98"
}
},
"backend": {
"version": "1.2.98",
"routers": {
"auth": "1.2.90",
"collections": "1.2.95",
"deal": "1.2.85",
"information": "1.2.92",
"news": "1.2.80",
"ocr": "1.2.75",
"operations": "1.2.70",
"seek": "1.2.70",
"users": "1.2.98",
"yichens": "1.2.70"
},
"app": {
"core": "1.2.0",
"models": "1.2.0",
"schemas": "1.2.0",
"services": "1.2.0",
"utils": "1.2.0"
}
}
}
}

View File

@ -1 +1 @@
VERSION=v0.2.1 1.2.98

View File

@ -59,14 +59,10 @@ def decode_access_token(token: str) -> Optional[dict]:
def get_current_user( def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
db: Session = Depends(lambda: SessionLocal()) db: Session = Depends(lambda: SessionLocal())
) -> User: ) -> Optional[User]:
"""获取当前用户""" """获取当前用户可返回None"""
if not credentials: if not credentials:
raise HTTPException( return None
status_code=status.HTTP_401_UNAUTHORIZED,
detail="未登录",
headers={"WWW-Authenticate": "Bearer"},
)
token = credentials.credentials token = credentials.credentials
payload = decode_access_token(token) payload = decode_access_token(token)
@ -88,10 +84,6 @@ def get_current_user(
user = db.query(User).filter(User.f99_90_id == user_id).first() user = db.query(User).filter(User.f99_90_id == user_id).first()
if not user: if not user:
raise HTTPException( return None
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在",
headers={"WWW-Authenticate": "Bearer"},
)
return user return user

View File

@ -19,7 +19,6 @@ from app.routers import information as information_router
from app.routers import yichens as yichens_router from app.routers import yichens as yichens_router
from app.routers import seek as seek_router from app.routers import seek as seek_router
from app.routers import deal as deal_router from app.routers import deal as deal_router
from app.routers import purchase as purchase_router
# 版本信息 - 从 config/VERSION 文件读取 # 版本信息 - 从 config/VERSION 文件读取
def get_version(): def get_version():
@ -89,7 +88,6 @@ app.include_router(information_router.router)
app.include_router(yichens_router.router) # 一尘看板 app.include_router(yichens_router.router) # 一尘看板
app.include_router(seek_router.router) # 寻配号 app.include_router(seek_router.router) # 寻配号
app.include_router(deal_router.router) # 成交行情 app.include_router(deal_router.router) # 成交行情
app.include_router(purchase_router.router) # 认购群
@app.get("/") @app.get("/")

View File

@ -1,74 +0,0 @@
# purchase - 认购群模型
# Version: 0.0.3 (2026-05-02)
# 认购群、成员、藏品管理
from sqlalchemy import Column, String, Text, Integer, DECIMAL, DateTime, ForeignKey, Boolean
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)
group_code = Column(String(20), nullable=False, unique=True) # 年+序号,如 26001
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="pending") # pending/active/closed
approved_by = Column(String(36), nullable=True) # 审批管理员ID
approved_at = Column(DateTime(timezone=True), nullable=True) # 审批时间
total_members = Column(Integer, default=0) # 总参与人数
total_pending = Column(Integer, default=0) # 待审批人数
total_collections = Column(Integer, default=0) # 总藏品数
total_confirmed = Column(Integer, default=0) # 已确认数
total_amount = Column(DECIMAL(12,2), default=0) # 总金额
cycle_months = Column(Integer, nullable=True) # 认购周期(月)
start_date = Column(DateTime(timezone=True), nullable=True) # 开始日期
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
status = Column(String(20), default="pending") # pending/approved/rejected
# 成员扩展信息
wechat_name = Column(String(50), nullable=True) # 微信名
contact = Column(String(50), nullable=True) # 联系方式
purchase_count = Column(Integer, default=0) # 认购数量
paid_amount = Column(DECIMAL(12,2), default=0) # 已付认购款
notes = Column(Text, nullable=True) # 备注
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) # 藏品编号 (自动生成001开始)
number = Column(String(50), nullable=False) # 冠字号 (必填)
grading = Column(String(50), nullable=False) # 评级 (必填)
boss_name = Column(String(50), nullable=False) # 认购老板 (必填,从成员中选择)
price = Column(DECIMAL(10,2), nullable=False) # 认购价格 (必填)
source = Column(String(20), nullable=False) # 来源: yichen/live/friend/other (必填)
paid = Column(Boolean, default=False) # 认购款是否结清
submit_date = Column(DateTime(timezone=True), server_default=func.now()) # 提交日期
status = Column(String(20), default="pending") # pending/confirmed
submitted_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@ -24,12 +24,6 @@ class SeekInfo(Base):
expect_price_min = Column(Float, nullable=True) # 期望最低价 expect_price_min = Column(Float, nullable=True) # 期望最低价
expect_price_max = Column(Float, nullable=True) # 期望最高价 expect_price_max = Column(Float, nullable=True) # 期望最高价
# 评级字段
number = Column(String(50), nullable=True) # 号码
is_graded = Column(Boolean, default=False) # 是否评级
grading_company = Column(String(100), nullable=True) # 评级公司
grading_score = Column(String(20), nullable=True) # 评级分数
# 匹配状态 # 匹配状态
status = Column(String(20), default="active") # active/closed/expired status = Column(String(20), default="active") # active/closed/expired
is_matched = Column(String(10), default="false") # 是否已匹配 is_matched = Column(String(10), default="false") # 是否已匹配
@ -39,7 +33,6 @@ class SeekInfo(Base):
# 统计 # 统计
view_count = Column(Integer, default=0) view_count = Column(Integer, default=0)
contact_count = Column(Integer, default=0) contact_count = Column(Integer, default=0)
network_matched_count = Column(Integer, default=0) # 网络匹配数量(缓存)
# 时间 # 时间
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@ -1,5 +1,5 @@
# auth - 认证路由 # auth - 认证路由
# Version: 0.0.1 # Version: 1.2.90
# 更新: # 更新:
from fastapi import APIRouter from fastapi import APIRouter
@ -91,8 +91,22 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)):
# 更新: # 更新:
# 更新: # 更新:
# 检查手机号是否已存在移除2026-06-13 # 检查手机号是否已存在
# 更新: # 更新:
if user_data.phone:
# 更新:
existing_phone = db.query(User).filter(User.phone == user_data.phone).first()
# 更新:
if existing_phone:
# 更新:
raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="E00040:该手机号已被注册,请更换手机号"
# 更新:
)
# 更新:
if user_data.email: if user_data.email:
# 更新: # 更新:
existing_email = db.query(User).filter(User.email == user_data.email).first() existing_email = db.query(User).filter(User.email == user_data.email).first()

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
# deal - 成交行情路由 # deal - 成交行情路由
# Version: 0.0.1 # Version: 1.2.85
# 更新: # 更新:
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
# 更新: # 更新:
from pydantic import BaseModel from pydantic import BaseModel
# 更新: # 更新:
from typing import Optional, List from typing import Optional
# 更新: # 更新:
from datetime import datetime, date from datetime import datetime, date
# 更新: # 更新:
@ -176,7 +176,7 @@ def generate_deal_no(db: Session):
# 更新: # 更新:
# ============ API ============ # ============ API ============
# 更新: # 更新:
@router.get("/list", response_model=List[DealInfoResponse]) @router.get("/list", response_model=list[DealInfoResponse])
# 更新: # 更新:
def get_deal_list( def get_deal_list(
# 更新: # 更新:

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
# news - 新闻路由 # news - 新闻路由
# Version: 0.0.1 # Version: 1.2.80
# 更新: # 更新:
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query

View File

@ -1,5 +1,5 @@
# ocr - OCR识别路由 # ocr - OCR识别路由
# Version: 0.0.1 # Version: 1.2.75
# 更新: # 更新:
import os import os

View File

@ -1,104 +1,210 @@
# 操作路由 # operations - 运营操作路由
# Version: 1.2.70
# 更新:
from typing import List, Optional from typing import List, Optional
# 更新:
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from app.core.database import get_db from app.core.database import get_db
# 更新:
from app.core.auth import get_current_user from app.core.auth import get_current_user
# 更新:
from app.models.models import User, Collection, Operation from app.models.models import User, Collection, Operation
# 更新:
from app.schemas.schemas import OperationCreate, OperationResponse from app.schemas.schemas import OperationCreate, OperationResponse
# 更新:
# 更新:
router = APIRouter(prefix="/api", tags=["操作"]) router = APIRouter(prefix="/api", tags=["操作"])
# 更新:
# 更新:
# 更新:
@router.get("/operations", response_model=List[OperationResponse]) @router.get("/operations", response_model=List[OperationResponse])
# 更新:
def get_operations( def get_operations(
# 更新:
collection_id: Optional[str] = None, collection_id: Optional[str] = None,
# 更新:
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
# 更新:
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
# 更新:
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取操作历史""" """获取操作历史"""
# 更新:
query = db.query(Operation).filter(Operation.user_id == current_user.id) query = db.query(Operation).filter(Operation.user_id == current_user.id)
# 更新:
# 更新:
if collection_id: if collection_id:
# 更新:
query = query.filter(Operation.collection_id == collection_id) query = query.filter(Operation.collection_id == collection_id)
# 更新:
# 更新:
operations = query.order_by(Operation.created_at.desc()) \ operations = query.order_by(Operation.created_at.desc()) \
# 更新:
.offset((page - 1) * limit) \ .offset((page - 1) * limit) \
# 更新:
.limit(limit) \ .limit(limit) \
# 更新:
.all() .all()
# 更新:
# 更新:
return operations return operations
# 更新:
# 更新:
# 更新:
@router.get("/operations/history") @router.get("/operations/history")
# 更新:
def get_operation_history( def get_operation_history(
# 更新:
collection_id: Optional[str] = None, collection_id: Optional[str] = None,
# 更新:
type: Optional[str] = None, type: Optional[str] = None,
# 更新:
start_date: Optional[str] = None, start_date: Optional[str] = None,
# 更新:
end_date: Optional[str] = None, end_date: Optional[str] = None,
# 更新:
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
# 更新:
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
# 更新:
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取操作历史(带统计)""" """获取操作历史(带统计)"""
# 更新:
query = db.query(Operation).filter(Operation.user_id == current_user.id) query = db.query(Operation).filter(Operation.user_id == current_user.id)
# 更新:
# 更新:
if collection_id: if collection_id:
# 更新:
query = query.filter(Operation.collection_id == collection_id) query = query.filter(Operation.collection_id == collection_id)
# 更新:
if type: if type:
# 更新:
query = query.filter(Operation.type == type) query = query.filter(Operation.type == type)
# 更新:
if start_date: if start_date:
# 更新:
query = query.filter(Operation.created_at >= start_date) query = query.filter(Operation.created_at >= start_date)
# 更新:
if end_date: if end_date:
# 更新:
query = query.filter(Operation.created_at <= end_date) query = query.filter(Operation.created_at <= end_date)
# 更新:
# 更新:
total = query.count() total = query.count()
# 更新:
# 更新:
data = query.order_by(Operation.created_at.desc()) \ data = query.order_by(Operation.created_at.desc()) \
# 更新:
.offset((page - 1) * limit) \ .offset((page - 1) * limit) \
# 更新:
.limit(limit) \ .limit(limit) \
# 更新:
.all() .all()
# 更新:
# 更新:
return { return {
# 更新:
"data": data, "data": data,
# 更新:
"pagination": { "pagination": {
# 更新:
"page": page, "page": page,
# 更新:
"limit": limit, "limit": limit,
# 更新:
"total": total, "total": total,
# 更新:
"pages": (total + limit - 1) // limit "pages": (total + limit - 1) // limit
# 更新:
} }
# 更新:
} }
# 更新:
# 更新:
# 更新:
@router.post("/operations", response_model=OperationResponse) @router.post("/operations", response_model=OperationResponse)
# 更新:
def create_operation( def create_operation(
# 更新:
operation_data: OperationCreate, operation_data: OperationCreate,
# 更新:
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""创建操作记录""" """创建操作记录"""
# 更新:
# 验证藏品存在 # 验证藏品存在
# 更新:
collection = db.query(Collection).filter( collection = db.query(Collection).filter(
# 更新:
Collection.id == operation_data.collection_id, Collection.id == operation_data.collection_id,
# 更新:
Collection.user_id == current_user.id Collection.user_id == current_user.id
# 更新:
).first() ).first()
# 更新:
# 更新:
if not collection: if not collection:
# 更新:
raise HTTPException(status_code=404, detail="藏品不存在") raise HTTPException(status_code=404, detail="藏品不存在")
# 更新:
# 更新:
operation = Operation( operation = Operation(
# 更新:
collection_id=operation_data.collection_id, collection_id=operation_data.collection_id,
# 更新:
user_id=current_user.id, user_id=current_user.id,
# 更新:
type=operation_data.type, type=operation_data.type,
# 更新:
price=operation_data.price, price=operation_data.price,
# 更新:
note=operation_data.note note=operation_data.note
# 更新:
) )
# 更新:
# 更新:
db.add(operation) db.add(operation)
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(operation) db.refresh(operation)
# 更新:
# 更新:
return operation return operation
# 更新:

View File

@ -1,759 +0,0 @@
# 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

View File

@ -1,516 +1,384 @@
# seek - 寻路由 # seek - 寻号匹配路由
# Version: 0.2.0 (2026-05-11) # Version: 1.2.70
# 稳定版支持评级字段number, is_graded, grading_company, grading_score # 更新:
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List # 更新:
from typing import Optional
# 更新:
from datetime import datetime from datetime import datetime
# 更新:
from app.core.database import get_db from app.core.database import get_db
# 更新:
from app.core.auth import get_current_user from app.core.auth import get_current_user
# 更新:
from app.models.seek_info import SeekInfo from app.models.seek_info import SeekInfo
from app.models.models import User, Collection # 更新:
from app.routers.information import match_collections_count, match_collections_count_from_coolbot, match_pattern
from app.utils.coolbot_matcher import (
match_self_collections_count,
match_collections_count_from_coolbot,
match_collections_list_from_coolbot
)
# 更新:
router = APIRouter(prefix="/api/seek", tags=["寻配号"]) router = APIRouter(prefix="/api/seek", tags=["寻配号"])
# 更新:
# 更新:
# ============ Schema ============ # ============ Schema ============
# 更新:
class SeekInfoCreate(BaseModel): class SeekInfoCreate(BaseModel):
# 更新:
title: str title: str
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 评级字段 # 更新:
number: Optional[str] = None
is_graded: Optional[bool] = False
grading_company: Optional[str] = None
grading_score: Optional[str] = None
# 更新:
class SeekInfoUpdate(BaseModel): class SeekInfoUpdate(BaseModel):
# 更新:
title: Optional[str] = None title: Optional[str] = None
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 更新:
status: Optional[str] = None status: Optional[str] = None
# 评级字段 # 更新:
number: Optional[str] = None
is_graded: Optional[bool] = False
grading_company: Optional[str] = None
grading_score: Optional[str] = None
# 更新:
class SeekInfoResponse(BaseModel): class SeekInfoResponse(BaseModel):
# 更新:
id: str id: str
# 更新:
user_id: str user_id: str
# 更新:
title: str title: str
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
expect_category: Optional[str] expect_category: Optional[str]
# 更新:
expect_version: Optional[str] expect_version: Optional[str]
# 更新:
expect_packaging: Optional[str] expect_packaging: Optional[str]
# 更新:
expect_number: Optional[str] expect_number: Optional[str]
# 更新:
expect_price_min: Optional[float] expect_price_min: Optional[float]
# 更新:
expect_price_max: Optional[float] expect_price_max: Optional[float]
# 更新:
status: str status: str
# 更新:
is_matched: Optional[str] is_matched: Optional[str]
# 更新:
matched_user_id: Optional[str] matched_user_id: Optional[str]
# 更新:
matched_contact: Optional[str] matched_contact: Optional[str]
# 更新:
view_count: int view_count: int
# 更新:
contact_count: int contact_count: int
# 更新:
created_at: Optional[datetime] created_at: Optional[datetime]
# 更新:
updated_at: Optional[datetime] updated_at: Optional[datetime]
user_name: Optional[str] = None # 发布者用户名 # 更新:
matched_count: Optional[int] = 0 # 自有匹配数量
network_matched_count: Optional[int] = 0 # 网络匹配数量
# 评级字段
number: Optional[str] = None
is_graded: Optional[bool] = False
grading_company: Optional[str] = None
grading_score: Optional[str] = None
# 更新:
class Config: class Config:
# 更新:
from_attributes = True from_attributes = True
# 更新:
class MatchConfirmRequest(BaseModel): # 更新:
info_id: str
contact: Optional[str] = None
collection_id: Optional[str] = None
# ============ API ============ # ============ API ============
@router.get("/list", response_model=List[SeekInfoResponse]) # 更新:
@router.get("/list", response_model=list[SeekInfoResponse])
# 更新:
def get_seek_list( def get_seek_list(
# 更新:
status: str = Query("active"), status: str = Query("active"),
# 更新:
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
# 更新:
page_size: int = Query(20, ge=1, le=1000), page_size: int = Query(20, ge=1, le=1000),
# 更新:
user_only: bool = Query(False), user_only: bool = Query(False),
# 更新:
current_user: Optional = Depends(get_current_user), current_user: Optional = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号列表""" """获取寻配号列表"""
# 更新:
query = db.query(SeekInfo).filter(SeekInfo.status == status) query = db.query(SeekInfo).filter(SeekInfo.status == status)
# 更新:
# 更新:
# 我的寻配号:只查看自己的 # 我的寻配号:只查看自己的
# 更新:
if user_only and current_user: if user_only and current_user:
# 更新:
query = query.filter(SeekInfo.user_id == current_user.f99_90_id) query = query.filter(SeekInfo.user_id == current_user.f99_90_id)
# 更新:
# 更新:
# 排序 # 排序
# 更新:
query = query.order_by(SeekInfo.created_at.desc()) query = query.order_by(SeekInfo.created_at.desc())
# 更新:
# 更新:
# 分页 # 分页
# 更新:
offset = (page - 1) * page_size offset = (page - 1) * page_size
# 更新:
items = query.offset(offset).limit(page_size).all() items = query.offset(offset).limit(page_size).all()
# 更新:
result = [] # 更新:
for item in items: return items
user = db.query(User).filter(User.f99_90_id == item.user_id).first() # 更新:
user_name = user.f01_01_name if user else '匿名用户'
# 构建响应(不实时计算网络匹配,网络匹配在点击时再查)
# 网络匹配数量从数据库读取不再实时计算初始为0
network_matched_count = 0
if hasattr(item, 'network_matched_count') and item.network_matched_count:
network_matched_count = item.network_matched_count
# 自有匹配数量使用缓存方式如为0则计算一次
matched_count = 0
if item.expect_number and len(item.expect_number) == 10:
if current_user and current_user.f99_90_id:
matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number)
result.append(SeekInfoResponse(
id=item.id,
user_id=item.user_id,
title=item.title,
content=item.content,
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,
status=item.status,
is_matched=item.is_matched,
matched_user_id=item.matched_user_id,
matched_contact=item.matched_contact,
view_count=item.view_count,
contact_count=item.contact_count,
created_at=item.created_at,
updated_at=item.updated_at,
user_name=user_name,
matched_count=matched_count,
network_matched_count=network_matched_count
))
return result
# 更新:
@router.get("/stats") @router.get("/stats")
# 更新:
def get_seek_stats( def get_seek_stats(
# 更新:
current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
"""获取寻配号统计(公开)""" # 更新:
"""获取寻配号统计"""
# 更新:
total = db.query(SeekInfo).filter(SeekInfo.status == "active").count() total = db.query(SeekInfo).filter(SeekInfo.status == "active").count()
# 更新:
matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count() matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count()
# 更新:
# 更新:
return { return {
# 更新:
"total": total, "total": total,
# 更新:
"matched": matched, "matched": matched,
# 更新:
"unmatched": total - matched "unmatched": total - matched
# 更新:
} }
# 更新:
# 更新:
@router.post("", response_model=SeekInfoResponse) @router.post("", response_model=SeekInfoResponse)
# 更新:
def create_seek( def create_seek(
# 更新:
data: SeekInfoCreate, data: SeekInfoCreate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""创建寻配号""" """创建寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 计算网络匹配数量(创建时初始化) # 更新:
network_matched_count = 0
if data.expect_number and len(data.expect_number) == 10:
network_matched_count = match_collections_count_from_coolbot(data.expect_number)
seek = SeekInfo( seek = SeekInfo(
# 更新:
user_id=current_user.f99_90_id, user_id=current_user.f99_90_id,
# 更新:
title=data.title, title=data.title,
# 更新:
content=data.content, content=data.content,
# 更新:
expect_category=data.expect_category, expect_category=data.expect_category,
# 更新:
expect_version=data.expect_version, expect_version=data.expect_version,
# 更新:
expect_packaging=data.expect_packaging, expect_packaging=data.expect_packaging,
# 更新:
expect_number=data.expect_number, expect_number=data.expect_number,
# 更新:
expect_price_min=data.expect_price_min, expect_price_min=data.expect_price_min,
# 更新:
expect_price_max=data.expect_price_max, expect_price_max=data.expect_price_max,
status="active", # 更新:
view_count=0, status="active"
contact_count=0, # 更新:
network_matched_count=network_matched_count, # 保存初始化值
# 评级字段
number=data.number,
is_graded=data.is_graded,
grading_company=data.grading_company,
grading_score=data.grading_score
) )
# 更新:
db.add(seek) db.add(seek)
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# ============ 匹配相关API ============ # 更新:
@router.get("/my-match")
def get_seek_match(
info_id: str,
current_user = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取符合条件的我的藏品推荐"""
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
info = db.query(SeekInfo).filter(SeekInfo.id == info_id).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 获取用户所有藏品
collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id,
Collection.f01_04_status == "in_collection"
).all()
# 按号码特征模式匹配
matched = []
if info.expect_number and len(info.expect_number) == 10:
pattern = info.expect_number[2:]
for c in collections:
number = c.f02_10_prefix_serial or ''
if len(number) >= 10 and number.startswith('J0'):
col_pattern = number[2:10]
if match_pattern(col_pattern, pattern):
matched.append(c)
elif len(number) >= 8:
col_pattern = number[:8]
if match_pattern(col_pattern, pattern):
matched.append(c)
else:
matched = collections
return {
"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}
for c in matched[:20]
]
}
@router.get("/network-match/{info_id}")
def get_network_match(
info_id: str,
limit: int = Query(100, ge=1, le=500),
db: Session = Depends(get_db)
):
"""获取一尘数据库中匹配的藏品列表点击时更新network_matched_count"""
info = db.query(SeekInfo).filter(SeekInfo.id == info_id).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
if not info.expect_number:
return {"matched_count": 0, "collections": []}
# 获取匹配列表
matched = match_collections_list_from_coolbot(info.expect_number, limit=limit)
actual_count = len(matched)
# 点击查看时更新network_matched_count
info.network_matched_count = actual_count
db.commit()
return {"matched_count": actual_count, "collections": matched}
@router.post("/match-confirm")
def match_seek_confirm(
request: MatchConfirmRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""确认匹配寻号 - 用户愿意交换联系方式给发布者"""
info = db.query(SeekInfo).filter(
SeekInfo.id == request.info_id,
SeekInfo.status == "active"
).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 检查是否已被匹配(一个寻号只能被一个用户匹配)
if info.is_matched == "matched":
raise HTTPException(status_code=400, detail="该寻号已被其他用户匹配")
# 检查是否是自己发布的
if info.user_id == current_user.f99_90_id:
raise HTTPException(status_code=400, detail="不能匹配自己发布的寻号")
# 更新匹配状态
info.is_matched = "matched"
info.matched_user_id = current_user.f99_90_id
info.matched_contact = request.contact or ''
# 更新发布寻号者的内容,显示有藏品被匹配
original_content = info.content or ""
match_info = f"\n藏品被{current_user.f01_01_name}匹配成功!联系方式:{request.collection_id or '已交换'}"
info.content = original_content + match_info
db.commit()
return {"message": "匹配成功,已通知发布者", "is_matched": "matched"}
@router.get("/matched-user/{info_id}")
def get_matched_user(
info_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取寻号的匹配者信息(仅发布者可见)"""
info = db.query(SeekInfo).filter(SeekInfo.id == info_id).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 只有发布者可以看到匹配者信息
if info.user_id != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="无权访问")
if not info.matched_user_id:
return {"message": "暂无匹配者"}
# 获取匹配者信息
matched_user = db.query(User).filter(User.f99_90_id == info.matched_user_id).first()
if not matched_user:
return {"message": "匹配者不存在"}
return {
"matched_user_id": info.matched_user_id,
"user_name": matched_user.f01_01_name,
"phone": matched_user.phone,
"matched_contact": info.matched_contact,
}
@router.get("/publisher/{info_id}")
def get_publisher_info(
info_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取寻号的发布者信息(仅匹配者可见)"""
info = db.query(SeekInfo).filter(SeekInfo.id == info_id).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 只有匹配者可以看到发布者信息
if not info.matched_user_id or info.matched_user_id != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="无权访问")
# 获取发布者信息
publisher = db.query(User).filter(User.f99_90_id == info.user_id).first()
if not publisher:
return {"message": "发布者不存在"}
# 从content中解析联系方式
contact = ''
if info.content:
import re
match = re.search(r'联系方式[:\s]*(.+?)(?:\n|$)', info.content)
if match:
contact = match.group(1).strip()
return {
"user_id": info.user_id,
"user_name": publisher.f01_01_name,
"phone": publisher.phone,
"contact": contact,
}
# ============ 通配路由(必须放最后,避免匹配冲突)============
@router.get("/{seek_id}", response_model=SeekInfoResponse) @router.get("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def get_seek( def get_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号详情""" """获取寻配号详情"""
# 更新:
seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first()
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
# 增加浏览数
# 更新:
seek.view_count += 1 seek.view_count += 1
# 更新:
db.commit() db.commit()
return seek # 更新:
# 更新:
return seek
# 更新:
# 更新:
@router.put("/{seek_id}", response_model=SeekInfoResponse) @router.put("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def update_seek( def update_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
data: SeekInfoUpdate, data: SeekInfoUpdate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""更新寻配号""" """更新寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() # 更新:
# 更新:
seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
if seek.user_id != current_user.f99_90_id: # 更新:
raise HTTPException(status_code=403, detail="无权限")
if data.title is not None: # 更新:
seek.title = data.title for key, value in data.model_dump(exclude_unset=True).items():
if data.content is not None: # 更新:
seek.content = data.content setattr(seek, key, value)
if data.expect_category is not None: # 更新:
seek.expect_category = data.expect_category
if data.expect_version is not None: # 更新:
seek.expect_version = data.expect_version
if data.expect_packaging is not None:
seek.expect_packaging = data.expect_packaging
if data.status is not None:
seek.status = data.status
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# 更新:
@router.delete("/{seek_id}") @router.delete("/{seek_id}")
# 更新:
def delete_seek( def delete_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""删除寻配号""" """删除寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() # 更新:
# 更新:
seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
if seek.user_id != current_user.f99_90_id: # 更新:
raise HTTPException(status_code=403, detail="无权限")
# 更新:
seek.status = "deleted" seek.status = "deleted"
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
return {"message": "删除成功"} return {"message": "删除成功"}
# 更新:
# ============ 留言功能从information.py迁移============
from pydantic import BaseModel
class CommentRequest(BaseModel):
information_id: str
content: str
@router.post("/comment")
def add_comment(
request: CommentRequest,
current_user = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""添加留言 - 支持seek_info表"""
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
# 查询seek_info表
info = db.query(SeekInfo).filter(SeekInfo.id == request.information_id).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号不存在")
# 创建留言
from app.models.models import InformationComment
comment = InformationComment(
information_id=request.information_id,
user_id=current_user.f99_90_id,
content=request.content
)
db.add(comment)
db.commit()
return {
"message": "留言成功",
"comment": {
"id": comment.id,
"content": comment.content,
"user_name": current_user.f01_01_name,
"user_avatar": current_user.avatar,
"created_at": comment.created_at
}
}
@router.get("/comments/{information_id}")
def get_comments(
information_id: str,
db: Session = Depends(get_db)
):
"""获取寻配号的评论列表"""
from app.models.models import InformationComment
comments = db.query(InformationComment).filter(
InformationComment.information_id == information_id
).order_by(InformationComment.created_at.desc()).all()
return [
{
"id": c.id,
"content": c.content,
"user_name": c.user.f01_01_name if c.user else '匿名用户',
"user_avatar": c.user.avatar if c.user else None,
"created_at": c.created_at
}
for c in comments
]

View File

@ -1,5 +1,5 @@
# users.py - 用户管理路由 # users.py - 用户管理路由
# Version: 0.0.1 (2026-04-19) # Version: 1.2.98 (2026-04-19)
# 更新:新增 dealCount 字段,从 Information 表统计用户发布的行情数量 # 更新:新增 dealCount 字段,从 Information 表统计用户发布的行情数量
from typing import Optional from typing import Optional
@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
from app.core.auth import get_current_user from app.core.auth import get_current_user
from app.models.models import User, Collection, Information from app.models.models import User, Collection
from app.schemas.schemas import UserResponse, UserUpdate from app.schemas.schemas import UserResponse, UserUpdate
router = APIRouter(prefix="/api", tags=["用户"]) router = APIRouter(prefix="/api", tags=["用户"])

View File

@ -1,377 +1,760 @@
# yichens - 一尘数据路由
# Version: 1.2.70
# 更新:
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy import func, text from sqlalchemy import func, text
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel from pydantic import BaseModel
# 更新:
from typing import Optional, List from typing import Optional, List
# 更新:
from datetime import datetime, date from datetime import datetime, date
# 更新:
from app.core.coolbot_db import get_coolbot_db from app.core.coolbot_db import get_coolbot_db
# 更新:
# 更新:
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"]) router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
# 更新:
# 更新:
# ============ 数据模型 ============ # ============ 数据模型 ============
# 更新:
class YichensPostStats(BaseModel): class YichensPostStats(BaseModel):
# 更新:
total_posts: int total_posts: int
# 更新:
total_deals: int # 出售 total_deals: int # 出售
# 更新:
total_wants: int # 求购 total_wants: int # 求购
# 更新:
total_replies: int total_replies: int
# 更新:
total_views: int total_views: int
# 更新:
avg_price: Optional[float] avg_price: Optional[float]
# 更新:
# 更新:
class CategoryStat(BaseModel): class CategoryStat(BaseModel):
# 更新:
category: str category: str
# 更新:
count: int count: int
# 更新:
# 更新:
class PostItem(BaseModel): class PostItem(BaseModel):
# 更新:
post_id: str post_id: str
# 更新:
title: str title: str
# 更新:
category: Optional[str] category: Optional[str]
# 更新:
post_type: str post_type: str
# 更新:
price: Optional[float] price: Optional[float]
# 更新:
author_username: str author_username: str
# 更新:
post_time: str post_time: str
# 更新:
reply_count: int reply_count: int
# 更新:
view_count: int view_count: int
# 更新:
url: Optional[str] url: Optional[str]
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
# 更新:
class UserStat(BaseModel): class UserStat(BaseModel):
# 更新:
total_users: int total_users: int
# 更新:
new_users_today: int new_users_today: int
# 更新:
sellers: int sellers: int
# 更新:
# 更新:
class UserItem(BaseModel): class UserItem(BaseModel):
# 更新:
user_id: str user_id: str
# 更新:
username: str username: str
# 更新:
avatar_url: Optional[str] avatar_url: Optional[str]
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
credit_level: Optional[str] credit_level: Optional[str]
# 更新:
credit_score: Optional[int] credit_score: Optional[int]
# 更新:
post_count: int post_count: int
# 更新:
is_seller: bool is_seller: bool
# 更新:
registration_date: Optional[str] registration_date: Optional[str]
# 更新:
# 更新:
# ============ 统计接口 ============ # ============ 统计接口 ============
# 更新:
@router.get("/stats/posts", response_model=YichensPostStats) @router.get("/stats/posts", response_model=YichensPostStats)
# 更新:
def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)): def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取帖子统计""" """获取帖子统计"""
# 更新:
result = db.execute(text(""" result = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total_posts, COUNT(*) as total_posts,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals, COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants, COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
# 更新:
COALESCE(SUM(reply_count), 0) as total_replies, COALESCE(SUM(reply_count), 0) as total_replies,
# 更新:
COALESCE(SUM(view_count), 0) as total_views, COALESCE(SUM(view_count), 0) as total_views,
# 更新:
AVG(price) as avg_price AVG(price) as avg_price
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
"""), {"days": days}).fetchone() """), {"days": days}).fetchone()
# 更新:
# 更新:
return YichensPostStats( return YichensPostStats(
# 更新:
total_posts=result[0] or 0, total_posts=result[0] or 0,
# 更新:
total_deals=result[1] or 0, total_deals=result[1] or 0,
# 更新:
total_wants=result[2] or 0, total_wants=result[2] or 0,
# 更新:
total_replies=result[3] or 0, total_replies=result[3] or 0,
# 更新:
total_views=result[4] or 0, total_views=result[4] or 0,
# 更新:
avg_price=float(result[5]) if result[5] else None avg_price=float(result[5]) if result[5] else None
# 更新:
) )
# 更新:
# 更新:
@router.get("/stats/categories", response_model=List[CategoryStat]) @router.get("/stats/categories", response_model=List[CategoryStat])
# 更新:
def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)): def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""按分类统计帖子数量""" """按分类统计帖子数量"""
# 更新:
results = db.execute(text(""" results = db.execute(text("""
# 更新:
SELECT category, COUNT(*) as count SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
GROUP BY category GROUP BY category
# 更新:
ORDER BY count DESC ORDER BY count DESC
# 更新:
"""), {"days": days}).fetchall() """), {"days": days}).fetchall()
# 更新:
# 更新:
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results] return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
# 更新:
# 更新:
@router.get("/stats/users", response_model=UserStat) @router.get("/stats/users", response_model=UserStat)
# 更新:
def get_user_stats(db: Session = Depends(get_coolbot_db)): def get_user_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取用户统计""" """获取用户统计"""
# 更新:
result = db.execute(text(""" result = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total_users, COUNT(*) as total_users,
# 更新:
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today, COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
# 更新:
COUNT(*) FILTER (WHERE is_seller = true) as sellers COUNT(*) FILTER (WHERE is_seller = true) as sellers
# 更新:
FROM yichens_users FROM yichens_users
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
return UserStat( return UserStat(
# 更新:
total_users=result[0] or 0, total_users=result[0] or 0,
# 更新:
new_users_today=result[1] or 0, new_users_today=result[1] or 0,
# 更新:
sellers=result[2] or 0 sellers=result[2] or 0
# 更新:
) )
# 更新:
# 更新:
@router.get("/posts") @router.get("/posts")
# 更新:
def get_posts( def get_posts(
# 更新:
limit: int = Query(20, ge=1, le=500), limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
# 更新:
category: Optional[str] = None, category: Optional[str] = None,
# 更新:
post_type: Optional[str] = None, post_type: Optional[str] = None,
# 更新:
keyword: Optional[str] = None, keyword: Optional[str] = None,
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取帖子列表 - 支持全局搜索,返回总数和分页信息""" """获取帖子列表 - 支持全局搜索,返回总数和分页信息"""
# 更新:
# 构建WHERE条件 # 构建WHERE条件
# 更新:
where_clauses = ["1=1"] where_clauses = ["1=1"]
# 更新:
params = {"limit": limit, "offset": offset} params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if category: if category:
# 更新:
where_clauses.append("category = :category") where_clauses.append("category = :category")
# 更新:
params["category"] = category params["category"] = category
# 更新:
# 更新:
if post_type: if post_type:
# 更新:
where_clauses.append("post_type = :post_type") where_clauses.append("post_type = :post_type")
# 更新:
params["post_type"] = post_type params["post_type"] = post_type
# 更新:
# 更新:
# 全局搜索 # 全局搜索
# 更新:
if keyword: if keyword:
# 更新:
where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)") where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)")
# 更新:
params["keyword"] = f"%{keyword}%" params["keyword"] = f"%{keyword}%"
# 更新:
# 更新:
where_sql = " AND ".join(where_clauses) where_sql = " AND ".join(where_clauses)
# 更新:
# 更新:
# 查询总数 # 查询总数
# 更新:
count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}" count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}"
# 更新:
total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0 total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0
# 更新:
# 更新:
# 查询数据 - 有post_time时按post_time排序没有时按crawled_at排序 # 查询数据 - 有post_time时按post_time排序没有时按crawled_at排序
# 更新:
data_query = f""" data_query = f"""
# 更新:
SELECT post_id, title, content, category, post_type, price, SELECT post_id, title, content, category, post_type, price,
# 更新:
author_username, post_time, reply_count, view_count, url author_username, post_time, reply_count, view_count, url
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE {where_sql} WHERE {where_sql}
# 更新:
ORDER BY COALESCE(post_time, crawled_at) DESC LIMIT :limit OFFSET :offset ORDER BY COALESCE(post_time, crawled_at) DESC LIMIT :limit OFFSET :offset
# 更新:
""" """
# 更新:
results = db.execute(text(data_query), params).fetchall() results = db.execute(text(data_query), params).fetchall()
# 更新:
# 更新:
posts = [PostItem( posts = [PostItem(
# 更新:
post_id=r[0], post_id=r[0],
# 更新:
title=r[1] or "", title=r[1] or "",
# 更新:
content=r[2] or "", content=r[2] or "",
# 更新:
category=r[3], category=r[3],
# 更新:
post_type=r[4] or "", post_type=r[4] or "",
# 更新:
price=float(r[5]) if r[5] else None, price=float(r[5]) if r[5] else None,
# 更新:
author_username=r[6] or "", author_username=r[6] or "",
# 更新:
post_time=str(r[7]) if r[7] else "", post_time=str(r[7]) if r[7] else "",
# 更新:
reply_count=r[8] or 0, reply_count=r[8] or 0,
# 更新:
view_count=r[9] or 0, view_count=r[9] or 0,
# 更新:
url=r[10] url=r[10]
# 更新:
) for r in results] ) for r in results]
# 更新:
# 更新:
return { return {
# 更新:
"posts": posts, "posts": posts,
# 更新:
"total": total_count, "total": total_count,
# 更新:
"page": offset // limit + 1, "page": offset // limit + 1,
# 更新:
"page_size": limit "page_size": limit
# 更新:
} }
# 更新:
# 更新:
@router.get("/users", response_model=List[UserItem]) @router.get("/users", response_model=List[UserItem])
# 更新:
def get_users( def get_users(
# 更新:
limit: int = Query(20, ge=1, le=500), limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
# 更新:
is_seller: Optional[bool] = None, is_seller: Optional[bool] = None,
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取用户列表""" """获取用户列表"""
# 更新:
query = """ query = """
# 更新:
SELECT user_id, username, avatar_url, credit_level, credit_score, SELECT user_id, username, avatar_url, credit_level, credit_score,
# 更新:
post_count, is_seller, registration_date post_count, is_seller, registration_date
# 更新:
FROM yichens_users FROM yichens_users
# 更新:
WHERE 1=1 WHERE 1=1
# 更新:
""" """
# 更新:
params = {"limit": limit, "offset": offset} params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if is_seller is not None: if is_seller is not None:
# 更新:
query += " AND is_seller = :is_seller" query += " AND is_seller = :is_seller"
# 更新:
params["is_seller"] = is_seller params["is_seller"] = is_seller
# 更新:
# 更新:
query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset" query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
# 更新:
# 更新:
results = db.execute(text(query), params).fetchall() results = db.execute(text(query), params).fetchall()
# 更新:
# 更新:
return [UserItem( return [UserItem(
# 更新:
user_id=r[0], user_id=r[0],
# 更新:
username=r[1] or "", username=r[1] or "",
# 更新:
avatar_url=r[2], avatar_url=r[2],
# 更新:
credit_level=r[3], credit_level=r[3],
# 更新:
credit_score=r[4], credit_score=r[4],
# 更新:
post_count=r[5] or 0, post_count=r[5] or 0,
# 更新:
is_seller=r[6] or False, is_seller=r[6] or False,
# 更新:
registration_date=str(r[7]) if r[7] else None registration_date=str(r[7]) if r[7] else None
# 更新:
) for r in results] ) for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/today") @router.get("/stats/today")
# 更新:
async def get_today_stats(db: Session = Depends(get_coolbot_db)): async def get_today_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日新增帖子统计""" """获取今日新增帖子统计"""
# 更新:
query = """ query = """
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes,
# 更新:
SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
""" """
# 更新:
result = db.execute(text(query)).fetchone() result = db.execute(text(query)).fetchone()
# 更新:
return { return {
# 更新:
"total": result[0] or 0, "total": result[0] or 0,
# 更新:
"deals": result[1] or 0, "deals": result[1] or 0,
# 更新:
"wants": result[2] or 0, "wants": result[2] or 0,
# 更新:
"others": result[3] or 0, "others": result[3] or 0,
# 更新:
"dragons": result[4] or 0, "dragons": result[4] or 0,
# 更新:
"horses": result[5] or 0, "horses": result[5] or 0,
# 更新:
"snakes": result[6] or 0, "snakes": result[6] or 0,
# 更新:
"tianma": result[7] or 0 "tianma": result[7] or 0
# 更新:
} }
# 更新:
# 更新:
@router.get("/stats/hour") @router.get("/stats/hour")
# 更新:
async def get_hour_stats(db: Session = Depends(get_coolbot_db)): async def get_hour_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取近一个小时新增帖子统计""" """获取近一个小时新增帖子统计"""
# 更新:
query = """ query = """
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 hour' WHERE post_time >= NOW() - INTERVAL '1 hour'
# 更新:
""" """
# 更新:
result = db.execute(text(query)).fetchone() result = db.execute(text(query)).fetchone()
# 更新:
return { return {
# 更新:
"total": result[0] or 0, "total": result[0] or 0,
# 更新:
"deals": result[1] or 0, "deals": result[1] or 0,
# 更新:
"wants": result[2] or 0, "wants": result[2] or 0,
# 更新:
"others": result[3] or 0, "others": result[3] or 0,
# 更新:
"dragons": result[3] or 0, "dragons": result[3] or 0,
# 更新:
"horses": result[4] or 0, "horses": result[4] or 0,
# 更新:
"snakes": result[5] or 0 "snakes": result[5] or 0
# 更新:
} }
# 更新:
# 更新:
@router.get("/stats/today-category") @router.get("/stats/today-category")
# 更新:
async def get_today_category_stats(db: Session = Depends(get_coolbot_db)): async def get_today_category_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日帖子分类统计""" """获取今日帖子分类统计"""
# 更新:
query = """ query = """
# 更新:
SELECT category, COUNT(*) as count SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
GROUP BY category GROUP BY category
# 更新:
ORDER BY count DESC ORDER BY count DESC
# 更新:
""" """
# 更新:
results = db.execute(text(query)).fetchall() results = db.execute(text(query)).fetchall()
# 更新:
return [{"category": r[0] or "未分类", "count": r[1]} for r in results] return [{"category": r[0] or "未分类", "count": r[1]} for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/dragons-today") @router.get("/stats/dragons-today")
# 更新:
def get_dragons_stats_today( def get_dragons_stats_today(
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取今日龙钞详细统计数据(按号码分类)- 就高不就低""" """获取今日龙钞详细统计数据(按号码分类)- 就高不就低"""
# 更新:
from sqlalchemy import text from sqlalchemy import text
# 更新:
# 更新:
# 1. 带4包含"带4"、"带四"、"通货" # 1. 带4包含"带4"、"带四"、"通货"
# 更新:
dai4 = db.execute(text(""" dai4 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%带4%' OR title LIKE '%带4%' content LIKE '%带4%' OR title LIKE '%带4%'
# 更新:
OR content LIKE '%带四%' OR title LIKE '%带四%' OR content LIKE '%带四%' OR title LIKE '%带四%'
# 更新:
OR content LIKE '%通货%' OR title LIKE '%通货%' OR content LIKE '%通货%' OR title LIKE '%通货%'
# 更新:
) )
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 2. 无4包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒" # 2. 无4包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒"
# 更新:
wu4 = db.execute(text(""" wu4 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无4%' OR title LIKE '%无4%' content LIKE '%无4%' OR title LIKE '%无4%'
# 更新:
OR content LIKE '%无四%' OR title LIKE '%无四%' OR content LIKE '%无四%' OR title LIKE '%无四%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%' AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%'
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
# 更新:
AND content NOT LIKE '%永恒%' AND title NOT LIKE '%永恒%' AND content NOT LIKE '%永恒%' AND title NOT LIKE '%永恒%'
# 更新:
AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%' AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 3. 无47包含"无47"、"永恒"、"无四七",排除"无247" # 3. 无47包含"无47"、"永恒"、"无四七",排除"无247"
# 更新:
wu47 = db.execute(text(""" wu47 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无47%' OR title LIKE '%无47%' content LIKE '%无47%' OR title LIKE '%无47%'
# 更新:
OR content LIKE '%永恒%' OR title LIKE '%永恒%' OR content LIKE '%永恒%' OR title LIKE '%永恒%'
# 更新:
OR content LIKE '%无四七%' OR title LIKE '%无四七%' OR content LIKE '%无四七%' OR title LIKE '%无四七%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 4. 无247包含"无247"、"天马"、"金山",排除"无347" # 4. 无247包含"无247"、"天马"、"金山",排除"无347"
# 更新:
wu247 = db.execute(text(""" wu247 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无247%' OR title LIKE '%无247%' content LIKE '%无247%' OR title LIKE '%无247%'
# 更新:
OR content LIKE '%天马%' OR title LIKE '%天马%' OR content LIKE '%天马%' OR title LIKE '%天马%'
# 更新:
OR content LIKE '%金山%' OR title LIKE '%金山%' OR content LIKE '%金山%' OR title LIKE '%金山%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%' AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 5. 无347包含"无347"、"钻石"、"金马"、"魅力"、"朦胧" # 5. 无347包含"无347"、"钻石"、"金马"、"魅力"、"朦胧"
# 更新:
wu347 = db.execute(text(""" wu347 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无347%' OR title LIKE '%无347%' content LIKE '%无347%' OR title LIKE '%无347%'
# 更新:
OR content LIKE '%钻石%' OR title LIKE '%钻石%' OR content LIKE '%钻石%' OR title LIKE '%钻石%'
# 更新:
OR content LIKE '%金马%' OR title LIKE '%金马%' OR content LIKE '%金马%' OR title LIKE '%金马%'
# 更新:
OR content LIKE '%魅力%' OR title LIKE '%魅力%' OR content LIKE '%魅力%' OR title LIKE '%魅力%'
# 更新:
OR content LIKE '%朦胧%' OR title LIKE '%朦胧%' OR content LIKE '%朦胧%' OR title LIKE '%朦胧%'
# 更新:
) )
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
return { return {
# 更新:
"dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0}, "dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0},
# 更新:
"wu4": {"total": wu4[0] or 0, "deals": wu4[1] or 0, "wants": wu4[2] or 0}, "wu4": {"total": wu4[0] or 0, "deals": wu4[1] or 0, "wants": wu4[2] or 0},
# 更新:
"wu47": {"total": wu47[0] or 0, "deals": wu47[1] or 0, "wants": wu47[2] or 0}, "wu47": {"total": wu47[0] or 0, "deals": wu47[1] or 0, "wants": wu47[2] or 0},
# 更新:
"wu247": {"total": wu247[0] or 0, "deals": wu247[1] or 0, "wants": wu247[2] or 0}, "wu247": {"total": wu247[0] or 0, "deals": wu247[1] or 0, "wants": wu247[2] or 0},
# 更新:
"wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0} "wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0}
# 更新:
} }
# 更新:
# 更新:

View File

@ -20,7 +20,6 @@ class UserBase(BaseModel):
class UserCreate(UserBase): class UserCreate(UserBase):
password: str = Field(..., min_length=6) password: str = Field(..., min_length=6)
invite_code: Optional[str] = Field(None, alias="inviteCode") # 填写的邀请码(选填) invite_code: Optional[str] = Field(None, alias="inviteCode") # 填写的邀请码(选填)
# 移除手机号和短信验证2026-06-13
class UserUpdate(BaseModel): class UserUpdate(BaseModel):

View File

@ -1,262 +0,0 @@
# coolbot_matcher - 一尘数据库号码匹配工具
# Version: 0.0.2 (2026-04-30)
# 描述:从一尘数据库(coolbot_data)查询匹配的藏品号码
# 更新:号码分类规则修正
from typing import List, Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.coolbot_db import coolbot_engine
def classify_number(number: str) -> str:
"""根据号码特征分类
规则2026-04-30修正版
首先区分标百标十单张
- 单张J后面9位
- 标十J后面8位最后一位是1
- 标百J后面7位最后两位是01
分类优先级
| 类型 | 排除 | 可用数字 | 必须包含 |
|------|------|----------|----------|
| 通货 | - | - | 4 |
| 带7号 | 4 | - | 7 |
| 永恒号 | 47 | - | - |
| 圆圆号 | 123457 | 0689 | - |
| 倒置号 | 23457 | 01689 | 1 |
| 金马王 | 12347 | 05689 | 5 |
| 金马号 | 2347 | 015689 | 1,5 |
| 金山王 | 12457 | 03689 | 3 |
| 天马王 | 1247 | 035689 | 3,5 |
| 金山号 | 2457 | 013689 | 1,3 |
| 天马号 | 247 | 0135689 | 1,3,5 |
| 朦胧王 | 13457 | - | - |
| 朦胧号 | 3457 | - | - |
| 如意号 | 1347 | - | - |
| 钻石号 | 347 | - | - |
"""
if not number:
return "未知"
if number.startswith('J0'):
digits = number[2:]
elif number.startswith('J'):
digits = number[1:]
else:
return "未知"
if len(digits) == 9:
d = digits
elif len(digits) == 8:
d = digits[:8]
elif len(digits) == 7:
d = digits
else:
return "未知"
unique = set(d)
# 1. 通货带4
if '4' in unique:
return "通货"
# 2. 带7号无4有7
if '7' in unique:
return "带7号"
# 3. 圆圆号无1234570689四个数字任意组合
if unique <= {'0', '6', '8', '9'}:
return "圆圆号"
# 4. 永恒号只有0和1无47但不符合圆圆号/倒置号)
if unique <= {'0', '1'}:
return "永恒号"
# 5. 倒置号01689组合必须有1且包含6/8/9中的至少一个
if unique <= {'0', '1', '6', '8', '9'} and '1' in unique and unique & {'6', '8', '9'}:
return "倒置号"
# 6. 金马王无1234705689组合必须有5排除1
if unique <= {'0', '5', '6', '8', '9'} and '5' in unique and '1' not in unique:
return "金马王"
# 7. 金马号无2347015689组合必须有1和5包含1
if unique <= {'0', '1', '5', '6', '8', '9'} and '1' in unique and '5' in unique:
return "金马号"
# 8. 金山王无1245703689组合必须有3排除1和5
if unique <= {'0', '3', '6', '8', '9'} and '3' in unique and '1' not in unique and '5' not in unique:
return "金山王"
# 9. 天马王无1247035689组合必须有3和5排除1
if unique <= {'0', '3', '5', '6', '8', '9'} and '3' in unique and '5' in unique and '1' not in unique:
return "天马王"
# 10. 金山号无2457013689组合必须有1和3排除5
if unique <= {'0', '1', '3', '6', '8', '9'} and '1' in unique and '3' in unique and '5' not in unique:
return "金山号"
# 11. 天马号无2470135689组合必须有1、3和5
if unique <= {'0', '1', '3', '5', '6', '8', '9'} and '1' in unique and '3' in unique and '5' in unique:
return "天马号"
# 12. 朦胧王无13457
if not unique & {'1', '3', '4', '5', '7'}:
return "朦胧王"
# 13. 朦胧号无3457
if not unique & {'3', '4', '5', '7'}:
return "朦胧号"
# 14. 如意号无1347
if not unique & {'1', '3', '4', '7'}:
return "如意号"
# 15. 钻石号无347
if not unique & {'3', '4', '7'}:
return "钻石号"
# 16. 永恒号兜底无47
if '7' not in unique:
return "永恒号"
return "其他"
def check_match(col_number: str, expect_number: str, expect_category: str) -> bool:
"""检查藏品号码是否符合期望的分类"""
if not col_number or not expect_category:
return False
col_cat = classify_number(col_number)
if col_cat == expect_category:
return True
if expect_category == "其他":
return check_number_pattern(col_number, expect_number)
return False
def check_number_pattern(col_number: str, expect_number: str) -> bool:
"""检查号码特征匹配"""
if not col_number or not expect_number:
return False
if col_number.startswith('J0'):
col_digits = col_number[2:]
else:
col_digits = col_number[1:] if len(col_number) > 1 else col_number
if expect_number.startswith('J0'):
exp_digits = expect_number[2:]
else:
exp_digits = expect_number[1:] if len(expect_number) > 1 else expect_number
min_len = min(len(col_digits), len(exp_digits))
return col_digits[:min_len] == exp_digits[:min_len]
def get_match_type(expect_number: str) -> str:
"""判断匹配类型"""
if not expect_number:
return "unknown"
if expect_number.startswith('J0'):
digits = expect_number[2:]
elif expect_number.startswith('J'):
digits = expect_number[1:]
else:
return "unknown"
if len(digits) == 8:
return "ten"
elif len(digits) == 7:
return "hundred"
elif len(digits) == 9:
return "single"
return "unknown"
def match_self_collections_count(db: Session, user_id: str, expect_number: str, expect_category: str) -> int:
"""计算匹配藏品数量(用户自有藏品)"""
from app.models.models import Collection
if not expect_number:
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 ''
if check_match(number, expect_number, expect_category):
count += 1
return count
def match_collections_count_from_coolbot(expect_number: str, expect_category: str) -> int:
"""计算匹配藏品数量(一尘数据库)"""
if not expect_number:
return 0
query = text("""
SELECT id, crown_code FROM collections
WHERE crown_code IS NOT NULL
AND crown_code != ''
AND crown_code LIKE 'J0%'
""")
try:
with coolbot_engine.connect() as conn:
result = conn.execute(query)
return sum(1 for row in result if check_match(row[1], expect_number, expect_category))
except Exception as e:
print(f"Error: {e}")
return 0
def match_collections_list_from_coolbot(expect_number: str, expect_category: str, limit: int = 20) -> List[dict]:
"""获取匹配的藏品列表(一尘数据库)"""
if not expect_number:
return []
query = text("""
SELECT id, name, category, crown_code, price, post_title, post_url, author, post_crawled_at
FROM collections
WHERE crown_code IS NOT NULL
AND crown_code != ''
AND crown_code LIKE 'J0%'
""")
try:
with coolbot_engine.connect() as conn:
result = conn.execute(query)
matched = []
for row in result:
if check_match(row[3], expect_number, expect_category):
matched.append({
"id": row[0],
"name": row[1],
"category": row[2],
"crown_code": row[3],
"price": float(row[4]) if row[4] else None,
"post_title": row[5],
"post_url": row[6],
"author": row[7],
"post_crawled_at": row[8].isoformat() if row[8] else None
})
if len(matched) >= limit:
break
return matched
except Exception as e:
print(f"Error: {e}")
return []

View File

@ -1,222 +1,5 @@
# 版本更新记录 # 版本更新记录
## v0.2.7 (2026-07-19) - 龙钞宣传功能
### 前端更新
- **Wiki.jsx (新增)**
- 龙钞百科页面,包含:什么是龙钞、号码分类、术语解释、评级知识、保养指南、交易指南
- 新手入门必备指南
- **SharePoster.jsx (新增)**
- 藏品分享海报组件
- 一键生成精美海报,朋友圈分享
- **List.jsx**
- 新增分享按钮,每个藏品卡片右上角显示"📤 分享"按钮
- 点击生成精美海报,保存到相册
- **Home.jsx**
- 快捷操作新增"📚 龙钞百科"入口
### 依赖更新
- 新增 html2canvas 库,用于生成海报图片
---
## v0.2.5 (2026-05-13)
### 前端更新
- **News.jsx**
- 移除成交行情价格统计表格,只显示筛选后的结果
- 成交行情获取数量改为500条
- **YichensBoard.jsx**
- 增加复制链接功能,点击可复制原帖链接
### 环境配置
- C环境Nginx配置为代理到B环境后端
## v0.2.3 (2026-05-11)
### 前端更新
- **News.jsx**
- 修复:统计表格点击均价弹出的成交详情列表按成交日期倒序排序
- 更新calcAvg函数过滤后对items按deal_date排序
## v0.2.2 (2026-05-11)
### 后端更新
- **seek.py**
- 新增寻配号支持评级字段number, is_graded, grading_company, grading_score
- 更新SeekInfoCreate/Update/Response Schema 新增评级字段支持
- **seek_info模型**
- 新增number, is_graded, grading_company, grading_score 字段
## v0.2.1 (2026-05-07)
### 前端更新
- **Add.jsx**
- 修复:行情录入时评级信息(is_graded, grading_company, grading_score)未保存到数据库的问题
- 修复:表单重置后未清空评级字段的问题
- 同步修复:批量录入时的评级字段问题
- **News.jsx**
- 修复:成交行情列表按成交日期(deal_date)倒序排序
- 修复:统计表格详情弹窗列表按成交日期倒序排序
### 后端更新
- **purchase.py**
- 新增:管理员审批功能 - 创建认购群需要管理员审批
- 新增pending-approval接口 - 获取待审批群列表(仅管理员可见)
- 新增approve接口 - 管理员审批/拒绝认购群
- 新增my-pending-groups接口 - 获取我创建的待审批群
- 修改普通用户创建群状态设为pending管理员创建直接active
- 修改加入群时检查pending/rejected状态
- **purchase模型**
- 新增approved_by字段 - 审批管理员ID
- 新增approved_at字段 - 审批时间
- 修改status字段扩展支持pending/active/closed/rejected状态
## v0.1.9 (2026-05-02)
### 后端更新
- **auth.py**
- 修复get_current_user在未登录时返回None导致500错误现在会返回401
- 修复用户不存在时返回None导致500错误现在返回401
- 修复所有需要认证的接口现在会正确返回401
- **seek.py**
- 修复:/api/seek/stats接口现在不需要登录公开接口
- **users.py**
- 修复添加Information模型导入
### 数据库更新
- seek_info表添加network_matched_count字段
### 环境配置
- B环境短信配置AccessKey/SignName/TemplateCode更新
- A环境短信配置添加短信环境变量
## v0.1.8 (2026-05-01)
### 前端更新
- **News.jsx**
- 新增:成交行情筛选功能(版别筛选:龙钞/马钞/蛇钞/其他)
- 新增:包装类型筛选(全部/标百/标十/单张)
- 新增:号码分类筛选(全部/带4号/带7号/永恒/钻石/天马/金山/金马/倒置/圆圆)
- 新增:清除筛选按钮
- 筛选同时作用于行情清单列表和统计表格
## v0.1.6 (2026-05-01)
### 前端更新
- **List.jsx**
- 新增:搜索框增强 - 支持搜索冠字号、编号、价格
- 新增:版别筛选按钮 - 20版/19版/18版/17版/16版
- 新增:包装类型筛选 - 单张/标十/标百
- 新增:号码分类筛选 - 圆圆号/倒置号/金马号/天马号/钻石号/永恒号/带7号/带4号
- 新增:清除筛选按钮
- 优化:筛选结果显示当前已选条件
## v0.1.5 (2026-05-01)
### 前端更新
- **Purchase.jsx**
- 新增待审批Tab黄色按钮- 显示需要审批的成员
- 新增:待审批成员列表 - 显示申请人信息+同意/拒绝按钮
- 新增:审批后状态 - 变灰+显示已批准/已拒绝
- 新增:申请加入后待审批状态 - 列表显示黄色"待审批"
- 修复:待审批数量计算准确
- 修复:只有群主能看到自己创建的群的待审批
### 后端更新
- **purchase.py**
- 修复join_group接口处理状态
- 新增MemberResponse扩展字段wechat_name, contact等
## v0.1.4 (2026-05-01)
### 前端更新
- **Purchase.jsx**
- 修复:列表隐藏群介绍,点进详情后才显示
- 修复:未登录不黑屏
- 新增:修改群信息弹窗(群主)
- 新增:修改个人信息弹窗(成员)
- 修复冠字号输入框maxLength
## v0.1.3 (2026-04-30)
### 前端更新
- **Home.jsx**
- 修复:我的认购入口字体颜色改为蓝色 #3b82f6
- **Purchase.jsx**
- 新增:权限控制 - 未加入的群只能看到基本信息(群名、编号、群主、介绍)
- 新增:申请加入按钮 - 未加入的群可申请加入
- 修复:敏感信息(统计、成员、藏品)只有已加入才可见
- 修复user.id改为user.f99_90_id
## v0.1.2 (2026-04-30)
### 前端更新
- **Purchase.jsx**
- 新增创建群表单6个必填字段
- 认购群名称
- 认购介绍(群介绍)
- 参与人数
- 总认购数
- 认购周期(月)
- 开始日期
### 后端更新
- **purchase.py**
- 新增:创建群接口支持新字段 total_members、total_collections
## v0.0.12 (2026-04-29)
### 前端更新
- **Purchase.jsx**
- 完善:入群申请审批流程 (待审批成员列表)
- 完善:新增认购藏品字段
- 编号自动生成001开始
- 冠字号:必填
- 评级:必填 (下拉选择)
- 认购老板:必填 (从成员中选择)
- 价格:必填
- 来源:必填 (一尘/直播/群友/其他)
- 认购款结清checkbox
- 优化:表单布局
### 后端更新
- **purchase.py**
- 新增:入群审批接口 /approve
- 新增:待审批成员列表 /pending-members
- 新增:认购藏品新字段 (grading, boss_name, source, paid, submit_date)
---
## v0.0.7 (2026-04-24)
### 代码清理
- **coolbot_matcher.py** (新建)
- 功能:创建一尘数据库号码匹配工具模块
- 包含match_pattern, match_self_collections_count, match_collections_count_from_coolbot, match_collections_list_from_coolbot
- **seek.py**
- 功能:更新导入,添加缺失端点
- 新增端点:/match-confirm, /matched-user/{id}, /publisher/{id}
- 优化:移除列表接口网络匹配实时计算,提升加载速度
- **information.py**
- 清理注释废弃的seek/deal重复端点统一使用/api/seek/*和/api/deal/*
- **News.jsx**
- 迁移seek相关API从/api/information/*迁移到/api/seek/*
---
## v1.2.98 (2026-04-19) ## v1.2.98 (2026-04-19)
### 前端更新 ### 前端更新
@ -250,8 +33,3 @@
### 更新内容 ### 更新内容
- (待记录) - (待记录)
## v0.0.6 (2026-04-24)
### Bug修复
- 修复"自有X条藏品匹配成功"点击后没有藏品列表的问题
- 原因seek/match API只查询information表但实际数据在seek_info表
- 修复API同时支持information表和seek_info表查询

View File

@ -1 +1 @@
VERSION=0.2.5 1.2.98

View File

@ -1,29 +1,47 @@
{ {
"version": "0.2.7", "version": "1.2.98",
"updated": "2026-07-19", "updated": "2026-04-19",
"modules": { "modules": {
"frontend": { "frontend": {
"version": "0.2.3", "version": "1.2.98",
"pages": { "pages": {
"Login": "0.0.2", "Add": "1.2.90",
"Add": "0.0.2", "Admin": "1.2.98",
"News": "0.1.2", "Detail": "1.2.90",
"YichensBoard": "0.0.5", "Edit": "1.2.90",
"Wiki": "1.0.0", "Home": "1.2.95",
"SharePoster": "1.0.0", "List": "1.2.93",
"List": "0.1.7" "Login": "1.2.85",
"News": "1.2.80",
"News_YichensBoard": "1.2.75",
"Settings": "1.2.75",
"Stats": "1.2.70",
"YichensBoard": "1.2.70"
},
"config": {
"version": "1.2.98"
} }
}, },
"backend": { "backend": {
"version": "0.2.2", "version": "1.2.98",
"routers": { "routers": {
"auth": "0.0.2", "auth": "1.2.90",
"seek": "0.2.0", "collections": "1.2.95",
"purchase": "0.3.0" "deal": "1.2.85",
"information": "1.2.92",
"news": "1.2.80",
"ocr": "1.2.75",
"operations": "1.2.70",
"seek": "1.2.70",
"users": "1.2.98",
"yichens": "1.2.70"
}, },
"models": { "app": {
"seek_info": "0.2.0", "core": "1.2.0",
"purchase": "0.0.3" "models": "1.2.0",
"schemas": "1.2.0",
"services": "1.2.0",
"utils": "1.2.0"
} }
} }
} }

View File

@ -1,449 +0,0 @@
# 🐉 甲辰藏品管理系统 环境部署手册
> **版本**: v1.0
> **更新日期**: 2026-04-24
> **维护人**: 龙大
---
## 📋 环境总览
| 环境 | 定位 | 域名 | 前端IP | 后端IP | 数据库 | 状态 |
|------|------|------|--------|--------|--------|------|
| **C** | 测试开发 | socoolbot.com.cn | 47.103.29.111 | 47.103.9.192:3000 | 本地PostgreSQL | ✅ 开放 |
| **A** | 灰度发布 | socoolbot.com | 120.26.133.10 | 120.26.144.208:3000 | RDS | 🔒 封闭 |
| **B** | 主系统(生产) | jiachenlong.com | 120.55.81.21 | 47.110.37.129:3000 | RDS | 🔒 封闭 |
| **D** | 备用环境 | socoolbot.cn | 47.111.184.210 | 47.111.184.210:3000 | RDS | 🔒 封闭 |
### 部署流程
```
开发(C) → 灰度(A测试) → 主系统(B发布) → 备用(D保留)
```
---
## ⚠️ 绝对准则
1. **环境操作准则**A、B环境的系统更新和操作必须经过酷博特允许否则绝对不允许操作
2. **版本管理准则**:版本管理必须经过酷博特确认,不许擅自更新版本号
3. **数据操作准则**RDS数据库中的数据没有酷博特允许不许进行增删改的操作
4. **操作原则**:以上三条为绝对准则,任何情况下都不得违反
---
## 🧪 C环境测试开发环境
### 特点
- **用途**:开发测试,所有新功能先在这里开发测试
- **数据库**本地PostgreSQL不连接RDS
- **权限**:开放,可自行操作
- **网络**:可直接访问公网
### 服务器信息
| 角色 | IP | SSH密码 | 服务 |
|------|-----|---------|------|
| 前端 | 47.103.29.111 | Test1234 | Nginx + React构建产物 |
| 后端 | 47.103.9.192 | Test1234 | FastAPI + 本地PostgreSQL |
### 访问链接
- 前端http://47.103.29.111
- APIhttp://47.103.9.192:3000
### 部署步骤
#### 前端部署
```bash
# 1. 本地构建
cd /root/.openclaw/workspace/jiachenlong/frontend
npm run build
# 2. 复制到服务器
sshpass -p 'Test1234' scp -o StrictHostKeyChecking=no \
/root/.openclaw/workspace/jiachenlong/frontend/dist/* \
root@47.103.29.111:/var/www/frontend/
# 3. 验证
curl -s http://47.103.29.111/ | grep -o 'index-[^"]*\.js'
```
#### 后端部署
```bash
# 1. 同步代码
sshpass -p 'Test1234' scp -o StrictHostKeyChecking=no \
-r /root/.openclaw/workspace/jiachenlong/backend \
root@47.103.9.192:/root/jiachenlong/
# 2. 启动服务C环境使用本地数据库不设置DATABASE_URL
sshpass -p 'Test1234' ssh -o StrictHostKeyChecking=no root@47.103.9.192 \
'cd /root/jiachenlong/backend && \
export SECRET_KEY="dev-secret-key-12345" && \
export OSS_ACCESS_KEY_ID="LTAI5t6HUnpFBLEK9194kPVG" && \
export OSS_ACCESS_KEY_SECRET="LEr4Q8yRxb8D5b24cKfCwlt4MMoke1" && \
pkill -f uvicorn 2>/dev/null; \
sleep 1; \
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 > /tmp/uvicorn.log 2>&1 &'
# 3. 验证
curl -s http://47.103.9.192:3000/
```
### 部署后检查
```bash
# 1. 前端访问
curl -s -o /dev/null -w "前端: %{http_code}" http://47.103.29.111/
# 2. 后端API
curl -s -o /dev/null -w "后端: %{http_code}" http://47.103.9.192:3000/
# 3. 登录功能
curl -s -X POST http://47.103.9.192:3000/api/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=admin123" | grep -q "token" && echo "登录OK"
# 4. 列表加载速度(应<1秒
time curl -s "http://47.103.9.192:3000/api/seek/list?page=1&page_size=500" -o /dev/null
```
### 常见问题
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 前端显示旧版本 | 浏览器缓存 | Ctrl+F5 强制刷新 |
| 后端启动失败 | 环境变量未设置 | 检查 start_backend.sh 脚本 |
| 数据库连接失败 | 使用了RDS地址 | C环境不使用DATABASE_URL环境变量 |
| 加载很慢 | seek列表计算网络匹配 | 检查seek.py是否移除了match_collections_count_from_coolbot |
---
## 🏭 A环境灰度发布环境
### 特点
- **用途**:发布前测试验证
- **数据库**阿里云RDS (pgm-bp1t5w248t7s1pvr)
- **权限**:🔒 封闭,需酷博特授权
- **域名**socoolbot.com
### 服务器信息
| 角色 | IP | 内网IP | SSH密码 |
|------|-----|--------|---------|
| WebA | 120.26.133.10 | 172.26.29.103 | AJiachen123 |
| AppA | 120.26.144.208 | 172.26.30.31 | AJiachen123 |
### 访问链接
- https://socoolbot.com
### 短信配置
- 签名:苏州双人旁
- 模板SMS_505015231
- AccessKeyLTAI5t86bc1nNKVNyYv4Af6x
### 部署步骤(需授权)
#### 部署前准备
1. 确认要部署的版本
2. 联系酷博特获取授权
3. 确认数据库密码
#### 第一步:前端部署
```bash
# SSH连接
ssh root@120.26.133.10 # 密码: AJiachen123
# 克隆代码
cd /root && rm -rf jiachenlong
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git
# 构建
cd /root/jiachenlong/frontend && npm install && npm run build
# 复制到Nginx目录
rm -rf /var/www/frontend/*
cp -r /root/jiachenlong/frontend/dist/* /var/www/frontend/
# 复制Logo
mkdir -p /var/www/frontend/static/images
cp /root/jiachenlong/static/images/jiachenlong-logo.png /var/www/frontend/static/images/
# 复制用户协议
cp /root/jiachenlong/frontend/dist/user_agreement.html /var/www/frontend/
# 重启Nginx
nginx -s reload
```
#### 第二步:后端部署
```bash
# SSH连接
ssh root@120.26.144.208 # 密码: AJiachen123
# 克隆代码
cd /root && rm -rf jiachenlong
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git
# 安装依赖
cd /root/jiachenlong/backend && pip3 install -r requirements.txt
# 创建启动脚本
cat > /root/jiachenlong/start.sh << 'EOF'
#!/bin/bash
cd /root/jiachenlong/backend
export DATABASE_URL="postgresql://jiachenlong:Passwd1%403@pgm-bp1t5w248t7s1pvr.pg.rds.aliyuncs.com:5432/jiachenlong"
export SECRET_KEY="production-secret-key-a-env"
export OSS_ACCESS_KEY_ID="LTAI5t6HUnpFBLEK9194kPVG"
export OSS_ACCESS_KEY_SECRET="LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"
export SMS_ACCESS_KEY_ID="LTAI5t86bc1nNKVNyYv4Af6x"
export SMS_ACCESS_KEY_SECRET="92EVAIE3GECr214c9UaSF6TSYJvDLY"
export SMS_SIGN_NAME="苏州双人旁"
export SMS_TEMPLATE_CODE="SMS_505015231"
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 > /tmp/uvicorn.log 2>&1 &
echo "A环境后端已启动"
EOF
chmod +x /root/jiachenlong/start.sh
# 启动服务
pkill -f uvicorn; sleep 1; /root/jiachenlong/start.sh
```
#### 第三步:验证部署
```bash
# 1. 版本检查
curl -s https://socoolbot.com/ | grep -o 'v[0-9.]*'
# 2. API检查
curl -s -o /dev/null -w "API: %{http_code}" https://socoolbot.com/api/
# 3. 登录测试
curl -s -X POST https://socoolbot.com/api/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=admin123"
```
### 部署后检查清单
- [ ] 前端页面可访问HTTP和HTTPS
- [ ] 版本号显示正确
- [ ] 登录功能正常
- [ ] 藏品列表加载正常
- [ ] 藏品新增/编辑功能正常
- [ ] 图片上传功能正常
- [ ] 短信发送功能正常
- [ ] 历史数据完整(用户数、藏品数)
---
## 🏭 B环境主系统生产环境
### 特点
- **用途**:主系统生产环境,统一入口
- **数据库**阿里云RDS (pgm-bp1t5w248t7s1pvr)
- **权限**:🔒 封闭,需酷博特授权
- **域名**jiachenlong.com
### 服务器信息
| 角色 | IP | 内网IP | SSH密码 |
|------|-----|--------|---------|
| WebB | 120.55.81.21 | 172.26.30.30 | Jiachen123 |
| AppB | 47.110.37.129 | 172.26.29.98 | Jiachen123 |
### 访问链接
- https://jiachenlong.com
### 短信配置
- 签名:双人旁文化传媒
- 模板SMS_504740243
- AccessKeyLTAI5tPzZDqbM1J6zbk8Wy4q
### 部署步骤(需授权)
#### 第一步:前端部署
```bash
ssh root@120.55.81.21 # 密码: Jiachen123
# 克隆代码并构建同A环境
cd /root && rm -rf jiachenlong
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git
cd /root/jiachenlong/frontend && npm install && npm run build
rm -rf /var/www/frontend/*
cp -r /root/jiachenlong/frontend/dist/* /var/www/frontend/
mkdir -p /var/www/frontend/static/images
cp /root/jiachenlong/static/images/jiachenlong-logo.png /var/www/frontend/static/images/
cp /root/jiachenlong/frontend/dist/user_agreement.html /var/www/frontend/
nginx -s reload
```
#### 第二步:后端部署
```bash
ssh root@47.110.37.129 # 密码: Jiachen123
# 克隆代码
cd /root && rm -rf jiachenlong
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git
cd /root/jiachenlong/backend && pip3 install -r requirements.txt
# 创建启动脚本注意B环境使用不同的短信配置
cat > /root/jiachenlong/start.sh << 'EOF'
#!/bin/bash
cd /root/jiachenlong/backend
export DATABASE_URL="postgresql://jiachenlong:Passwd1%403@pgm-bp1t5w248t7s1pvr.pg.rds.aliyuncs.com:5432/jiachenlong"
export SECRET_KEY="production-secret-key-b-env-20260401"
export OSS_ACCESS_KEY_ID="LTAI5t6HUnpFBLEK9194kPVG"
export OSS_ACCESS_KEY_SECRET="LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"
export SMS_ACCESS_KEY_ID="LTAI5tPzZDqbM1J6zbk8Wy4q"
export SMS_ACCESS_KEY_SECRET="ErT3jH9kL2mN5pQ7rS8uV0wX1yZ4aB6c"
export SMS_SIGN_NAME="双人旁文化传媒"
export SMS_TEMPLATE_CODE="SMS_504740243"
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 --workers 1 > /tmp/uvicorn.log 2>&1 &
echo "B环境后端已启动"
EOF
chmod +x /root/jiachenlong/start.sh
pkill -f uvicorn; sleep 1; /root/jiachenlong/start.sh
```
### 部署后检查清单
- [ ] 前端页面可访问HTTP和HTTPS
- [ ] 版本号显示正确
- [ ] 登录功能正常
- [ ] 所有功能正常
- [ ] 短信发送正常
- [ ] 数据完整
- [ ] 向酷博特汇报部署完成
---
## 🧪 D环境备用环境
### 特点
- **用途**:备用保留
- **数据库**阿里云RDS
- **权限**:🔒 封闭,需酷博特授权
### 服务器信息
| 角色 | IP | 内网IP | SSH密码 |
|------|-----|--------|---------|
| WebD | 47.111.184.210 | 172.26.30.32 | (同A/B) |
| AppD | 47.111.184.210 | 172.26.30.33 | (同A/B) |
### 访问链接
- http://47.111.184.210
- socoolbot.cn
### 短信配置
- 签名:双人旁文化传媒
- 模板SMS_504740243
---
## 🔧 常见问题处理
### 1. 前端显示旧版本
```bash
# 原因:浏览器缓存
# 解决Ctrl+F5 强制刷新或清理Nginx缓存
```
### 2. 后端启动失败
```bash
# 检查日志
tail -50 /tmp/uvicorn.log
# 常见错误:
# - SECRET_KEY未设置 → 添加环境变量
# - 数据库连接失败 → 检查DATABASE_URL格式密码需要URL编码
# - 端口被占用 → pkill -f uvicorn 后重试
```
### 3. 加载很慢seek列表
```bash
# 检查seek.py是否移除了match_collections_count_from_coolbot调用
grep -n "network_matched_count" /root/jiachenlong/backend/app/routers/seek.py
# 确认返回的network_matched_count为0
curl -s "http://后端IP:3000/api/seek/list?page=1&page_size=5" | grep network_matched_count
```
### 4. 图片无法上传
```bash
# 检查OSS配置
curl -s http://后端IP:3000/api/collections/test-upload
# 检查Nginx上传大小限制
grep client_max_body_size /etc/nginx/nginx.conf
```
### 5. 短信发送失败
```bash
# 检查环境变量
ssh root@后端IP "env | grep SMS"
# 检查模板代码和签名是否匹配当前环境
```
### 6. 数据库连接超时
```bash
# 检查RDS白名单
# C环境使用本地数据库不连RDS
# A/B/D环境需要检查安全组是否允许访问RDS
```
---
## 📝 版本管理
### 更新版本流程
1. 修改代码文件头部Version注释
2. 更新config/VERSION.json
3. 更新config/CHANGELOG.md
4. 提交Git
5. 部署后验证
### 版本检查命令
```bash
# 本地
cat /root/.openclaw/workspace/jiachenlong/config/VERSION.json
# 服务器
curl -s http://服务器IP/ | grep -o 'v[0-9.]*'
```
---
## 📞 紧急联系人
- **酷博特**:项目总负责,所有重大操作需汇报
- **龙大**:环境部署负责人
---
## 📝 部署记录模板
```
## 部署记录 - [日期]
### 环境:[A/B/C/D]
### 版本:[v0.0.x]
### 部署人:龙大
#### 部署步骤
1. [步骤1]
2. [步骤2]
#### 检查结果
- [x] 前端OK
- [x] 后端OK
- [x] 登录OK
#### 问题记录
#### 汇报状态
已向酷博特汇报
```

View File

@ -1,6 +0,0 @@
const fs = require('fs');
const htmlPath = './index.html';
let html = fs.readFileSync(htmlPath, 'utf-8');
html = html.replace('v=0.0.3', 'v=0.0.4');
fs.writeFileSync(htmlPath, html);
console.log('Done');

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v0.2.5</title> <title>甲辰收藏 v=1.2.97</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="/static/icons/favicon.png" /> <link rel="icon" type="image/png" href="/static/icons/favicon.png" />

View File

@ -9,7 +9,6 @@
"version": "1.2.82", "version": "1.2.82",
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"html2canvas": "^1.4.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.1.0" "react-router-dom": "^7.1.0"
@ -1240,15 +1239,6 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.10.8", "version": "2.10.8",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
@ -1362,15 +1352,6 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -1692,19 +1673,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -2030,15 +1998,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -2087,15 +2046,6 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.4.2", "version": "6.4.2",
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz",

View File

@ -10,7 +10,6 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"html2canvas": "^1.4.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.1.0" "react-router-dom": "^7.1.0"

View File

@ -5,12 +5,10 @@ import List from './pages/List'
import Add from './pages/Add' import Add from './pages/Add'
import Stats from './pages/Stats' import Stats from './pages/Stats'
import News from './pages/News' import News from './pages/News'
import Purchase from './pages/Purchase'
import Login from './pages/Login' import Login from './pages/Login'
import Detail from './pages/Detail' import Detail from './pages/Detail'
import Edit from './pages/Edit' import Edit from './pages/Edit'
import Admin from './pages/Admin' import Admin from './pages/Admin'
import Wiki from './pages/Wiki'
export default function App() { export default function App() {
const [path, setPath] = useState(window.location.hash.slice(1) || '/') const [path, setPath] = useState(window.location.hash.slice(1) || '/')
@ -32,14 +30,12 @@ export default function App() {
const basePath = path.split('?')[0] const basePath = path.split('?')[0]
if (basePath === '/') return <Home /> if (basePath === '/') return <Home />
if (basePath === '/news') return <News /> if (basePath === '/news') return <News />
if (basePath === '/purchase') return <Purchase />
if (basePath === '/stats') return <Stats /> if (basePath === '/stats') return <Stats />
if (basePath === '/list') return <List /> if (basePath === '/list') return <List />
if (basePath === '/add') return <Add /> if (basePath === '/add') return <Add />
if (basePath === '/login') return <Login /> if (basePath === '/login') return <Login />
if (basePath === '/settings') return <Settings /> if (basePath === '/settings') return <Settings />
if (basePath === '/admin') return <Admin /> if (basePath === '/admin') return <Admin />
if (basePath === '/wiki') return <Wiki />
if (basePath.startsWith('/edit')) return <Edit /> if (basePath.startsWith('/edit')) return <Edit />
if (basePath.startsWith('/detail')) return <Detail /> if (basePath.startsWith('/detail')) return <Detail />
return <Home /> return <Home />

View File

@ -1,404 +0,0 @@
/**
* SharePoster - 藏品分享海报组件
* 生成精美的藏品分享图片用于朋友圈分享
* Version: 1.0.0
*/
import React, { useState, useRef } from 'react'
import html2canvas from 'html2canvas'
export default function SharePoster({ collection, onClose }) {
const posterRef = useRef(null)
const [generating, setGenerating] = useState(false)
const [generated, setGenerated] = useState(false)
//
const getNumberCategoryColor = (cat) => {
const colors = {
'圆圆号': '#ef4444',
'倒置号': '#f97316',
'金马王': '#eab308',
'金马号': '#84cc16',
'金山王': '#22c55e',
'天马王': '#14b8a6',
'金山号': '#06b6d4',
'天马号': '#0ea5e9',
'朦胧王': '#3b82f6',
'朦胧号': '#6366f1',
'如意号': '#8b5cf6',
'钻石号': '#a855f7',
'永恒号': '#d946ef',
'带7号': '#ec4899',
'带4号': '#f43f5e',
}
return colors[cat] || '#94a3b8'
}
//
const getStatusColor = (status) => {
const colors = {
'in_collection': '#22c55e',
'selling': '#f59e0b',
'sold': '#ef4444',
'grading': '#06b6d4',
'repairing': '#8b5cf6',
'transit': '#6366f1',
'seeking': '#f97316',
'other': '#94a3b8'
}
return colors[status] || '#94a3b8'
}
//
const getStatusText = (status) => {
const texts = {
'in_collection': '收藏中',
'selling': '出售中',
'sold': '已售',
'grading': '送评中',
'repairing': '修复中',
'transit': '在途中',
'seeking': '寻号中',
'other': '其他'
}
return texts[status] || status
}
//
const formatPrefixSerial = (ps) => {
if (!ps) return '-'
const match = ps.match(/^([A-Z]+)(\d+)$/i)
if (match) {
return match[1].toUpperCase() + ' ' + match[2].match(/.{1,4}/g).join(' ')
}
return ps
}
//
const generatePoster = async () => {
if (!posterRef.current) return
setGenerating(true)
try {
const canvas = await html2canvas(posterRef.current, {
scale: 2,
backgroundColor: '#0f172a',
useCORS: true,
allowTaint: true
})
//
const link = document.createElement('a')
link.download = `龙钞-${collection.code || collection.prefixSerial || '藏品'}.png`
link.href = canvas.toDataURL('image/png')
link.click()
setGenerated(true)
} catch (e) {
console.error('生成海报失败:', e)
alert('生成海报失败,请重试')
}
setGenerating(false)
}
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.9)',
zIndex: 1000,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
{/* 标题栏 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
maxWidth: '400px',
marginBottom: '20px'
}}>
<h3 style={{ color: '#fff', margin: 0, fontSize: '18px' }}>📤 生成分享海报</h3>
<button
onClick={onClose}
style={{
background: 'rgba(255,255,255,0.1)',
border: 'none',
borderRadius: '50%',
width: '36px',
height: '36px',
color: '#fff',
fontSize: '18px',
cursor: 'pointer'
}}
></button>
</div>
{/* 海报预览区域 */}
<div ref={posterRef} style={{
width: '320px',
minHeight: '480px',
background: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)',
borderRadius: '16px',
padding: '20px',
boxSizing: 'border-box',
position: 'relative',
overflow: 'hidden'
}}>
{/* 背景装饰 */}
<div style={{
position: 'absolute',
top: '-50%',
right: '-30%',
width: '200px',
height: '200px',
background: 'radial-gradient(circle, rgba(251,191,36,0.15) 0%, transparent 70%)',
borderRadius: '50%'
}} />
{/* 头部 */}
<div style={{ textAlign: 'center', marginBottom: '20px', position: 'relative' }}>
<div style={{
fontSize: '28px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '4px'
}}>
🐉 甲辰龙钞
</div>
<div style={{ color: '#94a3b8', fontSize: '11px' }}>
一甲辰藏品管理系统
</div>
</div>
{/* 分割线 */}
<div style={{
height: '1px',
background: 'linear-gradient(90deg, transparent, rgba(251,191,36,0.3), transparent)',
marginBottom: '20px'
}} />
{/* 藏品信息 */}
<div style={{ marginBottom: '16px' }}>
{/* 冠字号 - 大字体 */}
<div style={{
textAlign: 'center',
marginBottom: '16px',
padding: '12px',
background: 'rgba(251,191,36,0.1)',
borderRadius: '12px',
border: '1px solid rgba(251,191,36,0.2)'
}}>
<div style={{ color: '#94a3b8', fontSize: '10px', marginBottom: '4px' }}>冠字号</div>
<div style={{
color: '#fbbf24',
fontSize: '24px',
fontFamily: 'monospace',
fontWeight: 'bold',
letterSpacing: '2px'
}}>
{formatPrefixSerial(collection.prefixSerial)}
</div>
{collection.code && (
<div style={{ color: '#64748b', fontSize: '12px', marginTop: '4px' }}>
编号: {collection.code}
</div>
)}
</div>
{/* 基本信息网格 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '16px' }}>
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '10px', borderRadius: '8px' }}>
<div style={{ color: '#64748b', fontSize: '10px' }}>版别</div>
<div style={{ color: '#fff', fontSize: '13px', fontWeight: 'bold' }}>{collection.version || '-'}</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '10px', borderRadius: '8px' }}>
<div style={{ color: '#64748b', fontSize: '10px' }}>包装</div>
<div style={{ color: '#fff', fontSize: '13px', fontWeight: 'bold' }}>{collection.packaging || '-'}</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '10px', borderRadius: '8px' }}>
<div style={{ color: '#64748b', fontSize: '10px' }}>珍惜度</div>
<div style={{ color: '#a78bfa', fontSize: '13px', fontWeight: 'bold' }}>{collection.rarity || '-'}</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '10px', borderRadius: '8px' }}>
<div style={{ color: '#64748b', fontSize: '10px' }}>状态</div>
<div style={{ color: getStatusColor(collection.status), fontSize: '13px', fontWeight: 'bold' }}>
{getStatusText(collection.status)}
</div>
</div>
</div>
{/* 号码分类标签 */}
{collection.numberCategory && (
<div style={{
textAlign: 'center',
marginBottom: '16px',
padding: '8px',
background: getNumberCategoryColor(collection.numberCategory) + '20',
borderRadius: '8px',
border: `1px solid ${getNumberCategoryColor(collection.numberCategory)}40`
}}>
<span style={{
color: getNumberCategoryColor(collection.numberCategory),
fontSize: '16px',
fontWeight: 'bold'
}}>
{collection.numberCategory}
</span>
</div>
)}
{/* 评级信息 */}
{collection.isGraded && (
<div style={{
display: 'flex',
gap: '8px',
marginBottom: '16px',
justifyContent: 'center'
}}>
{collection.gradingCompany && (
<span style={{
background: 'rgba(6,182,212,0.15)',
color: '#06b6d4',
padding: '4px 10px',
borderRadius: '6px',
fontSize: '11px'
}}>
{collection.gradingCompany}
</span>
)}
{collection.gradingScore && (
<span style={{
background: 'rgba(249,115,22,0.2)',
color: '#f97316',
padding: '4px 10px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: 'bold'
}}>
{collection.gradingScore}
</span>
)}
{collection.threeStar && (
<span style={{
background: 'rgba(6,182,212,0.15)',
color: '#06b6d4',
padding: '4px 10px',
borderRadius: '6px',
fontSize: '11px'
}}>
</span>
)}
</div>
)}
{/* 价格信息 */}
<div style={{
display: 'flex',
justifyContent: 'space-around',
padding: '12px',
background: 'rgba(255,255,255,0.03)',
borderRadius: '8px'
}}>
<div style={{ textAlign: 'center' }}>
<div style={{ color: '#64748b', fontSize: '10px' }}>成本</div>
<div style={{ color: '#e2e8f0', fontSize: '14px' }}>
{collection.costPrice ? `¥${collection.costPrice}` : '-'}
</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ color: '#64748b', fontSize: '10px' }}>目标价</div>
<div style={{ color: '#fbbf24', fontSize: '14px' }}>
{collection.targetPrice ? `¥${collection.targetPrice}` : '-'}
</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ color: '#64748b', fontSize: '10px' }}>售价</div>
<div style={{ color: '#4ade80', fontSize: '14px' }}>
{collection.goalPrice ? `¥${collection.goalPrice}` : '-'}
</div>
</div>
</div>
</div>
{/* 底部 */}
<div style={{
position: 'absolute',
bottom: '15px',
left: '20px',
right: '20px',
textAlign: 'center'
}}>
<div style={{
color: '#64748b',
fontSize: '10px',
marginBottom: '4px'
}}>
长按识别二维码了解更多
</div>
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '8px'
}}>
<span style={{ fontSize: '20px' }}>🐉</span>
<span style={{ color: '#94a3b8', fontSize: '11px' }}>jiachenlong.com</span>
</div>
</div>
</div>
{/* 操作按钮 */}
<div style={{
display: 'flex',
gap: '12px',
marginTop: '20px',
width: '100%',
maxWidth: '400px'
}}>
<button
onClick={onClose}
style={{
flex: 1,
padding: '14px',
background: 'rgba(255,255,255,0.1)',
border: 'none',
borderRadius: '12px',
color: '#fff',
fontSize: '15px',
cursor: 'pointer'
}}
>
取消
</button>
<button
onClick={generatePoster}
disabled={generating}
style={{
flex: 2,
padding: '14px',
background: generating ? '#94a3b8' : 'linear-gradient(135deg, #fbbf24, #f59e0b)',
border: 'none',
borderRadius: '12px',
color: '#1e293b',
fontSize: '15px',
fontWeight: 'bold',
cursor: generating ? 'not-allowed' : 'pointer'
}}
>
{generating ? '生成中...' : generated ? '已保存 ✓' : '保存到相册'}
</button>
</div>
</div>
)
}

View File

@ -3,7 +3,7 @@
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx // 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
// 从环境变量读取vite.config.js 注入) // 从环境变量读取vite.config.js 注入)
export const APP_VERSION = import.meta.env.APP_VERSION || '0.0.3' export const APP_VERSION = import.meta.env.APP_VERSION || '1.2.82'
// 版本信息 // 版本信息
export const VERSION_INFO = { export const VERSION_INFO = {

View File

@ -1,7 +1,7 @@
/** /**
* Add - 添加藏品页面 * Add - 添加藏品页面
* Version: 0.0.2 * Version: 1.2.90x
* 更新修复行情录入时评级信息(is_graded, grading_company, grading_score)未保存的问题 * 更新
*/ */
import React, { useState, useRef } from 'react' import React, { useState, useRef } from 'react'
@ -863,15 +863,12 @@ export default function Add() {
size_type: sizeType, size_type: sizeType,
platform: dealForm.platform, platform: dealForm.platform,
seller: dealForm.seller || '', seller: dealForm.seller || '',
buyer: dealForm.buyer || '', buyer: dealForm.buyer || ''
is_graded: !!dealForm.gradingCompany,
grading_company: dealForm.gradingCompany || '',
grading_score: dealForm.gradingScore || ''
}) })
}) })
if (response.ok) { if (response.ok) {
alert('行情录入成功!') alert('行情录入成功!')
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '抖音', seller: '', buyer: '', date: new Date().toISOString().split('T')[0], gradingCompany: '', gradingScore: '' }) setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '抖音', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
} else { } else {
const data = await response.json() const data = await response.json()
alert('录入失败: ' + (data.detail || '未知错误')) alert('录入失败: ' + (data.detail || '未知错误'))
@ -1058,7 +1055,7 @@ export default function Add() {
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
title: `${item.serial}${item.price}`, title: `${item.serial}${item.price}`,
content: `分类: ${item.category || '-'}\n尾号: ${tail_number || '-'}\n大小号: ${size_type || '-'}\n包装: ${item.packaging || '单张'}\n平台: ${item.platform || '-'}\n价格: ¥${item.price}\n出售者: ${item.seller || '-'}\n购买者: ${item.buyer || '-'}\n日期: ${item.deal_date || new Date().toISOString().split('T')[0]}\n评级: ${item.grading_company ? item.grading_company + ' ' + item.grading_score : '未评级'}`, content: `分类: ${item.category || '-'}\n尾号: ${tail_number || '-'}\n大小号: ${size_type || '-'}\n包装: ${item.packaging || '单张'}\n平台: ${item.platform || '-'}\n价格: ¥${item.price}\n出售者: ${item.seller || '-'}\n购买者: ${item.buyer || '-'}\n日期: ${item.deal_date || new Date().toISOString().split('T')[0]}`,
deal_price: parseFloat(item.price), deal_price: parseFloat(item.price),
deal_date: item.deal_date || new Date().toISOString().split('T')[0], deal_date: item.deal_date || new Date().toISOString().split('T')[0],
packaging: item.packaging || '单张', packaging: item.packaging || '单张',
@ -1067,10 +1064,7 @@ export default function Add() {
size_type: size_type, size_type: size_type,
platform: item.platform || '-', platform: item.platform || '-',
seller: item.seller || '', seller: item.seller || '',
buyer: item.buyer || '', buyer: item.buyer || ''
is_graded: !!item.grading_company,
grading_company: item.grading_company || '',
grading_score: item.grading_score || ''
}) })
}) })
count++ count++

View File

@ -1,6 +1,6 @@
/** /**
* Admin - 管理员用户管理页面 * Admin - 管理员用户管理页面
* Version: 0.0.2 (2026-04-19) * Version: 1.2.98 (2026-04-19)
* 更新新增 dealCount 字段显示用户发布的行情数量 * 更新新增 dealCount 字段显示用户发布的行情数量
*/ */
@ -589,7 +589,7 @@ export default function Admin() {
{/* 版本号 - 移到表格下方,避免被底部导航遮挡 */} {/* 版本号 - 移到表格下方,避免被底部导航遮挡 */}
<div style={{ textAlign: 'center', padding: '20px', color: 'rgba(255,255,255,0.3)', fontSize: '12px' }}> <div style={{ textAlign: 'center', padding: '20px', color: 'rgba(255,255,255,0.3)', fontSize: '12px' }}>
文件版本 v0.0.2 | 主版本 v{APP_VERSION} v{APP_VERSION}
</div> </div>
</div> </div>
) )

View File

@ -1,6 +1,6 @@
/** /**
* Detail - 藏品详情页面 * Detail - 藏品详情页面
* Version: 0.0.1 * Version: 1.2.90x
* 更新 * 更新
*/ */
@ -388,9 +388,6 @@ export default function Detail() {
</div> </div>
</div> </div>
)} )}
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.1</div>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
/** /**
* Edit - 编辑藏品页面 * Edit - 编辑藏品页面
* Version: 0.0.1 * Version: 1.2.90x
* 更新 * 更新
*/ */
@ -492,9 +492,6 @@ export default function Edit() {
🔄 返回列表 🔄 返回列表
</button> </button>
</div> </div>
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.1</div>
</div> </div>
) )
} }

View File

@ -1,7 +1,7 @@
/** /**
* Home - 首页 * Home - 首页
* Version: 0.0.4 (2026-04-24) * Version: 1.2.95x
* 更新修复寻配号数据统计API * 更新
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
@ -66,31 +66,27 @@ export default function Home() {
// //
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token') fetch('/api/yichens/stats/dragons-today').then(res => res.json()).then(data => {
const headers = token ? { 'Authorization': 'Bearer ' + token } : {} setDragonStats(data || {})
//
fetch('/api/seek/stats').then(res => res.json()).then(data => {
Promise.all([
fetch('/api/collections/stats', { headers }),
fetch('/api/yichens/posts?limit=1&offset=0')
]).then(([colRes, yichenRes]) => Promise.all([colRes.json(), yichenRes.json()])).then(([colData, yichenData]) => {
const colCount = colData.totalCount || 0
const yichenTotal = yichenData.total || yichenData.pagination?.total || 0
setSeekStats({
seekCount: data.total || 0,
matchedCount: data.matched || 0,
userMatchedCount: data.matched || 0,
totalMatchedCount: colCount + yichenTotal
})
}).catch(() => {
setSeekStats({ seekCount: data.total||0, matchedCount: data.matched||0, userMatchedCount: data.matched||0, totalMatchedCount: 0 })
})
}).catch(() => {}) }).catch(() => {})
fetch('/api/yichens/stats/dragons-today').then(res=>res.json()).then(d=>setDragonStats(d||{})).catch(()=>{}) fetch('/api/yichens/stats/today').then(res => res.json()).then(data => {
fetch('/api/yichens/stats/today').then(res=>res.json()).then(d=>setYichensStats(d||{})).catch(()=>{}) setYichensStats(data || {})
fetch('/api/yichens/posts?limit=20&offset=0').then(res=>res.json()).then(d=>setRecentPosts(d.posts||d||[])).catch(()=>{}) }).catch(() => {})
//
fetch('/api/yichens/stats/dragons-today').then(res => res.json()).then(data => {
setDragonStats(data || {})
}).catch(() => {})
fetch('/api/yichens/posts?limit=20&offset=0').then(res => res.json()).then(data => {
setRecentPosts(data.posts || data || [])
}).catch(() => {})
//
fetch('/api/information/seek/stats').then(res => res.json()).then(data => {
setSeekStats(data || {})
}).catch(() => {})
}, []) }, [])
// //
@ -182,10 +178,9 @@ export default function Home() {
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>快捷操作</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>快捷操作</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
<div onClick={() => window.location.hash = '#/add?mode=ocr'} style={{ <div onClick={() => window.location.hash = '#/add?mode=ocr'} style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
border: '1px solid rgba(255,255,255,0.08)',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -193,14 +188,13 @@ export default function Home() {
justifyContent: 'center', justifyContent: 'center',
textAlign: 'center' textAlign: 'center'
}}> }}>
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>藏品录入</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>藏品录入</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px', marginTop: '2px' }}>AI识别添加</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>AI识别添加</div>
</div> </div>
<div onClick={() => window.location.hash = '#/add?mode=deal'} style={{ <div onClick={() => window.location.hash = '#/add?mode=deal'} style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', background: 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
border: '1px solid rgba(255,255,255,0.08)',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -208,14 +202,13 @@ export default function Home() {
justifyContent: 'center', justifyContent: 'center',
textAlign: 'center' textAlign: 'center'
}}> }}>
<div style={{ color: '#ec4899', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>行情录入</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>行情录入</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px', marginTop: '2px' }}>录入成交行情</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>录入成交行情</div>
</div> </div>
<div onClick={() => window.location.hash = '#/news?type=seek'} style={{ <div onClick={() => window.location.hash = '#/news?type=seek'} style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
border: '1px solid rgba(255,255,255,0.08)',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -223,14 +216,13 @@ export default function Home() {
justifyContent: 'center', justifyContent: 'center',
textAlign: 'center' textAlign: 'center'
}}> }}>
<div style={{ color: '#f59e0b', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布寻号</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布寻号</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div>
</div> </div>
<div onClick={() => window.location.hash = '#/purchase'} style={{ <div onClick={() => window.location.hash = '#/add?mode=manual'} style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
border: '1px solid rgba(255,255,255,0.08)',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -238,23 +230,8 @@ export default function Home() {
justifyContent: 'center', justifyContent: 'center',
textAlign: 'center' textAlign: 'center'
}}> }}>
<div style={{ color: '#3b82f6', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>我的认购</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布藏品</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px', marginTop: '2px' }}>认购群入口</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>手动发布</div>
</div>
<div onClick={() => window.location.hash = '#/wiki'} style={{
background: 'linear-gradient(135deg, rgba(251,191,36,0.15) 0%, rgba(251,191,36,0.05) 100%)',
borderRadius: '12px',
padding: '12px 16px',
border: '1px solid rgba(251,191,36,0.2)',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
<div style={{ color: '#fbbf24', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>📚 龙钞百科</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px', marginTop: '2px' }}>新手入门指南</div>
</div> </div>
</div> </div>
</div> </div>
@ -280,7 +257,7 @@ export default function Home() {
</div> </div>
<div> <div>
<div style={{ color: '#a78bfa', fontSize: '18px', fontWeight: '700' }}>{seekStats.totalMatchedCount || 0}</div> <div style={{ color: '#a78bfa', fontSize: '18px', fontWeight: '700' }}>{seekStats.totalMatchedCount || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px' }}>总共匹配数据</div> <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px' }}>总共匹配</div>
</div> </div>
</div> </div>
@ -422,16 +399,14 @@ export default function Home() {
{(isAdmin ? [ {(isAdmin ? [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 }, { icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 }, { icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
{ icon: '🎯', label: '认购', hash: '#/purchase', idx: 2 }, { icon: '', label: '录入', hash: '#/ocr', idx: 2 },
{ icon: '', label: '录入', hash: '#/ocr', idx: 3 }, { icon: '📊', label: '统计', hash: '#/stats', idx: 3 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 4 }, { icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 }
{ icon: '⚙️', label: '管理', hash: '#/admin', idx: 5 }
] : [ ] : [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 }, { icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 }, { icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
{ icon: '🎯', label: '认购', hash: '#/purchase', idx: 2 }, { icon: '', label: '录入', hash: '#/ocr', idx: 2 },
{ icon: '', label: '录入', hash: '#/ocr', idx: 3 }, { icon: '📊', label: '统计', hash: '#/stats', idx: 3 }
{ icon: '📊', label: '统计', hash: '#/stats', idx: 4 }
]).map((item) => ( ]).map((item) => (
<div key={item.idx} onClick={() => window.location.hash = item.hash} style={{ <div key={item.idx} onClick={() => window.location.hash = item.hash} style={{
display: 'flex', display: 'flex',
@ -446,17 +421,6 @@ export default function Home() {
))} ))}
</div> </div>
<div style={{ height: '70px' }}></div> <div style={{ height: '70px' }}></div>
{/* 版本号显示 */}
<div style={{
position: 'fixed',
bottom: '70px',
right: '12px',
color: 'rgba(255,255,255,0.2)',
fontSize: '10px'
}}>
v0.0.2
</div>
</div> </div>
) )
} }

View File

@ -1,12 +1,11 @@
/** /**
* List - 藏品列表页面 * List - 藏品列表页面
* Version: 0.1.7 * Version: 1.2.93x
* 更新新增分享海报功能 * 更新
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version' import { APP_VERSION } from '../config/version'
import SharePoster from '../components/SharePoster'
const API_BASE = localStorage.getItem('API_BASE') || '' const API_BASE = localStorage.getItem('API_BASE') || ''
export default function List() { export default function List() {
@ -31,11 +30,6 @@ export default function List() {
const [dealSortOrder, setDealSortOrder] = useState('desc') const [dealSortOrder, setDealSortOrder] = useState('desc')
const [myDeals, setMyDeals] = useState([]) const [myDeals, setMyDeals] = useState([])
const [dealsLoading, setDealsLoading] = useState(false) const [dealsLoading, setDealsLoading] = useState(false)
//
const [dealVersionFilter, setDealVersionFilter] = useState('') //
const [dealPackagingFilter, setDealPackagingFilter] = useState('') //
const [dealNumberCatFilter, setDealNumberCatFilter] = useState('') //
const [shareCollection, setShareCollection] = useState(null) //
useEffect(() => { useEffect(() => {
// //
@ -112,33 +106,10 @@ export default function List() {
// //
const filteredDeals = myDeals.filter(deal => { const filteredDeals = myDeals.filter(deal => {
// 1. if (!dealSearch) return true
if (dealSearch) {
const s = dealSearch.toLowerCase().trim() const s = dealSearch.toLowerCase().trim()
const fields = [deal.title, deal.content, deal.category, deal.packaging, deal.grading_company, deal.grading_score, deal.deal_no, deal.seller, deal.platform, deal.buyer, deal.deal_price?.toString(), deal.deal_date].filter(v => v).map(v => v.toString().toLowerCase()) const fields = [deal.title, deal.content, deal.category, deal.packaging, deal.grading_company, deal.grading_score, deal.deal_no, deal.seller, deal.platform, deal.buyer, deal.deal_price?.toString(), deal.deal_date].filter(v => v).map(v => v.toString().toLowerCase())
if (!fields.some(f => f.includes(s))) return false return fields.some(f => f.includes(s))
}
// 2. - content
if (dealVersionFilter) {
let version = ''
if (deal.content) {
const match = deal.content.match(/版别:\s*([^\n]+)/)
if (match) version = match[1].trim()
}
if (version !== dealVersionFilter) return false
}
// 3.
if (dealPackagingFilter && deal.packaging !== dealPackagingFilter) return false
// 4. - content
if (dealNumberCatFilter) {
let numberCat = ''
if (deal.content) {
const match = deal.content.match(/号码分类:\s*([^\n]+)/)
if (match) numberCat = match[1].trim()
}
if (numberCat !== dealNumberCatFilter) return false
}
return true
}).sort((a, b) => { }).sort((a, b) => {
let aVal = a[dealSortField] || '' let aVal = a[dealSortField] || ''
let bVal = b[dealSortField] || '' let bVal = b[dealSortField] || ''
@ -499,26 +470,7 @@ export default function List() {
} }
const ListItem = ({ item }) => ( const ListItem = ({ item }) => (
<div onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '10px', marginBottom: '8px', cursor: 'pointer', position: 'relative' }}> <div onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '10px', marginBottom: '8px', cursor: 'pointer' }}>
{/* 分享按钮 */}
<button
onClick={(e) => { e.stopPropagation(); setShareCollection(item) }}
style={{
position: 'absolute',
top: '8px',
right: '8px',
background: 'rgba(251, 191, 36, 0.15)',
border: 'none',
borderRadius: '6px',
padding: '4px 8px',
fontSize: '12px',
cursor: 'pointer',
color: '#fbbf24',
zIndex: 10
}}
>
📤 分享
</button>
{/* 第1行编号 + 冠字号 + 包装(蓝色) | 状态 + 持仓类型 + 珍惜度(紫色) */} {/* 第1行编号 + 冠字号 + 包装(蓝色) | 状态 + 持仓类型 + 珍惜度(紫色) */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
@ -708,13 +660,13 @@ export default function List() {
{/* 行情搜索框 */} {/* 行情搜索框 */}
<div style={{ marginBottom: '12px' }}> <div style={{ marginBottom: '12px' }}>
<input type="text" <input type="text"
placeholder="🔍 搜索冠字号、编号、价格..." placeholder="🔍 搜索行情..."
value={dealSearch} value={dealSearch}
onChange={e => setDealSearch(e.target.value)} onChange={e => setDealSearch(e.target.value)}
style={{ style={{
width: '100%', width: '100%',
background: 'rgba(255,255,255,0.05)', background: 'rgba(255,255,255,0.05)',
border: dealSearch ? '1px solid rgba(59, 130, 246, 0.5)' : '1px solid rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.1)',
color: '#fff', color: '#fff',
padding: '10px 12px', padding: '10px 12px',
borderRadius: '8px', borderRadius: '8px',
@ -723,134 +675,6 @@ export default function List() {
boxSizing: 'border-box' boxSizing: 'border-box'
}} }}
/> />
{dealSearch && (
<button
onClick={() => setDealSearch('')}
style={{
position: 'absolute',
right: '24px',
top: '50%',
transform: 'translateY(-50%)',
background: 'rgba(255,255,255,0.1)',
border: 'none',
borderRadius: '50%',
width: '24px',
height: '24px',
color: '#fff',
fontSize: '12px',
cursor: 'pointer'
}}
>
</button>
)}
</div>
{/* 筛选条件按钮 */}
<div style={{ marginBottom: '12px', padding: '10px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
{/* 第一行:版别筛选 */}
<div style={{ marginBottom: '8px' }}>
<span style={{ color: '#94a3b8', fontSize: '11px', marginRight: '8px' }}>版别:</span>
{[
{ value: '', label: '全部' },
{ value: '20', label: '20版' },
{ value: '19', label: '19版' },
{ value: '18', label: '18版' },
{ value: '17', label: '17版' },
{ value: '16', label: '16版' },
].map(item => (
<button key={item.value} onClick={() => setDealVersionFilter(item.value)}
style={{
marginRight: '6px',
marginBottom: '4px',
padding: '4px 10px',
borderRadius: '6px',
fontSize: '11px',
cursor: 'pointer',
background: dealVersionFilter === item.value ? '#fbbf24' : 'rgba(255,255,255,0.08)',
color: dealVersionFilter === item.value ? '#1e293b' : '#94a3b8',
border: dealVersionFilter === item.value ? 'none' : '1px solid rgba(255,255,255,0.1)'
}}>
{item.label}
</button>
))}
</div>
{/* 第二行:包装类型筛选 */}
<div style={{ marginBottom: '8px' }}>
<span style={{ color: '#94a3b8', fontSize: '11px', marginRight: '8px' }}>包装:</span>
{[
{ value: '', label: '全部' },
{ value: '单张', label: '单张' },
{ value: '标十', label: '标十' },
{ value: '标百', label: '标百' },
].map(item => (
<button key={item.value} onClick={() => setDealPackagingFilter(item.value)}
style={{
marginRight: '6px',
marginBottom: '4px',
padding: '4px 10px',
borderRadius: '6px',
fontSize: '11px',
cursor: 'pointer',
background: dealPackagingFilter === item.value ? '#a78bfa' : 'rgba(255,255,255,0.08)',
color: dealPackagingFilter === item.value ? '#fff' : '#94a3b8',
border: dealPackagingFilter === item.value ? 'none' : '1px solid rgba(255,255,255,0.1)'
}}>
{item.label}
</button>
))}
</div>
{/* 第三行:号码分类筛选 */}
<div>
<span style={{ color: '#94a3b8', fontSize: '11px', marginRight: '8px' }}>号码:</span>
{[
{ value: '', label: '全部' },
{ value: '圆圆号', label: '圆圆号' },
{ value: '倒置号', label: '倒置号' },
{ value: '金马号', label: '金马号' },
{ value: '天马号', label: '天马号' },
{ value: '钻石号', label: '钻石号' },
{ value: '永恒号', label: '永恒号' },
{ value: '带7号', label: '带7号' },
{ value: '带4号', label: '带4号' },
].map(item => (
<button key={item.value} onClick={() => setDealNumberCatFilter(item.value)}
style={{
marginRight: '6px',
marginBottom: '4px',
padding: '4px 10px',
borderRadius: '6px',
fontSize: '11px',
cursor: 'pointer',
background: dealNumberCatFilter === item.value ? '#22c55e' : 'rgba(255,255,255,0.08)',
color: dealNumberCatFilter === item.value ? '#fff' : '#94a3b8',
border: dealNumberCatFilter === item.value ? 'none' : '1px solid rgba(255,255,255,0.1)'
}}>
{item.label}
</button>
))}
</div>
{/* 清除筛选按钮 */}
{(dealVersionFilter || dealPackagingFilter || dealNumberCatFilter) && (
<button
onClick={() => {
setDealVersionFilter('')
setDealPackagingFilter('')
setDealNumberCatFilter('')
}}
style={{
marginTop: '8px',
padding: '4px 10px',
borderRadius: '6px',
fontSize: '11px',
cursor: 'pointer',
background: 'rgba(239, 68, 68, 0.2)',
color: '#ef4444',
border: 'none'
}}>
清除筛选
</button>
)}
</div> </div>
{dealsLoading ? ( {dealsLoading ? (
@ -858,16 +682,7 @@ export default function List() {
) : filteredDeals.length === 0 ? ( ) : filteredDeals.length === 0 ? (
<div style={{ textAlign: 'center', padding: '60px 0' }}> <div style={{ textAlign: 'center', padding: '60px 0' }}>
<div style={{ fontSize: '50px', opacity: 0.3 }}>📊</div> <div style={{ fontSize: '50px', opacity: 0.3 }}>📊</div>
<div style={{ color: '#64748b', marginTop: '16px' }}> <div style={{ color: '#64748b', marginTop: '16px' }}>{dealSearch ? '没有匹配的行情' : '暂无行情记录'}</div>
{(dealSearch || dealVersionFilter || dealPackagingFilter || dealNumberCatFilter)
? '没有匹配的行情'
: '暂无行情记录'}
</div>
{(dealSearch || dealVersionFilter || dealPackagingFilter || dealNumberCatFilter) && (
<div style={{ color: '#64748b', fontSize: '11px', marginTop: '8px' }}>
已选筛选{dealSearch && `搜索"${dealSearch}" `}{dealVersionFilter && `版别${dealVersionFilter} `}{dealPackagingFilter && `包装${dealPackagingFilter} `}{dealNumberCatFilter && `号码${dealNumberCatFilter}`}
</div>
)}
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
@ -1134,17 +949,6 @@ function DealListItem({ deal, onRefresh }) {
)} )}
</div> </div>
)} )}
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.1</div>
{/* 分享海报弹窗 */}
{shareCollection && (
<SharePoster
collection={shareCollection}
onClose={() => setShareCollection(null)}
/>
)}
</div> </div>
) )
} }

View File

@ -1,7 +1,7 @@
/** /**
* Login - 登录页面 * Login - 登录页面
* Version: 0.0.2 (2026-06-13) * Version: 1.2.85x
* 更新移除注册时的手机号和短信验证功能 * 更新
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
@ -23,22 +23,79 @@ export default function Login() {
password: '', password: '',
confirmPassword: '', confirmPassword: '',
email: '', email: '',
phone: '',
verifyCode: '',
inviteCode: '' inviteCode: ''
}) })
const [registerLoading, setRegisterLoading] = useState(false) const [registerLoading, setRegisterLoading] = useState(false)
const [registerError, setRegisterError] = useState('') const [registerError, setRegisterError] = useState('')
//
const [sendingCode, setSendingCode] = useState(false)
const [codeCountdown, setCodeCountdown] = useState(0)
const [codeSent, setCodeSent] = useState(false)
// //
useEffect(() => { useEffect(() => {
generateCaptcha() generateCaptcha()
}, []) }, [])
//
useEffect(() => {
if (codeCountdown > 0) {
const timer = setTimeout(() => setCodeCountdown(codeCountdown - 1), 1000)
return () => clearTimeout(timer)
}
}, [codeCountdown])
const generateCaptcha = () => { const generateCaptcha = () => {
const num1 = Math.floor(Math.random() * 10) const num1 = Math.floor(Math.random() * 10)
const num2 = Math.floor(Math.random() * 10) const num2 = Math.floor(Math.random() * 10)
setCaptcha({ num1, num2, answer: '' }) setCaptcha({ num1, num2, answer: '' })
} }
//
const handleSendCode = async () => {
if (!registerData.phone) {
setRegisterError('请先输入手机号')
return
}
//
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(registerData.phone)) {
setRegisterError('请输入正确的手机号')
return
}
setSendingCode(true)
setRegisterError('')
try {
const res = await fetch('/api/auth/send-verification-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ phone: registerData.phone })
})
const data = await res.json()
if (data.success) {
setCodeSent(true)
setCodeCountdown(60)
setRegisterError('')
} else {
setRegisterError(data.message || '发送失败')
}
} catch (err) {
setRegisterError('发送失败,请稍后重试')
} finally {
setSendingCode(false)
}
}
// //
const handleRegister = async () => { const handleRegister = async () => {
// //
@ -67,13 +124,33 @@ export default function Login() {
return return
} }
//
if (!registerData.phone) {
setRegisterError('手机号为必填项')
return
}
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(registerData.phone)) {
setRegisterError('请输入正确的手机号')
return
}
//
if (!registerData.verifyCode) {
setRegisterError('请输入短信获取')
return
}
setRegisterLoading(true) setRegisterLoading(true)
setRegisterError('') setRegisterError('')
try { try {
const payload = { const payload = {
username: registerData.username, username: registerData.username,
password: registerData.password password: registerData.password,
phone: registerData.phone,
verifyCode: registerData.verifyCode
} }
if (registerData.email) { if (registerData.email) {
payload.email = registerData.email payload.email = registerData.email
@ -98,7 +175,9 @@ export default function Login() {
alert('注册成功!请登录') alert('注册成功!请登录')
setShowRegister(false) setShowRegister(false)
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', inviteCode: '' }) setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '', inviteCode: '' })
setCodeSent(false)
setCodeCountdown(0)
generateCaptcha() generateCaptcha()
} catch (err) { } catch (err) {
console.error('注册错误:', err) console.error('注册错误:', err)
@ -412,6 +491,68 @@ export default function Login() {
/> />
</div> </div>
{/* 手机号 */}
<div style={{ marginBottom: '16px' }}>
<input
type="tel"
value={registerData.phone}
onChange={(e) => setRegisterData(prev => ({ ...prev, phone: e.target.value }))}
placeholder="手机号11位"
maxLength={11}
style={{
width: '100%',
padding: '14px',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.2)',
background: 'rgba(255,255,255,0.05)',
color: '#fff',
fontSize: '16px',
outline: 'none'
}}
/>
</div>
{/* 短信获取 */}
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<input
type="text"
value={registerData.verifyCode}
onChange={(e) => setRegisterData(prev => ({ ...prev, verifyCode: e.target.value }))}
placeholder="短信验证码"
maxLength={6}
style={{
flex: 1, minWidth: "90px", width: "auto",
padding: '14px',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.2)',
background: 'rgba(255,255,255,0.05)',
color: '#fff',
fontSize: '16px',
outline: 'none'
}}
/>
<button
onClick={handleSendCode}
disabled={sendingCode || codeCountdown > 0}
style={{
minWidth: "90px", width: "auto",
padding: '14px',
borderRadius: '8px',
border: 'none',
background: codeCountdown > 0 ? 'rgba(148, 163, 184, 0.3)' : 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
color: '#fff',
fontSize: '14px',
fontWeight: '600',
cursor: codeCountdown > 0 ? 'not-allowed' : 'pointer',
whiteSpace: 'nowrap'
}}
>
{codeCountdown > 0 ? `${codeCountdown}` : sendingCode ? '发送中...' : '获取验证码'}
</button>
</div>
</div>
{/* 邮箱(可选) */} {/* 邮箱(可选) */}
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<input <input
@ -515,8 +656,10 @@ export default function Login() {
<button <button
onClick={() => { onClick={() => {
setShowRegister(false) setShowRegister(false)
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', inviteCode: '' }) setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '', inviteCode: '' })
setRegisterError('') setRegisterError('')
setCodeSent(false)
setCodeCountdown(0)
}} }}
style={{ style={{
flex: 1, minWidth: "90px", width: "auto", flex: 1, minWidth: "90px", width: "auto",
@ -553,9 +696,6 @@ export default function Login() {
</div> </div>
</div> </div>
)} )}
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.1</div>
</div> </div>
) )
} }

View File

@ -1,7 +1,7 @@
/** /**
* News - 资讯列表页面 * News - 资讯列表页面
* Version: 0.1.2 (2026-05-13) * Version: 1.2.80x
* 更新移除成交行情价格统计表格增加获取数量到500 * 更新
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
@ -39,15 +39,11 @@ export default function News() {
const [matchedStatus, setMatchedStatus] = useState({}) // const [matchedStatus, setMatchedStatus] = useState({}) //
const [showMatchList, setShowMatchList] = useState(false) const [showMatchList, setShowMatchList] = useState(false)
const [dealVersion, setDealVersion] = useState('龙钞') const [dealVersion, setDealVersion] = useState('龙钞')
const [dealPackagingFilter, setDealPackagingFilter] = useState('') //
const [dealNumberCatFilter, setDealNumberCatFilter] = useState('') //
const [dealDate, setDealDate] = useState('') const [dealDate, setDealDate] = useState('')
const [dealDetailItems, setDealDetailItems] = useState(null) // const [dealDetailItems, setDealDetailItems] = useState(null) //
const [matchCollections, setMatchCollections] = useState([]) const [matchCollections, setMatchCollections] = useState([])
const [networkMatchCollections, setNetworkMatchCollections] = useState([]) const [networkMatchCollections, setNetworkMatchCollections] = useState([])
const [showNetworkMatchList, setShowNetworkMatchList] = useState(false) const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
const [networkMatchInfoId, setNetworkMatchInfoId] = useState(null)
const [networkMatchTotalCount, setNetworkMatchTotalCount] = useState(0)
const [customModal, setCustomModal] = useState({show: false, title: '', content: ''}) const [customModal, setCustomModal] = useState({show: false, title: '', content: ''})
const [seekForm, setSeekForm] = useState({ const [seekForm, setSeekForm] = useState({
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || '' edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || ''
@ -75,10 +71,9 @@ export default function News() {
fetchInfoList() fetchInfoList()
}, [activeTab, dealDate]) }, [activeTab, dealDate])
// 8 //
useEffect(() => { useEffect(() => {
const features = (seekForm.features || '').slice(0, 8).padEnd(8, 'X') const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${seekForm.features || 'XXXXXXXX'}`
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${features}`
setSeekForm(prev => ({...prev, title})) setSeekForm(prev => ({...prev, title}))
}, [seekForm.edition, seekForm.features]) }, [seekForm.edition, seekForm.features])
@ -97,7 +92,7 @@ export default function News() {
? `${API_BASE}/api/seek/list` ? `${API_BASE}/api/seek/list`
: `${API_BASE}/api/deal/list` : `${API_BASE}/api/deal/list`
// // 500
if (activeTab === 'deal') { if (activeTab === 'deal') {
url += (url.includes('?') ? '&' : '?') + 'page_size=500' url += (url.includes('?') ? '&' : '?') + 'page_size=500'
if (dealDate) { if (dealDate) {
@ -149,7 +144,7 @@ export default function News() {
const fetchComments = async (infoId) => { const fetchComments = async (infoId) => {
// //
try { try {
const res = await fetch(`${API_BASE}/api/seek/comments/${infoId}`) const res = await fetch(`${API_BASE}/api/information/comments/${infoId}`)
const data = await res.json() const data = await res.json()
setComments(prev => ({...prev, [infoId]: data || []})) setComments(prev => ({...prev, [infoId]: data || []}))
} catch (e) { } catch (e) {
@ -163,7 +158,7 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const res = await fetch(`${API_BASE}/api/seek/comment`, { const res = await fetch(`${API_BASE}/api/information/comment`, {
method: 'POST', method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ information_id: infoId, content: commentText }) body: JSON.stringify({ information_id: infoId, content: commentText })
@ -199,7 +194,7 @@ export default function News() {
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const phone = getUserPhone() const phone = getUserPhone()
const res = await fetch(`${API_BASE}/api/seek/match-confirm`, { const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, {
method: 'POST', method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' }) body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' })
@ -236,7 +231,7 @@ export default function News() {
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
} }
try { try {
const res = await fetch(`${API_BASE}/api/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch(`${API_BASE}/api/information/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Matched user response:', res.status) console.log('Matched user response:', res.status)
const data = await res.json() const data = await res.json()
console.log('Matched user data:', data) console.log('Matched user data:', data)
@ -249,7 +244,7 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const res = await fetch(`${API_BASE}/api/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch(`${API_BASE}/api/information/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Publisher response:', res.status) console.log('Publisher response:', res.status)
const data = await res.json() const data = await res.json()
console.log('Publisher data:', data) console.log('Publisher data:', data)
@ -261,7 +256,7 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const res = await fetch(`${API_BASE}/api/seek/my-match?info_id=${infoId}`, { const res = await fetch(`${API_BASE}/api/information/seek/match?info_id=${infoId}`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}) })
const data = await res.json() const data = await res.json()
@ -279,7 +274,7 @@ export default function News() {
// //
const fetchNetworkMatchCollections = async (infoId) => { const fetchNetworkMatchCollections = async (infoId) => {
try { try {
const res = await fetch(`${API_BASE}/api/seek/network-match/${infoId}`) const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
const data = await res.json() const data = await res.json()
console.log('网络数据匹配结果:', data) console.log('网络数据匹配结果:', data)
setNetworkMatchCollections(data.collections || []) setNetworkMatchCollections(data.collections || [])
@ -335,7 +330,7 @@ export default function News() {
try { try {
// //
const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact
await fetch(`${API_BASE}/api/seek/${editingSeek.id}`, { await fetch(`${API_BASE}/api/information/${editingSeek.id}`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@ -359,13 +354,14 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
// editioncategory // editioncategory
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' } const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
const res = await fetch(`${API_BASE}/api/seek/`, { const res = await fetch(`${API_BASE}/api/information/`, {
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
title: seekForm.title, title: seekForm.title,
content, content,
info_type: 'seek',
expect_category: seekForm.edition, expect_category: seekForm.edition,
expect_number: seekForm.features ? 'J0' + seekForm.features : null expect_number: seekForm.features ? 'J0' + seekForm.features : None
}) })
}) })
const data = await res.json() const data = await res.json()
@ -455,22 +451,12 @@ export default function News() {
onChange={(e) => { onChange={(e) => {
if (i < 2) return if (i < 2) return
const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '') const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '')
if (!val) {
//
const newFeatures = (seekForm.features || '').split('') const newFeatures = (seekForm.features || '').split('')
newFeatures[i - 2] = ''
setSeekForm({...seekForm, features: newFeatures.join('').slice(0, 8)})
return
}
// 8
let currentLen = (seekForm.features || '').length
if (currentLen >= 8 && i - 2 >= currentLen) return // 8
const newFeatures = (seekForm.features || '').slice(0, 8).split('')
while (newFeatures.length < 8) newFeatures.push('') while (newFeatures.length < 8) newFeatures.push('')
newFeatures[i - 2] = val newFeatures[i - 2] = val
setSeekForm({...seekForm, features: newFeatures.join('').slice(0, 8)}) setSeekForm({...seekForm, features: newFeatures.join('')})
// //
if (i < 9) { if (val && i < 9) {
setTimeout(() => { setTimeout(() => {
const nextInput = document.querySelector(`input[data-index="${i+1}"]`) const nextInput = document.querySelector(`input[data-index="${i+1}"]`)
if (nextInput) nextInput.focus() if (nextInput) nextInput.focus()
@ -560,22 +546,6 @@ export default function News() {
else if (serial.startsWith('J1')) version = '马钞' else if (serial.startsWith('J1')) version = '马钞'
else if (serial.startsWith('J3')) version = '蛇钞' else if (serial.startsWith('J3')) version = '蛇钞'
if (version !== dealVersion) return false if (version !== dealVersion) return false
//
if (dealPackagingFilter) {
const content = item.content || ''
const p = content.includes('包装:') ? content.split('包装:')[1].split('\n')[0].trim() : (item.packaging || '')
if (p !== dealPackagingFilter) return false
}
//
if (dealNumberCatFilter) {
const content = item.content || ''
let c = content.includes('分类:') ? content.split('分类:')[1].split('\n')[0].trim() : (item.category || '')
c = normalizeCat(c)
if (c !== dealNumberCatFilter) return false
}
return true return true
}) })
@ -588,21 +558,13 @@ export default function News() {
return p === pkg && c === cat return p === pkg && c === cat
}) })
if (items.length === 0) return null if (items.length === 0) return null
// const sum = items.reduce((a, b) => a + (b.deal_price || 0), 0)
const sortedItems = [...items].sort((a, b) => { return { avg: Math.round(sum / items.length), count: items.length, items }
const timeA = a.created_at || '1970-01-01T00:00:00'
const timeB = b.created_at || '1970-01-01T00:00:00'
return timeB.localeCompare(timeA)
})
const sum = sortedItems.reduce((a, b) => a + (b.deal_price || 0), 0)
return { avg: Math.round(sum / sortedItems.length), count: sortedItems.length, items: sortedItems }
} }
return ( return (
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}> <div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
{/* 版别筛选 */}
<div style={{ color: '#94a3b8', fontSize: '11px', marginBottom: '8px' }}>版别筛选:</div>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}> <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}>
{versions.map(v => ( {versions.map(v => (
<button key={v} onClick={() => setDealVersion(v)} <button key={v} onClick={() => setDealVersion(v)}
@ -613,52 +575,60 @@ export default function News() {
</button> </button>
))} ))}
</div> </div>
{/* 包装类型筛选 */}
<div style={{ color: '#94a3b8', fontSize: '11px', marginBottom: '8px' }}>包装筛选:</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '12px' }}>
{[{value: '', label: '全部'}, ...packagings.map(p => ({value: p, label: p}))].map(item => (
<button key={item.value} onClick={() => setDealPackagingFilter(item.value)}
style={{ padding: '4px 12px', borderRadius: '6px', border: 'none', cursor: 'pointer',
background: dealPackagingFilter === item.value ? '#8b5cf6' : 'rgba(255,255,255,0.1)',
color: '#fff', fontSize: '12px' }}>
{item.label}
</button>
))}
</div> </div>
{/* 号码分类筛选 */} <div style={{ overflowX: 'auto' }}>
<div style={{ color: '#94a3b8', fontSize: '11px', marginBottom: '8px' }}>号码分类:</div> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '12px' }}> <thead>
{[ <tr>
{value: '', label: '全部'}, <th style={{ padding: '8px', textAlign: 'left', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}></th>
{value: '带4号', label: '带4号'}, {packagings.map(p => (
{value: '带7号', label: '带7号'}, <th key={p} style={{ padding: '8px', textAlign: 'center', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>{p}</th>
{value: '永恒号', label: '永恒'},
{value: '钻石号', label: '钻石'},
{value: '天马号', label: '天马'},
{value: '金山号', label: '金山'},
{value: '金马号', label: '金马'},
{value: '倒置号', label: '倒置'},
{value: '圆圆号', label: '圆圆'}
].map(item => (
<button key={item.value} onClick={() => setDealNumberCatFilter(item.value)}
style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', cursor: 'pointer',
background: dealNumberCatFilter === item.value ? '#22c55e' : 'rgba(255,255,255,0.08)',
color: '#fff', fontSize: '11px' }}>
{item.label}
</button>
))} ))}
</div> </tr>
</thead>
<tbody>
{(() => {
//
const catAvg = categories.map(cat => {
const prices = []
packagings.forEach(pkg => {
const d = calcAvg(pkg, cat)
if (d) prices.push(d.avg)
})
const avg = prices.length > 0 ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0
return { cat, avg }
}).filter(c => c.avg > 0)
//
catAvg.sort((a, b) => a.avg - b.avg)
const categoryOrder = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号']
const sortedCats = categoryOrder.filter(cat => catAvg.some(c => c.cat === cat))
.concat(catAvg.filter(c => !categoryOrder.includes(c.cat)).map(c => c.cat))
{/* 清除筛选 */} return sortedCats.map(cat => {
{(dealPackagingFilter || dealNumberCatFilter) && ( const rowData = packagings.map(pkg => calcAvg(pkg, cat))
<button onClick={() => { setDealPackagingFilter(''); setDealNumberCatFilter(''); }} const hasData = rowData.some(d => d !== null)
style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', cursor: 'pointer', if (!hasData) return null
background: 'rgba(239,68,68,0.2)', color: '#ef4444', fontSize: '11px' }}> return (
清除筛选 <tr key={cat}>
</button> <td style={{ padding: '8px', color: '#fbbf24', fontWeight: '500', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{cat}</td>
)} {rowData.map((d, i) => (
<td key={i} style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
{d ? (
<div style={{ color: '#22c55e', fontWeight: '600', cursor: 'pointer' }}
onClick={() => setDealDetailItems(d.items)}>
¥{d.avg.toLocaleString()}
<span style={{ color: '#64748b', fontSize: '11px', marginLeft: '4px' }}>({d.count})</span>
</div>
) : <span style={{ color: '#475569' }}>-</span>}
</td>
))}
</tr>
)
})
})()}
</tbody>
</table>
</div> </div>
</div> </div>
) )
@ -681,10 +651,7 @@ export default function News() {
style={{ background: 'none', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}></button> style={{ background: 'none', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}></button>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{/* 按创建时间倒序排序 */} {([...dealDetailItems].sort((a, b) => (a.deal_price || 0) - (b.deal_price || 0))).map((item, idx) => {
{dealDetailItems
.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''))
.map((item, idx) => {
const content = item.content || '' const content = item.content || ''
const grade = content.includes('评级:') ? content.split('评级:')[1].split('\n')[0].trim() : (item.grading_score || '') const grade = content.includes('评级:') ? content.split('评级:')[1].split('\n')[0].trim() : (item.grading_score || '')
const sizeMatch = content.match(/大小号:\s*(.+?)(?:\n|$)/) const sizeMatch = content.match(/大小号:\s*(.+?)(?:\n|$)/)
@ -717,42 +684,7 @@ export default function News() {
</div> </div>
)} )}
{infoList.filter(item => { {infoList.map(item => {
// tab -
if (activeTab === 'deal') {
const content = item.content || ''
const serial = (item.title || '').split('-')[0] || ''
//
let version = '其他'
if (serial.startsWith('J0')) version = '龙钞'
else if (serial.startsWith('J1')) version = '马钞'
else if (serial.startsWith('J3')) version = '蛇钞'
if (version !== dealVersion) return false
//
const packagingMatch = content.match(/包装:\s*(.+?)(?:\n|$)/)
const packaging = packagingMatch ? packagingMatch[1].trim() : (item.packaging || '')
if (dealPackagingFilter && packaging !== dealPackagingFilter) return false
//
const categoryMatch = content.match(/分类:\s*(.+?)(?:\n|$)/)
let category = categoryMatch ? categoryMatch[1].trim() : (item.category || '')
//
if (category === '通货' || category === '无4') category = '带4号'
if (category === '永恒') category = '永恒号'
if (category === '钻石') category = '钻石号'
if (category === '天马' || category === '天马号') category = '天马号'
if (category === '金山' || category === '金山号') category = '金山号'
if (category === '金马' || category === '金马号') category = '金马号'
if (category === '倒置') category = '倒置号'
if (category === '圆圆') category = '圆圆号'
if (dealNumberCatFilter && category !== dealNumberCatFilter) return false
return true
}
return true
}).map(item => {
// tab - // tab -
if (activeTab === 'deal') { if (activeTab === 'deal') {
const content = item.content || '' const content = item.content || ''
@ -820,7 +752,7 @@ export default function News() {
</div> </div>
{/* 创建日期 + 用户名 */} {/* 创建日期 + 用户名 */}
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}> <div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {item.user_name || '匿名用户'} 📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'}
</div> </div>
{/* 号码特征 */} {/* 号码特征 */}
{features && ( {features && (
@ -875,7 +807,7 @@ export default function News() {
> >
自有{item.matched_count || 0}条藏品匹配成功 自有{item.matched_count || 0}条藏品匹配成功
</span> </span>
{item.network_matched_count !== undefined && item.network_matched_count > 0 && ( {item.network_matched_count !== undefined && (
<span <span
style={{ color: '#3b82f6', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer', marginLeft: '12px' }} style={{ color: '#3b82f6', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer', marginLeft: '12px' }}
onClick={() => fetchNetworkMatchCollections(item.id)} onClick={() => fetchNetworkMatchCollections(item.id)}
@ -1020,10 +952,7 @@ export default function News() {
<div style={{ marginBottom: '8px' }}> <div style={{ marginBottom: '8px' }}>
{comments[item.id].map((c, idx) => ( {comments[item.id].map((c, idx) => (
<div key={c.id || idx} style={{ padding: '8px 0', borderBottom: '1px solid #334155' }}> <div key={c.id || idx} style={{ padding: '8px 0', borderBottom: '1px solid #334155' }}>
<div style={{ color: '#3b82f6', fontSize: '12px', marginBottom: '4px' }}> <div style={{ color: '#3b82f6', fontSize: '12px', marginBottom: '4px' }}>{c.user_name || '匿名用户'}</div>
{c.user_name || '匿名用户'}
{c.created_at && <span style={{ color: '#64748b', marginLeft: '8px' }}>{new Date(c.created_at).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>}
</div>
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>{c.content}</div> <div style={{ color: '#e2e8f0', fontSize: '13px' }}>{c.content}</div>
</div> </div>
))} ))}
@ -1116,7 +1045,7 @@ export default function News() {
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}> <div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单网络数据{networkMatchTotalCount > 0 ? `${networkMatchTotalCount}` : ''}</h3> <h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单网络数据</h3>
<button onClick={() => setShowNetworkMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button> <button onClick={() => setShowNetworkMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
</div> </div>
{networkMatchCollections.length === 0 ? ( {networkMatchCollections.length === 0 ? (
@ -1316,9 +1245,6 @@ export default function News() {
</div> </div>
</div> </div>
)} )}
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.1</div>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
/** /**
* News_YichensBoard - 一尘帖子页面 * News_YichensBoard - 一尘帖子页面
* Version: 0.0.1 * Version: 1.2.75x
* 更新 * 更新
*/ */
@ -219,9 +219,6 @@ export default function YichensBoard() {
</div> </div>
</div> </div>
)} )}
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.1</div>
</div> </div>
) )
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
/** /**
* Settings - 设置页面 * Settings - 设置页面
* Version: 0.0.1 * Version: 1.2.75x
* 更新 * 更新
*/ */
@ -435,9 +435,6 @@ export default function Settings() {
退出登录 退出登录
</button> </button>
</div> </div>
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.1</div>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
/** /**
* Stats - 统计页面 * Stats - 统计页面
* Version: 0.0.1 * Version: 1.2.70x
* 更新 * 更新
*/ */

View File

@ -1,401 +0,0 @@
/**
* Wiki - 龙钞百科页面
* Version: 1.0.0
* 新手入门知识号码分类术语解释
*/
import React, { useState } from 'react'
import { APP_VERSION } from '../config/version'
export default function Wiki() {
const [activeSection, setActiveSection] = useState('intro')
//
const sections = {
intro: {
title: '🐉 什么是龙钞',
content: `
## 中国人民银行龙钞纪念钞
**龙钞**全称第24届冬季奥林匹克运动会纪念钞是中国人民银行于2022年发行的纪念钞包含**冰上运动****雪上运动**两枚一套
### 基本信息
| 项目 | 内容 |
|------|------|
| 发行时间 | 2022年12月 |
| 发行量 | 2亿套 |
| 面值 | 20 |
| 规格 | 145mm × 70mm |
### 收藏价值
1. **题材热门** - 冬奥会是国际大型赛事题材热度高
2. **发行量适中** - 2亿的发行量在纪念钞中属于中等
3. **设计精美** - 首次采用塑料基片+纸基的组合设计
4. **号码玩法多** - 冠字号+8位数字组合丰富多样
`
},
category: {
title: '🔢 号码分类',
content: `
## 龙钞号码分类大全
### 顶级号码收藏价值最高
| 分类 | 条件 | 优先级 |
|------|------|--------|
| 🏆 圆圆号 | 不含0123457 | 1 |
| 🔄 倒置号 | 不含23457 | 2 |
| 🐎 金马王 | 不含12347 | 3 |
| 🐎 金马号 | 不含2347 | 4 |
| 金山王 | 不含12457 | 5 |
| 天马王 | 不含1247 | 6 |
### 高级号码
| 分类 | 条件 | 备注 |
|------|------|------|
| 🏔 金山号 | 不含2457 | |
| 🐎 天马号 | 不含247 | |
| 🌫 朦胧王 | 不含13457 | |
| 🌫 朦胧号 | 不含3457 | |
| 💎 如意号 | 不含1347 | |
| 💎 钻石号 | 不含347 | |
| 永恒号 | 不含47 | |
### 普通号码
| 分类 | 条件 | 备注 |
|------|------|------|
| 🔥 带7号 | 不含4 | 不带4的号码 |
| 带4号 | 包含4 | 传统说法带4不太好 |
### 分类说明
- **"无X"规则**号码中不包含数字X
- **就高不就低**同时满足多个条件时按最高价值分类
- **散钞看8位**散钞看全部8位数字
- **标十看7位**标十看后7位
- **标百看6位**标百看后6位
`
},
terms: {
title: '📖 术语解释',
content: `
## 收藏术语大全
### 基础术语
| 术语 | 解释 |
|------|------|
| 冠字号 | J+8位数字如J01234567 |
| 散钞 | 单张收藏的钞票 |
| 标十 | 10连号一刀 |
| 标百 | 100连号一捆 |
| 整刀 | 100张连号 |
| 整捆 | 1000张连号 |
### 评级术语
| 术语 | 解释 |
|------|------|
| 评级 | 由专业机构鉴定真伪品相 |
| 评级公司 | PCGSNGCPMG等 |
| 分数 | 评级公司给出的品相分数70-100 |
| 69+ | 顶级分数收藏价值高 |
| 68 | 高分品相极佳 |
| 67 | 不错品相良好 |
### 状态术语
| 术语 | 解释 |
|------|------|
| 自持 | 自己收藏 |
| 寄存 | 存放在别人那里 |
| 寄售 | 委托他人代卖 |
| 送评 | 送去评级公司评级 |
| 修复 | 经过专业修复 |
### 价格术语
| 术语 | 解释 |
|------|------|
| 成本价 | 入手的价格 |
| 目标价 | 预期出售价格 |
| 成交价 | 实际交易价格 |
| 行情 | 当前市场的买卖价格 |
`
},
grade: {
title: '⭐ 评级知识',
content: `
## 评级公司介绍
### 国际三大评级公司
#### 1. PCGS美国
- 成立于1986年
- 业界权威评级标准严格
- 特别擅长现代币评级
#### 2. NGC美国
- 成立于1987年
- 评级数量大
- 封装设计美观
#### 3. PMG美国
- 专注于纸币评级
- 评级标准清晰
- 国内认知度高
### 评分标准
| 分数 | 描述 | 等级 |
|------|------|------|
| 70 | 完美未流通 | Gem MS |
| 69 | 接近完美 | Gem MS |
| 68 | 极美 | MS |
| 67 | 精美 | MS |
| 66 | 优美 | MS |
| 65 | 精美 | MS |
### 评级建议
1. **高端收藏** - 建议送评69分以上
2. **普通收藏** - 67-68分性价比高
3. **投资需求** - 选择68分以上的评级钞
### 注意事项
- 评级有风险可能降级
- 评级费用几十到几百元不等
- 评级周期通常2-4
`
},
care: {
title: '💎 保养指南',
content: `
## 龙钞保养指南
### 存放环境
#### 温度与湿度
- **最佳温度**18-25°C
- **最佳湿度**40-55%
- 避免温差大的环境
- 避免潮湿易发霉
#### 存放方式
- 使用专用纸币保护套
- 避免直接用手触摸
- 远离阳光直射
- 远离热源暖气空调
### 注意事项
#### 禁止行为
- 用手指直接触摸钞面
- 折叠弯曲钞票
- 在钞面上写字
- 暴露在潮湿环境中
- 靠近化学物品
#### 正确做法
- 佩戴手套操作
- 使用镊子夹取
- 平放保存
- 定期检查状态
- 使用干燥剂
### 常见问题
**Q: 钞票有折痕怎么办**
A: 建议送专业修复或评级遮盖瑕疵
**Q: 钞票发黄怎么办**
A: 避免潮湿环境可使用干燥剂
**Q: 如何判断品相**
A: 对光查看是否有磨损污渍折痕
`
},
platform: {
title: '🛒 交易指南',
content: `
## 交易注意事项
### 交易平台
1. **专业钱币交易平台**
- 一尘网
- 华夏收藏网
- 钱币天堂
2. **二手交易平台**
- 闲鱼需谨慎
3. **微信群交易**
- 诚信商家直供
- 群内私下交易
### 防骗指南
#### 常见骗局
- 低价诱惑先款后货
- 假称货在朋友处
- 改号换号
- 假评级假分数
#### 安全交易
1. **走平台交易**
- 使用有保障的平台
- 保留交易凭证
2. **先看后买**
- 要求实拍视频
- 确认号码无误
3. **金额较大**
- 建议走中介
- 或面交交易
### 价格参考
- **散钞**面值+10~50
- **标十**150-300
- **标百**1500-3000
- **特殊号码**价格面议
### 投资建议
1. 理性投资量力而行
2. 注重品相号码为辅
3. 长期持有静待升值
4. 分散投资不要all in
`
}
}
// Markdown
const renderContent = (content) => {
const lines = content.trim().split('\n')
return lines.map((line, idx) => {
//
if (line.startsWith('## ')) {
return <h3 key={idx} style={{ color: '#fbbf24', fontSize: '16px', marginTop: '20px', marginBottom: '10px', borderLeft: '3px solid #fbbf24', paddingLeft: '10px' }}>{line.replace('## ', '')}</h3>
}
//
if (line.startsWith('### ')) {
return <h4 key={idx} style={{ color: '#fff', fontSize: '14px', marginTop: '16px', marginBottom: '8px' }}>{line.replace('### ', '')}</h4>
}
//
if (line.startsWith('|') && !line.startsWith('|---')) {
const cells = line.split('|').filter(c => c.trim())
if (cells.length > 1) {
const isHeader = idx > 0 && lines[idx-1]?.includes('---')
return (
<div key={idx} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '4px', padding: '8px 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
{cells.map((cell, i) => (
<div key={i} style={{ color: isHeader ? '#94a3b8' : '#e2e8f0', fontSize: '12px', padding: '4px', background: isHeader ? 'rgba(255,255,255,0.02)' : 'transparent' }}>
{cell.trim()}
</div>
))}
</div>
)
}
return null
}
//
if (line.startsWith('- ') || line.startsWith('1. ')) {
const text = line.replace(/^[-1-9.]\s*/, '')
return <div key={idx} style={{ color: '#cbd5e1', fontSize: '13px', paddingLeft: '16px', marginBottom: '6px', position: 'relative' }}><span style={{ position: 'absolute', left: '4px', color: '#fbbf24' }}></span>{text}</div>
}
//
if (line.trim()) {
//
let text = line.replace(/\*\*(.*?)\*\*/g, '<strong style="color:#fbbf24">$1</strong>')
return <p key={idx} style={{ color: '#cbd5e1', fontSize: '13px', lineHeight: '1.8', marginBottom: '8px' }} dangerouslySetInnerHTML={{ __html: text }} />
}
return null
})
}
return (
<div style={{ minHeight: '100vh', background: '#0f172a', paddingBottom: '80px' }}>
{/* 头部 */}
<div style={{ padding: '20px', background: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)', borderBottom: '1px solid rgba(251,191,36,0.2)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button onClick={() => window.location.hash = '#/home'} style={{ background: 'none', border: 'none', color: '#fbbf24', fontSize: '18px', cursor: 'pointer' }}></button>
<h1 style={{ color: '#fff', fontSize: '20px', margin: 0 }}>📚 龙钞百科</h1>
</div>
<span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '11px' }}>v{APP_VERSION}</span>
</div>
{/* 简介文字 */}
<p style={{ color: '#94a3b8', fontSize: '13px', margin: 0 }}>
新手入门指南 · 号码分类 · 术语解释 · 保养知识
</p>
</div>
{/* 导航标签 */}
<div style={{
display: 'flex',
gap: '6px',
padding: '12px 16px',
overflowX: 'auto',
background: 'rgba(255,255,255,0.02)'
}}>
{Object.entries(sections).map(([key, section]) => (
<button
key={key}
onClick={() => setActiveSection(key)}
style={{
padding: '8px 14px',
borderRadius: '20px',
border: 'none',
fontSize: '12px',
whiteSpace: 'nowrap',
cursor: 'pointer',
background: activeSection === key ? '#fbbf24' : 'rgba(255,255,255,0.08)',
color: activeSection === key ? '#1e293b' : '#94a3b8',
fontWeight: activeSection === key ? 'bold' : 'normal',
transition: 'all 0.2s'
}}
>
{section.title}
</button>
))}
</div>
{/* 内容区域 */}
<div style={{ padding: '16px' }}>
<div style={{
background: 'rgba(255,255,255,0.03)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(255,255,255,0.05)'
}}>
{renderContent(sections[activeSection].content)}
</div>
</div>
{/* 底部提示 */}
<div style={{
position: 'fixed',
bottom: '70px',
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(251,191,36,0.1)',
padding: '8px 16px',
borderRadius: '20px',
border: '1px solid rgba(251,191,36,0.2)'
}}>
<span style={{ color: '#fbbf24', fontSize: '12px' }}>
💡 了解更多请访问 jiachenlong.com
</span>
</div>
</div>
)
}

View File

@ -1,7 +1,7 @@
/** /**
* YichensBoard - 一尘看板页面 * YichensBoard - 一尘看板页面
* Version: 0.0.5 (2026-05-13) * Version: 1.2.70x
* 更新增加复制链接功能 * 更新
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
@ -15,7 +15,6 @@ export default function YichensBoard() {
const [expandedPosts, setExpandedPosts] = useState({}) const [expandedPosts, setExpandedPosts] = useState({})
const [postTypeFilter, setPostTypeFilter] = useState('all') const [postTypeFilter, setPostTypeFilter] = useState('all')
const [categoryFilter, setCategoryFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('')
const [numberFilter, setNumberFilter] = useState('')
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [totalPosts, setTotalPosts] = useState(0) const [totalPosts, setTotalPosts] = useState(0)
const [searchKeyword, setSearchKeyword] = useState('') const [searchKeyword, setSearchKeyword] = useState('')
@ -42,20 +41,18 @@ export default function YichensBoard() {
} }
} }
const fetchPosts = async (p, cat, kw, numFilter) => { const fetchPosts = async (p, cat, kw) => {
setLoading(true) setLoading(true)
setError(null) setError(null)
const currentPage = p !== undefined ? p : page const currentPage = p !== undefined ? p : page
const currentCat = cat !== undefined ? cat : categoryFilter const currentCat = cat !== undefined ? cat : categoryFilter
const searchKw = kw !== undefined ? kw : searchKeyword const searchKw = kw !== undefined ? kw : searchKeyword
const numFilterVal = numFilter !== undefined ? numFilter : numberFilter
let url = '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390) let url = '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
if (postTypeFilter === 'deal') url += '&post_type=deal' if (postTypeFilter === 'deal') url += '&post_type=deal'
else if (postTypeFilter === 'want') url += '&post_type=want' else if (postTypeFilter === 'want') url += '&post_type=want'
else if (postTypeFilter === 'other') url += '&post_type=normal' else if (postTypeFilter === 'other') url += '&post_type=normal'
if (searchKw && searchKw.trim()) url += '&keyword=' + encodeURIComponent(searchKw.trim()) if (searchKw && searchKw.trim()) url += '&keyword=' + encodeURIComponent(searchKw.trim())
if (numFilterVal && numFilterVal !== '') url += '&number_filter=' + encodeURIComponent(numFilterVal)
try { try {
console.log('YichensBoard: 请求 posts', url) console.log('YichensBoard: 请求 posts', url)
@ -78,29 +75,6 @@ export default function YichensBoard() {
data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马')) data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
} }
} }
//
if (numberFilter && Array.isArray(data)) {
const filterKeywords = {
'带4': ['带4', '带四', '通货', '标四'],
'无4': ['无4', '无四', '带7'],
'无47': ['无47', '无四七', '永恒'],
'无247': ['无247', '无二四七', '天马', '金山'],
'无347': ['无347', '无三四七', '钻石', '如意', '朦胧', '金马'],
'年份': ['年份', '生日', '生日号', '纪念'],
'特色': ['倒置', '圆圆', '豹子', '老虎', '狮子', '大象', '龙三', '龙二']
}
const kw = filterKeywords[numberFilter] || []
if (kw.length > 0) {
data = data.filter(p => {
const text = (p.title || '') + (p.content || '') + (p.description || '')
return kw.some(k => text.includes(k))
})
}
// totalPosts
if (numberFilter && data.length > 0) {
setTotalPosts(data.length)
}
}
// data // data
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
data = data.posts || data.data || [] data = data.posts || data.data || []
@ -133,14 +107,14 @@ export default function YichensBoard() {
useEffect(() => { useEffect(() => {
if (todayStats.total > 0) { if (todayStats.total > 0) {
fetchPosts(1, categoryFilter, searchKeyword, numberFilter) fetchPosts(1, categoryFilter, searchKeyword)
} }
}, [todayStats, categoryFilter, postTypeFilter, numberFilter]) }, [todayStats, categoryFilter, postTypeFilter])
useEffect(() => { useEffect(() => {
setPage(1) setPage(1)
fetchPosts(1, categoryFilter, searchKeyword, numberFilter) fetchPosts(1, categoryFilter, searchKeyword)
}, [searchKeyword, numberFilter]) }, [searchKeyword])
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] })) const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
@ -225,16 +199,6 @@ export default function YichensBoard() {
))} ))}
</div> </div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
{[{key:'',label:'全部'},{key:'带4',label:'带4'},{key:'无4',label:'无4'},{key:'无47',label:'无47'},{key:'无247',label:'无247'},{key:'无347',label:'无347'},{key:'年份',label:'年份'},{key:'特色',label:'其他'}].map(k => (
<button key={k.key} onClick={() => { setNumberFilter(k.key); setPage(1); }}
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
background: numberFilter===k.key ? 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
{k.label}
</button>
))}
</div>
{loading ? <div style={{textAlign:'center',color:'#9ca3af',padding:40}}>加载中...</div> : ( {loading ? <div style={{textAlign:'center',color:'#9ca3af',padding:40}}>加载中...</div> : (
<div> <div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
@ -265,12 +229,7 @@ export default function YichensBoard() {
<div style={{ color: '#e2e8f0', fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 300, overflowY: 'auto' }}> <div style={{ color: '#e2e8f0', fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 300, overflowY: 'auto' }}>
{post.content} {post.content}
</div> </div>
{post.url && ( {post.url && <a href={post.url} target='_blank' rel='noopener noreferrer' style={{ display:'inline-block', marginTop:12, padding:'8px 16px', background:'rgba(59,130,246,0.2)', borderRadius:8, color:'#60a5fa', textDecoration:'none', fontSize:13 }}>查看原帖</a>}
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<a href={post.url} target='_blank' rel='noopener noreferrer' style={{ padding:'8px 16px', background:'rgba(59,130,246,0.2)', borderRadius:8, color:'#60a5fa', textDecoration:'none', fontSize:13 }}>查看原帖</a>
<button onClick={() => { navigator.clipboard.writeText(post.url); alert('链接已复制!'); }} style={{ padding:'8px 16px', background:'rgba(34,197,94,0.2)', borderRadius:8, color:'#22c55e', border:'none', cursor:'pointer', fontSize:13 }}>复制链接</button>
</div>
)}
</div> </div>
)} )}
</div> </div>
@ -284,12 +243,12 @@ export default function YichensBoard() {
</div> </div>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
{page > 1 ? ( {page > 1 ? (
<button onClick={() => { const newPage = page - 1; setPage(newPage); fetchPosts(newPage, categoryFilter, searchKeyword, numberFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>上一页</button> <button onClick={() => { const newPage = page - 1; setPage(newPage); fetchPosts(newPage, categoryFilter, searchKeyword) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>上一页</button>
) : ( ) : (
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>上一页</span> <span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>上一页</span>
)} )}
{posts.length >= 390 ? ( {posts.length >= 390 && totalPosts > page * 390 ? (
<button onClick={() => { const newPage = page + 1; setPage(newPage); fetchPosts(newPage, categoryFilter, searchKeyword, numberFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>下一页</button> <button onClick={() => { const newPage = page + 1; setPage(newPage); fetchPosts(newPage, categoryFilter, searchKeyword) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>下一页</button>
) : ( ) : (
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>下一页</span> <span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>下一页</span>
)} )}
@ -297,9 +256,6 @@ export default function YichensBoard() {
</div> </div>
</div> </div>
)} )}
{/* 版本号显示 */}
<div style={{ position: 'fixed', bottom: '70px', right: '12px', color: 'rgba(255,255,255,0.2)', fontSize: '10px' }}>v0.0.3</div>
</div> </div>
) )
} }

View File

@ -3,48 +3,59 @@ import react from '@vitejs/plugin-react'
import { readFileSync, writeFileSync } from 'fs' import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path' import { join } from 'path'
// 从 config/VERSION 文件读取版本号
function getVersion() { function getVersion() {
try { try {
const versionFile = join(__dirname, '..', 'config', 'VERSION') const versionFile = join(__dirname, 'config', 'VERSION')
const content = readFileSync(versionFile, 'utf-8') const content = readFileSync(versionFile, 'utf-8').trim()
const match = content.match(/^VERSION=(.*)$/m) // 移除 VERSION= 前缀
return match ? match[1].trim() : '0.0.0' if (content.startsWith('VERSION=')) {
return content.substring(7).trim()
}
return content || '0.0.0'
} catch (e) { } catch (e) {
console.error('读取 VERSION 文件失败:', e.message)
return '0.0.0' return '0.0.0'
} }
} }
const APP_VERSION = getVersion() const APP_VERSION = getVersion()
console.log('📦 构建版本v' + APP_VERSION)
// 构建后执行 - 更新 dist/index.html 的 title
function updateHtmlTitle() { function updateHtmlTitle() {
return {
name: 'update-html-title',
closeBundle() {
try { try {
const htmlPath = join(__dirname, 'index.html') const htmlPath = join(__dirname, 'dist', 'index.html')
let htmlContent = readFileSync(htmlPath, 'utf-8') let htmlContent = readFileSync(htmlPath, 'utf-8')
// 替换 <title>甲辰收藏 vXXX</title>
htmlContent = htmlContent.replace( htmlContent = htmlContent.replace(
/<title>甲辰收藏 v[\d.]+<\/title>/, /<title>甲辰收藏 v[\d.]+<\/title>/,
`<title>甲辰收藏 v${APP_VERSION}</title>` '<title>甲辰收藏 v' + APP_VERSION + '</title>'
) )
writeFileSync(htmlPath, htmlContent, 'utf-8') writeFileSync(htmlPath, htmlContent, 'utf-8')
} catch (e) {} console.log('✅ 已更新 dist/index.html title: 甲辰收藏 v' + APP_VERSION)
} catch (e) {
console.error('更新 dist/index.html 失败:', e.message)
}
}
}
} }
updateHtmlTitle()
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react(), updateHtmlTitle()],
define: { define: {
'import.meta.env.APP_VERSION': JSON.stringify(APP_VERSION) 'import.meta.env.APP_VERSION': JSON.stringify(APP_VERSION)
}, },
build: { build: {
minify: false,
rollupOptions: { rollupOptions: {
treeshake: false,
output: { output: {
manualChunks: undefined entryFileNames: 'assets/[name]-[hash]-[name].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]'
} }
},
esbuild: {
treeShaking: false
} }
} }
}) })

View File

@ -1,136 +0,0 @@
#!/bin/bash
# ========================================
# CI/CD 版本自动同步脚本
# 每次代码更新后自动运行
# ========================================
set -e
REPO_DIR="/root/.openclaw/workspace/jiachenlong"
VERSION_FILE="$REPO_DIR/config/VERSION.json"
CHANGELOG_FILE="$REPO_DIR/config/CHANGELOG.md"
cd "$REPO_DIR"
echo "========================================="
echo " CI/CD 版本自动同步"
echo "========================================="
# 检查是否有未提交的更改
if [ -n "$(git status --porcelain)" ]; then
echo "⚠️ 有未提交的更改,请先提交"
exit 1
fi
# 获取当前版本
CURRENT_VERSION=$(python3 -c "import json; print(json.load(open('$VERSION_FILE'))['version'])")
echo "📌 当前版本: $CURRENT_VERSION"
# 解析版本号
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3)
# 询问是否升级版本
echo ""
echo "请选择版本升级类型:"
echo " 1) PATCH (补丁): ${MAJOR}.${MINOR}.$((PATCH+1))"
echo " 2) MINOR (小版本): ${MAJOR}.$((MINOR+1)).0"
echo " 3) MAJOR (大版本): $((MAJOR+1)).0.0"
echo " 4) 自定义版本"
read -p "选择 (1-4): " choice
case $choice in
1) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH+1))";;
2) NEW_VERSION="${MAJOR}.$((MINOR+1)).0";;
3) NEW_VERSION="$((MAJOR+1)).0.0";;
4) read -p "输入新版本号: " NEW_VERSION;;
*) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH+1))";;
esac
echo "📌 新版本: $NEW_VERSION"
# 更新 VERSION.json
python3 << EOF
import json
with open('$VERSION_FILE') as f:
v = json.load(f)
old_version = v['version']
v['version'] = '$NEW_VERSION'
v['updated'] = '$(date +%Y-%m-%d)'
# 更新主版本号到所有模块
for section in ['frontend', 'backend']:
if 'pages' in v['modules'][section]:
for k in v['modules'][section]['pages']:
v['modules'][section]['pages'][k] = '$NEW_VERSION'
if 'routers' in v['modules'][section]:
for k in v['modules'][section]['routers']:
v['modules'][section]['routers'][k] = '$NEW_VERSION'
if 'config' in v['modules'][section]:
for k in v['modules'][section]['config']:
v['modules'][section]['config'][k] = '$NEW_VERSION'
if 'app' in v['modules'][section]:
for k in v['modules'][section]['app']:
v['modules'][section]['app'][k] = '$NEW_VERSION'
with open('$VERSION_FILE', 'w') as f:
json.dump(v, f, indent=2, ensure_ascii=False)
print(f"✅ VERSION.json 已更新: {old_version} → $NEW_VERSION")
EOF
# 同步到所有文件头部
python3 << 'EOF'
import json
import re
import os
VERSION = '$NEW_VERSION'
with open('$VERSION_FILE') as f:
v = json.load(f)
frontend_pages = v['modules']['frontend']['pages']
backend_routers = v['modules']['backend']['routers']
# 前端文件
for fname in frontend_pages:
fpath = f"$REPO_DIR/frontend/src/pages/{fname}.jsx"
if os.path.exists(fpath):
with open(fpath, 'r') as f:
content = f.read()
new_content = re.sub(r'Version: [\d.]+', f'Version: {VERSION}', content, count=1)
new_content = re.sub(r'\d{4}-\d{2}-\d{2}', '$(date +%Y-%m-%d)', new_content, count=1)
with open(fpath, 'w') as f:
f.write(new_content)
print(f" ✅ {fname}.jsx")
# 后端文件
for fname in backend_routers:
fpath = f"$REPO_DIR/backend/app/routers/{fname}.py"
if os.path.exists(fpath):
with open(fpath, 'r') as f:
content = f.read()
new_content = re.sub(r'Version: [\d.]+', f'Version: {VERSION}', content, count=1)
new_content = re.sub(r'\d{4}-\d{2}-\d{2}', '$(date +%Y-%m-%d)', new_content, count=1)
with open(fpath, 'w') as f:
f.write(new_content)
print(f" ✅ {fname}.py")
print(f"✅ 所有文件版本已同步到 {VERSION}")
EOF
# 提交更改
git add .
git commit -m "v$NEW_VERSION - 自动版本更新"
git push
echo ""
echo "========================================="
echo " ✅ 版本更新完成!"
echo "========================================="
echo "新版本: $NEW_VERSION"
echo "已推送到远程仓库"

View File

@ -1,93 +0,0 @@
#!/bin/bash
# ========================================
# 版本同步脚本 - 只更新修改的文件
# 用法: ./scripts/version_sync.sh [patch|minor|major|custom]
# ========================================
set -e
REPO_DIR="/root/.openclaw/workspace/jiachenlong"
VERSION_FILE="$REPO_DIR/config/VERSION.json"
cd "$REPO_DIR"
# 获取当前版本
CURRENT=$(python3 -c "import json; print(json.load(open('$VERSION_FILE'))['version'])")
echo "📌 当前版本: $CURRENT"
# 解析版本号
MAJOR=$(echo $CURRENT | cut -d. -f1)
MINOR=$(echo $CURRENT | cut -d. -f2)
PATCH=$(echo $CURRENT | cut -d. -f3)
# 根据参数决定新版本
case "${1:-patch}" in
patch) NEW="$MAJOR.$MINOR.$((PATCH+1))";;
minor) NEW="$MAJOR.$((MINOR+1)).0";;
major) NEW="$((MAJOR+1)).0.0";;
*) NEW="${1:-$CURRENT}";;
esac
echo "📌 新版本: $NEW"
# 1. 更新VERSION.json主版本号
python3 << EOF
import json
with open('$VERSION_FILE') as f:
v = json.load(f)
v['version'] = '$NEW'
v['updated'] = '$(date +%Y-%m-%d)'
with open('$VERSION_FILE', 'w') as f:
json.dump(v, f, indent=2, ensure_ascii=False)
print("✅ VERSION.json 主版本已更新")
EOF
# 2. 同步VERSION.json中的版本到文件头部只同步VERSION.json中已记录的版本
python3 << 'EOF'
import json
import re
import os
VERSION = '$NEW'
REPO = '$REPO_DIR'
with open(f'{REPO}/config/VERSION.json') as f:
v = json.load(f)
pages = v['modules']['frontend']['pages']
routers = v['modules']['backend']['routers']
# 前端
for fname, ver in pages.items():
fpath = f'{REPO}/frontend/src/pages/{fname}.jsx'
if os.path.exists(fpath):
with open(fpath, 'r') as f:
content = f.read()
# 只更新VERSION.json中记录的版本
new_content = re.sub(r'Version: [\d.]+', f'Version: {ver}', content, count=1)
with open(fpath, 'w') as f:
f.write(new_content)
print(f" ✅ {fname}.jsx: {ver}")
# 后端
for fname, ver in routers.items():
fpath = f'{REPO}/backend/app/routers/{fname}.py'
if os.path.exists(fpath):
with open(fpath, 'r') as f:
content = f.read()
new_content = re.sub(r'Version: [\d.]+', f'Version: {ver}', content, count=1)
with open(fpath, 'w') as f:
f.write(new_content)
print(f" ✅ {fname}.py: {ver}")
print("✅ 所有文件版本已同步")
EOF
# 3. 提交
git add .
git commit -m "v$NEW - 版本自动更新" || echo "无新更改"
echo ""
echo "✅ 完成!版本: $CURRENT$NEW"