diff --git a/.gitignore b/.gitignore index 949a4ec..f079246 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,7 @@ desktop.ini .dotnet/ .nuget/ .userdata/ +dotnet-sdk/ # ===================== Docker ===================== docker-compose.override.yml diff --git a/auth-service b/auth-service new file mode 100644 index 0000000..71e080e Binary files /dev/null and b/auth-service differ diff --git a/launcher/MRCC.Launcher/App.xaml.cs b/launcher/MRCC.Launcher/App.xaml.cs index b9a5d57..b99f7d3 100644 --- a/launcher/MRCC.Launcher/App.xaml.cs +++ b/launcher/MRCC.Launcher/App.xaml.cs @@ -4,4 +4,11 @@ namespace MRCC.Launcher; public partial class App : Application { + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + DispatcherUnhandledException += (s, a) => Program.Log($"UI异常: {a.Exception}"); + AppDomain.CurrentDomain.UnhandledException += (s, a) => Program.Log($"未处理异常: {a.ExceptionObject}"); + TaskScheduler.UnobservedTaskException += (s, a) => Program.Log($"任务异常: {a.Exception}"); + } } diff --git a/launcher/MRCC.Launcher/GameWindow.xaml b/launcher/MRCC.Launcher/GameWindow.xaml new file mode 100644 index 0000000..c4e95bc --- /dev/null +++ b/launcher/MRCC.Launcher/GameWindow.xaml @@ -0,0 +1,5 @@ + + diff --git a/launcher/MRCC.Launcher/GameWindow.xaml.cs b/launcher/MRCC.Launcher/GameWindow.xaml.cs new file mode 100644 index 0000000..f744425 --- /dev/null +++ b/launcher/MRCC.Launcher/GameWindow.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace MRCC.Launcher; + +public partial class GameWindow : Window +{ + public GameWindow() + { + InitializeComponent(); + } +} diff --git a/launcher/MRCC.Launcher/MRCC.Launcher.csproj b/launcher/MRCC.Launcher/MRCC.Launcher.csproj index f7c8643..39ad81b 100644 --- a/launcher/MRCC.Launcher/MRCC.Launcher.csproj +++ b/launcher/MRCC.Launcher/MRCC.Launcher.csproj @@ -1,31 +1,25 @@ - WinExe net10.0-windows true true MRCC.Launcher - MRCC.Launcher + RedCircuit + ..\..\build\ enable 14.0 enable app.manifest - MRCC.Launcher.Program + Program + true + false + true - - + - - + - - - - PCL.metadata.json - - - diff --git a/launcher/MRCC.Launcher/MainWindow.xaml b/launcher/MRCC.Launcher/MainWindow.xaml index f0fc1a7..f0e5d02 100644 --- a/launcher/MRCC.Launcher/MainWindow.xaml +++ b/launcher/MRCC.Launcher/MainWindow.xaml @@ -1,128 +1,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
WASD 移动 · 空格跳跃 · 鼠标视角
左键破坏 · 右键放置 · 数字 1-9 快捷栏
E 打开背包 · ESC 暂停
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ RedCircuit AI 助手 + +
+
+
AI 助手你好!我是 RedCircuit AI 助手。
我可以帮你解答电路问题、推荐搭建方案。
试试问我:"如何搭建一个 AND 门?"
+
+
+ + +
+
F11 释放鼠标 · 按钮在右下角
+
+ diff --git a/prototype/webgl-test.html b/prototype/webgl-test.html new file mode 100644 index 0000000..88b3fd1 --- /dev/null +++ b/prototype/webgl-test.html @@ -0,0 +1,131 @@ + +WebGL Test + +

WebView2 / WebGL 完整诊断

