页脚展示前后端版本与构建信息

- 新增 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 · 时间”,三语文案补齐
This commit is contained in:
2026-09-22 12:04:04 +08:00
parent 55d024ae6c
commit 6f8edb16a8
18 files changed
+128 -15

No files matched your search

+6 -2
View File
@@ -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{},
+6 -2
View File
@@ -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"
}
}
},
+5 -2
View File
@@ -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:
+4
View File
@@ -1 +1,5 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string
declare const __GIT_HASH__: string
declare const __BUILD_TIME__: string
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
+10
View File
@@ -0,0 +1,10 @@
import { request } from './http'
export interface HealthInfo {
status: string
version: string
}
export function getHealth(): Promise<HealthInfo> {
return request<HealthInfo>('/health')
}
@@ -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()
</script>
<template>
@@ -44,5 +46,9 @@ const site = useSiteStore()
</template>
</nav>
<p>{{ site.footer || t('footer.copyright') }}</p>
<p class="mt-4 text-left">
{{ t('footer.versions', { frontend: version.frontend, backend: version.backendLabel })
}}<span v-if="version.build"> {{ t('footer.build', { build: version.build }) }}</span>
</p>
</footer>
</template>
+2
View File
@@ -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',
+2
View File
@@ -48,6 +48,8 @@ const jaJP: MessageSchema = {
},
footer: {
copyright: 'Rill 動画配信サービス · ページフレームのプレースホルダー · 画像はすべて空のプレースホルダーです',
versions: 'フロントエンド {frontend} バックエンド {backend}',
build: 'ビルド {build}',
},
language: {
title: '言語',
+2
View File
@@ -46,6 +46,8 @@ const zhCN = {
},
footer: {
copyright: 'Rill 流媒体服务平台 · 页面框架占位 · 所有图片均为空白占位框',
versions: '前端 {frontend} 后端 {backend}',
build: '构建 {build}',
},
language: {
title: '语言',
+4
View File
@@ -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()
+33
View File
@@ -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 }
})
+27
View File
@@ -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(),
+7 -5
View File
@@ -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})
}
}
+4
View File
@@ -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)
}
}
+6
View File
@@ -0,0 +1,6 @@
// Package version 保存服务端版本号,构建时可通过
// -ldflags "-X rill/internal/version.Version=x.y.z" 覆盖。
package version
// Version 当前服务端版本。
var Version = "0.1.0"
+1 -1
View File
@@ -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.