更新支付和推荐系统逻辑,新增根据推荐配置计算支付金额的功能,确保用户享受优惠。调整绑定推荐关系的有效期读取方式,支持从配置中获取。优化提现流程,增加最低提现金额的配置读取,提升系统灵活性和用户体验。同时,更新管理界面中的支付设置链接,确保一致性。
This commit is contained in:
@@ -47,7 +47,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
{ icon: BookOpen, label: "内容管理", href: "/admin/content" },
|
||||
{ icon: Users, label: "用户管理", href: "/admin/users" },
|
||||
{ icon: Wallet, label: "交易中心", href: "/admin/distribution" }, // 合并:分销+订单+提现
|
||||
{ icon: CreditCard, label: "支付设置", href: "/admin/payment" },
|
||||
{ icon: CreditCard, label: "推广设置", href: "/admin/referral-settings" }, // 单独入口,集中管理分销配置
|
||||
{ icon: Settings, label: "系统设置", href: "/admin/settings" },
|
||||
]
|
||||
|
||||
|
||||
272
app/admin/referral-settings/page.tsx
Normal file
272
app/admin/referral-settings/page.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Save, Percent, Users, Wallet, Info } from "lucide-react"
|
||||
|
||||
type ReferralConfig = {
|
||||
distributorShare: number
|
||||
minWithdrawAmount: number
|
||||
bindingDays: number
|
||||
userDiscount: number
|
||||
enableAutoWithdraw: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_REFERRAL_CONFIG: ReferralConfig = {
|
||||
distributorShare: 90,
|
||||
minWithdrawAmount: 10,
|
||||
bindingDays: 30,
|
||||
userDiscount: 5,
|
||||
enableAutoWithdraw: false,
|
||||
}
|
||||
|
||||
export default function ReferralSettingsPage() {
|
||||
const [config, setConfig] = useState<ReferralConfig>(DEFAULT_REFERRAL_CONFIG)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/db/config?key=referral_config")
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data?.success && data.config) {
|
||||
setConfig({
|
||||
distributorShare: data.config.distributorShare ?? 90,
|
||||
minWithdrawAmount: data.config.minWithdrawAmount ?? 10,
|
||||
bindingDays: data.config.bindingDays ?? 30,
|
||||
userDiscount: data.config.userDiscount ?? 5,
|
||||
enableAutoWithdraw: data.config.enableAutoWithdraw ?? false,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载 referral_config 失败:", e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
loadConfig()
|
||||
}, [])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
// 确保所有字段都是正确类型(防止字符串导致计算错误)
|
||||
const safeConfig = {
|
||||
distributorShare: Number(config.distributorShare) || 0,
|
||||
minWithdrawAmount: Number(config.minWithdrawAmount) || 0,
|
||||
bindingDays: Number(config.bindingDays) || 0,
|
||||
userDiscount: Number(config.userDiscount) || 0,
|
||||
enableAutoWithdraw: Boolean(config.enableAutoWithdraw),
|
||||
}
|
||||
|
||||
const body = {
|
||||
key: "referral_config",
|
||||
config: safeConfig,
|
||||
description: "分销 / 推广规则配置",
|
||||
}
|
||||
const res = await fetch("/api/db/config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok || !data?.success) {
|
||||
alert("保存失败: " + (data?.error || res.statusText))
|
||||
return
|
||||
}
|
||||
|
||||
alert("✅ 分销配置已保存成功!\n\n• 小程序与网站的推广规则会一起生效\n• 绑定关系会使用新的天数配置\n• 佣金比例会立即应用到新订单\n\n如有缓存,请刷新前台/小程序页面。")
|
||||
} catch (e: any) {
|
||||
console.error("保存 referral_config 失败:", e)
|
||||
alert("保存失败: " + (e?.message || String(e)))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNumberChange = (field: keyof ReferralConfig) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value || "0")
|
||||
setConfig((prev) => ({ ...prev, [field]: isNaN(value) ? 0 : value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Wallet className="w-5 h-5 text-[#38bdac]" />
|
||||
推广 / 分销设置
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">
|
||||
统一管理「好友优惠」「你得 90% 收益」「绑定期 30 天」「提现门槛」等规则,小程序和 Web 共用这套配置。
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving || loading}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{saving ? "保存中..." : "保存配置"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 核心规则卡片 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<Percent className="w-4 h-4 text-[#38bdac]" />
|
||||
推广规则
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
这三项会直接体现在小程序「推广规则」卡片上,同时影响实收佣金计算。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300 flex items-center gap-2">
|
||||
<Info className="w-3 h-3 text-[#38bdac]" />
|
||||
好友优惠(%)
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={config.userDiscount}
|
||||
onChange={handleNumberChange("userDiscount")}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">例如 5 表示好友立减 5%(在价格配置基础上生效)。</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300 flex items-center gap-2">
|
||||
<Users className="w-3 h-3 text-[#38bdac]" />
|
||||
推广者分成(%)
|
||||
</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
className="flex-1"
|
||||
min={10}
|
||||
max={100}
|
||||
step={1}
|
||||
value={[config.distributorShare]}
|
||||
onValueChange={([val]) => setConfig((prev) => ({ ...prev, distributorShare: val }))}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
className="w-20 bg-[#0a1628] border-gray-700 text-white text-center"
|
||||
value={config.distributorShare}
|
||||
onChange={handleNumberChange("distributorShare")}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
实际佣金 = 订单金额 × {" "}
|
||||
<span className="text-[#38bdac] font-mono">{config.distributorShare}%</span>,支付回调和分销统计都会用这个值。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300 flex items-center gap-2">
|
||||
<Users className="w-3 h-3 text-[#38bdac]" />
|
||||
绑定有效期(天)
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={365}
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={config.bindingDays}
|
||||
onChange={handleNumberChange("bindingDays")}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">好友通过你的链接进来并登录后,绑定在你名下的天数。</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 提现与自动提现 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<Wallet className="w-4 h-4 text-[#38bdac]" />
|
||||
提现规则
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
与「提现中心」「自动提现」相关的参数,影响推广者看到的可提现金额和最低门槛。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">最低提现金额(元)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={config.minWithdrawAmount}
|
||||
onChange={handleNumberChange("minWithdrawAmount")}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">小程序「满 X 元可提现」展示的门槛,同时用于后端接口校验。</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300 flex items-center gap-2">
|
||||
自动提现开关
|
||||
<Badge variant="outline" className="border-[#38bdac]/40 text-[#38bdac] text-[10px]">
|
||||
预留
|
||||
</Badge>
|
||||
</Label>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<Switch
|
||||
checked={config.enableAutoWithdraw}
|
||||
onCheckedChange={(checked) => setConfig((prev) => ({ ...prev, enableAutoWithdraw: checked }))}
|
||||
/>
|
||||
<span className="text-sm text-gray-400">
|
||||
开启后,可结合定时任务实现「收益自动打款到微信零钱」。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 提示卡片 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-200 text-sm">
|
||||
<Info className="w-4 h-4 text-[#38bdac]" />
|
||||
使用说明
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-xs text-gray-400 leading-relaxed">
|
||||
<p>
|
||||
1. 以上配置会写入 <code className="font-mono text-[11px] text-[#38bdac]">system_config.referral_config</code>,小程序「推广中心」、
|
||||
Web 推广页以及支付回调都会读取同一份配置。
|
||||
</p>
|
||||
<p>
|
||||
2. 修改后新订单立即生效;旧订单的历史佣金不会自动重算,只影响之后产生的订单。
|
||||
</p>
|
||||
<p>
|
||||
3. 如遇前端展示与实际结算不一致,优先以此处配置为准,再排查缓存和小程序版本。
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import crypto from 'crypto'
|
||||
import { query } from '@/lib/db'
|
||||
import { query, getConfig } from '@/lib/db'
|
||||
|
||||
// 微信支付配置 - 2026-01-25 更新
|
||||
// 小程序支付绑定状态: 审核中(申请单ID: 201554696918)
|
||||
@@ -101,8 +101,33 @@ export async function POST(request: Request) {
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// === 根据推广配置计算好友优惠后的实际支付金额 ===
|
||||
let finalAmount = amount
|
||||
try {
|
||||
// 读取推广/分销配置,获取好友优惠比例(如 5 表示 5%)
|
||||
const referralConfig = await getConfig('referral_config')
|
||||
const userDiscount = referralConfig?.userDiscount ? Number(referralConfig.userDiscount) : 0
|
||||
|
||||
// 若存在有效的推荐码且配置了优惠比例,则给好友打折
|
||||
if (userDiscount > 0 && body.referralCode) {
|
||||
const discountRate = userDiscount / 100
|
||||
const discounted = amount * (1 - discountRate)
|
||||
// 保证至少 0.01 元,并保留两位小数
|
||||
finalAmount = Math.max(0.01, Math.round(discounted * 100) / 100)
|
||||
console.log('[MiniPay] 应用好友优惠:', {
|
||||
originalAmount: amount,
|
||||
discountPercent: userDiscount,
|
||||
finalAmount,
|
||||
referralCode: body.referralCode,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[MiniPay] 读取 referral_config.userDiscount 失败,使用原价金额:', e)
|
||||
finalAmount = amount
|
||||
}
|
||||
|
||||
const orderSn = generateOrderSn()
|
||||
const totalFee = Math.round(amount * 100) // 转换为分
|
||||
const totalFee = Math.round(finalAmount * 100) // 转换为分(单位分)
|
||||
const goodsBody = description || (productType === 'fullbook' ? '《一场Soul的创业实验》全书' : `章节购买-${productId}`)
|
||||
|
||||
// 获取客户端IP
|
||||
@@ -207,7 +232,7 @@ export async function POST(request: Request) {
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`, [
|
||||
orderSn, orderSn, userId, openId,
|
||||
productType, productId || 'fullbook', amount, goodsBody,
|
||||
productType, productId || 'fullbook', finalAmount, goodsBody,
|
||||
'created', null, referrerId, orderReferralCode
|
||||
])
|
||||
} catch (insertErr: any) {
|
||||
@@ -224,7 +249,7 @@ export async function POST(request: Request) {
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`, [
|
||||
orderSn, orderSn, userId, openId,
|
||||
productType, productId || 'fullbook', amount, goodsBody,
|
||||
productType, productId || 'fullbook', finalAmount, goodsBody,
|
||||
'created', null, referrerId
|
||||
])
|
||||
console.log('[MiniPay] 订单已插入(未含 referral_code,请执行 scripts/add_orders_referral_code.py)')
|
||||
@@ -238,7 +263,7 @@ export async function POST(request: Request) {
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`, [
|
||||
orderSn, orderSn, userId, openId,
|
||||
productType, productId || 'fullbook', amount, goodsBody,
|
||||
productType, productId || 'fullbook', finalAmount, goodsBody,
|
||||
'created', null
|
||||
])
|
||||
console.log('[MiniPay] 订单已插入(未含 referrer_id/referral_code,请执行迁移脚本)')
|
||||
@@ -257,7 +282,8 @@ export async function POST(request: Request) {
|
||||
userId,
|
||||
productType,
|
||||
productId,
|
||||
amount
|
||||
originalAmount: amount,
|
||||
finalAmount,
|
||||
})
|
||||
} catch (dbError) {
|
||||
console.error('[MiniPay] ❌ 插入订单失败:', dbError)
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query, getConfig } from '@/lib/db'
|
||||
|
||||
// 绑定有效期(天)
|
||||
const BINDING_DAYS = 30
|
||||
// 绑定有效期(天)- 默认值,优先从配置读取
|
||||
const DEFAULT_BINDING_DAYS = 30
|
||||
|
||||
/**
|
||||
* POST - 绑定推荐关系(支持抢夺机制)
|
||||
@@ -32,6 +32,17 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 获取绑定天数配置
|
||||
let bindingDays = DEFAULT_BINDING_DAYS
|
||||
try {
|
||||
const config = await getConfig('referral_config')
|
||||
if (config?.bindingDays) {
|
||||
bindingDays = Number(config.bindingDays)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Referral Bind] 读取配置失败,使用默认值', DEFAULT_BINDING_DAYS)
|
||||
}
|
||||
|
||||
// 查找推荐人
|
||||
const referrers = await query(
|
||||
'SELECT id, nickname, referral_code FROM users WHERE referral_code = ?',
|
||||
@@ -111,9 +122,9 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算新的过期时间(30天)
|
||||
// 计算新的过期时间(从配置读取天数)
|
||||
const expiryDate = new Date()
|
||||
expiryDate.setDate(expiryDate.getDate() + BINDING_DAYS)
|
||||
expiryDate.setDate(expiryDate.getDate() + bindingDays)
|
||||
|
||||
// 创建或更新绑定记录
|
||||
const bindingId = 'bind_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { query, getConfig } from '@/lib/db'
|
||||
|
||||
// 确保提现表存在
|
||||
async function ensureWithdrawalsTable() {
|
||||
@@ -41,6 +41,25 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ success: false, message: '提现金额无效' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 读取最低提现门槛
|
||||
let minWithdrawAmount = 10 // 默认值
|
||||
try {
|
||||
const config = await getConfig('referral_config')
|
||||
if (config?.minWithdrawAmount) {
|
||||
minWithdrawAmount = Number(config.minWithdrawAmount)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Withdraw] 读取配置失败,使用默认值 10 元')
|
||||
}
|
||||
|
||||
// 检查最低提现门槛
|
||||
if (amount < minWithdrawAmount) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: `最低提现金额为 ¥${minWithdrawAmount},当前 ¥${amount}`
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 确保表存在
|
||||
await ensureWithdrawalsTable()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user