skyline il y a 1 mois
Parent
commit
aa5fc954cd
38 fichiers modifiés avec 6267 ajouts et 8 suppressions
  1. 68 3
      haha-admin-mp/src/App.vue
  2. 152 0
      haha-admin-mp/src/api/invite.ts
  3. 108 0
      haha-admin-mp/src/mock/invite.ts
  4. 18 0
      haha-admin-mp/src/pages.json
  5. 714 0
      haha-admin-mp/src/pages/invite/index.vue
  6. 720 0
      haha-admin-mp/src/pages/invite/records.vue
  7. 79 0
      haha-admin-web/src/api/invite.ts
  8. 41 0
      haha-admin-web/src/router/modules/marketing.ts
  9. 239 0
      haha-admin-web/src/views/marketing/invite/index.vue
  10. 308 0
      haha-admin-web/src/views/marketing/invite/records.vue
  11. 340 0
      haha-admin-web/src/views/marketing/invite/statistics.vue
  12. 429 0
      haha-admin-web/src/views/marketing/invite/utils/hook.tsx
  13. 96 0
      haha-admin-web/src/views/marketing/invite/utils/types.ts
  14. 250 0
      haha-admin/src/main/java/com/haha/admin/controller/InviteActivityAdminController.java
  15. 187 0
      haha-admin/src/main/java/com/haha/admin/task/InviteTask.java
  16. 91 0
      haha-admin/src/main/resources/sql/marketing.sql
  17. 32 0
      haha-common/src/main/java/com/haha/common/constant/MarketingConstants.java
  18. 2 1
      haha-common/src/main/java/com/haha/common/enums/ActivityTypeEnum.java
  19. 91 0
      haha-common/src/main/java/com/haha/common/enums/InviteRewardStatusEnum.java
  20. 96 0
      haha-common/src/main/java/com/haha/common/enums/InviteStatusEnum.java
  21. 112 0
      haha-entity/src/main/java/com/haha/entity/InviteActivity.java
  22. 89 0
      haha-entity/src/main/java/com/haha/entity/InviteRecord.java
  23. 93 0
      haha-entity/src/main/java/com/haha/entity/InviteReward.java
  24. 7 0
      haha-entity/src/main/java/com/haha/entity/dto/CouponDistributeDTO.java
  25. 74 0
      haha-entity/src/main/java/com/haha/entity/dto/InviteActivityCreateDTO.java
  26. 45 0
      haha-entity/src/main/java/com/haha/entity/dto/InviteActivityUpdateDTO.java
  27. 42 0
      haha-entity/src/main/java/com/haha/entity/dto/InviteRecordQueryDTO.java
  28. 24 0
      haha-entity/src/main/java/com/haha/entity/dto/InviteRewardQueryDTO.java
  29. 51 0
      haha-mapper/src/main/java/com/haha/mapper/InviteActivityMapper.java
  30. 58 0
      haha-mapper/src/main/java/com/haha/mapper/InviteRecordMapper.java
  31. 47 0
      haha-mapper/src/main/java/com/haha/mapper/InviteRewardMapper.java
  32. 219 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/InviteController.java
  33. 221 0
      haha-service/src/main/java/com/haha/service/InviteActivityService.java
  34. 39 0
      haha-service/src/main/java/com/haha/service/InviteNotificationService.java
  35. 914 0
      haha-service/src/main/java/com/haha/service/impl/InviteActivityServiceImpl.java
  36. 114 0
      haha-service/src/main/java/com/haha/service/impl/InviteNotificationServiceImpl.java
  37. 50 3
      haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java
  38. 7 1
      pom.xml

+ 68 - 3
haha-admin-mp/src/App.vue

@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
 
