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:
9
app/api/admin/logout/route.ts
Normal file
9
app/api/admin/logout/route.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getAdminCookieName, getAdminCookieOptions } from '@/lib/admin-auth'
|
||||
|
||||
export async function POST(_req: NextRequest) {
|
||||
const res = NextResponse.json({ success: true })
|
||||
const opts = getAdminCookieOptions()
|
||||
res.cookies.set(getAdminCookieName(), '', { ...opts, maxAge: 0 })
|
||||
return res
|
||||
}
|
||||
72
app/api/auth/login/route.ts
Normal file
72
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Web 端登录:手机号 + 密码
|
||||
* POST { phone, password } -> 校验后返回用户信息(不含密码)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { verifyPassword } from '@/lib/password'
|
||||
|
||||
function mapRowToUser(r: any) {
|
||||
return {
|
||||
id: r.id,
|
||||
phone: r.phone || '',
|
||||
nickname: r.nickname || '',
|
||||
isAdmin: !!r.is_admin,
|
||||
purchasedSections: Array.isArray(r.purchased_sections)
|
||||
? r.purchased_sections
|
||||
: (r.purchased_sections ? JSON.parse(String(r.purchased_sections)) : []) || [],
|
||||
hasFullBook: !!r.has_full_book,
|
||||
referralCode: r.referral_code || '',
|
||||
earnings: parseFloat(String(r.earnings || 0)),
|
||||
pendingEarnings: parseFloat(String(r.pending_earnings || 0)),
|
||||
withdrawnEarnings: parseFloat(String(r.withdrawn_earnings || 0)),
|
||||
referralCount: Number(r.referral_count) || 0,
|
||||
createdAt: r.created_at || '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { phone, password } = body
|
||||
|
||||
if (!phone || !password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入手机号和密码' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, phone, nickname, password, is_admin, has_full_book, referral_code, earnings, pending_earnings, withdrawn_earnings, referral_count, purchased_sections, created_at FROM users WHERE phone = ?',
|
||||
[String(phone).trim()]
|
||||
) as any[]
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户不存在或密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const row = rows[0]
|
||||
const storedPassword = row.password == null ? '' : String(row.password)
|
||||
|
||||
if (!verifyPassword(String(password), storedPassword)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const user = mapRowToUser(row)
|
||||
return NextResponse.json({ success: true, user })
|
||||
} catch (e) {
|
||||
console.error('[Auth Login] error:', e)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '登录失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
54
app/api/auth/reset-password/route.ts
Normal file
54
app/api/auth/reset-password/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 忘记密码 / 重置密码(Web 端)
|
||||
* POST { phone, newPassword } -> 按手机号更新密码(无验证码版本,适合内测/内部使用)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { hashPassword } from '@/lib/password'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { phone, newPassword } = body
|
||||
|
||||
if (!phone || !newPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入手机号和新密码' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const trimmedPhone = String(phone).trim()
|
||||
const trimmedPassword = String(newPassword).trim()
|
||||
|
||||
if (trimmedPassword.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '密码至少 6 位' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const rows = await query('SELECT id FROM users WHERE phone = ?', [trimmedPhone]) as any[]
|
||||
if (!rows || rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该手机号未注册' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const hashed = hashPassword(trimmedPassword)
|
||||
await query('UPDATE users SET password = ?, updated_at = NOW() WHERE phone = ?', [
|
||||
hashed,
|
||||
trimmedPhone,
|
||||
])
|
||||
|
||||
return NextResponse.json({ success: true, message: '密码已重置,请使用新密码登录' })
|
||||
} catch (e) {
|
||||
console.error('[Auth ResetPassword] error:', e)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '重置失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
97
app/api/content/upload/route.ts
Normal file
97
app/api/content/upload/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 内容上传 API
|
||||
* 供科室/Skill 直接上传单篇文章到书籍内容,写入 chapters 表
|
||||
* 字段:标题、定价、内容、格式、插入内容中的图片(URL 列表)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
function slug(id: string): string {
|
||||
return id.replace(/\s+/g, '-').replace(/[^\w\u4e00-\u9fa5-]/g, '').slice(0, 30) || 'section'
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
title,
|
||||
price = 1,
|
||||
content = '',
|
||||
format = 'markdown',
|
||||
images = [],
|
||||
partId = 'part-1',
|
||||
partTitle = '真实的人',
|
||||
chapterId = 'chapter-1',
|
||||
chapterTitle = '未分类',
|
||||
isFree = false,
|
||||
sectionId
|
||||
} = body
|
||||
|
||||
if (!title || typeof title !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '标题 title 不能为空' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 若内容中含占位符 {{image_0}} {{image_1}},用 images 数组替换
|
||||
let finalContent = typeof content === 'string' ? content : ''
|
||||
if (Array.isArray(images) && images.length > 0) {
|
||||
images.forEach((url: string, i: number) => {
|
||||
finalContent = finalContent.replace(
|
||||
new RegExp(`\\{\\{image_${i}\\}\\}`, 'g'),
|
||||
url.startsWith('http') ? `` : url
|
||||
)
|
||||
})
|
||||
}
|
||||
// 未替换的占位符去掉
|
||||
finalContent = finalContent.replace(/\{\{image_\d+\}\}/g, '')
|
||||
|
||||
const wordCount = (finalContent || '').length
|
||||
const id = sectionId || `upload.${slug(title)}.${Date.now()}`
|
||||
|
||||
await query(
|
||||
`INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, is_free, price, sort_order, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 9999, 'published')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
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,
|
||||
partId,
|
||||
partTitle,
|
||||
chapterId,
|
||||
chapterTitle,
|
||||
title,
|
||||
finalContent,
|
||||
wordCount,
|
||||
!!isFree,
|
||||
Number(price) || 1
|
||||
]
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id,
|
||||
message: '内容已上传并写入 chapters 表',
|
||||
title,
|
||||
price: Number(price) || 1,
|
||||
isFree: !!isFree,
|
||||
wordCount
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Content Upload]', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: '上传失败: ' + (error as Error).message
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
67
app/api/vip/members/route.ts
Normal file
67
app/api/vip/members/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
77
app/api/vip/profile/route.ts
Normal file
77
app/api/vip/profile/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
57
app/api/vip/purchase/route.ts
Normal file
57
app/api/vip/purchase/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
73
app/api/vip/status/route.ts
Normal file
73
app/api/vip/status/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user