skyline 1 місяць тому
батько
коміт
5bc93b283b
73 змінених файлів з 7154 додано та 40 видалено
  1. 132 0
      haha-admin-mp/src/api/distribution.ts
  2. 27 0
      haha-admin-mp/src/pages.json
  3. 399 0
      haha-admin-mp/src/pages/distribution/index.vue
  4. 277 0
      haha-admin-mp/src/pages/distribution/records.vue
  5. 301 0
      haha-admin-mp/src/pages/distribution/withdrawal.vue
  6. 108 0
      haha-admin-web/src/api/distribution.ts
  7. 99 0
      haha-admin-web/src/router/modules/distribution.ts
  8. 151 0
      haha-admin-web/src/views/distribution/commission/index.vue
  9. 181 0
      haha-admin-web/src/views/distribution/commission/utils/hook.tsx
  10. 184 0
      haha-admin-web/src/views/distribution/config/index.vue
  11. 234 0
      haha-admin-web/src/views/distribution/dashboard/index.vue
  12. 577 0
      haha-admin-web/src/views/distribution/distributor/detail.vue
  13. 239 0
      haha-admin-web/src/views/distribution/distributor/index.vue
  14. 140 0
      haha-admin-web/src/views/distribution/referral/index.vue
  15. 171 0
      haha-admin-web/src/views/distribution/referral/utils/hook.tsx
  16. 241 0
      haha-admin-web/src/views/distribution/report/index.vue
  17. 424 0
      haha-admin-web/src/views/distribution/utils/hook.tsx
  18. 182 0
      haha-admin-web/src/views/distribution/utils/types.ts
  19. 183 0
      haha-admin-web/src/views/distribution/withdrawal/index.vue
  20. 280 0
      haha-admin-web/src/views/distribution/withdrawal/utils/hook.tsx
  21. 2 1
      haha-admin-web/vite.config.ts
  22. 2 2
      haha-admin/src/main/java/com/haha/admin/config/JacksonConfig.java
  23. 200 0
      haha-admin/src/main/java/com/haha/admin/controller/DistributorAdminController.java
  24. 61 0
      haha-admin/src/main/java/com/haha/admin/controller/DistributorMpController.java
  25. 64 0
      haha-admin/src/main/java/com/haha/admin/task/DistributorTask.java
  26. 3 37
      haha-common/src/main/java/com/haha/common/vo/PageResult.java
  27. 6 0
      haha-entity/pom.xml
  28. 67 0
      haha-entity/src/main/java/com/haha/entity/Distributor.java
  29. 63 0
      haha-entity/src/main/java/com/haha/entity/DistributorCommission.java
  30. 34 0
      haha-entity/src/main/java/com/haha/entity/DistributorConfig.java
  31. 54 0
      haha-entity/src/main/java/com/haha/entity/DistributorMonthlyReport.java
  32. 63 0
      haha-entity/src/main/java/com/haha/entity/DistributorReferral.java
  33. 58 0
      haha-entity/src/main/java/com/haha/entity/DistributorWithdrawal.java
  34. 21 0
      haha-entity/src/main/java/com/haha/entity/dto/CommissionQueryDTO.java
  35. 14 0
      haha-entity/src/main/java/com/haha/entity/dto/DistributorConfigDTO.java
  36. 16 0
      haha-entity/src/main/java/com/haha/entity/dto/DistributorCreateDTO.java
  37. 17 0
      haha-entity/src/main/java/com/haha/entity/dto/DistributorQueryDTO.java
  38. 17 0
      haha-entity/src/main/java/com/haha/entity/dto/DistributorUpdateDTO.java
  39. 13 0
      haha-entity/src/main/java/com/haha/entity/dto/MonthlyReportQueryDTO.java
  40. 19 0
      haha-entity/src/main/java/com/haha/entity/dto/ReferralQueryDTO.java
  41. 15 0
      haha-entity/src/main/java/com/haha/entity/dto/WithdrawalApplyDTO.java
  42. 17 0
      haha-entity/src/main/java/com/haha/entity/dto/WithdrawalQueryDTO.java
  43. 16 0
      haha-entity/src/main/java/com/haha/entity/dto/WithdrawalReviewDTO.java
  44. 44 0
      haha-entity/src/main/java/com/haha/entity/vo/CommissionVO.java
  45. 25 0
      haha-entity/src/main/java/com/haha/entity/vo/DistributorConfigVO.java
  46. 32 0
      haha-entity/src/main/java/com/haha/entity/vo/DistributorDashboardVO.java
  47. 50 0
      haha-entity/src/main/java/com/haha/entity/vo/DistributorDetailVO.java
  48. 27 0
      haha-entity/src/main/java/com/haha/entity/vo/DistributorMpStatsVO.java
  49. 48 0
      haha-entity/src/main/java/com/haha/entity/vo/DistributorVO.java
  50. 40 0
      haha-entity/src/main/java/com/haha/entity/vo/MonthlyReportVO.java
  51. 23 0
      haha-entity/src/main/java/com/haha/entity/vo/MonthlyTrendVO.java
  52. 42 0
      haha-entity/src/main/java/com/haha/entity/vo/ReferralVO.java
  53. 40 0
      haha-entity/src/main/java/com/haha/entity/vo/WithdrawalVO.java
  54. 23 0
      haha-mapper/src/main/java/com/haha/mapper/DistributorCommissionMapper.java
  55. 14 0
      haha-mapper/src/main/java/com/haha/mapper/DistributorConfigMapper.java
  56. 38 0
      haha-mapper/src/main/java/com/haha/mapper/DistributorMapper.java
  57. 19 0
      haha-mapper/src/main/java/com/haha/mapper/DistributorMonthlyReportMapper.java
  58. 20 0
      haha-mapper/src/main/java/com/haha/mapper/DistributorReferralMapper.java
  59. 9 0
      haha-mapper/src/main/java/com/haha/mapper/DistributorWithdrawalMapper.java
  60. 17 0
      haha-service/src/main/java/com/haha/service/DistributorCommissionService.java
  61. 31 0
      haha-service/src/main/java/com/haha/service/DistributorConfigService.java
  62. 8 0
      haha-service/src/main/java/com/haha/service/DistributorQrcodeService.java
  63. 15 0
      haha-service/src/main/java/com/haha/service/DistributorReferralService.java
  64. 22 0
      haha-service/src/main/java/com/haha/service/DistributorReportService.java
  65. 25 0
      haha-service/src/main/java/com/haha/service/DistributorService.java
  66. 20 0
      haha-service/src/main/java/com/haha/service/DistributorWithdrawalService.java
  67. 209 0
      haha-service/src/main/java/com/haha/service/impl/DistributorCommissionServiceImpl.java
  68. 89 0
      haha-service/src/main/java/com/haha/service/impl/DistributorConfigServiceImpl.java
  69. 75 0
      haha-service/src/main/java/com/haha/service/impl/DistributorQrcodeServiceImpl.java
  70. 148 0
      haha-service/src/main/java/com/haha/service/impl/DistributorReferralServiceImpl.java
  71. 255 0
      haha-service/src/main/java/com/haha/service/impl/DistributorReportServiceImpl.java
  72. 206 0
      haha-service/src/main/java/com/haha/service/impl/DistributorServiceImpl.java
  73. 168 0
      haha-service/src/main/java/com/haha/service/impl/DistributorWithdrawalServiceImpl.java

+ 132 - 0
haha-admin-mp/src/api/distribution.ts

@@ -0,0 +1,132 @@
+/**
+ * 分销功能相关API
+ */
+
+import { USE_MOCK } from '@/utils/config';
+
+export enum WithdrawalStatus {
+  PENDING = 'pending',
+  APPROVED = 'approved',
+  REJECTED = 'rejected',
+  PAID = 'paid'
+}
+
+export enum WithdrawalStatusLabel {
+  PENDING = '审核中',
+  APPROVED = '已通过',
+  REJECTED = '已驳回',
+  PAID = '已打款'
+}
+
+export interface DistributionStats {
+  totalReferrals: number;
+  validReferrals: number;
+  commissionBalance: number;
+  frozenAmount: number;
+  totalCommission: number;
+  totalWithdrawn: number;
+}
+
+export interface WithdrawalRecord {
+  id: string;
+  amount: number;
+  status: WithdrawalStatus;
+  createTime: string;
+  auditTime?: string;
+  payTime?: string;
+  rejectReason?: string;
+}
+
+export interface WithdrawalListResponse {
+  list: WithdrawalRecord[];
+  total: number;
+  page: number;
+  pageSize: number;
+}
+
+export interface WithdrawalQueryParams {
+  page?: number;
+  pageSize?: number;
+  status?: WithdrawalStatus;
+}
+
+// Mock数据
+const mockStats: DistributionStats = {
+  totalReferrals: 56,
+  validReferrals: 38,
+  commissionBalance: 256.80,
+  frozenAmount: 50.00,
+  totalCommission: 1200.00,
+  totalWithdrawn: 943.20
+};
+
+const mockWithdrawalRecords: WithdrawalRecord[] = [
+  { id: '1', amount: 100, status: WithdrawalStatus.PAID, createTime: '2026-04-10 14:30:00', auditTime: '2026-04-10 16:00:00', payTime: '2026-04-11 09:00:00' },
+  { id: '2', amount: 200, status: WithdrawalStatus.APPROVED, createTime: '2026-04-12 10:20:00', auditTime: '2026-04-12 15:30:00' },
+  { id: '3', amount: 50, status: WithdrawalStatus.PENDING, createTime: '2026-04-15 09:00:00' },
+  { id: '4', amount: 80, status: WithdrawalStatus.REJECTED, createTime: '2026-04-13 11:00:00', auditTime: '2026-04-13 14:00:00', rejectReason: '余额不足' },
+  { id: '5', amount: 150, status: WithdrawalStatus.PAID, createTime: '2026-04-08 16:45:00', auditTime: '2026-04-09 09:00:00', payTime: '2026-04-09 15:00:00' }
+];
+
+/**
+ * 绑定分销员
+ */
+export async function bindDistributor(distributorCode: string): Promise<{ success: boolean; message: string }> {
+  if (USE_MOCK) {
+    return Promise.resolve({ success: true, message: '绑定成功' });
+  }
+
+  const { post } = await import('@/utils/request');
+  return post('/mp/distribution/bind', { distributorCode });
+}
+
+/**
+ * 获取我的分销统计
+ */
+export async function getMyStats(): Promise<DistributionStats> {
+  if (USE_MOCK) {
+    return Promise.resolve(mockStats);
+  }
+
+  const { get } = await import('@/utils/request');
+  return get('/mp/distribution/my-stats');
+}
+
+/**
+ * 申请提现
+ */
+export async function applyWithdrawal(amount: number): Promise<{ success: boolean; message: string }> {
+  if (USE_MOCK) {
+    return Promise.resolve({ success: true, message: '提现申请已提交' });
+  }
+
+  const { post } = await import('@/utils/request');
+  return post('/mp/distribution/withdrawal', { amount });
+}
+
+/**
+ * 获取提现记录列表
+ */
+export async function getMyWithdrawalList(params: WithdrawalQueryParams = {}): Promise<WithdrawalListResponse> {
+  if (USE_MOCK) {
+    const { page = 1, pageSize = 10, status } = params;
+    let list = [...mockWithdrawalRecords];
+
+    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('/mp/distribution/withdrawal/list', params);
+}

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

@@ -134,6 +134,33 @@
 				"navigationBarTextStyle": "black",
 				"navigationStyle": "custom"
 			}
