cards.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. """
  2. 卡密管理接口
  3. """
  4. import logging
  5. from fastapi import APIRouter, Depends, Query, Request
  6. from fastapi.responses import StreamingResponse
  7. from sqlalchemy.orm import Session
  8. from typing import Optional
  9. from pydantic import BaseModel, Field
  10. import secrets
  11. import string
  12. import csv
  13. import io
  14. from app.database import get_db
  15. from app.api.deps import get_current_admin
  16. from app.models.card import Card
  17. from datetime import datetime
  18. from app.utils.timezone import beijing_now
  19. logger = logging.getLogger(__name__)
  20. router = APIRouter()
  21. class CardGenerateRequest(BaseModel):
  22. """生成卡密请求"""
  23. software_code: str
  24. days: int
  25. discount: float
  26. count: int
  27. is_gift: int = Field(0, ge=0, le=1, description="0-收费卡密,1-赠送卡密")
  28. remark: Optional[str] = Field(None, max_length=500, description="备注,可选")
  29. class CardBatchExtractRequest(BaseModel):
  30. """批量提取卡密请求"""
  31. ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
  32. class CardIssueRequest(BaseModel):
  33. """发放卡密请求"""
  34. issue_price: float = Field(..., ge=0, description="发放价格")
  35. issued_to: str = Field(..., min_length=1, max_length=200, description="发放给谁(文本)")
  36. issue_remark: Optional[str] = Field(None, max_length=500, description="发放备注,可选")
  37. class CardBatchIssueRequest(BaseModel):
  38. """批量发放卡密请求"""
  39. ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
  40. issue_price: float = Field(..., ge=0, description="发放价格")
  41. issued_to: str = Field(..., min_length=1, max_length=200, description="发放给谁(文本)")
  42. issue_remark: Optional[str] = Field(None, max_length=500, description="发放备注,可选")
  43. class CardRevokeRequest(BaseModel):
  44. """注销卡密请求"""
  45. revoked_reason: Optional[str] = Field(None, max_length=500, description="注销原因,可选")
  46. class CardBatchRevokeRequest(BaseModel):
  47. """批量注销卡密请求"""
  48. ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
  49. revoked_reason: Optional[str] = Field(None, max_length=500, description="注销原因,可选")
  50. class CardBatchSpecialCloseRequest(BaseModel):
  51. """批量设置特殊关闭请求"""
  52. ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
  53. special_close: int = Field(..., ge=0, le=1, description="特殊关闭:0-关闭,1-打开")
  54. def generate_card_code() -> str:
  55. """生成卡密代码"""
  56. characters = string.ascii_uppercase + string.digits
  57. return ''.join(secrets.choice(characters) for _ in range(16))
  58. @router.post("/generate")
  59. async def generate_cards(
  60. req: CardGenerateRequest,
  61. request: Request,
  62. db: Session = Depends(get_db),
  63. current_admin = Depends(get_current_admin)
  64. ):
  65. """生成卡密"""
  66. try:
  67. remark_clean = (req.remark or "").strip() or None
  68. cards = []
  69. for _ in range(req.count):
  70. card_code = generate_card_code()
  71. # 确保卡密唯一
  72. while db.query(Card).filter(Card.card_code == card_code).first():
  73. card_code = generate_card_code()
  74. card = Card(
  75. card_code=card_code,
  76. software_code=req.software_code,
  77. days=req.days,
  78. discount=req.discount,
  79. status=0,
  80. is_gift=req.is_gift,
  81. remark=remark_clean,
  82. created_at=beijing_now(),
  83. updated_at=beijing_now()
  84. )
  85. db.add(card)
  86. cards.append(card)
  87. # 刷新以获取ID
  88. db.flush()
  89. # 创建历史记录
  90. from app.utils.history import create_card_history, create_operation_log
  91. for card in cards:
  92. create_card_history(db, card, "INSERT")
  93. # 记录操作日志
  94. client_ip = request.client.host if request.client else None
  95. gift_label = "赠送" if req.is_gift == 1 else "收费"
  96. create_operation_log(
  97. db=db,
  98. admin_id=current_admin.id,
  99. module="卡密管理",
  100. action="生成卡密",
  101. description=f"批量生成卡密:{req.count}张({gift_label}),软件:{req.software_code},天数:{req.days}天",
  102. new_data={
  103. "software_code": req.software_code,
  104. "days": req.days,
  105. "discount": req.discount,
  106. "count": req.count,
  107. "is_gift": req.is_gift,
  108. "remark": remark_clean,
  109. "card_codes": [c.card_code for c in cards]
  110. },
  111. ip_address=client_ip
  112. )
  113. # 一次性提交所有更改
  114. db.commit()
  115. return {
  116. "code": 200,
  117. "msg": "生成成功",
  118. "data": {
  119. "count": len(cards),
  120. "cards": [c.card_code for c in cards]
  121. }
  122. }
  123. except Exception as e:
  124. try:
  125. db.rollback()
  126. except Exception:
  127. # 回滚失败记录警告但继续处理
  128. pass
  129. logger.exception("生成卡密失败")
  130. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  131. @router.get("")
  132. async def get_cards(
  133. page: int = Query(1, ge=1),
  134. page_size: int = Query(10, ge=1, le=100),
  135. keyword: str = Query(None, description="搜索关键词(卡密代码或备注)"),
  136. software_code: str = Query(None, description="软件特征码筛选"),
  137. status: int = Query(None, description="状态筛选:0-未激活,1-已激活,2-已过期"),
  138. is_gift: int = Query(None, ge=0, le=1, description="类型筛选:0-收费,1-赠送"),
  139. is_extracted: int = Query(None, ge=0, le=1, description="提取状态筛选:0-未提取,1-已提取"),
  140. is_revoked: int = Query(None, ge=0, le=1, description="注销筛选:0-未注销,1-已注销"),
  141. special_close: int = Query(None, ge=0, le=1, description="特殊关闭筛选:0-关闭,1-打开"),
  142. db: Session = Depends(get_db),
  143. current_admin = Depends(get_current_admin)
  144. ):
  145. """获取卡密列表"""
  146. from sqlalchemy import or_
  147. from app.models.software import Software
  148. from app.models.user import User
  149. offset = (page - 1) * page_size
  150. # 构建查询,关联软件表和用户表
  151. query = db.query(Card, Software.name.label('software_name'), User.username.label('username'))\
  152. .join(Software, Card.software_code == Software.code)\
  153. .outerjoin(User, Card.user_id == User.id)\
  154. .filter(Card.is_deleted == 0)
  155. # 搜索
  156. if keyword:
  157. like = f"%{keyword}%"
  158. query = query.filter(
  159. or_(Card.card_code.like(like), Card.remark.like(like))
  160. )
  161. # 筛选
  162. if software_code:
  163. query = query.filter(Card.software_code == software_code)
  164. if status is not None:
  165. query = query.filter(Card.status == status)
  166. if is_gift is not None:
  167. query = query.filter(Card.is_gift == is_gift)
  168. if is_extracted is not None:
  169. query = query.filter(Card.is_extracted == is_extracted)
  170. if is_revoked is not None:
  171. query = query.filter(Card.is_revoked == is_revoked)
  172. if special_close is not None:
  173. query = query.filter(Card.special_close == special_close)
  174. # 排序(同创建时间时按 id 降序,保证更新后顺序稳定)
  175. query = query.order_by(Card.created_at.desc(), Card.id.desc())
  176. cards_with_names = query.offset(offset).limit(page_size).all()
  177. total = query.count()
  178. return {
  179. "code": 200,
  180. "msg": "成功",
  181. "data": [
  182. {
  183. "id": c.id,
  184. "card_code": c.card_code,
  185. "software_code": c.software_code,
  186. "software_name": software_name,
  187. "days": c.days,
  188. "discount": float(c.discount),
  189. "status": c.status,
  190. "is_gift": c.is_gift,
  191. "is_extracted": c.is_extracted,
  192. "special_close": c.special_close,
  193. "issue_price": float(c.issue_price) if c.issue_price is not None else None,
  194. "issued_to": c.issued_to,
  195. "issue_remark": c.issue_remark,
  196. "issued_at": c.issued_at.isoformat() if c.issued_at else None,
  197. "issued_admin_id": c.issued_admin_id,
  198. "remark": c.remark,
  199. "is_revoked": c.is_revoked,
  200. "revoked_at": c.revoked_at.isoformat() if c.revoked_at else None,
  201. "revoked_reason": c.revoked_reason,
  202. "revoked_admin_id": c.revoked_admin_id,
  203. "user_id": c.user_id,
  204. "username": username,
  205. "activated_at": c.activated_at.isoformat() if c.activated_at else None,
  206. "expire_time": c.expire_time.isoformat() if c.expire_time else None,
  207. "created_at": c.created_at.isoformat() if c.created_at else None,
  208. "updated_at": c.updated_at.isoformat() if c.updated_at else None
  209. }
  210. for c, software_name, username in cards_with_names
  211. ],
  212. "total": total,
  213. "page": page,
  214. "page_size": page_size
  215. }
  216. @router.put("/{card_id}/extract")
  217. async def extract_card(
  218. card_id: int,
  219. request: Request,
  220. db: Session = Depends(get_db),
  221. current_admin = Depends(get_current_admin)
  222. ):
  223. """提取单个卡密(标记为已提取)"""
  224. from app.utils.history import create_card_history, create_operation_log
  225. try:
  226. card = db.query(Card).filter(Card.id == card_id, Card.is_deleted == 0).first()
  227. if not card:
  228. return {"code": 404, "msg": "卡密不存在"}
  229. card.is_extracted = 1
  230. card.updated_at = beijing_now()
  231. create_card_history(db, card, "UPDATE")
  232. client_ip = request.client.host if request.client else None
  233. create_operation_log(
  234. db=db,
  235. admin_id=current_admin.id,
  236. module="卡密管理",
  237. action="提取卡密",
  238. description=f"提取卡密:{card.card_code}",
  239. new_data={"card_id": card.id, "card_code": card.card_code, "is_extracted": 1},
  240. ip_address=client_ip
  241. )
  242. db.commit()
  243. return {
  244. "code": 200,
  245. "msg": "提取成功",
  246. "data": {"id": card.id, "card_code": card.card_code, "is_extracted": card.is_extracted}
  247. }
  248. except Exception:
  249. db.rollback()
  250. logger.exception("提取卡密失败")
  251. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  252. @router.put("/{card_id}/issue")
  253. async def issue_card(
  254. card_id: int,
  255. req: CardIssueRequest,
  256. request: Request,
  257. db: Session = Depends(get_db),
  258. current_admin = Depends(get_current_admin)
  259. ):
  260. """发放单个卡密(记录发放信息)"""
  261. from app.utils.history import create_card_history, create_operation_log
  262. try:
  263. card = db.query(Card).filter(Card.id == card_id, Card.is_deleted == 0).first()
  264. if not card:
  265. return {"code": 404, "msg": "卡密不存在"}
  266. old_data = {
  267. "issue_price": float(card.issue_price) if card.issue_price is not None else None,
  268. "issued_to": card.issued_to,
  269. "issue_remark": card.issue_remark,
  270. "issued_at": card.issued_at.isoformat() if card.issued_at else None,
  271. "issued_admin_id": card.issued_admin_id,
  272. }
  273. now = beijing_now()
  274. card.issue_price = req.issue_price
  275. card.issued_to = req.issued_to.strip()
  276. card.issue_remark = (req.issue_remark or "").strip() or None
  277. card.issued_at = now
  278. card.issued_admin_id = current_admin.id
  279. card.updated_at = now
  280. create_card_history(db, card, "UPDATE")
  281. client_ip = request.client.host if request.client else None
  282. create_operation_log(
  283. db=db,
  284. admin_id=current_admin.id,
  285. module="卡密管理",
  286. action="发放卡密",
  287. description=f"发放卡密:{card.card_code}",
  288. old_data=old_data,
  289. new_data={
  290. "card_id": card.id,
  291. "card_code": card.card_code,
  292. "issue_price": req.issue_price,
  293. "issued_to": card.issued_to,
  294. "issue_remark": card.issue_remark,
  295. "issued_at": now.isoformat(),
  296. "issued_admin_id": current_admin.id,
  297. },
  298. ip_address=client_ip
  299. )
  300. db.commit()
  301. return {"code": 200, "msg": "发放成功"}
  302. except Exception:
  303. db.rollback()
  304. logger.exception("发放卡密失败")
  305. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  306. @router.post("/issue")
  307. async def issue_cards_batch(
  308. req: CardBatchIssueRequest,
  309. request: Request,
  310. db: Session = Depends(get_db),
  311. current_admin = Depends(get_current_admin)
  312. ):
  313. """批量发放卡密(记录发放信息)"""
  314. from app.utils.history import create_card_history, create_operation_log
  315. try:
  316. cards = db.query(Card).filter(
  317. Card.id.in_(req.ids),
  318. Card.is_deleted == 0
  319. ).all()
  320. if not cards:
  321. return {"code": 404, "msg": "未找到可发放的卡密"}
  322. now = beijing_now()
  323. issued_to_clean = req.issued_to.strip()
  324. issue_remark_clean = (req.issue_remark or "").strip() or None
  325. card_codes = []
  326. for card in cards:
  327. card.issue_price = req.issue_price
  328. card.issued_to = issued_to_clean
  329. card.issue_remark = issue_remark_clean
  330. card.issued_at = now
  331. card.issued_admin_id = current_admin.id
  332. card.updated_at = now
  333. card_codes.append(card.card_code)
  334. create_card_history(db, card, "UPDATE")
  335. client_ip = request.client.host if request.client else None
  336. create_operation_log(
  337. db=db,
  338. admin_id=current_admin.id,
  339. module="卡密管理",
  340. action="批量发放卡密",
  341. description=f"批量发放卡密:{len(card_codes)}张",
  342. new_data={
  343. "card_ids": req.ids,
  344. "card_codes": card_codes,
  345. "issue_price": req.issue_price,
  346. "issued_to": issued_to_clean,
  347. "issue_remark": issue_remark_clean,
  348. "issued_at": now.isoformat(),
  349. "issued_admin_id": current_admin.id,
  350. },
  351. ip_address=client_ip
  352. )
  353. db.commit()
  354. return {"code": 200, "msg": "批量发放成功", "data": {"count": len(card_codes)}}
  355. except Exception:
  356. db.rollback()
  357. logger.exception("批量发放卡密失败")
  358. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  359. @router.post("/extract")
  360. async def extract_cards_batch(
  361. req: CardBatchExtractRequest,
  362. request: Request,
  363. db: Session = Depends(get_db),
  364. current_admin = Depends(get_current_admin)
  365. ):
  366. """批量提取卡密(标记为已提取)"""
  367. from app.utils.history import create_card_history, create_operation_log
  368. try:
  369. cards = db.query(Card).filter(
  370. Card.id.in_(req.ids),
  371. Card.is_deleted == 0
  372. ).all()
  373. if not cards:
  374. return {"code": 404, "msg": "未找到可提取的卡密"}
  375. now = beijing_now()
  376. card_codes = []
  377. for card in cards:
  378. card.is_extracted = 1
  379. card.updated_at = now
  380. card_codes.append(card.card_code)
  381. create_card_history(db, card, "UPDATE")
  382. client_ip = request.client.host if request.client else None
  383. create_operation_log(
  384. db=db,
  385. admin_id=current_admin.id,
  386. module="卡密管理",
  387. action="批量提取卡密",
  388. description=f"批量提取卡密:{len(card_codes)}张",
  389. new_data={"card_ids": req.ids, "card_codes": card_codes, "is_extracted": 1},
  390. ip_address=client_ip
  391. )
  392. db.commit()
  393. return {
  394. "code": 200,
  395. "msg": "批量提取成功",
  396. "data": {"count": len(card_codes), "cards": card_codes}
  397. }
  398. except Exception:
  399. db.rollback()
  400. logger.exception("批量提取卡密失败")
  401. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  402. @router.post("/special-close")
  403. async def batch_set_special_close(
  404. req: CardBatchSpecialCloseRequest,
  405. request: Request,
  406. db: Session = Depends(get_db),
  407. current_admin = Depends(get_current_admin)
  408. ):
  409. """批量设置卡密特殊关闭标识(0-关闭,1-打开)"""
  410. from app.utils.history import create_card_history, create_operation_log
  411. try:
  412. cards = db.query(Card).filter(
  413. Card.id.in_(req.ids),
  414. Card.is_deleted == 0
  415. ).all()
  416. if not cards:
  417. return {"code": 404, "msg": "未找到可操作的卡密"}
  418. now = beijing_now()
  419. card_codes = []
  420. for card in cards:
  421. card.special_close = req.special_close
  422. card.updated_at = now
  423. card_codes.append(card.card_code)
  424. create_card_history(db, card, "UPDATE")
  425. action_label = "打开" if req.special_close == 1 else "关闭"
  426. client_ip = request.client.host if request.client else None
  427. create_operation_log(
  428. db=db,
  429. admin_id=current_admin.id,
  430. module="卡密管理",
  431. action=f"批量特殊关闭{action_label}",
  432. description=f"批量特殊关闭{action_label}:{len(card_codes)}张",
  433. new_data={
  434. "card_ids": req.ids,
  435. "card_codes": card_codes,
  436. "special_close": req.special_close,
  437. },
  438. ip_address=client_ip
  439. )
  440. db.commit()
  441. return {
  442. "code": 200,
  443. "msg": f"批量特殊关闭{action_label}成功",
  444. "data": {"count": len(card_codes), "special_close": req.special_close}
  445. }
  446. except Exception:
  447. db.rollback()
  448. logger.exception("批量设置特殊关闭失败")
  449. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  450. @router.get("/{card_id}")
  451. async def get_card_detail(
  452. card_id: int,
  453. db: Session = Depends(get_db),
  454. current_admin = Depends(get_current_admin)
  455. ):
  456. """获取卡密详情"""
  457. from app.models.user import User
  458. card = db.query(Card).filter(Card.id == card_id).first()
  459. if not card:
  460. return {"code": 404, "msg": "卡密不存在"}
  461. user_info = None
  462. if card.user_id:
  463. user = db.query(User).filter(User.id == card.user_id).first()
  464. if user:
  465. user_info = {
  466. "id": user.id,
  467. "username": user.username,
  468. "email": user.email,
  469. "machine_code": user.machine_code,
  470. }
  471. return {
  472. "code": 200,
  473. "msg": "成功",
  474. "data": {
  475. "id": card.id,
  476. "card_code": card.card_code,
  477. "software_code": card.software_code,
  478. "days": card.days,
  479. "discount": float(card.discount),
  480. "status": card.status,
  481. "is_gift": card.is_gift,
  482. "is_extracted": card.is_extracted,
  483. "special_close": card.special_close,
  484. "issue_price": float(card.issue_price) if card.issue_price is not None else None,
  485. "issued_to": card.issued_to,
  486. "issue_remark": card.issue_remark,
  487. "issued_at": card.issued_at.isoformat() if card.issued_at else None,
  488. "issued_admin_id": card.issued_admin_id,
  489. "remark": card.remark,
  490. "is_revoked": card.is_revoked,
  491. "revoked_at": card.revoked_at.isoformat() if card.revoked_at else None,
  492. "revoked_reason": card.revoked_reason,
  493. "revoked_admin_id": card.revoked_admin_id,
  494. "user_id": card.user_id,
  495. "user_info": user_info,
  496. "activated_at": card.activated_at.isoformat() if card.activated_at else None,
  497. "expire_time": card.expire_time.isoformat() if card.expire_time else None,
  498. "created_at": card.created_at.isoformat() if card.created_at else None,
  499. "updated_at": card.updated_at.isoformat() if card.updated_at else None
  500. }
  501. }
  502. @router.put("/{card_id}/revoke")
  503. async def revoke_card(
  504. card_id: int,
  505. req: CardRevokeRequest,
  506. request: Request,
  507. db: Session = Depends(get_db),
  508. current_admin = Depends(get_current_admin),
  509. ):
  510. """注销单个卡密(标记为不可再使用)"""
  511. from app.utils.history import create_card_history, create_operation_log
  512. try:
  513. card = db.query(Card).filter(Card.id == card_id, Card.is_deleted == 0).first()
  514. if not card:
  515. return {"code": 404, "msg": "卡密不存在"}
  516. if card.is_revoked == 1:
  517. return {"code": 400, "msg": "该卡密已注销"}
  518. if card.status != 0:
  519. return {"code": 400, "msg": "仅支持注销未激活的卡密"}
  520. old_data = {
  521. "is_revoked": card.is_revoked,
  522. "revoked_at": card.revoked_at.isoformat() if card.revoked_at else None,
  523. "revoked_reason": card.revoked_reason,
  524. "revoked_admin_id": card.revoked_admin_id,
  525. }
  526. now = beijing_now()
  527. card.is_revoked = 1
  528. card.is_deleted = 1
  529. card.revoked_at = now
  530. card.revoked_reason = (req.revoked_reason or "").strip() or None
  531. card.revoked_admin_id = current_admin.id
  532. card.updated_at = now
  533. create_card_history(db, card, "UPDATE")
  534. client_ip = request.client.host if request.client else None
  535. create_operation_log(
  536. db=db,
  537. admin_id=current_admin.id,
  538. module="卡密管理",
  539. action="注销卡密",
  540. description=f"注销卡密:{card.card_code}",
  541. old_data=old_data,
  542. new_data={
  543. "card_id": card.id,
  544. "card_code": card.card_code,
  545. "is_revoked": 1,
  546. "is_deleted": 1,
  547. "revoked_at": now.isoformat(),
  548. "revoked_reason": card.revoked_reason,
  549. "revoked_admin_id": current_admin.id,
  550. },
  551. ip_address=client_ip,
  552. )
  553. db.commit()
  554. return {"code": 200, "msg": "注销成功"}
  555. except Exception:
  556. db.rollback()
  557. logger.exception("注销卡密失败")
  558. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  559. @router.post("/revoke")
  560. async def revoke_cards_batch(
  561. req: CardBatchRevokeRequest,
  562. request: Request,
  563. db: Session = Depends(get_db),
  564. current_admin = Depends(get_current_admin),
  565. ):
  566. """批量注销卡密(标记为不可再使用)"""
  567. from app.utils.history import create_card_history, create_operation_log
  568. try:
  569. cards = db.query(Card).filter(
  570. Card.id.in_(req.ids),
  571. Card.is_deleted == 0,
  572. ).all()
  573. if not cards:
  574. return {"code": 404, "msg": "未找到可注销的卡密"}
  575. now = beijing_now()
  576. reason_clean = (req.revoked_reason or "").strip() or None
  577. revoked_codes: list[str] = []
  578. skipped: list[dict] = []
  579. for card in cards:
  580. if card.is_revoked == 1:
  581. skipped.append({"id": card.id, "card_code": card.card_code, "reason": "已注销"})
  582. continue
  583. if card.status != 0:
  584. skipped.append({"id": card.id, "card_code": card.card_code, "reason": "非未激活"})
  585. continue
  586. card.is_revoked = 1
  587. card.is_deleted = 1
  588. card.revoked_at = now
  589. card.revoked_reason = reason_clean
  590. card.revoked_admin_id = current_admin.id
  591. card.updated_at = now
  592. revoked_codes.append(card.card_code)
  593. create_card_history(db, card, "UPDATE")
  594. if not revoked_codes:
  595. return {"code": 400, "msg": "没有可注销的卡密", "data": {"skipped": skipped}}
  596. client_ip = request.client.host if request.client else None
  597. create_operation_log(
  598. db=db,
  599. admin_id=current_admin.id,
  600. module="卡密管理",
  601. action="批量注销卡密",
  602. description=f"批量注销卡密:{len(revoked_codes)}张",
  603. new_data={
  604. "card_ids": req.ids,
  605. "revoked_card_codes": revoked_codes,
  606. "revoked_at": now.isoformat(),
  607. "revoked_reason": reason_clean,
  608. "revoked_admin_id": current_admin.id,
  609. "is_deleted": 1,
  610. "skipped": skipped,
  611. },
  612. ip_address=client_ip,
  613. )
  614. db.commit()
  615. return {"code": 200, "msg": "批量注销成功", "data": {"count": len(revoked_codes), "skipped": skipped}}
  616. except Exception:
  617. db.rollback()
  618. logger.exception("批量注销卡密失败")
  619. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
  620. @router.get("/{card_id}/activate")
  621. async def get_card_activate_info(
  622. card_id: int,
  623. db: Session = Depends(get_db),
  624. current_admin = Depends(get_current_admin)
  625. ):
  626. """获取卡密激活信息"""
  627. card = db.query(Card).filter(Card.id == card_id).first()
  628. if not card:
  629. return {"code": 404, "msg": "卡密不存在"}
  630. return {
  631. "code": 200,
  632. "msg": "成功",
  633. "data": {
  634. "card_code": card.card_code,
  635. "user_id": card.user_id,
  636. "activated_at": card.activated_at.isoformat() if card.activated_at else None,
  637. "expire_time": card.expire_time.isoformat() if card.expire_time else None,
  638. "issue_price": float(card.issue_price) if card.issue_price is not None else None,
  639. "issued_to": card.issued_to,
  640. "issue_remark": card.issue_remark,
  641. "issued_at": card.issued_at.isoformat() if card.issued_at else None,
  642. "issued_admin_id": card.issued_admin_id,
  643. }
  644. }
  645. @router.get("/export/excel")
  646. async def export_cards_excel(
  647. request: Request,
  648. keyword: str = Query(None, description="搜索关键词"),
  649. software_code: str = Query(None, description="软件特征码筛选"),
  650. status: int = Query(None, description="状态筛选"),
  651. is_gift: int = Query(None, ge=0, le=1, description="类型筛选:0-收费,1-赠送"),
  652. is_extracted: int = Query(None, ge=0, le=1, description="提取状态筛选:0-未提取,1-已提取"),
  653. is_revoked: int = Query(None, ge=0, le=1, description="注销筛选:0-未注销,1-已注销"),
  654. special_close: int = Query(None, ge=0, le=1, description="特殊关闭筛选:0-关闭,1-打开"),
  655. db: Session = Depends(get_db),
  656. current_admin = Depends(get_current_admin)
  657. ):
  658. """导出卡密为Excel"""
  659. from sqlalchemy import or_
  660. from app.models.software import Software
  661. from app.models.user import User
  662. # 应用相同的筛选条件构建查询,关联软件表和用户表
  663. query = db.query(Card, Software.name.label('software_name'), User.username.label('username'))\
  664. .join(Software, Card.software_code == Software.code)\
  665. .outerjoin(User, Card.user_id == User.id)\
  666. .filter(Card.is_deleted == 0)
  667. # 搜索
  668. if keyword:
  669. like = f"%{keyword}%"
  670. query = query.filter(
  671. or_(Card.card_code.like(like), Card.remark.like(like))
  672. )
  673. # 筛选
  674. if software_code:
  675. query = query.filter(Card.software_code == software_code)
  676. if status is not None:
  677. query = query.filter(Card.status == status)
  678. if is_gift is not None:
  679. query = query.filter(Card.is_gift == is_gift)
  680. if is_extracted is not None:
  681. query = query.filter(Card.is_extracted == is_extracted)
  682. if is_revoked is not None:
  683. query = query.filter(Card.is_revoked == is_revoked)
  684. if special_close is not None:
  685. query = query.filter(Card.special_close == special_close)
  686. cards_with_names = query.order_by(Card.created_at.desc(), Card.id.desc()).all()
  687. # 记录操作日志
  688. from app.utils.history import create_operation_log
  689. client_ip = request.client.host if request.client else None
  690. create_operation_log(
  691. db=db,
  692. admin_id=current_admin.id,
  693. module="卡密管理",
  694. action="导出卡密Excel",
  695. description=f"导出卡密数据为Excel,共导出{len(cards_with_names)}条卡密记录",
  696. new_data={
  697. "export_count": len(cards_with_names),
  698. "export_format": "Excel",
  699. "filters": {
  700. "keyword": keyword,
  701. "software_code": software_code,
  702. "status": status,
  703. "is_gift": is_gift,
  704. "is_extracted": is_extracted,
  705. "is_revoked": is_revoked,
  706. "special_close": special_close,
  707. }
  708. },
  709. ip_address=client_ip
  710. )
  711. db.commit()
  712. # 生成CSV格式(临时方案,后续可以升级为真正的Excel格式)
  713. output = io.StringIO()
  714. writer = csv.writer(output)
  715. writer.writerow([
  716. 'ID', '卡密代码', '软件特征码', '软件名称', '天数', '折扣', '状态', '卡密类型', '提取状态', '特殊关闭',
  717. '注销状态', '注销时间', '注销原因', '注销管理员ID',
  718. '发放价格', '发放对象', '发放备注', '发放时间', '发放管理员ID',
  719. '备注',
  720. '激活用户ID', '用户名称', '激活时间', '到期时间', '创建时间', '更新时间'
  721. ])
  722. status_map = {0: '未激活', 1: '已激活', 2: '已过期'}
  723. gift_map = {0: '收费', 1: '赠送'}
  724. extracted_map = {0: '未提取', 1: '已提取'}
  725. special_close_map = {0: '关闭', 1: '打开'}
  726. revoked_map = {0: '未注销', 1: '已注销'}
  727. for card, software_name, username in cards_with_names:
  728. writer.writerow([
  729. card.id,
  730. card.card_code,
  731. card.software_code,
  732. software_name,
  733. card.days,
  734. float(card.discount),
  735. status_map.get(card.status, '未知'),
  736. gift_map.get(card.is_gift, '未知'),
  737. extracted_map.get(card.is_extracted, '未知'),
  738. special_close_map.get(getattr(card, "special_close", 0) or 0, '未知'),
  739. revoked_map.get(getattr(card, "is_revoked", 0), '未知'),
  740. card.revoked_at.isoformat() if getattr(card, "revoked_at", None) else '',
  741. (card.revoked_reason or '') if getattr(card, "revoked_reason", None) is not None else '',
  742. card.revoked_admin_id or '',
  743. float(card.issue_price) if card.issue_price is not None else '',
  744. card.issued_to or '',
  745. card.issue_remark or '',
  746. card.issued_at.isoformat() if card.issued_at else '',
  747. card.issued_admin_id or '',
  748. card.remark or '',
  749. card.user_id or '',
  750. username or '',
  751. card.activated_at.isoformat() if card.activated_at else '',
  752. card.expire_time.isoformat() if card.expire_time else '',
  753. card.created_at.isoformat() if card.created_at else '',
  754. card.updated_at.isoformat() if card.updated_at else ''
  755. ])
  756. output.seek(0)
  757. return StreamingResponse(
  758. iter([output.getvalue()]),
  759. media_type="text/csv",
  760. headers={"Content-Disposition": f"attachment; filename=cards_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"}
  761. )