-onLaunch(() => {
-  console.log("App Launch");
+onLaunch((options: any) => {
+  console.log("App Launch", options);
   checkLoginStatus();
+  checkInviteCode(options);
 });
 
 onShow(() => {
@@ -19,7 +20,7 @@ const checkLoginStatus = () => {
   const pages = getCurrentPages();
   const currentPage = pages[pages.length - 1];
   const currentPath = currentPage ? currentPage.route : '';
-  
+
   if (!token && currentPath !== 'pages/login/login') {
     console.log('未登录,跳转到登录页');
     uni.reLaunch({
@@ -27,6 +28,70 @@ const checkLoginStatus = () => {
     });
   }
 };
+
+/**
+ * 检测启动参数中的邀请码
+ * 场景:通过分享链接进入小程序时,会携带 inviteCode 参数
+ */
+const checkInviteCode = (options: any) => {
+  // 检查 query 参数中的邀请码
+  if (options && options.query && options.query.inviteCode) {
+    console.log('检测到邀请码:', options.query.inviteCode);
+    uni.setStorageSync('pendingInviteCode', options.query.inviteCode);
+    return;
+  }
+
+  // 检查 scene 场景值(小程序码场景)
+  if (options && options.scene) {
+    // scene 需要后端解码,这里仅做记录
+    console.log('检测到场景值:', options.scene);
+  }
+};
+
+/**
+ * 绑定邀请关系(注册成功后调用)
+ * @param onSuccess 绑定成功回调
+ */
+export const bindPendingInviteRelation = async (onSuccess?: () => void) => {
+  const inviteCode = uni.getStorageSync('pendingInviteCode');
+
+  if (!inviteCode) {
+    console.log('无待绑定的邀请码');
+    return false;
+  }
+
+  try {
+    const { bindInviteRelation } = await import('@/api/invite');
+
+    const result = await bindInviteRelation({ inviteCode });
+
+    if (result.success) {
+      // 绑定成功,清除本地存储的邀请码
+      uni.removeStorageSync('pendingInviteCode');
+
+      uni.showToast({
+        title: '邀请关系绑定成功',
+        icon: 'success'
+      });
+
+      if (onSuccess) {
+        onSuccess();
+      }
+
+      return true;
+    } else {
+      console.warn('绑定邀请关系返回失败:', result.message);
+      return false;
+    }
+  } catch (error) {
+    console.error('绑定邀请关系失败', error);
+
+    // 即使绑定失败也清除邀请码,避免重复尝试
+    uni.removeStorageSync('pendingInviteCode');
+
+    return false;
+  }
+};
 </script>
 <style>
 /* 

+ 152 - 0
haha-admin-mp/src/api/invite.ts

@@ -0,0 +1,152 @@
+/**
+ * 邀请有礼功能相关API
+ */
+
+import { USE_MOCK } from '@/utils/config';
+import { mockInviteRecords, mockInviteStats, mockActivityInfo, mockInviteCode } from '@/mock/invite';
+
+export enum InviteStatus {
+  PENDING = 'pending',
+  ACTIVATED = 'activated',
+  REWARDED = 'rewarded',
+  FAILED = 'failed'
+}
+
+export enum InviteStatusLabel {
+  PENDING = '待激活',
+  ACTIVATED = '已激活',
+  REWARDED = '奖励已发放',
+  FAILED = '发放失败'
+}
+
+export interface InviteCodeResponse {
+  code: string;
+  inviteUrl: string;
+  expireTime?: string;
+}
+
+export interface BindInviteParams {
+  inviteCode: string;
+}
+
+export interface InviteRecord {
+  id: string;
+  inviteePhone: string;
+  inviteeName?: string;
+  status: InviteStatus;
+  inviteTime: string;
+  activateTime?: string;
+  rewardTime?: string;
+  couponName?: string;
+  couponAmount?: number;
+}
+
+export interface InviteRecordListResponse {
+  list: InviteRecord[];
+  total: number;
+  page: number;
+  pageSize: number;
+}
+
+export interface InviteQueryParams {
+  page?: number;
+  pageSize?: number;
+  status?: InviteStatus;
+}
+
+export interface InviteStatistics {
+  totalInvite: number;
+  validInvite: number;
+  rewardCount: number;
+  pendingCount: number;
+  totalRewardAmount: number;
+}
+
+export interface ActivityInfo {
+  activityId: string;
+  title: string;
+  description: string;
+  couponName: string;
+  couponAmount: number;
+  startTime: string;
+  endTime: string;
+  rules: string[];
+  isActive: boolean;
+}
+
+export interface PosterResponse {
+  posterUrl: string;
+  expireTime: string;
+}
+
+export async function getInviteCode(): Promise<InviteCodeResponse> {
+  if (USE_MOCK) {
+    return Promise.resolve(mockInviteCode);
+  }
+
+  const { get } = await import('@/utils/request');
+  return get('/invite/code');
+}
+
+export async function bindInviteRelation(params: BindInviteParams): Promise<{ success: boolean; message: string }> {
+  if (USE_MOCK) {
+    return Promise.resolve({ success: true, message: '邀请关系绑定成功' });
+  }
+
+  const { post } = await import('@/utils/request');
+  return post('/invite/bind', params);
+}
+
+export async function getMyInviteRecords(params: InviteQueryParams = {}): Promise<InviteRecordListResponse> {
+  if (USE_MOCK) {
+    const { page = 1, pageSize = 10, status } = params;
+    let list = [...mockInviteRecords];
+
+    if (status) {
+      list = list.filter(item => item.status === status);
+    }
+
+    const total = list.length;
+    const startIndex = (page - 1) * pageSize;
+
+    return Promise.resolve({
+      list: list.slice(startIndex, startIndex + pageSize),
+      total,
+      page,
+      pageSize
+    });
+  }
+
+  const { get } = await import('@/utils/request');
+  return get('/invite/my-records', params);
+}
+
+export async function getMyInviteStatistics(): Promise<InviteStatistics> {
+  if (USE_MOCK) {
+    return Promise.resolve(mockInviteStats);
+  }
+
+  const { get } = await import('@/utils/request');
+  return get('/invite/my-statistics');
+}
+
+export async function getActivityInfo(): Promise<ActivityInfo> {
+  if (USE_MOCK) {
+    return Promise.resolve(mockActivityInfo);
+  }
+
+  const { get } = await import('@/utils/request');
+  return get('/invite/activity-info');
+}
+
+export async function generatePoster(): Promise<PosterResponse> {
+  if (USE_MOCK) {
+    return Promise.resolve({
+      posterUrl: 'https://example.com/poster/invite_' + Date.now() + '.png',
+      expireTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
+    });
+  }
+
+  const { get } = await import('@/utils/request');
+  return get('/invite/poster');
+}

+ 108 - 0
haha-admin-mp/src/mock/invite.ts

@@ -0,0 +1,108 @@
+/**
+ * 邀请功能 Mock 数据
+ */
+
+import { InviteRecord, InviteStatistics, ActivityInfo, InviteCodeResponse } from '@/api/invite';
+
+export const mockInviteCode: InviteCodeResponse = {
+  code: 'INV' + Math.random().toString(36).substring(2, 8).toUpperCase(),
+  inviteUrl: 'https://example.com/invite?code=INVITE123',
+  expireTime: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
+};
+
+export const mockInviteRecords: InviteRecord[] = [
+  {
+    id: 'inv_001',
+    inviteePhone: '138****8888',
+    inviteeName: '张三',
+    status: 'rewarded',
+    inviteTime: '2026-04-01 10:30:00',
+    activateTime: '2026-04-01 14:20:00',
+    rewardTime: '2026-04-02 09:00:00',
+    couponName: '满减优惠券',
+    couponAmount: 20
+  },
+  {
+    id: 'inv_002',
+    inviteePhone: '139****6666',
+    inviteeName: '李四',
+    status: 'activated',
+    inviteTime: '2026-04-02 11:15:00',
+    activateTime: '2026-04-03 09:45:00'
+  },
+  {
+    id: 'inv_003',
+    inviteePhone: '137****5555',
+    status: 'pending',
+    inviteTime: '2026-04-03 08:00:00'
+  },
+  {
+    id: 'inv_004',
+    inviteePhone: '136****4444',
+    inviteeName: '王五',
+    status: 'rewarded',
+    inviteTime: '2026-03-28 16:30:00',
+    activateTime: '2026-03-29 10:15:00',
+    rewardTime: '2026-03-30 08:30:00',
+    couponName: '满减优惠券',
+    couponAmount: 20
+  },
+  {
+    id: 'inv_005',
+    inviteePhone: '135****3333',
+    status: 'failed',
+    inviteTime: '2026-03-25 14:20:00',
+    activateTime: '2026-03-26 11:00:00'
+  },
+  {
+    id: 'inv_006',
+    inviteePhone: '134****2222',
+    inviteeName: '赵六',
+    status: 'activated',
+    inviteTime: '2026-04-02 15:40:00',
+    activateTime: '2026-04-03 08:20:00'
+  },
+  {
+    id: 'inv_007',
+    inviteePhone: '133****1111',
+    status: 'pending',
+    inviteTime: '2026-04-03 10:00:00'
+  },
+  {
+    id: 'inv_008',
+    inviteePhone: '132****9999',
+    inviteeName: '钱七',
+    status: 'rewarded',
+    inviteTime: '2026-03-20 09:30:00',
+    activateTime: '2026-03-21 13:50:00',
+    rewardTime: '2026-03-22 10:00:00',
+    couponName: '满减优惠券',
+    couponAmount: 20
+  }
+];
+
+export const mockInviteStats: InviteStatistics = {
+  totalInvite: 8,
+  validInvite: 6,
+  rewardCount: 4,
+  pendingCount: 2,
+  totalRewardAmount: 80
+};
+
+export const mockActivityInfo: ActivityInfo = {
+  activityId: 'act_001',
+  title: '邀请好友得优惠券',
+  description: '邀请好友注册并完成首单,双方均可获得优惠券奖励',
+  couponName: '满减优惠券',
+  couponAmount: 20,
+  startTime: '2026-03-01 00:00:00',
+  endTime: '2026-06-30 23:59:59',
+  rules: [
+    '邀请好友注册并完成首单',
+    '您将获得一张20元满减优惠券',
+    '被邀请人也将获得一张新人优惠券',
+    '优惠券可用于购买指定商品抵扣',
+    '每个用户最多可获得10次邀请奖励'
+  ],
+  isActive: true
+};

+ 18 - 0
haha-admin-mp/src/pages.json

@@ -116,6 +116,24 @@
 				"navigationBarTextStyle": "black",
 				"navigationStyle": "custom"
 			}
+		},
+		{
+			"path": "pages/invite/index",
+			"style": {
+				"navigationBarTitleText": "邀请好友",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/invite/records",
+			"style": {
+				"navigationBarTitleText": "我的邀请",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
 		}
 	],
 	"globalStyle": {

+ 714 - 0
haha-admin-mp/src/pages/invite/index.vue

@@ -0,0 +1,714 @@
+<template>
+  <view class="page">
+    <NavBar title="邀请好友" :showBack="true" />
+
+    <view class="content">
+      <!-- 活动规则展示区 -->
+      <view class="activity-rules" v-if="activityInfo">
+        <view class="rules-header">
+          <view class="gift-icon"></view>
+          <text class="rules-title">{{ activityInfo.title }}</text>
+        </view>
+
+        <view class="rules-content">
+          <view
+            class="rule-item"
+            v-for="(rule, index) in activityInfo.rules"
+            :key="index"
+          >
+            <view class="rule-number">{{ index + 1 }}</view>
+            <text class="rule-text">{{ rule }}</text>
+          </view>
+        </view>
+
+        <view class="activity-time">
+          <view class="time-icon"></view>
+          <text>活动时间:{{ activityInfo.startTime }} ~ {{ activityInfo.endTime }}</text>
+        </view>
+      </view>
+
+      <!-- 邀请数据统计 -->
+      <view class="invite-stats" v-if="myStats">
+        <view class="stat-card">
+          <text class="stat-num">{{ myStats.totalInvite || 0 }}</text>
+          <text class="stat-label">累计邀请</text>
+        </view>
+        <view class="stat-divider"></view>
+        <view class="stat-card">
+          <text class="stat-num success">{{ myStats.validInvite || 0 }}</text>
+          <text class="stat-label">成功邀请</text>
+        </view>
+        <view class="stat-divider"></view>
+        <view class="stat-card">
+          <text class="stat-num primary">{{ myStats.rewardCount || 0 }}</text>
+          <text class="stat-label">获得奖励</text>
+        </view>
+      </view>
+
+      <!-- 邀请码展示 -->
+      <view class="invite-code-section" v-if="inviteCodeInfo">
+        <view class="code-header">
+          <text class="code-title">我的邀请码</text>
+          <text class="code-tip">分享给好友即可获得奖励</text>
+        </view>
+        <view class="code-display">
+          <text class="code-text">{{ inviteCodeInfo.code }}</text>
+          <button class="copy-btn" @click="copyInviteCode">复制</button>
+        </view>
+      </view>
+
+      <!-- 操作按钮区 -->
+      <view class="action-buttons">
+        <button class="btn-primary" @click="handleShare" :disabled="isSharing">
+          <view class="btn-icon share"></view>
+          <text>{{ isSharing ? '分享中...' : '立即邀请好友' }}</text>
+        </button>
+        <button class="btn-secondary" @click="handleGeneratePoster" :disabled="isGenerating">
+          <view class="btn-icon poster"></view>
+          <text>{{ isGenerating ? '生成中...' : '生成邀请海报' }}</text>
+        </button>
+        <button class="btn-link" @click="goToRecords">
+          查看我的邀请记录
+          <view class="arrow-right"></view>
+        </button>
+      </view>
+
+      <!-- 奖励说明 -->
+      <view class="reward-info" v-if="myStats && myStats.totalRewardAmount > 0">
+        <view class="reward-header">
+          <view class="reward-icon"></view>
+          <text class="reward-title">累计奖励</text>
+        </view>
+        <view class="reward-amount">
+          <text class="amount-symbol">¥</text>
+          <text class="amount-value">{{ myStats.totalRewardAmount }}</text>
+        </view>
+        <text class="reward-desc">已获得 {{ myStats.rewardCount }} 张优惠券</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import {
+  getInviteCode,
+  getMyInviteStatistics,
+  getActivityInfo,
+  generatePoster
+} from '@/api/invite';
+import type { InviteCodeResponse, InviteStatistics, ActivityInfo } from '@/api/invite';
+
+const inviteCodeInfo = ref<InviteCodeResponse | null>(null);
+const myStats = ref<InviteStatistics | null>(null);
+const activityInfo = ref<ActivityInfo | null>(null);
+const isSharing = ref(false);
+const isGenerating = ref(false);
+
+onMounted(() => {
+  loadActivityInfo();
+  loadStatistics();
+  loadInviteCode();
+});
+
+const loadActivityInfo = async () => {
+  try {
+    activityInfo.value = await getActivityInfo();
+  } catch (error) {
+    console.error('加载活动信息失败', error);
+  }
+};
+
+const loadStatistics = async () => {
+  try {
+    myStats.value = await getMyInviteStatistics();
+  } catch (error) {
+    console.error('加载统计数据失败', error);
+  }
+};
+
+const loadInviteCode = async () => {
+  try {
+    inviteCodeInfo.value = await getInviteCode();
+  } catch (error) {
+    console.error('获取邀请码失败', error);
+  }
+};
+
+const copyInviteCode = () => {
+  if (!inviteCodeInfo.value) return;
+
+  uni.setClipboardData({
+    data: inviteCodeInfo.value.code,
+    success: () => {
+      uni.showToast({
+        title: '邀请码已复制',
+        icon: 'success'
+      });
+    },
+    fail: () => {
+      uni.showToast({
+        title: '复制失败',
+        icon: 'none'
+      });
+    }
+  });
+};
+
+const handleShare = async () => {
+  if (!inviteCodeInfo.value) {
+    uni.showToast({
+      title: '请先获取邀请码',
+      icon: 'none'
+    });
+    return;
+  }
+
+  isSharing.value = true;
+
+  try {
+    // #ifdef MP-WEIXIN
+    uni.share({
+      provider: 'weixin',
+      scene: 'WXSceneSession',
+      type: 0,
+      title: `${activityInfo.value?.title || '邀请好友得奖励'}`,
+      summary: `我的邀请码:${inviteCodeInfo.value.code}`,
+      href: inviteCodeInfo.value.inviteUrl,
+      imageUrl: '',
+      success: () => {
+        uni.showToast({
+          title: '分享成功',
+          icon: 'success'
+        });
+      },
+      fail: (err: any) => {
+        // 如果分享失败,尝试使用小程序分享
+        console.log('share failed, try fallback', err);
+        showShareMenu();
+      }
+    });
+    // #endif
+
+    // #ifndef MP-WEIXIN
+    showShareMenu();
+    // #endif
+  } catch (error) {
+    console.error('分享失败', error);
+    uni.showToast({
+      title: '分享失败,请重试',
+      icon: 'none'
+    });
+  } finally {
+    setTimeout(() => {
+      isSharing.value = false;
+    }, 1000);
+  }
+};
+
+const showShareMenu = () => {
+  // 使用系统分享菜单
+  // #ifdef MP-WEIXIN
+  // 在微信小程序中,可以通过按钮 open-type="share" 触发
+  uni.showModal({
+    title: '分享提示',
+    content: `邀请码:${inviteCodeInfo.value?.code}\n\n请点击右上角菜单分享给好友`,
+    showCancel: false,
+    confirmText: '我知道了'
+  });
+  // #endif
+
+  // #ifndef MP-WEIXIN
+  uni.showActionSheet({
+    itemList: ['复制邀请链接', '复制邀请码'],
+    success: (res) => {
+      if (res.tapIndex === 0 && inviteCodeInfo.value) {
+        uni.setClipboardData({
+          data: inviteCodeInfo.value.inviteUrl,
+          success: () => {
+            uni.showToast({ title: '链接已复制', icon: 'success' });
+          }
+        });
+      } else if (res.tapIndex === 1) {
+        copyInviteCode();
+      }
+    }
+  });
+  // #endif
+};
+
+const handleGeneratePoster = async () => {
+  isGenerating.value = true;
+
+  try {
+    const result = await generatePoster();
+
+    // 预览海报图片
+    uni.previewImage({
+      urls: [result.posterUrl],
+      current: result.posterUrl
+    });
+
+    uni.showToast({
+      title: '海报生成成功',
+      icon: 'success'
+    });
+  } catch (error) {
+    console.error('生成海报失败', error);
+    uni.showToast({
+      title: '生成海报失败',
+      icon: 'none'
+    });
+  } finally {
+    isGenerating.value = false;
+  }
+};
+
+const goToRecords = () => {
+  uni.navigateTo({
+    url: '/pages/invite/records'
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+}
+
+.content {
+  padding: 24rpx;
+  padding-top: 0;
+}
+
+.activity-rules {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+  color: #ffffff;
+}
+
+.rules-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.gift-icon {
+  width: 48rpx;
+  height: 48rpx;
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 12rpx;
+  margin-right: 16rpx;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -60%);
+    width: 24rpx;
+    height: 16rpx;
+    border: 3rpx solid #ffffff;
+    border-radius: 4rpx 4rpx 0 0;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    top: 60%;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 4rpx;
+    height: 12rpx;
+    background: #ffffff;
+  }
+}
+
+.rules-title {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #ffffff;
+}
+
+.rules-content {
+  margin-bottom: 24rpx;
+}
+
+.rule-item {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 16rpx;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.rule-number {
+  width: 36rpx;
+  height: 36rpx;
+  background: rgba(255, 255, 255, 0.25);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22rpx;
+  font-weight: 600;
+  color: #ffffff;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+
+.rule-text {
+  font-size: 26rpx;
+  color: rgba(255, 255, 255, 0.95);
+  line-height: 36rpx;
+}
+
+.activity-time {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 20rpx;
+  background: rgba(255, 255, 255, 0.15);
+  border-radius: 10rpx;
+}
+
+.time-icon {
+  width: 28rpx;
+  height: 28rpx;
+  border: 2rpx solid rgba(255, 255, 255, 0.7);
+  border-radius: 6rpx;
+  margin-right: 12rpx;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 8rpx;
+    height: 8rpx;
+    background: rgba(255, 255, 255, 0.7);
+    border-radius: 50%;
+
+  }
+
+  text {
+    font-size: 24rpx;
+    color: rgba(255, 255, 255, 0.9);
+  }
+}
+
+.invite-stats {
+  background: #ffffff;
+  border-radius: 20rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+  display: flex;
+  align-items: center;
+  border: 1rpx solid #e2e8f0;
+}
+
+.stat-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.stat-num {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 8rpx;
+
+  &.success {
+    color: #10b981;
+  }
+
+  &.primary {
+    color: #3b82f6;
+  }
+}
+
+.stat-label {
+  font-size: 24rpx;
+  color: #64748b;
+}
+
+.stat-divider {
+  width: 1rpx;
+  height: 60rpx;
+  background: #e2e8f0;
+}
+
+.invite-code-section {
+  background: #ffffff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  margin-bottom: 24rpx;
+  border: 1rpx solid #e2e8f0;
+}
+
+.code-header {
+  margin-bottom: 20rpx;
+}
+
+.code-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1e293b;
+  display: block;
+  margin-bottom: 4rpx;
+}
+
+.code-tip {
+  font-size: 24rpx;
+  color: #94a3b8;
+}
+
+.code-display {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.code-text {
+  flex: 1;
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #3b82f6;
+  letter-spacing: 4rpx;
+  padding: 16rpx 24rpx;
+  background: #eff6ff;
+  border-radius: 12rpx;
+  text-align: center;
+}
+
+.copy-btn {
+  padding: 16rpx 32rpx;
+  background: #3b82f6;
+  border-radius: 12rpx;
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #ffffff;
+  border: none;
+
+  &:active {
+    opacity: 0.9;
+  }
+}
+
+.action-buttons {
+  margin-bottom: 24rpx;
+}
+
+.btn-primary {
+  width: 100%;
+  height: 96rpx;
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  border-radius: 48rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #ffffff;
+  border: none;
+  margin-bottom: 20rpx;
+
+  &[disabled] {
+    background: #cbd5e1;
+    color: #94a3b8;
+  }
+
+  &:active:not([disabled]) {
+    opacity: 0.9;
+  }
+}
+
+.btn-secondary {
+  width: 100%;
+  height: 96rpx;
+  background: #ffffff;
+  border: 2rpx solid #3b82f6;
+  border-radius: 48rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 30rpx;
+  font-weight: 500;
+  color: #3b82f6;
+  margin-bottom: 20rpx;
+
+  &[disabled] {
+    border-color: #cbd5e1;
+    color: #94a3b8;
+  }
+
+  &:active:not([disabled]) {
+    background: #eff6ff;
+  }
+}
+
+.btn-link {
+  width: 100%;
+  height: 88rpx;
+  background: transparent;
+  border: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  color: #64748b;
+
+  &:active {
+    color: #475569;
+  }
+}
+
+.btn-icon {
+  width: 32rpx;
+  height: 32rpx;
+  margin-right: 12rpx;
+
+  &.share {
+    border: 3rpx solid #ffffff;
+    border-radius: 50%;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: -8rpx;
+      right: -8rpx;
+      width: 14rpx;
+      height: 14rpx;
+      background: #ffffff;
+      border-radius: 50%;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: -4rpx;
+      right: -4rpx;
+      width: 18rpx;
+      height: 10rpx;
+      border-left: 3rpx solid #ffffff;
+      border-bottom: 3rpx solid #ffffff;
+      transform: rotate(-45deg);
+    }
+  }
+
+  &.poster {
+    border: 3rpx solid #3b82f6;
+    border-radius: 6rpx;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 12rpx;
+      height: 12rpx;
+      background: #3b82f6;
+      border-radius: 2rpx;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 5rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 3rpx;
+      height: 6rpx;
+      background: #3b82f6;
+    }
+  }
+}
+
+.arrow-right {
+  width: 12rpx;
+  height: 12rpx;
+  border-top: 3rpx solid #94a3b8;
+  border-right: 3rpx solid #94a3b8;
+  transform: rotate(45deg);
+  margin-left: 8rpx;
+}
+
+.reward-info {
+  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+  border-radius: 20rpx;
+  padding: 32rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  border: 1rpx solid #fcd34d;
+}
+
+.reward-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+
+.reward-icon {
+  width: 40rpx;
+  height: 40rpx;
+  background: #f59e0b;
+  border-radius: 50%;
+  margin-right: 12rpx;
+  position: relative;
+
+  &::before {
+    content: '¥';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 22rpx;
+    font-weight: 700;
+    color: #ffffff;
+  }
+}
+
+.reward-title {
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #92400e;
+}
+
+.reward-amount {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 8rpx;
+}
+
+.amount-symbol {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #d97706;
+  margin-right: 4rpx;
+}
+
+.amount-value {
+  font-size: 56rpx;
+  font-weight: 700;
+  color: #d97706;
+}
+
+.reward-desc {
+  font-size: 24rpx;
+  color: #a16207;
+}
+</style>

+ 720 - 0
haha-admin-mp/src/pages/invite/records.vue

@@ -0,0 +1,720 @@
+<template>
+  <view class="page">
+    <NavBar title="我的邀请" :showBack="true">
+      <template #right>
+        <view class="nav-action" @click="goToInvite">
+          <view class="invite-icon"></view>
+        </view>
+      </template>
+    </NavBar>
+
+    <view class="content">
+      <!-- 统计概览 -->
+      <view class="stats-header" v-if="statistics">
+        <view class="stat-card">
+          <text class="stat-num">{{ statistics.totalInvite || 0 }}</text>
+          <text class="stat-label">总邀请数</text>
+        </view>
+        <view class="stat-card success">
+          <text class="stat-num">{{ statistics.validInvite || 0 }}</text>
+          <text class="stat-label">已激活</text>
+        </view>
+        <view class="stat-card primary">
+          <text class="stat-num">{{ statistics.rewardCount || 0 }}</text>
+          <text class="stat-label">已获奖励</text>
+        </view>
+      </view>
+
+      <!-- 邀请记录列表 -->
+      <view class="list-section">
+        <view class="list-header">
+          <text class="list-title">邀请记录</text>
+          <text class="list-count">共 {{ total }} 条</text>
+        </view>
+
+        <scroll-view
+          scroll-y
+          class="records-list"
+          @scrolltolower="loadMore"
+          :refresher-enabled="true"
+          :refresher-triggered="isRefreshing"
+          @refresherrefresh="onRefresh"
+        >
+          <view class="record-list">
+            <view
+              class="record-item"
+              v-for="(item, index) in recordList"
+              :key="item.id"
+            >
+              <view class="record-left">
+                <view class="avatar-placeholder">
+                  <text class="avatar-text">{{ getAvatarText(item.inviteeName) }}</text>
+                </view>
+              </view>
+
+              <view class="record-content">
+                <view class="record-header">
+                  <text class="invitee-name">{{ item.inviteeName || '用户' + (index + 1) }}</text>
+                  <view class="status-tag" :class="getStatusClass(item.status)">
+                    {{ getStatusText(item.status) }}
+                  </view>
+                </view>
+
+                <view class="record-body">
+                  <view class="info-row">
+                    <view class="info-icon phone"></view>
+                    <text class="info-text">{{ maskPhone(item.inviteePhone) }}</text>
+                  </view>
+                  <view class="info-row">
+                    <view class="info-icon time"></view>
+                    <text class="info-text">邀请时间:{{ formatTime(item.inviteTime) }}</text>
+                  </view>
+                  <view class="info-row" v-if="item.activateTime">
+                    <view class="info-icon activate"></view>
+                    <text class="info-text">激活时间:{{ formatTime(item.activateTime) }}</text>
+                  </view>
+                  <view class="info-row" v-if="item.rewardTime">
+                    <view class="info-icon reward"></view>
+                    <text class="info-text">奖励发放:{{ formatTime(item.rewardTime) }}</text>
+                  </view>
+                </view>
+
+                <view class="record-reward" v-if="item.couponName && item.couponAmount">
+                  <view class="reward-badge">
+                    <text class="reward-amount">+¥{{ item.couponAmount }}</text>
+                    <text class="reward-name">{{ item.couponName }}</text>
+                  </view>
+                </view>
+              </view>
+            </view>
+
+            <!-- 空状态 -->
+            <view class="empty-state" v-if="!isLoading && recordList.length === 0">
+              <view class="empty-icon"></view>
+              <text class="empty-text">暂无邀请记录</text>
+              <text class="empty-tip">快去邀请好友吧!</text>
+              <button class="empty-btn" @click="goToInvite">立即邀请</button>
+            </view>
+
+            <!-- 加载更多 -->
+            <view class="loading-more" v-if="isLoadingMore">
+              <view class="loading-spinner"></view>
+              <text class="loading-text">加载中...</text>
+            </view>
+
+            <view class="no-more" v-if="!hasMore && recordList.length > 0">
+              <text>没有更多了</text>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import { getMyInviteRecords, getMyInviteStatistics, InviteStatus, InviteStatusLabel } from '@/api/invite';
+import type { InviteRecord, InviteStatistics, InviteQueryParams } from '@/api/invite';
+
+const statistics = ref<InviteStatistics | null>(null);
+const recordList = ref<InviteRecord[]>([]);
+const total = ref(0);
+const page = ref(1);
+const pageSize = 10;
+const isLoading = ref(false);
+const isLoadingMore = ref(false);
+const isRefreshing = ref(false);
+const hasMore = ref(true);
+
+onMounted(() => {
+  loadStatistics();
+  loadData();
+});
+
+const loadStatistics = async () => {
+  try {
+    statistics.value = await getMyInviteStatistics();
+  } catch (error) {
+    console.error('加载统计数据失败', error);
+  }
+};
+
+const loadData = async (reset = false) => {
+  if (reset) {
+    page.value = 1;
+    hasMore.value = true;
+  }
+
+  if (isLoading.value) return;
+  isLoading.value = true;
+
+  try {
+    const params: InviteQueryParams = {
+      page: page.value,
+      pageSize
+    };
+
+    const result = await getMyInviteRecords(params);
+
+    if (reset) {
+      recordList.value = result.list;
+    } else {
+      recordList.value = [...recordList.value, ...result.list];
+    }
+
+    total.value = result.total;
+    hasMore.value = recordList.value.length < result.total;
+  } catch (error) {
+    console.error('加载邀请记录失败', error);
+  } finally {
+    isLoading.value = false;
+    isRefreshing.value = false;
+    isLoadingMore.value = false;
+  }
+};
+
+const loadMore = () => {
+  if (!hasMore.value || isLoadingMore.value) return;
+
+  isLoadingMore.value = true;
+  page.value++;
+  loadData();
+};
+
+const onRefresh = () => {
+  isRefreshing.value = true;
+  loadData(true);
+  loadStatistics();
+};
+
+const maskPhone = (phone: string) => {
+  if (!phone || phone.length < 7) return phone;
+
+  // 如果已经是脱敏格式,直接返回
+  if (phone.includes('*')) return phone;
+
+  return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4);
+};
+
+const getStatusClass = (status: InviteStatus) => {
+  const classMap: Record<InviteStatus, string> = {
+    [InviteStatus.PENDING]: 'pending',
+    [InviteStatus.ACTIVATED]: 'activated',
+    [InviteStatus.REWARDED]: 'rewarded',
+    [InviteStatus.FAILED]: 'failed'
+  };
+  return classMap[status] || '';
+};
+
+const getStatusText = (status: InviteStatus) => {
+  const textMap: Record<InviteStatus, string> = {
+    [InviteStatus.PENDING]: InviteStatusLabel.PENDING,
+    [InviteStatus.ACTIVATED]: InviteStatusLabel.ACTIVATED,
+    [InviteStatus.REWARDED]: InviteStatusLabel.REWARDED,
+    [InviteStatus.FAILED]: InviteStatusLabel.FAILED
+  };
+  return textMap[status] || status;
+};
+
+const formatTime = (timeStr: string) => {
+  if (!timeStr) return '';
+  return timeStr.replace('T', ' ').substring(0, 19);
+};
+
+const getAvatarText = (name?: string) => {
+  if (!name) return '?';
+  return name.charAt(name.length - 1);
+};
+
+const goToInvite = () => {
+  uni.navigateTo({
+    url: '/pages/invite/index'
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+  display: flex;
+  flex-direction: column;
+}
+
+.nav-action {
+  width: 64rpx;
+  height: 64rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.invite-icon {
+  width: 36rpx;
+  height: 36rpx;
+  border: 3rpx solid #3b82f6;
+  border-radius: 50%;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: -6rpx;
+    right: -6rpx;
+    width: 14rpx;
+    height: 14rpx;
+    background: #3b82f6;
+    border-radius: 50%;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    bottom: -2rpx;
+    right: -2rpx;
+    width: 16rpx;
+    height: 10rpx;
+    border-left: 3rpx solid #3b82f6;
+    border-bottom: 3rpx solid #3b82f6;
+    transform: rotate(-45deg);
+  }
+}
+
+.content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.stats-header {
+  display: flex;
+  padding: 24rpx;
+  background: #ffffff;
+  gap: 20rpx;
+}
+
+.stat-card {
+  flex: 1;
+  background: #f8fafc;
+  border-radius: 16rpx;
+  padding: 24rpx 16rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  border: 1rpx solid #e2e8f0;
+
+  &.success {
+    background: #ecfdf5;
+    border-color: #a7f3d0;
+
+    .stat-num {
+      color: #059669;
+    }
+
+    .stat-label {
+      color: #047857;
+    }
+  }
+
+  &.primary {
+    background: #eff6ff;
+    border-color: #bfdbfe;
+
+    .stat-num {
+      color: #2563eb;
+    }
+
+    .stat-label {
+      color: #1d4ed8;
+    }
+  }
+}
+
+.stat-num {
+  font-size: 40rpx;
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 8rpx;
+}
+
+.stat-label {
+  font-size: 22rpx;
+  color: #64748b;
+}
+
+.list-section {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.list-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx 24rpx;
+  background: #ffffff;
+  border-top: 1rpx solid #e2e8f0;
+}
+
+.list-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1e293b;
+}
+
+.list-count {
+  font-size: 24rpx;
+  color: #94a3b8;
+}
+
+.records-list {
+  flex: 1;
+  height: 0;
+}
+
+.record-list {
+  padding: 0 24rpx;
+  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+}
+
+.record-item {
+  display: flex;
+  align-items: flex-start;
+  background: #ffffff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  border: 1rpx solid #e2e8f0;
+}
+
+.record-left {
+  margin-right: 20rpx;
+}
+
+.avatar-placeholder {
+  width: 80rpx;
+  height: 80rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar-text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #ffffff;
+}
+
+.record-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.record-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+
+.invitee-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1e293b;
+}
+
+.status-tag {
+  padding: 4rpx 16rpx;
+  border-radius: 8rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+  white-space: nowrap;
+  margin-left: 12rpx;
+
+  &.pending {
+    background: #f1f5f9;
+    color: #64748b;
+  }
+
+  &.activated {
+    background: #ecfdf5;
+    color: #059669;
+  }
+
+  &.rewarded {
+    background: #eff6ff;
+    color: #2563eb;
+  }
+
+  &.failed {
+    background: #fef2f2;
+    color: #dc2626;
+  }
+}
+
+.record-body {
+  margin-bottom: 12rpx;
+}
+
+.info-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8rpx;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.info-icon {
+  width: 26rpx;
+  height: 26rpx;
+  margin-right: 8rpx;
+  border-radius: 50%;
+
+  &.phone {
+    background: #e0f2fe;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 10rpx;
+      height: 16rpx;
+      border: 2rpx solid #0ea5e9;
+      border-radius: 3rpx;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 3rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 4rpx;
+      height: 2rpx;
+      background: #0ea5e9;
+      border-radius: 1rpx;
+    }
+  }
+
+  &.time {
+    background: #faf5ff;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 11rpx;
+      height: 11rpx;
+      border: 2rpx solid #a855f7;
+      border-radius: 50%;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      top: 7rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 2rpx;
+      height: 5rpx;
+      background: #a855f7;
+    }
+  }
+
+  &.activate {
+    background: #ecfdf5;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 12rpx;
+      height: 8rpx;
+      border: 2rpx solid #10b981;
+      border-radius: 2rpx;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 4rpx;
+      height: 4rpx;
+      background: #10b981;
+      border-radius: 50%;
+    }
+  }
+
+  &.reward {
+    background: #fffbeb;
+    position: relative;
+
+    &::before {
+      content: '¥';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      font-size: 14rpx;
+      font-weight: 700;
+      color: #f59e0b;
+    }
+  }
+}
+
+.info-text {
+  font-size: 24rpx;
+  color: #64748b;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.record-reward {
+  padding-top: 12rpx;
+  border-top: 1rpx solid #f1f5f9;
+}
+
+.reward-badge {
+  display: inline-flex;
+  align-items: center;
+  padding: 8rpx 16rpx;
+  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+  border-radius: 8rpx;
+  gap: 8rpx;
+}
+
+.reward-amount {
+  font-size: 26rpx;
+  font-weight: 700;
+  color: #d97706;
+}
+
+.reward-name {
+  font-size: 22rpx;
+  color: #a16207;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 100rpx 40rpx;
+}
+
+.empty-icon {
+  width: 160rpx;
+  height: 160rpx;
+  background: #f1f5f9;
+  border-radius: 50%;
+  margin-bottom: 32rpx;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -60%);
+    width: 56rpx;
+    height: 36rpx;
+    border: 4rpx solid #cbd5e1;
+    border-radius: 8rpx 8rpx 0 0;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    top: 58%;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 6rpx;
+    height: 18rpx;
+    background: #cbd5e1;
+  }
+}
+
+.empty-text {
+  font-size: 30rpx;
+  color: #64748b;
+  margin-bottom: 12rpx;
+}
+
+.empty-tip {
+  font-size: 26rpx;
+  color: #94a3b8;
+  margin-bottom: 32rpx;
+}
+
+.empty-btn {
+  padding: 20rpx 48rpx;
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  border-radius: 44rpx;
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #ffffff;
+  border: none;
+
+  &:active {
+    opacity: 0.9;
+  }
+}
+
+.loading-more {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24rpx;
+}
+
+.loading-spinner {
+  width: 32rpx;
+  height: 32rpx;
+  border: 3rpx solid #e2e8f0;
+  border-top-color: #10b981;
+  border-radius: 50%;
+  animation: rotate 0.8s linear infinite;
+
+  @keyframes rotate {
+    from { transform: rotate(0deg); }
+    to { transform: rotate(360deg); }
+  }
+}
+
+.loading-text {
+  font-size: 24rpx;
+  color: #94a3b8;
+  margin-left: 12rpx;
+}
+
+.no-more {
+  text-align: center;
+  padding: 24rpx;
+
+  text {
+    font-size: 24rpx;
+    color: #94a3b8;
+  }
+}
+</style>

+ 79 - 0
haha-admin-web/src/api/invite.ts

@@ -0,0 +1,79 @@
+import { http } from "@/utils/http";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+
+// ==================== 邀请活动管理 ====================
+
+export const getInviteActivityList = (params: any) => {
+  return http.request<ResultTable>("get", "/marketing/invite/activity/list", { params });
+};
+
+export const getInviteActivityDetail = (id: number) => {
+  return http.request<Result>("get", `/marketing/invite/activity/${id}`);
+};
+
+export const createInviteActivity = (data: any) => {
+  return http.request<Result>("post", "/marketing/invite/activity", { data });
+};
+
+export const updateInviteActivity = (id: number, data: any) => {
+  return http.request<Result>("put", `/marketing/invite/activity/${id}`, { data });
+};
+
+export const publishInviteActivity = (id: number) => {
+  return http.request<Result>("put", `/marketing/invite/activity/${id}/publish`);
+};
+
+export const pauseInviteActivity = (id: number) => {
+  return http.request<Result>("put", `/marketing/invite/activity/${id}/pause`);
+};
+
+export const resumeInviteActivity = (id: number) => {
+  return http.request<Result>("put", `/marketing/invite/activity/${id}/resume`);
+};
+
+export const deleteInviteActivity = (id: number) => {
+  return http.request<Result>("delete", `/marketing/invite/activity/${id}`);
+};
+
+// ==================== 数据统计 ====================
+
+export const getInviteStatistics = (activityId: number, params?: any) => {
+  return http.request<Result>("get", `/marketing/invite/statistics/${activityId}`, { params });
+};
+
+// ==================== 邀请记录管理 ====================
+
+export const getInviteRecords = (params: any) => {
+  return http.request<ResultTable>("get", "/marketing/invite/records", { params });
+};
+
+export const exportInviteRecords = (params: any) => {
+  return http.request<Result>("get", "/marketing/invite/records/export", {
+    params,
+    responseType: "blob"
+  });
+};
+
+// ==================== 奖励管理 ====================
+
+export const manualReissueReward = (rewardId: number) => {
+  return http.request<Result>("post", "/marketing/invite/reward/manual-reissue", {
+    params: { rewardId }
+  });
+};

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

@@ -1,3 +1,5 @@
+import Layout from "@/layout/index.vue";
+
 export default {
   path: "/marketing",
   redirect: "/marketing/activity",
@@ -76,6 +78,45 @@ export default {
           }
         }
       ]
+    },
+    {
+      path: "/marketing/invite",
+      component: Layout,
+      redirect: "/marketing/invite/index",
+      meta: {
+        icon: "ep:present",
+        title: "邀请活动",
+        rank: 4
+      },
+      children: [
+        {
+          path: "index",
+          name: "InviteActivity",
+          component: () => import("@/views/marketing/invite/index.vue"),
+          meta: {
+            icon: "ep:list",
+            title: "活动管理"
+          }
+        },
+        {
+          path: "statistics",
+          name: "InviteStatistics",
+          component: () => import("@/views/marketing/invite/statistics.vue"),
+          meta: {
+            icon: "ep:data-analysis",
+            title: "数据统计"
+          }
+        },
+        {
+          path: "records",
+          name: "InviteRecords",
+          component: () => import("@/views/marketing/invite/records.vue"),
+          meta: {
+            icon: "ep:document",
+            title: "邀请记录"
+          }
+        }
+      ]
     }
   ]
 } satisfies RouteConfigsTable;

+ 239 - 0
haha-admin-web/src/views/marketing/invite/index.vue

@@ -0,0 +1,239 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useInviteActivity } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+
+import Delete from "~icons/ep/delete";
+import EditPen from "~icons/ep/edit-pen";
+import Refresh from "~icons/ep/refresh";
+import AddFill from "~icons/ri/add-circle-line";
+import VideoPlay from "~icons/ep/video-play";
+import VideoPause from "~icons/ep/video-pause";
+import DataAnalysis from "~icons/ep/data-analysis";
+
+defineOptions({
+  name: "InviteActivity"
+});
+
+const formRef = ref();
+const tableRef = ref();
+const timeRange = ref([]);
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  onSearch,
+  resetForm,
+  handleAdd,
+  handleEdit,
+  handleViewDetail,
+  handleDelete,
+  handlePublish,
+  handlePause,
+  handleResume,
+  handleSizeChange,
+  handleCurrentChange
+} = useInviteActivity();
+</script>
+
+<template>
+  <div class="main">
+    <el-form
+      ref="formRef"
+      :inline="true"
+      :model="form"
+      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
+    >
+      <el-form-item label="活动名称:" prop="name">
+        <el-input
+          v-model="form.name"
+          placeholder="请输入活动名称"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="状态:" prop="status">
+        <el-select
+          v-model="form.status"
+          placeholder="请选择"
+          clearable
+          class="w-[180px]!"
+        >
+          <el-option label="草稿" :value="0" />
+          <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="timeRange">
+        <el-date-picker
+          v-model="timeRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          @change="(val) => {
+            if (val && val.length === 2) {
+              form.startTime = val[0];
+              form.endTime = val[1];
+            } else {
+              form.startTime = '';
+              form.endTime = '';
+            }
+          }"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon('ri/search-line')"
+          :loading="loading"
+          @click="onSearch"
+        >
+          搜索
+        </el-button>
+        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <PureTableBar
+      title="邀请活动管理"
+      :columns="columns"
+      @refresh="onSearch"
+    >
+      <template #buttons>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="handleAdd"
+        >
+          新增活动
+        </el-button>
+      </template>
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          align-whole="center"
+          showOverflowTooltip
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :pagination="{ ...pagination, size }"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @page-size-change="handleSizeChange"
+          @page-current-change="handleCurrentChange"
+        >
+          <template #operation="{ row }">
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(DataAnalysis)"
+              @click="$router.push(`/marketing/invite/statistics?id=${row.id}`)"
+            >
+              统计
+            </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon('ri:eye-line')"
+              @click="handleViewDetail(row)"
+            >
+              查看
+            </el-button>
+            <el-button
+              v-if="row.status === 0"
+              class="reset-margin"
+              link
+              type="success"
+              :size="size"
+              :icon="useRenderIcon(VideoPlay)"
+              @click="handlePublish(row)"
+            >
+              发布
+            </el-button>
+            <el-button
+              v-if="row.status === 2"
+              class="reset-margin"
+              link
+              type="warning"
+              :size="size"
+              :icon="useRenderIcon(VideoPause)"
+              @click="handlePause(row)"
+            >
+              暂停
+            </el-button>
+            <el-button
+              v-if="row.status === 3"
+              class="reset-margin"
+              link
+              type="success"
+              :size="size"
+              :icon="useRenderIcon(VideoPlay)"
+              @click="handleResume(row)"
+            >
+              恢复
+            </el-button>
+            <el-button
+              v-if="row.status === 0 || row.status === 3"
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="handleEdit(row)"
+            >
+              编辑
+            </el-button>
+            <el-popconfirm
+              title="是否确认删除?"
+              @confirm="handleDelete(row)"
+            >
+              <template #reference>
+                <el-button
+                  class="reset-margin"
+                  link
+                  type="danger"
+                  :size="size"
+                  :icon="useRenderIcon(Delete)"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-popconfirm>
+          </template>
+        </pure-table>
+      </template>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 308 - 0
haha-admin-web/src/views/marketing/invite/records.vue

