Files
cpdaily_xiaozhushou/backed/internal/handler/cdk.go
T
becomer 2e2c161770 feat: init cpdaily-xiaozhushou with blue-green deploy
- backed/: Go Fiber backend with cpdaily integration
- frondend/: Vue 3 + Vite frontend
- Dockerfile: multi-stage build (Node → Go → Alpine)
- deploy/docker-compose.yml: production compose file
- .woodpecker/build.yml: CI/CD with blue-green zero-downtime deploy
- /health endpoint for deployment health checks
- Port 8884/8885 blue-green swap via 1Panel OpenResty
2026-07-03 15:37:55 +08:00

249 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"cpdaily-new/internal/config"
"cpdaily-new/internal/database"
"cpdaily-new/internal/model"
"cpdaily-new/internal/service"
"cpdaily-new/internal/utils"
"errors"
"log/slog"
"math/rand"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
// GenerateCDKRequest 生成CDK请求
type GenerateCDKRequest struct {
Count int `json:"count"` // 生成数量,默认1
MaxRuns int `json:"maxRuns"` // 每张CDK可运行次数,默认1
}
// 获取基础扣费(从系统设置或配置)
func getBaseDeduction() float64 {
var cfg model.SystemConfig
result := database.DB.Where("`key` = ?", "base_deduction_per_run").First(&cfg)
if result.Error == nil {
if v, err := strconv.ParseFloat(cfg.Value, 64); err == nil && v > 0 {
return v
}
}
// 兜底:使用 config.toml 中的值
return config.Global.Agent.SystemCdkPrice
}
// GenerateCDK 生成CDK
func GenerateCDK(c *fiber.Ctx) error {
agentID := c.Locals("adminId").(uint)
var req GenerateCDKRequest
if err := c.BodyParser(&req); err != nil {
return utils.FailParam(c, "请求参数错误")
}
if req.Count <= 0 {
req.Count = 1
}
if req.Count > 100 {
return utils.Fail(c, 400, "单次最多生成100个CDK")
}
if req.MaxRuns <= 0 {
req.MaxRuns = 1
}
if req.MaxRuns > 999 {
return utils.Fail(c, 400, "每张CDK最多999次")
}
// 获取代理信息
var agent model.AgentUser
if err := database.DB.First(&agent, agentID).Error; err != nil {
return utils.Fail(c, 404, "账号不存在")
}
if agent.Status == "disabled" {
return utils.Fail(c, 403, "账号已被禁用")
}
baseDeduction := getBaseDeduction()
cdkPrice := baseDeduction * float64(req.MaxRuns) * agent.Rate
costTotal := baseDeduction * float64(req.MaxRuns) * agent.Rate * float64(req.Count)
// 事务:扣余额 + 生成CDK
cdkCodes := make([]model.CDKCode, 0, req.Count)
err := database.DB.Transaction(func(tx *gorm.DB) error {
// 扣减代理余额(原子操作)
result := tx.Model(&model.AgentUser{}).
Where("id = ? AND balance >= ?", agentID, costTotal).
Update("balance", gorm.Expr("balance - ?", costTotal))
if result.RowsAffected == 0 {
return errors.New("余额不足")
}
// 批量生成CDK
now := time.Now()
for i := 0; i < req.Count; i++ {
code := generateCDKCode()
cdk := model.CDKCode{
Code: code,
Price: cdkPrice,
CostPrice: baseDeduction * float64(req.MaxRuns),
AgentID: agentID,
MaxRuns: req.MaxRuns,
Status: "unused",
CreatedAt: now,
UpdatedAt: now,
}
if err := tx.Create(&cdk).Error; err != nil {
return err
}
cdkCodes = append(cdkCodes, cdk)
}
return nil
})
if err != nil {
if err.Error() == "余额不足" {
return utils.Fail(c, 400, "账户余额不足,需要 "+formatFloat(costTotal)+" 元")
}
slog.Error("生成CDK失败", "error", err)
return utils.FailServer(c, "生成失败")
}
slog.Info("生成CDK成功", "agentId", agentID, "count", req.Count, "maxRuns", req.MaxRuns, "price", cdkPrice)
return utils.Success(c, fiber.Map{
"count": len(cdkCodes),
"price": cdkPrice,
"maxRuns": req.MaxRuns,
"costTotal": costTotal,
"cdkCodes": cdkCodes,
})
}
// ListCDKs 获取自己生成的CDK列表
func ListCDKs(c *fiber.Ctx) error {
agentID := c.Locals("adminId").(uint)
var cdks []model.CDKCode
database.DB.Where("agent_id = ?", agentID).Order("created_at DESC").Limit(100).Find(&cdks)
return utils.Success(c, cdks)
}
// ListAllCDKs 获取所有CDK列表(仅admin)
func ListAllCDKs(c *fiber.Ctx) error {
var cdks []model.CDKCode
database.DB.Preload("Agent").Order("created_at DESC").Limit(200).Find(&cdks)
return utils.Success(c, cdks)
}
// UserRedeemCDKRequest 用户兑换CDK请求
type UserRedeemCDKRequest struct {
Code string `json:"code"`
CaptchaID string `json:"captchaId"`
CaptchaCode string `json:"captchaCode"`
}
func UserRedeemCDK(c *fiber.Ctx) error {
userID := c.Locals("userId").(uint)
var req UserRedeemCDKRequest
if err := c.BodyParser(&req); err != nil {
return utils.FailParam(c, "请求参数错误")
}
if req.Code == "" {
return utils.FailParam(c, "CDK码不能为空")
}
if req.CaptchaID == "" || req.CaptchaCode == "" {
return utils.FailParam(c, "验证码不能为空")
}
// 校验图片验证码
if !service.VerifyCaptcha(req.CaptchaID, req.CaptchaCode) {
return utils.FailParam(c, "验证码错误或已过期")
}
// 查找CDK
var cdk model.CDKCode
err := database.DB.Where("code = ? AND status = 'unused'", req.Code).First(&cdk).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return utils.Fail(c, 404, "CDK码无效或已被使用")
}
return utils.FailServer(c, "查询失败")
}
// 使用行锁(悲观锁)确保并发安全
err = database.DB.Transaction(func(tx *gorm.DB) error {
// 重新查询并锁定该行
var lockedCDK model.CDKCode
if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&lockedCDK, cdk.ID).Error; err != nil {
return err
}
if lockedCDK.Status != "unused" {
return errors.New("CDK码已被使用")
}
if lockedCDK.MaxRuns <= 0 {
return errors.New("CDK码已无可运行次数")
}
now := time.Now()
// 一次性消耗全部次数
totalRuns := lockedCDK.MaxRuns
updates := map[string]interface{}{
"status": "used",
"user_id": userID,
"used_at": now,
}
if err := tx.Model(&lockedCDK).Updates(updates).Error; err != nil {
return err
}
// 增加用户签到次数(一次性加上所有剩余次数)
if err := tx.Model(&model.SysUser{}).Where("id = ?", userID).
Update("sign_count", gorm.Expr("sign_count + ?", totalRuns)).Error; err != nil {
return err
}
return nil
})
if err != nil {
if err.Error() == "CDK码已被使用" {
return utils.Fail(c, 400, "CDK码已被使用")
}
if err.Error() == "CDK码已无可运行次数" {
return utils.Fail(c, 400, "CDK码已无可运行次数")
}
slog.Error("兑换CDK失败", "error", err)
return utils.FailServer(c, "兑换失败")
}
slog.Info("CDK兑换成功", "userId", userID, "code", req.Code)
return utils.Success(c, fiber.Map{
"message": "兑换成功",
"totalRuns": cdk.MaxRuns,
})
}
// 生成CDK码(12位大写字母数字)
func generateCDKCode() string {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
code := make([]byte, 12)
for i := range code {
code[i] = chars[rng.Intn(len(chars))]
}
return string(code)
}
// formatFloat 格式化浮点数为字符串保留2位小数
func formatFloat(f float64) string {
return utils.FormatFloat(f)
}