feat: implement multi-user credits redeem and storage isolation
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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) 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})
|
||||
}
|
||||
Reference in New Issue
Block a user