2026-01-21 15:49:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Zustand 状态管理
|
|
|
|
|
|
* 开发: 卡若
|
|
|
|
|
|
* 技术支持: 存客宝
|
|
|
|
|
|
*/
|
2026-01-09 11:58:08 +08:00
|
|
|
|
"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
|
2026-01-21 15:49:12 +08:00
|
|
|
|
wechat?: string
|
|
|
|
|
|
alipay?: string
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
groupQrCode?: string // 微信群二维码链接
|
|
|
|
|
|
}
|
|
|
|
|
|
alipay: {
|
|
|
|
|
|
enabled: boolean
|
|
|
|
|
|
qrCode: string
|
|
|
|
|
|
account: string
|
|
|
|
|
|
partnerId: string // PID 合作者身份
|
|
|
|
|
|
securityKey: string // 安全校验码 Key
|
|
|
|
|
|
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 // 分钟
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 07:57:58 +00:00
|
|
|
|
export interface SiteConfig {
|
|
|
|
|
|
siteName: string
|
|
|
|
|
|
siteTitle: string
|
|
|
|
|
|
siteDescription: string
|
|
|
|
|
|
logo: string
|
|
|
|
|
|
favicon: string
|
|
|
|
|
|
primaryColor: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface MenuConfig {
|
|
|
|
|
|
home: { enabled: boolean; label: string }
|
|
|
|
|
|
chapters: { enabled: boolean; label: string }
|
|
|
|
|
|
match: { enabled: boolean; label: string }
|
|
|
|
|
|
my: { enabled: boolean; label: string }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface PageConfig {
|
|
|
|
|
|
homeTitle: string
|
|
|
|
|
|
homeSubtitle: string
|
|
|
|
|
|
chaptersTitle: string
|
|
|
|
|
|
matchTitle: string
|
|
|
|
|
|
myTitle: string
|
|
|
|
|
|
aboutTitle: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 11:58:08 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-01-14 07:57:58 +00:00
|
|
|
|
siteConfig: SiteConfig
|
|
|
|
|
|
menuConfig: MenuConfig
|
|
|
|
|
|
pageConfig: PageConfig
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 initialSettings: Settings = {
|
|
|
|
|
|
distributorShare: 90,
|
|
|
|
|
|
authorShare: 10,
|
|
|
|
|
|
paymentMethods: {
|
|
|
|
|
|
alipay: {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
enabled: false, // 已禁用支付宝
|
|
|
|
|
|
qrCode: "/images/alipay.png",
|
|
|
|
|
|
account: "卡若",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
partnerId: "2088511801157159",
|
|
|
|
|
|
securityKey: "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
|
|
|
|
|
|
mobilePayEnabled: true,
|
2026-01-14 07:57:58 +00:00
|
|
|
|
paymentInterface: "official_instant",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
wechat: {
|
|
|
|
|
|
enabled: true,
|
2026-01-21 15:49:12 +08:00
|
|
|
|
qrCode: "/images/wechat-pay.png",
|
|
|
|
|
|
account: "卡若",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
websiteAppId: "wx432c93e275548671",
|
|
|
|
|
|
websiteAppSecret: "25b7e7fdb7998e5107e242ebb6ddabd0",
|
|
|
|
|
|
serviceAppId: "wx7c0dbf34ddba300d",
|
|
|
|
|
|
serviceAppSecret: "f865ef18c43dfea6cbe3b1f1aebdb82e",
|
|
|
|
|
|
mpVerifyCode: "SP8AfZJyAvprRORT",
|
|
|
|
|
|
merchantId: "1318592501",
|
|
|
|
|
|
apiKey: "wx3e31b068be59ddc131b068be59ddc2",
|
2026-01-21 15:49:12 +08:00
|
|
|
|
groupQrCode: "/images/party-group-qr.png",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
usdt: {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
enabled: false,
|
2026-01-09 11:58:08 +08:00
|
|
|
|
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派对房",
|
|
|
|
|
|
},
|
2026-01-14 07:57:58 +00:00
|
|
|
|
siteConfig: {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
siteName: "一场soul的创业实验",
|
|
|
|
|
|
siteTitle: "一场soul的创业实验",
|
2026-01-14 07:57:58 +00:00
|
|
|
|
siteDescription: "来自Soul派对房的真实商业故事",
|
|
|
|
|
|
logo: "/logo.png",
|
|
|
|
|
|
favicon: "/favicon.ico",
|
|
|
|
|
|
primaryColor: "#00CED1",
|
|
|
|
|
|
},
|
|
|
|
|
|
menuConfig: {
|
|
|
|
|
|
home: { enabled: true, label: "首页" },
|
|
|
|
|
|
chapters: { enabled: true, label: "目录" },
|
|
|
|
|
|
match: { enabled: true, label: "匹配" },
|
|
|
|
|
|
my: { enabled: true, label: "我的" },
|
|
|
|
|
|
},
|
|
|
|
|
|
pageConfig: {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
homeTitle: "一场soul的创业实验",
|
2026-01-14 07:57:58 +00:00
|
|
|
|
homeSubtitle: "来自Soul派对房的真实商业故事",
|
|
|
|
|
|
chaptersTitle: "我要看",
|
|
|
|
|
|
matchTitle: "语音匹配",
|
|
|
|
|
|
myTitle: "我的",
|
|
|
|
|
|
aboutTitle: "关于作者",
|
|
|
|
|
|
},
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const useStore = create<StoreState>()(
|
|
|
|
|
|
persist(
|
|
|
|
|
|
(set, get) => ({
|
|
|
|
|
|
user: null,
|
|
|
|
|
|
isLoggedIn: false,
|
|
|
|
|
|
purchases: [],
|
|
|
|
|
|
withdrawals: [],
|
|
|
|
|
|
settings: initialSettings,
|
|
|
|
|
|
|
|
|
|
|
|
login: async (phone: string, code: string) => {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// 真实场景下应该调用后端API验证验证码
|
|
|
|
|
|
// 这里暂时保留简单验证用于演示
|
2026-01-09 11:58:08 +08:00
|
|
|
|
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]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 },
|
2026-01-14 07:57:58 +00:00
|
|
|
|
siteConfig: { ...settings.siteConfig, ...data.siteConfig },
|
|
|
|
|
|
menuConfig: { ...settings.menuConfig, ...data.menuConfig },
|
|
|
|
|
|
pageConfig: { ...settings.pageConfig, ...data.pageConfig },
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({ settings: { ...settings, ...newSettings } })
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Failed to sync settings:", error)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "soul-experiment-storage",
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|