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:
乘风
2026-02-24 09:42:34 +08:00
46 changed files with 3486 additions and 110 deletions

View 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
}

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

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

View 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') ? `![图${i + 1}](${url})` : 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 }
)
}
}

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