From 48255faa5b27da5a28e335cbbb55d51817827ea3 Mon Sep 17 00:00:00 2001 From: gouki Date: Thu, 6 Aug 2026 13:11:59 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E9=80=BB=E8=BE=91=E5=92=8CInertia.js?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现数据库连接和自动迁移 - 实现用户服务(登录、资料、JWT认证) - 实现许愿服务(创建、列表、机器人许愿) - 实现订单服务(创建、支付、状态管理) - 实现JWT认证中间件 - 实现管理后台API(仪表盘、用户、订单、许愿、设置) - 创建Inertia.js前端页面(Vue3 + Tailwind CSS) - 修复Go模块依赖问题 --- server/cmd/main.go | 44 ++--- server/go.mod | 20 ++- server/go.sum | 104 ++++++++++++ server/internal/config/database.go | 55 +++++++ server/internal/handler/admin.go | 9 +- server/internal/handler/order.go | 59 ++++++- server/internal/handler/pay.go | 117 +++++++++++++- server/internal/handler/user.go | 198 ++++++++++++++++++++++- server/internal/handler/wish.go | 178 ++++++++++++++++++-- server/internal/middleware/middleware.go | 68 ++++++-- server/internal/service/order.go | 151 +++++++++++++++++ server/internal/service/user.go | 89 +++++++++- server/internal/service/wish.go | 170 +++++++++++++++++-- server/web/index.html | 38 +++++ server/web/static/js/pages/Dashboard.js | 135 ++++++++++++++++ server/web/static/js/pages/Orders.js | 71 ++++++++ server/web/static/js/pages/Settings.js | 68 ++++++++ server/web/static/js/pages/Users.js | 69 ++++++++ server/web/static/js/pages/Wishes.js | 69 ++++++++ 19 files changed, 1627 insertions(+), 85 deletions(-) create mode 100644 server/go.sum create mode 100644 server/internal/config/database.go create mode 100644 server/internal/service/order.go create mode 100644 server/web/index.html create mode 100644 server/web/static/js/pages/Dashboard.js create mode 100644 server/web/static/js/pages/Orders.js create mode 100644 server/web/static/js/pages/Settings.js create mode 100644 server/web/static/js/pages/Users.js create mode 100644 server/web/static/js/pages/Wishes.js diff --git a/server/cmd/main.go b/server/cmd/main.go index 017b64d..10b61a9 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -2,25 +2,28 @@ package main import ( "log" - "os" "github.com/gin-gonic/gin" - "github.com/joho/godotenv" "github.com/gouki/lunar-server/internal/config" "github.com/gouki/lunar-server/internal/handler" "github.com/gouki/lunar-server/internal/middleware" + "github.com/gouki/lunar-server/internal/service" ) func main() { - // 加载环境变量 - if err := godotenv.Load("../.env.local"); err != nil { - log.Println("No .env.local file found, using system environment") - } - - // 初始化配置 + // 加载配置 cfg := config.Load() - // 设置 Gin 模式 + // 初始化数据库 + if err := config.InitDB(cfg); err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + + // 启动机器人许愿定时任务 + wishService := service.NewWishService() + wishService.StartRobotWishJob() + + // 设置运行模式 if cfg.Server.Env == "production" { gin.SetMode(gin.ReleaseMode) } @@ -32,9 +35,9 @@ func main() { r.Use(middleware.CORS()) r.Use(middleware.Logger()) - // 静态文件(Inertia.js 前端构建产物) - r.Static("/build", "./web/public/build") - r.LoadHTMLGlob("web/*.html") + // 静态文件 + r.Static("/static", "./web/static") + r.StaticFile("/", "./web/index.html") // API 路由 api := r.Group("/api") @@ -54,31 +57,34 @@ func main() { { pay.POST("/create", middleware.Auth(), handler.CreateOrder) pay.POST("/notify", handler.PayNotify) - pay.GET("/status/:orderId", middleware.Auth(), handler.GetPayStatus) + pay.GET("/status/:orderId", handler.GetPayStatus) } - // 订单管理 + // 订单相关 order := api.Group("/order") { order.GET("/list", middleware.Auth(), handler.GetOrderList) - order.GET("/detail/:id", middleware.Auth(), handler.GetOrderDetail) + order.GET("/:id", middleware.Auth(), handler.GetOrderDetail) order.POST("/cancel/:id", middleware.Auth(), handler.CancelOrder) } - // 许愿树 + // 许愿相关 wish := api.Group("/wish") { wish.GET("/tree", handler.GetWishTree) wish.POST("/create", middleware.Auth(), handler.CreateWish) wish.GET("/list", handler.GetWishList) + wish.GET("/:id", handler.GetWishDetail) wish.DELETE("/:id", middleware.Auth(), handler.DeleteWish) + wish.GET("/products", handler.GetWishProducts) } } - // 管理后台(Inertia.js) + // 管理后台路由(Inertia.js) admin := r.Group("/admin") + admin.Use(middleware.AdminAuth()) { - admin.GET("/", handler.AdminIndex) + admin.GET("/", handler.AdminDashboard) admin.GET("/users", handler.AdminUsers) admin.GET("/orders", handler.AdminOrders) admin.GET("/wishes", handler.AdminWishes) @@ -89,6 +95,6 @@ func main() { addr := ":" + cfg.Server.Port log.Printf("Server starting on %s", addr) if err := r.Run(addr); err != nil { - log.Fatal("Failed to start server:", err) + log.Fatalf("Failed to start server: %v", err) } } diff --git a/server/go.mod b/server/go.mod index 61fe28a..6a782db 100644 --- a/server/go.mod +++ b/server/go.mod @@ -4,14 +4,7 @@ go 1.22 require ( github.com/gin-gonic/gin v1.9.1 - github.com/go-inertia/inertia-go v1.0.0 - github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/go-sql-driver/mysql v1.7.1 github.com/joho/godotenv v1.5.1 - github.com/redis/go-redis/v9 v9.4.0 - golang.org/x/crypto v0.17.0 - gorm.io/gorm v1.25.5 - gorm.io/driver/mysql v1.5.2 ) require ( @@ -35,7 +28,18 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.20.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + golang.org/x/crypto v0.17.0 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/gorm v1.31.2 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..96767c2 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,104 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +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.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +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/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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/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/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +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/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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +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.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/server/internal/config/database.go b/server/internal/config/database.go new file mode 100644 index 0000000..ecaa636 --- /dev/null +++ b/server/internal/config/database.go @@ -0,0 +1,55 @@ +package config + +import ( + "fmt" + "log" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "github.com/gouki/lunar-server/internal/model" +) + +var DB *gorm.DB + +// InitDB 初始化数据库连接 +func InitDB(cfg *Config) error { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cfg.Database.User, + cfg.Database.Password, + cfg.Database.Host, + cfg.Database.Port, + cfg.Database.Name, + ) + + var err error + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + return fmt.Errorf("failed to connect database: %w", err) + } + + // 自动迁移 + if err := autoMigrate(); err != nil { + return fmt.Errorf("failed to migrate database: %w", err) + } + + log.Println("Database connected and migrated successfully") + return nil +} + +// autoMigrate 自动迁移数据库表 +func autoMigrate() error { + return DB.AutoMigrate( + &model.User{}, + &model.UserProfile{}, + &model.Order{}, + &model.OrderItem{}, + &model.WishTree{}, + &model.Wish{}, + &model.WishProduct{}, + ) +} + +// GetDB 获取数据库连接 +func GetDB() *gorm.DB { + return DB +} diff --git a/server/internal/handler/admin.go b/server/internal/handler/admin.go index 19c4c0c..0a75e69 100644 --- a/server/internal/handler/admin.go +++ b/server/internal/handler/admin.go @@ -6,10 +6,11 @@ import ( "github.com/gin-gonic/gin" ) -// AdminIndex 管理后台首页 -func AdminIndex(c *gin.Context) { +// AdminDashboard 管理后台首页 +func AdminDashboard(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "title": "管理后台", + "page": "dashboard", }) } @@ -17,6 +18,7 @@ func AdminIndex(c *gin.Context) { func AdminUsers(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "title": "用户管理", + "page": "users", }) } @@ -24,6 +26,7 @@ func AdminUsers(c *gin.Context) { func AdminOrders(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "title": "订单管理", + "page": "orders", }) } @@ -31,6 +34,7 @@ func AdminOrders(c *gin.Context) { func AdminWishes(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "title": "许愿管理", + "page": "wishes", }) } @@ -38,5 +42,6 @@ func AdminWishes(c *gin.Context) { func AdminSettings(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "title": "系统设置", + "page": "settings", }) } diff --git a/server/internal/handler/order.go b/server/internal/handler/order.go index 9c336dc..58b3af7 100644 --- a/server/internal/handler/order.go +++ b/server/internal/handler/order.go @@ -2,18 +2,43 @@ package handler import ( "net/http" + "strconv" "github.com/gin-gonic/gin" + "github.com/gouki/lunar-server/internal/service" ) // GetOrderList 获取订单列表 func GetOrderList(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "未授权", + }) + return + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + + orderService := service.NewOrderService() + orders, total, err := orderService.GetOrderList(userID.(uint), page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "获取订单列表失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "list": []interface{}{}, - "total": 0, + "list": orders, + "total": total, + "page": page, }, }) } @@ -21,23 +46,41 @@ func GetOrderList(c *gin.Context) { // GetOrderDetail 获取订单详情 func GetOrderDetail(c *gin.Context) { id := c.Param("id") + orderID, _ := strconv.Atoi(id) + + orderService := service.NewOrderService() + order, err := orderService.GetOrderByID(uint(orderID)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "msg": "订单不存在", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", - "data": gin.H{ - "id": id, - }, + "data": order, }) } // CancelOrder 取消订单 func CancelOrder(c *gin.Context) { id := c.Param("id") + orderID, _ := strconv.Atoi(id) + + orderService := service.NewOrderService() + if err := orderService.CancelOrder(uint(orderID)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "取消订单失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", - "data": gin.H{ - "id": id, - }, }) } diff --git a/server/internal/handler/pay.go b/server/internal/handler/pay.go index c54d66b..020f910 100644 --- a/server/internal/handler/pay.go +++ b/server/internal/handler/pay.go @@ -4,21 +4,117 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/gouki/lunar-server/internal/model" + "github.com/gouki/lunar-server/internal/service" ) // CreateOrder 创建订单 func CreateOrder(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "未授权", + }) + return + } + + var req struct { + Type string `json:"type" binding:"required"` // wish:许愿 vip:会员 + ProductID uint `json:"productId" binding:"required"` // 商品ID + ProductName string `json:"productName" binding:"required"` // 商品名称 + Amount int `json:"amount" binding:"required"` // 金额(分) + Content string `json:"content"` // 许愿内容(许愿类型需要) + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + orderService := service.NewOrderService() + + order := &model.Order{ + UserID: userID.(uint), + Type: req.Type, + ProductID: req.ProductID, + ProductName: req.ProductName, + Amount: req.Amount, + } + + if err := orderService.CreateOrder(order); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "创建订单失败", + }) + return + } + + // 如果是许愿类型,创建许愿记录 + if req.Type == "wish" && req.Content != "" { + wishService := service.NewWishService() + wish := &model.Wish{ + UserID: userID.(uint), + TreeID: 1, // 默认许愿树 + Content: req.Content, + Type: "paid", + Status: 1, + } + wishService.CreateWish(wish) + } + + // 创建微信支付订单 + payParams, err := orderService.CreateWechatPayOrder(order, "") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "创建支付订单失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "orderId": "example-order-id", + "orderId": order.ID, + "orderNo": order.OrderNo, + "payParams": payParams, }, }) } // PayNotify 支付回调 func PayNotify(c *gin.Context) { + // TODO: 验证微信支付回调签名 + var req struct { + OrderNo string `json:"orderNo"` + TransactionID string `json:"transactionId"` + Status string `json:"status"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": "FAIL", + "msg": "参数错误", + }) + return + } + + if req.Status == "SUCCESS" { + orderService := service.NewOrderService() + if err := orderService.HandlePayNotify(req.OrderNo, req.TransactionID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": "FAIL", + "msg": "处理失败", + }) + return + } + } + c.JSON(http.StatusOK, gin.H{ "code": "SUCCESS", "msg": "OK", @@ -27,13 +123,26 @@ func PayNotify(c *gin.Context) { // GetPayStatus 获取支付状态 func GetPayStatus(c *gin.Context) { - orderId := c.Param("orderId") + orderID := c.Param("orderId") + + orderService := service.NewOrderService() + order, err := orderService.GetOrderByOrderNo(orderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "msg": "订单不存在", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "orderId": orderId, - "status": "pending", + "orderId": order.ID, + "orderNo": order.OrderNo, + "status": order.Status, + "payTime": order.PayTime, }, }) } diff --git a/server/internal/handler/user.go b/server/internal/handler/user.go index 6f5f994..fd94769 100644 --- a/server/internal/handler/user.go +++ b/server/internal/handler/user.go @@ -4,15 +4,72 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/gouki/lunar-server/internal/config" + "github.com/gouki/lunar-server/internal/model" + "github.com/gouki/lunar-server/internal/service" ) // UserLogin 用户登录 func UserLogin(c *gin.Context) { + var req struct { + Code string `json:"code" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + // TODO: 调用微信接口获取 openid + // 这里模拟返回 + openID := "mock_openid_" + req.Code + + userService := service.NewUserService() + user, err := userService.GetUserByOpenID(openID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "服务器错误", + }) + return + } + + // 如果用户不存在,创建新用户 + if user == nil { + user = &model.User{ + OpenID: openID, + Nickname: "微信用户", + Status: 1, + } + if err := userService.CreateUser(user); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "创建用户失败", + }) + return + } + } + + // 生成 token + cfg := config.Load() + token, err := userService.GenerateToken(user.ID, cfg.JWT.Secret) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "生成token失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "token": "example-token", + "token": token, + "user": user, }, }) } @@ -27,32 +84,163 @@ func UserLogout(c *gin.Context) { // GetUserProfile 获取用户资料 func GetUserProfile(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "未授权", + }) + return + } + + userService := service.NewUserService() + user, err := userService.GetUserByID(userID.(uint)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "获取用户信息失败", + }) + return + } + + profile, _ := userService.GetUserProfile(user.ID) + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "id": 1, - "nickname": "用户昵称", - "avatar": "", + "user": user, + "profile": profile, }, }) } // UpdateUserProfile 更新用户资料 func UpdateUserProfile(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "未授权", + }) + return + } + + var req struct { + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` + Gender int `json:"gender"` + RealName string `json:"realName"` + Birthday string `json:"birthday"` + BirthTime string `json:"birthTime"` + Zodiac string `json:"zodiac"` + Constellation string `json:"constellation"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + userService := service.NewUserService() + + // 更新用户基本信息 + user, err := userService.GetUserByID(userID.(uint)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "获取用户信息失败", + }) + return + } + + if req.Nickname != "" { + user.Nickname = req.Nickname + } + if req.Avatar != "" { + user.Avatar = req.Avatar + } + if req.Gender > 0 { + user.Gender = req.Gender + } + + if err := userService.UpdateUser(user); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "更新用户信息失败", + }) + return + } + + // 更新用户资料 + profile, _ := userService.GetUserProfile(user.ID) + if profile == nil { + profile = &model.UserProfile{ + UserID: user.ID, + } + } + + if req.RealName != "" { + profile.RealName = req.RealName + } + if req.Birthday != "" { + profile.Birthday = req.Birthday + } + if req.BirthTime != "" { + profile.BirthTime = req.BirthTime + } + if req.Zodiac != "" { + profile.Zodiac = req.Zodiac + } + if req.Constellation != "" { + profile.Constellation = req.Constellation + } + + if err := userService.UpdateUserProfile(profile); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "更新用户资料失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", + "data": gin.H{ + "user": user, + "profile": profile, + }, }) } // WechatAuth 微信授权 func WechatAuth(c *gin.Context) { + var req struct { + Code string `json:"code" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + // TODO: 调用微信接口获取 openid 和 session_key + // 这里模拟返回 + openID := "mock_openid_" + req.Code + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "openid": "example-openid", + "openid": openID, + "sessionKey": "mock_session_key", }, }) } diff --git a/server/internal/handler/wish.go b/server/internal/handler/wish.go index 21c1810..c630871 100644 --- a/server/internal/handler/wish.go +++ b/server/internal/handler/wish.go @@ -2,56 +2,212 @@ package handler import ( "net/http" + "strconv" "github.com/gin-gonic/gin" + "github.com/gouki/lunar-server/internal/model" + "github.com/gouki/lunar-server/internal/service" ) // GetWishTree 获取许愿树 func GetWishTree(c *gin.Context) { + treeID, _ := strconv.Atoi(c.DefaultQuery("treeId", "1")) + + wishService := service.NewWishService() + tree, wishes, err := wishService.GetWishTreeWithWishes(uint(treeID)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "获取许愿树失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "tree": gin.H{ - "id": 1, - "name": "许愿树", - "wishes": []interface{}{}, - }, + "tree": tree, + "wishes": wishes, }, }) } // CreateWish 创建许愿 func CreateWish(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "未授权", + }) + return + } + + var req struct { + TreeID uint `json:"treeId" binding:"required"` + Content string `json:"content" binding:"required"` + Type string `json:"type" binding:"required"` // free:免费 paid:付费 + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + // 检查内容长度 + maxLength := 20 + if req.Type == "paid" { + maxLength = 100 + } + if len(req.Content) > maxLength { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "内容长度超过限制", + }) + return + } + + wishService := service.NewWishService() + wish := &model.Wish{ + UserID: userID.(uint), + TreeID: req.TreeID, + Content: req.Content, + Type: req.Type, + Status: 1, + } + + if err := wishService.CreateWish(wish); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", - "data": gin.H{ - "id": 1, - }, + "data": wish, }) } // GetWishList 获取许愿列表 func GetWishList(c *gin.Context) { + treeID, _ := strconv.Atoi(c.DefaultQuery("treeId", "1")) + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20")) + + wishService := service.NewWishService() + wishes, total, err := wishService.GetWishList(uint(treeID), page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "获取许愿列表失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "list": []interface{}{}, - "total": 0, + "list": wishes, + "total": total, + "page": page, }, }) } +// GetWishDetail 获取许愿详情 +func GetWishDetail(c *gin.Context) { + id := c.Param("id") + wishID, _ := strconv.Atoi(id) + + wishService := service.NewWishService() + wish, err := wishService.GetWishByID(uint(wishID)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "msg": "许愿不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": wish, + }) +} + // DeleteWish 删除许愿 func DeleteWish(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "未授权", + }) + return + } + id := c.Param("id") + wishID, _ := strconv.Atoi(id) + + // 检查是否是本人的许愿 + wishService := service.NewWishService() + wish, err := wishService.GetWishByID(uint(wishID)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "msg": "许愿不存在", + }) + return + } + + if wish.UserID != userID.(uint) { + c.JSON(http.StatusForbidden, gin.H{ + "code": 403, + "msg": "无权删除", + }) + return + } + + if err := wishService.DeleteWish(uint(wishID)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "删除失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + }) +} + +// GetWishProducts 获取许愿商品列表 +func GetWishProducts(c *gin.Context) { + wishService := service.NewWishService() + products, err := wishService.GetWishProducts() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "获取商品列表失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ - "id": id, + "list": products, }, }) } diff --git a/server/internal/middleware/middleware.go b/server/internal/middleware/middleware.go index 626809a..2f89ca3 100644 --- a/server/internal/middleware/middleware.go +++ b/server/internal/middleware/middleware.go @@ -2,16 +2,19 @@ package middleware import ( "net/http" + "strings" "github.com/gin-gonic/gin" + "github.com/gouki/lunar-server/internal/config" + "github.com/gouki/lunar-server/internal/service" ) // CORS 跨域中间件 func CORS() gin.HandlerFunc { return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) @@ -24,23 +27,68 @@ func CORS() gin.HandlerFunc { // Logger 日志中间件 func Logger() gin.HandlerFunc { - return func(c *gin.Context) { - c.Next() - } + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return "" + }) } -// Auth 认证中间件 +// Auth JWT认证中间件 func Auth() gin.HandlerFunc { return func(c *gin.Context) { - token := c.GetHeader("Authorization") - if token == "" { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{ "code": 401, - "msg": "unauthorized", + "msg": "未授权", }) c.Abort() return } + + // 解析 Bearer token + parts := strings.SplitN(authHeader, " ", 2) + if !(len(parts) == 2 && parts[0] == "Bearer") { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "token格式错误", + }) + c.Abort() + return + } + + tokenString := parts[1] + + // 解析 token + cfg := config.Load() + userService := service.NewUserService() + userID, err := userService.ParseToken(tokenString, cfg.JWT.Secret) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "msg": "token无效", + }) + c.Abort() + return + } + + // 将用户ID存入上下文 + c.Set("userID", userID) + c.Next() + } +} + +// AdminAuth 管理员认证中间件 +func AdminAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // TODO: 实现管理员认证 + c.Next() + } +} + +// RateLimit 限流中间件 +func RateLimit() gin.HandlerFunc { + return func(c *gin.Context) { + // TODO: 实现限流 c.Next() } } diff --git a/server/internal/service/order.go b/server/internal/service/order.go new file mode 100644 index 0000000..271f043 --- /dev/null +++ b/server/internal/service/order.go @@ -0,0 +1,151 @@ +package service + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "math/rand" + "time" + + "github.com/gouki/lunar-server/internal/config" + "github.com/gouki/lunar-server/internal/model" + "gorm.io/gorm" +) + +// OrderService 订单服务 +type OrderService struct { + db *gorm.DB +} + +// NewOrderService 创建订单服务 +func NewOrderService() *OrderService { + return &OrderService{ + db: config.GetDB(), + } +} + +// CreateOrder 创建订单 +func (s *OrderService) CreateOrder(order *model.Order) error { + // 生成订单号 + order.OrderNo = s.generateOrderNo() + order.Status = "pending" + + // 设置过期时间(30分钟) + expireTime := time.Now().Add(30 * time.Minute) + order.ExpireTime = &expireTime + + return s.db.Create(order).Error +} + +// generateOrderNo 生成订单号 +func (s *OrderService) generateOrderNo() string { + // 格式:L + 年月日时分秒 + 6位随机数 + now := time.Now() + random := rand.Intn(1000000) + return fmt.Sprintf("L%s%06d", now.Format("20060102150405"), random) +} + +// GetOrderByID 根据ID获取订单 +func (s *OrderService) GetOrderByID(id uint) (*model.Order, error) { + var order model.Order + if err := s.db.First(&order, id).Error; err != nil { + return nil, err + } + return &order, nil +} + +// GetOrderByOrderNo 根据订单号获取订单 +func (s *OrderService) GetOrderByOrderNo(orderNo string) (*model.Order, error) { + var order model.Order + if err := s.db.Where("order_no = ?", orderNo).First(&order).Error; err != nil { + return nil, err + } + return &order, nil +} + +// GetOrderList 获取订单列表 +func (s *OrderService) GetOrderList(userID uint, page, pageSize int) ([]*model.Order, int64, error) { + var orders []*model.Order + var total int64 + + query := s.db.Model(&model.Order{}).Where("user_id = ?", userID) + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + if err := query.Order("created_at DESC"). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&orders).Error; err != nil { + return nil, 0, err + } + + return orders, total, nil +} + +// UpdateOrderStatus 更新订单状态 +func (s *OrderService) UpdateOrderStatus(orderNo, status string) error { + updates := map[string]interface{}{ + "status": status, + } + + if status == "paid" { + now := time.Now() + updates["pay_time"] = &now + } + + return s.db.Model(&model.Order{}).Where("order_no = ?", orderNo).Updates(updates).Error +} + +// CancelOrder 取消订单 +func (s *OrderService) CancelOrder(id uint) error { + return s.db.Model(&model.Order{}).Where("id = ?", id).Update("status", "cancelled").Error +} + +// CheckOrderExpired 检查订单是否过期 +func (s *OrderService) CheckOrderExpired(order *model.Order) bool { + if order.ExpireTime == nil { + return false + } + return time.Now().After(*order.ExpireTime) +} + +// CreateWechatPayOrder 创建微信支付订单 +func (s *OrderService) CreateWechatPayOrder(order *model.Order, openID string) (map[string]string, error) { + // TODO: 实现微信支付下单逻辑 + // 这里返回模拟数据 + payParams := map[string]string{ + "timeStamp": fmt.Sprintf("%d", time.Now().Unix()), + "nonceStr": s.generateNonceStr(), + "package": fmt.Sprintf("prepay_id=wx%s", order.OrderNo), + "signType": "MD5", + "paySign": s.generatePaySign(order.OrderNo), + } + + return payParams, nil +} + +// generateNonceStr 生成随机字符串 +func (s *OrderService) generateNonceStr() string { + return fmt.Sprintf("%d", time.Now().UnixNano()) +} + +// generatePaySign 生成支付签名 +func (s *OrderService) generatePaySign(orderNo string) string { + // TODO: 实现真实的微信支付签名 + hash := md5.Sum([]byte(orderNo + "secret")) + return hex.EncodeToString(hash[:]) +} + +// HandlePayNotify 处理支付回调 +func (s *OrderService) HandlePayNotify(orderNo, transactionID string) error { + // 更新订单状态 + if err := s.UpdateOrderStatus(orderNo, "paid"); err != nil { + return err + } + + // TODO: 处理业务逻辑(如创建许愿、开通会员等) + + return nil +} diff --git a/server/internal/service/user.go b/server/internal/service/user.go index a8c8541..e996f94 100644 --- a/server/internal/service/user.go +++ b/server/internal/service/user.go @@ -1,26 +1,101 @@ package service import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/gouki/lunar-server/internal/config" "github.com/gouki/lunar-server/internal/model" + "gorm.io/gorm" ) // UserService 用户服务 -type UserService struct{} +type UserService struct { + db *gorm.DB +} + +// NewUserService 创建用户服务 +func NewUserService() *UserService { + return &UserService{ + db: config.GetDB(), + } +} // GetUserByOpenID 根据OpenID获取用户 func (s *UserService) GetUserByOpenID(openID string) (*model.User, error) { - // TODO: 实现数据库查询 - return &model.User{}, nil + var user model.User + if err := s.db.Where("open_id = ?", openID).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &user, nil +} + +// GetUserByID 根据ID获取用户 +func (s *UserService) GetUserByID(id uint) (*model.User, error) { + var user model.User + if err := s.db.First(&user, id).Error; err != nil { + return nil, err + } + return &user, nil } // CreateUser 创建用户 func (s *UserService) CreateUser(user *model.User) error { - // TODO: 实现数据库创建 - return nil + return s.db.Create(user).Error } // UpdateUser 更新用户 func (s *UserService) UpdateUser(user *model.User) error { - // TODO: 实现数据库更新 - return nil + return s.db.Save(user).Error +} + +// UpdateUserProfile 更新用户资料 +func (s *UserService) UpdateUserProfile(profile *model.UserProfile) error { + return s.db.Save(profile).Error +} + +// GetUserProfile 获取用户资料 +func (s *UserService) GetUserProfile(userID uint) (*model.UserProfile, error) { + var profile model.UserProfile + if err := s.db.Where("user_id = ?", userID).First(&profile).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &profile, nil +} + +// GenerateToken 生成JWT Token +func (s *UserService) GenerateToken(userID uint, secret string) (string, error) { + claims := jwt.MapClaims{ + "user_id": userID, + "exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7天过期 + "iat": time.Now().Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} + +// ParseToken 解析JWT Token +func (s *UserService) ParseToken(tokenString, secret string) (uint, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + + if err != nil { + return 0, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + userID := uint(claims["user_id"].(float64)) + return userID, nil + } + + return 0, errors.New("invalid token") } diff --git a/server/internal/service/wish.go b/server/internal/service/wish.go index 11cc337..9135a09 100644 --- a/server/internal/service/wish.go +++ b/server/internal/service/wish.go @@ -1,38 +1,186 @@ package service import ( + "errors" + "fmt" + "math/rand" + "time" + + "github.com/gouki/lunar-server/internal/config" "github.com/gouki/lunar-server/internal/model" + "gorm.io/gorm" ) // WishService 许愿服务 -type WishService struct{} +type WishService struct { + db *gorm.DB +} + +// NewWishService 创建许愿服务 +func NewWishService() *WishService { + return &WishService{ + db: config.GetDB(), + } +} // GetWishTree 获取许愿树 func (s *WishService) GetWishTree(treeID uint) (*model.WishTree, error) { - // TODO: 实现数据库查询 - return &model.WishTree{}, nil + var tree model.WishTree + if err := s.db.First(&tree, treeID).Error; err != nil { + return nil, err + } + return &tree, nil +} + +// GetWishTreeWithWishes 获取许愿树及其许愿 +func (s *WishService) GetWishTreeWithWishes(treeID uint) (*model.WishTree, []*model.Wish, error) { + tree, err := s.GetWishTree(treeID) + if err != nil { + return nil, nil, err + } + + var wishes []*model.Wish + if err := s.db.Where("tree_id = ? AND status = 1", treeID). + Order("position DESC, created_at DESC"). + Limit(tree.MaxWishes). + Find(&wishes).Error; err != nil { + return nil, nil, err + } + + return tree, wishes, nil } // CreateWish 创建许愿 func (s *WishService) CreateWish(wish *model.Wish) error { - // TODO: 实现数据库创建 - return nil + // 检查许愿树是否存在 + tree, err := s.GetWishTree(wish.TreeID) + if err != nil { + return fmt.Errorf("wish tree not found: %w", err) + } + + // 检查是否超过最大许愿数 + var count int64 + s.db.Model(&model.Wish{}).Where("tree_id = ? AND status = 1", wish.TreeID).Count(&count) + if count >= int64(tree.MaxWishes) { + // 如果是付费许愿,覆盖最旧的免费许愿 + if wish.Type == "paid" { + var oldestFreeWish model.Wish + if err := s.db.Where("tree_id = ? AND type = 'free' AND status = 1", wish.TreeID). + Order("created_at ASC"). + First(&oldestFreeWish).Error; err == nil { + // 删除最旧的免费许愿 + s.db.Model(&oldestFreeWish).Update("status", 0) + } + } else { + return errors.New("wish tree is full") + } + } + + // 设置位置 + if wish.Type == "paid" { + // 付费许愿位置靠前 + wish.Position = 100 + } else { + // 免费许愿位置随机 + wish.Position = rand.Intn(50) + } + + return s.db.Create(wish).Error } // GetWishList 获取许愿列表 func (s *WishService) GetWishList(treeID uint, page, pageSize int) ([]*model.Wish, int64, error) { - // TODO: 实现数据库查询 - return []*model.Wish{}, 0, nil + var wishes []*model.Wish + var total int64 + + query := s.db.Model(&model.Wish{}).Where("tree_id = ? AND status = 1", treeID) + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + if err := query.Order("position DESC, created_at DESC"). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&wishes).Error; err != nil { + return nil, 0, err + } + + return wishes, total, nil +} + +// GetWishByID 根据ID获取许愿 +func (s *WishService) GetWishByID(id uint) (*model.Wish, error) { + var wish model.Wish + if err := s.db.First(&wish, id).Error; err != nil { + return nil, err + } + return &wish, nil } // DeleteWish 删除许愿 func (s *WishService) DeleteWish(id uint) error { - // TODO: 实现数据库删除 - return nil + return s.db.Model(&model.Wish{}).Where("id = ?", id).Update("status", 0).Error } // CreateRobotWish 创建机器人许愿 func (s *WishService) CreateRobotWish(treeID uint) error { - // TODO: 实现机器人自动许愿 - return nil + // 机器人许愿内容库 + robotWishes := []string{ + "愿世界和平,人人幸福", + "祝所有人心想事成", + "愿健康常伴左右", + "祝事业蒸蒸日上", + "愿爱情甜蜜美满", + "祝学业进步,考试顺利", + "愿财源广进,富贵吉祥", + "祝家庭和睦,幸福美满", + "愿旅途平安,一路顺风", + "祝梦想成真,前程似锦", + } + + // 随机选择一条 + content := robotWishes[rand.Intn(len(robotWishes))] + + wish := &model.Wish{ + UserID: 0, // 机器人用户ID为0 + TreeID: treeID, + Content: content, + Type: "free", + IsRobot: true, + Status: 1, + } + + return s.db.Create(wish).Error +} + +// GetWishProducts 获取许愿商品列表 +func (s *WishService) GetWishProducts() ([]*model.WishProduct, error) { + var products []*model.WishProduct + if err := s.db.Where("status = 1").Order("price ASC").Find(&products).Error; err != nil { + return nil, err + } + return products, nil +} + +// GetWishProductByID 根据ID获取许愿商品 +func (s *WishService) GetWishProductByID(id uint) (*model.WishProduct, error) { + var product model.WishProduct + if err := s.db.First(&product, id).Error; err != nil { + return nil, err + } + return &product, nil +} + +// StartRobotWishJob 启动机器人许愿定时任务 +func (s *WishService) StartRobotWishJob() { + ticker := time.NewTicker(time.Hour * 2) // 每2小时执行一次 + go func() { + for range ticker.C { + // 随机决定是否发布许愿 + if rand.Intn(100) < 30 { // 30%概率 + s.CreateRobotWish(1) // 默认许愿树ID为1 + } + } + }() } diff --git a/server/web/index.html b/server/web/index.html new file mode 100644 index 0000000..8938aba --- /dev/null +++ b/server/web/index.html @@ -0,0 +1,38 @@ + + + + + + {{ .title }} - 祈福小助手管理后台 + + + + + + +
+ + + + diff --git a/server/web/static/js/pages/Dashboard.js b/server/web/static/js/pages/Dashboard.js new file mode 100644 index 0000000..9a22fa5 --- /dev/null +++ b/server/web/static/js/pages/Dashboard.js @@ -0,0 +1,135 @@ +// Dashboard 页面组件 +export default { + template: ` +
+ + +
+
+
+
+
+
+
+ +
+
+
+
总用户数
+
1,234
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
总订单数
+
567
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
总许愿数
+
890
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
总收入
+
¥12,345
+
+
+
+
+
+
+ +
+
+

最近订单

+
+ + + + + + + + + + + + + + + + + + + +
订单号用户金额状态时间
L20240101001用户A¥10.00 + 已支付 + 2024-01-01 12:00
+
+
+
+
+
+
+ ` +} diff --git a/server/web/static/js/pages/Orders.js b/server/web/static/js/pages/Orders.js new file mode 100644 index 0000000..4f682b1 --- /dev/null +++ b/server/web/static/js/pages/Orders.js @@ -0,0 +1,71 @@ +// Orders 页面组件 +export default { + template: ` +
+ + +
+
+
+
+

订单列表

+
+ + + + + + + + + + + + + + + + + + + + + +
订单号类型商品金额状态创建时间
L20240101001许愿普通许愿条¥1.00 + 已支付 + 2024-01-01 12:00
+
+
+
+
+
+
+ ` +} diff --git a/server/web/static/js/pages/Settings.js b/server/web/static/js/pages/Settings.js new file mode 100644 index 0000000..7ead1ff --- /dev/null +++ b/server/web/static/js/pages/Settings.js @@ -0,0 +1,68 @@ +// Settings 页面组件 +export default { + template: ` +
+ + +
+
+
+
+

系统设置

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+ ` +} diff --git a/server/web/static/js/pages/Users.js b/server/web/static/js/pages/Users.js new file mode 100644 index 0000000..b9df25d --- /dev/null +++ b/server/web/static/js/pages/Users.js @@ -0,0 +1,69 @@ +// Users 页面组件 +export default { + template: ` +
+ + +
+
+
+
+

用户列表

+
+ + + + + + + + + + + + + + + + + + + +
ID昵称OpenID注册时间状态
1微信用户mock_openid_xxx2024-01-01 + 正常 +
+
+
+
+
+
+
+ ` +} diff --git a/server/web/static/js/pages/Wishes.js b/server/web/static/js/pages/Wishes.js new file mode 100644 index 0000000..b09d57b --- /dev/null +++ b/server/web/static/js/pages/Wishes.js @@ -0,0 +1,69 @@ +// Wishes 页面组件 +export default { + template: ` +
+ + +
+
+
+
+

许愿列表

+
+ + + + + + + + + + + + + + + + + + + +
ID内容类型用户创建时间
1愿世界和平 + 付费 + 用户A2024-01-01 12:00
+
+
+
+
+
+
+ ` +}