cards.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. """
  2. 卡密管理接口
  3. """
  4. from fastapi import APIRouter, Depends, Query, Request
  5. from fastapi.responses import StreamingResponse
  6. from sqlalchemy.orm import Session
  7. from pydantic import BaseModel
  8. import secrets
  9. import string
  10. import csv
  11. import io
  12. from app.database import get_db
  13. from app.api.deps import get_current_admin
  14. from app.models.card import Card
  15. from datetime import datetime
  16. from app.utils.timezone import beijing_now
  17. router = APIRouter()
  18. class CardGenerateRequest(BaseModel):
  19. """生成卡密请求"""
  20. software_code: str
  21. days: int
  22. discount: float
  23. count: int
  24. def generate_card_code() -> str:
  25. """生成卡密代码"""
  26. characters = string.ascii_uppercase + string.digits
  27. return ''.join(secrets.choice(characters) for _ in range(16))
  28. @router.post("/generate")
  29. async def generate_cards(
  30. req: CardGenerateRequest,
  31. request: Request,
  32. db: Session = Depends(get_db),
  33. current_admin = Depends(get_current_admin)
  34. ):
  35. """生成卡密"""
  36. try:
  37. cards = []
  38. for _ in range(req.count):
  39. card_code = generate_card_code()
  40. # 确保卡密唯一
  41. while db.query(Card).filter(Card.card_code == card_code).first():
  42. card_code = generate_card_code()
  43. card = Card(
  44. card_code=card_code,
  45. software_code=req.software_code,
  46. days=req.days,
  47. discount=req.discount,
  48. status=0,
  49. created_at=beijing_now(),
  50. updated_at=beijing_now()
  51. )
  52. db.add(card)
  53. cards.append(card)
  54. # 刷新以获取ID
  55. db.flush()
  56. # 创建历史记录
  57. from app.utils.history import create_card_history, create_operation_log
  58. for card in cards:
  59. create_card_history(db, card, "INSERT")
  60. # 记录操作日志
  61. client_ip = request.client.host if request.client else None
  62. create_operation_log(
  63. db=db,
  64. admin_id=current_admin.id,
  65. module="卡密管理",
  66. action="生成卡密",
  67. description=f"批量生成卡密:{req.count}张,软件:{req.software_code},天数:{req.days}天",
  68. new_data={
  69. "software_code": req.software_code,
  70. "days": req.days,
  71. "discount": req.discount,
  72. "count": req.count,
  73. "card_codes": [c.card_code for c in cards]
  74. },
  75. ip_address=client_ip
  76. )
  77. # 一次性提交所有更改
  78. db.commit()
  79. return {
  80. "code": 200,
  81. "msg": "生成成功",
  82. "data": {
  83. "count": len(cards),
  84. "cards": [c.card_code for c in cards]
  85. }
  86. }
  87. except Exception as e:
  88. try:
  89. db.rollback()
  90. except Exception:
  91. # 回滚失败记录警告但继续处理
  92. pass
  93. logger.exception("生成卡密失败")
  94. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  95. @router.get("")
  96. async def get_cards(
  97. page: int = Query(1, ge=1),
  98. page_size: int = Query(10, ge=1, le=100),
  99. keyword: str = Query(None, description="搜索关键词(卡密代码)"),
  100. software_code: str = Query(None, description="软件特征码筛选"),
  101. status: int = Query(None, description="状态筛选:0-未激活,1-已激活,2-已过期"),
  102. db: Session = Depends(get_db),
  103. current_admin = Depends(get_current_admin)
  104. ):
  105. """获取卡密列表"""
  106. from sqlalchemy import or_
  107. from app.models.software import Software
  108. from app.models.user import User
  109. offset = (page - 1) * page_size
  110. # 构建查询,关联软件表和用户表
  111. query = db.query(Card, Software.name.label('software_name'), User.username.label('username'))\
  112. .join(Software, Card.software_code == Software.code)\
  113. .outerjoin(User, Card.user_id == User.id)\
  114. .filter(Card.is_deleted == 0)
  115. # 搜索
  116. if keyword:
  117. query = query.filter(Card.card_code.like(f"%{keyword}%"))
  118. # 筛选
  119. if software_code:
  120. query = query.filter(Card.software_code == software_code)
  121. if status is not None:
  122. query = query.filter(Card.status == status)
  123. # 排序
  124. query = query.order_by(Card.created_at.desc())
  125. cards_with_names = query.offset(offset).limit(page_size).all()
  126. total = query.count()
  127. return {
  128. "code": 200,
  129. "msg": "成功",
  130. "data": [
  131. {
  132. "id": c.id,
  133. "card_code": c.card_code,
  134. "software_code": c.software_code,
  135. "software_name": software_name,
  136. "days": c.days,
  137. "discount": float(c.discount),
  138. "status": c.status,
  139. "user_id": c.user_id,
  140. "username": username,
  141. "activated_at": c.activated_at.isoformat() if c.activated_at else None,
  142. "expire_time": c.expire_time.isoformat() if c.expire_time else None,
  143. "created_at": c.created_at.isoformat() if c.created_at else None,
  144. "updated_at": c.updated_at.isoformat() if c.updated_at else None
  145. }
  146. for c, software_name, username in cards_with_names
  147. ],
  148. "total": total,
  149. "page": page,
  150. "page_size": page_size
  151. }
  152. @router.get("/{card_id}")
  153. async def get_card_detail(
  154. card_id: int,
  155. db: Session = Depends(get_db),
  156. current_admin = Depends(get_current_admin)
  157. ):
  158. """获取卡密详情"""
  159. from app.models.user import User
  160. card = db.query(Card).filter(Card.id == card_id).first()
  161. if not card:
  162. return {"code": 404, "msg": "卡密不存在"}
  163. user_info = None
  164. if card.user_id:
  165. user = db.query(User).filter(User.id == card.user_id).first()
  166. if user:
  167. user_info = {
  168. "id": user.id,
  169. "username": user.username,
  170. "email": user.email,
  171. "machine_code": user.machine_code,
  172. }
  173. return {
  174. "code": 200,
  175. "msg": "成功",
  176. "data": {
  177. "id": card.id,
  178. "card_code": card.card_code,
  179. "software_code": card.software_code,
  180. "days": card.days,
  181. "discount": float(card.discount),
  182. "status": card.status,
  183. "user_id": card.user_id,
  184. "user_info": user_info,
  185. "activated_at": card.activated_at.isoformat() if card.activated_at else None,
  186. "expire_time": card.expire_time.isoformat() if card.expire_time else None,
  187. "created_at": card.created_at.isoformat() if card.created_at else None,
  188. "updated_at": card.updated_at.isoformat() if card.updated_at else None
  189. }
  190. }
  191. @router.get("/{card_id}/activate")
  192. async def get_card_activate_info(
  193. card_id: int,
  194. db: Session = Depends(get_db),
  195. current_admin = Depends(get_current_admin)
  196. ):
  197. """获取卡密激活信息"""
  198. card = db.query(Card).filter(Card.id == card_id).first()
  199. if not card:
  200. return {"code": 404, "msg": "卡密不存在"}
  201. return {
  202. "code": 200,
  203. "msg": "成功",
  204. "data": {
  205. "card_code": card.card_code,
  206. "user_id": card.user_id,
  207. "activated_at": card.activated_at.isoformat() if card.activated_at else None,
  208. "expire_time": card.expire_time.isoformat() if card.expire_time else None
  209. }
  210. }
  211. @router.get("/export/excel")
  212. async def export_cards_excel(
  213. request: Request,
  214. keyword: str = Query(None, description="搜索关键词"),
  215. software_code: str = Query(None, description="软件特征码筛选"),
  216. status: int = Query(None, description="状态筛选"),
  217. db: Session = Depends(get_db),
  218. current_admin = Depends(get_current_admin)
  219. ):
  220. """导出卡密为Excel"""
  221. from app.models.software import Software
  222. from app.models.user import User
  223. # 应用相同的筛选条件构建查询,关联软件表和用户表
  224. query = db.query(Card, Software.name.label('software_name'), User.username.label('username'))\
  225. .join(Software, Card.software_code == Software.code)\
  226. .outerjoin(User, Card.user_id == User.id)\
  227. .filter(Card.is_deleted == 0)
  228. # 搜索
  229. if keyword:
  230. query = query.filter(Card.card_code.like(f"%{keyword}%"))
  231. # 筛选
  232. if software_code:
  233. query = query.filter(Card.software_code == software_code)
  234. if status is not None:
  235. query = query.filter(Card.status == status)
  236. cards_with_names = query.order_by(Card.created_at.desc()).all()
  237. # 记录操作日志
  238. from app.utils.history import create_operation_log
  239. client_ip = request.client.host if request.client else None
  240. create_operation_log(
  241. db=db,
  242. admin_id=current_admin.id,
  243. module="卡密管理",
  244. action="导出卡密Excel",
  245. description=f"导出卡密数据为Excel,共导出{len(cards_with_names)}条卡密记录",
  246. new_data={
  247. "export_count": len(cards_with_names),
  248. "export_format": "Excel",
  249. "filters": {
  250. "keyword": keyword,
  251. "software_code": software_code,
  252. "status": status
  253. }
  254. },
  255. ip_address=client_ip
  256. )
  257. db.commit()
  258. # 生成CSV格式(临时方案,后续可以升级为真正的Excel格式)
  259. output = io.StringIO()
  260. writer = csv.writer(output)
  261. writer.writerow([
  262. 'ID', '卡密代码', '软件特征码', '软件名称', '天数', '折扣', '状态',
  263. '激活用户ID', '用户名称', '激活时间', '到期时间', '创建时间', '更新时间'
  264. ])
  265. status_map = {0: '未激活', 1: '已激活', 2: '已过期'}
  266. for card, software_name, username in cards_with_names:
  267. writer.writerow([
  268. card.id,
  269. card.card_code,
  270. card.software_code,
  271. software_name,
  272. card.days,
  273. float(card.discount),
  274. status_map.get(card.status, '未知'),
  275. card.user_id or '',
  276. username or '',
  277. card.activated_at.isoformat() if card.activated_at else '',
  278. card.expire_time.isoformat() if card.expire_time else '',
  279. card.created_at.isoformat() if card.created_at else '',
  280. card.updated_at.isoformat() if card.updated_at else ''
  281. ])
  282. output.seek(0)
  283. return StreamingResponse(
  284. iter([output.getvalue()]),
  285. media_type="text/csv",
  286. headers={"Content-Disposition": f"attachment; filename=cards_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"}
  287. )