2e2c161770
- 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
249 lines
6.5 KiB
Go
249 lines
6.5 KiB
Go
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)
|
||
}
|