|
|
@@ -1,10 +1,13 @@
|
|
|
"""
|
|
|
卡密管理接口
|
|
|
"""
|
|
|
+import logging
|
|
|
from fastapi import APIRouter, Depends, Query, Request
|
|
|
from fastapi.responses import StreamingResponse
|
|
|
from sqlalchemy.orm import Session
|
|
|
-from pydantic import BaseModel
|
|
|
+from typing import Optional
|
|
|
+
|
|
|
+from pydantic import BaseModel, Field
|
|
|
import secrets
|
|
|
import string
|
|
|
import csv
|
|
|
@@ -15,6 +18,8 @@ from app.models.card import Card
|
|
|
from datetime import datetime
|
|
|
from app.utils.timezone import beijing_now
|
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
router = APIRouter()
|
|
|
|
|
|
class CardGenerateRequest(BaseModel):
|
|
|
@@ -23,6 +28,39 @@ class CardGenerateRequest(BaseModel):
|
|
|
days: int
|
|
|
discount: float
|
|
|
count: int
|
|
|
+ is_gift: int = Field(0, ge=0, le=1, description="0-收费卡密,1-赠送卡密")
|
|
|
+ remark: Optional[str] = Field(None, max_length=500, description="备注,可选")
|
|
|
+
|
|
|
+class CardBatchExtractRequest(BaseModel):
|
|
|
+ """批量提取卡密请求"""
|
|
|
+ ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
|
|
|
+
|
|
|
+class CardIssueRequest(BaseModel):
|
|
|
+ """发放卡密请求"""
|
|
|
+ issue_price: float = Field(..., ge=0, description="发放价格")
|
|
|
+ issued_to: str = Field(..., min_length=1, max_length=200, description="发放给谁(文本)")
|
|
|
+ issue_remark: Optional[str] = Field(None, max_length=500, description="发放备注,可选")
|
|
|
+
|
|
|
+class CardBatchIssueRequest(BaseModel):
|
|
|
+ """批量发放卡密请求"""
|
|
|
+ ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
|
|
|
+ issue_price: float = Field(..., ge=0, description="发放价格")
|
|
|
+ issued_to: str = Field(..., min_length=1, max_length=200, description="发放给谁(文本)")
|
|
|
+ issue_remark: Optional[str] = Field(None, max_length=500, description="发放备注,可选")
|
|
|
+
|
|
|
+class CardRevokeRequest(BaseModel):
|
|
|
+ """注销卡密请求"""
|
|
|
+ revoked_reason: Optional[str] = Field(None, max_length=500, description="注销原因,可选")
|
|
|
+
|
|
|
+class CardBatchRevokeRequest(BaseModel):
|
|
|
+ """批量注销卡密请求"""
|
|
|
+ ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
|
|
|
+ revoked_reason: Optional[str] = Field(None, max_length=500, description="注销原因,可选")
|
|
|
+
|
|
|
+class CardBatchSpecialCloseRequest(BaseModel):
|
|
|
+ """批量设置特殊关闭请求"""
|
|
|
+ ids: list[int] = Field(..., min_length=1, description="卡密ID列表")
|
|
|
+ special_close: int = Field(..., ge=0, le=1, description="特殊关闭:0-关闭,1-打开")
|
|
|
|
|
|
def generate_card_code() -> str:
|
|
|
"""生成卡密代码"""
|
|
|
@@ -38,6 +76,8 @@ async def generate_cards(
|
|
|
):
|
|
|
"""生成卡密"""
|
|
|
try:
|
|
|
+ remark_clean = (req.remark or "").strip() or None
|
|
|
+
|
|
|
cards = []
|
|
|
for _ in range(req.count):
|
|
|
card_code = generate_card_code()
|
|
|
@@ -51,6 +91,8 @@ async def generate_cards(
|
|
|
days=req.days,
|
|
|
discount=req.discount,
|
|
|
status=0,
|
|
|
+ is_gift=req.is_gift,
|
|
|
+ remark=remark_clean,
|
|
|
created_at=beijing_now(),
|
|
|
updated_at=beijing_now()
|
|
|
)
|
|
|
@@ -67,17 +109,20 @@ async def generate_cards(
|
|
|
|
|
|
# 记录操作日志
|
|
|
client_ip = request.client.host if request.client else None
|
|
|
+ gift_label = "赠送" if req.is_gift == 1 else "收费"
|
|
|
create_operation_log(
|
|
|
db=db,
|
|
|
admin_id=current_admin.id,
|
|
|
module="卡密管理",
|
|
|
action="生成卡密",
|
|
|
- description=f"批量生成卡密:{req.count}张,软件:{req.software_code},天数:{req.days}天",
|
|
|
+ description=f"批量生成卡密:{req.count}张({gift_label}),软件:{req.software_code},天数:{req.days}天",
|
|
|
new_data={
|
|
|
"software_code": req.software_code,
|
|
|
"days": req.days,
|
|
|
"discount": req.discount,
|
|
|
"count": req.count,
|
|
|
+ "is_gift": req.is_gift,
|
|
|
+ "remark": remark_clean,
|
|
|
"card_codes": [c.card_code for c in cards]
|
|
|
},
|
|
|
ip_address=client_ip
|
|
|
@@ -108,14 +153,19 @@ async def generate_cards(
|
|
|
async def get_cards(
|
|
|
page: int = Query(1, ge=1),
|
|
|
page_size: int = Query(10, ge=1, le=100),
|
|
|
- keyword: str = Query(None, description="搜索关键词(卡密代码)"),
|
|
|
+ keyword: str = Query(None, description="搜索关键词(卡密代码或备注)"),
|
|
|
software_code: str = Query(None, description="软件特征码筛选"),
|
|
|
status: int = Query(None, description="状态筛选:0-未激活,1-已激活,2-已过期"),
|
|
|
+ is_gift: int = Query(None, ge=0, le=1, description="类型筛选:0-收费,1-赠送"),
|
|
|
+ is_extracted: int = Query(None, ge=0, le=1, description="提取状态筛选:0-未提取,1-已提取"),
|
|
|
+ is_revoked: int = Query(None, ge=0, le=1, description="注销筛选:0-未注销,1-已注销"),
|
|
|
+ special_close: int = Query(None, ge=0, le=1, description="特殊关闭筛选:0-关闭,1-打开"),
|
|
|
db: Session = Depends(get_db),
|
|
|
current_admin = Depends(get_current_admin)
|
|
|
):
|
|
|
"""获取卡密列表"""
|
|
|
from sqlalchemy import or_
|
|
|
+
|
|
|
from app.models.software import Software
|
|
|
from app.models.user import User
|
|
|
|
|
|
@@ -129,7 +179,10 @@ async def get_cards(
|
|
|
|
|
|
# 搜索
|
|
|
if keyword:
|
|
|
- query = query.filter(Card.card_code.like(f"%{keyword}%"))
|
|
|
+ like = f"%{keyword}%"
|
|
|
+ query = query.filter(
|
|
|
+ or_(Card.card_code.like(like), Card.remark.like(like))
|
|
|
+ )
|
|
|
|
|
|
# 筛选
|
|
|
if software_code:
|
|
|
@@ -138,8 +191,20 @@ async def get_cards(
|
|
|
if status is not None:
|
|
|
query = query.filter(Card.status == status)
|
|
|
|
|
|
- # 排序
|
|
|
- query = query.order_by(Card.created_at.desc())
|
|
|
+ if is_gift is not None:
|
|
|
+ query = query.filter(Card.is_gift == is_gift)
|
|
|
+
|
|
|
+ if is_extracted is not None:
|
|
|
+ query = query.filter(Card.is_extracted == is_extracted)
|
|
|
+
|
|
|
+ if is_revoked is not None:
|
|
|
+ query = query.filter(Card.is_revoked == is_revoked)
|
|
|
+
|
|
|
+ if special_close is not None:
|
|
|
+ query = query.filter(Card.special_close == special_close)
|
|
|
+
|
|
|
+ # 排序(同创建时间时按 id 降序,保证更新后顺序稳定)
|
|
|
+ query = query.order_by(Card.created_at.desc(), Card.id.desc())
|
|
|
|
|
|
cards_with_names = query.offset(offset).limit(page_size).all()
|
|
|
total = query.count()
|
|
|
@@ -156,6 +221,19 @@ async def get_cards(
|
|
|
"days": c.days,
|
|
|
"discount": float(c.discount),
|
|
|
"status": c.status,
|
|
|
+ "is_gift": c.is_gift,
|
|
|
+ "is_extracted": c.is_extracted,
|
|
|
+ "special_close": c.special_close,
|
|
|
+ "issue_price": float(c.issue_price) if c.issue_price is not None else None,
|
|
|
+ "issued_to": c.issued_to,
|
|
|
+ "issue_remark": c.issue_remark,
|
|
|
+ "issued_at": c.issued_at.isoformat() if c.issued_at else None,
|
|
|
+ "issued_admin_id": c.issued_admin_id,
|
|
|
+ "remark": c.remark,
|
|
|
+ "is_revoked": c.is_revoked,
|
|
|
+ "revoked_at": c.revoked_at.isoformat() if c.revoked_at else None,
|
|
|
+ "revoked_reason": c.revoked_reason,
|
|
|
+ "revoked_admin_id": c.revoked_admin_id,
|
|
|
"user_id": c.user_id,
|
|
|
"username": username,
|
|
|
"activated_at": c.activated_at.isoformat() if c.activated_at else None,
|
|
|
@@ -170,6 +248,270 @@ async def get_cards(
|
|
|
"page_size": page_size
|
|
|
}
|
|
|
|
|
|
+@router.put("/{card_id}/extract")
|
|
|
+async def extract_card(
|
|
|
+ card_id: int,
|
|
|
+ request: Request,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_admin = Depends(get_current_admin)
|
|
|
+):
|
|
|
+ """提取单个卡密(标记为已提取)"""
|
|
|
+ from app.utils.history import create_card_history, create_operation_log
|
|
|
+
|
|
|
+ try:
|
|
|
+ card = db.query(Card).filter(Card.id == card_id, Card.is_deleted == 0).first()
|
|
|
+ if not card:
|
|
|
+ return {"code": 404, "msg": "卡密不存在"}
|
|
|
+
|
|
|
+ card.is_extracted = 1
|
|
|
+ card.updated_at = beijing_now()
|
|
|
+
|
|
|
+ create_card_history(db, card, "UPDATE")
|
|
|
+ client_ip = request.client.host if request.client else None
|
|
|
+ create_operation_log(
|
|
|
+ db=db,
|
|
|
+ admin_id=current_admin.id,
|
|
|
+ module="卡密管理",
|
|
|
+ action="提取卡密",
|
|
|
+ description=f"提取卡密:{card.card_code}",
|
|
|
+ new_data={"card_id": card.id, "card_code": card.card_code, "is_extracted": 1},
|
|
|
+ ip_address=client_ip
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+
|
|
|
+ return {
|
|
|
+ "code": 200,
|
|
|
+ "msg": "提取成功",
|
|
|
+ "data": {"id": card.id, "card_code": card.card_code, "is_extracted": card.is_extracted}
|
|
|
+ }
|
|
|
+ except Exception:
|
|
|
+ db.rollback()
|
|
|
+ logger.exception("提取卡密失败")
|
|
|
+ return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|
|
|
+
|
|
|
+@router.put("/{card_id}/issue")
|
|
|
+async def issue_card(
|
|
|
+ card_id: int,
|
|
|
+ req: CardIssueRequest,
|
|
|
+ request: Request,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_admin = Depends(get_current_admin)
|
|
|
+):
|
|
|
+ """发放单个卡密(记录发放信息)"""
|
|
|
+ from app.utils.history import create_card_history, create_operation_log
|
|
|
+
|
|
|
+ try:
|
|
|
+ card = db.query(Card).filter(Card.id == card_id, Card.is_deleted == 0).first()
|
|
|
+ if not card:
|
|
|
+ return {"code": 404, "msg": "卡密不存在"}
|
|
|
+
|
|
|
+ old_data = {
|
|
|
+ "issue_price": float(card.issue_price) if card.issue_price is not None else None,
|
|
|
+ "issued_to": card.issued_to,
|
|
|
+ "issue_remark": card.issue_remark,
|
|
|
+ "issued_at": card.issued_at.isoformat() if card.issued_at else None,
|
|
|
+ "issued_admin_id": card.issued_admin_id,
|
|
|
+ }
|
|
|
+
|
|
|
+ now = beijing_now()
|
|
|
+ card.issue_price = req.issue_price
|
|
|
+ card.issued_to = req.issued_to.strip()
|
|
|
+ card.issue_remark = (req.issue_remark or "").strip() or None
|
|
|
+ card.issued_at = now
|
|
|
+ card.issued_admin_id = current_admin.id
|
|
|
+ card.updated_at = now
|
|
|
+
|
|
|
+ create_card_history(db, card, "UPDATE")
|
|
|
+ client_ip = request.client.host if request.client else None
|
|
|
+ create_operation_log(
|
|
|
+ db=db,
|
|
|
+ admin_id=current_admin.id,
|
|
|
+ module="卡密管理",
|
|
|
+ action="发放卡密",
|
|
|
+ description=f"发放卡密:{card.card_code}",
|
|
|
+ old_data=old_data,
|
|
|
+ new_data={
|
|
|
+ "card_id": card.id,
|
|
|
+ "card_code": card.card_code,
|
|
|
+ "issue_price": req.issue_price,
|
|
|
+ "issued_to": card.issued_to,
|
|
|
+ "issue_remark": card.issue_remark,
|
|
|
+ "issued_at": now.isoformat(),
|
|
|
+ "issued_admin_id": current_admin.id,
|
|
|
+ },
|
|
|
+ ip_address=client_ip
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+
|
|
|
+ return {"code": 200, "msg": "发放成功"}
|
|
|
+ except Exception:
|
|
|
+ db.rollback()
|
|
|
+ logger.exception("发放卡密失败")
|
|
|
+ return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|
|
|
+
|
|
|
+@router.post("/issue")
|
|
|
+async def issue_cards_batch(
|
|
|
+ req: CardBatchIssueRequest,
|
|
|
+ request: Request,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_admin = Depends(get_current_admin)
|
|
|
+):
|
|
|
+ """批量发放卡密(记录发放信息)"""
|
|
|
+ from app.utils.history import create_card_history, create_operation_log
|
|
|
+
|
|
|
+ try:
|
|
|
+ cards = db.query(Card).filter(
|
|
|
+ Card.id.in_(req.ids),
|
|
|
+ Card.is_deleted == 0
|
|
|
+ ).all()
|
|
|
+
|
|
|
+ if not cards:
|
|
|
+ return {"code": 404, "msg": "未找到可发放的卡密"}
|
|
|
+
|
|
|
+ now = beijing_now()
|
|
|
+ issued_to_clean = req.issued_to.strip()
|
|
|
+ issue_remark_clean = (req.issue_remark or "").strip() or None
|
|
|
+
|
|
|
+ card_codes = []
|
|
|
+ for card in cards:
|
|
|
+ card.issue_price = req.issue_price
|
|
|
+ card.issued_to = issued_to_clean
|
|
|
+ card.issue_remark = issue_remark_clean
|
|
|
+ card.issued_at = now
|
|
|
+ card.issued_admin_id = current_admin.id
|
|
|
+ card.updated_at = now
|
|
|
+ card_codes.append(card.card_code)
|
|
|
+ create_card_history(db, card, "UPDATE")
|
|
|
+
|
|
|
+ client_ip = request.client.host if request.client else None
|
|
|
+ create_operation_log(
|
|
|
+ db=db,
|
|
|
+ admin_id=current_admin.id,
|
|
|
+ module="卡密管理",
|
|
|
+ action="批量发放卡密",
|
|
|
+ description=f"批量发放卡密:{len(card_codes)}张",
|
|
|
+ new_data={
|
|
|
+ "card_ids": req.ids,
|
|
|
+ "card_codes": card_codes,
|
|
|
+ "issue_price": req.issue_price,
|
|
|
+ "issued_to": issued_to_clean,
|
|
|
+ "issue_remark": issue_remark_clean,
|
|
|
+ "issued_at": now.isoformat(),
|
|
|
+ "issued_admin_id": current_admin.id,
|
|
|
+ },
|
|
|
+ ip_address=client_ip
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+
|
|
|
+ return {"code": 200, "msg": "批量发放成功", "data": {"count": len(card_codes)}}
|
|
|
+ except Exception:
|
|
|
+ db.rollback()
|
|
|
+ logger.exception("批量发放卡密失败")
|
|
|
+ return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|
|
|
+
|
|
|
+@router.post("/extract")
|
|
|
+async def extract_cards_batch(
|
|
|
+ req: CardBatchExtractRequest,
|
|
|
+ request: Request,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_admin = Depends(get_current_admin)
|
|
|
+):
|
|
|
+ """批量提取卡密(标记为已提取)"""
|
|
|
+ from app.utils.history import create_card_history, create_operation_log
|
|
|
+
|
|
|
+ try:
|
|
|
+ cards = db.query(Card).filter(
|
|
|
+ Card.id.in_(req.ids),
|
|
|
+ Card.is_deleted == 0
|
|
|
+ ).all()
|
|
|
+
|
|
|
+ if not cards:
|
|
|
+ return {"code": 404, "msg": "未找到可提取的卡密"}
|
|
|
+
|
|
|
+ now = beijing_now()
|
|
|
+ card_codes = []
|
|
|
+ for card in cards:
|
|
|
+ card.is_extracted = 1
|
|
|
+ card.updated_at = now
|
|
|
+ card_codes.append(card.card_code)
|
|
|
+ create_card_history(db, card, "UPDATE")
|
|
|
+
|
|
|
+ client_ip = request.client.host if request.client else None
|
|
|
+ create_operation_log(
|
|
|
+ db=db,
|
|
|
+ admin_id=current_admin.id,
|
|
|
+ module="卡密管理",
|
|
|
+ action="批量提取卡密",
|
|
|
+ description=f"批量提取卡密:{len(card_codes)}张",
|
|
|
+ new_data={"card_ids": req.ids, "card_codes": card_codes, "is_extracted": 1},
|
|
|
+ ip_address=client_ip
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+
|
|
|
+ return {
|
|
|
+ "code": 200,
|
|
|
+ "msg": "批量提取成功",
|
|
|
+ "data": {"count": len(card_codes), "cards": card_codes}
|
|
|
+ }
|
|
|
+ except Exception:
|
|
|
+ db.rollback()
|
|
|
+ logger.exception("批量提取卡密失败")
|
|
|
+ return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|
|
|
+
|
|
|
+@router.post("/special-close")
|
|
|
+async def batch_set_special_close(
|
|
|
+ req: CardBatchSpecialCloseRequest,
|
|
|
+ request: Request,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_admin = Depends(get_current_admin)
|
|
|
+):
|
|
|
+ """批量设置卡密特殊关闭标识(0-关闭,1-打开)"""
|
|
|
+ from app.utils.history import create_card_history, create_operation_log
|
|
|
+
|
|
|
+ try:
|
|
|
+ cards = db.query(Card).filter(
|
|
|
+ Card.id.in_(req.ids),
|
|
|
+ Card.is_deleted == 0
|
|
|
+ ).all()
|
|
|
+
|
|
|
+ if not cards:
|
|
|
+ return {"code": 404, "msg": "未找到可操作的卡密"}
|
|
|
+
|
|
|
+ now = beijing_now()
|
|
|
+ card_codes = []
|
|
|
+ for card in cards:
|
|
|
+ card.special_close = req.special_close
|
|
|
+ card.updated_at = now
|
|
|
+ card_codes.append(card.card_code)
|
|
|
+ create_card_history(db, card, "UPDATE")
|
|
|
+
|
|
|
+ action_label = "打开" if req.special_close == 1 else "关闭"
|
|
|
+ client_ip = request.client.host if request.client else None
|
|
|
+ create_operation_log(
|
|
|
+ db=db,
|
|
|
+ admin_id=current_admin.id,
|
|
|
+ module="卡密管理",
|
|
|
+ action=f"批量特殊关闭{action_label}",
|
|
|
+ description=f"批量特殊关闭{action_label}:{len(card_codes)}张",
|
|
|
+ new_data={
|
|
|
+ "card_ids": req.ids,
|
|
|
+ "card_codes": card_codes,
|
|
|
+ "special_close": req.special_close,
|
|
|
+ },
|
|
|
+ ip_address=client_ip
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+
|
|
|
+ return {
|
|
|
+ "code": 200,
|
|
|
+ "msg": f"批量特殊关闭{action_label}成功",
|
|
|
+ "data": {"count": len(card_codes), "special_close": req.special_close}
|
|
|
+ }
|
|
|
+ except Exception:
|
|
|
+ db.rollback()
|
|
|
+ logger.exception("批量设置特殊关闭失败")
|
|
|
+ return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|
|
|
+
|
|
|
@router.get("/{card_id}")
|
|
|
async def get_card_detail(
|
|
|
card_id: int,
|
|
|
@@ -204,6 +546,19 @@ async def get_card_detail(
|
|
|
"days": card.days,
|
|
|
"discount": float(card.discount),
|
|
|
"status": card.status,
|
|
|
+ "is_gift": card.is_gift,
|
|
|
+ "is_extracted": card.is_extracted,
|
|
|
+ "special_close": card.special_close,
|
|
|
+ "issue_price": float(card.issue_price) if card.issue_price is not None else None,
|
|
|
+ "issued_to": card.issued_to,
|
|
|
+ "issue_remark": card.issue_remark,
|
|
|
+ "issued_at": card.issued_at.isoformat() if card.issued_at else None,
|
|
|
+ "issued_admin_id": card.issued_admin_id,
|
|
|
+ "remark": card.remark,
|
|
|
+ "is_revoked": card.is_revoked,
|
|
|
+ "revoked_at": card.revoked_at.isoformat() if card.revoked_at else None,
|
|
|
+ "revoked_reason": card.revoked_reason,
|
|
|
+ "revoked_admin_id": card.revoked_admin_id,
|
|
|
"user_id": card.user_id,
|
|
|
"user_info": user_info,
|
|
|
"activated_at": card.activated_at.isoformat() if card.activated_at else None,
|
|
|
@@ -213,6 +568,136 @@ async def get_card_detail(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+@router.put("/{card_id}/revoke")
|
|
|
+async def revoke_card(
|
|
|
+ card_id: int,
|
|
|
+ req: CardRevokeRequest,
|
|
|
+ request: Request,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_admin = Depends(get_current_admin),
|
|
|
+):
|
|
|
+ """注销单个卡密(标记为不可再使用)"""
|
|
|
+ from app.utils.history import create_card_history, create_operation_log
|
|
|
+
|
|
|
+ try:
|
|
|
+ card = db.query(Card).filter(Card.id == card_id, Card.is_deleted == 0).first()
|
|
|
+ if not card:
|
|
|
+ return {"code": 404, "msg": "卡密不存在"}
|
|
|
+ if card.is_revoked == 1:
|
|
|
+ return {"code": 400, "msg": "该卡密已注销"}
|
|
|
+ if card.status != 0:
|
|
|
+ return {"code": 400, "msg": "仅支持注销未激活的卡密"}
|
|
|
+
|
|
|
+ old_data = {
|
|
|
+ "is_revoked": card.is_revoked,
|
|
|
+ "revoked_at": card.revoked_at.isoformat() if card.revoked_at else None,
|
|
|
+ "revoked_reason": card.revoked_reason,
|
|
|
+ "revoked_admin_id": card.revoked_admin_id,
|
|
|
+ }
|
|
|
+
|
|
|
+ now = beijing_now()
|
|
|
+ card.is_revoked = 1
|
|
|
+ card.is_deleted = 1
|
|
|
+ card.revoked_at = now
|
|
|
+ card.revoked_reason = (req.revoked_reason or "").strip() or None
|
|
|
+ card.revoked_admin_id = current_admin.id
|
|
|
+ card.updated_at = now
|
|
|
+
|
|
|
+ create_card_history(db, card, "UPDATE")
|
|
|
+ client_ip = request.client.host if request.client else None
|
|
|
+ create_operation_log(
|
|
|
+ db=db,
|
|
|
+ admin_id=current_admin.id,
|
|
|
+ module="卡密管理",
|
|
|
+ action="注销卡密",
|
|
|
+ description=f"注销卡密:{card.card_code}",
|
|
|
+ old_data=old_data,
|
|
|
+ new_data={
|
|
|
+ "card_id": card.id,
|
|
|
+ "card_code": card.card_code,
|
|
|
+ "is_revoked": 1,
|
|
|
+ "is_deleted": 1,
|
|
|
+ "revoked_at": now.isoformat(),
|
|
|
+ "revoked_reason": card.revoked_reason,
|
|
|
+ "revoked_admin_id": current_admin.id,
|
|
|
+ },
|
|
|
+ ip_address=client_ip,
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+ return {"code": 200, "msg": "注销成功"}
|
|
|
+ except Exception:
|
|
|
+ db.rollback()
|
|
|
+ logger.exception("注销卡密失败")
|
|
|
+ return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/revoke")
|
|
|
+async def revoke_cards_batch(
|
|
|
+ req: CardBatchRevokeRequest,
|
|
|
+ request: Request,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_admin = Depends(get_current_admin),
|
|
|
+):
|
|
|
+ """批量注销卡密(标记为不可再使用)"""
|
|
|
+ from app.utils.history import create_card_history, create_operation_log
|
|
|
+
|
|
|
+ try:
|
|
|
+ cards = db.query(Card).filter(
|
|
|
+ Card.id.in_(req.ids),
|
|
|
+ Card.is_deleted == 0,
|
|
|
+ ).all()
|
|
|
+ if not cards:
|
|
|
+ return {"code": 404, "msg": "未找到可注销的卡密"}
|
|
|
+
|
|
|
+ now = beijing_now()
|
|
|
+ reason_clean = (req.revoked_reason or "").strip() or None
|
|
|
+
|
|
|
+ revoked_codes: list[str] = []
|
|
|
+ skipped: list[dict] = []
|
|
|
+ for card in cards:
|
|
|
+ if card.is_revoked == 1:
|
|
|
+ skipped.append({"id": card.id, "card_code": card.card_code, "reason": "已注销"})
|
|
|
+ continue
|
|
|
+ if card.status != 0:
|
|
|
+ skipped.append({"id": card.id, "card_code": card.card_code, "reason": "非未激活"})
|
|
|
+ continue
|
|
|
+ card.is_revoked = 1
|
|
|
+ card.is_deleted = 1
|
|
|
+ card.revoked_at = now
|
|
|
+ card.revoked_reason = reason_clean
|
|
|
+ card.revoked_admin_id = current_admin.id
|
|
|
+ card.updated_at = now
|
|
|
+ revoked_codes.append(card.card_code)
|
|
|
+ create_card_history(db, card, "UPDATE")
|
|
|
+
|
|
|
+ if not revoked_codes:
|
|
|
+ return {"code": 400, "msg": "没有可注销的卡密", "data": {"skipped": skipped}}
|
|
|
+
|
|
|
+ client_ip = request.client.host if request.client else None
|
|
|
+ create_operation_log(
|
|
|
+ db=db,
|
|
|
+ admin_id=current_admin.id,
|
|
|
+ module="卡密管理",
|
|
|
+ action="批量注销卡密",
|
|
|
+ description=f"批量注销卡密:{len(revoked_codes)}张",
|
|
|
+ new_data={
|
|
|
+ "card_ids": req.ids,
|
|
|
+ "revoked_card_codes": revoked_codes,
|
|
|
+ "revoked_at": now.isoformat(),
|
|
|
+ "revoked_reason": reason_clean,
|
|
|
+ "revoked_admin_id": current_admin.id,
|
|
|
+ "is_deleted": 1,
|
|
|
+ "skipped": skipped,
|
|
|
+ },
|
|
|
+ ip_address=client_ip,
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+ return {"code": 200, "msg": "批量注销成功", "data": {"count": len(revoked_codes), "skipped": skipped}}
|
|
|
+ except Exception:
|
|
|
+ db.rollback()
|
|
|
+ logger.exception("批量注销卡密失败")
|
|
|
+ return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|
|
|
+
|
|
|
@router.get("/{card_id}/activate")
|
|
|
async def get_card_activate_info(
|
|
|
card_id: int,
|
|
|
@@ -231,7 +716,12 @@ async def get_card_activate_info(
|
|
|
"card_code": card.card_code,
|
|
|
"user_id": card.user_id,
|
|
|
"activated_at": card.activated_at.isoformat() if card.activated_at else None,
|
|
|
- "expire_time": card.expire_time.isoformat() if card.expire_time else None
|
|
|
+ "expire_time": card.expire_time.isoformat() if card.expire_time else None,
|
|
|
+ "issue_price": float(card.issue_price) if card.issue_price is not None else None,
|
|
|
+ "issued_to": card.issued_to,
|
|
|
+ "issue_remark": card.issue_remark,
|
|
|
+ "issued_at": card.issued_at.isoformat() if card.issued_at else None,
|
|
|
+ "issued_admin_id": card.issued_admin_id,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -241,10 +731,16 @@ async def export_cards_excel(
|
|
|
keyword: str = Query(None, description="搜索关键词"),
|
|
|
software_code: str = Query(None, description="软件特征码筛选"),
|
|
|
status: int = Query(None, description="状态筛选"),
|
|
|
+ is_gift: int = Query(None, ge=0, le=1, description="类型筛选:0-收费,1-赠送"),
|
|
|
+ is_extracted: int = Query(None, ge=0, le=1, description="提取状态筛选:0-未提取,1-已提取"),
|
|
|
+ is_revoked: int = Query(None, ge=0, le=1, description="注销筛选:0-未注销,1-已注销"),
|
|
|
+ special_close: int = Query(None, ge=0, le=1, description="特殊关闭筛选:0-关闭,1-打开"),
|
|
|
db: Session = Depends(get_db),
|
|
|
current_admin = Depends(get_current_admin)
|
|
|
):
|
|
|
"""导出卡密为Excel"""
|
|
|
+ from sqlalchemy import or_
|
|
|
+
|
|
|
from app.models.software import Software
|
|
|
from app.models.user import User
|
|
|
|
|
|
@@ -256,7 +752,10 @@ async def export_cards_excel(
|
|
|
|
|
|
# 搜索
|
|
|
if keyword:
|
|
|
- query = query.filter(Card.card_code.like(f"%{keyword}%"))
|
|
|
+ like = f"%{keyword}%"
|
|
|
+ query = query.filter(
|
|
|
+ or_(Card.card_code.like(like), Card.remark.like(like))
|
|
|
+ )
|
|
|
|
|
|
# 筛选
|
|
|
if software_code:
|
|
|
@@ -265,7 +764,19 @@ async def export_cards_excel(
|
|
|
if status is not None:
|
|
|
query = query.filter(Card.status == status)
|
|
|
|
|
|
- cards_with_names = query.order_by(Card.created_at.desc()).all()
|
|
|
+ if is_gift is not None:
|
|
|
+ query = query.filter(Card.is_gift == is_gift)
|
|
|
+
|
|
|
+ if is_extracted is not None:
|
|
|
+ query = query.filter(Card.is_extracted == is_extracted)
|
|
|
+
|
|
|
+ if is_revoked is not None:
|
|
|
+ query = query.filter(Card.is_revoked == is_revoked)
|
|
|
+
|
|
|
+ if special_close is not None:
|
|
|
+ query = query.filter(Card.special_close == special_close)
|
|
|
+
|
|
|
+ cards_with_names = query.order_by(Card.created_at.desc(), Card.id.desc()).all()
|
|
|
|
|
|
# 记录操作日志
|
|
|
from app.utils.history import create_operation_log
|
|
|
@@ -282,7 +793,11 @@ async def export_cards_excel(
|
|
|
"filters": {
|
|
|
"keyword": keyword,
|
|
|
"software_code": software_code,
|
|
|
- "status": status
|
|
|
+ "status": status,
|
|
|
+ "is_gift": is_gift,
|
|
|
+ "is_extracted": is_extracted,
|
|
|
+ "is_revoked": is_revoked,
|
|
|
+ "special_close": special_close,
|
|
|
}
|
|
|
},
|
|
|
ip_address=client_ip
|
|
|
@@ -293,11 +808,18 @@ async def export_cards_excel(
|
|
|
output = io.StringIO()
|
|
|
writer = csv.writer(output)
|
|
|
writer.writerow([
|
|
|
- 'ID', '卡密代码', '软件特征码', '软件名称', '天数', '折扣', '状态',
|
|
|
+ 'ID', '卡密代码', '软件特征码', '软件名称', '天数', '折扣', '状态', '卡密类型', '提取状态', '特殊关闭',
|
|
|
+ '注销状态', '注销时间', '注销原因', '注销管理员ID',
|
|
|
+ '发放价格', '发放对象', '发放备注', '发放时间', '发放管理员ID',
|
|
|
+ '备注',
|
|
|
'激活用户ID', '用户名称', '激活时间', '到期时间', '创建时间', '更新时间'
|
|
|
])
|
|
|
|
|
|
status_map = {0: '未激活', 1: '已激活', 2: '已过期'}
|
|
|
+ gift_map = {0: '收费', 1: '赠送'}
|
|
|
+ extracted_map = {0: '未提取', 1: '已提取'}
|
|
|
+ special_close_map = {0: '关闭', 1: '打开'}
|
|
|
+ revoked_map = {0: '未注销', 1: '已注销'}
|
|
|
for card, software_name, username in cards_with_names:
|
|
|
writer.writerow([
|
|
|
card.id,
|
|
|
@@ -307,6 +829,19 @@ async def export_cards_excel(
|
|
|
card.days,
|
|
|
float(card.discount),
|
|
|
status_map.get(card.status, '未知'),
|
|
|
+ gift_map.get(card.is_gift, '未知'),
|
|
|
+ extracted_map.get(card.is_extracted, '未知'),
|
|
|
+ special_close_map.get(getattr(card, "special_close", 0) or 0, '未知'),
|
|
|
+ revoked_map.get(getattr(card, "is_revoked", 0), '未知'),
|
|
|
+ card.revoked_at.isoformat() if getattr(card, "revoked_at", None) else '',
|
|
|
+ (card.revoked_reason or '') if getattr(card, "revoked_reason", None) is not None else '',
|
|
|
+ card.revoked_admin_id or '',
|
|
|
+ float(card.issue_price) if card.issue_price is not None else '',
|
|
|
+ card.issued_to or '',
|
|
|
+ card.issue_remark or '',
|
|
|
+ card.issued_at.isoformat() if card.issued_at else '',
|
|
|
+ card.issued_admin_id or '',
|
|
|
+ card.remark or '',
|
|
|
card.user_id or '',
|
|
|
username or '',
|
|
|
card.activated_at.isoformat() if card.activated_at else '',
|