This commit is contained in:
yvan 2026-08-16 01:19:10 +08:00
parent c4b114762b
commit c554152f4f
43 changed files with 1315 additions and 322 deletions

View File

@ -72,9 +72,9 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
cleanup()
return nil, nil, err
}
mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage)
mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage, runtimeSettings)
taskExecutor := worker.NewTaskExecutor(taskUsecase, mediaUsecase, runtime)
taskScheduler := worker.NewTaskScheduler(taskUsecase, taskExecutor, logger)
taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger)
taskRuntime := worker.NewTaskRuntime(taskScheduler)
taskApplicationUsecase := biz.NewTaskApplicationUsecase(taskUsecase, taskRuntime)
taskService := service.NewTaskService(taskApplicationUsecase)

View File

@ -104,12 +104,16 @@ admin:
# Upload chunks are stored below .chunks in the selected storage backend.
session_ttl: 24
max_file_size: 0
chunk_dir: .chunks
system:
use_redis: false
use_multipoint: false
use_strict_auth: false
disable_auto_migrate: false
use_mongo: false
addr: 8000
iplimit_count: 0
iplimit_time: 0
zap:
level: info
prefix: "[kra] "

View File

@ -36,9 +36,20 @@ func (uc *AuthorityUsecase) AuthorityTree(ctx context.Context) ([]*Authority, er
if item.ParentID != nil && *item.ParentID != 0 && byID[*item.ParentID] != nil {
parent := byID[*item.ParentID]
parent.Children = append(parent.Children, item)
} else {
} else if item.ParentID == nil || *item.ParentID == 0 {
roots = append(roots, item)
}
}
// In GVA strict-authority mode a non-root actor receives its direct
// children as the top-level result even though their parent is omitted.
if len(roots) == 0 {
if actor, ok := ActorFromContext(ctx); ok {
for _, item := range items {
if item.ParentID != nil && *item.ParentID == actor.AuthorityID {
roots = append(roots, item)
}
}
}
}
return roots, nil
}

View File

@ -17,10 +17,12 @@ type Cache interface {
}
type StoredFile struct {
Name string
Path string
URL string
Size int64
Name string
Path string
URL string
Size int64
LastModified time.Time
ContentType string
}
// FileStorage owns the persistence boundary for uploaded files.
@ -50,6 +52,7 @@ type CaptchaSettings struct {
type MediaSettings struct {
SessionTTL int
MaxFileSize int64
ChunkDir string
}
// RuntimeSettings exposes only the active values needed by the application.
@ -75,6 +78,7 @@ type AuthClaims struct {
Username string
NickName string
AuthorityID uint
UserType string
BufferTime time.Duration
MustChangePwd bool
Issuer string

View File

@ -22,11 +22,12 @@ type MediaRepo interface {
type MediaUsecase struct {
MediaRepo
files FileStorage
files FileStorage
settings RuntimeSettings
}
func NewMediaUsecase(repo MediaRepo, files FileStorage) *MediaUsecase {
return &MediaUsecase{MediaRepo: repo, files: files}
func NewMediaUsecase(repo MediaRepo, files FileStorage, settings RuntimeSettings) *MediaUsecase {
return &MediaUsecase{MediaRepo: repo, files: files, settings: settings}
}
var allowedMediaExtensions = map[string]bool{

View File

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
"path"
"path/filepath"
"sort"
"strings"
@ -15,6 +16,20 @@ import (
"github.com/google/uuid"
)
func (uc *MediaUsecase) chunkPrefix(uploadID uint) string {
directory := ".chunks"
if uc.settings != nil {
if configured := strings.Trim(uc.settings.MediaSettings().ChunkDir, "/\\ "); configured != "" {
directory = configured
}
}
return path.Join(directory, fmt.Sprintf("%d", uploadID))
}
func (uc *MediaUsecase) chunkKey(uploadID uint, index int) string {
return path.Join(uc.chunkPrefix(uploadID), fmt.Sprintf("%08d", index))
}
func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash string, size, chunkSize int64, total int) (*UploadSession, *MediaFile, []int, error) {
if err := validateMediaName(name); err != nil {
return nil, nil, nil, err
@ -57,7 +72,7 @@ func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, in
return errors.New("上传会话状态不允许收片")
}
hash := md5.New()
key := fmt.Sprintf(".chunks/%d/%08d", uploadID, index)
key := uc.chunkKey(uploadID, index)
stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash))
if err != nil {
return err
@ -95,7 +110,7 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
if chunk.Index != index {
return fail(errors.New("分片序号不连续"))
}
names = append(names, fmt.Sprintf(".chunks/%d/%08d", uploadID, index))
names = append(names, uc.chunkKey(uploadID, index))
}
ext := strings.ToLower(filepath.Ext(session.FileName))
key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
@ -114,7 +129,7 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
}
_ = uc.CompleteUploadSession(ctx, uploadID, key, media.ID)
_ = uc.DeleteChunks(ctx, uploadID)
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
_ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID))
return media, nil
}
func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint) error {
@ -123,7 +138,7 @@ func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint)
return errors.New("上传会话不存在或无权操作")
}
_ = uc.DeleteChunks(ctx, uploadID)
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID))
_ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID))
return uc.DeleteUploadSession(ctx, uploadID)
}
func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error {
@ -138,7 +153,7 @@ func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error {
if err = uc.DeleteUploadData(ctx, id); err != nil {
return err
}
if err = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", id)); err != nil {
if err = uc.files.DeletePrefix(ctx, uc.chunkPrefix(id)); err != nil {
return err
}
}

View File

@ -91,8 +91,8 @@ type TaskRuntime interface {
TriggerID(context.Context, uint) error
NextRuns() map[uint]time.Time
Reload(context.Context) error
Subscribe() chan []byte
Unsubscribe(chan []byte)
Subscribe(uint) chan []byte
Unsubscribe(uint, chan []byte)
}
type TaskUsecase struct{ TaskRepo }
@ -115,7 +115,7 @@ func (uc *TaskUsecase) Validate(value *TimedTask) error {
switch value.ExecutorType {
case TaskExecutorMethod:
if !registeredTaskMethod(value.MethodName) {
return errors.New("方法未注册")
return fmt.Errorf("方法 %s 未注册", value.MethodName)
}
if len(value.Params) > 0 && !json.Valid(value.Params) {
return errors.New("params 必须是合法 JSON")
@ -128,7 +128,7 @@ func (uc *TaskUsecase) Validate(value *TimedTask) error {
if len(value.HTTPHeader) > 0 {
headers := map[string]string{}
if json.Unmarshal(value.HTTPHeader, &headers) != nil {
return errors.New("httpHeader 必须是 JSON 对象")
return errors.New(`httpHeader 必须是 {"Key":"Value"} 形式的 JSON 对象`)
}
}
default:
@ -229,7 +229,9 @@ func (uc *TaskApplicationUsecase) Reload(ctx context.Context) error {
return uc.runtime.Reload(ctx)
}
func (uc *TaskApplicationUsecase) Subscribe() chan []byte { return uc.runtime.Subscribe() }
func (uc *TaskApplicationUsecase) Unsubscribe(events chan []byte) {
uc.runtime.Unsubscribe(events)
func (uc *TaskApplicationUsecase) Subscribe(userID uint) chan []byte {
return uc.runtime.Subscribe(userID)
}
func (uc *TaskApplicationUsecase) Unsubscribe(userID uint, events chan []byte) {
uc.runtime.Unsubscribe(userID, events)
}

View File

@ -1168,6 +1168,7 @@ type AdminBackend_Media struct {
state protoimpl.MessageState `protogen:"open.v1"`
SessionTtl int32 `protobuf:"varint,1,opt,name=session_ttl,json=sessionTtl,proto3" json:"session_ttl,omitempty"`
MaxFileSize int64 `protobuf:"varint,2,opt,name=max_file_size,json=maxFileSize,proto3" json:"max_file_size,omitempty"`
ChunkDir string `protobuf:"bytes,3,opt,name=chunk_dir,json=chunkDir,proto3" json:"chunk_dir,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1216,6 +1217,13 @@ func (x *AdminBackend_Media) GetMaxFileSize() int64 {
return 0
}
func (x *AdminBackend_Media) GetChunkDir() string {
if x != nil {
return x.ChunkDir
}
return ""
}
type AdminBackend_Disk struct {
state protoimpl.MessageState `protogen:"open.v1"`
MountPoint string `protobuf:"bytes,1,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"`
@ -1267,6 +1275,9 @@ type AdminBackend_System struct {
UseStrictAuth bool `protobuf:"varint,3,opt,name=use_strict_auth,json=useStrictAuth,proto3" json:"use_strict_auth,omitempty"`
DisableAutoMigrate bool `protobuf:"varint,4,opt,name=disable_auto_migrate,json=disableAutoMigrate,proto3" json:"disable_auto_migrate,omitempty"`
UseMongo bool `protobuf:"varint,5,opt,name=use_mongo,json=useMongo,proto3" json:"use_mongo,omitempty"`
Addr int32 `protobuf:"varint,6,opt,name=addr,proto3" json:"addr,omitempty"`
IplimitCount int32 `protobuf:"varint,7,opt,name=iplimit_count,json=iplimitCount,proto3" json:"iplimit_count,omitempty"`
IplimitTime int32 `protobuf:"varint,8,opt,name=iplimit_time,json=iplimitTime,proto3" json:"iplimit_time,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1336,6 +1347,27 @@ func (x *AdminBackend_System) GetUseMongo() bool {
return false
}
func (x *AdminBackend_System) GetAddr() int32 {
if x != nil {
return x.Addr
}
return 0
}
func (x *AdminBackend_System) GetIplimitCount() int32 {
if x != nil {
return x.IplimitCount
}
return 0
}
func (x *AdminBackend_System) GetIplimitTime() int32 {
if x != nil {
return x.IplimitTime
}
return 0
}
type AdminBackend_Zap struct {
state protoimpl.MessageState `protogen:"open.v1"`
Level string `protobuf:"bytes,1,opt,name=level,proto3" json:"level,omitempty"`
@ -2054,7 +2086,7 @@ const file_conf_conf_proto_rawDesc = "" +
"\x12connect_timeout_ms\x18\n" +
" \x01(\x03R\x10connectTimeoutMs\x12\x15\n" +
"\x06is_zap\x18\v \x01(\bR\x05isZap\x120\n" +
"\x05hosts\x18\f \x03(\v2\x1a.kratos.api.Data.MongoHostR\x05hosts\"\xb4\x1b\n" +
"\x05hosts\x18\f \x03(\v2\x1a.kratos.api.Data.MongoHostR\x05hosts\"\xad\x1c\n" +
"\fAdminBackend\x12#\n" +
"\rrouter_prefix\x18\x01 \x01(\tR\frouterPrefix\x12.\n" +
"\x03jwt\x18\x02 \x01(\v2\x1c.kratos.api.AdminBackend.JWTR\x03jwt\x12:\n" +
@ -2097,20 +2129,24 @@ const file_conf_conf_proto_rawDesc = "" +
"\bnickname\x18\x05 \x01(\tR\bnickname\x12\x12\n" +
"\x04port\x18\x06 \x01(\x05R\x04port\x12\x15\n" +
"\x06is_ssl\x18\a \x01(\bR\x05isSsl\x12\"\n" +
"\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1aL\n" +
"\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1ai\n" +
"\x05Media\x12\x1f\n" +
"\vsession_ttl\x18\x01 \x01(\x05R\n" +
"sessionTtl\x12\"\n" +
"\rmax_file_size\x18\x02 \x01(\x03R\vmaxFileSize\x1a'\n" +
"\rmax_file_size\x18\x02 \x01(\x03R\vmaxFileSize\x12\x1b\n" +
"\tchunk_dir\x18\x03 \x01(\tR\bchunkDir\x1a'\n" +
"\x04Disk\x12\x1f\n" +
"\vmount_point\x18\x01 \x01(\tR\n" +
"mountPoint\x1a\xc3\x01\n" +
"mountPoint\x1a\x9f\x02\n" +
"\x06System\x12\x1b\n" +
"\tuse_redis\x18\x01 \x01(\bR\buseRedis\x12%\n" +
"\x0euse_multipoint\x18\x02 \x01(\bR\ruseMultipoint\x12&\n" +
"\x0fuse_strict_auth\x18\x03 \x01(\bR\ruseStrictAuth\x120\n" +
"\x14disable_auto_migrate\x18\x04 \x01(\bR\x12disableAutoMigrate\x12\x1b\n" +
"\tuse_mongo\x18\x05 \x01(\bR\buseMongo\x1a\xf6\x03\n" +
"\tuse_mongo\x18\x05 \x01(\bR\buseMongo\x12\x12\n" +
"\x04addr\x18\x06 \x01(\x05R\x04addr\x12#\n" +
"\riplimit_count\x18\a \x01(\x05R\fiplimitCount\x12!\n" +
"\fiplimit_time\x18\b \x01(\x05R\viplimitTime\x1a\xf6\x03\n" +
"\x03Zap\x12\x14\n" +
"\x05level\x18\x01 \x01(\tR\x05level\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\tR\x06prefix\x12\x16\n" +

View File

@ -127,6 +127,7 @@ message AdminBackend {
message Media {
int32 session_ttl = 1;
int64 max_file_size = 2;
string chunk_dir = 3;
}
message Disk {
@ -139,6 +140,9 @@ message AdminBackend {
bool use_strict_auth = 3;
bool disable_auto_migrate = 4;
bool use_mongo = 5;
int32 addr = 6;
int32 iplimit_count = 7;
int32 iplimit_time = 8;
}
message Zap {

View File

@ -94,7 +94,7 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
Label string
Value uint
}
err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Where("deleted_at IS NULL").Scan(&rows).Error
err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}

View File

@ -98,7 +98,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
return err
}
}
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
for _, item := range defaults {
var api apiPO
if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil {
@ -186,13 +186,23 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
})
}
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
return r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Updates(map[string]any{"authority_name": value.AuthorityName, "parent_id": value.ParentID, "data_scope": value.DataScope, "default_router": value.DefaultRouter}).Error
db := r.data.gormDB.WithContext(ctx)
var current authorityPO
if err := db.Where("authority_id = ?", value.AuthorityID).First(&current).Error; err != nil {
return errors.New("查询角色数据失败")
}
updates := &authorityPO{AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
return db.Model(&current).Updates(updates).Error
}
func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) error {
if id == 888 {
return errors.New("不能删除超级管理员")
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var authority authorityPO
if err := tx.Where("authority_id = ?", id).First(&authority).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("该角色不存在")
}
return err
}
var users, children int64
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Count(&users).Error; err != nil {
return err
@ -221,15 +231,12 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
return err
}
return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
})
}
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
var pos []authorityPO
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err
}
var allowed map[uint]bool

View File

@ -66,6 +66,16 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
}
}
maskDataSecrets(dataConfig)
safeAdmin := cloneAdminConfig(adminConfig)
if safeAdmin != nil {
if safeAdmin.Jwt != nil {
safeAdmin.Jwt.SigningKey = "******"
}
if safeAdmin.Email != nil {
safeAdmin.Email.Secret = "******"
}
maskStorageSecrets(safeAdmin.Storage)
}
dataMap := map[string]any{}
if dataConfig != nil {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig)
@ -76,7 +86,9 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
return nil, err
}
}
return json.Marshal(map[string]any{"config": map[string]any{"admin": admin, "email": email, "data": dataMap}})
config := gvaConfiguration(dataConfig, safeAdmin)
config["admin"], config["email"], config["data"] = admin, email, dataMap
return json.Marshal(map[string]any{"config": config})
}
func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error {
@ -86,6 +98,9 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
}
nextData := cloneDataConfig(currentData)
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
if err := applyGVAConfiguration(raw, nextData, nextAdmin); err != nil {
return err
}
var value configurationEnvelope
if err := json.Unmarshal(raw, &value); err != nil {
return err
@ -154,6 +169,13 @@ func cloneDataConfig(value *conf.Data) *conf.Data {
return proto.Clone(value).(*conf.Data)
}
func cloneAdminConfig(value *conf.AdminBackend) *conf.AdminBackend {
if value == nil {
return &conf.AdminBackend{}
}
return proto.Clone(value).(*conf.AdminBackend)
}
func refreshDatabaseSources(value *conf.Data) error {
if value == nil {
return nil
@ -222,6 +244,11 @@ func applyDataPatch(target, patch *conf.Data) {
func applyAdminPatch(target, patch *conf.AdminBackend) {
target.RouterPrefix = patch.RouterPrefix
if patch.System != nil {
if target.System != nil {
patch.System.Addr = target.System.Addr
patch.System.IplimitCount = target.System.IplimitCount
patch.System.IplimitTime = target.System.IplimitTime
}
target.System = patch.System
}
if patch.Jwt != nil {
@ -234,6 +261,9 @@ func applyAdminPatch(target, patch *conf.AdminBackend) {
target.Local = patch.Local
}
if patch.Media != nil {
if patch.Media.ChunkDir == "" && target.Media != nil {
patch.Media.ChunkDir = target.Media.ChunkDir
}
target.Media = patch.Media
}
if patch.Storage != nil {

View File

@ -4,8 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"kra/internal/conf"
@ -84,6 +86,45 @@ func mergeYAMLNode(dst, src *yaml.Node) {
dst.HeadComment, dst.LineComment, dst.FootComment = head, line, foot
}
func yamlMappingValue(node *yaml.Node, keys ...string) *yaml.Node {
if node == nil {
return nil
}
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
node = node.Content[0]
}
for _, key := range keys {
if node.Kind != yaml.MappingNode {
return nil
}
var next *yaml.Node
for i := 0; i+1 < len(node.Content); i += 2 {
if node.Content[i].Value == key {
next = node.Content[i+1]
break
}
}
if next == nil {
return nil
}
node = next
}
return node
}
func setServerHTTPPort(document *yaml.Node, port int32) error {
if port <= 0 {
return nil
}
host := "0.0.0.0"
if addr := yamlMappingValue(document, "server", "http", "addr"); addr != nil {
if currentHost, _, err := net.SplitHostPort(addr.Value); err == nil && currentHost != "" {
host = currentHost
}
}
return setYAMLMapping(document, "server", map[string]any{"http": map[string]any{"addr": net.JoinHostPort(host, strconv.Itoa(int(port)))}})
}
func (d *Data) persistConfig() error {
dataConfig, adminConfig := d.runtime.Values()
return d.persistConfigValues(dataConfig, adminConfig)
@ -122,6 +163,11 @@ func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *con
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
return err
}
if adminConfig.System != nil {
if err = setServerHTTPPort(&document, adminConfig.System.Addr); err != nil {
return err
}
}
return writeConfigDocument(configPath, &document)
}

View File

@ -108,12 +108,6 @@ func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
if count > 0 {
return errors.New("存在子部门,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("该部门下存在用户,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
return err
}
@ -135,7 +129,7 @@ func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Depa
}
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
var pos []departmentPO
db := r.data.gormDB.WithContext(ctx).Order("sort,id")
db := r.data.gormDB.WithContext(ctx).Order("sort")
if name != "" {
db = db.Where("name LIKE ?", "%"+name+"%")
}
@ -160,7 +154,7 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
n := nodes[po.ID]
if p := nodes[po.ParentID]; p != nil {
p.Children = append(p.Children, n)
} else {
} else if po.ParentID == 0 {
roots = append(roots, n)
}
}

View File

@ -111,19 +111,18 @@ func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary
return r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
}
func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&dictionaryPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
db := r.data.gormDB.WithContext(ctx)
var dictionary dictionaryPO
if err := db.First(&dictionary, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("请不要搞事")
}
if count > 0 {
return errors.New("存在子字典不可删除")
}
if err := tx.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error; err != nil {
return err
}
return tx.Delete(&dictionaryPO{}, id).Error
})
return err
}
if err := db.Delete(&dictionary).Error; err != nil {
return err
}
return db.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error
}
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
var po dictionaryPO

View File

@ -111,21 +111,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
})
}
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var tids []string
if err := tx.Model(&exportTemplatePO{}).Where("id IN ?", ids).Pluck("template_id", &tids).Error; err != nil {
return err
}
if len(tids) > 0 {
if err := tx.Where("template_id IN ?", tids).Delete(&exportConditionPO{}).Error; err != nil {
return err
}
if err := tx.Where("template_id IN ?", tids).Delete(&exportJoinPO{}).Error; err != nil {
return err
}
}
return tx.Delete(&exportTemplatePO{}, ids).Error
})
return r.data.gormDB.WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).Error
}
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
var po exportTemplatePO
@ -173,31 +159,9 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
if err := applyPagination(db, page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
templateIDs := make([]string, 0, len(pos))
for _, po := range pos {
templateIDs = append(templateIDs, po.TemplateID)
}
var conditions []exportConditionPO
var joins []exportJoinPO
if len(templateIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&conditions).Error; err != nil {
return nil, 0, err
}
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&joins).Error; err != nil {
return nil, 0, err
}
}
conditionsByTemplate := make(map[string][]exportConditionPO, len(pos))
for _, condition := range conditions {
conditionsByTemplate[condition.TemplateID] = append(conditionsByTemplate[condition.TemplateID], condition)
}
joinsByTemplate := make(map[string][]exportJoinPO, len(pos))
for _, join := range joins {
joinsByTemplate[join.TemplateID] = append(joinsByTemplate[join.TemplateID], join)
}
out := make([]*biz.ExportTemplate, 0, len(pos))
for _, po := range pos {
out = append(out, exportFromPO(po, conditionsByTemplate[po.TemplateID], joinsByTemplate[po.TemplateID]))
out = append(out, exportFromPO(po, nil, nil))
}
return out, total, nil
}

View File

@ -0,0 +1,545 @@
package data
import (
"encoding/json"
"strconv"
"strings"
"time"
"kra/internal/conf"
"google.golang.org/protobuf/types/known/durationpb"
)
func gvaDatabase(value *conf.Data_Database) map[string]any {
if value == nil {
return map[string]any{}
}
path := value.Host
if value.Driver == "sqlite" {
path = value.Path
}
return map[string]any{
"prefix": value.Prefix, "port": value.Port, "config": value.Config,
"db-name": value.Name, "username": value.User, "password": value.Password,
"path": path, "engine": value.Engine, "log-mode": value.LogMode,
"max-idle-conns": value.MaxIdleConns, "max-open-conns": value.MaxOpenConns,
"conn-max-lifetime": value.ConnMaxLifetime, "singular": value.Singular,
}
}
func gvaRedis(value *conf.Data_Redis) map[string]any {
if value == nil {
return map[string]any{}
}
return map[string]any{"name": value.Name, "addr": value.Addr, "password": value.Password, "db": value.Db, "useCluster": value.UseCluster, "clusterAddrs": value.ClusterAddrs}
}
func gvaObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
if value == nil {
return map[string]any{}
}
return map[string]any{
"endpoint": value.Endpoint, "region": value.Region, "bucket": value.Bucket,
"access-key": value.AccessKey, "secret-key": value.SecretKey, "base-url": value.BaseUrl,
"path-prefix": value.PathPrefix, "use-ssl": value.UseSsl,
"s3-force-path-style": value.ForcePathStyle, "account-id": value.AccountId,
}
}
func gvaObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]any {
if storage == nil {
storage = &conf.AdminBackend_Storage{}
}
aliyun, huawei := storage.AliyunOss, storage.HuaweiObs
tencent, aws := storage.TencentCos, storage.AwsS3
cloudflare, minio := storage.CloudflareR2, storage.Minio
if aliyun == nil {
aliyun = &conf.AdminBackend_ObjectStore{}
}
if huawei == nil {
huawei = &conf.AdminBackend_ObjectStore{}
}
if tencent == nil {
tencent = &conf.AdminBackend_ObjectStore{}
}
if aws == nil {
aws = &conf.AdminBackend_ObjectStore{}
}
if cloudflare == nil {
cloudflare = &conf.AdminBackend_ObjectStore{}
}
if minio == nil {
minio = &conf.AdminBackend_ObjectStore{}
}
return map[string]map[string]any{
"aliyun-oss": {"endpoint": aliyun.Endpoint, "access-key-id": aliyun.AccessKey, "access-key-secret": aliyun.SecretKey, "bucket-name": aliyun.Bucket, "bucket-url": aliyun.BaseUrl, "base-path": aliyun.PathPrefix},
"hua-wei-obs": {"path": huawei.PathPrefix, "bucket": huawei.Bucket, "endpoint": huawei.Endpoint, "access-key": huawei.AccessKey, "secret-key": huawei.SecretKey},
"tencent-cos": {"bucket": tencent.Bucket, "region": tencent.Region, "secret-id": tencent.AccessKey, "secret-key": tencent.SecretKey, "base-url": tencent.BaseUrl, "path-prefix": tencent.PathPrefix},
"aws-s3": {"bucket": aws.Bucket, "region": aws.Region, "endpoint": aws.Endpoint, "secret-id": aws.AccessKey, "secret-key": aws.SecretKey, "base-url": aws.BaseUrl, "path-prefix": aws.PathPrefix, "s3-force-path-style": aws.ForcePathStyle, "disable-ssl": !aws.UseSsl},
"cloudflare-r2": {"bucket": cloudflare.Bucket, "base-url": cloudflare.BaseUrl, "path": cloudflare.PathPrefix, "account-id": cloudflare.AccountId, "access-key-id": cloudflare.AccessKey, "secret-access-key": cloudflare.SecretKey},
"minio": {"endpoint": minio.Endpoint, "access-key-id": minio.AccessKey, "access-key-secret": minio.SecretKey, "bucket-name": minio.Bucket, "use-ssl": minio.UseSsl, "base-path": minio.PathPrefix, "bucket-url": minio.BaseUrl},
}
}
func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any {
result := map[string]any{}
if data == nil {
data = &conf.Data{}
}
if admin == nil {
admin = &conf.AdminBackend{}
}
jwt := admin.Jwt
if jwt == nil {
jwt = &conf.AdminBackend_JWT{}
}
result["jwt"] = map[string]any{"signing-key": jwt.SigningKey, "expires-time": durationString(jwt.ExpiresTime), "buffer-time": durationString(jwt.BufferTime), "issuer": jwt.Issuer}
zap := admin.Zap
if zap == nil {
zap = &conf.AdminBackend_Zap{}
}
result["zap"] = map[string]any{
"level": zap.Level, "prefix": zap.Prefix, "format": zap.Format, "director": zap.Director,
"encode-level": zap.EncodeLevel, "stacktrace-key": zap.StacktraceKey, "show-line": zap.ShowLine,
"log-in-console": zap.LogInConsole, "retention-day": zap.RetentionDay,
"access-req-body": zap.AccessReqBody, "access-resp-data": zap.AccessRespData,
"access-req-headers": zap.AccessReqHeaders, "access-log-max-bytes": zap.AccessLogMaxBytes,
"file-only-modules": zap.FileOnlyModules,
}
result["redis"] = gvaRedis(data.Redis)
redisList := make([]map[string]any, 0, len(data.RedisList))
for _, item := range data.RedisList {
redisList = append(redisList, gvaRedis(item))
}
result["redis-list"] = redisList
mongo := data.Mongo
if mongo == nil {
mongo = &conf.Data_Mongo{}
}
hosts := make([]map[string]any, 0, len(mongo.Hosts))
for _, host := range mongo.Hosts {
if host != nil {
hosts = append(hosts, map[string]any{"host": host.Host, "port": host.Port})
}
}
result["mongo"] = map[string]any{"coll": mongo.Coll, "options": mongo.Options, "database": mongo.Database, "username": mongo.Username, "password": mongo.Password, "auth-source": mongo.AuthSource, "min-pool-size": mongo.MinPoolSize, "max-pool-size": mongo.MaxPoolSize, "socket-timeout-ms": mongo.SocketTimeoutMs, "connect-timeout-ms": mongo.ConnectTimeoutMs, "is-zap": mongo.IsZap, "hosts": hosts}
email := admin.Email
if email == nil {
email = &conf.AdminBackend_Email{}
}
result["email"] = map[string]any{"to": email.To, "from": email.From, "host": email.Host, "secret": email.Secret, "nickname": email.Nickname, "port": email.Port, "is-ssl": email.IsSsl, "is-loginauth": email.IsLoginAuth}
system := admin.System
if system == nil {
system = &conf.AdminBackend_System{}
}
storageType := "local"
if admin.Storage != nil && admin.Storage.Type != "" {
storageType = admin.Storage.Type
}
driver := ""
if data.Database != nil {
driver = data.Database.Driver
}
result["system"] = map[string]any{"db-type": driver, "oss-type": storageType, "router-prefix": admin.RouterPrefix, "addr": system.Addr, "iplimit-count": system.IplimitCount, "iplimit-time": system.IplimitTime, "use-multipoint": system.UseMultipoint, "use-redis": system.UseRedis, "use-mongo": system.UseMongo, "use-strict-auth": system.UseStrictAuth, "disable-auto-migrate": system.DisableAutoMigrate}
for _, name := range []string{"mysql", "mssql", "pgsql", "oracle", "sqlite"} {
result[name] = map[string]any{}
}
if driver != "" {
result[driver] = gvaDatabase(data.Database)
}
dbList := make([]map[string]any, 0, len(data.DatabaseList))
for _, item := range data.DatabaseList {
row := gvaDatabase(item)
if item != nil {
row["type"], row["alias-name"], row["disable"] = item.Driver, item.AliasName, item.Disable
}
dbList = append(dbList, row)
}
result["db-list"] = dbList
local := admin.Local
if local == nil {
local = &conf.AdminBackend_Local{}
}
result["local"] = map[string]any{"path": local.PathPrefix, "store-path": local.StorePath}
media := admin.Media
if media == nil {
media = &conf.AdminBackend_Media{}
}
result["media"] = map[string]any{"session-ttl": media.SessionTtl, "max-file-size": media.MaxFileSize, "chunk-dir": media.ChunkDir}
if admin.Storage == nil {
admin.Storage = &conf.AdminBackend_Storage{}
}
qiniu := admin.Storage.Qiniu
if qiniu == nil {
qiniu = &conf.AdminBackend_Qiniu{}
}
result["qiniu"] = map[string]any{"zone": qiniu.Zone, "bucket": qiniu.Bucket, "img-path": qiniu.BaseUrl, "access-key": qiniu.AccessKey, "secret-key": qiniu.SecretKey, "use-https": qiniu.UseHttps, "use-cdn-domains": qiniu.UseCdnDomains}
for key, value := range gvaObjectStores(admin.Storage) {
result[key] = value
}
disks := make([]map[string]any, 0, len(admin.DiskList))
for _, disk := range admin.DiskList {
if disk != nil {
disks = append(disks, map[string]any{"mount-point": disk.MountPoint})
}
}
result["disk-list"] = disks
cors := admin.Cors
if cors == nil {
cors = &conf.AdminBackend_CORS{}
}
whitelist := make([]map[string]any, 0, len(cors.Whitelist))
for _, rule := range cors.Whitelist {
if rule != nil {
whitelist = append(whitelist, map[string]any{"allow-origin": rule.AllowOrigin, "allow-methods": rule.AllowMethods, "allow-headers": rule.AllowHeaders, "expose-headers": rule.ExposeHeaders, "allow-credentials": rule.AllowCredentials})
}
}
result["cors"] = map[string]any{"mode": cors.Mode, "whitelist": whitelist}
result["app"] = map[string]any{"node": admin.GetApp().GetNode(), "app-id": admin.GetApp().GetAppId(), "env": admin.GetApp().GetEnv()}
return result
}
func objectMap(value any) map[string]any {
result, _ := value.(map[string]any)
return result
}
func stringValue(values map[string]any, key string) string {
value, _ := values[key].(string)
return value
}
func boolValue(values map[string]any, key string, fallback bool) bool {
value, ok := values[key].(bool)
if !ok {
return fallback
}
return value
}
func int64Value(values map[string]any, key string, fallback int64) int64 {
value, ok := values[key].(float64)
if !ok {
return fallback
}
return int64(value)
}
func stringSliceValue(values map[string]any, key string, fallback []string) []string {
items, ok := values[key].([]any)
if !ok {
return fallback
}
result := make([]string, 0, len(items))
for _, item := range items {
if value, ok := item.(string); ok {
result = append(result, value)
}
}
return result
}
func applyGVARedis(target *conf.Data_Redis, values map[string]any) {
if target == nil || values == nil {
return
}
target.Name, target.Addr = stringValue(values, "name"), stringValue(values, "addr")
if password := stringValue(values, "password"); password != "" && password != "******" {
target.Password = password
}
target.Db = int32(int64Value(values, "db", int64(target.Db)))
target.UseCluster = boolValue(values, "useCluster", target.UseCluster)
target.ClusterAddrs = stringSliceValue(values, "clusterAddrs", target.ClusterAddrs)
}
func durationValue(raw string, fallback *durationpb.Duration) *durationpb.Duration {
if raw == "" {
return fallback
}
value, err := parseGVADuration(raw)
if err != nil {
return fallback
}
return durationpb.New(value)
}
func parseGVADuration(raw string) (time.Duration, error) {
raw = strings.TrimSpace(raw)
if value, err := time.ParseDuration(raw); err == nil {
return value, nil
}
if index := strings.Index(raw, "d"); index >= 0 {
days, _ := strconv.Atoi(raw[:index])
value := time.Duration(days) * 24 * time.Hour
remainder, err := time.ParseDuration(raw[index+1:])
if err != nil {
return value, nil
}
return value + remainder, nil
}
nanoseconds, err := strconv.ParseInt(raw, 10, 64)
return time.Duration(nanoseconds), err
}
func applyGVADatabase(target *conf.Data_Database, values map[string]any, driver string) {
if target == nil || values == nil {
return
}
target.Driver = driver
target.Prefix, target.Port, target.Config = stringValue(values, "prefix"), stringValue(values, "port"), stringValue(values, "config")
target.Name, target.User = stringValue(values, "db-name"), stringValue(values, "username")
if password := stringValue(values, "password"); password != "" && password != "******" {
target.Password = password
}
if driver == "sqlite" {
target.Path = stringValue(values, "path")
} else {
target.Host = stringValue(values, "path")
}
target.Engine, target.LogMode = stringValue(values, "engine"), stringValue(values, "log-mode")
target.MaxIdleConns = int32(int64Value(values, "max-idle-conns", int64(target.MaxIdleConns)))
target.MaxOpenConns = int32(int64Value(values, "max-open-conns", int64(target.MaxOpenConns)))
target.ConnMaxLifetime = int32(int64Value(values, "conn-max-lifetime", int64(target.ConnMaxLifetime)))
target.Singular = boolValue(values, "singular", target.Singular)
}
func applyGVAObjectStore(target *conf.AdminBackend_ObjectStore, values map[string]any) {
if target == nil || values == nil {
return
}
target.Endpoint, target.Region, target.Bucket = stringValue(values, "endpoint"), stringValue(values, "region"), stringValue(values, "bucket")
if target.Bucket == "" {
target.Bucket = stringValue(values, "bucket-name")
}
target.AccessKey = stringValue(values, "access-key")
if target.AccessKey == "" {
target.AccessKey = stringValue(values, "access-key-id")
}
if target.AccessKey == "" {
target.AccessKey = stringValue(values, "secret-id")
}
secret := stringValue(values, "secret-key")
if secret == "" {
secret = stringValue(values, "access-key-secret")
}
if secret == "" {
secret = stringValue(values, "secret-access-key")
}
if secret != "" && secret != "******" {
target.SecretKey = secret
}
target.BaseUrl = stringValue(values, "base-url")
if target.BaseUrl == "" {
target.BaseUrl = stringValue(values, "bucket-url")
}
target.PathPrefix = stringValue(values, "path-prefix")
if target.PathPrefix == "" {
target.PathPrefix = stringValue(values, "base-path")
}
if target.PathPrefix == "" {
target.PathPrefix = stringValue(values, "path")
}
target.UseSsl = boolValue(values, "use-ssl", !boolValue(values, "disable-ssl", !target.UseSsl))
target.ForcePathStyle = boolValue(values, "s3-force-path-style", target.ForcePathStyle)
target.AccountId = stringValue(values, "account-id")
}
func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.AdminBackend) error {
var values map[string]any
if err := json.Unmarshal(raw, &values); err != nil {
return err
}
if _, isGVA := values["system"]; !isGVA {
return nil
}
if data.Database == nil {
data.Database = &conf.Data_Database{}
}
if admin.System == nil {
admin.System = &conf.AdminBackend_System{}
}
if admin.Storage == nil {
admin.Storage = &conf.AdminBackend_Storage{}
}
system := objectMap(values["system"])
driver := stringValue(system, "db-type")
if driver == "" {
driver = data.Database.Driver
}
applyGVADatabase(data.Database, objectMap(values[driver]), driver)
admin.Storage.Type = stringValue(system, "oss-type")
admin.RouterPrefix = stringValue(system, "router-prefix")
admin.System.UseMultipoint = boolValue(system, "use-multipoint", admin.System.UseMultipoint)
admin.System.UseRedis = boolValue(system, "use-redis", admin.System.UseRedis)
admin.System.UseMongo = boolValue(system, "use-mongo", admin.System.UseMongo)
admin.System.UseStrictAuth = boolValue(system, "use-strict-auth", admin.System.UseStrictAuth)
admin.System.DisableAutoMigrate = boolValue(system, "disable-auto-migrate", admin.System.DisableAutoMigrate)
admin.System.Addr = int32(int64Value(system, "addr", int64(admin.System.Addr)))
admin.System.IplimitCount = int32(int64Value(system, "iplimit-count", int64(admin.System.IplimitCount)))
admin.System.IplimitTime = int32(int64Value(system, "iplimit-time", int64(admin.System.IplimitTime)))
if admin.Jwt == nil {
admin.Jwt = &conf.AdminBackend_JWT{}
}
jwt := objectMap(values["jwt"])
if secret := stringValue(jwt, "signing-key"); secret != "" && secret != "******" {
admin.Jwt.SigningKey = secret
}
admin.Jwt.ExpiresTime = durationValue(stringValue(jwt, "expires-time"), admin.Jwt.ExpiresTime)
admin.Jwt.BufferTime = durationValue(stringValue(jwt, "buffer-time"), admin.Jwt.BufferTime)
admin.Jwt.Issuer = stringValue(jwt, "issuer")
if admin.Zap == nil {
admin.Zap = &conf.AdminBackend_Zap{}
}
zap := objectMap(values["zap"])
admin.Zap.Level, admin.Zap.Prefix, admin.Zap.Format, admin.Zap.Director = stringValue(zap, "level"), stringValue(zap, "prefix"), stringValue(zap, "format"), stringValue(zap, "director")
admin.Zap.EncodeLevel, admin.Zap.StacktraceKey = stringValue(zap, "encode-level"), stringValue(zap, "stacktrace-key")
admin.Zap.ShowLine, admin.Zap.LogInConsole = boolValue(zap, "show-line", admin.Zap.ShowLine), boolValue(zap, "log-in-console", admin.Zap.LogInConsole)
admin.Zap.RetentionDay = int32(int64Value(zap, "retention-day", int64(admin.Zap.RetentionDay)))
admin.Zap.AccessReqBody = boolValue(zap, "access-req-body", admin.Zap.AccessReqBody)
admin.Zap.AccessRespData = boolValue(zap, "access-resp-data", admin.Zap.AccessRespData)
admin.Zap.AccessReqHeaders = boolValue(zap, "access-req-headers", admin.Zap.AccessReqHeaders)
admin.Zap.AccessLogMaxBytes = int32(int64Value(zap, "access-log-max-bytes", int64(admin.Zap.AccessLogMaxBytes)))
if list, ok := zap["file-only-modules"].([]any); ok {
admin.Zap.FileOnlyModules = admin.Zap.FileOnlyModules[:0]
for _, item := range list {
if value, ok := item.(string); ok {
admin.Zap.FileOnlyModules = append(admin.Zap.FileOnlyModules, value)
}
}
}
if data.Redis == nil {
data.Redis = &conf.Data_Redis{}
}
applyGVARedis(data.Redis, objectMap(values["redis"]))
if items, ok := values["redis-list"].([]any); ok {
current := make(map[string]*conf.Data_Redis, len(data.RedisList))
for _, item := range data.RedisList {
if item != nil {
current[item.Name] = item
}
}
data.RedisList = make([]*conf.Data_Redis, 0, len(items))
for _, item := range items {
values := objectMap(item)
target := current[stringValue(values, "name")]
if target == nil {
target = &conf.Data_Redis{}
}
applyGVARedis(target, values)
data.RedisList = append(data.RedisList, target)
}
}
if mongo := objectMap(values["mongo"]); mongo != nil {
if data.Mongo == nil {
data.Mongo = &conf.Data_Mongo{}
}
data.Mongo.Coll, data.Mongo.Options, data.Mongo.Database = stringValue(mongo, "coll"), stringValue(mongo, "options"), stringValue(mongo, "database")
data.Mongo.Username, data.Mongo.AuthSource = stringValue(mongo, "username"), stringValue(mongo, "auth-source")
if password := stringValue(mongo, "password"); password != "" && password != "******" {
data.Mongo.Password = password
}
data.Mongo.MinPoolSize = uint64(int64Value(mongo, "min-pool-size", int64(data.Mongo.MinPoolSize)))
data.Mongo.MaxPoolSize = uint64(int64Value(mongo, "max-pool-size", int64(data.Mongo.MaxPoolSize)))
data.Mongo.SocketTimeoutMs = int64Value(mongo, "socket-timeout-ms", data.Mongo.SocketTimeoutMs)
data.Mongo.ConnectTimeoutMs = int64Value(mongo, "connect-timeout-ms", data.Mongo.ConnectTimeoutMs)
data.Mongo.IsZap = boolValue(mongo, "is-zap", data.Mongo.IsZap)
if hosts, ok := mongo["hosts"].([]any); ok {
data.Mongo.Hosts = make([]*conf.Data_MongoHost, 0, len(hosts))
for _, item := range hosts {
host := objectMap(item)
data.Mongo.Hosts = append(data.Mongo.Hosts, &conf.Data_MongoHost{Host: stringValue(host, "host"), Port: stringValue(host, "port")})
}
}
}
if items, ok := values["db-list"].([]any); ok {
current := make(map[string]*conf.Data_Database, len(data.DatabaseList))
for _, item := range data.DatabaseList {
if item != nil {
current[item.AliasName] = item
}
}
data.DatabaseList = make([]*conf.Data_Database, 0, len(items))
for _, item := range items {
row := objectMap(item)
alias := stringValue(row, "alias-name")
target := current[alias]
if target == nil {
target = &conf.Data_Database{}
}
applyGVADatabase(target, row, stringValue(row, "type"))
target.AliasName, target.Disable = alias, boolValue(row, "disable", target.Disable)
data.DatabaseList = append(data.DatabaseList, target)
}
}
if admin.Email == nil {
admin.Email = &conf.AdminBackend_Email{}
}
email := objectMap(values["email"])
admin.Email.To, admin.Email.From, admin.Email.Host = stringValue(email, "to"), stringValue(email, "from"), stringValue(email, "host")
admin.Email.Nickname = stringValue(email, "nickname")
admin.Email.Port = int32(int64Value(email, "port", int64(admin.Email.Port)))
admin.Email.IsSsl, admin.Email.IsLoginAuth = boolValue(email, "is-ssl", admin.Email.IsSsl), boolValue(email, "is-loginauth", admin.Email.IsLoginAuth)
if secret := stringValue(email, "secret"); secret != "" && secret != "******" {
admin.Email.Secret = secret
}
if admin.Local == nil {
admin.Local = &conf.AdminBackend_Local{}
}
local := objectMap(values["local"])
admin.Local.PathPrefix, admin.Local.StorePath = stringValue(local, "path"), stringValue(local, "store-path")
if admin.Media == nil {
admin.Media = &conf.AdminBackend_Media{}
}
media := objectMap(values["media"])
admin.Media.SessionTtl = int32(int64Value(media, "session-ttl", int64(admin.Media.SessionTtl)))
admin.Media.MaxFileSize = int64Value(media, "max-file-size", admin.Media.MaxFileSize)
admin.Media.ChunkDir = stringValue(media, "chunk-dir")
if admin.Storage.Qiniu == nil {
admin.Storage.Qiniu = &conf.AdminBackend_Qiniu{}
}
qiniu := objectMap(values["qiniu"])
admin.Storage.Qiniu.Zone, admin.Storage.Qiniu.Bucket, admin.Storage.Qiniu.BaseUrl = stringValue(qiniu, "zone"), stringValue(qiniu, "bucket"), stringValue(qiniu, "img-path")
admin.Storage.Qiniu.AccessKey = stringValue(qiniu, "access-key")
if secret := stringValue(qiniu, "secret-key"); secret != "" && secret != "******" {
admin.Storage.Qiniu.SecretKey = secret
}
admin.Storage.Qiniu.UseHttps, admin.Storage.Qiniu.UseCdnDomains = boolValue(qiniu, "use-https", admin.Storage.Qiniu.UseHttps), boolValue(qiniu, "use-cdn-domains", admin.Storage.Qiniu.UseCdnDomains)
stores := []struct {
key string
target **conf.AdminBackend_ObjectStore
}{{"aliyun-oss", &admin.Storage.AliyunOss}, {"hua-wei-obs", &admin.Storage.HuaweiObs}, {"tencent-cos", &admin.Storage.TencentCos}, {"aws-s3", &admin.Storage.AwsS3}, {"cloudflare-r2", &admin.Storage.CloudflareR2}, {"minio", &admin.Storage.Minio}}
for _, store := range stores {
if *store.target == nil {
*store.target = &conf.AdminBackend_ObjectStore{}
}
applyGVAObjectStore(*store.target, objectMap(values[store.key]))
}
if disks, ok := values["disk-list"].([]any); ok {
admin.DiskList = make([]*conf.AdminBackend_Disk, 0, len(disks))
for _, item := range disks {
disk := objectMap(item)
admin.DiskList = append(admin.DiskList, &conf.AdminBackend_Disk{MountPoint: stringValue(disk, "mount-point")})
}
}
if cors := objectMap(values["cors"]); cors != nil {
if admin.Cors == nil {
admin.Cors = &conf.AdminBackend_CORS{}
}
admin.Cors.Mode = stringValue(cors, "mode")
if rules, ok := cors["whitelist"].([]any); ok {
admin.Cors.Whitelist = make([]*conf.AdminBackend_CORSRule, 0, len(rules))
for _, item := range rules {
rule := objectMap(item)
admin.Cors.Whitelist = append(admin.Cors.Whitelist, &conf.AdminBackend_CORSRule{AllowOrigin: stringValue(rule, "allow-origin"), AllowMethods: stringValue(rule, "allow-methods"), AllowHeaders: stringValue(rule, "allow-headers"), ExposeHeaders: stringValue(rule, "expose-headers"), AllowCredentials: boolValue(rule, "allow-credentials", false)})
}
}
}
if app := objectMap(values["app"]); app != nil {
if admin.App == nil {
admin.App = &conf.AdminBackend_App{}
}
admin.App.Node, admin.App.AppId, admin.App.Env = stringValue(app, "node"), stringValue(app, "app-id"), stringValue(app, "env")
}
return nil
}

View File

@ -4,8 +4,11 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
pathpkg "path"
"path/filepath"
"sort"
"strings"
@ -14,86 +17,320 @@ import (
"kra/internal/biz"
)
func (r *logFileRepo) logRoot() (string, error) {
const (
defaultLogChunkLines = 500
maxLogChunkBytes = 2 * 1024 * 1024
logReadBlockSize = 64 * 1024
)
func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) {
admin := r.data.runtime.Admin()
if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" {
return "", biz.ErrLogRootUnavailable
return "", false, biz.ErrLogRootUnavailable
}
root, err := filepath.Abs(admin.Zap.Director)
root, err = filepath.Abs(admin.Zap.Director)
if err != nil {
return "", errors.Join(biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
info, err := os.Stat(root)
if errors.Is(err, fs.ErrNotExist) {
return root, nil
return root, false, nil
}
if err != nil {
return "", errors.Join(biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
if !info.IsDir() {
return "", biz.ErrLogRootUnavailable
return "", false, biz.ErrLogRootUnavailable
}
return root, nil
return root, true, nil
}
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month {
return nil, biz.ErrInvalidLogMonth
func (r *logFileRepo) openConfiguredLogRoot() (root *os.Root, exists bool, err error) {
rootPath, exists, err := r.configuredLogRoot()
if err != nil || !exists {
return nil, exists, err
}
root, err := r.logRoot()
root, err = os.OpenRoot(rootPath)
if err != nil {
return nil, false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
return root, true, nil
}
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
if err := validateLogMonth(month); err != nil {
return nil, err
}
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil {
return nil, err
}
entries, err := os.ReadDir(root)
if errors.Is(err, fs.ErrNotExist) {
if !exists {
return []biz.LogDate{}, nil
}
defer logRoot.Close()
entries, err := fs.ReadDir(logRoot.FS(), ".")
if err != nil {
return nil, err
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
out := []biz.LogDate{}
result := make([]biz.LogDate, 0)
for _, entry := range entries {
if ctx.Err() != nil {
return nil, ctx.Err()
if err = ctx.Err(); err != nil {
return nil, err
}
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
continue
}
if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() {
if validateLogDate(entry.Name()) != nil {
continue
}
count := 0
_ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error {
if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") {
count++
}
return nil
})
count, countErr := countLogFiles(ctx, logRoot, entry.Name())
if countErr != nil {
return nil, countErr
}
if count > 0 {
out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count})
result = append(result, biz.LogDate{Date: entry.Name(), FileCount: count})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date })
return out, nil
sort.Slice(result, func(i, j int) bool { return result[i].Date < result[j].Date })
return result, nil
}
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
return nil, biz.ErrInvalidLogDate
if err := validateLogDate(date); err != nil {
return nil, err
}
root, err := r.logRoot()
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil {
return nil, err
}
dateRoot := filepath.Join(root, date)
out := []biz.LogFile{}
err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error {
if !exists {
return []biz.LogFile{}, nil
}
defer logRoot.Close()
info, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return []biz.LogFile{}, nil
}
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return []biz.LogFile{}, nil
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
defer dateRoot.Close()
result := make([]biz.LogFile, 0)
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
if errors.Is(walkErr, fs.ErrNotExist) {
return nil
}
return walkErr
}
if ctx.Err() != nil {
return ctx.Err()
if err := ctx.Err(); err != nil {
return err
}
if path == "." {
return nil
}
if entry.Type()&os.ModeSymlink != 0 {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".log") {
return nil
}
fileInfo, infoErr := entry.Info()
if infoErr != nil {
return infoErr
}
if fileInfo.Mode().IsRegular() {
result = append(result, biz.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path })
return result, nil
}
func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*biz.LogContent, error) {
result := &biz.LogContent{Date: date, Path: apiPath}
if err := ctx.Err(); err != nil {
return nil, err
}
if cursor != nil && *cursor < 0 {
return nil, biz.ErrInvalidLogPath
}
file, info, err := r.openValidatedLogFile(date, apiPath)
if err != nil {
return nil, err
}
defer file.Close()
end := info.Size()
if cursor != nil && *cursor <= end {
end = *cursor
}
start, limitedByBytes, err := findLogChunkStart(file, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
data, err := readLogRange(file, start, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if int64(len(data)) < end-start {
currentInfo, statErr := file.Stat()
if statErr != nil || currentInfo.Size() < start {
if statErr != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
}
return nil, biz.ErrLogFileUnreadable
}
info = currentInfo
}
result.Content = string(data)
result.LineCount = countLogicalLines(data)
result.NextCursor = start
result.HasMore = start > 0
result.LimitedByBytes = limitedByBytes
result.Size = info.Size()
result.ModifiedAt = info.ModTime()
return result, nil
}
func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File, info os.FileInfo, err error) {
if err = validateLogDate(date); err != nil {
return nil, nil, err
}
segments, err := validateLogAPIPath(apiPath)
if err != nil {
return nil, nil, err
}
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil {
return nil, nil, err
}
if !exists {
return nil, nil, biz.ErrLogFileNotFound
}
defer logRoot.Close()
dateInfo, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
defer dateRoot.Close()
relativePath := ""
var validatedInfo os.FileInfo
for index, segment := range segments {
relativePath = filepath.Join(relativePath, segment)
validatedInfo, err = dateRoot.Lstat(relativePath)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if validatedInfo.Mode()&os.ModeSymlink != 0 {
return nil, nil, biz.ErrInvalidLogPath
}
if index < len(segments)-1 && !validatedInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
}
}
if validatedInfo == nil || !validatedInfo.Mode().IsRegular() {
return nil, nil, biz.ErrInvalidLogPath
}
file, err = dateRoot.Open(relativePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
openedInfo, statErr := file.Stat()
if statErr != nil || !openedInfo.Mode().IsRegular() || !os.SameFile(validatedInfo, openedInfo) {
file.Close()
if statErr != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
}
return nil, nil, biz.ErrInvalidLogPath
}
return file, openedInfo, nil
}
func validateLogMonth(month string) error {
parsed, err := time.Parse("2006-01", month)
if err != nil || parsed.Format("2006-01") != month {
return biz.ErrInvalidLogMonth
}
return nil
}
func validateLogDate(date string) error {
parsed, err := time.Parse("2006-01-02", date)
if err != nil || parsed.Format("2006-01-02") != date {
return biz.ErrInvalidLogDate
}
return nil
}
func validateLogAPIPath(apiPath string) ([]string, error) {
if apiPath == "" || strings.Contains(apiPath, "\\") || strings.Contains(apiPath, ":") || pathpkg.IsAbs(apiPath) {
return nil, biz.ErrInvalidLogPath
}
segments := strings.Split(apiPath, "/")
for _, segment := range segments {
if segment == "" || segment == "." || segment == ".." {
return nil, biz.ErrInvalidLogPath
}
}
if !strings.EqualFold(pathpkg.Ext(apiPath), ".log") {
return nil, biz.ErrInvalidLogPath
}
return segments, nil
}
func countLogFiles(ctx context.Context, logRoot *os.Root, date string) (count int, err error) {
dateInfo, err := logRoot.Lstat(date)
if err != nil || dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return 0, err
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return 0, err
}
defer dateRoot.Close()
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if err := ctx.Err(); err != nil {
return err
}
if path == "." {
return nil
}
if entry.Type()&os.ModeSymlink != 0 {
if entry.IsDir() {
@ -108,70 +345,80 @@ func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile,
if infoErr != nil {
return infoErr
}
relative, _ := filepath.Rel(dateRoot, path)
out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()})
if info.Mode().IsRegular() {
count++
}
return nil
})
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
return out, err
}
func (r *logFileRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
return nil, biz.ErrInvalidLogDate
}
root, err := r.logRoot()
if err != nil {
return 0, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
return count, nil
}
func readLogRange(file *os.File, start, end int64) ([]byte, error) {
if end <= start {
return []byte{}, nil
}
data := make([]byte, int(end-start))
n, err := file.ReadAt(data, start)
if err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
dateRoot := filepath.Join(root, date)
target := filepath.Clean(filepath.Join(dateRoot, filepath.FromSlash(path)))
if target == dateRoot || !strings.HasPrefix(target, dateRoot+string(os.PathSeparator)) || !strings.EqualFold(filepath.Ext(target), ".log") {
return nil, biz.ErrInvalidLogPath
}
info, err := os.Lstat(target)
if errors.Is(err, fs.ErrNotExist) {
return nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return nil, biz.ErrInvalidLogPath
}
end := info.Size()
if cursor != nil && *cursor >= 0 && *cursor < end {
end = *cursor
}
start := end - int64(2*1024*1024)
limited := start > 0
if start < 0 {
start = 0
}
file, err := os.Open(target)
if err != nil {
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
}
defer file.Close()
data := make([]byte, end-start)
n, err := file.ReadAt(data, start)
if err != nil && n == 0 {
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
}
data = data[:n]
lines := bytes.Split(data, []byte("\n"))
if len(lines) > 501 {
drop := len(lines) - 501
offset := 0
for _, line := range lines[:drop] {
offset += len(line) + 1
}
start += int64(offset)
data = data[offset:]
limited = true
}
lineCount := bytes.Count(data, []byte("\n"))
if len(data) > 0 && data[len(data)-1] != '\n' {
lineCount++
}
return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: lineCount, NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil
return data[:n], nil
}
func findLogChunkStart(file *os.File, end int64) (start int64, limitedByBytes bool, err error) {
if end <= 0 {
return 0, false, nil
}
lastByte := []byte{0}
if _, err = file.ReadAt(lastByte, end-1); err != nil {
return 0, false, err
}
targetNewlines := defaultLogChunkLines
if lastByte[0] == '\n' {
targetNewlines++
}
position, scanned, newlines := end, int64(0), 0
for position > 0 && scanned < maxLogChunkBytes {
readSize := int64(logReadBlockSize)
if readSize > position {
readSize = position
}
if remaining := int64(maxLogChunkBytes) - scanned; readSize > remaining {
readSize = remaining
}
blockStart := position - readSize
block := make([]byte, int(readSize))
n, readErr := file.ReadAt(block, blockStart)
if readErr != nil && !errors.Is(readErr, io.EOF) {
return 0, false, readErr
}
for index := n - 1; index >= 0; index-- {
if block[index] == '\n' {
newlines++
if newlines == targetNewlines {
return blockStart + int64(index) + 1, false, nil
}
}
}
scanned += int64(n)
position = blockStart
}
if position > 0 {
return end - int64(maxLogChunkBytes), true, nil
}
return 0, false, nil
}
func countLogicalLines(data []byte) int {
if len(data) == 0 {
return 0
}
count := bytes.Count(data, []byte{'\n'})
if data[len(data)-1] != '\n' {
count++
}
return count
}

View File

@ -68,7 +68,7 @@ func (s *runtimeSettings) MediaSettings() biz.MediaSettings {
if config == nil || config.Media == nil {
return biz.MediaSettings{}
}
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize}
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
}
func (s *runtimeSettings) UseMultipoint() bool {
@ -110,5 +110,5 @@ func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
return nil, biz.ErrTokenInvalid
}
}
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, ExpiresAt: claims.ExpiresAt.Time}, nil
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, ExpiresAt: claims.ExpiresAt.Time}, nil
}

View File

@ -95,7 +95,9 @@ func (s *aliyunStorage) List(_ context.Context, prefix, cursor string, limit int
}
items := make([]*biz.StoredFile, 0, len(result.Objects))
for _, object := range result.Objects {
items = append(items, s.file(object.Key, object.Size))
item := s.file(object.Key, object.Size)
item.LastModified = object.LastModified
items = append(items, item)
}
next := ""
if result.IsTruncated {

View File

@ -141,7 +141,11 @@ func (s *awsStorage) List(ctx context.Context, prefix, cursor string, limit int)
if object.Size != nil {
size = *object.Size
}
items = append(items, s.file(*object.Key, size))
item := s.file(*object.Key, size)
if object.LastModified != nil {
item.LastModified = *object.LastModified
}
items = append(items, item)
}
next := ""
if result.NextContinuationToken != nil {

View File

@ -94,7 +94,9 @@ func (s *huaweiStorage) List(_ context.Context, prefix, cursor string, limit int
}
items := make([]*biz.StoredFile, 0, len(result.Contents))
for _, object := range result.Contents {
items = append(items, s.file(object.Key, object.Size))
item := s.file(object.Key, object.Size)
item.LastModified = object.LastModified
items = append(items, item)
}
next := result.NextMarker
if result.IsTruncated && next == "" && len(result.Contents) > 0 {

View File

@ -6,6 +6,7 @@ import (
"encoding/hex"
"fmt"
"io"
"mime"
"os"
"path/filepath"
"sort"
@ -200,7 +201,7 @@ func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int
if infoErr != nil {
return infoErr
}
items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size()})
items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size(), LastModified: info.ModTime(), ContentType: mime.TypeByExtension(filepath.Ext(entry.Name()))})
return nil
})
if err != nil {

View File

@ -120,7 +120,10 @@ func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit in
}
out := make([]*biz.StoredFile, 0, len(entries))
for _, entry := range entries {
out = append(out, s.file(entry.Key, entry.Fsize))
item := s.file(entry.Key, entry.Fsize)
item.LastModified = time.Unix(0, entry.PutTime*100)
item.ContentType = entry.MimeType
out = append(out, item)
}
return out, marker, more, nil
}

View File

@ -118,7 +118,10 @@ func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int)
if item.Err != nil {
return nil, "", false, item.Err
}
out = append(out, s.file(item.Key, item.Size))
file := s.file(item.Key, item.Size)
file.LastModified = item.LastModified
file.ContentType = item.ContentType
out = append(out, file)
if len(out) > limit {
break
}

View File

@ -8,6 +8,7 @@ import (
"net/url"
"path"
"strings"
"time"
cos "github.com/tencentyun/cos-go-sdk-v5"
"kra/internal/biz"
@ -107,7 +108,14 @@ func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit
}
items := make([]*biz.StoredFile, 0, len(result.Contents))
for _, object := range result.Contents {
items = append(items, s.file(object.Key, object.Size))
item := s.file(object.Key, object.Size)
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04:05.000Z"} {
if parsed, parseErr := time.Parse(layout, object.LastModified); parseErr == nil {
item.LastModified = parsed
break
}
}
items = append(items, item)
}
next := result.NextMarker
if result.IsTruncated && next == "" && len(result.Contents) > 0 {

View File

@ -429,6 +429,9 @@ func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User
}
func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ?", id).Delete(&userPO{}).Error; err != nil {
return err
}
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
@ -438,7 +441,7 @@ func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err
}
return tx.Delete(&userPO{}, id).Error
return nil
})
}
func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {

View File

@ -242,10 +242,10 @@ func (h *Dictionary) Path(c *gin.Context) {
httpx.Fail(c, "字典详情ID格式错误")
return
}
item, err := h.service.DictionaryDetail(c.Request.Context(), uint(id))
path, err := h.service.DictionaryPath(c.Request.Context(), uint(id))
if err != nil {
httpx.Fail(c, "获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"path": item.Path}, "获取成功")
httpx.Write(c, httpx.CodeSuccess, gin.H{"path": path}, "获取成功")
}

View File

@ -30,11 +30,6 @@ func exportParams(values url.Values) map[string]string {
out[key] = items[0]
}
}
for key, items := range values {
if key != "params" && len(items) > 0 {
out[key] = items[0]
}
}
return out
}

View File

