更新前后端代码

This commit is contained in:
2026-08-23 00:48:10 +08:00
parent 1b5468995f
commit 5c1313e4f6
74 changed files with 9995 additions and 306 deletions
+28
View File
@@ -0,0 +1,28 @@
# FileStorageSystem 配置文件
server:
port: 8100
mode: debug # debug/release
domain: https://file.yunzer.cn # 生产环境域名
database:
driver: sqlite
source: ./storage/data.db
storage:
root: ./storage/files # 文件存储根目录
backups: ./storage/backups # 数据库备份目录
admin:
username: admin
password: admin123 # 首次运行后请修改
email: admin@example.com
role: super_admin # 默认角色
jwt:
secret: please-change-this-secret-key # 生产环境请修改为随机密钥
expire: 24h # Token过期时间
log:
level: info
dir: ./storage/logs
max_days: 7
BIN
View File
Binary file not shown.
+66
View File
@@ -0,0 +1,66 @@
module filestoragesystem
go 1.21
require (
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/spf13/viper v1.19.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.26.0
golang.org/x/time v0.6.0
gorm.io/gorm v1.25.11
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // 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.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/hashicorp/hcl v1.0.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.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
+163
View File
@@ -0,0 +1,163 @@
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
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/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
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.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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/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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+139
View File
@@ -0,0 +1,139 @@
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/viper"
)
// Config 全局配置结构
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Storage StorageConfig `mapstructure:"storage"`
Admin AdminConfig `mapstructure:"admin"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"` // debug / release / test
Domain string `mapstructure:"domain"` // 生产环境域名
}
type DatabaseConfig struct {
Driver string `mapstructure:"driver"`
Source string `mapstructure:"source"`
}
type StorageConfig struct {
Root string `mapstructure:"root"` // 文件存储根目录
Backups string `mapstructure:"backups"` // 备份目录
}
type AdminConfig struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Email string `mapstructure:"email"`
Role string `mapstructure:"role"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
Expire string `mapstructure:"expire"` // 如 24h
}
type LogConfig struct {
Level string `mapstructure:"level"` // debug/info/warn/error
Dir string `mapstructure:"dir"` // 日志文件目录,为空则仅输出到控制台
MaxDays int `mapstructure:"max_days"` // 日志保留天数
}
var cfg *Config
// Load 加载配置文件,不存在时使用默认值并生成示例配置
func Load(path string) (*Config, error) {
v := viper.New()
setDefaults(v)
// 设置环境变量前缀
v.SetEnvPrefix("FSS")
v.AutomaticEnv()
if path == "" {
path = "config.yaml"
}
// 尝试加载配置文件
if _, err := os.Stat(path); err == nil {
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
} else {
// 配置文件不存在时生成一份默认配置
_ = v.SafeWriteConfigAs(path)
}
c := &Config{}
if err := v.Unmarshal(c); err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
cfg = c
return c, nil
}
func setDefaults(v *viper.Viper) {
v.SetDefault("server.port", "8100")
v.SetDefault("server.mode", "debug")
v.SetDefault("server.domain", "https://file.yunzer.cn")
v.SetDefault("database.driver", "sqlite")
v.SetDefault("database.source", "./storage/data.db")
v.SetDefault("storage.root", "./storage/files")
v.SetDefault("storage.backups", "./storage/backups")
v.SetDefault("admin.username", "admin")
v.SetDefault("admin.password", "admin123")
v.SetDefault("admin.email", "admin@example.com")
v.SetDefault("admin.role", "super_admin")
v.SetDefault("jwt.secret", "please-change-this-secret-key")
v.SetDefault("jwt.expire", "24h")
v.SetDefault("log.level", "info")
v.SetDefault("log.dir", "./storage/logs")
v.SetDefault("log.max_days", 7)
}
// Get 获取全局配置
func Get() *Config {
if cfg == nil {
c, _ := Load("")
cfg = c
}
return cfg
}
// EnsureDirs 确保所有存储相关目录存在
func (c *Config) EnsureDirs() error {
dirs := []string{
filepath.Dir(c.Database.Source),
c.Storage.Root,
c.Storage.Backups,
c.Log.Dir,
}
for _, d := range dirs {
if d == "" {
continue
}
if err := os.MkdirAll(d, 0o755); err != nil {
return fmt.Errorf("创建目录 %s 失败: %w", d, err)
}
}
return nil
}
+354
View File
@@ -0,0 +1,354 @@
package controller
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
)
// AdminController 系统管理控制器
type AdminController struct {
*Base
}
// NewAdminController 创建系统管理控制器
func NewAdminController(b *Base) *AdminController { return &AdminController{Base: b} }
// ---- 用户管理 ----
// ListUsers 用户列表
func (ctl *AdminController) ListUsers(c *gin.Context) {
page, pageSize := utils.ParsePage(c)
users, total, err := ctl.svc.User.AdminListUsers(
c.Query("keyword"), c.Query("role_id"), c.Query("status"), page, pageSize)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OKPage(c, users, total, page, pageSize)
}
// GetUser 用户详情
func (ctl *AdminController) GetUser(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
u, err := ctl.svc.User.AdminGetUser(id)
if err != nil {
utils.NotFound(c, "用户不存在")
return
}
utils.OK(c, u)
}
type adminCreateUserReq struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
RoleID uint `json:"role_id"`
StorageLimit int64 `json:"storage_limit"`
}
// CreateUser 创建用户
func (ctl *AdminController) CreateUser(c *gin.Context) {
var req adminCreateUserReq
if !bindJSON(c, &req) {
return
}
u, err := ctl.svc.User.AdminCreateUser(req.Username, req.Email, req.Password, req.RoleID, req.StorageLimit)
if err != nil {
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "user", 0, req.Username, false, err.Error())
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "user", u.ID, u.Username, true, "")
utils.OK(c, u)
}
type adminUpdateUserReq struct {
Email string `json:"email"`
RoleID uint `json:"role_id"`
StorageLimit int64 `json:"storage_limit"`
}
// UpdateUser 编辑用户
func (ctl *AdminController) UpdateUser(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req adminUpdateUserReq
if !bindJSON(c, &req) {
return
}
u, err := ctl.svc.User.AdminUpdateUser(id, req.Email, req.RoleID, req.StorageLimit)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "user", id, u.Username, true, "")
utils.OK(c, u)
}
// DeleteUser 删除用户
func (ctl *AdminController) DeleteUser(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
if err := ctl.svc.User.AdminDeleteUser(id); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "user", id, "", true, "")
utils.OKMsg(c, "用户已删除")
}
type setUserStatusReq struct {
Status int8 `json:"status"` // 1启用 0禁用(不加required,0为合法值)
}
// SetUserStatus 启用/禁用用户
func (ctl *AdminController) SetUserStatus(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req setUserStatusReq
if !bindJSON(c, &req) {
return
}
if req.Status != 0 && req.Status != 1 {
utils.FailMsg(c, "status仅支持0或1")
return
}
if err := ctl.svc.User.AdminSetUserStatus(id, req.Status); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "user", id, "status", true, "")
utils.OKMsg(c, "状态已更新")
}
type assignRoleReq struct {
RoleID uint `json:"role_id" binding:"required"`
}
// AssignRole 修改用户角色
func (ctl *AdminController) AssignRole(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req assignRoleReq
if !bindJSON(c, &req) {
return
}
if err := ctl.svc.User.AdminAssignRole(id, req.RoleID); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "user", id, "role", true, "")
utils.OKMsg(c, "角色已更新")
}
type resetPasswordReq struct {
Password string `json:"password" binding:"required"`
}
// ResetPassword 重置用户密码
func (ctl *AdminController) ResetPassword(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req resetPasswordReq
if !bindJSON(c, &req) {
return
}
if err := ctl.svc.User.AdminResetPassword(id, req.Password); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "user", id, "password", true, "")
utils.OKMsg(c, "密码已重置")
}
// ---- 系统设置 ----
// GetSettings 获取全部设置
func (ctl *AdminController) GetSettings(c *gin.Context) {
groups, err := ctl.svc.Admin.GetSettings()
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, groups)
}
// GetSettingsByGroup 按分组获取设置
func (ctl *AdminController) GetSettingsByGroup(c *gin.Context) {
group := c.Param("group")
settings, err := ctl.svc.Admin.GetSettingsByGroup(group)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, settings)
}
// UpdateSettings 批量更新设置
func (ctl *AdminController) UpdateSettings(c *gin.Context) {
var req map[string]string
if !bindJSON(c, &req) {
return
}
if err := ctl.svc.Admin.UpdateSettings(req); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "system", 0, "settings", true, "")
utils.OKMsg(c, "设置已更新")
}
// UpdateSettingsByGroup 按分组更新设置
func (ctl *AdminController) UpdateSettingsByGroup(c *gin.Context) {
var req map[string]string
if !bindJSON(c, &req) {
return
}
if err := ctl.svc.Admin.UpdateSettings(req); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "system", 0, "settings:"+c.Param("group"), true, "")
utils.OKMsg(c, "设置已更新")
}
// ---- 日志管理 ----
// ListOperationLogs 操作日志列表
func (ctl *AdminController) ListOperationLogs(c *gin.Context) {
page, pageSize := utils.ParsePage(c)
f := repository.OpLogFilter{
Username: c.Query("username"),
Action: c.Query("action"),
Status: c.Query("status"),
Days: parseIntQuery(c, "days", 0),
Page: page,
PageSize: pageSize,
}
if uid := parseUintQuery(c, "user_id"); uid > 0 {
f.UserID = &uid
}
logs, total, err := ctl.svc.Admin.ListOperationLogs(f)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OKPage(c, logs, total, page, pageSize)
}
// GetOperationLog 操作日志详情
func (ctl *AdminController) GetOperationLog(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
l, err := ctl.svc.Admin.GetOperationLog(id)
if err != nil {
utils.NotFound(c, "日志不存在")
return
}
utils.OK(c, l)
}
// ListSystemLogs 系统日志列表
func (ctl *AdminController) ListSystemLogs(c *gin.Context) {
page, pageSize := utils.ParsePage(c)
f := repository.SysLogFilter{
Level: c.Query("level"),
Module: c.Query("module"),
Days: parseIntQuery(c, "days", 0),
Page: page,
PageSize: pageSize,
}
logs, total, err := ctl.svc.Admin.ListSystemLogs(f)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OKPage(c, logs, total, page, pageSize)
}
// GetSystemLog 系统日志详情
func (ctl *AdminController) GetSystemLog(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
l, err := ctl.svc.Admin.GetSystemLog(id)
if err != nil {
utils.NotFound(c, "日志不存在")
return
}
utils.OK(c, l)
}
// CleanupLogs 清理历史日志
func (ctl *AdminController) CleanupLogs(c *gin.Context) {
days := parseIntQuery(c, "days", 30)
opDeleted, sysDeleted, err := ctl.svc.Admin.CleanupLogs(days)
if err != nil {
utils.ServerError(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "system", 0, "logs", true, "")
utils.OK(c, gin.H{
"days": days,
"operation_logs_deleted": opDeleted,
"system_logs_deleted": sysDeleted,
})
}
// ---- 数据备份 ----
// Backup 手动备份数据库
func (ctl *AdminController) Backup(c *gin.Context) {
name, err := ctl.svc.Admin.BackupDatabase()
if err != nil {
utils.ServerError(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "system", 0, "backup:"+name, true, "")
utils.OK(c, gin.H{"name": name})
}
// ListBackups 备份列表
func (ctl *AdminController) ListBackups(c *gin.Context) {
list, err := ctl.svc.Admin.ListBackups()
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, list)
}
// DownloadBackup 下载备份文件
func (ctl *AdminController) DownloadBackup(c *gin.Context) {
name := c.Query("name")
path, err := ctl.svc.Admin.BackupPath(name)
if err != nil {
utils.NotFound(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "download", "system", 0, "backup:"+name, true, "")
c.FileAttachment(path, name)
}
// SiteStatus 公开站点状态接口
func (ctl *AdminController) SiteStatus(c *gin.Context) {
utils.OK(c, ctl.svc.Admin.SiteStatus())
}
+127
View File
@@ -0,0 +1,127 @@
package controller
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/utils"
)
// AuthController 认证控制器
type AuthController struct {
*Base
}
// NewAuthController 创建认证控制器
func NewAuthController(b *Base) *AuthController { return &AuthController{Base: b} }
type registerReq struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
// Register 用户注册
func (ctl *AuthController) Register(c *gin.Context) {
var req registerReq
if !bindJSON(c, &req) {
return
}
u, err := ctl.svc.Auth.Register(req.Username, req.Email, req.Password)
if err != nil {
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "user", 0, req.Username, false, err.Error())
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "user", u.ID, u.Username, true, "")
utils.OK(c, gin.H{"id": u.ID, "username": u.Username, "email": u.Email})
}
type loginReq struct {
Account string `json:"account" binding:"required"` // 用户名或邮箱
Password string `json:"password" binding:"required"`
}
// Login 用户登录
func (ctl *AuthController) Login(c *gin.Context) {
var req loginReq
if !bindJSON(c, &req) {
return
}
ip := utils.ClientIP(c)
u, token, err := ctl.svc.Auth.Login(req.Account, req.Password, ip)
if err != nil {
ctl.svc.RecordOpLog(ctl.opCtx(c), "login", "user", 0, req.Account, false, err.Error())
utils.Unauthorized(c, err.Error())
return
}
oc := ctl.opCtx(c)
uid := u.ID
oc.UserID = &uid
oc.Username = u.Username
ctl.svc.RecordOpLog(oc, "login", "user", u.ID, u.Username, true, "")
utils.OK(c, gin.H{
"token": token,
"user": gin.H{
"id": u.ID,
"username": u.Username,
"email": u.Email,
"role": u.Role,
"storage_used": u.StorageUsed,
"storage_limit": u.StorageLimit,
},
})
}
type createAPIKeyReq struct {
Name string `json:"name"`
}
// CreateAPIKey 生成API密钥
func (ctl *AuthController) CreateAPIKey(c *gin.Context) {
var req createAPIKeyReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
key, secret, err := ctl.svc.Auth.CreateAPIKey(userID, req.Name)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "apikey", key.ID, key.Name, true, "")
utils.OK(c, gin.H{
"id": key.ID,
"access_key": key.AccessKey,
"secret_key": secret, // 仅创建时返回一次
"name": key.Name,
"notice": "secret_key仅在创建时展示,请妥善保存",
})
}
// ListAPIKeys 密钥列表
func (ctl *AuthController) ListAPIKeys(c *gin.Context) {
userID := middleware.CurrentUserID(c)
keys, err := ctl.svc.Auth.ListAPIKeys(userID)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, keys)
}
// DeleteAPIKey 删除密钥
func (ctl *AuthController) DeleteAPIKey(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
if err := ctl.svc.Auth.DeleteAPIKey(userID, id); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "apikey", id, "", true, "")
utils.OKMsg(c, "密钥已删除")
}
+71
View File
@@ -0,0 +1,71 @@
package controller
import (
"strconv"
"github.com/gin-gonic/gin"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/service"
"filestoragesystem/internal/utils"
)
// Base 控制器基类
type Base struct {
svc *service.Services
}
// NewBase 创建基类
func NewBase(svc *service.Services) *Base { return &Base{svc: svc} }
// opCtx 从请求上下文提取操作日志上下文
func (b *Base) opCtx(c *gin.Context) *service.OpLogContext {
ctx := &service.OpLogContext{
IP: utils.ClientIP(c),
UserAgent: c.GetHeader("User-Agent"),
Path: c.Request.URL.Path,
Method: c.Request.Method,
Username: c.GetString(middleware.CtxUsername),
}
if uid := middleware.CurrentUserID(c); uid > 0 {
ctx.UserID = &uid
}
return ctx
}
// bindJSON 绑定JSON请求体
func bindJSON(c *gin.Context, obj interface{}) bool {
if err := c.ShouldBindJSON(obj); err != nil {
utils.FailMsg(c, "参数错误: "+err.Error())
return false
}
return true
}
// paramID 解析路径参数ID
func paramID(c *gin.Context) (uint, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
utils.FailMsg(c, "无效的资源ID")
return 0, false
}
return uint(id), true
}
// parseUintQuery 解析整型query参数
func parseUintQuery(c *gin.Context, key string) uint {
v, err := strconv.ParseUint(c.Query(key), 10, 64)
if err != nil {
return 0
}
return uint(v)
}
// parseIntQuery 解析整型query参数(可为负)
func parseIntQuery(c *gin.Context, key string, def int) int {
n, err := strconv.Atoi(c.Query(key))
if err != nil {
return def
}
return n
}
+194
View File
@@ -0,0 +1,194 @@
package controller
import (
"fmt"
"net/http"
"net/url"
"path/filepath"
"github.com/gin-gonic/gin"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
)
// FileController 文件控制器
type FileController struct {
*Base
}
// NewFileController 创建文件控制器
func NewFileController(b *Base) *FileController { return &FileController{Base: b} }
// Upload 上传文件
func (ctl *FileController) Upload(c *gin.Context) {
fh, err := c.FormFile("file")
if err != nil {
utils.FailMsg(c, "缺少上传文件字段file")
return
}
userID := middleware.CurrentUserID(c)
f, err := ctl.svc.File.Upload(c, userID, fh)
if err != nil {
ctl.svc.RecordOpLog(ctl.opCtx(c), "upload", "file", 0, fh.Filename, false, err.Error())
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "upload", "file", f.ID, f.Filename, true, "")
utils.OK(c, f)
}
// List 文件列表(支持project_id/filename/prefix/trash过滤)
func (ctl *FileController) List(c *gin.Context) {
userID := middleware.CurrentUserID(c)
page, pageSize := utils.ParsePage(c)
filter := repository.FileListFilter{
ProjectID: parseUintQuery(c, "project_id"),
Filename: c.Query("filename"),
Prefix: utils.SanitizePath(c.Query("prefix")),
Trashed: c.Query("trash") == "true",
Page: page,
PageSize: pageSize,
}
if v := parseUintQuery(c, "visibility"); v > 0 {
filter.Visibility = int8(v)
}
files, total, err := ctl.svc.File.List(userID, middleware.IsAdmin(c), filter)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OKPage(c, files, total, page, pageSize)
}
// GetInfo 获取文件信息
func (ctl *FileController) GetInfo(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
f, err := ctl.svc.File.GetInfo(userID, id, middleware.IsAdmin(c))
if err != nil {
utils.FailMsg(c, err.Error())
return
}
utils.OK(c, f)
}
// Download 下载文件
func (ctl *FileController) Download(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
f, fp, err := ctl.svc.File.Open(c, userID, id, middleware.IsAdmin(c))
if err != nil {
utils.FailMsg(c, err.Error())
return
}
defer fp.Close()
ctl.svc.File.RecordDownload(c, f)
ctl.svc.RecordOpLog(ctl.opCtx(c), "download", "file", f.ID, f.Filename, true, "")
c.Header("Content-Disposition",
fmt.Sprintf(`attachment; filename*=UTF-8''%s`, url.PathEscape(filepath.Base(f.Filename))))
c.Header("Content-Type", f.MimeType)
http.ServeContent(c.Writer, c.Request, filepath.Base(f.Filename), f.UpdatedAt, fp)
}
// Delete 删除文件(permanent=true硬删除,否则进入回收站)
func (ctl *FileController) Delete(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
permanent := c.Query("permanent") == "true"
f, _ := ctl.svc.File.GetInfo(userID, id, middleware.IsAdmin(c))
if err := ctl.svc.File.Delete(userID, id, middleware.IsAdmin(c), permanent); err != nil {
utils.FailMsg(c, err.Error())
return
}
name := ""
if f != nil {
name = f.Filename
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "file", id, name, true, "")
if permanent {
utils.OKMsg(c, "文件已彻底删除")
} else {
utils.OKMsg(c, "文件已移入回收站")
}
}
// Restore 恢复回收站文件
func (ctl *FileController) Restore(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
if err := ctl.svc.File.Restore(userID, id, middleware.IsAdmin(c)); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "file", id, "restore", true, "")
utils.OKMsg(c, "文件已恢复")
}
type tempLinkReq struct {
Password string `json:"password"`
MaxCount int `json:"max_count"`
ExpiresIn int64 `json:"expires_in"` // 秒,0使用默认
}
// CreateTempLink 生成临时访问链接
func (ctl *FileController) CreateTempLink(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req tempLinkReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
link, err := ctl.svc.File.CreateTempLink(userID, id, middleware.IsAdmin(c), req.Password, req.MaxCount, req.ExpiresIn)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "file", id, "temp_link", true, "")
utils.OK(c, gin.H{
"token": link.Token,
"url": "/api/v1/files/temp/" + link.Token,
"expires_at": link.ExpiresAt,
"has_password": link.HasPwd,
"max_count": link.MaxCount,
})
}
// TempAccess 通过临时链接访问文件(公开接口,支持password参数)
func (ctl *FileController) TempAccess(c *gin.Context) {
token := c.Param("token")
password := c.Query("password")
if password == "" {
password = c.GetHeader("X-Link-Password")
}
f, fp, err := ctl.svc.File.ResolveTempLink(token, password)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
defer fp.Close()
ctl.svc.File.RecordDownload(c, f)
c.Header("Content-Disposition",
fmt.Sprintf(`inline; filename*=UTF-8''%s`, url.PathEscape(filepath.Base(f.Filename))))
c.Header("Content-Type", f.MimeType)
http.ServeContent(c.Writer, c.Request, filepath.Base(f.Filename), f.UpdatedAt, fp)
}
+113
View File
@@ -0,0 +1,113 @@
package controller
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/utils"
)
// ProjectController 项目控制器
type ProjectController struct {
*Base
}
// NewProjectController 创建项目控制器
func NewProjectController(b *Base) *ProjectController { return &ProjectController{Base: b} }
type createProjectReq struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
StorageLimit int64 `json:"storage_limit"` // 0使用默认
}
// Create 创建项目
func (ctl *ProjectController) Create(c *gin.Context) {
var req createProjectReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
p, err := ctl.svc.Project.Create(userID, req.Name, req.Description, req.StorageLimit)
if err != nil {
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "project", 0, req.Name, false, err.Error())
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "project", p.ID, p.Name, true, "")
utils.OK(c, p)
}
// List 项目列表
func (ctl *ProjectController) List(c *gin.Context) {
userID := middleware.CurrentUserID(c)
page, pageSize := utils.ParsePage(c)
all := c.Query("all") == "true" && middleware.IsAdmin(c)
projects, total, err := ctl.svc.Project.List(userID, c.Query("keyword"), all, page, pageSize)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OKPage(c, projects, total, page, pageSize)
}
// Get 项目详情
func (ctl *ProjectController) Get(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
p, err := ctl.svc.Project.Get(userID, id, middleware.IsAdmin(c))
if err != nil {
utils.FailMsg(c, err.Error())
return
}
utils.OK(c, p)
}
type updateProjectReq struct {
Name string `json:"name"`
Description string `json:"description"`
StorageLimit int64 `json:"storage_limit"`
}
// Update 更新项目
func (ctl *ProjectController) Update(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req updateProjectReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
p, err := ctl.svc.Project.Update(userID, id, middleware.IsAdmin(c), req.Name, req.Description, req.StorageLimit)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "project", id, p.Name, true, "")
utils.OK(c, p)
}
// Delete 删除项目
func (ctl *ProjectController) Delete(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
p, _ := ctl.svc.Project.Get(userID, id, middleware.IsAdmin(c))
if err := ctl.svc.Project.Delete(userID, id, middleware.IsAdmin(c)); err != nil {
utils.FailMsg(c, err.Error())
return
}
name := ""
if p != nil {
name = p.Name
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "project", id, name, true, "")
utils.OKMsg(c, "项目已删除")
}
+136
View File
@@ -0,0 +1,136 @@
package controller
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/utils"
)
// RoleController 角色权限控制器
type RoleController struct {
*Base
}
// NewRoleController 创建角色控制器
func NewRoleController(b *Base) *RoleController { return &RoleController{Base: b} }
// ListRoles 角色列表
func (ctl *RoleController) ListRoles(c *gin.Context) {
roles, err := ctl.svc.Role.ListRoles()
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, roles)
}
// GetRole 角色详情
func (ctl *RoleController) GetRole(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
role, err := ctl.svc.Role.GetRole(id)
if err != nil {
utils.NotFound(c, "角色不存在")
return
}
utils.OK(c, role)
}
type createRoleReq struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Description string `json:"description"`
Permissions []uint `json:"permissions"`
}
// CreateRole 创建角色
func (ctl *RoleController) CreateRole(c *gin.Context) {
var req createRoleReq
if !bindJSON(c, &req) {
return
}
role, err := ctl.svc.Role.CreateRole(req.Name, req.Code, req.Description, req.Permissions)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "role", role.ID, role.Name, true, "")
utils.OK(c, role)
}
type updateRoleReq struct {
Name string `json:"name"`
Description string `json:"description"`
Permissions []uint `json:"permissions"`
}
// UpdateRole 编辑角色
func (ctl *RoleController) UpdateRole(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req updateRoleReq
if !bindJSON(c, &req) {
return
}
role, err := ctl.svc.Role.UpdateRole(id, req.Name, req.Description, req.Permissions)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "role", id, role.Name, true, "")
utils.OK(c, role)
}
// DeleteRole 删除角色
func (ctl *RoleController) DeleteRole(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
if err := ctl.svc.Role.DeleteRole(id); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "role", id, "", true, "")
utils.OKMsg(c, "角色已删除")
}
type setPermissionsReq struct {
Permissions []uint `json:"permissions"`
}
// SetPermissions 配置角色权限
func (ctl *RoleController) SetPermissions(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req setPermissionsReq
if !bindJSON(c, &req) {
return
}
if err := ctl.svc.Role.SetPermissions(id, req.Permissions); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "role", id, "permissions", true, "")
utils.OKMsg(c, "角色权限已更新")
}
// ListPermissions 权限列表(按模块分组)
func (ctl *RoleController) ListPermissions(c *gin.Context) {
groups, order, err := ctl.svc.Role.PermissionsGrouped()
if err != nil {
utils.ServerError(c, err.Error())
return
}
result := make([]gin.H, 0, len(order))
for _, module := range order {
result = append(result, gin.H{"module": module, "permissions": groups[module]})
}
utils.OK(c, result)
}
+54
View File
@@ -0,0 +1,54 @@
package controller
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/utils"
)
// StatsController 统计控制器
type StatsController struct {
*Base
}
// NewStatsController 创建统计控制器
func NewStatsController(b *Base) *StatsController { return &StatsController{Base: b} }
// UserStats 用户存储统计
func (ctl *StatsController) UserStats(c *gin.Context) {
userID := middleware.CurrentUserID(c)
stats, err := ctl.svc.Stats.UserStats(userID)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, stats)
}
// ProjectStats 项目存储统计
func (ctl *StatsController) ProjectStats(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
stats, err := ctl.svc.Stats.ProjectStats(userID, id, middleware.IsAdmin(c))
if err != nil {
utils.FailMsg(c, err.Error())
return
}
utils.OK(c, stats)
}
// TrafficStats 流量统计
func (ctl *StatsController) TrafficStats(c *gin.Context) {
userID := middleware.CurrentUserID(c)
days := parseIntQuery(c, "days", 30)
stats, err := ctl.svc.Stats.TrafficStats(userID, days)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, stats)
}
+67
View File
@@ -0,0 +1,67 @@
package controller
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/utils"
)
// UserController 用户管理控制器(个人资料)
type UserController struct {
*Base
}
// NewUserController 创建用户控制器
func NewUserController(b *Base) *UserController { return &UserController{Base: b} }
// GetProfile 获取个人资料
func (ctl *UserController) GetProfile(c *gin.Context) {
userID := middleware.CurrentUserID(c)
u, err := ctl.svc.User.GetProfile(userID)
if err != nil {
utils.NotFound(c, "用户不存在")
return
}
utils.OK(c, u)
}
type updateProfileReq struct {
Email string `json:"email"`
}
// UpdateProfile 更新个人资料
func (ctl *UserController) UpdateProfile(c *gin.Context) {
var req updateProfileReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
if err := ctl.svc.User.UpdateProfile(userID, req.Email); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "user", userID, "", true, "")
utils.OKMsg(c, "资料已更新")
}
type changePasswordReq struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
// ChangePassword 修改密码
func (ctl *UserController) ChangePassword(c *gin.Context) {
var req changePasswordReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
if err := ctl.svc.User.ChangePassword(userID, req.OldPassword, req.NewPassword); err != nil {
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "user", userID, "password", false, err.Error())
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "user", userID, "password", true, "")
utils.OKMsg(c, "密码已修改")
}
+92
View File
@@ -0,0 +1,92 @@
package controller
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/utils"
)
// WebhookController Webhook控制器
type WebhookController struct {
*Base
}
// NewWebhookController 创建Webhook控制器
func NewWebhookController(b *Base) *WebhookController { return &WebhookController{Base: b} }
type createWebhookReq struct {
URL string `json:"url" binding:"required"`
ProjectID *uint `json:"project_id"`
Secret string `json:"secret"`
Events string `json:"events" binding:"required"` // 如 file.upload,file.delete
Status int8 `json:"status"`
}
// Create 创建webhook
func (ctl *WebhookController) Create(c *gin.Context) {
var req createWebhookReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
w, err := ctl.svc.Webhook.Create(userID, req.ProjectID, req.URL, req.Secret, req.Events, req.Status)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "webhook", w.ID, w.URL, true, "")
utils.OK(c, w)
}
// List webhook列表
func (ctl *WebhookController) List(c *gin.Context) {
userID := middleware.CurrentUserID(c)
hooks, err := ctl.svc.Webhook.List(userID)
if err != nil {
utils.ServerError(c, err.Error())
return
}
utils.OK(c, hooks)
}
type updateWebhookReq struct {
URL string `json:"url"`
Events string `json:"events"`
Status int8 `json:"status"`
}
// Update 更新webhook
func (ctl *WebhookController) Update(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
var req updateWebhookReq
if !bindJSON(c, &req) {
return
}
userID := middleware.CurrentUserID(c)
w, err := ctl.svc.Webhook.Update(userID, id, req.URL, req.Events, req.Status)
if err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "webhook", id, w.URL, true, "")
utils.OK(c, w)
}
// Delete 删除webhook
func (ctl *WebhookController) Delete(c *gin.Context) {
id, ok := paramID(c)
if !ok {
return
}
userID := middleware.CurrentUserID(c)
if err := ctl.svc.Webhook.Delete(userID, id); err != nil {
utils.FailMsg(c, err.Error())
return
}
ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "webhook", id, "", true, "")
utils.OKMsg(c, "webhook已删除")
}
+109
View File
@@ -0,0 +1,109 @@
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
"filestoragesystem/internal/model"
"filestoragesystem/internal/service"
"filestoragesystem/internal/utils"
"filestoragesystem/pkg/apperr"
)
// 上下文键
const (
CtxUser = "authUser" // *model.User
CtxUserID = "authUserID" // uint
CtxUsername = "authUsername" // string
CtxRoleCode = "authRoleCode" // string
CtxPermissions = "authPermissions" // map[string]bool
)
// Auth 认证中间件:支持 JWT Bearer 与 API Key HMAC-SHA256签名两种方式
func Auth(authSvc *service.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
var user *model.User
// 1. JWT Bearer Token
header := c.GetHeader("Authorization")
if strings.HasPrefix(header, "Bearer ") {
claims, err := utils.ParseToken(strings.TrimPrefix(header, "Bearer "))
if err != nil {
utils.Unauthorized(c, "Token无效或已过期")
return
}
u, err := authSvc.LoadUserByID(claims.UserID)
if err != nil {
utils.Unauthorized(c, apperr.ErrUnauthorized.Error())
return
}
user = u
} else if ak := c.GetHeader("X-Access-Key"); ak != "" {
// 2. API Key + Secret HMAC-SHA256签名
u, err := authSvc.VerifyAPIKeySignature(
ak,
c.GetHeader("X-Timestamp"),
c.GetHeader("X-Nonce"),
c.GetHeader("X-Signature"),
c.Request.Method,
c.Request.URL.Path,
)
if err != nil {
utils.Unauthorized(c, err.Error())
return
}
user = u
} else {
utils.Unauthorized(c, "缺少认证信息")
return
}
if user == nil {
utils.Unauthorized(c, apperr.ErrUnauthorized.Error())
return
}
c.Set(CtxUser, user)
c.Set(CtxUserID, user.ID)
c.Set(CtxUsername, user.Username)
roleCode := ""
if user.Role != nil {
roleCode = user.Role.Code
}
c.Set(CtxRoleCode, roleCode)
c.Set(CtxPermissions, service.PermissionCodesOf(user))
c.Next()
}
}
// CurrentUser 从上下文获取当前用户
func CurrentUser(c *gin.Context) *model.User {
if v, ok := c.Get(CtxUser); ok {
if u, ok := v.(*model.User); ok {
return u
}
}
return nil
}
// CurrentUserID 当前用户ID
func CurrentUserID(c *gin.Context) uint {
if v, ok := c.Get(CtxUserID); ok {
if id, ok := v.(uint); ok {
return id
}
}
return 0
}
// IsSuperAdmin 是否超级管理员
func IsSuperAdmin(c *gin.Context) bool {
return c.GetString(CtxRoleCode) == "super_admin"
}
// IsAdmin 是否管理员(super_admin或admin)
func IsAdmin(c *gin.Context) bool {
rc := c.GetString(CtxRoleCode)
return rc == "super_admin" || rc == "admin"
}
+27
View File
@@ -0,0 +1,27 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
// CORS 跨域中间件
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers",
"Origin, Content-Type, Accept, Authorization, X-Access-Key, X-Timestamp, X-Nonce, X-Signature, X-Request-Id")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
c.Header("Access-Control-Max-Age", "86400")
}
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
+60
View File
@@ -0,0 +1,60 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"filestoragesystem/internal/utils"
)
// RequestID 请求ID中间件
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-Id")
if id == "" {
id = utils.RandomKey(8)
}
c.Set("requestID", id)
c.Header("X-Request-Id", id)
c.Next()
}
}
// Logger 访问日志中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
args := []interface{}{
"status", status,
"method", c.Request.Method,
"path", path,
"ip", utils.ClientIP(c),
"latency", latency.String(),
"request_id", c.GetString("requestID"),
}
if uid := CurrentUserID(c); uid > 0 {
args = append(args, "user_id", uid)
}
if err := c.Errors.ByType(gin.ErrorTypePrivate).Last(); err != nil {
args = append(args, "error", err.Error())
}
sugar := utils.Logger().Sugar()
switch {
case status >= 500:
sugar.Errorw("access", args...)
case status >= 400:
sugar.Warnw("access", args...)
default:
sugar.Infow("access", args...)
}
}
}
+28
View File
@@ -0,0 +1,28 @@
package middleware
import (
"github.com/gin-gonic/gin"
"filestoragesystem/internal/utils"
)
// RequirePermission 权限检查中间件;super_admin拥有全部权限
func RequirePermission(code string) gin.HandlerFunc {
return func(c *gin.Context) {
perms, ok := c.Get(CtxPermissions)
if !ok {
utils.Unauthorized(c, "未认证")
return
}
m, ok := perms.(map[string]bool)
if !ok {
utils.Forbidden(c, "权限信息异常")
return
}
if m["*"] || m[code] {
c.Next()
return
}
utils.Forbidden(c, "没有操作权限: "+code)
}
}
+69
View File
@@ -0,0 +1,69 @@
package middleware
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
)
// RateLimit 令牌桶限流中间件
// 限额从系统设置api_rate_limit读取(次/分钟),按用户ID(已认证)或IP进行限制
func RateLimit(settingRepo *repository.SettingRepo) gin.HandlerFunc {
var mu sync.Mutex
buckets := make(map[string]*rate.Limiter)
lastClean := time.Now()
getLimit := func() int {
if v, err := settingRepo.GetValue("api_rate_limit"); err == nil {
var n int
if _, e := fmt.Sscanf(v, "%d", &n); e == nil && n > 0 {
return n
}
}
return 100
}
return func(c *gin.Context) {
// 管理员后台不限流
if IsAdmin(c) {
c.Next()
return
}
key := utils.ClientIP(c)
if uid := CurrentUserID(c); uid > 0 {
key = fmt.Sprintf("user:%d", uid)
}
limit := getLimit()
mu.Lock()
// 定期清理空闲桶
if time.Since(lastClean) > 10*time.Minute {
buckets = make(map[string]*rate.Limiter)
lastClean = time.Now()
}
limiter, ok := buckets[key]
if !ok {
// 每分钟limit次:速率=limit/60每秒,突发=limit
limiter = rate.NewLimiter(rate.Limit(float64(limit)/60.0), limit)
buckets[key] = limiter
}
mu.Unlock()
if !limiter.Allow() {
c.Header("Retry-After", "60")
c.AbortWithStatusJSON(http.StatusTooManyRequests, utils.Response{
Code: 429, Message: "请求过于频繁,请稍后再试",
})
return
}
c.Next()
}
}
+42
View File
@@ -0,0 +1,42 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"filestoragesystem/internal/service"
"filestoragesystem/internal/utils"
)
// SiteStatus 站点状态中间件
// 站点关闭(维护模式)时:放行状态接口/登录接口/管理员请求,其余返回503
func SiteStatus(adminSvc *service.AdminService) gin.HandlerFunc {
return func(c *gin.Context) {
if adminSvc.IsSiteEnabled() {
c.Next()
return
}
// 放行的路径
path := c.Request.URL.Path
if path == "/api/v1/status" || path == "/api/v1/auth/login" {
c.Next()
return
}
// 已认证的管理员放行(需在Auth之后挂载)
if IsAdmin(c) {
c.Next()
return
}
msg := "系统维护中,请稍后再访问"
if m, ok := adminSvc.SiteStatus()["maintenance_msg"].(string); ok && m != "" {
msg = m
}
c.AbortWithStatusJSON(http.StatusServiceUnavailable, utils.Response{
Code: http.StatusServiceUnavailable, Message: msg,
})
}
}
+169
View File
@@ -0,0 +1,169 @@
package model
import "time"
// Role 角色
type Role struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:50;uniqueIndex;not null" json:"name"`
Code string `gorm:"size:50;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:255" json:"description"`
IsSystem bool `gorm:"default:false" json:"is_system"`
Status int8 `gorm:"default:1" json:"status"` // 1:启用 0:禁用
Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Permission 权限
type Permission struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:50;not null" json:"name"`
Code string `gorm:"size:100;uniqueIndex;not null" json:"code"`
Module string `gorm:"size:50;not null" json:"module"`
Description string `gorm:"size:255" json:"description"`
CreatedAt time.Time `json:"created_at"`
}
// User 用户
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:50;uniqueIndex;not null" json:"username"`
Email string `gorm:"size:100;uniqueIndex;not null" json:"email"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
RoleID uint `gorm:"default:1" json:"role_id"`
Role *Role `gorm:"foreignKey:RoleID" json:"role,omitempty"`
Status int8 `gorm:"default:1" json:"status"` // 1:正常 0:禁用
StorageUsed int64 `gorm:"default:0" json:"storage_used"`
StorageLimit int64 `gorm:"default:10737418240" json:"storage_limit"`
LastLoginAt *time.Time `json:"last_login_at"`
LastLoginIP string `gorm:"size:50" json:"last_login_ip"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// APIKey API密钥
type APIKey struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
AccessKey string `gorm:"size:32;uniqueIndex;not null" json:"access_key"`
SecretKey string `gorm:"size:64;not null" json:"-"`
Name string `gorm:"size:50" json:"name"`
Status int8 `gorm:"default:1" json:"status"`
LastUsedAt *time.Time `json:"last_used_at"`
CreatedAt time.Time `json:"created_at"`
}
// Project 项目
type Project struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Name string `gorm:"size:100;not null;uniqueIndex:idx_user_project" json:"name"`
Description string `json:"description"`
StorageUsed int64 `gorm:"default:0" json:"storage_used"`
StorageLimit int64 `gorm:"default:5368709120" json:"storage_limit"`
Status int8 `gorm:"default:1" json:"status"` // 1:正常 0:禁用
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// File 文件
type File struct {
ID uint `gorm:"primaryKey" json:"id"`
ProjectID uint `gorm:"index;not null" json:"project_id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Filename string `gorm:"size:500;not null" json:"filename"` // 原始文件名(可含虚拟目录)
StoredPath string `gorm:"size:500;not null" json:"-"` // 物理存储路径
Size int64 `gorm:"not null" json:"size"`
MimeType string `gorm:"size:100" json:"mime_type"`
MD5 string `gorm:"size:32;index" json:"md5"`
StorageType int8 `gorm:"default:1" json:"storage_type"` // 1:本地存储
Visibility int8 `gorm:"default:1" json:"visibility"` // 1:私有 2:公开
DownloadCount int64 `gorm:"default:0" json:"download_count"`
Status int8 `gorm:"default:1;index" json:"status"` // 1:正常 0:已删除(回收站)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
Project *Project `gorm:"foreignKey:ProjectID" json:"project,omitempty"`
}
// TempLink 临时访问链接
type TempLink struct {
ID uint `gorm:"primaryKey" json:"id"`
FileID uint `gorm:"index;not null" json:"file_id"`
UserID uint `gorm:"not null" json:"user_id"`
Token string `gorm:"size:64;uniqueIndex;not null" json:"token"`
Password string `gorm:"size:50" json:"-"`
HasPwd bool `gorm:"-" json:"has_password"`
MaxCount int `gorm:"default:0" json:"max_count"` // 0为不限
UsedCount int `gorm:"default:0" json:"used_count"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
// TrafficLog 流量日志
type TrafficLog struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
ProjectID uint `json:"project_id"`
FileID uint `json:"file_id"`
Type int8 `gorm:"not null" json:"type"` // 1:上传 2:下载
Size int64 `gorm:"not null" json:"size"`
IP string `gorm:"size:50" json:"ip"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// Webhook Webhook回调配置
type Webhook struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
ProjectID *uint `json:"project_id"` // 为空则为用户级别
URL string `gorm:"size:500;not null" json:"url"`
Secret string `gorm:"size:64" json:"-"`
Events string `gorm:"size:255;not null" json:"events"` // 逗号分隔: file.upload,file.delete
Status int8 `gorm:"default:1" json:"status"`
LastTriggeredAt *time.Time `json:"last_triggered_at"`
CreatedAt time.Time `json:"created_at"`
}
// SystemSetting 系统设置
type SystemSetting struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"column:key;size:100;uniqueIndex;not null" json:"key"`
Value string `json:"value"`
Description string `gorm:"size:255" json:"description"`
GroupName string `gorm:"size:50;default:general" json:"group_name"`
UpdatedAt time.Time `json:"updated_at"`
}
// OperationLog 操作日志
type OperationLog struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID *uint `gorm:"index" json:"user_id"`
Username string `gorm:"size:50" json:"username"`
Action string `gorm:"size:50;index;not null" json:"action"` // login/logout/upload/download/delete/create/update
ResourceType string `gorm:"size:50" json:"resource_type"`
ResourceID uint `json:"resource_id"`
ResourceName string `gorm:"size:255" json:"resource_name"`
IP string `gorm:"size:50" json:"ip"`
UserAgent string `gorm:"size:500" json:"user_agent"`
RequestPath string `gorm:"size:500" json:"request_path"`
RequestMethod string `gorm:"size:10" json:"request_method"`
Status int8 `gorm:"default:1" json:"status"` // 1:成功 0:失败
ErrorMessage string `json:"error_message"`
ExtraData string `json:"extra_data"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// SystemLog 系统日志
type SystemLog struct {
ID uint `gorm:"primaryKey" json:"id"`
Level string `gorm:"size:10;index;not null" json:"level"` // debug/info/warn/error/fatal
Module string `gorm:"size:50;index" json:"module"`
Message string `gorm:"not null" json:"message"`
StackTrace string `json:"stack_trace"`
RequestID string `gorm:"size:50" json:"request_id"`
IP string `gorm:"size:50" json:"ip"`
ExtraData string `json:"extra_data"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
+159
View File
@@ -0,0 +1,159 @@
package model
import (
"log"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// AutoMigrate 自动迁移所有表
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(
&Role{},
&Permission{},
&User{},
&APIKey{},
&Project{},
&File{},
&TempLink{},
&TrafficLog{},
&Webhook{},
&SystemSetting{},
&OperationLog{},
&SystemLog{},
)
}
var defaultRoles = []Role{
{Name: "超级管理员", Code: "super_admin", Description: "系统超级管理员,拥有所有权限", IsSystem: true},
{Name: "管理员", Code: "admin", Description: "系统管理员,可管理用户和内容", IsSystem: true},
{Name: "普通用户", Code: "user", Description: "普通用户,管理自己的项目和文件", IsSystem: true},
}
var defaultPermissions = []Permission{
// 用户管理
{Name: "查看用户列表", Code: "user:list", Module: "user", Description: "查看所有用户列表"},
{Name: "查看用户详情", Code: "user:detail", Module: "user", Description: "查看用户详细信息"},
{Name: "创建用户", Code: "user:create", Module: "user", Description: "创建新用户"},
{Name: "编辑用户", Code: "user:edit", Module: "user", Description: "编辑用户信息"},
{Name: "删除用户", Code: "user:delete", Module: "user", Description: "删除用户"},
{Name: "禁用用户", Code: "user:disable", Module: "user", Description: "禁用/启用用户"},
{Name: "分配角色", Code: "user:assign_role", Module: "user", Description: "修改用户角色"},
{Name: "重置密码", Code: "user:reset_password", Module: "user", Description: "重置用户密码"},
// 项目管理
{Name: "查看项目列表", Code: "project:list", Module: "project", Description: "查看项目列表"},
{Name: "创建项目", Code: "project:create", Module: "project", Description: "创建新项目"},
{Name: "编辑项目", Code: "project:edit", Module: "project", Description: "编辑项目信息"},
{Name: "删除项目", Code: "project:delete", Module: "project", Description: "删除项目"},
// 文件管理
{Name: "上传文件", Code: "file:upload", Module: "file", Description: "上传文件"},
{Name: "下载文件", Code: "file:download", Module: "file", Description: "下载文件"},
{Name: "删除文件", Code: "file:delete", Module: "file", Description: "删除文件"},
{Name: "查看文件列表", Code: "file:list", Module: "file", Description: "查看文件列表"},
// 系统管理
{Name: "查看系统设置", Code: "system:settings:view", Module: "system", Description: "查看系统设置"},
{Name: "修改系统设置", Code: "system:settings:edit", Module: "system", Description: "修改系统设置"},
{Name: "查看操作日志", Code: "system:logs:operation", Module: "system", Description: "查看操作日志"},
{Name: "查看系统日志", Code: "system:logs:system", Module: "system", Description: "查看系统日志"},
{Name: "清理日志", Code: "system:logs:cleanup", Module: "system", Description: "清理历史日志"},
}
// admin角色不拥有的权限
var adminExcluded = map[string]bool{
"user:delete": true,
"user:assign_role": true,
"system:settings:edit": true,
}
// 普通用户拥有的权限
var userPerms = map[string]bool{
"project:list": true, "project:create": true, "project:edit": true, "project:delete": true,
"file:upload": true, "file:download": true, "file:delete": true, "file:list": true,
}
var defaultSettings = []SystemSetting{
{Key: "site_name", Value: "云泽文件存储云平台", Description: "站点名称", GroupName: "site"},
{Key: "site_description", Value: "高效、安全的文件存储服务", Description: "站点描述", GroupName: "site"},
{Key: "site_logo", Value: "/assets/logo.png", Description: "站点Logo路径", GroupName: "site"},
{Key: "site_icp", Value: "", Description: "ICP备案号", GroupName: "site"},
{Key: "site_copyright", Value: "© 2024 FileStorageSystem", Description: "版权信息", GroupName: "site"},
{Key: "register_enabled", Value: "true", Description: "是否开放注册", GroupName: "feature"},
{Key: "site_enabled", Value: "true", Description: "站点是否启用", GroupName: "feature"},
{Key: "site_maintenance_msg", Value: "系统维护中,请稍后再访问", Description: "维护提示信息", GroupName: "feature"},
{Key: "default_storage_limit", Value: "10737418240", Description: "默认用户存储配额(字节)", GroupName: "storage"},
{Key: "default_project_limit", Value: "5368709120", Description: "默认项目存储配额(字节)", GroupName: "storage"},
{Key: "max_file_size", Value: "104857600", Description: "单文件最大限制(字节)", GroupName: "storage"},
{Key: "allowed_file_types", Value: "*", Description: "允许的文件类型(*表示所有)", GroupName: "storage"},
{Key: "api_rate_limit", Value: "100", Description: "API请求频率限制(次/分钟)", GroupName: "api"},
{Key: "temp_link_max_age", Value: "3600", Description: "临时链接默认有效期(秒)", GroupName: "api"},
{Key: "webhook_enabled", Value: "true", Description: "是否启用Webhook回调", GroupName: "api"},
}
// SeedData 初始化种子数据(幂等)
func SeedData(db *gorm.DB) error {
db.Logger = db.Logger.LogMode(logger.Warn)
// 1. 角色
for i := range defaultRoles {
var r Role
if err := db.Where("code = ?", defaultRoles[i].Code).First(&r).Error; err != nil {
if err := db.Create(&defaultRoles[i]).Error; err != nil {
return err
}
}
}
// 2. 权限
for i := range defaultPermissions {
var p Permission
if err := db.Where("code = ?", defaultPermissions[i].Code).First(&p).Error; err != nil {
if err := db.Create(&defaultPermissions[i]).Error; err != nil {
return err
}
}
}
// 3. 角色权限关联
var perms []Permission
if err := db.Find(&perms).Error; err != nil {
return err
}
rolePerms := map[string]func(code string) bool{
"super_admin": func(string) bool { return true },
"admin": func(code string) bool { return !adminExcluded[code] },
"user": func(code string) bool { return userPerms[code] },
}
for roleCode, match := range rolePerms {
var role Role
if err := db.Where("code = ?", roleCode).First(&role).Error; err != nil {
continue
}
for _, p := range perms {
if !match(p.Code) {
continue
}
var count int64
db.Table("role_permissions").
Where("role_id = ? AND permission_id = ?", role.ID, p.ID).
Count(&count)
if count == 0 {
if err := db.Exec("INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)", role.ID, p.ID).Error; err != nil {
log.Printf("插入角色权限失败: %v", err)
}
}
}
}
// 4. 系统设置
for i := range defaultSettings {
var s SystemSetting
if err := db.Where("`key` = ?", defaultSettings[i].Key).First(&s).Error; err != nil {
if err := db.Create(&defaultSettings[i]).Error; err != nil {
return err
}
}
}
return nil
}
+220
View File
@@ -0,0 +1,220 @@
package repository
import (
"time"
"gorm.io/gorm"
"filestoragesystem/internal/model"
)
// FileListFilter 文件列表过滤条件
type FileListFilter struct {
UserID uint
ProjectID uint
Filename string // 模糊匹配
Prefix string // 虚拟目录前缀
Trashed bool // 查询回收站
Visibility int8 // 0为不过滤
Page int
PageSize int
}
// FileRepo 文件数据访问
type FileRepo struct{ DB *gorm.DB }
func (r *FileRepo) Create(f *model.File) error { return r.DB.Create(f).Error }
func (r *FileRepo) FindByID(id uint) (*model.File, error) {
var f model.File
err := r.DB.First(&f, id).Error
return &f, err
}
// List 分页查询文件
func (r *FileRepo) List(filter FileListFilter) ([]model.File, int64, error) {
q := r.DB.Model(&model.File{})
if filter.UserID > 0 {
q = q.Where("user_id = ?", filter.UserID)
}
if filter.ProjectID > 0 {
q = q.Where("project_id = ?", filter.ProjectID)
}
if filter.Filename != "" {
q = q.Where("filename LIKE ?", "%"+filter.Filename+"%")
}
if filter.Prefix != "" {
q = q.Where("(filename = ? OR filename LIKE ?)", filter.Prefix, filter.Prefix+"/%")
}
if filter.Trashed {
q = q.Where("status = 0")
} else {
q = q.Where("status = 1")
}
if filter.Visibility > 0 {
q = q.Where("visibility = ?", filter.Visibility)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var files []model.File
err := q.Preload("Project").Order("id DESC").
Offset((filter.Page - 1) * filter.PageSize).Limit(filter.PageSize).Find(&files).Error
return files, total, err
}
func (r *FileRepo) Update(f *model.File) error { return r.DB.Save(f).Error }
// SoftDelete 软删除(进入回收站)
func (r *FileRepo) SoftDelete(id uint) error {
now := time.Now()
return r.DB.Model(&model.File{}).Where("id = ?", id).
Updates(map[string]interface{}{"status": 0, "deleted_at": now}).Error
}
// Restore 从回收站恢复
func (r *FileRepo) Restore(id uint) error {
return r.DB.Model(&model.File{}).Where("id = ?", id).
Updates(map[string]interface{}{"status": 1, "deleted_at": nil}).Error
}
func (r *FileRepo) Delete(id uint) error { return r.DB.Delete(&model.File{}, id).Error }
// CountByStoredPath 统计引用同一物理文件的记录数(用于去重删除)
func (r *FileRepo) CountByStoredPath(storedPath string) (int64, error) {
var n int64
err := r.DB.Model(&model.File{}).Where("stored_path = ?", storedPath).Count(&n).Error
return n, err
}
// FindByMD5AndPath 基于MD5查找可复用的物理文件记录
func (r *FileRepo) FindByMD5AndPath(md5 string) (*model.File, error) {
var f model.File
err := r.DB.Where("md5 = ? AND stored_path != ''", md5).Order("id ASC").First(&f).Error
return &f, err
}
// ListByProject 项目下所有文件(用于项目删除级联)
func (r *FileRepo) ListByProject(projectID uint) ([]model.File, error) {
var files []model.File
err := r.DB.Where("project_id = ?", projectID).Find(&files).Error
return files, err
}
// ListByUser 用户所有文件(用于用户删除级联)
func (r *FileRepo) ListByUser(userID uint) ([]model.File, error) {
var files []model.File
err := r.DB.Where("user_id = ?", userID).Find(&files).Error
return files, err
}
// StatsByProject 项目文件统计
func (r *FileRepo) StatsByProject(projectID uint) (count int64, size int64, err error) {
var res struct {
Count int64 `json:"count"`
Size int64 `json:"size"`
}
err = r.DB.Model(&model.File{}).
Where("project_id = ? AND status = 1", projectID).
Select("COUNT(*) as count, COALESCE(SUM(size),0) as size").
Scan(&res).Error
return res.Count, res.Size, err
}
// StatsByUser 用户文件统计
func (r *FileRepo) StatsByUser(userID uint) (count int64, size int64, err error) {
var res struct {
Count int64 `json:"count"`
Size int64 `json:"size"`
}
err = r.DB.Model(&model.File{}).
Where("user_id = ? AND status = 1", userID).
Select("COUNT(*) as count, COALESCE(SUM(size),0) as size").
Scan(&res).Error
return res.Count, res.Size, err
}
// IncrDownloadCount 下载计数
func (r *FileRepo) IncrDownloadCount(id uint) error {
return r.DB.Model(&model.File{}).Where("id = ?", id).
UpdateColumn("download_count", gorm.Expr("download_count + 1")).Error
}
// ---- 临时链接 ----
// TempLinkRepo 临时链接数据访问
type TempLinkRepo struct{ DB *gorm.DB }
func (r *TempLinkRepo) Create(l *model.TempLink) error { return r.DB.Create(l).Error }
func (r *TempLinkRepo) FindByToken(token string) (*model.TempLink, error) {
var l model.TempLink
err := r.DB.Where("token = ?", token).First(&l).Error
return &l, err
}
func (r *TempLinkRepo) IncrUsed(id uint) error {
return r.DB.Model(&model.TempLink{}).Where("id = ?", id).
UpdateColumn("used_count", gorm.Expr("used_count + 1")).Error
}
func (r *TempLinkRepo) DeleteByFile(fileID uint) error {
return r.DB.Where("file_id = ?", fileID).Delete(&model.TempLink{}).Error
}
// ---- 流量日志 ----
// TrafficRepo 流量日志数据访问
type TrafficRepo struct{ DB *gorm.DB }
func (r *TrafficRepo) Create(t *model.TrafficLog) error { return r.DB.Create(t).Error }
// DailyStats 按天/类型聚合流量
func (r *TrafficRepo) DailyStats(userID uint, days int) ([]map[string]interface{}, error) {
var rows []map[string]interface{}
err := r.DB.Model(&model.TrafficLog{}).
Select("date(created_at) as date, type, COALESCE(SUM(size),0) as size, COUNT(*) as count").
Where("user_id = ? AND created_at >= date('now', ?)", userID, "-"+itoa(days)+" day").
Group("date(created_at), type").
Order("date DESC").
Find(&rows).Error
return rows, err
}
// SumByUser 汇总用户上传/下载流量
func (r *TrafficRepo) SumByUser(userID uint, days int) (upload, download int64, err error) {
var res []struct {
Type int64 `json:"type"`
Size int64 `json:"size"`
}
q := r.DB.Model(&model.TrafficLog{}).
Select("type, COALESCE(SUM(size),0) as size").
Where("user_id = ?", userID)
if days > 0 {
q = q.Where("created_at >= date('now', ?)", "-"+itoa(days)+" day")
}
err = q.Group("type").Scan(&res).Error
for _, r := range res {
if r.Type == 1 {
upload = r.Size
} else if r.Type == 2 {
download = r.Size
}
}
return upload, download, err
}
func itoa(n int) string {
if n == 0 {
return "0"
}
var buf [12]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
return string(buf[i:])
}
+60
View File
@@ -0,0 +1,60 @@
package repository
import (
"gorm.io/gorm"
"filestoragesystem/internal/model"
)
// ProjectRepo 项目数据访问
type ProjectRepo struct{ DB *gorm.DB }
func (r *ProjectRepo) Create(p *model.Project) error { return r.DB.Create(p).Error }
func (r *ProjectRepo) FindByID(id uint) (*model.Project, error) {
var p model.Project
err := r.DB.First(&p, id).Error
return &p, err
}
func (r *ProjectRepo) FindByName(userID uint, name string) (*model.Project, error) {
var p model.Project
err := r.DB.Where("user_id = ? AND name = ?", userID, name).First(&p).Error
return &p, err
}
// List 分页查询项目;keyword匹配名称;all为true时跨用户(管理员)
func (r *ProjectRepo) List(userID uint, keyword string, all bool, page, pageSize int) ([]model.Project, int64, error) {
q := r.DB.Model(&model.Project{})
if !all {
q = q.Where("user_id = ?", userID)
}
if keyword != "" {
q = q.Where("name LIKE ?", "%"+keyword+"%")
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var projects []model.Project
err := q.Order("id DESC").
Offset((page - 1) * pageSize).Limit(pageSize).Find(&projects).Error
return projects, total, err
}
func (r *ProjectRepo) Update(p *model.Project) error { return r.DB.Save(p).Error }
func (r *ProjectRepo) Delete(id uint) error { return r.DB.Delete(&model.Project{}, id).Error }
// UpdateStorageUsed 增减项目已用存储
func (r *ProjectRepo) UpdateStorageUsed(id uint, delta int64) error {
return r.DB.Model(&model.Project{}).Where("id = ?", id).
UpdateColumn("storage_used", gorm.Expr("storage_used + ?", delta)).Error
}
// ListByUser 用户全部项目(用于统计)
func (r *ProjectRepo) ListByUser(userID uint) ([]model.Project, error) {
var projects []model.Project
err := r.DB.Where("user_id = ?", userID).Find(&projects).Error
return projects, err
}
+79
View File
@@ -0,0 +1,79 @@
package repository
import (
"gorm.io/gorm"
"filestoragesystem/internal/model"
)
// RoleRepo 角色数据访问
type RoleRepo struct{ DB *gorm.DB }
func (r *RoleRepo) Create(role *model.Role) error { return r.DB.Create(role).Error }
func (r *RoleRepo) FindByID(id uint) (*model.Role, error) {
var role model.Role
err := r.DB.Preload("Permissions").First(&role, id).Error
return &role, err
}
func (r *RoleRepo) FindByCode(code string) (*model.Role, error) {
var role model.Role
err := r.DB.Where("code = ?", code).First(&role).Error
return &role, err
}
func (r *RoleRepo) List() ([]model.Role, error) {
var roles []model.Role
err := r.DB.Preload("Permissions").Order("id ASC").Find(&roles).Error
return roles, err
}
func (r *RoleRepo) Update(role *model.Role) error { return r.DB.Save(role).Error }
func (r *RoleRepo) Delete(id uint) error { return r.DB.Delete(&model.Role{}, id).Error }
// ReplacePermissions 覆盖式更新角色权限
func (r *RoleRepo) ReplacePermissions(roleID uint, permIDs []uint) error {
return r.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Exec("DELETE FROM role_permissions WHERE role_id = ?", roleID).Error; err != nil {
return err
}
for _, pid := range permIDs {
if err := tx.Exec("INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)", roleID, pid).Error; err != nil {
return err
}
}
return nil
})
}
// CountUsersByRole 统计角色下的用户数
func (r *RoleRepo) CountUsersByRole(roleID uint) (int64, error) {
var n int64
err := r.DB.Model(&model.User{}).Where("role_id = ?", roleID).Count(&n).Error
return n, err
}
// PermissionRepo 权限数据访问
type PermissionRepo struct{ DB *gorm.DB }
func (r *PermissionRepo) List() ([]model.Permission, error) {
var perms []model.Permission
err := r.DB.Order("module ASC, id ASC").Find(&perms).Error
return perms, err
}
func (r *PermissionRepo) ListByIDs(ids []uint) ([]model.Permission, error) {
var perms []model.Permission
err := r.DB.Where("id IN ?", ids).Find(&perms).Error
return perms, err
}
func (r *PermissionRepo) ListByRole(roleID uint) ([]model.Permission, error) {
var perms []model.Permission
err := r.DB.
Joins("JOIN role_permissions rp ON rp.permission_id = permissions.id").
Where("rp.role_id = ?", roleID).Find(&perms).Error
return perms, err
}
+195
View File
@@ -0,0 +1,195 @@
package repository
import (
"time"
"gorm.io/gorm"
"filestoragesystem/internal/model"
)
// SettingRepo 系统设置数据访问
type SettingRepo struct{ DB *gorm.DB }
// GetAll 获取全部设置
func (r *SettingRepo) GetAll() ([]model.SystemSetting, error) {
var settings []model.SystemSetting
err := r.DB.Order("group_name ASC, id ASC").Find(&settings).Error
return settings, err
}
// GetByGroup 按分组获取设置
func (r *SettingRepo) GetByGroup(group string) ([]model.SystemSetting, error) {
var settings []model.SystemSetting
err := r.DB.Where("group_name = ?", group).Find(&settings).Error
return settings, err
}
// GetValue 获取指定键的值
func (r *SettingRepo) GetValue(key string) (string, error) {
var s model.SystemSetting
err := r.DB.Where("`key` = ?", key).First(&s).Error
if err != nil {
return "", err
}
return s.Value, nil
}
// UpdateValues 批量更新键值
func (r *SettingRepo) UpdateValues(values map[string]string) error {
return r.DB.Transaction(func(tx *gorm.DB) error {
for k, v := range values {
if err := tx.Model(&model.SystemSetting{}).
Where("`key` = ?", k).
Updates(map[string]interface{}{"value": v, "updated_at": time.Now()}).Error; err != nil {
return err
}
}
return nil
})
}
// ---- 操作日志 ----
// OpLogRepo 操作日志数据访问
type OpLogRepo struct{ DB *gorm.DB }
func (r *OpLogRepo) Create(l *model.OperationLog) error { return r.DB.Create(l).Error }
func (r *OpLogRepo) FindByID(id uint) (*model.OperationLog, error) {
var l model.OperationLog
err := r.DB.First(&l, id).Error
return &l, err
}
// OpLogFilter 操作日志过滤
type OpLogFilter struct {
UserID *uint
Username string
Action string
Status string // "1"/"0"
Days int
Page int
PageSize int
}
func (r *OpLogRepo) List(f OpLogFilter) ([]model.OperationLog, int64, error) {
q := r.DB.Model(&model.OperationLog{})
if f.UserID != nil {
q = q.Where("user_id = ?", *f.UserID)
}
if f.Username != "" {
q = q.Where("username LIKE ?", "%"+f.Username+"%")
}
if f.Action != "" {
q = q.Where("action = ?", f.Action)
}
if f.Status != "" {
q = q.Where("status = ?", f.Status)
}
if f.Days > 0 {
q = q.Where("created_at >= date('now', ?)", "-"+itoa(f.Days)+" day")
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var logs []model.OperationLog
err := q.Order("id DESC").
Offset((f.Page - 1) * f.PageSize).Limit(f.PageSize).Find(&logs).Error
return logs, total, err
}
// DeleteBefore 删除指定天数前的日志,返回删除条数
func (r *OpLogRepo) DeleteBefore(days int) (int64, error) {
res := r.DB.Where("created_at < date('now', ?)", "-"+itoa(days)+" day").
Delete(&model.OperationLog{})
return res.RowsAffected, res.Error
}
// ---- 系统日志 ----
// SysLogRepo 系统日志数据访问
type SysLogRepo struct{ DB *gorm.DB }
func (r *SysLogRepo) FindByID(id uint) (*model.SystemLog, error) {
var l model.SystemLog
err := r.DB.First(&l, id).Error
return &l, err
}
// SysLogFilter 系统日志过滤
type SysLogFilter struct {
Level string
Module string
Days int
Page int
PageSize int
}
func (r *SysLogRepo) List(f SysLogFilter) ([]model.SystemLog, int64, error) {
q := r.DB.Model(&model.SystemLog{})
if f.Level != "" {
q = q.Where("level = ?", f.Level)
}
if f.Module != "" {
q = q.Where("module = ?", f.Module)
}
if f.Days > 0 {
q = q.Where("created_at >= date('now', ?)", "-"+itoa(f.Days)+" day")
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var logs []model.SystemLog
err := q.Order("id DESC").
Offset((f.Page - 1) * f.PageSize).Limit(f.PageSize).Find(&logs).Error
return logs, total, err
}
// DeleteBefore 删除指定天数前的日志
func (r *SysLogRepo) DeleteBefore(days int) (int64, error) {
res := r.DB.Where("created_at < date('now', ?)", "-"+itoa(days)+" day").
Delete(&model.SystemLog{})
return res.RowsAffected, res.Error
}
// ---- Webhook ----
// WebhookRepo Webhook数据访问
type WebhookRepo struct{ DB *gorm.DB }
func (r *WebhookRepo) Create(w *model.Webhook) error { return r.DB.Create(w).Error }
func (r *WebhookRepo) FindByID(id uint) (*model.Webhook, error) {
var w model.Webhook
err := r.DB.First(&w, id).Error
return &w, err
}
func (r *WebhookRepo) Update(w *model.Webhook) error { return r.DB.Save(w).Error }
func (r *WebhookRepo) Delete(id, userID uint) error {
return r.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&model.Webhook{}).Error
}
// ListByUser 用户webhook列表
func (r *WebhookRepo) ListByUser(userID uint) ([]model.Webhook, error) {
var hooks []model.Webhook
err := r.DB.Where("user_id = ?", userID).Order("id DESC").Find(&hooks).Error
return hooks, err
}
// ListActive 查询匹配事件的启用webhook:projectID为0表示仅用户级别
func (r *WebhookRepo) ListActive(userID uint, projectID uint) ([]model.Webhook, error) {
var hooks []model.Webhook
err := r.DB.Where("user_id = ? AND status = 1 AND (project_id IS NULL OR project_id = 0 OR project_id = ?)",
userID, projectID).Find(&hooks).Error
return hooks, err
}
func (r *WebhookRepo) Touch(id uint) error {
return r.DB.Model(&model.Webhook{}).Where("id = ?", id).
UpdateColumn("last_triggered_at", gorm.Expr("CURRENT_TIMESTAMP")).Error
}
+108
View File
@@ -0,0 +1,108 @@
package repository
import (
"gorm.io/gorm"
"filestoragesystem/internal/model"
)
// UserRepo 用户数据访问
type UserRepo struct{ DB *gorm.DB }
func (r *UserRepo) Create(u *model.User) error { return r.DB.Create(u).Error }
func (r *UserRepo) FindByID(id uint) (*model.User, error) {
var u model.User
err := r.DB.Preload("Role.Permissions").First(&u, id).Error
return &u, err
}
func (r *UserRepo) FindByUsernameOrEmail(account string) (*model.User, error) {
var u model.User
err := r.DB.Preload("Role.Permissions").
Where("username = ? OR email = ?", account, account).First(&u).Error
return &u, err
}
func (r *UserRepo) FindByUsername(username string) (*model.User, error) {
var u model.User
err := r.DB.Where("username = ?", username).First(&u).Error
return &u, err
}
func (r *UserRepo) FindByEmail(email string) (*model.User, error) {
var u model.User
err := r.DB.Where("email = ?", email).First(&u).Error
return &u, err
}
// List 分页查询用户,keyword匹配用户名/邮箱,roleID/status可选
func (r *UserRepo) List(keyword, roleID, status string, page, pageSize int) ([]model.User, int64, error) {
q := r.DB.Model(&model.User{})
if keyword != "" {
like := "%" + keyword + "%"
q = q.Where("username LIKE ? OR email LIKE ?", like, like)
}
if roleID != "" {
q = q.Where("role_id = ?", roleID)
}
if status != "" {
q = q.Where("status = ?", status)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var users []model.User
err := q.Preload("Role").Order("id DESC").
Offset((page - 1) * pageSize).Limit(pageSize).Find(&users).Error
return users, total, err
}
func (r *UserRepo) Update(u *model.User) error { return r.DB.Save(u).Error }
func (r *UserRepo) UpdateFields(id uint, fields map[string]interface{}) error {
return r.DB.Model(&model.User{}).Where("id = ?", id).Updates(fields).Error
}
func (r *UserRepo) Delete(id uint) error { return r.DB.Delete(&model.User{}, id).Error }
// UpdateStorageUsed 增减用户已用存储(delta可正可负)
func (r *UserRepo) UpdateStorageUsed(id uint, delta int64) error {
return r.DB.Model(&model.User{}).Where("id = ?", id).
UpdateColumn("storage_used", gorm.Expr("storage_used + ?", delta)).Error
}
// ---- API Key ----
// APIKeyRepo API密钥数据访问
type APIKeyRepo struct{ DB *gorm.DB }
func (r *APIKeyRepo) Create(k *model.APIKey) error { return r.DB.Create(k).Error }
func (r *APIKeyRepo) FindByID(id uint) (*model.APIKey, error) {
var k model.APIKey
err := r.DB.First(&k, id).Error
return &k, err
}
func (r *APIKeyRepo) FindByAccessKey(accessKey string) (*model.APIKey, error) {
var k model.APIKey
err := r.DB.Where("access_key = ?", accessKey).First(&k).Error
return &k, err
}
func (r *APIKeyRepo) ListByUser(userID uint) ([]model.APIKey, error) {
var keys []model.APIKey
err := r.DB.Where("user_id = ?", userID).Order("id DESC").Find(&keys).Error
return keys, err
}
func (r *APIKeyRepo) Delete(id, userID uint) error {
return r.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&model.APIKey{}).Error
}
func (r *APIKeyRepo) Touch(id uint) error {
return r.DB.Model(&model.APIKey{}).Where("id = ?", id).
UpdateColumn("last_used_at", gorm.Expr("CURRENT_TIMESTAMP")).Error
}
+155
View File
@@ -0,0 +1,155 @@
package router
import (
"os"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"filestoragesystem/internal/config"
"filestoragesystem/internal/controller"
"filestoragesystem/internal/middleware"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/service"
)
// New 创建gin引擎并注册全部路由
func New(cfg *config.Config, db *gorm.DB, svc *service.Services) *gin.Engine {
gin.SetMode(cfg.Server.Mode)
r := gin.New()
r.Use(gin.Recovery())
r.Use(middleware.RequestID())
r.Use(middleware.CORS())
r.Use(middleware.Logger())
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// 托管前端静态资源(前端为hash路由,全部路径回退到index.html)
dist := "../backend/dist"
if _, err := os.Stat(dist); err == nil {
r.Static("/assets", dist+"/assets")
r.StaticFile("/favicon.svg", dist+"/favicon.svg")
r.NoRoute(func(c *gin.Context) {
c.File(dist + "/index.html")
})
}
api := r.Group("/api/v1")
// 公开接口
api.GET("/status", controller.NewAdminController(controller.NewBase(svc)).SiteStatus)
authCtl := controller.NewAuthController(controller.NewBase(svc))
api.POST("/auth/register", authCtl.Register)
api.POST("/auth/login", authCtl.Login)
fileCtl := controller.NewFileController(controller.NewBase(svc))
api.GET("/files/temp/:token", fileCtl.TempAccess) // 临时链接公开访问
// 认证接口
authed := api.Group("")
authed.Use(middleware.Auth(svc.Auth))
authed.Use(middleware.RateLimit(&repository.SettingRepo{DB: db}))
authed.Use(middleware.SiteStatus(svc.Admin))
// API密钥管理
authed.POST("/auth/apikeys", authCtl.CreateAPIKey)
authed.GET("/auth/apikeys", authCtl.ListAPIKeys)
authed.DELETE("/auth/apikeys/:id", authCtl.DeleteAPIKey)
// 个人资料
userCtl := controller.NewUserController(controller.NewBase(svc))
authed.GET("/user/profile", userCtl.GetProfile)
authed.PUT("/user/profile", userCtl.UpdateProfile)
authed.PUT("/user/password", userCtl.ChangePassword)
// 项目管理
projectCtl := controller.NewProjectController(controller.NewBase(svc))
authed.POST("/projects", projectCtl.Create)
authed.GET("/projects", projectCtl.List)
authed.GET("/projects/:id", projectCtl.Get)
authed.PUT("/projects/:id", projectCtl.Update)
authed.DELETE("/projects/:id", projectCtl.Delete)
// 文件操作
authed.POST("/files/upload", fileCtl.Upload)
authed.GET("/files", fileCtl.List)
authed.GET("/files/:id", fileCtl.GetInfo)
authed.GET("/files/:id/download", fileCtl.Download)
authed.DELETE("/files/:id", fileCtl.Delete)
authed.POST("/files/:id/restore", fileCtl.Restore)
authed.POST("/files/:id/temp-link", fileCtl.CreateTempLink)
// 统计
statsCtl := controller.NewStatsController(controller.NewBase(svc))
authed.GET("/stats/user", statsCtl.UserStats)
authed.GET("/stats/project/:id", statsCtl.ProjectStats)
authed.GET("/stats/traffic", statsCtl.TrafficStats)
// Webhook管理
webhookCtl := controller.NewWebhookController(controller.NewBase(svc))
authed.POST("/webhooks", webhookCtl.Create)
authed.GET("/webhooks", webhookCtl.List)
authed.PUT("/webhooks/:id", webhookCtl.Update)
authed.DELETE("/webhooks/:id", webhookCtl.Delete)
// 管理后台
adminCtl := controller.NewAdminController(controller.NewBase(svc))
roleCtl := controller.NewRoleController(controller.NewBase(svc))
admin := authed.Group("/admin")
// 用户管理(需要对应权限)
users := admin.Group("/users")
{
users.GET("", middleware.RequirePermission("user:list"), adminCtl.ListUsers)
users.GET("/:id", middleware.RequirePermission("user:detail"), adminCtl.GetUser)
users.POST("", middleware.RequirePermission("user:create"), adminCtl.CreateUser)
users.PUT("/:id", middleware.RequirePermission("user:edit"), adminCtl.UpdateUser)
users.DELETE("/:id", middleware.RequirePermission("user:delete"), adminCtl.DeleteUser)
users.PUT("/:id/status", middleware.RequirePermission("user:disable"), adminCtl.SetUserStatus)
users.PUT("/:id/role", middleware.RequirePermission("user:assign_role"), adminCtl.AssignRole)
users.POST("/:id/reset-password", middleware.RequirePermission("user:reset_password"), adminCtl.ResetPassword)
}
// 角色权限管理(超级管理员)
roles := admin.Group("/roles")
{
roles.GET("", middleware.RequirePermission("user:assign_role"), roleCtl.ListRoles)
roles.GET("/:id", middleware.RequirePermission("user:assign_role"), roleCtl.GetRole)
roles.POST("", middleware.RequirePermission("user:assign_role"), roleCtl.CreateRole)
roles.PUT("/:id", middleware.RequirePermission("user:assign_role"), roleCtl.UpdateRole)
roles.DELETE("/:id", middleware.RequirePermission("user:assign_role"), roleCtl.DeleteRole)
roles.PUT("/:id/permissions", middleware.RequirePermission("user:assign_role"), roleCtl.SetPermissions)
}
admin.GET("/permissions", middleware.RequirePermission("user:assign_role"), roleCtl.ListPermissions)
// 系统设置
settings := admin.Group("/settings")
{
settings.GET("", middleware.RequirePermission("system:settings:view"), adminCtl.GetSettings)
settings.PUT("", middleware.RequirePermission("system:settings:edit"), adminCtl.UpdateSettings)
settings.GET("/:group", middleware.RequirePermission("system:settings:view"), adminCtl.GetSettingsByGroup)
settings.PUT("/:group", middleware.RequirePermission("system:settings:edit"), adminCtl.UpdateSettingsByGroup)
}
// 日志管理
logs := admin.Group("/logs")
{
logs.GET("/operation", middleware.RequirePermission("system:logs:operation"), adminCtl.ListOperationLogs)
logs.GET("/operation/:id", middleware.RequirePermission("system:logs:operation"), adminCtl.GetOperationLog)
logs.GET("/system", middleware.RequirePermission("system:logs:system"), adminCtl.ListSystemLogs)
logs.GET("/system/:id", middleware.RequirePermission("system:logs:system"), adminCtl.GetSystemLog)
logs.DELETE("/cleanup", middleware.RequirePermission("system:logs:cleanup"), adminCtl.CleanupLogs)
}
// 数据备份
admin.POST("/backup", middleware.RequirePermission("system:settings:edit"), adminCtl.Backup)
admin.GET("/backups", middleware.RequirePermission("system:settings:view"), adminCtl.ListBackups)
admin.GET("/backups/download", middleware.RequirePermission("system:settings:view"), adminCtl.DownloadBackup)
return r
}
+214
View File
@@ -0,0 +1,214 @@
package service
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"gorm.io/gorm"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
"filestoragesystem/pkg/apperr"
)
// AdminService 系统管理服务(设置/日志/备份)
type AdminService struct {
cfg *config.Config
db *gorm.DB
settingRepo *repository.SettingRepo
opLogRepo *repository.OpLogRepo
sysLogRepo *repository.SysLogRepo
}
// ---- 系统设置 ----
// GetSettings 获取全部设置(按分组组织)
func (s *AdminService) GetSettings() (map[string][]model.SystemSetting, error) {
settings, err := s.settingRepo.GetAll()
if err != nil {
return nil, err
}
groups := make(map[string][]model.SystemSetting)
for _, st := range settings {
groups[st.GroupName] = append(groups[st.GroupName], st)
}
return groups, nil
}
// GetSettingsByGroup 按分组获取设置
func (s *AdminService) GetSettingsByGroup(group string) ([]model.SystemSetting, error) {
return s.settingRepo.GetByGroup(group)
}
// UpdateSettings 批量更新设置(仅更新已存在的键)
func (s *AdminService) UpdateSettings(values map[string]string) error {
if len(values) == 0 {
return nil
}
existing := make(map[string]bool)
all, err := s.settingRepo.GetAll()
if err != nil {
return err
}
for _, st := range all {
existing[st.Key] = true
}
valid := make(map[string]string)
for k, v := range values {
if existing[k] {
valid[k] = v
}
}
if len(valid) == 0 {
return fmt.Errorf("没有可更新的有效设置项")
}
return s.settingRepo.UpdateValues(valid)
}
// ---- 日志 ----
// ListOperationLogs 操作日志列表
func (s *AdminService) ListOperationLogs(f repository.OpLogFilter) ([]model.OperationLog, int64, error) {
if f.Page <= 0 {
f.Page = 1
}
if f.PageSize <= 0 {
f.PageSize = 20
}
return s.opLogRepo.List(f)
}
// GetOperationLog 操作日志详情
func (s *AdminService) GetOperationLog(id uint) (*model.OperationLog, error) {
l, err := s.opLogRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
return l, nil
}
// ListSystemLogs 系统日志列表
func (s *AdminService) ListSystemLogs(f repository.SysLogFilter) ([]model.SystemLog, int64, error) {
if f.Page <= 0 {
f.Page = 1
}
if f.PageSize <= 0 {
f.PageSize = 20
}
return s.sysLogRepo.List(f)
}
// GetSystemLog 系统日志详情
func (s *AdminService) GetSystemLog(id uint) (*model.SystemLog, error) {
l, err := s.sysLogRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
return l, nil
}
// CleanupLogs 清理days天前的日志
func (s *AdminService) CleanupLogs(days int) (opDeleted, sysDeleted int64, err error) {
if days <= 0 {
days = 30
}
opDeleted, err = s.opLogRepo.DeleteBefore(days)
if err != nil {
return
}
sysDeleted, err = s.sysLogRepo.DeleteBefore(days)
utils.Info("system", "清理%d天前日志:操作日志%d条,系统日志%d条", days, opDeleted, sysDeleted)
return
}
// ---- 备份 ----
// BackupDatabase 使用VACUUM INTO备份数据库
func (s *AdminService) BackupDatabase() (string, error) {
if err := os.MkdirAll(s.cfg.Storage.Backups, 0o755); err != nil {
return "", err
}
name := fmt.Sprintf("backup-%s.db", time.Now().Format("20060102-150405"))
target := filepath.Join(s.cfg.Storage.Backups, name)
// SQLite安全备份:VACUUM INTO
quoted := strings.ReplaceAll(target, "'", "''")
if err := s.db.Exec(fmt.Sprintf("VACUUM INTO '%s'", quoted)).Error; err != nil {
utils.Error("system", "数据库备份失败: %v", err)
return "", fmt.Errorf("备份失败: %w", err)
}
utils.Info("system", "数据库备份完成: %s", name)
return name, nil
}
// ListBackups 备份列表
func (s *AdminService) ListBackups() ([]map[string]interface{}, error) {
entries, err := os.ReadDir(s.cfg.Storage.Backups)
if err != nil {
return []map[string]interface{}{}, nil
}
var list []map[string]interface{}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".db") {
continue
}
info, _ := e.Info()
list = append(list, map[string]interface{}{
"name": e.Name(),
"size": info.Size(),
"time": info.ModTime().Format(time.RFC3339),
})
}
sort.Slice(list, func(i, j int) bool {
return list[i]["name"].(string) > list[j]["name"].(string)
})
return list, nil
}
// BackupPath 校验备份文件名并返回其物理路径(供下载)
func (s *AdminService) BackupPath(name string) (string, error) {
// 只允许 backup-*.db 格式,防止路径穿越
if strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") || !strings.HasPrefix(name, "backup-") || !strings.HasSuffix(name, ".db") {
return "", fmt.Errorf("非法的备份文件名")
}
p := filepath.Join(s.cfg.Storage.Backups, name)
if _, err := os.Stat(p); err != nil {
return "", fmt.Errorf("备份文件不存在")
}
return p, nil
}
// ---- 站点状态 ----
// SiteStatus 公开的站点状态信息
func (s *AdminService) SiteStatus() map[string]interface{} {
get := func(k, def string) string {
if v, err := s.settingRepo.GetValue(k); err == nil {
return v
}
return def
}
return map[string]interface{}{
"site_name": get("site_name", "云泽文件存储云平台"),
"site_description": get("site_description", ""),
"site_logo": get("site_logo", ""),
"site_icp": get("site_icp", ""),
"site_copyright": get("site_copyright", ""),
"site_enabled": get("site_enabled", "true") == "true",
"register_enabled": get("register_enabled", "true") == "true",
"maintenance_msg": get("site_maintenance_msg", ""),
}
}
// IsSiteEnabled 站点是否开启
func (s *AdminService) IsSiteEnabled() bool {
if v, err := s.settingRepo.GetValue("site_enabled"); err == nil {
return v == "true" || v == "1"
}
return true
}
+346
View File
@@ -0,0 +1,346 @@
package service
import (
"fmt"
"strings"
"sync"
"time"
"unicode"
"gorm.io/gorm"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
"filestoragesystem/pkg/apperr"
)
const (
maxLoginFails = 5 // 最大登录失败次数
lockDuration = 15 * time.Minute // 锁定时长
signTimeWindow = 5 * time.Minute // 签名时间戳有效窗口
)
// AuthService 认证服务
type AuthService struct {
cfg *config.Config
userRepo *repository.UserRepo
apiKeyRepo *repository.APIKeyRepo
roleRepo *repository.RoleRepo
settingRepo *repository.SettingRepo
opLogRepo *repository.OpLogRepo
mu sync.Mutex
loginFails map[string]*loginState // 登录失败状态
nonces map[string]time.Time // 防重放nonce
}
type loginState struct {
fails int
lockedUntil time.Time
}
// ensureState 惰性初始化登录失败与nonce缓存
func (s *AuthService) ensureState() {
s.mu.Lock()
defer s.mu.Unlock()
if s.loginFails == nil {
s.loginFails = make(map[string]*loginState)
}
if s.nonces == nil {
s.nonces = make(map[string]time.Time)
}
}
// Register 用户注册
func (s *AuthService) Register(username, email, password string) (*model.User, error) {
// 注册开关
if v, err := s.settingRepo.GetValue("register_enabled"); err == nil && v != "true" {
return nil, apperr.ErrRegisterClosed
}
username = strings.TrimSpace(username)
email = strings.TrimSpace(strings.ToLower(email))
if len(username) < 3 || len(username) > 50 {
return nil, fmt.Errorf("用户名长度需在3-50之间")
}
for _, ch := range username {
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' && ch != '-' && ch < 0x80 {
return nil, fmt.Errorf("用户名仅支持字母、数字、下划线和横线")
}
}
if !utils.IsEmail(email) {
return nil, fmt.Errorf("邮箱格式不正确")
}
if !utils.IsValidPassword(password) {
return nil, fmt.Errorf("密码至少8位且需包含字母和数字")
}
if _, err := s.userRepo.FindByUsername(username); err == nil {
return nil, apperr.ErrUserExists
}
if _, err := s.userRepo.FindByEmail(email); err == nil {
return nil, apperr.ErrUserExists
}
// 默认角色与配额
role, err := s.roleRepo.FindByCode("user")
if err != nil {
return nil, fmt.Errorf("默认角色不存在: %w", err)
}
limit := int64(10737418240)
if v, err := s.settingRepo.GetValue("default_storage_limit"); err == nil {
var n int64
if _, err := fmt.Sscanf(v, "%d", &n); err == nil && n > 0 {
limit = n
}
}
hash, err := utils.HashPassword(password)
if err != nil {
return nil, err
}
u := &model.User{
Username: username, Email: email, PasswordHash: hash,
RoleID: role.ID, Status: 1, StorageLimit: limit,
}
if err := s.userRepo.Create(u); err != nil {
return nil, err
}
utils.Info("auth", "新用户注册: %s", username)
return u, nil
}
// Login 用户登录,成功返回用户与JWT token
func (s *AuthService) Login(account, password, ip string) (*model.User, string, error) {
s.ensureState()
account = strings.TrimSpace(account)
// 锁定检查
s.mu.Lock()
if st, ok := s.loginFails[account]; ok {
if time.Now().Before(st.lockedUntil) {
s.mu.Unlock()
return nil, "", apperr.ErrAccountLocked
}
}
s.mu.Unlock()
u, err := s.userRepo.FindByUsernameOrEmail(account)
if err != nil {
s.recordLoginFail(account)
return nil, "", apperr.ErrPasswordWrong
}
if u.Status != 1 {
return nil, "", apperr.ErrUserDisabled
}
if !utils.CheckPassword(u.PasswordHash, password) {
s.recordLoginFail(account)
return nil, "", apperr.ErrPasswordWrong
}
// 登录成功:清除失败计数
s.mu.Lock()
delete(s.loginFails, account)
s.mu.Unlock()
now := time.Now()
_ = s.userRepo.UpdateFields(u.ID, map[string]interface{}{
"last_login_at": now, "last_login_ip": ip,
})
token, err := utils.GenerateToken(u.ID, u.Username, roleCodeOf(u))
if err != nil {
return nil, "", err
}
return u, token, nil
}
func (s *AuthService) recordLoginFail(account string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.loginFails == nil {
s.loginFails = make(map[string]*loginState)
}
st, ok := s.loginFails[account]
if !ok {
st = &loginState{}
s.loginFails[account] = st
}
st.fails++
if st.fails >= maxLoginFails {
st.lockedUntil = time.Now().Add(lockDuration)
st.fails = 0
utils.Warn("auth", "账户 %s 因连续登录失败被锁定%d分钟", account, int(lockDuration.Minutes()))
}
}
// roleCodeOf 用户角色编码(内存缓存于User.Role)
func roleCodeOf(u *model.User) string {
if u.Role != nil {
return u.Role.Code
}
return ""
}
// CreateAPIKey 为用户生成API密钥,secret仅此一次返回
func (s *AuthService) CreateAPIKey(userID uint, name string) (*model.APIKey, string, error) {
u, err := s.userRepo.FindByID(userID)
if err != nil {
return nil, "", apperr.ErrNotFound
}
key := &model.APIKey{
UserID: userID,
AccessKey: utils.RandomKey(32),
SecretKey: utils.RandomKey(64),
Name: name,
Status: 1,
}
if key.Name == "" {
key.Name = u.Username + "的密钥"
}
if err := s.apiKeyRepo.Create(key); err != nil {
return nil, "", err
}
utils.Info("auth", "用户 %s 创建API密钥: %s", u.Username, key.AccessKey)
return key, key.SecretKey, nil
}
// ListAPIKeys 用户密钥列表
func (s *AuthService) ListAPIKeys(userID uint) ([]model.APIKey, error) {
return s.apiKeyRepo.ListByUser(userID)
}
// DeleteAPIKey 删除密钥
func (s *AuthService) DeleteAPIKey(userID, id uint) error {
if err := s.apiKeyRepo.Delete(id, userID); err != nil {
return apperr.ErrNotFound
}
return nil
}
// VerifyAPIKeySignature 校验API Key HMAC-SHA256签名,返回所属用户
// 签名串: accessKey + "\n" + timestamp + "\n" + nonce + "\n" + method + "\n" + path
func (s *AuthService) VerifyAPIKeySignature(accessKey, timestamp, nonce, signature, method, path string) (*model.User, error) {
s.ensureState()
// 时间窗口
var ts int64
if _, err := fmt.Sscanf(timestamp, "%d", &ts); err != nil {
return nil, apperr.ErrSignInvalid
}
t := time.Unix(ts, 0)
if diff := time.Since(t); diff > signTimeWindow || diff < -signTimeWindow {
return nil, apperr.ErrSignExpired
}
// 防重放
s.mu.Lock()
if s.nonces == nil {
s.nonces = make(map[string]time.Time)
}
if _, used := s.nonces[nonce]; used {
s.mu.Unlock()
return nil, apperr.ErrSignInvalid
}
s.nonces[nonce] = time.Now()
// 顺带清理过期nonce
if len(s.nonces) > 10000 {
cutoff := time.Now().Add(-signTimeWindow * 2)
for k, v := range s.nonces {
if v.Before(cutoff) {
delete(s.nonces, k)
}
}
}
s.mu.Unlock()
key, err := s.apiKeyRepo.FindByAccessKey(accessKey)
if err != nil {
return nil, apperr.ErrSignInvalid
}
if key.Status != 1 {
return nil, apperr.ErrForbidden
}
message := strings.Join([]string{accessKey, timestamp, nonce, method, path}, "\n")
expect := utils.HMACSHA256(key.SecretKey, message)
if !strings.EqualFold(expect, signature) {
return nil, apperr.ErrSignInvalid
}
_ = s.apiKeyRepo.Touch(key.ID)
u, err := s.userRepo.FindByID(key.UserID)
if err != nil {
return nil, apperr.ErrNotFound
}
if u.Status != 1 {
return nil, apperr.ErrUserDisabled
}
return u, nil
}
// LoadUserByID 加载用户(含角色权限)
func (s *AuthService) LoadUserByID(id uint) (*model.User, error) {
u, err := s.userRepo.FindByID(id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, apperr.ErrNotFound
}
return nil, err
}
if u.Status != 1 {
return nil, apperr.ErrUserDisabled
}
return u, nil
}
// PermissionCodesOf 用户拥有的权限编码集合
func PermissionCodesOf(u *model.User) map[string]bool {
codes := make(map[string]bool)
if u == nil || u.Role == nil {
return codes
}
if u.Role.Code == "super_admin" {
codes["*"] = true
return codes
}
for _, p := range u.Role.Permissions {
codes[p.Code] = true
}
return codes
}
// BootstrapAdmin 按配置确保管理员账户存在
func (s *AuthService) BootstrapAdmin() error {
ac := s.cfg.Admin
if ac.Username == "" {
return nil
}
u, err := s.userRepo.FindByUsername(ac.Username)
if err == nil {
// 已存在:确保其为配置的角色
if role, rerr := s.roleRepo.FindByCode(ac.Role); rerr == nil && u.RoleID != role.ID {
_ = s.userRepo.UpdateFields(u.ID, map[string]interface{}{"role_id": role.ID, "status": 1})
}
return nil
}
role, err := s.roleRepo.FindByCode(ac.Role)
if err != nil {
return fmt.Errorf("配置的管理员角色 %s 不存在: %w", ac.Role, err)
}
hash, err := utils.HashPassword(ac.Password)
if err != nil {
return err
}
admin := &model.User{
Username: ac.Username, Email: ac.Email, PasswordHash: hash,
RoleID: role.ID, Status: 1, StorageLimit: 1 << 40,
}
if err := s.userRepo.Create(admin); err != nil {
return err
}
utils.Info("auth", "已创建管理员账户: %s", ac.Username)
return nil
}
+423
View File
@@ -0,0 +1,423 @@
package service
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"mime"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
"filestoragesystem/pkg/apperr"
)
// FileService 文件服务
type FileService struct {
cfg *config.Config
fileRepo *repository.FileRepo
projectRepo *repository.ProjectRepo
userRepo *repository.UserRepo
tempLinkRepo *repository.TempLinkRepo
trafficRepo *repository.TrafficRepo
settingRepo *repository.SettingRepo
opLogRepo *repository.OpLogRepo
webhookSvc *WebhookService
}
// settingInt 读取数字型设置
func (s *FileService) settingInt(key string, def int64) int64 {
if v, err := s.settingRepo.GetValue(key); err == nil {
var n int64
if _, e := fmt.Sscanf(v, "%d", &n); e == nil && n > 0 {
return n
}
}
return def
}
// settingBool 读取布尔型设置
func (s *FileService) settingBool(key string, def bool) bool {
if v, err := s.settingRepo.GetValue(key); err == nil {
return v == "true" || v == "1"
}
return def
}
// Upload 上传文件:校验配额/类型/大小,MD5去重,落盘,记录统计
func (s *FileService) Upload(c *gin.Context, userID uint, fh *multipart.FileHeader) (*model.File, error) {
projectID, _ := strconvUint(c.PostForm("project_id"))
if projectID == 0 {
return nil, fmt.Errorf("缺少项目ID")
}
project, err := s.projectRepo.FindByID(projectID)
if err != nil || project.UserID != userID {
return nil, apperr.ErrForbidden
}
if project.Status != 1 {
return nil, fmt.Errorf("项目已被禁用")
}
src, err := fh.Open()
if err != nil {
return nil, err
}
defer src.Close()
size := fh.Size
// 大小限制
maxSize := s.settingInt("max_file_size", 104857600)
if size > maxSize {
return nil, apperr.ErrFileTooLarge
}
// 类型限制
allowed := "*"
if v, err := s.settingRepo.GetValue("allowed_file_types"); err == nil {
allowed = v
}
if !s.checkTypeAllowed(fh.Filename, allowed) {
return nil, apperr.ErrFileTypeDenied
}
// 配额
if project.StorageUsed+size > project.StorageLimit {
return nil, apperr.ErrQuotaExceeded
}
user, err := s.userRepo.FindByID(userID)
if err != nil {
return nil, apperr.ErrNotFound
}
if user.StorageUsed+size > user.StorageLimit {
return nil, apperr.ErrQuotaExceeded
}
// 虚拟路径 + 文件名
virtualPath := utils.SanitizePath(c.PostForm("path"))
baseName := filepath.Base(utils.SanitizePath(fh.Filename))
if baseName == "" || baseName == "." || baseName == "/" {
baseName = "unnamed"
}
filename := baseName
if virtualPath != "" {
filename = virtualPath + "/" + baseName
}
visibility := int8(1)
if c.PostForm("visibility") == "2" || c.PostForm("visibility") == "public" {
visibility = 2
}
// 写入临时文件并计算MD5
tmpFile, err := os.CreateTemp("", "fss-upload-*")
if err != nil {
return nil, err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
hasher := md5.New()
if _, err := io.Copy(io.MultiWriter(tmpFile, hasher), src); err != nil {
tmpFile.Close()
return nil, err
}
tmpFile.Close()
md5sum := hex.EncodeToString(hasher.Sum(nil))
// 去重:相同MD5复用已有物理文件
storedRel := ""
if exist, err := s.fileRepo.FindByMD5AndPath(md5sum); err == nil && exist.StoredPath != "" {
if _, serr := os.Stat(filepath.Join(s.cfg.Storage.Root, exist.StoredPath)); serr == nil {
storedRel = exist.StoredPath
}
}
// 无可复用文件则保存到 blob/<md5前2位>/<md5>
if storedRel == "" {
blobRel := filepath.ToSlash(filepath.Join("blobs", md5sum[:2], md5sum))
blobAbs := filepath.Join(s.cfg.Storage.Root, blobRel)
if err := os.MkdirAll(filepath.Dir(blobAbs), 0o755); err != nil {
return nil, err
}
if err := os.Rename(tmpPath, blobAbs); err != nil {
// 跨盘时回退为复制
if cerr := copyFile(tmpPath, blobAbs); cerr != nil {
return nil, cerr
}
}
storedRel = blobRel
}
mimeType := fh.Header.Get("Content-Type")
if mimeType == "" || mimeType == "application/octet-stream" {
if t := mime.TypeByExtension(strings.ToLower(filepath.Ext(baseName))); t != "" {
mimeType = t
}
}
f := &model.File{
ProjectID: projectID,
UserID: userID,
Filename: filename,
StoredPath: storedRel,
Size: size,
MimeType: mimeType,
MD5: md5sum,
Visibility: visibility,
Status: 1,
}
if err := s.fileRepo.Create(f); err != nil {
return nil, err
}
// 更新配额、流量、webhook
_ = s.projectRepo.UpdateStorageUsed(projectID, size)
_ = s.userRepo.UpdateStorageUsed(userID, size)
_ = s.trafficRepo.Create(&model.TrafficLog{
UserID: userID, ProjectID: projectID, FileID: f.ID,
Type: 1, Size: size, IP: utils.ClientIP(c), CreatedAt: time.Now(),
})
s.webhookSvc.Dispatch(userID, projectID, "file.upload", map[string]interface{}{
"file_id": f.ID, "filename": f.Filename, "size": f.Size, "md5": f.MD5, "project_id": projectID,
})
utils.Info("storage", "用户#%d 上传文件 %s (%d字节)", userID, f.Filename, f.Size)
return f, nil
}
func (s *FileService) checkTypeAllowed(filename, allowed string) bool {
allowed = strings.TrimSpace(allowed)
if allowed == "" || allowed == "*" {
return true
}
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
if ext == "" {
return true // 无扩展名不限制
}
for _, t := range strings.Split(allowed, ",") {
t = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(t, ".")))
if t == "*" || t == ext {
return true
}
}
return false
}
// GetInfo 获取文件信息(所有者或管理员)
func (s *FileService) GetInfo(userID uint, id uint, isAdmin bool) (*model.File, error) {
f, err := s.fileRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
if !isAdmin && f.UserID != userID {
return nil, apperr.ErrForbidden
}
return f, nil
}
// List 文件列表
func (s *FileService) List(userID uint, isAdmin bool, filter repository.FileListFilter) ([]model.File, int64, error) {
if !isAdmin {
filter.UserID = userID
}
if filter.Page <= 0 {
filter.Page = 1
}
if filter.PageSize <= 0 {
filter.PageSize = 20
}
return s.fileRepo.List(filter)
}
// Open 打开文件物理句柄并校验访问权,记录流量
func (s *FileService) Open(c *gin.Context, userID uint, id uint, isAdmin bool) (*model.File, *os.File, error) {
f, err := s.GetInfo(userID, id, isAdmin)
if err != nil {
return nil, nil, err
}
if f.Status != 1 {
return nil, nil, apperr.ErrFileDeleted
}
abs := filepath.Join(s.cfg.Storage.Root, f.StoredPath)
fp, err := os.Open(abs)
if err != nil {
utils.Error("storage", "打开文件失败 path=%s err=%v", abs, err)
return nil, nil, fmt.Errorf("文件读取失败")
}
return f, fp, nil
}
// RecordDownload 记录下载行为
func (s *FileService) RecordDownload(c *gin.Context, f *model.File) {
_ = s.fileRepo.IncrDownloadCount(f.ID)
_ = s.trafficRepo.Create(&model.TrafficLog{
UserID: f.UserID, ProjectID: f.ProjectID, FileID: f.ID,
Type: 2, Size: f.Size, IP: utils.ClientIP(c), CreatedAt: time.Now(),
})
}
// Delete 删除文件;permanent=true时硬删除
func (s *FileService) Delete(userID uint, id uint, isAdmin bool, permanent bool) error {
f, err := s.GetInfo(userID, id, isAdmin)
if err != nil {
return err
}
if !permanent {
if f.Status == 0 {
return apperr.ErrFileDeleted
}
if err := s.fileRepo.SoftDelete(id); err != nil {
return err
}
// 回收站不占配额
_ = s.projectRepo.UpdateStorageUsed(f.ProjectID, -f.Size)
_ = s.userRepo.UpdateStorageUsed(f.UserID, -f.Size)
return nil
}
// 硬删除
if err := s.fileRepo.Delete(id); err != nil {
return err
}
_ = s.tempLinkRepo.DeleteByFile(id)
// 若处于回收站,配额已扣减过
if f.Status == 1 {
_ = s.projectRepo.UpdateStorageUsed(f.ProjectID, -f.Size)
_ = s.userRepo.UpdateStorageUsed(f.UserID, -f.Size)
}
// 物理文件无其他引用时删除
if n, err := s.fileRepo.CountByStoredPath(f.StoredPath); err == nil && n == 0 {
_ = os.Remove(filepath.Join(s.cfg.Storage.Root, f.StoredPath))
}
s.webhookSvc.Dispatch(f.UserID, f.ProjectID, "file.delete", map[string]interface{}{
"file_id": f.ID, "filename": f.Filename, "permanent": permanent,
})
return nil
}
// Restore 从回收站恢复文件
func (s *FileService) Restore(userID uint, id uint, isAdmin bool) error {
f, err := s.GetInfo(userID, id, isAdmin)
if err != nil {
return err
}
if f.Status != 0 {
return fmt.Errorf("文件不在回收站中")
}
// 恢复前检查配额
project, err := s.projectRepo.FindByID(f.ProjectID)
if err == nil {
if project.StorageUsed+f.Size > project.StorageLimit {
return apperr.ErrQuotaExceeded
}
}
user, err := s.userRepo.FindByID(f.UserID)
if err == nil && user.StorageUsed+f.Size > user.StorageLimit {
return apperr.ErrQuotaExceeded
}
if err := s.fileRepo.Restore(id); err != nil {
return err
}
_ = s.projectRepo.UpdateStorageUsed(f.ProjectID, f.Size)
_ = s.userRepo.UpdateStorageUsed(f.UserID, f.Size)
return nil
}
// CreateTempLink 生成临时访问链接
func (s *FileService) CreateTempLink(userID uint, id uint, isAdmin bool, password string, maxCount int, expiresInSeconds int64) (*model.TempLink, error) {
f, err := s.GetInfo(userID, id, isAdmin)
if err != nil {
return nil, err
}
if f.Status != 1 {
return nil, apperr.ErrFileDeleted
}
if expiresInSeconds <= 0 {
expiresInSeconds = s.settingInt("temp_link_max_age", 3600)
}
if expiresInSeconds > 7*24*3600 {
expiresInSeconds = 7 * 24 * 3600
}
link := &model.TempLink{
FileID: f.ID,
UserID: userID,
Token: utils.RandomKey(48),
MaxCount: maxCount,
ExpiresAt: time.Now().Add(time.Duration(expiresInSeconds) * time.Second),
}
if password != "" {
link.Password = password
}
if err := s.tempLinkRepo.Create(link); err != nil {
return nil, err
}
link.HasPwd = link.Password != ""
return link, nil
}
// ResolveTempLink 通过token解析临时链接(校验有效期/次数/密码),返回文件与物理句柄
func (s *FileService) ResolveTempLink(token, password string) (*model.File, *os.File, error) {
link, err := s.tempLinkRepo.FindByToken(token)
if err != nil {
return nil, nil, apperr.ErrLinkExpired
}
if time.Now().After(link.ExpiresAt) {
return nil, nil, apperr.ErrLinkExpired
}
if link.MaxCount > 0 && link.UsedCount >= link.MaxCount {
return nil, nil, apperr.ErrLinkExpired
}
if link.Password != "" && link.Password != password {
return nil, nil, apperr.ErrLinkPassword
}
f, err := s.fileRepo.FindByID(link.FileID)
if err != nil || f.Status != 1 {
return nil, nil, apperr.ErrNotFound
}
fp, err := os.Open(filepath.Join(s.cfg.Storage.Root, f.StoredPath))
if err != nil {
return nil, nil, fmt.Errorf("文件读取失败")
}
_ = s.tempLinkRepo.IncrUsed(link.ID)
return f, fp, nil
}
// ---- 辅助函数 ----
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}
func strconvUint(s string) (uint, error) {
var n uint
if s == "" {
return 0, fmt.Errorf("empty")
}
for _, ch := range s {
if ch < '0' || ch > '9' {
return 0, fmt.Errorf("invalid")
}
n = n*10 + uint(ch-'0')
}
return n, nil
}
+110
View File
@@ -0,0 +1,110 @@
package service
import (
"fmt"
"os"
"path/filepath"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
"filestoragesystem/pkg/apperr"
)
// ProjectService 项目服务
type ProjectService struct {
cfg *config.Config
projectRepo *repository.ProjectRepo
fileRepo *repository.FileRepo
settingRepo *repository.SettingRepo
opLogRepo *repository.OpLogRepo
webhookSvc *WebhookService
}
// Create 创建项目
func (s *ProjectService) Create(userID uint, name, description string, storageLimit int64) (*model.Project, error) {
if name == "" || len(name) > 100 {
return nil, fmt.Errorf("项目名称不能为空且不超过100字符")
}
if _, err := s.projectRepo.FindByName(userID, name); err == nil {
return nil, apperr.ErrProjectExists
}
if storageLimit <= 0 {
storageLimit = 5368709120 // 5GB
if v, err := s.settingRepo.GetValue("default_project_limit"); err == nil {
var n int64
if _, e := fmt.Sscanf(v, "%d", &n); e == nil && n > 0 {
storageLimit = n
}
}
}
p := &model.Project{
UserID: userID, Name: name, Description: description,
StorageLimit: storageLimit, Status: 1,
}
if err := s.projectRepo.Create(p); err != nil {
return nil, err
}
return p, nil
}
// List 项目列表,all=true时管理员查看全部
func (s *ProjectService) List(userID uint, keyword string, all bool, page, pageSize int) ([]model.Project, int64, error) {
return s.projectRepo.List(userID, keyword, all, page, pageSize)
}
// Get 项目详情(所有者或管理员)
func (s *ProjectService) Get(userID uint, id uint, isAdmin bool) (*model.Project, error) {
p, err := s.projectRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
if !isAdmin && p.UserID != userID {
return nil, apperr.ErrForbidden
}
return p, nil
}
// Update 更新项目
func (s *ProjectService) Update(userID uint, id uint, isAdmin bool, name, description string, storageLimit int64) (*model.Project, error) {
p, err := s.Get(userID, id, isAdmin)
if err != nil {
return nil, err
}
if name != "" && name != p.Name {
if _, err := s.projectRepo.FindByName(userID, name); err == nil {
return nil, apperr.ErrProjectExists
}
p.Name = name
}
if description != "" {
p.Description = description
}
if storageLimit > 0 {
p.StorageLimit = storageLimit
}
if err := s.projectRepo.Update(p); err != nil {
return nil, err
}
return p, nil
}
// Delete 删除项目及其全部文件(硬删除)
func (s *ProjectService) Delete(userID uint, id uint, isAdmin bool) error {
if _, err := s.Get(userID, id, isAdmin); err != nil {
return err
}
// 删除物理文件
files, _ := s.fileRepo.ListByProject(id)
seen := make(map[string]bool)
for _, f := range files {
if f.StoredPath != "" && !seen[f.StoredPath] {
seen[f.StoredPath] = true
_ = os.Remove(filepath.Join(s.cfg.Storage.Root, f.StoredPath))
}
}
_ = s.fileRepo.DB.Exec("DELETE FROM files WHERE project_id = ?", id).Error
_ = s.fileRepo.DB.Exec("DELETE FROM temp_links WHERE file_id IN (SELECT id FROM files WHERE project_id = ?)", id).Error
return s.projectRepo.Delete(id)
}
+136
View File
@@ -0,0 +1,136 @@
package service
import (
"fmt"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
"filestoragesystem/pkg/apperr"
)
// RoleService 角色权限服务
type RoleService struct {
roleRepo *repository.RoleRepo
permRepo *repository.PermissionRepo
}
// ListRoles 角色列表
func (s *RoleService) ListRoles() ([]model.Role, error) {
return s.roleRepo.List()
}
// GetRole 角色详情(含权限)
func (s *RoleService) GetRole(id uint) (*model.Role, error) {
r, err := s.roleRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
return r, nil
}
// CreateRole 创建角色
func (s *RoleService) CreateRole(name, code, description string, permIDs []uint) (*model.Role, error) {
if name == "" || code == "" {
return nil, fmt.Errorf("角色名称和编码不能为空")
}
if _, err := s.roleRepo.FindByCode(code); err == nil {
return nil, fmt.Errorf("角色编码已存在")
}
role := &model.Role{
Name: name, Code: code, Description: description,
IsSystem: false, Status: 1,
Permissions: s.mustPerms(permIDs),
}
if err := s.roleRepo.Create(role); err != nil {
return nil, err
}
return role, nil
}
// UpdateRole 编辑角色(系统角色仅允许改描述与权限)
func (s *RoleService) UpdateRole(id uint, name, description string, permIDs []uint) (*model.Role, error) {
role, err := s.roleRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
if role.Code == "super_admin" {
return nil, apperr.ErrSystemRole
}
if name != "" && !role.IsSystem {
role.Name = name
}
if description != "" {
role.Description = description
}
if err := s.roleRepo.Update(role); err != nil {
return nil, err
}
if permIDs != nil {
if err := s.roleRepo.ReplacePermissions(id, permIDs); err != nil {
return nil, err
}
}
return s.roleRepo.FindByID(id)
}
// DeleteRole 删除角色(系统角色不可删,有用户的角色不可删)
func (s *RoleService) DeleteRole(id uint) error {
role, err := s.roleRepo.FindByID(id)
if err != nil {
return apperr.ErrNotFound
}
if role.IsSystem {
return apperr.ErrSystemRole
}
if n, _ := s.roleRepo.CountUsersByRole(id); n > 0 {
return fmt.Errorf("该角色下仍有 %d 个用户,无法删除", n)
}
return s.roleRepo.Delete(id)
}
// SetPermissions 配置角色权限
func (s *RoleService) SetPermissions(id uint, permIDs []uint) error {
role, err := s.roleRepo.FindByID(id)
if err != nil {
return apperr.ErrNotFound
}
if role.Code == "super_admin" {
return apperr.ErrSystemRole
}
return s.roleRepo.ReplacePermissions(id, permIDs)
}
// ListPermissions 全部权限
func (s *RoleService) ListPermissions() ([]model.Permission, error) {
return s.permRepo.List()
}
// PermissionsGrouped 按模块分组权限
func (s *RoleService) PermissionsGrouped() (map[string][]model.Permission, []string, error) {
perms, err := s.permRepo.List()
if err != nil {
return nil, nil, err
}
groups := make(map[string][]model.Permission)
var order []string
seen := make(map[string]bool)
for _, p := range perms {
if !seen[p.Module] {
seen[p.Module] = true
order = append(order, p.Module)
}
groups[p.Module] = append(groups[p.Module], p)
}
return groups, order, nil
}
func (s *RoleService) mustPerms(ids []uint) []model.Permission {
if len(ids) == 0 {
return nil
}
perms, err := s.permRepo.ListByIDs(ids)
if err != nil {
return nil
}
return perms
}
+116
View File
@@ -0,0 +1,116 @@
package service
import (
"time"
"gorm.io/gorm"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
)
// OpLogContext 操作日志上下文(由controller从请求中提取)
type OpLogContext struct {
UserID *uint
Username string
IP string
UserAgent string
Path string
Method string
}
// Services 服务容器
type Services struct {
cfg *config.Config
db *gorm.DB
Auth *AuthService
User *UserService
Project *ProjectService
File *FileService
Role *RoleService
Admin *AdminService
Stats *StatsService
Webhook *WebhookService
opLogRepo *repository.OpLogRepo
}
// NewServices 创建服务容器并初始化各服务
func NewServices(cfg *config.Config, db *gorm.DB) *Services {
userRepo := &repository.UserRepo{DB: db}
roleRepo := &repository.RoleRepo{DB: db}
permRepo := &repository.PermissionRepo{DB: db}
projectRepo := &repository.ProjectRepo{DB: db}
fileRepo := &repository.FileRepo{DB: db}
tempLinkRepo := &repository.TempLinkRepo{DB: db}
trafficRepo := &repository.TrafficRepo{DB: db}
settingRepo := &repository.SettingRepo{DB: db}
opLogRepo := &repository.OpLogRepo{DB: db}
sysLogRepo := &repository.SysLogRepo{DB: db}
webhookRepo := &repository.WebhookRepo{DB: db}
apiKeyRepo := &repository.APIKeyRepo{DB: db}
s := &Services{cfg: cfg, db: db, opLogRepo: opLogRepo}
s.Auth = &AuthService{
cfg: cfg, userRepo: userRepo, apiKeyRepo: apiKeyRepo,
roleRepo: roleRepo, settingRepo: settingRepo, opLogRepo: opLogRepo,
}
s.User = &UserService{
cfg: cfg, userRepo: userRepo, roleRepo: roleRepo,
settingRepo: settingRepo, opLogRepo: opLogRepo, fileRepo: fileRepo, projectRepo: projectRepo,
}
s.Project = &ProjectService{
cfg: cfg, projectRepo: projectRepo, fileRepo: fileRepo,
settingRepo: settingRepo, opLogRepo: opLogRepo, webhookSvc: nil,
}
s.Webhook = &WebhookService{cfg: cfg, webhookRepo: webhookRepo, settingRepo: settingRepo}
s.File = &FileService{
cfg: cfg, fileRepo: fileRepo, projectRepo: projectRepo, userRepo: userRepo,
tempLinkRepo: tempLinkRepo, trafficRepo: trafficRepo,
settingRepo: settingRepo, opLogRepo: opLogRepo, webhookSvc: s.Webhook,
}
s.Role = &RoleService{roleRepo: roleRepo, permRepo: permRepo}
s.Admin = &AdminService{
cfg: cfg, db: db, settingRepo: settingRepo,
opLogRepo: opLogRepo, sysLogRepo: sysLogRepo,
}
s.Stats = &StatsService{
cfg: cfg, userRepo: userRepo, projectRepo: projectRepo,
fileRepo: fileRepo, trafficRepo: trafficRepo,
}
s.Project.webhookSvc = s.Webhook
return s
}
// RecordOpLog 记录操作日志(异步容忍失败)
func (s *Services) RecordOpLog(ctx *OpLogContext, action, resourceType string, resourceID uint, resourceName string, success bool, errMsg string) {
log := model.OperationLog{
Action: action,
ResourceType: resourceType,
ResourceID: resourceID,
ResourceName: resourceName,
IP: ctx.IP,
UserAgent: truncate(ctx.UserAgent, 500),
RequestPath: truncate(ctx.Path, 500),
RequestMethod: ctx.Method,
Status: 1,
ErrorMessage: errMsg,
CreatedAt: time.Now(),
}
if ctx.UserID != nil {
log.UserID = ctx.UserID
}
log.Username = ctx.Username
if !success {
log.Status = 0
}
_ = s.opLogRepo.Create(&log)
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}
+109
View File
@@ -0,0 +1,109 @@
package service
import (
"filestoragesystem/internal/config"
"filestoragesystem/internal/repository"
"filestoragesystem/pkg/apperr"
)
// StatsService 存储与流量统计服务
type StatsService struct {
cfg *config.Config
userRepo *repository.UserRepo
projectRepo *repository.ProjectRepo
fileRepo *repository.FileRepo
trafficRepo *repository.TrafficRepo
}
// UserStats 用户存储统计:总体 + 项目明细 + 流量汇总
func (s *StatsService) UserStats(userID uint) (map[string]interface{}, error) {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return nil, err
}
fileCount, totalSize, err := s.fileRepo.StatsByUser(userID)
if err != nil {
return nil, err
}
projects, err := s.projectRepo.ListByUser(userID)
if err != nil {
return nil, err
}
projectStats := make([]map[string]interface{}, 0, len(projects))
for _, p := range projects {
count, size, err := s.fileRepo.StatsByProject(p.ID)
if err != nil {
continue
}
projectStats = append(projectStats, map[string]interface{}{
"project_id": p.ID,
"name": p.Name,
"file_count": count,
"storage_used": size,
"storage_limit": p.StorageLimit,
})
}
upload30, download30, _ := s.trafficRepo.SumByUser(userID, 30)
uploadAll, downloadAll, _ := s.trafficRepo.SumByUser(userID, 0)
return map[string]interface{}{
"storage_used": user.StorageUsed,
"storage_limit": user.StorageLimit,
"file_count": fileCount,
"total_size": totalSize,
"project_count": len(projects),
"projects": projectStats,
"traffic": map[string]interface{}{
"upload_30d": upload30,
"download_30d": download30,
"upload_total": uploadAll,
"download_total": downloadAll,
},
}, nil
}
// ProjectStats 项目存储统计
func (s *StatsService) ProjectStats(userID uint, projectID uint, isAdmin bool) (map[string]interface{}, error) {
p, err := s.projectRepo.FindByID(projectID)
if err != nil {
return nil, err
}
if !isAdmin && p.UserID != userID {
return nil, apperr.ErrForbidden
}
count, size, err := s.fileRepo.StatsByProject(projectID)
if err != nil {
return nil, err
}
var trashed int64
s.fileRepo.DB.Table("files").Where("project_id = ? AND status = 0", projectID).Count(&trashed)
return map[string]interface{}{
"project_id": p.ID,
"name": p.Name,
"file_count": count,
"storage_used": size,
"storage_limit": p.StorageLimit,
"trash_count": trashed,
}, nil
}
// TrafficStats 流量统计:按天聚合 + 汇总
func (s *StatsService) TrafficStats(userID uint, days int) (map[string]interface{}, error) {
if days <= 0 {
days = 30
}
daily, err := s.trafficRepo.DailyStats(userID, days)
if err != nil {
return nil, err
}
upload, download, _ := s.trafficRepo.SumByUser(userID, days)
return map[string]interface{}{
"days": days,
"daily": daily,
"upload": upload,
"download": download,
}, nil
}
+258
View File
@@ -0,0 +1,258 @@
package service
import (
"fmt"
"os"
"path/filepath"
"strings"
"gorm.io/gorm"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
"filestoragesystem/pkg/apperr"
)
// UserService 用户管理服务(个人资料 + 管理员用户管理)
type UserService struct {
cfg *config.Config
userRepo *repository.UserRepo
roleRepo *repository.RoleRepo
settingRepo *repository.SettingRepo
opLogRepo *repository.OpLogRepo
fileRepo *repository.FileRepo
projectRepo *repository.ProjectRepo
}
// GetProfile 获取个人资料
func (s *UserService) GetProfile(userID uint) (*model.User, error) {
u, err := s.userRepo.FindByID(userID)
if err != nil {
return nil, apperr.ErrNotFound
}
return u, nil
}
// UpdateProfile 更新个人资料(邮箱)
func (s *UserService) UpdateProfile(userID uint, email string) error {
email = strings.TrimSpace(strings.ToLower(email))
if email != "" && !utils.IsEmail(email) {
return fmt.Errorf("邮箱格式不正确")
}
if email != "" {
if other, err := s.userRepo.FindByEmail(email); err == nil && other.ID != userID {
return apperr.ErrUserExists
}
return s.userRepo.UpdateFields(userID, map[string]interface{}{"email": email})
}
return nil
}
// ChangePassword 修改自己的密码
func (s *UserService) ChangePassword(userID uint, oldPwd, newPwd string) error {
if !utils.IsValidPassword(newPwd) {
return fmt.Errorf("新密码至少8位且需包含字母和数字")
}
u, err := s.userRepo.FindByID(userID)
if err != nil {
return apperr.ErrNotFound
}
if !utils.CheckPassword(u.PasswordHash, oldPwd) {
return fmt.Errorf("原密码错误")
}
hash, err := utils.HashPassword(newPwd)
if err != nil {
return err
}
return s.userRepo.UpdateFields(userID, map[string]interface{}{"password_hash": hash})
}
// ---- 管理员操作 ----
// AdminListUsers 用户列表
func (s *UserService) AdminListUsers(keyword, roleID, status string, page, pageSize int) ([]model.User, int64, error) {
return s.userRepo.List(keyword, roleID, status, page, pageSize)
}
// AdminGetUser 用户详情
func (s *UserService) AdminGetUser(id uint) (*model.User, error) {
u, err := s.userRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
return u, nil
}
// AdminCreateUser 管理员创建用户
func (s *UserService) AdminCreateUser(username, email, password string, roleID uint, storageLimit int64) (*model.User, error) {
username = strings.TrimSpace(username)
email = strings.TrimSpace(strings.ToLower(email))
if len(username) < 3 {
return nil, fmt.Errorf("用户名至少3个字符")
}
if !utils.IsEmail(email) {
return nil, fmt.Errorf("邮箱格式不正确")
}
if !utils.IsValidPassword(password) {
return nil, fmt.Errorf("密码至少8位且需包含字母和数字")
}
if _, err := s.userRepo.FindByUsername(username); err == nil {
return nil, apperr.ErrUserExists
}
if _, err := s.userRepo.FindByEmail(email); err == nil {
return nil, apperr.ErrUserExists
}
role, err := s.roleRepo.FindByID(roleID)
if err != nil {
return nil, fmt.Errorf("角色不存在")
}
if storageLimit <= 0 {
storageLimit = 10737418240
if v, err := s.settingRepo.GetValue("default_storage_limit"); err == nil {
var n int64
if _, e := fmt.Sscanf(v, "%d", &n); e == nil && n > 0 {
storageLimit = n
}
}
}
hash, err := utils.HashPassword(password)
if err != nil {
return nil, err
}
u := &model.User{
Username: username, Email: email, PasswordHash: hash,
RoleID: role.ID, Status: 1, StorageLimit: storageLimit,
}
if err := s.userRepo.Create(u); err != nil {
return nil, err
}
utils.Info("user", "管理员创建用户: %s", username)
return u, nil
}
// AdminUpdateUser 编辑用户(邮箱/角色/配额)
func (s *UserService) AdminUpdateUser(id uint, email string, roleID uint, storageLimit int64) (*model.User, error) {
u, err := s.userRepo.FindByID(id)
if err != nil {
return nil, apperr.ErrNotFound
}
updates := map[string]interface{}{}
if email = strings.TrimSpace(strings.ToLower(email)); email != "" {
if !utils.IsEmail(email) {
return nil, fmt.Errorf("邮箱格式不正确")
}
if other, err := s.userRepo.FindByEmail(email); err == nil && other.ID != id {
return nil, apperr.ErrUserExists
}
updates["email"] = email
}
if roleID > 0 {
role, err := s.roleRepo.FindByID(roleID)
if err != nil {
return nil, fmt.Errorf("角色不存在")
}
// 防止移除最后一个超管
if u.Role != nil && u.Role.Code == "super_admin" && role.Code != "super_admin" {
if n, _ := s.roleRepo.CountUsersByRole(u.RoleID); n <= 1 {
return nil, fmt.Errorf("系统至少需要保留一个超级管理员")
}
}
updates["role_id"] = role.ID
}
if storageLimit > 0 {
updates["storage_limit"] = storageLimit
}
if len(updates) == 0 {
return u, nil
}
if err := s.userRepo.UpdateFields(id, updates); err != nil {
return nil, err
}
return s.userRepo.FindByID(id)
}
// AdminSetUserStatus 启用/禁用用户
func (s *UserService) AdminSetUserStatus(id uint, status int8) error {
u, err := s.userRepo.FindByID(id)
if err != nil {
return apperr.ErrNotFound
}
if u.Role != nil && u.Role.Code == "super_admin" && status == 0 {
if n, _ := s.roleRepo.CountUsersByRole(u.RoleID); n <= 1 {
return fmt.Errorf("不能禁用最后一个超级管理员")
}
}
return s.userRepo.UpdateFields(id, map[string]interface{}{"status": status})
}
// AdminAssignRole 修改用户角色
func (s *UserService) AdminAssignRole(id, roleID uint) error {
role, err := s.roleRepo.FindByID(roleID)
if err != nil {
return fmt.Errorf("角色不存在")
}
u, err := s.userRepo.FindByID(id)
if err != nil {
return apperr.ErrNotFound
}
if u.Role != nil && u.Role.Code == "super_admin" && role.Code != "super_admin" {
if n, _ := s.roleRepo.CountUsersByRole(u.RoleID); n <= 1 {
return fmt.Errorf("系统至少需要保留一个超级管理员")
}
}
return s.userRepo.UpdateFields(id, map[string]interface{}{"role_id": role.ID})
}
// AdminResetPassword 重置用户密码
func (s *UserService) AdminResetPassword(id uint, newPassword string) error {
if !utils.IsValidPassword(newPassword) {
return fmt.Errorf("密码至少8位且需包含字母和数字")
}
hash, err := utils.HashPassword(newPassword)
if err != nil {
return err
}
return s.userRepo.UpdateFields(id, map[string]interface{}{"password_hash": hash})
}
// AdminDeleteUser 删除用户及其全部数据
func (s *UserService) AdminDeleteUser(id uint) error {
u, err := s.userRepo.FindByID(id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return apperr.ErrNotFound
}
return err
}
if u.Role != nil && u.Role.Code == "super_admin" {
if n, _ := s.roleRepo.CountUsersByRole(u.RoleID); n <= 1 {
return fmt.Errorf("不能删除最后一个超级管理员")
}
}
// 删除物理文件
files, _ := s.fileRepo.ListByUser(id)
seen := make(map[string]bool)
for _, f := range files {
if f.StoredPath != "" && !seen[f.StoredPath] {
seen[f.StoredPath] = true
_ = os.Remove(filepath.Join(s.cfg.Storage.Root, f.StoredPath))
}
}
// 级联清理(文件/项目/密钥由外键无级联,手动删除)
_ = s.fileRepo.DB.Exec("DELETE FROM files WHERE user_id = ?", id).Error
_ = s.fileRepo.DB.Exec("DELETE FROM temp_links WHERE user_id = ?", id).Error
_ = s.fileRepo.DB.Exec("DELETE FROM projects WHERE user_id = ?", id).Error
_ = s.fileRepo.DB.Exec("DELETE FROM api_keys WHERE user_id = ?", id).Error
_ = s.fileRepo.DB.Exec("DELETE FROM webhooks WHERE user_id = ?", id).Error
_ = s.fileRepo.DB.Exec("DELETE FROM traffic_logs WHERE user_id = ?", id).Error
if err := s.userRepo.Delete(id); err != nil {
return err
}
utils.Info("user", "管理员删除用户: %s", u.Username)
return nil
}
+170
View File
@@ -0,0 +1,170 @@
package service
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/repository"
"filestoragesystem/internal/utils"
"filestoragesystem/pkg/apperr"
)
// WebhookService Webhook服务
type WebhookService struct {
cfg *config.Config
webhookRepo *repository.WebhookRepo
settingRepo *repository.SettingRepo
}
// Create 创建webhook
func (s *WebhookService) Create(userID uint, projectID *uint, url, secret, events string, status int8) (*model.Webhook, error) {
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return nil, fmt.Errorf("回调地址必须以http://或https://开头")
}
events = normalizeEvents(events)
if events == "" {
return nil, fmt.Errorf("至少订阅一个事件")
}
if projectID != nil && *projectID > 0 {
// 校验项目归属
var count int64
s.webhookRepo.DB.Model(&model.Project{}).Where("id = ? AND user_id = ?", *projectID, userID).Count(&count)
if count == 0 {
return nil, apperr.ErrForbidden
}
}
if secret == "" {
secret = utils.RandomKey(32)
}
if status == 0 {
status = 1
}
w := &model.Webhook{
UserID: userID, ProjectID: projectID, URL: url,
Secret: secret, Events: events, Status: status,
}
if err := s.webhookRepo.Create(w); err != nil {
return nil, err
}
return w, nil
}
// List 用户webhook列表
func (s *WebhookService) List(userID uint) ([]model.Webhook, error) {
return s.webhookRepo.ListByUser(userID)
}
// Update 更新webhook
func (s *WebhookService) Update(userID, id uint, url, events string, status int8) (*model.Webhook, error) {
w, err := s.webhookRepo.FindByID(id)
if err != nil || w.UserID != userID {
return nil, apperr.ErrNotFound
}
if url != "" {
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return nil, fmt.Errorf("回调地址必须以http://或https://开头")
}
w.URL = url
}
if events != "" {
events = normalizeEvents(events)
if events == "" {
return nil, fmt.Errorf("至少订阅一个事件")
}
w.Events = events
}
if status == 0 || status == 1 {
w.Status = status
}
if err := s.webhookRepo.Update(w); err != nil {
return nil, err
}
return w, nil
}
// Delete 删除webhook
func (s *WebhookService) Delete(userID, id uint) error {
if err := s.webhookRepo.Delete(id, userID); err != nil {
return apperr.ErrNotFound
}
return nil
}
func normalizeEvents(events string) string {
seen := make(map[string]bool)
parts := make([]string, 0, 4)
for _, e := range strings.Split(events, ",") {
e = strings.TrimSpace(e)
if e == "" || seen[e] {
continue
}
seen[e] = true
parts = append(parts, e)
}
return strings.Join(parts, ",")
}
func matchEvent(events, event string) bool {
for _, e := range strings.Split(events, ",") {
if strings.TrimSpace(e) == event {
return true
}
}
return false
}
// Dispatch 异步派发事件回调
func (s *WebhookService) Dispatch(userID, projectID uint, event string, payload map[string]interface{}) {
if v, err := s.settingRepo.GetValue("webhook_enabled"); err == nil && v != "true" {
return
}
hooks, err := s.webhookRepo.ListActive(userID, projectID)
if err != nil {
return
}
if len(hooks) == 0 {
return
}
body, _ := json.Marshal(map[string]interface{}{
"event": event,
"timestamp": time.Now().Unix(),
"data": payload,
})
for _, hook := range hooks {
if !matchEvent(hook.Events, event) {
continue
}
go s.deliver(hook, body)
}
}
func (s *WebhookService) deliver(hook model.Webhook, body []byte) {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodPost, hook.URL, bytes.NewReader(body))
if err != nil {
utils.Warn("webhook", "构建回调请求失败 url=%s err=%v", hook.URL, err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Webhook-Event", "filestoragesystem")
req.Header.Set("X-Webhook-Signature", utils.HMACSHA256(hook.Secret, string(body)))
resp, err := client.Do(req)
if err != nil {
utils.Warn("webhook", "回调发送失败 url=%s err=%v", hook.URL, err)
} else {
_ = resp.Body.Close()
if resp.StatusCode >= 300 {
utils.Warn("webhook", "回调响应异常 url=%s status=%d", hook.URL, resp.StatusCode)
}
}
_ = s.webhookRepo.Touch(hook.ID)
}
+142
View File
@@ -0,0 +1,142 @@
package utils
import (
"fmt"
"os"
"path/filepath"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gorm.io/gorm"
"filestoragesystem/internal/model"
)
var logger *zap.Logger
// Logger 获取全局日志对象
func Logger() *zap.Logger {
if logger == nil {
logger, _ = zap.NewProduction()
}
return logger
}
// InitLogger 初始化zap日志(控制台 + 可选文件)
func InitLogger(level, dir string) (*zap.Logger, error) {
lvl := zapcore.InfoLevel
_ = lvl.UnmarshalText([]byte(level))
encoderCfg := zap.NewProductionEncoderConfig()
encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
encoderCfg.EncodeLevel = zapcore.CapitalLevelEncoder
cores := make([]zapcore.Core, 0, 3)
// 控制台
consoleSyncer := zapcore.AddSync(os.Stdout)
consoleEncoder := zapcore.NewConsoleEncoder(encoderCfg)
cores = append(cores, zapcore.NewCore(consoleEncoder, consoleSyncer, lvl))
// 文件
if dir != "" {
if err := os.MkdirAll(dir, 0o755); err == nil {
fileSyncer := zapcore.AddSync(&dateRollFile{dir: dir})
fileEncoder := zapcore.NewJSONEncoder(encoderCfg)
cores = append(cores, zapcore.NewCore(fileEncoder, fileSyncer, lvl))
}
}
logger = zap.New(zapcore.NewTee(cores...), zap.AddCaller(), zap.AddCallerSkip(1))
return logger, nil
}
// dateRollFile 按天滚动的日志文件
type dateRollFile struct {
dir string
f *os.File
day string
}
func (d *dateRollFile) Write(p []byte) (int, error) {
today := time.Now().Format("2006-01-02")
if d.f == nil || d.day != today {
if d.f != nil {
_ = d.f.Close()
}
path := filepath.Join(d.dir, "app-"+today+".log")
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return 0, err
}
d.f, d.day = f, today
}
return d.f.Write(p)
}
func (d *dateRollFile) Sync() error {
if d.f != nil {
return d.f.Sync()
}
return nil
}
// ---- 系统日志异步落库 ----
var sysLogDB *gorm.DB
var sysLogCh chan model.SystemLog
// InitSystemLog 初始化系统日志异步写入器
func InitSystemLog(db *gorm.DB) {
sysLogDB = db
sysLogCh = make(chan model.SystemLog, 256)
go func() {
for entry := range sysLogCh {
if err := sysLogDB.Create(&entry).Error; err != nil {
Logger().Error("写入系统日志失败", zap.Error(err))
}
}
}()
}
// SystemLog 异步记录一条系统日志
func SystemLog(level, module, message string) {
if sysLogCh == nil {
return
}
select {
case sysLogCh <- model.SystemLog{Level: level, Module: module, Message: message, CreatedAt: time.Now()}:
default:
// 队列满则丢弃,避免阻塞业务
}
}
// SystemLogDetail 异步记录带详情的系统日志
func SystemLogDetail(level, module, message, requestID, ip, extra string) {
if sysLogCh == nil {
return
}
select {
case sysLogCh <- model.SystemLog{
Level: level, Module: module, Message: message,
RequestID: requestID, IP: ip, ExtraData: extra, CreatedAt: time.Now(),
}:
default:
}
}
// Info 记录info级别系统日志
func Info(module, format string, args ...interface{}) {
SystemLog("info", module, fmt.Sprintf(format, args...))
}
// Warn 记录warn级别系统日志
func Warn(module, format string, args ...interface{}) {
SystemLog("warn", module, fmt.Sprintf(format, args...))
}
// Error 记录error级别系统日志
func Error(module, format string, args ...interface{}) {
SystemLog("error", module, fmt.Sprintf(format, args...))
}
+133
View File
@@ -0,0 +1,133 @@
package utils
import (
"strings"
"github.com/gin-gonic/gin"
)
// ClientIP 获取客户端真实IP
func ClientIP(c *gin.Context) string {
// 优先取反向代理头
for _, header := range []string{"X-Real-IP", "X-Forwarded-For"} {
if v := c.GetHeader(header); v != "" {
ips := strings.Split(v, ",")
ip := strings.TrimSpace(ips[0])
if ip != "" {
return ip
}
}
}
return c.ClientIP()
}
// SanitizePath 清理虚拟路径,防止路径穿越
func SanitizePath(p string) string {
p = strings.ReplaceAll(p, "\\", "/")
parts := make([]string, 0, 8)
for _, seg := range strings.Split(p, "/") {
seg = strings.TrimSpace(seg)
if seg == "" || seg == "." {
continue
}
if seg == ".." {
if len(parts) > 0 {
parts = parts[:len(parts)-1]
}
continue
}
parts = append(parts, seg)
}
return strings.Join(parts, "/")
}
// IsValidPassword 检查密码强度:至少8位,包含字母和数字
func IsValidPassword(pwd string) bool {
if len(pwd) < 8 {
return false
}
hasLetter, hasDigit := false, false
for _, ch := range pwd {
switch {
case ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z':
hasLetter = true
case ch >= '0' && ch <= '9':
hasDigit = true
}
}
return hasLetter && hasDigit
}
// IsEmail 简单校验邮箱格式
func IsEmail(s string) bool {
at := strings.IndexByte(s, '@')
if at <= 0 || at == len(s)-1 {
return false
}
domain := s[at+1:]
return strings.Contains(domain, ".") && strings.IndexByte(domain, '.') != 0 && strings.IndexByte(domain, '.') != len(domain)-1
}
// HumanSize 将字节数转为可读大小
func HumanSize(n int64) string {
const unit = 1024
if n < unit {
return itoa(n) + " B"
}
div, exp := int64(unit), 0
for m := n / unit; m >= unit && exp < 4; m /= unit {
div *= unit
exp++
}
val := float64(n) / float64(div)
s := trimFloat(val)
units := []string{"KB", "MB", "GB", "TB", "PB"}
return s + " " + units[exp]
}
func itoa(n int64) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var buf [20]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}
func trimFloat(f float64) string {
// 保留两位小数并去掉末尾多余的0
s := itoa(int64(f))
frac := f - float64(int64(f))
if frac < 0.005 {
return s
}
d1 := int64(frac * 10)
if d1 > 9 {
d1 = 9
}
frac = frac*10 - float64(d1)
d2 := int64(frac * 10)
if d2 > 9 {
d2 = 9
}
if d2 < 5 {
if d1 == 0 {
return s
}
return s + "." + itoa(d1)
}
return s + "." + itoa(d1) + itoa(d2)
}
+96
View File
@@ -0,0 +1,96 @@
package utils
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// 统一响应码
const (
CodeSuccess = 0
CodeError = 1
CodeParamError = 400
CodeUnauthorized = 401
CodeForbidden = 403
CodeNotFound = 404
CodeServerErr = 500
)
// Response 统一响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// PageData 分页响应结构
type PageData struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// OK 成功响应
func OK(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{Code: CodeSuccess, Message: "success", Data: data})
}
// OKMsg 成功响应(仅消息)
func OKMsg(c *gin.Context, msg string) {
c.JSON(http.StatusOK, Response{Code: CodeSuccess, Message: msg})
}
// OKPage 分页成功响应
func OKPage(c *gin.Context, list interface{}, total int64, page, pageSize int) {
c.JSON(http.StatusOK, Response{
Code: CodeSuccess,
Message: "success",
Data: PageData{List: list, Total: total, Page: page, PageSize: pageSize},
})
}
// Fail 失败响应
func Fail(c *gin.Context, httpCode, code int, msg string) {
c.JSON(httpCode, Response{Code: code, Message: msg})
}
// FailMsg 默认400失败响应
func FailMsg(c *gin.Context, msg string) {
Fail(c, http.StatusBadRequest, CodeError, msg)
}
// Unauthorized 401
func Unauthorized(c *gin.Context, msg string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeUnauthorized, Message: msg})
}
// Forbidden 403
func Forbidden(c *gin.Context, msg string) {
c.AbortWithStatusJSON(http.StatusForbidden, Response{Code: CodeForbidden, Message: msg})
}
// NotFound 404
func NotFound(c *gin.Context, msg string) {
c.JSON(http.StatusNotFound, Response{Code: CodeNotFound, Message: msg})
}
// ServerError 500
func ServerError(c *gin.Context, msg string) {
c.JSON(http.StatusInternalServerError, Response{Code: CodeServerErr, Message: msg})
}
// ParsePage 解析分页参数
func ParsePage(c *gin.Context) (page, pageSize int) {
page = 1
pageSize = 20
if v, err := strconv.Atoi(c.Query("page")); err == nil && v > 0 {
page = v
}
if v, err := strconv.Atoi(c.Query("page_size")); err == nil && v > 0 && v <= 100 {
pageSize = v
}
return
}
+100
View File
@@ -0,0 +1,100 @@
package utils
import (
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
var jwtSecret = "please-change-this-secret-key"
var jwtExpire = 7 * time.Hour
// InitJWT 初始化JWT参数
func InitJWT(secret, expire string) {
if secret != "" {
jwtSecret = secret
}
if d, err := time.ParseDuration(expire); err == nil && d > 0 {
jwtExpire = d
}
}
// JWTClaims 自定义Claims
type JWTClaims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
RoleCode string `json:"role_code"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT Token
func GenerateToken(userID uint, username, roleCode string) (string, error) {
claims := JWTClaims{
UserID: userID,
Username: username,
RoleCode: roleCode,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpire)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "filestoragesystem",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
}
// ParseToken 解析并校验JWT Token
func ParseToken(tokenStr string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &JWTClaims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("非法的签名算法")
}
return []byte(jwtSecret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("无效的Token")
}
// HashPassword bcrypt加密密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPassword 校验密码
func CheckPassword(hash, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
// MD5 计算字节串的MD5
func MD5(data []byte) string {
h := md5.New()
h.Write(data)
return hex.EncodeToString(h.Sum(nil))
}
// RandomKey 生成指定长度的随机hex字符串
func RandomKey(n int) string {
b := make([]byte, (n+1)/2)
_, _ = rand.Read(b)
return hex.EncodeToString(b)[:n]
}
// HMACSHA256 计算HMAC-SHA256签名(hex编码)
func HMACSHA256(secret, message string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
return hex.EncodeToString(mac.Sum(nil))
}
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/glebarez/sqlite"
"go.uber.org/zap"
"gorm.io/gorm"
"filestoragesystem/internal/config"
"filestoragesystem/internal/model"
"filestoragesystem/internal/router"
"filestoragesystem/internal/service"
"filestoragesystem/internal/utils"
)
func main() {
// 1. 加载配置
cfg, err := config.Load("config.yaml")
if err != nil {
fmt.Println("加载配置失败:", err)
os.Exit(1)
}
if err := cfg.EnsureDirs(); err != nil {
fmt.Println("初始化目录失败:", err)
os.Exit(1)
}
// 2. 初始化日志
log, err := utils.InitLogger(cfg.Log.Level, cfg.Log.Dir)
if err != nil {
fmt.Println("初始化日志失败:", err)
os.Exit(1)
}
defer func() { _ = log.Sync() }()
zap.ReplaceGlobals(log)
// 3. 连接数据库
if cfg.Database.Driver != "sqlite" {
log.Warn("当前仅支持sqlite驱动,将按sqlite处理", zap.String("driver", cfg.Database.Driver))
}
db, err := gorm.Open(sqlite.Open(cfg.Database.Source), &gorm.Config{})
if err != nil {
log.Fatal("连接数据库失败", zap.Error(err))
}
sqlDB, err := db.DB()
if err == nil {
sqlDB.SetMaxOpenConns(10)
}
// 4. 迁移与种子数据
if err := model.AutoMigrate(db); err != nil {
log.Fatal("数据库迁移失败", zap.Error(err))
}
if err := model.SeedData(db); err != nil {
log.Fatal("初始化种子数据失败", zap.Error(err))
}
// 5. 系统日志异步落库
utils.InitSystemLog(db)
// 6. JWT参数
utils.InitJWT(cfg.JWT.Secret, cfg.JWT.Expire)
// 7. 服务与路由
svc := service.NewServices(cfg, db)
// 8. 引导管理员账户
if err := svc.Auth.BootstrapAdmin(); err != nil {
log.Error("初始化管理员账户失败", zap.Error(err))
}
// 9. 启动HTTP服务
r := router.New(cfg, db, svc)
addr := fmt.Sprintf(":%s", cfg.Server.Port)
absRoot, _ := filepath.Abs(cfg.Storage.Root)
log.Info("FileStorageSystem 启动",
zap.String("addr", addr),
zap.String("mode", cfg.Server.Mode),
zap.String("domain", cfg.Server.Domain),
zap.String("storage", absRoot),
)
if err := r.Run(addr); err != nil {
log.Fatal("服务启动失败", zap.Error(err))
}
}
+27
View File
@@ -0,0 +1,27 @@
package apperr
import "errors"
// 通用业务错误
var (
ErrUnauthorized = errors.New("未认证或认证已过期")
ErrForbidden = errors.New("没有操作权限")
ErrNotFound = errors.New("资源不存在")
ErrParamInvalid = errors.New("参数错误")
ErrUserExists = errors.New("用户名或邮箱已存在")
ErrUserDisabled = errors.New("账户已被禁用")
ErrPasswordWrong = errors.New("用户名或密码错误")
ErrAccountLocked = errors.New("失败次数过多,账户已临时锁定")
ErrRegisterClosed = errors.New("当前未开放注册")
ErrProjectExists = errors.New("同名项目已存在")
ErrQuotaExceeded = errors.New("存储空间不足,超出配额限制")
ErrFileTooLarge = errors.New("文件大小超出限制")
ErrFileTypeDenied = errors.New("文件类型不被允许")
ErrFileDeleted = errors.New("文件已在回收站中")
ErrLinkExpired = errors.New("链接已过期或失效")
ErrLinkPassword = errors.New("访问密码错误")
ErrSignInvalid = errors.New("API签名校验失败")
ErrSignExpired = errors.New("API签名时间戳已过期")
ErrSystemRole = errors.New("系统内置角色不允许此操作")
ErrMaintainMode = errors.New("站点维护中")
)
Binary file not shown.
+89
View File
@@ -0,0 +1,89 @@
{"level":"INFO","ts":"2026-08-23T00:08:16.874+0800","caller":"runtime/proc.go:285","msg":"FileStorageSystem 启动","addr":":8080","mode":"debug","storage":"E:\\Demo\\Go\\filestoragesystem\\go\\storage\\files"}
{"level":"INFO","ts":"2026-08-23T00:08:48.475+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"503.8µs","request_id":"0a3a9bb5"}
{"level":"INFO","ts":"2026-08-23T00:08:48.715+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/","ip":"127.0.0.1","latency":"76.1845ms","request_id":"ac79f6b9"}
{"level":"INFO","ts":"2026-08-23T00:08:56.736+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"POST","path":"/api/v1/auth/login","ip":"127.0.0.1","latency":"249.5619ms","request_id":"aa4c5097"}
{"level":"INFO","ts":"2026-08-23T00:08:56.742+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Layout-C5a0sB21.js","ip":"127.0.0.1","latency":"0s","request_id":"9289e0e1"}
{"level":"INFO","ts":"2026-08-23T00:08:56.743+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Layout-Btlzh6Ru.css","ip":"127.0.0.1","latency":"0s","request_id":"04f4b068"}
{"level":"INFO","ts":"2026-08-23T00:08:56.743+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/format-D7FCopCD.js","ip":"127.0.0.1","latency":"0s","request_id":"0ff50408"}
{"level":"INFO","ts":"2026-08-23T00:08:56.743+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Dashboard-BqiLLE7e.css","ip":"127.0.0.1","latency":"0s","request_id":"b386efdb"}
{"level":"INFO","ts":"2026-08-23T00:08:56.743+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Dashboard-lW8DgIjb.js","ip":"127.0.0.1","latency":"576.6µs","request_id":"544a298f"}
{"level":"INFO","ts":"2026-08-23T00:09:04.615+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/","ip":"127.0.0.1","latency":"0s","request_id":"6a9f5d3c"}
{"level":"INFO","ts":"2026-08-23T00:09:04.625+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/rolldown-runtime-hePW80VL.js","ip":"127.0.0.1","latency":"0s","request_id":"cd197ae7"}
{"level":"INFO","ts":"2026-08-23T00:09:04.626+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/user-r2t_U4TE.js","ip":"127.0.0.1","latency":"503.1µs","request_id":"160e146a"}
{"level":"INFO","ts":"2026-08-23T00:09:04.629+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/index-BblkT4KS.css","ip":"127.0.0.1","latency":"2.9257ms","request_id":"d3a10523"}
{"level":"INFO","ts":"2026-08-23T00:09:04.629+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/message-3Cyqmk3W.js","ip":"127.0.0.1","latency":"2.9257ms","request_id":"b5f830c2"}
{"level":"INFO","ts":"2026-08-23T00:09:04.634+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/index-BpBRyeZq.js","ip":"127.0.0.1","latency":"8.4495ms","request_id":"5b738cd0"}
{"level":"INFO","ts":"2026-08-23T00:09:04.680+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Layout-eCLSt2ht.css","ip":"127.0.0.1","latency":"0s","request_id":"05cbfd3d"}
{"level":"INFO","ts":"2026-08-23T00:09:04.680+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Layout-DoUztMxi.js","ip":"127.0.0.1","latency":"0s","request_id":"c40b1000"}
{"level":"INFO","ts":"2026-08-23T00:09:04.681+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Dashboard-BDi2x5O-.js","ip":"127.0.0.1","latency":"0s","request_id":"284dd8dc"}
{"level":"INFO","ts":"2026-08-23T00:09:04.681+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":304,"method":"GET","path":"/assets/format-D7FCopCD.js","ip":"127.0.0.1","latency":"0s","request_id":"b3637089"}
{"level":"INFO","ts":"2026-08-23T00:09:04.681+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/api-BkpNvY-0.js","ip":"127.0.0.1","latency":"0s","request_id":"f7a4a0bb"}
{"level":"INFO","ts":"2026-08-23T00:09:04.683+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":304,"method":"GET","path":"/assets/Dashboard-BqiLLE7e.css","ip":"127.0.0.1","latency":"0s","request_id":"1d9c49a3"}
{"level":"INFO","ts":"2026-08-23T00:09:04.753+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/traffic","ip":"127.0.0.1","latency":"1.5555ms","request_id":"df63fce4","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:09:04.754+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/user","ip":"127.0.0.1","latency":"2.5812ms","request_id":"293f810c","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:09:11.050+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"503.2µs","request_id":"39b12f84"}
{"level":"INFO","ts":"2026-08-23T00:09:16.606+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Settings-BIKcoGkF.css","ip":"127.0.0.1","latency":"0s","request_id":"94bba64e"}
{"level":"INFO","ts":"2026-08-23T00:09:16.606+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Settings-BRuY3Ydx.js","ip":"127.0.0.1","latency":"0s","request_id":"9369b4fc"}
{"level":"INFO","ts":"2026-08-23T00:09:16.627+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/settings","ip":"127.0.0.1","latency":"1.0135ms","request_id":"8d967e74","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:09:55.126+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Profile-Ck4Ovzla.css","ip":"127.0.0.1","latency":"0s","request_id":"66c6d0ce"}
{"level":"INFO","ts":"2026-08-23T00:09:55.126+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Profile-ZM-swGtq.js","ip":"127.0.0.1","latency":"0s","request_id":"3cd007c4"}
{"level":"INFO","ts":"2026-08-23T00:09:55.166+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/user/profile","ip":"127.0.0.1","latency":"1.0271ms","request_id":"2fc1945c","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:09:55.176+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/webhooks","ip":"127.0.0.1","latency":"503.4µs","request_id":"3eb1a053","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:09:55.177+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/projects","ip":"127.0.0.1","latency":"1.0234ms","request_id":"f867d686","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:09:55.178+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/auth/apikeys","ip":"127.0.0.1","latency":"2.5291ms","request_id":"3b10db1e","user_id":1}
{"level":"WARN","ts":"2026-08-23T00:10:06.221+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":400,"method":"PUT","path":"/api/v1/user/password","ip":"127.0.0.1","latency":"94.568ms","request_id":"77fd93a9","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:14.700+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"PUT","path":"/api/v1/user/password","ip":"127.0.0.1","latency":"260.8752ms","request_id":"fefb557f","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:14.704+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Login-CSTMQy1g.js","ip":"127.0.0.1","latency":"0s","request_id":"53e83a7f"}
{"level":"INFO","ts":"2026-08-23T00:10:14.704+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Login-CYddjswm.css","ip":"127.0.0.1","latency":"0s","request_id":"6871994d"}
{"level":"INFO","ts":"2026-08-23T00:10:14.722+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"502.4µs","request_id":"6ee21e66"}
{"level":"INFO","ts":"2026-08-23T00:10:22.157+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"POST","path":"/api/v1/auth/login","ip":"127.0.0.1","latency":"230.735ms","request_id":"c7851d1a"}
{"level":"INFO","ts":"2026-08-23T00:10:22.181+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/traffic","ip":"127.0.0.1","latency":"1.0133ms","request_id":"07d00621","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:22.183+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/user","ip":"127.0.0.1","latency":"2.7351ms","request_id":"7fe200c1","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:31.298+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/settings","ip":"127.0.0.1","latency":"503.7µs","request_id":"342cfc85","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:33.455+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Logs-BkPCLqi2.js","ip":"127.0.0.1","latency":"502.8µs","request_id":"72dbd460"}
{"level":"INFO","ts":"2026-08-23T00:10:33.503+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/logs/system","ip":"127.0.0.1","latency":"1.009ms","request_id":"db28fc57","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:33.504+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/logs/operation","ip":"127.0.0.1","latency":"2.0533ms","request_id":"923640d5","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:35.774+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Backups-Xd3Jdlbe.css","ip":"127.0.0.1","latency":"0s","request_id":"491d7bb8"}
{"level":"INFO","ts":"2026-08-23T00:10:35.774+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Backups-CJ9KxCza.js","ip":"127.0.0.1","latency":"0s","request_id":"24715045"}
{"level":"INFO","ts":"2026-08-23T00:10:35.797+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/backups","ip":"127.0.0.1","latency":"1.0793ms","request_id":"d5da8422","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:38.590+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Files-Cdgr0mkT.css","ip":"127.0.0.1","latency":"0s","request_id":"385d9f99"}
{"level":"INFO","ts":"2026-08-23T00:10:38.590+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Files-PDZGx5FS.js","ip":"127.0.0.1","latency":"0s","request_id":"efa1f33b"}
{"level":"INFO","ts":"2026-08-23T00:10:38.614+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/projects","ip":"127.0.0.1","latency":"527.6µs","request_id":"3a81d484","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:40.726+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Projects-pItre2FN.js","ip":"127.0.0.1","latency":"0s","request_id":"6f5c0d8f"}
{"level":"INFO","ts":"2026-08-23T00:10:40.738+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/projects","ip":"127.0.0.1","latency":"1.0336ms","request_id":"d1230e50","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:41.777+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/user","ip":"127.0.0.1","latency":"1.2307ms","request_id":"3d238d22","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:41.778+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/traffic","ip":"127.0.0.1","latency":"1.0325ms","request_id":"9d09b885","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:44.417+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/user/profile","ip":"127.0.0.1","latency":"1.0173ms","request_id":"11f1e1a7","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:44.424+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/auth/apikeys","ip":"127.0.0.1","latency":"576.6µs","request_id":"ab83f292","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:44.425+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/webhooks","ip":"127.0.0.1","latency":"1.6235ms","request_id":"17b9dfde","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:44.427+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/projects","ip":"127.0.0.1","latency":"3.19ms","request_id":"ea441b29","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:45.886+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Users-B8KtO8D_.js","ip":"127.0.0.1","latency":"0s","request_id":"8b1a9e89"}
{"level":"INFO","ts":"2026-08-23T00:10:45.908+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/users","ip":"127.0.0.1","latency":"503.3µs","request_id":"9ecfa71c","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:45.909+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/roles","ip":"127.0.0.1","latency":"2.0553ms","request_id":"c328cc81","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:46.318+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Roles-Bf3nS16n.js","ip":"127.0.0.1","latency":"0s","request_id":"61d8699e"}
{"level":"INFO","ts":"2026-08-23T00:10:46.318+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/assets/Roles-CmYeutUg.css","ip":"127.0.0.1","latency":"0s","request_id":"c035ba4a"}
{"level":"INFO","ts":"2026-08-23T00:10:46.331+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/roles","ip":"127.0.0.1","latency":"1.2849ms","request_id":"55d8686f","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:10:46.332+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/permissions","ip":"127.0.0.1","latency":"2.3374ms","request_id":"69b75799","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:11:00.741+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"PUT","path":"/api/v1/admin/roles/2","ip":"127.0.0.1","latency":"277.8926ms","request_id":"1b2b28f0","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:11:00.748+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/roles","ip":"127.0.0.1","latency":"1.0149ms","request_id":"5beb030f","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:11:15.091+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/admin/settings","ip":"127.0.0.1","latency":"602.5µs","request_id":"534f4f9b","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:12:11.612+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"1.0572ms","request_id":"56aab71e"}
{"level":"INFO","ts":"2026-08-23T00:12:12.806+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"1.1809ms","request_id":"1a46d2e2"}
{"level":"INFO","ts":"2026-08-23T00:12:13.878+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"513.9µs","request_id":"d710d19b"}
{"level":"INFO","ts":"2026-08-23T00:12:16.962+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"661.1µs","request_id":"b79760fe"}
{"level":"INFO","ts":"2026-08-23T00:12:24.991+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/user/profile","ip":"127.0.0.1","latency":"1.0185ms","request_id":"fbcc9872","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:12:24.998+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/auth/apikeys","ip":"127.0.0.1","latency":"1.0175ms","request_id":"bda576fc","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:12:24.998+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/webhooks","ip":"127.0.0.1","latency":"1.5251ms","request_id":"d7c971b9","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:12:25.001+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/projects","ip":"127.0.0.1","latency":"3.6629ms","request_id":"7827beec","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:12:44.648+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"POST","path":"/api/v1/auth/apikeys","ip":"127.0.0.1","latency":"305.4539ms","request_id":"fb375879","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:12:44.657+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/auth/apikeys","ip":"127.0.0.1","latency":"592.9µs","request_id":"ec73f901","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:16:48.332+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"1.0228ms","request_id":"ead573e6"}
{"level":"INFO","ts":"2026-08-23T00:20:01.015+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"127.0.0.1","latency":"1.0216ms","request_id":"f37b65ae"}
{"level":"INFO","ts":"2026-08-23T00:22:48.129+0800","caller":"runtime/proc.go:285","msg":"FileStorageSystem 启动","addr":":8080","mode":"debug","storage":"E:\\Demo\\Go\\filestoragesystem\\go\\storage\\files"}
{"level":"INFO","ts":"2026-08-23T00:23:29.028+0800","caller":"runtime/proc.go:285","msg":"FileStorageSystem 启动","addr":":8100","mode":"debug","storage":"E:\\Demo\\Go\\filestoragesystem\\go\\storage\\files"}
{"level":"INFO","ts":"2026-08-23T00:30:51.991+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/status","ip":"::1","latency":"1.0179ms","request_id":"55f0ce61"}
{"level":"WARN","ts":"2026-08-23T00:30:53.369+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":401,"method":"POST","path":"/api/v1/auth/login","ip":"::1","latency":"119.3334ms","request_id":"2519f5dc"}
{"level":"WARN","ts":"2026-08-23T00:31:01.711+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":401,"method":"POST","path":"/api/v1/auth/login","ip":"::1","latency":"93.7763ms","request_id":"15cf2093"}
{"level":"INFO","ts":"2026-08-23T00:31:06.530+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"POST","path":"/api/v1/auth/login","ip":"::1","latency":"257.1467ms","request_id":"87b574dc"}
{"level":"INFO","ts":"2026-08-23T00:31:06.669+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/traffic","ip":"::1","latency":"1.0274ms","request_id":"af7e5a49","user_id":1}
{"level":"INFO","ts":"2026-08-23T00:31:06.670+0800","caller":"gin@v1.10.0/context.go:185","msg":"access","status":200,"method":"GET","path":"/api/v1/stats/user","ip":"::1","latency":"2.627ms","request_id":"ed0cb391","user_id":1}