Compare commits

..

No commits in common. "main" and "yonghu" have entirely different histories.
main ... yonghu

70 changed files with 2881 additions and 11700 deletions

34
.gitignore vendored
View File

@ -1,34 +0,0 @@
# 依赖
node_modules/
__pycache__/
*.pyc
*.pyo
.venv/
venv/
# 环境配置
.env
.env.local
.env.production
# 构建产物
dist/
build/
*.log
# 上传文件
backend/uploads/*
!backend/uploads/.gitkeep
# 静态资源(保留目录,忽略大文件)
static/images/*.jpg
static/images/*.png
!static/images/.gitkeep
# 系统文件
.DS_Store
Thumbs.db
.idea/
.vscode/
*.swp
*.swo

100
README.md
View File

@ -1,100 +0,0 @@
# 甲辰藏品管理系统
> 生肖纪念钞收藏管理系统
## 版本信息
| 项目 | 内容 |
|------|------|
| **版本** | v1.2.38 |
| **代号** | 最终优化版本 |
| **发布日期** | 2026-03-31 |
## 技术栈
- **后端**: FastAPI + PostgreSQL + 阿里云OSS + 阿里云短信
- **前端**: React + Vite + Tailwind CSS
- **部署**: Nginx
## 核心功能
- 用户管理(管理员/普通用户)
- 藏品管理CRUD、多条件筛选
- OCR识别阿里云视觉智能
- 统计分析
- 图片上传阿里云OSS
- 短信验证码
- 用户协议
- 密码强度验证
## 项目结构
```
jiachenlong/
├── backend/ # FastAPI后端
│ ├── app/
│ │ ├── routers/ # API路由
│ │ ├── services/ # 业务服务
│ │ └── modules/ # 业务模块
│ └── config/
├── frontend/ # React前端
│ ├── src/pages/ # 页面组件
│ └── dist/ # 构建产物
└── config/ # 版本配置
```
## 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| v1.2.38 | 2026-03-31 | 最终优化版本 - Admin页面显示28字段、登录AI配号统计、修复OCR/短信/注册提示 |
| v1.2.35 | 2026-03-31 | Admin页面显示28字段登录/AI/配号次数统计 |
| v1.2.30 | 2026-03-25 | 资讯功能页面 + 管理后台V2 |
| v1.2.2 | 2026-03-20 | 列表布局优化 |
| v1.2.1 | 2026-03-20 | 密码强度验证、用户协议 |
| v1.2.0 | 2026-03-20 | 管理员查看用户藏品 |
## 快速开始
```bash
# 后端
cd backend && pip install -r requirements.txt
python -m uvicorn app.main:app --reload
# 前端
cd frontend && npm install && npm run dev
```
## API接口
### 认证
- POST /api/auth/register - 注册
- POST /api/auth/login - 登录
- GET /api/auth/me - 当前用户
### 藏品
- GET /api/collections - 列表
- POST /api/collections - 创建
- PUT /api/collections/{id} - 更新
- DELETE /api/collections/{id} - 删除
## 开发团队
| 角色 | 姓名 | 职责 |
|------|------|------|
| 产品负责人 | 酷博特 | 项目总负责 |
| 项目经理 | 龙大 | 项目整体管理 |
| 开发 | 龙二 | 本地开发 |
| 测试 | 龙A | 生产A环境负责人 |
| 测试 | 龙B | 生产B环境负责人 |
| 运维 | 龙运 | 运维负责人 |
| 运维 | 龙镜 | 镜像版本管理负责人 |
## 文档
- [部署手册](https://qcn9r1i8r51d.feishu.cn/docx/ZvEadXZ7XoxUtYxGLoIcMCfgn7g)
- [Git仓库](http://47.253.189.47:3000/coolbot/jiachenlong)
---
© 2026 甲辰收藏

1
VERSION Normal file
View File

@ -0,0 +1 @@
1.2.101

View File

@ -1 +1 @@
1.2.98 1.2.100

View File

@ -0,0 +1,2 @@
from app.models.deal_info import DealInfo
from app.models.models import User, Collection

View File

@ -1,489 +1,254 @@
# auth - 认证路由 # 认证路由 - 使用字段编码
# Version: 1.2.90 from fastapi import APIRouter, Depends, HTTPException, status, Body, Response
# 更新:
from fastapi import APIRouter
# Version: 1.2.x
# 更新:
from fastapi import APIRouter, Depends, HTTPException, status, Body
# 更新:
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from app.core.database import get_db from app.core.database import get_db
# 更新:
from app.core.auth import verify_password, create_access_token, get_password_hash, get_current_user from app.core.auth import verify_password, create_access_token, get_password_hash, get_current_user
# 更新:
from app.models.models import User from app.models.models import User
# 更新:
from app.schemas.schemas import Token, UserCreate, UserResponse from app.schemas.schemas import Token, UserCreate, UserResponse
# 更新:
# 更新:
router = APIRouter(prefix="/api/auth", tags=["认证"]) router = APIRouter(prefix="/api/auth", tags=["认证"])
# 更新:
# 更新:
# 更新:
# 更新:
# 更新:
def generate_user_code(db): def generate_user_code(db):
# 更新:
"""生成用户编码从201开始按自然数顺序递增跳过已存在的""" """生成用户编码从201开始按自然数顺序递增跳过已存在的"""
# 更新:
# 查找最大的user_code # 查找最大的user_code
# 更新:
max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first() 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]: if max_code and max_code[0]:
# 更新:
try: try:
# 更新:
num = int(max_code[0]) + 1 num = int(max_code[0]) + 1
# 更新:
if num < 201: if num < 201:
# 更新:
num = 201 num = 201
# 更新:
# 检查是否已存在,如果存在则继续递增 # 检查是否已存在,如果存在则继续递增
# 更新:
while db.query(User).filter(User.user_code == str(num)).first(): while db.query(User).filter(User.user_code == str(num)).first():
# 更新:
num += 1 num += 1
# 更新:
return str(num) return str(num)
# 更新:
except: except:
# 更新:
pass pass
# 更新:
return "201" return "201"
# 更新:
# 更新:
@router.post("/register", response_model=UserResponse) @router.post("/register", response_model=UserResponse)
# 更新:
def register(user_data: UserCreate, db: Session = Depends(get_db)): 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() existing_user = db.query(User).filter(User.f01_01_name == user_data.f01_01_name).first()
# 更新:
if existing_user: if existing_user:
# 更新:
raise HTTPException( raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="f01_01_name: 用户名已存在" detail="f01_01_name: 用户名已存在"
# 更新:
) )
# 更新:
# 更新:
# 检查邮箱是否已存在 # 检查邮箱是否已存在
# 更新:
# 更新:
# 检查手机号是否已存在 # 检查手机号是否已存在
# 更新:
if user_data.phone: if user_data.phone:
# 更新:
existing_phone = db.query(User).filter(User.phone == user_data.phone).first() existing_phone = db.query(User).filter(User.phone == user_data.phone).first()
# 更新:
if existing_phone: if existing_phone:
# 更新:
raise HTTPException( raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="E00040:该手机号已被注册,请更换手机号" detail="E00040:该手机号已被注册,请更换手机号"
# 更新:
) )
# 更新:
if user_data.email: if user_data.email:
# 更新:
existing_email = db.query(User).filter(User.email == user_data.email).first() existing_email = db.query(User).filter(User.email == user_data.email).first()
# 更新:
if existing_email: if existing_email:
# 更新:
raise HTTPException( raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="E00041:该邮箱已被注册,请更换邮箱" detail="E00041:该邮箱已被注册,请更换邮箱"
# 更新:
) )
# 更新:
# 更新:
# 处理邀请码 # 处理邀请码
# 更新:
invited_by_user = None invited_by_user = None
# 更新:
if user_data.invite_code: if user_data.invite_code:
# 更新:
# 查找邀请人 # 查找邀请人
# 更新:
invited_by_user = db.query(User).filter(User.user_code == user_data.invite_code).first() invited_by_user = db.query(User).filter(User.user_code == user_data.invite_code).first()
# 更新:
if not invited_by_user: if not invited_by_user:
# 更新:
raise HTTPException( raise HTTPException(
# 更新:
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
# 更新:
detail="E00042:邀请码无效" detail="E00042:邀请码无效"
# 更新:
) )
# 更新:
# 更新:
# 创建用户 # 创建用户
# 更新:
import uuid import uuid
# 更新:
hashed_password = get_password_hash(user_data.password) hashed_password = get_password_hash(user_data.password)
# 更新:
generated_code = generate_user_code(db) generated_code = generate_user_code(db)
# 更新:
user = User( user = User(
# 更新:
f99_90_id=str(uuid.uuid4()), f99_90_id=str(uuid.uuid4()),
# 更新:
f99_91_user_id=str(uuid.uuid4()), f99_91_user_id=str(uuid.uuid4()),
# 更新:
user_code=generated_code, user_code=generated_code,
# 更新:
f01_01_name=user_data.f01_01_name, f01_01_name=user_data.f01_01_name,
# 更新:
email=user_data.email, email=user_data.email,
# 更新:
phone=user_data.phone, phone=user_data.phone,
# 更新:
avatar=user_data.avatar, avatar=user_data.avatar,
# 更新:
address=user_data.address, address=user_data.address,
# 更新:
bio=user_data.bio, bio=user_data.bio,
# 更新:
password=hashed_password, password=hashed_password,
# 更新:
role="user" role="user"
# 更新:
) )
# 更新:
# 更新:
db.add(user) db.add(user)
# 更新:
db.flush() # 确保获取user ID db.flush() # 确保获取user ID
# 更新:
# 更新:
# 更新邀请人、被邀请人的关联关系 # 更新邀请人、被邀请人的关联关系
# 更新:
if invited_by_user: if invited_by_user:
# 更新:
# 记录是被谁邀请的 # 记录是被谁邀请的
# 更新:
user.f01_13_invite_code = invited_by_user.user_code 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 invited_by_user.f99_101_invited_count = (invited_by_user.f99_101_invited_count or 0) + 1
# 更新:
# 更新:
# 生成自己的邀请码用自己的user_code # 生成自己的邀请码用自己的user_code
# 更新:
user.f01_13_invite_code = generated_code user.f01_13_invite_code = generated_code
# 更新:
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(user) db.refresh(user)
# 更新:
# 更新:
# 返回用户信息避免Pydantic序列化问题 # 返回用户信息避免Pydantic序列化问题
# 更新:
return { return {
# 更新:
"id": user.f99_90_id, "id": user.f99_90_id,
# 更新:
"username": user.f01_01_name, "username": user.f01_01_name,
# 更新:
"user_code": user.user_code, "user_code": user.user_code,
# 更新:
"email": user.email, "email": user.email,
# 更新:
"phone": user.phone, "phone": user.phone,
# 更新:
"avatar": user.avatar, "avatar": user.avatar,
# 更新:
"role": user.role, "role": user.role,
# 更新:
"level": user.f99_94_level, "level": user.f99_94_level,
# 更新:
"aiCount": user.f99_95_ai_count or 0, "aiCount": user.f99_95_ai_count or 0,
# 更新:
"searchCount": user.f99_96_search_count or 0, "searchCount": user.f99_96_search_count or 0,
# 更新:
"collectionCount": user.f99_97_collection_count or 0 "collectionCount": user.f99_97_collection_count or 0
# 更新:
} }
# 更新:
# 更新:
# 更新: @router.post("/login")
@router.post("/login", response_model=Token)
# 更新:
def login( def login(
# 更新:
form_data: OAuth2PasswordRequestForm = Depends(), form_data: OAuth2PasswordRequestForm = Depends(),
# 更新: db: Session = Depends(get_db),
db: Session = Depends(get_db) response: Response = None
# 更新:
): ):
# 更新: """用户登录 - 支持用户名或用户编码登录返回Token并设置Cookie"""
"""用户登录 - 支持用户名或用户编码登录"""
# 更新:
# 先尝试用户名登录 # 先尝试用户名登录
# 更新:
user = db.query(User).filter(User.f01_01_name == form_data.username).first() user = db.query(User).filter(User.f01_01_name == form_data.username).first()
# 更新:
# 如果用户名不存在,尝试用户编码登录 # 如果用户名不存在,尝试用户编码登录
# 更新:
if not user: if not user:
# 更新:
user = db.query(User).filter(User.user_code == form_data.username).first() user = db.query(User).filter(User.user_code == form_data.username).first()
# 更新:
if not user: if not user:
# 更新:
raise HTTPException( raise HTTPException(
# 更新:
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
# 更新:
detail="E00011: 用户名或密码错误", detail="E00011: 用户名或密码错误",
# 更新:
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
# 更新:
) )
# 更新:
# 更新:
# 验证密码 # 验证密码
# 更新:
if not verify_password(form_data.password, user.password): if not verify_password(form_data.password, user.password):
# 更新:
raise HTTPException( raise HTTPException(
# 更新:
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
# 更新:
detail="E00011: 用户名或密码错误", detail="E00011: 用户名或密码错误",
# 更新:
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
# 更新:
) )
# 更新:
# 更新:
# 更新登录次数和最后登录时间 # 更新登录次数和最后登录时间
# 更新:
from datetime import datetime from datetime import datetime
# 更新:
user.f99_98_login_count = (user.f99_98_login_count or 0) + 1 user.f99_98_login_count = (user.f99_98_login_count or 0) + 1
# 更新:
user.f99_99_last_login = datetime.now() user.f99_99_last_login = datetime.now()
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
# 生成 token # 生成 token
# 更新:
access_token = create_access_token(data={"sub": user.f99_90_id}) access_token = create_access_token(data={"sub": user.f99_90_id})
# 更新:
# 更新: # 设置Cookie有效期7天
return { if response:
# 更新: response.set_cookie(
"access_token": access_token, key="token",
# 更新: value=access_token,
"token_type": "bearer" httponly=False, # 允许JS读取小程序需要
# 更新: max_age=7 * 24 * 60 * 60, # 7天
} samesite="lax",
# 更新: path="/"
)
# 更新:
return {
# 更新: "access_token": access_token,
@router.get("/me", response_model=UserResponse) "token_type": "bearer"
# 更新: }
def get_current_user_info(
# 更新:
current_user: User = Depends(lambda: None) @router.get("/me", response_model=UserResponse)
# 更新: def get_current_user_info(
): current_user: User = Depends(lambda: None)
# 更新: ):
"""获取当前用户信息""" """获取当前用户信息"""
# 更新: raise HTTPException(
raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED,
# 更新: detail="请使用正确的依赖注入"
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)
# 更新: ):
@router.post("/change-password") """修改当前用户密码"""
# 更新: from app.core.auth import verify_password, get_password_hash
def change_password(
# 更新: # 在当前session中重新查询用户
old_password: str = Body(...), user = db.query(User).filter(User.f99_90_id == current_user.f99_90_id).first()
# 更新: if not user:
new_password: str = Body(...), raise HTTPException(status_code=404, detail="用户不存在")
# 更新:
current_user: User = Depends(get_current_user), # 验证旧密码
# 更新: if not verify_password(old_password, user.password):
db: Session = Depends(get_db) raise HTTPException(
# 更新: status_code=status.HTTP_400_BAD_REQUEST,
): detail="当前密码错误"
# 更新:
"""修改当前用户密码"""
# 更新:
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) user.password = get_password_hash(new_password)
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
return {"message": "密码修改成功"} return {"message": "密码修改成功"}
# 更新:
# 更新:
# 更新:
# ============ 短信验证码接口 ============ # ============ 短信验证码接口 ============
# 更新:
# 更新:
@router.post("/send-verification-code") @router.post("/send-verification-code")
# 更新:
def send_verification_code( def send_verification_code(
# 更新:
phone: str = Body(..., min_length=11, max_length=11), phone: str = Body(..., min_length=11, max_length=11),
# 更新:
purpose: str = Body("register") # register | login | reset_password purpose: str = Body("register") # register | login | reset_password
# 更新:
): ):
# 更新:
"""发送短信验证码""" """发送短信验证码"""
# 更新:
from app.services.sms import send_verification_code as send_sms from app.services.sms import send_verification_code as send_sms
# 更新:
# 更新:
# 验证手机号格式 # 验证手机号格式
# 更新:
if not phone.startswith("1") or len(phone) != 11: if not phone.startswith("1") or len(phone) != 11:
# 更新:
return {"success": False, "message": "手机号格式不正确"} return {"success": False, "message": "手机号格式不正确"}
# 更新:
# 更新:
result = send_sms(phone) result = send_sms(phone)
# 更新:
# 更新:
if result["success"]: if result["success"]:
# 更新:
return { return {
# 更新:
"success": True, "success": True,
# 更新:
"message": f"验证码已发送到 {phone[:3]}****{phone[7:]}", "message": f"验证码已发送到 {phone[:3]}****{phone[7:]}",
# 更新:
"expire": result.get("expire", 300) "expire": result.get("expire", 300)
# 更新:
} }
# 更新:
else: else:
# 更新:
return result return result
# 更新:
# 更新:
# 更新:
@router.post("/verify-code") @router.post("/verify-code")
# 更新:
def verify_code( def verify_code(
# 更新:
phone: str = Body(...), phone: str = Body(...),
# 更新:
code: str = Body(..., min_length=6, max_length=6) code: str = Body(..., min_length=6, max_length=6)
# 更新:
): ):
# 更新:
"""验证短信验证码(仅验证,不执行后续操作)""" """验证短信验证码(仅验证,不执行后续操作)"""
# 更新:
from app.services.sms import verify_code as check_code from app.services.sms import verify_code as check_code
# 更新:
# 更新:
is_valid = check_code(phone, code) is_valid = check_code(phone, code)
# 更新:
# 更新:
if is_valid: if is_valid:
# 更新:
return {"success": True, "message": "验证成功"} return {"success": True, "message": "验证成功"}
# 更新:
else: else:
# 更新:
return {"success": False, "message": "验证码错误或已过期"} return {"success": False, "message": "验证码错误或已过期"}
# 更新:

File diff suppressed because it is too large Load Diff

View File

@ -1,502 +1,309 @@
# deal - 成交行情路由
# Version: 1.2.85
# 更新:
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel from pydantic import BaseModel
# 更新:
from typing import Optional from typing import Optional
# 更新:
from datetime import datetime, date from datetime import datetime, date
# 更新:
from app.core.database import get_db from app.core.database import get_db
# 更新:
from app.core.auth import get_current_user from app.core.auth import get_current_user
# 更新:
from app.models.deal_info import DealInfo from app.models.deal_info import DealInfo
# 更新:
# 更新:
router = APIRouter(prefix="/api/deal", tags=["成交行情"]) router = APIRouter(prefix="/api/deal", tags=["成交行情"])
# 更新:
# 更新:
# ============ Schema ============ # ============ Schema ============
# 更新:
class DealInfoCreate(BaseModel): class DealInfoCreate(BaseModel):
# 更新:
title: str title: str
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
deal_price: Optional[float] = None deal_price: Optional[float] = None
# 更新:
deal_date: Optional[str] = None # YYYY-MM-DD deal_date: Optional[str] = None # YYYY-MM-DD
# 更新:
packaging: Optional[str] = None packaging: Optional[str] = None
# 更新:
category: Optional[str] = None category: Optional[str] = None
# 更新:
is_graded: Optional[bool] = False is_graded: Optional[bool] = False
# 更新:
grading_company: Optional[str] = None grading_company: Optional[str] = None
# 更新:
grading_score: Optional[str] = None grading_score: Optional[str] = None
# 更新:
tail_number: Optional[str] = None tail_number: Optional[str] = None
# 更新:
size_type: Optional[str] = None size_type: Optional[str] = None
# 更新:
version: Optional[str] = None version: Optional[str] = None
# 更新:
platform: Optional[str] = None platform: Optional[str] = None
# 更新:
seller: Optional[str] = None seller: Optional[str] = None
# 更新:
buyer: Optional[str] = None buyer: Optional[str] = None
# 更新:
# 更新:
class DealInfoUpdate(BaseModel): class DealInfoUpdate(BaseModel):
# 更新:
title: Optional[str] = None title: Optional[str] = None
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
deal_price: Optional[float] = None deal_price: Optional[float] = None
# 更新:
deal_date: Optional[str] = None deal_date: Optional[str] = None
# 更新:
packaging: Optional[str] = None packaging: Optional[str] = None
# 更新:
category: Optional[str] = None category: Optional[str] = None
# 更新:
is_graded: Optional[bool] = None is_graded: Optional[bool] = None
# 更新:
grading_company: Optional[str] = None grading_company: Optional[str] = None
# 更新:
grading_score: Optional[str] = None grading_score: Optional[str] = None
# 更新:
tail_number: Optional[str] = None tail_number: Optional[str] = None
# 更新:
size_type: Optional[str] = None size_type: Optional[str] = None
# 更新:
version: Optional[str] = None version: Optional[str] = None
# 更新:
platform: Optional[str] = None platform: Optional[str] = None
# 更新:
seller: Optional[str] = None seller: Optional[str] = None
# 更新:
buyer: Optional[str] = None buyer: Optional[str] = None
# 更新:
status: Optional[str] = None status: Optional[str] = None
# 更新:
# 更新:
class DealInfoResponse(BaseModel): class DealInfoResponse(BaseModel):
# 更新:
id: str id: str
# 更新:
user_id: Optional[str] user_id: Optional[str]
# 更新:
title: str title: str
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
deal_price: Optional[float] deal_price: Optional[float]
# 更新:
deal_date: Optional[date] deal_date: Optional[date]
# 更新:
deal_no: Optional[str] deal_no: Optional[str]
# 更新:
packaging: Optional[str] packaging: Optional[str]
# 更新:
category: Optional[str] category: Optional[str]
# 更新:
is_graded: Optional[bool] is_graded: Optional[bool]
# 更新:
grading_company: Optional[str] grading_company: Optional[str]
# 更新:
grading_score: Optional[str] grading_score: Optional[str]
# 更新:
tail_number: Optional[str] tail_number: Optional[str]
# 更新:
size_type: Optional[str] size_type: Optional[str]
# 更新:
version: Optional[str] version: Optional[str]
# 更新:
platform: Optional[str] platform: Optional[str]
# 更新:
seller: Optional[str] seller: Optional[str]
# 更新:
buyer: Optional[str] buyer: Optional[str]
# 更新:
status: str status: str
# 更新:
view_count: int view_count: int
# 更新:
contact_count: int contact_count: int
# 更新:
created_at: Optional[datetime] created_at: Optional[datetime]
# 更新:
updated_at: Optional[datetime] updated_at: Optional[datetime]
# 更新:
# 更新:
class Config: class Config:
# 更新:
from_attributes = True from_attributes = True
# 更新:
# 更新:
# 生成行情编号 # 生成行情编号
# 更新:
def generate_deal_no(db: Session): def generate_deal_no(db: Session):
# 更新:
"""生成行情编号从A000001开始递增""" """生成行情编号从A000001开始递增"""
# 更新:
last = db.query(DealInfo).order_by(DealInfo.deal_no.desc()).first() last = db.query(DealInfo).order_by(DealInfo.deal_no.desc()).first()
# 更新:
if last and last.deal_no: if last and last.deal_no:
# 更新:
# 例如 A000001 -> 2 -> A000002 # 例如 A000001 -> 2 -> A000002
# 更新:
num = int(last.deal_no[1:]) + 1 num = int(last.deal_no[1:]) + 1
# 更新:
return f"A{num:06d}" return f"A{num:06d}"
# 更新:
return "A000001" return "A000001"
# 更新:
# 更新:
# ============ API ============ # ============ API ============
# 更新:
@router.get("/list", response_model=list[DealInfoResponse]) @router.get("/list", response_model=list[DealInfoResponse])
# 更新:
def get_deal_list( def get_deal_list(
# 更新:
status: str = Query("active"), status: str = Query("active"),
# 更新:
deal_date: Optional[str] = Query(None), deal_date: Optional[str] = Query(None),
# 更新:
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
# 更新:
page_size: int = Query(20, ge=1, le=1000), page_size: int = Query(20, ge=1, le=1000),
# 更新:
user_only: bool = Query(False), # 是否只查看自己的 user_only: bool = Query(False), # 是否只查看自己的
# 更新:
current_user: Optional = Depends(get_current_user), current_user: Optional = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取成交行情列表""" """获取成交行情列表"""
# 更新:
query = db.query(DealInfo).filter(DealInfo.status == status) query = db.query(DealInfo).filter(DealInfo.status == status)
# 更新:
# 更新:
# 我的行情:只查看自己的(管理员也只看自己的) # 我的行情:只查看自己的(管理员也只看自己的)
# 更新:
if user_only and current_user: if user_only and current_user:
# 更新:
query = query.filter(DealInfo.user_id == current_user.f99_90_id) query = query.filter(DealInfo.user_id == current_user.f99_90_id)
# 更新:
# 更新:
# 成交日期过滤 # 成交日期过滤
# 更新:
if deal_date: if deal_date:
# 更新:
query = query.filter(DealInfo.deal_date == deal_date) query = query.filter(DealInfo.deal_date == deal_date)
# 更新:
# 更新:
# 排序:优先成交日期倒序,同日按编号倒序 # 排序:优先成交日期倒序,同日按编号倒序
# 更新:
query = query.order_by(DealInfo.deal_date.desc().nullslast(), DealInfo.deal_no.desc().nullslast()) query = query.order_by(DealInfo.deal_date.desc().nullslast(), DealInfo.deal_no.desc().nullslast())
# 更新:
# 更新:
# 分页 # 分页
# 更新:
offset = (page - 1) * page_size offset = (page - 1) * page_size
# 更新: total_count = query.count()
total_pages = (total_count + page_size - 1) // page_size
items = query.offset(offset).limit(page_size).all() items = query.offset(offset).limit(page_size).all()
# 更新:
# 更新: # 返回Response对象以添加自定义头
return items from fastapi.responses import JSONResponse
# 更新: return JSONResponse(
content=[DealInfoResponse.model_validate(item).model_dump(mode='json') for item in items],
# 更新: headers={"X-Total-Pages": str(total_pages), "X-Total-Count": str(total_count)}
@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("/stats")
@router.get("/{deal_id}", response_model=DealInfoResponse) def get_deal_stats(
# 更新:
def get_deal(
# 更新:
deal_id: str,
# 更新:
current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新: """获取成交行情统计"""
"""获取成交行情详情""" total = db.query(DealInfo).filter(DealInfo.status == "active").count()
# 更新:
deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first()
# 更新:
if not deal:
# 更新:
raise HTTPException(status_code=404, detail="成交行情不存在")
# 更新:
# 更新: # 按日期统计
# 增加浏览数 from sqlalchemy import func
# 更新: date_stats = db.query(
deal.view_count += 1 DealInfo.deal_date,
# 更新: func.count(DealInfo.id).label('count')
db.commit() ).filter(
# 更新: DealInfo.status == "active",
DealInfo.deal_date.isnot(None)
).group_by(DealInfo.deal_date).order_by(DealInfo.deal_date.desc()).limit(10).all()
# 更新: return {
return deal "total": total,
# 更新: "by_date": [{"date": str(d.deal_date), "count": d.count} for d in date_stats]
}
# 更新: @router.post("", response_model=DealInfoResponse)
@router.put("/{deal_id}", response_model=DealInfoResponse) def create_deal(
# 更新: data: DealInfoCreate,
def update_deal(
# 更新:
deal_id: str,
# 更新:
data: DealInfoUpdate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新: """创建成交行情"""
"""更新成交行情"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新: # 生成行情编号
deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first() deal_no = generate_deal_no(db)
# 更新:
if not deal:
# 更新:
raise HTTPException(status_code=404, detail="成交行情不存在")
# 更新:
# 更新: # 解析日期
# 处理日期 deal_date = None
# 更新:
if data.deal_date: if data.deal_date:
# 更新:
try: try:
# 更新: deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date()
data.deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date()
# 更新:
except: except:
# 更新: pass
data.deal_date = None
# 更新:
# 更新: deal = DealInfo(
for key, value in data.model_dump(exclude_unset=True).items(): user_id=current_user.f99_90_id if current_user else None,
# 更新: title=data.title,
setattr(deal, key, value) 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.commit()
# 更新:
db.refresh(deal) db.refresh(deal)
# 更新:
return deal return deal
# 更新:
# 更新: @router.get("/category-stats")
@router.delete("/{deal_id}") def get_deal_category_stats(
# 更新: version: str = Query("龙钞", description="版本筛选:龙钞、马钞、蛇钞、其他"),
def delete_deal(
# 更新:
deal_id: str,
# 更新:
current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新: """获取成交行情分类汇总统计数据 - 后端计算优化版"""
"""删除成交行情""" from collections import defaultdict
# 更新:
if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新: # 定义版本前缀映射
version_prefix_map = {"龙钞": "J0", "马钞": "J1", "蛇钞": "J3"}
packagings = ["标百", "标十", "单张"]
category_map = {"通货": "带4号", "无4": "带7号", "永恒": "永恒号", "钻石": "钻石号"}
# 构建查询
query = db.query(DealInfo).filter(
DealInfo.status == "active", DealInfo.deal_price.isnot(None), DealInfo.deal_price > 0
)
if version != "其他" and version in version_prefix_map:
query = query.filter(DealInfo.title.startswith(version_prefix_map[version]))
deals = query.all()
stats = defaultdict(lambda: defaultdict(lambda: {"count": 0, "total": 0}))
for deal in deals:
content = deal.content or ""
packaging = deal.packaging
if not packaging and "包装:" in content:
packaging = content.split("包装:")[1].split("\n")[0].strip()
category = deal.category
if not category and "分类:" in content:
category = content.split("分类:")[1].split("\n")[0].strip()
if category in category_map:
category = category_map[category]
packaging = packaging or "单张"
category = category or "带4号"
stats[packaging][category]["count"] += 1
stats[packaging][category]["total"] += deal.deal_price
result = []
category_order = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号']
for cat in category_order:
row = {"category": cat}
has_data = False
for pkg in packagings:
data = stats[pkg][cat]
if data["count"] > 0:
row[pkg] = {"avg": round(data["total"] / data["count"]), "count": data["count"]}
has_data = True
else:
row[pkg] = None
if has_data:
result.append(row)
return {"version": version, "data": result}
@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() deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first()
# 更新:
if not deal: if not deal:
# 更新:
raise HTTPException(status_code=404, detail="成交行情不存在") raise HTTPException(status_code=404, detail="成交行情不存在")
# 更新:
# 更新: # 增加浏览数
deal.status = "deleted" deal.view_count += 1
# 更新:
db.commit() db.commit()
# 更新:
# 更新: return deal
return {"message": "删除成功"}
# 更新: @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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,384 +1,189 @@
# seek - 寻号匹配路由
# Version: 1.2.70
# 更新:
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel from pydantic import BaseModel
# 更新:
from typing import Optional from typing import Optional
# 更新:
from datetime import datetime from datetime import datetime
# 更新:
from app.core.database import get_db from app.core.database import get_db
# 更新:
from app.core.auth import get_current_user from app.core.auth import get_current_user
# 更新:
from app.models.seek_info import SeekInfo from app.models.seek_info import SeekInfo
# 更新:
# 更新:
router = APIRouter(prefix="/api/seek", tags=["寻配号"]) router = APIRouter(prefix="/api/seek", tags=["寻配号"])
# 更新:
# 更新:
# ============ Schema ============ # ============ Schema ============
# 更新:
class SeekInfoCreate(BaseModel): class SeekInfoCreate(BaseModel):
# 更新:
title: str title: str
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 更新:
# 更新:
class SeekInfoUpdate(BaseModel): class SeekInfoUpdate(BaseModel):
# 更新:
title: Optional[str] = None title: Optional[str] = None
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 更新:
status: Optional[str] = None status: Optional[str] = None
# 更新:
# 更新:
class SeekInfoResponse(BaseModel): class SeekInfoResponse(BaseModel):
# 更新:
id: str id: str
# 更新:
user_id: str user_id: str
# 更新:
title: str title: str
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
expect_category: Optional[str] expect_category: Optional[str]
# 更新:
expect_version: Optional[str] expect_version: Optional[str]
# 更新:
expect_packaging: Optional[str] expect_packaging: Optional[str]
# 更新:
expect_number: Optional[str] expect_number: Optional[str]
# 更新:
expect_price_min: Optional[float] expect_price_min: Optional[float]
# 更新:
expect_price_max: Optional[float] expect_price_max: Optional[float]
# 更新:
status: str status: str
# 更新:
is_matched: Optional[str] is_matched: Optional[str]
# 更新:
matched_user_id: Optional[str] matched_user_id: Optional[str]
# 更新:
matched_contact: Optional[str] matched_contact: Optional[str]
# 更新:
view_count: int view_count: int
# 更新:
contact_count: int contact_count: int
# 更新:
created_at: Optional[datetime] created_at: Optional[datetime]
# 更新:
updated_at: Optional[datetime] updated_at: Optional[datetime]
# 更新:
# 更新:
class Config: class Config:
# 更新:
from_attributes = True from_attributes = True
# 更新:
# 更新:
# ============ API ============ # ============ API ============
# 更新:
@router.get("/list", response_model=list[SeekInfoResponse]) @router.get("/list", response_model=list[SeekInfoResponse])
# 更新:
def get_seek_list( def get_seek_list(
# 更新:
status: str = Query("active"), status: str = Query("active"),
# 更新:
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
# 更新:
page_size: int = Query(20, ge=1, le=1000), page_size: int = Query(20, ge=1, le=1000),
# 更新:
user_only: bool = Query(False), user_only: bool = Query(False),
# 更新:
current_user: Optional = Depends(get_current_user), current_user: Optional = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号列表""" """获取寻配号列表"""
# 更新:
query = db.query(SeekInfo).filter(SeekInfo.status == status) query = db.query(SeekInfo).filter(SeekInfo.status == status)
# 更新:
# 更新:
# 我的寻配号:只查看自己的 # 我的寻配号:只查看自己的
# 更新:
if user_only and current_user: if user_only and current_user:
# 更新:
query = query.filter(SeekInfo.user_id == current_user.f99_90_id) query = query.filter(SeekInfo.user_id == current_user.f99_90_id)
# 更新:
# 更新:
# 排序 # 排序
# 更新:
query = query.order_by(SeekInfo.created_at.desc()) query = query.order_by(SeekInfo.created_at.desc())
# 更新:
# 更新:
# 分页 # 分页
# 更新:
offset = (page - 1) * page_size offset = (page - 1) * page_size
# 更新:
items = query.offset(offset).limit(page_size).all() items = query.offset(offset).limit(page_size).all()
# 更新:
# 更新:
return items return items
# 更新:
# 更新:
@router.get("/stats") @router.get("/stats")
# 更新:
def get_seek_stats( def get_seek_stats(
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号统计""" """获取寻配号统计"""
# 更新:
total = db.query(SeekInfo).filter(SeekInfo.status == "active").count() total = db.query(SeekInfo).filter(SeekInfo.status == "active").count()
# 更新:
matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count() matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count()
# 更新:
# 更新:
return { return {
# 更新:
"total": total, "total": total,
# 更新:
"matched": matched, "matched": matched,
# 更新:
"unmatched": total - matched "unmatched": total - matched
# 更新:
} }
# 更新:
# 更新:
@router.post("", response_model=SeekInfoResponse) @router.post("", response_model=SeekInfoResponse)
# 更新:
def create_seek( def create_seek(
# 更新:
data: SeekInfoCreate, data: SeekInfoCreate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""创建寻配号""" """创建寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = SeekInfo( seek = SeekInfo(
# 更新:
user_id=current_user.f99_90_id, user_id=current_user.f99_90_id,
# 更新:
title=data.title, title=data.title,
# 更新:
content=data.content, content=data.content,
# 更新:
expect_category=data.expect_category, expect_category=data.expect_category,
# 更新:
expect_version=data.expect_version, expect_version=data.expect_version,
# 更新:
expect_packaging=data.expect_packaging, expect_packaging=data.expect_packaging,
# 更新:
expect_number=data.expect_number, expect_number=data.expect_number,
# 更新:
expect_price_min=data.expect_price_min, expect_price_min=data.expect_price_min,
# 更新:
expect_price_max=data.expect_price_max, expect_price_max=data.expect_price_max,
# 更新:
status="active" status="active"
# 更新:
) )
# 更新:
db.add(seek) db.add(seek)
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# 更新:
@router.get("/{seek_id}", response_model=SeekInfoResponse) @router.get("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def get_seek( def get_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号详情""" """获取寻配号详情"""
# 更新:
seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first()
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
# 增加浏览数 # 增加浏览数
# 更新:
seek.view_count += 1 seek.view_count += 1
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
return seek return seek
# 更新:
# 更新:
@router.put("/{seek_id}", response_model=SeekInfoResponse) @router.put("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def update_seek( def update_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
data: SeekInfoUpdate, data: SeekInfoUpdate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""更新寻配号""" """更新寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = db.query(SeekInfo).filter( seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id, SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first() ).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
for key, value in data.model_dump(exclude_unset=True).items(): for key, value in data.model_dump(exclude_unset=True).items():
# 更新:
setattr(seek, key, value) setattr(seek, key, value)
# 更新:
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# 更新:
@router.delete("/{seek_id}") @router.delete("/{seek_id}")
# 更新:
def delete_seek( def delete_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""删除寻配号""" """删除寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = db.query(SeekInfo).filter( seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id, SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first() ).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
seek.status = "deleted" seek.status = "deleted"
# 更新:
db.commit() db.commit()
# 更新:
# 更新: return {"message": "删除成功"}
return {"message": "删除成功"}
# 更新:

View File

@ -1,13 +1,11 @@
# users.py - 用户管理路由 # 用户管理路由
# Version: 1.2.98 (2026-04-19)
# 更新:新增 dealCount 字段,从 Information 表统计用户发布的行情数量
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
from app.core.auth import get_current_user from app.core.auth import get_current_user
from app.models.models import User, Collection from app.models.models import User, Collection
from app.models.deal_info import DealInfo
from app.schemas.schemas import UserResponse, UserUpdate from app.schemas.schemas import UserResponse, UserUpdate
router = APIRouter(prefix="/api", tags=["用户"]) router = APIRouter(prefix="/api", tags=["用户"])
@ -100,7 +98,7 @@ def get_users(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取用户列表(仅管理员)""" """获取用户列表(仅管理员)"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问") raise HTTPException(status_code=403, detail="无权访问")
total = db.query(User).count() total = db.query(User).count()
@ -117,11 +115,8 @@ def get_users(
for u in users: for u in users:
# 统计每个用户的藏品数量 # 统计每个用户的藏品数量
count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count() count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count()
# 统计每个用户发布的行情数量info_type='deal' # 统计每个用户的行情数量
deal_count = db.query(Information).filter( deal_count = db.query(DealInfo).filter(DealInfo.user_id == u.f99_90_id).count()
Information.user_id == u.f99_90_id,
Information.info_type == "deal"
).count()
user_list.append({ user_list.append({
"id": u.f99_90_id, "id": u.f99_90_id,
"username": u.f01_01_name, "username": u.f01_01_name,
@ -152,7 +147,7 @@ def get_user(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取单个用户信息""" """获取单个用户信息"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问") raise HTTPException(status_code=403, detail="无权访问")
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(User.id == user_id).first()
@ -177,7 +172,7 @@ def get_user_collections(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取指定用户的藏品列表""" """获取指定用户的藏品列表"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问") raise HTTPException(status_code=403, detail="无权访问")
collections = db.query(Collection).filter( collections = db.query(Collection).filter(
@ -194,7 +189,7 @@ def get_user_collection_count(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取指定用户的藏品数量""" """获取指定用户的藏品数量"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问") raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
count = db.query(Collection).filter(Collection.user_id == user_id).count() count = db.query(Collection).filter(Collection.user_id == user_id).count()
@ -219,7 +214,7 @@ def update_user(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""更新用户信息(仅管理员)""" """更新用户信息(仅管理员)"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问") raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
user = db.query(User).filter(User.f99_90_id == user_id).first() user = db.query(User).filter(User.f99_90_id == user_id).first()
@ -297,7 +292,7 @@ def delete_user(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""删除用户(仅管理员)""" """删除用户(仅管理员)"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问") raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
# 不能删除自己 # 不能删除自己

View File

@ -1,760 +1,377 @@
# yichens - 一尘数据路由
# Version: 1.2.70
# 更新:
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy import func, text from sqlalchemy import func, text
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel from pydantic import BaseModel
# 更新:
from typing import Optional, List from typing import Optional, List
# 更新:
from datetime import datetime, date from datetime import datetime, date
# 更新:
from app.core.coolbot_db import get_coolbot_db from app.core.coolbot_db import get_coolbot_db
# 更新:
# 更新:
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"]) router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
# 更新:
# 更新:
# ============ 数据模型 ============ # ============ 数据模型 ============
# 更新:
class YichensPostStats(BaseModel): class YichensPostStats(BaseModel):
# 更新:
total_posts: int total_posts: int
# 更新:
total_deals: int # 出售 total_deals: int # 出售
# 更新:
total_wants: int # 求购 total_wants: int # 求购
# 更新:
total_replies: int total_replies: int
# 更新:
total_views: int total_views: int
# 更新:
avg_price: Optional[float] avg_price: Optional[float]
# 更新:
# 更新:
class CategoryStat(BaseModel): class CategoryStat(BaseModel):
# 更新:
category: str category: str
# 更新:
count: int count: int
# 更新:
# 更新:
class PostItem(BaseModel): class PostItem(BaseModel):
# 更新:
post_id: str post_id: str
# 更新:
title: str title: str
# 更新:
category: Optional[str] category: Optional[str]
# 更新:
post_type: str post_type: str
# 更新:
price: Optional[float] price: Optional[float]
# 更新:
author_username: str author_username: str
# 更新:
post_time: str post_time: str
# 更新:
reply_count: int reply_count: int
# 更新:
view_count: int view_count: int
# 更新:
url: Optional[str] url: Optional[str]
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
# 更新:
class UserStat(BaseModel): class UserStat(BaseModel):
# 更新:
total_users: int total_users: int
# 更新:
new_users_today: int new_users_today: int
# 更新:
sellers: int sellers: int
# 更新:
# 更新:
class UserItem(BaseModel): class UserItem(BaseModel):
# 更新:
user_id: str user_id: str
# 更新:
username: str username: str
# 更新:
avatar_url: Optional[str] avatar_url: Optional[str]
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
credit_level: Optional[str] credit_level: Optional[str]
# 更新:
credit_score: Optional[int] credit_score: Optional[int]
# 更新:
post_count: int post_count: int
# 更新:
is_seller: bool is_seller: bool
# 更新:
registration_date: Optional[str] registration_date: Optional[str]
# 更新:
# 更新:
# ============ 统计接口 ============ # ============ 统计接口 ============
# 更新:
@router.get("/stats/posts", response_model=YichensPostStats) @router.get("/stats/posts", response_model=YichensPostStats)
# 更新:
def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)): def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取帖子统计""" """获取帖子统计"""
# 更新:
result = db.execute(text(""" result = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total_posts, COUNT(*) as total_posts,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals, COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants, COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
# 更新:
COALESCE(SUM(reply_count), 0) as total_replies, COALESCE(SUM(reply_count), 0) as total_replies,
# 更新:
COALESCE(SUM(view_count), 0) as total_views, COALESCE(SUM(view_count), 0) as total_views,
# 更新:
AVG(price) as avg_price AVG(price) as avg_price
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
"""), {"days": days}).fetchone() """), {"days": days}).fetchone()
# 更新:
# 更新:
return YichensPostStats( return YichensPostStats(
# 更新:
total_posts=result[0] or 0, total_posts=result[0] or 0,
# 更新:
total_deals=result[1] or 0, total_deals=result[1] or 0,
# 更新:
total_wants=result[2] or 0, total_wants=result[2] or 0,
# 更新:
total_replies=result[3] or 0, total_replies=result[3] or 0,
# 更新:
total_views=result[4] or 0, total_views=result[4] or 0,
# 更新:
avg_price=float(result[5]) if result[5] else None avg_price=float(result[5]) if result[5] else None
# 更新:
) )
# 更新:
# 更新:
@router.get("/stats/categories", response_model=List[CategoryStat]) @router.get("/stats/categories", response_model=List[CategoryStat])
# 更新:
def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)): def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""按分类统计帖子数量""" """按分类统计帖子数量"""
# 更新:
results = db.execute(text(""" results = db.execute(text("""
# 更新:
SELECT category, COUNT(*) as count SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
GROUP BY category GROUP BY category
# 更新:
ORDER BY count DESC ORDER BY count DESC
# 更新:
"""), {"days": days}).fetchall() """), {"days": days}).fetchall()
# 更新:
# 更新:
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results] return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
# 更新:
# 更新:
@router.get("/stats/users", response_model=UserStat) @router.get("/stats/users", response_model=UserStat)
# 更新:
def get_user_stats(db: Session = Depends(get_coolbot_db)): def get_user_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取用户统计""" """获取用户统计"""
# 更新:
result = db.execute(text(""" result = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total_users, COUNT(*) as total_users,
# 更新:
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today, COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
# 更新:
COUNT(*) FILTER (WHERE is_seller = true) as sellers COUNT(*) FILTER (WHERE is_seller = true) as sellers
# 更新:
FROM yichens_users FROM yichens_users
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
return UserStat( return UserStat(
# 更新:
total_users=result[0] or 0, total_users=result[0] or 0,
# 更新:
new_users_today=result[1] or 0, new_users_today=result[1] or 0,
# 更新:
sellers=result[2] or 0 sellers=result[2] or 0
# 更新:
) )
# 更新:
# 更新:
@router.get("/posts") @router.get("/posts")
# 更新:
def get_posts( def get_posts(
# 更新:
limit: int = Query(20, ge=1, le=500), limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
# 更新:
category: Optional[str] = None, category: Optional[str] = None,
# 更新:
post_type: Optional[str] = None, post_type: Optional[str] = None,
# 更新:
keyword: Optional[str] = None, keyword: Optional[str] = None,
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取帖子列表 - 支持全局搜索,返回总数和分页信息""" """获取帖子列表 - 支持全局搜索,返回总数和分页信息"""
# 更新:
# 构建WHERE条件 # 构建WHERE条件
# 更新:
where_clauses = ["1=1"] where_clauses = ["1=1"]
# 更新:
params = {"limit": limit, "offset": offset} params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if category: if category:
# 更新:
where_clauses.append("category = :category") where_clauses.append("category = :category")
# 更新:
params["category"] = category params["category"] = category
# 更新:
# 更新:
if post_type: if post_type:
# 更新:
where_clauses.append("post_type = :post_type") where_clauses.append("post_type = :post_type")
# 更新:
params["post_type"] = post_type params["post_type"] = post_type
# 更新:
# 更新:
# 全局搜索 # 全局搜索
# 更新:
if keyword: if keyword:
# 更新:
where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)") where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)")
# 更新:
params["keyword"] = f"%{keyword}%" params["keyword"] = f"%{keyword}%"
# 更新:
# 更新:
where_sql = " AND ".join(where_clauses) where_sql = " AND ".join(where_clauses)
# 更新:
# 更新:
# 查询总数 # 查询总数
# 更新:
count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}" count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}"
# 更新:
total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0 total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0
# 更新:
# 更新: # 查询数据
# 查询数据 - 有post_time时按post_time排序没有时按crawled_at排序
# 更新:
data_query = f""" data_query = f"""
# 更新:
SELECT post_id, title, content, category, post_type, price, SELECT post_id, title, content, category, post_type, price,
# 更新:
author_username, post_time, reply_count, view_count, url author_username, post_time, reply_count, view_count, url
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE {where_sql} WHERE {where_sql}
# 更新: ORDER BY 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() results = db.execute(text(data_query), params).fetchall()
# 更新:
# 更新:
posts = [PostItem( posts = [PostItem(
# 更新:
post_id=r[0], post_id=r[0],
# 更新:
title=r[1] or "", title=r[1] or "",
# 更新:
content=r[2] or "", content=r[2] or "",
# 更新:
category=r[3], category=r[3],
# 更新:
post_type=r[4] or "", post_type=r[4] or "",
# 更新:
price=float(r[5]) if r[5] else None, price=float(r[5]) if r[5] else None,
# 更新:
author_username=r[6] or "", author_username=r[6] or "",
# 更新:
post_time=str(r[7]) if r[7] else "", post_time=str(r[7]) if r[7] else "",
# 更新:
reply_count=r[8] or 0, reply_count=r[8] or 0,
# 更新:
view_count=r[9] or 0, view_count=r[9] or 0,
# 更新:
url=r[10] url=r[10]
# 更新:
) for r in results] ) for r in results]
# 更新:
# 更新:
return { return {
# 更新:
"posts": posts, "posts": posts,
# 更新:
"total": total_count, "total": total_count,
# 更新:
"page": offset // limit + 1, "page": offset // limit + 1,
# 更新:
"page_size": limit "page_size": limit
# 更新:
} }
# 更新:
# 更新:
@router.get("/users", response_model=List[UserItem]) @router.get("/users", response_model=List[UserItem])
# 更新:
def get_users( def get_users(
# 更新:
limit: int = Query(20, ge=1, le=500), limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
# 更新:
is_seller: Optional[bool] = None, is_seller: Optional[bool] = None,
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取用户列表""" """获取用户列表"""
# 更新:
query = """ query = """
# 更新:
SELECT user_id, username, avatar_url, credit_level, credit_score, SELECT user_id, username, avatar_url, credit_level, credit_score,
# 更新:
post_count, is_seller, registration_date post_count, is_seller, registration_date
# 更新:
FROM yichens_users FROM yichens_users
# 更新:
WHERE 1=1 WHERE 1=1
# 更新:
""" """
# 更新:
params = {"limit": limit, "offset": offset} params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if is_seller is not None: if is_seller is not None:
# 更新:
query += " AND is_seller = :is_seller" query += " AND is_seller = :is_seller"
# 更新:
params["is_seller"] = is_seller params["is_seller"] = is_seller
# 更新:
# 更新:
query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset" query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
# 更新:
# 更新:
results = db.execute(text(query), params).fetchall() results = db.execute(text(query), params).fetchall()
# 更新:
# 更新:
return [UserItem( return [UserItem(
# 更新:
user_id=r[0], user_id=r[0],
# 更新:
username=r[1] or "", username=r[1] or "",
# 更新:
avatar_url=r[2], avatar_url=r[2],
# 更新:
credit_level=r[3], credit_level=r[3],
# 更新:
credit_score=r[4], credit_score=r[4],
# 更新:
post_count=r[5] or 0, post_count=r[5] or 0,
# 更新:
is_seller=r[6] or False, is_seller=r[6] or False,
# 更新:
registration_date=str(r[7]) if r[7] else None registration_date=str(r[7]) if r[7] else None
# 更新:
) for r in results] ) for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/today") @router.get("/stats/today")
# 更新:
async def get_today_stats(db: Session = Depends(get_coolbot_db)): async def get_today_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日新增帖子统计""" """获取今日新增帖子统计"""
# 更新:
query = """ query = """
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes,
# 更新:
SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
""" """
# 更新:
result = db.execute(text(query)).fetchone() result = db.execute(text(query)).fetchone()
# 更新:
return { return {
# 更新:
"total": result[0] or 0, "total": result[0] or 0,
# 更新:
"deals": result[1] or 0, "deals": result[1] or 0,
# 更新:
"wants": result[2] or 0, "wants": result[2] or 0,
# 更新:
"others": result[3] or 0, "others": result[3] or 0,
# 更新:
"dragons": result[4] or 0, "dragons": result[4] or 0,
# 更新:
"horses": result[5] or 0, "horses": result[5] or 0,
# 更新:
"snakes": result[6] or 0, "snakes": result[6] or 0,
# 更新:
"tianma": result[7] or 0 "tianma": result[7] or 0
# 更新:
} }
# 更新:
# 更新:
@router.get("/stats/hour") @router.get("/stats/hour")
# 更新:
async def get_hour_stats(db: Session = Depends(get_coolbot_db)): async def get_hour_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取近一个小时新增帖子统计""" """获取近一个小时新增帖子统计"""
# 更新:
query = """ query = """
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses, SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 hour' WHERE post_time >= NOW() - INTERVAL '1 hour'
# 更新:
""" """
# 更新:
result = db.execute(text(query)).fetchone() result = db.execute(text(query)).fetchone()
# 更新:
return { return {
# 更新:
"total": result[0] or 0, "total": result[0] or 0,
# 更新:
"deals": result[1] or 0, "deals": result[1] or 0,
# 更新:
"wants": result[2] or 0, "wants": result[2] or 0,
# 更新:
"others": result[3] or 0, "others": result[3] or 0,
# 更新:
"dragons": result[3] or 0, "dragons": result[3] or 0,
# 更新:
"horses": result[4] or 0, "horses": result[4] or 0,
# 更新:
"snakes": result[5] or 0 "snakes": result[5] or 0
# 更新:
} }
# 更新:
# 更新:
@router.get("/stats/today-category") @router.get("/stats/today-category")
# 更新:
async def get_today_category_stats(db: Session = Depends(get_coolbot_db)): async def get_today_category_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日帖子分类统计""" """获取今日帖子分类统计"""
# 更新:
query = """ query = """
# 更新:
SELECT category, COUNT(*) as count SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
GROUP BY category GROUP BY category
# 更新:
ORDER BY count DESC ORDER BY count DESC
# 更新:
""" """
# 更新:
results = db.execute(text(query)).fetchall() results = db.execute(text(query)).fetchall()
# 更新:
return [{"category": r[0] or "未分类", "count": r[1]} for r in results] return [{"category": r[0] or "未分类", "count": r[1]} for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/dragons-today") @router.get("/stats/dragons-today")
# 更新:
def get_dragons_stats_today( def get_dragons_stats_today(
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取今日龙钞详细统计数据(按号码分类)- 就高不就低""" """获取今日龙钞详细统计数据(按号码分类)- 就高不就低"""
# 更新:
from sqlalchemy import text from sqlalchemy import text
# 更新:
# 更新:
# 1. 带4包含"带4"、"带四"、"通货" # 1. 带4包含"带4"、"带四"、"通货"
# 更新:
dai4 = db.execute(text(""" dai4 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%带4%' OR title LIKE '%带4%' content LIKE '%带4%' OR title LIKE '%带4%'
# 更新:
OR content LIKE '%带四%' OR title LIKE '%带四%' OR content LIKE '%带四%' OR title LIKE '%带四%'
# 更新:
OR content LIKE '%通货%' OR title LIKE '%通货%' OR content LIKE '%通货%' OR title LIKE '%通货%'
# 更新:
) )
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 2. 无4包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒" # 2. 无4包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒"
# 更新:
wu4 = db.execute(text(""" wu4 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无4%' OR title LIKE '%无4%' content LIKE '%无4%' OR title LIKE '%无4%'
# 更新:
OR content LIKE '%无四%' OR title LIKE '%无四%' OR content LIKE '%无四%' OR title LIKE '%无四%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%' AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%'
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
# 更新:
AND content NOT LIKE '%永恒%' AND title NOT LIKE '%永恒%' AND content NOT LIKE '%永恒%' AND title NOT LIKE '%永恒%'
# 更新:
AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%' AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 3. 无47包含"无47"、"永恒"、"无四七",排除"无247" # 3. 无47包含"无47"、"永恒"、"无四七",排除"无247"
# 更新:
wu47 = db.execute(text(""" wu47 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无47%' OR title LIKE '%无47%' content LIKE '%无47%' OR title LIKE '%无47%'
# 更新:
OR content LIKE '%永恒%' OR title LIKE '%永恒%' OR content LIKE '%永恒%' OR title LIKE '%永恒%'
# 更新:
OR content LIKE '%无四七%' OR title LIKE '%无四七%' OR content LIKE '%无四七%' OR title LIKE '%无四七%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 4. 无247包含"无247"、"天马"、"金山",排除"无347" # 4. 无247包含"无247"、"天马"、"金山",排除"无347"
# 更新:
wu247 = db.execute(text(""" wu247 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无247%' OR title LIKE '%无247%' content LIKE '%无247%' OR title LIKE '%无247%'
# 更新:
OR content LIKE '%天马%' OR title LIKE '%天马%' OR content LIKE '%天马%' OR title LIKE '%天马%'
# 更新:
OR content LIKE '%金山%' OR title LIKE '%金山%' OR content LIKE '%金山%' OR title LIKE '%金山%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%' AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 5. 无347包含"无347"、"钻石"、"金马"、"魅力"、"朦胧" # 5. 无347包含"无347"、"钻石"、"金马"、"魅力"、"朦胧"
# 更新:
wu347 = db.execute(text(""" wu347 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无347%' OR title LIKE '%无347%' content LIKE '%无347%' OR title LIKE '%无347%'
# 更新:
OR content LIKE '%钻石%' OR title LIKE '%钻石%' OR content LIKE '%钻石%' OR title LIKE '%钻石%'
# 更新:
OR content LIKE '%金马%' OR title LIKE '%金马%' OR content LIKE '%金马%' OR title LIKE '%金马%'
# 更新:
OR content LIKE '%魅力%' OR title LIKE '%魅力%' OR content LIKE '%魅力%' OR title LIKE '%魅力%'
# 更新:
OR content LIKE '%朦胧%' OR title LIKE '%朦胧%' OR content LIKE '%朦胧%' OR title LIKE '%朦胧%'
# 更新:
) )
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
return { return {
# 更新:
"dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0}, "dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0},
# 更新:
"wu4": {"total": wu4[0] or 0, "deals": wu4[1] or 0, "wants": wu4[2] or 0}, "wu4": {"total": wu4[0] or 0, "deals": wu4[1] or 0, "wants": wu4[2] or 0},
# 更新:
"wu47": {"total": wu47[0] or 0, "deals": wu47[1] or 0, "wants": wu47[2] or 0}, "wu47": {"total": wu47[0] or 0, "deals": wu47[1] or 0, "wants": wu47[2] or 0},
# 更新:
"wu247": {"total": wu247[0] or 0, "deals": wu247[1] or 0, "wants": wu247[2] or 0}, "wu247": {"total": wu247[0] or 0, "deals": wu247[1] or 0, "wants": wu247[2] or 0},
# 更新:
"wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0} "wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0}
# 更新:
} }
# 更新:
# 更新:

47
backend/logs/app.log Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,35 +0,0 @@
# 版本更新记录
## 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 +0,0 @@
1.2.98

View File

@ -1,48 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,50 +0,0 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: jiachenlong-db-test
restart: unless-stopped
environment:
POSTGRES_DB: zodiac
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
backend:
image: python:3.11-slim
container_name: jiachenlong-backend-test
restart: unless-stopped
working_dir: /app
command: >
bash -c "pip install fastapi uvicorn sqlalchemy psycopg2-binary pydantic python-jose bcrypt python-multipart pillow dashscope alibabacloud-dysmsapi20170525 -q && uvicorn app.main:app --host 0.0.0.0 --port 3000"
ports:
- "3000:3000"
volumes:
- /root/jiachenlong/backend:/app
environment:
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/zodiac
- SECRET_KEY=test-secret-key-for-sms
- ACCESS_TOKEN_EXPIRE_MINUTES=60
- OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG
- OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1
- OSS_BUCKET=jiachenlong-oss
- OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
- SMS_ACCESS_KEY_ID=LTAI5tQAx5niD7JQVqGE5acE
- SMS_ACCESS_KEY_SECRET=QsQFAEKBkaNynIoKyvdIi3BUyWVZu1
- SMS_SIGN_NAME=苏州算力
- SMS_TEMPLATE_CODE=SMS_501590956
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:

View File

@ -1,73 +0,0 @@
version: '3.8'
# 甲辰藏品管理系统 v1.0.0 - Docker 配置
# 使用方式docker-compose up -d
services:
# PostgreSQL 数据库
postgres:
image: postgres:15-alpine
container_name: jiachenlong-db
restart: unless-stopped
environment:
POSTGRES_DB: zodiac
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# FastAPI 后端服务
backend:
build:
context: ../backend
dockerfile: Dockerfile
container_name: jiachenlong-backend
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ../backend/uploads:/app/uploads
- ../static:/app/static
environment:
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/zodiac
- SECRET_KEY=production-secret-key-change-me
- ACCESS_TOKEN_EXPIRE_MINUTES=60
- PORT=3000
- HOST=0.0.0.0
- DASHSCOPE_API_KEY=sk-your-api-key
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# Nginx 前端服务
frontend:
image: nginx:alpine
container_name: jiachenlong-frontend
restart: unless-stopped
ports:
- "80:80"
volumes:
- ../frontend/dist:/usr/share/nginx/html:ro
- ../static:/usr/share/nginx/html/static:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- backend
volumes:
postgres_data:
networks:
default:
name: jiachenlong-network

View File

@ -1,60 +0,0 @@
# Nginx 配置 - 甲辰藏品管理系统 v1.0.0
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.html;
# 允许上传最大 20MB 的文件
client_max_body_size 20M;
# 前端静态文件SPA 路由)
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源目录(图片、图标、字体)
location /static {
alias /var/www/html/static;
expires 30d;
add_header Cache-Control "public, immutable";
}
# API 代理到后端
location /api {
proxy_pass http://47.110.37.129:3000/api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 20M;
}
# 缓存静态资源(必须在 /uploads 之前,否则图片会被代理)
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 图片上传文件代理(必须在图片扩展名 location 之前)
location /uploads {
proxy_pass http://47.110.37.129:3000/uploads;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 20M;
}
# 前端静态图片缓存
location ~* ^/static/.*\.(png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
}

View File

@ -1,291 +0,0 @@
# 后端服务守护进程配置指南
**配置时间**: 2026-03-14
**版本**: v2.7.4
---
## 🔍 后端不稳定原因分析
### 可能原因
1. **手动启动无守护** - 之前使用 `nohup` 但没有监控
2. **服务器重启** - 服务器重启后需要手动启动
3. **内存不足** - 检查发现内存充足 (3.5GB 可用 1.5GB)
4. **磁盘空间** - 检查发现磁盘充足 (49GB 可用 31GB)
5. **进程意外终止** - 可能因系统资源调度被 kill
### 日志分析
检查 `/tmp/zodiac-backend.log` 发现:
- ✅ 没有 Python 异常
- ✅ 没有内存溢出
- ✅ 没有数据库连接错误
- ✅ 服务正常运行直到意外停止
**结论**: 进程缺少守护机制,意外停止后无法自动恢复
---
## ✅ 解决方案:双重守护
### 方案 1: 启动脚本 + Crontab 监控(已配置)
**启动脚本**: `/home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi/start.sh`
**功能**:
- ✅ 检查进程是否已在运行
- ✅ 停止旧进程
- ✅ 启动新进程
- ✅ 保存 PID 到文件
- ✅ 验证启动是否成功
**Crontab 监控**: 每 2 分钟检查一次
```bash
*/2 * * * * if ! ps aux | grep -v grep | grep 'uvicorn app.main:app' > /dev/null; then
/home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi/start.sh >> /tmp/backend-watch.log 2>&1;
fi
```
**优点**:
- 简单可靠
- 自动恢复
- 日志记录
---
### 方案 2: systemd 服务(备选)
如果 crontab 方案不可靠,可以使用 systemd
**服务文件**: `/etc/systemd/system/zodiac-backend.service`
```ini
[Unit]
Description=甲辰藏品管理系统 FastAPI 后端服务
After=network.target
[Service]
Type=simple
User=admin
WorkingDirectory=/home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi
ExecStart=/usr/local/python3.12/bin/python3.12 -m uvicorn app.main:app --port 3000 --host 0.0.0.0
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
```
**启用命令**:
```bash
sudo systemctl daemon-reload
sudo systemctl enable zodiac-backend
sudo systemctl start zodiac-backend
```
---
## 📋 使用指南
### 启动服务
```bash
# 方法 1: 使用启动脚本
/home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi/start.sh
# 方法 2: 手动启动
cd /home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi
nohup /usr/local/python3.12/bin/python3.12 -m uvicorn app.main:app --port 3000 --host 0.0.0.0 > /tmp/zodiac-backend.log 2>&1 &
```
### 停止服务
```bash
# 方法 1: 使用 PID 文件
kill $(cat /tmp/zodiac-backend.pid)
# 方法 2: 杀死进程
pkill -f "uvicorn app.main:app"
```
### 查看状态
```bash
# 查看进程
ps aux | grep uvicorn
# 查看日志
tail -f /tmp/zodiac-backend.log
# 查看监控日志
tail -f /tmp/backend-watch.log
```
### 重启服务
```bash
pkill -f "uvicorn app.main:app"
sleep 2
/home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi/start.sh
```
---
## 🔧 故障排查
### 问题 1: 服务无法启动
**检查端口占用**:
```bash
netstat -tlnp | grep 3000
# 如果占用,杀死进程
kill -9 $(lsof -t -i:3000)
```
**检查 Python 路径**:
```bash
which python3.12
# 应该是:/usr/local/python3.12/bin/python3.12
```
**检查依赖**:
```bash
cd /home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi
pip3 list | grep -i "fastapi\|uvicorn\|sqlalchemy"
```
### 问题 2: 服务频繁重启
**查看监控日志**:
```bash
tail -100 /tmp/backend-watch.log
```
**查看系统日志**:
```bash
dmesg | grep -i "killed\|oom"
```
**检查资源使用**:
```bash
free -h
df -h
top -bn1 | head -20
```
### 问题 3: Crontab 不执行
**检查 crontab 配置**:
```bash
crontab -l
```
**检查 cron 服务**:
```bash
systemctl status crond
```
**查看 cron 日志**:
```bash
tail -f /var/log/cron
```
---
## 📊 监控指标
### 进程状态
```bash
# 进程是否在运行
ps aux | grep uvicorn | grep -v grep | wc -l
# 应该返回1
```
### 服务响应
```bash
# 测试 API 响应
curl -s http://localhost:3000/api/auth/login -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=admin123" | python3 -c "import sys,json; d=json.load(sys.stdin); print('正常' if 'access_token' in d else '异常')"
```
### 日志大小
```bash
# 检查日志文件大小
ls -lh /tmp/zodiac-backend.log
# 如果>100MB考虑轮转
```
---
## 🎯 最佳实践
### 1. 定期重启
建议每周重启一次服务,释放内存:
```bash
# 添加到 crontab
0 3 * * 0 pkill -f "uvicorn app.main:app" && sleep 2 && /home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi/start.sh
```
### 2. 日志轮转
创建 `/etc/logrotate.d/zodiac-backend`:
```
/tmp/zodiac-backend.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0644 admin admin
}
```
### 3. 监控告警
可以添加简单的告警脚本:
```bash
#!/bin/bash
if ! curl -s http://localhost:3000/health > /dev/null; then
echo "后端服务异常!" | mail -s "告警:后端服务宕机" admin@example.com
fi
```
---
## 📝 配置文件清单
| 文件 | 路径 | 说明 |
|------|------|------|
| **启动脚本** | `backend-fastapi/start.sh` | 服务启动脚本 |
| **PID 文件** | `/tmp/zodiac-backend.pid` | 进程 ID |
| **日志文件** | `/tmp/zodiac-backend.log` | 运行日志 |
| **监控日志** | `/tmp/backend-watch.log` | 监控日志 |
| **Crontab** | `crontab -l` | 定时任务 |
---
## ✅ 验证清单
- [x] 启动脚本已创建
- [x] 脚本权限已设置 (chmod +x)
- [x] Crontab 监控已配置
- [x] 服务正在运行
- [x] API 响应正常
- [ ] systemd 服务(备选)
- [ ] 日志轮转配置
- [ ] 监控告警配置
---
**配置完成!后端服务现在具有自动恢复能力!** 🎉

View File

@ -1,249 +0,0 @@
# 服务器彻底清理报告
**清理时间**: 2026-03-16 09:35
**执行人**: 菜鸟小 D 🤖
**目标**: 清理所有 zodiac 相关的旧版本、材料、服务
---
## ✅ 清理完成清单
### 1. 前端应用服务器 (8.149.137.26)
**已删除的目录**:
- ❌ `/var/www/frontend/` - 旧前端目录
- ❌ `/var/www/mobile/` - 旧移动端目录
**已删除的配置文件**:
- ❌ `/etc/nginx/conf.d/zodiac.conf` - Nginx 配置
**已停止的服务**:
- ❌ Nginx 服务 (已停止)
**当前状态**:
```
/var/www/
└── html/ # 仅保留默认页面
/etc/nginx/conf.d/
└── (空) # 所有 zodiac 配置已删除
```
---
### 2. 后端应用服务器 (47.110.37.129)
**已删除的目录**:
- ❌ `/opt/zodiac-backend/` - 后端主目录
- ❌ `backend-fastapi/` - 后端代码
- ❌ `backend-fastapi-v2.7.9-backup/` - 备份
- ❌ `zodiac-mobile/` - 前端代码
- ❌ `zodiac-v2.8.0/` - 旧版本
- ❌ `/tmp/zodiac*` - 临时文件
- ❌ `/tmp/v280.zip` - 压缩包
**已删除的文件**:
- ❌ `/tmp/uvicorn*` - uvicorn 临时文件
- ❌ `/tmp/pip-build*` - pip 构建缓存
- ❌ `/tmp/zodiac-v280.log` - 日志文件
**已停止的服务**:
- ❌ uvicorn 后端服务 (PID 195421)
**当前状态**:
```
/opt/
└── (无 zodiac 相关目录)
/tmp/
└── (无 zodiac 相关文件)
```
---
### 3. 数据库服务器 (47.98.171.101)
**已删除的目录**:
- ❌ `/var/www/frontend/` - 旧前端目录
- ❌ `/var/www/mobile/` - 旧移动端目录
**已删除的数据库**:
- ❌ 数据库 `zodiac` (包含所有表和数据)
- ❌ `users`
- ❌ `collections`
- ❌ `collection_images`
- ❌ `custom_fields`
- ❌ `operations`
**已终止的连接**:
- ❌ 4 个活跃的 zodiac 数据库连接
**当前状态**:
```
/var/www/
└── html/ # 仅保留默认页面
PostgreSQL:
└── 数据库 zodiac (空数据库,已重建)
```
---
## 📊 清理统计
| 服务器 | 删除目录数 | 删除文件数 | 停止服务 | 删除数据库 |
|--------|-----------|-----------|----------|-----------|
| 8.149.137.26 | 2 | 1 | Nginx | - |
| 47.110.37.129 | 6+ | 10+ | uvicorn | - |
| 47.98.171.101 | 2 | 0 | - | 1 个数据库 + 5 个表 |
| **总计** | **10+** | **11+** | **2** | **1 个数据库** |
---
## 🗑️ 已清理的内容分类
### 代码目录
- ❌ `/opt/zodiac-backend/`
- ❌ `/var/www/frontend/`
- ❌ `/var/www/mobile/`
- ❌ `/tmp/zodiac-collector/`
### 配置文件
- ❌ `/etc/nginx/conf.d/zodiac.conf`
### 临时文件
- ❌ `/tmp/zodiac*`
- ❌ `/tmp/uvicorn*`
- ❌ `/tmp/pip-build*`
- ❌ `/tmp/v280.zip`
### 日志文件
- ❌ `/tmp/zodiac-v280.log`
### 数据库
- ❌ 数据库 `zodiac` (所有表和数据)
- ❌ `users`
- ❌ `collections`
- ❌ `collection_images`
- ❌ `custom_fields`
- ❌ `operations`
### 服务进程
- ❌ Nginx (前端服务器)
- ❌ uvicorn (后端服务器)
- ❌ 4 个数据库连接
---
## ✅ 保留的内容
### 数据库服务器
- ✅ PostgreSQL 服务 (运行中)
- ✅ 数据库 `zodiac` (空数据库,已重建)
- ❌ 所有业务数据已清理
- ✅ 数据库用户 `postgres`
### 工作区代码
- ✅ `/home/admin/.openclaw/workspace/jiachenlong/` - 新版本 v1.0.0 代码
---
## 🎯 当前服务器状态
### 前端服务器 (8.149.137.26)
- ✅ Nginx 已停止
- ✅ 所有 zodiac 文件已删除
- ✅ 等待新版本部署
### 后端服务器 (47.110.37.129)
- ✅ 后端服务已停止
- ✅ 所有 zodiac 文件已删除
- ✅ 等待新版本部署
### 数据库服务器 (47.98.171.101)
- ✅ PostgreSQL 运行正常
- ✅ 数据库数据完整
- ✅ 等待新版本连接
---
## 📋 下一步 - 部署 v1.0.0
### ⚠️ 重要提示
**数据库已清空**: 所有旧数据已删除,需要重新初始化数据库结构。
### 1. 准备新代码
```bash
cd /home/admin/.openclaw/workspace/jiachenlong
# 构建前端
cd frontend
npm install
npm run build
```
### 2. 部署后端到 47.110.37.129
```bash
# 创建目录
ssh root@47.110.37.129 "mkdir -p /opt/jiachenlong-backend"
# 复制代码
scp -r backend/* root@47.110.37.129:/opt/jiachenlong-backend/
# 安装依赖
ssh root@47.110.37.129 "cd /opt/jiachenlong-backend && pip3 install -r requirements.txt"
# 配置环境变量
ssh root@47.110.37.129 "cd /opt/jiachenlong-backend && cat > .env << EOF
DATABASE_URL=postgresql://postgres:postgres@47.98.171.101:5432/zodiac
SECRET_KEY=jiachenlong-secret-key-v1-0-0
ACCESS_TOKEN_EXPIRE_MINUTES=60
PORT=3000
HOST=0.0.0.0
DASHSCOPE_API_KEY=sk-your-api-key
EOF"
# 启动服务 (会自动创建数据库表)
ssh root@47.110.37.129 "cd /opt/jiachenlong-backend && nohup python3 -m uvicorn app.main:app --port 3000 --host 0.0.0.0 > /tmp/uvicorn.log 2>&1 &"
```
### 3. 部署前端到 8.149.137.26
```bash
# 复制构建文件
scp -r dist/* root@8.149.137.26:/var/www/html/
# 配置 Nginx
scp config/nginx.conf root@8.149.137.26:/etc/nginx/conf.d/jiachenlong.conf
# 启动 Nginx
ssh root@8.149.137.26 "nginx && nginx -s reload"
```
### 4. 验证部署
```bash
# 检查后端健康
curl http://47.110.37.129:3000/health
# 检查前端
curl http://8.149.137.26/
# 检查数据库表
ssh root@47.98.171.101 "sudo -u postgres psql -d zodiac -c '\\dt'"
```
---
## ⚠️ 注意事项
1. **全新部署**: 所有旧版本已彻底清理,需要全新部署 v1.0.0
2. **数据库保留**: 数据库和数据完整保留,可以直接使用
3. **配置更新**: 需要重新配置 Nginx 和后端环境变量
4. **服务重启**: 需要重新启动 Nginx 和 uvicorn 服务
---
**清理完成!服务器已准备就绪,可以部署新版本 v1.0.0** 🎉
**菜鸟小 D 整理** 🤖
2026-03-16 09:35

View File

@ -1,165 +0,0 @@
# 甲辰藏品管理系统 v1.0.1 部署完成报告
**部署时间**: 2026-03-16 11:37
**部署版本**: v1.0.1
**Git 提交**: 06a0fa6
**Git 标签**: v1.0.1
---
## ✅ 部署状态
| 服务器 | IP | 服务 | 状态 |
|--------|------|------|------|
| 前端服务器 | 8.149.137.26 | Nginx | ✅ 运行中 |
| 后端服务器 | 47.110.37.129 | FastAPI | ✅ 运行中 |
| 数据库服务器 | 47.98.171.101 | PostgreSQL | ✅ 运行中 |
---
## 🎯 v1.0.1 修复内容
### 1. Logo 显示问题修复
**问题**: 藏品详情页图片加载失败时显示 Logo导致混淆
**修复**: 显示"无图片"占位符Logo 仅在登录页/首页显示
**文件**: `frontend/src/pages/Detail.jsx`
### 2. 图片代理问题修复
**问题**: Nginx location 优先级错误,图片返回 404
**修复**: 调整 `/uploads` location 优先级最高
**文件**: `config/nginx.conf`
### 3. 后端图片数据加载
**问题**: 藏品列表 API 不返回图片数据
**修复**: `get_collections()` 添加图片数据加载
**文件**: `backend/app/routers/collections.py`
### 4. 前端图片路径修复
**问题**: 路径重复 `/uploads/uploads/`
**修复**: 直接使用 `path` 字段
**文件**: `frontend/src/pages/Detail.jsx`
---
## 📊 代码统计
| 项目 | 数量 |
|------|------|
| Git 提交 | 1 个 (初始提交) |
| 文件数 | 68 个 |
| 代码行数 | 14,824 行 |
| 标签 | v1.0.1 |
---
## 📁 目录结构
```
jiachenlong/
├── backend/ # FastAPI 后端
├── frontend/ # React 前端
├── config/ # 配置文件
├── static/ # 静态资源
├── docs/ # 文档
├── scripts/ # 部署脚本
├── README.md # 项目说明
└── .git/ # Git 仓库
```
---
## 🔧 配置信息
### 数据库
- **地址**: 47.98.171.101:5432
- **数据库**: zodiac
- **用户**: postgres
### 后端服务
- **地址**: 47.110.37.129:3000
- **路径**: /opt/jiachenlong-backend
### 前端服务
- **地址**: 8.149.137.26:80
- **路径**: /var/www/html
### Logo 文件
- **路径**: /var/www/html/static/images/jiachenlong-logo.png
- **大小**: 606KB
- **尺寸**: 1080x1080
---
## ✅ 功能验证
### Logo 显示
- ✅ 登录页面显示 Logo
- ✅ 首页显示 Logo
- ✅ 详情页无图片显示"无图片"占位符
- ✅ Logo 不用于替代缺失的藏品图片
### 图片功能
- ✅ 藏品列表显示缩略图
- ✅ 藏品详情显示大图
- ✅ 图片预览弹窗正常
- ✅ 图片切换功能正常
- ✅ 后端图片代理正常
### API 接口
- ✅ GET /api/collections - 返回图片数据
- ✅ GET /api/collections/:id - 返回图片详情
- ✅ POST /api/collections/upload-image - 图片上传
- ✅ GET /uploads/collections/xxx.jpg - 图片访问
---
## 📝 重要文档
| 文档 | 说明 |
|------|------|
| `docs/RELEASE_v1.0.1.md` | v1.0.1 发布说明 |
| `docs/IMAGE_PROCESSING_FLOW.md` | 图片处理流程 |
| `static/images/LOGO_GUIDE.md` | Logo 使用规范 |
| `docs/CLEANUP_REPORT.md` | 服务器清理报告 |
| `DEPLOYMENT_v1.0.0.md` | 部署指南 |
---
## 🌐 访问地址
**前端**: http://8.149.137.26/
**后端 API**: http://47.110.37.129:3000/
**数据库**: 47.98.171.101:5432
**默认账号**:
- 用户名admin
- 密码admin123
---
## 📋 下一步建议
1. ✅ 修改默认管理员密码
2. ✅ 备份数据库
3. ✅ 配置 HTTPS
4. ✅ 监控系统运行状态
5. ✅ 定期备份图片文件
---
## 📞 技术支持
- **代码仓库**: http://47.253.189.47:3000/coolbot/jiachenlong
- **版本标签**: v1.0.1
- **提交哈希**: 06a0fa6
---
**部署完成!系统运行正常!** 🎉
**甲辰藏品管理系统开发团队**
2026-03-16

View File

@ -1,114 +0,0 @@
# 甲辰藏品管理系统 v1.0.0 部署指南
**文档版本**: 1.0
**适用版本**: v1.0.0+
**更新日期**: 2026-03-16
---
## 环境要求
| 组件 | 最低版本 | 推荐版本 |
|------|---------|---------|
| Python | 3.8+ | 3.12 |
| Node.js | 18+ | 24 |
| PostgreSQL | 12+ | 15 |
| Nginx | 1.18+ | 1.20+ |
---
## 服务器架构
| 角色 | IP | 状态 | 服务 |
|------|------|------|------|
| 数据库 PostgreSQL 主 | 47.98.171.101 | ✅ 运行中 | PostgreSQL 16 |
| 后端 FastAPI App1 | 42.121.116.25 | ✅ 运行中 | FastAPI (端口 3000) |
| 前端 Nginx Web1 | 8.154.46.3 | ✅ 运行中 | Nginx (端口 80) |
| 域名入口 | 39.106.51.77 | ⏸️ 待配置 | SSH 认证失败 |
---
## 部署详情
### 1. 数据库服务器 (47.98.171.101)
- PostgreSQL 16 已安装并运行
- 数据库 `zodiac` 已创建
- 用户 `postgres` 密码 `postgres`
- 已配置远程访问0.0.0.0/0
- 数据表users, collections, collection_images, operations, custom_fields
### 2. 后端服务器 (42.121.116.25)
- 代码路径:`/opt/zodiac-collector/backend-fastapi`
- Python 版本3.11.13
- 服务systemd (zodiac-backend.service)
- 自启动:已启用
- 数据库连接postgresql://postgres:postgres@47.98.171.101:5432/zodiac
### 3. 前端服务器 (8.154.46.3)
- 代码路径:`/opt/zodiac-collector`
- Web 前端:`/var/www/frontend` (端口 80)
- 移动端:`/var/www/mobile/dist` (/mobile/)
- Nginx 已配置反向代理到后端 API
- 自启动:已启用
---
## 访问地址
- **Web 管理端**: http://8.154.46.3/
- **移动端**: http://8.154.46.3/mobile/
- **后端 API**: http://42.121.116.25:3000/api/
---
## 验证结果
✅ 后端 API 正常响应(需要认证)
✅ 前端 Nginx 反向代理正常
✅ 数据库连接正常
✅ 所有服务已配置自启动
---
## 问题修复 (2026-03-16 08:00)
### 1. 藏品标签黑屏问题 ✅ 已修复
**问题原因**: Collections 组件的 `load()` 函数缺少错误处理API 请求失败时导致组件崩溃。
**修复方案**:
- 添加 try-catch 错误处理
- 重新构建并部署前端
### 2. Logo 显示问题 ✅ 已修复
**问题原因**: Nginx 配置文件冲突,`conf.d/` 目录下的旧配置指向错误的后端地址。
**修复方案**:
- 删除旧的配置文件 (`mobile.conf`, `zodiac.conf`)
- 更新 Nginx 配置,正确代理 API 请求到新后端地址
- 重启 Nginx 服务
### 3. 数据库初始化 ✅ 已完成
**操作**: 创建默认管理员账号
- 用户名:`admin`
- 密码:`admin123`
---
## 默认管理员账号
**用户名**: `admin`
**密码**: `admin123`
⚠️ **重要**: 首次登录后请立即修改密码!
---
**部署人**: 菜鸟小 D
**部署状态**: ✅ 完成(域名入口待配置)
**最后更新**: 2026-03-16 08:05 CST

View File

@ -1,195 +0,0 @@
# 甲辰藏品管理系统 - 完整错误码文档
**版本**: v2.7.3
**更新时间**: 2026-03-14
---
## 📖 错误码格式
```
E + 模块 (2 位) + 序号 (3 位)
```
例如:`E00011` = 认证模块 (01) + 第 11 号错误
---
## 🔢 完整错误码列表
### 00-09: 通用错误
| 错误码 | 含义 | HTTP 状态 | 常见原因 | 解决方案 |
|--------|------|----------|----------|----------|
| E00000 | 未知错误 | 0 | 未定义的错误 | 检查日志 |
| E00001 | 网络连接失败 | 0 | 网络不通、服务未启动 | 检查网络和后端服务 |
| E00002 | 服务器响应超时 | 0 | 请求超时 | 重试或检查服务器负载 |
| E00003 | 服务器内部错误 | 500 | 代码异常、数据库错误 | 查看后端日志 |
### 10-19: 认证错误
| 错误码 | 含义 | HTTP 状态 | 常见原因 | 解决方案 |
|--------|------|----------|----------|----------|
| E00010 | 未登录或登录已过期 | 401 | Token 失效 | 重新登录 |
| E00011 | 用户名或密码错误 | 401 | 密码错误、用户名不存在 | 检查账号密码 |
| E00012 | 验证码错误 | 400 | 验证码输入错误 | 重新输入或刷新验证码 |
| E00013 | 账号已被禁用 | 403 | 账号被封禁 | 联系管理员 |
| E00014 | 无权访问此资源 | 403 | 权限不足 | 申请权限或用管理员账号 |
| E00015 | 令牌无效或已过期 | 401 | Token 过期 | 重新登录 |
### 20-29: 登录注册
| 错误码 | 含义 | HTTP 状态 | 常见原因 | 解决方案 |
|--------|------|----------|----------|----------|
| E00020 | 请输入用户名和密码 | 400 | 空表单 | 填写完整信息 |
| E00021 | 用户名至少 3 个字符 | 400 | 用户名太短 | 使用更长的用户名 |
| E00022 | 密码至少 6 个字符 | 400 | 密码太短 | 使用更长的密码 |
| E00023 | 用户名已存在 | 400 | 重复注册 | 更换用户名 |
| E00024 | 邮箱已被注册 | 400 | 邮箱重复 | 更换邮箱或找回密码 |
| E00025 | 邮箱格式不正确 | 400 | 邮箱格式错误 | 检查邮箱格式 |
| E00026 | 手机号格式不正确 | 400 | 手机号格式错误 | 检查手机号格式 |
### 30-39: 藏品管理
| 错误码 | 含义 | HTTP 状态 | 常见原因 | 解决方案 |
|--------|------|----------|----------|----------|
| E00030 | 藏品名称不能为空 | 400 | 名称为空 | 填写名称 |
| E00031 | 藏品名称至少 2 个字符 | 400 | 名称太短 | 使用更长的名称 |
| E00032 | 藏品分类不能为空 | 400 | 分类为空 | 选择分类 |
| E00033 | 藏品不存在 | 404 | ID 错误、已删除 | 检查藏品 ID |
| E00034 | 禁止重复:此冠字号已存在 | 400 | 重复编号 | 使用不同编号 |
| E00035 | 成本价格必须>=0 | 400 | 负数价格 | 输入正数 |
| E00036 | 目标价格必须>=0 | 400 | 负数价格 | 输入正数 |
| E00037 | 发行年份必须是 4 位数字 | 400 | 年份格式错误 | 如2024 |
| E00038 | 图片格式不正确 | 400 | 不支持的图片格式 | 使用 JPG/PNG |
| E00039 | 图片大小不能超过 10MB | 400 | 图片太大 | 压缩图片 |
### 40-49: OCR 识别
| 错误码 | 含义 | HTTP 状态 | 常见原因 | 解决方案 |
|--------|------|----------|----------|----------|
| E00040 | 请选择图片文件 | 400 | 未选择图片 | 上传图片 |
| E00041 | 图片尺寸太小,无法识别 | 400 | 图片分辨率太低 | 使用更清晰的图片 |
| E00042 | OCR 识别失败,请重试 | 500 | 识别服务异常 | 重试或更换图片 |
| E00043 | OCR 服务暂时不可用 | 503 | 服务宕机 | 稍后重试 |
| E00044 | 无法识别图片内容 | 400 | 图片内容不清晰 | 更换清晰的图片 |
### 50-59: 用户管理(仅管理员)
| 错误码 | 含义 | HTTP 状态 | 常见原因 | 解决方案 |
|--------|------|----------|----------|----------|
| E00050 | 仅管理员可访问 | 403 | 权限不足 | 使用管理员账号 |
| E00051 | 用户不存在 | 404 | 用户 ID 错误 | 检查用户 ID |
| E00052 | 不能删除自己 | 400 | 删除当前用户 | 删除其他用户 |
| E00053 | 不能修改自己的角色 | 403 | 权限限制 | 让其他管理员修改 |
### 60-69: 文件上传
| 错误码 | 含义 | HTTP 状态 | 常见原因 | 解决方案 |
|--------|------|----------|----------|----------|
| E00060 | 文件太大 | 400 | 超过大小限制 | 压缩文件 |
| E00061 | 不支持的文件格式 | 400 | 格式不支持 | 使用支持的格式 |
| E00062 | 上传失败 | 500 | 服务器错误 | 重试或联系管理员 |
---
## 🔍 特殊错误E00000 + JSON 解析错误
### 错误信息示例
```
⚠️ E00000: Unexpected token '<', "<html> <h"... is not valid JSON
```
### 原因分析
这个错误说明**前端期望 JSON 响应,但实际收到的是 HTML**。常见原因:
1. **后端服务未启动** - Nginx 返回 502/503 错误页面HTML
2. **API 地址配置错误** - 请求了错误的 URL返回 404 页面HTML
3. **网络代理问题** - 防火墙/代理服务器返回拦截页面HTML
4. **浏览器缓存** - 缓存了旧的错误页面
### 解决方案
#### 方案 1: 检查后端服务
```bash
# 检查后端是否运行
ps aux | grep uvicorn
# 如果没有,启动后端
cd /home/admin/.openclaw/workspace/zodiac-collector/backend-fastapi
uvicorn app.main:app --port 3000 --host 0.0.0.0
```
#### 方案 2: 检查 Nginx 配置
```bash
# 检查 Nginx 状态
systemctl status nginx
# 检查 Nginx 配置
nginx -t
```
#### 方案 3: 清除浏览器缓存
1. 按 `F12` 打开开发者工具
2. 右键点击刷新按钮
3. 选择"**清空缓存并硬性重新加载**"
#### 方案 4: 检查 API 地址
打开浏览器开发者工具 → Network 标签,查看登录请求的 URL
- 应该是:`http://120.26.133.10:3001/api/auth/login`
- 如果是其他地址,说明配置有误
---
## 🛠️ 调试技巧
### 1. 查看浏览器控制台
`F12` 打开开发者工具,查看:
- **Console** - JavaScript 错误
- **Network** - API 请求详情
### 2. 查看后端日志
```bash
tail -f /tmp/zodiac-backend.log
```
### 3. 查看 Nginx 日志
```bash
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
```
### 4. 测试 API
```bash
# 测试登录接口
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=admin123"
# 测试藏品列表
curl http://localhost:3000/api/collections \
-H "Authorization: Bearer YOUR_TOKEN"
```
---
## 📞 快速诊断流程
```
登录失败
1. 打开浏览器 F12 → Network 标签
2. 查看登录请求的状态码
├── 0 或 (failed) → 网络问题/服务未启动 → 检查后端服务
├── 401 → 密码错误 → 检查账号密码
├── 404 → API 地址错误 → 检查 Nginx 配置
├── 500 → 服务器错误 → 查看后端日志
└── 502/503 → Nginx 无法连接后端 → 重启后端服务
```
---
**文档维护**: 系统自动更新
**最后更新**: 2026-03-14 10:30

View File

@ -1,416 +0,0 @@
# 图片处理流程文档
**版本**: v1.0.0
**更新日期**: 2026-03-16
**作者**: 菜鸟小 D 🤖
---
## 📊 完整流程图
```
用户上传图片
[1] 前端上传组件
[2] 后端接收验证
[3] 文件命名处理
[4] 保存到服务器
[5] 数据库记录
[6] 返回图片 URL
```
---
## 1⃣ 前端上传组件
### 上传页面
**文件**: `frontend/src/pages/Add.jsx`
**上传逻辑**:
```jsx
// 选择图片后自动上传
const handleImageSelect = async (e) => {
const file = e.target.files[0]
if (!file) return
const formData = new FormData()
formData.append('file', file)
formData.append('collection_id', collectionId)
const res = await fetch('/api/ocr/recognize', {
method: 'POST',
body: formData
})
const data = await res.json()
// 处理 OCR 识别结果
}
```
### 图片显示
**文件**: `frontend/src/pages/Detail.jsx`
**显示逻辑**:
```jsx
<img
src={`/uploads/${img.path}`}
alt={img.originalName}
onError={(e) => {
// 加载失败显示"无图片"占位符
e.target.style.display = 'none';
e.target.parentElement.innerHTML = '<div>无图片</div>';
}}
/>
```
---
## 2⃣ 后端接收验证
### API 端点
**文件**: `backend/app/routers/collections.py`
**路由**: `POST /api/collections/upload-image`
### 验证流程
```python
@router.post("/upload-image")
async def upload_image(
collection_id: str = None,
file: UploadFile = File(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
```
### 验证步骤
1. **验证藏品是否存在**
```python
collection = db.query(Collection).filter(
Collection.f99_90_id == collection_id
).first()
if not collection:
raise HTTPException(status_code=404, detail="E00033: 藏品不存在")
```
2. **获取用户信息**
```python
owner = db.query(User).filter(
User.f99_90_id == collection.f99_91_user_id
).first()
username = owner.f01_01_name if owner else "unknown"
```
3. **获取藏品信息**
```python
code = collection.f01_02_code or "0000"
prefix_serial = collection.f02_10_prefix_serial or ""
```
4. **验证文件类型**
```python
if not file.content_type.startswith('image/'):
raise HTTPException(status_code=400,
detail="E00038: 只能上传图片文件")
```
5. **验证文件大小**
```python
file_size = len(content)
if file_size > 10 * 1024 * 1024: # 10MB
raise HTTPException(status_code=400,
detail=f"图片大小不能超过 10MB")
```
---
## 3⃣ 文件命名处理
### 命名规则
**格式**: `用户名 - 藏品编号 - 冠字号。扩展名`
**示例**:
- `admin-0001-J051963351.jpeg`
- `admin-0002-J035161361.JPG`
- `testuser-0015.jpeg` (无冠字号)
### 命名代码
```python
# 清理特殊字符,只保留字母、数字、中文、横杠
import re
clean_username = re.sub(r'[^\w\u4e00-\u9fff\-]', '', username)
clean_serial = re.sub(r'[^\w\u4e00-\u9fff\-]', '', prefix_serial)
# 生成文件名
file_extension = file.filename.split('.')[-1] if '.' in file.filename else 'jpg'
if clean_serial:
filename = f"{clean_username}-{code}-{clean_serial}.{file_extension}"
else:
filename = f"{clean_username}-{code}.{file_extension}"
```
### 避免重名
```python
# 如果文件已存在,添加时间戳
file_path = os.path.join(upload_dir, filename)
if os.path.exists(file_path):
import time
timestamp = int(time.time())
base_name = filename.rsplit('.', 1)[0]
filename = f"{base_name}-{timestamp}.{file_extension}"
file_path = os.path.join(upload_dir, filename)
```
---
## 4⃣ 保存到服务器
### 存储路径
**目录**: `backend/uploads/collections/`
**完整路径**: `/opt/jiachenlong-backend/uploads/collections/`
### 保存代码
```python
# 创建上传目录
upload_dir = "uploads/collections"
os.makedirs(upload_dir, exist_ok=True)
# 保存文件
with open(file_path, "wb") as buffer:
buffer.write(content)
```
### 文件权限
- **所有者**: root
- **权限**: 644 (rw-r--r--)
- **组**: root
---
## 5⃣ 数据库记录
### 数据表
**表名**: `collection_images`
### 表结构
```sql
CREATE TABLE collection_images (
id VARCHAR(36) PRIMARY KEY, -- UUID
collection_id VARCHAR(36), -- 关联藏品 ID
filename VARCHAR(255), -- 文件名
original_name VARCHAR(255), -- 原始文件名
path VARCHAR(500), -- 存储路径
created_at TIMESTAMP DEFAULT NOW() -- 创建时间
);
```
### 插入记录
```python
from app.models.models import CollectionImage
import uuid
image = CollectionImage(
id=str(uuid.uuid4()),
collection_id=collection_id,
filename=filename,
original_name=file.filename,
path=file_path
)
db.add(image)
db.commit()
db.refresh(image)
```
### 返回数据
```python
return {
"message": "上传成功",
"image_id": image.id,
"filename": filename
}
```
---
## 6⃣ 图片访问
### Nginx 代理配置
**文件**: `/etc/nginx/conf.d/jiachenlong.conf`
```nginx
# 图片上传文件代理
location /uploads {
proxy_pass http://47.110.37.129:3000/uploads;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 20M;
}
```
### 访问 URL 格式
```
http://8.149.137.26/uploads/collections/admin-0001-J051963351.jpeg
```
### 后端静态文件服务
**文件**: `backend/app/main.py`
```python
# 挂载静态文件目录(图片上传)
uploads_dir = "uploads"
os.makedirs(uploads_dir, exist_ok=True)
app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
```
---
## 🔍 OCR 识别流程
### API 端点
**路由**: `POST /api/ocr/recognize`
**文件**: `backend/app/routers/ocr.py`
### 识别步骤
1. **读取图片并转 Base64**
```python
image_data = await image.read()
image_base64 = base64.b64encode(image_data).decode('utf-8')
```
2. **调用阿里云 DashScope API**
```python
payload = {
"model": "qwen-vl-max",
"messages": [{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}},
{"type": "text", "text": PROFESSIONAL_PROMPT}
]
}]
}
```
3. **提取识别结果**
```python
def extract_fields(text: str) -> dict:
patterns = {
'version': r'✅.*?2.*?发行版别.*?[:]\s*(.+?)(?:\n|$)',
'prefix_serial': r'✅.*?6.*?冠字序号.*?[:]\s*(.+?)(?:\n|$)',
'grading_score': r'✅.*?8.*?评级分数.*?[:]\s*(.+?)(?:\n|$)',
# ... 更多字段
}
```
4. **返回结构化数据**
```python
return {
"success": True,
"text": text_content,
"fields": fields
}
```
---
## 📋 完整示例
### 用户上传流程
1. **用户选择图片** → 前端显示预览
2. **点击上传** → 发送到 `/api/ocr/recognize`
3. **OCR 识别** → 提取藏品信息
4. **填写表单** → 用户确认/修改信息
5. **保存藏品** → 创建藏品记录
6. **上传图片** → 发送到 `/api/collections/upload-image`
7. **保存成功** → 返回图片 URL
### 文件命名示例
**输入**:
- 用户名:`admin`
- 藏品编号:`0001`
- 冠字号:`J051963351`
- 原始文件名:`001.JPG`
**输出**:
- 文件名:`admin-0001-J051963351.JPG`
- 路径:`uploads/collections/admin-0001-J051963351.JPG`
- URL`http://8.149.137.26/uploads/collections/admin-0001-J051963351.JPG`
---
## ⚠️ 注意事项
### 安全限制
1. **文件大小**: 最大 10MB
2. **文件类型**: 仅支持图片image/*
3. **认证要求**: 必须登录才能上传
4. **权限控制**: 只能上传到自己的藏品
### 性能优化
1. **图片压缩**: 建议前端先压缩再上传
2. **CDN 加速**: 生产环境建议使用 CDN
3. **缓存策略**: Nginx 配置静态资源缓存
### 备份策略
1. **定期备份**: 备份 `uploads/collections/` 目录
2. **数据库备份**: 定期导出 `collection_images`
3. **异地备份**: 重要图片建议异地备份
---
## 🔧 故障排查
### 图片不显示
1. 检查文件是否存在:`ls -lh /opt/jiachenlong-backend/uploads/collections/`
2. 检查数据库记录:`SELECT * FROM collection_images;`
3. 检查 Nginx 日志:`tail -f /var/log/nginx/error.log`
4. 检查后端日志:`tail -f /tmp/uvicorn.log`
### 上传失败
1. 检查文件大小是否超限
2. 检查文件类型是否正确
3. 检查藏品 ID 是否存在
4. 检查磁盘空间是否充足
---
**最后更新**: 2026-03-16
**维护人员**: 菜鸟小 D 🤖

View File

@ -1,291 +0,0 @@
# 甲辰藏品管理系统 v1.0.0 发布说明
**发布日期**: 2026-03-16
**版本**: v1.0.0
**分支**: `main`
**提交**: `initial`
---
## 🎉 初始版本
这是精简重构后的第一个正式版本,包含核心功能。
---
## 🎯 版本亮点
### 1. 统一版本管理系统 📦
**问题**: 之前版本号分散在多个文件,修改麻烦且容易遗漏
**解决方案**:
- 新增根目录 `VERSION` 文件集中管理版本号
- 后端启动时自动读取 VERSION 文件
- 前端构建时自动注入版本号到所有页面
- 浏览器标签页标题自动更新
**使用方法**:
```bash
# 只需修改这一处
vi VERSION
# 修改VERSION=2.9.0
# 重新构建即可
npm run build
```
### 2. 冠字号查重功能 🔍
**功能**: 保存藏品时自动检测是否已有相同冠字号的藏品
**流程**:
1. 用户填写藏品信息(包含冠字号)
2. 点击保存 → 后端自动查重
3. 发现重复 → 弹窗提示:
```
⚠️ 发现重复冠字号!
冠字号J063558611
已存在于:龙钞 (编号0001)
是否继续保存?
```
4. 用户选择:
- **取消** → 终止保存
- **确认** → 强制保存(支持重复冠字号)
**适用场景**:
- 防止误操作重复录入
- 特殊情况下允许保存重复冠字号(如不同评级公司)
### 3. 图片重命名优化 📸
**旧格式**: `UUID.jpg` (如 `aaf56f63-548a-49f1-9b07-116a73b7dfa0.jpg`)
**新格式**: `用户名 - 藏品编号 - 冠字号.jpg`
**示例**:
```
酷博特 -0001-J063558611.jpg
酷博特 -0002-J051811231.jpg
admin-0001.jpg (无冠字号时)
```
**优势**:
- 文件名直观,一眼看出是谁的哪个藏品
- 便于手动查找和管理图片文件
- 自动清理特殊字符,兼容各操作系统
- 文件冲突时自动添加时间戳
---
## 🐛 Bug 修复
### 1. 用户管理 - 角色设置失效 ❌→✅
**问题**: 添加用户时选择"管理员"角色,保存后还是"普通用户"
**原因**:
- 前端调用 `/api/auth/register` 接口(硬编码 role="user"
- 后端使用 `Query` 而非 `Form` 接收参数
**修复**:
- 新增 `POST /api/admin/users` 接口(支持 role 参数)
- 前端改为调用管理员接口
- 修复 error_handler 字段映射错误
### 2. 图片显示 - 全部显示系统 Logo ❌→✅
**问题**: 所有藏品图片都显示系统 logo不显示实际图片
**原因**: Nginx 缺少 `/uploads` 路径代理配置
**修复**:
```nginx
location /uploads {
proxy_pass http://127.0.0.1:3000/uploads;
client_max_body_size 20M;
}
```
### 3. OCR 识别 - API 调用失败 ❌→✅
**问题**: OCR 识别返回 500 错误
**原因**: DashScope API 格式错误
```json
// ❌ 错误格式
{
"model": "qwen-vl-max",
"input": {"messages": [...]}
}
// ✅ 正确格式
{
"model": "qwen-vl-max",
"messages": [...],
"max_tokens": 1000
}
```
---
## ⚙️ 技术优化
### 1. 版本号显示位置
- **统计页面** (`/stats`) - 右上角
- **藏品列表** (`/list`) - 右上角
- **添加藏品** (`/add`) - 右下角浮动
- **用户管理** (`/admin`) - 右下角浮动
- **首页** (`/`) - 底部
- **登录页** (`/login`) - 底部
- **浏览器标签页** - 标题自动更新
### 2. 藏品编码逻辑
**规则**: 本用户所有藏品中最大编码 +1
```python
def generate_code(version: str, user_id: str, db: Session) -> str:
# 查询当前用户的所有编码
user_codes = db.query(Collection.f01_02_code).filter(
Collection.f01_02_code.isnot(None),
Collection.f99_91_user_id == user_id
).all()
# 找出最大数字编码4 位纯数字)
max_num = 0
for (code,) in user_codes:
if re.match(r'^\d{4}$', code):
num = int(code)
if num > max_num:
max_num = num
# 返回最大号 +1
return str(max_num + 1).zfill(4)
```
**特点**:
- ✅ 每个用户独立编码(不与其他用户混算)
- ✅ 自动找出当前用户最大编码
- ✅ 返回最大编码 +14 位数字,如 0001, 0002
### 3. 后端接口优化
- `POST /api/admin/users` - 支持 Form 参数
- `PUT /api/admin/users/{id}` - 同时支持 Query 和 JSON body
- `POST /api/collections?force=true` - 强制保存(忽略重复警告)
### 4. 日志记录增强
```python
logger.info(f"创建用户username={username}, role={role}")
logger.warning(f"发现重复冠字号:{serial}, 已存在 ID: {id}")
logger.info(f"图片上传成功:{filename}")
```
---
## 📊 文件变更统计
**提交**: `1e42b7f`
**变更**: 11 files changed, 206 insertions(+), 48 deletions(-)
### 修改文件列表
1. `VERSION` (新增) - 统一版本配置文件
2. `backend-fastapi/app/main.py` - 自动读取版本号
3. `backend-fastapi/app/routers/collections.py` - 查重 + 图片重命名
4. `backend-fastapi/app/routers/ocr.py` - API 格式修复
5. `backend-fastapi/app/routers/users.py` - 用户管理接口
6. `backend-fastapi/app/core/error_handler.py` - 错误映射修复
7. `zodiac-mobile/package.json` - 版本号
8. `zodiac-mobile/vite.config.js` - 自动更新 title
9. `zodiac-mobile/src/config/version.js` - 自动读取版本
10. `zodiac-mobile/src/pages/Add.jsx` - 查重弹窗
11. `zodiac-mobile/src/pages/Admin.jsx` - 版本号显示
12. `zodiac-mobile/src/pages/List.jsx` - 版本号显示
13. `zodiac-mobile/src/pages/Stats.jsx` - 版本号显示
---
## 🚀 升级指南
### 从 v2.7.x 升级到 v2.8.0
#### 1. 拉取新版本
```bash
cd /path/to/zodiac-collector
git fetch origin
git checkout v2.8.0
```
#### 2. 安装依赖
```bash
# 后端
cd backend-fastapi
pip install -r requirements.txt
# 前端
cd zodiac-mobile
pnpm install
```
#### 3. 重新构建
```bash
# 前端构建
npm run build
sudo cp -r dist/* /var/www/mobile/dist/
# 重启后端
pkill -f "uvicorn app.main:app"
nohup uvicorn app.main:app --port 3000 --host 0.0.0.0 &
```
#### 4. 验证版本
```bash
# 检查后端版本
curl http://localhost:3000/ | grep version
# {"name":"甲辰收藏系统 FastAPI 后端","version":"2.8.0",...}
# 检查前端版本
curl http://localhost:3001/ | grep title
# <title>甲辰收藏 v2.8.0</title>
```
---
## 📝 使用建议
### 1. 版本管理
- 每次发布新版本只需修改 `VERSION` 文件
- 构建前检查版本号是否正确
- 建议遵循语义化版本规范(主版本。次版本。修订版)
### 2. 冠字号查重
- 正常情况直接保存即可
- 如果确实需要保存重复冠字号,点击"确认"继续
- 建议在备注中说明重复原因
### 3. 图片管理
- 新上传的图片自动使用新命名格式
- 旧图片保持原有 UUID 格式(不影响使用)
- 建议定期整理图片文件
---
## 🐛 已知问题
暂无
---
## 📞 技术支持
- **代码仓库**: http://47.253.189.47:3000/coolbot/zodiac-collector
- **问题反馈**: 创建 Issue 或联系开发团队
- **在线系统**: http://120.26.133.10:3001/
---
## 🎉 致谢
感谢所有参与 v2.8.0 开发和测试的团队成员!
**特别感谢**:
- 产品需求提出
- Bug 报告与测试
- 代码审查与优化
---
**甲辰藏品管理系统开发团队**
2026-03-15

View File

@ -1,326 +0,0 @@
# 甲辰藏品管理系统 v1.0.1 发布说明
**发布日期**: 2026-03-16
**版本**: v1.0.1
**前置版本**: v1.0.0
**分支**: `main`
---
## 🎯 版本亮点
### 1. Logo 显示问题修复 🐉
**问题描述**:
- 藏品详情页面图片加载失败时显示 Logo导致所有无图片的藏品都显示 Logo
- 用户体验混淆,无法区分"无图片"和"图片加载失败"
**解决方案**:
- 修改 `frontend/src/pages/Detail.jsx``onError` 处理逻辑
- 图片加载失败时显示"无图片"占位符,不再显示 Logo
- Logo 仅在登录页、首页等指定位置显示
**代码变更**:
```jsx
// 修复前
onError={(e) => { e.target.src = '/static/images/jiachenlong-logo.png'; }}
// 修复后
onError={(e) => {
e.target.style.display = 'none';
e.target.parentElement.innerHTML = '<div>无图片</div>';
}}
```
**影响范围**:
- ✅ 藏品详情页图片显示
- ✅ 藏品列表页图片显示
- ✅ Logo 使用规范化
---
### 2. 图片代理问题修复 🔧
**问题描述**:
- 前端服务器 Nginx 配置中,图片扩展名 location 优先级高于 `/uploads`
- 导致 `.jpg/.jpeg` 文件在本地 `/var/www/html/` 查找,而不是代理到后端
- 所有藏品图片返回 404 错误
**根本原因**:
```nginx
# ❌ 错误配置(图片扩展名 location 优先级过高)
location /uploads {
proxy_pass http://backend:3000/uploads;
}
location ~* \.(jpg|jpeg|png)$ { # 这个优先级更高!
expires 1y;
}
```
**解决方案**:
- 调整 Nginx location 优先级,`/uploads` 移到图片扩展名 location 之前
- 图片扩展名 location 只处理字体文件woff、ttf 等)
- 前端静态图片使用 `/static/` 路径单独处理
**代码变更**:
```nginx
# ✅ 正确配置
# 1. 字体文件缓存(不影响图片)
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
expires 1y;
}
# 2. 图片上传文件代理(优先级最高)
location /uploads {
proxy_pass http://47.110.37.129:3000/uploads;
client_max_body_size 20M;
}
# 3. 前端静态图片(/static/ 目录)
location ~* ^/static/.*\.(png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
}
```
**影响范围**:
- ✅ 藏品详情图片显示
- ✅ 图片预览弹窗
- ✅ 图片切换功能
---
### 3. 后端图片数据加载修复 📊
**问题描述**:
- `get_collections()` API 函数中 `'images': []` 是硬编码的空数组
- 藏品列表 API 不返回图片数据,导致前端无法显示缩略图
**解决方案**:
- 在 `get_collections()` 函数中添加图片数据加载逻辑
- 查询 `collection_images` 表并返回图片信息
**代码变更**:
```python
# backend/app/routers/collections.py
# 修复前
'images': []
data_list.append(to_camel_case(item_dict))
# 修复后
'images': []
# 加载图片数据
from app.models.models import CollectionImage
images = db.query(CollectionImage).filter(
CollectionImage.collection_id == item.f99_90_id
).all()
for img in images:
item_dict['images'].append({
'id': img.id,
'filename': img.filename,
'original_name': img.original_name,
'path': img.path,
'created_at': img.created_at.isoformat() if img.created_at else None
})
data_list.append(to_camel_case(item_dict))
```
**影响范围**:
- ✅ 藏品列表 API
- ✅ 前端缩略图显示
- ✅ 所有依赖图片数据的页面
---
### 4. 前端图片路径修复 🔗
**问题描述**:
- 数据库中的 `path` 字段已包含 `uploads/` 前缀
- 前端代码又添加了 `/uploads/` 前缀,导致路径重复
- 最终 URL`/uploads/uploads/collections/xxx.jpg` (404 错误)
**解决方案**:
- 前端代码直接使用 `path` 字段,不添加额外前缀
**代码变更**:
```jsx
// frontend/src/pages/Detail.jsx
// 修复前
src={`/uploads/${img.path}`}
// 修复后
src={`/${img.path}`}
```
**影响范围**:
- ✅ 藏品详情页图片
- ✅ 图片预览弹窗
- ✅ 所有图片显示位置
---
### 5. 编辑页面图片预览修复 📝
**问题描述**:
- 编辑页面 `Edit.jsx` 中图片预览 URL 写死了错误的服务器地址
- 导致编辑页面无法显示图片预览
**解决方案**:
- 使用相对路径代替硬编码 URL
**代码变更**:
```jsx
// frontend/src/pages/Edit.jsx
// 修复前 (2 处)
preview: `http://120.26.133.10:3000/${img.path}`
// 修复后
preview: `/${img.path}`
```
**影响范围**:
- ✅ 编辑页面图片预览
- ✅ 图片上传后预览更新
---
## 📊 技术细节
### 图片访问流程
```
用户访问 http://8.149.137.26/uploads/collections/xxx.jpg
Nginx 接收请求(匹配 /uploads location
代理到 http://47.110.37.129:3000/uploads/collections/xxx.jpg
FastAPI 返回图片文件
用户看到图片 ✅
```
### 数据库存储
| 字段 | 示例值 |
|------|--------|
| `path` | `uploads/collections/admin-0001-J051963351.jpeg` |
| `filename` | `admin-0001-J051963351.jpeg` |
| `original_name` | `001.JPG` |
### 文件命名规则
**格式**: `用户名 - 藏品编号 - 冠字号。扩展名`
**示例**:
- `admin-0001-J051963351.jpeg`
- `admin-0002-J035161361.JPG`
---
## 📝 文件变更清单
### 前端文件
- ✅ `frontend/src/pages/Detail.jsx` - 图片路径和 onError 处理
- ✅ `frontend/src/pages/Home.jsx` - Logo 引用
- ✅ `frontend/src/pages/Login.jsx` - Logo 显示
- ✅ `frontend/package.json` - 版本号 1.0.1
### 后端文件
- ✅ `backend/app/routers/collections.py` - 图片数据加载
### 配置文件
- ✅ `config/VERSION` - 版本号 1.0.1
- ✅ `config/nginx.conf` - Nginx location 优先级调整
### 文档文件
- ✅ `docs/IMAGE_PROCESSING_FLOW.md` - 图片处理流程
- ✅ `docs/CLEANUP_REPORT.md` - 服务器清理报告
- ✅ `RELEASE_v1.0.1.md` - 本发布说明
---
## ✅ 测试验证
### 功能测试
| 测试项 | 状态 | 说明 |
|--------|------|------|
| Logo 显示 | ✅ 通过 | 仅在登录页、首页显示 |
| 藏品列表图片 | ✅ 通过 | 缩略图正常显示 |
| 藏品详情图片 | ✅ 通过 | 大图正常显示 |
| 图片预览弹窗 | ✅ 通过 | 点击可打开预览 |
| 图片切换 | ✅ 通过 | 左右按钮切换正常 |
| 无图片占位符 | ✅ 通过 | 显示"无图片"而非 Logo |
### API 测试
| 接口 | 状态 | 说明 |
|------|------|------|
| GET /api/collections | ✅ 200 | 返回图片数据 |
| GET /api/collections/:id | ✅ 200 | 返回图片详情 |
| POST /api/collections/upload-image | ✅ 200 | 图片上传正常 |
| GET /uploads/collections/xxx.jpg | ✅ 200 | 图片代理正常 |
---
## 🎯 升级建议
### 从 v1.0.0 升级
1. **拉取最新代码**
```bash
git pull origin main
```
2. **更新前端**
```bash
cd frontend
npm install
npm run build
```
3. **重启后端服务**
```bash
cd backend
pip install -r requirements.txt
pkill -f uvicorn
nohup python3 -m uvicorn app.main:app --port 3000 --host 0.0.0.0 &
```
4. **更新 Nginx 配置**
```bash
sudo cp config/nginx.conf /etc/nginx/conf.d/jiachenlong.conf
sudo nginx -s reload
```
---
## 📚 相关文档
- `docs/IMAGE_PROCESSING_FLOW.md` - 图片处理完整流程
- `static/images/LOGO_GUIDE.md` - Logo 使用规范
- `docs/CLEANUP_REPORT.md` - 服务器清理报告
---
## 🐛 已知问题
---
## 📞 技术支持
如有问题,请参考:
- 部署文档:`DEPLOYMENT_v1.0.0.md`
- 错误码文档:`ERROR_CODES.md`
- 后端服务指南:`BACKEND_SERVICE_GUIDE.md`
---
**甲辰藏品管理系统开发团队**
2026-03-16

View File

@ -1,121 +0,0 @@
# 代码优化测试报告
**测试时间**: 2026-03-16
**测试版本**: v1.0.0
**测试人**: 菜鸟小 D 🤖
---
## 📁 目录结构优化
**配置文件集中管理**:
- ✅ 创建 `config/` 目录
- ✅ 移动 `VERSION``config/`
- ✅ 移动 `docker-compose.yml``config/`
- ✅ 更新后端代码读取路径
- ✅ 更新前端代码读取路径
**文档集中管理**:
- ✅ 所有文档移动到 `docs/` 目录
- ✅ 根目录只保留代码和必要配置
---
## ✅ 测试结果
### 后端服务
| 测试项 | 结果 | 说明 |
|--------|------|------|
| Python 依赖检查 | ✅ 通过 | fastapi, sqlalchemy, uvicorn, bcrypt, jose |
| 代码导入测试 | ✅ 通过 | app.main 正常导入 |
| 服务启动测试 | ✅ 通过 | 端口 3001 启动成功 |
| 健康检查接口 | ✅ 通过 | `/health` 返回 `{"status":"healthy"}` |
| 版本信息接口 | ✅ 通过 | 返回 v2.8.0 |
### 前端服务
| 测试项 | 结果 | 说明 |
|--------|------|------|
| npm 依赖安装 | ✅ 通过 | 92 个包0 漏洞 |
| Vite 构建测试 | ✅ 通过 | 1.51s 构建完成 |
| 版本号读取 | ✅ 通过 | 从 VERSION 文件读取 v2.8.0 |
| 代码压缩 | ✅ 通过 | 282.81 kB → 82.19 kB (gzip) |
---
## 📁 目录结构优化
**配置文件集中管理**:
- ✅ 创建 `config/` 目录
- ✅ 移动 `VERSION``config/`
- ✅ 移动 `docker-compose.yml``config/`
- ✅ 更新后端代码读取路径
- ✅ 更新前端代码读取路径
**文档集中管理**:
- ✅ 所有文档移动到 `docs/` 目录
- ✅ 根目录只保留代码和必要配置
**静态资源集中管理**:
- ✅ 创建 `static/` 目录
- ✅ 子目录:`images/`, `icons/`, `fonts/`
- ✅ 移动 `logo.jpg``static/images/`
- ✅ 更新所有前端代码中的图片路径
- ✅ 创建各目录 README 说明文档
---
## 🧹 清理优化
### 后端清理
- ✅ 删除 `migrate_to_encoded_fields.sql` (迁移脚本)
- ✅ 删除 `start.sh` (旧启动脚本)
- ✅ 删除 `.env.example` (示例配置)
- ✅ 删除 `ocr_old.py` (旧 OCR 代码)
- ✅ 清理 `__pycache__/` (Python 缓存)
- ✅ 初始化 `uploads/` 目录
### 前端清理
- ✅ 删除 `assets/` (冗余目录)
- ✅ 删除 `title-gold.svg` (未使用文件)
- ✅ 删除 `pnpm-lock.yaml` (使用 npm)
- ✅ 删除 `dist/` (构建产物)
- ✅ 清理 `node_modules/` (重新安装)
### 文档优化
- ✅ 更新根目录 `README.md`
- ✅ 更新 `.gitignore`
- ✅ 创建 `backend/README.md`
- ✅ 创建 `frontend/README.md`
---
## 📊 代码统计
| 目录 | 文件数 | 大小 |
|------|--------|------|
| backend/ | ~20 | ~200KB |
| frontend/ | ~30 | ~100KB |
| static/ | 8 | ~110KB |
| config/ | 2 | ~1KB |
| docs/ | 6 | ~60KB |
| 根目录 | 5 | ~5KB |
| **总计** | **~71** | **~476KB** |
---
## ✅ 结论
**代码质量**: 优秀
**可运行性**: 完全正常
**文档完整性**: 良好
所有核心功能测试通过,代码已优化,可以正常部署使用。
---
**菜鸟小 D 测试报告** 🤖

View File

@ -1,205 +0,0 @@
# 甲辰藏品管理系统 v1.0.1 升级指南
**版本**: v1.0.1
**发布日期**: 2026-03-16
**前置版本**: v1.0.0
---
## 🎯 升级内容
### 主要修复
1. **Logo 显示规范化** - 仅在登录页、首页显示
2. **图片代理修复** - Nginx location 优先级调整
3. **后端图片数据加载** - 藏品列表 API 返回图片
4. **前端图片路径修复** - 避免路径重复
---
## 📋 升级步骤
### 方案一:完整升级(推荐)
#### 1. 备份当前版本
```bash
# 备份数据库
sudo -u postgres pg_dump zodiac > /backup/zodiac_v1.0.0.sql
# 备份代码
cp -r /opt/jiachenlong-backend /opt/jiachenlong-backend.backup
```
#### 2. 拉取最新代码
```bash
cd /opt/jiachenlong-backend
git pull origin master
git checkout v1.0.1
```
#### 3. 更新后端
```bash
# 安装依赖(如有更新)
pip3 install -r requirements.txt
# 重启后端服务
pkill -f 'python.*uvicorn'
sleep 2
export DATABASE_URL='postgresql://postgres:postgres@47.98.171.101:5432/zodiac'
nohup /usr/bin/python3.11 -m uvicorn app.main:app --port 3000 --host 0.0.0.0 > /tmp/uvicorn.log 2>&1 &
# 验证服务
sleep 5
curl http://localhost:3000/health
```
#### 4. 更新前端
```bash
# 构建前端
cd /path/to/jiachenlong/frontend
npm install
npm run build
# 部署到前端服务器
scp -r dist/* root@8.149.137.26:/var/www/html/
```
#### 5. 更新 Nginx 配置
```bash
# 复制新配置
scp config/nginx.conf root@8.149.137.26:/etc/nginx/conf.d/jiachenlong.conf
# 重启 Nginx
ssh root@8.149.137.26 "nginx -t && nginx -s reload"
```
#### 6. 验证升级
```bash
# 测试 Logo 显示
curl http://8.149.137.26/ | grep "甲辰收藏"
# 测试图片代理
curl -o /dev/null -w '%{http_code}' http://8.149.137.26/uploads/collections/admin-0001-J051963351.jpeg
# 测试 API
TOKEN=$(curl -s -X POST http://47.110.37.129:3000/api/auth/login -d 'username=admin&password=admin123' | grep -oP '"access_token":\s*"\K[^"]+')
curl -s -H "Authorization: Bearer $TOKEN" http://47.110.37.129:3000/api/collections?limit=1 | python3 -c "import sys,json; d=json.load(sys.stdin); print('图片数:', len(d.get('data',[{}])[0].get('images',[])))"
```
---
### 方案二:快速升级(仅修复图片问题)
#### 1. 仅更新 Nginx 配置
```bash
# 复制 Nginx 配置
scp config/nginx.conf root@8.149.137.26:/etc/nginx/conf.d/jiachenlong.conf
# 重启 Nginx
ssh root@8.149.137.26 "nginx -t && nginx -s reload"
```
#### 2. 仅更新后端代码
```bash
# 更新 collections.py
scp backend/app/routers/collections.py root@47.110.37.129:/opt/jiachenlong-backend/app/routers/
# 重启后端
ssh root@47.110.37.129 "pkill -f uvicorn && sleep 2 && export DATABASE_URL='postgresql://postgres:postgres@47.98.171.101:5432/zodiac' && cd /opt/jiachenlong-backend && nohup /usr/bin/python3.11 -m uvicorn app.main:app --port 3000 --host 0.0.0.0 > /tmp/uvicorn.log 2>&1 &"
```
#### 3. 仅更新前端代码
```bash
# 构建并部署
cd frontend
npm run build
scp -r dist/* root@8.149.137.26:/var/www/html/
```
---
## 🔍 验证清单
### Logo 显示
- [ ] 登录页面显示 Logo
- [ ] 首页显示 Logo
- [ ] 藏品详情页无图片时显示"无图片"
- [ ] Logo 不替代缺失的藏品图片
### 图片功能
- [ ] 藏品列表显示缩略图
- [ ] 藏品详情显示大图
- [ ] 图片预览弹窗正常
- [ ] 图片切换功能正常
- [ ] 后端图片代理正常HTTP 200
### API 接口
- [ ] GET /api/collections 返回图片数据
- [ ] GET /api/collections/:id 返回图片详情
- [ ] GET /uploads/collections/xxx.jpg 返回 200
---
## ⚠️ 注意事项
### 升级前
1. ✅ 备份数据库
2. ✅ 备份代码
3. ✅ 通知用户系统维护
### 升级中
1. ✅ 按顺序执行步骤
2. ✅ 每步验证成功再继续
3. ✅ 记录遇到的问题
### 升级后
1. ✅ 验证所有功能
2. ✅ 检查错误日志
3. ✅ 监控系统性能
---
## 🐛 回滚方案
### 回滚到 v1.0.0
```bash
# 回滚代码
cd /opt/jiachenlong-backend
git checkout v1.0.0
# 恢复 Nginx 配置
ssh root@8.149.137.26 "cp /etc/nginx/conf.d/jiachenlong.conf.backup /etc/nginx/conf.d/jiachenlong.conf && nginx -s reload"
# 重启服务
pkill -f uvicorn
cd /opt/jiachenlong-backend
nohup /usr/bin/python3.11 -m uvicorn app.main:app --port 3000 --host 0.0.0.0 &
```
---
## 📞 技术支持
- **代码仓库**: http://47.253.189.47:3000/coolbot/jiachenlong
- **版本标签**: v1.0.1
- **相关文档**:
- `docs/RELEASE_v1.0.1.md` - 发布说明
- `docs/IMAGE_PROCESSING_FLOW.md` - 图片处理流程
- `docs/DEPLOYMENT_COMPLETE_v1.0.1.md` - 部署报告
---
**升级完成!系统运行正常!** 🎉
**甲辰藏品管理系统开发团队**
2026-03-16

View File

@ -1,145 +0,0 @@
# v1.2.4 版本问题修复说明
**版本**: v1.2.4-stable
**发布日期**: 2026-03-20
**维护人**: 甲辰生产
---
## 问题列表及解决方案
### 问题1: OCR识别后保存藏品失败
**现象**: OCR识别成功后点击保存藏品返回404错误
**原因**:
- OCR代码只尝试小写扩展名(jpg/jpeg/png/gif)
- 用户上传的图片文件扩展名是大写的.JPG
**解决方案**:
修改 `backend/app/routers/ocr.py` 第299行
```python
# 修改前
temp_extensions = [jpg, jpeg, png, gif]
# 修改后
temp_extensions = [jpg, jpeg, png, gif, JPG, JPEG, PNG, GIF]
```
---
### 问题2: 用户协议页面乱码
**现象**: 点击用户协议显示乱码,只有一个标题
**原因**:
- 协议文件只有一个空的HTML骨架
- 没有实际内容
**解决方案**:
创建完整的用户协议HTML文件 `/var/www/frontend/user_agreement.html`,包含完整的中文协议内容(服务条款、用户责任、数据安全、免责声明等)
---
### 问题3: 用户协议页面无返回按钮
**现象**: 打开用户协议后无法返回
**解决方案**:
在用户协议页面添加返回按钮:
```html
<button class="back-btn" onclick="window.close()">← 返回</button>
```
---
### 问题4: 首页Logo显示不出来
**现象**: 登录页Logo正常登录后首页Logo显示404
**原因**:
- 部署时static目录没有复制到正确位置
- 浏览器缓存了旧的JS文件
**解决方案**:
1. 确保部署时复制static目录
```bash
cp -r /root/jiachenlong/static /var/www/frontend/
cp -r /root/jiachenlong/static /var/www/mobile/
```
2. 首页Logo添加版本号防止缓存
```jsx
// 修改Home.jsx
<img src="/static/images/jiachenlong-logo.png?v=1.2.4" ... />
```
---
### 问题5: 版本号显示重复
**现象**: 标题显示"vv1.2.4"v重复
**原因**:
- VERSION文件中版本号为"v1.2.4"带v前缀
- vite.config.js中又自动添加了"v"前缀
**解决方案**:
修改VERSION文件去掉v前缀
```
# 修改前
VERSION=v1.2.4
# 修改后
VERSION=1.2.4
```
---
## 部署检查清单
### 前端部署
```bash
# 1. 确保VERSION文件格式正确不带v前缀
cat config/VERSION
# 输出: VERSION=1.2.4
# 2. 构建
cd frontend
npm run build
# 3. 部署(两个目录都要部署)
rm -rf /var/www/mobile/*
rm -rf /var/www/frontend/*
cp -r dist/* /var/www/mobile/
cp -r dist/* /var/www/frontend/
cp -r ../static /var/www/mobile/
cp -r ../static /var/www/frontend/
# 4. 重载Nginx
nginx -s reload
```
### 后端部署
```bash
# 1. 拉取最新代码
cd /root/jiachenlong
git pull
# 2. 重启后端服务
pkill -f uvicorn
cd backend
python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 &
```
---
## 标签信息
- **当前稳定版本**: v1.2.4-stable
- **Gitea标签地址**: http://47.253.189.47:3000/coolbot/jiachenlong/tags
- **发布说明**: v1.2.4稳定版本 - 用户协议修复版
---
**文档结束**

View File

@ -1,297 +0,0 @@
# 甲辰藏品管理系统 - 标准部署流程
**版本**: v1.0
**创建时间**: 2026-03-21
**维护人**: 甲辰生产
---
## 📋 部署前检查清单
### 1. 获取信息
| 项目 | 内容 | 获取方式 |
|------|------|---------|
| 目标服务器IP | 如 8.149.137.26 | MEMORY.md |
| SSH密码 | 如 Jiachen123 | 询问酷博特 |
| 目标版本 | 如 v1.2.4 | Gitea tags |
| 数据库配置 | IP/密码/端口 | MEMORY.md |
### 2. 环境确认
```bash
# 登录目标服务器
ssh root@<目标IP>
# 检查已有配置(不要覆盖!)
cat /root/jiachenlong/config/VERSION
cat /etc/nginx/conf.d/*.conf
```
---
## 🚀 标准部署流程
### 前端部署(所有环境)
```bash
# 1. 登录服务器
ssh root@<前端IP>
# 2. 拉取代码(重要:不要覆盖已有目录)
cd /root
rm -rf jiachenlong_bak
mv jiachenlong jiachenlong_bak # 备份旧代码
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git jiachenlong
# 3. 检查并修改VERSION文件重要用sed保留原内容
# 先查看原内容
cat jiachenlong/config/VERSION
# 修改VERSION行保留其他行
sed -i 's/^VERSION=.*/VERSION=1.2.4/' jiachenlong/config/VERSION
# 4. 构建前端
cd jiachenlong/frontend
npm install
npm run build
# 5. 部署(两个目录都要部署!)
rm -rf /var/www/mobile/*
rm -rf /var/www/frontend/*
cp -r dist/* /var/www/mobile/
cp -r dist/* /var/www/frontend/
cp -r ../static /var/www/mobile/
cp -r ../static /var/www/frontend/
# 6. 部署用户协议(如有)
cp user_agreement.html /var/www/mobile/
cp user_agreement.html /var/www/frontend/
# 7. 重载Nginx
nginx -s reload
```
### 后端部署(所有环境)
```bash
# 1. 登录服务器
ssh root@<后端IP>
# 2. 拉取代码
cd /root
rm -rf jiachenlong_bak
mv jiachenlong jiachenlong_bak
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git jiachenlong
# 3. 检查OCR扩展名修复如无则手动修复
grep -n 'temp_extensions' jiachenlong/backend/app/routers/ocr.py
# 如只有小写,修复:
sed -i "s/temp_extensions = \['jpg', 'jpeg', 'png', 'gif'\]/temp_extensions = ['jpg', 'jpeg', 'png', 'gif', 'JPG', 'JPEG', 'PNG', 'GIF']/" jiachenlong/backend/app/routers/ocr.py
# 4. 检查数据库配置
cat jiachenlong/backend/.env | grep DATABASE_URL
# 5. 停止旧服务
pkill -f uvicorn
# 6. 启动新服务
cd jiachenlong/backend
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 > /tmp/uvicorn.log 2>&1 &
# 7. 等待启动
sleep 5
# 8. 验证
curl -s http://localhost:3000/ | head -c 100
```
---
## ✅ 部署后验证清单
### 必须验证的项目
| # | 验证项 | 命令 | 期望结果 |
|---|--------|------|---------|
| 1 | 前端页面 | curl http://<前端IP>/ | 200 + HTML |
| 2 | 前端版本 | curl http://<前端IP>/ \| grep title | v1.2.4 |
| 3 | 前端端口 | curl http://<前端IP>:3001/ | 200 |
| 4 | 后端健康 | curl http://<后端IP>:3000/ | 200 |
| 5 | 登录功能 | curl -X POST http://<后端IP>:3000/api/auth/login -d "username=admin&password=admin123" | 返回token |
| 6 | Logo图片 | curl -I http://<前端IP>/static/images/jiachenlong-logo.png | 200 |
| 7 | 用户协议 | curl http://<前端IP>/user_agreement.html | 200 + 内容 |
| 8 | 80端口 | curl -o /dev/null -w "%{http_code}" http://<前端IP>/ | 200 |
| 9 | API代理 | curl http://<前端IP>/api/collections | JSON响应 |
### 与基准环境对比
```bash
# 以C环境为基准对比关键文件
# C环境
curl -s http://47.103.29.111/ | grep title
# B环境
curl -s http://8.149.137.26/ | grep title
# 期望:版本号一致
```
---
## ⚠️ 常见错误及解决方案
### 1. git clone失败目录已存在
**错误**
```
fatal: destination path . already exists and is not an empty directory.
```
**解决**
```bash
# 方法1先备份再删除
mv jiachenlong jiachenlong_backup
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git
# 方法2删除后克隆
rm -rf jiachenlong
git clone http://47.253.189.47:3000/coolbot/jiachenlong.git
```
### 2. VERSION文件被覆盖
**错误**
```
VERSION=1.2.4
# 原有内容丢失
```
**解决**使用sed修改而非echo覆盖
```bash
# 错误方法
echo "VERSION=1.2.4" > VERSION # ❌ 会覆盖整个文件
# 正确方法
sed -i s/^VERSION=.*/VERSION=1.2.4/ VERSION # ✅ 只修改VERSION行
```
### 3. 版本号显示vv1.2.4
**原因**VERSION文件带v前缀 + vite.config.js又加v
**解决**
```bash
# VERSION文件不要带v
VERSION=1.2.4 # ✅
# 不是 VERSION=v1.2.4
# 源index.html如有vv先修复
sed -i s/vv/v/g index.html
```
### 4. 首页Logo显示404
**原因**static目录未部署
**解决**
```bash
# 部署时必须复制static目录
cp -r ../static /var/www/mobile/
cp -r ../static /var/www/frontend/
```
### 5. 浏览器缓存旧JS
**原因**JS文件名hash未变
**解决**首页Logo添加版本号
```jsx
// Home.jsx
<img src="/static/images/jiachenlong-logo.png?v=1.2.4" ... />
```
### 6. Nginx 80端口返回403
**原因**root目录为空或不存在
**解决**
```bash
# 检查目录
ls -la /var/www/frontend/
# 部署到正确目录
cp -r dist/* /var/www/frontend/
# 重载Nginx
nginx -s reload
```
---
## 📊 环境配置参考
### A环境生产
| 服务 | IP | 端口 |
|------|-----|------|
| 前端 | 8.154.46.3 | 80, 3001 |
| 后端 | 42.121.116.25 | 3000 |
| 数据库 | 47.98.171.101 | 5432 |
### B环境灰度
| 服务 | IP | 端口 |
|------|-----|------|
| 前端 | 8.149.137.26 | 80, 3001 |
| 后端 | 47.110.37.129 | 3000 |
| 数据库 | 47.96.181.36 | 5432 |
### C环境测试
| 服务 | IP | 端口 |
|------|-----|------|
| 前端 | 47.103.29.111 | 80 |
| 后端 | 47.103.9.192 | 3000 |
| 数据库 | 47.103.9.192 | 5432 |
---
## 📝 部署记录模板
每次部署后填写:
```markdown
## 部署记录
### 2026-03-21 v1.2.4
| 环境 | 部署时间 | 操作人 | 结果 |
|------|---------|--------|------|
| B环境 | 00:27 | 甲辰生产 | ✅ 成功 |
### 部署命令
```bash
# 前端
ssh root@8.149.137.26
cd /root/jiachenlong/frontend
npm run build
cp -r dist/* /var/www/mobile/
cp -r dist/* /var/www/frontend/
# 后端
ssh root@47.110.37.129
pkill -f uvicorn
cd /root/jiachenlong/backend
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 &
```
### 验证结果
- 前端版本v1.2.4 ✅
- 后端健康200 ✅
- Logo显示200 ✅
### 问题记录
```
---
**文档结束**

View File

@ -1,186 +0,0 @@
# 测试环境部署指南
## 测试环境架构
| 服务器 | IP | 服务 |
|--------|-----|------|
| 测试机1 | 47.103.29.111 | 前端 (Nginx) |
| 测试机2 | 47.103.9.192 | 后端 (FastAPI) + PostgreSQL |
## 部署步骤
### 1. 测试机1 - 前端部署
```bash
# 拉取代码
cd /root/jiachenlong
git fetch --all
git checkout v1.1.21
# 安装依赖并构建
cd frontend
npm install
npm run build
# 复制静态文件到可访问目录
mkdir -p /var/www/html
cp -r dist/* /var/www/html/
cp -r ../static /var/www/html/
chmod -R 755 /var/www/html
# 配置Nginx
cat > /etc/nginx/conf.d/jiachenlong-test.conf << 'EOF'
server {
listen 80;
server_name _;
root /var/www/html;
index index.html;
client_max_body_size 20M;
# 静态资源logo、图片等
location /static {
alias /var/www/html/static;
expires 30d;
add_header Cache-Control "public, immutable";
}
# SPA路由
location / {
try_files $uri $uri/ /index.html;
}
# API代理到后端
location /api {
proxy_pass http://47.103.9.192:3000/api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
EOF
nginx -t && systemctl enable nginx && systemctl restart nginx
```
### 2. 测试机2 - 后端部署
```bash
# 拉取代码
cd /root/jiachenlong
git fetch --all
git checkout v1.1.21
# 安装Python依赖
pip3 install fastapi uvicorn sqlalchemy psycopg2-binary pydantic python-jose bcrypt python-multipart pillow dashscope alibabacloud-dysmsapi20170525 oss2 email-validator httpx python-dotenv
# 创建环境变量文件
cat > backend/.env << 'EOF'
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/zodiac
SECRET_KEY=test-secret-key-for-sms
ACCESS_TOKEN_EXPIRE_MINUTES=60
OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG
OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1
OSS_BUCKET=jiachenlong-oss
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
SMS_ACCESS_KEY_ID=LTAI5tQAx5niD7JQVqGE5acE
SMS_ACCESS_KEY_SECRET=QsQFAEKBkaNynIoKyvdIi3BUyWVZu1
SMS_SIGN_NAME=苏州算力
SMS_TEMPLATE_CODE=SMS_501590956
EOF
# 启动后端
cd backend
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 > /tmp/backend.log 2>&1 &
# 复制静态文件
mkdir -p /var/www/html/static
cp -r ../static/* /var/www/html/static/
chmod -R 755 /var/www/html/static
# 配置Nginx
cat > /etc/nginx/conf.d/jiachenlong-test.conf << 'EOF'
server {
listen 80;
server_name _;
root /var/www/html;
index index.html;
client_max_body_size 20M;
location /static {
alias /var/www/html/static;
}
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0.1:3000/api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
EOF
nginx -t && systemctl enable nginx && systemctl restart nginx
```
## 常见问题
### 问题1: Logo不显示
**原因:**
1. Nginx未配置`/static`路径SPA路由捕获了请求
2. 静态文件在`/root`目录下nginx无权限读取
**解决:**
1. 添加`location /static`配置
2. 将静态文件复制到`/var/www/html/static`
### 问题2: 短信发送失败 "找不到模板"
**原因:** sms.py中的默认配置未更新
**解决:** 修改`backend/app/services/sms.py`:
```python
SMS_CONFIG = {
"access_key_id": os.getenv("SMS_ACCESS_KEY_ID", "LTAI5tQAx5niD7JQVqGE5acE"),
"access_key_secret": os.getenv("SMS_ACCESS_KEY_SECRET", "QsQFAEKBkaNynIoKyvdIi3BUyWVZu1"),
"sign_name": os.getenv("SMS_SIGN_NAME", "苏州算力"),
"template_code": os.getenv("SMS_TEMPLATE_CODE", "SMS_501590956"),
}
```
### 问题3: 数据库连接失败 "Ident authentication failed"
**原因:** PostgreSQL默认使用ident认证
**解决:**
```bash
sed -i 's/ident/trust/g' /var/lib/pgsql/data/pg_hba.conf
systemctl restart postgresql
```
## 验证命令
```bash
# 测试机1验证
curl -s -o /dev/null -w "%{http_code}" http://47.103.29.111/
curl -s -o /dev/null -w "%{http_code}" http://47.103.29.111/static/images/jiachenlong-logo.png
# 测试机2验证
curl -s http://47.103.9.192:3000/
curl -s -o /dev/null -w "%{http_code}" http://47.103.9.192/static/images/jiachenlong-logo.png
```
## 短信配置
| 项目 | 值 |
|------|-----|
| 模板Code | SMS_501590956 |
| 签名 | 苏州算力 |
| AccessKey ID | LTAI5tQAx5niD7JQVqGE5acE |
| AccessKey Secret | QsQFAEKBkaNynIoKyvdIi3BUyWVZu1 |

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +0,0 @@
# 部署检查清单
## 部署后必须检查
### 1. 环境变量检查
```bash
cat backend/.env
```
必须包含:
- ✅ DASHSCOPE_API_KEY不是sk-xxx
- ✅ SMS_ACCESS_KEY_ID
- ✅ SMS_SIGN_NAME
- ✅ SMS_TEMPLATE_CODE
### 2. 代码版本检查
```bash
git log --oneline -1
```
确认是目标版本
### 3. OCR扩展名检查
```bash
grep temp_extensions backend/app/routers/ocr.py
```
确认包含大小写扩展名 JPG JPEG PNG GIF
### 4. Python版本检查
```bash
python3 --version
```
必须是 3.11+
### 5. 服务启动检查
```bash
curl http://localhost:3000/
```
返回 200
### 6. API测试
```bash
# 登录
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=admin123" | python3 -c "import sys,json; print(json.load(sys.stdin).get(access_token,))")
# 测试验证码
curl -X POST http://localhost:3000/api/auth/send-verification-code \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"phone":"13800138000"}'
```
## 常见问题快速修复
| 问题 | 修复命令 |
|------|---------|
| OCR慢 | 检查DASHSCOPE_API_KEY是否正确 |
| 验证码失败 | 检查SMS_*配置是否完整 |
| 导入错误 | 使用python3.11启动 |
| 404错误 | 重启后端服务 |
### 7. 用户协议页面检查
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
确认返回HTML内容
### 8. 前端静态文件检查
确认用户协议文件存在

View File

@ -1,8 +0,0 @@
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

View File

@ -1 +0,0 @@
1.2.98

View File

@ -1,24 +1 @@
// 版本号配置文件 export const APP_VERSION = '1.2.101'
// ⚠️ 注意:版本号现在统一在根目录 VERSION 文件中管理
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
// 从环境变量读取vite.config.js 注入)
export const APP_VERSION = import.meta.env.APP_VERSION || '1.2.82'
// 版本信息
export const VERSION_INFO = {
version: APP_VERSION,
buildDate: new Date().toISOString().split('T')[0],
name: '甲辰收藏'
}
// 获取完整标题
export const getAppTitle = () => {
return `${VERSION_INFO.name} v${VERSION_INFO.version}`
}
export default {
APP_VERSION,
VERSION_INFO,
getAppTitle
}

View File

@ -1,9 +1,4 @@
/** // - AI /
* Add - 添加藏品页面
* Version: 1.2.90x
* 更新
*/
import React, { useState, useRef } from 'react' import React, { useState, useRef } from 'react'
import { APP_VERSION } from '../config/version' import { APP_VERSION } from '../config/version'
const API_BASE = localStorage.getItem('API_BASE') || '' const API_BASE = localStorage.getItem('API_BASE') || ''
@ -80,6 +75,7 @@ export default function Add() {
// //
const [dealForm, setDealForm] = useState({ const [dealForm, setDealForm] = useState({
serial: '', serial: '',
serialDigits: ['','','','','','','','','',''], // 10
category: '', category: '',
packaging: '标十', packaging: '标十',
price: '', price: '',
@ -735,9 +731,100 @@ export default function Add() {
<div> <div>
<div style={{ marginBottom: '12px' }}> <div style={{ marginBottom: '12px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>冠字号 *</div> <div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>冠字号 *</div>
<input value={dealForm.serial} onChange={(e) => setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})}
placeholder="J0xxxxxxxx" {/* 冠字号每位单独输入框 - 共10位J0可编辑后面8位数字 */}
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} /> <div style={{ display: 'flex', gap: '4px', justifyContent: 'center' }}>
{/* 第1位: J - 可编辑 */}
<input value={dealForm.serialDigits[0] || 'J'}
onChange={(e) => {
const val = e.target.value.replace(/[^Jj]/g, '').slice(-1).toUpperCase()
const newDigits = [...dealForm.serialDigits]
newDigits[0] = val || 'J'
const fullSerial = (newDigits[0] || 'J') + (newDigits[1] || '0') + newDigits.slice(2).join('')
setDealForm({...dealForm, serialDigits: newDigits, serial: fullSerial, category: autoCategory(fullSerial)})
}}
maxLength={1}
style={{
width: '32px', height: '44px', textAlign: 'center',
background: 'rgba(251,191,36,0.2)', border: '1px solid rgba(251,191,36,0.3)',
borderRadius: '6px', color: '#fbbf24', fontSize: '16px', fontWeight: 'bold'
}}
/>
{/* 第2位: 0 - 可编辑 */}
<input value={dealForm.serialDigits[1] || '0'}
onChange={(e) => {
const val = e.target.value.replace(/[^0]/g, '').slice(-1)
const newDigits = [...dealForm.serialDigits]
newDigits[1] = val || '0'
const fullSerial = (newDigits[0] || 'J') + (newDigits[1] || '0') + newDigits.slice(2).join('')
setDealForm({...dealForm, serialDigits: newDigits, serial: fullSerial, category: autoCategory(fullSerial)})
}}
maxLength={1}
style={{
width: '32px', height: '44px', textAlign: 'center',
background: 'rgba(251,191,36,0.2)', border: '1px solid rgba(251,191,36,0.3)',
borderRadius: '6px', color: '#fbbf24', fontSize: '16px', fontWeight: 'bold'
}}
/>
{/* 后面8位数字输入 - 带自动跳转 */}
{dealForm.serialDigits.slice(2).map((digit, idx) => {
const realIdx = idx + 2 // (2-9)
return (
<input key={realIdx} value={digit}
data-real-idx={realIdx}
onChange={(e) => {
const val = e.target.value.replace(/\D/g, '').slice(-1)
const newDigits = [...dealForm.serialDigits]
newDigits[realIdx] = val
const fullSerial = newDigits[0] + newDigits[1] + newDigits.slice(2).join('')
setDealForm({
...dealForm,
serialDigits: newDigits,
serial: fullSerial,
category: autoCategory(fullSerial)
})
// +1
if (val && realIdx < 9) {
setTimeout(() => {
const inputs = document.querySelectorAll('[data-real-idx]')
const nextInput = inputs[idx + 1] // idxrealIdx
if (nextInput) nextInput.focus()
}, 10)
}
}}
onKeyDown={(e) => {
// 退
if (e.key === 'Backspace' && !e.target.value && realIdx > 2) {
setTimeout(() => {
const inputs = document.querySelectorAll('[data-real-idx]')
const prevInput = inputs[idx - 1]
if (prevInput) prevInput.focus()
}, 10)
}
}}
maxLength={1}
placeholder="×"
style={{
width: '32px', height: '44px', textAlign: 'center',
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '6px', color: '#fff', fontSize: '16px', fontWeight: 'bold'
}}
/>
)
})}
</div>
{/* 快捷清除按钮 */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '8px' }}>
<button type="button" onClick={() => setDealForm({...dealForm, serial: '', serialDigits: ['','','','','','','','','',''], category: ''})}
style={{ padding: '4px 12px', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '4px', color: '#94a3b8', fontSize: '12px', cursor: 'pointer' }}>
清空
</button>
<div style={{ color: '#64748b', fontSize: '12px' }}>
已输入: {dealForm.serialDigits.filter(d => d).length}/8
</div>
</div>
</div> </div>
{dealForm.category && ( {dealForm.category && (
@ -746,19 +833,8 @@ export default function Add() {
<div style={{ fontSize: '14px', color: '#fbbf24', fontWeight: 'bold' }}>{dealForm.category}</div> <div style={{ fontSize: '14px', color: '#fbbf24', fontWeight: 'bold' }}>{dealForm.category}</div>
</div> </div>
)} )}
<div style={{ marginBottom: '12px' }}> {/* 成交价格 - 调整到冠字号下面 */}
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>包装类型</div>
<div style={{ display: 'flex', gap: '8px' }}>
{['单张', '标十', '标百'].map(p => (
<button key={p} onClick={() => setDealForm({...dealForm, packaging: p})}
style={{ flex: 1, padding: '10px', background: dealForm.packaging === p ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealForm.packaging === p ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
{p}
</button>
))}
</div>
</div>
<div style={{ marginBottom: '12px' }}> <div style={{ marginBottom: '12px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交价格 *</div> <div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交价格 *</div>
<input type="number" value={dealForm.price} onChange={(e) => setDealForm({...dealForm, price: e.target.value})} <input type="number" value={dealForm.price} onChange={(e) => setDealForm({...dealForm, price: e.target.value})}
@ -766,20 +842,34 @@ export default function Add() {
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} /> style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
</div> </div>
<div style={{ marginBottom: '12px' }}> {/* 成交价格和成交平台同一行 */}
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交平台 *</div> <div style={{ display: 'flex', gap: '12px', marginBottom: '12px' }}>
<select value={dealForm.platform} onChange={(e) => setDealForm({...dealForm, platform: e.target.value})} <div style={{ flex: 1 }}>
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }}> <div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>包装类型</div>
<option value="淘宝">淘宝</option> <div style={{ display: 'flex', gap: '4px' }}>
<option value="咸鱼">咸鱼</option> {['单张', '标十', '标百'].map(p => (
<option value="抖音">抖音</option> <button key={p} onClick={() => setDealForm({...dealForm, packaging: p})}
<option value="快手">快手</option> style={{ flex: 1, padding: '10px 4px', background: dealForm.packaging === p ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealForm.packaging === p ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '12px', fontWeight: '600', cursor: 'pointer' }}>
<option value="微拍堂">微拍堂</option> {p}
<option value="拼多多">拼多多</option> </button>
<option value="一尘">一尘</option> ))}
<option value="爱藏">爱藏</option> </div>
<option value="其他">其他</option> </div>
</select> <div style={{ flex: 1 }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交平台 *</div>
<select value={dealForm.platform} onChange={(e) => setDealForm({...dealForm, platform: e.target.value})}
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }}>
<option value="淘宝">淘宝</option>
<option value="咸鱼">咸鱼</option>
<option value="抖音">抖音</option>
<option value="快手">快手</option>
<option value="微拍堂">微拍堂</option>
<option value="拼多多">拼多多</option>
<option value="一尘">一尘</option>
<option value="爱藏">爱藏</option>
<option value="其他">其他</option>
</select>
</div>
</div> </div>
<div style={{ display: 'flex', gap: '12px', marginBottom: '12px' }}> <div style={{ display: 'flex', gap: '12px', marginBottom: '12px' }}>
@ -868,7 +958,7 @@ export default function Add() {
}) })
if (response.ok) { if (response.ok) {
alert('行情录入成功!') alert('行情录入成功!')
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '抖音', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] }) setDealForm({ serial: '', serialDigits: ['','','','','','','','','',''], category: '', packaging: '标十', price: '', platform: '抖音', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
} else { } else {
const data = await response.json() const data = await response.json()
alert('录入失败: ' + (data.detail || '未知错误')) alert('录入失败: ' + (data.detail || '未知错误'))

View File

@ -1,9 +1,3 @@
/**
* Admin - 管理员用户管理页面
* Version: 1.2.98 (2026-04-19)
* 更新新增 dealCount 字段显示用户发布的行情数量
*/
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version' import { APP_VERSION } from '../config/version'
@ -227,8 +221,11 @@ export default function Admin() {
</div> </div>
</div> </div>
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
<div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数</div> <div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数 | 行情数</div>
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collectionCount || 0}</div> <div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', alignItems: 'center' }}>
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/list?userId=' + user.id} title='点击查看藏品'>{user.collectionCount || 0}</div>
<div style={{ color: '#22c55e', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/news?userId=' + user.id} title='点击查看行情'>{user.dealCount || 0}</div>
</div>
</div> </div>
</div> </div>
{/* 更多字段 */} {/* 更多字段 */}
@ -240,7 +237,7 @@ export default function Admin() {
🤖 AI识别: {user.aiCount || 0} 🤖 AI识别: {user.aiCount || 0}
</div> </div>
<div style={{ background: 'rgba(20, 184, 166, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#14b8a6' }}> <div style={{ background: 'rgba(20, 184, 166, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#14b8a6' }}>
📈 行情: {user.dealCount || 0} 🎯 配号: {user.searchCount || 0}
</div> </div>
<div style={{ background: 'rgba(245, 158, 11, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#f59e0b' }}> <div style={{ background: 'rgba(245, 158, 11, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#f59e0b' }}>
🔍 寻号: {user.searchCount || 0} 🔍 寻号: {user.searchCount || 0}

View File

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

View File

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

View File

@ -1,9 +1,3 @@
/**
* Home - 首页
* Version: 1.2.95x
* 更新
*/
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version' import { APP_VERSION } from '../config/version'
@ -15,6 +9,8 @@ export default function Home() {
const [seekStats, setSeekStats] = useState({ seekCount: 0, matchedCount: 0 }) const [seekStats, setSeekStats] = useState({ seekCount: 0, matchedCount: 0 })
const [recentPosts, setRecentPosts] = useState([]) const [recentPosts, setRecentPosts] = useState([])
const [dragonStats, setDragonStats] = useState({}) const [dragonStats, setDragonStats] = useState({})
const [dealVersion, setDealVersion] = useState('龙钞')
const [dealCategoryStats, setDealCategoryStats] = useState([])
const currentPath = window.location.hash.slice(1) || '/' const currentPath = window.location.hash.slice(1) || '/'
useEffect(() => { useEffect(() => {
@ -84,8 +80,15 @@ export default function Home() {
}).catch(() => {}) }).catch(() => {})
// //
fetch('/api/information/seek/stats').then(res => res.json()).then(data => { fetch('/api/seek/stats').then(res => res.json()).then(data => {
setSeekStats(data || {}) setSeekStats({ seekCount: data.total || 0, userMatchedCount: data.matched || 0, totalMatchedCount: data.unmatched || 0 })
}).catch(() => {})
//
const token = localStorage.getItem('token')
const headers = token ? { 'Authorization': 'Bearer ' + token } : {}
fetch('/api/deal/category-stats?version=龙钞', { headers }).then(res => res.json()).then(data => {
setDealCategoryStats(data.data || [])
}).catch(() => {}) }).catch(() => {})
}, []) }, [])
@ -178,7 +181,7 @@ export default function Home() {
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>快捷操作</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>快捷操作</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
<div onClick={() => window.location.hash = '#/add?mode=ocr'} style={{ <div onClick={() => window.location.hash = '#/add?mode=ocr'} style={{
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)', background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
cursor: 'pointer', cursor: 'pointer',
@ -192,7 +195,7 @@ export default function Home() {
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>AI识别添加</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>AI识别添加</div>
</div> </div>
<div onClick={() => window.location.hash = '#/add?mode=deal'} style={{ <div onClick={() => window.location.hash = '#/add?mode=deal'} style={{
background: 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)', background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
cursor: 'pointer', cursor: 'pointer',
@ -206,7 +209,7 @@ export default function Home() {
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>录入成交行情</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>录入成交行情</div>
</div> </div>
<div onClick={() => window.location.hash = '#/news?type=seek'} style={{ <div onClick={() => window.location.hash = '#/news?type=seek'} style={{
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
cursor: 'pointer', cursor: 'pointer',
@ -220,7 +223,7 @@ export default function Home() {
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div>
</div> </div>
<div onClick={() => window.location.hash = '#/add?mode=manual'} style={{ <div onClick={() => window.location.hash = '#/add?mode=manual'} style={{
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 16px', padding: '12px 16px',
cursor: 'pointer', cursor: 'pointer',
@ -264,31 +267,103 @@ export default function Home() {
</div> </div>
</div> </div>
{/* 成交行情信息 */}
<div style={{ marginBottom: '20px' }}>
{(() => {
const versions = ['龙钞', '马钞', '蛇钞', '其他']
const packagings = ['标百', '标十', '单张']
return (
<div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>💰 龙钞成交数据分类汇总均价</div>
<div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '16px', border: '1px solid rgba(255,255,255,0.08)' }}>
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}>
{versions.map(v => (
<button key={v} onClick={() => {
setDealVersion(v)
//
const token = localStorage.getItem('token')
const headers = token ? { 'Authorization': 'Bearer ' + token } : {}
fetch(`/api/deal/category-stats?version=${v}`, { headers }).then(res => res.json()).then(data => {
setDealCategoryStats(data.data || [])
}).catch(() => {})
}}
style={{ padding: '6px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
background: dealVersion === v ? '#3b82f6' : 'rgba(255,255,255,0.1)',
color: '#fff', fontSize: '13px', fontWeight: dealVersion === v ? '600' : '400' }}>
{v}
</button>
))}
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr>
<th style={{ padding: '8px', textAlign: 'left', color: 'rgba(255,255,255,0.5)', borderBottom: '1px solid rgba(255,255,255,0.1)' }}></th>
{packagings.map(p => (
<th key={p} style={{ padding: '8px', textAlign: 'center', color: 'rgba(255,255,255,0.5)', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>{p}</th>
))}
</tr>
</thead>
<tbody>
{dealCategoryStats.length === 0 ? (
<tr>
<td colSpan={4} style={{ padding: '20px', textAlign: 'center', color: 'rgba(255,255,255,0.3)' }}>暂无数据</td>
</tr>
) : (
dealCategoryStats.map(row => (
<tr key={row.category}>
<td style={{ padding: '8px', color: '#fbbf24', fontWeight: '500', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{row.category}</td>
{packagings.map(pkg => (
<td key={pkg} style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
{row[pkg] ? (
<div style={{ color: '#22c55e', fontWeight: '600' }}>
¥{row[pkg].avg.toLocaleString()}
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '11px', marginLeft: '4px' }}>({row[pkg].count})</span>
</div>
) : <span style={{ color: 'rgba(255,255,255,0.2)' }}>-</span>}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
)
})()}
</div>
{/* 一尘今日数据 */} {/* 一尘今日数据 */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>📊 一尘今日连体纪念钞数据</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>📊 一尘今日连体纪念钞数据</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
<div style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(59,130,246,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(59,130,246,0.2)', textAlign: 'center' }}> <div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(255,255,255,0.08)', textAlign: 'center' }}>
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.total || 0}</div> <div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.total || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>总帖子</div> <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>总帖子</div>
</div> </div>
<div style={{ background: 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(16,185,129,0.2)', textAlign: 'center' }}> <div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(255,255,255,0.08)', textAlign: 'center' }}>
<div style={{ color: '#34d399', fontSize: '20px', fontWeight: '700' }}>{yichensStats.deals || 0}</div> <div style={{ color: '#34d399', fontSize: '20px', fontWeight: '700' }}>{yichensStats.deals || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>出售</div> <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>出售</div>
</div> </div>
<div style={{ background: 'linear-gradient(135deg, rgba(245,158,11,0.15) 0%, rgba(245,158,11,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(245,158,11,0.2)', textAlign: 'center' }}> <div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(255,255,255,0.08)', textAlign: 'center' }}>
<div style={{ color: '#fbbf24', fontSize: '20px', fontWeight: '700' }}>{yichensStats.wants || 0}</div> <div style={{ color: '#fbbf24', fontSize: '20px', fontWeight: '700' }}>{yichensStats.wants || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>求购</div> <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>求购</div>
</div> </div>
<div style={{ background: 'linear-gradient(135deg, rgba(139,92,246,0.15) 0%, rgba(139,92,246,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(139,92,246,0.2)', textAlign: 'center' }}> <div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(255,255,255,0.08)', textAlign: 'center' }}>
<div style={{ color: '#a78bfa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.dragons || 0}</div> <div style={{ color: '#a78bfa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.dragons || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>龙钞</div> <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>龙钞</div>
</div> </div>
<div style={{ background: 'linear-gradient(135deg, rgba(236,72,153,0.15) 0%, rgba(236,72,153,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(236,72,153,0.2)', textAlign: 'center' }}> <div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(255,255,255,0.08)', textAlign: 'center' }}>
<div style={{ color: '#f472b6', fontSize: '20px', fontWeight: '700' }}>{yichensStats.horses || 0}</div> <div style={{ color: '#f472b6', fontSize: '20px', fontWeight: '700' }}>{yichensStats.horses || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>马钞</div> <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>马钞</div>
</div> </div>
<div style={{ background: 'linear-gradient(135deg, rgba(20,184,166,0.15) 0%, rgba(20,184,166,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(20,184,166,0.2)', textAlign: 'center' }}> <div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(255,255,255,0.08)', textAlign: 'center' }}>
<div style={{ color: '#2dd4bf', fontSize: '20px', fontWeight: '700' }}>{yichensStats.tianma || 0}</div> <div style={{ color: '#2dd4bf', fontSize: '20px', fontWeight: '700' }}>{yichensStats.tianma || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>天马</div> <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>天马</div>
</div> </div>
@ -298,43 +373,43 @@ export default function Home() {
{/* 今日龙钞帖子数据统计 */} {/* 今日龙钞帖子数据统计 */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>🐉 今日龙钞帖子数据统计</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>🐉 今日龙钞帖子数据统计</div>
<div style={{ background: 'linear-gradient(180deg, rgba(99,102,241,0.15) 0%, rgba(99,102,241,0.05) 100%)', borderRadius: '12px', padding: '16px', border: '1px solid rgba(99,102,241,0.2)' }}> <div style={{ background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', borderRadius: '12px', padding: '16px', border: '1px solid rgba(255,255,255,0.08)' }}>
{/* 表头 */} {/* 表头 */}
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '8px' }}> <div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '8px' }}>
<div></div> <div></div>
<div style={{ color: '#22c55e', fontSize: '14px', fontWeight: '600', textAlign: 'center' }}>出售</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', textAlign: 'center' }}>出售</div>
<div style={{ color: '#f97316', fontSize: '14px', fontWeight: '600', textAlign: 'center' }}>求购</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', textAlign: 'center' }}>求购</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontWeight: '600', textAlign: 'center' }}>合计</div> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontWeight: '600', textAlign: 'center' }}>合计</div>
</div> </div>
{/* 数据行 */} {/* 数据行 */}
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}> <div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}>
<div style={{ color: '#ef4444', fontSize: '14px', fontWeight: '500' }}>带4</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '500' }}>带4</div>
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.dai4?.deals || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.dai4?.deals || 0}</div>
<div style={{ color: '#f97316', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.dai4?.wants || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.dai4?.wants || 0}</div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.dai4?.deals || 0) + (dragonStats.dai4?.wants || 0)}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.dai4?.deals || 0) + (dragonStats.dai4?.wants || 0)}</div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}> <div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}>
<div style={{ color: '#06b6d4', fontSize: '14px', fontWeight: '500' }}>带7号</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '500' }}>带7号</div>
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu4?.deals || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu4?.deals || 0}</div>
<div style={{ color: '#f97316', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu4?.wants || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu4?.wants || 0}</div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu4?.deals || 0) + (dragonStats.wu4?.wants || 0)}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu4?.deals || 0) + (dragonStats.wu4?.wants || 0)}</div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}> <div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}>
<div style={{ color: '#3b82f6', fontSize: '14px', fontWeight: '500' }}>无47</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '500' }}>无47</div>
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu47?.deals || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu47?.deals || 0}</div>
<div style={{ color: '#f97316', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu47?.wants || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu47?.wants || 0}</div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu47?.deals || 0) + (dragonStats.wu47?.wants || 0)}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu47?.deals || 0) + (dragonStats.wu47?.wants || 0)}</div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}> <div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}>
<div style={{ color: '#a855f7', fontSize: '14px', fontWeight: '500' }}>无247</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '500' }}>无247</div>
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu247?.deals || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu247?.deals || 0}</div>
<div style={{ color: '#f97316', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu247?.wants || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu247?.wants || 0}</div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu247?.deals || 0) + (dragonStats.wu247?.wants || 0)}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu247?.deals || 0) + (dragonStats.wu247?.wants || 0)}</div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px' }}> <div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px' }}>
<div style={{ color: '#ec4899', fontSize: '14px', fontWeight: '500' }}>无347</div> <div style={{ color: '#fff', fontSize: '14px', fontWeight: '500' }}>无347</div>
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu347?.deals || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu347?.deals || 0}</div>
<div style={{ color: '#f97316', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu347?.wants || 0}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu347?.wants || 0}</div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu347?.deals || 0) + (dragonStats.wu347?.wants || 0)}</div> <div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu347?.deals || 0) + (dragonStats.wu347?.wants || 0)}</div>
</div> </div>
</div> </div>

