Files
soul/app/api/search/route.ts
卡若 4dd2f9f4a7 feat: 完善后台管理+搜索功能+分销系统
主要更新:
- 后台菜单精简(9项→6项)
- 新增搜索功能(敏感信息过滤)
- 分销绑定和提现系统完善
- 数据库初始化API(自动修复表结构)
- 用户管理:显示绑定关系详情
- 小程序:上下章导航优化、匹配页面重构
- 修复hydration和数据类型问题
2026-01-25 19:37:59 +08:00

274 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 搜索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 })
}
}