From 6f8edb16a8148bf4773001bdb38e403ac5f5db4d Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 22 Sep 2026 12:04:04 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E8=84=9A=E5=B1=95=E7=A4=BA=E5=89=8D?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E7=89=88=E6=9C=AC=E4=B8=8E=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 internal/version 包维护服务端版本(默认 0.1.0,可用 -ldflags -X 覆盖),GET /api/health 返回 version 字段 - main.go Swagger @version 与常量对齐为 0.1.0,重新生成 docs;api_test 增加版本断言 - 前端 package.json 升至 0.1.0,Vite 注入版本、git 短哈希与构建时间(无 git 自动省略哈希) - 新增 api/version.ts 与 version store,启动时从 health 读取后端版本(失败显示 —) - 页脚左下角显示“前端 vX | 后端 vY | 构建 +hash · 时间”,三语文案补齐 --- docs/docs.go | 8 +++-- docs/swagger.json | 8 +++-- docs/swagger.yaml | 7 +++-- frontend/env.d.ts | 4 +++ frontend/package-lock.json | 4 +-- frontend/package.json | 2 +- frontend/src/api/version.ts | 10 ++++++ frontend/src/components/layout/AppFooter.vue | 6 ++++ frontend/src/i18n/locales/en-US.ts | 2 ++ frontend/src/i18n/locales/ja-JP.ts | 2 ++ frontend/src/i18n/locales/zh-CN.ts | 2 ++ frontend/src/main.ts | 4 +++ frontend/src/stores/version.ts | 33 ++++++++++++++++++++ frontend/vite.config.ts | 27 ++++++++++++++++ internal/api/api.go | 12 ++++--- internal/api/api_test.go | 4 +++ internal/version/version.go | 6 ++++ main.go | 2 +- 18 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 frontend/src/api/version.ts create mode 100644 frontend/src/stores/version.ts create mode 100644 internal/version/version.go diff --git a/docs/docs.go b/docs/docs.go index b98819f..1cc2b0f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -308,7 +308,7 @@ const docTemplate = `{ }, "/health": { "get": { - "description": "Check service and database connectivity; returns 503 when the database is unavailable.", + "description": "Check service and database connectivity; returns 503 when the database is unavailable. The version field reports the current server version.", "produces": [ "application/json" ], @@ -2401,6 +2401,10 @@ const docTemplate = `{ "status": { "type": "string", "example": "ok" + }, + "version": { + "type": "string", + "example": "0.1.0" } } }, @@ -3229,7 +3233,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "1.0", + Version: "0.1.0", Host: "", BasePath: "/api", Schemes: []string{}, diff --git a/docs/swagger.json b/docs/swagger.json index c895276..8500f90 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,7 +4,7 @@ "description": "Rill server HTTP API documentation. All endpoints are prefixed with api.prefix (default /api); requests and responses are JSON.\nSwagger UI: {prefix}/swagger/index.html; OpenAPI JSON: {prefix}/swagger/doc.json.\nEndpoints are grouped by required permission: public needs no authentication, user needs a Bearer JWT obtained from /auth/login, admin needs an admin user.", "title": "Rill API", "contact": {}, - "version": "1.0" + "version": "0.1.0" }, "basePath": "/api", "paths": { @@ -301,7 +301,7 @@ }, "/health": { "get": { - "description": "Check service and database connectivity; returns 503 when the database is unavailable.", + "description": "Check service and database connectivity; returns 503 when the database is unavailable. The version field reports the current server version.", "produces": [ "application/json" ], @@ -2394,6 +2394,10 @@ "status": { "type": "string", "example": "ok" + }, + "version": { + "type": "string", + "example": "0.1.0" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 52ff010..abda2a4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -8,6 +8,9 @@ definitions: status: example: ok type: string + version: + example: 0.1.0 + type: string type: object auth.LoginRequest: properties: @@ -584,7 +587,7 @@ info: Swagger UI: {prefix}/swagger/index.html; OpenAPI JSON: {prefix}/swagger/doc.json. Endpoints are grouped by required permission: public needs no authentication, user needs a Bearer JWT obtained from /auth/login, admin needs an admin user. title: Rill API - version: "1.0" + version: 0.1.0 paths: /auth/login: post: @@ -787,7 +790,7 @@ paths: /health: get: description: Check service and database connectivity; returns 503 when the database - is unavailable. + is unavailable. The version field reports the current server version. produces: - application/json responses: diff --git a/frontend/env.d.ts b/frontend/env.d.ts index 11f02fe..07ac6d9 100644 --- a/frontend/env.d.ts +++ b/frontend/env.d.ts @@ -1 +1,5 @@ /// + +declare const __APP_VERSION__: string +declare const __GIT_HASH__: string +declare const __BUILD_TIME__: string diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d574e63..8dffe54 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { "@tailwindcss/vite": "^4.3.3", "@vee-validate/zod": "^4.15.1", diff --git a/frontend/package.json b/frontend/package.json index 164ec5e..1ea46f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/api/version.ts b/frontend/src/api/version.ts new file mode 100644 index 0000000..fd3cfa0 --- /dev/null +++ b/frontend/src/api/version.ts @@ -0,0 +1,10 @@ +import { request } from './http' + +export interface HealthInfo { + status: string + version: string +} + +export function getHealth(): Promise { + return request('/health') +} diff --git a/frontend/src/components/layout/AppFooter.vue b/frontend/src/components/layout/AppFooter.vue index dd1520b..e479000 100644 --- a/frontend/src/components/layout/AppFooter.vue +++ b/frontend/src/components/layout/AppFooter.vue @@ -5,10 +5,12 @@ import LanguageSwitcher from '@/components/layout/LanguageSwitcher.vue' import ThemeSwitcher from '@/components/layout/ThemeSwitcher.vue' import { isInternalNavUrl, navLabel, useNavStore } from '@/stores/nav' import { useSiteStore } from '@/stores/site' +import { useVersionStore } from '@/stores/version' const { t, locale } = useI18n() const nav = useNavStore() const site = useSiteStore() +const version = useVersionStore()

{{ site.footer || t('footer.copyright') }}

+

+ {{ t('footer.versions', { frontend: version.frontend, backend: version.backendLabel }) + }} | {{ t('footer.build', { build: version.build }) }} +

diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 78aca0e..4ade3cc 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -48,6 +48,8 @@ const enUS: MessageSchema = { }, footer: { copyright: 'Rill streaming platform · Page frame placeholder · All images are blank placeholders', + versions: 'Frontend {frontend} | Backend {backend}', + build: 'Build {build}', }, language: { title: 'Language', diff --git a/frontend/src/i18n/locales/ja-JP.ts b/frontend/src/i18n/locales/ja-JP.ts index 8f1cd7a..38fb22f 100644 --- a/frontend/src/i18n/locales/ja-JP.ts +++ b/frontend/src/i18n/locales/ja-JP.ts @@ -48,6 +48,8 @@ const jaJP: MessageSchema = { }, footer: { copyright: 'Rill 動画配信サービス · ページフレームのプレースホルダー · 画像はすべて空のプレースホルダーです', + versions: 'フロントエンド {frontend} | バックエンド {backend}', + build: 'ビルド {build}', }, language: { title: '言語', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 1c0f5bf..ffaff0a 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -46,6 +46,8 @@ const zhCN = { }, footer: { copyright: 'Rill 流媒体服务平台 · 页面框架占位 · 所有图片均为空白占位框', + versions: '前端 {frontend} | 后端 {backend}', + build: '构建 {build}', }, language: { title: '语言', diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 0de6bee..0510503 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -10,6 +10,7 @@ import { setUnauthorizedHandler } from './api/http' import { useAuthStore } from './stores/auth' import { useNavStore } from './stores/nav' import { useSiteStore } from './stores/site' +import { useVersionStore } from './stores/version' const app = createApp(App) const pinia = createPinia() @@ -27,6 +28,9 @@ void site.load() const nav = useNavStore() void nav.load() +const version = useVersionStore() +void version.load() + setUnauthorizedHandler(() => { const wasAuthenticated = auth.isAuthenticated auth.logout() diff --git a/frontend/src/stores/version.ts b/frontend/src/stores/version.ts new file mode 100644 index 0000000..0c389cb --- /dev/null +++ b/frontend/src/stores/version.ts @@ -0,0 +1,33 @@ +import { computed, ref } from 'vue' +import { defineStore } from 'pinia' +import { getHealth } from '@/api/version' + +function buildLabel(): string { + const parts: string[] = [] + if (__GIT_HASH__) { + parts.push(`+${__GIT_HASH__}`) + } + if (__BUILD_TIME__) { + parts.push(__BUILD_TIME__) + } + return parts.join(' · ') +} + +export const useVersionStore = defineStore('version', () => { + const frontend = ref(`v${__APP_VERSION__}`) + const build = ref(buildLabel()) + const backend = ref('') + + const backendLabel = computed(() => (backend.value ? `v${backend.value}` : '—')) + + async function load() { + try { + const info = await getHealth() + backend.value = info.version + } catch { + // 加载失败时显示占位符 + } + } + + return { frontend, build, backend, backendLabel, load } +}) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 34a236e..d7f6ebe 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,3 +1,5 @@ +import { execSync } from 'node:child_process' +import { readFileSync } from 'node:fs' import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' @@ -5,8 +7,33 @@ import vue from '@vitejs/plugin-vue' import vueDevTools from 'vite-plugin-vue-devtools' import tailwindcss from '@tailwindcss/vite' +function gitHash(): string { + try { + return execSync('git rev-parse --short HEAD', { + cwd: fileURLToPath(new URL('.', import.meta.url)), + stdio: ['ignore', 'pipe', 'ignore'], + }) + .toString() + .trim() + } catch { + return '' + } +} + +const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8')) as { + version: string +} + +// 构建时间(本地时区 YYYY-MM-DD HH:mm) +const buildTime = new Date().toLocaleString('sv-SE').slice(0, 16) + // https://vite.dev/config/ export default defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + __GIT_HASH__: JSON.stringify(gitHash()), + __BUILD_TIME__: JSON.stringify(buildTime), + }, plugins: [ vue(), vueDevTools(), diff --git a/internal/api/api.go b/internal/api/api.go index 67e7bfc..ea2fed9 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -20,6 +20,7 @@ import ( "rill/internal/site" "rill/internal/user" "rill/internal/usergroup" + "rill/internal/version" "rill/internal/videocategory" ) @@ -107,12 +108,13 @@ func RegisterRoutes(rg *gin.RouterGroup, db *gorm.DB, cfg *config.Config) { // HealthResponse 健康检查响应。 type HealthResponse struct { - Status string `json:"status" example:"ok"` - Error string `json:"error,omitempty" example:"database unavailable"` + Status string `json:"status" example:"ok"` + Version string `json:"version" example:"0.1.0"` + Error string `json:"error,omitempty" example:"database unavailable"` } // @Summary Health check -// @Description Check service and database connectivity; returns 503 when the database is unavailable. +// @Description Check service and database connectivity; returns 503 when the database is unavailable. The version field reports the current server version. // @Tags public // @Produce json // @Success 200 {object} api.HealthResponse @@ -121,9 +123,9 @@ type HealthResponse struct { func health(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if err := database.Ping(c.Request.Context(), db); err != nil { - c.JSON(http.StatusServiceUnavailable, HealthResponse{Status: "error", Error: "database unavailable"}) + c.JSON(http.StatusServiceUnavailable, HealthResponse{Status: "error", Version: version.Version, Error: "database unavailable"}) return } - c.JSON(http.StatusOK, HealthResponse{Status: "ok"}) + c.JSON(http.StatusOK, HealthResponse{Status: "ok", Version: version.Version}) } } diff --git a/internal/api/api_test.go b/internal/api/api_test.go index d46566c..8057797 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -6,6 +6,7 @@ import ( "testing" "rill/internal/testutil" + "rill/internal/version" ) func TestHealth(t *testing.T) { @@ -23,4 +24,7 @@ func TestHealth(t *testing.T) { if resp["status"] != "ok" { t.Errorf("status = %q, 期望 ok", resp["status"]) } + if resp["version"] != version.Version { + t.Errorf("version = %q, 期望 %q", resp["version"], version.Version) + } } diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..c7d5ccf --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,6 @@ +// Package version 保存服务端版本号,构建时可通过 +// -ldflags "-X rill/internal/version.Version=x.y.z" 覆盖。 +package version + +// Version 当前服务端版本。 +var Version = "0.1.0" diff --git a/main.go b/main.go index 0ae01ad..4dca91f 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ import ( //go:generate go tool swag init -g main.go -o docs --parseInternal // @title Rill API -// @version 1.0 +// @version 0.1.0 // @description Rill server HTTP API documentation. All endpoints are prefixed with api.prefix (default /api); requests and responses are JSON. // @description Swagger UI: {prefix}/swagger/index.html; OpenAPI JSON: {prefix}/swagger/doc.json. // @description Endpoints are grouped by required permission: public needs no authentication, user needs a Bearer JWT obtained from /auth/login, admin needs an admin user.