feat: release v1.0.0

This commit is contained in:
2026-07-29 22:10:36 +08:00
parent ee9f50b859
commit c903b5a6b5
81 changed files with 2776 additions and 1044 deletions

View File

@@ -48,13 +48,18 @@ func main() {
fx.Provide( fx.Provide(
config.NewConfig, config.NewConfig,
logger.New, logger.New,
db.NewDB,
db.NewStore,
db.NewRedisClient,
cache.NewCaches, cache.NewCaches,
middleware.NewJWTMiddleware, middleware.NewJWTMiddleware,
middleware.NewAuthMiddleware, middleware.NewAuthMiddleware,
middleware.NewLoggerMiddleware, middleware.NewLoggerMiddleware,
router.NewRouter, router.NewRouter,
db.NewDB,
db.NewQueries,
), ),
handler.Module, handler.Module,
service.Module, service.Module,

View File

@@ -10,6 +10,12 @@ database:
dbname: db_name dbname: db_name
sslMode: disable sslMode: disable
redis:
addr: host:port
password: redis_password
db: 0
key_prefix: ""
jwt: jwt:
secret: secret:
signing_method: HS256 signing_method: HS256
@@ -19,6 +25,7 @@ jwt:
audience: audience:
token_header: Authorization token_header: Authorization
token_prefix: "Bearer " token_prefix: "Bearer "
cookie_name: "rt"
file: file:
base_url: http://127.0.0.1:8080/static/ #静态文件目录 用于本地访问开放静态资源 base_url: http://127.0.0.1:8080/static/ #静态文件目录 用于本地访问开放静态资源

8
go.mod
View File

@@ -11,14 +11,15 @@ require (
github.com/jackc/pgx/v5 v5.7.6 github.com/jackc/pgx/v5 v5.7.6
github.com/knadh/koanf v1.5.0 github.com/knadh/koanf v1.5.0
github.com/matoous/go-nanoid/v2 v2.1.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 go.uber.org/fx v1.24.0
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.46.0
golang.org/x/sync v0.19.0 golang.org/x/sync v0.19.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( 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/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/jackc/pgpassfile v1.0.0 // 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/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // 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/spf13/pflag v1.0.10 // indirect
github.com/stretchr/testify v1.11.1 // 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/dig v1.19.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.26.0 // indirect go.uber.org/zap v1.26.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )

16
go.sum
View File

@@ -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.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/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/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.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/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-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 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/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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/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 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= 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= 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.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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/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.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 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.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.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 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/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/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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/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/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/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.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.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 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=

View File

@@ -32,6 +32,13 @@ type Database struct {
SSLMode string `koanf:"sslMode"` 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 { type JWTConfig struct {
Secret string `koanf:"secret"` Secret string `koanf:"secret"`
SigningMethod string `koanf:"signing_method"` SigningMethod string `koanf:"signing_method"`
@@ -41,6 +48,7 @@ type JWTConfig struct {
Audience string `koanf:"audience"` Audience string `koanf:"audience"`
TokenHeader string `koanf:"token_header"` TokenHeader string `koanf:"token_header"`
TokenPrefix string `koanf:"token_prefix"` TokenPrefix string `koanf:"token_prefix"`
CookieName string `koanf:"cookie_name"`
} }
type LogConfig struct { type LogConfig struct {
@@ -56,6 +64,7 @@ type LogConfig struct {
type Config struct { type Config struct {
Server Server `koanf:"server"` Server Server `koanf:"server"`
Database Database `koanf:"database"` Database Database `koanf:"database"`
Redis Redis `koanf:"redis"`
JWTConfig JWTConfig `koanf:"jwt"` JWTConfig JWTConfig `koanf:"jwt"`
Log LogConfig `koanf:"log"` Log LogConfig `koanf:"log"`
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"server/internal/config" "server/internal/config"
"server/internal/db/sqlc"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/fx" "go.uber.org/fx"
@@ -42,7 +41,3 @@ func NewDB(lc fx.Lifecycle, cfg *config.Config) (*pgxpool.Pool, error) {
return pool, nil return pool, nil
} }
func NewQueries(pool *pgxpool.Pool) *db.Queries {
return db.New(pool)
}

View File

@@ -0,0 +1,5 @@
ALTER TABLE sys_users
DROP COLUMN IF EXISTS status;
ALTER TABLE sys_roles
DROP COLUMN IF EXISTS status;

View File

@@ -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启用';

View File

@@ -0,0 +1,6 @@
ALTER TABLE categories
DROP COLUMN IF EXISTS sort;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS post_tag;

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
-- name: CreateCategory :exec -- name: CreateCategory :exec
INSERT INTO categories(name, code) INSERT INTO categories(name, code, sort)
VALUES ($1, $2); VALUES ($1, $2, $3);
-- name: ListCategories :many -- name: ListCategories :many
SELECT * SELECT *
FROM categories FROM categories
ORDER BY id LIMIT $1 ORDER BY sort DESC, id
OFFSET $2; LIMIT $1 OFFSET $2;
-- name: CountCategories :one -- name: CountCategories :one
SELECT COUNT(*) SELECT COUNT(*)
@@ -14,17 +14,14 @@ FROM categories;
-- name: ListAllCategories :many -- name: ListAllCategories :many
SELECT * SELECT *
FROM categories;
-- name: GetCategoryById :one
SELECT *
FROM categories FROM categories
WHERE id = $1; ORDER BY sort DESC, id;
-- name: UpdateCategory :execrows -- name: UpdateCategory :execrows
UPDATE categories UPDATE categories
SET name = coalesce(sqlc.narg('name'), name), 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'); WHERE id = sqlc.arg('id');
-- name: DeleteCategory :execrows -- name: DeleteCategory :execrows
@@ -36,7 +33,12 @@ WHERE id = $1;
INSERT INTO post_category(post_id, category_id) INSERT INTO post_category(post_id, category_id)
VALUES ($1, $2); VALUES ($1, $2);
-- name: DeletePostCategory :exec -- name: DeletePostCategoryByPostID :exec
DELETE DELETE
FROM post_category FROM post_category
WHERE post_id = $1; WHERE post_id = $1;
-- name: DeletePostCategoryByCategoryID :exec
DELETE
FROM post_category
WHERE category_id = $1;

View File

@@ -18,18 +18,46 @@ WITH paginated_posts AS (
updated_at updated_at
FROM posts FROM posts
ORDER BY sort DESC, published_at DESC, id DESC ORDER BY sort DESC, published_at DESC, id DESC
LIMIT $1 OFFSET $2) LIMIT $1 OFFSET $2
)
-- 第二步:用这极少量的记录去进行 JOIN -- 第二步:用这极少量的记录去进行 JOIN
SELECT p.*, SELECT p.*,
f.file_path AS cover, f.file_path AS cover,
c.name AS category_name, c.name AS category_name,
c.id AS category_id, 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 FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_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_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; ORDER BY p.sort DESC, p.published_at DESC, p.id DESC;
-- name: CountPosts :one -- name: CountPosts :one
@@ -40,12 +68,21 @@ FROM posts;
SELECT p.*, SELECT p.*,
f.file_path AS cover, f.file_path AS cover,
c.name AS category_name, 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 FROM posts p
LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_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 WHERE p.id = $1
GROUP BY p.id,
f.file_path,
c.name,
c.id
LIMIT 1; LIMIT 1;
-- name: UpdatePost :execrows -- name: UpdatePost :execrows
@@ -98,12 +135,36 @@ SELECT p.*,
f.file_path AS cover, f.file_path AS cover,
c.name AS category_name, c.name AS category_name,
c.id AS category_id, 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 FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_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_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; ORDER BY p.sort DESC, p.published_at DESC, p.id DESC;
-- name: CountPublishedPosts :one -- name: CountPublishedPosts :one
@@ -142,4 +203,6 @@ FROM
AND p.published_at < NOW() AND p.published_at < NOW()
GROUP BY GROUP BY
c.id, c.id,
c.name; c.sort,
c.name
ORDER BY c.sort DESC, c.id;

View File

@@ -6,12 +6,16 @@ RETURNING id;
-- name: GetSysApis :many -- name: GetSysApis :many
SELECT * SELECT *
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'))
ORDER BY id ORDER BY id
LIMIT $1 OFFSET $2; LIMIT $1 OFFSET $2;
-- name: CountSysApis :one -- name: CountSysApis :one
SELECT COUNT(*) 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 -- name: GetSysApiByID :one
SELECT * SELECT *
@@ -41,13 +45,6 @@ DELETE
FROM sys_api_permission FROM sys_api_permission
WHERE api_id = $1; 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 -- name: GetSysApiGroupNames :many
SELECT DISTINCT group_name SELECT DISTINCT group_name
FROM sys_apis; FROM sys_apis;

View File

@@ -38,7 +38,7 @@ SET name = coalesce(sqlc.narg('name'), name),
hidden = coalesce(sqlc.narg('hidden'), hidden), hidden = coalesce(sqlc.narg('hidden'), hidden),
sort = coalesce(sqlc.narg('sort'), sort), sort = coalesce(sqlc.narg('sort'), sort),
status = coalesce(sqlc.narg('status'), status), 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 parent_id = CASE WHEN @update_parent_id::boolean THEN @parent_id ELSE parent_id END
WHERE id = sqlc.arg('id'); WHERE id = sqlc.arg('id');

View File

@@ -12,3 +12,12 @@ DELETE
FROM sys_permissions FROM sys_permissions
WHERE id = $1; 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;

View File

@@ -1,6 +1,6 @@
-- name: CreateSysRole :exec -- name: CreateSysRole :exec
INSERT INTO sys_roles (name, code) INSERT INTO sys_roles (name, code, status)
VALUES ($1, $2); VALUES ($1, $2, $3);
-- name: ListSysRoles :many -- name: ListSysRoles :many
SELECT * SELECT *
@@ -28,7 +28,8 @@ SELECT EXISTS(SELECT 1
-- name: UpdateSysRole :execrows -- name: UpdateSysRole :execrows
UPDATE sys_roles 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'); WHERE id = sqlc.arg('id');
-- name: DeleteSysRole :execrows -- name: DeleteSysRole :execrows
@@ -68,9 +69,24 @@ FROM sys_role_permission rp
JOIN sys_apis a ON a.id = ap.api_id JOIN sys_apis a ON a.id = ap.api_id
WHERE rp.role_id = $1; WHERE rp.role_id = $1;
-- name: DeleteSysRolePermission :exec -- name: DeleteSysRolePermissionByType :exec
DELETE DELETE
FROM sys_role_permission rp USING sys_permissions p FROM sys_role_permission rp USING sys_permissions p
WHERE rp.permission_id = p.id WHERE rp.permission_id = p.id
AND p."type" = $2 AND rp.role_id = $1
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;

View File

@@ -1,12 +1,20 @@
-- name: CreateSysUser :exec -- name: CreateSysUser :exec
INSERT INTO sys_users (account, username, password_hash, avatar_id) INSERT INTO sys_users (account, username, password_hash, status, avatar_id)
VALUES ($1, $2, $3, $4); 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 -- name: GetSysUserByID :one
SELECT u.id, SELECT u.id,
u.account, u.account,
u.username, u.username,
u.avatar_id, u.avatar_id,
u.status,
f.file_path AS avatar_url, f.file_path AS avatar_url,
u.created_at, u.created_at,
u.updated_at u.updated_at
@@ -19,6 +27,7 @@ SELECT u.id,
u.account, u.account,
u.username, u.username,
u.password_hash, u.password_hash,
u.status,
u.avatar_id, u.avatar_id,
f.file_path AS avatar_url, f.file_path AS avatar_url,
u.created_at, u.created_at,
@@ -31,22 +40,26 @@ WHERE u.account = $1;
SELECT u.id, SELECT u.id,
u.account, u.account,
u.username, u.username,
u.status,
u.avatar_id, u.avatar_id,
f.file_path AS avatar_url, f.file_path AS avatar_url,
u.created_at, u.created_at,
u.updated_at u.updated_at
FROM sys_users u FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id 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 ORDER BY u.id
LIMIT $1 OFFSET $2; LIMIT $1 OFFSET $2;
-- name: CountSysUsers :one -- name: CountSysUsers :one
SELECT COUNT(*) SELECT COUNT(*)
FROM sys_users; FROM sys_users
WHERE (sqlc.arg('username')::text = '' OR username ILIKE '%' || sqlc.arg('username') || '%');
-- name: UpdateSysUser :execrows -- name: UpdateSysUser :execrows
UPDATE sys_users UPDATE sys_users
SET username = coalesce(sqlc.narg('username'), username), 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 avatar_id = CASE WHEN @update_avatar_id::boolean THEN @avatar_id ELSE avatar_id END
WHERE id = sqlc.arg('id'); WHERE id = sqlc.arg('id');
@@ -66,7 +79,7 @@ DELETE
FROM sys_user_role FROM sys_user_role
WHERE user_id = $1; WHERE user_id = $1;
-- name: CreateSysUserRole :exec -- name: CreateSysUserRole :copyfrom
INSERT INTO sys_user_role (user_id, role_id) INSERT INTO sys_user_role (user_id, role_id)
VALUES ($1, $2); VALUES ($1, $2);
@@ -79,19 +92,44 @@ WHERE ur.user_id = $1;
-- name: GetSysAdminMenus :many -- name: GetSysAdminMenus :many
SELECT * SELECT *
FROM sys_menus FROM sys_menus
WHERE status = 1
AND type <> 2
ORDER BY sort, id; ORDER BY sort, id;
-- name: GetSysAdminPermissions :many
SELECT code
FROM sys_permissions
WHERE type = 0;
-- name: GetSysUserMenus :many -- name: GetSysUserMenus :many
SELECT DISTINCT m.* SELECT DISTINCT m.*
FROM sys_users u FROM sys_users u
JOIN sys_user_role ur ON ur.user_id = u.id JOIN sys_user_role ur ON ur.user_id = u.id
JOIN sys_roles r ON r.id = ur.role_id JOIN sys_roles r ON r.id = ur.role_id
JOIN sys_role_permission rp ON rp.role_id = r.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_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 = p.id
JOIN sys_menus m ON m.id = mp.menu_id JOIN sys_menus m ON m.id = mp.menu_id
WHERE u.id = $1 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 -- name: GetSysUserApis :many
SELECT DISTINCT a.id, SELECT DISTINCT a.id,
@@ -99,9 +137,10 @@ SELECT DISTINCT a.id,
a.METHOD a.METHOD
FROM sys_users u FROM sys_users u
JOIN sys_user_role ur ON u.id = ur.user_id 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_roles r ON r.id = ur.role_id
JOIN sys_permissions p ON p.id = rp.permission_id JOIN sys_role_permission rp ON rp.role_id = r.id
AND p.TYPE = 1 JOIN sys_api_permission ap ON ap.permission_id = rp.permission_id
JOIN sys_api_permission ap ON ap.permission_id = p.id
JOIN sys_apis a ON a.id = ap.api_id JOIN sys_apis a ON a.id = ap.api_id
WHERE u.id = $1; WHERE u.id = $1
AND u.status = 1
AND r.status = 1;

View File

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

27
internal/db/redis.go Normal file
View File

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

View File

@@ -4,6 +4,7 @@ CREATE TABLE categories
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL, name TEXT NOT NULL,
code TEXT UNIQUE NOT NULL, code TEXT UNIQUE NOT NULL,
sort INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ updated_at TIMESTAMPTZ
); );
@@ -16,4 +17,5 @@ EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN categories.id IS '分类ID'; COMMENT ON COLUMN categories.id IS '分类ID';
COMMENT ON COLUMN categories.name IS '分类名称'; COMMENT ON COLUMN categories.name IS '分类名称';
COMMENT ON COLUMN categories.code IS '分类编码'; COMMENT ON COLUMN categories.code IS '分类编码';
COMMENT ON COLUMN categories.sort IS '分类排序';

View File

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

View File

@@ -2,7 +2,7 @@ CREATE TABLE sys_menus
( (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL, name TEXT NOT NULL,
path TEXT NOT NULL, path TEXT,
component TEXT, component TEXT,
type SMALLINT NOT NULL, type SMALLINT NOT NULL,
hidden BOOLEAN DEFAULT FALSE, hidden BOOLEAN DEFAULT FALSE,
@@ -17,6 +17,9 @@ CREATE TABLE sys_menus
CHECK (status IN (0, 1)) 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 CREATE TRIGGER update_sys_menus_updated_at
BEFORE UPDATE BEFORE UPDATE

View File

@@ -3,7 +3,7 @@ CREATE TABLE sys_permissions
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
code TEXT UNIQUE, code TEXT UNIQUE,
type SMALLINT NOT NULL, type SMALLINT NOT NULL,
created_at TIMESTAMPTZtz NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ, updated_at TIMESTAMPTZ,
CONSTRAINT unique_type_code UNIQUE (type, code) CONSTRAINT unique_type_code UNIQUE (type, code)

View File

@@ -3,8 +3,11 @@ CREATE TABLE sys_roles
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL, name TEXT NOT NULL,
code TEXT NOT NULL UNIQUE, code TEXT NOT NULL UNIQUE,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 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 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.id IS '主键ID';
COMMENT ON COLUMN sys_roles.name IS '角色名称'; COMMENT ON COLUMN sys_roles.name IS '角色名称';
COMMENT ON COLUMN sys_roles.code 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.created_at IS '创建时间';
COMMENT ON COLUMN sys_roles.updated_at IS '更新时间'; COMMENT ON COLUMN sys_roles.updated_at IS '更新时间';

View File

@@ -1,12 +1,15 @@
CREATE TABLE sys_users CREATE TABLE sys_users
( (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
account TEXT NOT NULL UNIQUE, account TEXT NOT NULL UNIQUE,
username TEXT NOT NULL, username TEXT NOT NULL,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
status SMALLINT NOT NULL DEFAULT 1,
avatar_id INTEGER, avatar_id INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 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); 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.account IS '用户账号';
COMMENT ON COLUMN sys_users.username IS '用户名称'; COMMENT ON COLUMN sys_users.username IS '用户名称';
COMMENT ON COLUMN sys_users.password_hash IS 'hash密码'; 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.avatar_id IS '头像文件id';
COMMENT ON COLUMN sys_users.created_at IS '创建时间'; COMMENT ON COLUMN sys_users.created_at IS '创建时间';
COMMENT ON COLUMN sys_users.updated_at IS '更新时间'; COMMENT ON COLUMN sys_users.updated_at IS '更新时间';

View File

@@ -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 '标签排序';

View File

@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
// source: category.sql // source: categories.sql
package db package sqlc
import ( import (
"context" "context"
@@ -22,17 +22,18 @@ func (q *Queries) CountCategories(ctx context.Context) (int64, error) {
} }
const createCategory = `-- name: CreateCategory :exec const createCategory = `-- name: CreateCategory :exec
INSERT INTO categories(name, code) INSERT INTO categories(name, code, sort)
VALUES ($1, $2) VALUES ($1, $2, $3)
` `
type CreateCategoryParams struct { type CreateCategoryParams struct {
Name string `json:"name"` Name string `json:"name"`
Code string `json:"code"` Code string `json:"code"`
Sort *int32 `json:"sort"`
} }
func (q *Queries) CreateCategory(ctx context.Context, arg CreateCategoryParams) error { 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 return err
} }
@@ -65,39 +66,32 @@ func (q *Queries) DeleteCategory(ctx context.Context, id int32) (int64, error) {
return result.RowsAffected(), nil 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 DELETE
FROM post_category FROM post_category
WHERE post_id = $1 WHERE post_id = $1
` `
func (q *Queries) DeletePostCategory(ctx context.Context, postID int32) error { func (q *Queries) DeletePostCategoryByPostID(ctx context.Context, postID int32) error {
_, err := q.db.Exec(ctx, deletePostCategory, postID) _, err := q.db.Exec(ctx, deletePostCategoryByPostID, postID)
return err 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 const listAllCategories = `-- name: ListAllCategories :many
SELECT id, name, code, created_at, updated_at SELECT id, name, code, sort, created_at, updated_at
FROM categories FROM categories
ORDER BY sort DESC, id
` `
func (q *Queries) ListAllCategories(ctx context.Context) ([]Category, error) { 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.ID,
&i.Name, &i.Name,
&i.Code, &i.Code,
&i.Sort,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@@ -127,10 +122,10 @@ func (q *Queries) ListAllCategories(ctx context.Context) ([]Category, error) {
} }
const listCategories = `-- name: ListCategories :many const listCategories = `-- name: ListCategories :many
SELECT id, name, code, created_at, updated_at SELECT id, name, code, sort, created_at, updated_at
FROM categories FROM categories
ORDER BY id LIMIT $1 ORDER BY sort DESC, id
OFFSET $2 LIMIT $1 OFFSET $2
` `
type ListCategoriesParams struct { type ListCategoriesParams struct {
@@ -151,6 +146,7 @@ func (q *Queries) ListCategories(ctx context.Context, arg ListCategoriesParams)
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Code, &i.Code,
&i.Sort,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@@ -167,18 +163,25 @@ func (q *Queries) ListCategories(ctx context.Context, arg ListCategoriesParams)
const updateCategory = `-- name: UpdateCategory :execrows const updateCategory = `-- name: UpdateCategory :execrows
UPDATE categories UPDATE categories
SET name = coalesce($1, name), SET name = coalesce($1, name),
code = coalesce($2, code) code = coalesce($2, code),
WHERE id = $3 sort = coalesce($3, sort)
WHERE id = $4
` `
type UpdateCategoryParams struct { type UpdateCategoryParams struct {
Name *string `json:"name"` Name *string `json:"name"`
Code *string `json:"code"` Code *string `json:"code"`
Sort *int32 `json:"sort"`
ID int32 `json:"id"` ID int32 `json:"id"`
} }
func (q *Queries) UpdateCategory(ctx context.Context, arg UpdateCategoryParams) (int64, error) { 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 { if err != nil {
return 0, err return 0, err
} }

View File

@@ -3,12 +3,45 @@
// sqlc v1.31.1 // sqlc v1.31.1
// source: copyfrom.go // source: copyfrom.go
package db package sqlc
import ( import (
"context" "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. // iteratorForCreateSysRolePermission implements pgx.CopyFromSource.
type iteratorForCreateSysRolePermission struct { type iteratorForCreateSysRolePermission struct {
rows []CreateSysRolePermissionParams rows []CreateSysRolePermissionParams
@@ -41,3 +74,36 @@ func (r iteratorForCreateSysRolePermission) Err() error {
func (q *Queries) CreateSysRolePermission(ctx context.Context, arg []CreateSysRolePermissionParams) (int64, 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}) 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})
}

View File

@@ -2,7 +2,7 @@
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
package db package sqlc
import ( import (
"context" "context"

View File

@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
// source: file.sql // source: files.sql
package db package sqlc
import ( import (
"context" "context"

View File

@@ -2,7 +2,7 @@
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
package db package sqlc
import ( import (
"net/netip" "net/netip"
@@ -15,7 +15,9 @@ type Category struct {
// 分类名称 // 分类名称
Name string `json:"name"` Name string `json:"name"`
// 分类编码 // 分类编码
Code string `json:"code"` Code string `json:"code"`
// 分类排序
Sort *int32 `json:"sort"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
} }
@@ -80,6 +82,13 @@ type PostStat struct {
View int32 `json:"view"` View int32 `json:"view"`
} }
type PostTag struct {
// 文章ID
PostID int32 `json:"post_id"`
// 标签id
TagID int32 `json:"tag_id"`
}
type PostViewer struct { type PostViewer struct {
// 文章id // 文章id
PostID int32 `json:"post_id"` PostID int32 `json:"post_id"`
@@ -121,7 +130,7 @@ type SysMenu struct {
// 菜单名称 // 菜单名称
Name string `json:"name"` Name string `json:"name"`
// 菜单路径 // 菜单路径
Path string `json:"path"` Path *string `json:"path"`
// 组件路径 // 组件路径
Component *string `json:"component"` Component *string `json:"component"`
// 菜单类型 0: 目录 1: 菜单 2: button // 菜单类型 0: 目录 1: 菜单 2: button
@@ -156,7 +165,7 @@ type SysPermission struct {
Code *string `json:"code"` Code *string `json:"code"`
Type int16 `json:"type"` Type int16 `json:"type"`
// 创建时间 // 创建时间
CreatedAt interface{} `json:"created_at"` CreatedAt time.Time `json:"created_at"`
// 更新时间 // 更新时间
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
} }
@@ -168,6 +177,8 @@ type SysRole struct {
Name string `json:"name"` Name string `json:"name"`
// 角色编码,唯一 // 角色编码,唯一
Code string `json:"code"` Code string `json:"code"`
// 角色状态 0:禁用 1:启用
Status int16 `json:"status"`
// 创建时间 // 创建时间
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
// 更新时间 // 更新时间
@@ -190,6 +201,8 @@ type SysUser struct {
Username string `json:"username"` Username string `json:"username"`
// hash密码 // hash密码
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
// 用户状态 0:禁用 1:启用
Status int16 `json:"status"`
// 头像文件id // 头像文件id
AvatarID *int32 `json:"avatar_id"` AvatarID *int32 `json:"avatar_id"`
// 创建时间 // 创建时间
@@ -204,3 +217,16 @@ type SysUserRole struct {
// 角色ID // 角色ID
RoleID int32 `json:"role_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"`
}

View File

@@ -3,7 +3,7 @@
// sqlc v1.31.1 // sqlc v1.31.1
// source: post.sql // source: post.sql
package db package sqlc
import ( import (
"context" "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, 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, f.file_path AS cover,
c.name AS category_name, 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 FROM posts p
LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_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 WHERE p.id = $1
GROUP BY p.id,
f.file_path,
c.name,
c.id
LIMIT 1 LIMIT 1
` `
type GetPostByIdRow struct { type GetPostByIdRow struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
CoverID *int32 `json:"cover_id"` CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"` Slug string `json:"slug"`
Content string `json:"content"` Content string `json:"content"`
Summary string `json:"summary"` Summary string `json:"summary"`
Status int16 `json:"status"` Status int16 `json:"status"`
Sort *int32 `json:"sort"` Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"` PublishedAt time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
Cover *string `json:"cover"` Cover *string `json:"cover"`
CategoryName *string `json:"category_name"` CategoryName *string `json:"category_name"`
CategoryID *int32 `json:"category_id"` CategoryID *int32 `json:"category_id"`
Tags interface{} `json:"tags"`
} }
func (q *Queries) GetPostById(ctx context.Context, id int32) (GetPostByIdRow, error) { 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.Cover,
&i.CategoryName, &i.CategoryName,
&i.CategoryID, &i.CategoryID,
&i.Tags,
) )
return i, err return i, err
} }
@@ -252,7 +263,9 @@ FROM
AND p.published_at < NOW() AND p.published_at < NOW()
GROUP BY GROUP BY
c.id, c.id,
c.sort,
c.name c.name
ORDER BY c.sort DESC, c.id
` `
type ListCategoryStatsRow struct { type ListCategoryStatsRow struct {
@@ -296,17 +309,45 @@ WITH paginated_posts AS (
updated_at updated_at
FROM posts FROM posts
ORDER BY sort DESC, published_at DESC, id DESC 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, 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, f.file_path AS cover,
c.name AS category_name, c.name AS category_name,
c.id AS category_id, 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 FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_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_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 ORDER BY p.sort DESC, p.published_at DESC, p.id DESC
` `
@@ -316,20 +357,21 @@ type ListPostsParams struct {
} }
type ListPostsRow struct { type ListPostsRow struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
CoverID *int32 `json:"cover_id"` CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"` Slug string `json:"slug"`
Summary string `json:"summary"` Summary string `json:"summary"`
Status int16 `json:"status"` Status int16 `json:"status"`
Sort *int32 `json:"sort"` Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"` PublishedAt time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
Cover *string `json:"cover"` Cover *string `json:"cover"`
CategoryName *string `json:"category_name"` CategoryName *string `json:"category_name"`
CategoryID *int32 `json:"category_id"` CategoryID *int32 `json:"category_id"`
View int32 `json:"view"` View int32 `json:"view"`
Tags interface{} `json:"tags"`
} }
// 第二步:用这极少量的记录去进行 JOIN // 第二步:用这极少量的记录去进行 JOIN
@@ -357,6 +399,7 @@ func (q *Queries) ListPosts(ctx context.Context, arg ListPostsParams) ([]ListPos
&i.CategoryName, &i.CategoryName,
&i.CategoryID, &i.CategoryID,
&i.View, &i.View,
&i.Tags,
); err != nil { ); err != nil {
return nil, err 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, f.file_path AS cover,
c.name AS category_name, c.name AS category_name,
c.id AS category_id, 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 FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_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_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 ORDER BY p.sort DESC, p.published_at DESC, p.id DESC
` `
@@ -402,17 +469,18 @@ type ListPublishedPostsParams struct {
} }
type ListPublishedPostsRow struct { type ListPublishedPostsRow struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
CoverID *int32 `json:"cover_id"` CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"` Slug string `json:"slug"`
Summary string `json:"summary"` Summary string `json:"summary"`
Sort *int32 `json:"sort"` Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"` PublishedAt time.Time `json:"published_at"`
Cover *string `json:"cover"` Cover *string `json:"cover"`
CategoryName *string `json:"category_name"` CategoryName *string `json:"category_name"`
CategoryID *int32 `json:"category_id"` CategoryID *int32 `json:"category_id"`
View int32 `json:"view"` View int32 `json:"view"`
Tags interface{} `json:"tags"`
} }
func (q *Queries) ListPublishedPosts(ctx context.Context, arg ListPublishedPostsParams) ([]ListPublishedPostsRow, error) { 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.CategoryName,
&i.CategoryID, &i.CategoryID,
&i.View, &i.View,
&i.Tags,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@@ -3,7 +3,7 @@
// sqlc v1.31.1 // sqlc v1.31.1
// source: post_stats.sql // source: post_stats.sql
package db package sqlc
import ( import (
"context" "context"

View File

@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
// source: sys_api.sql // source: sys_apis.sql
package db package sqlc
import ( import (
"context" "context"
@@ -12,10 +12,17 @@ import (
const countSysApis = `-- name: CountSysApis :one const countSysApis = `-- name: CountSysApis :one
SELECT COUNT(*) SELECT COUNT(*)
FROM sys_apis 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) { type CountSysApisParams struct {
row := q.db.QueryRow(ctx, countSysApis) 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 var count int64
err := row.Scan(&count) err := row.Scan(&count)
return count, err return count, err
@@ -88,19 +95,6 @@ func (q *Queries) DeleteSysApiPermission(ctx context.Context, apiID int32) error
return err 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 const getAllSysApis = `-- name: GetAllSysApis :many
SELECT id, group_name, name, method, path, sort, created_at, updated_at SELECT id, group_name, name, method, path, sort, created_at, updated_at
FROM sys_apis FROM sys_apis
@@ -186,17 +180,26 @@ func (q *Queries) GetSysApiGroupNames(ctx context.Context) ([]string, error) {
const getSysApis = `-- name: GetSysApis :many const getSysApis = `-- name: GetSysApis :many
SELECT id, group_name, name, method, path, sort, created_at, updated_at SELECT id, group_name, name, method, path, sort, created_at, updated_at
FROM sys_apis FROM sys_apis
WHERE ($3::text = '' OR group_name ILIKE '%' || $3 || '%')
AND ($4::text = '' OR method = $4)
ORDER BY id ORDER BY id
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
` `
type GetSysApisParams struct { type GetSysApisParams struct {
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
GroupName string `json:"group_name"`
Method string `json:"method"`
} }
func (q *Queries) GetSysApis(ctx context.Context, arg GetSysApisParams) ([]SysApi, error) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
// source: sys_menu.sql // source: sys_menus.sql
package db package sqlc
import ( import (
"context" "context"
@@ -30,7 +30,7 @@ RETURNING id
type CreateSysMenuParams struct { type CreateSysMenuParams struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path *string `json:"path"`
Component *string `json:"component"` Component *string `json:"component"`
Type int16 `json:"type"` Type int16 `json:"type"`
Hidden *bool `json:"hidden"` Hidden *bool `json:"hidden"`
@@ -123,7 +123,7 @@ ORDER BY m.sort ASC,
type GetAllSysMenusRow struct { type GetAllSysMenusRow struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path *string `json:"path"`
Component *string `json:"component"` Component *string `json:"component"`
Type int16 `json:"type"` Type int16 `json:"type"`
Hidden *bool `json:"hidden"` Hidden *bool `json:"hidden"`
@@ -213,7 +213,7 @@ type ListSysMenusParams struct {
type ListSysMenusRow struct { type ListSysMenusRow struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path *string `json:"path"`
Component *string `json:"component"` Component *string `json:"component"`
Type int16 `json:"type"` Type int16 `json:"type"`
Hidden *bool `json:"hidden"` Hidden *bool `json:"hidden"`
@@ -269,9 +269,9 @@ SET name = coalesce($1, name),
hidden = coalesce($5, hidden), hidden = coalesce($5, hidden),
sort = coalesce($6, sort), sort = coalesce($6, sort),
status = coalesce($7, status), status = coalesce($7, status),
icon = coalesce($8, status), icon = CASE WHEN $8::boolean THEN $9 ELSE icon END,
parent_id = CASE WHEN $9::boolean THEN $10 ELSE parent_id END parent_id = CASE WHEN $10::boolean THEN $11 ELSE parent_id END
WHERE id = $11 WHERE id = $12
` `
type UpdateSysMenuParams struct { type UpdateSysMenuParams struct {
@@ -282,6 +282,7 @@ type UpdateSysMenuParams struct {
Hidden *bool `json:"hidden"` Hidden *bool `json:"hidden"`
Sort *int32 `json:"sort"` Sort *int32 `json:"sort"`
Status *int16 `json:"status"` Status *int16 `json:"status"`
UpdateIcon bool `json:"update_icon"`
Icon *int32 `json:"icon"` Icon *int32 `json:"icon"`
UpdateParentID bool `json:"update_parent_id"` UpdateParentID bool `json:"update_parent_id"`
ParentID *int32 `json:"parent_id"` ParentID *int32 `json:"parent_id"`
@@ -297,6 +298,7 @@ func (q *Queries) UpdateSysMenu(ctx context.Context, arg UpdateSysMenuParams) (i
arg.Hidden, arg.Hidden,
arg.Sort, arg.Sort,
arg.Status, arg.Status,
arg.UpdateIcon,
arg.Icon, arg.Icon,
arg.UpdateParentID, arg.UpdateParentID,
arg.ParentID, arg.ParentID,

View File

@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
// source: sys_permission.sql // source: sys_permissions.sql
package db package sqlc
import ( import (
"context" "context"
@@ -37,6 +37,32 @@ func (q *Queries) DeleteSysPermission(ctx context.Context, id int32) error {
return err 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 const updateSysPermission = `-- name: UpdateSysPermission :exec
UPDATE sys_permissions UPDATE sys_permissions
SET code = $2 SET code = $2

View File

@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
// source: sys_role.sql // source: sys_roles.sql
package db package sqlc
import ( import (
"context" "context"
@@ -22,17 +22,18 @@ func (q *Queries) CountSysRoles(ctx context.Context) (int64, error) {
} }
const createSysRole = `-- name: CreateSysRole :exec const createSysRole = `-- name: CreateSysRole :exec
INSERT INTO sys_roles (name, code) INSERT INTO sys_roles (name, code, status)
VALUES ($1, $2) VALUES ($1, $2, $3)
` `
type CreateSysRoleParams struct { type CreateSysRoleParams struct {
Name string `json:"name"` Name string `json:"name"`
Code string `json:"code"` Code string `json:"code"`
Status int16 `json:"status"`
} }
func (q *Queries) CreateSysRole(ctx context.Context, arg CreateSysRoleParams) error { 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 return err
} }
@@ -55,21 +56,54 @@ func (q *Queries) DeleteSysRole(ctx context.Context, id int32) (int64, error) {
return result.RowsAffected(), nil 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 DELETE
FROM sys_role_permission rp USING sys_permissions p FROM sys_role_permission rp USING sys_permissions p
WHERE rp.permission_id = p.id WHERE rp.permission_id = p.id
AND p."type" = $2
AND rp.role_id = $1 AND rp.role_id = $1
AND p."type" = $2
` `
type DeleteSysRolePermissionParams struct { type DeleteSysRolePermissionByTypeParams struct {
RoleID int32 `json:"role_id"` RoleID int32 `json:"role_id"`
Type int16 `json:"type"` Type int16 `json:"type"`
} }
func (q *Queries) DeleteSysRolePermission(ctx context.Context, arg DeleteSysRolePermissionParams) error { func (q *Queries) DeleteSysRolePermissionByType(ctx context.Context, arg DeleteSysRolePermissionByTypeParams) error {
_, err := q.db.Exec(ctx, deleteSysRolePermission, arg.RoleID, arg.Type) _, 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 return err
} }
@@ -96,7 +130,7 @@ func (q *Queries) ExistsSysRoleCode(ctx context.Context, arg ExistsSysRoleCodePa
} }
const getAllSysRoles = `-- name: GetAllSysRoles :many 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 from sys_roles
` `
@@ -113,6 +147,7 @@ func (q *Queries) GetAllSysRoles(ctx context.Context) ([]SysRole, error) {
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Code, &i.Code,
&i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@@ -212,7 +247,7 @@ func (q *Queries) GetSysRoleApis(ctx context.Context, roleID int32) ([]GetSysRol
} }
const getSysRoleByID = `-- name: GetSysRoleByID :one 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 FROM sys_roles
WHERE id = $1 WHERE id = $1
` `
@@ -224,6 +259,7 @@ func (q *Queries) GetSysRoleByID(ctx context.Context, id int32) (SysRole, error)
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Code, &i.Code,
&i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
) )
@@ -264,7 +300,7 @@ func (q *Queries) GetSysRoleMenus(ctx context.Context, roleID int32) ([]GetSysRo
} }
const listSysRoles = `-- name: ListSysRoles :many 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 FROM sys_roles
ORDER BY id ORDER BY id
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
@@ -288,6 +324,7 @@ func (q *Queries) ListSysRoles(ctx context.Context, arg ListSysRolesParams) ([]S
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Code, &i.Code,
&i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@@ -303,17 +340,19 @@ func (q *Queries) ListSysRoles(ctx context.Context, arg ListSysRolesParams) ([]S
const updateSysRole = `-- name: UpdateSysRole :execrows const updateSysRole = `-- name: UpdateSysRole :execrows
UPDATE sys_roles UPDATE sys_roles
SET name = coalesce($1, name) SET name = coalesce($1, name),
WHERE id = $2 status = coalesce($2, status)
WHERE id = $3
` `
type UpdateSysRoleParams struct { type UpdateSysRoleParams struct {
Name *string `json:"name"` Name *string `json:"name"`
ID int32 `json:"id"` Status *int16 `json:"status"`
ID int32 `json:"id"`
} }
func (q *Queries) UpdateSysRole(ctx context.Context, arg UpdateSysRoleParams) (int64, error) { 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 { if err != nil {
return 0, err return 0, err
} }

View File

@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.31.1 // sqlc v1.31.1
// source: sys_user.sql // source: sys_users.sql
package db package sqlc
import ( import (
"context" "context"
@@ -24,24 +24,26 @@ func (q *Queries) ClearSysUserRoles(ctx context.Context, userID int32) error {
const countSysUsers = `-- name: CountSysUsers :one const countSysUsers = `-- name: CountSysUsers :one
SELECT COUNT(*) SELECT COUNT(*)
FROM sys_users FROM sys_users
WHERE ($1::text = '' OR username ILIKE '%' || $1 || '%')
` `
func (q *Queries) CountSysUsers(ctx context.Context) (int64, error) { func (q *Queries) CountSysUsers(ctx context.Context, username string) (int64, error) {
row := q.db.QueryRow(ctx, countSysUsers) row := q.db.QueryRow(ctx, countSysUsers, username)
var count int64 var count int64
err := row.Scan(&count) err := row.Scan(&count)
return count, err return count, err
} }
const createSysUser = `-- name: CreateSysUser :exec const createSysUser = `-- name: CreateSysUser :exec
INSERT INTO sys_users (account, username, password_hash, avatar_id) INSERT INTO sys_users (account, username, password_hash, status, avatar_id)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
` `
type CreateSysUserParams struct { type CreateSysUserParams struct {
Account string `json:"account"` Account string `json:"account"`
Username string `json:"username"` Username string `json:"username"`
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
Status int16 `json:"status"`
AvatarID *int32 `json:"avatar_id"` AvatarID *int32 `json:"avatar_id"`
} }
@@ -50,26 +52,17 @@ func (q *Queries) CreateSysUser(ctx context.Context, arg CreateSysUserParams) er
arg.Account, arg.Account,
arg.Username, arg.Username,
arg.PasswordHash, arg.PasswordHash,
arg.Status,
arg.AvatarID, arg.AvatarID,
) )
return err return err
} }
const createSysUserRole = `-- name: CreateSysUserRole :exec
INSERT INTO sys_user_role (user_id, role_id)
VALUES ($1, $2)
`
type CreateSysUserRoleParams struct { type CreateSysUserRoleParams struct {
UserID int32 `json:"user_id"` UserID int32 `json:"user_id"`
RoleID int32 `json:"role_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 const deleteSysUser = `-- name: DeleteSysUser :execrows
DELETE DELETE
FROM sys_users FROM sys_users
@@ -85,9 +78,38 @@ func (q *Queries) DeleteSysUser(ctx context.Context, id int32) (int64, error) {
return result.RowsAffected(), nil 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 const getSysAdminMenus = `-- name: GetSysAdminMenus :many
SELECT id, name, path, component, type, hidden, sort, status, parent_id, icon, created_at, updated_at SELECT id, name, path, component, type, hidden, sort, status, parent_id, icon, created_at, updated_at
FROM sys_menus FROM sys_menus
WHERE status = 1
AND type <> 2
ORDER BY sort, id ORDER BY sort, id
` `
@@ -124,18 +146,45 @@ func (q *Queries) GetSysAdminMenus(ctx context.Context) ([]SysMenu, error) {
return items, nil 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 const getSysUserApis = `-- name: GetSysUserApis :many
SELECT DISTINCT a.id, SELECT DISTINCT a.id,
a.PATH, a.PATH,
a.METHOD a.METHOD
FROM sys_users u FROM sys_users u
JOIN sys_user_role ur ON u.id = ur.user_id 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_roles r ON r.id = ur.role_id
JOIN sys_permissions p ON p.id = rp.permission_id JOIN sys_role_permission rp ON rp.role_id = r.id
AND p.TYPE = 1 JOIN sys_api_permission ap ON ap.permission_id = rp.permission_id
JOIN sys_api_permission ap ON ap.permission_id = p.id
JOIN sys_apis a ON a.id = ap.api_id JOIN sys_apis a ON a.id = ap.api_id
WHERE u.id = $1 WHERE u.id = $1
AND u.status = 1
AND r.status = 1
` `
type GetSysUserApisRow struct { type GetSysUserApisRow struct {
@@ -169,6 +218,7 @@ SELECT u.id,
u.account, u.account,
u.username, u.username,
u.password_hash, u.password_hash,
u.status,
u.avatar_id, u.avatar_id,
f.file_path AS avatar_url, f.file_path AS avatar_url,
u.created_at, u.created_at,
@@ -183,6 +233,7 @@ type GetSysUserByAccountRow struct {
Account string `json:"account"` Account string `json:"account"`
Username string `json:"username"` Username string `json:"username"`
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
Status int16 `json:"status"`
AvatarID *int32 `json:"avatar_id"` AvatarID *int32 `json:"avatar_id"`
AvatarUrl *string `json:"avatar_url"` AvatarUrl *string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -197,6 +248,7 @@ func (q *Queries) GetSysUserByAccount(ctx context.Context, account string) (GetS
&i.Account, &i.Account,
&i.Username, &i.Username,
&i.PasswordHash, &i.PasswordHash,
&i.Status,
&i.AvatarID, &i.AvatarID,
&i.AvatarUrl, &i.AvatarUrl,
&i.CreatedAt, &i.CreatedAt,
@@ -210,6 +262,7 @@ SELECT u.id,
u.account, u.account,
u.username, u.username,
u.avatar_id, u.avatar_id,
u.status,
f.file_path AS avatar_url, f.file_path AS avatar_url,
u.created_at, u.created_at,
u.updated_at u.updated_at
@@ -223,6 +276,7 @@ type GetSysUserByIDRow struct {
Account string `json:"account"` Account string `json:"account"`
Username string `json:"username"` Username string `json:"username"`
AvatarID *int32 `json:"avatar_id"` AvatarID *int32 `json:"avatar_id"`
Status int16 `json:"status"`
AvatarUrl *string `json:"avatar_url"` AvatarUrl *string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
@@ -236,6 +290,7 @@ func (q *Queries) GetSysUserByID(ctx context.Context, id int32) (GetSysUserByIDR
&i.Account, &i.Account,
&i.Username, &i.Username,
&i.AvatarID, &i.AvatarID,
&i.Status,
&i.AvatarUrl, &i.AvatarUrl,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -249,10 +304,13 @@ FROM sys_users u
JOIN sys_user_role ur ON ur.user_id = u.id JOIN sys_user_role ur ON ur.user_id = u.id
JOIN sys_roles r ON r.id = ur.role_id JOIN sys_roles r ON r.id = ur.role_id
JOIN sys_role_permission rp ON rp.role_id = r.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 = rp.permission_id
JOIN sys_menu_permission mp ON mp.permission_id = p.id
JOIN sys_menus m ON m.id = mp.menu_id JOIN sys_menus m ON m.id = mp.menu_id
WHERE u.id = $1 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 ORDER BY m.sort, m.id
` `
@@ -289,8 +347,44 @@ func (q *Queries) GetSysUserMenus(ctx context.Context, id int32) ([]SysMenu, err
return items, nil 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 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 FROM sys_roles r
JOIN sys_user_role ur ON r.id = ur.role_id JOIN sys_user_role ur ON r.id = ur.role_id
WHERE ur.user_id = $1 WHERE ur.user_id = $1
@@ -309,6 +403,7 @@ func (q *Queries) GetSysUserRoles(ctx context.Context, userID int32) ([]SysRole,
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Code, &i.Code,
&i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@@ -326,25 +421,29 @@ const listSysUsers = `-- name: ListSysUsers :many
SELECT u.id, SELECT u.id,
u.account, u.account,
u.username, u.username,
u.status,
u.avatar_id, u.avatar_id,
f.file_path AS avatar_url, f.file_path AS avatar_url,
u.created_at, u.created_at,
u.updated_at u.updated_at
FROM sys_users u FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id LEFT JOIN files f ON u.avatar_id = f.id
WHERE ($3::text = '' OR u.username ILIKE '%' || $3 || '%')
ORDER BY u.id ORDER BY u.id
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
` `
type ListSysUsersParams struct { type ListSysUsersParams struct {
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
Username string `json:"username"`
} }
type ListSysUsersRow struct { type ListSysUsersRow struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Account string `json:"account"` Account string `json:"account"`
Username string `json:"username"` Username string `json:"username"`
Status int16 `json:"status"`
AvatarID *int32 `json:"avatar_id"` AvatarID *int32 `json:"avatar_id"`
AvatarUrl *string `json:"avatar_url"` AvatarUrl *string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -352,7 +451,7 @@ type ListSysUsersRow struct {
} }
func (q *Queries) ListSysUsers(ctx context.Context, arg ListSysUsersParams) ([]ListSysUsersRow, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -364,6 +463,7 @@ func (q *Queries) ListSysUsers(ctx context.Context, arg ListSysUsersParams) ([]L
&i.ID, &i.ID,
&i.Account, &i.Account,
&i.Username, &i.Username,
&i.Status,
&i.AvatarID, &i.AvatarID,
&i.AvatarUrl, &i.AvatarUrl,
&i.CreatedAt, &i.CreatedAt,
@@ -382,12 +482,14 @@ func (q *Queries) ListSysUsers(ctx context.Context, arg ListSysUsersParams) ([]L
const updateSysUser = `-- name: UpdateSysUser :execrows const updateSysUser = `-- name: UpdateSysUser :execrows
UPDATE sys_users UPDATE sys_users
SET username = coalesce($1, username), SET username = coalesce($1, username),
avatar_id = CASE WHEN $2::boolean THEN $3 ELSE avatar_id END status = coalesce($2, status),
WHERE id = $4 avatar_id = CASE WHEN $3::boolean THEN $4 ELSE avatar_id END
WHERE id = $5
` `
type UpdateSysUserParams struct { type UpdateSysUserParams struct {
Username *string `json:"username"` Username *string `json:"username"`
Status *int16 `json:"status"`
UpdateAvatarID bool `json:"update_avatar_id"` UpdateAvatarID bool `json:"update_avatar_id"`
AvatarID *int32 `json:"avatar_id"` AvatarID *int32 `json:"avatar_id"`
ID int32 `json:"id"` ID int32 `json:"id"`
@@ -396,6 +498,7 @@ type UpdateSysUserParams struct {
func (q *Queries) UpdateSysUser(ctx context.Context, arg UpdateSysUserParams) (int64, error) { func (q *Queries) UpdateSysUser(ctx context.Context, arg UpdateSysUserParams) (int64, error) {
result, err := q.db.Exec(ctx, updateSysUser, result, err := q.db.Exec(ctx, updateSysUser,
arg.Username, arg.Username,
arg.Status,
arg.UpdateAvatarID, arg.UpdateAvatarID,
arg.AvatarID, arg.AvatarID,
arg.ID, arg.ID,

View File

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

68
internal/db/store.go Normal file
View File

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

96
internal/handler/auth.go Normal file
View File

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

View File

@@ -13,6 +13,8 @@ var Module = fx.Module("handlers",
NewSysFileHandler, NewSysFileHandler,
NewSysPostHandler, NewSysPostHandler,
NewCategoryHandler, NewCategoryHandler,
NewAuthHandler,
NewTagHandler,
NewPostHandler, NewPostHandler,
NewWebHandler, NewWebHandler,

View File

@@ -81,3 +81,13 @@ func (h *PostHandler) ListArchives(w http.ResponseWriter, r *http.Request) {
httputil.Ok(w, list) 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)
}

View File

@@ -19,7 +19,13 @@ func NewSysApiHandler(apiService *service.SysApiService) *SysApiHandler {
func (h *SysApiHandler) ListPage(w http.ResponseWriter, r *http.Request) { func (h *SysApiHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r) 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 { if err != nil {
httputil.Fail(w, err) httputil.Fail(w, err)

View File

@@ -8,7 +8,6 @@ import (
"server/internal/pkg/errs" "server/internal/pkg/errs"
"server/internal/pkg/httputil" "server/internal/pkg/httputil"
"server/internal/service" "server/internal/service"
"time"
) )
type SysUserHandler struct { 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) { func (h *SysUserHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r) 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 { if err != nil {
httputil.Fail(w, err) httputil.Fail(w, err)
return return

99
internal/handler/tag.go Normal file
View File

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

View File

@@ -3,8 +3,10 @@ package middleware
import ( import (
"context" "context"
"net/http" "net/http"
db "server/internal/db/sqlc" "server/internal/db"
"server/internal/db/sqlc"
"server/internal/pkg/cache" "server/internal/pkg/cache"
"server/internal/pkg/cache/cachekey"
"server/internal/pkg/errs" "server/internal/pkg/errs"
"server/internal/pkg/httputil" "server/internal/pkg/httputil"
"strings" "strings"
@@ -17,20 +19,21 @@ const (
) )
type AuthMiddleware struct { type AuthMiddleware struct {
queries *db.Queries store *db.Store
cache *cache.Caches cache *cache.Caches
} }
func NewAuthMiddleware(queries *db.Queries, cache *cache.Caches) *AuthMiddleware { func NewAuthMiddleware(store *db.Store, cache *cache.Caches) *AuthMiddleware {
return &AuthMiddleware{ return &AuthMiddleware{
cache: cache, cache: cache,
queries: queries, store: store,
} }
} }
func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler { func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var err error
claims, ok := GetClaims(ctx) claims, ok := GetClaims(ctx)
if !ok || claims.UserID == 0 { if !ok || claims.UserID == 0 {
@@ -47,7 +50,8 @@ func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler {
return 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 { if err != nil {
httputil.Fail(w, err) httputil.Fail(w, err)
return return
@@ -70,21 +74,20 @@ func userIsAdmin(uid int32) bool {
return false 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 ( var (
apis []db.GetSysUserApisRow apis []sqlc.GetSysUserApisRow
err error 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 { if err != nil {
apis, err = queries.GetSysUserApis(ctx, uid) return false, err
if err != nil {
return false, err
}
cache.SysUserApisCache.Set(uid, apis)
} }
requestPath := chi.RouteContext(r.Context()).RoutePattern() requestPath := chi.RouteContext(r.Context()).RoutePattern()

View File

@@ -2,7 +2,9 @@ package middleware
import ( import (
"context" "context"
"errors" "crypto/rand"
"crypto/sha256"
"encoding/hex"
"net/http" "net/http"
"server/internal/config" "server/internal/config"
"server/internal/pkg/errs" "server/internal/pkg/errs"
@@ -71,46 +73,6 @@ func (m *JWTMiddleware) ParseToken(tokenStr string) (*Claims, error) {
return claims, nil 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 { func (m *JWTMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization") auth := r.Header.Get("Authorization")
@@ -161,24 +123,19 @@ func (m *JWTMiddleware) GenerateAccessToken(userID int32) (string, time.Time, er
return token, expiresAt, nil return token, expiresAt, nil
} }
func (m *JWTMiddleware) GenerateRefreshToken(userID int32) (string, time.Time, error) { func (m *JWTMiddleware) GenerateRefreshToken() (string, error) {
now := time.Now() b := make([]byte, 32)
expiresAt := now.Add(m.cfg.RefreshExpire)
refreshClaims := RefreshClaims{ _, err := rand.Read(b)
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))
if err != nil { 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[:])
} }

View File

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

View File

@@ -3,9 +3,11 @@ package request
type CreateCategoryRequest struct { type CreateCategoryRequest struct {
Name string `json:"name" validate:"required,min=1"` Name string `json:"name" validate:"required,min=1"`
Code string `json:"code" validate:"required,min=1"` Code string `json:"code" validate:"required,min=1"`
Sort *int32 `json:"sort" validate:"required"`
} }
type UpdateCategoryRequest struct { type UpdateCategoryRequest struct {
Name *string `json:"name" validate:"omitempty,min=1"` Name *string `json:"name" validate:"omitempty,min=1"`
Code *string `json:"code" validate:"omitempty,min=1"` Code *string `json:"code" validate:"omitempty,min=1"`
Sort *int32 `json:"sort" validate:"omitempty"`
} }

View File

@@ -6,7 +6,7 @@ import (
type CreatePostRequest struct { type CreatePostRequest struct {
Title string `json:"title" validate:"required,min=1"` 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"` Slug string `json:"slug" validate:"required,min=0"`
Content string `json:"content" validate:"required,min=0"` Content string `json:"content" validate:"required,min=0"`
Summary string `json:"summary" 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"` Sort *int32 `json:"sort" validate:"required,min=0"`
PublishedAt time.Time `json:"published_at" validate:"required"` PublishedAt time.Time `json:"published_at" validate:"required"`
CategoryID *int32 `json:"category_id" validate:"required,min=1"` CategoryID *int32 `json:"category_id" validate:"required,min=1"`
Tags []int32 `json:"tags" validate:"omitempty,dive,min=1"`
} }
type UpdatePostRequest struct { type UpdatePostRequest struct {
@@ -27,4 +28,5 @@ type UpdatePostRequest struct {
Sort *int32 `json:"sort" validate:"omitempty,min=0"` Sort *int32 `json:"sort" validate:"omitempty,min=0"`
PublishedAt *time.Time `json:"published_at" validate:"omitempty"` PublishedAt *time.Time `json:"published_at" validate:"omitempty"`
CategoryID *int32 `json:"category_id" validate:"required,min=1"` CategoryID *int32 `json:"category_id" validate:"required,min=1"`
Tags []int32 `json:"tags" validate:"omitempty,dive,min=1"`
} }

View File

@@ -1,5 +1,7 @@
package request package request
import "server/internal/model/common"
type CreateSysApiRequest struct { type CreateSysApiRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"` Name string `json:"name" validate:"required,min=1,max=100"`
GroupName string `json:"group_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"` Method string `json:"method" validate:"required,oneof=GET POST PUT PATCH DELETE"`
Sort *int32 `json:"sort" validate:"required,min=0"` 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"`
}

View File

@@ -6,7 +6,7 @@ import (
type CreateSysMenuRequest struct { type CreateSysMenuRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"` 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"` Type *int16 `json:"type" validate:"required,oneof=0 1 2"`
Component *string `json:"component" validate:"omitempty"` Component *string `json:"component" validate:"omitempty"`
Hidden *bool `json:"hidden" validate:"omitempty"` Hidden *bool `json:"hidden" validate:"omitempty"`
@@ -19,13 +19,13 @@ type CreateSysMenuRequest struct {
type UpdateSysMenuRequest struct { type UpdateSysMenuRequest struct {
Name *string `json:"name" validate:"omitempty,min=1,max=100"` 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"` Type *int16 `json:"type" validate:"omitempty,oneof=0 1 2"`
Component *string `json:"component" validate:"omitempty"` Component *string `json:"component" validate:"omitempty"`
Hidden *bool `json:"hidden" validate:"omitempty"` Hidden *bool `json:"hidden" validate:"omitempty"`
Sort *int32 `json:"sort" validate:"omitempty,min=0"` Sort *int32 `json:"sort" validate:"omitempty,min=0"`
Status *int16 `json:"status" validate:"omitempty,oneof=0 1"` Status *int16 `json:"status" validate:"omitempty,oneof=0 1"`
ParentID validator.NullInt32 `json:"parent_id" validate:"omitempty"` 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"` PermissionCode *string `json:"permission_code" validate:"omitempty,min=1,max=100"`
} }

View File

@@ -1 +0,0 @@
package request

View File

@@ -1,12 +1,14 @@
package request package request
type CreateSysRoleRequest struct { type CreateSysRoleRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"` Name string `json:"name" validate:"required,min=1,max=100"`
Code string `json:"code" validate:"required,max=100"` Code string `json:"code" validate:"required,max=100"`
Status *int16 `json:"status" validate:"required,oneof=0 1"`
} }
type UpdateSysRoleRequest struct { 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 { type SetSysRoleMenusRequest struct {

View File

@@ -1,19 +1,22 @@
package request package request
import ( import (
"server/internal/model/common"
"server/internal/pkg/validator" "server/internal/pkg/validator"
) )
type CreateSysUserRequest struct { 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"` Account string `json:"account" validate:"required,min=5,max=100"`
Password string `json:"password" validate:"required,min=6,max=255"` Password string `json:"password" validate:"required,min=6,max=255"`
AvatarID *int32 `json:"avatar_id" validate:"omitempty"` AvatarID *int32 `json:"avatar_id" validate:"omitempty"`
Status *int16 `json:"status" validate:"oneof=0 1"`
} }
type UpdateSysUserRequest struct { 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"` AvatarID validator.NullInt32 `json:"avatar_id" validate:"omitempty"`
Status *int16 `json:"status" validate:"omitempty,oneof=0 1"`
} }
type UpdateSysUserPassword struct { type UpdateSysUserPassword struct {
@@ -28,3 +31,8 @@ type LoginRequest struct {
Account string `json:"account" validate:"required,min=5,max=100"` Account string `json:"account" validate:"required,min=5,max=100"`
Password string `json:"password" validate:"required,min=6,max=255"` Password string `json:"password" validate:"required,min=6,max=255"`
} }
type SearchSysUserParams struct {
common.Pagination
Username string `json:"username" validate:"omitempty,max=50"`
}

View File

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

View File

@@ -1,12 +1,12 @@
package response package response
import ( import (
db "server/internal/db/sqlc" "server/internal/db/sqlc"
"server/internal/pkg/httputil" "server/internal/pkg/httputil"
) )
func ToFiles(files []db.File) []db.File { func ToFiles(files []sqlc.File) []sqlc.File {
result := make([]db.File, len(files)) result := make([]sqlc.File, len(files))
for i := range files { for i := range files {
result[i] = files[i] result[i] = files[i]
result[i].FilePath = httputil.BuildFileUrl(&files[i].FilePath) result[i].FilePath = httputil.BuildFileUrl(&files[i].FilePath)

View File

@@ -1,7 +1,7 @@
package response package response
import ( import (
db "server/internal/db/sqlc" "server/internal/db/sqlc"
"server/internal/pkg/httputil" "server/internal/pkg/httputil"
"time" "time"
) )
@@ -13,12 +13,13 @@ type SysUserRolesResponse struct {
} }
type SysUserInfo struct { type SysUserInfo struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Account string `json:"account"` Account string `json:"account"`
Username string `json:"username"` Username string `json:"username"`
AvatarUrl string `json:"avatar_url"` AvatarUrl string `json:"avatar_url"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Menus []db.SysMenu `json:"menus"` Menus []sqlc.SysMenu `json:"menus"`
Permissions []string `json:"permissions"`
} }
type LoginResponse struct { type LoginResponse struct {
@@ -28,18 +29,26 @@ type LoginResponse struct {
RefreshTokenExp time.Time `json:"refresh_token_exp"` 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)) roleCodes := make([]string, len(roles))
for i, role := range roles { for i, role := range roles {
roleCodes[i] = role.Code roleCodes[i] = role.Code
} }
permissions := make([]string, 0, len(p))
for _, code := range p {
if code != nil {
permissions = append(permissions, *code)
}
}
return &SysUserInfo{ return &SysUserInfo{
ID: user.ID, ID: user.ID,
Account: user.Account, Account: user.Account,
Username: user.Username, Username: user.Username,
AvatarUrl: httputil.BuildFileUrl(user.AvatarUrl), AvatarUrl: httputil.BuildFileUrl(user.AvatarUrl),
Roles: roleCodes, Roles: roleCodes,
Menus: menus, Menus: menus,
Permissions: permissions,
} }
} }

View File

@@ -1,31 +1,219 @@
package cache package cache
import ( 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 { type Caches struct {
SysUserApisCache *otter.Cache[int32, []db.GetSysUserApisRow] rdb *redis.Client
keyPrefix string
} }
func NewCaches() *Caches { func NewCaches(rdb *redis.Client, cfg *config.Config) *Caches {
sysUserApisCache := otter.Must(&otter.Options[int32, []db.GetSysUserApisRow]{
MaximumSize: 1_000,
})
return &Caches{ return &Caches{
SysUserApisCache: sysUserApisCache, rdb: rdb,
keyPrefix: cfg.Redis.KeyPrefix,
} }
} }
// ClearSysUserCache 清理单个用户缓存 func (c *Caches) key(key string) string {
func (c *Caches) ClearSysUserCache(userID int32) { if c.keyPrefix == "" {
c.SysUserApisCache.Invalidate(userID) return key
}
return c.keyPrefix + key
} }
// ClearAllSysUserCache 清理所有用户缓存 func (c *Caches) Set(ctx context.Context, key string, value any, expiration time.Duration) error {
func (c *Caches) ClearAllSysUserCache() { return c.rdb.Set(ctx, c.key(key), value, expiration).Err()
c.SysUserApisCache.InvalidateAll() }
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()
} }

25
internal/pkg/cache/cachekey/key.go vendored Normal file
View File

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

View File

@@ -14,6 +14,8 @@ const (
SysPermissionsCodeKey = "sys_permissions_code_key" SysPermissionsCodeKey = "sys_permissions_code_key"
SysApisMethodPathKey = "sys_apis_method_path_key" SysApisMethodPathKey = "sys_apis_method_path_key"
CategoryCodeKey = "categories_code_key" CategoryCodeKey = "categories_code_key"
TagCodeKey = "tags_code_key"
SysMenusPathUniqueIdx = "sys_menus_path_unique_idx"
) )
func MapRowsAffected(rows int64, err error, notFoundErr error) error { func MapRowsAffected(rows int64, err error, notFoundErr error) error {

View File

@@ -20,6 +20,7 @@ func New(httpCode int, msg string) *AppError {
var ( var (
ErrInvalidCredentials = New(http.StatusUnauthorized, "用户名或密码错误") ErrInvalidCredentials = New(http.StatusUnauthorized, "用户名或密码错误")
ErrUserDisabled = New(http.StatusForbidden, "用户已被禁用")
ErrUnauthenticated = New(http.StatusUnauthorized, "用户未登录或登录已失效") ErrUnauthenticated = New(http.StatusUnauthorized, "用户未登录或登录已失效")
ErrUnauthorized = New(http.StatusUnauthorized, "认证失败:无法获取当前用户信息") ErrUnauthorized = New(http.StatusUnauthorized, "认证失败:无法获取当前用户信息")
ErrInvalidToken = New(http.StatusUnauthorized, "登录凭证无效") ErrInvalidToken = New(http.StatusUnauthorized, "登录凭证无效")
@@ -28,6 +29,7 @@ var (
ErrExpiredRefreshToken = New(http.StatusBadRequest, "invalid_grant") ErrExpiredRefreshToken = New(http.StatusBadRequest, "invalid_grant")
ErrUserNotFound = New(http.StatusNotFound, "用户数据不存在") ErrUserNotFound = New(http.StatusNotFound, "用户数据不存在")
ErrCategoryNotFound = New(http.StatusNotFound, "分类数据不存在") ErrCategoryNotFound = New(http.StatusNotFound, "分类数据不存在")
ErrTagNotFound = New(http.StatusNotFound, "标签数据不存在")
ErrSysApiNotFound = New(http.StatusNotFound, "接口数据不存在") ErrSysApiNotFound = New(http.StatusNotFound, "接口数据不存在")
ErrPostNotFound = New(http.StatusNotFound, "文章数据不存在") ErrPostNotFound = New(http.StatusNotFound, "文章数据不存在")
ErrSysMenuNotFound = New(http.StatusNotFound, "菜单数据不存在") ErrSysMenuNotFound = New(http.StatusNotFound, "菜单数据不存在")
@@ -40,9 +42,11 @@ var (
ErrEmptyBody = New(http.StatusBadRequest, "请求内容(body)不能为空") ErrEmptyBody = New(http.StatusBadRequest, "请求内容(body)不能为空")
ErrInvalidJSON = New(http.StatusBadRequest, "请求数据格式错误") ErrInvalidJSON = New(http.StatusBadRequest, "请求数据格式错误")
ErrAccountAlreadyExists = New(http.StatusBadRequest, "账号已存在") ErrAccountAlreadyExists = New(http.StatusBadRequest, "账号已存在")
ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码已存在") ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码不允许重复")
ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug已存在") ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug不允许重复")
ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码已存在") ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码不允许重复")
ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码已存在") ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码不允许重复")
ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)已存在") ErrTagCodeAlreadyExists = New(http.StatusBadRequest, "标签编码不允许重复")
ErrSysMenusPathUniqueIdx = New(http.StatusBadRequest, "菜单路径不允许重复")
ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)不允许重复")
) )

View File

@@ -11,6 +11,7 @@ func SetupPostRoutes(r chi.Router, h *handler.PostHandler) {
r.Get("/post/{slug}", h.GetPost) r.Get("/post/{slug}", h.GetPost)
r.Get("/category/stats", h.ListCategoryStats) r.Get("/category/stats", h.ListCategoryStats)
r.Get("/post/archive", h.ListArchives) r.Get("/post/archive", h.ListArchives)
r.Get("/post/tags", h.ListPostTags)
} }
func SetupWebRoutes(r chi.Router, h *handler.WebHandler) { func SetupWebRoutes(r chi.Router, h *handler.WebHandler) {

View File

@@ -19,6 +19,7 @@ type Handlers struct {
AuthMiddleware *middleware.AuthMiddleware AuthMiddleware *middleware.AuthMiddleware
LoggerMiddleware *middleware.LoggerMiddleware LoggerMiddleware *middleware.LoggerMiddleware
// system // system
AuthHandler *handler.AuthHandler
SysUserHandler *handler.SysUserHandler SysUserHandler *handler.SysUserHandler
SysRoleHandler *handler.SysRoleHandler SysRoleHandler *handler.SysRoleHandler
SysMenuHandler *handler.SysMenuHandler SysMenuHandler *handler.SysMenuHandler
@@ -26,6 +27,7 @@ type Handlers struct {
SysFileHandler *handler.SysFileHandler SysFileHandler *handler.SysFileHandler
SysPostHandler *handler.SysPostHandler SysPostHandler *handler.SysPostHandler
CategoryHandler *handler.CategoryHandler CategoryHandler *handler.CategoryHandler
TagHandler *handler.TagHandler
// web // web
PostHandler *handler.PostHandler PostHandler *handler.PostHandler
WebHandler *handler.WebHandler WebHandler *handler.WebHandler
@@ -50,9 +52,9 @@ func NewRouter(m Handlers) *chi.Mux {
} }
r.Route("/api/admin", func(r chi.Router) { r.Route("/api/admin", func(r chi.Router) {
r.Post("/auth/login", m.SysUserHandler.Login) r.Post("/auth/login", m.AuthHandler.Login)
r.Post("/auth/refresh-token", m.SysUserHandler.RefreshToken) r.Post("/auth/refresh-token", m.AuthHandler.RefreshToken)
r.Post("/auth/logout", m.SysUserHandler.Logout) r.Post("/auth/logout", m.AuthHandler.Logout)
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(m.JwtMiddleware.Middleware) r.Use(m.JwtMiddleware.Middleware)
@@ -65,6 +67,7 @@ func NewRouter(m Handlers) *chi.Mux {
SetupSysFileRoutes(r, m.SysFileHandler) SetupSysFileRoutes(r, m.SysFileHandler)
SetupCategoryRoutes(r, m.CategoryHandler) SetupCategoryRoutes(r, m.CategoryHandler)
SetupSysPostRoutes(r, m.SysPostHandler) SetupSysPostRoutes(r, m.SysPostHandler)
SetupTagRoutes(r, m.TagHandler)
}) })
}) })

View File

@@ -6,6 +6,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// SetupSysUserRoutes 8
func SetupSysUserRoutes(r chi.Router, h *handler.SysUserHandler) { func SetupSysUserRoutes(r chi.Router, h *handler.SysUserHandler) {
r.Get("/user", h.ListPage) r.Get("/user", h.ListPage)
r.Get("/user/info", h.GetUserInfo) r.Get("/user/info", h.GetUserInfo)
@@ -17,6 +18,7 @@ func SetupSysUserRoutes(r chi.Router, h *handler.SysUserHandler) {
r.Delete("/user/{id}", h.Delete) r.Delete("/user/{id}", h.Delete)
} }
// SetupSysRoleRoutes 9
func SetupSysRoleRoutes(r chi.Router, h *handler.SysRoleHandler) { func SetupSysRoleRoutes(r chi.Router, h *handler.SysRoleHandler) {
r.Get("/role", h.ListPage) r.Get("/role", h.ListPage)
r.Get("/role/{id}/menus", h.GetRoleMenus) 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) r.Delete("/role/{id}", h.Delete)
} }
// SetupSysMenuRoutes 5
func SetupSysMenuRoutes(r chi.Router, h *handler.SysMenuHandler) { func SetupSysMenuRoutes(r chi.Router, h *handler.SysMenuHandler) {
r.Get("/menu", h.ListPage) r.Get("/menu", h.ListPage)
r.Get("/menu/all", h.GetMenus) r.Get("/menu/all", h.GetMenus)
@@ -37,6 +40,7 @@ func SetupSysMenuRoutes(r chi.Router, h *handler.SysMenuHandler) {
r.Delete("/menu/{id}", h.Delete) r.Delete("/menu/{id}", h.Delete)
} }
// SetupSysApiRoutes 6
func SetupSysApiRoutes(r chi.Router, h *handler.SysApiHandler) { func SetupSysApiRoutes(r chi.Router, h *handler.SysApiHandler) {
r.Get("/api", h.ListPage) r.Get("/api", h.ListPage)
r.Get("/api/all", h.GetAllSysApis) r.Get("/api/all", h.GetAllSysApis)
@@ -46,11 +50,13 @@ func SetupSysApiRoutes(r chi.Router, h *handler.SysApiHandler) {
r.Delete("/api/{id}", h.Delete) r.Delete("/api/{id}", h.Delete)
} }
// SetupSysFileRoutes 2
func SetupSysFileRoutes(r chi.Router, h *handler.SysFileHandler) { func SetupSysFileRoutes(r chi.Router, h *handler.SysFileHandler) {
r.Get("/file", h.ListPage) r.Get("/file", h.ListPage)
r.Post("/file", h.Upload) r.Post("/file", h.Upload)
} }
// SetupSysPostRoutes 5
func SetupSysPostRoutes(r chi.Router, h *handler.SysPostHandler) { func SetupSysPostRoutes(r chi.Router, h *handler.SysPostHandler) {
r.Get("/post", h.ListPage) r.Get("/post", h.ListPage)
r.Get("/post/{id}", h.GetPostById) r.Get("/post/{id}", h.GetPostById)
@@ -59,6 +65,7 @@ func SetupSysPostRoutes(r chi.Router, h *handler.SysPostHandler) {
r.Delete("/post/{id}", h.Delete) r.Delete("/post/{id}", h.Delete)
} }
// SetupCategoryRoutes 5
func SetupCategoryRoutes(r chi.Router, h *handler.CategoryHandler) { func SetupCategoryRoutes(r chi.Router, h *handler.CategoryHandler) {
r.Get("/category", h.ListPage) r.Get("/category", h.ListPage)
r.Get("/category/all", h.ListAll) 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.Patch("/category/{id}", h.Update)
r.Delete("/category/{id}", h.Delete) 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)
}

180
internal/service/auth.go Normal file
View File

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

View File

@@ -2,7 +2,8 @@ package service
import ( import (
"context" "context"
db "server/internal/db/sqlc" "server/internal/db"
"server/internal/db/sqlc"
"server/internal/model/common" "server/internal/model/common"
"server/internal/model/request" "server/internal/model/request"
"server/internal/pkg/dberr" "server/internal/pkg/dberr"
@@ -10,27 +11,27 @@ import (
) )
type CategoryService struct { type CategoryService struct {
queries *db.Queries store *db.Store
} }
func NewCategoryService(queries *db.Queries) *CategoryService { func NewCategoryService(store *db.Store) *CategoryService {
return &CategoryService{ return &CategoryService{
queries: queries, store: store,
} }
} }
func (s *CategoryService) ListPage(ctx context.Context, p *common.Pagination) ([]db.Category, int64, error) { func (s *CategoryService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.Category, int64, error) {
params := db.ListCategoriesParams{ params := sqlc.ListCategoriesParams{
Limit: p.PageSize, Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize, Offset: (p.Page - 1) * p.PageSize,
} }
total, err := s.queries.CountCategories(ctx) total, err := s.store.CountCategories(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
list, err := s.queries.ListCategories(ctx, params) list, err := s.store.ListCategories(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -38,27 +39,29 @@ func (s *CategoryService) ListPage(ctx context.Context, p *common.Pagination) ([
return list, total, nil return list, total, nil
} }
func (s *CategoryService) ListAll(ctx context.Context) ([]db.Category, error) { func (s *CategoryService) ListAll(ctx context.Context) ([]sqlc.Category, error) {
return s.queries.ListAllCategories(ctx) return s.store.ListAllCategories(ctx)
} }
func (s *CategoryService) Create(ctx context.Context, req request.CreateCategoryRequest) error { func (s *CategoryService) Create(ctx context.Context, req request.CreateCategoryRequest) error {
params := db.CreateCategoryParams{ params := sqlc.CreateCategoryParams{
Name: req.Name, Name: req.Name,
Code: req.Code, 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) return dberr.MapUniqueViolation(err, dberr.CategoryCodeKey, errs.ErrCategoryCodeAlreadyExists)
} }
func (s *CategoryService) Update(ctx context.Context, id int32, req request.UpdateCategoryRequest) error { func (s *CategoryService) Update(ctx context.Context, id int32, req request.UpdateCategoryRequest) error {
params := db.UpdateCategoryParams{ params := sqlc.UpdateCategoryParams{
ID: id, ID: id,
Name: req.Name, Name: req.Name,
Code: req.Code, 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 { if err = dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound); err != nil {
// 再判断code是否重复 // 再判断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 { func (s *CategoryService) Delete(ctx context.Context, id int32) error {
rows, err := s.queries.DeleteCategory(ctx, id) return s.store.WithTx(ctx, func(q *sqlc.Queries) error {
return dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound) err := q.DeletePostCategoryByCategoryID(ctx, id)
if err != nil {
return err
}
rows, err := q.DeleteCategory(ctx, id)
return dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound)
})
} }

View File

@@ -12,6 +12,8 @@ var Module = fx.Module("services",
NewSysApiService, NewSysApiService,
NewSysFileService, NewSysFileService,
NewSysPostService, NewSysPostService,
NewAuthService,
NewTagService,
NewPostService, NewPostService,
NewCategoryService, NewCategoryService,

View File

@@ -3,7 +3,8 @@ package service
import ( import (
"context" "context"
"net/netip" "net/netip"
db "server/internal/db/sqlc" "server/internal/db"
"server/internal/db/sqlc"
"server/internal/model/common" "server/internal/model/common"
"server/internal/model/response" "server/internal/model/response"
"server/internal/pkg/dberr" "server/internal/pkg/dberr"
@@ -12,27 +13,27 @@ import (
) )
type PostService struct { type PostService struct {
queries *db.Queries store *db.Store
} }
func NewPostService(queries *db.Queries) *PostService { func NewPostService(store *db.Store) *PostService {
return &PostService{ return &PostService{
queries: queries, store: store,
} }
} }
func (s *PostService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListPublishedPostsRow, int64, error) { func (s *PostService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.ListPublishedPostsRow, int64, error) {
params := db.ListPublishedPostsParams{ params := sqlc.ListPublishedPostsParams{
Limit: p.PageSize, Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize, Offset: (p.Page - 1) * p.PageSize,
} }
total, err := s.queries.CountPublishedPosts(ctx) total, err := s.store.CountPublishedPosts(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
list, err := s.queries.ListPublishedPosts(ctx, params) list, err := s.store.ListPublishedPosts(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -45,13 +46,13 @@ func (s *PostService) ListPage(ctx context.Context, p *common.Pagination) ([]db.
return list, total, nil return list, total, nil
} }
func (s *PostService) GetPost(ctx context.Context, slug string, ip netip.Addr) (*db.GetPublicPostBySlugRow, error) { func (s *PostService) GetPost(ctx context.Context, slug string, ip netip.Addr) (*sqlc.GetPublicPostBySlugRow, error) {
post, err := s.queries.GetPublicPostBySlug(ctx, slug) post, err := s.store.GetPublicPostBySlug(ctx, slug)
if err != nil { if err != nil {
return nil, dberr.MapNoRows(err, errs.ErrPostNotFound) return nil, dberr.MapNoRows(err, errs.ErrPostNotFound)
} }
_ = s.queries.IncrementPostStatsView(ctx, db.IncrementPostStatsViewParams{ _ = s.store.IncrementPostStatsView(ctx, sqlc.IncrementPostStatsViewParams{
PostID: post.ID, PostID: post.ID,
Ip: ip, Ip: ip,
}) })
@@ -59,12 +60,12 @@ func (s *PostService) GetPost(ctx context.Context, slug string, ip netip.Addr) (
return &post, nil return &post, nil
} }
func (s *PostService) ListCategoryStats(ctx context.Context) ([]db.ListCategoryStatsRow, error) { func (s *PostService) ListCategoryStats(ctx context.Context) ([]sqlc.ListCategoryStatsRow, error) {
return s.queries.ListCategoryStats(ctx) return s.store.ListCategoryStats(ctx)
} }
func (s *PostService) ListArchives(ctx context.Context) ([]response.ArchiveYear, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -111,3 +112,7 @@ func (s *PostService) ListArchives(ctx context.Context) ([]response.ArchiveYear,
return archive, nil return archive, nil
} }
func (s *PostService) ListPostTags(ctx context.Context) ([]sqlc.Tag, error) {
return s.store.ListAllTags(ctx)
}

View File

@@ -2,41 +2,46 @@ package service
import ( import (
"context" "context"
db "server/internal/db/sqlc" "server/internal/db"
"server/internal/model/common" "server/internal/db/sqlc"
"server/internal/model/enum" "server/internal/model/enum"
"server/internal/model/request" "server/internal/model/request"
"server/internal/pkg/cache" "server/internal/pkg/cache"
"server/internal/pkg/cache/cachekey"
"server/internal/pkg/dberr" "server/internal/pkg/dberr"
"server/internal/pkg/errs" "server/internal/pkg/errs"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
) )
type SysApiService struct { type SysApiService struct {
queries *db.Queries store *db.Store
pool *pgxpool.Pool cache *cache.Caches
cache *cache.Caches
} }
func NewSysApiService(queries *db.Queries, pool *pgxpool.Pool, cache *cache.Caches) *SysApiService { func NewSysApiService(store *db.Store, cache *cache.Caches) *SysApiService {
return &SysApiService{queries: queries, pool: pool, cache: cache} return &SysApiService{
store: store,
cache: cache,
}
} }
func (s *SysApiService) ListPage(ctx context.Context, p *common.Pagination) ([]db.SysApi, int64, error) { func (s *SysApiService) ListPage(ctx context.Context, p request.SearchSysApiParams) ([]sqlc.SysApi, int64, error) {
params := db.GetSysApisParams{ params := sqlc.GetSysApisParams{
Limit: p.PageSize, Limit: p.PageSize,
Offset: (p.Page - 1) * 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 { if err != nil {
return nil, 0, err return nil, 0, err
} }
list, err := s.queries.GetSysApis(ctx, params) list, err := s.store.GetSysApis(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -45,69 +50,54 @@ func (s *SysApiService) ListPage(ctx context.Context, p *common.Pagination) ([]d
return list, total, nil return list, total, nil
} }
func (s *SysApiService) GetAllSysApis(ctx context.Context) ([]db.SysApi, error) { func (s *SysApiService) GetAllSysApis(ctx context.Context) ([]sqlc.SysApi, error) {
return s.queries.GetAllSysApis(ctx) return s.store.GetAllSysApis(ctx)
} }
func (s *SysApiService) GetApiGroupNames(ctx context.Context) ([]string, error) { 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 { func (s *SysApiService) Create(ctx context.Context, req request.CreateSysApiRequest) error {
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) return s.store.WithTx(ctx, func(q *sqlc.Queries) error {
if err != nil { api := sqlc.CreateSysApiParams{
return err Name: req.Name,
} GroupName: req.GroupName,
Method: req.Method,
Path: req.Path,
Sort: req.Sort,
}
defer func(ctx context.Context) { // 创建权限
_ = tx.Rollback(ctx) permissionId, err := q.CreateSysPermission(ctx, sqlc.CreateSysPermissionParams{
}(ctx) Type: int16(enum.PermissionTypeApi),
})
q := db.New(tx) if err != nil {
return err
}
api := db.CreateSysApiParams{ // 创建api
Name: req.Name, apiId, err := q.CreateSysApi(ctx, api)
GroupName: req.GroupName, if err != nil {
Method: req.Method, return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists)
Path: req.Path, }
Sort: req.Sort,
} // 关联权限
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 { func (s *SysApiService) Update(ctx context.Context, id int32, req request.UpdateSysApiRequest) error {
api := db.UpdateSysApiParams{ api := sqlc.UpdateSysApiParams{
ID: id, ID: id,
Name: req.Name, Name: req.Name,
GroupName: req.GroupName, GroupName: req.GroupName,
@@ -116,43 +106,51 @@ func (s *SysApiService) Update(ctx context.Context, id int32, req request.Update
Sort: req.Sort, Sort: req.Sort,
} }
// 清理缓存 rows, err := s.store.UpdateSysApi(ctx, api)
s.cache.ClearAllSysUserCache()
rows, err := s.queries.UpdateSysApi(ctx, api)
if err = dberr.MapRowsAffected(rows, err, errs.ErrSysApiNotFound); err != nil { if err = dberr.MapRowsAffected(rows, err, errs.ErrSysApiNotFound); err != nil {
return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists) return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists)
} }
// 如果更新成功 则清理缓存
_ = s.cache.DelByPrefix(ctx, cachekey.UserApiPermissionsPattern)
return nil return nil
} }
func (s *SysApiService) Delete(ctx context.Context, id int32) error { 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 { if err != nil {
return err return err
} }
defer tx.Rollback(ctx) // 清理缓存 事务执行成功后才清理
_ = s.cache.DelByPrefix(ctx, cachekey.UserApiPermissionsPattern)
q := db.New(tx) return nil
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)
} }

View File

@@ -5,7 +5,8 @@ import (
"mime/multipart" "mime/multipart"
"os" "os"
"path/filepath" "path/filepath"
db "server/internal/db/sqlc" "server/internal/db"
"server/internal/db/sqlc"
"server/internal/model/common" "server/internal/model/common"
"server/internal/model/response" "server/internal/model/response"
"server/internal/pkg/httputil" "server/internal/pkg/httputil"
@@ -14,12 +15,12 @@ import (
) )
type SysFileService struct { type SysFileService struct {
queries *db.Queries store *db.Store
} }
func NewSysFileService(queries *db.Queries) *SysFileService { func NewSysFileService(store *db.Store) *SysFileService {
return &SysFileService{ return &SysFileService{
queries: queries, store: store,
} }
} }
@@ -40,19 +41,19 @@ func MakeSavedDir(folder string) (string, error) {
return uploadDir, nil return uploadDir, nil
} }
func (s *SysFileService) ListPage(ctx context.Context, p *common.Pagination) ([]db.File, int64, error) { func (s *SysFileService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.File, int64, error) {
params := db.GetFilesParams{ params := sqlc.GetFilesParams{
Limit: p.PageSize, Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize, Offset: (p.Page - 1) * p.PageSize,
} }
total, err := s.queries.CountFiles(ctx) total, err := s.store.CountFiles(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
list, err := s.queries.GetFiles(ctx, params) list, err := s.store.GetFiles(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -61,7 +62,7 @@ func (s *SysFileService) ListPage(ctx context.Context, p *common.Pagination) ([]
return response.ToFiles(list), total, nil 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() fileID, err := gonanoid.New()
if err != nil { if err != nil {
@@ -98,7 +99,7 @@ func (s *SysFileService) Upload(ctx context.Context, folder string, file *multip
return nil, err return nil, err
} }
params := db.CreateFileParams{ params := sqlc.CreateFileParams{
FileName: filename, FileName: filename,
FilePath: filePath, FilePath: filePath,
OriginalName: file.Filename, OriginalName: file.Filename,
@@ -107,7 +108,7 @@ func (s *SysFileService) Upload(ctx context.Context, folder string, file *multip
FileSize: file.Size, FileSize: file.Size,
} }
result, err := s.queries.CreateFile(ctx, params) result, err := s.store.CreateFile(ctx, params)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,75 +2,72 @@ package service
import ( import (
"context" "context"
db "server/internal/db/sqlc" "server/internal/db"
"server/internal/db/sqlc"
"server/internal/model/common" "server/internal/model/common"
"server/internal/model/enum" "server/internal/model/enum"
"server/internal/model/request" "server/internal/model/request"
"server/internal/pkg/cache"
"server/internal/pkg/cache/cachekey"
"server/internal/pkg/dberr" "server/internal/pkg/dberr"
"server/internal/pkg/errs" "server/internal/pkg/errs"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
) )
type SysMenuService struct { type SysMenuService struct {
pool *pgxpool.Pool store *db.Store
queries *db.Queries cache *cache.Caches
} }
func NewSysMenuService(queries *db.Queries, pool *pgxpool.Pool) *SysMenuService { func NewSysMenuService(store *db.Store, cache *cache.Caches) *SysMenuService {
return &SysMenuService{ return &SysMenuService{
queries: queries, store: store,
pool: pool, cache: cache,
} }
} }
func (s *SysMenuService) Create(ctx context.Context, req request.CreateSysMenuRequest) error { func (s *SysMenuService) Create(ctx context.Context, req request.CreateSysMenuRequest) error {
// 开启事务 err := s.store.WithTx(ctx, func(q *sqlc.Queries) error {
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) menu := sqlc.CreateSysMenuParams{
if err != nil { Name: req.Name,
return err 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) permissionId, err := q.CreateSysPermission(ctx, sqlc.CreateSysPermissionParams{
}(ctx) 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, menuId, err := q.CreateSysMenu(ctx, menu)
Path: req.Path, if err != nil {
Component: req.Component, return dberr.MapUniqueViolation(err, dberr.SysMenusPathUniqueIdx, errs.ErrSysMenusPathUniqueIdx)
Type: *req.Type, }
Status: *req.Status,
Hidden: req.Hidden,
Sort: req.Sort,
Icon: req.Icon,
}
permissionId, err := q.CreateSysPermission(ctx, db.CreateSysPermissionParams{ // 绑定权限
Type: int16(enum.PermissionTypeMenu), if err = q.CreateSysMenuPermission(ctx, sqlc.CreateSysMenuPermissionParams{
Code: &req.PermissionCode, MenuID: menuId,
PermissionID: permissionId,
}); err != nil {
return err
}
return nil
}) })
if err != 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 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 { 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 { if err != nil {
return err return err
} }
defer tx.Rollback(ctx)
q := db.New(tx) // 清理缓存
_ = s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern)
// 构造 menu 参数 return nil
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)
} }
func (s *SysMenuService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListSysMenusRow, int64, error) { func (s *SysMenuService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.ListSysMenusRow, int64, error) {
params := db.ListSysMenusParams{ params := sqlc.ListSysMenusParams{
Limit: p.PageSize, Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize, Offset: (p.Page - 1) * p.PageSize,
} }
total, err := s.queries.CountSysMenus(ctx) total, err := s.store.CountSysMenus(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
list, err := s.queries.ListSysMenus(ctx, params) list, err := s.store.ListSysMenus(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -145,34 +152,43 @@ func (s *SysMenuService) ListPage(ctx context.Context, p *common.Pagination) ([]
return list, total, nil return list, total, nil
} }
func (s *SysMenuService) GetMenus(ctx context.Context) ([]db.GetAllSysMenusRow, error) { func (s *SysMenuService) GetMenus(ctx context.Context) ([]sqlc.GetAllSysMenusRow, error) {
return s.queries.GetAllSysMenus(ctx) return s.store.GetAllSysMenus(ctx)
} }
func (s *SysMenuService) Delete(ctx context.Context, id int32) error { 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 { if err != nil {
return err return err
} }
defer tx.Rollback(ctx) // 清理缓存
_ = s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern)
q := db.New(tx) return nil
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)
} }

View File

@@ -2,41 +2,37 @@ package service
import ( import (
"context" "context"
db "server/internal/db/sqlc" "server/internal/db"
"server/internal/db/sqlc"
"server/internal/model/common" "server/internal/model/common"
"server/internal/model/request" "server/internal/model/request"
"server/internal/pkg/dberr" "server/internal/pkg/dberr"
"server/internal/pkg/errs" "server/internal/pkg/errs"
"server/internal/pkg/httputil" "server/internal/pkg/httputil"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
) )
type SysPostService struct { type SysPostService struct {
queries *db.Queries store *db.Store
pool *pgxpool.Pool
} }
func NewSysPostService(queries *db.Queries, pool *pgxpool.Pool) *SysPostService { func NewSysPostService(store *db.Store) *SysPostService {
return &SysPostService{ return &SysPostService{
queries: queries, store: store,
pool: pool,
} }
} }
func (s *SysPostService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListPostsRow, int64, error) { func (s *SysPostService) ListPage(ctx context.Context, p *common.Pagination) ([]sqlc.ListPostsRow, int64, error) {
params := db.ListPostsParams{ params := sqlc.ListPostsParams{
Limit: p.PageSize, Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize, Offset: (p.Page - 1) * p.PageSize,
} }
total, err := s.queries.CountPosts(ctx) total, err := s.store.CountPosts(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
list, err := s.queries.ListPosts(ctx, params) list, err := s.store.ListPosts(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -49,8 +45,8 @@ func (s *SysPostService) ListPage(ctx context.Context, p *common.Pagination) ([]
return list, total, nil return list, total, nil
} }
func (s *SysPostService) FindByID(ctx context.Context, id int32) (*db.GetPostByIdRow, error) { func (s *SysPostService) FindByID(ctx context.Context, id int32) (*sqlc.GetPostByIdRow, error) {
post, err := s.queries.GetPostById(ctx, id) post, err := s.store.GetPostById(ctx, id)
if err != nil { if err != nil {
return nil, dberr.MapNoRows(err, errs.ErrPostNotFound) 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) { func (s *SysPostService) Create(ctx context.Context, req request.CreatePostRequest) (int32, error) {
// 开启事务 postID, err := db.WithTxResult(ctx, s.store, func(q *sqlc.Queries) (int32, error) {
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) 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 { if err != nil {
return 0, err return 0, err
} }
defer func() { return postID, nil
_ = 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
} }
func (s *SysPostService) Update(ctx context.Context, id int32, req request.UpdatePostRequest) error { func (s *SysPostService) Update(ctx context.Context, id int32, req request.UpdatePostRequest) error {
// 开启事务 return s.store.WithTx(ctx, func(q *sqlc.Queries) error {
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) params := sqlc.UpdatePostParams{
if err != nil { Title: req.Title,
return err 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() { rows, err := q.UpdatePost(ctx, params)
_ = tx.Rollback(ctx) 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, if err = q.DeletePostCategoryByPostID(ctx, id); err != nil {
CoverID: req.CoverID, return err
Slug: req.Slug, }
Content: req.Content,
Summary: req.Summary,
Status: req.Status,
Sort: req.Sort,
PublishedAt: req.PublishedAt,
ID: id,
}
rows, err := q.UpdatePost(ctx, params) if err = q.CreatePostCategory(ctx, sqlc.CreatePostCategoryParams{
if err != nil { PostID: id,
return dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists) 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 { var tagsParams []sqlc.CreatePostTagParams
return err for _, tagID := range req.Tags {
} tagsParams = append(tagsParams, sqlc.CreatePostTagParams{
PostID: id,
TagID: tagID,
})
}
if err = q.CreatePostCategory(ctx, db.CreatePostCategoryParams{ if _, err = q.CreatePostTag(ctx, tagsParams); err != nil {
PostID: id, return err
CategoryID: *req.CategoryID, }
}); err != nil {
return err
}
// 提交 return nil
if err = tx.Commit(ctx); err != nil { })
return err
}
return nil
} }
func (s *SysPostService) Delete(ctx context.Context, id int32) error { func (s *SysPostService) Delete(ctx context.Context, id int32) error {
rows, err := s.queries.DeletePost(ctx, id) return s.store.WithTx(ctx, func(q *sqlc.Queries) error {
return dberr.MapRowsAffected(rows, err, errs.ErrPostNotFound) err := q.DeletePostCategoryByPostID(ctx, id)
if err != nil {
return err
}
rows, err := q.DeletePost(ctx, id)
return dberr.MapRowsAffected(rows, err, errs.ErrPostNotFound)
})
} }

View File

@@ -2,45 +2,56 @@ package service
import ( import (
"context" "context"
db "server/internal/db/sqlc" "errors"
"server/internal/db"
"server/internal/db/sqlc"
"server/internal/model/common" "server/internal/model/common"
"server/internal/model/enum" "server/internal/model/enum"
"server/internal/model/request" "server/internal/model/request"
"server/internal/pkg/cache" "server/internal/pkg/cache"
"server/internal/pkg/cache/cachekey"
"server/internal/pkg/dberr" "server/internal/pkg/dberr"
"server/internal/pkg/errs" "server/internal/pkg/errs"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
) )
type SysRoleService struct { type SysRoleService struct {
queries *db.Queries store *db.Store
pool *pgxpool.Pool cache *cache.Caches
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{ return &SysRoleService{
queries: queries, store: store,
pool: pool, cache: cache,
cache: cache,
} }
} }
func (s *SysRoleService) ListPage(ctx context.Context, p *common.Pagination) ([]db.SysRole, int64, error) { // clearCache 清理所有api权限和用户信息 禁用角色/删除角色需要清理
params := db.ListSysRolesParams{ 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, Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize, Offset: (p.Page - 1) * p.PageSize,
} }
total, err := s.queries.CountSysRoles(ctx) total, err := s.store.CountSysRoles(ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
list, err := s.queries.ListSysRoles(ctx, params) list, err := s.store.ListSysRoles(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -48,38 +59,36 @@ func (s *SysRoleService) ListPage(ctx context.Context, p *common.Pagination) ([]
return list, total, nil return list, total, nil
} }
func (s *SysRoleService) GetRoles(ctx context.Context) ([]db.SysRole, error) { func (s *SysRoleService) GetRoles(ctx context.Context) ([]sqlc.SysRole, error) {
return s.queries.GetAllSysRoles(ctx) return s.store.GetAllSysRoles(ctx)
} }
func (s *SysRoleService) GetRoleMenus(ctx context.Context, id int32) ([]db.GetSysRoleMenusRow, error) { func (s *SysRoleService) GetRoleMenus(ctx context.Context, id int32) ([]sqlc.GetSysRoleMenusRow, error) {
_, err := s.queries.GetSysRoleByID(ctx, id) _, err := s.store.GetSysRoleByID(ctx, id)
if err != nil { if err != nil {
return nil, dberr.MapNoRows(err, errs.ErrSysRoleNotFound) 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) { func (s *SysRoleService) GetRoleApis(ctx context.Context, id int32) ([]sqlc.GetSysRoleApisRow, error) {
_, err := s.queries.GetSysRoleByID(ctx, id) _, err := s.store.GetSysRoleByID(ctx, id)
if err != nil { if err != nil {
return nil, dberr.MapNoRows(err, errs.ErrSysRoleNotFound) 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 { func (s *SysRoleService) Create(ctx context.Context, req request.CreateSysRoleRequest) error {
params := db.CreateSysRoleParams{ params := sqlc.CreateSysRoleParams{
Name: req.Name, Name: req.Name,
Code: req.Code, Code: req.Code,
Status: *req.Status,
} }
// 清理缓存 err := s.store.CreateSysRole(ctx, params)
s.cache.ClearAllSysUserCache()
err := s.queries.CreateSysRole(ctx, params)
if err != nil { if err != nil {
return dberr.MapUniqueViolation(err, dberr.SysRoleCodeKey, errs.ErrCodeAlreadyExists) 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 { func (s *SysRoleService) Update(ctx context.Context, id int32, req request.UpdateSysRoleRequest) error {
params := db.UpdateSysRoleParams{ params := sqlc.UpdateSysRoleParams{
ID: id, ID: id,
Name: req.Name, Name: req.Name,
Status: req.Status,
} }
// 清理缓存 rows, err := s.store.UpdateSysRole(ctx, params)
s.cache.ClearAllSysUserCache() 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 { 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 { if err != nil {
return dberr.MapNoRows(err, errs.ErrSysRoleNotFound) 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 { if err != nil {
return err return err
} }
defer func(ctx context.Context) { // 清理缓存 这里只用清理用户信息
_ = tx.Rollback(ctx) _ = s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern)
}(ctx)
q := db.New(tx) return nil
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
} }
func (s *SysRoleService) SetRoleApis(ctx context.Context, roleID int32, req request.SetSysRoleApisRequest) error { 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 { if err != nil {
return dberr.MapNoRows(err, errs.ErrSysRoleNotFound) 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 { if err != nil {
return err return err
} }
defer func(ctx context.Context) { // 清理缓存 这里只用清理接口权限
_ = tx.Rollback(ctx) _ = s.cache.DelByPrefix(ctx, cachekey.UserApiPermissionsPattern)
}(ctx)
q := db.New(tx) return nil
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
} }
func (s *SysRoleService) Delete(ctx context.Context, id int32) error { 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() _ = s.clearCache(ctx)
rows, err := s.queries.DeleteSysRole(ctx, id)
return dberr.MapRowsAffected(rows, err, errs.ErrSysRoleNotFound) return nil
} }

View File

@@ -2,165 +2,166 @@ package service
import ( import (
"context" "context"
db "server/internal/db/sqlc" "errors"
"server/internal/db"
"server/internal/db/sqlc"
"server/internal/middleware" "server/internal/middleware"
"server/internal/model/common"
"server/internal/model/request" "server/internal/model/request"
"server/internal/model/response" "server/internal/model/response"
"server/internal/pkg/cache" "server/internal/pkg/cache"
"server/internal/pkg/cache/cachekey"
"server/internal/pkg/dberr" "server/internal/pkg/dberr"
"server/internal/pkg/errs" "server/internal/pkg/errs"
"server/internal/pkg/httputil" "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" "golang.org/x/sync/errgroup"
) )
type SysUserService struct { type SysUserService struct {
queries *db.Queries store *db.Store
pool *pgxpool.Pool jwt *middleware.JWTMiddleware
jwt *middleware.JWTMiddleware cache *cache.Caches
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{ return &SysUserService{
queries: queries, store: store,
pool: pool, jwt: jwt,
jwt: jwt, cache: cache,
cache: cache,
} }
} }
// generatePasswordHash 生成密码哈希 // clearUserCache
func generatePasswordHash(password string) (string, error) { func (s *SysUserService) clearUserCache(ctx context.Context, id int32) error {
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err := s.cache.Del(ctx, cachekey.UserApiPermissions(id)); err != nil {
if err != nil { return err
return "", err
} }
return string(hashed), nil
if err := s.cache.Del(ctx, cachekey.UserInfo(id)); err != nil {
return err
}
return nil
} }
// comparePasswordHash 比较密码哈希 // clearRefreshToken
func comparePasswordHash(passwordHash, inputPassword string) error { func (s *SysUserService) clearRefreshToken(ctx context.Context, id int32) 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)
keys, err := s.cache.SMembers(ctx, cachekey.AuthRefreshUser(id))
if err != nil { if err != nil {
return nil, errs.ErrInvalidCredentials return err
} }
if err = comparePasswordHash(user.PasswordHash, req.Password); err != nil { // 循环删除所有的token
return nil, errs.ErrInvalidCredentials 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 { err = s.cache.DelSetMembers(ctx, cachekey.AuthRefreshUser(id))
return nil, err return 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
} }
func (s *SysUserService) GetUserInfo(ctx context.Context, id int32, isAdmin bool) (*response.SysUserInfo, error) { 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 ( var (
m []db.SysMenu user sqlc.GetSysUserByIDRow
err error roles []sqlc.SysRole
menus []sqlc.SysMenu
permissions []*string
) )
if isAdmin {
m, err = s.queries.GetSysAdminMenus(ctx) g.Go(func() error {
} else { u, err := s.store.GetSysUserByID(ctx, id)
m, err = s.queries.GetSysUserMenus(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) { func (s *SysUserService) ListPage(ctx context.Context, p request.SearchSysUserParams) ([]sqlc.ListSysUsersRow, int64, error) {
params := db.ListSysUsersParams{ params := sqlc.ListSysUsersParams{
Limit: p.PageSize, Limit: p.PageSize,
Offset: (p.Page - 1) * 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 { if err != nil {
return nil, 0, err return nil, 0, err
} }
users, err := s.queries.ListSysUsers(ctx, params) users, err := s.store.ListSysUsers(ctx, params)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -174,14 +175,14 @@ func (s *SysUserService) ListPage(ctx context.Context, p *common.Pagination) ([]
return users, total, nil 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 { if err != nil {
return nil, dberr.MapNoRows(err, errs.ErrUserNotFound) 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 { 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 return err
} }
user := db.CreateSysUserParams{ user := sqlc.CreateSysUserParams{
Account: req.Account, Account: req.Account,
Username: req.Username, Username: req.Username,
PasswordHash: passwordHash, PasswordHash: passwordHash,
AvatarID: req.AvatarID, 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) 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 { func (s *SysUserService) Update(ctx context.Context, id int32, req request.UpdateSysUserRequest) error {
user := db.UpdateSysUserParams{ user := sqlc.UpdateSysUserParams{
Username: req.Username, Username: req.Username,
ID: id, ID: id,
Status: req.Status,
} }
if req.AvatarID.Set { 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) rows, err := s.store.UpdateSysUser(ctx, user)
return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) if err = dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound); err != nil {
}
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 {
return err 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 return nil
} }
@@ -268,13 +283,21 @@ func (s *SysUserService) UpdatePassword(ctx context.Context, id int32, req reque
return err return err
} }
params := db.UpdateSysUserPasswordParams{ params := sqlc.UpdateSysUserPasswordParams{
ID: id, ID: id,
PasswordHash: passwordHash, PasswordHash: passwordHash,
} }
rows, err := s.queries.UpdateSysUserPassword(ctx, params) rows, err := s.store.UpdateSysUserPassword(ctx, params)
return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) 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 { 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 return errs.ErrCannotDeleteSuperAdmin
} }
// 清理缓存 err := s.store.WithTx(ctx, func(q *sqlc.Queries) error {
s.cache.ClearSysUserCache(id) // 清理用户角色关联数据
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
} }

87
internal/service/tag.go Normal file
View File

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

View File

@@ -5,7 +5,7 @@ sql:
schema: "./internal/db/schema/*" schema: "./internal/db/schema/*"
gen: gen:
go: go:
package: "db" package: "sqlc"
out: "./internal/db/sqlc" out: "./internal/db/sqlc"
emit_empty_slices: true emit_empty_slices: true
emit_interface: false emit_interface: false