任务三

This commit is contained in:
Yvan 2026-01-07 19:10:02 +08:00
parent 3c0f3b7165
commit be3b6641a6
50 changed files with 2920 additions and 1171 deletions

View File

@ -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

17
go.mod
View File

@ -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

21
go.sum
View File

@ -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=

View File

@ -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,
)

View File

@ -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),
}
}

View File

@ -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),
}
}

View File

@ -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),
}
}

View File

@ -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)
}

49
internal/conf/provider.go Normal file
View File

@ -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)
}

View File

@ -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, "")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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 // 需要认证
}
}

View File

@ -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,
)

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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,
},
})

View File

@ -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 {

View File

@ -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 == "" {

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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},
})
}

View File

@ -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)
}

View File

@ -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{}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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)
}

15
pkg/jwt/provider.go Normal file
View File

@ -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)

47
pkg/response/response.go Normal file
View File

@ -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)
}

142
pkg/server/info.go Normal file
View File

@ -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
}

View File

@ -168,7 +168,6 @@ export default defineConfig({
mock: {
include: ["mock/**/*", "src/pages/**/_mock.ts"],
},
utoopack: {},
requestRecord: {},
exportStatic: {},
define: {

View File

@ -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": "" },
}
},
};

View File

@ -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',
},
],
},
{

View File

@ -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<LayoutSettings>;
currentUser?: API.CurrentUser;
currentUser?: API.UserInfo;
loading?: boolean;
fetchUserInfo?: () => Promise<Admin | undefined>;
fetchUserInfo?: () => Promise<API.UserInfo | undefined>;
}> {
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);
}

View File

@ -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 <span className="anticon">{currentUser?.name}</span>;
return <span className="anticon">{currentUser?.nickName || currentUser?.userName}</span>;
};
const useStyles = createStyles(({ token }) => {
@ -51,7 +48,9 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
* 退 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<GlobalHeaderRightProps> = ({
loginOut();
return;
}
if (key === "center") {
history.push('/person');
return;
}
history.push(`/account/${key}`);
};
@ -101,7 +104,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
const { currentUser } = initialState;
if (!currentUser || !currentUser.name) {
if (!currentUser || (!currentUser.nickName && !currentUser.userName)) {
return loading;
}

View File

@ -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;
}
}
}

View File