+		},
+		{
+			"path": "pages/distribution/index",
+			"style": {
+				"navigationBarTitleText": "分销中心",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/distribution/withdrawal",
+			"style": {
+				"navigationBarTitleText": "申请提现",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/distribution/records",
+			"style": {
+				"navigationBarTitleText": "提现记录",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
 		}
 	],
 	"globalStyle": {

+ 399 - 0
haha-admin-mp/src/pages/distribution/index.vue

@@ -0,0 +1,399 @@
+<template>
+  <view class="page">
+    <NavBar title="分销中心" :showBack="true" />
+
+    <view class="content">
+      <!-- 佣金概览 -->
+      <view class="commission-overview" v-if="myStats">
+        <view class="overview-header">
+          <text class="overview-title">佣金概览</text>
+        </view>
+        <view class="overview-amount">
+          <text class="amount-symbol">¥</text>
+          <text class="amount-value">{{ myStats.commissionBalance.toFixed(2) }}</text>
+        </view>
+        <text class="overview-desc">可提现余额</text>
+
+        <view class="overview-stats">
+          <view class="overview-stat-item">
+            <text class="stat-num">{{ myStats.totalCommission.toFixed(2) }}</text>
+            <text class="stat-label">累计佣金</text>
+          </view>
+          <view class="overview-stat-divider"></view>
+          <view class="overview-stat-item">
+            <text class="stat-num frozen">{{ myStats.frozenAmount.toFixed(2) }}</text>
+            <text class="stat-label">冻结金额</text>
+          </view>
+          <view class="overview-stat-divider"></view>
+          <view class="overview-stat-item">
+            <text class="stat-num">{{ myStats.totalWithdrawn.toFixed(2) }}</text>
+            <text class="stat-label">已提现</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 推荐数据统计 -->
+      <view class="referral-stats" v-if="myStats">
+        <view class="stat-card">
+          <text class="stat-num">{{ myStats.totalReferrals || 0 }}</text>
+          <text class="stat-label">累计推荐</text>
+        </view>
+        <view class="stat-divider"></view>
+        <view class="stat-card">
+          <text class="stat-num success">{{ myStats.validReferrals || 0 }}</text>
+          <text class="stat-label">有效推荐</text>
+        </view>
+      </view>
+
+      <!-- 快捷操作 -->
+      <view class="action-section">
+        <button class="btn-primary" @click="goToWithdrawal">
+          <text>申请提现</text>
+        </button>
+        <button class="btn-secondary" @click="goToRecords">
+          <text>提现记录</text>
+          <view class="arrow-right"></view>
+        </button>
+      </view>
+
+      <!-- 最近提现记录 -->
+      <view class="recent-section">
+        <view class="section-header">
+          <text class="section-title">最近提现</text>
+          <text class="section-more" @click="goToRecords">查看全部</text>
+        </view>
+
+        <view v-if="recentList.length === 0" class="empty-tip">
+          <text>暂无提现记录</text>
+        </view>
+
+        <view
+          class="record-item"
+          v-for="item in recentList"
+          :key="item.id"
+        >
+          <view class="record-left">
+            <text class="record-amount">¥{{ item.amount.toFixed(2) }}</text>
+            <text class="record-time">{{ item.createTime }}</text>
+          </view>
+          <view class="record-right">
+            <text :class="['record-status', `status-${item.status}`]">
+              {{ getStatusLabel(item.status) }}
+            </text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import { getMyStats, getMyWithdrawalList } from '@/api/distribution';
+import type { DistributionStats, WithdrawalRecord } from '@/api/distribution';
+import { WithdrawalStatus, WithdrawalStatusLabel } from '@/api/distribution';
+
+const myStats = ref<DistributionStats | null>(null);
+const recentList = ref<WithdrawalRecord[]>([]);
+
+onMounted(() => {
+  loadStats();
+  loadRecentRecords();
+});
+
+const loadStats = async () => {
+  try {
+    myStats.value = await getMyStats();
+  } catch (error) {
+    console.error('加载分销统计失败', error);
+  }
+};
+
+const loadRecentRecords = async () => {
+  try {
+    const result = await getMyWithdrawalList({ page: 1, pageSize: 5 });
+    recentList.value = result.list;
+  } catch (error) {
+    console.error('加载提现记录失败', error);
+  }
+};
+
+const getStatusLabel = (status: WithdrawalStatus): string => {
+  return WithdrawalStatusLabel[status] || '未知';
+};
+
+const goToWithdrawal = () => {
+  uni.navigateTo({
+    url: '/pages/distribution/withdrawal'
+  });
+};
+
+const goToRecords = () => {
+  uni.navigateTo({
+    url: '/pages/distribution/records'
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+}
+
+.content {
+  padding: 24rpx;
+  padding-top: 0;
+}
+
+.commission-overview {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  padding: 36rpx;
+  margin-bottom: 24rpx;
+  color: #ffffff;
+}
+
+.overview-header {
+  margin-bottom: 20rpx;
+}
+
+.overview-title {
+  font-size: 28rpx;
+  color: rgba(255, 255, 255, 0.85);
+}
+
+.overview-amount {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 8rpx;
+}
+
+.amount-symbol {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #ffffff;
+  margin-right: 4rpx;
+}
+
+.amount-value {
+  font-size: 64rpx;
+  font-weight: 700;
+  color: #ffffff;
+}
+
+.overview-desc {
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.7);
+  display: block;
+  margin-bottom: 28rpx;
+}
+
+.overview-stats {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 0 0;
+  border-top: 1rpx solid rgba(255, 255, 255, 0.2);
+}
+
+.overview-stat-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.overview-stat-item .stat-num {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #ffffff;
+  margin-bottom: 6rpx;
+
+  &.frozen {
+    color: #fbbf24;
+  }
+}
+
+.overview-stat-item .stat-label {
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.7);
+}
+
+.overview-stat-divider {
+  width: 1rpx;
+  height: 60rpx;
+  background: rgba(255, 255, 255, 0.2);
+}
+
+.referral-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-card .stat-num {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 8rpx;
+
+  &.success {
+    color: #10b981;
+  }
+}
+
+.stat-card .stat-label {
+  font-size: 24rpx;
+  color: #64748b;
+}
+
+.stat-divider {
+  width: 1rpx;
+  height: 60rpx;
+  background: #e2e8f0;
+}
+
+.action-section {
+  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: 16rpx;
+
+  &:active {
+    opacity: 0.9;
+  }
+}
+
+.btn-secondary {
+  width: 100%;
+  height: 88rpx;
+  background: #ffffff;
+  border: 2rpx solid #e2e8f0;
+  border-radius: 44rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #475569;
+
+  &:active {
+    background: #f8fafc;
+  }
+}
+
+.arrow-right {
+  width: 12rpx;
+  height: 12rpx;
+  border-top: 3rpx solid #94a3b8;
+  border-right: 3rpx solid #94a3b8;
+  transform: rotate(45deg);
+  margin-left: 8rpx;
+}
+
+.recent-section {
+  background: #ffffff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  border: 1rpx solid #e2e8f0;
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1e293b;
+}
+
+.section-more {
+  font-size: 24rpx;
+  color: #3b82f6;
+}
+
+.empty-tip {
+  padding: 40rpx 0;
+  text-align: center;
+  font-size: 26rpx;
+  color: #94a3b8;
+}
+
+.record-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f1f5f9;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.record-left {
+  display: flex;
+  flex-direction: column;
+}
+
+.record-amount {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1e293b;
+  margin-bottom: 4rpx;
+}
+
+.record-time {
+  font-size: 22rpx;
+  color: #94a3b8;
+}
+
+.record-status {
+  font-size: 24rpx;
+  font-weight: 500;
+
+  &.status-pending {
+    color: #f59e0b;
+  }
+
+  &.status-approved {
+    color: #3b82f6;
+  }
+
+  &.status-rejected {
+    color: #ef4444;
+  }
+
+  &.status-paid {
+    color: #10b981;
+  }
+}
+</style>

+ 277 - 0
haha-admin-mp/src/pages/distribution/records.vue

@@ -0,0 +1,277 @@
+<template>
+  <view class="page">
+    <NavBar title="提现记录" :showBack="true" />
+
+    <view class="content">
+      <!-- 状态筛选 -->
+      <view class="filter-tabs">
+        <view
+          :class="['tab-item', { active: currentStatus === '' }]"
+          @click="switchTab('')"
+        >
+          <text>全部</text>
+        </view>
+        <view
+          :class="['tab-item', { active: currentStatus === 'pending' }]"
+          @click="switchTab('pending')"
+        >
+          <text>审核中</text>
+        </view>
+        <view
+          :class="['tab-item', { active: currentStatus === 'approved' }]"
+          @click="switchTab('approved')"
+        >
+          <text>已通过</text>
+        </view>
+        <view
+          :class="['tab-item', { active: currentStatus === 'paid' }]"
+          @click="switchTab('paid')"
+        >
+          <text>已打款</text>
+        </view>
+        <view
+          :class="['tab-item', { active: currentStatus === 'rejected' }]"
+          @click="switchTab('rejected')"
+        >
+          <text>已驳回</text>
+        </view>
+      </view>
+
+      <!-- 记录列表 -->
+      <view v-if="recordList.length === 0" class="empty-tip">
+        <text>暂无提现记录</text>
+      </view>
+
+      <view
+        class="record-card"
+        v-for="item in recordList"
+        :key="item.id"
+      >
+        <view class="card-header">
+          <text class="card-amount">¥{{ item.amount.toFixed(2) }}</text>
+          <text :class="['card-status', `status-${item.status}`]">
+            {{ getStatusLabel(item.status) }}
+          </text>
+        </view>
+        <view class="card-info">
+          <view class="info-row">
+            <text class="info-label">申请时间</text>
+            <text class="info-value">{{ item.createTime }}</text>
+          </view>
+          <view class="info-row" v-if="item.auditTime">
+            <text class="info-label">审核时间</text>
+            <text class="info-value">{{ item.auditTime }}</text>
+          </view>
+          <view class="info-row" v-if="item.payTime">
+            <text class="info-label">打款时间</text>
+            <text class="info-value">{{ item.payTime }}</text>
+          </view>
+          <view class="info-row" v-if="item.rejectReason">
+            <text class="info-label">驳回原因</text>
+            <text class="info-value reject">{{ item.rejectReason }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 加载更多 -->
+      <view class="load-more" v-if="recordList.length > 0">
+        <text v-if="isLoading">加载中...</text>
+        <text v-else-if="noMore">没有更多了</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import { getMyWithdrawalList } from '@/api/distribution';
+import type { WithdrawalRecord } from '@/api/distribution';
+import { WithdrawalStatus, WithdrawalStatusLabel } from '@/api/distribution';
+
+const recordList = ref<WithdrawalRecord[]>([]);
+const currentStatus = ref<string>('');
+const page = ref(1);
+const pageSize = 10;
+const isLoading = ref(false);
+const noMore = ref(false);
+
+onMounted(() => {
+  loadRecords(true);
+});
+
+const switchTab = (status: string) => {
+  currentStatus.value = status;
+  page.value = 1;
+  noMore.value = false;
+  loadRecords(true);
+};
+
+const loadRecords = async (reset = false) => {
+  if (isLoading.value) return;
+
+  isLoading.value = true;
+
+  try {
+    const params: any = { page: page.value, pageSize };
+    if (currentStatus.value) {
+      params.status = currentStatus.value;
+    }
+
+    const result = await getMyWithdrawalList(params);
+
+    if (reset) {
+      recordList.value = result.list;
+    } else {
+      recordList.value = [...recordList.value, ...result.list];
+    }
+
+    if (result.list.length < pageSize) {
+      noMore.value = true;
+    } else {
+      page.value++;
+    }
+  } catch (error) {
+    console.error('加载提现记录失败', error);
+  } finally {
+    isLoading.value = false;
+  }
+};
+
+const getStatusLabel = (status: WithdrawalStatus): string => {
+  return WithdrawalStatusLabel[status] || '未知';
+};
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+}
+
+.content {
+  padding: 24rpx;
+  padding-top: 0;
+}
+
+.filter-tabs {
+  display: flex;
+  background: #ffffff;
+  border-radius: 16rpx;
+  padding: 8rpx;
+  margin-bottom: 24rpx;
+  border: 1rpx solid #e2e8f0;
+}
+
+.tab-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 64rpx;
+  border-radius: 12rpx;
+  font-size: 24rpx;
+  color: #64748b;
+  transition: all 0.2s;
+
+  &.active {
+    background: #10b981;
+    color: #ffffff;
+    font-weight: 600;
+  }
+
+  &:active:not(.active) {
+    background: #f1f5f9;
+  }
+}
+
+.empty-tip {
+  padding: 80rpx 0;
+  text-align: center;
+  font-size: 28rpx;
+  color: #94a3b8;
+}
+
+.record-card {
+  background: #ffffff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  margin-bottom: 20rpx;
+  border: 1rpx solid #e2e8f0;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20rpx;
+  padding-bottom: 20rpx;
+  border-bottom: 1rpx solid #f1f5f9;
+}
+
+.card-amount {
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #1e293b;
+}
+
+.card-status {
+  font-size: 24rpx;
+  font-weight: 500;
+  padding: 6rpx 16rpx;
+  border-radius: 8rpx;
+
+  &.status-pending {
+    color: #f59e0b;
+    background: #fef3c7;
+  }
+
+  &.status-approved {
+    color: #3b82f6;
+    background: #dbeafe;
+  }
+
+  &.status-rejected {
+    color: #ef4444;
+    background: #fee2e2;
+  }
+
+  &.status-paid {
+    color: #10b981;
+    background: #d1fae5;
+  }
+}
+
+.card-info {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+
+.info-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.info-label {
+  font-size: 24rpx;
+  color: #94a3b8;
+}
+
+.info-value {
+  font-size: 24rpx;
+  color: #475569;
+
+  &.reject {
+    color: #ef4444;
+  }
+}
+
+.load-more {
+  padding: 24rpx 0;
+  text-align: center;
+  font-size: 24rpx;
+  color: #94a3b8;
+}
+</style>

+ 301 - 0
haha-admin-mp/src/pages/distribution/withdrawal.vue

@@ -0,0 +1,301 @@
+<template>
+  <view class="page">
+    <NavBar title="申请提现" :showBack="true" />
+
+    <view class="content">
+      <!-- 当前余额 -->
+      <view class="balance-card" v-if="myStats">
+        <text class="balance-label">可提现余额</text>
+        <view class="balance-amount">
+          <text class="amount-symbol">¥</text>
+          <text class="amount-value">{{ myStats.commissionBalance.toFixed(2) }}</text>
+        </view>
+        <view class="balance-detail">
+          <text class="detail-item">冻结金额:¥{{ myStats.frozenAmount.toFixed(2) }}</text>
+        </view>
+      </view>
+
+      <!-- 提现金额输入 -->
+      <view class="input-section">
+        <text class="input-label">提现金额</text>
+        <view class="input-wrapper">
+          <text class="input-prefix">¥</text>
+          <input
+            class="amount-input"
+            type="digit"
+            v-model="withdrawalAmount"
+            placeholder="请输入提现金额"
+            :maxlength="10"
+          />
+        </view>
+        <view class="input-actions">
+          <text class="action-link" @click="fillAll">全部提现</text>
+        </view>
+      </view>
+
+      <!-- 提现说明 -->
+      <view class="tips-section">
+        <text class="tips-title">提现说明</text>
+        <view class="tip-item">
+          <text class="tip-dot"></text>
+          <text class="tip-text">提现金额最低 10 元</text>
+        </view>
+        <view class="tip-item">
+          <text class="tip-dot"></text>
+          <text class="tip-text">提现申请提交后,将在1-3个工作日内审核</text>
+        </view>
+        <view class="tip-item">
+          <text class="tip-dot"></text>
+          <text class="tip-text">审核通过后,款项将打入您的绑定账户</text>
+        </view>
+      </view>
+
+      <!-- 提交按钮 -->
+      <button class="btn-submit" @click="handleSubmit" :disabled="isSubmitting">
+        {{ isSubmitting ? '提交中...' : '确认提现' }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import { getMyStats, applyWithdrawal } from '@/api/distribution';
+import type { DistributionStats } from '@/api/distribution';
+
+const myStats = ref<DistributionStats | null>(null);
+const withdrawalAmount = ref('');
+const isSubmitting = ref(false);
+
+onMounted(() => {
+  loadStats();
+});
+
+const loadStats = async () => {
+  try {
+    myStats.value = await getMyStats();
+  } catch (error) {
+    console.error('加载分销统计失败', error);
+  }
+};
+
+const fillAll = () => {
+  if (myStats.value) {
+    withdrawalAmount.value = myStats.value.commissionBalance.toFixed(2);
+  }
+};
+
+const handleSubmit = async () => {
+  const amount = parseFloat(withdrawalAmount.value);
+
+  if (!withdrawalAmount.value || isNaN(amount)) {
+    uni.showToast({ title: '请输入提现金额', icon: 'none' });
+    return;
+  }
+
+  if (amount < 10) {
+    uni.showToast({ title: '提现金额最低10元', icon: 'none' });
+    return;
+  }
+
+  if (myStats.value && amount > myStats.value.commissionBalance) {
+    uni.showToast({ title: '提现金额不能超过可提现余额', icon: 'none' });
+    return;
+  }
+
+  isSubmitting.value = true;
+
+  try {
+    const result = await applyWithdrawal(amount);
+
+    if (result.success) {
+      uni.showToast({ title: '提现申请已提交', icon: 'success' });
+
+      setTimeout(() => {
+        uni.navigateBack();
+      }, 1500);
+    } else {
+      uni.showToast({ title: result.message || '提现申请失败', icon: 'none' });
+    }
+  } catch (error) {
+    console.error('提现申请失败', error);
+    uni.showToast({ title: '提现申请失败,请重试', icon: 'none' });
+  } finally {
+    isSubmitting.value = false;
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+}
+
+.content {
+  padding: 24rpx;
+  padding-top: 0;
+}
+
+.balance-card {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  padding: 36rpx;
+  margin-bottom: 24rpx;
+  color: #ffffff;
+}
+
+.balance-label {
+  font-size: 26rpx;
+  color: rgba(255, 255, 255, 0.8);
+  display: block;
+  margin-bottom: 12rpx;
+}
+
+.balance-amount {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 16rpx;
+}
+
+.amount-symbol {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #ffffff;
+  margin-right: 4rpx;
+}
+
+.amount-value {
+  font-size: 64rpx;
+  font-weight: 700;
+  color: #ffffff;
+}
+
+.balance-detail {
+  padding-top: 16rpx;
+  border-top: 1rpx solid rgba(255, 255, 255, 0.2);
+}
+
+.detail-item {
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.7);
+}
+
+.input-section {
+  background: #ffffff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  margin-bottom: 24rpx;
+  border: 1rpx solid #e2e8f0;
+}
+
+.input-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1e293b;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.input-wrapper {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 24rpx;
+  background: #f8fafc;
+  border-radius: 12rpx;
+  border: 2rpx solid #e2e8f0;
+}
+
+.input-prefix {
+  font-size: 40rpx;
+  font-weight: 700;
+  color: #1e293b;
+  margin-right: 12rpx;
+}
+
+.amount-input {
+  flex: 1;
+  font-size: 40rpx;
+  font-weight: 600;
+  color: #1e293b;
+  height: 56rpx;
+}
+
+.input-actions {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 16rpx;
+}
+
+.action-link {
+  font-size: 26rpx;
+  color: #3b82f6;
+  font-weight: 500;
+}
+
+.tips-section {
+  background: #ffffff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  margin-bottom: 40rpx;
+  border: 1rpx solid #e2e8f0;
+}
+
+.tips-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1e293b;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.tip-item {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 12rpx;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.tip-dot {
+  width: 10rpx;
+  height: 10rpx;
+  background: #94a3b8;
+  border-radius: 50%;
+  margin-right: 12rpx;
+  margin-top: 12rpx;
+  flex-shrink: 0;
+}
+
+.tip-text {
+  font-size: 24rpx;
+  color: #64748b;
+  line-height: 36rpx;
+}
+
+.btn-submit {
+  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;
+
+  &[disabled] {
+    background: #cbd5e1;
+    color: #94a3b8;
+  }
+
+  &:active:not([disabled]) {
+    opacity: 0.9;
+  }
+}
+</style>

+ 108 - 0
haha-admin-web/src/api/distribution.ts

@@ -0,0 +1,108 @@
+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 createDistributor = (data: object) => {
+  return http.request<Result>("post", "/distribution/distributor", { data });
+};
+
+export const updateDistributor = (data: object) => {
+  return http.request<Result>("put", "/distribution/distributor", { data });
+};
+
+export const getDistributorList = (params: object) => {
+  return http.request<ResultTable>("get", "/distribution/distributor/list", { params });
+};
+
+export const getDistributorDetail = (id: string) => {
+  return http.request<Result>("get", `/distribution/distributor/${id}`);
+};
+
+export const activateDistributor = (id: string) => {
+  return http.request<Result>("put", `/distribution/distributor/${id}/activate`);
+};
+
+export const deactivateDistributor = (id: string) => {
+  return http.request<Result>("put", `/distribution/distributor/${id}/deactivate`);
+};
+
+export const deleteDistributor = (id: string) => {
+  return http.request<Result>("delete", `/distribution/distributor/${id}`);
+};
+
+export const generateQrcode = (id: string) => {
+  return http.request<Result>("post", `/distribution/distributor/${id}/qrcode`);
+};
+
+// ==================== 推荐记录 ====================
+
+export const getReferralList = (params: object) => {
+  return http.request<ResultTable>("get", "/distribution/referral/list", { params });
+};
+
+// ==================== 佣金记录 ====================
+
+export const getCommissionList = (params: object) => {
+  return http.request<ResultTable>("get", "/distribution/commission/list", { params });
+};
+
+// ==================== 提现管理 ====================
+
+export const getWithdrawalList = (params: object) => {
+  return http.request<ResultTable>("get", "/distribution/withdrawal/list", { params });
+};
+
+export const reviewWithdrawal = (data: object) => {
+  return http.request<Result>("put", "/distribution/withdrawal/review", { data });
+};
+
+export const confirmTransfer = (id: string) => {
+  return http.request<Result>("put", `/distribution/withdrawal/${id}/confirm`);
+};
+
+// ==================== 报表 ====================
+
+export const getReportList = (params: object) => {
+  return http.request<ResultTable>("get", "/distribution/report/list", { params });
+};
+
+export const generateReport = (yearMonth: string) => {
+  return http.request<Result>("post", "/distribution/report/generate", { params: { yearMonth } });
+};
+
+export const exportReport = (yearMonth: string) => {
+  return http.request<Result>("get", "/distribution/report/export", { params: { yearMonth } });
+};
+
+// ==================== 仪表盘 ====================
+
+export const getDashboard = () => {
+  return http.request<Result>("get", "/distribution/dashboard");
+};
+
+// ==================== 配置 ====================
+
+export const getConfigList = () => {
+  return http.request<Result>("get", "/distribution/config/list");
+};
+
+export const updateConfig = (data: object) => {
+  return http.request<Result>("put", "/distribution/config", { data });
+};

+ 99 - 0
haha-admin-web/src/router/modules/distribution.ts

@@ -0,0 +1,99 @@
+/**
+ * 分销管理路由配置
+ *
+ * 【模块说明】
+ * - /distribution/dashboard  : 分销概览
+ * - /distribution/distributor : 分销员管理
+ * - /distribution/referral   : 推荐记录
+ * - /distribution/commission : 佣金记录
+ * - /distribution/withdrawal : 提现管理
+ * - /distribution/report     : 月度报表
+ * - /distribution/config     : 分销配置
+ *
+ * 【小程序端 API】
+ * - /mp/distribution/*       : 小程序端专属接口
+ */
+
+export default {
+  path: "/distribution",
+  redirect: "/distribution/dashboard",
+  meta: {
+    icon: "ep:share",
+    title: "分销管理",
+    rank: 6
+  },
+  children: [
+    {
+      path: "/distribution/dashboard",
+      name: "DistributionDashboard",
+      component: () => import("@/views/distribution/dashboard/index.vue"),
+      meta: {
+        icon: "ep:data-analysis",
+        title: "分销概览"
+      }
+    },
+    {
+      path: "/distribution/distributor",
+      name: "DistributionDistributor",
+      component: () => import("@/views/distribution/distributor/index.vue"),
+      meta: {
+        icon: "ep:user",
+        title: "分销员管理"
+      }
+    },
+    {
+      path: "/distribution/distributor/detail",
+      name: "DistributionDistributorDetail",
+      component: () => import("@/views/distribution/distributor/detail.vue"),
+      meta: {
+        title: "分销员详情",
+        showLink: false
+      }
+    },
+    {
+      path: "/distribution/referral",
+      name: "DistributionReferral",
+      component: () => import("@/views/distribution/referral/index.vue"),
+      meta: {
+        icon: "ep:connection",
+        title: "推荐记录"
+      }
+    },
+    {
+      path: "/distribution/commission",
+      name: "DistributionCommission",
+      component: () => import("@/views/distribution/commission/index.vue"),
+      meta: {
+        icon: "ep:money",
+        title: "佣金记录"
+      }
+    },
+    {
+      path: "/distribution/withdrawal",
+      name: "DistributionWithdrawal",
+      component: () => import("@/views/distribution/withdrawal/index.vue"),
+      meta: {
+        icon: "ep:wallet",
+        title: "提现管理"
+      }
+    },
+    {
+      path: "/distribution/report",
+      name: "DistributionReport",
+      component: () => import("@/views/distribution/report/index.vue"),
+      meta: {
+        icon: "ep:document",
+        title: "月度报表"
+      }
+    },
+    {
+      path: "/distribution/config",
+      name: "DistributionConfig",
+      component: () => import("@/views/distribution/config/index.vue"),
+      meta: {
+        icon: "ep:setting",
+        title: "分销配置"
+      }
+    }
+  ]
+} satisfies RouteConfigsTable;

+ 151 - 0
haha-admin-web/src/views/distribution/commission/index.vue

@@ -0,0 +1,151 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useCommission } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import Refresh from "~icons/ep/refresh";
+
+defineOptions({
+  name: "DistributionCommission"
+});
+
+const formRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  onSearch,
+  resetForm,
+  handleSizeChange,
+  handleCurrentChange
+} = useCommission();
+
+const timeRange = ref([]);
+
+function handleTimeRangeChange(val) {
+  if (val && val.length === 2) {
+    form.startDate = val[0];
+    form.endDate = val[1];
+  } else {
+    form.startDate = "";
+    form.endDate = "";
+  }
+}
+</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="distributorId">
+        <el-input
+          v-model="form.distributorId"
+          placeholder="请输入分销员ID"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="用户ID:" prop="userId">
+        <el-input
+          v-model="form.userId"
+          placeholder="请输入用户ID"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="佣金类型:" prop="commissionType">
+        <el-select
+          v-model="form.commissionType"
+          placeholder="请选择"
+          clearable
+          class="w-[150px]!"
+        >
+          <el-option label="拉新奖励" :value="0" />
+          <el-option label="消费提成" :value="1" />
+        </el-select>
+      </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-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="handleTimeRangeChange"
+        />
+      </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 v-slot="{ size, dynamicColumns }">
+        <pure-table
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :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>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 181 - 0
haha-admin-web/src/views/distribution/commission/utils/hook.tsx

@@ -0,0 +1,181 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { getCommissionList } from "@/api/distribution";
+import type { PaginationProps } from "@pureadmin/table";
+import { onMounted, reactive, ref } from "vue";
+import type { CommissionQuery } from "../../utils/types";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  resetPagination
+} from "@/utils/paginationHelper";
+
+export function useCommission() {
+  const form = reactive<CommissionQuery>({
+    distributorId: undefined,
+    userId: undefined,
+    commissionType: undefined,
+    status: undefined,
+    startDate: "",
+    endDate: ""
+  });
+  const loading = ref(true);
+  const dataList = ref([]);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  const commissionTypeMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+    0: { text: "拉新奖励", type: "primary" },
+    1: { text: "消费提成", type: "success" }
+  };
+
+  const statusMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+    0: { text: "待结算", type: "warning" },
+    1: { text: "已结算", type: "success" },
+    2: { text: "已冻结", type: "info" }
+  };
+
+  const columns: TableColumnList = [
+    {
+      label: "分销员名称",
+      prop: "distributorName",
+      minWidth: 120,
+      formatter: ({ distributorName }) => distributorName || "-"
+    },
+    {
+      label: "用户名称",
+      prop: "userName",
+      minWidth: 120,
+      formatter: ({ userName }) => userName || "-"
+    },
+    {
+      label: "佣金类型",
+      prop: "commissionType",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        const item =
+          commissionTypeMap[row.commissionType] || { text: "未知", type: "info" };
+        return <el-tag type={item.type as any} size="small">{item.text}</el-tag>;
+      }
+    },
+    {
+      label: "佣金金额",
+      prop: "amount",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        return row.amount ? (
+          <span class="text-red-600 font-bold">¥{row.amount}</span>
+        ) : (
+          <span class="text-gray-400">-</span>
+        );
+      }
+    },
+    {
+      label: "订单金额",
+      prop: "orderAmount",
+      minWidth: 100,
+      formatter: ({ orderAmount }) =>
+        orderAmount ? `¥${orderAmount}` : "-"
+    },
+    {
+      label: "提成比例",
+      prop: "rate",
+      minWidth: 90,
+      cellRenderer: ({ row }) => {
+        return row.rate !== undefined && row.rate !== null ? (
+          <span>{row.rate}%</span>
+        ) : (
+          <span class="text-gray-400">-</span>
+        );
+      }
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 90,
+      cellRenderer: ({ row }) => {
+        const item = statusMap[row.status] || { text: "未知", type: "info" };
+        return <el-tag type={item.type as any} size="small">{item.text}</el-tag>;
+      }
+    },
+    {
+      label: "结算时间",
+      prop: "settleTime",
+      minWidth: 160,
+      formatter: ({ settleTime }) =>
+        settleTime ? dayjs(settleTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+
+      if (form.distributorId) searchParams.distributorId = form.distributorId;
+      if (form.userId) searchParams.userId = form.userId;
+      if (form.commissionType !== undefined && form.commissionType !== null)
+        searchParams.commissionType = form.commissionType;
+      if (form.status !== undefined && form.status !== null)
+        searchParams.status = form.status;
+      if (form.startDate) searchParams.startDate = form.startDate;
+      if (form.endDate) searchParams.endDate = form.endDate;
+
+      const { data } = await getCommissionList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
+    } catch (error) {
+      console.error("获取佣金记录列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    }
+  }
+
+  const resetForm = formEl => {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  };
+
+  function handleSizeChange(val: number) {
+    handlePageSizeChange(val, pagination, onSearch);
+  }
+
+  function handleCurrentChange(val: number) {
+    handleCurrentPageChange(val, pagination, onSearch);
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    commissionTypeMap,
+    statusMap,
+    onSearch,
+    resetForm,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}

+ 184 - 0
haha-admin-web/src/views/distribution/config/index.vue

@@ -0,0 +1,184 @@
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { getConfigList, updateConfig } from "@/api/distribution";
+import type { DistributorConfig } from "../utils/types";
+import { message } from "@/utils/message";
+
+defineOptions({
+  name: "DistributionConfig"
+});
+
+const loading = ref(false);
+const saving = ref(false);
+
+const configList = ref<DistributorConfig[]>([]);
+
+const form = reactive({
+  fixedCommissionAmount: 0,
+  firstOrderRate: 0,
+  validUserRule: "FIRST_ORDER",
+  minConsumeAmount: 0,
+  validTimeWindow: 0,
+  minWithdrawAmount: 0
+});
+
+const validUserRuleOptions = [
+  { label: "首单完成", value: "FIRST_ORDER" },
+  { label: "最低消费金额", value: "MIN_AMOUNT" },
+  { label: "首单且最低金额", value: "BOTH" }
+];
+
+// configKey 与 form 字段的映射
+const configKeyMap: Record<string, string> = {
+  FIXED_COMMISSION_AMOUNT: "fixedCommissionAmount",
+  FIRST_ORDER_RATE: "firstOrderRate",
+  VALID_USER_RULE: "validUserRule",
+  MIN_CONSUME_AMOUNT: "minConsumeAmount",
+  VALID_TIME_WINDOW: "validTimeWindow",
+  MIN_WITHDRAW_AMOUNT: "minWithdrawAmount"
+};
+
+// form 字段与 configKey 的反向映射
+const formKeyToConfigKey: Record<string, string> = {};
+for (const [key, val] of Object.entries(configKeyMap)) {
+  formKeyToConfigKey[val] = key;
+}
+
+async function fetchConfigList() {
+  loading.value = true;
+  try {
+    const { data } = await getConfigList();
+    if (data) {
+      configList.value = Array.isArray(data) ? data : data.list || [];
+      // 将配置列表映射到表单字段
+      for (const config of configList.value) {
+        const formKey = configKeyMap[config.configKey];
+        if (formKey) {
+          if (config.configKey === "VALID_USER_RULE") {
+            (form as any)[formKey] = config.configValue;
+          } else {
+            (form as any)[formKey] = Number(config.configValue) || 0;
+          }
+        }
+      }
+    }
+  } catch (error) {
+    console.error("获取分销配置失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function handleSave() {
+  saving.value = true;
+  try {
+    // 遍历所有配置项,逐个更新
+    for (const config of configList.value) {
+      const formKey = configKeyMap[config.configKey];
+      if (formKey) {
+        const newValue = String((form as any)[formKey]);
+        // 仅更新有变化的配置
+        if (newValue !== config.configValue) {
+          await updateConfig({
+            id: config.id,
+            configKey: config.configKey,
+            configValue: newValue
+          });
+        }
+      }
+    }
+    message("配置保存成功", { type: "success" });
+    // 重新加载配置以获取最新数据
+    await fetchConfigList();
+  } catch (error) {
+    console.error("保存分销配置失败:", error);
+    message("配置保存失败", { type: "error" });
+  } finally {
+    saving.value = false;
+  }
+}
+
+onMounted(() => {
+  fetchConfigList();
+});
+</script>
+
+<template>
+  <div class="main">
+    <el-card v-loading="loading">
+      <template #header>
+        <span class="font-semibold">分销配置</span>
+      </template>
+
+      <el-form
+        label-position="left"
+        label-width="180px"
+        style="max-width: 600px"
+      >
+        <el-form-item label="固定佣金金额 (元/人)">
+          <el-input-number
+            v-model="form.fixedCommissionAmount"
+            :min="0"
+            :precision="2"
+            controls-position="right"
+          />
+        </el-form-item>
+
+        <el-form-item label="首单佣金比例 (%)">
+          <el-input-number
+            v-model="form.firstOrderRate"
+            :min="0"
+            :max="100"
+            :precision="2"
+            controls-position="right"
+          />
+        </el-form-item>
+
+        <el-form-item label="有效用户判定规则">
+          <el-select v-model="form.validUserRule" style="width: 100%">
+            <el-option
+              v-for="item in validUserRuleOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="最低消费金额 (元)">
+          <el-input-number
+            v-model="form.minConsumeAmount"
+            :min="0"
+            :precision="2"
+            controls-position="right"
+          />
+        </el-form-item>
+
+        <el-form-item label="有效时间窗口 (天,0表示不限)">
+          <el-input-number
+            v-model="form.validTimeWindow"
+            :min="0"
+            controls-position="right"
+          />
+        </el-form-item>
+
+        <el-form-item label="最低提现金额 (元)">
+          <el-input-number
+            v-model="form.minWithdrawAmount"
+            :min="0"
+            :precision="2"
+            controls-position="right"
+          />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" :loading="saving" @click="handleSave">
+            保存配置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped></style>

+ 234 - 0
haha-admin-web/src/views/distribution/dashboard/index.vue

@@ -0,0 +1,234 @@
+<script setup lang="ts">
+import { ref, onMounted, onBeforeUnmount } from "vue";
+import * as echarts from "echarts";
+import { getDashboard } from "@/api/distribution";
+import type { DashboardData, MonthlyTrend } from "../utils/types";
+
+defineOptions({
+  name: "DistributionDashboard"
+});
+
+const loading = ref(false);
+const dashboardData = ref<DashboardData>({
+  totalDistributors: 0,
+  activeDistributors: 0,
+  totalReferrals: 0,
+  validReferrals: 0,
+  totalCommission: 0,
+  monthlyCommission: 0,
+  trends: []
+});
+
+const chartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: echarts.ECharts | null = null;
+
+function formatMoney(value: number): string {
+  return `¥${value.toLocaleString("zh-CN", {
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2
+  })}`;
+}
+
+async function fetchDashboardData() {
+  loading.value = true;
+  try {
+    const { data } = await getDashboard();
+    if (data) {
+      dashboardData.value = data;
+      initChart();
+    }
+  } catch (error) {
+    console.error("获取分销仪表盘数据失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function initChart() {
+  setTimeout(() => {
+    const chartDom = chartRef.value;
+    if (!chartDom) return;
+
+    if (chartInstance) {
+      chartInstance.dispose();
+    }
+
+    chartInstance = echarts.init(chartDom);
+
+    const trends: MonthlyTrend[] = dashboardData.value.trends || [];
+    const xData = trends.map(item => item.yearMonth);
+    const referralData = trends.map(item => item.referralCount);
+    const validData = trends.map(item => item.validCount);
+    const commissionData = trends.map(item => item.commissionAmount);
+
+    const option: echarts.EChartsOption = {
+      title: {
+        text: "分销趋势",
+        left: "center",
+        textStyle: { fontSize: 14, fontWeight: "normal" }
+      },
+      tooltip: {
+        trigger: "axis",
+        backgroundColor: "rgba(255, 255, 255, 0.9)",
+        borderColor: "#e5e7eb",
+        borderWidth: 1,
+        axisPointer: {
+          type: "cross"
+        }
+      },
+      legend: {
+        data: ["拉新人数", "有效用户数", "佣金金额"],
+        bottom: 0
+      },
+      grid: {
+        left: "3%",
+        right: "4%",
+        bottom: "15%",
+        top: "12%",
+        containLabel: true
+      },
+      xAxis: {
+        type: "category",
+        data: xData,
+        axisPointer: {
+          type: "shadow"
+        }
+      },
+      yAxis: [
+        {
+          type: "value",
+          name: "人数",
+          position: "left"
+        },
+        {
+          type: "value",
+          name: "金额(元)",
+          position: "right"
+        }
+      ],
+      series: [
+        {
+          name: "拉新人数",
+          type: "line",
+          smooth: true,
+          data: referralData,
+          itemStyle: { color: "#409eff" },
+          yAxisIndex: 0
+        },
+        {
+          name: "有效用户数",
+          type: "line",
+          smooth: true,
+          data: validData,
+          itemStyle: { color: "#67c23a" },
+          yAxisIndex: 0
+        },
+        {
+          name: "佣金金额",
+          type: "bar",
+          data: commissionData,
+          itemStyle: { color: "#e6a23c" },
+          yAxisIndex: 1
+        }
+      ]
+    };
+
+    chartInstance.setOption(option);
+  }, 100);
+}
+
+function resizeChart() {
+  chartInstance?.resize();
+}
+
+onMounted(() => {
+  fetchDashboardData();
+  window.addEventListener("resize", resizeChart);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", resizeChart);
+  if (chartInstance) {
+    chartInstance.dispose();
+    chartInstance = null;
+  }
+});
+</script>
+
+<template>
+  <div class="distribution-dashboard">
+    <!-- 数据概览卡片 -->
+    <el-row :gutter="20" class="mb-6">
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="card-header-text">总分销员数</span>
+          </template>
+          <el-statistic :value="dashboardData.totalDistributors" />
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="card-header-text">活跃分销员数</span>
+          </template>
+          <el-statistic :value="dashboardData.activeDistributors" />
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="card-header-text">总拉新用户数</span>
+          </template>
+          <el-statistic :value="dashboardData.totalReferrals" />
+        </el-card>
+      </el-col>
+    </el-row>
+    <el-row :gutter="20" class="mb-6">
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="card-header-text">有效用户数</span>
+          </template>
+          <el-statistic :value="dashboardData.validReferrals" />
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="card-header-text">总佣金支出</span>
+          </template>
+          <el-statistic
+            :value="dashboardData.totalCommission"
+            :formatter="(val: number) => formatMoney(val)"
+          />
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="card-header-text">本月佣金支出</span>
+          </template>
+          <el-statistic
+            :value="dashboardData.monthlyCommission"
+            :formatter="(val: number) => formatMoney(val)"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 趋势图表 -->
+    <el-card shadow="hover">
+      <div ref="chartRef" style="width: 100%; height: 400px"></div>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.distribution-dashboard {
+  .card-header-text {
+    font-size: 14px;
+    color: var(--el-text-color-secondary);
+  }
+}
+</style>

+ 577 - 0
haha-admin-web/src/views/distribution/distributor/detail.vue

@@ -0,0 +1,577 @@
+<script setup lang="ts">
+import { ref, reactive, onMounted, watch, h } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessage, ElTag } from "element-plus";
+import { ArrowLeft } from "@element-plus/icons-vue";
+import QRCode from "qrcode";
+import {
+  getDistributorDetail,
+  getReferralList,
+  getCommissionList
+} from "@/api/distribution";
+import type { PaginationProps } from "@pureadmin/table";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  updatePaginationData
+} from "@/utils/paginationHelper";
+import type { Distributor, Referral, Commission } from "../utils/types";
+
+defineOptions({
+  name: "DistributorDetail"
+});
+
+const route = useRoute();
+const router = useRouter();
+
+const loading = ref(false);
+const detail = ref<Distributor | null>(null);
+const qrcodeDataUrl = ref<string>("");
+
+const statusMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+  0: { text: "待激活", type: "info" },
+  1: { text: "已激活", type: "success" },
+  2: { text: "已停用", type: "danger" }
+};
+
+const referralLoading = ref(false);
+const referralList = ref<Referral[]>([]);
+const referralPagination = reactive<PaginationProps>(initPagination());
+
+const referralStatusMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+  0: { text: "待生效", type: "info" },
+  1: { text: "已生效", type: "success" },
+  2: { text: "已失效", type: "danger" }
+};
+
+const referralColumns = [
+  { label: "用户ID", prop: "userId", width: 80 },
+  { label: "用户名", prop: "userName", minWidth: 120 },
+  {
+    label: "状态",
+    prop: "status",
+    minWidth: 100,
+    cellRenderer: ({ row }) => {
+      const item = referralStatusMap[row.status] || {
+        text: "未知",
+        type: "info"
+      };
+      return h(ElTag, { type: item.type as any }, () => item.text);
+    }
+  },
+  {
+    label: "首单金额",
+    prop: "firstOrderAmount",
+    minWidth: 120,
+    formatter: ({ firstOrderAmount }) =>
+      firstOrderAmount !== undefined && firstOrderAmount !== null
+        ? `¥${firstOrderAmount.toFixed(2)}`
+        : "-"
+  },
+  {
+    label: "首单时间",
+    prop: "firstOrderTime",
+    minWidth: 160,
+    formatter: ({ firstOrderTime }) => firstOrderTime || "-"
+  },
+  {
+    label: "生效时间",
+    prop: "effectiveTime",
+    minWidth: 160,
+    formatter: ({ effectiveTime }) => effectiveTime || "-"
+  },
+  {
+    label: "创建时间",
+    prop: "createTime",
+    minWidth: 160,
+    formatter: ({ createTime }) => createTime || "-"
+  }
+];
+
+const commissionLoading = ref(false);
+const commissionList = ref<Commission[]>([]);
+const commissionPagination = reactive<PaginationProps>(initPagination());
+
+const commissionStatusMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+  0: { text: "待结算", type: "info" },
+  1: { text: "已结算", type: "success" },
+  2: { text: "已冻结", type: "warning" }
+};
+
+const commissionColumns = [
+  { label: "用户ID", prop: "userId", width: 80 },
+  { label: "用户名", prop: "userName", minWidth: 120 },
+  {
+    label: "佣金类型",
+    prop: "commissionTypeLabel",
+    minWidth: 100
+  },
+  {
+    label: "佣金金额",
+    prop: "amount",
+    minWidth: 120,
+    formatter: ({ amount }) =>
+      amount !== undefined && amount !== null
+        ? `¥${amount.toFixed(2)}`
+        : "¥0.00"
+  },
+  {
+    label: "订单金额",
+    prop: "orderAmount",
+    minWidth: 120,
+    formatter: ({ orderAmount }) =>
+      orderAmount !== undefined && orderAmount !== null
+        ? `¥${orderAmount.toFixed(2)}`
+        : "¥0.00"
+  },
+  {
+    label: "佣金比例",
+    prop: "rate",
+    minWidth: 100,
+    formatter: ({ rate }) =>
+      rate !== undefined && rate !== null ? `${(rate * 100).toFixed(1)}%` : "-"
+  },
+  {
+    label: "状态",
+    prop: "status",
+    minWidth: 100,
+    cellRenderer: ({ row }) => {
+      const item = commissionStatusMap[row.status] || {
+        text: "未知",
+        type: "info"
+      };
+      return h(ElTag, { type: item.type as any }, () => item.text);
+    }
+  },
+  {
+    label: "创建时间",
+    prop: "createTime",
+    minWidth: 160,
+    formatter: ({ createTime }) => createTime || "-"
+  }
+];
+
+const activeTab = ref("referral");
+
+async function generateQrcodeDataUrl(content: string) {
+  if (!content) {
+    qrcodeDataUrl.value = "";
+    return;
+  }
+  try {
+    qrcodeDataUrl.value = await QRCode.toDataURL(content, {
+      width: 200,
+      margin: 2,
+      color: {
+        dark: "#000000",
+        light: "#ffffff"
+      }
+    });
+  } catch (error) {
+    console.error("生成二维码失败:", error);
+    qrcodeDataUrl.value = "";
+  }
+}
+
+watch(detail, (newVal) => {
+  if (newVal?.qrcodeContent) {
+    generateQrcodeDataUrl(newVal.qrcodeContent);
+  } else {
+    qrcodeDataUrl.value = "";
+  }
+});
+
+async function fetchDetail() {
+  const id = route.query.id as string;
+  if (!id) {
+    ElMessage.warning("缺少分销员ID参数");
+    return;
+  }
+  loading.value = true;
+  try {
+    const res = await getDistributorDetail(id);
+    if (res && res.data) {
+      detail.value = res.data;
+    } else {
+      ElMessage.error("获取分销员详情失败");
+    }
+  } catch (error) {
+    console.error("获取分销员详情失败:", error);
+    ElMessage.error("获取分销员详情失败");
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function fetchReferralList() {
+  const id = route.query.id as string;
+  if (!id) return;
+  referralLoading.value = true;
+  try {
+    const res = await getReferralList({
+      distributorId: id,
+      page: referralPagination.currentPage,
+      pageSize: referralPagination.pageSize
+    });
+    if (res && res.data) {
+      referralList.value = res.data.list || [];
+      updatePaginationData(referralPagination, res.data);
+    }
+  } finally {
+    referralLoading.value = false;
+  }
+}
+
+async function fetchCommissionList() {
+  const id = route.query.id as string;
+  if (!id) return;
+  commissionLoading.value = true;
+  try {
+    const res = await getCommissionList({
+      distributorId: id,
+      page: commissionPagination.currentPage,
+      pageSize: commissionPagination.pageSize
+    });
+    if (res && res.data) {
+      commissionList.value = res.data.list || [];
+      updatePaginationData(commissionPagination, res.data);
+    }
+  } finally {
+    commissionLoading.value = false;
+  }
+}
+
+function handleDownloadQrcode() {
+  if (!qrcodeDataUrl.value) {
+    ElMessage.warning("该分销员尚未生成二维码");
+    return;
+  }
+  try {
+    const link = document.createElement("a");
+    link.href = qrcodeDataUrl.value;
+    link.download = `${detail.value?.name || detail.value?.code}_qrcode.png`;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    ElMessage.success("下载成功");
+  } catch {
+    ElMessage.error("下载失败");
+  }
+}
+
+function handleTabChange(tab: string) {
+  if (tab === "referral") {
+    fetchReferralList();
+  } else if (tab === "commission") {
+    fetchCommissionList();
+  }
+}
+
+function handleReferralSizeChange(val: number) {
+  handlePageSizeChange(val, referralPagination, fetchReferralList);
+}
+
+function handleReferralCurrentChange(val: number) {
+  handleCurrentPageChange(val, referralPagination, fetchReferralList);
+}
+
+function handleCommissionSizeChange(val: number) {
+  handlePageSizeChange(val, commissionPagination, fetchCommissionList);
+}
+
+function handleCommissionCurrentChange(val: number) {
+  handleCurrentPageChange(val, commissionPagination, fetchCommissionList);
+}
+
+function goBack() {
+  router.push("/distribution/distributor");
+}
+
+onMounted(() => {
+  fetchDetail();
+  fetchReferralList();
+});
+</script>
+
+<template>
+  <div class="distributor-detail" v-loading="loading">
+    <div class="detail-header">
+      <el-button :icon="ArrowLeft" @click="goBack">返回列表</el-button>
+      <span class="header-title">分销员详情</span>
+    </div>
+
+    <template v-if="detail">
+      <el-card class="detail-card" shadow="never">
+        <template #header>
+          <div class="card-header">
+            <span>基本信息</span>
+          </div>
+        </template>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="姓名">
+            {{ detail.name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="分销员编码">
+            {{ detail.code }}
+          </el-descriptions-item>
+          <el-descriptions-item label="手机号">
+            {{ detail.phone }}
+          </el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="statusMap[detail.status]?.type">
+              {{ statusMap[detail.status]?.text || "未知" }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="创建时间">
+            {{ detail.createTime }}
+          </el-descriptions-item>
+          <el-descriptions-item label="备注">
+            {{ detail.remark || "-" }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-card>
+
+      <el-card class="detail-card" shadow="never">
+        <template #header>
+          <div class="card-header">
+            <span>推广二维码</span>
+            <div v-if="qrcodeDataUrl" class="card-header-actions">
+              <el-button size="small" @click="handleDownloadQrcode">
+                下载二维码
+              </el-button>
+            </div>
+          </div>
+        </template>
+        <div class="qrcode-area">
+          <template v-if="qrcodeDataUrl">
+            <div class="qrcode-container">
+              <el-image
+                :src="qrcodeDataUrl"
+                fit="contain"
+                class="qrcode-image"
+                :preview-src-list="[qrcodeDataUrl]"
+              />
+              <div class="qrcode-content">
+                <el-text type="info" size="small">{{ detail.qrcodeContent }}</el-text>
+              </div>
+            </div>
+          </template>
+          <div v-else class="qrcode-empty">
+            <el-empty description="暂无二维码,请在列表页生成" :image-size="80" />
+          </div>
+        </div>
+      </el-card>
+
+      <div class="stats-grid">
+        <el-card class="stat-card" shadow="hover">
+          <div class="stat-item">
+            <div class="stat-value">{{ detail.totalReferrals || 0 }}</div>
+            <div class="stat-label">总推荐数</div>
+          </div>
+        </el-card>
+        <el-card class="stat-card" shadow="hover">
+          <div class="stat-item">
+            <div class="stat-value">{{ detail.validReferrals || 0 }}</div>
+            <div class="stat-label">有效推荐数</div>
+          </div>
+        </el-card>
+        <el-card class="stat-card stat-card--primary" shadow="hover">
+          <div class="stat-item">
+            <div class="stat-value">
+              ¥{{ detail.commissionBalance?.toFixed(2) || "0.00" }}
+            </div>
+            <div class="stat-label">佣金余额</div>
+          </div>
+        </el-card>
+        <el-card class="stat-card stat-card--warning" shadow="hover">
+          <div class="stat-item">
+            <div class="stat-value">
+              ¥{{ detail.frozenAmount?.toFixed(2) || "0.00" }}
+            </div>
+            <div class="stat-label">冻结金额</div>
+          </div>
+        </el-card>
+        <el-card class="stat-card stat-card--success" shadow="hover">
+          <div class="stat-item">
+            <div class="stat-value">
+              ¥{{ detail.totalCommission?.toFixed(2) || "0.00" }}
+            </div>
+            <div class="stat-label">累计佣金</div>
+          </div>
+        </el-card>
+        <el-card class="stat-card" shadow="hover">
+          <div class="stat-item">
+            <div class="stat-value">
+              ¥{{ detail.totalWithdrawn?.toFixed(2) || "0.00" }}
+            </div>
+            <div class="stat-label">已提现金额</div>
+          </div>
+        </el-card>
+      </div>
+
+      <el-card class="detail-card" shadow="never">
+        <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+          <el-tab-pane label="推荐用户列表" name="referral">
+            <pure-table
+              align-whole="center"
+              showOverflowTooltip
+              table-layout="auto"
+              :loading="referralLoading"
+              :data="referralList"
+              :columns="referralColumns"
+              :pagination="referralPagination"
+              @page-size-change="handleReferralSizeChange"
+              @page-current-change="handleReferralCurrentChange"
+            />
+          </el-tab-pane>
+
+          <el-tab-pane label="佣金记录列表" name="commission">
+            <pure-table
+              align-whole="center"
+              showOverflowTooltip
+              table-layout="auto"
+              :loading="commissionLoading"
+              :data="commissionList"
+              :columns="commissionColumns"
+              :pagination="commissionPagination"
+              @page-size-change="handleCommissionSizeChange"
+              @page-current-change="handleCommissionCurrentChange"
+            />
+          </el-tab-pane>
+        </el-tabs>
+      </el-card>
+    </template>
+    <template v-else-if="!loading">
+      <el-empty description="分销员信息加载失败,请返回列表重试" />
+    </template>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.distributor-detail {
+  padding: 16px;
+}
+
+.detail-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+
+  .header-title {
+    margin-left: 12px;
+    font-size: 18px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+  }
+}
+
+.detail-card {
+  margin-bottom: 16px;
+
+  .card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    span {
+      font-size: 16px;
+      font-weight: 600;
+    }
+
+    .card-header-actions {
+      display: flex;
+      gap: 8px;
+    }
+  }
+}
+
+.qrcode-area {
+  display: flex;
+  justify-content: center;
+  padding: 20px;
+
+  .qrcode-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .qrcode-image {
+    width: 200px;
+    height: 200px;
+    border: 1px solid var(--el-border-color-lighter);
+    border-radius: 8px;
+  }
+
+  .qrcode-content {
+    max-width: 300px;
+    text-align: center;
+    word-break: break-all;
+  }
+
+  .qrcode-empty {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 200px;
+    height: 200px;
+    border: 1px dashed var(--el-border-color);
+    border-radius: 8px;
+  }
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(6, 1fr);
+  gap: 16px;
+  margin-bottom: 16px;
+
+  @media (max-width: 1200px) {
+    grid-template-columns: repeat(3, 1fr);
+  }
+
+  @media (max-width: 768px) {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+
+.stat-card {
+  text-align: center;
+
+  .stat-item {
+    padding: 12px 0;
+  }
+
+  .stat-value {
+    font-size: 24px;
+    font-weight: 700;
+    color: var(--el-text-color-primary);
+    margin-bottom: 8px;
+  }
+
+  .stat-label {
+    font-size: 13px;
+    color: var(--el-text-color-secondary);
+  }
+
+  &.stat-card--primary {
+    .stat-value {
+      color: var(--el-color-primary);
+    }
+  }
+
+  &.stat-card--warning {
+    .stat-value {
+      color: var(--el-color-warning);
+    }
+  }
+
+  &.stat-card--success {
+    .stat-value {
+      color: var(--el-color-success);
+    }
+  }
+}
+</style>

+ 239 - 0
haha-admin-web/src/views/distribution/distributor/index.vue

@@ -0,0 +1,239 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useDistributor } from "../utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { ElImageViewer } from "element-plus";
+
+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 View from "~icons/ep/view";
+import Grid from "~icons/ep/grid";
+
+defineOptions({
+  name: "DistributorManage"
+});
+
+const formRef = ref();
+const tableRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  statusOptions,
+  onSearch,
+  resetForm,
+  handleCreate,
+  handleUpdate,
+  handleActivate,
+  handleDeactivate,
+  handleDelete,
+  handleGenerateQrcode,
+  handlePreviewQrcode,
+  handleSizeChange,
+  handleCurrentChange,
+  showImageViewer,
+  currentPreviewUrl
+} = useDistributor();
+</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="code">
+        <el-input
+          v-model="form.code"
+          placeholder="请输入编码"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <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="phone">
+        <el-input
+          v-model="form.phone"
+          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
+            v-for="item in statusOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </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="handleCreate"
+        >
+          新增分销员
+        </el-button>
+      </template>
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          row-key="id"
+          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, size }">
+            <el-button
+              v-if="row.status === 0"
+              class="reset-margin"
+              link
+              type="success"
+              :size="size"
+              :icon="useRenderIcon(VideoPlay)"
+              @click="handleActivate(row)"
+            >
+              激活
+            </el-button>
+            <el-button
+              v-if="row.status === 1"
+              class="reset-margin"
+              link
+              type="warning"
+              :size="size"
+              :icon="useRenderIcon(VideoPause)"
+              @click="handleDeactivate(row)"
+            >
+              停用
+            </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="handleUpdate(row)"
+            >
+              编辑
+            </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(View)"
+              @click="$router.push(`/distribution/distributor/detail?id=${row.id}`)"
+            >
+              详情
+            </el-button>
+            <el-button
+              v-if="!row.qrcodeContent"
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(Grid)"
+              @click="handleGenerateQrcode(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>
+
+    <!-- 二维码预览 -->
+    <teleport to="body">
+      <ElImageViewer
+        v-if="showImageViewer"
+        :url-list="[currentPreviewUrl]"
+        :initial-index="0"
+        :teleported="false"
+        :close-on-click-modal="true"
+        @close="showImageViewer = false"
+      />
+    </teleport>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 140 - 0
haha-admin-web/src/views/distribution/referral/index.vue

@@ -0,0 +1,140 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useReferral } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import Refresh from "~icons/ep/refresh";
+
+defineOptions({
+  name: "DistributionReferral"
+});
+
+const formRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  onSearch,
+  resetForm,
+  handleSizeChange,
+  handleCurrentChange
+} = useReferral();
+
+const timeRange = ref([]);
+
+function handleTimeRangeChange(val) {
+  if (val && val.length === 2) {
+    form.startDate = val[0];
+    form.endDate = val[1];
+  } else {
+    form.startDate = "";
+    form.endDate = "";
+  }
+}
+</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="distributorId">
+        <el-input
+          v-model="form.distributorId"
+          placeholder="请输入分销员ID"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="用户ID:" prop="userId">
+        <el-input
+          v-model="form.userId"
+          placeholder="请输入用户ID"
+          clearable
+          class="w-[180px]!"
+        />
+      </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-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="handleTimeRangeChange"
+        />
+      </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 v-slot="{ size, dynamicColumns }">
+        <pure-table
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :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>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 171 - 0
haha-admin-web/src/views/distribution/referral/utils/hook.tsx

@@ -0,0 +1,171 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { getReferralList } from "@/api/distribution";
+import type { PaginationProps } from "@pureadmin/table";
+import { onMounted, reactive, ref } from "vue";
+import type { ReferralQuery } from "../../utils/types";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  resetPagination
+} from "@/utils/paginationHelper";
+
+export function useReferral() {
+  const form = reactive<ReferralQuery>({
+    distributorId: undefined,
+    userId: undefined,
+    status: undefined,
+    startDate: "",
+    endDate: ""
+  });
+  const loading = ref(true);
+  const dataList = ref([]);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  const statusMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+    0: { text: "待生效", type: "warning" },
+    1: { text: "已生效", type: "success" },
+    2: { text: "已失效", type: "danger" }
+  };
+
+  const columns: TableColumnList = [
+    {
+      label: "分销员名称",
+      prop: "distributorName",
+      minWidth: 120,
+      formatter: ({ distributorName }) => distributorName || "-"
+    },
+    {
+      label: "用户名称",
+      prop: "userName",
+      minWidth: 120,
+      formatter: ({ userName }) => userName || "-"
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 90,
+      cellRenderer: ({ row }) => {
+        const item = statusMap[row.status] || { text: "未知", type: "info" };
+        return <el-tag type={item.type as any} size="small">{item.text}</el-tag>;
+      }
+    },
+    {
+      label: "首单ID",
+      prop: "firstOrderId",
+      minWidth: 100,
+      formatter: ({ firstOrderId }) => firstOrderId || "-"
+    },
+    {
+      label: "首单金额",
+      prop: "firstOrderAmount",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        return row.firstOrderAmount ? (
+          <span class="text-red-600 font-bold">¥{row.firstOrderAmount}</span>
+        ) : (
+          <span class="text-gray-400">-</span>
+        );
+      }
+    },
+    {
+      label: "首单时间",
+      prop: "firstOrderTime",
+      minWidth: 160,
+      formatter: ({ firstOrderTime }) =>
+        firstOrderTime
+          ? dayjs(firstOrderTime).format("YYYY-MM-DD HH:mm:ss")
+          : "-"
+    },
+    {
+      label: "生效时间",
+      prop: "effectiveTime",
+      minWidth: 160,
+      formatter: ({ effectiveTime }) =>
+        effectiveTime
+          ? dayjs(effectiveTime).format("YYYY-MM-DD HH:mm:ss")
+          : "-"
+    },
+    {
+      label: "失效时间",
+      prop: "expireTime",
+      minWidth: 160,
+      formatter: ({ expireTime }) =>
+        expireTime
+          ? dayjs(expireTime).format("YYYY-MM-DD HH:mm:ss")
+          : "-"
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime
+          ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
+          : "-"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+
+      if (form.distributorId) searchParams.distributorId = form.distributorId;
+      if (form.userId) searchParams.userId = form.userId;
+      if (form.status !== undefined && form.status !== null)
+        searchParams.status = form.status;
+      if (form.startDate) searchParams.startDate = form.startDate;
+      if (form.endDate) searchParams.endDate = form.endDate;
+
+      const { data } = await getReferralList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
+    } catch (error) {
+      console.error("获取推荐记录列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    }
+  }
+
+  const resetForm = formEl => {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  };
+
+  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,
+    onSearch,
+    resetForm,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}

+ 241 - 0
haha-admin-web/src/views/distribution/report/index.vue

@@ -0,0 +1,241 @@
+<script setup lang="ts">
+import { ref, onMounted, reactive, h } from "vue";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import {
+  getReportList,
+  generateReport,
+  exportReport
+} from "@/api/distribution";
+import type { PaginationProps } from "@pureadmin/table";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange
+} from "@/utils/paginationHelper";
+import { message } from "@/utils/message";
+import Refresh from "~icons/ep/refresh";
+
+defineOptions({
+  name: "DistributionReport"
+});
+
+const loading = ref(false);
+const dataList = ref([]);
+const pagination = reactive<PaginationProps>(initPagination());
+const selectedMonth = ref("");
+
+const columns: TableColumnList = [
+  {
+    label: "分销员名称",
+    prop: "distributorName",
+    minWidth: 120,
+    formatter: ({ distributorName }) => distributorName || "-"
+  },
+  {
+    label: "月份",
+    prop: "yearMonth",
+    minWidth: 100,
+    formatter: ({ yearMonth }) => yearMonth || "-"
+  },
+  {
+    label: "总拉新数",
+    prop: "totalReferrals",
+    minWidth: 90,
+    formatter: ({ totalReferrals }) => totalReferrals ?? "-"
+  },
+  {
+    label: "有效用户数",
+    prop: "validReferrals",
+    minWidth: 90,
+    formatter: ({ validReferrals }) => validReferrals ?? "-"
+  },
+  {
+    label: "待确认数",
+    prop: "pendingReferrals",
+    minWidth: 90,
+    formatter: ({ pendingReferrals }) => pendingReferrals ?? "-"
+  },
+  {
+    label: "已过期数",
+    prop: "expiredReferrals",
+    minWidth: 90,
+    formatter: ({ expiredReferrals }) => expiredReferrals ?? "-"
+  },
+  {
+    label: "总佣金(¥)",
+    prop: "totalCommission",
+    minWidth: 110,
+    cellRenderer: ({ row }) => {
+      return row.totalCommission !== undefined &&
+        row.totalCommission !== null
+        ? h("span", { class: "text-red-600 font-bold" }, `¥${Number(row.totalCommission).toFixed(2)}`)
+        : h("span", { class: "text-gray-400" }, "-");
+    }
+  },
+  {
+    label: "已结算佣金(¥)",
+    prop: "settledCommission",
+    minWidth: 120,
+    cellRenderer: ({ row }) => {
+      return row.settledCommission !== undefined &&
+        row.settledCommission !== null
+        ? h("span", {}, `¥${Number(row.settledCommission).toFixed(2)}`)
+        : h("span", { class: "text-gray-400" }, "-");
+    }
+  },
+  {
+    label: "待结算佣金(¥)",
+    prop: "pendingCommission",
+    minWidth: 120,
+    cellRenderer: ({ row }) => {
+      return row.pendingCommission !== undefined &&
+        row.pendingCommission !== null
+        ? h("span", {}, `¥${Number(row.pendingCommission).toFixed(2)}`)
+        : h("span", { class: "text-gray-400" }, "-");
+    }
+  }
+];
+
+async function onSearch() {
+  loading.value = true;
+  try {
+    const searchParams: any = {
+      page: pagination.currentPage,
+      pageSize: pagination.pageSize
+    };
+    if (selectedMonth.value) {
+      searchParams.yearMonth = selectedMonth.value;
+    }
+    const { data } = await getReportList(searchParams);
+    if (data) {
+      dataList.value = data.list || [];
+      pagination.total = data.total || 0;
+    }
+  } catch (error) {
+    console.error("获取月度报表列表失败:", error);
+    dataList.value = [];
+    pagination.total = 0;
+  } finally {
+    setTimeout(() => {
+      loading.value = false;
+    }, 300);
+  }
+}
+
+async function handleGenerate() {
+  if (!selectedMonth.value) {
+    message("请先选择月份", { type: "warning" });
+    return;
+  }
+  try {
+    await generateReport(selectedMonth.value);
+    message("报表生成成功", { type: "success" });
+    onSearch();
+  } catch (error) {
+    console.error("生成报表失败:", error);
+    message("生成报表失败", { type: "error" });
+  }
+}
+
+async function handleExport() {
+  if (!selectedMonth.value) {
+    message("请先选择月份", { type: "warning" });
+    return;
+  }
+  try {
+    const res = await exportReport(selectedMonth.value);
+    const blob = new Blob([res as any], {
+      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+    });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = `分销月度报表_${selectedMonth.value}.xlsx`;
+    link.click();
+    window.URL.revokeObjectURL(url);
+    message("导出成功", { type: "success" });
+  } catch (error) {
+    console.error("导出报表失败:", error);
+    message("导出报表失败", { type: "error" });
+  }
+}
+
+function handleSizeChange(val: number) {
+  handlePageSizeChange(val, pagination, onSearch);
+}
+
+function handleCurrentChange(val: number) {
+  handleCurrentPageChange(val, pagination, onSearch);
+}
+
+onMounted(() => {
+  onSearch();
+});
+</script>
+
+<template>
+  <div class="main">
+    <!-- 顶部操作栏 -->
+    <div class="search-form bg-bg_color w-full pl-8 pt-[12px] pb-[12px] overflow-auto">
+      <el-form :inline="true">
+        <el-form-item label="选择月份:">
+          <el-date-picker
+            v-model="selectedMonth"
+            type="month"
+            format="YYYY-MM"
+            value-format="YYYY-MM"
+            placeholder="请选择月份"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            :loading="loading"
+            @click="handleGenerate"
+          >
+            生成报表
+          </el-button>
+          <el-button @click="handleExport">
+            导出
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <PureTableBar
+      title="月度报表"
+      :columns="columns"
+      @refresh="onSearch"
+    >
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :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>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 424 - 0
haha-admin-web/src/views/distribution/utils/hook.tsx

