Initial sanitized code sync
This commit is contained in:
324
internal/handler/card.go
Normal file
324
internal/handler/card.go
Normal file
@@ -0,0 +1,324 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user