@@ -0,0 +1,308 @@
+<script setup lang="ts">
+import { ref, onMounted, reactive } from "vue";
+import { getInviteRecords, exportInviteRecords, manualReissueReward } from "@/api/invite";
+import { message } from "@/utils/message";
+import type { InviteRecordsSearchParams } from "./utils/types";
+
+defineOptions({
+  name: "InviteRecords"
+});
+
+const formRef = ref();
+const loading = ref(false);
+const dataList = ref([]);
+const timeRange = ref([]);
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 搜索表单
+const form = reactive<InviteRecordsSearchParams>({
+  activityId: undefined,
+  inviterId: "",
+  status: undefined,
+  startTime: "",
+  endTime: ""
+});
+
+// 状态映射
+const statusMap: Record<number, { text: string; type: string }> = {
+  0: { text: "待激活", type: "info" },
+  1: { text: "已激活", type: "primary" },
+  2: { text: "奖励已发放", type: "success" },
+  3: { text: "发放失败", type: "danger" }
+};
+
+// 手机号脱敏
+function maskPhone(phone: string): string {
+  if (!phone) return "-";
+  return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
+}
+
+// 加载记录列表
+async function onSearch() {
+  loading.value = true;
+  try {
+    const params = {
+      page: pagination.currentPage,
+      pageSize: pagination.pageSize,
+      ...form
+    };
+
+    const { data } = await getInviteRecords(params);
+    if (data) {
+      dataList.value = data.list || [];
+      pagination.total = data.total || 0;
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 重置表单
+function resetForm(formEl) {
+  if (!formEl) return;
+  formEl.resetFields();
+  Object.assign(form, {
+    activityId: undefined,
+    inviterId: "",
+    status: undefined,
+    startTime: "",
+    endTime: ""
+  });
+  pagination.currentPage = 1;
+  onSearch();
+}
+
+// 分页大小变化
+function handleSizeChange(val: number) {
+  pagination.pageSize = val;
+  pagination.currentPage = 1;
+  onSearch();
+}
+
+// 当前页变化
+function handleCurrentChange(val: number) {
+  pagination.currentPage = val;
+  onSearch();
+}
+
+// 导出 Excel
+async function handleExport() {
+  try {
+    const params = { ...form };
+    const res = await exportInviteRecords(params);
+
+    // 创建下载链接
+    const blob = new Blob([res.data], {
+      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+    });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = `邀请记录_${new Date().getTime()}.xlsx`;
+    link.click();
+    window.URL.revokeObjectURL(url);
+
+    message("导出成功", { type: "success" });
+  } catch (error) {
+    message("导出失败", { type: "error" });
+  }
+}
+
+// 手动补发奖励
+async function handleReissue(row: any) {
+  try {
+    const { code } = await manualReissueReward(row.id);
+    if (code === 200) {
+      message("补发成功", { type: "success" });
+      onSearch();
+    }
+  } catch (error) {
+    message("补发失败", { type: "error" });
+  }
+}
+
+onMounted(() => {
+  onSearch();
+});
+</script>
+
+<template>
+  <div class="main">
+    <el-form
+      ref="formRef"
+      :inline="true"
+      :model="form"
+      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
+    >
+      <el-form-item label="活动ID:" prop="activityId">
+        <el-input-number
+          v-model="form.activityId"
+          placeholder="请输入活动ID"
+          :min="0"
+          controls-position="right"
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="邀请人ID:" prop="inviterId">
+        <el-input
+          v-model="form.inviterId"
+          placeholder="请输入邀请人ID或手机号"
+          clearable
+          class="w-[200px]!"
+        />
+      </el-form-item>
+      <el-form-item label="状态:" prop="status">
+        <el-select
+          v-model="form.status"
+          placeholder="请选择"
+          clearable
+          class="w-[150px]!"
+        >
+          <el-option label="待激活" :value="0" />
+          <el-option label="已激活" :value="1" />
+          <el-option label="奖励已发放" :value="2" />
+          <el-option label="发放失败" :value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间范围:" prop="timeRange">
+        <el-date-picker
+          v-model="timeRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          @change="(val) => {
+            if (val && val.length === 2) {
+              form.startTime = val[0];
+              form.endTime = val[1];
+            } else {
+              form.startTime = '';
+              form.endTime = '';
+            }
+          }"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="'ri/search-line'"
+          :loading="loading"
+          @click="onSearch"
+        >
+          搜索
+        </el-button>
+        <el-button :icon="'ep/refresh'" @click="resetForm(formRef)">
+          重置
+        </el-button>
+        <el-button
+          type="warning"
+          :icon="'ep/download'"
+          @click="handleExport"
+        >
+          导出 Excel
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 数据表格 -->
+    <div class="table-container bg-white p-4 mt-2">
+      <el-table
+        v-loading="loading"
+        :data="dataList"
+        border
+        stripe
+        style="width: 100%"
+        :header-cell-style="{
+          background: 'var(--el-fill-color-light)',
+          color: 'var(--el-text-color-primary)'
+        }"
+      >
+        <el-table-column label="记录ID" prop="id" width="80" align="center" />
+        <el-table-column label="活动名称" prop="activityName" min-width="140" align="center" show-overflow-tooltip />
+        <el-table-column label="邀请人ID" prop="inviterId" width="100" align="center" />
+        <el-table-column label="邀请人手机号(脱敏)" min-width="160" align="center">
+          <template #default="{ row }">
+            {{ maskPhone(row.inviterPhone) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="被邀请人ID" prop="inviteeId" width="100" align="center" />
+        <el-table-column label="被邀请人手机号(脱敏)" min-width="160" align="center">
+          <template #default="{ row }">
+            {{ maskPhone(row.inviteePhone) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="邀请码" prop="inviteCode" width="120" align="center" show-overflow-tooltip />
+        <el-table-column label="邀请时间" prop="inviteTime" width="170" align="center" />
+        <el-table-column label="激活时间" prop="activateTime" width="170" align="center">
+          <template #default="{ row }">
+            {{ row.activateTime || "-" }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" prop="status" width="110" align="center">
+          <template #default="{ row }">
+            <el-tag :type="statusMap[row.status]?.type || 'info'" size="small">
+              {{ statusMap[row.status]?.text || "未知" }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="奖励金额" prop="rewardAmount" width="100" align="center">
+          <template #default="{ row }">
+            <span v-if="row.rewardAmount">¥{{ row.rewardAmount }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" width="120" align="center">
+          <template #default="{ row }">
+            <el-button
+              v-if="row.status === 3"
+              link
+              type="primary"
+              size="small"
+              @click="handleReissue(row)"
+            >
+              补发奖励
+            </el-button>
+            <span v-else class="text-gray-400 text-sm">-</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="flex justify-end mt-4">
+        <el-pagination
+          v-model:current-page="pagination.currentPage"
+          v-model:page-size="pagination.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="pagination.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+
+      <!-- 空状态 -->
+      <el-empty v-if="!loading && dataList.length === 0" description="暂无数据" />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+
+.table-container {
+  border-radius: 4px;
+}
+</style>

+ 340 - 0
haha-admin-web/src/views/marketing/invite/statistics.vue

@@ -0,0 +1,340 @@
+<script setup lang="ts">
+import { ref, onMounted, computed } from "vue";
+import { useRoute } from "vue-router";
+import { getInviteStatistics } from "@/api/invite";
+import type { InviteStatisticsData } from "./utils/types";
+
+defineOptions({
+  name: "InviteStatistics"
+});
+
+const route = useRoute();
+const loading = ref(false);
+const statisticsData = ref<InviteStatisticsData | null>(null);
+
+// 时间范围选择
+const timeRange = ref("week");
+const customDateRange = ref<[string, string] | null>(null);
+
+// 图表相关
+const chartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: any = null;
+
+// 从路由参数获取活动ID
+const activityId = computed(() => {
+  return Number(route.query.id) || 0;
+});
+
+// 核心指标数据
+const metrics = computed(() => {
+  if (!statisticsData.value) {
+    return [
+      { title: "总邀请次数", value: 0, icon: "ep:user", color: "#409EFF" },
+      { title: "有效邀请数", value: 0, icon: "ep:check", color: "#67C23A" },
+      { title: "奖励发放总数", value: 0, icon: "ep:present", color: "#E6A23C" },
+      { title: "今日新增邀请数", value: 0, icon: "ep:trend-charts", color: "#F56C6C" },
+      { title: "转化率", value: "0%", icon: "ep:data-analysis", color: "#909399" },
+      { title: "奖励发放成功率", value: "0%", icon: "ep:circle-check", color: "#409EFF" }
+    ];
+  }
+
+  const data = statisticsData.value;
+  return [
+    { title: "总邀请次数", value: data.totalInvites || 0, icon: "ep:user", color: "#409EFF" },
+    { title: "有效邀请数", value: data.validInvites || 0, icon: "ep:check", color: "#67C23A" },
+    { title: "奖励发放总数", value: data.totalRewardsIssued || 0, icon: "ep:present", color: "#E6A23C" },
+    { title: "今日新增邀请数", value: data.todayNewInvites || 0, icon: "ep:trend-charts", color: "#F56C6C" },
+    {
+      title: "转化率",
+      value: data.conversionRate !== undefined ? `${(data.conversionRate * 100).toFixed(2)}%` : "0%",
+      icon: "ep:data-analysis",
+      color: "#909399"
+    },
+    {
+      title: "奖励发放成功率",
+      value: data.rewardSuccessRate !== undefined ? `${(data.rewardSuccessRate * 100).toFixed(2)}%` : "0%",
+      icon: "ep:circle-check",
+      color: "#409EFF"
+    }
+  ];
+});
+
+// 排行榜数据
+const rankingList = computed(() => {
+  if (!statisticsData.value?.rankingList) return [];
+  return statisticsData.value.rankingList;
+});
+
+// 加载统计数据
+async function loadStatistics() {
+  if (!activityId.value) {
+    // 如果没有活动ID,可以显示默认数据或提示用户选择活动
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const params: any = {};
+    if (timeRange.value !== "custom") {
+      params.period = timeRange.value; // day, week, month
+    }
+    if (customDateRange.value && customDateRange.value.length === 2) {
+      params.startDate = customDateRange.value[0];
+      params.endDate = customDateRange.value[1];
+    }
+
+    const { data } = await getInviteStatistics(activityId.value, params);
+    if (data) {
+      statisticsData.value = data as InviteStatisticsData;
+      renderChart();
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 渲染趋势图
+function renderChart() {
+  if (!chartRef.value) return;
+
+  // 动态导入 ECharts
+  import("echarts").then(echarts => {
+    if (chartInstance) {
+      chartInstance.dispose();
+    }
+
+    chartInstance = echarts.init(chartRef.value!);
+
+    const trendData = statisticsData.value?.trendData || [];
+    const dates = trendData.map(item => item.date);
+    const invites = trendData.map(item => item.invites);
+    const activations = trendData.map(item => item.activations);
+    const rewards = trendData.map(item => item.rewards);
+
+    const option = {
+      tooltip: {
+        trigger: "axis",
+        axisPointer: {
+          type: "cross"
+        }
+      },
+      legend: {
+        data: ["邀请数量", "激活数量", "奖励发放"],
+        bottom: 0
+      },
+      grid: {
+        left: "3%",
+        right: "4%",
+        bottom: "15%",
+        top: "10%",
+        containLabel: true
+      },
+      xAxis: {
+        type: "category",
+        boundaryGap: false,
+        data: dates.length > 0 ? dates : [],
+        axisLabel: {
+          rotate: dates.length > 7 ? 45 : 0
+        }
+      },
+      yAxis: {
+        type: "value"
+      },
+      series: [
+        {
+          name: "邀请数量",
+          type: "line",
+          smooth: true,
+          data: invites.length > 0 ? invites : [],
+          itemStyle: { color: "#409EFF" },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: "rgba(64, 158, 255, 0.3)" },
+              { offset: 1, color: "rgba(64, 158, 255, 0.05)" }
+            ])
+          }
+        },
+        {
+          name: "激活数量",
+          type: "line",
+          smooth: true,
+          data: activations.length > 0 ? activations : [],
+          itemStyle: { color: "#67C23A" },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: "rgba(103, 194, 58, 0.3)" },
+              { offset: 1, color: "rgba(103, 194, 58, 0.05)" }
+            ])
+          }
+        },
+        {
+          name: "奖励发放",
+          type: "line",
+          smooth: true,
+          data: rewards.length > 0 ? rewards : [],
+          itemStyle: { color: "#E6A23C" },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: "rgba(230, 162, 60, 0.3)" },
+              { offset: 1, color: "rgba(230, 162, 60, 0.05)" }
+            ])
+          }
+        }
+      ]
+    };
+
+    chartInstance.setOption(option);
+  });
+}
+
+function handleTimeRangeChange(value: string) {
+  timeRange.value = value;
+  loadStatistics();
+}
+
+onMounted(() => {
+  loadStatistics();
+
+  // 监听窗口大小变化
+  window.addEventListener("resize", () => {
+    chartInstance?.resize();
+  });
+});
+</script>
+
+<template>
+  <div class="invite-statistics">
+    <!-- 活动选择提示 -->
+    <el-alert
+      v-if="!activityId"
+      title="请先从活动列表页面进入统计页面"
+      type="warning"
+      show-icon
+      class="mb-4"
+    />
+
+    <!-- 时间范围选择器 -->
+    <div class="mb-4 flex items-center justify-between">
+      <div>
+        <span class="mr-2 text-sm text-gray-600">时间范围:</span>
+        <el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
+          <el-radio-button value="day">按日</el-radio-button>
+          <el-radio-button value="week">按周</el-radio-button>
+          <el-radio-button value="month">按月</el-radio-button>
+        </el-radio-group>
+      </div>
+      <el-button type="primary" :loading="loading" @click="loadStatistics">
+        刷新数据
+      </el-button>
+    </div>
+
+    <!-- 核心指标卡片 -->
+    <el-row :gutter="20" class="mb-5">
+      <el-col
+        v-for="(metric, index) in metrics"
+        :key="index"
+        :xs="24"
+        :sm="12"
+        :md="8"
+        :lg="4"
+      >
+        <el-card shadow="hover" class="metric-card">
+          <div class="flex items-center justify-between">
+            <div>
+              <div class="text-gray-500 text-sm mb-1">{{ metric.title }}</div>
+              <div class="text-2xl font-bold" :style="{ color: metric.color }">
+                {{ typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value }}
+              </div>
+            </div>
+            <div
+              class="w-12 h-12 rounded-full flex items-center justify-center"
+              :style="{ backgroundColor: metric.color + '20' }"
+            >
+              <el-icon :size="24" :color="metric.color">
+                <component :is="metric.icon" />
+              </el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 趋势图表 -->
+    <el-card class="mb-5">
+      <template #header>
+        <div class="flex items-center justify-between">
+          <span class="font-semibold">邀请数量趋势图</span>
+        </div>
+      </template>
+      <div ref="chartRef" style="width: 100%; height: 400px"></div>
+    </el-card>
+
+    <!-- 邀请达人排行榜 -->
+    <el-card>
+      <template #header>
+        <div class="flex items-center justify-between">
+          <span class="font-semibold">邀请达人排行榜</span>
+        </div>
+      </template>
+      <el-table :data="rankingList" stripe border style="width: 100%">
+        <el-table-column label="排名" width="80" align="center">
+          <template #default="{ $index }">
+            <el-tag
+              v-if="$index === 0"
+              type="danger"
+              size="small"
+              round
+            >
+              {{ $index + 1 }}
+            </el-tag>
+            <el-tag
+              v-else-if="$index === 1"
+              type="warning"
+              size="small"
+              round
+            >
+              {{ $index + 1 }}
+            </el-tag>
+            <el-tag
+              v-else-if="$index === 2"
+              size="small"
+              round
+            >
+              {{ $index + 1 }}
+            </el-tag>
+            <span v-else>{{ $index + 1 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="userId" label="用户ID" width="120" align="center" />
+        <el-table-column prop="userPhone" label="手机号(脱敏)" min-width="150" align="center">
+          <template #default="{ row }">
+            {{ row.userPhone ? row.userPhone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="inviteCount" label="邀请数量" width="120" align="center">
+          <template #default="{ row }">
+            <span class="font-semibold text-blue-600">{{ row.inviteCount || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 空状态 -->
+      <el-empty v-if="!loading && rankingList.length === 0" description="暂无排行榜数据" />
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.invite-statistics {
+  padding: 16px;
+}
+
+.metric-card {
+  margin-bottom: 12px;
+
+  &:hover {
+    transform: translateY(-2px);
+    transition: all 0.3s ease;
+  }
+}
+</style>

+ 429 - 0
haha-admin-web/src/views/marketing/invite/utils/hook.tsx

@@ -0,0 +1,429 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { addDialog } from "@/components/ReDialog";
+import {
+  getInviteActivityList,
+  getInviteActivityDetail,
+  createInviteActivity,
+  updateInviteActivity,
+  deleteInviteActivity,
+  publishInviteActivity,
+  pauseInviteActivity,
+  resumeInviteActivity
+} from "@/api/invite";
+import type { PaginationProps, TableColumnList } from "@pureadmin/table";
+import { deviceDetection } from "@pureadmin/utils";
+import { onMounted, reactive, ref, toRaw } from "vue";
+import {
+  ElForm,
+  ElInput,
+  ElFormItem,
+  ElDatePicker,
+  ElInputNumber,
+  ElDescriptions,
+  ElDescriptionsItem,
+  ElTag,
+  ElSelect,
+  ElOption
+} from "element-plus";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  resetPagination,
+  updatePaginationData
+} from "@/utils/paginationHelper";
+import type { InviteActivityFormProps, InviteActivitySearchFormProps } from "./types";
+
+interface SearchFormProps extends InviteActivitySearchFormProps {}
+
+export function useInviteActivity() {
+  const form = reactive<SearchFormProps>({
+    name: "",
+    status: undefined,
+    startTime: "",
+    endTime: ""
+  });
+  const loading = ref(true);
+  const dataList = ref([]);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  const statusMap: Record<number, { text: string; type: string }> = {
+    0: { text: "草稿", type: "info" },
+    1: { text: "已发布", type: "primary" },
+    2: { text: "进行中", type: "success" },
+    3: { text: "已暂停", type: "warning" },
+    4: { text: "已结束", type: "info" }
+  };
+
+  const rewardRuleTypeMap: Record<number, { text: string; type: string }> = {
+    1: { text: "固定奖励", type: "primary" },
+    2: { text: "阶梯奖励", type: "success" }
+  };
+
+  const columns: TableColumnList = [
+    {
+      label: "活动ID",
+      prop: "id",
+      width: 80
+    },
+    {
+      label: "活动名称",
+      prop: "activityName",
+      minWidth: 150
+    },
+    {
+      label: "活动类型",
+      prop: "activityType",
+      minWidth: 100,
+      cellRenderer: () => <ElTag type="warning">邀请有礼</ElTag>
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        const item = statusMap[row.status] || { text: "未知", type: "info" };
+        return <ElTag type={item.type}>{item.text}</ElTag>;
+      }
+    },
+    {
+      label: "时间范围",
+      prop: "timeRange",
+      minWidth: 280,
+      formatter: ({ startTime, endTime }) =>
+        `${dayjs(startTime).format("YYYY-MM-DD HH:mm")} ~ ${dayjs(endTime).format("YYYY-MM-DD HH:mm")}`
+    },
+    {
+      label: "总邀请数",
+      prop: "totalInviteCount",
+      width: 100,
+      formatter: ({ totalInviteCount }) => totalInviteCount || 0
+    },
+    {
+      label: "有效邀请数",
+      prop: "validInviteCount",
+      width: 100,
+      formatter: ({ validInviteCount }) => validInviteCount || 0
+    },
+    {
+      label: "奖励发放数",
+      prop: "rewardIssuedCount",
+      width: 100,
+      formatter: ({ rewardIssuedCount }) => rewardIssuedCount || 0
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : ""
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 320,
+      slot: "operation"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+
+      if (form.name) searchParams.activityName = form.name;
+      if (form.status !== undefined) searchParams.status = form.status;
+      if (form.startTime && form.endTime) {
+        searchParams.startTime = form.startTime;
+        searchParams.endTime = form.endTime;
+      }
+
+      const { data } = await getInviteActivityList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        updatePaginationData(pagination, data);
+      }
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function resetForm(formEl) {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  }
+
+  async function handleDelete(row) {
+    const { code } = await deleteInviteActivity(row.id);
+    if (code === 200) {
+      message("删除成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handlePublish(row) {
+    const { code } = await publishInviteActivity(row.id);
+    if (code === 200) {
+      message("发布成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handlePause(row) {
+    const { code } = await pauseInviteActivity(row.id);
+    if (code === 200) {
+      message("暂停成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handleResume(row) {
+    const { code } = await resumeInviteActivity(row.id);
+    if (code === 200) {
+      message("恢复成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  function handleViewDetail(row) {
+    addDialog({
+      title: "邀请活动详情",
+      width: "800px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => (
+        <div>
+          <ElDescriptions column={2} border>
+            <ElDescriptionsItem label="活动名称">{row.activityName}</ElDescriptionsItem>
+            <ElDescriptionsItem label="活动类型">
+              <ElTag type="warning">邀请有礼</ElTag>
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="状态">
+              <ElTag type={statusMap[row.status]?.type}>{statusMap[row.status]?.text}</ElTag>
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="奖励规则">
+              <ElTag type={rewardRuleTypeMap[row.rewardRuleType]?.type}>{rewardRuleTypeMap[row.rewardRuleType]?.text}</ElTag>
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="开始时间">{dayjs(row.startTime).format("YYYY-MM-DD HH:mm:ss")}</ElDescriptionsItem>
+            <ElDescriptionsItem label="结束时间">{dayjs(row.endTime).format("YYYY-MM-DD HH:mm:ss")}</ElDescriptionsItem>
+            <ElDescriptionsItem label="邀请人奖励">¥{row.inviterRewardAmount || 0}</ElDescriptionsItem>
+            <ElDescriptionsItem label="被邀请人奖励">¥{row.inviteeRewardAmount || 0}</ElDescriptionsItem>
+            <ElDescriptionsItem label="每人最大邀请次数">{row.maxInviteCount || "不限"}</ElDescriptionsItem>
+            <ElDescriptionsItem label="首充最低金额">¥{row.inviteeFirstChargeMinAmount || 0}</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}>
+              <ElDescriptionsItem label="总邀请数">{row.totalInviteCount || 0}</ElDescriptionsItem>
+              <ElDescriptionsItem label="有效邀请数">{row.validInviteCount || 0}</ElDescriptionsItem>
+              <ElDescriptionsItem label="奖励发放数">{row.rewardIssuedCount || 0}</ElDescriptionsItem>
+            </ElDescriptions>
+          </div>
+        </div>
+      )
+    });
+  }
+
+  function renderForm(formData: InviteActivityFormProps) {
+    return (
+      <div class="invite-form">
+        {/* 基本信息 */}
+        <div class="form-block">
+          <div class="block-title">基本信息</div>
+          <ElForm label-width="100px" size="default">
+            <div class="form-grid-2">
+              <ElFormItem label="活动名称" required>
+                <ElInput v-model={formData.activityName} placeholder="请输入活动名称" clearable maxlength={50} showWordLimit />
+              </ElFormItem>
+              <ElFormItem label="奖励规则类型" required>
+                <ElSelect v-model={formData.rewardRuleType} placeholder="请选择" class="w-full">
+                  <ElOption label="固定奖励" value={1} />
+                  <ElOption label="阶梯奖励" value={2} />
+                </ElSelect>
+              </ElFormItem>
+            </div>
+
+            <ElFormItem label="活动时间" required>
+              <div class="time-range">
+                <ElDatePicker v-model={formData.startTime} type="datetime" placeholder="开始时间" format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm:ss" />
+                <span class="time-sep">至</span>
+                <ElDatePicker v-model={formData.endTime} type="datetime" placeholder="结束时间" format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm:ss" />
+              </div>
+            </ElFormItem>
+
+            <ElFormItem label="活动说明">
+              <ElInput v-model={formData.activityDesc} type="textarea" rows={2} placeholder="请输入活动说明(可选)" maxlength={500} showWordLimit />
+            </ElFormItem>
+          </ElForm>
+        </div>
+
+        {/* 邀请规则 */}
+        <div class="form-block">
+          <div class="block-title">邀请规则</div>
+          <ElForm label-width="140px" size="default">
+            <div class="form-grid-2">
+              <ElFormItem label="邀请人奖励金额" required>
+                <ElInputNumber v-model={formData.inviterRewardAmount} min={0} precision={2} class="w-full" placeholder="每成功邀请一人获得的奖励" prefix="¥" />
+              </ElFormItem>
+              <ElFormItem label="被邀请人奖励金额" required>
+                <ElInputNumber v-model={formData.inviteeRewardAmount} min={0} precision={2} class="w-full" placeholder="被邀请人获得的奖励" prefix="¥" />
+              </ElFormItem>
+            </div>
+            <div class="form-grid-2">
+              <ElFormItem label="首充最低金额">
+                <ElInputNumber v-model={formData.inviteeFirstChargeMinAmount} min={0} precision={2} class="w-full" placeholder="被邀请人首次充值最低金额(元)" prefix="¥" />
+              </ElFormItem>
+              <ElFormItem label="每人最大邀请次数">
+                <ElInputNumber v-model={formData.maxInviteCount} min={1} class="w-full" placeholder="不填则不限制" />
+              </ElFormItem>
+            </div>
+          </ElForm>
+        </div>
+      </div>
+    );
+  }
+
+  function validateForm(formData: InviteActivityFormProps): boolean {
+    if (!formData.activityName) {
+      message("请输入活动名称", { type: "warning" });
+      return false;
+    }
+    if (!formData.rewardRuleType) {
+      message("请选择奖励规则类型", { type: "warning" });
+      return false;
+    }
+    if (!formData.startTime) {
+      message("请选择开始时间", { type: "warning" });
+      return false;
+    }
+    if (!formData.endTime) {
+      message("请选择结束时间", { type: "warning" });
+      return false;
+    }
+    if (!formData.inviterRewardAmount || formData.inviterRewardAmount <= 0) {
+      message("请输入有效的邀请人奖励金额", { type: "warning" });
+      return false;
+    }
+    if (!formData.inviteeRewardAmount || formData.inviteeRewardAmount <= 0) {
+      message("请输入有效的被邀请人奖励金额", { type: "warning" });
+      return false;
+    }
+    return true;
+  }
+
+  function handleEdit(row) {
+    const formData = reactive<InviteActivityFormProps>({
+      id: row.id,
+      activityName: row.activityName,
+      activityType: row.activityType || 10, // 固定为邀请有礼类型
+      activityDesc: row.activityDesc || "",
+      startTime: row.startTime,
+      endTime: row.endTime,
+      status: row.status,
+      maxInviteCount: row.maxInviteCount,
+      rewardRuleType: row.rewardRuleType || 1,
+      inviterRewardAmount: row.inviterRewardAmount,
+      inviteeRewardAmount: row.inviteeRewardAmount,
+      inviteeFirstChargeMinAmount: row.inviteeFirstChargeMinAmount
+    });
+
+    addDialog({
+      title: "编辑邀请活动",
+      width: "900px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => renderForm(formData),
+      beforeSure: async (done) => {
+        if (!validateForm(formData)) return;
+        try {
+          const { code } = await updateInviteActivity(formData.id!, toRaw(formData));
+          if (code === 200) {
+            message("编辑成功", { type: "success" });
+            done();
+            onSearch();
+          }
+        } catch (error) {
+          message("编辑失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  function handleAdd() {
+    const formData = reactive<InviteActivityFormProps>({
+      activityName: "",
+      activityType: 10, // 固定为邀请有礼类型
+      activityDesc: "",
+      startTime: "",
+      endTime: "",
+      status: 0,
+      maxInviteCount: undefined,
+      rewardRuleType: 1,
+      inviterRewardAmount: null,
+      inviteeRewardAmount: null,
+      inviteeFirstChargeMinAmount: undefined
+    });
+
+    addDialog({
+      title: "新增邀请活动",
+      width: "900px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => renderForm(formData),
+      beforeSure: async (done) => {
+        if (!validateForm(formData)) return;
+        try {
+          const { code } = await createInviteActivity(toRaw(formData));
+          if (code === 200) {
+            message("新增成功", { type: "success" });
+            done();
+            onSearch();
+          }
+        } catch (error) {
+          message("新增失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  function handleSizeChange(val: number) {
+    handlePageSizeChange(val, pagination, onSearch);
+  }
+
+  function handleCurrentChange(val: number) {
+    handleCurrentPageChange(val, pagination, onSearch);
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    statusMap,
+    rewardRuleTypeMap,
+    onSearch,
+    resetForm,
+    handleAdd,
+    handleEdit,
+    handleViewDetail,
+    handleDelete,
+    handlePublish,
+    handlePause,
+    handleResume,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}

+ 96 - 0
haha-admin-web/src/views/marketing/invite/utils/types.ts

@@ -0,0 +1,96 @@
+interface InviteActivityFormProps {
+  id?: number;
+  activityName: string;
+  activityType: number;
+  activityDesc?: string;
+  startTime: string;
+  endTime: string;
+  status: number;
+  // 邀请规则
+  maxInviteCount?: number; // 每人最大邀请次数
+  rewardRuleType: number; // 奖励规则类型:1-固定奖励 2-阶梯奖励
+  inviterRewardAmount?: number; // 邀请人奖励金额
+  inviteeRewardAmount?: number; // 被邀请人奖励金额
+  inviteeFirstChargeMinAmount?: number; // 被邀请人首充最低金额(元)
+  // 统计数据
+  totalInviteCount?: number; // 总邀请数
+  validInviteCount?: number; // 有效邀请数
+  rewardIssuedCount?: number; // 奖励发放数
+}
+
+interface InviteActivitySearchFormProps {
+  name?: string;
+  status?: number | undefined;
+  startTime?: string;
+  endTime?: string;
+}
+
+interface InviteActivityDetail extends InviteActivityFormProps {
+  id: number;
+  creatorId: number;
+  creatorName: string;
+  createTime: string;
+  updateTime: string;
+  totalBudget?: number;
+  usedBudget?: number;
+}
+
+// 邀请记录相关类型
+interface InviteRecordItem {
+  id: number;
+  activityId: number;
+  activityName: string;
+  inviterId: number;
+  inviterPhone: string;
+  inviteeId: number;
+  inviteePhone: string;
+  inviteCode: string;
+  inviteTime: string;
+  activateTime: string;
+  status: number; // 0-待激活 1-已激活 2-奖励已发放 3-发放失败
+  rewardAmount?: number;
+  createTime: string;
+}
+
+interface InviteRecordsSearchParams {
+  activityId?: number | undefined;
+  inviterId?: string;
+  status?: number | undefined;
+  startTime?: string;
+  endTime?: string;
+  page?: number;
+  pageSize?: number;
+}
+
+// 统计数据类型
+interface InviteStatisticsData {
+  totalInvites: number; // 总邀请次数
+  validInvites: number; // 有效邀请数
+  totalRewardsIssued: number; // 奖励发放总数
+  todayNewInvites: number; // 今日新增邀请数
+  conversionRate: number; // 转化率
+  rewardSuccessRate: number; // 奖励发放成功率
+  // 趋势数据
+  trendData?: Array<{
+    date: string;
+    invites: number;
+    activations: number;
+    rewards: number;
+  }>;
+  // 排行榜数据
+  rankingList?: Array<{
+    rank: number;
+    userId: number;
+    userPhone: string;
+    inviteCount: number;
+  }>;
+}
+
+export type {
+  InviteActivityFormProps,
+  InviteActivitySearchFormProps,
+  InviteActivityDetail,
+  InviteRecordItem,
+  InviteRecordsSearchParams,
+  InviteStatisticsData
+};

+ 250 - 0
haha-admin/src/main/java/com/haha/admin/controller/InviteActivityAdminController.java

@@ -0,0 +1,250 @@
+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.InviteActivity;
+import com.haha.entity.InviteRecord;
+import com.haha.entity.dto.InviteActivityCreateDTO;
+import com.haha.entity.dto.InviteActivityUpdateDTO;
+import com.haha.entity.dto.InviteRecordQueryDTO;
+import com.haha.service.InviteActivityService;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 邀请活动管理端控制器
+ * 提供邀请活动的CRUD管理、数据统计、邀请记录查询、奖励补发等功能
+ */
+@Slf4j
+@RestController
+@RequestMapping("/marketing/invite")
+@RequiredArgsConstructor
+public class InviteActivityAdminController {
+
+    private final InviteActivityService inviteActivityService;
+
+    // ==================== 活动管理 ====================
+
+    /**
+     * 创建邀请活动
+     *
+     * @param dto 创建请求DTO
+     * @return 创建的活动对象
+     */
+    @RequirePermission("marketing:invite:create")
+    @Log(module = "邀请活动", operation = OperationType.INSERT, summary = "创建邀请活动")
+    @PostMapping("/activity")
+    public Result<InviteActivity> createActivity(@RequestBody @Valid InviteActivityCreateDTO dto) {
+        Long creatorId = StpUtil.getLoginIdAsLong();
+        String creatorName = (String) StpUtil.getSession().get("name");
+        log.info("[邀请活动] 创建邀请活动 - creatorId: {}, activityName: {}", creatorId, dto.getActivityName());
+        InviteActivity activity = inviteActivityService.create(dto, creatorId, creatorName);
+        return Result.success("创建成功", activity);
+    }
+
+    /**
+     * 更新邀请活动
+     *
+     * @param id  活动ID
+     * @param dto 更新请求DTO
+     * @return 更新后的活动对象
+     */
+    @RequirePermission("marketing:invite:edit")
+    @Log(module = "邀请活动", operation = OperationType.UPDATE, summary = "编辑邀请活动")
+    @PutMapping("/activity/{id}")
+    public Result<InviteActivity> updateActivity(@PathVariable Long id,
+                                                  @RequestBody @Valid InviteActivityUpdateDTO dto) {
+        dto.setId(id);
+        log.info("[邀请活动] 更新邀请活动 - id: {}", id);
+        InviteActivity activity = inviteActivityService.update(dto);
+        return Result.success("更新成功", activity);
+    }
+
+    /**
+     * 获取邀请活动详情
+     *
+     * @param id 活动ID
+     * @return 活动详情
+     */
+    @RequirePermission("marketing:invite:read")
+    @GetMapping("/activity/{id}")
+    public Result<InviteActivity> getActivityDetail(@PathVariable Long id) {
+        log.info("[邀请活动] 查询活动详情 - id: {}", id);
+        InviteActivity activity = inviteActivityService.getDetail(id);
+        return Result.success("查询成功", activity);
+    }
+
+    /**
+     * 分页查询邀请活动列表
+     *
+     * @param queryDTO 查询条件(包含分页参数)
+     * @return 分页结果
+     */
+    @RequirePermission("marketing:invite:read")
+    @GetMapping("/activity/list")
+    public Result<PageResult<InviteActivity>> getActivityList(InviteRecordQueryDTO queryDTO) {
+        queryDTO.validate();
+        log.info("[邀请活动] 分页查询活动列表 - page: {}, pageSize: {}", queryDTO.getPage(), queryDTO.getPageSize());
+        IPage<InviteActivity> page = inviteActivityService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    /**
+     * 发布邀请活动(草稿状态 -> 已发布)
+     *
+     * @param id 活动ID
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:invite:publish")
+    @Log(module = "邀请活动", operation = OperationType.UPDATE, summary = "发布邀请活动")
+    @PutMapping("/activity/{id}/publish")
+    public Result<Void> publishActivity(@PathVariable Long id) {
+        log.info("[邀请活动] 发布活动 - id: {}", id);
+        boolean success = inviteActivityService.publish(id);
+        if (!success) {
+            return Result.error("发布失败,请检查活动状态");
+        }
+        return Result.success("发布成功", null);
+    }
+
+    /**
+     * 暂停邀请活动
+     *
+     * @param id 活动ID
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:invite:edit")
+    @Log(module = "邀请活动", operation = OperationType.UPDATE, summary = "暂停邀请活动")
+    @PutMapping("/activity/{id}/pause")
+    public Result<Void> pauseActivity(@PathVariable Long id) {
+        log.info("[邀请活动] 暂停活动 - id: {}", id);
+        boolean success = inviteActivityService.pause(id);
+        if (!success) {
+            return Result.error("暂停失败,请检查活动状态");
+        }
+        return Result.success("暂停成功", null);
+    }
+
+    /**
+     * 恢复邀请活动
+     *
+     * @param id 活动ID
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:invite:edit")
+    @Log(module = "邀请活动", operation = OperationType.UPDATE, summary = "恢复邀请活动")
+    @PutMapping("/activity/{id}/resume")
+    public Result<Void> resumeActivity(@PathVariable Long id) {
+        log.info("[邀请活动] 恢复活动 - id: {}", id);
+        boolean success = inviteActivityService.resume(id);
+        if (!success) {
+            return Result.error("恢复失败,请检查活动状态");
+        }
+        return Result.success("恢复成功", null);
+    }
+
+    /**
+     * 删除邀请活动(逻辑删除)
+     *
+     * @param id 活动ID
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:invite:delete")
+    @Log(module = "邀请活动", operation = OperationType.DELETE, summary = "删除邀请活动")
+    @DeleteMapping("/activity/{id}")
+    public Result<Void> deleteActivity(@PathVariable Long id) {
+        log.info("[邀请活动] 删除活动 - id: {}", id);
+        boolean success = inviteActivityService.deleteActivity(id);
+        if (!success) {
+            return Result.error("删除失败,请检查活动状态");
+        }
+        return Result.success("删除成功", null);
+    }
+
+    // ==================== 数据统计 ====================
+
+    /**
+     * 获取邀请活动统计数据
+     *
+     * @param activityId 活动ID
+     * @return 统计数据Map
+     */
+    @RequirePermission("marketing:invite:read")
+    @GetMapping("/statistics/{activityId}")
+    public Result<Map<String, Object>> getStatistics(@PathVariable Long activityId) {
+        log.info("[邀请活动] 查询活动统计 - activityId: {}", activityId);
+        Map<String, Object> statistics = inviteActivityService.getActivityStatistics(activityId);
+        return Result.success("查询成功", statistics);
+    }
+
+    // ==================== 邀请记录管理 ====================
+
+    /**
+     * 分页查询邀请记录列表
+     *
+     * @param queryDTO 查询条件(包含分页参数)
+     * @return 分页结果
+     */
+    @RequirePermission("marketing:invite:read")
+    @GetMapping("/records")
+    public Result<PageResult<InviteRecord>> getRecords(InviteRecordQueryDTO queryDTO) {
+        queryDTO.validate();
+        log.info("[邀请活动] 分页查询邀请记录 - page: {}, pageSize: {}, activityId: {}, status: {}",
+                queryDTO.getPage(), queryDTO.getPageSize(), queryDTO.getActivityId(), queryDTO.getStatus());
+        IPage<InviteRecord> page = inviteActivityService.getInviteRecords(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    /**
+     * 导出邀请记录Excel
+     *
+     * @param queryDTO 查询条件
+     * @param response HTTP响应
+     */
+    @RequirePermission("marketing:invite:export")
+    @Log(module = "邀请活动", operation = OperationType.EXPORT, summary = "导出邀请记录")
+    @GetMapping("/records/export")
+    public void exportRecords(InviteRecordQueryDTO queryDTO, HttpServletResponse response) {
+        log.info("[邀请活动] 导出邀请记录 - activityId: {}, status: {}", queryDTO.getActivityId(), queryDTO.getStatus());
+        // TODO: 实现Excel导出逻辑
+        // 1. 设置响应头:Content-Type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+        // 2. 设置文件名:UTF-8编码的 attachment;filename*=UTF-8''xxx.xlsx
+        // 3. 查询全量数据(不分页)
+        // 4. 使用EasyExcel写入响应输出流
+    }
+
+    // ==================== 奖励管理 ====================
+
+    /**
+     * 手动补发失败的奖励
+     * 管理员操作,用于处理发放失败的奖励记录
+     *
+     * @param rewardId 奖励记录ID
+     * @return 操作结果
+     */
+    @RequirePermission("marketing:invite:edit")
+    @Log(module = "邀请活动", operation = OperationType.UPDATE, summary = "手动补发邀请奖励")
+    @PostMapping("/reward/manual-reissue")
+    public Result<Void> manualReissueReward(@RequestParam Long rewardId) {
+        Long operatorId = StpUtil.getLoginIdAsLong();
+        String operatorName = (String) StpUtil.getSession().get("name");
+        log.info("[邀请活动] 手动补发奖励 - rewardId: {}, operatorId: {}, operatorName: {}",
+                rewardId, operatorId, operatorName);
+        boolean success = inviteActivityService.manualReissueReward(rewardId, operatorId, operatorName);
+        if (!success) {
+            return Result.error("补发失败,请检查奖励记录状态");
+        }
+        return Result.success("补发成功", null);
+    }
+}

+ 187 - 0
haha-admin/src/main/java/com/haha/admin/task/InviteTask.java

@@ -0,0 +1,187 @@
+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.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 邀请活动定时任务
+ * 负责自动更新活动状态、重试失败奖励、数据统计汇总等定时任务
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class InviteTask {
+
+    private final InviteActivityService inviteActivityService;
+    private final InviteActivityMapper inviteActivityMapper;
+    private final InviteRewardMapper inviteRewardMapper;
+
+    /**
+     * 邀请活动状态自动更新任务
+     * 每分钟执行一次,检查活动的开始/结束时间,自动更新状态
+     * - 草稿/已发布 + 开始时间到了 → 进行中
+     * - 进行中 + 结束时间到了 → 已结束
+     */
+    @Scheduled(cron = "0 * * * * ?")
+    public void updateActivityStatus() {
+        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());
+            }
+
+            log.info("[邀请定时任务] 活动状态更新任务执行完成,开始{}个,结束{}个",
+                     shouldStart.size(), shouldEnd.size());
+
+        } catch (Exception e) {
+            log.error("[邀请定时任务] 活动状态更新任务执行失败", e);
+        }
+    }
+
+    /**
+     * 失败奖励重试任务
+     * 每30分钟执行一次,重试发放失败的奖励(最多重试3次)
+     */
+    @Scheduled(cron = "0 */30 * * * ?")
+    public void retryFailedRewards() {
+        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);
+                }
+            }
+
+            log.info("[邀请定时任务] 失败奖励重试任务执行完成,成功{}个,失败{}个",
+                     successCount, failCount);
+
+        } catch (Exception e) {
+            log.error("[邀请定时任务] 失败奖励重试任务执行失败", e);
+        }
+    }
+
+    /**
+     * 邀请数据汇总统计任务
+     * 每天凌晨1点执行,汇总前一天的数据到统计表(如果有的话)
+     */
+    @Scheduled(cron = "0 0 1 * * ?")
+    public void summarizeDailyStatistics() {
+        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());
+
+            for (InviteActivity activity : ongoingActivities) {
+                Map<String, Object> stats = inviteActivityMapper.selectStatisticsByActivityId(
+                    activity.getId(), startOfDay, endOfDay
+                );
+
+                log.info("[邀请定时任务] 活动[{}]昨日统计 - 总邀请: {}, 有效邀请: {}",
+                         activity.getActivityName(),
+                         stats.get("totalInvite"),
+                         stats.get("validInvite"));
+
+                // TODO: 如果有统计表,可以在这里写入每日统计数据
+            }
+
+            log.info("[邀请定时任务] 每日数据汇总统计任务执行完成");
+
+        } catch (Exception e) {
+            log.error("[邀请定时任务] 每日数据汇总统计任务执行失败", e);
+        }
+    }
+
+    /**
+     * 活动即将结束提醒任务
+     * 提前3天检查即将结束的活动,发送提醒通知给参与用户(可选)
+     */
+    @Scheduled(cron = "0 0 10 * * ?") // 每天10点执行
+    public void remindEndingActivities() {
+        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)
+            );
+
+            for (InviteActivity activity : endingSoon) {
+                long daysLeft = java.time.Duration.between(now, activity.getEndTime()).toDays();
+                log.info("[邀请定时任务] 活动[{}]将在{}天后结束",
+                         activity.getActivityName(), daysLeft);
+
+                // TODO: 发送站内信或模板消息提醒参与用户
+                // sendReminderNotification(activity, daysLeft);
+            }
+
+            if (!endingSoon.isEmpty()) {
+                log.info("[邀请定时任务] 发现{}个即将结束的活动", endingSoon.size());
+            }
+
+        } catch (Exception e) {
+            log.error("[邀请定时任务] 活动即将结束提醒任务执行失败", e);
+        }
+    }
+}

+ 91 - 0
haha-admin/src/main/resources/sql/marketing.sql

@@ -171,3 +171,94 @@ CREATE TABLE IF NOT EXISTS `t_marketing_statistics` (
   UNIQUE KEY `uk_activity_date` (`activity_id`, `stat_date`),
   KEY `idx_stat_date` (`stat_date`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='营销活动统计表';
+
+-- ============================================================
+-- 邀请有礼模块
+-- ============================================================
+
+-- 10. 邀请活动配置表
+CREATE TABLE IF NOT EXISTS `t_invite_activity` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_name` varchar(100) NOT NULL COMMENT '活动名称',
+  `activity_desc` varchar(500) DEFAULT NULL COMMENT '活动描述',
+  `start_time` datetime NOT NULL COMMENT '开始时间',
+  `end_time` datetime NOT NULL COMMENT '结束时间',
+  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束',
+  `coupon_template_id` bigint DEFAULT NULL COMMENT '奖励优惠券模板ID',
+  `daily_limit` int NOT NULL DEFAULT 10 COMMENT '每人每日最大邀请数',
+  `total_limit` int NOT NULL DEFAULT 50 COMMENT '每人活动期间最大邀请总数',
+  `require_first_order` tinyint NOT NULL DEFAULT 1 COMMENT '是否要求完成首单:0-否 1-是',
+  `min_order_amount` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '首单最低金额要求(0表示不限制)',
+  `invite_code_prefix` varchar(10) DEFAULT NULL COMMENT '邀请码前缀',
+  `apply_scope` tinyint NOT NULL DEFAULT 1 COMMENT '适用范围:1-全平台 2-指定门店',
+  `product_scope` tinyint NOT NULL DEFAULT 1 COMMENT '商品范围:1-全部商品 2-指定商品',
+  `total_invite_count` int NOT NULL DEFAULT 0 COMMENT '总邀请次数',
+  `valid_invite_count` int NOT NULL DEFAULT 0 COMMENT '有效邀请数(已激活)',
+  `reward_count` int NOT NULL DEFAULT 0 COMMENT '奖励发放总数',
+  `creator_id` bigint NOT NULL COMMENT '创建人ID',
+  `creator_name` varchar(50) DEFAULT NULL COMMENT '创建人姓名',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
+  PRIMARY KEY (`id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_time` (`start_time`, `end_time`),
+  KEY `idx_creator` (`creator_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邀请活动配置表';
+
+-- 11. 邀请记录表
+CREATE TABLE IF NOT EXISTS `t_invite_record` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_id` bigint NOT NULL COMMENT '邀请活动ID',
+  `inviter_user_id` bigint NOT NULL COMMENT '邀请人用户ID',
+  `invitee_user_id` bigint NOT NULL COMMENT '被邀请人用户ID',
+  `invite_code` varchar(32) NOT NULL COMMENT '邀请码',
+  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-待激活 1-已激活 2-奖励已发放 3-发放失败 4-已取消',
+  `invite_time` datetime NOT NULL COMMENT '邀请时间',
+  `activate_time` datetime DEFAULT NULL COMMENT '激活时间(首单完成时间)',
+  `source_channel` varchar(50) DEFAULT NULL COMMENT '来源渠道',
+  `device_info` varchar(200) DEFAULT NULL COMMENT '设备信息',
+  `ip_address` varchar(50) DEFAULT NULL COMMENT 'IP地址',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注(失败原因等)',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_activity` (`activity_id`),
+  KEY `idx_inviter` (`inviter_user_id`),
+  KEY `idx_invitee` (`invitee_user_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_invite_time` (`invite_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邀请记录表';
+
+-- 12. 邀请奖励发放记录表
+CREATE TABLE IF NOT EXISTS `t_invite_reward` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_id` bigint NOT NULL COMMENT '邀请活动ID',
+  `record_id` bigint NOT NULL COMMENT '关联的邀请记录ID',
+  `inviter_user_id` bigint NOT NULL COMMENT '邀请人用户ID',
+  `coupon_template_id` bigint NOT NULL COMMENT '优惠券模板ID',
+  `user_coupon_id` bigint DEFAULT NULL COMMENT '发放的用户优惠券ID',
+  `coupon_code` varchar(32) DEFAULT NULL COMMENT '优惠券编码',
+  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-待发放 1-已发放 2-发放失败 3-已补发',
+  `fail_reason` varchar(500) DEFAULT NULL COMMENT '失败原因',
+  `distribute_time` datetime DEFAULT NULL COMMENT '发放时间',
+  `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数',
+  `operator_id` bigint DEFAULT NULL COMMENT '手动补发操作人ID(自动发放时为空)',
+  `operator_name` varchar(50) DEFAULT NULL COMMENT '操作人姓名',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_activity` (`activity_id`),
+  KEY `idx_record` (`record_id`),
+  KEY `idx_inviter` (`inviter_user_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邀请奖励发放记录表';
+
+-- ============================================================
+-- 修改现有表结构
+-- ============================================================
+
+-- 为优惠券模板表添加邀请活动相关字段
+ALTER TABLE `t_coupon_template`
+  ADD COLUMN `invite_activity_id` bigint DEFAULT NULL COMMENT '关联的邀请活动ID(非空表示该券为邀请奖励券)' AFTER `activity_id`,
+  ADD COLUMN `is_invite_only` tinyint NOT NULL DEFAULT 0 COMMENT '是否仅限邀请奖励发放:0-否 1-是' AFTER `invite_activity_id`;

+ 32 - 0
haha-common/src/main/java/com/haha/common/constant/MarketingConstants.java

@@ -47,4 +47,36 @@ public final class MarketingConstants {
     public static final int MAX_DISCOUNT_PERCENT = 100;
 
     public static final int MIN_DISCOUNT_PERCENT = 1;
+
+    // ==================== 邀请活动相关常量 ====================
+
+    /**
+     * 邀请码前缀
+     */
+    public static final String INVITE_CODE_PREFIX = "INV";
+
+    /**
+     * 默认每日邀请上限
+     */
+    public static final int DEFAULT_DAILY_LIMIT = 10;
+
+    /**
+     * 默认总邀请上限
+     */
+    public static final int DEFAULT_TOTAL_LIMIT = 50;
+
+    /**
+     * 默认邀请码有效期(天)
+     */
+    public static final int DEFAULT_INVITE_CODE_EXPIRE_DAYS = 30;
+
+    /**
+     * Redis键:邀请码缓存
+     */
+    public static final String REDIS_KEY_INVITE_CODE = "invite:code:";
+
+    /**
+     * Redis键:邀请次数限制
+     */
+    public static final String REDIS_KEY_INVITE_LIMIT = "invite:limit:";
 }

+ 2 - 1
haha-common/src/main/java/com/haha/common/enums/ActivityTypeEnum.java

@@ -6,7 +6,8 @@ public enum ActivityTypeEnum {
 
     FIRST_ORDER_DISCOUNT(1, "首单立减", "primary"),
     DISCOUNT(2, "优惠折扣", "success"),
-    FULL_REDUCTION(3, "商品满减", "warning");
+    FULL_REDUCTION(3, "商品满减", "warning"),
+    INVITE_REWARD(4, "邀请有礼", "danger");
 
     private final int code;
     private final String label;

+ 91 - 0
haha-common/src/main/java/com/haha/common/enums/InviteRewardStatusEnum.java

@@ -0,0 +1,91 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+/**
+ * 奖励发放状态枚举
+ */
+public enum InviteRewardStatusEnum {
+
+    /**
+     * 待发放 - 等待发放
+     */
+    PENDING(0, "待发放", "warning"),
+
+    /**
+     * 已发放 - 发放成功
+     */
+    DISTRIBUTED(1, "已发放", "success"),
+
+    /**
+     * 发放失败 - 发放失败
+     */
+    FAILED(2, "发放失败", "danger"),
+
+    /**
+     * 已补发 - 手动补发成功
+     */
+    RETRIED(3, "已补发", "primary");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    InviteRewardStatusEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    /**
+     * 根据code获取状态标签
+     *
+     * @param code 状态码
+     * @return 状态标签
+     */
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (InviteRewardStatusEnum status : values()) {
+            if (status.code == code) {
+                return status.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    /**
+     * 根据code获取枚举对象
+     *
+     * @param code 状态码
+     * @return 枚举对象,如果不存在返回null
+     */
+    public static InviteRewardStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (InviteRewardStatusEnum status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 96 - 0
haha-common/src/main/java/com/haha/common/enums/InviteStatusEnum.java

@@ -0,0 +1,96 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+/**
+ * 邀请记录状态枚举
+ */
+public enum InviteStatusEnum {
+
+    /**
+     * 待激活 - 新用户注册但未完成首单
+     */
+    PENDING(0, "待激活", "info"),
+
+    /**
+     * 已激活 - 已完成首单,等待发放奖励
+     */
+    ACTIVATED(1, "已激活", "success"),
+
+    /**
+     * 奖励已发放 - 奖励已成功发放
+     */
+    REWARDED(2, "奖励已发放", "primary"),
+
+    /**
+     * 发放失败 - 奖励发放失败
+     */
+    FAILED(3, "发放失败", "danger"),
+
+    /**
+     * 已取消 - 已取消(如活动结束)
+     */
+    CANCELLED(4, "已取消", "warning");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    InviteStatusEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    /**
+     * 根据code获取状态标签
+     *
+     * @param code 状态码
+     * @return 状态标签
+     */
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (InviteStatusEnum status : values()) {
+            if (status.code == code) {
+                return status.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    /**
+     * 根据code获取枚举对象
+     *
+     * @param code 状态码
+     * @return 枚举对象,如果不存在返回null
+     */
+    public static InviteStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (InviteStatusEnum status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 112 - 0
haha-entity/src/main/java/com/haha/entity/InviteActivity.java

@@ -0,0 +1,112 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_invite_activity")
+public class InviteActivity implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String activityName;
+
+    private String activityDesc;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime startTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime endTime;
+
+    /**
+     * 状态:0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束
+     */
+    private Integer status;
+
+    /**
+     * 奖励优惠券模板ID
+     */
+    private Long couponTemplateId;
+
+    /**
+     * 每人每日最大邀请数
+     */
+    private Integer dailyLimit;
+
+    /**
+     * 每人活动期间最大邀请总数
+     */
+    private Integer totalLimit;
+
+    /**
+     * 是否要求完成首单:0-否 1-是
+     */
+    private Integer requireFirstOrder;
+
+    /**
+     * 首单最低金额要求
+     */
+    private BigDecimal minOrderAmount;
+
+    /**
+     * 邀请码前缀
+     */
+    private String inviteCodePrefix;
+
+    /**
+     * 适用范围:1-全平台 2-指定门店
+     */
+    private Integer applyScope;
+
+    /**
+     * 商品范围:1-全部商品 2-指定商品
+     */
+    private Integer productScope;
+
+    /**
+     * 总邀请次数
+     */
+    private Integer totalInviteCount;
+
+    /**
+     * 有效邀请数
+     */
+    private Integer validInviteCount;
+
+    /**
+     * 奖励发放总数
+     */
+    private Integer rewardCount;
+
+    private Long creatorId;
+
+    private String creatorName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    private Integer deleted;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String applyScopeLabel;
+
+    @TableField(exist = false)
+    private String productScopeLabel;
+}

+ 89 - 0
haha-entity/src/main/java/com/haha/entity/InviteRecord.java

@@ -0,0 +1,89 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_invite_record")
+public class InviteRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 活动ID
+     */
+    private Long activityId;
+
+    /**
+     * 邀请人用户ID
+     */
+    private Long inviterUserId;
+
+    /**
+     * 被邀请人用户ID
+     */
+    private Long inviteeUserId;
+
+    /**
+     * 邀请码
+     */
+    private String inviteCode;
+
+    /**
+     * 状态:0-待激活 1-已激活 2-已失效 3-已奖励
+     */
+    private Integer status;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime inviteTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime activateTime;
+
+    /**
+     * 来源渠道
+     */
+    private String sourceChannel;
+
+    /**
+     * 设备信息
+     */
+    private String deviceInfo;
+
+    /**
+     * IP地址
+     */
+    private String ipAddress;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String inviterUserName;
+
+    @TableField(exist = false)
+    private String inviteeUserName;
+
+    @TableField(exist = false)
+    private String activityName;
+}

+ 93 - 0
haha-entity/src/main/java/com/haha/entity/InviteReward.java

@@ -0,0 +1,93 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_invite_reward")
+public class InviteReward implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 活动ID
+     */
+    private Long activityId;
+
+    /**
+     * 邀请记录ID
+     */
+    private Long recordId;
+
+    /**
+     * 邀请人用户ID
+     */
+    private Long inviterUserId;
+
+    /**
+     * 优惠券模板ID
+     */
+    private Long couponTemplateId;
+
+    /**
+     * 用户优惠券ID
+     */
+    private Long userCouponId;
+
+    /**
+     * 优惠券编码
+     */
+    private String couponCode;
+
+    /**
+     * 状态:0-待发放 1-已发放 2-发放失败
+     */
+    private Integer status;
+
+    /**
+     * 失败原因
+     */
+    private String failReason;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime distributeTime;
+
+    /**
+     * 重试次数
+     */
+    private Integer retryCount;
+
+    /**
+     * 操作人ID
+     */
+    private Long operatorId;
+
+    /**
+     * 操作人姓名
+     */
+    private String operatorName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String couponTemplateName;
+
+    @TableField(exist = false)
+    private String inviterUserName;
+}

+ 7 - 0
haha-entity/src/main/java/com/haha/entity/dto/CouponDistributeDTO.java

@@ -14,4 +14,11 @@ public class CouponDistributeDTO {
     private List<Long> userIds;
 
     private String remark;
+
+    private Long userId;
+
+    private String source;
+
+    private String sourceId;
+
 }

+ 74 - 0
haha-entity/src/main/java/com/haha/entity/dto/InviteActivityCreateDTO.java

@@ -0,0 +1,74 @@
+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;
+
+@Data
+public class InviteActivityCreateDTO {
+
+    @NotBlank(message = "活动名称不能为空")
+    private String activityName;
+
+    private String activityDesc;
+
+    @NotNull(message = "开始时间不能为空")
+    private LocalDateTime startTime;
+
+    @NotNull(message = "结束时间不能为空")
+    private LocalDateTime endTime;
+
+    @NotNull(message = "奖励优惠券模板ID不能为空")
+    private Long couponTemplateId;
+
+    /**
+     * 每人每日最大邀请数
+     */
+    private Integer dailyLimit;
+
+    /**
+     * 每人活动期间最大邀请总数
+     */
+    private Integer totalLimit;
+
+    /**
+     * 是否要求完成首单:0-否 1-是
+     */
+    private Integer requireFirstOrder;
+
+    /**
+     * 首单最低金额要求
+     */
+    private BigDecimal minOrderAmount;
+
+    /**
+     * 邀请码前缀
+     */
+    private String inviteCodePrefix;
+
+    /**
+     * 适用范围:1-全平台 2-指定门店
+     */
+    @NotNull(message = "适用范围不能为空")
+    private Integer applyScope;
+
+    /**
+     * 商品范围:1-全部商品 2-指定商品
+     */
+    @NotNull(message = "商品范围不能为空")
+    private Integer productScope;
+
+    /**
+     * 指定门店ID列表(applyScope=2时必填)
+     */
+    private List<Long> shopIds;
+
+    /**
+     * 指定商品ID列表(productScope=2时必填)
+     */
+    private List<Long> productIds;
+}

+ 45 - 0
haha-entity/src/main/java/com/haha/entity/dto/InviteActivityUpdateDTO.java

@@ -0,0 +1,45 @@
+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;
+
+@Data
+public class InviteActivityUpdateDTO {
+
+    @NotNull(message = "ID不能为空")
+    private Long id;
+
+    private String activityName;
+
+    private String activityDesc;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime endTime;
+
+    private Integer status;
+
+    private Long couponTemplateId;
+
+    private Integer dailyLimit;
+
+    private Integer totalLimit;
+
+    private Integer requireFirstOrder;
+
+    private BigDecimal minOrderAmount;
+
+    private String inviteCodePrefix;
+
+    private Integer applyScope;
+
+    private Integer productScope;
+
+    private List<Long> shopIds;
+
+    private List<Long> productIds;
+}

+ 42 - 0
haha-entity/src/main/java/com/haha/entity/dto/InviteRecordQueryDTO.java

@@ -0,0 +1,42 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class InviteRecordQueryDTO extends PageQueryDTO {
+
+    /**
+     * 活动ID
+     */
+    private Long activityId;
+
+    /**
+     * 邀请人用户ID
+     */
+    private Long inviterUserId;
+
+    /**
+     * 状态:0-待激活 1-已激活 2-已失效 3-已奖励
+     */
+    private Integer status;
+
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startDate;
+
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endDate;
+
+    /**
+     * 活动名称
+     */
+    private String activityName;
+
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/dto/InviteRewardQueryDTO.java

@@ -0,0 +1,24 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class InviteRewardQueryDTO extends PageQueryDTO {
+
+    /**
+     * 活动ID
+     */
+    private Long activityId;
+
+    /**
+     * 邀请记录ID
+     */
+    private Long recordId;
+
+    /**
+     * 状态:0-待发放 1-已发放 2-发放失败
+     */
+    private Integer status;
+}

+ 51 - 0
haha-mapper/src/main/java/com/haha/mapper/InviteActivityMapper.java

@@ -0,0 +1,51 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.InviteActivity;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface InviteActivityMapper extends BaseMapper<InviteActivity> {
+
+    // 查询所有未删除的邀请活动
+    @Select("SELECT * FROM t_invite_activity WHERE deleted = 0 ORDER BY create_time DESC")
+    List<InviteActivity> selectAll();
+
+    // 查询进行中的邀请活动
+    @Select("SELECT * FROM t_invite_activity WHERE status = 2 AND deleted = 0 AND start_time <= #{now} AND end_time >= #{now}")
+    List<InviteActivity> selectOngoing(@Param("now") LocalDateTime now);
+
+    // 更新活动状态
+    @Update("UPDATE t_invite_activity SET status = #{status}, update_time = NOW() WHERE id = #{id}")
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+
+    // 更新统计数据(总邀请数+1)
+    @Update("UPDATE t_invite_activity SET total_invite_count = total_invite_count + 1, update_time = NOW() WHERE id = #{id}")
+    int incrementTotalInviteCount(@Param("id") Long id);
+
+    // 更新统计数据(有效邀请数+1)
+    @Update("UPDATE t_invite_activity SET valid_invite_count = valid_invite_count + 1, update_time = NOW() WHERE id = #{id}")
+    int incrementValidInviteCount(@Param("id") Long id);
+
+    // 更新统计数据(奖励发放数+1)
+    @Update("UPDATE t_invite_activity SET reward_count = reward_count + 1, update_time = NOW() WHERE id = #{id}")
+    int incrementRewardCount(@Param("id") Long id);
+
+    // 统计查询:按活动ID统计各项指标
+    @Select("SELECT " +
+            "COUNT(*) as totalInvite, " +
+            "SUM(CASE WHEN status >= 1 THEN 1 ELSE 0 END) as validInvite, " +
+            "SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as rewardedCount, " +
+            "SUM(CASE WHEN invite_time >= #{startDate} AND invite_time <= #{endDate} THEN 1 ELSE 0 END) as todayInvite " +
+            "FROM t_invite_record WHERE activity_id = #{activityId} AND deleted = 0")
+    Map<String, Object> selectStatisticsByActivityId(@Param("activityId") Long activityId,
+                                                      @Param("startDate") LocalDateTime startDate,
+                                                      @Param("endDate") LocalDateTime endDate);
+}

+ 58 - 0
haha-mapper/src/main/java/com/haha/mapper/InviteRecordMapper.java

@@ -0,0 +1,58 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.InviteRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface InviteRecordMapper extends BaseMapper<InviteRecord> {
+
+    // 根据被邀请人ID查询是否已被邀请过
+    @Select("SELECT * FROM t_invite_record WHERE invitee_user_id = #{inviteeUserId} AND deleted = 0 LIMIT 1")
+    InviteRecord selectByInviteeUserId(@Param("inviteeUserId") Long inviteeUserId);
+
+    // 根据邀请码查询记录
+    @Select("SELECT * FROM t_invite_record WHERE invite_code = #{inviteCode} AND deleted = 0 LIMIT 1")
+    InviteRecord selectByInviteCode(@Param("inviteCode") String inviteCode);
+
+    // 更新记录状态为已激活
+    @Update("UPDATE t_invite_record SET status = 1, activate_time = NOW(), update_time = NOW() WHERE id = #{id}")
+    int updateToActivated(@Param("id") Long id);
+
+    // 更新记录状态为已奖励
+    @Update("UPDATE t_invite_record SET status = 2, update_time = NOW() WHERE id = #{id}")
+    int updateToRewarded(@Param("id") Long id);
+
+    // 统计某用户今日邀请数量
+    @Select("SELECT COUNT(*) FROM t_invite_record WHERE inviter_user_id = #{inviterUserId} AND activity_id = #{activityId} " +
+            "AND DATE(invite_time) = #{today} AND deleted = 0")
+    int countTodayInvites(@Param("inviterUserId") Long inviterUserId,
+                          @Param("activityId") Long activityId,
+                          @Param("today") LocalDate today);
+
+    // 统计某用户在活动中总邀请数量
+    @Select("SELECT COUNT(*) FROM t_invite_record WHERE inviter_user_id = #{inviterUserId} AND activity_id = #{activityId} AND deleted = 0")
+    int countTotalInvitesByUser(@Param("inviterUserId") Long inviterUserId, @Param("activityId") Long activityId);
+
+    // 查询某用户的所有邀请记录(分页)
+    @Select("SELECT * FROM t_invite_record WHERE inviter_user_id = #{inviterUserId} AND activity_id = #{activityId} AND deleted = 0 " +
+            "ORDER BY invite_time DESC LIMIT #{offset}, #{pageSize}")
+    List<InviteRecord> selectByInviterUserId(@Param("inviterUserId") Long inviterUserId,
+                                              @Param("activityId") Long activityId,
+                                              @Param("offset") Integer offset,
+                                              @Param("pageSize") Integer pageSize);
+
+    // 查询Top邀请达人
+    @Select("SELECT inviter_user_id, COUNT(*) as invite_count FROM t_invite_record " +
+            "WHERE activity_id = #{activityId} AND status >= 1 AND deleted = 0 " +
+            "GROUP BY inviter_user_id ORDER BY invite_count DESC LIMIT #{limit}")
+    List<Map<String, Object>> selectTopInviters(@Param("activityId") Long activityId, @Param("limit") Integer limit);
+}

+ 47 - 0
haha-mapper/src/main/java/com/haha/mapper/InviteRewardMapper.java

@@ -0,0 +1,47 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.InviteReward;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface InviteRewardMapper extends BaseMapper<InviteReward> {
+
+    // 根据邀请记录ID查询奖励记录
+    @Select("SELECT * FROM t_invite_reward WHERE record_id = #{recordId} AND deleted = 0 LIMIT 1")
+    InviteReward selectByRecordId(@Param("recordId") Long recordId);
+
+    // 更新奖励状态为已发放
+    @Update("UPDATE t_invite_reward SET status = 1, user_coupon_id = #{userCouponId}, coupon_code = #{couponCode}, " +
+            "distribute_time = NOW(), update_time = NOW() WHERE id = #{id}")
+    int updateToDistributed(@Param("id") Long id, @Param("userCouponId") Long userCouponId, @Param("couponCode") String couponCode);
+
+    // 更新奖励状态为发放失败
+    @Update("UPDATE t_invite_reward SET status = 2, fail_reason = #{failReason}, retry_count = retry_count + 1, update_time = NOW() WHERE id = #{id}")
+    int updateToFailed(@Param("id") Long id, @Param("failReason") String failReason);
+
+    // 更新奖励状态为已补发
+    @Update("UPDATE t_invite_reward SET status = 3, user_coupon_id = #{userCouponId}, coupon_code = #{couponCode}, " +
+            "operator_id = #{operatorId}, operator_name = #{operatorName}, distribute_time = NOW(), update_time = NOW() WHERE id = #{id}")
+    int updateToRetried(@Param("id") Long id, @Param("userCouponId") Long userCouponId,
+                        @Param("couponCode") String couponCode, @Param("operatorId") Long operatorId,
+                        @Param("operatorName") String operatorName);
+
+    // 查询待重试的失败奖励记录
+    @Select("SELECT * FROM t_invite_reward WHERE status = 2 AND retry_count < #{maxRetry} AND deleted = 0 ORDER BY create_time ASC LIMIT #{limit}")
+    List<InviteReward> selectFailedRewardsForRetry(@Param("maxRetry") Integer maxRetry, @Param("limit") Integer limit);
+
+    // 统计某活动的奖励发放情况
+    @Select("SELECT COUNT(*) as totalCount, " +
+            "SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as distributedCount, " +
+            "SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failedCount " +
+            "FROM t_invite_reward WHERE activity_id = #{activityId} AND deleted = 0")
+    Map<String, Object> selectRewardStatistics(@Param("activityId") Long activityId);
+}

+ 219 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/InviteController.java

@@ -0,0 +1,219 @@
+package com.haha.miniapp.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.InviteActivity;
+import com.haha.entity.InviteRecord;
+import com.haha.service.InviteActivityService;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 小程序端邀请活动控制器
+ * 提供邀请码获取、邀请关系绑定、我的邀请记录、邀请统计、海报生成等功能
+ */
+@Slf4j
+@RestController
+@RequestMapping("/invite")
+@RequiredArgsConstructor
+public class InviteController {
+
+    private final InviteActivityService inviteActivityService;
+
+    /**
+     * 获取当前用户的邀请码和链接
+     *
+     * 业务流程:
+     * 1. 获取当前登录用户ID
+     * 2. 查询当前进行中的邀请活动
+     * 3. 为该用户生成专属邀请码
+     * 4. 返回邀请码、邀请链接及活动信息
+     *
+     * @return 包含邀请码、邀请链接、活动信息的Map
+     */
+    @GetMapping("/code")
+    public Result<Map<String, Object>> getInviteCode() {
+        Long userId = StpUtil.getLoginIdAsLong();
+        log.info("[邀请] 用户请求邀请码 - userId: {}", userId);
+
+        InviteActivity ongoingActivity = inviteActivityService.getOngoingActivity();
+        if (ongoingActivity == null) {
+            log.info("[邀请] 当前无进行中的邀请活动 - userId: {}", userId);
+            return Result.error("暂无进行中的邀请活动");
+        }
+
+        String inviteCode = inviteActivityService.generateInviteCode(userId, ongoingActivity.getId());
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("inviteCode", inviteCode);
+        result.put("inviteLink", "pages/index/index?inviteCode=" + inviteCode);
+        result.put("activityInfo", ongoingActivity);
+
+        log.info("[邀请] 生成邀请码成功 - userId: {}, activityId: {}, inviteCode: {}",
+                userId, ongoingActivity.getId(), inviteCode);
+        return Result.success("查询成功", result);
+    }
+
+    /**
+     * 绑定邀请关系(新用户注册时调用)
+     *
+     * 业务流程:
+     * 1. 获取新注册用户的ID
+     * 2. 解析邀请码获取活动ID和邀请人信息
+     * 3. 建立邀请人与被邀请人的绑定关系
+     * 4. 记录来源渠道、设备信息等用于防刷检查
+     *
+     * @param params 包含 inviteCode(必填)、sourceChannel(可选)、deviceInfo(可选)
+     * @return 操作结果
+     */
+    @PostMapping("/bind")
+    public Result<Boolean> bindInviteRelation(@RequestBody Map<String, Object> params) {
+        Long userId = StpUtil.getLoginIdAsLong();
+
+        if (!params.containsKey("inviteCode")) {
+            return Result.error(400, "参数错误:inviteCode 不能为空");
+        }
+
+        String inviteCode = params.get("inviteCode").toString();
+        String sourceChannel = params.containsKey("sourceChannel") ? params.get("sourceChannel").toString() : "";
+        String deviceInfo = params.containsKey("deviceInfo") ? params.get("deviceInfo").toString() : "";
+
+        log.info("[邀请] 绑定邀请关系 - userId: {}, inviteCode: {}, sourceChannel: {}",
+                userId, inviteCode, sourceChannel);
+
+        // 通过邀请码查询对应的邀请记录,获取活动ID和邀请人ID
+        InviteRecord record = inviteActivityService.getInviteRecordByCode(inviteCode);
+
+        if (record == null) {
+            log.warn("[邀请] 邀请码无效或已过期 - userId: {}, inviteCode: {}", userId, inviteCode);
+            return Result.error(400, "邀请码无效或已过期");
+        }
+
+        boolean success = inviteActivityService.bindInviteRelation(
+                record.getActivityId(),
+                record.getInviterUserId(),
+                userId,
+                inviteCode,
+                sourceChannel,
+                deviceInfo,
+                ""  // IP地址由服务端获取
+        );
+
+        if (!success) {
+            return Result.error("绑定邀请关系失败");
+        }
+
+        log.info("[邀请] 绑定邀请关系成功 - userId: {}, inviterUserId: {}, inviteCode: {}",
+                userId, record.getInviterUserId(), inviteCode);
+        return Result.success("绑定成功", true);
+    }
+
+    /**
+     * 获取我的邀请记录列表
+     *
+     * 业务流程:
+     * 1. 获取当前登录用户ID
+     * 2. 查询当前进行中的邀请活动
+     * 3. 分页查询该用户作为邀请人的所有邀请记录
+     *
+     * @param page     页码(默认1)
+     * @param pageSize 每页条数(默认10)
+     * @return 分页的邀请记录列表
+     */
+    @GetMapping("/my-records")
+    public Result<PageResult<InviteRecord>> getMyInviteRecords(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        Long userId = StpUtil.getLoginIdAsLong();
+        log.info("[邀请] 查询我的邀请记录 - userId: {}, page: {}, pageSize: {}", userId, page, pageSize);
+
+        InviteActivity ongoingActivity = inviteActivityService.getOngoingActivity();
+        if (ongoingActivity == null) {
+            log.info("[邀请] 当前无进行中的邀请活动,返回空数据 - userId: {}", userId);
+            return Result.success("查询成功", new PageResult<>());
+        }
+
+        IPage<InviteRecord> records = inviteActivityService.getMyInviteRecords(
+                userId, ongoingActivity.getId(), page, pageSize);
+
+        log.info("[邀请] 我的邀请记录查询完成 - userId: {}, total: {}", userId, records.getTotal());
+        return Result.success("查询成功", PageResult.of(records));
+    }
+
+    /**
+     * 获取我的邀请统计数据
+     *
+     * 业务流程:
+     * 1. 获取当前登录用户ID
+     * 2. 查询当前进行中的邀请活动
+     * 3. 统计该用户的邀请数据(总邀请数、有效邀请数、今日邀请数等)
+     *
+     * @return 用户邀请统计数据
+     */
+    @GetMapping("/my-statistics")
+    public Result<Map<String, Object>> getMyInviteStatistics() {
+        Long userId = StpUtil.getLoginIdAsLong();
+        log.info("[邀请] 查询我的邀请统计 - userId: {}", userId);
+
+        InviteActivity ongoingActivity = inviteActivityService.getOngoingActivity();
+        if (ongoingActivity == null) {
+            log.info("[邀请] 当前无进行中的邀请活动,返回空数据 - userId: {}", userId);
+            return Result.success("查询成功", new HashMap<>());
+        }
+
+        Map<String, Object> statistics = inviteActivityService.getMyInviteStatistics(userId, ongoingActivity.getId());
+
+        log.info("[邀请] 我的邀请统计数据 - userId: {}, statistics: {}", userId, statistics);
+        return Result.success("查询成功", statistics);
+    }
+
+    /**
+     * 获取当前有效的邀请活动信息
+     *
+     * @return 进行中的邀请活动信息,若无则返回null
+     */
+    @GetMapping("/activity-info")
+    public Result<InviteActivity> getActivityInfo() {
+        log.info("[邀请] 查询当前邀请活动信息");
+        InviteActivity activity = inviteActivityService.getOngoingActivity();
+        return Result.success("查询成功", activity);
+    }
+
+    /**
+     * 生成邀请海报
+     *
+     * 业务流程:
+     * 1. 获取当前登录用户ID
+     * 2. 查询当前进行中的邀请活动
+     * 3. 生成该用户的专属邀请码
+     * 4. 返回海报相关数据(后续可接入Canvas绘制或第三方海报生成服务)
+     *
+     * @return 包含邀请码和海报URL的Map
+     */
+    @GetMapping("/poster")
+    public Result<Map<String, Object>> generatePoster() {
+        Long userId = StpUtil.getLoginIdAsLong();
+        log.info("[邀请] 生成邀请海报 - userId: {}", userId);
+
+        InviteActivity ongoingActivity = inviteActivityService.getOngoingActivity();
+        if (ongoingActivity == null) {
+            log.info("[邀请] 当前无进行中的邀请活动 - userId: {}", userId);
+            return Result.error("暂无进行中的邀请活动");
+        }
+
+        String inviteCode = inviteActivityService.generateInviteCode(userId, ongoingActivity.getId());
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("inviteCode", inviteCode);
+        result.put("posterUrl", "");
+
+        log.info("[邀请] 生成邀请海报成功 - userId: {}, inviteCode: {}", userId, inviteCode);
+        return Result.success("查询成功", result);
+    }
+}

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

@@ -0,0 +1,221 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.InviteActivity;
+import com.haha.entity.InviteRecord;
+import com.haha.entity.dto.InviteActivityCreateDTO;
+import com.haha.entity.dto.InviteActivityUpdateDTO;
+import com.haha.entity.dto.InviteRecordQueryDTO;
+
+import java.util.Map;
+
+/**
+ * 邀请活动服务接口
+ * 提供邀请活动的完整生命周期管理,包括活动CRUD、邀请码生成、邀请关系绑定、
+ * 邀请激活、奖励发放、防刷检查等功能
+ */
+public interface InviteActivityService extends IService<InviteActivity> {
+
+    // ==================== CRUD 操作 ====================
+
+    /**
+     * 分页查询邀请记录列表
+     *
+     * @param queryDTO 查询条件(包含分页参数)
+     * @return 分页结果
+     */
+    IPage<InviteActivity> getPage(InviteRecordQueryDTO queryDTO);
+
+    /**
+     * 获取邀请活动详情
+     *
+     * @param id 活动ID
+     * @return 活动详情
+     */
+    InviteActivity getDetail(Long id);
+
+    /**
+     * 创建邀请活动
+     *
+     * @param dto         创建请求DTO
+     * @param creatorId   创建人ID
+     * @param creatorName 创建人姓名
+     * @return 创建的活动对象
+     */
+    InviteActivity create(InviteActivityCreateDTO dto, Long creatorId, String creatorName);
+
+    /**
+     * 更新邀请活动
+     *
+     * @param dto 更新请求DTO
+     * @return 更新后的活动对象
+     */
+    InviteActivity update(InviteActivityUpdateDTO dto);
+
+    /**
+     * 发布活动(草稿状态 -> 已发布/进行中)
+     *
+     * @param id 活动ID
+     * @return 是否成功
+     */
+    boolean publish(Long id);
+
+    /**
+     * 暂停活动
+     *
+     * @param id 活动ID
+     * @return 是否成功
+     */
+    boolean pause(Long id);
+
+    /**
+     * 恢复活动
+     *
+     * @param id 活动ID
+     * @return 是否成功
+     */
+    boolean resume(Long id);
+
+    /**
+     * 删除活动(逻辑删除)
+     *
+     * @param id 活动ID
+     * @return 是否成功
+     */
+    boolean deleteActivity(Long id);
+
+    // ==================== 邀请码相关 ====================
+
+    /**
+     * 生成邀请码
+     * 格式:前缀 + 用户ID编码 + 活动ID编码 + 时间戳 + 4位随机数
+     * 使用Base62编码压缩长度,保证唯一性
+     *
+     * @param userId    邀请人用户ID
+     * @param activityId 活动ID
+     * @return 生成的邀请码
+     */
+    String generateInviteCode(Long userId, Long activityId);
+
+    /**
+     * 获取当前进行中的邀请活动
+     *
+     * @return 进行中的活动,如果没有返回null
+     */
+    InviteActivity getOngoingActivity();
+
+    // ==================== 邀请关系绑定 ====================
+
+    /**
+     * 绑定邀请关系
+     * 当被邀请人通过邀请码注册时调用此方法建立邀请关系
+     *
+     * @param activityId     活动ID
+     * @param inviterUserId  邀请人用户ID
+     * @param inviteeUserId  被邀请人用户ID
+     * @param inviteCode     邀请码
+     * @param sourceChannel  来源渠道(如:微信分享、二维码扫描等)
+     * @param deviceInfo     设备信息
+     * @param ipAddress      IP地址
+     * @return 是否绑定成功
+     */
+    boolean bindInviteRelation(Long activityId, Long inviterUserId, Long inviteeUserId,
+                               String inviteCode, String sourceChannel, String deviceInfo, String ipAddress);
+
+    // ==================== 邀请激活 ====================
+
+    /**
+     * 激活邀请记录
+     * 当被邀请人完成首单时调用此方法,将邀请记录从"待激活"变为"已激活"
+     *
+     * @param inviteeUserId 被邀请人用户ID
+     * @param orderId       订单ID
+     * @return 是否激活成功
+     */
+    boolean activateInviteRecord(Long inviteeUserId, Long orderId);
+
+    // ==================== 奖励发放 ====================
+
+    /**
+     * 发放奖励
+     * 根据邀请记录发放优惠券奖励给邀请人
+     *
+     * @param recordId 邀请记录ID
+     * @return 是否发放成功
+     */
+    boolean distributeReward(Long recordId);
+
+    // ==================== 统计查询 ====================
+
+    /**
+     * 分页查询邀请记录列表(管理端)
+     *
+     * @param queryDTO 查询条件(包含分页参数)
+     * @return 分页结果
+     */
+    IPage<InviteRecord> getInviteRecords(InviteRecordQueryDTO queryDTO);
+
+    /**
+     * 获取活动统计数据
+     *
+     * @param activityId 活动ID
+     * @return 统计数据Map(包含总邀请数、有效邀请数、今日邀请数等)
+     */
+    Map<String, Object> getActivityStatistics(Long activityId);
+
+    /**
+     * 获取当前用户的邀请统计
+     *
+     * @param userId     用户ID
+     * @param activityId 活动ID
+     * @return 用户邀请统计数据
+     */
+    Map<String, Object> getMyInviteStatistics(Long userId, Long activityId);
+
+    /**
+     * 分页查询当前用户的邀请记录列表
+     *
+     * @param userId     用户ID
+     * @param activityId 活动ID
+     * @param page       页码
+     * @param pageSize   每页条数
+     * @return 分页结果
+     */
+    IPage<InviteRecord> getMyInviteRecords(Long userId, Long activityId, Integer page, Integer pageSize);
+
+    // ==================== 防刷检查 ====================
+
+    /**
+     * 防刷检查
+     * 检查用户是否超过邀请限制,防止恶意刷邀请
+     *
+     * @param inviterUserId 邀请人用户ID
+     * @param activityId    活动ID
+     * @param ipAddress     IP地址
+     * @param deviceInfo    设备信息
+     * @return true-通过检查可以继续邀请;false-未通过检查,存在刷邀请嫌疑
+     */
+    boolean checkAntiFraud(Long inviterUserId, Long activityId, String ipAddress, String deviceInfo);
+
+    // ==================== 手动补发奖励 ====================
+
+    /**
+     * 手动补发失败的奖励
+     * 管理员操作,用于处理发放失败的奖励记录
+     *
+     * @param rewardId     奖励记录ID
+     * @param operatorId   操作人ID
+     * @param operatorName 操作人姓名
+     * @return 是否补发成功
+     */
+    boolean manualReissueReward(Long rewardId, Long operatorId, String operatorName);
+
+    /**
+     * 根据邀请码查询邀请记录
+     *
+     * @param inviteCode 邀请码
+     * @return 邀请记录,如果不存在返回null
+     */
+    InviteRecord getInviteRecordByCode(String inviteCode);
+}

+ 39 - 0
haha-service/src/main/java/com/haha/service/InviteNotificationService.java

@@ -0,0 +1,39 @@
+package com.haha.service;
+
+import java.math.BigDecimal;
+
+/**
+ * 邀请活动消息通知服务接口
+ * 定义邀请活动相关的各类通知方法,支持多种通知渠道扩展
+ */
+public interface InviteNotificationService {
+
+    /**
+     * 发送邀请奖励发放成功通知
+     * 当邀请人成功获得奖励(优惠券)时发送通知
+     *
+     * @param userId      用户ID(邀请人)
+     * @param couponName  优惠券名称
+     * @param amount      优惠券金额
+     */
+    void sendRewardSuccessNotification(Long userId, String couponName, BigDecimal amount);
+
+    /**
+     * 发送被邀请人注册成功通知(告知邀请人)
+     * 当有新用户通过邀请码注册时,通知邀请人
+     *
+     * @param inviterUserId   邀请人ID
+     * @param inviteeNickname 被邀请人昵称
+     */
+    void sendNewInviteeNotification(Long inviterUserId, String inviteeNickname);
+
+    /**
+     * 发送活动即将结束提醒
+     * 在活动即将结束时提醒参与用户抓紧时间完成邀请任务
+     *
+     * @param userId        参与用户ID
+     * @param activityName  活动名称
+     * @param daysLeft      剩余天数
+     */
+    void sendActivityEndingReminder(Long userId, String activityName, int daysLeft);
+}

+ 914 - 0
haha-service/src/main/java/com/haha/service/impl/InviteActivityServiceImpl.java

@@ -0,0 +1,914 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.enums.ActivityStatusEnum;
+import com.haha.common.enums.InviteRewardStatusEnum;
+import com.haha.common.enums.InviteStatusEnum;
+import com.haha.common.exception.BusinessException;
+import com.haha.common.vo.StatusLabel;
+import com.haha.entity.CouponTemplate;
+import com.haha.entity.InviteActivity;
+import com.haha.entity.InviteRecord;
+import com.haha.entity.InviteReward;
+import com.haha.entity.UserCoupon;
+import com.haha.entity.dto.CouponDistributeDTO;
+import com.haha.entity.dto.InviteActivityCreateDTO;
+import com.haha.entity.dto.InviteActivityUpdateDTO;
+import com.haha.entity.dto.InviteRecordQueryDTO;
+import com.haha.mapper.InviteActivityMapper;
+import com.haha.mapper.InviteRecordMapper;
+import com.haha.mapper.InviteRewardMapper;
+import com.haha.service.CouponTemplateService;
+import com.haha.service.InviteActivityService;
+import com.haha.service.InviteNotificationService;
+import com.haha.service.RedisService;
+import com.haha.service.UserCouponService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.security.SecureRandom;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 邀请活动服务实现类
+ * 实现邀请活动的完整业务逻辑,包括活动管理、邀请码生成、邀请关系绑定、
+ * 邀请激活、奖励发放、防刷机制等功能
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper, InviteActivity> implements InviteActivityService {
+
+    private final InviteActivityMapper inviteActivityMapper;
+    private final InviteRecordMapper inviteRecordMapper;
+    private final InviteRewardMapper inviteRewardMapper;
+    private final CouponTemplateService couponTemplateService;
+    private final UserCouponService userCouponService;
+    private final RedisService redisService;
+    private final StringRedisTemplate stringRedisTemplate;
+    private final InviteNotificationService notificationService;
+
+    /**
+     * Base62编码字符集,用于生成短邀请码
+     */
+    private static final String BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+    /**
+     * 随机数生成器,用于生成安全的随机数
+     */
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+    /**
+     * Redis防刷Key前缀:IP频率限制
+     */
+    private static final String REDIS_KEY_INVITE_IP_PREFIX = "invite:anti_fraud:ip:";
+
+    /**
+     * Redis防刷Key前缀:设备频率限制
+     */
+    private static final String REDIS_KEY_INVITE_DEVICE_PREFIX = "invite:anti_fraud:device:";
+
+    // ==================== CRUD 操作 ====================
+
+    @Override
+    public IPage<InviteActivity> getPage(InviteRecordQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<InviteActivity> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<InviteActivity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.hasText(queryDTO.getActivityName()), InviteActivity::getActivityName, queryDTO.getActivityName());
+        wrapper.eq(queryDTO.getStatus() != null, InviteActivity::getStatus, queryDTO.getStatus());
+        wrapper.ge(queryDTO.getStartDate() != null, InviteActivity::getStartTime, queryDTO.getStartDate());
+        wrapper.le(queryDTO.getEndDate() != null, InviteActivity::getEndTime, queryDTO.getEndDate());
+        wrapper.eq(InviteActivity::getDeleted, 0);
+        wrapper.orderByDesc(InviteActivity::getCreateTime);
+
+        IPage<InviteActivity> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillLabels);
+
+        return result;
+    }
+
+    @Override
+    public InviteActivity getDetail(Long id) {
+        InviteActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+        fillLabels(activity);
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public InviteActivity create(InviteActivityCreateDTO dto, Long creatorId, String creatorName) {
+        // 参数校验:结束时间必须大于开始时间
+        if (dto.getEndTime().isBefore(dto.getStartTime()) || dto.getEndTime().isEqual(dto.getStartTime())) {
+            throw new BusinessException(400, "结束时间必须大于开始时间");
+        }
+
+        // 校验优惠券模板是否存在且可用
+        CouponTemplate template = couponTemplateService.getById(dto.getCouponTemplateId());
+        if (template == null) {
+            throw new BusinessException(400, "优惠券模板不存在");
+        }
+
+        InviteActivity activity = new InviteActivity();
+        activity.setActivityName(dto.getActivityName());
+        activity.setActivityDesc(dto.getActivityDesc());
+        activity.setStartTime(dto.getStartTime());
+        activity.setEndTime(dto.getEndTime());
+        activity.setStatus(ActivityStatusEnum.DRAFT.getCode());
+        activity.setCouponTemplateId(dto.getCouponTemplateId());
+        activity.setDailyLimit(dto.getDailyLimit() != null ? dto.getDailyLimit() : 10);
+        activity.setTotalLimit(dto.getTotalLimit() != null ? dto.getTotalLimit() : 100);
+        activity.setRequireFirstOrder(dto.getRequireFirstOrder() != null ? dto.getRequireFirstOrder() : 1);
+        activity.setMinOrderAmount(dto.getMinOrderAmount() != null ? dto.getMinOrderAmount() : BigDecimal.ZERO);
+        activity.setInviteCodePrefix(StringUtils.hasText(dto.getInviteCodePrefix()) ? dto.getInviteCodePrefix() : "INV");
+        activity.setApplyScope(dto.getApplyScope());
+        activity.setProductScope(dto.getProductScope());
+        activity.setTotalInviteCount(0);
+        activity.setValidInviteCount(0);
+        activity.setRewardCount(0);
+        activity.setCreatorId(creatorId);
+        activity.setCreatorName(creatorName);
+        activity.setDeleted(0);
+
+        this.save(activity);
+
+        log.info("创建邀请活动成功: id={}, name={}, creator={}", activity.getId(), activity.getActivityName(), creatorName);
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public InviteActivity update(InviteActivityUpdateDTO dto) {
+        InviteActivity activity = this.getById(dto.getId());
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+
+        // 只有草稿状态允许编辑
+        if (!ActivityStatusEnum.canEdit(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许编辑");
+        }
+
+        // 参数校验:如果传了时间,需要校验
+        if (dto.getStartTime() != null && dto.getEndTime() != null) {
+            if (dto.getEndTime().isBefore(dto.getStartTime()) || dto.getEndTime().isEqual(dto.getStartTime())) {
+                throw new BusinessException(400, "结束时间必须大于开始时间");
+            }
+        }
+
+        if (dto.getActivityName() != null) {
+            activity.setActivityName(dto.getActivityName());
+        }
+        if (dto.getActivityDesc() != null) {
+            activity.setActivityDesc(dto.getActivityDesc());
+        }
+        if (dto.getStartTime() != null) {
+            activity.setStartTime(dto.getStartTime());
+        }
+        if (dto.getEndTime() != null) {
+            activity.setEndTime(dto.getEndTime());
+        }
+        if (dto.getCouponTemplateId() != null) {
+            activity.setCouponTemplateId(dto.getCouponTemplateId());
+        }
+        if (dto.getDailyLimit() != null) {
+            activity.setDailyLimit(dto.getDailyLimit());
+        }
+        if (dto.getTotalLimit() != null) {
+            activity.setTotalLimit(dto.getTotalLimit());
+        }
+        if (dto.getRequireFirstOrder() != null) {
+            activity.setRequireFirstOrder(dto.getRequireFirstOrder());
+        }
+        if (dto.getMinOrderAmount() != null) {
+            activity.setMinOrderAmount(dto.getMinOrderAmount());
+        }
+        if (dto.getInviteCodePrefix() != null) {
+            activity.setInviteCodePrefix(dto.getInviteCodePrefix());
+        }
+        if (dto.getApplyScope() != null) {
+            activity.setApplyScope(dto.getApplyScope());
+        }
+        if (dto.getProductScope() != null) {
+            activity.setProductScope(dto.getProductScope());
+        }
+
+        this.updateById(activity);
+
+        log.info("更新邀请活动成功: id={}, name={}", activity.getId(), activity.getActivityName());
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean publish(Long id) {
+        InviteActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canPublish(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许发布");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        int newStatus = (now.isAfter(activity.getStartTime()) && now.isBefore(activity.getEndTime()))
+                ? ActivityStatusEnum.ONGOING.getCode()
+                : ActivityStatusEnum.PUBLISHED.getCode();
+
+        boolean result = inviteActivityMapper.updateStatus(id, newStatus) > 0;
+
+        if (result) {
+            log.info("发布邀请活动成功: id={}, status={}", id, newStatus);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean pause(Long id) {
+        InviteActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canPause(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许暂停");
+        }
+
+        boolean result = inviteActivityMapper.updateStatus(id, ActivityStatusEnum.PAUSED.getCode()) > 0;
+
+        if (result) {
+            log.info("暂停邀请活动成功: id={}", id);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean resume(Long id) {
+        InviteActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canResume(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许恢复");
+        }
+
+        boolean result = inviteActivityMapper.updateStatus(id, ActivityStatusEnum.ONGOING.getCode()) > 0;
+
+        if (result) {
+            log.info("恢复邀请活动成功: id={}", id);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteActivity(Long id) {
+        InviteActivity activity = this.getById(id);
+        if (activity == null) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canDelete(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许删除");
+        }
+
+        activity.setDeleted(1);
+        boolean result = this.updateById(activity);
+
+        if (result) {
+            log.info("删除邀请活动成功: id={}", id);
+        }
+        return result;
+    }
+
+    // ==================== 邀请码相关 ====================
+
+    @Override
+    public String generateInviteCode(Long userId, Long activityId) {
+        // 获取活动信息以获取前缀配置
+        InviteActivity activity = inviteActivityMapper.selectById(activityId);
+        if (activity == null) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+
+        String prefix = activity.getInviteCodePrefix();
+        LocalDateTime now = LocalDateTime.now();
+
+        // 构建邀请码组成部分
+        String timestamp = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        String userIdEncoded = encodeToBase62(userId);
+        String activityIdEncoded = encodeToBase62(activityId);
+        String randomPart = generateRandomString(4);
+
+        // 组装邀请码:前缀 + 日期 + 用户ID编码 + 活动ID编码 + 随机数
+        String inviteCode = String.format("%s%s%s%s%s",
+                prefix,
+                timestamp,
+                userIdEncoded,
+                activityIdEncoded,
+                randomPart);
+
+        log.info("生成邀请码成功: code={}, userId={}, activityId={}", inviteCode, userId, activityId);
+        return inviteCode;
+    }
+
+    @Override
+    public InviteActivity getOngoingActivity() {
+        LocalDateTime now = LocalDateTime.now();
+        List<InviteActivity> ongoingList = inviteActivityMapper.selectOngoing(now);
+        if (ongoingList == null || ongoingList.isEmpty()) {
+            return null;
+        }
+        // 返回第一个进行中的活动
+        InviteActivity activity = ongoingList.get(0);
+        fillLabels(activity);
+        return activity;
+    }
+
+    // ==================== 邀请关系绑定 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean bindInviteRelation(Long activityId, Long inviterUserId, Long inviteeUserId,
+                                      String inviteCode, String sourceChannel, String deviceInfo, String ipAddress) {
+        // 1. 参数校验
+        if (inviterUserId.equals(inviteeUserId)) {
+            throw new BusinessException(400, "不能邀请自己");
+        }
+
+        // 2. 检查被邀请人是否已被邀请过
+        InviteRecord existingRecord = inviteRecordMapper.selectByInviteeUserId(inviteeUserId);
+        if (existingRecord != null) {
+            log.warn("该用户已被邀请过: inviteeUserId={}, existingRecordId={}", inviteeUserId, existingRecord.getId());
+            throw new BusinessException(400, "该用户已参与过邀请活动");
+        }
+
+        // 3. 检查活动是否在进行中
+        InviteActivity activity = this.getById(activityId);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+        if (activity.getStatus() != ActivityStatusEnum.ONGOING.getCode()) {
+            throw new BusinessException(400, "邀请活动未在进行中");
+        }
+        LocalDateTime now = LocalDateTime.now();
+        if (now.isBefore(activity.getStartTime()) || now.isAfter(activity.getEndTime())) {
+            throw new BusinessException(400, "邀请活动不在有效期内");
+        }
+
+        // 4. 执行防刷检查
+        boolean antiFraudPass = checkAntiFraud(inviterUserId, activityId, ipAddress, deviceInfo);
+        if (!antiFraudPass) {
+            throw new BusinessException(400, "邀请频率过高,请稍后再试");
+        }
+
+        // 5. 创建邀请记录
+        InviteRecord record = new InviteRecord();
+        record.setActivityId(activityId);
+        record.setInviterUserId(inviterUserId);
+        record.setInviteeUserId(inviteeUserId);
+        record.setInviteCode(inviteCode);
+        record.setStatus(InviteStatusEnum.PENDING.getCode()); // 待激活
+        record.setInviteTime(LocalDateTime.now());
+        record.setSourceChannel(sourceChannel);
+        record.setDeviceInfo(deviceInfo);
+        record.setIpAddress(ipAddress);
+        record.setRemark("");
+
+        inviteRecordMapper.insert(record);
+
+        // 6. 更新活动的总邀请数+1
+        inviteActivityMapper.incrementTotalInviteCount(activityId);
+
+        // 7. 发送新好友注册通知给邀请人(异步,不影响主流程)
+        try {
+            notificationService.sendNewInviteeNotification(inviterUserId, inviteeUserId.toString());
+        } catch (Exception notifyEx) {
+            log.error("发送新好友注册通知失败(不影响业务): inviterUserId={}, error={}",
+                    inviterUserId, notifyEx.getMessage(), notifyEx);
+        }
+
+        log.info("绑定邀请关系成功: recordId={}, inviterUserId={}, inviteeUserId={}, inviteCode={}",
+                record.getId(), inviterUserId, inviteeUserId, inviteCode);
+        return true;
+    }
+
+    // ==================== 邀请激活 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean activateInviteRecord(Long inviteeUserId, Long orderId) {
+        // 1. 根据被邀请人ID查询待激活的邀请记录
+        LambdaQueryWrapper<InviteRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(InviteRecord::getInviteeUserId, inviteeUserId);
+        wrapper.eq(InviteRecord::getStatus, InviteStatusEnum.PENDING.getCode()); // 待激活状态
+        wrapper.orderByDesc(InviteRecord::getCreateTime);
+        wrapper.last("LIMIT 1");
+
+        InviteRecord record = inviteRecordMapper.selectOne(wrapper);
+        if (record == null) {
+            log.warn("未找到待激活的邀请记录: inviteeUserId={}", inviteeUserId);
+            return false;
+        }
+
+        // 2. 获取活动配置,检查是否要求首单
+        InviteActivity activity = this.getById(record.getActivityId());
+        if (activity == null) {
+            log.error("邀请记录关联的活动不存在: activityId={}", record.getActivityId());
+            return false;
+        }
+
+        // TODO: 这里可以添加订单校验逻辑,验证是否为首单以及金额是否达标
+        // 目前简化处理,直接激活
+
+        // 3. 更新邀请记录状态为已激活
+        int updateResult = inviteRecordMapper.updateToActivated(record.getId());
+        if (updateResult <= 0) {
+            log.error("更新邀请记录为已激活失败: recordId={}", record.getId());
+            return false;
+        }
+
+        // 4. 更新活动的有效邀请数+1
+        inviteActivityMapper.incrementValidInviteCount(record.getActivityId());
+
+        log.info("激活邀请记录成功: recordId={}, inviteeUserId={}, orderId={}",
+                record.getId(), inviteeUserId, orderId);
+
+        // 5. 异步发放奖励(或同步发放)
+        try {
+            distributeReward(record.getId());
+        } catch (Exception e) {
+            log.error("发放奖励失败(不影响激活结果): recordId={}, error={}",
+                    record.getId(), e.getMessage(), e);
+            // 奖励发放失败不影响激活结果,可以后续手动补发
+        }
+
+        return true;
+    }
+
+    // ==================== 奖励发放 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean distributeReward(Long recordId) {
+        // 1. 根据记录ID查询邀请记录
+        InviteRecord record = inviteRecordMapper.selectById(recordId);
+        if (record == null) {
+            log.error("邀请记录不存在: recordId={}", recordId);
+            return false;
+        }
+
+        // 2. 检查记录状态是否为已激活
+        if (record.getStatus() != InviteStatusEnum.ACTIVATED.getCode()) {
+            log.warn("邀请记录状态不正确,无法发放奖励: recordId={}, status={}", recordId, record.getStatus());
+            return false;
+        }
+
+        // 3. 检查是否已经发放过奖励
+        InviteReward existingReward = inviteRewardMapper.selectByRecordId(recordId);
+        if (existingReward != null && existingReward.getStatus() == InviteRewardStatusEnum.DISTRIBUTED.getCode()) {
+            log.warn("奖励已发放,不能重复发放: recordId={}, rewardId={}", recordId, existingReward.getId());
+            return false;
+        }
+
+        // 4. 获取活动配置的优惠券模板ID
+        InviteActivity activity = this.getById(record.getActivityId());
+        if (activity == null) {
+            log.error("活动不存在: activityId={}", record.getActivityId());
+            return false;
+        }
+
+        // 5. 创建待发放的奖励记录(先创建记录,再发放优惠券)
+        InviteReward reward = new InviteReward();
+        reward.setActivityId(record.getActivityId());
+        reward.setRecordId(recordId);
+        reward.setInviterUserId(record.getInviterUserId());
+        reward.setCouponTemplateId(activity.getCouponTemplateId());
+        reward.setStatus(InviteRewardStatusEnum.PENDING.getCode()); // 待发放
+        reward.setUserCouponId(null);
+        reward.setCouponCode(null);
+        reward.setFailReason(null);
+        reward.setDistributeTime(null);
+        reward.setRetryCount(0);
+        reward.setOperatorId(null);
+        reward.setOperatorName(null);
+
+        inviteRewardMapper.insert(reward);
+
+        // 6. 调用UserCouponService发放优惠券给邀请人
+        try {
+            CouponDistributeDTO distributeDTO = new CouponDistributeDTO();
+            distributeDTO.setUserId(record.getInviterUserId());
+            distributeDTO.setTemplateId(activity.getCouponTemplateId());
+            distributeDTO.setSource("邀请活动奖励");
+            distributeDTO.setSourceId(String.valueOf(recordId));
+
+            userCouponService.distributeCoupon(distributeDTO, null, "系统自动发放");
+
+            // TODO: 需要从distributeCoupon返回值中获取userCouponId和couponCode
+            // 这里暂时设置为占位值,实际应根据返回值更新
+            Long userCouponId = -1L; // 应从实际发放结果获取
+            String couponCode = "AUTO_" + System.currentTimeMillis(); // 应从实际发放结果获取
+
+            // 7. 更新奖励记录为已发放
+            inviteRewardMapper.updateToDistributed(reward.getId(), userCouponId, couponCode);
+
+            // 8. 更新邀请记录状态为已奖励
+            inviteRecordMapper.updateToRewarded(recordId);
+
+            // 9. 更新活动的奖励发放数+1
+            inviteActivityMapper.incrementRewardCount(record.getActivityId());
+
+            // 10. 发送消息通知
+            try {
+                // 获取优惠券模板信息用于通知
+                CouponTemplate couponTemplate = couponTemplateService.getById(activity.getCouponTemplateId());
+                if (couponTemplate != null) {
+                    notificationService.sendRewardSuccessNotification(
+                        record.getInviterUserId(),
+                        couponTemplate.getCouponName(),
+                        couponTemplate.getDiscountValue()
+                    );
+                }
+            } catch (Exception notifyEx) {
+                log.error("发送奖励通知失败(不影响业务): recordId={}, error={}",
+                        recordId, notifyEx.getMessage(), notifyEx);
+            }
+
+            log.info("发放邀请奖励成功: recordId={}, rewardId={}, inviterUserId={}",
+                    recordId, reward.getId(), record.getInviterUserId());
+            return true;
+
+        } catch (Exception e) {
+            // 发放失败,更新奖励状态为失败
+            log.error("发放优惠券失败: recordId={}, error={}", recordId, e.getMessage(), e);
+            inviteRewardMapper.updateToFailed(reward.getId(), "优惠券发放失败: " + e.getMessage());
+            return false;
+        }
+    }
+
+    // ==================== 防刷检查 ====================
+
+    @Override
+    public boolean checkAntiFraud(Long inviterUserId, Long activityId, String ipAddress, String deviceInfo) {
+        // 1. 获取活动配置
+        InviteActivity activity = this.getById(activityId);
+        if (activity == null) {
+            return false;
+        }
+
+        LocalDate today = LocalDate.now();
+
+        // 2. 检查今日邀请数量是否超过每日限制
+        int todayInviteCount = inviteRecordMapper.countTodayInvites(inviterUserId, activityId, today);
+        if (todayInviteCount >= activity.getDailyLimit()) {
+            log.warn("今日邀请数量已达上限: userId={}, activityId={}, todayCount={}, dailyLimit={}",
+                    inviterUserId, activityId, todayInviteCount, activity.getDailyLimit());
+            return false;
+        }
+
+        // 3. 检查总邀请数量是否超过总限制
+        int totalInviteCount = inviteRecordMapper.countTotalInvitesByUser(inviterUserId, activityId);
+        if (totalInviteCount >= activity.getTotalLimit()) {
+            log.warn("总邀请数量已达上限: userId={}, activityId{}, totalCount={}, totalLimit={}",
+                    inviterUserId, activityId, totalInviteCount, activity.getTotalLimit());
+            return false;
+        }
+
+        // 4. 使用Redis检查IP频率限制(同一IP在1小时内最多邀请50次)
+        if (StringUtils.hasText(ipAddress)) {
+            String ipKey = REDIS_KEY_INVITE_IP_PREFIX + ipAddress;
+            Long ipCount = stringRedisTemplate.opsForValue().increment(ipKey);
+            if (ipCount != null && ipCount == 1) {
+                // 第一次设置,过期时间1小时
+                stringRedisTemplate.expire(ipKey, 1, TimeUnit.HOURS);
+            }
+            if (ipCount != null && ipCount > 50) {
+                log.warn("IP邀请频率过高: ipAddress={}, count={}", ipAddress, ipCount);
+                return false;
+            }
+        }
+
+        // 5. 使用Redis检查设备频率限制(同一设备在1小时内最多邀请30次)
+        if (StringUtils.hasText(deviceInfo)) {
+            String deviceKey = REDIS_KEY_INVITE_DEVICE_PREFIX + deviceInfo.hashCode();
+            Long deviceCount = stringRedisTemplate.opsForValue().increment(deviceKey);
+            if (deviceCount != null && deviceCount == 1) {
+                // 第一次设置,过期时间1小时
+                stringRedisTemplate.expire(deviceKey, 1, TimeUnit.HOURS);
+            }
+            if (deviceCount != null && deviceCount > 30) {
+                log.warn("设备邀请频率过高: deviceInfo={}, count={}", deviceInfo, deviceCount);
+                return false;
+            }
+        }
+
+        // 所有检查通过
+        return true;
+    }
+
+    // ==================== 统计查询 ====================
+
+    @Override
+    public Map<String, Object> getActivityStatistics(Long activityId) {
+        Map<String, Object> stats = new HashMap<>();
+
+        // 1. 获取活动基本信息
+        InviteActivity activity = this.getById(activityId);
+        if (activity == null) {
+            throw new BusinessException(404, "邀请活动不存在");
+        }
+
+        // 2. 从数据库统计各项指标
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime todayStart = now.toLocalDate().atStartOfDay();
+        LocalDateTime todayEnd = todayStart.plusDays(1).minusNanos(1);
+
+        Map<String, Object> dbStats = inviteActivityMapper.selectStatisticsByActivityId(
+                activityId, todayStart, todayEnd);
+
+        // 3. 组装统计数据
+        stats.put("activityId", activityId);
+        stats.put("activityName", activity.getActivityName());
+        stats.put("status", activity.getStatus());
+        stats.put("statusLabel", InviteStatusEnum.getLabelByCode(activity.getStatus()).getLabel());
+        stats.put("startTime", activity.getStartTime());
+        stats.put("endTime", activity.getEndTime());
+        stats.put("totalInviteCount", activity.getTotalInviteCount()); // 总邀请数(从活动表)
+        stats.put("validInviteCount", activity.getValidInviteCount()); // 有效邀请数(从活动表)
+        stats.put("rewardCount", activity.getRewardCount()); // 奖励发放数(从活动表)
+
+        // 从查询结果补充详细统计
+        if (dbStats != null) {
+            stats.put("todayInvite", dbStats.get("todayInvite")); // 今日新增邀请数
+            stats.put("validRate", calculateRate(activity.getTotalInviteCount(), activity.getValidInviteCount())); // 有效率
+            stats.put("rewardRate", calculateRate(activity.getValidInviteCount(), activity.getRewardCount())); // 奖励发放率
+        }
+
+        // 4. 获取Top邀请达人
+        List<Map<String, Object>> topInviters = inviteRecordMapper.selectTopInviters(activityId, 10);
+        stats.put("topInviters", topInviters);
+
+        // 5. 获取奖励发放统计
+        Map<String, Object> rewardStats = inviteRewardMapper.selectRewardStatistics(activityId);
+        if (rewardStats != null) {
+            stats.put("rewardDistributedCount", rewardStats.get("distributedCount"));
+            stats.put("rewardFailedCount", rewardStats.get("failedCount"));
+        }
+
+        return stats;
+    }
+
+    @Override
+    public Map<String, Object> getMyInviteStatistics(Long userId, Long activityId) {
+        Map<String, Object> stats = new HashMap<>();
+
+        // 1. 统计当前用户的邀请数据
+        int totalInvites = inviteRecordMapper.countTotalInvitesByUser(userId, activityId);
+        LocalDate today = LocalDate.now();
+        int todayInvites = inviteRecordMapper.countTodayInvites(userId, activityId, today);
+
+        // 2. 统计各状态的邀请数量
+        LambdaQueryWrapper<InviteRecord> pendingWrapper = new LambdaQueryWrapper<>();
+        pendingWrapper.eq(InviteRecord::getInviterUserId, userId);
+        pendingWrapper.eq(InviteRecord::getActivityId, activityId);
+        pendingWrapper.eq(InviteRecord::getStatus, InviteStatusEnum.PENDING.getCode());
+        long pendingCount = inviteRecordMapper.selectCount(pendingWrapper);
+
+        LambdaQueryWrapper<InviteRecord> activatedWrapper = new LambdaQueryWrapper<>();
+        activatedWrapper.eq(InviteRecord::getInviterUserId, userId);
+        activatedWrapper.eq(InviteRecord::getActivityId, activityId);
+        activatedWrapper.eq(InviteRecord::getStatus, InviteStatusEnum.ACTIVATED.getCode());
+        long activatedCount = inviteRecordMapper.selectCount(activatedWrapper);
+
+        LambdaQueryWrapper<InviteRecord> rewardedWrapper = new LambdaQueryWrapper<>();
+        rewardedWrapper.eq(InviteRecord::getInviterUserId, userId);
+        rewardedWrapper.eq(InviteRecord::getActivityId, activityId);
+        rewardedWrapper.eq(InviteRecord::getStatus, InviteStatusEnum.REWARDED.getCode());
+        long rewardedCount = inviteRecordMapper.selectCount(rewardedWrapper);
+
+        // 3. 组装统计数据
+        stats.put("userId", userId);
+        stats.put("activityId", activityId);
+        stats.put("totalInvites", totalInvites); // 总邀请数
+        stats.put("todayInvites", todayInvites); // 今日邀请数
+        stats.put("pendingCount", pendingCount); // 待激活数量
+        stats.put("activatedCount", activatedCount); // 已激活数量
+        stats.put("rewardedCount", rewardedCount); // 已获奖励数量
+
+        return stats;
+    }
+
+    @Override
+    public IPage<InviteRecord> getMyInviteRecords(Long userId, Long activityId, Integer page, Integer pageSize) {
+        // 参数校验
+        if (page == null || page < 1) {
+            page = 1;
+        }
+        if (pageSize == null || pageSize < 1) {
+            pageSize = 10;
+        }
+        if (pageSize > 100) {
+            pageSize = 100;
+        }
+
+        int offset = (page - 1) * pageSize;
+
+        // 查询用户的邀请记录
+        List<InviteRecord> records = inviteRecordMapper.selectByInviterUserId(userId, activityId, offset, pageSize);
+
+        // 查询总数
+        LambdaQueryWrapper<InviteRecord> countWrapper = new LambdaQueryWrapper<>();
+        countWrapper.eq(InviteRecord::getInviterUserId, userId);
+        countWrapper.eq(InviteRecord::getActivityId, activityId);
+        Long total = inviteRecordMapper.selectCount(countWrapper);
+
+        // 组装分页结果
+        Page<InviteRecord> result = new Page<>(page, pageSize, total);
+        result.setRecords(records);
+
+        // 填充标签
+        records.forEach(record -> {
+            StatusLabel statusLabel = InviteStatusEnum.getLabelByCode(record.getStatus());
+            record.setStatusLabel(statusLabel.getLabel());
+        });
+        return result;
+    }
+
+    @Override
+    public IPage<InviteRecord> getInviteRecords(InviteRecordQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<InviteRecord> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<InviteRecord> wrapper = new LambdaQueryWrapper<>();
+        if (queryDTO.getActivityId() != null) {
+            wrapper.eq(InviteRecord::getActivityId, queryDTO.getActivityId());
+        }
+        if (queryDTO.getInviterUserId() != null) {
+            wrapper.eq(InviteRecord::getInviterUserId, queryDTO.getInviterUserId());
+        }
+        if (queryDTO.getStatus() != null) {
+            wrapper.eq(InviteRecord::getStatus, queryDTO.getStatus());
+        }
+        if (queryDTO.getStartDate() != null) {
+            wrapper.ge(InviteRecord::getInviteTime, queryDTO.getStartDate());
+        }
+        if (queryDTO.getEndDate() != null) {
+            wrapper.le(InviteRecord::getInviteTime, queryDTO.getEndDate());
+        }
+        wrapper.orderByDesc(InviteRecord::getCreateTime);
+
+        IPage<InviteRecord> result = inviteRecordMapper.selectPage(page, wrapper);
+        result.getRecords().forEach(record -> {
+            StatusLabel statusLabel = InviteStatusEnum.getLabelByCode(record.getStatus());
+            record.setStatusLabel(statusLabel.getLabel());
+        });
+
+        return result;
+    }
+
+    // ==================== 手动补发奖励 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean manualReissueReward(Long rewardId, Long operatorId, String operatorName) {
+        // 1. 查询失败的奖励记录
+        InviteReward reward = inviteRewardMapper.selectById(rewardId);
+        if (reward == null) {
+            throw new BusinessException(404, "奖励记录不存在");
+        }
+
+        // 2. 检查状态是否为发放失败
+        if (reward.getStatus() != InviteRewardStatusEnum.FAILED.getCode()) {
+            throw new BusinessException(400, "只有发放失败的奖励才能补发");
+        }
+
+        // 3. 重新调用优惠券发放逻辑
+        try {
+            CouponDistributeDTO distributeDTO = new CouponDistributeDTO();
+            distributeDTO.setUserId(reward.getInviterUserId());
+            distributeDTO.setTemplateId(reward.getCouponTemplateId());
+            distributeDTO.setSource("邀请活动奖励-手动补发");
+            distributeDTO.setSourceId(String.valueOf(reward.getRecordId()));
+
+            userCouponService.distributeCoupon(distributeDTO, operatorId, operatorName);
+
+            // TODO: 应从实际发放结果获取userCouponId和couponCode
+            Long newUserCouponId = -2L; // 占位值
+            String newCouponCode = "MANUAL_" + System.currentTimeMillis(); // 占位值
+
+            // 4. 更新奖励状态为已补发
+            inviteRewardMapper.updateToRetried(rewardId, newUserCouponId, newCouponCode,
+                    operatorId, operatorName);
+
+            log.info("手动补发奖励成功: rewardId={}, operatorId={}, operatorName={}",
+                    rewardId, operatorId, operatorName);
+            return true;
+
+        } catch (Exception e) {
+            log.error("手动补发奖励失败: rewardId={}, error={}", rewardId, e.getMessage(), e);
+            // 再次标记为失败,增加重试次数
+            inviteRewardMapper.updateToFailed(rewardId, "手动补发失败: " + e.getMessage());
+            throw new BusinessException(500, "补发奖励失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public InviteRecord getInviteRecordByCode(String inviteCode) {
+        return inviteRecordMapper.selectByInviteCode(inviteCode);
+    }
+
+    // ==================== 私有工具方法 ====================
+
+    /**
+     * 将数字编码为Base62字符串
+     * 用于压缩用户ID和活动ID,缩短邀请码长度
+     *
+     * @param num 要编码的数字
+     * @return Base62编码后的字符串
+     */
+    private String encodeToBase62(long num) {
+        if (num == 0) {
+            return "0";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        while (num > 0) {
+            sb.append(BASE62_CHARS.charAt((int) (num % 62)));
+            num /= 62;
+        }
+        return sb.reverse().toString();
+    }
+
+    /**
+     * 生成指定长度的随机字符串(由Base62字符组成)
+     *
+     * @param length 字符串长度
+     * @return 随机字符串
+     */
+    private String generateRandomString(int length) {
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            sb.append(BASE62_CHARS.charAt(SECURE_RANDOM.nextInt(BASE62_CHARS.length())));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 计算比率(百分比)
+     *
+     * @param total 总数
+     * @param part  部分
+     * @return 百分比字符串(如 "85.5%")
+     */
+    private String calculateRate(Integer total, Integer part) {
+        if (total == null || total == 0) {
+            return "0%";
+        }
+        double rate = (part.doubleValue() / total.doubleValue()) * 100;
+        return String.format("%.1f%%", rate);
+    }
+
+    /**
+     * 填充活动对象的标签字段(用于前端展示)
+     *
+     * @param activity 活动对象
+     */
+    private void fillLabels(InviteActivity activity) {
+        // 状态标签
+        StatusLabel statusLabel = ActivityStatusEnum.getLabelByCode(activity.getStatus());
+        activity.setStatusLabel(statusLabel.getLabel());
+
+        // 适用范围标签(如果有枚举的话,这里简单处理)
+        if (activity.getApplyScope() != null) {
+            activity.setApplyScopeLabel(activity.getApplyScope() == 1 ? "全平台" : "指定门店");
+        }
+
+        // 商品范围标签
+        if (activity.getProductScope() != null) {
+            activity.setProductScopeLabel(activity.getProductScope() == 1 ? "全部商品" : "指定商品");
+        }
+    }
+}

+ 114 - 0
haha-service/src/main/java/com/haha/service/impl/InviteNotificationServiceImpl.java

@@ -0,0 +1,114 @@
+package com.haha.service.impl;
+
+import com.haha.service.InviteNotificationService;
+import com.haha.service.UserService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+
+/**
+ * 邀请活动消息通知服务实现类
+ * 提供默认的通知实现(日志输出),可根据需要扩展为真实的通知渠道
+ * 支持的扩展方式:
+ * - 站内信通知
+ * - 微信小程序模板消息
+ * - 短信通知
+ * - APP推送通知
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class InviteNotificationServiceImpl implements InviteNotificationService {
+
+    private final UserService userService;
+
+    // 可以注入其他消息服务(如微信模板消息服务、短信服务等)
+    // private final WechatMessageService wechatMessageService;
+    // private final SmsService smsService;
+    // private final MessageService messageService;
+
+    @Override
+    public void sendRewardSuccessNotification(Long userId, String couponName, BigDecimal amount) {
+        try {
+            log.info("[邀请通知] 准备发送奖励发放成功通知 - userId: {}, couponName: {}, amount: {}",
+                     userId, couponName, amount);
+
+            // TODO: 根据实际的消息渠道发送通知
+
+            // 方式1: 站内信(推荐优先使用)
+            // messageService.sendSystemMessage(userId, "邀请奖励",
+            //     "恭喜!您邀请的好友已完成首单,获得" + couponName + "一张(面值:" + amount + "元)");
+
+            // 方式2: 微信小程序模板消息(需要配置模板ID)
+            // Map<String, String> data = new HashMap<>();
+            // data.put("thing1", couponName);  // 优惠券名称
+            // data.put("amount2", amount.toString());  // 优惠券金额
+            // data.put("time3", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));  // 发放时间
+            // wechatMessageService.sendTemplateMessage(userId, templateId, data);
+
+            // 方式3: 短信通知(可选,适用于重要奖励)
+            // String phone = userService.getUserPhone(userId);
+            // if (phone != null) {
+            //     smsService.sendSms(phone, "【哈哈智能售卖机】恭喜您获得" + couponName +
+            //         "一张(面值" + amount + "元),已发放至您的账户,请查收。");
+            // }
+
+            log.info("[邀请通知] 奖励发放成功通知已发送 - userId: {}", userId);
+
+        } catch (Exception e) {
+            log.error("[邀请通知] 发送奖励发放成功通知失败 - userId: {}", userId, e);
+        }
+    }
+
+    @Override
+    public void sendNewInviteeNotification(Long inviterUserId, String inviteeNickname) {
+        try {
+            log.info("[邀请通知] 准备发送新好友注册通知 - inviterUserId: {}, inviteeNickname: {}",
+                     inviterUserId, inviteeNickname);
+
+            // TODO: 发送通知告知邀请人有新用户通过他的邀请注册了
+
+            // 方式1: 站内信
+            // messageService.sendSystemMessage(inviterUserId, "新好友加入",
+            //     "恭喜!您的好友" + inviteeNickname + " 已通过您的邀请码注册,待对方完成首单后即可获得奖励");
+
+            // 方式2: 微信模板消息
+            // Map<String, String> data = new HashMap<>();
+            // data.put("thing1", inviteeNickname);  // 新好友昵称
+            // data.put("time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));  // 注册时间
+            // wechatMessageService.sendTemplateMessage(inviterUserId, templateId, data);
+
+            log.info("[邀请通知] 新好友注册通知已发送 - inviterUserId: {}", inviterUserId);
+
+        } catch (Exception e) {
+            log.error("[邀请通知] 发送新好友注册通知失败 - inviterUserId: {}", inviterUserId, e);
+        }
+    }
+
+    @Override
+    public void sendActivityEndingReminder(Long userId, String activityName, int daysLeft) {
+        try {
+            log.info("[邀请通知] 准备发送活动即将结束提醒 - userId: {}, activityName: {}, daysLeft: {}",
+                     userId, activityName, daysLeft);
+
+            // TODO: 发送活动即将结束提醒
+
+            // 方式1: 站内信
+            // messageService.sendSystemMessage(userId, "活动即将结束提醒",
+            //     "温馨提示:[" + activityName + "]将在" + daysLeft + "天后结束,抓紧时间邀请好友赢取奖励吧!");
+
+            // 方式2: 微信模板消息
+            // Map<String, String> data = new HashMap<>();
+            // data.put("thing1", activityName);  // 活动名称
+            // data.put("number2", String.valueOf(daysLeft));  // 剩余天数
+            // wechatMessageService.sendTemplateMessage(userId, templateId, data);
+
+            log.info("[邀请通知] 活动即将结束提醒已发送 - userId: {}", userId);
+
+        } catch (Exception e) {
+            log.error("[邀请通知] 发送活动即将结束提醒失败 - userId: {}", userId, e);
+        }
+    }
+}

+ 50 - 3
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -23,6 +23,7 @@ import com.haha.mapper.OrderMapper;
 import com.haha.mapper.ShopMapper;
 import com.haha.service.OrderGoodsService;
 import com.haha.service.OrderService;
+import com.haha.service.InviteActivityService;
 import com.haha.service.payment.PaymentService;
 import com.haha.service.payment.RefundResult;
 import com.haha.service.payment.payscore.PayScoreResult;
@@ -31,6 +32,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -63,6 +65,10 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
     @Lazy
     private OrderGoodsService orderGoodsService;
 
+    @Autowired
+    @Lazy
+    private InviteActivityService inviteActivityService;
+
     @Override
     public IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId, 
                                  String payStatus, Integer status, String startDate, String endDate) {
@@ -467,17 +473,28 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             order.setPayStatus(PayStatus.PAID.getCode());
             order.setStatus(OrderStatus.COMPLETED.getCode());
             order.setPayTime(LocalDateTime.now());
-            return this.updateById(order);
+            boolean updateResult = this.updateById(order);
+
+            // 订单完成后异步触发邀请激活逻辑
+            if (updateResult) {
+                triggerInviteActivationAsync(order.getUserId(), orderId);
+            }
+
+            return updateResult;
         }
 
         try {
             PayScoreResult result = payScoreService.completePayScoreOrder(orderId, totalAmount, null);
-            
+
             if (result.isSuccess()) {
                 log.info("[支付分集成] 支付分完结成功 - orderId: {}, state: {}", orderId, result.getState());
+
+                // 订单完成后异步触发邀请激活逻辑
+                triggerInviteActivationAsync(order.getUserId(), orderId);
+
                 return true;
             } else {
-                log.error("[支付分集成] 支付分完结失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                log.error("[支付分集成] 支付分完结失败 - orderId: {}, errorCode: {}, errorMsg: {}",
                         orderId, result.getErrorCode(), result.getErrorMsg());
                 return false;
             }
@@ -486,4 +503,34 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             return false;
         }
     }
+
+    /**
+     * 异步触发邀请活动激活
+     * 当被邀请人完成首单后,自动激活邀请关系并发放奖励优惠券
+     *
+     * 使用 @Async 异步执行,避免阻塞订单主流程
+     * 邀请激活失败不影响订单流程的正常运行
+     *
+     * @param userId  下单用户ID(被邀请人)
+     * @param orderId 订单ID
+     */
+    @Async("taskExecutor")
+    public void triggerInviteActivationAsync(Long userId, Long orderId) {
+        try {
+            log.info("[邀请激活] 收到订单完成事件,开始处理邀请激活 - orderId: {}, userId: {}", orderId, userId);
+
+            // 调用邀请服务激活邀请记录并发放奖励
+            boolean success = inviteActivityService.activateInviteRecord(userId, orderId);
+
+            if (success) {
+                log.info("[邀请激活] 邀请激活成功 - orderId: {}, userId: {}", orderId, userId);
+            } else {
+                log.warn("[邀请激活] 邀请激活失败或无待激活记录(可能该用户未被邀请或已激活)- orderId: {}, userId: {}", orderId, userId);
+            }
+        } catch (Exception e) {
+            // 邀请激活异常不应影响订单流程,仅记录错误日志
+            // 失败的奖励可以通过定时任务或手动补发来处理
+            log.error("[邀请激活] 处理订单完成事件时发生异常 - orderId: {}, userId: {}", orderId, userId, e);
+        }
+    }
 }

+ 7 - 1
pom.xml

@@ -70,7 +70,7 @@
             </dependency>
 
 
-            <!-- FastJson2 -->
+            <!-- FastJson2 - JSON 处理 -->
             <dependency>
                 <groupId>com.alibaba.fastjson2</groupId>
                 <artifactId>fastjson2</artifactId>
@@ -136,6 +136,12 @@
                 <artifactId>wechatpay-java</artifactId>
                 <version>${wechatpay.version}</version>
             </dependency>
+            <!-- 验证框架 -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-validation</artifactId>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>