chore: 初始开源发布
iotlcm-openapi:对外 OpenAPI 网关(V3 接口 + 旧版兼容接口)。
- Go module:iotlcm-openapi → git.5iot.com/iotlcm-public/iotlcm-openapi(178 文件)
- 凭据与地址脱敏:JWT signingKey、MySQL DSN、Redis 口令、
RabbitMQ DSN 全部改为 ${*} 占位;私网 IP → 文档地址段
- 7 处真实手机号 → 合法 11 位编造号
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- 移除内部资产:CI 触发器
- Dockerfile 改用公共基础镜像,移除 GOPRIVATE 内网设置
- README 重写(原为 GoFrame 模板占位文案),补 Apache-2.0 LICENSE,
LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package batchimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
batchNoPrefixLength = 3
|
||||
batchNoSequenceLength = 3
|
||||
batchNoSequenceMax = 999
|
||||
batchNoSequenceTTL = 48 * time.Hour
|
||||
batchNoSequenceRedisPrefix = "iotlcm:batch:no:seq:"
|
||||
)
|
||||
|
||||
func NextBatchNo(prefix string) (string, error) {
|
||||
return NextBatchNoContext(context.Background(), prefix)
|
||||
}
|
||||
|
||||
func NextBatchNoContext(ctx context.Context, prefix string) (string, error) {
|
||||
prefix, err := normalizeBatchNoPrefix(prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
now := time.Now()
|
||||
seq, err := nextDailyBatchSequence(ctx, now)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return formatBatchNo(prefix, now, seq)
|
||||
}
|
||||
|
||||
func normalizeBatchNoPrefix(prefix string) (string, error) {
|
||||
prefix = strings.TrimSpace(strings.ToUpper(prefix))
|
||||
if prefix == "" {
|
||||
return "", gerror.New("批次号前缀不能为空")
|
||||
}
|
||||
if len(prefix) != batchNoPrefixLength {
|
||||
return "", gerror.Newf("批次号前缀必须为%d位", batchNoPrefixLength)
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
func nextDailyBatchSequence(ctx context.Context, now time.Time) (int64, error) {
|
||||
key := batchNoSequenceKey(now)
|
||||
script := `
|
||||
local v = redis.call("INCR", KEYS[1])
|
||||
if v == 1 then
|
||||
redis.call("EXPIRE", KEYS[1], ARGV[1])
|
||||
end
|
||||
return v`
|
||||
res, err := g.Redis().Do(ctx, "EVAL", script, 1, key, int(batchNoSequenceTTL.Seconds()))
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "生成批次号序号失败")
|
||||
}
|
||||
seq := res.Int64()
|
||||
if seq <= 0 {
|
||||
return 0, gerror.New("生成批次号序号失败")
|
||||
}
|
||||
if seq > batchNoSequenceMax {
|
||||
return 0, gerror.Newf("当日批次号序号超过%03d", batchNoSequenceMax)
|
||||
}
|
||||
return seq, nil
|
||||
}
|
||||
|
||||
func batchNoSequenceKey(now time.Time) string {
|
||||
return batchNoSequenceRedisPrefix + now.Format("20060102")
|
||||
}
|
||||
|
||||
func formatBatchNo(prefix string, now time.Time, seq int64) (string, error) {
|
||||
if seq <= 0 || seq > batchNoSequenceMax {
|
||||
return "", gerror.Newf("批次号序号必须在1-%03d之间", batchNoSequenceMax)
|
||||
}
|
||||
return fmt.Sprintf("%s%s%0*d", prefix, now.Format("20060102150405"), batchNoSequenceLength, seq), nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package batchimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNormalizeBatchNoPrefix(t *testing.T) {
|
||||
prefix, err := normalizeBatchNoPrefix(" inb ")
|
||||
if err != nil {
|
||||
t.Fatalf("normalize prefix: %v", err)
|
||||
}
|
||||
if prefix != "INB" {
|
||||
t.Fatalf("unexpected prefix: %s", prefix)
|
||||
}
|
||||
|
||||
if _, err = normalizeBatchNoPrefix("IN"); err == nil {
|
||||
t.Fatal("expected error for non-three-character prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBatchNo(t *testing.T) {
|
||||
now := time.Date(2026, 8, 10, 12, 34, 56, 789000000, time.Local)
|
||||
batchNo, err := formatBatchNo("REN", now, 7)
|
||||
if err != nil {
|
||||
t.Fatalf("format batch no: %v", err)
|
||||
}
|
||||
if batchNo != "REN20260810123456007" {
|
||||
t.Fatalf("unexpected batch no: %s", batchNo)
|
||||
}
|
||||
if len(batchNo) != 20 {
|
||||
t.Fatalf("unexpected batch no length: %d", len(batchNo))
|
||||
}
|
||||
if strings.Contains(batchNo, "789") {
|
||||
t.Fatalf("batch no should not include milliseconds: %s", batchNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBatchNoRejectsOverflowSequence(t *testing.T) {
|
||||
_, err := formatBatchNo("REN", time.Now(), 1000)
|
||||
if err == nil {
|
||||
t.Fatal("expected overflow sequence error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchNoBoundaries(t *testing.T) {
|
||||
now := time.Date(2026, 10, 12, 23, 59, 59, 0, time.FixedZone("CST", 8*3600))
|
||||
for _, sequence := range []int64{-1, 0, 1000} {
|
||||
if _, err := formatBatchNo("REN", now, sequence); err == nil {
|
||||
t.Fatalf("sequence %d should be rejected", sequence)
|
||||
}
|
||||
}
|
||||
for _, sequence := range []int64{1, 999} {
|
||||
number, err := formatBatchNo("REN", now, sequence)
|
||||
if err != nil || len(number) != 20 {
|
||||
t.Fatalf("sequence %d: number=%q err=%v", sequence, number, err)
|
||||
}
|
||||
}
|
||||
if batchNoSequenceKey(now) != "iotlcm:batch:no:seq:20261012" || batchNoSequenceKey(now.Add(time.Second)) != "iotlcm:batch:no:seq:20261013" {
|
||||
t.Fatal("daily sequence key does not roll over at midnight")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextBatchNoRejectsInvalidPrefixBeforeRedis(t *testing.T) {
|
||||
for _, prefix := range []string{"", " ", "RE", "RENEW"} {
|
||||
for _, ctx := range []context.Context{nil, context.Background()} {
|
||||
number, err := NextBatchNoContext(ctx, prefix)
|
||||
if err == nil || number != "" {
|
||||
t.Fatalf("prefix %q: number=%q err=%v", prefix, number, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package batchmetadata
|
||||
|
||||
const (
|
||||
API = "api"
|
||||
Unknown = 0
|
||||
Single = 1
|
||||
Bulk = 2
|
||||
)
|
||||
|
||||
func ValidMode(mode int) bool { return mode >= Unknown && mode <= Bulk }
|
||||
@@ -0,0 +1,6 @@
|
||||
// Package bcache
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package bcache
|
||||
@@ -0,0 +1,115 @@
|
||||
package bcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/cache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/mcache"
|
||||
"time"
|
||||
)
|
||||
|
||||
const apiAuthLocalBackfillTTL = 6 * time.Hour
|
||||
|
||||
// SetApiAuthToken 存储接口认证长令牌,先写本地内存缓存,再写共享缓存。
|
||||
func SetApiAuthToken(ctx context.Context, uuid string, token string, timeout time.Duration) (err error) {
|
||||
if uuid == "" {
|
||||
return errors.New("api auth token uuid invalid")
|
||||
}
|
||||
key := fmt.Sprintf(consts.KeyApiAuthToken, uuid)
|
||||
if err = mcache.Set(ctx, key, token, timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cache.Instance().Set(ctx, key, token, timeout); err != nil {
|
||||
_ = mcache.Remove(ctx, key)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetApiAuthToken 获取接口认证长令牌,优先读取本地内存缓存。
|
||||
func GetApiAuthToken(ctx context.Context, uuid string) (token string, err error) {
|
||||
if uuid == "" {
|
||||
return "", errors.New("api auth token uuid invalid")
|
||||
}
|
||||
key := fmt.Sprintf(consts.KeyApiAuthToken, uuid)
|
||||
t, err := mcache.Instance().Get(ctx, key)
|
||||
if err == nil && !t.IsNil() {
|
||||
return t.String(), nil
|
||||
}
|
||||
|
||||
t, err = cache.Instance().Get(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !t.IsNil() {
|
||||
_ = mcache.Set(ctx, key, t.String(), apiAuthLocalBackfillTTL)
|
||||
return t.String(), nil
|
||||
}
|
||||
return "", errors.New("api auth token not found")
|
||||
}
|
||||
|
||||
// DelApiAuthToken 删除接口认证长令牌,先删本地内存缓存,再删共享缓存。
|
||||
func DelApiAuthToken(ctx context.Context, uuid string) (err error) {
|
||||
if uuid == "" {
|
||||
return errors.New("api auth token uuid invalid")
|
||||
}
|
||||
key := fmt.Sprintf(consts.KeyApiAuthToken, uuid)
|
||||
if err = mcache.Remove(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cache.Instance().Remove(ctx, key)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetApiAuthActiveToken 设置机构维度的当前有效令牌索引。
|
||||
func SetApiAuthActiveToken(ctx context.Context, deptId int64, uuid string, timeout time.Duration) (err error) {
|
||||
if deptId <= 0 || uuid == "" {
|
||||
return errors.New("api auth active token key invalid")
|
||||
}
|
||||
key := fmt.Sprintf(consts.KeyApiAuthActiveToken, deptId)
|
||||
if err = mcache.Set(ctx, key, uuid, timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cache.Instance().Set(ctx, key, uuid, timeout); err != nil {
|
||||
_ = mcache.Remove(ctx, key)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetApiAuthActiveToken 获取机构维度的当前有效令牌索引。
|
||||
func GetApiAuthActiveToken(ctx context.Context, deptId int64) (uuid string, err error) {
|
||||
if deptId <= 0 {
|
||||
return "", errors.New("api auth active token key invalid")
|
||||
}
|
||||
key := fmt.Sprintf(consts.KeyApiAuthActiveToken, deptId)
|
||||
t, err := mcache.Instance().Get(ctx, key)
|
||||
if err == nil && !t.IsNil() {
|
||||
return t.String(), nil
|
||||
}
|
||||
|
||||
t, err = cache.Instance().Get(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !t.IsNil() {
|
||||
_ = mcache.Set(ctx, key, t.String(), apiAuthLocalBackfillTTL)
|
||||
return t.String(), nil
|
||||
}
|
||||
return "", errors.New("api auth active token not found")
|
||||
}
|
||||
|
||||
// DelApiAuthActiveToken 删除机构维度的当前有效令牌索引。
|
||||
func DelApiAuthActiveToken(ctx context.Context, deptId int64) (err error) {
|
||||
if deptId <= 0 {
|
||||
return errors.New("api auth active token key invalid")
|
||||
}
|
||||
key := fmt.Sprintf(consts.KeyApiAuthActiveToken, deptId)
|
||||
if err = mcache.Remove(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cache.Instance().Remove(ctx, key)
|
||||
return err
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
// Package cache
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/cache/file"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
)
|
||||
|
||||
// cache 缓存驱动
|
||||
var cacheVal *gcache.Cache
|
||||
|
||||
// Instance 缓存实例
|
||||
func Instance() *gcache.Cache {
|
||||
if cacheVal == nil {
|
||||
panic("cache uninitialized.")
|
||||
}
|
||||
return cacheVal
|
||||
}
|
||||
|
||||
// SetAdapter 设置缓存适配器
|
||||
func SetAdapter(ctx context.Context) {
|
||||
var cacheAdapter gcache.Adapter
|
||||
adapter := g.Cfg().MustGet(ctx, "cache.adapter").String()
|
||||
fileDir := g.Cfg().MustGet(ctx, "cache.fileDir").String()
|
||||
|
||||
switch adapter {
|
||||
case "redis":
|
||||
cacheAdapter = gcache.NewAdapterRedis(g.Redis())
|
||||
case "file":
|
||||
if fileDir == "" {
|
||||
g.Log().Fatal(ctx, "file path must be configured for file caching.")
|
||||
return
|
||||
}
|
||||
|
||||
if !gfile.Exists(fileDir) {
|
||||
if err := gfile.Mkdir(fileDir); err != nil {
|
||||
g.Log().Fatalf(ctx, "failed to create the cache directory. procedure, err:%+v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
cacheAdapter = file.NewAdapterFile(fileDir)
|
||||
default:
|
||||
cacheAdapter = gcache.NewAdapterMemory()
|
||||
}
|
||||
|
||||
// 数据库缓存,默认和通用缓冲驱动一致,如果你不想使用默认的,可以自行调整
|
||||
g.DB().GetCache().SetAdapter(cacheAdapter)
|
||||
|
||||
// 通用缓存
|
||||
cacheVal = gcache.New()
|
||||
cacheVal.SetAdapter(cacheAdapter)
|
||||
}
|
||||
Vendored
+440
@@ -0,0 +1,440 @@
|
||||
// Package file
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type (
|
||||
// AdapterFile gcache适配器使用文件的实现
|
||||
AdapterFile struct {
|
||||
dir string
|
||||
mutex sync.RWMutex // 添加互斥锁以支持并发操作
|
||||
}
|
||||
|
||||
fileContent struct {
|
||||
Duration int64 `json:"duration"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
const perm = 0o666 //设置文件所有者、同用户组成员和其他用户都可以读写该文件,但都不能执。
|
||||
|
||||
var (
|
||||
CacheExpiredErr = errors.New("cache expired")
|
||||
)
|
||||
|
||||
// NewAdapterFile creates and returns a new memory cache object.
|
||||
func NewAdapterFile(dir string) gcache.Adapter {
|
||||
return &AdapterFile{
|
||||
dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, lifeTime time.Duration) (err error) {
|
||||
c.mutex.Lock() // 加锁
|
||||
defer c.mutex.Unlock() // 解锁
|
||||
fileKey := gconv.String(key)
|
||||
if value == nil || lifeTime < 0 {
|
||||
return c.Delete(fileKey)
|
||||
}
|
||||
return c.Save(fileKey, gconv.String(value), lifeTime)
|
||||
}
|
||||
|
||||
// SetMap 批量设置多个键值对,并且每个键值对的有效期是一样的
|
||||
func (c *AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) (err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
for key, value := range data {
|
||||
fileKey := gconv.String(key)
|
||||
if err = c.Save(fileKey, gconv.String(value), duration); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetIfNotExist 在指定的键不存在时设置其值
|
||||
func (c *AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
if c.Has(fileKey) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = c.Save(fileKey, gconv.String(value), duration)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// SetIfNotExistFunc 在指定的键不存在时,通过一个函数来生成值并设置它
|
||||
func (c *AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
if c.Has(fileKey) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
value, err := f(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = c.Save(fileKey, gconv.String(value), duration)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// SetIfNotExistFuncLock 与 SetIfNotExistFunc 类似,但它在调用生成值的函数时提供了额外的锁机制,以避免在生成值期间的并发问题。
|
||||
func (c *AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
if c.Has(fileKey) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 在这里加锁是为了确保在值生成期间不会有并发的写入操作
|
||||
value, err := f(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = c.Save(fileKey, gconv.String(value), duration)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, error) {
|
||||
fetch, err := c.Fetch(gconv.String(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gvar.New(fetch), nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
|
||||
result, err = c.Get(ctx, key)
|
||||
if err != nil && !errors.Is(err, CacheExpiredErr) {
|
||||
return nil, err
|
||||
}
|
||||
if result.IsNil() {
|
||||
return gvar.New(value), c.Set(ctx, key, value, duration)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
|
||||
v, err := c.Get(ctx, key)
|
||||
if err != nil && !errors.Is(err, CacheExpiredErr) {
|
||||
return nil, err
|
||||
}
|
||||
if v.IsNil() {
|
||||
value, err := f(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return gvar.New(value), c.Set(ctx, key, value, duration)
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
|
||||
return c.GetOrSetFunc(ctx, key, f, duration)
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) {
|
||||
return c.Has(gconv.String(key)), nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Size(ctx context.Context) (size int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Data 获取所有缓存的键值对
|
||||
func (c *AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
// 初始化返回的数据结构
|
||||
data = make(map[interface{}]interface{})
|
||||
|
||||
// 读取文件目录中的所有缓存文件
|
||||
files, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 遍历缓存文件,提取数据
|
||||
for _, file := range files {
|
||||
content, err := c.read(file.Name())
|
||||
if err != nil {
|
||||
continue // 忽略无法读取的文件
|
||||
}
|
||||
data[file.Name()] = content.Data
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Keys 获取所有缓存的键
|
||||
func (c *AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
files, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys = make([]interface{}, 0, len(files))
|
||||
for _, file := range files {
|
||||
keys = append(keys, file.Name())
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// Values 获取所有缓存的值
|
||||
func (c *AdapterFile) Values(ctx context.Context) (values []interface{}, err error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
files, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
values = make([]interface{}, 0, len(files))
|
||||
for _, file := range files {
|
||||
content, err := c.read(file.Name())
|
||||
if err != nil {
|
||||
continue // 忽略无法读取的文件
|
||||
}
|
||||
values = append(values, content.Data)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Update 更新一个指定键的缓存值,如果这个键不存在,则返回错误
|
||||
func (c *AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
content, err := c.read(fileKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if content == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
oldValue = gvar.New(content.Data)
|
||||
err = c.Save(fileKey, gconv.String(value), time.Duration(content.Duration)*time.Second)
|
||||
return oldValue, true, err
|
||||
}
|
||||
|
||||
func (c *AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
|
||||
var (
|
||||
v *gvar.Var
|
||||
oldTTL int64
|
||||
fileKey = gconv.String(key)
|
||||
)
|
||||
// TTL.
|
||||
expire, err := c.GetExpire(ctx, fileKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
oldTTL = int64(expire)
|
||||
if oldTTL == -2 {
|
||||
// It does not exist.
|
||||
return
|
||||
}
|
||||
oldDuration = time.Duration(oldTTL) * time.Second
|
||||
// DEL.
|
||||
if duration < 0 {
|
||||
err = c.Delete(fileKey)
|
||||
return
|
||||
}
|
||||
v, err = c.Get(ctx, fileKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Set(ctx, fileKey, v.Val(), duration)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
|
||||
content, err := c.read(gconv.String(key))
|
||||
if err != nil {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
if content.Duration <= time.Now().Unix() {
|
||||
return -1, nil
|
||||
}
|
||||
return time.Duration(time.Now().Unix()-content.Duration) * time.Second, nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Retrieves the last key value.
|
||||
if lastValue, err = c.Get(ctx, gconv.String(keys[len(keys)-1])); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Deletes all given keys.
|
||||
err = c.DeleteMulti(gconv.Strings(keys)...)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Clear(ctx context.Context) error {
|
||||
return c.Flush()
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) createName(key string) string {
|
||||
h := sha256.New()
|
||||
_, _ = h.Write([]byte(key))
|
||||
hash := hex.EncodeToString(h.Sum(nil))
|
||||
return filepath.Join(c.dir, fmt.Sprintf("%s.cache", hash))
|
||||
}
|
||||
|
||||
func (c *AdapterFile) read(key string) (*fileContent, error) {
|
||||
rp := gfile.RealPath(c.createName(key))
|
||||
if rp == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value, err := os.ReadFile(rp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := &fileContent{}
|
||||
if err := json.Unmarshal(value, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content.Duration == 0 {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
if content.Duration <= time.Now().Unix() {
|
||||
_ = c.Delete(key)
|
||||
return nil, CacheExpiredErr
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Has checks if the cached key exists into the File storage
|
||||
func (c *AdapterFile) Has(key string) bool {
|
||||
fc, err := c.read(key)
|
||||
return err == nil && fc != nil
|
||||
}
|
||||
|
||||
// Delete the cached key from File storage
|
||||
func (c *AdapterFile) Delete(key string) error {
|
||||
_, err := os.Stat(c.createName(key))
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(c.createName(key))
|
||||
}
|
||||
|
||||
// DeleteMulti the cached key from File storage
|
||||
func (c *AdapterFile) DeleteMulti(keys ...string) (err error) {
|
||||
for _, key := range keys {
|
||||
if err = c.Delete(key); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch retrieves the cached value from key of the File storage
|
||||
func (c *AdapterFile) Fetch(key string) (interface{}, error) {
|
||||
content, err := c.read(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return content.Data, nil
|
||||
}
|
||||
|
||||
// FetchMulti retrieve multiple cached values from keys of the File storage
|
||||
func (c *AdapterFile) FetchMulti(keys []string) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, key := range keys {
|
||||
if value, err := c.Fetch(key); err == nil {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Flush removes all cached keys of the File storage
|
||||
func (c *AdapterFile) Flush() error {
|
||||
dir, err := os.Open(c.dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = dir.Close()
|
||||
}()
|
||||
|
||||
names, _ := dir.Readdirnames(-1)
|
||||
|
||||
for _, name := range names {
|
||||
_ = os.Remove(filepath.Join(c.dir, name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save a value in File storage by key
|
||||
func (c *AdapterFile) Save(key string, value string, lifeTime time.Duration) error {
|
||||
duration := int64(0)
|
||||
|
||||
if lifeTime > 0 {
|
||||
duration = time.Now().Unix() + int64(lifeTime.Seconds())
|
||||
}
|
||||
|
||||
content := &fileContent{duration, value}
|
||||
|
||||
data, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(c.createName(key), data, perm)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// Package contexts
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package contexts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
"slices"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type ContextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var contextKey = &ContextKey{
|
||||
name: consts.ContextKey,
|
||||
}
|
||||
|
||||
// Init初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改。
|
||||
func Init(r *ghttp.Request, customCtx *model.Context) {
|
||||
r.SetCtxVar(contextKey, customCtx)
|
||||
}
|
||||
|
||||
// Set设置本地上下文
|
||||
func Set(ctx context.Context, customCtx *model.Context) context.Context {
|
||||
ctx = context.WithValue(ctx, contextKey, customCtx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Get 获取上下文变量;
|
||||
func Get(ctx context.Context) *model.Context {
|
||||
customCtx, ok := ctx.Value(contextKey).(*model.Context)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return customCtx
|
||||
}
|
||||
|
||||
// Set 设置上下文变量
|
||||
func SetUser(ctx context.Context, user *model.Identity) {
|
||||
customCtx := Get(ctx)
|
||||
customCtx.User = user
|
||||
}
|
||||
|
||||
// GetUser 获取上下文变量中的用户信息
|
||||
func GetUser(ctx context.Context) *model.Identity {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return nil
|
||||
}
|
||||
return customCtx.User
|
||||
}
|
||||
|
||||
// GetUserId 获取上下文变量中的用户ID
|
||||
func GetUserId(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.ID
|
||||
}
|
||||
|
||||
// GetUserUuid 获取上下文变量中的用户UUID
|
||||
func GetUserUuid(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.UUID
|
||||
}
|
||||
|
||||
// GetUserName 获取上下文变量中的用户名
|
||||
func GetUserName(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
// GetNickName 获取上下文变量中的用户昵称
|
||||
func GetNickName(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.NickName
|
||||
}
|
||||
|
||||
// GetDeptId 获取上下文变量中的部门ID
|
||||
func GetDeptId(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
g.Log().Debug(ctx, user.DeptId)
|
||||
return user.DeptId
|
||||
}
|
||||
|
||||
// GetTenantId 获取上下文变量中的租户ID
|
||||
func GetTenantId(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.TenantId
|
||||
}
|
||||
|
||||
// GetRoles 获取上下文变量中的角色列表
|
||||
func GetRoles(ctx context.Context) []model.Role {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return user.Roles
|
||||
}
|
||||
|
||||
// GetLoginAt 获取上下文变量中的登录时间
|
||||
func GetLoginAt(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.LoginAt
|
||||
}
|
||||
|
||||
// IsSystemTenant 判断是否是系统租户
|
||||
func IsSystemTenant(ctx context.Context) bool {
|
||||
tenantId := GetTenantId(ctx)
|
||||
return tenantId == consts.DefaultSystemTenantCode
|
||||
}
|
||||
|
||||
// IsSuperAdmin 判断是否是超级管理员角色
|
||||
func IsSuperAdmin(ctx context.Context) bool {
|
||||
roles := GetRoles(ctx)
|
||||
return slices.ContainsFunc(roles, func(role model.Role) bool {
|
||||
return role.RoleId == consts.SuperAdminRoleId
|
||||
})
|
||||
}
|
||||
func IsWebSocket(ctx context.Context) bool {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return false
|
||||
}
|
||||
return customCtx.IsWebSocket
|
||||
}
|
||||
func SetIsWebSocket(ctx context.Context, isWebSocket bool) {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return
|
||||
}
|
||||
customCtx.IsWebSocket = isWebSocket
|
||||
g.Log().Debugf(ctx, "SetIsWebSocket: %v", isWebSocket)
|
||||
}
|
||||
|
||||
// GetAccessCodeList 获取上下文变量中的访问码列表
|
||||
func GetAccessCodeList(ctx context.Context) []string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
if len(user.AccessCodeList) == 0 {
|
||||
return nil
|
||||
}
|
||||
return user.AccessCodeList
|
||||
}
|
||||
|
||||
func GetDataValue(ctx context.Context, key string) interface{} {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return nil
|
||||
}
|
||||
if customCtx.Data == nil {
|
||||
return nil
|
||||
}
|
||||
value, ok := customCtx.Data[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func SetDataValue(ctx context.Context, key string, value interface{}) {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return
|
||||
}
|
||||
if customCtx.Data == nil {
|
||||
customCtx.Data = make(g.Map)
|
||||
}
|
||||
customCtx.Data[key] = value
|
||||
}
|
||||
|
||||
func GetAgentUser(ctx context.Context) *model.BaseClaimsApi {
|
||||
customCtx := Get(ctx)
|
||||
g.Log().Debug(ctx, customCtx)
|
||||
if customCtx == nil {
|
||||
return nil
|
||||
}
|
||||
return customCtx.AgentUser
|
||||
}
|
||||
|
||||
func GetAgentDeptId(ctx context.Context) int64 {
|
||||
agentUser := GetAgentUser(ctx)
|
||||
if agentUser == nil {
|
||||
return 0
|
||||
}
|
||||
return agentUser.DeptId
|
||||
}
|
||||
|
||||
func GetDeptKey(ctx context.Context) string {
|
||||
agentUser := GetAgentUser(ctx)
|
||||
if agentUser == nil {
|
||||
return ""
|
||||
}
|
||||
return agentUser.DeptKey
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Package mcache
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// 内存缓存管理
|
||||
package mcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
)
|
||||
|
||||
// cache 缓存驱动
|
||||
var (
|
||||
mcache *gcache.Cache
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// Instance 缓存实例
|
||||
func Instance() *gcache.Cache {
|
||||
if mcache == nil {
|
||||
mu.Lock()
|
||||
if mcache == nil {
|
||||
mcache = gcache.NewWithAdapter(gcache.NewAdapterMemory())
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
return mcache
|
||||
}
|
||||
|
||||
func Set(ctx context.Context, key string, value interface{}, timeout time.Duration) error {
|
||||
return Instance().Set(ctx, key, value, timeout)
|
||||
}
|
||||
|
||||
func Get(ctx context.Context, key string) (interface{}, error) {
|
||||
return Instance().Get(ctx, key)
|
||||
}
|
||||
|
||||
func Remove(ctx context.Context, key string) error {
|
||||
_, err := Instance().Remove(ctx, key)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
// Config 保存 RabbitMQ 最小连接配置。
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Dsn string
|
||||
}
|
||||
|
||||
// Handler 是原始字节消息的消费回调。
|
||||
type Handler func(ctx context.Context, body []byte) error
|
||||
|
||||
// JSONHandler 是 JSON 消息的消费回调,框架会先反序列化再交给业务。
|
||||
type JSONHandler[T any] func(ctx context.Context, message T) error
|
||||
|
||||
var (
|
||||
conn *amqp.Connection
|
||||
connMu sync.Mutex
|
||||
closers []func() error
|
||||
closerMu sync.Mutex
|
||||
)
|
||||
|
||||
// GetConfig 从配置中心读取 RabbitMQ 开关和 DSN。
|
||||
func GetConfig(ctx context.Context) Config {
|
||||
return Config{
|
||||
Enabled: g.Cfg().MustGet(ctx, "rabbitmq.enabled", false).Bool(),
|
||||
Dsn: g.Cfg().MustGet(ctx, "rabbitmq.dsn", "").String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled 判断当前环境是否启用了 RabbitMQ。
|
||||
func Enabled(ctx context.Context) bool {
|
||||
cfg := GetConfig(ctx)
|
||||
return cfg.Enabled && cfg.Dsn != ""
|
||||
}
|
||||
|
||||
// Conn 返回共享连接,首次调用时懒加载创建。
|
||||
func Conn(ctx context.Context) (*amqp.Connection, error) {
|
||||
cfg := GetConfig(ctx)
|
||||
if !cfg.Enabled {
|
||||
return nil, gerror.New("rabbitmq is disabled")
|
||||
}
|
||||
if cfg.Dsn == "" {
|
||||
return nil, gerror.New("rabbitmq dsn is empty")
|
||||
}
|
||||
|
||||
connMu.Lock()
|
||||
defer connMu.Unlock()
|
||||
if conn != nil && !conn.IsClosed() {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
c, err := amqp.Dial(cfg.Dsn)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "rabbitmq connect failed")
|
||||
}
|
||||
conn = c
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Channel 基于共享连接创建独立 channel。
|
||||
func Channel(ctx context.Context) (*amqp.Channel, error) {
|
||||
c, err := Conn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch, err := c.Channel()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "rabbitmq open channel failed")
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// DeclareQueue 预声明一个持久化队列。
|
||||
func DeclareQueue(ctx context.Context, queue string) error {
|
||||
ch, err := Channel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ch.Close()
|
||||
return declareQueue(ch, queue)
|
||||
}
|
||||
|
||||
// Publish 将结构体序列化为 JSON 后发送到普通队列。
|
||||
func Publish(ctx context.Context, queue string, data any) error {
|
||||
body, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "rabbitmq marshal message failed")
|
||||
}
|
||||
return PublishBytes(ctx, queue, body)
|
||||
}
|
||||
|
||||
// PublishBytes 将原始消息体发送到普通队列。
|
||||
func PublishBytes(ctx context.Context, queue string, body []byte) error {
|
||||
ch, err := Channel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ch.Close()
|
||||
|
||||
if err = declareQueue(ch, queue); err != nil {
|
||||
return err
|
||||
}
|
||||
return ch.PublishWithContext(ctx, "", queue, false, false, amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
DeliveryMode: amqp.Persistent,
|
||||
Timestamp: time.Now(),
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
// Consume 启动普通消费者,业务成功后才 Ack。
|
||||
func Consume(ctx context.Context, queue, consumer string, handler Handler) error {
|
||||
ch, err := Channel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = declareQueue(ch, queue); err != nil {
|
||||
_ = ch.Close()
|
||||
return err
|
||||
}
|
||||
if err = ch.Qos(1, 0, false); err != nil {
|
||||
_ = ch.Close()
|
||||
return gerror.Wrap(err, "rabbitmq set qos failed")
|
||||
}
|
||||
|
||||
deliveries, err := ch.Consume(queue, consumer, false, false, false, false, nil)
|
||||
if err != nil {
|
||||
_ = ch.Close()
|
||||
return gerror.Wrap(err, "rabbitmq consume failed")
|
||||
}
|
||||
|
||||
registerCloser(ch.Close)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = ch.Close()
|
||||
return
|
||||
case delivery, ok := <-deliveries:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err = handler(ctx, delivery.Body); err != nil {
|
||||
g.Log().Errorf(ctx, "rabbitmq handle message failed, queue:%s, err:%+v", queue, err)
|
||||
_ = delivery.Nack(false, false)
|
||||
continue
|
||||
}
|
||||
_ = delivery.Ack(false)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConsumeJSON 启动 JSON 消费者,自动完成反序列化。
|
||||
func ConsumeJSON[T any](ctx context.Context, queue, consumer string, handler JSONHandler[T]) error {
|
||||
return Consume(ctx, queue, consumer, func(ctx context.Context, body []byte) error {
|
||||
var message T
|
||||
if err := json.Unmarshal(body, &message); err != nil {
|
||||
g.Log().Errorf(ctx, "rabbitmq message dropped, invalid json, queue:%s, body:%s, err:%+v", queue, string(body), err)
|
||||
return nil
|
||||
}
|
||||
return handler(ctx, message)
|
||||
})
|
||||
}
|
||||
|
||||
// Close 关闭所有注册的 channel 和共享连接。
|
||||
func Close() error {
|
||||
closerMu.Lock()
|
||||
for _, closer := range closers {
|
||||
_ = closer()
|
||||
}
|
||||
closers = nil
|
||||
closerMu.Unlock()
|
||||
|
||||
connMu.Lock()
|
||||
defer connMu.Unlock()
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
err := conn.Close()
|
||||
conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// declareQueue 声明持久化普通队列。
|
||||
func declareQueue(ch *amqp.Channel, queue string) error {
|
||||
_, err := ch.QueueDeclare(queue, true, false, false, false, nil)
|
||||
if err != nil {
|
||||
return gerror.Wrapf(err, "rabbitmq declare queue failed, queue:%s", queue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerCloser 记录 channel 关闭函数,供服务退出时统一清理。
|
||||
func registerCloser(closer func() error) {
|
||||
closerMu.Lock()
|
||||
defer closerMu.Unlock()
|
||||
closers = append(closers, closer)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseDuration(d string) (time.Duration, error) {
|
||||
d = strings.TrimSpace(d)
|
||||
dr, err := time.ParseDuration(d)
|
||||
if err == nil {
|
||||
return dr, nil
|
||||
}
|
||||
if strings.Contains(d, "d") {
|
||||
index := strings.Index(d, "d")
|
||||
day, _ := strconv.Atoi(d[:index])
|
||||
dr = time.Hour * 24 * time.Duration(day)
|
||||
ndr, err := time.ParseDuration(d[index+1:])
|
||||
if err != nil {
|
||||
return dr, nil
|
||||
}
|
||||
return dr + ndr, nil
|
||||
}
|
||||
dv, err := strconv.ParseInt(d, 10, 64)
|
||||
return time.Duration(dv), err
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// package xgorm
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package xgorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/utility"
|
||||
"slices"
|
||||
|
||||
"reflect"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
var (
|
||||
descTags = []string{"description", "dc", "json"} // 实体描述标签
|
||||
fieldTags = []string{"json"} // 实体字段名称映射
|
||||
)
|
||||
|
||||
// GetFieldsToSlice 获取dao实例中的所有字段
|
||||
func GetFieldsToSlice(ctx context.Context, dao daoInstance) ([]string, error) {
|
||||
fields, err := dao.Ctx(ctx).TableFields(dao.Table())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil, gerror.New("field not found")
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for _, field := range fields {
|
||||
keys = append(keys, field.Name)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// GetPkField 获取dao实例中的主键名称
|
||||
func GetPkField(ctx context.Context, dao daoInstance) (string, error) {
|
||||
fields, err := dao.Ctx(ctx).TableFields(dao.Table())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return "", gerror.New("field not found")
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
if field.Key == "PRI" {
|
||||
return field.Name, nil
|
||||
}
|
||||
}
|
||||
return "", gerror.New("no primary key")
|
||||
}
|
||||
|
||||
// IsUnique 是否唯一
|
||||
func IsUnique(ctx context.Context, dao daoInstance, where g.Map, message string, pkId ...interface{}) error {
|
||||
if len(where) == 0 {
|
||||
return gerror.New("where condition cannot be empty")
|
||||
}
|
||||
|
||||
m := dao.Ctx(ctx).Where(where)
|
||||
if len(pkId) > 0 {
|
||||
field, err := GetPkField(ctx, dao)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m = m.WhereNot(field, pkId[0])
|
||||
}
|
||||
|
||||
count, err := m.Count(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
if message == "" {
|
||||
for k := range where {
|
||||
message = fmt.Sprintf("in the table:%s, %v not uniqued", dao.Table(), where[k])
|
||||
break
|
||||
}
|
||||
}
|
||||
return gerror.New(message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func JoinFields(ctx context.Context, entity interface{}, dao daoInstance, as string) (fs string) {
|
||||
entityFs, err := GetEntityFieldTags(entity)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "JoinFields err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(entityFs) == 0 {
|
||||
g.Log().Errorf(ctx, "JoinFields entityFs is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fields, err := dao.Ctx(ctx).TableFields(dao.Table())
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "JoinFields err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var columns []string
|
||||
for _, v := range entityFs {
|
||||
if !gstr.HasPrefix(v, as) {
|
||||
g.Log().Infof(ctx, "JoinFields gstr.HasPrefix: %+v, %+v", v, as)
|
||||
continue
|
||||
}
|
||||
g.Log().Infof(ctx, "JoinFields gstr.HasPrefix2: %+v, %+v", v, as)
|
||||
field := gstr.CaseSnakeFirstUpper(gstr.StrEx(v, as))
|
||||
if _, ok := fields[field]; ok {
|
||||
columns = append(columns, fmt.Sprintf("`%s`.`%s` as `%s`", dao.Table(), field, v))
|
||||
}
|
||||
g.Log().Infof(ctx, "JoinFields field: %+v fields: %+v", field, fields)
|
||||
}
|
||||
|
||||
if len(columns) > 0 {
|
||||
g.Log().Infof(ctx, "JoinFields columns: %+v", columns)
|
||||
return gstr.Implode(",", utility.UniqueSlice(columns))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetEntityFieldTags 获取实体中的字段名称
|
||||
func GetEntityFieldTags(entity interface{}) (tags []string, err error) {
|
||||
var formRef = reflect.TypeOf(entity)
|
||||
for i := 0; i < formRef.NumField(); i++ {
|
||||
field := formRef.Field(i)
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
addTags, err := reflectTag(field.Type, fieldTags, []string{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, addTags...)
|
||||
continue
|
||||
}
|
||||
tags = append(tags, reflectTagName(field, fieldTags, true))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// reflectTag 层级递增解析tag
|
||||
func reflectTag(reflectType reflect.Type, filterTags []string, tags []string) ([]string, error) {
|
||||
if reflectType.Kind() == reflect.Ptr {
|
||||
return nil, gerror.Newf("reflect type do not support reflect.Ptr yet, reflectType:%+v", reflectType)
|
||||
}
|
||||
if reflectType.Kind() != reflect.Struct {
|
||||
return nil, nil
|
||||
}
|
||||
for i := 0; i < reflectType.NumField(); i++ {
|
||||
tag := reflectTagName(reflectType.Field(i), filterTags, false)
|
||||
if tag == "" {
|
||||
addTags, err := reflectTag(reflectType.Field(i).Type, filterTags, tags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, addTags...)
|
||||
continue
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// reflectTagName 解析实体中的描述标签,优先级:description > dc > json > Name
|
||||
func reflectTagName(field reflect.StructField, filterTags []string, isDef bool) string {
|
||||
if slices.Contains(filterTags, "description") {
|
||||
if description, ok := field.Tag.Lookup("description"); ok && description != "" {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(filterTags, "dc") {
|
||||
if dc, ok := field.Tag.Lookup("dc"); ok && dc != "" {
|
||||
return dc
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(filterTags, "json") {
|
||||
if jsonName, ok := field.Tag.Lookup("json"); ok && jsonName != "" {
|
||||
return jsonName
|
||||
}
|
||||
}
|
||||
|
||||
if !isDef {
|
||||
return ""
|
||||
}
|
||||
return field.Name
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// package xgorm
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package xgorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
gormmodel "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/xgorm/gorm_model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/utility/tree"
|
||||
"slices"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
idDefaulField = "id"
|
||||
pidDefaulField = "pid"
|
||||
levelDefaulField = "level"
|
||||
treeDefaulField = "tree"
|
||||
)
|
||||
|
||||
type TreeFiledOption struct {
|
||||
IdField string
|
||||
PidField string
|
||||
LevelField string
|
||||
TreeField string
|
||||
}
|
||||
|
||||
type daoInstance interface {
|
||||
Table() string
|
||||
Ctx(ctx context.Context) *gdb.Model
|
||||
}
|
||||
|
||||
func CustomTreeFiledToDefault(old map[string]interface{}, option *TreeFiledOption) *gormmodel.DefaultTree {
|
||||
if option == nil {
|
||||
option = &TreeFiledOption{
|
||||
IdField: idDefaulField,
|
||||
PidField: pidDefaulField,
|
||||
LevelField: levelDefaulField,
|
||||
TreeField: treeDefaulField,
|
||||
}
|
||||
}
|
||||
pd := &gormmodel.DefaultTree{}
|
||||
if id, ok := old[option.IdField]; ok {
|
||||
pd.Id = gconv.Int64(id)
|
||||
}
|
||||
if pid, ok := old[option.PidField]; ok {
|
||||
pd.Pid = gconv.Int64(pid)
|
||||
}
|
||||
if level, ok := old[option.LevelField]; ok {
|
||||
pd.Level = gconv.Int(level)
|
||||
}
|
||||
if tree, ok := old[option.TreeField]; ok {
|
||||
pd.Tree = gconv.String(tree)
|
||||
}
|
||||
return pd
|
||||
}
|
||||
|
||||
// AutoUpdateTree 自动更新关系树
|
||||
func AutoUpdateTree(ctx context.Context, dao daoInstance, id, pid int64, option *TreeFiledOption) (newPid int64, newLevel int, newTree string, err error) {
|
||||
if option == nil {
|
||||
option = &TreeFiledOption{
|
||||
IdField: idDefaulField,
|
||||
PidField: pidDefaulField,
|
||||
LevelField: levelDefaulField,
|
||||
TreeField: treeDefaulField,
|
||||
}
|
||||
}
|
||||
if err = CheckTreeTable(ctx, dao, option); err != nil {
|
||||
return
|
||||
}
|
||||
if pid <= 0 {
|
||||
newPid = 0
|
||||
newLevel = 1
|
||||
newTree = ""
|
||||
} else {
|
||||
var pdMap map[string]interface{}
|
||||
row, err := dao.Ctx(ctx).Fields(option.IdField, option.PidField, option.LevelField, option.TreeField).WherePri(pid).One()
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
if row != nil {
|
||||
pdMap = row.Map()
|
||||
}
|
||||
|
||||
if len(pdMap) == 0 {
|
||||
return 0, 0, "", gerror.New("未查询到树表上级信息,请检查!")
|
||||
}
|
||||
pd := CustomTreeFiledToDefault(pdMap, option)
|
||||
|
||||
if id > 0 && slices.Contains(tree.GetIds(pd.Tree), id) {
|
||||
return 0, 0, "", gerror.New("上级不能设为自己当前的子级!")
|
||||
}
|
||||
|
||||
newPid = pid
|
||||
newLevel = pd.Level + 1
|
||||
newTree = tree.GenLabel(pd.Tree, pid)
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
if pid == id {
|
||||
return 0, 0, "", gerror.New("上级不能是自己!")
|
||||
}
|
||||
|
||||
var models *gormmodel.DefaultTree
|
||||
if err = dao.Ctx(ctx).WherePri(id).Scan(&models); err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
|
||||
if models == nil {
|
||||
return 0, 0, "", gerror.New("树表信息不存在,请检查!")
|
||||
}
|
||||
// g.Log().Infof(ctx, "models: %+v, pid: %v, newPid: %v, newLevel: %v, newTree: %v", models, pid, newPid, newLevel, newTree)
|
||||
// 上级发生变化时,遍历修改其所有的下级关系树
|
||||
if models.Pid != pid {
|
||||
if err = updateChildrenTree(ctx, dao, models.Id, newLevel, newTree, option); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CheckTreeTable 检查树表
|
||||
func CheckTreeTable(ctx context.Context, dao daoInstance, option *TreeFiledOption) (err error) {
|
||||
fields, err := GetFieldsToSlice(ctx, dao)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !slices.Contains(fields, option.PidField) {
|
||||
return gerror.New("树表必须包含`pid`字段")
|
||||
}
|
||||
|
||||
if !slices.Contains(fields, option.LevelField) {
|
||||
return gerror.New("树表必须包含`level`字段")
|
||||
}
|
||||
|
||||
if !slices.Contains(fields, option.TreeField) {
|
||||
return gerror.New("树表必须包含`tree`字段")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// updateChildrenTree 更新下级关系树
|
||||
func updateChildrenTree(ctx context.Context, dao daoInstance, pid int64, pLevel int, pTree string, option *TreeFiledOption) (err error) {
|
||||
var list []*gormmodel.DefaultTree
|
||||
// var pdMaps []map[string]interface{}
|
||||
pdMaps, err := dao.Ctx(ctx).Fields(option.IdField, option.PidField, option.LevelField, option.TreeField).Where(option.PidField, pid).All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range pdMaps {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
pb := CustomTreeFiledToDefault(v.Map(), option)
|
||||
if pb.Id == 0 {
|
||||
g.Log().Warningf(ctx, "updateChildrenTree pb.Id is 0, pb: %+v, option: %+v, v: %+v, pdMaps: %+v", pb, option, v, pdMaps)
|
||||
continue
|
||||
}
|
||||
// g.Log().Debugf(ctx, "updateChildrenTree pb: %+v", pb)
|
||||
list = append(list, pb)
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
newLevel := pLevel + 1
|
||||
newTree := tree.GenLabel(pTree, pid)
|
||||
|
||||
var updateIds []int64
|
||||
for _, v := range list {
|
||||
updateIds = append(updateIds, v.Id)
|
||||
if err = updateChildrenTree(ctx, dao, v.Id, newLevel, newTree, option); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(updateIds) > 0 {
|
||||
update := g.Map{
|
||||
option.LevelField: newLevel,
|
||||
option.TreeField: newTree,
|
||||
}
|
||||
_, err = dao.Ctx(ctx).WhereIn(option.IdField, updateIds).Data(update).Update()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// package gormmodel
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package gormmodel
|
||||
|
||||
// DefaultTree 默认树表字段
|
||||
type DefaultTree struct {
|
||||
Id int64 `json:"id" description:"ID"`
|
||||
Pid int64 `json:"pid" description:"父ID"`
|
||||
Level int `json:"level" description:"关系树等级"`
|
||||
Tree string `json:"tree" description:"关系树"`
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// package handler
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/utility/convert"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// FilterAuth 过滤数据权限
|
||||
// 通过上下文中的用户角色权限和表中是否含有需要过滤的字段附加查询条件
|
||||
func FilterAuth(m *gdb.Model) *gdb.Model {
|
||||
var (
|
||||
ctx = m.GetCtx()
|
||||
fields = convert.EscapeFieldsToSlice(m.GetFieldsStr())
|
||||
)
|
||||
|
||||
// 超管拥有全部权限
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
return m
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return m
|
||||
}
|
||||
if len(user.Roles) == 0 {
|
||||
return m
|
||||
}
|
||||
|
||||
deptIds := make([]int64, 0)
|
||||
userIds := make([]int64, 0)
|
||||
// 是否进入了非「全部数据」的受限范围;解析结果为空时必须拒绝,不能漏过滤
|
||||
hasRestrictedScope := false
|
||||
currentRoleId := ""
|
||||
currentRoleDataScope := ""
|
||||
currentRoleIdIf := contexts.GetDataValue(ctx, "currentRoleId")
|
||||
if currentRoleIdIf != nil {
|
||||
currentRoleId = currentRoleIdIf.(string)
|
||||
}
|
||||
currentRoleDataScopeIf := contexts.GetDataValue(ctx, "currentRoleDataScope")
|
||||
if currentRoleDataScopeIf != nil {
|
||||
currentRoleDataScope = currentRoleDataScopeIf.(string)
|
||||
}
|
||||
if len(currentRoleDataScope) > 0 && len(currentRoleId) > 0 {
|
||||
dataScope := currentRoleDataScope
|
||||
roleId := gconv.Int64(currentRoleId)
|
||||
g.Log().Infof(ctx, "uid:%d, currentRoleId: %d, currentRoleDataScope: %s", user.ID, roleId, dataScope)
|
||||
if dataScope == consts.SysRoleDataScopeAll { // 角色数据范围: 1全部数据权限
|
||||
return m
|
||||
}
|
||||
hasRestrictedScope = true
|
||||
if dataScope == consts.SysRoleDataScopeCustom { // 角色数据范围: 2自定数据权限
|
||||
var roleDepts []*entity.SysRoleDept
|
||||
err := g.Model("sys_role_dept").Where("tenant_id", contexts.GetTenantId(ctx)).Where("role_id", roleId).Scan(&roleDepts)
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "failed to get role dept data err:%+v", err)
|
||||
}
|
||||
for _, roleDept := range roleDepts {
|
||||
deptIds = append(deptIds, roleDept.DeptId)
|
||||
}
|
||||
} else if dataScope == consts.SysRoleDataScopeDept { // 角色数据范围: 3本部门数据权限
|
||||
deptIds = append(deptIds, user.DeptId)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelow { // 角色数据范围: 4本部门及以下数据权限
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopePersonal { // 角色数据范围: 5仅本人数据权限
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelowOrPersonal { // 角色数据范围: 6本部门及以下或本人数据权限
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgAndNextLevel { // 角色数据范围: 7本组织及本组织下一级数据权限
|
||||
deptIds = append(deptIds, GetOrgAndNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgNextLevel { // 角色数据范围: 8本组织下一级数据权限
|
||||
deptIds = append(deptIds, GetOrgNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
}
|
||||
} else {
|
||||
for _, role := range user.Roles {
|
||||
dataScope := role.DataScope
|
||||
roleId := role.RoleId
|
||||
if dataScope == consts.SysRoleDataScopeAll { // 角色数据范围: 1全部数据权限
|
||||
return m
|
||||
} else if dataScope == consts.SysRoleDataScopeCustom { // 角色数据范围: 2自定数据权限
|
||||
hasRestrictedScope = true
|
||||
var roleDepts []*entity.SysRoleDept
|
||||
err := g.Model("sys_role_dept").Where("tenant_id", contexts.GetTenantId(ctx)).Where("role_id", roleId).Scan(&roleDepts)
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "failed to get role dept data err:%+v", err)
|
||||
}
|
||||
for _, roleDept := range roleDepts {
|
||||
deptIds = append(deptIds, roleDept.DeptId)
|
||||
}
|
||||
} else if dataScope == consts.SysRoleDataScopeDept { // 角色数据范围: 3本部门数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, user.DeptId)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelow { // 角色数据范围: 4本部门及以下数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopePersonal { // 角色数据范围: 5仅本人数据权限
|
||||
hasRestrictedScope = true
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelowOrPersonal { // 角色数据范围: 6本部门及以下或本人数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgAndNextLevel { // 角色数据范围: 7本组织及本组织下一级数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetOrgAndNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgNextLevel { // 角色数据范围: 8本组织下一级数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetOrgNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deptFilterField := ""
|
||||
if gstr.InArray(fields, "dept_id") {
|
||||
deptFilterField = "dept_id"
|
||||
} else if gstr.InArray(fields, "created_dept") {
|
||||
deptFilterField = "created_dept"
|
||||
}
|
||||
userFilterField := ""
|
||||
if gstr.InArray(fields, "user_id") {
|
||||
userFilterField = "user_id"
|
||||
} else if gstr.InArray(fields, "created_by") {
|
||||
userFilterField = "created_by"
|
||||
}
|
||||
|
||||
if len(deptIds) > 0 && len(userIds) > 0 {
|
||||
if deptFilterField != "" && userFilterField != "" {
|
||||
m = m.Where(deptFilterField+" IN (?) OR "+userFilterField+" IN (?)", deptIds, userIds)
|
||||
} else if deptFilterField != "" {
|
||||
m = m.WhereIn(deptFilterField, deptIds)
|
||||
} else if userFilterField != "" {
|
||||
m = m.WhereIn(userFilterField, userIds)
|
||||
}
|
||||
} else if len(deptIds) > 0 && deptFilterField != "" {
|
||||
m = m.WhereIn(deptFilterField, deptIds)
|
||||
} else if len(userIds) > 0 && userFilterField != "" {
|
||||
m = m.WhereIn(userFilterField, userIds)
|
||||
} else if hasRestrictedScope && (deptFilterField != "" || userFilterField != "") {
|
||||
// 例:范围8无下级部门、范围2未配自定义部门 → 可见集合为空,禁止放行全表
|
||||
g.Log().Warningf(ctx, "FilterAuth empty scope result, deny all data. uid:%d, deptIds:%v, userIds:%v", user.ID, deptIds, userIds)
|
||||
m = m.Where("1 = 0")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// GetParentCompanyDeptId 获取用户所属上级公司组织ID
|
||||
func GetParentCompanyDeptId(ctx context.Context, deptId int64) int64 {
|
||||
if deptId <= 0 {
|
||||
return deptId
|
||||
}
|
||||
var dept entity.SysDept
|
||||
err := g.Model("sys_dept").Where("dept_id", deptId).Scan(&dept)
|
||||
if err != nil || dept.DeptId == 0 {
|
||||
return deptId
|
||||
}
|
||||
if dept.ParentId == 0 || dept.DeptType == 1 {
|
||||
return dept.DeptId
|
||||
}
|
||||
return GetParentCompanyDeptId(ctx, dept.ParentId)
|
||||
}
|
||||
|
||||
// GetDeptDirectSub 获取指定部门的直接下级部门ID
|
||||
func GetDeptDirectSub(ctx context.Context, deptId int64) (ids []int64) {
|
||||
array, err := g.Model("sys_dept").
|
||||
Where("parent_id", deptId).
|
||||
Fields("dept_id").
|
||||
Array()
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "GetDeptDirectSub err:%+v", err)
|
||||
return
|
||||
}
|
||||
for _, v := range array {
|
||||
ids = append(ids, v.Int64())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOrgAndNextLevelDeptIds 获取本组织及本组织下一级数据权限范围:所属公司组织及直接下级
|
||||
func GetOrgAndNextLevelDeptIds(ctx context.Context, deptId int64) (ids []int64) {
|
||||
orgDeptId := GetParentCompanyDeptId(ctx, deptId)
|
||||
ids = append(ids, orgDeptId)
|
||||
ids = append(ids, GetDeptDirectSub(ctx, orgDeptId)...)
|
||||
return
|
||||
}
|
||||
|
||||
// GetOrgNextLevelDeptIds 获取本组织下一级数据权限范围:所属公司组织的直接下级
|
||||
func GetOrgNextLevelDeptIds(ctx context.Context, deptId int64) (ids []int64) {
|
||||
orgDeptId := GetParentCompanyDeptId(ctx, deptId)
|
||||
return GetDeptDirectSub(ctx, orgDeptId)
|
||||
}
|
||||
|
||||
// GetDeptAndSub 获取指定部门的所有下级,含本部门
|
||||
func GetDeptAndSub(ctx context.Context, deptId int64) (ids []int64) {
|
||||
array, err := g.Model("sys_dept").
|
||||
WhereLike("ancestors", "%,"+fmt.Sprintf("%d", deptId)+",%").
|
||||
Fields("dept_id").
|
||||
Array()
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "GetDeptAndSub err:%+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range array {
|
||||
ids = append(ids, v.Int64())
|
||||
}
|
||||
|
||||
ids = append(ids, deptId)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// package handler
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package handler
|
||||
|
||||
import "github.com/gogf/gf/v2/database/gdb"
|
||||
|
||||
// ForceCache 强制缓存
|
||||
func ForceCache(m *gdb.Model) *gdb.Model {
|
||||
return m.Cache(gdb.CacheOption{Duration: 0, Force: true})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// package handler
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package handler
|
||||
|
||||
import "github.com/gogf/gf/v2/database/gdb"
|
||||
|
||||
// Option 预处理选项
|
||||
type Option struct {
|
||||
FilterAuth bool // 过滤权限
|
||||
ForceCache bool // 强制缓存
|
||||
FilterTenant bool // 过滤多租户权限
|
||||
}
|
||||
|
||||
// DefaultOption 默认预处理选项
|
||||
var DefaultOption = &Option{
|
||||
FilterAuth: true,
|
||||
FilterTenant: true,
|
||||
}
|
||||
|
||||
func Model(m *gdb.Model, opt ...*Option) *gdb.Model {
|
||||
var option *Option
|
||||
if len(opt) > 0 {
|
||||
option = opt[0]
|
||||
} else {
|
||||
option = DefaultOption
|
||||
}
|
||||
if option.FilterAuth {
|
||||
m = m.Handler(FilterAuth)
|
||||
}
|
||||
if option.ForceCache {
|
||||
m = m.Handler(ForceCache)
|
||||
}
|
||||
if option.FilterTenant {
|
||||
m = m.Handler(FilterTenant)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// package handler
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/utility/convert"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// FilterTenant 过滤多租户数据权限
|
||||
func FilterTenant(m *gdb.Model) *gdb.Model {
|
||||
var (
|
||||
ctx = m.GetCtx()
|
||||
fields = convert.EscapeFieldsToSlice(m.GetFieldsStr())
|
||||
)
|
||||
// 如果包含tenant_id字段,则过滤多租户数据权限
|
||||
if gstr.InArray(fields, "tenant_id") {
|
||||
m = m.Where("tenant_id", contexts.GetTenantId(ctx))
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// package hook
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package hook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/utility"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type MemberSumma struct {
|
||||
UserId int64 `json:"userId" orm:"user_id" description:"用户ID"`
|
||||
UserName string `json:"userName" orm:"user_name" description:"用户账号"`
|
||||
NickName string `json:"nickName" orm:"nick_name" description:"用户昵称"`
|
||||
Avatar string `json:"avatar" orm:"avatar" description:"头像地址"`
|
||||
TenantId string `json:"tenantId" orm:"tenant_id" description:"租户编号"`
|
||||
DeptId int64 `json:"deptId" orm:"dept_id" description:"部门ID"`
|
||||
}
|
||||
|
||||
// MemberSummary 操作人摘要信息
|
||||
var MemberSummary = gdb.HookHandler{
|
||||
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
|
||||
result, err = in.Next(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
createdByIds []int64
|
||||
updatedByIds []int64
|
||||
deletedByIds []int64
|
||||
memberIds []int64
|
||||
)
|
||||
|
||||
for _, record := range result {
|
||||
if record["created_by"].Int64() > 0 {
|
||||
createdByIds = append(createdByIds, record["created_by"].Int64())
|
||||
}
|
||||
if record["updated_by"].Int64() > 0 {
|
||||
updatedByIds = append(updatedByIds, record["updated_by"].Int64())
|
||||
}
|
||||
if record["deleted_by"].Int64() > 0 {
|
||||
deletedByIds = append(deletedByIds, record["deleted_by"].Int64())
|
||||
}
|
||||
if record["user_id"].Int64() > 0 {
|
||||
memberIds = append(memberIds, record["user_id"].Int64())
|
||||
}
|
||||
}
|
||||
|
||||
memberIds = append(memberIds, createdByIds...)
|
||||
memberIds = append(memberIds, updatedByIds...)
|
||||
memberIds = append(memberIds, deletedByIds...)
|
||||
memberIds = utility.UniqueSlice(memberIds)
|
||||
if len(memberIds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var members []*MemberSumma
|
||||
if err = g.Model("sys_user").Ctx(ctx).WhereIn("user_id", memberIds).Scan(&members); err != nil {
|
||||
g.Log().Errorf(ctx, "[MemberSummary] 查询用户信息失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
g.Log().Warningf(ctx, "[MemberSummary] 没有找到任何用户信息,用户IDs: %v", memberIds)
|
||||
return
|
||||
}
|
||||
|
||||
findMember := func(id *gvar.Var) *MemberSumma {
|
||||
userId := id.Int64()
|
||||
for _, v := range members {
|
||||
if v.UserId == userId {
|
||||
return v
|
||||
}
|
||||
}
|
||||
// 只在找不到用户时记录警告(避免日志过多)
|
||||
g.Log().Warningf(ctx, "[MemberSummary] 未找到用户ID: %d", userId)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, record := range result {
|
||||
if record["created_by"].Int64() > 0 {
|
||||
member := findMember(record["created_by"])
|
||||
if member != nil {
|
||||
record["createdBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
|
||||
if record["updated_by"].Int64() > 0 {
|
||||
member := findMember(record["updated_by"])
|
||||
if member != nil {
|
||||
record["updatedBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
|
||||
if record["deleted_by"].Int64() > 0 {
|
||||
member := findMember(record["deleted_by"])
|
||||
if member != nil {
|
||||
record["deletedBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
|
||||
if record["user_id"].Int64() > 0 {
|
||||
member := findMember(record["user_id"])
|
||||
if member != nil {
|
||||
record["memberBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user