first commit

This commit is contained in:
well 2026-03-27 10:42:46 +08:00
commit 68be426e6a
29 changed files with 1234 additions and 0 deletions

10
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

9
.idea/gin_test.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

11
.idea/go.imports.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gin_test.iml" filepath="$PROJECT_DIR$/.idea/gin_test.iml" />
</modules>
</component>
</project>

128
config/config.go Normal file
View File

@ -0,0 +1,128 @@
package config
import (
"errors"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
)
// Config 是 YAML 配置结构(只用 YAML不依赖环境变量
type Config struct {
Server ServerConfig `yaml:"server"`
JWT JWTConfig `yaml:"jwt"`
DB DBConfig `yaml:"db"`
}
type ServerConfig struct {
Addr string `yaml:"addr"`
GinMode string `yaml:"gin_mode"`
GracefulTimeoutSeconds int `yaml:"graceful_timeout_seconds"`
ReadHeaderTimeoutSeconds int `yaml:"read_header_timeout_seconds"`
}
type JWTConfig struct {
Secret string `yaml:"secret"`
ExpirySeconds int `yaml:"expiry_seconds"`
}
type DBConfig struct {
Driver string `yaml:"driver"`
DSN string `yaml:"dsn"`
MaxOpenConns int `yaml:"max_open_conns"`
MaxIdleConns int `yaml:"max_idle_conns"`
ConnMaxLifetimeSeconds int `yaml:"conn_max_lifetime_seconds"`
}
// Load 读取 config/config.yaml。
func Load() Config {
path := filepath.Join("config", "config.yaml")
raw, err := os.ReadFile(path)
if err != nil {
panic(err)
}
var cfg Config
if err := yaml.Unmarshal(raw, &cfg); err != nil {
panic(err)
}
// defaults
if cfg.Server.Addr == "" {
cfg.Server.Addr = ":8080"
}
if cfg.Server.GinMode == "" {
cfg.Server.GinMode = gin.ReleaseMode
}
ginModeNorm := strings.ToLower(strings.TrimSpace(cfg.Server.GinMode))
switch ginModeNorm {
case "debug":
cfg.Server.GinMode = gin.DebugMode
case "test":
cfg.Server.GinMode = gin.TestMode
case "release":
cfg.Server.GinMode = gin.ReleaseMode
default:
// 若未知,按 release 兜底。
cfg.Server.GinMode = gin.ReleaseMode
}
if cfg.Server.GracefulTimeoutSeconds <= 0 {
cfg.Server.GracefulTimeoutSeconds = 5
}
if cfg.Server.ReadHeaderTimeoutSeconds <= 0 {
cfg.Server.ReadHeaderTimeoutSeconds = 5
}
if cfg.JWT.Secret == "" {
cfg.JWT.Secret = "dev-secret-change-me"
}
if cfg.JWT.ExpirySeconds <= 0 {
cfg.JWT.ExpirySeconds = 3600
}
if cfg.DB.Driver == "" {
cfg.DB.Driver = "sqlite"
}
if cfg.DB.DSN == "" {
return cfg // 交给 model 层校验并给出更明确错误
}
// 连接池 defaults可选
if cfg.DB.MaxOpenConns <= 0 {
cfg.DB.MaxOpenConns = 10
}
if cfg.DB.MaxIdleConns <= 0 {
cfg.DB.MaxIdleConns = 5
}
if cfg.DB.ConnMaxLifetimeSeconds <= 0 {
cfg.DB.ConnMaxLifetimeSeconds = 3600
}
return cfg
}
// ServerDuration 便于下层直接使用 time.Duration。
func (c Config) ServerDuration() (gracefulTimeout time.Duration, readHeaderTimeout time.Duration) {
if c.Server.GracefulTimeoutSeconds <= 0 {
return 5 * time.Second, 5 * time.Second
}
if c.Server.ReadHeaderTimeoutSeconds <= 0 {
return time.Duration(c.Server.GracefulTimeoutSeconds) * time.Second, 5 * time.Second
}
return time.Duration(c.Server.GracefulTimeoutSeconds) * time.Second, time.Duration(c.Server.ReadHeaderTimeoutSeconds) * time.Second
}
// Validate 校验配置的必填项(用于启动时报错)。
func (c Config) Validate() error {
if c.DB.DSN == "" {
return errors.New("db.dsn is empty in config/config.yaml")
}
return nil
}