@ -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: <UserOutlined />, trend: '+80%', color: '#1890ff', percent: 80 },
{ title: '新增客户', value: 12580, icon: <TeamOutlined />, trend: '+25%', color: '#52c41a', percent: 65 },
{ title: '解决数量', value: 8846, icon: <ApiOutlined />, trend: '+15%', color: '#722ed1', percent: 45 },
];
// 快捷功能
const shortcuts = [
{ icon: <MenuOutlined />, title: '菜单管理', path: '/system/menu' },
{ icon: <ApiOutlined />, title: 'API管理', path: '/system/api' },
{ icon: <TeamOutlined />, title: '角色管理', path: '/system/role' },
{ icon: <UserOutlined />, title: '用户管理', path: '/system/user' },
{ icon: <DatabaseOutlined />, title: '字典管理', path: '/system/dict' },
{ icon: <SettingOutlined />, 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 (
<PageContainer>
<Row gutter={16}>
<Col span={6}>
<Card>
<Statistic title="用户总数" value={128} prefix={<UserOutlined />} />
<PageContainer
header={{ title: '' }}
className={styles.dashboard}
>
{/* 统计卡片行 */}
<Row gutter={[16, 16]}>
{statsData.map((item, index) => (
<Col xs={24} sm={12} lg={8} key={index}>
<Card className={styles.statCard} bordered={false}>
<div className={styles.statContent}>
<div className={styles.statInfo}>
<Text type="secondary">{item.title}</Text>
<div className={styles.statValue}>
<Statistic value={item.value} valueStyle={{ fontSize: 28, fontWeight: 600 }} />
</div>
<div className={styles.statTrend}>
<RiseOutlined style={{ color: '#52c41a' }} />
<Text style={{ color: '#52c41a', marginLeft: 4 }}>{item.trend}</Text>
</div>
</div>
<div className={styles.statChart}>
<Progress
type="circle"
percent={item.percent}
size={80}
strokeColor={item.color}
format={() => <span style={{ fontSize: 14 }}>{item.percent}%</span>}
/>
</div>
</div>
</Card>
</Col>
))}
</Row>
{/* 主内容区 */}
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
{/* 左侧内容数据 */}
<Col xs={24} lg={18}>
<Card title="内容数据" bordered={false} className={styles.chartCard}>
<Row gutter={[16, 24]}>
{contentData.map((item, index) => (
<Col xs={12} sm={8} md={4} key={index}>
<div className={styles.dataItem}>
<div className={styles.dataValue} style={{ color: item.color }}>
{item.value}
</div>
<div className={styles.dataName}>{item.name}</div>
<Progress
percent={Math.min((item.value / 300) * 100, 100)}
showInfo={false}
strokeColor={item.color}
size="small"
/>
</div>
</Col>
))}
</Row>
<div className={styles.progressSection}>
<div className={styles.progressTitle}>使</div>
<Row gutter={[32, 16]} style={{ marginTop: 16 }}>
<Col xs={24} sm={12}>
<div className={styles.progressItem}>
<div className={styles.progressLabel}>
<CloudServerOutlined /> CPU 使
</div>
<Progress percent={45} strokeColor="#1890ff" />
</div>
</Col>
<Col xs={24} sm={12}>
<div className={styles.progressItem}>
<div className={styles.progressLabel}>
<DatabaseOutlined /> 使
</div>
<Progress percent={68} strokeColor="#52c41a" />
</div>
</Col>
<Col xs={24} sm={12}>
<div className={styles.progressItem}>
<div className={styles.progressLabel}>
<SafetyOutlined /> 使
</div>
<Progress percent={32} strokeColor="#722ed1" />
</div>
</Col>
<Col xs={24} sm={12}>
<div className={styles.progressItem}>
<div className={styles.progressLabel}>
<ApiOutlined /> API
</div>
<Progress percent={85} strokeColor="#faad14" />
</div>
</Col>
</Row>
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="角色数量" value={8} prefix={<TeamOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="API数量" value={256} prefix={<ApiOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="菜单数量" value={32} prefix={<MenuOutlined />} />
{/* 右侧快捷功能 */}
<Col xs={24} lg={6}>
<Card title="快捷功能" bordered={false} className={styles.quickCard}>
<Row gutter={[8, 16]}>
{shortcuts.map((item, index) => (
<Col span={8} key={index}>
<div
className={styles.shortcutItem}
onClick={() => history.push(item.path)}
>
<div className={styles.shortcutIcon}>{item.icon}</div>
<Text className={styles.shortcutTitle}>{item.title}</Text>
</div>
</Col>
))}
</Row>
</Card>
</Col>
</Row>
{/* 底部区域 */}
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
{/* 公告 */}
<Col xs={24} md={12} lg={8}>
<Card
title="公告"
bordered={false}
extra={<a></a>}
className={styles.noticeCard}
>
<List
size="small"
dataSource={notices}
renderItem={(item) => (
<List.Item style={{ padding: '8px 0', border: 'none' }}>
<Space>
<Tag color={item.type}>{item.typeTitle}</Tag>
<Tooltip title={item.title}>
<Text ellipsis style={{ maxWidth: 200 }}>{item.title}</Text>
</Tooltip>
</Space>
</List.Item>
)}
/>
</Card>
</Col>
{/* 文档链接 */}
<Col xs={24} md={12} lg={8}>
<Card
title="文档"
bordered={false}
extra={<a></a>}
className={styles.docCard}
>
<List
size="small"
dataSource={docLinks}
renderItem={(item) => (
<List.Item style={{ padding: '8px 0', border: 'none' }}>
<a href={item.url} target="_blank" rel="noopener noreferrer">
<Space>
<LinkOutlined />
<Text>{item.title}</Text>
</Space>
</a>
</List.Item>
)}
/>
</Card>
</Col>
{/* 系统信息 */}
<Col xs={24} lg={8}>
<Card title="系统信息" bordered={false} className={styles.sysCard}>
<div className={styles.sysItem}>
<Text type="secondary"></Text>
<Text strong>Kratos Admin</Text>
</div>
<div className={styles.sysItem}>
<Text type="secondary"></Text>
<Text strong>Go Kratos</Text>
</div>
<div className={styles.sysItem}>
<Text type="secondary"></Text>
<Text strong>React + Ant Design Pro</Text>
</div>
<div className={styles.sysItem}>
<Text type="secondary"></Text>
<Text strong>v1.0.0</Text>
</div>
<div className={styles.sysItem}>
<Text type="secondary"></Text>
<Tag color="success"></Tag>
</div>
</Card>
</Col>
</Row>
<Card style={{ marginTop: 16 }} title="欢迎使用 Kratos Admin">
<p> Kratos 使 React + Ant Design Pro</p>
<p> GVA (gin-vue-admin) </p>
</Card>
</PageContainer>
);
};

View File

@ -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<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<API.SysParams>();
const [isEdit, setIsEdit] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
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<API.SysParams>[] = [
{ 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) => (
<Space>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => {
setCurrentRow(record);
setIsEdit(true);
setModalVisible(true);
}}
>
</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
<Button type="link" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer>
<ProTable<API.SysParams>
headerTitle="参数列表"
actionRef={actionRef}
rowKey="ID"
columns={columns}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys as number[]),
}}
toolBarRender={() => [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setCurrentRow(undefined);
setIsEdit(false);
setModalVisible(true);
}}
>
</Button>,
<Popconfirm key="batch" title="确定批量删除?" onConfirm={handleBatchDelete}>
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>
</Button>
</Popconfirm>,
]}
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,
};
}}
/>
<ModalForm
title={isEdit ? '编辑参数' : '新增参数'}
open={modalVisible}
onOpenChange={setModalVisible}
initialValues={currentRow}
modalProps={{ destroyOnClose: true }}
onFinish={async (values) => {
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;
}}
>
<ProFormText name="name" label="参数名" rules={[{ required: true, message: '请输入参数名' }]} />
<ProFormText name="key" label="参数键" rules={[{ required: true, message: '请输入参数键' }]} />
<ProFormTextArea name="value" label="参数值" rules={[{ required: true, message: '请输入参数值' }]} />
<ProFormTextArea name="desc" label="描述" />
</ModalForm>
</PageContainer>
);
};
export default ParamsPage;

