skyline 1 månad sedan
förälder
incheckning
d5154e67cc

+ 39 - 2
haha-admin-mp/src/api/invite.ts

@@ -29,6 +29,19 @@ export interface BindInviteParams {
   inviteCode: string;
 }
 
+export interface BindInviteResponse {
+  success: boolean;
+  message: string;
+  // 双向奖励发放结果
+  rewardResult?: {
+    inviteeCouponGranted: boolean;     // 是否已向被邀请人发券
+    inviteeCouponName?: string;        // 被邀请人获得的优惠券名称
+    inviteeCouponAmount?: number;      // 被邀请人获得的优惠券金额
+    inviterCouponPending: boolean;     // 邀请人奖励是否待发放(需满足条件)
+    triggerCondition?: string;         // 触发条件说明
+  };
+}
+
 export interface InviteRecord {
   id: string;
   inviteePhone: string;
@@ -39,6 +52,7 @@ export interface InviteRecord {
   rewardTime?: string;
   couponName?: string;
   couponAmount?: number;
+  rewardType?: 'inviter' | 'invitee';  // 奖励类型:inviter=邀请人推荐奖励, invitee=被邀请人新人券
 }
 
 export interface InviteRecordListResponse {
@@ -60,6 +74,11 @@ export interface InviteStatistics {
   rewardCount: number;
   pendingCount: number;
   totalRewardAmount: number;
+  // 双向奖励统计
+  inviterRewardCount?: number;       // 邀请人推荐奖励数量
+  inviteeRewardCount?: number;       // 被邀请人新人券发放数量
+  inviterRewardAmount?: number;      // 邀请人推荐奖励总金额
+  inviteeRewardAmount?: number;      // 被邀请人新人券总金额
 }
 
 export interface ActivityInfo {
@@ -72,6 +91,14 @@ export interface ActivityInfo {
   endTime: string;
   rules: string[];
   isActive: boolean;
+  // 双向奖励配置
+  dualRewardConfig?: {
+    inviteeCouponName: string;      // 被邀请人新人券名称
+    inviteeCouponAmount: number;    // 被邀请人新人券金额
+    inviterCouponName: string;     // 邀请人推荐奖励券名称
+    inviterCouponAmount: number;   // 邀请人推荐奖励券金额
+    triggerCondition: string;       // 触发条件描述(如"好友完成首单")
+  };
 }
 
 export interface PosterResponse {
@@ -88,9 +115,19 @@ export async function getInviteCode(): Promise<InviteCodeResponse> {
   return get('/invite/code');
 }
 
-export async function bindInviteRelation(params: BindInviteParams): Promise<{ success: boolean; message: string }> {
+export async function bindInviteRelation(params: BindInviteParams): Promise<BindInviteResponse> {
   if (USE_MOCK) {
-    return Promise.resolve({ success: true, message: '邀请关系绑定成功' });
+    return Promise.resolve({
+      success: true,
+      message: '邀请关系绑定成功',
+      rewardResult: {
+        inviteeCouponGranted: true,
+        inviteeCouponName: '新人专属优惠券',
+        inviteeCouponAmount: 10,
+        inviterCouponPending: true,
+        triggerCondition: '好友完成首单后发放'
+      }
+    });
   }
 
   const { post } = await import('@/utils/request');

+ 62 - 8
haha-admin-mp/src/pages/invite/index.vue

@@ -7,13 +7,13 @@
       <view class="activity-rules" v-if="activityInfo">
         <view class="rules-header">
           <view class="gift-icon"></view>
-          <text class="rules-title">{{ activityInfo.title }}</text>
+          <text class="rules-title">{{ activityInfo.title || '邀请有礼,双方共赢' }}</text>
         </view>
 
         <view class="rules-content">
           <view
             class="rule-item"
-            v-for="(rule, index) in activityInfo.rules"
+            v-for="(rule, index) in displayRules"
             :key="index"
           >
             <view class="rule-number">{{ index + 1 }}</view>
@@ -21,6 +21,12 @@
           </view>
         </view>
 
+        <!-- 双向奖励亮点提示 -->
+        <view class="dual-reward-highlight">
+          <view class="highlight-icon">🎁</view>
+          <text class="highlight-text">双方均可获得优惠券,上不封顶</text>
+        </view>
+
         <view class="activity-time">
           <view class="time-icon"></view>
           <text>活动时间:{{ activityInfo.startTime }} ~ {{ activityInfo.endTime }}</text>
@@ -49,7 +55,7 @@
       <view class="invite-code-section" v-if="inviteCodeInfo">
         <view class="code-header">
           <text class="code-title">我的邀请码</text>
-          <text class="code-tip">分享给好友即可获得奖励</text>
+          <text class="code-tip">分享给好友,双方各得优惠券</text>
         </view>
         <view class="code-display">
           <text class="code-text">{{ inviteCodeInfo.code }}</text>
@@ -61,8 +67,11 @@
       <view class="action-buttons">
         <button class="btn-primary" @click="handleShare" :disabled="isSharing">
           <view class="btn-icon share"></view>
-          <text>{{ isSharing ? '分享中...' : '立即邀请好友' }}</text>
+          <text>{{ isSharing ? '分享中...' : '立即邀请,双方得券' }}</text>
         </button>
+        <view class="btn-desc">
+          <text class="desc-text">邀请好友注册即送新人券,完成首单再送推荐奖励</text>
+        </view>
         <button class="btn-secondary" @click="handleGeneratePoster" :disabled="isGenerating">
           <view class="btn-icon poster"></view>
           <text>{{ isGenerating ? '生成中...' : '生成邀请海报' }}</text>
@@ -90,7 +99,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref, computed, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import {
   getInviteCode,
@@ -106,6 +115,19 @@ const activityInfo = ref<ActivityInfo | null>(null);
 const isSharing = ref(false);
 const isGenerating = ref(false);
 
+// 双向奖励规则展示(如果后端未返回则使用默认文案)
+const displayRules = computed(() => {
+  if (activityInfo.value && activityInfo.value.rules && activityInfo.value.rules.length > 0) {
+    return activityInfo.value.rules;
+  }
+  // 默认双向奖励规则
+  return [
+    '邀请好友注册,好友立即获得新人优惠券',
+    '好友完成首单后,您将获得推荐奖励券',
+    '双方均可享受专属优惠,上不封顶'
+  ];
+});
+
 onMounted(() => {
   loadActivityInfo();
   loadStatistics();
@@ -173,8 +195,8 @@ const handleShare = async () => {
       provider: 'weixin',
       scene: 'WXSceneSession',
       type: 0,
-      title: `${activityInfo.value?.title || '邀请好友得奖励'}`,
-      summary: `我的邀请码:${inviteCodeInfo.value.code}`,
+      title: `【双享福利】${activityInfo.value?.title || '邀请您领取专属优惠券'}`,
+      summary: `接受邀请即得新人券,完成首单再送推荐奖\n\n我的邀请码:${inviteCodeInfo.value.code}`,
       href: inviteCodeInfo.value.inviteUrl,
       imageUrl: '',
       success: () => {
@@ -378,6 +400,28 @@ const goToRecords = () => {
   border-radius: 10rpx;
 }
 
+.dual-reward-highlight {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 16rpx 24rpx;
+  margin-bottom: 20rpx;
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 10rpx;
+  border: 1rpx dashed rgba(255, 255, 255, 0.4);
+}
+
+.highlight-icon {
+  font-size: 32rpx;
+  margin-right: 12rpx;
+}
+
+.highlight-text {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #ffffff;
+}
+
 .time-icon {
   width: 28rpx;
   height: 28rpx;
@@ -521,7 +565,7 @@ const goToRecords = () => {
   font-weight: 600;
   color: #ffffff;
   border: none;
-  margin-bottom: 20rpx;
+  margin-bottom: 12rpx;
 
   &[disabled] {
     background: #cbd5e1;
@@ -533,6 +577,16 @@ const goToRecords = () => {
   }
 }
 
+.btn-desc {
+  margin-bottom: 20rpx;
+  text-align: center;
+}
+
+.desc-text {
+  font-size: 24rpx;
+  color: #64748b;
+}
+
 .btn-secondary {
   width: 100%;
   height: 96rpx;

+ 79 - 1
haha-admin-mp/src/pages/invite/records.vue

@@ -25,6 +25,12 @@
         </view>
       </view>
 
+      <!-- 双向奖励说明 -->
+      <view class="dual-reward-notice">
+        <view class="notice-icon">💡</view>
+        <text class="notice-text">统计包含:邀请人推荐奖励 + 被邀请人新人优惠券</text>
+      </view>
+
       <!-- 邀请记录列表 -->
       <view class="list-section">
         <view class="list-header">
@@ -80,7 +86,8 @@
                 </view>
 
                 <view class="record-reward" v-if="item.couponName && item.couponAmount">
-                  <view class="reward-badge">
+                  <view class="reward-badge" :class="getRewardTypeClass(item.rewardType)">
+                    <text class="reward-type-tag">{{ getRewardTypeText(item.rewardType) }}</text>
                     <text class="reward-amount">+¥{{ item.couponAmount }}</text>
                     <text class="reward-name">{{ item.couponName }}</text>
                   </view>
@@ -228,6 +235,23 @@ const getAvatarText = (name?: string) => {
   return name.charAt(name.length - 1);
 };
 
+// 奖励类型判断(区分邀请人奖励和被邀请人新人券)
+const getRewardTypeClass = (rewardType?: string) => {
+  const typeMap: Record<string, string> = {
+    'inviter': 'type-inviter',      // 邀请人推荐奖励
+    'invitee': 'type-invitee'       // 被邀请人新人券
+  };
+  return typeMap[rewardType || ''] || '';
+};
+
+const getRewardTypeText = (rewardType?: string) => {
+  const textMap: Record<string, string> = {
+    'inviter': '推荐奖',
+    'invitee': '新人券'
+  };
+  return textMap[rewardType || ''] || '';
+};
+
 const goToInvite = () => {
   uni.navigateTo({
     url: '/pages/invite/index'
@@ -296,6 +320,27 @@ const goToInvite = () => {
   gap: 20rpx;
 }
 
+.dual-reward-notice {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 16rpx 24rpx;
+  background: #fef3c7;
+  margin: 0 24rpx 16rpx;
+  border-radius: 10rpx;
+  border: 1rpx solid #fcd34d;
+}
+
+.notice-icon {
+  font-size: 28rpx;
+  margin-right: 10rpx;
+}
+
+.notice-text {
+  font-size: 24rpx;
+  color: #92400e;
+}
+
 .stat-card {
   flex: 1;
   background: #f8fafc;
@@ -603,6 +648,39 @@ const goToInvite = () => {
   background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
   border-radius: 8rpx;
   gap: 8rpx;
+
+  &.type-inviter {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+
+    .reward-amount {
+      color: #1d4ed8;
+    }
+
+    .reward-name {
+      color: #1e40af;
+    }
+  }
+
+  &.type-invitee {
+    background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
+
+    .reward-amount {
+      color: #059669;
+    }
+
+    .reward-name {
+      color: #047857;
+    }
+  }
+}
+
+.reward-type-tag {
+  font-size: 20rpx;
+  font-weight: 600;
+  padding: 2rpx 10rpx;
+  background: rgba(255, 255, 255, 0.6);
+  border-radius: 6rpx;
+  color: #92400e;
 }
 
 .reward-amount {

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

@@ -236,4 +236,78 @@ const {
     margin-bottom: 12px;
   }
 }
+
+// 双卡片奖励配置样式
+:deep(.invite-form) {
+  .reward-cards-container {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 20px;
+    margin-top: 16px;
+
+    @media (max-width: 768px) {
+      grid-template-columns: 1fr;
+    }
+  }
+
+  .reward-card {
+    transition: all 0.3s ease;
+
+    &:hover {
+      transform: translateY(-2px);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+
+    &.invitee-card {
+      border-left: 4px solid #e6a23c;
+    }
+
+    &:not(.invitee-card) {
+      border-left: 4px solid #f56c6c;
+    }
+
+    .card-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .card-title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+    }
+
+    .card-content {
+      .card-description {
+        line-height: 1.6;
+        color: #909399;
+      }
+
+      .tip-box {
+        display: flex;
+        align-items: flex-start;
+        padding: 8px 12px;
+        background-color: #fdf6ec;
+        border-radius: 4px;
+        border-left: 3px solid #e6a23c;
+
+        .el-icon {
+          margin-top: 2px;
+          color: #e6a23c;
+        }
+      }
+    }
+
+    :deep(.el-card__header) {
+      padding: 16px 20px;
+      background-color: #fafafa;
+      border-bottom: 1px solid #ebeef5;
+    }
+
+    :deep(.el-card__body) {
+      padding: 20px;
+    }
+  }
+}
 </style>

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

@@ -23,6 +23,7 @@ const form = reactive<InviteRecordsSearchParams>({
   activityId: undefined,
   inviterId: "",
   status: undefined,
+  rewardType: undefined,
   startTime: "",
   endTime: ""
 });
@@ -35,12 +36,40 @@ const statusMap: Record<number, { text: string; type: string }> = {
   3: { text: "发放失败", type: "danger" }
 };
 
+// 奖励类型映射
+const rewardTypeMap: Record<number, { text: string; type: string; icon: string }> = {
+  0: { text: "邀请人推荐奖", type: "success", icon: "🎯" },
+  1: { text: "被邀请人新人券", type: "primary", icon: "🎁" }
+};
+
+// 触发条件映射
+const triggerTypeMap: Record<number, { text: string; type: string }> = {
+  0: { text: "首单完成", type: "warning" },
+  1: { text: "注册绑定", type: "" }
+};
+
 // 手机号脱敏
 function maskPhone(phone: string): string {
   if (!phone) return "-";
   return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
 }
 
+// 格式化奖励类型
+function formatRewardType(type: number | undefined): { text: string; type: string; icon: string } {
+  if (type === undefined || type === null) {
+    return { text: "未知", type: "info", icon: "" };
+  }
+  return rewardTypeMap[type] || { text: "未知", type: "info", icon: "" };
+}
+
+// 格式化触发条件
+function formatTriggerType(type: number | undefined): { text: string; type: string } {
+  if (type === undefined || type === null) {
+    return { text: "未知", type: "info" };
+  }
+  return triggerTypeMap[type] || { text: "未知", type: "info" };
+}
+
 // 加载记录列表
 async function onSearch() {
   loading.value = true;
@@ -69,6 +98,7 @@ function resetForm(formEl) {
     activityId: undefined,
     inviterId: "",
     status: undefined,
+    rewardType: undefined,
     startTime: "",
     endTime: ""
   });
@@ -168,6 +198,18 @@ onMounted(() => {
           <el-option label="发放失败" :value="3" />
         </el-select>
       </el-form-item>
+      <el-form-item label="奖励类型:" prop="rewardType">
+        <el-select
+          v-model="form.rewardType"
+          placeholder="奖励类型"
+          clearable
+          class="w-[150px]!"
+        >
+          <el-option label="全部" :value="undefined" />
+          <el-option label="邀请人推荐奖" :value="0" />
+          <el-option label="被邀请人新人券" :value="1" />
+        </el-select>
+      </el-form-item>
       <el-form-item label="时间范围:" prop="timeRange">
         <el-date-picker
           v-model="timeRange"
@@ -250,6 +292,30 @@ onMounted(() => {
             </el-tag>
           </template>
         </el-table-column>
+        <el-table-column label="奖励类型" prop="rewardType" width="150" align="center">
+          <template #default="{ row }">
+            <el-tag
+              v-if="row.rewardType !== undefined && row.rewardType !== null"
+              :type="formatRewardType(row.rewardType).type"
+              size="small"
+            >
+              {{ formatRewardType(row.rewardType).icon }} {{ formatRewardType(row.rewardType).text }}
+            </el-tag>
+            <span v-else class="text-gray-400">—</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="触发条件" prop="triggerType" width="130" align="center">
+          <template #default="{ row }">
+            <el-tag
+              v-if="row.triggerType !== undefined && row.triggerType !== null"
+              :type="formatTriggerType(row.triggerType).type"
+              size="small"
+            >
+              {{ formatTriggerType(row.triggerType).text }}
+            </el-tag>
+            <span v-else class="text-gray-400">—</span>
+          </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>

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

@@ -65,6 +65,67 @@ const rankingList = computed(() => {
   return statisticsData.value.rankingList;
 });
 
+// 双向奖励统计 - 被邀请人优惠券
+const inviteeCouponMetrics = computed(() => {
+  if (!statisticsData.value?.inviteeCoupon) {
+    return {
+      totalDistributed: 0,
+      totalUsed: 0,
+      usageRate: "0%"
+    };
+  }
+
+  const data = statisticsData.value.inviteeCoupon;
+  return {
+    totalDistributed: data.totalDistributed || 0,
+    totalUsed: data.totalUsed || 0,
+    usageRate: data.usageRate !== undefined ? `${data.usageRate.toFixed(2)}%` : "0%"
+  };
+});
+
+// 双向奖励统计 - 邀请人优惠券
+const inviterCouponMetrics = computed(() => {
+  if (!statisticsData.value?.inviterCoupon) {
+    return {
+      totalDistributed: 0,
+      totalUsed: 0,
+      usageRate: "0%"
+    };
+  }
+
+  const data = statisticsData.value.inviterCoupon;
+  return {
+    totalDistributed: data.totalDistributed || 0,
+    totalUsed: data.totalUsed || 0,
+    usageRate: data.usageRate !== undefined ? `${data.usageRate.toFixed(2)}%` : "0%"
+  };
+});
+
+// 成本分析数据
+const costAnalysisMetrics = computed(() => {
+  if (!statisticsData.value?.costAnalysis) {
+    return {
+      totalDiscountAmount: 0,
+      gmv: 0,
+      roi: "0",
+      avgAcquisitionCost: 0
+    };
+  }
+
+  const data = statisticsData.value.costAnalysis;
+  return {
+    totalDiscountAmount: data.totalDiscountAmount || 0,
+    gmv: data.gmv || 0,
+    roi: data.roi !== undefined ? data.roi.toFixed(2) : "0",
+    avgAcquisitionCost: data.avgAcquisitionCost || 0
+  };
+});
+
+// 格式化金额
+function formatMoney(value: number): string {
+  return `¥${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+}
+
 // 加载统计数据
 async function loadStatistics() {
   if (!activityId.value) {
@@ -269,6 +330,127 @@ onMounted(() => {
       <div ref="chartRef" style="width: 100%; height: 400px"></div>
     </el-card>
 
+    <!-- 双向奖励统计 - 被邀请人优惠券(蓝色系) -->
+    <el-card class="mb-5">
+      <template #header>
+        <div class="flex items-center">
+          <el-icon class="mr-2" color="#409EFF" :size="20"><ep:ticket /></el-icon>
+          <span class="font-semibold">被邀请人优惠券统计</span>
+          <el-tag type="primary" size="small" class="ml-2">被邀请人</el-tag>
+        </div>
+      </template>
+      <el-row :gutter="20">
+        <el-col :xs="24" :sm="8">
+          <div class="stat-item">
+            <div class="text-gray-500 text-sm mb-2">优惠券发放总数</div>
+            <div class="text-3xl font-bold text-blue-600">{{ inviteeCouponMetrics.totalDistributed.toLocaleString() }}</div>
+            <div class="text-xs text-gray-400 mt-1">张</div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="8">
+          <div class="stat-item">
+            <div class="text-gray-500 text-sm mb-2">优惠券使用数量</div>
+            <div class="text-3xl font-bold text-blue-500">{{ inviteeCouponMetrics.totalUsed.toLocaleString() }}</div>
+            <div class="text-xs text-gray-400 mt-1">张</div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="8">
+          <div class="stat-item">
+            <div class="text-gray-500 text-sm mb-2">优惠券核销率</div>
+            <div class="text-3xl font-bold" style="color: #409EFF">{{ inviteeCouponMetrics.usageRate }}</div>
+            <div class="text-xs text-gray-400 mt-1">使用率</div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <!-- 双向奖励统计 - 邀请人优惠券(绿色系) -->
+    <el-card class="mb-5">
+      <template #header>
+        <div class="flex items-center">
+          <el-icon class="mr-2" color="#67C23A" :size="20"><ep:present /></el-icon>
+          <span class="font-semibold">邀请人优惠券统计</span>
+          <el-tag type="success" size="small" class="ml-2">邀请人</el-tag>
+        </div>
+      </template>
+      <el-row :gutter="20">
+        <el-col :xs="24" :sm="8">
+          <div class="stat-item">
+            <div class="text-gray-500 text-sm mb-2">优惠券发放总数</div>
+            <div class="text-3xl font-bold text-green-600">{{ inviterCouponMetrics.totalDistributed.toLocaleString() }}</div>
+            <div class="text-xs text-gray-400 mt-1">张</div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="8">
+          <div class="stat-item">
+            <div class="text-gray-500 text-sm mb-2">优惠券使用数量</div>
+            <div class="text-3xl font-bold text-green-500">{{ inviterCouponMetrics.totalUsed.toLocaleString() }}</div>
+            <div class="text-xs text-gray-400 mt-1">张</div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="8">
+          <div class="stat-item">
+            <div class="text-gray-500 text-sm mb-2">优惠券核销率</div>
+            <div class="text-3xl font-bold" style="color: #67C23A">{{ inviterCouponMetrics.usageRate }}</div>
+            <div class="text-xs text-gray-400 mt-1">使用率</div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <!-- 综合成本分析模块(橙色系) -->
+    <el-card class="mb-5 cost-analysis-card">
+      <template #header>
+        <div class="flex items-center">
+          <el-icon class="mr-2" color="#E6A23C" :size="20"><ep:data-analysis /></el-icon>
+          <span class="font-semibold">综合成本分析</span>
+          <el-tag type="warning" size="small" class="ml-2">ROI</el-tag>
+        </div>
+      </template>
+      <el-row :gutter="20">
+        <el-col :xs="24" :sm="12" :md="6">
+          <div class="cost-item">
+            <div class="flex items-center mb-2">
+              <div class="w-3 h-3 rounded-full mr-2" style="background-color: #E6A23C"></div>
+              <span class="text-gray-600 text-sm font-medium">总优惠金额</span>
+            </div>
+            <div class="text-2xl font-bold" style="color: #E6A23C">{{ formatMoney(costAnalysisMetrics.totalDiscountAmount) }}</div>
+            <div class="text-xs text-gray-400 mt-1">双方合计优惠成本</div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="6">
+          <div class="cost-item">
+            <div class="flex items-center mb-2">
+              <div class="w-3 h-3 rounded-full mr-2" style="background-color: #F56C6C"></div>
+              <span class="text-gray-600 text-sm font-medium">GMV(总交易额)</span>
+            </div>
+            <div class="text-2xl font-bold" style="color: #F56C6C">{{ formatMoney(costAnalysisMetrics.gmv) }}</div>
+            <div class="text-xs text-gray-400 mt-1">活动期间总交易金额</div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="6">
+          <div class="cost-item">
+            <div class="flex items-center mb-2">
+              <div class="w-3 h-3 rounded-full mr-2" style="background-color: #67C23A"></div>
+              <span class="text-gray-600 text-sm font-medium">ROI(投资回报率)</span>
+            </div>
+            <div class="text-2xl font-bold" style="color: #67C23A">{{ costAnalysisMetrics.roi }}</div>
+            <div class="text-xs text-gray-400 mt-1">GMV / 总优惠成本</div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="6">
+          <div class="cost-item">
+            <div class="flex items-center mb-2">
+              <div class="w-3 h-3 rounded-full mr-2" style="background-color: #909399"></div>
+              <span class="text-gray-600 text-sm font-medium">平均获客成本</span>
+            </div>
+            <div class="text-2xl font-bold" style="color: #909399">{{ formatMoney(costAnalysisMetrics.avgAcquisitionCost) }}</div>
+            <div class="text-xs text-gray-400 mt-1">总优惠成本 / 有效新用户数</div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
     <!-- 邀请达人排行榜 -->
     <el-card>
       <template #header>
@@ -337,4 +519,44 @@ onMounted(() => {
     transition: all 0.3s ease;
   }
 }
+
+// 双向奖励统计项样式
+.stat-item {
+  text-align: center;
+  padding: 20px 10px;
+  border-radius: 8px;
+  background-color: #fafafa;
+  transition: all 0.3s ease;
+
+  &:hover {
+    background-color: #f5f5f5;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+  }
+}
+
+// 成本分析卡片特殊样式
+.cost-analysis-card {
+  background: linear-gradient(135deg, #fff9f0 0%, #ffffff 100%);
+  border: 1px solid #faecd8;
+
+  :deep(.el-card__header) {
+    background-color: #fdf6ec;
+    border-bottom: 1px solid #faecd8;
+  }
+}
+
+// 成本分析项样式
+.cost-item {
+  padding: 20px 15px;
+  border-radius: 8px;
+  background-color: rgba(255, 255, 255, 0.8);
+  transition: all 0.3s ease;
+
+  &:hover {
+    background-color: #ffffff;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 16px rgba(230, 162, 60, 0.15);
+  }
+}
 </style>

+ 146 - 3
haha-admin-web/src/views/marketing/invite/utils/hook.tsx

@@ -1,5 +1,6 @@
 import dayjs from "dayjs";
 import { message } from "@/utils/message";
+import { ElMessage } from "element-plus";
 import { addDialog } from "@/components/ReDialog";
 import {
   getInviteActivityList,
@@ -24,8 +25,11 @@ import {
   ElDescriptionsItem,
   ElTag,
   ElSelect,
-  ElOption
+  ElOption,
+  ElCard,
+  ElDivider
 } from "element-plus";
+import { InfoFilled } from "@element-plus/icons-vue";
 import {
   initPagination,
   handlePageSizeChange,
@@ -48,6 +52,14 @@ export function useInviteActivity() {
   const dataList = ref([]);
   const pagination = reactive<PaginationProps>(initPagination());
 
+  // 优惠券模板列表(模拟数据,实际应从API获取)
+  const couponTemplateList = ref([
+    { id: 1, name: "满100减20元券", value: 20, minAmount: 100 },
+    { id: 2, name: "满200减50元券", value: 50, minAmount: 200 },
+    { id: 3, name: "新人专享10元券", value: 10, minAmount: 0 },
+    { id: 4, name: "首充赠送30元券", value: 30, minAmount: 50 }
+  ]);
+
   const statusMap: Record<number, { text: string; type: string }> = {
     0: { text: "草稿", type: "info" },
     1: { text: "已发布", type: "primary" },
@@ -219,6 +231,29 @@ export function useInviteActivity() {
             <ElDescriptionsItem label="活动说明" span={2}>{row.activityDesc || "暂无说明"}</ElDescriptionsItem>
           </ElDescriptions>
 
+          {/* 双向优惠券配置展示 */}
+          <div class="mt-4">
+            <h4 class="mb-3 text-base font-semibold">🎁 优惠券奖励配置</h4>
+            <ElDescriptions column={1} border>
+              <ElDescriptionsItem label="邀请人优惠券">
+                {row.inviterCouponTemplateId ? (
+                  <ElTag type="danger">已配置(ID: {row.inviterCouponTemplateId})</ElTag>
+                ) : (
+                  <span class="text-gray-400">未配置</span>
+                )}
+                <span class="ml-2 text-xs text-gray-500">被邀请人完成首单后发放</span>
+              </ElDescriptionsItem>
+              <ElDescriptionsItem label="被邀请人优惠券">
+                {row.inviteeCouponTemplateId ? (
+                  <ElTag type="warning">已配置(ID: {row.inviteeCouponTemplateId})</ElTag>
+                ) : (
+                  <span class="text-gray-400">未配置</span>
+                )}
+                <span class="ml-2 text-xs text-gray-500">注册绑定时即时发放</span>
+              </ElDescriptionsItem>
+            </ElDescriptions>
+          </div>
+
           <div class="mt-4">
             <h4 class="mb-2">统计数据</h4>
             <ElDescriptions column={3}>
@@ -287,6 +322,95 @@ export function useInviteActivity() {
             </div>
           </ElForm>
         </div>
+
+        {/* 双向优惠券配置 - 双卡片布局 */}
+        <ElDivider content-position="left">
+          <span style={{ fontSize: '16px', fontWeight: 'bold' }}>🎁 奖励配置(优惠券)</span>
+        </ElDivider>
+
+        <div class="reward-cards-container">
+          {/* 卡片1:邀请人奖励配置 */}
+          <ElCard shadow="hover" class="reward-card">
+            {{
+              header: () => (
+                <div class="card-header">
+                  <span class="card-title">🎁 邀请人奖励配置</span>
+                  <ElTag type="danger" size="small" effect="dark">必填</ElTag>
+                </div>
+              ),
+              default: () => (
+                <div class="card-content">
+                  <p class="card-description text-gray-500 text-sm mb-3">
+                    被邀请人完成首单后发放给邀请人的优惠券
+                  </p>
+                  <ElForm label-width="120px" size="default">
+                    <ElFormItem label="优惠券模板" required>
+                      <ElSelect
+                        v-model={formData.inviterCouponTemplateId}
+                        placeholder="请选择邀请人优惠券模板"
+                        clearable
+                        class="w-full"
+                      >
+                        {couponTemplateList.value.map(template => (
+                          <ElOption
+                            key={template.id}
+                            label={`${template.name}(满${template.minAmount}减${template.value}元)`}
+                            value={template.id}
+                          />
+                        ))}
+                      </ElSelect>
+                    </ElFormItem>
+                  </ElForm>
+                  <div class="tip-box mt-2">
+                    <el-icon class="mr-1"><InfoFilled /></el-icon>
+                    <span class="text-xs text-gray-400">选择后将自动关联该优惠券模板,被邀请人首单完成后系统将自动发放给邀请人</span>
+                  </div>
+                </div>
+              )
+            }}
+          </ElCard>
+
+          {/* 卡片2:被邀请人奖励配置 */}
+          <ElCard shadow="hover" class="reward-card invitee-card">
+            {{
+              header: () => (
+                <div class="card-header">
+                  <span class="card-title">🎊 被邀请人奖励配置</span>
+                  <ElTag type="warning" size="small" effect="plain">可选</ElTag>
+                </div>
+              ),
+              default: () => (
+                <div class="card-content">
+                  <p class="card-description text-gray-500 text-sm mb-3">
+                    注册绑定时即时发放给被邀请人的优惠券
+                  </p>
+                  <ElForm label-width="120px" size="default">
+                    <ElFormItem label="优惠券模板">
+                      <ElSelect
+                        v-model={formData.inviteeCouponTemplateId}
+                        placeholder="请选择被邀请人优惠券模板(可选)"
+                        clearable
+                        class="w-full"
+                      >
+                        {couponTemplateList.value.map(template => (
+                          <ElOption
+                            key={template.id}
+                            label={`${template.name}(满${template.minAmount}减${template.value}元)`}
+                            value={template.id}
+                          />
+                        ))}
+                      </ElSelect>
+                    </ElFormItem>
+                  </ElForm>
+                  <div class="tip-box mt-2">
+                    <el-icon class="mr-1"><InfoFilled /></el-icon>
+                    <span class="text-xs text-gray-400">可选配置,用于提升新用户注册转化率,注册成功后立即发放</span>
+                  </div>
+                </div>
+              )
+            }}
+          </ElCard>
+        </div>
       </div>
     );
   }
@@ -316,6 +440,19 @@ export function useInviteActivity() {
       message("请输入有效的被邀请人奖励金额", { type: "warning" });
       return false;
     }
+
+    // 联动校验:至少需要配置一组优惠券奖励
+    if (!formData.inviterCouponTemplateId && !formData.inviteeCouponTemplateId) {
+      ElMessage.warning('至少需要配置一组优惠券奖励(邀请人或被邀请人)');
+      return false;
+    }
+
+    // 校验邀请人优惠券(必填)
+    if (!formData.inviterCouponTemplateId) {
+      ElMessage.warning('请配置邀请人优惠券模板(必填项)');
+      return false;
+    }
+
     return true;
   }
 
@@ -332,7 +469,10 @@ export function useInviteActivity() {
       rewardRuleType: row.rewardRuleType || 1,
       inviterRewardAmount: row.inviterRewardAmount,
       inviteeRewardAmount: row.inviteeRewardAmount,
-      inviteeFirstChargeMinAmount: row.inviteeFirstChargeMinAmount
+      inviteeFirstChargeMinAmount: row.inviteeFirstChargeMinAmount,
+      // 双向优惠券配置
+      inviterCouponTemplateId: row.inviterCouponTemplateId || null,
+      inviteeCouponTemplateId: row.inviteeCouponTemplateId || null
     });
 
     addDialog({
@@ -369,7 +509,10 @@ export function useInviteActivity() {
       rewardRuleType: 1,
       inviterRewardAmount: null,
       inviteeRewardAmount: null,
-      inviteeFirstChargeMinAmount: undefined
+      inviteeFirstChargeMinAmount: undefined,
+      // 双向优惠券配置
+      inviterCouponTemplateId: null,
+      inviteeCouponTemplateId: null
     });
 
     addDialog({

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

@@ -12,6 +12,9 @@ interface InviteActivityFormProps {
   inviterRewardAmount?: number; // 邀请人奖励金额
   inviteeRewardAmount?: number; // 被邀请人奖励金额
   inviteeFirstChargeMinAmount?: number; // 被邀请人首充最低金额(元)
+  // 双向优惠券配置
+  inviterCouponTemplateId?: number | null; // 邀请人优惠券模板ID
+  inviteeCouponTemplateId?: number | null; // 被邀请人优惠券模板ID(可选)
   // 统计数据
   totalInviteCount?: number; // 总邀请数
   validInviteCount?: number; // 有效邀请数
@@ -48,6 +51,8 @@ interface InviteRecordItem {
   inviteTime: string;
   activateTime: string;
   status: number; // 0-待激活 1-已激活 2-奖励已发放 3-发放失败
+  rewardType?: number | null; // 奖励类型:0-邀请人推荐奖 1-被邀请人新人券
+  triggerType?: number | null; // 触发条件:0-首单完成 1-注册绑定
   rewardAmount?: number;
   createTime: string;
 }
@@ -56,6 +61,7 @@ interface InviteRecordsSearchParams {
   activityId?: number | undefined;
   inviterId?: string;
   status?: number | undefined;
+  rewardType?: number | undefined; // 奖励类型筛选
   startTime?: string;
   endTime?: string;
   page?: number;
@@ -70,6 +76,29 @@ interface InviteStatisticsData {
   todayNewInvites: number; // 今日新增邀请数
   conversionRate: number; // 转化率
   rewardSuccessRate: number; // 奖励发放成功率
+
+  // 新增:双向奖励统计 - 被邀请人优惠券
+  inviteeCoupon?: {
+    totalDistributed: number; // 发放总数
+    totalUsed: number; // 使用总数
+    usageRate: number; // 核销率 (%)
+  };
+
+  // 新增:双向奖励统计 - 邀请人优惠券
+  inviterCoupon?: {
+    totalDistributed: number; // 发放总数
+    totalUsed: number; // 使用总数
+    usageRate: number; // 核销率 (%)
+  };
+
+  // 新增:成本分析
+  costAnalysis?: {
+    totalDiscountAmount: number; // 总优惠金额(元)
+    gmv: number; // GMV(元)
+    roi: number; // ROI
+    avgAcquisitionCost: number; // 平均获客成本(元)
+  };
+
   // 趋势数据
   trendData?: Array<{
     date: string;

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

@@ -262,3 +262,17 @@ CREATE TABLE IF NOT EXISTS `t_invite_reward` (
 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`;
+
+-- ============================================================
+-- 邀请有礼活动双向奖励机制扩展(2026-04-03)
+-- 支持邀请人和被邀请人双方均可获得奖励
+-- ============================================================
+
+-- 为邀请活动配置表添加被邀请人奖励字段
+ALTER TABLE `t_invite_activity`
+  ADD COLUMN `invitee_coupon_template_id` bigint DEFAULT NULL COMMENT '被邀请人优惠券模板ID(支持双向奖励时配置)' AFTER `coupon_template_id`;
+
+-- 为邀请奖励发放记录表添加奖励类型和触发条件字段
+ALTER TABLE `t_invite_reward`
+  ADD COLUMN `reward_type` tinyint NOT NULL DEFAULT 0 COMMENT '奖励类型:0-邀请人奖励 1-被邀请人奖励' AFTER `inviter_user_id`,
+  ADD COLUMN `trigger_type` tinyint NOT NULL DEFAULT 0 COMMENT '触发条件:0-首单完成 1-邀请绑定' AFTER `reward_type`;

+ 7 - 1
haha-entity/src/main/java/com/haha/entity/InviteActivity.java

@@ -35,10 +35,16 @@ public class InviteActivity implements Serializable {
     private Integer status;
 
     /**
-     * 奖励优惠券模板ID
+     * 邀请人奖励优惠券模板ID
      */
     private Long couponTemplateId;
 
+    /**
+     * 被邀请人奖励优惠券模板ID(双向奖励时使用)
+     */
+    @TableField("invitee_coupon_template_id")
+    private Long inviteeCouponTemplateId;
+
     /**
      * 每人每日最大邀请数
      */

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

@@ -38,6 +38,18 @@ public class InviteReward implements Serializable {
      */
     private Long couponTemplateId;
 
+    /**
+     * 奖励类型:0-邀请人 1-被邀请人
+     */
+    @TableField("reward_type")
+    private Integer rewardType;
+
+    /**
+     * 触发类型:0-首单完成 1-邀请绑定
+     */
+    @TableField("trigger_type")
+    private Integer triggerType;
+
     /**
      * 用户优惠券ID
      */

+ 7 - 2
haha-entity/src/main/java/com/haha/entity/dto/InviteActivityCreateDTO.java

@@ -22,8 +22,13 @@ public class InviteActivityCreateDTO {
     @NotNull(message = "结束时间不能为空")
     private LocalDateTime endTime;
 
-    @NotNull(message = "奖励优惠券模板ID不能为空")
-    private Long couponTemplateId;
+    @NotNull(message = "邀请人奖励优惠券模板ID不能为空")
+    private Long inviterCouponTemplateId;
+
+    /**
+     * 被邀请人奖励优惠券模板ID(双向奖励时使用,可选)
+     */
+    private Long inviteeCouponTemplateId;
 
     /**
      * 每人每日最大邀请数

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

@@ -25,6 +25,16 @@ public class InviteActivityUpdateDTO {
 
     private Long couponTemplateId;
 
+    /**
+     * 邀请人奖励优惠券模板ID
+     */
+    private Long inviterCouponTemplateId;
+
+    /**
+     * 被邀请人奖励优惠券模板ID(双向奖励时使用)
+     */
+    private Long inviteeCouponTemplateId;
+
     private Integer dailyLimit;
 
     private Integer totalLimit;

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

@@ -18,6 +18,10 @@ public interface InviteRewardMapper extends BaseMapper<InviteReward> {
     @Select("SELECT * FROM t_invite_reward WHERE record_id = #{recordId} AND deleted = 0 LIMIT 1")
     InviteReward selectByRecordId(@Param("recordId") Long recordId);
 
+    // 根据邀请记录ID和奖励类型查询奖励记录(用于检查是否已发放过被邀请人奖励)
+    @Select("SELECT * FROM t_invite_reward WHERE record_id = #{recordId} AND reward_type = #{rewardType} AND deleted = 0 LIMIT 1")
+    InviteReward selectByRecordIdAndRewardType(@Param("recordId") Long recordId, @Param("rewardType") Integer rewardType);
+
     // 更新奖励状态为已发放
     @Update("UPDATE t_invite_reward SET status = 1, user_coupon_id = #{userCouponId}, coupon_code = #{couponCode}, " +
             "distribute_time = NOW(), update_time = NOW() WHERE id = #{id}")

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

@@ -146,6 +146,15 @@ public interface InviteActivityService extends IService<InviteActivity> {
      */
     boolean distributeReward(Long recordId);
 
+    /**
+     * 向被邀请人发放新人优惠券
+     * 当新用户通过邀请链接注册并绑定关系时,立即向被邀请人发放优惠券
+     *
+     * @param recordId 邀请记录ID
+     * @return 是否发放成功
+     */
+    boolean distributeInviteeCoupon(Long recordId);
+
     // ==================== 统计查询 ====================
 
     /**

+ 234 - 37
haha-service/src/main/java/com/haha/service/impl/InviteActivityServiceImpl.java

@@ -121,10 +121,29 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
             throw new BusinessException(400, "结束时间必须大于开始时间");
         }
 
-        // 校验优惠券模板是否存在且可用
-        CouponTemplate template = couponTemplateService.getById(dto.getCouponTemplateId());
-        if (template == null) {
-            throw new BusinessException(400, "优惠券模板不存在");
+        // 获取双向奖励配置
+        Long inviterCouponTemplateId = dto.getInviterCouponTemplateId();
+        Long inviteeCouponTemplateId = dto.getInviteeCouponTemplateId();
+
+        // 【核心校验】至少需要配置一组奖励(邀请人奖励或被邀请人奖励)
+        if (inviterCouponTemplateId == null && inviteeCouponTemplateId == null) {
+            throw new BusinessException(400, "至少需要配置一组奖励(邀请人奖励或被邀请人奖励)");
+        }
+
+        // 校验邀请人优惠券模板是否存在且可用(如果配置了)
+        if (inviterCouponTemplateId != null) {
+            CouponTemplate inviterTemplate = couponTemplateService.getById(inviterCouponTemplateId);
+            if (inviterTemplate == null) {
+                throw new BusinessException(400, "邀请人优惠券模板不存在");
+            }
+        }
+
+        // 校验被邀请人优惠券模板是否存在且可用(如果配置了)
+        if (inviteeCouponTemplateId != null) {
+            CouponTemplate inviteeTemplate = couponTemplateService.getById(inviteeCouponTemplateId);
+            if (inviteeTemplate == null) {
+                throw new BusinessException(400, "被邀请人优惠券模板不存在");
+            }
         }
 
         InviteActivity activity = new InviteActivity();
@@ -133,7 +152,8 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
         activity.setStartTime(dto.getStartTime());
         activity.setEndTime(dto.getEndTime());
         activity.setStatus(ActivityStatusEnum.DRAFT.getCode());
-        activity.setCouponTemplateId(dto.getCouponTemplateId());
+        activity.setCouponTemplateId(inviterCouponTemplateId); // 邀请人优惠券模板ID
+        activity.setInviteeCouponTemplateId(inviteeCouponTemplateId); // 被邀请人优惠券模板ID
         activity.setDailyLimit(dto.getDailyLimit() != null ? dto.getDailyLimit() : 10);
         activity.setTotalLimit(dto.getTotalLimit() != null ? dto.getTotalLimit() : 100);
         activity.setRequireFirstOrder(dto.getRequireFirstOrder() != null ? dto.getRequireFirstOrder() : 1);
@@ -150,7 +170,8 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
 
         this.save(activity);
 
-        log.info("创建邀请活动成功: id={}, name={}, creator={}", activity.getId(), activity.getActivityName(), creatorName);
+        log.info("创建邀请活动成功: id={}, name={}, creator={}, inviterCouponTemplateId={}, inviteeCouponTemplateId={}",
+                activity.getId(), activity.getActivityName(), creatorName, inviterCouponTemplateId, inviteeCouponTemplateId);
         return activity;
     }
 
@@ -186,9 +207,39 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
         if (dto.getEndTime() != null) {
             activity.setEndTime(dto.getEndTime());
         }
-        if (dto.getCouponTemplateId() != null) {
-            activity.setCouponTemplateId(dto.getCouponTemplateId());
+
+        // 【双向奖励】处理邀请人优惠券模板ID更新(优先使用 inviterCouponTemplateId,兼容旧的 couponTemplateId)
+        Long newInviterCouponTemplateId = dto.getInviterCouponTemplateId();
+        if (newInviterCouponTemplateId == null) {
+            newInviterCouponTemplateId = dto.getCouponTemplateId(); // 兼容旧字段
         }
+        if (newInviterCouponTemplateId != null) {
+            // 校验邀请人优惠券模板是否存在
+            CouponTemplate inviterTemplate = couponTemplateService.getById(newInviterCouponTemplateId);
+            if (inviterTemplate == null) {
+                throw new BusinessException(400, "邀请人优惠券模板不存在");
+            }
+            activity.setCouponTemplateId(newInviterCouponTemplateId);
+        }
+
+        // 【双向奖励】处理被邀请人优惠券模板ID更新
+        Long newInviteeCouponTemplateId = dto.getInviteeCouponTemplateId();
+        if (newInviteeCouponTemplateId != null) {
+            // 校验被邀请人优惠券模板是否存在
+            CouponTemplate inviteeTemplate = couponTemplateService.getById(newInviteeCouponTemplateId);
+            if (inviteeTemplate == null) {
+                throw new BusinessException(400, "被邀请人优惠券模板不存在");
+            }
+            activity.setInviteeCouponTemplateId(newInviteeCouponTemplateId);
+        }
+
+        // 【核心校验】更新后至少需要配置一组奖励
+        Long finalInviterCouponTemplateId = activity.getCouponTemplateId();
+        Long finalInviteeCouponTemplateId = activity.getInviteeCouponTemplateId();
+        if (finalInviterCouponTemplateId == null && finalInviteeCouponTemplateId == null) {
+            throw new BusinessException(400, "至少需要配置一组奖励(邀请人奖励或被邀请人奖励)");
+        }
+
         if (dto.getDailyLimit() != null) {
             activity.setDailyLimit(dto.getDailyLimit());
         }
@@ -213,7 +264,8 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
 
         this.updateById(activity);
 
-        log.info("更新邀请活动成功: id={}, name={}", activity.getId(), activity.getActivityName());
+        log.info("更新邀请活动成功: id={}, name={}, inviterCouponTemplateId={}, inviteeCouponTemplateId={}",
+                activity.getId(), activity.getActivityName(), finalInviterCouponTemplateId, finalInviteeCouponTemplateId);
         return activity;
     }
 
@@ -229,6 +281,13 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
             throw new BusinessException(400, "当前状态不允许发布");
         }
 
+        // 【发布前校验】检查是否已配置至少一组奖励(邀请人奖励或被邀请人奖励)
+        Long inviterCouponTemplateId = activity.getCouponTemplateId();
+        Long inviteeCouponTemplateId = activity.getInviteeCouponTemplateId();
+        if (inviterCouponTemplateId == null && inviteeCouponTemplateId == null) {
+            throw new BusinessException(400, "发布失败:活动未配置任何奖励,请至少配置邀请人奖励或被邀请人奖励");
+        }
+
         LocalDateTime now = LocalDateTime.now();
         int newStatus = (now.isAfter(activity.getStartTime()) && now.isBefore(activity.getEndTime()))
                 ? ActivityStatusEnum.ONGOING.getCode()
@@ -237,7 +296,8 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
         boolean result = inviteActivityMapper.updateStatus(id, newStatus) > 0;
 
         if (result) {
-            log.info("发布邀请活动成功: id={}, status={}", id, newStatus);
+            log.info("发布邀请活动成功: id={}, status={}, inviterCouponTemplateId={}, inviteeCouponTemplateId={}",
+                    id, newStatus, inviterCouponTemplateId, inviteeCouponTemplateId);
         }
         return result;
     }
@@ -410,6 +470,20 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
                     inviterUserId, notifyEx.getMessage(), notifyEx);
         }
 
+        // 8. 向被邀请人即时发放新人优惠券(异步,发券失败不影响绑定关系)
+        try {
+            boolean couponResult = distributeInviteeCoupon(record.getId());
+            if (couponResult) {
+                log.info("被邀请人优惠券发放成功: recordId={}, inviteeUserId={}", record.getId(), inviteeUserId);
+            } else {
+                log.warn("被邀请人优惠券发放失败(已记录,可后续手动补发): recordId={}, inviteeUserId={}",
+                        record.getId(), inviteeUserId);
+            }
+        } catch (Exception couponEx) {
+            log.error("向被邀请人发放优惠券异常(不影响绑定关系): recordId={}, inviteeUserId={}, error={}",
+                    record.getId(), inviteeUserId, couponEx.getMessage(), couponEx);
+        }
+
         log.info("绑定邀请关系成功: recordId={}, inviterUserId={}, inviteeUserId={}, inviteCode={}",
                 record.getId(), inviterUserId, inviteeUserId, inviteCode);
         return true;
@@ -473,39 +547,60 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean distributeReward(Long recordId) {
+        log.info("[邀请人奖励] 开始发放邀请人奖励: recordId={}", recordId);
+
         // 1. 根据记录ID查询邀请记录
         InviteRecord record = inviteRecordMapper.selectById(recordId);
         if (record == null) {
-            log.error("邀请记录不存在: recordId={}", recordId);
+            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());
+            log.warn("[邀请人奖励] 邀请记录状态不正确,无法发放奖励: recordId={}, status={}", recordId, record.getStatus());
             return false;
         }
 
-        // 4. 获取活动配置的优惠券模板ID
+        // 3. 获取活动配置
         InviteActivity activity = this.getById(record.getActivityId());
         if (activity == null) {
-            log.error("活动不存在: activityId={}", record.getActivityId());
+            log.error("[邀请人奖励] 活动不存在: activityId={}", record.getActivityId());
             return false;
         }
 
-        // 5. 创建待发放的奖励记录(先创建记录,再发放优惠券)
+        // 4. 【新增】配置检查:仅在配置了邀请人优惠券模板时才发放(与被邀请人即时发券逻辑解耦)
+        Long inviterCouponTemplateId = activity.getCouponTemplateId(); // couponTemplateId 即为邀请人优惠券模板ID
+        if (inviterCouponTemplateId == null) {
+            log.info("[邀请人奖励] 活动未配置邀请人优惠券模板,跳过发放: activityId={}, recordId={}, inviterUserId={}",
+                    activity.getId(), recordId, record.getInviterUserId());
+            return true; // 返回true表示成功(虽然没发券,但这是预期行为,兼容单向奖励模式)
+        }
+
+        // 5. 【优化】防重复发放检查:查询是否已存在该邀请记录的邀请人奖励记录(reward_type=0)
+        InviteReward existingInviterReward = inviteRewardMapper.selectByRecordIdAndRewardType(recordId, 0); // rewardType=0 表示邀请人
+        if (existingInviterReward != null) {
+            if (existingInviterReward.getStatus() == InviteRewardStatusEnum.DISTRIBUTED.getCode()) {
+                log.warn("[邀请人奖励] 邀请人奖励已发放,不能重复发放: recordId={}, rewardId={}, inviterUserId={}",
+                        recordId, existingInviterReward.getId(), record.getInviterUserId());
+                return false;
+            } else if (existingInviterReward.getStatus() == InviteRewardStatusEnum.PENDING.getCode()
+                    || existingInviterReward.getStatus() == InviteRewardStatusEnum.FAILED.getCode()) {
+                log.warn("[邀请人奖励] 邀请人奖励记录已存在(状态={}),不能重复创建: recordId={}, rewardId={}, status={}",
+                        existingInviterReward.getStatus() == InviteRewardStatusEnum.PENDING.getCode() ? "待发放" : "失败",
+                        recordId, existingInviterReward.getId(), existingInviterReward.getStatus());
+                return false;
+            }
+        }
+
+        // 6. 创建待发放的奖励记录(先创建记录,再发放优惠券)
         InviteReward reward = new InviteReward();
         reward.setActivityId(record.getActivityId());
         reward.setRecordId(recordId);
         reward.setInviterUserId(record.getInviterUserId());
-        reward.setCouponTemplateId(activity.getCouponTemplateId());
+        reward.setCouponTemplateId(inviterCouponTemplateId); // 使用经过配置检查的模板ID
+        reward.setRewardType(0); // 邀请人奖励(确保与被邀请人奖励rewardType=1区分)
+        reward.setTriggerType(0); // 首单完成触发(确保与被邀请人即时发放triggerType=1区分)
         reward.setStatus(InviteRewardStatusEnum.PENDING.getCode()); // 待发放
         reward.setUserCouponId(null);
         reward.setCouponCode(null);
@@ -517,12 +612,15 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
 
         inviteRewardMapper.insert(reward);
 
-        // 6. 调用UserCouponService发放优惠券给邀请人
+        log.info("[邀请人奖励] 创建邀请人奖励记录成功: recordId={}, rewardId={}, inviterUserId={}, couponTemplateId={}",
+                recordId, reward.getId(), record.getInviterUserId(), inviterCouponTemplateId);
+
+        // 7. 调用UserCouponService发放优惠券给邀请人
         try {
             CouponDistributeDTO distributeDTO = new CouponDistributeDTO();
             distributeDTO.setUserId(record.getInviterUserId());
-            distributeDTO.setTemplateId(activity.getCouponTemplateId());
-            distributeDTO.setSource("邀请活动奖励");
+            distributeDTO.setTemplateId(inviterCouponTemplateId); // 使用经过配置检查的模板ID
+            distributeDTO.setSource("邀请活动-邀请人奖励");
             distributeDTO.setSourceId(String.valueOf(recordId));
 
             userCouponService.distributeCoupon(distributeDTO, null, "系统自动发放");
@@ -530,21 +628,21 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
             // TODO: 需要从distributeCoupon返回值中获取userCouponId和couponCode
             // 这里暂时设置为占位值,实际应根据返回值更新
             Long userCouponId = -1L; // 应从实际发放结果获取
-            String couponCode = "AUTO_" + System.currentTimeMillis(); // 应从实际发放结果获取
+            String couponCode = "INVITER_" + System.currentTimeMillis(); // 应从实际发放结果获取
 
-            // 7. 更新奖励记录为已发放
+            // 8. 更新奖励记录为已发放
             inviteRewardMapper.updateToDistributed(reward.getId(), userCouponId, couponCode);
 
-            // 8. 更新邀请记录状态为已奖励
+            // 9. 更新邀请记录状态为已奖励
             inviteRecordMapper.updateToRewarded(recordId);
 
-            // 9. 更新活动的奖励发放数+1
+            // 10. 更新活动的奖励发放数+1
             inviteActivityMapper.incrementRewardCount(record.getActivityId());
 
-            // 10. 发送消息通知
+            // 11. 发送消息通知(仅邀请人奖励通知)
             try {
                 // 获取优惠券模板信息用于通知
-                CouponTemplate couponTemplate = couponTemplateService.getById(activity.getCouponTemplateId());
+                CouponTemplate couponTemplate = couponTemplateService.getById(inviterCouponTemplateId);
                 if (couponTemplate != null) {
                     notificationService.sendRewardSuccessNotification(
                         record.getInviterUserId(),
@@ -553,18 +651,117 @@ public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper,
                     );
                 }
             } catch (Exception notifyEx) {
-                log.error("发送奖励通知失败(不影响业务): recordId={}, error={}",
+                log.error("[邀请人奖励] 发送奖励通知失败(不影响业务): recordId={}, error={}",
                         recordId, notifyEx.getMessage(), notifyEx);
             }
 
-            log.info("发放邀请奖励成功: recordId={}, rewardId={}, inviterUserId={}",
-                    recordId, reward.getId(), record.getInviterUserId());
+            log.info("[邀请人奖励] 发放邀请奖励成功: recordId={}, rewardId={}, inviterUserId={}, userCouponId={}",
+                    recordId, reward.getId(), record.getInviterUserId(), userCouponId);
             return true;
 
         } catch (Exception e) {
             // 发放失败,更新奖励状态为失败
-            log.error("发放优惠券失败: recordId={}, error={}", recordId, e.getMessage(), e);
-            inviteRewardMapper.updateToFailed(reward.getId(), "优惠券发放失败: " + e.getMessage());
+            log.error("[邀请人奖励] 发放优惠券失败: recordId={}, inviterUserId={}, couponTemplateId={}, error={}",
+                    recordId, record.getInviterUserId(), inviterCouponTemplateId, e.getMessage(), e);
+            inviteRewardMapper.updateToFailed(reward.getId(), "邀请人优惠券发放失败: " + e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean distributeInviteeCoupon(Long recordId) {
+        log.info("开始向被邀请人发放新人优惠券: recordId={}", recordId);
+
+        // 1. 查询邀请记录
+        InviteRecord record = inviteRecordMapper.selectById(recordId);
+        if (record == null) {
+            log.error("邀请记录不存在,无法发放被邀请人优惠券: recordId={}", recordId);
+            return false;
+        }
+
+        // 2. 检查是否已发放过被邀请人奖励(防止重复发放)
+        InviteReward existingInviteeReward = inviteRewardMapper.selectByRecordIdAndRewardType(recordId, 1); // rewardType=1 表示被邀请人
+        if (existingInviteeReward != null && existingInviteeReward.getStatus() == InviteRewardStatusEnum.DISTRIBUTED.getCode()) {
+            log.warn("被邀请人奖励已发放,不能重复发放: recordId={}, rewardId={}", recordId, existingInviteeReward.getId());
+            return false;
+        }
+
+        // 3. 查询活动配置,获取 inviteeCouponTemplateId
+        InviteActivity activity = this.getById(record.getActivityId());
+        if (activity == null) {
+            log.error("活动不存在,无法发放被邀请人优惠券: activityId={}", record.getActivityId());
+            return false;
+        }
+
+        // 4. 如果 inviteeCouponTemplateId 为空,跳过不发券(兼容单向奖励模式)
+        if (activity.getInviteeCouponTemplateId() == null) {
+            log.info("活动未配置被邀请人优惠券模板,跳过发券: activityId={}, recordId={}", activity.getId(), recordId);
+            return true; // 返回true表示成功(虽然没发券,但这是预期行为)
+        }
+
+        // 5. 创建待发放的被邀请人奖励记录
+        InviteReward inviteeReward = new InviteReward();
+        inviteeReward.setActivityId(record.getActivityId());
+        inviteeReward.setRecordId(recordId);
+        inviteeReward.setInviterUserId(record.getInviteeUserId()); // 注意:这里是被邀请人的用户ID
+        inviteeReward.setCouponTemplateId(activity.getInviteeCouponTemplateId());
+        inviteeReward.setRewardType(1); // 被邀请人奖励
+        inviteeReward.setTriggerType(1); // 邀请绑定触发(即时发放)
+        inviteeReward.setStatus(InviteRewardStatusEnum.PENDING.getCode()); // 待发放
+        inviteeReward.setUserCouponId(null);
+        inviteeReward.setCouponCode(null);
+        inviteeReward.setFailReason(null);
+        inviteeReward.setDistributeTime(null);
+        inviteeReward.setRetryCount(0);
+        inviteeReward.setOperatorId(null);
+        inviteeReward.setOperatorName(null);
+
+        inviteRewardMapper.insert(inviteeReward);
+
+        // 6. 调用优惠券服务发放优惠券给被邀请人
+        try {
+            CouponDistributeDTO distributeDTO = new CouponDistributeDTO();
+            distributeDTO.setUserId(record.getInviteeUserId()); // 发放给被邀请人
+            distributeDTO.setTemplateId(activity.getInviteeCouponTemplateId()); // 使用被邀请人优惠券模板
+            distributeDTO.setSource("邀请活动-新人注册奖励");
+            distributeDTO.setSourceId(String.valueOf(recordId));
+
+            userCouponService.distributeCoupon(distributeDTO, null, "系统自动发放");
+
+            // TODO: 需要从distributeCoupon返回值中获取userCouponId和couponCode
+            Long userCouponId = -1L; // 应从实际发放结果获取
+            String couponCode = "INVITEE_" + System.currentTimeMillis(); // 应从实际发放结果获取
+
+            // 7. 更新奖励记录为已发放
+            inviteRewardMapper.updateToDistributed(inviteeReward.getId(), userCouponId, couponCode);
+
+            // 8. 记录日志
+            log.info("向被邀请人发放新人优惠券成功: recordId={}, rewardId={}, inviteeUserId={}, couponTemplateId={}",
+                    recordId, inviteeReward.getId(), record.getInviteeUserId(), activity.getInviteeCouponTemplateId());
+
+            // 9. 发送通知给被邀请人(可选)
+            try {
+                CouponTemplate couponTemplate = couponTemplateService.getById(activity.getInviteeCouponTemplateId());
+                if (couponTemplate != null) {
+                    notificationService.sendRewardSuccessNotification(
+                        record.getInviteeUserId(),
+                        couponTemplate.getCouponName(),
+                        couponTemplate.getDiscountValue()
+                    );
+                }
+            } catch (Exception notifyEx) {
+                log.error("发送被邀请人奖励通知失败(不影响业务): recordId={}, error={}",
+                        recordId, notifyEx.getMessage(), notifyEx);
+            }
+
+            return true;
+
+        } catch (Exception e) {
+            // 10. 发券失败时记录到 InviteReward 表(status=失败, 失败原因),不抛出异常
+            log.error("向被邀请人发放优惠券失败: recordId={}, inviteeUserId={}, error={}",
+                    recordId, record.getInviteeUserId(), e.getMessage(), e);
+            inviteRewardMapper.updateToFailed(inviteeReward.getId(), "被邀请人优惠券发放失败: " + e.getMessage());
             return false;
         }
     }