pudongliang 4 месяцев назад
Родитель
Сommit
f0828a00a3
26 измененных файлов с 1817 добавлено и 68 удалено
  1. 60 0
      portal/package-lock.json
  2. 3 2
      portal/src/api/account.js
  3. 95 0
      portal/src/api/accountGroup.js
  4. 1 0
      portal/src/api/image.js
  5. 1 0
      portal/src/api/video.js
  6. 6 0
      portal/src/router/index.js
  7. 572 0
      portal/src/views/account-group/index.vue
  8. 7 0
      portal/src/views/image/index.vue
  9. 7 0
      portal/src/views/video/index.vue
  10. 219 0
      src/main/java/com/moka/gdtauto/controller/AccountGroupController.java
  11. 28 0
      src/main/java/com/moka/gdtauto/controller/TencentAccountController.java
  12. 23 0
      src/main/java/com/moka/gdtauto/controller/vo/AccountGroupAccountRequest.java
  13. 29 0
      src/main/java/com/moka/gdtauto/controller/vo/CreateAccountGroupRequest.java
  14. 32 0
      src/main/java/com/moka/gdtauto/controller/vo/UpdateAccountGroupRequest.java
  15. 61 0
      src/main/java/com/moka/gdtauto/entity/AccountGroup.java
  16. 45 0
      src/main/java/com/moka/gdtauto/entity/AccountGroupAccountRelationship.java
  17. 26 0
      src/main/java/com/moka/gdtauto/mapper/AccountGroupAccountRelationshipMapper.java
  18. 15 0
      src/main/java/com/moka/gdtauto/mapper/AccountGroupMapper.java
  19. 98 0
      src/main/java/com/moka/gdtauto/service/AccountGroupService.java
  20. 293 0
      src/main/java/com/moka/gdtauto/service/impl/AccountGroupServiceImpl.java
  21. 61 64
      src/main/java/com/moka/gdtauto/service/impl/TencentImagesServiceImpl.java
  22. 3 1
      src/main/java/com/moka/gdtauto/service/impl/TencentVideoServiceImpl.java
  23. 77 1
      src/main/java/com/moka/gdtauto/util/ImageWatermarkUtil.java
  24. 23 0
      src/main/resources/mapper/AccountGroupAccountRelationshipMapper.xml
  25. 5 0
      src/main/resources/mapper/AccountGroupMapper.xml
  26. 27 0
      src/main/resources/sql/create_account_group_tables.sql

+ 60 - 0
portal/package-lock.json

@@ -1518,6 +1518,66 @@
         "node": ">=14.0.0"
       }
     },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+      "version": "1.7.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.1.0",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+      "version": "1.7.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+      "version": "1.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1",
+        "@tybys/wasm-util": "^0.10.1"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+      "version": "2.8.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "0BSD",
+      "optional": true
+    },
     "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
       "version": "4.1.18",
       "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",

+ 3 - 2
portal/src/api/account.js

