更新支付和推荐系统逻辑,新增根据推荐配置计算支付金额的功能,确保用户享受优惠。调整绑定推荐关系的有效期读取方式,支持从配置中获取。优化提现流程,增加最低提现金额的配置读取,提升系统灵活性和用户体验。同时,更新管理界面中的支付设置链接,确保一致性。

This commit is contained in:
2026-02-05 18:45:28 +08:00
parent 19d0e625db
commit 1a95aee112
9 changed files with 858 additions and 24 deletions

View File

@@ -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" },
]

View 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>
)
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()