commit a5a24562b259a937ba46ee0de91bc4633e294cd2 Author: nepiedg <806669289@qq.com.com> Date: Mon Dec 29 09:32:44 2025 +0000 Sure! Pl diff --git a/.env.example b/.env.example new file mode 100755 index 0000000..6c249db --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# 服务器配置 +SERVER_PORT=8080 +GIN_MODE=debug + +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_password +DB_NAME=wx_service + +# 微信小程序配置 +WECHAT_APP_ID=your_app_id +WECHAT_APP_SECRET=your_app_secret + +# JWT配置 +JWT_SECRET=your-secret-key-change-in-production diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..b9cbf5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Go workspace file +go.work + +# Environment variables +.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Build +/bin +/dist +/tmp + +# Logs +*.log diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..0c0050c --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + + "wx_service/config" + "wx_service/internal/database" + "wx_service/internal/handler" + "wx_service/internal/model" + "wx_service/internal/service" +) + +func main() { + config.LoadConfig() + + if err := database.InitDB(); err != nil { + log.Fatalf("init database failed: %v", err) + } + if err := database.AutoMigrate(&model.User{}); err != nil { + log.Fatalf("auto migrate failed: %v", err) + } + + if config.AppConfig.WeChat.AppID == "" || config.AppConfig.WeChat.AppSecret == "" { + log.Fatal("wechat app id/secret are not configured") + } + + gin.SetMode(config.AppConfig.Server.Mode) + router := gin.Default() + + wechatClient := service.NewWeChatClient( + config.AppConfig.WeChat.AppID, + config.AppConfig.WeChat.AppSecret, + nil, + ) + authService := service.NewAuthService(database.DB, wechatClient) + authHandler := handler.NewAuthHandler(authService) + + api := router.Group("/api/v1") + { + api.POST("/auth/login", authHandler.LoginWithWeChat) + } + + router.GET("/healthz", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + addr := ":" + config.AppConfig.Server.Port + if err := router.Run(addr); err != nil { + log.Fatalf("server stopped: %v", err) + } +} diff --git a/config/config.go b/config/config.go new file mode 100755 index 0000000..06d549a --- /dev/null +++ b/config/config.go @@ -0,0 +1,76 @@ +package config + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +type Config struct { + Server ServerConfig + Database DatabaseConfig + WeChat WeChatConfig + JWT JWTConfig +} + +type ServerConfig struct { + Port string + Mode string +} + +type DatabaseConfig struct { + Host string + Port string + User string + Password string + DBName string +} + +type WeChatConfig struct { + AppID string + AppSecret string +} + +type JWTConfig struct { + Secret string + Expire int +} + +var AppConfig *Config + +func LoadConfig() { + // 加载 .env 文件 + if err := godotenv.Load(); err != nil { + log.Println("未找到 .env 文件,使用环境变量") + } + + AppConfig = &Config{ + Server: ServerConfig{ + Port: getEnv("SERVER_PORT", "8080"), + Mode: getEnv("GIN_MODE", "debug"), + }, + Database: DatabaseConfig{ + Host: getEnv("DB_HOST", "localhost"), + Port: getEnv("DB_PORT", "3306"), + User: getEnv("DB_USER", "root"), + Password: getEnv("DB_PASSWORD", ""), + DBName: getEnv("DB_NAME", "wx_service"), + }, + WeChat: WeChatConfig{ + AppID: getEnv("WECHAT_APP_ID", ""), + AppSecret: getEnv("WECHAT_APP_SECRET", ""), + }, + JWT: JWTConfig{ + Secret: getEnv("JWT_SECRET", "your-secret-key"), + Expire: 86400, // 24小时 + }, + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} diff --git a/go.mod b/go.mod new file mode 100755 index 0000000..b799745 --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module wx_service + +go 1.23.6 + +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/joho/godotenv v1.5.1 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // 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.27.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.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-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..06930d7 --- /dev/null +++ b/go.sum @@ -0,0 +1,102 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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/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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +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/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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100755 index 0000000..8142c97 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,41 @@ +package database + +import ( + "fmt" + "log" + "wx_service/config" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +func InitDB() error { + cfg := config.AppConfig.Database + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cfg.User, + cfg.Password, + cfg.Host, + cfg.Port, + cfg.DBName, + ) + + var err error + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + + if err != nil { + return fmt.Errorf("连接数据库失败: %v", err) + } + + log.Println("数据库连接成功") + return nil +} + +func AutoMigrate(models ...interface{}) error { + return DB.AutoMigrate(models...) +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go new file mode 100644 index 0000000..b98157c --- /dev/null +++ b/internal/handler/auth_handler.go @@ -0,0 +1,76 @@ +package handler + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + + "wx_service/internal/model" + "wx_service/internal/service" +) + +type AuthHandler struct { + authService *service.AuthService +} + +func NewAuthHandler(authService *service.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +type weChatLoginRequest struct { + Code string `json:"code" binding:"required"` + NickName string `json:"nickname"` + AvatarURL string `json:"avatar_url"` + Gender *int `json:"gender"` + Phone string `json:"phone"` +} + +func (h *AuthHandler) LoginWithWeChat(c *gin.Context) { + var req weChatLoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload")) + return + } + + result, err := h.authService.LoginWithCode(c.Request.Context(), service.LoginRequest{ + Code: req.Code, + NickName: req.NickName, + AvatarURL: req.AvatarURL, + Gender: req.Gender, + Phone: req.Phone, + }) + if err != nil { + switch { + case errors.Is(err, service.ErrCodeRequired): + c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "code is required")) + default: + var apiErr *service.WeChatError + if errors.As(err, &apiErr) { + c.JSON(http.StatusBadGateway, model.Error(http.StatusBadGateway, apiErr.Error())) + return + } + c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "login failed")) + } + return + } + + userPayload := gin.H{ + "id": result.User.ID, + "open_id": result.User.OpenID, + "nickname": result.User.NickName, + "avatar_url": result.User.AvatarURL, + "gender": result.User.Gender, + "phone": result.User.Phone, + } + if result.User.UnionID != "" { + userPayload["union_id"] = result.User.UnionID + } + + c.JSON(http.StatusOK, model.Success(gin.H{ + "user": userPayload, + "session_key": result.SessionKey, + })) +} diff --git a/internal/model/product.go b/internal/model/product.go new file mode 100755 index 0000000..7a9fb4a --- /dev/null +++ b/internal/model/product.go @@ -0,0 +1,26 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +type Product struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Name string `gorm:"size:200;not null" json:"name"` + Description string `gorm:"type:text" json:"description"` + Price float64 `gorm:"type:decimal(10,2);not null" json:"price"` + Stock int `gorm:"default:0" json:"stock"` + ImageURL string `gorm:"size:500" json:"image_url"` + Category string `gorm:"size:50" json:"category"` + Status int `gorm:"default:1" json:"status"` +} + +func (Product) TableName() string { + return "products" +} diff --git a/internal/model/response.go b/internal/model/response.go new file mode 100755 index 0000000..a2b94a7 --- /dev/null +++ b/internal/model/response.go @@ -0,0 +1,22 @@ +package model + +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +func Success(data interface{}) Response { + return Response{ + Code: 200, + Message: "success", + Data: data, + } +} + +func Error(code int, message string) Response { + return Response{ + Code: code, + Message: message, + } +} diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100755 index 0000000..c3471e2 --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,27 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +type User struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + OpenID string `gorm:"uniqueIndex;size:100" json:"open_id"` + UnionID string `gorm:"size:100" json:"union_id,omitempty"` + NickName string `gorm:"size:100" json:"nickname"` + AvatarURL string `gorm:"size:500" json:"avatar_url"` + Gender int `gorm:"default:0" json:"gender"` + Phone string `gorm:"size:20" json:"phone,omitempty"` + + SessionKey string `gorm:"size:100" json:"-"` +} + +func (User) TableName() string { + return "users" +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go new file mode 100644 index 0000000..1403305 --- /dev/null +++ b/internal/service/auth_service.go @@ -0,0 +1,111 @@ +package service + +import ( + "context" + "errors" + "fmt" + "strings" + + "wx_service/internal/model" + + "gorm.io/gorm" +) + +var ErrCodeRequired = errors.New("code is required") + +type AuthService struct { + db *gorm.DB + wechat *WeChatClient +} + +type LoginRequest struct { + Code string + NickName string + AvatarURL string + Gender *int + Phone string +} + +type LoginResult struct { + User *model.User + SessionKey string +} + +func NewAuthService(db *gorm.DB, wechat *WeChatClient) *AuthService { + return &AuthService{ + db: db, + wechat: wechat, + } +} + +func (s *AuthService) LoginWithCode(ctx context.Context, req LoginRequest) (*LoginResult, error) { + if strings.TrimSpace(req.Code) == "" { + return nil, ErrCodeRequired + } + + session, err := s.wechat.Code2Session(ctx, req.Code) + if err != nil { + return nil, err + } + if session.OpenID == "" { + return nil, fmt.Errorf("wechat response missing openid") + } + + tx := s.db.WithContext(ctx) + var user model.User + err = tx.Where("open_id = ?", session.OpenID).First(&user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + user = model.User{ + OpenID: session.OpenID, + UnionID: session.UnionID, + NickName: req.NickName, + AvatarURL: req.AvatarURL, + Phone: req.Phone, + SessionKey: session.SessionKey, + } + if req.Gender != nil { + user.Gender = *req.Gender + } + if err := tx.Create(&user).Error; err != nil { + return nil, fmt.Errorf("create user: %w", err) + } + } else if err != nil { + return nil, fmt.Errorf("query user: %w", err) + } else { + updates := map[string]interface{}{ + "session_key": session.SessionKey, + } + if session.UnionID != "" && session.UnionID != user.UnionID { + updates["union_id"] = session.UnionID + user.UnionID = session.UnionID + } + if req.NickName != "" && req.NickName != user.NickName { + updates["nick_name"] = req.NickName + user.NickName = req.NickName + } + if req.AvatarURL != "" && req.AvatarURL != user.AvatarURL { + updates["avatar_url"] = req.AvatarURL + user.AvatarURL = req.AvatarURL + } + if req.Phone != "" && req.Phone != user.Phone { + updates["phone"] = req.Phone + user.Phone = req.Phone + } + if req.Gender != nil && user.Gender != *req.Gender { + updates["gender"] = *req.Gender + user.Gender = *req.Gender + } + if len(updates) > 0 { + if err := tx.Model(&user).Updates(updates).Error; err != nil { + return nil, fmt.Errorf("update user: %w", err) + } + } + user.SessionKey = session.SessionKey + } + + result := &LoginResult{ + User: &user, + SessionKey: session.SessionKey, + } + return result, nil +} diff --git a/internal/service/wechat_client.go b/internal/service/wechat_client.go new file mode 100644 index 0000000..afc783e --- /dev/null +++ b/internal/service/wechat_client.go @@ -0,0 +1,89 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" +) + +const weChatCode2SessionURL = "https://api.weixin.qq.com/sns/jscode2session" + +// WeChatClient 调用微信接口获取 session/openid。 +type WeChatClient struct { + appID string + appSecret string + client *http.Client +} + +type WeChatSession struct { + OpenID string `json:"openid"` + UnionID string `json:"unionid"` + SessionKey string `json:"session_key"` +} + +type weChatSessionResponse struct { + WeChatSession + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +// WeChatError 表示微信接口级错误。 +type WeChatError struct { + Code int + Msg string +} + +func (e *WeChatError) Error() string { + return fmt.Sprintf("wechat error: code=%d msg=%s", e.Code, e.Msg) +} + +func NewWeChatClient(appID, appSecret string, client *http.Client) *WeChatClient { + if client == nil { + client = &http.Client{ + Timeout: 5 * time.Second, + } + } + + return &WeChatClient{ + appID: appID, + appSecret: appSecret, + client: client, + } +} + +func (c *WeChatClient) Code2Session(ctx context.Context, code string) (*WeChatSession, error) { + query := url.Values{} + query.Set("appid", c.appID) + query.Set("secret", c.appSecret) + query.Set("js_code", code) + query.Set("grant_type", "authorization_code") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s?%s", weChatCode2SessionURL, query.Encode()), nil) + if err != nil { + return nil, fmt.Errorf("build wechat request: %w", err) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("call wechat api: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("wechat api unexpected status: %s", resp.Status) + } + + var raw weChatSessionResponse + if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { + return nil, fmt.Errorf("decode wechat response: %w", err) + } + + if raw.ErrCode != 0 { + return nil, &WeChatError{Code: raw.ErrCode, Msg: raw.ErrMsg} + } + + return &raw.WeChatSession, nil +}