更新前后端代码
This commit is contained in:
@@ -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
Binary file not shown.
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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, "密钥已删除")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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, "项目已删除")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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, "密码已修改")
|
||||
}
|
||||
@@ -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已删除")
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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...))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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}
|
||||
Reference in New Issue
Block a user