@ -1,8 +1,6 @@
package handler
import (
"strconv"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
@ -24,7 +22,7 @@ func (h *Organization) ListDepartments(c *gin.Context) {
_ = c.ShouldBindJSON(&req)
items, err := h.departments.Departments(c.Request.Context(), req.Name)
if err != nil {
httpx.Fail(c, "获取失败")
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
@ -63,19 +61,23 @@ func (h *Organization) UpdateDepartment(c *gin.Context) {
}
func (h *Organization) DeleteDepartment(c *gin.Context) {
var req dto.DeleteDepartmentRequest
if c.ShouldBindJSON(&req) != nil {
httpx.Fail(c, "参数错误")
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.departments.Delete(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, err.Error())
httpx.Fail(c, "删除失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
}
func (h *Organization) FindDepartment(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
item, err := h.departments.Department(c.Request.Context(), uint(id))
var req dto.DeleteDepartmentRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.departments.Department(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
@ -83,8 +85,12 @@ func (h *Organization) FindDepartment(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
}
func (h *Organization) DepartmentUsers(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("deptId"), 10, 64)
ids, err := h.departments.UserIDs(c.Request.Context(), uint(id))
var req dto.SetDepartmentUsersRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.departments.UserIDs(c.Request.Context(), req.DepartmentID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
@ -96,8 +102,12 @@ func (h *Organization) DepartmentUsers(c *gin.Context) {
}
func (h *Organization) SetDepartmentUsers(c *gin.Context) {
var req dto.SetDepartmentUsersRequest
if c.ShouldBindJSON(&req) != nil {
httpx.Fail(c, "参数错误")
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.DepartmentID == 0 {
httpx.Fail(c, "部门ID不能为空")
return
}
if err := h.departments.SetUsers(c.Request.Context(), &req); err != nil {
@ -108,8 +118,8 @@ func (h *Organization) SetDepartmentUsers(c *gin.Context) {
}
func (h *Organization) SetUserDepartments(c *gin.Context) {
var req dto.SetUserDepartmentsRequest
if c.ShouldBindJSON(&req) != nil {
httpx.Fail(c, "参数错误")
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.departments.SetUserDepartments(c.Request.Context(), &req); err != nil {
@ -121,13 +131,13 @@ func (h *Organization) SetUserDepartments(c *gin.Context) {
func (h *Organization) ListPositions(c *gin.Context) {
var req dto.PositionListRequest
if c.ShouldBindJSON(&req) != nil {
httpx.Fail(c, "参数错误")
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
items, total, err := h.positions.Positions(c.Request.Context(), &req)
if err != nil {
httpx.Fail(c, "获取失败")
httpx.Fail(c, "获取失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
@ -166,8 +176,8 @@ func (h *Organization) UpdatePosition(c *gin.Context) {
}
func (h *Organization) DeletePosition(c *gin.Context) {
var req dto.DeletePositionRequest
if c.ShouldBindJSON(&req) != nil {
httpx.Fail(c, "参数错误")
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.positions.Delete(c.Request.Context(), req.ID); err != nil {
@ -177,8 +187,12 @@ func (h *Organization) DeletePosition(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
}
func (h *Organization) FindPosition(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
item, err := h.positions.Position(c.Request.Context(), uint(id))
var req dto.DeletePositionRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.positions.Position(c.Request.Context(), req.ID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
@ -186,8 +200,12 @@ func (h *Organization) FindPosition(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
}
func (h *Organization) PositionUsers(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("positionId"), 10, 64)
ids, err := h.positions.UserIDs(c.Request.Context(), uint(id))
var req dto.SetPositionUsersRequest
if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.positions.UserIDs(c.Request.Context(), req.PositionID)
if err != nil {
httpx.Fail(c, "获取失败:"+err.Error())
return
@ -199,8 +217,12 @@ func (h *Organization) PositionUsers(c *gin.Context) {
}
func (h *Organization) SetPositionUsers(c *gin.Context) {
var req dto.SetPositionUsersRequest
if c.ShouldBindJSON(&req) != nil {
httpx.Fail(c, "参数错误")
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if req.PositionID == 0 {
httpx.Fail(c, "岗位ID不能为空")
return
}
if err := h.positions.SetUsers(c.Request.Context(), &req); err != nil {
@ -211,8 +233,8 @@ func (h *Organization) SetPositionUsers(c *gin.Context) {
}
func (h *Organization) SetUserPositions(c *gin.Context) {
var req dto.SetUserPositionsRequest
if c.ShouldBindJSON(&req) != nil {
httpx.Fail(c, "参数错误")
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.positions.SetUserPositions(c.Request.Context(), &req); err != nil {

View File

@ -123,11 +123,7 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
values = append(values, dto.Route{Path: route.Path, Method: route.Method})
}
if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil {
if errors.Is(err, biz.ErrTaskRuntimeReload) {
httpx.Fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error())
return
}
httpx.Fail(c, "自动创建数据库失败: "+err.Error())
httpx.Fail(c, "自动创建数据库失败,请查看后台日志,检查后在进行初始化")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功")

View File

@ -1,9 +1,6 @@
package handler
import (
"errors"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
@ -67,10 +64,6 @@ func (h *SystemConfig) Set(c *gin.Context) {
func (h *SystemConfig) Reload(c *gin.Context) {
if err := h.system.ReloadConfig(c.Request.Context()); err != nil {
if errors.Is(err, biz.ErrTaskRuntimeReload) {
httpx.Fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error())
return
}
httpx.Fail(c, "重载系统失败:"+err.Error())
return
}

View File

@ -1,14 +1,13 @@
package handler
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/server/middleware"
"kra/internal/service"
"kra/internal/service/dto"
@ -31,13 +30,7 @@ func (h *Task) Create(c *gin.Context) {
}
_, err := h.service.CreateRequest(c.Request.Context(), &req)
if err != nil {
if errors.Is(err, biz.ErrTaskSchedule) {
var scheduleErr *biz.TaskScheduleError
errors.As(err, &scheduleErr)
httpx.Fail(c, "调度失败:"+scheduleErr.Error())
return
}
httpx.Fail(c, "创建失败:"+err.Error())
httpx.Fail(c, "创建失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
@ -50,13 +43,7 @@ func (h *Task) Update(c *gin.Context) {
return
}
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
if errors.Is(err, biz.ErrTaskSchedule) {
var scheduleErr *biz.TaskScheduleError
errors.As(err, &scheduleErr)
httpx.Fail(c, "调度失败:"+scheduleErr.Error())
return
}
httpx.Fail(c, "更新失败:"+err.Error())
httpx.Fail(c, "更新失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
@ -134,8 +121,13 @@ func (h *Task) Methods(c *gin.Context) {
}
func (h *Task) AlertStream(c *gin.Context) {
claims := middleware.Claims(c)
if claims == nil || claims.ID == 0 {
httpx.Fail(c, "未获取到用户身份")
return
}
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Cache-Control", "no-cache, no-transform")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
flusher, ok := c.Writer.(http.Flusher)
@ -145,11 +137,11 @@ func (h *Task) AlertStream(c *gin.Context) {
}
// Remove the normal server deadline for long-lived SSE connections.
_ = http.NewResponseController(c.Writer).SetWriteDeadline(time.Time{})
events := h.service.Subscribe()
defer h.service.Unsubscribe(events)
ticker := time.NewTicker(20 * time.Second)
events := h.service.Subscribe(claims.ID)
defer h.service.Unsubscribe(claims.ID, events)
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
_, _ = fmt.Fprint(c.Writer, "event: connected\ndata: {}\n\n")
_, _ = fmt.Fprint(c.Writer, ": connected\n\n")
flusher.Flush()
for {
select {
@ -157,10 +149,10 @@ func (h *Task) AlertStream(c *gin.Context) {
if !open {
return
}
_, _ = fmt.Fprintf(c.Writer, "event: alert\ndata: %s\n\n", event)
_, _ = fmt.Fprintf(c.Writer, "event: timedTask:alert\ndata: %s\n\n", event)
flusher.Flush()
case <-ticker.C:
_, _ = fmt.Fprint(c.Writer, ": keepalive\n\n")
_, _ = fmt.Fprint(c.Writer, ": ping\n\n")
flusher.Flush()
case <-c.Request.Context().Done():
return

View File

@ -124,6 +124,21 @@ func (s *DictionaryService) DictionaryDetail(ctx context.Context, id uint) (*dto
}
return detailDTO(v), nil
}
func (s *DictionaryService) DictionaryPath(ctx context.Context, id uint) ([]*dto.DictionaryDetailResponse, error) {
path := make([]*dto.DictionaryDetailResponse, 0)
for id != 0 {
value, err := s.uc.FindDictionaryDetail(ctx, id)
if err != nil {
return nil, err
}
path = append([]*dto.DictionaryDetailResponse{detailDTO(value)}, path...)
if value.ParentID == nil {
break
}
id = *value.ParentID
}
return path, nil
}
func (s *DictionaryService) DictionaryTree(ctx context.Context, id uint, typ string) ([]*dto.DictionaryDetailResponse, error) {
items, err := s.uc.DictionaryDetailTree(ctx, id, typ)
if err != nil {

View File

@ -29,10 +29,10 @@ type MediaCategoryResponse struct {
}
type StorageObjectResponse struct {
Name string `json:"name"`
Key string `json:"key"`
URL string `json:"url"`
Size int64 `json:"size"`
Key string `json:"Key"`
Size int64 `json:"Size"`
LastModified time.Time `json:"LastModified"`
ContentType string `json:"ContentType"`
}
type InitUploadResponse struct {

View File

@ -15,10 +15,10 @@ type DepartmentListRequest struct {
Name string `json:"name"`
}
type DeleteDepartmentRequest struct {
ID uint `json:"ID"`
ID uint `json:"ID" form:"id"`
}
type SetDepartmentUsersRequest struct {
DepartmentID uint `json:"deptId"`
DepartmentID uint `json:"deptId" form:"deptId"`
UserIDs []uint `json:"userIds"`
}
type SetUserDepartmentsRequest struct {
@ -60,10 +60,10 @@ type PositionListRequest struct {
Status *bool `json:"status"`
}
type DeletePositionRequest struct {
ID uint `json:"ID"`
ID uint `json:"ID" form:"id"`
}
type SetPositionUsersRequest struct {
PositionID uint `json:"positionId"`
PositionID uint `json:"positionId" form:"positionId"`
UserIDs []uint `json:"userIds"`
}
type SetUserPositionsRequest struct {

View File

@ -87,13 +87,19 @@ func (s *ExportService) TemplatesFilter(ctx context.Context, page, size int, nam
return s.Templates(ctx, page, size, &biz.ExportTemplate{Name: name, TableName: tableName, TemplateID: templateID, StartCreatedAt: start, EndCreatedAt: end})
}
func exportDTO(v *biz.ExportTemplate) *dto.ExportTemplateResponse {
conditions := make([]dto.ExportConditionResponse, 0, len(v.Conditions))
for _, x := range v.Conditions {
conditions = append(conditions, dto.ExportConditionResponse{From: x.From, Column: x.Column, Operator: x.Operator})
var conditions []dto.ExportConditionResponse
if v.Conditions != nil {
conditions = make([]dto.ExportConditionResponse, 0, len(v.Conditions))
for _, x := range v.Conditions {
conditions = append(conditions, dto.ExportConditionResponse{From: x.From, Column: x.Column, Operator: x.Operator})
}
}
joins := make([]dto.ExportJoinResponse, 0, len(v.Joins))
for _, x := range v.Joins {
joins = append(joins, dto.ExportJoinResponse{Join: x.Join, Table: x.Table, On: x.On})
var joins []dto.ExportJoinResponse
if v.Joins != nil {
joins = make([]dto.ExportJoinResponse, 0, len(v.Joins))
for _, x := range v.Joins {
joins = append(joins, dto.ExportJoinResponse{Join: x.Join, Table: x.Table, On: x.On})
}
}
return &dto.ExportTemplateResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, DBName: v.DBName, Name: v.Name, TableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order, Conditions: conditions, Joins: joins}
}

View File

@ -6,7 +6,9 @@ import (
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/xuri/excelize/v2"
)
@ -59,7 +61,23 @@ func (s *ExportService) Export(ctx context.Context, tid string, params map[strin
for rowIndex, row := range rows {
for column, key := range keys {
cell, _ := excelize.CoordinatesToCellName(column+1, rowIndex+2)
_ = file.SetCellValue(sheet, cell, row[key])
lookup := strings.ReplaceAll(strings.ReplaceAll(key, `"`, ""), "`", "")
if len(template.Joins) > 0 {
if parts := strings.Split(lookup, " as "); len(parts) > 1 {
lookup = strings.TrimSpace(parts[1])
} else if parts = strings.Split(lookup, "."); len(parts) > 1 {
lookup = parts[1]
}
}
value := fmt.Sprintf("%v", row[lookup])
if timestamp, ok := row[lookup].(time.Time); ok {
value = timestamp.Format("2006-01-02 15:04:05")
}
if number, parseErr := strconv.ParseFloat(value, 64); parseErr == nil {
_ = file.SetCellValue(sheet, cell, number)
} else {
_ = file.SetCellValue(sheet, cell, value)
}
}
}
buffer, err := file.WriteToBuffer()
@ -108,11 +126,7 @@ func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader
return err
}
defer file.Close()
sheets := file.GetSheetList()
if len(sheets) == 0 {
return errors.New("工作簿没有工作表")
}
rows, err := file.GetRows(sheets[0])
rows, err := file.GetRows("Sheet1")
if err != nil {
return err
}
@ -128,6 +142,9 @@ func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader
reverse[labels[key]] = key
}
headers := rows[0]
for index := range headers {
headers[index] = strings.TrimSpace(headers[index])
}
items := make([]map[string]any, 0, len(rows)-1)
for _, row := range rows[1:] {
item := map[string]any{}
@ -142,8 +159,5 @@ func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader
}
items = append(items, item)
}
if err = s.uc.ImportExportRows(ctx, template, items); err != nil {
return fmt.Errorf("导入失败: %w", err)
}
return nil
return s.uc.ImportExportRows(ctx, template, items)
}

View File

@ -75,7 +75,7 @@ func (s *MediaService) Storage(ctx context.Context, prefix, cursor string, limit
}
out := make([]*dto.StorageObjectResponse, 0, len(items))
for _, v := range items {
out = append(out, &dto.StorageObjectResponse{Name: v.Name, Key: v.Path, URL: v.URL, Size: v.Size})
out = append(out, &dto.StorageObjectResponse{Key: v.Path, Size: v.Size, LastModified: v.LastModified, ContentType: v.ContentType})
}
return out, next, more, nil
}

View File

@ -76,10 +76,10 @@ func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, sta
return out, total, nil
}
func (s *TaskService) Trigger(ctx context.Context, id uint) error { return s.uc.Trigger(ctx, id) }
func (s *TaskService) Reload(ctx context.Context) error { return s.uc.Reload(ctx) }
func (s *TaskService) Subscribe() chan []byte { return s.uc.Subscribe() }
func (s *TaskService) Unsubscribe(events chan []byte) { s.uc.Unsubscribe(events) }
func (s *TaskService) Trigger(ctx context.Context, id uint) error { return s.uc.Trigger(ctx, id) }
func (s *TaskService) Reload(ctx context.Context) error { return s.uc.Reload(ctx) }
func (s *TaskService) Subscribe(userID uint) chan []byte { return s.uc.Subscribe(userID) }
func (s *TaskService) Unsubscribe(userID uint, events chan []byte) { s.uc.Unsubscribe(userID, events) }
func (s *TaskService) RegisteredMethods() []*dto.TaskMethodResponse {
methods := biz.RegisteredTaskMethods()

View File

@ -56,7 +56,7 @@ func taskHTTPClient(allowPrivate bool) *http.Client {
func (e *TaskExecutor) runHTTP(ctx context.Context, task *biz.TimedTask) (string, error) {
parsed, err := url.Parse(task.HTTPURL)
if err != nil {
return "", err
return "", fmt.Errorf("URL 非法: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", fmt.Errorf("仅允许 http/https, 实际为 %q", parsed.Scheme)
@ -70,7 +70,11 @@ func (e *TaskExecutor) runHTTP(ctx context.Context, task *biz.TimedTask) (string
return "", err
}
headers := map[string]string{}
_ = json.Unmarshal(task.HTTPHeader, &headers)
if len(task.HTTPHeader) > 0 {
if err = json.Unmarshal(task.HTTPHeader, &headers); err != nil {
return "", fmt.Errorf("http_header 必须是 JSON 对象: %w", err)
}
}
for key, value := range headers {
request.Header.Set(key, value)
}
@ -118,6 +122,8 @@ func (e *TaskExecutor) runMethod(task *biz.TimedTask) error {
ttl = int(config.Media.SessionTtl)
}
err = e.media.CleanupStale(ctx, ttl)
default:
err = fmt.Errorf("方法 %s 未注册(需在 initialize/timer.go 经 task.Register 注册)", task.MethodName)
}
done <- err
}()

View File

@ -14,6 +14,7 @@ import (
type TaskScheduler struct {
tasks *biz.TaskUsecase
authorities *biz.AuthorityUsecase
executor *TaskExecutor
logger *slog.Logger
standard *cron.Cron
@ -24,7 +25,7 @@ type TaskScheduler struct {
runContext context.Context
cancel context.CancelFunc
subMu sync.RWMutex
subscribers map[chan []byte]struct{}
subscribers map[uint]map[chan []byte]struct{}
}
type scheduledEntry struct {
@ -32,8 +33,8 @@ type scheduledEntry struct {
entry cron.EntryID
}
func NewTaskScheduler(tasks *biz.TaskUsecase, executor *TaskExecutor, logger *slog.Logger) *TaskScheduler {
return &TaskScheduler{tasks: tasks, executor: executor, logger: logger, standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, subscribers: map[chan []byte]struct{}{}}
func NewTaskScheduler(tasks *biz.TaskUsecase, authorities *biz.AuthorityUsecase, executor *TaskExecutor, logger *slog.Logger) *TaskScheduler {
return &TaskScheduler{tasks: tasks, authorities: authorities, executor: executor, logger: logger, standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, subscribers: map[uint]map[chan []byte]struct{}{}}
}
func NewTaskRuntime(scheduler *TaskScheduler) biz.TaskRuntime { return scheduler }
@ -131,7 +132,12 @@ func (s *TaskScheduler) executionContext() context.Context {
func (s *TaskScheduler) run(task *biz.TimedTask, trigger string) {
log := s.executor.Run(s.executionContext(), task, trigger)
if log.Status != "success" {
s.Broadcast(map[string]any{"taskId": task.ID, "taskName": task.Name, "status": log.Status, "errorMsg": log.ErrorMsg, "time": time.Now()})
ids, err := s.authorities.AuthorityUserIDs(context.Background(), 888)
if err != nil {
s.logger.Error("query timed task alert recipients failed", "error", err)
return
}
s.PublishToUsers(ids, map[string]any{"taskId": task.ID, "name": task.Name, "error": log.ErrorMsg, "time": time.Now().Format(time.RFC3339)})
}
}
@ -196,32 +202,44 @@ func (s *TaskScheduler) Trigger(task *biz.TimedTask) {
go s.run(&copy, "manual")
}
func (s *TaskScheduler) Subscribe() chan []byte {
func (s *TaskScheduler) Subscribe(userID uint) chan []byte {
ch := make(chan []byte, 16)
s.subMu.Lock()
s.subscribers[ch] = struct{}{}
if s.subscribers[userID] == nil {
s.subscribers[userID] = map[chan []byte]struct{}{}
}
s.subscribers[userID][ch] = struct{}{}
s.subMu.Unlock()
return ch
}
func (s *TaskScheduler) Unsubscribe(ch chan []byte) {
func (s *TaskScheduler) Unsubscribe(userID uint, ch chan []byte) {
s.subMu.Lock()
if _, ok := s.subscribers[ch]; ok {
delete(s.subscribers, ch)
if subscribers := s.subscribers[userID]; subscribers != nil {
if _, ok := subscribers[ch]; !ok {
s.subMu.Unlock()
return
}
delete(subscribers, ch)
if len(subscribers) == 0 {
delete(s.subscribers, userID)
}
close(ch)
}
s.subMu.Unlock()
}
func (s *TaskScheduler) Broadcast(value any) {
func (s *TaskScheduler) PublishToUsers(userIDs []uint, value any) {
raw, err := json.Marshal(value)
if err != nil {
raw = []byte(fmt.Sprint(value))
}
s.subMu.RLock()
defer s.subMu.RUnlock()
for ch := range s.subscribers {
select {
case ch <- raw:
default:
for _, userID := range userIDs {
for ch := range s.subscribers[userID] {
select {
case ch <- raw:
default:
}
}
}
}

View File

@ -22,6 +22,7 @@ type Claims struct {
NickName string
AuthorityID uint `json:"AuthorityId"`
BufferTime int64
UserType string
MustChangePwd bool `json:"mustChangePwd"`
jwt.RegisteredClaims
}
@ -31,7 +32,7 @@ func Generate(secret, issuer string, expires, buffer time.Duration, userID, auth
return "", nil, errors.New("empty JWT signing key")
}
now := time.Now()
claims := &Claims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), MustChangePwd: mustChange, RegisteredClaims: jwt.RegisteredClaims{Issuer: issuer, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now.Add(-time.Second)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}}
claims := &Claims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), UserType: "admin", MustChangePwd: mustChange, RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"GVA"}, Issuer: issuer, NotBefore: jwt.NewNumericDate(now.Add(-1000)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
return token, claims, err
}