325 lines
8.0 KiB
Go
325 lines
8.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gpt-plus/internal/db"
|
|
"gpt-plus/pkg/provider/card"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func RegisterCardRoutes(api *gin.RouterGroup) {
|
|
// Cards
|
|
api.GET("/cards", ListCards)
|
|
api.GET("/cards/active", GetActiveCard)
|
|
api.GET("/cards/stats", GetCardStats)
|
|
api.POST("/cards", AddCard)
|
|
api.PUT("/cards/:id/activate", ActivateCard)
|
|
api.PUT("/cards/:id/status", UpdateCardStatus)
|
|
api.DELETE("/cards/:id", DeleteCard)
|
|
|
|
// Card codes
|
|
api.GET("/card-codes", ListCardCodes)
|
|
api.GET("/card-codes/stats", GetCardCodeStats)
|
|
api.POST("/card-codes/import", ImportCardCodes)
|
|
api.POST("/card-codes/redeem", RedeemCardCode)
|
|
api.DELETE("/card-codes/:id", DeleteCardCode)
|
|
}
|
|
|
|
func ListCards(c *gin.Context) {
|
|
d := db.GetDB()
|
|
query := d.Model(&db.Card{}).Order("created_at DESC")
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
|
|
p := db.PaginationParams{
|
|
Page: intQuery(c, "page", 1),
|
|
Size: intQuery(c, "size", 20),
|
|
}
|
|
|
|
var cards []db.Card
|
|
result, err := db.Paginate(query, p, &cards)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
for i := range cards {
|
|
number, _ := db.Decrypt(cards[i].NumberEnc)
|
|
cards[i].NumberLast4 = number
|
|
cvc, _ := db.Decrypt(cards[i].CVCEnc)
|
|
cards[i].CVCPlain = cvc
|
|
}
|
|
result.Items = cards
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func GetActiveCard(c *gin.Context) {
|
|
d := db.GetDB()
|
|
var card db.Card
|
|
if err := d.Where("status = ?", "active").First(&card).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "无激活的卡片"})
|
|
return
|
|
}
|
|
number, _ := db.Decrypt(card.NumberEnc)
|
|
card.NumberLast4 = number
|
|
cvc, _ := db.Decrypt(card.CVCEnc)
|
|
card.CVCPlain = cvc
|
|
c.JSON(http.StatusOK, card)
|
|
}
|
|
|
|
func GetCardStats(c *gin.Context) {
|
|
c.JSON(http.StatusOK, db.GetCardStats(db.GetDB()))
|
|
}
|
|
|
|
type addCardRequest struct {
|
|
Number string `json:"number" binding:"required"`
|
|
CVC string `json:"cvc" binding:"required"`
|
|
ExpMonth string `json:"exp_month" binding:"required"`
|
|
ExpYear string `json:"exp_year" binding:"required"`
|
|
Name string `json:"name"`
|
|
Country string `json:"country"`
|
|
}
|
|
|
|
func AddCard(c *gin.Context) {
|
|
var req addCardRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
numberEnc, _ := db.Encrypt(req.Number)
|
|
cvcEnc, _ := db.Encrypt(req.CVC)
|
|
|
|
maxBinds := 0 // 0 = unlimited
|
|
var cfg db.SystemConfig
|
|
if db.GetDB().Where("key = ?", "card.max_binds").First(&cfg).Error == nil {
|
|
maxBinds, _ = strconv.Atoi(cfg.Value)
|
|
}
|
|
|
|
card := &db.Card{
|
|
NumberHash: db.HashSHA256(req.Number),
|
|
NumberEnc: numberEnc,
|
|
CVCEnc: cvcEnc,
|
|
ExpMonth: req.ExpMonth,
|
|
ExpYear: req.ExpYear,
|
|
Name: req.Name,
|
|
Country: req.Country,
|
|
Source: "manual",
|
|
Status: "available",
|
|
MaxBinds: maxBinds,
|
|
}
|
|
|
|
if err := db.GetDB().Create(card).Error; err != nil {
|
|
if strings.Contains(err.Error(), "UNIQUE") {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "该卡号已存在"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, gin.H{"id": card.ID, "message": "卡片已添加"})
|
|
}
|
|
|
|
func ActivateCard(c *gin.Context) {
|
|
id := c.Param("id")
|
|
d := db.GetDB()
|
|
|
|
// Deactivate current active card
|
|
d.Model(&db.Card{}).Where("status = ?", "active").Update("status", "available")
|
|
|
|
var card db.Card
|
|
if err := d.First(&card, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "卡片不存在"})
|
|
return
|
|
}
|
|
card.Status = "active"
|
|
d.Save(&card)
|
|
c.JSON(http.StatusOK, gin.H{"message": "已激活"})
|
|
}
|
|
|
|
func UpdateCardStatus(c *gin.Context) {
|
|
id := c.Param("id")
|
|
var req struct {
|
|
Status string `json:"status" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if req.Status != "disabled" && req.Status != "available" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "状态只能为 disabled 或 available"})
|
|
return
|
|
}
|
|
if err := db.GetDB().Model(&db.Card{}).Where("id = ?", id).Update("status", req.Status).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "状态已更新"})
|
|
}
|
|
|
|
func DeleteCard(c *gin.Context) {
|
|
if err := db.GetDB().Delete(&db.Card{}, c.Param("id")).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
|
|
}
|
|
|
|
// Card Codes
|
|
|
|
func ListCardCodes(c *gin.Context) {
|
|
d := db.GetDB()
|
|
query := d.Model(&db.CardCode{}).Order("created_at DESC")
|
|
if status := c.Query("status"); status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
p := db.PaginationParams{Page: intQuery(c, "page", 1), Size: intQuery(c, "size", 20)}
|
|
var codes []db.CardCode
|
|
result, err := db.Paginate(query, p, &codes)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func GetCardCodeStats(c *gin.Context) {
|
|
c.JSON(http.StatusOK, db.GetCardCodeStats(db.GetDB()))
|
|
}
|
|
|
|
func ImportCardCodes(c *gin.Context) {
|
|
var req struct {
|
|
Codes []string `json:"codes" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
d := db.GetDB()
|
|
var count int
|
|
for _, code := range req.Codes {
|
|
code = strings.TrimSpace(code)
|
|
if code == "" {
|
|
continue
|
|
}
|
|
cc := &db.CardCode{Code: code, Status: "unused"}
|
|
if d.Create(cc).Error == nil {
|
|
count++
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"imported": count})
|
|
}
|
|
|
|
func RedeemCardCode(c *gin.Context) {
|
|
var req struct {
|
|
ID uint `json:"id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
d := db.GetDB()
|
|
var code db.CardCode
|
|
if err := d.First(&code, req.ID).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "卡密不存在"})
|
|
return
|
|
}
|
|
if code.Status != "unused" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "该卡密状态不是 unused"})
|
|
return
|
|
}
|
|
|
|
code.Status = "redeeming"
|
|
d.Save(&code)
|
|
|
|
// Redeem in background
|
|
go func() {
|
|
var cfgBase db.SystemConfig
|
|
apiBase := ""
|
|
if db.GetDB().Where("key = ?", "card.api_base_url").First(&cfgBase).Error == nil {
|
|
apiBase = cfgBase.Value
|
|
}
|
|
if apiBase == "" {
|
|
code.Status = "failed"
|
|
code.Error = "开卡 API 地址未配置"
|
|
d.Save(&code)
|
|
return
|
|
}
|
|
|
|
apiProv, err := card.NewAPIProvider(card.APIProviderConfig{
|
|
BaseURL: apiBase,
|
|
Codes: []string{code.Code},
|
|
PoolCfg: card.PoolConfig{MultiBind: true, MaxBinds: 999},
|
|
})
|
|
if err != nil {
|
|
code.Status = "failed"
|
|
code.Error = err.Error()
|
|
d.Save(&code)
|
|
return
|
|
}
|
|
|
|
cardInfo, err := apiProv.GetCard(context.Background())
|
|
if err != nil {
|
|
code.Status = "failed"
|
|
code.Error = err.Error()
|
|
d.Save(&code)
|
|
return
|
|
}
|
|
|
|
numberEnc, _ := db.Encrypt(cardInfo.Number)
|
|
cvcEnc, _ := db.Encrypt(cardInfo.CVC)
|
|
newCard := &db.Card{
|
|
NumberHash: db.HashSHA256(cardInfo.Number),
|
|
NumberEnc: numberEnc,
|
|
CVCEnc: cvcEnc,
|
|
ExpMonth: cardInfo.ExpMonth,
|
|
ExpYear: cardInfo.ExpYear,
|
|
Name: cardInfo.Name,
|
|
Country: cardInfo.Country,
|
|
Source: "api",
|
|
CardCodeID: &code.ID,
|
|
Status: "available",
|
|
MaxBinds: 1,
|
|
}
|
|
d.Create(newCard)
|
|
|
|
now := time.Now()
|
|
code.Status = "redeemed"
|
|
code.CardID = &newCard.ID
|
|
code.RedeemedAt = &now
|
|
d.Save(&code)
|
|
}()
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "兑换已开始"})
|
|
}
|
|
|
|
func DeleteCardCode(c *gin.Context) {
|
|
if err := db.GetDB().Delete(&db.CardCode{}, c.Param("id")).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
|
|
}
|
|
|
|
func intQuery(c *gin.Context, key string, def int) int {
|
|
v := c.Query(key)
|
|
if v == "" {
|
|
return def
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return n
|
|
}
|