From 6989ade3e271234da1f1b855c8f1447cfe2816f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Thu, 29 Jan 2026 09:47:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=86=E9=94=80=E8=A7=84=E5=88=99?= =?UTF-8?q?=E5=AE=8C=E5=96=84=20+=20=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 分销规则: - 链接带ID绑定推荐关系 - 一级分销 + 30天有效期 - 客户抢夺机制(过期可被抢走) - 90%收益归分发者 2. 新增统计数据: - 绑定用户数 - 链接进入人数 - 带来付款人数 3. 微信支付: - 添加点击反馈 - 优化支付流程日志 - 改善错误提示 4. 分销中心UI优化 --- app/api/miniprogram/pay/notify/route.ts | 136 +++++++++++++- app/api/referral/bind/route.ts | 148 +++++++++++---- app/api/referral/data/route.ts | 229 ++++++++++++++++------- lib/db.ts | 16 ++ miniprogram/pages/read/read.js | 91 +++++++-- miniprogram/pages/referral/referral.js | 94 ++++++---- miniprogram/pages/referral/referral.wxml | 43 +++-- miniprogram/pages/referral/referral.wxss | 25 ++- 8 files changed, 598 insertions(+), 184 deletions(-) diff --git a/app/api/miniprogram/pay/notify/route.ts b/app/api/miniprogram/pay/notify/route.ts index 60f5532..1bed009 100644 --- a/app/api/miniprogram/pay/notify/route.ts +++ b/app/api/miniprogram/pay/notify/route.ts @@ -1,10 +1,15 @@ /** * 小程序支付回调通知处理 * 微信支付成功后会调用此接口 + * + * 分销规则: + * - 约90%给分发者(可在system_config配置) + * - 一级分销,只算直接推荐人 */ import { NextResponse } from 'next/server' import crypto from 'crypto' +import { query, getConfig } from '@/lib/db' const WECHAT_PAY_CONFIG = { appId: 'wxb8bbb2b10dec74aa', @@ -12,6 +17,9 @@ const WECHAT_PAY_CONFIG = { mchKey: 'wx3e31b068be59ddc131b068be59ddc2', } +// 默认分成比例(90%给推广者) +const DEFAULT_DISTRIBUTOR_SHARE = 0.9 + // 生成签名 function generateSign(params: Record, key: string): string { const sortedKeys = Object.keys(params).sort() @@ -94,6 +102,7 @@ export async function POST(request: Request) { const orderSn = data.out_trade_no const transactionId = data.transaction_id const totalFee = parseInt(data.total_fee || '0', 10) + const totalAmount = totalFee / 100 // 转为元 const openId = data.openid console.log('[PayNotify] 支付成功:', { @@ -115,16 +124,65 @@ export async function POST(request: Request) { const { productType, productId, userId } = attach - // TODO: 这里应该更新数据库中的订单状态 // 1. 更新订单状态为已支付 - // 2. 如果是章节购买,将章节添加到用户已购列表 - // 3. 如果是全书购买,更新用户为全书用户 + try { + await query(` + UPDATE orders + SET status = 'paid', + transaction_id = ?, + pay_time = CURRENT_TIMESTAMP + WHERE order_sn = ? AND status = 'pending' + `, [transactionId, orderSn]) + console.log('[PayNotify] 订单状态已更新:', orderSn) + } catch (e) { + console.error('[PayNotify] 更新订单状态失败:', e) + } + + // 2. 获取用户信息 + let buyerUserId = userId + if (!buyerUserId && openId) { + try { + const users = await query('SELECT id FROM users WHERE open_id = ?', [openId]) as any[] + if (users.length > 0) { + buyerUserId = users[0].id + } + } catch (e) { + console.error('[PayNotify] 获取用户信息失败:', e) + } + } + + // 3. 更新用户购买记录 + if (buyerUserId) { + try { + if (productType === 'fullbook') { + // 全书购买 + await query('UPDATE users SET has_full_book = TRUE WHERE id = ?', [buyerUserId]) + console.log('[PayNotify] 用户已购全书:', buyerUserId) + } else if (productType === 'section' && productId) { + // 单章购买 + await query(` + UPDATE users + SET purchased_sections = JSON_ARRAY_APPEND( + COALESCE(purchased_sections, '[]'), + '$', ? + ) + WHERE id = ? AND NOT JSON_CONTAINS(COALESCE(purchased_sections, '[]'), ?) + `, [productId, buyerUserId, JSON.stringify(productId)]) + console.log('[PayNotify] 用户已购章节:', buyerUserId, productId) + } + } catch (e) { + console.error('[PayNotify] 更新用户购买记录失败:', e) + } + + // 4. 处理分销佣金(90%给推广者) + await processReferralCommission(buyerUserId, totalAmount, orderSn) + } console.log('[PayNotify] 订单处理完成:', { orderSn, productType, productId, - userId, + userId: buyerUserId, }) // 返回成功响应给微信 @@ -139,3 +197,73 @@ export async function POST(request: Request) { }) } } + +/** + * 处理分销佣金 + * 规则:约90%给分发者(一级分销) + */ +async function processReferralCommission(buyerUserId: string, amount: number, orderSn: string) { + try { + // 获取分成配置 + let distributorShare = DEFAULT_DISTRIBUTOR_SHARE + try { + const config = await getConfig('referral_config') + if (config?.distributorShare) { + distributorShare = config.distributorShare / 100 + } + } catch (e) { /* 使用默认配置 */ } + + // 查找有效的推广绑定关系 + const bindings = await query(` + SELECT rb.id, rb.referrer_id, rb.referee_id, rb.expiry_date, rb.status + FROM referral_bindings rb + WHERE rb.referee_id = ? + AND rb.status = 'active' + AND rb.expiry_date > NOW() + ORDER BY rb.binding_date DESC + LIMIT 1 + `, [buyerUserId]) as any[] + + if (bindings.length === 0) { + console.log('[PayNotify] 用户无有效推广绑定,跳过分佣:', buyerUserId) + return + } + + const binding = bindings[0] + const referrerId = binding.referrer_id + + // 计算佣金(90%) + const commission = Math.round(amount * distributorShare * 100) / 100 + + console.log('[PayNotify] 处理分佣:', { + referrerId, + buyerUserId, + amount, + commission, + shareRate: `${distributorShare * 100}%` + }) + + // 更新推广者的待结算收益 + await query(` + UPDATE users + SET pending_earnings = pending_earnings + ? + WHERE id = ? + `, [commission, referrerId]) + + // 更新绑定记录状态为已转化 + await query(` + UPDATE referral_bindings + SET status = 'converted', + conversion_date = CURRENT_TIMESTAMP, + commission_amount = ?, + order_id = (SELECT id FROM orders WHERE order_sn = ? LIMIT 1) + WHERE id = ? + `, [commission, orderSn, binding.id]) + + console.log('[PayNotify] 分佣完成: 推广者', referrerId, '获得', commission, '元') + + } catch (error) { + console.error('[PayNotify] 处理分佣失败:', error) + // 分佣失败不影响主流程 + } +} diff --git a/app/api/referral/bind/route.ts b/app/api/referral/bind/route.ts index 8547b72..758e989 100644 --- a/app/api/referral/bind/route.ts +++ b/app/api/referral/bind/route.ts @@ -1,18 +1,27 @@ /** - * 推荐码绑定API - * 用于处理分享带来的推荐关系绑定 + * 推荐码绑定API - 增强版 + * + * 核心规则: + * 1. 链接带ID:谁发的链接,进的人就绑谁 + * 2. 一级、一月:只有一级分销;绑定有效期一个月 + * 3. 长期不发:别人发得多,客户会被「抢走」 + * 4. 每天发:持续发的人绑定一直有效,收益越来越高 + * 5. 约90%给分发:谁发得多谁拿得多 */ import { NextRequest, NextResponse } from 'next/server' -import { query } from '@/lib/db' +import { query, getConfig } from '@/lib/db' + +// 绑定有效期(天) +const BINDING_DAYS = 30 /** - * POST - 绑定推荐关系 + * POST - 绑定推荐关系(支持抢夺机制) */ export async function POST(request: NextRequest) { try { const body = await request.json() - const { userId, referralCode, openId } = body + const { userId, referralCode, openId, source } = body // 验证参数 const effectiveUserId = userId || (openId ? `user_${openId.slice(-8)}` : null) @@ -46,7 +55,7 @@ export async function POST(request: NextRequest) { }, { status: 400 }) } - // 检查用户是否已有推荐人 + // 检查用户是否存在 const users = await query( 'SELECT id, referred_by FROM users WHERE id = ? OR open_id = ?', [effectiveUserId, openId || effectiveUserId] @@ -60,46 +69,119 @@ export async function POST(request: NextRequest) { } const user = users[0] + const now = new Date() - if (user.referred_by) { - return NextResponse.json({ - success: false, - error: '已绑定其他推荐人' - }, { status: 400 }) + // 检查现有绑定关系 + const existingBindings = await query(` + SELECT id, referrer_id, expiry_date, status + FROM referral_bindings + WHERE referee_id = ? AND status = 'active' + ORDER BY binding_date DESC LIMIT 1 + `, [user.id]) as any[] + + let action = 'new' // new=新绑定, renew=续期, takeover=抢夺 + let oldReferrerId = null + + if (existingBindings.length > 0) { + const existing = existingBindings[0] + const expiryDate = new Date(existing.expiry_date) + + // 同一个推荐人 - 续期 + if (existing.referrer_id === referrer.id) { + action = 'renew' + } + // 不同推荐人 - 检查是否可以抢夺 + else if (expiryDate < now) { + // 已过期,可以被抢夺 + action = 'takeover' + oldReferrerId = existing.referrer_id + + // 将旧绑定标记为过期 + await query( + "UPDATE referral_bindings SET status = 'expired' WHERE id = ?", + [existing.id] + ) + } else { + // 未过期,不能被抢夺 + return NextResponse.json({ + success: false, + error: '用户已绑定其他推荐人,绑定有效期内无法更换', + expiryDate: expiryDate.toISOString() + }, { status: 400 }) + } } - // 绑定推荐关系 - await query( - 'UPDATE users SET referred_by = ? WHERE id = ?', - [referrer.id, user.id] - ) - - // 更新推荐人的推广数量 - await query( - 'UPDATE users SET referral_count = referral_count + 1 WHERE id = ?', - [referrer.id] - ) - - // 创建推荐绑定记录 - const bindingId = 'bind_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6) + // 计算新的过期时间(30天) const expiryDate = new Date() - expiryDate.setDate(expiryDate.getDate() + 30) // 30天有效期 + expiryDate.setDate(expiryDate.getDate() + BINDING_DAYS) - try { + // 创建或更新绑定记录 + const bindingId = 'bind_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6) + + if (action === 'renew') { + // 续期:更新过期时间 + await query(` + UPDATE referral_bindings + SET expiry_date = ?, binding_date = CURRENT_TIMESTAMP + WHERE referee_id = ? AND referrer_id = ? AND status = 'active' + `, [expiryDate, user.id, referrer.id]) + + console.log(`[Referral Bind] 续期: ${user.id} -> ${referrer.id}`) + } else { + // 新绑定或抢夺 await query(` INSERT INTO referral_bindings ( - id, referrer_id, referee_id, referral_code, status, expiry_date - ) VALUES (?, ?, ?, ?, 'active', ?) + id, referrer_id, referee_id, referral_code, status, expiry_date, binding_date + ) VALUES (?, ?, ?, ?, 'active', ?, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE + referrer_id = VALUES(referrer_id), + referral_code = VALUES(referral_code), + expiry_date = VALUES(expiry_date), + binding_date = CURRENT_TIMESTAMP, + status = 'active' `, [bindingId, referrer.id, user.id, referralCode, expiryDate]) - } catch (e) { - console.log('[Referral Bind] 创建绑定记录失败(可能是重复绑定):', e) + + // 更新用户的推荐人 + await query( + 'UPDATE users SET referred_by = ? WHERE id = ?', + [referrer.id, user.id] + ) + + // 更新推荐人的推广数量(仅新绑定时) + if (action === 'new') { + await query( + 'UPDATE users SET referral_count = referral_count + 1 WHERE id = ?', + [referrer.id] + ) + } + + // 如果是抢夺,减少原推荐人的推广数量 + if (action === 'takeover' && oldReferrerId) { + await query( + 'UPDATE users SET referral_count = GREATEST(referral_count - 1, 0) WHERE id = ?', + [oldReferrerId] + ) + console.log(`[Referral Bind] 抢夺: ${user.id}: ${oldReferrerId} -> ${referrer.id}`) + } else { + console.log(`[Referral Bind] 新绑定: ${user.id} -> ${referrer.id}`) + } } - console.log(`[Referral Bind] 成功: ${user.id} -> ${referrer.id} (${referralCode})`) + // 记录访问日志(用于统计「通过链接进的人数」) + try { + await query(` + INSERT INTO referral_visits (referrer_id, visitor_id, source, created_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + `, [referrer.id, user.id, source || 'miniprogram']) + } catch (e) { + // 访问日志表可能不存在,忽略错误 + } return NextResponse.json({ success: true, - message: '绑定成功', + message: action === 'renew' ? '绑定已续期' : (action === 'takeover' ? '绑定已更新' : '绑定成功'), + action, + expiryDate: expiryDate.toISOString(), referrer: { id: referrer.id, nickname: referrer.nickname diff --git a/app/api/referral/data/route.ts b/app/api/referral/data/route.ts index c972730..873ead9 100644 --- a/app/api/referral/data/route.ts +++ b/app/api/referral/data/route.ts @@ -1,10 +1,18 @@ /** - * 分销数据API - * 获取用户的推广数据、绑定用户列表、收益统计 + * 分销数据API - 增强版 + * + * 可见数据: + * - 绑定用户数(当前有效绑定) + * - 通过链接进的人数(总访问量) + * - 带来的付款人数(已转化购买) + * - 收益统计(90%归分发者) */ import { NextRequest, NextResponse } from 'next/server' -import { query } from '@/lib/db' +import { query, getConfig } from '@/lib/db' + +// 分成比例(默认90%给推广者) +const DISTRIBUTOR_SHARE = 0.9 /** * GET - 获取分销数据 @@ -21,6 +29,15 @@ export async function GET(request: NextRequest) { } try { + // 获取分销配置 + let distributorShare = DISTRIBUTOR_SHARE + try { + const config = await getConfig('referral_config') + if (config?.distributorShare) { + distributorShare = config.distributorShare / 100 + } + } catch (e) { /* 使用默认配置 */ } + // 1. 获取用户基本信息 const users = await query(` SELECT id, nickname, referral_code, earnings, pending_earnings, @@ -37,97 +54,173 @@ export async function GET(request: NextRequest) { const user = users[0] - // 2. 获取推荐的用户列表 - const referees = await query(` - SELECT id, nickname, avatar, phone, wechat_id, - has_full_book, created_at, - DATEDIFF(DATE_ADD(created_at, INTERVAL 30 DAY), NOW()) as days_remaining - FROM users - WHERE referred_by = ? - ORDER BY created_at DESC - `, [userId]) as any[] - - // 3. 分类绑定用户 - const now = new Date() - const activeBindings: any[] = [] - const convertedBindings: any[] = [] - const expiredBindings: any[] = [] - - for (const referee of referees) { - const binding = { - id: referee.id, - nickname: referee.nickname || '用户' + referee.id.slice(-4), - avatar: referee.avatar || `https://picsum.photos/100/100?random=${referee.id.slice(-2)}`, - phone: referee.phone ? referee.phone.slice(0, 3) + '****' + referee.phone.slice(-4) : null, - hasFullBook: referee.has_full_book, - daysRemaining: Math.max(0, referee.days_remaining || 0), - createdAt: referee.created_at - } + // 2. 获取绑定关系统计(从referral_bindings表) + let bindingStats = { total: 0, active: 0, converted: 0, expired: 0 } + try { + const bindings = await query(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN status = 'active' AND expiry_date > NOW() THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN status = 'converted' THEN 1 ELSE 0 END) as converted, + SUM(CASE WHEN status = 'expired' OR (status = 'active' AND expiry_date <= NOW()) THEN 1 ELSE 0 END) as expired + FROM referral_bindings + WHERE referrer_id = ? + `, [userId]) as any[] - if (referee.has_full_book) { - // 已转化(已购买) - convertedBindings.push(binding) - } else if (binding.daysRemaining <= 0) { - // 已过期 - expiredBindings.push(binding) - } else { - // 活跃中 - activeBindings.push(binding) + if (bindings.length > 0) { + bindingStats = { + total: parseInt(bindings[0].total) || 0, + active: parseInt(bindings[0].active) || 0, + converted: parseInt(bindings[0].converted) || 0, + expired: parseInt(bindings[0].expired) || 0 + } } + } catch (e) { /* 忽略 */ } + + // 3. 获取通过链接进入的总人数(访问日志) + let totalVisits = 0 + try { + const visits = await query(` + SELECT COUNT(DISTINCT visitor_id) as count + FROM referral_visits + WHERE referrer_id = ? + `, [userId]) as any[] + totalVisits = parseInt(visits[0]?.count) || 0 + } catch (e) { /* 访问记录表可能不存在 */ } + + // 如果没有访问记录表,用绑定总数替代 + if (totalVisits === 0) { + totalVisits = bindingStats.total } - // 4. 获取收益明细(最近的订单) + // 4. 获取带来的付款人数和金额 + let paymentStats = { paidCount: 0, totalAmount: 0 } + try { + const payments = await query(` + SELECT + COUNT(DISTINCT o.user_id) as paid_count, + COALESCE(SUM(o.amount), 0) as total_amount + FROM orders o + JOIN referral_bindings rb ON o.user_id = rb.referee_id + WHERE rb.referrer_id = ? AND o.status = 'paid' + `, [userId]) as any[] + + if (payments.length > 0) { + paymentStats = { + paidCount: parseInt(payments[0].paid_count) || 0, + totalAmount: parseFloat(payments[0].total_amount) || 0 + } + } + } catch (e) { /* 忽略 */ } + + // 5. 获取活跃绑定用户列表 + const activeBindings = await query(` + SELECT rb.id, rb.referee_id, rb.expiry_date, rb.binding_date, + u.nickname, u.avatar, u.has_full_book, + DATEDIFF(rb.expiry_date, NOW()) as days_remaining + FROM referral_bindings rb + JOIN users u ON rb.referee_id = u.id + WHERE rb.referrer_id = ? AND rb.status = 'active' AND rb.expiry_date > NOW() + ORDER BY rb.binding_date DESC + LIMIT 50 + `, [userId]) as any[] + + // 6. 获取已转化用户列表 + const convertedBindings = await query(` + SELECT rb.id, rb.referee_id, rb.conversion_date, rb.commission_amount, + u.nickname, u.avatar + FROM referral_bindings rb + JOIN users u ON rb.referee_id = u.id + WHERE rb.referrer_id = ? AND rb.status = 'converted' + ORDER BY rb.conversion_date DESC + LIMIT 50 + `, [userId]) as any[] + + // 7. 获取收益明细 let earningsDetails: any[] = [] try { earningsDetails = await query(` - SELECT o.id, o.amount, o.product_type, o.created_at, - u.nickname as buyer_nickname + SELECT o.id, o.order_sn, o.amount, o.product_type, o.pay_time, + u.nickname as buyer_nickname, + rb.commission_amount FROM orders o JOIN users u ON o.user_id = u.id - WHERE u.referred_by = ? AND o.status = 'paid' - ORDER BY o.created_at DESC - LIMIT 20 + JOIN referral_bindings rb ON o.user_id = rb.referee_id AND rb.referrer_id = ? + WHERE o.status = 'paid' + ORDER BY o.pay_time DESC + LIMIT 30 `, [userId]) as any[] - } catch (e) { - // 订单表可能不存在,忽略 - } + } catch (e) { /* 忽略 */ } - // 5. 统计数据 - const stats = { - totalReferrals: referees.length, - activeCount: activeBindings.length, - convertedCount: convertedBindings.length, - expiredCount: expiredBindings.length, - expiringCount: activeBindings.filter(b => b.daysRemaining <= 7).length - } + // 8. 计算预估收益 + const estimatedEarnings = paymentStats.totalAmount * distributorShare return NextResponse.json({ success: true, data: { - // 收益数据 + // === 核心可见数据 === + // 绑定用户数(当前有效绑定) + bindingCount: bindingStats.active, + // 通过链接进的人数 + visitCount: totalVisits, + // 带来的付款人数 + paidCount: paymentStats.paidCount, + + // === 收益数据 === + // 已结算收益 earnings: parseFloat(user.earnings) || 0, + // 待结算收益 pendingEarnings: parseFloat(user.pending_earnings) || 0, + // 已提现金额 withdrawnEarnings: parseFloat(user.withdrawn_earnings) || 0, + // 预估总收益 + estimatedEarnings: Math.round(estimatedEarnings * 100) / 100, + // 分成比例 + shareRate: Math.round(distributorShare * 100), - // 推荐码 + // === 推荐码 === referralCode: user.referral_code, - referralCount: user.referral_count || referees.length, + referralCount: user.referral_count || bindingStats.total, - // 绑定用户分类 - activeBindings, - convertedBindings, - expiredBindings, + // === 详细统计 === + stats: { + totalBindings: bindingStats.total, + activeBindings: bindingStats.active, + convertedBindings: bindingStats.converted, + expiredBindings: bindingStats.expired, + // 即将过期(7天内) + expiringCount: activeBindings.filter((b: any) => b.days_remaining <= 7 && b.days_remaining > 0).length, + // 总支付金额 + totalPaymentAmount: paymentStats.totalAmount + }, - // 统计 - stats, + // === 用户列表 === + activeUsers: activeBindings.map((b: any) => ({ + id: b.referee_id, + nickname: b.nickname || '用户' + b.referee_id.slice(-4), + avatar: b.avatar, + daysRemaining: Math.max(0, b.days_remaining), + hasFullBook: b.has_full_book, + bindingDate: b.binding_date + })), - // 收益明细 - earningsDetails: earningsDetails.map(e => ({ + convertedUsers: convertedBindings.map((b: any) => ({ + id: b.referee_id, + nickname: b.nickname || '用户' + b.referee_id.slice(-4), + avatar: b.avatar, + commission: parseFloat(b.commission_amount) || 0, + conversionDate: b.conversion_date + })), + + // === 收益明细 === + earningsDetails: earningsDetails.map((e: any) => ({ id: e.id, - amount: parseFloat(e.amount) * 0.9, // 90%佣金 + orderSn: e.order_sn, + amount: parseFloat(e.amount), + commission: parseFloat(e.commission_amount) || parseFloat(e.amount) * distributorShare, productType: e.product_type, buyerNickname: e.buyer_nickname, - createdAt: e.created_at + payTime: e.pay_time })) } }) diff --git a/lib/db.ts b/lib/db.ts index b333ca6..ffb0b6f 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -182,6 +182,22 @@ export async function initDatabase() { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `) + // 推广访问记录表(用于统计「通过链接进的人数」) + await query(` + CREATE TABLE IF NOT EXISTS referral_visits ( + id INT AUTO_INCREMENT PRIMARY KEY, + referrer_id VARCHAR(50) NOT NULL COMMENT '推广者ID', + visitor_id VARCHAR(50) COMMENT '访客ID(可能为空)', + visitor_openid VARCHAR(100) COMMENT '访客openId', + source VARCHAR(50) DEFAULT 'miniprogram' COMMENT '来源:miniprogram/web/share', + page VARCHAR(200) COMMENT '落地页路径', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_referrer_id (referrer_id), + INDEX idx_visitor_id (visitor_id), + INDEX idx_created_at (created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `) + // 系统配置表 await query(` CREATE TABLE IF NOT EXISTS system_config ( diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index e3a3469..8de44cf 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -455,26 +455,50 @@ Page({ // 购买章节 - 直接调起支付 async handlePurchaseSection() { + console.log('[Pay] 点击购买章节按钮') + wx.showLoading({ title: '处理中...', mask: true }) + if (!this.data.isLoggedIn) { + wx.hideLoading() + console.log('[Pay] 用户未登录,显示登录弹窗') this.setData({ showLoginModal: true }) return } - await this.processPayment('section', this.data.sectionId, this.data.section.price) + const price = this.data.section?.price || 1 + console.log('[Pay] 开始支付流程:', { sectionId: this.data.sectionId, price }) + wx.hideLoading() + await this.processPayment('section', this.data.sectionId, price) }, // 购买全书 - 直接调起支付 async handlePurchaseFullBook() { + console.log('[Pay] 点击购买全书按钮') + wx.showLoading({ title: '处理中...', mask: true }) + if (!this.data.isLoggedIn) { + wx.hideLoading() + console.log('[Pay] 用户未登录,显示登录弹窗') this.setData({ showLoginModal: true }) return } + console.log('[Pay] 开始支付流程: 全书', { price: this.data.fullBookPrice }) + wx.hideLoading() await this.processPayment('fullbook', null, this.data.fullBookPrice) }, // 处理支付 - 调用真实微信支付接口 async processPayment(type, sectionId, amount) { + console.log('[Pay] processPayment开始:', { type, sectionId, amount }) + + // 检查金额是否有效 + if (!amount || amount <= 0) { + console.error('[Pay] 金额无效:', amount) + wx.showToast({ title: '价格信息错误', icon: 'none' }) + return + } + // 检查是否已购买(避免重复购买) if (type === 'section' && sectionId) { const purchasedSections = app.globalData.purchasedSections || [] @@ -490,6 +514,7 @@ Page({ } this.setData({ isPaying: true }) + wx.showLoading({ title: '正在发起支付...', mask: true }) try { // 1. 先获取openId (支付必需) @@ -497,6 +522,7 @@ Page({ if (!openId) { console.log('[Pay] 需要先获取openId,尝试静默获取') + wx.showLoading({ title: '获取支付凭证...', mask: true }) openId = await app.getOpenId() if (!openId) { @@ -505,6 +531,7 @@ Page({ console.log('[Pay] 使用用户ID作为替代') openId = app.globalData.userInfo.id } else { + wx.hideLoading() wx.showModal({ title: '提示', content: '需要登录后才能支付,请先登录', @@ -517,6 +544,7 @@ Page({ } console.log('[Pay] 开始创建订单:', { type, sectionId, amount, openId: openId.slice(0, 10) + '...' }) + wx.showLoading({ title: '创建订单中...', mask: true }) // 2. 调用后端创建预支付订单 let paymentData = null @@ -538,12 +566,13 @@ Page({ if (res.success && res.data?.payParams) { paymentData = res.data.payParams - console.log('[Pay] 获取支付参数成功') + console.log('[Pay] 获取支付参数成功:', paymentData) } else { - throw new Error(res.error || '创建订单失败') + throw new Error(res.error || res.message || '创建订单失败') } } catch (apiError) { - console.error('[Pay] API创建订单失败:', apiError.message) + console.error('[Pay] API创建订单失败:', apiError) + wx.hideLoading() // 支付接口失败时,显示客服联系方式 wx.showModal({ title: '支付通道维护中', @@ -566,24 +595,48 @@ Page({ } // 3. 调用微信支付 - console.log('[Pay] 调起微信支付') - await this.callWechatPay(paymentData) + wx.hideLoading() + console.log('[Pay] 调起微信支付, paymentData:', paymentData) - // 4. 支付成功,更新本地数据 - this.mockPaymentSuccess(type, sectionId) - wx.showToast({ title: '购买成功', icon: 'success' }) - - // 5. 刷新页面 - this.initSection(this.data.sectionId) + try { + await this.callWechatPay(paymentData) + + // 4. 支付成功,更新本地数据 + console.log('[Pay] 微信支付成功!') + this.mockPaymentSuccess(type, sectionId) + wx.showToast({ title: '购买成功', icon: 'success' }) + + // 5. 刷新页面 + this.initSection(this.data.sectionId) + } catch (payErr) { + console.error('[Pay] 微信支付调起失败:', payErr) + if (payErr.errMsg && payErr.errMsg.includes('cancel')) { + wx.showToast({ title: '已取消支付', icon: 'none' }) + } else if (payErr.errMsg && payErr.errMsg.includes('requestPayment:fail')) { + // 支付失败,可能是参数错误或权限问题 + wx.showModal({ + title: '支付失败', + content: '微信支付暂不可用,请添加客服微信(28533368)手动购买', + confirmText: '复制微信号', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + wx.setClipboardData({ + data: '28533368', + success: () => wx.showToast({ title: '微信号已复制', icon: 'success' }) + }) + } + } + }) + } else { + wx.showToast({ title: payErr.errMsg || '支付失败', icon: 'none' }) + } + } } catch (e) { - console.error('[Pay] 支付失败:', e) - - if (e.errMsg && e.errMsg.includes('cancel')) { - wx.showToast({ title: '已取消支付', icon: 'none' }) - } else { - wx.showToast({ title: e.errMsg || '支付失败', icon: 'none' }) - } + console.error('[Pay] 支付流程异常:', e) + wx.hideLoading() + wx.showToast({ title: '支付出错,请重试', icon: 'none' }) } finally { this.setData({ isPaying: false }) } diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js index ea032f3..005b817 100644 --- a/miniprogram/pages/referral/referral.js +++ b/miniprogram/pages/referral/referral.js @@ -1,6 +1,11 @@ /** * Soul创业派对 - 分销中心页 - * 1:1还原Web版本 + * + * 可见数据: + * - 绑定用户数(当前有效绑定) + * - 通过链接进的人数(总访问量) + * - 带来的付款人数(已转化购买) + * - 收益统计(90%归分发者) */ const app = getApp() @@ -10,19 +15,25 @@ Page({ isLoggedIn: false, userInfo: null, - // 收益数据 - earnings: 0, - pendingEarnings: 0, - distributorShare: 90, + // === 核心可见数据 === + bindingCount: 0, // 绑定用户数(当前有效) + visitCount: 0, // 通过链接进的人数 + paidCount: 0, // 带来的付款人数 - // 统计数据 - referralCount: 0, - expiringCount: 0, + // === 收益数据 === + earnings: 0, // 已结算收益 + pendingEarnings: 0, // 待结算收益 + withdrawnEarnings: 0, // 已提现金额 + shareRate: 90, // 分成比例(90%) + + // === 统计数据 === + referralCount: 0, // 总推荐人数 + expiringCount: 0, // 即将过期人数 // 邀请码 referralCode: '', - // 绑定用户 + // 绑定用户列表 showBindingList: true, activeTab: 'active', activeBindings: [], @@ -31,6 +42,9 @@ Page({ currentBindings: [], totalBindings: 0, + // 收益明细 + earningsDetails: [], + // 海报 showPosterModal: false, isGeneratingPoster: false @@ -61,48 +75,58 @@ Page({ }) if (res.success) { realData = res.data + console.log('[Referral] 获取推广数据成功:', realData) } } catch (e) { - console.log('获取推广数据失败,使用本地数据') + console.log('[Referral] 获取推广数据失败,使用本地数据') } - // 使用真实数据或本地存储的数据 - const storedBindings = wx.getStorageSync('referral_bindings') || [] - const storedEarnings = wx.getStorageSync('referral_earnings') || { total: 0, pending: 0 } + // 使用真实数据或默认值 + let activeBindings = realData?.activeUsers || [] + let convertedBindings = realData?.convertedUsers || [] + let expiredBindings = [] - let activeBindings, convertedBindings, expiredBindings - - if (realData) { - activeBindings = realData.activeBindings || [] - convertedBindings = realData.convertedBindings || [] - expiredBindings = realData.expiredBindings || [] - } else if (storedBindings.length > 0) { - // 使用本地存储的数据 - activeBindings = storedBindings.filter(b => b.status === 'active') - convertedBindings = storedBindings.filter(b => b.status === 'converted') - expiredBindings = storedBindings.filter(b => b.status === 'expired') - } else { - // 默认空数据 - activeBindings = [] - convertedBindings = [] - expiredBindings = [] + // 兼容旧字段名 + if (!activeBindings.length && realData?.activeBindings) { + activeBindings = realData.activeBindings + } + if (!convertedBindings.length && realData?.convertedBindings) { + convertedBindings = realData.convertedBindings + } + if (realData?.expiredBindings) { + expiredBindings = realData.expiredBindings } - const expiringCount = activeBindings.filter(b => b.daysRemaining <= 7).length + const expiringCount = activeBindings.filter(b => b.daysRemaining <= 7 && b.daysRemaining > 0).length this.setData({ isLoggedIn: true, userInfo, - earnings: realData?.earnings || storedEarnings.total || 0, - pendingEarnings: realData?.pendingEarnings || storedEarnings.pending || 0, - referralCount: realData?.referralCount || activeBindings.length + convertedBindings.length, + + // 核心可见数据 + bindingCount: realData?.bindingCount || activeBindings.length, + visitCount: realData?.visitCount || 0, + paidCount: realData?.paidCount || convertedBindings.length, + + // 收益数据 + earnings: realData?.earnings || 0, + pendingEarnings: realData?.pendingEarnings || 0, + withdrawnEarnings: realData?.withdrawnEarnings || 0, + shareRate: realData?.shareRate || 90, + + // 统计 + referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length, + expiringCount, + referralCode, activeBindings, convertedBindings, expiredBindings, - expiringCount, currentBindings: activeBindings, - totalBindings: activeBindings.length + convertedBindings.length + expiredBindings.length + totalBindings: activeBindings.length + convertedBindings.length + expiredBindings.length, + + // 收益明细 + earningsDetails: realData?.earningsDetails || [] }) } }, diff --git a/miniprogram/pages/referral/referral.wxml b/miniprogram/pages/referral/referral.wxml index d02c12d..42b4277 100644 --- a/miniprogram/pages/referral/referral.wxml +++ b/miniprogram/pages/referral/referral.wxml @@ -32,7 +32,7 @@ 💰 累计收益 - {{distributorShare}}% 返利 + {{shareRate}}% 返利 @@ -40,42 +40,51 @@ 待结算: ¥{{pendingEarnings}} - - {{earnings < 10 ? '满10元可提现' : '申请提现'}} + + 已提现: ¥{{withdrawnEarnings}} + + + {{pendingEarnings < 10 ? '满10元可提现' : '申请提现'}} - + - - {{activeBindings.length}} - 绑定中 + + {{bindingCount}} + 绑定用户数 + 当前有效绑定 - {{convertedBindings.length}} - 已付款 + {{visitCount}} + 链接进入人数 + 通过你的链接进入 + + + {{paidCount}} + 付款人数 + 成功转化购买 {{expiringCount}} 即将过期 - - - {{referralCount}} - 总邀请 + 7天内到期 - ℹ️ + 📋 推广规则 - • 好友通过你的链接购买,立享5%优惠 - • 好友成功付款后,你获得 {{distributorShare}}% 收益 - • 绑定期30天,期满未付款自动解除 + 链接带ID:谁发的链接,进的人就绑谁 + 一级、一月:只有一级分销,绑定有效期30天 + 长期不发:别人发得多,过期后客户会被「抢走」 + 每天发:持续发的人绑定续期,收益越来越高 + {{shareRate}}%给分发:好友付款后,你得 {{shareRate}}% 收益 diff --git a/miniprogram/pages/referral/referral.wxss b/miniprogram/pages/referral/referral.wxss index bddebcd..4d2503d 100644 --- a/miniprogram/pages/referral/referral.wxss +++ b/miniprogram/pages/referral/referral.wxss @@ -35,12 +35,20 @@ .withdraw-btn { padding: 24rpx; background: #00CED1; color: #000; font-size: 28rpx; font-weight: 600; text-align: center; border-radius: 24rpx; } .withdraw-btn.btn-disabled { background: rgba(0,206,209,0.3); color: rgba(0,0,0,0.5); } -/* 数据统计 */ -.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; } -.stat-card { background: #1c1c1e; border-radius: 24rpx; padding: 24rpx 16rpx; text-align: center; } -.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; } +/* 收益详情 */ +.earnings-detail { padding-top: 16rpx; border-top: 2rpx solid rgba(255,255,255,0.1); margin-bottom: 24rpx; } +.detail-item { font-size: 24rpx; color: rgba(255,255,255,0.5); } + +/* 核心数据统计 */ +.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; } +.stat-card { background: #1c1c1e; border-radius: 24rpx; padding: 28rpx 20rpx; text-align: center; position: relative; } +.stat-card.highlight { background: linear-gradient(135deg, rgba(0,206,209,0.1) 0%, rgba(0,206,209,0.05) 100%); border: 2rpx solid rgba(0,206,209,0.2); } +.stat-value { font-size: 48rpx; font-weight: 700; color: #fff; display: block; } +.stat-value.brand { color: #00CED1; } +.stat-value.gold { color: #FFD700; } .stat-value.orange { color: #FFA500; } -.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; } +.stat-label { font-size: 24rpx; color: rgba(255,255,255,0.7); margin-top: 8rpx; display: block; font-weight: 500; } +.stat-tip { font-size: 20rpx; color: rgba(255,255,255,0.4); margin-top: 4rpx; display: block; } /* 推广规则 */ .rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 24rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; } @@ -48,9 +56,10 @@ .rules-icon { width: 56rpx; height: 56rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; } .rules-title { font-size: 28rpx; font-weight: 500; color: #fff; } .rules-list { padding-left: 8rpx; } -.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.8; display: block; } -.rule-item .gold { color: #FFD700; } -.rule-item .brand { color: #00CED1; } +.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; } +.rule-item .gold { color: #FFD700; font-weight: 500; } +.rule-item .brand { color: #00CED1; font-weight: 500; } +.rule-item .orange { color: #FFA500; font-weight: 500; } /* 绑定用户卡片 */ .binding-card { background: #1c1c1e; border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }