From a862cf92728df78ad5fb2d1476bd21f72c8c42b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=99=E5=A4=A7?= Date: Wed, 15 Apr 2026 21:42:52 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.98=20-=20=E6=89=B9=E9=87=8F=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 99 --- VERSION | 2 +- backend/VERSION | 2 +- backend/app/core/auth.py | 4 +- backend/app/core/coolbot_db.py | 37 ++ backend/app/core/database.py | 2 +- backend/app/main.py | 24 +- backend/app/models/deal_info.py | 54 ++ backend/app/models/models.py | 15 +- backend/app/models/seek_info.py | 39 ++ backend/app/routers/auth.py | 59 +- backend/app/routers/collections.py | 35 +- backend/app/routers/deal.py | 248 +++++++ backend/app/routers/seek.py | 189 ++++++ backend/app/routers/users.py | 29 +- backend/app/routers/yichens.py | 377 +++++++++++ backend/app/schemas/schemas.py | 2 + backend/app/utils/number_category.py | 132 ++++ config/VERSION | 2 +- env.conf | 8 + frontend/VERSION | 1 + frontend/index.html | 2 +- frontend/package-lock.json | 12 +- frontend/package.json | 8 +- frontend/package.txt | 1 + frontend/src/App.jsx | 11 +- frontend/src/config/version.js | 2 +- frontend/src/pages/Add.jsx | 454 ++++++++++++- frontend/src/pages/Admin.jsx | 55 +- frontend/src/pages/AdminV2.jsx | 272 -------- frontend/src/pages/BatchMode.jsx | 513 --------------- frontend/src/pages/Edit.jsx | 6 +- frontend/src/pages/Edit.jsx.dual_column_bak | 575 ----------------- frontend/src/pages/Home.jsx | 243 +++++-- frontend/src/pages/Info.jsx | 677 -------------------- frontend/src/pages/List.jsx | 372 ++++++++++- frontend/src/pages/Login.jsx | 32 +- frontend/src/pages/News.jsx | 420 +++++++++++- frontend/src/pages/News.jsx.backup_20260328 | 554 ---------------- frontend/src/pages/News_YichensBoard.jsx | 218 +++++++ frontend/src/pages/News_original_backup.jsx | 31 - frontend/src/pages/OCR.jsx | 654 ------------------- frontend/src/pages/Stats.jsx | 4 +- frontend/src/pages/YichensBoard.jsx | 255 ++++++++ frontend/src/pages/news.py | 128 ++++ frontend/src/utils/numberCategory.js | 129 ++++ frontend/vite.config.js | 26 +- start.sh | 13 + 48 files changed, 3466 insertions(+), 3561 deletions(-) delete mode 100644 README.md create mode 100644 backend/app/core/coolbot_db.py create mode 100644 backend/app/models/deal_info.py create mode 100644 backend/app/models/seek_info.py create mode 100644 backend/app/routers/deal.py create mode 100644 backend/app/routers/seek.py create mode 100644 backend/app/routers/yichens.py create mode 100644 backend/app/utils/number_category.py create mode 100644 env.conf create mode 100644 frontend/VERSION create mode 100644 frontend/package.txt delete mode 100644 frontend/src/pages/AdminV2.jsx delete mode 100644 frontend/src/pages/BatchMode.jsx delete mode 100644 frontend/src/pages/Edit.jsx.dual_column_bak delete mode 100644 frontend/src/pages/Info.jsx delete mode 100644 frontend/src/pages/News.jsx.backup_20260328 create mode 100644 frontend/src/pages/News_YichensBoard.jsx delete mode 100644 frontend/src/pages/News_original_backup.jsx delete mode 100644 frontend/src/pages/OCR.jsx create mode 100644 frontend/src/pages/YichensBoard.jsx create mode 100644 frontend/src/pages/news.py create mode 100644 frontend/src/utils/numberCategory.js create mode 100755 start.sh diff --git a/README.md b/README.md deleted file mode 100644 index 4143811..0000000 --- a/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# 甲辰藏品管理系统 - -> 生肖纪念钞收藏管理系统 - -## 版本信息 - -| 项目 | 内容 | -|------|------| -| **版本** | v1.2.2 | -| **代号** | 列表优化版本 | -| **发布日期** | 2026-03-20 | - -## 技术栈 - -- **后端**: FastAPI + PostgreSQL + 阿里云OSS + 阿里云短信 -- **前端**: React + Vite + Tailwind CSS -- **部署**: Nginx - -## 核心功能 - -- 用户管理(管理员/普通用户) -- 藏品管理(CRUD、多条件筛选) -- OCR识别(阿里云视觉智能) -- 统计分析 -- 图片上传(阿里云OSS) -- 短信验证码 -- 用户协议 -- 密码强度验证 - -## 项目结构 - -``` -jiachenlong/ -├── backend/ # FastAPI后端 -│ ├── app/ -│ │ ├── routers/ # API路由 -│ │ ├── services/ # 业务服务 -│ │ └── modules/ # 业务模块 -│ └── config/ -├── frontend/ # React前端 -│ ├── src/pages/ # 页面组件 -│ └── dist/ # 构建产物 -└── config/ # 版本配置 -``` - -## 版本历史 - -| 版本 | 日期 | 说明 | -|------|------|------| -| v1.2.2 | 2026-03-20 | 列表布局优化 | -| v1.2.1 | 2026-03-20 | 密码强度验证、用户协议 | -| v1.2.0 | 2026-03-20 | 管理员查看用户藏品 | -| v1.1.21 | 2026-03-19 | Nginx优化、OSS集成 | -| v1.1.0 | 2026-03-18 | 用户设置功能 | - -## 快速开始 - -```bash -# 后端 -cd backend && pip install -r requirements.txt -python -m uvicorn app.main:app --reload - -# 前端 -cd frontend && npm install && npm run dev -``` - -## API接口 - -### 认证 -- POST /api/auth/register - 注册 -- POST /api/auth/login - 登录 -- GET /api/auth/me - 当前用户 - -### 藏品 -- GET /api/collections - 列表 -- POST /api/collections - 创建 -- PUT /api/collections/{id} - 更新 -- DELETE /api/collections/{id} - 删除 - -## 开发团队 - -| 角色 | 姓名 | 职责 | -|------|------|------| -| 产品负责人 | 酷博特 | 项目总负责 | -| 项目经理 | 龙大 | 项目整体管理 | -| 开发 | 龙二 | 本地开发 | -| 测试 | 龙A | 生产A环境负责人 | -| 测试 | 龙B | 生产B环境负责人 | -| 运维 | 龙运 | 运维负责人 | -| 运维 | 龙镜 | 镜像版本管理负责人 | - -## 文档 - -- [部署手册](https://qcn9r1i8r51d.feishu.cn/docx/ZvEadXZ7XoxUtYxGLoIcMCfgn7g) -- [Git仓库](http://47.253.189.47:3000/coolbot/jiachenlong) - ---- - -© 2026 甲辰收藏 diff --git a/VERSION b/VERSION index 1f8d37f..dd91a8b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.38 +1.2.98 diff --git a/backend/VERSION b/backend/VERSION index 0b1f1ed..dd91a8b 100644 --- a/backend/VERSION +++ b/backend/VERSION @@ -1 +1 @@ -1.2.13 +1.2.98 diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index 602723a..2fe7a71 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -11,7 +11,9 @@ from app.core.database import SessionLocal from app.models.models import User # 配置 -SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production") +SECRET_KEY = os.getenv("SECRET_KEY") +if not SECRET_KEY: + raise ValueError("SECRET_KEY environment variable is not set. Please configure it in production!") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7天 diff --git a/backend/app/core/coolbot_db.py b/backend/app/core/coolbot_db.py new file mode 100644 index 0000000..a0b9014 --- /dev/null +++ b/backend/app/core/coolbot_db.py @@ -0,0 +1,37 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import QueuePool + +COOLBOT_DB_URL = os.getenv( + "COOLBOT_DB_URL", + "postgresql://coolbot:Coolbot123@pgm-bp1t1008h019ez6cno.pg.rds.aliyuncs.com:5432/coolbot_data" +) + +coolbot_engine = create_engine( + COOLBOT_DB_URL, + poolclass=QueuePool, + pool_size=10, + max_overflow=20, + pool_timeout=30, + pool_recycle=1800, + pool_pre_ping=True, + echo=False, + connect_args={ + "connect_timeout": 10, + "application_name": "zodiac-coolbot" + } +) + +CoolbotSession = sessionmaker(autocommit=False, autoflush=False, bind=coolbot_engine) + +def get_coolbot_db(): + """获取coolbot数据库会话""" + db = CoolbotSession() + try: + yield db + except Exception: + db.rollback() + raise + finally: + db.close() diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 2218c71..8683b0b 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) DATABASE_URL = os.getenv( "DATABASE_URL", - "postgresql://postgres:postgres@localhost:5432/zodiac" + "postgresql://postgres:postgres@127.0.0.1:5432/zodiac" ) # 增强版数据库引擎配置 diff --git a/backend/app/main.py b/backend/app/main.py index 7624c39..0587121 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,6 +16,9 @@ from app.routers import auth, collections, operations from app.routers import ocr as ocr_router from app.routers import users as users_router from app.routers import information as information_router +from app.routers import yichens as yichens_router +from app.routers import seek as seek_router +from app.routers import deal as deal_router # 版本信息 - 从 config/VERSION 文件读取 def get_version(): @@ -51,10 +54,11 @@ app = FastAPI( # 设置全局错误处理器 setup_error_handlers(app) -# CORS 配置 +# CORS 配置 - 生产环境限制域名 +ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://47.103.29.111,http://120.55.81.21,https://socoolbot.com").split(",") app.add_middleware( CORSMiddleware, - allow_origins=["*"], # 生产环境应该限制域名 + allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -65,10 +69,10 @@ uploads_dir = "uploads" os.makedirs(uploads_dir, exist_ok=True) app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads") -# 挂载项目静态资源目录(可选,生产环境建议用 Nginx) -# static_dir = Path(__file__).parent.parent.parent / "static" -# if static_dir.exists(): -# app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") +# 挂载项目静态资源目录 +static_dir = Path(__file__).parent.parent / "static" +if static_dir.exists(): + app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") # 添加日志中间件 app.middleware("http")(logging_middleware) @@ -80,7 +84,10 @@ app.include_router(operations.router) app.include_router(ocr_router.router) # OCR 识别 app.include_router(users_router.router) # 当前用户接口 app.include_router(users_router.admin_router) # 管理员用户管理 -app.include_router(information_router.router) # 资讯 +app.include_router(information_router.router) +app.include_router(yichens_router.router) # 一尘看板 +app.include_router(seek_router.router) # 寻配号 +app.include_router(deal_router.router) # 成交行情 @app.get("/") @@ -103,3 +110,6 @@ if __name__ == "__main__": import uvicorn port = int(os.getenv("PORT", "3000")) uvicorn.run(app, host="0.0.0.0", port=port) + +from app.routers import seek as seek_router +from app.routers import deal as deal_router diff --git a/backend/app/models/deal_info.py b/backend/app/models/deal_info.py new file mode 100644 index 0000000..82e3206 --- /dev/null +++ b/backend/app/models/deal_info.py @@ -0,0 +1,54 @@ +from sqlalchemy import Column, String, Text, Float, Date, Boolean, Integer, DateTime +from sqlalchemy.sql import func +from app.core.database import Base +import uuid + +def generate_uuid(): + return str(uuid.uuid4()) + +class DealInfo(Base): + __tablename__ = "deal_info" + + id = Column(String(36), primary_key=True, default=generate_uuid) + user_id = Column(String(36), nullable=True, index=True) + + # 标题和内容 + title = Column(String(255), nullable=False) + content = Column(Text, nullable=True) + + # 成交信息 + deal_price = Column(Float, nullable=True) # 成交价格 + deal_date = Column(Date, nullable=True) # 成交日期 + deal_no = Column(String(20), nullable=True, index=True) # 行情编号(从A000001开始递增) + + # 包装和分类 + packaging = Column(String(50), nullable=True) # 包装(标百/标十/单张) + category = Column(String(100), nullable=True) # 分类 + + # 评级相关 + is_graded = Column(Boolean, default=False) # 是否评级 + grading_company = Column(String(100), nullable=True) # 评级机构 + grading_score = Column(String(50), nullable=True) # 评级分数 + + # 号码特征 + tail_number = Column(String(10), nullable=True) # 尾号 + size_type = Column(String(20), nullable=True) # 大小号 + + # 版别 + version = Column(String(50), nullable=True) # 版别 + + # 交易信息 + platform = Column(String(50), nullable=True) # 成交平台 + seller = Column(String(100), nullable=True) # 出售者 + buyer = Column(String(100), nullable=True) # 购买者 + + # 状态 + status = Column(String(20), default="active") + + # 统计 + view_count = Column(Integer, default=0) + contact_count = Column(Integer, default=0) + + # 时间 + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 40811e7..6ea0838 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -44,7 +44,8 @@ class User(Base): f99_100_points = Column(Integer, default=0) # 积分 f01_11_balance = Column(Float, default=0) # 余额 f01_12_total_amount = Column(Float, default=0) # 累计金额 - f01_13_invite_code = Column(String(20), nullable=True) # 邀请码 + f01_13_invite_code = Column(String(20), nullable=True) # 邀请码(自己的邀请码) + f99_101_invited_count = Column(Integer, default=0) # 通过自己邀请码注册的用户数量 collections = relationship("Collection", back_populates="user", cascade="all, delete-orphan") operations = relationship("Operation", back_populates="user", cascade="all, delete-orphan") @@ -163,7 +164,7 @@ class Information(Base): # 内容描述 content = Column(Text, nullable=True) - + # 关联藏品ID collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True) @@ -179,6 +180,16 @@ class Information(Base): deal_price = Column(Float, nullable=True) deal_date = Column(Date, nullable=True) + # 评级相关字段 + packaging = Column(String(50), nullable=True) + is_graded = Column(Boolean, default=False) + grading_company = Column(String(100), nullable=True) + grading_score = Column(String(50), nullable=True) + category = Column(String(100), nullable=True) + + # 行情编号 + deal_no = Column(String(50), nullable=True, index=True) + # 状态: active-有效, closed-已关闭, expired-已过期 status = Column(String(20), default="active", index=True) diff --git a/backend/app/models/seek_info.py b/backend/app/models/seek_info.py new file mode 100644 index 0000000..eb3fd6c --- /dev/null +++ b/backend/app/models/seek_info.py @@ -0,0 +1,39 @@ +from sqlalchemy import Column, String, Text, Float, Date, Boolean, Integer, DateTime +from sqlalchemy.sql import func +from app.core.database import Base +import uuid + +def generate_uuid(): + return str(uuid.uuid4()) + +class SeekInfo(Base): + __tablename__ = "seek_info" + + id = Column(String(36), primary_key=True, default=generate_uuid) + user_id = Column(String(36), nullable=False, index=True) + + # 标题和内容 + title = Column(String(255), nullable=False) + content = Column(Text, nullable=True) + + # 期望条件(求购条件) + expect_category = Column(String(100), nullable=True) # 期望类别 + expect_version = Column(String(100), nullable=True) # 期望版别 + expect_packaging = Column(String(100), nullable=True) # 期望包装 + expect_number = Column(String(50), nullable=True) # 期望号码 + expect_price_min = Column(Float, nullable=True) # 期望最低价 + expect_price_max = Column(Float, nullable=True) # 期望最高价 + + # 匹配状态 + status = Column(String(20), default="active") # active/closed/expired + is_matched = Column(String(10), default="false") # 是否已匹配 + matched_user_id = Column(String(36), nullable=True) # 匹配的用户ID + matched_contact = Column(String(100), nullable=True) # 匹配的联系方式 + + # 统计 + view_count = Column(Integer, default=0) + contact_count = Column(Integer, default=0) + + # 时间 + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 750a671..0c6fe73 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -13,16 +13,21 @@ router = APIRouter(prefix="/api/auth", tags=["认证"]) def generate_user_code(db): - """生成用户编码,从0001开始""" + """生成用户编码,从201开始,按自然数顺序递增,跳过已存在的""" # 查找最大的user_code max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first() if max_code and max_code[0]: try: num = int(max_code[0]) + 1 - return f"{num:04d}" + if num < 201: + num = 201 + # 检查是否已存在,如果存在则继续递增 + while db.query(User).filter(User.user_code == str(num)).first(): + num += 1 + return str(num) except: pass - return "000501" + return "201" @router.post("/register", response_model=UserResponse) def register(user_data: UserCreate, db: Session = Depends(get_db)): @@ -53,13 +58,25 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)): detail="E00041:该邮箱已被注册,请更换邮箱" ) + # 处理邀请码 + invited_by_user = None + if user_data.invite_code: + # 查找邀请人 + invited_by_user = db.query(User).filter(User.user_code == user_data.invite_code).first() + if not invited_by_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="E00042:邀请码无效" + ) + # 创建用户 import uuid hashed_password = get_password_hash(user_data.password) + generated_code = generate_user_code(db) user = User( f99_90_id=str(uuid.uuid4()), f99_91_user_id=str(uuid.uuid4()), - user_code=generate_user_code(db), + user_code=generated_code, f01_01_name=user_data.f01_01_name, email=user_data.email, phone=user_data.phone, @@ -71,10 +88,35 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)): ) db.add(user) + db.flush() # 确保获取user ID + + # 更新邀请人、被邀请人的关联关系 + if invited_by_user: + # 记录是被谁邀请的 + user.f01_13_invite_code = invited_by_user.user_code + # 增加邀请人的邀请计数 + invited_by_user.f99_101_invited_count = (invited_by_user.f99_101_invited_count or 0) + 1 + + # 生成自己的邀请码(用自己的user_code) + user.f01_13_invite_code = generated_code + db.commit() db.refresh(user) - return user + # 返回用户信息(避免Pydantic序列化问题) + return { + "id": user.f99_90_id, + "username": user.f01_01_name, + "user_code": user.user_code, + "email": user.email, + "phone": user.phone, + "avatar": user.avatar, + "role": user.role, + "level": user.f99_94_level, + "aiCount": user.f99_95_ai_count or 0, + "searchCount": user.f99_96_search_count or 0, + "collectionCount": user.f99_97_collection_count or 0 + } @router.post("/login", response_model=Token) @@ -82,9 +124,12 @@ def login( form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) ): - """用户登录""" - # 查找用户 + """用户登录 - 支持用户名或用户编码登录""" + # 先尝试用户名登录 user = db.query(User).filter(User.f01_01_name == form_data.username).first() + # 如果用户名不存在,尝试用户编码登录 + if not user: + user = db.query(User).filter(User.user_code == form_data.username).first() if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py index bf48d73..d04bb8f 100644 --- a/backend/app/routers/collections.py +++ b/backend/app/routers/collections.py @@ -56,6 +56,7 @@ def to_camel_case(data: dict) -> dict: 'f05_43_repair_fee': 'repairFee', 'f05_44_grading_fee': 'gradingFee', 'f06_50_purpose': 'purpose', + 'images': 'images', } return {mapping.get(k, k): v for k, v in data.items()} @@ -66,11 +67,11 @@ def generate_code(version: str, user_id: str, db: Session) -> str: """自动生成藏品编号 - 按用户独立编码""" import re - # 查询当前用户的非空编码(不与其他用户混算) + # 查询当前用户的非空编码(不与其他用户混算)- 使用行锁防止并发 user_codes = db.query(Collection.f01_02_code).filter( Collection.f01_02_code.isnot(None), Collection.f99_91_user_id == user_id - ).all() + ).with_for_update().all() max_num = 0 for (code,) in user_codes: @@ -131,14 +132,17 @@ def get_collections( # 如果指定 all_users=true,则返回所有用户藏品 from sqlalchemy.orm import joinedload - # 管理员默认查看全库,普通用户只看自己 - if current_user.role == "admin": - # 联表查询获取用户名 + # 管理员默认查看全库,普通用户只看自己,未登录返回空列表 + if current_user is None or current_user.role != "admin": + # 非管理员或未登录用户只能查看自己的藏品(如果没有登录则返回空) + if current_user is None: + return {"data": [], "total": 0, "page": 1, "limit": 20} + query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id) + else: + # 管理员查看所有藏品 query = db.query(Collection, User.f01_01_name.label('owner_name')).join( User, Collection.f99_91_user_id == User.f99_90_id, isouter=True ) - else: - query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id) # 如果指定了user_id参数,则只返回该用户的藏品 if user_id: @@ -201,7 +205,7 @@ def get_collections( data_list = [] for item in data: # 处理联表查询结果 - if current_user.role == "admin": + if current_user is not None and current_user.role == "admin": collection_item, owner_name = item else: collection_item = item @@ -277,12 +281,12 @@ def get_stats( ): """获取藏品统计""" # 获取所有藏品 - if current_user.role == "admin": - all_collections = db.query(Collection).all() - else: + if current_user is None or current_user.role != "admin": all_collections = db.query(Collection).filter( Collection.f99_91_user_id == current_user.f99_90_id ).all() + else: + all_collections = db.query(Collection).all() # 总数 total_count = len(all_collections) @@ -381,7 +385,7 @@ def get_collection( collection = dict(result._mapping) # 非管理员只能查看自己的藏品 - if current_user.role != "admin" and collection.get('f99_91_user_id') != current_user.f99_90_id: + if (current_user is None or current_user.role != "admin") and collection.get('f99_91_user_id') != current_user.f99_90_id: raise HTTPException(status_code=403, detail="无权访问") result_dict = { @@ -495,6 +499,11 @@ def create_collection( } } + # 自动分类:如果未提供号码分类,则根据冠字号自动分类 + if not collection_data.f02_14_number_category and collection_data.f02_10_prefix_serial: + from app.utils.number_category import get_number_category + collection_data.f02_14_number_category = get_number_category(collection_data.f02_10_prefix_serial) + collection = Collection( f99_91_user_id=current_user.f99_90_id, f01_01_name=collection_data.f01_01_name, @@ -708,7 +717,7 @@ async def delete_image( Collection.f99_90_id == image.collection_id ).first() - if collection and current_user.role != "admin" and collection.f99_91_user_id != current_user.f99_90_id: + if collection and (current_user is None or current_user.role != "admin") and collection.f99_91_user_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="E00014: 无权删除此图片") # 删除OSS文件(如果path是OSS URL) diff --git a/backend/app/routers/deal.py b/backend/app/routers/deal.py new file mode 100644 index 0000000..314f3ca --- /dev/null +++ b/backend/app/routers/deal.py @@ -0,0 +1,248 @@ +from fastapi import APIRouter, Depends, Query, HTTPException +from sqlalchemy.orm import Session +from pydantic import BaseModel +from typing import Optional +from datetime import datetime, date +from app.core.database import get_db +from app.core.auth import get_current_user +from app.models.deal_info import DealInfo + +router = APIRouter(prefix="/api/deal", tags=["成交行情"]) + +# ============ Schema ============ +class DealInfoCreate(BaseModel): + title: str + content: Optional[str] = None + deal_price: Optional[float] = None + deal_date: Optional[str] = None # YYYY-MM-DD + packaging: Optional[str] = None + category: Optional[str] = None + is_graded: Optional[bool] = False + grading_company: Optional[str] = None + grading_score: Optional[str] = None + tail_number: Optional[str] = None + size_type: Optional[str] = None + version: Optional[str] = None + platform: Optional[str] = None + seller: Optional[str] = None + buyer: Optional[str] = None + +class DealInfoUpdate(BaseModel): + title: Optional[str] = None + content: Optional[str] = None + deal_price: Optional[float] = None + deal_date: Optional[str] = None + packaging: Optional[str] = None + category: Optional[str] = None + is_graded: Optional[bool] = None + grading_company: Optional[str] = None + grading_score: Optional[str] = None + tail_number: Optional[str] = None + size_type: Optional[str] = None + version: Optional[str] = None + platform: Optional[str] = None + seller: Optional[str] = None + buyer: Optional[str] = None + status: Optional[str] = None + +class DealInfoResponse(BaseModel): + id: str + user_id: Optional[str] + title: str + content: Optional[str] + deal_price: Optional[float] + deal_date: Optional[date] + deal_no: Optional[str] + packaging: Optional[str] + category: Optional[str] + is_graded: Optional[bool] + grading_company: Optional[str] + grading_score: Optional[str] + tail_number: Optional[str] + size_type: Optional[str] + version: Optional[str] + platform: Optional[str] + seller: Optional[str] + buyer: Optional[str] + status: str + view_count: int + contact_count: int + created_at: Optional[datetime] + updated_at: Optional[datetime] + + class Config: + from_attributes = True + +# 生成行情编号 +def generate_deal_no(db: Session): + """生成行情编号,从A000001开始递增""" + last = db.query(DealInfo).order_by(DealInfo.deal_no.desc()).first() + if last and last.deal_no: + # 例如 A000001 -> 2 -> A000002 + num = int(last.deal_no[1:]) + 1 + return f"A{num:06d}" + return "A000001" + +# ============ API ============ +@router.get("/list", response_model=list[DealInfoResponse]) +def get_deal_list( + status: str = Query("active"), + deal_date: Optional[str] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=1000), + user_only: bool = Query(False), # 是否只查看自己的 + current_user: Optional = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取成交行情列表""" + query = db.query(DealInfo).filter(DealInfo.status == status) + + # 我的行情:只查看自己的(管理员也只看自己的) + if user_only and current_user: + query = query.filter(DealInfo.user_id == current_user.f99_90_id) + + # 成交日期过滤 + if deal_date: + query = query.filter(DealInfo.deal_date == deal_date) + + # 排序:优先成交日期倒序,同日按编号倒序 + query = query.order_by(DealInfo.deal_date.desc().nullslast(), DealInfo.deal_no.desc().nullslast()) + + # 分页 + offset = (page - 1) * page_size + items = query.offset(offset).limit(page_size).all() + + return items + +@router.get("/stats") +def get_deal_stats( + db: Session = Depends(get_db) +): + """获取成交行情统计""" + total = db.query(DealInfo).filter(DealInfo.status == "active").count() + + # 按日期统计 + from sqlalchemy import func + date_stats = db.query( + DealInfo.deal_date, + func.count(DealInfo.id).label('count') + ).filter( + DealInfo.status == "active", + DealInfo.deal_date.isnot(None) + ).group_by(DealInfo.deal_date).order_by(DealInfo.deal_date.desc()).limit(10).all() + + return { + "total": total, + "by_date": [{"date": str(d.deal_date), "count": d.count} for d in date_stats] + } + +@router.post("", response_model=DealInfoResponse) +def create_deal( + data: DealInfoCreate, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """创建成交行情""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + # 生成行情编号 + deal_no = generate_deal_no(db) + + # 解析日期 + deal_date = None + if data.deal_date: + try: + deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date() + except: + pass + + deal = DealInfo( + user_id=current_user.f99_90_id if current_user else None, + title=data.title, + content=data.content, + deal_price=data.deal_price, + deal_date=deal_date, + deal_no=deal_no, + packaging=data.packaging, + category=data.category, + is_graded=data.is_graded or False, + grading_company=data.grading_company, + grading_score=data.grading_score, + tail_number=data.tail_number, + size_type=data.size_type, + version=data.version, + platform=data.platform, + seller=data.seller, + buyer=data.buyer, + status="active" + ) + db.add(deal) + db.commit() + db.refresh(deal) + return deal + +@router.get("/{deal_id}", response_model=DealInfoResponse) +def get_deal( + deal_id: str, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取成交行情详情""" + deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first() + if not deal: + raise HTTPException(status_code=404, detail="成交行情不存在") + + # 增加浏览数 + deal.view_count += 1 + db.commit() + + return deal + +@router.put("/{deal_id}", response_model=DealInfoResponse) +def update_deal( + deal_id: str, + data: DealInfoUpdate, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """更新成交行情""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first() + if not deal: + raise HTTPException(status_code=404, detail="成交行情不存在") + + # 处理日期 + if data.deal_date: + try: + data.deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date() + except: + data.deal_date = None + + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(deal, key, value) + + db.commit() + db.refresh(deal) + return deal + +@router.delete("/{deal_id}") +def delete_deal( + deal_id: str, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """删除成交行情""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first() + if not deal: + raise HTTPException(status_code=404, detail="成交行情不存在") + + deal.status = "deleted" + db.commit() + + return {"message": "删除成功"} \ No newline at end of file diff --git a/backend/app/routers/seek.py b/backend/app/routers/seek.py new file mode 100644 index 0000000..76ff298 --- /dev/null +++ b/backend/app/routers/seek.py @@ -0,0 +1,189 @@ +from fastapi import APIRouter, Depends, Query, HTTPException +from sqlalchemy.orm import Session +from pydantic import BaseModel +from typing import Optional +from datetime import datetime +from app.core.database import get_db +from app.core.auth import get_current_user +from app.models.seek_info import SeekInfo + +router = APIRouter(prefix="/api/seek", tags=["寻配号"]) + +# ============ Schema ============ +class SeekInfoCreate(BaseModel): + title: str + content: Optional[str] = None + expect_category: Optional[str] = None + expect_version: Optional[str] = None + expect_packaging: Optional[str] = None + expect_number: Optional[str] = None + expect_price_min: Optional[float] = None + expect_price_max: Optional[float] = None + +class SeekInfoUpdate(BaseModel): + title: Optional[str] = None + content: Optional[str] = None + expect_category: Optional[str] = None + expect_version: Optional[str] = None + expect_packaging: Optional[str] = None + expect_number: Optional[str] = None + expect_price_min: Optional[float] = None + expect_price_max: Optional[float] = None + status: Optional[str] = None + +class SeekInfoResponse(BaseModel): + id: str + user_id: str + title: str + content: Optional[str] + expect_category: Optional[str] + expect_version: Optional[str] + expect_packaging: Optional[str] + expect_number: Optional[str] + expect_price_min: Optional[float] + expect_price_max: Optional[float] + status: str + is_matched: Optional[str] + matched_user_id: Optional[str] + matched_contact: Optional[str] + view_count: int + contact_count: int + created_at: Optional[datetime] + updated_at: Optional[datetime] + + class Config: + from_attributes = True + +# ============ API ============ +@router.get("/list", response_model=list[SeekInfoResponse]) +def get_seek_list( + status: str = Query("active"), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=1000), + user_only: bool = Query(False), + current_user: Optional = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取寻配号列表""" + query = db.query(SeekInfo).filter(SeekInfo.status == status) + + # 我的寻配号:只查看自己的 + if user_only and current_user: + query = query.filter(SeekInfo.user_id == current_user.f99_90_id) + + # 排序 + query = query.order_by(SeekInfo.created_at.desc()) + + # 分页 + offset = (page - 1) * page_size + items = query.offset(offset).limit(page_size).all() + + return items + +@router.get("/stats") +def get_seek_stats( + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取寻配号统计""" + total = db.query(SeekInfo).filter(SeekInfo.status == "active").count() + matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count() + + return { + "total": total, + "matched": matched, + "unmatched": total - matched + } + +@router.post("", response_model=SeekInfoResponse) +def create_seek( + data: SeekInfoCreate, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """创建寻配号""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + seek = SeekInfo( + user_id=current_user.f99_90_id, + title=data.title, + content=data.content, + expect_category=data.expect_category, + expect_version=data.expect_version, + expect_packaging=data.expect_packaging, + expect_number=data.expect_number, + expect_price_min=data.expect_price_min, + expect_price_max=data.expect_price_max, + status="active" + ) + db.add(seek) + db.commit() + db.refresh(seek) + return seek + +@router.get("/{seek_id}", response_model=SeekInfoResponse) +def get_seek( + seek_id: str, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取寻配号详情""" + seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() + if not seek: + raise HTTPException(status_code=404, detail="寻配号不存在") + + # 增加浏览数 + seek.view_count += 1 + db.commit() + + return seek + +@router.put("/{seek_id}", response_model=SeekInfoResponse) +def update_seek( + seek_id: str, + data: SeekInfoUpdate, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """更新寻配号""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + seek = db.query(SeekInfo).filter( + SeekInfo.id == seek_id, + SeekInfo.user_id == current_user.f99_90_id + ).first() + + if not seek: + raise HTTPException(status_code=404, detail="寻配号不存在") + + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(seek, key, value) + + db.commit() + db.refresh(seek) + return seek + +@router.delete("/{seek_id}") +def delete_seek( + seek_id: str, + current_user = Depends(get_current_user), + db: Session = Depends(get_db) +): + """删除寻配号""" + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + seek = db.query(SeekInfo).filter( + SeekInfo.id == seek_id, + SeekInfo.user_id == current_user.f99_90_id + ).first() + + if not seek: + raise HTTPException(status_code=404, detail="寻配号不存在") + + seek.status = "deleted" + db.commit() + + return {"message": "删除成功"} \ No newline at end of file diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index b0c9a00..c7e0fa2 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -2,6 +2,7 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status, Query, Body from sqlalchemy.orm import Session +from sqlalchemy import text from app.core.database import get_db from app.core.auth import get_current_user from app.models.models import User, Collection @@ -11,12 +12,14 @@ router = APIRouter(prefix="/api", tags=["用户"]) # ============ 当前用户接口 ============ -@router.get("/users/me", response_model=UserResponse) +@router.get("/users/me") # 无 response_model,避免 Pydantic 序列化问题 def get_current_user_info( current_user: User = Depends(get_current_user) ): """获取当前登录用户信息""" return { + "id": current_user.f99_90_id, + "username": current_user.f01_01_name, "f99_90_id": current_user.f99_90_id, "f01_01_name": current_user.f01_01_name, "email": current_user.email, @@ -25,11 +28,27 @@ def get_current_user_info( "address": current_user.address, "bio": current_user.bio, "role": current_user.role, + "level": current_user.f99_94_level, + "aiCount": current_user.f99_95_ai_count or 0, + "searchCount": current_user.f99_96_search_count or 0, + "collectionCount": current_user.f99_97_collection_count or 0, + "phoneVerified": current_user.f01_06_phone_verified or False, + "loginCount": current_user.f99_98_login_count or 0, + "lastLogin": current_user.f99_99_last_login.isoformat() if current_user.f99_99_last_login else None, + "gender": current_user.f01_07_gender, + "birthday": current_user.f01_08_birthday.isoformat() if current_user.f01_08_birthday else None, + "region": current_user.f01_09_region, + "realnameVerified": current_user.f01_10_realname_verified or False, + "points": current_user.f99_100_points or 0, + "balance": float(current_user.f01_11_balance) if current_user.f01_11_balance else 0, + "totalAmount": float(current_user.f01_12_total_amount) if current_user.f01_12_total_amount else 0, + "inviteCode": current_user.f01_13_invite_code, + "user_code": current_user.user_code, "f99_92_created_at": current_user.f99_92_created_at.isoformat() if current_user.f99_92_created_at else None, "f99_93_updated_at": current_user.f99_93_updated_at.isoformat() if current_user.f99_93_updated_at else None } -@router.put("/users/me", response_model=UserResponse) +@router.put("/users/me") # 无 response_model,避免 Pydantic 序列化问题 def update_current_user( user_update: UserUpdate, current_user: User = Depends(get_current_user), @@ -96,6 +115,11 @@ def get_users( for u in users: # 统计每个用户的藏品数量 count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count() + # 统计每个用户的行情信息数量 + info_count = db.execute(text( + "SELECT COUNT(*) FROM information WHERE user_id = :user_id" + ), {"user_id": str(u.f99_90_id)}).fetchone()[0] + user_list.append({ "id": u.f99_90_id, "username": u.f01_01_name, @@ -105,6 +129,7 @@ def get_users( "user_code": u.user_code, "created_at": u.f99_92_created_at.isoformat() if u.f99_92_created_at else None, "collectionCount": count, + "infoCount": info_count, "level": u.f99_94_level, "aiCount": u.f99_95_ai_count, "searchCount": u.f99_96_search_count, diff --git a/backend/app/routers/yichens.py b/backend/app/routers/yichens.py new file mode 100644 index 0000000..de8e152 --- /dev/null +++ b/backend/app/routers/yichens.py @@ -0,0 +1,377 @@ +from fastapi import APIRouter, Depends, Query +from sqlalchemy import func, text +from sqlalchemy.orm import Session +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime, date +from app.core.coolbot_db import get_coolbot_db + +router = APIRouter(prefix="/api/yichens", tags=["一尘看板"]) + +# ============ 数据模型 ============ +class YichensPostStats(BaseModel): + total_posts: int + total_deals: int # 出售 + total_wants: int # 求购 + total_replies: int + total_views: int + avg_price: Optional[float] + +class CategoryStat(BaseModel): + category: str + count: int + +class PostItem(BaseModel): + post_id: str + title: str + category: Optional[str] + post_type: str + price: Optional[float] + author_username: str + post_time: str + reply_count: int + view_count: int + url: Optional[str] + content: Optional[str] + +class UserStat(BaseModel): + total_users: int + new_users_today: int + sellers: int + +class UserItem(BaseModel): + user_id: str + username: str + avatar_url: Optional[str] + content: Optional[str] + credit_level: Optional[str] + credit_score: Optional[int] + post_count: int + is_seller: bool + registration_date: Optional[str] + +# ============ 统计接口 ============ +@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)): + """获取帖子统计""" + result = db.execute(text(""" + SELECT + COUNT(*) as total_posts, + COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals, + COUNT(*) FILTER (WHERE post_type = 'want') as total_wants, + COALESCE(SUM(reply_count), 0) as total_replies, + COALESCE(SUM(view_count), 0) as total_views, + AVG(price) as avg_price + FROM yichens_posts + WHERE post_time >= NOW() - INTERVAL '1 day' * :days + """), {"days": days}).fetchone() + + return YichensPostStats( + total_posts=result[0] or 0, + total_deals=result[1] or 0, + total_wants=result[2] or 0, + total_replies=result[3] or 0, + total_views=result[4] or 0, + avg_price=float(result[5]) if result[5] else None + ) + +@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)): + """按分类统计帖子数量""" + results = db.execute(text(""" + SELECT category, COUNT(*) as count + FROM yichens_posts + WHERE post_time >= NOW() - INTERVAL '1 day' * :days + GROUP BY category + ORDER BY count DESC + """), {"days": days}).fetchall() + + return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results] + +@router.get("/stats/users", response_model=UserStat) +def get_user_stats(db: Session = Depends(get_coolbot_db)): + """获取用户统计""" + result = db.execute(text(""" + SELECT + COUNT(*) as total_users, + COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today, + COUNT(*) FILTER (WHERE is_seller = true) as sellers + FROM yichens_users + """)).fetchone() + + return UserStat( + total_users=result[0] or 0, + new_users_today=result[1] or 0, + sellers=result[2] or 0 + ) + +@router.get("/posts") +def get_posts( + limit: int = Query(20, ge=1, le=500), + offset: int = Query(0, ge=0), + category: Optional[str] = None, + post_type: Optional[str] = None, + keyword: Optional[str] = None, + db: Session = Depends(get_coolbot_db) +): + """获取帖子列表 - 支持全局搜索,返回总数和分页信息""" + # 构建WHERE条件 + where_clauses = ["1=1"] + params = {"limit": limit, "offset": offset} + + if category: + where_clauses.append("category = :category") + params["category"] = category + + if post_type: + where_clauses.append("post_type = :post_type") + params["post_type"] = post_type + + # 全局搜索 + if keyword: + where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)") + params["keyword"] = f"%{keyword}%" + + where_sql = " AND ".join(where_clauses) + + # 查询总数 + 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 + + # 查询数据 + data_query = f""" + SELECT post_id, title, content, category, post_type, price, + author_username, post_time, reply_count, view_count, url + FROM yichens_posts + WHERE {where_sql} + ORDER BY post_time DESC LIMIT :limit OFFSET :offset + """ + results = db.execute(text(data_query), params).fetchall() + + posts = [PostItem( + post_id=r[0], + title=r[1] or "", + content=r[2] or "", + category=r[3], + post_type=r[4] or "", + price=float(r[5]) if r[5] else None, + author_username=r[6] or "", + post_time=str(r[7]) if r[7] else "", + reply_count=r[8] or 0, + view_count=r[9] or 0, + url=r[10] + ) for r in results] + + return { + "posts": posts, + "total": total_count, + "page": offset // limit + 1, + "page_size": limit + } + +@router.get("/users", response_model=List[UserItem]) +def get_users( + limit: int = Query(20, ge=1, le=500), + offset: int = Query(0, ge=0), + is_seller: Optional[bool] = None, + db: Session = Depends(get_coolbot_db) +): + """获取用户列表""" + query = """ + SELECT user_id, username, avatar_url, credit_level, credit_score, + post_count, is_seller, registration_date + FROM yichens_users + WHERE 1=1 + """ + params = {"limit": limit, "offset": offset} + + if is_seller is not None: + query += " AND is_seller = :is_seller" + params["is_seller"] = is_seller + + query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset" + + results = db.execute(text(query), params).fetchall() + + return [UserItem( + user_id=r[0], + username=r[1] or "", + avatar_url=r[2], + credit_level=r[3], + credit_score=r[4], + post_count=r[5] or 0, + is_seller=r[6] or False, + registration_date=str(r[7]) if r[7] else None + ) for r in results] + + +@router.get("/stats/today") +async def get_today_stats(db: Session = Depends(get_coolbot_db)): + """获取今日新增帖子统计""" + query = """ + SELECT + COUNT(*) as total, + 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 = '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 horses, + 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 + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + """ + result = db.execute(text(query)).fetchone() + return { + "total": result[0] or 0, + "deals": result[1] or 0, + "wants": result[2] or 0, + "others": result[3] or 0, + "dragons": result[4] or 0, + "horses": result[5] or 0, + "snakes": result[6] or 0, + "tianma": result[7] or 0 + } + +@router.get("/stats/hour") +async def get_hour_stats(db: Session = Depends(get_coolbot_db)): + """获取近一个小时新增帖子统计""" + query = """ + SELECT + COUNT(*) as total, + 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 = '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 horses, + SUM(CASE WHEN category LIKE '%蛇%' THEN 1 ELSE 0 END) as snakes + FROM yichens_posts + WHERE post_time >= NOW() - INTERVAL '1 hour' + """ + result = db.execute(text(query)).fetchone() + return { + "total": result[0] or 0, + "deals": result[1] or 0, + "wants": result[2] or 0, + "others": result[3] or 0, + "dragons": result[3] or 0, + "horses": result[4] or 0, + "snakes": result[5] or 0 + } + +@router.get("/stats/today-category") +async def get_today_category_stats(db: Session = Depends(get_coolbot_db)): + """获取今日帖子分类统计""" + query = """ + SELECT category, COUNT(*) as count + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + GROUP BY category + ORDER BY count DESC + """ + results = db.execute(text(query)).fetchall() + return [{"category": r[0] or "未分类", "count": r[1]} for r in results] + + +@router.get("/stats/dragons-today") +def get_dragons_stats_today( + db: Session = Depends(get_coolbot_db) +): + """获取今日龙钞详细统计数据(按号码分类)- 就高不就低""" + from sqlalchemy import text + + # 1. 带4:包含"带4"、"带四"、"通货" + dai4 = db.execute(text(""" + SELECT + COUNT(*) as total, + 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 + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + AND category LIKE '%龙%' + AND ( + content LIKE '%带4%' OR title LIKE '%带4%' + OR content LIKE '%带四%' OR title LIKE '%带四%' + OR content LIKE '%通货%' OR title LIKE '%通货%' + ) + """)).fetchone() + + # 2. 无4:包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒" + wu4 = db.execute(text(""" + SELECT + COUNT(*) as total, + 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 + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + AND category LIKE '%龙%' + AND ( + content LIKE '%无4%' OR title LIKE '%无4%' + OR content LIKE '%无四%' OR title LIKE '%无四%' + ) + AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%' + 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 '%无四七%' + """)).fetchone() + + # 3. 无47:包含"无47"、"永恒"、"无四七",排除"无247" + wu47 = db.execute(text(""" + SELECT + COUNT(*) as total, + 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 + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + AND category LIKE '%龙%' + AND ( + content LIKE '%无47%' OR title LIKE '%无47%' + OR content LIKE '%永恒%' OR title LIKE '%永恒%' + OR content LIKE '%无四七%' OR title LIKE '%无四七%' + ) + AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' + """)).fetchone() + + # 4. 无247:包含"无247"、"天马"、"金山",排除"无347" + wu247 = db.execute(text(""" + SELECT + COUNT(*) as total, + 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 + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + AND category LIKE '%龙%' + AND ( + content LIKE '%无247%' OR title LIKE '%无247%' + OR content LIKE '%天马%' OR title LIKE '%天马%' + OR content LIKE '%金山%' OR title LIKE '%金山%' + ) + AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%' + """)).fetchone() + + # 5. 无347:包含"无347"、"钻石"、"金马"、"魅力"、"朦胧" + wu347 = db.execute(text(""" + SELECT + COUNT(*) as total, + 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 + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + AND category LIKE '%龙%' + AND ( + 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 '%朦胧%' + ) + """)).fetchone() + + return { + "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}, + "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}, + "wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0} + } + diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py index fe79fe6..a8497b5 100644 --- a/backend/app/schemas/schemas.py +++ b/backend/app/schemas/schemas.py @@ -19,6 +19,7 @@ class UserBase(BaseModel): class UserCreate(UserBase): password: str = Field(..., min_length=6) + invite_code: Optional[str] = Field(None, alias="inviteCode") # 填写的邀请码(选填) class UserUpdate(BaseModel): @@ -50,6 +51,7 @@ class UserResponse(UserBase): f01_11_balance: Optional[float] = Field(0, alias="balance") f01_12_total_amount: Optional[float] = Field(0, alias="totalAmount") f01_13_invite_code: Optional[str] = Field(None, alias="inviteCode") + f99_101_invited_count: Optional[int] = Field(0, alias="invitedCount") f99_90_id: str = Field(..., alias="id") f01_01_name: str = Field(..., alias="username") role: str diff --git a/backend/app/utils/number_category.py b/backend/app/utils/number_category.py new file mode 100644 index 0000000..340e9f2 --- /dev/null +++ b/backend/app/utils/number_category.py @@ -0,0 +1,132 @@ +# 号码分类工具 +# 按MEMORY.md最新规则 (2026-04-02) +# 优先级:数字越小越有价值 + +CATEGORIES = [ + # 1. 圆圆号:无.123457,只能用0689 + {"name": "圆圆号", "cannot_use": ".123457", "must_have": ""}, + # 2. 倒置号:无23457,可用01689必须有1 + {"name": "倒置号", "cannot_use": "23457", "must_have": "1"}, + # 3. 金马王:无12347,可用05689必须有5 + {"name": "金马王", "cannot_use": "12347", "must_have": "5"}, + # 4. 金马号:无2347,可用015689必须有1和5 + {"name": "金马号", "cannot_use": "2347", "must_have": "15"}, + # 5. 金山王:无12457,可用03689必须有3 + {"name": "金山王", "cannot_use": "12457", "must_have": "3"}, + # 6. 天马王:无1247,可用035689必须有3和5 + {"name": "天马王", "cannot_use": "1247", "must_have": "35"}, + # 7. 金山号:无2457,可用013689必须有1和3 + {"name": "金山号", "cannot_use": "2457", "must_have": "13"}, + # 8. 天马号:无247,可用0135689必须有1、3和5 + {"name": "天马号", "cannot_use": "247", "must_have": "135"}, + # 9. 朦胧王:无13457 + {"name": "朦胧王", "cannot_use": "13457", "must_have": ""}, + # 10. 朦胧号:无3457 + {"name": "朦胧号", "cannot_use": "3457", "must_have": ""}, + # 11. 如意号:无1347 + {"name": "如意号", "cannot_use": "1347", "must_have": ""}, + # 12. 钻石号:无347 + {"name": "钻石号", "cannot_use": "347", "must_have": ""}, + # 13. 永恒号:无47 + {"name": "永恒号", "cannot_use": "47", "must_have": ""}, + # 14. 无4号:不包含4 + {"name": "无4号", "cannot_use": "4", "must_have": ""}, + # 15. 通货:含4 + {"name": "通货", "cannot_use": "", "must_have": "4"}, +] + + +def extract_digits(serial: str) -> dict: + """提取冠字号中的数字部分""" + if not serial: + return {"digits": "", "type": "single"} + + nums = serial.replace("J", "").replace(",", "").replace(".", "").strip() + nums = "".join(c for c in nums if c.isdigit()) + + if nums.endswith("01"): + return {"digits": nums[:-2], "type": "hundred"} + elif nums.endswith("1"): + return {"digits": nums[:-1], "type": "ten"} + else: + return {"digits": nums, "type": "single"} + + +def matches_category(digits: str, category: dict) -> bool: + cannot_use = category.get("cannot_use", "") + must_have = category.get("must_have", "") + + # 检查不能用的数字 + for n in cannot_use: + if n in digits: + return False + + # 检查必须有的数字 + if must_have: + for n in must_have: + if n not in digits: + return False + + return True + + +def get_number_category(serial: str) -> str: + """号码分类函数""" + if not serial: + return "" + + # 提取数字 + info = extract_digits(serial) + digits = info["digits"] + + if not digits: + return "" + + # 根据类型取对应位数 + digits_type = info["type"] + if digits_type == "hundred": + # 标百看后6位 + check_digits = digits[-6:] if len(digits) >= 6 else digits + elif digits_type == "ten": + # 标十看后7位 + check_digits = digits[-7:] if len(digits) >= 7 else digits + else: + # 散钞看全部 + check_digits = digits + + # 按优先级匹配分类 + for category in CATEGORIES: + if matches_category(check_digits, category): + return category["name"] + + return "通货" + + +def get_number_category_color(category: str) -> str: + """获取分类颜色""" + colors = { + "圆圆号": "#ef4444", # 红 + "倒置号": "#f97316", # 橙 + "金马王": "#eab308", # 黄 + "金马号": "#84cc16", # 绿 + "金山王": "#22c55e", # 深绿 + "天马王": "#14b8a6", # 青 + "金山号": "#06b6d4", # 蓝 + "天马号": "#0ea5e9", # 浅蓝 + "朦胧王": "#6366f1", # 靛蓝 + "朦胧号": "#8b5cf6", # 紫 + "如意号": "#a855f7", # 深紫 + "钻石号": "#d946ef", # 品红 + "永恒号": "#ec4899", # 粉红 + "无4号": "#64748b", # 灰 + "通货": "#9ca3af", # 浅灰 + } + return colors.get(category, "#9ca3af") + + +def get_category_priority(category: str) -> int: + """获取分类优先级(数字越小越高级)""" + for i, cat in enumerate(CATEGORIES, 1): + if cat["name"] == category: + return i + return 999 \ No newline at end of file diff --git a/config/VERSION b/config/VERSION index 86aee77..dd91a8b 100644 --- a/config/VERSION +++ b/config/VERSION @@ -1 +1 @@ -VERSION=1.2.30 +1.2.98 diff --git a/env.conf b/env.conf new file mode 100644 index 0000000..3f35852 --- /dev/null +++ b/env.conf @@ -0,0 +1,8 @@ +DATABASE_URL=postgresql://jiachenlong:Passwd1%403@pgm-bp1t5w248t7s1pvr.pg.rds.aliyuncs.com:5432/jiachenlong +SECRET_KEY=production-secret-key-b-env +OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG +OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1 +SMS_ACCESS_KEY_ID=LTAI5tQAx5niD7JQVqGE5acE +SMS_ACCESS_KEY_SECRET=QsQFAEKBkaNynIoKyvdIi3BUyWVZu1 +SMS_SIGN_NAME=苏州算力 +SMS_TEMPLATE_CODE=SMS_501590956 diff --git a/frontend/VERSION b/frontend/VERSION new file mode 100644 index 0000000..dd91a8b --- /dev/null +++ b/frontend/VERSION @@ -0,0 +1 @@ +1.2.98 diff --git a/frontend/index.html b/frontend/index.html index 1c7f88f..7335504 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.2.30 + 甲辰收藏 v=1.2.98 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 156d83c..f22a1fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "jiachenlong-frontend", - "version": "1.2.12", + "version": "1.2.82", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jiachenlong-frontend", - "version": "1.2.12", + "version": "1.2.82", "dependencies": { "axios": "^1.7.9", "react": "^18.3.1", @@ -15,7 +15,7 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", - "vite": "^6.0.7" + "vite": "^6.4.2" } }, "node_modules/@babel/code-frame": { @@ -2047,9 +2047,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index ad0ec71..1826e55 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jiachenlong-frontend", - "version": "1.2.12", + "version": "1.2.82", "private": true, "description": "甲辰藏品管理系统 - 移动端前端", "scripts": { @@ -9,13 +9,13 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.1.0", - "axios": "^1.7.9" + "react-router-dom": "^7.1.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", - "vite": "^6.0.7" + "vite": "^6.4.2" } } diff --git a/frontend/package.txt b/frontend/package.txt new file mode 100644 index 0000000..1d01bc1 --- /dev/null +++ b/frontend/package.txt @@ -0,0 +1 @@ +v=1.2.80 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ae35683..34e17ce 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,7 +5,6 @@ import List from './pages/List' import Add from './pages/Add' import Stats from './pages/Stats' import News from './pages/News' -import Info from './pages/Info' import Login from './pages/Login' import Detail from './pages/Detail' import Edit from './pages/Edit' @@ -31,7 +30,6 @@ export default function App() { const basePath = path.split('?')[0] if (basePath === '/') return if (basePath === '/news') return - if (basePath === '/info') return if (basePath === '/stats') return if (basePath === '/list') return if (basePath === '/add') return @@ -53,7 +51,6 @@ export default function App() { } const isAdmin = user && user.role === 'admin' const isEditor = user && user.role === 'editor' - const canAccessInfo = isAdmin || isEditor // 登录页面独立渲染,不显示底部导航 if (!token) { @@ -88,11 +85,10 @@ export default function App() { }}> {[ { path: '/', icon: '🏠', label: '首页' }, - { path: '/news', icon: '📰', label: '资讯' }, + { path: '/news', icon: '📰', label: '行情' }, { path: '/stats', icon: '📊', label: '统计' }, - { path: '/list', icon: '📚', label: '藏品' }, - { path: '/add', icon: '🎯', label: '添加' }, - ...(canAccessInfo ? [{ path: '/info', icon: '📝', label: '信息' }] : []), + { path: '/list', icon: '📚', label: '我的' }, + { path: '/add', icon: '🎯', label: '录入' }, ...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : []) ].map(tab => (
) } +// Fri Apr 10 03:44:24 PM CST 2026 diff --git a/frontend/src/config/version.js b/frontend/src/config/version.js index cf4abcc..e808cde 100644 --- a/frontend/src/config/version.js +++ b/frontend/src/config/version.js @@ -3,7 +3,7 @@ // 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx // 从环境变量读取(vite.config.js 注入) -export const APP_VERSION = import.meta.env.APP_VERSION || '1.0.0' +export const APP_VERSION = import.meta.env.APP_VERSION || '1.2.82' // 版本信息 export const VERSION_INFO = { diff --git a/frontend/src/pages/Add.jsx b/frontend/src/pages/Add.jsx index f09e0cb..09a9208 100644 --- a/frontend/src/pages/Add.jsx +++ b/frontend/src/pages/Add.jsx @@ -1,7 +1,8 @@ -// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入 +// 添加藏品页面 - 支持 AI 识别/手工录入 import React, { useState, useRef } from 'react' import { APP_VERSION } from '../config/version' -const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}]; +const API_BASE = localStorage.getItem('API_BASE') || '' +const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'带7号',label:'带7号'},{value:'带4号',label:'带4号'},{value:'其他',label:'其他'}]; // 字段转换函数 const convertField = (obj) => { @@ -65,18 +66,63 @@ const getDefaultForm = () => ({ name: '藏品', code: '', category: '自持', rarity: '通货', prefixSerial: '', version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张', material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false, - gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '', + gradingCompany: '爱藏', gradingScore: '67+', threeStar: false, specialMark: '', serialFeature: '', numberCategory: '', issuer: '中国人民银行', issueYear: '2024', costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: '' }) export default function Add() { + // 行情录入表单 + const [dealForm, setDealForm] = useState({ + serial: '', + category: '', + packaging: '标十', + price: '', + platform: '抖音', + seller: '', + buyer: '', + date: new Date().toISOString().split('T')[0], + isGraded: false, + gradingCompany: '爱藏', + gradingScore: '67+' + }) + const [batchDefaultPackaging, setBatchDefaultPackaging] = useState('') + const [batchDefaultDate, setBatchDefaultDate] = useState('') + const [batchDefaultPlatform, setBatchDefaultPlatform] = useState('抖音') + const [dealMode, setDealMode] = useState('single') // single-单条录入, batch-批量录入 + const [batchText, setBatchText] = useState('') + const [batchResult, setBatchResult] = useState([]) + const [parsing, setParsing] = useState(false) + const [savingDeal, setSavingDeal] = useState(false) + + // 号码分类函数 + const autoCategory = (serial) => { + const digits = serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9) + if (!digits) return '' + const d = digits + if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '圆圆号' + if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '倒置号' + if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马王' + if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马号' + if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山王' + if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马王' + if (!d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山号' + if (!d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马号' + if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧王' + if (!d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧号' + if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '如意号' + if (!d.includes('3') && !d.includes('4') && !d.includes('7')) return '钻石号' + if (!d.includes('4') && !d.includes('7')) return '永恒号' + if (!d.includes('4')) return '带7号' + return '带4号' + } + // 根据 URL 参数确定默认标签 const params = new URLSearchParams(window.location.hash.split('?')[1] || '') const modeParam = params.get('mode') const defaultTab = modeParam === 'manual' ? 'manual' : 'ai' - const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, batch + const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, deal const [form, setForm] = useState(getDefaultForm()) const [saving, setSaving] = useState(false) const [error, setError] = useState('') @@ -158,8 +204,8 @@ export default function Add() { else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347' else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247' else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47' - else if (digits.includes('7') && !digits.includes('4')) cat = '无4' - else if (digits.includes('4')) cat = '带4' + else if (digits.includes('7') && !digits.includes('4')) cat = '带7号' + else if (digits.includes('4')) cat = '带4号' else cat = '其他' } setForm(prev => ({ ...prev, numberCategory: cat })) @@ -274,8 +320,8 @@ export default function Add() { else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347' else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247' else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47' - else if (digits.includes('7') && !digits.includes('4')) cat = '无4' - else if (digits.includes('4')) cat = '带4' + else if (digits.includes('7') && !digits.includes('4')) cat = '带7号' + else if (digits.includes('4')) cat = '带4号' else cat = '其他' formWithCategory.numberCategory = cat } @@ -405,15 +451,15 @@ export default function Add() {
-
@@ -435,7 +481,7 @@ export default function Add() { style={{ padding: '12px 24px', background: 'rgba(239, 68, 68, 0.2)', color: '#ef4444', border: '1px solid #ef4444', borderRadius: '8px', fontSize: '14px', cursor: 'pointer' }}>🗑️ 重新选择 @@ -657,16 +703,380 @@ export default function Add() { )} - {/* 批量录入模式 */} - {activeTab === 'batch' && ( -
-
🚧
-
批量录入开发中
-
敬请期待后续版本
-
- )} + {/* 版本号 */} + + {/* 行情录入模式 */} + {activeTab === 'deal' && ( +
+
+
成交行情录入
+ + {/* 单条/批量切换 */} +
+ + +
+ + {/* 单条录入 */} + {dealMode === 'single' && ( +
+
+
冠字号 *
+ setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})} + placeholder="J0xxxxxxxx" + style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} /> +
+ + {dealForm.category && ( +
+
号码分类
+
{dealForm.category}
+
+ )} + +
+
包装类型
+
+ {['单张', '标十', '标百'].map(p => ( + + ))} +
+
+ +
+
成交价格 *
+ setDealForm({...dealForm, price: e.target.value})} + placeholder="请输入成交价格" + style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} /> +
+ +
+
成交平台 *
+ +
+ +
+
+
出售者
+ setDealForm({...dealForm, seller: e.target.value})} + placeholder="请输入出售者" + style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} /> +
+
+
购买者
+ setDealForm({...dealForm, buyer: e.target.value})} + placeholder="请输入购买者" + style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} /> +
+
+ +
+
成交日期 *
+ setDealForm({...dealForm, date: e.target.value})} + style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} /> +
+ + {/* 评级字段 */} +
+
评级(可选)
+
+ + setDealForm({...dealForm, gradingScore: e.target.value})} + placeholder="评级分数" + style={{ flex: 1, padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }} /> +
+
+ + +
+ )} + + {/* 批量录入 */} + {dealMode === 'batch' && ( +
+ {/* 日期和平台 */} +
+
+
默认日期(可选)
+ setBatchDefaultDate(e.target.value)} + style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '12px' }} /> +
+
+
成交平台(可选)
+ +
+
+ + {/* 包装类型选择 */} +
+
批量默认包装类型(可选)
+
+ + + +
+
+ +