增加数据库模块
This commit is contained in:
12 files changed
+829
-5
No files matched your search
@@ -5,20 +5,30 @@ go 1.26.4
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.8
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.3 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -28,6 +38,7 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.60.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
|
||||
@@ -37,4 +48,8 @@ require (
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
|
||||
@@ -9,6 +11,8 @@ github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/cors v1.7.8 h1:U0jjyXlWXMEx27hFE6hmdg/TOZviYgsOGGT+kLmUZrc=
|
||||
@@ -17,6 +21,10 @@ github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -25,6 +33,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
@@ -32,6 +42,14 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
@@ -40,6 +58,8 @@ 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/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -55,6 +75,9 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -90,3 +113,17 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
@@ -0,0 +1,35 @@
|
||||
// Package api 提供 HTTP 接口路由。
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"rill/internal/database"
|
||||
)
|
||||
|
||||
// RegisterRoutes 注册 API 路由。
|
||||
func RegisterRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
rg.GET("/health", health(db))
|
||||
|
||||
notes := rg.Group("/notes")
|
||||
{
|
||||
notes.GET("", listNotes(db))
|
||||
notes.POST("", createNote(db))
|
||||
notes.GET("/:id", getNote(db))
|
||||
notes.PUT("/:id", updateNote(db))
|
||||
notes.DELETE("/:id", deleteNote(db))
|
||||
}
|
||||
}
|
||||
|
||||
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, gin.H{"status": "error", "error": "数据库不可用"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"rill/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPageSize = 20
|
||||
maxPageSize = 100
|
||||
)
|
||||
|
||||
type noteRequest struct {
|
||||
Title string `json:"title" binding:"required,max=200"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func listNotes(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", strconv.Itoa(defaultPageSize)))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > maxPageSize {
|
||||
pageSize = defaultPageSize
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var total int64
|
||||
if err := db.WithContext(ctx).Model(&model.Note{}).Count(&total).Error; err != nil {
|
||||
respondDBError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
var notes []model.Note
|
||||
if err := db.WithContext(ctx).
|
||||
Order("id DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(¬es).Error; err != nil {
|
||||
respondDBError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": notes,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createNote(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req noteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
note := model.Note{Title: req.Title, Content: req.Content}
|
||||
if err := db.WithContext(c.Request.Context()).Create(¬e).Error; err != nil {
|
||||
respondDBError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, note)
|
||||
}
|
||||
}
|
||||
|
||||
func getNote(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var note model.Note
|
||||
if err := db.WithContext(c.Request.Context()).First(¬e, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在"})
|
||||
return
|
||||
}
|
||||
respondDBError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, note)
|
||||
}
|
||||
}
|
||||
|
||||
func updateNote(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req noteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var note model.Note
|
||||
if err := db.WithContext(ctx).First(¬e, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在"})
|
||||
return
|
||||
}
|
||||
respondDBError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
note.Title = req.Title
|
||||
note.Content = req.Content
|
||||
if err := db.WithContext(ctx).Save(¬e).Error; err != nil {
|
||||
respondDBError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, note)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteNote(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
result := db.WithContext(c.Request.Context()).Delete(&model.Note{}, id)
|
||||
if result.Error != nil {
|
||||
respondDBError(c, result.Error)
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id 无效"})
|
||||
return 0, false
|
||||
}
|
||||
return uint(id), true
|
||||
}
|
||||
|
||||
func respondDBError(c *gin.Context, err error) {
|
||||
slog.ErrorContext(c.Request.Context(), "数据库操作失败", "err", err, "path", c.Request.URL.Path)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器内部错误"})
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"rill/internal/config"
|
||||
"rill/internal/database"
|
||||
"rill/internal/model"
|
||||
)
|
||||
|
||||
func setupRouter(t *testing.T) *gin.Engine {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
cfg := &config.Config{
|
||||
Database: config.DatabaseConfig{
|
||||
Driver: "sqlite3",
|
||||
ConnectTimeout: "5s",
|
||||
SQLite: config.SQLiteConfig{Path: filepath.Join(t.TempDir(), "test.db")},
|
||||
},
|
||||
}
|
||||
db, err := database.Open(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("打开测试数据库失败: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := database.Close(db); err != nil {
|
||||
t.Errorf("关闭测试数据库失败: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := database.Migrate(context.Background(), db); err != nil {
|
||||
t.Fatalf("执行测试迁移失败: %v", err)
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
RegisterRoutes(r.Group("/api"), db)
|
||||
return r
|
||||
}
|
||||
|
||||
func call(t *testing.T, r http.Handler, method, path string, body any) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
var payload []byte
|
||||
if body != nil {
|
||||
var err error
|
||||
payload, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatalf("序列化请求体失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, path, bytes.NewReader(payload))
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func decodeNote(t *testing.T, w *httptest.ResponseRecorder) model.Note {
|
||||
t.Helper()
|
||||
var note model.Note
|
||||
if err := json.Unmarshal(w.Body.Bytes(), ¬e); err != nil {
|
||||
t.Fatalf("解析响应失败: %v, body=%s", err, w.Body.String())
|
||||
}
|
||||
return note
|
||||
}
|
||||
|
||||
func TestHealth(t *testing.T) {
|
||||
r := setupRouter(t)
|
||||
|
||||
w := call(t, r, http.MethodGet, "/api/health", nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("状态码 = %d, 期望 %d, body=%s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("解析响应失败: %v", err)
|
||||
}
|
||||
if resp["status"] != "ok" {
|
||||
t.Errorf("status = %q, 期望 ok", resp["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoteCRUD(t *testing.T) {
|
||||
r := setupRouter(t)
|
||||
|
||||
w := call(t, r, http.MethodPost, "/api/notes", map[string]string{"title": "第一条", "content": "内容"})
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("创建状态码 = %d, 期望 %d, body=%s", w.Code, http.StatusCreated, w.Body.String())
|
||||
}
|
||||
created := decodeNote(t, w)
|
||||
if created.ID == 0 || created.Title != "第一条" || created.Content != "内容" {
|
||||
t.Fatalf("创建结果异常: %+v", created)
|
||||
}
|
||||
|
||||
detailPath := fmt.Sprintf("/api/notes/%d", created.ID)
|
||||
w = call(t, r, http.MethodGet, detailPath, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("详情状态码 = %d, 期望 %d", w.Code, http.StatusOK)
|
||||
}
|
||||
if got := decodeNote(t, w); got.ID != created.ID {
|
||||
t.Errorf("详情 ID = %d, 期望 %d", got.ID, created.ID)
|
||||
}
|
||||
|
||||
w = call(t, r, http.MethodPut, detailPath, map[string]string{"title": "已更新", "content": "新内容"})
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("更新状态码 = %d, 期望 %d, body=%s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
updated := decodeNote(t, w)
|
||||
if updated.Title != "已更新" || updated.Content != "新内容" {
|
||||
t.Errorf("更新结果异常: %+v", updated)
|
||||
}
|
||||
|
||||
w = call(t, r, http.MethodGet, "/api/notes?page=1&page_size=10", nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("列表状态码 = %d, 期望 %d", w.Code, http.StatusOK)
|
||||
}
|
||||
var list struct {
|
||||
Items []model.Note `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
|
||||
t.Fatalf("解析列表响应失败: %v", err)
|
||||
}
|
||||
if list.Total != 1 || len(list.Items) != 1 {
|
||||
t.Fatalf("列表结果异常: total=%d, items=%d", list.Total, len(list.Items))
|
||||
}
|
||||
if list.Items[0].Title != "已更新" {
|
||||
t.Errorf("列表项标题 = %q, 期望 已更新", list.Items[0].Title)
|
||||
}
|
||||
|
||||
w = call(t, r, http.MethodDelete, detailPath, nil)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("删除状态码 = %d, 期望 %d", w.Code, http.StatusNoContent)
|
||||
}
|
||||
|
||||
w = call(t, r, http.MethodGet, detailPath, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("删除后详情状态码 = %d, 期望 %d", w.Code, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoteValidation(t *testing.T) {
|
||||
r := setupRouter(t)
|
||||
|
||||
w := call(t, r, http.MethodPost, "/api/notes", map[string]string{"content": "缺少标题"})
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("缺少标题状态码 = %d, 期望 %d", w.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
w = call(t, r, http.MethodGet, "/api/notes/abc", nil)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("非法 id 状态码 = %d, 期望 %d", w.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
w = call(t, r, http.MethodGet, "/api/notes/9999", nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("不存在记录状态码 = %d, 期望 %d", w.Code, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ api:
|
||||
allow_credentials: false
|
||||
max_age: "12h"
|
||||
|
||||
# 数据库(当前仅配置,服务暂不建立连接)
|
||||
# 数据库
|
||||
database:
|
||||
driver: sqlite3 # sqlite3 / mysql
|
||||
connect_timeout: "10s" # 建立连接超时
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Package database 负责数据库连接的建立、健康检查与关闭。
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"rill/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
driverSQLite = "sqlite3"
|
||||
driverMySQL = "mysql"
|
||||
)
|
||||
|
||||
// Open 依据配置建立数据库连接,并在 connect_timeout 内完成连通性检查。
|
||||
func Open(cfg *config.Config) (*gorm.DB, error) {
|
||||
timeout, err := time.ParseDuration(cfg.Database.ConnectTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析 database.connect_timeout 失败: %w", err)
|
||||
}
|
||||
|
||||
dialector, err := newDialector(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := gorm.Open(dialector, &gorm.Config{
|
||||
Logger: newLogger(cfg.LogLevel()),
|
||||
DisableAutomaticPing: true,
|
||||
TranslateError: true,
|
||||
DisableForeignKeyConstraintWhenMigrating: cfg.Database.Driver == driverSQLite,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开数据库失败: %w", err)
|
||||
}
|
||||
|
||||
if err := applyPool(db, cfg); err != nil {
|
||||
Close(db)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
if err := Ping(ctx, db); err != nil {
|
||||
Close(db)
|
||||
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func newDialector(cfg *config.Config) (gorm.Dialector, error) {
|
||||
switch cfg.Database.Driver {
|
||||
case driverSQLite:
|
||||
path := filepath.Clean(cfg.Database.SQLite.Path)
|
||||
if dir := filepath.Dir(path); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("创建数据库目录 %s 失败: %w", dir, err)
|
||||
}
|
||||
}
|
||||
dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)", path)
|
||||
return sqlite.Open(dsn), nil
|
||||
case driverMySQL:
|
||||
m := cfg.Database.MySQL
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
|
||||
m.User, m.Password, m.Host, m.Port, m.Database, m.Charset)
|
||||
return mysql.Open(dsn), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的数据库驱动: %q", cfg.Database.Driver)
|
||||
}
|
||||
}
|
||||
|
||||
// applyPool 应用连接池配置:SQLite 使用单连接串行化写入,避免并发写触发 SQLITE_BUSY。
|
||||
func applyPool(db *gorm.DB, cfg *config.Config) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取数据库连接池失败: %w", err)
|
||||
}
|
||||
|
||||
if cfg.Database.Driver == driverMySQL {
|
||||
m := cfg.Database.MySQL
|
||||
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
|
||||
if lifetime, err := time.ParseDuration(m.ConnMaxLifetime); err == nil {
|
||||
sqlDB.SetConnMaxLifetime(lifetime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping 检查数据库连通性。
|
||||
func Ping(ctx context.Context, db *gorm.DB) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接池。
|
||||
func Close(db *gorm.DB) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"rill/internal/config"
|
||||
"rill/internal/model"
|
||||
)
|
||||
|
||||
func testConfig(t *testing.T) *config.Config {
|
||||
t.Helper()
|
||||
return &config.Config{
|
||||
Database: config.DatabaseConfig{
|
||||
Driver: driverSQLite,
|
||||
ConnectTimeout: "5s",
|
||||
SQLite: config.SQLiteConfig{Path: filepath.Join(t.TempDir(), "test.db")},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func openTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := Open(testConfig(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Open 失败: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := Close(db); err != nil {
|
||||
t.Errorf("Close 失败: %v", err)
|
||||
}
|
||||
})
|
||||
return db
|
||||
}
|
||||
|
||||
func TestOpenAndPing(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
|
||||
if err := Ping(context.Background(), db); err != nil {
|
||||
t.Fatalf("Ping 失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenUnsupportedDriver(t *testing.T) {
|
||||
cfg := testConfig(t)
|
||||
cfg.Database.Driver = "postgres"
|
||||
|
||||
if _, err := Open(cfg); err == nil {
|
||||
t.Fatal("期望不支持的驱动返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateIdempotentAndCRUD(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 1; i <= 2; i++ {
|
||||
if err := Migrate(ctx, db); err != nil {
|
||||
t.Fatalf("第 %d 次 Migrate 失败: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !db.Migrator().HasTable(&model.Note{}) {
|
||||
t.Error("notes 表未创建")
|
||||
}
|
||||
if !db.Migrator().HasTable(&schemaMigration{}) {
|
||||
t.Error("schema_migrations 表未创建")
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := db.Model(&schemaMigration{}).Count(&count).Error; err != nil {
|
||||
t.Fatalf("统计迁移记录失败: %v", err)
|
||||
}
|
||||
if count != int64(len(migrations)) {
|
||||
t.Errorf("迁移记录数 = %d, 期望 %d", count, len(migrations))
|
||||
}
|
||||
|
||||
note := model.Note{Title: "标题", Content: "内容"}
|
||||
if err := db.WithContext(ctx).Create(¬e).Error; err != nil {
|
||||
t.Fatalf("创建记录失败: %v", err)
|
||||
}
|
||||
|
||||
var got model.Note
|
||||
if err := db.WithContext(ctx).First(&got, note.ID).Error; err != nil {
|
||||
t.Fatalf("查询记录失败: %v", err)
|
||||
}
|
||||
if got.Title != note.Title || got.Content != note.Content {
|
||||
t.Errorf("查询结果 = %+v, 期望 Title=%q Content=%q", got, note.Title, note.Content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
gormlogger "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// slowThreshold 超过该耗时的 SQL 记为慢查询。
|
||||
const slowThreshold = 500 * time.Millisecond
|
||||
|
||||
// slogLogger 将 GORM 日志转接到 slog。
|
||||
type slogLogger struct {
|
||||
logger *slog.Logger
|
||||
level gormlogger.LogLevel
|
||||
}
|
||||
|
||||
func newLogger(level slog.Level) gormlogger.Interface {
|
||||
return &slogLogger{
|
||||
logger: slog.Default(),
|
||||
level: gormLogLevel(level),
|
||||
}
|
||||
}
|
||||
|
||||
func gormLogLevel(level slog.Level) gormlogger.LogLevel {
|
||||
switch {
|
||||
case level <= slog.LevelDebug:
|
||||
return gormlogger.Info
|
||||
case level <= slog.LevelWarn:
|
||||
return gormlogger.Warn
|
||||
default:
|
||||
return gormlogger.Error
|
||||
}
|
||||
}
|
||||
|
||||
func (l *slogLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
|
||||
next := *l
|
||||
next.level = level
|
||||
return &next
|
||||
}
|
||||
|
||||
func (l *slogLogger) Info(ctx context.Context, msg string, args ...any) {
|
||||
if l.level >= gormlogger.Info {
|
||||
l.logger.InfoContext(ctx, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *slogLogger) Warn(ctx context.Context, msg string, args ...any) {
|
||||
if l.level >= gormlogger.Warn {
|
||||
l.logger.WarnContext(ctx, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *slogLogger) Error(ctx context.Context, msg string, args ...any) {
|
||||
if l.level >= gormlogger.Error {
|
||||
l.logger.ErrorContext(ctx, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *slogLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
if l.level <= gormlogger.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
elapsed := time.Since(begin)
|
||||
sql, rows := fc()
|
||||
attrs := []any{"elapsed", elapsed, "rows", rows, "sql", sql}
|
||||
|
||||
switch {
|
||||
case err != nil && l.level >= gormlogger.Error && !errors.Is(err, gormlogger.ErrRecordNotFound):
|
||||
l.logger.ErrorContext(ctx, "数据库执行出错", append(attrs, "err", err)...)
|
||||
case elapsed > slowThreshold && l.level >= gormlogger.Warn:
|
||||
l.logger.WarnContext(ctx, "慢查询", attrs...)
|
||||
case l.level >= gormlogger.Info:
|
||||
l.logger.DebugContext(ctx, "数据库执行", attrs...)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"rill/internal/model"
|
||||
)
|
||||
|
||||
// Migration 描述一次数据库结构变更,Version 必须唯一且递增。
|
||||
type Migration struct {
|
||||
Version int
|
||||
Name string
|
||||
Up func(*gorm.DB) error
|
||||
}
|
||||
|
||||
// migrations 按 Version 升序登记所有迁移,新增迁移只能追加。
|
||||
var migrations = []Migration{
|
||||
{
|
||||
Version: 1,
|
||||
Name: "create_notes",
|
||||
Up: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.Note{})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// schemaMigration 记录已应用的迁移版本。
|
||||
type schemaMigration struct {
|
||||
Version int `gorm:"primaryKey"`
|
||||
Name string
|
||||
AppliedAt time.Time
|
||||
}
|
||||
|
||||
func (schemaMigration) TableName() string { return "schema_migrations" }
|
||||
|
||||
// Migrate 依次执行未应用的迁移,每个迁移在独立事务中完成并记录版本,可重复调用。
|
||||
func Migrate(ctx context.Context, db *gorm.DB) error {
|
||||
tx := db.WithContext(ctx)
|
||||
if err := tx.AutoMigrate(&schemaMigration{}); err != nil {
|
||||
return fmt.Errorf("初始化迁移记录表失败: %w", err)
|
||||
}
|
||||
|
||||
var applied []int
|
||||
if err := tx.Model(&schemaMigration{}).Pluck("version", &applied).Error; err != nil {
|
||||
return fmt.Errorf("读取迁移记录失败: %w", err)
|
||||
}
|
||||
done := make(map[int]bool, len(applied))
|
||||
for _, version := range applied {
|
||||
done[version] = true
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
if done[m.Version] {
|
||||
continue
|
||||
}
|
||||
err := tx.Transaction(func(tx *gorm.DB) error {
|
||||
if err := m.Up(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&schemaMigration{Version: m.Version, Name: m.Name, AppliedAt: time.Now()}).Error
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("执行迁移 %d_%s 失败: %w", m.Version, m.Name, err)
|
||||
}
|
||||
slog.Info("数据库迁移已应用", "version", m.Version, "name", m.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Package model 定义数据库模型。
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Note 示例便签。
|
||||
type Note struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Title string `gorm:"size:200;not null" json:"title"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"rill/internal/api"
|
||||
"rill/internal/config"
|
||||
"rill/internal/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -52,11 +54,28 @@ func main() {
|
||||
}))
|
||||
}
|
||||
|
||||
// 初始化数据库并执行迁移
|
||||
db, err := database.Open(cfg)
|
||||
if err != nil {
|
||||
slog.Error("初始化数据库失败", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
if err := database.Close(db); err != nil {
|
||||
slog.Warn("关闭数据库失败", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
migrateCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
err = database.Migrate(migrateCtx, db)
|
||||
cancel()
|
||||
if err != nil {
|
||||
slog.Error("数据库迁移失败", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// API 路由
|
||||
api := r.Group(cfg.API.Prefix)
|
||||
api.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
api.RegisterRoutes(r.Group(cfg.API.Prefix), db)
|
||||
|
||||
// 静态文件服务
|
||||
fs := http.FileServer(http.Dir(cfg.Static.Dir))
|
||||
|
||||
Reference in New Issue
Block a user