Forráskód Böngészése

feat: 新增用户绑定管理页面,支持 username 兜底匹配及 /bind 私聊失败提示

urban 3 hete
szülő
commit
1703c1a500

+ 18 - 12
magic_admin/model/biz_modules/app/tg_red_packet_record.go

@@ -1,18 +1,24 @@
 package app
 
-// TgRedPacketRecord 红包领取记录
+import (
+	"github.com/shopspring/decimal"
+	"time"
+)
+
+// TgRedPacketRecord 抢红包记录
 type TgRedPacketRecord struct {
-	Id          int64  `json:"id" gorm:"column:id;type:bigint;comment:id;primarykey;NOT NULL"`
-	CreatedAt   int64  `json:"createdAt" gorm:"column:created_at;type:bigint;comment:创建时间"`
-	UpdatedAt   int64  `json:"updatedAt" gorm:"column:updated_at;type:bigint;comment:更新时间"`
-	PacketNo    string `json:"packetNo" gorm:"column:packet_no;type:varchar(50);index;comment:红包编号"`
-	ChatId      int64  `json:"chatId" gorm:"column:chat_id;index;comment:群组ID"`
-	UserId      int64  `json:"userId" gorm:"column:user_id;index;comment:用户ID"`
-	Username    string `json:"username" gorm:"column:username;type:varchar(100);comment:用户名"`
-	Amount      int64  `json:"amount" gorm:"column:amount;comment:领取金额(分)"`
-	Status      int    `json:"status" gorm:"column:status;default:0;comment:状态 0-待领取 1-已领取 2-已过期"`
-	ReceivedAt  int64  `json:"receivedAt" gorm:"column:received_at;comment:领取时间戳"`
-	MessageId   int64  `json:"messageId" gorm:"column:message_id;comment:消息ID"`
+	Id               int64           `json:"id" gorm:"column:id;type:bigint;comment:id;primarykey;NOT NULL"`
+	CreatedAt        int64           `json:"createdAt" gorm:"column:created_at;type:bigint;comment:创建时间"`
+	UpdatedAt        int64           `json:"updatedAt" gorm:"column:updated_at;type:bigint;comment:更新时间"`
+	PacketId         int64           `json:"packetId" gorm:"column:packet_id;type:bigint;index;comment:红包ID"`
+	PacketNo         string          `json:"packetNo" gorm:"column:packet_no;type:varchar(64);index;comment:红包编号"`
+	UserId           int64           `json:"userId" gorm:"column:user_id;type:bigint;index;comment:用户ID"`
+	TelegramId       int64           `json:"telegramId" gorm:"column:telegram_id;type:bigint;index;comment:Telegram用户ID"`
+	TelegramUsername string          `json:"telegramUsername" gorm:"column:telegram_username;type:varchar(255);comment:Telegram用户名"`
+	Amount           decimal.Decimal `json:"amount" gorm:"column:amount;type:decimal(25,8);comment:抢到金额"`
+	IsBest           int             `json:"isBest" gorm:"column:is_best;type:tinyint;default:0;comment:是否手气最佳"`
+	Sequence         int64           `json:"sequence" gorm:"column:sequence;type:bigint;comment:抢红包序号"`
+	GrabbedAt        time.Time       `json:"grabbedAt" gorm:"column:grabbed_at;type:timestamp;comment:抢红包时间"`
 }
 
 func (*TgRedPacketRecord) TableName() string {

+ 28 - 0
magic_admin/model/biz_modules/app/tg_user_bind.go

@@ -0,0 +1,28 @@
+package app
+
+import "time"
+
+// TgUserBind Telegram用户绑定信息
+type TgUserBind struct {
+	Id                int64     `json:"id" gorm:"column:id;type:bigint;comment:id;primarykey;NOT NULL"`
+	CreatedAt         int64     `json:"createdAt" gorm:"column:created_at;type:bigint;comment:创建时间"`
+	UpdatedAt         int64     `json:"updatedAt" gorm:"column:updated_at;type:bigint;comment:更新时间"`
+	UserId            int64     `json:"userId" gorm:"column:user_id;type:bigint;index;comment:平台用户ID;NOT NULL"`
+	TelegramId        int64     `json:"telegramId" gorm:"column:telegram_id;type:bigint;uniqueIndex;comment:Telegram用户ID;NOT NULL"`
+	TelegramUsername  string    `json:"telegramUsername" gorm:"column:telegram_username;type:varchar(255);comment:Telegram用户名"`
+	TelegramFirstName string    `json:"telegramFirstName" gorm:"column:telegram_first_name;type:varchar(255);comment:Telegram名字"`
+	BindStatus        int       `json:"bindStatus" gorm:"column:bind_status;type:tinyint;default:1;comment:绑定状态: 1=已绑定, 0=已解绑"`
+	BindTime          time.Time `json:"bindTime" gorm:"column:bind_time;type:timestamp;default:CURRENT_TIMESTAMP;comment:绑定时间"`
+}
+
+func (*TgUserBind) TableName() string {
+	return "magic_tg_user_bind"
+}
+
+func (*TgUserBind) Comment() string {
+	return "Telegram用户绑定表"
+}
+
+func NewTgUserBind() *TgUserBind {
+	return &TgUserBind{}
+}

+ 1 - 1
magic_admin/router/app/enter.go

@@ -8,7 +8,7 @@ import (
 // All routes to be registered
 
 var (
-	allRouters = []global.ContextInterface{MagicAssetRouter{}, MagicAssetBillRouter{}, MagicAssetRwCallbackLogRouter{}, MagicAssetRwRecordRouter{}, MagicStakeProductRouter{}, MagicStakeQueueInfoRouter{}, MagicStakeUserCurrentOpsRecordRouter{}, MagicStakeUserCurrentOrderRouter{}, MagicUserRouter{}, MagicUserActionLogRouter{}, MagicUserLogsRouter{}, MagicUserProfitRouter{}, MagicUserProfitRecordRouter{}, MagicUserQuotaRouter{}, NodeBannerRouter{}, NodeInfoRouter{}, NodeOrderRouter{}, NodeOrderPaymentsRouter{}, PromotionUpgradeLevelRouter{}, SysCoinRouter{}, SysI18nRouter{}, SysJobRouter{}, SysLevelConfigRouter{}, SysSignConfigRouter{}, SysStakePeriodJobRouter{}, MagicExtraStakeOrderOpsRecordRouter{}, MagicExtraStakeOrderRouter{}, TgRedPacketConfigRouter{}, TgRedPacketSendRouter{}, TgRedPacketRecordRouter{}, TgGroupRouter{}}
+	allRouters = []global.ContextInterface{MagicAssetRouter{}, MagicAssetBillRouter{}, MagicAssetRwCallbackLogRouter{}, MagicAssetRwRecordRouter{}, MagicStakeProductRouter{}, MagicStakeQueueInfoRouter{}, MagicStakeUserCurrentOpsRecordRouter{}, MagicStakeUserCurrentOrderRouter{}, MagicUserRouter{}, MagicUserActionLogRouter{}, MagicUserLogsRouter{}, MagicUserProfitRouter{}, MagicUserProfitRecordRouter{}, MagicUserQuotaRouter{}, NodeBannerRouter{}, NodeInfoRouter{}, NodeOrderRouter{}, NodeOrderPaymentsRouter{}, PromotionUpgradeLevelRouter{}, SysCoinRouter{}, SysI18nRouter{}, SysJobRouter{}, SysLevelConfigRouter{}, SysSignConfigRouter{}, SysStakePeriodJobRouter{}, MagicExtraStakeOrderOpsRecordRouter{}, MagicExtraStakeOrderRouter{}, TgRedPacketConfigRouter{}, TgRedPacketSendRouter{}, TgRedPacketRecordRouter{}, TgGroupRouter{}, TgUserBindRouter{}}
 )
 
 type RouterGroup struct {

+ 22 - 0
magic_admin/router/app/tg_user_bind.go

@@ -0,0 +1,22 @@
+package app
+
+import (
+	"github.com/gin-gonic/gin"
+	"go_server/service"
+)
+
+type TgUserBindRouter struct{}
+
+func (TgUserBindRouter) Route() string {
+	return "/tg_user_bind"
+}
+
+var tgUserBindService = service.RealizationLayer.AppServiceGroup.TgUserBindService
+
+func (h TgUserBindRouter) Register(group *gin.RouterGroup) {
+	group.GET("find", tgUserBindService.Find)
+	group.GET("get", tgUserBindService.Get)
+	group.POST("create", tgUserBindService.Create)
+	group.POST("update", tgUserBindService.Update)
+	group.GET("delete", tgUserBindService.Delete)
+}

+ 1 - 0
magic_admin/service/app/enter.go

@@ -32,4 +32,5 @@ type ServiceGroup struct {
 	TgRedPacketSendService
 	TgRedPacketRecordService
 	TgGroupService
+	TgUserBindService
 }

+ 214 - 0
magic_admin/service/app/tg_user_bind.go

@@ -0,0 +1,214 @@
+package app
+
+import (
+	"time"
+
+	"github.com/demdxx/gocast"
+	"github.com/gin-gonic/gin"
+	"go_server/model/biz_modules/app"
+	"go_server/model/common/response"
+	"go_server/service/base"
+)
+
+type TgUserBindService struct {
+	base.BizCommonService
+}
+
+// Find 查询用户绑定列表
+func (s *TgUserBindService) Find(c *gin.Context) {
+	s.SetDbAlias("app")
+	type request[T any] struct {
+		base.ListRequest[T]
+		UserId           *int64  `form:"user_id" json:"user_id"`
+		TelegramId       *int64  `form:"telegram_id" json:"telegram_id"`
+		TelegramUsername string  `form:"telegram_username" json:"telegram_username"`
+		BindStatus       *int    `form:"bind_status" json:"bind_status"`
+	}
+	req := new(request[app.TgUserBind])
+	if err := c.BindQuery(req); err != nil {
+		response.Resp(c, err.Error())
+		return
+	}
+
+	db := s.DB()
+
+	// 条件筛选
+	if req.UserId != nil && *req.UserId != 0 {
+		db = db.Where("user_id = ?", *req.UserId)
+	}
+	if req.TelegramId != nil && *req.TelegramId != 0 {
+		db = db.Where("telegram_id = ?", *req.TelegramId)
+	}
+	if req.TelegramUsername != "" {
+		db = db.Where("telegram_username LIKE ?", "%"+req.TelegramUsername+"%")
+	}
+	if req.BindStatus != nil {
+		db = db.Where("bind_status = ?", *req.BindStatus)
+	}
+
+	// 按创建时间倒序
+	db = db.Order("created_at DESC")
+
+	resp, err := base.NewQueryBaseHandler(app.NewTgUserBind()).List(db, req)
+	if err != nil {
+		response.Resp(c, err.Error())
+		return
+	}
+	response.Resp(c, resp)
+}
+
+// Get 获取用户绑定详情
+func (s *TgUserBindService) Get(c *gin.Context) {
+	s.SetDbAlias("app")
+	base.NewBaseHandler(app.NewTgUserBind()).Get(c, s.DB())
+}
+
+// Create 创建用户绑定
+func (s *TgUserBindService) Create(c *gin.Context) {
+	s.SetDbAlias("app")
+	type request struct {
+		UserId            interface{} `json:"userId"`
+		TelegramId        interface{} `json:"telegramId"`
+		TelegramUsername  string      `json:"telegramUsername"`
+		TelegramFirstName string      `json:"telegramFirstName"`
+	}
+	req := new(request)
+	if err := c.BindJSON(req); err != nil {
+		response.Resp(c, err.Error())
+		return
+	}
+
+	userId := gocast.ToInt64(req.UserId)
+	telegramId := gocast.ToInt64(req.TelegramId)
+
+	// 验证必填字段
+	if userId == 0 {
+		response.Resp(c, "平台用户ID不能为空")
+		return
+	}
+	if req.TelegramUsername == "" {
+		response.Resp(c, "Telegram用户名不能为空")
+		return
+	}
+
+	// 检查是否已绑定
+	var count int64
+	s.DB().Model(&app.TgUserBind{}).Where("telegram_username = ? AND bind_status = 1", req.TelegramUsername).Count(&count)
+	if count > 0 {
+		response.Resp(c, "该Telegram用户名已被绑定")
+		return
+	}
+
+	// 检查 UserId 是否已绑定
+	s.DB().Model(&app.TgUserBind{}).Where("user_id = ? AND bind_status = 1", userId).Count(&count)
+	if count > 0 {
+		response.Resp(c, "该平台用户已绑定其他Telegram账号")
+		return
+	}
+
+	now := time.Now()
+	bind := &app.TgUserBind{
+		UserId:            userId,
+		TelegramId:        telegramId,
+		TelegramUsername:  req.TelegramUsername,
+		TelegramFirstName: req.TelegramFirstName,
+		BindStatus:        1,
+		BindTime:          now,
+		CreatedAt:         now.Unix(),
+		UpdatedAt:         now.Unix(),
+	}
+
+	if err := s.DB().Create(bind).Error; err != nil {
+		response.Resp(c, err.Error())
+		return
+	}
+
+	response.Resp(c, bind)
+}
+
+// Update 更新用户绑定
+func (s *TgUserBindService) Update(c *gin.Context) {
+	s.SetDbAlias("app")
+	type request struct {
+		Id                int64       `json:"id" binding:"required"`
+		UserId            interface{} `json:"userId"`
+		TelegramId        interface{} `json:"telegramId"`
+		TelegramUsername  string      `json:"telegramUsername"`
+		TelegramFirstName string      `json:"telegramFirstName"`
+		BindStatus        *int        `json:"bindStatus"`
+	}
+	req := new(request)
+	if err := c.BindJSON(req); err != nil {
+		response.Resp(c, err.Error())
+		return
+	}
+
+	bind, ok := base.GetOne[app.TgUserBind](s.DB(), "id", req.Id)
+	if !ok {
+		response.Resp(c, "绑定记录不存在")
+		return
+	}
+
+	userId := gocast.ToInt64(req.UserId)
+	telegramId := gocast.ToInt64(req.TelegramId)
+
+	// 更新字段
+	updates := make(map[string]interface{})
+	if userId != 0 {
+		updates["user_id"] = userId
+	}
+	if telegramId != 0 {
+		updates["telegram_id"] = telegramId
+	}
+	if req.TelegramUsername != "" {
+		updates["telegram_username"] = req.TelegramUsername
+	}
+	if req.TelegramFirstName != "" {
+		updates["telegram_first_name"] = req.TelegramFirstName
+	}
+	if req.BindStatus != nil {
+		updates["bind_status"] = *req.BindStatus
+	}
+	updates["updated_at"] = time.Now().Unix()
+
+	if err := s.DB().Model(&bind).Updates(updates).Error; err != nil {
+		response.Resp(c, err.Error())
+		return
+	}
+
+	response.Resp(c)
+}
+
+// Delete 删除用户绑定(软删除:修改状态为已解绑)
+func (s *TgUserBindService) Delete(c *gin.Context) {
+	s.SetDbAlias("app")
+	id, ok := c.GetQuery("id")
+	if !ok {
+		response.Resp(c, "未填写ID")
+		return
+	}
+
+	bindId := gocast.ToInt64(id)
+	if bindId == 0 {
+		response.Resp(c, "ID无效")
+		return
+	}
+
+	bind, ok := base.GetOne[app.TgUserBind](s.DB(), "id", bindId)
+	if !ok {
+		response.Resp(c, "绑定记录不存在")
+		return
+	}
+
+	// 软删除:状态改为0(已解绑)
+	updates := map[string]interface{}{
+		"bind_status": 0,
+		"updated_at":  time.Now().Unix(),
+	}
+	if err := s.DB().Model(&bind).Updates(updates).Error; err != nil {
+		response.Resp(c, err.Error())
+		return
+	}
+
+	response.Resp(c)
+}

+ 42 - 0
magic_admin_web/src/api/modules/redpacket.js

@@ -151,3 +151,45 @@ export const deleteGroup = id => {
 export const syncGroupsFromBot = () => {
   return http.post(`/admin/api/app/tg_group/syncFromBot`);
 };
+
+// ==================== 用户绑定管理 ====================
+
+/**
+ * 获取用户绑定列表
+ * @param {Object} params - 查询参数
+ */
+export const getUserBindList = params => {
+  return http.get(`/admin/api/app/tg_user_bind/find`, params);
+};
+
+/**
+ * 获取用户绑定详情
+ * @param {Number} id - 绑定ID
+ */
+export const getUserBind = id => {
+  return http.get(`/admin/api/app/tg_user_bind/get`, { id });
+};
+
+/**
+ * 创建用户绑定
+ * @param {Object} data - 绑定数据
+ */
+export const createUserBind = data => {
+  return http.post(`/admin/api/app/tg_user_bind/create`, data);
+};
+
+/**
+ * 更新用户绑定
+ * @param {Object} data - 绑定数据
+ */
+export const updateUserBind = data => {
+  return http.post(`/admin/api/app/tg_user_bind/update`, data);
+};
+
+/**
+ * 解绑用户(软删除)
+ * @param {Number} id - 绑定ID
+ */
+export const deleteUserBind = id => {
+  return http.get(`/admin/api/app/tg_user_bind/delete`, { id });
+};

+ 6 - 0
magic_admin_web/src/routers/modules/staticRouter.js

@@ -58,6 +58,12 @@ export const staticRouter = [
             name: "telegramGroup",
             component: () => import("@/views/redpacket/group/index.vue"),
             meta: { title: "群组管理", icon: "Menu", isKeepAlive: true }
+          },
+          {
+            path: "user-bind",
+            name: "userBind",
+            component: () => import("@/views/redpacket/user-bind/index.vue"),
+            meta: { title: "用户绑定", icon: "Menu", isKeepAlive: true }
           }
         ]
       },

+ 322 - 0
magic_admin_web/src/views/redpacket/user-bind/index.vue

@@ -0,0 +1,322 @@
+<template>
+  <div class="user-bind-container">
+    <ProTable
+      ref="proTableRef"
+      :columns="columns"
+      :request-api="getUserBindList"
+      :search-columns="searchColumns"
+      :toolbar-buttons="toolbarButtons"
+      :show-pagination="true"
+    >
+      <!-- 绑定状态列 -->
+      <template #bindStatus="{ row }">
+        <el-tag :type="getStatusType(row.bindStatus)">
+          {{ getStatusText(row.bindStatus) }}
+        </el-tag>
+      </template>
+
+      <!-- 绑定时间列 -->
+      <template #bindTime="{ row }">
+        {{ formatTime(row.bindTime) }}
+      </template>
+
+      <!-- 操作列 -->
+      <template #actions="{ row }">
+        <el-button link type="primary" @click="handleView(row)">查看</el-button>
+        <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+        <el-button
+          link
+          type="danger"
+          @click="handleDelete(row)"
+          :disabled="row.bindStatus === 0"
+        >
+          解绑
+        </el-button>
+      </template>
+    </ProTable>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog
+      v-model="dialogVisible"
+      :title="dialogTitle"
+      width="600px"
+      @close="handleDialogClose"
+    >
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="140px"
+      >
+        <el-form-item label="平台用户ID" prop="userId">
+          <el-input
+            v-model.number="formData.userId"
+            placeholder="请输入平台用户ID"
+          />
+        </el-form-item>
+        <el-form-item label="Telegram ID" prop="telegramId">
+          <el-input
+            v-model.number="formData.telegramId"
+            placeholder="请输入Telegram用户ID(可选)"
+          />
+        </el-form-item>
+        <el-form-item label="Telegram用户名" prop="telegramUsername">
+          <el-input
+            v-model="formData.telegramUsername"
+            placeholder="请输入Telegram用户名(不含@)"
+          />
+        </el-form-item>
+        <el-form-item label="Telegram名字">
+          <el-input
+            v-model="formData.telegramFirstName"
+            placeholder="请输入Telegram名字(可选)"
+          />
+        </el-form-item>
+        <el-form-item label="绑定状态" v-if="isEdit">
+          <el-select v-model="formData.bindStatus" placeholder="请选择状态">
+            <el-option label="已绑定" :value="1" />
+            <el-option label="已解绑" :value="0" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 查看对话框 -->
+    <el-dialog v-model="viewDialogVisible" title="绑定详情" width="600px">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="ID">
+          {{ viewData.id }}
+        </el-descriptions-item>
+        <el-descriptions-item label="平台用户ID">
+          {{ viewData.userId }}
+        </el-descriptions-item>
+        <el-descriptions-item label="Telegram ID">
+          {{ viewData.telegramId || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="Telegram用户名">
+          {{ viewData.telegramUsername || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="Telegram名字">
+          {{ viewData.telegramFirstName || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="绑定状态">
+          <el-tag :type="getStatusType(viewData.bindStatus)">
+            {{ getStatusText(viewData.bindStatus) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="绑定时间" :span="2">
+          {{ formatTime(viewData.bindTime) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间" :span="2">
+          {{ formatTimestamp(viewData.createdAt) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="更新时间" :span="2">
+          {{ formatTimestamp(viewData.updatedAt) }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import ProTable from '@/components/ProTable/index.vue';
+import {
+  getUserBindList,
+  createUserBind,
+  updateUserBind,
+  deleteUserBind
+} from '@/api/modules/redpacket';
+
+const proTableRef = ref();
+const formRef = ref();
+const dialogVisible = ref(false);
+const viewDialogVisible = ref(false);
+const dialogTitle = ref('');
+const isEdit = ref(false);
+
+const formData = reactive({
+  id: null,
+  userId: null,
+  telegramId: null,
+  telegramUsername: '',
+  telegramFirstName: '',
+  bindStatus: 1
+});
+
+const viewData = ref({});
+
+// 表格列配置
+const columns = [
+  { prop: 'id', label: 'ID', width: 80 },
+  { prop: 'userId', label: '平台用户ID', width: 120 },
+  { prop: 'telegramId', label: 'Telegram ID', width: 150 },
+  { prop: 'telegramUsername', label: 'Telegram用户名', minWidth: 150 },
+  { prop: 'telegramFirstName', label: 'Telegram名字', width: 150 },
+  { prop: 'bindStatus', label: '绑定状态', width: 100, slot: 'bindStatus' },
+  { prop: 'bindTime', label: '绑定时间', width: 180, slot: 'bindTime' }
+];
+
+// 搜索列配置
+const searchColumns = [
+  { prop: 'user_id', label: '平台用户ID', type: 'input' },
+  { prop: 'telegram_username', label: 'Telegram用户名', type: 'input' },
+  {
+    prop: 'bind_status',
+    label: '绑定状态',
+    type: 'select',
+    options: [
+      { label: '已绑定', value: 1 },
+      { label: '已解绑', value: 0 }
+    ]
+  }
+];
+
+// 工具栏按钮
+const toolbarButtons = [
+  {
+    label: '新增绑定',
+    type: 'primary',
+    icon: 'Plus',
+    onClick: handleAdd
+  }
+];
+
+// 表单验证规则
+const formRules = {
+  userId: [{ required: true, message: '请输入平台用户ID', trigger: 'blur' }],
+  telegramUsername: [{ required: true, message: '请输入Telegram用户名', trigger: 'blur' }]
+};
+
+// 获取状态文本
+const getStatusText = status => {
+  const statusMap = {
+    1: '已绑定',
+    0: '已解绑'
+  };
+  return statusMap[status] !== undefined ? statusMap[status] : '未知';
+};
+
+// 获取状态类型
+const getStatusType = status => {
+  const typeMap = {
+    1: 'success',
+    0: 'info'
+  };
+  return typeMap[status] || '';
+};
+
+// 格式化时间戳(秒)
+const formatTimestamp = timestamp => {
+  if (!timestamp) return '-';
+  const date = new Date(timestamp * 1000);
+  return date.toLocaleString('zh-CN');
+};
+
+// 格式化时间(ISO 字符串或时间戳)
+const formatTime = time => {
+  if (!time) return '-';
+  const date = new Date(time);
+  if (isNaN(date.getTime())) return '-';
+  return date.toLocaleString('zh-CN');
+};
+
+// 新增
+function handleAdd() {
+  dialogTitle.value = '新增绑定';
+  isEdit.value = false;
+  resetForm();
+  dialogVisible.value = true;
+}
+
+// 编辑
+function handleEdit(row) {
+  dialogTitle.value = '编辑绑定';
+  isEdit.value = true;
+  Object.assign(formData, {
+    id: row.id,
+    userId: row.userId,
+    telegramId: row.telegramId,
+    telegramUsername: row.telegramUsername,
+    telegramFirstName: row.telegramFirstName,
+    bindStatus: row.bindStatus
+  });
+  dialogVisible.value = true;
+}
+
+// 查看
+function handleView(row) {
+  viewData.value = { ...row };
+  viewDialogVisible.value = true;
+}
+
+// 解绑
+async function handleDelete(row) {
+  try {
+    await ElMessageBox.confirm(
+      `确定要解绑用户 "${row.telegramUsername}" 吗?此操作会将绑定状态改为已解绑。`,
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    );
+
+    await deleteUserBind(row.id);
+    ElMessage.success('解绑成功');
+    proTableRef.value.refresh();
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error(error.message || '解绑失败');
+    }
+  }
+}
+
+// 提交表单
+async function handleSubmit() {
+  try {
+    await formRef.value.validate();
+
+    const api = isEdit.value ? updateUserBind : createUserBind;
+    await api(formData);
+
+    ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
+    dialogVisible.value = false;
+    proTableRef.value.refresh();
+  } catch (error) {
+    if (error !== false) {
+      ElMessage.error(error.message || '操作失败');
+    }
+  }
+}
+
+// 重置表单
+function resetForm() {
+  Object.assign(formData, {
+    id: null,
+    userId: null,
+    telegramId: null,
+    telegramUsername: '',
+    telegramFirstName: '',
+    bindStatus: 1
+  });
+  formRef.value?.clearValidate();
+}
+
+// 关闭对话框
+function handleDialogClose() {
+  resetForm();
+}
+</script>
+
+<style scoped lang="scss">
+.user-bind-container {
+  padding: 20px;
+}
+</style>

+ 17 - 7
magic_server/commons/services/tg_bind_service.go

@@ -10,8 +10,6 @@ import (
 	"fmt"
 	"math/big"
 	"time"
-
-	"gorm.io/gorm"
 )
 
 type TgBindService struct {
@@ -140,8 +138,10 @@ func (s *TgBindService) BindTelegramUser(userId int64, token string) (*entity.Tg
 	return bind, nil
 }
 
-// IsUserBound 检查 telegramId 是否已绑定,返回 (是否绑定, userId)
-func (s *TgBindService) IsUserBound(telegramId int64) (bool, int64) {
+// IsUserBound 检查是否已绑定,返回 (是否绑定, userId)
+// 优先用 telegramId 查,查不到时用 telegramUsername 兜底(后台手动绑定场景)
+// 兜底匹配成功后自动回填 telegram_id,后续直接用 id 查
+func (s *TgBindService) IsUserBound(telegramId int64, telegramUsername ...string) (bool, int64) {
 	ctx := context.Background()
 	rdb := redisclient.DefaultClient()
 
@@ -156,14 +156,24 @@ func (s *TgBindService) IsUserBound(telegramId int64) (bool, int64) {
 		}
 	}
 
-	// 查 DB
+	// 查 DB —— 优先用 telegram_id
 	var bind entity.TgUserBind
 	err = s.DB().Where("telegram_id = ? AND bind_status = 1", telegramId).First(&bind).Error
 	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
+		// telegram_id 查不到,用 telegram_username 兜底
+		if len(telegramUsername) > 0 && telegramUsername[0] != "" {
+			err = s.DB().Where("telegram_username = ? AND bind_status = 1", telegramUsername[0]).First(&bind).Error
+			if err == nil {
+				// 自动回填 telegram_id,下次直接用 id 查
+				s.DB().Model(&bind).Updates(map[string]interface{}{
+					"telegram_id": telegramId,
+				})
+				bind.TelegramId = telegramId
+			}
+		}
+		if err != nil {
 			return false, 0
 		}
-		return false, 0
 	}
 
 	// 写入缓存

+ 13 - 7
magic_server/telegram/commands.go

@@ -91,7 +91,7 @@ func handleBindCommand(msg *tgbotapi.Message) {
 	bindService := &services.TgBindService{}
 
 	// 检查是否已绑定
-	bound, _ := bindService.IsUserBound(telegramId)
+	bound, _ := bindService.IsUserBound(telegramId, telegramUsername)
 	if bound {
 		sendTextMessage(msg.Chat.ID, "✅ 您已绑定平台账户,无需重复绑定。")
 		return
@@ -106,11 +106,6 @@ func handleBindCommand(msg *tgbotapi.Message) {
 
 	bindURL := generateBindURL(telegramId, token)
 
-	// 如果在群组中,提示查看私聊
-	if msg.Chat.Type == "group" || msg.Chat.Type == "supergroup" {
-		sendTextMessage(msg.Chat.ID, "📩 绑定信息已通过私信发送,请查看机器人私聊消息。")
-	}
-
 	// 发送绑定信息到私聊
 	text := fmt.Sprintf("🔗 绑定平台账户\n\n"+
 		"您的绑定码: *%s*\n"+
@@ -127,7 +122,18 @@ func handleBindCommand(msg *tgbotapi.Message) {
 	)
 
 	// 私聊发送(telegramId 就是私聊 chatId)
-	sendMessageWithKeyboard(telegramId, text, keyboard)
+	sendErr := sendMessageWithKeyboard(telegramId, text, keyboard)
+
+	if msg.Chat.Type == "group" || msg.Chat.Type == "supergroup" {
+		if sendErr != nil {
+			// 用户未私聊过 Bot,无法发送私信
+			botUsername := bot.Self.UserName
+			sendTextMessage(msg.Chat.ID, fmt.Sprintf(
+				"⚠️ 无法发送私信,请先点击 @%s 并发送 /start 开启私聊,然后再发送 /bind", botUsername))
+		} else {
+			sendTextMessage(msg.Chat.ID, "📩 绑定信息已通过私信发送,请查看机器人私聊消息。")
+		}
+	}
 }
 
 // handleBalanceCommand 处理 /balance 命令

+ 3 - 2
magic_server/telegram/messages.go

@@ -37,9 +37,9 @@ func sendHTMLMessage(chatID int64, text string) {
 }
 
 // sendMessageWithKeyboard 发送带按钮的消息
-func sendMessageWithKeyboard(chatID int64, text string, keyboard tgbotapi.InlineKeyboardMarkup) {
+func sendMessageWithKeyboard(chatID int64, text string, keyboard tgbotapi.InlineKeyboardMarkup) error {
 	if !IsEnabled() {
-		return
+		return fmt.Errorf("bot未启用")
 	}
 
 	msg := tgbotapi.NewMessage(chatID, text)
@@ -50,6 +50,7 @@ func sendMessageWithKeyboard(chatID int64, text string, keyboard tgbotapi.Inline
 	if err != nil {
 		core.Log.Errorf("发送消息失败: %v", err)
 	}
+	return err
 }
 
 // editMessage 编辑消息

+ 1 - 1
magic_server/telegram/redpacket.go

@@ -30,7 +30,7 @@ func grabRedPacket(callback *tgbotapi.CallbackQuery, packetNo string) {
 
 	// 检查绑定状态
 	bindService := &services.TgBindService{}
-	bound, userId := bindService.IsUserBound(telegramID)
+	bound, userId := bindService.IsUserBound(telegramID, username)
 
 	if !bound {
 		answerCallback(callback.ID, "⚠️ 请先绑定账户!私聊我发送 /bind")