View File

@ -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<API.ServerInfo>();
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 (
<PageContainer
extra={
<Button icon={<ReloadOutlined />} onClick={fetchServerInfo} loading={loading}>
</Button>
}
>
<Spin spinning={loading}>
<Row gutter={[16, 16]}>
<Col span={12}>
<Card title="操作系统信息">
<Row gutter={16}>
<Col span={12}>
<Statistic title="操作系统" value={serverInfo?.os?.goos || '-'} />
</Col>
<Col span={12}>
<Statistic title="CPU核心数" value={serverInfo?.os?.numCpu || 0} />
</Col>
<Col span={12}>
<Statistic title="Go版本" value={serverInfo?.os?.goVersion || '-'} />
</Col>
<Col span={12}>
<Statistic title="Goroutine数" value={serverInfo?.os?.numGoroutine || 0} />
</Col>
</Row>
</Card>
</Col>
<Col span={12}>
<Card title="CPU使用率">
<Progress
type="dashboard"
percent={getCpuAvg()}
format={(percent) => `${percent}%`}
status={getCpuAvg() > 80 ? 'exception' : 'normal'}
/>
<div style={{ marginTop: 16 }}>
<Statistic title="CPU核心数" value={serverInfo?.cpu?.cores || 0} />
</div>
</Card>
</Col>
<Col span={12}>
<Card title="内存使用">
<Progress
type="dashboard"
percent={serverInfo?.ram?.usedPercent || 0}
format={(percent) => `${percent}%`}
status={(serverInfo?.ram?.usedPercent || 0) > 80 ? 'exception' : 'normal'}
/>
<Row gutter={16} style={{ marginTop: 16 }}>
<Col span={12}>
<Statistic title="已用内存" value={serverInfo?.ram?.usedMb || 0} suffix="MB" />
</Col>
<Col span={12}>
<Statistic title="总内存" value={serverInfo?.ram?.totalMb || 0} suffix="MB" />
</Col>
</Row>
</Card>
</Col>
<Col span={12}>
<Card title="磁盘使用">
{serverInfo?.disk?.map((d, i) => (
<div key={i} style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8 }}>{d.mountPoint}</div>
<Progress
percent={d.usedPercent}
status={d.usedPercent > 80 ? 'exception' : 'normal'}
/>
<Row gutter={16} style={{ marginTop: 8 }}>
<Col span={12}>
<Statistic title="已用" value={d.usedGb} suffix="GB" valueStyle={{ fontSize: 14 }} />
</Col>
<Col span={12}>
<Statistic title="总计" value={d.totalGb} suffix="GB" valueStyle={{ fontSize: 14 }} />
</Col>
</Row>
</div>
))}
</Card>
</Col>
</Row>
</Spin>
</PageContainer>
);
};
export default StatePage;