+
+ + + diff --git a/redcircuit.log b/redcircuit.log new file mode 100644 index 0000000..96c7565 --- /dev/null +++ b/redcircuit.log @@ -0,0 +1,23 @@ +[23:51:26.073] === RedCircuit 开发者模式 === +[23:51:26.099] 工作目录: D:\Code\MRCC +[23:51:26.104] EXE目录: D:\Code\MRCC\build\ +[23:51:26.161] 游戏目录: D:\Code\MRCC\build\game +[23:51:26.194] HTTP Server: http://localhost:55698/ +[23:51:26.205] 游戏URL: http://localhost:55698/voxel-world.html +[23:51:26.336] 游戏窗口已创建 +[23:51:26.648] === 游戏窗口 OnLoaded === +[23:51:26.649] URL: http://localhost:55698/voxel-world.html +[23:51:26.652] WebView2 Runtime: 148.0.3967.96 +[23:51:26.652] 初始化 WebView2... +[23:51:26.917] WebView2 初始化完成 +[23:51:26.918] 导航到: http://localhost:55698/voxel-world.html +[23:51:26.944] 导航开始: http://localhost:55698/voxel-world.html +[23:51:27.205] 导航完成: HTTP 200 +[23:51:27.207] 注入监控脚本... +[23:51:27.224] 监控脚本已注入 +[23:51:29.269] [JS] "THREE:loaded" +[23:51:29.270] [JS] "WEBGL:available" +[23:51:29.281] [JS] "WEBGL_CTX:created" +[23:51:32.261] [JS] "WORLD:meshes=N/A scene_children=17" +[23:51:32.263] [JS] "FALLBACK:ground_added" +[23:51:50.952] 游戏窗口关闭,停止HTTP服务器 diff --git a/server/cmd/auth-service/main.go b/server/cmd/auth-service/main.go index 9b11b02..db2afcf 100644 --- a/server/cmd/auth-service/main.go +++ b/server/cmd/auth-service/main.go @@ -2,7 +2,9 @@ package main import ( "log" + "os" + "mrcc/internal/auth" "mrcc/internal/config" "mrcc/internal/httpserver" "mrcc/internal/logger" @@ -14,12 +16,25 @@ func main() { srv := httpserver.New(cfg) - // TODO: 注册认证路由 - // r := srv.Group("/api/auth") - // r.POST("/register", ...) - // r.POST("/login", ...) + // JWT 密钥:默认开发密钥,生产环境通过 AUTH_JWT_SECRET 覆盖 + secret := os.Getenv("AUTH_JWT_SECRET") + if secret == "" { + secret = "mrcc-dev-secret-change-me" + } - log.Printf("auth-service listening on %s", cfg.Address()) + store := auth.NewUserStore() + jwtMgr := auth.NewJWTManager(secret) + h := auth.NewHandler(store, jwtMgr) + + // 认证路由: /api/auth/register|login|me + authGroup := srv.Group("/api/auth") + h.RegisterAuthRoutes(authGroup) + + // 经济路由: /api/daily-reward|spend|buy|use-item + apiGroup := srv.Group("/api") + h.RegisterEconomyRoutes(apiGroup) + + log.Printf("auth-service listening on %s (endpoints: /api/auth/*, /api/daily-reward ...)", cfg.Address()) if err := srv.Run(cfg.Address()); err != nil { log.Fatal(err) } diff --git a/server/go.mod b/server/go.mod index 5554de8..3d1a358 100644 --- a/server/go.mod +++ b/server/go.mod @@ -4,7 +4,9 @@ go 1.22 require ( github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/gorilla/websocket v1.5.3 + golang.org/x/crypto v0.23.0 ) require ( @@ -22,13 +24,12 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..5fd0e25 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,93 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +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.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +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/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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +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/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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/server/internal/auth/auth.go b/server/internal/auth/auth.go new file mode 100644 index 0000000..b7ab449 --- /dev/null +++ b/server/internal/auth/auth.go @@ -0,0 +1,324 @@ +package auth + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +// User 用户数据 +type User struct { + ID string `json:"id"` + Username string `json:"username"` + PasswordHash string `json:"-"` // 不返回给客户端 + CreatedAt int64 `json:"created_at"` + Level int `json:"level"` + Exp int `json:"exp"` + Diamonds int `json:"diamonds"` + RedstoneCoins int64 `json:"redstone_coins"` + GoldCoins int `json:"gold_coins"` + LastLoginDate string `json:"last_login_date"` + Inventory map[string]int `json:"inventory"` // itemId → count + EquippedItem string `json:"equipped_item"` // 当前装备的道具ID + SpeedBoostUntil int64 `json:"speed_boost_until"` // 速度加成到期时间(unix) + PurchaseHistory []PurchaseRecord `json:"purchase_history"` // 最近50条购买记录 +} + +// PurchaseRecord 购买记录 +type PurchaseRecord struct { + ItemID string `json:"item_id"` + ItemName string `json:"item_name"` + Currency string `json:"currency"` + Amount int64 `json:"amount"` + Timestamp int64 `json:"timestamp"` +} + +// UserStore 内存用户存储 (后续可替换为 PostgreSQL) +type UserStore struct { + mu sync.RWMutex + users map[string]*User // key = username (lowercase) +} + +func NewUserStore() *UserStore { + return &UserStore{users: make(map[string]*User)} +} + +func (s *UserStore) Create(username, password string) (*User, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.users[username]; exists { + return nil, errors.New("用户名已存在") + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("密码加密失败: %w", err) + } + + now := time.Now() + user := &User{ + ID: fmt.Sprintf("u_%d", now.UnixNano()), + Username: username, + PasswordHash: string(hash), + CreatedAt: now.Unix(), + Level: 1, + Exp: 0, + Diamonds: 20, + RedstoneCoins: 3000, + GoldCoins: 1, + LastLoginDate: "", // 留空:注册当天可正常签到领奖 + Inventory: make(map[string]int), + PurchaseHistory: []PurchaseRecord{}, + } + s.users[username] = user + return user, nil +} + +func (s *UserStore) GetByUsername(username string) (*User, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + u, ok := s.users[username] + return u, ok +} + +func (s *UserStore) GetByID(id string) (*User, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + for _, u := range s.users { + if u.ID == id { + return u, true + } + } + return nil, false +} + +func (s *UserStore) VerifyPassword(username, password string) (*User, error) { + user, ok := s.GetByUsername(username) + if !ok { + return nil, errors.New("用户不存在") + } + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { + return nil, errors.New("密码错误") + } + return user, nil +} + +// DailyLogin 每日登录奖励。返回奖励详情和是否已领取。 +func (s *UserStore) DailyLogin(userID string) (map[string]interface{}, error) { + s.mu.Lock() + defer s.mu.Unlock() + + var user *User + for _, u := range s.users { + if u.ID == userID { + user = u + break + } + } + if user == nil { + return nil, errors.New("用户不存在") + } + + today := time.Now().Format("2006-01-02") + if user.LastLoginDate == today { + return map[string]interface{}{ + "claimed": true, + "message": "今日已领取", + }, nil + } + + // 发放奖励 + const rewardDiamonds = 20 + const rewardRedstoneCoins = 3000 + const rewardGoldCoins = 1 + + user.Diamonds += rewardDiamonds + user.RedstoneCoins += rewardRedstoneCoins + user.GoldCoins += rewardGoldCoins + user.LastLoginDate = today + + return map[string]interface{}{ + "claimed": false, + "message": "领取成功", + "diamonds": rewardDiamonds, + "redstone": rewardRedstoneCoins, + "gold": rewardGoldCoins, + "total_diamonds": user.Diamonds, + "total_redstone": user.RedstoneCoins, + "total_gold": user.GoldCoins, + }, nil +} + +// SpendCurrency 消费货币,返回成功或余额不足错误 +func (s *UserStore) SpendCurrency(userID, currency string, amount int64) error { + s.mu.Lock() + defer s.mu.Unlock() + + var user *User + for _, u := range s.users { + if u.ID == userID { + user = u + break + } + } + if user == nil { + return errors.New("用户不存在") + } + + switch currency { + case "diamonds": + if user.Diamonds < int(amount) { + return fmt.Errorf("钻石不足,当前: %d,需要: %d", user.Diamonds, amount) + } + user.Diamonds -= int(amount) + case "redstone_coins": + if user.RedstoneCoins < amount { + return fmt.Errorf("红石币不足,当前: %d,需要: %d", user.RedstoneCoins, amount) + } + user.RedstoneCoins -= amount + case "gold_coins": + if user.GoldCoins < int(amount) { + return fmt.Errorf("金币不足,当前: %d,需要: %d", user.GoldCoins, amount) + } + user.GoldCoins -= int(amount) + default: + return fmt.Errorf("未知货币类型: %s", currency) + } + return nil +} + +// BuyItem 购买商品 (货币扣款 + 库存追踪 + 交易记录) +func (s *UserStore) BuyItem(userID, itemID, itemName, currency string, amount int64, quantity int) (map[string]interface{}, error) { + s.mu.Lock() + defer s.mu.Unlock() + var user *User + for _, u := range s.users { if u.ID == userID { user = u; break } } + if user == nil { return nil, errors.New("用户不存在") } + switch currency { + case "diamonds": if user.Diamonds < int(amount) { return nil, fmt.Errorf("钻石不足") }; user.Diamonds -= int(amount) + case "redstone_coins": if user.RedstoneCoins < amount { return nil, fmt.Errorf("红石币不足") }; user.RedstoneCoins -= amount + case "gold_coins": if user.GoldCoins < int(amount) { return nil, fmt.Errorf("金币不足") }; user.GoldCoins -= int(amount) + default: return nil, fmt.Errorf("未知货币") + } + if user.Inventory == nil { user.Inventory = make(map[string]int) } + user.Inventory[itemID] += quantity + if itemID == "speed_boost" { user.SpeedBoostUntil = time.Now().Unix() + 3600 } + rec := PurchaseRecord{ItemID: itemID, ItemName: itemName, Currency: currency, Amount: amount, Timestamp: time.Now().Unix()} + user.PurchaseHistory = append([]PurchaseRecord{rec}, user.PurchaseHistory...) + if len(user.PurchaseHistory) > 50 { user.PurchaseHistory = user.PurchaseHistory[:50] } + return map[string]interface{}{"message": "购买成功", "item_id": itemID, "quantity": quantity, "user": userToMap(user)}, nil +} + +// UseItem 使用道具 +func (s *UserStore) UseItem(userID, itemID string) (map[string]interface{}, error) { + s.mu.Lock(); defer s.mu.Unlock() + var user *User + for _, u := range s.users { if u.ID == userID { user = u; break } } + if user == nil { return nil, errors.New("用户不存在") } + if user.Inventory == nil || user.Inventory[itemID] <= 0 { return nil, errors.New("道具数量不足") } + user.Inventory[itemID]-- + msg := "使用成功" + switch itemID { + case "speed_boost": + user.SpeedBoostUntil = time.Now().Unix() + 3600 + user.EquippedItem = itemID + msg = "速度加成已激活(1小时)" + case "exp_boost": + user.Exp += 500 + if user.Exp >= user.Level*1000 { + user.Level++ + user.Exp -= (user.Level - 1) * 1000 + msg = fmt.Sprintf("升级! Lv.%d", user.Level) + } else { + msg = "获得500经验值" + } + case "diamond_pack": + user.Diamonds += 10 + msg = "获得10钻石" + case "gold_pack": + user.GoldCoins += 5 + msg = "获得5金币" + case "rs_pack": + user.RedstoneCoins += 10000 + msg = "获得10000红石币" + } + return map[string]interface{}{"message": msg, "user": userToMap(user)}, nil +} + +// userToMap 将 User 转换为返回给客户端的 map +func userToMap(u *User) map[string]interface{} { + inv := u.Inventory + if inv == nil { inv = make(map[string]int) } + hist := u.PurchaseHistory + if hist == nil { hist = []PurchaseRecord{} } + return map[string]interface{}{ + "id": u.ID, + "username": u.Username, + "level": u.Level, + "exp": u.Exp, + "diamonds": u.Diamonds, + "redstone_coins": u.RedstoneCoins, + "gold_coins": u.GoldCoins, + "last_login_date": u.LastLoginDate, + "created_at": u.CreatedAt, + "inventory": inv, + "equipped_item": u.EquippedItem, + "speed_boost_until": u.SpeedBoostUntil, + "purchase_history": hist, + } +} + +// ==================== JWT ==================== + +type JWTManager struct { + secretKey []byte + expires time.Duration +} + +func NewJWTManager(secret string) *JWTManager { + return &JWTManager{ + secretKey: []byte(secret), + expires: 24 * time.Hour, + } +} + +type Claims struct { + UserID string `json:"uid"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +func (m *JWTManager) Generate(user *User) (string, error) { + claims := &Claims{ + UserID: user.ID, + Username: user.Username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.expires)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(m.secretKey) +} + +func (m *JWTManager) Verify(tokenStr string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) + } + return m.secretKey, nil + }) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, errors.New("invalid token") + } + return claims, nil +} diff --git a/server/internal/auth/handler.go b/server/internal/auth/handler.go new file mode 100644 index 0000000..8751a09 --- /dev/null +++ b/server/internal/auth/handler.go @@ -0,0 +1,225 @@ +package auth + +import ( + "net/http" + "strings" + + "mrcc/pkg/response" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + store *UserStore + jwt *JWTManager +} + +func NewHandler(store *UserStore, jwtMgr *JWTManager) *Handler { + return &Handler{store: store, jwt: jwtMgr} +} + +// RegisterAuthRoutes 注册认证路由 (/api/auth) +func (h *Handler) RegisterAuthRoutes(r *gin.RouterGroup) { + r.POST("/register", h.Register) + r.POST("/login", h.Login) + r.GET("/me", h.AuthMiddleware(), h.Me) +} + +// RegisterEconomyRoutes 注册经济路由 (/api) +func (h *Handler) RegisterEconomyRoutes(r *gin.RouterGroup) { + r.POST("/daily-reward", h.AuthMiddleware(), h.DailyReward) + r.POST("/spend", h.AuthMiddleware(), h.Spend) + r.POST("/buy", h.AuthMiddleware(), h.Buy) + r.POST("/use-item", h.AuthMiddleware(), h.UseItem) +} + +// RegisterRoutes 注册全部路由(兼容旧调用) +func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { + h.RegisterAuthRoutes(r) + h.RegisterEconomyRoutes(r) +} + +type registerReq struct { + Username string `json:"username" binding:"required,min=3,max=20"` + Password string `json:"password" binding:"required,min=6,max=50"` +} + +// Register 用户注册 +func (h *Handler) Register(c *gin.Context) { + var req registerReq + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "参数无效: "+err.Error()) + return + } + + user, err := h.store.Create(req.Username, req.Password) + if err != nil { + response.Error(c, http.StatusConflict, err.Error()) + return + } + + token, err := h.jwt.Generate(user) + if err != nil { + response.Error(c, http.StatusInternalServerError, "生成令牌失败") + return + } + + response.OK(c, gin.H{ + "token": token, + "user": userToMap(user), + }) +} + +// Login 用户登录 +func (h *Handler) Login(c *gin.Context) { + var req registerReq + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "参数无效: "+err.Error()) + return + } + + user, err := h.store.VerifyPassword(req.Username, req.Password) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + + token, err := h.jwt.Generate(user) + if err != nil { + response.Error(c, http.StatusInternalServerError, "生成令牌失败") + return + } + + response.OK(c, gin.H{ + "token": token, + "user": userToMap(user), + }) +} + +// Me 获取当前用户完整信息 +func (h *Handler) Me(c *gin.Context) { + claims, exists := c.Get("claims") + if !exists { + response.Error(c, http.StatusUnauthorized, "未认证") + return + } + cl := claims.(*Claims) + user, ok := h.store.GetByID(cl.UserID) + if !ok { + response.Error(c, http.StatusNotFound, "用户不存在") + return + } + response.OK(c, userToMap(user)) +} + +// DailyReward 每日登录奖励 +func (h *Handler) DailyReward(c *gin.Context) { + claims, exists := c.Get("claims") + if !exists { + response.Error(c, http.StatusUnauthorized, "未认证") + return + } + cl := claims.(*Claims) + + result, err := h.store.DailyLogin(cl.UserID) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + + response.OK(c, result) +} + +type spendReq struct { + Currency string `json:"currency" binding:"required"` + Amount int64 `json:"amount" binding:"required,min=1"` +} + +// Spend 消费货币 +func (h *Handler) Spend(c *gin.Context) { + claims, exists := c.Get("claims") + if !exists { + response.Error(c, http.StatusUnauthorized, "未认证") + return + } + cl := claims.(*Claims) + + var req spendReq + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "参数无效: "+err.Error()) + return + } + + if err := h.store.SpendCurrency(cl.UserID, req.Currency, req.Amount); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + + // 返回最新余额 + user, _ := h.store.GetByID(cl.UserID) + response.OK(c, gin.H{ + "message": "消费成功", + "currency": req.Currency, + "amount": req.Amount, + "user": userToMap(user), + }) +} + +type buyReq struct { + ItemID string `json:"item_id" binding:"required"` + ItemName string `json:"item_name" binding:"required"` + Currency string `json:"currency" binding:"required"` + Amount int64 `json:"amount" binding:"required,min=1"` + Quantity int `json:"quantity"` +} + +// Buy 购买商品 (含库存追踪) +func (h *Handler) Buy(c *gin.Context) { + claims, _ := c.Get("claims"); cl := claims.(*Claims) + var req buyReq + if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, err.Error()); return } + if req.Quantity <= 0 { req.Quantity = 1 } + result, err := h.store.BuyItem(cl.UserID, req.ItemID, req.ItemName, req.Currency, req.Amount, req.Quantity) + if err != nil { response.Error(c, http.StatusBadRequest, err.Error()); return } + response.OK(c, result) +} + +type useReq struct { + ItemID string `json:"item_id" binding:"required"` +} + +// UseItem 使用道具 +func (h *Handler) UseItem(c *gin.Context) { + claims, _ := c.Get("claims"); cl := claims.(*Claims) + var req useReq + if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, err.Error()); return } + result, err := h.store.UseItem(cl.UserID, req.ItemID) + if err != nil { response.Error(c, http.StatusBadRequest, err.Error()); return } + response.OK(c, result) +} + +// AuthMiddleware JWT 认证中间件 +func (h *Handler) AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + auth := c.GetHeader("Authorization") + if auth == "" { + response.Error(c, http.StatusUnauthorized, "缺少认证令牌") + c.Abort() + return + } + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + response.Error(c, http.StatusUnauthorized, "认证格式错误") + c.Abort() + return + } + claims, err := h.jwt.Verify(parts[1]) + if err != nil { + response.Error(c, http.StatusUnauthorized, "令牌无效或已过期") + c.Abort() + return + } + c.Set("claims", claims) + c.Next() + } +}