feat: 完善后台管理+搜索功能+分销系统
主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
This commit is contained in:
273
app/api/search/route.ts
Normal file
273
app/api/search/route.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 搜索API
|
||||
* 支持从数据库搜索标题和内容
|
||||
* 同时支持搜索匹配的人和事情(隐藏功能)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { bookData } from '@/lib/book-data'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* 从文件系统搜索章节
|
||||
*/
|
||||
function searchFromFiles(keyword: string): any[] {
|
||||
const results: any[] = []
|
||||
const lowerKeyword = keyword.toLowerCase()
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
// 搜索标题
|
||||
if (section.title.toLowerCase().includes(lowerKeyword)) {
|
||||
results.push({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
partTitle: part.title,
|
||||
chapterTitle: chapter.title,
|
||||
price: section.price,
|
||||
isFree: section.isFree,
|
||||
matchType: 'title',
|
||||
score: 10 // 标题匹配得分更高
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 搜索内容
|
||||
const filePath = path.join(process.cwd(), section.filePath)
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
if (content.toLowerCase().includes(lowerKeyword)) {
|
||||
// 提取匹配的上下文
|
||||
const lowerContent = content.toLowerCase()
|
||||
const matchIndex = lowerContent.indexOf(lowerKeyword)
|
||||
const start = Math.max(0, matchIndex - 50)
|
||||
const end = Math.min(content.length, matchIndex + keyword.length + 50)
|
||||
const snippet = content.substring(start, end)
|
||||
|
||||
results.push({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
partTitle: part.title,
|
||||
chapterTitle: chapter.title,
|
||||
price: section.price,
|
||||
isFree: section.isFree,
|
||||
matchType: 'content',
|
||||
snippet: (start > 0 ? '...' : '') + snippet + (end < content.length ? '...' : ''),
|
||||
score: 5 // 内容匹配得分较低
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略读取错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按得分排序
|
||||
return results.sort((a, b) => b.score - a.score)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库搜索章节
|
||||
*/
|
||||
async function searchFromDB(keyword: string): Promise<any[]> {
|
||||
try {
|
||||
const results = await query(`
|
||||
SELECT
|
||||
id,
|
||||
section_title as title,
|
||||
part_title as partTitle,
|
||||
chapter_title as chapterTitle,
|
||||
price,
|
||||
is_free as isFree,
|
||||
CASE
|
||||
WHEN section_title LIKE ? THEN 'title'
|
||||
ELSE 'content'
|
||||
END as matchType,
|
||||
CASE
|
||||
WHEN section_title LIKE ? THEN 10
|
||||
ELSE 5
|
||||
END as score,
|
||||
SUBSTRING(content,
|
||||
GREATEST(1, LOCATE(?, content) - 50),
|
||||
150
|
||||
) as snippet
|
||||
FROM chapters
|
||||
WHERE section_title LIKE ?
|
||||
OR content LIKE ?
|
||||
ORDER BY score DESC, id ASC
|
||||
LIMIT 50
|
||||
`, [
|
||||
`%${keyword}%`,
|
||||
`%${keyword}%`,
|
||||
keyword,
|
||||
`%${keyword}%`,
|
||||
`%${keyword}%`
|
||||
]) as any[]
|
||||
|
||||
return results
|
||||
} catch (e) {
|
||||
console.error('[Search API] 数据库搜索失败:', e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取文章中的人物信息(隐藏功能)
|
||||
* 用于"找伙伴"功能的智能匹配
|
||||
*/
|
||||
function extractPeopleFromContent(content: string): string[] {
|
||||
const people: string[] = []
|
||||
|
||||
// 匹配常见人名模式
|
||||
// 中文名:2-4个汉字
|
||||
const chineseNames = content.match(/[\u4e00-\u9fa5]{2,4}(?=:|:|说|的|告诉|表示)/g) || []
|
||||
// 英文名/昵称:带@或引号的名称
|
||||
const nicknames = content.match(/["'@]([^"'@\s]+)["']?/g) || []
|
||||
// 职位+名字模式
|
||||
const titleNames = content.match(/(?:老板|总|经理|创始人|合伙人|店长)[\u4e00-\u9fa5]{2,3}/g) || []
|
||||
|
||||
people.push(...chineseNames.slice(0, 10))
|
||||
people.push(...nicknames.map(n => n.replace(/["'@]/g, '')).slice(0, 5))
|
||||
people.push(...titleNames.slice(0, 5))
|
||||
|
||||
// 去重
|
||||
return [...new Set(people)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取文章中的关键事件/标签
|
||||
*/
|
||||
function extractKeywords(content: string): string[] {
|
||||
const keywords: string[] = []
|
||||
|
||||
// 行业关键词
|
||||
const industries = ['电商', '私域', '社群', '抖音', '直播', '餐饮', '美业', '健康', 'AI', '供应链', '金融', '拍卖', '游戏', '电竞']
|
||||
// 模式关键词
|
||||
const patterns = ['轻资产', '复购', '被动收入', '杠杆', '信息差', '流量', '分销', '代理', '加盟']
|
||||
// 金额模式
|
||||
const amounts = content.match(/(\d+)万/g) || []
|
||||
|
||||
for (const ind of industries) {
|
||||
if (content.includes(ind)) keywords.push(ind)
|
||||
}
|
||||
for (const pat of patterns) {
|
||||
if (content.includes(pat)) keywords.push(pat)
|
||||
}
|
||||
keywords.push(...amounts.slice(0, 5))
|
||||
|
||||
return [...new Set(keywords)]
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - 搜索
|
||||
* 参数:
|
||||
* - q: 搜索关键词
|
||||
* - type: 'all' | 'title' | 'content' | 'people' | 'keywords'
|
||||
* - source: 'db' | 'file' | 'auto' (默认auto)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const keyword = searchParams.get('q') || searchParams.get('keyword') || ''
|
||||
const type = searchParams.get('type') || 'all'
|
||||
const source = searchParams.get('source') || 'auto'
|
||||
|
||||
if (!keyword || keyword.length < 1) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请输入搜索关键词'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
let results: any[] = []
|
||||
|
||||
// 根据source选择搜索方式
|
||||
if (source === 'db') {
|
||||
results = await searchFromDB(keyword)
|
||||
} else if (source === 'file') {
|
||||
results = searchFromFiles(keyword)
|
||||
} else {
|
||||
// auto: 先尝试数据库,失败则使用文件
|
||||
results = await searchFromDB(keyword)
|
||||
if (results.length === 0) {
|
||||
results = searchFromFiles(keyword)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据type过滤
|
||||
if (type === 'title') {
|
||||
results = results.filter(r => r.matchType === 'title')
|
||||
} else if (type === 'content') {
|
||||
results = results.filter(r => r.matchType === 'content')
|
||||
}
|
||||
|
||||
// 如果搜索人物或关键词(隐藏功能)
|
||||
let people: string[] = []
|
||||
let keywords: string[] = []
|
||||
|
||||
if (type === 'people' || type === 'all') {
|
||||
// 从搜索结果的内容中提取人物
|
||||
for (const result of results.slice(0, 5)) {
|
||||
const filePath = path.join(process.cwd(), 'book')
|
||||
// 从bookData找到对应文件
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find(s => s.id === result.id)
|
||||
if (section) {
|
||||
const fullPath = path.join(process.cwd(), section.filePath)
|
||||
if (fs.existsSync(fullPath)) {
|
||||
const content = fs.readFileSync(fullPath, 'utf-8')
|
||||
people.push(...extractPeopleFromContent(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
people = [...new Set(people)].slice(0, 20)
|
||||
}
|
||||
|
||||
if (type === 'keywords' || type === 'all') {
|
||||
// 从搜索结果的内容中提取关键词
|
||||
for (const result of results.slice(0, 5)) {
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find(s => s.id === result.id)
|
||||
if (section) {
|
||||
const fullPath = path.join(process.cwd(), section.filePath)
|
||||
if (fs.existsSync(fullPath)) {
|
||||
const content = fs.readFileSync(fullPath, 'utf-8')
|
||||
keywords.push(...extractKeywords(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keywords = [...new Set(keywords)].slice(0, 20)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
keyword,
|
||||
total: results.length,
|
||||
results: results.slice(0, 20), // 限制返回数量
|
||||
// 隐藏功能数据
|
||||
people: type === 'people' || type === 'all' ? people : undefined,
|
||||
keywords: type === 'keywords' || type === 'all' ? keywords : undefined
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Search API] 搜索失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '搜索失败: ' + (error as Error).message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user