View File

@ -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;
}
}

View File

@ -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 (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}}>
<Spin spinning={loading}>
<LoginForm
title="Kratos Admin"
subTitle="基于 Kratos 的后台管理系统"
onFinish={handleSubmit}
initialValues={{ username: 'admin' }}
>
<ProFormText
name="username"
fieldProps={{ size: 'large', prefix: <UserOutlined /> }}
placeholder="请输入用户名"
rules={[
{ required: true, message: '请输入用户名' },
{ min: 5, message: '用户名至少5个字符' },
]}
/>
<ProFormText.Password
name="password"
fieldProps={{ size: 'large', prefix: <LockOutlined /> }}
placeholder="请输入密码"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6个字符' },
]}
/>
{captchaInfo.openCaptcha && (
<div style={{ display: 'flex', gap: 8, marginBottom: 24 }}>
<ProFormText
name="captcha"
fieldProps={{ size: 'large', prefix: <SafetyCertificateOutlined /> }}
placeholder="请输入验证码"
rules={[{ required: true, message: '请输入验证码' }]}
/>
<Image
src={captchaInfo.picPath}
alt="验证码"
preview={false}
style={{ height: 40, cursor: 'pointer' }}
onClick={getCaptcha}
/>
<div className={styles.container}>
<div className={styles.content}>
{/* 左侧登录表单区域 */}
<div className={styles.leftSection}>
{/* 斜切装饰块 */}
<div className={styles.oblique} />
<div className={styles.formWrapper}>
<Spin spinning={loading}>
{/* Logo 和标题 */}
<div className={styles.header}>
<div className={styles.logo}>
<svg viewBox="0 0 24 24" width="48" height="48" fill="#194bfb">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
stroke="#194bfb" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<h1 className={styles.title}>Kratos Admin</h1>
<p className={styles.subtitle}> Go Kratos React </p>
</div>
{/* 登录表单 */}
<Form
form={form}
onFinish={handleSubmit}
initialValues={{ username: 'admin' }}
size="large"
>
<Form.Item
name="username"
rules={[
{ required: true, message: '请输入用户名' },
{ min: 5, message: '用户名至少5个字符' },
]}
>
<Input
prefix={<UserOutlined className={styles.inputIcon} />}
placeholder="请输入用户名"
className={styles.input}
/>
</Form.Item>
<Form.Item
name="password"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6个字符' },
]}
>
<Input.Password
prefix={<LockOutlined className={styles.inputIcon} />}
placeholder="请输入密码"
className={styles.input}
/>
</Form.Item>
{captchaInfo.openCaptcha && (
<Form.Item
name="captcha"
rules={[
{ required: true, message: '请输入验证码' },
{ pattern: /^\d+$/, message: '验证码须为数字' },
]}
>
<div className={styles.captchaRow}>
<Input
prefix={<SafetyCertificateOutlined className={styles.inputIcon} />}
placeholder="请输入验证码"
className={styles.captchaInput}
/>
<div className={styles.captchaImg} onClick={getCaptcha}>
{captchaInfo.picPath && (
<img src={captchaInfo.picPath} alt="验证码" />
)}
</div>
</div>
</Form.Item>
)}
<Form.Item>
<Button type="primary" htmlType="submit" block className={styles.loginBtn}>
</Button>
</Form.Item>
</Form>
</Spin>
</div>
</div>
{/* 右侧图片区域 */}
<div className={styles.rightSection}>
<div className={styles.bannerContent}>
<h2>使 Kratos Admin</h2>
<p></p>
<div className={styles.features}>
<div className={styles.featureItem}>
<span className={styles.featureIcon}>🚀</span>
<span></span>
</div>
<div className={styles.featureItem}>
<span className={styles.featureIcon}>🔒</span>
<span></span>
</div>
<div className={styles.featureItem}>
<span className={styles.featureIcon}>📦</span>
<span></span>
</div>
</div>
)}
</LoginForm>
</Spin>
</div>
</div>
</div>
{/* 底部信息 */}
<div className={styles.footer}>
<span>Kratos Admin © 2024 Go Kratos </span>
</div>
</div>
);
};

