diff --git a/configs/config.yaml b/configs/config.yaml index 9dc736e..90540f8 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -11,73 +11,73 @@ server: # jwt configuration jwt: - signing-key: kratos-admin - expires-time: 7d - buffer-time: 1d + signing_key: kratos-admin + expires_time: 7d + buffer_time: 1d issuer: kratos-admin # system configuration system: env: local - db-type: mysql - oss-type: local - use-redis: false - use-multipoint: false - iplimit-count: 15000 - iplimit-time: 3600 - router-prefix: "" - use-strict-auth: false + db_type: mysql + oss_type: local + use_redis: false + use_multipoint: false + iplimit_count: 15000 + iplimit_time: 3600 + router_prefix: "" + use_strict_auth: false # captcha configuration captcha: - key-long: 6 - img-width: 240 - img-height: 80 - open-captcha: 0 - open-captcha-timeout: 3600 + key_long: 6 + img_width: 240 + img_height: 80 + open_captcha: 0 + open_captcha_timeout: 3600 # mysql configuration mysql: path: 127.0.0.1 port: "3306" config: charset=utf8mb4&parseTime=True&loc=Local - db-name: kratos_admin + db_name: spa username: root - password: root - max-idle-conns: 10 - max-open-conns: 100 - log-mode: info - log-zap: false + password: Xu950329. + max_idle_conns: 10 + max_open_conns: 100 + log_mode: info + log_zap: false # pgsql configuration pgsql: path: "" port: "" config: "" - db-name: "" + db_name: "" username: "" password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false + max_idle_conns: 10 + max_open_conns: 100 + log_mode: "" + log_zap: false # sqlite configuration sqlite: path: "" - db-name: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false + db_name: "" + max_idle_conns: 10 + max_open_conns: 100 + log_mode: "" + log_zap: false # redis configuration redis: - use-cluster: false + use_cluster: false addr: 127.0.0.1:6379 password: "" db: 0 - cluster-addrs: + cluster_addrs: - "127.0.0.1:7000" - "127.0.0.1:7001" - "127.0.0.1:7002" @@ -85,74 +85,74 @@ redis: # local storage configuration local: path: uploads/file - store-path: uploads/file + store_path: uploads/file # qiniu configuration qiniu: zone: ZoneHuaDong bucket: "" - img-path: "" - use-https: false - access-key: "" - secret-key: "" - use-cdn-domains: false + img_path: "" + use_https: false + access_key: "" + secret_key: "" + use_cdn_domains: false # minio configuration minio: endpoint: "" - access-key-id: "" - access-key-secret: "" - bucket-name: "" - use-ssl: false - base-path: "" - bucket-url: "" + access_key_id: "" + access_key_secret: "" + bucket_name: "" + use_ssl: false + base_path: "" + bucket_url: "" # aliyun oss configuration -aliyun-oss: +aliyun_oss: endpoint: "" - access-key-id: "" - access-key-secret: "" - bucket-name: "" - bucket-url: "" - base-path: "" + access_key_id: "" + access_key_secret: "" + bucket_name: "" + bucket_url: "" + base_path: "" # tencent cos configuration -tencent-cos: +tencent_cos: bucket: "" region: "" - secret-id: "" - secret-key: "" - base-url: "" - path-prefix: "" + secret_id: "" + secret_key: "" + base_url: "" + path_prefix: "" # aws s3 configuration -aws-s3: +aws_s3: bucket: "" region: "" endpoint: "" - s3-force-path-style: false - disable-ssl: false - secret-id: "" - secret-key: "" - base-url: "" - path-prefix: "" + s3_force_path_style: false + disable_ssl: false + secret_id: "" + secret_key: "" + base_url: "" + path_prefix: "" # cloudflare r2 configuration -cloudflare-r2: +cloudflare_r2: bucket: "" - base-url: "" + base_url: "" path: uploads - account-id: "" - access-key-id: "" - secret-access-key: "" + account_id: "" + access_key_id: "" + secret_access_key: "" # huawei obs configuration -huawei-obs: +huawei_obs: path: "" bucket: "" endpoint: "" - access-key: "" - secret-key: "" + access_key: "" + secret_key: "" # email configuration email: @@ -160,7 +160,7 @@ email: port: 465 from: "" host: "" - is-ssl: true + is_ssl: true secret: "" nickname: "" @@ -172,11 +172,11 @@ excel: cors: mode: allow-all whitelist: - - allow-origin: "*" - allow-headers: Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,X-Token,X-User-Id - allow-methods: POST,GET,OPTIONS,DELETE,PUT - expose-headers: Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Content-Type - allow-credentials: true + - allow_origin: "*" + allow_headers: Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,X-Token,X-User-Id + allow_methods: POST,GET,OPTIONS,DELETE,PUT + expose_headers: Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Content-Type + allow_credentials: true # zap logger configuration zap: @@ -184,8 +184,8 @@ zap: format: console prefix: "[kratos-admin]" director: log - show-line: true - encode-level: LowercaseColorLevelEncoder - stacktrace-key: stacktrace - log-in-console: true - retention-day: -1 + show_line: true + encode_level: LowercaseColorLevelEncoder + stacktrace_key: stacktrace + log_in_console: true + retention_day: -1 diff --git a/go.mod b/go.mod index d24084f..efd7269 100644 --- a/go.mod +++ b/go.mod @@ -23,17 +23,14 @@ require ( github.com/qiniu/go-sdk/v7 v7.25.2 github.com/redis/go-redis/v9 v9.7.0 github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v3 v3.24.5 github.com/tencentyun/cos-go-sdk-v5 v0.7.60 github.com/xuri/excelize/v2 v2.9.0 go.einride.tech/aip v0.78.0 go.uber.org/automaxprocs v1.6.0 golang.org/x/crypto v0.38.0 - google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 - google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.10 gorm.io/driver/mysql v1.5.7 - gorm.io/driver/postgres v1.5.11 - gorm.io/driver/sqlite v1.6.0 gorm.io/gen v0.3.27 gorm.io/gorm v1.30.0 gorm.io/plugin/dbresolver v1.6.2 @@ -73,6 +70,7 @@ require ( github.com/go-kratos/aegis v0.2.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/form/v4 v4.2.0 // indirect github.com/goccy/go-json v0.10.4 // indirect github.com/gofrs/flock v0.8.1 // indirect @@ -89,19 +87,24 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/microsoft/go-mssqldb v1.6.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mozillazg/go-httpheader v0.2.1 // indirect + github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect @@ -114,9 +117,13 @@ require ( golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.30.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + google.golang.org/grpc v1.74.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/datatypes v1.2.4 // indirect + gorm.io/driver/postgres v1.5.11 // indirect + gorm.io/driver/sqlite v1.6.0 // indirect gorm.io/driver/sqlserver v1.5.3 // indirect gorm.io/hints v1.1.0 // indirect modernc.org/fileutil v1.0.0 // indirect diff --git a/go.sum b/go.sum index 73681aa..3c3ab46 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/form/v4 v4.2.0 h1:N1wh+Goz61e6w66vo8vJkQt+uwZSoLz50kZPJWR8eic= @@ -153,6 +155,7 @@ github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -216,6 +219,8 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= +github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -245,6 +250,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= @@ -269,6 +276,12 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 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= @@ -287,6 +300,10 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72NrI6G/Tv0o= github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= @@ -294,6 +311,8 @@ github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmji github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.einride.tech/aip v0.78.0 h1:nbnM/OuGt5Pyz/r2KOxB6Hp+ey2e0+MNnfIPBtY45pY= go.einride.tech/aip v0.78.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -362,8 +381,10 @@ golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/biz/biz.go b/internal/biz/biz.go index df9cd3b..c2dd76e 100644 --- a/internal/biz/biz.go +++ b/internal/biz/biz.go @@ -7,4 +7,14 @@ import ( ) // ProviderSet is biz providers. -var ProviderSet = wire.NewSet(system.NewUserUsecase, system.NewApiUsecase) +var ProviderSet = wire.NewSet( + system.NewUserUsecase, + system.NewApiUsecase, + system.NewAuthorityUsecase, + system.NewAuthorityBtnUsecase, + system.NewMenuUsecase, + system.NewDictionaryUsecase, + system.NewJwtBlacklistUsecase, + system.NewOperationRecordUsecase, + system.NewParamsUsecase, +) diff --git a/internal/biz/system/api.go b/internal/biz/system/api.go index de1ade0..7b6ad5a 100644 --- a/internal/biz/system/api.go +++ b/internal/biz/system/api.go @@ -4,6 +4,8 @@ import ( "context" "errors" "strings" + + "kra/internal/conf" ) // Api API实体 @@ -71,12 +73,12 @@ type RouterInfo struct { } // NewApiUsecase 创建API用例 -func NewApiUsecase(repo ApiRepo, casbinRepo CasbinRepo, authRepo AuthorityRepo, useStrictAuth bool) *ApiUsecase { +func NewApiUsecase(repo ApiRepo, casbinRepo CasbinRepo, authRepo AuthorityRepo, useStrictAuth conf.UseStrictAuth) *ApiUsecase { return &ApiUsecase{ repo: repo, casbinRepo: casbinRepo, authRepo: authRepo, - useStrictAuth: useStrictAuth, + useStrictAuth: bool(useStrictAuth), routers: make([]RouterInfo, 0), } } diff --git a/internal/biz/system/authority.go b/internal/biz/system/authority.go index a3d22e9..bd3e7be 100644 --- a/internal/biz/system/authority.go +++ b/internal/biz/system/authority.go @@ -4,6 +4,8 @@ import ( "context" "errors" "strconv" + + "kra/internal/conf" ) var ( @@ -84,11 +86,11 @@ type AuthorityUsecase struct { } // NewAuthorityUsecase 创建角色用例 -func NewAuthorityUsecase(repo AuthorityRepo, casbinRepo CasbinRepo, useStrictAuth bool) *AuthorityUsecase { +func NewAuthorityUsecase(repo AuthorityRepo, casbinRepo CasbinRepo, useStrictAuth conf.UseStrictAuth) *AuthorityUsecase { return &AuthorityUsecase{ repo: repo, casbinRepo: casbinRepo, - useStrictAuth: useStrictAuth, + useStrictAuth: bool(useStrictAuth), } } diff --git a/internal/biz/system/menu.go b/internal/biz/system/menu.go index 5fa9487..e76c11a 100644 --- a/internal/biz/system/menu.go +++ b/internal/biz/system/menu.go @@ -4,6 +4,8 @@ import ( "context" "errors" "strconv" + + "kra/internal/conf" ) // BaseMenu 基础菜单实体 @@ -84,11 +86,11 @@ type MenuUsecase struct { } // NewMenuUsecase 创建菜单用例 -func NewMenuUsecase(repo MenuRepo, authRepo AuthorityRepo, useStrictAuth bool) *MenuUsecase { +func NewMenuUsecase(repo MenuRepo, authRepo AuthorityRepo, useStrictAuth conf.UseStrictAuth) *MenuUsecase { return &MenuUsecase{ repo: repo, authRepo: authRepo, - useStrictAuth: useStrictAuth, + useStrictAuth: bool(useStrictAuth), } } diff --git a/internal/biz/system/params.go b/internal/biz/system/params.go new file mode 100644 index 0000000..e9fe5ae --- /dev/null +++ b/internal/biz/system/params.go @@ -0,0 +1,73 @@ +package system + +import ( + "context" + "time" +) + +// Params 系统参数实体 +type Params struct { + ID uint + CreatedAt time.Time + UpdatedAt time.Time + Name string // 参数名 + Key string // 参数键 + Value string // 参数值 + Desc string // 参数描述 +} + +// ParamsRepo 系统参数仓储接口 +type ParamsRepo interface { + Create(ctx context.Context, params *Params) error + Delete(ctx context.Context, id uint) error + DeleteByIds(ctx context.Context, ids []uint) error + Update(ctx context.Context, params *Params) error + FindByID(ctx context.Context, id uint) (*Params, error) + FindByKey(ctx context.Context, key string) (*Params, error) + FindList(ctx context.Context, page, pageSize int, filters map[string]interface{}) ([]*Params, int64, error) +} + +// ParamsUsecase 系统参数用例 +type ParamsUsecase struct { + repo ParamsRepo +} + +// NewParamsUsecase 创建系统参数用例 +func NewParamsUsecase(repo ParamsRepo) *ParamsUsecase { + return &ParamsUsecase{repo: repo} +} + +// Create 创建参数 +func (uc *ParamsUsecase) Create(ctx context.Context, params *Params) error { + return uc.repo.Create(ctx, params) +} + +// Delete 删除参数 +func (uc *ParamsUsecase) Delete(ctx context.Context, id uint) error { + return uc.repo.Delete(ctx, id) +} + +// DeleteByIds 批量删除参数 +func (uc *ParamsUsecase) DeleteByIds(ctx context.Context, ids []uint) error { + return uc.repo.DeleteByIds(ctx, ids) +} + +// Update 更新参数 +func (uc *ParamsUsecase) Update(ctx context.Context, params *Params) error { + return uc.repo.Update(ctx, params) +} + +// GetByID 根据ID获取参数 +func (uc *ParamsUsecase) GetByID(ctx context.Context, id uint) (*Params, error) { + return uc.repo.FindByID(ctx, id) +} + +// GetByKey 根据Key获取参数 +func (uc *ParamsUsecase) GetByKey(ctx context.Context, key string) (*Params, error) { + return uc.repo.FindByKey(ctx, key) +} + +// GetList 获取参数列表 +func (uc *ParamsUsecase) GetList(ctx context.Context, page, pageSize int, filters map[string]interface{}) ([]*Params, int64, error) { + return uc.repo.FindList(ctx, page, pageSize, filters) +} diff --git a/internal/conf/provider.go b/internal/conf/provider.go new file mode 100644 index 0000000..60fc580 --- /dev/null +++ b/internal/conf/provider.go @@ -0,0 +1,49 @@ +package conf + +import "github.com/google/wire" + +// ProviderSet is conf providers. +var ProviderSet = wire.NewSet( + ProvideServer, + ProvideJWT, + ProvideMysql, + ProvideCaptcha, + ProvideSystem, + ProvideUseStrictAuth, +) + +// ProvideServer 提供Server配置 +func ProvideServer(c *Bootstrap) *Server { + return c.Server +} + +// ProvideJWT 提供JWT配置 +func ProvideJWT(c *Bootstrap) *JWT { + return c.Jwt +} + +// ProvideMysql 提供Mysql配置 +func ProvideMysql(c *Bootstrap) *Mysql { + return c.Mysql +} + +// ProvideCaptcha 提供Captcha配置 +func ProvideCaptcha(c *Bootstrap) *Captcha { + return c.Captcha +} + +// ProvideSystem 提供System配置 +func ProvideSystem(c *Bootstrap) *System { + return c.System +} + +// UseStrictAuth 用于wire注入的类型包装 +type UseStrictAuth bool + +// ProvideUseStrictAuth 提供UseStrictAuth配置 +func ProvideUseStrictAuth(c *System) UseStrictAuth { + if c == nil { + return false + } + return UseStrictAuth(c.UseStrictAuth) +} diff --git a/internal/data/data.go b/internal/data/data.go index b8db616..9b0e73c 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -4,7 +4,9 @@ import ( "fmt" "kra/internal/conf" datasystem "kra/internal/data/system" + pkgcasbin "kra/pkg/casbin" + "github.com/casbin/casbin/v2" _ "github.com/go-sql-driver/mysql" "github.com/google/wire" "gorm.io/driver/mysql" @@ -16,8 +18,18 @@ import ( var ProviderSet = wire.NewSet( NewDB, NewData, + NewCasbinEnforcer, datasystem.NewUserRepo, datasystem.NewApiRepo, + datasystem.NewAuthorityRepo, + datasystem.NewAuthorityBtnRepo, + datasystem.NewCasbinRepo, + datasystem.NewMenuRepo, + datasystem.NewDictionaryRepo, + datasystem.NewDictionaryDetailRepo, + datasystem.NewJwtBlacklistRepo, + datasystem.NewOperationRecordRepo, + datasystem.NewParamsRepo, ) // Data 数据层包装器 @@ -49,3 +61,8 @@ func NewDB(c *conf.Mysql) (*gorm.DB, error) { } return db, nil } + +// NewCasbinEnforcer 创建Casbin Enforcer +func NewCasbinEnforcer(db *gorm.DB) (*casbin.SyncedCachedEnforcer, error) { + return pkgcasbin.InitCasbin(db, "") +} diff --git a/internal/data/gorm.go b/internal/data/gorm.go deleted file mode 100644 index 2067df5..0000000 --- a/internal/data/gorm.go +++ /dev/null @@ -1,74 +0,0 @@ -package data - -import ( - "time" - - "kra/internal/conf" - - "github.com/go-kratos/kratos/v2/log" - "gorm.io/driver/mysql" - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" - "gorm.io/gorm/schema" -) - -// NewGormDB 创建GORM数据库连接 -func NewGormDB(c *conf.Data, l log.Logger) (*gorm.DB, func(), error) { - helper := log.NewHelper(l) - - var dialector gorm.Dialector - switch c.Database.Driver { - case "mysql": - dialector = mysql.Open(c.Database.Source) - case "postgres": - dialector = postgres.Open(c.Database.Source) - case "sqlite": - dialector = sqlite.Open(c.Database.Source) - default: - dialector = mysql.Open(c.Database.Source) - } - - gormConfig := &gorm.Config{ - NamingStrategy: schema.NamingStrategy{ - TablePrefix: c.Database.TablePrefix, - SingularTable: true, - }, - DisableForeignKeyConstraintWhenMigrating: true, - } - - // 设置日志级别 - if c.Database.LogMode { - gormConfig.Logger = logger.Default.LogMode(logger.Info) - } else { - gormConfig.Logger = logger.Default.LogMode(logger.Silent) - } - - db, err := gorm.Open(dialector, gormConfig) - if err != nil { - helper.Errorf("failed to connect database: %v", err) - return nil, nil, err - } - - sqlDB, err := db.DB() - if err != nil { - helper.Errorf("failed to get sql.DB: %v", err) - return nil, nil, err - } - - // 连接池配置 - sqlDB.SetMaxIdleConns(int(c.Database.MaxIdleConns)) - sqlDB.SetMaxOpenConns(int(c.Database.MaxOpenConns)) - sqlDB.SetConnMaxLifetime(time.Duration(c.Database.MaxLifetime) * time.Second) - - cleanup := func() { - helper.Info("closing database connection") - if err := sqlDB.Close(); err != nil { - helper.Errorf("failed to close database: %v", err) - } - } - - helper.Info("database connected successfully") - return db, cleanup, nil -} diff --git a/internal/data/redis.go b/internal/data/redis.go deleted file mode 100644 index 78be53b..0000000 --- a/internal/data/redis.go +++ /dev/null @@ -1,47 +0,0 @@ -package data - -import ( - "context" - - "kra/internal/conf" - - "github.com/go-kratos/kratos/v2/log" - "github.com/redis/go-redis/v9" -) - -// NewRedisClient 创建Redis客户端 -func NewRedisClient(c *conf.Data, l log.Logger) (redis.UniversalClient, func(), error) { - helper := log.NewHelper(l) - - var client redis.UniversalClient - - if c.Redis.UseCluster { - client = redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: c.Redis.ClusterAddrs, - Password: c.Redis.Password, - }) - } else { - client = redis.NewClient(&redis.Options{ - Addr: c.Redis.Addr, - Password: c.Redis.Password, - DB: int(c.Redis.Db), - }) - } - - // 测试连接 - ctx := context.Background() - if _, err := client.Ping(ctx).Result(); err != nil { - helper.Errorf("failed to connect redis: %v", err) - return nil, nil, err - } - - cleanup := func() { - helper.Info("closing redis connection") - if err := client.Close(); err != nil { - helper.Errorf("failed to close redis: %v", err) - } - } - - helper.Info("redis connected successfully") - return client, cleanup, nil -} diff --git a/internal/data/system/params.go b/internal/data/system/params.go new file mode 100644 index 0000000..aa816e1 --- /dev/null +++ b/internal/data/system/params.go @@ -0,0 +1,139 @@ +package system + +import ( + "context" + "time" + + "kra/internal/biz/system" + "kra/internal/data/model" + "kra/internal/data/query" + + "gorm.io/gorm" +) + +// ParamsRepo 系统参数仓储实现 +type ParamsRepo struct { + data *gorm.DB +} + +// NewParamsRepo 创建系统参数仓储 +func NewParamsRepo(data *gorm.DB) system.ParamsRepo { + return &ParamsRepo{data: data} +} + +// Create 创建参数 +func (r *ParamsRepo) Create(ctx context.Context, params *system.Params) error { + m := toParamsModel(params) + return query.Use(r.data).SysParam.WithContext(ctx).Create(m) +} + +// Delete 删除参数 +func (r *ParamsRepo) Delete(ctx context.Context, id uint) error { + q := query.Use(r.data).SysParam + _, err := q.WithContext(ctx).Where(q.ID.Eq(int64(id))).Delete() + return err +} + +// DeleteByIds 批量删除参数 +func (r *ParamsRepo) DeleteByIds(ctx context.Context, ids []uint) error { + q := query.Use(r.data).SysParam + int64Ids := make([]int64, len(ids)) + for i, id := range ids { + int64Ids[i] = int64(id) + } + _, err := q.WithContext(ctx).Where(q.ID.In(int64Ids...)).Delete() + return err +} + +// Update 更新参数 +func (r *ParamsRepo) Update(ctx context.Context, params *system.Params) error { + q := query.Use(r.data).SysParam + _, err := q.WithContext(ctx).Where(q.ID.Eq(int64(params.ID))).Updates(toParamsModel(params)) + return err +} + +// FindByID 根据ID查找参数 +func (r *ParamsRepo) FindByID(ctx context.Context, id uint) (*system.Params, error) { + q := query.Use(r.data).SysParam + m, err := q.WithContext(ctx).Where(q.ID.Eq(int64(id))).First() + if err != nil { + return nil, err + } + return toParamsBiz(m), nil +} + +// FindByKey 根据Key查找参数 +func (r *ParamsRepo) FindByKey(ctx context.Context, key string) (*system.Params, error) { + q := query.Use(r.data).SysParam + m, err := q.WithContext(ctx).Where(q.Key.Eq(key)).First() + if err != nil { + return nil, err + } + return toParamsBiz(m), nil +} + +// FindList 查找参数列表 +func (r *ParamsRepo) FindList(ctx context.Context, page, pageSize int, filters map[string]interface{}) ([]*system.Params, int64, error) { + q := query.Use(r.data).SysParam + do := q.WithContext(ctx) + + // 应用过滤条件 + if name, ok := filters["name"].(string); ok && name != "" { + do = do.Where(q.Name.Like("%" + name + "%")) + } + if key, ok := filters["key"].(string); ok && key != "" { + do = do.Where(q.Key.Like("%" + key + "%")) + } + if startTime, ok := filters["start_time"].(*time.Time); ok && startTime != nil { + do = do.Where(q.CreatedAt.Gte(*startTime)) + } + if endTime, ok := filters["end_time"].(*time.Time); ok && endTime != nil { + do = do.Where(q.CreatedAt.Lte(*endTime)) + } + + // 获取总数 + total, err := do.Count() + if err != nil { + return nil, 0, err + } + + // 分页查询 + if pageSize > 0 { + do = do.Limit(pageSize).Offset((page - 1) * pageSize) + } + + list, err := do.Find() + if err != nil { + return nil, 0, err + } + + result := make([]*system.Params, len(list)) + for i, m := range list { + result[i] = toParamsBiz(m) + } + return result, total, nil +} + +// toParamsModel 转换为数据模型 +func toParamsModel(p *system.Params) *model.SysParam { + return &model.SysParam{ + ID: int64(p.ID), + Name: p.Name, + Key: p.Key, + Value: p.Value, + Desc: p.Desc, + } +} + +// toParamsBiz 转换为业务实体 +func toParamsBiz(m *model.SysParam) *system.Params { + return &system.Params{ + ID: uint(m.ID), + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + Name: m.Name, + Key: m.Key, + Value: m.Value, + Desc: m.Desc, + } +} diff --git a/internal/server/http.go b/internal/server/http.go index 495e69e..161df53 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -1,28 +1,93 @@ package server import ( + "context" + "kra/internal/conf" + "kra/internal/server/middleware" + "kra/internal/service/system" + "kra/pkg/jwt" + "kra/pkg/response" "github.com/go-kratos/kratos/v2/middleware/recovery" - "github.com/go-kratos/kratos/v2/transport/http" + "github.com/go-kratos/kratos/v2/middleware/selector" + khttp "github.com/go-kratos/kratos/v2/transport/http" ) // NewHTTPServer new an HTTP server. -func NewHTTPServer(c *conf.Server) *http.Server { - var opts = []http.ServerOption{ - http.Middleware( +func NewHTTPServer( + c *conf.Server, + jwtPkg *jwt.JWT, + userSvc *system.UserService, + apiSvc *system.ApiService, + authoritySvc *system.AuthorityService, + authorityBtnSvc *system.AuthorityBtnService, + captchaSvc *system.CaptchaService, + casbinSvc *system.CasbinService, + menuSvc *system.MenuService, + dictionarySvc *system.DictionaryService, + jwtBlacklistSvc *system.JwtBlacklistService, + operationRecordSvc *system.OperationRecordService, + paramsSvc *system.ParamsService, + systemConfigSvc *system.SystemConfigService, +) *khttp.Server { + var opts = []khttp.ServerOption{ + khttp.Middleware( recovery.Recovery(), + // JWT认证中间件(排除公开路由) + selector.Server( + middleware.JWTAuthSimple(jwtPkg), + ).Match(newWhiteListMatcher()).Build(), ), + khttp.ResponseEncoder(response.Encoder), + khttp.ErrorEncoder(response.ErrorEncoder), } if c.Http.Network != "" { - opts = append(opts, http.Network(c.Http.Network)) + opts = append(opts, khttp.Network(c.Http.Network)) } if c.Http.Addr != "" { - opts = append(opts, http.Address(c.Http.Addr)) + opts = append(opts, khttp.Address(c.Http.Addr)) } if c.Http.Timeout != nil { - opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration())) + opts = append(opts, khttp.Timeout(c.Http.Timeout.AsDuration())) } - srv := http.NewServer(opts...) + srv := khttp.NewServer(opts...) + + // 创建路由组 + rg := &system.RouterGroup{ + Public: srv.Route("/"), + Private: srv.Route("/"), + PrivateWithRecord: srv.Route("/"), + } + + // 注册各模块路由 + userSvc.RegisterRoutes(rg) + apiSvc.RegisterRoutes(rg) + authoritySvc.RegisterRoutes(rg) + authorityBtnSvc.RegisterRoutes(rg) + captchaSvc.RegisterRoutes(rg) + casbinSvc.RegisterRoutes(rg) + menuSvc.RegisterRoutes(rg) + dictionarySvc.RegisterRoutes(rg) + jwtBlacklistSvc.RegisterRoutes(rg) + operationRecordSvc.RegisterRoutes(rg) + paramsSvc.RegisterRoutes(rg) + systemConfigSvc.RegisterRoutes(rg) + return srv } + +// newWhiteListMatcher 创建白名单匹配器(这些路由不需要JWT认证) +func newWhiteListMatcher() selector.MatchFunc { + whiteList := map[string]struct{}{ + "/base/login": {}, + "/base/captcha": {}, + "/api/freshCasbin": {}, + } + return func(ctx context.Context, operation string) bool { + if _, ok := whiteList[operation]; ok { + return false // 不需要认证 + } + return true // 需要认证 + } +} diff --git a/internal/service/service.go b/internal/service/service.go index 1a3a55a..b546d01 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,6 +1,23 @@ package service -import "github.com/google/wire" +import ( + "kra/internal/service/system" + + "github.com/google/wire" +) // ProviderSet is service providers. -var ProviderSet = wire.NewSet() +var ProviderSet = wire.NewSet( + system.NewUserService, + system.NewApiService, + system.NewAuthorityService, + system.NewAuthorityBtnService, + system.NewCaptchaService, + system.NewCasbinService, + system.NewMenuService, + system.NewDictionaryService, + system.NewJwtBlacklistService, + system.NewOperationRecordService, + system.NewParamsService, + system.NewSystemConfigService, +) diff --git a/internal/service/system/api.go b/internal/service/system/api.go index f3f3cdf..321f132 100644 --- a/internal/service/system/api.go +++ b/internal/service/system/api.go @@ -1,13 +1,12 @@ package system import ( - "context" - "kra/internal/biz/system" "kra/internal/server/middleware" + types "kra/internal/service/types/system" "github.com/go-kratos/kratos/v2/errors" - "github.com/go-kratos/kratos/v2/transport/http" + khttp "github.com/go-kratos/kratos/v2/transport/http" ) // ApiService API服务 @@ -20,45 +19,30 @@ func NewApiService(uc *system.ApiUsecase) *ApiService { return &ApiService{uc: uc} } -// ApiInfo API信息 -type ApiInfo struct { - ID uint `json:"ID"` - Path string `json:"path"` - Description string `json:"description"` - ApiGroup string `json:"apiGroup"` - Method string `json:"method"` -} - -// CreateApiRequest 创建API请求 -type CreateApiRequest struct { - Path string `json:"path"` - Description string `json:"description"` - ApiGroup string `json:"apiGroup"` - Method string `json:"method"` -} - // CreateApi 创建API -func (s *ApiService) CreateApi(ctx context.Context, req *CreateApiRequest) error { +func (s *ApiService) CreateApi(ctx khttp.Context) error { + var req types.CreateApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } api := &system.Api{ Path: req.Path, Description: req.Description, ApiGroup: req.ApiGroup, Method: req.Method, } - return s.uc.CreateApi(ctx, api) -} - -// UpdateApiRequest 更新API请求 -type UpdateApiRequest struct { - ID uint `json:"ID"` - Path string `json:"path"` - Description string `json:"description"` - ApiGroup string `json:"apiGroup"` - Method string `json:"method"` + if err := s.uc.CreateApi(ctx, api); err != nil { + return errors.BadRequest("CREATE_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // UpdateApi 更新API -func (s *ApiService) UpdateApi(ctx context.Context, req *UpdateApiRequest) error { +func (s *ApiService) UpdateApi(ctx khttp.Context) error { + var req types.UpdateApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } api := &system.Api{ ID: req.ID, Path: req.Path, @@ -66,55 +50,55 @@ func (s *ApiService) UpdateApi(ctx context.Context, req *UpdateApiRequest) error ApiGroup: req.ApiGroup, Method: req.Method, } - return s.uc.UpdateApi(ctx, api) + if err := s.uc.UpdateApi(ctx, api); err != nil { + return errors.BadRequest("UPDATE_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // DeleteApi 删除API -func (s *ApiService) DeleteApi(ctx context.Context, id uint) error { - return s.uc.DeleteApi(ctx, id) -} - -// DeleteApisByIdsRequest 批量删除API请求 -type DeleteApisByIdsRequest struct { - Ids []uint `json:"ids"` +func (s *ApiService) DeleteApi(ctx khttp.Context) error { + var req types.DeleteApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.uc.DeleteApi(ctx, req.ID); err != nil { + return errors.BadRequest("DELETE_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // DeleteApisByIds 批量删除API -func (s *ApiService) DeleteApisByIds(ctx context.Context, req *DeleteApisByIdsRequest) error { - return s.uc.DeleteApisByIds(ctx, req.Ids) +func (s *ApiService) DeleteApisByIds(ctx khttp.Context) error { + var req types.DeleteApisByIdsRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.uc.DeleteApisByIds(ctx, req.Ids); err != nil { + return errors.BadRequest("DELETE_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // GetApiById 根据ID获取API -func (s *ApiService) GetApiById(ctx context.Context, id uint) (*ApiInfo, error) { - api, err := s.uc.GetApiById(ctx, id) - if err != nil { - return nil, err +func (s *ApiService) GetApiById(ctx khttp.Context) error { + var req types.GetApiByIdRequest + if err := ctx.Bind(&req); err != nil { + return err } - return toApiInfo(api), nil -} - -// GetApiListRequest 获取API列表请求 -type GetApiListRequest struct { - Page int `json:"page"` - PageSize int `json:"pageSize"` - Path string `json:"path"` - Description string `json:"description"` - Method string `json:"method"` - ApiGroup string `json:"apiGroup"` - OrderKey string `json:"orderKey"` - Desc bool `json:"desc"` -} - -// GetApiListResponse 获取API列表响应 -type GetApiListResponse struct { - List []*ApiInfo `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"pageSize"` + api, err := s.uc.GetApiById(ctx, req.ID) + if err != nil { + return errors.NotFound("NOT_FOUND", err.Error()) + } + return ctx.Result(200, &types.GetApiByIdReply{Api: toTypesApiInfo(api)}) } // GetApiList 获取API列表 -func (s *ApiService) GetApiList(ctx context.Context, req *GetApiListRequest) (*GetApiListResponse, error) { +func (s *ApiService) GetApiList(ctx khttp.Context) error { + var req types.GetApiListRequest + if err := ctx.Bind(&req); err != nil { + return err + } filters := make(map[string]string) if req.Path != "" { filters["path"] = req.Path @@ -128,94 +112,71 @@ func (s *ApiService) GetApiList(ctx context.Context, req *GetApiListRequest) (*G if req.ApiGroup != "" { filters["api_group"] = req.ApiGroup } - apis, total, err := s.uc.GetAPIInfoList(ctx, req.Page, req.PageSize, filters, req.OrderKey, req.Desc) if err != nil { - return nil, err + return errors.InternalServer("LIST_ERROR", err.Error()) } - - list := make([]*ApiInfo, len(apis)) + list := make([]types.ApiInfo, len(apis)) for i, api := range apis { - list[i] = toApiInfo(api) + list[i] = toTypesApiInfo(api) } - - return &GetApiListResponse{ - List: list, - Total: total, - Page: req.Page, - PageSize: req.PageSize, - }, nil + return ctx.Result(200, &types.GetApiListReply{List: list, Total: total, Page: req.Page, PageSize: req.PageSize}) } // GetAllApis 获取所有API -func (s *ApiService) GetAllApis(ctx context.Context, authorityID uint) ([]*ApiInfo, error) { +func (s *ApiService) GetAllApis(ctx khttp.Context) error { + authorityID := middleware.GetAuthorityID(ctx) apis, err := s.uc.GetAllApis(ctx, authorityID) if err != nil { - return nil, err + return errors.InternalServer("LIST_ERROR", err.Error()) } - list := make([]*ApiInfo, len(apis)) + list := make([]types.ApiInfo, len(apis)) for i, api := range apis { - list[i] = toApiInfo(api) + list[i] = toTypesApiInfo(api) } - return list, nil + return ctx.Result(200, &types.GetAllApisReply{Apis: list}) } // GetApiGroups 获取API分组 -func (s *ApiService) GetApiGroups(ctx context.Context) ([]string, map[string]string, error) { - return s.uc.GetApiGroups(ctx) -} - -// SyncApiResponse 同步API响应 -type SyncApiResponse struct { - NewApis []*ApiInfo `json:"newApis"` - DeleteApis []*ApiInfo `json:"deleteApis"` - IgnoreApis []*ApiInfo `json:"ignoreApis"` +func (s *ApiService) GetApiGroups(ctx khttp.Context) error { + groups, apiGroupMap, err := s.uc.GetApiGroups(ctx) + if err != nil { + return errors.InternalServer("LIST_ERROR", err.Error()) + } + return ctx.Result(200, &types.GetApiGroupsReply{Groups: groups, ApiGroupMap: apiGroupMap}) } // SyncApi 同步API -func (s *ApiService) SyncApi(ctx context.Context) (*SyncApiResponse, error) { +func (s *ApiService) SyncApi(ctx khttp.Context) error { newApis, deleteApis, ignoreApis, err := s.uc.SyncApi(ctx) if err != nil { - return nil, err + return errors.InternalServer("SYNC_ERROR", err.Error()) } - - resp := &SyncApiResponse{ - NewApis: make([]*ApiInfo, len(newApis)), - DeleteApis: make([]*ApiInfo, len(deleteApis)), - IgnoreApis: make([]*ApiInfo, len(ignoreApis)), - } - for i, api := range newApis { - resp.NewApis[i] = toApiInfo(api) - } - for i, api := range deleteApis { - resp.DeleteApis[i] = toApiInfo(api) - } - for i, api := range ignoreApis { - resp.IgnoreApis[i] = toApiInfo(api) - } - return resp, nil -} - -// IgnoreApiRequest 忽略API请求 -type IgnoreApiRequest struct { - Path string `json:"path"` - Method string `json:"method"` - Flag bool `json:"flag"` + return ctx.Result(200, &types.SyncApiReply{ + NewApis: toTypesApiInfoList(newApis), + DeleteApis: toTypesApiInfoList(deleteApis), + IgnoreApis: toTypesApiInfoList(ignoreApis), + }) } // IgnoreApi 忽略API -func (s *ApiService) IgnoreApi(ctx context.Context, req *IgnoreApiRequest) error { - return s.uc.IgnoreApi(ctx, req.Path, req.Method, req.Flag) -} - -// EnterSyncApiRequest 确认同步API请求 -type EnterSyncApiRequest struct { - NewApis []*ApiInfo `json:"newApis"` - DeleteApis []*ApiInfo `json:"deleteApis"` +func (s *ApiService) IgnoreApi(ctx khttp.Context) error { + var req types.IgnoreApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.uc.IgnoreApi(ctx, req.Path, req.Method, req.Flag); err != nil { + return errors.BadRequest("IGNORE_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // EnterSyncApi 确认同步API -func (s *ApiService) EnterSyncApi(ctx context.Context, req *EnterSyncApiRequest) error { +func (s *ApiService) EnterSyncApi(ctx khttp.Context) error { + var req types.EnterSyncApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } newApis := make([]*system.Api, len(req.NewApis)) for i, api := range req.NewApis { newApis[i] = &system.Api{ @@ -233,17 +194,26 @@ func (s *ApiService) EnterSyncApi(ctx context.Context, req *EnterSyncApiRequest) Method: api.Method, } } - return s.uc.EnterSyncApi(ctx, newApis, deleteApis) + if err := s.uc.EnterSyncApi(ctx, newApis, deleteApis); err != nil { + return errors.BadRequest("SYNC_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // FreshCasbin 刷新Casbin缓存 -func (s *ApiService) FreshCasbin() error { - return s.uc.FreshCasbin() +func (s *ApiService) FreshCasbin(ctx khttp.Context) error { + if err := s.uc.FreshCasbin(); err != nil { + return errors.InternalServer("FRESH_FAILED", err.Error()) + } + return ctx.Result(200, nil) } -// 转换函数 -func toApiInfo(api *system.Api) *ApiInfo { - return &ApiInfo{ +// toTypesApiInfo 转换API信息为types格式 +func toTypesApiInfo(api *system.Api) types.ApiInfo { + if api == nil { + return types.ApiInfo{} + } + return types.ApiInfo{ ID: api.ID, Path: api.Path, Description: api.Description, @@ -252,147 +222,32 @@ func toApiInfo(api *system.Api) *ApiInfo { } } -// RegisterRoutes 注册路由 -func (s *ApiService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") - - r.POST("/api/createApi", s.handleCreateApi) - r.POST("/api/updateApi", s.handleUpdateApi) - r.POST("/api/deleteApi", s.handleDeleteApi) - r.DELETE("/api/deleteApisByIds", s.handleDeleteApisByIds) - r.POST("/api/getApiList", s.handleGetApiList) - r.POST("/api/getApiById", s.handleGetApiById) - r.POST("/api/getAllApis", s.handleGetAllApis) - r.GET("/api/getApiGroups", s.handleGetApiGroups) - r.GET("/api/syncApi", s.handleSyncApi) - r.POST("/api/ignoreApi", s.handleIgnoreApi) - r.POST("/api/enterSyncApi", s.handleEnterSyncApi) - r.GET("/api/freshCasbin", s.handleFreshCasbin) +// toTypesApiInfoList 批量转换API信息 +func toTypesApiInfoList(apis []*system.Api) []types.ApiInfo { + list := make([]types.ApiInfo, len(apis)) + for i, api := range apis { + list[i] = toTypesApiInfo(api) + } + return list } -// HTTP Handlers -func (s *ApiService) handleCreateApi(ctx http.Context) error { - var req CreateApiRequest - if err := ctx.Bind(&req); err != nil { - return err - } - if err := s.CreateApi(ctx, &req); err != nil { - return errors.BadRequest("CREATE_FAILED", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "创建成功"}) -} +// RegisterRoutes 注册API管理路由 +func (s *ApiService) RegisterRoutes(rg *RouterGroup) { + // 公开路由 + rg.Public.GET("/api/freshCasbin", s.FreshCasbin) -func (s *ApiService) handleUpdateApi(ctx http.Context) error { - var req UpdateApiRequest - if err := ctx.Bind(&req); err != nil { - return err - } - if err := s.UpdateApi(ctx, &req); err != nil { - return errors.BadRequest("UPDATE_FAILED", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "修改成功"}) -} + // 私有路由 + 操作记录(写操作) + rg.PrivateWithRecord.GET("/api/getApiGroups", s.GetApiGroups) + rg.PrivateWithRecord.GET("/api/syncApi", s.SyncApi) + rg.PrivateWithRecord.POST("/api/ignoreApi", s.IgnoreApi) + rg.PrivateWithRecord.POST("/api/enterSyncApi", s.EnterSyncApi) + rg.PrivateWithRecord.POST("/api/createApi", s.CreateApi) + rg.PrivateWithRecord.POST("/api/deleteApi", s.DeleteApi) + rg.PrivateWithRecord.POST("/api/getApiById", s.GetApiById) + rg.PrivateWithRecord.POST("/api/updateApi", s.UpdateApi) + rg.PrivateWithRecord.DELETE("/api/deleteApisByIds", s.DeleteApisByIds) -func (s *ApiService) handleDeleteApi(ctx http.Context) error { - var req struct { - ID uint `json:"ID"` - } - if err := ctx.Bind(&req); err != nil { - return err - } - if err := s.DeleteApi(ctx, req.ID); err != nil { - return errors.BadRequest("DELETE_FAILED", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "删除成功"}) -} - -func (s *ApiService) handleDeleteApisByIds(ctx http.Context) error { - var req DeleteApisByIdsRequest - if err := ctx.Bind(&req); err != nil { - return err - } - if err := s.DeleteApisByIds(ctx, &req); err != nil { - return errors.BadRequest("DELETE_FAILED", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "删除成功"}) -} - -func (s *ApiService) handleGetApiList(ctx http.Context) error { - var req GetApiListRequest - if err := ctx.Bind(&req); err != nil { - return err - } - resp, err := s.GetApiList(ctx, &req) - if err != nil { - return errors.InternalServer("LIST_ERROR", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": resp}) -} - -func (s *ApiService) handleGetApiById(ctx http.Context) error { - var req struct { - ID uint `json:"ID"` - } - if err := ctx.Bind(&req); err != nil { - return err - } - resp, err := s.GetApiById(ctx, req.ID) - if err != nil { - return errors.NotFound("NOT_FOUND", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"api": resp}}) -} - -func (s *ApiService) handleGetAllApis(ctx http.Context) error { - authorityID := middleware.GetAuthorityID(ctx) - resp, err := s.GetAllApis(ctx, authorityID) - if err != nil { - return errors.InternalServer("LIST_ERROR", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"apis": resp}}) -} - -func (s *ApiService) handleGetApiGroups(ctx http.Context) error { - groups, apiGroupMap, err := s.GetApiGroups(ctx) - if err != nil { - return errors.InternalServer("LIST_ERROR", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"groups": groups, "apiGroupMap": apiGroupMap}}) -} - -func (s *ApiService) handleSyncApi(ctx http.Context) error { - resp, err := s.SyncApi(ctx) - if err != nil { - return errors.InternalServer("SYNC_ERROR", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "data": resp}) -} - -func (s *ApiService) handleIgnoreApi(ctx http.Context) error { - var req IgnoreApiRequest - if err := ctx.Bind(&req); err != nil { - return err - } - if err := s.IgnoreApi(ctx, &req); err != nil { - return errors.BadRequest("IGNORE_FAILED", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "操作成功"}) -} - -func (s *ApiService) handleEnterSyncApi(ctx http.Context) error { - var req EnterSyncApiRequest - if err := ctx.Bind(&req); err != nil { - return err - } - if err := s.EnterSyncApi(ctx, &req); err != nil { - return errors.BadRequest("SYNC_FAILED", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "同步成功"}) -} - -func (s *ApiService) handleFreshCasbin(ctx http.Context) error { - if err := s.FreshCasbin(); err != nil { - return errors.InternalServer("FRESH_FAILED", err.Error()) - } - return ctx.Result(200, map[string]any{"code": 0, "msg": "刷新成功"}) + // 私有路由(读操作,不记录) + rg.Private.POST("/api/getAllApis", s.GetAllApis) + rg.Private.POST("/api/getApiList", s.GetApiList) } diff --git a/internal/service/system/authority.go b/internal/service/system/authority.go index 1cdc82d..30800cd 100644 --- a/internal/service/system/authority.go +++ b/internal/service/system/authority.go @@ -5,6 +5,7 @@ import ( "kra/internal/biz/system" "kra/internal/server/middleware" + types "kra/internal/service/types/system" "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/transport/http" @@ -26,12 +27,12 @@ func NewAuthorityService(uc *system.AuthorityUsecase, casbin system.CasbinRepo) // AuthorityInfo 角色信息 type AuthorityFullInfo struct { - AuthorityId uint `json:"authorityId"` - AuthorityName string `json:"authorityName"` - ParentId *uint `json:"parentId,omitempty"` - DefaultRouter string `json:"defaultRouter"` - DataAuthorityId []*AuthorityInfo `json:"dataAuthorityId,omitempty"` - Children []*AuthorityFullInfo `json:"children,omitempty"` + AuthorityId uint `json:"authorityId"` + AuthorityName string `json:"authorityName"` + ParentId *uint `json:"parentId,omitempty"` + DefaultRouter string `json:"defaultRouter"` + DataAuthorityId []*types.AuthorityInfo `json:"dataAuthorityId,omitempty"` + Children []*AuthorityFullInfo `json:"children,omitempty"` } // CreateAuthorityRequest 创建角色请求 @@ -172,9 +173,9 @@ func toAuthorityFullInfoList(list []*system.AuthorityFull) []*AuthorityFullInfo } // DataAuthorityId if len(auth.DataAuthorityId) > 0 { - result[i].DataAuthorityId = make([]*AuthorityInfo, len(auth.DataAuthorityId)) + result[i].DataAuthorityId = make([]*types.AuthorityInfo, len(auth.DataAuthorityId)) for j, da := range auth.DataAuthorityId { - result[i].DataAuthorityId[j] = &AuthorityInfo{ + result[i].DataAuthorityId[j] = &types.AuthorityInfo{ AuthorityId: da.AuthorityId, AuthorityName: da.AuthorityName, ParentId: da.ParentId, @@ -187,15 +188,16 @@ func toAuthorityFullInfoList(list []*system.AuthorityFull) []*AuthorityFullInfo } // RegisterRoutes 注册路由 -func (s *AuthorityService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") +func (s *AuthorityService) RegisterRoutes(rg *RouterGroup) { + // 私有路由 + 操作记录(写操作) + rg.PrivateWithRecord.POST("/authority/createAuthority", s.handleCreateAuthority) + rg.PrivateWithRecord.POST("/authority/copyAuthority", s.handleCopyAuthority) + rg.PrivateWithRecord.PUT("/authority/updateAuthority", s.handleUpdateAuthority) + rg.PrivateWithRecord.POST("/authority/deleteAuthority", s.handleDeleteAuthority) + rg.PrivateWithRecord.POST("/authority/setDataAuthority", s.handleSetDataAuthority) - r.POST("/authority/createAuthority", s.handleCreateAuthority) - r.POST("/authority/copyAuthority", s.handleCopyAuthority) - r.PUT("/authority/updateAuthority", s.handleUpdateAuthority) - r.POST("/authority/deleteAuthority", s.handleDeleteAuthority) - r.POST("/authority/getAuthorityList", s.handleGetAuthorityList) - r.POST("/authority/setDataAuthority", s.handleSetDataAuthority) + // 私有路由(读操作,不记录) + rg.Private.POST("/authority/getAuthorityList", s.handleGetAuthorityList) } // HTTP Handlers diff --git a/internal/service/system/authority_btn.go b/internal/service/system/authority_btn.go index 810932d..4b3a6a6 100644 --- a/internal/service/system/authority_btn.go +++ b/internal/service/system/authority_btn.go @@ -38,11 +38,11 @@ type SetAuthorityBtnRequest struct { } // RegisterRoutes 注册路由 -func (s *AuthorityBtnService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") - r.POST("/authorityBtn/getAuthorityBtn", s.handleGetAuthorityBtn) - r.POST("/authorityBtn/setAuthorityBtn", s.handleSetAuthorityBtn) - r.POST("/authorityBtn/canRemoveAuthorityBtn", s.handleCanRemoveAuthorityBtn) +func (s *AuthorityBtnService) RegisterRoutes(rg *RouterGroup) { + // 角色按钮权限(不需要操作记录) + rg.Private.POST("/authorityBtn/getAuthorityBtn", s.handleGetAuthorityBtn) + rg.Private.POST("/authorityBtn/setAuthorityBtn", s.handleSetAuthorityBtn) + rg.Private.POST("/authorityBtn/canRemoveAuthorityBtn", s.handleCanRemoveAuthorityBtn) } func (s *AuthorityBtnService) handleGetAuthorityBtn(ctx http.Context) error { diff --git a/internal/service/system/captcha.go b/internal/service/system/captcha.go index d717e11..5b02693 100644 --- a/internal/service/system/captcha.go +++ b/internal/service/system/captcha.go @@ -4,24 +4,30 @@ import ( "sync" "time" + "kra/internal/conf" + "github.com/go-kratos/kratos/v2/transport/http" "github.com/mojocn/base64Captcha" ) // CaptchaService 验证码服务 type CaptchaService struct { - config CaptchaConfig - store base64Captcha.Store - blackCache *BlackCache + keyLong int + imgWidth int + imgHeight int + openCaptcha int // 防爆次数,0表示关闭 + openCaptchaTimeOut int // 缓存超时时间(秒) + store base64Captcha.Store + blackCache *BlackCache } -// CaptchaConfig 验证码配置 +// CaptchaConfig 验证码配置(保留用于兼容) type CaptchaConfig struct { KeyLong int ImgWidth int ImgHeight int - OpenCaptcha int // 防爆次数,0表示关闭 - OpenCaptchaTimeOut int // 缓存超时时间(秒) + OpenCaptcha int + OpenCaptchaTimeOut int } // BlackCache 黑名单缓存 @@ -86,21 +92,22 @@ type CaptchaResponse struct { } // NewCaptchaService 创建验证码服务 -func NewCaptchaService(config CaptchaConfig, store base64Captcha.Store) *CaptchaService { - if store == nil { - store = base64Captcha.DefaultMemStore - } +func NewCaptchaService(c *conf.Captcha) *CaptchaService { return &CaptchaService{ - config: config, - store: store, - blackCache: NewBlackCache(), + keyLong: int(c.KeyLong), + imgWidth: int(c.ImgWidth), + imgHeight: int(c.ImgHeight), + openCaptcha: int(c.OpenCaptcha), + openCaptchaTimeOut: int(c.OpenCaptchaTimeout), + store: base64Captcha.DefaultMemStore, + blackCache: NewBlackCache(), } } // RegisterRoutes 注册路由 -func (s *CaptchaService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") - r.POST("/base/captcha", s.handleCaptcha) +func (s *CaptchaService) RegisterRoutes(rg *RouterGroup) { + // 验证码(公开路由,无需认证) + rg.Public.POST("/base/captcha", s.handleCaptcha) } func (s *CaptchaService) handleCaptcha(ctx http.Context) error { @@ -108,8 +115,8 @@ func (s *CaptchaService) handleCaptcha(ctx http.Context) error { clientIP := ctx.Request().RemoteAddr // 判断验证码是否开启 - openCaptcha := s.config.OpenCaptcha - openCaptchaTimeOut := s.config.OpenCaptchaTimeOut + openCaptcha := s.openCaptcha + openCaptchaTimeOut := s.openCaptchaTimeOut v, ok := s.blackCache.Get(clientIP) if !ok { @@ -123,9 +130,9 @@ func (s *CaptchaService) handleCaptcha(ctx http.Context) error { // 生成验证码 driver := base64Captcha.NewDriverDigit( - s.config.ImgHeight, - s.config.ImgWidth, - s.config.KeyLong, + s.imgHeight, + s.imgWidth, + s.keyLong, 0.7, 80, ) @@ -144,7 +151,7 @@ func (s *CaptchaService) handleCaptcha(ctx http.Context) error { "data": CaptchaResponse{ CaptchaId: id, PicPath: b64s, - CaptchaLength: s.config.KeyLong, + CaptchaLength: s.keyLong, OpenCaptcha: oc, }, }) diff --git a/internal/service/system/casbin.go b/internal/service/system/casbin.go index 6a81d40..2cd58cc 100644 --- a/internal/service/system/casbin.go +++ b/internal/service/system/casbin.go @@ -5,6 +5,7 @@ import ( "strconv" "kra/internal/biz/system" + "kra/internal/conf" "kra/internal/server/middleware" kerrors "github.com/go-kratos/kratos/v2/errors" @@ -20,12 +21,12 @@ type CasbinService struct { } // NewCasbinService 创建Casbin服务 -func NewCasbinService(casbinRepo system.CasbinRepo, authUc *system.AuthorityUsecase, apiUc *system.ApiUsecase, useStrictAuth bool) *CasbinService { +func NewCasbinService(casbinRepo system.CasbinRepo, authUc *system.AuthorityUsecase, apiUc *system.ApiUsecase, useStrictAuth conf.UseStrictAuth) *CasbinService { return &CasbinService{ casbinRepo: casbinRepo, authUc: authUc, apiUc: apiUc, - useStrictAuth: useStrictAuth, + useStrictAuth: bool(useStrictAuth), } } @@ -81,10 +82,12 @@ func (s *CasbinService) GetPolicyPathByAuthorityId(authorityId uint) []system.Ca } // RegisterRoutes 注册路由 -func (s *CasbinService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") - r.POST("/casbin/UpdateCasbin", s.handleUpdateCasbin) - r.POST("/casbin/getPolicyPathByAuthorityId", s.handleGetPolicyPath) +func (s *CasbinService) RegisterRoutes(rg *RouterGroup) { + // 私有路由 + 操作记录(写操作) + rg.PrivateWithRecord.POST("/casbin/updateCasbin", s.handleUpdateCasbin) + + // 私有路由(读操作,不记录) + rg.Private.POST("/casbin/getPolicyPathByAuthorityId", s.handleGetPolicyPath) } func (s *CasbinService) handleUpdateCasbin(ctx http.Context) error { diff --git a/internal/service/system/dictionary.go b/internal/service/system/dictionary.go index 5f07575..add7a7a 100644 --- a/internal/service/system/dictionary.go +++ b/internal/service/system/dictionary.go @@ -365,28 +365,30 @@ func toDictionaryDetailInfoList(list []*system.DictionaryDetail) []*DictionaryDe } // RegisterRoutes 注册路由 -func (s *DictionaryService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") +func (s *DictionaryService) RegisterRoutes(rg *RouterGroup) { + // 字典接口 - 写操作(带操作记录) + rg.PrivateWithRecord.POST("/sysDictionary/createSysDictionary", s.handleCreateDictionary) + rg.PrivateWithRecord.DELETE("/sysDictionary/deleteSysDictionary", s.handleDeleteDictionary) + rg.PrivateWithRecord.PUT("/sysDictionary/updateSysDictionary", s.handleUpdateDictionary) + rg.PrivateWithRecord.POST("/sysDictionary/importSysDictionary", s.handleImportDictionary) + rg.PrivateWithRecord.GET("/sysDictionary/exportSysDictionary", s.handleExportDictionary) - // 字典接口 - r.POST("/sysDictionary/createSysDictionary", s.handleCreateDictionary) - r.DELETE("/sysDictionary/deleteSysDictionary", s.handleDeleteDictionary) - r.PUT("/sysDictionary/updateSysDictionary", s.handleUpdateDictionary) - r.GET("/sysDictionary/findSysDictionary", s.handleGetDictionary) - r.GET("/sysDictionary/getSysDictionaryList", s.handleGetDictionaryList) - r.GET("/sysDictionary/exportSysDictionary", s.handleExportDictionary) - r.POST("/sysDictionary/importSysDictionary", s.handleImportDictionary) + // 字典接口 - 读操作(不记录) + rg.Private.GET("/sysDictionary/findSysDictionary", s.handleGetDictionary) + rg.Private.GET("/sysDictionary/getSysDictionaryList", s.handleGetDictionaryList) - // 字典详情接口 - r.POST("/sysDictionaryDetail/createSysDictionaryDetail", s.handleCreateDictionaryDetail) - r.DELETE("/sysDictionaryDetail/deleteSysDictionaryDetail", s.handleDeleteDictionaryDetail) - r.PUT("/sysDictionaryDetail/updateSysDictionaryDetail", s.handleUpdateDictionaryDetail) - r.GET("/sysDictionaryDetail/findSysDictionaryDetail", s.handleGetDictionaryDetail) - r.GET("/sysDictionaryDetail/getSysDictionaryDetailList", s.handleGetDictionaryDetailList) - r.GET("/sysDictionaryDetail/getDictionaryTreeList", s.handleGetDictionaryTreeList) - r.POST("/sysDictionaryDetail/getDictionaryDetailsByParent", s.handleGetDictionaryDetailsByParent) - r.GET("/sysDictionaryDetail/getDictionaryListByType", s.handleGetDictionaryListByType) - r.GET("/sysDictionaryDetail/getDictionaryTreeListByType", s.handleGetDictionaryTreeListByType) + // 字典详情接口 - 写操作(带操作记录) + rg.PrivateWithRecord.POST("/sysDictionaryDetail/createSysDictionaryDetail", s.handleCreateDictionaryDetail) + rg.PrivateWithRecord.DELETE("/sysDictionaryDetail/deleteSysDictionaryDetail", s.handleDeleteDictionaryDetail) + rg.PrivateWithRecord.PUT("/sysDictionaryDetail/updateSysDictionaryDetail", s.handleUpdateDictionaryDetail) + + // 字典详情接口 - 读操作(不记录) + rg.Private.GET("/sysDictionaryDetail/findSysDictionaryDetail", s.handleGetDictionaryDetail) + rg.Private.GET("/sysDictionaryDetail/getSysDictionaryDetailList", s.handleGetDictionaryDetailList) + rg.Private.GET("/sysDictionaryDetail/getDictionaryTreeList", s.handleGetDictionaryTreeList) + rg.Private.GET("/sysDictionaryDetail/getDictionaryTreeListByType", s.handleGetDictionaryTreeListByType) + rg.Private.GET("/sysDictionaryDetail/getDictionaryDetailsByParent", s.handleGetDictionaryDetailsByParent) + rg.Private.GET("/sysDictionaryDetail/getDictionaryPath", s.handleGetDictionaryPath) } // HTTP Handlers - Dictionary @@ -568,6 +570,16 @@ func (s *DictionaryService) handleGetDictionaryTreeListByType(ctx http.Context) return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"list": resp}}) } +func (s *DictionaryService) handleGetDictionaryPath(ctx http.Context) error { + idStr := ctx.Query().Get("id") + id, _ := parseUint(idStr) + resp, err := s.uc.GetDictionaryPath(ctx, id) + if err != nil { + return errors.InternalServer("PATH_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"path": toDictionaryDetailInfoList(resp)}}) +} + // parseUint 解析uint func parseUint(s string) (uint, error) { if s == "" { diff --git a/internal/service/system/jwt_blacklist.go b/internal/service/system/jwt_blacklist.go index be364b7..d93b835 100644 --- a/internal/service/system/jwt_blacklist.go +++ b/internal/service/system/jwt_blacklist.go @@ -34,7 +34,7 @@ func (s *JwtBlacklistService) JoinBlacklist(ctx http.Context) error { } // RegisterRoutes 注册路由 -func (s *JwtBlacklistService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") - r.POST("/jwt/jsonInBlacklist", s.JoinBlacklist) +func (s *JwtBlacklistService) RegisterRoutes(rg *RouterGroup) { + // JWT黑名单(不需要操作记录) + rg.Private.POST("/jwt/jsonInBlacklist", s.JoinBlacklist) } diff --git a/internal/service/system/menu.go b/internal/service/system/menu.go index dc24567..100a98a 100644 --- a/internal/service/system/menu.go +++ b/internal/service/system/menu.go @@ -322,18 +322,19 @@ func toBaseMenuInfo(m *system.BaseMenu) *MenuInfo { } // RegisterRoutes 注册路由 -func (s *MenuService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") +func (s *MenuService) RegisterRoutes(rg *RouterGroup) { + // 私有路由 + 操作记录(写操作) + rg.PrivateWithRecord.POST("/menu/addBaseMenu", s.handleAddBaseMenu) + rg.PrivateWithRecord.POST("/menu/addMenuAuthority", s.handleAddMenuAuthority) + rg.PrivateWithRecord.POST("/menu/deleteBaseMenu", s.handleDeleteBaseMenu) + rg.PrivateWithRecord.POST("/menu/updateBaseMenu", s.handleUpdateBaseMenu) - r.POST("/menu/getMenu", s.handleGetMenu) - r.POST("/menu/getBaseMenuTree", s.handleGetBaseMenuTree) - r.POST("/menu/getMenuList", s.handleGetMenuList) - r.POST("/menu/addBaseMenu", s.handleAddBaseMenu) - r.POST("/menu/updateBaseMenu", s.handleUpdateBaseMenu) - r.POST("/menu/deleteBaseMenu", s.handleDeleteBaseMenu) - r.POST("/menu/getBaseMenuById", s.handleGetBaseMenuById) - r.POST("/menu/addMenuAuthority", s.handleAddMenuAuthority) - r.POST("/menu/getMenuAuthority", s.handleGetMenuAuthority) + // 私有路由(读操作,不记录) + rg.Private.POST("/menu/getMenu", s.handleGetMenu) + rg.Private.POST("/menu/getMenuList", s.handleGetMenuList) + rg.Private.POST("/menu/getBaseMenuTree", s.handleGetBaseMenuTree) + rg.Private.POST("/menu/getMenuAuthority", s.handleGetMenuAuthority) + rg.Private.POST("/menu/getBaseMenuById", s.handleGetBaseMenuById) } // HTTP Handlers diff --git a/internal/service/system/operation_record.go b/internal/service/system/operation_record.go index bd1b74a..ead67b3 100644 --- a/internal/service/system/operation_record.go +++ b/internal/service/system/operation_record.go @@ -4,6 +4,7 @@ import ( "fmt" "kra/internal/biz/system" + types "kra/internal/service/types/system" "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/transport/http" @@ -21,18 +22,18 @@ func NewOperationRecordService(uc *system.OperationRecordUsecase) *OperationReco // OperationRecordInfo 操作记录信息 type OperationRecordInfo struct { - ID uint `json:"id"` - IP string `json:"ip"` - Method string `json:"method"` - Path string `json:"path"` - Status int `json:"status"` - Latency int64 `json:"latency"` - Agent string `json:"agent"` - ErrorMessage string `json:"error_message"` - Body string `json:"body"` - Resp string `json:"resp"` - UserID uint `json:"user_id"` - User *UserInfo `json:"user,omitempty"` + ID uint `json:"id"` + IP string `json:"ip"` + Method string `json:"method"` + Path string `json:"path"` + Status int `json:"status"` + Latency int64 `json:"latency"` + Agent string `json:"agent"` + ErrorMessage string `json:"error_message"` + Body string `json:"body"` + Resp string `json:"resp"` + UserID uint `json:"user_id"` + User *types.UserInfo `json:"user,omitempty"` } // GetOperationRecordListRequest 获取操作记录列表请求 @@ -64,12 +65,12 @@ type DeleteOperationRecordByIdsRequest struct { } // RegisterRoutes 注册路由 -func (s *OperationRecordService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") - r.POST("/sysOperationRecord/getSysOperationRecordList", s.handleGetList) - r.DELETE("/sysOperationRecord/deleteSysOperationRecord", s.handleDelete) - r.DELETE("/sysOperationRecord/deleteSysOperationRecordByIds", s.handleDeleteByIds) - r.GET("/sysOperationRecord/findSysOperationRecord", s.handleFind) +func (s *OperationRecordService) RegisterRoutes(rg *RouterGroup) { + // 操作记录(不需要操作记录中间件,避免递归) + rg.Private.DELETE("/sysOperationRecord/deleteSysOperationRecord", s.handleDelete) + rg.Private.DELETE("/sysOperationRecord/deleteSysOperationRecordByIds", s.handleDeleteByIds) + rg.Private.GET("/sysOperationRecord/findSysOperationRecord", s.handleFind) + rg.Private.GET("/sysOperationRecord/getSysOperationRecordList", s.handleGetList) } func (s *OperationRecordService) handleGetList(ctx http.Context) error { @@ -159,7 +160,8 @@ func toOperationRecordInfo(r *system.OperationRecord) *OperationRecordInfo { UserID: r.UserID, } if r.User != nil { - info.User = toUserInfo(r.User) + userInfo := toTypesUserInfo(r.User) + info.User = &userInfo } return info } diff --git a/internal/service/system/params.go b/internal/service/system/params.go new file mode 100644 index 0000000..ded5fc3 --- /dev/null +++ b/internal/service/system/params.go @@ -0,0 +1,224 @@ +package system + +import ( + "strconv" + + "kra/internal/biz/system" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/transport/http" +) + +// ParamsService 系统参数服务 +type ParamsService struct { + uc *system.ParamsUsecase +} + +// NewParamsService 创建系统参数服务 +func NewParamsService(uc *system.ParamsUsecase) *ParamsService { + return &ParamsService{uc: uc} +} + +// ParamsInfo 参数信息 +type ParamsInfo struct { + ID uint `json:"ID"` + Name string `json:"name"` + Key string `json:"key"` + Value string `json:"value"` + Desc string `json:"desc"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` +} + +// CreateParamsRequest 创建参数请求 +type CreateParamsRequest struct { + Name string `json:"name"` + Key string `json:"key"` + Value string `json:"value"` + Desc string `json:"desc"` +} + +// UpdateParamsRequest 更新参数请求 +type UpdateParamsRequest struct { + ID uint `json:"ID"` + Name string `json:"name"` + Key string `json:"key"` + Value string `json:"value"` + Desc string `json:"desc"` +} + +// GetParamsListRequest 获取参数列表请求 +type GetParamsListRequest struct { + Page int `json:"page" form:"page"` + PageSize int `json:"pageSize" form:"pageSize"` + Name string `json:"name" form:"name"` + Key string `json:"key" form:"key"` +} + +// RegisterRoutes 注册路由 +func (s *ParamsService) RegisterRoutes(rg *RouterGroup) { + // 写操作(带操作记录) + rg.PrivateWithRecord.POST("/sysParams/createSysParams", s.handleCreateParams) + rg.PrivateWithRecord.DELETE("/sysParams/deleteSysParams", s.handleDeleteParams) + rg.PrivateWithRecord.DELETE("/sysParams/deleteSysParamsByIds", s.handleDeleteParamsByIds) + rg.PrivateWithRecord.PUT("/sysParams/updateSysParams", s.handleUpdateParams) + + // 读操作(不记录) + rg.Private.GET("/sysParams/findSysParams", s.handleFindParams) + rg.Private.GET("/sysParams/getSysParamsList", s.handleGetParamsList) + rg.Private.GET("/sysParams/getSysParam", s.handleGetParamByKey) +} + +// handleCreateParams 创建参数 +func (s *ParamsService) handleCreateParams(ctx http.Context) error { + var req CreateParamsRequest + if err := ctx.Bind(&req); err != nil { + return err + } + params := &system.Params{ + Name: req.Name, + Key: req.Key, + Value: req.Value, + Desc: req.Desc, + } + if err := s.uc.Create(ctx, params); err != nil { + return errors.BadRequest("CREATE_FAILED", "创建失败:"+err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "创建成功"}) +} + +// handleDeleteParams 删除参数 +func (s *ParamsService) handleDeleteParams(ctx http.Context) error { + idStr := ctx.Query().Get("ID") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return errors.BadRequest("INVALID_ID", "无效的ID") + } + if err := s.uc.Delete(ctx, uint(id)); err != nil { + return errors.BadRequest("DELETE_FAILED", "删除失败:"+err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "删除成功"}) +} + +// handleDeleteParamsByIds 批量删除参数 +func (s *ParamsService) handleDeleteParamsByIds(ctx http.Context) error { + idsStr := ctx.Query()["IDs[]"] + if len(idsStr) == 0 { + return errors.BadRequest("INVALID_IDS", "无效的IDs") + } + ids := make([]uint, 0, len(idsStr)) + for _, idStr := range idsStr { + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + continue + } + ids = append(ids, uint(id)) + } + if err := s.uc.DeleteByIds(ctx, ids); err != nil { + return errors.BadRequest("DELETE_FAILED", "批量删除失败:"+err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "批量删除成功"}) +} + +// handleUpdateParams 更新参数 +func (s *ParamsService) handleUpdateParams(ctx http.Context) error { + var req UpdateParamsRequest + if err := ctx.Bind(&req); err != nil { + return err + } + params := &system.Params{ + ID: req.ID, + Name: req.Name, + Key: req.Key, + Value: req.Value, + Desc: req.Desc, + } + if err := s.uc.Update(ctx, params); err != nil { + return errors.BadRequest("UPDATE_FAILED", "更新失败:"+err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "更新成功"}) +} + +// handleFindParams 根据ID查询参数 +func (s *ParamsService) handleFindParams(ctx http.Context) error { + idStr := ctx.Query().Get("ID") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return errors.BadRequest("INVALID_ID", "无效的ID") + } + params, err := s.uc.GetByID(ctx, uint(id)) + if err != nil { + return errors.NotFound("NOT_FOUND", "查询失败:"+err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "查询成功", "data": toParamsInfo(params)}) +} + +// handleGetParamsList 分页获取参数列表 +func (s *ParamsService) handleGetParamsList(ctx http.Context) error { + var req GetParamsListRequest + if err := ctx.BindQuery(&req); err != nil { + return err + } + if req.Page <= 0 { + req.Page = 1 + } + if req.PageSize <= 0 { + req.PageSize = 10 + } + filters := make(map[string]interface{}) + if req.Name != "" { + filters["name"] = req.Name + } + if req.Key != "" { + filters["key"] = req.Key + } + list, total, err := s.uc.GetList(ctx, req.Page, req.PageSize, filters) + if err != nil { + return errors.InternalServer("LIST_ERROR", "获取失败:"+err.Error()) + } + result := make([]ParamsInfo, len(list)) + for i, p := range list { + result[i] = toParamsInfo(p) + } + return ctx.Result(200, map[string]any{ + "code": 0, + "msg": "获取成功", + "data": map[string]any{ + "list": result, + "total": total, + "page": req.Page, + "pageSize": req.PageSize, + }, + }) +} + +// handleGetParamByKey 根据key获取参数 +func (s *ParamsService) handleGetParamByKey(ctx http.Context) error { + key := ctx.Query().Get("key") + if key == "" { + return errors.BadRequest("INVALID_KEY", "无效的key") + } + params, err := s.uc.GetByKey(ctx, key) + if err != nil { + return errors.NotFound("NOT_FOUND", "获取失败:"+err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": toParamsInfo(params)}) +} + +// toParamsInfo 转换为ParamsInfo +func toParamsInfo(p *system.Params) ParamsInfo { + info := ParamsInfo{ + ID: p.ID, + Name: p.Name, + Key: p.Key, + Value: p.Value, + Desc: p.Desc, + } + if !p.CreatedAt.IsZero() { + info.CreatedAt = p.CreatedAt.Format("2006-01-02 15:04:05") + } + if !p.UpdatedAt.IsZero() { + info.UpdatedAt = p.UpdatedAt.Format("2006-01-02 15:04:05") + } + return info +} diff --git a/internal/service/system/system_config.go b/internal/service/system/system_config.go new file mode 100644 index 0000000..b9b7b15 --- /dev/null +++ b/internal/service/system/system_config.go @@ -0,0 +1,40 @@ +package system + +import ( + "kra/pkg/server" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/transport/http" +) + +// SystemConfigService 系统配置服务 +type SystemConfigService struct { + diskMountPoints []string +} + +// NewSystemConfigService 创建系统配置服务 +func NewSystemConfigService() *SystemConfigService { + // 默认磁盘挂载点,可以从配置中读取 + return &SystemConfigService{ + diskMountPoints: []string{"/"}, + } +} + +// RegisterRoutes 注册路由 +func (s *SystemConfigService) RegisterRoutes(rg *RouterGroup) { + // 读操作(不记录) + rg.Private.POST("/system/getServerInfo", s.handleGetServerInfo) +} + +// handleGetServerInfo 获取服务器信息 +func (s *SystemConfigService) handleGetServerInfo(ctx http.Context) error { + serverInfo, err := server.GetServerInfo(s.diskMountPoints) + if err != nil { + return errors.InternalServer("GET_SERVER_INFO_FAILED", "获取失败:"+err.Error()) + } + return ctx.Result(200, map[string]any{ + "code": 0, + "msg": "获取成功", + "data": map[string]any{"server": serverInfo}, + }) +} diff --git a/internal/service/system/user.go b/internal/service/system/user.go index 30176c9..e935392 100644 --- a/internal/service/system/user.go +++ b/internal/service/system/user.go @@ -1,18 +1,24 @@ package system import ( - "context" "encoding/json" - "fmt" "kra/internal/biz/system" "kra/internal/server/middleware" + types "kra/internal/service/types/system" "kra/pkg/jwt" "github.com/go-kratos/kratos/v2/errors" - "github.com/go-kratos/kratos/v2/transport/http" + khttp "github.com/go-kratos/kratos/v2/transport/http" ) +// RouterGroup 路由组接口 +type RouterGroup struct { + Public *khttp.Router // 公开路由(无需认证) + Private *khttp.Router // 私有路由(需要JWT认证) + PrivateWithRecord *khttp.Router // 私有路由 + 操作记录 +} + // UserService 用户服务 type UserService struct { uc *system.UserUsecase @@ -24,93 +30,43 @@ func NewUserService(uc *system.UserUsecase, jwtPkg *jwt.JWT) *UserService { return &UserService{uc: uc, jwtPkg: jwtPkg} } -// LoginRequest 登录请求 -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` - Captcha string `json:"captcha"` - CaptchaId string `json:"captchaId"` -} - -// LoginResponse 登录响应 -type LoginResponse struct { - User *UserInfo `json:"user"` - Token string `json:"token"` - ExpiresAt int64 `json:"expiresAt"` -} - -// UserInfo 用户信息 -type UserInfo struct { - ID uint `json:"id"` - UUID string `json:"uuid"` - Username string `json:"username"` - NickName string `json:"nickName"` - SideMode string `json:"sideMode"` - HeaderImg string `json:"headerImg"` - BaseColor string `json:"baseColor"` - AuthorityId uint `json:"authorityId"` - Phone string `json:"phone"` - Email string `json:"email"` - Enable int `json:"enable"` - OriginSetting json.RawMessage `json:"originSetting,omitempty"` - Authority *AuthorityInfo `json:"authority,omitempty"` - Authorities []*AuthorityInfo `json:"authorities,omitempty"` -} - -// AuthorityInfo 角色信息 -type AuthorityInfo struct { - AuthorityId uint `json:"authorityId"` - AuthorityName string `json:"authorityName"` - ParentId *uint `json:"parentId,omitempty"` - DefaultRouter string `json:"defaultRouter"` -} - // Login 用户登录 -func (s *UserService) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) { +func (s *UserService) Login(ctx khttp.Context) error { + var req types.LoginRequest + if err := ctx.Bind(&req); err != nil { + return err + } user, err := s.uc.Login(ctx, req.Username, req.Password) if err != nil { - return nil, errors.Unauthorized("LOGIN_FAILED", err.Error()) + return errors.Unauthorized("LOGIN_FAILED", err.Error()) } - if user.Enable != 1 { - return nil, errors.Forbidden("USER_DISABLED", "用户被禁止登录") + return errors.Forbidden("USER_DISABLED", "用户被禁止登录") } - - // 生成 JWT token claims := s.jwtPkg.CreateClaims(jwt.BaseClaims{ UUID: user.UUID.String(), - ID: uint(user.ID), + ID: user.ID, Username: user.Username, NickName: user.NickName, AuthorityID: user.AuthorityId, }) token, err := s.jwtPkg.CreateToken(claims.BaseClaims) if err != nil { - return nil, errors.InternalServer("TOKEN_ERROR", "生成token失败") + return errors.InternalServer("TOKEN_ERROR", "生成token失败") } - - return &LoginResponse{ - User: toUserInfo(user), + return ctx.Result(200, &types.LoginReply{ + User: toTypesUserInfo(user), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), - }, nil -} - -// RegisterRequest 注册请求 -type RegisterRequest struct { - Username string `json:"username"` - Password string `json:"password"` - NickName string `json:"nickName"` - HeaderImg string `json:"headerImg"` - AuthorityId uint `json:"authorityId"` - AuthorityIds []uint `json:"authorityIds"` - Phone string `json:"phone"` - Email string `json:"email"` - Enable int `json:"enable"` + }) } // Register 用户注册 -func (s *UserService) Register(ctx context.Context, req *RegisterRequest) (*UserInfo, error) { +func (s *UserService) Register(ctx khttp.Context) error { + var req types.RegisterRequest + if err := ctx.Bind(&req); err != nil { + return err + } user := &system.User{ Username: req.Username, Password: req.Password, @@ -121,66 +77,60 @@ func (s *UserService) Register(ctx context.Context, req *RegisterRequest) (*User Email: req.Email, Enable: req.Enable, } - created, err := s.uc.Register(ctx, user) if err != nil { - return nil, errors.BadRequest("REGISTER_FAILED", err.Error()) + return errors.BadRequest("REGISTER_FAILED", err.Error()) } - - return toUserInfo(created), nil -} - -// ChangePasswordRequest 修改密码请求 -type ChangePasswordRequest struct { - Password string `json:"password"` - NewPassword string `json:"newPassword"` + return ctx.Result(200, &types.UserInfoReply{User: toTypesUserInfo(created)}) } // ChangePassword 修改密码 -func (s *UserService) ChangePassword(ctx context.Context, userID uint, req *ChangePasswordRequest) error { - return s.uc.ChangePassword(ctx, userID, req.Password, req.NewPassword) -} - -// ResetPasswordRequest 重置密码请求 -type ResetPasswordRequest struct { - ID uint `json:"id"` - Password string `json:"password"` +func (s *UserService) ChangePassword(ctx khttp.Context) error { + var req types.ChangePasswordRequest + if err := ctx.Bind(&req); err != nil { + return err + } + userID := middleware.GetUserID(ctx) + if userID == 0 { + return errors.Unauthorized("UNAUTHORIZED", "请先登录") + } + if err := s.uc.ChangePassword(ctx, userID, req.Password, req.NewPassword); err != nil { + return errors.BadRequest("CHANGE_PASSWORD_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // ResetPassword 重置密码 -func (s *UserService) ResetPassword(ctx context.Context, req *ResetPasswordRequest) error { - return s.uc.ResetPassword(ctx, req.ID, req.Password) +func (s *UserService) ResetPassword(ctx khttp.Context) error { + var req types.ResetPasswordRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.uc.ResetPassword(ctx, req.ID, req.Password); err != nil { + return errors.BadRequest("RESET_PASSWORD_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // GetUserInfo 获取用户信息 -func (s *UserService) GetUserInfo(ctx context.Context, uuid string) (*UserInfo, error) { - user, err := s.uc.GetUserInfo(ctx, uuid) - if err != nil { - return nil, errors.NotFound("USER_NOT_FOUND", err.Error()) +func (s *UserService) GetUserInfo(ctx khttp.Context) error { + claims, ok := middleware.GetClaims(ctx) + if !ok { + return errors.Unauthorized("UNAUTHORIZED", "请先登录") } - return toUserInfo(user), nil -} - -// GetUserListRequest 获取用户列表请求 -type GetUserListRequest struct { - Page int `json:"page"` - PageSize int `json:"pageSize"` - Username string `json:"username"` - NickName string `json:"nickName"` - Phone string `json:"phone"` - Email string `json:"email"` -} - -// GetUserListResponse 获取用户列表响应 -type GetUserListResponse struct { - List []*UserInfo `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"pageSize"` + user, err := s.uc.GetUserInfo(ctx, claims.UUID) + if err != nil { + return errors.NotFound("USER_NOT_FOUND", err.Error()) + } + return ctx.Result(200, &types.GetUserInfoReply{UserInfo: toTypesUserInfo(user)}) } // GetUserList 获取用户列表 -func (s *UserService) GetUserList(ctx context.Context, req *GetUserListRequest) (*GetUserListResponse, error) { +func (s *UserService) GetUserList(ctx khttp.Context) error { + var req types.GetUserListRequest + if err := ctx.Bind(&req); err != nil { + return err + } filters := make(map[string]string) if req.Username != "" { filters["username"] = req.Username @@ -194,45 +144,23 @@ func (s *UserService) GetUserList(ctx context.Context, req *GetUserListRequest) if req.Email != "" { filters["email"] = req.Email } - users, total, err := s.uc.GetUserList(ctx, req.Page, req.PageSize, filters) if err != nil { - return nil, errors.InternalServer("LIST_ERROR", err.Error()) + return errors.InternalServer("LIST_ERROR", err.Error()) } - - list := make([]*UserInfo, len(users)) + list := make([]types.UserInfo, len(users)) for i, u := range users { - list[i] = toUserInfo(u) + list[i] = toTypesUserInfo(u) } - - return &GetUserListResponse{ - List: list, - Total: total, - Page: req.Page, - PageSize: req.PageSize, - }, nil -} - -// SetUserInfoRequest 设置用户信息请求(管理员用) -type SetUserInfoRequest struct { - ID uint `json:"id"` - NickName string `json:"nickName"` - HeaderImg string `json:"headerImg"` - Phone string `json:"phone"` - Email string `json:"email"` - Enable int `json:"enable"` - AuthorityIds []uint `json:"authorityIds"` + return ctx.Result(200, &types.GetUserListReply{List: list, Total: total, Page: req.Page, PageSize: req.PageSize}) } // SetUserInfo 设置用户信息 -func (s *UserService) SetUserInfo(ctx context.Context, adminAuthorityID uint, req *SetUserInfoRequest) error { - // 如果提供了AuthorityIds,先设置用户角色(与GVA保持一致) - if len(req.AuthorityIds) > 0 { - if err := s.uc.SetUserAuthorities(ctx, adminAuthorityID, req.ID, req.AuthorityIds); err != nil { - return err - } +func (s *UserService) SetUserInfo(ctx khttp.Context) error { + var req types.SetUserInfoRequest + if err := ctx.Bind(&req); err != nil { + return err } - user := &system.User{ ID: req.ID, NickName: req.NickName, @@ -241,360 +169,177 @@ func (s *UserService) SetUserInfo(ctx context.Context, adminAuthorityID uint, re Email: req.Email, Enable: req.Enable, } - return s.uc.SetUserInfo(ctx, user) + if err := s.uc.SetUserInfo(ctx, user); err != nil { + return errors.BadRequest("SET_USER_INFO_FAILED", err.Error()) + } + if len(req.AuthorityIds) > 0 { + adminAuthorityID := middleware.GetAuthorityID(ctx) + if err := s.uc.SetUserAuthorities(ctx, adminAuthorityID, req.ID, req.AuthorityIds); err != nil { + return errors.BadRequest("SET_USER_AUTHORITIES_FAILED", err.Error()) + } + } + return ctx.Result(200, nil) } -// SetSelfInfoRequest 设置自己信息请求(用户用) -type SetSelfInfoRequest struct { - NickName string `json:"nickName"` - HeaderImg string `json:"headerImg"` - Phone string `json:"phone"` - Email string `json:"email"` - SideMode string `json:"sideMode"` - BaseColor string `json:"baseColor"` -} - -// SetSelfInfo 设置自己的信息 -func (s *UserService) SetSelfInfo(ctx context.Context, userID uint, req *SetSelfInfoRequest) error { +// SetSelfInfo 设置自己信息 +func (s *UserService) SetSelfInfo(ctx khttp.Context) error { + var req types.SetSelfInfoRequest + if err := ctx.Bind(&req); err != nil { + return err + } + userID := middleware.GetUserID(ctx) + if userID == 0 { + return errors.Unauthorized("UNAUTHORIZED", "请先登录") + } user := &system.User{ ID: userID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, - SideMode: req.SideMode, - BaseColor: req.BaseColor, } - return s.uc.SetSelfInfo(ctx, user) + if err := s.uc.SetSelfInfo(ctx, user); err != nil { + return errors.BadRequest("SET_SELF_INFO_FAILED", err.Error()) + } + return ctx.Result(200, nil) } -// SetSelfSetting 设置自己的配置 -func (s *UserService) SetSelfSetting(ctx context.Context, userID uint, setting json.RawMessage) error { - return s.uc.SetSelfSetting(ctx, userID, setting) +// SetSelfSetting 设置自己配置 +func (s *UserService) SetSelfSetting(ctx khttp.Context) error { + var req types.SetSelfSettingRequest + if err := ctx.Bind(&req); err != nil { + return err + } + userID := middleware.GetUserID(ctx) + if userID == 0 { + return errors.Unauthorized("UNAUTHORIZED", "请先登录") + } + setting := json.RawMessage(req.Setting) + if err := s.uc.SetSelfSetting(ctx, userID, setting); err != nil { + return errors.BadRequest("SET_SELF_SETTING_FAILED", err.Error()) + } + return ctx.Result(200, nil) } // DeleteUser 删除用户 -func (s *UserService) DeleteUser(ctx context.Context, id uint) error { - return s.uc.DeleteUser(ctx, id) -} - -// SetUserAuthorityRequest 设置用户角色请求 -type SetUserAuthorityRequest struct { - AuthorityId uint `json:"authorityId"` -} - -// SetUserAuthorityResponse 设置用户角色响应 -type SetUserAuthorityResponse struct { - Token string `json:"token"` - ExpiresAt int64 `json:"expiresAt"` -} - -// SetUserAuthority 设置用户角色(切换角色)- 返回新token -func (s *UserService) SetUserAuthority(ctx context.Context, userID uint, uuid string, username string, nickName string, req *SetUserAuthorityRequest) (*SetUserAuthorityResponse, error) { - if err := s.uc.SetUserAuthority(ctx, userID, req.AuthorityId); err != nil { - return nil, err +func (s *UserService) DeleteUser(ctx khttp.Context) error { + var req types.DeleteUserRequest + if err := ctx.Bind(&req); err != nil { + return err } + currentUserID := middleware.GetUserID(ctx) + if currentUserID == req.ID { + return errors.BadRequest("DELETE_FAILED", "无法删除自己") + } + if err := s.uc.DeleteUser(ctx, req.ID); err != nil { + return errors.BadRequest("DELETE_FAILED", err.Error()) + } + return ctx.Result(200, nil) +} - // 生成新的JWT token(与GVA保持一致) - claims := s.jwtPkg.CreateClaims(jwt.BaseClaims{ - UUID: uuid, - ID: userID, - Username: username, - NickName: nickName, - AuthorityID: req.AuthorityId, - }) +// SetUserAuthority 设置用户角色 +func (s *UserService) SetUserAuthority(ctx khttp.Context) error { + var req types.SetUserAuthorityRequest + if err := ctx.Bind(&req); err != nil { + return err + } + userID := middleware.GetUserID(ctx) + if userID == 0 { + return errors.Unauthorized("UNAUTHORIZED", "请先登录") + } + if err := s.uc.SetUserAuthority(ctx, userID, req.AuthorityId); err != nil { + return errors.BadRequest("SET_AUTHORITY_FAILED", err.Error()) + } + // 重新生成token + claims, ok := middleware.GetClaims(ctx) + if !ok { + return errors.Unauthorized("UNAUTHORIZED", "请先登录") + } + claims.AuthorityID = req.AuthorityId token, err := s.jwtPkg.CreateToken(claims.BaseClaims) if err != nil { - return nil, errors.InternalServer("TOKEN_ERROR", "生成token失败") + return errors.InternalServer("TOKEN_ERROR", "生成token失败") } - - return &SetUserAuthorityResponse{ - Token: token, - ExpiresAt: claims.ExpiresAt.UnixMilli(), - }, nil -} - -// SetUserAuthoritiesRequest 设置用户多角色请求 -type SetUserAuthoritiesRequest struct { - ID uint `json:"id"` - AuthorityIds []uint `json:"authorityIds"` + return ctx.Result(200, &types.SetUserAuthorityReply{Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli()}) } // SetUserAuthorities 设置用户多角色 -func (s *UserService) SetUserAuthorities(ctx context.Context, adminAuthorityID uint, req *SetUserAuthoritiesRequest) error { - return s.uc.SetUserAuthorities(ctx, adminAuthorityID, req.ID, req.AuthorityIds) +func (s *UserService) SetUserAuthorities(ctx khttp.Context) error { + var req types.SetUserAuthoritiesRequest + if err := ctx.Bind(&req); err != nil { + return err + } + adminAuthorityID := middleware.GetAuthorityID(ctx) + if err := s.uc.SetUserAuthorities(ctx, adminAuthorityID, req.ID, req.AuthorityIds); err != nil { + return errors.BadRequest("SET_AUTHORITIES_FAILED", err.Error()) + } + return ctx.Result(200, nil) } -// 转换函数 -func toUserInfo(u *system.User) *UserInfo { - info := &UserInfo{ - ID: u.ID, - UUID: u.UUID.String(), - Username: u.Username, - NickName: u.NickName, - SideMode: u.SideMode, - HeaderImg: u.HeaderImg, - BaseColor: u.BaseColor, - AuthorityId: u.AuthorityId, - Phone: u.Phone, - Email: u.Email, - Enable: u.Enable, - OriginSetting: u.OriginSetting, +// toTypesUserInfo 转换用户信息为types格式 +func toTypesUserInfo(user *system.User) types.UserInfo { + if user == nil { + return types.UserInfo{} } - - if u.Authority != nil { - info.Authority = &AuthorityInfo{ - AuthorityId: u.Authority.AuthorityId, - AuthorityName: u.Authority.AuthorityName, - ParentId: u.Authority.ParentId, - DefaultRouter: u.Authority.DefaultRouter, + info := types.UserInfo{ + ID: user.ID, + UUID: user.UUID.String(), + Username: user.Username, + NickName: user.NickName, + SideMode: user.SideMode, + HeaderImg: user.HeaderImg, + BaseColor: user.BaseColor, + AuthorityId: user.AuthorityId, + Phone: user.Phone, + Email: user.Email, + Enable: user.Enable, + } + if user.OriginSetting != nil { + info.OriginSetting = string(user.OriginSetting) + } + if user.Authority != nil { + auth := toTypesAuthorityInfo(user.Authority) + info.Authority = &auth + } + if len(user.Authorities) > 0 { + info.Authorities = make([]types.AuthorityInfo, len(user.Authorities)) + for i, a := range user.Authorities { + info.Authorities[i] = toTypesAuthorityInfo(a) } } - - if len(u.Authorities) > 0 { - info.Authorities = make([]*AuthorityInfo, len(u.Authorities)) - for i, a := range u.Authorities { - info.Authorities[i] = &AuthorityInfo{ - AuthorityId: a.AuthorityId, - AuthorityName: a.AuthorityName, - ParentId: a.ParentId, - DefaultRouter: a.DefaultRouter, - } - } - } - return info } -// RegisterRoutes 注册路由 -func (s *UserService) RegisterRoutes(srv *http.Server) { - r := srv.Route("/") - - // 公开接口 - r.POST("/base/login", s.handleLogin) - - // 需要认证的接口 - r.POST("/user/register", s.handleRegister) - r.POST("/user/changePassword", s.handleChangePassword) - r.POST("/user/resetPassword", s.handleResetPassword) - r.GET("/user/getUserInfo", s.handleGetUserInfo) - r.POST("/user/getUserList", s.handleGetUserList) - r.PUT("/user/setUserInfo", s.handleSetUserInfo) - r.PUT("/user/setSelfInfo", s.handleSetSelfInfo) - r.PUT("/user/setSelfSetting", s.handleSetSelfSetting) - r.DELETE("/user/deleteUser", s.handleDeleteUser) - r.POST("/user/setUserAuthority", s.handleSetUserAuthority) - r.POST("/user/setUserAuthorities", s.handleSetUserAuthorities) +// toTypesAuthorityInfo 转换角色信息为types格式 +func toTypesAuthorityInfo(auth *system.Authority) types.AuthorityInfo { + if auth == nil { + return types.AuthorityInfo{} + } + return types.AuthorityInfo{ + AuthorityId: auth.AuthorityId, + AuthorityName: auth.AuthorityName, + ParentId: auth.ParentId, + DefaultRouter: auth.DefaultRouter, + } } -// HTTP Handlers -func (s *UserService) handleLogin(ctx http.Context) error { - var req LoginRequest - if err := ctx.Bind(&req); err != nil { - return err - } - resp, err := s.Login(ctx, &req) - if err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "登录成功", - "data": resp, - }) -} +// RegisterRoutes 注册用户路由 +func (s *UserService) RegisterRoutes(rg *RouterGroup) { + // 公开路由(无需认证) + rg.Public.POST("/base/login", s.Login) -func (s *UserService) handleRegister(ctx http.Context) error { - var req RegisterRequest - if err := ctx.Bind(&req); err != nil { - return err - } - resp, err := s.Register(ctx, &req) - if err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "注册成功", - "data": resp, - }) -} + // 私有路由 + 操作记录(写操作) + rg.PrivateWithRecord.POST("/user/admin_register", s.Register) + rg.PrivateWithRecord.POST("/user/changePassword", s.ChangePassword) + rg.PrivateWithRecord.POST("/user/setUserAuthority", s.SetUserAuthority) + rg.PrivateWithRecord.DELETE("/user/deleteUser", s.DeleteUser) + rg.PrivateWithRecord.PUT("/user/setUserInfo", s.SetUserInfo) + rg.PrivateWithRecord.PUT("/user/setSelfInfo", s.SetSelfInfo) + rg.PrivateWithRecord.POST("/user/setUserAuthorities", s.SetUserAuthorities) + rg.PrivateWithRecord.POST("/user/resetPassword", s.ResetPassword) + rg.PrivateWithRecord.PUT("/user/setSelfSetting", s.SetSelfSetting) -func (s *UserService) handleChangePassword(ctx http.Context) error { - var req ChangePasswordRequest - if err := ctx.Bind(&req); err != nil { - return err - } - userID := middleware.GetUserID(ctx) - if userID == 0 { - return errors.Unauthorized("UNAUTHORIZED", "请先登录") - } - if err := s.ChangePassword(ctx, userID, &req); err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "修改成功", - }) -} - -func (s *UserService) handleResetPassword(ctx http.Context) error { - var req ResetPasswordRequest - if err := ctx.Bind(&req); err != nil { - return err - } - if err := s.ResetPassword(ctx, &req); err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "重置成功", - }) -} - -func (s *UserService) handleGetUserInfo(ctx http.Context) error { - claims, ok := middleware.GetClaims(ctx) - if !ok { - return errors.Unauthorized("UNAUTHORIZED", "请先登录") - } - resp, err := s.GetUserInfo(ctx, claims.UUID) - if err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "获取成功", - "data": map[string]any{"userInfo": resp}, - }) -} - -func (s *UserService) handleGetUserList(ctx http.Context) error { - var req GetUserListRequest - if err := ctx.Bind(&req); err != nil { - return err - } - resp, err := s.GetUserList(ctx, &req) - if err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "获取成功", - "data": resp, - }) -} - -func (s *UserService) handleSetUserInfo(ctx http.Context) error { - var req SetUserInfoRequest - if err := ctx.Bind(&req); err != nil { - return err - } - adminAuthorityID := middleware.GetAuthorityID(ctx) - if adminAuthorityID == 0 { - return errors.Unauthorized("UNAUTHORIZED", "请先登录") - } - if err := s.SetUserInfo(ctx, adminAuthorityID, &req); err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "设置成功", - }) -} - -func (s *UserService) handleSetSelfInfo(ctx http.Context) error { - var req SetSelfInfoRequest - if err := ctx.Bind(&req); err != nil { - return err - } - userID := middleware.GetUserID(ctx) - if userID == 0 { - return errors.Unauthorized("UNAUTHORIZED", "请先登录") - } - if err := s.SetSelfInfo(ctx, userID, &req); err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "设置成功", - }) -} - -func (s *UserService) handleSetSelfSetting(ctx http.Context) error { - var req json.RawMessage - if err := ctx.Bind(&req); err != nil { - return err - } - userID := middleware.GetUserID(ctx) - if userID == 0 { - return errors.Unauthorized("UNAUTHORIZED", "请先登录") - } - if err := s.SetSelfSetting(ctx, userID, req); err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "设置成功", - }) -} - -func (s *UserService) handleDeleteUser(ctx http.Context) error { - var req struct { - ID uint `json:"id"` - } - if err := ctx.Bind(&req); err != nil { - return err - } - // 不能删除自己 - userID := middleware.GetUserID(ctx) - if userID == req.ID { - return errors.BadRequest("DELETE_SELF", "删除失败, 无法删除自己") - } - if err := s.DeleteUser(ctx, req.ID); err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "删除成功", - }) -} - -func (s *UserService) handleSetUserAuthority(ctx http.Context) error { - var req SetUserAuthorityRequest - if err := ctx.Bind(&req); err != nil { - return err - } - claims, ok := middleware.GetClaims(ctx) - if !ok { - return errors.Unauthorized("UNAUTHORIZED", "请先登录") - } - resp, err := s.SetUserAuthority(ctx, claims.BaseClaims.ID, claims.UUID, claims.Username, claims.NickName, &req) - if err != nil { - return err - } - // 设置响应头(与GVA保持一致) - ctx.Response().Header().Set("new-token", resp.Token) - ctx.Response().Header().Set("new-expires-at", fmt.Sprintf("%d", resp.ExpiresAt/1000)) - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "修改成功", - }) -} - -func (s *UserService) handleSetUserAuthorities(ctx http.Context) error { - var req SetUserAuthoritiesRequest - if err := ctx.Bind(&req); err != nil { - return err - } - adminAuthorityID := middleware.GetAuthorityID(ctx) - if adminAuthorityID == 0 { - return errors.Unauthorized("UNAUTHORIZED", "请先登录") - } - if err := s.SetUserAuthorities(ctx, adminAuthorityID, &req); err != nil { - return err - } - return ctx.Result(200, map[string]any{ - "code": 0, - "msg": "修改成功", - }) + // 私有路由(读操作,不记录) + rg.Private.POST("/user/getUserList", s.GetUserList) + rg.Private.GET("/user/getUserInfo", s.GetUserInfo) } diff --git a/internal/service/types/common/response.go b/internal/service/types/common/response.go new file mode 100644 index 0000000..2adad68 --- /dev/null +++ b/internal/service/types/common/response.go @@ -0,0 +1,22 @@ +package common + +// PageResult 分页响应 +type PageResult struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// IdsRequest 批量ID请求 +type IdsRequest struct { + Ids []uint `json:"ids"` +} + +// IdRequest 单个ID请求 +type IdRequest struct { + ID uint `json:"id"` +} + +// Empty 空响应 +type Empty struct{} diff --git a/internal/service/types/system/api.go b/internal/service/types/system/api.go new file mode 100644 index 0000000..f38f667 --- /dev/null +++ b/internal/service/types/system/api.go @@ -0,0 +1,98 @@ +package system + +// ApiInfo API信息 +type ApiInfo struct { + ID uint `json:"ID"` + Path string `json:"path"` + Description string `json:"description"` + ApiGroup string `json:"apiGroup"` + Method string `json:"method"` +} + +// CreateApiRequest 创建API请求 +type CreateApiRequest struct { + Path string `json:"path" binding:"required"` + Description string `json:"description"` + ApiGroup string `json:"apiGroup"` + Method string `json:"method" binding:"required"` +} + +// UpdateApiRequest 更新API请求 +type UpdateApiRequest struct { + ID uint `json:"ID" binding:"required"` + Path string `json:"path"` + Description string `json:"description"` + ApiGroup string `json:"apiGroup"` + Method string `json:"method"` +} + +// DeleteApiRequest 删除API请求 +type DeleteApiRequest struct { + ID uint `json:"ID" binding:"required"` +} + +// DeleteApisByIdsRequest 批量删除API请求 +type DeleteApisByIdsRequest struct { + Ids []uint `json:"ids" binding:"required"` +} + +// GetApiByIdRequest 根据ID获取API请求 +type GetApiByIdRequest struct { + ID uint `json:"ID" binding:"required"` +} + +// GetApiByIdReply 根据ID获取API响应 +type GetApiByIdReply struct { + Api ApiInfo `json:"api"` +} + +// GetApiListRequest 获取API列表请求 +type GetApiListRequest struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + Path string `json:"path"` + Description string `json:"description"` + Method string `json:"method"` + ApiGroup string `json:"apiGroup"` + OrderKey string `json:"orderKey"` + Desc bool `json:"desc"` +} + +// GetApiListReply 获取API列表响应 +type GetApiListReply struct { + List []ApiInfo `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// GetAllApisReply 获取所有API响应 +type GetAllApisReply struct { + Apis []ApiInfo `json:"apis"` +} + +// GetApiGroupsReply 获取API分组响应 +type GetApiGroupsReply struct { + Groups []string `json:"groups"` + ApiGroupMap map[string]string `json:"apiGroupMap"` +} + +// SyncApiReply 同步API响应 +type SyncApiReply struct { + NewApis []ApiInfo `json:"newApis"` + DeleteApis []ApiInfo `json:"deleteApis"` + IgnoreApis []ApiInfo `json:"ignoreApis"` +} + +// IgnoreApiRequest 忽略API请求 +type IgnoreApiRequest struct { + Path string `json:"path" binding:"required"` + Method string `json:"method" binding:"required"` + Flag bool `json:"flag"` +} + +// EnterSyncApiRequest 确认同步API请求 +type EnterSyncApiRequest struct { + NewApis []ApiInfo `json:"newApis"` + DeleteApis []ApiInfo `json:"deleteApis"` +} diff --git a/internal/service/types/system/user.go b/internal/service/types/system/user.go new file mode 100644 index 0000000..32359e6 --- /dev/null +++ b/internal/service/types/system/user.go @@ -0,0 +1,143 @@ +package system + +// UserInfo 用户信息 +type UserInfo struct { + ID uint `json:"id"` + UUID string `json:"uuid"` + Username string `json:"userName"` + NickName string `json:"nickName"` + SideMode string `json:"sideMode"` + HeaderImg string `json:"headerImg"` + BaseColor string `json:"baseColor"` + AuthorityId uint `json:"authorityId"` + Phone string `json:"phone"` + Email string `json:"email"` + Enable int `json:"enable"` + OriginSetting string `json:"originSetting,omitempty"` + Authority *AuthorityInfo `json:"authority,omitempty"` + Authorities []AuthorityInfo `json:"authorities,omitempty"` +} + +// AuthorityInfo 角色信息 +type AuthorityInfo struct { + AuthorityId uint `json:"authorityId"` + AuthorityName string `json:"authorityName"` + ParentId *uint `json:"parentId,omitempty"` + DefaultRouter string `json:"defaultRouter"` +} + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Captcha string `json:"captcha"` + CaptchaId string `json:"captchaId"` +} + +// LoginReply 登录响应 +type LoginReply struct { + User UserInfo `json:"user"` + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` +} + +// RegisterRequest 注册请求 +type RegisterRequest struct { + Username string `json:"userName" binding:"required"` + Password string `json:"passWord" binding:"required"` + NickName string `json:"nickName"` + HeaderImg string `json:"headerImg"` + AuthorityId uint `json:"authorityId"` + AuthorityIds []uint `json:"authorityIds"` + Phone string `json:"phone"` + Email string `json:"email"` + Enable int `json:"enable"` +} + +// UserInfoReply 用户信息响应 +type UserInfoReply struct { + User UserInfo `json:"user"` +} + +// ChangePasswordRequest 修改密码请求 +type ChangePasswordRequest struct { + Password string `json:"password" binding:"required"` + NewPassword string `json:"newPassword" binding:"required"` +} + +// ResetPasswordRequest 重置密码请求 +type ResetPasswordRequest struct { + ID uint `json:"ID" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// GetUserInfoReply 获取用户信息响应 +type GetUserInfoReply struct { + UserInfo UserInfo `json:"userInfo"` +} + +// GetUserListRequest 获取用户列表请求 +type GetUserListRequest struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + Username string `json:"username"` + NickName string `json:"nickName"` + Phone string `json:"phone"` + Email string `json:"email"` +} + +// GetUserListReply 获取用户列表响应 +type GetUserListReply struct { + List []UserInfo `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// SetUserInfoRequest 设置用户信息请求 +type SetUserInfoRequest struct { + ID uint `json:"ID" binding:"required"` + NickName string `json:"nickName"` + HeaderImg string `json:"headerImg"` + Phone string `json:"phone"` + Email string `json:"email"` + Enable int `json:"enable"` + AuthorityIds []uint `json:"authorityIds"` +} + +// SetSelfInfoRequest 设置自己信息请求 +type SetSelfInfoRequest struct { + NickName string `json:"nickName"` + HeaderImg string `json:"headerImg"` + Phone string `json:"phone"` + Email string `json:"email"` + SideMode string `json:"sideMode"` + BaseColor string `json:"baseColor"` +} + +// SetSelfSettingRequest 设置自己配置请求 +type SetSelfSettingRequest struct { + Setting string `json:"setting"` +} + +// DeleteUserRequest 删除用户请求 +type DeleteUserRequest struct { + ID uint `json:"ID" binding:"required"` +} + +// SetUserAuthorityRequest 设置用户角色请求 +type SetUserAuthorityRequest struct { + AuthorityId uint `json:"authorityId" binding:"required"` +} + +// SetUserAuthorityReply 设置用户角色响应 +type SetUserAuthorityReply struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` +} + +// SetUserAuthoritiesRequest 设置用户多角色请求 +type SetUserAuthoritiesRequest struct { + ID uint `json:"ID" binding:"required"` + AuthorityIds []uint `json:"authorityIds" binding:"required"` +} diff --git a/pkg/jwt/jwt.go b/pkg/jwt/jwt.go index 3e96c0c..9b22c0d 100644 --- a/pkg/jwt/jwt.go +++ b/pkg/jwt/jwt.go @@ -120,3 +120,16 @@ func (j *JWT) CreateTokenByOldToken(oldToken string, oldClaims CustomClaims) (st } return newToken, &oldClaims, nil } + +// NewJWTFromConfig 从配置创建JWT实例(内部使用) +func NewJWTFromConfig(signingKey, issuer, expiresTime, bufferTime string) *JWT { + expires, _ := time.ParseDuration(expiresTime) + if expires == 0 { + expires = 7 * 24 * time.Hour // 默认7天 + } + buffer, _ := time.ParseDuration(bufferTime) + if buffer == 0 { + buffer = 24 * time.Hour // 默认1天 + } + return NewJWT(signingKey, issuer, expires, buffer) +} diff --git a/pkg/jwt/provider.go b/pkg/jwt/provider.go new file mode 100644 index 0000000..3cc9662 --- /dev/null +++ b/pkg/jwt/provider.go @@ -0,0 +1,15 @@ +package jwt + +import ( + "kra/internal/conf" + + "github.com/google/wire" +) + +// NewJWTFromConf 从conf.JWT配置创建JWT实例(用于wire注入) +func NewJWTFromConf(c *conf.JWT) *JWT { + return NewJWTFromConfig(c.SigningKey, c.Issuer, c.ExpiresTime, c.BufferTime) +} + +// ProviderSet is jwt providers for wire. +var ProviderSet = wire.NewSet(NewJWTFromConf) diff --git a/pkg/response/response.go b/pkg/response/response.go new file mode 100644 index 0000000..346a447 --- /dev/null +++ b/pkg/response/response.go @@ -0,0 +1,47 @@ +package response + +import ( + "encoding/json" + "net/http" + + "github.com/go-kratos/kratos/v2/errors" +) + +// Response 统一响应格式 +type Response struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} + +// Encoder 响应编码器,包装响应为 {code, msg, data} 格式 +func Encoder(w http.ResponseWriter, r *http.Request, v interface{}) error { + if v == nil { + v = struct{}{} + } + reply := Response{ + Code: 0, + Msg: "success", + Data: v, + } + w.Header().Set("Content-Type", "application/json") + return json.NewEncoder(w).Encode(reply) +} + +// ErrorEncoder 错误编码器,统一错误响应格式 +func ErrorEncoder(w http.ResponseWriter, r *http.Request, err error) { + se := errors.FromError(err) + code := int(se.Code) + if code >= 400 && code < 500 { + code = 7 + } else if code >= 500 { + code = 7 + } + reply := Response{ + Code: code, + Msg: se.Message, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(int(se.Code)) + json.NewEncoder(w).Encode(reply) +} diff --git a/pkg/server/info.go b/pkg/server/info.go new file mode 100644 index 0000000..e75f95b --- /dev/null +++ b/pkg/server/info.go @@ -0,0 +1,142 @@ +package server + +import ( + "runtime" + "time" + + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" +) + +const ( + B = 1 + KB = 1024 * B + MB = 1024 * KB + GB = 1024 * MB +) + +// Server 服务器信息 +type Server struct { + Os Os `json:"os"` + Cpu Cpu `json:"cpu"` + Ram Ram `json:"ram"` + Disk []Disk `json:"disk"` +} + +// Os 操作系统信息 +type Os struct { + GOOS string `json:"goos"` + NumCPU int `json:"numCpu"` + Compiler string `json:"compiler"` + GoVersion string `json:"goVersion"` + NumGoroutine int `json:"numGoroutine"` +} + +// Cpu CPU信息 +type Cpu struct { + Cpus []float64 `json:"cpus"` + Cores int `json:"cores"` +} + +// Ram 内存信息 +type Ram struct { + UsedMB int `json:"usedMb"` + TotalMB int `json:"totalMb"` + UsedPercent int `json:"usedPercent"` +} + +// Disk 磁盘信息 +type Disk struct { + MountPoint string `json:"mountPoint"` + UsedMB int `json:"usedMb"` + UsedGB int `json:"usedGb"` + TotalMB int `json:"totalMb"` + TotalGB int `json:"totalGb"` + UsedPercent int `json:"usedPercent"` +} + +// InitOS 获取操作系统信息 +func InitOS() Os { + return Os{ + GOOS: runtime.GOOS, + NumCPU: runtime.NumCPU(), + Compiler: runtime.Compiler, + GoVersion: runtime.Version(), + NumGoroutine: runtime.NumGoroutine(), + } +} + +// InitCPU 获取CPU信息 +func InitCPU() (Cpu, error) { + var c Cpu + cores, err := cpu.Counts(false) + if err != nil { + return c, err + } + c.Cores = cores + + cpus, err := cpu.Percent(time.Duration(200)*time.Millisecond, true) + if err != nil { + return c, err + } + c.Cpus = cpus + return c, nil +} + +// InitRAM 获取内存信息 +func InitRAM() (Ram, error) { + var r Ram + u, err := mem.VirtualMemory() + if err != nil { + return r, err + } + r.UsedMB = int(u.Used) / MB + r.TotalMB = int(u.Total) / MB + r.UsedPercent = int(u.UsedPercent) + return r, nil +} + +// InitDisk 获取磁盘信息 +func InitDisk(mountPoints []string) ([]Disk, error) { + var d []Disk + for _, mp := range mountPoints { + u, err := disk.Usage(mp) + if err != nil { + return d, err + } + d = append(d, Disk{ + MountPoint: mp, + UsedMB: int(u.Used) / MB, + UsedGB: int(u.Used) / GB, + TotalMB: int(u.Total) / MB, + TotalGB: int(u.Total) / GB, + UsedPercent: int(u.UsedPercent), + }) + } + return d, nil +} + +// GetServerInfo 获取服务器完整信息 +func GetServerInfo(diskMountPoints []string) (*Server, error) { + var s Server + var err error + + s.Os = InitOS() + + if s.Cpu, err = InitCPU(); err != nil { + return &s, err + } + + if s.Ram, err = InitRAM(); err != nil { + return &s, err + } + + if len(diskMountPoints) > 0 { + if s.Disk, err = InitDisk(diskMountPoints); err != nil { + return &s, err + } + } + + return &s, nil +} diff --git a/web/config/config.ts b/web/config/config.ts index 1a98ebe..40e29df 100644 --- a/web/config/config.ts +++ b/web/config/config.ts @@ -168,7 +168,6 @@ export default defineConfig({ mock: { include: ["mock/**/*", "src/pages/**/_mock.ts"], }, - utoopack: {}, requestRecord: {}, exportStatic: {}, define: { diff --git a/web/config/proxy.ts b/web/config/proxy.ts index c7ddc88..854f271 100644 --- a/web/config/proxy.ts +++ b/web/config/proxy.ts @@ -15,9 +15,10 @@ export default { * @doc https://github.com/chimurai/http-proxy-middleware */ dev: { - "/v1/": { + "/api/": { target: "http://localhost:8000", changeOrigin: true, - }, + pathRewrite: { "^/api": "" }, + } }, }; diff --git a/web/config/routes.ts b/web/config/routes.ts index dc37f75..b041188 100644 --- a/web/config/routes.ts +++ b/web/config/routes.ts @@ -62,6 +62,18 @@ export default [ icon: 'fileSearch', component: './system/operation', }, + { + path: '/system/params', + name: 'params', + icon: 'control', + component: './system/params', + }, + { + path: '/system/state', + name: 'state', + icon: 'monitor', + component: './system/state', + }, ], }, { diff --git a/web/src/app.tsx b/web/src/app.tsx index 2c1dd16..759c004 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -5,8 +5,7 @@ import { Question, SelectLang, } from "@/components"; -import { createAdminService } from "@/services/index"; -import { Admin } from "@/services/kratos/admin/v1/index"; +import { getUserInfo } from "@/services/system/user"; import { LinkOutlined } from "@ant-design/icons"; import type { Settings as LayoutSettings } from "@ant-design/pro-components"; import { SettingDrawer } from "@ant-design/pro-components"; @@ -19,20 +18,26 @@ const isDev = process.env.NODE_ENV === "development"; const isDevOrTest = isDev || process.env.CI; const loginPath = "/user/login"; -const adminService = createAdminService(); - /** * @see https://umijs.org/docs/api/runtime-config#getinitialstate * */ export async function getInitialState(): Promise<{ settings?: Partial; - currentUser?: API.CurrentUser; + currentUser?: API.UserInfo; loading?: boolean; - fetchUserInfo?: () => Promise; + fetchUserInfo?: () => Promise; }> { const fetchUserInfo = async () => { try { - return await adminService.Current({}); + const token = localStorage.getItem('token'); + if (!token) { + return undefined; + } + const res = await getUserInfo(); + if (res.code === 0 && res.data?.userInfo) { + return res.data.userInfo; + } + return undefined; } catch (error) { history.push(loginPath); } diff --git a/web/src/components/RightContent/AvatarDropdown.tsx b/web/src/components/RightContent/AvatarDropdown.tsx index c14dc35..6ef7fc1 100644 --- a/web/src/components/RightContent/AvatarDropdown.tsx +++ b/web/src/components/RightContent/AvatarDropdown.tsx @@ -1,4 +1,3 @@ -import { createAdminService } from "@/services/index"; import { LogoutOutlined, SettingOutlined, @@ -12,8 +11,6 @@ import React from "react"; import { flushSync } from "react-dom"; import HeaderDropdown from "../HeaderDropdown"; -const adminService = createAdminService(); - export type GlobalHeaderRightProps = { menu?: boolean; children?: React.ReactNode; @@ -22,7 +19,7 @@ export type GlobalHeaderRightProps = { export const AvatarName = () => { const { initialState } = useModel("@@initialState"); const { currentUser } = initialState || {}; - return {currentUser?.name}; + return {currentUser?.nickName || currentUser?.userName}; }; const useStyles = createStyles(({ token }) => { @@ -51,7 +48,9 @@ export const AvatarDropdown: React.FC = ({ * 退出登录,并且将当前的 url 保存 */ const loginOut = async () => { - await adminService.Logout({}); + // 清除本地存储的token和用户信息 + localStorage.removeItem('token'); + localStorage.removeItem('userInfo'); const { search, pathname } = window.location; const urlParams = new URL(window.location.href).searchParams; const searchParams = new URLSearchParams({ @@ -80,6 +79,10 @@ export const AvatarDropdown: React.FC = ({ loginOut(); return; } + if (key === "center") { + history.push('/person'); + return; + } history.push(`/account/${key}`); }; @@ -101,7 +104,7 @@ export const AvatarDropdown: React.FC = ({ const { currentUser } = initialState; - if (!currentUser || !currentUser.name) { + if (!currentUser || (!currentUser.nickName && !currentUser.userName)) { return loading; } diff --git a/web/src/pages/dashboard/index.less b/web/src/pages/dashboard/index.less new file mode 100644 index 0000000..50b8b7e --- /dev/null +++ b/web/src/pages/dashboard/index.less @@ -0,0 +1,133 @@ +.dashboard { + :global { + .ant-page-header { + padding: 0; + } + } +} + +.statCard { + border-radius: 8px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + + .statContent { + display: flex; + justify-content: space-between; + align-items: flex-start; + } + + .statInfo { + flex: 1; + } + + .statValue { + margin: 8px 0; + } + + .statTrend { + display: flex; + align-items: center; + } + + .statChart { + width: 120px; + height: 80px; + } +} + +.chartCard { + border-radius: 8px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + min-height: 280px; +} + +.quickCard { + border-radius: 8px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + min-height: 280px; + + .shortcutItem { + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: all 0.3s; + + &:hover { + background: #f0f5ff; + + .shortcutIcon { + background: #1890ff; + color: #fff; + } + } + } + + .shortcutIcon { + width: 40px; + height: 40px; + border-radius: 8px; + background: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: #666; + transition: all 0.3s; + } + + .shortcutTitle { + margin-top: 8px; + font-size: 12px; + color: #666; + } +} + +.noticeCard, +.docCard, +.sysCard { + border-radius: 8px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + min-height: 240px; +} + +.sysCard { + .sysItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } + } +} + +/* 响应式适配 */ +@media (max-width: 768px) { + .statCard { + .statChart { + display: none; + } + } + + .quickCard { + .shortcutItem { + padding: 4px; + } + + .shortcutIcon { + width: 32px; + height: 32px; + font-size: 14px; + } + + .shortcutTitle { + font-size: 11px; + } + } +} diff --git a/web/src/pages/dashboard/index.tsx b/web/src/pages/dashboard/index.tsx index 87a2c44..0243758 100644 --- a/web/src/pages/dashboard/index.tsx +++ b/web/src/pages/dashboard/index.tsx @@ -1,39 +1,268 @@ /** * 仪表盘页面 + * 参考 GVA 的 Dashboard 布局 */ import { PageContainer } from '@ant-design/pro-components'; -import { Card, Col, Row, Statistic } from 'antd'; -import { UserOutlined, TeamOutlined, ApiOutlined, MenuOutlined } from '@ant-design/icons'; +import { Card, Col, Row, Statistic, Tag, List, Typography, Progress, Space, Tooltip } from 'antd'; +import { + UserOutlined, + TeamOutlined, + ApiOutlined, + MenuOutlined, + RiseOutlined, + SettingOutlined, + DatabaseOutlined, + LinkOutlined, + SafetyOutlined, + CloudServerOutlined, +} from '@ant-design/icons'; +import { history } from '@umijs/max'; +import styles from './index.less'; + +const { Text } = Typography; + +// 统计卡片数据 +const statsData = [ + { title: '访问人数', value: 268500, icon: , trend: '+80%', color: '#1890ff', percent: 80 }, + { title: '新增客户', value: 12580, icon: , trend: '+25%', color: '#52c41a', percent: 65 }, + { title: '解决数量', value: 8846, icon: , trend: '+15%', color: '#722ed1', percent: 45 }, +]; + +// 快捷功能 +const shortcuts = [ + { icon: , title: '菜单管理', path: '/system/menu' }, + { icon: , title: 'API管理', path: '/system/api' }, + { icon: , title: '角色管理', path: '/system/role' }, + { icon: , title: '用户管理', path: '/system/user' }, + { icon: , title: '字典管理', path: '/system/dict' }, + { icon: , title: '系统配置', path: '/system/config' }, +]; + +// 公告数据 +const notices = [ + { type: 'processing', typeTitle: '公告', title: 'Kratos Admin v1.0 正式发布,欢迎使用!' }, + { type: 'success', typeTitle: '通知', title: '系统已完成安全升级,请放心使用。' }, + { type: 'warning', typeTitle: '警告', title: '请定期修改密码,确保账户安全。' }, + { type: 'error', typeTitle: '重要', title: '数据库将于本周日凌晨进行维护。' }, + { type: 'default', typeTitle: '信息', title: '感谢您对 Kratos Admin 的支持!' }, +]; + +// 内容数据 +const contentData = [ + { name: '用户', value: 128, color: '#1890ff' }, + { name: '角色', value: 8, color: '#52c41a' }, + { name: 'API', value: 256, color: '#722ed1' }, + { name: '菜单', value: 32, color: '#faad14' }, + { name: '字典', value: 15, color: '#13c2c2' }, + { name: '部门', value: 12, color: '#eb2f96' }, +]; + +// 文档链接 +const docLinks = [ + { title: 'Kratos 官方文档', url: 'https://go-kratos.dev/' }, + { title: 'Ant Design Pro', url: 'https://pro.ant.design/' }, + { title: 'React 官方文档', url: 'https://react.dev/' }, + { title: 'Go 语言文档', url: 'https://go.dev/doc/' }, +]; const Dashboard: React.FC = () => { return ( - - - - - } /> + + {/* 统计卡片行 */} + + {statsData.map((item, index) => ( + + +
+
+ {item.title} +
+ +
+
+ + {item.trend} +
+
+
+ {item.percent}%} + /> +
+
+
+ + ))} +
+ + {/* 主内容区 */} + + {/* 左侧内容数据 */} + + + + {contentData.map((item, index) => ( + +
+
+ {item.value} +
+
{item.name}
+ +
+ + ))} +
+
+
系统资源使用情况
+ + +
+
+ CPU 使用率 +
+ +
+ + +
+
+ 内存使用率 +
+ +
+ + +
+
+ 磁盘使用率 +
+ +
+ + +
+
+ API 调用量 +
+ +
+ +
+
- - - } /> - - - - - } /> - - - - - } /> + + {/* 右侧快捷功能 */} + + + + {shortcuts.map((item, index) => ( + +
history.push(item.path)} + > +
{item.icon}
+ {item.title} +
+ + ))} +
+
+ +
+ + {/* 底部区域 */} + + {/* 公告 */} + + 查看更多} + className={styles.noticeCard} + > + ( + + + {item.typeTitle} + + {item.title} + + + + )} + /> + + + + {/* 文档链接 */} + + 查看更多} + className={styles.docCard} + > + ( + + + + + {item.title} + + + + )} + /> + + + + {/* 系统信息 */} + + +
+ 系统名称 + Kratos Admin +
+
+ 后端框架 + Go Kratos +
+
+ 前端框架 + React + Ant Design Pro +
+
+ 版本号 + v1.0.0 +
+
+ 服务器状态 + 运行中 +
- -