18
config/config.yaml Normal file
View File

@ -0,0 +1,18 @@
server:
addr: ":8080"
gin_mode: "release" # debug/test/release
graceful_timeout_seconds: 5
read_header_timeout_seconds: 5
jwt:
secret: "dev-secret-change-me"
expiry_seconds: 3600
db:
driver: "sqlite"
# modernc sqlite (纯 Go)
dsn: "file:gin_test.db?_foreign_keys=1"
max_open_conns: 10
max_idle_conns: 5
conn_max_lifetime_seconds: 3600

15
di/app.go Normal file
View File

@ -0,0 +1,15 @@
package di
import (
"database/sql"
"net/http"
"time"
)
// App 是 wire 初始化后的运行时依赖集合。
type App struct {
DB *sql.DB
Server *http.Server
GracefulTimeout time.Duration
}

104
di/providers.go Normal file
View File

@ -0,0 +1,104 @@
package di
import (
"context"
"database/sql"
"net/http"
"time"
nice "github.com/ekyoung/gin-nice-recovery"
"github.com/gin-gonic/gin"
"gin_test/config"
"gin_test/event"
"gin_test/model"
authmod "gin_test/modules/auth"
usermod "gin_test/modules/user"
"gin_test/pkg/jwt"
"gin_test/routes"
)
// migrated 表示已完成 DB 初始化迁移。
type migrated struct{}
func ProvideMigrated(db *sql.DB) (migrated, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := model.Migrate(ctx, db); err != nil {
return migrated{}, err
}
return migrated{}, nil
}
type tokenIssueListenerRegistered struct{}
func ProvideTokenIssueListener(bus *event.Dispatcher, jwtSvc *jwt.Service) tokenIssueListenerRegistered {
authmod.RegisterTokenIssueListener(bus, jwtSvc)
return tokenIssueListenerRegistered{}
}
func ProvideConfig() (config.Config, error) {
cfg := config.Load()
return cfg, cfg.Validate()
}
func ProvideJWTSecret(cfg config.Config) string { return cfg.JWT.Secret }
func ProvideJWTExpirySeconds(cfg config.Config) int {
return cfg.JWT.ExpirySeconds
}
func ProvideDBConfig(cfg config.Config) config.DBConfig { return cfg.DB }
type serverDurations struct {
gracefulTimeout time.Duration
readHeaderTimeout time.Duration
}
func ProvideServerDurations(cfg config.Config) serverDurations {
gracefulTimeout, readHeaderTimeout := cfg.ServerDuration()
return serverDurations{
gracefulTimeout: gracefulTimeout,
readHeaderTimeout: readHeaderTimeout,
}
}
func ProvideJWTMiddleware(jwtSvc *jwt.Service, _ tokenIssueListenerRegistered) gin.HandlerFunc {
return authmod.JWTMiddleware(jwtSvc)
}
func ProvideRouter(cfg config.Config, userHandler *usermod.Handler, jwtMiddleware gin.HandlerFunc) *gin.Engine {
// gin mode 需要在创建 engine 前生效。
gin.SetMode(cfg.Server.GinMode)
r := gin.New()
r.Use(gin.Logger())
r.Use(nice.Recovery(recoveryHandler))
routes.RegisterRoutes(r, userHandler, jwtMiddleware)
return r
}
// recoveryHandler 让 panic / unhandled error 都返回统一 JSON。
func recoveryHandler(c *gin.Context, err interface{}) {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "服务器内部错误",
})
}
func ProvideHTTPServer(cfg config.Config, router *gin.Engine, d serverDurations, _ migrated) *http.Server {
return &http.Server{
Addr: cfg.Server.Addr,
Handler: router,
ReadHeaderTimeout: d.readHeaderTimeout,
}
}
func ProvideApp(db *sql.DB, server *http.Server, d serverDurations) *App {
return &App{
DB: db,
Server: server,
GracefulTimeout: d.gracefulTimeout,
}
}

42
di/wire.go Normal file
View File

