更新管理后台布局,优化菜单项标签,新增支付配置项。同时,调整API响应字段命名,确保一致性,提升代码可读性和维护性。
This commit is contained in:
@@ -4,7 +4,7 @@ import type React from "react"
|
|||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { usePathname, useRouter } from "next/navigation"
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
import { LayoutDashboard, FileText, Users, CreditCard, Settings, LogOut, Wallet, Globe, BookOpen } from "lucide-react"
|
import { LayoutDashboard, FileText, Users, CreditCard, Settings, LogOut, Wallet, Globe, BookOpen, Banknote } from "lucide-react"
|
||||||
|
|
||||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
@@ -43,11 +43,12 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||||||
// 简化菜单:按功能归类,保留核心功能
|
// 简化菜单:按功能归类,保留核心功能
|
||||||
// PDF需求:分账管理、分销管理、订单管理三合一 → 交易中心
|
// PDF需求:分账管理、分销管理、订单管理三合一 → 交易中心
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ icon: LayoutDashboard, label: "数据概览1888", href: "/admin" },
|
{ icon: LayoutDashboard, label: "数据概览", href: "/admin" },
|
||||||
{ icon: BookOpen, label: "内容管理", href: "/admin/content" },
|
{ icon: BookOpen, label: "内容管理", href: "/admin/content" },
|
||||||
{ icon: Users, label: "用户管理", href: "/admin/users" },
|
{ icon: Users, label: "用户管理", href: "/admin/users" },
|
||||||
{ icon: Wallet, label: "交易中心", href: "/admin/distribution" }, // 合并:分销+订单+提现
|
{ icon: Wallet, label: "交易中心", href: "/admin/distribution" }, // 合并:分销+订单+提现
|
||||||
{ icon: CreditCard, label: "推广设置", href: "/admin/referral-settings" }, // 单独入口,集中管理分销配置
|
{ icon: CreditCard, label: "推广设置", href: "/admin/referral-settings" },
|
||||||
|
{ icon: Banknote, label: "支付配置", href: "/admin/payment" },
|
||||||
{ icon: Settings, label: "系统设置", href: "/admin/settings" },
|
{ icon: Settings, label: "系统设置", href: "/admin/settings" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -32,15 +32,15 @@ export async function GET(request: NextRequest) {
|
|||||||
id: w.id,
|
id: w.id,
|
||||||
amount: Number(w.amount),
|
amount: Number(w.amount),
|
||||||
package: w.package_info ?? '',
|
package: w.package_info ?? '',
|
||||||
created_at: w.created_at,
|
createdAt: w.created_at,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
list: items,
|
list: items,
|
||||||
mch_id: mchId,
|
mchId,
|
||||||
app_id: appId,
|
appId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export async function GET(request: NextRequest) {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
amount: Number(r.amount),
|
amount: Number(r.amount),
|
||||||
status: r.status,
|
status: r.status,
|
||||||
created_at: r.created_at,
|
createdAt: r.created_at,
|
||||||
processed_at: r.processed_at,
|
processedAt: r.processed_at,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@@ -157,12 +157,12 @@ Page({
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
amount: (item.amount || 0).toFixed(2),
|
amount: (item.amount || 0).toFixed(2),
|
||||||
package: item.package,
|
package: item.package,
|
||||||
created_at: item.created_at ? this.formatDateMy(item.created_at) : '--'
|
createdAt: (item.createdAt ?? item.created_at) ? this.formatDateMy(item.createdAt ?? item.created_at) : '--'
|
||||||
}))
|
}))
|
||||||
this.setData({
|
this.setData({
|
||||||
pendingConfirmList: list,
|
pendingConfirmList: list,
|
||||||
withdrawMchId: res.data.mch_id || '',
|
withdrawMchId: res.data.mchId ?? res.data.mch_id ?? '',
|
||||||
withdrawAppId: res.data.app_id || ''
|
withdrawAppId: res.data.appId ?? res.data.app_id ?? ''
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '' })
|
this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '' })
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
<view class="pending-confirm-item" wx:for="{{pendingConfirmList}}" wx:key="id">
|
<view class="pending-confirm-item" wx:for="{{pendingConfirmList}}" wx:key="id">
|
||||||
<view class="pending-confirm-info">
|
<view class="pending-confirm-info">
|
||||||
<text class="pending-confirm-amount">¥{{item.amount}}</text>
|
<text class="pending-confirm-amount">¥{{item.amount}}</text>
|
||||||
<text class="pending-confirm-time">{{item.created_at}}</text>
|
<text class="pending-confirm-time">{{item.createdAt}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="pending-confirm-btn" bindtap="confirmReceive" data-index="{{index}}">确认收款</view>
|
<view class="pending-confirm-btn" bindtap="confirmReceive" data-index="{{index}}">确认收款</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Page({
|
|||||||
amount: (item.amount != null ? item.amount : 0).toFixed(2),
|
amount: (item.amount != null ? item.amount : 0).toFixed(2),
|
||||||
status: this.statusText(item.status),
|
status: this.statusText(item.status),
|
||||||
statusRaw: item.status,
|
statusRaw: item.status,
|
||||||
created_at: item.created_at ? this.formatDate(item.created_at) : '--'
|
createdAt: (item.createdAt ?? item.created_at) ? this.formatDate(item.createdAt ?? item.created_at) : '--'
|
||||||
}))
|
}))
|
||||||
this.setData({ list, loading: false })
|
this.setData({ list, loading: false })
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<view class="item" wx:for="{{list}}" wx:key="id">
|
<view class="item" wx:for="{{list}}" wx:key="id">
|
||||||
<view class="item-left">
|
<view class="item-left">
|
||||||
<text class="amount">¥{{item.amount}}</text>
|
<text class="amount">¥{{item.amount}}</text>
|
||||||
<text class="time">{{item.created_at}}</text>
|
<text class="time">{{item.createdAt}}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="status status-{{item.statusRaw}}">{{item.status}}</text>
|
<text class="status status-{{item.statusRaw}}">{{item.status}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
# 开发环境:对接当前 Next 后端(与现网 API 路径完全一致,无缝切换)
|
# 对接后端 base URL(不改 API 路径,仅改此处即可切换 Next → Gin)
|
||||||
VITE_API_BASE_URL=http://localhost:3006
|
# VITE_API_BASE_URL=http://localhost:3006
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
# 对接后端 base URL(不改 API 路径,仅改此处即可切换 Next → Gin)
|
# 对接后端 base URL(不改 API 路径,仅改此处即可切换 Next → Gin)
|
||||||
VITE_API_BASE_URL=http://localhost:3006
|
# VITE_API_BASE_URL=http://localhost:3006
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
|||||||
2
soul-admin/.env.production
Normal file
2
soul-admin/.env.production
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 开发环境:对接当前 Next 后端(与现网 API 路径完全一致,无缝切换)
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-C8xDvmbL.css
vendored
Normal file
1
soul-admin/dist/assets/index-C8xDvmbL.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-Z7C0sgIG.css
vendored
1
soul-admin/dist/assets/index-Z7C0sgIG.css
vendored
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>管理后台 - Soul创业派对</title>
|
<title>管理后台 - Soul创业派对</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DX3SXTVU.js"></script>
|
<script type="module" crossorigin src="/assets/index-BV6dxvbB.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Z7C0sgIG.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-C8xDvmbL.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -39,20 +39,20 @@ interface UserDetail {
|
|||||||
phone?: string
|
phone?: string
|
||||||
nickname: string
|
nickname: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
wechat_id?: string
|
wechatId?: string
|
||||||
open_id?: string
|
openId?: string
|
||||||
referral_code: string
|
referralCode?: string
|
||||||
referred_by?: string
|
referredBy?: string
|
||||||
has_full_book?: boolean
|
hasFullBook?: boolean
|
||||||
is_admin?: boolean
|
isAdmin?: boolean
|
||||||
earnings?: number
|
earnings?: number
|
||||||
pending_earnings?: number
|
pendingEarnings?: number
|
||||||
referral_count?: number
|
referralCount?: number
|
||||||
created_at?: string
|
createdAt?: string
|
||||||
updated_at?: string
|
updatedAt?: string
|
||||||
tags?: string
|
tags?: string
|
||||||
ckb_tags?: string
|
ckbTags?: string
|
||||||
ckb_synced_at?: string
|
ckbSyncedAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserTrack {
|
interface UserTrack {
|
||||||
@@ -234,19 +234,19 @@ export function UserDetailModal({
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-bold text-white">{user.nickname}</h3>
|
<h3 className="text-lg font-bold text-white">{user.nickname}</h3>
|
||||||
{user.is_admin && (
|
{user.isAdmin && (
|
||||||
<Badge className="bg-purple-500/20 text-purple-400 border-0">管理员</Badge>
|
<Badge className="bg-purple-500/20 text-purple-400 border-0">管理员</Badge>
|
||||||
)}
|
)}
|
||||||
{user.has_full_book && (
|
{user.hasFullBook && (
|
||||||
<Badge className="bg-green-500/20 text-green-400 border-0">全书已购</Badge>
|
<Badge className="bg-green-500/20 text-green-400 border-0">全书已购</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 text-sm mt-1">
|
<p className="text-gray-400 text-sm mt-1">
|
||||||
{user.phone ? `📱 ${user.phone}` : '未绑定手机'}
|
{user.phone ? `📱 ${user.phone}` : '未绑定手机'}
|
||||||
{user.wechat_id && ` · 💬 ${user.wechat_id}`}
|
{user.wechatId && ` · 💬 ${user.wechatId}`}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
<p className="text-gray-500 text-xs mt-1">
|
||||||
ID: {user.id} · 推广码: {user.referral_code}
|
ID: {user.id} · 推广码: {user.referralCode ?? '-'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
@@ -295,18 +295,18 @@ export function UserDetailModal({
|
|||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
<p className="text-gray-400 text-sm">推荐人数</p>
|
<p className="text-gray-400 text-sm">推荐人数</p>
|
||||||
<p className="text-2xl font-bold text-white">{user.referral_count || 0}</p>
|
<p className="text-2xl font-bold text-white">{user.referralCount ?? 0}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
<p className="text-gray-400 text-sm">待提现</p>
|
<p className="text-gray-400 text-sm">待提现</p>
|
||||||
<p className="text-2xl font-bold text-yellow-400">
|
<p className="text-2xl font-bold text-yellow-400">
|
||||||
¥{(user.pending_earnings || 0).toFixed(2)}
|
¥{(user.pendingEarnings ?? 0).toFixed(2)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
<p className="text-gray-400 text-sm">创建时间</p>
|
<p className="text-gray-400 text-sm">创建时间</p>
|
||||||
<p className="text-sm text-white">
|
<p className="text-sm text-white">
|
||||||
{user.created_at ? new Date(user.created_at).toLocaleDateString() : '-'}
|
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,7 +336,7 @@ export function UserDetailModal({
|
|||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">同步状态:</span>
|
<span className="text-gray-500">同步状态:</span>
|
||||||
{user.ckb_synced_at ? (
|
{user.ckbSyncedAt ? (
|
||||||
<Badge className="bg-green-500/20 text-green-400 border-0 ml-1">已同步</Badge>
|
<Badge className="bg-green-500/20 text-green-400 border-0 ml-1">已同步</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge className="bg-gray-500/20 text-gray-400 border-0 ml-1">未同步</Badge>
|
<Badge className="bg-gray-500/20 text-gray-400 border-0 ml-1">未同步</Badge>
|
||||||
@@ -345,7 +345,7 @@ export function UserDetailModal({
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">最后同步:</span>
|
<span className="text-gray-500">最后同步:</span>
|
||||||
<span className="text-gray-300 ml-1">
|
<span className="text-gray-300 ml-1">
|
||||||
{user.ckb_synced_at ? new Date(user.ckb_synced_at).toLocaleString() : '-'}
|
{user.ckbSyncedAt ? new Date(user.ckbSyncedAt).toLocaleString() : '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,10 +119,10 @@ function buildTree(sections: SectionListItem[]): Part[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTxtToJson(content: string, fileName: string): { id: string; title: string; price: number; content?: string; is_free?: boolean }[] {
|
function parseTxtToJson(content: string, fileName: string): { id: string; title: string; price: number; content?: string; isFree?: boolean }[] {
|
||||||
const lines = content.split('\n')
|
const lines = content.split('\n')
|
||||||
const sections: { id: string; title: string; price: number; content?: string; is_free?: boolean }[] = []
|
const sections: { id: string; title: string; price: number; content?: string; isFree?: boolean }[] = []
|
||||||
let currentSection: { id: string; title: string; price: number; content?: string; is_free?: boolean } | null = null
|
let currentSection: { id: string; title: string; price: number; content?: string; isFree?: boolean } | null = null
|
||||||
let currentContent: string[] = []
|
let currentContent: string[] = []
|
||||||
let sectionIndex = 1
|
let sectionIndex = 1
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ function parseTxtToJson(content: string, fileName: string): { id: string; title:
|
|||||||
id: `import-${sectionIndex}`,
|
id: `import-${sectionIndex}`,
|
||||||
title: titleMatch[1].replace(/^#+\s*/, '').trim(),
|
title: titleMatch[1].replace(/^#+\s*/, '').trim(),
|
||||||
price: 1,
|
price: 1,
|
||||||
is_free: sectionIndex <= 3,
|
isFree: sectionIndex <= 3,
|
||||||
}
|
}
|
||||||
currentContent = []
|
currentContent = []
|
||||||
sectionIndex++
|
sectionIndex++
|
||||||
@@ -148,7 +148,7 @@ function parseTxtToJson(content: string, fileName: string): { id: string; title:
|
|||||||
id: `import-${sectionIndex}`,
|
id: `import-${sectionIndex}`,
|
||||||
title: fileName.replace(/\.(txt|md|markdown)$/i, ''),
|
title: fileName.replace(/\.(txt|md|markdown)$/i, ''),
|
||||||
price: 1,
|
price: 1,
|
||||||
is_free: true,
|
isFree: true,
|
||||||
}
|
}
|
||||||
currentContent.push(line)
|
currentContent.push(line)
|
||||||
sectionIndex++
|
sectionIndex++
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface UserRow {
|
|||||||
id: string
|
id: string
|
||||||
nickname?: string
|
nickname?: string
|
||||||
phone?: string
|
phone?: string
|
||||||
referral_code?: string
|
referralCode?: string
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ export function DashboardPage() {
|
|||||||
: undefined
|
: undefined
|
||||||
const inviteCode =
|
const inviteCode =
|
||||||
p.referralCode ||
|
p.referralCode ||
|
||||||
referrer?.referral_code ||
|
referrer?.referralCode ||
|
||||||
referrer?.nickname ||
|
referrer?.nickname ||
|
||||||
(p.referrerId ? String(p.referrerId).slice(0, 8) : '')
|
(p.referrerId ? String(p.referrerId).slice(0, 8) : '')
|
||||||
const product = formatOrderProduct(p)
|
const product = formatOrderProduct(p)
|
||||||
|
|||||||
@@ -42,24 +42,22 @@ interface DistributionOverview {
|
|||||||
|
|
||||||
interface Binding {
|
interface Binding {
|
||||||
id: string
|
id: string
|
||||||
referrer_id: string
|
referrerId: string
|
||||||
referrer_name?: string
|
referrerName?: string
|
||||||
referrer_code: string
|
referrerCode: string
|
||||||
referee_id: string
|
refereeId: string
|
||||||
referee_phone?: string
|
refereePhone?: string
|
||||||
referee_nickname?: string
|
refereeNickname?: string
|
||||||
bound_at: string
|
boundAt: string
|
||||||
expires_at: string
|
expiresAt: string
|
||||||
status: 'active' | 'converted' | 'expired' | 'cancelled'
|
status: 'active' | 'converted' | 'expired' | 'cancelled'
|
||||||
commission?: number
|
commission?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Withdrawal {
|
interface Withdrawal {
|
||||||
id: string
|
id: string
|
||||||
user_id?: string
|
|
||||||
userId?: string
|
userId?: string
|
||||||
user_name?: string
|
userName?: string
|
||||||
userNickname?: string
|
|
||||||
userPhone?: string
|
userPhone?: string
|
||||||
userAvatar?: string
|
userAvatar?: string
|
||||||
amount: number
|
amount: number
|
||||||
@@ -67,17 +65,15 @@ interface Withdrawal {
|
|||||||
account?: string
|
account?: string
|
||||||
name?: string
|
name?: string
|
||||||
status: string
|
status: string
|
||||||
created_at?: string
|
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
processedAt?: string
|
processedAt?: string
|
||||||
completed_at?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
nickname: string
|
nickname: string
|
||||||
phone: string
|
phone: string
|
||||||
referral_code: string
|
referralCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
@@ -165,7 +161,7 @@ export function DistributionPage() {
|
|||||||
userNickname: user?.nickname || order.userNickname || '未知用户',
|
userNickname: user?.nickname || order.userNickname || '未知用户',
|
||||||
userPhone: user?.phone || order.userPhone || '-',
|
userPhone: user?.phone || order.userPhone || '-',
|
||||||
referrerNickname: referrer?.nickname || null,
|
referrerNickname: referrer?.nickname || null,
|
||||||
referrerCode: referrer?.referral_code || null,
|
referrerCode: referrer?.referralCode ?? null,
|
||||||
type: order.productType || order.type,
|
type: order.productType || order.type,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -197,9 +193,6 @@ export function DistributionPage() {
|
|||||||
if (withdrawalsData?.success && withdrawalsData.withdrawals) {
|
if (withdrawalsData?.success && withdrawalsData.withdrawals) {
|
||||||
const formatted = withdrawalsData.withdrawals.map((w) => ({
|
const formatted = withdrawalsData.withdrawals.map((w) => ({
|
||||||
...w,
|
...w,
|
||||||
user_name: w.userNickname ?? w.user_name,
|
|
||||||
created_at: w.created_at ?? w.createdAt,
|
|
||||||
completed_at: w.processedAt ?? w.completed_at,
|
|
||||||
account: w.account ?? '未绑定微信号',
|
account: w.account ?? '未绑定微信号',
|
||||||
status:
|
status:
|
||||||
w.status === 'success' ? 'completed' : w.status === 'failed' ? 'rejected' : w.status,
|
w.status === 'success' ? 'completed' : w.status === 'failed' ? 'rejected' : w.status,
|
||||||
@@ -294,10 +287,10 @@ export function DistributionPage() {
|
|||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const term = searchTerm.toLowerCase()
|
const term = searchTerm.toLowerCase()
|
||||||
return (
|
return (
|
||||||
b.referee_nickname?.toLowerCase().includes(term) ||
|
b.refereeNickname?.toLowerCase().includes(term) ||
|
||||||
b.referee_phone?.includes(term) ||
|
b.refereePhone?.includes(term) ||
|
||||||
b.referrer_name?.toLowerCase().includes(term) ||
|
b.referrerName?.toLowerCase().includes(term) ||
|
||||||
b.referrer_code?.toLowerCase().includes(term)
|
b.referrerCode?.toLowerCase().includes(term)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -308,7 +301,7 @@ export function DistributionPage() {
|
|||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const term = searchTerm.toLowerCase()
|
const term = searchTerm.toLowerCase()
|
||||||
return (
|
return (
|
||||||
w.user_name?.toLowerCase().includes(term) || w.account?.toLowerCase().includes(term)
|
w.userName?.toLowerCase().includes(term) || w.account?.toLowerCase().includes(term)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -776,27 +769,27 @@ export function DistributionPage() {
|
|||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white font-medium">
|
<p className="text-white font-medium">
|
||||||
{binding.referee_nickname || '匿名用户'}
|
{binding.refereeNickname || '匿名用户'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 text-xs">{binding.referee_phone}</p>
|
<p className="text-gray-500 text-xs">{binding.refereePhone}</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white">{binding.referrer_name || '-'}</p>
|
<p className="text-white">{binding.referrerName || '-'}</p>
|
||||||
<p className="text-gray-500 text-xs font-mono">
|
<p className="text-gray-500 text-xs font-mono">
|
||||||
{binding.referrer_code}
|
{binding.referrerCode}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-gray-400">
|
<td className="p-4 text-gray-400">
|
||||||
{binding.bound_at
|
{binding.boundAt
|
||||||
? new Date(binding.bound_at).toLocaleDateString('zh-CN')
|
? new Date(binding.boundAt).toLocaleDateString('zh-CN')
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-gray-400">
|
<td className="p-4 text-gray-400">
|
||||||
{binding.expires_at
|
{binding.expiresAt
|
||||||
? new Date(binding.expires_at).toLocaleDateString('zh-CN')
|
? new Date(binding.expiresAt).toLocaleDateString('zh-CN')
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">{getStatusBadge(binding.status)}</td>
|
<td className="p-4">{getStatusBadge(binding.status)}</td>
|
||||||
@@ -874,11 +867,11 @@ export function DistributionPage() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-8 h-8 rounded-full bg-gray-600 flex items-center justify-center text-white text-sm font-medium">
|
<div className="w-8 h-8 rounded-full bg-gray-600 flex items-center justify-center text-white text-sm font-medium">
|
||||||
{(withdrawal.user_name || withdrawal.name || '?').slice(0, 1)}
|
{(withdrawal.userName || withdrawal.name || '?').slice(0, 1)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="text-white font-medium">
|
<p className="text-white font-medium">
|
||||||
{withdrawal.user_name || withdrawal.name}
|
{withdrawal.userName || withdrawal.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -907,10 +900,8 @@ export function DistributionPage() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-gray-400">
|
<td className="p-4 text-gray-400">
|
||||||
{(withdrawal.created_at || withdrawal.createdAt)
|
{withdrawal.createdAt
|
||||||
? new Date(
|
? new Date(withdrawal.createdAt).toLocaleString('zh-CN')
|
||||||
withdrawal.created_at || withdrawal.createdAt || '',
|
|
||||||
).toLocaleString('zh-CN')
|
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">{getStatusBadge(withdrawal.status)}</td>
|
<td className="p-4">{getStatusBadge(withdrawal.status)}</td>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function ReferralSettingsPage() {
|
|||||||
}
|
}
|
||||||
const body = {
|
const body = {
|
||||||
key: 'referral_config',
|
key: 'referral_config',
|
||||||
config: safeConfig,
|
value: safeConfig,
|
||||||
description: '分销 / 推广规则配置',
|
description: '分销 / 推广规则配置',
|
||||||
}
|
}
|
||||||
const res = await post<{ success?: boolean; error?: string }>('/api/db/config', body)
|
const res = await post<{ success?: boolean; error?: string }>('/api/db/config', body)
|
||||||
|
|||||||
@@ -115,13 +115,13 @@ function mergeFromConfigList(list: unknown[]): ReturnType<typeof parseConfigResp
|
|||||||
const out: ReturnType<typeof parseConfigResponse> = {}
|
const out: ReturnType<typeof parseConfigResponse> = {}
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
if (!item || typeof item !== 'object') continue
|
if (!item || typeof item !== 'object') continue
|
||||||
const row = item as { config_key?: string; config_value?: string }
|
const row = item as { configKey?: string; configValue?: string }
|
||||||
const key = row.config_key
|
const key = row.configKey
|
||||||
let val: unknown
|
let val: unknown
|
||||||
try {
|
try {
|
||||||
val = typeof row.config_value === 'string' ? JSON.parse(row.config_value) : row.config_value
|
val = typeof row.configValue === 'string' ? JSON.parse(row.configValue) : row.configValue
|
||||||
} catch {
|
} catch {
|
||||||
val = row.config_value
|
val = row.configValue
|
||||||
}
|
}
|
||||||
if (key === 'feature_config' && val && typeof val === 'object') out.features = val as Partial<FeatureConfig>
|
if (key === 'feature_config' && val && typeof val === 'object') out.features = val as Partial<FeatureConfig>
|
||||||
if (key === 'mp_config' && val && typeof val === 'object') out.mpConfig = val as Partial<MpConfig>
|
if (key === 'mp_config' && val && typeof val === 'object') out.mpConfig = val as Partial<MpConfig>
|
||||||
@@ -205,7 +205,6 @@ export function SettingsPage() {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
try {
|
try {
|
||||||
await post('/api/db/settings', localSettings).catch(() => {})
|
|
||||||
await post('/api/db/config', {
|
await post('/api/db/config', {
|
||||||
key: 'free_chapters',
|
key: 'free_chapters',
|
||||||
value: freeChapters,
|
value: freeChapters,
|
||||||
|
|||||||
@@ -37,20 +37,20 @@ import { get, del, post, put } from '@/api/client'
|
|||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
open_id?: string | null
|
openId?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
nickname: string
|
nickname: string
|
||||||
wechat_id?: string | null
|
wechatId?: string | null
|
||||||
avatar?: string | null
|
avatar?: string | null
|
||||||
is_admin?: boolean | number
|
isAdmin?: boolean | number
|
||||||
has_full_book?: boolean | number
|
hasFullBook?: boolean | number
|
||||||
referral_code: string
|
referralCode?: string
|
||||||
earnings: number | string
|
earnings: number | string
|
||||||
pending_earnings?: number | string
|
pendingEarnings?: number | string
|
||||||
withdrawn_earnings?: number | string
|
withdrawnEarnings?: number | string
|
||||||
referral_count: number
|
referralCount?: number
|
||||||
created_at: string
|
createdAt: string
|
||||||
updated_at?: string | null
|
updatedAt?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsersPage() {
|
export function UsersPage() {
|
||||||
@@ -77,8 +77,8 @@ export function UsersPage() {
|
|||||||
phone: '',
|
phone: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
password: '',
|
password: '',
|
||||||
is_admin: false,
|
isAdmin: false,
|
||||||
has_full_book: false,
|
hasFullBook: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
@@ -126,8 +126,8 @@ export function UsersPage() {
|
|||||||
phone: user.phone || '',
|
phone: user.phone || '',
|
||||||
nickname: user.nickname || '',
|
nickname: user.nickname || '',
|
||||||
password: '',
|
password: '',
|
||||||
is_admin: !!(user.is_admin ?? false),
|
isAdmin: !!(user.isAdmin ?? false),
|
||||||
has_full_book: !!(user.has_full_book ?? false),
|
hasFullBook: !!(user.hasFullBook ?? false),
|
||||||
})
|
})
|
||||||
setShowUserModal(true)
|
setShowUserModal(true)
|
||||||
}
|
}
|
||||||
@@ -138,8 +138,8 @@ export function UsersPage() {
|
|||||||
phone: '',
|
phone: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
password: '',
|
password: '',
|
||||||
is_admin: false,
|
isAdmin: false,
|
||||||
has_full_book: false,
|
hasFullBook: false,
|
||||||
})
|
})
|
||||||
setShowUserModal(true)
|
setShowUserModal(true)
|
||||||
}
|
}
|
||||||
@@ -155,8 +155,8 @@ export function UsersPage() {
|
|||||||
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', {
|
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', {
|
||||||
id: editingUser.id,
|
id: editingUser.id,
|
||||||
nickname: formData.nickname,
|
nickname: formData.nickname,
|
||||||
is_admin: formData.is_admin,
|
isAdmin: formData.isAdmin,
|
||||||
has_full_book: formData.has_full_book,
|
hasFullBook: formData.hasFullBook,
|
||||||
...(formData.password && { password: formData.password }),
|
...(formData.password && { password: formData.password }),
|
||||||
})
|
})
|
||||||
if (!data?.success) {
|
if (!data?.success) {
|
||||||
@@ -168,7 +168,7 @@ export function UsersPage() {
|
|||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
nickname: formData.nickname,
|
nickname: formData.nickname,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
is_admin: formData.is_admin,
|
isAdmin: formData.isAdmin,
|
||||||
})
|
})
|
||||||
if (!data?.success) {
|
if (!data?.success) {
|
||||||
alert('创建失败: ' + (data?.error || '未知错误'))
|
alert('创建失败: ' + (data?.error || '未知错误'))
|
||||||
@@ -329,15 +329,15 @@ export function UsersPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-gray-300">管理员权限</Label>
|
<Label className="text-gray-300">管理员权限</Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={formData.is_admin}
|
checked={formData.isAdmin}
|
||||||
onCheckedChange={(checked) => setFormData({ ...formData, is_admin: checked })}
|
onCheckedChange={(checked) => setFormData({ ...formData, isAdmin: checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-gray-300">已购全书</Label>
|
<Label className="text-gray-300">已购全书</Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={formData.has_full_book}
|
checked={formData.hasFullBook}
|
||||||
onCheckedChange={(checked) => setFormData({ ...formData, has_full_book: checked })}
|
onCheckedChange={(checked) => setFormData({ ...formData, hasFullBook: checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -559,19 +559,19 @@ export function UsersPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="font-medium text-white">{user.nickname}</p>
|
<p className="font-medium text-white">{user.nickname}</p>
|
||||||
{user.is_admin && (
|
{user.isAdmin && (
|
||||||
<Badge className="bg-purple-500/20 text-purple-400 hover:bg-purple-500/20 border-0 text-xs">
|
<Badge className="bg-purple-500/20 text-purple-400 hover:bg-purple-500/20 border-0 text-xs">
|
||||||
管理员
|
管理员
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{user.open_id && !user.id?.startsWith('user_') && (
|
{user.openId && !user.id?.startsWith('user_') && (
|
||||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0 text-xs">
|
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0 text-xs">
|
||||||
微信
|
微信
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 font-mono">
|
<p className="text-xs text-gray-500 font-mono">
|
||||||
{user.open_id ? user.open_id.slice(0, 12) + '...' : user.id?.slice(0, 12)}
|
{user.openId ? user.openId.slice(0, 12) + '...' : user.id?.slice(0, 12)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -584,27 +584,27 @@ export function UsersPage() {
|
|||||||
<span className="text-gray-300">{user.phone}</span>
|
<span className="text-gray-300">{user.phone}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{user.wechat_id && (
|
{user.wechatId && (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
<span className="text-gray-500">💬</span>
|
<span className="text-gray-500">💬</span>
|
||||||
<span className="text-gray-300">{user.wechat_id}</span>
|
<span className="text-gray-300">{user.wechatId}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{user.open_id && (
|
{user.openId && (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
<span className="text-gray-500">🔗</span>
|
<span className="text-gray-500">🔗</span>
|
||||||
<span className="text-gray-500 truncate max-w-[100px]" title={user.open_id}>
|
<span className="text-gray-500 truncate max-w-[100px]" title={user.openId}>
|
||||||
{user.open_id.slice(0, 12)}...
|
{user.openId.slice(0, 12)}...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!user.phone && !user.wechat_id && !user.open_id && (
|
{!user.phone && !user.wechatId && !user.openId && (
|
||||||
<span className="text-gray-600 text-xs">未绑定</span>
|
<span className="text-gray-600 text-xs">未绑定</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{user.has_full_book ? (
|
{user.hasFullBook ? (
|
||||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||||
全书已购
|
全书已购
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -619,9 +619,9 @@ export function UsersPage() {
|
|||||||
<div className="text-white font-medium">
|
<div className="text-white font-medium">
|
||||||
¥{parseFloat(String(user.earnings || 0)).toFixed(2)}
|
¥{parseFloat(String(user.earnings || 0)).toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
{parseFloat(String(user.pending_earnings || 0)) > 0 && (
|
{parseFloat(String(user.pendingEarnings || 0)) > 0 && (
|
||||||
<div className="text-xs text-yellow-400">
|
<div className="text-xs text-yellow-400">
|
||||||
待提现: ¥{parseFloat(String(user.pending_earnings || 0)).toFixed(2)}
|
待提现: ¥{parseFloat(String(user.pendingEarnings || 0)).toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@@ -632,17 +632,17 @@ export function UsersPage() {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Users className="w-3 h-3" />
|
<Users className="w-3 h-3" />
|
||||||
绑定{user.referral_count || 0}人
|
绑定{user.referralCount || 0}人
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<code className="text-[#38bdac] text-xs bg-[#38bdac]/10 px-2 py-0.5 rounded">
|
<code className="text-[#38bdac] text-xs bg-[#38bdac]/10 px-2 py-0.5 rounded">
|
||||||
{user.referral_code || '-'}
|
{user.referralCode || '-'}
|
||||||
</code>
|
</code>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-gray-400">
|
<TableCell className="text-gray-400">
|
||||||
{user.created_at ? new Date(user.created_at).toLocaleDateString() : '-'}
|
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import { get, put } from '@/api/client'
|
|||||||
|
|
||||||
interface Withdrawal {
|
interface Withdrawal {
|
||||||
id: string
|
id: string
|
||||||
user_id?: string
|
|
||||||
userId?: string
|
userId?: string
|
||||||
userNickname?: string
|
userName?: string
|
||||||
user_name?: string
|
|
||||||
userPhone?: string
|
userPhone?: string
|
||||||
userAvatar?: string
|
userAvatar?: string
|
||||||
referralCode?: string
|
referralCode?: string
|
||||||
@@ -20,9 +18,7 @@ interface Withdrawal {
|
|||||||
transactionId?: string
|
transactionId?: string
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
created_at?: string
|
|
||||||
processedAt?: string
|
processedAt?: string
|
||||||
completed_at?: string
|
|
||||||
method?: 'wechat' | 'alipay'
|
method?: 'wechat' | 'alipay'
|
||||||
account?: string
|
account?: string
|
||||||
name?: string
|
name?: string
|
||||||
@@ -66,11 +62,7 @@ export function WithdrawalsPage() {
|
|||||||
stats?: Partial<Stats>
|
stats?: Partial<Stats>
|
||||||
}>(`/api/admin/withdrawals?status=${filter}`)
|
}>(`/api/admin/withdrawals?status=${filter}`)
|
||||||
if (data?.success) {
|
if (data?.success) {
|
||||||
const list = (data.withdrawals || []).map((w) => ({
|
const list = data.withdrawals || []
|
||||||
...w,
|
|
||||||
createdAt: w.created_at ?? w.createdAt,
|
|
||||||
userNickname: w.user_name ?? w.userNickname,
|
|
||||||
}))
|
|
||||||
setWithdrawals(list)
|
setWithdrawals(list)
|
||||||
setStats({
|
setStats({
|
||||||
total: data.stats?.total ?? list.length,
|
total: data.stats?.total ?? list.length,
|
||||||
@@ -130,7 +122,7 @@ export function WithdrawalsPage() {
|
|||||||
try {
|
try {
|
||||||
const data = await put<{ success?: boolean; error?: string }>(
|
const data = await put<{ success?: boolean; error?: string }>(
|
||||||
'/api/admin/withdrawals',
|
'/api/admin/withdrawals',
|
||||||
{ id, action: 'reject', reason },
|
{ id, action: 'reject', errorMessage: reason },
|
||||||
)
|
)
|
||||||
if (data?.success) loadWithdrawals()
|
if (data?.success) loadWithdrawals()
|
||||||
else alert('操作失败: ' + (data?.error ?? ''))
|
else alert('操作失败: ' + (data?.error ?? ''))
|
||||||
@@ -310,27 +302,27 @@ export function WithdrawalsPage() {
|
|||||||
{withdrawals.map((w) => (
|
{withdrawals.map((w) => (
|
||||||
<tr key={w.id} className="hover:bg-[#0a1628] transition-colors">
|
<tr key={w.id} className="hover:bg-[#0a1628] transition-colors">
|
||||||
<td className="p-4 text-gray-400">
|
<td className="p-4 text-gray-400">
|
||||||
{new Date(w.created_at ?? w.createdAt ?? '').toLocaleString()}
|
{new Date(w.createdAt ?? '').toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{w.userAvatar ? (
|
{w.userAvatar ? (
|
||||||
<img
|
<img
|
||||||
src={w.userAvatar}
|
src={w.userAvatar}
|
||||||
alt={w.userNickname ?? w.user_name ?? ''}
|
alt={w.userName ?? ''}
|
||||||
className="w-8 h-8 rounded-full object-cover"
|
className="w-8 h-8 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac]">
|
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac]">
|
||||||
{(w.userNickname ?? w.user_name ?? '?').charAt(0)}
|
{(w.userName ?? '?').charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-white">
|
<p className="font-medium text-white">
|
||||||
{w.userNickname ?? w.user_name ?? '未知'}
|
{w.userName ?? '未知'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
{w.userPhone ?? w.referralCode ?? (w.user_id ?? w.userId ?? '').slice(0, 10)}
|
{w.userPhone ?? w.referralCode ?? (w.userId ?? '').slice(0, 10)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,9 +377,7 @@ export function WithdrawalsPage() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-gray-400">
|
<td className="p-4 text-gray-400">
|
||||||
{(w.processedAt ?? w.completed_at)
|
{w.processedAt ? new Date(w.processedAt).toLocaleString() : '-'}
|
||||||
? new Date(w.processedAt ?? w.completed_at ?? '').toLocaleString()
|
|
||||||
: '-'}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-right">
|
<td className="p-4 text-right">
|
||||||
{(w.status === 'pending' || w.status === 'pending_confirm') && (
|
{(w.status === 'pending' || w.status === 'pending_confirm') && (
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/layouts/adminlayout.tsx","./src/lib/utils.ts","./src/pages/chapters/chapterspage.tsx","./src/pages/content/contentpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/settingspage.tsx","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/withdrawals/withdrawalspage.tsx"],"version":"5.6.3"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/layouts/adminlayout.tsx","./src/lib/utils.ts","./src/pages/chapters/chapterspage.tsx","./src/pages/content/contentpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/not-found/notfoundpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/settingspage.tsx","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/withdrawals/withdrawalspage.tsx"],"version":"5.6.3"}
|
||||||
23
soul-api/.air.toml
Normal file
23
soul-api/.air.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Air 热重载配置:改 .go 后自动重新编译并重启
|
||||||
|
root = "."
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
# Windows 下用 .exe 避免系统弹出「选择应用打开 main」
|
||||||
|
[build]
|
||||||
|
bin = "./tmp/main.exe"
|
||||||
|
cmd = "go build -o ./tmp/main.exe ./cmd/server"
|
||||||
|
delay = 800
|
||||||
|
exclude_dir = ["tmp", "vendor"]
|
||||||
|
exclude_regex = ["_test\\.go$"]
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
log = "build-errors.log"
|
||||||
|
stop_on_error = true
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = true
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
12
soul-api/.env
Normal file
12
soul-api/.env
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 服务
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=debug
|
||||||
|
|
||||||
|
# 数据air库(与 Next 现网一致:腾讯云 CDB soul_miniprogram)
|
||||||
|
DB_DSN=cdb_outerroot:Zhiqun1984@tcp(56b4c23f6853c.gz.cdb.myqcloud.com:14413)/soul_miniprogram?charset=utf8mb4&parseTime=True
|
||||||
|
|
||||||
|
# 可选:管理端鉴权密钥(若用 JWT)
|
||||||
|
# JWT_SECRET=your-secret
|
||||||
|
|
||||||
|
# 可选:信任代理 IP(逗号分隔),部署在 Nginx 后时填写
|
||||||
|
# TRUSTED_PROXIES=127.0.0.1,::1
|
||||||
12
soul-api/.env.example
Normal file
12
soul-api/.env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 服务
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=debug
|
||||||
|
|
||||||
|
# 数据库(与 Next 现网一致:腾讯云 CDB soul_miniprogram)
|
||||||
|
DB_DSN=cdb_outerroot:Zhiqun1984@tcp(56b4c23f6853c.gz.cdb.myqcloud.com:14413)/soul_miniprogram?charset=utf8mb4&parseTime=True
|
||||||
|
|
||||||
|
# 可选:管理端鉴权密钥(若用 JWT)
|
||||||
|
# JWT_SECRET=your-secret
|
||||||
|
|
||||||
|
# 可选:信任代理 IP(逗号分隔),部署在 Nginx 后时填写
|
||||||
|
# TRUSTED_PROXIES=127.0.0.1,::1
|
||||||
9
soul-api/Makefile
Normal file
9
soul-api/Makefile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 开发:热重载(需先安装 air: go install github.com/air-verse/air@latest)
|
||||||
|
dev:
|
||||||
|
air
|
||||||
|
|
||||||
|
# 普通运行(无热重载)
|
||||||
|
run:
|
||||||
|
go run ./cmd/server
|
||||||
|
|
||||||
|
.PHONY: dev run
|
||||||
50
soul-api/cmd/server/main.go
Normal file
50
soul-api/cmd/server/main.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"soul-api/internal/config"
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("load config: ", err)
|
||||||
|
}
|
||||||
|
if err := database.Init(cfg.DBDSN); err != nil {
|
||||||
|
log.Fatal("database: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := router.Setup(cfg)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":" + cfg.Port,
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("soul-api listen on :%s (mode=%s)", cfg.Port, cfg.Mode)
|
||||||
|
log.Printf(" -> 访问地址: http://localhost:%s (健康检查: http://localhost:%s/health)", cfg.Port, cfg.Port)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatal("listen: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
log.Println("shutting down...")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatal("server shutdown: ", err)
|
||||||
|
}
|
||||||
|
log.Println("bye")
|
||||||
|
}
|
||||||
46
soul-api/go.mod
Normal file
46
soul-api/go.mod
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
module soul-api
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-contrib/cors v1.7.2
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/unrolled/secure v1.17.0
|
||||||
|
golang.org/x/time v0.8.0
|
||||||
|
gorm.io/driver/mysql v1.5.7
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
|
golang.org/x/net v0.25.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
116
soul-api/go.sum
Normal file
116
soul-api/go.sum
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||||
|
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
|
||||||
|
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||||
|
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
42
soul-api/internal/config/config.go
Normal file
42
soul-api/internal/config/config.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 应用配置(从环境变量读取)
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
Mode string
|
||||||
|
DBDSN string
|
||||||
|
TrustedProxies []string
|
||||||
|
CORSOrigins []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 加载配置,开发环境可读 .env
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
_ = godotenv.Load()
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
mode := os.Getenv("GIN_MODE")
|
||||||
|
if mode == "" {
|
||||||
|
mode = "debug"
|
||||||
|
}
|
||||||
|
dsn := os.Getenv("DB_DSN")
|
||||||
|
if dsn == "" {
|
||||||
|
dsn = "user:pass@tcp(127.0.0.1:3306)/soul?charset=utf8mb4&parseTime=True"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
Port: port,
|
||||||
|
Mode: mode,
|
||||||
|
DBDSN: dsn,
|
||||||
|
TrustedProxies: []string{"127.0.0.1", "::1"},
|
||||||
|
CORSOrigins: []string{"http://localhost:5174", "http://127.0.0.1:5174", "https://soul.quwanzhi.com"},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
26
soul-api/internal/database/database.go
Normal file
26
soul-api/internal/database/database.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *gorm.DB
|
||||||
|
|
||||||
|
// Init 使用 DSN 连接 MySQL,供 handler 通过 DB() 使用
|
||||||
|
func Init(dsn string) error {
|
||||||
|
var err error
|
||||||
|
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("database: connected")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB 返回全局 *gorm.DB,仅在 Init 成功后调用
|
||||||
|
func DB() *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
33
soul-api/internal/handler/admin.go
Normal file
33
soul-api/internal/handler/admin.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminCheck GET /api/admin 鉴权检查
|
||||||
|
func AdminCheck(c *gin.Context) {
|
||||||
|
// TODO: 校验 session/token,返回 success: true 或 401
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminLogin POST /api/admin 登录
|
||||||
|
func AdminLogin(c *gin.Context) {
|
||||||
|
var body struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: 校验用户名密码,写 session,返回 success
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminLogout POST /api/admin/logout
|
||||||
|
func AdminLogout(c *gin.Context) {
|
||||||
|
// TODO: 清除 session
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
101
soul-api/internal/handler/admin_chapters.go
Normal file
101
soul-api/internal/handler/admin_chapters.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminChaptersList GET /api/admin/chapters 从 chapters 表组树:part -> chapters -> sections
|
||||||
|
func AdminChaptersList(c *gin.Context) {
|
||||||
|
var list []model.Chapter
|
||||||
|
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"structure": []interface{}{}, "stats": nil}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type section struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
IsFree bool `json:"isFree"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
type chapter struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Sections []section `json:"sections"`
|
||||||
|
}
|
||||||
|
type part struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Chapters []chapter `json:"chapters"`
|
||||||
|
}
|
||||||
|
partMap := make(map[string]*part)
|
||||||
|
chapterMap := make(map[string]map[string]*chapter)
|
||||||
|
for _, row := range list {
|
||||||
|
if partMap[row.PartID] == nil {
|
||||||
|
partMap[row.PartID] = &part{ID: row.PartID, Title: row.PartTitle, Type: "part", Chapters: []chapter{}}
|
||||||
|
chapterMap[row.PartID] = make(map[string]*chapter)
|
||||||
|
}
|
||||||
|
p := partMap[row.PartID]
|
||||||
|
if chapterMap[row.PartID][row.ChapterID] == nil {
|
||||||
|
ch := chapter{ID: row.ChapterID, Title: row.ChapterTitle, Sections: []section{}}
|
||||||
|
p.Chapters = append(p.Chapters, ch)
|
||||||
|
chapterMap[row.PartID][row.ChapterID] = &p.Chapters[len(p.Chapters)-1]
|
||||||
|
}
|
||||||
|
ch := chapterMap[row.PartID][row.ChapterID]
|
||||||
|
price := 1.0
|
||||||
|
if row.Price != nil {
|
||||||
|
price = *row.Price
|
||||||
|
}
|
||||||
|
isFree := false
|
||||||
|
if row.IsFree != nil {
|
||||||
|
isFree = *row.IsFree
|
||||||
|
}
|
||||||
|
st := "published"
|
||||||
|
if row.Status != nil {
|
||||||
|
st = *row.Status
|
||||||
|
}
|
||||||
|
ch.Sections = append(ch.Sections, section{ID: row.ID, Title: row.SectionTitle, Price: price, IsFree: isFree, Status: st})
|
||||||
|
}
|
||||||
|
structure := make([]part, 0, len(partMap))
|
||||||
|
for _, p := range partMap {
|
||||||
|
structure = append(structure, *p)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
database.DB().Model(&model.Chapter{}).Count(&total)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": gin.H{"structure": structure, "stats": gin.H{"totalSections": total}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminChaptersAction POST/PUT/DELETE /api/admin/chapters
|
||||||
|
func AdminChaptersAction(c *gin.Context) {
|
||||||
|
var body struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Price *float64 `json:"price"`
|
||||||
|
IsFree *bool `json:"isFree"`
|
||||||
|
Status *string `json:"status"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db := database.DB()
|
||||||
|
if body.Action == "updatePrice" && body.ID != "" && body.Price != nil {
|
||||||
|
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("price", *body.Price)
|
||||||
|
}
|
||||||
|
if body.Action == "toggleFree" && body.ID != "" && body.IsFree != nil {
|
||||||
|
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("is_free", *body.IsFree)
|
||||||
|
}
|
||||||
|
if body.Action == "updateStatus" && body.ID != "" && body.Status != nil {
|
||||||
|
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("status", *body.Status)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
99
soul-api/internal/handler/admin_distribution.go
Normal file
99
soul-api/internal/handler/admin_distribution.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminDistributionOverview GET /api/admin/distribution/overview(全部使用 GORM,无 Raw SQL)
|
||||||
|
func AdminDistributionOverview(c *gin.Context) {
|
||||||
|
now := time.Now()
|
||||||
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
todayEnd := todayStart.Add(24 * time.Hour)
|
||||||
|
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||||
|
db := database.DB()
|
||||||
|
overview := gin.H{
|
||||||
|
"todayClicks": 0, "todayBindings": 0, "todayConversions": 0, "todayEarnings": 0,
|
||||||
|
"monthClicks": 0, "monthBindings": 0, "monthConversions": 0, "monthEarnings": 0,
|
||||||
|
"totalClicks": 0, "totalBindings": 0, "totalConversions": 0, "totalEarnings": 0,
|
||||||
|
"expiringBindings": 0, "pendingWithdrawals": 0, "pendingWithdrawAmount": 0,
|
||||||
|
"conversionRate": "0.00", "totalDistributors": 0, "activeDistributors": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单:仅用 Where + Count / Select(Sum) 参数化
|
||||||
|
var totalOrders int64
|
||||||
|
db.Model(&model.Order{}).Where("status = ?", "paid").Count(&totalOrders)
|
||||||
|
var totalAmount float64
|
||||||
|
db.Model(&model.Order{}).Where("status = ?", "paid").Select("COALESCE(SUM(amount),0)").Scan(&totalAmount)
|
||||||
|
var todayOrders int64
|
||||||
|
db.Model(&model.Order{}).Where("status = ? AND created_at >= ? AND created_at < ?", "paid", todayStart, todayEnd).Count(&todayOrders)
|
||||||
|
var todayAmount float64
|
||||||
|
db.Model(&model.Order{}).Where("status = ? AND created_at >= ? AND created_at < ?", "paid", todayStart, todayEnd).Select("COALESCE(SUM(amount),0)").Scan(&todayAmount)
|
||||||
|
var monthOrders int64
|
||||||
|
db.Model(&model.Order{}).Where("status = ? AND created_at >= ?", "paid", monthStart).Count(&monthOrders)
|
||||||
|
var monthAmount float64
|
||||||
|
db.Model(&model.Order{}).Where("status = ? AND created_at >= ?", "paid", monthStart).Select("COALESCE(SUM(amount),0)").Scan(&monthAmount)
|
||||||
|
overview["totalEarnings"] = totalAmount
|
||||||
|
overview["todayEarnings"] = todayAmount
|
||||||
|
overview["monthEarnings"] = monthAmount
|
||||||
|
|
||||||
|
// 绑定:全部 GORM Where
|
||||||
|
var totalBindings int64
|
||||||
|
db.Model(&model.ReferralBinding{}).Count(&totalBindings)
|
||||||
|
var converted int64
|
||||||
|
db.Model(&model.ReferralBinding{}).Where("status = ?", "converted").Count(&converted)
|
||||||
|
var todayBindings int64
|
||||||
|
db.Model(&model.ReferralBinding{}).Where("binding_date >= ? AND binding_date < ?", todayStart, todayEnd).Count(&todayBindings)
|
||||||
|
var todayConv int64
|
||||||
|
db.Model(&model.ReferralBinding{}).Where("status = ? AND binding_date >= ? AND binding_date < ?", "converted", todayStart, todayEnd).Count(&todayConv)
|
||||||
|
var monthBindings int64
|
||||||
|
db.Model(&model.ReferralBinding{}).Where("binding_date >= ?", monthStart).Count(&monthBindings)
|
||||||
|
var monthConv int64
|
||||||
|
db.Model(&model.ReferralBinding{}).Where("status = ? AND binding_date >= ?", "converted", monthStart).Count(&monthConv)
|
||||||
|
expiringEnd := now.Add(7 * 24 * time.Hour)
|
||||||
|
var expiring int64
|
||||||
|
db.Model(&model.ReferralBinding{}).Where("status = ? AND expiry_date > ? AND expiry_date <= ?", "active", now, expiringEnd).Count(&expiring)
|
||||||
|
overview["totalBindings"] = totalBindings
|
||||||
|
overview["totalConversions"] = converted
|
||||||
|
overview["todayBindings"] = todayBindings
|
||||||
|
overview["todayConversions"] = todayConv
|
||||||
|
overview["monthBindings"] = monthBindings
|
||||||
|
overview["monthConversions"] = monthConv
|
||||||
|
overview["expiringBindings"] = expiring
|
||||||
|
|
||||||
|
// 访问数
|
||||||
|
var visitTotal int64
|
||||||
|
db.Model(&model.ReferralVisit{}).Count(&visitTotal)
|
||||||
|
overview["totalClicks"] = visitTotal
|
||||||
|
if visitTotal > 0 && converted > 0 {
|
||||||
|
overview["conversionRate"] = formatPercent(float64(converted)/float64(visitTotal)*100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提现待处理
|
||||||
|
var pendCount int64
|
||||||
|
db.Model(&model.Withdrawal{}).Where("status = ?", "pending").Count(&pendCount)
|
||||||
|
var pendSum float64
|
||||||
|
db.Model(&model.Withdrawal{}).Where("status = ?", "pending").Select("COALESCE(SUM(amount),0)").Scan(&pendSum)
|
||||||
|
overview["pendingWithdrawals"] = pendCount
|
||||||
|
overview["pendingWithdrawAmount"] = pendSum
|
||||||
|
|
||||||
|
// 分销商
|
||||||
|
var distTotal int64
|
||||||
|
db.Model(&model.User{}).Where("referral_code IS NOT NULL AND referral_code != ?", "").Count(&distTotal)
|
||||||
|
var distActive int64
|
||||||
|
db.Model(&model.User{}).Where("referral_code IS NOT NULL AND referral_code != ? AND earnings > ?", "", 0).Count(&distActive)
|
||||||
|
overview["totalDistributors"] = distTotal
|
||||||
|
overview["activeDistributors"] = distActive
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "overview": overview})
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPercent(v float64) string {
|
||||||
|
return fmt.Sprintf("%.2f", v) + "%"
|
||||||
|
}
|
||||||
22
soul-api/internal/handler/admin_extra.go
Normal file
22
soul-api/internal/handler/admin_extra.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminContent GET/POST/PUT/DELETE /api/admin/content
|
||||||
|
func AdminContent(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPayment GET/POST/PUT/DELETE /api/admin/payment
|
||||||
|
func AdminPayment(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminReferral GET/POST/PUT/DELETE /api/admin/referral
|
||||||
|
func AdminReferral(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
119
soul-api/internal/handler/admin_withdrawals.go
Normal file
119
soul-api/internal/handler/admin_withdrawals.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminWithdrawalsList GET /api/admin/withdrawals
|
||||||
|
func AdminWithdrawalsList(c *gin.Context) {
|
||||||
|
statusFilter := c.Query("status")
|
||||||
|
var list []model.Withdrawal
|
||||||
|
q := database.DB().Order("created_at DESC").Limit(100)
|
||||||
|
if statusFilter != "" {
|
||||||
|
q = q.Where("status = ?", statusFilter)
|
||||||
|
}
|
||||||
|
if err := q.Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "withdrawals": []interface{}{}, "stats": gin.H{"total": 0}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userIds := make([]string, 0, len(list))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, w := range list {
|
||||||
|
if !seen[w.UserID] {
|
||||||
|
seen[w.UserID] = true
|
||||||
|
userIds = append(userIds, w.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var users []model.User
|
||||||
|
if len(userIds) > 0 {
|
||||||
|
database.DB().Where("id IN ?", userIds).Find(&users)
|
||||||
|
}
|
||||||
|
userMap := make(map[string]*model.User)
|
||||||
|
for i := range users {
|
||||||
|
userMap[users[i].ID] = &users[i]
|
||||||
|
}
|
||||||
|
withdrawals := make([]gin.H, 0, len(list))
|
||||||
|
for _, w := range list {
|
||||||
|
u := userMap[w.UserID]
|
||||||
|
userName := "未知用户"
|
||||||
|
var userAvatar *string
|
||||||
|
account := "未绑定微信号"
|
||||||
|
if w.WechatID != nil && *w.WechatID != "" {
|
||||||
|
account = *w.WechatID
|
||||||
|
}
|
||||||
|
if u != nil {
|
||||||
|
if u.Nickname != nil {
|
||||||
|
userName = *u.Nickname
|
||||||
|
}
|
||||||
|
userAvatar = u.Avatar
|
||||||
|
if u.WechatID != nil && *u.WechatID != "" {
|
||||||
|
account = *u.WechatID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st := "pending"
|
||||||
|
if w.Status != nil {
|
||||||
|
st = *w.Status
|
||||||
|
if st == "success" {
|
||||||
|
st = "completed"
|
||||||
|
} else if st == "failed" {
|
||||||
|
st = "rejected"
|
||||||
|
} else if st == "pending_confirm" {
|
||||||
|
st = "pending_confirm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withdrawals = append(withdrawals, gin.H{
|
||||||
|
"id": w.ID, "userId": w.UserID, "userName": userName, "userAvatar": userAvatar,
|
||||||
|
"amount": w.Amount, "status": st, "createdAt": w.CreatedAt,
|
||||||
|
"method": "wechat", "account": account,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "withdrawals": withdrawals, "stats": gin.H{"total": len(withdrawals)}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminWithdrawalsAction PUT /api/admin/withdrawals 审核/打款
|
||||||
|
func AdminWithdrawalsAction(c *gin.Context) {
|
||||||
|
var body struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
ErrorMessage string `json:"errorMessage"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reason := body.ErrorMessage
|
||||||
|
if reason == "" {
|
||||||
|
reason = body.Reason
|
||||||
|
}
|
||||||
|
if reason == "" && body.Action == "reject" {
|
||||||
|
reason = "管理员拒绝"
|
||||||
|
}
|
||||||
|
var newStatus string
|
||||||
|
switch body.Action {
|
||||||
|
case "approve":
|
||||||
|
newStatus = "success"
|
||||||
|
case "reject":
|
||||||
|
newStatus = "failed"
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "action 须为 approve 或 reject"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
err := database.DB().Model(&model.Withdrawal{}).Where("id = ?", body.ID).Updates(map[string]interface{}{
|
||||||
|
"status": newStatus,
|
||||||
|
"error_message": reason,
|
||||||
|
"processed_at": now,
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "操作成功"})
|
||||||
|
}
|
||||||
17
soul-api/internal/handler/auth.go
Normal file
17
soul-api/internal/handler/auth.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthLogin POST /api/auth/login
|
||||||
|
func AuthLogin(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResetPassword POST /api/auth/reset-password
|
||||||
|
func AuthResetPassword(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
160
soul-api/internal/handler/book.go
Normal file
160
soul-api/internal/handler/book.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BookAllChapters GET /api/book/all-chapters 返回所有章节(列表,来自 chapters 表)
|
||||||
|
func BookAllChapters(c *gin.Context) {
|
||||||
|
var list []model.Chapter
|
||||||
|
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookChapterByID GET /api/book/chapter/:id
|
||||||
|
func BookChapterByID(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ch model.Chapter
|
||||||
|
if err := database.DB().Where("id = ?", id).First(&ch).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "章节不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": ch})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookChapters GET/POST/PUT/DELETE /api/book/chapters(与 app/api/book/chapters 一致,用 GORM)
|
||||||
|
func BookChapters(c *gin.Context) {
|
||||||
|
db := database.DB()
|
||||||
|
switch c.Request.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
partId := c.Query("partId")
|
||||||
|
status := c.Query("status")
|
||||||
|
if status == "" {
|
||||||
|
status = "published"
|
||||||
|
}
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "100"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 500 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
q := db.Model(&model.Chapter{})
|
||||||
|
if partId != "" {
|
||||||
|
q = q.Where("part_id = ?", partId)
|
||||||
|
}
|
||||||
|
if status != "" && status != "all" {
|
||||||
|
q = q.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
q.Count(&total)
|
||||||
|
var list []model.Chapter
|
||||||
|
q.Order("sort_order ASC, id ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list)
|
||||||
|
totalPages := int(total) / pageSize
|
||||||
|
if int(total)%pageSize > 0 {
|
||||||
|
totalPages++
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": gin.H{
|
||||||
|
"list": list, "total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case http.MethodPost:
|
||||||
|
var body model.Chapter
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求体无效"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.ID == "" || body.PartID == "" || body.ChapterID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少必要字段 id/partId/chapterId"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.Create(&body).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
|
||||||
|
return
|
||||||
|
case http.MethodPut:
|
||||||
|
var body model.Chapter
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(map[string]interface{}{
|
||||||
|
"part_title": body.PartTitle, "chapter_title": body.ChapterTitle, "section_title": body.SectionTitle,
|
||||||
|
"content": body.Content, "word_count": body.WordCount, "is_free": body.IsFree, "price": body.Price,
|
||||||
|
"sort_order": body.SortOrder, "status": body.Status,
|
||||||
|
}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
return
|
||||||
|
case http.MethodDelete:
|
||||||
|
id := c.Query("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.Where("id = ?", id).Delete(&model.Chapter{}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookHot GET /api/book/hot
|
||||||
|
func BookHot(c *gin.Context) {
|
||||||
|
var list []model.Chapter
|
||||||
|
database.DB().Order("sort_order ASC, id ASC").Limit(10).Find(&list)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookLatestChapters GET /api/book/latest-chapters
|
||||||
|
func BookLatestChapters(c *gin.Context) {
|
||||||
|
var list []model.Chapter
|
||||||
|
database.DB().Order("updated_at DESC, id ASC").Limit(20).Find(&list)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookSearch GET /api/book/search 同 /api/search,由 SearchGet 处理
|
||||||
|
func BookSearch(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookStats GET /api/book/stats
|
||||||
|
func BookStats(c *gin.Context) {
|
||||||
|
var total int64
|
||||||
|
database.DB().Model(&model.Chapter{}).Count(&total)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalChapters": total}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookSync GET/POST /api/book/sync
|
||||||
|
func BookSync(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步由 DB 维护"})
|
||||||
|
}
|
||||||
22
soul-api/internal/handler/ckb.go
Normal file
22
soul-api/internal/handler/ckb.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CKBJoin POST /api/ckb/join
|
||||||
|
func CKBJoin(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CKBMatch POST /api/ckb/match
|
||||||
|
func CKBMatch(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CKBSync GET/POST /api/ckb/sync
|
||||||
|
func CKBSync(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
63
soul-api/internal/handler/config.go
Normal file
63
soul-api/internal/handler/config.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetConfig GET /api/config 从 system_config 读取并合并(与 app/api/config 结构一致)
|
||||||
|
func GetConfig(c *gin.Context) {
|
||||||
|
var list []model.SystemConfig
|
||||||
|
if err := database.DB().Order("config_key ASC").Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true, "paymentMethods": gin.H{}, "liveQRCodes": []interface{}{},
|
||||||
|
"siteConfig": gin.H{}, "menuConfig": gin.H{}, "pageConfig": gin.H{},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := gin.H{
|
||||||
|
"success": true, "paymentMethods": gin.H{}, "liveQRCodes": []interface{}{},
|
||||||
|
"siteConfig": gin.H{}, "menuConfig": gin.H{}, "pageConfig": gin.H{},
|
||||||
|
"authorInfo": gin.H{}, "marketing": gin.H{}, "system": gin.H{},
|
||||||
|
}
|
||||||
|
for _, row := range list {
|
||||||
|
var val interface{}
|
||||||
|
_ = json.Unmarshal(row.ConfigValue, &val)
|
||||||
|
switch row.ConfigKey {
|
||||||
|
case "site_config", "siteConfig":
|
||||||
|
if m, ok := val.(map[string]interface{}); ok {
|
||||||
|
out["siteConfig"] = m
|
||||||
|
}
|
||||||
|
case "menu_config", "menuConfig":
|
||||||
|
out["menuConfig"] = val
|
||||||
|
case "page_config", "pageConfig":
|
||||||
|
if m, ok := val.(map[string]interface{}); ok {
|
||||||
|
out["pageConfig"] = m
|
||||||
|
}
|
||||||
|
case "payment_methods", "paymentMethods":
|
||||||
|
if m, ok := val.(map[string]interface{}); ok {
|
||||||
|
out["paymentMethods"] = m
|
||||||
|
}
|
||||||
|
case "live_qr_codes", "liveQRCodes":
|
||||||
|
out["liveQRCodes"] = val
|
||||||
|
case "author_info", "authorInfo":
|
||||||
|
if m, ok := val.(map[string]interface{}); ok {
|
||||||
|
out["authorInfo"] = m
|
||||||
|
}
|
||||||
|
case "marketing":
|
||||||
|
if m, ok := val.(map[string]interface{}); ok {
|
||||||
|
out["marketing"] = m
|
||||||
|
}
|
||||||
|
case "system":
|
||||||
|
if m, ok := val.(map[string]interface{}); ok {
|
||||||
|
out["system"] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, out)
|
||||||
|
}
|
||||||
12
soul-api/internal/handler/content.go
Normal file
12
soul-api/internal/handler/content.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContentGet GET /api/content
|
||||||
|
func ContentGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
17
soul-api/internal/handler/cron.go
Normal file
17
soul-api/internal/handler/cron.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CronSyncOrders GET/POST /api/cron/sync-orders
|
||||||
|
func CronSyncOrders(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CronUnbindExpired GET/POST /api/cron/unbind-expired
|
||||||
|
func CronUnbindExpired(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
391
soul-api/internal/handler/db.go
Normal file
391
soul-api/internal/handler/db.go
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DBConfigGet GET /api/db/config
|
||||||
|
func DBConfigGet(c *gin.Context) {
|
||||||
|
key := c.Query("key")
|
||||||
|
db := database.DB()
|
||||||
|
var list []model.SystemConfig
|
||||||
|
q := db.Table("system_config")
|
||||||
|
if key != "" {
|
||||||
|
q = q.Where("config_key = ?", key)
|
||||||
|
}
|
||||||
|
if err := q.Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if key != "" && len(list) == 1 {
|
||||||
|
var val interface{}
|
||||||
|
_ = json.Unmarshal(list[0].ConfigValue, &val)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": val})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := make([]gin.H, 0, len(list))
|
||||||
|
for _, row := range list {
|
||||||
|
var val interface{}
|
||||||
|
_ = json.Unmarshal(row.ConfigValue, &val)
|
||||||
|
data = append(data, gin.H{"configKey": row.ConfigKey, "configValue": val})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBConfigPost POST /api/db/config
|
||||||
|
func DBConfigPost(c *gin.Context) {
|
||||||
|
var body struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || body.Key == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "配置键不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
valBytes, err := json.Marshal(body.Value)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db := database.DB()
|
||||||
|
desc := body.Description
|
||||||
|
var row model.SystemConfig
|
||||||
|
err = db.Where("config_key = ?", body.Key).First(&row).Error
|
||||||
|
if err != nil {
|
||||||
|
row = model.SystemConfig{ConfigKey: body.Key, ConfigValue: valBytes, Description: &desc}
|
||||||
|
err = db.Create(&row).Error
|
||||||
|
} else {
|
||||||
|
row.ConfigValue = valBytes
|
||||||
|
if body.Description != "" {
|
||||||
|
row.Description = &desc
|
||||||
|
}
|
||||||
|
err = db.Save(&row).Error
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "配置保存成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBUsersList GET /api/db/users
|
||||||
|
func DBUsersList(c *gin.Context) {
|
||||||
|
var users []model.User
|
||||||
|
if err := database.DB().Find(&users).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "users": []interface{}{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "users": users})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBUsersAction POST /api/db/users(创建)、PUT /api/db/users(更新)
|
||||||
|
func DBUsersAction(c *gin.Context) {
|
||||||
|
db := database.DB()
|
||||||
|
if c.Request.Method == http.MethodPost {
|
||||||
|
var body struct {
|
||||||
|
OpenID *string `json:"openId"`
|
||||||
|
Phone *string `json:"phone"`
|
||||||
|
Nickname *string `json:"nickname"`
|
||||||
|
WechatID *string `json:"wechatId"`
|
||||||
|
Avatar *string `json:"avatar"`
|
||||||
|
IsAdmin *bool `json:"isAdmin"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := "user_" + randomSuffix()
|
||||||
|
code := "SOUL" + randomSuffix()[:4]
|
||||||
|
nick := "用户"
|
||||||
|
if body.Nickname != nil && *body.Nickname != "" {
|
||||||
|
nick = *body.Nickname
|
||||||
|
} else {
|
||||||
|
nick = nick + userID[len(userID)-4:]
|
||||||
|
}
|
||||||
|
u := model.User{
|
||||||
|
ID: userID, Nickname: &nick, ReferralCode: &code,
|
||||||
|
OpenID: body.OpenID, Phone: body.Phone, WechatID: body.WechatID, Avatar: body.Avatar,
|
||||||
|
}
|
||||||
|
if body.IsAdmin != nil {
|
||||||
|
u.IsAdmin = body.IsAdmin
|
||||||
|
}
|
||||||
|
if err := db.Create(&u).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "user": u, "isNew": true, "message": "用户创建成功"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// PUT 更新
|
||||||
|
var body struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Nickname *string `json:"nickname"`
|
||||||
|
Phone *string `json:"phone"`
|
||||||
|
WechatID *string `json:"wechatId"`
|
||||||
|
Avatar *string `json:"avatar"`
|
||||||
|
HasFullBook *bool `json:"hasFullBook"`
|
||||||
|
IsAdmin *bool `json:"isAdmin"`
|
||||||
|
Earnings *float64 `json:"earnings"`
|
||||||
|
PendingEarnings *float64 `json:"pendingEarnings"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户ID不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
if body.Nickname != nil {
|
||||||
|
updates["nickname"] = *body.Nickname
|
||||||
|
}
|
||||||
|
if body.Phone != nil {
|
||||||
|
updates["phone"] = *body.Phone
|
||||||
|
}
|
||||||
|
if body.WechatID != nil {
|
||||||
|
updates["wechat_id"] = *body.WechatID
|
||||||
|
}
|
||||||
|
if body.Avatar != nil {
|
||||||
|
updates["avatar"] = *body.Avatar
|
||||||
|
}
|
||||||
|
if body.HasFullBook != nil {
|
||||||
|
updates["has_full_book"] = *body.HasFullBook
|
||||||
|
}
|
||||||
|
if body.IsAdmin != nil {
|
||||||
|
updates["is_admin"] = *body.IsAdmin
|
||||||
|
}
|
||||||
|
if body.Earnings != nil {
|
||||||
|
updates["earnings"] = *body.Earnings
|
||||||
|
}
|
||||||
|
if body.PendingEarnings != nil {
|
||||||
|
updates["pending_earnings"] = *body.PendingEarnings
|
||||||
|
}
|
||||||
|
if len(updates) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "没有需要更新的字段"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.User{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户更新成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomSuffix() string {
|
||||||
|
return fmt.Sprintf("%d%x", time.Now().UnixNano()%100000, time.Now().UnixNano()&0xfff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBUsersDelete DELETE /api/db/users
|
||||||
|
func DBUsersDelete(c *gin.Context) {
|
||||||
|
id := c.Query("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户ID不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := database.DB().Where("id = ?", id).Delete(&model.User{}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户删除成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBUsersReferrals GET /api/db/users/referrals
|
||||||
|
func DBUsersReferrals(c *gin.Context) {
|
||||||
|
userId := c.Query("userId")
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db := database.DB()
|
||||||
|
var bindings []model.ReferralBinding
|
||||||
|
if err := db.Where("referrer_id = ?", userId).Order("binding_date DESC").Find(&bindings).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "referrals": []interface{}{}, "stats": gin.H{"total": 0, "purchased": 0, "free": 0, "earnings": 0, "pendingEarnings": 0, "withdrawnEarnings": 0}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refereeIds := make([]string, 0, len(bindings))
|
||||||
|
for _, b := range bindings {
|
||||||
|
refereeIds = append(refereeIds, b.RefereeID)
|
||||||
|
}
|
||||||
|
var users []model.User
|
||||||
|
if len(refereeIds) > 0 {
|
||||||
|
db.Where("id IN ?", refereeIds).Find(&users)
|
||||||
|
}
|
||||||
|
userMap := make(map[string]*model.User)
|
||||||
|
for i := range users {
|
||||||
|
userMap[users[i].ID] = &users[i]
|
||||||
|
}
|
||||||
|
referrals := make([]gin.H, 0, len(bindings))
|
||||||
|
for _, b := range bindings {
|
||||||
|
u := userMap[b.RefereeID]
|
||||||
|
nick := "微信用户"
|
||||||
|
var avatar *string
|
||||||
|
var phone *string
|
||||||
|
hasFullBook := false
|
||||||
|
if u != nil {
|
||||||
|
if u.Nickname != nil {
|
||||||
|
nick = *u.Nickname
|
||||||
|
}
|
||||||
|
avatar, phone = u.Avatar, u.Phone
|
||||||
|
if u.HasFullBook != nil {
|
||||||
|
hasFullBook = *u.HasFullBook
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status := "active"
|
||||||
|
if b.Status != nil {
|
||||||
|
status = *b.Status
|
||||||
|
}
|
||||||
|
daysRemaining := 0
|
||||||
|
if b.ExpiryDate.After(time.Now()) {
|
||||||
|
daysRemaining = int(b.ExpiryDate.Sub(time.Now()).Hours() / 24)
|
||||||
|
}
|
||||||
|
referrals = append(referrals, gin.H{
|
||||||
|
"id": b.RefereeID, "nickname": nick, "avatar": avatar, "phone": phone,
|
||||||
|
"hasFullBook": hasFullBook || status == "converted",
|
||||||
|
"createdAt": b.BindingDate, "bindingStatus": status, "daysRemaining": daysRemaining, "commission": b.CommissionAmount,
|
||||||
|
"status": status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var referrer model.User
|
||||||
|
earningsE, pendingE, withdrawnE := 0.0, 0.0, 0.0
|
||||||
|
if err := db.Where("id = ?", userId).Select("earnings", "pending_earnings", "withdrawn_earnings").First(&referrer).Error; err == nil {
|
||||||
|
if referrer.Earnings != nil {
|
||||||
|
earningsE = *referrer.Earnings
|
||||||
|
}
|
||||||
|
if referrer.PendingEarnings != nil {
|
||||||
|
pendingE = *referrer.PendingEarnings
|
||||||
|
}
|
||||||
|
if referrer.WithdrawnEarnings != nil {
|
||||||
|
withdrawnE = *referrer.WithdrawnEarnings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
purchased := 0
|
||||||
|
for _, b := range bindings {
|
||||||
|
u := userMap[b.RefereeID]
|
||||||
|
if (u != nil && u.HasFullBook != nil && *u.HasFullBook) || (b.Status != nil && *b.Status == "converted") {
|
||||||
|
purchased++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true, "referrals": referrals,
|
||||||
|
"stats": gin.H{
|
||||||
|
"total": len(bindings), "purchased": purchased, "free": len(bindings) - purchased,
|
||||||
|
"earnings": earningsE, "pendingEarnings": pendingE, "withdrawnEarnings": withdrawnE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBInit POST /api/db/init
|
||||||
|
func DBInit(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "初始化接口已就绪(表结构由迁移维护)"}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBDistribution GET /api/db/distribution
|
||||||
|
func DBDistribution(c *gin.Context) {
|
||||||
|
userId := c.Query("userId")
|
||||||
|
db := database.DB()
|
||||||
|
var bindings []model.ReferralBinding
|
||||||
|
q := db.Order("binding_date DESC").Limit(500)
|
||||||
|
if userId != "" {
|
||||||
|
q = q.Where("referrer_id = ?", userId)
|
||||||
|
}
|
||||||
|
if err := q.Find(&bindings).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": []interface{}{}, "total": 0})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
referrerIds := make(map[string]bool)
|
||||||
|
refereeIds := make(map[string]bool)
|
||||||
|
for _, b := range bindings {
|
||||||
|
referrerIds[b.ReferrerID] = true
|
||||||
|
refereeIds[b.RefereeID] = true
|
||||||
|
}
|
||||||
|
allIds := make([]string, 0, len(referrerIds)+len(refereeIds))
|
||||||
|
for id := range referrerIds {
|
||||||
|
allIds = append(allIds, id)
|
||||||
|
}
|
||||||
|
for id := range refereeIds {
|
||||||
|
if !referrerIds[id] {
|
||||||
|
allIds = append(allIds, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var users []model.User
|
||||||
|
if len(allIds) > 0 {
|
||||||
|
db.Where("id IN ?", allIds).Find(&users)
|
||||||
|
}
|
||||||
|
userMap := make(map[string]*model.User)
|
||||||
|
for i := range users {
|
||||||
|
userMap[users[i].ID] = &users[i]
|
||||||
|
}
|
||||||
|
out := make([]gin.H, 0, len(bindings))
|
||||||
|
for _, b := range bindings {
|
||||||
|
refNick := "用户"
|
||||||
|
if u := userMap[b.RefereeID]; u != nil && u.Nickname != nil {
|
||||||
|
refNick = *u.Nickname
|
||||||
|
} else {
|
||||||
|
refNick = refNick + b.RefereeID
|
||||||
|
}
|
||||||
|
var referrerName *string
|
||||||
|
if u := userMap[b.ReferrerID]; u != nil {
|
||||||
|
referrerName = u.Nickname
|
||||||
|
}
|
||||||
|
days := 0
|
||||||
|
if b.ExpiryDate.After(time.Now()) {
|
||||||
|
days = int(b.ExpiryDate.Sub(time.Now()).Hours() / 24)
|
||||||
|
}
|
||||||
|
var refereePhone *string
|
||||||
|
if u := userMap[b.RefereeID]; u != nil {
|
||||||
|
refereePhone = u.Phone
|
||||||
|
}
|
||||||
|
out = append(out, gin.H{
|
||||||
|
"id": b.ID, "referrer_id": b.ReferrerID, "referrer_name": referrerName, "referrer_code": b.ReferralCode,
|
||||||
|
"referee_id": b.RefereeID, "referee_nickname": refNick, "referee_phone": refereePhone,
|
||||||
|
"bound_at": b.BindingDate, "expires_at": b.ExpiryDate, "status": b.Status,
|
||||||
|
"days_remaining": days, "commission": b.CommissionAmount, "source": "miniprogram",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": out, "total": len(out)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBChapters GET/POST /api/db/chapters
|
||||||
|
func DBChapters(c *gin.Context) {
|
||||||
|
var list []model.Chapter
|
||||||
|
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "data": []interface{}{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBConfigDelete DELETE /api/db/config
|
||||||
|
func DBConfigDelete(c *gin.Context) {
|
||||||
|
key := c.Query("key")
|
||||||
|
if key == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "配置键不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := database.DB().Where("config_key = ?", key).Delete(&model.SystemConfig{}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBInitGet GET /api/db/init
|
||||||
|
func DBInitGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "ok"}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBMigrateGet GET /api/db/migrate
|
||||||
|
func DBMigrateGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "迁移状态查询(由 Prisma/外部维护)"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBMigratePost POST /api/db/migrate
|
||||||
|
func DBMigratePost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "迁移由 Prisma/外部执行"})
|
||||||
|
}
|
||||||
247
soul-api/internal/handler/db_book.go
Normal file
247
soul-api/internal/handler/db_book.go
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sectionListItem 与前端 SectionListItem 一致(小写驼峰)
|
||||||
|
type sectionListItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
IsFree *bool `json:"isFree,omitempty"`
|
||||||
|
PartID string `json:"partId"`
|
||||||
|
PartTitle string `json:"partTitle"`
|
||||||
|
ChapterID string `json:"chapterId"`
|
||||||
|
ChapterTitle string `json:"chapterTitle"`
|
||||||
|
FilePath *string `json:"filePath,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBBookAction GET/POST/PUT /api/db/book
|
||||||
|
func DBBookAction(c *gin.Context) {
|
||||||
|
db := database.DB()
|
||||||
|
switch c.Request.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
action := c.Query("action")
|
||||||
|
id := c.Query("id")
|
||||||
|
switch action {
|
||||||
|
case "list":
|
||||||
|
var rows []model.Chapter
|
||||||
|
if err := db.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sections := make([]sectionListItem, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
price := 1.0
|
||||||
|
if r.Price != nil {
|
||||||
|
price = *r.Price
|
||||||
|
}
|
||||||
|
sections = append(sections, sectionListItem{
|
||||||
|
ID: r.ID,
|
||||||
|
Title: r.SectionTitle,
|
||||||
|
Price: price,
|
||||||
|
IsFree: r.IsFree,
|
||||||
|
PartID: r.PartID,
|
||||||
|
PartTitle: r.PartTitle,
|
||||||
|
ChapterID: r.ChapterID,
|
||||||
|
ChapterTitle: r.ChapterTitle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections, "total": len(sections)})
|
||||||
|
return
|
||||||
|
case "read":
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ch model.Chapter
|
||||||
|
if err := db.Where("id = ?", id).First(&ch).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "章节不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
price := 1.0
|
||||||
|
if ch.Price != nil {
|
||||||
|
price = *ch.Price
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"section": gin.H{
|
||||||
|
"id": ch.ID,
|
||||||
|
"title": ch.SectionTitle,
|
||||||
|
"price": price,
|
||||||
|
"content": ch.Content,
|
||||||
|
"partId": ch.PartID,
|
||||||
|
"partTitle": ch.PartTitle,
|
||||||
|
"chapterId": ch.ChapterID,
|
||||||
|
"chapterTitle": ch.ChapterTitle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case "export":
|
||||||
|
var rows []model.Chapter
|
||||||
|
if err := db.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sections := make([]sectionListItem, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
price := 1.0
|
||||||
|
if r.Price != nil {
|
||||||
|
price = *r.Price
|
||||||
|
}
|
||||||
|
sections = append(sections, sectionListItem{
|
||||||
|
ID: r.ID, Title: r.SectionTitle, Price: price, IsFree: r.IsFree,
|
||||||
|
PartID: r.PartID, PartTitle: r.PartTitle, ChapterID: r.ChapterID, ChapterTitle: r.ChapterTitle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections})
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无效的 action"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case http.MethodPost:
|
||||||
|
var body struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Data []importItem `json:"data"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch body.Action {
|
||||||
|
case "sync":
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步完成(Gin 无文件源时可从 DB 已存在数据视为已同步)"})
|
||||||
|
return
|
||||||
|
case "import":
|
||||||
|
imported, failed := 0, 0
|
||||||
|
for _, item := range body.Data {
|
||||||
|
price := 1.0
|
||||||
|
if item.Price != nil {
|
||||||
|
price = *item.Price
|
||||||
|
}
|
||||||
|
isFree := false
|
||||||
|
if item.IsFree != nil {
|
||||||
|
isFree = *item.IsFree
|
||||||
|
}
|
||||||
|
wordCount := len(item.Content)
|
||||||
|
status := "published"
|
||||||
|
ch := model.Chapter{
|
||||||
|
ID: item.ID,
|
||||||
|
PartID: strPtr(item.PartID, "part-1"),
|
||||||
|
PartTitle: strPtr(item.PartTitle, "未分类"),
|
||||||
|
ChapterID: strPtr(item.ChapterID, "chapter-1"),
|
||||||
|
ChapterTitle: strPtr(item.ChapterTitle, "未分类"),
|
||||||
|
SectionTitle: item.Title,
|
||||||
|
Content: item.Content,
|
||||||
|
WordCount: &wordCount,
|
||||||
|
IsFree: &isFree,
|
||||||
|
Price: &price,
|
||||||
|
Status: &status,
|
||||||
|
}
|
||||||
|
err := db.Where("id = ?", item.ID).First(&model.Chapter{}).Error
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
err = db.Create(&ch).Error
|
||||||
|
} else if err == nil {
|
||||||
|
err = db.Model(&model.Chapter{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||||
|
"section_title": ch.SectionTitle,
|
||||||
|
"content": ch.Content,
|
||||||
|
"word_count": ch.WordCount,
|
||||||
|
"is_free": ch.IsFree,
|
||||||
|
"price": ch.Price,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imported++
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "导入完成", "imported": imported, "failed": failed})
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无效的 action"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case http.MethodPut:
|
||||||
|
var body struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Price *float64 `json:"price"`
|
||||||
|
IsFree *bool `json:"isFree"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
price := 1.0
|
||||||
|
if body.Price != nil {
|
||||||
|
price = *body.Price
|
||||||
|
}
|
||||||
|
isFree := false
|
||||||
|
if body.IsFree != nil {
|
||||||
|
isFree = *body.IsFree
|
||||||
|
}
|
||||||
|
wordCount := len(body.Content)
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"section_title": body.Title,
|
||||||
|
"content": body.Content,
|
||||||
|
"word_count": wordCount,
|
||||||
|
"price": price,
|
||||||
|
"is_free": isFree,
|
||||||
|
}
|
||||||
|
err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type importItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Price *float64 `json:"price"`
|
||||||
|
IsFree *bool `json:"isFree"`
|
||||||
|
PartID *string `json:"partId"`
|
||||||
|
PartTitle *string `json:"partTitle"`
|
||||||
|
ChapterID *string `json:"chapterId"`
|
||||||
|
ChapterTitle *string `json:"chapterTitle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func strPtr(s *string, def string) string {
|
||||||
|
if s != nil && *s != "" {
|
||||||
|
return *s
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBBookDelete DELETE /api/db/book
|
||||||
|
func DBBookDelete(c *gin.Context) {
|
||||||
|
id := c.Query("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := database.DB().Where("id = ?", id).Delete(&model.Chapter{}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
22
soul-api/internal/handler/distribution.go
Normal file
22
soul-api/internal/handler/distribution.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DistributionGet POST /api/distribution GET/POST/PUT
|
||||||
|
func DistributionGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistributionAutoWithdrawConfig GET/POST/DELETE /api/distribution/auto-withdraw-config
|
||||||
|
func DistributionAutoWithdrawConfig(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistributionMessages GET/POST /api/distribution/messages
|
||||||
|
func DistributionMessages(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
12
soul-api/internal/handler/documentation.go
Normal file
12
soul-api/internal/handler/documentation.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocGenerate POST /api/documentation/generate
|
||||||
|
func DocGenerate(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
22
soul-api/internal/handler/match.go
Normal file
22
soul-api/internal/handler/match.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MatchConfigGet GET /api/match/config
|
||||||
|
func MatchConfigGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchConfigPost POST /api/match/config
|
||||||
|
func MatchConfigPost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchUsers POST /api/match/users (Next 为 POST,拆解计划写 GET,两法都挂)
|
||||||
|
func MatchUsers(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||||
|
}
|
||||||
12
soul-api/internal/handler/menu.go
Normal file
12
soul-api/internal/handler/menu.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MenuGet GET /api/menu
|
||||||
|
func MenuGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||||
|
}
|
||||||
32
soul-api/internal/handler/miniprogram.go
Normal file
32
soul-api/internal/handler/miniprogram.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MiniprogramLogin POST /api/miniprogram/login
|
||||||
|
func MiniprogramLogin(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniprogramPay GET/POST /api/miniprogram/pay
|
||||||
|
func MiniprogramPay(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniprogramPayNotify POST /api/miniprogram/pay/notify
|
||||||
|
func MiniprogramPayNotify(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniprogramPhone POST /api/miniprogram/phone
|
||||||
|
func MiniprogramPhone(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniprogramQrcode POST /api/miniprogram/qrcode (Next 为 POST)
|
||||||
|
func MiniprogramQrcode(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
20
soul-api/internal/handler/orders.go
Normal file
20
soul-api/internal/handler/orders.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OrdersList GET /api/orders
|
||||||
|
func OrdersList(c *gin.Context) {
|
||||||
|
var orders []model.Order
|
||||||
|
if err := database.DB().Order("created_at DESC").Find(&orders).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "orders": []interface{}{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "orders": orders})
|
||||||
|
}
|
||||||
52
soul-api/internal/handler/payment.go
Normal file
52
soul-api/internal/handler/payment.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaymentAlipayNotify POST /api/payment/alipay/notify
|
||||||
|
func PaymentAlipayNotify(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentCallback POST /api/payment/callback
|
||||||
|
func PaymentCallback(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentCreateOrder POST /api/payment/create-order
|
||||||
|
func PaymentCreateOrder(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentMethods GET /api/payment/methods
|
||||||
|
func PaymentMethods(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentQuery GET /api/payment/query
|
||||||
|
func PaymentQuery(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentStatusOrderSn GET /api/payment/status/:orderSn
|
||||||
|
func PaymentStatusOrderSn(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentVerify POST /api/payment/verify
|
||||||
|
func PaymentVerify(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentWechatNotify POST /api/payment/wechat/notify
|
||||||
|
func PaymentWechatNotify(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentWechatTransferNotify POST /api/payment/wechat/transfer/notify
|
||||||
|
func PaymentWechatTransferNotify(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
22
soul-api/internal/handler/referral.go
Normal file
22
soul-api/internal/handler/referral.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReferralBind POST /api/referral/bind
|
||||||
|
func ReferralBind(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferralData GET /api/referral/data
|
||||||
|
func ReferralData(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferralVisit POST /api/referral/visit
|
||||||
|
func ReferralVisit(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
81
soul-api/internal/handler/search.go
Normal file
81
soul-api/internal/handler/search.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// escapeLike 转义 LIKE 中的 % _ \,防止注入与通配符滥用
|
||||||
|
func escapeLike(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||||
|
s = strings.ReplaceAll(s, "%", "\\%")
|
||||||
|
s = strings.ReplaceAll(s, "_", "\\_")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchGet GET /api/search?q= 从 chapters 表搜索(GORM,参数化)
|
||||||
|
func SearchGet(c *gin.Context) {
|
||||||
|
q := strings.TrimSpace(c.Query("q"))
|
||||||
|
if q == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请输入搜索关键词"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pattern := "%" + escapeLike(q) + "%"
|
||||||
|
var list []model.Chapter
|
||||||
|
err := database.DB().Model(&model.Chapter{}).
|
||||||
|
Where("section_title LIKE ? OR content LIKE ?", pattern, pattern).
|
||||||
|
Order("sort_order ASC, id ASC").
|
||||||
|
Limit(50).
|
||||||
|
Find(&list).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"keyword": q, "total": 0, "results": []interface{}{}}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lowerQ := strings.ToLower(q)
|
||||||
|
results := make([]gin.H, 0, len(list))
|
||||||
|
for _, ch := range list {
|
||||||
|
matchType := "content"
|
||||||
|
score := 5
|
||||||
|
if strings.Contains(strings.ToLower(ch.SectionTitle), lowerQ) {
|
||||||
|
matchType = "title"
|
||||||
|
score = 10
|
||||||
|
}
|
||||||
|
snippet := ""
|
||||||
|
pos := strings.Index(strings.ToLower(ch.Content), lowerQ)
|
||||||
|
if pos >= 0 && len(ch.Content) > 0 {
|
||||||
|
start := pos - 50
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
end := pos + utf8.RuneCountInString(q) + 50
|
||||||
|
if end > len(ch.Content) {
|
||||||
|
end = len(ch.Content)
|
||||||
|
}
|
||||||
|
snippet = ch.Content[start:end]
|
||||||
|
if start > 0 {
|
||||||
|
snippet = "..." + snippet
|
||||||
|
}
|
||||||
|
if end < len(ch.Content) {
|
||||||
|
snippet = snippet + "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
price := 1.0
|
||||||
|
if ch.Price != nil {
|
||||||
|
price = *ch.Price
|
||||||
|
}
|
||||||
|
results = append(results, gin.H{
|
||||||
|
"id": ch.ID, "title": ch.SectionTitle, "partTitle": ch.PartTitle, "chapterTitle": ch.ChapterTitle,
|
||||||
|
"price": price, "isFree": ch.IsFree, "matchType": matchType, "score": score, "snippet": snippet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": gin.H{"keyword": q, "total": len(results), "results": results},
|
||||||
|
})
|
||||||
|
}
|
||||||
22
soul-api/internal/handler/sync.go
Normal file
22
soul-api/internal/handler/sync.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncGet GET /api/sync
|
||||||
|
func SyncGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncPost POST /api/sync
|
||||||
|
func SyncPost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncPut PUT /api/sync
|
||||||
|
func SyncPut(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
17
soul-api/internal/handler/upload.go
Normal file
17
soul-api/internal/handler/upload.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UploadPost POST /api/upload
|
||||||
|
func UploadPost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "url": ""})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadDelete DELETE /api/upload
|
||||||
|
func UploadDelete(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
157
soul-api/internal/handler/user.go
Normal file
157
soul-api/internal/handler/user.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAddressesGet GET /api/user/addresses
|
||||||
|
func UserAddressesGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAddressesPost POST /api/user/addresses
|
||||||
|
func UserAddressesPost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAddressesByID GET/PUT/DELETE /api/user/addresses/:id
|
||||||
|
func UserAddressesByID(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserCheckPurchased GET /api/user/check-purchased
|
||||||
|
func UserCheckPurchased(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfileGet GET /api/user/profile
|
||||||
|
func UserProfileGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfilePost POST /api/user/profile
|
||||||
|
func UserProfilePost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPurchaseStatus GET /api/user/purchase-status
|
||||||
|
func UserPurchaseStatus(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserReadingProgressGet GET /api/user/reading-progress
|
||||||
|
func UserReadingProgressGet(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserReadingProgressPost POST /api/user/reading-progress
|
||||||
|
func UserReadingProgressPost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTrackGet GET /api/user/track?userId=&limit= 从 user_tracks 表查(GORM)
|
||||||
|
func UserTrackGet(c *gin.Context) {
|
||||||
|
userId := c.Query("userId")
|
||||||
|
phone := c.Query("phone")
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
if userId == "" && phone == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "需要用户ID或手机号"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db := database.DB()
|
||||||
|
if userId == "" && phone != "" {
|
||||||
|
var u model.User
|
||||||
|
if err := db.Where("phone = ?", phone).First(&u).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId = u.ID
|
||||||
|
}
|
||||||
|
var tracks []model.UserTrack
|
||||||
|
if err := db.Where("user_id = ?", userId).Order("created_at DESC").Limit(limit).Find(&tracks).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "tracks": []interface{}{}, "stats": gin.H{}, "total": 0})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stats := make(map[string]int)
|
||||||
|
formatted := make([]gin.H, 0, len(tracks))
|
||||||
|
for _, t := range tracks {
|
||||||
|
stats[t.Action]++
|
||||||
|
target := ""
|
||||||
|
if t.Target != nil {
|
||||||
|
target = *t.Target
|
||||||
|
}
|
||||||
|
if t.ChapterID != nil && target == "" {
|
||||||
|
target = *t.ChapterID
|
||||||
|
}
|
||||||
|
formatted = append(formatted, gin.H{
|
||||||
|
"id": t.ID, "action": t.Action, "target": target, "chapterTitle": t.ChapterID,
|
||||||
|
"createdAt": t.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "tracks": formatted, "stats": stats, "total": len(formatted)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTrackPost POST /api/user/track 记录行为(GORM)
|
||||||
|
func UserTrackPost(c *gin.Context) {
|
||||||
|
var body struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
ExtraData interface{} `json:"extraData"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求体无效"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.UserID == "" && body.Phone == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "需要用户ID或手机号"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Action == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "行为类型不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db := database.DB()
|
||||||
|
userId := body.UserID
|
||||||
|
if userId == "" {
|
||||||
|
var u model.User
|
||||||
|
if err := db.Where("phone = ?", body.Phone).First(&u).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId = u.ID
|
||||||
|
}
|
||||||
|
trackID := fmt.Sprintf("track_%d", time.Now().UnixNano()%100000000)
|
||||||
|
chID := body.Target
|
||||||
|
if body.Action == "view_chapter" {
|
||||||
|
chID = body.Target
|
||||||
|
}
|
||||||
|
t := model.UserTrack{
|
||||||
|
ID: trackID, UserID: userId, Action: body.Action, Target: &body.Target,
|
||||||
|
}
|
||||||
|
if body.Target != "" {
|
||||||
|
t.ChapterID = &chID
|
||||||
|
}
|
||||||
|
if err := db.Create(&t).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "trackId": trackID, "message": "行为记录成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserUpdate POST /api/user/update
|
||||||
|
func UserUpdate(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
12
soul-api/internal/handler/wechat.go
Normal file
12
soul-api/internal/handler/wechat.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WechatLogin POST /api/wechat/login
|
||||||
|
func WechatLogin(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
60
soul-api/internal/handler/withdraw.go
Normal file
60
soul-api/internal/handler/withdraw.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"soul-api/internal/database"
|
||||||
|
"soul-api/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithdrawPost POST /api/withdraw 创建提现申请(占位:仅返回成功,实际需对接微信打款)
|
||||||
|
func WithdrawPost(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithdrawRecords GET /api/withdraw/records?userId= 当前用户提现记录(GORM)
|
||||||
|
func WithdrawRecords(c *gin.Context) {
|
||||||
|
userId := c.Query("userId")
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var list []model.Withdrawal
|
||||||
|
if err := database.DB().Where("user_id = ?", userId).Order("created_at DESC").Limit(100).Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": []interface{}{}}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]gin.H, 0, len(list))
|
||||||
|
for _, w := range list {
|
||||||
|
st := ""
|
||||||
|
if w.Status != nil {
|
||||||
|
st = *w.Status
|
||||||
|
}
|
||||||
|
out = append(out, gin.H{
|
||||||
|
"id": w.ID, "amount": w.Amount, "status": st,
|
||||||
|
"createdAt": w.CreatedAt, "processedAt": w.ProcessedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": out}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithdrawPendingConfirm GET /api/withdraw/pending-confirm?userId= 待确认收款列表(GORM)
|
||||||
|
func WithdrawPendingConfirm(c *gin.Context) {
|
||||||
|
userId := c.Query("userId")
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var list []model.Withdrawal
|
||||||
|
if err := database.DB().Where("user_id = ? AND status = ?", userId, "pending_confirm").Order("created_at DESC").Find(&list).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": []interface{}{}, "mch_id": "", "app_id": ""}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]gin.H, 0, len(list))
|
||||||
|
for _, w := range list {
|
||||||
|
out = append(out, gin.H{"id": w.ID, "amount": w.Amount, "createdAt": w.CreatedAt})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": out, "mchId": "", "appId": ""}})
|
||||||
|
}
|
||||||
25
soul-api/internal/middleware/admin_auth.go
Normal file
25
soul-api/internal/middleware/admin_auth.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminAuth 管理端鉴权:校验登录态(Cookie 或 Authorization),未登录返回 401
|
||||||
|
// 开发模式(GIN_MODE=debug)下暂不校验,便于联调;生产请实现 Session/JWT
|
||||||
|
func AdminAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if os.Getenv("GIN_MODE") == "debug" {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := c.Cookie("admin_session")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"success": false, "error": "未登录"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
65
soul-api/internal/middleware/ratelimit.go
Normal file
65
soul-api/internal/middleware/ratelimit.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RateLimiter 按 IP 的限流器
|
||||||
|
type RateLimiter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
clients map[string]*rate.Limiter
|
||||||
|
r rate.Limit
|
||||||
|
b int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRateLimiter 创建限流中间件,r 每秒请求数,b 突发容量
|
||||||
|
func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
|
||||||
|
return &RateLimiter{
|
||||||
|
clients: make(map[string]*rate.Limiter),
|
||||||
|
r: r,
|
||||||
|
b: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLimiter 获取或创建该 key 的 limiter
|
||||||
|
func (rl *RateLimiter) getLimiter(key string) *rate.Limiter {
|
||||||
|
rl.mu.Lock()
|
||||||
|
defer rl.mu.Unlock()
|
||||||
|
if lim, ok := rl.clients[key]; ok {
|
||||||
|
return lim
|
||||||
|
}
|
||||||
|
lim := rate.NewLimiter(rl.r, rl.b)
|
||||||
|
rl.clients[key] = lim
|
||||||
|
return lim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware 返回 Gin 限流中间件(按客户端 IP)
|
||||||
|
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
key := c.ClientIP()
|
||||||
|
lim := rl.getLimiter(key)
|
||||||
|
if !lim.Allow() {
|
||||||
|
c.AbortWithStatus(http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup 定期清理过期 limiter(可选,避免 map 无限增长)
|
||||||
|
func (rl *RateLimiter) Cleanup(interval time.Duration) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
rl.mu.Lock()
|
||||||
|
rl.clients = make(map[string]*rate.Limiter)
|
||||||
|
rl.mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
25
soul-api/internal/middleware/secure.go
Normal file
25
soul-api/internal/middleware/secure.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/unrolled/secure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Secure 安全响应头中间件
|
||||||
|
func Secure() gin.HandlerFunc {
|
||||||
|
s := secure.New(secure.Options{
|
||||||
|
FrameDeny: true,
|
||||||
|
ContentTypeNosniff: true,
|
||||||
|
BrowserXssFilter: true,
|
||||||
|
ContentSecurityPolicy: "frame-ancestors 'none'",
|
||||||
|
ReferrerPolicy: "no-referrer",
|
||||||
|
})
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
err := s.Process(c.Writer, c.Request)
|
||||||
|
if err != nil {
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
1
soul-api/internal/model/README.txt
Normal file
1
soul-api/internal/model/README.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
在此目录放置 GORM 模型与请求/响应结构体,例如 User、Order、Withdrawal、Config 等。
|
||||||
23
soul-api/internal/model/chapter.go
Normal file
23
soul-api/internal/model/chapter.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Chapter 对应表 chapters(与 Prisma 一致),JSON 小写驼峰
|
||||||
|
type Chapter struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey;size:20" json:"id"`
|
||||||
|
PartID string `gorm:"column:part_id;size:20" json:"partId"`
|
||||||
|
PartTitle string `gorm:"column:part_title;size:100" json:"partTitle"`
|
||||||
|
ChapterID string `gorm:"column:chapter_id;size:20" json:"chapterId"`
|
||||||
|
ChapterTitle string `gorm:"column:chapter_title;size:200" json:"chapterTitle"`
|
||||||
|
SectionTitle string `gorm:"column:section_title;size:200" json:"sectionTitle"`
|
||||||
|
Content string `gorm:"column:content;type:longtext" json:"content,omitempty"`
|
||||||
|
WordCount *int `gorm:"column:word_count" json:"wordCount,omitempty"`
|
||||||
|
IsFree *bool `gorm:"column:is_free" json:"isFree,omitempty"`
|
||||||
|
Price *float64 `gorm:"column:price;type:decimal(10,2)" json:"price,omitempty"`
|
||||||
|
SortOrder *int `gorm:"column:sort_order" json:"sortOrder,omitempty"`
|
||||||
|
Status *string `gorm:"column:status;size:20" json:"status,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Chapter) TableName() string { return "chapters" }
|
||||||
24
soul-api/internal/model/order.go
Normal file
24
soul-api/internal/model/order.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Order 对应表 orders,JSON 输出与现网接口 1:1(小写驼峰)
|
||||||
|
type Order struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey;size:50" json:"id"`
|
||||||
|
OrderSN string `gorm:"column:order_sn;uniqueIndex;size:50" json:"orderSn"`
|
||||||
|
UserID string `gorm:"column:user_id;size:50" json:"userId"`
|
||||||
|
OpenID string `gorm:"column:open_id;size:100" json:"openId"`
|
||||||
|
ProductType string `gorm:"column:product_type;size:50" json:"productType"`
|
||||||
|
ProductID *string `gorm:"column:product_id;size:50" json:"productId,omitempty"`
|
||||||
|
Amount float64 `gorm:"column:amount;type:decimal(10,2)" json:"amount"`
|
||||||
|
Description *string `gorm:"column:description;size:200" json:"description,omitempty"`
|
||||||
|
Status *string `gorm:"column:status;size:20" json:"status,omitempty"`
|
||||||
|
TransactionID *string `gorm:"column:transaction_id;size:100" json:"transactionId,omitempty"`
|
||||||
|
PayTime *time.Time `gorm:"column:pay_time" json:"payTime,omitempty"`
|
||||||
|
ReferralCode *string `gorm:"column:referral_code;size:255" json:"referralCode,omitempty"`
|
||||||
|
ReferrerID *string `gorm:"column:referrer_id;size:255" json:"referrerId,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Order) TableName() string { return "orders" }
|
||||||
19
soul-api/internal/model/referral_binding.go
Normal file
19
soul-api/internal/model/referral_binding.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ReferralBinding 对应表 referral_bindings
|
||||||
|
type ReferralBinding struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey;size:50"`
|
||||||
|
ReferrerID string `gorm:"column:referrer_id;size:50"`
|
||||||
|
RefereeID string `gorm:"column:referee_id;size:50"`
|
||||||
|
ReferralCode string `gorm:"column:referral_code;size:20"`
|
||||||
|
Status *string `gorm:"column:status;size:20"`
|
||||||
|
BindingDate time.Time `gorm:"column:binding_date"`
|
||||||
|
ExpiryDate time.Time `gorm:"column:expiry_date"`
|
||||||
|
CommissionAmount *float64 `gorm:"column:commission_amount;type:decimal(10,2)"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ReferralBinding) TableName() string { return "referral_bindings" }
|
||||||
13
soul-api/internal/model/referral_visit.go
Normal file
13
soul-api/internal/model/referral_visit.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ReferralVisit 对应表 referral_visits
|
||||||
|
type ReferralVisit struct {
|
||||||
|
ID int `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
ReferrerID string `gorm:"column:referrer_id;size:50"`
|
||||||
|
VisitorID *string `gorm:"column:visitor_id;size:50"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ReferralVisit) TableName() string { return "referral_visits" }
|
||||||
35
soul-api/internal/model/system_config.go
Normal file
35
soul-api/internal/model/system_config.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigValue 存 system_config.config_value(JSON 列,可为 object 或 array)
|
||||||
|
type ConfigValue []byte
|
||||||
|
|
||||||
|
func (c ConfigValue) Value() (driver.Value, error) { return []byte(c), nil }
|
||||||
|
func (c *ConfigValue) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*c = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*c = append((*c)[0:0], b...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemConfig 对应表 system_config,JSON 输出与现网 1:1(小写驼峰)
|
||||||
|
type SystemConfig struct {
|
||||||
|
ID int `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
|
ConfigKey string `gorm:"column:config_key;uniqueIndex;size:100" json:"configKey"`
|
||||||
|
ConfigValue ConfigValue `gorm:"column:config_value;type:json" json:"configValue"`
|
||||||
|
Description *string `gorm:"column:description;size:200" json:"description,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SystemConfig) TableName() string { return "system_config" }
|
||||||
26
soul-api/internal/model/user.go
Normal file
26
soul-api/internal/model/user.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
|
||||||
|
// User 对应表 users,JSON 输出与现网接口 1:1(小写驼峰)
|
||||||
|
type User struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey;size:50" json:"id"`
|
||||||
|
OpenID *string `gorm:"column:open_id;size:100" json:"openId,omitempty"`
|
||||||
|
Nickname *string `gorm:"column:nickname;size:100" json:"nickname,omitempty"`
|
||||||
|
Avatar *string `gorm:"column:avatar;size:500" json:"avatar,omitempty"`
|
||||||
|
Phone *string `gorm:"column:phone;size:20" json:"phone,omitempty"`
|
||||||
|
WechatID *string `gorm:"column:wechat_id;size:100" json:"wechatId,omitempty"`
|
||||||
|
ReferralCode *string `gorm:"column:referral_code;size:20" json:"referralCode,omitempty"`
|
||||||
|
HasFullBook *bool `gorm:"column:has_full_book" json:"hasFullBook,omitempty"`
|
||||||
|
Earnings *float64 `gorm:"column:earnings;type:decimal(10,2)" json:"earnings,omitempty"`
|
||||||
|
PendingEarnings *float64 `gorm:"column:pending_earnings;type:decimal(10,2)" json:"pendingEarnings,omitempty"`
|
||||||
|
ReferralCount *int `gorm:"column:referral_count" json:"referralCount,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||||
|
IsAdmin *bool `gorm:"column:is_admin" json:"isAdmin,omitempty"`
|
||||||
|
WithdrawnEarnings *float64 `gorm:"column:withdrawn_earnings;type:decimal(10,2)" json:"withdrawnEarnings,omitempty"`
|
||||||
|
Source *string `gorm:"column:source;size:50" json:"source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string { return "users" }
|
||||||
16
soul-api/internal/model/user_track.go
Normal file
16
soul-api/internal/model/user_track.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// UserTrack 对应表 user_tracks
|
||||||
|
type UserTrack struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey;size:50"`
|
||||||
|
UserID string `gorm:"column:user_id;size:100"`
|
||||||
|
Action string `gorm:"column:action;size:50"`
|
||||||
|
ChapterID *string `gorm:"column:chapter_id;size:100"`
|
||||||
|
Target *string `gorm:"column:target;size:200"`
|
||||||
|
ExtraData []byte `gorm:"column:extra_data;type:json"`
|
||||||
|
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UserTrack) TableName() string { return "user_tracks" }
|
||||||
17
soul-api/internal/model/withdrawal.go
Normal file
17
soul-api/internal/model/withdrawal.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Withdrawal 对应表 withdrawals
|
||||||
|
type Withdrawal struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey;size:50" json:"id"`
|
||||||
|
UserID string `gorm:"column:user_id;size:50" json:"userId"`
|
||||||
|
Amount float64 `gorm:"column:amount;type:decimal(10,2)" json:"amount"`
|
||||||
|
Status *string `gorm:"column:status;size:20" json:"status"`
|
||||||
|
WechatID *string `gorm:"column:wechat_id;size:100" json:"wechatId"`
|
||||||
|
WechatOpenid *string `gorm:"column:wechat_openid;size:100" json:"wechatOpenid"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||||
|
ProcessedAt *time.Time `gorm:"column:processed_at" json:"processedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Withdrawal) TableName() string { return "withdrawals" }
|
||||||
1
soul-api/internal/repository/README.txt
Normal file
1
soul-api/internal/repository/README.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
在此目录放置数据库访问层,供 service 调用,例如 UserRepo、OrderRepo、ConfigRepo 等。
|
||||||
213
soul-api/internal/router/router.go
Normal file
213
soul-api/internal/router/router.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"soul-api/internal/config"
|
||||||
|
"soul-api/internal/handler"
|
||||||
|
"soul-api/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup 创建并配置 Gin 引擎,路径与 app/api 一致
|
||||||
|
func Setup(cfg *config.Config) *gin.Engine {
|
||||||
|
gin.SetMode(cfg.Mode)
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(gin.Recovery())
|
||||||
|
r.Use(gin.Logger())
|
||||||
|
_ = r.SetTrustedProxies(cfg.TrustedProxies)
|
||||||
|
|
||||||
|
r.Use(middleware.Secure())
|
||||||
|
r.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: cfg.CORSOrigins,
|
||||||
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 86400,
|
||||||
|
}))
|
||||||
|
rateLimiter := middleware.NewRateLimiter(100, 200)
|
||||||
|
r.Use(rateLimiter.Middleware())
|
||||||
|
|
||||||
|
api := r.Group("/api")
|
||||||
|
{
|
||||||
|
// ----- 管理端 -----
|
||||||
|
api.GET("/admin", handler.AdminCheck)
|
||||||
|
api.POST("/admin", handler.AdminLogin)
|
||||||
|
api.POST("/admin/logout", handler.AdminLogout)
|
||||||
|
|
||||||
|
admin := api.Group("/admin")
|
||||||
|
admin.Use(middleware.AdminAuth())
|
||||||
|
{
|
||||||
|
admin.GET("/chapters", handler.AdminChaptersList)
|
||||||
|
admin.POST("/chapters", handler.AdminChaptersAction)
|
||||||
|
admin.PUT("/chapters", handler.AdminChaptersAction)
|
||||||
|
admin.DELETE("/chapters", handler.AdminChaptersAction)
|
||||||
|
admin.GET("/content", handler.AdminContent)
|
||||||
|
admin.POST("/content", handler.AdminContent)
|
||||||
|
admin.PUT("/content", handler.AdminContent)
|
||||||
|
admin.DELETE("/content", handler.AdminContent)
|
||||||
|
admin.GET("/distribution/overview", handler.AdminDistributionOverview)
|
||||||
|
admin.GET("/payment", handler.AdminPayment)
|
||||||
|
admin.POST("/payment", handler.AdminPayment)
|
||||||
|
admin.PUT("/payment", handler.AdminPayment)
|
||||||
|
admin.DELETE("/payment", handler.AdminPayment)
|
||||||
|
admin.GET("/referral", handler.AdminReferral)
|
||||||
|
admin.POST("/referral", handler.AdminReferral)
|
||||||
|
admin.PUT("/referral", handler.AdminReferral)
|
||||||
|
admin.DELETE("/referral", handler.AdminReferral)
|
||||||
|
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
|
||||||
|
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- 鉴权 -----
|
||||||
|
api.POST("/auth/login", handler.AuthLogin)
|
||||||
|
api.POST("/auth/reset-password", handler.AuthResetPassword)
|
||||||
|
|
||||||
|
// ----- 书籍/章节 -----
|
||||||
|
api.GET("/book/all-chapters", handler.BookAllChapters)
|
||||||
|
api.GET("/book/chapter/:id", handler.BookChapterByID)
|
||||||
|
api.GET("/book/chapters", handler.BookChapters)
|
||||||
|
api.POST("/book/chapters", handler.BookChapters)
|
||||||
|
api.PUT("/book/chapters", handler.BookChapters)
|
||||||
|
api.DELETE("/book/chapters", handler.BookChapters)
|
||||||
|
api.GET("/book/hot", handler.BookHot)
|
||||||
|
api.GET("/book/latest-chapters", handler.BookLatestChapters)
|
||||||
|
api.GET("/book/search", handler.BookSearch)
|
||||||
|
api.GET("/book/stats", handler.BookStats)
|
||||||
|
api.GET("/book/sync", handler.BookSync)
|
||||||
|
api.POST("/book/sync", handler.BookSync)
|
||||||
|
|
||||||
|
// ----- CKB -----
|
||||||
|
api.POST("/ckb/join", handler.CKBJoin)
|
||||||
|
api.POST("/ckb/match", handler.CKBMatch)
|
||||||
|
api.GET("/ckb/sync", handler.CKBSync)
|
||||||
|
api.POST("/ckb/sync", handler.CKBSync)
|
||||||
|
|
||||||
|
// ----- 配置 -----
|
||||||
|
api.GET("/config", handler.GetConfig)
|
||||||
|
|
||||||
|
// ----- 内容 -----
|
||||||
|
api.GET("/content", handler.ContentGet)
|
||||||
|
|
||||||
|
// ----- 定时任务 -----
|
||||||
|
api.GET("/cron/sync-orders", handler.CronSyncOrders)
|
||||||
|
api.POST("/cron/sync-orders", handler.CronSyncOrders)
|
||||||
|
api.GET("/cron/unbind-expired", handler.CronUnbindExpired)
|
||||||
|
api.POST("/cron/unbind-expired", handler.CronUnbindExpired)
|
||||||
|
|
||||||
|
// ----- 数据库(管理端) -----
|
||||||
|
db := api.Group("/db")
|
||||||
|
db.Use(middleware.AdminAuth())
|
||||||
|
{
|
||||||
|
db.GET("/book", handler.DBBookAction)
|
||||||
|
db.POST("/book", handler.DBBookAction)
|
||||||
|
db.PUT("/book", handler.DBBookAction)
|
||||||
|
db.DELETE("/book", handler.DBBookDelete)
|
||||||
|
db.GET("/chapters", handler.DBChapters)
|
||||||
|
db.POST("/chapters", handler.DBChapters)
|
||||||
|
db.GET("/config", handler.DBConfigGet)
|
||||||
|
db.POST("/config", handler.DBConfigPost)
|
||||||
|
db.DELETE("/config", handler.DBConfigDelete)
|
||||||
|
db.GET("/distribution", handler.DBDistribution)
|
||||||
|
db.GET("/init", handler.DBInitGet)
|
||||||
|
db.POST("/init", handler.DBInit)
|
||||||
|
db.GET("/migrate", handler.DBMigrateGet)
|
||||||
|
db.POST("/migrate", handler.DBMigratePost)
|
||||||
|
db.GET("/users", handler.DBUsersList)
|
||||||
|
db.POST("/users", handler.DBUsersAction)
|
||||||
|
db.PUT("/users", handler.DBUsersAction)
|
||||||
|
db.DELETE("/users", handler.DBUsersDelete)
|
||||||
|
db.GET("/users/referrals", handler.DBUsersReferrals)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- 分销 -----
|
||||||
|
api.GET("/distribution", handler.DistributionGet)
|
||||||
|
api.POST("/distribution", handler.DistributionGet)
|
||||||
|
api.PUT("/distribution", handler.DistributionGet)
|
||||||
|
api.GET("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||||||
|
api.POST("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||||||
|
api.DELETE("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||||||
|
api.GET("/distribution/messages", handler.DistributionMessages)
|
||||||
|
api.POST("/distribution/messages", handler.DistributionMessages)
|
||||||
|
|
||||||
|
// ----- 文档生成 -----
|
||||||
|
api.POST("/documentation/generate", handler.DocGenerate)
|
||||||
|
|
||||||
|
// ----- 找伙伴 -----
|
||||||
|
api.GET("/match/config", handler.MatchConfigGet)
|
||||||
|
api.POST("/match/config", handler.MatchConfigPost)
|
||||||
|
api.POST("/match/users", handler.MatchUsers)
|
||||||
|
|
||||||
|
// ----- 菜单 -----
|
||||||
|
api.GET("/menu", handler.MenuGet)
|
||||||
|
|
||||||
|
// ----- 小程序 -----
|
||||||
|
api.POST("/miniprogram/login", handler.MiniprogramLogin)
|
||||||
|
api.GET("/miniprogram/pay", handler.MiniprogramPay)
|
||||||
|
api.POST("/miniprogram/pay", handler.MiniprogramPay)
|
||||||
|
api.POST("/miniprogram/pay/notify", handler.MiniprogramPayNotify)
|
||||||
|
api.POST("/miniprogram/phone", handler.MiniprogramPhone)
|
||||||
|
api.POST("/miniprogram/qrcode", handler.MiniprogramQrcode)
|
||||||
|
|
||||||
|
// ----- 订单 -----
|
||||||
|
api.GET("/orders", handler.OrdersList)
|
||||||
|
|
||||||
|
// ----- 支付 -----
|
||||||
|
api.POST("/payment/alipay/notify", handler.PaymentAlipayNotify)
|
||||||
|
api.POST("/payment/callback", handler.PaymentCallback)
|
||||||
|
api.POST("/payment/create-order", handler.PaymentCreateOrder)
|
||||||
|
api.GET("/payment/methods", handler.PaymentMethods)
|
||||||
|
api.GET("/payment/query", handler.PaymentQuery)
|
||||||
|
api.GET("/payment/status/:orderSn", handler.PaymentStatusOrderSn)
|
||||||
|
api.POST("/payment/verify", handler.PaymentVerify)
|
||||||
|
api.POST("/payment/wechat/notify", handler.PaymentWechatNotify)
|
||||||
|
api.POST("/payment/wechat/transfer/notify", handler.PaymentWechatTransferNotify)
|
||||||
|
|
||||||
|
// ----- 推荐 -----
|
||||||
|
api.POST("/referral/bind", handler.ReferralBind)
|
||||||
|
api.GET("/referral/data", handler.ReferralData)
|
||||||
|
api.POST("/referral/visit", handler.ReferralVisit)
|
||||||
|
|
||||||
|
// ----- 搜索 -----
|
||||||
|
api.GET("/search", handler.SearchGet)
|
||||||
|
|
||||||
|
// ----- 同步 -----
|
||||||
|
api.GET("/sync", handler.SyncGet)
|
||||||
|
api.POST("/sync", handler.SyncPost)
|
||||||
|
api.PUT("/sync", handler.SyncPut)
|
||||||
|
|
||||||
|
// ----- 上传 -----
|
||||||
|
api.POST("/upload", handler.UploadPost)
|
||||||
|
api.DELETE("/upload", handler.UploadDelete)
|
||||||
|
|
||||||
|
// ----- 用户 -----
|
||||||
|
api.GET("/user/addresses", handler.UserAddressesGet)
|
||||||
|
api.POST("/user/addresses", handler.UserAddressesPost)
|
||||||
|
api.GET("/user/addresses/:id", handler.UserAddressesByID)
|
||||||
|
api.PUT("/user/addresses/:id", handler.UserAddressesByID)
|
||||||
|
api.DELETE("/user/addresses/:id", handler.UserAddressesByID)
|
||||||
|
api.GET("/user/check-purchased", handler.UserCheckPurchased)
|
||||||
|
api.GET("/user/profile", handler.UserProfileGet)
|
||||||
|
api.POST("/user/profile", handler.UserProfilePost)
|
||||||
|
api.GET("/user/purchase-status", handler.UserPurchaseStatus)
|
||||||
|
api.GET("/user/reading-progress", handler.UserReadingProgressGet)
|
||||||
|
api.POST("/user/reading-progress", handler.UserReadingProgressPost)
|
||||||
|
api.GET("/user/track", handler.UserTrackGet)
|
||||||
|
api.POST("/user/track", handler.UserTrackPost)
|
||||||
|
api.POST("/user/update", handler.UserUpdate)
|
||||||
|
|
||||||
|
// ----- 微信登录 -----
|
||||||
|
api.POST("/wechat/login", handler.WechatLogin)
|
||||||
|
|
||||||
|
// ----- 提现 -----
|
||||||
|
api.POST("/withdraw", handler.WithdrawPost)
|
||||||
|
api.GET("/withdraw/records", handler.WithdrawRecords)
|
||||||
|
api.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.GET("/health", func(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{"status": "ok"})
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
1
soul-api/internal/service/README.txt
Normal file
1
soul-api/internal/service/README.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
在此目录放置业务逻辑,供 handler 调用,例如 AdminService、UserService、PaymentService 等。
|
||||||
BIN
soul-api/server.exe
Normal file
BIN
soul-api/server.exe
Normal file
Binary file not shown.
BIN
soul-api/tmp/main.exe
Normal file
BIN
soul-api/tmp/main.exe
Normal file
Binary file not shown.
120
开发文档/2、架构/Gin技术栈-Go1.25依赖清单.md
Normal file
120
开发文档/2、架构/Gin技术栈-Go1.25依赖清单.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Gin 技术栈依赖清单(适配 Go 1.25.7)
|
||||||
|
|
||||||
|
**目标版本**:Go 1.25.7
|
||||||
|
**原则**:精简、高效、易上手、好用、安全;所有依赖均兼容 Go 1.25,无需更换或移除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、Go 版本说明
|
||||||
|
|
||||||
|
- Go 1.25 于 2025 年 8 月发布,遵守 Go 1 兼容性承诺,现有主流库均可使用。
|
||||||
|
- **Gin** 官方要求 Go 1.24+,1.25.7 满足要求。
|
||||||
|
- **golang.org/x/\***(crypto、time 等)随 Go 工具链维护,支持当前稳定版。
|
||||||
|
- 若某依赖未显式声明支持 1.25,只要其 `go.mod` 为 `go 1.21` 或更高,在 1.25 下均可正常编译使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、依赖列表(均适配 Go 1.25.7)
|
||||||
|
|
||||||
|
### 1. 核心与数据库
|
||||||
|
|
||||||
|
| 依赖 | 版本建议 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `github.com/gin-gonic/gin` | 最新 v1.x | 要求 Go 1.24+,1.25.7 兼容。 |
|
||||||
|
| `gorm.io/gorm` | 最新 v1.31.x | 无对高版本 Go 的限制。 |
|
||||||
|
| `gorm.io/driver/mysql` | 最新 | 与 GORM 配套。 |
|
||||||
|
|
||||||
|
### 2. 安全
|
||||||
|
|
||||||
|
| 依赖 | 版本建议 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `github.com/unrolled/secure` | v1.17+ | 标准 net/http 中间件,兼容 Go 1.25。 |
|
||||||
|
| `golang.org/x/crypto` | 最新 | 使用 `bcrypt` 等,随 Go 生态更新。 |
|
||||||
|
| `golang.org/x/time` | 最新 | 使用 `rate` 限流,兼容 Go 1.25。 |
|
||||||
|
|
||||||
|
### 3. 配置
|
||||||
|
|
||||||
|
| 依赖 | 版本建议 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `github.com/joho/godotenv` | v1.5.x | 仅读 .env,无高版本 Go 要求。 |
|
||||||
|
| 或 `github.com/caarlos0/env/v11` | v11.x | 解析 env 到结构体,兼容当前 Go。 |
|
||||||
|
|
||||||
|
### 4. 跨域与鉴权
|
||||||
|
|
||||||
|
| 依赖 | 版本建议 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `github.com/gin-contrib/cors` | v1.6+(务必 ≥1.6,修复 CVE) | 与 Gin 1.24+ / Go 1.25 兼容。 |
|
||||||
|
| `github.com/golang-jwt/jwt/v5` | 最新 v5.x | 推荐 v5,与 Go 1.25 兼容。 |
|
||||||
|
|
||||||
|
### 5. 接口文档(可选)
|
||||||
|
|
||||||
|
| 依赖 | 版本建议 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `github.com/swaggo/swag/cmd/swag` | 最新(CLI 工具) | 代码生成,使用最新 CLI 即可。 |
|
||||||
|
| `github.com/swaggo/gin-swagger` | 最新 | 与 Gin 配套。 |
|
||||||
|
| `github.com/swaggo/files` | 最新 | Swagger UI 静态资源。 |
|
||||||
|
|
||||||
|
### 6. 开发工具(仅开发环境)
|
||||||
|
|
||||||
|
| 依赖 | 版本建议 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `github.com/cosmtrek/air` | 最新 | 热重载,与 Go 1.25 兼容。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、无需更换或移除
|
||||||
|
|
||||||
|
- 上述依赖在 Go 1.25.7 下**均无需更换或移除**。
|
||||||
|
- 未列入的冗余依赖(如单独再引入 validator、Viper、zap 等)按此前「精简版」建议已不纳入,无需因 Go 1.25 再改。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、推荐 go.mod 片段(Go 1.25)
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod init soul-server
|
||||||
|
go mod edit -go=1.25
|
||||||
|
```
|
||||||
|
|
||||||
|
然后按需拉取依赖(示例):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/gin-gonic/gin
|
||||||
|
go get gorm.io/gorm gorm.io/driver/mysql
|
||||||
|
go get github.com/unrolled/secure
|
||||||
|
go get golang.org/x/crypto golang.org/x/time
|
||||||
|
go get github.com/gin-contrib/cors
|
||||||
|
go get github.com/golang-jwt/jwt/v5
|
||||||
|
go get github.com/joho/godotenv
|
||||||
|
# 可选
|
||||||
|
go get github.com/swaggo/gin-swagger github.com/swaggo/files
|
||||||
|
# 开发
|
||||||
|
go install github.com/cosmtrek/air@latest
|
||||||
|
go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、验证方式
|
||||||
|
|
||||||
|
在 soul-server 目录下执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
go build ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
若通过,则当前依赖与 Go 1.25.7 兼容。若某库报错,优先升级该库至最新 minor/patch 再试。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、小结
|
||||||
|
|
||||||
|
| 项目 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| Go 1.25.7 | 支持,所有推荐依赖均适用。 |
|
||||||
|
| 需要更换的依赖 | 无。 |
|
||||||
|
| 需要移除的依赖 | 无(按本清单与精简原则已不包含不必要项)。 |
|
||||||
|
| 建议 | 使用 `go 1.25`,定期 `go get -u ./...` 与 `go mod tidy` 保持依赖健康。 |
|
||||||
Reference in New Issue
Block a user