这是一个基于 Kratos 框架的后台管理系统,前端使用 React + Ant Design Pro。

-

功能与 GVA (gin-vue-admin) 保持一致。

-
); }; diff --git a/web/src/pages/system/params/index.tsx b/web/src/pages/system/params/index.tsx new file mode 100644 index 0000000..108b1a2 --- /dev/null +++ b/web/src/pages/system/params/index.tsx @@ -0,0 +1,157 @@ +/** + * 系统参数管理页面 + * 与 GVA 保持一致的功能 + */ +import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; +import { PageContainer, ProTable, ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components'; +import type { ProColumns, ActionType } from '@ant-design/pro-components'; +import { Button, message, Popconfirm, Space } from 'antd'; +import { useRef, useState } from 'react'; +import { + getSysParamsList, + createSysParams, + updateSysParams, + deleteSysParams, + deleteSysParamsByIds, +} from '@/services/system/params'; + +const ParamsPage: React.FC = () => { + const actionRef = useRef(); + const [modalVisible, setModalVisible] = useState(false); + const [currentRow, setCurrentRow] = useState(); + const [isEdit, setIsEdit] = useState(false); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const handleDelete = async (id: number) => { + const res = await deleteSysParams({ ID: id }); + if (res.code === 0) { + message.success('删除成功'); + actionRef.current?.reload(); + } + }; + + const handleBatchDelete = async () => { + if (selectedRowKeys.length === 0) { + message.warning('请选择要删除的记录'); + return; + } + const res = await deleteSysParamsByIds({ 'IDs[]': selectedRowKeys }); + if (res.code === 0) { + message.success('批量删除成功'); + setSelectedRowKeys([]); + actionRef.current?.reload(); + } + }; + + const columns: ProColumns[] = [ + { title: 'ID', dataIndex: 'ID', search: false, width: 80 }, + { title: '参数名', dataIndex: 'name', width: 150 }, + { title: '参数键', dataIndex: 'key', width: 150 }, + { title: '参数值', dataIndex: 'value', ellipsis: true, search: false }, + { title: '描述', dataIndex: 'desc', ellipsis: true, search: false }, + { title: '创建时间', dataIndex: 'createdAt', search: false, width: 180 }, + { + title: '操作', + valueType: 'option', + width: 150, + render: (_, record) => ( + + + handleDelete(record.ID!)}> + + + + ), + }, + ]; + + + return ( + + + headerTitle="参数列表" + actionRef={actionRef} + rowKey="ID" + columns={columns} + rowSelection={{ + selectedRowKeys, + onChange: (keys) => setSelectedRowKeys(keys as number[]), + }} + toolBarRender={() => [ + , + + + , + ]} + request={async (params) => { + const res = await getSysParamsList({ + page: params.current, + pageSize: params.pageSize, + name: params.name, + key: params.key, + }); + return { + data: res.data?.list || [], + total: res.data?.total || 0, + success: res.code === 0, + }; + }} + /> + + { + let res; + if (isEdit) { + res = await updateSysParams({ ...currentRow, ...values }); + } else { + res = await createSysParams(values); + } + if (res.code === 0) { + message.success(isEdit ? '编辑成功' : '创建成功'); + setModalVisible(false); + actionRef.current?.reload(); + return true; + } + return false; + }} + > + + + + + + + ); +}; + +export default ParamsPage; diff --git a/web/src/pages/system/state/index.tsx b/web/src/pages/system/state/index.tsx new file mode 100644 index 0000000..eb778e8 --- /dev/null +++ b/web/src/pages/system/state/index.tsx @@ -0,0 +1,126 @@ +/** + * 系统状态页面 + * 与 GVA 保持一致的功能 + */ +import { PageContainer } from '@ant-design/pro-components'; +import { Card, Row, Col, Progress, Statistic, Spin, Button } from 'antd'; +import { ReloadOutlined } from '@ant-design/icons'; +import { useEffect, useState } from 'react'; +import { getServerInfo } from '@/services/system/systemConfig'; + +const StatePage: React.FC = () => { + const [loading, setLoading] = useState(false); + const [serverInfo, setServerInfo] = useState(); + + const fetchServerInfo = async () => { + setLoading(true); + try { + const res = await getServerInfo(); + if (res.code === 0 && res.data?.server) { + setServerInfo(res.data.server); + } + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchServerInfo(); + }, []); + + const getCpuAvg = () => { + if (!serverInfo?.cpu?.cpus?.length) return 0; + const sum = serverInfo.cpu.cpus.reduce((a, b) => a + b, 0); + return Math.round(sum / serverInfo.cpu.cpus.length); + }; + + return ( + } onClick={fetchServerInfo} loading={loading}> + 刷新 + + } + > + + + + + + + + + + + + + + + + + + + + + + + + `${percent}%`} + status={getCpuAvg() > 80 ? 'exception' : 'normal'} + /> +
+ +
+
+ + + + + `${percent}%`} + status={(serverInfo?.ram?.usedPercent || 0) > 80 ? 'exception' : 'normal'} + /> + + + + + + + + + + + + + + {serverInfo?.disk?.map((d, i) => ( +
+
{d.mountPoint}
+ 80 ? 'exception' : 'normal'} + /> + + + + + + + + +
+ ))} +
+ +
+
+
+ ); +}; + +export default StatePage; diff --git a/web/src/pages/user/login/index.less b/web/src/pages/user/login/index.less new file mode 100644 index 0000000..4e289af --- /dev/null +++ b/web/src/pages/user/login/index.less @@ -0,0 +1,204 @@ +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + background: #194bfb; + position: relative; + overflow: hidden; +} + +.content { + flex: 1; + display: flex; + width: 100%; + height: 100%; +} + +.leftSection { + width: 60%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + position: relative; + background: #fff; +} + +.oblique { + position: absolute; + right: -100px; + top: -15%; + width: 60%; + height: 130%; + background: #fff; + transform: rotate(-12deg); + z-index: 1; +} + +.formWrapper { + position: relative; + z-index: 10; + width: 380px; + padding: 40px; + background: #fff; + border-radius: 8px; +} + +.header { + text-align: center; + margin-bottom: 40px; +} + +.logo { + display: flex; + justify-content: center; + margin-bottom: 16px; +} + +.title { + font-size: 32px; + font-weight: 700; + color: #1f2937; + margin: 0 0 8px 0; +} + +.subtitle { + font-size: 14px; + color: #6b7280; + margin: 0; +} + +.input { + height: 44px; + border-radius: 6px; + + &:hover, &:focus { + border-color: #194bfb; + } +} + +.inputIcon { + color: #9ca3af; +} + +.captchaRow { + display: flex; + gap: 12px; + width: 100%; +} + +.captchaInput { + flex: 1; + height: 44px; +} + +.captchaImg { + width: 120px; + height: 44px; + background: #c3d4f2; + border-radius: 6px; + cursor: pointer; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.loginBtn { + height: 44px; + font-size: 16px; + font-weight: 500; + background: #194bfb; + border-color: #194bfb; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(25, 75, 251, 0.3); + + &:hover { + background: #1240d9; + border-color: #1240d9; + } +} + +.rightSection { + width: 40%; + height: 100%; + background: #194bfb; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.bannerContent { + text-align: center; + color: #fff; + padding: 40px; + + h2 { + font-size: 28px; + font-weight: 600; + margin-bottom: 12px; + } + + p { + font-size: 16px; + opacity: 0.9; + margin-bottom: 40px; + } +} + +.features { + display: flex; + justify-content: center; + gap: 32px; +} + +.featureItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + + span { + font-size: 14px; + } +} + +.featureIcon { + font-size: 32px !important; +} + +.footer { + position: absolute; + bottom: 16px; + left: 0; + right: 0; + text-align: center; + color: rgba(255, 255, 255, 0.7); + font-size: 12px; + z-index: 20; +} + +/* 响应式适配 */ +@media (max-width: 768px) { + .leftSection { + width: 100%; + } + + .rightSection { + display: none; + } + + .oblique { + display: none; + } + + .formWrapper { + width: 90%; + max-width: 380px; + } +} diff --git a/web/src/pages/user/login/index.tsx b/web/src/pages/user/login/index.tsx index 5c2ddaa..556ac64 100644 --- a/web/src/pages/user/login/index.tsx +++ b/web/src/pages/user/login/index.tsx @@ -1,21 +1,23 @@ /** * 登录页面 - * 与 GVA 保持一致的功能 + * 参考 GVA 的 UI 风格 */ import { LockOutlined, UserOutlined, SafetyCertificateOutlined } from '@ant-design/icons'; -import { LoginForm, ProFormText } from '@ant-design/pro-components'; import { history, useModel } from '@umijs/max'; -import { message, Image, Spin } from 'antd'; +import { message, Input, Button, Form, Spin } from 'antd'; import { useState, useEffect } from 'react'; import { login, captcha } from '@/services/system/user'; +import styles from './index.less'; const Login: React.FC = () => { const { setInitialState } = useModel('@@initialState'); + const [form] = Form.useForm(); const [captchaInfo, setCaptchaInfo] = useState<{ captchaId: string; picPath: string; openCaptcha: boolean; - }>({ captchaId: '', picPath: '', openCaptcha: false }); + captchaLength: number; + }>({ captchaId: '', picPath: '', openCaptcha: false, captchaLength: 6 }); const [loading, setLoading] = useState(false); // 获取验证码 @@ -27,6 +29,7 @@ const Login: React.FC = () => { captchaId: res.data.captchaId, picPath: res.data.picPath, openCaptcha: res.data.openCaptcha, + captchaLength: res.data.captchaLength || 6, }); } } catch (error) { @@ -48,15 +51,12 @@ const Login: React.FC = () => { }); if (res.code === 0 && res.data) { message.success('登录成功'); - // 存储token localStorage.setItem('token', res.data.token); localStorage.setItem('userInfo', JSON.stringify(res.data.user)); - // 更新全局状态 await setInitialState((s) => ({ ...s, currentUser: res.data?.user, })); - // 跳转到首页 const urlParams = new URL(window.location.href).searchParams; history.push(urlParams.get('redirect') || '/'); } else { @@ -71,57 +71,122 @@ const Login: React.FC = () => { }; return ( -
- - - }} - placeholder="请输入用户名" - rules={[ - { required: true, message: '请输入用户名' }, - { min: 5, message: '用户名至少5个字符' }, - ]} - /> - }} - placeholder="请输入密码" - rules={[ - { required: true, message: '请输入密码' }, - { min: 6, message: '密码至少6个字符' }, - ]} - /> - {captchaInfo.openCaptcha && ( -
- }} - placeholder="请输入验证码" - rules={[{ required: true, message: '请输入验证码' }]} - /> - 验证码 +
+
+ {/* 左侧登录表单区域 */} +
+ {/* 斜切装饰块 */} +
+ +
+ + {/* Logo 和标题 */} +
+
+ + + +
+

