149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func hashRedeemCode(code string) string {
|
|
s := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code))))
|
|
return hex.EncodeToString(s[:])
|
|
}
|
|
|
|
func generateRedeemCode() (string, error) {
|
|
b := make([]byte, 9)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
out := make([]byte, 12)
|
|
for i := range out {
|
|
out[i] = alphabet[int(b[i%len(b)])%len(alphabet)]
|
|
}
|
|
return string(out[:4]) + "-" + string(out[4:8]) + "-" + string(out[8:]), nil
|
|
}
|
|
|
|
func (a *App) adminCreateRedeemBatch(c *gin.Context) {
|
|
var in struct {
|
|
Credits int64 `json:"credits"`
|
|
Count int `json:"count"`
|
|
ExpiresAt *time.Time `json:"expiresAt"`
|
|
Note string `json:"note"`
|
|
}
|
|
if c.BindJSON(&in) != nil || in.Credits <= 0 || in.Count < 1 || in.Count > 10000 {
|
|
c.JSON(400, gin.H{"error": "credits/count invalid"})
|
|
return
|
|
}
|
|
admin := c.MustGet("user").(*User)
|
|
batch := RedeemBatch{Code: fmt.Sprintf("B-%d", time.Now().UnixNano()), Credits: in.Credits, Count: in.Count, ExpiresAt: in.ExpiresAt, Note: in.Note, CreatedBy: admin.ID}
|
|
codes := make([]string, 0, in.Count)
|
|
err := a.db.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Create(&batch).Error; err != nil {
|
|
return err
|
|
}
|
|
for i := 0; i < in.Count; i++ {
|
|
code, err := generateRedeemCode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row := RedeemCode{BatchID: batch.ID, CodeHash: hashRedeemCode(code), Prefix: code[:4], Credits: in.Credits, Status: "unused", ExpiresAt: in.ExpiresAt}
|
|
if err := tx.Create(&row).Error; err != nil {
|
|
return err
|
|
}
|
|
codes = append(codes, code)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(201, gin.H{"batch": batch, "codes": codes})
|
|
}
|
|
|
|
func (a *App) redeem(c *gin.Context) {
|
|
user := c.MustGet("user").(*User)
|
|
var in struct {
|
|
Code string `json:"code"`
|
|
}
|
|
if c.BindJSON(&in) != nil || strings.TrimSpace(in.Code) == "" {
|
|
c.JSON(400, gin.H{"error": "code required"})
|
|
return
|
|
}
|
|
now := time.Now()
|
|
var amount int64
|
|
err := a.db.Transaction(func(tx *gorm.DB) error {
|
|
var code RedeemCode
|
|
if err := tx.Where("code_hash = ?", hashRedeemCode(in.Code)).First(&code).Error; err != nil {
|
|
return err
|
|
}
|
|
if code.Status != "unused" {
|
|
return errors.New("code already used or unavailable")
|
|
}
|
|
if code.ExpiresAt != nil && code.ExpiresAt.Before(now) {
|
|
return errors.New("code expired")
|
|
}
|
|
var account CreditAccount
|
|
if err := tx.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
account = CreditAccount{UserID: user.ID}
|
|
if err := tx.Create(&account).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
account.Balance += code.Credits
|
|
if err := tx.Save(&account).Error; err != nil {
|
|
return err
|
|
}
|
|
code.Status, code.RedeemedBy, code.RedeemedAt = "used", &user.ID, &now
|
|
if err := tx.Save(&code).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Create(&RedeemRecord{CodeID: code.ID, BatchID: code.BatchID, UserID: user.ID, Credits: code.Credits}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Create(&CreditTransaction{UserID: user.ID, Type: "redeem", Amount: code.Credits, BalanceAfter: account.Balance, ReferenceType: "redeem_code", ReferenceID: fmt.Sprint(code.ID), Reason: "redeem code"}).Error; err != nil {
|
|
return err
|
|
}
|
|
amount = code.Credits
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
status := 500
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
status = 404
|
|
}
|
|
c.JSON(status, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"credits": amount})
|
|
}
|
|
|
|
func (a *App) adminRevokeRedeemBatch(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := a.db.Model(&RedeemCode{}).Where("batch_id = ? AND status = ?", id, "unused").Update("status", "revoked").Error; err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (a *App) adminListRedeemBatches(c *gin.Context) {
|
|
var batches []RedeemBatch
|
|
if err := a.db.Order("id desc").Limit(100).Find(&batches).Error; err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"data": batches})
|
|
}
|