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 放行。
This commit is contained in:
2026-09-29 16:24:25 +08:00
parent 3b703c70e8
commit 4bb349fd56
12 changed files with 1141 additions and 1105 deletions
+124 -124
View File
@@ -1,124 +1,124 @@
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)
}
})
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)
}
})
+40 -40
View File
@@ -1,40 +1,40 @@
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, getHeader, getCookie, getQuery } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 查询支付渠道侧的状态(已支付则后端顺带修复本地订单状态)
* GET /api/tender/pay-status?orderNo=xxx
* 代理到 mp-api /api/hjc/order/pay-status/{orderNo};需登录态。
*
* 这是 hjc-web **唯一**的支付确认通道:它向渠道查单,以渠道侧的结论为准。
* 后端按订单的 pay_method 自动打到微信或支付宝,**前端不需要传渠道**。
* 不要退回 mark-paid——那个接口不查单,点一下就能把订单置为已支付(见 ADR 0010)。
*
* 约定:原样透传 ApiResult{code,message,data},由前端按 code 判定登录态与业务失败。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const { orderNo } = getQuery(event)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
return await $fetch(`/hjc/order/pay-status/${encodeURIComponent(String(orderNo ?? ''))}`, {
baseURL: config.public.modulesApiBase,
method: 'GET',
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: { TenantId: ctx.tenantId }
})
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to query pay status'
})
}
})
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, getHeader, getCookie, getQuery } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 查询支付渠道侧的状态(已支付则后端顺带修复本地订单状态)
* GET /api/tender/pay-status?orderNo=xxx
* 代理到 mp-api /api/hjc/order/pay-status/{orderNo};需登录态。
*
* 这是 hjc-web **唯一**的支付确认通道:它向渠道查单,以渠道侧的结论为准。
* 后端按订单的 pay_method 自动打到微信或支付宝,**前端不需要传渠道**。
* 不要退回 mark-paid——那个接口不查单,点一下就能把订单置为已支付(见 ADR 0010)。
*
* 约定:原样透传 ApiResult{code,message,data},由前端按 code 判定登录态与业务失败。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const { orderNo } = getQuery(event)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
return await $fetch(`/hjc/order/pay-status/${encodeURIComponent(String(orderNo ?? ''))}`, {
baseURL: config.public.modulesApiBase,
method: 'GET',
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: { TenantId: ctx.tenantId }
})
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to query pay status'
})
}
})
+40 -40
View File
@@ -1,40 +1,40 @@
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 发起支付(微信 Native 扫码 / 支付宝电脑网站支付)
* POST /api/tender/pay { orderNo, payType? }
* 代理到 mp-api /api/hjc/order/pay;需登录态。
*
* `payType`:`wechat`(默认,与接入支付宝前行为一致)/ `alipay`。
* 请求体原样透传,因此新增支付方式**不需要改这个代理**。
*
* 约定:原样透传 ApiResult{code,message,data},由前端按 code 判定登录态与业务失败。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const body = await readBody(event)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
return await $fetch('/hjc/order/pay', {
baseURL: config.public.modulesApiBase,
method: 'POST',
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: { TenantId: ctx.tenantId },
body
})
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to pay'
})
}
})
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 发起支付(微信 Native 扫码 / 支付宝电脑网站支付)
* POST /api/tender/pay { orderNo, payType? }
* 代理到 mp-api /api/hjc/order/pay;需登录态。
*
* `payType`:`wechat`(默认,与接入支付宝前行为一致)/ `alipay`。
* 请求体原样透传,因此新增支付方式**不需要改这个代理**。
*
* 约定:原样透传 ApiResult{code,message,data},由前端按 code 判定登录态与业务失败。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const body = await readBody(event)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
return await $fetch('/hjc/order/pay', {
baseURL: config.public.modulesApiBase,
method: 'POST',
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: { TenantId: ctx.tenantId },
body
})
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to pay'
})
}
})