Prechádzať zdrojové kódy

fix: 修复抢红包状态提前完成bug,优化群消息实时更新

- 修复 GORM Updates 回写导致 RemainCount/GrabbedCount 判断使用更新后的值,
  造成红包只抢1个就标记已抢完的bug(保存原始值再判断)
- 抢红包成功后自动创建用户资产记录(解决 ActionAsset UPDATE 找不到行的问题)
- 重构群消息更新逻辑:从数据库查询完整红包+领取记录构建消息,实时展示所有人的领取明细
- 红包未抢完时保留按钮并显示进度(n/m),抢完后移除按钮
- editMessage 去掉 Markdown 模式,避免用户名下划线解析出错
- 处理 Telegram 群组升级为超级群时自动重发消息
- 修复发送页面群组列和时间戳显示问题
urban 3 týždňov pred
rodič
commit
3b6d1bf4d9

+ 2 - 0
magic_admin_web/src/views/redpacket/group/index.vue

@@ -228,6 +228,7 @@ const formRules = {
 // 获取状态文本
 const getStatusText = status => {
   const statusMap = {
+    0: '已失效',
     1: '正常',
     2: '已禁用',
     3: '已退出'
@@ -238,6 +239,7 @@ const getStatusText = status => {
 // 获取状态类型
 const getStatusType = status => {
   const typeMap = {
+    0: 'danger',
     1: 'success',
     2: 'warning',
     3: 'info'

+ 12 - 2
magic_admin_web/src/views/redpacket/send/index.vue

@@ -155,7 +155,7 @@
 
       <el-table :data="recentRecords" border stripe>
         <el-table-column prop="packetNo" label="红包编号" width="180" />
-        <el-table-column prop="groupName" label="群组" />
+        <el-table-column prop="groupId" label="群组" />
         <el-table-column prop="totalAmount" label="金额" />
         <el-table-column prop="totalCount" label="个数" />
         <el-table-column prop="grabbedCount" label="已抢" />
@@ -166,7 +166,11 @@
             <el-tag v-else type="warning">已过期</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="createdAt" label="发送时间" />
+        <el-table-column prop="createdAt" label="发送时间">
+          <template #default="{ row }">
+            {{ formatTimestamp(row.createdAt) }}
+          </template>
+        </el-table-column>
       </el-table>
     </el-card>
   </div>
@@ -291,6 +295,12 @@ const handleReset = () => {
   });
 };
 
+// 格式化时间戳
+const formatTimestamp = ts => {
+  if (!ts) return '-';
+  return new Date(ts * 1000).toLocaleString('zh-CN');
+};
+
 // 页面加载
 onMounted(() => {
   loadGroups();

+ 25 - 7
magic_server/commons/services/redpacket_service.go

@@ -159,11 +159,15 @@ func (s *RedPacketService) GrabRedPacket(
 		// 计算抢到的金额
 		amount := s.CalculateGrabAmount(&packet)
 
+		// 先保存原始值(GORM Updates 会回写到 packet 对象,导致后续判断用到更新后的值)
+		origGrabbedCount := packet.GrabbedCount
+		origRemainCount := packet.RemainCount
+
 		// 更新红包信息
 		updates := map[string]interface{}{
-			"grabbed_count":  packet.GrabbedCount + 1,
+			"grabbed_count":  origGrabbedCount + 1,
 			"grabbed_amount": packet.GrabbedAmount.Add(amount),
-			"remain_count":   packet.RemainCount - 1,
+			"remain_count":   origRemainCount - 1,
 			"remain_amount":  packet.RemainAmount.Sub(amount),
 		}
 
@@ -179,7 +183,7 @@ func (s *RedPacketService) GrabRedPacket(
 			TelegramId:       telegramId,
 			TelegramUsername: telegramUsername,
 			Amount:           amount,
-			Sequence:         packet.GrabbedCount + 1,
+			Sequence:         int(origGrabbedCount) + 1,
 			GrabbedAt:        time.Now(),
 		}
 
@@ -187,15 +191,29 @@ func (s *RedPacketService) GrabRedPacket(
 			return fmt.Errorf("创建记录失败: %w", err)
 		}
 
-		// 增加用户余额(从冻结转为可用),userId=0 时跳过(Telegram用户未绑定平台账号)
+		// 增加用户余额,userId=0 时跳过(Telegram用户未绑定平台账号)
 		if userId > 0 {
-			if err := s.ActionAsset(tx, userId, packet.Symbol, amount, amount.Neg()); err != nil {
+			// 确保用户有资产记录(不存在则创建)
+			var assetCount int64
+			tx.Model(&entity.Asset{}).Where("user_id = ? AND symbol = ?", userId, packet.Symbol).Count(&assetCount)
+			if assetCount == 0 {
+				newAsset := &entity.Asset{
+					UserId:  userId,
+					Symbol:  packet.Symbol,
+					Balance: decimal.Zero,
+					Frozen:  decimal.Zero,
+				}
+				if err := tx.Create(newAsset).Error; err != nil {
+					return fmt.Errorf("创建资产记录失败: %w", err)
+				}
+			}
+			if err := s.ActionAsset(tx, userId, packet.Symbol, amount, decimal.Zero); err != nil {
 				return fmt.Errorf("增加余额失败: %w", err)
 			}
 		}
 
-		// 如果抢完了,更新红包状态
-		if packet.RemainCount == 1 {
+		// 如果抢完了,更新红包状态(使用原始值判断,因为 GORM Updates 已回写 packet)
+		if origRemainCount == 1 {
 			now := time.Now()
 			if err := tx.Model(&packet).Updates(map[string]interface{}{
 				"status":       2, // 已抢完

+ 43 - 2
magic_server/telegram/messages.go

@@ -2,6 +2,7 @@ package telegram
 
 import (
 	"app/commons/core"
+	"app/commons/services"
 	"fmt"
 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 )
@@ -48,19 +49,46 @@ func sendMessageWithKeyboard(chatID int64, text string, keyboard tgbotapi.Inline
 
 	_, err := bot.Send(msg)
 	if err != nil {
+		// 处理群组升级为超级群的情况
+		if tgErr, ok := err.(*tgbotapi.Error); ok && tgErr.Code == 400 && tgErr.ResponseParameters.MigrateToChatID != 0 {
+			newChatID := tgErr.ResponseParameters.MigrateToChatID
+			core.Log.Infof("群组已升级,自动重试: %d -> %d", chatID, newChatID)
+			// 异步更新数据库中的群组ID
+			go updateGroupChatID(chatID, newChatID)
+			// 用新 ID 重发
+			msg.ChatID = newChatID
+			_, err = bot.Send(msg)
+			if err != nil {
+				core.Log.Errorf("用新群组ID重发消息失败: %v", err)
+			}
+			return err
+		}
 		core.Log.Errorf("发送消息失败: %v", err)
 	}
 	return err
 }
 
-// editMessage 编辑消息
+// editMessage 编辑消息(纯文本,避免用户名中 _ 等字符被 Markdown 解析出错)
 func editMessage(chatID int64, messageID int, text string) {
 	if !IsEnabled() {
 		return
 	}
 
 	edit := tgbotapi.NewEditMessageText(chatID, messageID, text)
-	edit.ParseMode = "Markdown"
+
+	_, err := bot.Send(edit)
+	if err != nil {
+		core.Log.Errorf("编辑消息失败: %v", err)
+	}
+}
+
+// editMessageWithKeyboard 编辑消息并保留按钮(纯文本)
+func editMessageWithKeyboard(chatID int64, messageID int, text string, keyboard tgbotapi.InlineKeyboardMarkup) {
+	if !IsEnabled() {
+		return
+	}
+
+	edit := tgbotapi.NewEditMessageTextAndMarkup(chatID, messageID, text, keyboard)
 
 	_, err := bot.Send(edit)
 	if err != nil {
@@ -206,3 +234,16 @@ func PreviewRedPacketToGroup(groupID string, senderName string, totalAmount floa
 
 	sendTextMessage(chatID, text)
 }
+
+// updateGroupChatID 群组升级后更新数据库中的 chat_id
+func updateGroupChatID(oldChatID, newChatID int64) {
+	db := services.NewComService().DB()
+	if db == nil {
+		return
+	}
+	// 将旧群组记录标记为无效
+	db.Table("magic_tg_group").
+		Where("chat_id = ?", fmt.Sprintf("%d", oldChatID)).
+		Updates(map[string]interface{}{"status": 0})
+	core.Log.Infof("群组升级: 旧ID %d 已标记无效, 新ID %d", oldChatID, newChatID)
+}

+ 46 - 4
magic_server/telegram/redpacket.go

@@ -60,10 +60,52 @@ func grabRedPacket(callback *tgbotapi.CallbackQuery, packetNo string) {
 	amount := record.Amount.InexactFloat64()
 	answerCallback(callback.ID, fmt.Sprintf("🎉 恭喜抢到 %.2f VND!", amount))
 
-	// 更新原消息
-	editText := fmt.Sprintf("%s\n\n✅ @%s 抢到了 %.2f VND",
-		callback.Message.Text, username, amount)
-	editMessage(callback.Message.Chat.ID, callback.Message.MessageID, editText)
+	// 从数据库查询完整的红包信息和所有领取记录,构建实时消息
+	packet, records, _ := redPacketService.GetRedPacketDetail(packetNo)
+	if packet == nil {
+		return
+	}
+
+	// 构建红包头部信息
+	var packetTypeText string
+	if packet.PacketType == 1 {
+		packetTypeText = "普通红包"
+	} else {
+		packetTypeText = "手气红包"
+	}
+	editText := fmt.Sprintf("🧧 红包\n💰 %s\n\n总金额: %.2f VND | 个数: %d | %s",
+		packet.BlessingWords,
+		packet.TotalAmount.InexactFloat64(),
+		packet.TotalCount,
+		packetTypeText)
+
+	// 追加每个人的领取记录
+	editText += fmt.Sprintf("\n\n📋 领取明细 (%d/%d):", packet.GrabbedCount, packet.TotalCount)
+	for _, r := range records {
+		displayName := r.TelegramUsername
+		if displayName != "" {
+			displayName = "@" + displayName
+		} else {
+			displayName = fmt.Sprintf("用户%d", r.TelegramId)
+		}
+		editText += fmt.Sprintf("\n✅ %s — %.2f VND", displayName, r.Amount.InexactFloat64())
+	}
+
+	// 根据剩余情况决定是否保留按钮
+	if packet.RemainCount > 0 {
+		keyboard := tgbotapi.NewInlineKeyboardMarkup(
+			tgbotapi.NewInlineKeyboardRow(
+				tgbotapi.NewInlineKeyboardButtonData(
+					fmt.Sprintf("🎁 抢红包 (%d/%d)", packet.GrabbedCount, packet.TotalCount),
+					fmt.Sprintf("grab_%s", packetNo),
+				),
+			),
+		)
+		editMessageWithKeyboard(callback.Message.Chat.ID, callback.Message.MessageID, editText, keyboard)
+	} else {
+		editText += "\n\n🎊 红包已抢完!"
+		editMessage(callback.Message.Chat.ID, callback.Message.MessageID, editText)
+	}
 
 	core.Log.Infof("抢红包 - 用户: %s(userId:%d), 红包: %s, 金额: %s, 结果: 成功",
 		username, userId, packetNo, record.Amount.String())