# -*- coding: utf-8 -*- """次数卡密生成 / 兑换 / 管理。 (自 auth_system.py 拆出,行为与原实现一致;DB 访问已迁移为原生异步。) """ import secrets import time from typing import Any from .db import ( _q, write_transaction, ) # ============ 卡密 ============ async def generate_cards(count: int, credits: int, created_by: str = '', prefix: str = '', note: str = '', valid_days: int = 0) -> list[str]: """生成次数卡密(credits = 每张卡可下载次数) prefix: 自定义卡密前缀(默认 VIP-) note: 备注信息 valid_days: 有效天数(0 = 永久有效,过期后不可兑换) """ count = max(1, min(int(count), 500)) prefix = ''.join(ch for ch in (prefix or 'VIP') if ch.isalnum()).upper()[:10] or 'VIP' expire_at = int(time.time()) + int(valid_days) * 86400 if valid_days and int(valid_days) > 0 else 0 codes = [] async with write_transaction() as conn: for _ in range(count): code = prefix + '-' + secrets.token_hex(5).upper() await conn.execute( 'INSERT INTO cards (code, minutes, created_by, created_at, note, expire_at) ' 'VALUES (?, ?, ?, ?, ?, ?)', (code, int(credits), created_by, int(time.time()), note or '', expire_at)) codes.append(code) return codes async def list_cards(limit: int = 100, offset: int = 0) -> tuple[list[dict[str, Any]], int]: """卡密列表(支持分页 limit/offset,管理路由透传) 返回 (当页卡密列表, 卡密总数)——总数供前端分页控件用,调用方按元组解包""" limit = max(1, min(int(limit), 500)) offset = max(0, int(offset)) rows = await _q('SELECT * FROM cards ORDER BY id DESC LIMIT ? OFFSET ?', (limit, offset)) total_row = await _q('SELECT COUNT(*) AS c FROM cards') total = int(total_row[0]['c']) if total_row else 0 now = time.time() cards = [] for r in rows: d = dict(r) # 过期判定以服务端时钟为准(前端时钟偏差会误标已过期/未使用) d['expired'] = bool(d.get('expire_at') and d['expire_at'] < now) cards.append(d) return cards, total async def set_card_disabled(code: str, disabled: bool) -> None: """禁用/恢复未使用卡密(已使用卡密无禁用意义,拒绝操作)""" code = (code or '').strip().upper() async with write_transaction() as conn: row = await (await conn.execute('SELECT used_by, disabled FROM cards WHERE code = ?', (code,))).fetchone() if not row: raise ValueError('卡密不存在') if row['used_by']: raise ValueError('卡密已被使用,无需禁用') if bool(row['disabled']) == bool(disabled): raise ValueError('卡密已处于该状态') await conn.execute('UPDATE cards SET disabled = ? WHERE code = ?', (1 if disabled else 0, code)) async def delete_card(code: str) -> None: """删除未使用卡密(已使用卡密含兑换凭证:使用者/时间,按 owner 规则不可删)""" code = (code or '').strip().upper() async with write_transaction() as conn: row = await (await conn.execute('SELECT used_by FROM cards WHERE code = ?', (code,))).fetchone() if not row: raise ValueError('卡密不存在') if row['used_by']: raise ValueError('已使用的卡密不可删除(保留兑换凭证)') await conn.execute('DELETE FROM cards WHERE code = ?', (code,)) async def redeem_card(code: str, username: str) -> tuple[int, int]: """兑换卡密,返回 (卡密次数, 新剩余次数)(标记已用与加余额同事务,失败整体回滚不吞卡)""" code = (code or '').strip().upper() async with write_transaction() as conn: row = await (await conn.execute('SELECT * FROM cards WHERE code = ?', (code,))).fetchone() if not row: raise ValueError('卡密不存在') if row['used_by']: raise ValueError('卡密已被使用') if row['disabled']: raise ValueError('卡密已被禁用,请联系管理员') if row['expire_at'] and row['expire_at'] < time.time(): raise ValueError('卡密已过期') # 卡密标记 + 余额增加同事务:任一失败整体回滚,卡密可重试兑换。 # WHERE 带 used_by=""/disabled=0 防重标记 + rowcount 校验:命中 0 行 # (跨进程并发抢卡)立即回滚报错,绝不出现"未标记已用却加了余额" cur = await conn.execute( 'UPDATE cards SET used_by = ?, used_at = ? ' 'WHERE code = ? AND used_by = "" AND disabled = 0', (username, int(time.time()), code)) if cur.rowcount != 1: raise ValueError('卡密已被使用') urow = await (await conn.execute('SELECT role, duration_seconds FROM users WHERE username = ?', (username,))).fetchone() if not urow: raise ValueError('用户不存在') minutes = int(row['minutes']) remaining = int(urow['duration_seconds'] or 0) if urow['role'] == 'admin' or remaining == -1: remaining = -1 else: remaining = remaining + minutes await conn.execute('UPDATE users SET duration_seconds = ? WHERE username = ?', (remaining, username)) return minutes, remaining