fix(server): 安全加固与版本链路修复
- 微信登录改为真实 jscode2session,session_key 不再下发客户端 - 支付回调增加 HMAC 验签(X-Pay-Sign)与幂等处理,未配置密钥时拒绝回调 - 订单金额一律以服务端商品表定价,禁止客户端传入金额 - 付费许愿改为支付成功后创建,不再先许愿后付款 - 管理后台增加登录认证(ADMIN_PASSWORD + role=admin JWT + HttpOnly Cookie) - 订单详情/取消增加本人归属校验,修复越权访问 - 版本信息改为 ldflags 注入单一链路,GoVersion 用 runtime.Version() - 恢复 gin 默认访问日志(原 Logger 中间件输出为空) - 加载 HTML 模板修复后台页面 500;godotenv 加载 .env.local - CORS 支持 CORS_ORIGINS 白名单配置
This commit is contained in:
@@ -1,47 +1,108 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gouki/lunar-server/internal/config"
|
||||
"github.com/gouki/lunar-server/internal/service"
|
||||
)
|
||||
|
||||
// adminPage 统一的管理后台页面渲染(补充 Inertia 需要的 url 属性)
|
||||
func adminPage(c *gin.Context, title, page string) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": title,
|
||||
"page": page,
|
||||
"url": c.Request.URL.Path,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminLogin 管理员登录:密码正确签发 role=admin 的 JWT
|
||||
func AdminLogin(c *gin.Context) {
|
||||
cfg := config.Load()
|
||||
if cfg.Admin.Password == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"code": 503,
|
||||
"msg": "后台登录未启用",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 恒定时间比较,防时序攻击
|
||||
if subtle.ConstantTimeCompare([]byte(req.Password), []byte(cfg.Admin.Password)) != 1 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"msg": "密码错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userService := service.NewUserService()
|
||||
token, err := userService.GenerateAdminToken(cfg.JWT.Secret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "生成token失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 同时写入 HttpOnly Cookie,支持浏览器直接导航后台页面
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
c.SetCookie(service.AdminTokenCookie, token, 12*3600, "/admin", "", cfg.Server.Env == "production", true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": gin.H{
|
||||
"token": token,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AdminLogout 管理员登出:清除会话 Cookie
|
||||
func AdminLogout(c *gin.Context) {
|
||||
cfg := config.Load()
|
||||
c.SetCookie(service.AdminTokenCookie, "", -1, "/admin", "", cfg.Server.Env == "production", true)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// AdminDashboard 管理后台首页
|
||||
func AdminDashboard(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "管理后台",
|
||||
"page": "dashboard",
|
||||
})
|
||||
adminPage(c, "管理后台", "dashboard")
|
||||
}
|
||||
|
||||
// AdminUsers 用户管理
|
||||
func AdminUsers(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "用户管理",
|
||||
"page": "users",
|
||||
})
|
||||
adminPage(c, "用户管理", "users")
|
||||
}
|
||||
|
||||
// AdminOrders 订单管理
|
||||
func AdminOrders(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "订单管理",
|
||||
"page": "orders",
|
||||
})
|
||||
adminPage(c, "订单管理", "orders")
|
||||
}
|
||||
|
||||
// AdminWishes 许愿管理
|
||||
func AdminWishes(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "许愿管理",
|
||||
"page": "wishes",
|
||||
})
|
||||
adminPage(c, "许愿管理", "wishes")
|
||||
}
|
||||
|
||||
// AdminSettings 系统设置
|
||||
func AdminSettings(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "系统设置",
|
||||
"page": "settings",
|
||||
})
|
||||
adminPage(c, "系统设置", "settings")
|
||||
}
|
||||
|
||||
@@ -43,14 +43,16 @@ func GetOrderList(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetOrderDetail 获取订单详情
|
||||
// GetOrderDetail 获取订单详情(仅限本人订单)
|
||||
func GetOrderDetail(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
id := c.Param("id")
|
||||
orderID, _ := strconv.Atoi(id)
|
||||
|
||||
orderService := service.NewOrderService()
|
||||
order, err := orderService.GetOrderByID(uint(orderID))
|
||||
if err != nil {
|
||||
if err != nil || order.UserID != userID.(uint) {
|
||||
// 不存在与无权访问统一返回 404,避免枚举他人订单
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
"msg": "订单不存在",
|
||||
@@ -65,16 +67,17 @@ func GetOrderDetail(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// CancelOrder 取消订单
|
||||
// CancelOrder 取消订单(仅限本人待支付订单)
|
||||
func CancelOrder(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
id := c.Param("id")
|
||||
orderID, _ := strconv.Atoi(id)
|
||||
|
||||
orderService := service.NewOrderService()
|
||||
if err := orderService.CancelOrder(uint(orderID)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "取消订单失败",
|
||||
if err := orderService.CancelOrder(userID.(uint), uint(orderID)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
+103
-38
@@ -1,14 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gouki/lunar-server/internal/config"
|
||||
"github.com/gouki/lunar-server/internal/model"
|
||||
"github.com/gouki/lunar-server/internal/service"
|
||||
)
|
||||
|
||||
// CreateOrder 创建订单
|
||||
// 安全约束:金额与商品名一律以服务端商品表为准,不信任客户端传入值
|
||||
func CreateOrder(c *gin.Context) {
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
@@ -20,11 +26,9 @@ func CreateOrder(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"` // wish:许愿 vip:会员
|
||||
ProductID uint `json:"productId" binding:"required"` // 商品ID
|
||||
ProductName string `json:"productName" binding:"required"` // 商品名称
|
||||
Amount int `json:"amount" binding:"required"` // 金额(分)
|
||||
Content string `json:"content"` // 许愿内容(许愿类型需要)
|
||||
Type string `json:"type" binding:"required"` // wish:许愿 vip:会员
|
||||
ProductID uint `json:"productId" binding:"required"` // 商品ID
|
||||
Content string `json:"content"` // 许愿内容(许愿类型需要)
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -35,14 +39,48 @@ func CreateOrder(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
orderService := service.NewOrderService()
|
||||
// 服务端定价:按商品 ID 查库取价格,防止客户端篡改金额
|
||||
wishService := service.NewWishService()
|
||||
product, err := wishService.GetWishProductByID(req.ProductID)
|
||||
if err != nil || product.Status != 1 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "商品不存在或已下架",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type == "wish" {
|
||||
if req.Content == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "请输入许愿内容",
|
||||
})
|
||||
return
|
||||
}
|
||||
if len([]rune(req.Content)) > 100 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "许愿内容超过 100 字限制",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "不支持的订单类型",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderService := service.NewOrderService()
|
||||
order := &model.Order{
|
||||
UserID: userID.(uint),
|
||||
Type: req.Type,
|
||||
ProductID: req.ProductID,
|
||||
ProductName: req.ProductName,
|
||||
Amount: req.Amount,
|
||||
ProductID: product.ID,
|
||||
ProductName: product.Name,
|
||||
Amount: product.Price, // 金额以商品表为准
|
||||
Remark: req.Content, // 许愿内容暂存订单,支付成功后才创建许愿
|
||||
}
|
||||
|
||||
if err := orderService.CreateOrder(order); err != nil {
|
||||
@@ -53,19 +91,6 @@ func CreateOrder(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是许愿类型,创建许愿记录
|
||||
if req.Type == "wish" && req.Content != "" {
|
||||
wishService := service.NewWishService()
|
||||
wish := &model.Wish{
|
||||
UserID: userID.(uint),
|
||||
TreeID: 1, // 默认许愿树
|
||||
Content: req.Content,
|
||||
Type: "paid",
|
||||
Status: 1,
|
||||
}
|
||||
wishService.CreateWish(wish)
|
||||
}
|
||||
|
||||
// 创建微信支付订单
|
||||
payParams, err := orderService.CreateWechatPayOrder(order, "")
|
||||
if err != nil {
|
||||
@@ -87,12 +112,29 @@ func CreateOrder(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// PayNotify 支付回调
|
||||
// paySignOf 计算支付回调签名:HMAC-SHA256(orderNo|transactionId, APIKEY)
|
||||
// 过渡方案:真实微信支付 V3 回调验签接入前的内部协议
|
||||
func paySignOf(orderNo, transactionID, key string) string {
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(orderNo + "|" + transactionID))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// PayNotify 支付回调:必须携带 X-Pay-Sign 签名头,验签通过且幂等处理
|
||||
func PayNotify(c *gin.Context) {
|
||||
// TODO: 验证微信支付回调签名
|
||||
cfg := config.Load()
|
||||
if cfg.Wechat.PayKey == "" {
|
||||
// 未配置支付密钥时拒绝一切回调,避免裸奔
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"code": "FAIL",
|
||||
"msg": "支付服务未配置",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
OrderNo string `json:"orderNo"`
|
||||
TransactionID string `json:"transactionId"`
|
||||
OrderNo string `json:"orderNo" binding:"required"`
|
||||
TransactionID string `json:"transactionId" binding:"required"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
@@ -104,15 +146,32 @@ func PayNotify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status == "SUCCESS" {
|
||||
orderService := service.NewOrderService()
|
||||
if err := orderService.HandlePayNotify(req.OrderNo, req.TransactionID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": "FAIL",
|
||||
"msg": "处理失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 验签:恒定时间比较,防时序攻击
|
||||
sign := c.GetHeader("X-Pay-Sign")
|
||||
expected := paySignOf(req.OrderNo, req.TransactionID, cfg.Wechat.PayKey)
|
||||
if sign == "" || !hmac.Equal([]byte(sign), []byte(expected)) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": "FAIL",
|
||||
"msg": "签名验证失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != "SUCCESS" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": "SUCCESS",
|
||||
"msg": "OK",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderService := service.NewOrderService()
|
||||
if err := orderService.HandlePayNotify(req.OrderNo, req.TransactionID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": "FAIL",
|
||||
"msg": "处理失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -121,12 +180,18 @@ func PayNotify(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetPayStatus 获取支付状态
|
||||
// GetPayStatus 获取支付状态(支持订单号或订单 ID)
|
||||
func GetPayStatus(c *gin.Context) {
|
||||
orderID := c.Param("orderId")
|
||||
param := c.Param("orderId")
|
||||
|
||||
orderService := service.NewOrderService()
|
||||
order, err := orderService.GetOrderByOrderNo(orderID)
|
||||
order, err := orderService.GetOrderByOrderNo(param)
|
||||
if err != nil {
|
||||
// 兼容传数字 ID 的调用方
|
||||
if id, convErr := strconv.Atoi(param); convErr == nil {
|
||||
order, err = orderService.GetOrderByID(uint(id))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
|
||||
+101
-31
@@ -1,7 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gouki/lunar-server/internal/config"
|
||||
@@ -9,7 +14,71 @@ import (
|
||||
"github.com/gouki/lunar-server/internal/service"
|
||||
)
|
||||
|
||||
// UserLogin 用户登录
|
||||
// code2Session 调用微信 jscode2session 接口换取 openid
|
||||
// session_key 仅保存在服务端,绝不下发给客户端
|
||||
func code2Session(code string) (openID, unionID string, err error) {
|
||||
cfg := config.Load()
|
||||
if cfg.Wechat.AppID == "" || cfg.Wechat.AppSecret == "" {
|
||||
return "", "", fmt.Errorf("wechat appid/secret not configured")
|
||||
}
|
||||
|
||||
query := url.Values{
|
||||
"appid": {cfg.Wechat.AppID},
|
||||
"secret": {cfg.Wechat.AppSecret},
|
||||
"js_code": {code},
|
||||
"grant_type": {"authorization_code"},
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get("https://api.weixin.qq.com/sns/jscode2session?" + query.Encode())
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("jscode2session request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("read jscode2session response failed: %w", err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
OpenID string `json:"openid"`
|
||||
UnionID string `json:"unionid"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", "", fmt.Errorf("parse jscode2session response failed: %w", err)
|
||||
}
|
||||
if result.ErrCode != 0 || result.OpenID == "" {
|
||||
return "", "", fmt.Errorf("wechat auth failed: errcode=%d errmsg=%s", result.ErrCode, result.ErrMsg)
|
||||
}
|
||||
return result.OpenID, result.UnionID, nil
|
||||
}
|
||||
|
||||
// upsertUser 根据 openid 获取用户,不存在则创建
|
||||
func upsertUser(openID, unionID string) (*model.User, error) {
|
||||
userService := service.NewUserService()
|
||||
user, err := userService.GetUserByOpenID(openID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
user = &model.User{
|
||||
OpenID: openID,
|
||||
UnionID: unionID,
|
||||
Nickname: "微信用户",
|
||||
Status: 1,
|
||||
}
|
||||
if err := userService.CreateUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UserLogin 用户登录(小程序 wx.login 的 code 换取 token)
|
||||
func UserLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
@@ -23,38 +92,35 @@ func UserLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 调用微信接口获取 openid
|
||||
// 这里模拟返回
|
||||
openID := "mock_openid_" + req.Code
|
||||
|
||||
userService := service.NewUserService()
|
||||
user, err := userService.GetUserByOpenID(openID)
|
||||
openID, unionID, err := code2Session(req.Code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "服务器错误",
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"msg": "微信登录失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果用户不存在,创建新用户
|
||||
if user == nil {
|
||||
user = &model.User{
|
||||
OpenID: openID,
|
||||
Nickname: "微信用户",
|
||||
Status: 1,
|
||||
}
|
||||
if err := userService.CreateUser(user); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "创建用户失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
user, err := upsertUser(openID, unionID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "创建用户失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 禁用用户不允许登录
|
||||
if user.Status != 1 {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 403,
|
||||
"msg": "账号已被禁用",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成 token
|
||||
cfg := config.Load()
|
||||
userService := service.NewUserService()
|
||||
token, err := userService.GenerateToken(user.ID, cfg.JWT.Secret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
@@ -217,7 +283,7 @@ func UpdateUserProfile(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// WechatAuth 微信授权
|
||||
// WechatAuth 微信授权(仅返回 openid,session_key 属敏感凭证不下发)
|
||||
func WechatAuth(c *gin.Context) {
|
||||
var req struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
@@ -231,16 +297,20 @@ func WechatAuth(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 调用微信接口获取 openid 和 session_key
|
||||
// 这里模拟返回
|
||||
openID := "mock_openid_" + req.Code
|
||||
openID, _, err := code2Session(req.Code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"msg": "微信授权失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": gin.H{
|
||||
"openid": openID,
|
||||
"sessionKey": "mock_session_key",
|
||||
"openid": openID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,11 +2,18 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 版本信息:由 cmd/main.go 将 CI 的 ldflags 注入值赋入,保证单一数据源
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildTime = "unknown"
|
||||
CommitSha = "unknown"
|
||||
)
|
||||
|
||||
// VersionInfo 版本信息
|
||||
type VersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
@@ -17,26 +24,10 @@ type VersionInfo struct {
|
||||
|
||||
// GetVersion 获取服务器版本信息
|
||||
func GetVersion(c *gin.Context) {
|
||||
// 从环境变量读取版本信息(由 CI 构建时注入)
|
||||
version := os.Getenv("APP_VERSION")
|
||||
if version == "" {
|
||||
version = "dev"
|
||||
}
|
||||
|
||||
buildTime := os.Getenv("APP_BUILD_TIME")
|
||||
if buildTime == "" {
|
||||
buildTime = "unknown"
|
||||
}
|
||||
|
||||
commitSha := os.Getenv("APP_COMMIT_SHA")
|
||||
if commitSha == "" {
|
||||
commitSha = "unknown"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, VersionInfo{
|
||||
Version: version,
|
||||
BuildTime: buildTime,
|
||||
CommitSha: commitSha,
|
||||
GoVersion: "go1.22", // 可以通过 runtime.Version() 获取
|
||||
Version: Version,
|
||||
BuildTime: BuildTime,
|
||||
CommitSha: CommitSha,
|
||||
GoVersion: runtime.Version(),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user