skyline 1 месяц назад
Родитель
Сommit
921e27caa9

+ 17 - 0
haha-admin-web/src/router/modules/marketing.ts

@@ -1,3 +1,20 @@
+/**
+ * 营销中心路由配置
+ *
+ * 【统一入口说明】
+ * - /marketing/activity  : 统一活动管理入口(已整合邀请有礼 activityType=4)
+ *   支持类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼
+ *   后端通过 UnifiedActivityService 根据 activityType 分发到对应 Service
+ *
+ * 【向后兼容路由】
+ * - /marketing/invite     : 邀请活动独立管理页面(保留,向后兼容)
+ *   包含子路由:index(活动管理) / statistics(数据统计) / records(邀请记录)
+ *   该路由使用独立的 InviteActivity API(/marketing/invite/*),与统一入口共存
+ *
+ * 【小程序端 API】
+ * - /invite/*             : 小程序端专属接口(haha-miniapp 模块的 InviteController)
+ *   与管理端完全独立,直接调用 InviteActivityService
+ */
 import Layout from "@/layout/index.vue";
 
 export default {

+ 1 - 0
haha-admin-web/src/views/marketing/activity/index.vue

@@ -64,6 +64,7 @@ const {
           <el-option label="首单立减" :value="1" />
           <el-option label="优惠折扣" :value="2" />
           <el-option label="商品满减" :value="3" />
+          <el-option label="邀请有礼" :value="4" />
         </el-select>
       </el-form-item>
       <el-form-item label="状态:" prop="status">

+ 381 - 0
haha-admin-web/src/views/marketing/activity/utils/components/InviteRewardBlock.vue

@@ -0,0 +1,381 @@
+<template>
+  <el-card class="invite-reward-block" shadow="never">
+    <template #header>
+      <div class="card-header">
+        <span class="header-title">邀请规则配置</span>
+        <el-tag type="danger" size="small">仅邀请有礼活动</el-tag>
+      </div>
+    </template>
+
+    <!-- 奖励规则类型 -->
+    <el-form-item label="奖励规则类型">
+      <el-select
+        v-model="localData.rewardRuleType"
+        disabled
+        style="width: 200px"
+      >
+        <el-option label="固定奖励" value="fixed" />
+      </el-select>
+      <span class="tip-text">当前仅支持固定奖励模式</span>
+    </el-form-item>
+
+    <!-- 分隔线 -->
+    <el-divider content-position="left">
+      <span class="divider-text">邀请人奖励配置(必填)</span>
+    </el-divider>
+
+    <!-- 邀请人优惠券模板 -->
+    <el-form-item label="邀请人优惠券" required>
+      <el-select
+        v-model="localData.inviterCouponTemplateId"
+        placeholder="请选择邀请人优惠券模板"
+        filterable
+        clearable
+        style="width: 100%"
+      >
+        <el-option
+          v-for="item in couponTemplateList"
+          :key="item.id"
+          :label="`${item.name} (面额: ${item.discountValue}元)`"
+          :value="item.id"
+        />
+      </el-select>
+      <div class="tip-text">被邀请人完成首单后发放给邀请人的优惠券</div>
+    </el-form-item>
+
+    <!-- 分隔线 -->
+    <el-divider content-position="left">
+      <span class="divider-text">被邀请人奖励配置(可选)</span>
+    </el-divider>
+
+    <!-- 被邀请人优惠券模板 -->
+    <el-form-item label="被邀请人优惠券">
+      <el-select
+        v-model="localData.inviteeCouponTemplateId"
+        placeholder="可选:选择被邀请人新人券"
+        filterable
+        clearable
+        style="width: 100%"
+      >
+        <el-option
+          v-for="item in couponTemplateList"
+          :key="item.id"
+          :label="`${item.name} (面额: ${item.discountValue}元)`"
+          :value="item.id"
+        />
+      </el-select>
+      <div class="tip-text">新用户注册绑定时即时发放的优惠券</div>
+    </el-form-item>
+
+    <!-- 分隔线 -->
+    <el-divider content-position="left">
+      <span class="divider-text">限制条件</span>
+    </el-divider>
+
+    <!-- 每日邀请上限 -->
+    <el-form-item label="每日邀请上限">
+      <el-input-number
+        v-model="localData.dailyLimit"
+        :min="1"
+        :max="100"
+        :step="1"
+        placeholder="默认10"
+        controls-position="right"
+      />
+      <span class="tip-text">每人每天最多可发送的邀请次数</span>
+    </el-form-item>
+
+    <!-- 总邀请上限 -->
+    <el-form-item label="总邀请上限">
+      <el-input-number
+        v-model="localData.totalLimit"
+        :min="1"
+        :max="10000"
+        :step="10"
+        placeholder="不填则不限制"
+        controls-position="right"
+      />
+      <span class="tip-text">活动期间每人最多邀请人数(留空表示不限)</span>
+    </el-form-item>
+
+    <!-- 首单要求 -->
+    <el-form-item label="首单要求">
+      <el-switch
+        v-model="localData.requireFirstOrder"
+        :active-value="1"
+        :inactive-value="0"
+        active-text="要求完成首单"
+        inactive-text="无要求"
+      />
+    </el-form-item>
+
+    <!-- 首单最低金额(条件显示) -->
+    <el-form-item
+      v-if="localData.requireFirstOrder === 1"
+      label="首单最低金额"
+    >
+      <el-input-number
+        v-model="localData.minOrderAmount"
+        :min="0"
+        :precision="2"
+        :step="10"
+        placeholder="不填则不限金额"
+        controls-position="right"
+      />
+      <span class="tip-text">被邀请人首单需达到的最低消费金额(元)</span>
+    </el-form-item>
+
+    <!-- 预览区域 -->
+    <el-divider content-position="left">
+      <span class="divider-text">配置预览</span>
+    </el-divider>
+    <div class="preview-box">
+      <div class="preview-title">当前配置摘要:</div>
+      <ul class="preview-list">
+        <li>
+          <strong>邀请人奖励:</strong>
+          {{ getInviterCouponName() || "未配置" }}
+        </li>
+        <li>
+          <strong>被邀请人奖励:</strong>
+          {{ getInviteeCouponName() || "未配置" }}
+        </li>
+        <li>
+          <strong>每日限制:</strong>
+          {{ localData.dailyLimit || 10 }} 次/天
+        </li>
+        <li>
+          <strong>首单要求:</strong>
+          {{
+            localData.requireFirstOrder === 1
+              ? `需完成${localData.minOrderAmount || 0}元以上首单`
+              : "无"
+          }}
+        </li>
+      </ul>
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from "vue";
+
+/** 优惠券模板数据结构 */
+interface CouponTemplate {
+  id: number;
+  name: string;
+  discountValue: number;
+  [key: string]: any;
+}
+
+/** 邀请奖励配置数据结构 */
+interface InviteRewardConfig {
+  rewardRuleType: string;
+  inviterCouponTemplateId: number | null;
+  inviteeCouponTemplateId: number | null;
+  dailyLimit: number | null;
+  totalLimit: number | null;
+  requireFirstOrder: number;
+  minOrderAmount: number | null;
+}
+
+const props = withDefaults(
+  defineProps<{
+    modelValue?: InviteRewardConfig;
+    couponTemplateList?: CouponTemplate[];
+  }>(),
+  {
+    modelValue: () => ({
+      rewardRuleType: "fixed",
+      inviterCouponTemplateId: null,
+      inviteeCouponTemplateId: null,
+      dailyLimit: 10,
+      totalLimit: null,
+      requireFirstOrder: 1,
+      minOrderAmount: null
+    }),
+    couponTemplateList: () => []
+  }
+);
+
+const emit = defineEmits<{
+  (e: "update:modelValue", value: InviteRewardConfig): void;
+}>();
+
+/** 本地数据副本 */
+const localData = ref<InviteRewardConfig>({
+  rewardRuleType: "fixed",
+  inviterCouponTemplateId: null,
+  inviteeCouponTemplateId: null,
+  dailyLimit: 10,
+  totalLimit: null,
+  requireFirstOrder: 1,
+  minOrderAmount: null
+});
+
+/** 监听 props 变化同步到 localData */
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal) {
+      Object.assign(localData.value, newVal);
+    }
+  },
+  { immediate: true, deep: true }
+);
+
+/** 监听 localData 变化通知父组件 */
+watch(
+  localData,
+  (newVal) => {
+    emit("update:modelValue", { ...newVal });
+  },
+  { deep: true }
+);
+
+/**
+ * 获取邀请人优惠券名称
+ * @returns 优惠券显示名称
+ */
+function getInviterCouponName(): string {
+  if (!localData.value.inviterCouponTemplateId) return "";
+  const coupon = props.couponTemplateList.find(
+    (item) => item.id === localData.value.inviterCouponTemplateId
+  );
+  return coupon ? `${coupon.name} (${coupon.discountValue}元)` : "";
+}
+
+/**
+ * 获取被邀请人优惠券名称
+ * @returns 优惠券显示名称
+ */
+function getInviteeCouponName(): string {
+  if (!localData.value.inviteeCouponTemplateId) return "";
+  const coupon = props.couponTemplateList.find(
+    (item) => item.id === localData.value.inviteeCouponTemplateId
+  );
+  return coupon ? `${coupon.name} (${coupon.discountValue}元)` : "";
+}
+
+/** 暴露方法供父组件调用 */
+defineExpose({
+  /** 获取当前配置数据 */
+  getConfig: (): InviteRewardConfig => ({ ...localData.value }),
+  /** 重置为默认值 */
+  reset: () => {
+    localData.value = {
+      rewardRuleType: "fixed",
+      inviterCouponTemplateId: null,
+      inviteeCouponTemplateId: null,
+      dailyLimit: 10,
+      totalLimit: null,
+      requireFirstOrder: 1,
+      minOrderAmount: null
+    };
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.invite-reward-block {
+  margin-bottom: 20px;
+  border-color: #f56c6c;
+
+  :deep(.el-card__header) {
+    padding: 12px 20px;
+    border-bottom: 1px solid #f56c6c20;
+    background: #fef0f010;
+  }
+
+  :deep(.el-card__body) {
+    padding: 20px;
+  }
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  .header-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+  }
+}
+
+.tip-text {
+  display: block;
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.divider-text {
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.preview-box {
+  background: #fdf6ec;
+  padding: 15px 20px;
+  border-radius: 4px;
+  border: 1px solid #faecd8;
+
+  .preview-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #e6a23c;
+    margin-bottom: 10px;
+  }
+
+  .preview-list {
+    margin: 0;
+    padding-left: 20px;
+    list-style: disc;
+
+    li {
+      line-height: 2;
+      font-size: 13px;
+      color: #606266;
+
+      strong {
+        color: #303133;
+        min-width: 90px;
+        display: inline-block;
+      }
+    }
+  }
+}
+
+/* 表单项间距调整 */
+:deep(.el-form-item) {
+  margin-bottom: 18px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+/* 分隔线样式调整 */
+:deep(.el-divider) {
+  margin: 24px 0 18px;
+
+  .el-divider__text {
+    background-color: transparent;
+    padding: 0 10px;
+  }
+}
+
+/* 输入框数字控件宽度 */
+:deep(.el-input-number) {
+  width: 200px;
+}
+
+/* 开关控件对齐 */
+:deep(.el-switch) {
+  --el-switch-on-color: #f56c6c;
+}
+</style>

+ 274 - 58
haha-admin-web/src/views/marketing/activity/utils/hook.tsx

@@ -36,6 +36,7 @@ import {
 import ShopSelector from "../../components/ShopSelector.vue";
 import DeviceSelector from "../../components/DeviceSelector.vue";
 import ProductSelector from "../../components/ProductSelector.vue";
+import InviteRewardBlock from "./components/InviteRewardBlock.vue";
 import type { FormItemProps } from "./types";
 
 interface SearchFormProps {
@@ -54,10 +55,19 @@ export function useActivity() {
   const dataList = ref([]);
   const pagination = reactive<PaginationProps>(initPagination());
 
+  // ===== 优惠券模板列表(用于邀请活动选择器)=====
+  // TODO: 后续替换为 getCouponTemplateList() API 调用
+  const couponTemplateList = ref([
+    { id: 1, name: "满100减20元券", discountValue: 20 },
+    { id: 2, name: "满200减50元券", discountValue: 50 },
+    { id: 3, name: "新人专享10元券", discountValue: 10 }
+  ]);
+
   const typeMap: Record<number, { text: string; type: string; color: string }> = {
     1: { text: "首单立减", type: "primary", color: "#409EFF" },
     2: { text: "优惠折扣", type: "success", color: "#67C23A" },
-    3: { text: "商品满减", type: "warning", color: "#E6A23C" }
+    3: { text: "商品满减", type: "warning", color: "#E6A23C" },
+    4: { text: "邀请有礼", type: "danger", color: "#F56C6C" }
   };
 
   const statusMap: Record<number, { text: string; type: string }> = {
@@ -106,14 +116,26 @@ export function useActivity() {
     {
       label: "优惠规则",
       prop: "discountValue",
-      minWidth: 120,
-      formatter: ({ activityType, discountValue, minAmount }) => {
-        if (activityType === 1) {
-          return `首单立减 ¥${discountValue || 0}`;
-        } else if (activityType === 2) {
-          return `${((discountValue || 0) * 10).toFixed(1)}折优惠`;
-        } else if (activityType === 3) {
-          return `满${minAmount || 0}减${discountValue || 0}`;
+      minWidth: 180,
+      cellRenderer: ({ row }) => {
+        if (row.activityType === 4) {
+          return (
+            <div>
+              <div style={{ color: "#F56C6C", fontWeight: 600, marginBottom: "4px" }}>
+                双向优惠券奖励
+              </div>
+              <div style={{ fontSize: "12px", color: "#666", lineHeight: "1.6" }}>
+                <div>邀请人: {row.inviterCouponName || "未配置"}</div>
+                <div>被邀请人: {row.inviteeCouponName || "未配置"}</div>
+              </div>
+            </div>
+          );
+        } else if (row.activityType === 1) {
+          return `首单立减 ¥${row.discountValue || 0}`;
+        } else if (row.activityType === 2) {
+          return `${((row.discountValue || 0) * 10).toFixed(1)}折优惠`;
+        } else if (row.activityType === 3) {
+          return `满${row.minAmount || 0}减${row.discountValue || 0}`;
         }
         return "-";
       }
@@ -217,9 +239,11 @@ export function useActivity() {
   }
 
   function handleViewDetail(row) {
+    const isInviteActivity = row.activityType === 4;
+
     addDialog({
-      title: "活动详情",
-      width: "800px",
+      title: isInviteActivity ? "邀请活动详情" : "活动详情",
+      width: isInviteActivity ? "900px" : "800px",
       draggable: true,
       fullscreen: deviceDetection(),
       contentRenderer: () => (
@@ -233,6 +257,17 @@ export function useActivity() {
               {row.activityType === 1 && `首单立减 ¥${row.discountValue}`}
               {row.activityType === 2 && `${(row.discountValue * 10).toFixed(1)}折优惠`}
               {row.activityType === 3 && `满${row.minAmount}减${row.discountValue}`}
+              {row.activityType === 4 && (
+                <div>
+                  <div style={{ color: "#F56C6C", fontWeight: 600, marginBottom: "8px" }}>
+                    双向优惠券奖励
+                  </div>
+                  <div style={{ fontSize: "13px", lineHeight: "2" }}>
+                    <div><strong>邀请人奖励:</strong> {row.inviterCouponName || "未配置"}</div>
+                    <div><strong>被邀请人奖励:</strong> {row.inviteeCouponName || "未配置"}</div>
+                  </div>
+                </div>
+              )}
             </ElDescriptionsItem>
             <ElDescriptionsItem label="最大优惠">
               {row.maxDiscount ? `¥${row.maxDiscount}` : "不限制"}
@@ -248,16 +283,39 @@ export function useActivity() {
             <ElDescriptionsItem label="适用范围">
               <ElTag type={applyScopeMap[row.applyScope]?.type}>{applyScopeMap[row.applyScope]?.text}</ElTag>
             </ElDescriptionsItem>
-            <ElDescriptionsItem label="设备范围">
-              <ElTag type={deviceScopeMap[row.deviceScope]?.type}>{deviceScopeMap[row.deviceScope]?.text}</ElTag>
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="商品范围">
-              <ElTag type={productScopeMap[row.productScope]?.type}>{productScopeMap[row.productScope]?.text}</ElTag>
-            </ElDescriptionsItem>
+            {!isInviteActivity && (
+              <>
+                <ElDescriptionsItem label="设备范围">
+                  <ElTag type={deviceScopeMap[row.deviceScope]?.type}>{deviceScopeMap[row.deviceScope]?.text}</ElTag>
+                </ElDescriptionsItem>
+                <ElDescriptionsItem label="商品范围">
+                  <ElTag type={productScopeMap[row.productScope]?.type}>{productScopeMap[row.productScope]?.text}</ElTag>
+                </ElDescriptionsItem>
+              </>
+            )}
+            {isInviteActivity && (
+              <>
+                <ElDescriptionsItem label="每日邀请上限">
+                  {row.dailyLimit || 10} 次/天
+                </ElDescriptionsItem>
+                <ElDescriptionsItem label="总邀请上限">
+                  {row.totalLimit ? `${row.totalLimit} 人` : "不限制"}
+                </ElDescriptionsItem>
+                <ElDescriptionsItem label="首单要求">
+                  {row.requireFirstOrder === 1
+                    ? `需完成${row.minOrderAmount ? `¥${row.minOrderAmount}以上` : ""}首单`
+                    : "无要求"}
+                </ElDescriptionsItem>
+                <ElDescriptionsItem label="邀请码前缀">
+                  {row.inviteCodePrefix || "INV"}
+                </ElDescriptionsItem>
+              </>
+            )}
             <ElDescriptionsItem label="创建时间">{dayjs(row.createTime).format("YYYY-MM-DD HH:mm:ss")}</ElDescriptionsItem>
             <ElDescriptionsItem label="活动说明" span={2}>{row.activityDesc || "暂无说明"}</ElDescriptionsItem>
           </ElDescriptions>
-          
+
+          {/* ===== 通用统计数据 ===== */}
           <div class="mt-4">
             <h4 class="mb-2">统计数据</h4>
             <ElDescriptions column={3}>
@@ -266,6 +324,85 @@ export function useActivity() {
               <ElDescriptionsItem label="总营收">¥{row.totalRevenue || 0}</ElDescriptionsItem>
             </ElDescriptions>
           </div>
+
+          {/* ===== 邀请活动专属:邀请数据统计 + 查看邀请记录入口 (Task 10) ===== */}
+          {isInviteActivity && (
+            <div class="mt-4">
+              <div style={{
+                background: "linear-gradient(135deg, #fef0f0 0%, #fde8e8 100%)",
+                borderRadius: "8px",
+                padding: "16px 20px",
+                border: "1px solid #f56c6c30"
+              }}>
+                <h4 style={{
+                  margin: "0 0 12px 0",
+                  fontSize: "15px",
+                  fontWeight: 600,
+                  color: "#F56C6C",
+                  display: "flex",
+                  alignItems: "center",
+                  gap: "6px"
+                }}>
+                  📊 邀请数据统计
+                </h4>
+                <ElDescriptions column={4} border style={{
+                  background: "#fff",
+                  borderRadius: "6px"
+                }}>
+                  <ElDescriptionsItem label="总邀请数">
+                    <span style={{ fontWeight: 600, color: "#409EFF", fontSize: "16px" }}>
+                      {row.totalInviteCount || 0}
+                    </span>
+                  </ElDescriptionsItem>
+                  <ElDescriptionsItem label="有效邀请">
+                    <span style={{ fontWeight: 600, color: "#67C23A", fontSize: "16px" }}>
+                      {row.validInviteCount || 0}
+                    </span>
+                  </ElDescriptionsItem>
+                  <ElDescriptionsItem label="奖励发放">
+                    <span style={{ fontWeight: 600, color: "#E6A23C", fontSize: "16px" }}>
+                      {row.rewardCount || 0}
+                    </span>
+                  </ElDescriptionsItem>
+                  <ElDescriptionsItem label="转化率">
+                    <span style={{ fontWeight: 600, color: "#909399", fontSize: "14px" }}>
+                      {row.totalInviteCount > 0
+                        ? ((row.validInviteCount || 0) / row.totalInviteCount * 100).toFixed(1) + "%"
+                        : "-"}
+                    </span>
+                  </ElDescriptionsItem>
+                </ElDescriptions>
+
+                {/* 查看邀请记录按钮/链接 */}
+                <div style={{
+                  marginTop: "16px",
+                  textAlign: "center",
+                  paddingTop: "12px",
+                  borderTop: "1px dashed #f56c6c40"
+                }}>
+                  <a
+                    href="#/marketing/invite/records"
+                    target="_blank"
+                    style={{
+                      color: "#F56C6C",
+                      textDecoration: "none",
+                      fontSize: "14px",
+                      fontWeight: 500,
+                      display: "inline-flex",
+                      alignItems: "center",
+                      gap: "4px",
+                      padding: "6px 16px",
+                      borderRadius: "4px",
+                      background: "#fef0f0",
+                      border: "1px solid #f56c6c30"
+                    }}
+                  >
+                    📋 查看完整邀请记录与数据统计 →
+                  </a>
+                </div>
+              </div>
+            </div>
+          )}
         </div>
       )
     });
@@ -483,13 +620,13 @@ export function useActivity() {
     return (
       <div class="activity-form">
         <style>{formStyles}</style>
-        
-        {/* 活动类型选择 */}
+
+        {/* ===== 通用区块:活动类型选择 ===== */}
         <div class="form-block">
           <div class="block-title">活动类型</div>
           <div class="type-selector">
-            {[1, 2, 3].map(type => (
-              <div 
+            {[1, 2, 3, 4].map(type => (
+              <div
                 class={`type-btn ${formData.activityType === type ? 'active' : ''}`}
                 onClick={() => { formData.activityType = type; }}
               >
@@ -498,15 +635,16 @@ export function useActivity() {
                   {type === 1 && "新用户首单优惠"}
                   {type === 2 && "全场折扣优惠"}
                   {type === 3 && "满额减免优惠"}
+                  {type === 4 && "邀请好友双向奖励"}
                 </div>
               </div>
             ))}
           </div>
         </div>
 
-        {/* 基本信息 */}
+        {/* ===== 通用区块:基本信息 ===== */}
         <div class="form-block">
-          <div class="block-title">基本信息</div>
+          <div class="block-title">📝 基本信息</div>
           <ElForm label-width="80px" size="default">
             <div class="form-grid-2">
               <ElFormItem label="活动名称" required>
@@ -526,12 +664,13 @@ export function useActivity() {
           </ElForm>
         </div>
 
-        {/* 优惠规则 */}
+        {/* ===== 动态区块:根据 activityType 渲染不同配置 ===== */}
         <div class="form-block">
           <div class="block-title">优惠规则</div>
-          
+
           {!formData.activityType && <div class="empty-tip">请先选择活动类型</div>}
-          
+
+          {/* 首单立减配置 (type=1) */}
           {formData.activityType === 1 && (
             <div>
               <ElForm label-width="80px" size="default">
@@ -546,6 +685,7 @@ export function useActivity() {
             </div>
           )}
 
+          {/* 优惠折扣配置 (type=2) */}
           {formData.activityType === 2 && (
             <div>
               <ElForm label-width="80px" size="default">
@@ -565,6 +705,7 @@ export function useActivity() {
             </div>
           )}
 
+          {/* 商品满减配置 (type=3) */}
           {formData.activityType === 3 && (
             <div>
               <ElForm label-width="80px" size="default">
@@ -583,11 +724,19 @@ export function useActivity() {
               </div>
             </div>
           )}
+
+          {/* 邀请有礼配置 (type=4) - 使用 InviteRewardBlock 组件 */}
+          {formData.activityType === 4 && (
+            <InviteRewardBlock
+              v-model={formData}
+              couponTemplateList={couponTemplateList.value}
+            />
+          )}
         </div>
 
-        {/* 适用范围 */}
+        {/* ===== 通用区块:适用范围 ===== */}
         <div class="form-block">
-          <div class="block-title">适用范围</div>
+          <div class="block-title">🎯 适用范围</div>
           <div class="scope-grid">
             <div class="scope-card">
               <div class="card-header">
@@ -628,7 +777,7 @@ export function useActivity() {
           </div>
         </div>
 
-        {/* 活动说明 */}
+        {/* ===== 通用区块:活动说明 ===== */}
         <div class="form-block">
           <div class="block-title">活动说明</div>
           <ElForm size="default">
@@ -642,6 +791,7 @@ export function useActivity() {
   }
 
   function validateForm(formData: FormItemProps): boolean {
+    // ===== 通用校验 =====
     if (!formData.activityName) {
       message("请输入活动名称", { type: "warning" });
       return false;
@@ -658,14 +808,35 @@ export function useActivity() {
       message("请选择结束时间", { type: "warning" });
       return false;
     }
-    if (!formData.discountValue || formData.discountValue <= 0) {
-      message("请输入有效的优惠值", { type: "warning" });
-      return false;
-    }
-    if (formData.activityType === 3 && (!formData.minAmount || formData.minAmount <= 0)) {
-      message("请输入有效的满减门槛", { type: "warning" });
-      return false;
+
+    // ===== 根据活动类型进行特定校验 =====
+    // 邀请活动特有校验 (type=4)
+    if (formData.activityType === 4) {
+      if (!formData.inviterCouponTemplateId) {
+        message("请选择邀请人优惠券模板", { type: "warning" });
+        return false;
+      }
+      if (formData.dailyLimit && (formData.dailyLimit < 1 || formData.dailyLimit > 100)) {
+        message("每日邀请上限应在 1-100 之间", { type: "warning" });
+        return false;
+      }
+      if (formData.totalLimit && (formData.totalLimit < 1 || formData.totalLimit > 10000)) {
+        message("总邀请上限应在 1-10000 之间", { type: "warning" });
+        return false;
+      }
+    } else {
+      // 其他类型活动的优惠值校验
+      if (!formData.discountValue || formData.discountValue <= 0) {
+        message("请输入有效的优惠值", { type: "warning" });
+        return false;
+      }
+      if (formData.activityType === 3 && (!formData.minAmount || formData.minAmount <= 0)) {
+        message("请输入有效的满减门槛", { type: "warning" });
+        return false;
+      }
     }
+
+    // ===== 适用范围校验 =====
     if (formData.applyScope === 2 && (!formData.shopIds || formData.shopIds.length === 0)) {
       message("请选择至少一个门店", { type: "warning" });
       return false;
@@ -678,6 +849,7 @@ export function useActivity() {
       message("请选择至少一个商品", { type: "warning" });
       return false;
     }
+
     return true;
   }
 
@@ -699,12 +871,24 @@ export function useActivity() {
       totalBudget: row.totalBudget,
       shopIds: row.shopIds || [],
       deviceIds: row.deviceIds || [],
-      productIds: row.productIds || []
+      productIds: row.productIds || [],
+
+      // 邀请活动特有字段的回显 (type=4)
+      ...(row.activityType === 4 ? {
+        inviterCouponTemplateId: row.inviterCouponTemplateId || row.inviterCouponId || null,
+        inviteeCouponTemplateId: row.inviteeCouponTemplateId || row.inviteeCouponId || null,
+        dailyLimit: row.dailyLimit || 10,
+        totalLimit: row.totalLimit || null,
+        requireFirstOrder: row.requireFirstOrder ?? 1,
+        minOrderAmount: row.minOrderAmount || null,
+        rewardRuleType: "fixed",
+        inviteCodePrefix: row.inviteCodePrefix || "INV"
+      } : {})
     });
 
     addDialog({
-      title: "编辑活动",
-      width: "1100px",
+      title: `编辑${typeMap[row.activityType]?.text || ''}活动`,
+      width: row.activityType === 4 ? "900px" : "700px",  // 邀请表单更宽
       draggable: true,
       fullscreen: deviceDetection(),
       contentRenderer: () => renderForm(formData),
@@ -724,29 +908,59 @@ export function useActivity() {
     });
   }
 
+  // ===== 默认表单数据 =====
+  const defaultFormData = {
+    activityName: "",
+    activityType: undefined as number | undefined,
+    activityDesc: "",
+    startTime: "",
+    endTime: "",
+    status: 0,
+    discountValue: null as number | null,
+    minAmount: null as number | null,
+    maxDiscount: null as number | null,
+    applyScope: 1,
+    deviceScope: 1,
+    productScope: 1,
+    totalBudget: null as number | null,
+    shopIds: [] as number[],
+    deviceIds: [] as string[],
+    productIds: [] as number[],
+    // 邀请有礼特有字段 (type=4)
+    rewardRuleType: "fixed",
+    inviterCouponTemplateId: null as number | null,
+    inviteeCouponTemplateId: null as number | null,
+    dailyLimit: 10,
+    totalLimit: null as number | null,
+    requireFirstOrder: 1,
+    minOrderAmount: null as number | null,
+    inviteCodePrefix: "INV"
+  };
+
+  // 当前选中的活动类型(用于新增时预选)
+  const selectedType = ref<number | undefined>(undefined);
+
   function handleAdd() {
+    if (!selectedType.value) {
+      message("请先选择活动类型", { type: "warning" });
+      return;
+    }
+
     const formData = reactive<FormItemProps>({
-      activityName: "",
-      activityType: undefined,
-      activityDesc: "",
-      startTime: "",
-      endTime: "",
-      status: 0,
-      discountValue: null,
-      minAmount: null,
-      maxDiscount: null,
-      applyScope: 1,
-      deviceScope: 1,
-      productScope: 1,
-      totalBudget: null,
-      shopIds: [],
-      deviceIds: [],
-      productIds: []
+      ...defaultFormData,
+      activityType: selectedType.value,
+
+      // 根据类型设置不同的默认值
+      ...(selectedType.value === 4 ? {
+        rewardRuleType: "fixed",
+        dailyLimit: 10,
+        requireFirstOrder: 1
+      } : {})
     });
 
     addDialog({
-      title: "新增活动",
-      width: "1100px",
+      title: `新增${typeMap[selectedType.value]?.text || ''}活动`,
+      width: selectedType.value === 4 ? "900px" : "700px",  // 邀请表单更宽
       draggable: true,
       fullscreen: deviceDetection(),
       contentRenderer: () => renderForm(formData),
@@ -789,6 +1003,8 @@ export function useActivity() {
     applyScopeMap,
     deviceScopeMap,
     productScopeMap,
+    selectedType,           // 当前选中的活动类型
+    couponTemplateList,    // 优惠券模板列表
     onSearch,
     resetForm,
     handleAdd,

+ 18 - 0
haha-admin-web/src/views/marketing/activity/utils/types.ts

@@ -16,6 +16,19 @@ interface FormItemProps {
   shopIds?: number[];
   deviceIds?: string[];
   productIds?: number[];
+  // 邀请有礼活动特有字段 (type=4)
+  inviterCouponName?: string;      // 邀请人优惠券名称
+  inviteeCouponName?: string;      // 被邀请人优惠券名称
+  inviterCouponId?: number;        // 邀请人优惠券ID
+  inviteeCouponId?: number;        // 被邀请人优惠券ID
+  rewardRuleType?: string;         // 奖励规则类型:fixed 固定奖励
+  inviterCouponTemplateId?: number | null;  // 邀请人优惠券模板ID
+  inviteeCouponTemplateId?: number | null;  // 被邀请人优惠券模板ID
+  dailyLimit?: number | null;              // 每日邀请上限
+  totalLimit?: number | null;              // 总邀请上限
+  requireFirstOrder?: number;             // 要求首单:1-是 0-否
+  minOrderAmount?: number | null;          // 首单最低金额
+  inviteCodePrefix?: string;              // 邀请码前缀
 }
 
 interface SearchFormProps {
@@ -55,6 +68,11 @@ interface ActivityDetail {
   shopIds: number[];
   deviceIds: string[];
   productIds: number[];
+  // 邀请有礼活动特有字段
+  inviterCouponName?: string;      // 邀请人优惠券名称
+  inviteeCouponName?: string;      // 被邀请人优惠券名称
+  inviterCouponId?: number;        // 邀请人优惠券ID
+  inviteeCouponId?: number;        // 被邀请人优惠券ID
 }
 
 export type { FormItemProps, SearchFormProps, ActivityDetail };

+ 238 - 0
haha-admin/src/main/java/com/haha/admin/controller/UnifiedActivityController.java

@@ -0,0 +1,238 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.InviteRecord;
+import com.haha.entity.dto.InviteRecordQueryDTO;
+import com.haha.entity.dto.UnifiedActivityCreateDTO;
+import com.haha.entity.dto.UnifiedActivityQueryDTO;
+import com.haha.entity.dto.UnifiedActivityUpdateDTO;
+import com.haha.entity.vo.UnifiedActivityVO;
+import com.haha.service.UnifiedActivityService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 统一活动管理控制器
+ * 提供统一的 RESTful API 接口,支持所有类型活动的 CRUD 操作
+ * 内部调用 UnifiedActivityService 调度器,根据活动类型自动分发到对应的具体服务实现
+ *
+ * <p>支持的活动类型:
+ * <ul>
+ *   <li>1 - 首单立减(营销活动)</li>
+ *   <li>2 - 优惠折扣(营销活动)</li>
+ *   <li>3 - 商品满减(营销活动)</li>
+ *   <li>4 - 邀请有礼(邀请活动)</li>
+ * </ul>
+ *
+ * <p>API 基础路径:/marketing/activities/unified
+ */
+@Slf4j
+@RestController
+@RequestMapping("/marketing/activities/unified")
+@RequiredArgsConstructor
+public class UnifiedActivityController {
+
+    private final UnifiedActivityService unifiedActivityService;
+
+    // ==================== 查询接口 ====================
+
+    /**
+     * 统一分页查询所有类型活动(包括邀请有礼)
+     * 支持按 activityType 筛选特定类型的活动,或查询全部活动
+     *
+     * @param queryDTO 查询条件(包含分页参数和筛选条件)
+     * @return 分页结果,统一转换为 UnifiedActivityVO 格式
+     */
+    @RequirePermission("marketing:activity:list")
+    @GetMapping("/page")
+    public Result<PageResult<UnifiedActivityVO>> getPage(UnifiedActivityQueryDTO queryDTO) {
+        queryDTO.validate();
+        log.info("[统一活动] 分页查询 - type: {}, page: {}", queryDTO.getActivityType(), queryDTO.getPage());
+        IPage<UnifiedActivityVO> page = unifiedActivityService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    /**
+     * 统一详情查询
+     * 根据 type 参数从对应的服务获取详情并转换为统一格式
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     * @return 活动详情(统一VO格式)
+     */
+    @RequirePermission("marketing:activity:read")
+    @GetMapping("/{id}")
+    public Result<UnifiedActivityVO> getDetail(@PathVariable Long id,
+                                               @RequestParam Integer type) {
+        log.info("[统一活动] 详情 - id: {}, type: {}", id, type);
+        UnifiedActivityVO detail = unifiedActivityService.getDetail(id, type);
+        return Result.success(detail);
+    }
+
+    // ==================== 创建和更新接口 ====================
+
+    /**
+     * 统一创建活动(自动根据 type 分发)
+     * 根据 activityType 自动路由到 MarketingActivityService 或 InviteActivityService
+     *
+     * @param dto 统一创建请求DTO(包含 activityType 字段用于类型判断)
+     * @return 创建成功后的活动ID
+     */
+    @RequirePermission("marketing:activity:create")
+    @Log(module = "统一活动", operation = OperationType.INSERT, summary = "创建活动")
+    @PostMapping
+    public Result<Long> create(@RequestBody UnifiedActivityCreateDTO dto) {
+        // 根据类型进行分组校验
+        if (dto.getActivityType() == 4) {
+            // 邀请活动的特殊校验
+            if (dto.getInviterCouponTemplateId() == null) {
+                return Result.error("邀请人优惠券模板不能为空");
+            }
+            if (dto.getDailyLimit() == null) {
+                return Result.error("每日邀请上限不能为空");
+            }
+        }
+
+        log.info("[统一活动] 创建 - type: {}, name: {}", dto.getActivityType(), dto.getActivityName());
+        Long id = unifiedActivityService.create(dto);
+        return Result.success("创建成功", id);
+    }
+
+    /**
+     * 统一更新活动
+     * 根据 type 参数自动分发到对应的服务
+     *
+     * @param id  活动ID
+     * @param type 活动类型(1-4)
+     * @param dto 统一更新请求DTO
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:activity:edit")
+    @Log(module = "统一活动", operation = OperationType.UPDATE, summary = "编辑活动")
+    @PutMapping("/{id}")
+    public Result<Void> update(@PathVariable Long id,
+                               @RequestParam Integer type,
+                               @RequestBody UnifiedActivityUpdateDTO dto) {
+        dto.setId(id);
+        log.info("[统一活动] 更新 - id: {}, type: {}", id, type);
+        unifiedActivityService.update(dto);
+        return Result.success("更新成功");
+    }
+
+    // ==================== 状态管理接口 ====================
+
+    /**
+     * 发布活动
+     * 将活动从草稿状态变更为已发布/进行中状态
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:activity:publish")
+    @Log(module = "统一活动", operation = OperationType.UPDATE, summary = "发布活动")
+    @PutMapping("/{id}/publish")
+    public Result<Void> publish(@PathVariable Long id,
+                                @RequestParam Integer type) {
+        log.info("[统一活动] 发布 - id: {}, type: {}", id, type);
+        unifiedActivityService.publish(id, type);
+        return Result.success("发布成功");
+    }
+
+    /**
+     * 暂停活动
+     * 将活动从进行中状态变更为已暂停状态
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:activity:edit")
+    @Log(module = "统一活动", operation = OperationType.UPDATE, summary = "暂停活动")
+    @PutMapping("/{id}/pause")
+    public Result<Void> pause(@PathVariable Long id,
+                              @RequestParam Integer type) {
+        log.info("[统一活动] 暂停 - id: {}, type: {}", id, type);
+        unifiedActivityService.pause(id, type);
+        return Result.success("暂停成功");
+    }
+
+    /**
+     * 恢复活动
+     * 将活动从暂停状态恢复为进行中状态
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:activity:edit")
+    @Log(module = "统一活动", operation = OperationType.UPDATE, summary = "恢复活动")
+    @PutMapping("/{id}/resume")
+    public Result<Void> resume(@PathVariable Long id,
+                               @RequestParam Integer type) {
+        log.info("[统一活动] 恢复 - id: {}, type: {}", id, type);
+        unifiedActivityService.resume(id, type);
+        return Result.success("恢复成功");
+    }
+
+    /**
+     * 删除活动(逻辑删除)
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:activity:delete")
+    @Log(module = "统一活动", operation = OperationType.DELETE, summary = "删除活动")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id,
+                               @RequestParam Integer type) {
+        log.warn("[统一活动] 删除 - id: {}, type: {}", id, type);
+        unifiedActivityService.delete(id, type);
+        return Result.success("删除成功");
+    }
+
+    // ==================== 邀请活动特有接口 (type=4) ====================
+
+    /**
+     * 查询邀请记录(仅 type=4 时调用)
+     * 获取指定邀请活动的邀请记录详情
+     *
+     * @param activityId 邀请活动ID
+     * @param queryDTO   查询条件(包含分页参数和筛选条件)
+     * @return 邀请记录分页结果
+     */
+    @RequirePermission("marketing:activity:read")
+    @GetMapping("/{activityId}/invite-records")
+    public Result<PageResult<InviteRecord>> getInviteRecords(
+            @PathVariable Long activityId,
+            InviteRecordQueryDTO queryDTO) {
+        log.info("[统一活动] 查询邀请记录 - activityId: {}, page: {}",
+                activityId, queryDTO.getPage());
+        IPage<InviteRecord> page = unifiedActivityService.getInviteRecords(activityId, queryDTO);
+        return Result.success(PageResult.of(page));
+    }
+
+    /**
+     * 获取邀请统计数据(仅 type=4 时调用)
+     * 包含总邀请数、有效邀请数、今日邀请数、奖励发放统计等
+     *
+     * @param activityId 邀请活动ID
+     * @return 统计数据Map
+     */
+    @RequirePermission("marketing:activity:read")
+    @GetMapping("/{activityId}/invite-statistics")
+    public Result<Map<String, Object>> getInviteStatistics(@PathVariable Long activityId) {
+        log.info("[统一活动] 查询邀请统计 - activityId: {}", activityId);
+        Map<String, Object> stats = unifiedActivityService.getInviteStatistics(activityId);
+        return Result.success(stats);
+    }
+}

+ 78 - 105
haha-admin/src/main/java/com/haha/admin/task/InviteTask.java

@@ -1,23 +1,24 @@
 package com.haha.admin.task;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.haha.entity.InviteActivity;
-import com.haha.entity.InviteReward;
-import com.haha.mapper.InviteActivityMapper;
-import com.haha.mapper.InviteRewardMapper;
 import com.haha.service.InviteActivityService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.jdbc.CannotGetJdbcConnectionException;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * 邀请活动定时任务
  * 负责自动更新活动状态、重试失败奖励、数据统计汇总等定时任务
+ * 
+ * 设计说明:所有数据库操作均通过 Service 层完成(而非直接调用 Mapper),
+ * 以确保事务管理一致性,并遵循项目规范(参考 MarketingTask、CouponExpireTask)
  */
 @Slf4j
 @Component
@@ -25,163 +26,135 @@ import java.util.Map;
 public class InviteTask {
 
     private final InviteActivityService inviteActivityService;
-    private final InviteActivityMapper inviteActivityMapper;
-    private final InviteRewardMapper inviteRewardMapper;
-
-    /**
-     * 邀请活动状态自动更新任务
-     * 每分钟执行一次,检查活动的开始/结束时间,自动更新状态
-     * - 草稿/已发布 + 开始时间到了 → 进行中
-     * - 进行中 + 结束时间到了 → 已结束
-     */
+
+    private static final AtomicInteger consecutiveConnectionFailures = new AtomicInteger(0);
+    private static final int MAX_CONSECUTIVE_FAILURES = 5;
+
     @Scheduled(cron = "0 * * * * ?")
     public void updateActivityStatus() {
+        if (!checkConnectionHealth()) {
+            return;
+        }
         log.info("[邀请定时任务] 开始执行活动状态更新任务");
         try {
-            LocalDateTime now = LocalDateTime.now();
-
-            // 1. 将应该开始的活动状态更新为"进行中"
-            List<InviteActivity> shouldStart = inviteActivityMapper.selectList(
-                new LambdaQueryWrapper<InviteActivity>()
-                    .in(InviteActivity::getStatus, 0, 1) // 草稿或已发布
-                    .le(InviteActivity::getStartTime, now)
-                    .ge(InviteActivity::getEndTime, now)
-                    .eq(InviteActivity::getDeleted, 0)
-            );
-
-            for (InviteActivity activity : shouldStart) {
-                inviteActivityMapper.updateStatus(activity.getId(), 2); // 进行中
-                log.info("[邀请定时任务] 活动{}已自动开始", activity.getActivityName());
-            }
-
-            // 2. 将应该结束的活动状态更新为"已结束"
-            List<InviteActivity> shouldEnd = inviteActivityMapper.selectList(
-                new LambdaQueryWrapper<InviteActivity>()
-                    .eq(InviteActivity::getStatus, 2) // 进行中
-                    .lt(InviteActivity::getEndTime, now)
-                    .eq(InviteActivity::getDeleted, 0)
-            );
-
-            for (InviteActivity activity : shouldEnd) {
-                inviteActivityMapper.updateStatus(activity.getId(), 4); // 已结束
-                log.info("[邀请定时任务] 活动{}已自动结束", activity.getActivityName());
-            }
-
+            int[] result = inviteActivityService.autoUpdateActivityStatus();
+            resetConnectionFailure();
             log.info("[邀请定时任务] 活动状态更新任务执行完成,开始{}个,结束{}个",
-                     shouldStart.size(), shouldEnd.size());
-
+                     result[0], result[1]);
+        } catch (CannotGetJdbcConnectionException e) {
+            handleConnectionFailure("活动状态更新", e);
         } catch (Exception e) {
             log.error("[邀请定时任务] 活动状态更新任务执行失败", e);
         }
     }
 
-    /**
-     * 失败奖励重试任务
-     * 每30分钟执行一次,重试发放失败的奖励(最多重试3次)
-     */
     @Scheduled(cron = "0 */30 * * * ?")
     public void retryFailedRewards() {
+        if (!checkConnectionHealth()) {
+            return;
+        }
         log.info("[邀请定时任务] 开始执行失败奖励重试任务");
         try {
-            // 查询待重试的失败奖励记录(重试次数<3)
-            List<InviteReward> failedRewards = inviteRewardMapper.selectFailedRewardsForRetry(3, 50);
-
-            int successCount = 0;
-            int failCount = 0;
-
-            for (InviteReward reward : failedRewards) {
-                try {
-                    boolean success = inviteActivityService.distributeReward(reward.getRecordId());
-                    if (success) {
-                        successCount++;
-                        log.info("[邀请定时任务] 奖励重试成功 - rewardId: {}", reward.getId());
-                    } else {
-                        failCount++;
-                        log.warn("[邀请定时任务] 奖励重试仍失败 - rewardId: {}", reward.getId());
-                    }
-                } catch (Exception e) {
-                    failCount++;
-                    log.error("[邀请定时任务] 奖励重试异常 - rewardId: {}", reward.getId(), e);
-                }
-            }
-
+            int[] result = inviteActivityService.retryFailedRewards(3, 50);
+            resetConnectionFailure();
             log.info("[邀请定时任务] 失败奖励重试任务执行完成,成功{}个,失败{}个",
-                     successCount, failCount);
-
+                     result[0], result[1]);
+        } catch (CannotGetJdbcConnectionException e) {
+            handleConnectionFailure("失败奖励重试", e);
         } catch (Exception e) {
             log.error("[邀请定时任务] 失败奖励重试任务执行失败", e);
         }
     }
 
-    /**
-     * 邀请数据汇总统计任务
-     * 每天凌晨1点执行,汇总前一天的数据到统计表(如果有的话)
-     */
     @Scheduled(cron = "0 0 1 * * ?")
     public void summarizeDailyStatistics() {
+        if (!checkConnectionHealth()) {
+            return;
+        }
         log.info("[邀请定时任务] 开始执行每日数据汇总统计任务");
         try {
             LocalDateTime yesterday = LocalDateTime.now().minusDays(1);
             LocalDateTime startOfDay = yesterday.toLocalDate().atStartOfDay();
             LocalDateTime endOfDay = yesterday.toLocalDate().atTime(23, 59, 59);
 
-            // 查询所有进行中的活动
-            List<InviteActivity> ongoingActivities = inviteActivityMapper.selectOngoing(LocalDateTime.now());
+            List<InviteActivity> ongoingActivities = inviteActivityService.getOngoingActivities();
 
             for (InviteActivity activity : ongoingActivities) {
-                Map<String, Object> stats = inviteActivityMapper.selectStatisticsByActivityId(
-                    activity.getId(), startOfDay, endOfDay
-                );
+                Map<String, Object> stats = inviteActivityService.getActivityStatistics(activity.getId());
 
                 log.info("[邀请定时任务] 活动[{}]昨日统计 - 总邀请: {}, 有效邀请: {}",
                          activity.getActivityName(),
-                         stats.get("totalInvite"),
-                         stats.get("validInvite"));
-
-                // TODO: 如果有统计表,可以在这里写入每日统计数据
+                         stats.get("totalInviteCount"),
+                         stats.get("validInviteCount"));
             }
 
+            resetConnectionFailure();
             log.info("[邀请定时任务] 每日数据汇总统计任务执行完成");
-
+        } catch (CannotGetJdbcConnectionException e) {
+            handleConnectionFailure("每日数据汇总统计", e);
         } catch (Exception e) {
             log.error("[邀请定时任务] 每日数据汇总统计任务执行失败", e);
         }
     }
 
-    /**
-     * 活动即将结束提醒任务
-     * 提前3天检查即将结束的活动,发送提醒通知给参与用户(可选)
-     */
-    @Scheduled(cron = "0 0 10 * * ?") // 每天10点执行
+    @Scheduled(cron = "0 0 10 * * ?")
     public void remindEndingActivities() {
+        if (!checkConnectionHealth()) {
+            return;
+        }
         log.info("[邀请定时任务] 开始执行活动即将结束提醒任务");
         try {
-            LocalDateTime now = LocalDateTime.now();
-            LocalDateTime threeDaysLater = now.plusDays(3);
-
-            // 查询3天内将要结束的进行中活动
-            List<InviteActivity> endingSoon = inviteActivityMapper.selectList(
-                new LambdaQueryWrapper<InviteActivity>()
-                    .eq(InviteActivity::getStatus, 2) // 进行中
-                    .between(InviteActivity::getEndTime, now, threeDaysLater)
-                    .eq(InviteActivity::getDeleted, 0)
-            );
+            List<InviteActivity> endingSoon = inviteActivityService.getEndingSoonActivities(3);
 
             for (InviteActivity activity : endingSoon) {
-                long daysLeft = java.time.Duration.between(now, activity.getEndTime()).toDays();
+                long daysLeft = java.time.Duration.between(LocalDateTime.now(), activity.getEndTime()).toDays();
                 log.info("[邀请定时任务] 活动[{}]将在{}天后结束",
                          activity.getActivityName(), daysLeft);
-
-                // TODO: 发送站内信或模板消息提醒参与用户
-                // sendReminderNotification(activity, daysLeft);
             }
 
             if (!endingSoon.isEmpty()) {
                 log.info("[邀请定时任务] 发现{}个即将结束的活动", endingSoon.size());
             }
 
+            resetConnectionFailure();
+        } catch (CannotGetJdbcConnectionException e) {
+            handleConnectionFailure("活动即将结束提醒", e);
         } catch (Exception e) {
             log.error("[邀请定时任务] 活动即将结束提醒任务执行失败", e);
         }
     }
+
+    private boolean checkConnectionHealth() {
+        int failures = consecutiveConnectionFailures.get();
+        if (failures >= MAX_CONSECUTIVE_FAILURES) {
+            if (failures == MAX_CONSECUTIVE_FAILURES) {
+                log.warn("[邀请定时任务] 数据库连续连接失败{}次,后续{}次执行将跳过以避免日志刷屏。"
+                         + "待数据库恢复后将自动重置。",
+                         MAX_CONSECUTIVE_FAILURES, MAX_CONSECUTIVE_FAILURES);
+                consecutiveConnectionFailures.incrementAndGet();
+            }
+            return false;
+        }
+        return true;
+    }
+
+    private void handleConnectionFailure(String taskName, CannotGetJdbcConnectionException e) {
+        int failures = consecutiveConnectionFailures.incrementAndGet();
+        if (failures <= MAX_CONSECUTIVE_FAILURES) {
+            log.error("[邀请定时任务] {}任务执行失败 - 数据库连接不可用 (连续第{}次)"
+                     + "\n请检查: \n1. 数据库服务器 server.kuaiyuman.cn:3306 是否可达"
+                     + "\n2. HikariCP连接池配置 (当前max=10, minIdle=3)"
+                     + "\n3. 网络连接/防火墙设置",
+                     taskName, failures, e);
+        }
+    }
+
+    private void resetConnectionFailure() {
+        if (consecutiveConnectionFailures.get() > 0) {
+            int oldVal = consecutiveConnectionFailures.getAndSet(0);
+            if (oldVal > MAX_CONSECUTIVE_FAILURES) {
+                log.info("[邀请定时任务] 数据库连接已恢复,之前连续失败{}次", oldVal);
+            }
+        }
+    }
 }

+ 19 - 0
haha-admin/src/main/resources/db/migration/V1__add_payscore_enabled_field.sql

@@ -0,0 +1,19 @@
+-- =============================================
+-- 为用户表添加微信支付分开通状态字段
+-- 版本:1.0
+-- 日期:2026-03-31
+-- 说明:添加 payscore_enabled 字段用于标识用户是否开通微信支付分
+-- =============================================
+
+-- 添加字段(如果不存在)
+ALTER TABLE `t_user` 
+ADD COLUMN IF NOT EXISTS `payscore_enabled` int(11) DEFAULT 0 COMMENT '微信支付分开通状态:0-未开通,1-已开通' AFTER `status`;
+
+-- 索引优化(可选,提升查询性能)
+-- CREATE INDEX idx_payscore_enabled ON t_user(payscore_enabled);
+
+-- 数据初始化(将已有用户设置为未开通状态)
+-- UPDATE `t_user` SET `payscore_enabled` = COALESCE(`payscore_enabled`, 0);
+
+-- 测试数据(开发环境使用)
+-- UPDATE `t_user` SET `payscore_enabled` = 1 WHERE `id` IN (1, 2, 3);

+ 151 - 0
haha-entity/src/main/java/com/haha/entity/dto/UnifiedActivityCreateDTO.java

@@ -0,0 +1,151 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 统一活动创建DTO
+ * 支持所有活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼
+ */
+@Data
+public class UnifiedActivityCreateDTO {
+
+    // ===== 通用字段 (所有类型共用) =====
+
+    /**
+     * 活动名称
+     */
+    @NotBlank(message = "活动名称不能为空")
+    private String activityName;
+
+    /**
+     * 活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼
+     */
+    @NotNull(message = "活动类型不能为空")
+    private Integer activityType;
+
+    /**
+     * 活动描述
+     */
+    private String activityDesc;
+
+    /**
+     * 开始时间
+     */
+    @NotNull(message = "开始时间不能为空")
+    private LocalDateTime startTime;
+
+    /**
+     * 结束时间
+     */
+    @NotNull(message = "结束时间不能为空")
+    private LocalDateTime endTime;
+
+    // ===== 适用范围字段 =====
+
+    /**
+     * 适用范围:0-全平台 1-指定设备/门店
+     */
+    private Integer applyScope;
+
+    /**
+     * 设备范围(applyScope=1时使用)
+     */
+    private Integer deviceScope;
+
+    /**
+     * 商品范围:0-全部商品 1-指定商品
+     */
+    private Integer productScope;
+
+    // ===== 预算控制 =====
+
+    /**
+     * 总预算金额
+     */
+    private BigDecimal totalBudget;
+
+    // ===== 优惠字段 (type 1,2,3 使用) =====
+
+    /**
+     * 优惠值(立减金额、折扣率、满减门槛等)
+     */
+    private BigDecimal discountValue;
+
+    /**
+     * 最低消费金额
+     */
+    private BigDecimal minAmount;
+
+    /**
+     * 最大优惠金额上限
+     */
+    private BigDecimal maxDiscount;
+
+    // ===== 邀请特有字段 (type 4 使用) =====
+
+    /**
+     * 邀请人奖励优惠券模板ID(type=4时必填)
+     */
+    @NotNull(message = "邀请人优惠券模板ID不能为空", groups = InviteCheck.class)
+    private Long inviterCouponTemplateId;
+
+    /**
+     * 被邀请人奖励优惠券模板ID(双向奖励时使用,可选)
+     */
+    private Long inviteeCouponTemplateId;
+
+    /**
+     * 每人每日最大邀请数,默认10
+     */
+    @NotNull(message = "每日邀请上限不能为空", groups = InviteCheck.class)
+    private Integer dailyLimit;
+
+    /**
+     * 每人活动期间最大邀请总数(不填则不限制)
+     */
+    private Integer totalLimit;
+
+    /**
+     * 是否要求完成首单:0-否 1-是,默认1
+     */
+    private Integer requireFirstOrder;
+
+    /**
+     * 首单最低金额要求
+     */
+    private BigDecimal minOrderAmount;
+
+    /**
+     * 邀请码前缀(用于生成唯一邀请码)
+     */
+    private String inviteCodePrefix;
+
+    // ===== 关联ID列表 =====
+
+    /**
+     * 指定门店ID列表(applyScope=1时使用)
+     */
+    private List<Long> shopIds;
+
+    /**
+     * 指定设备ID列表(deviceScope指定时使用)
+     */
+    private List<String> deviceIds;
+
+    /**
+     * 指定商品ID列表(productScope=1时使用)
+     */
+    private List<Long> productIds;
+
+    /**
+     * 校验分组接口 - 用于邀请有礼(type=4)的条件性校验
+     * 当 activityType == 4 时,使用该分组进行校验
+     */
+    public interface InviteCheck {}
+}

+ 97 - 0
haha-entity/src/main/java/com/haha/entity/dto/UnifiedActivityQueryDTO.java

@@ -0,0 +1,97 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 统一活动查询DTO
+ * 支持所有活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼
+ * 继承 PageQueryDTO 支持分页查询
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class UnifiedActivityQueryDTO extends PageQueryDTO {
+
+    // ===== 基础查询条件 =====
+
+    /**
+     * 活动名称(模糊查询)
+     */
+    private String activityName;
+
+    /**
+     * 活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼
+     * 支持查询单个类型或所有类型(不传则查询全部)
+     */
+    private Integer activityType;
+
+    /**
+     * 活动状态:0-未开始 1-进行中 2-已结束 3-已停用
+     */
+    private Integer status;
+
+    /**
+     * 创建人ID
+     */
+    private Long creatorId;
+
+    // ===== 时间范围查询 =====
+
+    /**
+     * 开始时间范围 - 起始
+     */
+    private LocalDateTime startTimeBegin;
+
+    /**
+     * 开始时间范围 - 结束
+     */
+    private LocalDateTime startTimeEnd;
+
+    /**
+     * 结束时间范围 - 起始
+     */
+    private LocalDateTime endTimeBegin;
+
+    /**
+     * 结束时间范围 - 结束
+     */
+    private LocalDateTime endTimeEnd;
+
+    // ===== 适用范围筛选 =====
+
+    /**
+     * 适用范围:0-全平台 1-指定设备/门店
+     */
+    private Integer applyScope;
+
+    // ===== 邀请活动特有筛选条件 (type=4) =====
+
+    /**
+     * 是否包含已达到每日上限的活动(仅对邀请有礼类型有效)
+     */
+    private Boolean includeDailyLimitReached;
+
+    /**
+     * 是否包含已达到总上限的活动(仅对邀请有礼类型有效)
+     */
+    private Boolean includeTotalLimitReached;
+
+    /**
+     * 按邀请人数排序:asc-升序 desc-降序(仅对邀请有礼类型有效)
+     */
+    private String orderByInviteCount;
+
+    // ===== 排序字段 =====
+
+    /**
+     * 排序字段:createTime, startTime, endTime, activityName 等
+     */
+    private String sortField;
+
+    /**
+     * 排序方式:asc-升序 desc-降序
+     */
+    private String sortOrder;
+}

+ 142 - 0
haha-entity/src/main/java/com/haha/entity/dto/UnifiedActivityUpdateDTO.java

@@ -0,0 +1,142 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+
+import jakarta.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 统一活动更新DTO
+ * 支持所有活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼
+ * 与 CreateDTO 类似,但 id 字段必填,其他字段可选(用于部分更新)
+ */
+@Data
+public class UnifiedActivityUpdateDTO {
+
+    // ===== 必填字段 =====
+
+    /**
+     * 活动ID(必填)
+     */
+    @NotNull(message = "活动ID不能为空")
+    private Long id;
+
+    // ===== 通用字段 (所有类型共用) - 可选更新 =====
+
+    /**
+     * 活动名称
+     */
+    private String activityName;
+
+    /**
+     * 活动描述
+     */
+    private String activityDesc;
+
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endTime;
+
+    // ===== 适用范围字段 - 可选更新 =====
+
+    /**
+     * 适用范围:0-全平台 1-指定设备/门店
+     */
+    private Integer applyScope;
+
+    /**
+     * 设备范围
+     */
+    private Integer deviceScope;
+
+    /**
+     * 商品范围
+     */
+    private Integer productScope;
+
+    // ===== 预算控制 - 可选更新 =====
+
+    /**
+     * 总预算金额
+     */
+    private BigDecimal totalBudget;
+
+    // ===== 优惠字段 (type 1,2,3 使用) - 可选更新 =====
+
+    /**
+     * 优惠值
+     */
+    private BigDecimal discountValue;
+
+    /**
+     * 最低消费金额
+     */
+    private BigDecimal minAmount;
+
+    /**
+     * 最大优惠金额上限
+     */
+    private BigDecimal maxDiscount;
+
+    // ===== 邀请特有字段 (type 4 使用) - 可选更新 =====
+
+    /**
+     * 邀请人奖励优惠券模板ID
+     */
+    private Long inviterCouponTemplateId;
+
+    /**
+     * 被邀请人奖励优惠券模板ID
+     */
+    private Long inviteeCouponTemplateId;
+
+    /**
+     * 每人每日最大邀请数
+     */
+    private Integer dailyLimit;
+
+    /**
+     * 每人活动期间最大邀请总数
+     */
+    private Integer totalLimit;
+
+    /**
+     * 是否要求完成首单:0-否 1-是
+     */
+    private Integer requireFirstOrder;
+
+    /**
+     * 首单最低金额要求
+     */
+    private BigDecimal minOrderAmount;
+
+    /**
+     * 邀请码前缀
+     */
+    private String inviteCodePrefix;
+
+    // ===== 关联ID列表 - 可选更新 =====
+
+    /**
+     * 指定门店ID列表
+     */
+    private List<Long> shopIds;
+
+    /**
+     * 指定设备ID列表
+     */
+    private List<String> deviceIds;
+
+    /**
+     * 指定商品ID列表
+     */
+    private List<Long> productIds;
+}

+ 174 - 0
haha-entity/src/main/java/com/haha/entity/vo/UnifiedActivityVO.java

@@ -0,0 +1,174 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 统一活动视图对象
+ * 用于统一返回营销活动(type 1-3)和邀请活动(type 4)的数据
+ * 隐藏底层表结构差异,提供统一的展示格式
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UnifiedActivityVO {
+
+    // ===== 通用字段 (所有类型共用) =====
+
+    /**
+     * 活动ID
+     */
+    private Long id;
+
+    /**
+     * 活动名称
+     */
+    private String activityName;
+
+    /**
+     * 活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼
+     */
+    private Integer activityType;
+
+    /**
+     * 类型名称(中文显示)
+     */
+    private String activityTypeName;
+
+    /**
+     * 活动描述
+     */
+    private String activityDesc;
+
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endTime;
+
+    /**
+     * 状态:0-草稿/未开始 1-已发布/进行中 2-已暂停 3-已结束/已停用
+     */
+    private Integer status;
+
+    /**
+     * 状态名称(中文显示)
+     */
+    private String statusName;
+
+    // ===== 营销活动特有字段 (type 1,2,3) =====
+
+    /**
+     * 优惠值(立减金额、折扣率、满减门槛等)
+     */
+    private BigDecimal discountValue;
+
+    /**
+     * 最低消费金额
+     */
+    private BigDecimal minAmount;
+
+    /**
+     * 最大优惠金额上限
+     */
+    private BigDecimal maxDiscount;
+
+    /**
+     * 总预算金额
+     */
+    private BigDecimal totalBudget;
+
+    /**
+     * 已使用预算
+     */
+    private BigDecimal usedBudget;
+
+    // ===== 邀请活动特有字段 (type 4) =====
+
+    /**
+     * 邀请人奖励优惠券模板ID
+     */
+    private Long inviterCouponTemplateId;
+
+    /**
+     * 邀请人奖励优惠券名称(关联查询)
+     */
+    private String inviterCouponName;
+
+    /**
+     * 被邀请人奖励优惠券模板ID
+     */
+    private Long inviteeCouponTemplateId;
+
+    /**
+     * 被邀请人奖励优惠券名称(关联查询)
+     */
+    private String inviteeCouponName;
+
+    /**
+     * 每人每日最大邀请数
+     */
+    private Integer dailyLimit;
+
+    /**
+     * 每人活动期间最大邀请总数
+     */
+    private Integer totalLimit;
+
+    /**
+     * 是否要求完成首单:0-否 1-是
+     */
+    private Integer requireFirstOrder;
+
+    /**
+     * 首单最低金额要求
+     */
+    private BigDecimal minOrderAmount;
+
+    /**
+     * 总邀请次数
+     */
+    private Integer totalInviteCount;
+
+    /**
+     * 有效邀请数
+     */
+    private Integer validInviteCount;
+
+    /**
+     * 奖励发放总数
+     */
+    private Integer rewardCount;
+
+    // ===== 统计信息 =====
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 创建人姓名
+     */
+    private String creatorName;
+
+    /**
+     * 适用范围:0-全平台 1-指定设备/门店
+     */
+    private Integer applyScope;
+
+    /**
+     * 适用范围名称
+     */
+    private String applyScopeLabel;
+}

+ 33 - 0
haha-service/src/main/java/com/haha/service/InviteActivityService.java

@@ -8,6 +8,7 @@ import com.haha.entity.dto.InviteActivityCreateDTO;
 import com.haha.entity.dto.InviteActivityUpdateDTO;
 import com.haha.entity.dto.InviteRecordQueryDTO;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -105,6 +106,13 @@ public interface InviteActivityService extends IService<InviteActivity> {
      */
     InviteActivity getOngoingActivity();
 
+    /**
+     * 获取所有进行中的邀请活动列表(定时任务使用)
+     *
+     * @return 进行中的活动列表
+     */
+    List<InviteActivity> getOngoingActivities();
+
     // ==================== 邀请关系绑定 ====================
 
     /**
@@ -209,6 +217,31 @@ public interface InviteActivityService extends IService<InviteActivity> {
 
     // ==================== 手动补发奖励 ====================
 
+    /**
+     * 自动更新活动状态(定时任务调用)
+     * 检查活动的开始/结束时间,自动将活动状态更新为"进行中"或"已结束"
+     *
+     * @return 更新结果:[0]=应开始数量, [1]=应结束数量
+     */
+    int[] autoUpdateActivityStatus();
+
+    /**
+     * 重试发放失败的奖励(定时任务调用)
+     *
+     * @param maxRetryCount 最大重试次数
+     * @param batchSize      每批处理数量
+     * @return [0]=成功数, [1]=失败数
+     */
+    int[] retryFailedRewards(int maxRetryCount, int batchSize);
+
+    /**
+     * 获取即将结束的活动列表(定时任务调用)
+     *
+     * @param daysAhead 提前天数
+     * @return 即将结束的活动列表
+     */
+    List<InviteActivity> getEndingSoonActivities(int daysAhead);
+
     /**
      * 手动补发失败的奖励
      * 管理员操作,用于处理发放失败的奖励记录

+ 132 - 0
haha-service/src/main/java/com/haha/service/UnifiedActivityService.java

@@ -0,0 +1,132 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.entity.InviteRecord;
+import com.haha.entity.dto.InviteRecordQueryDTO;
+import com.haha.entity.dto.UnifiedActivityCreateDTO;
+import com.haha.entity.dto.UnifiedActivityQueryDTO;
+import com.haha.entity.dto.UnifiedActivityUpdateDTO;
+import com.haha.entity.vo.UnifiedActivityVO;
+
+import java.util.Map;
+
+/**
+ * 统一活动服务接口
+ * 作为前端和后端之间的中间层,负责将不同类型活动的 CRUD 操作分发到对应的具体服务实现
+ *
+ * <p>支持的活动类型:
+ * <ul>
+ *   <li>1 - 首单立减(营销活动)</li>
+ *   <li>2 - 优惠折扣(营销活动)</li>
+ *   <li>3 - 商品满减(营销活动)</li>
+ *   <li>4 - 邀请有礼(邀请活动)</li>
+ * </ul>
+ *
+ * <p>设计原则:
+ * <ul>
+ *   <li>统一入口:所有活动操作通过此接口进行</li>
+ *   <li>类型分发:根据 activityType 自动路由到对应的 Service</li>
+ *   <li>统一返回:使用 UnifiedActivityVO 统一返回格式</li>
+ *   <li>透明转换:隐藏底层表结构差异</li>
+ * </ul>
+ */
+public interface UnifiedActivityService {
+
+    // ==================== 统一 CRUD 操作 ====================
+
+    /**
+     * 统一创建活动
+     * 根据 activityType 自动分发到 MarketingActivityService 或 InviteActivityService
+     *
+     * @param dto 统一创建请求DTO(包含 activityType 字段用于类型判断)
+     * @return 创建成功后的活动ID
+     */
+    Long create(UnifiedActivityCreateDTO dto);
+
+    /**
+     * 统一更新活动
+     * 根据 type 参数自动分发到对应的服务
+     *
+     * @param dto 统一更新请求DTO(包含 id 和 type 字段)
+     */
+    void update(UnifiedActivityUpdateDTO dto);
+
+    /**
+     * 统一分页查询(聚合两种活动)
+     * 支持按 activityType 筛选特定类型的活动,或查询全部活动
+     *
+     * @param queryDTO 查询条件(包含分页参数和筛选条件)
+     * @return 分页结果,统一转换为 UnifiedActivityVO 格式
+     */
+    IPage<UnifiedActivityVO> getPage(UnifiedActivityQueryDTO queryDTO);
+
+    /**
+     * 统一详情查询
+     * 根据 type 参数从对应的服务获取详情并转换为统一格式
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     * @return 活动详情(统一VO格式)
+     */
+    UnifiedActivityVO getDetail(Long id, Integer type);
+
+    // ==================== 统一状态管理 ====================
+
+    /**
+     * 发布活动
+     * 将活动从草稿状态变更为已发布/进行中状态
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     */
+    void publish(Long id, Integer type);
+
+    /**
+     * 暂停活动
+     * 将活动从进行中状态变更为已暂停状态
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     */
+    void pause(Long id, Integer type);
+
+    /**
+     * 恢复活动
+     * 将活动从暂停状态恢复为进行中状态
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     */
+    void resume(Long id, Integer type);
+
+    // ==================== 删除操作 ====================
+
+    /**
+     * 删除活动(逻辑删除)
+     *
+     * @param id   活动ID
+     * @param type 活动类型(1-4)
+     */
+    void delete(Long id, Integer type);
+
+    // ==================== 邀请活动特有方法 ====================
+
+    /**
+     * 分页查询邀请记录列表(仅当 type=4 时调用)
+     * 获取指定邀请活动的邀请记录详情
+     *
+     * @param activityId 邀请活动ID
+     * @param queryDTO   查询条件(包含分页参数和筛选条件)
+     * @return 邀请记录分页结果
+     */
+    IPage<InviteRecord> getInviteRecords(Long activityId, InviteRecordQueryDTO queryDTO);
+
+    /**
+     * 获取邀请活动统计数据(仅当 type=4 时调用)
+     * 包含总邀请数、有效邀请数、今日邀请数、奖励发放统计等
+     *
+     * @param activityId 邀请活动ID
+     * @return 统计数据Map
+     */
+    Map<String, Object> getInviteStatistics(Long activityId);
+}

+ 92 - 1
haha-service/src/main/java/com/haha/service/impl/InviteActivityServiceImpl.java

@@ -401,12 +401,21 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
         if (ongoingList == null || ongoingList.isEmpty()) {
             return null;
         }
-        // 返回第一个进行中的活动
         InviteActivity activity = ongoingList.get(0);
         fillLabels(activity);
         return activity;
     }
 
+    @Override
+    public List<InviteActivity> getOngoingActivities() {
+        LocalDateTime now = LocalDateTime.now();
+        List<InviteActivity> ongoingList = inviteActivityMapper.selectOngoing(now);
+        if (ongoingList == null) {
+            return java.util.Collections.emptyList();
+        }
+        return ongoingList;
+    }
+
     // ==================== 邀请关系绑定 ====================
 
     @Override
@@ -1037,6 +1046,88 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
         return inviteRecordMapper.selectByInviteCode(inviteCode);
     }
 
+    // ==================== 定时任务方法 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int[] autoUpdateActivityStatus() {
+        LocalDateTime now = LocalDateTime.now();
+        int startedCount = 0;
+        int endedCount = 0;
+
+        // 1. 将应该开始的活动状态更新为"进行中"(草稿/已发布 + 开始时间到了 + 未结束)
+        List<InviteActivity> shouldStart = inviteActivityMapper.selectList(
+            new LambdaQueryWrapper<InviteActivity>()
+                .in(InviteActivity::getStatus, 0, 1)
+                .le(InviteActivity::getStartTime, now)
+                .ge(InviteActivity::getEndTime, now)
+                .eq(InviteActivity::getDeleted, 0)
+        );
+
+        for (InviteActivity activity : shouldStart) {
+            inviteActivityMapper.updateStatus(activity.getId(), 2);
+            startedCount++;
+            log.info("[邀请定时任务] 活动{}已自动开始", activity.getActivityName());
+        }
+
+        // 2. 将应该结束的活动状态更新为"已结束"(进行中 + 结束时间过了)
+        List<InviteActivity> shouldEnd = inviteActivityMapper.selectList(
+            new LambdaQueryWrapper<InviteActivity>()
+                .eq(InviteActivity::getStatus, 2)
+                .lt(InviteActivity::getEndTime, now)
+                .eq(InviteActivity::getDeleted, 0)
+        );
+
+        for (InviteActivity activity : shouldEnd) {
+            inviteActivityMapper.updateStatus(activity.getId(), 4);
+            endedCount++;
+            log.info("[邀请定时任务] 活动{}已自动结束", activity.getActivityName());
+        }
+
+        log.info("[邀请定时任务] 活动状态更新完成,开始{}个,结束{}个", startedCount, endedCount);
+        return new int[]{startedCount, endedCount};
+    }
+
+    @Override
+    public int[] retryFailedRewards(int maxRetryCount, int batchSize) {
+        List<InviteReward> failedRewards = inviteRewardMapper.selectFailedRewardsForRetry(maxRetryCount, batchSize);
+
+        int successCount = 0;
+        int failCount = 0;
+
+        for (InviteReward reward : failedRewards) {
+            try {
+                boolean success = distributeReward(reward.getRecordId());
+                if (success) {
+                    successCount++;
+                    log.info("[邀请定时任务] 奖励重试成功 - rewardId: {}", reward.getId());
+                } else {
+                    failCount++;
+                    log.warn("[邀请定时任务] 奖励重试仍失败 - rewardId: {}", reward.getId());
+                }
+            } catch (Exception e) {
+                failCount++;
+                log.error("[邀请定时任务] 奖励重试异常 - rewardId: {}", reward.getId(), e);
+            }
+        }
+
+        log.info("[邀请定时任务] 失败奖励重试完成,成功{}个,失败{}个", successCount, failCount);
+        return new int[]{successCount, failCount};
+    }
+
+    @Override
+    public List<InviteActivity> getEndingSoonActivities(int daysAhead) {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime deadline = now.plusDays(daysAhead);
+
+        return inviteActivityMapper.selectList(
+            new LambdaQueryWrapper<InviteActivity>()
+                .eq(InviteActivity::getStatus, 2)
+                .between(InviteActivity::getEndTime, now, deadline)
+                .eq(InviteActivity::getDeleted, 0)
+        );
+    }
+
     // ==================== 私有工具方法 ====================
 
     /**

+ 669 - 0
haha-service/src/main/java/com/haha/service/impl/UnifiedActivityServiceImpl.java

@@ -0,0 +1,669 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.haha.common.enums.ActivityStatusEnum;
+import com.haha.common.enums.ActivityTypeEnum;
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.InviteActivity;
+import com.haha.entity.InviteRecord;
+import com.haha.entity.MarketingActivity;
+import com.haha.entity.dto.ActivityCreateDTO;
+import com.haha.entity.dto.ActivityQueryDTO;
+import com.haha.entity.dto.ActivityUpdateDTO;
+import com.haha.entity.dto.InviteActivityCreateDTO;
+import com.haha.entity.dto.InviteActivityUpdateDTO;
+import com.haha.entity.dto.InviteRecordQueryDTO;
+import com.haha.entity.dto.UnifiedActivityCreateDTO;
+import com.haha.entity.dto.UnifiedActivityQueryDTO;
+import com.haha.entity.dto.UnifiedActivityUpdateDTO;
+import com.haha.entity.vo.UnifiedActivityVO;
+import com.haha.service.CouponTemplateService;
+import com.haha.service.InviteActivityService;
+import com.haha.service.MarketingActivityService;
+import com.haha.service.UnifiedActivityService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 统一活动服务实现类
+ * 作为前端和后端之间的中间层,根据 activityType 分发请求到对应的 Service
+ *
+ * <p>核心设计:
+ * <ul>
+ *   <li><b>类型分发</b>:使用 if-else 根据 type 字段路由请求(type=4 -> 邀请服务,其他 -> 营销服务)</li>
+ *   <li><b>DTO 转换</b>:将统一 DTO 转换为各服务所需的特定 DTO 格式</li>
+ *   <li><b>VO 统一</b>:将不同实体的数据转换为统一的 VO 格式返回</li>
+ *   <li><b>日志记录</b>:完整的操作日志便于问题排查和审计</li>
+ *   <li><b>异常处理</b>:对不支持的类型抛出明确的业务异常</li>
+ * </ul>
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UnifiedActivityServiceImpl implements UnifiedActivityService {
+
+    private final MarketingActivityService marketingActivityService;
+    private final InviteActivityService inviteActivityService;
+    private final CouponTemplateService couponTemplateService;
+
+    // ==================== 统一 CRUD 操作 ====================
+
+    @Override
+    public Long create(UnifiedActivityCreateDTO dto) {
+        log.info("[统一活动] 创建活动 - type: {}, name: {}", dto.getActivityType(), dto.getActivityName());
+
+        // TODO: 从安全上下文获取当前用户信息(需要集成 Spring Security 或自定义上下文)
+        Long creatorId = 1L;  // 默认值,实际应从 SecurityContext 获取
+        String creatorName = "系统管理员";  // 默认值,实际应从 SecurityContext 获取
+
+        if (dto.getActivityType() == 4) {
+            log.info("[统一活动] 路由到邀请活动服务");
+            InviteActivityCreateDTO inviteDTO = convertToInviteCreateDTO(dto);
+            InviteActivity activity = inviteActivityService.create(inviteDTO, creatorId, creatorName);
+
+            // 【Task 8】创建活动成功后,关联优惠券模板的 activityId
+            associateCouponTemplatesWithActivity(activity.getId(),
+                    dto.getInviterCouponTemplateId(), dto.getInviteeCouponTemplateId());
+
+            return activity.getId();
+        } else if (dto.getActivityType() >= 1 && dto.getActivityType() <= 3) {
+            log.info("[统一活动] 路由到营销活动服务");
+            ActivityCreateDTO marketingDTO = convertToMarketingCreateDTO(dto);
+            MarketingActivity activity = marketingActivityService.create(marketingDTO, creatorId, creatorName);
+            return activity.getId();
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + dto.getActivityType());
+        }
+    }
+
+    @Override
+    public void update(UnifiedActivityUpdateDTO dto) {
+        log.info("[统一活动] 更新活动 - id: {}, type: {}", dto.getId(), getActivityTypeFromUpdateDTO(dto));
+
+        Integer type = getActivityTypeFromUpdateDTO(dto);
+
+        if (type == 4) {
+            log.info("[统一活动] 路由到邀请活动服务进行更新");
+
+            // 【Task 8】更新前获取旧的优惠券模板ID,用于后续清理旧关联
+            Long oldInviterCouponId = null;
+            Long oldInviteeCouponId = null;
+            try {
+                InviteActivity existingActivity = inviteActivityService.getDetail(dto.getId());
+                if (existingActivity != null) {
+                    oldInviterCouponId = existingActivity.getCouponTemplateId();
+                    oldInviteeCouponId = existingActivity.getInviteeCouponTemplateId();
+                }
+            } catch (Exception e) {
+                log.warn("[统一活动] 获取原活动信息失败,跳过旧关联清理: {}", e.getMessage());
+            }
+
+            InviteActivityUpdateDTO inviteDTO = convertToInviteUpdateDTO(dto);
+            inviteActivityService.update(inviteDTO);
+
+            // 【Task 8】更新后同步优惠券模板的 activityId 关联
+            syncCouponTemplateAssociation(dto.getId(),
+                    oldInviterCouponId, oldInviteeCouponId,
+                    dto.getInviterCouponTemplateId(), dto.getInviteeCouponTemplateId());
+        } else if (type >= 1 && type <= 3) {
+            log.info("[统一活动] 路由到营销活动服务进行更新");
+            ActivityUpdateDTO marketingDTO = convertToMarketingUpdateDTO(dto);
+            marketingActivityService.update(marketingDTO);
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + type);
+        }
+    }
+
+    @Override
+    public IPage<UnifiedActivityVO> getPage(UnifiedActivityQueryDTO queryDTO) {
+        log.info("[统一活动] 分页查询 - type: {}, page: {}, size: {}",
+                queryDTO.getActivityType(), queryDTO.getPage(), queryDTO.getPageSize());
+
+        if (queryDTO.getActivityType() != null && queryDTO.getActivityType() == 4) {
+            // 仅查询邀请活动,并转换为统一 VO 格式
+            log.info("[统一活动] 查询邀请活动列表");
+            InviteRecordQueryDTO inviteQuery = convertToInviteQuery(queryDTO);
+            IPage<InviteActivity> page = inviteActivityService.getPage(inviteQuery);
+            return convertInvitePageToUnified(page);
+        } else if (queryDTO.getActivityType() == null ||
+                (queryDTO.getActivityType() >= 1 && queryDTO.getActivityType() <= 3)) {
+            // 查询营销活动 (type 1-3 或全部)
+            log.info("[统一活动] 查询营销活动列表");
+            ActivityQueryDTO marketingQuery = convertToMarketingQuery(queryDTO);
+            IPage<MarketingActivity> page = marketingActivityService.getPage(marketingQuery);
+            return convertMarketingPageToUnified(page);
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + queryDTO.getActivityType());
+        }
+    }
+
+    @Override
+    public UnifiedActivityVO getDetail(Long id, Integer type) {
+        log.info("[统一活动] 查询详情 - id: {}, type: {}", id, type);
+
+        if (type == 4) {
+            log.info("[统一活动] 从邀请活动服务获取详情");
+            InviteActivity activity = inviteActivityService.getDetail(id);
+            return convertInviteToUnifiedVO(activity);
+        } else if (type >= 1 && type <= 3) {
+            log.info("[统一活动] 从营销活动服务获取详情");
+            MarketingActivity activity = marketingActivityService.getDetail(id);
+            return convertMarketingToUnifiedVO(activity);
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + type);
+        }
+    }
+
+    // ==================== 统一状态管理 ====================
+
+    @Override
+    public void publish(Long id, Integer type) {
+        log.info("[统一活动] 发布活动 - id: {}, type: {}", id, type);
+
+        if (type == 4) {
+            boolean result = inviteActivityService.publish(id);
+            logPublishResult(id, type, result);
+        } else if (type >= 1 && type <= 3) {
+            boolean result = marketingActivityService.publish(id);
+            logPublishResult(id, type, result);
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + type);
+        }
+    }
+
+    @Override
+    public void pause(Long id, Integer type) {
+        log.info("[统一活动] 暂停活动 - id: {}, type: {}", id, type);
+
+        if (type == 4) {
+            boolean result = inviteActivityService.pause(id);
+            logOperationResult("暂停", id, type, result);
+        } else if (type >= 1 && type <= 3) {
+            boolean result = marketingActivityService.pause(id);
+            logOperationResult("暂停", id, type, result);
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + type);
+        }
+    }
+
+    @Override
+    public void resume(Long id, Integer type) {
+        log.info("[统一活动] 恢复活动 - id: {}, type: {}", id, type);
+
+        if (type == 4) {
+            boolean result = inviteActivityService.resume(id);
+            logOperationResult("恢复", id, type, result);
+        } else if (type >= 1 && type <= 3) {
+            boolean result = marketingActivityService.resume(id);
+            logOperationResult("恢复", id, type, result);
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + type);
+        }
+    }
+
+    // ==================== 删除操作 ====================
+
+    @Override
+    public void delete(Long id, Integer type) {
+        log.info("[统一活动] 删除活动 - id: {}, type: {}", id, type);
+
+        if (type == 4) {
+            // 【Task 8】删除前清理关联的优惠券模板 activityId
+            try {
+                InviteActivity existingActivity = inviteActivityService.getDetail(id);
+                if (existingActivity != null) {
+                    disassociateCouponTemplatesFromActivity(
+                            existingActivity.getCouponTemplateId(),
+                            existingActivity.getInviteeCouponTemplateId());
+                }
+            } catch (Exception e) {
+                log.warn("[统一活动] 清理优惠券关联失败(不影响删除操作): {}", e.getMessage());
+            }
+
+            boolean result = inviteActivityService.deleteActivity(id);
+            logOperationResult("删除", id, type, result);
+        } else if (type >= 1 && type <= 3) {
+            boolean result = marketingActivityService.deleteActivity(id);
+            logOperationResult("删除", id, type, result);
+        } else {
+            throw new BusinessException(400, "不支持的活动类型: " + type);
+        }
+    }
+
+    // ==================== 邀请活动特有方法 ====================
+
+    @Override
+    public IPage<InviteRecord> getInviteRecords(Long activityId, InviteRecordQueryDTO queryDTO) {
+        log.info("[统一活动] 查询邀请记录 - activityId: {}", activityId);
+        queryDTO.setActivityId(activityId);
+        return inviteActivityService.getInviteRecords(queryDTO);
+    }
+
+    @Override
+    public Map<String, Object> getInviteStatistics(Long activityId) {
+        log.info("[统一活动] 查询邀请统计 - activityId: {}", activityId);
+        return inviteActivityService.getActivityStatistics(activityId);
+    }
+
+    // ==================== DTO 转换方法 ====================
+
+    /**
+     * 将 UnifiedActivityCreateDTO 转换为 InviteActivityCreateDTO
+     * 提取邀请活动特有的字段
+     */
+    private InviteActivityCreateDTO convertToInviteCreateDTO(UnifiedActivityCreateDTO dto) {
+        InviteActivityCreateDTO inviteDTO = new InviteActivityCreateDTO();
+        inviteDTO.setActivityName(dto.getActivityName());
+        inviteDTO.setActivityDesc(dto.getActivityDesc());
+        inviteDTO.setStartTime(dto.getStartTime());
+        inviteDTO.setEndTime(dto.getEndTime());
+        inviteDTO.setApplyScope(dto.getApplyScope());
+        inviteDTO.setProductScope(dto.getProductScope());
+
+        // 邀请特有字段
+        inviteDTO.setInviterCouponTemplateId(dto.getInviterCouponTemplateId());
+        inviteDTO.setInviteeCouponTemplateId(dto.getInviteeCouponTemplateId());
+        inviteDTO.setDailyLimit(dto.getDailyLimit());
+        inviteDTO.setTotalLimit(dto.getTotalLimit());
+        inviteDTO.setRequireFirstOrder(dto.getRequireFirstOrder());
+        inviteDTO.setMinOrderAmount(dto.getMinOrderAmount());
+        inviteDTO.setInviteCodePrefix(dto.getInviteCodePrefix());
+
+        // 关联ID列表
+        inviteDTO.setShopIds(dto.getShopIds());
+        inviteDTO.setProductIds(dto.getProductIds());
+
+        return inviteDTO;
+    }
+
+    /**
+     * 将 UnifiedActivityCreateDTO 转换为 ActivityCreateDTO(营销活动)
+     * 提取营销活动特有的字段
+     */
+    private ActivityCreateDTO convertToMarketingCreateDTO(UnifiedActivityCreateDTO dto) {
+        ActivityCreateDTO marketingDTO = new ActivityCreateDTO();
+        marketingDTO.setActivityName(dto.getActivityName());
+        marketingDTO.setActivityType(dto.getActivityType());
+        marketingDTO.setActivityDesc(dto.getActivityDesc());
+        marketingDTO.setStartTime(dto.getStartTime());
+        marketingDTO.setEndTime(dto.getEndTime());
+        marketingDTO.setApplyScope(dto.getApplyScope());
+        marketingDTO.setDeviceScope(dto.getDeviceScope());
+        marketingDTO.setProductScope(dto.getProductScope());
+        marketingDTO.setTotalBudget(dto.getTotalBudget());
+
+        // 优惠字段 (type 1-3)
+        marketingDTO.setDiscountValue(dto.getDiscountValue());
+        marketingDTO.setMinAmount(dto.getMinAmount());
+        marketingDTO.setMaxDiscount(dto.getMaxDiscount());
+
+        // 关联ID列表
+        marketingDTO.setShopIds(dto.getShopIds());
+        marketingDTO.setDeviceIds(dto.getDeviceIds());
+        marketingDTO.setProductIds(dto.getProductIds());
+
+        return marketingDTO;
+    }
+
+    /**
+     * 将 UnifiedActivityUpdateDTO 转换为 InviteActivityUpdateDTO
+     */
+    private InviteActivityUpdateDTO convertToInviteUpdateDTO(UnifiedActivityUpdateDTO dto) {
+        InviteActivityUpdateDTO inviteDTO = new InviteActivityUpdateDTO();
+        inviteDTO.setId(dto.getId());
+        inviteDTO.setActivityName(dto.getActivityName());
+        inviteDTO.setActivityDesc(dto.getActivityDesc());
+        inviteDTO.setStartTime(dto.getStartTime());
+        inviteDTO.setEndTime(dto.getEndTime());
+        inviteDTO.setApplyScope(dto.getApplyScope());
+        inviteDTO.setProductScope(dto.getProductScope());
+
+        // 邀请特有字段
+        inviteDTO.setInviterCouponTemplateId(dto.getInviterCouponTemplateId());
+        inviteDTO.setInviteeCouponTemplateId(dto.getInviteeCouponTemplateId());
+        inviteDTO.setDailyLimit(dto.getDailyLimit());
+        inviteDTO.setTotalLimit(dto.getTotalLimit());
+        inviteDTO.setRequireFirstOrder(dto.getRequireFirstOrder());
+        inviteDTO.setMinOrderAmount(dto.getMinOrderAmount());
+        inviteDTO.setInviteCodePrefix(dto.getInviteCodePrefix());
+
+        // 关联ID列表
+        inviteDTO.setShopIds(dto.getShopIds());
+        inviteDTO.setProductIds(dto.getProductIds());
+
+        return inviteDTO;
+    }
+
+    /**
+     * 将 UnifiedActivityUpdateDTO 转换为 ActivityUpdateDTO(营销活动)
+     */
+    private ActivityUpdateDTO convertToMarketingUpdateDTO(UnifiedActivityUpdateDTO dto) {
+        ActivityUpdateDTO marketingDTO = new ActivityUpdateDTO();
+        marketingDTO.setId(dto.getId());
+        marketingDTO.setActivityName(dto.getActivityName());
+        marketingDTO.setActivityDesc(dto.getActivityDesc());
+        marketingDTO.setStartTime(dto.getStartTime());
+        marketingDTO.setEndTime(dto.getEndTime());
+        marketingDTO.setApplyScope(dto.getApplyScope());
+        marketingDTO.setDeviceScope(dto.getDeviceScope());
+        marketingDTO.setProductScope(dto.getProductScope());
+        marketingDTO.setTotalBudget(dto.getTotalBudget());
+
+        // 优惠字段 (type 1-3)
+        marketingDTO.setDiscountValue(dto.getDiscountValue());
+        marketingDTO.setMinAmount(dto.getMinAmount());
+        marketingDTO.setMaxDiscount(dto.getMaxDiscount());
+
+        // 关联ID列表
+        marketingDTO.setShopIds(dto.getShopIds());
+        marketingDTO.setDeviceIds(dto.getDeviceIds());
+        marketingDTO.setProductIds(dto.getProductIds());
+
+        return marketingDTO;
+    }
+
+    /**
+     * 将 UnifiedActivityQueryDTO 转换为 InviteRecordQueryDTO(邀请活动查询)
+     */
+    private InviteRecordQueryDTO convertToInviteQuery(UnifiedActivityQueryDTO queryDTO) {
+        InviteRecordQueryDTO inviteQuery = new InviteRecordQueryDTO();
+        inviteQuery.setPage(queryDTO.getPage());
+        inviteQuery.setPageSize(queryDTO.getPageSize());
+        inviteQuery.setActivityName(queryDTO.getActivityName());
+        inviteQuery.setStatus(queryDTO.getStatus());
+        inviteQuery.setStartDate(queryDTO.getStartTimeBegin());
+        inviteQuery.setEndDate(queryDTO.getEndTimeEnd());
+
+        return inviteQuery;
+    }
+
+    /**
+     * 将 UnifiedActivityQueryDTO 转换为 ActivityQueryDTO(营销活动查询)
+     */
+    private ActivityQueryDTO convertToMarketingQuery(UnifiedActivityQueryDTO queryDTO) {
+        ActivityQueryDTO marketingQuery = new ActivityQueryDTO();
+        marketingQuery.setPage(queryDTO.getPage());
+        marketingQuery.setPageSize(queryDTO.getPageSize());
+        marketingQuery.setActivityName(queryDTO.getActivityName());
+        marketingQuery.setActivityType(queryDTO.getActivityType());
+        marketingQuery.setStatus(queryDTO.getStatus());
+        marketingQuery.setCreatorId(queryDTO.getCreatorId());
+        marketingQuery.setStartTimeBegin(queryDTO.getStartTimeBegin());
+        marketingQuery.setStartTimeEnd(queryDTO.getStartTimeEnd());
+
+        return marketingQuery;
+    }
+
+    // ==================== VO 转换方法 ====================
+
+    /**
+     * 将营销活动分页结果转换为统一 VO 分页结果
+     */
+    private IPage<UnifiedActivityVO> convertMarketingPageToUnified(IPage<MarketingActivity> page) {
+        Page<UnifiedActivityVO> unifiedPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
+        List<UnifiedActivityVO> records = page.getRecords().stream()
+                .map(this::convertMarketingToUnifiedVO)
+                .collect(Collectors.toList());
+        unifiedPage.setRecords(records);
+        return unifiedPage;
+    }
+
+    /**
+     * 将邀请活动分页结果转换为统一 VO 分页结果
+     */
+    private IPage<UnifiedActivityVO> convertInvitePageToUnified(IPage<InviteActivity> page) {
+        Page<UnifiedActivityVO> unifiedPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
+        List<UnifiedActivityVO> records = page.getRecords().stream()
+                .map(this::convertInviteToUnifiedVO)
+                .collect(Collectors.toList());
+        unifiedPage.setRecords(records);
+        return unifiedPage;
+    }
+
+    /**
+     * 将单个营销活动实体转换为统一 VO
+     */
+    private UnifiedActivityVO convertMarketingToUnifiedVO(MarketingActivity activity) {
+        return UnifiedActivityVO.builder()
+                .id(activity.getId())
+                .activityName(activity.getActivityName())
+                .activityType(activity.getActivityType())
+                .activityTypeName(ActivityTypeEnum.getLabelByCode(activity.getActivityType()).getLabel())
+                .activityDesc(activity.getActivityDesc())
+                .startTime(activity.getStartTime())
+                .endTime(activity.getEndTime())
+                .status(activity.getStatus())
+                .statusName(ActivityStatusEnum.getLabelByCode(activity.getStatus()).getLabel())
+
+                // 营销活动特有字段
+                .discountValue(activity.getDiscountValue())
+                .minAmount(activity.getMinAmount())
+                .maxDiscount(activity.getMaxDiscount())
+                .totalBudget(activity.getTotalBudget())
+                .usedBudget(activity.getUsedBudget())
+
+                // 统计信息
+                .createTime(activity.getCreateTime())
+                .creatorName(activity.getCreatorName())
+                .applyScope(activity.getApplyScope())
+
+                .build();
+    }
+
+    /**
+     * 将单个邀请活动实体转换为统一 VO
+     */
+    private UnifiedActivityVO convertInviteToUnifiedVO(InviteActivity activity) {
+        return UnifiedActivityVO.builder()
+                .id(activity.getId())
+                .activityName(activity.getActivityName())
+                .activityType(4)  // 邀请活动固定为 type 4
+                .activityTypeName(ActivityTypeEnum.INVITE_REWARD.getLabel())
+                .activityDesc(activity.getActivityDesc())
+                .startTime(activity.getStartTime())
+                .endTime(activity.getEndTime())
+                .status(activity.getStatus())
+                .statusName(ActivityStatusEnum.getLabelByCode(activity.getStatus()).getLabel())
+
+                // 邀请活动特有字段
+                .inviterCouponTemplateId(activity.getCouponTemplateId())
+                .inviteeCouponTemplateId(activity.getInviteeCouponTemplateId())
+                .dailyLimit(activity.getDailyLimit())
+                .totalLimit(activity.getTotalLimit())
+                .requireFirstOrder(activity.getRequireFirstOrder())
+                .minOrderAmount(activity.getMinOrderAmount())
+                .totalInviteCount(activity.getTotalInviteCount())
+                .validInviteCount(activity.getValidInviteCount())
+                .rewardCount(activity.getRewardCount())
+
+                // 统计信息
+                .createTime(activity.getCreateTime())
+                .creatorName(activity.getCreatorName())
+                .applyScope(activity.getApplyScope())
+
+                .build();
+    }
+
+    // ==================== 辅助方法 ====================
+
+    /**
+     * 从 UpdateDTO 中推断活动类型
+     * 由于 UpdateDTO 不包含 type 字段,需要先查询原活动获取类型
+     * 实现策略:先尝试从邀请活动表查询,再尝试从营销活动表查询
+     */
+    private Integer getActivityTypeFromUpdateDTO(UnifiedActivityUpdateDTO dto) {
+        Long id = dto.getId();
+
+        // 1. 先尝试从邀请活动表查询(type=4)
+        try {
+            InviteActivity inviteActivity = inviteActivityService.getById(id);
+            if (inviteActivity != null && inviteActivity.getDeleted() != null && inviteActivity.getDeleted() == 0) {
+                log.info("[统一活动] 从邀请活动表查到活动类型: id={}, type=4", id);
+                return 4;
+            }
+        } catch (Exception e) {
+            log.debug("[统一活动] 邀请活动表未找到该记录: id={}", id);
+        }
+
+        // 2. 再尝试从营销活动表查询(type=1,2,3)
+        try {
+            MarketingActivity marketingActivity = marketingActivityService.getById(id);
+            if (marketingActivity != null) {
+                log.info("[统一活动] 从营销活动表查到活动类型: id={}, type={}", id, marketingActivity.getActivityType());
+                return marketingActivity.getActivityType();
+            }
+        } catch (Exception e) {
+            log.debug("[统一活动] 营销活动表未找到该记录: id={}", id);
+        }
+
+        throw new BusinessException(404, "活动不存在或已被删除,id: " + id);
+    }
+
+    /**
+     * 记录发布操作结果日志
+     */
+    private void logPublishResult(Long id, Integer type, boolean success) {
+        if (success) {
+            log.info("[统一活动] 发布成功 - id: {}, type: {}", id, type);
+        } else {
+            log.warn("[统一活动] 发布失败 - id: {}, type: {}", id, type);
+        }
+    }
+
+    /**
+     * 记录通用操作结果日志
+     */
+    private void logOperationResult(String operation, Long id, Integer type, boolean success) {
+        if (success) {
+            log.info("[统一活动] {}成功 - id: {}, type: {}", operation, id, type);
+        } else {
+            log.warn("[统一活动] {}失败 - id: {}, type: {}", operation, id, type);
+        }
+    }
+
+    // ==================== 优惠券-活动关联方法 (Task 8) ====================
+
+    /**
+     * 创建活动后,将优惠券模板与活动关联
+     * 更新 CouponTemplate 的 activityId 字段指向当前活动
+     *
+     * @param activityId              活动ID
+     * @param inviterCouponTemplateId 邀请人优惠券模板ID(可为null)
+     * @param inviteeCouponTemplateId 被邀请人优惠券模板ID(可为null)
+     */
+    private void associateCouponTemplatesWithActivity(Long activityId,
+                                                       Long inviterCouponTemplateId,
+                                                       Long inviteeCouponTemplateId) {
+        if (inviterCouponTemplateId != null) {
+            updateCouponTemplateActivityId(inviterCouponTemplateId, activityId, "邀请人");
+        }
+        if (inviteeCouponTemplateId != null) {
+            updateCouponTemplateActivityId(inviteeCouponTemplateId, activityId, "被邀请人");
+        }
+        log.info("[统一活动] 优惠券模板关联完成 - activityId={}, inviter={}, invitee={}",
+                activityId, inviterCouponTemplateId, inviteeCouponTemplateId);
+    }
+
+    /**
+     * 更新活动时同步优惠券模板关联
+     * 处理逻辑:
+     * 1. 如果旧模板ID与新不同,清除旧的 activityId
+     * 2. 如果新模板ID不为空且与旧的不同,设置新的 activityId
+     *
+     * @param activityId           活动ID
+     * @param oldInviterId         旧的邀请人优惠券模板ID
+     * @param oldInviteeId         旧的被邀请人优惠券模板ID
+     * @param newInviterId         新的邀请人优惠券模板ID
+     * @param newInviteeId         新的被邀请人优惠券模板ID
+     */
+    private void syncCouponTemplateAssociation(Long activityId,
+                                                Long oldInviterId, Long oldInviteeId,
+                                                Long newInviterId, Long newInviteeId) {
+        // 同步邀请人优惠券模板
+        if (oldInviterId != null && !oldInviterId.equals(newInviterId)) {
+            clearCouponTemplateActivityId(oldInviterId); // 清除旧关联
+        }
+        if (newInviterId != null && !newInviterId.equals(oldInviterId)) {
+            updateCouponTemplateActivityId(newInviterId, activityId, "邀请人"); // 建立新关联
+        }
+
+        // 同步被邀请人优惠券模板
+        if (oldInviteeId != null && !oldInviteeId.equals(newInviteeId)) {
+            clearCouponTemplateActivityId(oldInviteeId); // 清除旧关联
+        }
+        if (newInviteeId != null && !newInviteeId.equals(oldInviteeId)) {
+            updateCouponTemplateActivityId(newInviteeId, activityId, "被邀请人"); // 建立新关联
+        }
+
+        log.info("[统一活动] 优惠券模板关联同步完成 - activityId={}, 旧:[{},{}] -> 新:[{},{}]",
+                activityId, oldInviterId, oldInviteeId, newInviterId, newInviteeId);
+    }
+
+    /**
+     * 删除活动时,解除优惠券模板的活动关联
+     * 将相关 CouponTemplate 的 activityId 设为 null
+     *
+     * @param inviterCouponTemplateId 邀请人优惠券模板ID
+     * @param inviteeCouponTemplateId 被邀请人优惠券模板ID
+     */
+    private void disassociateCouponTemplatesFromActivity(Long inviterCouponTemplateId,
+                                                          Long inviteeCouponTemplateId) {
+        if (inviterCouponTemplateId != null) {
+            clearCouponTemplateActivityId(inviterCouponTemplateId);
+        }
+        if (inviteeCouponTemplateId != null) {
+            clearCouponTemplateActivityId(inviteeCouponTemplateId);
+        }
+        log.info("[统一活动] 已解除优惠券模板关联 - inviter={}, invitee={}",
+                inviterCouponTemplateId, inviteeCouponTemplateId);
+    }
+
+    /**
+     * 更新单个优惠券模板的 activityId
+     *
+     * @param templateId  优惠券模板ID
+     * @param activityId 要关联的活动ID
+     * @param role       角色(用于日志:邀请人/被邀请人)
+     */
+    private void updateCouponTemplateActivityId(Long templateId, Long activityId, String role) {
+        try {
+            com.haha.entity.CouponTemplate template = couponTemplateService.getById(templateId);
+            if (template != null) {
+                template.setActivityId(activityId);
+                couponTemplateService.updateById(template);
+                log.info("[统一活动] 已更新{}优惠券模板关联 - templateId={}, activityId={}",
+                        role, templateId, activityId);
+            } else {
+                log.warn("[统一活动] {}优惠券模板不存在,跳过关联 - templateId={}", role, templateId);
+            }
+        } catch (Exception e) {
+            log.error("[统一活动] 更新{}优惠券模板关联失败 - templateId={}, error={}",
+                    role, templateId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 清除单个优惠券模板的 activityId(设为 null)
+     *
+     * @param templateId 优惠券模板ID
+     */
+    private void clearCouponTemplateActivityId(Long templateId) {
+        try {
+            com.haha.entity.CouponTemplate template = couponTemplateService.getById(templateId);
+            if (template != null) {
+                template.setActivityId(null);
+                couponTemplateService.updateById(template);
+                log.info("[统一活动] 已清除优惠券模板关联 - templateId={}", templateId);
+            }
+        } catch (Exception e) {
+            log.error("[统一活动] 清除优惠券模板关联失败 - templateId={}, error={}",
+                    templateId, e.getMessage(), e);
+        }
+    }
+}