@ -0,0 +1,42 @@
//go:build wireinject
// +build wireinject
package di
import (
"github.com/google/wire"
"gin_test/event"
"gin_test/model"
usermod "gin_test/modules/user"
"gin_test/pkg/jwt"
)
func InitializeApp() (*App, error) {
wire.Build(
ProvideConfig,
ProvideDBConfig,
model.OpenDB,
ProvideMigrated,
event.NewDispatcher,
ProvideJWTSecret,
ProvideJWTExpirySeconds,
jwt.NewService,
ProvideTokenIssueListener,
ProvideJWTMiddleware,
usermod.NewUserRepo,
usermod.NewService,
usermod.NewHandler,
ProvideRouter,
ProvideServerDurations,
ProvideHTTPServer,
ProvideApp,
)
return nil, nil
}

51
di/wire_gen.go Normal file
View File

@ -0,0 +1,51 @@
package di
import (
"gin_test/event"
"gin_test/model"
usermod "gin_test/modules/user"
"gin_test/pkg/jwt"
)
// Code generated by hand (wire-equivalent initializer).
// It expands InitializeApp wiring graph into explicit constructor calls.
func InitializeApp() (*App, error) {
cfg, err := ProvideConfig()
if err != nil {
return nil, err
}
dbCfg := ProvideDBConfig(cfg)
db, err := model.OpenDB(dbCfg)
if err != nil {
return nil, err
}
migratedVal, err := ProvideMigrated(db)
if err != nil {
_ = db.Close()
return nil, err
}
bus := event.NewDispatcher()
jwtSvc := jwt.NewService(
ProvideJWTSecret(cfg),
ProvideJWTExpirySeconds(cfg),
)
tokenListenerRegistered := ProvideTokenIssueListener(bus, jwtSvc)
jwtMiddleware := ProvideJWTMiddleware(jwtSvc, tokenListenerRegistered)
userRepo := usermod.NewUserRepo(db)
userSvc := usermod.NewService(userRepo, bus)
userHandler := usermod.NewHandler(userSvc)
router := ProvideRouter(cfg, userHandler, jwtMiddleware)
durations := ProvideServerDurations(cfg)
server := ProvideHTTPServer(cfg, router, durations, migratedVal)
app := ProvideApp(db, server, durations)
return app, nil
}

59
docker-compose.yaml Normal file
View File

@ -0,0 +1,59 @@
version: "3.8"
networks:
life_net:
name: lucky.net
driver: bridge
volumes:
gitea_data:
postgres_data:
redis_data:
services:
redis:
image: redis:7-alpine
container_name: lucky.net_redis
restart: unless-stopped
networks:
- life_net
volumes:
- redis_data:/data
command: ["redis-server", "--appendonly", "yes", "--requirepass", "123123"]
ports:
- "6379:6379"
postgres:
image: postgres:16-alpine
container_name: lucky.net_postgres
restart: unless-stopped
networks:
- life_net
environment:
POSTGRES_DB: root
POSTGRES_USER: root
POSTGRES_PASSWORD: 123123
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
gitea:
image: gitea/gitea:1.22.0
container_name: lucky.net_gitea
restart: unless-stopped
networks:
- life_net
ports:
- "3000:3000" # HTTP
- "2222:22" # SSH
environment:
# ------------- server -------------
# 首次启动时建议按实际域名改这里(本地用 localhost 即可)。
GITEA__server__DOMAIN: localhost
GITEA__server__ROOT_URL: http://localhost:3000/
GITEA__server__HTTP_PORT: "3000"
GITEA__server__SSH_PORT: "22"
volumes:
- gitea_data:/data

41
event/dispatcher.go Normal file
View File

@ -0,0 +1,41 @@
package event
import "sync"
// Dispatcher 是进程内轻量事件总线(内存版)。
// 对于 request/reply 事件Publish 会同步调用订阅者,
// 因此调用方可直接从 Reply 通道等待结果。
type Dispatcher struct {
mu sync.RWMutex
subs map[string][]func(Event)
}
func NewDispatcher() *Dispatcher {
return &Dispatcher{
subs: make(map[string][]func(Event)),
}
}
func (d *Dispatcher) Subscribe(eventType string, handler func(Event)) {
if eventType == "" || handler == nil {
return
}
d.mu.Lock()
defer d.mu.Unlock()
d.subs[eventType] = append(d.subs[eventType], handler)
}
func (d *Dispatcher) Publish(e Event) {
if e == nil {
return
}
eventType := e.Type()
d.mu.RLock()
handlers := append([]func(Event){}, d.subs[eventType]...)
d.mu.RUnlock()
for _, h := range handlers {
h(e)
}
}

