diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e69de29..d8c88cb 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -0,0 +1,2 @@ +from app.models.deal_info import DealInfo +from app.models.models import User, Collection diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py index d909d01..9185671 100644 --- a/backend/app/routers/information.py +++ b/backend/app/routers/information.py @@ -1,5 +1,5 @@ # 资讯API路由 -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Body from sqlalchemy.orm import Session, joinedload from typing import List, Optional from pydantic import BaseModel @@ -1082,3 +1082,179 @@ def get_seek_list_all( content=result, headers={"X-Total-Pages": str(total_pages), "X-Total-Count": str(total_count)} ) +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 = '带7号' + + # 提取评级机构 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}