| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023 |
- """
- 客户端认证接口 - 统一API格式
- """
- from fastapi import APIRouter, Depends, HTTPException, Request
- from fastapi.responses import JSONResponse
- from sqlalchemy.orm import Session
- from app.database import get_db
- from app.api.deps import get_current_user_session, get_current_user_session_for_heartbeat
- from app.models.user import User
- from app.models.card import Card
- from app.models.recharge_record import RechargeRecord
- from app.models.system_config import SystemConfig
- from app.core.security import verify_password, get_password_hash
- from app.core.encrypt import encryptor
- from app.core.logger import logger
- from app.config import settings
- from app.schemas.client_api import *
- from app.services.key_rotation_service import KeyRotationService
- from app.services.session_service import SessionService
- from app.services.heartbeat_monitor import HeartbeatMonitor
- from app.services.anti_repeat_service import anti_repeat_service
- from app.utils.timestamp_validator import TimestampValidator
- from app.utils.timezone import beijing_now, datetime_to_beijing
- from datetime import datetime, timedelta
- import json
- import base64
- import hmac
- import hashlib
- from cryptography.hazmat.primitives import hashes
- from cryptography.hazmat.primitives.asymmetric import padding
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from cryptography.hazmat.backends import default_backend
- router = APIRouter()
- def _verify_hmac_signature(data: str, signature_b64: str, secret_key: str) -> bool:
- try:
- expected = hmac.new(secret_key.encode(), data.encode(), hashlib.sha256).digest()
- return base64.b64encode(expected).decode() == signature_b64
- except Exception:
- logger.exception("签名验证异常")
- return False
- def _process_mode_b_request(data: dict, db: Session) -> tuple[dict, bytes]:
- """
- 处理 Mode B 请求格式(encrypted_params + aes_key + key_version + timestamp + nonce + signature)
- 返回 (解密后的 params dict, AES密钥) 或抛出 HTTPException
- """
- try:
- required_keys = ("encrypted_params", "aes_key", "key_version", "timestamp", "nonce", "signature")
- if not all(k in data for k in required_keys):
- raise ValueError("缺少加密请求必需字段")
- encrypted_params_b64 = data.get("encrypted_params")
- encrypted_aes_key_b64 = data.get("aes_key")
- key_version = int(data.get("key_version"))
- timestamp = int(data.get("timestamp"))
- nonce = data.get("nonce")
- signature = data.get("signature")
- # 1. 验证时间戳
- is_valid, err_msg = TimestampValidator.validate_timestamp(timestamp, db)
- if not is_valid:
- raise ValueError(f"时间戳验证失败: {err_msg}")
- # 2. 验证签名(字段顺序需与 Rust 端一致)
- signature_data = f"{timestamp}:{nonce}:{encrypted_params_b64}:{encrypted_aes_key_b64}:{key_version}"
- if not _verify_hmac_signature(signature_data, signature, settings.SECRET_KEY):
- raise ValueError("签名验证失败")
- # 3. 加载私钥版本并用私钥解密 AES 密钥
- if not encryptor.load_key_by_version(db, key_version):
- raise ValueError(f"无效的密钥版本 {key_version}")
- encrypted_aes_key_bytes = base64.b64decode(encrypted_aes_key_b64)
- aes_key_bytes = encryptor.private_key.decrypt(
- encrypted_aes_key_bytes,
- padding.OAEP(
- mgf=padding.MGF1(algorithm=hashes.SHA256()),
- algorithm=hashes.SHA256(),
- label=None
- )
- )
- # 4. 解密 encrypted_params(IV(16) + payload),与 Rust 使用 CTR 模式保持一致
- encrypted_params_bytes = base64.b64decode(encrypted_params_b64)
- if len(encrypted_params_bytes) < 16:
- raise ValueError("加密参数长度无效")
- iv = encrypted_params_bytes[:16]
- encrypted_payload = encrypted_params_bytes[16:]
- cipher = Cipher(algorithms.AES(aes_key_bytes), modes.CTR(iv), backend=default_backend())
- decryptor = cipher.decryptor()
- decrypted_bytes = decryptor.update(encrypted_payload) + decryptor.finalize()
- params = json.loads(decrypted_bytes.decode("utf-8"))
- return params, aes_key_bytes
- except HTTPException:
- raise
- except Exception as e:
- logger.exception("Mode B 请求处理失败")
- raise HTTPException(status_code=400, detail=str(e))
- def process_client_request(request: Request, db: Session) -> tuple[ClientApiRequest, dict]:
- """
- 处理客户端请求:解密、验证时间戳
- Returns:
- tuple: (解析后的请求对象, 响应头)
- """
- try:
- # 获取请求数据
- data = request.json() if hasattr(request, 'json') else {}
- # 调试日志:打印原始请求数据
- logger.debug(f"[API请求] 原始请求数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
- # 解密请求数据
- if settings.ENCRYPT_ENABLED:
- encrypted_req = EncryptedRequest(**data)
- decrypted_data = encryptor.decrypt_from_client(
- encrypted_req.encrypted,
- encrypted_req.key_version,
- db
- )
- logger.debug(f"[API请求] 解密后数据: {json.dumps(decrypted_data, ensure_ascii=False, indent=2)}")
- else:
- decrypted_data = data
- logger.debug(f"[API请求] 未加密数据: {json.dumps(decrypted_data, ensure_ascii=False, indent=2)}")
- # 解析为统一格式
- api_request = ClientApiRequest(**decrypted_data)
- logger.info(f"[API请求] 解析后的请求 - 操作: {api_request.action}, 时间戳: {api_request.timestamp}, 版本: {api_request.version}")
- # 验证时间戳
- is_valid, error_msg = TimestampValidator.validate_timestamp(api_request.timestamp, db)
- if not is_valid:
- logger.warning(f"[API请求] 时间戳验证失败: {error_msg}")
- raise HTTPException(status_code=400, detail=error_msg)
- # 准备响应头
- response_headers = {}
- if settings.ENCRYPT_ENABLED:
- active_version = KeyRotationService.get_active_key_version(db)
- response_headers["X-Key-Version"] = str(active_version)
- logger.debug(f"[API请求] 响应密钥版本: {active_version}")
- return api_request, response_headers
- except Exception as e:
- logger.error(f"[API请求] 处理客户端请求失败: {e}")
- raise HTTPException(status_code=400, detail=f"请求格式错误: {str(e)}")
- def create_client_response(data: dict = None, code: int = 200, msg: str = "成功", db: Session = None) -> dict:
- """
- 创建客户端响应:统一格式化并加密
- Returns:
- dict: 响应数据(已加密)
- """
- try:
- # 创建响应对象
- response_data = ClientApiResponse(
- code=code,
- msg=msg,
- data=data,
- timestamp=TimestampValidator.generate_timestamp(),
- version="1.0"
- )
- # 转换为字典
- response_dict = response_data.dict()
- # 调试日志:打印响应数据
- logger.info(f"[API响应] 响应数据 - 状态码: {code}, 消息: {msg}, 数据大小: {len(str(data)) if data else 0} 字符")
- logger.debug(f"[API响应] 完整响应数据: {json.dumps(response_dict, ensure_ascii=False, indent=2)}")
- # 加密响应
- if settings.ENCRYPT_ENABLED and db:
- encrypted_data, key_version = encryptor.encrypt_for_client(response_dict, db)
- encrypted_response = {
- "encrypted": encrypted_data,
- "key_version": key_version
- }
- logger.debug(f"[API响应] 加密响应数据: {json.dumps(encrypted_response, ensure_ascii=False, indent=2)}")
- return encrypted_response
- return response_dict
- except Exception as e:
- logger.error(f"[API响应] 创建客户端响应失败: {e}")
- # 返回未加密的错误响应
- error_response = {
- "code": 500,
- "msg": f"响应处理失败: {str(e)}",
- "timestamp": TimestampValidator.generate_timestamp(),
- "version": "1.0"
- }
- logger.debug(f"[API响应] 错误响应数据: {json.dumps(error_response, ensure_ascii=False, indent=2)}")
- return error_response
- @router.post("/register")
- async def register(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """用户注册(仅支持加密调用)"""
- try:
- # 检查是否为 Mode B 加密格式(encrypted_params + aes_key)
- data = await request.json()
- is_mode_b = bool(data.get("encrypted_params") and data.get("aes_key"))
- if is_mode_b:
- # Mode B 加密请求(来自 Rust secure_*)
- params, aes_key_bytes = _process_mode_b_request(data, db)
- # 验证操作类型如果有(可选)
- # 解析注册参数
- register_params = RegisterRequest(**{
- "username": params.get("username"),
- "password": params.get("password"),
- "email": params.get("email"),
- "card_code": params.get("card_code") or params.get("cardCode"),
- "machine_code": params.get("machine_code") or params.get("machineCode")
- })
- else:
- # 加密请求:使用原有逻辑(模式 A)
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.REGISTER:
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析注册参数
- register_params = RegisterRequest(**api_request.params)
- # 检查用户名是否存在
- logger.info(f"[用户注册] 检查用户名是否存在: {register_params.username}")
- existing_user = db.query(User).filter(User.username == register_params.username).first()
- if existing_user:
- logger.warning(f"[用户注册] 用户名已存在: {register_params.username}")
- return create_secure_response(code=400, msg="用户名已存在", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- logger.info(f"[用户注册] 用户名可用: {register_params.username}")
- # 验证卡密
- logger.info(f"[用户注册] 检查卡密: {register_params.card_code}")
- card = db.query(Card).filter(
- Card.card_code == register_params.card_code,
- Card.status == 0
- ).first()
- if not card:
- logger.warning(f"[用户注册] 卡密无效或已被使用: {register_params.card_code}")
- return create_secure_response(code=400, msg="卡密无效或已被使用", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- logger.info(f"[用户注册] 卡密有效: {register_params.card_code}")
- # 计算到期时间
- expire_time = beijing_now() + timedelta(days=card.days)
- # 创建用户
- user = User(
- username=register_params.username,
- password=get_password_hash(register_params.password),
- email=register_params.email,
- machine_code=register_params.machine_code, # 可能为None,第一次登录时绑定
- card_id=card.id,
- expire_time=expire_time,
- status=1,
- created_at=beijing_now(),
- updated_at=beijing_now()
- )
- db.add(user)
- db.flush() # 刷新以获取user.id
-
- # 更新卡密状态
- card.user_id = user.id
- card.activated_at = beijing_now()
- card.expire_time = expire_time
- card.status = 1
- card.updated_at = beijing_now()
-
- # 创建充值记录
- recharge_record = RechargeRecord(
- user_id=user.id,
- card_id=card.id,
- software_code=card.software_code,
- days=card.days,
- expire_time_before=None, # 新用户,之前没有到期时间
- expire_time_after=expire_time,
- created_at=beijing_now()
- )
- db.add(recharge_record)
- db.flush() # 刷新以获取recharge_record.id
- # 创建历史记录
- from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
- create_user_history(db, user, "INSERT")
- create_card_history(db, card, "UPDATE")
- create_recharge_record_history(db, recharge_record, "INSERT")
- # 一次性提交所有更改
- db.commit()
- logger.info(f"[用户注册] 数据库提交成功,用户ID: {user.id}")
- # 准备响应数据
- response_data = RegisterResponseData(user_id=user.id)
- logger.info(f"用户注册成功: {register_params.username}")
- return create_secure_response(data=response_data.dict(), aes_key=aes_key_bytes, key_version=data.get("key_version"))
- except Exception as e:
- db.rollback()
- logger.error(f"用户注册失败: {e}")
- return create_secure_response(code=500, msg="注册失败", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/login")
- async def login(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """用户登录(仅支持加密调用)"""
- try:
- logger.info("[用户登录] 开始处理登录请求")
- # 检查是否为 Mode B 加密格式
- data = await request.json()
- is_mode_b = bool(data.get("encrypted_params") and data.get("aes_key"))
- if is_mode_b:
- # Mode B 加密请求(来自 Rust secure_*)
- params, aes_key_bytes = _process_mode_b_request(data, db)
- login_params = LoginRequest(**{
- "username": params.get("username"),
- "password": params.get("password"),
- "machine_code": params.get("machine_code") or params.get("machineCode")
- })
- else:
- # 不支持的请求格式
- logger.warning("[用户登录] 不支持的请求格式:仅支持Mode B加密调用")
- return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
- logger.info(f"[用户登录] 用户名: {login_params.username}, 机器码: {login_params.machine_code}")
- # 获取系统配置(使用 KeyRotationService 提供的配置字典)
- # 注意:KeyRotationService 暴露的是 get_config_dict,而非 get_config
- config = KeyRotationService.get_config_dict()
- # 查找用户
- user = db.query(User).filter(User.username == login_params.username).first()
- if not user:
- logger.warning(f"[用户登录] 用户不存在: {login_params.username}")
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证密码
- if not verify_password(login_params.password, user.password):
- logger.warning(f"[用户登录] 密码验证失败: {login_params.username}")
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证/绑定机器码
- if user.machine_code:
- if user.machine_code != login_params.machine_code:
- logger.warning(f"[用户登录] 机器码不匹配 - 用户: {user.machine_code}, 请求: {login_params.machine_code}")
- if is_plaintext:
- return {"code": 403, "msg": "机器码不匹配,请先解绑"}
- else:
- return create_client_response(code=403, msg="机器码不匹配,请先解绑", db=db)
- else:
- # 用户已解绑或尚未绑定机器码,第一次登录时绑定当前机器码
- user.machine_code = login_params.machine_code
- logger.info(f"[用户登录] 绑定新机器码: {login_params.machine_code}")
- # 检查是否过期
- current_time = beijing_now().replace(tzinfo=None) # 转换为naive datetime进行比较
- if user.expire_time and user.expire_time < current_time:
- logger.warning(f"[用户登录] 用户卡密已过期: {user.expire_time}")
- return create_secure_response(code=403, msg="卡密已过期", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 获取客户端IP和User-Agent
- client_ip = request.client.host if request.client else None
- user_agent = request.headers.get("User-Agent")
- # 创建用户会话
- logger.debug(f"[用户登录] 创建会话 - 用户ID: {user.id}, IP: {client_ip}")
- session_success, session_msg, session_id = SessionService.create_session(
- db=db,
- user_id=user.id,
- machine_code=login_params.machine_code,
- ip_address=client_ip,
- user_agent=user_agent
- )
- if not session_success:
- logger.error(f"[用户登录] 创建会话失败: {session_msg}")
- return create_secure_response(code=500, msg=f"创建会话失败: {session_msg}", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 更新登录信息
- user.last_login_time = beijing_now()
- user.last_login_ip = client_ip
- user.updated_at = beijing_now()
- db.commit()
- # 准备响应数据
- response_data = LoginResponseData(
- user_id=user.id,
- username=user.username,
- session_id=session_id,
- expire_time=user.expire_time.isoformat() if user.expire_time else None,
- machine_code=user.machine_code
- )
- logger.info(f"[用户登录] 登录成功 - 用户: {login_params.username}, SessionID: {session_id[:8]}...")
- return create_secure_response(data=response_data.dict(), aes_key=aes_key_bytes, key_version=data.get("key_version"))
- except Exception as e:
- # 回滚数据库事务以避免半提交状态
- try:
- db.rollback()
- except Exception:
- # 回滚失败不影响主流程,继续记录异常
- logger.warning("回滚数据库事务时发生异常")
- # 记录完整堆栈以便排查
- logger.exception("用户登录失败")
- # 返回给客户端一个友好的错误提示(不暴露内部细节)
- return create_secure_response(code=500, msg="系统异常,登录失败,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/unbind")
- async def unbind(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """机器解绑(仅支持加密调用)"""
- try:
- # 获取请求数据
- data = await request.json()
- # 仅支持 Mode B 加密格式
- if data.get("encrypted_params") and data.get("aes_key"):
- params, aes_key_bytes = _process_mode_b_request(data, db)
- req = UnbindRequest(**{
- "username": params.get("username"),
- "password": params.get("password")
- })
- else:
- # 不支持的请求格式
- logger.warning("[机器解绑] 不支持的请求格式:仅支持Mode B加密调用")
- return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
- # 检查重复调用
- allow_call, error_msg = await anti_repeat_service.check_and_set_repeat_lock(
- "unbind", req.username, lock_duration=5
- )
- if not allow_call:
- logger.warning(f"[机器解绑] 重复调用检测: {req.username} - {error_msg}")
- return create_secure_response(code=429, msg=error_msg, aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 查找用户
- user = db.query(User).filter(User.username == req.username).first()
- if not user:
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证密码
- if not verify_password(req.password, user.password):
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 解绑机器码
- user.machine_code = None
- user.updated_at = beijing_now()
- db.commit()
- logger.info(f"机器解绑成功: {req.username}")
- return create_secure_response(code=200, msg="解绑成功", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- except Exception as e:
- try:
- db.rollback()
- except Exception:
- logger.warning("回滚数据库事务时发生异常")
- logger.exception("机器解绑失败")
- return create_secure_response(code=500, msg="系统异常,操作未完成,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/recharge")
- async def recharge(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """卡密充值(仅支持加密调用)"""
- try:
- # 获取请求数据(仅支持 Mode B 加密格式)
- data = await request.json()
- if data.get("encrypted_params") and data.get("aes_key"):
- params, aes_key_bytes = _process_mode_b_request(data, db)
- req = {
- "username": params.get("username"),
- "password": params.get("password"),
- "card_code": params.get("card_code") or params.get("cardCode")
- }
- else:
- # 不支持的请求格式
- logger.warning("[卡密充值] 不支持的请求格式:仅支持Mode B加密调用")
- return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
- # 验证必填字段
- if not req["username"] or not req["password"] or not req["card_code"]:
- return {"code": 400, "msg": "请填写完整的充值信息"}
- # 检查重复调用
- allow_call, error_msg = await anti_repeat_service.check_and_set_repeat_lock(
- "recharge", req["username"], lock_duration=5
- )
- if not allow_call:
- logger.warning(f"[卡密充值] 重复调用检测: {req['username']} - {error_msg}")
- return create_secure_response(code=429, msg=error_msg, aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 查找用户
- user = db.query(User).filter(User.username == req["username"]).first()
- if not user:
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证密码
- if not verify_password(req["password"], user.password):
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证卡密
- card = db.query(Card).filter(
- Card.card_code == req["card_code"],
- Card.status == 0
- ).first()
- if not card:
- return create_secure_response(code=400, msg="卡密无效或已被使用", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 计算新的到期时间
- expire_time_before = user.expire_time
- if expire_time_before and datetime_to_beijing(expire_time_before) > beijing_now():
- # 如果还有有效期,则累加
- expire_time_after = expire_time_before + timedelta(days=card.days)
- else:
- # 如果已过期或没有有效期,则从当前时间开始计算
- expire_time_after = beijing_now() + timedelta(days=card.days)
- # 更新用户到期时间
- user.expire_time = expire_time_after
- user.updated_at = beijing_now()
- # 更新卡密状态
- card.user_id = user.id
- card.activated_at = beijing_now()
- card.expire_time = expire_time_after
- card.status = 1
- card.updated_at = beijing_now()
- # 创建充值记录
- recharge_record = RechargeRecord(
- user_id=user.id,
- card_id=card.id,
- software_code=card.software_code,
- days=card.days,
- expire_time_before=expire_time_before,
- expire_time_after=expire_time_after,
- created_at=beijing_now()
- )
- db.add(recharge_record)
- # 先提交主记录以获取ID
- db.commit()
- # 刷新对象以获取生成的ID
- db.refresh(recharge_record)
- # 创建历史记录
- from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
- create_user_history(db, user, "UPDATE")
- create_card_history(db, card, "UPDATE")
- create_recharge_record_history(db, recharge_record, "INSERT")
- # 提交历史记录
- db.commit()
- logger.info(f"用户 {req['username']} 卡密充值成功: {req['card_code']}")
- return create_secure_response(
- code=200,
- msg="充值成功",
- data={
- "expire_time": expire_time_after.isoformat(),
- "days": card.days
- },
- aes_key=aes_key_bytes,
- key_version=data.get("key_version")
- )
- except Exception as e:
- try:
- db.rollback()
- except Exception:
- logger.warning("回滚数据库事务时发生异常")
- logger.exception("卡密充值失败")
- return create_secure_response(code=500, msg="系统异常,充值失败,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/heartbeat")
- async def heartbeat(
- user_session: tuple[User, Session] = Depends(get_current_user_session_for_heartbeat),
- db: Session = Depends(get_db)
- ):
- """心跳检测"""
- try:
- user, session = user_session
- # 记录心跳日志
- logger.info(f"[心跳检测] 用户: {user.username}(ID:{user.id}), Session: {session.session_id[:8]}...")
- # 记录心跳成功
- HeartbeatMonitor.record_heartbeat_success(db, user.id, session.session_id)
- # 检查用户是否过期
- if user.expire_time and datetime_to_beijing(user.expire_time) < beijing_now():
- logger.warning(f"用户 {user.username} 卡密已过期")
- return create_client_response(code=403, msg="卡密已过期", db=db)
- # 检查用户状态
- if user.status != 1:
- logger.warning(f"用户 {user.username} 状态异常: {user.status}")
- return create_client_response(code=403, msg="账号状态异常", db=db)
- # 更新会话活动时间(已在依赖注入中自动更新)
- # SessionService.validate_session 中已经调用了 update_activity
- # 获取系统配置
- from app.services.session_service import SessionService
- config = SessionService.get_session_config(db)
- # 返回心跳响应,包含更多状态信息
- response_data = HeartbeatResponseData(
- expire_time=user.expire_time.isoformat() if user.expire_time else None
- )
- # 获取session延长配置
- session_config = SessionService.get_session_config(db)
- # 获取客户端配置(通过心跳实时更新)
- from app.models.system_config import SystemConfig
- client_configs = {
- "api_timeout_seconds": SystemConfig.get_config_value('api_timeout_seconds', 30),
- "client_heartbeat_interval_seconds": SystemConfig.get_config_value('client_heartbeat_interval_seconds', 30),
- "max_heartbeat_failures": SystemConfig.get_config_value('max_heartbeat_failures', 10),
- "max_network_failures": SystemConfig.get_config_value('max_network_failures', 15),
- "sweep_batch_size": SystemConfig.get_config_value('sweep_batch_size', 10),
- "sweep_purchase_interval": SystemConfig.get_config_value('sweep_purchase_interval', 30),
- "sweep_max_purchases": SystemConfig.get_config_value('sweep_max_purchases', 100),
- "page_size": SystemConfig.get_config_value('page_size', 10),
- "steam_auth_countdown_seconds": SystemConfig.get_config_value('steam_auth_countdown_seconds', 30),
- }
- # 获取当前激活的密钥信息
- from app.models.api_key import APIKey
- current_key = db.query(APIKey).filter(APIKey.is_active == 1).first()
- key_info = None
- if current_key:
- key_info = {
- "key_version": current_key.key_version,
- "key_updated_at": current_key.updated_at.isoformat() if current_key.updated_at else None,
- }
- # 添加扩展响应数据
- extended_data = response_data.dict()
- extended_data.update({
- "server_time": beijing_now().isoformat(),
- "session_status": "active",
- "next_heartbeat": client_configs.get("client_heartbeat_interval_seconds", 30),
- "session_expires_at": session.expires_at.isoformat(),
- "session_extend_enabled": session_config.get("session_extend_enabled", True),
- "session_extend_threshold_hours": session_config.get("session_extend_threshold_hours", 4),
- # 实时配置更新
- "client_configs": client_configs,
- # 密钥版本信息(用于客户端密钥同步)
- "key_info": key_info
- })
- logger.debug(f"用户 {user.username} 心跳检测成功")
- return create_client_response(data=extended_data, db=db)
- except Exception as e:
- logger.error(f"心跳检测失败: {e}")
- return create_client_response(code=500, msg="心跳检测失败", db=db)
- def create_secure_response(data: dict = None, code: int = 200, msg: str = "成功", aes_key: bytes = None, key_version: int = None) -> dict:
- """
- 创建安全通信响应:使用前端提供的AES密钥加密
- Args:
- data: 响应数据
- code: 响应码
- msg: 响应消息
- aes_key: 前端提供的AES密钥(32字节)
- key_version: 密钥版本
- Returns:
- dict: 加密后的响应
- """
- try:
- # 创建响应对象
- response_data = ClientApiResponse(
- code=code,
- msg=msg,
- data=data,
- timestamp=TimestampValidator.generate_timestamp(),
- version="1.0"
- )
- # 转换为字典
- response_dict = response_data.dict()
- logger.debug(f"[安全响应] 原始响应数据: {json.dumps(response_dict, ensure_ascii=False, indent=2)}")
- # 使用前端提供的AES密钥和CTR模式加密(与公告API完全一致)
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from cryptography.hazmat.backends import default_backend
- # 生成随机IV(与公告API完全一致)
- import secrets
- response_iv = secrets.token_bytes(16)
- logger.debug(f"[安全响应] AES密钥长度: {len(aes_key)}, AES密钥前8字节: {aes_key[:8].hex()}, 随机IV: {response_iv.hex()}")
- cipher = Cipher(algorithms.AES(aes_key), modes.CTR(response_iv), backend=default_backend())
- encryptor_obj = cipher.encryptor()
- data_bytes = json.dumps(response_dict, ensure_ascii=False).encode('utf-8')
- logger.debug(f"[安全响应] JSON数据长度: {len(data_bytes)}, 内容: {data_bytes[:200]}...")
- # CTR模式不需要填充
- encrypted_response = encryptor_obj.update(data_bytes) + encryptor_obj.finalize()
- logger.debug(f"[安全响应] 加密payload长度: {len(encrypted_response)}")
- # 组合:IV(16) + 加密数据(与公告API完全一致)
- encrypted_response_with_iv = response_iv + encrypted_response
- logger.debug(f"[安全响应] 组合结果长度: {len(encrypted_response_with_iv)}")
- # Base64编码
- encrypted_data = base64.b64encode(encrypted_response_with_iv).decode('utf-8')
- logger.debug(f"[安全响应] Base64结果长度: {len(encrypted_data)}")
- logger.debug(f"[安全响应] 加密响应数据: {json.dumps({'encrypted': encrypted_data, 'key_version': key_version}, ensure_ascii=False, indent=2)}")
- return {
- "encrypted": encrypted_data,
- "key_version": key_version
- }
- except Exception as e:
- logger.error(f"[安全响应] 创建安全响应失败: {e}")
- # 返回未加密的错误响应
- error_response = {
- "code": 500,
- "msg": f"响应处理失败: {str(e)}",
- "timestamp": TimestampValidator.generate_timestamp(),
- "version": "1.0"
- }
- return error_response
- @router.post("/logout")
- async def logout(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """用户登出"""
- try:
- logger.info("[用户登出] 开始处理登出请求")
- # 处理客户端请求
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.LOGOUT:
- logger.warning(f"[用户登出] 操作类型不匹配: {api_request.action}")
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析登出参数
- logout_params = LogoutRequest(**api_request.params)
- logger.info(f"[用户登出] 用户ID: {logout_params.user_id}, Session: {logout_params.session_id[:8]}...")
- # 验证会话(确保是当前用户的有效会话)
- is_valid, session, error_msg = SessionService.validate_session(db, logout_params.session_id)
- if not is_valid:
- logger.warning(f"[用户登出] 会话验证失败: {error_msg}")
- # 即使会话验证失败,也允许登出(可能是会话已过期但用户主动登出)
- logger.info("[用户登出] 会话验证失败但仍允许登出")
- # 停用会话
- if session:
- SessionService.deactivate_session(db, logout_params.session_id)
- logger.info(f"[用户登出] 用户 {logout_params.user_id} 的会话已停用")
- # 记录心跳监控:清除用户的缓存状态
- HeartbeatMonitor.reset_user_heartbeat(logout_params.user_id, logout_params.session_id)
- # 记录登出日志(可以后续在API日志中体现)
- logger.info(f"[用户登出] 用户 {logout_params.user_id} 成功登出")
- return create_client_response(msg="登出成功", db=db)
- except Exception as e:
- logger.error(f"[用户登出] 登出失败: {e}")
- return create_client_response(code=500, msg="登出失败", db=db)
- @router.post("/validate-session")
- async def validate_session(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """验证用户会话(用于自动登录验证)"""
- try:
- logger.info("[会话验证] 开始验证用户会话")
- # 处理客户端请求
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.VALIDATE_SESSION:
- logger.warning(f"[会话验证] 操作类型不匹配: {api_request.action}")
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析验证参数
- validate_params = ValidateSessionRequest(**api_request.params)
- logger.info(f"[会话验证] 验证会话ID: {validate_params.session_id[:8]}...")
- # 验证会话
- is_valid, session, error_msg = SessionService.validate_session(db, validate_params.session_id)
- if not is_valid:
- logger.warning(f"[会话验证] 会话验证失败: {error_msg}")
- return create_client_response(code=401, msg=f"会话验证失败: {error_msg}", db=db)
- # 获取用户信息
- user = db.query(User).filter(User.id == session.user_id).first()
- if not user:
- logger.warning("[会话验证] 用户不存在")
- return create_client_response(code=401, msg="用户不存在", db=db)
- # 检查用户是否过期
- if user.expire_time and datetime_to_beijing(user.expire_time) < beijing_now():
- logger.warning(f"[会话验证] 用户 {user.username} 卡密已过期")
- return create_client_response(code=403, msg="卡密已过期", db=db)
- # 检查用户状态
- if user.status != 1:
- logger.warning(f"[会话验证] 用户 {user.username} 状态异常: {user.status}")
- return create_client_response(code=403, msg="账号状态异常", db=db)
- # 更新会话活动时间
- SessionService.update_session_activity(db, session.session_id)
- # 准备响应数据
- response_data = ValidateSessionResponseData(
- user_id=user.id,
- username=user.username,
- expire_time=user.expire_time.isoformat() if user.expire_time else None,
- is_active=user.status == 1,
- machine_code=user.machine_code or ""
- )
- logger.info(f"[会话验证] 用户 {user.username} 会话验证成功")
- return create_client_response(data=response_data.dict(), db=db)
- except Exception as e:
- logger.error(f"[会话验证] 验证失败: {e}")
- return create_client_response(code=500, msg="会话验证失败", db=db)
- @router.post("/activate-card")
- async def activate_card(
- user: User = Depends(get_current_user_session),
- request: Request = None,
- db: Session = Depends(get_db)
- ):
- """激活卡密(充值)"""
- try:
- logger.info(f"[卡密激活] 开始处理激活请求 - 用户: {user.username}(ID:{user.id})")
- # 处理客户端请求
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.ACTIVATE_CARD:
- logger.warning(f"[卡密激活] 操作类型不匹配: {api_request.action}")
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析激活参数
- activate_params = ActivateCardRequest(**api_request.params)
- logger.info(f"[卡密激活] 激活参数 - 用户ID: {activate_params.user_id}, 卡密: {activate_params.card_code[:8]}...")
- # 验证用户ID是否匹配当前登录用户
- if activate_params.user_id != user.id:
- return create_client_response(code=403, msg="无权操作其他用户", db=db)
-
- # 验证卡密
- card = db.query(Card).filter(
- Card.card_code == activate_params.card_code,
- Card.status == 0
- ).first()
- if not card:
- return create_client_response(code=400, msg="卡密无效或已被使用", db=db)
- # 检查软件特征码是否匹配
- software_code = api_request.params.get("software_code", "")
- if card.software_code != software_code:
- return create_client_response(code=400, msg="卡密与当前软件不匹配", db=db)
-
- # 计算新的到期时间
- expire_time_before = user.expire_time
- if expire_time_before and datetime_to_beijing(expire_time_before) > beijing_now():
- # 如果还有有效期,则累加
- expire_time_after = expire_time_before + timedelta(days=card.days)
- else:
- # 如果已过期或没有有效期,则从当前时间开始计算
- expire_time_after = beijing_now() + timedelta(days=card.days)
- # 更新用户到期时间
- user.expire_time = expire_time_after
- user.updated_at = beijing_now()
-
- # 更新卡密状态
- card.user_id = user.id
- card.activated_at = beijing_now()
- card.expire_time = expire_time_after
- card.status = 1
- card.updated_at = beijing_now()
-
- # 创建充值记录
- recharge_record = RechargeRecord(
- user_id=user.id,
- card_id=card.id,
- software_code=card.software_code,
- days=card.days,
- expire_time_before=expire_time_before,
- expire_time_after=expire_time_after,
- created_at=beijing_now()
- )
- db.add(recharge_record)
- # 创建历史记录
- from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
- create_user_history(db, user, "UPDATE")
- create_card_history(db, card, "UPDATE")
- create_recharge_record_history(db, recharge_record, "INSERT")
- # 一次性提交所有更改
- db.commit()
- # 准备响应数据
- response_data = ActivateCardResponseData(
- expire_time=expire_time_after.isoformat(),
- days=card.days
- )
- logger.info(f"用户 {user.username} 激活卡密成功: {activate_params.card_code}")
- return create_client_response(data=response_data.dict(), db=db)
- except Exception as e:
- db.rollback()
- logger.error(f"激活卡密失败: {e}")
- return create_client_response(code=500, msg="激活失败", db=db)
- @router.get("/recharge-records")
- async def get_recharge_records(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """获取充值记录"""
- try:
- # 获取请求参数
- params = dict(request.query_params)
-
- # 解密请求数据(如果有)
- user_id = None
- if settings.ENCRYPT_ENABLED:
- # GET请求可能通过query参数传递加密数据
- encrypted_data = params.get("encrypted", "")
- if encrypted_data:
- decrypted = encryptor.decrypt(encrypted_data)
- user_id = decrypted.get("user_id")
- else:
- user_id = params.get("user_id")
- if user_id:
- user_id = int(user_id)
-
- if not user_id:
- return {"code": 400, "msg": "缺少用户ID"}
-
- # 查询充值记录
- records = db.query(RechargeRecord).filter(
- RechargeRecord.user_id == user_id,
- RechargeRecord.is_deleted == 0
- ).order_by(RechargeRecord.created_at.desc()).all()
-
- # 格式化返回数据
- records_data = []
- for record in records:
- # 获取卡密代码
- card_code = ""
- if record.card_id:
- card = db.query(Card).filter(Card.id == record.card_id).first()
- if card:
- card_code = card.card_code
-
- records_data.append({
- "id": record.id,
- "card_code": card_code,
- "days": record.days,
- "expire_time_before": record.expire_time_before.isoformat() if record.expire_time_before else None,
- "expire_time_after": record.expire_time_after.isoformat() if record.expire_time_after else None,
- "created_at": record.created_at.isoformat() if record.created_at else None
- })
-
- return {
- "code": 200,
- "msg": "成功",
- "data": records_data
- }
-
- except Exception as e:
- logger.exception("获取充值记录失败")
- return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|