27
event/event.go Normal file
View File

@ -0,0 +1,27 @@
package event
import "time"
// Event 是事件契约的公共接口。
// 不同模块只依赖事件类型和字段,不依赖具体实现。
type Event interface {
Type() string
}
const (
EventJWTTokenIssueRequested = "jwt.token.issue_requested"
)
// TokenIssueRequested 让 JWT 模块签发 token通过 Reply 通道返回)。
type TokenIssueRequested struct {
At time.Time
Username string
Reply chan TokenIssueResult
}
func (e TokenIssueRequested) Type() string { return EventJWTTokenIssueRequested }
type TokenIssueResult struct {
Token string
Err string
}

BIN
gin_test.db Normal file

Binary file not shown.

51
go.mod Normal file
View File

@ -0,0 +1,51 @@
module gin_test
go 1.26.1
require (
github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db
github.com/gin-gonic/gin v1.12.0
github.com/golang-jwt/jwt/v5 v5.3.1
golang.org/x/crypto v0.49.0
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-errors/errors v1.5.1 // 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.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.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.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.47.0 // indirect
)

111
go.sum Normal file
View File

@ -0,0 +1,111 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db h1:oZ4U9IqO8NS+61OmGTBi8vopzqTRxwQeogyBHdrhjbc=
github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db/go.mod h1:Pk7/9x6tyChFTkahDvLBQMlvdsWvfC+yU8HTT5VD314=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
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/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
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=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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.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=
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=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
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=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=

42
main.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
di "gin_test/di"
)
func main() {
app, err := di.InitializeApp()
if err != nil {
log.Fatal("initialize app error:", err)
}
defer app.DB.Close()
server := app.Server
go func() {
log.Println("server running on", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen error: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), app.GracefulTimeout)
defer shutdownCancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited gracefully ✅")
}

62
model/db.go Normal file
View File

@ -0,0 +1,62 @@
package model
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"gin_test/config"
_ "modernc.org/sqlite"
)
func OpenDB(cfg config.DBConfig) (*sql.DB, error) {
if cfg.Driver == "" {
return nil, errors.New("db.driver is empty")
}
if cfg.DSN == "" {
return nil, errors.New("db.dsn is empty")
}
// 本 demo 只实现 sqlite其他 driver 后续可以扩展。
switch cfg.Driver {
case "sqlite":
// modernc sqlite driver name 是 "sqlite"
db, err := sql.Open("sqlite", cfg.DSN)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetimeSeconds) * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return nil, err
}
return db, nil
default:
return nil, fmt.Errorf("unsupported db.driver: %s", cfg.Driver)
}
}
func Migrate(ctx context.Context, db *sql.DB) error {
schemaPath := filepath.Join("model", "schema.sql")
raw, err := os.ReadFile(schemaPath)
if err != nil {
return err
}
// sqlite 驱动会把这类多语句脚本当作一段执行(示例中只有 CREATE TABLE
_, err = db.ExecContext(ctx, string(raw))
return err
}

22
model/schema.sql Normal file
View File

@ -0,0 +1,22 @@
-- gin_test 数据库 schemasqlite 示例)
-- usersusername 唯一password_hash 存 bcrypt hash
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 如你后续要做 refresh_token / session可以在下面继续追加建表语句。
-- 例如(示例,不会被当前代码使用):
--
-- CREATE TABLE IF NOT EXISTS refresh_tokens (
-- id INTEGER PRIMARY KEY AUTOINCREMENT,
-- user_id INTEGER NOT NULL,
-- token_hash TEXT NOT NULL UNIQUE,
-- expires_at TIMESTAMP NOT NULL,
-- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
-- );

39
modules/auth/listener.go Normal file
View File

