670 lines
20 KiB
TypeScript
670 lines
20 KiB
TypeScript
|
|
"use client"
|
|||
|
|
|
|||
|
|
import { create } from "zustand"
|
|||
|
|
import { persist } from "zustand/middleware"
|
|||
|
|
import { getFullBookPrice } from "./book-data"
|
|||
|
|
|
|||
|
|
export interface User {
|
|||
|
|
id: string
|
|||
|
|
phone: string
|
|||
|
|
nickname: string
|
|||
|
|
isAdmin: boolean
|
|||
|
|
purchasedSections: string[]
|
|||
|
|
hasFullBook: boolean
|
|||
|
|
referralCode: string
|
|||
|
|
referredBy?: string
|
|||
|
|
earnings: number
|
|||
|
|
pendingEarnings: number
|
|||
|
|
withdrawnEarnings: number
|
|||
|
|
referralCount: number
|
|||
|
|
createdAt: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface Withdrawal {
|
|||
|
|
id: string
|
|||
|
|
userId: string
|
|||
|
|
amount: number
|
|||
|
|
method: "wechat" | "alipay"
|
|||
|
|
account: string
|
|||
|
|
name: string
|
|||
|
|
status: "pending" | "completed" | "rejected"
|
|||
|
|
createdAt: string
|
|||
|
|
completedAt?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface Purchase {
|
|||
|
|
id: string
|
|||
|
|
userId: string
|
|||
|
|
userPhone?: string
|
|||
|
|
userNickname?: string
|
|||
|
|
type: "section" | "fullbook"
|
|||
|
|
sectionId?: string
|
|||
|
|
sectionTitle?: string
|
|||
|
|
amount: number
|
|||
|
|
paymentMethod?: string
|
|||
|
|
referralCode?: string
|
|||
|
|
referrerEarnings?: number
|
|||
|
|
status: "pending" | "completed" | "refunded"
|
|||
|
|
createdAt: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface LiveQRCodeConfig {
|
|||
|
|
id: string
|
|||
|
|
name: string
|
|||
|
|
urls: string[] // 多个URL随机跳转
|
|||
|
|
imageUrl: string
|
|||
|
|
redirectType: "random" | "sequential" | "weighted"
|
|||
|
|
weights?: number[]
|
|||
|
|
clickCount: number
|
|||
|
|
enabled: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface QRCodeConfig {
|
|||
|
|
id: string
|
|||
|
|
name: string
|
|||
|
|
url: string
|
|||
|
|
imageUrl: string
|
|||
|
|
weight: number
|
|||
|
|
enabled: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface PaymentAccountConfig {
|
|||
|
|
wechat: {
|
|||
|
|
enabled: boolean
|
|||
|
|
qrCode: string
|
|||
|
|
account: string
|
|||
|
|
websiteAppId?: string
|
|||
|
|
websiteAppSecret?: string
|
|||
|
|
serviceAppId?: string
|
|||
|
|
serviceAppSecret?: string
|
|||
|
|
mpVerifyCode?: string
|
|||
|
|
merchantId?: string
|
|||
|
|
apiKey?: string
|
|||
|
|
}
|
|||
|
|
alipay: {
|
|||
|
|
enabled: boolean
|
|||
|
|
qrCode: string
|
|||
|
|
account: string
|
|||
|
|
partnerId?: string // PID
|
|||
|
|
securityKey?: string
|
|||
|
|
mobilePayEnabled?: boolean
|
|||
|
|
paymentInterface?: string
|
|||
|
|
}
|
|||
|
|
usdt: {
|
|||
|
|
enabled: boolean
|
|||
|
|
network: "TRC20" | "ERC20" | "BEP20"
|
|||
|
|
address: string
|
|||
|
|
exchangeRate: number
|
|||
|
|
}
|
|||
|
|
paypal: {
|
|||
|
|
enabled: boolean
|
|||
|
|
email: string
|
|||
|
|
exchangeRate: number
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface FeishuSyncConfig {
|
|||
|
|
enabled: boolean
|
|||
|
|
docUrl: string
|
|||
|
|
lastSyncAt?: string
|
|||
|
|
autoSync: boolean
|
|||
|
|
syncInterval: number // 分钟
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface Settings {
|
|||
|
|
distributorShare: number
|
|||
|
|
authorShare: number
|
|||
|
|
paymentMethods: PaymentAccountConfig
|
|||
|
|
sectionPrice: number
|
|||
|
|
baseBookPrice: number
|
|||
|
|
pricePerSection: number
|
|||
|
|
qrCodes: QRCodeConfig[]
|
|||
|
|
liveQRCodes: LiveQRCodeConfig[]
|
|||
|
|
feishuSync: FeishuSyncConfig
|
|||
|
|
authorInfo: {
|
|||
|
|
name: string
|
|||
|
|
description: string
|
|||
|
|
liveTime: string
|
|||
|
|
platform: string
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface StoreState {
|
|||
|
|
user: User | null
|
|||
|
|
isLoggedIn: boolean
|
|||
|
|
purchases: Purchase[]
|
|||
|
|
withdrawals: Withdrawal[]
|
|||
|
|
settings: Settings
|
|||
|
|
|
|||
|
|
login: (phone: string, code: string) => Promise<boolean>
|
|||
|
|
logout: () => void
|
|||
|
|
register: (phone: string, nickname: string, referralCode?: string) => Promise<boolean>
|
|||
|
|
purchaseSection: (sectionId: string, sectionTitle?: string, paymentMethod?: string) => Promise<boolean>
|
|||
|
|
purchaseFullBook: (paymentMethod?: string) => Promise<boolean>
|
|||
|
|
hasPurchased: (sectionId: string) => boolean
|
|||
|
|
adminLogin: (username: string, password: string) => boolean
|
|||
|
|
updateSettings: (newSettings: Partial<Settings>) => void
|
|||
|
|
getAllUsers: () => User[]
|
|||
|
|
getAllPurchases: () => Purchase[]
|
|||
|
|
addUser: (user: Partial<User>) => User
|
|||
|
|
updateUser: (userId: string, updates: Partial<User>) => void
|
|||
|
|
deleteUser: (userId: string) => void
|
|||
|
|
addPurchase: (purchase: Omit<Purchase, "id" | "createdAt">) => void
|
|||
|
|
requestWithdrawal: (amount: number, method: "wechat" | "alipay", account: string, name: string) => void
|
|||
|
|
completeWithdrawal: (id: string) => void
|
|||
|
|
updateLiveQRCode: (config: Partial<LiveQRCodeConfig>) => void
|
|||
|
|
getRandomQRCode: () => QRCodeConfig | null
|
|||
|
|
getLiveQRCodeUrl: (qrId: string) => string | null
|
|||
|
|
exportData: () => string
|
|||
|
|
fetchSettings: () => Promise<void>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const defaultSettings: Settings = {
|
|||
|
|
distributorShare: 90,
|
|||
|
|
authorShare: 10,
|
|||
|
|
paymentMethods: {
|
|||
|
|
wechat: {
|
|||
|
|
enabled: true,
|
|||
|
|
qrCode: "",
|
|||
|
|
account: "",
|
|||
|
|
websiteAppId: "",
|
|||
|
|
websiteAppSecret: "",
|
|||
|
|
serviceAppId: "",
|
|||
|
|
serviceAppSecret: "",
|
|||
|
|
mpVerifyCode: "",
|
|||
|
|
merchantId: "",
|
|||
|
|
apiKey: "",
|
|||
|
|
},
|
|||
|
|
alipay: {
|
|||
|
|
enabled: true,
|
|||
|
|
qrCode: "",
|
|||
|
|
account: "",
|
|||
|
|
partnerId: "",
|
|||
|
|
securityKey: "",
|
|||
|
|
mobilePayEnabled: true,
|
|||
|
|
paymentInterface: "official",
|
|||
|
|
},
|
|||
|
|
usdt: {
|
|||
|
|
enabled: true,
|
|||
|
|
network: "TRC20",
|
|||
|
|
address: "",
|
|||
|
|
exchangeRate: 7.2,
|
|||
|
|
},
|
|||
|
|
paypal: {
|
|||
|
|
enabled: false,
|
|||
|
|
email: "",
|
|||
|
|
exchangeRate: 7.2,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
sectionPrice: 1,
|
|||
|
|
baseBookPrice: 9.9,
|
|||
|
|
pricePerSection: 1,
|
|||
|
|
qrCodes: [
|
|||
|
|
{
|
|||
|
|
id: "default",
|
|||
|
|
name: "Soul派对群",
|
|||
|
|
url: "https://soul.cn/party",
|
|||
|
|
imageUrl: "/images/image.png",
|
|||
|
|
weight: 1,
|
|||
|
|
enabled: true,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
liveQRCodes: [
|
|||
|
|
{
|
|||
|
|
id: "party-group",
|
|||
|
|
name: "派对群活码",
|
|||
|
|
urls: ["https://soul.cn/party1", "https://soul.cn/party2", "https://soul.cn/party3"],
|
|||
|
|
imageUrl: "/images/image.png",
|
|||
|
|
redirectType: "random",
|
|||
|
|
clickCount: 0,
|
|||
|
|
enabled: true,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
feishuSync: {
|
|||
|
|
enabled: false,
|
|||
|
|
docUrl: "",
|
|||
|
|
autoSync: false,
|
|||
|
|
syncInterval: 60,
|
|||
|
|
},
|
|||
|
|
authorInfo: {
|
|||
|
|
name: "卡若",
|
|||
|
|
description: "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
|
|||
|
|
liveTime: "06:00-09:00",
|
|||
|
|
platform: "Soul派对房",
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const useStore = create<StoreState>()(
|
|||
|
|
persist(
|
|||
|
|
(set, get) => ({
|
|||
|
|
user: null,
|
|||
|
|
isLoggedIn: false,
|
|||
|
|
purchases: [],
|
|||
|
|
withdrawals: [],
|
|||
|
|
settings: defaultSettings,
|
|||
|
|
|
|||
|
|
login: async (phone: string, code: string) => {
|
|||
|
|
if (code !== "123456") {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
const existingUser = users.find((u) => u.phone === phone)
|
|||
|
|
if (existingUser) {
|
|||
|
|
set({ user: existingUser, isLoggedIn: true })
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
logout: () => {
|
|||
|
|
set({ user: null, isLoggedIn: false })
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
register: async (phone: string, nickname: string, referralCode?: string) => {
|
|||
|
|
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
if (users.find((u) => u.phone === phone)) {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const newUser: User = {
|
|||
|
|
id: `user_${Date.now()}`,
|
|||
|
|
phone,
|
|||
|
|
nickname,
|
|||
|
|
isAdmin: false,
|
|||
|
|
purchasedSections: [],
|
|||
|
|
hasFullBook: false,
|
|||
|
|
referralCode: `REF${Date.now().toString(36).toUpperCase()}`,
|
|||
|
|
referredBy: referralCode,
|
|||
|
|
earnings: 0,
|
|||
|
|
pendingEarnings: 0,
|
|||
|
|
withdrawnEarnings: 0,
|
|||
|
|
referralCount: 0,
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (referralCode) {
|
|||
|
|
const referrer = users.find((u) => u.referralCode === referralCode)
|
|||
|
|
if (referrer) {
|
|||
|
|
referrer.referralCount = (referrer.referralCount || 0) + 1
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
users.push(newUser)
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
set({ user: newUser, isLoggedIn: true })
|
|||
|
|
return true
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
purchaseSection: async (sectionId: string, sectionTitle?: string, paymentMethod?: string) => {
|
|||
|
|
const { user, settings } = get()
|
|||
|
|
if (!user) return false
|
|||
|
|
|
|||
|
|
const amount = settings.sectionPrice
|
|||
|
|
const purchase: Purchase = {
|
|||
|
|
id: `purchase_${Date.now()}`,
|
|||
|
|
userId: user.id,
|
|||
|
|
userPhone: user.phone,
|
|||
|
|
userNickname: user.nickname,
|
|||
|
|
type: "section",
|
|||
|
|
sectionId,
|
|||
|
|
sectionTitle,
|
|||
|
|
amount,
|
|||
|
|
paymentMethod,
|
|||
|
|
referralCode: user.referredBy,
|
|||
|
|
status: "completed",
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updatedUser = {
|
|||
|
|
...user,
|
|||
|
|
purchasedSections: [...user.purchasedSections, sectionId],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
const userIndex = users.findIndex((u) => u.id === user.id)
|
|||
|
|
if (userIndex !== -1) {
|
|||
|
|
users[userIndex] = updatedUser
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (user.referredBy) {
|
|||
|
|
const referrer = users.find((u) => u.referralCode === user.referredBy)
|
|||
|
|
if (referrer) {
|
|||
|
|
const referrerEarnings = amount * (settings.distributorShare / 100)
|
|||
|
|
referrer.earnings += referrerEarnings
|
|||
|
|
referrer.pendingEarnings = (referrer.pendingEarnings || 0) + referrerEarnings
|
|||
|
|
purchase.referrerEarnings = referrerEarnings
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const purchases = JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
|
|||
|
|
purchases.push(purchase)
|
|||
|
|
localStorage.setItem("all_purchases", JSON.stringify(purchases))
|
|||
|
|
|
|||
|
|
set({ user: updatedUser, purchases: [...get().purchases, purchase] })
|
|||
|
|
return true
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
purchaseFullBook: async (paymentMethod?: string) => {
|
|||
|
|
const { user, settings } = get()
|
|||
|
|
if (!user) return false
|
|||
|
|
|
|||
|
|
const fullBookPrice = getFullBookPrice()
|
|||
|
|
const purchase: Purchase = {
|
|||
|
|
id: `purchase_${Date.now()}`,
|
|||
|
|
userId: user.id,
|
|||
|
|
userPhone: user.phone,
|
|||
|
|
userNickname: user.nickname,
|
|||
|
|
type: "fullbook",
|
|||
|
|
amount: fullBookPrice,
|
|||
|
|
paymentMethod,
|
|||
|
|
referralCode: user.referredBy,
|
|||
|
|
status: "completed",
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updatedUser = { ...user, hasFullBook: true }
|
|||
|
|
|
|||
|
|
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
const userIndex = users.findIndex((u) => u.id === user.id)
|
|||
|
|
if (userIndex !== -1) {
|
|||
|
|
users[userIndex] = updatedUser
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (user.referredBy) {
|
|||
|
|
const referrer = users.find((u) => u.referralCode === user.referredBy)
|
|||
|
|
if (referrer) {
|
|||
|
|
const referrerEarnings = fullBookPrice * (settings.distributorShare / 100)
|
|||
|
|
referrer.earnings += referrerEarnings
|
|||
|
|
referrer.pendingEarnings = (referrer.pendingEarnings || 0) + referrerEarnings
|
|||
|
|
purchase.referrerEarnings = referrerEarnings
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const purchases = JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
|
|||
|
|
purchases.push(purchase)
|
|||
|
|
localStorage.setItem("all_purchases", JSON.stringify(purchases))
|
|||
|
|
|
|||
|
|
set({ user: updatedUser, purchases: [...get().purchases, purchase] })
|
|||
|
|
return true
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
hasPurchased: (sectionId: string) => {
|
|||
|
|
const { user } = get()
|
|||
|
|
if (!user) return false
|
|||
|
|
if (user.hasFullBook) return true
|
|||
|
|
return user.purchasedSections.includes(sectionId)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
adminLogin: (username: string, password: string) => {
|
|||
|
|
if (username.toLowerCase() === "admin" && password === "key123456") {
|
|||
|
|
const adminUser: User = {
|
|||
|
|
id: "admin",
|
|||
|
|
phone: "admin",
|
|||
|
|
nickname: "管理员",
|
|||
|
|
isAdmin: true,
|
|||
|
|
purchasedSections: [],
|
|||
|
|
hasFullBook: true,
|
|||
|
|
referralCode: "ADMIN",
|
|||
|
|
earnings: 0,
|
|||
|
|
pendingEarnings: 0,
|
|||
|
|
withdrawnEarnings: 0,
|
|||
|
|
referralCount: 0,
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
}
|
|||
|
|
set({ user: adminUser, isLoggedIn: true })
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
updateSettings: (newSettings: Partial<Settings>) => {
|
|||
|
|
const { settings } = get()
|
|||
|
|
const updatedSettings = { ...settings, ...newSettings }
|
|||
|
|
if (newSettings.distributorShare !== undefined) {
|
|||
|
|
updatedSettings.authorShare = 100 - newSettings.distributorShare
|
|||
|
|
}
|
|||
|
|
set({ settings: updatedSettings })
|
|||
|
|
if (typeof window !== "undefined") {
|
|||
|
|
localStorage.setItem("app_settings", JSON.stringify(updatedSettings))
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getAllUsers: () => {
|
|||
|
|
if (typeof window === "undefined") return []
|
|||
|
|
return JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getAllPurchases: () => {
|
|||
|
|
if (typeof window === "undefined") return []
|
|||
|
|
return JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
addUser: (userData: Partial<User>) => {
|
|||
|
|
if (typeof window === "undefined") return { id: "temp", ...userData } as User
|
|||
|
|
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
const newUser: User = {
|
|||
|
|
id: `user_${Date.now()}`,
|
|||
|
|
phone: userData.phone || "",
|
|||
|
|
nickname: userData.nickname || "新用户",
|
|||
|
|
isAdmin: false,
|
|||
|
|
purchasedSections: [],
|
|||
|
|
hasFullBook: false,
|
|||
|
|
referralCode: `REF${Date.now().toString(36).toUpperCase()}`,
|
|||
|
|
earnings: 0,
|
|||
|
|
pendingEarnings: 0,
|
|||
|
|
withdrawnEarnings: 0,
|
|||
|
|
referralCount: 0,
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
...userData,
|
|||
|
|
}
|
|||
|
|
users.push(newUser)
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
return newUser
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
updateUser: (userId: string, updates: Partial<User>) => {
|
|||
|
|
if (typeof window === "undefined") return
|
|||
|
|
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
const index = users.findIndex((u) => u.id === userId)
|
|||
|
|
if (index !== -1) {
|
|||
|
|
users[index] = { ...users[index], ...updates }
|
|||
|
|
localStorage.setItem("users", JSON.stringify(users))
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
deleteUser: (userId: string) => {
|
|||
|
|
if (typeof window === "undefined") return
|
|||
|
|
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
|
|||
|
|
const filtered = users.filter((u) => u.id !== userId)
|
|||
|
|
localStorage.setItem("users", JSON.stringify(filtered))
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
addPurchase: (purchaseData) =>
|
|||
|
|
set((state) => {
|
|||
|
|
const newPurchase: Purchase = {
|
|||
|
|
id: Math.random().toString(36).substring(2, 9),
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
...purchaseData,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是全书购买,更新用户状态
|
|||
|
|
if (state.user && purchaseData.userId === state.user.id) {
|
|||
|
|
const updatedUser = { ...state.user }
|
|||
|
|
if (purchaseData.type === "fullbook") {
|
|||
|
|
updatedUser.hasFullBook = true
|
|||
|
|
} else if (purchaseData.sectionId) {
|
|||
|
|
updatedUser.purchasedSections = [
|
|||
|
|
...updatedUser.purchasedSections,
|
|||
|
|
purchaseData.sectionId,
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新 users 数组
|
|||
|
|
const updatedUsers = state.users?.map((u) =>
|
|||
|
|
u.id === updatedUser.id ? updatedUser : u
|
|||
|
|
) || []
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
purchases: [...state.purchases, newPurchase],
|
|||
|
|
user: updatedUser,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
purchases: [...state.purchases, newPurchase],
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
requestWithdrawal: (amount, method, account, name) =>
|
|||
|
|
set((state) => {
|
|||
|
|
if (!state.user) return {}
|
|||
|
|
if (state.user.earnings < amount) return {}
|
|||
|
|
|
|||
|
|
const newWithdrawal: Withdrawal = {
|
|||
|
|
id: Math.random().toString(36).substring(2, 9),
|
|||
|
|
userId: state.user.id,
|
|||
|
|
amount,
|
|||
|
|
method,
|
|||
|
|
account,
|
|||
|
|
name,
|
|||
|
|
status: "pending",
|
|||
|
|
createdAt: new Date().toISOString()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 扣除余额,增加冻结/提现中金额
|
|||
|
|
const updatedUser = {
|
|||
|
|
...state.user,
|
|||
|
|
earnings: state.user.earnings - amount,
|
|||
|
|
pendingEarnings: state.user.pendingEarnings + amount
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
withdrawals: [...(state.withdrawals || []), newWithdrawal],
|
|||
|
|
user: updatedUser,
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
completeWithdrawal: (id) =>
|
|||
|
|
set((state) => {
|
|||
|
|
const withdrawals = state.withdrawals || []
|
|||
|
|
const withdrawalIndex = withdrawals.findIndex(w => w.id === id)
|
|||
|
|
if (withdrawalIndex === -1) return {}
|
|||
|
|
|
|||
|
|
const withdrawal = withdrawals[withdrawalIndex]
|
|||
|
|
if (withdrawal.status !== "pending") return {}
|
|||
|
|
|
|||
|
|
const updatedWithdrawals = [...withdrawals]
|
|||
|
|
updatedWithdrawals[withdrawalIndex] = {
|
|||
|
|
...withdrawal,
|
|||
|
|
status: "completed",
|
|||
|
|
completedAt: new Date().toISOString()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 这里我们只是更新状态,资金已经在申请时扣除了
|
|||
|
|
// 实际场景中可能需要确认转账成功
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
withdrawals: updatedWithdrawals,
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
updateLiveQRCode: (config) =>
|
|||
|
|
set((state) => {
|
|||
|
|
const { settings } = state
|
|||
|
|
const updatedLiveQRCodes = settings.liveQRCodes.map((qr) =>
|
|||
|
|
qr.id === config.id ? { ...qr, ...config } : qr
|
|||
|
|
)
|
|||
|
|
// 如果不存在且有id,则添加? 暂时只支持更新
|
|||
|
|
return {
|
|||
|
|
settings: { ...settings, liveQRCodes: updatedLiveQRCodes },
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
getRandomQRCode: () => {
|
|||
|
|
const { settings } = get()
|
|||
|
|
const enabledQRs = settings.qrCodes.filter((qr) => qr.enabled)
|
|||
|
|
if (enabledQRs.length === 0) return null
|
|||
|
|
const totalWeight = enabledQRs.reduce((sum, qr) => sum + qr.weight, 0)
|
|||
|
|
let random = Math.random() * totalWeight
|
|||
|
|
for (const qr of enabledQRs) {
|
|||
|
|
random -= qr.weight
|
|||
|
|
if (random <= 0) return qr
|
|||
|
|
}
|
|||
|
|
return enabledQRs[0]
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getLiveQRCodeUrl: (qrId: string) => {
|
|||
|
|
const { settings } = get()
|
|||
|
|
const liveQR = settings.liveQRCodes.find((qr) => qr.id === qrId && qr.enabled)
|
|||
|
|
if (!liveQR || liveQR.urls.length === 0) return null
|
|||
|
|
|
|||
|
|
// 更新点击次数
|
|||
|
|
liveQR.clickCount++
|
|||
|
|
|
|||
|
|
if (liveQR.redirectType === "random") {
|
|||
|
|
const randomIndex = Math.floor(Math.random() * liveQR.urls.length)
|
|||
|
|
return liveQR.urls[randomIndex]
|
|||
|
|
} else if (liveQR.redirectType === "sequential") {
|
|||
|
|
const index = liveQR.clickCount % liveQR.urls.length
|
|||
|
|
return liveQR.urls[index]
|
|||
|
|
} else if (liveQR.redirectType === "weighted" && liveQR.weights) {
|
|||
|
|
const totalWeight = liveQR.weights.reduce((sum, w) => sum + w, 0)
|
|||
|
|
let random = Math.random() * totalWeight
|
|||
|
|
for (let i = 0; i < liveQR.urls.length; i++) {
|
|||
|
|
random -= liveQR.weights[i] || 1
|
|||
|
|
if (random <= 0) return liveQR.urls[i]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return liveQR.urls[0]
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
exportData: () => {
|
|||
|
|
const { user, purchases, settings } = get()
|
|||
|
|
const data = {
|
|||
|
|
user,
|
|||
|
|
purchases,
|
|||
|
|
settings,
|
|||
|
|
exportDate: new Date().toISOString(),
|
|||
|
|
}
|
|||
|
|
return JSON.stringify(data, null, 2)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
fetchSettings: async () => {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch("/api/config")
|
|||
|
|
if (!res.ok) throw new Error("Failed to fetch config")
|
|||
|
|
const data = await res.json()
|
|||
|
|
|
|||
|
|
const { settings } = get()
|
|||
|
|
|
|||
|
|
// Deep merge payment methods to preserve existing defaults if API is partial
|
|||
|
|
const mergedPaymentMethods = {
|
|||
|
|
...settings.paymentMethods,
|
|||
|
|
wechat: { ...settings.paymentMethods.wechat, ...data.paymentMethods?.wechat },
|
|||
|
|
alipay: { ...settings.paymentMethods.alipay, ...data.paymentMethods?.alipay },
|
|||
|
|
usdt: { ...settings.paymentMethods.usdt, ...data.paymentMethods?.usdt },
|
|||
|
|
paypal: { ...settings.paymentMethods.paypal, ...data.paymentMethods?.paypal },
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const newSettings: Partial<Settings> = {
|
|||
|
|
paymentMethods: mergedPaymentMethods,
|
|||
|
|
authorInfo: { ...settings.authorInfo, ...data.authorInfo },
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
set({ settings: { ...settings, ...newSettings } })
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("Failed to sync settings:", error)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
}),
|
|||
|
|
{
|
|||
|
|
name: "soul-experiment-storage",
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
)
|