From c903b5a6b55830049e1281cf89e940102a0bf6e3 Mon Sep 17 00:00:00 2001 From: xy <10816187@qq.com> Date: Wed, 29 Jul 2026 22:10:36 +0800 Subject: [PATCH] feat: release v1.0.0 --- cmd/main.go | 9 +- example.yaml | 7 + go.mod | 8 +- go.sum | 16 +- internal/config/config.go | 9 + internal/db/db.go | 5 - ...008_add_status_to_users_and_roles.down.sql | 5 + ...00008_add_status_to_users_and_roles.up.sql | 9 + .../migrations/000009_add_post_tags.down.sql | 6 + .../db/migrations/000009_add_post_tags.up.sql | 38 ++ .../000010_set_sys_menus_path_unique.down.sql | 10 + .../000010_set_sys_menus_path_unique.up.sql | 18 + .../db/query/{category.sql => categories.sql} | 26 +- internal/db/query/{file.sql => files.sql} | 0 internal/db/query/post.sql | 73 +++- .../db/query/{sys_api.sql => sys_apis.sql} | 13 +- .../db/query/{sys_menu.sql => sys_menus.sql} | 2 +- ...sys_permission.sql => sys_permissions.sql} | 9 + .../db/query/{sys_role.sql => sys_roles.sql} | 28 +- .../db/query/{sys_user.sql => sys_users.sql} | 59 ++- internal/db/query/tags.sql | 44 +++ internal/db/redis.go | 27 ++ internal/db/schema/categories.sql | 4 +- internal/db/schema/post_tag.sql | 9 + internal/db/schema/sys_menus.sql | 5 +- internal/db/schema/sys_permissions.sql | 2 +- internal/db/schema/sys_roles.sql | 6 +- internal/db/schema/sys_users.sql | 12 +- internal/db/schema/tags.sql | 20 + .../{category.sql.go => categories.sql.go} | 71 ++-- internal/db/sqlc/copyfrom.go | 68 +++- internal/db/sqlc/db.go | 2 +- .../db/sqlc/{file.sql.go => files.sql.go} | 4 +- internal/db/sqlc/models.go | 34 +- internal/db/sqlc/post.sql.go | 157 +++++--- internal/db/sqlc/post_stats.sql.go | 2 +- .../sqlc/{sys_api.sql.go => sys_apis.sql.go} | 43 +- .../{sys_menu.sql.go => sys_menus.sql.go} | 18 +- ...rmission.sql.go => sys_permissions.sql.go} | 30 +- .../{sys_role.sql.go => sys_roles.sql.go} | 79 +++- .../{sys_user.sql.go => sys_users.sql.go} | 159 ++++++-- internal/db/sqlc/tags.sql.go | 179 +++++++++ internal/db/store.go | 68 ++++ internal/handler/auth.go | 96 +++++ internal/handler/module.go | 2 + internal/handler/post.go | 10 + internal/handler/sys_api.go | 8 +- internal/handler/sys_user.go | 72 +--- internal/handler/tag.go | 99 +++++ internal/middleware/auth.go | 37 +- internal/middleware/jwt.go | 71 +--- internal/model/auth/jwt.go | 9 + internal/model/request/category.go | 2 + internal/model/request/post.go | 4 +- internal/model/request/sys_api.go | 8 + internal/model/request/sys_menu.go | 6 +- internal/model/request/sys_permission.go | 1 - internal/model/request/sys_role.go | 8 +- internal/model/request/sys_user.go | 12 +- internal/model/request/tag.go | 13 + internal/model/response/sys_file.go | 6 +- internal/model/response/sys_user.go | 37 +- internal/pkg/cache/cache.go | 218 ++++++++++- internal/pkg/cache/cachekey/key.go | 25 ++ internal/pkg/dberr/postgres.go | 2 + internal/pkg/errs/errs.go | 14 +- internal/router/client.go | 1 + internal/router/router.go | 9 +- internal/router/system.go | 16 + internal/service/auth.go | 180 +++++++++ internal/service/category.go | 42 +- internal/service/module.go | 2 + internal/service/post.go | 33 +- internal/service/sys_api.go | 178 +++++---- internal/service/sys_file.go | 23 +- internal/service/sys_menu.go | 242 ++++++------ internal/service/sys_post.go | 202 +++++----- internal/service/sys_role.go | 280 +++++++------ internal/service/sys_user.go | 370 ++++++++++-------- internal/service/tag.go | 87 ++++ sqlc.yaml | 2 +- 81 files changed, 2776 insertions(+), 1044 deletions(-) create mode 100644 internal/db/migrations/000008_add_status_to_users_and_roles.down.sql create mode 100644 internal/db/migrations/000008_add_status_to_users_and_roles.up.sql create mode 100644 internal/db/migrations/000009_add_post_tags.down.sql create mode 100644 internal/db/migrations/000009_add_post_tags.up.sql create mode 100644 internal/db/migrations/000010_set_sys_menus_path_unique.down.sql create mode 100644 internal/db/migrations/000010_set_sys_menus_path_unique.up.sql rename internal/db/query/{category.sql => categories.sql} (58%) rename internal/db/query/{file.sql => files.sql} (100%) rename internal/db/query/{sys_api.sql => sys_apis.sql} (72%) rename internal/db/query/{sys_menu.sql => sys_menus.sql} (96%) rename internal/db/query/{sys_permission.sql => sys_permissions.sql} (55%) rename internal/db/query/{sys_role.sql => sys_roles.sql} (74%) rename internal/db/query/{sys_user.sql => sys_users.sql} (59%) create mode 100644 internal/db/query/tags.sql create mode 100644 internal/db/redis.go create mode 100644 internal/db/schema/post_tag.sql create mode 100644 internal/db/schema/tags.sql rename internal/db/sqlc/{category.sql.go => categories.sql.go} (73%) rename internal/db/sqlc/{file.sql.go => files.sql.go} (98%) rename internal/db/sqlc/{sys_api.sql.go => sys_apis.sql.go} (87%) rename internal/db/sqlc/{sys_menu.sql.go => sys_menus.sql.go} (95%) rename internal/db/sqlc/{sys_permission.sql.go => sys_permissions.sql.go} (58%) rename internal/db/sqlc/{sys_role.sql.go => sys_roles.sql.go} (76%) rename internal/db/sqlc/{sys_user.sql.go => sys_users.sql.go} (72%) create mode 100644 internal/db/sqlc/tags.sql.go create mode 100644 internal/db/store.go create mode 100644 internal/handler/auth.go create mode 100644 internal/handler/tag.go create mode 100644 internal/model/auth/jwt.go delete mode 100644 internal/model/request/sys_permission.go create mode 100644 internal/model/request/tag.go create mode 100644 internal/pkg/cache/cachekey/key.go create mode 100644 internal/service/auth.go create mode 100644 internal/service/tag.go diff --git a/cmd/main.go b/cmd/main.go index 1e78498..a60a6c4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -48,13 +48,18 @@ func main() { fx.Provide( config.NewConfig, logger.New, + + db.NewDB, + db.NewStore, + db.NewRedisClient, + cache.NewCaches, + middleware.NewJWTMiddleware, middleware.NewAuthMiddleware, middleware.NewLoggerMiddleware, + router.NewRouter, - db.NewDB, - db.NewQueries, ), handler.Module, service.Module, diff --git a/example.yaml b/example.yaml index c96bb5f..e916598 100644 --- a/example.yaml +++ b/example.yaml @@ -10,6 +10,12 @@ database: dbname: db_name sslMode: disable +redis: + addr: host:port + password: redis_password + db: 0 + key_prefix: "" + jwt: secret: signing_method: HS256 @@ -19,6 +25,7 @@ jwt: audience: token_header: Authorization token_prefix: "Bearer " + cookie_name: "rt" file: base_url: http://127.0.0.1:8080/static/ #静态文件目录 用于本地访问开放静态资源 diff --git a/go.mod b/go.mod index dec9e28..d535afa 100644 --- a/go.mod +++ b/go.mod @@ -11,14 +11,15 @@ require ( github.com/jackc/pgx/v5 v5.7.6 github.com/knadh/koanf v1.5.0 github.com/matoous/go-nanoid/v2 v2.1.0 - github.com/maypok86/otter/v2 v2.3.0 + github.com/redis/go-redis/v9 v9.21.0 go.uber.org/fx v1.24.0 golang.org/x/crypto v0.46.0 golang.org/x/sync v0.19.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -29,14 +30,13 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/testify v1.11.1 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 71d4b65..b496598 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,14 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -162,6 +168,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -188,8 +196,6 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w= -github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -244,6 +250,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E= +github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -270,10 +278,14 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= diff --git a/internal/config/config.go b/internal/config/config.go index 985a858..5e67758 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -32,6 +32,13 @@ type Database struct { SSLMode string `koanf:"sslMode"` } +type Redis struct { + Addr string `koanf:"addr"` + Password string `koanf:"password"` + DB int `koanf:"db"` + KeyPrefix string `koanf:"key_prefix"` +} + type JWTConfig struct { Secret string `koanf:"secret"` SigningMethod string `koanf:"signing_method"` @@ -41,6 +48,7 @@ type JWTConfig struct { Audience string `koanf:"audience"` TokenHeader string `koanf:"token_header"` TokenPrefix string `koanf:"token_prefix"` + CookieName string `koanf:"cookie_name"` } type LogConfig struct { @@ -56,6 +64,7 @@ type LogConfig struct { type Config struct { Server Server `koanf:"server"` Database Database `koanf:"database"` + Redis Redis `koanf:"redis"` JWTConfig JWTConfig `koanf:"jwt"` Log LogConfig `koanf:"log"` } diff --git a/internal/db/db.go b/internal/db/db.go index 1a1535d..94b09df 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "server/internal/config" - "server/internal/db/sqlc" "github.com/jackc/pgx/v5/pgxpool" "go.uber.org/fx" @@ -42,7 +41,3 @@ func NewDB(lc fx.Lifecycle, cfg *config.Config) (*pgxpool.Pool, error) { return pool, nil } - -func NewQueries(pool *pgxpool.Pool) *db.Queries { - return db.New(pool) -} diff --git a/internal/db/migrations/000008_add_status_to_users_and_roles.down.sql b/internal/db/migrations/000008_add_status_to_users_and_roles.down.sql new file mode 100644 index 0000000..c90ec9c --- /dev/null +++ b/internal/db/migrations/000008_add_status_to_users_and_roles.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE sys_users + DROP COLUMN IF EXISTS status; + +ALTER TABLE sys_roles + DROP COLUMN IF EXISTS status; \ No newline at end of file diff --git a/internal/db/migrations/000008_add_status_to_users_and_roles.up.sql b/internal/db/migrations/000008_add_status_to_users_and_roles.up.sql new file mode 100644 index 0000000..5e1c08a --- /dev/null +++ b/internal/db/migrations/000008_add_status_to_users_and_roles.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE sys_users + ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 1; + +ALTER TABLE sys_roles + ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 1; + +COMMENT ON COLUMN sys_users.status IS '用户状态(0禁用,1启用)'; + +COMMENT ON COLUMN sys_roles.status IS '角色(0禁用,1启用)'; \ No newline at end of file diff --git a/internal/db/migrations/000009_add_post_tags.down.sql b/internal/db/migrations/000009_add_post_tags.down.sql new file mode 100644 index 0000000..0cca591 --- /dev/null +++ b/internal/db/migrations/000009_add_post_tags.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE categories + DROP COLUMN IF EXISTS sort; + +DROP TABLE IF EXISTS tags; + +DROP TABLE IF EXISTS post_tag; diff --git a/internal/db/migrations/000009_add_post_tags.up.sql b/internal/db/migrations/000009_add_post_tags.up.sql new file mode 100644 index 0000000..5931875 --- /dev/null +++ b/internal/db/migrations/000009_add_post_tags.up.sql @@ -0,0 +1,38 @@ +-- 新增字段 +ALTER TABLE categories + ADD COLUMN IF NOT EXISTS sort INTEGER DEFAULT 0; + +COMMENT ON COLUMN categories.sort IS '分类排序'; + +-- tag表 +CREATE TABLE tags +( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL, + code TEXT UNIQUE NOT NULL, + sort INTEGER DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); + +CREATE TRIGGER update_tags_updated_at + BEFORE UPDATE + ON tags + FOR EACH ROW +EXECUTE FUNCTION update_updated_at_column(); + +COMMENT ON COLUMN tags.id IS '标签ID'; +COMMENT ON COLUMN tags.name IS '标签名称'; +COMMENT ON COLUMN tags.code IS '标签编码'; +COMMENT ON COLUMN tags.sort IS '标签排序'; + +-- 关联表 +CREATE TABLE post_tag +( + post_id INT NOT NULL, + tag_id INT NOT NULL, + PRIMARY KEY (post_id, tag_id) +); + +COMMENT ON COLUMN post_tag.post_id IS '文章ID'; +COMMENT ON COLUMN post_tag.tag_id IS '标签id'; \ No newline at end of file diff --git a/internal/db/migrations/000010_set_sys_menus_path_unique.down.sql b/internal/db/migrations/000010_set_sys_menus_path_unique.down.sql new file mode 100644 index 0000000..62e75d6 --- /dev/null +++ b/internal/db/migrations/000010_set_sys_menus_path_unique.down.sql @@ -0,0 +1,10 @@ +ALTER TABLE sys_menus + DROP CONSTRAINT IF EXISTS sys_menus_path_required_check; + +DROP INDEX IF EXISTS sys_menus_path_unique_idx; + +ALTER TABLE sys_menus + ADD CONSTRAINT sys_menus_path_key UNIQUE (path); + +ALTER TABLE sys_menus + ALTER COLUMN path SET NOT NULL; \ No newline at end of file diff --git a/internal/db/migrations/000010_set_sys_menus_path_unique.up.sql b/internal/db/migrations/000010_set_sys_menus_path_unique.up.sql new file mode 100644 index 0000000..7bd9b14 --- /dev/null +++ b/internal/db/migrations/000010_set_sys_menus_path_unique.up.sql @@ -0,0 +1,18 @@ +ALTER TABLE sys_menus + ALTER COLUMN path DROP NOT NULL; + +ALTER TABLE sys_menus + DROP CONSTRAINT IF EXISTS sys_menus_path_key; + +DROP INDEX IF EXISTS sys_menus_path_unique_idx; + +CREATE UNIQUE INDEX sys_menus_path_unique_idx + ON sys_menus (path) + WHERE type IN (0, 1); + +ALTER TABLE sys_menus + DROP CONSTRAINT IF EXISTS sys_menus_path_required_check; + +ALTER TABLE sys_menus + ADD CONSTRAINT sys_menus_path_required_check + CHECK (type = 2 OR path IS NOT NULL); \ No newline at end of file diff --git a/internal/db/query/category.sql b/internal/db/query/categories.sql similarity index 58% rename from internal/db/query/category.sql rename to internal/db/query/categories.sql index ec912e5..85a6ae0 100644 --- a/internal/db/query/category.sql +++ b/internal/db/query/categories.sql @@ -1,12 +1,12 @@ -- name: CreateCategory :exec -INSERT INTO categories(name, code) -VALUES ($1, $2); +INSERT INTO categories(name, code, sort) +VALUES ($1, $2, $3); -- name: ListCategories :many SELECT * FROM categories -ORDER BY id LIMIT $1 -OFFSET $2; +ORDER BY sort DESC, id +LIMIT $1 OFFSET $2; -- name: CountCategories :one SELECT COUNT(*) @@ -14,17 +14,14 @@ FROM categories; -- name: ListAllCategories :many SELECT * -FROM categories; - --- name: GetCategoryById :one -SELECT * FROM categories -WHERE id = $1; +ORDER BY sort DESC, id; -- name: UpdateCategory :execrows UPDATE categories SET name = coalesce(sqlc.narg('name'), name), - code = coalesce(sqlc.narg('code'), code) + code = coalesce(sqlc.narg('code'), code), + sort = coalesce(sqlc.narg('sort'), sort) WHERE id = sqlc.arg('id'); -- name: DeleteCategory :execrows @@ -36,7 +33,12 @@ WHERE id = $1; INSERT INTO post_category(post_id, category_id) VALUES ($1, $2); --- name: DeletePostCategory :exec +-- name: DeletePostCategoryByPostID :exec DELETE FROM post_category -WHERE post_id = $1; \ No newline at end of file +WHERE post_id = $1; + +-- name: DeletePostCategoryByCategoryID :exec +DELETE +FROM post_category +WHERE category_id = $1; \ No newline at end of file diff --git a/internal/db/query/file.sql b/internal/db/query/files.sql similarity index 100% rename from internal/db/query/file.sql rename to internal/db/query/files.sql diff --git a/internal/db/query/post.sql b/internal/db/query/post.sql index a386584..ec14cb0 100644 --- a/internal/db/query/post.sql +++ b/internal/db/query/post.sql @@ -18,18 +18,46 @@ WITH paginated_posts AS ( updated_at FROM posts ORDER BY sort DESC, published_at DESC, id DESC - LIMIT $1 OFFSET $2) + LIMIT $1 OFFSET $2 +) -- 第二步:用这极少量的记录去进行 JOIN SELECT p.*, f.file_path AS cover, c.name AS category_name, c.id AS category_id, - COALESCE(ps.view, 0) AS view + COALESCE(ps.view, 0) AS view, + COALESCE( + jsonb_agg( + jsonb_build_object( + 'id', t.id, + 'name', t.name, + 'code', t.code + ) + ORDER BY t.sort DESC, t.id + ) FILTER (WHERE t.id IS NOT NULL), + '[]'::jsonb + ) AS tags FROM paginated_posts p LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN categories c ON c.id = pc.category_id LEFT JOIN post_stats ps ON ps.post_id = p.id + LEFT JOIN post_tag pt ON pt.post_id = p.id + LEFT JOIN tags t ON t.id = pt.tag_id +GROUP BY p.id, + p.title, + p.cover_id, + p.slug, + p.summary, + p.status, + p.sort, + p.published_at, + p.created_at, + p.updated_at, + f.file_path, + c.name, + c.id, + ps.view ORDER BY p.sort DESC, p.published_at DESC, p.id DESC; -- name: CountPosts :one @@ -40,12 +68,21 @@ FROM posts; SELECT p.*, f.file_path AS cover, c.name AS category_name, - c.id AS category_id + c.id AS category_id, + COALESCE( + array_agg(pt.tag_id ORDER BY pt.tag_id) FILTER (WHERE pt.tag_id IS NOT NULL), + ARRAY[]::integer[] + ) AS tags FROM posts p LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN categories c ON c.id = pc.category_id + LEFT JOIN post_tag pt ON pt.post_id = p.id WHERE p.id = $1 +GROUP BY p.id, + f.file_path, + c.name, + c.id LIMIT 1; -- name: UpdatePost :execrows @@ -98,12 +135,36 @@ SELECT p.*, f.file_path AS cover, c.name AS category_name, c.id AS category_id, - COALESCE(ps.view, 0) AS view + COALESCE(ps.view, 0) AS view, + COALESCE( + jsonb_agg( + jsonb_build_object( + 'id', t.id, + 'name', t.name, + 'code', t.code + ) + ORDER BY t.sort DESC, t.id + ) FILTER (WHERE t.id IS NOT NULL), + '[]'::jsonb + ) AS tags FROM paginated_posts p LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN categories c ON c.id = pc.category_id LEFT JOIN post_stats ps ON ps.post_id = p.id + LEFT JOIN post_tag pt ON pt.post_id = p.id + LEFT JOIN tags t ON t.id = pt.tag_id +GROUP BY p.id, + p.title, + p.cover_id, + p.slug, + p.summary, + p.sort, + p.published_at, + f.file_path, + c.name, + c.id, + ps.view ORDER BY p.sort DESC, p.published_at DESC, p.id DESC; -- name: CountPublishedPosts :one @@ -142,4 +203,6 @@ FROM AND p.published_at < NOW() GROUP BY c.id, - c.name; \ No newline at end of file + c.sort, + c.name +ORDER BY c.sort DESC, c.id; \ No newline at end of file diff --git a/internal/db/query/sys_api.sql b/internal/db/query/sys_apis.sql similarity index 72% rename from internal/db/query/sys_api.sql rename to internal/db/query/sys_apis.sql index ffabc71..5b176b1 100644 --- a/internal/db/query/sys_api.sql +++ b/internal/db/query/sys_apis.sql @@ -6,12 +6,16 @@ RETURNING id; -- name: GetSysApis :many SELECT * FROM sys_apis +WHERE (sqlc.arg('group_name')::text = '' OR group_name ILIKE '%' || sqlc.arg('group_name') || '%') + AND (sqlc.arg('method')::text = '' OR method = sqlc.arg('method')) ORDER BY id LIMIT $1 OFFSET $2; -- name: CountSysApis :one SELECT COUNT(*) -FROM sys_apis; +FROM sys_apis +WHERE (sqlc.arg('group_name')::text = '' OR group_name ILIKE '%' || sqlc.arg('group_name') || '%') + AND (sqlc.arg('method')::text = '' OR method = sqlc.arg('method')); -- name: GetSysApiByID :one SELECT * @@ -41,13 +45,6 @@ DELETE FROM sys_api_permission WHERE api_id = $1; --- name: DeleteSysPermissionBySysApiID :exec -DELETE -FROM sys_permissions -WHERE id IN (SELECT permission_id - FROM sys_api_permission - WHERE api_id = $1); - -- name: GetSysApiGroupNames :many SELECT DISTINCT group_name FROM sys_apis; diff --git a/internal/db/query/sys_menu.sql b/internal/db/query/sys_menus.sql similarity index 96% rename from internal/db/query/sys_menu.sql rename to internal/db/query/sys_menus.sql index 4488199..60dac97 100644 --- a/internal/db/query/sys_menu.sql +++ b/internal/db/query/sys_menus.sql @@ -38,7 +38,7 @@ SET name = coalesce(sqlc.narg('name'), name), hidden = coalesce(sqlc.narg('hidden'), hidden), sort = coalesce(sqlc.narg('sort'), sort), status = coalesce(sqlc.narg('status'), status), - icon = coalesce(sqlc.narg('icon'), status), + icon = CASE WHEN @update_icon::boolean THEN @icon ELSE icon END, parent_id = CASE WHEN @update_parent_id::boolean THEN @parent_id ELSE parent_id END WHERE id = sqlc.arg('id'); diff --git a/internal/db/query/sys_permission.sql b/internal/db/query/sys_permissions.sql similarity index 55% rename from internal/db/query/sys_permission.sql rename to internal/db/query/sys_permissions.sql index d97b6c3..ee33e63 100644 --- a/internal/db/query/sys_permission.sql +++ b/internal/db/query/sys_permissions.sql @@ -12,3 +12,12 @@ DELETE FROM sys_permissions WHERE id = $1; +-- name: GetSysPermissionIDBySysApiID :one +SELECT permission_id +FROM sys_api_permission +WHERE api_id = $1; + +-- name: GetSysPermissionIDBySysMenuID :one +SELECT permission_id +FROM sys_menu_permission +WHERE menu_id = $1; \ No newline at end of file diff --git a/internal/db/query/sys_role.sql b/internal/db/query/sys_roles.sql similarity index 74% rename from internal/db/query/sys_role.sql rename to internal/db/query/sys_roles.sql index 139b20d..2b03e99 100644 --- a/internal/db/query/sys_role.sql +++ b/internal/db/query/sys_roles.sql @@ -1,6 +1,6 @@ -- name: CreateSysRole :exec -INSERT INTO sys_roles (name, code) -VALUES ($1, $2); +INSERT INTO sys_roles (name, code, status) +VALUES ($1, $2, $3); -- name: ListSysRoles :many SELECT * @@ -28,7 +28,8 @@ SELECT EXISTS(SELECT 1 -- name: UpdateSysRole :execrows UPDATE sys_roles -SET name = coalesce(sqlc.narg('name'), name) +SET name = coalesce(sqlc.narg('name'), name), + status = coalesce(sqlc.narg('status'), status) WHERE id = sqlc.arg('id'); -- name: DeleteSysRole :execrows @@ -68,9 +69,24 @@ FROM sys_role_permission rp JOIN sys_apis a ON a.id = ap.api_id WHERE rp.role_id = $1; --- name: DeleteSysRolePermission :exec +-- name: DeleteSysRolePermissionByType :exec DELETE FROM sys_role_permission rp USING sys_permissions p WHERE rp.permission_id = p.id - AND p."type" = $2 - AND rp.role_id = $1; \ No newline at end of file + AND rp.role_id = $1 + AND p."type" = $2; + +-- name: DeleteSysRolePermissionByRoleID :exec +DELETE +FROM sys_role_permission +WHERE role_id = $1; + +-- name: DeleteSysRolePermissionByPermissionID :exec +DELETE +FROM sys_role_permission +WHERE permission_id = $1; + +-- name: DeleteSysUserRoleByRoleID :exec +DELETE +FROM sys_user_role +WHERE role_id = $1; \ No newline at end of file diff --git a/internal/db/query/sys_user.sql b/internal/db/query/sys_users.sql similarity index 59% rename from internal/db/query/sys_user.sql rename to internal/db/query/sys_users.sql index 8695549..b664024 100644 --- a/internal/db/query/sys_user.sql +++ b/internal/db/query/sys_users.sql @@ -1,12 +1,20 @@ -- name: CreateSysUser :exec -INSERT INTO sys_users (account, username, password_hash, avatar_id) -VALUES ($1, $2, $3, $4); +INSERT INTO sys_users (account, username, password_hash, status, avatar_id) +VALUES ($1, $2, $3, $4, $5); + +-- name: GetActiveSysUserByID :one +-- 场景:用户登录、获取个人信息、刷新 Token(严格校验 status = 1) +SELECT id, account, username, status +FROM sys_users +WHERE id = $1 + AND status = 1; -- name: GetSysUserByID :one SELECT u.id, u.account, u.username, u.avatar_id, + u.status, f.file_path AS avatar_url, u.created_at, u.updated_at @@ -19,6 +27,7 @@ SELECT u.id, u.account, u.username, u.password_hash, + u.status, u.avatar_id, f.file_path AS avatar_url, u.created_at, @@ -31,22 +40,26 @@ WHERE u.account = $1; SELECT u.id, u.account, u.username, + u.status, u.avatar_id, f.file_path AS avatar_url, u.created_at, u.updated_at FROM sys_users u LEFT JOIN files f ON u.avatar_id = f.id +WHERE (sqlc.arg('username')::text = '' OR u.username ILIKE '%' || sqlc.arg('username') || '%') ORDER BY u.id LIMIT $1 OFFSET $2; -- name: CountSysUsers :one SELECT COUNT(*) -FROM sys_users; +FROM sys_users +WHERE (sqlc.arg('username')::text = '' OR username ILIKE '%' || sqlc.arg('username') || '%'); -- name: UpdateSysUser :execrows UPDATE sys_users SET username = coalesce(sqlc.narg('username'), username), + status = coalesce(sqlc.narg('status'), status), avatar_id = CASE WHEN @update_avatar_id::boolean THEN @avatar_id ELSE avatar_id END WHERE id = sqlc.arg('id'); @@ -66,7 +79,7 @@ DELETE FROM sys_user_role WHERE user_id = $1; --- name: CreateSysUserRole :exec +-- name: CreateSysUserRole :copyfrom INSERT INTO sys_user_role (user_id, role_id) VALUES ($1, $2); @@ -79,19 +92,44 @@ WHERE ur.user_id = $1; -- name: GetSysAdminMenus :many SELECT * FROM sys_menus +WHERE status = 1 + AND type <> 2 ORDER BY sort, id; +-- name: GetSysAdminPermissions :many +SELECT code +FROM sys_permissions +WHERE type = 0; + -- name: GetSysUserMenus :many SELECT DISTINCT m.* FROM sys_users u JOIN sys_user_role ur ON ur.user_id = u.id JOIN sys_roles r ON r.id = ur.role_id JOIN sys_role_permission rp ON rp.role_id = r.id + JOIN sys_menu_permission mp ON mp.permission_id = rp.permission_id + JOIN sys_menus m ON m.id = mp.menu_id +WHERE u.id = $1 + AND u.status = 1 + AND r.status = 1 + AND m.status = 1 + AND m.type <> 2 +ORDER BY m.sort, m.id; + +-- name: GetSysUserPermissions :many +SELECT DISTINCT p.code +FROM sys_users u + JOIN sys_user_role ur ON u.id = ur.user_id + JOIN sys_roles r on r.id = ur.role_id + JOIN sys_role_permission rp ON rp.role_id = r.id JOIN sys_permissions p ON p.id = rp.permission_id JOIN sys_menu_permission mp ON mp.permission_id = p.id JOIN sys_menus m ON m.id = mp.menu_id WHERE u.id = $1 -ORDER BY m.sort, m.id; + AND u.status = 1 + AND r.status = 1 + AND m.status = 1 + AND p."type" = 0; -- name: GetSysUserApis :many SELECT DISTINCT a.id, @@ -99,9 +137,10 @@ SELECT DISTINCT a.id, a.METHOD FROM sys_users u JOIN sys_user_role ur ON u.id = ur.user_id - JOIN sys_role_permission rp ON rp.role_id = ur.role_id - JOIN sys_permissions p ON p.id = rp.permission_id - AND p.TYPE = 1 - JOIN sys_api_permission ap ON ap.permission_id = p.id + JOIN sys_roles r ON r.id = ur.role_id + JOIN sys_role_permission rp ON rp.role_id = r.id + JOIN sys_api_permission ap ON ap.permission_id = rp.permission_id JOIN sys_apis a ON a.id = ap.api_id -WHERE u.id = $1; \ No newline at end of file +WHERE u.id = $1 + AND u.status = 1 + AND r.status = 1; \ No newline at end of file diff --git a/internal/db/query/tags.sql b/internal/db/query/tags.sql new file mode 100644 index 0000000..30dadc9 --- /dev/null +++ b/internal/db/query/tags.sql @@ -0,0 +1,44 @@ +-- name: ListTags :many +SELECT * +FROM tags +ORDER BY sort DESC, id +LIMIT $1 OFFSET $2; + +-- name: CountTags :one +SELECT COUNT(*) +FROM tags; + +-- name: ListAllTags :many +SELECT * +FROM tags +ORDER BY sort DESC, id; + +-- name: CreateTag :exec +INSERT INTO tags(name, code, sort) +VALUES ($1, $2, $3); + +-- name: UpdateTag :execrows +UPDATE tags +SET name = coalesce(sqlc.narg('name'), name), + code = coalesce(sqlc.narg('code'), code), + sort = coalesce(sqlc.narg('sort'), sort) +WHERE id = sqlc.arg('id'); + +-- name: DeleteTag :execrows +DELETE +FROM tags +WHERE id = $1; + +-- name: CreatePostTag :copyfrom +INSERT INTO post_tag(post_id, tag_id) +values ($1, $2); + +-- name: DeletePostTagByPostID :exec +DELETE +FROM post_tag +WHERE post_id = $1; + +-- name: DeletePostTagByTagID :exec +DELETE +FROM post_tag +WHERE tag_id = $1; \ No newline at end of file diff --git a/internal/db/redis.go b/internal/db/redis.go new file mode 100644 index 0000000..c4d8a03 --- /dev/null +++ b/internal/db/redis.go @@ -0,0 +1,27 @@ +package db + +import ( + "context" + "log/slog" + + "github.com/redis/go-redis/v9" + + "server/internal/config" +) + +func NewRedisClient(cfg *config.Config) (*redis.Client, error) { + client := redis.NewClient(&redis.Options{ + Addr: cfg.Redis.Addr, + Password: cfg.Redis.Password, + DB: cfg.Redis.DB, + }) + + if err := client.Ping(context.Background()).Err(); err != nil { + slog.Error("failed to connect redis", "error", err) + return nil, err + } + + slog.Info("redis connected", "addr", cfg.Redis.Addr) + + return client, nil +} diff --git a/internal/db/schema/categories.sql b/internal/db/schema/categories.sql index 23b5cc6..929a0b5 100644 --- a/internal/db/schema/categories.sql +++ b/internal/db/schema/categories.sql @@ -4,6 +4,7 @@ CREATE TABLE categories id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL, code TEXT UNIQUE NOT NULL, + sort INTEGER DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ ); @@ -16,4 +17,5 @@ EXECUTE FUNCTION update_updated_at_column(); COMMENT ON COLUMN categories.id IS '分类ID'; COMMENT ON COLUMN categories.name IS '分类名称'; -COMMENT ON COLUMN categories.code IS '分类编码'; \ No newline at end of file +COMMENT ON COLUMN categories.code IS '分类编码'; +COMMENT ON COLUMN categories.sort IS '分类排序'; \ No newline at end of file diff --git a/internal/db/schema/post_tag.sql b/internal/db/schema/post_tag.sql new file mode 100644 index 0000000..5fb41a5 --- /dev/null +++ b/internal/db/schema/post_tag.sql @@ -0,0 +1,9 @@ +CREATE TABLE post_tag +( + post_id INT NOT NULL, + tag_id INT NOT NULL, + PRIMARY KEY (post_id, tag_id) +); + +COMMENT ON COLUMN post_tag.post_id IS '文章ID'; +COMMENT ON COLUMN post_tag.tag_id IS '标签id'; \ No newline at end of file diff --git a/internal/db/schema/sys_menus.sql b/internal/db/schema/sys_menus.sql index da08ac4..9f7ec50 100644 --- a/internal/db/schema/sys_menus.sql +++ b/internal/db/schema/sys_menus.sql @@ -2,7 +2,7 @@ CREATE TABLE sys_menus ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL, - path TEXT NOT NULL, + path TEXT, component TEXT, type SMALLINT NOT NULL, hidden BOOLEAN DEFAULT FALSE, @@ -17,6 +17,9 @@ CREATE TABLE sys_menus CHECK (status IN (0, 1)) ); +CREATE UNIQUE INDEX sys_menus_path_unique_idx + ON sys_menus (path) + WHERE type IN (0, 1); CREATE TRIGGER update_sys_menus_updated_at BEFORE UPDATE diff --git a/internal/db/schema/sys_permissions.sql b/internal/db/schema/sys_permissions.sql index 4de2b82..55f23d4 100644 --- a/internal/db/schema/sys_permissions.sql +++ b/internal/db/schema/sys_permissions.sql @@ -3,7 +3,7 @@ CREATE TABLE sys_permissions id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, code TEXT UNIQUE, type SMALLINT NOT NULL, - created_at TIMESTAMPTZtz NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ, CONSTRAINT unique_type_code UNIQUE (type, code) diff --git a/internal/db/schema/sys_roles.sql b/internal/db/schema/sys_roles.sql index 45c7433..8dbe3f2 100644 --- a/internal/db/schema/sys_roles.sql +++ b/internal/db/schema/sys_roles.sql @@ -3,8 +3,11 @@ CREATE TABLE sys_roles id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL, code TEXT NOT NULL UNIQUE, + status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ + updated_at TIMESTAMPTZ, + + CHECK (status IN (0, 1)) ); CREATE TRIGGER update_sys_roles_updated_at @@ -16,5 +19,6 @@ EXECUTE FUNCTION update_updated_at_column(); COMMENT ON COLUMN sys_roles.id IS '主键ID'; COMMENT ON COLUMN sys_roles.name IS '角色名称'; COMMENT ON COLUMN sys_roles.code IS '角色编码,唯一'; +COMMENT ON COLUMN sys_roles.status IS '角色状态 0:禁用 1:启用'; COMMENT ON COLUMN sys_roles.created_at IS '创建时间'; COMMENT ON COLUMN sys_roles.updated_at IS '更新时间'; \ No newline at end of file diff --git a/internal/db/schema/sys_users.sql b/internal/db/schema/sys_users.sql index 49dd4fa..565a6d0 100644 --- a/internal/db/schema/sys_users.sql +++ b/internal/db/schema/sys_users.sql @@ -1,12 +1,15 @@ CREATE TABLE sys_users ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - account TEXT NOT NULL UNIQUE, - username TEXT NOT NULL, - password_hash TEXT NOT NULL, + account TEXT NOT NULL UNIQUE, + username TEXT NOT NULL, + password_hash TEXT NOT NULL, + status SMALLINT NOT NULL DEFAULT 1, avatar_id INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ + updated_at TIMESTAMPTZ, + + CHECK (status IN (0, 1)) ); CREATE INDEX idx_sys_users_account ON sys_users (account); @@ -22,6 +25,7 @@ COMMENT ON COLUMN sys_users.id IS '主键ID'; COMMENT ON COLUMN sys_users.account IS '用户账号'; COMMENT ON COLUMN sys_users.username IS '用户名称'; COMMENT ON COLUMN sys_users.password_hash IS 'hash密码'; +COMMENT ON COLUMN sys_users.status IS '用户状态 0:禁用 1:启用'; COMMENT ON COLUMN sys_users.avatar_id IS '头像文件id'; COMMENT ON COLUMN sys_users.created_at IS '创建时间'; COMMENT ON COLUMN sys_users.updated_at IS '更新时间'; \ No newline at end of file diff --git a/internal/db/schema/tags.sql b/internal/db/schema/tags.sql new file mode 100644 index 0000000..6f1069c --- /dev/null +++ b/internal/db/schema/tags.sql @@ -0,0 +1,20 @@ +CREATE TABLE tags +( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL, + code TEXT UNIQUE NOT NULL, + sort INTEGER DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); + +CREATE TRIGGER update_tags_updated_at + BEFORE UPDATE + ON tags + FOR EACH ROW +EXECUTE FUNCTION update_updated_at_column(); + +COMMENT ON COLUMN tags.id IS '标签ID'; +COMMENT ON COLUMN tags.name IS '标签名称'; +COMMENT ON COLUMN tags.code IS '标签编码'; +COMMENT ON COLUMN tags.sort IS '标签排序'; \ No newline at end of file diff --git a/internal/db/sqlc/category.sql.go b/internal/db/sqlc/categories.sql.go similarity index 73% rename from internal/db/sqlc/category.sql.go rename to internal/db/sqlc/categories.sql.go index 0811b62..e05c809 100644 --- a/internal/db/sqlc/category.sql.go +++ b/internal/db/sqlc/categories.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 -// source: category.sql +// source: categories.sql -package db +package sqlc import ( "context" @@ -22,17 +22,18 @@ func (q *Queries) CountCategories(ctx context.Context) (int64, error) { } const createCategory = `-- name: CreateCategory :exec -INSERT INTO categories(name, code) -VALUES ($1, $2) +INSERT INTO categories(name, code, sort) +VALUES ($1, $2, $3) ` type CreateCategoryParams struct { Name string `json:"name"` Code string `json:"code"` + Sort *int32 `json:"sort"` } func (q *Queries) CreateCategory(ctx context.Context, arg CreateCategoryParams) error { - _, err := q.db.Exec(ctx, createCategory, arg.Name, arg.Code) + _, err := q.db.Exec(ctx, createCategory, arg.Name, arg.Code, arg.Sort) return err } @@ -65,39 +66,32 @@ func (q *Queries) DeleteCategory(ctx context.Context, id int32) (int64, error) { return result.RowsAffected(), nil } -const deletePostCategory = `-- name: DeletePostCategory :exec +const deletePostCategoryByCategoryID = `-- name: DeletePostCategoryByCategoryID :exec +DELETE +FROM post_category +WHERE category_id = $1 +` + +func (q *Queries) DeletePostCategoryByCategoryID(ctx context.Context, categoryID int32) error { + _, err := q.db.Exec(ctx, deletePostCategoryByCategoryID, categoryID) + return err +} + +const deletePostCategoryByPostID = `-- name: DeletePostCategoryByPostID :exec DELETE FROM post_category WHERE post_id = $1 ` -func (q *Queries) DeletePostCategory(ctx context.Context, postID int32) error { - _, err := q.db.Exec(ctx, deletePostCategory, postID) +func (q *Queries) DeletePostCategoryByPostID(ctx context.Context, postID int32) error { + _, err := q.db.Exec(ctx, deletePostCategoryByPostID, postID) return err } -const getCategoryById = `-- name: GetCategoryById :one -SELECT id, name, code, created_at, updated_at -FROM categories -WHERE id = $1 -` - -func (q *Queries) GetCategoryById(ctx context.Context, id int32) (Category, error) { - row := q.db.QueryRow(ctx, getCategoryById, id) - var i Category - err := row.Scan( - &i.ID, - &i.Name, - &i.Code, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - const listAllCategories = `-- name: ListAllCategories :many -SELECT id, name, code, created_at, updated_at +SELECT id, name, code, sort, created_at, updated_at FROM categories +ORDER BY sort DESC, id ` func (q *Queries) ListAllCategories(ctx context.Context) ([]Category, error) { @@ -113,6 +107,7 @@ func (q *Queries) ListAllCategories(ctx context.Context) ([]Category, error) { &i.ID, &i.Name, &i.Code, + &i.Sort, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -127,10 +122,10 @@ func (q *Queries) ListAllCategories(ctx context.Context) ([]Category, error) { } const listCategories = `-- name: ListCategories :many -SELECT id, name, code, created_at, updated_at +SELECT id, name, code, sort, created_at, updated_at FROM categories -ORDER BY id LIMIT $1 -OFFSET $2 +ORDER BY sort DESC, id +LIMIT $1 OFFSET $2 ` type ListCategoriesParams struct { @@ -151,6 +146,7 @@ func (q *Queries) ListCategories(ctx context.Context, arg ListCategoriesParams) &i.ID, &i.Name, &i.Code, + &i.Sort, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -167,18 +163,25 @@ func (q *Queries) ListCategories(ctx context.Context, arg ListCategoriesParams) const updateCategory = `-- name: UpdateCategory :execrows UPDATE categories SET name = coalesce($1, name), - code = coalesce($2, code) -WHERE id = $3 + code = coalesce($2, code), + sort = coalesce($3, sort) +WHERE id = $4 ` type UpdateCategoryParams struct { Name *string `json:"name"` Code *string `json:"code"` + Sort *int32 `json:"sort"` ID int32 `json:"id"` } func (q *Queries) UpdateCategory(ctx context.Context, arg UpdateCategoryParams) (int64, error) { - result, err := q.db.Exec(ctx, updateCategory, arg.Name, arg.Code, arg.ID) + result, err := q.db.Exec(ctx, updateCategory, + arg.Name, + arg.Code, + arg.Sort, + arg.ID, + ) if err != nil { return 0, err } diff --git a/internal/db/sqlc/copyfrom.go b/internal/db/sqlc/copyfrom.go index 80e2b71..ca0c4f2 100644 --- a/internal/db/sqlc/copyfrom.go +++ b/internal/db/sqlc/copyfrom.go @@ -3,12 +3,45 @@ // sqlc v1.31.1 // source: copyfrom.go -package db +package sqlc import ( "context" ) +// iteratorForCreatePostTag implements pgx.CopyFromSource. +type iteratorForCreatePostTag struct { + rows []CreatePostTagParams + skippedFirstNextCall bool +} + +func (r *iteratorForCreatePostTag) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForCreatePostTag) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].PostID, + r.rows[0].TagID, + }, nil +} + +func (r iteratorForCreatePostTag) Err() error { + return nil +} + +func (q *Queries) CreatePostTag(ctx context.Context, arg []CreatePostTagParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"post_tag"}, []string{"post_id", "tag_id"}, &iteratorForCreatePostTag{rows: arg}) +} + // iteratorForCreateSysRolePermission implements pgx.CopyFromSource. type iteratorForCreateSysRolePermission struct { rows []CreateSysRolePermissionParams @@ -41,3 +74,36 @@ func (r iteratorForCreateSysRolePermission) Err() error { func (q *Queries) CreateSysRolePermission(ctx context.Context, arg []CreateSysRolePermissionParams) (int64, error) { return q.db.CopyFrom(ctx, []string{"sys_role_permission"}, []string{"role_id", "permission_id"}, &iteratorForCreateSysRolePermission{rows: arg}) } + +// iteratorForCreateSysUserRole implements pgx.CopyFromSource. +type iteratorForCreateSysUserRole struct { + rows []CreateSysUserRoleParams + skippedFirstNextCall bool +} + +func (r *iteratorForCreateSysUserRole) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForCreateSysUserRole) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].UserID, + r.rows[0].RoleID, + }, nil +} + +func (r iteratorForCreateSysUserRole) Err() error { + return nil +} + +func (q *Queries) CreateSysUserRole(ctx context.Context, arg []CreateSysUserRoleParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"sys_user_role"}, []string{"user_id", "role_id"}, &iteratorForCreateSysUserRole{rows: arg}) +} diff --git a/internal/db/sqlc/db.go b/internal/db/sqlc/db.go index c3d191a..ad0c777 100644 --- a/internal/db/sqlc/db.go +++ b/internal/db/sqlc/db.go @@ -2,7 +2,7 @@ // versions: // sqlc v1.31.1 -package db +package sqlc import ( "context" diff --git a/internal/db/sqlc/file.sql.go b/internal/db/sqlc/files.sql.go similarity index 98% rename from internal/db/sqlc/file.sql.go rename to internal/db/sqlc/files.sql.go index 847df5b..eccfea2 100644 --- a/internal/db/sqlc/file.sql.go +++ b/internal/db/sqlc/files.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 -// source: file.sql +// source: files.sql -package db +package sqlc import ( "context" diff --git a/internal/db/sqlc/models.go b/internal/db/sqlc/models.go index 96720a8..2922832 100644 --- a/internal/db/sqlc/models.go +++ b/internal/db/sqlc/models.go @@ -2,7 +2,7 @@ // versions: // sqlc v1.31.1 -package db +package sqlc import ( "net/netip" @@ -15,7 +15,9 @@ type Category struct { // 分类名称 Name string `json:"name"` // 分类编码 - Code string `json:"code"` + Code string `json:"code"` + // 分类排序 + Sort *int32 `json:"sort"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` } @@ -80,6 +82,13 @@ type PostStat struct { View int32 `json:"view"` } +type PostTag struct { + // 文章ID + PostID int32 `json:"post_id"` + // 标签id + TagID int32 `json:"tag_id"` +} + type PostViewer struct { // 文章id PostID int32 `json:"post_id"` @@ -121,7 +130,7 @@ type SysMenu struct { // 菜单名称 Name string `json:"name"` // 菜单路径 - Path string `json:"path"` + Path *string `json:"path"` // 组件路径 Component *string `json:"component"` // 菜单类型 0: 目录 1: 菜单 2: button @@ -156,7 +165,7 @@ type SysPermission struct { Code *string `json:"code"` Type int16 `json:"type"` // 创建时间 - CreatedAt interface{} `json:"created_at"` + CreatedAt time.Time `json:"created_at"` // 更新时间 UpdatedAt *time.Time `json:"updated_at"` } @@ -168,6 +177,8 @@ type SysRole struct { Name string `json:"name"` // 角色编码,唯一 Code string `json:"code"` + // 角色状态 0:禁用 1:启用 + Status int16 `json:"status"` // 创建时间 CreatedAt time.Time `json:"created_at"` // 更新时间 @@ -190,6 +201,8 @@ type SysUser struct { Username string `json:"username"` // hash密码 PasswordHash string `json:"password_hash"` + // 用户状态 0:禁用 1:启用 + Status int16 `json:"status"` // 头像文件id AvatarID *int32 `json:"avatar_id"` // 创建时间 @@ -204,3 +217,16 @@ type SysUserRole struct { // 角色ID RoleID int32 `json:"role_id"` } + +type Tag struct { + // 标签ID + ID int32 `json:"id"` + // 标签名称 + Name string `json:"name"` + // 标签编码 + Code string `json:"code"` + // 标签排序 + Sort *int32 `json:"sort"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/internal/db/sqlc/post.sql.go b/internal/db/sqlc/post.sql.go index 1ea2aee..99befb3 100644 --- a/internal/db/sqlc/post.sql.go +++ b/internal/db/sqlc/post.sql.go @@ -3,7 +3,7 @@ // sqlc v1.31.1 // source: post.sql -package db +package sqlc import ( "context" @@ -87,30 +87,40 @@ const getPostById = `-- name: GetPostById :one SELECT p.id, p.title, p.cover_id, p.slug, p.content, p.summary, p.status, p.sort, p.published_at, p.created_at, p.updated_at, f.file_path AS cover, c.name AS category_name, - c.id AS category_id + c.id AS category_id, + COALESCE( + array_agg(pt.tag_id ORDER BY pt.tag_id) FILTER (WHERE pt.tag_id IS NOT NULL), + ARRAY[]::integer[] + ) AS tags FROM posts p LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN categories c ON c.id = pc.category_id + LEFT JOIN post_tag pt ON pt.post_id = p.id WHERE p.id = $1 +GROUP BY p.id, + f.file_path, + c.name, + c.id LIMIT 1 ` type GetPostByIdRow struct { - ID int32 `json:"id"` - Title string `json:"title"` - CoverID *int32 `json:"cover_id"` - Slug string `json:"slug"` - Content string `json:"content"` - Summary string `json:"summary"` - Status int16 `json:"status"` - Sort *int32 `json:"sort"` - PublishedAt time.Time `json:"published_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Cover *string `json:"cover"` - CategoryName *string `json:"category_name"` - CategoryID *int32 `json:"category_id"` + ID int32 `json:"id"` + Title string `json:"title"` + CoverID *int32 `json:"cover_id"` + Slug string `json:"slug"` + Content string `json:"content"` + Summary string `json:"summary"` + Status int16 `json:"status"` + Sort *int32 `json:"sort"` + PublishedAt time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Cover *string `json:"cover"` + CategoryName *string `json:"category_name"` + CategoryID *int32 `json:"category_id"` + Tags interface{} `json:"tags"` } func (q *Queries) GetPostById(ctx context.Context, id int32) (GetPostByIdRow, error) { @@ -131,6 +141,7 @@ func (q *Queries) GetPostById(ctx context.Context, id int32) (GetPostByIdRow, er &i.Cover, &i.CategoryName, &i.CategoryID, + &i.Tags, ) return i, err } @@ -252,7 +263,9 @@ FROM AND p.published_at < NOW() GROUP BY c.id, + c.sort, c.name +ORDER BY c.sort DESC, c.id ` type ListCategoryStatsRow struct { @@ -296,17 +309,45 @@ WITH paginated_posts AS ( updated_at FROM posts ORDER BY sort DESC, published_at DESC, id DESC - LIMIT $1 OFFSET $2) + LIMIT $1 OFFSET $2 +) SELECT p.id, p.title, p.cover_id, p.slug, p.summary, p.status, p.sort, p.published_at, p.created_at, p.updated_at, f.file_path AS cover, c.name AS category_name, c.id AS category_id, - COALESCE(ps.view, 0) AS view + COALESCE(ps.view, 0) AS view, + COALESCE( + jsonb_agg( + jsonb_build_object( + 'id', t.id, + 'name', t.name, + 'code', t.code + ) + ORDER BY t.sort DESC, t.id + ) FILTER (WHERE t.id IS NOT NULL), + '[]'::jsonb + ) AS tags FROM paginated_posts p LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN categories c ON c.id = pc.category_id LEFT JOIN post_stats ps ON ps.post_id = p.id + LEFT JOIN post_tag pt ON pt.post_id = p.id + LEFT JOIN tags t ON t.id = pt.tag_id +GROUP BY p.id, + p.title, + p.cover_id, + p.slug, + p.summary, + p.status, + p.sort, + p.published_at, + p.created_at, + p.updated_at, + f.file_path, + c.name, + c.id, + ps.view ORDER BY p.sort DESC, p.published_at DESC, p.id DESC ` @@ -316,20 +357,21 @@ type ListPostsParams struct { } type ListPostsRow struct { - ID int32 `json:"id"` - Title string `json:"title"` - CoverID *int32 `json:"cover_id"` - Slug string `json:"slug"` - Summary string `json:"summary"` - Status int16 `json:"status"` - Sort *int32 `json:"sort"` - PublishedAt time.Time `json:"published_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Cover *string `json:"cover"` - CategoryName *string `json:"category_name"` - CategoryID *int32 `json:"category_id"` - View int32 `json:"view"` + ID int32 `json:"id"` + Title string `json:"title"` + CoverID *int32 `json:"cover_id"` + Slug string `json:"slug"` + Summary string `json:"summary"` + Status int16 `json:"status"` + Sort *int32 `json:"sort"` + PublishedAt time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Cover *string `json:"cover"` + CategoryName *string `json:"category_name"` + CategoryID *int32 `json:"category_id"` + View int32 `json:"view"` + Tags interface{} `json:"tags"` } // 第二步:用这极少量的记录去进行 JOIN @@ -357,6 +399,7 @@ func (q *Queries) ListPosts(ctx context.Context, arg ListPostsParams) ([]ListPos &i.CategoryName, &i.CategoryID, &i.View, + &i.Tags, ); err != nil { return nil, err } @@ -387,12 +430,36 @@ SELECT p.id, p.title, p.cover_id, p.slug, p.summary, p.sort, p.published_at, f.file_path AS cover, c.name AS category_name, c.id AS category_id, - COALESCE(ps.view, 0) AS view + COALESCE(ps.view, 0) AS view, + COALESCE( + jsonb_agg( + jsonb_build_object( + 'id', t.id, + 'name', t.name, + 'code', t.code + ) + ORDER BY t.sort DESC, t.id + ) FILTER (WHERE t.id IS NOT NULL), + '[]'::jsonb + ) AS tags FROM paginated_posts p LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN categories c ON c.id = pc.category_id LEFT JOIN post_stats ps ON ps.post_id = p.id + LEFT JOIN post_tag pt ON pt.post_id = p.id + LEFT JOIN tags t ON t.id = pt.tag_id +GROUP BY p.id, + p.title, + p.cover_id, + p.slug, + p.summary, + p.sort, + p.published_at, + f.file_path, + c.name, + c.id, + ps.view ORDER BY p.sort DESC, p.published_at DESC, p.id DESC ` @@ -402,17 +469,18 @@ type ListPublishedPostsParams struct { } type ListPublishedPostsRow struct { - ID int32 `json:"id"` - Title string `json:"title"` - CoverID *int32 `json:"cover_id"` - Slug string `json:"slug"` - Summary string `json:"summary"` - Sort *int32 `json:"sort"` - PublishedAt time.Time `json:"published_at"` - Cover *string `json:"cover"` - CategoryName *string `json:"category_name"` - CategoryID *int32 `json:"category_id"` - View int32 `json:"view"` + ID int32 `json:"id"` + Title string `json:"title"` + CoverID *int32 `json:"cover_id"` + Slug string `json:"slug"` + Summary string `json:"summary"` + Sort *int32 `json:"sort"` + PublishedAt time.Time `json:"published_at"` + Cover *string `json:"cover"` + CategoryName *string `json:"category_name"` + CategoryID *int32 `json:"category_id"` + View int32 `json:"view"` + Tags interface{} `json:"tags"` } func (q *Queries) ListPublishedPosts(ctx context.Context, arg ListPublishedPostsParams) ([]ListPublishedPostsRow, error) { @@ -436,6 +504,7 @@ func (q *Queries) ListPublishedPosts(ctx context.Context, arg ListPublishedPosts &i.CategoryName, &i.CategoryID, &i.View, + &i.Tags, ); err != nil { return nil, err } diff --git a/internal/db/sqlc/post_stats.sql.go b/internal/db/sqlc/post_stats.sql.go index 3914257..8253734 100644 --- a/internal/db/sqlc/post_stats.sql.go +++ b/internal/db/sqlc/post_stats.sql.go @@ -3,7 +3,7 @@ // sqlc v1.31.1 // source: post_stats.sql -package db +package sqlc import ( "context" diff --git a/internal/db/sqlc/sys_api.sql.go b/internal/db/sqlc/sys_apis.sql.go similarity index 87% rename from internal/db/sqlc/sys_api.sql.go rename to internal/db/sqlc/sys_apis.sql.go index 8581e60..f65486a 100644 --- a/internal/db/sqlc/sys_api.sql.go +++ b/internal/db/sqlc/sys_apis.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 -// source: sys_api.sql +// source: sys_apis.sql -package db +package sqlc import ( "context" @@ -12,10 +12,17 @@ import ( const countSysApis = `-- name: CountSysApis :one SELECT COUNT(*) FROM sys_apis +WHERE ($1::text = '' OR group_name ILIKE '%' || $1 || '%') + AND ($2::text = '' OR method = $2) ` -func (q *Queries) CountSysApis(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, countSysApis) +type CountSysApisParams struct { + GroupName string `json:"group_name"` + Method string `json:"method"` +} + +func (q *Queries) CountSysApis(ctx context.Context, arg CountSysApisParams) (int64, error) { + row := q.db.QueryRow(ctx, countSysApis, arg.GroupName, arg.Method) var count int64 err := row.Scan(&count) return count, err @@ -88,19 +95,6 @@ func (q *Queries) DeleteSysApiPermission(ctx context.Context, apiID int32) error return err } -const deleteSysPermissionBySysApiID = `-- name: DeleteSysPermissionBySysApiID :exec -DELETE -FROM sys_permissions -WHERE id IN (SELECT permission_id - FROM sys_api_permission - WHERE api_id = $1) -` - -func (q *Queries) DeleteSysPermissionBySysApiID(ctx context.Context, apiID int32) error { - _, err := q.db.Exec(ctx, deleteSysPermissionBySysApiID, apiID) - return err -} - const getAllSysApis = `-- name: GetAllSysApis :many SELECT id, group_name, name, method, path, sort, created_at, updated_at FROM sys_apis @@ -186,17 +180,26 @@ func (q *Queries) GetSysApiGroupNames(ctx context.Context) ([]string, error) { const getSysApis = `-- name: GetSysApis :many SELECT id, group_name, name, method, path, sort, created_at, updated_at FROM sys_apis +WHERE ($3::text = '' OR group_name ILIKE '%' || $3 || '%') + AND ($4::text = '' OR method = $4) ORDER BY id LIMIT $1 OFFSET $2 ` type GetSysApisParams struct { - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` + GroupName string `json:"group_name"` + Method string `json:"method"` } func (q *Queries) GetSysApis(ctx context.Context, arg GetSysApisParams) ([]SysApi, error) { - rows, err := q.db.Query(ctx, getSysApis, arg.Limit, arg.Offset) + rows, err := q.db.Query(ctx, getSysApis, + arg.Limit, + arg.Offset, + arg.GroupName, + arg.Method, + ) if err != nil { return nil, err } diff --git a/internal/db/sqlc/sys_menu.sql.go b/internal/db/sqlc/sys_menus.sql.go similarity index 95% rename from internal/db/sqlc/sys_menu.sql.go rename to internal/db/sqlc/sys_menus.sql.go index 94194c4..3f6cd31 100644 --- a/internal/db/sqlc/sys_menu.sql.go +++ b/internal/db/sqlc/sys_menus.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 -// source: sys_menu.sql +// source: sys_menus.sql -package db +package sqlc import ( "context" @@ -30,7 +30,7 @@ RETURNING id type CreateSysMenuParams struct { Name string `json:"name"` - Path string `json:"path"` + Path *string `json:"path"` Component *string `json:"component"` Type int16 `json:"type"` Hidden *bool `json:"hidden"` @@ -123,7 +123,7 @@ ORDER BY m.sort ASC, type GetAllSysMenusRow struct { ID int32 `json:"id"` Name string `json:"name"` - Path string `json:"path"` + Path *string `json:"path"` Component *string `json:"component"` Type int16 `json:"type"` Hidden *bool `json:"hidden"` @@ -213,7 +213,7 @@ type ListSysMenusParams struct { type ListSysMenusRow struct { ID int32 `json:"id"` Name string `json:"name"` - Path string `json:"path"` + Path *string `json:"path"` Component *string `json:"component"` Type int16 `json:"type"` Hidden *bool `json:"hidden"` @@ -269,9 +269,9 @@ SET name = coalesce($1, name), hidden = coalesce($5, hidden), sort = coalesce($6, sort), status = coalesce($7, status), - icon = coalesce($8, status), - parent_id = CASE WHEN $9::boolean THEN $10 ELSE parent_id END -WHERE id = $11 + icon = CASE WHEN $8::boolean THEN $9 ELSE icon END, + parent_id = CASE WHEN $10::boolean THEN $11 ELSE parent_id END +WHERE id = $12 ` type UpdateSysMenuParams struct { @@ -282,6 +282,7 @@ type UpdateSysMenuParams struct { Hidden *bool `json:"hidden"` Sort *int32 `json:"sort"` Status *int16 `json:"status"` + UpdateIcon bool `json:"update_icon"` Icon *int32 `json:"icon"` UpdateParentID bool `json:"update_parent_id"` ParentID *int32 `json:"parent_id"` @@ -297,6 +298,7 @@ func (q *Queries) UpdateSysMenu(ctx context.Context, arg UpdateSysMenuParams) (i arg.Hidden, arg.Sort, arg.Status, + arg.UpdateIcon, arg.Icon, arg.UpdateParentID, arg.ParentID, diff --git a/internal/db/sqlc/sys_permission.sql.go b/internal/db/sqlc/sys_permissions.sql.go similarity index 58% rename from internal/db/sqlc/sys_permission.sql.go rename to internal/db/sqlc/sys_permissions.sql.go index a058c4d..70619cf 100644 --- a/internal/db/sqlc/sys_permission.sql.go +++ b/internal/db/sqlc/sys_permissions.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 -// source: sys_permission.sql +// source: sys_permissions.sql -package db +package sqlc import ( "context" @@ -37,6 +37,32 @@ func (q *Queries) DeleteSysPermission(ctx context.Context, id int32) error { return err } +const getSysPermissionIDBySysApiID = `-- name: GetSysPermissionIDBySysApiID :one +SELECT permission_id +FROM sys_api_permission +WHERE api_id = $1 +` + +func (q *Queries) GetSysPermissionIDBySysApiID(ctx context.Context, apiID int32) (int32, error) { + row := q.db.QueryRow(ctx, getSysPermissionIDBySysApiID, apiID) + var permission_id int32 + err := row.Scan(&permission_id) + return permission_id, err +} + +const getSysPermissionIDBySysMenuID = `-- name: GetSysPermissionIDBySysMenuID :one +SELECT permission_id +FROM sys_menu_permission +WHERE menu_id = $1 +` + +func (q *Queries) GetSysPermissionIDBySysMenuID(ctx context.Context, menuID int32) (int32, error) { + row := q.db.QueryRow(ctx, getSysPermissionIDBySysMenuID, menuID) + var permission_id int32 + err := row.Scan(&permission_id) + return permission_id, err +} + const updateSysPermission = `-- name: UpdateSysPermission :exec UPDATE sys_permissions SET code = $2 diff --git a/internal/db/sqlc/sys_role.sql.go b/internal/db/sqlc/sys_roles.sql.go similarity index 76% rename from internal/db/sqlc/sys_role.sql.go rename to internal/db/sqlc/sys_roles.sql.go index 350b60c..0bd22c0 100644 --- a/internal/db/sqlc/sys_role.sql.go +++ b/internal/db/sqlc/sys_roles.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 -// source: sys_role.sql +// source: sys_roles.sql -package db +package sqlc import ( "context" @@ -22,17 +22,18 @@ func (q *Queries) CountSysRoles(ctx context.Context) (int64, error) { } const createSysRole = `-- name: CreateSysRole :exec -INSERT INTO sys_roles (name, code) -VALUES ($1, $2) +INSERT INTO sys_roles (name, code, status) +VALUES ($1, $2, $3) ` type CreateSysRoleParams struct { - Name string `json:"name"` - Code string `json:"code"` + Name string `json:"name"` + Code string `json:"code"` + Status int16 `json:"status"` } func (q *Queries) CreateSysRole(ctx context.Context, arg CreateSysRoleParams) error { - _, err := q.db.Exec(ctx, createSysRole, arg.Name, arg.Code) + _, err := q.db.Exec(ctx, createSysRole, arg.Name, arg.Code, arg.Status) return err } @@ -55,21 +56,54 @@ func (q *Queries) DeleteSysRole(ctx context.Context, id int32) (int64, error) { return result.RowsAffected(), nil } -const deleteSysRolePermission = `-- name: DeleteSysRolePermission :exec +const deleteSysRolePermissionByPermissionID = `-- name: DeleteSysRolePermissionByPermissionID :exec +DELETE +FROM sys_role_permission +WHERE permission_id = $1 +` + +func (q *Queries) DeleteSysRolePermissionByPermissionID(ctx context.Context, permissionID int32) error { + _, err := q.db.Exec(ctx, deleteSysRolePermissionByPermissionID, permissionID) + return err +} + +const deleteSysRolePermissionByRoleID = `-- name: DeleteSysRolePermissionByRoleID :exec +DELETE +FROM sys_role_permission +WHERE role_id = $1 +` + +func (q *Queries) DeleteSysRolePermissionByRoleID(ctx context.Context, roleID int32) error { + _, err := q.db.Exec(ctx, deleteSysRolePermissionByRoleID, roleID) + return err +} + +const deleteSysRolePermissionByType = `-- name: DeleteSysRolePermissionByType :exec DELETE FROM sys_role_permission rp USING sys_permissions p WHERE rp.permission_id = p.id - AND p."type" = $2 AND rp.role_id = $1 + AND p."type" = $2 ` -type DeleteSysRolePermissionParams struct { +type DeleteSysRolePermissionByTypeParams struct { RoleID int32 `json:"role_id"` Type int16 `json:"type"` } -func (q *Queries) DeleteSysRolePermission(ctx context.Context, arg DeleteSysRolePermissionParams) error { - _, err := q.db.Exec(ctx, deleteSysRolePermission, arg.RoleID, arg.Type) +func (q *Queries) DeleteSysRolePermissionByType(ctx context.Context, arg DeleteSysRolePermissionByTypeParams) error { + _, err := q.db.Exec(ctx, deleteSysRolePermissionByType, arg.RoleID, arg.Type) + return err +} + +const deleteSysUserRoleByRoleID = `-- name: DeleteSysUserRoleByRoleID :exec +DELETE +FROM sys_user_role +WHERE role_id = $1 +` + +func (q *Queries) DeleteSysUserRoleByRoleID(ctx context.Context, roleID int32) error { + _, err := q.db.Exec(ctx, deleteSysUserRoleByRoleID, roleID) return err } @@ -96,7 +130,7 @@ func (q *Queries) ExistsSysRoleCode(ctx context.Context, arg ExistsSysRoleCodePa } const getAllSysRoles = `-- name: GetAllSysRoles :many -SELECT id, name, code, created_at, updated_at +SELECT id, name, code, status, created_at, updated_at from sys_roles ` @@ -113,6 +147,7 @@ func (q *Queries) GetAllSysRoles(ctx context.Context) ([]SysRole, error) { &i.ID, &i.Name, &i.Code, + &i.Status, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -212,7 +247,7 @@ func (q *Queries) GetSysRoleApis(ctx context.Context, roleID int32) ([]GetSysRol } const getSysRoleByID = `-- name: GetSysRoleByID :one -SELECT id, name, code, created_at, updated_at +SELECT id, name, code, status, created_at, updated_at FROM sys_roles WHERE id = $1 ` @@ -224,6 +259,7 @@ func (q *Queries) GetSysRoleByID(ctx context.Context, id int32) (SysRole, error) &i.ID, &i.Name, &i.Code, + &i.Status, &i.CreatedAt, &i.UpdatedAt, ) @@ -264,7 +300,7 @@ func (q *Queries) GetSysRoleMenus(ctx context.Context, roleID int32) ([]GetSysRo } const listSysRoles = `-- name: ListSysRoles :many -SELECT id, name, code, created_at, updated_at +SELECT id, name, code, status, created_at, updated_at FROM sys_roles ORDER BY id LIMIT $1 OFFSET $2 @@ -288,6 +324,7 @@ func (q *Queries) ListSysRoles(ctx context.Context, arg ListSysRolesParams) ([]S &i.ID, &i.Name, &i.Code, + &i.Status, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -303,17 +340,19 @@ func (q *Queries) ListSysRoles(ctx context.Context, arg ListSysRolesParams) ([]S const updateSysRole = `-- name: UpdateSysRole :execrows UPDATE sys_roles -SET name = coalesce($1, name) -WHERE id = $2 +SET name = coalesce($1, name), + status = coalesce($2, status) +WHERE id = $3 ` type UpdateSysRoleParams struct { - Name *string `json:"name"` - ID int32 `json:"id"` + Name *string `json:"name"` + Status *int16 `json:"status"` + ID int32 `json:"id"` } func (q *Queries) UpdateSysRole(ctx context.Context, arg UpdateSysRoleParams) (int64, error) { - result, err := q.db.Exec(ctx, updateSysRole, arg.Name, arg.ID) + result, err := q.db.Exec(ctx, updateSysRole, arg.Name, arg.Status, arg.ID) if err != nil { return 0, err } diff --git a/internal/db/sqlc/sys_user.sql.go b/internal/db/sqlc/sys_users.sql.go similarity index 72% rename from internal/db/sqlc/sys_user.sql.go rename to internal/db/sqlc/sys_users.sql.go index 288ebc2..87d9f13 100644 --- a/internal/db/sqlc/sys_user.sql.go +++ b/internal/db/sqlc/sys_users.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 -// source: sys_user.sql +// source: sys_users.sql -package db +package sqlc import ( "context" @@ -24,24 +24,26 @@ func (q *Queries) ClearSysUserRoles(ctx context.Context, userID int32) error { const countSysUsers = `-- name: CountSysUsers :one SELECT COUNT(*) FROM sys_users +WHERE ($1::text = '' OR username ILIKE '%' || $1 || '%') ` -func (q *Queries) CountSysUsers(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, countSysUsers) +func (q *Queries) CountSysUsers(ctx context.Context, username string) (int64, error) { + row := q.db.QueryRow(ctx, countSysUsers, username) var count int64 err := row.Scan(&count) return count, err } const createSysUser = `-- name: CreateSysUser :exec -INSERT INTO sys_users (account, username, password_hash, avatar_id) -VALUES ($1, $2, $3, $4) +INSERT INTO sys_users (account, username, password_hash, status, avatar_id) +VALUES ($1, $2, $3, $4, $5) ` type CreateSysUserParams struct { Account string `json:"account"` Username string `json:"username"` PasswordHash string `json:"password_hash"` + Status int16 `json:"status"` AvatarID *int32 `json:"avatar_id"` } @@ -50,26 +52,17 @@ func (q *Queries) CreateSysUser(ctx context.Context, arg CreateSysUserParams) er arg.Account, arg.Username, arg.PasswordHash, + arg.Status, arg.AvatarID, ) return err } -const createSysUserRole = `-- name: CreateSysUserRole :exec -INSERT INTO sys_user_role (user_id, role_id) -VALUES ($1, $2) -` - type CreateSysUserRoleParams struct { UserID int32 `json:"user_id"` RoleID int32 `json:"role_id"` } -func (q *Queries) CreateSysUserRole(ctx context.Context, arg CreateSysUserRoleParams) error { - _, err := q.db.Exec(ctx, createSysUserRole, arg.UserID, arg.RoleID) - return err -} - const deleteSysUser = `-- name: DeleteSysUser :execrows DELETE FROM sys_users @@ -85,9 +78,38 @@ func (q *Queries) DeleteSysUser(ctx context.Context, id int32) (int64, error) { return result.RowsAffected(), nil } +const getActiveSysUserByID = `-- name: GetActiveSysUserByID :one +SELECT id, account, username, status +FROM sys_users +WHERE id = $1 + AND status = 1 +` + +type GetActiveSysUserByIDRow struct { + ID int32 `json:"id"` + Account string `json:"account"` + Username string `json:"username"` + Status int16 `json:"status"` +} + +// 场景:用户登录、获取个人信息、刷新 Token(严格校验 status = 1) +func (q *Queries) GetActiveSysUserByID(ctx context.Context, id int32) (GetActiveSysUserByIDRow, error) { + row := q.db.QueryRow(ctx, getActiveSysUserByID, id) + var i GetActiveSysUserByIDRow + err := row.Scan( + &i.ID, + &i.Account, + &i.Username, + &i.Status, + ) + return i, err +} + const getSysAdminMenus = `-- name: GetSysAdminMenus :many SELECT id, name, path, component, type, hidden, sort, status, parent_id, icon, created_at, updated_at FROM sys_menus +WHERE status = 1 + AND type <> 2 ORDER BY sort, id ` @@ -124,18 +146,45 @@ func (q *Queries) GetSysAdminMenus(ctx context.Context) ([]SysMenu, error) { return items, nil } +const getSysAdminPermissions = `-- name: GetSysAdminPermissions :many +SELECT code +FROM sys_permissions +WHERE type = 0 +` + +func (q *Queries) GetSysAdminPermissions(ctx context.Context) ([]*string, error) { + rows, err := q.db.Query(ctx, getSysAdminPermissions) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*string{} + for rows.Next() { + var code *string + if err := rows.Scan(&code); err != nil { + return nil, err + } + items = append(items, code) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getSysUserApis = `-- name: GetSysUserApis :many SELECT DISTINCT a.id, a.PATH, a.METHOD FROM sys_users u JOIN sys_user_role ur ON u.id = ur.user_id - JOIN sys_role_permission rp ON rp.role_id = ur.role_id - JOIN sys_permissions p ON p.id = rp.permission_id - AND p.TYPE = 1 - JOIN sys_api_permission ap ON ap.permission_id = p.id + JOIN sys_roles r ON r.id = ur.role_id + JOIN sys_role_permission rp ON rp.role_id = r.id + JOIN sys_api_permission ap ON ap.permission_id = rp.permission_id JOIN sys_apis a ON a.id = ap.api_id WHERE u.id = $1 + AND u.status = 1 + AND r.status = 1 ` type GetSysUserApisRow struct { @@ -169,6 +218,7 @@ SELECT u.id, u.account, u.username, u.password_hash, + u.status, u.avatar_id, f.file_path AS avatar_url, u.created_at, @@ -183,6 +233,7 @@ type GetSysUserByAccountRow struct { Account string `json:"account"` Username string `json:"username"` PasswordHash string `json:"password_hash"` + Status int16 `json:"status"` AvatarID *int32 `json:"avatar_id"` AvatarUrl *string `json:"avatar_url"` CreatedAt time.Time `json:"created_at"` @@ -197,6 +248,7 @@ func (q *Queries) GetSysUserByAccount(ctx context.Context, account string) (GetS &i.Account, &i.Username, &i.PasswordHash, + &i.Status, &i.AvatarID, &i.AvatarUrl, &i.CreatedAt, @@ -210,6 +262,7 @@ SELECT u.id, u.account, u.username, u.avatar_id, + u.status, f.file_path AS avatar_url, u.created_at, u.updated_at @@ -223,6 +276,7 @@ type GetSysUserByIDRow struct { Account string `json:"account"` Username string `json:"username"` AvatarID *int32 `json:"avatar_id"` + Status int16 `json:"status"` AvatarUrl *string `json:"avatar_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` @@ -236,6 +290,7 @@ func (q *Queries) GetSysUserByID(ctx context.Context, id int32) (GetSysUserByIDR &i.Account, &i.Username, &i.AvatarID, + &i.Status, &i.AvatarUrl, &i.CreatedAt, &i.UpdatedAt, @@ -249,10 +304,13 @@ FROM sys_users u JOIN sys_user_role ur ON ur.user_id = u.id JOIN sys_roles r ON r.id = ur.role_id JOIN sys_role_permission rp ON rp.role_id = r.id - JOIN sys_permissions p ON p.id = rp.permission_id - JOIN sys_menu_permission mp ON mp.permission_id = p.id + JOIN sys_menu_permission mp ON mp.permission_id = rp.permission_id JOIN sys_menus m ON m.id = mp.menu_id WHERE u.id = $1 + AND u.status = 1 + AND r.status = 1 + AND m.status = 1 + AND m.type <> 2 ORDER BY m.sort, m.id ` @@ -289,8 +347,44 @@ func (q *Queries) GetSysUserMenus(ctx context.Context, id int32) ([]SysMenu, err return items, nil } +const getSysUserPermissions = `-- name: GetSysUserPermissions :many +SELECT DISTINCT p.code +FROM sys_users u + JOIN sys_user_role ur ON u.id = ur.user_id + JOIN sys_roles r on r.id = ur.role_id + JOIN sys_role_permission rp ON rp.role_id = r.id + JOIN sys_permissions p ON p.id = rp.permission_id + JOIN sys_menu_permission mp ON mp.permission_id = p.id + JOIN sys_menus m ON m.id = mp.menu_id +WHERE u.id = $1 + AND u.status = 1 + AND r.status = 1 + AND m.status = 1 + AND p."type" = 0 +` + +func (q *Queries) GetSysUserPermissions(ctx context.Context, id int32) ([]*string, error) { + rows, err := q.db.Query(ctx, getSysUserPermissions, id) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*string{} + for rows.Next() { + var code *string + if err := rows.Scan(&code); err != nil { + return nil, err + } + items = append(items, code) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getSysUserRoles = `-- name: GetSysUserRoles :many -SELECT r.id, r.name, r.code, r.created_at, r.updated_at +SELECT r.id, r.name, r.code, r.status, r.created_at, r.updated_at FROM sys_roles r JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = $1 @@ -309,6 +403,7 @@ func (q *Queries) GetSysUserRoles(ctx context.Context, userID int32) ([]SysRole, &i.ID, &i.Name, &i.Code, + &i.Status, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -326,25 +421,29 @@ const listSysUsers = `-- name: ListSysUsers :many SELECT u.id, u.account, u.username, + u.status, u.avatar_id, f.file_path AS avatar_url, u.created_at, u.updated_at FROM sys_users u LEFT JOIN files f ON u.avatar_id = f.id +WHERE ($3::text = '' OR u.username ILIKE '%' || $3 || '%') ORDER BY u.id LIMIT $1 OFFSET $2 ` type ListSysUsersParams struct { - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` + Username string `json:"username"` } type ListSysUsersRow struct { ID int32 `json:"id"` Account string `json:"account"` Username string `json:"username"` + Status int16 `json:"status"` AvatarID *int32 `json:"avatar_id"` AvatarUrl *string `json:"avatar_url"` CreatedAt time.Time `json:"created_at"` @@ -352,7 +451,7 @@ type ListSysUsersRow struct { } func (q *Queries) ListSysUsers(ctx context.Context, arg ListSysUsersParams) ([]ListSysUsersRow, error) { - rows, err := q.db.Query(ctx, listSysUsers, arg.Limit, arg.Offset) + rows, err := q.db.Query(ctx, listSysUsers, arg.Limit, arg.Offset, arg.Username) if err != nil { return nil, err } @@ -364,6 +463,7 @@ func (q *Queries) ListSysUsers(ctx context.Context, arg ListSysUsersParams) ([]L &i.ID, &i.Account, &i.Username, + &i.Status, &i.AvatarID, &i.AvatarUrl, &i.CreatedAt, @@ -382,12 +482,14 @@ func (q *Queries) ListSysUsers(ctx context.Context, arg ListSysUsersParams) ([]L const updateSysUser = `-- name: UpdateSysUser :execrows UPDATE sys_users SET username = coalesce($1, username), - avatar_id = CASE WHEN $2::boolean THEN $3 ELSE avatar_id END -WHERE id = $4 + status = coalesce($2, status), + avatar_id = CASE WHEN $3::boolean THEN $4 ELSE avatar_id END +WHERE id = $5 ` type UpdateSysUserParams struct { Username *string `json:"username"` + Status *int16 `json:"status"` UpdateAvatarID bool `json:"update_avatar_id"` AvatarID *int32 `json:"avatar_id"` ID int32 `json:"id"` @@ -396,6 +498,7 @@ type UpdateSysUserParams struct { func (q *Queries) UpdateSysUser(ctx context.Context, arg UpdateSysUserParams) (int64, error) { result, err := q.db.Exec(ctx, updateSysUser, arg.Username, + arg.Status, arg.UpdateAvatarID, arg.AvatarID, arg.ID, diff --git a/internal/db/sqlc/tags.sql.go b/internal/db/sqlc/tags.sql.go new file mode 100644 index 0000000..c035aaa --- /dev/null +++ b/internal/db/sqlc/tags.sql.go @@ -0,0 +1,179 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: tags.sql + +package sqlc + +import ( + "context" +) + +const countTags = `-- name: CountTags :one +SELECT COUNT(*) +FROM tags +` + +func (q *Queries) CountTags(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countTags) + var count int64 + err := row.Scan(&count) + return count, err +} + +type CreatePostTagParams struct { + PostID int32 `json:"post_id"` + TagID int32 `json:"tag_id"` +} + +const createTag = `-- name: CreateTag :exec +INSERT INTO tags(name, code, sort) +VALUES ($1, $2, $3) +` + +type CreateTagParams struct { + Name string `json:"name"` + Code string `json:"code"` + Sort *int32 `json:"sort"` +} + +func (q *Queries) CreateTag(ctx context.Context, arg CreateTagParams) error { + _, err := q.db.Exec(ctx, createTag, arg.Name, arg.Code, arg.Sort) + return err +} + +const deletePostTagByPostID = `-- name: DeletePostTagByPostID :exec +DELETE +FROM post_tag +WHERE post_id = $1 +` + +func (q *Queries) DeletePostTagByPostID(ctx context.Context, postID int32) error { + _, err := q.db.Exec(ctx, deletePostTagByPostID, postID) + return err +} + +const deletePostTagByTagID = `-- name: DeletePostTagByTagID :exec +DELETE +FROM post_tag +WHERE tag_id = $1 +` + +func (q *Queries) DeletePostTagByTagID(ctx context.Context, tagID int32) error { + _, err := q.db.Exec(ctx, deletePostTagByTagID, tagID) + return err +} + +const deleteTag = `-- name: DeleteTag :execrows +DELETE +FROM tags +WHERE id = $1 +` + +func (q *Queries) DeleteTag(ctx context.Context, id int32) (int64, error) { + result, err := q.db.Exec(ctx, deleteTag, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const listAllTags = `-- name: ListAllTags :many +SELECT id, name, code, sort, created_at, updated_at +FROM tags +ORDER BY sort DESC, id +` + +func (q *Queries) ListAllTags(ctx context.Context) ([]Tag, error) { + rows, err := q.db.Query(ctx, listAllTags) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Tag{} + for rows.Next() { + var i Tag + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Code, + &i.Sort, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listTags = `-- name: ListTags :many +SELECT id, name, code, sort, created_at, updated_at +FROM tags +ORDER BY sort DESC, id +LIMIT $1 OFFSET $2 +` + +type ListTagsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListTags(ctx context.Context, arg ListTagsParams) ([]Tag, error) { + rows, err := q.db.Query(ctx, listTags, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Tag{} + for rows.Next() { + var i Tag + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Code, + &i.Sort, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateTag = `-- name: UpdateTag :execrows +UPDATE tags +SET name = coalesce($1, name), + code = coalesce($2, code), + sort = coalesce($3, sort) +WHERE id = $4 +` + +type UpdateTagParams struct { + Name *string `json:"name"` + Code *string `json:"code"` + Sort *int32 `json:"sort"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateTag(ctx context.Context, arg UpdateTagParams) (int64, error) { + result, err := q.db.Exec(ctx, updateTag, + arg.Name, + arg.Code, + arg.Sort, + arg.ID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} diff --git a/internal/db/store.go b/internal/db/store.go new file mode 100644 index 0000000..3f2cebc --- /dev/null +++ b/internal/db/store.go @@ -0,0 +1,68 @@ +package db + +import ( + "context" + "fmt" + db "server/internal/db/sqlc" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type Store struct { + *db.Queries + pool *pgxpool.Pool +} + +func NewStore(pool *pgxpool.Pool) *Store { + return &Store{ + pool: pool, + Queries: db.New(pool), + } +} + +func (store *Store) WithTx(ctx context.Context, fn func(q *db.Queries) error, opts ...pgx.TxOptions) error { + txOpt := pgx.TxOptions{} + if len(opts) > 0 { + txOpt = opts[0] + } + + tx, err := store.pool.BeginTx(ctx, txOpt) + if err != nil { + return fmt.Errorf("begin tx: %w", err) + } + + defer func() { + // 使用 context.Background() 确保即使请求 context 被取消, + // 回滚指令也能发送给 DB,从而安全地回收连接而不是销毁连接。 + _ = tx.Rollback(context.Background()) + }() + + q := store.Queries.WithTx(tx) + + if err := fn(q); err != nil { + return err // 业务侧的错误保持原样返回 + } + + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("commit tx: %w", err) + } + + return nil +} + +// WithTxResult 带返回值的事务 +func WithTxResult[T any](ctx context.Context, store *Store, fn func(q *db.Queries) (T, error), opts ...pgx.TxOptions) (T, error) { + var result T + + err := store.WithTx(ctx, func(q *db.Queries) error { + r, err := fn(q) + if err != nil { + return err + } + result = r + return nil + }, opts...) + + return result, err +} diff --git a/internal/handler/auth.go b/internal/handler/auth.go new file mode 100644 index 0000000..0e90a29 --- /dev/null +++ b/internal/handler/auth.go @@ -0,0 +1,96 @@ +package handler + +import ( + "net/http" + "server/internal/config" + "server/internal/model/request" + "server/internal/pkg/errs" + "server/internal/pkg/httputil" + "server/internal/service" + "time" +) + +type AuthHandler struct { + authService *service.AuthService + cfg *config.Config +} + +func NewAuthHandler(authService *service.AuthService, cfg *config.Config) *AuthHandler { + return &AuthHandler{authService: authService, cfg: cfg} +} + +func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: h.cfg.JWTConfig.CookieName, + Value: "", + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteNoneMode, + MaxAge: -1, + Expires: time.Unix(0, 0), + }) +} + +func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { + var req request.LoginRequest + + if err := httputil.BindJson(r, &req); err != nil { + httputil.Fail(w, errs.ErrInvalidCredentials) + return + } + + res, err := h.authService.Login(r.Context(), req) + if err != nil { + httputil.Fail(w, err) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: h.cfg.JWTConfig.CookieName, + Value: res.RefreshToken, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteNoneMode, + Expires: res.RefreshTokenExp, + }) + + httputil.Ok(w, map[string]any{ + "access_token": res.AccessToken, + "access_token_exp": res.AccessTokenExp, + }) +} + +func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie(h.cfg.JWTConfig.CookieName) + if err == nil { + _ = h.authService.Logout(r.Context(), cookie.Value) + } + + h.clearRefreshTokenCookie(w) + httputil.Ok(w) +} + +func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie(h.cfg.JWTConfig.CookieName) + if err != nil { + h.clearRefreshTokenCookie(w) + httputil.Fail(w, errs.ErrInvalidRefreshToken) + return + } + + res, err := h.authService.RefreshToken(r.Context(), cookie.Value) + if err != nil { + // 用户被禁用 调用logout + _ = h.authService.Logout(r.Context(), cookie.Value) + h.clearRefreshTokenCookie(w) + httputil.Fail(w, err) + return + } + + httputil.Ok(w, map[string]any{ + "access_token": res.AccessToken, + "access_token_exp": res.AccessTokenExp, + }) +} diff --git a/internal/handler/module.go b/internal/handler/module.go index 31cdfda..4266555 100644 --- a/internal/handler/module.go +++ b/internal/handler/module.go @@ -13,6 +13,8 @@ var Module = fx.Module("handlers", NewSysFileHandler, NewSysPostHandler, NewCategoryHandler, + NewAuthHandler, + NewTagHandler, NewPostHandler, NewWebHandler, diff --git a/internal/handler/post.go b/internal/handler/post.go index 71b543e..d90a967 100644 --- a/internal/handler/post.go +++ b/internal/handler/post.go @@ -81,3 +81,13 @@ func (h *PostHandler) ListArchives(w http.ResponseWriter, r *http.Request) { httputil.Ok(w, list) } + +func (h *PostHandler) ListPostTags(w http.ResponseWriter, r *http.Request) { + list, err := h.postService.ListPostTags(r.Context()) + if err != nil { + httputil.Fail(w, err) + return + } + + httputil.Ok(w, list) +} diff --git a/internal/handler/sys_api.go b/internal/handler/sys_api.go index 72089e7..9b412d6 100644 --- a/internal/handler/sys_api.go +++ b/internal/handler/sys_api.go @@ -19,7 +19,13 @@ func NewSysApiHandler(apiService *service.SysApiService) *SysApiHandler { func (h *SysApiHandler) ListPage(w http.ResponseWriter, r *http.Request) { pagination := httputil.Pagination(r) - list, total, err := h.apiService.ListPage(r.Context(), pagination) + params := request.SearchSysApiParams{ + Pagination: *pagination, + GroupName: r.URL.Query().Get("group_name"), + Method: r.URL.Query().Get("method"), + } + + list, total, err := h.apiService.ListPage(r.Context(), params) if err != nil { httputil.Fail(w, err) diff --git a/internal/handler/sys_user.go b/internal/handler/sys_user.go index ab214cb..faea7a0 100644 --- a/internal/handler/sys_user.go +++ b/internal/handler/sys_user.go @@ -8,7 +8,6 @@ import ( "server/internal/pkg/errs" "server/internal/pkg/httputil" "server/internal/service" - "time" ) type SysUserHandler struct { @@ -21,74 +20,15 @@ func NewSysUserHandler(userService *service.SysUserService) *SysUserHandler { } } -func (h *SysUserHandler) Login(w http.ResponseWriter, r *http.Request) { - var req request.LoginRequest - - if err := httputil.BindJson(r, &req); err != nil { - httputil.Fail(w, errs.ErrInvalidCredentials) - return - } - - res, err := h.userService.Login(r.Context(), req) - if err != nil { - httputil.Fail(w, err) - return - } - - http.SetCookie(w, &http.Cookie{ - Name: "refresh_token", - Value: res.RefreshToken, - Path: "/", - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteNoneMode, - Expires: res.RefreshTokenExp, - }) - - httputil.Ok(w, map[string]any{ - "access_token": res.AccessToken, - "access_token_exp": res.AccessTokenExp, - }) -} - -func (h *SysUserHandler) Logout(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{ - Name: "refresh_token", - Value: "", - Path: "/", - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteNoneMode, - MaxAge: -1, - Expires: time.Unix(0, 0), - }) - - httputil.Ok(w) -} - -func (h *SysUserHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("refresh_token") - if err != nil { - httputil.Fail(w, errs.ErrInvalidRefreshToken) - return - } - - res, err := h.userService.RefreshToken(r.Context(), cookie.Value) - if err != nil { - httputil.Fail(w, err) - return - } - - httputil.Ok(w, map[string]any{ - "access_token": res.AccessToken, - "access_token_exp": res.AccessTokenExp, - }) -} - func (h *SysUserHandler) ListPage(w http.ResponseWriter, r *http.Request) { pagination := httputil.Pagination(r) - list, total, err := h.userService.ListPage(r.Context(), pagination) + params := request.SearchSysUserParams{ + Pagination: *pagination, + Username: r.URL.Query().Get("username"), + } + + list, total, err := h.userService.ListPage(r.Context(), params) if err != nil { httputil.Fail(w, err) return diff --git a/internal/handler/tag.go b/internal/handler/tag.go new file mode 100644 index 0000000..6812ff3 --- /dev/null +++ b/internal/handler/tag.go @@ -0,0 +1,99 @@ +package handler + +import ( + "net/http" + "server/internal/model/common" + "server/internal/model/request" + "server/internal/pkg/httputil" + "server/internal/service" +) + +type TagHandler struct { + tagService *service.TagService +} + +func NewTagHandler(tagService *service.TagService) *TagHandler { + return &TagHandler{ + tagService: tagService, + } +} + +func (h *TagHandler) ListPage(w http.ResponseWriter, r *http.Request) { + pagination := httputil.Pagination(r) + list, total, err := h.tagService.ListPage(r.Context(), pagination) + if err != nil { + httputil.Fail(w, err) + return + } + + resp := common.PageResponse{ + Page: pagination.Page, + PageSize: pagination.PageSize, + List: list, + Total: total, + } + httputil.OkWithPage(w, &resp) +} + +func (h *TagHandler) ListAll(w http.ResponseWriter, r *http.Request) { + list, err := h.tagService.ListAll(r.Context()) + if err != nil { + httputil.Fail(w, err) + return + } + + httputil.Ok(w, list) +} + +func (h *TagHandler) Create(w http.ResponseWriter, r *http.Request) { + var req request.CreateTagRequest + + if err := httputil.BindJson(r, &req); err != nil { + httputil.Fail(w, err) + return + } + + if err := h.tagService.Create(r.Context(), req); err != nil { + httputil.Fail(w, err) + return + } + + httputil.Ok(w) +} + +func (h *TagHandler) Update(w http.ResponseWriter, r *http.Request) { + id, err := httputil.URLParamInt32(r, "id") + if err != nil { + httputil.Fail(w, err) + return + } + + var req request.UpdateTagRequest + + if err = httputil.BindJson(r, &req); err != nil { + httputil.Fail(w, err) + return + } + + if err = h.tagService.Update(r.Context(), id, req); err != nil { + httputil.Fail(w, err) + return + } + + httputil.Ok(w) +} + +func (h *TagHandler) Delete(w http.ResponseWriter, r *http.Request) { + id, err := httputil.URLParamInt32(r, "id") + if err != nil { + httputil.Fail(w, err) + return + } + + if err = h.tagService.Delete(r.Context(), id); err != nil { + httputil.Fail(w, err) + return + } + + httputil.Ok(w) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 0bf3e0c..06ed2f7 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -3,8 +3,10 @@ package middleware import ( "context" "net/http" - db "server/internal/db/sqlc" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/pkg/cache" + "server/internal/pkg/cache/cachekey" "server/internal/pkg/errs" "server/internal/pkg/httputil" "strings" @@ -17,20 +19,21 @@ const ( ) type AuthMiddleware struct { - queries *db.Queries - cache *cache.Caches + store *db.Store + cache *cache.Caches } -func NewAuthMiddleware(queries *db.Queries, cache *cache.Caches) *AuthMiddleware { +func NewAuthMiddleware(store *db.Store, cache *cache.Caches) *AuthMiddleware { return &AuthMiddleware{ - cache: cache, - queries: queries, + cache: cache, + store: store, } } func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + var err error claims, ok := GetClaims(ctx) if !ok || claims.UserID == 0 { @@ -47,7 +50,8 @@ func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler { return } - hasPermission, err := userHasApiPermission(ctx, r, m.queries, claims.UserID, m.cache) + // 不是管理员 判断api权限 + hasPermission, err := userHasApiPermission(ctx, r, m.store, claims.UserID, m.cache) if err != nil { httputil.Fail(w, err) return @@ -70,21 +74,20 @@ func userIsAdmin(uid int32) bool { return false } -func userHasApiPermission(ctx context.Context, r *http.Request, queries *db.Queries, uid int32, cache *cache.Caches) (bool, error) { +func userHasApiPermission(ctx context.Context, r *http.Request, store *db.Store, uid int32, c *cache.Caches) (bool, error) { var ( - apis []db.GetSysUserApisRow + apis []sqlc.GetSysUserApisRow err error ) - apis, ok := cache.SysUserApisCache.GetIfPresent(uid) + // 先从缓存中获取api数据 + k := cachekey.UserApiPermissions(uid) + apis, err = cache.GetOrSetJSON[[]sqlc.GetSysUserApisRow](ctx, c, k, 0, func() ([]sqlc.GetSysUserApisRow, error) { + return store.GetSysUserApis(ctx, uid) + }) - if !ok { - apis, err = queries.GetSysUserApis(ctx, uid) - - if err != nil { - return false, err - } - cache.SysUserApisCache.Set(uid, apis) + if err != nil { + return false, err } requestPath := chi.RouteContext(r.Context()).RoutePattern() diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go index cc5b0be..a0c1577 100644 --- a/internal/middleware/jwt.go +++ b/internal/middleware/jwt.go @@ -2,7 +2,9 @@ package middleware import ( "context" - "errors" + "crypto/rand" + "crypto/sha256" + "encoding/hex" "net/http" "server/internal/config" "server/internal/pkg/errs" @@ -71,46 +73,6 @@ func (m *JWTMiddleware) ParseToken(tokenStr string) (*Claims, error) { return claims, nil } -// ParseRefreshToken 解析RefreshToken -func (m *JWTMiddleware) ParseRefreshToken(tokenStr string) (*RefreshClaims, error) { - token, err := jwt.ParseWithClaims( - tokenStr, - &RefreshClaims{}, - func(token *jwt.Token) (interface{}, error) { - return []byte(m.cfg.Secret), nil - }, - ) - - if err != nil { - // Token 已过期 - if errors.Is(err, jwt.ErrTokenExpired) { - return nil, errs.ErrExpiredRefreshToken - } - - // 签名错误、格式错误、非法 Token - return nil, errs.ErrInvalidRefreshToken - } - - if !token.Valid { - return nil, errs.ErrInvalidRefreshToken - } - - claims, ok := token.Claims.(*RefreshClaims) - if !ok { - return nil, errs.ErrInvalidRefreshToken - } - - if claims.UserID == 0 { - return nil, errs.ErrInvalidRefreshToken - } - - if claims.Type != RefreshTokenType { - return nil, errs.ErrInvalidRefreshToken - } - - return claims, nil -} - func (m *JWTMiddleware) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") @@ -161,24 +123,19 @@ func (m *JWTMiddleware) GenerateAccessToken(userID int32) (string, time.Time, er return token, expiresAt, nil } -func (m *JWTMiddleware) GenerateRefreshToken(userID int32) (string, time.Time, error) { - now := time.Now() - expiresAt := now.Add(m.cfg.RefreshExpire) +func (m *JWTMiddleware) GenerateRefreshToken() (string, error) { + b := make([]byte, 32) - refreshClaims := RefreshClaims{ - UserID: userID, - Type: RefreshTokenType, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(expiresAt), - IssuedAt: jwt.NewNumericDate(now), - }, - } - - refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) - token, err := refreshToken.SignedString([]byte(m.cfg.Secret)) + _, err := rand.Read(b) if err != nil { - return "", time.Time{}, err + return "", err } - return token, expiresAt, nil + return hex.EncodeToString(b), nil +} + +// HashRefreshToken 哈希token 存储至redis +func (m *JWTMiddleware) HashRefreshToken(token string) string { + hash := sha256.Sum256([]byte(token)) + return hex.EncodeToString(hash[:]) } diff --git a/internal/model/auth/jwt.go b/internal/model/auth/jwt.go new file mode 100644 index 0000000..d23212a --- /dev/null +++ b/internal/model/auth/jwt.go @@ -0,0 +1,9 @@ +package auth + +import "time" + +type RefreshTokenRecord struct { + UserID int32 `json:"user_id"` + CreatedAt time.Time `json:"created_at"` + ExpiresAt time.Time `json:"expires_at"` +} diff --git a/internal/model/request/category.go b/internal/model/request/category.go index d37a2ce..b9621e1 100644 --- a/internal/model/request/category.go +++ b/internal/model/request/category.go @@ -3,9 +3,11 @@ package request type CreateCategoryRequest struct { Name string `json:"name" validate:"required,min=1"` Code string `json:"code" validate:"required,min=1"` + Sort *int32 `json:"sort" validate:"required"` } type UpdateCategoryRequest struct { Name *string `json:"name" validate:"omitempty,min=1"` Code *string `json:"code" validate:"omitempty,min=1"` + Sort *int32 `json:"sort" validate:"omitempty"` } diff --git a/internal/model/request/post.go b/internal/model/request/post.go index 9d909ab..1f42cf3 100644 --- a/internal/model/request/post.go +++ b/internal/model/request/post.go @@ -6,7 +6,7 @@ import ( type CreatePostRequest struct { Title string `json:"title" validate:"required,min=1"` - CoverID *int32 `json:"cover_id" validate:"required,min=1"` + CoverID int32 `json:"cover_id" validate:"required,min=1"` Slug string `json:"slug" validate:"required,min=0"` Content string `json:"content" validate:"required,min=0"` Summary string `json:"summary" validate:"required,min=0"` @@ -14,6 +14,7 @@ type CreatePostRequest struct { Sort *int32 `json:"sort" validate:"required,min=0"` PublishedAt time.Time `json:"published_at" validate:"required"` CategoryID *int32 `json:"category_id" validate:"required,min=1"` + Tags []int32 `json:"tags" validate:"omitempty,dive,min=1"` } type UpdatePostRequest struct { @@ -27,4 +28,5 @@ type UpdatePostRequest struct { Sort *int32 `json:"sort" validate:"omitempty,min=0"` PublishedAt *time.Time `json:"published_at" validate:"omitempty"` CategoryID *int32 `json:"category_id" validate:"required,min=1"` + Tags []int32 `json:"tags" validate:"omitempty,dive,min=1"` } diff --git a/internal/model/request/sys_api.go b/internal/model/request/sys_api.go index ce7bb64..9a3067d 100644 --- a/internal/model/request/sys_api.go +++ b/internal/model/request/sys_api.go @@ -1,5 +1,7 @@ package request +import "server/internal/model/common" + type CreateSysApiRequest struct { Name string `json:"name" validate:"required,min=1,max=100"` GroupName string `json:"group_name" validate:"required,min=1,max=100"` @@ -15,3 +17,9 @@ type UpdateSysApiRequest struct { Method string `json:"method" validate:"required,oneof=GET POST PUT PATCH DELETE"` Sort *int32 `json:"sort" validate:"required,min=0"` } + +type SearchSysApiParams struct { + common.Pagination + Method string `json:"method" form:"method" validate:"omitempty,oneof=GET POST PUT PATCH DELETE"` + GroupName string `json:"group_name" form:"group_name" validate:"omitempty,max=100"` +} diff --git a/internal/model/request/sys_menu.go b/internal/model/request/sys_menu.go index fbda2ef..d0628eb 100644 --- a/internal/model/request/sys_menu.go +++ b/internal/model/request/sys_menu.go @@ -6,7 +6,7 @@ import ( type CreateSysMenuRequest struct { Name string `json:"name" validate:"required,min=1,max=100"` - Path string `json:"path" validate:"required,min=1,max=100"` + Path *string `json:"path" validate:"omitempty,max=100"` Type *int16 `json:"type" validate:"required,oneof=0 1 2"` Component *string `json:"component" validate:"omitempty"` Hidden *bool `json:"hidden" validate:"omitempty"` @@ -19,13 +19,13 @@ type CreateSysMenuRequest struct { type UpdateSysMenuRequest struct { Name *string `json:"name" validate:"omitempty,min=1,max=100"` - Path *string `json:"path" validate:"omitempty,min=1,max=100"` + Path *string `json:"path" validate:"omitempty,max=100"` Type *int16 `json:"type" validate:"omitempty,oneof=0 1 2"` Component *string `json:"component" validate:"omitempty"` Hidden *bool `json:"hidden" validate:"omitempty"` Sort *int32 `json:"sort" validate:"omitempty,min=0"` Status *int16 `json:"status" validate:"omitempty,oneof=0 1"` ParentID validator.NullInt32 `json:"parent_id" validate:"omitempty"` - Icon *int32 `json:"icon" validate:"omitempty"` + Icon validator.NullInt32 `json:"icon" validate:"omitempty"` PermissionCode *string `json:"permission_code" validate:"omitempty,min=1,max=100"` } diff --git a/internal/model/request/sys_permission.go b/internal/model/request/sys_permission.go deleted file mode 100644 index 725b8fc..0000000 --- a/internal/model/request/sys_permission.go +++ /dev/null @@ -1 +0,0 @@ -package request diff --git a/internal/model/request/sys_role.go b/internal/model/request/sys_role.go index 5b11c25..9d4c74d 100644 --- a/internal/model/request/sys_role.go +++ b/internal/model/request/sys_role.go @@ -1,12 +1,14 @@ package request type CreateSysRoleRequest struct { - Name string `json:"name" validate:"required,min=1,max=100"` - Code string `json:"code" validate:"required,max=100"` + Name string `json:"name" validate:"required,min=1,max=100"` + Code string `json:"code" validate:"required,max=100"` + Status *int16 `json:"status" validate:"required,oneof=0 1"` } type UpdateSysRoleRequest struct { - Name *string `json:"name" validate:"min=1,max=100"` + Name *string `json:"name" validate:"min=1,max=100"` + Status *int16 `json:"status" validate:"omitempty,oneof=0 1"` } type SetSysRoleMenusRequest struct { diff --git a/internal/model/request/sys_user.go b/internal/model/request/sys_user.go index d653175..6004d82 100644 --- a/internal/model/request/sys_user.go +++ b/internal/model/request/sys_user.go @@ -1,19 +1,22 @@ package request import ( + "server/internal/model/common" "server/internal/pkg/validator" ) type CreateSysUserRequest struct { - Username string `json:"username" validate:"required"` + Username string `json:"username" validate:"required,min=1,max=50"` Account string `json:"account" validate:"required,min=5,max=100"` Password string `json:"password" validate:"required,min=6,max=255"` AvatarID *int32 `json:"avatar_id" validate:"omitempty"` + Status *int16 `json:"status" validate:"oneof=0 1"` } type UpdateSysUserRequest struct { - Username *string `json:"username" validate:"omitempty,min=0,max=50"` + Username *string `json:"username" validate:"omitempty,min=1,max=50"` AvatarID validator.NullInt32 `json:"avatar_id" validate:"omitempty"` + Status *int16 `json:"status" validate:"omitempty,oneof=0 1"` } type UpdateSysUserPassword struct { @@ -28,3 +31,8 @@ type LoginRequest struct { Account string `json:"account" validate:"required,min=5,max=100"` Password string `json:"password" validate:"required,min=6,max=255"` } + +type SearchSysUserParams struct { + common.Pagination + Username string `json:"username" validate:"omitempty,max=50"` +} diff --git a/internal/model/request/tag.go b/internal/model/request/tag.go new file mode 100644 index 0000000..d468bf0 --- /dev/null +++ b/internal/model/request/tag.go @@ -0,0 +1,13 @@ +package request + +type CreateTagRequest struct { + Name string `json:"name" validate:"required,min=1"` + Code string `json:"code" validate:"required,min=1"` + Sort *int32 `json:"sort" validate:"required"` +} + +type UpdateTagRequest struct { + Name *string `json:"name" validate:"omitempty,min=1"` + Code *string `json:"code" validate:"omitempty,min=1"` + Sort *int32 `json:"sort" validate:"omitempty"` +} diff --git a/internal/model/response/sys_file.go b/internal/model/response/sys_file.go index 6c75027..d141787 100644 --- a/internal/model/response/sys_file.go +++ b/internal/model/response/sys_file.go @@ -1,12 +1,12 @@ package response import ( - db "server/internal/db/sqlc" + "server/internal/db/sqlc" "server/internal/pkg/httputil" ) -func ToFiles(files []db.File) []db.File { - result := make([]db.File, len(files)) +func ToFiles(files []sqlc.File) []sqlc.File { + result := make([]sqlc.File, len(files)) for i := range files { result[i] = files[i] result[i].FilePath = httputil.BuildFileUrl(&files[i].FilePath) diff --git a/internal/model/response/sys_user.go b/internal/model/response/sys_user.go index 69fae98..ea46f3e 100644 --- a/internal/model/response/sys_user.go +++ b/internal/model/response/sys_user.go @@ -1,7 +1,7 @@ package response import ( - db "server/internal/db/sqlc" + "server/internal/db/sqlc" "server/internal/pkg/httputil" "time" ) @@ -13,12 +13,13 @@ type SysUserRolesResponse struct { } type SysUserInfo struct { - ID int32 `json:"id"` - Account string `json:"account"` - Username string `json:"username"` - AvatarUrl string `json:"avatar_url"` - Roles []string `json:"roles"` - Menus []db.SysMenu `json:"menus"` + ID int32 `json:"id"` + Account string `json:"account"` + Username string `json:"username"` + AvatarUrl string `json:"avatar_url"` + Roles []string `json:"roles"` + Menus []sqlc.SysMenu `json:"menus"` + Permissions []string `json:"permissions"` } type LoginResponse struct { @@ -28,18 +29,26 @@ type LoginResponse struct { RefreshTokenExp time.Time `json:"refresh_token_exp"` } -func NewSysUserInfo(user db.GetSysUserByIDRow, roles []db.SysRole, menus []db.SysMenu) *SysUserInfo { +func NewSysUserInfo(user sqlc.GetSysUserByIDRow, roles []sqlc.SysRole, menus []sqlc.SysMenu, p []*string) *SysUserInfo { roleCodes := make([]string, len(roles)) for i, role := range roles { roleCodes[i] = role.Code } + permissions := make([]string, 0, len(p)) + for _, code := range p { + if code != nil { + permissions = append(permissions, *code) + } + } + return &SysUserInfo{ - ID: user.ID, - Account: user.Account, - Username: user.Username, - AvatarUrl: httputil.BuildFileUrl(user.AvatarUrl), - Roles: roleCodes, - Menus: menus, + ID: user.ID, + Account: user.Account, + Username: user.Username, + AvatarUrl: httputil.BuildFileUrl(user.AvatarUrl), + Roles: roleCodes, + Menus: menus, + Permissions: permissions, } } diff --git a/internal/pkg/cache/cache.go b/internal/pkg/cache/cache.go index b5aec24..341b72d 100644 --- a/internal/pkg/cache/cache.go +++ b/internal/pkg/cache/cache.go @@ -1,31 +1,219 @@ package cache import ( - "server/internal/db/sqlc" + "context" + "encoding/json" + "errors" + "strings" + "time" - "github.com/maypok86/otter/v2" + "github.com/redis/go-redis/v9" + + "server/internal/config" ) type Caches struct { - SysUserApisCache *otter.Cache[int32, []db.GetSysUserApisRow] + rdb *redis.Client + keyPrefix string } -func NewCaches() *Caches { - sysUserApisCache := otter.Must(&otter.Options[int32, []db.GetSysUserApisRow]{ - MaximumSize: 1_000, - }) - +func NewCaches(rdb *redis.Client, cfg *config.Config) *Caches { return &Caches{ - SysUserApisCache: sysUserApisCache, + rdb: rdb, + keyPrefix: cfg.Redis.KeyPrefix, } } -// ClearSysUserCache 清理单个用户缓存 -func (c *Caches) ClearSysUserCache(userID int32) { - c.SysUserApisCache.Invalidate(userID) +func (c *Caches) key(key string) string { + if c.keyPrefix == "" { + return key + } + return c.keyPrefix + key } -// ClearAllSysUserCache 清理所有用户缓存 -func (c *Caches) ClearAllSysUserCache() { - c.SysUserApisCache.InvalidateAll() +func (c *Caches) Set(ctx context.Context, key string, value any, expiration time.Duration) error { + return c.rdb.Set(ctx, c.key(key), value, expiration).Err() +} + +func (c *Caches) Get(ctx context.Context, key string) (string, bool, error) { + val, err := c.rdb.Get(ctx, c.key(key)).Result() + if errors.Is(err, redis.Nil) { + return "", false, nil + } + + if err != nil { + return "", false, err + } + + return val, true, nil +} + +func (c *Caches) SetJSON(ctx context.Context, key string, value any, expiration time.Duration) error { + b, err := json.Marshal(value) + if err != nil { + return err + } + return c.rdb.Set(ctx, c.key(key), b, expiration).Err() +} + +func GetJSON[T any](ctx context.Context, c *Caches, key string) (*T, bool, error) { + val, err := c.rdb.Get(ctx, c.key(key)).Bytes() + if errors.Is(err, redis.Nil) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + + var result T + if err = json.Unmarshal(val, &result); err != nil { + return nil, false, err + } + return &result, true, nil +} + +func GetOrSetJSON[T any](ctx context.Context, c *Caches, key string, expiration time.Duration, fetch func() (T, error)) (T, error) { + var zero T + + cacheKey := c.key(key) + + val, err := c.rdb.Get(ctx, cacheKey).Bytes() + if err == nil { + var result T + if err = json.Unmarshal(val, &result); err == nil { + return result, nil + } + } + if !errors.Is(err, redis.Nil) { + return zero, err + } + + result, err := fetch() + if err != nil { + return zero, err + } + + b, err := json.Marshal(result) + if err != nil { + return zero, err + } + _ = c.rdb.Set(ctx, cacheKey, b, expiration).Err() + + return result, nil +} + +func (c *Caches) Del(ctx context.Context, keys ...string) error { + for i := range keys { + keys[i] = c.key(keys[i]) + } + return c.rdb.Del(ctx, keys...).Err() +} + +func (c *Caches) Exists(ctx context.Context, key string) (bool, error) { + n, err := c.rdb.Exists(ctx, c.key(key)).Result() + if err != nil { + return false, err + } + + return n > 0, nil +} + +// SAdd 向 Set 中添加成员 +func (c *Caches) SAdd(ctx context.Context, key string, members ...any) error { + return c.rdb.SAdd(ctx, c.key(key), members...).Err() +} + +// SMembers 获取 Set 中所有成员 +func (c *Caches) SMembers(ctx context.Context, key string) ([]string, error) { + return c.rdb.SMembers(ctx, c.key(key)).Result() +} + +// DelSetMembers 删除 Set 中记录的所有成员对应的缓存 key,并删除 Set 本身 +func (c *Caches) DelSetMembers(ctx context.Context, key string) error { + members, err := c.SMembers(ctx, key) + if err != nil { + return err + } + + keys := append(members, key) + return c.Del(ctx, keys...) +} + +// SRem 从 Set 中移除成员 +func (c *Caches) SRem(ctx context.Context, key string, members ...any) error { + return c.rdb.SRem(ctx, c.key(key), members...).Err() +} + +// SIsMember 判断 member 是否在 Set 中 +func (c *Caches) SIsMember(ctx context.Context, key string, member any) (bool, error) { + return c.rdb.SIsMember(ctx, c.key(key), member).Result() +} + +// MGet 批量获取多个 key 的值 +func (c *Caches) MGet(ctx context.Context, keys ...string) (map[string]string, error) { + if len(keys) == 0 { + return map[string]string{}, nil + } + + redisKeys := make([]string, len(keys)) + for i, key := range keys { + redisKeys[i] = c.key(key) + } + + values, err := c.rdb.MGet(ctx, redisKeys...).Result() + if err != nil { + return nil, err + } + + result := make(map[string]string, len(keys)) + for i, value := range values { + if value == nil { + continue + } + + str, ok := value.(string) + if !ok { + continue + } + + result[keys[i]] = str + } + + return result, nil +} + +// DelByPrefix 批量删除指定前缀的缓存键 +func (c *Caches) DelByPrefix(ctx context.Context, prefix string) error { + prefix = c.key(prefix) + + if !strings.HasSuffix(prefix, "*") { + prefix += "*" + } + + var cursor uint64 + var keys []string + var err error + + for { + keys, cursor, err = c.rdb.Scan(ctx, cursor, prefix, 100).Result() + if err != nil { + return err + } + + if len(keys) > 0 { + if err = c.rdb.Del(ctx, keys...).Err(); err != nil { + return err + } + } + + if cursor == 0 { + break + } + } + + return nil +} + +func (c *Caches) Expire(ctx context.Context, key string, expiration time.Duration) error { + return c.rdb.Expire(ctx, c.keyPrefix+key, expiration).Err() } diff --git a/internal/pkg/cache/cachekey/key.go b/internal/pkg/cache/cachekey/key.go new file mode 100644 index 0000000..126d548 --- /dev/null +++ b/internal/pkg/cache/cachekey/key.go @@ -0,0 +1,25 @@ +package cachekey + +import "fmt" + +const ( + UserApiPermissionsPattern = "user:api:permissions:*" + UserInfoPattern = "user:info:*" +) + +func UserApiPermissions(id int32) string { + return fmt.Sprintf("user:api:permissions:%d", id) +} + +func UserInfo(id int32) string { + return fmt.Sprintf("user:info:%d", id) +} + +func AuthRefresh(hash string) string { + return fmt.Sprintf("auth:refresh:%s", hash) +} + +// AuthRefreshUser 反向索引 用于定位refresh token来删除 +func AuthRefreshUser(userID int32) string { + return fmt.Sprintf("auth:refresh:user:%d", userID) +} diff --git a/internal/pkg/dberr/postgres.go b/internal/pkg/dberr/postgres.go index b00d040..0d8c076 100644 --- a/internal/pkg/dberr/postgres.go +++ b/internal/pkg/dberr/postgres.go @@ -14,6 +14,8 @@ const ( SysPermissionsCodeKey = "sys_permissions_code_key" SysApisMethodPathKey = "sys_apis_method_path_key" CategoryCodeKey = "categories_code_key" + TagCodeKey = "tags_code_key" + SysMenusPathUniqueIdx = "sys_menus_path_unique_idx" ) func MapRowsAffected(rows int64, err error, notFoundErr error) error { diff --git a/internal/pkg/errs/errs.go b/internal/pkg/errs/errs.go index 88d59ad..6290bf9 100644 --- a/internal/pkg/errs/errs.go +++ b/internal/pkg/errs/errs.go @@ -20,6 +20,7 @@ func New(httpCode int, msg string) *AppError { var ( ErrInvalidCredentials = New(http.StatusUnauthorized, "用户名或密码错误") + ErrUserDisabled = New(http.StatusForbidden, "用户已被禁用") ErrUnauthenticated = New(http.StatusUnauthorized, "用户未登录或登录已失效") ErrUnauthorized = New(http.StatusUnauthorized, "认证失败:无法获取当前用户信息") ErrInvalidToken = New(http.StatusUnauthorized, "登录凭证无效") @@ -28,6 +29,7 @@ var ( ErrExpiredRefreshToken = New(http.StatusBadRequest, "invalid_grant") ErrUserNotFound = New(http.StatusNotFound, "用户数据不存在") ErrCategoryNotFound = New(http.StatusNotFound, "分类数据不存在") + ErrTagNotFound = New(http.StatusNotFound, "标签数据不存在") ErrSysApiNotFound = New(http.StatusNotFound, "接口数据不存在") ErrPostNotFound = New(http.StatusNotFound, "文章数据不存在") ErrSysMenuNotFound = New(http.StatusNotFound, "菜单数据不存在") @@ -40,9 +42,11 @@ var ( ErrEmptyBody = New(http.StatusBadRequest, "请求内容(body)不能为空") ErrInvalidJSON = New(http.StatusBadRequest, "请求数据格式错误") ErrAccountAlreadyExists = New(http.StatusBadRequest, "账号已存在") - ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码已存在") - ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug已存在") - ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码已存在") - ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码已存在") - ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)已存在") + ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码不允许重复") + ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug不允许重复") + ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码不允许重复") + ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码不允许重复") + ErrTagCodeAlreadyExists = New(http.StatusBadRequest, "标签编码不允许重复") + ErrSysMenusPathUniqueIdx = New(http.StatusBadRequest, "菜单路径不允许重复") + ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)不允许重复") ) diff --git a/internal/router/client.go b/internal/router/client.go index 477b6f2..7405c62 100644 --- a/internal/router/client.go +++ b/internal/router/client.go @@ -11,6 +11,7 @@ func SetupPostRoutes(r chi.Router, h *handler.PostHandler) { r.Get("/post/{slug}", h.GetPost) r.Get("/category/stats", h.ListCategoryStats) r.Get("/post/archive", h.ListArchives) + r.Get("/post/tags", h.ListPostTags) } func SetupWebRoutes(r chi.Router, h *handler.WebHandler) { diff --git a/internal/router/router.go b/internal/router/router.go index 2e22c77..dbbb880 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -19,6 +19,7 @@ type Handlers struct { AuthMiddleware *middleware.AuthMiddleware LoggerMiddleware *middleware.LoggerMiddleware // system + AuthHandler *handler.AuthHandler SysUserHandler *handler.SysUserHandler SysRoleHandler *handler.SysRoleHandler SysMenuHandler *handler.SysMenuHandler @@ -26,6 +27,7 @@ type Handlers struct { SysFileHandler *handler.SysFileHandler SysPostHandler *handler.SysPostHandler CategoryHandler *handler.CategoryHandler + TagHandler *handler.TagHandler // web PostHandler *handler.PostHandler WebHandler *handler.WebHandler @@ -50,9 +52,9 @@ func NewRouter(m Handlers) *chi.Mux { } r.Route("/api/admin", func(r chi.Router) { - r.Post("/auth/login", m.SysUserHandler.Login) - r.Post("/auth/refresh-token", m.SysUserHandler.RefreshToken) - r.Post("/auth/logout", m.SysUserHandler.Logout) + r.Post("/auth/login", m.AuthHandler.Login) + r.Post("/auth/refresh-token", m.AuthHandler.RefreshToken) + r.Post("/auth/logout", m.AuthHandler.Logout) r.Group(func(r chi.Router) { r.Use(m.JwtMiddleware.Middleware) @@ -65,6 +67,7 @@ func NewRouter(m Handlers) *chi.Mux { SetupSysFileRoutes(r, m.SysFileHandler) SetupCategoryRoutes(r, m.CategoryHandler) SetupSysPostRoutes(r, m.SysPostHandler) + SetupTagRoutes(r, m.TagHandler) }) }) diff --git a/internal/router/system.go b/internal/router/system.go index cc4b031..56ec531 100644 --- a/internal/router/system.go +++ b/internal/router/system.go @@ -6,6 +6,7 @@ import ( "github.com/go-chi/chi/v5" ) +// SetupSysUserRoutes 8 func SetupSysUserRoutes(r chi.Router, h *handler.SysUserHandler) { r.Get("/user", h.ListPage) r.Get("/user/info", h.GetUserInfo) @@ -17,6 +18,7 @@ func SetupSysUserRoutes(r chi.Router, h *handler.SysUserHandler) { r.Delete("/user/{id}", h.Delete) } +// SetupSysRoleRoutes 9 func SetupSysRoleRoutes(r chi.Router, h *handler.SysRoleHandler) { r.Get("/role", h.ListPage) r.Get("/role/{id}/menus", h.GetRoleMenus) @@ -29,6 +31,7 @@ func SetupSysRoleRoutes(r chi.Router, h *handler.SysRoleHandler) { r.Delete("/role/{id}", h.Delete) } +// SetupSysMenuRoutes 5 func SetupSysMenuRoutes(r chi.Router, h *handler.SysMenuHandler) { r.Get("/menu", h.ListPage) r.Get("/menu/all", h.GetMenus) @@ -37,6 +40,7 @@ func SetupSysMenuRoutes(r chi.Router, h *handler.SysMenuHandler) { r.Delete("/menu/{id}", h.Delete) } +// SetupSysApiRoutes 6 func SetupSysApiRoutes(r chi.Router, h *handler.SysApiHandler) { r.Get("/api", h.ListPage) r.Get("/api/all", h.GetAllSysApis) @@ -46,11 +50,13 @@ func SetupSysApiRoutes(r chi.Router, h *handler.SysApiHandler) { r.Delete("/api/{id}", h.Delete) } +// SetupSysFileRoutes 2 func SetupSysFileRoutes(r chi.Router, h *handler.SysFileHandler) { r.Get("/file", h.ListPage) r.Post("/file", h.Upload) } +// SetupSysPostRoutes 5 func SetupSysPostRoutes(r chi.Router, h *handler.SysPostHandler) { r.Get("/post", h.ListPage) r.Get("/post/{id}", h.GetPostById) @@ -59,6 +65,7 @@ func SetupSysPostRoutes(r chi.Router, h *handler.SysPostHandler) { r.Delete("/post/{id}", h.Delete) } +// SetupCategoryRoutes 5 func SetupCategoryRoutes(r chi.Router, h *handler.CategoryHandler) { r.Get("/category", h.ListPage) r.Get("/category/all", h.ListAll) @@ -66,3 +73,12 @@ func SetupCategoryRoutes(r chi.Router, h *handler.CategoryHandler) { r.Patch("/category/{id}", h.Update) r.Delete("/category/{id}", h.Delete) } + +// SetupTagRoutes 5 +func SetupTagRoutes(r chi.Router, h *handler.TagHandler) { + r.Get("/tag", h.ListPage) + r.Get("/tag/all", h.ListAll) + r.Post("/tag", h.Create) + r.Patch("/tag/{id}", h.Update) + r.Delete("/tag/{id}", h.Delete) +} diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000..af6c2e3 --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,180 @@ +package service + +import ( + "context" + "server/internal/config" + "server/internal/db" + "server/internal/middleware" + "server/internal/model/auth" + "server/internal/model/request" + "server/internal/model/response" + "server/internal/pkg/cache" + "server/internal/pkg/cache/cachekey" + "server/internal/pkg/errs" + "time" + + "golang.org/x/crypto/bcrypt" +) + +type AuthService struct { + jwt *middleware.JWTMiddleware + cache *cache.Caches + store *db.Store + cfg *config.Config +} + +func NewAuthService(jwt *middleware.JWTMiddleware, cache *cache.Caches, store *db.Store, cfg *config.Config) *AuthService { + return &AuthService{ + jwt: jwt, + cache: cache, + store: store, + cfg: cfg, + } +} + +// generatePasswordHash 生成密码哈希 +func generatePasswordHash(password string) (string, error) { + hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashed), nil +} + +// comparePasswordHash 比较密码哈希 +func comparePasswordHash(passwordHash, inputPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(inputPassword)) +} + +func (s *AuthService) Login(ctx context.Context, req request.LoginRequest) (*response.LoginResponse, error) { + user, err := s.store.GetSysUserByAccount(ctx, req.Account) + + if err != nil { + return nil, errs.ErrInvalidCredentials + } + + // 此处判断如果用户id不为1 且状态为0表示用户已被禁用 + if user.ID != 1 && user.Status == 0 { + return nil, errs.ErrUserDisabled + } + + if err = comparePasswordHash(user.PasswordHash, req.Password); err != nil { + return nil, errs.ErrInvalidCredentials + } + + accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(user.ID) + if err != nil { + return nil, err + } + + refreshToken, err := s.jwt.GenerateRefreshToken() + if err != nil { + return nil, err + } + + // 哈希 + hash := s.jwt.HashRefreshToken(refreshToken) + + now := time.Now() + + refreshTokenExp := now.Add(s.cfg.JWTConfig.RefreshExpire) + refreshTokenRecord := &auth.RefreshTokenRecord{ + UserID: user.ID, + CreatedAt: now, + ExpiresAt: refreshTokenExp, + } + + // 存入redis + err = s.cache.SetJSON(ctx, cachekey.AuthRefresh(hash), refreshTokenRecord, s.cfg.JWTConfig.RefreshExpire) + + if err != nil { + return nil, err + } + + // 反向索引 + err = s.cache.SAdd(ctx, cachekey.AuthRefreshUser(user.ID), hash) + + if err != nil { + return nil, err + } + + // 给反向索引设置过期时间 这个过期时间需要覆盖最后一个token的过期时间 所以用最新的就行了 + if err = s.cache.Expire(ctx, cachekey.AuthRefreshUser(user.ID), s.cfg.JWTConfig.RefreshExpire); err != nil { + return nil, err + } + + return &response.LoginResponse{ + AccessToken: accessToken, + AccessTokenExp: accessTokenExp, + RefreshToken: refreshToken, + RefreshTokenExp: refreshTokenExp, + }, nil +} + +func (s *AuthService) Logout(ctx context.Context, refreshToken string) error { + hash := s.jwt.HashRefreshToken(refreshToken) + + // 获取用户信息 + user, ok, err := cache.GetJSON[auth.RefreshTokenRecord](ctx, s.cache, cachekey.AuthRefresh(hash)) + if err != nil { + return err + } + + if ok { + // 清理反向索引 + if err = s.cache.SRem(ctx, cachekey.AuthRefreshUser(user.UserID), hash); err != nil { + return err + } + } + + // 清理当前登录的token + if err = s.cache.Del(ctx, cachekey.AuthRefresh(hash)); err != nil { + return err + } + + return nil +} + +func (s *AuthService) GetActiveSysUser(ctx context.Context, id int32) error { + var err error + + _, err = s.store.GetActiveSysUserByID(ctx, id) + if err != nil { + return err + } + + return nil +} + +func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*response.LoginResponse, error) { + // 获取哈希 + hash := s.jwt.HashRefreshToken(refreshToken) + // 从redis中获取数据 + record, ok, err := cache.GetJSON[auth.RefreshTokenRecord](ctx, s.cache, cachekey.AuthRefresh(hash)) + if err != nil { + return nil, errs.ErrInvalidRefreshToken + } + + if !ok { + return nil, errs.ErrInvalidRefreshToken + } + + // 如果不是超级用户 需要判断用户状态 + if record.UserID != 1 { + err = s.GetActiveSysUser(ctx, record.UserID) + if err != nil { + return nil, errs.ErrInvalidRefreshToken + } + } + + // 获取新的access token + accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(record.UserID) + if err != nil { + return nil, err + } + + return &response.LoginResponse{ + AccessToken: accessToken, + AccessTokenExp: accessTokenExp, + }, nil +} diff --git a/internal/service/category.go b/internal/service/category.go index 2965f8d..a5f7dce 100644 --- a/internal/service/category.go +++ b/internal/service/category.go @@ -2,7 +2,8 @@ package service import ( "context" - db "server/internal/db/sqlc" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/model/common" "server/internal/model/request" "server/internal/pkg/dberr" @@ -10,27 +11,27 @@ import ( ) type CategoryService struct { - queries *db.Queries + store *db.Store } -func NewCategoryService(queries *db.Queries) *CategoryService { +func NewCategoryService(store *db.Store) *CategoryService { return &CategoryService{ - queries: queries, + store: store, } } -func (s *CategoryService) ListPage(ctx context.Context, p *common.Pagination) ([]db.Category, int64, error) { - params := db.ListCategoriesParams{ +func (s *CategoryService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.Category, int64, error) { + params := sqlc.ListCategoriesParams{ Limit: p.PageSize, Offset: (p.Page - 1) * p.PageSize, } - total, err := s.queries.CountCategories(ctx) + total, err := s.store.CountCategories(ctx) if err != nil { return nil, 0, err } - list, err := s.queries.ListCategories(ctx, params) + list, err := s.store.ListCategories(ctx, params) if err != nil { return nil, 0, err } @@ -38,27 +39,29 @@ func (s *CategoryService) ListPage(ctx context.Context, p *common.Pagination) ([ return list, total, nil } -func (s *CategoryService) ListAll(ctx context.Context) ([]db.Category, error) { - return s.queries.ListAllCategories(ctx) +func (s *CategoryService) ListAll(ctx context.Context) ([]sqlc.Category, error) { + return s.store.ListAllCategories(ctx) } func (s *CategoryService) Create(ctx context.Context, req request.CreateCategoryRequest) error { - params := db.CreateCategoryParams{ + params := sqlc.CreateCategoryParams{ Name: req.Name, Code: req.Code, + Sort: req.Sort, } - err := s.queries.CreateCategory(ctx, params) + err := s.store.CreateCategory(ctx, params) return dberr.MapUniqueViolation(err, dberr.CategoryCodeKey, errs.ErrCategoryCodeAlreadyExists) } func (s *CategoryService) Update(ctx context.Context, id int32, req request.UpdateCategoryRequest) error { - params := db.UpdateCategoryParams{ + params := sqlc.UpdateCategoryParams{ ID: id, Name: req.Name, Code: req.Code, + Sort: req.Sort, } - rows, err := s.queries.UpdateCategory(ctx, params) + rows, err := s.store.UpdateCategory(ctx, params) // 先判断数据条目是否存在 if err = dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound); err != nil { // 再判断code是否重复 @@ -69,6 +72,13 @@ func (s *CategoryService) Update(ctx context.Context, id int32, req request.Upda } func (s *CategoryService) Delete(ctx context.Context, id int32) error { - rows, err := s.queries.DeleteCategory(ctx, id) - return dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound) + return s.store.WithTx(ctx, func(q *sqlc.Queries) error { + err := q.DeletePostCategoryByCategoryID(ctx, id) + if err != nil { + return err + } + + rows, err := q.DeleteCategory(ctx, id) + return dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound) + }) } diff --git a/internal/service/module.go b/internal/service/module.go index cc08d8a..e3e6c28 100644 --- a/internal/service/module.go +++ b/internal/service/module.go @@ -12,6 +12,8 @@ var Module = fx.Module("services", NewSysApiService, NewSysFileService, NewSysPostService, + NewAuthService, + NewTagService, NewPostService, NewCategoryService, diff --git a/internal/service/post.go b/internal/service/post.go index 958b86a..a5f70b8 100644 --- a/internal/service/post.go +++ b/internal/service/post.go @@ -3,7 +3,8 @@ package service import ( "context" "net/netip" - db "server/internal/db/sqlc" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/model/common" "server/internal/model/response" "server/internal/pkg/dberr" @@ -12,27 +13,27 @@ import ( ) type PostService struct { - queries *db.Queries + store *db.Store } -func NewPostService(queries *db.Queries) *PostService { +func NewPostService(store *db.Store) *PostService { return &PostService{ - queries: queries, + store: store, } } -func (s *PostService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListPublishedPostsRow, int64, error) { - params := db.ListPublishedPostsParams{ +func (s *PostService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.ListPublishedPostsRow, int64, error) { + params := sqlc.ListPublishedPostsParams{ Limit: p.PageSize, Offset: (p.Page - 1) * p.PageSize, } - total, err := s.queries.CountPublishedPosts(ctx) + total, err := s.store.CountPublishedPosts(ctx) if err != nil { return nil, 0, err } - list, err := s.queries.ListPublishedPosts(ctx, params) + list, err := s.store.ListPublishedPosts(ctx, params) if err != nil { return nil, 0, err } @@ -45,13 +46,13 @@ func (s *PostService) ListPage(ctx context.Context, p *common.Pagination) ([]db. return list, total, nil } -func (s *PostService) GetPost(ctx context.Context, slug string, ip netip.Addr) (*db.GetPublicPostBySlugRow, error) { - post, err := s.queries.GetPublicPostBySlug(ctx, slug) +func (s *PostService) GetPost(ctx context.Context, slug string, ip netip.Addr) (*sqlc.GetPublicPostBySlugRow, error) { + post, err := s.store.GetPublicPostBySlug(ctx, slug) if err != nil { return nil, dberr.MapNoRows(err, errs.ErrPostNotFound) } - _ = s.queries.IncrementPostStatsView(ctx, db.IncrementPostStatsViewParams{ + _ = s.store.IncrementPostStatsView(ctx, sqlc.IncrementPostStatsViewParams{ PostID: post.ID, Ip: ip, }) @@ -59,12 +60,12 @@ func (s *PostService) GetPost(ctx context.Context, slug string, ip netip.Addr) ( return &post, nil } -func (s *PostService) ListCategoryStats(ctx context.Context) ([]db.ListCategoryStatsRow, error) { - return s.queries.ListCategoryStats(ctx) +func (s *PostService) ListCategoryStats(ctx context.Context) ([]sqlc.ListCategoryStatsRow, error) { + return s.store.ListCategoryStats(ctx) } func (s *PostService) ListArchives(ctx context.Context) ([]response.ArchiveYear, error) { - list, err := s.queries.ListArchives(ctx) + list, err := s.store.ListArchives(ctx) if err != nil { return nil, err } @@ -111,3 +112,7 @@ func (s *PostService) ListArchives(ctx context.Context) ([]response.ArchiveYear, return archive, nil } + +func (s *PostService) ListPostTags(ctx context.Context) ([]sqlc.Tag, error) { + return s.store.ListAllTags(ctx) +} diff --git a/internal/service/sys_api.go b/internal/service/sys_api.go index d7c86aa..6bb291c 100644 --- a/internal/service/sys_api.go +++ b/internal/service/sys_api.go @@ -2,41 +2,46 @@ package service import ( "context" - db "server/internal/db/sqlc" - "server/internal/model/common" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/model/enum" "server/internal/model/request" "server/internal/pkg/cache" + "server/internal/pkg/cache/cachekey" "server/internal/pkg/dberr" "server/internal/pkg/errs" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" ) type SysApiService struct { - queries *db.Queries - pool *pgxpool.Pool - cache *cache.Caches + store *db.Store + cache *cache.Caches } -func NewSysApiService(queries *db.Queries, pool *pgxpool.Pool, cache *cache.Caches) *SysApiService { - return &SysApiService{queries: queries, pool: pool, cache: cache} +func NewSysApiService(store *db.Store, cache *cache.Caches) *SysApiService { + return &SysApiService{ + store: store, + cache: cache, + } } -func (s *SysApiService) ListPage(ctx context.Context, p *common.Pagination) ([]db.SysApi, int64, error) { - params := db.GetSysApisParams{ - Limit: p.PageSize, - Offset: (p.Page - 1) * p.PageSize, +func (s *SysApiService) ListPage(ctx context.Context, p request.SearchSysApiParams) ([]sqlc.SysApi, int64, error) { + params := sqlc.GetSysApisParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + GroupName: p.GroupName, + Method: p.Method, } - total, err := s.queries.CountSysApis(ctx) + total, err := s.store.CountSysApis(ctx, sqlc.CountSysApisParams{ + GroupName: p.GroupName, + Method: p.Method, + }) if err != nil { return nil, 0, err } - list, err := s.queries.GetSysApis(ctx, params) + list, err := s.store.GetSysApis(ctx, params) if err != nil { return nil, 0, err @@ -45,69 +50,54 @@ func (s *SysApiService) ListPage(ctx context.Context, p *common.Pagination) ([]d return list, total, nil } -func (s *SysApiService) GetAllSysApis(ctx context.Context) ([]db.SysApi, error) { - return s.queries.GetAllSysApis(ctx) +func (s *SysApiService) GetAllSysApis(ctx context.Context) ([]sqlc.SysApi, error) { + return s.store.GetAllSysApis(ctx) } func (s *SysApiService) GetApiGroupNames(ctx context.Context) ([]string, error) { - return s.queries.GetSysApiGroupNames(ctx) + return s.store.GetSysApiGroupNames(ctx) } func (s *SysApiService) Create(ctx context.Context, req request.CreateSysApiRequest) error { - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) - if err != nil { - return err - } + return s.store.WithTx(ctx, func(q *sqlc.Queries) error { + api := sqlc.CreateSysApiParams{ + Name: req.Name, + GroupName: req.GroupName, + Method: req.Method, + Path: req.Path, + Sort: req.Sort, + } - defer func(ctx context.Context) { - _ = tx.Rollback(ctx) - }(ctx) + // 创建权限 + permissionId, err := q.CreateSysPermission(ctx, sqlc.CreateSysPermissionParams{ + Type: int16(enum.PermissionTypeApi), + }) - q := db.New(tx) + if err != nil { + return err + } - api := db.CreateSysApiParams{ - Name: req.Name, - GroupName: req.GroupName, - Method: req.Method, - Path: req.Path, - Sort: req.Sort, - } + // 创建api + apiId, err := q.CreateSysApi(ctx, api) + if err != nil { + return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists) + } + + // 关联权限 + if err = q.CreateSysApiPermission(ctx, sqlc.CreateSysApiPermissionParams{ + ApiID: apiId, + PermissionID: permissionId, + }); err != nil { + return err + } + + return nil - // 创建权限 - permissionId, err := q.CreateSysPermission(ctx, db.CreateSysPermissionParams{ - Type: int16(enum.PermissionTypeApi), }) - - if err != nil { - return err - } - - // 创建api - apiId, err := q.CreateSysApi(ctx, api) - if err != nil { - return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists) - } - - // 关联权限 - if err = q.CreateSysApiPermission(ctx, db.CreateSysApiPermissionParams{ - ApiID: apiId, - PermissionID: permissionId, - }); err != nil { - return err - } - - if err = tx.Commit(ctx); err != nil { - return err - } - - // 清理缓存 - s.cache.ClearAllSysUserCache() - - return nil } func (s *SysApiService) Update(ctx context.Context, id int32, req request.UpdateSysApiRequest) error { - api := db.UpdateSysApiParams{ + api := sqlc.UpdateSysApiParams{ ID: id, Name: req.Name, GroupName: req.GroupName, @@ -116,43 +106,51 @@ func (s *SysApiService) Update(ctx context.Context, id int32, req request.Update Sort: req.Sort, } - // 清理缓存 - s.cache.ClearAllSysUserCache() - - rows, err := s.queries.UpdateSysApi(ctx, api) + rows, err := s.store.UpdateSysApi(ctx, api) if err = dberr.MapRowsAffected(rows, err, errs.ErrSysApiNotFound); err != nil { return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists) } + // 如果更新成功 则清理缓存 + _ = s.cache.DelByPrefix(ctx, cachekey.UserApiPermissionsPattern) + return nil } func (s *SysApiService) Delete(ctx context.Context, id int32) error { - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + err := s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 1. 找出权限id + permissionID, err := q.GetSysPermissionIDBySysApiID(ctx, id) + if err != nil { + return dberr.MapNoRows(err, errs.ErrSysApiNotFound) + } + // 2. 根据权限id,删除角色权限关联数据 + if err = q.DeleteSysRolePermissionByPermissionID(ctx, permissionID); err != nil { + return err + } + // 3. 根据api id 删除sys_api_permission关联表数据 + if err = q.DeleteSysApiPermission(ctx, id); err != nil { + return err + } + // 4. 根据权限id,删除权限表权限数据 + if err = q.DeleteSysPermission(ctx, permissionID); err != nil { + return err + } + // 5. 删除api + rows, err := q.DeleteSysApi(ctx, id) + if err = dberr.MapRowsAffected(rows, err, errs.ErrSysApiNotFound); err != nil { + return err + } + + return nil + }) if err != nil { return err } - defer tx.Rollback(ctx) + // 清理缓存 事务执行成功后才清理 + _ = s.cache.DelByPrefix(ctx, cachekey.UserApiPermissionsPattern) - q := db.New(tx) - - rows, err := q.DeleteSysApi(ctx, id) - if err = dberr.MapRowsAffected(rows, err, errs.ErrSysApiNotFound); err != nil { - return err - } - - if err = q.DeleteSysPermissionBySysApiID(ctx, id); err != nil { - return err - } - - if err = q.DeleteSysApiPermission(ctx, id); err != nil { - return err - } - - // 清理缓存 - s.cache.ClearAllSysUserCache() - - return tx.Commit(ctx) + return nil } diff --git a/internal/service/sys_file.go b/internal/service/sys_file.go index f93d28e..0a0b0fe 100644 --- a/internal/service/sys_file.go +++ b/internal/service/sys_file.go @@ -5,7 +5,8 @@ import ( "mime/multipart" "os" "path/filepath" - db "server/internal/db/sqlc" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/model/common" "server/internal/model/response" "server/internal/pkg/httputil" @@ -14,12 +15,12 @@ import ( ) type SysFileService struct { - queries *db.Queries + store *db.Store } -func NewSysFileService(queries *db.Queries) *SysFileService { +func NewSysFileService(store *db.Store) *SysFileService { return &SysFileService{ - queries: queries, + store: store, } } @@ -40,19 +41,19 @@ func MakeSavedDir(folder string) (string, error) { return uploadDir, nil } -func (s *SysFileService) ListPage(ctx context.Context, p *common.Pagination) ([]db.File, int64, error) { - params := db.GetFilesParams{ +func (s *SysFileService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.File, int64, error) { + params := sqlc.GetFilesParams{ Limit: p.PageSize, Offset: (p.Page - 1) * p.PageSize, } - total, err := s.queries.CountFiles(ctx) + total, err := s.store.CountFiles(ctx) if err != nil { return nil, 0, err } - list, err := s.queries.GetFiles(ctx, params) + list, err := s.store.GetFiles(ctx, params) if err != nil { return nil, 0, err @@ -61,7 +62,7 @@ func (s *SysFileService) ListPage(ctx context.Context, p *common.Pagination) ([] return response.ToFiles(list), total, nil } -func (s *SysFileService) Upload(ctx context.Context, folder string, file *multipart.FileHeader) (*db.CreateFileRow, error) { +func (s *SysFileService) Upload(ctx context.Context, folder string, file *multipart.FileHeader) (*sqlc.CreateFileRow, error) { // 生成文件名 fileID, err := gonanoid.New() if err != nil { @@ -98,7 +99,7 @@ func (s *SysFileService) Upload(ctx context.Context, folder string, file *multip return nil, err } - params := db.CreateFileParams{ + params := sqlc.CreateFileParams{ FileName: filename, FilePath: filePath, OriginalName: file.Filename, @@ -107,7 +108,7 @@ func (s *SysFileService) Upload(ctx context.Context, folder string, file *multip FileSize: file.Size, } - result, err := s.queries.CreateFile(ctx, params) + result, err := s.store.CreateFile(ctx, params) if err != nil { return nil, err } diff --git a/internal/service/sys_menu.go b/internal/service/sys_menu.go index 54e3d7d..04997cf 100644 --- a/internal/service/sys_menu.go +++ b/internal/service/sys_menu.go @@ -2,75 +2,72 @@ package service import ( "context" - db "server/internal/db/sqlc" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/model/common" "server/internal/model/enum" "server/internal/model/request" + "server/internal/pkg/cache" + "server/internal/pkg/cache/cachekey" "server/internal/pkg/dberr" "server/internal/pkg/errs" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" ) type SysMenuService struct { - pool *pgxpool.Pool - queries *db.Queries + store *db.Store + cache *cache.Caches } -func NewSysMenuService(queries *db.Queries, pool *pgxpool.Pool) *SysMenuService { +func NewSysMenuService(store *db.Store, cache *cache.Caches) *SysMenuService { return &SysMenuService{ - queries: queries, - pool: pool, + store: store, + cache: cache, } } func (s *SysMenuService) Create(ctx context.Context, req request.CreateSysMenuRequest) error { - // 开启事务 - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) - if err != nil { - return err - } + err := s.store.WithTx(ctx, func(q *sqlc.Queries) error { + menu := sqlc.CreateSysMenuParams{ + Name: req.Name, + Path: req.Path, + Component: req.Component, + Type: *req.Type, + Status: *req.Status, + Hidden: req.Hidden, + Sort: req.Sort, + Icon: req.Icon, + ParentID: req.ParentID, + } - defer func(ctx context.Context) { - _ = tx.Rollback(ctx) - }(ctx) + // 先创建权限 + permissionId, err := q.CreateSysPermission(ctx, sqlc.CreateSysPermissionParams{ + Type: int16(enum.PermissionTypeMenu), + Code: &req.PermissionCode, + }) - q := db.New(tx) + // code重复性校验 + if err != nil { + return dberr.MapUniqueViolation(err, dberr.SysPermissionsCodeKey, errs.ErrPermissionCodeAlreadyExists) + } - menu := db.CreateSysMenuParams{ - Name: req.Name, - Path: req.Path, - Component: req.Component, - Type: *req.Type, - Status: *req.Status, - Hidden: req.Hidden, - Sort: req.Sort, - Icon: req.Icon, - } + // 创建菜单 + menuId, err := q.CreateSysMenu(ctx, menu) + if err != nil { + return dberr.MapUniqueViolation(err, dberr.SysMenusPathUniqueIdx, errs.ErrSysMenusPathUniqueIdx) + } - permissionId, err := q.CreateSysPermission(ctx, db.CreateSysPermissionParams{ - Type: int16(enum.PermissionTypeMenu), - Code: &req.PermissionCode, + // 绑定权限 + if err = q.CreateSysMenuPermission(ctx, sqlc.CreateSysMenuPermissionParams{ + MenuID: menuId, + PermissionID: permissionId, + }); err != nil { + return err + } + + return nil }) if err != nil { - return dberr.MapUniqueViolation(err, dberr.SysPermissionsCodeKey, errs.ErrPermissionCodeAlreadyExists) - } - - menuId, err := q.CreateSysMenu(ctx, menu) - if err != nil { - return err - } - - if err = q.CreateSysMenuPermission(ctx, db.CreateSysMenuPermissionParams{ - MenuID: menuId, - PermissionID: permissionId, - }); err != nil { - return err - } - - if err = tx.Commit(ctx); err != nil { return err } @@ -78,66 +75,76 @@ func (s *SysMenuService) Create(ctx context.Context, req request.CreateSysMenuRe } func (s *SysMenuService) Update(ctx context.Context, id int32, req request.UpdateSysMenuRequest) error { - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + err := s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 构造 menu 参数 + menu := sqlc.UpdateSysMenuParams{ + ID: id, + Name: req.Name, + Path: req.Path, + Component: req.Component, + Hidden: req.Hidden, + Sort: req.Sort, + Type: req.Type, + Status: req.Status, + } + + if req.Icon.Set { + menu.UpdateIcon = true + if req.Icon.Valid { + menu.Icon = &req.Icon.Value + } + } + + if req.ParentID.Set { + menu.UpdateParentID = true + if req.ParentID.Valid { + menu.ParentID = &req.ParentID.Value + } + } + + // 执行更新 + rows, err := q.UpdateSysMenu(ctx, menu) + + // 先判断数据是否存在 在判断path是否重复 + if err = dberr.MapRowsAffected(rows, err, errs.ErrSysMenuNotFound); err != nil { + return dberr.MapUniqueViolation(err, dberr.SysMenusPathUniqueIdx, errs.ErrSysMenusPathUniqueIdx) + } + + permission := sqlc.UpdateSysMenuPermissionCodeParams{ + MenuID: id, + Code: req.PermissionCode, + } + + if err = q.UpdateSysMenuPermissionCode(ctx, permission); err != nil { + return dberr.MapUniqueViolation(err, dberr.SysPermissionsCodeKey, errs.ErrPermissionCodeAlreadyExists) + } + + return nil + }) + if err != nil { return err } - defer tx.Rollback(ctx) - q := db.New(tx) + // 清理缓存 + _ = s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern) - // 构造 menu 参数 - menu := db.UpdateSysMenuParams{ - ID: id, - Name: req.Name, - Path: req.Path, - Component: req.Component, - Hidden: req.Hidden, - Sort: req.Sort, - Type: req.Type, - Status: req.Status, - Icon: req.Icon, - } - - if req.ParentID.Set { - menu.UpdateParentID = true - if req.ParentID.Valid { - menu.ParentID = &req.ParentID.Value - } - } - - // 执行更新 - rows, err := q.UpdateSysMenu(ctx, menu) - - if err = dberr.MapRowsAffected(rows, err, errs.ErrSysMenuNotFound); err != nil { - return err - } - - permission := db.UpdateSysMenuPermissionCodeParams{ - MenuID: id, - Code: req.PermissionCode, - } - - if err = q.UpdateSysMenuPermissionCode(ctx, permission); err != nil { - return dberr.MapUniqueViolation(err, dberr.SysPermissionsCodeKey, errs.ErrPermissionCodeAlreadyExists) - } - - return tx.Commit(ctx) + return nil } -func (s *SysMenuService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListSysMenusRow, int64, error) { - params := db.ListSysMenusParams{ +func (s *SysMenuService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.ListSysMenusRow, int64, error) { + params := sqlc.ListSysMenusParams{ Limit: p.PageSize, Offset: (p.Page - 1) * p.PageSize, } - total, err := s.queries.CountSysMenus(ctx) + total, err := s.store.CountSysMenus(ctx) if err != nil { return nil, 0, err } - list, err := s.queries.ListSysMenus(ctx, params) + list, err := s.store.ListSysMenus(ctx, params) if err != nil { return nil, 0, err } @@ -145,34 +152,43 @@ func (s *SysMenuService) ListPage(ctx context.Context, p *common.Pagination) ([] return list, total, nil } -func (s *SysMenuService) GetMenus(ctx context.Context) ([]db.GetAllSysMenusRow, error) { - return s.queries.GetAllSysMenus(ctx) +func (s *SysMenuService) GetMenus(ctx context.Context) ([]sqlc.GetAllSysMenusRow, error) { + return s.store.GetAllSysMenus(ctx) } func (s *SysMenuService) Delete(ctx context.Context, id int32) error { - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + err := s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 1. 根据菜单id, 找出对应的权限id + permissionID, err := q.GetSysPermissionIDBySysMenuID(ctx, id) + if err != nil { + return dberr.MapNoRows(err, errs.ErrSysMenuNotFound) + } + // 2. 根据权限id 删除角色权限关联 + if err = q.DeleteSysRolePermissionByPermissionID(ctx, permissionID); err != nil { + return err + } + // 3. 根据菜单id 删除sys_menu_permission关联表数据 + if err = q.DeleteSysMenuPermission(ctx, id); err != nil { + return err + } + // 4. 根据权限id删除权限表数据 + if err = q.DeleteSysPermission(ctx, permissionID); err != nil { + return err + } + // 5. 删除菜单 + rows, err := q.DeleteSysMenu(ctx, id) + if err = dberr.MapRowsAffected(rows, err, errs.ErrSysMenuNotFound); err != nil { + return err + } + + return nil + }) if err != nil { return err } - defer tx.Rollback(ctx) - - q := db.New(tx) - - rows, err := q.DeleteSysMenu(ctx, id) - - if err = dberr.MapRowsAffected(rows, err, errs.ErrSysMenuNotFound); err != nil { - return err - } - - if err = q.DeleteSysPermissionByMenuID(ctx, id); err != nil { - return err - } - - if err = q.DeleteSysMenuPermission(ctx, id); err != nil { - return err - } - - return tx.Commit(ctx) + // 清理缓存 + _ = s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern) + return nil } diff --git a/internal/service/sys_post.go b/internal/service/sys_post.go index 4f758c9..e6d7606 100644 --- a/internal/service/sys_post.go +++ b/internal/service/sys_post.go @@ -2,41 +2,37 @@ package service import ( "context" - db "server/internal/db/sqlc" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/model/common" "server/internal/model/request" "server/internal/pkg/dberr" "server/internal/pkg/errs" "server/internal/pkg/httputil" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" ) type SysPostService struct { - queries *db.Queries - pool *pgxpool.Pool + store *db.Store } -func NewSysPostService(queries *db.Queries, pool *pgxpool.Pool) *SysPostService { +func NewSysPostService(store *db.Store) *SysPostService { return &SysPostService{ - queries: queries, - pool: pool, + store: store, } } -func (s *SysPostService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListPostsRow, int64, error) { - params := db.ListPostsParams{ +func (s *SysPostService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.ListPostsRow, int64, error) { + params := sqlc.ListPostsParams{ Limit: p.PageSize, Offset: (p.Page - 1) * p.PageSize, } - total, err := s.queries.CountPosts(ctx) + total, err := s.store.CountPosts(ctx) if err != nil { return nil, 0, err } - list, err := s.queries.ListPosts(ctx, params) + list, err := s.store.ListPosts(ctx, params) if err != nil { return nil, 0, err } @@ -49,8 +45,8 @@ func (s *SysPostService) ListPage(ctx context.Context, p *common.Pagination) ([] return list, total, nil } -func (s *SysPostService) FindByID(ctx context.Context, id int32) (*db.GetPostByIdRow, error) { - post, err := s.queries.GetPostById(ctx, id) +func (s *SysPostService) FindByID(ctx context.Context, id int32) (*sqlc.GetPostByIdRow, error) { + post, err := s.store.GetPostById(ctx, id) if err != nil { return nil, dberr.MapNoRows(err, errs.ErrPostNotFound) } @@ -61,103 +57,117 @@ func (s *SysPostService) FindByID(ctx context.Context, id int32) (*db.GetPostByI } func (s *SysPostService) Create(ctx context.Context, req request.CreatePostRequest) (int32, error) { - // 开启事务 - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + postID, err := db.WithTxResult(ctx, s.store, func(q *sqlc.Queries) (int32, error) { + params := sqlc.CreatePostParams{ + Title: req.Title, + CoverID: &req.CoverID, + Slug: req.Slug, + Content: req.Content, + Summary: req.Summary, + Status: *req.Status, + Sort: req.Sort, + PublishedAt: req.PublishedAt, + } + + // 创建博客 + postID, err := q.CreatePost(ctx, params) + if err != nil { + return 0, dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists) + } + + // 关联分类 + if err = q.CreatePostCategory(ctx, sqlc.CreatePostCategoryParams{ + PostID: postID, + CategoryID: *req.CategoryID, + }); err != nil { + return 0, err + } + + var tagsParams []sqlc.CreatePostTagParams + for _, tagID := range req.Tags { + tagsParams = append(tagsParams, sqlc.CreatePostTagParams{ + PostID: postID, + TagID: tagID, + }) + } + + if _, err = q.CreatePostTag(ctx, tagsParams); err != nil { + return 0, err + } + + return postID, nil + }) if err != nil { return 0, err } - defer func() { - _ = tx.Rollback(ctx) - }() - - q := db.New(tx) - - params := db.CreatePostParams{ - Title: req.Title, - CoverID: req.CoverID, - Slug: req.Slug, - Content: req.Content, - Summary: req.Summary, - Status: *req.Status, - Sort: req.Sort, - PublishedAt: req.PublishedAt, - } - - postId, err := q.CreatePost(ctx, params) - if err != nil { - return 0, dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists) - } - - if err = q.CreatePostCategory(ctx, db.CreatePostCategoryParams{ - PostID: postId, - CategoryID: *req.CategoryID, - }); err != nil { - return 0, err - } - - // 提交 - if err = tx.Commit(ctx); err != nil { - return 0, err - } - - return postId, nil + return postID, nil } func (s *SysPostService) Update(ctx context.Context, id int32, req request.UpdatePostRequest) error { - // 开启事务 - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) - if err != nil { - return err - } + return s.store.WithTx(ctx, func(q *sqlc.Queries) error { + params := sqlc.UpdatePostParams{ + Title: req.Title, + CoverID: req.CoverID, + Slug: req.Slug, + Content: req.Content, + Summary: req.Summary, + Status: req.Status, + Sort: req.Sort, + PublishedAt: req.PublishedAt, + ID: id, + } - defer func() { - _ = tx.Rollback(ctx) - }() + rows, err := q.UpdatePost(ctx, params) + if err != nil { + return dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists) + } - q := db.New(tx) + if err = dberr.MapRowsAffected(rows, nil, errs.ErrPostNotFound); err != nil { + return err + } - params := db.UpdatePostParams{ - Title: req.Title, - CoverID: req.CoverID, - Slug: req.Slug, - Content: req.Content, - Summary: req.Summary, - Status: req.Status, - Sort: req.Sort, - PublishedAt: req.PublishedAt, - ID: id, - } + // 先删除分类 + if err = q.DeletePostCategoryByPostID(ctx, id); err != nil { + return err + } - rows, err := q.UpdatePost(ctx, params) - if err != nil { - return dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists) - } + if err = q.CreatePostCategory(ctx, sqlc.CreatePostCategoryParams{ + PostID: id, + CategoryID: *req.CategoryID, + }); err != nil { + return err + } - if err = dberr.MapRowsAffected(rows, nil, errs.ErrPostNotFound); err != nil { - return err - } + // 先删除标签 + if err = q.DeletePostTagByPostID(ctx, id); err != nil { + return err + } - if err = q.DeletePostCategory(ctx, id); err != nil { - return err - } + var tagsParams []sqlc.CreatePostTagParams + for _, tagID := range req.Tags { + tagsParams = append(tagsParams, sqlc.CreatePostTagParams{ + PostID: id, + TagID: tagID, + }) + } - if err = q.CreatePostCategory(ctx, db.CreatePostCategoryParams{ - PostID: id, - CategoryID: *req.CategoryID, - }); err != nil { - return err - } + if _, err = q.CreatePostTag(ctx, tagsParams); err != nil { + return err + } - // 提交 - if err = tx.Commit(ctx); err != nil { - return err - } - - return nil + return nil + }) } func (s *SysPostService) Delete(ctx context.Context, id int32) error { - rows, err := s.queries.DeletePost(ctx, id) - return dberr.MapRowsAffected(rows, err, errs.ErrPostNotFound) + return s.store.WithTx(ctx, func(q *sqlc.Queries) error { + err := q.DeletePostCategoryByPostID(ctx, id) + if err != nil { + return err + } + + rows, err := q.DeletePost(ctx, id) + return dberr.MapRowsAffected(rows, err, errs.ErrPostNotFound) + }) } diff --git a/internal/service/sys_role.go b/internal/service/sys_role.go index 18d3332..7bb3d5f 100644 --- a/internal/service/sys_role.go +++ b/internal/service/sys_role.go @@ -2,45 +2,56 @@ package service import ( "context" - db "server/internal/db/sqlc" + "errors" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/model/common" "server/internal/model/enum" "server/internal/model/request" "server/internal/pkg/cache" + "server/internal/pkg/cache/cachekey" "server/internal/pkg/dberr" "server/internal/pkg/errs" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" ) type SysRoleService struct { - queries *db.Queries - pool *pgxpool.Pool - cache *cache.Caches + store *db.Store + cache *cache.Caches } -func NewSysRoleService(queries *db.Queries, pool *pgxpool.Pool, cache *cache.Caches) *SysRoleService { +func NewSysRoleService(store *db.Store, cache *cache.Caches) *SysRoleService { return &SysRoleService{ - queries: queries, - pool: pool, - cache: cache, + store: store, + cache: cache, } } -func (s *SysRoleService) ListPage(ctx context.Context, p *common.Pagination) ([]db.SysRole, int64, error) { - params := db.ListSysRolesParams{ +// clearCache 清理所有api权限和用户信息 禁用角色/删除角色需要清理 +func (s *SysRoleService) clearCache(ctx context.Context) error { + if err := s.cache.DelByPrefix(ctx, cachekey.UserApiPermissionsPattern); err != nil { + return err + } + + if err := s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern); err != nil { + return err + } + + return nil +} + +func (s *SysRoleService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.SysRole, int64, error) { + params := sqlc.ListSysRolesParams{ Limit: p.PageSize, Offset: (p.Page - 1) * p.PageSize, } - total, err := s.queries.CountSysRoles(ctx) + total, err := s.store.CountSysRoles(ctx) if err != nil { return nil, 0, err } - list, err := s.queries.ListSysRoles(ctx, params) + list, err := s.store.ListSysRoles(ctx, params) if err != nil { return nil, 0, err } @@ -48,38 +59,36 @@ func (s *SysRoleService) ListPage(ctx context.Context, p *common.Pagination) ([] return list, total, nil } -func (s *SysRoleService) GetRoles(ctx context.Context) ([]db.SysRole, error) { - return s.queries.GetAllSysRoles(ctx) +func (s *SysRoleService) GetRoles(ctx context.Context) ([]sqlc.SysRole, error) { + return s.store.GetAllSysRoles(ctx) } -func (s *SysRoleService) GetRoleMenus(ctx context.Context, id int32) ([]db.GetSysRoleMenusRow, error) { - _, err := s.queries.GetSysRoleByID(ctx, id) +func (s *SysRoleService) GetRoleMenus(ctx context.Context, id int32) ([]sqlc.GetSysRoleMenusRow, error) { + _, err := s.store.GetSysRoleByID(ctx, id) if err != nil { return nil, dberr.MapNoRows(err, errs.ErrSysRoleNotFound) } - return s.queries.GetSysRoleMenus(ctx, id) + return s.store.GetSysRoleMenus(ctx, id) } -func (s *SysRoleService) GetRoleApis(ctx context.Context, id int32) ([]db.GetSysRoleApisRow, error) { - _, err := s.queries.GetSysRoleByID(ctx, id) +func (s *SysRoleService) GetRoleApis(ctx context.Context, id int32) ([]sqlc.GetSysRoleApisRow, error) { + _, err := s.store.GetSysRoleByID(ctx, id) if err != nil { return nil, dberr.MapNoRows(err, errs.ErrSysRoleNotFound) } - return s.queries.GetSysRoleApis(ctx, id) + return s.store.GetSysRoleApis(ctx, id) } func (s *SysRoleService) Create(ctx context.Context, req request.CreateSysRoleRequest) error { - params := db.CreateSysRoleParams{ - Name: req.Name, - Code: req.Code, + params := sqlc.CreateSysRoleParams{ + Name: req.Name, + Code: req.Code, + Status: *req.Status, } - // 清理缓存 - s.cache.ClearAllSysUserCache() - - err := s.queries.CreateSysRole(ctx, params) + err := s.store.CreateSysRole(ctx, params) if err != nil { return dberr.MapUniqueViolation(err, dberr.SysRoleCodeKey, errs.ErrCodeAlreadyExists) } @@ -87,125 +96,156 @@ func (s *SysRoleService) Create(ctx context.Context, req request.CreateSysRoleRe } func (s *SysRoleService) Update(ctx context.Context, id int32, req request.UpdateSysRoleRequest) error { - params := db.UpdateSysRoleParams{ - ID: id, - Name: req.Name, + params := sqlc.UpdateSysRoleParams{ + ID: id, + Name: req.Name, + Status: req.Status, } - // 清理缓存 - s.cache.ClearAllSysUserCache() + rows, err := s.store.UpdateSysRole(ctx, params) + err = dberr.MapRowsAffected(rows, err, errs.ErrSysRoleNotFound) + if err != nil { + if errors.Is(err, errs.ErrSysRoleNotFound) { + } + return err + } - rows, err := s.queries.UpdateSysRole(ctx, params) - return dberr.MapRowsAffected(rows, err, errs.ErrSysRoleNotFound) + // 清理缓存 无论启用禁用 启用时也需要清理缓存以刷新权限 + _ = s.clearCache(ctx) + + return nil } func (s *SysRoleService) SetRoleMenus(ctx context.Context, roleID int32, req request.SetSysRoleMenusRequest) error { - _, err := s.queries.GetSysRoleByID(ctx, roleID) + // 先检查角色是否存在 + _, err := s.store.GetSysRoleByID(ctx, roleID) if err != nil { return dberr.MapNoRows(err, errs.ErrSysRoleNotFound) } - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + err = s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 1. 先删除所有关联表数据 + if err = q.DeleteSysRolePermissionByType(ctx, sqlc.DeleteSysRolePermissionByTypeParams{ + RoleID: roleID, + Type: int16(enum.PermissionTypeMenu), + }); err != nil { + return err + } + + // 2. 查出所有权限id并组装数据 + permissionIds, err := q.GetSysPermissionIdsByMenuIDs(ctx, req.MenuIDs) + if err != nil { + return err + } + + params := make([]sqlc.CreateSysRolePermissionParams, 0, len(permissionIds)) + for _, id := range permissionIds { + params = append(params, sqlc.CreateSysRolePermissionParams{ + RoleID: roleID, + PermissionID: id, + }) + } + + // 批量插入 + _, err = q.CreateSysRolePermission(ctx, params) + if err != nil { + return err + } + + return nil + }) + if err != nil { return err } - defer func(ctx context.Context) { - _ = tx.Rollback(ctx) - }(ctx) + // 清理缓存 这里只用清理用户信息 + _ = s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern) - q := db.New(tx) - - permissionIds, err := s.queries.GetSysPermissionIdsByMenuIDs(ctx, req.MenuIDs) - if err != nil { - return err - } - - params := make([]db.CreateSysRolePermissionParams, 0, len(permissionIds)) - for _, id := range permissionIds { - params = append(params, db.CreateSysRolePermissionParams{ - RoleID: roleID, - PermissionID: id, - }) - } - - if err = q.DeleteSysRolePermission(ctx, db.DeleteSysRolePermissionParams{ - RoleID: roleID, - Type: int16(enum.PermissionTypeMenu), - }); err != nil { - return err - } - - _, err = q.CreateSysRolePermission(ctx, params) - if err != nil { - return err - } - - if err = tx.Commit(ctx); err != nil { - return err - } - - // 清理缓存 - s.cache.ClearAllSysUserCache() - - return err + return nil } func (s *SysRoleService) SetRoleApis(ctx context.Context, roleID int32, req request.SetSysRoleApisRequest) error { - _, err := s.queries.GetSysRoleByID(ctx, roleID) + _, err := s.store.GetSysRoleByID(ctx, roleID) if err != nil { return dberr.MapNoRows(err, errs.ErrSysRoleNotFound) } - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + err = s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 先删除所有旧权限数据 + if err = q.DeleteSysRolePermissionByType(ctx, sqlc.DeleteSysRolePermissionByTypeParams{ + RoleID: roleID, + Type: int16(enum.PermissionTypeApi), + }); err != nil { + return err + } + + // 查出数据并组装 + permissionIds, err := q.GetSysPermissionIdsByApiIDs(ctx, req.ApiIDs) + if err != nil { + return err + } + + params := make([]sqlc.CreateSysRolePermissionParams, 0, len(permissionIds)) + for _, id := range permissionIds { + params = append(params, sqlc.CreateSysRolePermissionParams{ + RoleID: roleID, + PermissionID: id, + }) + } + + // 批量插入 + _, err = q.CreateSysRolePermission(ctx, params) + if err != nil { + return err + } + + return nil + }) + if err != nil { return err } - defer func(ctx context.Context) { - _ = tx.Rollback(ctx) - }(ctx) + // 清理缓存 这里只用清理接口权限 + _ = s.cache.DelByPrefix(ctx, cachekey.UserApiPermissionsPattern) - q := db.New(tx) - - permissionIds, err := s.queries.GetSysPermissionIdsByApiIDs(ctx, req.ApiIDs) - if err != nil { - return err - } - - params := make([]db.CreateSysRolePermissionParams, 0, len(permissionIds)) - for _, id := range permissionIds { - params = append(params, db.CreateSysRolePermissionParams{ - RoleID: roleID, - PermissionID: id, - }) - } - - if err = q.DeleteSysRolePermission(ctx, db.DeleteSysRolePermissionParams{ - RoleID: roleID, - Type: int16(enum.PermissionTypeApi), - }); err != nil { - return err - } - - _, err = q.CreateSysRolePermission(ctx, params) - if err != nil { - return err - } - - if err = tx.Commit(ctx); err != nil { - return err - } - - // 清理缓存 - s.cache.ClearAllSysUserCache() - - return err + return nil } func (s *SysRoleService) Delete(ctx context.Context, id int32) error { + err := s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 1.删除角色关联权限 + err := q.DeleteSysRolePermissionByRoleID(ctx, id) + if err != nil { + return err + } + + // 2. 根据角色id删除用户关联的角色 + err = q.DeleteSysUserRoleByRoleID(ctx, id) + if err != nil { + return err + } + + // 3. 删除角色信息 + rows, err := q.DeleteSysRole(ctx, id) + err = dberr.MapRowsAffected(rows, err, errs.ErrSysRoleNotFound) + if err != nil { + // 如果是数据库不存在这条数据 此时也执行清理 + if errors.Is(err, errs.ErrSysRoleNotFound) { + } + return err + } + + return nil + }) + + if err != nil { + return err + } + // 清理缓存 - s.cache.ClearAllSysUserCache() - rows, err := s.queries.DeleteSysRole(ctx, id) - return dberr.MapRowsAffected(rows, err, errs.ErrSysRoleNotFound) + _ = s.clearCache(ctx) + + return nil } diff --git a/internal/service/sys_user.go b/internal/service/sys_user.go index 6ff0d2b..b2403a5 100644 --- a/internal/service/sys_user.go +++ b/internal/service/sys_user.go @@ -2,165 +2,166 @@ package service import ( "context" - db "server/internal/db/sqlc" + "errors" + "server/internal/db" + "server/internal/db/sqlc" "server/internal/middleware" - "server/internal/model/common" "server/internal/model/request" "server/internal/model/response" "server/internal/pkg/cache" + "server/internal/pkg/cache/cachekey" "server/internal/pkg/dberr" "server/internal/pkg/errs" "server/internal/pkg/httputil" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - "golang.org/x/crypto/bcrypt" "golang.org/x/sync/errgroup" ) type SysUserService struct { - queries *db.Queries - pool *pgxpool.Pool - jwt *middleware.JWTMiddleware - cache *cache.Caches + store *db.Store + jwt *middleware.JWTMiddleware + cache *cache.Caches } -func NewSysUserService(queries *db.Queries, pool *pgxpool.Pool, jwt *middleware.JWTMiddleware, cache *cache.Caches) *SysUserService { +func NewSysUserService(store *db.Store, jwt *middleware.JWTMiddleware, cache *cache.Caches) *SysUserService { return &SysUserService{ - queries: queries, - pool: pool, - jwt: jwt, - cache: cache, + store: store, + jwt: jwt, + cache: cache, } } -// generatePasswordHash 生成密码哈希 -func generatePasswordHash(password string) (string, error) { - hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", err +// clearUserCache +func (s *SysUserService) clearUserCache(ctx context.Context, id int32) error { + if err := s.cache.Del(ctx, cachekey.UserApiPermissions(id)); err != nil { + return err } - return string(hashed), nil + + if err := s.cache.Del(ctx, cachekey.UserInfo(id)); err != nil { + return err + } + + return nil } -// comparePasswordHash 比较密码哈希 -func comparePasswordHash(passwordHash, inputPassword string) error { - return bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(inputPassword)) -} - -func (s *SysUserService) Login(ctx context.Context, req request.LoginRequest) (*response.LoginResponse, error) { - user, err := s.queries.GetSysUserByAccount(ctx, req.Account) +// clearRefreshToken +func (s *SysUserService) clearRefreshToken(ctx context.Context, id int32) error { + keys, err := s.cache.SMembers(ctx, cachekey.AuthRefreshUser(id)) if err != nil { - return nil, errs.ErrInvalidCredentials + return err } - if err = comparePasswordHash(user.PasswordHash, req.Password); err != nil { - return nil, errs.ErrInvalidCredentials + // 循环删除所有的token + for _, key := range keys { + if err = s.cache.Del(ctx, cachekey.AuthRefresh(key)); err != nil { + return err + } } - accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(user.ID) - if err != nil { - return nil, err - } - - refreshToken, refreshTokenExp, err := s.jwt.GenerateRefreshToken(user.ID) - if err != nil { - return nil, err - } - - return &response.LoginResponse{ - AccessToken: accessToken, - AccessTokenExp: accessTokenExp, - RefreshToken: refreshToken, - RefreshTokenExp: refreshTokenExp, - }, nil -} - -func (s *SysUserService) RefreshToken(ctx context.Context, refreshToken string) (*response.LoginResponse, error) { - claims, err := s.jwt.ParseRefreshToken(refreshToken) - if err != nil { - return nil, errs.ErrInvalidRefreshToken - } - - accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(claims.UserID) - if err != nil { - return nil, err - } - - return &response.LoginResponse{ - AccessToken: accessToken, - AccessTokenExp: accessTokenExp, - }, nil + // 删除反向索引 + err = s.cache.DelSetMembers(ctx, cachekey.AuthRefreshUser(id)) + return err } func (s *SysUserService) GetUserInfo(ctx context.Context, id int32, isAdmin bool) (*response.SysUserInfo, error) { - g, ctx := errgroup.WithContext(ctx) + return cache.GetOrSetJSON[*response.SysUserInfo](ctx, s.cache, cachekey.UserInfo(id), 0, func() (*response.SysUserInfo, error) { + g, ctx := errgroup.WithContext(ctx) - var ( - user db.GetSysUserByIDRow - roles []db.SysRole - menus []db.SysMenu - ) - - g.Go(func() error { - u, err := s.queries.GetSysUserByID(ctx, id) - if err != nil { - return dberr.MapNoRows(err, errs.ErrUserNotFound) - } - user = u - return nil - }) - - g.Go(func() error { - r, err := s.queries.GetSysUserRoles(ctx, id) - if err != nil { - return err - } - roles = r - return nil - }) - - g.Go(func() error { var ( - m []db.SysMenu - err error + user sqlc.GetSysUserByIDRow + roles []sqlc.SysRole + menus []sqlc.SysMenu + permissions []*string ) - if isAdmin { - m, err = s.queries.GetSysAdminMenus(ctx) - } else { - m, err = s.queries.GetSysUserMenus(ctx, id) + + g.Go(func() error { + u, err := s.store.GetSysUserByID(ctx, id) + if err != nil { + return dberr.MapNoRows(err, errs.ErrUserNotFound) + } + + user = u + return nil + }) + + g.Go(func() error { + r, err := s.store.GetSysUserRoles(ctx, id) + if err != nil { + return err + } + roles = r + return nil + }) + + g.Go(func() error { + var ( + p []*string + err error + ) + + if isAdmin { + p, err = s.store.GetSysAdminPermissions(ctx) + } else { + p, err = s.store.GetSysUserPermissions(ctx, id) + } + + if err != nil { + return err + } + + permissions = p + + return nil + }) + + g.Go(func() error { + var ( + m []sqlc.SysMenu + err error + ) + if isAdmin { + m, err = s.store.GetSysAdminMenus(ctx) + } else { + m, err = s.store.GetSysUserMenus(ctx, id) + } + if err != nil { + return err + } + menus = m + return nil + }) + + if err := g.Wait(); err != nil { + return nil, err } - if err != nil { - return err + + // 如果用户被禁用 返回错误 超管不用管状态 + if user.ID != 1 && user.Status != 1 { + return nil, errs.ErrUserDisabled } - menus = m - return nil + + // 处理角色 + userInfo := response.NewSysUserInfo(user, roles, menus, permissions) + + return userInfo, nil }) - - if err := g.Wait(); err != nil { - return nil, err - } - - // 处理角色 - userInfo := response.NewSysUserInfo(user, roles, menus) - - return userInfo, nil } -func (s *SysUserService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListSysUsersRow, int64, error) { - params := db.ListSysUsersParams{ - Limit: p.PageSize, - Offset: (p.Page - 1) * p.PageSize, +func (s *SysUserService) ListPage(ctx context.Context, p request.SearchSysUserParams) ([]sqlc.ListSysUsersRow, int64, error) { + params := sqlc.ListSysUsersParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + Username: p.Username, } - total, err := s.queries.CountSysUsers(ctx) + total, err := s.store.CountSysUsers(ctx, p.Username) if err != nil { return nil, 0, err } - users, err := s.queries.ListSysUsers(ctx, params) + users, err := s.store.ListSysUsers(ctx, params) if err != nil { return nil, 0, err } @@ -174,14 +175,14 @@ func (s *SysUserService) ListPage(ctx context.Context, p *common.Pagination) ([] return users, total, nil } -func (s *SysUserService) GetRoles(ctx context.Context, id int32) ([]db.SysRole, error) { +func (s *SysUserService) GetRoles(ctx context.Context, id int32) ([]sqlc.SysRole, error) { // 先查询用户是否存在 - _, err := s.queries.GetSysUserByID(ctx, id) + _, err := s.store.GetSysUserByID(ctx, id) if err != nil { return nil, dberr.MapNoRows(err, errs.ErrUserNotFound) } - return s.queries.GetSysUserRoles(ctx, id) + return s.store.GetSysUserRoles(ctx, id) } func (s *SysUserService) Create(ctx context.Context, req request.CreateSysUserRequest) error { @@ -190,14 +191,15 @@ func (s *SysUserService) Create(ctx context.Context, req request.CreateSysUserRe return err } - user := db.CreateSysUserParams{ + user := sqlc.CreateSysUserParams{ Account: req.Account, Username: req.Username, PasswordHash: passwordHash, AvatarID: req.AvatarID, + Status: *req.Status, } - if err = s.queries.CreateSysUser(ctx, user); err != nil { + if err = s.store.CreateSysUser(ctx, user); err != nil { return dberr.MapUniqueViolation(err, dberr.SysUserAccountKey, errs.ErrAccountAlreadyExists) } @@ -205,9 +207,10 @@ func (s *SysUserService) Create(ctx context.Context, req request.CreateSysUserRe } func (s *SysUserService) Update(ctx context.Context, id int32, req request.UpdateSysUserRequest) error { - user := db.UpdateSysUserParams{ + user := sqlc.UpdateSysUserParams{ Username: req.Username, ID: id, + Status: req.Status, } if req.AvatarID.Set { @@ -217,47 +220,59 @@ func (s *SysUserService) Update(ctx context.Context, id int32, req request.Updat } } - rows, err := s.queries.UpdateSysUser(ctx, user) - return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) -} - -func (s *SysUserService) SetRoles(ctx context.Context, userID int32, req request.SetSysUserRolesRequest) error { - // 先查询用户是否存在 - _, err := s.queries.GetSysUserByID(ctx, userID) - if err != nil { - return dberr.MapNoRows(err, errs.ErrUserNotFound) - } - - tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) - if err != nil { - return err - } - - defer func(ctx context.Context) { - _ = tx.Rollback(ctx) - }(ctx) - - q := db.New(tx) - - if err = q.ClearSysUserRoles(ctx, userID); err != nil { - return err - } - - for _, roleID := range req.RoleIDs { - if err = q.CreateSysUserRole(ctx, db.CreateSysUserRoleParams{ - UserID: userID, - RoleID: roleID, - }); err != nil { - return err - } - } - - if err = tx.Commit(ctx); err != nil { + rows, err := s.store.UpdateSysUser(ctx, user) + if err = dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound); err != nil { return err } // 清理缓存 - s.cache.ClearSysUserCache(userID) + _ = s.clearUserCache(ctx, id) + + // 如果将用户的状态修改为0,则清除用户刷新令牌 + if req.Status != nil && *req.Status == 0 { + _ = s.clearRefreshToken(ctx, id) + } + + return nil +} + +func (s *SysUserService) SetRoles(ctx context.Context, userID int32, req request.SetSysUserRolesRequest) error { + // 先查询用户是否存在 + _, err := s.store.GetSysUserByID(ctx, userID) + if err != nil { + return dberr.MapNoRows(err, errs.ErrUserNotFound) + } + + err = s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 1. 先删除关联数据 + if err = q.ClearSysUserRoles(ctx, userID); err != nil { + return err + } + + var params []sqlc.CreateSysUserRoleParams + + for _, roleID := range req.RoleIDs { + params = append(params, sqlc.CreateSysUserRoleParams{ + UserID: userID, + RoleID: roleID, + }) + } + + // 2. 关联用户角色数据 + _, err = q.CreateSysUserRole(ctx, params) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return err + } + + // 3. 清理缓存 + _ = s.clearUserCache(ctx, userID) return nil } @@ -268,13 +283,21 @@ func (s *SysUserService) UpdatePassword(ctx context.Context, id int32, req reque return err } - params := db.UpdateSysUserPasswordParams{ + params := sqlc.UpdateSysUserPasswordParams{ ID: id, PasswordHash: passwordHash, } - rows, err := s.queries.UpdateSysUserPassword(ctx, params) - return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) + rows, err := s.store.UpdateSysUserPassword(ctx, params) + err = dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) + if err != nil { + return err + } + + // 下线当前用户 + _ = s.clearRefreshToken(ctx, id) + + return nil } func (s *SysUserService) Delete(ctx context.Context, id int32) error { @@ -282,9 +305,28 @@ func (s *SysUserService) Delete(ctx context.Context, id int32) error { return errs.ErrCannotDeleteSuperAdmin } - // 清理缓存 - s.cache.ClearSysUserCache(id) + err := s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 清理用户角色关联数据 + err := q.ClearSysUserRoles(ctx, id) + if err != nil { + return err + } - rows, err := s.queries.DeleteSysUser(ctx, id) - return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) + // 删除用户 + rows, err := q.DeleteSysUser(ctx, id) + return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) + }) + + if err != nil { + if errors.Is(err, errs.ErrUserNotFound) { + // 用户不存在也执行清理缓存操作 + _ = s.clearUserCache(ctx, id) + } + return err + } + + // 事务执行成功后清理缓存 + _ = s.clearUserCache(ctx, id) + _ = s.clearRefreshToken(ctx, id) + return nil } diff --git a/internal/service/tag.go b/internal/service/tag.go new file mode 100644 index 0000000..cd6398d --- /dev/null +++ b/internal/service/tag.go @@ -0,0 +1,87 @@ +package service + +import ( + "context" + "server/internal/db" + "server/internal/db/sqlc" + "server/internal/model/common" + "server/internal/model/request" + "server/internal/pkg/dberr" + "server/internal/pkg/errs" +) + +type TagService struct { + store *db.Store +} + +func NewTagService(store *db.Store) *TagService { + return &TagService{ + store: store, + } +} + +func (s *TagService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.Tag, int64, error) { + params := sqlc.ListTagsParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + } + + total, err := s.store.CountTags(ctx) + if err != nil { + return nil, 0, err + } + + list, err := s.store.ListTags(ctx, params) + if err != nil { + return nil, 0, err + } + + return list, total, nil +} + +func (s *TagService) ListAll(ctx context.Context) ([]sqlc.Tag, error) { + return s.store.ListAllTags(ctx) +} + +func (s *TagService) Create(ctx context.Context, req request.CreateTagRequest) error { + params := sqlc.CreateTagParams{ + Name: req.Name, + Code: req.Code, + Sort: req.Sort, + } + + err := s.store.CreateTag(ctx, params) + return dberr.MapUniqueViolation(err, dberr.TagCodeKey, errs.ErrTagCodeAlreadyExists) +} + +func (s *TagService) Update(ctx context.Context, id int32, req request.UpdateTagRequest) error { + params := sqlc.UpdateTagParams{ + ID: id, + Name: req.Name, + Code: req.Code, + Sort: req.Sort, + } + rows, err := s.store.UpdateTag(ctx, params) + // 先判断数据条目是否存在 + if err = dberr.MapRowsAffected(rows, err, errs.ErrTagNotFound); err != nil { + // 再判断code是否重复 + return dberr.MapUniqueViolation(err, dberr.TagCodeKey, errs.ErrTagCodeAlreadyExists) + } + + return nil +} + +func (s *TagService) Delete(ctx context.Context, id int32) error { + return s.store.WithTx(ctx, func(q *sqlc.Queries) error { + // 删除中间表数据 + err := s.store.DeletePostTagByTagID(ctx, id) + if err != nil { + return err + } + + // 删除标签数据 + rows, err := s.store.DeleteTag(ctx, id) + return dberr.MapRowsAffected(rows, err, errs.ErrTagNotFound) + }) + +} diff --git a/sqlc.yaml b/sqlc.yaml index f56c60e..a876470 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -5,7 +5,7 @@ sql: schema: "./internal/db/schema/*" gen: go: - package: "db" + package: "sqlc" out: "./internal/db/sqlc" emit_empty_slices: true emit_interface: false