Files
soul/app/api/db/migrate/route.ts

220 lines
7.9 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)
}
}
}
// 用户标签定义表
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']
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 })
}
}