Compare commits

..

3 Commits

Author SHA1 Message Date
83f3c6e11f feat: docker. 2025-03-20 17:02:12 +08:00
8911c6708a feat: predict value. 2025-03-20 16:42:27 +08:00
c09b51ba02 feat: auth. 2025-03-20 16:40:39 +08:00
7 changed files with 167 additions and 10 deletions

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
# 构建阶段
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# 运行阶段
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
COPY config.yaml ./
EXPOSE 8080
CMD ["./main"]

33
docker-compose.yml Normal file
View File

@ -0,0 +1,33 @@
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
environment:
- TZ=Asia/Shanghai
volumes:
- ./config.yaml:/app/config.yaml
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: root
POSTGRES_DB: training_db
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
ports:
- "5432:5432"
volumes:
pgdata:

3
go.mod
View File

@ -4,7 +4,8 @@ go 1.23.3
require (
github.com/gin-gonic/gin v1.10.0
github.com/lib/pq v1.10.9
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/spf13/viper v1.20.0
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12

6
go.sum
View File

@ -31,6 +31,10 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -58,8 +62,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

84
middleware/auth.go Normal file
View File

@ -0,0 +1,84 @@
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"net/http"
"strings"
"time"
)
const (
ApiSecret = "your-super-secret-key" // 预共享密钥
TokenExp = 1 * time.Hour // Token有效期
)
type JWTService struct {
secretKey []byte
expiresIn time.Duration
}
func NewJWTService(secret string, expiresIn time.Duration) *JWTService {
return &JWTService{
secretKey: []byte(secret),
expiresIn: expiresIn,
}
}
// 生成带HMAC签名的Token
func (s *JWTService) GenerateToken() (string, error) {
claims := jwt.MapClaims{
"exp": time.Now().Add(s.expiresIn).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.secretKey)
}
// 验证HMAC签名的Token
func (s *JWTService) ValidateToken(tokenString string) (jwt.Claims, error) {
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return s.secretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrTokenInvalidClaims
}
// 鉴权中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(ApiSecret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
c.Next()
}
}

View File

@ -26,6 +26,7 @@ type HeartRate struct {
BeltAddr string `gorm:"column:belt_addr;size:255" json:"beltAddr"` // 对应Dart的beltAddr字段
Time int64 `gorm:"type:bigint" json:"time"` // 保持与前端一致的毫秒时间戳[3](@ref)
Value int `gorm:"type:int" json:"value"`
PredictValue int `gorm:"type:int" json:"predictValue"`
Identifier string `gorm:"uniqueIndex;type:varchar(255)" json:"identifier"`
}

View File

@ -4,20 +4,41 @@ import (
"github.com/gin-gonic/gin"
"hr_receiver/controllers"
"hr_receiver/middleware"
"net/http"
)
func SetupRouter() *gin.Engine {
jwtService := middleware.NewJWTService(middleware.ApiSecret, middleware.TokenExp)
r := gin.Default()
r.Use(middleware.GzipMiddleware())
trainingController := controllers.NewTrainingController()
v1 := r.Group("/api/v1")
{
records := v1.Group("/train-records")
records := v1.Group("/train-records").Use(middleware.AuthMiddleware())
{
records.POST("", trainingController.CreateTrainingRecord)
// 可扩展其他路由GET, PUT, DELETE等
}
auth := v1.Group("/auth")
{
auth.GET("/token", func(c *gin.Context) {
clientSecret := c.GetHeader("X-API-Key")
if clientSecret != middleware.ApiSecret {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid secret"})
return
}
token, err := jwtService.GenerateToken()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
})
}
}
return r
}