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>
This commit is contained in:
卡若
2026-02-23 14:07:41 +08:00
parent e91a5d9f7a
commit afc2376e96
49 changed files with 1898 additions and 561 deletions

View File

@@ -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 })
}
}

View File

@@ -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 {

View File

@@ -0,0 +1,67 @@
/**
* VIP会员列表 - 用于「创业老板排行」展示
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
export async function GET(request: NextRequest) {
const limit = parseInt(new URL(request.url).searchParams.get('limit') || '20')
const memberId = new URL(request.url).searchParams.get('id')
try {
// 查询单个会员详情
if (memberId) {
const rows = await query(
`SELECT id, nickname, avatar, vip_name, vip_project, vip_contact, vip_avatar, vip_bio,
is_vip, vip_expire_date, created_at
FROM users WHERE id = ? AND is_vip = TRUE AND vip_expire_date > NOW()`,
[memberId]
) as any[]
if (!rows.length) {
return NextResponse.json({ success: false, error: '会员不存在或已过期' }, { status: 404 })
}
const m = rows[0]
return NextResponse.json({
success: true,
data: {
id: m.id,
name: m.vip_name || m.nickname || '创业者',
avatar: m.vip_avatar || m.avatar || '',
project: m.vip_project || '',
contact: m.vip_contact || '',
bio: m.vip_bio || '',
joinDate: m.created_at
}
})
}
// 获取VIP会员列表已填写资料的优先排前面
const members = await query(
`SELECT id, nickname, avatar, vip_name, vip_project, vip_avatar, vip_bio
FROM users
WHERE is_vip = TRUE AND vip_expire_date > NOW()
ORDER BY
CASE WHEN vip_name IS NOT NULL AND vip_name != '' THEN 0 ELSE 1 END,
vip_expire_date DESC
LIMIT ?`,
[limit]
) as any[]
return NextResponse.json({
success: true,
data: members.map((m: any) => ({
id: m.id,
name: m.vip_name || m.nickname || '创业者',
avatar: m.vip_avatar || m.avatar || '',
project: m.vip_project || '',
bio: m.vip_bio || ''
})),
total: members.length
})
} catch (error) {
console.error('[VIP Members]', error)
return NextResponse.json({ success: false, error: '查询失败', data: [], total: 0 })
}
}

View File

@@ -0,0 +1,77 @@
/**
* VIP会员资料填写/更新
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
export async function POST(request: NextRequest) {
try {
const { userId, name, project, contact, avatar, bio } = await request.json()
if (!userId) {
return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 })
}
const users = await query('SELECT is_vip, vip_expire_date FROM users WHERE id = ?', [userId]) as any[]
if (!users.length) {
return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 })
}
const user = users[0]
if (!user.is_vip || !user.vip_expire_date || new Date(user.vip_expire_date) <= new Date()) {
return NextResponse.json({ success: false, error: '仅VIP会员可填写资料' }, { status: 403 })
}
const updates: string[] = []
const params: any[] = []
if (name !== undefined) { updates.push('vip_name = ?'); params.push(name) }
if (project !== undefined) { updates.push('vip_project = ?'); params.push(project) }
if (contact !== undefined) { updates.push('vip_contact = ?'); params.push(contact) }
if (avatar !== undefined) { updates.push('vip_avatar = ?'); params.push(avatar) }
if (bio !== undefined) { updates.push('vip_bio = ?'); params.push(bio) }
if (!updates.length) {
return NextResponse.json({ success: false, error: '无更新内容' }, { status: 400 })
}
params.push(userId)
await query(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, params)
return NextResponse.json({ success: true, message: '资料已更新' })
} catch (error) {
console.error('[VIP Profile]', error)
return NextResponse.json({ success: false, error: '更新失败' }, { status: 500 })
}
}
export async function GET(request: NextRequest) {
const userId = new URL(request.url).searchParams.get('userId')
if (!userId) {
return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 })
}
try {
const rows = await query(
'SELECT vip_name, vip_project, vip_contact, vip_avatar, vip_bio FROM users WHERE id = ?',
[userId]
) as any[]
if (!rows.length) {
return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 })
}
return NextResponse.json({
success: true,
data: {
name: rows[0].vip_name || '',
project: rows[0].vip_project || '',
contact: rows[0].vip_contact || '',
avatar: rows[0].vip_avatar || '',
bio: rows[0].vip_bio || ''
}
})
} catch (error) {
console.error('[VIP Profile GET]', error)
return NextResponse.json({ success: false, error: '查询失败' }, { status: 500 })
}
}

View File

@@ -0,0 +1,57 @@
/**
* VIP会员购买 - 创建VIP订单
*/
import { NextRequest, NextResponse } from 'next/server'
import { query, getConfig } from '@/lib/db'
export async function POST(request: NextRequest) {
try {
const { userId } = await request.json()
if (!userId) {
return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 })
}
const users = await query(
'SELECT id, open_id, is_vip, vip_expire_date FROM users WHERE id = ?',
[userId]
) as any[]
if (!users.length) {
return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 })
}
const user = users[0]
// 如果已经是VIP且未过期
if (user.is_vip && user.vip_expire_date && new Date(user.vip_expire_date) > new Date()) {
return NextResponse.json({ success: false, error: '当前已是VIP会员' }, { status: 400 })
}
let vipPrice = 1980
try {
const config = await getConfig('vip_price')
if (config) vipPrice = Number(config) || 1980
} catch { /* 默认 */ }
const orderId = 'vip_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6)
const orderSn = 'VIP' + Date.now() + Math.floor(Math.random() * 1000)
await query(
`INSERT INTO orders (id, order_sn, user_id, open_id, product_type, amount, description, status)
VALUES (?, ?, ?, ?, 'vip', ?, 'VIP年度会员', 'created')`,
[orderId, orderSn, userId, user.open_id || '', vipPrice]
)
return NextResponse.json({
success: true,
data: {
orderId,
orderSn,
amount: vipPrice,
productType: 'vip',
description: 'VIP年度会员365天'
}
})
} catch (error) {
console.error('[VIP Purchase]', error)
return NextResponse.json({ success: false, error: '创建订单失败' }, { status: 500 })
}
}

