Files
soul/app/api/db/migrate/route.ts
卡若 afc2376e96 v1.19 全面改版:VIP会员系统、我的收益、创业老板排行、阅读量排序
- 后端: users表新增VIP字段, 4个VIP API (purchase/status/profile/members)
- 后端: hot接口改按user_tracks阅读量排序
- 后端: orders表支持vip产品类型, migrate新增vip_fields迁移
- 小程序「我的」: 推广中心改为我的收益, 头像VIP标识, VIP入口卡片
- 小程序「我的」: 最近阅读显示真实章节名称
- 小程序首页: 去掉内容概览, 新增创业老板排行(4列网格)
- 小程序首页: 精选推荐从hot接口获取, goToRead增加track记录
- 新增页面: VIP详情页, 会员详情页
- 开发文档精简为10个标准目录, 创建SKILL.md, 需求日志规范化

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 14:07:41 +08:00

261 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

/**
* 数据库迁移API
* 用于升级数据库结构
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
/**
* POST - 执行数据库迁移
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}))
const { migration } = body
const results: string[] = []
// 用户表扩展字段(存客宝同步和标签)
if (!migration || migration === 'user_ckb_fields') {
const userFields = [
{ name: 'ckb_user_id', def: "VARCHAR(100) DEFAULT NULL COMMENT '存客宝用户ID'" },
{ name: 'ckb_synced_at', def: "DATETIME DEFAULT NULL COMMENT '最后同步时间'" },
{ name: 'ckb_tags', def: "JSON DEFAULT NULL COMMENT '存客宝标签'" },
{ name: 'tags', def: "JSON DEFAULT NULL COMMENT '系统标签'" },
{ name: 'source_tags', def: "JSON DEFAULT NULL COMMENT '来源标签'" },
{ name: 'merged_tags', def: "JSON DEFAULT NULL COMMENT '合并后的标签'" },
{ name: 'source', def: "VARCHAR(50) DEFAULT NULL COMMENT '用户来源'" },
{ name: 'created_by', def: "VARCHAR(100) DEFAULT NULL COMMENT '创建人'" },
{ name: 'matched_by', def: "VARCHAR(100) DEFAULT NULL COMMENT '匹配人'" }
]
let addedCount = 0
let existCount = 0
for (const field of userFields) {
try {
// 检查字段是否存在
await query(`SELECT ${field.name} FROM users LIMIT 1`)
existCount++
} catch {
// 字段不存在,添加它
try {
await query(`ALTER TABLE users ADD COLUMN ${field.name} ${field.def}`)
addedCount++
} catch (e: any) {
if (e.code !== 'ER_DUP_FIELDNAME') {
results.push(`⚠️ 添加字段 ${field.name} 失败: ${e.message}`)
}
}
}
}
if (addedCount > 0) {
results.push(`✅ 用户表新增 ${addedCount} 个字段`)
}
if (existCount > 0) {
results.push(` 用户表已有 ${existCount} 个字段存在`)
}
}
// 用户行为轨迹表
if (!migration || migration === 'user_tracks') {
try {
await query(`
CREATE TABLE IF NOT EXISTS user_tracks (
id VARCHAR(50) PRIMARY KEY,
user_id VARCHAR(100) NOT NULL COMMENT '用户ID',
action VARCHAR(50) NOT NULL COMMENT '行为类型',
chapter_id VARCHAR(100) DEFAULT NULL COMMENT '章节ID',
target VARCHAR(200) DEFAULT NULL COMMENT '目标对象',
extra_data JSON DEFAULT NULL COMMENT '额外数据',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_user_id (user_id),
INDEX idx_action (action),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为轨迹表'
`)
results.push('✅ 用户行为轨迹表创建成功')
} catch (e: any) {
if (e.code === 'ER_TABLE_EXISTS_ERROR') {
results.push(' 用户行为轨迹表已存在')
} else {
results.push('⚠️ 用户行为轨迹表创建失败: ' + e.message)
}
}
}
// 存客宝同步记录表
if (!migration || migration === 'ckb_sync_logs') {
try {
await query(`
CREATE TABLE IF NOT EXISTS ckb_sync_logs (
id VARCHAR(50) PRIMARY KEY,
user_id VARCHAR(100) NOT NULL COMMENT '用户ID',
phone VARCHAR(20) NOT NULL COMMENT '手机号',
action VARCHAR(50) NOT NULL COMMENT '同步动作: pull/push/full',
status VARCHAR(20) NOT NULL COMMENT '状态: success/failed',
request_data JSON DEFAULT NULL COMMENT '请求数据',
response_data JSON DEFAULT NULL COMMENT '响应数据',
error_msg TEXT DEFAULT NULL COMMENT '错误信息',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_user_id (user_id),
INDEX idx_phone (phone),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存客宝同步日志表'
`)
results.push('✅ 存客宝同步日志表创建成功')
} catch (e: any) {
if (e.code === 'ER_TABLE_EXISTS_ERROR') {
results.push(' 存客宝同步日志表已存在')
} else {
results.push('⚠️ 存客宝同步日志表创建失败: ' + e.message)
}
}
}
// VIP会员字段
if (!migration || migration === 'vip_fields') {
const vipFields = [
{ name: 'is_vip', def: "BOOLEAN DEFAULT FALSE COMMENT 'VIP会员'" },
{ name: 'vip_expire_date', def: "TIMESTAMP NULL COMMENT 'VIP到期时间'" },
{ name: 'vip_name', def: "VARCHAR(100) COMMENT '会员真实姓名'" },
{ name: 'vip_project', def: "VARCHAR(200) COMMENT '会员项目名称'" },
{ name: 'vip_contact', def: "VARCHAR(100) COMMENT '会员联系方式'" },
{ name: 'vip_avatar', def: "VARCHAR(500) COMMENT '会员展示头像'" },
{ name: 'vip_bio', def: "VARCHAR(500) COMMENT '会员简介'" },
]
let addedCount = 0
let existCount = 0
for (const field of vipFields) {
try {
await query(`SELECT ${field.name} FROM users LIMIT 1`)
existCount++
} catch {
try {
await query(`ALTER TABLE users ADD COLUMN ${field.name} ${field.def}`)
addedCount++
} catch (e: any) {
if (e.code !== 'ER_DUP_FIELDNAME') {
results.push(`⚠️ 添加VIP字段 ${field.name} 失败: ${e.message}`)
}
}
}
}
// 扩展 orders.product_type 支持 vip
try {
await query(`ALTER TABLE orders MODIFY COLUMN product_type ENUM('section', 'fullbook', 'match', 'vip') NOT NULL`)
results.push('✅ orders.product_type 已支持 vip')
} catch (e: any) {
results.push(' orders.product_type 更新跳过: ' + e.message)
}
if (addedCount > 0) results.push(`✅ VIP字段新增 ${addedCount}`)
if (existCount > 0) results.push(` VIP字段已有 ${existCount} 个存在`)
}
// 用户标签定义表
if (!migration || migration === 'user_tag_definitions') {
try {
await query(`
CREATE TABLE IF NOT EXISTS user_tag_definitions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE COMMENT '标签名称',
category VARCHAR(50) NOT NULL COMMENT '标签分类: system/ckb/behavior/source',
color VARCHAR(20) DEFAULT '#38bdac' COMMENT '标签颜色',
description VARCHAR(200) DEFAULT NULL COMMENT '标签描述',
is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_category (category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户标签定义表'
`)
// 插入默认标签
await query(`
INSERT IGNORE INTO user_tag_definitions (name, category, color, description) VALUES
('已购全书', 'system', '#22c55e', '购买了完整书籍'),
('VIP用户', 'system', '#eab308', 'VIP会员'),
('活跃用户', 'behavior', '#38bdac', '最近7天有访问'),
('高价值用户', 'behavior', '#f59e0b', '消费超过100元'),
('推广达人', 'behavior', '#8b5cf6', '成功推荐5人以上'),
('微信用户', 'source', '#07c160', '通过微信授权登录'),
('手动创建', 'source', '#6b7280', '后台手动创建')
`)
results.push('✅ 用户标签定义表创建成功')
} catch (e: any) {
if (e.code === 'ER_TABLE_EXISTS_ERROR') {
results.push(' 用户标签定义表已存在')
} else {
results.push('⚠️ 用户标签定义表创建失败: ' + e.message)
}
}
}
return NextResponse.json({
success: true,
results,
message: '数据库迁移完成'
})
} catch (error) {
console.error('[DB Migrate] Error:', error)
return NextResponse.json({
success: false,
error: '数据库迁移失败: ' + (error as Error).message
}, { status: 500 })
}
}
/**
* GET - 获取迁移状态
*/
export async function GET() {
try {
const tables: Record<string, boolean> = {}
// 检查各表是否存在
const checkTables = ['user_tracks', 'ckb_sync_logs', 'user_tag_definitions']
for (const table of checkTables) {
try {
await query(`SELECT 1 FROM ${table} LIMIT 1`)
tables[table] = true
} catch {
tables[table] = false
}
}
// 检查用户表字段
const userFields: Record<string, boolean> = {}
const checkFields = ['ckb_user_id', 'ckb_synced_at', 'ckb_tags', 'tags', 'merged_tags', 'is_vip', 'vip_expire_date', 'vip_name']
for (const field of checkFields) {
try {
await query(`SELECT ${field} FROM users LIMIT 1`)
userFields[field] = true
} catch {
userFields[field] = false
}
}
return NextResponse.json({
success: true,
status: {
tables,
userFields,
allReady: Object.values(tables).every(v => v) && Object.values(userFields).every(v => v)
}
})
} catch (error) {
console.error('[DB Migrate] GET Error:', error)
return NextResponse.json({
success: false,
error: '获取迁移状态失败'
}, { status: 500 })
}
}