Compare commits

...

22 Commits

Author SHA1 Message Date
甲辰生产 a9a68de436 v1.2.98 - 同步所有文件版本号与VERSION.json一致 2026-04-19 21:58:28 +08:00
甲辰生产 36b467895c v1.2.98 - 完善版本管理体系:代码文件添加版本注释 2026-04-19 21:34:14 +08:00
甲辰生产 591db03ee1 v1.2.98 - 分离前后端版本文件,独立管理 2026-04-19 21:17:06 +08:00
甲辰生产 fb987dc5b9 v1.2.98 - 修复前端构建版本号不更新问题 2026-04-19 21:11:49 +08:00
甲辰生产 d578289c7c v1.2.98 - 更新版本号 2026-04-19 21:10:11 +08:00
甲辰生产 b0f28466c8 v1.2.98 - 添加前端版本文件 2026-04-19 21:09:50 +08:00
甲辰生产 57e51f6c28 v1.2.98 - 修复管理员用户列表行情数显示问题,改为显示实际发布的行情数量 2026-04-19 20:07:24 +08:00
甲辰生产 524601afc3 fix: 一尘看板按最新帖子发表时间排序 - 使用COALESCE(post_time, crawled_at) 2026-04-17 15:13:50 +08:00
甲辰生产 58c2291e83 fix: 修复统计页面无数据问题 - base_filter判断逻辑修正 2026-04-17 14:19:07 +08:00
甲辰生产 4ae29eb4bf v1.2.97 - 完整同步 2026-04-15 17:48:30 +08:00
甲辰生产 257ca40079 v1.2.97 - 修复行情录入接口、优化权限控制 2026-04-15 17:48:08 +08:00
甲辰生产 aec6ef7c79 feat: 表拆分seek_info和deal_info,优化显示和分页 2026-04-14 23:47:16 +08:00
甲辰生产 ea6528b7f3 v1.2.93 - 成交行情列表优化、行情编号格式调整 2026-04-14 16:56:29 +08:00
甲辰生产 e381427138 fix: 行情编号改为纯数字格式(如2404140001) 2026-04-14 16:34:39 +08:00
甲辰生产 0ffc10ad9a fix: 行情编号改为年份后两位+月日+4位序号(如DJ2404140001) 2026-04-14 16:19:56 +08:00
甲辰生产 c1f3b9bd3c fix: remove invalid category field check 2026-04-14 15:35:51 +08:00
甲辰生产 d2c4d5bf5f fix: handle Optional[User] in update_information 2026-04-14 15:31:02 +08:00
甲辰生产 38f1da12b5 fix: handle None f99_90_id in update_information 2026-04-14 15:28:32 +08:00
甲辰生产 2204e3a2af fix: InformationUpdate content field make optional 2026-04-14 15:24:10 +08:00
甲辰生产 e45899db0c v1.2.92 - 成交行情页面显示500条数据 2026-04-14 08:39:46 +08:00
甲辰生产 9fe3f77d45 v1.2.91 - 修复单条录入冠字号生成规则(7位→J0+0+数字,8位→J0+数字,9位→J0+后8位) 2026-04-14 08:02:45 +08:00
甲辰生产 2306e35dec v1.2.90 - 修复行情编辑保存、编号格式、默认值等问题 2026-04-13 09:08:19 +08:00
40 changed files with 5726 additions and 593 deletions

3
.gitignore vendored
View File

@ -32,6 +32,3 @@ Thumbs.db
.vscode/
*.swp
*.swo
# 版本文件
VERSION

View File

@ -1 +0,0 @@
VERSION=1.2.84

View File

@ -1 +1 @@
VERSION=1.2.79
1.2.98

View File

@ -17,6 +17,8 @@ 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():
@ -84,6 +86,8 @@ app.include_router(users_router.router) # 当前用户接口
app.include_router(users_router.admin_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("/")
@ -106,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

View File

@ -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())

View File

@ -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())

View File

@ -1,242 +1,489 @@
# 认证路由 - 使用字段编码
# auth - 认证路由
# Version: 1.2.90
# 更新:
from fastapi import APIRouter
# Version: 1.2.x
# 更新:
from fastapi import APIRouter, Depends, HTTPException, status, Body
# 更新:
from fastapi.security import OAuth2PasswordRequestForm
# 更新:
from sqlalchemy.orm import Session
# 更新:
from app.core.database import get_db
# 更新:
from app.core.auth import verify_password, create_access_token, get_password_hash, get_current_user
# 更新:
from app.models.models import User
# 更新:
from app.schemas.schemas import Token, UserCreate, UserResponse
# 更新:
# 更新:
router = APIRouter(prefix="/api/auth", tags=["认证"])
# 更新:
# 更新:
# 更新:
# 更新:
# 更新:
def generate_user_code(db):
# 更新:
"""生成用户编码从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
# 更新:
if num < 201:
# 更新:
num = 201
# 更新:
# 检查是否已存在,如果存在则继续递增
# 更新:
while db.query(User).filter(User.user_code == str(num)).first():
# 更新:
num += 1
# 更新:
return str(num)
# 更新:
except:
# 更新:
pass
# 更新:
return "201"
# 更新:
# 更新:
@router.post("/register", response_model=UserResponse)
# 更新:
def register(user_data: UserCreate, db: Session = Depends(get_db)):
# 更新:
"""用户注册"""
# 更新:
# 检查用户名是否已存在
# 更新:
existing_user = db.query(User).filter(User.f01_01_name == user_data.f01_01_name).first()
# 更新:
if existing_user:
# 更新:
raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="f01_01_name: 用户名已存在"
# 更新:
)
# 更新:
# 更新:
# 检查邮箱是否已存在
# 更新:
# 更新:
# 检查手机号是否已存在
# 更新:
if user_data.phone:
# 更新:
existing_phone = db.query(User).filter(User.phone == user_data.phone).first()
# 更新:
if existing_phone:
# 更新:
raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="E00040:该手机号已被注册,请更换手机号"
# 更新:
)
# 更新:
if user_data.email:
# 更新:
existing_email = db.query(User).filter(User.email == user_data.email).first()
# 更新:
if existing_email:
# 更新:
raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
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=generated_code,
# 更新:
f01_01_name=user_data.f01_01_name,
# 更新:
email=user_data.email,
# 更新:
phone=user_data.phone,
# 更新:
avatar=user_data.avatar,
# 更新:
address=user_data.address,
# 更新:
bio=user_data.bio,
# 更新:
password=hashed_password,
# 更新:
role="user"
# 更新:
)
# 更新:
# 更新:
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)
# 更新:
# 更新:
# 返回用户信息避免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)
# 更新:
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,
# 更新:
detail="E00011: 用户名或密码错误",
# 更新:
headers={"WWW-Authenticate": "Bearer"},
# 更新:
)
# 更新:
# 更新:
# 验证密码
# 更新:
if not verify_password(form_data.password, user.password):
# 更新:
raise HTTPException(
# 更新:
status_code=status.HTTP_401_UNAUTHORIZED,
# 更新:
detail="E00011: 用户名或密码错误",
# 更新:
headers={"WWW-Authenticate": "Bearer"},
# 更新:
)
# 更新:
# 更新:
# 更新登录次数和最后登录时间
# 更新:
from datetime import datetime
# 更新:
user.f99_98_login_count = (user.f99_98_login_count or 0) + 1
# 更新:
user.f99_99_last_login = datetime.now()
# 更新:
db.commit()
# 更新:
# 更新:
# 生成 token
# 更新:
access_token = create_access_token(data={"sub": user.f99_90_id})
# 更新:
# 更新:
return {
# 更新:
"access_token": access_token,
# 更新:
"token_type": "bearer"
# 更新:
}
# 更新:
# 更新:
# 更新:
@router.get("/me", response_model=UserResponse)
# 更新:
def get_current_user_info(
# 更新:
current_user: User = Depends(lambda: None)
# 更新:
):
# 更新:
"""获取当前用户信息"""
# 更新:
raise HTTPException(
# 更新:
status_code=status.HTTP_501_NOT_IMPLEMENTED,
# 更新:
detail="请使用正确的依赖注入"
# 更新:
)
# 更新:
# 更新:
# 更新:
@router.post("/change-password")
# 更新:
def change_password(
# 更新:
old_password: str = Body(...),
# 更新:
new_password: str = Body(...),
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""修改当前用户密码"""
# 更新:
from app.core.auth import verify_password, get_password_hash
# 更新:
# 更新:
# 在当前session中重新查询用户
# 更新:
user = db.query(User).filter(User.f99_90_id == current_user.f99_90_id).first()
# 更新:
if not user:
# 更新:
raise HTTPException(status_code=404, detail="用户不存在")
# 更新:
# 更新:
# 验证旧密码
# 更新:
if not verify_password(old_password, user.password):
# 更新:
raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="当前密码错误"
# 更新:
)
# 更新:
# 更新:
# 更新密码
# 更新:
user.password = get_password_hash(new_password)
# 更新:
db.commit()
# 更新:
# 更新:
return {"message": "密码修改成功"}
# 更新:
# 更新:
# 更新:
# ============ 短信验证码接口 ============
# 更新:
# 更新:
@router.post("/send-verification-code")
# 更新:
def send_verification_code(
# 更新:
phone: str = Body(..., min_length=11, max_length=11),
# 更新:
purpose: str = Body("register") # register | login | reset_password
# 更新:
):
# 更新:
"""发送短信验证码"""
# 更新:
from app.services.sms import send_verification_code as send_sms
# 更新:
# 更新:
# 验证手机号格式
# 更新:
if not phone.startswith("1") or len(phone) != 11:
# 更新:
return {"success": False, "message": "手机号格式不正确"}
# 更新:
# 更新:
result = send_sms(phone)
# 更新:
# 更新:
if result["success"]:
# 更新:
return {
# 更新:
"success": True,
# 更新:
"message": f"验证码已发送到 {phone[:3]}****{phone[7:]}",
# 更新:
"expire": result.get("expire", 300)
# 更新:
}
# 更新:
else:
# 更新:
return result
# 更新:
# 更新:
# 更新:
@router.post("/verify-code")
# 更新:
def verify_code(
# 更新:
phone: str = Body(...),
# 更新:
code: str = Body(..., min_length=6, max_length=6)
# 更新:
):
# 更新:
"""验证短信验证码(仅验证,不执行后续操作)"""
# 更新:
from app.services.sms import verify_code as check_code
# 更新:
# 更新:
is_valid = check_code(phone, code)
# 更新:
# 更新:
if is_valid:
# 更新:
return {"success": True, "message": "验证成功"}
# 更新:
else:
# 更新:
return {"success": False, "message": "验证码错误或已过期"}
# 更新:

File diff suppressed because it is too large Load Diff

502
backend/app/routers/deal.py Normal file
View File

@ -0,0 +1,502 @@
# deal - 成交行情路由
# Version: 1.2.85
# 更新:
from fastapi import APIRouter, Depends, Query, HTTPException
# 更新:
# Version: 1.2.x
# 更新:
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": "删除成功"}
# 更新:

File diff suppressed because it is too large Load Diff

View File

@ -1,128 +1,262 @@
# news - 新闻路由
# Version: 1.2.80
# 更新:
from fastapi import APIRouter, Depends, HTTPException, Query
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy import Table, MetaData
# 更新:
from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel
# 更新:
from typing import Optional, List
# 更新:
from datetime import datetime, date
# 更新:
from app.core.database import get_db, engine
# 更新:
from app.models.models import User
# 更新:
from app.routers.auth import get_current_user
# 更新:
# 更新:
router = APIRouter(prefix="/api/news", tags=["资讯"])
# 更新:
metadata = MetaData()
# 更新:
# 更新:
# 分类表
# 更新:
categories_table = Table('news_categories', metadata, autoload_with=engine)
# 更新:
news_table = Table('news', metadata, autoload_with=engine)
# 更新:
user_posts_table = Table('user_posts', metadata, autoload_with=engine)
# 更新:
users_table = Table('users', metadata, autoload_with=engine)
# 更新:
deals_table = Table('deals', metadata, autoload_with=engine)
# 更新:
notifications_table = Table('notifications', metadata, autoload_with=engine)
# 更新:
# 更新:
# ============ 获取分类 ============
# 更新:
@router.get("/categories")
# 更新:
def get_categories(db: Session = Depends(get_db)):
# 更新:
results = db.query(categories_table).order_by(categories_table.c.sort_order).all()
# 更新:
return [dict(r._mapping) for r in results]
# 更新:
# 更新:
# ============ 获取资讯 ============
# 更新:
@router.get("")
# 更新:
def get_news(
# 更新:
category_id: Optional[int] = None,
# 更新:
page: int = 1,
# 更新:
limit: int = 20,
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
query = db.query(news_table)
# 更新:
if category_id:
# 更新:
query = query.filter(news_table.c.category_id == category_id)
# 更新:
offset = (page - 1) * limit
# 更新:
results = query.order_by(news_table.c.created_at.desc()).offset(offset).limit(limit).all()
# 更新:
return [dict(r._mapping) for r in results]
# 更新:
# 更新:
# ============ 获取用户发布 ============
# 更新:
@router.get("/posts")
# 更新:
def get_posts(
# 更新:
post_type: Optional[str] = None,
# 更新:
status: str = "active",
# 更新:
page: int = 1,
# 更新:
limit: int = 20,
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
query = db.query(user_posts_table).filter(user_posts_table.c.status == status)
# 更新:
if post_type:
# 更新:
query = query.filter(user_posts_table.c.post_type == post_type)
# 更新:
offset = (page - 1) * limit
# 更新:
results = query.order_by(user_posts_table.c.created_at.desc()).offset(offset).limit(limit).all()
# 更新:
return [dict(r._mapping) for r in results]
# 更新:
# 更新:
# ============ 创建发布 ============
# 更新:
class PostCreate(BaseModel):
# 更新:
post_type: str
# 更新:
title: str
# 更新:
content: Optional[str] = None
# 更新:
zodiac_type: Optional[str] = None
# 更新:
packaging: Optional[str] = None
# 更新:
# 更新:
@router.post("/posts")
# 更新:
def create_post(
# 更新:
post: PostCreate,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
result = db.execute(user_posts_table.insert().values(
# 更新:
user_id=current_user.f99_90_id,
# 更新:
post_type=post.post_type,
# 更新:
title=post.title,
# 更新:
content=post.content,
# 更新:
zodiac_type=post.zodiac_type,
# 更新:
packaging=post.packaging,
# 更新:
status="pending"
# 更新:
))
# 更新:
db.commit()
# 更新:
return {"success": True, "id": result.inserted_primary_key[0]}
# 更新:
# 更新:
# ============ 成交数据 ============
# 更新:
@router.get("/deals")
# 更新:
def get_deals(
# 更新:
zodiac_type: Optional[str] = None,
# 更新:
limit: int = 20,
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
query = db.query(deals_table)
# 更新:
if zodiac_type:
# 更新:
query = query.filter(deals_table.c.zodiac_type == zodiac_type)
# 更新:
results = query.order_by(deals_table.c.deal_date.desc()).limit(limit).all()
# 更新:
return [dict(r._mapping) for r in results]
# 更新:
# 更新:
# ============ 通知 ============
# 更新:
@router.get("/notifications")
# 更新:
def get_notifications(limit: int = 10, db: Session = Depends(get_db)):
# 更新:
results = db.query(notifications_table).filter(
# 更新:
notifications_table.c.is_published == True
# 更新:
).order_by(notifications_table.c.created_at.desc()).limit(limit).all()
# 更新:
return [dict(r._mapping) for r in results]
# 更新:
# 更新:
# ============ 首页数据 ============
# 更新:
@router.get("/home")
# 更新:
def get_home(db: Session = Depends(get_db)):
# 更新:
# 推荐发布
# 更新:
posts = db.query(user_posts_table).filter(
# 更新:
user_posts_table.c.status == "active"
# 更新:
).order_by(user_posts_table.c.created_at.desc()).limit(10).all()
# 更新:
# 更新:
# 成交
# 更新:
deals = db.query(deals_table).order_by(
# 更新:
deals_table.c.deal_date.desc()
# 更新:
).limit(10).all()
# 更新:
# 更新:
# 通知
# 更新:
notices = db.query(notifications_table).filter(
# 更新:
notifications_table.c.is_published == True
# 更新:
).order_by(notifications_table.c.created_at.desc()).limit(5).all()
# 更新:
# 更新:
return {
# 更新:
"posts": [dict(p._mapping) for p in posts],
# 更新:
"deals": [dict(d._mapping) for d in deals],
# 更新:
"notices": [dict(n._mapping) for n in notices]
# 更新:
}
# 更新:

View File

@ -1,384 +1,770 @@
# OCR 识别路由 - 专业人民币生肖纪念钞鉴定
# ocr - OCR识别路由
# Version: 1.2.75
# 更新:
import os
# 更新:
import uuid
# 更新:
import base64
# 更新:
import httpx
# 更新:
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
# 更新:
from sqlalchemy.orm import Session
# 更新:
from app.core.database import get_db
# 更新:
from app.core.auth import get_current_user
# 更新:
from app.models.models import User
# 更新:
# 更新:
router = APIRouter(prefix="/api/ocr", tags=["OCR 识别"])
# 更新:
# 更新:
# 阿里云 DashScope API 配置
# 更新:
DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY", "sk-9389024a37da4f7bb455ac9a6b28776f")
# 更新:
# 更新:
# 阿里云 OSS 配置
# 更新:
OSS_CONFIG = {
# 更新:
"access_key_id": os.getenv("OSS_ACCESS_KEY_ID", "LTAI5t6HUnpFBLEK9194kPVG"),
# 更新:
"access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET", "LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"),
# 更新:
"bucket_name": "jiachenlong-oss",
# 更新:
"endpoint": "oss-cn-hangzhou.aliyuncs.com",
# 更新:
"public_url": "https://jiachenlong-oss.oss-cn-hangzhou.aliyuncs.com"
# 更新:
}
# 更新:
# 更新:
# 临时上传目录用于OCR识别本地备选
# 更新:
UPLOAD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "uploads", "temp")
# 更新:
os.makedirs(UPLOAD_DIR, exist_ok=True)
# 更新:
# 更新:
# 更新:
def get_oss_path(file_type: str, user_id: str = None, collection_id: str = None, filename: str = None):
# 更新:
"""生成OSS路径 - 按年/月/日分类"""
# 更新:
from datetime import datetime
# 更新:
now = datetime.now()
# 更新:
year = now.strftime("%Y")
# 更新:
month = now.strftime("%m")
# 更新:
day = now.strftime("%d")
# 更新:
# 更新:
if file_type == "temp":
# 更新:
# 临时文件: temp/{year}/{month}/{day}/{uuid}.{ext}
# 更新:
import uuid
# 更新:
unique_id = str(uuid.uuid4())
# 更新:
ext = filename.split('.')[-1] if filename and '.' in filename else 'jpg'
# 更新:
return f"temp/{year}/{month}/{day}/{unique_id}.{ext}", unique_id
# 更新:
# 更新:
elif file_type == "collection":
# 更新:
# 藏品文件: collections/{user_id}/{year}/{collection_id}/{filename}
# 更新:
if not user_id or not collection_id:
# 更新:
raise ValueError("user_id and collection_id required for collection")
# 更新:
return f"collections/{user_id}/{year}/{collection_id}/{filename}"
# 更新:
# 更新:
elif file_type == "avatar":
# 更新:
# 头像: avatars/{user_id}/avatar.{ext}
# 更新:
if not user_id:
# 更新:
raise ValueError("user_id required for avatar")
# 更新:
ext = filename.split('.')[-1] if filename and '.' in filename else 'jpg'
# 更新:
return f"avatars/{user_id}/avatar.{ext}"
# 更新:
# 更新:
return None
# 更新:
# 更新:
# 更新:
# 上传图片到OSS - 使用服务层(带压缩)
# 更新:
from app.services.oss import upload_to_oss as oss_upload
# 更新:
# 更新:
def upload_to_oss(file_data, oss_key):
# 更新:
"""上传文件到阿里云OSS带自动压缩"""
# 更新:
return oss_upload(file_data, oss_key)
# 更新:
# 更新:
# 专业提示词
# 更新:
PROFESSIONAL_PROMPT = """你是一名专业的人民币生肖纪念钞鉴定专家。请严格按照以下步骤分析这张图片,并提取准确信息。
# 更新:
# 更新:
识别流程
# 更新:
1. 判断类型是否评级钞首先确认是否为裸钞还是评级钞有封装盒和标签
# 更新:
2. 验证纪念钞特征对照生肖纪念钞特征进行确认
# 更新:
3. 验证评级类型'标十'字眼的为标十'百连'字眼的为标百其他为单张
# 更新:
4. 提取信息仔细阅读标签上的所有文字内容
# 更新:
# 更新:
版别格式要求
# 更新:
只需要年份 + 属相例如
# 更新:
- 2024
# 更新:
- 2025
# 更新:
- 2026
# 更新:
# 更新:
输出要求
# 更新:
严格按照以下格式输出每个字段必须填写具体值
# 更新:
1 发行机构中国人民银行
# 更新:
2 发行版别2024
# 更新:
3 面额贰拾圆
# 更新:
4 是否评级/
# 更新:
5 封装类型裸钞/单张/标十/标百
# 更新:
6 冠字序号J0xxxxxxxx
# 更新:
7 评级机构ACG/PCGS/PMG
# 更新:
8 评级分数67/68/69
# 更新:
9 是否三星/
# 更新:
10 特殊标识金山标/天马标/红绳版等
# 更新:
11 号码特征金山号 2 天马号 3 张等
# 更新:
# 更新:
现在请仔细分析提供的图片按上述格式输出结果"""
# 更新:
# 更新:
# 更新:
@router.post("/recognize")
# 更新:
async def recognize_image(
# 更新:
image: UploadFile = File(...),
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""OCR 图片识别 - 识别后自动保存图片到OSS临时目录"""
# 更新:
try:
# 更新:
# 读取图片数据
# 更新:
image_data = await image.read()
# 更新:
image_base64 = base64.b64encode(image_data).decode('utf-8')
# 更新:
# 更新:
# 生成OSS临时路径: temp/{year}/{month}/{day}/{uuid}.{ext}
# 更新:
oss_key, temp_id = get_oss_path("temp", filename=image.filename)
# 更新:
# 更新:
# 初始化temp_path为空
# 更新:
temp_path = None
# 更新:
# 更新:
# 上传到OSS
# 更新:
try:
# 更新:
image_url = upload_to_oss(image_data, oss_key)
# 更新:
except Exception as oss_err:
# 更新:
# OSS失败时保存到本地作为备选
# 更新:
temp_path = os.path.join(UPLOAD_DIR, oss_key.split('/')[-1])
# 更新:
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
# 更新:
with open(temp_path, 'wb') as f:
# 更新:
f.write(image_data)
# 更新:
image_url = f"/uploads/temp/{oss_key.split('/')[-1]}"
# 更新:
# 更新:
headers = {
# 更新:
"Authorization": f"Bearer {DASHSCOPE_API_KEY}",
# 更新:
"Content-Type": "application/json"
# 更新:
}
# 更新:
# 更新:
# 阿里云 DashScope API 格式 (qwen-vl-plus 视觉模型)
# 更新:
payload = {
# 更新:
"model": "qwen-vl-plus",
# 更新:
"input": {
# 更新:
"messages": [{
# 更新:
"role": "user",
# 更新:
"content": [
# 更新:
{
# 更新:
"image": f"data:{image.content_type};base64,{image_base64}"
# 更新:
},
# 更新:
{
# 更新:
"text": PROFESSIONAL_PROMPT
# 更新:
}
# 更新:
]
# 更新:
}]
# 更新:
},
# 更新:
"parameters": {
# 更新:
"max_tokens": 1000
# 更新:
}
# 更新:
}
# 更新:
# 更新:
async with httpx.AsyncClient(timeout=60.0) as client:
# 更新:
response = await client.post(
# 更新:
"https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
# 更新:
json=payload,
# 更新:
headers=headers
# 更新:
)
# 更新:
# 更新:
if response.status_code != 200:
# 更新:
# 识别失败,删除临时文件
# 更新:
if temp_path and os.path.exists(temp_path):
# 更新:
os.remove(temp_path)
# 更新:
raise HTTPException(status_code=500, detail=f"OCR API 调用失败:{response.text[:200]}")
# 更新:
# 更新:
ocr_result = response.json()
# 更新:
text_content = ""
# 更新:
# 新版API返回格式
# 更新:
if "output" in ocr_result and "choices" in ocr_result["output"]:
# 更新:
choices = ocr_result["output"]["choices"]
# 更新:
if choices and len(choices) > 0:
# 更新:
content = choices[0].get("message", {}).get("content", [])
# 更新:
if content and len(content) > 0:
# 更新:
text_content = content[0].get("text", "")
# 更新:
# 更新:
fields = extract_fields(text_content)
# 更新:
# 更新:
# 更新用户AI识别次数
# 更新:
current_user.f99_95_ai_count = (current_user.f99_95_ai_count or 0) + 1
# 更新:
db.commit()
# 更新:
# 更新:
# 返回识别结果和临时图片路径
# 更新:
return {
# 更新:
"success": True,
# 更新:
"text": text_content,
# 更新:
"fields": fields,
# 更新:
"aiCount": current_user.f99_95_ai_count,
# 更新:
"temp_image": {
# 更新:
"id": temp_id,
# 更新:
"filename": oss_key.split('/')[-1],
# 更新:
"path": image_url,
# 更新:
"original_name": image.filename,
# 更新:
"is_oss": image_url.startswith("https://")
# 更新:
}
# 更新:
}
# 更新:
# 更新:
except Exception as e:
# 更新:
import traceback
# 更新:
error_detail = f"识别失败:{str(e)}\n{traceback.format_exc()}"
# 更新:
raise HTTPException(status_code=500, detail=error_detail)
# 更新:
# 更新:
# 更新:
def extract_fields(text: str) -> dict:
# 更新:
"""从 OCR 文本中提取字段 - 直接返回 AI 识别结果"""
# 更新:
import re
# 更新:
fields = {}
# 更新:
# 更新:
# 解析结构化输出
# 更新:
patterns = {
# 更新:
'issuer': r'✅.*?1.*?发行机构.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'version': r'✅.*?2.*?发行版别.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'denomination': r'✅.*?3.*?面额.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'is_graded_text': r'✅.*?4.*?是否评级.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'packaging': r'✅.*?5.*?封装类型.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'prefix_serial': r'✅.*?6.*?冠字序号.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'grading_company': r'✅.*?7.*?评级机构.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'grading_score': r'✅.*?8.*?评级分数.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'three_star_text': r'✅.*?9.*?是否三星.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'special_mark': r'✅.*?10.*?特殊标识.*?[:]\s*(.+?)(?:\n|$)',
# 更新:
'serial_feature': r'✅.*?11.*?号码特征.*?[:]\s*(.+?)(?:\n|$)'
# 更新:
}
# 更新:
# 更新:
for field, pattern in patterns.items():
# 更新:
match = re.search(pattern, text, re.IGNORECASE | re.DOTALL)
# 更新:
if match:
# 更新:
value = match.group(1).strip()
# 更新:
# 保留所有值,包括"无"和"未识别",让前端处理
# 更新:
fields[field] = value
# 更新:
# 更新:
# 处理是否评级
# 更新:
if 'is_graded_text' in fields:
# 更新:
fields['is_graded'] = '' in fields.pop('is_graded_text')
# 更新:
# 更新:
# 处理是否三星
# 更新:
if 'three_star_text' in fields:
# 更新:
fields['three_star'] = '' in fields.pop('three_star_text')
# 更新:
# 更新:
# 简化版别字段2024 龙年贺岁纪念钞(标十) → 2024 龙)
# 更新:
if 'version' in fields:
# 更新:
version = fields['version']
# 更新:
# 提取年份和生肖
# 更新:
year_match = re.search(r'(20\d{2})', version)
# 更新:
animal = ''
# 更新:
if '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
elif '' in version:
# 更新:
animal = ''
# 更新:
# 更新:
if year_match and animal:
# 更新:
fields['version'] = f"{year_match.group(1)}{animal}"
# 更新:
# 更新:
return fields
# 更新:
# 更新:
# 更新:
@router.post("/claim-temp-image")
# 更新:
async def claim_temp_image(
# 更新:
temp_id: str,
# 更新:
collection_id: str,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""将临时图片移动到正式藏品目录 - 按用户/年/藏品ID分类"""
# 更新:
from app.models.models import Collection, CollectionImage
# 更新:
from datetime import datetime
# 更新:
# 更新:
# 验证藏品是否存在
# 更新:
collection = db.query(Collection).filter(
# 更新:
Collection.f99_90_id == collection_id,
# 更新:
Collection.f99_91_user_id == current_user.f99_90_id
# 更新:
).first()
# 更新:
# 更新:
if not collection:
# 更新:
raise HTTPException(status_code=404, detail="藏品不存在")
# 更新:
# 更新:
# 生成正式文件名和OSS路径: collections/{user_id}/{year}/{collection_id}/{filename}
# 更新:
code = collection.f01_02_code or "0000"
# 更新:
prefix = collection.f02_10_prefix_serial or ""
# 更新:
username = current_user.f01_01_name
# 更新:
import time
# 更新:
final_filename = f"{username}-{code}-{prefix}-{int(time.time())}.jpg"
# 更新:
oss_key = get_oss_path("collection", user_id=current_user.f99_90_id, collection_id=collection_id, filename=final_filename)
# 更新:
# 更新:
# 尝试从OSS获取临时图片 - 尝试多种扩展名和日期路径
# 更新:
temp_extensions = ['jpg', 'jpeg', 'png', 'gif', 'JPG', 'JPEG', 'PNG', 'GIF']
# 更新:
temp_content = None
# 更新:
found_key = None
# 更新:
# 更新:
# 尝试最近7天的路径
# 更新:
from datetime import timedelta
# 更新:
for i in range(7):
# 更新:
date = datetime.now() - timedelta(days=i)
# 更新:
year = date.strftime("%Y")
# 更新:
month = date.strftime("%m")
# 更新:
day = date.strftime("%d")
# 更新:
# 更新:
for ext in temp_extensions:
# 更新:
try:
# 更新:
temp_oss_key = f"temp/{year}/{month}/{day}/{temp_id}.{ext}"
# 更新:
import oss2
# 更新:
auth = oss2.Auth(OSS_CONFIG["access_key_id"], OSS_CONFIG["access_key_secret"])
# 更新:
bucket = oss2.Bucket(auth, OSS_CONFIG["endpoint"], OSS_CONFIG["bucket_name"])
# 更新:
temp_content = bucket.get_object(temp_oss_key).read()
# 更新:
found_key = temp_oss_key
# 更新:
break
# 更新:
except:
# 更新:
continue
# 更新:
if temp_content:
# 更新:
break
# 更新:
# 更新:
if temp_content:
# 更新:
# 上传到正式目录
# 更新:
bucket.put_object(oss_key, temp_content)
# 更新:
# 更新:
# 删除临时图片
# 更新:
try:
# 更新:
bucket.delete_object(found_key)
# 更新:
except:
# 更新:
pass
# 更新:
# 更新:
# OSS URL
# 更新:
image_path = f"{OSS_CONFIG['public_url']}/{oss_key}"
# 更新:
# 更新:
else:
# 更新:
# OSS失败使用本地文件
# 更新:
temp_path = None
# 更新:
for ext in temp_extensions:
# 更新:
temp_path = os.path.join(UPLOAD_DIR, f"{temp_id}.{ext}")
# 更新:
if os.path.exists(temp_path):
# 更新:
break
# 更新:
# 更新:
if not temp_path or not os.path.exists(temp_path):
# 更新:
raise HTTPException(status_code=404, detail="临时图片不存在或已过期")
# 更新:
# 更新:
# 保存到本地
# 更新:
collection_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "uploads", "collections")
# 更新:
os.makedirs(collection_dir, exist_ok=True)
# 更新:
# 更新:
new_path = os.path.join(collection_dir, final_filename)
# 更新:
import shutil
# 更新:
shutil.move(temp_path, new_path)
# 更新:
image_path = f"uploads/collections/{final_filename}"
# 更新:
# 更新:
# 创建图片记录
# 更新:
image_record = CollectionImage(
# 更新:
id=str(uuid.uuid4()),
# 更新:
collection_id=collection.f99_90_id,
# 更新:
filename=final_filename,
# 更新:
original_name=temp_id,
# 更新:
path=image_path
# 更新:
)
# 更新:
db.add(image_record)
# 更新:
db.commit()
# 更新:
# 更新:
return {
# 更新:
"success": True,
# 更新:
"image": {
# 更新:
"id": image_record.id,
# 更新:
"filename": image_record.filename,
# 更新:
"path": image_record.path
# 更新:
}
# 更新:
}
# 更新:

View File

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

384
backend/app/routers/seek.py Normal file
View File

@ -0,0 +1,384 @@
# seek - 寻号匹配路由
# Version: 1.2.70
# 更新:
from fastapi import APIRouter, Depends, Query, HTTPException
# 更新:
# Version: 1.2.x
# 更新:
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": "删除成功"}
# 更新:

View File

@ -1,4 +1,7 @@
# 用户管理路由
# users.py - 用户管理路由
# Version: 1.2.98 (2026-04-19)
# 更新:新增 dealCount 字段,从 Information 表统计用户发布的行情数量
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
from sqlalchemy.orm import Session
@ -114,6 +117,11 @@ def get_users(
for u in users:
# 统计每个用户的藏品数量
count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count()
# 统计每个用户发布的行情数量info_type='deal'
deal_count = db.query(Information).filter(
Information.user_id == u.f99_90_id,
Information.info_type == "deal"
).count()
user_list.append({
"id": u.f99_90_id,
"username": u.f01_01_name,
@ -123,6 +131,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,
"dealCount": deal_count,
"level": u.f99_94_level,
"aiCount": u.f99_95_ai_count,
"searchCount": u.f99_96_search_count,

View File

@ -1,377 +1,760 @@
# yichens - 一尘数据路由
# Version: 1.2.70
# 更新:
from fastapi import APIRouter, Depends, Query
# 更新:
# Version: 1.2.x
# 更新:
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
# 更新:
# 查询数据
# 更新:
# 查询数据 - 有post_time时按post_time排序没有时按crawled_at排序
# 更新:
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
# 更新:
ORDER BY COALESCE(post_time, crawled_at) 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}
# 更新:
}
# 更新:
# 更新:

35
config/CHANGELOG.md Normal file
View File

@ -0,0 +1,35 @@
# 版本更新记录
## v1.2.98 (2026-04-19)
### 前端更新
- **Admin.jsx**
- 功能:修复管理员用户列表行情数显示问题
- 改进:新增 dealCount 字段显示用户发布的成交行情数量
- 代码:将"配号"改为"📈 行情: X条"
### 后端更新
- **users.py**
- 功能修复管理员用户列表行情数API
- 改进:新增 dealCount 字段,从 Information 表统计用户发布的 deal 数量
- 代码:新增 Information 模型导入,统计查询
### Vite配置更新
- **vite.config.js**
- 功能:修复前端构建版本号不更新问题
- 改进:将版本更新改为 closeBundle hook 执行
- 代码:重构 updateHtmlTitle 函数
---
## v1.2.97 (2026-04-XX)
### 更新内容
- (待记录)
---
## v1.2.96 (2026-04-XX)
### 更新内容
- (待记录)

View File

@ -1 +1 @@
VERSION=1.2.81
1.2.98

48
config/VERSION.json Normal file
View File

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

8
env.conf Normal file
View File

@ -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

1
frontend/VERSION Normal file
View File

@ -0,0 +1 @@
VERSION=1.2.97

1
frontend/config/VERSION Normal file
View File

@ -0,0 +1 @@
1.2.98

View File

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

View File

@ -1,12 +1,12 @@
{
"name": "jiachenlong-frontend",
"version": "1.2.81",
"version": "1.2.82",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jiachenlong-frontend",
"version": "1.2.81",
"version": "1.2.82",
"dependencies": {
"axios": "^1.7.9",
"react": "^18.3.1",

View File

@ -1,4 +1,9 @@
// - AI /
/**
* Add - 添加藏品页面
* Version: 1.2.90x
* 更新
*/
import React, { useState, useRef } from 'react'
import { APP_VERSION } from '../config/version'
const API_BASE = localStorage.getItem('API_BASE') || ''
@ -66,7 +71,7 @@ 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: ''
})
@ -78,17 +83,17 @@ export default function Add() {
category: '',
packaging: '标十',
price: '',
platform: '淘宝',
platform: '抖音',
seller: '',
buyer: '',
date: new Date().toISOString().split('T')[0],
isGraded: false,
gradingCompany: '',
gradingScore: ''
gradingCompany: '爱藏',
gradingScore: '67+'
})
const [batchDefaultPackaging, setBatchDefaultPackaging] = useState('')
const [batchDefaultDate, setBatchDefaultDate] = useState('')
const [batchDefaultPlatform, setBatchDefaultPlatform] = useState('')
const [batchDefaultPlatform, setBatchDefaultPlatform] = useState('抖音')
const [dealMode, setDealMode] = useState('single') // single-, batch-
const [batchText, setBatchText] = useState('')
const [batchResult, setBatchResult] = useState([])
@ -824,34 +829,46 @@ export default function Add() {
setSavingDeal(true)
try {
const token = localStorage.getItem('token')
const digits = dealForm.serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
let tailNumber = '', sizeType = ''
if (dealForm.packaging === '标十' && digits.length >= 2) {
tailNumber = digits.slice(-2)
sizeType = ['01','11','21','31','41','51'].includes(tailNumber) ? '小号' : '大号'
} else if (dealForm.packaging === '标百' && digits.length >= 3) {
tailNumber = digits.slice(-3)
sizeType = ['101','201','301','401','501'].includes(tailNumber) ? '小号' : '大号'
//
// 7 J0 + 0 + ( J00123456)
// 8 J0 + ( J01234567)
// 9 J0 + 8 ( J02345678)
const rawSerial = dealForm.serial.replace('J', '').replace(/\D/g, '')
let normalizedSerial = ''
if (rawSerial.length === 7) {
normalizedSerial = 'J0' + '0' + rawSerial
} else if (rawSerial.length === 8) {
normalizedSerial = 'J0' + rawSerial
} else if (rawSerial.length >= 9) {
normalizedSerial = 'J0' + rawSerial.slice(1, 9) // 8
} else {
normalizedSerial = 'J0' + rawSerial
}
const response = await fetch(`${API_BASE}/api/information/`, {
const tailNumber = rawSerial.slice(-2)
const sizeType = (dealForm.packaging === '标十' && ['01','11','21','31','41','51','61','71','81','91'].includes(tailNumber)) ? '小号' :
(dealForm.packaging === '标百' && ['001','011','021','031','041','051','061','071','081','091'].includes(rawSerial.slice(-3))) ? '小号' : '大号'
const response = await fetch(`${API_BASE}/api/deal`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
info_type: 'deal',
title: `J0${dealForm.serial.replace('J', '').slice(0, 8)}${dealForm.price}`,
title: `${normalizedSerial}${dealForm.price}`,
content: `分类: ${dealForm.category || '未分类'}\n尾号: ${tailNumber || '-'}\n大小号: ${sizeType || '-'}\n包装: ${dealForm.packaging}\n平台: ${dealForm.platform}\n价格: ¥${dealForm.price}\n出售者: ${dealForm.seller || '-'}\n购买者: ${dealForm.buyer || '-'}\n日期: ${dealForm.date}\n评级: ${dealForm.gradingCompany ? dealForm.gradingCompany + ' ' + dealForm.gradingScore : '未评级'}`,
deal_price: parseFloat(dealForm.price),
deal_date: dealForm.date,
packaging: dealForm.packaging,
category: dealForm.category,
is_graded: !!dealForm.gradingCompany,
grading_company: dealForm.gradingCompany || '',
grading_score: dealForm.gradingScore || ''
tail_number: tailNumber,
size_type: sizeType,
platform: dealForm.platform,
seller: dealForm.seller || '',
buyer: dealForm.buyer || ''
})
})
if (response.ok) {
alert('行情录入成功!')
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '淘宝', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '抖音', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
} else {
const data = await response.json()
alert('录入失败: ' + (data.detail || '未知错误'))
@ -1034,21 +1051,20 @@ export default function Add() {
tail_number = item.packaging === '标十' ? digits.slice(-2) : digits.slice(-3)
size_type = (item.packaging === '标十' && ['01','11','21','31','41','51'].includes(tail_number)) || (item.packaging === '标百' && ['101','201','301','401','501'].includes(tail_number)) ? '小号' : '大号'
}
await fetch(`${API_BASE}/api/information/`, {
await fetch(`${API_BASE}/api/deal`, {
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
info_type: 'deal',
title: `${item.serial}${item.price}`,
content: `分类: ${item.category || '-'}\n尾号: ${tail_number || '-'}\n大小号: ${size_type || '-'}\n包装: ${item.packaging || '单张'}\n平台: ${item.platform || '-'}\n价格: ¥${item.price}\n出售者: ${item.seller || '-'}\n购买者: ${item.buyer || '-'}\n日期: ${item.deal_date || new Date().toISOString().split('T')[0]}`,
deal_price: parseFloat(item.price),
deal_date: item.deal_date || new Date().toISOString().split('T')[0],
packaging: item.packaging || '单张',
is_graded: item.is_graded || false,
grading_company: item.grading_company || '',
grading_score: item.grade || '',
category: item.category || '',
tail_number: tail_number,
size_type: size_type
size_type: size_type,
platform: item.platform || '-',
seller: item.seller || '',
buyer: item.buyer || ''
})
})
count++

View File

@ -1,3 +1,9 @@
/**
* Admin - 管理员用户管理页面
* Version: 1.2.98 (2026-04-19)
* 更新新增 dealCount 字段显示用户发布的行情数量
*/
import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'
@ -234,7 +240,7 @@ export default function Admin() {
🤖 AI识别: {user.aiCount || 0}
</div>
<div style={{ background: 'rgba(20, 184, 166, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#14b8a6' }}>
🎯 配号: {user.searchCount || 0}
📈 行情: {user.dealCount || 0}
</div>
<div style={{ background: 'rgba(245, 158, 11, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#f59e0b' }}>
🔍 寻号: {user.searchCount || 0}

View File

@ -1,3 +1,9 @@
/**
* Detail - 藏品详情页面
* Version: 1.2.90x
* 更新
*/
import React, { useState, useEffect } from 'react'
export default function Detail() {

View File

@ -1,3 +1,9 @@
/**
* Edit - 编辑藏品页面
* Version: 1.2.90x
* 更新
*/
import React, { useState, useRef, useEffect } from 'react'
// Input Add.jsx

View File

@ -1,3 +1,9 @@
/**
* Home - 首页
* Version: 1.2.95x
* 更新
*/
import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'
@ -174,32 +180,58 @@ export default function Home() {
<div onClick={() => window.location.hash = '#/add?mode=ocr'} style={{
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
borderRadius: '12px',
padding: '16px',
padding: '12px 16px',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '12px'
justifyContent: 'center',
textAlign: 'center'
}}>
<div style={{ fontSize: '24px' }}>📷</div>
<div>
<div style={{ color: '#fff', fontSize: '15px', fontWeight: '600' }}>AI识别</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '11px' }}>拍照识别藏品</div>
</div>
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>藏品录入</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>AI识别添加</div>
</div>
<div onClick={() => window.location.hash = '#/add?mode=deal'} style={{
background: 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)',
borderRadius: '12px',
padding: '12px 16px',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>行情录入</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>录入成交行情</div>
</div>
<div onClick={() => window.location.hash = '#/news?type=seek'} style={{
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
borderRadius: '12px',
padding: '12px 16px',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布寻号</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div>
</div>
<div onClick={() => window.location.hash = '#/add?mode=manual'} style={{
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
borderRadius: '12px',
padding: '16px',
padding: '12px 16px',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '12px'
justifyContent: 'center',
textAlign: 'center'
}}>
<div style={{ fontSize: '24px' }}></div>
<div>
<div style={{ color: '#fff', fontSize: '15px', fontWeight: '600' }}>手动录入</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '11px' }}>添加新藏品</div>
</div>
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布藏品</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>手动发布</div>
</div>
</div>
</div>

View File

@ -1,3 +1,9 @@
/**
* List - 藏品列表页面
* Version: 1.2.93x
* 更新
*/
import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'
const API_BASE = localStorage.getItem('API_BASE') || ''
@ -79,7 +85,7 @@ export default function List() {
}
try {
const user = JSON.parse(userStr)
const res = await fetch(`${API_BASE}/api/information/list?info_type=deal&user_id=${user.f99_90_id}&page_size=500`, {
const res = await fetch(`${API_BASE}/api/deal/list?user_only=true&page_size=500`, {
headers: { 'Authorization': 'Bearer ' + token }
})
const data = await res.json()
@ -758,7 +764,7 @@ function DealListItem({ deal, onRefresh }) {
const saveEdit = async () => {
const token = localStorage.getItem('token')
try {
await fetch(`${API_BASE}/api/information/${deal.id}`, {
await fetch(`${API_BASE}/api/deal/${deal.id}`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(editForm)
@ -772,7 +778,7 @@ function DealListItem({ deal, onRefresh }) {
if (!confirm('确定删除这条行情?')) return
const token = localStorage.getItem('token')
try {
await fetch(`${API_BASE}/api/information/${deal.id}`, {
await fetch(`${API_BASE}/api/deal/${deal.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})

View File

@ -1,3 +1,9 @@
/**
* Login - 登录页面
* Version: 1.2.85x
* 更新
*/
import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'
import { api } from '../utils/api'

View File

@ -1,3 +1,9 @@
/**
* News - 资讯列表页面
* Version: 1.2.80x
* 更新
*/
import React, { useState, useEffect } from 'react'
import YichensBoard from './YichensBoard'
@ -33,10 +39,7 @@ export default function News() {
const [matchedStatus, setMatchedStatus] = useState({}) //
const [showMatchList, setShowMatchList] = useState(false)
const [dealVersion, setDealVersion] = useState('龙钞')
const [dealDate, setDealDate] = useState(() => {
const today = new Date()
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
})
const [dealDate, setDealDate] = useState('')
const [dealDetailItems, setDealDetailItems] = useState(null) //
const [matchCollections, setMatchCollections] = useState([])
const [networkMatchCollections, setNetworkMatchCollections] = useState([])
@ -82,13 +85,21 @@ export default function News() {
const headers = token ? { Authorization: `Bearer ${token}` } : {}
// URL
let url = `${API_BASE}/api/information/list?info_type=${type}`
// 使APIseekdeal
let url = activeTab === 'yichen'
? `${API_BASE}/api/information/list?info_type=${activeTab}`
: activeTab === 'seek'
? `${API_BASE}/api/seek/list`
: `${API_BASE}/api/deal/list`
// tab
if (activeTab === 'deal' && dealDate) {
url += `&deal_date=${dealDate}&page_size=500`
} else {
url += `&page=${currentPage}&page_size=50`
// 500
if (activeTab === 'deal') {
url += (url.includes('?') ? '&' : '?') + 'page_size=500'
if (dealDate) {
url += '&deal_date=' + dealDate
}
} else if (activeTab === 'seek') {
url += (url.includes("?") ? "&" : "?") + "page=" + currentPage + "&page_size=500"
}
const res = await fetch(url, { headers })
@ -107,25 +118,17 @@ export default function News() {
//
let sortedData = data || []
if (activeTab === 'deal') {
const categoryOrder = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号']
// ->44->7
const normalizeCat = (c) => {
if (c === '通货') return '带4号'
if (c === '无4') return '带7号'
return c
}
if (activeTab === 'deal' || activeTab === 'seek') {
//
sortedData = [...(data || [])].sort((a, b) => {
const contentA = a.content || ''
const contentB = b.content || ''
const catA = normalizeCat(contentA.match(/分类:\s*(.+?)(?:\n|$)/)?.[1]?.trim() || a.category || '')
const catB = normalizeCat(contentB.match(/分类:\s*(.+?)(?:\n|$)/)?.[1]?.trim() || b.category || '')
const idxA = categoryOrder.indexOf(catA)
const idxB = categoryOrder.indexOf(catB)
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB)
const dateA = a.deal_date || a.created_at?.split('T')[0] || ""
const dateB = b.deal_date || b.created_at?.split('T')[0] || ""
if (dateA !== dateB) return dateB.localeCompare(dateA)
const noA = a.deal_no || ""
const noB = b.deal_no || ""
return noB.localeCompare(noA)
})
}
setInfoList(sortedData)
} catch (e) {
console.error(e)
@ -293,12 +296,12 @@ export default function News() {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/my/list`, {
const res = await fetch(`${API_BASE}/api/seek/list`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()
//
const seeks = Array.isArray(data) ? data.filter(item => item.info_type === 'seek' && item.user_id === currentUserId) : []
//
const seeks = Array.isArray(data) ? data.filter(item => item.user_id === currentUserId) : []
setMySeekList(seeks)
setInfoList(seeks) //
} catch (e) { console.error(e) }
@ -309,7 +312,7 @@ export default function News() {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/list?info_type=seek`, {
const res = await fetch(`${API_BASE}/api/seek/list`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()
@ -707,31 +710,26 @@ export default function News() {
else if (serial.startsWith('J3')) version = '蛇钞'
return (
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '12px', marginBottom: '10px' }}>
{/* 第一行:冠字号 | 版别 | 包装 | 分数 + 价格 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '600' }}>{serial}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#3b82f6', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{version}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#8b5cf6', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{packaging}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#06b6d4', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{grade}</span>
</div>
<div style={{ color: '#22c55e', fontSize: '16px', fontWeight: '700' }}>¥{item.deal_price?.toLocaleString()}</div>
<div key={item.id} style={{ borderBottom: '1px solid #334155', padding: '12px 0' }}>
{/* 标题行:冠字号 + 价格 */}
<div style={{ fontSize: '15px', fontWeight: '500', marginBottom: '6px' }}>
<span style={{ color: '#fbbf24' }}>{serial}</span>
<span style={{ color: '#22c55e', marginLeft: '12px' }}>¥{item.deal_price?.toLocaleString()}</span>
<span style={{ color: '#64748b', marginLeft: '12px', fontSize: '12px' }}>{item.deal_no}</span>
</div>
{/* 第二行:编码 | 分类 | 评级机构 | 平台 | 日期 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<span style={{ color: '#64748b', fontSize: '11px' }}>{item.deal_no}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#f97316', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{category}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
{gradingCompany && <span style={{ background: '#ec4899', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{gradingCompany}</span>}
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#10b981', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{platform}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#64748b', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{dealDate}</span>
{/* 信息行:版别 | 包装 | 分类 | 评级 | 分数 | 平台 */}
<div style={{ fontSize: '12px', color: '#94a3b8' }}>
<span style={{ color: '#fff' }}>{version}</span>
<span style={{ margin: '0 8px' }}>|</span>
<span style={{ color: '#3b82f6' }}>{packaging}</span>
<span style={{ margin: '0 8px' }}>|</span>
<span style={{ color: '#fbbf24' }}>{category}</span>
{gradingCompany && <><span style={{ margin: '0 8px' }}>|</span><span>{gradingCompany}</span></>}
{grade && <><span style={{ margin: '0 8px' }}>|</span><span>{grade}</span></>}
<span style={{ margin: '0 8px' }}>|</span>
<span>{platform}</span>
<span style={{ margin: '0 8px' }}>|</span>
<span>{dealDate}</span>
</div>
</div>
)
@ -1088,7 +1086,7 @@ export default function News() {
)}
{/* 分页组件 */}
{totalPages > 1 && (
{infoList.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px', padding: '16px', marginTop: '8px' }}>
<button
onClick={() => { if (currentPage > 1) { setCurrentPage(currentPage - 1); fetchInfoList() } }}

View File

@ -1,3 +1,9 @@
/**
* News_YichensBoard - 一尘帖子页面
* Version: 1.2.75x
* 更新
*/
import React, { useState, useEffect } from 'react'
export default function YichensBoard() {

View File

@ -1,3 +1,9 @@
/**
* Settings - 设置页面
* Version: 1.2.75x
* 更新
*/
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'

View File

@ -1,4 +1,9 @@
// -
/**
* Stats - 统计页面
* Version: 1.2.70x
* 更新
*/
import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'

View File

@ -1,3 +1,9 @@
/**
* YichensBoard - 一尘看板页面
* Version: 1.2.70x
* 更新
*/
import React, { useState, useEffect } from 'react'
export default function YichensBoard() {

128
frontend/src/pages/news.py Normal file
View File

@ -0,0 +1,128 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import Table, MetaData
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime, date
from app.core.database import get_db, engine
from app.models.models import User
from app.routers.auth import get_current_user
router = APIRouter(prefix="/api/news", tags=["资讯"])
metadata = MetaData()
# 分类表
categories_table = Table('news_categories', metadata, autoload_with=engine)
news_table = Table('news', metadata, autoload_with=engine)
user_posts_table = Table('user_posts', metadata, autoload_with=engine)
users_table = Table('users', metadata, autoload_with=engine)
deals_table = Table('deals', metadata, autoload_with=engine)
notifications_table = Table('notifications', metadata, autoload_with=engine)
# ============ 获取分类 ============
@router.get("/categories")
def get_categories(db: Session = Depends(get_db)):
results = db.query(categories_table).order_by(categories_table.c.sort_order).all()
return [dict(r._mapping) for r in results]
# ============ 获取资讯 ============
@router.get("")
def get_news(
category_id: Optional[int] = None,
page: int = 1,
limit: int = 20,
db: Session = Depends(get_db)
):
query = db.query(news_table)
if category_id:
query = query.filter(news_table.c.category_id == category_id)
offset = (page - 1) * limit
results = query.order_by(news_table.c.created_at.desc()).offset(offset).limit(limit).all()
return [dict(r._mapping) for r in results]
# ============ 获取用户发布 ============
@router.get("/posts")
def get_posts(
post_type: Optional[str] = None,
status: str = "active",
page: int = 1,
limit: int = 20,
db: Session = Depends(get_db)
):
query = db.query(user_posts_table).filter(user_posts_table.c.status == status)
if post_type:
query = query.filter(user_posts_table.c.post_type == post_type)
offset = (page - 1) * limit
results = query.order_by(user_posts_table.c.created_at.desc()).offset(offset).limit(limit).all()
return [dict(r._mapping) for r in results]
# ============ 创建发布 ============
class PostCreate(BaseModel):
post_type: str
title: str
content: Optional[str] = None
zodiac_type: Optional[str] = None
packaging: Optional[str] = None
@router.post("/posts")
def create_post(
post: PostCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
result = db.execute(user_posts_table.insert().values(
user_id=current_user.f99_90_id,
post_type=post.post_type,
title=post.title,
content=post.content,
zodiac_type=post.zodiac_type,
packaging=post.packaging,
status="pending"
))
db.commit()
return {"success": True, "id": result.inserted_primary_key[0]}
# ============ 成交数据 ============
@router.get("/deals")
def get_deals(
zodiac_type: Optional[str] = None,
limit: int = 20,
db: Session = Depends(get_db)
):
query = db.query(deals_table)
if zodiac_type:
query = query.filter(deals_table.c.zodiac_type == zodiac_type)
results = query.order_by(deals_table.c.deal_date.desc()).limit(limit).all()
return [dict(r._mapping) for r in results]
# ============ 通知 ============
@router.get("/notifications")
def get_notifications(limit: int = 10, db: Session = Depends(get_db)):
results = db.query(notifications_table).filter(
notifications_table.c.is_published == True
).order_by(notifications_table.c.created_at.desc()).limit(limit).all()
return [dict(r._mapping) for r in results]
# ============ 首页数据 ============
@router.get("/home")
def get_home(db: Session = Depends(get_db)):
# 推荐发布
posts = db.query(user_posts_table).filter(
user_posts_table.c.status == "active"
).order_by(user_posts_table.c.created_at.desc()).limit(10).all()
# 成交
deals = db.query(deals_table).order_by(
deals_table.c.deal_date.desc()
).limit(10).all()
# 通知
notices = db.query(notifications_table).filter(
notifications_table.c.is_published == True
).order_by(notifications_table.c.created_at.desc()).limit(5).all()
return {
"posts": [dict(p._mapping) for p in posts],
"deals": [dict(d._mapping) for d in deals],
"notices": [dict(n._mapping) for n in notices]
}

View File

@ -6,7 +6,7 @@ import { join } from 'path'
// 从 config/VERSION 文件读取版本号
function getVersion() {
try {
const versionFile = join(__dirname, '..', 'config', 'VERSION')
const versionFile = join(__dirname, 'config', 'VERSION')
const content = readFileSync(versionFile, 'utf-8').trim()
// 移除 VERSION= 前缀
if (content.startsWith('VERSION=')) {
@ -22,28 +22,30 @@ function getVersion() {
const APP_VERSION = getVersion()
console.log('📦 构建版本v' + APP_VERSION)
// 构建时自动更新 index.html 的 title
// 构建后执行 - 更新 dist/index.html 的 title
function updateHtmlTitle() {
try {
const htmlPath = join(__dirname, 'index.html')
let htmlContent = readFileSync(htmlPath, 'utf-8')
// 替换 <title>甲辰收藏 vXXX</title>
htmlContent = htmlContent.replace(
/<title>甲辰收藏 v[\d.]+<\/title>/,
'<title>甲辰收藏 v' + APP_VERSION + '</title>'
)
writeFileSync(htmlPath, htmlContent, 'utf-8')
console.log('✅ 已更新 index.html title: 甲辰收藏 v' + APP_VERSION)
} catch (e) {
console.error('更新 index.html 失败:', e.message)
return {
name: 'update-html-title',
closeBundle() {
try {
const htmlPath = join(__dirname, 'dist', 'index.html')
let htmlContent = readFileSync(htmlPath, 'utf-8')
// 替换 <title>甲辰收藏 vXXX</title>
htmlContent = htmlContent.replace(
/<title>甲辰收藏 v[\d.]+<\/title>/,
'<title>甲辰收藏 v' + APP_VERSION + '</title>'
)
writeFileSync(htmlPath, htmlContent, 'utf-8')
console.log('✅ 已更新 dist/index.html title: 甲辰收藏 v' + APP_VERSION)
} catch (e) {
console.error('更新 dist/index.html 失败:', e.message)
}
}
}
}
// 构建前执行
updateHtmlTitle()
export default defineConfig({
plugins: [react()],
plugins: [react(), updateHtmlTitle()],
define: {
'import.meta.env.APP_VERSION': JSON.stringify(APP_VERSION)
},

39
scripts/check_version.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
# 版本一致性检查脚本
# 用法: ./scripts/check_version.sh
echo "========================================="
echo " 版本一致性检查"
echo "========================================="
CONFIG_FILE="/root/.openclaw/workspace/jiachenlong/config/VERSION.json"
VERSION=$(grep -o '"version": "[^"]*"' $CONFIG_FILE | cut -d'"' -f4)
UPDATED=$(grep -o '"updated": "[^"]*"' $CONFIG_FILE | cut -d'"' -f4)
echo ""
echo "当前版本: $VERSION"
echo "更新时间: $UPDATED"
echo ""
# 检查前端页面版本
echo "--- 前端页面 (最后修改时间) ---"
for file in /root/.openclaw/workspace/jiachenlong/frontend/src/pages/*.jsx; do
name=$(basename $file .jsx)
mtime=$(date -r "$file" "+%Y-%m-%d %H:%M")
echo "$name: $mtime"
done
echo ""
echo "--- 后端路由 (最后修改时间) ---"
for file in /root/.openclaw/workspace/jiachenlong/backend/app/routers/*.py; do
name=$(basename $file .py)
if [ "$name" != "__init__" ]; then
mtime=$(date -r "$file" "+%Y-%m-%d %H:%M")
echo "$name: $mtime"
fi
done
echo ""
echo "========================================="
echo " 检查完成"
echo "========================================="

13
start.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
cd /root/jiachenlong/backend
export DATABASE_URL='postgresql://jiachenlong:Passwd1%403@pgm-bp1t5w248t7s1pvr.pg.rds.aliyuncs.com:5432/jiachenlong'
export SECRET_KEY=production-secret-key-b-env-20260401
export OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG
export OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1
export SMS_ACCESS_KEY_ID=LTAI5t86bc1nNKVNyYv4Af6x
export SMS_ACCESS_KEY_SECRET=92EVAIE3GECr214c9UaSF6TSYJvDLY
export SMS_SIGN_NAME=苏州双人旁
export SMS_TEMPLATE_CODE=SMS_505015231
export DASHSCOPE_API_KEY=sk-9389024a37da4f7bb455ac9a6b28776f
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 --workers 1 > /tmp/uvicorn.log 2>&1 &
echo 'B后端服务已启动'