Kratos Admin

+

基于 Go Kratos 和 React 的后台管理系统

+
+ + {/* 登录表单 */} +
+ + } + placeholder="请输入用户名" + className={styles.input} + /> + + + + } + placeholder="请输入密码" + className={styles.input} + /> + + + {captchaInfo.openCaptcha && ( + +
+ } + placeholder="请输入验证码" + className={styles.captchaInput} + /> +
+ {captchaInfo.picPath && ( + 验证码 + )} +
+
+
+ )} + + + + +
+
+
+
+ + {/* 右侧图片区域 */} +
+
+

欢迎使用 Kratos Admin

+

高效、安全、易用的企业级后台管理系统

+
+
+ 🚀 + 高性能 +
+
+ 🔒 + 安全可靠 +
+
+ 📦 + 开箱即用 +
- )} - - +
+
+
+ + {/* 底部信息 */} +
+ Kratos Admin © 2024 基于 Go Kratos 框架 +
); }; diff --git a/web/src/requestErrorConfig.ts b/web/src/requestErrorConfig.ts index 4ceb8a3..0764429 100644 --- a/web/src/requestErrorConfig.ts +++ b/web/src/requestErrorConfig.ts @@ -1,33 +1,43 @@ import type { RequestOptions } from "@@/plugin-request/request"; import type { RequestConfig } from "@umijs/max"; import { message as toast } from "antd"; +import { history } from "@umijs/max"; // Define the structure of the expected response. interface ResponseStructure { code: number; reason?: string; message?: string; + msg?: string; metadata?: Map; } +const loginPath = "/user/login"; + // Request configuration with error handling and interceptors. export const errorConfig: RequestConfig = { errorConfig: { errorThrower: (res) => { - const { code, reason, message } = res as unknown as ResponseStructure; - if (code != 200) { - const error: any = new Error(message); - error.info = { reason, message }; + const { code, reason, message, msg } = res as unknown as ResponseStructure; + if (code !== 0 && code !== 200) { + const error: any = new Error(message || msg); + error.info = { reason, message: message || msg }; throw error; } }, errorHandler: (error: any, opts: any) => { if (opts?.skipErrorHandler) throw error; + if (error.response?.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('userInfo'); + history.push(loginPath); + return; + } if (error.response?.data) { const errorInfo: ResponseStructure | undefined = error.response?.data; if (errorInfo) { - const { message, reason } = errorInfo; - toast.error(reason + ": " + message); + const { message, msg, reason } = errorInfo; + toast.error((reason ? reason + ": " : "") + (message || msg || "请求失败")); } } else if (error.response) { toast.error(`Response status: ${error.response.status}`); @@ -40,12 +50,20 @@ export const errorConfig: RequestConfig = { }, requestInterceptors: [ (config: RequestOptions) => { + // 添加token到请求头 + const token = localStorage.getItem('token'); + if (token) { + config.headers = { + ...config.headers, + 'x-token': token, + }; + } return config; }, ], responseInterceptors: [ (response) => { - if (response.status != 200) { + if (response.status !== 200) { toast.error(`Request error: ${response.status}`); } return response; diff --git a/web/src/services/system/index.ts b/web/src/services/system/index.ts index 80f3f21..71d043e 100644 --- a/web/src/services/system/index.ts +++ b/web/src/services/system/index.ts @@ -9,3 +9,5 @@ export * from './dictionary'; export * from './dictionaryDetail'; export * from './operationRecord'; export * from './casbin'; +export * from './params'; +export * from './systemConfig'; diff --git a/web/src/services/system/params.ts b/web/src/services/system/params.ts new file mode 100644 index 0000000..0671ecd --- /dev/null +++ b/web/src/services/system/params.ts @@ -0,0 +1,61 @@ +/** + * 系统参数管理 API 服务 + * 与 GVA 保持一致的接口定义 + */ +import { request } from '@umijs/max'; + +/** 创建参数 POST /sysParams/createSysParams */ +export async function createSysParams(data: API.SysParams) { + return request('/api/sysParams/createSysParams', { + method: 'POST', + data, + }); +} + +/** 删除参数 DELETE /sysParams/deleteSysParams */ +export async function deleteSysParams(params: { ID: number }) { + return request('/api/sysParams/deleteSysParams', { + method: 'DELETE', + params, + }); +} + +/** 批量删除参数 DELETE /sysParams/deleteSysParamsByIds */ +export async function deleteSysParamsByIds(params: { 'IDs[]': number[] }) { + return request('/api/sysParams/deleteSysParamsByIds', { + method: 'DELETE', + params, + }); +} + +/** 更新参数 PUT /sysParams/updateSysParams */ +export async function updateSysParams(data: API.SysParams) { + return request('/api/sysParams/updateSysParams', { + method: 'PUT', + data, + }); +} + +/** 根据ID查询参数 GET /sysParams/findSysParams */ +export async function findSysParams(params: { ID: number }) { + return request('/api/sysParams/findSysParams', { + method: 'GET', + params, + }); +} + +/** 分页获取参数列表 GET /sysParams/getSysParamsList */ +export async function getSysParamsList(params: API.PageParams & { name?: string; key?: string }) { + return request('/api/sysParams/getSysParamsList', { + method: 'GET', + params, + }); +} + +/** 根据key获取参数 GET /sysParams/getSysParam */ +export async function getSysParam(params: { key: string }) { + return request('/api/sysParams/getSysParam', { + method: 'GET', + params, + }); +} diff --git a/web/src/services/system/systemConfig.ts b/web/src/services/system/systemConfig.ts new file mode 100644 index 0000000..b328dd9 --- /dev/null +++ b/web/src/services/system/systemConfig.ts @@ -0,0 +1,12 @@ +/** + * 系统配置 API 服务 + * 与 GVA 保持一致的接口定义 + */ +import { request } from '@umijs/max'; + +/** 获取服务器信息 POST /system/getServerInfo */ +export async function getServerInfo() { + return request('/api/system/getServerInfo', { + method: 'POST', + }); +} diff --git a/web/src/services/system/typings.d.ts b/web/src/services/system/typings.d.ts index 64b52b3..4f37aaa 100644 --- a/web/src/services/system/typings.d.ts +++ b/web/src/services/system/typings.d.ts @@ -398,3 +398,71 @@ declare namespace API { }; }; } + + + // 系统参数 + type SysParams = { + ID?: number; + name?: string; + key?: string; + value?: string; + desc?: string; + createdAt?: string; + updatedAt?: string; + }; + + // 系统参数结果 + type SysParamsResult = { + code: number; + msg: string; + data?: SysParams; + }; + + // 系统参数列表结果 + type SysParamsListResult = { + code: number; + msg: string; + data?: { + list: SysParams[]; + total: number; + page: number; + pageSize: number; + }; + }; + + // 服务器信息 + type ServerInfo = { + os: { + goos: string; + numCpu: number; + compiler: string; + goVersion: string; + numGoroutine: number; + }; + cpu: { + cpus: number[]; + cores: number; + }; + ram: { + usedMb: number; + totalMb: number; + usedPercent: number; + }; + disk: { + mountPoint: string; + usedMb: number; + usedGb: number; + totalMb: number; + totalGb: number; + usedPercent: number; + }[]; + }; + + // 服务器信息结果 + type ServerInfoResult = { + code: number; + msg: string; + data?: { + server: ServerInfo; + }; + };