View File

@ -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<string, number>;
}
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;

View File

@ -9,3 +9,5 @@ export * from './dictionary';
export * from './dictionaryDetail';
export * from './operationRecord';
export * from './casbin';
export * from './params';
export * from './systemConfig';

View File

@ -0,0 +1,61 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 创建参数 POST /sysParams/createSysParams */
export async function createSysParams(data: API.SysParams) {
return request<API.BaseResult>('/api/sysParams/createSysParams', {
method: 'POST',
data,
});
}
/** 删除参数 DELETE /sysParams/deleteSysParams */
export async function deleteSysParams(params: { ID: number }) {
return request<API.BaseResult>('/api/sysParams/deleteSysParams', {
method: 'DELETE',
params,
});
}
/** 批量删除参数 DELETE /sysParams/deleteSysParamsByIds */
export async function deleteSysParamsByIds(params: { 'IDs[]': number[] }) {
return request<API.BaseResult>('/api/sysParams/deleteSysParamsByIds', {
method: 'DELETE',
params,
});
}
/** 更新参数 PUT /sysParams/updateSysParams */
export async function updateSysParams(data: API.SysParams) {
return request<API.BaseResult>('/api/sysParams/updateSysParams', {
method: 'PUT',
data,
});
}
/** 根据ID查询参数 GET /sysParams/findSysParams */
export async function findSysParams(params: { ID: number }) {
return request<API.SysParamsResult>('/api/sysParams/findSysParams', {
method: 'GET',
params,
});
}
/** 分页获取参数列表 GET /sysParams/getSysParamsList */
export async function getSysParamsList(params: API.PageParams & { name?: string; key?: string }) {
return request<API.SysParamsListResult>('/api/sysParams/getSysParamsList', {
method: 'GET',
params,
});
}
/** 根据key获取参数 GET /sysParams/getSysParam */
export async function getSysParam(params: { key: string }) {
return request<API.SysParamsResult>('/api/sysParams/getSysParam', {
method: 'GET',
params,
});
}

View File

@ -0,0 +1,12 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 获取服务器信息 POST /system/getServerInfo */
export async function getServerInfo() {
return request<API.ServerInfoResult>('/api/system/getServerInfo', {
method: 'POST',
});
}

View File

@ -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;
};
};