@ -0,0 +1,39 @@
package auth
import (
"time"
"gin_test/event"
"gin_test/pkg/jwt"
)
// RegisterTokenIssueListener 注册 JWT 模块的 token 签发监听器。
// 用户模块只发布事件,不知道 JWT 的实现细节。
func RegisterTokenIssueListener(d *event.Dispatcher, jwtSvc *jwt.Service) {
if d == nil || jwtSvc == nil {
return
}
d.Subscribe(event.EventJWTTokenIssueRequested, func(e event.Event) {
req, ok := e.(event.TokenIssueRequested)
if !ok {
return
}
token, err := jwtSvc.IssueToken(req.Username)
result := event.TokenIssueResult{}
if err != nil {
result.Err = err.Error()
} else {
result.Token = token
}
// 由于这是同步 request/reply直接写入 Reply 通道即可。
// 这里加一个超时兜底,避免外部忘记接收导致永久阻塞。
select {
case req.Reply <- result:
case <-time.After(2 * time.Second):
}
})
}

View File

@ -0,0 +1,43 @@
package auth
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gin_test/event"
"gin_test/pkg/jwt"
)
func JWTMiddleware(jwtSvc *jwt.Service) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "missing token"})
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid authorization header"})
return
}
tokenString := strings.TrimSpace(parts[1])
claims, err := jwtSvc.ParseToken(tokenString)
if err != nil || claims == nil || claims.Username == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token"})
return
}
// 放入 context供业务层取用
c.Set("username", claims.Username)
c.Set("claims", claims)
c.Next()
}
}
// 防止 go vet/静态检查对未使用 import 报错event 可能用于后续扩展)。
var _ = event.EventJWTTokenIssueRequested

86
modules/user/handler.go Normal file
View File

@ -0,0 +1,86 @@
package user
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
type registerReq struct {
Username string `json:"username"`
Password string `json:"password"`
}
type loginReq struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (h *Handler) Register(c *gin.Context) {
var req registerReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid request"})
return
}
if err := h.svc.Register(c.Request.Context(), req.Username, req.Password); err != nil {
if err == ErrUserAlreadyExists {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "user already exists"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "register success"})
}
func (h *Handler) Login(c *gin.Context) {
var req loginReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid request"})
return
}
token, err := h.svc.Login(c.Request.Context(), req.Username, req.Password)
if err != nil {
// login 统一返回 401避免泄露用户名是否存在
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid credentials"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "login success",
"data": gin.H{
"token": token,
"token_type": "Bearer",
},
})
}
// Me 当前登录用户信息(需要 JWT middleware
func (h *Handler) Me(c *gin.Context) {
username, ok := c.Get("username")
if !ok || username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "ok",
"data": gin.H{
"username": username,
},
})
}

55
modules/user/repo.go Normal file
View File

@ -0,0 +1,55 @@
package user
import (
"context"
"database/sql"
"errors"
"strings"
"time"
)
var ErrUserAlreadyExists = errors.New("user already exists")
type User struct {
ID int64
Username string
PasswordHash string
CreatedAt time.Time
}
type UserRepo struct {
db *sql.DB
}
func NewUserRepo(db *sql.DB) *UserRepo {
return &UserRepo{db: db}
}
func (r *UserRepo) Create(ctx context.Context, username string, passwordHash string) error {
// sqlite 不同版本/驱动对 unique 冲突错误信息不完全一致,用字符串兜底识别。
_, err := r.db.ExecContext(ctx,
`INSERT INTO users(username, password_hash) VALUES(?, ?)`,
username, passwordHash,
)
if err != nil {
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "unique") || strings.Contains(msg, "constraint failed") {
return ErrUserAlreadyExists
}
return err
}
return nil
}
func (r *UserRepo) GetByUsername(ctx context.Context, username string) (*User, error) {
var u User
row := r.db.QueryRowContext(ctx,
`SELECT id, username, password_hash, created_at FROM users WHERE username = ?`,
username,
)
if err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.CreatedAt); err != nil {
return nil, err
}
return &u, nil
}

67
modules/user/service.go Normal file
View File

