urban 3 тижнів тому
батько
коміт
b60a390164

+ 15 - 0
magic_admin/cmd/test_sign.go

@@ -0,0 +1,15 @@
+package main
+
+import (
+	"fmt"
+	"go_server/utils"
+)
+
+func main() {
+	sign, err := utils.BuildSignMessage()
+	if err != nil {
+		fmt.Printf("Error: %v\n", err)
+		return
+	}
+	fmt.Println(sign)
+}

+ 12 - 6
magic_admin/service/app/tg_red_packet_send.go

@@ -14,6 +14,7 @@ import (
 	model "go_server/model/biz_modules/app"
 	"go_server/model/common/response"
 	"go_server/service/base"
+	"go_server/utils"
 )
 
 type TgRedPacketSendService struct {
@@ -64,7 +65,7 @@ func (s *TgRedPacketSendService) SendManual(c *gin.Context) {
 func (s *TgRedPacketSendService) SendDirect(c *gin.Context) {
 	s.SetDbAlias("app")
 	type request struct {
-		GroupId       string          `json:"groupId" binding:"required"`
+		GroupId       int64           `json:"groupId" binding:"required"`
 		GroupName     string          `json:"groupName"`
 		PacketType    int8            `json:"packetType" binding:"required"`
 		TotalAmount   decimal.Decimal `json:"totalAmount" binding:"required"`
@@ -106,7 +107,7 @@ func (s *TgRedPacketSendService) SendDirect(c *gin.Context) {
 
 	// 构建临时配置
 	config := &model.TgRedPacketConfig{
-		GroupId:       req.GroupId,
+		GroupId:       fmt.Sprintf("%d", req.GroupId),
 		GroupName:     req.GroupName,
 		PacketType:    req.PacketType,
 		TotalAmount:   req.TotalAmount,
@@ -131,10 +132,10 @@ func (s *TgRedPacketSendService) SendDirect(c *gin.Context) {
 
 // ExecuteSendRedPacket 执行发送红包的核心逻辑
 func (s *TgRedPacketSendService) ExecuteSendRedPacket(config *model.TgRedPacketConfig) (string, error) {
-	// 调用 magic_server API 发送红包
+	// 调用 magic_server API 创建红包并发送到 Telegram
 	packetNo, err := s.callMagicServerSendRedPacket(config)
 	if err != nil {
-		core.Log.Errorf("调用 magic_server 发送红包失败: %v", err)
+		core.Log.Errorf("创建并发送红包失败: %v", err)
 		return "", err
 	}
 
@@ -147,7 +148,7 @@ func (s *TgRedPacketSendService) ExecuteSendRedPacket(config *model.TgRedPacketC
 // callMagicServerSendRedPacket 调用 magic_server API 发送红包
 func (s *TgRedPacketSendService) callMagicServerSendRedPacket(config *model.TgRedPacketConfig) (string, error) {
 	// magic_server API 地址
-	apiURL := "http://localhost:2011/api/v1/redpacket/send"
+	apiURL := "http://localhost:2011/api/v1/adi/telegram/redpacket/send"
 
 	// 构造请求参数
 	payload := map[string]interface{}{
@@ -170,7 +171,9 @@ func (s *TgRedPacketSendService) callMagicServerSendRedPacket(config *model.TgRe
 		return "", fmt.Errorf("创建 HTTP 请求失败: %v", err)
 	}
 
+	signMessage, _ := utils.BuildSignMessage()
 	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("sign", signMessage)
 	// TODO: 添加认证 Token
 	// req.Header.Set("Authorization", "Bearer "+token)
 
@@ -196,8 +199,11 @@ func (s *TgRedPacketSendService) callMagicServerSendRedPacket(config *model.TgRe
 		return "", fmt.Errorf("读取响应失败: %v", err)
 	}
 
+	// 记录原始响应,用于调试
+	core.Log.Infof("magic_server 原始响应: %s", string(body))
+
 	if err := json.Unmarshal(body, &result); err != nil {
-		return "", fmt.Errorf("解析响应失败: %v", err)
+		return "", fmt.Errorf("解析响应失败: %v (响应内容: %s)", err, string(body))
 	}
 
 	if result.Code != 0 && result.Code != 200 {

+ 125 - 46
magic_admin_web/src/views/redpacket/send/index.vue

@@ -23,9 +23,9 @@
           >
             <el-option
               v-for="group in groupList"
-              :key="group.id"
-              :label="group.name"
-              :value="group.id"
+              :key="group.chatId"
+              :label="group.title"
+              :value="group.chatId"
             />
           </el-select>
           <div class="form-tip">如果没有群组,请先将机器人添加到Telegram群组中</div>
@@ -102,30 +102,44 @@
         </el-form-item>
       </el-form>
 
-      <!-- 预览信息 -->
+      <!-- Telegram 消息预览 -->
       <el-divider />
       <div class="preview-box">
-        <h3>📋 发送预览</h3>
-        <div class="preview-content">
-          <div class="preview-item">
-            <span class="label">群组:</span>
-            <span class="value">{{ formData.groupName || '未选择' }}</span>
+        <h3>📱 Telegram 消息预览</h3>
+        <div class="telegram-preview">
+          <div class="telegram-message">
+            <div class="message-header">
+              <span class="sender-name">🤖 系统管理员</span>
+            </div>
+            <div class="message-content">
+              <div class="red-packet-icon">🧧</div>
+              <div class="red-packet-text">发了一个红包</div>
+              <div class="blessing-words">💰 {{ formData.blessingWords || '恭喜发财,大吉大利!' }}</div>
+              <div class="divider"></div>
+              <div class="packet-info">
+                <div class="info-row">
+                  <span class="info-label">总金额:</span>
+                  <span class="info-value">{{ formData.totalAmount.toFixed(2) }} VND</span>
+                </div>
+                <div class="info-row">
+                  <span class="info-label">个数:</span>
+                  <span class="info-value">{{ formData.totalCount }} 个</span>
+                </div>
+                <div class="info-row">
+                  <span class="info-label">类型:</span>
+                  <span class="info-value">{{ formData.packetType === 1 ? '普通红包' : '手气红包' }}</span>
+                </div>
+              </div>
+              <div class="call-to-action">快来抢红包吧!</div>
+              <div class="grab-button">
+                <el-button type="danger" size="large" style="width: 100%">
+                  🎁 抢红包
+                </el-button>
+              </div>
+            </div>
           </div>
-          <div class="preview-item">
-            <span class="label">类型:</span>
-            <span class="value">{{ formData.packetType === 1 ? '普通红包' : '手气红包' }}</span>
-          </div>
-          <div class="preview-item">
-            <span class="label">总金额:</span>
-            <span class="value highlight">{{ formData.totalAmount }} VND</span>
-          </div>
-          <div class="preview-item">
-            <span class="label">个数:</span>
-            <span class="value">{{ formData.totalCount }} 个</span>
-          </div>
-          <div class="preview-item">
-            <span class="label">祝福语:</span>
-            <span class="value">{{ formData.blessingWords || '无' }}</span>
+          <div class="preview-tip">
+            💡 这是预览效果,实际发送到 Telegram 群组的消息样式可能略有不同
           </div>
         </div>
       </div>
@@ -325,38 +339,103 @@ onMounted(() => {
     h3 {
       margin-bottom: 15px;
       font-size: 16px;
+      color: #303133;
     }
 
-    .preview-content {
-      background: #f5f7fa;
-      padding: 15px;
-      border-radius: 4px;
+    .telegram-preview {
+      .telegram-message {
+        max-width: 400px;
+        background: #ffffff;
+        border: 1px solid #e4e7ed;
+        border-radius: 8px;
+        padding: 16px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+        .message-header {
+          margin-bottom: 12px;
+
+          .sender-name {
+            font-weight: 600;
+            color: #409eff;
+            font-size: 14px;
+          }
+        }
 
-      .preview-item {
-        margin-bottom: 10px;
-        display: flex;
+        .message-content {
+          .red-packet-icon {
+            font-size: 32px;
+            text-align: center;
+            margin-bottom: 8px;
+          }
 
-        &:last-child {
-          margin-bottom: 0;
-        }
+          .red-packet-text {
+            text-align: center;
+            font-size: 16px;
+            color: #303133;
+            margin-bottom: 12px;
+          }
 
-        .label {
-          width: 80px;
-          color: #606266;
-          flex-shrink: 0;
-        }
+          .blessing-words {
+            text-align: center;
+            font-size: 14px;
+            color: #606266;
+            margin-bottom: 16px;
+            font-style: italic;
+          }
 
-        .value {
-          flex: 1;
-          color: #303133;
+          .divider {
+            height: 1px;
+            background: #ebeef5;
+            margin: 16px 0;
+          }
 
-          &.highlight {
-            color: #f56c6c;
-            font-weight: bold;
-            font-size: 16px;
+          .packet-info {
+            margin-bottom: 16px;
+
+            .info-row {
+              display: flex;
+              justify-content: space-between;
+              margin-bottom: 8px;
+              font-size: 14px;
+
+              &:last-child {
+                margin-bottom: 0;
+              }
+
+              .info-label {
+                color: #909399;
+              }
+
+              .info-value {
+                color: #303133;
+                font-weight: 500;
+              }
+            }
+          }
+
+          .call-to-action {
+            text-align: center;
+            font-size: 14px;
+            color: #67c23a;
+            margin-bottom: 12px;
+            font-weight: 500;
+          }
+
+          .grab-button {
+            margin-top: 12px;
           }
         }
       }
+
+      .preview-tip {
+        margin-top: 12px;
+        padding: 8px 12px;
+        background: #f4f4f5;
+        border-radius: 4px;
+        font-size: 12px;
+        color: #909399;
+        text-align: center;
+      }
     }
   }
 }

+ 112 - 0
magic_server/apis/admin/redpacket.go

@@ -0,0 +1,112 @@
+package admin
+
+import (
+	"app/commons/services"
+	"app/commons/core"
+	"app/commons/model/entity"
+	"app/telegram"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/shopspring/decimal"
+)
+
+// SendRedPacketAdmin 管理端发送红包(不需要JWT,使用签名验证)
+func (s *Server) SendRedPacketAdmin(ctx *gin.Context) {
+	c := s.FromContext(ctx)
+
+	type request struct {
+		GroupID       string          `json:"groupId" binding:"required"`     // Telegram群组ID
+		TotalAmount   decimal.Decimal `json:"totalAmount" binding:"required"` // 红包总金额
+		TotalCount    int             `json:"totalCount" binding:"required"`  // 红包个数
+		PacketType    int             `json:"packetType" binding:"required"`  // 1=普通, 2=手气
+		Symbol        string          `json:"symbol"`                         // 币种,默认VND
+		BlessingWords string          `json:"blessingWords"`                  // 祝福语
+	}
+
+	req := new(request)
+	if err := c.ShouldBindBodyWithJSON(req); err != nil {
+		c.Fail(fmt.Sprintf("参数错误: %v", err))
+		return
+	}
+
+	// 参数验证
+	if req.PacketType != 1 && req.PacketType != 2 {
+		c.Fail("红包类型错误,必须是1(普通)或2(手气)")
+		return
+	}
+
+	if req.TotalCount <= 0 || req.TotalCount > 100 {
+		c.Fail("红包个数必须在1-100之间")
+		return
+	}
+
+	if req.TotalAmount.LessThanOrEqual(decimal.Zero) {
+		c.Fail("红包金额必须大于0")
+		return
+	}
+
+	// 默认币种
+	if req.Symbol == "" {
+		req.Symbol = "VND"
+	}
+
+	// 默认祝福语
+	if req.BlessingWords == "" {
+		if req.PacketType == 1 {
+			req.BlessingWords = "恭喜发财,大吉大利"
+		} else {
+			req.BlessingWords = "拼手气红包,快来抢!"
+		}
+	}
+
+	// 创建红包(使用管理员ID,这里简化处理用0)
+	redPacketService := &services.RedPacketService{}
+
+	packet, err := redPacketService.CreateRedPacket(
+		0, // 管理员发送,userId 为 0
+		req.GroupID,
+		req.PacketType,
+		req.TotalAmount,
+		req.TotalCount,
+		req.Symbol,
+		req.BlessingWords,
+	)
+
+	if err != nil {
+		core.Log.Errorf("创建红包失败: %v", err)
+		c.Fail(fmt.Sprintf("创建红包失败: %v", err))
+		return
+	}
+
+	// 发送到 Telegram 群组
+	go func() {
+		sendToTelegram(packet, "系统管理员")
+	}()
+
+	c.Resp(gin.H{
+		"packetId": packet.Id,
+		"packetNo": packet.PacketNo,
+		"message":  "红包发送成功",
+	})
+}
+
+// sendToTelegram 发送红包到Telegram群组
+func sendToTelegram(packet *entity.TgRedPacket, senderName string) {
+	if packet == nil {
+		return
+	}
+
+	// 调用 telegram 包的发送函数
+	telegram.SendRedPacketToGroup(
+		packet.GroupId,
+		packet.PacketNo,
+		senderName,
+		packet.TotalAmount.InexactFloat64(),
+		packet.TotalCount,
+		packet.PacketType,
+		packet.BlessingWords,
+	)
+
+	core.Log.Infof("红包已发送到Telegram群组 - PacketNo: %s, GroupID: %s",
+		packet.PacketNo, packet.GroupId)
+}

+ 5 - 2
magic_server/apis/admin/routers.go

@@ -44,8 +44,11 @@ func (h Router) Register(group *gin.RouterGroup) {
 	group.POST("node/banner/find", server().FindNodeBanner)     // 分页查询节点Banner
 
 	// Telegram 群组管理
-	group.GET("telegram/groups", server().GetTelegramGroups)       // 获取 Bot 加入的群组列表
+	group.GET("telegram/groups", server().GetTelegramGroups)        // 获取 Bot 加入的群组列表
 	group.POST("telegram/groups/sync", server().SyncTelegramGroups) // 同步群组信息
-	group.GET("telegram/chat/info", server().GetChatInfo)          // 获取群组详细信息
+	group.GET("telegram/chat/info", server().GetChatInfo)           // 获取群组详细信息
+
+	// Telegram 红包管理
+	group.POST("telegram/redpacket/send", server().SendRedPacketAdmin) // 管理端发送红包
 
 }

+ 32 - 18
magic_server/commons/services/redpacket_service.go

@@ -47,21 +47,27 @@ func (s *RedPacketService) CreateRedPacket(
 		return nil, errors.New("红包类型错误")
 	}
 
-	// 检查用户资产
-	userAsset, err := s.GetAssetBySymbol(userId, symbol)
-	if err != nil {
-		return nil, errors.New("获取资产失败")
-	}
-	if userAsset.Balance.LessThan(totalAmount) {
-		return nil, errors.New("余额不足")
+	// 管理员发红包(userId=0)跳过资产检查和扣款
+	isAdmin := userId == 0
+	if !isAdmin {
+		// 检查用户资产
+		userAsset, err := s.GetAssetBySymbol(userId, symbol)
+		if err != nil {
+			return nil, errors.New("获取资产失败")
+		}
+		if userAsset.Balance.LessThan(totalAmount) {
+			return nil, errors.New("余额不足")
+		}
 	}
 
 	// 开始事务
 	var packet *entity.TgRedPacket
-	err = s.DB().Transaction(func(tx *gorm.DB) error {
-		// 扣除用户余额(转为冻结)
-		if err := s.ActionAsset(tx, userId, symbol, totalAmount.Neg(), totalAmount); err != nil {
-			return fmt.Errorf("扣除余额失败: %w", err)
+	err := s.DB().Transaction(func(tx *gorm.DB) error {
+		// 扣除用户余额(转为冻结),管理员跳过
+		if !isAdmin {
+			if err := s.ActionAsset(tx, userId, symbol, totalAmount.Neg(), totalAmount); err != nil {
+				return fmt.Errorf("扣除余额失败: %w", err)
+			}
 		}
 
 		// 创建红包记录
@@ -132,11 +138,17 @@ func (s *RedPacketService) GrabRedPacket(
 		return nil, errors.New("红包已抢完")
 	}
 
-	// 检查是否已经抢过
+	// 检查是否已经抢过(优先用telegramId判断,兼容未绑定平台账号的用户)
 	var count int64
-	s.DB().Model(&entity.TgRedPacketRecord{}).
-		Where("packet_id = ? AND user_id = ?", packet.Id, userId).
-		Count(&count)
+	if telegramId > 0 {
+		s.DB().Model(&entity.TgRedPacketRecord{}).
+			Where("packet_id = ? AND telegram_id = ?", packet.Id, telegramId).
+			Count(&count)
+	} else {
+		s.DB().Model(&entity.TgRedPacketRecord{}).
+			Where("packet_id = ? AND user_id = ?", packet.Id, userId).
+			Count(&count)
+	}
 	if count > 0 {
 		return nil, errors.New("您已经抢过这个红包了")
 	}
@@ -175,9 +187,11 @@ func (s *RedPacketService) GrabRedPacket(
 			return fmt.Errorf("创建记录失败: %w", err)
 		}
 
-		// 增加用户余额(从冻结转为可用)
-		if err := s.ActionAsset(tx, userId, packet.Symbol, amount, amount.Neg()); err != nil {
-			return fmt.Errorf("增加余额失败: %w", err)
+		// 增加用户余额(从冻结转为可用),userId=0 时跳过(Telegram用户未绑定平台账号)
+		if userId > 0 {
+			if err := s.ActionAsset(tx, userId, packet.Symbol, amount, amount.Neg()); err != nil {
+				return fmt.Errorf("增加余额失败: %w", err)
+			}
 		}
 
 		// 如果抢完了,更新红包状态

+ 38 - 0
magic_server/telegram/messages.go

@@ -167,3 +167,41 @@ func SendRedPacketToGroup(groupID string, packetNo string, senderName string, to
 
 	sendMessageWithKeyboard(chatID, text, keyboard)
 }
+
+// PreviewRedPacketToGroup 发送红包预览消息到群组(供外部调用)
+func PreviewRedPacketToGroup(groupID string, senderName string, totalAmount float64, totalCount int, packetType int, blessingWords string) {
+	if !IsEnabled() {
+		core.Log.Warn("Telegram Bot 未启用,无法发送预览消息")
+		return
+	}
+
+	var packetTypeText string
+	if packetType == 1 {
+		packetTypeText = "普通红包"
+	} else {
+		packetTypeText = "手气红包"
+	}
+
+	text := fmt.Sprintf(`🔍 【预览模式】红包预览
+
+🧧 %s 发了一个红包
+💰 %s
+
+总金额: %.2f VND
+个数: %d 个
+类型: %s
+
+⚠️ 这是预览消息,不是真实红包
+实际发送时才能抢红包`,
+		senderName,
+		blessingWords,
+		totalAmount,
+		totalCount,
+		packetTypeText)
+
+	// 解析群组 ID(字符串转 int64)
+	var chatID int64
+	fmt.Sscanf(groupID, "%d", &chatID)
+
+	sendTextMessage(chatID, text)
+}

+ 23 - 65
magic_server/telegram/redpacket.go

@@ -2,11 +2,9 @@ package telegram
 
 import (
 	"app/commons/core"
-	"encoding/json"
+	"app/commons/services"
 	"fmt"
 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
-	"io"
-	"net/http"
 	"strings"
 )
 
@@ -27,75 +25,35 @@ func handleCallbackQuery(callback *tgbotapi.CallbackQuery) {
 
 // grabRedPacket 抢红包
 func grabRedPacket(callback *tgbotapi.CallbackQuery, packetNo string) {
-	// TODO: 检查用户是否已绑定
-	// TODO: 调用 API 抢红包
-
-	// 临时实现
-	userID := callback.From.ID
+	telegramID := callback.From.ID
 	username := callback.From.UserName
 
-	// 模拟调用 API
-	success, amount, message := callGrabRedPacketAPI(packetNo, userID, username)
-
-	if success {
-		// 抢到红包
-		answerCallback(callback.ID, fmt.Sprintf("🎉 恭喜抢到 %.2f VND!", amount))
+	// 直接调用 service 层抢红包
+	redPacketService := &services.RedPacketService{}
 
-		// 更新原消息
-		editText := fmt.Sprintf("%s\n\n✅ @%s 抢到了 %.2f VND",
-			callback.Message.Text, username, amount)
-		editMessage(callback.Message.Chat.ID, callback.Message.MessageID, editText)
-	} else {
-		// 抢红包失败
-		answerCallback(callback.ID, message)
-	}
-
-	core.Log.Infof("抢红包 - 用户: %s, 红包: %s, 结果: %s",
-		username, packetNo, message)
-}
-
-// callGrabRedPacketAPI 调用抢红包 API(模拟)
-func callGrabRedPacketAPI(packetNo string, telegramID int64, username string) (bool, float64, string) {
-	// TODO: 实现真实的 API 调用
-	// 这里是模拟实现
-
-	apiURL := "http://localhost:2001/api/v1/redpacket/grab"
-
-	payload := map[string]interface{}{
-		"packetNo":         packetNo,
-		"telegramId":       telegramID,
-		"telegramUsername": username,
-	}
+	record, err := redPacketService.GrabRedPacket(
+		packetNo,
+		0, // Telegram用户暂无平台userId
+		telegramID,
+		username,
+	)
 
-	jsonData, _ := json.Marshal(payload)
-
-	req, err := http.NewRequest("POST", apiURL, strings.NewReader(string(jsonData)))
-	if err != nil {
-		return false, 0, "系统错误"
-	}
-
-	req.Header.Set("Content-Type", "application/json")
-	// TODO: 添加 JWT Token
-	// req.Header.Set("Authorization", "Bearer "+token)
-
-	client := &http.Client{}
-	resp, err := client.Do(req)
 	if err != nil {
-		return false, 0, "网络错误"
+		answerCallback(callback.ID, err.Error())
+		core.Log.Infof("抢红包 - 用户: %s, 红包: %s, 结果: %s",
+			username, packetNo, err.Error())
+		return
 	}
-	defer resp.Body.Close()
 
-	body, _ := io.ReadAll(resp.Body)
+	// 抢到红包
+	amount := record.Amount.InexactFloat64()
+	answerCallback(callback.ID, fmt.Sprintf("🎉 恭喜抢到 %.2f VND!", amount))
 
-	var result map[string]interface{}
-	json.Unmarshal(body, &result)
-
-	if resp.StatusCode == 200 {
-		data := result["data"].(map[string]interface{})
-		amount := data["amount"].(float64)
-		return true, amount, "抢红包成功"
-	}
+	// 更新原消息
+	editText := fmt.Sprintf("%s\n\n✅ @%s 抢到了 %.2f VND",
+		callback.Message.Text, username, amount)
+	editMessage(callback.Message.Chat.ID, callback.Message.MessageID, editText)
 
-	message := result["message"].(string)
-	return false, 0, message
+	core.Log.Infof("抢红包 - 用户: %s, 红包: %s, 金额: %s, 结果: 成功",
+		username, packetNo, record.Amount.String())
 }