feat: add IM admin P1 operations

This commit is contained in:
richjohnrambo
2026-08-21 17:25:19 +08:00
parent 77b19a8157
commit c89e35d0e0
35 changed files with 2587 additions and 53 deletions
@@ -0,0 +1,35 @@
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("ImExternalServiceOptions", "IM外部服务选项", ImExternalServiceOptions)
dict.RegisterEnums("ImExternalConfigStatusOptions", "IM外部配置状态选项", ImExternalConfigStatusOptions)
}
const (
ImExternalServiceSms = "sms"
ImExternalServicePush = "push"
ImExternalServiceTurn = "turn"
ImExternalServiceStorage = "storage"
ImExternalServiceModeration = "moderation"
ImExternalConfigStatusConfigured = "configured"
ImExternalConfigStatusUnconfigured = "unconfigured"
)
var ImExternalServiceOptions = []*model.Option{
dict.GenSuccessOption(ImExternalServiceSms, "SMS"),
dict.GenPrimaryOption(ImExternalServicePush, "Push"),
dict.GenInfoOption(ImExternalServiceTurn, "TURN/STUN"),
dict.GenWarningOption(ImExternalServiceStorage, "对象存储"),
dict.GenDefaultOption(ImExternalServiceModeration, "审核插件"),
}
var ImExternalConfigStatusOptions = []*model.Option{
dict.GenSuccessOption(ImExternalConfigStatusConfigured, "已配置"),
dict.GenWarningOption(ImExternalConfigStatusUnconfigured, "未配置"),
}
+52
View File
@@ -0,0 +1,52 @@
// Package consts
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2026 HotGo
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("ImFileStatusOptions", "IM文件状态选项", ImFileStatusOptions)
dict.RegisterEnums("ImFilePurposeOptions", "IM文件用途选项", ImFilePurposeOptions)
}
const (
ImFileStatusUploading = 0
ImFileStatusCompleted = 1
ImFileStatusFailed = 2
ImFileStatusDeleted = 3
ImFileStatusQuarantined = -1
ImFilePurposeUnknown = "unknown"
ImFilePurposeChat = "chat"
ImFilePurposeUserAvatar = "user_avatar"
ImFilePurposeGroupAvatar = "group_avatar"
ImFilePurposeGroupFile = "group_file"
ImFilePurposeAbuseEvidence = "abuse_evidence"
ImFilePurposeFeedback = "feedback"
ImFilePurposeOrphan = "orphan"
)
var ImFileStatusOptions = []*model.Option{
dict.GenWarningOption(ImFileStatusUploading, "上传中"),
dict.GenSuccessOption(ImFileStatusCompleted, "已完成"),
dict.GenErrorOption(ImFileStatusFailed, "失败"),
dict.GenDefaultOption(ImFileStatusDeleted, "已删除"),
dict.GenErrorOption(ImFileStatusQuarantined, "已隔离"),
}
var ImFilePurposeOptions = []*model.Option{
dict.GenDefaultOption(ImFilePurposeUnknown, "未知"),
dict.GenSuccessOption(ImFilePurposeChat, "聊天附件"),
dict.GenInfoOption(ImFilePurposeUserAvatar, "用户头像"),
dict.GenInfoOption(ImFilePurposeGroupAvatar, "群头像"),
dict.GenPrimaryOption(ImFilePurposeGroupFile, "群文件"),
dict.GenWarningOption(ImFilePurposeAbuseEvidence, "举报证据"),
dict.GenWarningOption(ImFilePurposeFeedback, "反馈附件"),
dict.GenDefaultOption(ImFilePurposeOrphan, "孤儿文件"),
}
@@ -0,0 +1,22 @@
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("ImTaskDashboardStatusOptions", "IM任务看板状态选项", ImTaskDashboardStatusOptions)
}
const (
ImTaskDashboardStatusHealthy = "healthy"
ImTaskDashboardStatusWarning = "warning"
ImTaskDashboardStatusError = "error"
)
var ImTaskDashboardStatusOptions = []*model.Option{
dict.GenSuccessOption(ImTaskDashboardStatusHealthy, "正常"),
dict.GenWarningOption(ImTaskDashboardStatusWarning, "待处理"),
dict.GenErrorOption(ImTaskDashboardStatusError, "异常"),
}
@@ -0,0 +1,46 @@
package sys
import (
"context"
"hotgo/api/admin/imexternalconfig"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImExternalConfig = cImExternalConfig{}
type cImExternalConfig struct{}
func (c *cImExternalConfig) List(ctx context.Context, req *imexternalconfig.ListReq) (res *imexternalconfig.ListRes, err error) {
list, totalCount, err := service.SysImExternalConfig().List(ctx, &req.ImExternalConfigListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImExternalConfigListModel{}
}
res = new(imexternalconfig.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImExternalConfig) View(ctx context.Context, req *imexternalconfig.ViewReq) (res *imexternalconfig.ViewRes, err error) {
data, err := service.SysImExternalConfig().View(ctx, &req.ImExternalConfigViewInp)
if err != nil {
return
}
res = new(imexternalconfig.ViewRes)
res.ImExternalConfigViewModel = data
return
}
func (c *cImExternalConfig) Update(ctx context.Context, req *imexternalconfig.UpdateReq) (res *imexternalconfig.UpdateRes, err error) {
err = service.SysImExternalConfig().Update(ctx, &req.ImExternalConfigUpdateInp)
return
}
func (c *cImExternalConfig) Check(ctx context.Context, req *imexternalconfig.CheckReq) (res *imexternalconfig.CheckRes, err error) {
err = service.SysImExternalConfig().Check(ctx, &req.ImExternalConfigCheckInp)
return
}
@@ -0,0 +1,68 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2026 HotGo
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"hotgo/api/admin/imfile"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var (
ImFile = cImFile{}
)
type cImFile struct{}
// List 查看IM文件列表
func (c *cImFile) List(ctx context.Context, req *imfile.ListReq) (res *imfile.ListRes, err error) {
list, totalCount, err := service.SysImFile().List(ctx, &req.ImFileListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImFileListModel{}
}
res = new(imfile.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// View 获取IM文件详情
func (c *cImFile) View(ctx context.Context, req *imfile.ViewReq) (res *imfile.ViewRes, err error) {
data, err := service.SysImFile().View(ctx, &req.ImFileViewInp)
if err != nil {
return
}
res = new(imfile.ViewRes)
res.ImFileViewModel = data
return
}
// Quarantine 隔离IM文件
func (c *cImFile) Quarantine(ctx context.Context, req *imfile.QuarantineReq) (res *imfile.QuarantineRes, err error) {
err = service.SysImFile().Quarantine(ctx, &req.ImFileActionInp)
return
}
// Delete 删除IM文件
func (c *cImFile) Delete(ctx context.Context, req *imfile.DeleteReq) (res *imfile.DeleteRes, err error) {
err = service.SysImFile().Delete(ctx, &req.ImFileActionInp)
return
}
// CleanupOrphan 清理IM孤儿文件
func (c *cImFile) CleanupOrphan(ctx context.Context, req *imfile.CleanupOrphanReq) (res *imfile.CleanupOrphanRes, err error) {
data, err := service.SysImFile().CleanupOrphan(ctx, &req.ImFileCleanupOrphanInp)
if err != nil {
return
}
res = new(imfile.CleanupOrphanRes)
res.ImFileCleanupOrphanModel = data
return
}
@@ -0,0 +1,26 @@
package sys
import (
"context"
"hotgo/api/admin/imtaskdashboard"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImTaskDashboard = cImTaskDashboard{}
type cImTaskDashboard struct{}
func (c *cImTaskDashboard) List(ctx context.Context, req *imtaskdashboard.ListReq) (res *imtaskdashboard.ListRes, err error) {
list, totalCount, err := service.SysImTaskDashboard().List(ctx, &req.ImTaskDashboardListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImTaskDashboardListModel{}
}
res = new(imtaskdashboard.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
@@ -0,0 +1,98 @@
// Package crons
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2026 HotGo
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package crons
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/cron"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
func init() {
cron.Register(ImFileCleanupOrphan)
}
// ImFileCleanupOrphan 清理IM孤儿文件,参数:保留天数、单租户处理上限。
var ImFileCleanupOrphan = &cImFileCleanupOrphan{name: "imFileCleanupOrphan"}
type cImFileCleanupOrphan struct {
name string
}
func (c *cImFileCleanupOrphan) GetName() string {
return c.name
}
func (c *cImFileCleanupOrphan) Execute(ctx context.Context, parser *cron.Parser) (err error) {
days := 7
limit := 100
if len(parser.Args) > 0 {
days = gconv.Int(parser.Args[0])
}
if len(parser.Args) > 1 {
limit = gconv.Int(parser.Args[1])
}
if days <= 0 {
days = 7
}
if limit <= 0 || limit > 1000 {
limit = 100
}
var tenants []*entity.ImTenant
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
Where(dao.ImTenant.Columns().State, consts.ImTenantStateActive).
Scan(&tenants); err != nil {
parser.Logger.Warningf(ctx, "cron ImFileCleanupOrphan load tenants err:%+v", err)
return err
}
cutoff := gtime.Now().AddDate(0, 0, -days)
for _, tenant := range tenants {
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
if dbErr != nil {
parser.Logger.Warningf(ctx, "cron ImFileCleanupOrphan tenant:%d db err:%+v", tenant.Id, dbErr)
continue
}
rows, dbErr := db.Model("im_fileuploads fu").Safe().Ctx(ctx).
LeftJoin("im_filemsglinks fml", "fml.fileid=fu.id").
Fields("fu.id").
Where("fml.id IS NULL").
WhereLT("fu.updatedat", cutoff).
WhereNotIn("fu.status", []int{consts.ImFileStatusDeleted, consts.ImFileStatusQuarantined}).
Group("fu.id").
OrderAsc("fu.updatedat").
Limit(limit).
All()
if dbErr != nil {
parser.Logger.Warningf(ctx, "cron ImFileCleanupOrphan tenant:%d scan err:%+v", tenant.Id, dbErr)
continue
}
if len(rows) == 0 {
continue
}
ids := make([]int64, 0, len(rows))
for _, row := range rows {
ids = append(ids, row["id"].Int64())
}
if _, dbErr = db.Model("im_fileuploads").Safe().Ctx(ctx).
WhereIn("id", ids).
Data(g.Map{"status": consts.ImFileStatusDeleted, "updatedat": gtime.Now()}).
Update(); dbErr != nil {
parser.Logger.Warningf(ctx, "cron ImFileCleanupOrphan tenant:%d update err:%+v", tenant.Id, dbErr)
continue
}
parser.Logger.Infof(ctx, "cron ImFileCleanupOrphan tenant:%d count:%d", tenant.Id, len(ids))
}
return nil
}
@@ -0,0 +1,227 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type sSysImExternalConfig struct{}
func NewSysImExternalConfig() *sSysImExternalConfig { return &sSysImExternalConfig{} }
func init() {
service.RegisterSysImExternalConfig(NewSysImExternalConfig())
}
func (s *sSysImExternalConfig) List(ctx context.Context, in *sysin.ImExternalConfigListInp) (list []*sysin.ImExternalConfigListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
tenant, err := s.resolveTenant(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureRows(ctx, tenant.Id); err != nil {
return
}
mod := s.model(ctx).Where("tenant_id", tenant.Id)
if in.Service != "" {
mod = mod.Where("service", in.Service)
}
if in.Provider != "" {
mod = mod.WhereLike("provider", "%"+in.Provider+"%")
}
if in.Enabled != nil {
mod = mod.Where("enabled", *in.Enabled)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("service")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM外部服务配置失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantName = tenant.Name
}
return
}
func (s *sSysImExternalConfig) View(ctx context.Context, in *sysin.ImExternalConfigViewInp) (res *sysin.ImExternalConfigViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
tenant, err := s.resolveTenant(ctx, in.TenantId)
if err != nil {
return nil, err
}
if err = s.ensureRows(ctx, tenant.Id); err != nil {
return nil, err
}
if err = s.model(ctx).Where("tenant_id", tenant.Id).Where("service", in.Service).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取IM外部服务配置详情失败,请稍后重试!")
}
if res != nil {
res.TenantName = tenant.Name
}
return
}
func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalConfigUpdateInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
if !s.validService(in.Service) {
return gerror.New("服务类型不正确")
}
tenant, err := s.resolveTenant(ctx, in.TenantId)
if err != nil {
return err
}
if err = s.ensureRows(ctx, tenant.Id); err != nil {
return err
}
configJson := "{}"
if in.ConfigJson != nil && in.ConfigJson.String() != "" && in.ConfigJson.String() != "null" {
configJson = in.ConfigJson.String()
}
data := g.Map{
"provider": in.Provider,
"endpoint": in.Endpoint,
"enabled": in.Enabled,
"config_json": configJson,
"updated_at": gtime.Now(),
"updated_by": contexts.GetUserId(ctx),
}
if in.SecretJson != nil && in.SecretJson.String() != "" && in.SecretJson.String() != "null" && in.SecretJson.String() != "{}" {
data["secret_json"] = in.SecretJson.String()
}
if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("service", in.Service).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新IM外部服务配置失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_external_config_update", "im_external_config", 0, "success", g.Map{
"service": in.Service, "provider": in.Provider, "enabled": in.Enabled,
})
return nil
}
func (s *sSysImExternalConfig) Check(ctx context.Context, in *sysin.ImExternalConfigCheckInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
tenant, err := s.resolveTenant(ctx, in.TenantId)
if err != nil {
return err
}
if err = s.ensureRows(ctx, tenant.Id); err != nil {
return err
}
var row gdb.Record
if row, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("service", in.Service).One(); err != nil {
return gerror.Wrap(err, "检查IM外部服务配置失败,请稍后重试!")
}
status := consts.ImExternalConfigStatusConfigured
result := "配置完整"
if row == nil || row["enabled"].Int() != 1 || row["provider"].String() == "" {
status = consts.ImExternalConfigStatusUnconfigured
result = "未启用或服务商为空"
}
if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("service", in.Service).
Data(g.Map{"status": status, "checked_at": gtime.Now(), "check_result": result, "updated_at": gtime.Now()}).
Update(); err != nil {
return gerror.Wrap(err, "更新IM外部服务检查结果失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_external_config_check", "im_external_config", 0, "success", g.Map{
"service": in.Service, "status": status,
})
return nil
}
func (s *sSysImExternalConfig) model(ctx context.Context) *gdb.Model {
return imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).Fields(
"tenant_id",
"id",
"service",
"provider",
"endpoint",
"enabled",
"status",
"CAST(config_json AS CHAR) config_json",
"CASE WHEN secret_json IS NULL OR CAST(secret_json AS CHAR)='{}' THEN 0 ELSE 1 END has_secret",
"checked_at",
"check_result",
"created_at",
"updated_at",
)
}
func (s *sSysImExternalConfig) resolveTenant(ctx context.Context, selectedTenantId int64) (tenant *entity.ImTenant, err error) {
scope, err := resolveImAdminScope(ctx, selectedTenantId)
if err != nil {
return nil, err
}
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(scope.TenantId).Scan(&tenant); err != nil {
return nil, gerror.Wrap(err, "获取IM租户失败")
}
if tenant == nil {
return nil, gerror.New("IM租户不存在")
}
return tenant, nil
}
func (s *sSysImExternalConfig) ensureRows(ctx context.Context, tenantId int64) error {
services := []string{
consts.ImExternalServiceSms,
consts.ImExternalServicePush,
consts.ImExternalServiceTurn,
consts.ImExternalServiceStorage,
consts.ImExternalServiceModeration,
}
for _, serviceName := range services {
count, err := imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
Where("tenant_id", tenantId).Where("service", serviceName).Count()
if err != nil {
return gerror.Wrap(err, "初始化IM外部服务配置失败")
}
if count > 0 {
continue
}
_, err = imtenant.RegistryDB().Exec(ctx, `INSERT INTO im_external_config
(tenant_id,service,provider,endpoint,enabled,status,config_json,secret_json,created_by,updated_by)
VALUES(?,?,?,?,?,?,CAST(? AS JSON),CAST(? AS JSON),?,?)`,
tenantId, serviceName, "", "", 0, consts.ImExternalConfigStatusUnconfigured, "{}", "{}", contexts.GetUserId(ctx), contexts.GetUserId(ctx))
if err != nil {
return gerror.Wrap(err, "初始化IM外部服务配置失败")
}
}
return nil
}
func (s *sSysImExternalConfig) validService(serviceName string) bool {
switch serviceName {
case consts.ImExternalServiceSms, consts.ImExternalServicePush, consts.ImExternalServiceTurn, consts.ImExternalServiceStorage, consts.ImExternalServiceModeration:
return true
default:
return false
}
}
func externalConfigJson(value string) *gjson.Json {
if value == "" {
return gjson.New("{}")
}
return gjson.New(value)
}
+234
View File
@@ -0,0 +1,234 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2026 HotGo
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type sSysImFile struct{}
func NewSysImFile() *sSysImFile {
return &sSysImFile{}
}
func init() {
service.RegisterSysImFile(NewSysImFile())
}
// List 获取IM文件列表
func (s *sSysImFile) List(ctx context.Context, in *sysin.ImFileListInp) (list []*sysin.ImFileListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
mod := s.fileModel(ctx, db)
mod = s.applyWhere(mod, in)
mod = mod.Page(in.Page, in.PerPage).OrderDesc("fu.updatedat").OrderDesc("fu.id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM文件列表失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
// View 获取IM文件详情
func (s *sSysImFile) View(ctx context.Context, in *sysin.ImFileViewInp) (res *sysin.ImFileViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
if err = s.fileModel(ctx, db).Where("fu.id", in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取IM文件详情失败,请稍后重试!")
}
if res == nil {
return nil, nil
}
res.TenantId = tenant.Id
res.TenantName = tenant.Name
if err = db.Model("im_filemsglinks").Safe().Ctx(ctx).
Fields("id", "createdat created_at", "fileid file_id", "COALESCE(msgid,0) msg_id", "COALESCE(topic,'') topic", "COALESCE(userid,0) user_id").
Where("fileid", in.Id).
OrderDesc("id").
Limit(50).
Scan(&res.Links); err != nil {
return nil, gerror.Wrap(err, "获取IM文件引用失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_file_view", "im_file", in.Id, "success", g.Map{
"file_id": in.Id,
})
return
}
// Quarantine 隔离IM文件
func (s *sSysImFile) Quarantine(ctx context.Context, in *sysin.ImFileActionInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
return s.updateStatus(ctx, in, consts.ImFileStatusQuarantined, "im_file_quarantine")
}
// Delete 删除IM文件
func (s *sSysImFile) Delete(ctx context.Context, in *sysin.ImFileActionInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
return s.updateStatus(ctx, in, consts.ImFileStatusDeleted, "im_file_delete")
}
// CleanupOrphan 清理IM孤儿文件
func (s *sSysImFile) CleanupOrphan(ctx context.Context, in *sysin.ImFileCleanupOrphanInp) (res *sysin.ImFileCleanupOrphanModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
cutoff := gtime.Now().AddDate(0, 0, -in.Days)
rows, err := db.Model("im_fileuploads fu").Safe().Ctx(ctx).
LeftJoin("im_filemsglinks fml", "fml.fileid=fu.id").
Fields("fu.id", "fu.location").
Where("fml.id IS NULL").
WhereLT("fu.updatedat", cutoff).
WhereNotIn("fu.status", []int{consts.ImFileStatusDeleted, consts.ImFileStatusQuarantined}).
Group("fu.id").
OrderAsc("fu.updatedat").
Limit(in.Limit).
All()
if err != nil {
return nil, gerror.Wrap(err, "扫描IM孤儿文件失败,请稍后重试!")
}
res = &sysin.ImFileCleanupOrphanModel{Locations: []string{}}
if len(rows) == 0 {
return res, nil
}
ids := make([]int64, 0, len(rows))
for _, row := range rows {
ids = append(ids, row["id"].Int64())
if location := row["location"].String(); location != "" {
res.Locations = append(res.Locations, location)
}
}
if _, err = db.Model("im_fileuploads").Safe().Ctx(ctx).
WhereIn("id", ids).
Data(g.Map{"status": consts.ImFileStatusDeleted, "updatedat": gtime.Now()}).
Update(); err != nil {
return nil, gerror.Wrap(err, "标记IM孤儿文件失败,请稍后重试!")
}
res.Count = len(ids)
_ = writeImAdminAudit(ctx, tenant.Id, "im_file_cleanup_orphan", "im_file", 0, "success", g.Map{
"days": in.Days, "limit": in.Limit, "count": res.Count,
})
return
}
func (s *sSysImFile) fileModel(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model("im_fileuploads fu").Safe().Ctx(ctx).
LeftJoin("im_filemsglinks fml", "fml.fileid=fu.id").
Fields(
"fu.id",
"fu.createdat created_at",
"fu.updatedat updated_at",
"COALESCE(fu.userid,0) user_id",
"fu.status",
"fu.mimetype mime_type",
"fu.size",
"COALESCE(fu.etag,'') etag",
"fu.location",
"COUNT(fml.id) link_count",
"COALESCE(MIN(fml.topic),'') first_topic",
"COALESCE(MIN(fml.msgid),0) first_msg_id",
"MIN(fml.createdat) first_link_at",
`CASE
WHEN COUNT(fml.id)=0 THEN 'orphan'
WHEN SUM(CASE WHEN fml.topic LIKE 'grp%' THEN 1 ELSE 0 END)>0 THEN 'chat'
ELSE 'chat'
END purpose`,
).
Group("fu.id")
}
func (s *sSysImFile) applyWhere(mod *gdb.Model, in *sysin.ImFileListInp) *gdb.Model {
if in.FileId > 0 {
mod = mod.Where("fu.id", in.FileId)
}
if in.UserId > 0 {
mod = mod.Where("fu.userid", in.UserId)
}
if in.Status != nil {
mod = mod.Where("fu.status", *in.Status)
}
if in.MimeType != "" {
mod = mod.WhereLike("fu.mimetype", "%"+in.MimeType+"%")
}
if in.Topic != "" {
mod = mod.WhereLike("fml.topic", "%"+in.Topic+"%")
}
if in.Purpose == consts.ImFilePurposeOrphan {
mod = mod.Having("link_count = 0")
}
if in.Purpose != "" && in.Purpose != consts.ImFilePurposeOrphan && in.Purpose != consts.ImFilePurposeUnknown {
mod = mod.Having("purpose", in.Purpose)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween("fu.createdat", in.CreatedAt[0], in.CreatedAt[1])
}
if len(in.UpdatedAt) == 2 {
mod = mod.WhereBetween("fu.updatedat", in.UpdatedAt[0], in.UpdatedAt[1])
}
return mod
}
func (s *sSysImFile) updateStatus(ctx context.Context, in *sysin.ImFileActionInp, status int, action string) (err error) {
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return err
}
current, err := db.Model("im_fileuploads").Safe().Ctx(ctx).Where("id", in.Id).One()
if err != nil {
return gerror.Wrap(err, "获取IM文件失败,请稍后重试!")
}
if current == nil {
return gerror.New("IM文件不存在")
}
if current["status"].Int() == consts.ImFileStatusDeleted && status == consts.ImFileStatusDeleted {
return gerror.New("IM文件已删除")
}
if _, err = db.Model("im_fileuploads").Safe().Ctx(ctx).
Where("id", in.Id).
Data(g.Map{"status": status, "updatedat": gtime.Now()}).
Update(); err != nil {
return gerror.Wrap(err, "更新IM文件状态失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, action, "im_file", in.Id, "success", g.Map{
"file_id": in.Id, "before_status": current["status"].Int(), "after_status": status, "reason": in.Reason,
})
return nil
}
func (s *sSysImFile) tenantDB(ctx context.Context, selectedTenantId int64) (db gdb.DB, tenant *entity.ImTenant, err error) {
return resolveImTenantDB(ctx, selectedTenantId)
}
@@ -0,0 +1,117 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type sSysImTaskDashboard struct{}
func NewSysImTaskDashboard() *sSysImTaskDashboard { return &sSysImTaskDashboard{} }
func init() {
service.RegisterSysImTaskDashboard(NewSysImTaskDashboard())
}
func (s *sSysImTaskDashboard) List(ctx context.Context, in *sysin.ImTaskDashboardListInp) (list []*sysin.ImTaskDashboardListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScopeWithAll(ctx, in.TenantId, true)
if err != nil {
return
}
mod := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx)
if scope.TenantId > 0 {
mod = mod.WherePri(scope.TenantId)
}
if in.TenantId > 0 {
mod = mod.WherePri(in.TenantId)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc(dao.ImTenant.Columns().Id)
var tenants []*entity.ImTenant
if err = mod.ScanAndCount(&tenants, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM任务看板租户失败,请稍后重试!")
return
}
list = make([]*sysin.ImTaskDashboardListModel, 0, len(tenants))
for _, tenant := range tenants {
item := s.buildTenantSummary(ctx, tenant)
if in.Status != "" && item.Status != in.Status {
continue
}
list = append(list, item)
}
if in.Status != "" {
totalCount = len(list)
}
return
}
func (s *sSysImTaskDashboard) buildTenantSummary(ctx context.Context, tenant *entity.ImTenant) *sysin.ImTaskDashboardListModel {
item := &sysin.ImTaskDashboardListModel{
TenantId: tenant.Id,
TenantName: tenant.Name,
TenantCode: tenant.Code,
TenantState: tenant.State,
DbStatus: "ok",
Status: consts.ImTaskDashboardStatusHealthy,
RecommendedNextAction: "无需处理",
}
db, _, err := imtenant.DB(ctx, tenant.Id)
if err != nil {
item.DbStatus = "error"
item.Status = consts.ImTaskDashboardStatusError
item.RecommendedNextAction = "检查租户数据库连接"
return item
}
item.OutboxPendingCount, _ = s.count(ctx, db, "im_business_events_outbox", "status='pending'")
item.OutboxFailedCount, _ = s.count(ctx, db, "im_business_events_outbox", "status='failed'")
item.GroupOutboxPending, _ = s.count(ctx, db, "im_group_events_outbox", "status='pending'")
item.GroupOutboxFailed, _ = s.count(ctx, db, "im_group_events_outbox", "status='failed'")
item.OrphanFileCount, _ = s.countOrphanFiles(ctx, db)
item.DeletedFileCount, _ = s.count(ctx, db, "im_fileuploads", "status=3")
item.LastOutboxCreatedAt, _ = s.maxTime(ctx, db, "im_business_events_outbox", "created_at")
item.LastGroupEventAt, _ = s.maxTime(ctx, db, "im_group_events_outbox", "created_at")
item.LastFileUpdatedAt, _ = s.maxTime(ctx, db, "im_fileuploads", "updatedat")
if item.OutboxFailedCount > 0 || item.GroupOutboxFailed > 0 {
item.Status = consts.ImTaskDashboardStatusError
item.RecommendedNextAction = "处理失败Outbox事件"
} else if item.OutboxPendingCount > 0 || item.GroupOutboxPending > 0 || item.OrphanFileCount > 0 || item.DeletedFileCount > 0 {
item.Status = consts.ImTaskDashboardStatusWarning
item.RecommendedNextAction = "检查待处理任务和文件清理"
}
return item
}
func (s *sSysImTaskDashboard) count(ctx context.Context, db gdb.DB, table string, where string) (int, error) {
return db.Model(table).Safe().Ctx(ctx).Where(where).Count()
}
func (s *sSysImTaskDashboard) countOrphanFiles(ctx context.Context, db gdb.DB) (int, error) {
return db.Model("im_fileuploads fu").Safe().Ctx(ctx).
LeftJoin("im_filemsglinks fml", "fml.fileid=fu.id").
Where("fml.id IS NULL").
WhereNotIn("fu.status", []int{consts.ImFileStatusDeleted, consts.ImFileStatusQuarantined}).
Count()
}
func (s *sSysImTaskDashboard) maxTime(ctx context.Context, db gdb.DB, table string, field string) (res *gtime.Time, err error) {
var row struct {
Value *gtime.Time `orm:"value"`
}
if err = db.Model(table).Safe().Ctx(ctx).Fields("MAX(" + field + ") value").Scan(&row); err != nil {
return nil, err
}
return row.Value, nil
}
@@ -0,0 +1,85 @@
package sysin
import (
"context"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImExternalConfigListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
Service string `json:"service" dc:"服务"`
Provider string `json:"provider" dc:"服务商"`
Enabled *int `json:"enabled" dc:"是否启用"`
}
func (in *ImExternalConfigListInp) Filter(ctx context.Context) (err error) { return }
type ImExternalConfigListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
Service string `json:"service" dc:"服务"`
Provider string `json:"provider" dc:"服务商"`
Endpoint string `json:"endpoint" dc:"端点"`
Enabled int `json:"enabled" dc:"是否启用"`
Status string `json:"status" dc:"配置状态"`
ConfigJson string `json:"configJson" dc:"普通配置"`
HasSecret bool `json:"hasSecret" dc:"是否有敏感配置"`
CheckedAt *gtime.Time `json:"checkedAt" dc:"检查时间"`
CheckResult string `json:"checkResult" dc:"检查结果"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImExternalConfigViewInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Service string `json:"service" v:"required#服务不能为空" dc:"服务"`
}
func (in *ImExternalConfigViewInp) Filter(ctx context.Context) (err error) {
if in.Service == "" {
return gerror.New("服务不能为空")
}
return
}
type ImExternalConfigViewModel struct {
ImExternalConfigListModel
}
type ImExternalConfigUpdateInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Service string `json:"service" v:"required#服务不能为空" dc:"服务"`
Provider string `json:"provider" dc:"服务商"`
Endpoint string `json:"endpoint" dc:"端点"`
Enabled int `json:"enabled" dc:"是否启用"`
ConfigJson *gjson.Json `json:"configJson" dc:"普通配置"`
SecretJson *gjson.Json `json:"secretJson" dc:"敏感配置,空则保持原值"`
}
func (in *ImExternalConfigUpdateInp) Filter(ctx context.Context) (err error) {
if in.Service == "" {
return gerror.New("服务不能为空")
}
if in.Enabled != 0 && in.Enabled != 1 {
return gerror.New("启用状态不正确")
}
return
}
type ImExternalConfigCheckInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Service string `json:"service" v:"required#服务不能为空" dc:"服务"`
}
func (in *ImExternalConfigCheckInp) Filter(ctx context.Context) (err error) {
if in.Service == "" {
return gerror.New("服务不能为空")
}
return
}
@@ -0,0 +1,116 @@
// Package sysin
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2026 HotGo
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sysin
import (
"context"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImFileListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
FileId int64 `json:"fileId" dc:"文件ID"`
UserId int64 `json:"userId" dc:"上传用户ID"`
Status *int `json:"status" dc:"状态"`
Purpose string `json:"purpose" dc:"用途"`
MimeType string `json:"mimeType" dc:"MIME类型"`
Topic string `json:"topic" dc:"关联Topic"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt []*gtime.Time `json:"updatedAt" dc:"更新时间"`
}
func (in *ImFileListInp) Filter(ctx context.Context) (err error) {
return
}
type ImFileListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"文件ID"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
UserId int64 `json:"userId" dc:"上传用户ID"`
Status int `json:"status" dc:"状态"`
MimeType string `json:"mimeType" dc:"MIME类型"`
Size int64 `json:"size" dc:"大小"`
Etag string `json:"etag" dc:"ETag"`
Location string `json:"location" dc:"文件位置"`
Purpose string `json:"purpose" dc:"用途"`
LinkCount int `json:"linkCount" dc:"引用数"`
FirstTopic string `json:"firstTopic" dc:"首个关联Topic"`
FirstMsgId int64 `json:"firstMsgId" dc:"首个关联消息ID"`
FirstLinkAt *gtime.Time `json:"firstLinkAt" dc:"首个关联时间"`
}
type ImFileViewInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" v:"required#文件ID不能为空" dc:"文件ID"`
}
func (in *ImFileViewInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 {
return gerror.New("文件ID不能为空")
}
return
}
type ImFileViewModel struct {
ImFileListModel
Links []*ImFileLinkModel `json:"links" dc:"引用列表"`
}
type ImFileLinkModel struct {
Id int64 `json:"id" dc:"ID"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
FileId int64 `json:"fileId" dc:"文件ID"`
MsgId int64 `json:"msgId" dc:"消息ID"`
Topic string `json:"topic" dc:"Topic"`
UserId int64 `json:"userId" dc:"用户ID"`
}
type ImFileActionInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" v:"required#文件ID不能为空" dc:"文件ID"`
Reason string `json:"reason" dc:"原因"`
}
func (in *ImFileActionInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 {
return gerror.New("文件ID不能为空")
}
if len([]rune(in.Reason)) > 200 {
return gerror.New("原因不能超过200个字符")
}
return
}
type ImFileCleanupOrphanInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Days int `json:"days" dc:"保留天数"`
Limit int `json:"limit" dc:"处理上限"`
}
func (in *ImFileCleanupOrphanInp) Filter(ctx context.Context) (err error) {
if in.Days <= 0 {
in.Days = 7
}
if in.Limit <= 0 {
in.Limit = 100
}
if in.Limit > 1000 {
return gerror.New("单次清理上限不能超过1000")
}
return
}
type ImFileCleanupOrphanModel struct {
Count int `json:"count" dc:"标记数量"`
Locations []string `json:"locations" dc:"待物理清理位置"`
}
@@ -0,0 +1,35 @@
package sysin
import (
"context"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/os/gtime"
)
type ImTaskDashboardListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
Status string `json:"status" dc:"状态"`
}
func (in *ImTaskDashboardListInp) Filter(ctx context.Context) (err error) { return }
type ImTaskDashboardListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
TenantCode string `json:"tenantCode" dc:"租户编码"`
TenantState int `json:"tenantState" dc:"租户状态"`
DbStatus string `json:"dbStatus" dc:"数据库状态"`
Status string `json:"status" dc:"看板状态"`
OutboxPendingCount int `json:"outboxPendingCount" dc:"业务Outbox待处理"`
OutboxFailedCount int `json:"outboxFailedCount" dc:"业务Outbox失败"`
GroupOutboxPending int `json:"groupOutboxPending" dc:"群Outbox待处理"`
GroupOutboxFailed int `json:"groupOutboxFailed" dc:"群Outbox失败"`
OrphanFileCount int `json:"orphanFileCount" dc:"孤儿文件数"`
DeletedFileCount int `json:"deletedFileCount" dc:"待物理清理文件数"`
LastOutboxCreatedAt *gtime.Time `json:"lastOutboxCreatedAt" dc:"最新Outbox时间"`
LastGroupEventAt *gtime.Time `json:"lastGroupEventAt" dc:"最新群事件时间"`
LastFileUpdatedAt *gtime.Time `json:"lastFileUpdatedAt" dc:"最新文件更新时间"`
RecommendedNextAction string `json:"recommendedNextAction" dc:"建议动作"`
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImExternalConfig) // IM外部服务配置
}
@@ -0,0 +1,12 @@
// Package genrouter
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2026 HotGo
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImFile) // IM文件治理
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImTaskDashboard) // IM任务看板
}
+92 -30
View File
@@ -293,6 +293,32 @@ type (
// Dead 标记IM Outbox死信
Dead(ctx context.Context, in *sysin.ImOutboxActionInp) (err error)
}
ISysImFile interface {
// List 获取IM文件列表
List(ctx context.Context, in *sysin.ImFileListInp) (list []*sysin.ImFileListModel, totalCount int, err error)
// View 获取IM文件详情
View(ctx context.Context, in *sysin.ImFileViewInp) (res *sysin.ImFileViewModel, err error)
// Quarantine 隔离IM文件
Quarantine(ctx context.Context, in *sysin.ImFileActionInp) (err error)
// Delete 删除IM文件
Delete(ctx context.Context, in *sysin.ImFileActionInp) (err error)
// CleanupOrphan 清理IM孤儿文件
CleanupOrphan(ctx context.Context, in *sysin.ImFileCleanupOrphanInp) (res *sysin.ImFileCleanupOrphanModel, err error)
}
ISysImExternalConfig interface {
// List 获取IM外部服务配置列表
List(ctx context.Context, in *sysin.ImExternalConfigListInp) (list []*sysin.ImExternalConfigListModel, totalCount int, err error)
// View 获取IM外部服务配置详情
View(ctx context.Context, in *sysin.ImExternalConfigViewInp) (res *sysin.ImExternalConfigViewModel, err error)
// Update 更新IM外部服务配置
Update(ctx context.Context, in *sysin.ImExternalConfigUpdateInp) (err error)
// Check 检查IM外部服务配置
Check(ctx context.Context, in *sysin.ImExternalConfigCheckInp) (err error)
}
ISysImTaskDashboard interface {
// List 获取IM任务看板列表
List(ctx context.Context, in *sysin.ImTaskDashboardListInp) (list []*sysin.ImTaskDashboardListModel, totalCount int, err error)
}
ISysImAuditLog interface {
// List 获取IM审计日志列表
List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error)
@@ -536,36 +562,39 @@ type (
)
var (
localSysAddons ISysAddons
localSysAddonsConfig ISysAddonsConfig
localSysAttachment ISysAttachment
localSysBlacklist ISysBlacklist
localSysConfig ISysConfig
localSysCron ISysCron
localSysCronGroup ISysCronGroup
localSysCurdDemo ISysCurdDemo
localSysImTenant ISysImTenant
localSysImRegistration ISysImRegistration
localSysImInvite ISysImInvite
localSysImUser ISysImUser
localSysImGroup ISysImGroup
localSysImRealname ISysImRealname
localSysImWorkOrder ISysImWorkOrder
localSysImOutbox ISysImOutbox
localSysImAuditLog ISysImAuditLog
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
localSysGenCodes ISysGenCodes
localSysLog ISysLog
localSysLoginLog ISysLoginLog
localSysNormalTreeDemo ISysNormalTreeDemo
localSysOptionTreeDemo ISysOptionTreeDemo
localSysProvinces ISysProvinces
localSysServeLicense ISysServeLicense
localSysServeLog ISysServeLog
localSysSmsLog ISysSmsLog
localSysTestCategory ISysTestCategory
localSysAddons ISysAddons
localSysAddonsConfig ISysAddonsConfig
localSysAttachment ISysAttachment
localSysBlacklist ISysBlacklist
localSysConfig ISysConfig
localSysCron ISysCron
localSysCronGroup ISysCronGroup
localSysCurdDemo ISysCurdDemo
localSysImTenant ISysImTenant
localSysImRegistration ISysImRegistration
localSysImInvite ISysImInvite
localSysImUser ISysImUser
localSysImGroup ISysImGroup
localSysImRealname ISysImRealname
localSysImWorkOrder ISysImWorkOrder
localSysImOutbox ISysImOutbox
localSysImFile ISysImFile
localSysImExternalConfig ISysImExternalConfig
localSysImTaskDashboard ISysImTaskDashboard
localSysImAuditLog ISysImAuditLog
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
localSysGenCodes ISysGenCodes
localSysLog ISysLog
localSysLoginLog ISysLoginLog
localSysNormalTreeDemo ISysNormalTreeDemo
localSysOptionTreeDemo ISysOptionTreeDemo
localSysProvinces ISysProvinces
localSysServeLicense ISysServeLicense
localSysServeLog ISysServeLog
localSysSmsLog ISysSmsLog
localSysTestCategory ISysTestCategory
)
func SysAddons() ISysAddons {
@@ -744,6 +773,39 @@ func RegisterSysImOutbox(i ISysImOutbox) {
localSysImOutbox = i
}
func SysImFile() ISysImFile {
if localSysImFile == nil {
panic("implement not found for interface ISysImFile, forgot register?")
}
return localSysImFile
}
func RegisterSysImFile(i ISysImFile) {
localSysImFile = i
}
func SysImExternalConfig() ISysImExternalConfig {
if localSysImExternalConfig == nil {
panic("implement not found for interface ISysImExternalConfig, forgot register?")
}
return localSysImExternalConfig
}
func RegisterSysImExternalConfig(i ISysImExternalConfig) {
localSysImExternalConfig = i
}
func SysImTaskDashboard() ISysImTaskDashboard {
if localSysImTaskDashboard == nil {
panic("implement not found for interface ISysImTaskDashboard, forgot register?")
}
return localSysImTaskDashboard
}
func RegisterSysImTaskDashboard(i ISysImTaskDashboard) {
localSysImTaskDashboard = i
}
func SysImAuditLog() ISysImAuditLog {
if localSysImAuditLog == nil {
panic("implement not found for interface ISysImAuditLog, forgot register?")