Merge branch 'yongpxu-soul' of https://github.com/fnvtk/Mycontent into yongpxu-soul
# Conflicts: # .cursorrules resolved by yongpxu-soul version # miniprogram/app.json resolved by yongpxu-soul version # miniprogram/pages/index/index.js resolved by yongpxu-soul version # miniprogram/pages/index/index.wxml resolved by yongpxu-soul version # miniprogram/pages/my/my.js resolved by yongpxu-soul version # miniprogram/pages/my/my.wxml resolved by yongpxu-soul version # miniprogram/pages/my/my.wxss resolved by yongpxu-soul version # miniprogram/pages/settings/settings.js resolved by yongpxu-soul version # miniprogram/pages/settings/settings.wxml resolved by yongpxu-soul version # miniprogram/上传小程序.py resolved by yongpxu-soul version # next-project/app/admin/error.tsx resolved by yongpxu-soul version # next-project/app/admin/page.tsx resolved by yongpxu-soul version # next-project/app/api/book/all-chapters/route.ts resolved by yongpxu-soul version # next-project/lib/admin-auth.ts resolved by yongpxu-soul version # next-project/lib/db.ts resolved by yongpxu-soul version # next-project/lib/password.ts resolved by yongpxu-soul version # next-project/package.json resolved by yongpxu-soul version # next-project/scripts/.env.feishu.example resolved by yongpxu-soul version # next-project/scripts/fix_souladmin_login.sh resolved by yongpxu-soul version # next-project/scripts/send_poster_to_feishu.py resolved by yongpxu-soul version # next-project/scripts/upload_soul_article.sh resolved by yongpxu-soul version # soul-admin/dist/assets/index-CbOmKBRd.js resolved by yongpxu-soul version # soul-admin/dist/index.html resolved by yongpxu-soul version # 开发文档/10、项目管理/产研团队 第21场 20260129 许永平.txt resolved by yongpxu-soul version # 开发文档/5、接口/配置清单-完整版.md resolved by yongpxu-soul version # 开发文档/服务器管理/references/端口配置表.md resolved by yongpxu-soul version
This commit is contained in:
@@ -1,74 +1,86 @@
|
||||
/**
|
||||
* 热门章节API
|
||||
* 返回点击量最高的章节
|
||||
* 按阅读量(user_tracks view_chapter)排序
|
||||
*/
|
||||
import { NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
const DEFAULT_CHAPTERS = [
|
||||
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人', views: 0 },
|
||||
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', tag: '热门', tagClass: 'tag-pink', part: '真实的赚钱', views: 0 },
|
||||
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业', views: 0 },
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱', views: 0 },
|
||||
{ id: '9.13', title: 'AI工具推广:一个隐藏的高利润赛道', tag: '最新', tagClass: 'tag-green', part: '真实的赚钱', views: 0 },
|
||||
]
|
||||
|
||||
const SECTION_INFO: Record<string, any> = {
|
||||
'1.1': { title: '荷包:电动车出租的被动收入模式', part: '真实的人', tag: '免费', tagClass: 'tag-free' },
|
||||
'1.2': { title: '老墨:资源整合高手的社交方法', part: '真实的人', tag: '推荐', tagClass: 'tag-purple' },
|
||||
'2.1': { title: '电商的底层逻辑', part: '真实的行业', tag: '推荐', tagClass: 'tag-purple' },
|
||||
'3.1': { title: '3000万流水如何跑出来', part: '真实的行业', tag: '热门', tagClass: 'tag-pink' },
|
||||
'4.1': { title: '我的第一次创业失败', part: '真实的错误', tag: '热门', tagClass: 'tag-pink' },
|
||||
'5.1': { title: '未来职业的三个方向', part: '真实的社会', tag: '推荐', tagClass: 'tag-purple' },
|
||||
'8.1': { title: '流量杠杆:抖音、Soul、飞书', part: '真实的赚钱', tag: '推荐', tagClass: 'tag-purple' },
|
||||
'9.12': { title: '美业整合:一个人的公司如何月入十万', part: '真实的赚钱', tag: '热门', tagClass: 'tag-pink' },
|
||||
'9.13': { title: 'AI工具推广:一个隐藏的高利润赛道', part: '真实的赚钱', tag: '最新', tagClass: 'tag-green' },
|
||||
'9.14': { title: '大健康私域:一个月150万的70后', part: '真实的赚钱', tag: '热门', tagClass: 'tag-pink' },
|
||||
'9.15': { title: '本地同城运营拿150万投资', part: '真实的赚钱', tag: '热门', tagClass: 'tag-pink' },
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// 从数据库查询点击量高的章节(如果有统计表)
|
||||
let hotChapters = []
|
||||
|
||||
let hotChapters: any[] = []
|
||||
|
||||
try {
|
||||
// 尝试从订单表统计购买量高的章节
|
||||
// 按 user_tracks 的 view_chapter 阅读量排序
|
||||
const rows = await query(`
|
||||
SELECT
|
||||
section_id as id,
|
||||
COUNT(*) as purchase_count
|
||||
FROM orders
|
||||
WHERE status = 'completed' AND section_id IS NOT NULL
|
||||
GROUP BY section_id
|
||||
ORDER BY purchase_count DESC
|
||||
SELECT chapter_id as id, COUNT(*) as view_count
|
||||
FROM user_tracks
|
||||
WHERE action = 'view_chapter' AND chapter_id IS NOT NULL AND chapter_id != ''
|
||||
GROUP BY chapter_id
|
||||
ORDER BY view_count DESC
|
||||
LIMIT 10
|
||||
`) as any[]
|
||||
|
||||
if (rows && rows.length > 0) {
|
||||
// 补充章节信息
|
||||
const sectionInfo: Record<string, any> = {
|
||||
'1.1': { title: '荷包:电动车出租的被动收入模式', part: '真实的人', tag: '免费' },
|
||||
'9.12': { title: '美业整合:一个人的公司如何月入十万', part: '真实的赚钱', tag: '热门' },
|
||||
'3.1': { title: '3000万流水如何跑出来', part: '真实的行业', tag: '热门' },
|
||||
'8.1': { title: '流量杠杆:抖音、Soul、飞书', part: '真实的赚钱', tag: '推荐' },
|
||||
'9.13': { title: 'AI工具推广:一个隐藏的高利润赛道', part: '真实的赚钱', tag: '最新' },
|
||||
'9.14': { title: '大健康私域:一个月150万的70后', part: '真实的赚钱', tag: '热门' },
|
||||
'1.2': { title: '老墨:资源整合高手的社交方法', part: '真实的人', tag: '推荐' },
|
||||
'2.1': { title: '电商的底层逻辑', part: '真实的行业', tag: '推荐' },
|
||||
'4.1': { title: '我的第一次创业失败', part: '真实的错误', tag: '热门' },
|
||||
'5.1': { title: '未来职业的三个方向', part: '真实的社会', tag: '推荐' }
|
||||
}
|
||||
|
||||
hotChapters = rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
...(sectionInfo[row.id] || { title: `章节${row.id}`, part: '', tag: '热门' }),
|
||||
purchaseCount: row.purchase_count
|
||||
}))
|
||||
|
||||
if (rows?.length) {
|
||||
hotChapters = rows.map((row: any) => {
|
||||
const info = SECTION_INFO[row.id] || {}
|
||||
return {
|
||||
id: row.id,
|
||||
title: info.title || `章节 ${row.id}`,
|
||||
part: info.part || '',
|
||||
tag: info.tag || '热门',
|
||||
tagClass: info.tagClass || 'tag-pink',
|
||||
views: row.view_count
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Hot] 数据库查询失败,使用默认数据')
|
||||
console.log('[Hot] user_tracks查询失败,尝试订单统计')
|
||||
// 降级:从订单表统计
|
||||
try {
|
||||
const rows = await query(`
|
||||
SELECT product_id as id, COUNT(*) as purchase_count
|
||||
FROM orders WHERE status = 'paid' AND product_id IS NOT NULL
|
||||
GROUP BY product_id ORDER BY purchase_count DESC LIMIT 10
|
||||
`) as any[]
|
||||
if (rows?.length) {
|
||||
hotChapters = rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
...(SECTION_INFO[row.id] || { title: `章节 ${row.id}`, part: '', tag: '热门', tagClass: 'tag-pink' }),
|
||||
views: row.purchase_count
|
||||
}))
|
||||
}
|
||||
} catch { /* 使用默认 */ }
|
||||
}
|
||||
|
||||
// 如果没有数据,返回默认热门章节
|
||||
if (hotChapters.length === 0) {
|
||||
hotChapters = [
|
||||
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', part: '真实的人' },
|
||||
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', tag: '热门', part: '真实的赚钱' },
|
||||
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', part: '真实的行业' },
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', part: '真实的赚钱' },
|
||||
{ id: '9.13', title: 'AI工具推广:一个隐藏的高利润赛道', tag: '最新', part: '真实的赚钱' }
|
||||
]
|
||||
|
||||
if (!hotChapters.length) {
|
||||
hotChapters = DEFAULT_CHAPTERS
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
chapters: hotChapters
|
||||
})
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, chapters: hotChapters })
|
||||
} catch (error) {
|
||||
console.error('[Hot] Error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
chapters: []
|
||||
})
|
||||
return NextResponse.json({ success: true, chapters: DEFAULT_CHAPTERS })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,109 @@
|
||||
// app/api/book/latest-chapters/route.ts
|
||||
// 获取最新章节列表
|
||||
// 获取最新章节:有2日内更新则取最新3章,否则随机取免费章节
|
||||
// 排除序言、尾声、附录,只推荐正文章节
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getBookStructure } from '@/lib/book-file-system'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000
|
||||
|
||||
/** 是否应排除(序言、尾声、附录等特殊章节) */
|
||||
function isExcludedChapter(id: string, partTitle: string): boolean {
|
||||
const lowerId = String(id || '').toLowerCase()
|
||||
if (lowerId === 'preface' || lowerId === 'epilogue') return true
|
||||
if (lowerId.startsWith('appendix-') || lowerId.startsWith('appendix_')) return true
|
||||
const pt = String(partTitle || '')
|
||||
if (/序言|尾声/.test(pt)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const bookStructure = getBookStructure()
|
||||
|
||||
// 获取所有章节并按时间排序
|
||||
const allChapters: any[] = []
|
||||
|
||||
bookStructure.forEach((part: any) => {
|
||||
part.chapters.forEach((chapter: any) => {
|
||||
allChapters.push({
|
||||
id: chapter.slug,
|
||||
title: chapter.title,
|
||||
part: part.title,
|
||||
words: Math.floor(Math.random() * 3000) + 1500, // 模拟字数
|
||||
updateTime: getRelativeTime(new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000)),
|
||||
readTime: Math.ceil((Math.random() * 3000 + 1500) / 300)
|
||||
})
|
||||
let allChapters: Array<{
|
||||
id: string
|
||||
title: string
|
||||
part: string
|
||||
isFree: boolean
|
||||
price: number
|
||||
updatedAt: Date | string | null
|
||||
createdAt: Date | string | null
|
||||
}> = []
|
||||
|
||||
try {
|
||||
const dbRows = (await query(`
|
||||
SELECT id, part_title, section_title, is_free, price, created_at, updated_at
|
||||
FROM chapters
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
`)) as any[]
|
||||
|
||||
if (dbRows?.length > 0) {
|
||||
allChapters = dbRows
|
||||
.map((row: any) => ({
|
||||
id: row.id,
|
||||
title: row.section_title || row.title || '',
|
||||
part: row.part_title || '真实的行业',
|
||||
isFree: !!row.is_free,
|
||||
price: row.price || 0,
|
||||
updatedAt: row.updated_at || row.created_at,
|
||||
createdAt: row.created_at
|
||||
}))
|
||||
.filter((c) => !isExcludedChapter(c.id, c.part))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[latest-chapters] 数据库读取失败:', (e as Error).message)
|
||||
}
|
||||
|
||||
if (allChapters.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
banner: { id: '1.1', title: '荷包:电动车出租的被动收入模式', part: '真实的人' },
|
||||
label: '为你推荐',
|
||||
chapters: [],
|
||||
hasNewUpdates: false
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const sorted = [...allChapters].sort((a, b) => {
|
||||
const ta = a.updatedAt ? new Date(a.updatedAt).getTime() : 0
|
||||
const tb = b.updatedAt ? new Date(b.updatedAt).getTime() : 0
|
||||
return tb - ta
|
||||
})
|
||||
|
||||
// 取最新的3章
|
||||
const latestChapters = allChapters.slice(0, 3)
|
||||
const mostRecentTime = sorted[0]?.updatedAt ? new Date(sorted[0].updatedAt).getTime() : 0
|
||||
const hasNewUpdates = now - mostRecentTime < TWO_DAYS_MS
|
||||
|
||||
let banner: { id: string; title: string; part: string }
|
||||
let label: string
|
||||
let chapters: typeof allChapters
|
||||
|
||||
if (hasNewUpdates && sorted.length > 0) {
|
||||
chapters = sorted.slice(0, 3)
|
||||
banner = { id: chapters[0].id, title: chapters[0].title, part: chapters[0].part }
|
||||
label = '最新更新'
|
||||
} else {
|
||||
const freeChapters = allChapters.filter((c) => c.isFree || c.price === 0)
|
||||
const candidates = freeChapters.length > 0 ? freeChapters : allChapters
|
||||
const shuffled = [...candidates].sort(() => Math.random() - 0.5)
|
||||
chapters = shuffled.slice(0, 3)
|
||||
banner = chapters[0]
|
||||
? { id: chapters[0].id, title: chapters[0].title, part: chapters[0].part }
|
||||
: { id: allChapters[0].id, title: allChapters[0].title, part: allChapters[0].part }
|
||||
label = '为你推荐'
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
chapters: latestChapters,
|
||||
total: allChapters.length
|
||||
banner,
|
||||
label,
|
||||
chapters: chapters.map((c) => ({ id: c.id, title: c.title, part: c.part, isFree: c.isFree })),
|
||||
hasNewUpdates
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取章节失败:', error)
|
||||
console.error('[latest-chapters] Error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: '获取章节失败' },
|
||||
{ success: false, error: '获取失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取相对时间
|
||||
function getRelativeTime(date: Date): string {
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days === 0) return '今天'
|
||||
if (days === 1) return '昨天'
|
||||
if (days < 7) return `${days}天前`
|
||||
if (days < 30) return `${Math.floor(days / 7)}周前`
|
||||
return `${Math.floor(days / 30)}个月前`
|
||||
}
|
||||
|
||||
@@ -146,13 +146,40 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 列出所有章节(不含内容)
|
||||
// 优先从数据库读取,确保新建章节能立即显示
|
||||
if (action === 'list') {
|
||||
const sectionsFromDb = new Map<string, any>()
|
||||
try {
|
||||
const rows = await query(`
|
||||
SELECT id, part_id, part_title, chapter_id, chapter_title, section_title,
|
||||
price, is_free, content
|
||||
FROM chapters ORDER BY part_id, chapter_id, id
|
||||
`) as any[]
|
||||
if (rows && rows.length > 0) {
|
||||
for (const r of rows) {
|
||||
sectionsFromDb.set(r.id, {
|
||||
id: r.id,
|
||||
title: r.section_title || '',
|
||||
price: r.price ?? 1,
|
||||
isFree: !!r.is_free,
|
||||
partId: r.part_id || 'part-1',
|
||||
partTitle: r.part_title || '',
|
||||
chapterId: r.chapter_id || 'chapter-1',
|
||||
chapterTitle: r.chapter_title || '',
|
||||
filePath: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Book API] list 从数据库读取失败,回退到 bookData:', (e as Error).message)
|
||||
}
|
||||
// 合并:以数据库为准,数据库没有的用 bookData 补
|
||||
const sections: any[] = []
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
sections.push({
|
||||
const dbRow = sectionsFromDb.get(section.id)
|
||||
sections.push(dbRow || {
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
@@ -163,14 +190,25 @@ export async function GET(request: NextRequest) {
|
||||
chapterTitle: chapter.title,
|
||||
filePath: section.filePath
|
||||
})
|
||||
sectionsFromDb.delete(section.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库有但 bookData 没有的(新建章节)
|
||||
for (const [, v] of sectionsFromDb) {
|
||||
sections.push(v)
|
||||
}
|
||||
// 按 id 去重,避免数据库重复或合并逻辑导致同一文章出现多次
|
||||
const seen = new Set<string>()
|
||||
const deduped = sections.filter((s) => {
|
||||
if (seen.has(s.id)) return false
|
||||
seen.add(s.id)
|
||||
return true
|
||||
})
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
sections,
|
||||
total: sections.length
|
||||
sections: deduped,
|
||||
total: deduped.length
|
||||
})
|
||||
}
|
||||
|
||||
@@ -324,7 +362,7 @@ export async function POST(request: NextRequest) {
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id, title, content, price, saveToFile = true } = body
|
||||
const { id, title, content, price, saveToFile = true, partId, chapterId, partTitle, chapterTitle, isFree } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
@@ -334,28 +372,40 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
|
||||
const sectionInfo = getSectionInfo(id)
|
||||
const finalPartId = partId || sectionInfo?.partId || 'part-1'
|
||||
const finalPartTitle = partTitle || sectionInfo?.partTitle || '未分类'
|
||||
const finalChapterId = chapterId || sectionInfo?.chapterId || 'chapter-1'
|
||||
const finalChapterTitle = chapterTitle || sectionInfo?.chapterTitle || '未分类'
|
||||
const finalPrice = price ?? sectionInfo?.section?.price ?? 1
|
||||
const finalIsFree = isFree ?? sectionInfo?.section?.isFree ?? false
|
||||
|
||||
// 更新数据库
|
||||
// 更新数据库(含新建章节)
|
||||
try {
|
||||
await query(`
|
||||
INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, price, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'published')
|
||||
INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, is_free, price, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
part_id = VALUES(part_id),
|
||||
part_title = VALUES(part_title),
|
||||
chapter_id = VALUES(chapter_id),
|
||||
chapter_title = VALUES(chapter_title),
|
||||
section_title = VALUES(section_title),
|
||||
content = VALUES(content),
|
||||
word_count = VALUES(word_count),
|
||||
is_free = VALUES(is_free),
|
||||
price = VALUES(price),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, [
|
||||
id,
|
||||
sectionInfo?.partId || 'part-1',
|
||||
sectionInfo?.partTitle || '未分类',
|
||||
sectionInfo?.chapterId || 'chapter-1',
|
||||
sectionInfo?.chapterTitle || '未分类',
|
||||
title || sectionInfo?.section.title || '',
|
||||
finalPartId,
|
||||
finalPartTitle,
|
||||
finalChapterId,
|
||||
finalChapterTitle,
|
||||
title || sectionInfo?.section?.title || '',
|
||||
content || '',
|
||||
(content || '').length,
|
||||
price ?? sectionInfo?.section.price ?? 1
|
||||
finalIsFree,
|
||||
finalPrice
|
||||
])
|
||||
} catch (e) {
|
||||
console.error('[Book API] 更新数据库失败:', e)
|
||||
|
||||
@@ -115,6 +115,47 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -189,7 +230,7 @@ export async function GET() {
|
||||
|
||||
// 检查用户表字段
|
||||
const userFields: Record<string, boolean> = {}
|
||||
const checkFields = ['ckb_user_id', 'ckb_synced_at', 'ckb_tags', 'tags', 'merged_tags']
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user