diff --git a/VERSION b/VERSION index 1ce7217..aea1186 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -VERSION=1.2.41 +1.2.82 diff --git a/backend/VERSION b/backend/VERSION index 1f8d37f..8a27477 100644 --- a/backend/VERSION +++ b/backend/VERSION @@ -1 +1 @@ -1.2.38 +VERSION=1.2.79 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/main.py b/backend/app/main.py index 2ec184f..73ade76 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -52,10 +52,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=["*"], diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 387f670..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") @@ -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/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/information.py b/backend/app/routers/information.py index a249fbb..e620732 100644 --- a/backend/app/routers/information.py +++ b/backend/app/routers/information.py @@ -1,10 +1,11 @@ # 资讯API路由 -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body from sqlalchemy.orm import Session, joinedload from sqlalchemy import text from typing import List, Optional from pydantic import BaseModel from datetime import datetime, date +import os from app.core.database import get_db from app.core.auth import get_current_user @@ -28,6 +29,12 @@ class InformationCreate(BaseModel): expect_price_max: Optional[float] = None deal_price: Optional[float] = None deal_date: Optional[date] = None + packaging: Optional[str] = None + is_graded: Optional[bool] = False + grading_company: Optional[str] = None + grading_score: Optional[str] = None + category: Optional[str] = None + deal_no: Optional[str] = None class InformationUpdate(BaseModel): @@ -42,6 +49,10 @@ class InformationUpdate(BaseModel): expect_price_max: Optional[float] = None deal_price: Optional[float] = None deal_date: Optional[date] = None + packaging: Optional[str] = None + is_graded: Optional[bool] = None + grading_company: Optional[str] = None + grading_score: Optional[str] = None class InformationResponse(BaseModel): @@ -66,6 +77,13 @@ class InformationResponse(BaseModel): view_count: int contact_count: int created_at: datetime + # 评级相关字段 + packaging: Optional[str] = None + is_graded: Optional[bool] = False + grading_company: Optional[str] = None + grading_score: Optional[str] = None + category: Optional[str] = None + deal_no: Optional[str] = None # 用户信息 user_name: Optional[str] = None user_avatar: Optional[str] = None @@ -89,7 +107,7 @@ def get_information_list( info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"), status: str = Query("active", description="状态: active/closed/expired"), page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), current_user: Optional[User] = Depends(get_current_user), db: Session = Depends(get_db), response: Response = None @@ -146,6 +164,12 @@ def get_information_list( collection_category=item.collection.f01_03_category if item.collection else None, collection_version=item.collection.f02_11_version if item.collection else None, collection_number=item.collection.f02_10_prefix_serial if item.collection else None, + packaging=item.packaging, + is_graded=item.is_graded or False, + grading_company=item.grading_company, + grading_score=item.grading_score, + category=item.category, + deal_no=item.deal_no, matched_count=matched_count, network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0, )) @@ -394,6 +418,11 @@ def get_information( view_count=item.view_count, contact_count=item.contact_count, created_at=item.created_at, + packaging=item.packaging, + is_graded=item.is_graded or False, + grading_company=item.grading_company, + grading_score=item.grading_score, + category=item.category, user_name=item.user.f01_01_name if item.user else None, user_avatar=item.user.avatar if item.user else None, collection_name=item.collection.f01_01_name if item.collection else None, @@ -411,6 +440,20 @@ def create_information( db: Session = Depends(get_db) ): """发布资讯""" + # 生成行情编号:日期 + 5位自然数(从00001开始) + deal_no = None + if data.info_type == 'deal': + today = datetime.now().strftime('%Y%m%d') + # 查询当天已有行情数量 + from app.models.models import Information + count_today = db.query(Information).filter( + Information.info_type == 'deal', + Information.deal_no.like(f'DJ{today}%') + ).count() + # 编号 = 日期 + 5位自然数(如 DJ2026041100001) + seq = count_today + 1 + deal_no = f'DJ{today}{seq:05d}' + info = Information( user_id=current_user.f99_90_id, info_type=data.info_type, @@ -425,6 +468,12 @@ def create_information( expect_price_max=data.expect_price_max, deal_price=data.deal_price, deal_date=data.deal_date, + packaging=data.packaging, + is_graded=data.is_graded or False, + grading_company=data.grading_company, + grading_score=data.grading_score, + category=data.category, + deal_no=deal_no, status="active" ) db.add(info) @@ -499,6 +548,16 @@ def update_information( info.deal_price = data.deal_price if data.deal_date is not None: info.deal_date = data.deal_date + if data.packaging is not None: + info.packaging = data.packaging + if data.is_graded is not None: + info.is_graded = data.is_graded + if data.grading_company is not None: + info.grading_company = data.grading_company + if data.grading_score is not None: + info.grading_score = data.grading_score + if data.category is not None: + info.category = data.category db.commit() db.refresh(info) @@ -522,6 +581,11 @@ def update_information( view_count=info.view_count, contact_count=info.contact_count, created_at=info.created_at, + packaging=info.packaging, + is_graded=info.is_graded or False, + grading_company=info.grading_company, + grading_score=info.grading_score, + category=info.category, user_name=current_user.f01_01_name, user_avatar=current_user.avatar, collection_name=info.collection.f01_01_name if info.collection else None, @@ -796,7 +860,7 @@ def get_deal_stats( @router.get("/my/list", response_model=List[InformationResponse]) def get_my_information_list( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): @@ -1070,3 +1134,326 @@ def get_yichen_posts(category: str = None, search: str = None, page: int = 1, pa offset = (page - 1) * page_size items = query.order_by(desc(Information.created_at)).offset(offset).limit(page_size).all() return {"data": items, "pagination": {"page": page, "limit": page_size, "total": total}} + + +@router.get("/seek/stats") +def get_seek_stats( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取寻配号统计数据""" + # 寻号需求数(seek类型且expect_number不为空的总数) + seek_count = db.query(Information).filter( + Information.info_type == 'seek', + Information.expect_number.isnot(None), + Information.expect_number != '' + ).count() + + # 我的匹配:自有藏品匹配成功的寻号帖子数量 + # 即 is_matched = 'confirmed' 的记录,用户ID等于当前用户 + user_matched_count = 0 + if current_user: + user_matched_count = db.query(Information).filter( + Information.info_type == 'seek', + Information.expect_number.isnot(None), + Information.expect_number != '', + Information.matched_user_id == current_user.f99_90_id, + Information.is_matched == 'confirmed' + ).count() + + # 总共匹配:自有匹配成功 + 网络数据匹配成功 + # 自有匹配成功:is_matched = 'confirmed' + # 网络数据匹配成功:查询每个帖子的network_matched_count并求和 + seeks = db.query(Information).filter( + Information.info_type == 'seek', + Information.expect_number.isnot(None), + Information.expect_number != '' + ).all() + + total_self_matched = 0 + total_network_matched = 0 + for seek in seeks: + # 自身匹配成功 + if seek.is_matched == 'confirmed': + total_self_matched += 1 + # 网络数据匹配成功(通过coolbot数据库查询) + if seek.expect_number: + network_count = match_collections_count_from_coolbot(seek.expect_number) + total_network_matched += network_count + + total_matched_count = total_self_matched + total_network_matched + + return { + "seekCount": seek_count, + "userMatchedCount": user_matched_count, + "totalMatchedCount": total_matched_count + } + +# 批量解析行情数据API +@router.post("/batch-parse") +async def batch_parse_deals(text: str = Body(..., embed=True)): + """使用AI智能解析批量行情文本""" + import httpx + import json + import re + + # 使用阿里云百炼Coding Plan API + api_key = "sk-sp-d5ce68bb203e48ca857c2aea25255b26" + base_url = "https://coding.dashscope.aliyuncs.com/v1" + + # 更详细的解析提示词 + prompt = f"""你是一个专业的龙钞行情数据提取助手。请从以下文本中提取所有龙钞行情记录。 + +【解析规则】 +1. 每条记录格式:冠字号 价格 评级/包装 出售者 +2. 冠字号:J0开头的9位数字(如J0298810101) +3. 价格:¥xxx,xxx 格式,去掉逗号转为数字 +4. 评级/包装:PC69/PMG68/爱藏67+/爱藏67 标十 标百 单张 +5. 出售者:人名 +6. 号码分类:根据冠字号数字特征判断(圆圆号/倒置号/金马王/金马号/金山王/天马王/金山号/天马号/朦胧王/朦胧号/如意号/钻石/永恒/无4/通货) + +【输出格式】 +返回JSON数组,每条记录包含: +- serial: 冠字号(完整9位,如J0298810101) +- price: 价格(数字) +- grade: 评级(如PC69, PMG68, 爱藏67+, 爱藏67) +- packaging: 包装类型(标十/标百/单张) +- category: 号码分类 +- seller: 出售者 +- date: 交易日(从文本中提取日期,如2026-03-29) + +只返回JSON数组,不要其他内容。 + +文本: +{text}""" + + try: + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post( + f"{base_url}/chat/completions", + json={ + "model": "qwen3.6-plus", + "messages": [ + {"role": "system", "content": "你是一个专业的收藏品行情数据提取助手,擅长从文本中提取结构化的交易数据。只返回JSON数组。"}, + {"role": "user", "content": prompt} + ], + "temperature": 0.1 + }, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + ) + + if response.status_code != 200: + return {"success": False, "error": f"API错误: {response.status_code}, {response.text[:200]}"} + + result = response.json() + # 阿里云百炼OpenAI兼容格式 + choices = result.get("choices", []) + content = "" + if choices and len(choices) > 0: + content = choices[0].get("message", {}).get("content", "") + + # 解析JSON + try: + # 尝试提取JSON + if "```json" in content: + content = content.split("```json")[1].split("```")[0] + elif "```" in content: + content = content.split("```")[1].split("```")[0] + + # 尝试直接解析 + data = json.loads(content.strip()) + return {"success": True, "data": data} + except json.JSONDecodeError: + # 尝试用正则提取 + match = re.search(r'\[.*\]', content, re.DOTALL) + if match: + try: + data = json.loads(match.group()) + return {"success": True, "data": data} + except: + pass + return {"success": False, "error": "解析失败", "raw": content[:500]} + + except Exception as e: + return {"success": False, "error": str(e)} + +# 本地正则解析函数 +def parse_deals_locally(text: str, default_packaging: str = '', default_date: str = '', default_platform: str = ''): + """本地正则解析批量行情文本""" + import re + from datetime import datetime + results = [] + + # 尝试从文本中提取日期(可能出现在标题或时间戳中) + # 格式如: 3月29日, 2026年3月29日, 2026-03-29 + date_patterns = [ + r'(\d{1,2})月(\d{1,2})日', + r'(\d{4})年(\d{1,2})月(\d{1,2})日', + r'(\d{4})-(\d{1,2})-(\d{1,2})' + ] + + extracted_date = None + for pattern in date_patterns: + match = re.search(pattern, text) + if match: + try: + if len(match.groups()) == 2: + # 3月29日 - 使用当前年份 + extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}" + elif len(match.groups()) == 3: + if int(match.group(1)) > 2000: + # 2026年3月29日 + extracted_date = f"{match.group(1)}-{int(match.group(2)):02d}-{int(match.group(3)):02d}" + else: + # 3月29日格式 + extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}" + break + except: + pass + + # 默认使用今天 + default_date = datetime.now().strftime('%Y-%m-%d') + deal_date = extracted_date or default_date + + lines = text.strip().split('\n') + + for line in lines: + line = line.strip() + if not line or 'J0' not in line: + continue + + # 提取冠字号 J0 + 8-9位数字 + serial_match = re.search(r'J0(\d{8,9})', line) + if not serial_match: + continue + + serial_num = serial_match.group(1) + if len(serial_num) == 9: + serial_num = serial_num[:8] + serial = 'J0' + serial_num + + # 提取价格 ¥xxx,xxx 或 xxx,xxx(必须在J0之后) + serial_pos = line.find(serial) + after_serial = line[serial_pos + len(serial):] + price_match = re.search(r'[¥¥]?\s*([1-9][\d,]{2,})', after_serial) + if not price_match: + continue + price = int(price_match.group(1).replace(',', '')) + + # 提取卖家(价格后面的中文字符) + after_price_pos = after_serial.find(price_match.group(0)) + len(price_match.group(0)) + after_price = after_serial[after_price_pos:] + seller_match = re.search(r'([^\d\s¥¥]+?)(?:\s*$|\s*\n)', after_price) + seller = seller_match.group(1).strip() if seller_match else '' + + # 分类判断 + digits = serial_num + d = digits + category = '通货' + if not any(c in d for c in ['1','2','3','4','5','7']): category = '圆圆号' + elif not any(c in d for c in ['2','3','4','5','7']): category = '倒置号' + elif not any(c in d for c in ['1','2','3','4','7']): category = '金马王' + elif not any(c in d for c in ['2','3','4','7']): category = '金马号' + elif not any(c in d for c in ['1','2','4','5','7']): category = '金山王' + elif not any(c in d for c in ['1','2','4','7']): category = '天马王' + elif not any(c in d for c in ['2','4','5','7']): category = '金山号' + elif not any(c in d for c in ['2','4','7']): category = '天马号' + elif not any(c in d for c in ['1','3','4','5','7']): category = '朦胧王' + elif not any(c in d for c in ['3','4','5','7']): category = '朦胧号' + elif not any(c in d for c in ['1','3','4','7']): category = '如意号' + elif not any(c in d for c in ['3','4','7']): category = '钻石' + elif not any(c in d for c in ['4','7']): category = '永恒' + elif '4' not in d: category = '无4' + + # 提取评级机构 PCGS/PMG/ACG/爱藏 + grade = '' + grading_company = '' + packaging = '单张' + + if 'PC69' in line or 'PC68' in line or 'PC67' in line: + grade_match = re.search(r'PC(6[789]|5\d?)', line) + grade = 'PC' + grade_match.group(1) if grade_match else '' + grading_company = 'PCGS' + elif 'PMG68' in line or 'PMG67' in line: + grade_match = re.search(r'PMG(6[789]|5\d?)', line) + grade = 'PMG' + grade_match.group(1) if grade_match else '' + grading_company = 'PMG' + elif 'ACG' in line: + grade_match = re.search(r'ACG(6[789]|5\d?)', line) + grade = 'ACG' + grade_match.group(1) if grade_match else '' + grading_company = 'ACG' + elif '爱藏67+' in line: + grade = '67+' + grading_company = '爱藏' + elif '爱藏67' in line: + grade = '67' + grading_company = '爱藏' + + # 判断包装类型 + packaging = '单张' + + # 如果传入了默认包装类型,先使用默认 + if default_packaging: + packaging = default_packaging + + # 简化识别:带"刀"字=标百,带"标"字=标十 + if '刀' in line: + packaging = '标百' + elif '标' in line: + packaging = '标十' + + # 尾号判断:如果冠字号尾号是01/11/21/31/41/51/61/71/81/91,且有刀/标字样,基本确认是标百 + if len(serial_num) >= 2: + tail = serial_num[-2:] + if tail in ['01', '11', '21', '31', '41', '51', '61', '71', '81', '91']: + if '刀' in line or ('标' in line and packaging == '单张'): + packaging = '标百' + packaging = '标百' + + # 如果没有刀/标字样,但尾号是01且没有其他特征,可能是标百 + if packaging == '单张' and len(serial_num) >= 2: + tail = serial_num[-2:] + if tail == '01': + # 检查是否在特定语境下 + packaging = '标百' + + # 计算尾号和大小号 + tail_number = '' + size_type = '' + if packaging == '标十' and len(serial_num) >= 2: + tail_number = serial_num[-2:] + size_type = tail_number in ['01','11','21','31','41','51'] and '小号' or '大号' + elif packaging == '标百' and len(serial_num) >= 3: + tail_number = serial_num[-3:] + size_type = tail_number in ['101','201','301','401','501'] and '小号' or '大号' + + results.append({ + 'serial': serial, + 'price': price, + 'category': category, + 'seller': seller, + 'packaging': packaging, + 'grade': grade, + 'grading_company': grading_company, + 'deal_date': deal_date or default_date, # 成交时间 + 'entry_date': default_date, # 录入时间 + 'is_graded': bool(grade), + 'tail_number': tail_number, # 尾号 + 'size_type': size_type, # 大小号 + 'platform': default_platform # 平台 + }) + + return results + +@router.post("/batch-parse-local") +async def batch_parse_deals_local(request: dict = Body(...)): + """本地正则解析批量行情文本(无需AI)""" + text = request.get('text', '') + default_packaging = request.get('defaultPackaging', '') + default_date = request.get('defaultDate', '') + default_platform = request.get('defaultPlatform', '') + results = parse_deals_locally(text, default_packaging, default_date, default_platform) + return {"success": True, "data": results} diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index 079be6e..c8cf2da 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -11,7 +11,7 @@ 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) ): @@ -47,7 +47,7 @@ def get_current_user_info( "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), diff --git a/backend/app/routers/yichens.py b/backend/app/routers/yichens.py index 9b55df7..de8e152 100644 --- a/backend/app/routers/yichens.py +++ b/backend/app/routers/yichens.py @@ -105,36 +105,50 @@ def get_user_stats(db: Session = Depends(get_coolbot_db)): sellers=result[2] or 0 ) -@router.get("/posts", response_model=List[PostItem]) +@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) ): - """获取帖子列表""" - query = """ - SELECT post_id, title, content, category, post_type, price, - author_username, post_time, reply_count, view_count, url - FROM yichens_posts - WHERE 1=1 - """ + """获取帖子列表 - 支持全局搜索,返回总数和分页信息""" + # 构建WHERE条件 + where_clauses = ["1=1"] params = {"limit": limit, "offset": offset} if category: - query += " AND category = :category" + where_clauses.append("category = :category") params["category"] = category if post_type: - query += " AND post_type = :post_type" + where_clauses.append("post_type = :post_type") params["post_type"] = post_type - query += " ORDER BY post_time DESC LIMIT :limit OFFSET :offset" + # 全局搜索 + 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}%" - results = db.execute(text(query), params).fetchall() + where_sql = " AND ".join(where_clauses) - return [PostItem( + # 查询总数 + 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 "", @@ -147,6 +161,13 @@ def get_posts( 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( @@ -250,3 +271,107 @@ async def get_today_category_stats(db: Session = Depends(get_coolbot_db)): """ 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 532db09..f73f4c0 100644 --- a/config/VERSION +++ b/config/VERSION @@ -1 +1 @@ -VERSION=1.2.50 +VERSION=1.2.81 diff --git a/frontend/index.html b/frontend/index.html index 6525875..e3a4d95 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.2.50 + 甲辰收藏 v=1.2.81 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 156d83c..79e65c8 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.81", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jiachenlong-frontend", - "version": "1.2.12", + "version": "1.2.81", "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..c37ff78 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jiachenlong-frontend", - "version": "1.2.12", + "version": "1.2.81", "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..9ccd98f 100644 --- a/frontend/src/pages/Add.jsx +++ b/frontend/src/pages/Add.jsx @@ -1,6 +1,7 @@ -// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入 +// 添加藏品页面 - 支持 AI 识别/手工录入 import React, { useState, useRef } from 'react' import { APP_VERSION } from '../config/version' +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:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}]; // 字段转换函数 @@ -71,12 +72,57 @@ const getDefaultForm = () => ({ }) 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: '' + }) + 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 '无4' + return '通货' + } + // 根据 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('') @@ -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,369 @@ 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' }} /> +
+
+
成交平台(可选)
+ +
+
+ + {/* 包装类型选择 */} +
+
批量默认包装类型(可选)
+
+ + + +
+
+ +