Compare commits
4 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
088dc90661 | |
|
|
c78640edb8 | |
|
|
63718824a9 | |
|
|
17fb08b487 |
102
README.md
102
README.md
|
|
@ -1,8 +1,8 @@
|
||||||
# 甲辰藏品管理系统
|
# 甲辰藏品管理系统
|
||||||
|
|
||||||
**版本**: v1.0.0
|
**版本**: v1.1.22
|
||||||
**代号**: 新生
|
**代号**: 新生
|
||||||
**发布日期**: 2026-03-16
|
**发布日期**: 2026-03-20
|
||||||
|
|
||||||
生肖纪念钞收藏管理系统 - 支持藏品管理、OCR 识别、统计分析等功能。
|
生肖纪念钞收藏管理系统 - 支持藏品管理、OCR 识别、统计分析等功能。
|
||||||
|
|
||||||
|
|
@ -23,6 +23,30 @@ jiachenlong/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🖥️ 环境配置
|
||||||
|
|
||||||
|
### 生产环境 (A环境) - 已锁定
|
||||||
|
| 角色 | IP | 服务 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 负载均衡/Nginx | 39.106.51.77 | Nginx反向代理 |
|
||||||
|
| 前端 | 8.154.46.3 | React静态页面 |
|
||||||
|
| 后端 | 42.121.116.25:3000 | FastAPI |
|
||||||
|
| 数据库 | 47.98.171.101 | PostgreSQL |
|
||||||
|
|
||||||
|
### 测试环境 (C环境)
|
||||||
|
| 角色 | IP | 服务 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 前端 | 47.103.29.111 | Nginx + React |
|
||||||
|
| 后端 | 47.103.9.192:3000 | FastAPI |
|
||||||
|
| 数据库 | 本地 | PostgreSQL |
|
||||||
|
|
||||||
|
### 运维信息
|
||||||
|
- **负载均衡密码**: Jiachenlong123
|
||||||
|
- **C环境SSH密码**: Passwd1@3
|
||||||
|
- **Gitea仓库**: http://47.253.189.47:3000/coolbot/jiachenlong.git
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 后端启动
|
### 后端启动
|
||||||
|
|
@ -55,61 +79,63 @@ npm run dev
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 配置文件
|
### 部署流程
|
||||||
|
1. C环境测试 → B环境灰度 → A环境发布
|
||||||
所有配置文件在 `config/` 目录:
|
2. 每次发布需更新 config/VERSION 文件
|
||||||
|
3. 提交代码到Gitea
|
||||||
- `config/VERSION` - 版本号配置
|
|
||||||
- `config/docker-compose.yml` - Docker 部署配置
|
|
||||||
- `.gitignore` - Git 忽略配置(根目录)
|
|
||||||
|
|
||||||
### 静态资源
|
|
||||||
|
|
||||||
所有静态资源在 `static/` 目录:
|
|
||||||
|
|
||||||
- `static/images/` - 图片资源(Logo、背景图等)
|
|
||||||
- `static/icons/` - 图标资源(favicon、应用图标等)
|
|
||||||
- `static/fonts/` - 字体文件
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 文档
|
## 📋 核心功能
|
||||||
|
|
||||||
所有文档都在 `docs/` 目录下:
|
- ✅ 用户管理(管理员/普通用户)
|
||||||
|
- ✅ 藏品管理(CRUD)
|
||||||
- `docs/DEPLOYMENT_v1.0.0.md` - 部署指南
|
- ✅ OCR 识别(阿里云DashScope)
|
||||||
- `docs/RELEASE_v1.0.0.md` - 发布说明
|
- ✅ 统计分析
|
||||||
- `docs/ERROR_CODES.md` - 错误码
|
- ✅ 图片上传(阿里云OSS)
|
||||||
- `docs/部署手册.md` - 完整部署手册
|
- ✅ 移动端适配
|
||||||
- `docs/BACKEND_SERVICE_GUIDE.md` - 后端服务指南
|
- ✅ 操作日志记录
|
||||||
- `docs/TEST_REPORT.md` - 测试报告
|
- ✅ 管理页面查看用户藏品
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
## 🔧 技术栈
|
||||||
|
|
||||||
**后端**:
|
**后端**:
|
||||||
- FastAPI + SQLAlchemy
|
- FastAPI + SQLAlchemy
|
||||||
- PostgreSQL
|
- PostgreSQL 15+
|
||||||
- JWT 认证
|
- JWT 认证
|
||||||
- DashScope OCR
|
- DashScope OCR (阿里云)
|
||||||
|
- 阿里云OSS存储
|
||||||
|
|
||||||
**前端**:
|
**前端**:
|
||||||
- React + Vite
|
- React + Vite
|
||||||
- 移动端适配
|
- 移动端适配(PWA)
|
||||||
- 暗色主题
|
- 暗色主题
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 核心功能
|
## 📊 数据库表
|
||||||
|
|
||||||
- ✅ 用户管理(管理员/普通用户)
|
- `users` - 用户表
|
||||||
- ✅ 藏品管理(CRUD)
|
- `collections` - 藏品表
|
||||||
- ✅ OCR 识别
|
- `collection_images` - 藏品图片表
|
||||||
- ✅ 统计分析
|
- `operations` - 操作日志表
|
||||||
- ✅ 图片上传
|
- `custom_fields` - 自定义字段表
|
||||||
- ✅ 移动端适配
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**开发团队**: 酷博特 + 菜鸟小 D 🤖
|
## 📝 字段命名规范
|
||||||
|
|
||||||
|
采用 fXX_XX_XXX 格式:
|
||||||
|
- f01 - 基本信息
|
||||||
|
- f02 - 详细字段
|
||||||
|
- f03 - 评级信息
|
||||||
|
- f04 - 特殊信息
|
||||||
|
- f05 - 价格信息
|
||||||
|
- f06 - 其他
|
||||||
|
- f99 - 系统字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**开发团队**: 酷博特 + 菜鸟生产 🤖
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class JSONFormatter(logging.Formatter):
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
log_data = {
|
log_data = {
|
||||||
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
|
||||||
'level': record.levelname,
|
'level': record.levelname,
|
||||||
'logger': record.name,
|
'logger': record.name,
|
||||||
'message': record.getMessage(),
|
'message': record.getMessage(),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,43 @@ router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||||
@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)):
|
||||||
"""用户注册"""
|
"""用户注册"""
|
||||||
|
# 验证短信验证码
|
||||||
|
if user_data.phone and user_data.verifyCode:
|
||||||
|
from app.services.sms import verify_code
|
||||||
|
if not verify_code(user_data.phone, user_data.verifyCode):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="验证码错误或已过期"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="手机号和验证码为必填项"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证密码复杂度(至少8位,包含大写、小写、数字)
|
||||||
|
if len(user_data.password) < 8:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="密码至少8位"
|
||||||
|
)
|
||||||
|
import re
|
||||||
|
if not re.search(r'[A-Z]', user_data.password):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="密码必须包含大写字母"
|
||||||
|
)
|
||||||
|
if not re.search(r'[a-z]', user_data.password):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="密码必须包含小写字母"
|
||||||
|
)
|
||||||
|
if not re.search(r'[0-9]', user_data.password):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="密码必须包含数字"
|
||||||
|
)
|
||||||
|
|
||||||
# 检查用户名是否已存在
|
# 检查用户名是否已存在
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import uuid
|
||||||
import re
|
import re
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File
|
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File
|
||||||
from sqlalchemy import func, text
|
from sqlalchemy import or_, func, text
|
||||||
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
|
||||||
|
|
@ -101,6 +101,7 @@ def get_next_code(
|
||||||
def get_collections(
|
def get_collections(
|
||||||
category: Optional[str] = None,
|
category: Optional[str] = None,
|
||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
limit: int = Query(20, ge=1, le=100),
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
|
@ -117,10 +118,12 @@ def get_collections(
|
||||||
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
|
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
|
query = db.query(Collection).filter(or_(Collection.f99_91_user_id == current_user.f99_90_id, current_user.role == "admin"))
|
||||||
|
|
||||||
if category:
|
if category:
|
||||||
query = query.filter(Collection.f01_03_category == category)
|
query = query.filter(Collection.f01_03_category == category)
|
||||||
|
if user_id:
|
||||||
|
query = query.filter(Collection.f99_91_user_id == user_id)
|
||||||
if status:
|
if status:
|
||||||
query = query.filter(Collection.f01_04_status == status)
|
query = query.filter(Collection.f01_04_status == status)
|
||||||
if search:
|
if search:
|
||||||
|
|
@ -224,7 +227,7 @@ def get_stats(
|
||||||
all_collections = db.query(Collection).all()
|
all_collections = db.query(Collection).all()
|
||||||
else:
|
else:
|
||||||
all_collections = db.query(Collection).filter(
|
all_collections = db.query(Collection).filter(
|
||||||
Collection.f99_91_user_id == current_user.f99_90_id
|
or_(Collection.f99_91_user_id == current_user.f99_90_id, current_user.role == "admin")
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# 总数
|
# 总数
|
||||||
|
|
@ -304,6 +307,44 @@ def get_stats(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-user/{user_id}")
|
||||||
|
def get_collections_by_user(
|
||||||
|
user_id: str,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取指定用户的所有藏品(管理员专用)"""
|
||||||
|
if current_user.role != "admin":
|
||||||
|
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
|
||||||
|
|
||||||
|
collections = db.query(Collection).filter(
|
||||||
|
Collection.f99_91_user_id == user_id
|
||||||
|
).all()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for c in collections:
|
||||||
|
result.append({
|
||||||
|
"f99_90_id": c.f99_90_id,
|
||||||
|
"name": c.f01_01_name,
|
||||||
|
"code": c.f01_02_code,
|
||||||
|
"prefixSerial": c.f02_10_prefix_serial,
|
||||||
|
"version": c.f02_11_version,
|
||||||
|
"category": c.f01_03_category,
|
||||||
|
"status": c.f01_04_status,
|
||||||
|
"packaging": c.f02_12_packaging,
|
||||||
|
"isGraded": c.f03_20_is_graded,
|
||||||
|
"costPrice": c.f05_40_cost_price,
|
||||||
|
"f01_01_name": c.f01_01_name,
|
||||||
|
"f02_10_prefix_serial": c.f02_10_prefix_serial,
|
||||||
|
"f02_11_version": c.f02_11_version,
|
||||||
|
"f01_04_status": c.f01_04_status,
|
||||||
|
"userId": c.f99_91_user_id,
|
||||||
|
"f05_40_cost_price": c.f05_40_cost_price,
|
||||||
|
"f99_92_created_at": c.f99_92_created_at.isoformat() if c.f99_92_created_at else None
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/{collection_id}")
|
@router.get("/{collection_id}")
|
||||||
def get_collection(
|
def get_collection(
|
||||||
collection_id: str,
|
collection_id: str,
|
||||||
|
|
@ -396,7 +437,7 @@ def create_collection(
|
||||||
if not force and final_code:
|
if not force and final_code:
|
||||||
existing_code = db.query(Collection).filter(
|
existing_code = db.query(Collection).filter(
|
||||||
Collection.f01_02_code == final_code,
|
Collection.f01_02_code == final_code,
|
||||||
Collection.f99_91_user_id == current_user.f99_90_id
|
or_(Collection.f99_91_user_id == current_user.f99_90_id, current_user.role == "admin")
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing_code:
|
if existing_code:
|
||||||
|
|
@ -413,7 +454,7 @@ def create_collection(
|
||||||
# 查询当前用户是否有相同冠字号的藏品
|
# 查询当前用户是否有相同冠字号的藏品
|
||||||
existing = db.query(Collection).filter(
|
existing = db.query(Collection).filter(
|
||||||
Collection.f02_10_prefix_serial == collection_data.f02_10_prefix_serial,
|
Collection.f02_10_prefix_serial == collection_data.f02_10_prefix_serial,
|
||||||
Collection.f99_91_user_id == current_user.f99_90_id
|
or_(Collection.f99_91_user_id == current_user.f99_90_id, current_user.role == "admin")
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
|
|
@ -492,7 +533,7 @@ def update_collection(
|
||||||
"""更新藏品"""
|
"""更新藏品"""
|
||||||
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
|
or_(Collection.f99_91_user_id == current_user.f99_90_id, current_user.role == "admin")
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
|
|
@ -523,7 +564,7 @@ def delete_collection(
|
||||||
# 验证权限并检查是否存在
|
# 验证权限并检查是否存在
|
||||||
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
|
or_(Collection.f99_91_user_id == current_user.f99_90_id, current_user.role == "admin")
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,7 @@ async def claim_temp_image(
|
||||||
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']
|
temp_extensions = ['jpg', 'jpeg', 'png', 'gif', 'JPG', 'JPEG', 'PNG', 'GIF']
|
||||||
temp_content = None
|
temp_content = None
|
||||||
found_key = None
|
found_key = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,27 @@ def update_current_user(
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="用户不存在")
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
# 处理手机号更换验证码
|
||||||
|
new_phone = user_update.phone
|
||||||
|
verify_code = user_update.verifyCode
|
||||||
|
|
||||||
|
if new_phone and new_phone != user.phone:
|
||||||
|
# 需要验证验证码
|
||||||
|
if not verify_code:
|
||||||
|
raise HTTPException(status_code=400, detail="更换手机号需要验证码")
|
||||||
|
|
||||||
|
# 验证验证码
|
||||||
|
from app.services.sms import verify_code as sms_verify
|
||||||
|
import os
|
||||||
|
if os.getenv("SMS_TEST_MODE") == "true":
|
||||||
|
# 测试模式:任何6位数字有效
|
||||||
|
is_valid = len(verify_code) == 6 and verify_code.isdigit()
|
||||||
|
else:
|
||||||
|
is_valid = sms_verify(new_phone, verify_code)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
raise HTTPException(status_code=400, detail="验证码错误或已过期")
|
||||||
|
|
||||||
# 更新字段
|
# 更新字段
|
||||||
update_data = user_update.model_dump(exclude_unset=True)
|
update_data = user_update.model_dump(exclude_unset=True)
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
|
|
@ -55,6 +76,8 @@ def update_current_user(
|
||||||
user.f01_01_name = value
|
user.f01_01_name = value
|
||||||
elif field == 'username':
|
elif field == 'username':
|
||||||
pass # skip, already handled as f01_01_name
|
pass # skip, already handled as f01_01_name
|
||||||
|
elif field == 'verifyCode':
|
||||||
|
pass # skip,验证码不存储
|
||||||
elif hasattr(user, field):
|
elif hasattr(user, field):
|
||||||
setattr(user, field, value)
|
setattr(user, field, value)
|
||||||
|
|
||||||
|
|
@ -62,7 +85,7 @@ def update_current_user(
|
||||||
db.flush()
|
db.flush()
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
logger.info(f"After commit, user email={user.email}")
|
logger.info(f"After commit, user email={user.email}, phone={user.phone}")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from datetime import datetime
|
||||||
# ============ 用户相关 ============
|
# ============ 用户相关 ============
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
f01_01_name: str = Field(..., min_length=3, max_length=255, alias="username")
|
f01_01_name: str = Field(..., min_length=2, max_length=255, alias="username")
|
||||||
email: Optional[EmailStr] = None
|
email: Optional[EmailStr] = None
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
avatar: Optional[str] = None
|
avatar: Optional[str] = None
|
||||||
|
|
@ -18,13 +18,15 @@ class UserBase(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
password: str = Field(..., min_length=6)
|
password: str = Field(..., min_length=8)
|
||||||
|
verifyCode: Optional[str] = Field(None, alias="verifyCode")
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(BaseModel):
|
class UserUpdate(BaseModel):
|
||||||
f01_01_name: Optional[str] = Field(None, alias="username")
|
f01_01_name: Optional[str] = Field(None, alias="username")
|
||||||
email: Optional[EmailStr] = None
|
email: Optional[str] = None # 改为字符串,不强制验证EmailStr
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
|
verifyCode: Optional[str] = Field(None, alias="verifyCode") # 更换手机号验证码
|
||||||
avatar: Optional[str] = None
|
avatar: Optional[str] = None
|
||||||
address: Optional[str] = None
|
address: Optional[str] = None
|
||||||
bio: Optional[str] = None
|
bio: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ from typing import Optional
|
||||||
|
|
||||||
# 阿里云短信配置
|
# 阿里云短信配置
|
||||||
SMS_CONFIG = {
|
SMS_CONFIG = {
|
||||||
"access_key_id": os.getenv("SMS_ACCESS_KEY_ID", "LTAI5t6HUnpFBLEK9194kPVG"),
|
"access_key_id": os.getenv("SMS_ACCESS_KEY_ID", "LTAI5tQAx5niD7JQVqGE5acE"),
|
||||||
"access_key_secret": os.getenv("SMS_ACCESS_KEY_SECRET", "LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"),
|
"access_key_secret": os.getenv("SMS_ACCESS_KEY_SECRET", "QsQFAEKBkaNynIoKyvdIi3BUyWVZu1"),
|
||||||
"sign_name": "阿里云",
|
"sign_name": os.getenv("SMS_SIGN_NAME", "苏州算力"),
|
||||||
"template_code": "100001",
|
"template_code": os.getenv("SMS_TEMPLATE_CODE", "SMS_501590956"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 验证码缓存(生产环境建议用Redis)
|
# 验证码缓存(生产环境建议用Redis)
|
||||||
|
|
@ -82,6 +82,11 @@ def send_verification_code(phone: str) -> dict:
|
||||||
|
|
||||||
def verify_code(phone: str, code: str) -> bool:
|
def verify_code(phone: str, code: str) -> bool:
|
||||||
"""验证验证码"""
|
"""验证验证码"""
|
||||||
|
# 测试模式:任何6位数字验证码都有效
|
||||||
|
import os
|
||||||
|
if os.getenv("SMS_TEST_MODE") == "true":
|
||||||
|
return len(code) == 6 and code.isdigit()
|
||||||
|
|
||||||
if phone not in VERIFICATION_CODES:
|
if phone not in VERIFICATION_CODES:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1 @@
|
||||||
# 甲辰藏品管理系统 - 版本配置
|
v1.2.0
|
||||||
# Version Configuration for Zodiac Collection Management System
|
|
||||||
|
|
||||||
# 当前版本号 (语义化版本:主版本。次版本.修订版)
|
|
||||||
VERSION=1.1.21
|
|
||||||
|
|
||||||
# 版本代号 (可选)
|
|
||||||
VERSION_CODENAME="新生"
|
|
||||||
|
|
||||||
# 发布日期
|
|
||||||
RELEASE_DATE=2026-03-18
|
|
||||||
|
|
||||||
# 版本说明
|
|
||||||
VERSION_NOTES="性能优化 - Nginx反向代理、图片OSS直连、自动压缩、上传限制20MB"
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<title>甲辰收藏 v1.1.19</title>
|
<title>甲辰收藏 v1.1.21</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Loading…
Reference in New Issue