first commit
This commit is contained in:
commit
68be426e6a
10
.idea/.gitignore
vendored
Normal file
10
.idea/.gitignore
vendored
Normal 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
9
.idea/gin_test.iml
Normal 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
11
.idea/go.imports.xml
Normal 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
8
.idea/modules.xml
Normal 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
128
config/config.go
Normal 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
18
config/config.yaml
Normal 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
15
di/app.go
Normal 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
104
di/providers.go
Normal 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
42
di/wire.go
Normal 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
51
di/wire_gen.go
Normal 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
59
docker-compose.yaml
Normal 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
41
event/dispatcher.go
Normal 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
27
event/event.go
Normal 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
BIN
gin_test.db
Normal file
Binary file not shown.
51
go.mod
Normal file
51
go.mod
Normal 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
111
go.sum
Normal 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
42
main.go
Normal 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
62
model/db.go
Normal 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
22
model/schema.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- gin_test 数据库 schema(sqlite 示例)
|
||||||
|
|
||||||
|
-- users:username 唯一,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
39
modules/auth/listener.go
Normal 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):
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
43
modules/auth/middleware.go
Normal file
43
modules/auth/middleware.go
Normal 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
86
modules/user/handler.go
Normal 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
55
modules/user/repo.go
Normal 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
67
modules/user/service.go
Normal 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 模块签发 token(request/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
75
pkg/jwt/token.go
Normal 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
26
routes/routes.go
Normal 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
31
test.http
Normal 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
1
tmp/build-errors.log
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user