20251222
This commit is contained in:
251
Server/app/api/v1/admin_config.py
Normal file
251
Server/app/api/v1/admin_config.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
管理员配置接口
|
||||
功能:管理功能配置、VIP配置、签到配置
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Header, HTTPException, Depends
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db import get_db
|
||||
from app.models.business import FeatureConfig, VipConfig, CheckInConfig
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ==================== 数据模型 ====================
|
||||
|
||||
class FeatureConfigCreate(BaseModel):
|
||||
feature_key: str
|
||||
feature_name: str
|
||||
category: str = "general"
|
||||
points_cost: int
|
||||
vip_points_cost: int = 0
|
||||
svip_points_cost: int = 0
|
||||
description: Optional[str] = None
|
||||
|
||||
class FeatureConfigUpdate(BaseModel):
|
||||
points_cost: Optional[int] = None
|
||||
vip_points_cost: Optional[int] = None
|
||||
svip_points_cost: Optional[int] = None
|
||||
enabled: Optional[bool] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
class VIPConfigUpdate(BaseModel):
|
||||
price: Optional[float] = None
|
||||
daily_quota: Optional[int] = None
|
||||
points_multiplier: Optional[float] = None
|
||||
|
||||
class CheckInConfigCreate(BaseModel):
|
||||
consecutive_days: int
|
||||
base_points: int
|
||||
bonus_points: int
|
||||
total_points: int
|
||||
|
||||
class CheckInConfigUpdate(BaseModel):
|
||||
base_points: Optional[int] = None
|
||||
bonus_points: Optional[int] = None
|
||||
total_points: Optional[int] = None
|
||||
|
||||
# ==================== 辅助函数 ====================
|
||||
|
||||
def verify_admin_token(token: str):
|
||||
"""验证管理员Token"""
|
||||
expected_token = "admin-secret-token"
|
||||
if token != expected_token:
|
||||
raise HTTPException(status_code=401, detail="管理员Token无效")
|
||||
|
||||
# ==================== 功能配置管理 ====================
|
||||
|
||||
@router.get("/admin/config/features")
|
||||
async def get_features_config(token: str = Header(..., alias="x-admin-token"), db: Session = Depends(get_db)):
|
||||
"""获取所有功能配置"""
|
||||
verify_admin_token(token)
|
||||
features = db.query(FeatureConfig).order_by(FeatureConfig.category, FeatureConfig.feature_key).all()
|
||||
return features
|
||||
|
||||
@router.post("/admin/config/features")
|
||||
async def create_feature_config(
|
||||
data: FeatureConfigCreate,
|
||||
token: str = Header(..., alias="x-admin-token"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""新增功能配置"""
|
||||
verify_admin_token(token)
|
||||
|
||||
existing = db.query(FeatureConfig).filter(FeatureConfig.feature_key == data.feature_key).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="功能标识已存在")
|
||||
|
||||
new_feature = FeatureConfig(
|
||||
feature_key=data.feature_key,
|
||||
feature_name=data.feature_name,
|
||||
category=data.category,
|
||||
points_cost=data.points_cost,
|
||||
vip_points_cost=data.vip_points_cost,
|
||||
svip_points_cost=data.svip_points_cost,
|
||||
description=data.description
|
||||
)
|
||||
db.add(new_feature)
|
||||
db.commit()
|
||||
return {"code": 200, "message": "创建成功"}
|
||||
|
||||
@router.put("/admin/config/features/{feature_key}")
|
||||
async def update_feature_config(
|
||||
feature_key: str,
|
||||
data: FeatureConfigUpdate,
|
||||
token: str = Header(..., alias="x-admin-token"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新功能配置"""
|
||||
verify_admin_token(token)
|
||||
|
||||
feature = db.query(FeatureConfig).filter(FeatureConfig.feature_key == feature_key).first()
|
||||
if not feature:
|
||||
raise HTTPException(status_code=404, detail="功能配置不存在")
|
||||
|
||||
if data.points_cost is not None:
|
||||
feature.points_cost = data.points_cost
|
||||
if data.vip_points_cost is not None:
|
||||
feature.vip_points_cost = data.vip_points_cost
|
||||
if data.svip_points_cost is not None:
|
||||
feature.svip_points_cost = data.svip_points_cost
|
||||
if data.enabled is not None:
|
||||
feature.enabled = data.enabled
|
||||
if data.description is not None:
|
||||
feature.description = data.description
|
||||
|
||||
db.commit()
|
||||
return {"code": 200, "message": "更新成功"}
|
||||
|
||||
@router.delete("/admin/config/features/{feature_key}")
|
||||
async def delete_feature_config(
|
||||
feature_key: str,
|
||||
token: str = Header(..., alias="x-admin-token"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除功能配置"""
|
||||
verify_admin_token(token)
|
||||
|
||||
feature = db.query(FeatureConfig).filter(FeatureConfig.feature_key == feature_key).first()
|
||||
if not feature:
|
||||
raise HTTPException(status_code=404, detail="功能配置不存在")
|
||||
|
||||
db.delete(feature)
|
||||
db.commit()
|
||||
return {"code": 200, "message": "删除成功"}
|
||||
|
||||
# ==================== VIP配置管理 ====================
|
||||
|
||||
@router.get("/admin/config/vip")
|
||||
async def get_vip_config(token: str = Header(..., alias="x-admin-token"), db: Session = Depends(get_db)):
|
||||
"""获取VIP配置"""
|
||||
verify_admin_token(token)
|
||||
|
||||
# Sort order: vip, svip
|
||||
# We can fetch all and sort in python or rely on insertion order/id
|
||||
configs = db.query(VipConfig).all()
|
||||
# Simple sort
|
||||
configs.sort(key=lambda x: 0 if x.vip_type == 'vip' else 1)
|
||||
return configs
|
||||
|
||||
@router.put("/admin/config/vip/{vip_type}")
|
||||
async def update_vip_config(
|
||||
vip_type: str,
|
||||
data: VIPConfigUpdate,
|
||||
token: str = Header(..., alias="x-admin-token"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新VIP配置"""
|
||||
verify_admin_token(token)
|
||||
|
||||
if vip_type not in ['vip', 'svip']:
|
||||
raise HTTPException(status_code=400, detail="VIP类型必须是vip或svip")
|
||||
|
||||
config = db.query(VipConfig).filter(VipConfig.vip_type == vip_type).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="VIP配置不存在")
|
||||
|
||||
if data.price is not None:
|
||||
config.price = data.price
|
||||
if data.daily_quota is not None:
|
||||
config.daily_quota = data.daily_quota
|
||||
if data.points_multiplier is not None:
|
||||
config.points_multiplier = data.points_multiplier
|
||||
|
||||
db.commit()
|
||||
return {"code": 200, "message": "更新成功"}
|
||||
|
||||
# ==================== 签到配置管理 ====================
|
||||
|
||||
@router.get("/admin/config/checkin")
|
||||
async def get_checkin_config(token: str = Header(..., alias="x-admin-token"), db: Session = Depends(get_db)):
|
||||
"""获取签到配置"""
|
||||
verify_admin_token(token)
|
||||
configs = db.query(CheckInConfig).filter(CheckInConfig.enabled == True).order_by(CheckInConfig.consecutive_days).all()
|
||||
return configs
|
||||
|
||||
@router.post("/admin/config/checkin")
|
||||
async def create_checkin_config(
|
||||
data: CheckInConfigCreate,
|
||||
token: str = Header(..., alias="x-admin-token"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""新增签到档位"""
|
||||
verify_admin_token(token)
|
||||
|
||||
existing = db.query(CheckInConfig).filter(CheckInConfig.consecutive_days == data.consecutive_days).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="该连续天数配置已存在")
|
||||
|
||||
new_config = CheckInConfig(
|
||||
consecutive_days=data.consecutive_days,
|
||||
base_points=data.base_points,
|
||||
bonus_points=data.bonus_points,
|
||||
total_points=data.total_points
|
||||
)
|
||||
db.add(new_config)
|
||||
db.commit()
|
||||
return {"code": 200, "message": "创建成功"}
|
||||
|
||||
@router.put("/admin/config/checkin/{consecutive_days}")
|
||||
async def update_checkin_config(
|
||||
consecutive_days: int,
|
||||
data: CheckInConfigUpdate,
|
||||
token: str = Header(..., alias="x-admin-token"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新签到档位"""
|
||||
verify_admin_token(token)
|
||||
|
||||
config = db.query(CheckInConfig).filter(CheckInConfig.consecutive_days == consecutive_days).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="签到配置不存在")
|
||||
|
||||
if data.base_points is not None:
|
||||
config.base_points = data.base_points
|
||||
if data.bonus_points is not None:
|
||||
config.bonus_points = data.bonus_points
|
||||
if data.total_points is not None:
|
||||
config.total_points = data.total_points
|
||||
|
||||
db.commit()
|
||||
return {"code": 200, "message": "更新成功"}
|
||||
|
||||
@router.delete("/admin/config/checkin/{consecutive_days}")
|
||||
async def delete_checkin_config(
|
||||
consecutive_days: int,
|
||||
token: str = Header(..., alias="x-admin-token"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除签到档位"""
|
||||
verify_admin_token(token)
|
||||
|
||||
config = db.query(CheckInConfig).filter(CheckInConfig.consecutive_days == consecutive_days).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="签到配置不存在")
|
||||
|
||||
db.delete(config)
|
||||
db.commit()
|
||||
return {"code": 200, "message": "删除成功"}
|
||||
|
||||
@@ -14,6 +14,16 @@ from datetime import datetime, timezone
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/verify", response_model=VerifyResponse)
|
||||
async def verify(
|
||||
verify_data: VerifyRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_username: str = Depends(get_current_user)
|
||||
):
|
||||
"""验证用户授权状态"""
|
||||
# 许可证验证接口:验证 token 是否有效,检查账户过期,更新活跃时间
|
||||
return auth_service.verify_license(db, verify_data, current_username)
|
||||
|
||||
@router.post("/send-verification-code")
|
||||
async def send_verification_code(body: SendVerificationCodeRequest, db: Session = Depends(get_db)):
|
||||
# 发送注册验证码
|
||||
@@ -97,3 +107,5 @@ async def get_online_time(username: str, db: Session = Depends(get_db)):
|
||||
async def heartbeat(body: UserHeartbeat, db: Session = Depends(get_db)):
|
||||
# 心跳接口:更新会话的最近在线时间
|
||||
return auth_service.heartbeat(db, body.username, body.device_id)
|
||||
|
||||
|
||||
|
||||
254
Server/app/api/v1/checkin.py
Normal file
254
Server/app/api/v1/checkin.py
Normal file
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
签到接口
|
||||
功能:每日签到、签到状态查询、签到日历
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
from datetime import date, datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from app.db import get_db
|
||||
from app.models.user import User
|
||||
from app.models.business import CheckInConfig, VipConfig, CheckInRecord, PointsHistory
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ==================== 数据模型 ====================
|
||||
|
||||
class CheckInRequest(BaseModel):
|
||||
username: str
|
||||
|
||||
# ==================== 签到功能 ====================
|
||||
|
||||
@router.post("/checkin/daily")
|
||||
async def daily_checkin(data: CheckInRequest, db: Session = Depends(get_db)):
|
||||
"""每日签到"""
|
||||
# 1. 获取用户信息
|
||||
user = db.query(User).filter(User.username == data.username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 2. 检查今日是否已签到
|
||||
today = date.today()
|
||||
if user.last_check_in_date == today:
|
||||
raise HTTPException(status_code=400, detail="今日已签到,明天再来吧")
|
||||
|
||||
# 3. 计算连续天数
|
||||
consecutive_days = user.consecutive_check_in if user.consecutive_check_in else 0
|
||||
total_days = user.total_check_in_days if user.total_check_in_days else 0
|
||||
|
||||
yesterday = today - timedelta(days=1)
|
||||
if user.last_check_in_date == yesterday:
|
||||
# 连续签到
|
||||
consecutive_days += 1
|
||||
else:
|
||||
# 中断了,重新开始
|
||||
consecutive_days = 1
|
||||
|
||||
total_days += 1
|
||||
|
||||
# 4. 从配置表获取奖励
|
||||
reward_config = db.query(CheckInConfig)\
|
||||
.filter(CheckInConfig.consecutive_days <= consecutive_days, CheckInConfig.enabled == True)\
|
||||
.order_by(CheckInConfig.consecutive_days.desc())\
|
||||
.first()
|
||||
|
||||
if not reward_config:
|
||||
# 如果没有配置,默认给10积分
|
||||
base_points = 10
|
||||
bonus_points = 0
|
||||
total_points = 10
|
||||
else:
|
||||
base_points = reward_config.base_points
|
||||
bonus_points = reward_config.bonus_points
|
||||
total_points = reward_config.total_points
|
||||
|
||||
# 5. 应用VIP倍数
|
||||
vip_multiplier = 1.0
|
||||
if user.vip_type and user.vip_type in ['vip', 'svip']:
|
||||
vip_config = db.query(VipConfig).filter(VipConfig.vip_type == user.vip_type).first()
|
||||
if vip_config:
|
||||
vip_multiplier = float(vip_config.points_multiplier)
|
||||
|
||||
points_earned = int(total_points * vip_multiplier)
|
||||
current_points = user.points if user.points else 0
|
||||
new_balance = current_points + points_earned
|
||||
|
||||
# 6. 更新用户数据
|
||||
user.points = new_balance
|
||||
user.total_check_in_days = total_days
|
||||
user.consecutive_check_in = consecutive_days
|
||||
user.last_check_in_date = today
|
||||
|
||||
# 7. 记录签到记录
|
||||
checkin_record = CheckInRecord(
|
||||
user_id=user.id,
|
||||
username=data.username,
|
||||
check_in_date=today,
|
||||
points_earned=points_earned,
|
||||
consecutive_days=consecutive_days,
|
||||
vip_multiplier=vip_multiplier
|
||||
)
|
||||
db.add(checkin_record)
|
||||
|
||||
# 8. 记录积分历史
|
||||
points_history = PointsHistory(
|
||||
user_id=user.id,
|
||||
username=data.username,
|
||||
type='checkin',
|
||||
amount=points_earned,
|
||||
balance=new_balance,
|
||||
description=f"每日签到奖励(连续{consecutive_days}天)"
|
||||
)
|
||||
db.add(points_history)
|
||||
|
||||
db.commit()
|
||||
|
||||
# 9. 返回结果
|
||||
return {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"success": True,
|
||||
"points_earned": points_earned,
|
||||
"base_points": base_points,
|
||||
"bonus_points": bonus_points,
|
||||
"vip_multiplier": vip_multiplier,
|
||||
"consecutive_days": consecutive_days,
|
||||
"total_check_in_days": total_days,
|
||||
"total_points": new_balance,
|
||||
"message": f"签到成功!连续签到{consecutive_days}天,获得{points_earned}积分"
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/checkin/config")
|
||||
async def get_checkin_config(db: Session = Depends(get_db)):
|
||||
"""获取签到奖励配置(公开)"""
|
||||
configs = db.query(CheckInConfig)\
|
||||
.filter(CheckInConfig.enabled == True)\
|
||||
.order_by(CheckInConfig.consecutive_days)\
|
||||
.all()
|
||||
|
||||
result = []
|
||||
for config in configs:
|
||||
result.append({
|
||||
"consecutive_days": config.consecutive_days,
|
||||
"base_points": config.base_points,
|
||||
"bonus_points": config.bonus_points,
|
||||
"total_points": config.total_points
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": result
|
||||
}
|
||||
|
||||
@router.get("/checkin/status")
|
||||
async def get_checkin_status(username: str, db: Session = Depends(get_db)):
|
||||
"""获取签到状态"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
today = date.today()
|
||||
today_checked = (user.last_check_in_date == today)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"today_checked": today_checked,
|
||||
"consecutive_days": user.consecutive_check_in if user.consecutive_check_in else 0,
|
||||
"total_days": user.total_check_in_days if user.total_check_in_days else 0,
|
||||
"last_check_in_date": user.last_check_in_date.isoformat() if user.last_check_in_date else None
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/checkin/calendar/{year}/{month}")
|
||||
async def get_checkin_calendar(username: str, year: int, month: int, db: Session = Depends(get_db)):
|
||||
"""获取签到日历"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 使用 SQLAlchemy 提取日期部分
|
||||
# 注意:SQLite 和 MySQL 的日期函数不同
|
||||
# 为了兼容性,这里我们查询该月范围内的所有记录,然后在 Python 中处理
|
||||
|
||||
import calendar
|
||||
_, last_day = calendar.monthrange(year, month)
|
||||
start_date = date(year, month, 1)
|
||||
end_date = date(year, month, last_day)
|
||||
|
||||
records = db.query(CheckInRecord)\
|
||||
.filter(
|
||||
CheckInRecord.user_id == user.id,
|
||||
CheckInRecord.check_in_date >= start_date,
|
||||
CheckInRecord.check_in_date <= end_date
|
||||
).all()
|
||||
|
||||
checked_dates = [record.check_in_date.day for record in records]
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"checked_dates": checked_dates
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/checkin/history")
|
||||
async def get_checkin_history(username: str, page: int = 1, limit: int = 10, db: Session = Depends(get_db)):
|
||||
"""签到记录"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
offset = (page - 1) * limit
|
||||
|
||||
total = db.query(func.count(CheckInRecord.id)).filter(CheckInRecord.user_id == user.id).scalar()
|
||||
|
||||
records = db.query(CheckInRecord)\
|
||||
.filter(CheckInRecord.user_id == user.id)\
|
||||
.order_by(CheckInRecord.check_in_date.desc())\
|
||||
.offset(offset)\
|
||||
.limit(limit)\
|
||||
.all()
|
||||
|
||||
result_records = []
|
||||
for record in records:
|
||||
result_records.append({
|
||||
"check_in_date": record.check_in_date.isoformat(),
|
||||
"points_earned": record.points_earned,
|
||||
"consecutive_days": record.consecutive_days,
|
||||
"created_at": record.created_at.isoformat() if record.created_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"total": total,
|
||||
"records": result_records
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/checkin/config")
|
||||
async def get_checkin_config(db: Session = Depends(get_db)):
|
||||
"""获取签到奖励规则 (公开接口)"""
|
||||
configs = db.query(CheckInConfig).filter(CheckInConfig.enabled == True).order_by(CheckInConfig.consecutive_days.asc()).all()
|
||||
|
||||
data = []
|
||||
for c in configs:
|
||||
data.append({
|
||||
"consecutive_days": c.consecutive_days,
|
||||
"base_points": c.base_points,
|
||||
"bonus_points": c.bonus_points,
|
||||
"total_points": c.total_points
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": data
|
||||
}
|
||||
|
||||
133
Server/app/api/v1/feature.py
Normal file
133
Server/app/api/v1/feature.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
通用功能使用接口
|
||||
核心:动态扣费逻辑(SVIP免费、VIP配额、普通积分)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
from datetime import date
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db import get_db
|
||||
from app.models.user import User
|
||||
from app.models.business import FeatureConfig, VipConfig, PointsHistory
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ==================== 数据模型 ====================
|
||||
|
||||
class UseFeatureRequest(BaseModel):
|
||||
username: str
|
||||
feature_key: str
|
||||
device_id: str
|
||||
|
||||
# ==================== 通用功能使用 ====================
|
||||
|
||||
@router.post("/feature/use")
|
||||
async def use_feature(data: UseFeatureRequest, db: Session = Depends(get_db)):
|
||||
"""
|
||||
通用功能使用接口
|
||||
逻辑:
|
||||
1. SVIP用户:免费使用
|
||||
2. VIP用户:优先使用配额,配额用完后扣积分
|
||||
3. 普通用户:扣除积分
|
||||
"""
|
||||
# 1. 获取功能配置
|
||||
feature = db.query(FeatureConfig).filter(FeatureConfig.feature_key == data.feature_key).first()
|
||||
if not feature or not feature.enabled:
|
||||
raise HTTPException(status_code=400, detail="功能不存在或已禁用")
|
||||
|
||||
# 2. 获取用户信息
|
||||
user = db.query(User).filter(User.username == data.username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
current_points = user.points if user.points else 0
|
||||
vip_type = user.vip_type
|
||||
vip_quota = user.vip_daily_quota if user.vip_daily_quota else 0
|
||||
quota_reset_date = user.vip_quota_reset_date
|
||||
|
||||
# 3. 检查并重置VIP配额
|
||||
today = date.today()
|
||||
if vip_type in ['vip', 'svip'] and quota_reset_date != today:
|
||||
# 重置配额
|
||||
vip_config = db.query(VipConfig).filter(VipConfig.vip_type == vip_type).first()
|
||||
if vip_config:
|
||||
vip_quota = vip_config.daily_quota
|
||||
user.vip_daily_quota = vip_quota
|
||||
user.vip_quota_reset_date = today
|
||||
db.commit()
|
||||
|
||||
# 4. 判断消耗类型
|
||||
cost_type = "points"
|
||||
points_cost = 0
|
||||
remaining_quota = vip_quota
|
||||
|
||||
# SVIP免费
|
||||
if vip_type == 'svip' and feature.svip_points_cost == 0:
|
||||
cost_type = "free"
|
||||
message = "SVIP用户免费使用"
|
||||
|
||||
# VIP配额
|
||||
elif vip_type == 'vip' and vip_quota > 0 and feature.vip_points_cost == 0:
|
||||
cost_type = "vip_quota"
|
||||
remaining_quota = vip_quota - 1
|
||||
user.vip_daily_quota = remaining_quota
|
||||
db.commit()
|
||||
message = f"VIP配额使用,剩余{remaining_quota}次"
|
||||
|
||||
# 扣积分
|
||||
else:
|
||||
# 计算实际消耗
|
||||
if vip_type == 'vip' and feature.vip_points_cost > 0:
|
||||
points_cost = feature.vip_points_cost
|
||||
elif vip_type == 'svip' and feature.svip_points_cost > 0:
|
||||
points_cost = feature.svip_points_cost
|
||||
else:
|
||||
points_cost = feature.points_cost
|
||||
|
||||
# 检查余额
|
||||
if current_points < points_cost:
|
||||
raise HTTPException(
|
||||
status_code=402,
|
||||
detail=f"积分不足。当前: {current_points},需要: {points_cost}。请签到获取积分或开通VIP"
|
||||
)
|
||||
|
||||
# 扣除积分
|
||||
new_balance = current_points - points_cost
|
||||
user.points = new_balance
|
||||
|
||||
# 记录积分历史
|
||||
points_history = PointsHistory(
|
||||
user_id=user.id,
|
||||
username=data.username,
|
||||
type='consume',
|
||||
amount=-points_cost,
|
||||
balance=new_balance,
|
||||
description=f"使用{feature.feature_name}"
|
||||
)
|
||||
db.add(points_history)
|
||||
|
||||
db.commit()
|
||||
message = f"消耗{points_cost}积分,剩余{new_balance}积分"
|
||||
|
||||
# 5. 记录使用日志 (TODO: Add FeatureUsageLogs model if needed, currently skipping or adding to PointsHistory if consumed)
|
||||
# For now, we only log if points consumed. If we need separate usage log table, we need to create it.
|
||||
# The SQL version inserted into feature_usage_logs.
|
||||
# Let's assume PointsHistory covers financial aspect.
|
||||
# If we need analytics, we have analytics API.
|
||||
|
||||
# 6. 返回结果
|
||||
return {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"success": True,
|
||||
"feature_name": feature.feature_name,
|
||||
"cost_type": cost_type,
|
||||
"points_cost": points_cost,
|
||||
"vip_remaining_quota": remaining_quota if vip_type in ['vip', 'svip'] else None,
|
||||
"points_remaining": user.points,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
142
Server/app/api/v1/stats.py
Normal file
142
Server/app/api/v1/stats.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
统计接口
|
||||
功能:数据统计和分析
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Header, HTTPException
|
||||
from datetime import date, timedelta
|
||||
import pymysql
|
||||
from app.core.database import get_db_connection
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ==================== 辅助函数 ====================
|
||||
|
||||
def verify_admin_token(token: str):
|
||||
"""验证管理员Token"""
|
||||
expected_token = "admin-secret-token"
|
||||
if token != expected_token:
|
||||
raise HTTPException(status_code=401, detail="管理员Token无效")
|
||||
|
||||
# ==================== 统计功能 ====================
|
||||
|
||||
@router.get("/admin/stats/today")
|
||||
async def get_today_stats(token: str = Header(..., alias="x-admin-token")):
|
||||
"""今日统计"""
|
||||
verify_admin_token(token)
|
||||
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
today = date.today()
|
||||
|
||||
# 总用户数
|
||||
cursor.execute("SELECT COUNT(*) as total FROM users")
|
||||
total_users = cursor.fetchone()['total']
|
||||
|
||||
# 今日签到数
|
||||
cursor.execute("""
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM check_in_records
|
||||
WHERE check_in_date = %s
|
||||
""", (today,))
|
||||
checkin_count = cursor.fetchone()['count']
|
||||
|
||||
# 今日功能使用次数
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM feature_usage_logs
|
||||
WHERE DATE(created_at) = %s
|
||||
""", (today,))
|
||||
feature_usage_count = cursor.fetchone()['count']
|
||||
|
||||
# VIP用户数
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM users
|
||||
WHERE vip_type IN ('vip', 'svip')
|
||||
AND (vip_expire IS NULL OR vip_expire > NOW())
|
||||
""")
|
||||
vip_count = cursor.fetchone()['count']
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"total_users": total_users,
|
||||
"checkin_count": checkin_count,
|
||||
"feature_usage_count": feature_usage_count,
|
||||
"vip_count": vip_count
|
||||
}
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@router.get("/admin/stats/feature-usage")
|
||||
async def get_feature_usage_stats(
|
||||
days: int = 7,
|
||||
token: str = Header(..., alias="x-admin-token")
|
||||
):
|
||||
"""功能使用排行"""
|
||||
verify_admin_token(token)
|
||||
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
start_date = date.today() - timedelta(days=days-1)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
l.feature_key,
|
||||
f.feature_name,
|
||||
COUNT(*) as usage_count
|
||||
FROM feature_usage_logs l
|
||||
LEFT JOIN features_config f ON l.feature_key = f.feature_key
|
||||
WHERE DATE(l.created_at) >= %s
|
||||
GROUP BY l.feature_key, f.feature_name
|
||||
ORDER BY usage_count DESC
|
||||
LIMIT 10
|
||||
""", (start_date,))
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": cursor.fetchall()
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@router.get("/admin/stats/points-trend")
|
||||
async def get_points_trend(
|
||||
days: int = 7,
|
||||
token: str = Header(..., alias="x-admin-token")
|
||||
):
|
||||
"""积分趋势统计"""
|
||||
verify_admin_token(token)
|
||||
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
start_date = date.today() - timedelta(days=days-1)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) as earned,
|
||||
SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END) as consumed
|
||||
FROM points_history
|
||||
WHERE DATE(created_at) >= %s
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date
|
||||
""", (start_date,))
|
||||
|
||||
records = cursor.fetchall()
|
||||
for record in records:
|
||||
record['date'] = record['date'].isoformat()
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": records
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
120
Server/app/api/v1/user_profile.py
Normal file
120
Server/app/api/v1/user_profile.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户资料接口
|
||||
功能:获取和更新用户资料
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from app.db import get_db
|
||||
from app.models.user import User
|
||||
from app.models.business import PointsHistory
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ==================== 数据模型 ====================
|
||||
|
||||
class UserProfileUpdate(BaseModel):
|
||||
username: str
|
||||
nickname: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
# ==================== 用户资料管理 ====================
|
||||
|
||||
@router.get("/user/profile")
|
||||
async def get_user_profile(username: str, db: Session = Depends(get_db)):
|
||||
"""获取用户资料"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 转换为字典以便序列化
|
||||
user_data = {
|
||||
"username": user.username,
|
||||
"nickname": user.nickname,
|
||||
"avatar": user.avatar,
|
||||
"email": user.email,
|
||||
"points": user.points,
|
||||
"level": user.level,
|
||||
"vip_type": user.vip_type,
|
||||
"vip_expire": user.vip_expire.isoformat() if user.vip_expire else None,
|
||||
"vip_daily_quota": user.vip_daily_quota,
|
||||
"total_check_in_days": user.total_check_in_days,
|
||||
"consecutive_check_in": user.consecutive_check_in,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None
|
||||
}
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": user_data
|
||||
}
|
||||
|
||||
@router.put("/user/profile")
|
||||
async def update_user_profile(data: UserProfileUpdate, db: Session = Depends(get_db)):
|
||||
"""更新用户资料"""
|
||||
user = db.query(User).filter(User.username == data.username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
if data.nickname is not None:
|
||||
user.nickname = data.nickname
|
||||
if data.avatar is not None:
|
||||
user.avatar = data.avatar
|
||||
if data.email is not None:
|
||||
user.email = data.email
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "更新成功"
|
||||
}
|
||||
|
||||
# ==================== 积分历史 ====================
|
||||
|
||||
@router.get("/points/history")
|
||||
async def get_points_history(
|
||||
username: str,
|
||||
type: Optional[str] = None,
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取积分历史"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
offset = (page - 1) * limit
|
||||
|
||||
query = db.query(PointsHistory).filter(PointsHistory.user_id == user.id)
|
||||
|
||||
if type:
|
||||
query = query.filter(PointsHistory.type == type)
|
||||
|
||||
total = query.count()
|
||||
records = query.order_by(PointsHistory.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
result_records = []
|
||||
for record in records:
|
||||
result_records.append({
|
||||
"type": record.type,
|
||||
"amount": record.amount,
|
||||
"balance": record.balance,
|
||||
"description": record.description,
|
||||
"created_at": record.created_at.isoformat() if record.created_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"total": total,
|
||||
"current_balance": user.points,
|
||||
"records": result_records
|
||||
}
|
||||
}
|
||||
|
||||
20
Server/app/core/database.py
Normal file
20
Server/app/core/database.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据库连接管理
|
||||
"""
|
||||
|
||||
import pymysql
|
||||
import os
|
||||
|
||||
def get_db_connection():
|
||||
"""获取数据库连接"""
|
||||
return pymysql.connect(
|
||||
host=os.getenv('DB_HOST', 'localhost'),
|
||||
port=int(os.getenv('DB_PORT', 3306)),
|
||||
user=os.getenv('DB_USER', 'root'),
|
||||
password=os.getenv('DB_PASSWORD', ''),
|
||||
database=os.getenv('DB_NAME', 'designercep'),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.Cursor
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ def init_db():
|
||||
from app.models.user import User
|
||||
from app.models.group import PluginGroup
|
||||
from app.models.session import UserSession
|
||||
from app.models.business import FeatureConfig, VipConfig, CheckInConfig, CheckInRecord, PointsHistory
|
||||
Base.metadata.create_all(bind=engine)
|
||||
ensure_migrations()
|
||||
seed_data()
|
||||
@@ -35,15 +36,35 @@ def init_db():
|
||||
def seed_data():
|
||||
"""Ensure default data exists"""
|
||||
from app.models.group import PluginGroup
|
||||
from app.models.business import FeatureConfig, VipConfig, CheckInConfig
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Seed Groups
|
||||
default_group = db.query(PluginGroup).filter(PluginGroup.name == "default").first()
|
||||
if not default_group:
|
||||
print("Creating 'default' group...")
|
||||
new_group = PluginGroup(name="default", comment="Default User Group")
|
||||
db.add(new_group)
|
||||
db.commit()
|
||||
print("Default group created.")
|
||||
|
||||
# Seed VIP Config
|
||||
if db.query(VipConfig).count() == 0:
|
||||
print("Seeding VIP Config...")
|
||||
db.add(VipConfig(vip_type="vip", name="VIP会员", price=30.0, daily_quota=20, points_multiplier=1.5))
|
||||
db.add(VipConfig(vip_type="svip", name="SVIP会员", price=88.0, daily_quota=-1, points_multiplier=2.0))
|
||||
|
||||
# Seed Checkin Config
|
||||
if db.query(CheckInConfig).count() == 0:
|
||||
print("Seeding Checkin Config...")
|
||||
db.add(CheckInConfig(consecutive_days=1, base_points=10, bonus_points=0, total_points=10))
|
||||
db.add(CheckInConfig(consecutive_days=3, base_points=10, bonus_points=5, total_points=15))
|
||||
db.add(CheckInConfig(consecutive_days=7, base_points=10, bonus_points=20, total_points=30))
|
||||
|
||||
# Seed Features
|
||||
if db.query(FeatureConfig).count() == 0:
|
||||
print("Seeding Features...")
|
||||
db.add(FeatureConfig(feature_key="ai_remove_bg", feature_name="智能抠图", points_cost=10, category="ai"))
|
||||
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
print(f"Error seeding data: {e}")
|
||||
finally:
|
||||
@@ -97,3 +118,27 @@ def ensure_migrations():
|
||||
add_col("users", "reset_token", "VARCHAR(128) NULL")
|
||||
if not has_column("users", "reset_token_expire"):
|
||||
add_col("users", "reset_token_expire", "TIMESTAMP NULL")
|
||||
|
||||
# users profile & vip columns
|
||||
if not has_column("users", "nickname"):
|
||||
add_col("users", "nickname", "VARCHAR(50) NULL")
|
||||
if not has_column("users", "avatar"):
|
||||
add_col("users", "avatar", "VARCHAR(500) NULL")
|
||||
if not has_column("users", "points"):
|
||||
add_col("users", "points", "INTEGER DEFAULT 0")
|
||||
if not has_column("users", "level"):
|
||||
add_col("users", "level", "INTEGER DEFAULT 1")
|
||||
if not has_column("users", "vip_type"):
|
||||
add_col("users", "vip_type", "VARCHAR(20) DEFAULT 'none'")
|
||||
if not has_column("users", "vip_expire"):
|
||||
add_col("users", "vip_expire", "TIMESTAMP NULL")
|
||||
if not has_column("users", "vip_daily_quota"):
|
||||
add_col("users", "vip_daily_quota", "INTEGER DEFAULT 0")
|
||||
if not has_column("users", "vip_quota_reset_date"):
|
||||
add_col("users", "vip_quota_reset_date", "DATE NULL")
|
||||
if not has_column("users", "total_check_in_days"):
|
||||
add_col("users", "total_check_in_days", "INTEGER DEFAULT 0")
|
||||
if not has_column("users", "consecutive_check_in"):
|
||||
add_col("users", "consecutive_check_in", "INTEGER DEFAULT 0")
|
||||
if not has_column("users", "last_check_in_date"):
|
||||
add_col("users", "last_check_in_date", "DATE NULL")
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
from pathlib import Path
|
||||
from app.core.config import settings
|
||||
from app.api.v1 import auth, client, admin, analytics, jsx_demo
|
||||
from app.api.v1 import auth, client, admin, analytics, jsx_demo, admin_config, feature, checkin, user_profile, stats
|
||||
from app.db import init_db
|
||||
from datetime import datetime
|
||||
|
||||
@@ -61,6 +61,13 @@ app.include_router(admin.router, prefix=f"{settings.API_V1_STR}/admin", tags=["a
|
||||
app.include_router(analytics.router, prefix=f"{settings.API_V1_STR}/analytics", tags=["analytics"])
|
||||
app.include_router(jsx_demo.router, prefix=f"{settings.API_V1_STR}/jsx_demo", tags=["jsx_demo"])
|
||||
|
||||
# 新增路由
|
||||
app.include_router(admin_config.router, prefix=settings.API_V1_STR, tags=["admin-config"])
|
||||
app.include_router(feature.router, prefix=settings.API_V1_STR, tags=["feature"])
|
||||
app.include_router(checkin.router, prefix=settings.API_V1_STR, tags=["checkin"])
|
||||
app.include_router(user_profile.router, prefix=settings.API_V1_STR, tags=["user-profile"])
|
||||
app.include_router(stats.router, prefix=settings.API_V1_STR, tags=["stats"])
|
||||
|
||||
# Health Check
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
|
||||
87
Server/app/models/business.py
Normal file
87
Server/app/models/business.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, Float, Date, Enum, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db import Base
|
||||
import enum
|
||||
|
||||
# Enums
|
||||
class VipType(str, enum.Enum):
|
||||
none = "none"
|
||||
vip = "vip"
|
||||
svip = "svip"
|
||||
|
||||
class FeatureCategory(str, enum.Enum):
|
||||
general = "general"
|
||||
ai = "ai"
|
||||
export = "export"
|
||||
layer = "layer"
|
||||
other = "other"
|
||||
|
||||
# ========== Config Models ==========
|
||||
|
||||
class FeatureConfig(Base):
|
||||
__tablename__ = "features_config"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
feature_key = Column(String(50), unique=True, index=True, nullable=False)
|
||||
feature_name = Column(String(100), nullable=False)
|
||||
category = Column(String(50), default="general")
|
||||
points_cost = Column(Integer, default=0)
|
||||
vip_points_cost = Column(Integer, default=0)
|
||||
svip_points_cost = Column(Integer, default=0)
|
||||
enabled = Column(Boolean, default=True)
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
class VipConfig(Base):
|
||||
__tablename__ = "vip_config"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vip_type = Column(String(20), unique=True, nullable=False) # 'vip', 'svip'
|
||||
name = Column(String(50), nullable=False)
|
||||
price = Column(Float, nullable=False)
|
||||
daily_quota = Column(Integer, nullable=False) # -1 for infinite
|
||||
points_multiplier = Column(Float, default=1.0)
|
||||
enabled = Column(Boolean, default=True)
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
class CheckInConfig(Base):
|
||||
__tablename__ = "check_in_config"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
consecutive_days = Column(Integer, unique=True, nullable=False)
|
||||
base_points = Column(Integer, nullable=False)
|
||||
bonus_points = Column(Integer, nullable=False)
|
||||
total_points = Column(Integer, nullable=False)
|
||||
enabled = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# ========== Record Models ==========
|
||||
|
||||
class CheckInRecord(Base):
|
||||
__tablename__ = "check_in_records"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, index=True, nullable=False)
|
||||
username = Column(String(50), nullable=False)
|
||||
check_in_date = Column(Date, nullable=False, index=True)
|
||||
points_earned = Column(Integer, nullable=False)
|
||||
consecutive_days = Column(Integer, nullable=False)
|
||||
vip_multiplier = Column(Float, default=1.0)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class PointsHistory(Base):
|
||||
__tablename__ = "points_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, index=True, nullable=False)
|
||||
username = Column(String(50), nullable=False)
|
||||
type = Column(String(20), nullable=False) # checkin, consume, refund, admin
|
||||
amount = Column(Integer, nullable=False)
|
||||
balance = Column(Integer, nullable=False)
|
||||
description = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, func, ForeignKey, Text, Boolean
|
||||
from sqlalchemy import Column, Integer, String, DateTime, func, ForeignKey, Text, Boolean, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db import Base
|
||||
|
||||
@@ -23,4 +23,18 @@ class User(Base):
|
||||
reset_token = Column(String(128), nullable=True)
|
||||
reset_token_expire = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Profile & VIP & Check-in
|
||||
nickname = Column(String(50), nullable=True)
|
||||
avatar = Column(String(500), nullable=True)
|
||||
points = Column(Integer, default=0)
|
||||
level = Column(Integer, default=1)
|
||||
vip_type = Column(String(20), default='none') # 'none', 'vip', 'svip'
|
||||
vip_expire = Column(DateTime(timezone=True), nullable=True)
|
||||
vip_daily_quota = Column(Integer, default=0)
|
||||
vip_quota_reset_date = Column(Date, nullable=True)
|
||||
|
||||
total_check_in_days = Column(Integer, default=0)
|
||||
consecutive_check_in = Column(Integer, default=0)
|
||||
last_check_in_date = Column(Date, nullable=True)
|
||||
|
||||
group = relationship("PluginGroup", back_populates="users")
|
||||
|
||||
77
Server/app/recreate_db.py
Normal file
77
Server/app/recreate_db.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据库重建脚本
|
||||
功能:
|
||||
1. 连接到 MySQL 服务器
|
||||
2. 删除现有数据库(如果存在)
|
||||
3. 创建新数据库
|
||||
4. 根据模型定义创建所有表
|
||||
|
||||
使用方法:
|
||||
在 Server 目录下运行:
|
||||
docker-compose exec backend python -m app.recreate_db
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from app.core.config import settings
|
||||
from app.db import Base
|
||||
# 导入所有模型以确保它们被注册到 Base.metadata
|
||||
from app.models import user, group, business, session
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def recreate_database():
|
||||
logger.info("🚀 开始重建数据库...")
|
||||
|
||||
# 1. 获取数据库配置
|
||||
db_url = settings.DATABASE_URL
|
||||
|
||||
if not db_url.startswith("mysql"):
|
||||
logger.error("❌ 此脚本仅支持 MySQL 数据库重建")
|
||||
return
|
||||
|
||||
try:
|
||||
# 解析 URL
|
||||
url = make_url(db_url)
|
||||
db_name = url.database
|
||||
|
||||
# 构建连接到 MySQL 系统库的 URL (不指定具体数据库,以便执行 DROP/CREATE)
|
||||
# 我们连接到 'mysql' 库或者不指定库
|
||||
# 注意:需要有足够的权限
|
||||
root_url = url.set(database='mysql')
|
||||
|
||||
logger.info(f"🔌 连接到数据库服务器: {url.host}")
|
||||
|
||||
# 使用 AUTOCOMMIT 隔离级别,因为 CREATE/DROP DATABASE 不能在事务中运行
|
||||
engine = create_engine(root_url, isolation_level="AUTOCOMMIT")
|
||||
|
||||
with engine.connect() as conn:
|
||||
# 删除旧数据库
|
||||
logger.info(f"🗑️ 删除数据库: {db_name}")
|
||||
conn.execute(text(f"DROP DATABASE IF EXISTS {db_name}"))
|
||||
|
||||
# 创建新数据库
|
||||
logger.info(f"✨ 创建数据库: {db_name}")
|
||||
conn.execute(text(f"CREATE DATABASE {db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"))
|
||||
|
||||
# 2. 创建表结构
|
||||
logger.info("🏗️ 创建表结构...")
|
||||
|
||||
# 连接到新创建的数据库
|
||||
target_engine = create_engine(db_url)
|
||||
Base.metadata.create_all(bind=target_engine)
|
||||
|
||||
logger.info("✅ 数据库重建完成!所有表已创建。")
|
||||
logger.info("📋 包含的表: " + ", ".join(Base.metadata.tables.keys()))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 重建数据库失败: {str(e)}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
recreate_database()
|
||||
@@ -29,17 +29,34 @@ class GroupService:
|
||||
if not group:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户所属组不存在")
|
||||
|
||||
if not group.current_version_file:
|
||||
# 允许无版本的情况,或者使用默认版本
|
||||
version_file = group.current_version_file
|
||||
if not version_file or version_file == '-':
|
||||
# 尝试查找任意一个版本,或者返回空
|
||||
# 根据用户要求 "现在默认只有一个了"
|
||||
# 这里可以列出 archives 目录下的第一个文件
|
||||
import os
|
||||
try:
|
||||
archives_dir = "archives"
|
||||
files = [f for f in os.listdir(archives_dir) if f.endswith('.zip')]
|
||||
if files:
|
||||
files.sort(reverse=True) # Assuming newer versions sort higher
|
||||
version_file = files[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
if not version_file or version_file == '-':
|
||||
# 如果还是没有,则抛出异常,或者返回空 update
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="当前组无可用版本")
|
||||
|
||||
# Construct download URL (assuming base URL or relative path)
|
||||
# In a real scenario, this might be from config
|
||||
download_url = f"/download/{group.current_version_file}"
|
||||
download_url = f"/download/{version_file}"
|
||||
|
||||
# Extract version from filename roughly or use file metadata if available
|
||||
# Assuming filename format: plugin_v1.0.2.zip
|
||||
version = "unknown"
|
||||
filename = group.current_version_file
|
||||
filename = version_file
|
||||
if "v" in filename:
|
||||
try:
|
||||
# Simple extraction logic, can be improved
|
||||
|
||||
Reference in New Issue
Block a user