主要更新: 1. 按H5网页端完全重构匹配功能(match页面) - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募 - 资源对接等类型弹出手机号/微信号输入框 - 去掉重新匹配按钮,改为返回按钮 2. 修复所有卡片对齐和宽度问题 - 目录页附录卡片居中 - 首页阅读进度卡片满宽度 - 我的页面菜单卡片对齐 - 推广中心分享卡片统一宽度 3. 修复目录页图标和文字对齐 - section-icon固定40rpx宽高 - section-title与图标垂直居中 4. 更新真实完整文章标题(62篇) - 从book目录读取真实markdown文件名 - 替换之前的简化标题 5. 新增文章数据API - /api/db/chapters - 获取完整书籍结构 - 支持按ID获取单篇文章内容
512 lines
15 KiB
TypeScript
512 lines
15 KiB
TypeScript
// 数据库连接配置
|
||
// 使用腾讯云数据库
|
||
|
||
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')
|
||
}
|