Files
soul/lib/store.ts

665 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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
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 // 分钟
}
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 initialSettings: Settings = {
distributorShare: 90,
authorShare: 10,
paymentMethods: {
alipay: {
enabled: true,
qrCode: "",
account: "",
partnerId: "2088511801157159",
securityKey: "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
mobilePayEnabled: true,
paymentInterface: "official_instant", // 支付宝官方即时到账接口
},
wechat: {
enabled: true,
qrCode: "",
account: "",
websiteAppId: "wx432c93e275548671",
websiteAppSecret: "25b7e7fdb7998e5107e242ebb6ddabd0",
serviceAppId: "wx7c0dbf34ddba300d",
serviceAppSecret: "f865ef18c43dfea6cbe3b1f1aebdb82e",
mpVerifyCode: "SP8AfZJyAvprRORT",
merchantId: "1318592501",
apiKey: "wx3e31b068be59ddc131b068be59ddc2",
groupQrCode: "", // 微信群二维码链接,管理员配置
},
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: initialSettings,
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",
},
),
)