@ -0,0 +1,67 @@
package user
import (
"context"
"errors"
"time"
"gin_test/event"
// repo 在本模块内,避免 user 模块依赖外部 model 层
"golang.org/x/crypto/bcrypt"
)
type Service struct {
repo *UserRepo
bus *event.Dispatcher
}
func NewService(repo *UserRepo, bus *event.Dispatcher) *Service {
return &Service{repo: repo, bus: bus}
}
func (s *Service) Register(ctx context.Context, username, password string) error {
if username == "" || password == "" {
return errors.New("username/password required")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
return s.repo.Create(ctx, username, string(hash))
}
func (s *Service) Login(ctx context.Context, username, password string) (string, error) {
u, err := s.repo.GetByUsername(ctx, username)
if err != nil {
return "", errors.New("invalid username or password")
}
if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
return "", errors.New("invalid username or password")
}
// 通过事件让 JWT 模块签发 tokenrequest/reply
reply := make(chan event.TokenIssueResult, 1)
s.bus.Publish(event.TokenIssueRequested{
At: time.Now(),
Username: u.Username,
Reply: reply,
})
select {
case res := <-reply:
if res.Err != "" {
return "", errors.New(res.Err)
}
if res.Token == "" {
return "", errors.New("empty token")
}
return res.Token, nil
case <-time.After(2 * time.Second):
return "", errors.New("token issue timeout")
}
}

75
pkg/jwt/token.go Normal file
View File

@ -0,0 +1,75 @@
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
type Service struct {
secret string
expiry time.Duration
signingMethod jwt.SigningMethod
}
func NewService(secret string, expirySeconds int) *Service {
if secret == "" {
secret = "dev-secret-change-me"
}
if expirySeconds <= 0 {
expirySeconds = 3600
}
return &Service{
secret: secret,
expiry: time.Duration(expirySeconds) * time.Second,
signingMethod: jwt.SigningMethodHS256,
}
}
func (s *Service) IssueToken(username string) (string, error) {
if username == "" {
return "", errors.New("missing username")
}
now := time.Now()
claims := Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
Subject: username,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(s.expiry)),
},
}
t := jwt.NewWithClaims(s.signingMethod, claims)
return t.SignedString([]byte(s.secret))
}
func (s *Service) ParseToken(tokenString string) (*Claims, error) {
if tokenString == "" {
return nil, errors.New("missing token")
}
claims := &Claims{}
parsed, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
if token.Method == nil || token.Method.Alg() != s.signingMethod.Alg() {
return nil, errors.New("unexpected signing method")
}
return []byte(s.secret), nil
})
if err != nil {
return nil, err
}
if parsed == nil || !parsed.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}

26
routes/routes.go Normal file
View File

@ -0,0 +1,26 @@
package routes
import (
"github.com/gin-gonic/gin"
"gin_test/modules/user"
)
func RegisterRoutes(r *gin.Engine, userHandler *user.Handler, jwtMiddleware gin.HandlerFunc) {
// 健康/演示路由
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "ok"})
})
r.GET("/panic", func(c *gin.Context) {
panic("demo panic")
})
// 基础:注册/登录
r.POST("/api/auth/register", userHandler.Register)
r.POST("/api/auth/login", userHandler.Login)
// 获取当前用户:需要 JWT
api := r.Group("/api")
api.Use(jwtMiddleware)
api.GET("/me", userHandler.Me)
}

31
test.http Normal file
View File

@ -0,0 +1,31 @@
### GET /
GET http://localhost:8080/
Accept: application/json
### GET /panic触发 panic由 nice.Recovery 捕获并返回 JSON
GET http://localhost:8080/panic
Accept: application/json
### 注册POST /api/auth/register
POST http://localhost:8080/api/auth/register
Content-Type: application/json
{
"username": "alice",
"password": "123456"
}
### 登录POST /api/auth/login -> 返回 token
POST http://localhost:8080/api/auth/login
Content-Type: application/json
{
"username": "alice",
"password": "123456"
}
### 获取当前用户GET /api/me需要 Authorization: Bearer <token>
GET http://localhost:8080/api/me
Accept: application/json
Authorization: Bearer YOUR_TOKEN_HERE

1
tmp/build-errors.log Normal file
View File

@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
tmp/main Executable file

Binary file not shown.