From 7f2af7f7e249b36460db732be51827b3c163423d Mon Sep 17 00:00:00 2001 From: yyhuni Date: Sat, 3 Jan 2026 13:22:21 +0800 Subject: [PATCH] feat(search): add result export functionality and pagination limit support - Add optional limit parameter to AssetSearchService.search() method for controlling result set size - Implement AssetSearchExportView for exporting search results as CSV files with UTF-8 BOM encoding - Add CSV export endpoint at GET /api/assets/search/export/ with configurable MAX_EXPORT_ROWS limit (10000) - Support both website and endpoint asset types with type-specific column mappings in CSV export - Format array fields (tech, matched_gf_patterns) and dates appropriately in exported CSV - Update URL routing to include new search export endpoint - Update views __init__.py to export AssetSearchExportView - Add CSV generation with streaming response for efficient memory usage on large exports - Update frontend search service to support export functionality - Add internationalization strings for export feature in en.json and zh.json - Update smart-filter-input and search-results-table components to support export UI - Update installation and Docker startup scripts for deployment compatibility --- backend/apps/asset/services/search_service.py | 8 +- backend/apps/asset/urls.py | 2 + backend/apps/asset/views/__init__.py | 3 +- backend/apps/asset/views/search_views.py | 129 ++++++++++++++++++ docker/start.sh | 7 + .../components/common/smart-filter-input.tsx | 16 +-- frontend/components/search/search-page.tsx | 91 ++++++------ .../search/search-results-table.tsx | 63 +++++---- frontend/messages/en.json | 14 ++ frontend/messages/zh.json | 14 ++ frontend/services/search.service.ts | 36 ++++- install.sh | 11 +- 12 files changed, 304 insertions(+), 90 deletions(-) diff --git a/backend/apps/asset/services/search_service.py b/backend/apps/asset/services/search_service.py index 34dabffd..cb012678 100644 --- a/backend/apps/asset/services/search_service.py +++ b/backend/apps/asset/services/search_service.py @@ -323,7 +323,8 @@ class AssetSearchService: def search( self, query: str, - asset_type: AssetType = 'website' + asset_type: AssetType = 'website', + limit: Optional[int] = None ) -> List[Dict[str, Any]]: """ 搜索资产 @@ -331,6 +332,7 @@ class AssetSearchService: Args: query: 搜索查询字符串 asset_type: 资产类型 ('website' 或 'endpoint') + limit: 最大返回数量(可选) Returns: List[Dict]: 搜索结果列表 @@ -348,6 +350,10 @@ class AssetSearchService: ORDER BY created_at DESC """ + # 添加 LIMIT + if limit is not None and limit > 0: + sql += f" LIMIT {int(limit)}" + try: with connection.cursor() as cursor: cursor.execute(sql, params) diff --git a/backend/apps/asset/urls.py b/backend/apps/asset/urls.py index 45790194..d97b39fe 100644 --- a/backend/apps/asset/urls.py +++ b/backend/apps/asset/urls.py @@ -11,6 +11,7 @@ from .views import ( VulnerabilityViewSet, AssetStatisticsViewSet, AssetSearchView, + AssetSearchExportView, ) # 创建 DRF 路由器 @@ -27,4 +28,5 @@ router.register(r'statistics', AssetStatisticsViewSet, basename='asset-statistic urlpatterns = [ path('assets/', include(router.urls)), path('assets/search/', AssetSearchView.as_view(), name='asset-search'), + path('assets/search/export/', AssetSearchExportView.as_view(), name='asset-search-export'), ] diff --git a/backend/apps/asset/views/__init__.py b/backend/apps/asset/views/__init__.py index a7f1c07a..648a80a8 100644 --- a/backend/apps/asset/views/__init__.py +++ b/backend/apps/asset/views/__init__.py @@ -19,7 +19,7 @@ from .asset_views import ( HostPortMappingSnapshotViewSet, VulnerabilitySnapshotViewSet, ) -from .search_views import AssetSearchView +from .search_views import AssetSearchView, AssetSearchExportView __all__ = [ 'AssetStatisticsViewSet', @@ -36,4 +36,5 @@ __all__ = [ 'HostPortMappingSnapshotViewSet', 'VulnerabilitySnapshotViewSet', 'AssetSearchView', + 'AssetSearchExportView', ] diff --git a/backend/apps/asset/views/search_views.py b/backend/apps/asset/views/search_views.py index 2e3f964a..21a95b45 100644 --- a/backend/apps/asset/views/search_views.py +++ b/backend/apps/asset/views/search_views.py @@ -3,6 +3,7 @@ 提供资产搜索的 REST API 接口: - GET /api/assets/search/ - 搜索资产 +- GET /api/assets/search/export/ - 导出搜索结果为 CSV 搜索语法: - field="value" 模糊匹配(ILIKE %value%) @@ -27,10 +28,14 @@ import logging import json +import csv +from io import StringIO +from datetime import datetime from urllib.parse import urlparse, urlunparse from rest_framework import status from rest_framework.views import APIView from rest_framework.request import Request +from django.http import StreamingHttpResponse from django.db import connection from apps.common.response_helpers import success_response, error_response @@ -269,3 +274,127 @@ class AssetSearchView(APIView): 'totalPages': total_pages, 'assetType': asset_type, }) + + +class AssetSearchExportView(APIView): + """ + 资产搜索导出 API + + GET /api/assets/search/export/ + + Query Parameters: + q: 搜索查询表达式 + asset_type: 资产类型 ('website' 或 'endpoint',默认 'website') + + Response: + CSV 文件流 + """ + + # 导出数量限制 + MAX_EXPORT_ROWS = 10000 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.service = AssetSearchService() + + def _parse_headers(self, headers_data) -> str: + """解析响应头为字符串""" + if not headers_data: + return '' + try: + headers = json.loads(headers_data) + return '; '.join(f'{k}: {v}' for k, v in headers.items()) + except (json.JSONDecodeError, TypeError): + return str(headers_data) + + def _generate_csv(self, results: list, asset_type: str): + """生成 CSV 内容的生成器""" + # 定义列 + if asset_type == 'website': + columns = ['url', 'host', 'title', 'status_code', 'content_type', 'content_length', + 'webserver', 'location', 'tech', 'vhost', 'created_at'] + headers = ['URL', 'Host', 'Title', 'Status', 'Content-Type', 'Content-Length', + 'Webserver', 'Location', 'Technologies', 'VHost', 'Created At'] + else: + columns = ['url', 'host', 'title', 'status_code', 'content_type', 'content_length', + 'webserver', 'location', 'tech', 'matched_gf_patterns', 'vhost', 'created_at'] + headers = ['URL', 'Host', 'Title', 'Status', 'Content-Type', 'Content-Length', + 'Webserver', 'Location', 'Technologies', 'GF Patterns', 'VHost', 'Created At'] + + # 写入 BOM 和表头 + output = StringIO() + writer = csv.writer(output) + + # UTF-8 BOM + yield '\ufeff' + + # 表头 + writer.writerow(headers) + yield output.getvalue() + output.seek(0) + output.truncate(0) + + # 数据行 + for result in results: + row = [] + for col in columns: + value = result.get(col) + if col == 'tech' or col == 'matched_gf_patterns': + # 数组转字符串 + row.append('; '.join(value) if value else '') + elif col == 'created_at': + # 日期格式化 + row.append(value.strftime('%Y-%m-%d %H:%M:%S') if value else '') + elif col == 'vhost': + row.append('true' if value else 'false' if value is False else '') + else: + row.append(str(value) if value is not None else '') + + writer.writerow(row) + yield output.getvalue() + output.seek(0) + output.truncate(0) + + def get(self, request: Request): + """导出搜索结果为 CSV""" + # 获取搜索查询 + query = request.query_params.get('q', '').strip() + + if not query: + return error_response( + code=ErrorCodes.VALIDATION_ERROR, + message='Search query (q) is required', + status_code=status.HTTP_400_BAD_REQUEST + ) + + # 获取并验证资产类型 + asset_type = request.query_params.get('asset_type', 'website').strip().lower() + if asset_type not in VALID_ASSET_TYPES: + return error_response( + code=ErrorCodes.VALIDATION_ERROR, + message=f'Invalid asset_type. Must be one of: {", ".join(VALID_ASSET_TYPES)}', + status_code=status.HTTP_400_BAD_REQUEST + ) + + # 获取搜索结果(限制数量) + results = self.service.search(query, asset_type, limit=self.MAX_EXPORT_ROWS) + + if not results: + return error_response( + code=ErrorCodes.NOT_FOUND, + message='No results to export', + status_code=status.HTTP_404_NOT_FOUND + ) + + # 生成文件名 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f'search_{asset_type}_{timestamp}.csv' + + # 返回流式响应 + response = StreamingHttpResponse( + self._generate_csv(results, asset_type), + content_type='text/csv; charset=utf-8' + ) + response['Content-Disposition'] = f'attachment; filename="{filename}"' + + return response diff --git a/docker/start.sh b/docker/start.sh index f1c44e2d..b378588a 100755 --- a/docker/start.sh +++ b/docker/start.sh @@ -15,10 +15,12 @@ NC='\033[0m' # 解析参数 WITH_FRONTEND=true DEV_MODE=false +QUIET_MODE=false for arg in "$@"; do case $arg in --no-frontend) WITH_FRONTEND=false ;; --dev) DEV_MODE=true ;; + --quiet) QUIET_MODE=true ;; esac done @@ -155,6 +157,11 @@ echo -e "${GREEN}[OK]${NC} 服务已启动" # 数据初始化 ./scripts/init-data.sh +# 静默模式下不显示结果(由调用方显示) +if [ "$QUIET_MODE" = true ]; then + exit 0 +fi + # 获取访问地址 PUBLIC_HOST=$(grep "^PUBLIC_HOST=" .env 2>/dev/null | cut -d= -f2) if [ -n "$PUBLIC_HOST" ] && [ "$PUBLIC_HOST" != "server" ]; then diff --git a/frontend/components/common/smart-filter-input.tsx b/frontend/components/common/smart-filter-input.tsx index a593b0a9..f45b993c 100644 --- a/frontend/components/common/smart-filter-input.tsx +++ b/frontend/components/common/smart-filter-input.tsx @@ -381,9 +381,9 @@ export function SmartFilterInput({ return (
- - -
+
+ +
)}
- -
- + + +
) } diff --git a/frontend/components/search/search-page.tsx b/frontend/components/search/search-page.tsx index 38fcf5ec..f6ea22f4 100644 --- a/frontend/components/search/search-page.tsx +++ b/frontend/components/search/search-page.tsx @@ -1,24 +1,25 @@ "use client" import { useState, useCallback, useMemo, useEffect } from "react" +import { useSearchParams } from "next/navigation" import { motion, AnimatePresence } from "framer-motion" -import { Search, AlertCircle, Globe, Link2, ShieldAlert, History, X } from "lucide-react" +import { Search, AlertCircle, History, X, Download } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" -import { useQuery } from "@tanstack/react-query" import { SmartFilterInput, type FilterField } from "@/components/common/smart-filter-input" import { SearchPagination } from "./search-pagination" import { useAssetSearch } from "@/hooks/use-search" import { VulnerabilityDetailDialog } from "@/components/vulnerabilities/vulnerability-detail-dialog" import { VulnerabilityService } from "@/services/vulnerability.service" +import { SearchService } from "@/services/search.service" import type { SearchParams, SearchState, Vulnerability as SearchVuln, AssetType } from "@/types/search.types" import type { Vulnerability } from "@/types/vulnerability.types" import { Alert, AlertDescription } from "@/components/ui/alert" +import { Button } from "@/components/ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { SearchResultsTable } from "./search-results-table" import { SearchResultCard } from "./search-result-card" import { Badge } from "@/components/ui/badge" -import { getAssetStatistics } from "@/services/dashboard.service" import { cn } from "@/lib/utils" // Website 搜索示例 @@ -98,6 +99,7 @@ function removeRecentSearch(query: string) { export function SearchPage() { const t = useTranslations('search') + const urlSearchParams = useSearchParams() const [searchState, setSearchState] = useState("initial") const [query, setQuery] = useState("") const [assetType, setAssetType] = useState("website") @@ -108,19 +110,28 @@ export function SearchPage() { const [vulnDialogOpen, setVulnDialogOpen] = useState(false) const [, setLoadingVuln] = useState(false) const [recentSearches, setRecentSearches] = useState([]) - - // 获取资产统计数据 - const { data: stats } = useQuery({ - queryKey: ['assetStatistics'], - queryFn: getAssetStatistics, - staleTime: 5 * 60 * 1000, // 5 minutes - }) + const [initialQueryProcessed, setInitialQueryProcessed] = useState(false) // 加载最近搜索记录 useEffect(() => { setRecentSearches(getRecentSearches()) }, []) + // 处理 URL 参数中的搜索查询 + useEffect(() => { + if (initialQueryProcessed) return + + const q = urlSearchParams.get('q') + if (q) { + setQuery(q) + setSearchParams({ q, asset_type: assetType }) + setSearchState("searching") + saveRecentSearch(q) + setRecentSearches(getRecentSearches()) + } + setInitialQueryProcessed(true) + }, [urlSearchParams, assetType, initialQueryProcessed]) + // 根据资产类型选择搜索示例 const searchExamples = useMemo(() => { return assetType === 'endpoint' ? ENDPOINT_SEARCH_EXAMPLES : WEBSITE_SEARCH_EXAMPLES @@ -178,6 +189,25 @@ export function SearchPage() { setRecentSearches(getRecentSearches()) }, []) + // 导出状态 + const [isExporting, setIsExporting] = useState(false) + + // 导出 CSV(调用后端 API 导出全部结果) + const handleExportCSV = useCallback(async () => { + if (!searchParams.q) return + + setIsExporting(true) + try { + await SearchService.exportCSV(searchParams.q, assetType) + toast.success(t('exportSuccess')) + } catch (error) { + console.error('Export failed:', error) + toast.error(t('exportFailed')) + } finally { + setIsExporting(false) + } + }, [searchParams.q, assetType, t]) + // 当数据加载完成时更新状态 if (searchState === "searching" && data && !isLoading) { setSearchState("results") @@ -289,38 +319,6 @@ export function SearchPage() { ))} - {/* 资产统计 */} - {stats && ( - -
- - - {stats.totalWebsites.toLocaleString()} - - {t('assetTypes.website')} -
-
- - - {stats.totalEndpoints.toLocaleString()} - - {t('assetTypes.endpoint')} -
-
- - - {stats.totalVulns.toLocaleString()} - - {t('stats.vulnerabilities')} -
-
- )} - {/* 最近搜索 */} {recentSearches.length > 0 && ( {isFetching ? t('loading') : t('resultsCount', { count: data?.total ?? 0 })} + diff --git a/frontend/components/search/search-results-table.tsx b/frontend/components/search/search-results-table.tsx index ea87c700..d94f1533 100644 --- a/frontend/components/search/search-results-table.tsx +++ b/frontend/components/search/search-results-table.tsx @@ -1,7 +1,7 @@ "use client" import { useMemo } from "react" -import { useTranslations, useFormatter } from "next-intl" +import { useFormatter } from "next-intl" import type { ColumnDef } from "@tanstack/react-table" import { Badge } from "@/components/ui/badge" import { DataTableColumnHeader, UnifiedDataTable } from "@/components/ui/data-table" @@ -15,7 +15,6 @@ interface SearchResultsTableProps { } export function SearchResultsTable({ results, assetType }: SearchResultsTableProps) { - const t = useTranslations('search.table') const format = useFormatter() const formatDate = (dateString: string) => { @@ -33,9 +32,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "url", accessorKey: "url", - meta: { title: t('url') }, + meta: { title: "URL" }, header: ({ column }) => ( - + ), size: 350, minSize: 200, @@ -47,9 +46,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "host", accessorKey: "host", - meta: { title: t('host') }, + meta: { title: "Host" }, header: ({ column }) => ( - + ), size: 180, minSize: 100, @@ -61,9 +60,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "title", accessorKey: "title", - meta: { title: t('title') }, + meta: { title: "Title" }, header: ({ column }) => ( - + ), size: 150, minSize: 100, @@ -75,9 +74,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "statusCode", accessorKey: "statusCode", - meta: { title: t('status') }, + meta: { title: "Status" }, header: ({ column }) => ( - + ), size: 80, minSize: 60, @@ -103,9 +102,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "technologies", accessorKey: "technologies", - meta: { title: t('technologies') }, + meta: { title: "Tech" }, header: ({ column }) => ( - + ), size: 180, minSize: 120, @@ -118,9 +117,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "contentLength", accessorKey: "contentLength", - meta: { title: t('contentLength') }, + meta: { title: "Length" }, header: ({ column }) => ( - + ), size: 100, minSize: 80, @@ -134,9 +133,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "location", accessorKey: "location", - meta: { title: t('location') }, + meta: { title: "Location" }, header: ({ column }) => ( - + ), size: 150, minSize: 100, @@ -148,9 +147,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "webserver", accessorKey: "webserver", - meta: { title: t('webserver') }, + meta: { title: "Server" }, header: ({ column }) => ( - + ), size: 120, minSize: 80, @@ -162,9 +161,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "contentType", accessorKey: "contentType", - meta: { title: t('contentType') }, + meta: { title: "Type" }, header: ({ column }) => ( - + ), size: 120, minSize: 80, @@ -176,9 +175,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "responseBody", accessorKey: "responseBody", - meta: { title: t('responseBody') }, + meta: { title: "Body" }, header: ({ column }) => ( - + ), size: 300, minSize: 200, @@ -189,9 +188,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "responseHeaders", accessorKey: "responseHeaders", - meta: { title: t('responseHeaders') }, + meta: { title: "Headers" }, header: ({ column }) => ( - + ), size: 250, minSize: 150, @@ -210,9 +209,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "vhost", accessorKey: "vhost", - meta: { title: t('vhost') }, + meta: { title: "VHost" }, header: ({ column }) => ( - + ), size: 80, minSize: 60, @@ -226,9 +225,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro { id: "createdAt", accessorKey: "createdAt", - meta: { title: t('createdAt') }, + meta: { title: "Created" }, header: ({ column }) => ( - + ), size: 150, minSize: 120, @@ -239,16 +238,16 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro return {formatDate(createdAt)} }, }, - ], [t, formatDate]) + ], [formatDate]) // Endpoint 特有列 const endpointColumns: ColumnDef[] = useMemo(() => [ { id: "matchedGfPatterns", accessorKey: "matchedGfPatterns", - meta: { title: t('gfPatterns') }, + meta: { title: "GF Patterns" }, header: ({ column }) => ( - + ), size: 150, minSize: 100, @@ -259,7 +258,7 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro return }, }, - ], [t]) + ], []) // 根据资产类型组合列 const columns = useMemo(() => { diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 574a7316..b03491ac 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -326,6 +326,10 @@ "noResultsHint": "Try adjusting your search criteria", "vulnLoadError": "Failed to load vulnerability details", "recentSearches": "Recent Searches", + "export": "Export", + "exporting": "Exporting...", + "exportSuccess": "Export successful", + "exportFailed": "Export failed", "stats": { "vulnerabilities": "Vulnerabilities" }, @@ -1971,6 +1975,16 @@ "formatInvalid": "Invalid format" } }, + "globalSearch": { + "search": "Search", + "placeholder": "Search assets... (host=\"api\" && tech=\"nginx\")", + "noResults": "No results found", + "searchFor": "Search for", + "recent": "Recent Searches", + "quickSearch": "Quick Search", + "hint": "Supports FOFA-style syntax", + "toSearch": "to search" + }, "errors": { "unknown": "Operation failed, please try again later", "validation": "Invalid input data", diff --git a/frontend/messages/zh.json b/frontend/messages/zh.json index 052f3b7d..b041f140 100644 --- a/frontend/messages/zh.json +++ b/frontend/messages/zh.json @@ -326,6 +326,10 @@ "noResultsHint": "请尝试调整搜索条件", "vulnLoadError": "加载漏洞详情失败", "recentSearches": "最近搜索", + "export": "导出", + "exporting": "导出中...", + "exportSuccess": "导出成功", + "exportFailed": "导出失败", "stats": { "vulnerabilities": "漏洞" }, @@ -1971,6 +1975,16 @@ "formatInvalid": "格式无效" } }, + "globalSearch": { + "search": "搜索", + "placeholder": "搜索资产... (host=\"api\" && tech=\"nginx\")", + "noResults": "未找到结果", + "searchFor": "搜索", + "recent": "最近搜索", + "quickSearch": "快捷搜索", + "hint": "支持 FOFA 风格语法", + "toSearch": "搜索" + }, "errors": { "unknown": "操作失败,请稍后重试", "validation": "输入数据无效", diff --git a/frontend/services/search.service.ts b/frontend/services/search.service.ts index a64b0eac..5f6bb31c 100644 --- a/frontend/services/search.service.ts +++ b/frontend/services/search.service.ts @@ -1,5 +1,5 @@ import { api } from "@/lib/api-client" -import type { SearchParams, SearchResponse } from "@/types/search.types" +import type { SearchParams, SearchResponse, AssetType } from "@/types/search.types" /** * 资产搜索 API 服务 @@ -38,4 +38,38 @@ export class SearchService { ) return response.data } + + /** + * 导出搜索结果为 CSV + * GET /api/assets/search/export/ + */ + static async exportCSV(query: string, assetType: AssetType): Promise { + const queryParams = new URLSearchParams() + queryParams.append('q', query) + queryParams.append('asset_type', assetType) + + const response = await api.get( + `/assets/search/export/?${queryParams.toString()}`, + { responseType: 'blob' } + ) + + // 从响应头获取文件名 + const contentDisposition = response.headers?.['content-disposition'] + let filename = `search_${assetType}_${new Date().toISOString().slice(0, 10)}.csv` + if (contentDisposition) { + const match = contentDisposition.match(/filename="?([^"]+)"?/) + if (match) filename = match[1] + } + + // 创建下载链接 + const blob = new Blob([response.data as BlobPart], { type: 'text/csv;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } } diff --git a/install.sh b/install.sh index 093c793c..16069cde 100755 --- a/install.sh +++ b/install.sh @@ -94,7 +94,7 @@ show_banner() { echo -e "${MAGENTA} ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝${RESET}" echo -e "" echo -e "${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo -e "${BOLD} 🔒 分布式安全扫描平台 │ 一键部署安装程序${RESET}" + echo -e "${BOLD} 🔒 分布式安全扫描平台 │ 一键部署 (Ubuntu)${RESET}" echo -e "${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo -e "" } @@ -132,7 +132,6 @@ fi # 显示标题 show_banner -header "XingRin 一键安装脚本 (Ubuntu)" info "当前用户: ${BOLD}$REAL_USER${RESET}" info "项目路径: ${BOLD}$ROOT_DIR${RESET}" info "安装版本: ${BOLD}$APP_VERSION${RESET}" @@ -324,7 +323,7 @@ check_pg_ivm() { # 显示安装总结信息 show_summary() { echo - if [ "$1" == "success" ]; then + if [ "$1" = "success" ]; then # 成功 Banner echo -e "" echo -e "${GREEN}${BOLD} ╔═══════════════════════════════════════════════════╗${RESET}" @@ -378,7 +377,9 @@ show_summary() { echo -e " ${YELLOW} ⚠ 请首次登录后修改密码!${RESET}" echo - if [ "$1" != "success" ]; then + if [ "$1" = "success" ]; then + : # 成功模式,不显示后续命令 + else echo -e "${DIM} ──────────────────────────────────────────────────────${RESET}" echo -e " ${BLUE}🚀 后续命令${RESET}" echo -e " ${DIM}├─${RESET} ./start.sh ${DIM}# 启动所有服务${RESET}" @@ -743,7 +744,7 @@ fi # 启动服务 # ============================================================================== step "正在启动服务..." -"$ROOT_DIR/start.sh" $START_ARGS +"$ROOT_DIR/start.sh" ${START_ARGS} --quiet # ============================================================================== # 完成总结