diff --git a/api/core/config.go b/api/core/config.go index 04160490..153a38c6 100644 --- a/api/core/config.go +++ b/api/core/config.go @@ -38,7 +38,6 @@ func NewDefaultConfig() *types.AppConfig { BasePath: "./static/upload", }, }, - WeChatBot: false, AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false}, } } diff --git a/api/core/types/config.go b/api/core/types/config.go index 2085dc9c..34287bae 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -24,7 +24,6 @@ type AppConfig struct { ApiConfig ApiConfig // ChatPlus API authorization configs SMS SMSConfig // send mobile message config OSS OSSConfig // OSS config - WeChatBot bool // 是否启用微信机器人 XXLConfig XXLConfig AlipayConfig AlipayConfig // 支付宝支付渠道配置 diff --git a/api/handler/admin/dashboard_handler.go b/api/handler/admin/dashboard_handler.go index 536cd497..2ce5d34e 100644 --- a/api/handler/admin/dashboard_handler.go +++ b/api/handler/admin/dashboard_handler.go @@ -60,13 +60,6 @@ func (h *DashboardHandler) Stats(c *gin.Context) { stats.Tokens += item.Tokens } - // 众筹收入 - var rewards []model.Reward - res = h.DB.Where("created_at > ?", zeroTime).Find(&rewards) - for _, item := range rewards { - stats.Income += item.Amount - } - // 订单收入 var orders []model.Order res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders) @@ -101,13 +94,6 @@ func (h *DashboardHandler) Stats(c *gin.Context) { historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens) } - // 浮点数相加? - // 统计最近7天的众筹 - res = h.DB.Where("created_at > ?", startDate).Find(&rewards) - for _, item := range rewards { - incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64() - } - // 统计最近7天的订单 res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders) for _, item := range orders { diff --git a/api/handler/admin/redeem_handler.go b/api/handler/admin/redeem_handler.go new file mode 100644 index 00000000..97fc3620 --- /dev/null +++ b/api/handler/admin/redeem_handler.go @@ -0,0 +1,164 @@ +package admin + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "geekai/core" + "geekai/core/types" + "geekai/handler" + "geekai/store/model" + "geekai/store/vo" + "geekai/utils" + "geekai/utils/resp" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type RedeemHandler struct { + handler.BaseHandler +} + +func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler { + return &RedeemHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} +} + +func (h *RedeemHandler) List(c *gin.Context) { + page := h.GetInt(c, "page", 1) + pageSize := h.GetInt(c, "page_size", 20) + code := c.Query("code") + status := h.GetInt(c, "status", -1) + + session := h.DB.Session(&gorm.Session{}) + if code != "" { + session.Where("code LIKE ?", "%"+code+"%") + } + if status == 0 { + session.Where("redeem_at = ?", 0) + } else if status == 1 { + session.Where("redeem_at > ?", 0) + } + + var total int64 + session.Model(&model.Redeem{}).Count(&total) + var redeems []model.Redeem + offset := (page - 1) * pageSize + err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&redeems).Error + if err != nil { + resp.ERROR(c, err.Error()) + return + } + var items = make([]vo.Redeem, 0) + userIds := make([]uint, 0) + for _, v := range redeems { + userIds = append(userIds, v.UserId) + } + var users []model.User + h.DB.Where("id IN ?", userIds).Find(&users) + var userMap = make(map[uint]model.User) + for _, u := range users { + userMap[u.Id] = u + } + + for _, v := range redeems { + var r vo.Redeem + err = utils.CopyObject(v, &r) + if err != nil { + continue + } + + r.Id = v.Id + r.Username = userMap[v.UserId].Username + r.CreatedAt = v.CreatedAt.Unix() + items = append(items, r) + } + + resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items)) +} + +func (h *RedeemHandler) Create(c *gin.Context) { + var data struct { + Name string `json:"name"` + Power int `json:"power"` + Num int `json:"num"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + counter := 0 + codes := make([]string, 0) + var errMsg = "" + if data.Num > 0 { + for i := 0; i < data.Num; i++ { + code, err := utils.GenRedeemCode(32) + if err != nil { + errMsg = err.Error() + continue + } + err = h.DB.Create(&model.Redeem{ + Code: code, + Name: data.Name, + Power: data.Power, + Enabled: true, + }).Error + if err != nil { + errMsg = err.Error() + continue + } + codes = append(codes, code) + counter++ + } + } + if counter == 0 { + resp.ERROR(c, errMsg) + return + } + + resp.SUCCESS(c, gin.H{ + "counter": counter, + }) +} + +func (h *RedeemHandler) Set(c *gin.Context) { + var data struct { + Id uint `json:"id"` + Filed string `json:"filed"` + Value interface{} `json:"value"` + } + + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + err := h.DB.Model(&model.Redeem{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error + if err != nil { + resp.ERROR(c, err.Error()) + return + } + resp.SUCCESS(c) +} + +func (h *RedeemHandler) Remove(c *gin.Context) { + var data struct { + Id uint + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + if data.Id > 0 { + err := h.DB.Where("id", data.Id).Delete(&model.Redeem{}).Error + if err != nil { + resp.ERROR(c, err.Error()) + return + } + } + resp.SUCCESS(c) +} diff --git a/api/handler/admin/reward_handler.go b/api/handler/admin/reward_handler.go deleted file mode 100644 index fb479d3c..00000000 --- a/api/handler/admin/reward_handler.go +++ /dev/null @@ -1,80 +0,0 @@ -package admin - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "geekai/core" - "geekai/core/types" - "geekai/handler" - "geekai/store/model" - "geekai/store/vo" - "geekai/utils" - "geekai/utils/resp" - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -type RewardHandler struct { - handler.BaseHandler -} - -func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler { - return &RewardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} -} - -func (h *RewardHandler) List(c *gin.Context) { - var items []model.Reward - res := h.DB.Order("id DESC").Find(&items) - var rewards = make([]vo.Reward, 0) - if res.Error == nil { - userIds := make([]uint, 0) - for _, v := range items { - userIds = append(userIds, v.UserId) - } - var users []model.User - h.DB.Where("id IN ?", userIds).Find(&users) - var userMap = make(map[uint]model.User) - for _, u := range users { - userMap[u.Id] = u - } - - for _, v := range items { - var r vo.Reward - err := utils.CopyObject(v, &r) - if err != nil { - continue - } - - r.Id = v.Id - r.Username = userMap[v.UserId].Username - r.CreatedAt = v.CreatedAt.Unix() - r.UpdatedAt = v.UpdatedAt.Unix() - rewards = append(rewards, r) - } - } - - resp.SUCCESS(c, rewards) -} - -func (h *RewardHandler) Remove(c *gin.Context) { - var data struct { - Id uint - } - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - if data.Id > 0 { - err := h.DB.Where("id = ?", data.Id).Delete(&model.Reward{}).Error - if err != nil { - resp.ERROR(c, err.Error()) - return - } - } - resp.SUCCESS(c) -} diff --git a/api/handler/admin/user_handler.go b/api/handler/admin/user_handler.go index 7a32595a..07b93700 100644 --- a/api/handler/admin/user_handler.go +++ b/api/handler/admin/user_handler.go @@ -225,7 +225,7 @@ func (h *UserHandler) Remove(c *gin.Context) { // 删除算力日志 h.DB.Where("user_id = ?", id).Delete(&model.PowerLog{}) // 删除众筹日志 - h.DB.Where("user_id = ?", id).Delete(&model.Reward{}) + h.DB.Where("user_id = ?", id).Delete(&model.Redeem{}) // 删除绘图任务 h.DB.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}) h.DB.Where("user_id = ?", id).Delete(&model.SdJob{}) diff --git a/api/handler/redeem_handler.go b/api/handler/redeem_handler.go new file mode 100644 index 00000000..ba1fb4ed --- /dev/null +++ b/api/handler/redeem_handler.go @@ -0,0 +1,102 @@ +package handler + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "fmt" + "geekai/core" + "geekai/core/types" + "geekai/store/model" + "geekai/utils/resp" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "sync" + "time" +) + +type RedeemHandler struct { + BaseHandler + lock sync.Mutex +} + +func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler { + return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}} +} + +func (h *RedeemHandler) Verify(c *gin.Context) { + var data struct { + Code string `json:"code"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + userId := h.GetLoginUserId(c) + + h.lock.Lock() + defer h.lock.Unlock() + + var item model.Redeem + res := h.DB.Where("code", data.Code).First(&item) + if res.Error != nil { + resp.ERROR(c, "无效的兑换码!") + return + } + + if !item.Enabled { + resp.ERROR(c, "当前兑换码已被禁用!") + return + } + + if item.RedeemedAt > 0 { + resp.ERROR(c, "当前兑换码已使用,请勿重复使用!") + return + } + + tx := h.DB.Begin() + err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power + ?", item.Power)).Error + if err != nil { + tx.Rollback() + resp.ERROR(c, err.Error()) + return + } + + // 更新核销状态 + item.RedeemedAt = time.Now().Unix() + item.UserId = userId + err = tx.Updates(&item).Error + if err != nil { + tx.Rollback() + resp.ERROR(c, err.Error()) + return + } + + // 记录算力充值日志 + var user model.User + err = h.DB.Where("id", userId).First(&user).Error + if err != nil { + tx.Rollback() + resp.ERROR(c, err.Error()) + return + } + + h.DB.Create(&model.PowerLog{ + UserId: userId, + Username: user.Username, + Type: types.PowerReward, + Amount: item.Power, + Balance: user.Power, + Mark: types.PowerAdd, + Model: "兑换码", + Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]), + CreatedAt: time.Now(), + }) + tx.Commit() + resp.SUCCESS(c) + +} diff --git a/api/handler/reward_handler.go b/api/handler/reward_handler.go deleted file mode 100644 index 6db5834f..00000000 --- a/api/handler/reward_handler.go +++ /dev/null @@ -1,106 +0,0 @@ -package handler - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "fmt" - "geekai/core" - "geekai/core/types" - "geekai/store/model" - "geekai/store/vo" - "geekai/utils" - "geekai/utils/resp" - "github.com/gin-gonic/gin" - "gorm.io/gorm" - "math" - "strings" - "sync" - "time" -) - -type RewardHandler struct { - BaseHandler - lock sync.Mutex -} - -func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler { - return &RewardHandler{BaseHandler: BaseHandler{App: app, DB: db}} -} - -// Verify 打赏码核销 -func (h *RewardHandler) Verify(c *gin.Context) { - var data struct { - TxId string `json:"tx_id"` - } - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - user, err := h.GetLoginUser(c) - if err != nil { - resp.HACKER(c) - return - } - - // 移除转账单号中间的空格,防止有人复制的时候多复制了空格 - data.TxId = strings.ReplaceAll(data.TxId, " ", "") - - h.lock.Lock() - defer h.lock.Unlock() - - var item model.Reward - res := h.DB.Where("tx_id = ?", data.TxId).First(&item) - if res.Error != nil { - resp.ERROR(c, "无效的众筹交易流水号!") - return - } - - if item.Status { - resp.ERROR(c, "当前众筹交易流水号已经被核销,请不要重复核销!") - return - } - - tx := h.DB.Begin() - exchange := vo.RewardExchange{} - power := math.Ceil(item.Amount / h.App.SysConfig.PowerPrice) - exchange.Power = int(power) - err = tx.Model(&user).UpdateColumn("power", gorm.Expr("power + ?", exchange.Power)).Error - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } - - // 更新核销状态 - item.Status = true - item.UserId = user.Id - item.Exchange = utils.JsonEncode(exchange) - err = tx.Updates(&item).Error - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } - - // 记录算力充值日志 - h.DB.Create(&model.PowerLog{ - UserId: user.Id, - Username: user.Username, - Type: types.PowerReward, - Amount: exchange.Power, - Balance: user.Power + exchange.Power, - Mark: types.PowerAdd, - Model: "众筹支付", - Remark: fmt.Sprintf("众筹充值算力,金额:%f,价格:%f", item.Amount, h.App.SysConfig.PowerPrice), - CreatedAt: time.Now(), - }) - tx.Commit() - resp.SUCCESS(c) - -} diff --git a/api/main.go b/api/main.go index d91c892f..f8ed1b5e 100644 --- a/api/main.go +++ b/api/main.go @@ -24,7 +24,6 @@ import ( "geekai/service/sd" "geekai/service/sms" "geekai/service/suno" - "geekai/service/wx" "geekai/store" "io" "log" @@ -131,7 +130,7 @@ func main() { fx.Provide(chatimpl.NewChatHandler), fx.Provide(handler.NewUploadHandler), fx.Provide(handler.NewSmsHandler), - fx.Provide(handler.NewRewardHandler), + fx.Provide(handler.NewRedeemHandler), fx.Provide(handler.NewCaptchaHandler), fx.Provide(handler.NewMidJourneyHandler), fx.Provide(handler.NewChatModelHandler), @@ -147,7 +146,7 @@ func main() { fx.Provide(admin.NewApiKeyHandler), fx.Provide(admin.NewUserHandler), fx.Provide(admin.NewChatRoleHandler), - fx.Provide(admin.NewRewardHandler), + fx.Provide(admin.NewRedeemHandler), fx.Provide(admin.NewDashboardHandler), fx.Provide(admin.NewChatModelHandler), fx.Provide(admin.NewProductHandler), @@ -177,17 +176,6 @@ func main() { licenseService.SyncLicense() }), - // 微信机器人服务 - fx.Provide(wx.NewWeChatBot), - fx.Invoke(func(config *types.AppConfig, bot *wx.Bot) { - if config.WeChatBot { - err := bot.Run() - if err != nil { - logger.Error("微信登录失败:", err) - } - } - }), - // MidJourney service pool fx.Provide(mj.NewService), fx.Provide(mj.NewClient), @@ -276,8 +264,8 @@ func main() { group.GET("slide/get", h.SlideGet) group.POST("slide/check", h.SlideCheck) }), - fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) { - group := s.Engine.Group("/api/reward/") + fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) { + group := s.Engine.Group("/api/redeem/") group.POST("verify", h.Verify) }), fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) { @@ -348,9 +336,11 @@ func main() { group.POST("set", h.Set) group.GET("remove", h.Remove) }), - fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) { - group := s.Engine.Group("/api/admin/reward/") + fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) { + group := s.Engine.Group("/api/admin/redeem/") group.GET("list", h.List) + group.POST("create", h.Create) + group.POST("set", h.Set) group.POST("remove", h.Remove) }), fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { diff --git a/api/service/wx/bot.go b/api/service/wx/bot.go deleted file mode 100644 index 47384581..00000000 --- a/api/service/wx/bot.go +++ /dev/null @@ -1,101 +0,0 @@ -package wx - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - logger2 "geekai/logger" - "geekai/store/model" - "github.com/eatmoreapple/openwechat" - "github.com/skip2/go-qrcode" - "gorm.io/gorm" - "os" - "strconv" -) - -// 微信收款机器人 -var logger = logger2.GetLogger() - -type Bot struct { - bot *openwechat.Bot - token string - db *gorm.DB -} - -func NewWeChatBot(db *gorm.DB) *Bot { - bot := openwechat.DefaultBot(openwechat.Desktop) - return &Bot{ - bot: bot, - db: db, - } -} - -func (b *Bot) Run() error { - logger.Info("Starting WeChat Bot...") - - // set message handler - b.bot.MessageHandler = func(msg *openwechat.Message) { - b.messageHandler(msg) - } - // scan code login callback - b.bot.UUIDCallback = b.qrCodeCallBack - debug, err := strconv.ParseBool(os.Getenv("APP_DEBUG")) - if debug { - reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json") - err = b.bot.HotLogin(reloadStorage, true) - } else { - err = b.bot.Login() - } - if err != nil { - return err - } - - logger.Info("微信登录成功!") - return nil -} - -// message handler -func (b *Bot) messageHandler(msg *openwechat.Message) { - sender, err := msg.Sender() - if err != nil { - return - } - - // 只处理微信支付的推送消息 - if sender.NickName == "微信支付" || - msg.MsgType == openwechat.MsgTypeApp || - msg.AppMsgType == openwechat.AppMsgTypeUrl { - // 解析支付金额 - message := parseTransactionMessage(msg.Content) - transaction := extractTransaction(message) - logger.Infof("解析到收款信息:%+v", transaction) - if transaction.TransId != "" { - var item model.Reward - res := b.db.Where("tx_id = ?", transaction.TransId).First(&item) - if item.Id > 0 { - logger.Error("当前交易 ID 己经存在!") - return - } - - res = b.db.Create(&model.Reward{ - TxId: transaction.TransId, - Amount: transaction.Amount, - Remark: transaction.Remark, - Status: false, - }) - if res.Error != nil { - logger.Errorf("交易保存失败: %v", res.Error) - } - } - } -} - -func (b *Bot) qrCodeCallBack(uuid string) { - logger.Info("请使用微信扫描下面二维码登录") - q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.Medium) - logger.Info(q.ToString(true)) -} diff --git a/api/service/wx/tranaction.go b/api/service/wx/tranaction.go deleted file mode 100644 index 2dfb529e..00000000 --- a/api/service/wx/tranaction.go +++ /dev/null @@ -1,112 +0,0 @@ -package wx - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "encoding/xml" - "net/url" - "strconv" - "strings" -) - -// Message 转账消息 -type Message struct { - Des string - Url string -} - -// Transaction 解析后的交易信息 -type Transaction struct { - TransId string `json:"trans_id"` // 微信转账交易 ID - Amount float64 `json:"amount"` // 微信转账交易金额 - Remark string `json:"remark"` // 转账备注 -} - -// 解析微信转账消息 -func parseTransactionMessage(xmlData string) *Message { - decoder := xml.NewDecoder(strings.NewReader(xmlData)) - message := Message{} - for { - token, err := decoder.Token() - if err != nil { - break - } - - switch se := token.(type) { - case xml.StartElement: - var value string - if se.Name.Local == "des" && message.Des == "" { - if err := decoder.DecodeElement(&value, &se); err == nil { - message.Des = strings.TrimSpace(value) - } - break - } - if se.Name.Local == "weapp_path" || se.Name.Local == "url" { - if err := decoder.DecodeElement(&value, &se); err == nil { - if strings.Contains(value, "?trans_id=") || strings.Contains(value, "?id=") { - message.Url = value - } - } - break - } - } - } - - // 兼容旧版消息记录 - if message.Url == "" { - var msg struct { - XMLName xml.Name `xml:"msg"` - AppMsg struct { - Des string `xml:"des"` - Url string `xml:"url"` - } `xml:"appmsg"` - } - if err := xml.Unmarshal([]byte(xmlData), &msg); err == nil { - message.Url = msg.AppMsg.Url - } - } - return &message -} - -// 导出交易信息 -func extractTransaction(message *Message) Transaction { - var tx = Transaction{} - // 导出交易金额和备注 - lines := strings.Split(message.Des, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if len(line) == 0 { - continue - } - // 解析收款金额 - prefix := "收款金额¥" - if strings.HasPrefix(line, prefix) { - if value, err := strconv.ParseFloat(line[len(prefix):], 64); err == nil { - tx.Amount = value - continue - } - } - // 解析收款备注 - prefix = "付款方备注" - if strings.HasPrefix(line, prefix) { - tx.Remark = line[len(prefix):] - break - } - } - - // 解析交易 ID - parse, err := url.Parse(message.Url) - if err == nil { - tx.TransId = parse.Query().Get("id") - if tx.TransId == "" { - tx.TransId = parse.Query().Get("trans_id") - } - } - - return tx -} diff --git a/api/store/model/redeem.go b/api/store/model/redeem.go new file mode 100644 index 00000000..6cb1c0d3 --- /dev/null +++ b/api/store/model/redeem.go @@ -0,0 +1,16 @@ +package model + +import "time" + +// 兑换码 + +type Redeem struct { + Id uint `gorm:"primarykey;column:id"` + UserId uint // 用户 ID + Name string // 名称 + Power int // 算力 + Code string // 兑换码 + Enabled bool // 启用状态 + RedeemedAt int64 // 兑换时间 + CreatedAt time.Time +} diff --git a/api/store/model/reward.go b/api/store/model/reward.go deleted file mode 100644 index 43b9c8ce..00000000 --- a/api/store/model/reward.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -// 用户打赏 - -type Reward struct { - BaseModel - UserId uint // 用户 ID - TxId string // 交易ID - Amount float64 // 打赏金额 - Remark string // 打赏备注 - Status bool // 核销状态 - Exchange string // 众筹兑换详情,JSON -} diff --git a/api/store/vo/redeem.go b/api/store/vo/redeem.go new file mode 100644 index 00000000..6fdc7961 --- /dev/null +++ b/api/store/vo/redeem.go @@ -0,0 +1,13 @@ +package vo + +type Redeem struct { + Id uint `json:"id"` + UserId uint `json:"user_id"` // 用户 ID + Name string `json:"name"` + Username string `json:"username"` + Power int `json:"power"` // 算力 + Code string `json:"code"` // 兑换码 + Enabled bool `json:"enabled"` + RedeemedAt int64 `json:"redeemed_at"` // 兑换时间 + CreatedAt int64 `json:"created_at"` +} diff --git a/api/store/vo/reward.go b/api/store/vo/reward.go deleted file mode 100644 index b3c5ac18..00000000 --- a/api/store/vo/reward.go +++ /dev/null @@ -1,16 +0,0 @@ -package vo - -type Reward struct { - BaseVo - UserId uint `json:"user_id"` // 用户 ID - Username string `json:"username"` - TxId string `json:"tx_id"` // 交易ID - Amount float64 `json:"amount"` // 打赏金额 - Remark string `json:"remark"` // 打赏备注 - Status bool `json:"status"` // 核销状态 - Exchange RewardExchange `json:"exchange"` -} - -type RewardExchange struct { - Power int `json:"power"` -} diff --git a/api/test/test.go b/api/test/test.go index 9fe7506e..99fd702a 100644 --- a/api/test/test.go +++ b/api/test/test.go @@ -1,16 +1,55 @@ package main import ( + "crypto/rand" + "encoding/hex" "fmt" - "geekai/utils" + "sync" ) -func main() { - file := "http://nk.img.r9it.com/chatgpt-plus/1719389335351828.xlsx" - content, err := utils.ReadFileContent(file, "http://172.22.11.69:9998") - if err != nil { - panic(err) - } +const ( + codeLength = 32 // 兑换码长度 +) - fmt.Println(content) +var ( + codeMap = make(map[string]bool) + mapMutex = &sync.Mutex{} +) + +// GenerateUniqueCode 生成唯一兑换码 +func GenerateUniqueCode() (string, error) { + for { + code, err := generateCode() + if err != nil { + return "", err + } + + mapMutex.Lock() + if !codeMap[code] { + codeMap[code] = true + mapMutex.Unlock() + return code, nil + } + mapMutex.Unlock() + } +} + +// generateCode 生成兑换码 +func generateCode() (string, error) { + bytes := make([]byte, codeLength/2) // 因为 hex 编码会使长度翻倍 + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +func main() { + for i := 0; i < 10; i++ { + code, err := GenerateUniqueCode() + if err != nil { + fmt.Println("Error generating code:", err) + return + } + fmt.Println("Generated code:", code) + } } diff --git a/api/utils/strings.go b/api/utils/strings.go index feb377de..12c8b107 100644 --- a/api/utils/strings.go +++ b/api/utils/strings.go @@ -8,14 +8,16 @@ package utils // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" - "math/rand" "strings" "time" "unicode" "golang.org/x/crypto/sha3" + rand2 "math/rand" ) // RandString generate rand string with specified length @@ -23,7 +25,7 @@ func RandString(length int) string { str := "0123456789abcdefghijklmnopqrstuvwxyz" data := []byte(str) var result []byte - r := rand.New(rand.NewSource(time.Now().UnixNano())) + r := rand2.New(rand2.NewSource(time.Now().UnixNano())) for i := 0; i < length; i++ { result = append(result, data[r.Intn(len(data))]) } @@ -34,8 +36,8 @@ func RandomNumber(bit int) int { minNum := intPow(10, bit-1) maxNum := intPow(10, bit) - 1 - rand.NewSource(time.Now().UnixNano()) - return rand.Intn(maxNum-minNum+1) + minNum + rand2.NewSource(time.Now().UnixNano()) + return rand2.Intn(maxNum-minNum+1) + minNum } func intPow(x, y int) int { @@ -124,3 +126,11 @@ func HasChinese(text string) bool { } return false } + +func GenRedeemCode(codeLength int) (string, error) { + bytes := make([]byte, codeLength/2) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} diff --git a/database/update-v4.1.2.sql b/database/update-v4.1.2.sql index 40acbd67..d3e2c2bd 100644 --- a/database/update-v4.1.2.sql +++ b/database/update-v4.1.2.sql @@ -1,2 +1,14 @@ ALTER TABLE `chatgpt_suno_jobs` MODIFY `id` INT AUTO_INCREMENT; -ALTER TABLE `chatgpt_mj_jobs` CHANGE `channel_id` `channel_id` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '频道ID'; \ No newline at end of file +ALTER TABLE `chatgpt_mj_jobs` CHANGE `channel_id` `channel_id` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '频道ID'; + +ALTER TABLE chatgpt_reward RENAME TO chatgpt_redeems; +ALTER TABLE chatgpt_redeems COMMENT '兑换码'; +ALTER TABLE `chatgpt_redeems` CHANGE `tx_id` `power` INT NOT NULL COMMENT '算力'; +ALTER TABLE `chatgpt_redeems` DROP `remark`; +ALTER TABLE `chatgpt_redeems` DROP `exchange`; +ALTER TABLE `chatgpt_redeems` CHANGE `updated_at` `redeemed_at` INT NOT NULL COMMENT '兑换时间'; +ALTER TABLE `chatgpt_redeems` CHANGE `amount` `code` VARCHAR(100) NOT NULL COMMENT '兑换码'; +ALTER TABLE `chatgpt_redeems` DROP INDEX `tx_id`; +ALTER TABLE `chatgpt_redeems` ADD UNIQUE(`code`); +ALTER TABLE `chatgpt_redeems` ADD `name` VARCHAR(30) NOT NULL COMMENT '兑换码名称' AFTER `user_id`; +ALTER TABLE `chatgpt_redeems` CHANGE `status` `enabled` TINYINT(1) NOT NULL COMMENT '是否启用'; \ No newline at end of file diff --git a/web/src/components/admin/AdminSidebar.vue b/web/src/components/admin/AdminSidebar.vue index 9238b58e..7034fc10 100644 --- a/web/src/components/admin/AdminSidebar.vue +++ b/web/src/components/admin/AdminSidebar.vue @@ -118,8 +118,8 @@ const items = [ }, { icon: 'reward', - index: '/admin/reward', - title: '众筹管理', + index: '/admin/redeem', + title: '兑换码', }, { icon: 'control', diff --git a/web/src/router.js b/web/src/router.js index 19a9a0cf..512dfd8c 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -186,10 +186,10 @@ const routes = [ component: () => import('@/views/admin/Order.vue'), }, { - path: '/admin/reward', - name: 'admin-reward', - meta: {title: '众筹管理'}, - component: () => import('@/views/admin/Reward.vue'), + path: '/admin/redeem', + name: 'admin-redeem', + meta: {title: '兑换码管理'}, + component: () => import('@/views/admin/Redeem.vue'), }, { path: '/admin/loginLog', diff --git a/web/src/views/admin/Redeem.vue b/web/src/views/admin/Redeem.vue new file mode 100644 index 00000000..17d89e4d --- /dev/null +++ b/web/src/views/admin/Redeem.vue @@ -0,0 +1,251 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/admin/Reward.vue b/web/src/views/admin/Reward.vue deleted file mode 100644 index f6cb4069..00000000 --- a/web/src/views/admin/Reward.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - - - \ No newline at end of file