@@ -0,0 +1,424 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { addDialog } from "@/components/ReDialog";
+import QRCode from "qrcode";
+import {
+  getDistributorList,
+  createDistributor,
+  updateDistributor,
+  activateDistributor,
+  deactivateDistributor,
+  deleteDistributor,
+  generateQrcode
+} from "@/api/distribution";
+import type { PaginationProps } from "@pureadmin/table";
+import { deviceDetection } from "@pureadmin/utils";
+import { onMounted, reactive, ref, toRaw, h } from "vue";
+import {
+  ElForm,
+  ElInput,
+  ElFormItem,
+  ElTag,
+  ElSelect,
+  ElOption,
+  ElImageViewer
+} from "element-plus";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  resetPagination,
+  updatePaginationData
+} from "@/utils/paginationHelper";
+import type { Distributor, DistributorQuery } from "./types";
+
+interface SearchFormProps {
+  code?: string;
+  name?: string;
+  phone?: string;
+  status?: number | undefined;
+}
+
+interface DistributorFormProps {
+  id?: string;
+  name: string;
+  phone: string;
+  remark: string;
+}
+
+const statusMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+  0: { text: "待激活", type: "info" },
+  1: { text: "已激活", type: "success" },
+  2: { text: "已停用", type: "danger" }
+};
+
+const statusOptions = [
+  { value: 0, label: "待激活" },
+  { value: 1, label: "已激活" },
+  { value: 2, label: "已停用" }
+];
+
+export function useDistributor() {
+  const form = reactive<SearchFormProps>({
+    code: "",
+    name: "",
+    phone: "",
+    status: undefined
+  });
+  const loading = ref(true);
+  const dataList = ref<Distributor[]>([]);
+  const pagination = reactive<PaginationProps>(initPagination());
+  const qrcodeMap = reactive<Record<string, string>>({});
+  const qrcodePreviewMap = reactive<Record<string, string>>({});
+  const showImageViewer = ref(false);
+  const currentPreviewUrl = ref<string>("");
+
+  async function generateQrcodeForRow(row: Distributor) {
+    if (!row.qrcodeContent) return "";
+    if (qrcodeMap[row.id]) return qrcodeMap[row.id];
+    try {
+      const dataUrl = await QRCode.toDataURL(row.qrcodeContent, {
+        width: 80,
+        margin: 1,
+        color: { dark: "#000000", light: "#ffffff" }
+      });
+      qrcodeMap[row.id] = dataUrl;
+      return dataUrl;
+    } catch {
+      return "";
+    }
+  }
+
+  async function generateQrcodePreview(row: Distributor) {
+    if (!row.qrcodeContent) return "";
+    if (qrcodePreviewMap[row.id]) return qrcodePreviewMap[row.id];
+    try {
+      const dataUrl = await QRCode.toDataURL(row.qrcodeContent, {
+        width: 400,
+        margin: 2,
+        color: { dark: "#000000", light: "#ffffff" }
+      });
+      qrcodePreviewMap[row.id] = dataUrl;
+      return dataUrl;
+    } catch {
+      return "";
+    }
+  }
+
+  function handlePreviewQrcode(row: Distributor) {
+    if (!row.qrcodeContent) return;
+    const previewUrl = qrcodePreviewMap[row.id];
+    if (previewUrl) {
+      currentPreviewUrl.value = previewUrl;
+      showImageViewer.value = true;
+    } else {
+      generateQrcodePreview(row).then(url => {
+        if (url) {
+          currentPreviewUrl.value = url;
+          showImageViewer.value = true;
+        }
+      });
+    }
+  }
+
+  const columns = [
+    {
+      label: "分销员编码",
+      prop: "code",
+      minWidth: 120
+    },
+    {
+      label: "姓名",
+      prop: "name",
+      minWidth: 100
+    },
+    {
+      label: "手机号",
+      prop: "phone",
+      minWidth: 130
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        const item = statusMap[row.status] || { text: "未知", type: "info" as const };
+        return <ElTag type={item.type as "success" | "warning" | "danger" | "info" | "primary"}>{item.text}</ElTag>;
+      }
+    },
+    {
+      label: "二维码",
+      prop: "qrcodeContent",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        if (!row.qrcodeContent) {
+          return <span style="color: var(--el-text-color-placeholder)">未生成</span>;
+        }
+        const qrcodeUrl = qrcodeMap[row.id];
+        if (!qrcodeUrl) {
+          generateQrcodeForRow(row);
+          return <span style="color: var(--el-text-color-secondary)">加载中...</span>;
+        }
+        return (
+          <img
+            src={qrcodeUrl}
+            style={{ width: "60px", height: "60px", cursor: "pointer", borderRadius: "4px" }}
+            onClick={() => handlePreviewQrcode(row)}
+            title="点击查看大图"
+          />
+        );
+      }
+    },
+    {
+      label: "总推荐数",
+      prop: "totalReferrals",
+      width: 100,
+      formatter: ({ totalReferrals }) => totalReferrals || 0
+    },
+    {
+      label: "有效推荐数",
+      prop: "validReferrals",
+      width: 100,
+      formatter: ({ validReferrals }) => validReferrals || 0
+    },
+    {
+      label: "佣金余额",
+      prop: "commissionBalance",
+      width: 120,
+      formatter: ({ commissionBalance }) =>
+        commissionBalance !== undefined && commissionBalance !== null
+          ? `¥${commissionBalance.toFixed(2)}`
+          : "¥0.00"
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : ""
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 280,
+      slot: "operation"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: DistributorQuery = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+
+      if (form.code) searchParams.code = form.code;
+      if (form.name) searchParams.name = form.name;
+      if (form.phone) searchParams.phone = form.phone;
+      if (form.status !== undefined && form.status !== null)
+        searchParams.status = form.status;
+
+      const res = await getDistributorList(searchParams);
+      if (res && res.data) {
+        dataList.value = res.data.list || [];
+        updatePaginationData(pagination, res.data);
+        dataList.value.forEach((row: Distributor) => {
+          if (row.qrcodeContent) {
+            generateQrcodeForRow(row);
+            generateQrcodePreview(row);
+          }
+        });
+      }
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function resetForm(formEl) {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  }
+
+  async function handleDelete(row) {
+    const { code } = await deleteDistributor(row.id);
+    if (code === 200) {
+      message("删除成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handleActivate(row) {
+    const { code } = await activateDistributor(row.id);
+    if (code === 200) {
+      message("激活成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handleDeactivate(row) {
+    const { code } = await deactivateDistributor(row.id);
+    if (code === 200) {
+      message("停用成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handleGenerateQrcode(row) {
+    if (row.qrcodeContent) {
+      message("该分销员已生成二维码", { type: "info" });
+      return;
+    }
+    const { code } = await generateQrcode(row.id);
+    if (code === 200) {
+      message("二维码生成成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  function renderForm(formData: DistributorFormProps) {
+    return (
+      <ElForm label-width="100px" size="default">
+        <ElFormItem label="姓名" required>
+          <ElInput
+            v-model={formData.name}
+            placeholder="请输入分销员姓名"
+            clearable
+            maxlength={20}
+            showWordLimit
+          />
+        </ElFormItem>
+        <ElFormItem label="手机号" required>
+          <ElInput
+            v-model={formData.phone}
+            placeholder="请输入手机号"
+            clearable
+            maxlength={11}
+          />
+        </ElFormItem>
+        <ElFormItem label="备注">
+          <ElInput
+            v-model={formData.remark}
+            type="textarea"
+            rows={3}
+            placeholder="请输入备注(可选)"
+            maxlength={200}
+            showWordLimit
+          />
+        </ElFormItem>
+      </ElForm>
+    );
+  }
+
+  function validateForm(formData: DistributorFormProps): boolean {
+    if (!formData.name) {
+      message("请输入分销员姓名", { type: "warning" });
+      return false;
+    }
+    if (!formData.phone) {
+      message("请输入手机号", { type: "warning" });
+      return false;
+    }
+    const phoneReg = /^1[3-9]\d{9}$/;
+    if (!phoneReg.test(formData.phone)) {
+      message("请输入正确的手机号", { type: "warning" });
+      return false;
+    }
+    return true;
+  }
+
+  function handleCreate() {
+    const formData = reactive<DistributorFormProps>({
+      name: "",
+      phone: "",
+      remark: ""
+    });
+
+    addDialog({
+      title: "新增分销员",
+      width: "500px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => renderForm(formData),
+      beforeSure: async done => {
+        if (!validateForm(formData)) return;
+        try {
+          const { code } = await createDistributor(toRaw(formData));
+          if (code === 200) {
+            message("新增成功", { type: "success" });
+            done();
+            onSearch();
+          }
+        } catch (error) {
+          message("新增失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  function handleUpdate(row) {
+    const formData = reactive<DistributorFormProps>({
+      id: row.id,
+      name: row.name,
+      phone: row.phone,
+      remark: row.remark || ""
+    });
+
+    addDialog({
+      title: "编辑分销员",
+      width: "500px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => renderForm(formData),
+      beforeSure: async done => {
+        if (!validateForm(formData)) return;
+        try {
+          const { code } = await updateDistributor(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,
+    statusOptions,
+    onSearch,
+    resetForm,
+    handleCreate,
+    handleUpdate,
+    handleActivate,
+    handleDeactivate,
+    handleDelete,
+    handleGenerateQrcode,
+    handlePreviewQrcode,
+    handleSizeChange,
+    handleCurrentChange,
+    showImageViewer,
+    currentPreviewUrl
+  };
+}

+ 182 - 0
haha-admin-web/src/views/distribution/utils/types.ts

@@ -0,0 +1,182 @@
+// 分销员信息类型
+interface Distributor {
+  id: string;
+  code: string;
+  name: string;
+  phone: string;
+  status: number;
+  statusLabel: string;
+  qrcodeContent: string;
+  totalReferrals: number;
+  validReferrals: number;
+  commissionBalance: number;
+  frozenAmount: number;
+  totalCommission: number;
+  totalWithdrawn: number;
+  remark: string;
+  creatorName: string;
+  createTime: string;
+  updateTime?: string;
+}
+
+// 分销员查询参数
+interface DistributorQuery {
+  page?: number;
+  pageSize?: number;
+  code?: string;
+  name?: string;
+  phone?: string;
+  status?: number;
+}
+
+// 推荐记录类型
+interface Referral {
+  id: string;
+  distributorId: string;
+  distributorName: string;
+  userId: string;
+  userName: string;
+  status: number;
+  statusLabel: string;
+  firstOrderId: string;
+  firstOrderAmount: number;
+  firstOrderTime: string;
+  effectiveTime: string;
+  expireTime: string;
+  createTime: string;
+}
+
+// 推荐记录查询参数
+interface ReferralQuery {
+  page?: number;
+  pageSize?: number;
+  distributorId?: string;
+  userId?: string;
+  status?: number;
+  startDate?: string;
+  endDate?: string;
+}
+
+// 佣金记录类型
+interface Commission {
+  id: string;
+  distributorId: string;
+  distributorName: string;
+  userId: string;
+  userName: string;
+  commissionType: number;
+  commissionTypeLabel: string;
+  amount: number;
+  orderAmount: number;
+  rate: number;
+  status: number;
+  statusLabel: string;
+  settleTime: string;
+  createTime: string;
+}
+
+// 佣金记录查询参数
+interface CommissionQuery {
+  page?: number;
+  pageSize?: number;
+  distributorId?: string;
+  userId?: string;
+  commissionType?: number;
+  status?: number;
+  startDate?: string;
+  endDate?: string;
+}
+
+// 提现记录类型
+interface Withdrawal {
+  id: string;
+  distributorId: string;
+  distributorName: string;
+  amount: number;
+  status: number;
+  statusLabel: string;
+  reviewerId: string;
+  reviewerName: string;
+  reviewTime: string;
+  reviewRemark: string;
+  transferTime: string;
+  createTime: string;
+}
+
+// 提现记录查询参数
+interface WithdrawalQuery {
+  page?: number;
+  pageSize?: number;
+  distributorId?: string;
+  status?: number;
+  startDate?: string;
+  endDate?: string;
+}
+
+// 月度报表类型
+interface MonthlyReport {
+  id: string;
+  distributorId: string;
+  distributorName: string;
+  yearMonth: string;
+  totalReferrals: number;
+  validReferrals: number;
+  pendingReferrals: number;
+  expiredReferrals: number;
+  totalCommission: number;
+  settledCommission: number;
+  pendingCommission: number;
+  createTime: string;
+}
+
+// 月度报表查询参数
+interface MonthlyReportQuery {
+  page?: number;
+  pageSize?: number;
+  yearMonth?: string;
+  distributorId?: string;
+}
+
+// 仪表盘数据类型
+interface DashboardData {
+  totalDistributors: number;
+  activeDistributors: number;
+  totalReferrals: number;
+  validReferrals: number;
+  totalCommission: number;
+  monthlyCommission: number;
+  trends: MonthlyTrend[];
+}
+
+// 月度趋势类型
+interface MonthlyTrend {
+  yearMonth: string;
+  referralCount: number;
+  validCount: number;
+  commissionAmount: number;
+}
+
+// 分销配置类型
+interface DistributorConfig {
+  id: string;
+  configKey: string;
+  configValue: string;
+  configDesc: string;
+  updateTime: string;
+}
+
+export type {
+  Distributor,
+  DistributorQuery,
+  Referral,
+  ReferralQuery,
+  Commission,
+  CommissionQuery,
+  Withdrawal,
+  WithdrawalQuery,
+  MonthlyReport,
+  MonthlyReportQuery,
+  DashboardData,
+  MonthlyTrend,
+  DistributorConfig
+};

+ 183 - 0
haha-admin-web/src/views/distribution/withdrawal/index.vue

@@ -0,0 +1,183 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useWithdrawal } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import Refresh from "~icons/ep/refresh";
+import Check from "~icons/ep/check";
+import Close from "~icons/ep/close";
+import Money from "~icons/ep/money";
+
+defineOptions({
+  name: "DistributionWithdrawal"
+});
+
+const formRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  onSearch,
+  resetForm,
+  handleReview,
+  handleConfirmTransfer,
+  handleSizeChange,
+  handleCurrentChange
+} = useWithdrawal();
+
+const timeRange = ref([]);
+
+function handleTimeRangeChange(val) {
+  if (val && val.length === 2) {
+    form.startDate = val[0];
+    form.endDate = val[1];
+  } else {
+    form.startDate = "";
+    form.endDate = "";
+  }
+}
+</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="distributorId">
+        <el-input
+          v-model="form.distributorId"
+          placeholder="请输入分销员ID"
+          clearable
+          class="w-[180px]!"
+        />
+      </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="handleTimeRangeChange"
+        />
+      </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 v-slot="{ size, dynamicColumns }">
+        <pure-table
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :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, size }">
+            <!-- 待审核状态:显示通过和拒绝按钮 -->
+            <template v-if="row.status === 0">
+              <el-button
+                class="reset-margin"
+                link
+                type="success"
+                :size="size"
+                :icon="useRenderIcon(Check)"
+                @click="handleReview(row, 1)"
+              >
+                通过
+              </el-button>
+              <el-button
+                class="reset-margin"
+                link
+                type="danger"
+                :size="size"
+                :icon="useRenderIcon(Close)"
+                @click="handleReview(row, 2)"
+              >
+                拒绝
+              </el-button>
+            </template>
+            <!-- 已通过状态:显示确认到账按钮 -->
+            <el-button
+              v-if="row.status === 1"
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(Money)"
+              @click="handleConfirmTransfer(row)"
+            >
+              确认到账
+            </el-button>
+            <!-- 其他状态:无操作 -->
+            <span
+              v-if="row.status !== 0 && row.status !== 1"
+              class="text-gray-400 text-sm"
+            >
+              -
+            </span>
+          </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>

+ 280 - 0
haha-admin-web/src/views/distribution/withdrawal/utils/hook.tsx

@@ -0,0 +1,280 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { addDialog } from "@/components/ReDialog";
+import { getWithdrawalList, reviewWithdrawal, confirmTransfer } from "@/api/distribution";
+import type { PaginationProps } from "@pureadmin/table";
+import { deviceDetection } from "@pureadmin/utils";
+import { onMounted, reactive, ref } from "vue";
+import type { WithdrawalQuery } from "../../utils/types";
+import {
+  ElForm,
+  ElFormItem,
+  ElInput
+} from "element-plus";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  resetPagination
+} from "@/utils/paginationHelper";
+
+export function useWithdrawal() {
+  const form = reactive<WithdrawalQuery>({
+    distributorId: undefined,
+    status: undefined,
+    startDate: "",
+    endDate: ""
+  });
+  const loading = ref(true);
+  const dataList = ref([]);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  const statusMap: Record<number, { text: string; type: "success" | "warning" | "danger" | "info" | "primary" }> = {
+    0: { text: "待审核", type: "warning" },
+    1: { text: "已通过", type: "success" },
+    2: { text: "已拒绝", type: "danger" },
+    3: { text: "已到账", type: "info" }
+  };
+
+  const columns: TableColumnList = [
+    {
+      label: "分销员名称",
+      prop: "distributorName",
+      minWidth: 120,
+      formatter: ({ distributorName }) => distributorName || "-"
+    },
+    {
+      label: "提现金额",
+      prop: "amount",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        return row.amount ? (
+          <span class="text-red-600 font-bold">¥{row.amount}</span>
+        ) : (
+          <span class="text-gray-400">-</span>
+        );
+      }
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 90,
+      cellRenderer: ({ row }) => {
+        const item = statusMap[row.status] || { text: "未知", type: "info" };
+        return <el-tag type={item.type as any} size="small">{item.text}</el-tag>;
+      }
+    },
+    {
+      label: "审核人",
+      prop: "reviewerName",
+      minWidth: 100,
+      formatter: ({ reviewerName }) => reviewerName || "-"
+    },
+    {
+      label: "审核时间",
+      prop: "reviewTime",
+      minWidth: 160,
+      formatter: ({ reviewTime }) =>
+        reviewTime ? dayjs(reviewTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "审核备注",
+      prop: "reviewRemark",
+      minWidth: 140,
+      showOverflowTooltip: true,
+      formatter: ({ reviewRemark }) => reviewRemark || "-"
+    },
+    {
+      label: "到账时间",
+      prop: "transferTime",
+      minWidth: 160,
+      formatter: ({ transferTime }) =>
+        transferTime ? dayjs(transferTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 200,
+      slot: "operation"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+
+      if (form.distributorId) searchParams.distributorId = form.distributorId;
+      if (form.status !== undefined && form.status !== null)
+        searchParams.status = form.status;
+      if (form.startDate) searchParams.startDate = form.startDate;
+      if (form.endDate) searchParams.endDate = form.endDate;
+
+      const { data } = await getWithdrawalList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
+    } catch (error) {
+      console.error("获取提现记录列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    }
+  }
+
+  const resetForm = formEl => {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  };
+
+  function handleSizeChange(val: number) {
+    handlePageSizeChange(val, pagination, onSearch);
+  }
+
+  function handleCurrentChange(val: number) {
+    handleCurrentPageChange(val, pagination, onSearch);
+  }
+
+  // 审核提现(通过/拒绝)
+  const reviewFormRef = ref();
+  const reviewFormData = reactive({
+    id: 0,
+    status: 1,
+    remark: ""
+  });
+
+  function handleReview(row: any, status: number) {
+    reviewFormData.id = row.id;
+    reviewFormData.status = status;
+    reviewFormData.remark = "";
+
+    const title = status === 1 ? "通过提现申请" : "拒绝提现申请";
+
+    addDialog({
+      title,
+      width: "30%",
+      draggable: true,
+      closeOnClickModal: false,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => (
+        <ElForm ref={reviewFormRef} model={reviewFormData} label-width="80px">
+          <ElFormItem
+            label="备注"
+            prop="remark"
+            rules={[
+              {
+                required: status === 2,
+                message: "拒绝时请填写备注",
+                trigger: "blur"
+              }
+            ]}
+          >
+            <ElInput
+              v-model={reviewFormData.remark}
+              type="textarea"
+              placeholder={status === 2 ? "请填写拒绝原因" : "备注(选填)"}
+              rows={3}
+            />
+          </ElFormItem>
+        </ElForm>
+      ),
+      beforeSure: async done => {
+        if (status === 2) {
+          const valid = await reviewFormRef.value
+            ?.validate()
+            .catch(() => false);
+          if (!valid) return;
+        }
+
+        try {
+          const res = await reviewWithdrawal({
+            id: reviewFormData.id,
+            status: reviewFormData.status,
+            remark: reviewFormData.remark
+          });
+          if (res.code === 200) {
+            message(
+              status === 1 ? "已通过提现申请" : "已拒绝提现申请",
+              { type: "success" }
+            );
+            done();
+            onSearch();
+          } else {
+            message(res.message || "操作失败", { type: "error" });
+          }
+        } catch (error) {
+          message("操作失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  // 确认到账
+  function handleConfirmTransfer(row: any) {
+    addDialog({
+      title: "确认到账",
+      width: "30%",
+      draggable: true,
+      closeOnClickModal: false,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => (
+        <div>
+          <p>
+            确认提现金额 <span class="text-red-600 font-bold">¥{row.amount}</span> 已到账?
+          </p>
+          <p class="text-gray-500 text-sm mt-2">
+            分销员:{row.distributorName}
+          </p>
+        </div>
+      ),
+      beforeSure: async done => {
+        try {
+          const res = await confirmTransfer(row.id);
+          if (res.code === 200) {
+            message("已确认到账", { type: "success" });
+            done();
+            onSearch();
+          } else {
+            message(res.message || "操作失败", { type: "error" });
+          }
+        } catch (error) {
+          message("操作失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    statusMap,
+    onSearch,
+    resetForm,
+    handleReview,
+    handleConfirmTransfer,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}

+ 2 - 1
haha-admin-web/vite.config.ts

@@ -37,7 +37,8 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
     "/dashboard",
     "/dict",
     "/timed-discount",
-    "/layer-templates"
+    "/layer-templates",
+    "/distribution"
   ];
   
   // 动态生成代理配置

+ 2 - 2
haha-admin/src/main/java/com/haha/admin/config/JacksonConfig.java

@@ -53,10 +53,10 @@ public class JacksonConfig implements WebMvcConfigurer {
     public ObjectMapper objectMapper() {
         ObjectMapper mapper = new ObjectMapper();
 
-        // 注册Long类型转String序列化模块,避免前端精度丢失
+        // 注册Long包装类转String序列化模块,避免前端精度丢失
+        // 注意:不处理基本类型 long,因为分页字段(total/pageSize等)需要保持数字类型
         SimpleModule longModule = new SimpleModule("LongToStringModule");
         longModule.addSerializer(Long.class, ToStringSerializer.instance);
-        longModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
         mapper.registerModule(longModule);
 
         JavaTimeModule javaTimeModule = new JavaTimeModule();

+ 200 - 0
haha-admin/src/main/java/com/haha/admin/controller/DistributorAdminController.java

@@ -0,0 +1,200 @@
+package com.haha.admin.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+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.Distributor;
+import com.haha.entity.DistributorCommission;
+import com.haha.entity.DistributorConfig;
+import com.haha.entity.DistributorMonthlyReport;
+import com.haha.entity.DistributorReferral;
+import com.haha.entity.DistributorWithdrawal;
+import com.haha.entity.dto.CommissionQueryDTO;
+import com.haha.entity.dto.DistributorConfigDTO;
+import com.haha.entity.dto.DistributorCreateDTO;
+import com.haha.entity.dto.DistributorQueryDTO;
+import com.haha.entity.dto.DistributorUpdateDTO;
+import com.haha.entity.dto.MonthlyReportQueryDTO;
+import com.haha.entity.dto.ReferralQueryDTO;
+import com.haha.entity.dto.WithdrawalQueryDTO;
+import com.haha.entity.dto.WithdrawalReviewDTO;
+import com.haha.service.DistributorCommissionService;
+import com.haha.service.DistributorConfigService;
+import com.haha.service.DistributorQrcodeService;
+import com.haha.service.DistributorReferralService;
+import com.haha.service.DistributorReportService;
+import com.haha.service.DistributorService;
+import com.haha.service.DistributorWithdrawalService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/distribution")
+@RequiredArgsConstructor
+public class DistributorAdminController {
+
+    private final DistributorService distributorService;
+    private final DistributorReferralService referralService;
+    private final DistributorCommissionService commissionService;
+    private final DistributorWithdrawalService withdrawalService;
+    private final DistributorReportService reportService;
+    private final DistributorConfigService configService;
+    private final DistributorQrcodeService qrcodeService;
+
+    @RequirePermission("distribution:distributor:create")
+    @Log(module = "分销管理", operation = OperationType.INSERT, summary = "创建分销员")
+    @PostMapping("/distributor")
+    public Result<Distributor> createDistributor(@RequestBody @Valid DistributorCreateDTO dto) {
+        Long creatorId = StpUtil.getLoginIdAsLong();
+        String creatorName = (String) StpUtil.getSession().get("name");
+        Distributor distributor = distributorService.create(dto, creatorId, creatorName);
+        return Result.success("创建成功", distributor);
+    }
+
+    @RequirePermission("distribution:distributor:update")
+    @Log(module = "分销管理", operation = OperationType.UPDATE, summary = "更新分销员")
+    @PutMapping("/distributor")
+    public Result<Distributor> updateDistributor(@RequestBody @Valid DistributorUpdateDTO dto) {
+        Distributor result = distributorService.update(dto);
+        return Result.success("更新成功", result);
+    }
+
+    @RequirePermission("distribution:distributor:read")
+    @GetMapping("/distributor/list")
+    public Result<PageResult<Distributor>> listDistributors(DistributorQueryDTO queryDTO) {
+        IPage<Distributor> page = distributorService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("distribution:distributor:read")
+    @GetMapping("/distributor/{id}")
+    public Result<Distributor> getDistributorDetail(@PathVariable Long id) {
+        Distributor result = distributorService.getDetail(id);
+        return Result.success("查询成功", result);
+    }
+
+    @RequirePermission("distribution:distributor:update")
+    @Log(module = "分销管理", operation = OperationType.UPDATE, summary = "激活分销员")
+    @PutMapping("/distributor/{id}/activate")
+    public Result<Void> activateDistributor(@PathVariable Long id) {
+        distributorService.activate(id);
+        qrcodeService.generateQrcode(id);
+        return Result.success("激活成功", null);
+    }
+
+    @RequirePermission("distribution:distributor:update")
+    @Log(module = "分销管理", operation = OperationType.UPDATE, summary = "停用分销员")
+    @PutMapping("/distributor/{id}/deactivate")
+    public Result<Void> deactivateDistributor(@PathVariable Long id) {
+        distributorService.deactivate(id);
+        return Result.success("停用成功", null);
+    }
+
+    @RequirePermission("distribution:distributor:delete")
+    @Log(module = "分销管理", operation = OperationType.DELETE, summary = "删除分销员")
+    @DeleteMapping("/distributor/{id}")
+    public Result<Void> deleteDistributor(@PathVariable Long id) {
+        distributorService.delete(id);
+        return Result.success("删除成功", null);
+    }
+
+    @RequirePermission("distribution:distributor:update")
+    @Log(module = "分销管理", operation = OperationType.UPDATE, summary = "生成二维码")
+    @PostMapping("/distributor/{id}/qrcode")
+    public Result<String> generateQrcode(@PathVariable Long id) {
+        String qrcodeContent = qrcodeService.generateQrcode(id);
+        return Result.success("二维码生成成功", qrcodeContent);
+    }
+
+    @RequirePermission("distribution:referral:read")
+    @GetMapping("/referral/list")
+    public Result<PageResult<DistributorReferral>> listReferrals(ReferralQueryDTO queryDTO) {
+        IPage<DistributorReferral> page = referralService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("distribution:commission:read")
+    @GetMapping("/commission/list")
+    public Result<PageResult<DistributorCommission>> listCommissions(CommissionQueryDTO queryDTO) {
+        IPage<DistributorCommission> page = commissionService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("distribution:withdrawal:read")
+    @GetMapping("/withdrawal/list")
+    public Result<PageResult<DistributorWithdrawal>> listWithdrawals(WithdrawalQueryDTO queryDTO) {
+        IPage<DistributorWithdrawal> page = withdrawalService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("distribution:withdrawal:review")
+    @Log(module = "分销管理", operation = OperationType.UPDATE, summary = "审核提现")
+    @PutMapping("/withdrawal/review")
+    public Result<DistributorWithdrawal> reviewWithdrawal(@RequestBody @Valid WithdrawalReviewDTO dto) {
+        Long reviewerId = StpUtil.getLoginIdAsLong();
+        String reviewerName = (String) StpUtil.getSession().get("name");
+        DistributorWithdrawal result = withdrawalService.reviewWithdrawal(dto, reviewerId, reviewerName);
+        return Result.success("审核成功", result);
+    }
+
+    @RequirePermission("distribution:withdrawal:review")
+    @Log(module = "分销管理", operation = OperationType.UPDATE, summary = "确认到账")
+    @PutMapping("/withdrawal/{id}/confirm")
+    public Result<DistributorWithdrawal> confirmTransfer(@PathVariable Long id) {
+        DistributorWithdrawal result = withdrawalService.confirmTransfer(id);
+        return Result.success("确认到账成功", result);
+    }
+
+    @RequirePermission("distribution:report:read")
+    @GetMapping("/report/list")
+    public Result<PageResult<DistributorMonthlyReport>> listReports(MonthlyReportQueryDTO queryDTO) {
+        IPage<DistributorMonthlyReport> page = reportService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("distribution:report:generate")
+    @Log(module = "分销管理", operation = OperationType.INSERT, summary = "生成月度报表")
+    @PostMapping("/report/generate")
+    public Result<Void> generateReport(@RequestParam String yearMonth) {
+        reportService.generateMonthlyReport(yearMonth);
+        return Result.success("报表生成成功", null);
+    }
+
+    @RequirePermission("distribution:report:export")
+    @GetMapping("/report/export")
+    public Result<List<DistributorMonthlyReport>> exportReport(@RequestParam String yearMonth) {
+        List<DistributorMonthlyReport> list = reportService.exportMonthlyReport(yearMonth);
+        return Result.success("查询成功", list);
+    }
+
+    @RequirePermission("distribution:dashboard:read")
+    @GetMapping("/dashboard")
+    public Result<Object> getDashboard() {
+        Object result = reportService.getDashboard();
+        return Result.success("查询成功", result);
+    }
+
+    @RequirePermission("distribution:config:read")
+    @GetMapping("/config/list")
+    public Result<List<DistributorConfig>> listConfigs() {
+        List<DistributorConfig> list = configService.getAllConfigs();
+        return Result.success("查询成功", list);
+    }
+
+    @RequirePermission("distribution:config:update")
+    @Log(module = "分销管理", operation = OperationType.UPDATE, summary = "更新分销配置")
+    @PutMapping("/config")
+    public Result<DistributorConfig> updateConfig(@RequestBody @Valid DistributorConfigDTO dto) {
+        DistributorConfig result = configService.updateConfig(dto);
+        return Result.success("更新成功", result);
+    }
+}

+ 61 - 0
haha-admin/src/main/java/com/haha/admin/controller/DistributorMpController.java

@@ -0,0 +1,61 @@
+package com.haha.admin.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.haha.common.exception.BusinessException;
+import com.haha.common.vo.Result;
+import com.haha.entity.DistributorReferral;
+import com.haha.entity.dto.WithdrawalApplyDTO;
+import com.haha.entity.vo.DistributorMpStatsVO;
+import com.haha.service.DistributorReferralService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+@Slf4j
+@RestController
+@RequestMapping("/mp/distribution")
+@RequiredArgsConstructor
+public class DistributorMpController {
+
+    private final DistributorReferralService referralService;
+
+    @PostMapping("/bind")
+    public Result<DistributorReferral> bindReferral(@RequestParam String distributorCode) {
+        Long userId = StpUtil.getLoginIdAsLong();
+        DistributorReferral referral = referralService.bindReferral(distributorCode, userId);
+        return Result.success("绑定成功", referral);
+    }
+
+    @GetMapping("/my-stats")
+    public Result<DistributorMpStatsVO> getMyStats() {
+        Long userId = StpUtil.getLoginIdAsLong();
+        // TODO: 需要建立小程序用户与分销员的关联关系
+        DistributorMpStatsVO stats = DistributorMpStatsVO.builder()
+                .totalReferrals(0L)
+                .validReferrals(0L)
+                .commissionBalance(BigDecimal.ZERO)
+                .frozenAmount(BigDecimal.ZERO)
+                .totalCommission(BigDecimal.ZERO)
+                .totalWithdrawn(BigDecimal.ZERO)
+                .build();
+        return Result.success("查询成功", stats);
+    }
+
+    @PostMapping("/withdrawal")
+    public Result<Void> applyWithdrawal(@RequestBody @Valid WithdrawalApplyDTO dto) {
+        Long userId = StpUtil.getLoginIdAsLong();
+        // TODO: 需要建立小程序用户与分销员的关联关系
+        throw new BusinessException(500, "功能开发中");
+    }
+
+    @GetMapping("/withdrawal/list")
+    public Result<Object> getMyWithdrawals() {
+        Long userId = StpUtil.getLoginIdAsLong();
+        // TODO: 需要建立小程序用户与分销员的关联关系
+        return Result.success("查询成功", Collections.emptyList());
+    }
+}

+ 64 - 0
haha-admin/src/main/java/com/haha/admin/task/DistributorTask.java

@@ -0,0 +1,64 @@
+package com.haha.admin.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.haha.entity.DistributorReferral;
+import com.haha.service.DistributorConfigService;
+import com.haha.service.DistributorReferralService;
+import com.haha.service.DistributorReportService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DistributorTask {
+
+    private final DistributorReportService reportService;
+    private final DistributorReferralService referralService;
+    private final DistributorConfigService configService;
+
+    @Scheduled(cron = "0 0 2 1 * ?")
+    public void generateMonthlyReport() {
+        log.info("[分销定时任务] 开始生成月度报表");
+        try {
+            String lastMonth = LocalDate.now().minusMonths(1).format(DateTimeFormatter.ofPattern("yyyy-MM"));
+            reportService.generateMonthlyReport(lastMonth);
+            log.info("[分销定时任务] 月度报表生成完成 - month: {}", lastMonth);
+        } catch (Exception e) {
+            log.error("[分销定时任务] 月度报表生成失败", e);
+        }
+    }
+
+    @Scheduled(cron = "0 0 3 * * ?")
+    public void expirePendingReferrals() {
+        log.info("[分销定时任务] 开始检查过期推荐关系");
+        try {
+            Integer windowDays = configService.getValidTimeWindowDays();
+            if (windowDays == null || windowDays <= 0) {
+                log.info("[分销定时任务] 未配置有效时间窗口,跳过过期检查");
+                return;
+            }
+            LocalDateTime expireThreshold = LocalDateTime.now().minusDays(windowDays);
+            LambdaQueryWrapper<DistributorReferral> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(DistributorReferral::getStatus, 0)
+                   .le(DistributorReferral::getCreateTime, expireThreshold)
+                   .isNotNull(DistributorReferral::getExpireTime)
+                   .le(DistributorReferral::getExpireTime, LocalDateTime.now());
+            List<DistributorReferral> expiredReferrals = referralService.list(wrapper);
+            for (DistributorReferral referral : expiredReferrals) {
+                referral.setStatus(2);
+                referralService.updateById(referral);
+            }
+            log.info("[分销定时任务] 过期推荐关系处理完成 - count: {}", expiredReferrals.size());
+        } catch (Exception e) {
+            log.error("[分销定时任务] 过期推荐关系处理失败", e);
+        }
+    }
+}

+ 3 - 37
haha-common/src/main/java/com/haha/common/vo/PageResult.java

@@ -1,45 +1,27 @@
 package com.haha.common.vo;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import lombok.Data;
 
 import java.io.Serializable;
 import java.util.List;
 
-/**
- * 分页响应结果
- * 统一的分页数据返回格式
- *
- * @param <T> 数据类型
- */
 @Data
 public class PageResult<T> implements Serializable {
     
     private static final long serialVersionUID = 1L;
     
-    /**
-     * 数据列表
-     */
     private List<T> list;
     
-    /**
-     * 总记录数
-     */
     private long total;
     
-    /**
-     * 每页条数
-     */
     private long pageSize;
     
-    /**
-     * 当前页码
-     */
     private long currentPage;
     
-    /**
-     * 总页数
-     */
     private long totalPages;
 
     public PageResult() {
@@ -53,13 +35,6 @@ public class PageResult<T> implements Serializable {
         this.totalPages = pageSize > 0 ? (total + pageSize - 1) / pageSize : 0;
     }
 
-    /**
-     * 从MyBatis-Plus的IPage转换
-     *
-     * @param page IPage对象
-     * @param <T>  数据类型
-     * @return PageResult
-     */
     public static <T> PageResult<T> of(IPage<T> page) {
         PageResult<T> result = new PageResult<>();
         result.setList(page.getRecords());
@@ -70,15 +45,6 @@ public class PageResult<T> implements Serializable {
         return result;
     }
 
-    /**
-     * 从MyBatis-Plus的IPage转换(带数据转换)
-     *
-     * @param page   IPage对象
-     * @param mapper 数据转换函数
-     * @param <T>    原始数据类型
-     * @param <R>    转换后数据类型
-     * @return PageResult
-     */
     public static <T, R> PageResult<R> of(IPage<T> page, java.util.function.Function<T, R> mapper) {
         PageResult<R> result = new PageResult<>();
         result.setList(page.getRecords().stream().map(mapper).toList());

+ 6 - 0
haha-entity/pom.xml

@@ -33,6 +33,12 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-annotations</artifactId>
         </dependency>
+
+        <!-- Jackson Databind -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 67 - 0
haha-entity/src/main/java/com/haha/entity/Distributor.java

@@ -0,0 +1,67 @@
+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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@TableName("t_distributor")
+public class Distributor implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    private String code;
+
+    private String name;
+
+    private String phone;
+
+    private Integer status;
+
+    @TableField("qrcode_content")
+    private String qrcodeContent;
+
+    private Integer totalReferrals;
+
+    private Integer validReferrals;
+
+    private BigDecimal commissionBalance;
+
+    private BigDecimal frozenAmount;
+
+    private BigDecimal totalCommission;
+
+    private BigDecimal totalWithdrawn;
+
+    private String remark;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    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;
+}

+ 63 - 0
haha-entity/src/main/java/com/haha/entity/DistributorCommission.java

@@ -0,0 +1,63 @@
+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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@TableName("t_distributor_commission")
+public class DistributorCommission implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long distributorId;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long referralId;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long userId;
+
+    private Integer commissionType;
+
+    private BigDecimal amount;
+
+    private BigDecimal orderAmount;
+
+    private BigDecimal rate;
+
+    private Integer status;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime settleTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @TableField(exist = false)
+    private String commissionTypeLabel;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String distributorName;
+
+    @TableField(exist = false)
+    private String userName;
+}

+ 34 - 0
haha-entity/src/main/java/com/haha/entity/DistributorConfig.java

@@ -0,0 +1,34 @@
+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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@TableName("t_distributor_config")
+public class DistributorConfig implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    private String configKey;
+
+    private String configValue;
+
+    private String configDesc;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+}

+ 54 - 0
haha-entity/src/main/java/com/haha/entity/DistributorMonthlyReport.java

@@ -0,0 +1,54 @@
+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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@TableName("t_distributor_monthly_report")
+public class DistributorMonthlyReport implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long distributorId;
+
+    private String yearMonth;
+
+    private Integer totalReferrals;
+
+    private Integer validReferrals;
+
+    private Integer pendingReferrals;
+
+    private Integer expiredReferrals;
+
+    private BigDecimal totalCommission;
+
+    private BigDecimal settledCommission;
+
+    private BigDecimal pendingCommission;
+
+    @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 distributorName;
+}

+ 63 - 0
haha-entity/src/main/java/com/haha/entity/DistributorReferral.java

@@ -0,0 +1,63 @@
+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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@TableName("t_distributor_referral")
+public class DistributorReferral implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long distributorId;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long userId;
+
+    private Integer status;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long firstOrderId;
+
+    private BigDecimal firstOrderAmount;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime firstOrderTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime effectiveTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime expireTime;
+
+    @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 distributorName;
+
+    @TableField(exist = false)
+    private String userName;
+}

+ 58 - 0
haha-entity/src/main/java/com/haha/entity/DistributorWithdrawal.java

@@ -0,0 +1,58 @@
+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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@TableName("t_distributor_withdrawal")
+public class DistributorWithdrawal implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long distributorId;
+
+    private BigDecimal amount;
+
+    private Integer status;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long reviewerId;
+
+    private String reviewerName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime reviewTime;
+
+    private String reviewRemark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime transferTime;
+
+    @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 distributorName;
+}

+ 21 - 0
haha-entity/src/main/java/com/haha/entity/dto/CommissionQueryDTO.java

@@ -0,0 +1,21 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CommissionQueryDTO extends PageQueryDTO {
+
+    private Long distributorId;
+
+    private Long userId;
+
+    private Integer commissionType;
+
+    private Integer status;
+
+    private String startDate;
+
+    private String endDate;
+}

+ 14 - 0
haha-entity/src/main/java/com/haha/entity/dto/DistributorConfigDTO.java

@@ -0,0 +1,14 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DistributorConfigDTO {
+
+    @NotBlank(message = "配置键不能为空")
+    private String configKey;
+
+    @NotBlank(message = "配置值不能为空")
+    private String configValue;
+}

+ 16 - 0
haha-entity/src/main/java/com/haha/entity/dto/DistributorCreateDTO.java

@@ -0,0 +1,16 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DistributorCreateDTO {
+
+    @NotBlank(message = "分销商名称不能为空")
+    private String name;
+
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+
+    private String remark;
+}

+ 17 - 0
haha-entity/src/main/java/com/haha/entity/dto/DistributorQueryDTO.java

@@ -0,0 +1,17 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class DistributorQueryDTO extends PageQueryDTO {
+
+    private String code;
+
+    private String name;
+
+    private String phone;
+
+    private Integer status;
+}

+ 17 - 0
haha-entity/src/main/java/com/haha/entity/dto/DistributorUpdateDTO.java

@@ -0,0 +1,17 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class DistributorUpdateDTO {
+
+    @NotNull(message = "ID不能为空")
+    private Long id;
+
+    private String name;
+
+    private String phone;
+
+    private String remark;
+}

+ 13 - 0
haha-entity/src/main/java/com/haha/entity/dto/MonthlyReportQueryDTO.java

@@ -0,0 +1,13 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MonthlyReportQueryDTO extends PageQueryDTO {
+
+    private String yearMonth;
+
+    private Long distributorId;
+}

+ 19 - 0
haha-entity/src/main/java/com/haha/entity/dto/ReferralQueryDTO.java

@@ -0,0 +1,19 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ReferralQueryDTO extends PageQueryDTO {
+
+    private Long distributorId;
+
+    private Long userId;
+
+    private Integer status;
+
+    private String startDate;
+
+    private String endDate;
+}

+ 15 - 0
haha-entity/src/main/java/com/haha/entity/dto/WithdrawalApplyDTO.java

@@ -0,0 +1,15 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class WithdrawalApplyDTO {
+
+    @NotNull(message = "提现金额不能为空")
+    @DecimalMin(value = "0.01", message = "提现金额不能小于0.01")
+    private BigDecimal amount;
+}

+ 17 - 0
haha-entity/src/main/java/com/haha/entity/dto/WithdrawalQueryDTO.java

@@ -0,0 +1,17 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WithdrawalQueryDTO extends PageQueryDTO {
+
+    private Long distributorId;
+
+    private Integer status;
+
+    private String startDate;
+
+    private String endDate;
+}

+ 16 - 0
haha-entity/src/main/java/com/haha/entity/dto/WithdrawalReviewDTO.java

@@ -0,0 +1,16 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class WithdrawalReviewDTO {
+
+    @NotNull(message = "ID不能为空")
+    private Long id;
+
+    @NotNull(message = "审核状态不能为空")
+    private Integer status;
+
+    private String reviewRemark;
+}

+ 44 - 0
haha-entity/src/main/java/com/haha/entity/vo/CommissionVO.java

@@ -0,0 +1,44 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CommissionVO {
+
+    private Long id;
+
+    private Long distributorId;
+
+    private String distributorName;
+
+    private Long userId;
+
+    private String userName;
+
+    private Integer commissionType;
+
+    private String commissionTypeLabel;
+
+    private BigDecimal amount;
+
+    private BigDecimal orderAmount;
+
+    private BigDecimal rate;
+
+    private Integer status;
+
+    private String statusLabel;
+
+    private LocalDateTime settleTime;
+
+    private LocalDateTime createTime;
+}

+ 25 - 0
haha-entity/src/main/java/com/haha/entity/vo/DistributorConfigVO.java

@@ -0,0 +1,25 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DistributorConfigVO {
+
+    private Long id;
+
+    private String configKey;
+
+    private String configValue;
+
+    private String configDesc;
+
+    private LocalDateTime updateTime;
+}

+ 32 - 0
haha-entity/src/main/java/com/haha/entity/vo/DistributorDashboardVO.java

@@ -0,0 +1,32 @@
+package com.haha.entity.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DistributorDashboardVO {
+
+    private Long totalDistributors;
+
+    private Long activeDistributors;
+
+    private Long totalReferrals;
+
+    private Long validReferrals;
+
+    private BigDecimal totalCommission;
+
+    private BigDecimal monthlyCommission;
+
+    private List<MonthlyTrendVO> trends;
+}

+ 50 - 0
haha-entity/src/main/java/com/haha/entity/vo/DistributorDetailVO.java

@@ -0,0 +1,50 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DistributorDetailVO {
+
+    private Long id;
+
+    private String code;
+
+    private String name;
+
+    private String phone;
+
+    private Integer status;
+
+    private String statusLabel;
+
+    private String qrcodeUrl;
+
+    private Integer totalReferrals;
+
+    private Integer validReferrals;
+
+    private BigDecimal commissionBalance;
+
+    private BigDecimal frozenAmount;
+
+    private BigDecimal totalCommission;
+
+    private BigDecimal totalWithdrawn;
+
+    private String remark;
+
+    private String creatorName;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 27 - 0
haha-entity/src/main/java/com/haha/entity/vo/DistributorMpStatsVO.java

@@ -0,0 +1,27 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DistributorMpStatsVO {
+
+    private Long totalReferrals;
+
+    private Long validReferrals;
+
+    private BigDecimal commissionBalance;
+
+    private BigDecimal frozenAmount;
+
+    private BigDecimal totalCommission;
+
+    private BigDecimal totalWithdrawn;
+}

+ 48 - 0
haha-entity/src/main/java/com/haha/entity/vo/DistributorVO.java

@@ -0,0 +1,48 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DistributorVO {
+
+    private Long id;
+
+    private String code;
+
+    private String name;
+
+    private String phone;
+
+    private Integer status;
+
+    private String statusLabel;
+
+    private String qrcodeUrl;
+
+    private Integer totalReferrals;
+
+    private Integer validReferrals;
+
+    private BigDecimal commissionBalance;
+
+    private BigDecimal frozenAmount;
+
+    private BigDecimal totalCommission;
+
+    private BigDecimal totalWithdrawn;
+
+    private String remark;
+
+    private String creatorName;
+
+    private LocalDateTime createTime;
+}

+ 40 - 0
haha-entity/src/main/java/com/haha/entity/vo/MonthlyReportVO.java

@@ -0,0 +1,40 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MonthlyReportVO {
+
+    private Long id;
+
+    private Long distributorId;
+
+    private String distributorName;
+
+    private String yearMonth;
+
+    private Integer totalReferrals;
+
+    private Integer validReferrals;
+
+    private Integer pendingReferrals;
+
+    private Integer expiredReferrals;
+
+    private BigDecimal totalCommission;
+
+    private BigDecimal settledCommission;
+
+    private BigDecimal pendingCommission;
+
+    private LocalDateTime createTime;
+}

+ 23 - 0
haha-entity/src/main/java/com/haha/entity/vo/MonthlyTrendVO.java

@@ -0,0 +1,23 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MonthlyTrendVO {
+
+    private String yearMonth;
+
+    private Long referralCount;
+
+    private Long validCount;
+
+    private BigDecimal commissionAmount;
+}

+ 42 - 0
haha-entity/src/main/java/com/haha/entity/vo/ReferralVO.java

@@ -0,0 +1,42 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReferralVO {
+
+    private Long id;
+
+    private Long distributorId;
+
+    private String distributorName;
+
+    private Long userId;
+
+    private String userName;
+
+    private Integer status;
+
+    private String statusLabel;
+
+    private Long firstOrderId;
+
+    private BigDecimal firstOrderAmount;
+
+    private LocalDateTime firstOrderTime;
+
+    private LocalDateTime effectiveTime;
+
+    private LocalDateTime expireTime;
+
+    private LocalDateTime createTime;
+}

+ 40 - 0
haha-entity/src/main/java/com/haha/entity/vo/WithdrawalVO.java

@@ -0,0 +1,40 @@
+package com.haha.entity.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WithdrawalVO {
+
+    private Long id;
+
+    private Long distributorId;
+
+    private String distributorName;
+
+    private BigDecimal amount;
+
+    private Integer status;
+
+    private String statusLabel;
+
+    private Long reviewerId;
+
+    private String reviewerName;
+
+    private LocalDateTime reviewTime;
+
+    private String reviewRemark;
+
+    private LocalDateTime transferTime;
+
+    private LocalDateTime createTime;
+}

+ 23 - 0
haha-mapper/src/main/java/com/haha/mapper/DistributorCommissionMapper.java

@@ -0,0 +1,23 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.DistributorCommission;
+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.math.BigDecimal;
+
+@Mapper
+public interface DistributorCommissionMapper extends BaseMapper<DistributorCommission> {
+
+    @Select("SELECT COALESCE(SUM(amount), 0) FROM t_distributor_commission WHERE distributor_id = #{distributorId} AND status = 0")
+    BigDecimal sumPendingCommission(@Param("distributorId") Long distributorId);
+
+    @Select("SELECT COALESCE(SUM(amount), 0) FROM t_distributor_commission WHERE distributor_id = #{distributorId} AND status = 0 AND DATE_FORMAT(create_time, '%Y-%m') = #{yearMonth}")
+    BigDecimal sumPendingCommissionByMonth(@Param("distributorId") Long distributorId, @Param("yearMonth") String yearMonth);
+
+    @Update("UPDATE t_distributor_commission SET status = 1, settle_time = NOW() WHERE distributor_id = #{distributorId} AND status = 0 AND DATE_FORMAT(create_time, '%Y-%m') = #{yearMonth}")
+    int settleByMonth(@Param("distributorId") Long distributorId, @Param("yearMonth") String yearMonth);
+}

+ 14 - 0
haha-mapper/src/main/java/com/haha/mapper/DistributorConfigMapper.java

@@ -0,0 +1,14 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.DistributorConfig;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface DistributorConfigMapper extends BaseMapper<DistributorConfig> {
+
+    @Select("SELECT * FROM t_distributor_config WHERE config_key = #{configKey}")
+    DistributorConfig selectByKey(@Param("configKey") String configKey);
+}

+ 38 - 0
haha-mapper/src/main/java/com/haha/mapper/DistributorMapper.java

@@ -0,0 +1,38 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.Distributor;
+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.math.BigDecimal;
+
+@Mapper
+public interface DistributorMapper extends BaseMapper<Distributor> {
+
+    @Select("SELECT MAX(code) FROM t_distributor WHERE deleted = 0")
+    String selectMaxCode();
+
+    @Select("SELECT COUNT(*) FROM t_distributor WHERE phone = #{phone} AND deleted = 0 AND id != #{excludeId}")
+    int countByPhone(@Param("phone") String phone, @Param("excludeId") Long excludeId);
+
+    @Update("UPDATE t_distributor SET total_referrals = total_referrals + 1, update_time = NOW() WHERE id = #{id} AND deleted = 0")
+    int incrementTotalReferrals(@Param("id") Long id);
+
+    @Update("UPDATE t_distributor SET valid_referrals = valid_referrals + 1, update_time = NOW() WHERE id = #{id} AND deleted = 0")
+    int incrementValidReferrals(@Param("id") Long id);
+
+    @Update("UPDATE t_distributor SET commission_balance = commission_balance + #{amount}, total_commission = total_commission + #{amount}, update_time = NOW() WHERE id = #{id} AND deleted = 0")
+    int addCommissionBalance(@Param("id") Long id, @Param("amount") BigDecimal amount);
+
+    @Update("UPDATE t_distributor SET commission_balance = commission_balance - #{amount}, frozen_amount = frozen_amount + #{amount}, update_time = NOW() WHERE id = #{id} AND deleted = 0")
+    int freezeAmount(@Param("id") Long id, @Param("amount") BigDecimal amount);
+
+    @Update("UPDATE t_distributor SET frozen_amount = frozen_amount - #{amount}, total_withdrawn = total_withdrawn + #{amount}, update_time = NOW() WHERE id = #{id} AND deleted = 0")
+    int deductWithdrawal(@Param("id") Long id, @Param("amount") BigDecimal amount);
+
+    @Update("UPDATE t_distributor SET commission_balance = commission_balance + #{amount}, frozen_amount = frozen_amount - #{amount}, update_time = NOW() WHERE id = #{id} AND deleted = 0")
+    int unfreezeAmount(@Param("id") Long id, @Param("amount") BigDecimal amount);
+}

+ 19 - 0
haha-mapper/src/main/java/com/haha/mapper/DistributorMonthlyReportMapper.java

@@ -0,0 +1,19 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.DistributorMonthlyReport;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface DistributorMonthlyReportMapper extends BaseMapper<DistributorMonthlyReport> {
+
+    @Select("SELECT * FROM t_distributor_monthly_report WHERE year_month = #{yearMonth} ORDER BY total_commission DESC")
+    List<DistributorMonthlyReport> selectByMonth(@Param("yearMonth") String yearMonth);
+
+    @Select("SELECT * FROM t_distributor_monthly_report WHERE distributor_id = #{distributorId} ORDER BY year_month DESC")
+    List<DistributorMonthlyReport> selectByDistributor(@Param("distributorId") Long distributorId);
+}

+ 20 - 0
haha-mapper/src/main/java/com/haha/mapper/DistributorReferralMapper.java

@@ -0,0 +1,20 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.DistributorReferral;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface DistributorReferralMapper extends BaseMapper<DistributorReferral> {
+
+    @Select("SELECT COUNT(*) FROM t_distributor_referral WHERE user_id = #{userId}")
+    int countByUserId(@Param("userId") Long userId);
+
+    @Select("SELECT * FROM t_distributor_referral WHERE user_id = #{userId} LIMIT 1")
+    DistributorReferral selectByUserId(@Param("userId") Long userId);
+
+    @Select("SELECT COUNT(*) FROM t_distributor_referral WHERE distributor_id = #{distributorId} AND user_id = #{userId}")
+    int countByDistributorAndUser(@Param("distributorId") Long distributorId, @Param("userId") Long userId);
+}

+ 9 - 0
haha-mapper/src/main/java/com/haha/mapper/DistributorWithdrawalMapper.java

@@ -0,0 +1,9 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.DistributorWithdrawal;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DistributorWithdrawalMapper extends BaseMapper<DistributorWithdrawal> {
+}

+ 17 - 0
haha-service/src/main/java/com/haha/service/DistributorCommissionService.java

@@ -0,0 +1,17 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.DistributorCommission;
+import com.haha.entity.dto.CommissionQueryDTO;
+
+import java.math.BigDecimal;
+
+public interface DistributorCommissionService extends IService<DistributorCommission> {
+
+    void processOrderCommission(Long userId, Long orderId, BigDecimal orderAmount);
+
+    BigDecimal settleCommissions(Long distributorId, String yearMonth);
+
+    IPage<DistributorCommission> getPage(CommissionQueryDTO queryDTO);
+}

+ 31 - 0
haha-service/src/main/java/com/haha/service/DistributorConfigService.java

@@ -0,0 +1,31 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.DistributorConfig;
+import com.haha.entity.dto.DistributorConfigDTO;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+public interface DistributorConfigService extends IService<DistributorConfig> {
+
+    String getConfigValue(String key);
+
+    String getConfigValue(String key, String defaultValue);
+
+    BigDecimal getFixedCommissionAmount();
+
+    BigDecimal getFirstOrderCommissionRate();
+
+    String getValidUserRule();
+
+    BigDecimal getMinValidOrderAmount();
+
+    Integer getValidTimeWindowDays();
+
+    BigDecimal getMinWithdrawalAmount();
+
+    List<DistributorConfig> getAllConfigs();
+
+    DistributorConfig updateConfig(DistributorConfigDTO dto);
+}

+ 8 - 0
haha-service/src/main/java/com/haha/service/DistributorQrcodeService.java

@@ -0,0 +1,8 @@
+package com.haha.service;
+
+public interface DistributorQrcodeService {
+
+    String generateQrcode(Long distributorId);
+
+    byte[] downloadQrcode(Long distributorId);
+}

+ 15 - 0
haha-service/src/main/java/com/haha/service/DistributorReferralService.java

@@ -0,0 +1,15 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.DistributorReferral;
+import com.haha.entity.dto.ReferralQueryDTO;
+
+public interface DistributorReferralService extends IService<DistributorReferral> {
+
+    DistributorReferral bindReferral(String distributorCode, Long userId);
+
+    IPage<DistributorReferral> getPage(ReferralQueryDTO queryDTO);
+
+    DistributorReferral getByUserId(Long userId);
+}

+ 22 - 0
haha-service/src/main/java/com/haha/service/DistributorReportService.java

@@ -0,0 +1,22 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.DistributorMonthlyReport;
+import com.haha.entity.dto.MonthlyReportQueryDTO;
+import com.haha.entity.vo.DistributorDashboardVO;
+
+import java.util.List;
+
+public interface DistributorReportService extends IService<DistributorMonthlyReport> {
+
+    void generateMonthlyReport(String yearMonth);
+
+    DistributorMonthlyReport generateDistributorMonthlyReport(Long distributorId, String yearMonth);
+
+    IPage<DistributorMonthlyReport> getPage(MonthlyReportQueryDTO queryDTO);
+
+    DistributorDashboardVO getDashboard();
+
+    List<DistributorMonthlyReport> exportMonthlyReport(String yearMonth);
+}

+ 25 - 0
haha-service/src/main/java/com/haha/service/DistributorService.java

@@ -0,0 +1,25 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.Distributor;
+import com.haha.entity.dto.DistributorCreateDTO;
+import com.haha.entity.dto.DistributorQueryDTO;
+import com.haha.entity.dto.DistributorUpdateDTO;
+
+public interface DistributorService extends IService<Distributor> {
+
+    Distributor create(DistributorCreateDTO dto, Long creatorId, String creatorName);
+
+    Distributor update(DistributorUpdateDTO dto);
+
+    boolean activate(Long id);
+
+    boolean deactivate(Long id);
+
+    IPage<Distributor> getPage(DistributorQueryDTO queryDTO);
+
+    Distributor getDetail(Long id);
+
+    boolean delete(Long id);
+}

+ 20 - 0
haha-service/src/main/java/com/haha/service/DistributorWithdrawalService.java

@@ -0,0 +1,20 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.DistributorWithdrawal;
+import com.haha.entity.dto.WithdrawalQueryDTO;
+import com.haha.entity.dto.WithdrawalReviewDTO;
+
+import java.math.BigDecimal;
+
+public interface DistributorWithdrawalService extends IService<DistributorWithdrawal> {
+
+    DistributorWithdrawal applyWithdrawal(Long distributorId, BigDecimal amount);
+
+    DistributorWithdrawal reviewWithdrawal(WithdrawalReviewDTO dto, Long reviewerId, String reviewerName);
+
+    DistributorWithdrawal confirmTransfer(Long withdrawalId);
+
+    IPage<DistributorWithdrawal> getPage(WithdrawalQueryDTO queryDTO);
+}

+ 209 - 0
haha-service/src/main/java/com/haha/service/impl/DistributorCommissionServiceImpl.java

@@ -0,0 +1,209 @@
+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.vo.StatusLabel;
+import com.haha.entity.Distributor;
+import com.haha.entity.DistributorCommission;
+import com.haha.entity.DistributorReferral;
+import com.haha.entity.User;
+import com.haha.entity.dto.CommissionQueryDTO;
+import com.haha.mapper.DistributorCommissionMapper;
+import com.haha.mapper.DistributorMapper;
+import com.haha.mapper.UserMapper;
+import com.haha.service.DistributorCommissionService;
+import com.haha.service.DistributorConfigService;
+import com.haha.service.DistributorReferralService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DistributorCommissionServiceImpl extends ServiceImpl<DistributorCommissionMapper, DistributorCommission> implements DistributorCommissionService {
+
+    private final DistributorCommissionMapper commissionMapper;
+    private final DistributorReferralService referralService;
+    private final DistributorConfigService configService;
+    private final DistributorMapper distributorMapper;
+    private final UserMapper userMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void processOrderCommission(Long userId, Long orderId, BigDecimal orderAmount) {
+        DistributorReferral referral = referralService.getByUserId(userId);
+        if (referral == null) {
+            return;
+        }
+        if (referral.getStatus() != 0) {
+            return;
+        }
+        LocalDateTime now = LocalDateTime.now();
+        if (referral.getExpireTime() != null && now.isAfter(referral.getExpireTime())) {
+            referral.setStatus(2);
+            referral.setUpdateTime(now);
+            referralService.updateById(referral);
+            return;
+        }
+        String validUserRule = configService.getValidUserRule();
+        boolean isValidUser = false;
+        switch (validUserRule) {
+            case "FIRST_ORDER":
+                isValidUser = true;
+                break;
+            case "MIN_AMOUNT":
+                BigDecimal minAmount = configService.getMinValidOrderAmount();
+                isValidUser = orderAmount.compareTo(minAmount) >= 0;
+                break;
+            case "BOTH":
+                BigDecimal minAmountBoth = configService.getMinValidOrderAmount();
+                isValidUser = orderAmount.compareTo(minAmountBoth) >= 0;
+                break;
+            default:
+                isValidUser = true;
+        }
+        if (isValidUser) {
+            referral.setStatus(1);
+            referral.setFirstOrderId(orderId);
+            referral.setFirstOrderAmount(orderAmount);
+            referral.setFirstOrderTime(now);
+            referral.setEffectiveTime(now);
+            referral.setUpdateTime(now);
+            referralService.updateById(referral);
+            distributorMapper.incrementValidReferrals(referral.getDistributorId());
+            calculateCommission(referral, orderAmount);
+        } else {
+            referral.setFirstOrderId(orderId);
+            referral.setFirstOrderAmount(orderAmount);
+            referral.setFirstOrderTime(now);
+            referral.setUpdateTime(now);
+            referralService.updateById(referral);
+        }
+    }
+
+    private void calculateCommission(DistributorReferral referral, BigDecimal orderAmount) {
+        List<DistributorCommission> commissions = new ArrayList<>();
+        BigDecimal fixedCommissionAmount = configService.getFixedCommissionAmount();
+        if (fixedCommissionAmount.compareTo(BigDecimal.ZERO) > 0) {
+            DistributorCommission fixedCommission = new DistributorCommission();
+            fixedCommission.setDistributorId(referral.getDistributorId());
+            fixedCommission.setReferralId(referral.getId());
+            fixedCommission.setUserId(referral.getUserId());
+            fixedCommission.setCommissionType(0);
+            fixedCommission.setAmount(fixedCommissionAmount);
+            fixedCommission.setOrderAmount(null);
+            fixedCommission.setRate(null);
+            fixedCommission.setStatus(0);
+            fixedCommission.setCreateTime(LocalDateTime.now());
+            commissions.add(fixedCommission);
+        }
+        BigDecimal firstOrderCommissionRate = configService.getFirstOrderCommissionRate();
+        if (firstOrderCommissionRate.compareTo(BigDecimal.ZERO) > 0) {
+            BigDecimal commissionAmount = orderAmount.multiply(firstOrderCommissionRate)
+                    .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
+            if (commissionAmount.compareTo(new BigDecimal("0.01")) < 0) {
+                commissionAmount = new BigDecimal("0.01");
+            }
+            DistributorCommission rateCommission = new DistributorCommission();
+            rateCommission.setDistributorId(referral.getDistributorId());
+            rateCommission.setReferralId(referral.getId());
+            rateCommission.setUserId(referral.getUserId());
+            rateCommission.setCommissionType(1);
+            rateCommission.setAmount(commissionAmount);
+            rateCommission.setOrderAmount(orderAmount);
+            rateCommission.setRate(firstOrderCommissionRate);
+            rateCommission.setStatus(0);
+            rateCommission.setCreateTime(LocalDateTime.now());
+            commissions.add(rateCommission);
+        }
+        if (!commissions.isEmpty()) {
+            this.saveBatch(commissions);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public BigDecimal settleCommissions(Long distributorId, String yearMonth) {
+        BigDecimal sum = commissionMapper.sumPendingCommissionByMonth(distributorId, yearMonth);
+        if (sum.compareTo(BigDecimal.ZERO) > 0) {
+            commissionMapper.settleByMonth(distributorId, yearMonth);
+            distributorMapper.addCommissionBalance(distributorId, sum);
+        }
+        return sum;
+    }
+
+    @Override
+    public IPage<DistributorCommission> getPage(CommissionQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<DistributorCommission> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<DistributorCommission> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(queryDTO.getDistributorId() != null, DistributorCommission::getDistributorId, queryDTO.getDistributorId());
+        wrapper.eq(queryDTO.getUserId() != null, DistributorCommission::getUserId, queryDTO.getUserId());
+        wrapper.eq(queryDTO.getCommissionType() != null, DistributorCommission::getCommissionType, queryDTO.getCommissionType());
+        wrapper.eq(queryDTO.getStatus() != null, DistributorCommission::getStatus, queryDTO.getStatus());
+        if (queryDTO.getStartDate() != null) {
+            wrapper.ge(DistributorCommission::getCreateTime, queryDTO.getStartDate());
+        }
+        if (queryDTO.getEndDate() != null) {
+            wrapper.le(DistributorCommission::getCreateTime, queryDTO.getEndDate() + " 23:59:59");
+        }
+        wrapper.orderByDesc(DistributorCommission::getCreateTime);
+
+        IPage<DistributorCommission> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillLabels);
+
+        return result;
+    }
+
+    private void fillLabels(DistributorCommission commission) {
+        if (commission == null) {
+            return;
+        }
+        if (commission.getCommissionType() != null) {
+            switch (commission.getCommissionType()) {
+                case 0:
+                    commission.setCommissionTypeLabel("拉新奖励");
+                    break;
+                case 1:
+                    commission.setCommissionTypeLabel("消费提成");
+                    break;
+                default:
+                    commission.setCommissionTypeLabel("未知");
+            }
+        }
+        if (commission.getStatus() != null) {
+            StatusLabel statusLabel = switch (commission.getStatus()) {
+                case 0 -> StatusLabel.warning("待结算");
+                case 1 -> StatusLabel.success("已结算");
+                case 2 -> StatusLabel.info("已冻结");
+                default -> StatusLabel.info("未知");
+            };
+            commission.setStatusLabel(statusLabel.getLabel());
+        }
+        if (commission.getDistributorId() != null) {
+            Distributor distributor = distributorMapper.selectById(commission.getDistributorId());
+            if (distributor != null) {
+                commission.setDistributorName(distributor.getName());
+            }
+        }
+        if (commission.getUserId() != null) {
+            User user = userMapper.selectById(commission.getUserId());
+            if (user != null) {
+                commission.setUserName(user.getNickname());
+            } else {
+                commission.setUserName("未知用户");
+            }
+        }
+    }
+}

+ 89 - 0
haha-service/src/main/java/com/haha/service/impl/DistributorConfigServiceImpl.java

@@ -0,0 +1,89 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.DistributorConfig;
+import com.haha.entity.dto.DistributorConfigDTO;
+import com.haha.mapper.DistributorConfigMapper;
+import com.haha.service.DistributorConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DistributorConfigServiceImpl extends ServiceImpl<DistributorConfigMapper, DistributorConfig> implements DistributorConfigService {
+
+    private final DistributorConfigMapper configMapper;
+
+    @Override
+    public String getConfigValue(String key) {
+        DistributorConfig config = configMapper.selectByKey(key);
+        return config != null ? config.getConfigValue() : null;
+    }
+
+    @Override
+    public String getConfigValue(String key, String defaultValue) {
+        String value = getConfigValue(key);
+        return value != null ? value : defaultValue;
+    }
+
+    @Override
+    public BigDecimal getFixedCommissionAmount() {
+        String value = getConfigValue("fixed_commission_amount", "20.00");
+        return new BigDecimal(value);
+    }
+
+    @Override
+    public BigDecimal getFirstOrderCommissionRate() {
+        String value = getConfigValue("first_order_commission_rate", "5.00");
+        return new BigDecimal(value);
+    }
+
+    @Override
+    public String getValidUserRule() {
+        return getConfigValue("valid_user_rule", "FIRST_ORDER");
+    }
+
+    @Override
+    public BigDecimal getMinValidOrderAmount() {
+        String value = getConfigValue("min_valid_order_amount", "0.00");
+        return new BigDecimal(value);
+    }
+
+    @Override
+    public Integer getValidTimeWindowDays() {
+        String value = getConfigValue("valid_time_window_days", "0");
+        return Integer.parseInt(value);
+    }
+
+    @Override
+    public BigDecimal getMinWithdrawalAmount() {
+        String value = getConfigValue("min_withdrawal_amount", "100.00");
+        return new BigDecimal(value);
+    }
+
+    @Override
+    public List<DistributorConfig> getAllConfigs() {
+        return this.list();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DistributorConfig updateConfig(DistributorConfigDTO dto) {
+        DistributorConfig config = configMapper.selectByKey(dto.getConfigKey());
+        if (config == null) {
+            throw new BusinessException(404, "配置项不存在: " + dto.getConfigKey());
+        }
+        config.setConfigValue(dto.getConfigValue());
+        this.updateById(config);
+
+        log.info("更新分销配置: key={}, value={}", dto.getConfigKey(), dto.getConfigValue());
+        return config;
+    }
+}

+ 75 - 0
haha-service/src/main/java/com/haha/service/impl/DistributorQrcodeServiceImpl.java

@@ -0,0 +1,75 @@
+package com.haha.service.impl;
+
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.Distributor;
+import com.haha.mapper.DistributorMapper;
+import com.haha.service.DistributorQrcodeService;
+import com.haha.service.DistributorService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DistributorQrcodeServiceImpl implements DistributorQrcodeService {
+
+    private final DistributorMapper distributorMapper;
+    private final DistributorService distributorService;
+
+    @Value("${distributor.qrcode.appid:}")
+    private String appid;
+
+    @Value("${distributor.qrcode.page-path:pages/index/index}")
+    private String pagePath;
+
+    @Value("${distributor.qrcode.param-name:distributorCode}")
+    private String paramName;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String generateQrcode(Long distributorId) {
+        Distributor distributor = distributorService.getById(distributorId);
+        if (distributor == null || distributor.getDeleted() == 1) {
+            throw new BusinessException(404, "分销员不存在");
+        }
+
+        if (distributor.getQrcodeContent() != null) {
+            return distributor.getQrcodeContent();
+        }
+
+        String qrcodeContent = buildQrcodeContent(distributor.getCode());
+        log.info("[分销二维码] 生成二维码内容 - distributorId: {}, code: {}, content: {}", 
+                distributorId, distributor.getCode(), qrcodeContent);
+
+        distributor.setQrcodeContent(qrcodeContent);
+        distributorService.updateById(distributor);
+
+        return qrcodeContent;
+    }
+
+    @Override
+    public byte[] downloadQrcode(Long distributorId) {
+        throw new BusinessException(400, "二维码由前端动态生成,无需下载");
+    }
+
+    private String buildQrcodeContent(String distributorCode) {
+        if (appid == null || appid.isEmpty()) {
+            log.warn("[分销二维码] 未配置微信小程序appid,使用默认格式");
+            return "DISTRIBUTOR:" + distributorCode;
+        }
+
+        String query = paramName + "=" + distributorCode;
+        String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
+        
+        return String.format("weixin://dl/business/?appid=%s&path=%s&query=%s",
+                appid,
+                URLEncoder.encode(pagePath, StandardCharsets.UTF_8),
+                encodedQuery);
+    }
+}

+ 148 - 0
haha-service/src/main/java/com/haha/service/impl/DistributorReferralServiceImpl.java

@@ -0,0 +1,148 @@
+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.exception.BusinessException;
+import com.haha.entity.Distributor;
+import com.haha.entity.DistributorReferral;
+import com.haha.entity.User;
+import com.haha.entity.dto.ReferralQueryDTO;
+import com.haha.mapper.DistributorMapper;
+import com.haha.mapper.DistributorReferralMapper;
+import com.haha.mapper.UserMapper;
+import com.haha.service.DistributorConfigService;
+import com.haha.service.DistributorReferralService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DistributorReferralServiceImpl extends ServiceImpl<DistributorReferralMapper, DistributorReferral> implements DistributorReferralService {
+
+    private final DistributorReferralMapper referralMapper;
+    private final DistributorMapper distributorMapper;
+    private final DistributorConfigService configService;
+    private final UserMapper userMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DistributorReferral bindReferral(String distributorCode, Long userId) {
+        if (!StringUtils.hasText(distributorCode)) {
+            throw new BusinessException(400, "分销员编码不能为空");
+        }
+
+        LambdaQueryWrapper<Distributor> distributorWrapper = new LambdaQueryWrapper<>();
+        distributorWrapper.eq(Distributor::getCode, distributorCode);
+        distributorWrapper.eq(Distributor::getStatus, 1);
+        distributorWrapper.eq(Distributor::getDeleted, 0);
+        Distributor distributor = distributorMapper.selectOne(distributorWrapper);
+        if (distributor == null) {
+            throw new BusinessException(404, "分销员不存在或未激活");
+        }
+
+        if (distributor.getId().equals(userId)) {
+            throw new BusinessException(400, "不能推荐自己");
+        }
+
+        int userReferralCount = referralMapper.countByUserId(userId);
+        if (userReferralCount > 0) {
+            throw new BusinessException(400, "该用户已有分销推荐关系");
+        }
+
+        int duplicateCount = referralMapper.countByDistributorAndUser(distributor.getId(), userId);
+        if (duplicateCount > 0) {
+            log.info("推荐关系已存在,跳过绑定: distributorId={}, userId={}", distributor.getId(), userId);
+            return referralMapper.selectByUserId(userId);
+        }
+
+        DistributorReferral referral = new DistributorReferral();
+        referral.setDistributorId(distributor.getId());
+        referral.setUserId(userId);
+        referral.setStatus(0);
+
+        Integer validTimeWindowDays = configService.getValidTimeWindowDays();
+        if (validTimeWindowDays != null && validTimeWindowDays > 0) {
+            referral.setExpireTime(LocalDateTime.now().plusDays(validTimeWindowDays));
+        }
+
+        referralMapper.insert(referral);
+        distributorMapper.incrementTotalReferrals(distributor.getId());
+
+        log.info("绑定分销推荐关系成功: distributorId={}, userId={}", distributor.getId(), userId);
+        return referral;
+    }
+
+    @Override
+    public IPage<DistributorReferral> getPage(ReferralQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<DistributorReferral> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<DistributorReferral> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(queryDTO.getDistributorId() != null, DistributorReferral::getDistributorId, queryDTO.getDistributorId());
+        wrapper.eq(queryDTO.getUserId() != null, DistributorReferral::getUserId, queryDTO.getUserId());
+        wrapper.eq(queryDTO.getStatus() != null, DistributorReferral::getStatus, queryDTO.getStatus());
+        if (queryDTO.getStartDate() != null) {
+            wrapper.ge(DistributorReferral::getCreateTime, queryDTO.getStartDate());
+        }
+        if (queryDTO.getEndDate() != null) {
+            wrapper.le(DistributorReferral::getCreateTime, queryDTO.getEndDate() + " 23:59:59");
+        }
+        wrapper.orderByDesc(DistributorReferral::getCreateTime);
+
+        IPage<DistributorReferral> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillLabels);
+
+        return result;
+    }
+
+    @Override
+    public DistributorReferral getByUserId(Long userId) {
+        return referralMapper.selectByUserId(userId);
+    }
+
+    private void fillLabels(DistributorReferral referral) {
+        if (referral == null) {
+            return;
+        }
+
+        if (referral.getStatus() != null) {
+            switch (referral.getStatus()) {
+                case 0:
+                    referral.setStatusLabel("待生效");
+                    break;
+                case 1:
+                    referral.setStatusLabel("已生效");
+                    break;
+                case 2:
+                    referral.setStatusLabel("已失效");
+                    break;
+                default:
+                    referral.setStatusLabel("未知");
+            }
+        }
+
+        if (referral.getDistributorId() != null) {
+            Distributor distributor = distributorMapper.selectById(referral.getDistributorId());
+            if (distributor != null) {
+                referral.setDistributorName(distributor.getName());
+            }
+        }
+
+        if (referral.getUserId() != null) {
+            User user = userMapper.selectById(referral.getUserId());
+            if (user != null) {
+                referral.setUserName(user.getNickname());
+            } else {
+                referral.setUserName("未知用户");
+            }
+        }
+    }
+}

+ 255 - 0
haha-service/src/main/java/com/haha/service/impl/DistributorReportServiceImpl.java

@@ -0,0 +1,255 @@
+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.exception.BusinessException;
+import com.haha.entity.Distributor;
+import com.haha.entity.DistributorCommission;
+import com.haha.entity.DistributorMonthlyReport;
+import com.haha.entity.DistributorReferral;
+import com.haha.entity.dto.MonthlyReportQueryDTO;
+import com.haha.entity.vo.DistributorDashboardVO;
+import com.haha.entity.vo.MonthlyTrendVO;
+import com.haha.mapper.DistributorCommissionMapper;
+import com.haha.mapper.DistributorMapper;
+import com.haha.mapper.DistributorMonthlyReportMapper;
+import com.haha.mapper.DistributorReferralMapper;
+import com.haha.service.DistributorCommissionService;
+import com.haha.service.DistributorReportService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DistributorReportServiceImpl extends ServiceImpl<DistributorMonthlyReportMapper, DistributorMonthlyReport> implements DistributorReportService {
+
+    private final DistributorMonthlyReportMapper reportMapper;
+    private final DistributorMapper distributorMapper;
+    private final DistributorReferralMapper referralMapper;
+    private final DistributorCommissionMapper commissionMapper;
+    private final DistributorCommissionService commissionService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void generateMonthlyReport(String yearMonth) {
+        LambdaQueryWrapper<Distributor> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Distributor::getStatus, 1);
+        wrapper.eq(Distributor::getDeleted, 0);
+        List<Distributor> distributors = distributorMapper.selectList(wrapper);
+
+        for (Distributor distributor : distributors) {
+            generateDistributorMonthlyReport(distributor.getId(), yearMonth);
+            commissionService.settleCommissions(distributor.getId(), yearMonth);
+        }
+
+        log.info("[月度报表] 生成完成 - yearMonth: {}, 处理分销员数量: {}", yearMonth, distributors.size());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DistributorMonthlyReport generateDistributorMonthlyReport(Long distributorId, String yearMonth) {
+        LambdaQueryWrapper<DistributorMonthlyReport> existWrapper = new LambdaQueryWrapper<>();
+        existWrapper.eq(DistributorMonthlyReport::getDistributorId, distributorId);
+        existWrapper.eq(DistributorMonthlyReport::getYearMonth, yearMonth);
+        DistributorMonthlyReport report = reportMapper.selectOne(existWrapper);
+
+        if (report == null) {
+            report = new DistributorMonthlyReport();
+            report.setDistributorId(distributorId);
+            report.setYearMonth(yearMonth);
+            report.setCreateTime(LocalDateTime.now());
+        }
+
+        LambdaQueryWrapper<DistributorReferral> totalReferralWrapper = new LambdaQueryWrapper<>();
+        totalReferralWrapper.eq(DistributorReferral::getDistributorId, distributorId);
+        totalReferralWrapper.apply("DATE_FORMAT(create_time, '%Y-%m') = {0}", yearMonth);
+        Long totalReferrals = referralMapper.selectCount(totalReferralWrapper);
+
+        LambdaQueryWrapper<DistributorReferral> validReferralWrapper = new LambdaQueryWrapper<>();
+        validReferralWrapper.eq(DistributorReferral::getDistributorId, distributorId);
+        validReferralWrapper.eq(DistributorReferral::getStatus, 1);
+        validReferralWrapper.apply("DATE_FORMAT(effective_time, '%Y-%m') = {0}", yearMonth);
+        Long validReferrals = referralMapper.selectCount(validReferralWrapper);
+
+        LambdaQueryWrapper<DistributorReferral> pendingReferralWrapper = new LambdaQueryWrapper<>();
+        pendingReferralWrapper.eq(DistributorReferral::getDistributorId, distributorId);
+        pendingReferralWrapper.eq(DistributorReferral::getStatus, 0);
+        pendingReferralWrapper.apply("DATE_FORMAT(create_time, '%Y-%m') = {0}", yearMonth);
+        Long pendingReferrals = referralMapper.selectCount(pendingReferralWrapper);
+
+        LambdaQueryWrapper<DistributorReferral> expiredReferralWrapper = new LambdaQueryWrapper<>();
+        expiredReferralWrapper.eq(DistributorReferral::getDistributorId, distributorId);
+        expiredReferralWrapper.eq(DistributorReferral::getStatus, 2);
+        expiredReferralWrapper.apply("DATE_FORMAT(update_time, '%Y-%m') = {0}", yearMonth);
+        Long expiredReferrals = referralMapper.selectCount(expiredReferralWrapper);
+
+        LambdaQueryWrapper<DistributorCommission> totalCommissionWrapper = new LambdaQueryWrapper<>();
+        totalCommissionWrapper.eq(DistributorCommission::getDistributorId, distributorId);
+        totalCommissionWrapper.apply("DATE_FORMAT(create_time, '%Y-%m') = {0}", yearMonth);
+        List<DistributorCommission> totalCommissions = commissionMapper.selectList(totalCommissionWrapper);
+        BigDecimal totalCommission = totalCommissions.stream()
+                .map(DistributorCommission::getAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        LambdaQueryWrapper<DistributorCommission> settledCommissionWrapper = new LambdaQueryWrapper<>();
+        settledCommissionWrapper.eq(DistributorCommission::getDistributorId, distributorId);
+        settledCommissionWrapper.eq(DistributorCommission::getStatus, 1);
+        settledCommissionWrapper.apply("DATE_FORMAT(settle_time, '%Y-%m') = {0}", yearMonth);
+        List<DistributorCommission> settledCommissions = commissionMapper.selectList(settledCommissionWrapper);
+        BigDecimal settledCommission = settledCommissions.stream()
+                .map(DistributorCommission::getAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        LambdaQueryWrapper<DistributorCommission> pendingCommissionWrapper = new LambdaQueryWrapper<>();
+        pendingCommissionWrapper.eq(DistributorCommission::getDistributorId, distributorId);
+        pendingCommissionWrapper.eq(DistributorCommission::getStatus, 0);
+        pendingCommissionWrapper.apply("DATE_FORMAT(create_time, '%Y-%m') = {0}", yearMonth);
+        List<DistributorCommission> pendingCommissions = commissionMapper.selectList(pendingCommissionWrapper);
+        BigDecimal pendingCommission = pendingCommissions.stream()
+                .map(DistributorCommission::getAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        report.setTotalReferrals(totalReferrals.intValue());
+        report.setValidReferrals(validReferrals.intValue());
+        report.setPendingReferrals(pendingReferrals.intValue());
+        report.setExpiredReferrals(expiredReferrals.intValue());
+        report.setTotalCommission(totalCommission);
+        report.setSettledCommission(settledCommission);
+        report.setPendingCommission(pendingCommission);
+        report.setUpdateTime(LocalDateTime.now());
+
+        if (report.getId() == null) {
+            reportMapper.insert(report);
+        } else {
+            reportMapper.updateById(report);
+        }
+
+        log.info("[月度报表] 生成完成 - distributorId: {}, yearMonth: {}", distributorId, yearMonth);
+        return report;
+    }
+
+    @Override
+    public IPage<DistributorMonthlyReport> getPage(MonthlyReportQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<DistributorMonthlyReport> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<DistributorMonthlyReport> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(queryDTO.getYearMonth() != null, DistributorMonthlyReport::getYearMonth, queryDTO.getYearMonth());
+        wrapper.eq(queryDTO.getDistributorId() != null, DistributorMonthlyReport::getDistributorId, queryDTO.getDistributorId());
+        wrapper.orderByDesc(DistributorMonthlyReport::getYearMonth);
+
+        IPage<DistributorMonthlyReport> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillDistributorName);
+
+        return result;
+    }
+
+    @Override
+    public DistributorDashboardVO getDashboard() {
+        Long totalDistributors = distributorMapper.selectCount(
+                new LambdaQueryWrapper<Distributor>().eq(Distributor::getDeleted, 0));
+
+        Long activeDistributors = distributorMapper.selectCount(
+                new LambdaQueryWrapper<Distributor>().eq(Distributor::getStatus, 1).eq(Distributor::getDeleted, 0));
+
+        List<Distributor> allDistributors = distributorMapper.selectList(
+                new LambdaQueryWrapper<Distributor>().eq(Distributor::getDeleted, 0));
+
+        Long totalReferrals = allDistributors.stream()
+                .mapToLong(d -> d.getTotalReferrals() != null ? d.getTotalReferrals() : 0)
+                .sum();
+
+        Long validReferrals = allDistributors.stream()
+                .mapToLong(d -> d.getValidReferrals() != null ? d.getValidReferrals() : 0)
+                .sum();
+
+        BigDecimal totalCommission = allDistributors.stream()
+                .map(d -> d.getTotalCommission() != null ? d.getTotalCommission() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        String currentMonth = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
+        LambdaQueryWrapper<DistributorCommission> monthCommissionWrapper = new LambdaQueryWrapper<>();
+        monthCommissionWrapper.apply("DATE_FORMAT(create_time, '%Y-%m') = {0}", currentMonth);
+        List<DistributorCommission> monthCommissions = commissionMapper.selectList(monthCommissionWrapper);
+        BigDecimal monthlyCommission = monthCommissions.stream()
+                .map(c -> c.getAmount() != null ? c.getAmount() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        List<MonthlyTrendVO> trends = buildMonthlyTrends();
+
+        return DistributorDashboardVO.builder()
+                .totalDistributors(totalDistributors)
+                .activeDistributors(activeDistributors)
+                .totalReferrals(totalReferrals)
+                .validReferrals(validReferrals)
+                .totalCommission(totalCommission)
+                .monthlyCommission(monthlyCommission)
+                .trends(trends)
+                .build();
+    }
+
+    @Override
+    public List<DistributorMonthlyReport> exportMonthlyReport(String yearMonth) {
+        List<DistributorMonthlyReport> reports = reportMapper.selectByMonth(yearMonth);
+        reports.forEach(this::fillDistributorName);
+        return reports;
+    }
+
+    private List<MonthlyTrendVO> buildMonthlyTrends() {
+        List<MonthlyTrendVO> trends = new ArrayList<>();
+        LocalDateTime now = LocalDateTime.now();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
+
+        for (int i = 11; i >= 0; i--) {
+            LocalDateTime month = now.minusMonths(i);
+            String yearMonth = month.format(formatter);
+
+            LambdaQueryWrapper<DistributorMonthlyReport> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(DistributorMonthlyReport::getYearMonth, yearMonth);
+            List<DistributorMonthlyReport> monthReports = reportMapper.selectList(wrapper);
+
+            Long referralCount = monthReports.stream()
+                    .mapToLong(r -> r.getTotalReferrals() != null ? r.getTotalReferrals() : 0)
+                    .sum();
+
+            Long validCount = monthReports.stream()
+                    .mapToLong(r -> r.getValidReferrals() != null ? r.getValidReferrals() : 0)
+                    .sum();
+
+            BigDecimal commissionAmount = monthReports.stream()
+                    .map(r -> r.getTotalCommission() != null ? r.getTotalCommission() : BigDecimal.ZERO)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+            trends.add(MonthlyTrendVO.builder()
+                    .yearMonth(yearMonth)
+                    .referralCount(referralCount)
+                    .validCount(validCount)
+                    .commissionAmount(commissionAmount)
+                    .build());
+        }
+
+        return trends;
+    }
+
+    private void fillDistributorName(DistributorMonthlyReport report) {
+        if (report == null || report.getDistributorId() == null) {
+            return;
+        }
+        Distributor distributor = distributorMapper.selectById(report.getDistributorId());
+        if (distributor != null) {
+            report.setDistributorName(distributor.getName());
+        }
+    }
+}

+ 206 - 0
haha-service/src/main/java/com/haha/service/impl/DistributorServiceImpl.java

@@ -0,0 +1,206 @@
+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.exception.BusinessException;
+import com.haha.common.vo.StatusLabel;
+import com.haha.entity.Distributor;
+import com.haha.entity.DistributorReferral;
+import com.haha.entity.dto.DistributorCreateDTO;
+import com.haha.entity.dto.DistributorQueryDTO;
+import com.haha.entity.dto.DistributorUpdateDTO;
+import com.haha.mapper.DistributorMapper;
+import com.haha.mapper.DistributorReferralMapper;
+import com.haha.service.DistributorService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DistributorServiceImpl extends ServiceImpl<DistributorMapper, Distributor> implements DistributorService {
+
+    private final DistributorMapper distributorMapper;
+    private final DistributorReferralMapper distributorReferralMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Distributor create(DistributorCreateDTO dto, Long creatorId, String creatorName) {
+        int phoneCount = distributorMapper.countByPhone(dto.getPhone(), 0L);
+        if (phoneCount > 0) {
+            throw new BusinessException(400, "手机号已存在");
+        }
+
+        String maxCode = distributorMapper.selectMaxCode();
+        int nextNum = 1;
+        if (maxCode != null && maxCode.startsWith("DS")) {
+            String numPart = maxCode.substring(2);
+            try {
+                nextNum = Integer.parseInt(numPart) + 1;
+            } catch (NumberFormatException e) {
+                nextNum = 1;
+            }
+        }
+        String code = "DS" + String.format("%06d", nextNum);
+
+        Distributor distributor = new Distributor();
+        distributor.setCode(code);
+        distributor.setName(dto.getName());
+        distributor.setPhone(dto.getPhone());
+        distributor.setStatus(0);
+        distributor.setRemark(dto.getRemark());
+        distributor.setCreatorId(creatorId);
+        distributor.setCreatorName(creatorName);
+        distributor.setCreateTime(LocalDateTime.now());
+        distributor.setUpdateTime(LocalDateTime.now());
+        distributor.setDeleted(0);
+
+        this.save(distributor);
+
+        log.info("[分销员] 创建成功 - id: {}, code: {}, name: {}", distributor.getId(), distributor.getCode(), distributor.getName());
+        return distributor;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Distributor update(DistributorUpdateDTO dto) {
+        Distributor distributor = this.getById(dto.getId());
+        if (distributor == null || distributor.getDeleted() == 1) {
+            throw new BusinessException(404, "分销员不存在");
+        }
+
+        if (dto.getPhone() != null && !dto.getPhone().equals(distributor.getPhone())) {
+            int phoneCount = distributorMapper.countByPhone(dto.getPhone(), dto.getId());
+            if (phoneCount > 0) {
+                throw new BusinessException(400, "手机号已存在");
+            }
+            distributor.setPhone(dto.getPhone());
+        }
+
+        if (dto.getName() != null) {
+            distributor.setName(dto.getName());
+        }
+        if (dto.getRemark() != null) {
+            distributor.setRemark(dto.getRemark());
+        }
+        distributor.setUpdateTime(LocalDateTime.now());
+
+        this.updateById(distributor);
+
+        log.info("[分销员] 更新成功 - id: {}, name: {}", distributor.getId(), distributor.getName());
+        return distributor;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean activate(Long id) {
+        Distributor distributor = this.getById(id);
+        if (distributor == null || distributor.getDeleted() == 1) {
+            throw new BusinessException(404, "分销员不存在");
+        }
+        if (distributor.getStatus() != 0) {
+            throw new BusinessException(400, "当前状态不允许激活");
+        }
+
+        distributor.setStatus(1);
+        distributor.setUpdateTime(LocalDateTime.now());
+        this.updateById(distributor);
+
+        // TODO: 触发二维码生成
+
+        log.info("[分销员] 激活成功 - id: {}, code: {}", distributor.getId(), distributor.getCode());
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deactivate(Long id) {
+        Distributor distributor = this.getById(id);
+        if (distributor == null || distributor.getDeleted() == 1) {
+            throw new BusinessException(404, "分销员不存在");
+        }
+        if (distributor.getStatus() != 1) {
+            throw new BusinessException(400, "当前状态不允许停用");
+        }
+
+        distributor.setStatus(2);
+        distributor.setUpdateTime(LocalDateTime.now());
+        this.updateById(distributor);
+
+        log.info("[分销员] 停用成功 - id: {}, code: {}", distributor.getId(), distributor.getCode());
+        return true;
+    }
+
+    @Override
+    public IPage<Distributor> getPage(DistributorQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<Distributor> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<Distributor> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(queryDTO.getCode() != null, Distributor::getCode, queryDTO.getCode());
+        wrapper.like(queryDTO.getName() != null, Distributor::getName, queryDTO.getName());
+        wrapper.eq(queryDTO.getPhone() != null, Distributor::getPhone, queryDTO.getPhone());
+        wrapper.eq(queryDTO.getStatus() != null, Distributor::getStatus, queryDTO.getStatus());
+        wrapper.eq(Distributor::getDeleted, 0);
+        wrapper.orderByDesc(Distributor::getCreateTime);
+
+        IPage<Distributor> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillStatusLabel);
+
+        return result;
+    }
+
+    @Override
+    public Distributor getDetail(Long id) {
+        Distributor distributor = this.getById(id);
+        if (distributor == null || distributor.getDeleted() == 1) {
+            throw new BusinessException(404, "分销员不存在");
+        }
+        fillStatusLabel(distributor);
+        return distributor;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean delete(Long id) {
+        Distributor distributor = this.getById(id);
+        if (distributor == null || distributor.getDeleted() == 1) {
+            throw new BusinessException(404, "分销员不存在");
+        }
+
+        LambdaQueryWrapper<DistributorReferral> referralWrapper = new LambdaQueryWrapper<>();
+        referralWrapper.eq(DistributorReferral::getDistributorId, id);
+        Long referralCount = distributorReferralMapper.selectCount(referralWrapper);
+        if (referralCount > 0) {
+            throw new BusinessException(400, "该分销员已有推荐记录,无法删除");
+        }
+
+        distributor.setDeleted(1);
+        distributor.setUpdateTime(LocalDateTime.now());
+        this.updateById(distributor);
+
+        log.info("[分销员] 删除成功 - id: {}, code: {}", distributor.getId(), distributor.getCode());
+        return true;
+    }
+
+    private void fillStatusLabel(Distributor distributor) {
+        if (distributor.getStatus() == null) {
+            return;
+        }
+        StatusLabel statusLabel = switch (distributor.getStatus()) {
+            case 0 -> StatusLabel.info("待激活");
+            case 1 -> StatusLabel.success("已激活");
+            case 2 -> StatusLabel.danger("已停用");
+            default -> null;
+        };
+        if (statusLabel != null) {
+            distributor.setStatusLabel(statusLabel.getLabel());
+        }
+    }
+}

+ 168 - 0
haha-service/src/main/java/com/haha/service/impl/DistributorWithdrawalServiceImpl.java

@@ -0,0 +1,168 @@
+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.exception.BusinessException;
+import com.haha.common.vo.StatusLabel;
+import com.haha.entity.Distributor;
+import com.haha.entity.DistributorWithdrawal;
+import com.haha.entity.dto.WithdrawalQueryDTO;
+import com.haha.entity.dto.WithdrawalReviewDTO;
+import com.haha.mapper.DistributorMapper;
+import com.haha.mapper.DistributorWithdrawalMapper;
+import com.haha.service.DistributorConfigService;
+import com.haha.service.DistributorWithdrawalService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DistributorWithdrawalServiceImpl extends ServiceImpl<DistributorWithdrawalMapper, DistributorWithdrawal> implements DistributorWithdrawalService {
+
+    private final DistributorWithdrawalMapper withdrawalMapper;
+    private final DistributorMapper distributorMapper;
+    private final DistributorConfigService configService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DistributorWithdrawal applyWithdrawal(Long distributorId, BigDecimal amount) {
+        Distributor distributor = distributorMapper.selectById(distributorId);
+        if (distributor == null) {
+            throw new BusinessException(404, "分销员不存在");
+        }
+
+        BigDecimal minWithdrawalAmount = configService.getMinWithdrawalAmount();
+        if (amount.compareTo(minWithdrawalAmount) < 0) {
+            throw new BusinessException(400, "提现金额不能低于最低提现金额" + minWithdrawalAmount);
+        }
+
+        if (amount.compareTo(distributor.getCommissionBalance()) > 0) {
+            throw new BusinessException(400, "提现金额不能超过可提现余额");
+        }
+
+        distributorMapper.freezeAmount(distributorId, amount);
+
+        DistributorWithdrawal withdrawal = new DistributorWithdrawal();
+        withdrawal.setDistributorId(distributorId);
+        withdrawal.setAmount(amount);
+        withdrawal.setStatus(0);
+        withdrawal.setCreateTime(LocalDateTime.now());
+        withdrawal.setUpdateTime(LocalDateTime.now());
+
+        this.save(withdrawal);
+
+        log.info("[提现] 申请成功 - id: {}, distributorId: {}, amount: {}", withdrawal.getId(), distributorId, amount);
+        return withdrawal;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DistributorWithdrawal reviewWithdrawal(WithdrawalReviewDTO dto, Long reviewerId, String reviewerName) {
+        DistributorWithdrawal withdrawal = this.getById(dto.getId());
+        if (withdrawal == null) {
+            throw new BusinessException(404, "提现申请不存在");
+        }
+
+        if (withdrawal.getStatus() != 0) {
+            throw new BusinessException(400, "该申请已处理");
+        }
+
+        withdrawal.setReviewerId(reviewerId);
+        withdrawal.setReviewerName(reviewerName);
+        withdrawal.setReviewTime(LocalDateTime.now());
+        withdrawal.setReviewRemark(dto.getReviewRemark());
+
+        if (dto.getStatus() == 1) {
+            withdrawal.setStatus(1);
+        } else if (dto.getStatus() == 2) {
+            withdrawal.setStatus(2);
+            distributorMapper.unfreezeAmount(withdrawal.getDistributorId(), withdrawal.getAmount());
+        }
+
+        withdrawal.setUpdateTime(LocalDateTime.now());
+        this.updateById(withdrawal);
+
+        log.info("[提现] 审核完成 - id: {}, status: {}", withdrawal.getId(), dto.getStatus());
+        return withdrawal;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DistributorWithdrawal confirmTransfer(Long withdrawalId) {
+        DistributorWithdrawal withdrawal = this.getById(withdrawalId);
+        if (withdrawal == null) {
+            throw new BusinessException(404, "提现申请不存在");
+        }
+
+        if (withdrawal.getStatus() != 1) {
+            throw new BusinessException(400, "提现申请未通过审核");
+        }
+
+        withdrawal.setStatus(3);
+        withdrawal.setTransferTime(LocalDateTime.now());
+        withdrawal.setUpdateTime(LocalDateTime.now());
+
+        distributorMapper.deductWithdrawal(withdrawal.getDistributorId(), withdrawal.getAmount());
+
+        this.updateById(withdrawal);
+
+        log.info("[提现] 确认到账 - id: {}, distributorId: {}, amount: {}", withdrawal.getId(), withdrawal.getDistributorId(), withdrawal.getAmount());
+        return withdrawal;
+    }
+
+    @Override
+    public IPage<DistributorWithdrawal> getPage(WithdrawalQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<DistributorWithdrawal> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<DistributorWithdrawal> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(queryDTO.getDistributorId() != null, DistributorWithdrawal::getDistributorId, queryDTO.getDistributorId());
+        wrapper.eq(queryDTO.getStatus() != null, DistributorWithdrawal::getStatus, queryDTO.getStatus());
+        if (queryDTO.getStartDate() != null) {
+            wrapper.ge(DistributorWithdrawal::getCreateTime, queryDTO.getStartDate());
+        }
+        if (queryDTO.getEndDate() != null) {
+            wrapper.le(DistributorWithdrawal::getCreateTime, queryDTO.getEndDate() + " 23:59:59");
+        }
+        wrapper.orderByDesc(DistributorWithdrawal::getCreateTime);
+
+        IPage<DistributorWithdrawal> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillLabels);
+
+        return result;
+    }
+
+    private void fillLabels(DistributorWithdrawal withdrawal) {
+        if (withdrawal == null) {
+            return;
+        }
+
+        if (withdrawal.getStatus() != null) {
+            StatusLabel statusLabel = switch (withdrawal.getStatus()) {
+                case 0 -> StatusLabel.warning("待审核");
+                case 1 -> StatusLabel.success("已通过");
+                case 2 -> StatusLabel.danger("已拒绝");
+                case 3 -> StatusLabel.info("已到账");
+                default -> null;
+            };
+            if (statusLabel != null) {
+                withdrawal.setStatusLabel(statusLabel.getLabel());
+            }
+        }
+
+        if (withdrawal.getDistributorId() != null) {
+            Distributor distributor = distributorMapper.selectById(withdrawal.getDistributorId());
+            if (distributor != null) {
+                withdrawal.setDistributorName(distributor.getName());
+            }
+        }
+    }
+}