Compare commits

...

4 Commits
v1.3.4 ... main

Author SHA1 Message Date
AI Engineer
71983652a9 feat: align JS exports to PascalCase and flatten default instance (by AI) 2026-06-10 12:04:34 +08:00
AI Engineer
c8e214547c fix: align with encoding v1.5.2 (by AI) 2026-06-10 09:52:41 +08:00
AI Engineer
3495b56a36 feat: add SetConfig for dynamic configuration (by AI) 2026-06-08 20:47:52 +08:00
AI Engineer
c36b771ca0 对齐 Tag v1.5.0 (By AI) 2026-06-03 20:11:25 +08:00
7 changed files with 153 additions and 72 deletions

View File

@ -1,5 +1,9 @@
# CHANGELOG - redis # CHANGELOG - redis
## v1.5.1 (2026-06-08)
- **新增**: `SetConfig(name, setting string)` 方法,支持动态配置 Redis 连接(不依赖配置文件),方便通过别名获取连接。
- **优化**: 重构配置加载逻辑,确保动态配置与配置文件配置的共存与优先级。
## v1.3.3 (2026-05-30) ## v1.3.3 (2026-05-30)
- **新增**: 注册到 `jsmod` - **新增**: 注册到 `jsmod`
- **安全性**: 引入基于 Context 的细粒度权限控制。在 `SafeMode`仅允许读取操作GET/EXISTS/ZRANGE等所有写操作SET/DEL/EXPIRE/DO等将被拦截并返回错误。 - **安全性**: 引入基于 Context 的细粒度权限控制。在 `SafeMode`仅允许读取操作GET/EXISTS/ZRANGE等所有写操作SET/DEL/EXPIRE/DO等将被拦截并返回错误。

View File

@ -11,7 +11,8 @@
## API 指南 ## API 指南
### 基础连接 ### 基础连接
- `GetRedis(name string, logger *log.Logger) *Redis`: 获取或创建一个 Redis 实例(支持 DSN 或配置文件名)。 - `SetConfig(name, setting string)`: 动态设置 Redis 配置(不依赖配置文件),可通过别名获取连接。
- `GetRedis(name string, logger *log.Logger) *Redis`: 获取或创建一个 Redis 实例(支持 DSN、别名或配置文件名
- `NewRedis(conf *Config, logger *log.Logger) *Redis`: 使用指定配置创建 Redis 实例。 - `NewRedis(conf *Config, logger *log.Logger) *Redis`: 使用指定配置创建 Redis 实例。
### 核心操作 ### 核心操作

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"apigo.cc/go/cast" "apigo.cc/go/cast"
"apigo.cc/go/config"
"apigo.cc/go/crypto" "apigo.cc/go/crypto"
"apigo.cc/go/log" "apigo.cc/go/log"
"apigo.cc/go/safe" "apigo.cc/go/safe"
@ -30,6 +31,19 @@ type Config struct {
var redisConfigs = make(map[string]*Config) var redisConfigs = make(map[string]*Config)
var redisConfigsLock = sync.RWMutex{} var redisConfigsLock = sync.RWMutex{}
var redisConfigsOnce sync.Once
func SetConfig(name, setting string) {
redisConfigsOnce.Do(func() {
_ = config.Load(&redisConfigs, "redis")
})
conf := new(Config)
conf.ConfigureBy(setting)
redisConfigsLock.Lock()
redisConfigs[name] = conf
redisConfigsLock.Unlock()
}
var confAES *crypto.Symmetric var confAES *crypto.Symmetric

26
go.mod
View File

@ -3,22 +3,22 @@ module apigo.cc/go/redis
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/cast v1.3.3 apigo.cc/go/cast v1.5.0
apigo.cc/go/config v1.3.1 apigo.cc/go/config v1.5.0
apigo.cc/go/crypto v1.3.1 apigo.cc/go/crypto v1.5.0
apigo.cc/go/encoding v1.3.1 apigo.cc/go/encoding v1.5.0
apigo.cc/go/id v1.3.1 apigo.cc/go/id v1.5.0
apigo.cc/go/jsmod v1.0.1 apigo.cc/go/jsmod v1.5.0
apigo.cc/go/log v1.3.4 apigo.cc/go/log v1.5.0
apigo.cc/go/safe v1.3.1 apigo.cc/go/safe v1.5.0
github.com/gomodule/redigo v2.0.0+incompatible github.com/gomodule/redigo v2.0.0+incompatible
) )
require ( require (
apigo.cc/go/file v1.3.2 // indirect apigo.cc/go/file v1.5.0 // indirect
apigo.cc/go/rand v1.3.1 // indirect apigo.cc/go/rand v1.5.0 // indirect
apigo.cc/go/shell v1.3.1 // indirect apigo.cc/go/shell v1.5.0 // indirect
golang.org/x/crypto v0.51.0 // indirect golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.44.0 // indirect golang.org/x/sys v0.45.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

50
go.sum
View File

@ -1,25 +1,25 @@
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34= apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE= apigo.cc/go/config v1.5.0 h1:Yuz9QEb11XXG4XkhDi/ueT2M1T3Q9PElE5tiakvjehs=
apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U= apigo.cc/go/config v1.5.0/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM=
apigo.cc/go/crypto v1.3.1 h1:ulQ2zX9bUWirk0sEacx1Srsjs2Jow7HlZq7ED7msNcg= apigo.cc/go/crypto v1.5.0 h1:Nxz7a6VKCdvaF258IU0NkjQyureOLxfR308Sy2iftUI=
apigo.cc/go/crypto v1.3.1/go.mod h1:SwHlBFDPddttWgFFtzsEMla8CM/rcFy9nvdsJjW4CIs= apigo.cc/go/crypto v1.5.0/go.mod h1:F9M6nXv+5328r1ZwbTvI6fcr8VdgqHVzALOcsdv6ntE=
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y= apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY= apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA=
apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4= apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw=
apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs= apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0=
apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk= apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8=
apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4= apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA=
apigo.cc/go/jsmod v1.0.1 h1:vaz3cMQi75UVoALLfyV/Trs8iP/Nh28yN57IvBFpPGk= apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
apigo.cc/go/jsmod v1.0.1/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g= apigo.cc/go/log v1.5.0 h1:kQuLLtbt33mEuc/xJVcy8NODXkso/QKSZWNclKrSpsI=
apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20= apigo.cc/go/log v1.5.0/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8= apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q= apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE= apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM=
apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8= apigo.cc/go/shell v1.5.0 h1:WLDMMqUU0INeaBDmQsTPr0h/NfB2RknAtiJ5NL467+Q=
apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw= apigo.cc/go/shell v1.5.0/go.mod h1:rYHA77d5hEsQHcJrbAWf1pHy0sxayeJ0gU55LA/JWQk=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@ -28,10 +28,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -3,6 +3,7 @@ package redis
import ( import (
"context" "context"
"errors" "errors"
"strings"
"apigo.cc/go/id" "apigo.cc/go/id"
"apigo.cc/go/jsmod" "apigo.cc/go/jsmod"
@ -10,13 +11,60 @@ import (
func init() { func init() {
jsmod.Register("redis", map[string]any{ jsmod.Register("redis", map[string]any{
"get": func(ctx context.Context, name string) (*jsRedis, error) { // 入口:支持别名获取,不传则默认 "default"
rd := GetRedis(name, nil) "Get": func(ctx context.Context, name *string) (*jsRedis, error) {
target := "default"
if name != nil {
target = *name
}
rd := GetRedis(target, nil)
if rd.Error != nil { if rd.Error != nil {
return nil, rd.Error return nil, rd.Error
} }
return &jsRedis{rd: rd, ctx: ctx}, nil return &jsRedis{rd: rd, ctx: ctx}, nil
}, },
// 默认快捷调用 (面向 "default" 实例)
"Do": func(ctx context.Context, cmd string, args ...any) (*Result, error) {
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
if jr.rd.Error != nil {
return nil, jr.rd.Error
}
res := jr.Do(cmd, args...)
return res, res.Error
},
// 常用命令平铺 (面向 "default" 实例)
"SET": func(ctx context.Context, key string, val any) (*Result, error) {
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
res := jr.Do("SET", key, val)
return res, res.Error
},
"GET": func(ctx context.Context, key string) (*Result, error) {
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
res := jr.Do("GET", key)
return res, res.Error
},
"DEL": func(ctx context.Context, key string) (*Result, error) {
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
res := jr.Do("DEL", key)
return res, res.Error
},
"EXISTS": func(ctx context.Context, key string) (*Result, error) {
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
res := jr.Do("EXISTS", key)
return res, res.Error
},
"EXPIRE": func(ctx context.Context, key string, seconds int) (*Result, error) {
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
res := jr.Do("EXPIRE", key, seconds)
return res, res.Error
},
"PUBLISH": func(ctx context.Context, channel, data string) (*Result, error) {
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
res := jr.Do("PUBLISH", channel, data)
return res, res.Error
},
}) })
} }
@ -28,40 +76,60 @@ type jsRedis struct {
var errSafeMode = errors.New("redis operation is restricted in safe mode") var errSafeMode = errors.New("redis operation is restricted in safe mode")
func (jr *jsRedis) checkSafe() error { // 核心写操作指令集
var writeCommands = map[string]bool{
"SET": true, "SETEX": true, "SETNX": true, "MSET": true, "MSETNX": true,
"DEL": true, "EXPIRE": true, "EXPIREAT": true, "PEXPIRE": true, "PEXPIREAT": true,
"HSET": true, "HSETNX": true, "HDEL": true, "HMSET": true,
"LPUSH": true, "RPUSH": true, "LPOP": true, "RPOP": true, "LREM": true, "LTRIM": true,
"SADD": true, "SREM": true, "SPOP": true, "SMOVE": true,
"ZADD": true, "ZREM": true, "ZREMRANGEBYRANK": true, "ZREMRANGEBYSCORE": true,
"PUBLISH": true, "FLUSHDB": true, "FLUSHALL": true,
}
func (jr *jsRedis) checkSafe(cmd string) error {
if jsmod.IsSafeMode(jr.ctx) { if jsmod.IsSafeMode(jr.ctx) {
return errSafeMode cmd = strings.ToUpper(cmd)
if writeCommands[cmd] || !strings.Contains(" GET EXISTS ZRANGE HGET HGETALL SMEMBERS SISMEMBER LINDEX LLEN ", " "+cmd+" ") {
// 严格模式:不在白名单内的或在黑名单内的都禁止
return errSafeMode
}
} }
return nil return nil
} }
// Do executes any redis command. In SafeMode, it only allows read-only commands.
// Note: Since we don't have a reliable way to categorize all redis commands as read-only,
// and 'DO' is used for everything, we strictly block 'DO' in SafeMode if it's not a known read-only command.
// For simplicity and maximum safety as requested, we block 'DO' entirely in SafeMode.
func (jr *jsRedis) Do(cmd string, args ...any) *Result { func (jr *jsRedis) Do(cmd string, args ...any) *Result {
if jr.checkSafe() != nil { if err := jr.checkSafe(cmd); err != nil {
return &Result{Error: errSafeMode} return &Result{Error: err}
} }
return jr.rd.Do(cmd, args...) return jr.rd.Do(cmd, args...)
} }
// ID Generation Helpers // 实例方法 PascalCase 对齐
func (jr *jsRedis) getIDMaker() *id.IDMaker { func (jr *jsRedis) SET(key string, val any) *Result { return jr.Do("SET", key, val) }
func (jr *jsRedis) GET(key string) *Result { return jr.Do("GET", key) }
func (jr *jsRedis) DEL(key string) *Result { return jr.Do("DEL", key) }
func (jr *jsRedis) EXISTS(key string) *Result { return jr.Do("EXISTS", key) }
func (jr *jsRedis) EXPIRE(key string, s int) *Result { return jr.Do("EXPIRE", key, s) }
func (jr *jsRedis) HSET(key, field string, v any) *Result { return jr.Do("HSET", key, field, v) }
func (jr *jsRedis) HGET(key, field string) *Result { return jr.Do("HGET", key, field) }
func (jr *jsRedis) PUBLISH(ch, data string) *Result { return jr.Do("PUBLISH", ch, data) }
// ID Generation
func (jr *jsRedis) MakeID(size int, forDB *string) string {
if jr.idMaker == nil { if jr.idMaker == nil {
jr.idMaker = NewIDMaker(jr.rd) jr.idMaker = NewIDMaker(jr.rd)
} }
return jr.idMaker dbType := ""
} if forDB != nil {
dbType = strings.ToLower(*forDB)
func (jr *jsRedis) GetID(size int) string { }
return jr.getIDMaker().Get(size) switch dbType {
} case "mysql":
return jr.idMaker.GetForMysql(size)
func (jr *jsRedis) GetForMysql(size int) string { case "postgres", "pg", "pgsql":
return jr.getIDMaker().GetForMysql(size) return jr.idMaker.GetForPostgreSQL(size)
} default:
return jr.idMaker.Get(size)
func (jr *jsRedis) GetForPostgreSQL(size int) string { }
return jr.getIDMaker().GetForPostgreSQL(size)
} }

View File

@ -57,13 +57,9 @@ func GetRedis(name string, logger *log.Logger) *Redis {
return oldConn.CopyByLogger(logger) return oldConn.CopyByLogger(logger)
} }
redisConfigsLock.RLock() redisConfigsOnce.Do(func() {
configsLen := len(redisConfigs)
redisConfigsLock.RUnlock()
if configsLen == 0 {
_ = config.Load(&redisConfigs, "redis") _ = config.Load(&redisConfigs, "redis")
} })
fullName := name fullName := name
@ -76,7 +72,7 @@ func GetRedis(name string, logger *log.Logger) *Redis {
conf = parseByName(name) conf = parseByName(name)
} }
if pwd, err := confAES.Decrypt(cast.As(encoding.UnUrlBase64FromString(conf.Password))); err == nil { if pwd, err := confAES.Decrypt(cast.As(encoding.UnURLBase64(conf.Password))); err == nil {
conf.pwd = pwd conf.pwd = pwd
} else { } else {
conf.pwd = safe.NewSafeBuf([]byte(conf.Password)) conf.pwd = safe.NewSafeBuf([]byte(conf.Password))