View File

@ -1,9 +1,3 @@
/**
* List - 藏品列表页面
* Version: 1.2.93x
* 更新
*/
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version' import { APP_VERSION } from '../config/version'
const API_BASE = localStorage.getItem('API_BASE') || '' const API_BASE = localStorage.getItem('API_BASE') || ''

View File

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

View File

@ -1,9 +1,3 @@
/**
* News - 资讯列表页面
* Version: 1.2.80x
* 更新
*/
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import YichensBoard from './YichensBoard' import YichensBoard from './YichensBoard'
@ -53,6 +47,10 @@ export default function News() {
const [expandedItems, setExpandedItems] = useState({}) // const [expandedItems, setExpandedItems] = useState({}) //
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [urlUserId, setUrlUserId] = useState(() => {
const params = new URLSearchParams(window.location.hash.split("?")[1] || "")
return params.get("userId") || null
})
const [totalPages, setTotalPages] = useState(1) const [totalPages, setTotalPages] = useState(1)
const API_BASE = localStorage.getItem('API_BASE') || '' const API_BASE = localStorage.getItem('API_BASE') || ''
@ -85,16 +83,17 @@ export default function News() {
const headers = token ? { Authorization: `Bearer ${token}` } : {} const headers = token ? { Authorization: `Bearer ${token}` } : {}
// URL // URL
// 使APIseekdeal // 使 information/list
let url = activeTab === 'yichen' let url = activeTab === 'yichen'
? `${API_BASE}/api/information/list?info_type=${activeTab}` ? `${API_BASE}/api/information/list?info_type=${activeTab}`
: activeTab === 'seek' : activeTab === 'seek'
? `${API_BASE}/api/seek/list` ? `${API_BASE}/api/information/list?info_type=seek`
: `${API_BASE}/api/deal/list` : `${API_BASE}/api/deal/list`
// 500 // 100100
if (activeTab === 'deal') { if (activeTab === 'deal') {
url += (url.includes('?') ? '&' : '?') + 'page_size=500' url += (url.includes('?') ? '&' : '?') + 'page_size=500'
if (urlUserId) { url += "&user_id=" + urlUserId }
if (dealDate) { if (dealDate) {
url += '&deal_date=' + dealDate url += '&deal_date=' + dealDate
} }
@ -296,7 +295,7 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const res = await fetch(`${API_BASE}/api/seek/list`, { const res = await fetch(`${API_BASE}/api/information/seek/list`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}) })
const data = await res.json() const data = await res.json()
@ -312,7 +311,7 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const res = await fetch(`${API_BASE}/api/seek/list`, { const res = await fetch(`${API_BASE}/api/information/seek/list`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}) })
const data = await res.json() const data = await res.json()
@ -498,7 +497,7 @@ export default function News() {
</div> </div>
)} )}
<h3 style={{ color: '#fff', marginBottom: '16px' }}> <h3 style={{ color: '#fff', marginBottom: '16px' }}>
{activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? `成交行情信息 (${dealDate})` : '一尘看板'} {activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? `成交行情信息 ${dealDate}` : '一尘看板'}
{activeTab === 'seek' && ( {activeTab === 'seek' && (
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}> <div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
<button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button> <button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button>

View File

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

View File

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

View File

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

View File

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

View File

@ -20,8 +20,40 @@ class ApiError extends Error {
} }
} }
// 获取 Token // 获取 Token - 优先从Cookie读取兼容localStorage
const getToken = () => localStorage.getItem('token') const getToken = () => {
// 先尝试从Cookie获取
const cookies = document.cookie.split(';')
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=')
if (name === 'token') {
return value
}
}
// 兼容再从localStorage获取
return localStorage.getItem('token')
}
// 设置 Token - 同时设置Cookie和localStorage
const setToken = (token) => {
if (token) {
// 设置Cookie7天有效期
const expires = new Date()
expires.setDate(expires.getDate() + 7)
document.cookie = `token=${token};expires=${expires.toUTCString()};path=/;samesite=lax`
// 同时存localStorage兼容原有逻辑
localStorage.setItem('token', token)
}
}
// 清除 Token
const removeToken = () => {
// 清除Cookie
document.cookie = 'token=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'
// 清除localStorage
localStorage.removeItem('token')
localStorage.removeItem('user')
}
// 统一请求方法 // 统一请求方法
async function request(endpoint, options = {}) { async function request(endpoint, options = {}) {
@ -51,8 +83,7 @@ async function request(endpoint, options = {}) {
if (!response.ok) { if (!response.ok) {
// 处理 401 未授权 // 处理 401 未授权
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('token') removeToken()
localStorage.removeItem('user')
window.location.hash = '#/login' window.location.hash = '#/login'
throw new ApiError( throw new ApiError(
data.detail || data.message || '登录已过期,请重新登录', data.detail || data.message || '登录已过期,请重新登录',
@ -105,6 +136,7 @@ export const api = {
const response = await fetch(`${API_BASE}/api/auth/login`, { const response = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST', method: 'POST',
credentials: 'include', // 包含Cookie
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString() body: params.toString()
}) })
@ -121,6 +153,11 @@ export const api = {
throw error throw error
} }
// 登录成功保存Token同时存Cookie和localStorage
if (data.access_token) {
setToken(data.access_token)
}
return data return data
}, },

View File

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

View File

@ -1,39 +0,0 @@
#!/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 "========================================="

View File

@ -1,130 +0,0 @@
#!/bin/bash
# 甲辰藏品管理系统 v1.0.0 - 部署脚本
# 使用方式:./deploy.sh [版本号] [环境]
# 示例:./deploy.sh 1.0.0 production
set -e
VERSION=${1:-1.0.0}
ENV=${2:-test}
log_info() { echo "[INFO] $1"; }
log_error() { echo "[ERROR] $1" && exit 1; }
log_info "=== 甲辰藏品管理系统 v${VERSION} 部署开始 ==="
log_info "目标环境:$ENV"
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
# 1. 备份当前版本
log_info "[1/6] 备份当前版本..."
BACKUP_DIR="$PROJECT_ROOT/backups/v$VERSION-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
cp -r backend "$BACKUP_DIR/" 2>/dev/null || true
cp -r frontend "$BACKUP_DIR/" 2>/dev/null || true
cp -r static "$BACKUP_DIR/" 2>/dev/null || true
log_info "备份完成:$BACKUP_DIR"
# 2. Git 提交(如果有 Git 仓库)
log_info "[2/6] Git 提交..."
if [ -d ".git" ]; then
git add -A
git commit -m "release(v$VERSION): 部署新版本" 2>/dev/null || log_info "无更改需要提交"
git tag "v$VERSION" 2>/dev/null || true
log_info "Git 操作完成"
else
log_info "非 Git 仓库,跳过"
fi
# 3. 构建前端
log_info "[3/6] 构建前端..."
cd "$PROJECT_ROOT/frontend"
rm -rf dist
npm install
npm run build
log_info "前端构建完成"
# 检查 Logo 文件
log_info "[3.5/6] 检查 Logo 资源..."
if [ ! -f "$PROJECT_ROOT/static/images/jiachenlong-logo.png" ]; then
log_error "Logo 文件不存在static/images/jiachenlong-logo.png"
fi
log_info "Logo 文件确认jiachenlong-logo.png"
# 4. 安装后端依赖
log_info "[4/6] 安装后端依赖..."
cd "$PROJECT_ROOT/backend"
pip3 install -r requirements.txt
log_info "后端依赖安装完成"
# 5. 部署(根据环境选择)
log_info "[5/6] 部署到服务器..."
if [ "$ENV" == "local" ]; then
# 本地部署
DEPLOY_DIR="/var/www/jiachenlong"
mkdir -p "$DEPLOY_DIR/frontend"
cp -r "$PROJECT_ROOT/frontend/dist/"* "$DEPLOY_DIR/frontend/"
log_info "本地部署完成:$DEPLOY_DIR"
elif [ "$ENV" == "test" ]; then
# 测试服务器
SERVER="root@120.26.133.10"
DEST_DIR="/var/www/mobile"
# 部署前端构建文件
cd "$PROJECT_ROOT/frontend/dist"
tar -czf - . | ssh -o StrictHostKeyChecking=no "$SERVER" "cd $DEST_DIR && rm -rf dist/* && tar -xzf -"
# 部署静态资源(包含 Logo
ssh -o StrictHostKeyChecking=no "$SERVER" "mkdir -p $DEST_DIR/static/images"
scp -o StrictHostKeyChecking=no "$PROJECT_ROOT/static/images/jiachenlong-logo.png" "$SERVER:$DEST_DIR/static/images/"
ssh -o StrictHostKeyChecking=no "$SERVER" "nginx -s reload"
log_info "测试环境部署完成http://120.26.133.10/"
elif [ "$ENV" == "production" ]; then
# 生产服务器 A (WebA: 8.154.46.3)
SERVER="root@8.154.46.3"
DEPLOY_DIR="/var/www/frontend"
# 部署前端构建文件
cd "$PROJECT_ROOT/frontend/dist"
tar -czf - . | ssh -o StrictHostKeyChecking=no "$SERVER" "cd $DEPLOY_DIR && rm -rf * && tar -xzf -"
# 部署静态资源(包含 Logo
ssh -o StrictHostKeyChecking=no "$SERVER" "mkdir -p $DEPLOY_DIR/static/images"
scp -o StrictHostKeyChecking=no "$PROJECT_ROOT/static/images/jiachenlong-logo.png" "$SERVER:$DEPLOY_DIR/static/images/"
ssh -o StrictHostKeyChecking=no "$SERVER" "nginx -s reload"
log_info "生产环境A部署完成http://8.154.46.3/"
scp -o StrictHostKeyChecking=no "$PROJECT_ROOT/static/images/jiachenlong-logo.png" "$SERVER:$DEST_DIR/static/images/"
ssh -o StrictHostKeyChecking=no "$SERVER" "nginx -s reload"
log_info "生产环境部署完成http://$SERVER/"
else
log_error "未知环境:$ENV (支持local, test, production)"
fi
# 6. 重启后端服务
log_info "[6/6] 重启后端服务..."
pkill -f "uvicorn app.main:app" || true
sleep 2
cd "$PROJECT_ROOT/backend"
nohup python3 -m uvicorn app.main:app --port 3000 --host 0.0.0.0 > /tmp/uvicorn.log 2>&1 &
sleep 2
if curl -s http://localhost:3000/health | grep -q "healthy"; then
log_info "后端服务启动成功"
else
log_error "后端服务启动失败,请检查日志:/tmp/uvicorn.log"
fi
log_info "=== 部署完成 ==="
log_info "版本v$VERSION"
log_info "环境:$ENV"

View File

@ -1,13 +0,0 @@
#!/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后端服务已启动'

View File

@ -1,77 +0,0 @@
# 静态资源目录
本目录存放项目的所有静态资源文件。
## 📁 目录结构
```
static/
├── images/ # 图片资源
│ └── logo.jpg # 系统 Logo106KB
├── icons/ # 图标资源
│ ├── favicon.ico # 浏览器标签页图标
│ ├── apple-touch-icon.png
│ └── android-chrome-*.png
└── fonts/ # 字体文件
```
## 📋 文件说明
### images/
- `logo.jpg` - 系统主 Logo用于登录页面和首页
### icons/
- `favicon.ico` - 16x16 浏览器标签页图标
- `apple-touch-icon.png` - 180x180 iOS 设备图标
- `android-chrome-192.png` - 192x192 Android 图标
- `android-chrome-512.png` - 512x512 Android 图标
### fonts/
- 自定义字体文件(如有需要)
## 🎨 资源规范
### Logo
- 格式JPG/PNG/SVG
- 建议尺寸512x512 或更大
- 用途:登录页面、首页、关于页面
### Favicon
- 格式ICO多尺寸包含 16x16, 32x32
- 用途:浏览器标签页、书签
### 应用图标
- 格式PNG透明背景
- 尺寸192x192, 512x512
- 用途PWA、主屏幕快捷方式
## 📦 部署说明
### 后端访问
```python
# FastAPI 挂载静态文件目录
app.mount("/static", StaticFiles(directory="static"), name="static")
```
### 前端访问
```javascript
// 开发环境
<img src="/static/images/logo.jpg" />
// 生产环境(由 Nginx 代理)
<img src="/static/images/logo.jpg" />
```
### Nginx 配置示例
```nginx
# 静态资源
location /static {
alias /path/to/jiachenlong/static;
expires 30d;
add_header Cache-Control "public, immutable";
}
```
---
**最后更新**: 2026-03-16

View File

View File

@ -1,57 +0,0 @@
# 字体文件
本目录存放自定义字体文件。
## 📁 支持的格式
- `.woff2` - Web Open Font Format 2推荐
- `.woff` - Web Open Font Format
- `.ttf` - TrueType Font
- `.otf` - OpenType Font
## 🎨 使用示例
### CSS 中引入
```css
@font-face {
font-family: 'CustomFont';
src: url('/static/fonts/CustomFont.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'CustomFont', -apple-system, BlinkMacSystemFont, sans-serif;
}
```
### React 组件中使用
```jsx
<div style={{ fontFamily: 'CustomFont, sans-serif' }}>
自定义字体文本
</div>
```
## 📦 常用字体
### 中文字体
- 思源黑体Source Han Sans
- 思源宋体Source Han Serif
- 站酷系列字体
### 英文字体
- Inter
- Roboto
- Open Sans
## ⚠️ 注意事项
1. **字体版权**:确保有商用授权
2. **文件大小**:中文字体较大,建议压缩或使用子集
3. **加载性能**:使用 `font-display: swap` 避免 FOIT
---
**最后更新**: 2026-03-16

View File

View File

@ -1,48 +0,0 @@
# 图标资源
本目录存放项目的各种图标文件。
## 📁 需要的图标
### 浏览器图标
- `favicon.ico` - 16x16, 32x32浏览器标签页
### iOS 设备
- `apple-touch-icon.png` - 180x180iPhone/iPad 主屏幕)
### Android 设备
- `android-chrome-192.png` - 192x192
- `android-chrome-512.png` - 512x512
### PWA
- `maskable-icon.png` - 512x512可适配图标
## 🎨 生成工具
推荐使用在线工具生成全套图标:
- [RealFaviconGenerator](https://realfavicongenerator.net/)
- [Favicon Generator](https://www.favicon-generator.org/)
## 📝 使用示例
`frontend/index.html` 中添加:
```html
<head>
<!-- 标准 favicon -->
<link rel="icon" href="/static/icons/favicon.ico" />
<!-- iOS 设备 -->
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png" />
<!-- Android Chrome -->
<link rel="icon" type="image/png" sizes="192x192"
href="/static/icons/android-chrome-192.png" />
<link rel="icon" type="image/png" sizes="512x512"
href="/static/icons/android-chrome-512.png" />
</head>
```
---
**最后更新**: 2026-03-16

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

View File

View File

@ -1,240 +0,0 @@
# Logo 使用规范
**版本**: v1.0.0
**更新日期**: 2026-03-16
**状态**: ✅ 官方指定 Logo
---
## 🐉 官方 Logo
### 主 Logo
**文件**: `jiachenlong-logo.png`
**位置**:
- 本地:`/static/images/jiachenlong-logo.png`
- 前端服务器:`/var/www/html/static/images/jiachenlong-logo.png`
**规格**:
- 格式PNG
- 大小606KB
- 尺寸:正方形(适合圆形裁剪)
- 颜色:橙色(中国传统色)
- 设计:龙型环绕 + "甲辰收藏"文字
---
## 📋 使用场景
### 1. 登录页面
**文件**: `frontend/src/pages/Login.jsx`
```jsx
<img
src="/static/images/jiachenlong-logo.png"
alt="甲辰收藏"
style={{
width: '200px',
height: '200px',
borderRadius: '50%',
boxShadow: '0 0 40px rgba(251, 191, 36, 0.4)',
background: '#fff'
}}
/>
```
### 2. 首页
**文件**: `frontend/src/pages/Home.jsx`
```jsx
<img
src="/static/images/jiachenlong-logo.png"
alt="甲辰收藏"
style={{
width: '48px',
height: '48px',
borderRadius: '50%',
objectFit: 'cover'
}}
/>
```
### 3. 藏品详情页
**文件**: `frontend/src/pages/Detail.jsx`
```jsx
<img
src="/static/images/jiachenlong-logo.png"
alt="甲辰收藏"
onError={(e) => {
e.target.src = '/static/images/jiachenlong-logo.png';
}}
/>
```
---
## 🎨 样式规范
### 圆形样式(推荐)
```css
.logo {
width: 200px;
height: 200px;
border-radius: 50%;
object-fit: cover;
box-shadow: 0 0 40px rgba(251, 191, 36, 0.4);
background: #fff;
}
```
### 小尺寸(导航栏等)
```css
.logo-small {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}
```
### 中等尺寸
```css
.logo-medium {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
```
---
## 📦 部署规范
### 部署脚本
**文件**: `scripts/deploy.sh`
部署脚本会自动:
1. ✅ 检查 Logo 文件是否存在
2. ✅ 部署前端构建文件
3. ✅ 部署 Logo 到服务器
4. ✅ 重启 Nginx
### 部署命令
```bash
# 测试环境
./scripts/deploy.sh 1.0.0 test
# 生产环境
./scripts/deploy.sh 1.0.0 production
```
### 手动部署
```bash
# 1. 构建前端
cd frontend
npm run build
# 2. 部署到服务器
scp -r dist/* root@8.149.137.26:/var/www/html/
scp static/images/jiachenlong-logo.png root@8.149.137.26:/var/www/html/static/images/
# 3. 重启 Nginx
ssh root@8.149.137.26 "nginx -s reload"
```
---
## ⚠️ 注意事项
### 必须遵守
1. ✅ **统一使用** `jiachenlong-logo.png`
2. ✅ **禁止使用** 旧版 `logo.jpg`、`dragon-logo.jpg`、`title_logo.svg`
3. ✅ **保持比例** - 始终使用正方形容器
4. ✅ **圆形裁剪** - 使用 `border-radius: 50%`
5. ✅ **白色背景** - Logo 需要白色背景衬托
### 禁止行为
- ❌ 不要修改 Logo 颜色
- ❌ 不要拉伸变形
- ❌ 不要添加其他效果
- ❌ 不要使用其他 Logo 文件
---
## 📁 文件位置
### 本地开发
```
jiachenlong/
└── static/
└── images/
└── jiachenlong-logo.png # ✅ 官方 Logo
```
### 前端服务器
```
/var/www/html/
└── static/
└── images/
└── jiachenlong-logo.png # ✅ 官方 Logo
```
---
## 🔄 更新流程
如需更新 Logo
1. **替换文件**
```bash
cp new-logo.png /static/images/jiachenlong-logo.png
```
2. **重新构建**
```bash
cd frontend
npm run build
```
3. **部署到服务器**
```bash
./scripts/deploy.sh 1.0.1 production
```
4. **验证部署**
```bash
curl http://8.149.137.26/static/images/jiachenlong-logo.png -o /tmp/logo-check.png
```
---
## 📊 Logo 对比
| 文件 | 状态 | 说明 |
|------|------|------|
| `jiachenlong-logo.png` | ✅ **官方指定** | 橙色圆形龙型 Logo |
| `logo.jpg` | ❌ 废弃 | 旧版 Logo |
| `dragon-logo.jpg` | ❌ 废弃 | 旧版龙型 Logo |
| `title_logo.svg` | ❌ 废弃 | 旧版 SVG Logo |
---
**所有部署必须使用 `jiachenlong-logo.png`**
**最后更新**: 2026-03-16

View File

@ -1,36 +0,0 @@
# 图片资源
本目录存放项目的所有图片资源。
## 📁 文件列表
- `logo.jpg` - 系统主 Logo106KB, 512x512
## 🎨 使用方式
### 前端访问
```jsx
<img src="/static/images/logo.jpg" alt="logo" />
```
### 后端访问FastAPI
```python
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
```
## 📐 建议尺寸
- **Logo**: 512x512 或更大(用于缩放)
- **背景图**: 1920x1080全屏背景
- **头像**: 200x200用户头像
## 📦 格式建议
- **Logo**: PNG透明背景或 JPG
- **照片**: JPG压缩比好
- **图标**: SVG矢量可缩放或 PNG
---
**最后更新**: 2026-03-16

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

View File

@ -1,26 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 80">
<defs>
<linearGradient id="goldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFE4B5"/>
<stop offset="25%" stop-color="#FFD700"/>
<stop offset="50%" stop-color="#FFA500"/>
<stop offset="75%" stop-color="#DAA520"/>
<stop offset="100%" stop-color="#B8860B"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feFlood flood-color="#FFD700" flood-opacity="0.6"/>
<feComposite in2="blur" operator="in"/>
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="shadow">
<feDropShadow dx="2" dy="3" stdDeviation="2" flood-color="#000" flood-opacity="0.5"/>
</filter>
</defs>
<!-- Main title -->
<text x="0" y="45" font-family="'Kaiti SC', 'STKaiti', 'KaiTi', 'SimKai', serif" font-size="52" font-weight="bold" fill="url(#goldGrad)" filter="url(#shadow)">甲辰收藏</text>
<!-- Subtitle -->
<text x="0" y="72" font-family="'Kaiti SC', 'STKaiti', 'KaiTi', 'SimKai', serif" font-size="20" fill="#DAA520" letter-spacing="4">生肖纪念钞管理系统</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB