// 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/library/contexts" "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 if !contexts.IsCompanyDept(ctx) { item.Location = "" } } 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 !contexts.IsCompanyDept(ctx) { res.Location = "" } 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) if !contexts.IsCompanyDept(ctx) { res.Locations = []string{} } _ = 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) }