@@ -41,9 +41,10 @@ export function getAccountDetail(accountId) {
 /**
  * 查询所有广告账户
  */
-export function getAllAccounts() {
+export function getAllAccounts(accountId) {
   return request({
-    url: '/api/gdt/account/all',
+    url: '/api/gdt/account/search',
+    params: { accountId },
     method: 'get'
   })
 }

+ 95 - 0
portal/src/api/accountGroup.js

@@ -0,0 +1,95 @@
+import request from '@/utils/request'
+
+/**
+ * 创建账号组
+ * @param {Object} data 账号组数据
+ */
+export function createAccountGroup(data) {
+  return request({
+    url: '/api/account-group/create',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 更新账号组
+ * @param {Object} data 账号组数据
+ */
+export function updateAccountGroup(data) {
+  return request({
+    url: '/api/account-group/update',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 删除账号组
+ * @param {Number} groupId 账号组ID
+ */
+export function deleteAccountGroup(groupId) {
+  return request({
+    url: `/api/account-group/${groupId}`,
+    method: 'delete'
+  })
+}
+
+/**
+ * 获取账号组列表(分页)
+ * @param {Number} pageNum 页码
+ * @param {Number} pageSize 每页数量
+ */
+export function getAccountGroupList(pageNum = 1, pageSize = 10) {
+  return request({
+    url: '/api/account-group/list',
+    method: 'get',
+    params: { pageNum, pageSize }
+  })
+}
+
+/**
+ * 获取账号组详情
+ * @param {Number} groupId 账号组ID
+ */
+export function getAccountGroupDetail(groupId) {
+  return request({
+    url: `/api/account-group/${groupId}`,
+    method: 'get'
+  })
+}
+
+/**
+ * 添加账号到账号组
+ * @param {Object} data { groupId, accountIds }
+ */
+export function addAccountsToGroup(data) {
+  return request({
+    url: '/api/account-group/add-accounts',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 从账号组移除账号
+ * @param {Object} data { groupId, accountIds }
+ */
+export function removeAccountsFromGroup(data) {
+  return request({
+    url: '/api/account-group/remove-accounts',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 获取账号组的账号列表
+ * @param {Number} groupId 账号组ID
+ */
+export function getGroupAccounts(groupId) {
+  return request({
+    url: `/api/account-group/${groupId}/accounts`,
+    method: 'get'
+  })
+}

+ 1 - 0
portal/src/api/image.js

@@ -5,6 +5,7 @@ import request from '../utils/request'
  */
 export function uploadImage(formData) {
   return request({
+    timeout: 300000,
     url: '/api/tencent-images/upload',
     method: 'post',
     data: formData,

+ 1 - 0
portal/src/api/video.js

@@ -6,6 +6,7 @@ import request from '@/utils/request'
  */
 export function uploadVideo(formData) {
   return request({
+    timeout: 300000,
     url: '/api/tencent-video/upload',
     method: 'post',
     data: formData,

+ 6 - 0
portal/src/router/index.js

@@ -44,6 +44,12 @@ const routes = [
         meta: { title: '广告账户管理', icon: 'User' }
       },
       {
+        path: 'account-group',
+        name: 'AccountGroup',
+        component: () => import('@/views/account-group/index.vue'),
+        meta: { title: '账号组管理', icon: 'Grid' }
+      },
+      {
         path: 'conversion',
         name: 'Conversion',
         component: () => import('@/views/conversion/index.vue'),

+ 572 - 0
portal/src/views/account-group/index.vue

@@ -0,0 +1,572 @@
+<template>
+  <div class="space-y-6">
+    <!-- 主卡片 -->
+    <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
+      <div class="border-b border-gray-200 pb-4 mb-6">
+        <div class="flex items-center justify-between">
+          <h2 class="text-xl font-semibold text-gray-800 flex items-center gap-2">
+            <el-icon><Grid /></el-icon>
+            账号组管理
+          </h2>
+          <el-button type="primary" @click="handleCreate">
+            <el-icon><Plus /></el-icon>
+            创建账号组
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 账号组列表 -->
+      <el-table
+        :data="groupList"
+        style="width: 100%"
+        v-loading="loading"
+        stripe
+        border
+      >
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="groupName" label="账号组名称" width="200" />
+        <el-table-column label="投放模式" width="200">
+          <template #default="{ row }">
+            <el-tag :type="getDeliveryModeType(row.deliveryMode)">
+              {{ getDeliveryModeText(row.deliveryMode) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="remark" label="备注" show-overflow-tooltip />
+        <el-table-column label="创建时间" width="180">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="300" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" size="small" link @click="handleEdit(row)">
+              <el-icon><Edit /></el-icon>
+              编辑
+            </el-button>
+            <el-button type="danger" size="small" link @click="handleDelete(row)">
+              <el-icon><Delete /></el-icon>
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="mt-6 flex justify-end">
+        <el-pagination
+          v-if="total > 0"
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 20, 50]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+        />
+      </div>
+    </div>
+
+    <!-- 创建/编辑账号组对话框 -->
+    <el-dialog
+      v-model="dialogVisible"
+      :title="dialogTitle"
+      width="80%"
+      @close="handleDialogClose"
+    >
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="120px"
+      >
+        <el-form-item label="账号组名称" prop="groupName">
+          <el-input
+            v-model="formData.groupName"
+            placeholder="请输入账号组名称"
+            clearable
+          />
+        </el-form-item>
+        <el-form-item label="投放模式" prop="deliveryMode">
+          <el-select
+            v-model="formData.deliveryMode"
+            placeholder="请选择投放模式"
+            style="width: 100%"
+          >
+            <el-option label="线索-企微-加企微" :value="1" />
+            <el-option label="用户增长-短剧-注册变现ROI" :value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注信息" prop="remark">
+          <el-input
+            v-model="formData.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入备注信息"
+          />
+        </el-form-item>
+        <el-form-item>
+            <el-input
+              v-model="searchKeyword"
+              placeholder="搜索账户ID"
+              clearable
+              @input="handleSearchAccounts"
+              style="width: 300px"
+            >
+              <template #prefix>
+                <el-icon><Search /></el-icon>
+              </template>
+            </el-input>
+            <span class="ml-2 text-sm text-gray-500">已加载 {{ allAccounts.length }} 条账号,已选择 {{ formData.accountIds.length }} 条</span>
+        </el-form-item>
+        <el-form-item label="选择账号">
+          
+          
+          <!-- 左中右三列布局 -->
+          <div class="grid grid-cols-4 gap-4">
+            <!-- 批量粘贴区域 -->
+              <div class="mb-3">
+                <div class="text-sm text-gray-600 mb-1">批量粘贴账号ID(换行分隔):</div>
+                <el-input
+                  v-model="batchAccountIds"
+                  type="textarea"
+                  :rows="3"
+                  placeholder="粘贴账号ID,每行一个"
+                  class="mb-2"
+                />
+                <el-button 
+                  size="small" 
+                  type="primary"
+                  @click="handleBatchAdd"
+                  :disabled="!batchAccountIds.trim()"
+                  style="width: 100%"
+                >
+                  批量添加
+                </el-button>
+              </div>
+            <!-- 左侧:可选账号 -->
+            <div class="border rounded-lg p-4">
+              <div class="flex items-center justify-between mb-3 pb-2 border-b">
+                <span class="font-medium text-gray-700">可选账号</span>
+                <el-button 
+                  size="small" 
+                  @click="selectAll"
+                  :disabled="allAccounts.length === 0"
+                >
+                  全选
+                </el-button>
+              </div>
+              
+              
+              <div class="max-h-96 overflow-y-auto">
+                <el-checkbox-group v-model="selectAccounts">
+                  <div 
+                    v-for="account in allAccounts" 
+                    :key="account.accountId"
+                    class="mb-2"
+                  >
+                    <el-checkbox 
+                      :label="account.accountId"
+                      class="w-full"
+                    >
+                      <span class="text-sm">
+                        {{ account.accountId }}
+                      </span>
+                    </el-checkbox>
+                  </div>
+                </el-checkbox-group>
+                <div v-if="allAccounts.length === 0" class="text-center text-gray-400 py-8">
+                  暂无可选账号
+                </div>
+              </div>
+            </div>
+            <!-- 选择确认按钮 垂直居中 -->
+            <el-button type="primary" @click="handleSelectAccounts" class="self-center justify-self-center">添加</el-button>
+
+            
+            <!-- 右侧:已选账号 -->
+            <div class="border rounded-lg p-4 bg-blue-50">
+              <div class="flex items-center justify-between mb-3 pb-2 border-b border-blue-200">
+                <span class="font-medium text-gray-700">已选账号</span>
+                <el-button 
+                  size="small" 
+                  type="danger"
+                  @click="clearAll"
+                  :disabled="formData.accountIds.length === 0"
+                >
+                  清空
+                </el-button>
+              </div>
+              <div class="max-h-96 overflow-y-auto">
+                <div 
+                  v-for="accountId in formData.accountIds" 
+                  :key="accountId"
+                  class="mb-2 flex items-center justify-between bg-white rounded px-3 py-2"
+                >
+                  <span class="text-sm">
+                    {{ accountId }}
+                  </span>
+                  <el-button 
+                    type="danger" 
+                    size="small" 
+                    link
+                    @click="removeAccount(accountId)"
+                  >
+                    <el-icon><Close /></el-icon>
+                  </el-button>
+                </div>
+                <div v-if="formData.accountIds.length === 0" class="text-center text-gray-400 py-8">
+                  请从左侧选择账号
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">
+          确定
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  getAccountGroupList,
+  createAccountGroup,
+  updateAccountGroup,
+  deleteAccountGroup,
+  getGroupAccounts
+} from '@/api/accountGroup'
+import { getAllAccounts } from '@/api/account'
+import {
+  Grid,
+  Plus,
+  View,
+  Edit,
+  Delete,
+  Search,
+  Close
+} from '@element-plus/icons-vue'
+
+// 数据
+const loading = ref(false)
+const submitting = ref(false)
+const groupList = ref([])
+const currentPage = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+
+// 对话框
+const dialogVisible = ref(false)
+const dialogTitle = ref('创建账号组')
+const formRef = ref(null)
+const editingId = ref(null)
+
+// 表单数据
+const formData = ref({
+  groupName: '',
+  deliveryMode: null,
+  remark: '',
+  accountIds: []
+})
+
+// 表单验证规则
+const formRules = {
+  groupName: [
+    { required: true, message: '请输入账号组名称', trigger: 'blur' }
+  ],
+  deliveryMode: [
+    { required: true, message: '请选择投放模式', trigger: 'change' }
+  ]
+}
+
+// 所有账号列表
+const allAccounts = ref([])
+const selectAccounts = ref([])
+const searchKeyword = ref('')
+const searchTimer = ref(null)
+const batchAccountIds = ref('') // 批量账号ID
+
+// 查看账号对话框
+const accountsDialogVisible = ref(false)
+const accountsLoading = ref(false)
+const groupAccounts = ref([])
+const currentGroupId = ref(null)
+
+
+// 方法
+const loadGroupList = async () => {
+  loading.value = true
+  try {
+    const res = await getAccountGroupList(currentPage.value, pageSize.value)
+    if (res.success) {
+      groupList.value = res.data?.records || []
+      total.value = res.data?.total || 0
+    } else {
+      ElMessage.error(res.message || '加载账号组列表失败')
+    }
+  } catch (error) {
+    console.error('加载账号组列表失败:', error)
+    ElMessage.error('加载账号组列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadAllAccounts = async () => {
+  try {
+    const res = await getAllAccounts()
+    if (res.success) {
+      const accounts = res.data || []
+      allAccounts.value = accounts.map(account => ({
+        accountId: account.accountId,
+        label: `${account.accountId} - ${account.corporationName || '未命名'}`,
+        ...account
+      }))
+      selectAccounts.value = []
+    }
+  } catch (error) {
+    console.error('加载账号列表失败:', error)
+  }
+}
+
+// 搜索账号(防抖处理)
+const handleSearchAccounts = () => {
+  if (searchTimer.value) {
+    clearTimeout(searchTimer.value)
+  }
+  
+  searchTimer.value = setTimeout(async () => {
+    try {
+      const keyword = searchKeyword.value?.trim()
+      const res = keyword 
+        ? await getAllAccounts(keyword) 
+        : await getAllAccounts()
+      
+      if (res.success) {
+        const accounts = res.data || []
+        allAccounts.value = accounts.map(account => ({
+          accountId: account.accountId,
+          label: `${account.accountId} - ${account.corporationName || '未命名'}`,
+          ...account
+        }))
+      }
+    } catch (error) {
+      console.error('搜索账号失败:', error)
+    }
+  }, 500) // 500ms 防抖
+}
+
+
+// 全选
+const selectAll = () => {
+  const allAvailableIds = allAccounts.value.map(acc => acc.accountId)
+  selectAccounts.value = [...allAvailableIds]
+}
+
+// 清空
+const clearAll = () => {
+  formData.value.accountIds = []
+  selectAccounts.value = []
+}
+
+const handleSelectAccounts = () => {
+  formData.value.accountIds = [...new Set([...formData.value.accountIds, ...selectAccounts.value])]
+  selectAccounts.value = []
+}
+
+// 批量添加账号
+const handleBatchAdd = () => {
+  const ids = batchAccountIds.value
+    .split('\n')
+    .map(id => id.trim())
+    .filter(id => id !== '')
+    .map(id => parseInt(id))
+    .filter(id => !isNaN(id))
+  
+  if (ids.length === 0) {
+    ElMessage.warning('请输入有效的账号ID')
+    return
+  }
+  
+  // 合并到已选列表,去重
+  formData.value.accountIds = [...new Set([...formData.value.accountIds, ...ids])]
+  selectAccounts.value = [...formData.value.accountIds]
+  
+  // 清空textarea
+  batchAccountIds.value = ''
+  
+}
+
+// 移除单个账号
+const removeAccount = (accountId) => {
+  const index = formData.value.accountIds.indexOf(accountId)
+  if (index > -1) {
+    formData.value.accountIds.splice(index, 1)
+    selectAccounts.value = selectAccounts.value.filter(accId => accId !== accountId)
+  }
+}
+
+const getDeliveryModeText = (mode) => {
+  const modeMap = {
+    1: '线索-企微-加企微',
+    2: '用户增长-短剧-注册变现ROI'
+  }
+  return modeMap[mode] || '未知'
+}
+
+const getDeliveryModeType = (mode) => {
+  const typeMap = {
+    1: 'success',
+    2: 'warning'
+  }
+  return typeMap[mode] || 'info'
+}
+
+const formatDate = (dateStr) => {
+  if (!dateStr) return '-'
+  return new Date(dateStr).toLocaleString('zh-CN')
+}
+
+const handleCreate = async () => {
+  await loadAllAccounts()
+  dialogTitle.value = '创建账号组'
+  editingId.value = null
+  formData.value = {
+    groupName: '',
+    deliveryMode: null,
+    remark: '',
+    accountIds: []
+  }
+  dialogVisible.value = true
+}
+
+const handleEdit = async (row) => {
+  await loadAllAccounts()
+  dialogTitle.value = '编辑账号组'
+  editingId.value = row.id
+  
+  // 加载该账号组的账号列表
+  try {
+    const res = await getGroupAccounts(row.id)
+    const accountIds = res.success ? (res.data || []) : []
+    
+    formData.value = {
+      groupName: row.groupName,
+      deliveryMode: row.deliveryMode,
+      remark: row.remark,
+      accountIds: accountIds
+    }
+    dialogVisible.value = true
+    selectAccounts.value = []
+  } catch (error) {
+    console.error('加载账号组账号列表失败:', error)
+    ElMessage.error('加载账号组信息失败')
+  }
+}
+
+const handleDelete = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除账号组"${row.groupName}"吗?`,
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+
+    const res = await deleteAccountGroup(row.id)
+    if (res.success) {
+      ElMessage.success('删除成功')
+      await loadGroupList()
+    } else {
+      ElMessage.error(res.message || '删除失败')
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除账号组失败:', error)
+      ElMessage.error('删除失败')
+    }
+  }
+}
+
+const handleSubmit = async () => {
+  try {
+    await formRef.value.validate()
+    
+    submitting.value = true
+    const data = {
+      groupName: formData.value.groupName,
+      deliveryMode: formData.value.deliveryMode,
+      remark: formData.value.remark,
+      accountIds: formData.value.accountIds
+    }
+
+    let res
+    if (editingId.value) {
+      // 编辑
+      data.groupId = editingId.value
+      res = await updateAccountGroup(data)
+    } else {
+      // 创建
+      res = await createAccountGroup(data)
+    }
+
+    if (res.success) {
+      ElMessage.success(editingId.value ? '更新成功' : '创建成功')
+      dialogVisible.value = false
+      await loadGroupList()
+    } else {
+      ElMessage.error(res.message || '操作失败')
+    }
+  } catch (error) {
+    if (error !== false) {
+      console.error('提交失败:', error)
+      ElMessage.error('操作失败')
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+
+const handleDialogClose = () => {
+  formRef.value?.resetFields()
+  formData.value = {
+    groupName: '',
+    deliveryMode: null,
+    remark: '',
+    accountIds: []
+  },
+  selectAccounts.value = []
+}
+
+// 监听分页变化
+watch([currentPage, pageSize], () => {
+  loadGroupList()
+})
+
+// 生命周期
+onMounted(() => {
+  loadGroupList()
+})
+</script>
+
+<style scoped>
+/* 自定义checkbox样式 */
+:deep(.el-checkbox) {
+  width: 100%;
+  margin-right: 0;
+}
+
+:deep(.el-checkbox__label) {
+  width: 100%;
+  white-space: normal;
+  word-break: break-all;
+}
+</style>

+ 7 - 0
portal/src/views/image/index.vue

@@ -311,6 +311,8 @@ const handleExceed = () => {
 }
 
 // 提交上传
+// 10MB 文件大小限制
+const MAX_FILE_SIZE = 10 * 1024 * 1024
 const submitUpload = async () => {
   try {
     await uploadFormRef.value.validate()
@@ -320,6 +322,11 @@ const submitUpload = async () => {
       return
     }
 
+    if (uploadForm.file.size > MAX_FILE_SIZE) {
+      ElMessage.warning('图片文件大小不能超过10MB')
+      return
+    }
+
     if (uploadForm.hasWatermark && !uploadForm.dramaId) {
       ElMessage.error('添加水印时必须选择剧')
       return

+ 7 - 0
portal/src/views/video/index.vue

@@ -283,6 +283,8 @@ const handleBeforeRemove = () => {
 }
 
 // 开始上传
+// 100MB 文件大小限制
+const MAX_FILE_SIZE = 100 * 1024 * 1024
 const handleUpload = async () => {
   if (!uploadFormRef.value) return
 
@@ -293,6 +295,11 @@ const handleUpload = async () => {
       ElMessage.warning('请选择视频文件')
       return
     }
+    
+    if (uploadForm.file.size > MAX_FILE_SIZE) {
+      ElMessage.warning('视频文件大小不能超过100MB')
+      return
+    }
 
     // 校验水印
     if (uploadForm.hasWatermark && !uploadForm.dramaId) {

+ 219 - 0
src/main/java/com/moka/gdtauto/controller/AccountGroupController.java

@@ -0,0 +1,219 @@
+package com.moka.gdtauto.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.moka.gdtauto.annotation.AuthUser;
+import com.moka.gdtauto.common.Result;
+import com.moka.gdtauto.controller.vo.AccountGroupAccountRequest;
+import com.moka.gdtauto.controller.vo.CreateAccountGroupRequest;
+import com.moka.gdtauto.controller.vo.UpdateAccountGroupRequest;
+import com.moka.gdtauto.entity.AccountGroup;
+import com.moka.gdtauto.entity.SysUser;
+import com.moka.gdtauto.service.AccountGroupService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 账号组管理控制器
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Tag(name = "账号组管理", description = "账号组的创建、更新、删除及账号关联管理")
+@Slf4j
+@RestController
+@RequestMapping("/api/account-group")
+@RequiredArgsConstructor
+public class AccountGroupController {
+
+    private final AccountGroupService accountGroupService;
+
+    /**
+     * 创建账号组
+     */
+    @Operation(summary = "创建账号组", description = "为当前用户创建新的账号组,可同时选择账号")
+    @PostMapping("/create")
+    public Result<Long> createAccountGroup(
+            @RequestBody CreateAccountGroupRequest request,
+            @AuthUser SysUser sysUser) {
+        try {
+            Long groupId = accountGroupService.createAccountGroup(
+                    sysUser.getId(),
+                    request.getGroupName(),
+                    request.getDeliveryMode(),
+                    request.getRemark(),
+                    request.getAccountIds()
+            );
+            return Result.success(groupId, "创建账号组成功");
+        } catch (Exception e) {
+            log.error("创建账号组失败", e);
+            return Result.fail("创建账号组失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 更新账号组
+     */
+    @Operation(summary = "更新账号组", description = "更新指定账号组的信息,可同时更新关联账号")
+    @PostMapping("/update")
+    public Result<Void> updateAccountGroup(
+            @RequestBody UpdateAccountGroupRequest request,
+            @AuthUser SysUser sysUser) {
+        try {
+            boolean success = accountGroupService.updateAccountGroup(
+                    request.getGroupId(),
+                    sysUser.getId(),
+                    request.getGroupName(),
+                    request.getDeliveryMode(),
+                    request.getRemark(),
+                    request.getAccountIds()
+            );
+            if (success) {
+                return Result.success("更新账号组成功");
+            } else {
+                return Result.fail("账号组不存在或无权限");
+            }
+        } catch (Exception e) {
+            log.error("更新账号组失败", e);
+            return Result.fail("更新账号组失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 删除账号组
+     */
+    @Operation(summary = "删除账号组", description = "删除指定的账号组")
+    @DeleteMapping("/{groupId}")
+    public Result<Void> deleteAccountGroup(
+            @Parameter(description = "账号组ID", required = true)
+            @PathVariable Long groupId,
+            @AuthUser SysUser sysUser) {
+        try {
+            boolean success = accountGroupService.deleteAccountGroup(groupId, sysUser.getId());
+            if (success) {
+                return Result.success("删除账号组成功");
+            } else {
+                return Result.fail("账号组不存在或无权限");
+            }
+        } catch (Exception e) {
+            log.error("删除账号组失败", e);
+            return Result.fail("删除账号组失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取用户的账号组列表(分页)
+     */
+    @Operation(summary = "获取账号组列表", description = "分页获取当前用户的所有账号组")
+    @GetMapping("/list")
+    public Result<Page<AccountGroup>> getAccountGroups(
+            @Parameter(description = "页码", example = "1")
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @Parameter(description = "每页大小", example = "10")
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @AuthUser SysUser sysUser) {
+        try {
+            Page<AccountGroup> page = accountGroupService.getUserAccountGroups(
+                    sysUser.getId(), pageNum, pageSize);
+            return Result.success(page, "查询成功");
+        } catch (Exception e) {
+            log.error("查询账号组列表失败", e);
+            return Result.fail("查询账号组列表失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取账号组详情
+     */
+    @Operation(summary = "获取账号组详情", description = "根据ID获取账号组详细信息")
+    @GetMapping("/{groupId}")
+    public Result<AccountGroup> getAccountGroup(
+            @Parameter(description = "账号组ID", required = true)
+            @PathVariable Long groupId,
+            @AuthUser SysUser sysUser) {
+        try {
+            AccountGroup accountGroup = accountGroupService.getAccountGroupById(groupId, sysUser.getId());
+            if (accountGroup != null) {
+                return Result.success(accountGroup, "查询成功");
+            } else {
+                return Result.fail("账号组不存在或无权限");
+            }
+        } catch (Exception e) {
+            log.error("查询账号组详情失败", e);
+            return Result.fail("查询账号组详情失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 添加账号到账号组
+     */
+    @Operation(summary = "添加账号到账号组", description = "将一个或多个账号添加到指定账号组")
+    @PostMapping("/add-accounts")
+    public Result<Void> addAccountsToGroup(
+            @RequestBody AccountGroupAccountRequest request,
+            @AuthUser SysUser sysUser) {
+        try {
+            boolean success = accountGroupService.addAccountsToGroup(
+                    request.getGroupId(),
+                    request.getAccountIds(),
+                    sysUser.getId()
+            );
+            if (success) {
+                return Result.success("添加账号成功");
+            } else {
+                return Result.fail("账号组不存在或无权限");
+            }
+        } catch (Exception e) {
+            log.error("添加账号到账号组失败", e);
+            return Result.fail("添加账号失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 从账号组移除账号
+     */
+    @Operation(summary = "从账号组移除账号", description = "从指定账号组移除一个或多个账号")
+    @PostMapping("/remove-accounts")
+    public Result<Void> removeAccountsFromGroup(
+            @RequestBody AccountGroupAccountRequest request,
+            @AuthUser SysUser sysUser) {
+        try {
+            boolean success = accountGroupService.removeAccountsFromGroup(
+                    request.getGroupId(),
+                    request.getAccountIds(),
+                    sysUser.getId()
+            );
+            if (success) {
+                return Result.success("移除账号成功");
+            } else {
+                return Result.fail("账号组不存在或无权限");
+            }
+        } catch (Exception e) {
+            log.error("从账号组移除账号失败", e);
+            return Result.fail("移除账号失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取账号组下的所有账号ID
+     */
+    @Operation(summary = "获取账号组的账号列表", description = "获取指定账号组关联的所有账号ID")
+    @GetMapping("/{groupId}/accounts")
+    public Result<List<Long>> getAccountIdsByGroupId(
+            @Parameter(description = "账号组ID", required = true)
+            @PathVariable Long groupId,
+            @AuthUser SysUser sysUser) {
+        try {
+            List<Long> accountIds = accountGroupService.getAccountIdsByGroupId(groupId, sysUser.getId());
+            return Result.success(accountIds, "查询成功");
+        } catch (Exception e) {
+            log.error("查询账号组账号列表失败", e);
+            return Result.fail("查询账号组账号列表失败: " + e.getMessage());
+        }
+    }
+}

+ 28 - 0
src/main/java/com/moka/gdtauto/controller/TencentAccountController.java

@@ -14,6 +14,11 @@ import com.moka.gdtauto.entity.SysUser;
 import com.moka.gdtauto.annotation.AuthUser;
 
 import java.util.List;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
 
 /**
  * 腾讯广告账户控制器
@@ -134,4 +139,27 @@ public class TencentAccountController {
             return Result.fail("查询失败:" + e.getMessage());
         }
     }
+
+    @GetMapping("/search")
+    public Result<List<TencentAccount>> searchAccount(@Parameter(description = "广告账户ID", required = false, example = "9876543210")
+            @RequestParam(name = "accountId", required = false) Long accountId) {
+        if (accountId == null) {
+            Page<TencentAccount> page = new Page<TencentAccount>(1, 20);
+            // order by id asc
+            Page<TencentAccount> pageResult = tencentAccountService.page(page, new LambdaQueryWrapper<TencentAccount>().orderByAsc(TencentAccount::getId));
+            return Result.success(pageResult.getRecords(), "查询成功");
+        } else {
+            TencentAccount account = tencentAccountService.lambdaQuery()
+                    .eq(TencentAccount::getAccountId, accountId)
+                    .last("LIMIT 1")
+                    .oneOpt()
+                    .orElse(null);
+                    if (account != null) {
+                        return Result.success(List.of(account), "查询成功");
+                    } else {
+                        return Result.fail("未找到该广告账户信息");
+                    }
+        }
+    }
+    
 }

+ 23 - 0
src/main/java/com/moka/gdtauto/controller/vo/AccountGroupAccountRequest.java

@@ -0,0 +1,23 @@
+package com.moka.gdtauto.controller.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 账号组添加/移除账号请求VO
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Data
+@Schema(description = "账号组添加/移除账号请求")
+public class AccountGroupAccountRequest {
+
+    @Schema(description = "账号组ID", example = "1")
+    private Long groupId;
+
+    @Schema(description = "账号ID列表", example = "[123456, 789012]")
+    private List<Long> accountIds;
+}

+ 29 - 0
src/main/java/com/moka/gdtauto/controller/vo/CreateAccountGroupRequest.java

@@ -0,0 +1,29 @@
+package com.moka.gdtauto.controller.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 创建账号组请求VO
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Data
+@Schema(description = "创建账号组请求")
+public class CreateAccountGroupRequest {
+
+    @Schema(description = "账号组名称", example = "测试账号组")
+    private String groupName;
+
+    @Schema(description = "投放模式:1-线索-企微-加企微,2-用户增长-短剧-注册变现ROI", example = "1")
+    private Integer deliveryMode;
+
+    @Schema(description = "备注信息", example = "这是一个测试账号组")
+    private String remark;
+
+    @Schema(description = "账号ID列表", example = "[123456, 789012]")
+    private List<Long> accountIds;
+}

+ 32 - 0
src/main/java/com/moka/gdtauto/controller/vo/UpdateAccountGroupRequest.java

@@ -0,0 +1,32 @@
+package com.moka.gdtauto.controller.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 更新账号组请求VO
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Data
+@Schema(description = "更新账号组请求")
+public class UpdateAccountGroupRequest {
+
+    @Schema(description = "账号组ID", example = "1")
+    private Long groupId;
+
+    @Schema(description = "账号组名称", example = "测试账号组")
+    private String groupName;
+
+    @Schema(description = "投放模式:1-线索-企微-加企微,2-用户增长-短剧-注册变现ROI", example = "1")
+    private Integer deliveryMode;
+
+    @Schema(description = "备注信息", example = "这是一个测试账号组")
+    private String remark;
+
+    @Schema(description = "账号ID列表", example = "[123456, 789012]")
+    private List<Long> accountIds;
+}

+ 61 - 0
src/main/java/com/moka/gdtauto/entity/AccountGroup.java

@@ -0,0 +1,61 @@
+package com.moka.gdtauto.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 账号组实体
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Data 
+@TableName("account_group")
+public class AccountGroup {
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户ID,关联sys_user表
+     */
+    private Long userId;
+
+    /**
+     * 账号组名称
+     */
+    private String groupName;
+
+    /**
+     * 投放模式:1-线索-企微-加企微,2-用户增长-短剧-注册变现ROI
+     */
+    private Integer deliveryMode;
+
+    /**
+     * 备注信息
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 逻辑删除标识:0-未删除,1-已删除
+     */
+    private Integer deleted;
+}
+

+ 45 - 0
src/main/java/com/moka/gdtauto/entity/AccountGroupAccountRelationship.java

@@ -0,0 +1,45 @@
+package com.moka.gdtauto.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 账号组与账号关系表实体
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Data
+@TableName("account_group_account_relationship")
+public class AccountGroupAccountRelationship {
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 账号组ID,关联account_group表
+     */
+    private Long groupId;
+
+    /**
+     * 腾讯账户ID,关联tencent_account表
+     */
+    private Long accountId;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 26 - 0
src/main/java/com/moka/gdtauto/mapper/AccountGroupAccountRelationshipMapper.java

@@ -0,0 +1,26 @@
+package com.moka.gdtauto.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.moka.gdtauto.entity.AccountGroupAccountRelationship;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 账号组与账号关系Mapper接口
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Mapper
+public interface AccountGroupAccountRelationshipMapper extends BaseMapper<AccountGroupAccountRelationship> {
+
+    /**
+     * 批量插入账号组与账号关系
+     * 
+     * @param relationships 关系列表
+     * @return 插入行数
+     */
+    int batchInsert(@Param("relationships") List<AccountGroupAccountRelationship> relationships);
+}

+ 15 - 0
src/main/java/com/moka/gdtauto/mapper/AccountGroupMapper.java

@@ -0,0 +1,15 @@
+package com.moka.gdtauto.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.moka.gdtauto.entity.AccountGroup;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 账号组Mapper接口
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Mapper
+public interface AccountGroupMapper extends BaseMapper<AccountGroup> {
+}

+ 98 - 0
src/main/java/com/moka/gdtauto/service/AccountGroupService.java

@@ -0,0 +1,98 @@
+package com.moka.gdtauto.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.moka.gdtauto.entity.AccountGroup;
+
+import java.util.List;
+
+/**
+ * 账号组服务接口
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+public interface AccountGroupService extends IService<AccountGroup> {
+
+    /**
+     * 创建账号组
+     * 
+     * @param userId 用户ID
+     * @param groupName 账号组名称
+     * @param deliveryMode 投放模式
+     * @param remark 备注
+     * @param accountIds 账号ID列表
+     * @return 账号组ID
+     */
+    Long createAccountGroup(Long userId, String groupName, Integer deliveryMode, String remark, List<Long> accountIds);
+
+    /**
+     * 更新账号组
+     * 
+     * @param groupId 账号组ID
+     * @param userId 用户ID
+     * @param groupName 账号组名称
+     * @param deliveryMode 投放模式
+     * @param remark 备注
+     * @param accountIds 账号ID列表
+     * @return 是否成功
+     */
+    boolean updateAccountGroup(Long groupId, Long userId, String groupName, Integer deliveryMode, String remark, List<Long> accountIds);
+
+    /**
+     * 删除账号组
+     * 
+     * @param groupId 账号组ID
+     * @param userId 用户ID
+     * @return 是否成功
+     */
+    boolean deleteAccountGroup(Long groupId, Long userId);
+
+    /**
+     * 获取用户的账号组列表(分页)
+     * 
+     * @param userId 用户ID
+     * @param pageNum 页码
+     * @param pageSize 每页大小
+     * @return 账号组分页列表
+     */
+    Page<AccountGroup> getUserAccountGroups(Long userId, Integer pageNum, Integer pageSize);
+
+    /**
+     * 获取账号组详情
+     * 
+     * @param groupId 账号组ID
+     * @param userId 用户ID
+     * @return 账号组信息
+     */
+    AccountGroup getAccountGroupById(Long groupId, Long userId);
+
+    /**
+     * 添加账号到账号组
+     * 
+     * @param groupId 账号组ID
+     * @param accountIds 账号ID列表
+     * @param userId 用户ID
+     * @return 是否成功
+     */
+    boolean addAccountsToGroup(Long groupId, List<Long> accountIds, Long userId);
+
+    /**
+     * 从账号组移除账号
+     * 
+     * @param groupId 账号组ID
+     * @param accountIds 账号ID列表
+     * @param userId 用户ID
+     * @return 是否成功
+     */
+    boolean removeAccountsFromGroup(Long groupId, List<Long> accountIds, Long userId);
+
+    /**
+     * 获取账号组下的所有账号ID
+     * 
+     * @param groupId 账号组ID
+     * @param userId 用户ID
+     * @return 账号ID列表
+     */
+    List<Long> getAccountIdsByGroupId(Long groupId, Long userId);
+}

+ 293 - 0
src/main/java/com/moka/gdtauto/service/impl/AccountGroupServiceImpl.java

@@ -0,0 +1,293 @@
+package com.moka.gdtauto.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.moka.gdtauto.entity.AccountGroup;
+import com.moka.gdtauto.entity.AccountGroupAccountRelationship;
+import com.moka.gdtauto.entity.TencentAccount;
+import com.moka.gdtauto.mapper.AccountGroupAccountRelationshipMapper;
+import com.moka.gdtauto.mapper.AccountGroupMapper;
+import com.moka.gdtauto.service.AccountGroupService;
+import com.moka.gdtauto.service.TencentAccountService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 账号组服务实现类
+ * 
+ * @author moka
+ * @since 2026-02-24
+ */
+@Slf4j
+@Service
+public class AccountGroupServiceImpl extends ServiceImpl<AccountGroupMapper, AccountGroup> implements AccountGroupService {
+
+    @Autowired
+    private AccountGroupAccountRelationshipMapper relationshipMapper;
+    
+    @Autowired
+    private TencentAccountService tencentAccountService;
+
+    /**
+     * 校验账号ID是否存在
+     * 
+     * @param accountIds 账号ID列表
+     * @return 不存在的账号ID列表
+     */
+    private List<Long> validateAccountIds(List<Long> accountIds) {
+        if (accountIds == null || accountIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        
+        List<Long> invalidIds = new ArrayList<>();
+        for (Long accountId : accountIds) {
+            TencentAccount account = tencentAccountService.lambdaQuery()
+                    .eq(TencentAccount::getAccountId, accountId)
+                    .eq(TencentAccount::getDeleted, 0)
+                    .one();
+            if (account == null) {
+                invalidIds.add(accountId);
+            }
+        }
+        return invalidIds;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createAccountGroup(Long userId, String groupName, Integer deliveryMode, String remark, List<Long> accountIds) {
+        // 校验账号ID
+        if (accountIds != null && !accountIds.isEmpty()) {
+            List<Long> invalidIds = validateAccountIds(accountIds);
+            if (!invalidIds.isEmpty()) {
+                throw new RuntimeException("以下账号ID不存在:" + invalidIds.toString());
+            }
+        }
+        
+        AccountGroup accountGroup = new AccountGroup();
+        accountGroup.setUserId(userId);
+        accountGroup.setGroupName(groupName);
+        accountGroup.setDeliveryMode(deliveryMode);
+        accountGroup.setRemark(remark);
+        accountGroup.setCreateTime(LocalDateTime.now());
+        accountGroup.setUpdateTime(LocalDateTime.now());
+        accountGroup.setDeleted(0);
+        
+        this.save(accountGroup);
+        log.info("创建账号组成功,用户ID: {}, 账号组ID: {}, 账号组名称: {}", userId, accountGroup.getId(), groupName);
+        
+        // 如果有账号ID列表,批量添加关系
+        if (accountIds != null && !accountIds.isEmpty()) {
+            List<AccountGroupAccountRelationship> relationships = new ArrayList<>();
+            LocalDateTime now = LocalDateTime.now();
+            for (Long accountId : accountIds) {
+                AccountGroupAccountRelationship relationship = new AccountGroupAccountRelationship();
+                relationship.setGroupId(accountGroup.getId());
+                relationship.setAccountId(accountId);
+                relationship.setCreateTime(now);
+                relationship.setUpdateTime(now);
+                relationships.add(relationship);
+            }
+            relationshipMapper.batchInsert(relationships);
+            log.info("批量添加账号到账号组,账号组ID: {}, 账号数: {}", accountGroup.getId(), accountIds.size());
+        }
+        
+        return accountGroup.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateAccountGroup(Long groupId, Long userId, String groupName, Integer deliveryMode, String remark, List<Long> accountIds) {
+        // 验证账号组是否属于该用户
+        AccountGroup existingGroup = this.getOne(new LambdaQueryWrapper<AccountGroup>()
+                .eq(AccountGroup::getId, groupId)
+                .eq(AccountGroup::getUserId, userId)
+                .eq(AccountGroup::getDeleted, 0));
+        
+        if (existingGroup == null) {
+            log.warn("账号组不存在或无权限,账号组ID: {}, 用户ID: {}", groupId, userId);
+            return false;
+        }
+        
+        // 校验账号ID
+        if (accountIds != null && !accountIds.isEmpty()) {
+            List<Long> invalidIds = validateAccountIds(accountIds);
+            if (!invalidIds.isEmpty()) {
+                throw new RuntimeException("以下账号ID不存在:" + invalidIds.toString());
+            }
+        }
+        
+        existingGroup.setGroupName(groupName);
+        existingGroup.setDeliveryMode(deliveryMode);
+        existingGroup.setRemark(remark);
+        existingGroup.setUpdateTime(LocalDateTime.now());
+        
+        boolean result = this.updateById(existingGroup);
+        log.info("更新账号组成功,账号组ID: {}, 用户ID: {}", groupId, userId);
+        
+        // 如果有账号ID列表,更新关系(先删除旧关系,再添加新关系)
+        if (accountIds != null) {
+            // 删除旧关系
+            relationshipMapper.delete(new LambdaQueryWrapper<AccountGroupAccountRelationship>()
+                    .eq(AccountGroupAccountRelationship::getGroupId, groupId));
+            
+            // 添加新关系
+            if (!accountIds.isEmpty()) {
+                List<AccountGroupAccountRelationship> relationships = new ArrayList<>();
+                LocalDateTime now = LocalDateTime.now();
+                for (Long accountId : accountIds) {
+                    AccountGroupAccountRelationship relationship = new AccountGroupAccountRelationship();
+                    relationship.setGroupId(groupId);
+                    relationship.setAccountId(accountId);
+                    relationship.setCreateTime(now);
+                    relationship.setUpdateTime(now);
+                    relationships.add(relationship);
+                }
+                relationshipMapper.batchInsert(relationships);
+                log.info("更新账号组关联账号,账号组ID: {}, 新账号数: {}", groupId, accountIds.size());
+            } else {
+                log.info("清空账号组关联账号,账号组ID: {}", groupId);
+            }
+        }
+        
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteAccountGroup(Long groupId, Long userId) {
+        // 验证账号组是否属于该用户
+        AccountGroup existingGroup = this.getOne(new LambdaQueryWrapper<AccountGroup>()
+                .eq(AccountGroup::getId, groupId)
+                .eq(AccountGroup::getUserId, userId)
+                .eq(AccountGroup::getDeleted, 0));
+        
+        if (existingGroup == null) {
+            log.warn("账号组不存在或无权限,账号组ID: {}, 用户ID: {}", groupId, userId);
+            return false;
+        }
+        
+        // 逻辑删除账号组
+        existingGroup.setDeleted(1);
+        existingGroup.setUpdateTime(LocalDateTime.now());
+        this.updateById(existingGroup);
+        
+        // 删除关联关系
+        relationshipMapper.delete(new LambdaQueryWrapper<AccountGroupAccountRelationship>()
+                .eq(AccountGroupAccountRelationship::getGroupId, groupId));
+        
+        log.info("删除账号组成功,账号组ID: {}, 用户ID: {}", groupId, userId);
+        return true;
+    }
+
+    @Override
+    public Page<AccountGroup> getUserAccountGroups(Long userId, Integer pageNum, Integer pageSize) {
+        Page<AccountGroup> page = new Page<>(pageNum, pageSize);
+        
+        LambdaQueryWrapper<AccountGroup> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(AccountGroup::getUserId, userId)
+                .eq(AccountGroup::getDeleted, 0)
+                .orderByDesc(AccountGroup::getCreateTime);
+        
+        return this.page(page, queryWrapper);
+    }
+
+    @Override
+    public AccountGroup getAccountGroupById(Long groupId, Long userId) {
+        return this.getOne(new LambdaQueryWrapper<AccountGroup>()
+                .eq(AccountGroup::getId, groupId)
+                .eq(AccountGroup::getUserId, userId)
+                .eq(AccountGroup::getDeleted, 0));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean addAccountsToGroup(Long groupId, List<Long> accountIds, Long userId) {
+        // 验证账号组是否属于该用户
+        AccountGroup existingGroup = this.getAccountGroupById(groupId, userId);
+        if (existingGroup == null) {
+            log.warn("账号组不存在或无权限,账号组ID: {}, 用户ID: {}", groupId, userId);
+            return false;
+        }
+        
+        // 查询已存在的关系
+        List<AccountGroupAccountRelationship> existingRelations = relationshipMapper.selectList(
+                new LambdaQueryWrapper<AccountGroupAccountRelationship>()
+                        .eq(AccountGroupAccountRelationship::getGroupId, groupId)
+                        .in(AccountGroupAccountRelationship::getAccountId, accountIds));
+        
+        List<Long> existingAccountIds = existingRelations.stream()
+                .map(AccountGroupAccountRelationship::getAccountId)
+                .collect(Collectors.toList());
+        
+        // 过滤出需要新增的账号ID
+        List<Long> newAccountIds = accountIds.stream()
+                .filter(id -> !existingAccountIds.contains(id))
+                .collect(Collectors.toList());
+        
+        if (newAccountIds.isEmpty()) {
+            log.info("所有账号已存在于账号组中,账号组ID: {}", groupId);
+            return true;
+        }
+        
+        // 批量插入关系
+        List<AccountGroupAccountRelationship> relationships = new ArrayList<>();
+        LocalDateTime now = LocalDateTime.now();
+        for (Long accountId : newAccountIds) {
+            AccountGroupAccountRelationship relationship = new AccountGroupAccountRelationship();
+            relationship.setGroupId(groupId);
+            relationship.setAccountId(accountId);
+            relationship.setCreateTime(now);
+            relationship.setUpdateTime(now);
+            relationships.add(relationship);
+        }
+        relationshipMapper.batchInsert(relationships);
+        
+        log.info("添加账号到账号组成功,账号组ID: {}, 新增账号数: {}", groupId, newAccountIds.size());
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean removeAccountsFromGroup(Long groupId, List<Long> accountIds, Long userId) {
+        // 验证账号组是否属于该用户
+        AccountGroup existingGroup = this.getAccountGroupById(groupId, userId);
+        if (existingGroup == null) {
+            log.warn("账号组不存在或无权限,账号组ID: {}, 用户ID: {}", groupId, userId);
+            return false;
+        }
+        
+        // 删除关系
+        int deletedCount = relationshipMapper.delete(new LambdaQueryWrapper<AccountGroupAccountRelationship>()
+                .eq(AccountGroupAccountRelationship::getGroupId, groupId)
+                .in(AccountGroupAccountRelationship::getAccountId, accountIds));
+        
+        log.info("从账号组移除账号成功,账号组ID: {}, 移除账号数: {}", groupId, deletedCount);
+        return true;
+    }
+
+    @Override
+    public List<Long> getAccountIdsByGroupId(Long groupId, Long userId) {
+        // 验证账号组是否属于该用户
+        AccountGroup existingGroup = this.getAccountGroupById(groupId, userId);
+        if (existingGroup == null) {
+            log.warn("账号组不存在或无权限,账号组ID: {}, 用户ID: {}", groupId, userId);
+            return List.of();
+        }
+        
+        List<AccountGroupAccountRelationship> relationships = relationshipMapper.selectList(
+                new LambdaQueryWrapper<AccountGroupAccountRelationship>()
+                        .eq(AccountGroupAccountRelationship::getGroupId, groupId));
+        
+        return relationships.stream()
+                .map(AccountGroupAccountRelationship::getAccountId)
+                .collect(Collectors.toList());
+    }
+}

+ 61 - 64
src/main/java/com/moka/gdtauto/service/impl/TencentImagesServiceImpl.java

@@ -16,13 +16,13 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.File;
-import java.io.FileInputStream;
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
 import java.math.BigInteger;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.security.MessageDigest;
 import java.time.LocalDateTime;
+import java.util.Base64;
 
 /**
  * 腾讯广告图片Service实现类
@@ -54,59 +54,57 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
         imageRecord.setCreatedTime(LocalDateTime.now());
         imageRecord.setUpdatedTime(LocalDateTime.now());
 
-        File tempFile = null;
-        File watermarkedFile = null;
-
         try {
-            // 1. 保存临时文件
-            tempFile = File.createTempFile("upload-", getFileExtension(file.getOriginalFilename()));
-            file.transferTo(tempFile);
-            log.info("临时文件保存成功: {}", tempFile.getAbsolutePath());
+            // 1. 获取原始图片字节数据
+            byte[] originalImageBytes = file.getBytes();
+            log.info("获取原始图片数据成功,大小: {} bytes", originalImageBytes.length);
 
-            File fileToUpload = tempFile;
+            byte[] finalImageBytes = originalImageBytes;
 
-            // 2. 如果需要添加水印
+            // 2. 如果需要添加水印,在内存中处理
             if (hasWatermark != null && hasWatermark && dramaId != null) {
                 TencentAssetDrama drama = dramaService.getById(dramaId);
                 if (drama != null && drama.getMarketingAssetName() != null) {
-                    log.info("开始添加水印,剧名: {}", drama.getMarketingAssetName());
-                    
-                    // 创建水印文件
-                    watermarkedFile = File.createTempFile("watermarked-", getFileExtension(file.getOriginalFilename()));
+                    log.info("开始在内存中添加水印,剧名: {}", drama.getMarketingAssetName());
                     
-                    // 添加水印到左上角,使用默认字体大小48,位置(50, 50)
-                    ImageWatermarkUtil.addWatermark(
-                        tempFile.getAbsolutePath(), 
-                        watermarkedFile.getAbsolutePath(), 
+                    // 从字节数组读取图片到BufferedImage
+                    BufferedImage watermarkedImage = ImageWatermarkUtil.addWatermarkInMemory(
+                        originalImageBytes, 
                         drama.getMarketingAssetName(),
                         48,  // 字体大小
                         50,  // x坐标 - 左上角
                         50   // y坐标 - 左上角
                     );
                     
-                    fileToUpload = watermarkedFile;
+                    // 将BufferedImage转换回字节数组
+                    finalImageBytes = bufferedImageToBytes(watermarkedImage, getImageFormat(file.getOriginalFilename()));
+                    
                     imageRecord.setWatermarkText(drama.getMarketingAssetName());
-                    log.info("水印添加成功: {}", drama.getMarketingAssetName());
+                    log.info("水印添加成功: {},新图片大小: {} bytes", drama.getMarketingAssetName(), finalImageBytes.length);
                 }
             }
 
             // 3. 计算MD5签名
-            String signature = calculateMD5(fileToUpload);
-            log.info("文件MD5签名: {}", signature);
+            String signature = calculateMD5(finalImageBytes);
+            log.info("图片MD5签名: {}", signature);
 
-            // 4. 获取TencentAds客户端
-            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber,accountId);
+            // 4. 转换为Base64编码
+            String base64Image = Base64.getEncoder().encodeToString(finalImageBytes);
+            log.info("Base64编码完成,长度: {}", base64Image.length());
+
+            // 5. 获取TencentAds客户端
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber, accountId);
             log.info("开始上传图片到腾讯广告,账户ID: {}", accountId);
 
-            // 5. 调用腾讯广告API上传图片
+            // 6. 调用腾讯广告API上传图片(使用UPLOAD_TYPE_BYTES方式)
             ImagesAddResponseData response = tencentAds.images()
                 .imagesAdd(
-                    "UPLOAD_TYPE_FILE",
+                    "UPLOAD_TYPE_BYTES",  // 使用字节方式上传
                     signature,
                     accountId,
                     null,
-                    fileToUpload,
-                    null,
+                    null,                  // file参数为null
+                    base64Image,           // 传入Base64编码的图片
                     null,
                     description,
                     null,
@@ -114,7 +112,7 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
                     null
                 );
 
-            // 6. 保存上传结果
+            // 7. 保存上传结果
             if (response != null) {
                 imageRecord.setImageId(response.getImageId());
                 imageRecord.setImageWidth(response.getImageWidth() != null ? response.getImageWidth().intValue() : null);
@@ -140,27 +138,9 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
             log.error("图片上传失败", e);
             imageRecord.setUploadStatus("FAILED");
             imageRecord.setErrorMessage(e.getMessage());
-        } finally {
-            // 清理临时文件
-            if (tempFile != null && tempFile.exists()) {
-                try {
-                    Files.delete(tempFile.toPath());
-                    log.debug("临时文件删除成功: {}", tempFile.getAbsolutePath());
-                } catch (Exception e) {
-                    log.warn("临时文件删除失败: {}", tempFile.getAbsolutePath(), e);
-                }
-            }
-            if (watermarkedFile != null && watermarkedFile.exists()) {
-                try {
-                    Files.delete(watermarkedFile.toPath());
-                    log.debug("水印文件删除成功: {}", watermarkedFile.getAbsolutePath());
-                } catch (Exception e) {
-                    log.warn("水印文件删除失败: {}", watermarkedFile.getAbsolutePath(), e);
-                }
-            }
         }
 
-        // 7. 保存记录到数据库
+        // 8. 保存记录到数据库
         this.save(imageRecord);
         log.info("图片记录保存成功,ID: {}", imageRecord.getId());
 
@@ -168,17 +148,11 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
     }
 
     /**
-     * 计算文件的MD5签名
+     * 计算字节数组的MD5签名
      */
-    private String calculateMD5(File file) throws Exception {
+    private String calculateMD5(byte[] bytes) throws Exception {
         MessageDigest md = MessageDigest.getInstance("MD5");
-        try (FileInputStream fis = new FileInputStream(file)) {
-            byte[] buffer = new byte[8192];
-            int bytesRead;
-            while ((bytesRead = fis.read(buffer)) != -1) {
-                md.update(buffer, 0, bytesRead);
-            }
-        }
+        md.update(bytes);
         byte[] digest = md.digest();
         BigInteger bigInt = new BigInteger(1, digest);
         String hashtext = bigInt.toString(16);
@@ -190,12 +164,35 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
     }
 
     /**
-     * 获取文件扩展名
+     * 将BufferedImage转换为字节数组
+     */
+    private byte[] bufferedImageToBytes(BufferedImage image, String formatName) throws Exception {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            ImageIO.write(image, formatName, baos);
+            baos.flush();
+            return baos.toByteArray();
+        }
+    }
+
+    /**
+     * 获取图片格式名称
      */
-    private String getFileExtension(String filename) {
+    private String getImageFormat(String filename) {
         if (filename == null || !filename.contains(".")) {
-            return ".jpg";
+            return "jpg";
+        }
+        String extension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
+        switch (extension) {
+            case "jpeg":
+                return "jpg";
+            case "png":
+                return "png";
+            case "gif":
+                return "gif";
+            case "bmp":
+                return "bmp";
+            default:
+                return "jpg";
         }
-        return filename.substring(filename.lastIndexOf("."));
     }
 }

+ 3 - 1
src/main/java/com/moka/gdtauto/service/impl/TencentVideoServiceImpl.java

@@ -63,7 +63,9 @@ public class TencentVideoServiceImpl extends ServiceImpl<TencentVideoMapper, Ten
 
         try {
             // 1. 保存临时文件
-            tempFile = File.createTempFile("upload-video-", getFileExtension(file.getOriginalFilename()));
+            // 加个随机数,避免文件名冲突
+            String randomSuffix = String.valueOf(System.currentTimeMillis());
+            tempFile = File.createTempFile("upload-video-" + randomSuffix + "-", getFileExtension(file.getOriginalFilename()));
             file.transferTo(tempFile);
             log.info("临时视频文件保存成功: {}", tempFile.getAbsolutePath());
 

+ 77 - 1
src/main/java/com/moka/gdtauto/util/ImageWatermarkUtil.java

@@ -6,8 +6,10 @@ import net.coobird.thumbnailator.Thumbnails;
 import javax.imageio.ImageIO;
 import java.awt.*;
 import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 
 /**
  * 图片水印工具类
@@ -64,7 +66,6 @@ public class ImageWatermarkUtil {
         
         // 添加阴影效果(描边)
         g2d.setStroke(new BasicStroke(2));
-        FontMetrics fontMetrics = g2d.getFontMetrics();
         
         // 绘制黑色描边
         g2d.setColor(new Color(0, 0, 0, 180));
@@ -141,6 +142,81 @@ public class ImageWatermarkUtil {
     }
 
     /**
+     * 在内存中为BufferedImage添加文字水印
+     *
+     * @param originalImage 原始图片
+     * @param watermarkText 水印文字(如:《绝世龙王》)
+     * @param fontSize      字体大小
+     * @param x             水印X坐标(从左边距)
+     * @param y             水印Y坐标(从顶部距)
+     * @return 添加水印后的图片
+     */
+    public static BufferedImage addWatermarkInMemory(BufferedImage originalImage, 
+                                                      String watermarkText, int fontSize, int x, int y) {
+        log.info("开始在内存中添加水印, 水印文字: {}", watermarkText);
+        
+        // 创建Graphics2D对象
+        Graphics2D g2d = originalImage.createGraphics();
+        
+        // 设置抗锯齿
+        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        
+        // 设置字体(使用系统默认黑体或宋体)
+        Font font = new Font("Microsoft YaHei", Font.BOLD, fontSize);
+        g2d.setFont(font);
+        
+        // 设置颜色和透明度
+        g2d.setColor(new Color(255, 255, 255, 230)); // 白色,透明度230
+        
+        // 添加阴影效果(描边)
+        g2d.setStroke(new BasicStroke(2));
+        
+        // 绘制黑色描边
+        g2d.setColor(new Color(0, 0, 0, 180));
+        for (int i = -1; i <= 1; i++) {
+            for (int j = -1; j <= 1; j++) {
+                g2d.drawString(watermarkText, x + i, y + j);
+            }
+        }
+        
+        // 绘制白色文字
+        g2d.setColor(new Color(255, 255, 255, 230));
+        g2d.drawString(watermarkText, x, y);
+        
+        // 释放资源
+        g2d.dispose();
+        
+        log.info("内存水印添加成功");
+        return originalImage;
+    }
+
+    /**
+     * 在内存中为图片字节数组添加文字水印
+     *
+     * @param imageBytes    原始图片字节数组
+     * @param watermarkText 水印文字(如:《绝世龙王》)
+     * @param fontSize      字体大小
+     * @param x             水印X坐标(从左边距)
+     * @param y             水印Y坐标(从顶部距)
+     * @return 添加水印后的图片
+     * @throws IOException IO异常
+     */
+    public static BufferedImage addWatermarkInMemory(byte[] imageBytes, 
+                                                      String watermarkText, int fontSize, int x, int y) throws IOException {
+        log.info("从字节数组加载图片并添加水印");
+        
+        // 从字节数组读取图片
+        try (InputStream inputStream = new ByteArrayInputStream(imageBytes)) {
+            BufferedImage originalImage = ImageIO.read(inputStream);
+            if (originalImage == null) {
+                throw new IOException("无法解析图片数据");
+            }
+            return addWatermarkInMemory(originalImage, watermarkText, fontSize, x, y);
+        }
+    }
+
+    /**
      * 获取图片格式
      */
     private static String getImageFormat(String filePath) {

+ 23 - 0
src/main/resources/mapper/AccountGroupAccountRelationshipMapper.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.moka.gdtauto.mapper.AccountGroupAccountRelationshipMapper">
+
+    <!-- 批量插入账号组与账号关系 -->
+    <insert id="batchInsert" parameterType="java.util.List">
+        INSERT INTO account_group_account_relationship (
+            group_id,
+            account_id,
+            create_time,
+            update_time
+        ) VALUES
+        <foreach collection="relationships" item="item" separator=",">
+            (
+                #{item.groupId},
+                #{item.accountId},
+                #{item.createTime},
+                #{item.updateTime}
+            )
+        </foreach>
+    </insert>
+
+</mapper>

+ 5 - 0
src/main/resources/mapper/AccountGroupMapper.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.moka.gdtauto.mapper.AccountGroupMapper">
+
+</mapper>

+ 27 - 0
src/main/resources/sql/create_account_group_tables.sql

@@ -0,0 +1,27 @@
+-- 创建账号组表
+CREATE TABLE IF NOT EXISTS `account_group` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `user_id` BIGINT NOT NULL COMMENT '用户ID,关联sys_user表',
+    `group_name` VARCHAR(100) NOT NULL COMMENT '账号组名称',
+    `delivery_mode` TINYINT NOT NULL COMMENT '投放模式:1-线索-企微-加企微,2-用户增长-短剧-注册变现ROI',
+    `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注信息',
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识:0-未删除,1-已删除',
+    PRIMARY KEY (`id`),
+    KEY `idx_user_id` (`user_id`),
+    KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账号组表';
+
+-- 创建账号组与账号关系表
+CREATE TABLE IF NOT EXISTS `account_group_account_relationship` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `group_id` BIGINT NOT NULL COMMENT '账号组ID,关联account_group表',
+    `account_id` BIGINT NOT NULL COMMENT '腾讯账户ID,关联tencent_account表',
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_group_account` (`group_id`, `account_id`),
+    KEY `idx_group_id` (`group_id`),
+    KEY `idx_account_id` (`account_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账号组与账号关系表';