This commit is contained in:
zuowei1216
2025-12-22 21:06:29 +08:00
parent 8ea58fe480
commit 1b19ff1b92
179 changed files with 21895 additions and 3774 deletions

View 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": "删除成功"}

View File

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

View 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
}

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

View 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
}
}

View 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
)

View File

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

View File

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

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

View File

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

View File

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