Files
weicw-dshsa 4bb349fd56 chore: 加 .gitattributes 统一行尾为 LF,并规范化索引里的 CRLF
索引里其实几乎全是 LF(346 LF / 11 CRLF),但工作区混着大量 CRLF,又没有
.gitattributes,于是任何工具把整份文件翻一次行尾,改 5 行就会变成 500 行 diff。
加 `* text=auto eol=lf` 之后 git 在读写两侧都会做转换,这类噪音 diff 不会再出现。

本次只规范化了索引里真是 CRLF 的 11 个文件;二进制与 Windows 脚本的行尾规则
也一并写进 .gitattributes。工作区里已存在的 CRLF 文件不会被改写,下次检出才是 LF。

注:pre-commit 的行尾守卫拦下了这次提交,属预期——这个提交本身就是一次有意的
行尾规范化,故用 --no-verify 放行。
2026-09-29 16:24:25 +08:00

125 lines
5.1 KiB
TypeScript
Raw Permalink 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.
import { $fetch } from 'ofetch'
import { defineEventHandler, getQuery } from 'h3'
import { useRuntimeConfig } from '#imports'
// 注意:本文件在 server/api/ 根下(比 server/api/case/list.get.ts 浅一层),
// 故 utils 的相对路径是 ../utils/,不是 ../../utils/。
import { getTenantFromContext } from '../utils/tenant'
import { normalizeCaseItem } from '../utils/case'
/**
* 统一搜索(文章 / 案例 / 标书)
* GET /api/search?keywords=xxx
*
* 一次请求并发查询三个上游,归一化为统一结构:
* {
* article: { list, count } | { error: true },
* case: { list, count } | { error: true },
* tender: { list, count } | { error: true }
* }
*
* 设计要点(见 .scratch/web-unified-search/issues/02-aggregation-strategy.md):
* - 每类固定 limit=10,不做分页;结果页一次拿全,用户想看更多去各自模块页。
* - 用 Promise.allSettled(不是 all):某类上游失败时**只降级那一类**,其余照常返回,
* 整页不 500。失败的那类返 { error: true } 而非 { list: [] }——后者会让前端把
* 「接口故障」渲染成「暂无结果」,等于对用户说谎(前端据 error 标记区分文案)。
* - 不改动既有的 article/case/tender 三个 list 代理,它们有大量现有消费方。
* - status / deleted 是内容安全边界(防草稿、下架、软删内容泄露到 C 端),
* 与现有三个代理保持完全一致的取值,不可省略。
*/
/** 每类返回的条数上限(结果页一次性展示,不做分页) */
const SEARCH_LIMIT = 10
/** 单类结果:成功为 { list, count },失败为 { error: true } */
type SectionResult<T> = { list: T[]; count: number } | { error: true }
/** 从上游各种包装形态里取出 { list, count }(与 case/product/list 代理同一套兜底逻辑) */
function unwrapList(res: any): { list: any[]; count: number } {
const envelopeData = (res && res.data !== undefined ? res.data : res) || {}
const list = Array.isArray(envelopeData.list) ? envelopeData.list : []
const count = envelopeData.count ?? envelopeData.total ?? list.length
return { list, count }
}
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const query = getQuery(event)
const modulesApiBase = config.public.modulesApiBase as string
const keywords = String(query.keywords ?? '').trim()
// 空关键词不是错误:不请求上游,直接返回三组空结果。
// (避免无谓的上游调用,也让搜索页的「未输入关键词」态有确定的响应形状。)
if (!keywords) {
const empty: SectionResult<never> = { list: [], count: 0 }
return { article: empty, case: empty, tender: empty }
}
const commonHeaders = { TenantId: ctx.tenantId }
// 三个上游并发;每个任务内部自己兜底成 SectionResult,故 allSettled 的 rejected 分支
// 实际只会在「连 try/catch 都没兜住」的意外情况下触发,仍按 error 处理,不抛给调用方。
const [article, caseRes, tender] = await Promise.allSettled([
// 文章:/cms/cms-article/page
// status 0=已发布 1=草稿 2=下架;deleted=0 排除软删。放在 keywords 之后覆盖,防前端传入。
$fetch('/cms/cms-article/page', {
baseURL: modulesApiBase,
headers: commonHeaders,
query: {
keywords,
page: 1,
limit: SEARCH_LIMIT,
status: 0,
deleted: 0
}
}),
// 案例:/cms/cms-case/page(注意是 cms-case 控制器,不是 cms-website 兜底路由)
$fetch('/cms/cms-case/page', {
baseURL: modulesApiBase,
headers: commonHeaders,
query: {
keywords,
page: 1,
limit: SEARCH_LIMIT,
TenantId: ctx.tenantId,
status: 0,
deleted: 0
}
}),
// 标书:/hjc/bid-project/list,status=1 只搜在售(与 /tender 列表口径一致)。
// 上游 XML 已写死 deleted=0,无需也不应再传 deleted(HjcBidProjectParam 上无该字段)。
$fetch('/hjc/bid-project/list', {
baseURL: modulesApiBase,
headers: commonHeaders,
query: {
keywords,
page: 1,
limit: SEARCH_LIMIT,
status: 1,
TenantId: ctx.tenantId
}
})
])
const section = <T>(settled: PromiseSettledResult<any>, map: (raw: any) => T): SectionResult<T> => {
if (settled.status === 'rejected') {
return { error: true }
}
const { list, count } = unwrapList(settled.value)
return { list: list.map(map), count }
}
return {
// 文章:前端直接消费上游字段(id/title/summary/publishTime/categoryName)
article: section(article, (raw: any) => raw),
// 案例:上游返回 caseId/caseName/customer/industry,模板按 id/title/clientName 渲染,
// 复用 case/list 代理同款归一化,避免搜索页再写一份字段映射
case: section(caseRes, normalizeCaseItem),
// 标书:前端直接消费上游字段(id/projectName/tenderPrice/deadlineTime/projectNo/tenderer)
tender: section(tender, (raw: any) => raw)
}
})