chore: 并入 iotlcm-app 历史到 app/(原提交 f40bda9)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// package convert
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package convert
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
var (
|
||||
descTags = []string{"description", "dc", "json"} // 实体描述标签
|
||||
fieldTags = []string{"json"} // 实体字段名称映射
|
||||
)
|
||||
|
||||
// EscapeFieldsToSlice 将转义过的字段转换为字段集切片
|
||||
func EscapeFieldsToSlice(s string) []string {
|
||||
return gstr.Explode(",", gstr.Replace(gstr.Replace(s, "`,`", ","), "`", ""))
|
||||
}
|
||||
|
||||
// GetEntityDescTags 获取实体中的描述标签
|
||||
func GetEntityDescTags(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, descTags, []string{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, addTags...)
|
||||
continue
|
||||
}
|
||||
tags = append(tags, reflectTagName(field, descTags, 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,138 @@
|
||||
// package excel
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package excel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"unicode"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
excelize "github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// 单元格表头
|
||||
char = []string{"", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
|
||||
// 默认行样式
|
||||
// defaultRowStyle = `{"font":{"color":"#666666","size":13,"family":"arial"},"alignment":{"vertical":"center","horizontal":"center"}}`
|
||||
defaultRowStyle = &excelize.Style{
|
||||
Font: &excelize.Font{
|
||||
Color: "#666666",
|
||||
Size: 13,
|
||||
Family: "arial",
|
||||
},
|
||||
Alignment: &excelize.Alignment{
|
||||
Vertical: "center",
|
||||
Horizontal: "center",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// ExportByStructs 导出切片结构体到excel表格
|
||||
func ExportByStructs(ctx context.Context, tags []string, list interface{}, fileName string, sheetName string) (err error) {
|
||||
f := excelize.NewFile()
|
||||
f.SetSheetName("Sheet1", sheetName)
|
||||
_ = f.SetRowHeight("Sheet1", 1, 30)
|
||||
|
||||
rowStyleID, err := f.NewStyle(defaultRowStyle)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = f.SetSheetRow(sheetName, "A1", &tags)
|
||||
var (
|
||||
length = len(tags)
|
||||
headStyle = letter(length)
|
||||
lastRow string
|
||||
widthRow string
|
||||
)
|
||||
for k, v := range headStyle {
|
||||
if k == length-1 {
|
||||
lastRow = fmt.Sprintf("%s1", v)
|
||||
widthRow = v
|
||||
}
|
||||
}
|
||||
|
||||
if err = f.SetColWidth(sheetName, "A", widthRow, 30); err != nil {
|
||||
return err
|
||||
}
|
||||
var rowNum = 1
|
||||
for _, v := range gconv.Interfaces(list) {
|
||||
t := reflect.TypeOf(v)
|
||||
value := reflect.ValueOf(v)
|
||||
row := make([]interface{}, 0)
|
||||
for l := 0; l < t.NumField(); l++ {
|
||||
val := value.Field(l).Interface()
|
||||
row = append(row, val)
|
||||
}
|
||||
rowNum++
|
||||
if err = f.SetSheetRow(sheetName, "A"+gconv.String(rowNum), &row); err != nil {
|
||||
return
|
||||
}
|
||||
if err = f.SetCellStyle(sheetName, fmt.Sprintf("A%d", rowNum), lastRow, rowStyleID); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
r := ghttp.RequestFromCtx(ctx)
|
||||
if r == nil {
|
||||
err = gerror.New("ctx not http request")
|
||||
return
|
||||
}
|
||||
writer := r.Response.Writer
|
||||
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||
writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", url.QueryEscape(fileName)))
|
||||
writer.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
writer.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
|
||||
if err = f.Write(writer); err != nil {
|
||||
return
|
||||
}
|
||||
r.Response.Status = http.StatusOK
|
||||
f.Close()
|
||||
writer.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// letter 生成完整的表头
|
||||
func letter(length int) []string {
|
||||
var str []string
|
||||
for i := 1; i <= length; i++ {
|
||||
str = append(str, numToChars(i))
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// numToChars 将数字转换为具体的表格表头名称
|
||||
func numToChars(num int) string {
|
||||
var cols string
|
||||
v := num
|
||||
for v > 0 {
|
||||
k := v % 26
|
||||
if k == 0 {
|
||||
k = 26
|
||||
}
|
||||
v = (v - k) / 26
|
||||
cols = char[k] + cols
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
// NextLetter 传入一个字母,获取下一个字母
|
||||
func NextLetter(input string) string {
|
||||
if len(input) == 0 {
|
||||
return ""
|
||||
}
|
||||
upperInput := unicode.ToUpper(rune(input[0]))
|
||||
if upperInput >= 'A' && upperInput < 'Z' {
|
||||
return string(upperInput + 1)
|
||||
}
|
||||
return "A"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (r *Redis) Conn(ctx context.Context) (*redis.Conn, error) {
|
||||
return r.rdb.Conn(), nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultGroupName = "default" // Default configuration group name.
|
||||
)
|
||||
|
||||
var (
|
||||
lock sync.RWMutex
|
||||
instances map[string]*Redis
|
||||
)
|
||||
|
||||
func init() {
|
||||
instances = make(map[string]*Redis)
|
||||
}
|
||||
|
||||
type Redis struct {
|
||||
config *Config
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
// Config is redis configuration.
|
||||
type Config struct {
|
||||
// Address It supports single and cluster redis server. Multiple addresses joined with char ','. Eg: 192.0.2.10:6379, 192.0.2.10:6379.
|
||||
Address string `json:"address"`
|
||||
Db int `json:"db"` // Redis db.
|
||||
User string `json:"user"` // Username for AUTH.
|
||||
Pass string `json:"pass"` // Password for AUTH.
|
||||
SentinelUser string `json:"sentinel_user"` // Username for sentinel AUTH.
|
||||
SentinelPass string `json:"sentinel_pass"` // Password for sentinel AUTH.
|
||||
MinIdle int `json:"minIdle"` // Minimum number of connections allowed to be idle (default is 0)
|
||||
MaxIdle int `json:"maxIdle"` // Maximum number of connections allowed to be idle (default is 10)
|
||||
MaxActive int `json:"maxActive"` // Maximum number of connections limit (default is 0 means no limit).
|
||||
MaxConnLifetime time.Duration `json:"maxConnLifetime"` // Maximum lifetime of the connection (default is 30 seconds, not allowed to be set to 0)
|
||||
IdleTimeout time.Duration `json:"idleTimeout"` // Maximum idle time for connection (default is 10 seconds, not allowed to be set to 0)
|
||||
WaitTimeout time.Duration `json:"waitTimeout"` // Timed out duration waiting to get a connection from the connection pool.
|
||||
DialTimeout time.Duration `json:"dialTimeout"` // Dial connection timeout for TCP.
|
||||
ReadTimeout time.Duration `json:"readTimeout"` // Read timeout for TCP. DO NOT set it if not necessary.
|
||||
WriteTimeout time.Duration `json:"writeTimeout"` // Write timeout for TCP.
|
||||
MasterName string `json:"masterName"` // Used in Redis Sentinel mode.
|
||||
TLS bool `json:"tls"` // Specifies whether TLS should be used when connecting to the server.
|
||||
TLSSkipVerify bool `json:"tlsSkipVerify"` // Disables server name verification when connecting over TLS.
|
||||
TLSConfig *tls.Config `json:"-"` // TLS Config to use. When set TLS will be negotiated.
|
||||
SlaveOnly bool `json:"slaveOnly"` // Route all commands to slave read-only nodes.
|
||||
Cluster bool `json:"cluster"` // Specifies whether cluster mode be used.
|
||||
Protocol int `json:"protocol"` // Specifies the RESP version (Protocol 2 or 3.)
|
||||
}
|
||||
|
||||
func New(groupName string, config *Config) *Redis {
|
||||
instance := &Redis{
|
||||
config: config,
|
||||
}
|
||||
if config == nil {
|
||||
instance.rdb = redis.NewClient(&redis.Options{})
|
||||
return instance
|
||||
}
|
||||
poolSize := config.MaxActive
|
||||
if poolSize == 0 {
|
||||
poolSize = 10
|
||||
config.MaxActive = 10
|
||||
}
|
||||
|
||||
if config.IdleTimeout < 1*time.Minute {
|
||||
config.IdleTimeout = 30 * time.Minute
|
||||
}
|
||||
if config.MaxConnLifetime <= 1 {
|
||||
config.MaxConnLifetime = 24 * 365 * 10 * time.Hour
|
||||
}
|
||||
if config.WaitTimeout < 1*time.Second {
|
||||
config.WaitTimeout = 6 * time.Second
|
||||
}
|
||||
if config.DialTimeout < 1*time.Second {
|
||||
config.DialTimeout = 5 * time.Second
|
||||
}
|
||||
if config.ReadTimeout < 1*time.Second {
|
||||
config.ReadTimeout = 5 * time.Second
|
||||
}
|
||||
if config.WriteTimeout < 1*time.Second {
|
||||
config.WriteTimeout = 5 * time.Second
|
||||
}
|
||||
opt := &redis.Options{
|
||||
Addr: config.Address,
|
||||
Password: config.Pass,
|
||||
DB: config.Db,
|
||||
MinIdleConns: config.MinIdle,
|
||||
MaxIdleConns: config.MaxIdle,
|
||||
MaxActiveConns: config.MaxActive,
|
||||
ConnMaxIdleTime: config.IdleTimeout,
|
||||
PoolSize: poolSize,
|
||||
PoolTimeout: config.WaitTimeout,
|
||||
DialTimeout: config.DialTimeout,
|
||||
ReadTimeout: config.ReadTimeout,
|
||||
WriteTimeout: config.WriteTimeout,
|
||||
TLSConfig: config.TLSConfig,
|
||||
// IdentitySuffix: config.IdentitySuffix,
|
||||
// ConnMaxLifetime: config.MaxConnLifetime,
|
||||
}
|
||||
instance.rdb = redis.NewClient(opt)
|
||||
g.Log().Noticef(context.Background(), "groupName: %s, opt: %+v", groupName, opt)
|
||||
return instance
|
||||
}
|
||||
|
||||
func GetConfig(name string) *Config {
|
||||
cfg, err := g.Cfg().Get(context.Background(), "redis."+name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if cfg == nil || cfg.IsNil() {
|
||||
panic("redis config not found: " + name)
|
||||
}
|
||||
config := &Config{}
|
||||
if err = gconv.Scan(cfg.Map(), config); err != nil {
|
||||
panic(`invalid redis configuration: ` + cfg.String())
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func Instance(name ...string) *Redis {
|
||||
groupName := DefaultGroupName
|
||||
if len(name) > 0 {
|
||||
groupName = name[0]
|
||||
if groupName == "" {
|
||||
groupName = DefaultGroupName
|
||||
}
|
||||
}
|
||||
lock.RLock()
|
||||
instance, ok := instances[groupName]
|
||||
lock.RUnlock()
|
||||
if !ok {
|
||||
lock.Lock()
|
||||
instance, ok = instances[groupName]
|
||||
if !ok {
|
||||
instance = New(groupName, GetConfig(groupName))
|
||||
instances[groupName] = instance
|
||||
}
|
||||
lock.Unlock()
|
||||
return instance
|
||||
}
|
||||
return instance
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (r *Redis) HMSet(ctx context.Context, key string, mp map[string]interface{}) error {
|
||||
mp2 := make(map[string]string)
|
||||
for k, v := range mp {
|
||||
mp2[k] = gconv.String(v)
|
||||
}
|
||||
cmd := r.rdb.HMSet(ctx, key, mp2)
|
||||
return cmd.Err()
|
||||
}
|
||||
func (r *Redis) HGetAll(ctx context.Context, key string) (map[string]string, error) {
|
||||
cmd := r.rdb.HGetAll(ctx, key)
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
|
||||
func (r *Redis) HGet(ctx context.Context, key, field string) (string, error) {
|
||||
cmd := r.rdb.HGet(ctx, key, field)
|
||||
if cmd.Err() == redis.Nil {
|
||||
return "", nil
|
||||
}
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
|
||||
func (r *Redis) HSet(ctx context.Context, key string, mp map[string]interface{}) error {
|
||||
mp2 := make(map[string]string)
|
||||
for k, v := range mp {
|
||||
mp2[k] = gconv.String(v)
|
||||
}
|
||||
cmd := r.rdb.HSet(ctx, key, mp2)
|
||||
return cmd.Err()
|
||||
}
|
||||
|
||||
func (r *Redis) HSetNX(ctx context.Context, key, field string, value interface{}) (bool, error) {
|
||||
cmd := r.rdb.HSetNX(ctx, key, field, gconv.String(value))
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
func (r *Redis) HDel(ctx context.Context, key string, field string) (int64, error) {
|
||||
cmd := r.rdb.HDel(ctx, key, field)
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
func (r *Redis) HLen(ctx context.Context, key string) (int64, error) {
|
||||
cmd := r.rdb.HLen(ctx, key)
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (r *Redis) Do(ctx context.Context, args ...interface{}) (*g.Var, error) {
|
||||
cmd := r.rdb.Do(ctx, args...)
|
||||
if cmd.Err() == redis.Nil {
|
||||
return g.NewVar(nil), nil
|
||||
}
|
||||
return g.NewVar(cmd.Val()), cmd.Err()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (r *Redis) LRange(ctx context.Context, key string, start, stop int64) (*g.Var, error) {
|
||||
cmd := r.rdb.LRange(ctx, key, start, stop)
|
||||
if cmd.Err() == redis.Nil {
|
||||
return g.NewVar(nil), nil
|
||||
}
|
||||
return g.NewVar(cmd.Val()), cmd.Err()
|
||||
}
|
||||
func (r *Redis) RPush(ctx context.Context, key string, value interface{}) (int64, error) {
|
||||
cmd := r.rdb.RPush(ctx, key, gconv.String(value))
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
func (r *Redis) LLen(ctx context.Context, key string) (int64, error) {
|
||||
cmd := r.rdb.LLen(ctx, key)
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
func (r *Redis) LTrim(ctx context.Context, key string, start, stop int64) error {
|
||||
cmd := r.rdb.LTrim(ctx, key, start, stop)
|
||||
return cmd.Err()
|
||||
}
|
||||
func (r *Redis) BLPop(ctx context.Context, timeout time.Duration, keys ...string) (*g.Var, error) {
|
||||
cmd := r.rdb.BLPop(ctx, timeout, keys...)
|
||||
if cmd.Err() == redis.Nil {
|
||||
return g.NewVar(nil), nil
|
||||
}
|
||||
return g.NewVar(cmd.Val()), cmd.Err()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (r *Redis) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
cmd := r.rdb.Set(ctx, key, gconv.String(value), expiration)
|
||||
return cmd.Err()
|
||||
}
|
||||
|
||||
func (r *Redis) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error) {
|
||||
cmd := r.rdb.SetNX(ctx, key, gconv.String(value), expiration)
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
|
||||
func (r *Redis) Get(ctx context.Context, key string) (*g.Var, error) {
|
||||
cmd := r.rdb.Get(ctx, key)
|
||||
if cmd.Err() == redis.Nil {
|
||||
return g.NewVar(nil), nil
|
||||
}
|
||||
return g.NewVar(cmd.Val()), cmd.Err()
|
||||
}
|
||||
|
||||
func (r *Redis) UpdateExpire(ctx context.Context, key string, expiration time.Duration) (bool, error) {
|
||||
cmd := r.rdb.Expire(ctx, key, expiration)
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
|
||||
func (r *Redis) Remove(ctx context.Context, key string) (bool, error) {
|
||||
cmd := r.rdb.Del(ctx, key)
|
||||
return cmd.Val() > 0, cmd.Err()
|
||||
}
|
||||
func (r *Redis) Del(ctx context.Context, key string) (int64, error) {
|
||||
cmd := r.rdb.Del(ctx, key)
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (r *Redis) Subscribe(ctx context.Context, channels ...string) (*redis.PubSub, error) {
|
||||
cmd := r.rdb.Subscribe(ctx, channels...)
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (r *Redis) Publish(ctx context.Context, channel string, message interface{}) (int64, error) {
|
||||
cmd := r.rdb.Publish(ctx, channel, gconv.String(message))
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package gfredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func (r *Redis) ZPopByScore(ctx context.Context, key string, min, max string, limit int64) (res []string, err error) {
|
||||
var luaScript = `
|
||||
local members = redis.call('ZRANGEBYSCORE', KEYS[1], ARGV[1], ARGV[2], 'LIMIT', 0, ARGV[3])
|
||||
if #members > 0 then
|
||||
redis.call('ZREM', KEYS[1], unpack(members))
|
||||
end
|
||||
return members
|
||||
`
|
||||
cmd := r.rdb.Eval(ctx, luaScript, []string{key}, min, max, limit)
|
||||
if err = cmd.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = cmd.StringSlice()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Table of CRC values for high–order byte
|
||||
var crcHighBytes = []byte{
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
}
|
||||
|
||||
// Table of CRC values for low-order byte
|
||||
var crcLowBytes = []byte{
|
||||
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04,
|
||||
0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8,
|
||||
0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
|
||||
0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10,
|
||||
0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
|
||||
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
|
||||
0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C,
|
||||
0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0,
|
||||
0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
|
||||
0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
|
||||
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C,
|
||||
0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
|
||||
0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54,
|
||||
0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98,
|
||||
0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
|
||||
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40,
|
||||
}
|
||||
|
||||
// Cyclical Redundancy Checking
|
||||
type Crc16 struct {
|
||||
high byte
|
||||
low byte
|
||||
}
|
||||
|
||||
func (crc *Crc16) Reset() *Crc16 {
|
||||
crc.high = 0xFF
|
||||
crc.low = 0xFF
|
||||
return crc
|
||||
}
|
||||
|
||||
func (crc *Crc16) PushBytes(bs []byte) *Crc16 {
|
||||
var idx, b byte
|
||||
|
||||
for _, b = range bs {
|
||||
idx = crc.low ^ b
|
||||
crc.low = crc.high ^ crcHighBytes[idx]
|
||||
crc.high = crcLowBytes[idx]
|
||||
}
|
||||
return crc
|
||||
}
|
||||
|
||||
func (crc *Crc16) Value() uint16 {
|
||||
return uint16(crc.high)<<8 | uint16(crc.low)
|
||||
}
|
||||
|
||||
func Crc16ChecksumData(bs []byte) uint16 {
|
||||
crc := &Crc16{}
|
||||
crc.Reset()
|
||||
crc.PushBytes(bs)
|
||||
return crc.Value()
|
||||
}
|
||||
|
||||
func Crc16ChecksumFile(filePath string) uint16 {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer file.Close()
|
||||
crc := &Crc16{}
|
||||
crc.Reset()
|
||||
// 分块读取
|
||||
blockSize := 1024 * 1024
|
||||
buf := make([]byte, blockSize)
|
||||
for {
|
||||
n, err := file.Read(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
crc.PushBytes(buf[:n])
|
||||
}
|
||||
return crc.Value()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package hash
|
||||
|
||||
import "os"
|
||||
|
||||
func Sum16ChecksumData(bs []byte) uint16 {
|
||||
sum := uint16(0)
|
||||
for _, b := range bs {
|
||||
sum += uint16(b)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func Sum16ChecksumFile(filePath string) uint16 {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer file.Close()
|
||||
// 分块读取
|
||||
blockSize := 1024 * 1024
|
||||
buf := make([]byte, blockSize)
|
||||
sum := uint16(0)
|
||||
for {
|
||||
n, err := file.Read(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
sum += Sum16ChecksumData(buf[:n])
|
||||
}
|
||||
return sum
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type BatchResultRow struct {
|
||||
RowNo int
|
||||
ItemID string
|
||||
Status string
|
||||
ErrorReason string
|
||||
}
|
||||
|
||||
type BatchInfoItem struct {
|
||||
Field string
|
||||
Value string
|
||||
}
|
||||
|
||||
func BuildBatchResultExcel(rows []BatchResultRow, infos []BatchInfoItem) (*excelize.File, error) {
|
||||
f := excelize.NewFile()
|
||||
leftStyleID, err := f.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "left",
|
||||
Vertical: "center",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultSheet := "导入结果"
|
||||
if err := f.SetSheetName("Sheet1", resultSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := []string{"源文件行号", "item_id", "处理状态", "异常原因"}
|
||||
if err := f.SetSheetRow(resultSheet, "A1", &headers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, item := range rows {
|
||||
values := []interface{}{item.RowNo, item.ItemID, item.Status, item.ErrorReason}
|
||||
cell, err := excelize.CoordinatesToCellName(1, i+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(resultSheet, cell, &values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resultEndCell, err := excelize.CoordinatesToCellName(4, len(rows)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.SetCellStyle(resultSheet, "A1", resultEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infoSheet := "批次信息"
|
||||
if _, err := f.NewSheet(infoSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoHeaders := []string{"字段", "值"}
|
||||
if err := f.SetSheetRow(infoSheet, "A1", &infoHeaders); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, item := range infos {
|
||||
values := []interface{}{item.Field, item.Value}
|
||||
cell, err := excelize.CoordinatesToCellName(1, i+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(infoSheet, cell, &values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
infoEndCell, err := excelize.CoordinatesToCellName(2, len(infos)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.SetCellStyle(infoSheet, "A1", infoEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = f.SetColWidth(resultSheet, "A", "A", 14)
|
||||
_ = f.SetColWidth(resultSheet, "B", "B", 24)
|
||||
_ = f.SetColWidth(resultSheet, "C", "C", 12)
|
||||
_ = f.SetColWidth(resultSheet, "D", "D", 40)
|
||||
_ = f.SetColWidth(infoSheet, "A", "B", 24)
|
||||
f.SetActiveSheet(0)
|
||||
return f, nil
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// BuildByStructs creates an Excel workbook from a struct slice without writing
|
||||
// it to HTTP response or disk.
|
||||
func BuildByStructs(tags []string, list interface{}, sheetName string) (f *excelize.File, err error) {
|
||||
return BuildByStructsWithOption(tags, list, sheetName, nil)
|
||||
}
|
||||
|
||||
// BuildByStructsWithOption creates an Excel workbook and applies optional
|
||||
// styles or advanced customizations.
|
||||
func BuildByStructsWithOption(tags []string, list interface{}, sheetName string, option *BuildOption) (f *excelize.File, err error) {
|
||||
if sheetName == "" {
|
||||
sheetName = "Sheet1"
|
||||
}
|
||||
|
||||
f = excelize.NewFile()
|
||||
if err = f.SetSheetName("Sheet1", sheetName); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = f.SetSheetRow(sheetName, "A1", &tags); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
length := len(tags)
|
||||
rowNum := 1
|
||||
for _, item := range gconv.Interfaces(list) {
|
||||
row := structValues(item)
|
||||
rowNum++
|
||||
if err = f.SetSheetRow(sheetName, "A"+gconv.String(rowNum), &row); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = applyBuildOption(f, sheetName, length, rowNum, option); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func applyBuildOption(f *excelize.File, sheetName string, colCount int, rowCount int, option *BuildOption) error {
|
||||
if option == nil {
|
||||
return nil
|
||||
}
|
||||
if option.HeaderHeight > 0 {
|
||||
if err := f.SetRowHeight(sheetName, 1, option.HeaderHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if colCount > 0 && option.ColWidth > 0 {
|
||||
lastCol := letter(colCount)[colCount-1]
|
||||
if err := f.SetColWidth(sheetName, "A", lastCol, option.ColWidth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if colCount > 0 && option.HeaderStyle != nil {
|
||||
styleID, err := f.NewStyle(option.HeaderStyle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lastCell, err := excelize.CoordinatesToCellName(colCount, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetCellStyle(sheetName, "A1", lastCell, styleID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if colCount > 0 && rowCount > 1 && option.RowStyle != nil {
|
||||
styleID, err := f.NewStyle(option.RowStyle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
startCell := "A2"
|
||||
endCell, err := excelize.CoordinatesToCellName(colCount, rowCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetCellStyle(sheetName, startCell, endCell, styleID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if option.AfterBuild != nil {
|
||||
return option.AfterBuild(f, sheetName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func structValues(item interface{}) []interface{} {
|
||||
value := reflect.ValueOf(item)
|
||||
if !value.IsValid() {
|
||||
return nil
|
||||
}
|
||||
if value.Kind() == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
value = value.Elem()
|
||||
}
|
||||
if value.Kind() != reflect.Struct {
|
||||
return []interface{}{item}
|
||||
}
|
||||
return collectStructValues(value)
|
||||
}
|
||||
|
||||
func collectStructValues(value reflect.Value) []interface{} {
|
||||
var row []interface{}
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
fieldValue := value.Field(i)
|
||||
fieldInfo := value.Type().Field(i)
|
||||
if !fieldInfo.IsExported() {
|
||||
continue
|
||||
}
|
||||
if fieldInfo.Anonymous && derefType(fieldInfo.Type).Kind() == reflect.Struct {
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
if fieldValue.IsNil() {
|
||||
continue
|
||||
}
|
||||
fieldValue = fieldValue.Elem()
|
||||
}
|
||||
row = append(row, collectStructValues(fieldValue)...)
|
||||
continue
|
||||
}
|
||||
row = append(row, fieldValue.Interface())
|
||||
}
|
||||
return row
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// ImportByReader parses an uploaded Excel file into typed rows. The first row is
|
||||
// treated as the header row and matched by excel/dc/description/json tags.
|
||||
func ImportByReader[T any](r io.Reader, sheetName string) (*ImportResult[T], error) {
|
||||
f, err := excelize.OpenReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if sheetName == "" {
|
||||
sheetName = firstSheetName(f)
|
||||
}
|
||||
rows, err := f.Rows(sheetName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
schemas := parseSchema(reflect.TypeOf((*T)(nil)).Elem())
|
||||
schemaByHeader := make(map[string]fieldSchema, len(schemas))
|
||||
for _, schema := range schemas {
|
||||
schemaByHeader[normalizeHeader(schema.Header)] = schema
|
||||
}
|
||||
if len(schemaByHeader) == 0 {
|
||||
return nil, gerror.New("导入模型没有可读取字段")
|
||||
}
|
||||
|
||||
result := &ImportResult[T]{Rows: make([]T, 0)}
|
||||
headerIndex := make(map[int]fieldSchema)
|
||||
rowIndex := 0
|
||||
hasHeader := false
|
||||
|
||||
for rows.Next() {
|
||||
rowIndex++
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rowIndex == 1 {
|
||||
hasHeader = true
|
||||
for i, header := range cols {
|
||||
if schema, ok := schemaByHeader[normalizeHeader(header)]; ok {
|
||||
headerIndex[i] = schema
|
||||
}
|
||||
}
|
||||
if len(headerIndex) == 0 {
|
||||
return nil, gerror.New("Excel表头与导入模型不匹配")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isEmptyRow(cols) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.TotalRows++
|
||||
item, rowErrors := parseImportRow[T](rowIndex, cols, headerIndex, schemas)
|
||||
if len(rowErrors) > 0 {
|
||||
result.Errors = append(result.Errors, rowErrors...)
|
||||
result.FailedRows++
|
||||
continue
|
||||
}
|
||||
result.Rows = append(result.Rows, item)
|
||||
result.SuccessRows++
|
||||
}
|
||||
if err = rows.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasHeader {
|
||||
return nil, gerror.New("Excel文件缺少表头")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseImportRow[T any](rowIndex int, cols []string, headerIndex map[int]fieldSchema, schemas []fieldSchema) (T, []RowError) {
|
||||
var item T
|
||||
var errors []RowError
|
||||
value := reflect.ValueOf(&item).Elem()
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for colIndex, schema := range headerIndex {
|
||||
raw := ""
|
||||
if colIndex < len(cols) {
|
||||
raw = normalizeHeader(cols[colIndex])
|
||||
}
|
||||
seen[schema.Name] = true
|
||||
if raw == "" {
|
||||
if schema.Required {
|
||||
errors = append(errors, RowError{
|
||||
Row: rowIndex,
|
||||
Column: schema.Header,
|
||||
Field: schema.Name,
|
||||
Message: requiredMessage(schema.Field, schema.Header),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := setFieldValue(value, schema, raw); err != nil {
|
||||
errors = append(errors, RowError{
|
||||
Row: rowIndex,
|
||||
Column: schema.Header,
|
||||
Field: schema.Name,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, schema := range schemas {
|
||||
if schema.Required && !seen[schema.Name] {
|
||||
errors = append(errors, RowError{
|
||||
Row: rowIndex,
|
||||
Column: schema.Header,
|
||||
Field: schema.Name,
|
||||
Message: "缺少必要列:" + schema.Header,
|
||||
})
|
||||
}
|
||||
}
|
||||
return item, errors
|
||||
}
|
||||
|
||||
func setFieldValue(root reflect.Value, schema fieldSchema, raw string) error {
|
||||
field := fieldByIndexAlloc(root, schema.Index)
|
||||
if field.Kind() == reflect.Ptr {
|
||||
elemType := field.Type().Elem()
|
||||
elemValue := reflect.New(elemType).Elem()
|
||||
if err := setValue(elemValue, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(elemValue.Addr())
|
||||
return nil
|
||||
}
|
||||
return setValue(field, raw)
|
||||
}
|
||||
|
||||
func fieldByIndexAlloc(root reflect.Value, index []int) reflect.Value {
|
||||
field := root
|
||||
for _, i := range index {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
field = field.Elem()
|
||||
}
|
||||
field = field.Field(i)
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
func setValue(field reflect.Value, raw string) error {
|
||||
if !field.CanSet() {
|
||||
return nil
|
||||
}
|
||||
if field.Type() == reflect.TypeOf(gtime.Time{}) {
|
||||
t := gtime.NewFromStr(raw)
|
||||
if t == nil {
|
||||
return gconv.Scan(raw, field.Addr().Interface())
|
||||
}
|
||||
field.Set(reflect.ValueOf(*t))
|
||||
return nil
|
||||
}
|
||||
if field.Type() == reflect.TypeOf(>ime.Time{}) {
|
||||
t := gtime.NewFromStr(raw)
|
||||
if t == nil {
|
||||
return gconv.Scan(raw, field.Addr().Interface())
|
||||
}
|
||||
field.Set(reflect.ValueOf(t))
|
||||
return nil
|
||||
}
|
||||
return gconv.Scan(raw, field.Addr().Interface())
|
||||
}
|
||||
|
||||
func firstSheetName(f *excelize.File) string {
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return "Sheet1"
|
||||
}
|
||||
return sheets[0]
|
||||
}
|
||||
|
||||
func isEmptyRow(cols []string) bool {
|
||||
for _, col := range cols {
|
||||
if normalizeHeader(col) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// SaveLocal saves an Excel workbook through SysOss local storage and records it
|
||||
// in the sys_oss table.
|
||||
func SaveLocal(ctx context.Context, f *excelize.File, fileName string, category string) (*entity.SysOss, error) {
|
||||
if category == "" {
|
||||
category = "export"
|
||||
}
|
||||
category = safePathName(category)
|
||||
if fileName == "" {
|
||||
fileName = "excel"
|
||||
}
|
||||
|
||||
fileName = uniqueExcelFileName(fileName)
|
||||
reader, err := ToReadSeeker(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = reader.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{
|
||||
Content: content,
|
||||
FileName: fileName,
|
||||
NewFileType: consts.SysUploadNewFileTypeUserFile,
|
||||
SubDirName: "excel/" + category,
|
||||
NotAddDate: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return result.SysOss, nil
|
||||
}
|
||||
|
||||
func safeExcelFileName(fileName string) string {
|
||||
fileName = strings.TrimSpace(fileName)
|
||||
if fileName == "" {
|
||||
fileName = "excel"
|
||||
}
|
||||
fileName = strings.TrimSuffix(fileName, ".xlsx")
|
||||
for _, old := range []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"} {
|
||||
fileName = strings.ReplaceAll(fileName, old, "_")
|
||||
}
|
||||
return fileName + ".xlsx"
|
||||
}
|
||||
|
||||
func uniqueExcelFileName(fileName string) string {
|
||||
fileName = strings.TrimSuffix(safeExcelFileName(fileName), ".xlsx")
|
||||
uid := uuid32.S()
|
||||
if len(uid) > 8 {
|
||||
uid = uid[:8]
|
||||
}
|
||||
return fmt.Sprintf("%s_%s_%s.xlsx", time.Now().Format("20060102150405000"), uid, fileName)
|
||||
}
|
||||
|
||||
func safePathName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "export"
|
||||
}
|
||||
for _, old := range []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|", "."} {
|
||||
name = strings.ReplaceAll(name, old, "_")
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type fieldSchema struct {
|
||||
Index []int
|
||||
Field reflect.StructField
|
||||
Name string
|
||||
Header string
|
||||
Required bool
|
||||
}
|
||||
|
||||
func parseSchema(t reflect.Type) []fieldSchema {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
return parseStructSchema(t, nil)
|
||||
}
|
||||
|
||||
func parseStructSchema(t reflect.Type, parent []int) []fieldSchema {
|
||||
var fields []fieldSchema
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
if field.Anonymous && derefType(field.Type).Kind() == reflect.Struct {
|
||||
fields = append(fields, parseStructSchema(derefType(field.Type), appendIndex(parent, i))...)
|
||||
continue
|
||||
}
|
||||
|
||||
header := fieldHeader(field)
|
||||
if header == "-" || header == "" {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, fieldSchema{
|
||||
Index: appendIndex(parent, i),
|
||||
Field: field,
|
||||
Name: field.Name,
|
||||
Header: header,
|
||||
Required: isRequired(field),
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func fieldHeader(field reflect.StructField) string {
|
||||
for _, tag := range []string{"excel", "dc", "description", "json"} {
|
||||
if value, ok := field.Tag.Lookup(tag); ok {
|
||||
value = strings.TrimSpace(strings.Split(value, ",")[0])
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return field.Name
|
||||
}
|
||||
|
||||
func isRequired(field reflect.StructField) bool {
|
||||
rule := field.Tag.Get("v")
|
||||
if rule == "" {
|
||||
return false
|
||||
}
|
||||
for _, item := range strings.Split(rule, "|") {
|
||||
if strings.HasPrefix(strings.TrimSpace(item), "required") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func requiredMessage(field reflect.StructField, header string) string {
|
||||
rule := field.Tag.Get("v")
|
||||
for _, item := range strings.Split(rule, "|") {
|
||||
item = strings.TrimSpace(item)
|
||||
if !strings.HasPrefix(item, "required") {
|
||||
continue
|
||||
}
|
||||
if parts := strings.SplitN(item, "#", 2); len(parts) == 2 && strings.TrimSpace(parts[1]) != "" {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return header + " is required"
|
||||
}
|
||||
|
||||
func appendIndex(parent []int, i int) []int {
|
||||
index := make([]int, 0, len(parent)+1)
|
||||
index = append(index, parent...)
|
||||
index = append(index, i)
|
||||
return index
|
||||
}
|
||||
|
||||
func derefType(t reflect.Type) reflect.Type {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func normalizeHeader(s string) string {
|
||||
return strings.TrimSpace(strings.ReplaceAll(s, "\uFEFF", ""))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// ApplyHeaderColumnsTextStyle sets all columns with non-empty first-row headers
|
||||
// in the active sheet to Excel text format.
|
||||
func ApplyHeaderColumnsTextStyle(f *excelize.File) error {
|
||||
if f == nil {
|
||||
return gerror.New("Excel文件不能为空")
|
||||
}
|
||||
|
||||
sheetName := f.GetSheetName(f.GetActiveSheetIndex())
|
||||
if sheetName == "" {
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return gerror.New("Excel文件没有工作表")
|
||||
}
|
||||
sheetName = sheets[0]
|
||||
}
|
||||
|
||||
rows, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 || len(rows[0]) == 0 {
|
||||
return gerror.New("Excel文件表头为空")
|
||||
}
|
||||
|
||||
styleID, err := f.NewStyle(&excelize.Style{NumFmt: 49})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, header := range rows[0] {
|
||||
if strings.TrimSpace(header) == "" {
|
||||
continue
|
||||
}
|
||||
colName, err := excelize.ColumnNumberToName(i + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetColStyle(sheetName, colName, styleID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package iot_excelize
|
||||
|
||||
import "github.com/xuri/excelize/v2"
|
||||
|
||||
// BuildOption customizes workbook styling and post-processing.
|
||||
type BuildOption struct {
|
||||
HeaderStyle *excelize.Style
|
||||
RowStyle *excelize.Style
|
||||
ColWidth float64
|
||||
HeaderHeight float64
|
||||
AfterBuild func(f *excelize.File, sheetName string) error
|
||||
}
|
||||
|
||||
func letter(n int) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
var result []string
|
||||
for i := 0; i < n; i++ {
|
||||
result = append(result, colToLetter(i))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func colToLetter(n int) string {
|
||||
result := ""
|
||||
for n >= 0 {
|
||||
result = string(rune('A'+n%26)) + result
|
||||
n = n/26 - 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RowError records one import error at row/column level.
|
||||
type RowError struct {
|
||||
Row int `json:"row"`
|
||||
Column string `json:"column"`
|
||||
Field string `json:"field"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ImportResult contains parsed rows and row-level validation/conversion errors.
|
||||
type ImportResult[T any] struct {
|
||||
Rows []T `json:"rows"`
|
||||
Errors []RowError `json:"errors"`
|
||||
TotalRows int `json:"totalRows"`
|
||||
SuccessRows int `json:"successRows"`
|
||||
FailedRows int `json:"failedRows"`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// WriteHTTP writes an Excel workbook as an attachment to the current HTTP response.
|
||||
func WriteHTTP(ctx context.Context, f *excelize.File, fileName string) (err error) {
|
||||
r := ghttp.RequestFromCtx(ctx)
|
||||
if r == nil {
|
||||
return gerror.New("当前上下文不是HTTP请求")
|
||||
}
|
||||
if fileName == "" {
|
||||
fileName = "export"
|
||||
}
|
||||
|
||||
writer := r.Response.Writer
|
||||
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||
writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", url.QueryEscape(fileName)))
|
||||
writer.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
writer.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
|
||||
if err = f.Write(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Response.Status = http.StatusOK
|
||||
writer.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToReadSeeker serializes an Excel workbook into memory for secondary handling.
|
||||
func ToReadSeeker(f *excelize.File) (io.ReadSeeker, error) {
|
||||
var buffer bytes.Buffer
|
||||
if err := f.Write(&buffer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewReader(buffer.Bytes()), nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gqueue"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gtrace"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/grpool"
|
||||
)
|
||||
|
||||
type tMemoryQueue struct {
|
||||
q *gqueue.Queue
|
||||
p Process
|
||||
grpool *grpool.Pool
|
||||
}
|
||||
|
||||
var (
|
||||
memRwLock sync.RWMutex
|
||||
memQueues = make(map[string]*tMemoryQueue)
|
||||
)
|
||||
|
||||
func RegisterMemoryQueue(ctx context.Context, topic string, p Process, config *Config) *tMemoryQueue {
|
||||
memRwLock.Lock()
|
||||
defer memRwLock.Unlock()
|
||||
q, ok := memQueues[topic]
|
||||
if !ok {
|
||||
q = &tMemoryQueue{
|
||||
q: gqueue.New(),
|
||||
p: p,
|
||||
}
|
||||
memQueues[topic] = q
|
||||
}
|
||||
workers := 1
|
||||
if config != nil && config.Workers > 0 {
|
||||
workers = config.Workers
|
||||
}
|
||||
if p != nil {
|
||||
q.p = p
|
||||
q.grpool = grpool.New(workers)
|
||||
go q.Handle(ctx)
|
||||
}
|
||||
g.Log().Infof(ctx, "RegisterMemoryQueue %s success, workers: %d, process: %v", topic, workers, p)
|
||||
return q
|
||||
}
|
||||
|
||||
func UnregisterMemoryQueue(ctx context.Context, topic string) {
|
||||
memRwLock.Lock()
|
||||
defer memRwLock.Unlock()
|
||||
q, ok := memQueues[topic]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q.q.Close()
|
||||
delete(memQueues, topic)
|
||||
}
|
||||
|
||||
func (q *tMemoryQueue) GetType() QueueType {
|
||||
return QueueTypeMemory
|
||||
}
|
||||
|
||||
func (q *tMemoryQueue) GetTopic() string {
|
||||
return q.p.GetTopic()
|
||||
}
|
||||
|
||||
func (q *tMemoryQueue) Push(ctx context.Context, topic string, data interface{}, expire int64) (err error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := Payload{
|
||||
Ts: time.Now().UnixMilli(),
|
||||
TraceId: gctx.CtxId(ctx),
|
||||
Topic: topic,
|
||||
Data: json.RawMessage(jsonData),
|
||||
Expire: expire,
|
||||
}
|
||||
q.q.Push(payload)
|
||||
return
|
||||
}
|
||||
|
||||
func (q *tMemoryQueue) Handle(ctx context.Context) (err error) {
|
||||
if q.grpool == nil {
|
||||
g.Log().Errorf(ctx, "grpool is nil")
|
||||
return errors.New("grpool is nil")
|
||||
}
|
||||
p := q.p
|
||||
if p == nil {
|
||||
g.Log().Errorf(ctx, "process is nil")
|
||||
return errors.New("process is nil")
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
data := q.q.Pop()
|
||||
if data == nil {
|
||||
break
|
||||
}
|
||||
payload, ok := data.(Payload)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "memqueue process unmarshal error: %v", data)
|
||||
continue
|
||||
}
|
||||
subCtx := gctx.New()
|
||||
gtrace.WithTraceID(subCtx, payload.TraceId)
|
||||
q.grpool.Add(ctx, func(ctx context.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
g.Log().Errorf(ctx, "memqueue process panic: %v, topic: %s, trace_id: %s, hex(data): %v",
|
||||
r, q.p.GetTopic(), payload.TraceId, hex.EncodeToString(payload.Data))
|
||||
}
|
||||
}()
|
||||
err := p.Handle(subCtx, payload)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "memqueue process error: %v, topic: %s, trace_id: %s, hex(data): %v",
|
||||
err, q.p.GetTopic(), payload.TraceId, hex.EncodeToString(payload.Data))
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var ErrNotFoundTopic = errors.New("not found topic")
|
||||
|
||||
type Payload struct {
|
||||
Ts int64 `json:"ts"` // 毫秒时间戳
|
||||
TraceId string `json:"trace_id"` // 链路ID
|
||||
Topic string `json:"topic"` // 消费主题 队列名
|
||||
Data json.RawMessage `json:"data"` // 数据
|
||||
Expire int64 `json:"expire"` // 过期时长毫秒 0:不过期 业务内自行处理
|
||||
ClientId string `json:"client_id"` // 客户端ID
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RedisName string `json:"redis_name"` // redis名称
|
||||
Workers int `json:"workers"` // 工作线程数
|
||||
ClientId string `json:"client_id"` // 客户端ID
|
||||
}
|
||||
|
||||
type Process interface {
|
||||
GetTopic() string // 获取消费主题
|
||||
GetType() QueueType // 获取类型
|
||||
Handle(ctx context.Context, p Payload) (err error) // 处理过程的方法
|
||||
}
|
||||
|
||||
type QueueType string
|
||||
|
||||
const (
|
||||
QueueTypeMemory QueueType = "memory_queue"
|
||||
QueueTypeRedis QueueType = "redis_queue"
|
||||
QueueTypeRedisBroadcast QueueType = "redis_broadcast"
|
||||
)
|
||||
|
||||
type sProducer interface {
|
||||
GetType() QueueType // 获取类型
|
||||
GetTopic() string // 获取主题
|
||||
Push(ctx context.Context, topic string, data interface{}, expire int64) (err error) // 推送消息
|
||||
}
|
||||
|
||||
type sConsumer interface {
|
||||
GetType() QueueType
|
||||
GetTopic() string // 获取消费主题
|
||||
Handle(ctx context.Context) (err error) // 处理过程的方法
|
||||
}
|
||||
|
||||
var (
|
||||
rwLock sync.RWMutex
|
||||
producer = make(map[string]sProducer)
|
||||
consumer = make(map[string]sConsumer)
|
||||
)
|
||||
|
||||
func NewProducer(ctx context.Context, t QueueType, topic string, config *Config) error {
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
if _, ok := producer[topic]; ok {
|
||||
return errors.New("producer already exists")
|
||||
}
|
||||
var p sProducer
|
||||
switch t {
|
||||
case QueueTypeMemory:
|
||||
p = RegisterMemoryQueue(ctx, topic, nil, config)
|
||||
case QueueTypeRedis:
|
||||
p = RegisterRedisQueue(ctx, topic, nil, config)
|
||||
case QueueTypeRedisBroadcast:
|
||||
p = RegisterRedisBroadcast(ctx, topic, nil, config)
|
||||
}
|
||||
rwLock.Lock()
|
||||
producer[topic] = p
|
||||
rwLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewConsumer(ctx context.Context, p Process, config *Config) error {
|
||||
if p == nil {
|
||||
return errors.New("process is nil")
|
||||
}
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
if _, ok := consumer[p.GetTopic()+"__"+config.ClientId]; ok {
|
||||
return errors.New("consumer already exists")
|
||||
}
|
||||
var c sConsumer
|
||||
switch p.GetType() {
|
||||
case QueueTypeMemory:
|
||||
c = RegisterMemoryQueue(ctx, p.GetTopic(), p, config)
|
||||
case QueueTypeRedis:
|
||||
c = RegisterRedisQueue(ctx, p.GetTopic(), p, config)
|
||||
case QueueTypeRedisBroadcast:
|
||||
c = RegisterRedisBroadcast(ctx, p.GetTopic(), p, config)
|
||||
}
|
||||
rwLock.Lock()
|
||||
consumer[p.GetTopic()+"__"+config.ClientId] = c
|
||||
rwLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnregisterProducer(ctx context.Context, topic string) error {
|
||||
rwLock.Lock()
|
||||
defer rwLock.Unlock()
|
||||
p, ok := producer[topic]
|
||||
if !ok {
|
||||
return errors.New("producer not found")
|
||||
}
|
||||
switch p.GetType() {
|
||||
case QueueTypeMemory:
|
||||
UnregisterMemoryQueue(ctx, topic)
|
||||
case QueueTypeRedis:
|
||||
UnregisterRedisQueue(ctx, topic)
|
||||
case QueueTypeRedisBroadcast:
|
||||
UnregisterRedisBroadcast(ctx, topic, "")
|
||||
}
|
||||
delete(producer, topic)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnregisterConsumer(ctx context.Context, topic string, groupId string) error {
|
||||
rwLock.Lock()
|
||||
defer rwLock.Unlock()
|
||||
c, ok := consumer[topic+"__"+groupId]
|
||||
if !ok {
|
||||
return ErrNotFoundTopic
|
||||
}
|
||||
switch c.GetType() {
|
||||
case QueueTypeMemory:
|
||||
UnregisterMemoryQueue(ctx, topic)
|
||||
case QueueTypeRedis:
|
||||
UnregisterRedisQueue(ctx, topic)
|
||||
case QueueTypeRedisBroadcast:
|
||||
UnregisterRedisBroadcast(ctx, topic, groupId)
|
||||
}
|
||||
delete(consumer, topic+"__"+groupId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProducerTopicExists(topic string) bool {
|
||||
rwLock.RLock()
|
||||
_, ok := producer[topic]
|
||||
rwLock.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func ConsumerTopicExists(topic string) bool {
|
||||
rwLock.RLock()
|
||||
_, ok := consumer[topic]
|
||||
rwLock.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func Push(ctx context.Context, topic string, data interface{}) (err error) {
|
||||
rwLock.RLock()
|
||||
p, ok := producer[topic]
|
||||
rwLock.RUnlock()
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "topic not found. topic: %s", topic)
|
||||
return ErrNotFoundTopic
|
||||
}
|
||||
return p.Push(ctx, topic, data, 0)
|
||||
}
|
||||
|
||||
func PushWithExpire(ctx context.Context, topic string, data interface{}, expire int64) (err error) {
|
||||
rwLock.RLock()
|
||||
p, ok := producer[topic]
|
||||
rwLock.RUnlock()
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "topic not found. topic: %s", topic)
|
||||
return ErrNotFoundTopic
|
||||
}
|
||||
return p.Push(ctx, topic, data, expire)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
type testMemoryConsumer struct {
|
||||
}
|
||||
|
||||
func (c *testMemoryConsumer) GetTopic() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
func (c *testMemoryConsumer) GetType() QueueType {
|
||||
return QueueTypeMemory
|
||||
}
|
||||
|
||||
func (c *testMemoryConsumer) Handle(ctx context.Context, p Payload) error {
|
||||
fmt.Println("testMemoryConsumer", p)
|
||||
return nil
|
||||
}
|
||||
|
||||
type testRedisConsumer struct {
|
||||
QueueType QueueType
|
||||
}
|
||||
|
||||
func (c *testRedisConsumer) GetTopic() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
func (c *testRedisConsumer) GetType() QueueType {
|
||||
return c.QueueType
|
||||
}
|
||||
|
||||
func (c *testRedisConsumer) Handle(ctx context.Context, p Payload) error {
|
||||
fmt.Printf("testRedisConsumer %+v \n", p)
|
||||
var data string
|
||||
json.Unmarshal(p.Data, &data)
|
||||
fmt.Printf("testRedisConsumer %+v \n", data)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
NewProducer(ctx, QueueTypeMemory, "test", nil)
|
||||
NewConsumer(ctx, &testMemoryConsumer{}, nil)
|
||||
Push(ctx, "test", "content1")
|
||||
Push(ctx, "test", "content2")
|
||||
Push(ctx, "test", "content3")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestRedisQueue(t *testing.T) {
|
||||
ctx := gctx.New()
|
||||
|
||||
NewProducer(ctx, QueueTypeRedis, "test", &Config{
|
||||
RedisName: "default",
|
||||
})
|
||||
NewConsumer(ctx, &testRedisConsumer{QueueType: QueueTypeRedis}, &Config{
|
||||
RedisName: "default",
|
||||
})
|
||||
time.Sleep(2 * time.Second)
|
||||
Push(ctx, "test", "content1")
|
||||
Push(ctx, "test", "content2")
|
||||
Push(ctx, "test", "content3")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
func TestRedisBroadcast(t *testing.T) {
|
||||
ctx := gctx.New()
|
||||
NewProducer(ctx, QueueTypeRedisBroadcast, "test", &Config{
|
||||
RedisName: "default",
|
||||
})
|
||||
consumerHandle := &testRedisConsumer{QueueType: QueueTypeRedisBroadcast}
|
||||
NewConsumer(ctx, consumerHandle, &Config{
|
||||
RedisName: "default",
|
||||
ClientId: "group1",
|
||||
})
|
||||
NewConsumer(ctx, consumerHandle, &Config{
|
||||
RedisName: "default",
|
||||
ClientId: "group2",
|
||||
})
|
||||
Push(ctx, "test", "content1")
|
||||
Push(ctx, "test", "content2")
|
||||
Push(ctx, "test", "content3")
|
||||
time.Sleep(50 * time.Second)
|
||||
UnregisterConsumer(ctx, "test", "group1")
|
||||
UnregisterConsumer(ctx, "test", "group2")
|
||||
UnregisterProducer(ctx, "test")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/gfredis"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gtrace"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/grpool"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type tRedisBroadcast struct {
|
||||
p Process
|
||||
RedisName string
|
||||
isRunning bool
|
||||
grpool *grpool.Pool
|
||||
clientId string
|
||||
pubSub *redis.PubSub
|
||||
}
|
||||
|
||||
var (
|
||||
redisBroadcastRwLock sync.RWMutex
|
||||
redisBroadcastQueues = make(map[string]*tRedisBroadcast)
|
||||
)
|
||||
|
||||
func RegisterRedisBroadcast(ctx context.Context, topic string, p Process, config *Config) *tRedisBroadcast {
|
||||
redisBroadcastRwLock.Lock()
|
||||
defer redisBroadcastRwLock.Unlock()
|
||||
q, ok := redisBroadcastQueues[topic+"__"+config.ClientId]
|
||||
if ok {
|
||||
q.p = p
|
||||
q.RedisName = config.RedisName
|
||||
q.isRunning = true
|
||||
q.clientId = config.ClientId
|
||||
} else {
|
||||
q = &tRedisBroadcast{
|
||||
p: p,
|
||||
RedisName: config.RedisName,
|
||||
isRunning: true,
|
||||
clientId: config.ClientId,
|
||||
}
|
||||
redisBroadcastQueues[topic+"__"+config.ClientId] = q
|
||||
}
|
||||
workers := 1
|
||||
if config != nil && config.Workers > 0 {
|
||||
workers = config.Workers
|
||||
}
|
||||
if p != nil {
|
||||
q.p = p
|
||||
q.grpool = grpool.New(workers)
|
||||
|
||||
q.pubSub, _ = q.getConn(ctx)
|
||||
go q.Handle(ctx)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func UnregisterRedisBroadcast(ctx context.Context, topic string, clientId string) {
|
||||
redisBroadcastRwLock.Lock()
|
||||
defer redisBroadcastRwLock.Unlock()
|
||||
q, ok := redisBroadcastQueues[topic+"__"+clientId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q.isRunning = false
|
||||
delete(redisBroadcastQueues, topic+"__"+clientId)
|
||||
}
|
||||
|
||||
func (q *tRedisBroadcast) GetType() QueueType {
|
||||
return QueueTypeRedis
|
||||
}
|
||||
|
||||
func (q *tRedisBroadcast) GetTopic() string {
|
||||
return q.p.GetTopic()
|
||||
}
|
||||
|
||||
func (q *tRedisBroadcast) Push(ctx context.Context, topic string, data interface{}, expire int64) (err error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := Payload{
|
||||
Ts: time.Now().UnixMilli(),
|
||||
TraceId: gctx.CtxId(ctx),
|
||||
Topic: topic,
|
||||
Data: json.RawMessage(jsonData),
|
||||
Expire: expire,
|
||||
}
|
||||
payloadJson, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
num, err := gfredis.Instance(q.RedisName).Publish(ctx, topic, payloadJson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "redisqueue Push %s %d", topic, num)
|
||||
return
|
||||
}
|
||||
|
||||
func (q *tRedisBroadcast) getConn(ctx context.Context) (conn *redis.PubSub, err error) {
|
||||
|
||||
conn, err = gfredis.Instance(q.RedisName).Subscribe(ctx, q.p.GetTopic())
|
||||
if err != nil || conn == nil {
|
||||
g.Log().Errorf(ctx, "tRedisBroadcast.Handle Conn %s error: %v", q.p.GetTopic(), err)
|
||||
return
|
||||
}
|
||||
g.Log().Debugf(ctx, "tRedisBroadcast.Handle Subscribe %s 成功, sub:%+v", q.p.GetTopic(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
func (q *tRedisBroadcast) Handle(ctx context.Context) (err error) {
|
||||
g.Log().Debugf(ctx, "tRedisBroadcast.Handle 开始处理 %s, group_id: %s", q.p.GetTopic(), q.clientId)
|
||||
topic := q.p.GetTopic()
|
||||
|
||||
for {
|
||||
if !q.isRunning {
|
||||
break
|
||||
}
|
||||
if q.pubSub == nil {
|
||||
q.pubSub, err = q.getConn(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "tRedisBroadcast.Handle getConn %s error: %v", topic, err)
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
data, err := q.pubSub.ReceiveMessage(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "tRedisBroadcast.Handle ReceiveMessage %s error: %v", topic, err)
|
||||
q.pubSub.Close()
|
||||
q.pubSub = nil
|
||||
continue
|
||||
}
|
||||
var payload Payload
|
||||
err = json.Unmarshal([]byte(data.Payload), &payload)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "tRedisBroadcast.Handle unmarshal error: %v", data)
|
||||
continue
|
||||
}
|
||||
payload.ClientId = q.clientId
|
||||
subCtx := gctx.New()
|
||||
gtrace.WithTraceID(subCtx, payload.TraceId)
|
||||
err = q.grpool.Add(subCtx, func(ctx context.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
g.Log().Errorf(ctx, "tRedisBroadcast.Handle process panic: %v, topic: %s, trace_id: %s, len(data): %d, hex(data): %v",
|
||||
r, q.p.GetTopic(), payload.TraceId, len(payload.Data), hex.EncodeToString(payload.Data))
|
||||
}
|
||||
}()
|
||||
err = q.p.Handle(subCtx, payload)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "tRedisBroadcast.Handle process error: %v, topic: %s, trace_id: %s, len(data): %d, hex(data): %v",
|
||||
err, q.p.GetTopic(), payload.TraceId, len(payload.Data), hex.EncodeToString(payload.Data))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
redisBroadcastRwLock.Lock()
|
||||
delete(redisBroadcastQueues, topic)
|
||||
redisBroadcastRwLock.Unlock()
|
||||
if q.pubSub != nil {
|
||||
q.pubSub.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/gfredis"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gtrace"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/grpool"
|
||||
)
|
||||
|
||||
type tRedisQueue struct {
|
||||
p Process
|
||||
RedisName string
|
||||
isRunning bool
|
||||
grpool *grpool.Pool
|
||||
}
|
||||
|
||||
var (
|
||||
redisRwLock sync.RWMutex
|
||||
redisQueues = make(map[string]*tRedisQueue)
|
||||
)
|
||||
|
||||
func RegisterRedisQueue(ctx context.Context, topic string, p Process, config *Config) *tRedisQueue {
|
||||
redisRwLock.Lock()
|
||||
defer redisRwLock.Unlock()
|
||||
q, ok := redisQueues[topic]
|
||||
if ok {
|
||||
if p != nil {
|
||||
q.p = p
|
||||
}
|
||||
q.RedisName = config.RedisName
|
||||
q.isRunning = true
|
||||
} else {
|
||||
q = &tRedisQueue{
|
||||
p: p,
|
||||
RedisName: config.RedisName,
|
||||
isRunning: true,
|
||||
}
|
||||
redisQueues[topic] = q
|
||||
}
|
||||
workers := 1
|
||||
if config != nil && config.Workers > 0 {
|
||||
workers = config.Workers
|
||||
}
|
||||
if p != nil {
|
||||
q.p = p
|
||||
q.grpool = grpool.New(workers)
|
||||
go q.Handle(ctx)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func UnregisterRedisQueue(ctx context.Context, topic string) {
|
||||
redisRwLock.Lock()
|
||||
defer redisRwLock.Unlock()
|
||||
q, ok := redisQueues[topic]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q.isRunning = false
|
||||
}
|
||||
|
||||
func (q *tRedisQueue) GetType() QueueType {
|
||||
return QueueTypeRedis
|
||||
}
|
||||
|
||||
func (q *tRedisQueue) GetTopic() string {
|
||||
return q.p.GetTopic()
|
||||
}
|
||||
|
||||
func (q *tRedisQueue) Push(ctx context.Context, topic string, data interface{}, expire int64) (err error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := Payload{
|
||||
Ts: time.Now().UnixMilli(),
|
||||
TraceId: gctx.CtxId(ctx),
|
||||
Topic: topic,
|
||||
Data: json.RawMessage(jsonData),
|
||||
Expire: expire,
|
||||
}
|
||||
payloadJson, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = gfredis.Instance(q.RedisName).RPush(ctx, topic, payloadJson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// g.Log().Debugf(ctx, "redisqueue Push %s %d", topic, num)
|
||||
return
|
||||
}
|
||||
|
||||
func (q *tRedisQueue) Handle(ctx context.Context) (err error) {
|
||||
topic := q.p.GetTopic()
|
||||
redisName := q.RedisName
|
||||
for {
|
||||
if !q.isRunning {
|
||||
break
|
||||
}
|
||||
data, err := gfredis.Instance(redisName).BLPop(ctx, 1*time.Second, topic)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "redisqueue Pop %s error: %v", topic, err)
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
arr := data.Strings()
|
||||
if len(arr) < 2 {
|
||||
continue
|
||||
}
|
||||
// g.Log().Debugf(ctx, "redisqueue Pop %s %v", topic, arr)
|
||||
var payload Payload
|
||||
err = json.Unmarshal([]byte(arr[1]), &payload)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "redisqueue process unmarshal error: %v, data: %v, topic: %s",
|
||||
err, data, topic)
|
||||
continue
|
||||
}
|
||||
if q.p == nil {
|
||||
g.Log().Errorf(ctx, "redisqueue process is nil, topic: %s, trace_id: %s, data: %v",
|
||||
topic, payload.TraceId, payload.Data)
|
||||
continue
|
||||
}
|
||||
subCtx := gctx.New()
|
||||
gtrace.WithTraceID(subCtx, payload.TraceId)
|
||||
err = q.grpool.Add(subCtx, func(ctx context.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
g.Log().Errorf(ctx, "redisqueue process panic: %v, topic: %s, trace_id: %s, len(data): %d, hex(data): %v",
|
||||
r, q.p.GetTopic(), payload.TraceId, len(payload.Data), hex.EncodeToString(payload.Data))
|
||||
}
|
||||
}()
|
||||
err = q.p.Handle(subCtx, payload)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "redisqueue process error: %v, topic: %s, trace_id: %s, len(data): %d, hex(data): %v",
|
||||
err, q.p.GetTopic(), payload.TraceId, len(payload.Data), hex.EncodeToString(payload.Data))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
redisRwLock.Lock()
|
||||
delete(redisQueues, topic)
|
||||
redisRwLock.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// package tree
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package tree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
treeDefaultId = "id" // 树ID
|
||||
treeDefaultPid = "pid" // 上级树ID
|
||||
treeDefaultChildren = "children" // 下级属性
|
||||
treeBeginCut = "tr_" // 树标识开头
|
||||
treeEndCut = " " // 树标识结尾
|
||||
)
|
||||
|
||||
// GenOption 生成选项
|
||||
type GenOption struct {
|
||||
IdField string
|
||||
PidField string
|
||||
ChildrenField string
|
||||
}
|
||||
|
||||
// GenLabel 生成关系树标识
|
||||
func GenLabel(basic string, appendId int64) string {
|
||||
return fmt.Sprintf("%v%v%v%v", basic, treeBeginCut, appendId, treeEndCut)
|
||||
}
|
||||
|
||||
// GetIdLabel 获取指定Id的树标签
|
||||
func GetIdLabel(id int64) string {
|
||||
return fmt.Sprintf("%v%v%v", treeBeginCut, id, treeEndCut)
|
||||
}
|
||||
|
||||
// GetIds 获取上级ID集合
|
||||
func GetIds(tree string) (ids []int64) {
|
||||
idsStr := strings.Split(tree, treeEndCut)
|
||||
if len(idsStr) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range idsStr {
|
||||
newId := gconv.Int64(strings.ReplaceAll(v, treeBeginCut, ""))
|
||||
if newId > 0 {
|
||||
ids = append(ids, newId)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GenTree 生成关系树
|
||||
func GenTree(menus []map[string]interface{}) (realMenu []map[string]interface{}) {
|
||||
return GenTreeWithField(menus, GenOption{
|
||||
IdField: treeDefaultId,
|
||||
PidField: treeDefaultPid,
|
||||
ChildrenField: treeDefaultChildren,
|
||||
})
|
||||
}
|
||||
|
||||
// GenTreeWithField 生成关系树 自定义生成属性
|
||||
func GenTreeWithField(menus []map[string]interface{}, op GenOption) (realMenu []map[string]interface{}) {
|
||||
if len(menus) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
minPid := GetMinPid(menus, op.PidField)
|
||||
formatMenu := make(map[int]map[string]interface{})
|
||||
for _, m := range menus {
|
||||
formatMenu[gconv.Int(m[op.IdField])] = m
|
||||
if gconv.Int(m[op.PidField]) == minPid {
|
||||
realMenu = append(realMenu, m) // 需要返回的顶级菜单
|
||||
}
|
||||
}
|
||||
// 得益于都是地址操作,可以直接往父级塞
|
||||
for _, m := range formatMenu {
|
||||
if formatMenu[gconv.Int(m[op.PidField])] == nil {
|
||||
continue
|
||||
}
|
||||
if formatMenu[gconv.Int(m[op.PidField])][op.ChildrenField] == nil {
|
||||
formatMenu[gconv.Int(m[op.PidField])][op.ChildrenField] = []map[string]interface{}{}
|
||||
}
|
||||
|
||||
formatMenu[gconv.Int(m[op.PidField])][op.ChildrenField] = append(formatMenu[gconv.Int(m[op.PidField])][op.ChildrenField].([]map[string]interface{}), m)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetMinPid(menus []map[string]interface{}, pidField string) int {
|
||||
index := -1
|
||||
for _, m := range menus {
|
||||
if index == -1 {
|
||||
index = gconv.Int(m[pidField])
|
||||
continue
|
||||
}
|
||||
if gconv.Int(m[pidField]) < index {
|
||||
index = gconv.Int(m[pidField])
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return 0
|
||||
}
|
||||
return index
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// package tree
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package tree
|
||||
|
||||
type Node interface {
|
||||
ID() int64
|
||||
PID() int64
|
||||
SetChildren([]Node)
|
||||
}
|
||||
|
||||
func ListToTree(rootPid int64, nodes []Node) []Node {
|
||||
nodeMap := make(map[int64]Node, len(nodes))
|
||||
childrenMap := make(map[int64][]Node)
|
||||
for _, node := range nodes {
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
nodeMap[node.ID()] = node
|
||||
}
|
||||
|
||||
treeList := make([]Node, 0)
|
||||
for _, node := range nodes {
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
if node.PID() == rootPid {
|
||||
treeList = append(treeList, node)
|
||||
continue
|
||||
}
|
||||
if parent, ok := nodeMap[node.PID()]; ok {
|
||||
childrenMap[parent.ID()] = append(childrenMap[parent.ID()], node)
|
||||
}
|
||||
}
|
||||
for parentId, children := range childrenMap {
|
||||
if parent, ok := nodeMap[parentId]; ok {
|
||||
parent.SetChildren(children)
|
||||
}
|
||||
}
|
||||
return treeList
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// package utility
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package utility
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/encoding/gcharset"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/mileusna/useragent"
|
||||
"golang.org/x/exp/rand"
|
||||
)
|
||||
|
||||
func Md5(str string) string {
|
||||
return gmd5.MustEncryptString(str)
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
func PasswordEncrypt(password string, salt string) string {
|
||||
return Md5(password + salt)
|
||||
}
|
||||
|
||||
// 解析时间
|
||||
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")
|
||||
|
||||
hour, _ := strconv.Atoi(d[:index])
|
||||
dr = time.Hour * 24 * time.Duration(hour)
|
||||
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
|
||||
}
|
||||
|
||||
// 随机生成5位字符
|
||||
func RandomString(n int) string {
|
||||
letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
func GetClientIp(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if ghttp.RequestFromCtx(ctx) == nil {
|
||||
return ""
|
||||
}
|
||||
return ghttp.RequestFromCtx(ctx).GetClientIp()
|
||||
}
|
||||
|
||||
// 获取user-agent
|
||||
func GetUserAgent(ctx context.Context) string {
|
||||
return ghttp.RequestFromCtx(ctx).Header.Get("User-Agent")
|
||||
}
|
||||
|
||||
// 获取操作系统
|
||||
func GetOs(ctx context.Context) string {
|
||||
userAgent := GetUserAgent(ctx)
|
||||
ua := useragent.Parse(userAgent)
|
||||
return ua.OS
|
||||
}
|
||||
|
||||
// 获取浏览器
|
||||
func GetBrowser(ctx context.Context) string {
|
||||
userAgent := GetUserAgent(ctx)
|
||||
ua := useragent.Parse(userAgent)
|
||||
return ua.Name
|
||||
}
|
||||
|
||||
// 获取ip所属城市
|
||||
func GetCityByIp(ip string) string {
|
||||
if ip == "" {
|
||||
return ""
|
||||
}
|
||||
if ip == "[::1]" || ip == "127.0.0.1" {
|
||||
return "内网IP"
|
||||
}
|
||||
url := "https://whois.pconline.com.cn/ipJson.jsp?json=true&ip=" + ip
|
||||
bytes := g.Client().GetBytes(context.TODO(), url)
|
||||
src := string(bytes)
|
||||
srcCharset := "GBK"
|
||||
tmp, _ := gcharset.ToUTF8(srcCharset, src)
|
||||
json, err := gjson.DecodeToJson(tmp)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if json.Get("code").Int() == 0 {
|
||||
city := ""
|
||||
if strings.EqualFold(json.Get("pro").String(), json.Get("city").String()) {
|
||||
city = fmt.Sprintf("%s", json.Get("pro").String())
|
||||
} else {
|
||||
city = fmt.Sprintf("%s %s", json.Get("pro").String(), json.Get("city").String())
|
||||
}
|
||||
|
||||
return city
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 时间戳转 yyyy-MM-dd HH:mm:ss
|
||||
func TimeStampToDateTime(timeStamp int64) string {
|
||||
tm := gtime.NewFromTimeStamp(timeStamp)
|
||||
return tm.Format("Y-m-d H:i:s")
|
||||
}
|
||||
|
||||
// 时间戳转 yyyy-MM-dd
|
||||
func TimeStampToDate(timeStamp int64) string {
|
||||
tm := gtime.NewFromTimeStamp(timeStamp)
|
||||
return tm.Format("Y-m-d")
|
||||
}
|
||||
|
||||
// 服务端ip
|
||||
func GetLocalIP() (ip string, err error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipAddr, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipAddr.IP.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
if !ipAddr.IP.IsGlobalUnicast() {
|
||||
continue
|
||||
}
|
||||
return ipAddr.IP.String(), nil
|
||||
}
|
||||
return
|
||||
}
|
||||
func GetPublicIP() (ip string, err error) {
|
||||
resp, err := http.Get("https://ifconfig.co/ip")
|
||||
if err != nil {
|
||||
fmt.Println("GetPublicIP", err)
|
||||
return
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
if err := Body.Close(); err != nil {
|
||||
fmt.Println("GetPublicIP Body.Close", err)
|
||||
}
|
||||
}(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("GetPublicIP ReadAll", err)
|
||||
return
|
||||
}
|
||||
ip = string(body)
|
||||
// 去除空格
|
||||
ip = strings.Replace(ip, " ", "", -1)
|
||||
// 去除换行符
|
||||
ip = strings.Replace(ip, "\n", "", -1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 获取两个字符串之间的字符串
|
||||
func GetBetweenStr(str, start, end string) (result string) {
|
||||
startIndex := strings.Index(str, start)
|
||||
if startIndex == -1 {
|
||||
return ""
|
||||
}
|
||||
// 更新startIndex为开始字符串之后的位置
|
||||
startIndex += len(start)
|
||||
|
||||
// 找到结束字符串的位置,从更新后的startIndex开始查找
|
||||
endIndex := strings.Index(str[startIndex:], end)
|
||||
if endIndex == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 切片操作获取中间的字符串
|
||||
result = str[startIndex : startIndex+endIndex]
|
||||
return
|
||||
}
|
||||
|
||||
// A和B两个数组,A数组中存在,B数组中不存在
|
||||
func ArrayRightDiff(a, b []int64) []int64 {
|
||||
diff := make([]int64, 0)
|
||||
for _, v := range a {
|
||||
if !slices.Contains(b, v) {
|
||||
diff = append(diff, v)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
}
|
||||
|
||||
// WalkDir 获取目录下文件的名称和大小
|
||||
func WalkDir(dirname string) ([]fileInfo, error) {
|
||||
var fileInfos []fileInfo
|
||||
err := filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
fileInfos = append(fileInfos, fileInfo{name: path, size: info.Size()})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return fileInfos, err
|
||||
}
|
||||
func DirSize(dirname string) string {
|
||||
var (
|
||||
s int64
|
||||
files, _ = WalkDir(dirname)
|
||||
)
|
||||
for _, n := range files {
|
||||
s += n.size
|
||||
}
|
||||
return FileSize(s)
|
||||
}
|
||||
func MergeAbs(path string, fileName ...string) string {
|
||||
var paths = []string{gfile.RealPath(path)}
|
||||
paths = append(paths, fileName...)
|
||||
return gfile.Join(paths...)
|
||||
}
|
||||
|
||||
// FileSize 字节的单位转换 保留两位小数
|
||||
func FileSize(fileSize int64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "EB"}
|
||||
var size = float64(fileSize)
|
||||
var i int
|
||||
for i = 0; size > 1024; i++ {
|
||||
size /= 1024
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", size, units[i])
|
||||
}
|
||||
|
||||
// UniqueSlice 切片去重
|
||||
func UniqueSlice[K comparable](languages []K) []K {
|
||||
result := make([]K, 0, len(languages))
|
||||
temp := map[K]struct{}{}
|
||||
for _, item := range languages {
|
||||
if _, ok := temp[item]; !ok {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package uuid32
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func New() string {
|
||||
return strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||
}
|
||||
|
||||
func S() string {
|
||||
return New()
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// package version
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package version
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var (
|
||||
BuildTime string
|
||||
BuildVersion string
|
||||
CommitID string
|
||||
)
|
||||
|
||||
func ShowLogo(buildVersion, buildTime, commitID string) {
|
||||
BuildVersion = buildVersion
|
||||
BuildTime = buildTime
|
||||
CommitID = commitID
|
||||
//版本号
|
||||
|
||||
fmt.Println(" __ __ _ _ _ _ \n \\ \\/ /(_) _ _ / \\ __| | _ __ ___ (_) _ __ \n \\ / | || | | | / _ \\ / _` || '_ ` _ \\ | || '_ \\ \n / \\ | || |_| | / ___ \\| (_| || | | | | || || | | |\n /_/\\_\\|_| \\__,_| /_/ \\_\\\\__,_||_| |_| |_||_||_| |_|")
|
||||
fmt.Println("")
|
||||
|
||||
fmt.Println("Version :", buildVersion)
|
||||
fmt.Println("BuildTime :", buildTime)
|
||||
fmt.Println("CommitID :", commitID)
|
||||
fmt.Println("")
|
||||
fmt.Println("Copyright:", "XiuJieZhiLian Technology Co.,Ltd")
|
||||
}
|
||||
|
||||
func AppName(ctx context.Context) string {
|
||||
return g.Cfg().MustGet(ctx, "system.appName", "XiuAdmin").String()
|
||||
}
|
||||
Reference in New Issue
Block a user