chore: 并入 iotlcm-openapi 历史到 openapi/(原提交 3b05d5a)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:02 +08:00
484 changed files with 34732 additions and 0 deletions
@@ -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 <xxx@example.com>
// @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
}
+60
View File
@@ -0,0 +1,60 @@
// Package cache
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <xxx@example.com>
// @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)
}
+440
View File
@@ -0,0 +1,440 @@
// Package file
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <xxx@example.com>
// @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 <xxx@example.com>
// @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
}
+46
View File
@@ -0,0 +1,46 @@
// Package mcache
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <xxx@example.com>
// @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)
}
+27
View File
@@ -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
}
+197
View File
@@ -0,0 +1,197 @@
// package xgorm
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <xxx@example.com>
// @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
}
+195
View File
@@ -0,0 +1,195 @@
// package xgorm
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <xxx@example.com>
// @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 <xxx@example.com>
// @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 <xxx@example.com>
// @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 <xxx@example.com>
// @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 <xxx@example.com>
// @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 <xxx@example.com>
// @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 <xxx@example.com>
// @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
},
}