View File

@@ -0,0 +1,73 @@
/**
* VIP会员状态查询
*/
import { NextRequest, NextResponse } from 'next/server'
import { query, getConfig } from '@/lib/db'
export async function GET(request: NextRequest) {
const userId = new URL(request.url).searchParams.get('userId')
if (!userId) {
return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 })
}
try {
const rows = await query(
`SELECT is_vip, vip_expire_date, vip_name, vip_project, vip_contact, vip_avatar, vip_bio,
has_full_book, nickname, avatar
FROM users WHERE id = ?`,
[userId]
) as any[]
if (!rows.length) {
return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 })
}
const user = rows[0]
const now = new Date()
const isVip = user.is_vip && user.vip_expire_date && new Date(user.vip_expire_date) > now
// 若过期则自动标记
if (user.is_vip && !isVip) {
await query('UPDATE users SET is_vip = FALSE WHERE id = ?', [userId]).catch(() => {})
}
let vipPrice = 1980
let vipRights: string[] = []
try {
const priceConfig = await getConfig('vip_price')
if (priceConfig) vipPrice = Number(priceConfig) || 1980
const rightsConfig = await getConfig('vip_rights')
if (rightsConfig) vipRights = Array.isArray(rightsConfig) ? rightsConfig : JSON.parse(rightsConfig)
} catch { /* 使用默认 */ }
if (!vipRights.length) {
vipRights = [
'解锁全部章节内容365天',
'匹配所有创业伙伴',
'创业老板排行榜展示',
'专属VIP标识'
]
}
return NextResponse.json({
success: true,
data: {
isVip,
expireDate: user.vip_expire_date,
daysRemaining: isVip ? Math.ceil((new Date(user.vip_expire_date).getTime() - now.getTime()) / 86400000) : 0,
profile: {
name: user.vip_name || '',
project: user.vip_project || '',
contact: user.vip_contact || '',
avatar: user.vip_avatar || user.avatar || '',
bio: user.vip_bio || ''
},
price: vipPrice,
rights: vipRights
}
})
} catch (error) {
console.error('[VIP Status]', error)
return NextResponse.json({ success: false, error: '查询失败' }, { status: 500 })
}
}