Compare commits
49 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
98b3917acc | |
|
|
02711d73c9 | |
|
|
645819844e | |
|
|
9b960c1d12 | |
|
|
b1d5398220 | |
|
|
9b7f634ad3 | |
|
|
33ca624e70 | |
|
|
73075a1474 | |
|
|
bae5b6073a | |
|
|
aa10dc4cc7 | |
|
|
87da78d6d8 | |
|
|
3eb8aac2be | |
|
|
f695dfbfa0 | |
|
|
a31d6cc153 | |
|
|
5a5ec0ee2c | |
|
|
1fef5177f3 | |
|
|
ee728a5f4e | |
|
|
d8aac9bc3e | |
|
|
a2e6d328f0 | |
|
|
d1a245c340 | |
|
|
3a2258d2d3 | |
|
|
877f314617 | |
|
|
c326d184f9 | |
|
|
c17525c1e1 | |
|
|
875e40a79d | |
|
|
2dc0e6a1de | |
|
|
bb8ae49ec3 | |
|
|
9d623821f6 | |
|
|
0b56cd7891 | |
|
|
7100f2d40e | |
|
|
41b3353613 | |
|
|
e6a0b4fb34 | |
|
|
ae87b10fe0 | |
|
|
3cd660eb13 | |
|
|
eea94ec381 | |
|
|
7d0f92b1eb | |
|
|
d47a002495 | |
|
|
6667671f46 | |
|
|
c70a4f1221 | |
|
|
65d5603f9a | |
|
|
5959d4c41a | |
|
|
0aab735feb | |
|
|
0b1643ae5b | |
|
|
3faac301a0 | |
|
|
9c70250234 | |
|
|
64776a6e14 | |
|
|
45a0bd30d8 | |
|
|
dbddb10823 | |
|
|
0bd2d78697 |
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
| 项目 | 内容 |
|
| 项目 | 内容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **版本** | v1.2.38 |
|
| **版本** | v0.0.7 |
|
||||||
| **代号** | 最终优化版本 |
|
| **代号** | 寻配号重构版本 |
|
||||||
| **发布日期** | 2026-03-31 |
|
| **发布日期** | 2026-04-24 |
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
1.2.98
|
VERSION=v0.2.1
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,14 @@ 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())
|
||||||
) -> Optional[User]:
|
) -> User:
|
||||||
"""获取当前用户(可返回None)"""
|
"""获取当前用户"""
|
||||||
if not credentials:
|
if not credentials:
|
||||||
return None
|
raise HTTPException(
|
||||||
|
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)
|
||||||
|
|
@ -84,6 +88,10 @@ 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:
|
||||||
return None
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="用户不存在",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ 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():
|
||||||
|
|
@ -88,6 +89,7 @@ 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("/")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
# 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())
|
||||||
|
|
@ -24,6 +24,12 @@ 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") # 是否已匹配
|
||||||
|
|
@ -33,6 +39,7 @@ 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())
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# auth - 认证路由
|
# auth - 认证路由
|
||||||
# Version: 1.2.90
|
# Version: 0.0.1
|
||||||
# 更新:
|
# 更新:
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
@ -91,22 +91,8 @@ 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
|
|
@ -1,5 +1,5 @@
|
||||||
# deal - 成交行情路由
|
# deal - 成交行情路由
|
||||||
# Version: 1.2.85
|
# Version: 0.0.1
|
||||||
# 更新:
|
# 更新:
|
||||||
|
|
||||||
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
|
from typing import Optional, List
|
||||||
# 更新:
|
# 更新:
|
||||||
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
|
|
@ -1,5 +1,5 @@
|
||||||
# news - 新闻路由
|
# news - 新闻路由
|
||||||
# Version: 1.2.80
|
# Version: 0.0.1
|
||||||
# 更新:
|
# 更新:
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# ocr - OCR识别路由
|
# ocr - OCR识别路由
|
||||||
# Version: 1.2.75
|
# Version: 0.0.1
|
||||||
# 更新:
|
# 更新:
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
|
||||||
|
|
@ -1,210 +1,104 @@
|
||||||
# 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
|
||||||
# 更新:
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,759 @@
|
||||||
|
# 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
|
||||||
|
|
@ -1,384 +1,516 @@
|
||||||
# seek - 寻号匹配路由
|
# seek - 寻配号路由
|
||||||
# Version: 1.2.70
|
# Version: 0.2.0 (2026-05-11)
|
||||||
# 更新:
|
# 稳定版:支持评级字段(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 = []
|
||||||
return items
|
for item in 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",
|
||||||
status="active"
|
view_count=0,
|
||||||
# 更新:
|
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:
|
||||||
for key, value in data.model_dump(exclude_unset=True).items():
|
seek.title = data.title
|
||||||
# 更新:
|
if data.content is not None:
|
||||||
setattr(seek, key, value)
|
seek.content = data.content
|
||||||
# 更新:
|
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
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# users.py - 用户管理路由
|
# users.py - 用户管理路由
|
||||||
# Version: 1.2.98 (2026-04-19)
|
# Version: 0.0.1 (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
|
from app.models.models import User, Collection, Information
|
||||||
from app.schemas.schemas import UserResponse, UserUpdate
|
from app.schemas.schemas import UserResponse, UserUpdate
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["用户"])
|
router = APIRouter(prefix="/api", tags=["用户"])
|
||||||
|
|
|
||||||
|
|
@ -1,760 +1,377 @@
|
||||||
# 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}
|
||||||
# 更新:
|
|
||||||
}
|
}
|
||||||
# 更新:
|
|
||||||
|
|
||||||
# 更新:
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ 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):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
# 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. 圆圆号:无123457,0689四个数字任意组合
|
||||||
|
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. 金马王:无12347,05689组合,必须有5(排除1)
|
||||||
|
if unique <= {'0', '5', '6', '8', '9'} and '5' in unique and '1' not in unique:
|
||||||
|
return "金马王"
|
||||||
|
|
||||||
|
# 7. 金马号:无2347,015689组合,必须有1和5(包含1)
|
||||||
|
if unique <= {'0', '1', '5', '6', '8', '9'} and '1' in unique and '5' in unique:
|
||||||
|
return "金马号"
|
||||||
|
|
||||||
|
# 8. 金山王:无12457,03689组合,必须有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. 天马王:无1247,035689组合,必须有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. 金山号:无2457,013689组合,必须有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. 天马号:无247,0135689组合,必须有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 []
|
||||||
|
|
@ -1,5 +1,222 @@
|
||||||
# 版本更新记录
|
# 版本更新记录
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
### 前端更新
|
### 前端更新
|
||||||
|
|
@ -33,3 +250,8 @@
|
||||||
|
|
||||||
### 更新内容
|
### 更新内容
|
||||||
- (待记录)
|
- (待记录)
|
||||||
|
## v0.0.6 (2026-04-24)
|
||||||
|
### Bug修复
|
||||||
|
- 修复"自有X条藏品匹配成功"点击后没有藏品列表的问题
|
||||||
|
- 原因:seek/match API只查询information表,但实际数据在seek_info表
|
||||||
|
- 修复:API同时支持information表和seek_info表查询
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
1.2.98
|
VERSION=0.2.5
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,29 @@
|
||||||
{
|
{
|
||||||
"version": "1.2.98",
|
"version": "0.2.7",
|
||||||
"updated": "2026-04-19",
|
"updated": "2026-07-19",
|
||||||
"modules": {
|
"modules": {
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"version": "1.2.98",
|
"version": "0.2.3",
|
||||||
"pages": {
|
"pages": {
|
||||||
"Add": "1.2.90",
|
"Login": "0.0.2",
|
||||||
"Admin": "1.2.98",
|
"Add": "0.0.2",
|
||||||
"Detail": "1.2.90",
|
"News": "0.1.2",
|
||||||
"Edit": "1.2.90",
|
"YichensBoard": "0.0.5",
|
||||||
"Home": "1.2.95",
|
"Wiki": "1.0.0",
|
||||||
"List": "1.2.93",
|
"SharePoster": "1.0.0",
|
||||||
"Login": "1.2.85",
|
"List": "0.1.7"
|
||||||
"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": "1.2.98",
|
"version": "0.2.2",
|
||||||
"routers": {
|
"routers": {
|
||||||
"auth": "1.2.90",
|
"auth": "0.0.2",
|
||||||
"collections": "1.2.95",
|
"seek": "0.2.0",
|
||||||
"deal": "1.2.85",
|
"purchase": "0.3.0"
|
||||||
"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": {
|
"models": {
|
||||||
"core": "1.2.0",
|
"seek_info": "0.2.0",
|
||||||
"models": "1.2.0",
|
"purchase": "0.0.3"
|
||||||
"schemas": "1.2.0",
|
|
||||||
"services": "1.2.0",
|
|
||||||
"utils": "1.2.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,449 @@
|
||||||
|
# 🐉 甲辰藏品管理系统 环境部署手册
|
||||||
|
|
||||||
|
> **版本**: 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
|
||||||
|
- API:http://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
|
||||||
|
- AccessKey:LTAI5t86bc1nNKVNyYv4Af6x
|
||||||
|
|
||||||
|
### 部署步骤(需授权)
|
||||||
|
|
||||||
|
#### 部署前准备
|
||||||
|
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
|
||||||
|
- AccessKey:LTAI5tPzZDqbM1J6zbk8Wy4q
|
||||||
|
|
||||||
|
### 部署步骤(需授权)
|
||||||
|
|
||||||
|
#### 第一步:前端部署
|
||||||
|
```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
|
||||||
|
|
||||||
|
#### 问题记录
|
||||||
|
无
|
||||||
|
|
||||||
|
#### 汇报状态
|
||||||
|
已向酷博特汇报
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
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');
|
||||||
|
|
@ -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>甲辰收藏 v=1.2.97</title>
|
<title>甲辰收藏 v0.2.5</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" />
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"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"
|
||||||
|
|
@ -1239,6 +1240,15 @@
|
||||||
"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",
|
||||||
|
|
@ -1352,6 +1362,15 @@
|
||||||
"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",
|
||||||
|
|
@ -1673,6 +1692,19 @@
|
||||||
"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",
|
||||||
|
|
@ -1998,6 +2030,15 @@
|
||||||
"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",
|
||||||
|
|
@ -2046,6 +2087,15 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
},
|
},
|
||||||
"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"
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ 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) || '/')
|
||||||
|
|
@ -30,12 +32,14 @@ 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 />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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 || '1.2.82'
|
export const APP_VERSION = import.meta.env.APP_VERSION || '0.0.3'
|
||||||
|
|
||||||
// 版本信息
|
// 版本信息
|
||||||
export const VERSION_INFO = {
|
export const VERSION_INFO = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Add - 添加藏品页面
|
* Add - 添加藏品页面
|
||||||
* Version: 1.2.90x
|
* Version: 0.0.2
|
||||||
* 更新:
|
* 更新:修复行情录入时评级信息(is_graded, grading_company, grading_score)未保存的问题
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react'
|
import React, { useState, useRef } from 'react'
|
||||||
|
|
@ -863,12 +863,15 @@ 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] })
|
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '抖音', seller: '', buyer: '', date: new Date().toISOString().split('T')[0], gradingCompany: '', gradingScore: '' })
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
alert('录入失败: ' + (data.detail || '未知错误'))
|
alert('录入失败: ' + (data.detail || '未知错误'))
|
||||||
|
|
@ -1055,7 +1058,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]}`,
|
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 : '未评级'}`,
|
||||||
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 || '单张',
|
||||||
|
|
@ -1064,7 +1067,10 @@ 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++
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Admin - 管理员用户管理页面
|
* Admin - 管理员用户管理页面
|
||||||
* Version: 1.2.98 (2026-04-19)
|
* Version: 0.0.2 (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' }}>
|
||||||
v{APP_VERSION}
|
文件版本 v0.0.2 | 主版本 v{APP_VERSION}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Detail - 藏品详情页面
|
* Detail - 藏品详情页面
|
||||||
* Version: 1.2.90x
|
* Version: 0.0.1
|
||||||
* 更新:
|
* 更新:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -388,6 +388,9 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Edit - 编辑藏品页面
|
* Edit - 编辑藏品页面
|
||||||
* Version: 1.2.90x
|
* Version: 0.0.1
|
||||||
* 更新:
|
* 更新:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -492,6 +492,9 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Home - 首页
|
* Home - 首页
|
||||||
* Version: 1.2.95x
|
* Version: 0.0.4 (2026-04-24)
|
||||||
* 更新:
|
* 更新:修复寻配号数据统计API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
@ -66,27 +66,31 @@ export default function Home() {
|
||||||
|
|
||||||
// 获取一尘看板数据
|
// 获取一尘看板数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/yichens/stats/dragons-today').then(res => res.json()).then(data => {
|
const token = localStorage.getItem('token')
|
||||||
setDragonStats(data || {})
|
const headers = token ? { 'Authorization': 'Bearer ' + token } : {}
|
||||||
|
|
||||||
|
// 寻配号统计数据
|
||||||
|
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/today').then(res => res.json()).then(data => {
|
fetch('/api/yichens/stats/dragons-today').then(res=>res.json()).then(d=>setDragonStats(d||{})).catch(()=>{})
|
||||||
setYichensStats(data || {})
|
fetch('/api/yichens/stats/today').then(res=>res.json()).then(d=>setYichensStats(d||{})).catch(()=>{})
|
||||||
}).catch(() => {})
|
fetch('/api/yichens/posts?limit=20&offset=0').then(res=>res.json()).then(d=>setRecentPosts(d.posts||d||[])).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(() => {})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 检查是否为管理员
|
// 检查是否为管理员
|
||||||
|
|
@ -178,9 +182,10 @@ 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, #22c55e 0%, #16a34a 100%)',
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 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',
|
||||||
|
|
@ -188,13 +193,14 @@ export default function Home() {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>藏品录入</div>
|
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>藏品录入</div>
|
||||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>AI识别添加</div>
|
<div style={{ color: 'rgba(255,255,255,0.5)', 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, #ec4899 0%, #db2777 100%)',
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 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',
|
||||||
|
|
@ -202,13 +208,14 @@ export default function Home() {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>行情录入</div>
|
<div style={{ color: '#ec4899', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>行情录入</div>
|
||||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>录入成交行情</div>
|
<div style={{ color: 'rgba(255,255,255,0.5)', 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, #f59e0b 0%, #d97706 100%)',
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 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',
|
||||||
|
|
@ -216,13 +223,14 @@ export default function Home() {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布寻号</div>
|
<div style={{ color: '#f59e0b', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布寻号</div>
|
||||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div>
|
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={() => window.location.hash = '#/add?mode=manual'} style={{
|
<div onClick={() => window.location.hash = '#/purchase'} style={{
|
||||||
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 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',
|
||||||
|
|
@ -230,8 +238,23 @@ export default function Home() {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布藏品</div>
|
<div style={{ color: '#3b82f6', fontSize: '18px', fontWeight: '600', whiteSpace: 'nowrap' }}>我的认购</div>
|
||||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>手动发布</div>
|
<div style={{ color: 'rgba(255,255,255,0.5)', 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>
|
||||||
|
|
@ -257,7 +280,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>
|
||||||
|
|
||||||
|
|
@ -399,14 +422,16 @@ 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: '#/ocr', idx: 2 },
|
{ icon: '🎯', label: '认购', hash: '#/purchase', idx: 2 },
|
||||||
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 },
|
{ icon: '➕', label: '录入', hash: '#/ocr', idx: 3 },
|
||||||
{ icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 }
|
{ icon: '📊', label: '统计', hash: '#/stats', 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: '#/ocr', idx: 2 },
|
{ icon: '🎯', label: '认购', hash: '#/purchase', idx: 2 },
|
||||||
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 }
|
{ icon: '➕', label: '录入', hash: '#/ocr', 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',
|
||||||
|
|
@ -421,6 +446,17 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* List - 藏品列表页面
|
* List - 藏品列表页面
|
||||||
* Version: 1.2.93x
|
* Version: 0.1.7
|
||||||
* 更新:
|
* 更新:新增分享海报功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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() {
|
||||||
|
|
@ -30,6 +31,11 @@ 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(() => {
|
||||||
// 检查是否管理员
|
// 检查是否管理员
|
||||||
|
|
@ -106,10 +112,33 @@ export default function List() {
|
||||||
|
|
||||||
// 行情过滤和排序
|
// 行情过滤和排序
|
||||||
const filteredDeals = myDeals.filter(deal => {
|
const filteredDeals = myDeals.filter(deal => {
|
||||||
if (!dealSearch) return true
|
// 1. 搜索过滤
|
||||||
const s = dealSearch.toLowerCase().trim()
|
if (dealSearch) {
|
||||||
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 s = dealSearch.toLowerCase().trim()
|
||||||
return fields.some(f => f.includes(s))
|
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
|
||||||
|
}
|
||||||
|
// 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] || ''
|
||||||
|
|
@ -470,7 +499,26 @@ 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' }}>
|
<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' }}>
|
||||||
|
{/* 分享按钮 */}
|
||||||
|
<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' }}>
|
||||||
|
|
@ -660,13 +708,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: '1px solid rgba(255,255,255,0.1)',
|
border: dealSearch ? '1px solid rgba(59, 130, 246, 0.5)' : '1px solid rgba(255,255,255,0.1)',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
|
|
@ -675,6 +723,134 @@ 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 ? (
|
||||||
|
|
@ -682,7 +858,16 @@ 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' }}>{dealSearch ? '没有匹配的行情' : '暂无行情记录'}</div>
|
<div style={{ color: '#64748b', marginTop: '16px' }}>
|
||||||
|
{(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' }}>
|
||||||
|
|
@ -949,6 +1134,17 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Login - 登录页面
|
* Login - 登录页面
|
||||||
* Version: 1.2.85x
|
* Version: 0.0.2 (2026-06-13)
|
||||||
* 更新:
|
* 更新:移除注册时的手机号和短信验证功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
@ -23,79 +23,22 @@ 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 () => {
|
||||||
// 验证输入
|
// 验证输入
|
||||||
|
|
@ -124,33 +67,13 @@ 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
|
||||||
|
|
@ -175,9 +98,7 @@ export default function Login() {
|
||||||
|
|
||||||
alert('注册成功!请登录')
|
alert('注册成功!请登录')
|
||||||
setShowRegister(false)
|
setShowRegister(false)
|
||||||
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '', inviteCode: '' })
|
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', inviteCode: '' })
|
||||||
setCodeSent(false)
|
|
||||||
setCodeCountdown(0)
|
|
||||||
generateCaptcha()
|
generateCaptcha()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('注册错误:', err)
|
console.error('注册错误:', err)
|
||||||
|
|
@ -491,68 +412,6 @@ 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
|
||||||
|
|
@ -656,10 +515,8 @@ export default function Login() {
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowRegister(false)
|
setShowRegister(false)
|
||||||
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '', inviteCode: '' })
|
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', inviteCode: '' })
|
||||||
setRegisterError('')
|
setRegisterError('')
|
||||||
setCodeSent(false)
|
|
||||||
setCodeCountdown(0)
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, minWidth: "90px", width: "auto",
|
flex: 1, minWidth: "90px", width: "auto",
|
||||||
|
|
@ -696,6 +553,9 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* News - 资讯列表页面
|
* News - 资讯列表页面
|
||||||
* Version: 1.2.80x
|
* Version: 0.1.2 (2026-05-13)
|
||||||
* 更新:
|
* 更新:移除成交行情价格统计表格,增加获取数量到500
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
@ -39,11 +39,15 @@ 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() || ''
|
||||||
|
|
@ -71,9 +75,10 @@ export default function News() {
|
||||||
fetchInfoList()
|
fetchInfoList()
|
||||||
}, [activeTab, dealDate])
|
}, [activeTab, dealDate])
|
||||||
|
|
||||||
// 自动生成寻号标题
|
// 自动生成寻号标题(限制8位冠字号)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${seekForm.features || 'XXXXXXXX'}」`
|
const features = (seekForm.features || '').slice(0, 8).padEnd(8, 'X')
|
||||||
|
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${features}」`
|
||||||
setSeekForm(prev => ({...prev, title}))
|
setSeekForm(prev => ({...prev, title}))
|
||||||
}, [seekForm.edition, seekForm.features])
|
}, [seekForm.edition, seekForm.features])
|
||||||
|
|
||||||
|
|
@ -92,7 +97,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) {
|
||||||
|
|
@ -144,7 +149,7 @@ export default function News() {
|
||||||
const fetchComments = async (infoId) => {
|
const fetchComments = async (infoId) => {
|
||||||
// 每次都重新获取评论
|
// 每次都重新获取评论
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/information/comments/${infoId}`)
|
const res = await fetch(`${API_BASE}/api/seek/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) {
|
||||||
|
|
@ -158,7 +163,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/information/comment`, {
|
const res = await fetch(`${API_BASE}/api/seek/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 })
|
||||||
|
|
@ -194,7 +199,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/information/seek/match-confirm`, {
|
const res = await fetch(`${API_BASE}/api/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 || '用户已同意' })
|
||||||
|
|
@ -231,7 +236,7 @@ export default function News() {
|
||||||
if (!token) { alert('请先登录'); return }
|
if (!token) { alert('请先登录'); return }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/information/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
|
const res = await fetch(`${API_BASE}/api/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)
|
||||||
|
|
@ -244,7 +249,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/information/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
|
const res = await fetch(`${API_BASE}/api/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)
|
||||||
|
|
@ -256,7 +261,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/information/seek/match?info_id=${infoId}`, {
|
const res = await fetch(`${API_BASE}/api/seek/my-match?info_id=${infoId}`, {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
@ -274,7 +279,7 @@ export default function News() {
|
||||||
// 获取网络数据匹配列表
|
// 获取网络数据匹配列表
|
||||||
const fetchNetworkMatchCollections = async (infoId) => {
|
const fetchNetworkMatchCollections = async (infoId) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
|
const res = await fetch(`${API_BASE}/api/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 || [])
|
||||||
|
|
@ -330,7 +335,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/information/${editingSeek.id}`, {
|
await fetch(`${API_BASE}/api/seek/${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({
|
||||||
|
|
@ -354,14 +359,13 @@ export default function News() {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
// 从edition映射到category
|
// 从edition映射到category
|
||||||
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
|
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
|
||||||
const res = await fetch(`${API_BASE}/api/information/`, {
|
const res = await fetch(`${API_BASE}/api/seek/`, {
|
||||||
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 : None
|
expect_number: seekForm.features ? 'J0' + seekForm.features : null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
@ -451,12 +455,22 @@ 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, '')
|
||||||
const newFeatures = (seekForm.features || '').split('')
|
if (!val) {
|
||||||
|
// 删除操作
|
||||||
|
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('')})
|
setSeekForm({...seekForm, features: newFeatures.join('').slice(0, 8)})
|
||||||
// 自动跳转下一个
|
// 自动跳转下一个
|
||||||
if (val && i < 9) {
|
if (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()
|
||||||
|
|
@ -546,6 +560,22 @@ 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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -558,13 +588,21 @@ 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)
|
// 按创建时间倒序排序
|
||||||
return { avg: Math.round(sum / items.length), count: items.length, items }
|
const sortedItems = [...items].sort((a, b) => {
|
||||||
|
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)}
|
||||||
|
|
@ -575,60 +613,52 @@ export default function News() {
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ overflowX: 'auto' }}>
|
{/* 包装类型筛选 */}
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
|
<div style={{ color: '#94a3b8', fontSize: '11px', marginBottom: '8px' }}>包装筛选:</div>
|
||||||
<thead>
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '12px' }}>
|
||||||
<tr>
|
{[{value: '', label: '全部'}, ...packagings.map(p => ({value: p, label: p}))].map(item => (
|
||||||
<th style={{ padding: '8px', textAlign: 'left', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}></th>
|
<button key={item.value} onClick={() => setDealPackagingFilter(item.value)}
|
||||||
{packagings.map(p => (
|
style={{ padding: '4px 12px', borderRadius: '6px', border: 'none', cursor: 'pointer',
|
||||||
<th key={p} style={{ padding: '8px', textAlign: 'center', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>{p}</th>
|
background: dealPackagingFilter === item.value ? '#8b5cf6' : 'rgba(255,255,255,0.1)',
|
||||||
))}
|
color: '#fff', fontSize: '12px' }}>
|
||||||
</tr>
|
{item.label}
|
||||||
</thead>
|
</button>
|
||||||
<tbody>
|
))}
|
||||||
{(() => {
|
</div>
|
||||||
// 计算每个分类的平均价格
|
|
||||||
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 => {
|
{/* 号码分类筛选 */}
|
||||||
const rowData = packagings.map(pkg => calcAvg(pkg, cat))
|
<div style={{ color: '#94a3b8', fontSize: '11px', marginBottom: '8px' }}>号码分类:</div>
|
||||||
const hasData = rowData.some(d => d !== null)
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '12px' }}>
|
||||||
if (!hasData) return null
|
{[
|
||||||
return (
|
{value: '', label: '全部'},
|
||||||
<tr key={cat}>
|
{value: '带4号', label: '带4号'},
|
||||||
<td style={{ padding: '8px', color: '#fbbf24', fontWeight: '500', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{cat}</td>
|
{value: '带7号', label: '带7号'},
|
||||||
{rowData.map((d, i) => (
|
{value: '永恒号', label: '永恒'},
|
||||||
<td key={i} style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
{value: '钻石号', label: '钻石'},
|
||||||
{d ? (
|
{value: '天马号', label: '天马'},
|
||||||
<div style={{ color: '#22c55e', fontWeight: '600', cursor: 'pointer' }}
|
{value: '金山号', label: '金山'},
|
||||||
onClick={() => setDealDetailItems(d.items)}>
|
{value: '金马号', label: '金马'},
|
||||||
¥{d.avg.toLocaleString()}
|
{value: '倒置号', label: '倒置'},
|
||||||
<span style={{ color: '#64748b', fontSize: '11px', marginLeft: '4px' }}>({d.count})</span>
|
{value: '圆圆号', label: '圆圆'}
|
||||||
</div>
|
].map(item => (
|
||||||
) : <span style={{ color: '#475569' }}>-</span>}
|
<button key={item.value} onClick={() => setDealNumberCatFilter(item.value)}
|
||||||
</td>
|
style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', cursor: 'pointer',
|
||||||
))}
|
background: dealNumberCatFilter === item.value ? '#22c55e' : 'rgba(255,255,255,0.08)',
|
||||||
</tr>
|
color: '#fff', fontSize: '11px' }}>
|
||||||
)
|
{item.label}
|
||||||
})
|
</button>
|
||||||
})()}
|
))}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
|
{/* 清除筛选 */}
|
||||||
|
{(dealPackagingFilter || dealNumberCatFilter) && (
|
||||||
|
<button onClick={() => { setDealPackagingFilter(''); setDealNumberCatFilter(''); }}
|
||||||
|
style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', cursor: 'pointer',
|
||||||
|
background: 'rgba(239,68,68,0.2)', color: '#ef4444', fontSize: '11px' }}>
|
||||||
|
✕ 清除筛选
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -651,7 +681,10 @@ 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|$)/)
|
||||||
|
|
@ -684,7 +717,42 @@ export default function News() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{infoList.map(item => {
|
{infoList.filter(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 || ''
|
||||||
|
|
@ -752,7 +820,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)} | 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'}
|
📅 {formatDate(item.created_at)} | 👤 {item.user_name || '匿名用户'}
|
||||||
</div>
|
</div>
|
||||||
{/* 号码特征 */}
|
{/* 号码特征 */}
|
||||||
{features && (
|
{features && (
|
||||||
|
|
@ -807,7 +875,7 @@ export default function News() {
|
||||||
>
|
>
|
||||||
自有{item.matched_count || 0}条藏品匹配成功
|
自有{item.matched_count || 0}条藏品匹配成功
|
||||||
</span>
|
</span>
|
||||||
{item.network_matched_count !== undefined && (
|
{item.network_matched_count !== undefined && item.network_matched_count > 0 && (
|
||||||
<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)}
|
||||||
|
|
@ -952,7 +1020,10 @@ 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' }}>{c.user_name || '匿名用户'}</div>
|
<div style={{ color: '#3b82f6', fontSize: '12px', marginBottom: '4px' }}>
|
||||||
|
{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>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1045,7 +1116,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 }}>匹配藏品清单(网络数据)</h3>
|
<h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单(网络数据{networkMatchTotalCount > 0 ? `共${networkMatchTotalCount}条` : ''})</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 ? (
|
||||||
|
|
@ -1245,6 +1316,9 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* News_YichensBoard - 一尘帖子页面
|
* News_YichensBoard - 一尘帖子页面
|
||||||
* Version: 1.2.75x
|
* Version: 0.0.1
|
||||||
* 更新:
|
* 更新:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -219,6 +219,9 @@ 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
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Settings - 设置页面
|
* Settings - 设置页面
|
||||||
* Version: 1.2.75x
|
* Version: 0.0.1
|
||||||
* 更新:
|
* 更新:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -435,6 +435,9 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Stats - 统计页面
|
* Stats - 统计页面
|
||||||
* Version: 1.2.70x
|
* Version: 0.0.1
|
||||||
* 更新:
|
* 更新:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,401 @@
|
||||||
|
/**
|
||||||
|
* 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: `
|
||||||
|
## 龙钞号码分类大全
|
||||||
|
|
||||||
|
### 顶级号码(收藏价值最高)
|
||||||
|
|
||||||
|
| 分类 | 条件 | 优先级 |
|
||||||
|
|------|------|--------|
|
||||||
|
| 🏆 圆圆号 | 不含0、1、2、3、4、5、7 | 1 |
|
||||||
|
| 🔄 倒置号 | 不含2、3、4、5、7 | 2 |
|
||||||
|
| 🐎 金马王 | 不含1、2、3、4、7 | 3 |
|
||||||
|
| 🐎 金马号 | 不含2、3、4、7 | 4 |
|
||||||
|
| ⛰️ 金山王 | 不含1、2、4、5、7 | 5 |
|
||||||
|
| ⛰️ 天马王 | 不含1、2、4、7 | 6 |
|
||||||
|
|
||||||
|
### 高级号码
|
||||||
|
|
||||||
|
| 分类 | 条件 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| 🏔️ 金山号 | 不含2、4、5、7 | |
|
||||||
|
| 🐎 天马号 | 不含2、4、7 | |
|
||||||
|
| 🌫️ 朦胧王 | 不含1、3、4、5、7 | |
|
||||||
|
| 🌫️ 朦胧号 | 不含3、4、5、7 | |
|
||||||
|
| 💎 如意号 | 不含1、3、4、7 | |
|
||||||
|
| 💎 钻石号 | 不含3、4、7 | |
|
||||||
|
| ♾️ 永恒号 | 不含4、7 | |
|
||||||
|
|
||||||
|
### 普通号码
|
||||||
|
|
||||||
|
| 分类 | 条件 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| 🔥 带7号 | 不含4 | 不带4的号码 |
|
||||||
|
| ⚠️ 带4号 | 包含4 | 传统说法带4不太好 |
|
||||||
|
|
||||||
|
### 分类说明
|
||||||
|
|
||||||
|
- **"无X"规则**:号码中不包含数字X
|
||||||
|
- **就高不就低**:同时满足多个条件时,按最高价值分类
|
||||||
|
- **散钞看8位**:散钞看全部8位数字
|
||||||
|
- **标十看7位**:标十看后7位
|
||||||
|
- **标百看6位**:标百看后6位
|
||||||
|
`
|
||||||
|
},
|
||||||
|
terms: {
|
||||||
|
title: '📖 术语解释',
|
||||||
|
content: `
|
||||||
|
## 收藏术语大全
|
||||||
|
|
||||||
|
### 基础术语
|
||||||
|
|
||||||
|
| 术语 | 解释 |
|
||||||
|
|------|------|
|
||||||
|
| 冠字号 | J+8位数字,如J01234567 |
|
||||||
|
| 散钞 | 单张收藏的钞票 |
|
||||||
|
| 标十 | 10连号,一刀 |
|
||||||
|
| 标百 | 100连号,一捆 |
|
||||||
|
| 整刀 | 100张连号 |
|
||||||
|
| 整捆 | 1000张连号 |
|
||||||
|
|
||||||
|
### 评级术语
|
||||||
|
|
||||||
|
| 术语 | 解释 |
|
||||||
|
|------|------|
|
||||||
|
| 评级 | 由专业机构鉴定真伪、品相 |
|
||||||
|
| 评级公司 | PCGS、NGC、PMG等 |
|
||||||
|
| 分数 | 评级公司给出的品相分数(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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* YichensBoard - 一尘看板页面
|
* YichensBoard - 一尘看板页面
|
||||||
* Version: 1.2.70x
|
* Version: 0.0.5 (2026-05-13)
|
||||||
* 更新:
|
* 更新:增加复制链接功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
@ -15,6 +15,7 @@ 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('')
|
||||||
|
|
@ -41,18 +42,20 @@ export default function YichensBoard() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPosts = async (p, cat, kw) => {
|
const fetchPosts = async (p, cat, kw, numFilter) => {
|
||||||
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)
|
||||||
|
|
@ -75,6 +78,29 @@ 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 || []
|
||||||
|
|
@ -107,14 +133,14 @@ export default function YichensBoard() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (todayStats.total > 0) {
|
if (todayStats.total > 0) {
|
||||||
fetchPosts(1, categoryFilter, searchKeyword)
|
fetchPosts(1, categoryFilter, searchKeyword, numberFilter)
|
||||||
}
|
}
|
||||||
}, [todayStats, categoryFilter, postTypeFilter])
|
}, [todayStats, categoryFilter, postTypeFilter, numberFilter])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
fetchPosts(1, categoryFilter, searchKeyword)
|
fetchPosts(1, categoryFilter, searchKeyword, numberFilter)
|
||||||
}, [searchKeyword])
|
}, [searchKeyword, numberFilter])
|
||||||
|
|
||||||
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
||||||
|
|
||||||
|
|
@ -199,6 +225,16 @@ 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 }}>
|
||||||
|
|
@ -229,7 +265,12 @@ 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 && <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>}
|
{post.url && (
|
||||||
|
<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>
|
||||||
|
|
@ -243,12 +284,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) }} 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, numberFilter) }} 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 && totalPosts > page * 390 ? (
|
{posts.length >= 390 ? (
|
||||||
<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>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|
@ -256,6 +297,9 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,59 +3,48 @@ 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').trim()
|
const content = readFileSync(versionFile, 'utf-8')
|
||||||
// 移除 VERSION= 前缀
|
const match = content.match(/^VERSION=(.*)$/m)
|
||||||
if (content.startsWith('VERSION=')) {
|
return match ? match[1].trim() : '0.0.0'
|
||||||
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 {
|
try {
|
||||||
name: 'update-html-title',
|
const htmlPath = join(__dirname, 'index.html')
|
||||||
closeBundle() {
|
let htmlContent = readFileSync(htmlPath, 'utf-8')
|
||||||
try {
|
htmlContent = htmlContent.replace(
|
||||||
const htmlPath = join(__dirname, 'dist', 'index.html')
|
/<title>甲辰收藏 v[\d.]+<\/title>/,
|
||||||
let htmlContent = readFileSync(htmlPath, 'utf-8')
|
`<title>甲辰收藏 v${APP_VERSION}</title>`
|
||||||
// 替换 <title>甲辰收藏 vXXX</title>
|
)
|
||||||
htmlContent = htmlContent.replace(
|
writeFileSync(htmlPath, htmlContent, 'utf-8')
|
||||||
/<title>甲辰收藏 v[\d.]+<\/title>/,
|
} catch (e) {}
|
||||||
'<title>甲辰收藏 v' + APP_VERSION + '</title>'
|
|
||||||
)
|
|
||||||
writeFileSync(htmlPath, htmlContent, 'utf-8')
|
|
||||||
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(), updateHtmlTitle()],
|
plugins: [react()],
|
||||||
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: {
|
||||||
entryFileNames: 'assets/[name]-[hash]-[name].js',
|
manualChunks: undefined
|
||||||
chunkFileNames: 'assets/[name]-[hash].js',
|
|
||||||
assetFileNames: 'assets/[name]-[hash].[ext]'
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
treeShaking: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
#!/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 "已推送到远程仓库"
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/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"
|
||||||
Loading…
Reference in New Issue