更新服务器信息为新的 IP 地址,调整相关文档和代码中的默认配置,确保部署和连接的一致性。同时,优化订单管理界面,增强商品信息的格式化逻辑,提升用户体验。
This commit is contained in:
4
.github/workflows/README.md
vendored
4
.github/workflows/README.md
vendored
@@ -18,7 +18,7 @@
|
||||
### 1. 在服务器上生成 SSH 密钥对
|
||||
|
||||
```bash
|
||||
ssh root@42.194.232.22
|
||||
ssh root@43.139.27.93
|
||||
ssh-keygen -t rsa -b 4096 -C "github-actions-deploy"
|
||||
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
|
||||
cat ~/.ssh/id_rsa # 复制私钥内容
|
||||
@@ -32,7 +32,7 @@ cat ~/.ssh/id_rsa # 复制私钥内容
|
||||
|
||||
| Secret 名称 | 值 | 说明 |
|
||||
|------------|-----|------|
|
||||
| `SSH_HOST` | `42.194.232.22` | 服务器 IP |
|
||||
| `SSH_HOST` | `43.139.27.93` | 服务器 IP |
|
||||
| `SSH_USERNAME` | `root` | SSH 用户名 |
|
||||
| `SSH_PRIVATE_KEY` | `-----BEGIN OPENSSH PRIVATE KEY-----...` | 服务器 SSH 私钥(完整内容) |
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ pip install -r requirements-deploy.txt
|
||||
|
||||
### 2. 配置(可选)
|
||||
|
||||
脚本默认使用 `.cursorrules` 中的服务器信息(42.194.232.22、root、项目路径 /www/wwwroot/soul 等)。如需覆盖,可设置环境变量:
|
||||
脚本默认使用 `.cursorrules` 中的服务器信息(43.139.27.93、root、项目路径 /www/wwwroot/soul 等)。如需覆盖,可设置环境变量:
|
||||
|
||||
- `DEPLOY_HOST`、`DEPLOY_USER`、`DEPLOY_PASSWORD` 或 `DEPLOY_SSH_KEY`
|
||||
- `DEPLOY_PROJECT_PATH`(如 /www/wwwroot/soul)
|
||||
|
||||
@@ -24,23 +24,41 @@ interface Purchase {
|
||||
|
||||
function OrdersContent() {
|
||||
const { getAllPurchases, getAllUsers } = useStore()
|
||||
const [purchases, setPurchases] = useState<Purchase[]>([])
|
||||
const [purchases, setPurchases] = useState<any[]>([]) // 改为 any[] 以支持新字段
|
||||
const [users, setUsers] = useState<any[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 从API获取订单(包含用户昵称)
|
||||
async function loadOrders() {
|
||||
setIsLoading(true)
|
||||
setPurchases(getAllPurchases())
|
||||
setUsers(getAllUsers())
|
||||
setIsLoading(false)
|
||||
}, [getAllPurchases, getAllUsers])
|
||||
try {
|
||||
const ordersRes = await fetch('/api/orders')
|
||||
const ordersData = await ordersRes.json()
|
||||
if (ordersData.success && ordersData.orders) {
|
||||
setPurchases(ordersData.orders)
|
||||
}
|
||||
|
||||
const usersRes = await fetch('/api/db/users')
|
||||
const usersData = await usersRes.json()
|
||||
if (usersData.success && usersData.users) {
|
||||
setUsers(usersData.users)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载订单失败', e)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户昵称
|
||||
const getUserNickname = (userId: string) => {
|
||||
const user = users.find(u => u.id === userId)
|
||||
return user?.nickname || "未知用户"
|
||||
useEffect(() => {
|
||||
loadOrders()
|
||||
}, [])
|
||||
|
||||
// 获取用户昵称(优先使用 order.userNickname)
|
||||
const getUserNickname = (order: any) => {
|
||||
return order.userNickname || users.find((u: any) => u.id === order.userId)?.nickname || "匿名用户"
|
||||
}
|
||||
|
||||
// 获取用户手机号
|
||||
@@ -48,28 +66,64 @@ function OrdersContent() {
|
||||
const user = users.find(u => u.id === userId)
|
||||
return user?.phone || "-"
|
||||
}
|
||||
|
||||
// 格式化商品信息
|
||||
const formatProduct = (order: any) => {
|
||||
const type = order.productType || ""
|
||||
const desc = order.description || ""
|
||||
|
||||
if (desc) {
|
||||
if (type === "section" && desc.includes("章节")) {
|
||||
if (desc.includes("-")) {
|
||||
const parts = desc.split("-")
|
||||
if (parts.length >= 3) {
|
||||
return {
|
||||
name: `第${parts[1]}章 第${parts[2]}节`,
|
||||
type: "《一场Soul的创业实验》"
|
||||
}
|
||||
}
|
||||
}
|
||||
return { name: desc, type: "章节购买" }
|
||||
}
|
||||
if (type === "fullbook" || desc.includes("全书")) {
|
||||
return { name: "《一场Soul的创业实验》", type: "全书购买" }
|
||||
}
|
||||
if (type === "match" || desc.includes("伙伴")) {
|
||||
return { name: "找伙伴匹配", type: "功能服务" }
|
||||
}
|
||||
return { name: desc, type: "其他" }
|
||||
}
|
||||
|
||||
if (type === "section") return { name: `章节 ${order.productId || ""}`, type: "单章" }
|
||||
if (type === "fullbook") return { name: "《一场Soul的创业实验》", type: "全书" }
|
||||
if (type === "match") return { name: "找伙伴匹配", type: "功能" }
|
||||
return { name: "未知商品", type: type || "其他" }
|
||||
}
|
||||
|
||||
// 过滤订单
|
||||
const filteredPurchases = purchases.filter((p) => {
|
||||
const product = formatProduct(p)
|
||||
const matchSearch =
|
||||
getUserNickname(p.userId).includes(searchTerm) ||
|
||||
getUserNickname(p).includes(searchTerm) ||
|
||||
getUserPhone(p.userId).includes(searchTerm) ||
|
||||
p.sectionTitle?.includes(searchTerm) ||
|
||||
p.id.includes(searchTerm)
|
||||
product.name.includes(searchTerm) ||
|
||||
(p.orderSn && p.orderSn.includes(searchTerm)) ||
|
||||
(p.id && p.id.includes(searchTerm))
|
||||
|
||||
const matchStatus = statusFilter === "all" || p.status === statusFilter
|
||||
const matchStatus = statusFilter === "all" || p.status === statusFilter ||
|
||||
(statusFilter === "completed" && p.status === "paid")
|
||||
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const totalRevenue = purchases.filter(p => p.status === "completed").reduce((sum, p) => sum + p.amount, 0)
|
||||
// 统计数据(status 可能是 'paid' 或 'completed')
|
||||
const totalRevenue = purchases.filter(p => p.status === "paid" || p.status === "completed").reduce((sum, p) => sum + Number(p.amount || 0), 0)
|
||||
const todayRevenue = purchases
|
||||
.filter(p => {
|
||||
const today = new Date().toDateString()
|
||||
return p.status === "completed" && new Date(p.createdAt).toDateString() === today
|
||||
return (p.status === "paid" || p.status === "completed") && new Date(p.createdAt).toDateString() === today
|
||||
})
|
||||
.reduce((sum, p) => sum + p.amount, 0)
|
||||
.reduce((sum, p) => sum + Number(p.amount || 0), 0)
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
@@ -110,6 +164,7 @@ function OrdersContent() {
|
||||
<option value="all">全部状态</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="pending">待支付</option>
|
||||
<option value="created">已创建</option>
|
||||
<option value="failed">已失败</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -144,61 +199,57 @@ function OrdersContent() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPurchases.map((purchase) => (
|
||||
<TableRow key={purchase.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||||
<TableCell className="font-mono text-xs text-gray-400">
|
||||
{purchase.id.slice(0, 12)}...
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-white text-sm">{getUserNickname(purchase.userId)}</p>
|
||||
<p className="text-gray-500 text-xs">{getUserPhone(purchase.userId)}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-white text-sm">
|
||||
{purchase.type === "fullbook" ? "整本购买" :
|
||||
purchase.type === "match" ? "匹配次数" :
|
||||
purchase.sectionTitle || `章节${purchase.sectionId}`}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">
|
||||
{purchase.type === "fullbook" ? "全书" :
|
||||
purchase.type === "match" ? "功能" : "单章"}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-[#38bdac] font-bold">
|
||||
¥{purchase.amount.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-300">
|
||||
{purchase.paymentMethod === "wechat" ? "微信支付" :
|
||||
purchase.paymentMethod === "alipay" ? "支付宝" :
|
||||
purchase.paymentMethod || "微信支付"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{purchase.status === "completed" ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||
已完成
|
||||
</Badge>
|
||||
) : purchase.status === "pending" ? (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/20 border-0">
|
||||
待支付
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-500/20 text-red-400 hover:bg-red-500/20 border-0">
|
||||
已失败
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-[#FFD700]">
|
||||
{purchase.referrerEarnings ? `¥${purchase.referrerEarnings.toFixed(2)}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-400 text-sm">
|
||||
{new Date(purchase.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredPurchases.map((purchase) => {
|
||||
const product = formatProduct(purchase)
|
||||
return (
|
||||
<TableRow key={purchase.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||||
<TableCell className="font-mono text-xs text-gray-400">
|
||||
{(purchase.orderSn || purchase.id || "").slice(0, 12)}...
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-white text-sm">{getUserNickname(purchase)}</p>
|
||||
<p className="text-gray-500 text-xs">{getUserPhone(purchase.userId)}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-white text-sm">{product.name}</p>
|
||||
<p className="text-gray-500 text-xs">{product.type}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-[#38bdac] font-bold">
|
||||
¥{Number(purchase.amount || 0).toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-300">
|
||||
{purchase.paymentMethod === "wechat" ? "微信支付" :
|
||||
purchase.paymentMethod === "alipay" ? "支付宝" :
|
||||
purchase.paymentMethod || "微信支付"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{purchase.status === "paid" || purchase.status === "completed" ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||
已完成
|
||||
</Badge>
|
||||
) : purchase.status === "pending" || purchase.status === "created" ? (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/20 border-0">
|
||||
待支付
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-500/20 text-red-400 hover:bg-red-500/20 border-0">
|
||||
已失败
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-[#FFD700]">
|
||||
{purchase.referrerEarnings ? `¥${Number(purchase.referrerEarnings).toFixed(2)}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-400 text-sm">
|
||||
{new Date(purchase.createdAt).toLocaleString('zh-CN')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
{filteredPurchases.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12 text-gray-500">
|
||||
|
||||
@@ -67,13 +67,56 @@ export default function AdminDashboard() {
|
||||
const totalUsers = users.length
|
||||
const totalPurchases = purchases.length
|
||||
|
||||
// 订单类型对应中文(product_type: section | fullbook | match)
|
||||
const productTypeLabel = (p: { productType?: string; productId?: string; sectionTitle?: string }) => {
|
||||
// 格式化订单商品信息(显示书名和章节)
|
||||
const formatOrderProduct = (p: any) => {
|
||||
const type = p.productType || ""
|
||||
if (type === "section") return p.productId ? `单章 ${p.productId}` : "单章"
|
||||
if (type === "fullbook") return "整本购买"
|
||||
if (type === "match") return "找伙伴"
|
||||
return p.sectionTitle || "其他"
|
||||
const desc = p.description || ""
|
||||
|
||||
// 优先使用 description,因为它包含完整的商品描述
|
||||
if (desc) {
|
||||
// 如果是章节购买,提取章节标题
|
||||
if (type === "section" && desc.includes("章节")) {
|
||||
// description 格式可能是:"章节购买-1-2" 或具体章节标题
|
||||
if (desc.includes("-")) {
|
||||
const parts = desc.split("-")
|
||||
if (parts.length >= 3) {
|
||||
return {
|
||||
title: `第${parts[1]}章 第${parts[2]}节`,
|
||||
subtitle: "《一场Soul的创业实验》"
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: desc,
|
||||
subtitle: "章节购买"
|
||||
}
|
||||
}
|
||||
// 如果是整本购买
|
||||
if (type === "fullbook" || desc.includes("全书")) {
|
||||
return {
|
||||
title: "《一场Soul的创业实验》",
|
||||
subtitle: "全书购买"
|
||||
}
|
||||
}
|
||||
// 如果是找伙伴
|
||||
if (type === "match" || desc.includes("伙伴")) {
|
||||
return {
|
||||
title: "找伙伴匹配",
|
||||
subtitle: "功能服务"
|
||||
}
|
||||
}
|
||||
// 其他情况直接显示 description
|
||||
return {
|
||||
title: desc,
|
||||
subtitle: type === "section" ? "单章" : type === "fullbook" ? "全书" : "其他"
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 description,fallback 到原逻辑
|
||||
if (type === "section") return { title: `章节 ${p.productId || ""}`, subtitle: "单章购买" }
|
||||
if (type === "fullbook") return { title: "《一场Soul的创业实验》", subtitle: "全书购买" }
|
||||
if (type === "match") return { title: "找伙伴匹配", subtitle: "功能服务" }
|
||||
return { title: "未知商品", subtitle: type || "其他" }
|
||||
}
|
||||
|
||||
const stats = [
|
||||
@@ -137,26 +180,75 @@ export default function AdminDashboard() {
|
||||
.map((p) => {
|
||||
const referrer = p.referrerId && users.find((u: any) => u.id === p.referrerId)
|
||||
const inviteCode = p.referralCode || referrer?.referral_code || referrer?.nickname || p.referrerId?.slice(0, 8)
|
||||
const product = formatOrderProduct(p)
|
||||
const buyer = p.userNickname || users.find((u: any) => u.id === p.userId)?.nickname || "匿名用户"
|
||||
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex items-center justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30"
|
||||
className="flex items-start justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30 hover:border-[#38bdac]/30 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{productTypeLabel(p)}</p>
|
||||
<p className="text-xs text-gray-500">{new Date(p.createdAt).toLocaleString()}</p>
|
||||
{inviteCode && (
|
||||
<p className="text-xs text-gray-500 mt-0.5">邀请码: {inviteCode}</p>
|
||||
)}
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{/* 购买者头像 */}
|
||||
{p.userAvatar ? (
|
||||
<img
|
||||
src={p.userAvatar}
|
||||
alt={buyer}
|
||||
className="w-9 h-9 rounded-full object-cover flex-shrink-0 mt-0.5"
|
||||
onError={(e) => {
|
||||
// 头像加载失败时显示首字母
|
||||
e.currentTarget.style.display = 'none'
|
||||
e.currentTarget.nextElementSibling?.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={`w-9 h-9 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac] flex-shrink-0 mt-0.5 ${p.userAvatar ? 'hidden' : ''}`}
|
||||
>
|
||||
{buyer.charAt(0)}
|
||||
</div>
|
||||
|
||||
{/* 订单信息 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* 购买者 + 商品名称 */}
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm text-gray-300">{buyer}</span>
|
||||
<span className="text-gray-600">·</span>
|
||||
<span className="text-sm font-medium text-white truncate">{product.title}</span>
|
||||
</div>
|
||||
|
||||
{/* 商品类型 + 时间 */}
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span className="px-1.5 py-0.5 bg-gray-700/50 rounded">{product.subtitle}</span>
|
||||
<span>{new Date(p.createdAt).toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</span>
|
||||
</div>
|
||||
|
||||
{/* 邀请码 */}
|
||||
{inviteCode && (
|
||||
<p className="text-xs text-gray-600 mt-1">推荐: {inviteCode}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-[#38bdac]">+¥{p.amount}</p>
|
||||
<p className="text-xs text-gray-400">{p.paymentMethod || "微信支付"}</p>
|
||||
|
||||
{/* 金额 */}
|
||||
<div className="text-right ml-4 flex-shrink-0">
|
||||
<p className="text-sm font-bold text-[#38bdac]">+¥{Number(p.amount).toFixed(2)}</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{p.paymentMethod || "微信"}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{purchases.length === 0 && <p className="text-gray-500 text-center py-8">暂无订单数据</p>}
|
||||
{purchases.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<ShoppingBag className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500">暂无订单数据</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -23,7 +23,6 @@ interface User {
|
||||
is_admin?: boolean | number
|
||||
has_full_book?: boolean | number
|
||||
referral_code: string
|
||||
referred_by?: string | null
|
||||
earnings: number | string
|
||||
pending_earnings: number | string
|
||||
withdrawn_earnings?: number | string
|
||||
@@ -635,9 +634,6 @@ function UsersContent() {
|
||||
<code className="text-[#38bdac] text-xs bg-[#38bdac]/10 px-2 py-0.5 rounded">
|
||||
{user.referral_code || '-'}
|
||||
</code>
|
||||
{user.referred_by && (
|
||||
<div className="text-xs text-gray-500">来自: {user.referred_by.slice(0, 8)}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-400">
|
||||
|
||||
@@ -18,7 +18,6 @@ function mapRowToUser(r: any) {
|
||||
: (r.purchased_sections ? JSON.parse(String(r.purchased_sections)) : []) || [],
|
||||
hasFullBook: !!r.has_full_book,
|
||||
referralCode: r.referral_code || '',
|
||||
referredBy: r.referred_by || undefined,
|
||||
earnings: parseFloat(String(r.earnings || 0)),
|
||||
pendingEarnings: parseFloat(String(r.pending_earnings || 0)),
|
||||
withdrawnEarnings: parseFloat(String(r.withdrawn_earnings || 0)),
|
||||
@@ -40,7 +39,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, phone, nickname, password, is_admin, has_full_book, referral_code, referred_by, earnings, pending_earnings, withdrawn_earnings, referral_count, purchased_sections, created_at FROM users WHERE phone = ?',
|
||||
'SELECT id, phone, nickname, password, is_admin, has_full_book, referral_code, earnings, pending_earnings, withdrawn_earnings, referral_count, purchased_sections, created_at FROM users WHERE phone = ?',
|
||||
[String(phone).trim()]
|
||||
) as any[]
|
||||
|
||||
|
||||
152
app/api/cron/unbind-expired/route.ts
Normal file
152
app/api/cron/unbind-expired/route.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 自动解绑过期推荐关系定时任务 API
|
||||
* GET /api/cron/unbind-expired?secret=YOUR_SECRET
|
||||
*
|
||||
* 功能:
|
||||
* 1. 查询 status = 'active' 且 expiry_date < NOW() 且 purchase_count = 0 的绑定
|
||||
* 2. 批量更新为 status = 'expired'
|
||||
* 3. 更新推荐人的 referral_count(减少)
|
||||
*
|
||||
* 调用方式:
|
||||
* - 宝塔面板计划任务:每30分钟执行
|
||||
* curl "https://soul.quwanzhi.com/api/cron/unbind-expired?secret=soul_cron_unbind_2026"
|
||||
*
|
||||
* 规则说明:
|
||||
* - 只解绑「活跃状态 + 已过期 + 从未购买」的绑定关系
|
||||
* - 如果用户购买过(purchase_count > 0),即使过期也不解绑
|
||||
* - 这样可以保留有价值的推荐关系记录
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
// 触发解绑的密钥(防止误触)
|
||||
const CRON_SECRET = 'soul_cron_unbind_2026'
|
||||
|
||||
/**
|
||||
* 主函数:自动解绑过期推荐关系
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
|
||||
// 1. 验证密钥
|
||||
const { searchParams } = new URL(request.url)
|
||||
const secret = searchParams.get('secret')
|
||||
|
||||
if (secret !== CRON_SECRET) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '未授权访问'
|
||||
}, { status: 401 })
|
||||
}
|
||||
|
||||
console.log('[UnbindExpired] ========== 自动解绑任务开始 ==========')
|
||||
|
||||
try {
|
||||
// 2. 查找需要解绑的记录
|
||||
const expiredBindings = await query(`
|
||||
SELECT
|
||||
id,
|
||||
referrer_id,
|
||||
referee_id,
|
||||
binding_date,
|
||||
expiry_date,
|
||||
purchase_count,
|
||||
total_commission
|
||||
FROM referral_bindings
|
||||
WHERE status = 'active'
|
||||
AND expiry_date < NOW()
|
||||
AND purchase_count = 0
|
||||
ORDER BY expiry_date ASC
|
||||
`) as any[]
|
||||
|
||||
if (expiredBindings.length === 0) {
|
||||
console.log('[UnbindExpired] 无需解绑的记录')
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '无需解绑的记录',
|
||||
unbound: 0,
|
||||
duration: Date.now() - startTime
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`[UnbindExpired] 找到 ${expiredBindings.length} 条需要解绑的记录`)
|
||||
|
||||
// 3. 输出详细日志
|
||||
expiredBindings.forEach((binding, index) => {
|
||||
const bindingDate = new Date(binding.binding_date).toLocaleDateString('zh-CN')
|
||||
const expiryDate = new Date(binding.expiry_date).toLocaleDateString('zh-CN')
|
||||
const daysExpired = Math.floor((Date.now() - new Date(binding.expiry_date).getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
console.log(`[UnbindExpired] ${index + 1}. 用户 ${binding.referee_id}`)
|
||||
console.log(` 推荐人: ${binding.referrer_id}`)
|
||||
console.log(` 绑定时间: ${bindingDate}`)
|
||||
console.log(` 过期时间: ${expiryDate} (已过期 ${daysExpired} 天)`)
|
||||
console.log(` 购买次数: ${binding.purchase_count}`)
|
||||
console.log(` 累计佣金: ¥${(binding.total_commission || 0).toFixed(2)}`)
|
||||
})
|
||||
|
||||
// 4. 批量更新为 expired
|
||||
const ids = expiredBindings.map(b => b.id)
|
||||
const placeholders = ids.map(() => '?').join(',')
|
||||
|
||||
const result = await query(
|
||||
`UPDATE referral_bindings SET status = 'expired' WHERE id IN (${placeholders})`,
|
||||
ids
|
||||
) as any
|
||||
|
||||
console.log(`[UnbindExpired] 已成功解绑 ${result.affectedRows || expiredBindings.length} 条记录`)
|
||||
|
||||
// 5. 更新推荐人的 referral_count(减少)
|
||||
// 注意:这里需要按推荐人分组计算
|
||||
const referrerUpdates = new Map<string, number>()
|
||||
expiredBindings.forEach(binding => {
|
||||
const count = referrerUpdates.get(binding.referrer_id) || 0
|
||||
referrerUpdates.set(binding.referrer_id, count + 1)
|
||||
})
|
||||
|
||||
let updatedReferrers = 0
|
||||
for (const [referrerId, count] of referrerUpdates.entries()) {
|
||||
try {
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET referral_count = GREATEST(0, referral_count - ?)
|
||||
WHERE id = ?
|
||||
`, [count, referrerId])
|
||||
updatedReferrers++
|
||||
console.log(`[UnbindExpired] 更新推荐人 ${referrerId} 的 referral_count (-${count})`)
|
||||
} catch (err) {
|
||||
console.error(`[UnbindExpired] 更新推荐人 ${referrerId} 失败:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
console.log(`[UnbindExpired] 解绑完成: ${expiredBindings.length} 条记录,更新 ${updatedReferrers} 个推荐人`)
|
||||
console.log(`[UnbindExpired] ========== 任务结束 (耗时 ${duration}ms) ==========`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '自动解绑完成',
|
||||
unbound: expiredBindings.length,
|
||||
updatedReferrers,
|
||||
details: expiredBindings.map(b => ({
|
||||
refereeId: b.referee_id,
|
||||
referrerId: b.referrer_id,
|
||||
bindingDate: b.binding_date,
|
||||
expiryDate: b.expiry_date,
|
||||
daysExpired: Math.floor((Date.now() - new Date(b.expiry_date).getTime()) / (1000 * 60 * 60 * 24))
|
||||
})),
|
||||
duration
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[UnbindExpired] 解绑失败:', error)
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '自动解绑失败',
|
||||
detail: error instanceof Error ? error.message : String(error)
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* 用户绑定关系API
|
||||
* 获取指定用户的所有绑定用户列表
|
||||
*
|
||||
* 优先从referral_bindings表查询,同时兼容users表的referred_by字段
|
||||
* 从 referral_bindings 表查询(已弃用 users.referred_by)
|
||||
*/
|
||||
import { NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
@@ -59,25 +59,8 @@ export async function GET(request: Request) {
|
||||
referrals = bindingsReferrals
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Referrals] referral_bindings表查询失败,使用users表')
|
||||
}
|
||||
|
||||
// 2. 如果referral_bindings表没有数据,再从users表查询
|
||||
if (referrals.length === 0 && code) {
|
||||
referrals = await query(`
|
||||
SELECT
|
||||
id, nickname, avatar, phone, open_id,
|
||||
has_full_book, purchased_sections,
|
||||
created_at, updated_at,
|
||||
NULL as binding_status,
|
||||
NULL as binding_date,
|
||||
NULL as expiry_date,
|
||||
NULL as days_remaining,
|
||||
NULL as commission_amount
|
||||
FROM users
|
||||
WHERE referred_by = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [code]) as any[]
|
||||
console.log('[Referrals] referral_bindings表查询失败:', e)
|
||||
// 注意:已弃用 users.referred_by,只使用 referral_bindings
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
|
||||
@@ -128,13 +128,13 @@ export async function POST(request: NextRequest) {
|
||||
const userId = generateUserId()
|
||||
const referralCode = generateReferralCode(openId || phone || userId)
|
||||
|
||||
// 创建用户
|
||||
// 创建用户(注意:不再使用 referred_by 字段)
|
||||
await query(`
|
||||
INSERT INTO users (
|
||||
id, open_id, phone, nickname, password, wechat_id, avatar,
|
||||
referral_code, referred_by, has_full_book, is_admin,
|
||||
referral_code, has_full_book, is_admin,
|
||||
earnings, pending_earnings, referral_count
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, ?, 0, 0, 0)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, FALSE, ?, 0, 0, 0)
|
||||
`, [
|
||||
userId,
|
||||
openId || null,
|
||||
@@ -144,7 +144,6 @@ export async function POST(request: NextRequest) {
|
||||
wechatId || null,
|
||||
avatar || null,
|
||||
referralCode,
|
||||
referredBy || null,
|
||||
is_admin || false
|
||||
])
|
||||
|
||||
|
||||
@@ -360,13 +360,13 @@ async function processReferralCommission(buyerUserId: string, amount: number, or
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
|
||||
// 查找有效的推广绑定关系
|
||||
// 查找当前有效的推广绑定关系(新逻辑:购买时的绑定关系)
|
||||
const bindings = await query(`
|
||||
SELECT rb.id, rb.referrer_id, rb.referee_id, rb.expiry_date, rb.status
|
||||
SELECT rb.id, rb.referrer_id, rb.referee_id, rb.expiry_date, rb.status,
|
||||
rb.purchase_count, rb.total_commission
|
||||
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[]
|
||||
@@ -378,16 +378,32 @@ async function processReferralCommission(buyerUserId: string, amount: number, or
|
||||
|
||||
const binding = bindings[0]
|
||||
const referrerId = binding.referrer_id
|
||||
|
||||
// 检查是否已过期(过期也不分佣)
|
||||
const expiryDate = new Date(binding.expiry_date)
|
||||
const now = new Date()
|
||||
if (expiryDate < now) {
|
||||
console.log('[PayNotify] 绑定已过期,跳过分佣:', {
|
||||
buyerUserId,
|
||||
referrerId,
|
||||
expiryDate: expiryDate.toISOString()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 计算佣金(90%)
|
||||
// 计算佣金
|
||||
const commission = Math.round(amount * distributorShare * 100) / 100
|
||||
const newPurchaseCount = (binding.purchase_count || 0) + 1
|
||||
const newTotalCommission = (binding.total_commission || 0) + commission
|
||||
|
||||
console.log('[PayNotify] 处理分佣:', {
|
||||
referrerId,
|
||||
buyerUserId,
|
||||
amount,
|
||||
commission,
|
||||
shareRate: `${distributorShare * 100}%`
|
||||
shareRate: `${distributorShare * 100}%`,
|
||||
purchaseCount: `${binding.purchase_count || 0} -> ${newPurchaseCount}`,
|
||||
totalCommission: `${binding.total_commission || 0} -> ${newTotalCommission.toFixed(2)}`
|
||||
})
|
||||
|
||||
// 更新推广者的待结算收益
|
||||
@@ -397,17 +413,16 @@ async function processReferralCommission(buyerUserId: string, amount: number, or
|
||||
WHERE id = ?
|
||||
`, [commission, referrerId])
|
||||
|
||||
// 更新绑定记录状态为已转化
|
||||
// 更新绑定记录:累加购买次数和佣金,记录最后购买时间(保持 active 状态)
|
||||
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)
|
||||
SET last_purchase_date = CURRENT_TIMESTAMP,
|
||||
purchase_count = purchase_count + 1,
|
||||
total_commission = total_commission + ?
|
||||
WHERE id = ?
|
||||
`, [commission, orderSn, binding.id])
|
||||
`, [commission, binding.id])
|
||||
|
||||
console.log('[PayNotify] 分佣完成: 推广者', referrerId, '获得', commission, '元')
|
||||
console.log('[PayNotify] 分佣完成: 推广者', referrerId, '获得', commission, '元(第', newPurchaseCount, '次购买,累计', newTotalCommission.toFixed(2), '元)')
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PayNotify] 处理分佣失败:', error)
|
||||
|
||||
@@ -26,6 +26,9 @@ function rowToOrder(row: Record<string, unknown>) {
|
||||
referralCode: row.referral_code ?? null,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
// 新增:购买者信息
|
||||
userNickname: row.user_nickname ?? null,
|
||||
userAvatar: row.user_avatar ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,14 +40,22 @@ export async function GET(request: NextRequest) {
|
||||
let rows: Record<string, unknown>[] = []
|
||||
try {
|
||||
if (userId) {
|
||||
// 按用户查询订单(JOIN users 表获取用户信息)
|
||||
rows = (await query(
|
||||
"SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC",
|
||||
`SELECT o.*, u.nickname as user_nickname, u.avatar as user_avatar
|
||||
FROM orders o
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
WHERE o.user_id = ?
|
||||
ORDER BY o.created_at DESC`,
|
||||
[userId]
|
||||
)) as Record<string, unknown>[]
|
||||
} else {
|
||||
// 管理后台:无 userId 时返回全部订单
|
||||
// 管理后台:无 userId 时返回全部订单(JOIN users 表获取购买者昵称)
|
||||
rows = (await query(
|
||||
"SELECT * FROM orders ORDER BY created_at DESC"
|
||||
`SELECT o.*, u.nickname as user_nickname, u.avatar as user_avatar
|
||||
FROM orders o
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
ORDER BY o.created_at DESC`
|
||||
)) as Record<string, unknown>[]
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -86,9 +86,9 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// === ✅ 3. 分配佣金(如果有推荐人) ===
|
||||
try {
|
||||
// 查询用户的推荐人
|
||||
// 查询用户的推荐人(从 referral_bindings)
|
||||
const userRows = await query(`
|
||||
SELECT u.id, u.referred_by, rb.referrer_id, rb.status
|
||||
SELECT u.id, rb.referrer_id, rb.status
|
||||
FROM users u
|
||||
LEFT JOIN referral_bindings rb ON rb.referee_id = u.id AND rb.status = 'active' AND rb.expiry_date > NOW()
|
||||
WHERE u.id = ?
|
||||
|
||||
@@ -78,9 +78,9 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// === ✅ 3. 分配佣金(如果有推荐人) ===
|
||||
try {
|
||||
// 查询用户的推荐人
|
||||
// 查询用户的推荐人(从 referral_bindings)
|
||||
const userRows = await query(`
|
||||
SELECT u.id, u.referred_by, rb.referrer_id, rb.status
|
||||
SELECT u.id, rb.referrer_id, rb.status
|
||||
FROM users u
|
||||
LEFT JOIN referral_bindings rb ON rb.referee_id = u.id AND rb.status = 'active' AND rb.expiry_date > NOW()
|
||||
WHERE u.id = ?
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 检查用户是否存在
|
||||
const users = await query(
|
||||
'SELECT id, referred_by FROM users WHERE id = ? OR open_id = ?',
|
||||
'SELECT id FROM users WHERE id = ? OR open_id = ?',
|
||||
[effectiveUserId, openId || effectiveUserId]
|
||||
) as any[]
|
||||
|
||||
@@ -90,35 +90,28 @@ export async function POST(request: NextRequest) {
|
||||
ORDER BY binding_date DESC LIMIT 1
|
||||
`, [user.id]) as any[]
|
||||
|
||||
let action = 'new' // new=新绑定, renew=续期, takeover=抢夺
|
||||
let action = 'new' // new=新绑定, renew=续期, switch=立即切换
|
||||
let oldReferrerId = null
|
||||
|
||||
if (existingBindings.length > 0) {
|
||||
const existing = existingBindings[0]
|
||||
const expiryDate = new Date(existing.expiry_date)
|
||||
|
||||
// 同一个推荐人 - 续期
|
||||
// 同一个推荐人 - 续期(刷新30天)
|
||||
if (existing.referrer_id === referrer.id) {
|
||||
action = 'renew'
|
||||
}
|
||||
// 不同推荐人 - 检查是否可以抢夺
|
||||
else if (expiryDate < now) {
|
||||
// 已过期,可以被抢夺
|
||||
action = 'takeover'
|
||||
// 不同推荐人 - 立即切换(新逻辑:无条件切换)
|
||||
else {
|
||||
action = 'switch'
|
||||
oldReferrerId = existing.referrer_id
|
||||
|
||||
// 将旧绑定标记为过期
|
||||
// 将旧绑定标记为 cancelled(被切换)
|
||||
await query(
|
||||
"UPDATE referral_bindings SET status = 'expired' WHERE id = ?",
|
||||
"UPDATE referral_bindings SET status = 'cancelled' WHERE id = ?",
|
||||
[existing.id]
|
||||
)
|
||||
} else {
|
||||
// 未过期,不能被抢夺
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户已绑定其他推荐人,绑定有效期内无法更换',
|
||||
expiryDate: expiryDate.toISOString()
|
||||
}, { status: 400 })
|
||||
|
||||
console.log(`[Referral Bind] 立即切换: ${user.id}: ${oldReferrerId} -> ${referrer.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,9 +130,9 @@ export async function POST(request: NextRequest) {
|
||||
WHERE referee_id = ? AND referrer_id = ? AND status = 'active'
|
||||
`, [expiryDate, user.id, referrer.id])
|
||||
|
||||
console.log(`[Referral Bind] 续期: ${user.id} -> ${referrer.id}`)
|
||||
console.log(`[Referral Bind] 续期: ${user.id} -> ${referrer.id},新过期时间: ${expiryDate.toISOString()}`)
|
||||
} else {
|
||||
// 新绑定或抢夺
|
||||
// 新绑定或切换
|
||||
await query(`
|
||||
INSERT INTO referral_bindings (
|
||||
id, referrer_id, referee_id, referral_code, status, expiry_date, binding_date
|
||||
@@ -152,11 +145,7 @@ export async function POST(request: NextRequest) {
|
||||
status = 'active'
|
||||
`, [bindingId, referrer.id, user.id, referralCode, expiryDate])
|
||||
|
||||
// 更新用户的推荐人
|
||||
await query(
|
||||
'UPDATE users SET referred_by = ? WHERE id = ?',
|
||||
[referrer.id, user.id]
|
||||
)
|
||||
// 注意:不再更新 users.referred_by(已弃用,只使用 referral_bindings)
|
||||
|
||||
// 更新推荐人的推广数量(仅新绑定时)
|
||||
if (action === 'new') {
|
||||
@@ -164,17 +153,22 @@ export async function POST(request: NextRequest) {
|
||||
'UPDATE users SET referral_count = referral_count + 1 WHERE id = ?',
|
||||
[referrer.id]
|
||||
)
|
||||
console.log(`[Referral Bind] 新绑定: ${user.id} -> ${referrer.id}`)
|
||||
}
|
||||
|
||||
// 如果是抢夺,减少原推荐人的推广数量
|
||||
if (action === 'takeover' && oldReferrerId) {
|
||||
// 如果是立即切换,更新双方的推广数量
|
||||
if (action === 'switch' && 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}`)
|
||||
// 增加新推荐人的数量
|
||||
await query(
|
||||
'UPDATE users SET referral_count = referral_count + 1 WHERE id = ?',
|
||||
[referrer.id]
|
||||
)
|
||||
console.log(`[Referral Bind] 立即切换完成: ${user.id}: ${oldReferrerId} -> ${referrer.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,15 +182,23 @@ export async function POST(request: NextRequest) {
|
||||
// 访问日志表可能不存在,忽略错误
|
||||
}
|
||||
|
||||
const messages = {
|
||||
new: '绑定成功',
|
||||
renew: '绑定已续期',
|
||||
switch: '已切换推荐人'
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: action === 'renew' ? '绑定已续期' : (action === 'takeover' ? '绑定已更新' : '绑定成功'),
|
||||
message: messages[action] || '绑定成功',
|
||||
action,
|
||||
expiryDate: expiryDate.toISOString(),
|
||||
bindingDays,
|
||||
referrer: {
|
||||
id: referrer.id,
|
||||
nickname: referrer.nickname
|
||||
}
|
||||
},
|
||||
...(oldReferrerId && { oldReferrerId })
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
@@ -238,9 +240,9 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
// 查询用户的推荐关系
|
||||
// 查询用户是否存在
|
||||
const users = await query(
|
||||
'SELECT id, referred_by FROM users WHERE id = ?',
|
||||
'SELECT id FROM users WHERE id = ?',
|
||||
[userId]
|
||||
) as any[]
|
||||
|
||||
@@ -251,25 +253,50 @@ export async function GET(request: NextRequest) {
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
const user = users[0]
|
||||
|
||||
// 如果有推荐人,获取推荐人信息
|
||||
// 从 referral_bindings 查询当前有效的推荐人
|
||||
let referrer = null
|
||||
if (user.referred_by) {
|
||||
const referrers = await query(
|
||||
'SELECT id, nickname, avatar FROM users WHERE id = ?',
|
||||
[user.referred_by]
|
||||
) as any[]
|
||||
if (referrers.length > 0) {
|
||||
referrer = referrers[0]
|
||||
const activeBinding = await query(`
|
||||
SELECT
|
||||
rb.referrer_id,
|
||||
u.nickname,
|
||||
u.avatar,
|
||||
rb.expiry_date,
|
||||
rb.purchase_count
|
||||
FROM referral_bindings rb
|
||||
JOIN users u ON rb.referrer_id = u.id
|
||||
WHERE rb.referee_id = ?
|
||||
AND rb.status = 'active'
|
||||
AND rb.expiry_date > NOW()
|
||||
ORDER BY rb.binding_date DESC
|
||||
LIMIT 1
|
||||
`, [userId]) as any[]
|
||||
|
||||
if (activeBinding.length > 0) {
|
||||
referrer = {
|
||||
id: activeBinding[0].referrer_id,
|
||||
nickname: activeBinding[0].nickname,
|
||||
avatar: activeBinding[0].avatar,
|
||||
expiryDate: activeBinding[0].expiry_date,
|
||||
purchaseCount: activeBinding[0].purchase_count
|
||||
}
|
||||
}
|
||||
|
||||
// 获取该用户推荐的人
|
||||
const referees = await query(
|
||||
'SELECT id, nickname, avatar, created_at FROM users WHERE referred_by = ?',
|
||||
[userId]
|
||||
) as any[]
|
||||
// 获取该用户推荐的人(所有活跃绑定)
|
||||
const referees = await query(`
|
||||
SELECT
|
||||
u.id,
|
||||
u.nickname,
|
||||
u.avatar,
|
||||
rb.binding_date as created_at,
|
||||
rb.purchase_count,
|
||||
rb.total_commission
|
||||
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
|
||||
`, [userId]) as any[]
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -31,11 +31,15 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 获取分销配置
|
||||
let distributorShare = DISTRIBUTOR_SHARE
|
||||
let minWithdrawAmount = 10 // 默认最低提现金额
|
||||
try {
|
||||
const config = await getConfig('referral_config')
|
||||
if (config?.distributorShare) {
|
||||
distributorShare = config.distributorShare / 100
|
||||
}
|
||||
if (config?.minWithdrawAmount) {
|
||||
minWithdrawAmount = Number(config.minWithdrawAmount)
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
|
||||
// 1. 获取用户基本信息
|
||||
@@ -54,15 +58,15 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const user = users[0]
|
||||
|
||||
// 2. 获取绑定关系统计(从referral_bindings表)
|
||||
// 2. 获取绑定关系统计(新逻辑:基于 purchase_count)
|
||||
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
|
||||
SUM(CASE WHEN status = 'active' AND purchase_count > 0 THEN 1 ELSE 0 END) as converted,
|
||||
SUM(CASE WHEN status IN ('expired', 'cancelled') OR (status = 'active' AND expiry_date <= NOW()) THEN 1 ELSE 0 END) as expired
|
||||
FROM referral_bindings
|
||||
WHERE referrer_id = ?
|
||||
`, [userId]) as any[]
|
||||
@@ -125,15 +129,16 @@ export async function GET(request: NextRequest) {
|
||||
LIMIT 50
|
||||
`, [userId]) as any[]
|
||||
|
||||
// 6. 获取已转化用户列表
|
||||
// 6. 获取已转化用户列表(新逻辑:有购买记录的活跃绑定)
|
||||
const convertedBindings = await query(`
|
||||
SELECT rb.id, rb.referee_id, rb.conversion_date, rb.commission_amount,
|
||||
SELECT rb.id, rb.referee_id, rb.last_purchase_date as conversion_date,
|
||||
rb.total_commission as commission_amount, rb.purchase_count,
|
||||
u.nickname, u.avatar,
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM orders WHERE user_id = rb.referee_id AND status = 'paid') as order_amount
|
||||
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
|
||||
WHERE rb.referrer_id = ? AND rb.status = 'active' AND rb.purchase_count > 0
|
||||
ORDER BY rb.last_purchase_date DESC
|
||||
LIMIT 50
|
||||
`, [userId]) as any[]
|
||||
|
||||
@@ -148,21 +153,44 @@ export async function GET(request: NextRequest) {
|
||||
LIMIT 50
|
||||
`, [userId]) as any[]
|
||||
|
||||
// 7. 获取收益明细
|
||||
// 7. 获取待审核提现金额
|
||||
let pendingWithdrawAmount = 0
|
||||
try {
|
||||
const pendingResult = await query(`
|
||||
SELECT COALESCE(SUM(amount), 0) as pending_amount
|
||||
FROM withdrawals
|
||||
WHERE user_id = ? AND status = 'pending'
|
||||
`, [userId]) as any[]
|
||||
pendingWithdrawAmount = parseFloat(pendingResult[0]?.pending_amount || 0)
|
||||
} catch (e) {
|
||||
console.log('[ReferralData] 获取待审核提现金额失败:', e)
|
||||
}
|
||||
|
||||
// 8. 获取收益明细(包含买家信息和商品详情)
|
||||
let earningsDetails: any[] = []
|
||||
try {
|
||||
earningsDetails = await query(`
|
||||
SELECT o.id, o.order_sn, o.amount, o.product_type, o.pay_time,
|
||||
u.nickname as buyer_nickname,
|
||||
rb.commission_amount
|
||||
SELECT
|
||||
o.id,
|
||||
o.order_sn,
|
||||
o.amount,
|
||||
o.product_type,
|
||||
o.product_id,
|
||||
o.description,
|
||||
o.pay_time,
|
||||
u.nickname as buyer_nickname,
|
||||
u.avatar as buyer_avatar,
|
||||
rb.total_commission / rb.purchase_count as commission_per_order
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id
|
||||
JOIN referral_bindings rb ON o.user_id = rb.referee_id AND rb.referrer_id = ?
|
||||
WHERE o.status = 'paid'
|
||||
WHERE o.status = 'paid' AND o.referrer_id = ?
|
||||
ORDER BY o.pay_time DESC
|
||||
LIMIT 30
|
||||
`, [userId]) as any[]
|
||||
} catch (e) { /* 忽略 */ }
|
||||
`, [userId, userId]) as any[]
|
||||
} catch (e) {
|
||||
console.log('[ReferralData] 获取收益明细失败:', e)
|
||||
}
|
||||
|
||||
// 8. 计算预估收益
|
||||
const estimatedEarnings = paymentStats.totalAmount * distributorShare
|
||||
@@ -181,16 +209,28 @@ export async function GET(request: NextRequest) {
|
||||
expiredCount: bindingStats.expired,
|
||||
|
||||
// === 收益数据 ===
|
||||
// 已结算收益
|
||||
earnings: parseFloat(user.earnings) || 0,
|
||||
// 待结算收益
|
||||
pendingEarnings: parseFloat(user.pending_earnings) || 0,
|
||||
// 累计佣金总额(所有获得的佣金)
|
||||
totalCommission: Math.round((
|
||||
(parseFloat(user.earnings) || 0) +
|
||||
(parseFloat(user.pending_earnings) || 0) +
|
||||
(parseFloat(user.withdrawn_earnings) || 0)
|
||||
) * 100) / 100,
|
||||
// 可提现金额(pending_earnings)
|
||||
availableEarnings: parseFloat(user.pending_earnings) || 0,
|
||||
// 待审核金额(提现申请中的金额)
|
||||
pendingWithdrawAmount: Math.round(pendingWithdrawAmount * 100) / 100,
|
||||
// 已提现金额
|
||||
withdrawnEarnings: parseFloat(user.withdrawn_earnings) || 0,
|
||||
// 已结算收益(保留兼容)
|
||||
earnings: parseFloat(user.earnings) || 0,
|
||||
// 待结算收益(保留兼容)
|
||||
pendingEarnings: parseFloat(user.pending_earnings) || 0,
|
||||
// 预估总收益
|
||||
estimatedEarnings: Math.round(estimatedEarnings * 100) / 100,
|
||||
// 分成比例
|
||||
shareRate: Math.round(distributorShare * 100),
|
||||
// 最低提现金额(新增:给小程序使用)
|
||||
minWithdrawAmount,
|
||||
|
||||
// === 推荐码 ===
|
||||
referralCode: user.referral_code,
|
||||
@@ -225,6 +265,7 @@ export async function GET(request: NextRequest) {
|
||||
avatar: b.avatar,
|
||||
commission: parseFloat(b.commission_amount) || 0,
|
||||
orderAmount: parseFloat(b.order_amount) || 0,
|
||||
purchaseCount: parseInt(b.purchase_count) || 0,
|
||||
conversionDate: b.conversion_date,
|
||||
status: 'converted'
|
||||
})),
|
||||
@@ -244,9 +285,12 @@ export async function GET(request: NextRequest) {
|
||||
id: e.id,
|
||||
orderSn: e.order_sn,
|
||||
amount: parseFloat(e.amount),
|
||||
commission: parseFloat(e.commission_amount) || parseFloat(e.amount) * distributorShare,
|
||||
commission: parseFloat(e.commission_per_order) || parseFloat(e.amount) * distributorShare,
|
||||
productType: e.product_type,
|
||||
buyerNickname: e.buyer_nickname,
|
||||
productId: e.product_id,
|
||||
description: e.description,
|
||||
buyerNickname: e.buyer_nickname || '用户' + e.id?.toString().slice(-4),
|
||||
buyerAvatar: e.buyer_avatar,
|
||||
payTime: e.pay_time
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -60,26 +60,18 @@ export async function POST(req: NextRequest) {
|
||||
const userReferralCode = generateInviteCode(openid)
|
||||
const nickname = '用户' + openid.substr(-4)
|
||||
|
||||
// 处理推荐绑定
|
||||
let referredBy = null
|
||||
if (referralCode) {
|
||||
const referrers = await query('SELECT id FROM users WHERE referral_code = ?', [referralCode]) as any[]
|
||||
if (referrers.length > 0) {
|
||||
referredBy = referrers[0].id
|
||||
// 更新推荐人的推广数量
|
||||
await query('UPDATE users SET referral_count = referral_count + 1 WHERE id = ?', [referredBy])
|
||||
}
|
||||
}
|
||||
// 注意:推荐绑定逻辑已移至 /api/referral/bind,这里只创建用户
|
||||
// 如果有 referralCode,会在前端调用 /api/referral/bind 建立绑定关系
|
||||
|
||||
await query(`
|
||||
INSERT INTO users (
|
||||
id, open_id, session_key, nickname, avatar, referral_code, referred_by,
|
||||
id, open_id, session_key, nickname, avatar, referral_code,
|
||||
has_full_book, purchased_sections, earnings, pending_earnings, referral_count
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, FALSE, '[]', 0, 0, 0)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, FALSE, '[]', 0, 0, 0)
|
||||
`, [
|
||||
userId, openid, session_key, nickname,
|
||||
'https://picsum.photos/200/200?random=' + openid.substr(-2),
|
||||
userReferralCode, referredBy
|
||||
userReferralCode
|
||||
])
|
||||
|
||||
// 获取新创建的用户
|
||||
@@ -115,7 +107,6 @@ export async function POST(req: NextRequest) {
|
||||
phone: user.phone,
|
||||
wechatId: user.wechat_id,
|
||||
referralCode: user.referral_code,
|
||||
referredBy: user.referred_by,
|
||||
hasFullBook: user.has_full_book || false,
|
||||
purchasedSections: typeof user.purchased_sections === 'string'
|
||||
? JSON.parse(user.purchased_sections || '[]')
|
||||
|
||||
@@ -45,12 +45,12 @@ DEFAULT_SSH_PORT = int(os.environ.get("DEPLOY_SSH_PORT", "22022"))
|
||||
def get_cfg():
|
||||
"""获取基础部署配置(deploy 模式与 devlop 共用 SSH/宝塔)"""
|
||||
return {
|
||||
"host": os.environ.get("DEPLOY_HOST", "42.194.232.22"),
|
||||
"host": os.environ.get("DEPLOY_HOST", "43.139.27.93"),
|
||||
"user": os.environ.get("DEPLOY_USER", "root"),
|
||||
"password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"),
|
||||
"ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""),
|
||||
"project_path": os.environ.get("DEPLOY_PROJECT_PATH", DEPLOY_PROJECT_PATH),
|
||||
"panel_url": os.environ.get("BAOTA_PANEL_URL", "https://42.194.232.22:9988"),
|
||||
"panel_url": os.environ.get("BAOTA_PANEL_URL", "https://43.139.27.93:9988"),
|
||||
"api_key": os.environ.get("BAOTA_API_KEY", "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"),
|
||||
"pm2_name": os.environ.get("DEPLOY_PM2_APP", DEPLOY_PM2_APP),
|
||||
"site_url": os.environ.get("DEPLOY_SITE_URL", DEPLOY_SITE_URL),
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
App({
|
||||
globalData: {
|
||||
// API基础地址 - 连接真实后端
|
||||
baseUrl: 'https://soul.quwanzhi.com',
|
||||
// baseUrl: 'http://localhost:3006',
|
||||
// baseUrl: 'https://soul.quwanzhi.com',
|
||||
baseUrl: 'http://localhost:3006',
|
||||
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: 'wxb8bbb2b10dec74aa',
|
||||
|
||||
@@ -46,7 +46,11 @@ Page({
|
||||
|
||||
// 登录弹窗
|
||||
showLoginModal: false,
|
||||
isLoggingIn: false
|
||||
isLoggingIn: false,
|
||||
|
||||
// 修改昵称弹窗
|
||||
showNicknameModal: false,
|
||||
editingNickname: ''
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -142,30 +146,68 @@ Page({
|
||||
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调)
|
||||
async onChooseAvatar(e) {
|
||||
const avatarUrl = e.detail.avatarUrl
|
||||
if (!avatarUrl) return
|
||||
const tempAvatarUrl = e.detail.avatarUrl
|
||||
if (!tempAvatarUrl) return
|
||||
|
||||
wx.showLoading({ title: '更新中...', mask: true })
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 先上传图片到服务器
|
||||
console.log('[My] 开始上传头像:', tempAvatarUrl)
|
||||
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/upload',
|
||||
filePath: tempAvatarUrl,
|
||||
name: 'file',
|
||||
formData: {
|
||||
folder: 'avatars'
|
||||
},
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.success) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data.error || '上传失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
console.log('[My] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地头像
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.avatar = avatarUrl
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 同步到服务器
|
||||
// 4. 同步到服务器数据库
|
||||
await app.request('/api/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, avatar: avatarUrl }
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已获取', icon: 'success' })
|
||||
wx.showToast({ title: '头像更新成功', icon: 'success' })
|
||||
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.log('同步头像失败', e)
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
console.error('[My] 上传头像失败:', e)
|
||||
wx.showToast({
|
||||
title: e.message || '上传失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -193,38 +235,76 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 点击昵称修改(备用)
|
||||
// 打开昵称修改弹窗
|
||||
editNickname() {
|
||||
wx.showModal({
|
||||
title: '修改昵称',
|
||||
editable: true,
|
||||
placeholderText: '请输入昵称',
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const newNickname = res.content.trim()
|
||||
if (newNickname.length < 1 || newNickname.length > 20) {
|
||||
wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 更新本地
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.nickname = newNickname
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 同步到服务器
|
||||
try {
|
||||
await app.request('/api/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, nickname: newNickname }
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('同步昵称到服务器失败', e)
|
||||
}
|
||||
|
||||
wx.showToast({ title: '昵称已更新', icon: 'success' })
|
||||
this.setData({
|
||||
showNicknameModal: true,
|
||||
editingNickname: this.data.userInfo?.nickname || ''
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭昵称弹窗
|
||||
closeNicknameModal() {
|
||||
this.setData({
|
||||
showNicknameModal: false,
|
||||
editingNickname: ''
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
// 昵称输入实时更新
|
||||
onNicknameInput(e) {
|
||||
this.setData({
|
||||
editingNickname: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 昵称变化(微信自动填充时触发)
|
||||
onNicknameChange(e) {
|
||||
console.log('[My] 昵称已自动填充:', e.detail.value)
|
||||
this.setData({
|
||||
editingNickname: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 确认修改昵称
|
||||
async confirmNickname() {
|
||||
const newNickname = this.data.editingNickname.trim()
|
||||
|
||||
if (!newNickname) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (newNickname.length < 1 || newNickname.length > 20) {
|
||||
wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
this.closeNicknameModal()
|
||||
|
||||
// 显示加载
|
||||
wx.showLoading({ title: '更新中...' })
|
||||
|
||||
try {
|
||||
// 更新本地
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.nickname = newNickname
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 同步到服务器
|
||||
await app.request('/api/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, nickname: newNickname }
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '昵称已更新', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -244,6 +244,36 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改昵称弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
|
||||
<view class="modal-content nickname-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeNicknameModal">✕</view>
|
||||
<view class="modal-header">
|
||||
<text class="modal-icon">✏️</text>
|
||||
<text class="modal-title">修改昵称</text>
|
||||
</view>
|
||||
|
||||
<view class="nickname-input-wrap">
|
||||
<input
|
||||
class="nickname-input"
|
||||
type="nickname"
|
||||
value="{{editingNickname}}"
|
||||
placeholder="点击输入昵称"
|
||||
placeholder-class="nickname-placeholder"
|
||||
bindchange="onNicknameChange"
|
||||
bindinput="onNicknameInput"
|
||||
maxlength="20"
|
||||
/>
|
||||
<text class="input-tip">微信用户可点击自动填充昵称</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<view class="modal-btn modal-btn-cancel" bindtap="closeNicknameModal">取消</view>
|
||||
<view class="modal-btn modal-btn-confirm" bindtap="confirmNickname">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
|
||||
@@ -1103,3 +1103,84 @@
|
||||
font-size: 28rpx;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
/* ===== 修改昵称弹窗 ===== */
|
||||
.nickname-modal {
|
||||
width: 600rpx;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
font-size: 60rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nickname-input-wrap {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 2rpx solid rgba(56, 189, 172, 0.3);
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nickname-placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.input-tip {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(56, 189, 172, 0.6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.modal-btn-cancel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.modal-btn-confirm {
|
||||
background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(56, 189, 172, 0.3);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ Page({
|
||||
statusBarHeight: 44,
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
isLoading: false, // 加载状态
|
||||
|
||||
// === 核心可见数据 ===
|
||||
bindingCount: 0, // 绑定用户数(当前有效)
|
||||
@@ -23,10 +24,14 @@ Page({
|
||||
expiredCount: 0, // 已过期人数
|
||||
|
||||
// === 收益数据 ===
|
||||
earnings: 0, // 已结算收益
|
||||
pendingEarnings: 0, // 待结算收益
|
||||
totalCommission: 0, // 累计佣金总额(所有获得的佣金)
|
||||
availableEarnings: 0, // 可提现金额(未申请提现的佣金)
|
||||
pendingWithdrawAmount: 0, // 待审核金额(已申请提现但未审核)
|
||||
withdrawnEarnings: 0, // 已提现金额
|
||||
earnings: 0, // 已结算收益(保留兼容)
|
||||
pendingEarnings: 0, // 待结算收益(保留兼容)
|
||||
shareRate: 90, // 分成比例(90%)
|
||||
minWithdrawAmount: 10, // 最低提现金额(从后端获取)
|
||||
|
||||
// === 统计数据 ===
|
||||
referralCount: 0, // 总推荐人数
|
||||
@@ -70,6 +75,9 @@ Page({
|
||||
async initData() {
|
||||
const { isLoggedIn, userInfo } = app.globalData
|
||||
if (isLoggedIn && userInfo) {
|
||||
// 显示加载状态
|
||||
this.setData({ isLoading: true })
|
||||
|
||||
// 生成邀请码
|
||||
const referralCode = userInfo.referralCode || 'SOUL' + (userInfo.id || Date.now().toString(36)).toUpperCase().slice(-6)
|
||||
|
||||
@@ -126,8 +134,11 @@ Page({
|
||||
status: type,
|
||||
daysRemaining: user.daysRemaining || 0,
|
||||
bindingDate: user.bindingDate ? this.formatDate(user.bindingDate) : '--',
|
||||
expiryDate: user.expiryDate ? this.formatDate(user.expiryDate) : '--',
|
||||
commission: (user.commission || 0).toFixed(2),
|
||||
orderAmount: (user.orderAmount || 0).toFixed(2)
|
||||
orderAmount: (user.orderAmount || 0).toFixed(2),
|
||||
purchaseCount: user.purchaseCount || 0,
|
||||
conversionDate: user.conversionDate ? this.formatDate(user.conversionDate) : '--'
|
||||
}
|
||||
console.log('[Referral] 格式化用户:', formatted.nickname, formatted.status, formatted.daysRemaining + '天')
|
||||
return formatted
|
||||
@@ -150,10 +161,14 @@ Page({
|
||||
expiredCount,
|
||||
|
||||
// 收益数据 - 格式化为两位小数
|
||||
totalCommission: formatMoney(realData?.totalCommission || 0),
|
||||
availableEarnings: formatMoney(realData?.availableEarnings || 0),
|
||||
pendingWithdrawAmount: formatMoney(realData?.pendingWithdrawAmount || 0),
|
||||
withdrawnEarnings: formatMoney(realData?.withdrawnEarnings || 0),
|
||||
earnings: formatMoney(realData?.earnings || 0),
|
||||
pendingEarnings: formatMoney(realData?.pendingEarnings || 0),
|
||||
withdrawnEarnings: formatMoney(realData?.withdrawnEarnings || 0),
|
||||
shareRate: realData?.shareRate || 90,
|
||||
minWithdrawAmount: realData?.minWithdrawAmount || 10,
|
||||
|
||||
// 统计
|
||||
referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length,
|
||||
@@ -167,19 +182,33 @@ Page({
|
||||
totalBindings: activeBindings.length + convertedBindings.length + expiredBindings.length,
|
||||
|
||||
// 收益明细
|
||||
earningsDetails: (realData?.earningsDetails || []).map(item => ({
|
||||
id: item.id,
|
||||
productType: item.productType,
|
||||
commission: (item.commission || 0).toFixed(2),
|
||||
payTime: item.payTime ? this.formatDate(item.payTime) : '--',
|
||||
buyerNickname: item.buyerNickname
|
||||
}))
|
||||
earningsDetails: (realData?.earningsDetails || []).map(item => {
|
||||
// 解析商品描述,获取书名和章节
|
||||
const productInfo = this.parseProductDescription(item.description, item.productType)
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
productType: item.productType,
|
||||
bookTitle: productInfo.bookTitle,
|
||||
chapterTitle: productInfo.chapterTitle,
|
||||
commission: (item.commission || 0).toFixed(2),
|
||||
payTime: item.payTime ? this.formatDate(item.payTime) : '--',
|
||||
buyerNickname: item.buyerNickname || '用户',
|
||||
buyerAvatar: item.buyerAvatar
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log('[Referral] ✅ 数据设置完成')
|
||||
console.log('[Referral] - 绑定中:', this.data.bindingCount)
|
||||
console.log('[Referral] - 即将过期:', this.data.expiringCount)
|
||||
console.log('[Referral] - 收益:', this.data.earnings)
|
||||
|
||||
// 隐藏加载状态
|
||||
this.setData({ isLoading: false })
|
||||
} else {
|
||||
// 未登录时也隐藏loading
|
||||
this.setData({ isLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -550,23 +579,33 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 提现 - 直接到微信零钱(无门槛)
|
||||
// 提现 - 直接到微信零钱
|
||||
async handleWithdraw() {
|
||||
const pendingEarnings = parseFloat(this.data.pendingEarnings) || 0
|
||||
const availableEarnings = parseFloat(this.data.availableEarnings) || 0
|
||||
const minWithdrawAmount = this.data.minWithdrawAmount || 10
|
||||
|
||||
if (pendingEarnings <= 0) {
|
||||
if (availableEarnings <= 0) {
|
||||
wx.showToast({ title: '暂无可提现收益', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否达到最低提现金额
|
||||
if (availableEarnings < minWithdrawAmount) {
|
||||
wx.showToast({
|
||||
title: `满${minWithdrawAmount}元可提现`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 确认提现
|
||||
wx.showModal({
|
||||
title: '确认提现',
|
||||
content: `将提现 ¥${pendingEarnings.toFixed(2)} 到您的微信零钱`,
|
||||
content: `将提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱`,
|
||||
confirmText: '立即提现',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await this.doWithdraw(pendingEarnings)
|
||||
await this.doWithdraw(availableEarnings)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -668,6 +707,32 @@ Page({
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
// 解析商品描述,获取书名和章节
|
||||
parseProductDescription(description, productType) {
|
||||
if (!description) {
|
||||
return {
|
||||
bookTitle: '未知商品',
|
||||
chapterTitle: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配格式:《书名》- 章节名
|
||||
const match = description.match(/《(.+?)》(?:\s*-\s*(.+))?/)
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
bookTitle: match[1] || '未知书籍',
|
||||
chapterTitle: match[2] || (productType === 'fullbook' ? '全书购买' : '')
|
||||
}
|
||||
}
|
||||
|
||||
// 如果匹配失败,直接返回原始描述
|
||||
return {
|
||||
bookTitle: description.split('-')[0] || description,
|
||||
chapterTitle: description.split('-')[1] || ''
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
|
||||
@@ -18,7 +18,15 @@
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-overlay" wx:if="{{isLoading}}">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content {{isLoading ? 'content-loading' : ''}}">
|
||||
<!-- 过期提醒横幅 -->
|
||||
<view class="expiring-banner" wx:if="{{expiringCount > 0}}">
|
||||
<view class="banner-icon">
|
||||
@@ -40,17 +48,17 @@
|
||||
<image class="icon-wallet" src="/assets/icons/wallet.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="earnings-info">
|
||||
<text class="earnings-label">累计收益</text>
|
||||
<text class="earnings-label">累计佣金</text>
|
||||
<text class="commission-rate">{{shareRate}}% 返利</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="earnings-right">
|
||||
<text class="earnings-value">¥{{earnings}}</text>
|
||||
<text class="pending-text">待结算: ¥{{pendingEarnings}}</text>
|
||||
<text class="earnings-value">¥{{totalCommission}}</text>
|
||||
<text class="pending-text">待审核: ¥{{pendingWithdrawAmount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="withdraw-btn {{earnings < 10 ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
|
||||
{{earnings < 10 ? '满10元可提现' : '申请提现'}}
|
||||
<view class="withdraw-btn {{availableEarnings < minWithdrawAmount ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
|
||||
{{availableEarnings < minWithdrawAmount ? '满' + minWithdrawAmount + '元可提现' : '申请提现 ¥' + availableEarnings}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -147,11 +155,15 @@
|
||||
<view class="user-status">
|
||||
<block wx:if="{{item.status === 'converted'}}">
|
||||
<text class="status-amount">+¥{{item.commission}}</text>
|
||||
<text class="status-order">订单 ¥{{item.orderAmount}}</text>
|
||||
<text class="status-order">已购{{item.purchaseCount || 1}}次</text>
|
||||
</block>
|
||||
<block wx:elif="{{item.status === 'expired'}}">
|
||||
<text class="status-tag tag-gray">已过期</text>
|
||||
<text class="status-time">{{item.expiryDate}}</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="status-tag {{item.daysRemaining <= 3 ? 'tag-red' : item.daysRemaining <= 7 ? 'tag-orange' : 'tag-green'}}">
|
||||
{{item.status === 'expired' ? '已过期' : item.daysRemaining + '天'}}
|
||||
{{item.daysRemaining}}天
|
||||
</text>
|
||||
</block>
|
||||
</view>
|
||||
@@ -197,23 +209,38 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收益明细 - 移到分享按钮后面 -->
|
||||
<!-- 收益明细 - 增强版 -->
|
||||
<view class="earnings-detail-card" wx:if="{{earningsDetails.length > 0}}">
|
||||
<view class="detail-header">
|
||||
<text class="detail-title">收益明细</text>
|
||||
</view>
|
||||
<view class="detail-list">
|
||||
<view class="detail-item" wx:for="{{earningsDetails}}" wx:key="id">
|
||||
<view class="detail-left">
|
||||
<view class="detail-icon">
|
||||
<image class="icon-gift" src="/assets/icons/gift.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="detail-info">
|
||||
<text class="detail-type">{{item.productType === 'fullbook' ? '整本书购买' : '单节购买'}}</text>
|
||||
<text class="detail-time">{{item.payTime}}</text>
|
||||
<!-- 买家头像 -->
|
||||
<view class="detail-avatar-wrap">
|
||||
<image
|
||||
class="detail-avatar"
|
||||
wx:if="{{item.buyerAvatar}}"
|
||||
src="{{item.buyerAvatar}}"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="detail-avatar-text" wx:else>
|
||||
{{item.buyerNickname.charAt(0)}}
|
||||
</view>
|
||||
</view>
|
||||
<text class="detail-amount">+¥{{item.commission}}</text>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<view class="detail-content">
|
||||
<view class="detail-top">
|
||||
<text class="detail-buyer">{{item.buyerNickname}}</text>
|
||||
<text class="detail-amount">+¥{{item.commission}}</text>
|
||||
</view>
|
||||
<view class="detail-product">
|
||||
<text class="detail-book">{{item.bookTitle}}</text>
|
||||
<text class="detail-chapter" wx:if="{{item.chapterTitle}}"> - {{item.chapterTitle}}</text>
|
||||
</view>
|
||||
<text class="detail-time">{{item.payTime}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -265,7 +292,7 @@
|
||||
<text class="poster-stat-label">好友优惠</text>
|
||||
</view>
|
||||
<view class="poster-stat">
|
||||
<text class="poster-stat-value poster-stat-pink">90%</text>
|
||||
<text class="poster-stat-value poster-stat-pink">{{shareRate}}%</text>
|
||||
<text class="poster-stat-label">你的收益</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -216,3 +216,149 @@
|
||||
.empty-gift-icon { width: 64rpx; height: 64rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
|
||||
.empty-title { font-size: 30rpx; font-weight: 500; color: #fff; display: block; margin-bottom: 16rpx; }
|
||||
.empty-desc { font-size: 26rpx; color: rgba(255,255,255,0.6); display: block; line-height: 1.5; }
|
||||
|
||||
| ||||