From bd4e71203152cdb31ecb939c6113e9db78d1d767 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期一, 05 十月 2026 11:57:04 +0800
Subject: [PATCH] docs: fix handoff todo section reference

---
 traffic-audit-server/src/main/java/com/trafficaudit/system/controller/VersionController.java |   83 +++++++++++++++++++++++++++++++++++++++++
 1 files changed, 83 insertions(+), 0 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/system/controller/VersionController.java b/traffic-audit-server/src/main/java/com/trafficaudit/system/controller/VersionController.java
new file mode 100644
index 0000000..a4d9de6
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/system/controller/VersionController.java
@@ -0,0 +1,83 @@
+package com.trafficaudit.system.controller;
+
+import com.trafficaudit.common.Result;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Exposes the product version so the UI (and operators) can confirm which build
+ * is actually running on a server. Kept public on purpose: the login page shows
+ * the version before any token exists.
+ */
+@RestController
+@RequestMapping("/api")
+public class VersionController {
+
+    private static final String VERSION_FILE = "app-version.properties";
+
+    @GetMapping("/version")
+    public Result<Map<String, Object>> version() {
+        Map<String, Object> info = new LinkedHashMap<>();
+        info.put("version", readVersion());
+        info.put("buildTime", resolveBuildTime());
+        return Result.ok(info);
+    }
+
+    private String readVersion() {
+        try (InputStream in = new ClassPathResource(VERSION_FILE).getInputStream()) {
+            Properties props = new Properties();
+            props.load(in);
+            String value = props.getProperty("version");
+            return value == null || value.trim().isEmpty() ? "unknown" : value.trim();
+        } catch (Exception e) {
+            return "unknown";
+        }
+    }
+
+    /**
+     * Build time of the running artifact. In a packaged run this is the jar file's
+     * last-modified time; when running from exploded classes (mvn spring-boot:run)
+     * there is no jar, so the value is null.
+     */
+    private String resolveBuildTime() {
+        try {
+            URI location = VersionController.class.getProtectionDomain()
+                    .getCodeSource().getLocation().toURI();
+            Path path = resolveArtifactPath(location);
+            if (path != null && Files.isRegularFile(path)) {
+                return Files.getLastModifiedTime(path).toInstant().toString();
+            }
+        } catch (Exception ignored) {
+            // Fall through: build time is optional information.
+        }
+        return null;
+    }
+
+    /**
+     * Spring Boot executable jars expose a nested classpath such as
+     * "jar:file:/app.jar!/BOOT-INF/classes!/". Strip the nested part before
+     * asking the filesystem for the jar timestamp.
+     */
+    private Path resolveArtifactPath(URI location) {
+        String value = location.toString();
+        int nested = value.indexOf("!/");
+        if (nested >= 0) {
+            value = value.substring(0, nested);
+        }
+        if (value.startsWith("jar:")) {
+            value = value.substring(4);
+        }
+        return Paths.get(URI.create(value));
+    }
+}

--
Gitblit v1.9.1