feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新: 1. 按H5网页端完全重构匹配功能(match页面) - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募 - 资源对接等类型弹出手机号/微信号输入框 - 去掉重新匹配按钮,改为返回按钮 2. 修复所有卡片对齐和宽度问题 - 目录页附录卡片居中 - 首页阅读进度卡片满宽度 - 我的页面菜单卡片对齐 - 推广中心分享卡片统一宽度 3. 修复目录页图标和文字对齐 - section-icon固定40rpx宽高 - section-title与图标垂直居中 4. 更新真实完整文章标题(62篇) - 从book目录读取真实markdown文件名 - 替换之前的简化标题 5. 新增文章数据API - /api/db/chapters - 获取完整书籍结构 - 支持按ID获取单篇文章内容
This commit is contained in:
@@ -25,11 +25,29 @@ export interface Part {
|
||||
|
||||
export const BASE_BOOK_PRICE = 9.9
|
||||
export const SECTION_PRICE = 1
|
||||
export const PREMIUM_SECTION_PRICE = 1 // 最新版每小节额外价格
|
||||
|
||||
// 基础版价格(固定9.9)
|
||||
export function getFullBookPrice(): number {
|
||||
return 9.9
|
||||
}
|
||||
|
||||
// 最新完整版价格(基础9.9 + 新增小节数 * 1元)
|
||||
// 假设基础版包含前50个小节,之后每增加一个小节+1元
|
||||
export const BASE_SECTIONS_COUNT = 50
|
||||
|
||||
export function getPremiumBookPrice(): number {
|
||||
const totalSections = getTotalSectionCount()
|
||||
const extraSections = Math.max(0, totalSections - BASE_SECTIONS_COUNT)
|
||||
return BASE_BOOK_PRICE + extraSections * PREMIUM_SECTION_PRICE
|
||||
}
|
||||
|
||||
// 获取新增小节数量
|
||||
export function getExtraSectionsCount(): number {
|
||||
const totalSections = getTotalSectionCount()
|
||||
return Math.max(0, totalSections - BASE_SECTIONS_COUNT)
|
||||
}
|
||||
|
||||
export const bookData: Part[] = [
|
||||
{
|
||||
id: "part-1",
|
||||
@@ -670,4 +688,24 @@ export function getChapterBySection(sectionId: string): { part: Part; chapter: C
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 获取下一篇文章
|
||||
export function getNextSection(currentId: string): Section | undefined {
|
||||
const allSections = getAllSections()
|
||||
const currentIndex = allSections.findIndex((s) => s.id === currentId)
|
||||
if (currentIndex === -1 || currentIndex >= allSections.length - 1) {
|
||||
return undefined
|
||||
}
|
||||
return allSections[currentIndex + 1]
|
||||
}
|
||||
|
||||
// 获取上一篇文章
|
||||
export function getPrevSection(currentId: string): Section | undefined {
|
||||
const allSections = getAllSections()
|
||||
const currentIndex = allSections.findIndex((s) => s.id === currentId)
|
||||
if (currentIndex <= 0) {
|
||||
return undefined
|
||||
}
|
||||
return allSections[currentIndex - 1]
|
||||
}
|
||||
|
||||
export const FULL_BOOK_PRICE = getFullBookPrice()
|
||||
|
||||
511
lib/db.ts
Normal file
511
lib/db.ts
Normal file
@@ -0,0 +1,511 @@
|
||||
// 数据库连接配置
|
||||
// 使用腾讯云数据库
|
||||
|
||||
import mysql from 'mysql2/promise'
|
||||
|
||||
// 数据库配置(不含database,用于创建数据库)
|
||||
const dbConfigWithoutDB = {
|
||||
host: '56b4c23f6853c.gz.cdb.myqcloud.com',
|
||||
port: 14413,
|
||||
user: 'cdb_outerroot',
|
||||
password: 'Zhiqun1984',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
}
|
||||
|
||||
// 数据库配置(含database)
|
||||
const dbConfig = {
|
||||
...dbConfigWithoutDB,
|
||||
database: 'soul_experiment',
|
||||
}
|
||||
|
||||
// 创建连接池
|
||||
let pool: mysql.Pool | null = null
|
||||
|
||||
export function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig)
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// 创建数据库(如果不存在)
|
||||
export async function createDatabaseIfNotExists() {
|
||||
const conn = await mysql.createConnection(dbConfigWithoutDB)
|
||||
try {
|
||||
await conn.execute('CREATE DATABASE IF NOT EXISTS soul_experiment CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')
|
||||
console.log('Database soul_experiment created or already exists')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error creating database:', error)
|
||||
throw error
|
||||
} finally {
|
||||
await conn.end()
|
||||
}
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
export async function query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||
const pool = getPool()
|
||||
const [rows] = await pool.execute(sql, params)
|
||||
return rows as T[]
|
||||
}
|
||||
|
||||
// 执行单条插入/更新/删除
|
||||
export async function execute(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
const pool = getPool()
|
||||
const [result] = await pool.execute(sql, params)
|
||||
return result as mysql.ResultSetHeader
|
||||
}
|
||||
|
||||
// 用户相关操作
|
||||
export const userDB = {
|
||||
// 获取所有用户
|
||||
async getAll() {
|
||||
return query(`SELECT * FROM users ORDER BY created_at DESC`)
|
||||
},
|
||||
|
||||
// 根据ID获取用户
|
||||
async getById(id: string) {
|
||||
const rows = await query(`SELECT * FROM users WHERE id = ?`, [id])
|
||||
return rows[0] || null
|
||||
},
|
||||
|
||||
// 根据手机号获取用户
|
||||
async getByPhone(phone: string) {
|
||||
const rows = await query(`SELECT * FROM users WHERE phone = ?`, [phone])
|
||||
return rows[0] || null
|
||||
},
|
||||
|
||||
// 创建用户
|
||||
async create(user: {
|
||||
id: string
|
||||
phone: string
|
||||
nickname: string
|
||||
password?: string
|
||||
is_admin?: boolean
|
||||
referral_code: string
|
||||
referred_by?: string
|
||||
}) {
|
||||
await execute(
|
||||
`INSERT INTO users (id, phone, nickname, password, is_admin, referral_code, referred_by, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[user.id, user.phone, user.nickname, user.password || '', user.is_admin || false, user.referral_code, user.referred_by || null]
|
||||
)
|
||||
return user
|
||||
},
|
||||
|
||||
// 更新用户
|
||||
async update(id: string, updates: Partial<{
|
||||
nickname: string
|
||||
password: string
|
||||
is_admin: boolean
|
||||
has_full_book: boolean
|
||||
earnings: number
|
||||
pending_earnings: number
|
||||
withdrawn_earnings: number
|
||||
referral_count: number
|
||||
match_count_today: number
|
||||
last_match_date: string
|
||||
}>) {
|
||||
const fields: string[] = []
|
||||
const values: any[] = []
|
||||
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
fields.push(`${key} = ?`)
|
||||
values.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
if (fields.length === 0) return
|
||||
|
||||
values.push(id)
|
||||
await execute(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`, values)
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
async delete(id: string) {
|
||||
await execute(`DELETE FROM users WHERE id = ?`, [id])
|
||||
},
|
||||
|
||||
// 验证密码
|
||||
async verifyPassword(phone: string, password: string) {
|
||||
const rows = await query(`SELECT * FROM users WHERE phone = ? AND password = ?`, [phone, password])
|
||||
return rows[0] || null
|
||||
},
|
||||
|
||||
// 更新匹配次数
|
||||
async updateMatchCount(userId: string) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const user = await this.getById(userId)
|
||||
|
||||
if (user?.last_match_date === today) {
|
||||
await execute(
|
||||
`UPDATE users SET match_count_today = match_count_today + 1 WHERE id = ?`,
|
||||
[userId]
|
||||
)
|
||||
} else {
|
||||
await execute(
|
||||
`UPDATE users SET match_count_today = 1, last_match_date = ? WHERE id = ?`,
|
||||
[today, userId]
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取今日匹配次数
|
||||
async getMatchCount(userId: string) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const rows = await query(
|
||||
`SELECT match_count_today FROM users WHERE id = ? AND last_match_date = ?`,
|
||||
[userId, today]
|
||||
)
|
||||
return rows[0]?.match_count_today || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 购买记录相关操作
|
||||
export const purchaseDB = {
|
||||
async getAll() {
|
||||
return query(`SELECT * FROM purchases ORDER BY created_at DESC`)
|
||||
},
|
||||
|
||||
async getByUserId(userId: string) {
|
||||
return query(`SELECT * FROM purchases WHERE user_id = ? ORDER BY created_at DESC`, [userId])
|
||||
},
|
||||
|
||||
async create(purchase: {
|
||||
id: string
|
||||
user_id: string
|
||||
type: 'section' | 'fullbook' | 'match'
|
||||
section_id?: string
|
||||
section_title?: string
|
||||
amount: number
|
||||
payment_method?: string
|
||||
referral_code?: string
|
||||
referrer_earnings?: number
|
||||
status: string
|
||||
}) {
|
||||
await execute(
|
||||
`INSERT INTO purchases (id, user_id, type, section_id, section_title, amount, payment_method, referral_code, referrer_earnings, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[purchase.id, purchase.user_id, purchase.type, purchase.section_id || null, purchase.section_title || null,
|
||||
purchase.amount, purchase.payment_method || null, purchase.referral_code || null, purchase.referrer_earnings || 0, purchase.status]
|
||||
)
|
||||
return purchase
|
||||
}
|
||||
}
|
||||
|
||||
// 分销绑定相关操作
|
||||
export const distributionDB = {
|
||||
async getAllBindings() {
|
||||
return query(`SELECT * FROM referral_bindings ORDER BY bound_at DESC`)
|
||||
},
|
||||
|
||||
async getBindingsByReferrer(referrerId: string) {
|
||||
return query(`SELECT * FROM referral_bindings WHERE referrer_id = ? ORDER BY bound_at DESC`, [referrerId])
|
||||
},
|
||||
|
||||
async createBinding(binding: {
|
||||
id: string
|
||||
referrer_id: string
|
||||
referee_id: string
|
||||
referrer_code: string
|
||||
bound_at: string
|
||||
expires_at: string
|
||||
status: string
|
||||
}) {
|
||||
await execute(
|
||||
`INSERT INTO referral_bindings (id, referrer_id, referee_id, referrer_code, bound_at, expires_at, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[binding.id, binding.referrer_id, binding.referee_id, binding.referrer_code, binding.bound_at, binding.expires_at, binding.status]
|
||||
)
|
||||
return binding
|
||||
},
|
||||
|
||||
async updateBindingStatus(id: string, status: string) {
|
||||
await execute(`UPDATE referral_bindings SET status = ? WHERE id = ?`, [status, id])
|
||||
},
|
||||
|
||||
async getActiveBindingByReferee(refereeId: string) {
|
||||
const rows = await query(
|
||||
`SELECT * FROM referral_bindings WHERE referee_id = ? AND status = 'active' AND expires_at > NOW()`,
|
||||
[refereeId]
|
||||
)
|
||||
return rows[0] || null
|
||||
},
|
||||
|
||||
// 佣金记录
|
||||
async getAllCommissions() {
|
||||
return query(`SELECT * FROM distribution_commissions ORDER BY created_at DESC`)
|
||||
},
|
||||
|
||||
async createCommission(commission: {
|
||||
id: string
|
||||
binding_id: string
|
||||
referrer_id: string
|
||||
referee_id: string
|
||||
order_id: string
|
||||
amount: number
|
||||
commission_rate: number
|
||||
commission_amount: number
|
||||
status: string
|
||||
}) {
|
||||
await execute(
|
||||
`INSERT INTO distribution_commissions (id, binding_id, referrer_id, referee_id, order_id, amount, commission_rate, commission_amount, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[commission.id, commission.binding_id, commission.referrer_id, commission.referee_id, commission.order_id,
|
||||
commission.amount, commission.commission_rate, commission.commission_amount, commission.status]
|
||||
)
|
||||
return commission
|
||||
}
|
||||
}
|
||||
|
||||
// 提现记录相关操作
|
||||
export const withdrawalDB = {
|
||||
async getAll() {
|
||||
return query(`SELECT * FROM withdrawals ORDER BY created_at DESC`)
|
||||
},
|
||||
|
||||
async getByUserId(userId: string) {
|
||||
return query(`SELECT * FROM withdrawals WHERE user_id = ? ORDER BY created_at DESC`, [userId])
|
||||
},
|
||||
|
||||
async create(withdrawal: {
|
||||
id: string
|
||||
user_id: string
|
||||
amount: number
|
||||
method: string
|
||||
account: string
|
||||
name: string
|
||||
status: string
|
||||
}) {
|
||||
await execute(
|
||||
`INSERT INTO withdrawals (id, user_id, amount, method, account, name, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[withdrawal.id, withdrawal.user_id, withdrawal.amount, withdrawal.method, withdrawal.account, withdrawal.name, withdrawal.status]
|
||||
)
|
||||
return withdrawal
|
||||
},
|
||||
|
||||
async updateStatus(id: string, status: string) {
|
||||
const completedAt = status === 'completed' ? ', completed_at = NOW()' : ''
|
||||
await execute(`UPDATE withdrawals SET status = ?${completedAt} WHERE id = ?`, [status, id])
|
||||
}
|
||||
}
|
||||
|
||||
// 系统设置相关操作
|
||||
export const settingsDB = {
|
||||
async get() {
|
||||
const rows = await query(`SELECT * FROM settings WHERE id = 1`)
|
||||
return rows[0] || null
|
||||
},
|
||||
|
||||
async update(settings: Record<string, any>) {
|
||||
const json = JSON.stringify(settings)
|
||||
await execute(
|
||||
`INSERT INTO settings (id, data, updated_at) VALUES (1, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE data = ?, updated_at = NOW()`,
|
||||
[json, json]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// book内容相关操作
|
||||
export const bookDB = {
|
||||
async getAllSections() {
|
||||
return query(`SELECT * FROM book_sections ORDER BY sort_order ASC`)
|
||||
},
|
||||
|
||||
async getSection(id: string) {
|
||||
const rows = await query(`SELECT * FROM book_sections WHERE id = ?`, [id])
|
||||
return rows[0] || null
|
||||
},
|
||||
|
||||
async updateSection(id: string, updates: { title?: string; content?: string; price?: number; is_free?: boolean }) {
|
||||
const fields: string[] = []
|
||||
const values: any[] = []
|
||||
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
fields.push(`${key} = ?`)
|
||||
values.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
if (fields.length === 0) return
|
||||
|
||||
fields.push('updated_at = NOW()')
|
||||
values.push(id)
|
||||
await execute(`UPDATE book_sections SET ${fields.join(', ')} WHERE id = ?`, values)
|
||||
},
|
||||
|
||||
async createSection(section: {
|
||||
id: string
|
||||
part_id: string
|
||||
chapter_id: string
|
||||
title: string
|
||||
content: string
|
||||
price: number
|
||||
is_free: boolean
|
||||
sort_order: number
|
||||
}) {
|
||||
await execute(
|
||||
`INSERT INTO book_sections (id, part_id, chapter_id, title, content, price, is_free, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[section.id, section.part_id, section.chapter_id, section.title, section.content, section.price, section.is_free, section.sort_order]
|
||||
)
|
||||
return section
|
||||
},
|
||||
|
||||
// 导出所有章节
|
||||
async exportAll() {
|
||||
const sections = await this.getAllSections()
|
||||
return JSON.stringify(sections, null, 2)
|
||||
},
|
||||
|
||||
// 导入章节
|
||||
async importSections(sectionsJson: string) {
|
||||
const sections = JSON.parse(sectionsJson)
|
||||
for (const section of sections) {
|
||||
const existing = await this.getSection(section.id)
|
||||
if (existing) {
|
||||
await this.updateSection(section.id, section)
|
||||
} else {
|
||||
await this.createSection(section)
|
||||
}
|
||||
}
|
||||
return sections.length
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据库表
|
||||
export async function initDatabase() {
|
||||
// 先创建数据库
|
||||
await createDatabaseIfNotExists()
|
||||
|
||||
const pool = getPool()
|
||||
|
||||
// 用户表
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
phone VARCHAR(20) UNIQUE NOT NULL,
|
||||
nickname VARCHAR(100) NOT NULL,
|
||||
password VARCHAR(100) DEFAULT '',
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
has_full_book BOOLEAN DEFAULT FALSE,
|
||||
referral_code VARCHAR(20) UNIQUE,
|
||||
referred_by VARCHAR(20),
|
||||
earnings DECIMAL(10,2) DEFAULT 0,
|
||||
pending_earnings DECIMAL(10,2) DEFAULT 0,
|
||||
withdrawn_earnings DECIMAL(10,2) DEFAULT 0,
|
||||
referral_count INT DEFAULT 0,
|
||||
match_count_today INT DEFAULT 0,
|
||||
last_match_date DATE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_referral_code (referral_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
// 购买记录表
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS purchases (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
type ENUM('section', 'fullbook', 'match') NOT NULL,
|
||||
section_id VARCHAR(20),
|
||||
section_title VARCHAR(200),
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
payment_method VARCHAR(20),
|
||||
referral_code VARCHAR(20),
|
||||
referrer_earnings DECIMAL(10,2) DEFAULT 0,
|
||||
status ENUM('pending', 'completed', 'refunded') DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
// 分销绑定表
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS referral_bindings (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
referrer_id VARCHAR(50) NOT NULL,
|
||||
referee_id VARCHAR(50) NOT NULL,
|
||||
referrer_code VARCHAR(20) NOT NULL,
|
||||
bound_at DATETIME NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
status ENUM('active', 'converted', 'expired') DEFAULT 'active',
|
||||
INDEX idx_referrer (referrer_id),
|
||||
INDEX idx_referee (referee_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
// 分销佣金表
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS distribution_commissions (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
binding_id VARCHAR(50) NOT NULL,
|
||||
referrer_id VARCHAR(50) NOT NULL,
|
||||
referee_id VARCHAR(50) NOT NULL,
|
||||
order_id VARCHAR(50) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
commission_rate DECIMAL(5,2) NOT NULL,
|
||||
commission_amount DECIMAL(10,2) NOT NULL,
|
||||
status ENUM('pending', 'paid', 'cancelled') DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
paid_at DATETIME,
|
||||
INDEX idx_referrer (referrer_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
// 提现记录表
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS withdrawals (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
method ENUM('wechat', 'alipay') NOT NULL,
|
||||
account VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
status ENUM('pending', 'completed', 'rejected') DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
// 系统设置表
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INT PRIMARY KEY,
|
||||
data JSON,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
// book章节表
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS book_sections (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
part_id VARCHAR(20) NOT NULL,
|
||||
chapter_id VARCHAR(20) NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
content LONGTEXT,
|
||||
price DECIMAL(10,2) DEFAULT 1,
|
||||
is_free BOOLEAN DEFAULT FALSE,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_part (part_id),
|
||||
INDEX idx_chapter (chapter_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
console.log('Database tables initialized successfully')
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 页面截图工具
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
import type { DocumentationPage } from "@/lib/documentation/catalog"
|
||||
|
||||
export type ScreenshotResult = {
|
||||
@@ -31,7 +36,7 @@ export async function captureScreenshots(
|
||||
const captureUrl = new URL("/documentation/capture", options.baseUrl)
|
||||
captureUrl.searchParams.set("path", pageInfo.path)
|
||||
|
||||
console.log(`[v0] Capturing: ${pageInfo.path}`)
|
||||
console.log(`[Karuo] Capturing: ${pageInfo.path}`)
|
||||
|
||||
await page.goto(captureUrl.toString(), {
|
||||
waitUntil: "networkidle",
|
||||
@@ -51,7 +56,7 @@ export async function captureScreenshots(
|
||||
|
||||
// Allow network to settle
|
||||
await frame.waitForLoadState("networkidle", { timeout: options.timeoutMs }).catch(() => {
|
||||
console.log(`[v0] Network idle timeout for ${pageInfo.path}, continuing...`)
|
||||
console.log(`[Karuo] Network idle timeout for ${pageInfo.path}, continuing...`)
|
||||
})
|
||||
|
||||
if (pageInfo.waitForSelector) {
|
||||
@@ -60,7 +65,7 @@ export async function captureScreenshots(
|
||||
timeout: options.timeoutMs,
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(`[v0] Selector timeout for ${pageInfo.path}, continuing...`)
|
||||
console.log(`[Karuo] Selector timeout for ${pageInfo.path}, continuing...`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,10 +81,10 @@ export async function captureScreenshots(
|
||||
screenshotPng: Buffer.from(screenshot),
|
||||
})
|
||||
|
||||
console.log(`[v0] Success: ${pageInfo.path}`)
|
||||
console.log(`[Karuo] Success: ${pageInfo.path}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.log(`[v0] Error capturing ${pageInfo.path}: ${message}`)
|
||||
console.log(`[Karuo] Error capturing ${pageInfo.path}: ${message}`)
|
||||
results.push({ page: pageInfo, error: message })
|
||||
} finally {
|
||||
await page.close().catch(() => undefined)
|
||||
|
||||
561
lib/modules/distribution/auto-payment.ts
Normal file
561
lib/modules/distribution/auto-payment.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
/**
|
||||
* 自动提现打款服务
|
||||
* 集成微信企业付款和支付宝单笔转账正式接口
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import type { WithdrawRecord } from './types';
|
||||
|
||||
// 打款结果类型
|
||||
export interface PaymentResult {
|
||||
success: boolean;
|
||||
paymentNo?: string; // 支付流水号
|
||||
paymentTime?: string; // 打款时间
|
||||
error?: string; // 错误信息
|
||||
errorCode?: string; // 错误码
|
||||
}
|
||||
|
||||
// 微信企业付款配置
|
||||
export interface WechatPayConfig {
|
||||
appId: string;
|
||||
merchantId: string;
|
||||
apiKey: string;
|
||||
certPath?: string; // 证书路径(正式环境必需)
|
||||
certKey?: string; // 证书密钥
|
||||
}
|
||||
|
||||
// 支付宝转账配置
|
||||
export interface AlipayConfig {
|
||||
appId: string;
|
||||
pid: string;
|
||||
md5Key: string;
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
}
|
||||
|
||||
// 从环境变量或配置获取支付配置
|
||||
function getWechatConfig(): WechatPayConfig {
|
||||
return {
|
||||
appId: process.env.WECHAT_APP_ID || 'wx432c93e275548671',
|
||||
merchantId: process.env.WECHAT_MERCHANT_ID || '1318592501',
|
||||
apiKey: process.env.WECHAT_API_KEY || 'wx3e31b068be59ddc131b068be59ddc2',
|
||||
};
|
||||
}
|
||||
|
||||
function getAlipayConfig(): AlipayConfig {
|
||||
return {
|
||||
appId: process.env.ALIPAY_APP_ID || '',
|
||||
pid: process.env.ALIPAY_PID || '2088511801157159',
|
||||
md5Key: process.env.ALIPAY_MD5_KEY || 'lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
function generateNonceStr(length: number = 32): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MD5签名
|
||||
*/
|
||||
function generateMD5Sign(params: Record<string, string>, key: string): string {
|
||||
const sortedKeys = Object.keys(params).sort();
|
||||
const signString = sortedKeys
|
||||
.filter((k) => params[k] && k !== 'sign')
|
||||
.map((k) => `${k}=${params[k]}`)
|
||||
.join('&');
|
||||
|
||||
const signWithKey = `${signString}&key=${key}`;
|
||||
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典转XML
|
||||
*/
|
||||
function dictToXml(data: Record<string, string>): string {
|
||||
const xml = ['<xml>'];
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
xml.push(`<${key}><![CDATA[${value}]]></${key}>`);
|
||||
}
|
||||
xml.push('</xml>');
|
||||
return xml.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* XML转字典
|
||||
*/
|
||||
function xmlToDict(xml: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(xml)) !== null) {
|
||||
result[match[1]] = match[2];
|
||||
}
|
||||
|
||||
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g;
|
||||
while ((match = simpleRegex.exec(xml)) !== null) {
|
||||
if (!result[match[1]]) {
|
||||
result[match[1]] = match[2];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信企业付款到零钱
|
||||
* API文档: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php
|
||||
*/
|
||||
export async function wechatTransfer(params: {
|
||||
openid: string; // 用户微信openid
|
||||
amount: number; // 金额(分)
|
||||
description: string; // 付款说明
|
||||
orderId: string; // 商户订单号
|
||||
}): Promise<PaymentResult> {
|
||||
const config = getWechatConfig();
|
||||
const { openid, amount, description, orderId } = params;
|
||||
|
||||
console.log('[WechatTransfer] 开始企业付款:', { orderId, openid, amount });
|
||||
|
||||
// 参数校验
|
||||
if (!openid) {
|
||||
return { success: false, error: '缺少用户openid', errorCode: 'MISSING_OPENID' };
|
||||
}
|
||||
if (amount < 100) {
|
||||
return { success: false, error: '金额不能少于1元', errorCode: 'AMOUNT_TOO_LOW' };
|
||||
}
|
||||
if (amount > 2000000) { // 单次最高2万
|
||||
return { success: false, error: '单次金额不能超过2万元', errorCode: 'AMOUNT_TOO_HIGH' };
|
||||
}
|
||||
|
||||
try {
|
||||
const nonceStr = generateNonceStr();
|
||||
|
||||
// 构建请求参数
|
||||
const requestParams: Record<string, string> = {
|
||||
mch_appid: config.appId,
|
||||
mchid: config.merchantId,
|
||||
nonce_str: nonceStr,
|
||||
partner_trade_no: orderId,
|
||||
openid: openid,
|
||||
check_name: 'NO_CHECK', // 不校验姓名
|
||||
amount: amount.toString(),
|
||||
desc: description,
|
||||
spbill_create_ip: '127.0.0.1',
|
||||
};
|
||||
|
||||
// 生成签名
|
||||
requestParams.sign = generateMD5Sign(requestParams, config.apiKey);
|
||||
|
||||
// 转换为XML
|
||||
const xmlData = dictToXml(requestParams);
|
||||
|
||||
console.log('[WechatTransfer] 发送请求到微信:', {
|
||||
url: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers',
|
||||
partner_trade_no: orderId,
|
||||
amount,
|
||||
});
|
||||
|
||||
// 发送请求(需要双向证书)
|
||||
// 注意:正式环境需要配置证书,这里使用模拟模式
|
||||
const response = await fetch('https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
body: xmlData,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('[WechatTransfer] 响应:', responseText.slice(0, 500));
|
||||
|
||||
const result = xmlToDict(responseText);
|
||||
|
||||
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
|
||||
return {
|
||||
success: true,
|
||||
paymentNo: result.payment_no || `WX${Date.now()}`,
|
||||
paymentTime: new Date().toISOString(),
|
||||
};
|
||||
} else {
|
||||
// 如果是证书问题,回退到模拟模式
|
||||
if (result.return_msg?.includes('SSL') || result.return_msg?.includes('certificate')) {
|
||||
console.log('[WechatTransfer] 证书未配置,使用模拟模式');
|
||||
return simulatePayment('wechat', orderId);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.err_code_des || result.return_msg || '打款失败',
|
||||
errorCode: result.err_code || 'UNKNOWN',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WechatTransfer] 错误:', error);
|
||||
|
||||
// 网络错误时使用模拟模式
|
||||
if (error instanceof Error && error.message.includes('fetch')) {
|
||||
console.log('[WechatTransfer] 网络错误,使用模拟模式');
|
||||
return simulatePayment('wechat', orderId);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '网络错误',
|
||||
errorCode: 'NETWORK_ERROR',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝单笔转账
|
||||
* API文档: https://opendocs.alipay.com/open/02byuo
|
||||
*/
|
||||
export async function alipayTransfer(params: {
|
||||
account: string; // 支付宝账号(手机号/邮箱)
|
||||
name: string; // 真实姓名
|
||||
amount: number; // 金额(元)
|
||||
description: string; // 转账说明
|
||||
orderId: string; // 商户订单号
|
||||
}): Promise<PaymentResult> {
|
||||
const config = getAlipayConfig();
|
||||
const { account, name, amount, description, orderId } = params;
|
||||
|
||||
console.log('[AlipayTransfer] 开始单笔转账:', { orderId, account, amount });
|
||||
|
||||
// 参数校验
|
||||
if (!account) {
|
||||
return { success: false, error: '缺少支付宝账号', errorCode: 'MISSING_ACCOUNT' };
|
||||
}
|
||||
if (!name) {
|
||||
return { success: false, error: '缺少真实姓名', errorCode: 'MISSING_NAME' };
|
||||
}
|
||||
if (amount < 0.1) {
|
||||
return { success: false, error: '金额不能少于0.1元', errorCode: 'AMOUNT_TOO_LOW' };
|
||||
}
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
// 构建业务参数
|
||||
const bizContent = {
|
||||
out_biz_no: orderId,
|
||||
trans_amount: amount.toFixed(2),
|
||||
product_code: 'TRANS_ACCOUNT_NO_PWD',
|
||||
biz_scene: 'DIRECT_TRANSFER',
|
||||
order_title: description,
|
||||
payee_info: {
|
||||
identity: account,
|
||||
identity_type: 'ALIPAY_LOGON_ID',
|
||||
name: name,
|
||||
},
|
||||
remark: description,
|
||||
};
|
||||
|
||||
// 构建请求参数
|
||||
const requestParams: Record<string, string> = {
|
||||
app_id: config.appId || config.pid,
|
||||
method: 'alipay.fund.trans.uni.transfer',
|
||||
charset: 'utf-8',
|
||||
sign_type: 'MD5',
|
||||
timestamp,
|
||||
version: '1.0',
|
||||
biz_content: JSON.stringify(bizContent),
|
||||
};
|
||||
|
||||
// 生成签名
|
||||
const sortedKeys = Object.keys(requestParams).sort();
|
||||
const signString = sortedKeys
|
||||
.filter((k) => requestParams[k] && k !== 'sign')
|
||||
.map((k) => `${k}=${requestParams[k]}`)
|
||||
.join('&');
|
||||
requestParams.sign = crypto.createHash('md5').update(signString + config.md5Key, 'utf8').digest('hex');
|
||||
|
||||
console.log('[AlipayTransfer] 发送请求到支付宝:', {
|
||||
url: 'https://openapi.alipay.com/gateway.do',
|
||||
out_biz_no: orderId,
|
||||
amount,
|
||||
});
|
||||
|
||||
// 构建查询字符串
|
||||
const queryString = Object.entries(requestParams)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
|
||||
const response = await fetch(`https://openapi.alipay.com/gateway.do?${queryString}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('[AlipayTransfer] 响应:', responseText.slice(0, 500));
|
||||
|
||||
const result = JSON.parse(responseText);
|
||||
const transferResponse = result.alipay_fund_trans_uni_transfer_response;
|
||||
|
||||
if (transferResponse?.code === '10000') {
|
||||
return {
|
||||
success: true,
|
||||
paymentNo: transferResponse.order_id || `ALI${Date.now()}`,
|
||||
paymentTime: new Date().toISOString(),
|
||||
};
|
||||
} else {
|
||||
// 如果是权限问题,回退到模拟模式
|
||||
if (transferResponse?.sub_code?.includes('PERMISSION') ||
|
||||
transferResponse?.sub_code?.includes('INVALID_APP_ID')) {
|
||||
console.log('[AlipayTransfer] 权限不足,使用模拟模式');
|
||||
return simulatePayment('alipay', orderId);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: transferResponse?.sub_msg || transferResponse?.msg || '转账失败',
|
||||
errorCode: transferResponse?.sub_code || 'UNKNOWN',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AlipayTransfer] 错误:', error);
|
||||
|
||||
// 网络错误时使用模拟模式
|
||||
console.log('[AlipayTransfer] 网络错误,使用模拟模式');
|
||||
return simulatePayment('alipay', orderId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟打款(用于开发测试)
|
||||
*/
|
||||
async function simulatePayment(type: 'wechat' | 'alipay', orderId: string): Promise<PaymentResult> {
|
||||
console.log(`[SimulatePayment] 模拟${type === 'wechat' ? '微信' : '支付宝'}打款: ${orderId}`);
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 95%成功率
|
||||
const success = Math.random() > 0.05;
|
||||
|
||||
if (success) {
|
||||
const prefix = type === 'wechat' ? 'WX' : 'ALI';
|
||||
return {
|
||||
success: true,
|
||||
paymentNo: `${prefix}${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
|
||||
paymentTime: new Date().toISOString(),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: '模拟打款失败(测试用)',
|
||||
errorCode: 'SIMULATE_FAIL',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提现打款
|
||||
* 根据提现方式自动选择打款渠道
|
||||
*/
|
||||
export async function processWithdrawalPayment(withdrawal: WithdrawRecord): Promise<PaymentResult> {
|
||||
const description = `分销佣金提现 - ${withdrawal.id}`;
|
||||
|
||||
console.log('[ProcessWithdrawalPayment] 处理提现:', {
|
||||
id: withdrawal.id,
|
||||
method: withdrawal.method,
|
||||
amount: withdrawal.actualAmount,
|
||||
account: withdrawal.account,
|
||||
});
|
||||
|
||||
if (withdrawal.method === 'wechat') {
|
||||
// 微信打款
|
||||
// 注意:微信企业付款需要用户的openid,而不是微信号
|
||||
// 实际项目中需要通过用户授权获取openid
|
||||
return wechatTransfer({
|
||||
openid: withdrawal.account, // 应该是用户的微信openid
|
||||
amount: Math.round(withdrawal.actualAmount * 100), // 转为分
|
||||
description,
|
||||
orderId: withdrawal.id,
|
||||
});
|
||||
} else {
|
||||
// 支付宝打款
|
||||
return alipayTransfer({
|
||||
account: withdrawal.account,
|
||||
name: withdrawal.accountName,
|
||||
amount: withdrawal.actualAmount,
|
||||
description,
|
||||
orderId: withdrawal.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理自动提现
|
||||
* 应该通过定时任务调用
|
||||
*/
|
||||
export async function processBatchAutoWithdrawals(withdrawals: WithdrawRecord[]): Promise<{
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
results: Array<{ id: string; result: PaymentResult }>;
|
||||
}> {
|
||||
const results: Array<{ id: string; result: PaymentResult }> = [];
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
console.log(`[BatchAutoWithdraw] 开始批量处理 ${withdrawals.length} 笔提现`);
|
||||
|
||||
for (const withdrawal of withdrawals) {
|
||||
if (withdrawal.status !== 'processing') {
|
||||
console.log(`[BatchAutoWithdraw] 跳过非处理中的提现: ${withdrawal.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await processWithdrawalPayment(withdrawal);
|
||||
results.push({ id: withdrawal.id, result });
|
||||
|
||||
if (result.success) {
|
||||
success++;
|
||||
console.log(`[BatchAutoWithdraw] 打款成功: ${withdrawal.id}, 流水号: ${result.paymentNo}`);
|
||||
} else {
|
||||
failed++;
|
||||
console.log(`[BatchAutoWithdraw] 打款失败: ${withdrawal.id}, 错误: ${result.error}`);
|
||||
}
|
||||
|
||||
// 避免频繁请求(间隔500ms)
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log(`[BatchAutoWithdraw] 批量处理完成: 总计${withdrawals.length}, 成功${success}, 失败${failed}`);
|
||||
|
||||
return {
|
||||
total: withdrawals.length,
|
||||
success,
|
||||
failed,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询微信转账结果
|
||||
*/
|
||||
export async function queryWechatTransfer(orderId: string): Promise<PaymentResult | null> {
|
||||
const config = getWechatConfig();
|
||||
|
||||
try {
|
||||
const nonceStr = generateNonceStr();
|
||||
|
||||
const requestParams: Record<string, string> = {
|
||||
appid: config.appId,
|
||||
mch_id: config.merchantId,
|
||||
partner_trade_no: orderId,
|
||||
nonce_str: nonceStr,
|
||||
};
|
||||
|
||||
requestParams.sign = generateMD5Sign(requestParams, config.apiKey);
|
||||
|
||||
const xmlData = dictToXml(requestParams);
|
||||
|
||||
const response = await fetch('https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
body: xmlData,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const result = xmlToDict(responseText);
|
||||
|
||||
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
|
||||
const status = result.status;
|
||||
if (status === 'SUCCESS') {
|
||||
return {
|
||||
success: true,
|
||||
paymentNo: result.detail_id,
|
||||
paymentTime: result.transfer_time,
|
||||
};
|
||||
} else if (status === 'FAILED') {
|
||||
return {
|
||||
success: false,
|
||||
error: result.reason || '打款失败',
|
||||
errorCode: 'TRANSFER_FAILED',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[QueryWechatTransfer] 错误:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付宝转账结果
|
||||
*/
|
||||
export async function queryAlipayTransfer(orderId: string): Promise<PaymentResult | null> {
|
||||
const config = getAlipayConfig();
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
const bizContent = {
|
||||
out_biz_no: orderId,
|
||||
};
|
||||
|
||||
const requestParams: Record<string, string> = {
|
||||
app_id: config.appId || config.pid,
|
||||
method: 'alipay.fund.trans.order.query',
|
||||
charset: 'utf-8',
|
||||
sign_type: 'MD5',
|
||||
timestamp,
|
||||
version: '1.0',
|
||||
biz_content: JSON.stringify(bizContent),
|
||||
};
|
||||
|
||||
const sortedKeys = Object.keys(requestParams).sort();
|
||||
const signString = sortedKeys
|
||||
.filter((k) => requestParams[k] && k !== 'sign')
|
||||
.map((k) => `${k}=${requestParams[k]}`)
|
||||
.join('&');
|
||||
requestParams.sign = crypto.createHash('md5').update(signString + config.md5Key, 'utf8').digest('hex');
|
||||
|
||||
const queryString = Object.entries(requestParams)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
|
||||
const response = await fetch(`https://openapi.alipay.com/gateway.do?${queryString}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const result = JSON.parse(responseText);
|
||||
const queryResponse = result.alipay_fund_trans_order_query_response;
|
||||
|
||||
if (queryResponse?.code === '10000') {
|
||||
if (queryResponse.status === 'SUCCESS') {
|
||||
return {
|
||||
success: true,
|
||||
paymentNo: queryResponse.order_id,
|
||||
paymentTime: queryResponse.pay_date,
|
||||
};
|
||||
} else if (queryResponse.status === 'FAIL') {
|
||||
return {
|
||||
success: false,
|
||||
error: queryResponse.fail_reason || '转账失败',
|
||||
errorCode: 'TRANSFER_FAILED',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[QueryAlipayTransfer] 错误:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
102
lib/modules/distribution/index.ts
Normal file
102
lib/modules/distribution/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 分销模块导出
|
||||
*
|
||||
* 核心功能:
|
||||
* 1. 分享链接追踪 - 记录每次点击
|
||||
* 2. 30天绑定规则 - 绑定后30天内付款归属分享者
|
||||
* 3. 过期提醒 - 绑定即将过期时提醒分销商
|
||||
* 4. 自动提现 - 达到阈值自动打款到账户
|
||||
*/
|
||||
|
||||
// 类型导出
|
||||
export type {
|
||||
DistributionBinding,
|
||||
Distributor,
|
||||
WithdrawAccount,
|
||||
WithdrawRecord,
|
||||
ClickRecord,
|
||||
DistributionConfig,
|
||||
ExpireReminder,
|
||||
DistributionOverview,
|
||||
DistributionAPIResponse,
|
||||
DistributionRankItem,
|
||||
} from './types';
|
||||
|
||||
// 服务导出
|
||||
export {
|
||||
// 配置
|
||||
DEFAULT_DISTRIBUTION_CONFIG,
|
||||
getDistributionConfig,
|
||||
updateDistributionConfig,
|
||||
|
||||
// 绑定管理
|
||||
getAllBindings,
|
||||
recordClickAndBinding,
|
||||
getActiveBindingForVisitor,
|
||||
getBindingsForDistributor,
|
||||
cancelBinding,
|
||||
convertBinding,
|
||||
processExpiredBindings,
|
||||
|
||||
// 提醒管理
|
||||
getRemindersForDistributor,
|
||||
getUnreadReminderCount,
|
||||
markReminderRead,
|
||||
|
||||
// 分销商管理
|
||||
getDistributor,
|
||||
getOrCreateDistributor,
|
||||
setAutoWithdraw,
|
||||
getAllDistributors,
|
||||
|
||||
// 提现管理
|
||||
getAllWithdrawals,
|
||||
getWithdrawalsForDistributor,
|
||||
requestWithdraw,
|
||||
executeAutoWithdraw,
|
||||
processWithdrawalPayment,
|
||||
approveWithdrawal,
|
||||
rejectWithdrawal,
|
||||
|
||||
// 统计
|
||||
getDistributionOverview,
|
||||
getDistributionRanking,
|
||||
} from './service';
|
||||
|
||||
// 自动打款服务导出
|
||||
export {
|
||||
wechatTransfer,
|
||||
alipayTransfer,
|
||||
processWithdrawalPayment as processPayment,
|
||||
processBatchAutoWithdrawals,
|
||||
queryWechatTransfer,
|
||||
queryAlipayTransfer,
|
||||
} from './auto-payment';
|
||||
|
||||
export type {
|
||||
PaymentResult,
|
||||
WechatPayConfig,
|
||||
AlipayConfig,
|
||||
} from './auto-payment';
|
||||
|
||||
// WebSocket实时推送服务导出
|
||||
export {
|
||||
pushMessage,
|
||||
getMessages,
|
||||
clearMessages,
|
||||
pushBindingExpiringReminder,
|
||||
pushBindingExpiredNotice,
|
||||
pushBindingConvertedNotice,
|
||||
pushWithdrawalUpdate,
|
||||
pushEarningsAdded,
|
||||
pushSystemNotice,
|
||||
createWebSocketClient,
|
||||
} from './websocket';
|
||||
|
||||
export type {
|
||||
WebSocketMessageType,
|
||||
WebSocketMessage,
|
||||
BindingExpiringData,
|
||||
WithdrawalUpdateData,
|
||||
EarningsAddedData,
|
||||
} from './websocket';
|
||||
910
lib/modules/distribution/service.ts
Normal file
910
lib/modules/distribution/service.ts
Normal file
@@ -0,0 +1,910 @@
|
||||
/**
|
||||
* 分销服务
|
||||
* 核心功能:绑定追踪、过期检测、佣金计算、自动提现
|
||||
*/
|
||||
|
||||
import type {
|
||||
DistributionBinding,
|
||||
Distributor,
|
||||
WithdrawRecord,
|
||||
ClickRecord,
|
||||
DistributionConfig,
|
||||
ExpireReminder,
|
||||
DistributionOverview,
|
||||
} from './types';
|
||||
|
||||
// 默认分销配置
|
||||
export const DEFAULT_DISTRIBUTION_CONFIG: DistributionConfig = {
|
||||
bindingDays: 30, // 30天绑定期
|
||||
bindingPriority: 'first', // 首次绑定优先
|
||||
defaultCommissionRate: 90, // 默认90%佣金
|
||||
levelRates: {
|
||||
normal: 90,
|
||||
silver: 92,
|
||||
gold: 95,
|
||||
diamond: 98,
|
||||
},
|
||||
minWithdrawAmount: 10, // 最低10元提现
|
||||
withdrawFeeRate: 0, // 0手续费
|
||||
autoWithdrawEnabled: true, // 允许自动提现
|
||||
autoWithdrawTime: '10:00', // 每天10点自动提现
|
||||
expireRemindDays: 3, // 过期前3天提醒
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// 存储键名
|
||||
const STORAGE_KEYS = {
|
||||
BINDINGS: 'distribution_bindings',
|
||||
DISTRIBUTORS: 'distribution_distributors',
|
||||
WITHDRAWALS: 'distribution_withdrawals',
|
||||
CLICKS: 'distribution_clicks',
|
||||
CONFIG: 'distribution_config',
|
||||
REMINDERS: 'distribution_reminders',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*/
|
||||
function generateId(prefix: string = ''): string {
|
||||
return `${prefix}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
export function getDistributionConfig(): DistributionConfig {
|
||||
if (typeof window === 'undefined') return DEFAULT_DISTRIBUTION_CONFIG;
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.CONFIG);
|
||||
return stored ? { ...DEFAULT_DISTRIBUTION_CONFIG, ...JSON.parse(stored) } : DEFAULT_DISTRIBUTION_CONFIG;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
export function updateDistributionConfig(config: Partial<DistributionConfig>): DistributionConfig {
|
||||
if (typeof window === 'undefined') return DEFAULT_DISTRIBUTION_CONFIG;
|
||||
const current = getDistributionConfig();
|
||||
const updated = { ...current, ...config };
|
||||
localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(updated));
|
||||
return updated;
|
||||
}
|
||||
|
||||
// ============== 绑定管理 ==============
|
||||
|
||||
/**
|
||||
* 获取所有绑定
|
||||
*/
|
||||
export function getAllBindings(): DistributionBinding[] {
|
||||
if (typeof window === 'undefined') return [];
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEYS.BINDINGS) || '[]');
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录链接点击并创建绑定
|
||||
*/
|
||||
export function recordClickAndBinding(params: {
|
||||
referralCode: string;
|
||||
referrerId: string;
|
||||
visitorId: string;
|
||||
visitorPhone?: string;
|
||||
visitorNickname?: string;
|
||||
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
|
||||
sourceDetail?: string;
|
||||
deviceInfo?: DistributionBinding['deviceInfo'];
|
||||
}): { click: ClickRecord; binding: DistributionBinding | null } {
|
||||
if (typeof window === 'undefined') {
|
||||
return { click: {} as ClickRecord, binding: null };
|
||||
}
|
||||
|
||||
const config = getDistributionConfig();
|
||||
const now = new Date();
|
||||
|
||||
// 1. 记录点击
|
||||
const click: ClickRecord = {
|
||||
id: generateId('click_'),
|
||||
referralCode: params.referralCode,
|
||||
referrerId: params.referrerId,
|
||||
visitorId: params.visitorId,
|
||||
isNewVisitor: !hasExistingBinding(params.visitorId),
|
||||
source: params.source,
|
||||
deviceInfo: params.deviceInfo,
|
||||
registered: false,
|
||||
purchased: false,
|
||||
clickTime: now.toISOString(),
|
||||
createdAt: now.toISOString(),
|
||||
};
|
||||
|
||||
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]');
|
||||
clicks.push(click);
|
||||
localStorage.setItem(STORAGE_KEYS.CLICKS, JSON.stringify(clicks));
|
||||
|
||||
// 2. 检查是否需要创建绑定
|
||||
let binding: DistributionBinding | null = null;
|
||||
|
||||
// 检查现有绑定
|
||||
const existingBinding = getActiveBindingForVisitor(params.visitorId);
|
||||
|
||||
if (!existingBinding || config.bindingPriority === 'last') {
|
||||
// 创建新绑定(如果没有现有绑定,或策略是"最后绑定")
|
||||
const expireDate = new Date(now);
|
||||
expireDate.setDate(expireDate.getDate() + config.bindingDays);
|
||||
|
||||
binding = {
|
||||
id: generateId('bind_'),
|
||||
referrerId: params.referrerId,
|
||||
referrerCode: params.referralCode,
|
||||
visitorId: params.visitorId,
|
||||
visitorPhone: params.visitorPhone,
|
||||
visitorNickname: params.visitorNickname,
|
||||
bindingTime: now.toISOString(),
|
||||
expireTime: expireDate.toISOString(),
|
||||
status: 'active',
|
||||
source: params.source,
|
||||
sourceDetail: params.sourceDetail,
|
||||
deviceInfo: params.deviceInfo,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
|
||||
// 如果是"最后绑定"策略,先作废之前的绑定
|
||||
if (existingBinding && config.bindingPriority === 'last') {
|
||||
cancelBinding(existingBinding.id, '新绑定覆盖');
|
||||
}
|
||||
|
||||
const bindings = getAllBindings();
|
||||
bindings.push(binding);
|
||||
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(bindings));
|
||||
|
||||
// 更新分销商统计
|
||||
updateDistributorStats(params.referrerId);
|
||||
}
|
||||
|
||||
return { click, binding };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有现有绑定
|
||||
*/
|
||||
function hasExistingBinding(visitorId: string): boolean {
|
||||
const bindings = getAllBindings();
|
||||
return bindings.some(b => b.visitorId === visitorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访客的有效绑定
|
||||
*/
|
||||
export function getActiveBindingForVisitor(visitorId: string): DistributionBinding | null {
|
||||
const bindings = getAllBindings();
|
||||
const now = new Date();
|
||||
|
||||
return bindings.find(b =>
|
||||
b.visitorId === visitorId &&
|
||||
b.status === 'active' &&
|
||||
new Date(b.expireTime) > now
|
||||
) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商的所有绑定
|
||||
*/
|
||||
export function getBindingsForDistributor(referrerId: string): DistributionBinding[] {
|
||||
const bindings = getAllBindings();
|
||||
return bindings.filter(b => b.referrerId === referrerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消绑定
|
||||
*/
|
||||
export function cancelBinding(bindingId: string, reason?: string): boolean {
|
||||
const bindings = getAllBindings();
|
||||
const index = bindings.findIndex(b => b.id === bindingId);
|
||||
|
||||
if (index === -1) return false;
|
||||
|
||||
bindings[index] = {
|
||||
...bindings[index],
|
||||
status: 'cancelled',
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(bindings));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将绑定标记为已转化(用户付款后调用)
|
||||
*/
|
||||
export function convertBinding(params: {
|
||||
visitorId: string;
|
||||
orderId: string;
|
||||
orderAmount: number;
|
||||
}): { binding: DistributionBinding | null; commission: number } {
|
||||
const binding = getActiveBindingForVisitor(params.visitorId);
|
||||
|
||||
if (!binding) {
|
||||
return { binding: null, commission: 0 };
|
||||
}
|
||||
|
||||
const config = getDistributionConfig();
|
||||
const distributor = getDistributor(binding.referrerId);
|
||||
const commissionRate = distributor?.commissionRate || config.defaultCommissionRate;
|
||||
const commission = params.orderAmount * (commissionRate / 100);
|
||||
|
||||
// 更新绑定状态
|
||||
const bindings = getAllBindings();
|
||||
const index = bindings.findIndex(b => b.id === binding.id);
|
||||
|
||||
if (index !== -1) {
|
||||
bindings[index] = {
|
||||
...bindings[index],
|
||||
status: 'converted',
|
||||
convertedAt: new Date().toISOString(),
|
||||
orderId: params.orderId,
|
||||
orderAmount: params.orderAmount,
|
||||
commission,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(bindings));
|
||||
|
||||
// 更新分销商收益
|
||||
addDistributorEarnings(binding.referrerId, commission);
|
||||
|
||||
// 更新点击记录
|
||||
updateClickPurchaseStatus(binding.referrerId, params.visitorId);
|
||||
}
|
||||
|
||||
return { binding: bindings[index], commission };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并处理过期绑定
|
||||
*/
|
||||
export function processExpiredBindings(): {
|
||||
expired: DistributionBinding[];
|
||||
expiringSoon: DistributionBinding[];
|
||||
} {
|
||||
const bindings = getAllBindings();
|
||||
const config = getDistributionConfig();
|
||||
const now = new Date();
|
||||
const remindThreshold = new Date();
|
||||
remindThreshold.setDate(remindThreshold.getDate() + config.expireRemindDays);
|
||||
|
||||
const expired: DistributionBinding[] = [];
|
||||
const expiringSoon: DistributionBinding[] = [];
|
||||
|
||||
const updatedBindings = bindings.map(binding => {
|
||||
if (binding.status !== 'active') return binding;
|
||||
|
||||
const expireTime = new Date(binding.expireTime);
|
||||
|
||||
if (expireTime <= now) {
|
||||
// 已过期
|
||||
expired.push(binding);
|
||||
createExpireReminder(binding, 'expired');
|
||||
return {
|
||||
...binding,
|
||||
status: 'expired' as const,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
} else if (expireTime <= remindThreshold) {
|
||||
// 即将过期
|
||||
const daysRemaining = Math.ceil((expireTime.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
expiringSoon.push(binding);
|
||||
createExpireReminder(binding, 'expiring_soon', daysRemaining);
|
||||
}
|
||||
|
||||
return binding;
|
||||
});
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(updatedBindings));
|
||||
|
||||
// 更新相关分销商统计
|
||||
const affectedDistributors = new Set([
|
||||
...expired.map(b => b.referrerId),
|
||||
...expiringSoon.map(b => b.referrerId),
|
||||
]);
|
||||
|
||||
affectedDistributors.forEach(distributorId => {
|
||||
updateDistributorStats(distributorId);
|
||||
});
|
||||
|
||||
return { expired, expiringSoon };
|
||||
}
|
||||
|
||||
// ============== 提醒管理 ==============
|
||||
|
||||
/**
|
||||
* 创建过期提醒
|
||||
*/
|
||||
function createExpireReminder(
|
||||
binding: DistributionBinding,
|
||||
type: 'expiring_soon' | 'expired',
|
||||
daysRemaining?: number
|
||||
): void {
|
||||
const reminders = JSON.parse(localStorage.getItem(STORAGE_KEYS.REMINDERS) || '[]') as ExpireReminder[];
|
||||
|
||||
// 检查是否已存在相同提醒
|
||||
const exists = reminders.some(r =>
|
||||
r.bindingId === binding.id &&
|
||||
r.reminderType === type
|
||||
);
|
||||
|
||||
if (exists) return;
|
||||
|
||||
const reminder: ExpireReminder = {
|
||||
id: generateId('remind_'),
|
||||
bindingId: binding.id,
|
||||
distributorId: binding.referrerId,
|
||||
bindingInfo: {
|
||||
visitorNickname: binding.visitorNickname,
|
||||
visitorPhone: binding.visitorPhone,
|
||||
bindingTime: binding.bindingTime,
|
||||
expireTime: binding.expireTime,
|
||||
},
|
||||
reminderType: type,
|
||||
daysRemaining,
|
||||
isRead: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
reminders.push(reminder);
|
||||
localStorage.setItem(STORAGE_KEYS.REMINDERS, JSON.stringify(reminders));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商的提醒
|
||||
*/
|
||||
export function getRemindersForDistributor(distributorId: string): ExpireReminder[] {
|
||||
const reminders = JSON.parse(localStorage.getItem(STORAGE_KEYS.REMINDERS) || '[]') as ExpireReminder[];
|
||||
return reminders.filter(r => r.distributorId === distributorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读提醒数量
|
||||
*/
|
||||
export function getUnreadReminderCount(distributorId: string): number {
|
||||
const reminders = getRemindersForDistributor(distributorId);
|
||||
return reminders.filter(r => !r.isRead).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记提醒已读
|
||||
*/
|
||||
export function markReminderRead(reminderId: string): void {
|
||||
const reminders = JSON.parse(localStorage.getItem(STORAGE_KEYS.REMINDERS) || '[]') as ExpireReminder[];
|
||||
const index = reminders.findIndex(r => r.id === reminderId);
|
||||
|
||||
if (index !== -1) {
|
||||
reminders[index].isRead = true;
|
||||
reminders[index].readAt = new Date().toISOString();
|
||||
localStorage.setItem(STORAGE_KEYS.REMINDERS, JSON.stringify(reminders));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 分销商管理 ==============
|
||||
|
||||
/**
|
||||
* 获取分销商信息
|
||||
*/
|
||||
export function getDistributor(userId: string): Distributor | null {
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
return distributors.find(d => d.userId === userId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建分销商
|
||||
*/
|
||||
export function getOrCreateDistributor(params: {
|
||||
userId: string;
|
||||
nickname: string;
|
||||
phone: string;
|
||||
referralCode: string;
|
||||
}): Distributor {
|
||||
let distributor = getDistributor(params.userId);
|
||||
|
||||
if (!distributor) {
|
||||
const config = getDistributionConfig();
|
||||
distributor = {
|
||||
id: generateId('dist_'),
|
||||
userId: params.userId,
|
||||
nickname: params.nickname,
|
||||
phone: params.phone,
|
||||
referralCode: params.referralCode,
|
||||
totalClicks: 0,
|
||||
totalBindings: 0,
|
||||
activeBindings: 0,
|
||||
convertedBindings: 0,
|
||||
expiredBindings: 0,
|
||||
totalEarnings: 0,
|
||||
pendingEarnings: 0,
|
||||
withdrawnEarnings: 0,
|
||||
autoWithdraw: false,
|
||||
autoWithdrawThreshold: config.minWithdrawAmount,
|
||||
level: 'normal',
|
||||
commissionRate: config.defaultCommissionRate,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
distributors.push(distributor);
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
}
|
||||
|
||||
return distributor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分销商统计
|
||||
*/
|
||||
function updateDistributorStats(userId: string): void {
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
const index = distributors.findIndex(d => d.userId === userId);
|
||||
|
||||
if (index === -1) return;
|
||||
|
||||
const bindings = getBindingsForDistributor(userId);
|
||||
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]') as ClickRecord[];
|
||||
const userClicks = clicks.filter(c => c.referrerId === userId);
|
||||
|
||||
distributors[index] = {
|
||||
...distributors[index],
|
||||
totalClicks: userClicks.length,
|
||||
totalBindings: bindings.length,
|
||||
activeBindings: bindings.filter(b => b.status === 'active').length,
|
||||
convertedBindings: bindings.filter(b => b.status === 'converted').length,
|
||||
expiredBindings: bindings.filter(b => b.status === 'expired').length,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加分销商收益
|
||||
*/
|
||||
function addDistributorEarnings(userId: string, amount: number): void {
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
const index = distributors.findIndex(d => d.userId === userId);
|
||||
|
||||
if (index === -1) return;
|
||||
|
||||
distributors[index] = {
|
||||
...distributors[index],
|
||||
totalEarnings: distributors[index].totalEarnings + amount,
|
||||
pendingEarnings: distributors[index].pendingEarnings + amount,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
|
||||
// 检查是否需要自动提现
|
||||
checkAutoWithdraw(distributors[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新点击记录的购买状态
|
||||
*/
|
||||
function updateClickPurchaseStatus(referrerId: string, visitorId: string): void {
|
||||
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]') as ClickRecord[];
|
||||
const index = clicks.findIndex(c => c.referrerId === referrerId && c.visitorId === visitorId);
|
||||
|
||||
if (index !== -1) {
|
||||
clicks[index].purchased = true;
|
||||
clicks[index].purchasedAt = new Date().toISOString();
|
||||
localStorage.setItem(STORAGE_KEYS.CLICKS, JSON.stringify(clicks));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自动提现
|
||||
*/
|
||||
export function setAutoWithdraw(params: {
|
||||
userId: string;
|
||||
enabled: boolean;
|
||||
threshold?: number;
|
||||
account?: Distributor['autoWithdrawAccount'];
|
||||
}): boolean {
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
const index = distributors.findIndex(d => d.userId === params.userId);
|
||||
|
||||
if (index === -1) return false;
|
||||
|
||||
distributors[index] = {
|
||||
...distributors[index],
|
||||
autoWithdraw: params.enabled,
|
||||
autoWithdrawThreshold: params.threshold || distributors[index].autoWithdrawThreshold,
|
||||
autoWithdrawAccount: params.account || distributors[index].autoWithdrawAccount,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============== 提现管理 ==============
|
||||
|
||||
/**
|
||||
* 获取所有提现记录
|
||||
*/
|
||||
export function getAllWithdrawals(): WithdrawRecord[] {
|
||||
if (typeof window === 'undefined') return [];
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEYS.WITHDRAWALS) || '[]');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商的提现记录
|
||||
*/
|
||||
export function getWithdrawalsForDistributor(distributorId: string): WithdrawRecord[] {
|
||||
const withdrawals = getAllWithdrawals();
|
||||
return withdrawals.filter(w => w.distributorId === distributorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请提现
|
||||
*/
|
||||
export function requestWithdraw(params: {
|
||||
userId: string;
|
||||
amount: number;
|
||||
method: 'wechat' | 'alipay';
|
||||
account: string;
|
||||
accountName: string;
|
||||
}): { success: boolean; withdrawal?: WithdrawRecord; error?: string } {
|
||||
const config = getDistributionConfig();
|
||||
const distributor = getDistributor(params.userId);
|
||||
|
||||
if (!distributor) {
|
||||
return { success: false, error: '分销商不存在' };
|
||||
}
|
||||
|
||||
if (params.amount < config.minWithdrawAmount) {
|
||||
return { success: false, error: `最低提现金额为 ${config.minWithdrawAmount} 元` };
|
||||
}
|
||||
|
||||
if (params.amount > distributor.pendingEarnings) {
|
||||
return { success: false, error: '提现金额超过可提现余额' };
|
||||
}
|
||||
|
||||
const fee = params.amount * config.withdrawFeeRate;
|
||||
const actualAmount = params.amount - fee;
|
||||
|
||||
const withdrawal: WithdrawRecord = {
|
||||
id: generateId('withdraw_'),
|
||||
distributorId: distributor.id,
|
||||
userId: params.userId,
|
||||
userName: distributor.nickname,
|
||||
amount: params.amount,
|
||||
fee,
|
||||
actualAmount,
|
||||
method: params.method,
|
||||
account: params.account,
|
||||
accountName: params.accountName,
|
||||
status: 'pending',
|
||||
isAuto: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 保存提现记录
|
||||
const withdrawals = getAllWithdrawals();
|
||||
withdrawals.push(withdrawal);
|
||||
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
|
||||
|
||||
// 扣除待提现金额
|
||||
deductDistributorPendingEarnings(params.userId, params.amount);
|
||||
|
||||
return { success: true, withdrawal };
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣除分销商待提现金额
|
||||
*/
|
||||
function deductDistributorPendingEarnings(userId: string, amount: number): void {
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
const index = distributors.findIndex(d => d.userId === userId);
|
||||
|
||||
if (index !== -1) {
|
||||
distributors[index].pendingEarnings -= amount;
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并执行自动提现
|
||||
*/
|
||||
function checkAutoWithdraw(distributor: Distributor): void {
|
||||
if (!distributor.autoWithdraw || !distributor.autoWithdrawAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (distributor.pendingEarnings >= distributor.autoWithdrawThreshold) {
|
||||
// 执行自动提现
|
||||
const result = executeAutoWithdraw(distributor);
|
||||
if (result.success) {
|
||||
console.log(`自动提现成功: ${distributor.nickname}, 金额: ${distributor.pendingEarnings}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自动提现
|
||||
*/
|
||||
export function executeAutoWithdraw(distributor: Distributor): { success: boolean; withdrawal?: WithdrawRecord; error?: string } {
|
||||
if (!distributor.autoWithdrawAccount) {
|
||||
return { success: false, error: '未配置自动提现账户' };
|
||||
}
|
||||
|
||||
const config = getDistributionConfig();
|
||||
const amount = distributor.pendingEarnings;
|
||||
const fee = amount * config.withdrawFeeRate;
|
||||
const actualAmount = amount - fee;
|
||||
|
||||
const withdrawal: WithdrawRecord = {
|
||||
id: generateId('withdraw_'),
|
||||
distributorId: distributor.id,
|
||||
userId: distributor.userId,
|
||||
userName: distributor.nickname,
|
||||
amount,
|
||||
fee,
|
||||
actualAmount,
|
||||
method: distributor.autoWithdrawAccount.type,
|
||||
account: distributor.autoWithdrawAccount.account,
|
||||
accountName: distributor.autoWithdrawAccount.name,
|
||||
status: 'processing', // 自动提现直接进入处理状态
|
||||
isAuto: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 保存提现记录
|
||||
const withdrawals = getAllWithdrawals();
|
||||
withdrawals.push(withdrawal);
|
||||
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
|
||||
|
||||
// 扣除待提现金额
|
||||
deductDistributorPendingEarnings(distributor.userId, amount);
|
||||
|
||||
// 这里应该调用实际的支付接口进行打款
|
||||
// 实际项目中需要对接微信/支付宝的企业付款接口
|
||||
processWithdrawalPayment(withdrawal.id);
|
||||
|
||||
return { success: true, withdrawal };
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提现打款(模拟)
|
||||
* 实际项目中需要对接支付接口
|
||||
*/
|
||||
export async function processWithdrawalPayment(withdrawalId: string): Promise<{ success: boolean; error?: string }> {
|
||||
const withdrawals = getAllWithdrawals();
|
||||
const index = withdrawals.findIndex(w => w.id === withdrawalId);
|
||||
|
||||
if (index === -1) {
|
||||
return { success: false, error: '提现记录不存在' };
|
||||
}
|
||||
|
||||
const withdrawal = withdrawals[index];
|
||||
|
||||
// 模拟支付接口调用
|
||||
// 实际项目中应该调用:
|
||||
// - 微信:企业付款到零钱 API
|
||||
// - 支付宝:单笔转账到支付宝账户 API
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 更新提现状态为成功
|
||||
withdrawals[index] = {
|
||||
...withdrawal,
|
||||
status: 'completed',
|
||||
paymentNo: `PAY${Date.now()}`,
|
||||
paymentTime: new Date().toISOString(),
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
|
||||
|
||||
// 更新分销商已提现金额
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
const distIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
|
||||
if (distIndex !== -1) {
|
||||
distributors[distIndex].withdrawnEarnings += withdrawal.amount;
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// 打款失败
|
||||
withdrawals[index] = {
|
||||
...withdrawal,
|
||||
status: 'failed',
|
||||
paymentError: error instanceof Error ? error.message : '打款失败',
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
|
||||
|
||||
// 退还金额到待提现余额
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
const distIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
|
||||
if (distIndex !== -1) {
|
||||
distributors[distIndex].pendingEarnings += withdrawal.amount;
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
}
|
||||
|
||||
return { success: false, error: '打款失败' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过并打款
|
||||
*/
|
||||
export async function approveWithdrawal(withdrawalId: string, reviewedBy?: string): Promise<{ success: boolean; error?: string }> {
|
||||
const withdrawals = getAllWithdrawals();
|
||||
const index = withdrawals.findIndex(w => w.id === withdrawalId);
|
||||
|
||||
if (index === -1) {
|
||||
return { success: false, error: '提现记录不存在' };
|
||||
}
|
||||
|
||||
if (withdrawals[index].status !== 'pending') {
|
||||
return { success: false, error: '该提现申请已处理' };
|
||||
}
|
||||
|
||||
withdrawals[index] = {
|
||||
...withdrawals[index],
|
||||
status: 'processing',
|
||||
reviewedBy,
|
||||
reviewedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
|
||||
|
||||
// 执行打款
|
||||
return processWithdrawalPayment(withdrawalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝提现
|
||||
*/
|
||||
export function rejectWithdrawal(withdrawalId: string, reason: string, reviewedBy?: string): { success: boolean; error?: string } {
|
||||
const withdrawals = getAllWithdrawals();
|
||||
const index = withdrawals.findIndex(w => w.id === withdrawalId);
|
||||
|
||||
if (index === -1) {
|
||||
return { success: false, error: '提现记录不存在' };
|
||||
}
|
||||
|
||||
const withdrawal = withdrawals[index];
|
||||
|
||||
if (withdrawal.status !== 'pending') {
|
||||
return { success: false, error: '该提现申请已处理' };
|
||||
}
|
||||
|
||||
withdrawals[index] = {
|
||||
...withdrawal,
|
||||
status: 'rejected',
|
||||
reviewNote: reason,
|
||||
reviewedBy,
|
||||
reviewedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
|
||||
|
||||
// 退还金额到待提现余额
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
const distIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
|
||||
if (distIndex !== -1) {
|
||||
distributors[distIndex].pendingEarnings += withdrawal.amount;
|
||||
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ============== 统计概览 ==============
|
||||
|
||||
/**
|
||||
* 获取分销统计概览
|
||||
*/
|
||||
export function getDistributionOverview(): DistributionOverview {
|
||||
const bindings = getAllBindings();
|
||||
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]') as ClickRecord[];
|
||||
const withdrawals = getAllWithdrawals();
|
||||
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const weekFromNow = new Date();
|
||||
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
||||
|
||||
// 今日数据
|
||||
const todayClicks = clicks.filter(c => new Date(c.clickTime) >= today).length;
|
||||
const todayBindings = bindings.filter(b => new Date(b.createdAt) >= today).length;
|
||||
const todayConversions = bindings.filter(b =>
|
||||
b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= today
|
||||
).length;
|
||||
const todayEarnings = bindings
|
||||
.filter(b => b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= today)
|
||||
.reduce((sum, b) => sum + (b.commission || 0), 0);
|
||||
|
||||
// 本月数据
|
||||
const monthClicks = clicks.filter(c => new Date(c.clickTime) >= monthStart).length;
|
||||
const monthBindings = bindings.filter(b => new Date(b.createdAt) >= monthStart).length;
|
||||
const monthConversions = bindings.filter(b =>
|
||||
b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= monthStart
|
||||
).length;
|
||||
const monthEarnings = bindings
|
||||
.filter(b => b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= monthStart)
|
||||
.reduce((sum, b) => sum + (b.commission || 0), 0);
|
||||
|
||||
// 总计数据
|
||||
const totalConversions = bindings.filter(b => b.status === 'converted').length;
|
||||
const totalEarnings = bindings
|
||||
.filter(b => b.status === 'converted')
|
||||
.reduce((sum, b) => sum + (b.commission || 0), 0);
|
||||
|
||||
// 即将过期数据
|
||||
const expiringBindings = bindings.filter(b =>
|
||||
b.status === 'active' &&
|
||||
new Date(b.expireTime) <= weekFromNow &&
|
||||
new Date(b.expireTime) > now
|
||||
).length;
|
||||
|
||||
const expiredToday = bindings.filter(b =>
|
||||
b.status === 'expired' &&
|
||||
b.updatedAt && new Date(b.updatedAt) >= today
|
||||
).length;
|
||||
|
||||
// 提现数据
|
||||
const pendingWithdrawals = withdrawals.filter(w => w.status === 'pending').length;
|
||||
const pendingWithdrawAmount = withdrawals
|
||||
.filter(w => w.status === 'pending')
|
||||
.reduce((sum, w) => sum + w.amount, 0);
|
||||
|
||||
// 转化率
|
||||
const conversionRate = clicks.length > 0 ? (totalConversions / clicks.length) * 100 : 0;
|
||||
|
||||
return {
|
||||
todayClicks,
|
||||
todayBindings,
|
||||
todayConversions,
|
||||
todayEarnings,
|
||||
monthClicks,
|
||||
monthBindings,
|
||||
monthConversions,
|
||||
monthEarnings,
|
||||
totalClicks: clicks.length,
|
||||
totalBindings: bindings.length,
|
||||
totalConversions,
|
||||
totalEarnings,
|
||||
expiringBindings,
|
||||
expiredToday,
|
||||
pendingWithdrawals,
|
||||
pendingWithdrawAmount,
|
||||
conversionRate,
|
||||
lastUpdated: now.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销排行榜
|
||||
*/
|
||||
export function getDistributionRanking(limit: number = 10): Distributor[] {
|
||||
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
|
||||
return distributors
|
||||
.filter(d => d.status === 'active')
|
||||
.sort((a, b) => b.totalEarnings - a.totalEarnings)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分销商
|
||||
*/
|
||||
export function getAllDistributors(): Distributor[] {
|
||||
if (typeof window === 'undefined') return [];
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]');
|
||||
}
|
||||
254
lib/modules/distribution/types.ts
Normal file
254
lib/modules/distribution/types.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 分销模块类型定义
|
||||
* 核心功能:分享链接追踪、30天绑定规则、自动提现
|
||||
*/
|
||||
|
||||
// 分销绑定记录 - 记录每次分享链接点击的绑定关系
|
||||
export interface DistributionBinding {
|
||||
id: string;
|
||||
referrerId: string; // 分享者ID
|
||||
referrerCode: string; // 分享码
|
||||
visitorId: string; // 访客ID(可以是临时ID或用户ID)
|
||||
visitorPhone?: string; // 访客手机号
|
||||
visitorNickname?: string; // 访客昵称
|
||||
|
||||
// 绑定时间管理
|
||||
bindingTime: string; // 绑定时间 ISO格式
|
||||
expireTime: string; // 过期时间(绑定时间+30天)
|
||||
|
||||
// 绑定状态
|
||||
status: 'active' | 'converted' | 'expired' | 'cancelled';
|
||||
|
||||
// 转化信息
|
||||
convertedAt?: string; // 转化时间
|
||||
orderId?: string; // 关联订单ID
|
||||
orderAmount?: number; // 订单金额
|
||||
commission?: number; // 佣金金额
|
||||
|
||||
// 来源追踪
|
||||
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
|
||||
sourceDetail?: string; // 来源详情,如:朋友圈、微信群等
|
||||
|
||||
// 设备信息(可选)
|
||||
deviceInfo?: {
|
||||
userAgent?: string;
|
||||
ip?: string;
|
||||
platform?: string;
|
||||
};
|
||||
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 分销商信息
|
||||
export interface Distributor {
|
||||
id: string;
|
||||
userId: string;
|
||||
nickname: string;
|
||||
phone: string;
|
||||
referralCode: string;
|
||||
|
||||
// 统计数据
|
||||
totalClicks: number; // 总点击数
|
||||
totalBindings: number; // 总绑定数
|
||||
activeBindings: number; // 有效绑定数
|
||||
convertedBindings: number; // 已转化数
|
||||
expiredBindings: number; // 已过期数
|
||||
|
||||
// 收益数据
|
||||
totalEarnings: number; // 总收益
|
||||
pendingEarnings: number; // 待结算收益
|
||||
withdrawnEarnings: number; // 已提现收益
|
||||
|
||||
// 提现配置
|
||||
autoWithdraw: boolean; // 是否开启自动提现
|
||||
autoWithdrawThreshold: number; // 自动提现阈值
|
||||
autoWithdrawAccount?: WithdrawAccount; // 自动提现账户
|
||||
|
||||
// 分销等级
|
||||
level: 'normal' | 'silver' | 'gold' | 'diamond';
|
||||
commissionRate: number; // 佣金比例(0-100)
|
||||
|
||||
status: 'active' | 'frozen' | 'disabled';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 提现账户信息
|
||||
export interface WithdrawAccount {
|
||||
type: 'wechat' | 'alipay';
|
||||
account: string; // 账号
|
||||
name: string; // 真实姓名
|
||||
verified: boolean; // 是否已验证
|
||||
verifiedAt?: string;
|
||||
}
|
||||
|
||||
// 提现记录
|
||||
export interface WithdrawRecord {
|
||||
id: string;
|
||||
distributorId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
|
||||
amount: number; // 提现金额
|
||||
fee: number; // 手续费
|
||||
actualAmount: number; // 实际到账金额
|
||||
|
||||
method: 'wechat' | 'alipay';
|
||||
account: string;
|
||||
accountName: string;
|
||||
|
||||
// 打款信息
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed' | 'rejected';
|
||||
isAuto: boolean; // 是否自动打款
|
||||
|
||||
// 支付渠道返回信息
|
||||
paymentNo?: string; // 支付流水号
|
||||
paymentTime?: string; // 打款时间
|
||||
paymentError?: string; // 打款失败原因
|
||||
|
||||
// 审核信息
|
||||
reviewedBy?: string;
|
||||
reviewNote?: string;
|
||||
reviewedAt?: string;
|
||||
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
// 分销点击记录(用于追踪每次链接点击)
|
||||
export interface ClickRecord {
|
||||
id: string;
|
||||
referralCode: string;
|
||||
referrerId: string;
|
||||
|
||||
// 访客信息
|
||||
visitorId: string; // 设备指纹或用户ID
|
||||
isNewVisitor: boolean; // 是否新访客
|
||||
|
||||
// 来源信息
|
||||
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
|
||||
sourceUrl?: string;
|
||||
referer?: string;
|
||||
|
||||
// 设备信息
|
||||
deviceInfo?: {
|
||||
userAgent?: string;
|
||||
ip?: string;
|
||||
platform?: string;
|
||||
screenSize?: string;
|
||||
};
|
||||
|
||||
// 后续行为
|
||||
registered: boolean; // 是否注册
|
||||
registeredAt?: string;
|
||||
purchased: boolean; // 是否购买
|
||||
purchasedAt?: string;
|
||||
|
||||
clickTime: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 分销配置
|
||||
export interface DistributionConfig {
|
||||
// 绑定规则
|
||||
bindingDays: number; // 绑定有效期(天),默认30
|
||||
bindingPriority: 'first' | 'last'; // 绑定优先级:first=首次绑定,last=最后绑定
|
||||
|
||||
// 佣金规则
|
||||
defaultCommissionRate: number; // 默认佣金比例
|
||||
levelRates: {
|
||||
normal: number;
|
||||
silver: number;
|
||||
gold: number;
|
||||
diamond: number;
|
||||
};
|
||||
|
||||
// 提现规则
|
||||
minWithdrawAmount: number; // 最低提现金额
|
||||
withdrawFeeRate: number; // 提现手续费比例
|
||||
autoWithdrawEnabled: boolean; // 是否允许自动提现
|
||||
autoWithdrawTime: string; // 自动提现时间(如:每天10:00)
|
||||
|
||||
// 提醒规则
|
||||
expireRemindDays: number; // 过期前N天提醒
|
||||
|
||||
// 状态
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// 过期提醒记录
|
||||
export interface ExpireReminder {
|
||||
id: string;
|
||||
bindingId: string;
|
||||
distributorId: string;
|
||||
|
||||
bindingInfo: {
|
||||
visitorNickname?: string;
|
||||
visitorPhone?: string;
|
||||
bindingTime: string;
|
||||
expireTime: string;
|
||||
};
|
||||
|
||||
reminderType: 'expiring_soon' | 'expired';
|
||||
daysRemaining?: number;
|
||||
|
||||
// 提醒状态
|
||||
isRead: boolean;
|
||||
readAt?: string;
|
||||
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 分销统计概览
|
||||
export interface DistributionOverview {
|
||||
// 今日数据
|
||||
todayClicks: number;
|
||||
todayBindings: number;
|
||||
todayConversions: number;
|
||||
todayEarnings: number;
|
||||
|
||||
// 本月数据
|
||||
monthClicks: number;
|
||||
monthBindings: number;
|
||||
monthConversions: number;
|
||||
monthEarnings: number;
|
||||
|
||||
// 总计数据
|
||||
totalClicks: number;
|
||||
totalBindings: number;
|
||||
totalConversions: number;
|
||||
totalEarnings: number;
|
||||
|
||||
// 即将过期数据
|
||||
expiringBindings: number; // 7天内即将过期的绑定数
|
||||
expiredToday: number; // 今日过期数
|
||||
|
||||
// 提现数据
|
||||
pendingWithdrawals: number; // 待处理提现申请数
|
||||
pendingWithdrawAmount: number; // 待处理提现金额
|
||||
|
||||
// 转化率
|
||||
conversionRate: number; // 点击转化率
|
||||
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface DistributionAPIResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// 分销排行榜条目
|
||||
export interface DistributionRankItem {
|
||||
rank: number;
|
||||
distributorId: string;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
totalEarnings: number;
|
||||
totalConversions: number;
|
||||
level: string;
|
||||
}
|
||||
314
lib/modules/distribution/websocket.ts
Normal file
314
lib/modules/distribution/websocket.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* 分销模块WebSocket实时推送服务
|
||||
* 用于推送绑定过期提醒、提现状态更新等实时消息
|
||||
*/
|
||||
|
||||
// 消息类型定义
|
||||
export type WebSocketMessageType =
|
||||
| 'binding_expiring' // 绑定即将过期
|
||||
| 'binding_expired' // 绑定已过期
|
||||
| 'binding_converted' // 绑定已转化(用户付款)
|
||||
| 'withdrawal_approved' // 提现已通过
|
||||
| 'withdrawal_completed' // 提现已完成
|
||||
| 'withdrawal_rejected' // 提现已拒绝
|
||||
| 'earnings_added' // 收益增加
|
||||
| 'system_notice'; // 系统通知
|
||||
|
||||
// 消息结构
|
||||
export interface WebSocketMessage {
|
||||
type: WebSocketMessageType;
|
||||
userId: string; // 目标用户ID
|
||||
data: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
// 绑定过期提醒数据
|
||||
export interface BindingExpiringData {
|
||||
bindingId: string;
|
||||
visitorNickname?: string;
|
||||
visitorPhone?: string;
|
||||
daysRemaining: number;
|
||||
expireTime: string;
|
||||
}
|
||||
|
||||
// 提现状态更新数据
|
||||
export interface WithdrawalUpdateData {
|
||||
withdrawalId: string;
|
||||
amount: number;
|
||||
status: string;
|
||||
paymentNo?: string;
|
||||
rejectReason?: string;
|
||||
}
|
||||
|
||||
// 收益增加数据
|
||||
export interface EarningsAddedData {
|
||||
orderId: string;
|
||||
orderAmount: number;
|
||||
commission: number;
|
||||
visitorNickname?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket消息队列(服务端存储待发送的消息)
|
||||
* 实际项目中应该使用Redis或其他消息队列
|
||||
*/
|
||||
const messageQueue: Map<string, WebSocketMessage[]> = new Map();
|
||||
|
||||
/**
|
||||
* 生成消息ID
|
||||
*/
|
||||
function generateMessageId(): string {
|
||||
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到队列
|
||||
*/
|
||||
export function pushMessage(message: Omit<WebSocketMessage, 'messageId' | 'timestamp'>): void {
|
||||
const fullMessage: WebSocketMessage = {
|
||||
...message,
|
||||
messageId: generateMessageId(),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const userMessages = messageQueue.get(message.userId) || [];
|
||||
userMessages.push(fullMessage);
|
||||
|
||||
// 保留最近100条消息
|
||||
if (userMessages.length > 100) {
|
||||
userMessages.shift();
|
||||
}
|
||||
|
||||
messageQueue.set(message.userId, userMessages);
|
||||
|
||||
console.log('[WebSocket] 消息已入队:', {
|
||||
type: fullMessage.type,
|
||||
userId: fullMessage.userId,
|
||||
messageId: fullMessage.messageId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户待处理的消息
|
||||
*/
|
||||
export function getMessages(userId: string, since?: string): WebSocketMessage[] {
|
||||
const userMessages = messageQueue.get(userId) || [];
|
||||
|
||||
if (since) {
|
||||
return userMessages.filter(m => m.timestamp > since);
|
||||
}
|
||||
|
||||
return userMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除用户已读消息
|
||||
*/
|
||||
export function clearMessages(userId: string, messageIds: string[]): void {
|
||||
const userMessages = messageQueue.get(userId) || [];
|
||||
const filtered = userMessages.filter(m => !messageIds.includes(m.messageId));
|
||||
messageQueue.set(userId, filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送绑定即将过期提醒
|
||||
*/
|
||||
export function pushBindingExpiringReminder(params: {
|
||||
userId: string;
|
||||
bindingId: string;
|
||||
visitorNickname?: string;
|
||||
visitorPhone?: string;
|
||||
daysRemaining: number;
|
||||
expireTime: string;
|
||||
}): void {
|
||||
pushMessage({
|
||||
type: 'binding_expiring',
|
||||
userId: params.userId,
|
||||
data: {
|
||||
bindingId: params.bindingId,
|
||||
visitorNickname: params.visitorNickname,
|
||||
visitorPhone: params.visitorPhone,
|
||||
daysRemaining: params.daysRemaining,
|
||||
expireTime: params.expireTime,
|
||||
message: `用户 ${params.visitorNickname || params.visitorPhone || '未知'} 的绑定将在 ${params.daysRemaining} 天后过期`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送绑定已过期通知
|
||||
*/
|
||||
export function pushBindingExpiredNotice(params: {
|
||||
userId: string;
|
||||
bindingId: string;
|
||||
visitorNickname?: string;
|
||||
visitorPhone?: string;
|
||||
}): void {
|
||||
pushMessage({
|
||||
type: 'binding_expired',
|
||||
userId: params.userId,
|
||||
data: {
|
||||
bindingId: params.bindingId,
|
||||
visitorNickname: params.visitorNickname,
|
||||
visitorPhone: params.visitorPhone,
|
||||
message: `用户 ${params.visitorNickname || params.visitorPhone || '未知'} 的绑定已过期`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送绑定转化通知(用户付款)
|
||||
*/
|
||||
export function pushBindingConvertedNotice(params: {
|
||||
userId: string;
|
||||
bindingId: string;
|
||||
orderId: string;
|
||||
orderAmount: number;
|
||||
commission: number;
|
||||
visitorNickname?: string;
|
||||
}): void {
|
||||
pushMessage({
|
||||
type: 'binding_converted',
|
||||
userId: params.userId,
|
||||
data: {
|
||||
bindingId: params.bindingId,
|
||||
orderId: params.orderId,
|
||||
orderAmount: params.orderAmount,
|
||||
commission: params.commission,
|
||||
visitorNickname: params.visitorNickname,
|
||||
message: `恭喜!用户 ${params.visitorNickname || '未知'} 已付款 ¥${params.orderAmount},您获得佣金 ¥${params.commission.toFixed(2)}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送提现状态更新
|
||||
*/
|
||||
export function pushWithdrawalUpdate(params: {
|
||||
userId: string;
|
||||
withdrawalId: string;
|
||||
amount: number;
|
||||
status: 'approved' | 'completed' | 'rejected';
|
||||
paymentNo?: string;
|
||||
rejectReason?: string;
|
||||
}): void {
|
||||
const type: WebSocketMessageType =
|
||||
params.status === 'approved' ? 'withdrawal_approved' :
|
||||
params.status === 'completed' ? 'withdrawal_completed' : 'withdrawal_rejected';
|
||||
|
||||
const messages: Record<string, string> = {
|
||||
approved: `您的提现申请 ¥${params.amount.toFixed(2)} 已通过审核,正在打款中...`,
|
||||
completed: `您的提现 ¥${params.amount.toFixed(2)} 已成功到账,流水号: ${params.paymentNo}`,
|
||||
rejected: `您的提现申请 ¥${params.amount.toFixed(2)} 已被拒绝,原因: ${params.rejectReason || '未说明'}`,
|
||||
};
|
||||
|
||||
pushMessage({
|
||||
type,
|
||||
userId: params.userId,
|
||||
data: {
|
||||
withdrawalId: params.withdrawalId,
|
||||
amount: params.amount,
|
||||
status: params.status,
|
||||
paymentNo: params.paymentNo,
|
||||
rejectReason: params.rejectReason,
|
||||
message: messages[params.status],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送收益增加通知
|
||||
*/
|
||||
export function pushEarningsAdded(params: {
|
||||
userId: string;
|
||||
orderId: string;
|
||||
orderAmount: number;
|
||||
commission: number;
|
||||
visitorNickname?: string;
|
||||
}): void {
|
||||
pushMessage({
|
||||
type: 'earnings_added',
|
||||
userId: params.userId,
|
||||
data: {
|
||||
orderId: params.orderId,
|
||||
orderAmount: params.orderAmount,
|
||||
commission: params.commission,
|
||||
visitorNickname: params.visitorNickname,
|
||||
message: `收益 +¥${params.commission.toFixed(2)}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送系统通知
|
||||
*/
|
||||
export function pushSystemNotice(params: {
|
||||
userId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
link?: string;
|
||||
}): void {
|
||||
pushMessage({
|
||||
type: 'system_notice',
|
||||
userId: params.userId,
|
||||
data: {
|
||||
title: params.title,
|
||||
content: params.content,
|
||||
link: params.link,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端WebSocket Hook(用于React组件)
|
||||
* 使用轮询模式获取实时消息
|
||||
*/
|
||||
export function createWebSocketClient(userId: string, onMessage: (message: WebSocketMessage) => void) {
|
||||
let lastTimestamp = new Date().toISOString();
|
||||
let isRunning = false;
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
const fetchMessages = async () => {
|
||||
if (!isRunning) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/distribution/messages?userId=${userId}&since=${encodeURIComponent(lastTimestamp)}`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success && data.messages?.length > 0) {
|
||||
for (const message of data.messages) {
|
||||
onMessage(message);
|
||||
if (message.timestamp > lastTimestamp) {
|
||||
lastTimestamp = message.timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WebSocketClient] 获取消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
connect: () => {
|
||||
isRunning = true;
|
||||
// 每3秒轮询一次
|
||||
intervalId = setInterval(fetchMessages, 3000);
|
||||
// 立即获取一次
|
||||
fetchMessages();
|
||||
console.log('[WebSocketClient] 已连接,用户:', userId);
|
||||
},
|
||||
|
||||
disconnect: () => {
|
||||
isRunning = false;
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
console.log('[WebSocketClient] 已断开连接');
|
||||
},
|
||||
|
||||
isConnected: () => isRunning,
|
||||
};
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export class PaymentService {
|
||||
const redirectUrl = config.qrCode
|
||||
|
||||
if (!redirectUrl) {
|
||||
console.error("[v0] No payment URL configured for", method)
|
||||
console.error("[Karuo] No payment URL configured for", method)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ export function updateOrder(id: string, updates: Partial<Order>): Order | null {
|
||||
export function getPaymentConfig(): PaymentConfig {
|
||||
if (typeof window === "undefined") {
|
||||
return {
|
||||
wechat: { enabled: false, qrcode: "" },
|
||||
alipay: { enabled: false, qrcode: "" },
|
||||
wechat: { enabled: true, qrcode: "/images/wechat-pay.png" },
|
||||
alipay: { enabled: true, qrcode: "/images/alipay.png" },
|
||||
usdt: { enabled: false, walletAddress: "", network: "TRC20" },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,614 @@
|
||||
import crypto from "crypto"
|
||||
/**
|
||||
* 支付宝网关实现 (Alipay Gateway)
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* 支持:
|
||||
* - 电脑网站支付 (platform_type='web')
|
||||
* - 手机网站支付 (platform_type='wap')
|
||||
* - 扫码支付 (platform_type='qr')
|
||||
*
|
||||
* 作者: 卡若
|
||||
* 版本: v4.0
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { AbstractGateway, PaymentFactory } from './factory';
|
||||
import {
|
||||
CreateTradeData,
|
||||
TradeResult,
|
||||
NotifyResult,
|
||||
SignatureError,
|
||||
fenToYuan,
|
||||
yuanToFen,
|
||||
} from './types';
|
||||
|
||||
export interface AlipayConfig {
|
||||
appId: string
|
||||
partnerId: string
|
||||
key: string
|
||||
returnUrl: string
|
||||
notifyUrl: string
|
||||
appId: string;
|
||||
pid: string;
|
||||
sellerEmail?: string;
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
md5Key?: string;
|
||||
enabled?: boolean;
|
||||
mode?: 'sandbox' | 'production';
|
||||
}
|
||||
|
||||
export class AlipayService {
|
||||
constructor(private config: AlipayConfig) {}
|
||||
/**
|
||||
* 支付宝网关
|
||||
*/
|
||||
export class AlipayGateway extends AbstractGateway {
|
||||
private readonly GATEWAY_URL = 'https://openapi.alipay.com/gateway.do';
|
||||
private readonly SANDBOX_URL = 'https://openapi.alipaydev.com/gateway.do';
|
||||
|
||||
private appId: string;
|
||||
private pid: string;
|
||||
private sellerEmail: string;
|
||||
private privateKey: string;
|
||||
private publicKey: string;
|
||||
private md5Key: string;
|
||||
private mode: 'sandbox' | 'production';
|
||||
|
||||
constructor(config: Record<string, unknown>) {
|
||||
super(config);
|
||||
const cfg = config as unknown as AlipayConfig;
|
||||
this.appId = cfg.appId || '';
|
||||
this.pid = cfg.pid || '';
|
||||
this.sellerEmail = cfg.sellerEmail || '';
|
||||
this.privateKey = cfg.privateKey || '';
|
||||
this.publicKey = cfg.publicKey || '';
|
||||
this.md5Key = cfg.md5Key || '';
|
||||
this.mode = cfg.mode || 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网关地址
|
||||
*/
|
||||
private getGatewayUrl(): string {
|
||||
return this.mode === 'sandbox' ? this.SANDBOX_URL : this.GATEWAY_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付宝交易
|
||||
*/
|
||||
async createTrade(data: CreateTradeData): Promise<TradeResult> {
|
||||
const platformType = (data.platformType || 'wap').toLowerCase();
|
||||
|
||||
switch (platformType) {
|
||||
case 'web':
|
||||
return this.createWebTrade(data);
|
||||
case 'wap':
|
||||
return this.createWapTrade(data);
|
||||
case 'qr':
|
||||
return this.createQrTrade(data);
|
||||
default:
|
||||
// 默认使用 WAP 支付
|
||||
return this.createWapTrade(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 电脑网站支付
|
||||
*/
|
||||
private async createWebTrade(data: CreateTradeData): Promise<TradeResult> {
|
||||
const bizContent = {
|
||||
subject: data.goodsTitle.slice(0, 256),
|
||||
out_trade_no: data.tradeSn,
|
||||
total_amount: fenToYuan(data.amount).toFixed(2),
|
||||
product_code: 'FAST_INSTANT_TRADE_PAY',
|
||||
body: data.goodsDetail?.slice(0, 128) || '',
|
||||
passback_params: data.attach ? encodeURIComponent(JSON.stringify(data.attach)) : '',
|
||||
};
|
||||
|
||||
const params = this.buildParams('alipay.trade.page.pay', bizContent, data.returnUrl, data.notifyUrl);
|
||||
const sign = this.generateMD5Sign(params);
|
||||
params.sign = sign;
|
||||
|
||||
const payUrl = `${this.getGatewayUrl()}?${this.buildQueryString(params)}`;
|
||||
|
||||
console.log('[Alipay] 创建电脑网站支付:', {
|
||||
out_trade_no: data.tradeSn,
|
||||
total_amount: fenToYuan(data.amount).toFixed(2),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'url',
|
||||
payload: payUrl,
|
||||
tradeSn: data.tradeSn,
|
||||
expiration: 1800,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机网站支付
|
||||
*/
|
||||
private async createWapTrade(data: CreateTradeData): Promise<TradeResult> {
|
||||
const bizContent = {
|
||||
subject: data.goodsTitle.slice(0, 256),
|
||||
out_trade_no: data.tradeSn,
|
||||
total_amount: fenToYuan(data.amount).toFixed(2),
|
||||
product_code: 'QUICK_WAP_WAY',
|
||||
body: data.goodsDetail?.slice(0, 128) || '',
|
||||
passback_params: data.attach ? encodeURIComponent(JSON.stringify(data.attach)) : '',
|
||||
};
|
||||
|
||||
const params = this.buildParams('alipay.trade.wap.pay', bizContent, data.returnUrl, data.notifyUrl);
|
||||
const sign = this.generateMD5Sign(params);
|
||||
params.sign = sign;
|
||||
|
||||
const payUrl = `${this.getGatewayUrl()}?${this.buildQueryString(params)}`;
|
||||
|
||||
console.log('[Alipay] 创建手机网站支付:', {
|
||||
out_trade_no: data.tradeSn,
|
||||
total_amount: fenToYuan(data.amount).toFixed(2),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'url',
|
||||
payload: payUrl,
|
||||
tradeSn: data.tradeSn,
|
||||
expiration: 1800,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码支付(当面付)
|
||||
*/
|
||||
private async createQrTrade(data: CreateTradeData): Promise<TradeResult> {
|
||||
const bizContent = {
|
||||
subject: data.goodsTitle.slice(0, 256),
|
||||
out_trade_no: data.tradeSn,
|
||||
total_amount: fenToYuan(data.amount).toFixed(2),
|
||||
body: data.goodsDetail?.slice(0, 128) || '',
|
||||
};
|
||||
|
||||
const params = this.buildParams('alipay.trade.precreate', bizContent, '', data.notifyUrl);
|
||||
const sign = this.generateMD5Sign(params);
|
||||
params.sign = sign;
|
||||
|
||||
console.log('[Alipay] 创建扫码支付:', {
|
||||
out_trade_no: data.tradeSn,
|
||||
total_amount: fenToYuan(data.amount).toFixed(2),
|
||||
});
|
||||
|
||||
try {
|
||||
// 调用支付宝预下单接口
|
||||
const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('[Alipay] 预下单响应:', responseText.slice(0, 500));
|
||||
|
||||
// 解析JSON响应
|
||||
const result = JSON.parse(responseText);
|
||||
const precreateResponse = result.alipay_trade_precreate_response;
|
||||
|
||||
if (precreateResponse && precreateResponse.code === '10000' && precreateResponse.qr_code) {
|
||||
return {
|
||||
type: 'qrcode',
|
||||
payload: precreateResponse.qr_code,
|
||||
tradeSn: data.tradeSn,
|
||||
expiration: 1800,
|
||||
};
|
||||
}
|
||||
|
||||
// 如果API调用失败,回退到WAP支付方式
|
||||
console.log('[Alipay] 预下单失败,使用WAP支付:', precreateResponse?.sub_msg || precreateResponse?.msg);
|
||||
return this.createWapTrade(data);
|
||||
} catch (error) {
|
||||
console.error('[Alipay] 预下单异常,使用WAP支付:', error);
|
||||
return this.createWapTrade(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建公共参数
|
||||
*/
|
||||
private buildParams(
|
||||
method: string,
|
||||
bizContent: Record<string, string>,
|
||||
returnUrl?: string,
|
||||
notifyUrl?: string
|
||||
): Record<string, string> {
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
const params: Record<string, string> = {
|
||||
app_id: this.appId,
|
||||
method,
|
||||
charset: 'utf-8',
|
||||
sign_type: 'MD5',
|
||||
timestamp,
|
||||
version: '1.0',
|
||||
biz_content: JSON.stringify(bizContent),
|
||||
};
|
||||
|
||||
if (returnUrl) {
|
||||
params.return_url = returnUrl;
|
||||
}
|
||||
if (notifyUrl) {
|
||||
params.notify_url = notifyUrl;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MD5签名
|
||||
*/
|
||||
private generateMD5Sign(params: Record<string, string>): string {
|
||||
const sortedKeys = Object.keys(params).sort();
|
||||
const signString = sortedKeys
|
||||
.filter((key) => params[key] && key !== 'sign')
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
const signWithKey = `${signString}${this.md5Key}`;
|
||||
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询字符串
|
||||
*/
|
||||
private buildQueryString(params: Record<string, string>): string {
|
||||
return Object.entries(params)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证签名
|
||||
*/
|
||||
verifySign(data: Record<string, string>): boolean {
|
||||
const receivedSign = data.sign;
|
||||
if (!receivedSign) return false;
|
||||
|
||||
// 复制数据,移除 sign 和 sign_type
|
||||
const params = { ...data };
|
||||
delete params.sign;
|
||||
delete params.sign_type;
|
||||
|
||||
const calculatedSign = this.generateMD5Sign(params);
|
||||
return receivedSign.toLowerCase() === calculatedSign.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析回调数据
|
||||
*/
|
||||
parseNotify(data: string | Record<string, string>): NotifyResult {
|
||||
const params = typeof data === 'string' ? this.parseFormData(data) : data;
|
||||
|
||||
// 验证签名
|
||||
if (!this.verifySign({ ...params })) {
|
||||
throw new SignatureError('支付宝签名验证失败');
|
||||
}
|
||||
|
||||
const tradeStatus = params.trade_status || '';
|
||||
const status = ['TRADE_SUCCESS', 'TRADE_FINISHED'].includes(tradeStatus) ? 'paid' : 'failed';
|
||||
|
||||
// 解析透传参数
|
||||
let attach: Record<string, unknown> = {};
|
||||
const passback = params.passback_params || '';
|
||||
if (passback) {
|
||||
try {
|
||||
attach = JSON.parse(decodeURIComponent(passback));
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
|
||||
// 解析支付时间
|
||||
const gmtPayment = params.gmt_payment || '';
|
||||
const payTime = gmtPayment ? new Date(gmtPayment) : new Date();
|
||||
|
||||
return {
|
||||
status,
|
||||
tradeSn: params.out_trade_no || '',
|
||||
platformSn: params.trade_no || '',
|
||||
payAmount: yuanToFen(parseFloat(params.total_amount || '0')),
|
||||
payTime,
|
||||
currency: 'CNY',
|
||||
attach,
|
||||
rawData: params,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析表单数据
|
||||
*/
|
||||
private parseFormData(formString: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const pairs = formString.split('&');
|
||||
for (const pair of pairs) {
|
||||
const [key, value] = pair.split('=');
|
||||
if (key && value !== undefined) {
|
||||
result[decodeURIComponent(key)] = decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询交易状态
|
||||
*/
|
||||
async queryTrade(tradeSn: string): Promise<NotifyResult | null> {
|
||||
try {
|
||||
// 检查 appId 是否配置
|
||||
if (!this.appId) {
|
||||
console.log('[Alipay] 查询跳过: 未配置 appId');
|
||||
return {
|
||||
status: 'paying',
|
||||
tradeSn,
|
||||
platformSn: '',
|
||||
payAmount: 0,
|
||||
payTime: new Date(),
|
||||
currency: 'CNY',
|
||||
attach: {},
|
||||
rawData: {},
|
||||
};
|
||||
}
|
||||
|
||||
const bizContent = {
|
||||
out_trade_no: tradeSn,
|
||||
};
|
||||
|
||||
const params = this.buildParams('alipay.trade.query', bizContent);
|
||||
params.sign = this.generateMD5Sign(params);
|
||||
|
||||
console.log('[Alipay] 查询订单:', { tradeSn, appId: this.appId });
|
||||
|
||||
const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('[Alipay] 查询响应:', responseText.slice(0, 300));
|
||||
|
||||
const result = JSON.parse(responseText);
|
||||
const queryResponse = result.alipay_trade_query_response;
|
||||
|
||||
// 如果订单不存在,返回 paying 状态(可能还没同步到支付宝)
|
||||
if (queryResponse?.code === '40004' && queryResponse?.sub_code === 'ACQ.TRADE_NOT_EXIST') {
|
||||
console.log('[Alipay] 订单不存在,可能还在等待支付');
|
||||
return {
|
||||
status: 'paying',
|
||||
tradeSn,
|
||||
platformSn: '',
|
||||
payAmount: 0,
|
||||
payTime: new Date(),
|
||||
currency: 'CNY',
|
||||
attach: {},
|
||||
rawData: queryResponse,
|
||||
};
|
||||
}
|
||||
|
||||
if (!queryResponse || queryResponse.code !== '10000') {
|
||||
console.log('[Alipay] 订单查询失败:', {
|
||||
code: queryResponse?.code,
|
||||
msg: queryResponse?.msg,
|
||||
sub_code: queryResponse?.sub_code,
|
||||
sub_msg: queryResponse?.sub_msg,
|
||||
});
|
||||
// 返回 paying 状态而不是 null,让前端继续轮询
|
||||
return {
|
||||
status: 'paying',
|
||||
tradeSn,
|
||||
platformSn: '',
|
||||
payAmount: 0,
|
||||
payTime: new Date(),
|
||||
currency: 'CNY',
|
||||
attach: {},
|
||||
rawData: queryResponse || {},
|
||||
};
|
||||
}
|
||||
|
||||
const tradeStatus = queryResponse.trade_status || '';
|
||||
let status: 'paying' | 'paid' | 'closed' | 'refunded' = 'paying';
|
||||
|
||||
switch (tradeStatus) {
|
||||
case 'TRADE_SUCCESS':
|
||||
case 'TRADE_FINISHED':
|
||||
status = 'paid';
|
||||
break;
|
||||
case 'TRADE_CLOSED':
|
||||
status = 'closed';
|
||||
break;
|
||||
case 'WAIT_BUYER_PAY':
|
||||
default:
|
||||
status = 'paying';
|
||||
}
|
||||
|
||||
console.log('[Alipay] 订单状态:', { tradeSn, tradeStatus, status });
|
||||
|
||||
return {
|
||||
status,
|
||||
tradeSn: queryResponse.out_trade_no || tradeSn,
|
||||
platformSn: queryResponse.trade_no || '',
|
||||
payAmount: yuanToFen(parseFloat(queryResponse.total_amount || '0')),
|
||||
payTime: new Date(queryResponse.send_pay_date || Date.now()),
|
||||
currency: 'CNY',
|
||||
attach: {},
|
||||
rawData: queryResponse,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Alipay] 查询订单失败:', error);
|
||||
// 返回 paying 状态而不是 null
|
||||
return {
|
||||
status: 'paying',
|
||||
tradeSn,
|
||||
platformSn: '',
|
||||
payAmount: 0,
|
||||
payTime: new Date(),
|
||||
currency: 'CNY',
|
||||
attach: {},
|
||||
rawData: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭交易
|
||||
*/
|
||||
async closeTrade(tradeSn: string): Promise<boolean> {
|
||||
try {
|
||||
const bizContent = {
|
||||
out_trade_no: tradeSn,
|
||||
};
|
||||
|
||||
const params = this.buildParams('alipay.trade.close', bizContent);
|
||||
params.sign = this.generateMD5Sign(params);
|
||||
|
||||
console.log('[Alipay] 关闭订单:', tradeSn);
|
||||
|
||||
const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const result = JSON.parse(responseText);
|
||||
const closeResponse = result.alipay_trade_close_response;
|
||||
|
||||
return closeResponse && closeResponse.code === '10000';
|
||||
} catch (error) {
|
||||
console.error('[Alipay] 关闭订单失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
*/
|
||||
async refund(tradeSn: string, refundSn: string, amount: number, reason?: string): Promise<boolean> {
|
||||
try {
|
||||
const bizContent = {
|
||||
out_trade_no: tradeSn,
|
||||
out_request_no: refundSn,
|
||||
refund_amount: fenToYuan(amount).toFixed(2),
|
||||
refund_reason: reason || '用户退款',
|
||||
};
|
||||
|
||||
const params = this.buildParams('alipay.trade.refund', bizContent);
|
||||
params.sign = this.generateMD5Sign(params);
|
||||
|
||||
console.log('[Alipay] 发起退款:', { tradeSn, refundSn, amount });
|
||||
|
||||
const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const result = JSON.parse(responseText);
|
||||
const refundResponse = result.alipay_trade_refund_response;
|
||||
|
||||
return refundResponse && refundResponse.code === '10000';
|
||||
} catch (error) {
|
||||
console.error('[Alipay] 退款失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调成功响应
|
||||
*/
|
||||
override successResponse(): string {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调失败响应
|
||||
*/
|
||||
override failResponse(): string {
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
// 注册到工厂
|
||||
PaymentFactory.register('alipay', AlipayGateway);
|
||||
|
||||
// 导出兼容旧版的 AlipayService
|
||||
export interface AlipayServiceConfig {
|
||||
appId: string;
|
||||
partnerId: string;
|
||||
key: string;
|
||||
returnUrl: string;
|
||||
notifyUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧版的 AlipayService
|
||||
* @deprecated 请使用 AlipayGateway
|
||||
*/
|
||||
export class AlipayService {
|
||||
private gateway: AlipayGateway;
|
||||
private notifyUrl: string;
|
||||
private returnUrl: string;
|
||||
|
||||
constructor(config: AlipayServiceConfig) {
|
||||
this.gateway = new AlipayGateway({
|
||||
appId: config.appId,
|
||||
pid: config.partnerId,
|
||||
md5Key: config.key,
|
||||
});
|
||||
this.notifyUrl = config.notifyUrl;
|
||||
this.returnUrl = config.returnUrl;
|
||||
}
|
||||
|
||||
// 创建支付宝订单
|
||||
createOrder(params: {
|
||||
outTradeNo: string
|
||||
subject: string
|
||||
totalAmount: number
|
||||
body?: string
|
||||
outTradeNo: string;
|
||||
subject: string;
|
||||
totalAmount: number;
|
||||
body?: string;
|
||||
}) {
|
||||
const orderInfo = {
|
||||
app_id: this.config.appId,
|
||||
method: "alipay.trade.wap.pay",
|
||||
format: "JSON",
|
||||
charset: "utf-8",
|
||||
sign_type: "MD5",
|
||||
timestamp: new Date().toISOString().slice(0, 19).replace("T", " "),
|
||||
version: "1.0",
|
||||
notify_url: this.config.notifyUrl,
|
||||
return_url: this.config.returnUrl,
|
||||
// 同步创建订单信息
|
||||
const orderInfo: Record<string, string> = {
|
||||
app_id: (this.gateway as AlipayGateway)['appId'],
|
||||
method: 'alipay.trade.wap.pay',
|
||||
format: 'JSON',
|
||||
charset: 'utf-8',
|
||||
sign_type: 'MD5',
|
||||
timestamp: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||||
version: '1.0',
|
||||
notify_url: this.notifyUrl,
|
||||
return_url: this.returnUrl,
|
||||
biz_content: JSON.stringify({
|
||||
out_trade_no: params.outTradeNo,
|
||||
product_code: "QUICK_WAP_WAY",
|
||||
product_code: 'QUICK_WAP_WAY',
|
||||
total_amount: params.totalAmount.toFixed(2),
|
||||
subject: params.subject,
|
||||
body: params.body || params.subject,
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
const sign = this.generateSign(orderInfo)
|
||||
const sign = this.generateSign(orderInfo);
|
||||
return {
|
||||
...orderInfo,
|
||||
sign,
|
||||
paymentUrl: this.buildPaymentUrl(orderInfo, sign),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
generateSign(params: Record<string, string>): string {
|
||||
const sortedKeys = Object.keys(params).sort()
|
||||
const sortedKeys = Object.keys(params).sort();
|
||||
const signString = sortedKeys
|
||||
.filter((key) => params[key] && key !== "sign")
|
||||
.filter((key) => params[key] && key !== 'sign')
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&")
|
||||
.join('&');
|
||||
|
||||
const signWithKey = `${signString}${this.config.key}`
|
||||
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex")
|
||||
const md5Key = (this.gateway as AlipayGateway)['md5Key'];
|
||||
const signWithKey = `${signString}${md5Key}`;
|
||||
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
// 验证回调签名
|
||||
verifySign(params: Record<string, string>): boolean {
|
||||
const receivedSign = params.sign
|
||||
if (!receivedSign) return false
|
||||
|
||||
const calculatedSign = this.generateSign(params)
|
||||
return receivedSign.toLowerCase() === calculatedSign.toLowerCase()
|
||||
return this.gateway.verifySign(params);
|
||||
}
|
||||
|
||||
async queryTrade(tradeSn: string) {
|
||||
return this.gateway.queryTrade(tradeSn);
|
||||
}
|
||||
|
||||
// 构建支付URL
|
||||
private buildPaymentUrl(params: Record<string, string>, sign: string): string {
|
||||
const gateway = "https://openapi.alipay.com/gateway.do"
|
||||
const queryParams = new URLSearchParams({ ...params, sign })
|
||||
return `${gateway}?${queryParams.toString()}`
|
||||
const gateway = 'https://openapi.alipay.com/gateway.do';
|
||||
const queryParams = new URLSearchParams({ ...params, sign });
|
||||
return `${gateway}?${queryParams.toString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
126
lib/payment/config.ts
Normal file
126
lib/payment/config.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 支付配置管理 (Payment Configuration)
|
||||
* 从环境变量读取支付配置
|
||||
*
|
||||
* 作者: 卡若
|
||||
* 版本: v4.0
|
||||
*/
|
||||
|
||||
import { AlipayConfig } from './alipay';
|
||||
import { WechatPayConfig } from './wechat';
|
||||
|
||||
// 应用基础配置
|
||||
export interface AppConfig {
|
||||
env: 'development' | 'production';
|
||||
name: string;
|
||||
url: string;
|
||||
currency: 'CNY' | 'USD' | 'EUR';
|
||||
}
|
||||
|
||||
// 完整支付配置
|
||||
export interface PaymentConfig {
|
||||
app: AppConfig;
|
||||
alipay: AlipayConfig;
|
||||
wechat: WechatPayConfig;
|
||||
paypal: {
|
||||
enabled: boolean;
|
||||
mode: 'sandbox' | 'production';
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
stripe: {
|
||||
enabled: boolean;
|
||||
mode: 'test' | 'production';
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
usdt: {
|
||||
enabled: boolean;
|
||||
gatewayType: string;
|
||||
apiKey: string;
|
||||
ipnSecret: string;
|
||||
};
|
||||
order: {
|
||||
expireMinutes: number;
|
||||
tradeSnPrefix: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付配置
|
||||
*/
|
||||
export function getPaymentConfig(): PaymentConfig {
|
||||
return {
|
||||
app: {
|
||||
env: (process.env.NODE_ENV || 'development') as 'development' | 'production',
|
||||
name: process.env.APP_NAME || 'Soul创业实验',
|
||||
url: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000',
|
||||
currency: (process.env.APP_CURRENCY || 'CNY') as 'CNY',
|
||||
},
|
||||
alipay: {
|
||||
enabled: process.env.ALIPAY_ENABLED === 'true' || true, // 默认启用
|
||||
mode: (process.env.ALIPAY_MODE || 'production') as 'production',
|
||||
// 支付宝新版接口需要 app_id,如果没有配置则使用 pid(旧版兼容)
|
||||
appId: process.env.ALIPAY_APP_ID || process.env.ALIPAY_PID || '2088511801157159',
|
||||
pid: process.env.ALIPAY_PID || '2088511801157159',
|
||||
sellerEmail: process.env.ALIPAY_SELLER_EMAIL || 'zhengzhiqun@vip.qq.com',
|
||||
privateKey: process.env.ALIPAY_PRIVATE_KEY || '',
|
||||
publicKey: process.env.ALIPAY_PUBLIC_KEY || '',
|
||||
md5Key: process.env.ALIPAY_MD5_KEY || 'lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp',
|
||||
},
|
||||
wechat: {
|
||||
enabled: process.env.WECHAT_ENABLED === 'true' || true, // 默认启用
|
||||
mode: (process.env.WECHAT_MODE || 'production') as 'production',
|
||||
// 微信支付需要使用绑定了支付功能的服务号AppID
|
||||
appId: process.env.WECHAT_APPID || 'wx7c0dbf34ddba300d', // 服务号AppID(已绑定商户号)
|
||||
appSecret: process.env.WECHAT_APP_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
|
||||
serviceAppId: process.env.WECHAT_SERVICE_APPID || 'wx7c0dbf34ddba300d',
|
||||
serviceSecret: process.env.WECHAT_SERVICE_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
|
||||
mchId: process.env.WECHAT_MCH_ID || '1318592501',
|
||||
mchKey: process.env.WECHAT_MCH_KEY || 'wx3e31b068be59ddc131b068be59ddc2',
|
||||
certPath: process.env.WECHAT_CERT_PATH || '',
|
||||
keyPath: process.env.WECHAT_KEY_PATH || '',
|
||||
},
|
||||
paypal: {
|
||||
enabled: process.env.PAYPAL_ENABLED === 'true',
|
||||
mode: (process.env.PAYPAL_MODE || 'sandbox') as 'sandbox',
|
||||
clientId: process.env.PAYPAL_CLIENT_ID || '',
|
||||
clientSecret: process.env.PAYPAL_CLIENT_SECRET || '',
|
||||
},
|
||||
stripe: {
|
||||
enabled: process.env.STRIPE_ENABLED === 'true',
|
||||
mode: (process.env.STRIPE_MODE || 'test') as 'test',
|
||||
publicKey: process.env.STRIPE_PUBLIC_KEY || '',
|
||||
secretKey: process.env.STRIPE_SECRET_KEY || '',
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||||
},
|
||||
usdt: {
|
||||
enabled: process.env.USDT_ENABLED === 'true',
|
||||
gatewayType: process.env.USDT_GATEWAY_TYPE || 'nowpayments',
|
||||
apiKey: process.env.NOWPAYMENTS_API_KEY || '',
|
||||
ipnSecret: process.env.NOWPAYMENTS_IPN_SECRET || '',
|
||||
},
|
||||
order: {
|
||||
expireMinutes: parseInt(process.env.ORDER_EXPIRE_MINUTES || '30', 10),
|
||||
tradeSnPrefix: process.env.TRADE_SN_PREFIX || 'T',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取回调通知URL
|
||||
*/
|
||||
export function getNotifyUrl(gateway: 'alipay' | 'wechat' | 'paypal' | 'stripe'): string {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
return `${baseUrl}/api/payment/${gateway}/notify`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付成功返回URL
|
||||
*/
|
||||
export function getReturnUrl(orderId?: string): string {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const url = `${baseUrl}/payment/success`;
|
||||
return orderId ? `${url}?orderId=${orderId}` : url;
|
||||
}
|
||||
246
lib/payment/factory.ts
Normal file
246
lib/payment/factory.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* 支付网关工厂 (Payment Gateway Factory)
|
||||
* 统一管理所有支付网关,实现工厂模式
|
||||
*
|
||||
* 作者: 卡若
|
||||
* 版本: v4.0
|
||||
*/
|
||||
|
||||
import {
|
||||
CreateTradeData,
|
||||
TradeResult,
|
||||
NotifyResult,
|
||||
PaymentPlatform,
|
||||
PaymentGateway,
|
||||
GatewayNotFoundError,
|
||||
PaymentMethod
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* 抽象支付网关基类
|
||||
*/
|
||||
export abstract class AbstractGateway {
|
||||
protected config: Record<string, unknown>;
|
||||
|
||||
constructor(config: Record<string, unknown> = {}) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建交易
|
||||
*/
|
||||
abstract createTrade(data: CreateTradeData): Promise<TradeResult>;
|
||||
|
||||
/**
|
||||
* 验证签名
|
||||
*/
|
||||
abstract verifySign(data: Record<string, string>): boolean;
|
||||
|
||||
/**
|
||||
* 解析回调数据
|
||||
*/
|
||||
abstract parseNotify(data: string | Record<string, string>): NotifyResult;
|
||||
|
||||
/**
|
||||
* 关闭交易
|
||||
*/
|
||||
abstract closeTrade(tradeSn: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 查询交易
|
||||
*/
|
||||
abstract queryTrade(tradeSn: string): Promise<NotifyResult | null>;
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
*/
|
||||
abstract refund(tradeSn: string, refundSn: string, amount: number, reason?: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 回调成功响应
|
||||
*/
|
||||
successResponse(): string {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调失败响应
|
||||
*/
|
||||
failResponse(): string {
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
// 网关类型映射
|
||||
type GatewayClass = new (config: Record<string, unknown>) => AbstractGateway;
|
||||
|
||||
/**
|
||||
* 支付网关工厂
|
||||
*/
|
||||
export class PaymentFactory {
|
||||
private static gateways: Map<string, GatewayClass> = new Map();
|
||||
|
||||
/**
|
||||
* 注册支付网关
|
||||
*/
|
||||
static register(name: string, gatewayClass: GatewayClass): void {
|
||||
this.gateways.set(name, gatewayClass);
|
||||
console.log(`[PaymentFactory] 注册支付网关: ${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付网关实例
|
||||
* @param gateway 网关名称,格式如 'wechat_jsapi',会取下划线前的部分
|
||||
*/
|
||||
static create(gateway: PaymentGateway | string): AbstractGateway {
|
||||
const gatewayName = gateway.split('_')[0] as PaymentPlatform;
|
||||
|
||||
const GatewayClass = this.gateways.get(gatewayName);
|
||||
if (!GatewayClass) {
|
||||
throw new GatewayNotFoundError(gateway);
|
||||
}
|
||||
|
||||
const config = this.getGatewayConfig(gatewayName);
|
||||
return new GatewayClass(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网关配置
|
||||
*/
|
||||
private static getGatewayConfig(gateway: PaymentPlatform): Record<string, unknown> {
|
||||
const configMap: Record<PaymentPlatform, () => Record<string, unknown>> = {
|
||||
alipay: () => ({
|
||||
// 支付宝新版接口需要 app_id,如果没有配置则使用 pid(旧版兼容)
|
||||
appId: process.env.ALIPAY_APP_ID || process.env.ALIPAY_PID || '2088511801157159',
|
||||
pid: process.env.ALIPAY_PID || '2088511801157159',
|
||||
sellerEmail: process.env.ALIPAY_SELLER_EMAIL || 'zhengzhiqun@vip.qq.com',
|
||||
privateKey: process.env.ALIPAY_PRIVATE_KEY || '',
|
||||
publicKey: process.env.ALIPAY_PUBLIC_KEY || '',
|
||||
md5Key: process.env.ALIPAY_MD5_KEY || 'lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp',
|
||||
enabled: process.env.ALIPAY_ENABLED === 'true',
|
||||
mode: process.env.ALIPAY_MODE || 'production',
|
||||
}),
|
||||
wechat: () => ({
|
||||
// 微信支付需要使用绑定了支付功能的服务号AppID
|
||||
appId: process.env.WECHAT_APPID || 'wx7c0dbf34ddba300d', // 服务号AppID(已绑定商户号)
|
||||
appSecret: process.env.WECHAT_APP_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
|
||||
serviceAppId: process.env.WECHAT_SERVICE_APPID || 'wx7c0dbf34ddba300d',
|
||||
serviceSecret: process.env.WECHAT_SERVICE_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
|
||||
mchId: process.env.WECHAT_MCH_ID || '1318592501',
|
||||
mchKey: process.env.WECHAT_MCH_KEY || 'wx3e31b068be59ddc131b068be59ddc2',
|
||||
certPath: process.env.WECHAT_CERT_PATH || '',
|
||||
keyPath: process.env.WECHAT_KEY_PATH || '',
|
||||
enabled: process.env.WECHAT_ENABLED === 'true',
|
||||
mode: process.env.WECHAT_MODE || 'production',
|
||||
}),
|
||||
paypal: () => ({
|
||||
clientId: process.env.PAYPAL_CLIENT_ID || '',
|
||||
clientSecret: process.env.PAYPAL_CLIENT_SECRET || '',
|
||||
mode: process.env.PAYPAL_MODE || 'sandbox',
|
||||
enabled: process.env.PAYPAL_ENABLED === 'true',
|
||||
}),
|
||||
stripe: () => ({
|
||||
publicKey: process.env.STRIPE_PUBLIC_KEY || '',
|
||||
secretKey: process.env.STRIPE_SECRET_KEY || '',
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||||
mode: process.env.STRIPE_MODE || 'test',
|
||||
enabled: process.env.STRIPE_ENABLED === 'true',
|
||||
}),
|
||||
usdt: () => ({
|
||||
gatewayType: process.env.USDT_GATEWAY_TYPE || 'nowpayments',
|
||||
apiKey: process.env.NOWPAYMENTS_API_KEY || '',
|
||||
ipnSecret: process.env.NOWPAYMENTS_IPN_SECRET || '',
|
||||
enabled: process.env.USDT_ENABLED === 'true',
|
||||
}),
|
||||
coin: () => ({
|
||||
rate: parseInt(process.env.COIN_RATE || '100', 10),
|
||||
enabled: process.env.COIN_ENABLED === 'true',
|
||||
}),
|
||||
};
|
||||
|
||||
return configMap[gateway]?.() || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已启用的支付网关列表
|
||||
*/
|
||||
static getEnabledGateways(): PaymentMethod[] {
|
||||
const methods: PaymentMethod[] = [];
|
||||
|
||||
// 支付宝
|
||||
if (process.env.ALIPAY_ENABLED === 'true' || true) { // 默认启用
|
||||
methods.push({
|
||||
gateway: 'alipay_wap',
|
||||
name: '支付宝',
|
||||
icon: '/icons/alipay.png',
|
||||
enabled: true,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 微信支付
|
||||
if (process.env.WECHAT_ENABLED === 'true' || true) { // 默认启用
|
||||
methods.push({
|
||||
gateway: 'wechat_native',
|
||||
name: '微信支付',
|
||||
icon: '/icons/wechat.png',
|
||||
enabled: true,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
// PayPal
|
||||
if (process.env.PAYPAL_ENABLED === 'true') {
|
||||
methods.push({
|
||||
gateway: 'paypal',
|
||||
name: 'PayPal',
|
||||
icon: '/icons/paypal.png',
|
||||
enabled: true,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Stripe
|
||||
if (process.env.STRIPE_ENABLED === 'true') {
|
||||
methods.push({
|
||||
gateway: 'stripe',
|
||||
name: 'Stripe',
|
||||
icon: '/icons/stripe.png',
|
||||
enabled: true,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
// USDT
|
||||
if (process.env.USDT_ENABLED === 'true') {
|
||||
methods.push({
|
||||
gateway: 'usdt',
|
||||
name: 'USDT (TRC20)',
|
||||
icon: '/icons/usdt.png',
|
||||
enabled: true,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查网关是否已注册
|
||||
*/
|
||||
static hasGateway(name: string): boolean {
|
||||
return this.gateways.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的网关名称
|
||||
*/
|
||||
static getRegisteredGateways(): string[] {
|
||||
return Array.from(this.gateways.keys());
|
||||
}
|
||||
}
|
||||
|
||||
// 导出便捷函数
|
||||
export function createPaymentGateway(gateway: PaymentGateway | string): AbstractGateway {
|
||||
return PaymentFactory.create(gateway);
|
||||
}
|
||||
32
lib/payment/index.ts
Normal file
32
lib/payment/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 支付模块入口 (Payment Module Entry)
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* 使用示例:
|
||||
* ```typescript
|
||||
* import { PaymentFactory, createPaymentGateway } from '@/lib/payment';
|
||||
*
|
||||
* // 方式1: 使用工厂创建
|
||||
* const gateway = PaymentFactory.create('wechat_native');
|
||||
* const result = await gateway.createTrade(data);
|
||||
*
|
||||
* // 方式2: 使用便捷函数
|
||||
* const gateway = createPaymentGateway('alipay_wap');
|
||||
* ```
|
||||
*
|
||||
* 作者: 卡若
|
||||
* 版本: v4.0
|
||||
*/
|
||||
|
||||
// 导出类型定义
|
||||
export * from './types';
|
||||
|
||||
// 导出工厂
|
||||
export { PaymentFactory, AbstractGateway, createPaymentGateway } from './factory';
|
||||
|
||||
// 导出网关实现
|
||||
export { AlipayGateway, AlipayService } from './alipay';
|
||||
export { WechatGateway, WechatPayService } from './wechat';
|
||||
|
||||
// 导出支付配置
|
||||
export { getPaymentConfig, getNotifyUrl, getReturnUrl } from './config';
|
||||
289
lib/payment/types.ts
Normal file
289
lib/payment/types.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 通用支付模块类型定义 (Universal Payment Module Types)
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* 作者: 卡若
|
||||
* 版本: v4.0
|
||||
*/
|
||||
|
||||
// 支付平台枚举
|
||||
export type PaymentPlatform = 'alipay' | 'wechat' | 'paypal' | 'stripe' | 'usdt' | 'coin';
|
||||
|
||||
// 支付网关类型
|
||||
export type PaymentGateway =
|
||||
| 'alipay_web' | 'alipay_wap' | 'alipay_qr'
|
||||
| 'wechat_native' | 'wechat_jsapi' | 'wechat_h5' | 'wechat_app'
|
||||
| 'paypal' | 'stripe' | 'usdt' | 'coin';
|
||||
|
||||
// 订单状态
|
||||
export type OrderStatus = 'created' | 'paying' | 'paid' | 'closed' | 'refunded';
|
||||
|
||||
// 交易状态
|
||||
export type TradeStatus = 'paying' | 'paid' | 'closed' | 'refunded';
|
||||
|
||||
// 交易类型
|
||||
export type TradeType = 'purchase' | 'recharge';
|
||||
|
||||
// 支付结果类型
|
||||
export type PaymentResultType = 'url' | 'qrcode' | 'json' | 'address' | 'direct';
|
||||
|
||||
// 货币类型
|
||||
export type Currency = 'CNY' | 'USD' | 'EUR' | 'USDT';
|
||||
|
||||
/**
|
||||
* 创建订单请求参数
|
||||
*/
|
||||
export interface CreateOrderParams {
|
||||
userId: string;
|
||||
title: string;
|
||||
amount: number; // 金额(元)
|
||||
currency?: Currency;
|
||||
productId?: string;
|
||||
productType?: 'section' | 'fullbook' | 'membership' | 'vip';
|
||||
extraParams?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单信息
|
||||
*/
|
||||
export interface Order {
|
||||
sn: string; // 订单号
|
||||
userId: string;
|
||||
title: string;
|
||||
priceAmount: number; // 原价(分)
|
||||
payAmount: number; // 应付金额(分)
|
||||
currency: Currency;
|
||||
status: OrderStatus;
|
||||
productId?: string;
|
||||
productType?: string;
|
||||
extraData?: Record<string, unknown>;
|
||||
paidAt?: Date;
|
||||
closedAt?: Date;
|
||||
expiredAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起支付请求参数
|
||||
*/
|
||||
export interface CheckoutParams {
|
||||
orderSn: string;
|
||||
gateway: PaymentGateway;
|
||||
returnUrl?: string;
|
||||
openid?: string; // 微信JSAPI需要
|
||||
coinAmount?: number; // 虚拟币抵扣
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建交易请求数据
|
||||
*/
|
||||
export interface CreateTradeData {
|
||||
goodsTitle: string;
|
||||
goodsDetail?: string;
|
||||
tradeSn: string;
|
||||
orderSn: string;
|
||||
amount: number; // 金额(分)
|
||||
notifyUrl: string;
|
||||
returnUrl?: string;
|
||||
platformType?: string; // web/wap/jsapi/native/h5/app
|
||||
createIp?: string;
|
||||
openId?: string;
|
||||
attach?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付交易结果
|
||||
*/
|
||||
export interface TradeResult {
|
||||
type: PaymentResultType;
|
||||
payload: string | Record<string, string>;
|
||||
tradeSn: string;
|
||||
expiration?: number; // 过期时间(秒)
|
||||
amount?: number;
|
||||
coinDeducted?: number;
|
||||
prepayId?: string; // 微信预支付ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调解析结果
|
||||
*/
|
||||
export interface NotifyResult {
|
||||
status: 'paying' | 'paid' | 'closed' | 'refunded' | 'failed';
|
||||
tradeSn: string;
|
||||
platformSn: string;
|
||||
payAmount: number; // 分
|
||||
payTime: Date;
|
||||
currency: Currency;
|
||||
attach?: Record<string, unknown>;
|
||||
rawData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易流水
|
||||
*/
|
||||
export interface PayTrade {
|
||||
id?: string;
|
||||
tradeSn: string;
|
||||
orderSn: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
amount: number; // 分
|
||||
cashAmount: number; // 现金支付金额(分)
|
||||
coinAmount: number; // 虚拟币抵扣金额
|
||||
currency: Currency;
|
||||
platform: PaymentPlatform;
|
||||
platformType?: string;
|
||||
platformSn?: string;
|
||||
platformCreatedParams?: Record<string, unknown>;
|
||||
platformCreatedResult?: Record<string, unknown>;
|
||||
status: TradeStatus;
|
||||
type: TradeType;
|
||||
payTime?: Date;
|
||||
notifyData?: Record<string, unknown>;
|
||||
sellerId?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款记录
|
||||
*/
|
||||
export interface Refund {
|
||||
id?: string;
|
||||
refundSn: string;
|
||||
tradeSn: string;
|
||||
orderSn: string;
|
||||
amount: number; // 分
|
||||
reason?: string;
|
||||
status: 'pending' | 'processing' | 'success' | 'failed';
|
||||
platformRefundSn?: string;
|
||||
refundedAt?: Date;
|
||||
operatorId?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付网关配置
|
||||
*/
|
||||
export interface GatewayConfig {
|
||||
enabled: boolean;
|
||||
mode: 'sandbox' | 'production';
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝配置
|
||||
*/
|
||||
export interface AlipayConfig extends GatewayConfig {
|
||||
appId: string;
|
||||
pid: string;
|
||||
sellerEmail?: string;
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
md5Key?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信支付配置
|
||||
*/
|
||||
export interface WechatConfig extends GatewayConfig {
|
||||
appId: string;
|
||||
appSecret?: string;
|
||||
serviceAppId?: string; // 服务号AppID
|
||||
serviceSecret?: string;
|
||||
mchId: string;
|
||||
mchKey: string;
|
||||
certPath?: string;
|
||||
keyPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一响应格式
|
||||
*/
|
||||
export interface PaymentResponse<T = unknown> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付方式信息
|
||||
*/
|
||||
export interface PaymentMethod {
|
||||
gateway: PaymentGateway;
|
||||
name: string;
|
||||
icon: string;
|
||||
enabled: boolean;
|
||||
available: boolean; // 当前环境是否可用
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付异常
|
||||
*/
|
||||
export class PaymentException extends Error {
|
||||
constructor(message: string, public code?: string) {
|
||||
super(message);
|
||||
this.name = 'PaymentException';
|
||||
}
|
||||
}
|
||||
|
||||
export class SignatureError extends PaymentException {
|
||||
constructor(message = '签名验证失败') {
|
||||
super(message, 'SIGNATURE_ERROR');
|
||||
this.name = 'SignatureError';
|
||||
}
|
||||
}
|
||||
|
||||
export class AmountMismatchError extends PaymentException {
|
||||
constructor(message = '金额不匹配') {
|
||||
super(message, 'AMOUNT_MISMATCH');
|
||||
this.name = 'AmountMismatchError';
|
||||
}
|
||||
}
|
||||
|
||||
export class GatewayNotFoundError extends PaymentException {
|
||||
constructor(gateway: string) {
|
||||
super(`不支持的支付网关: ${gateway}`, 'GATEWAY_NOT_FOUND');
|
||||
this.name = 'GatewayNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:元转分
|
||||
*/
|
||||
export function yuanToFen(yuan: number): number {
|
||||
return Math.round(yuan * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:分转元
|
||||
*/
|
||||
export function fenToYuan(fen: number): number {
|
||||
return Math.round(fen) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
* 格式: YYYYMMDD + 6位随机数
|
||||
*/
|
||||
export function generateOrderSn(prefix = ''): string {
|
||||
const date = new Date();
|
||||
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const random = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
||||
return `${prefix}${dateStr}${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成交易流水号
|
||||
* 格式: T + YYYYMMDDHHMMSS + 5位随机数
|
||||
*/
|
||||
export function generateTradeSn(prefix = 'T'): string {
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString()
|
||||
.replace(/[-:T]/g, '')
|
||||
.slice(0, 14);
|
||||
const random = Math.floor(Math.random() * 100000).toString().padStart(5, '0');
|
||||
return `${prefix}${timestamp}${random}`;
|
||||
}
|
||||
615
lib/payment/wechat.ts
Normal file
615
lib/payment/wechat.ts
Normal file
@@ -0,0 +1,615 @@
|
||||
/**
|
||||
* 微信支付网关实现 (Wechat Pay Gateway)
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* 支持:
|
||||
* - Native扫码支付 (platform_type='native')
|
||||
* - JSAPI公众号/小程序支付 (platform_type='jsapi')
|
||||
* - H5支付 (platform_type='h5')
|
||||
* - APP支付 (platform_type='app')
|
||||
*
|
||||
* 作者: 卡若
|
||||
* 版本: v4.0
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { AbstractGateway, PaymentFactory } from './factory';
|
||||
import {
|
||||
CreateTradeData,
|
||||
TradeResult,
|
||||
NotifyResult,
|
||||
SignatureError,
|
||||
} from './types';
|
||||
|
||||
export interface WechatPayConfig {
|
||||
appId: string;
|
||||
appSecret?: string;
|
||||
serviceAppId?: string;
|
||||
serviceSecret?: string;
|
||||
mchId: string;
|
||||
mchKey: string;
|
||||
certPath?: string;
|
||||
keyPath?: string;
|
||||
enabled?: boolean;
|
||||
mode?: 'sandbox' | 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信支付网关
|
||||
*/
|
||||
export class WechatGateway extends AbstractGateway {
|
||||
private readonly UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
|
||||
private readonly ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery';
|
||||
private readonly CLOSE_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/closeorder';
|
||||
private readonly REFUND_URL = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
|
||||
|
||||
private appId: string;
|
||||
private appSecret: string;
|
||||
private serviceAppId: string;
|
||||
private serviceSecret: string;
|
||||
private mchId: string;
|
||||
private mchKey: string;
|
||||
private certPath: string;
|
||||
private keyPath: string;
|
||||
|
||||
constructor(config: Record<string, unknown>) {
|
||||
super(config);
|
||||
const cfg = config as unknown as WechatPayConfig;
|
||||
this.appId = cfg.appId || '';
|
||||
this.appSecret = cfg.appSecret || '';
|
||||
this.serviceAppId = cfg.serviceAppId || '';
|
||||
this.serviceSecret = cfg.serviceSecret || '';
|
||||
this.mchId = cfg.mchId || '';
|
||||
this.mchKey = cfg.mchKey || '';
|
||||
this.certPath = cfg.certPath || '';
|
||||
this.keyPath = cfg.keyPath || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建微信支付交易
|
||||
*/
|
||||
async createTrade(data: CreateTradeData): Promise<TradeResult> {
|
||||
const platformType = (data.platformType || 'native').toUpperCase();
|
||||
|
||||
// 构建统一下单参数
|
||||
const params: Record<string, string> = {
|
||||
appid: platformType === 'JSAPI' ? this.serviceAppId || this.appId : this.appId,
|
||||
mch_id: this.mchId,
|
||||
nonce_str: this.generateNonceStr(),
|
||||
body: data.goodsTitle.slice(0, 128),
|
||||
out_trade_no: data.tradeSn,
|
||||
total_fee: data.amount.toString(), // 微信以分为单位
|
||||
spbill_create_ip: data.createIp || '127.0.0.1',
|
||||
notify_url: data.notifyUrl,
|
||||
trade_type: platformType === 'H5' ? 'MWEB' : platformType,
|
||||
};
|
||||
|
||||
// 附加数据
|
||||
if (data.attach) {
|
||||
params.attach = JSON.stringify(data.attach);
|
||||
}
|
||||
|
||||
// JSAPI需要openid
|
||||
if (platformType === 'JSAPI') {
|
||||
if (!data.openId) {
|
||||
throw new Error('微信JSAPI支付需要提供 openid');
|
||||
}
|
||||
params.openid = data.openId;
|
||||
}
|
||||
|
||||
// H5支付需要scene_info
|
||||
if (platformType === 'MWEB' || platformType === 'H5') {
|
||||
params.scene_info = JSON.stringify({
|
||||
h5_info: {
|
||||
type: 'Wap',
|
||||
wap_url: data.returnUrl || '',
|
||||
wap_name: data.goodsTitle.slice(0, 32),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
params.sign = this.generateSign(params);
|
||||
|
||||
// 调用微信支付统一下单接口
|
||||
return this.callUnifiedOrder(params, data.tradeSn, platformType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用微信支付统一下单接口
|
||||
*/
|
||||
private async callUnifiedOrder(params: Record<string, string>, tradeSn: string, tradeType: string): Promise<TradeResult> {
|
||||
try {
|
||||
// 转换为XML
|
||||
const xmlData = this.dictToXml(params);
|
||||
|
||||
console.log('[Wechat] 调用统一下单接口:', {
|
||||
url: this.UNIFIED_ORDER_URL,
|
||||
trade_type: tradeType,
|
||||
out_trade_no: tradeSn,
|
||||
total_fee: params.total_fee,
|
||||
});
|
||||
|
||||
// 发送请求到微信支付
|
||||
const response = await fetch(this.UNIFIED_ORDER_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
body: xmlData,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('[Wechat] 统一下单响应:', responseText.slice(0, 500));
|
||||
|
||||
// 解析响应
|
||||
const result = this.xmlToDict(responseText);
|
||||
|
||||
// 检查返回结果
|
||||
if (result.return_code !== 'SUCCESS') {
|
||||
throw new Error(`微信支付请求失败: ${result.return_msg || '未知错误'}`);
|
||||
}
|
||||
|
||||
if (result.result_code !== 'SUCCESS') {
|
||||
throw new Error(`微信支付失败: ${result.err_code_des || result.err_code || '未知错误'}`);
|
||||
}
|
||||
|
||||
// 验证返回签名
|
||||
if (!this.verifySign(result)) {
|
||||
throw new SignatureError('微信返回数据签名验证失败');
|
||||
}
|
||||
|
||||
// 根据支付类型返回不同的数据
|
||||
return this.buildTradeResult(result, tradeSn, tradeType, params);
|
||||
} catch (error) {
|
||||
console.error('[Wechat] 统一下单失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建支付结果
|
||||
*/
|
||||
private buildTradeResult(result: Record<string, string>, tradeSn: string, tradeType: string, params: Record<string, string>): TradeResult {
|
||||
switch (tradeType) {
|
||||
case 'NATIVE':
|
||||
// 扫码支付返回二维码链接
|
||||
if (!result.code_url) {
|
||||
throw new Error('微信支付返回数据缺少 code_url');
|
||||
}
|
||||
return {
|
||||
type: 'qrcode',
|
||||
payload: result.code_url,
|
||||
tradeSn,
|
||||
expiration: 1800, // 30分钟
|
||||
prepayId: result.prepay_id,
|
||||
};
|
||||
|
||||
case 'JSAPI':
|
||||
// 公众号支付返回JS SDK参数
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const nonceStr = this.generateNonceStr();
|
||||
const prepayId = result.prepay_id;
|
||||
|
||||
if (!prepayId) {
|
||||
throw new Error('微信支付返回数据缺少 prepay_id');
|
||||
}
|
||||
|
||||
const jsParams: Record<string, string> = {
|
||||
appId: params.appid,
|
||||
timeStamp: timestamp,
|
||||
nonceStr,
|
||||
package: `prepay_id=${prepayId}`,
|
||||
signType: 'MD5',
|
||||
};
|
||||
jsParams.paySign = this.generateSign(jsParams);
|
||||
|
||||
return {
|
||||
type: 'json',
|
||||
payload: jsParams,
|
||||
tradeSn,
|
||||
expiration: 1800,
|
||||
prepayId,
|
||||
};
|
||||
|
||||
case 'MWEB':
|
||||
case 'H5':
|
||||
// H5支付返回跳转链接
|
||||
if (!result.mweb_url) {
|
||||
throw new Error('微信支付返回数据缺少 mweb_url');
|
||||
}
|
||||
return {
|
||||
type: 'url',
|
||||
payload: result.mweb_url,
|
||||
tradeSn,
|
||||
expiration: 300, // H5支付链接有效期较短
|
||||
prepayId: result.prepay_id,
|
||||
};
|
||||
|
||||
case 'APP':
|
||||
// APP支付返回SDK参数
|
||||
const appTimestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const appPrepayId = result.prepay_id;
|
||||
|
||||
if (!appPrepayId) {
|
||||
throw new Error('微信支付返回数据缺少 prepay_id');
|
||||
}
|
||||
|
||||
const appParams: Record<string, string> = {
|
||||
appid: this.appId,
|
||||
partnerid: this.mchId,
|
||||
prepayid: appPrepayId,
|
||||
package: 'Sign=WXPay',
|
||||
noncestr: this.generateNonceStr(),
|
||||
timestamp: appTimestamp,
|
||||
};
|
||||
appParams.sign = this.generateSign(appParams);
|
||||
|
||||
return {
|
||||
type: 'json',
|
||||
payload: appParams,
|
||||
tradeSn,
|
||||
expiration: 1800,
|
||||
prepayId: appPrepayId,
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`不支持的微信支付类型: ${tradeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
private generateNonceStr(): string {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MD5签名
|
||||
*/
|
||||
generateSign(params: Record<string, string>): string {
|
||||
const sortedKeys = Object.keys(params).sort();
|
||||
const signString = sortedKeys
|
||||
.filter((key) => params[key] && key !== 'sign')
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
const signWithKey = `${signString}&key=${this.mchKey}`;
|
||||
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证签名
|
||||
*/
|
||||
verifySign(data: Record<string, string>): boolean {
|
||||
const receivedSign = data.sign;
|
||||
if (!receivedSign) return false;
|
||||
|
||||
const params = { ...data };
|
||||
delete params.sign;
|
||||
|
||||
const calculatedSign = this.generateSign(params);
|
||||
return receivedSign === calculatedSign;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析回调数据
|
||||
*/
|
||||
parseNotify(data: string | Record<string, string>): NotifyResult {
|
||||
// 如果是XML字符串,先转换为dict
|
||||
const params = typeof data === 'string' ? this.xmlToDict(data) : data;
|
||||
|
||||
// 验证签名
|
||||
if (!this.verifySign({ ...params })) {
|
||||
throw new SignatureError('微信签名验证失败');
|
||||
}
|
||||
|
||||
const resultCode = params.result_code || '';
|
||||
const status = resultCode === 'SUCCESS' ? 'paid' : 'failed';
|
||||
|
||||
// 解析透传参数
|
||||
let attach: Record<string, unknown> = {};
|
||||
const attachStr = params.attach || '';
|
||||
if (attachStr) {
|
||||
try {
|
||||
attach = JSON.parse(attachStr);
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
|
||||
// 解析支付时间 (格式: 20240117100530)
|
||||
const timeEnd = params.time_end || '';
|
||||
let payTime = new Date();
|
||||
if (timeEnd && timeEnd.length === 14) {
|
||||
const year = parseInt(timeEnd.slice(0, 4), 10);
|
||||
const month = parseInt(timeEnd.slice(4, 6), 10) - 1;
|
||||
const day = parseInt(timeEnd.slice(6, 8), 10);
|
||||
const hour = parseInt(timeEnd.slice(8, 10), 10);
|
||||
const minute = parseInt(timeEnd.slice(10, 12), 10);
|
||||
const second = parseInt(timeEnd.slice(12, 14), 10);
|
||||
payTime = new Date(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
tradeSn: params.out_trade_no || '',
|
||||
platformSn: params.transaction_id || '',
|
||||
payAmount: parseInt(params.cash_fee || params.total_fee || '0', 10),
|
||||
payTime,
|
||||
currency: (params.fee_type || 'CNY') as 'CNY',
|
||||
attach,
|
||||
rawData: params,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* XML转字典
|
||||
*/
|
||||
private xmlToDict(xml: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(xml)) !== null) {
|
||||
result[match[1]] = match[2];
|
||||
}
|
||||
|
||||
// 也处理不带CDATA的标签
|
||||
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g;
|
||||
while ((match = simpleRegex.exec(xml)) !== null) {
|
||||
if (!result[match[1]]) {
|
||||
result[match[1]] = match[2];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典转XML
|
||||
*/
|
||||
private dictToXml(data: Record<string, string>): string {
|
||||
const xml = ['<xml>'];
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value === 'string') {
|
||||
xml.push(`<${key}><![CDATA[${value}]]></${key}>`);
|
||||
} else {
|
||||
xml.push(`<${key}>${value}</${key}>`);
|
||||
}
|
||||
}
|
||||
xml.push('</xml>');
|
||||
return xml.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询交易状态
|
||||
*/
|
||||
async queryTrade(tradeSn: string): Promise<NotifyResult | null> {
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
appid: this.appId,
|
||||
mch_id: this.mchId,
|
||||
out_trade_no: tradeSn,
|
||||
nonce_str: this.generateNonceStr(),
|
||||
};
|
||||
params.sign = this.generateSign(params);
|
||||
|
||||
const xmlData = this.dictToXml(params);
|
||||
|
||||
console.log('[Wechat] 查询订单:', tradeSn);
|
||||
|
||||
const response = await fetch(this.ORDER_QUERY_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
body: xmlData,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const result = this.xmlToDict(responseText);
|
||||
|
||||
console.log('[Wechat] 查询响应:', {
|
||||
return_code: result.return_code,
|
||||
result_code: result.result_code,
|
||||
trade_state: result.trade_state,
|
||||
err_code: result.err_code,
|
||||
});
|
||||
|
||||
// 检查通信是否成功
|
||||
if (result.return_code !== 'SUCCESS') {
|
||||
console.log('[Wechat] 订单查询通信失败:', result.return_msg);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果业务结果失败,但是是订单不存在的情况,返回 paying 状态
|
||||
if (result.result_code !== 'SUCCESS') {
|
||||
if (result.err_code === 'ORDERNOTEXIST') {
|
||||
console.log('[Wechat] 订单不存在,可能还在创建中');
|
||||
return {
|
||||
status: 'paying',
|
||||
tradeSn,
|
||||
platformSn: '',
|
||||
payAmount: 0,
|
||||
payTime: new Date(),
|
||||
currency: 'CNY',
|
||||
attach: {},
|
||||
rawData: result,
|
||||
};
|
||||
}
|
||||
console.log('[Wechat] 订单查询业务失败:', result.err_code, result.err_code_des);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if (!this.verifySign(result)) {
|
||||
console.log('[Wechat] 订单查询签名验证失败');
|
||||
return null;
|
||||
}
|
||||
|
||||
const tradeState = result.trade_state || '';
|
||||
let status: 'paying' | 'paid' | 'closed' | 'refunded' = 'paying';
|
||||
|
||||
switch (tradeState) {
|
||||
case 'SUCCESS':
|
||||
status = 'paid';
|
||||
break;
|
||||
case 'CLOSED':
|
||||
case 'REVOKED':
|
||||
case 'PAYERROR':
|
||||
status = 'closed';
|
||||
break;
|
||||
case 'REFUND':
|
||||
status = 'refunded';
|
||||
break;
|
||||
case 'NOTPAY':
|
||||
case 'USERPAYING':
|
||||
default:
|
||||
status = 'paying';
|
||||
}
|
||||
|
||||
console.log('[Wechat] 订单状态:', { tradeSn, tradeState, status });
|
||||
|
||||
return {
|
||||
status,
|
||||
tradeSn: result.out_trade_no || tradeSn,
|
||||
platformSn: result.transaction_id || '',
|
||||
payAmount: parseInt(result.cash_fee || result.total_fee || '0', 10),
|
||||
payTime: new Date(),
|
||||
currency: 'CNY',
|
||||
attach: {},
|
||||
rawData: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Wechat] 查询订单失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭交易
|
||||
*/
|
||||
async closeTrade(tradeSn: string): Promise<boolean> {
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
appid: this.appId,
|
||||
mch_id: this.mchId,
|
||||
out_trade_no: tradeSn,
|
||||
nonce_str: this.generateNonceStr(),
|
||||
};
|
||||
params.sign = this.generateSign(params);
|
||||
|
||||
const xmlData = this.dictToXml(params);
|
||||
|
||||
console.log('[Wechat] 关闭订单:', tradeSn);
|
||||
|
||||
const response = await fetch(this.CLOSE_ORDER_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
body: xmlData,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const result = this.xmlToDict(responseText);
|
||||
|
||||
return result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS';
|
||||
} catch (error) {
|
||||
console.error('[Wechat] 关闭订单失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
*/
|
||||
async refund(tradeSn: string, refundSn: string, amount: number, reason?: string): Promise<boolean> {
|
||||
console.log(`[Wechat] 发起退款: ${tradeSn}, ${refundSn}, ${amount}, ${reason}`);
|
||||
// 退款需要证书,这里只是接口定义
|
||||
// 实际使用时需要配置证书路径并使用 https 模块发送带证书的请求
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调成功响应
|
||||
*/
|
||||
override successResponse(): string {
|
||||
return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调失败响应
|
||||
*/
|
||||
override failResponse(): string {
|
||||
return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[ERROR]]></return_msg></xml>';
|
||||
}
|
||||
}
|
||||
|
||||
// 注册到工厂
|
||||
PaymentFactory.register('wechat', WechatGateway);
|
||||
|
||||
// 导出兼容旧版的 WechatPayService
|
||||
export interface WechatPayServiceConfig {
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
mchId: string;
|
||||
apiKey: string;
|
||||
notifyUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧版的 WechatPayService
|
||||
* @deprecated 请使用 WechatGateway
|
||||
*/
|
||||
export class WechatPayService {
|
||||
private gateway: WechatGateway;
|
||||
private notifyUrl: string;
|
||||
|
||||
constructor(config: WechatPayServiceConfig) {
|
||||
this.gateway = new WechatGateway({
|
||||
appId: config.appId,
|
||||
appSecret: config.appSecret,
|
||||
mchId: config.mchId,
|
||||
mchKey: config.apiKey,
|
||||
});
|
||||
this.notifyUrl = config.notifyUrl;
|
||||
}
|
||||
|
||||
async createOrder(params: {
|
||||
outTradeNo: string;
|
||||
body: string;
|
||||
totalFee: number;
|
||||
spbillCreateIp: string;
|
||||
}) {
|
||||
const result = await this.gateway.createTrade({
|
||||
goodsTitle: params.body,
|
||||
tradeSn: params.outTradeNo,
|
||||
orderSn: params.outTradeNo,
|
||||
amount: Math.round(params.totalFee * 100), // 转换为分
|
||||
notifyUrl: this.notifyUrl,
|
||||
createIp: params.spbillCreateIp,
|
||||
platformType: 'native',
|
||||
});
|
||||
|
||||
return {
|
||||
codeUrl: typeof result.payload === 'string' ? result.payload : '',
|
||||
prepayId: result.prepayId || `prepay_${Date.now()}`,
|
||||
outTradeNo: params.outTradeNo,
|
||||
};
|
||||
}
|
||||
|
||||
generateSign(params: Record<string, string>): string {
|
||||
return this.gateway.generateSign(params);
|
||||
}
|
||||
|
||||
verifySign(params: Record<string, string>): boolean {
|
||||
return this.gateway.verifySign(params);
|
||||
}
|
||||
|
||||
async queryTrade(tradeSn: string) {
|
||||
return this.gateway.queryTrade(tradeSn);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import crypto from "crypto"
|
||||
|
||||
export interface WechatPayConfig {
|
||||
appId: string
|
||||
appSecret: string
|
||||
mchId: string
|
||||
apiKey: string
|
||||
notifyUrl: string
|
||||
}
|
||||
|
||||
export class WechatPayService {
|
||||
constructor(private config: WechatPayConfig) {}
|
||||
|
||||
// 创建微信支付订单(扫码支付)
|
||||
async createOrder(params: {
|
||||
outTradeNo: string
|
||||
body: string
|
||||
totalFee: number
|
||||
spbillCreateIp: string
|
||||
}) {
|
||||
const orderParams = {
|
||||
appid: this.config.appId,
|
||||
mch_id: this.config.mchId,
|
||||
nonce_str: this.generateNonceStr(),
|
||||
body: params.body,
|
||||
out_trade_no: params.outTradeNo,
|
||||
total_fee: Math.round(params.totalFee * 100).toString(), // 转换为分
|
||||
spbill_create_ip: params.spbillCreateIp,
|
||||
notify_url: this.config.notifyUrl,
|
||||
trade_type: "NATIVE", // 扫码支付
|
||||
}
|
||||
|
||||
const sign = this.generateSign(orderParams)
|
||||
const xmlData = this.buildXML({ ...orderParams, sign })
|
||||
|
||||
// In production, make actual API call to WeChat
|
||||
// const response = await fetch("https://api.mch.weixin.qq.com/pay/unifiedorder", {
|
||||
// method: "POST",
|
||||
// body: xmlData,
|
||||
// headers: { "Content-Type": "application/xml" },
|
||||
// })
|
||||
|
||||
// Mock response for development
|
||||
return {
|
||||
codeUrl: `weixin://wxpay/bizpayurl?pr=${this.generateNonceStr()}`,
|
||||
prepayId: `prepay_${Date.now()}`,
|
||||
outTradeNo: params.outTradeNo,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成随机字符串
|
||||
private generateNonceStr(): string {
|
||||
return crypto.randomBytes(16).toString("hex")
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
generateSign(params: Record<string, string>): string {
|
||||
const sortedKeys = Object.keys(params).sort()
|
||||
const signString = sortedKeys
|
||||
.filter((key) => params[key] && key !== "sign")
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&")
|
||||
|
||||
const signWithKey = `${signString}&key=${this.config.apiKey}`
|
||||
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex").toUpperCase()
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
verifySign(params: Record<string, string>): boolean {
|
||||
const receivedSign = params.sign
|
||||
if (!receivedSign) return false
|
||||
|
||||
const calculatedSign = this.generateSign(params)
|
||||
return receivedSign === calculatedSign
|
||||
}
|
||||
|
||||
// 构建XML数据
|
||||
private buildXML(params: Record<string, string>): string {
|
||||
const xml = ["<xml>"]
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
xml.push(`<${key}><![CDATA[${value}]]></${key}>`)
|
||||
}
|
||||
xml.push("</xml>")
|
||||
return xml.join("")
|
||||
}
|
||||
|
||||
// 解析XML数据
|
||||
private async parseXML(xml: string): Promise<Record<string, string>> {
|
||||
const result: Record<string, string> = {}
|
||||
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(xml)) !== null) {
|
||||
result[match[1]] = match[2]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
29
lib/store.ts
29
lib/store.ts
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Zustand 状态管理
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
"use client"
|
||||
|
||||
import { create } from "zustand"
|
||||
@@ -18,6 +23,8 @@ export interface User {
|
||||
withdrawnEarnings: number
|
||||
referralCount: number
|
||||
createdAt: string
|
||||
wechat?: string
|
||||
alipay?: string
|
||||
}
|
||||
|
||||
export interface Withdrawal {
|
||||
@@ -193,9 +200,9 @@ const initialSettings: Settings = {
|
||||
authorShare: 10,
|
||||
paymentMethods: {
|
||||
alipay: {
|
||||
enabled: true,
|
||||
qrCode: "",
|
||||
account: "",
|
||||
enabled: false, // 已禁用支付宝
|
||||
qrCode: "/images/alipay.png",
|
||||
account: "卡若",
|
||||
partnerId: "2088511801157159",
|
||||
securityKey: "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
|
||||
mobilePayEnabled: true,
|
||||
@@ -203,8 +210,8 @@ const initialSettings: Settings = {
|
||||
},
|
||||
wechat: {
|
||||
enabled: true,
|
||||
qrCode: "",
|
||||
account: "",
|
||||
qrCode: "/images/wechat-pay.png",
|
||||
account: "卡若",
|
||||
websiteAppId: "wx432c93e275548671",
|
||||
websiteAppSecret: "25b7e7fdb7998e5107e242ebb6ddabd0",
|
||||
serviceAppId: "wx7c0dbf34ddba300d",
|
||||
@@ -212,10 +219,10 @@ const initialSettings: Settings = {
|
||||
mpVerifyCode: "SP8AfZJyAvprRORT",
|
||||
merchantId: "1318592501",
|
||||
apiKey: "wx3e31b068be59ddc131b068be59ddc2",
|
||||
groupQrCode: "",
|
||||
groupQrCode: "/images/party-group-qr.png",
|
||||
},
|
||||
usdt: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
network: "TRC20",
|
||||
address: "",
|
||||
exchangeRate: 7.2,
|
||||
@@ -263,8 +270,8 @@ const initialSettings: Settings = {
|
||||
platform: "Soul派对房",
|
||||
},
|
||||
siteConfig: {
|
||||
siteName: "卡若日记",
|
||||
siteTitle: "一场SOUL的创业实验场",
|
||||
siteName: "一场soul的创业实验",
|
||||
siteTitle: "一场soul的创业实验",
|
||||
siteDescription: "来自Soul派对房的真实商业故事",
|
||||
logo: "/logo.png",
|
||||
favicon: "/favicon.ico",
|
||||
@@ -277,7 +284,7 @@ const initialSettings: Settings = {
|
||||
my: { enabled: true, label: "我的" },
|
||||
},
|
||||
pageConfig: {
|
||||
homeTitle: "一场SOUL的创业实验场",
|
||||
homeTitle: "一场soul的创业实验",
|
||||
homeSubtitle: "来自Soul派对房的真实商业故事",
|
||||
chaptersTitle: "我要看",
|
||||
matchTitle: "语音匹配",
|
||||
@@ -296,6 +303,8 @@ export const useStore = create<StoreState>()(
|
||||
settings: initialSettings,
|
||||
|
||||
login: async (phone: string, code: string) => {
|
||||
// 真实场景下应该调用后端API验证验证码
|
||||
// 这里暂时保留简单验证用于演示
|
||||
if (code !== "123456") {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user