feat: add IM file policy and org exports

This commit is contained in:
richjohnrambo
2026-08-21 23:07:06 +08:00
parent 204f96e07a
commit 5c3f6ae58d
19 changed files with 912 additions and 3 deletions
+20
View File
@@ -0,0 +1,20 @@
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("ImFilePolicySwitchOptions", "IM文件策略开关选项", ImFilePolicySwitchOptions)
}
const (
ImFilePolicySwitchOff = 0
ImFilePolicySwitchOn = 1
)
var ImFilePolicySwitchOptions = []*model.Option{
dict.GenWarningOption(ImFilePolicySwitchOff, "关闭"),
dict.GenSuccessOption(ImFilePolicySwitchOn, "开启"),
}
@@ -0,0 +1,31 @@
package sys
import (
"context"
"hotgo/api/admin/imfilepolicy"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImFilePolicy = cImFilePolicy{}
type cImFilePolicy struct{}
func (c *cImFilePolicy) List(ctx context.Context, req *imfilepolicy.ListReq) (res *imfilepolicy.ListRes, err error) {
list, totalCount, err := service.SysImFilePolicy().List(ctx, &req.ImFilePolicyListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImFilePolicyListModel{}
}
res = new(imfilepolicy.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImFilePolicy) Update(ctx context.Context, req *imfilepolicy.UpdateReq) (res *imfilepolicy.UpdateRes, err error) {
err = service.SysImFilePolicy().Update(ctx, &req.ImFilePolicyUpdateInp)
return
}
@@ -30,6 +30,11 @@ func (c *cImOrg) DepartmentEdit(ctx context.Context, req *imorg.DepartmentEditRe
return
}
func (c *cImOrg) DepartmentExport(ctx context.Context, req *imorg.DepartmentExportReq) (res *imorg.DepartmentExportRes, err error) {
err = service.SysImOrg().DepartmentExport(ctx, &req.ImOrgDepartmentListInp)
return
}
func (c *cImOrg) DepartmentDelete(ctx context.Context, req *imorg.DepartmentDeleteReq) (res *imorg.DepartmentDeleteRes, err error) {
err = service.SysImOrg().DepartmentDelete(ctx, &req.ImOrgDepartmentDeleteInp)
return
@@ -54,6 +59,11 @@ func (c *cImOrg) MemberEdit(ctx context.Context, req *imorg.MemberEditReq) (res
return
}
func (c *cImOrg) MemberExport(ctx context.Context, req *imorg.MemberExportReq) (res *imorg.MemberExportRes, err error) {
err = service.SysImOrg().MemberExport(ctx, &req.ImOrgMemberListInp)
return
}
func (c *cImOrg) MemberDelete(ctx context.Context, req *imorg.MemberDeleteReq) (res *imorg.MemberDeleteRes, err error) {
err = service.SysImOrg().MemberDelete(ctx, &req.ImOrgMemberDeleteInp)
return
+169
View File
@@ -0,0 +1,169 @@
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/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type sSysImFilePolicy struct{}
func NewSysImFilePolicy() *sSysImFilePolicy {
return &sSysImFilePolicy{}
}
func init() {
service.RegisterSysImFilePolicy(NewSysImFilePolicy())
}
func (s *sSysImFilePolicy) List(ctx context.Context, in *sysin.ImFilePolicyListInp) (list []*sysin.ImFilePolicyListModel, 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.Purpose != "" {
mod = mod.Where("purpose", in.Purpose)
}
if in.AutoCleanup != nil {
mod = mod.Where("auto_cleanup", *in.AutoCleanup)
}
if in.ScanEnabled != nil {
mod = mod.Where("scan_enabled", *in.ScanEnabled)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("id")
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 *sSysImFilePolicy) Update(ctx context.Context, in *sysin.ImFilePolicyUpdateInp) (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
}
allowedMimes := "[]"
if in.AllowedMimes != nil && in.AllowedMimes.String() != "" && in.AllowedMimes.String() != "null" {
allowedMimes = in.AllowedMimes.String()
}
if _, err = imtenant.RegistryDB().Model("im_file_policy").Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).
Where("purpose", in.Purpose).
Data(g.Map{
"max_size": in.MaxSize,
"retention_days": in.RetentionDays,
"allowed_mimes": allowedMimes,
"auto_cleanup": in.AutoCleanup,
"scan_enabled": in.ScanEnabled,
"remark": in.Remark,
"updated_at": gtime.Now(),
"updated_by": contexts.GetUserId(ctx),
}).Update(); err != nil {
return gerror.Wrap(err, "更新IM文件策略失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_file_policy_update", "im_file_policy", 0, "success", g.Map{
"purpose": in.Purpose, "auto_cleanup": in.AutoCleanup, "scan_enabled": in.ScanEnabled,
})
return nil
}
func (s *sSysImFilePolicy) model(ctx context.Context) *gdb.Model {
return imtenant.RegistryDB().Model("im_file_policy").Safe().Ctx(ctx).Fields(
"id",
"tenant_id",
"purpose",
"max_size",
"retention_days",
"CAST(allowed_mimes AS CHAR) allowed_mimes",
"auto_cleanup",
"scan_enabled",
"remark",
"updated_at",
)
}
func (s *sSysImFilePolicy) 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 *sSysImFilePolicy) ensureRows(ctx context.Context, tenantId int64) error {
for _, item := range defaultFilePolicies() {
count, err := imtenant.RegistryDB().Model("im_file_policy").Safe().Ctx(ctx).
Where("tenant_id", tenantId).
Where("purpose", item.Purpose).
Count()
if err != nil {
return gerror.Wrap(err, "检查IM文件策略失败")
}
if count > 0 {
continue
}
_, err = imtenant.RegistryDB().Exec(ctx, `INSERT INTO im_file_policy
(tenant_id,purpose,max_size,retention_days,allowed_mimes,auto_cleanup,scan_enabled,remark,created_by,updated_by)
VALUES(?,?,?,?,CAST(? AS JSON),?,?,?,?,?)`,
tenantId, item.Purpose, item.MaxSize, item.RetentionDays, item.AllowedMimes, item.AutoCleanup, item.ScanEnabled, item.Remark, contexts.GetUserId(ctx), contexts.GetUserId(ctx))
if err != nil {
return gerror.Wrap(err, "初始化IM文件策略失败")
}
}
return nil
}
type filePolicySeed struct {
Purpose string
MaxSize int64
RetentionDays int
AllowedMimes string
AutoCleanup int
ScanEnabled int
Remark string
}
func defaultFilePolicies() []*filePolicySeed {
const mb = 1024 * 1024
return []*filePolicySeed{
{Purpose: consts.ImFilePurposeChat, MaxSize: 100 * mb, RetentionDays: 365, AllowedMimes: "[]", AutoCleanup: 0, ScanEnabled: 0, Remark: "聊天附件默认策略"},
{Purpose: consts.ImFilePurposeUserAvatar, MaxSize: 5 * mb, RetentionDays: 0, AllowedMimes: `["image/jpeg","image/png","image/webp"]`, AutoCleanup: 0, ScanEnabled: 0, Remark: "用户头像长期保留"},
{Purpose: consts.ImFilePurposeGroupAvatar, MaxSize: 5 * mb, RetentionDays: 0, AllowedMimes: `["image/jpeg","image/png","image/webp"]`, AutoCleanup: 0, ScanEnabled: 0, Remark: "群头像长期保留"},
{Purpose: consts.ImFilePurposeGroupFile, MaxSize: 200 * mb, RetentionDays: 365, AllowedMimes: "[]", AutoCleanup: 0, ScanEnabled: 0, Remark: "群文件默认策略"},
{Purpose: consts.ImFilePurposeAbuseEvidence, MaxSize: 50 * mb, RetentionDays: 730, AllowedMimes: "[]", AutoCleanup: 0, ScanEnabled: 0, Remark: "举报证据默认策略"},
{Purpose: consts.ImFilePurposeFeedback, MaxSize: 50 * mb, RetentionDays: 365, AllowedMimes: "[]", AutoCleanup: 0, ScanEnabled: 0, Remark: "反馈附件默认策略"},
}
}
+47
View File
@@ -2,16 +2,21 @@ package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/excel"
"strings"
"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/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/guid"
)
@@ -58,6 +63,27 @@ func (s *sSysImOrg) DepartmentList(ctx context.Context, in *sysin.ImOrgDepartmen
return
}
func (s *sSysImOrg) DepartmentExport(ctx context.Context, in *sysin.ImOrgDepartmentListInp) (err error) {
list, totalCount, err := s.DepartmentList(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(sysin.ImOrgDepartmentExportModel{})
if err != nil {
return
}
var exports []sysin.ImOrgDepartmentExportModel
if err = gconv.Scan(list, &exports); err != nil {
return
}
fileName := "导出IM组织部门-" + gctx.CtxId(ctx)
sheetName := fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(list))
if len(list) > 0 {
_ = writeImAdminAudit(ctx, list[0].TenantId, "im_org_department_export", "im_org_department", 0, "success", g.Map{"count": len(list)})
}
return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
}
func (s *sSysImOrg) DepartmentEdit(ctx context.Context, in *sysin.ImOrgDepartmentEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
@@ -199,6 +225,27 @@ func (s *sSysImOrg) MemberList(ctx context.Context, in *sysin.ImOrgMemberListInp
return
}
func (s *sSysImOrg) MemberExport(ctx context.Context, in *sysin.ImOrgMemberListInp) (err error) {
list, totalCount, err := s.MemberList(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(sysin.ImOrgMemberExportModel{})
if err != nil {
return
}
var exports []sysin.ImOrgMemberExportModel
if err = gconv.Scan(list, &exports); err != nil {
return
}
fileName := "导出IM组织成员-" + gctx.CtxId(ctx)
sheetName := fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(list))
if len(list) > 0 {
_ = writeImAdminAudit(ctx, list[0].TenantId, "im_org_member_export", "im_org_member", 0, "success", g.Map{"count": len(list), "department_id": in.DepartmentId})
}
return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
}
func (s *sSysImOrg) MemberEdit(ctx context.Context, in *sysin.ImOrgMemberEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
@@ -0,0 +1,75 @@
package sysin
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"strings"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImFilePolicyListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
Purpose string `json:"purpose" dc:"用途"`
AutoCleanup *int `json:"autoCleanup" dc:"自动清理"`
ScanEnabled *int `json:"scanEnabled" dc:"病毒扫描"`
}
func (in *ImFilePolicyListInp) Filter(ctx context.Context) (err error) { return }
type ImFilePolicyListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
Purpose string `json:"purpose" dc:"用途"`
MaxSize int64 `json:"maxSize" dc:"单文件大小上限"`
RetentionDays int `json:"retentionDays" dc:"保留天数"`
AllowedMimes string `json:"allowedMimes" dc:"允许MIME JSON"`
AutoCleanup int `json:"autoCleanup" dc:"自动清理"`
ScanEnabled int `json:"scanEnabled" dc:"病毒扫描"`
Remark string `json:"remark" dc:"备注"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImFilePolicyUpdateInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Purpose string `json:"purpose" v:"required#用途不能为空" dc:"用途"`
MaxSize int64 `json:"maxSize" dc:"单文件大小上限"`
RetentionDays int `json:"retentionDays" dc:"保留天数"`
AllowedMimes *gjson.Json `json:"allowedMimes" dc:"允许MIME JSON"`
AutoCleanup int `json:"autoCleanup" dc:"自动清理"`
ScanEnabled int `json:"scanEnabled" dc:"病毒扫描"`
Remark string `json:"remark" dc:"备注"`
}
func (in *ImFilePolicyUpdateInp) Filter(ctx context.Context) (err error) {
in.Purpose = strings.TrimSpace(in.Purpose)
in.Remark = strings.TrimSpace(in.Remark)
if !dict.HasOptionKey(consts.ImFilePurposeOptions, in.Purpose) {
return gerror.New("文件用途不合法")
}
if in.Purpose == consts.ImFilePurposeUnknown || in.Purpose == consts.ImFilePurposeOrphan {
return gerror.New("该文件用途不允许配置策略")
}
if in.MaxSize <= 0 {
return gerror.New("单文件大小上限必须大于0")
}
if in.RetentionDays < 0 {
return gerror.New("保留天数不能小于0")
}
if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.AutoCleanup) {
return gerror.New("自动清理开关不合法")
}
if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.ScanEnabled) {
return gerror.New("病毒扫描开关不合法")
}
if len([]rune(in.Remark)) > 200 {
return gerror.New("备注不能超过200个字符")
}
return nil
}
@@ -38,6 +38,20 @@ type ImOrgDepartmentModel struct {
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImOrgDepartmentExportModel struct {
TenantName string `json:"tenantName" dc:"租户名称"`
DepartmentId string `json:"departmentId" dc:"部门ID"`
Name string `json:"name" dc:"部门名称"`
ParentId string `json:"parentId" dc:"上级部门ID"`
SortOrder int `json:"sortOrder" dc:"排序"`
Version int `json:"version" dc:"版本"`
State string `json:"state" dc:"状态"`
ChildCount int `json:"childCount" dc:"子部门数"`
MemberCount int `json:"memberCount" dc:"成员数"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImOrgDepartmentEditInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" dc:"ID"`
@@ -112,6 +126,23 @@ type ImOrgMemberModel struct {
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImOrgMemberExportModel struct {
TenantName string `json:"tenantName" dc:"租户名称"`
DepartmentId string `json:"departmentId" dc:"部门ID"`
DepartmentName string `json:"departmentName" dc:"部门名称"`
UserId int64 `json:"userId" dc:"用户ID"`
DisplayName string `json:"displayName" dc:"展示名称"`
Position string `json:"position" dc:"岗位"`
Mobile string `json:"mobile" dc:"手机号"`
Email string `json:"email" dc:"邮箱"`
EmploymentState string `json:"employmentState" dc:"在职状态"`
Contactable int `json:"contactable" dc:"是否可联系"`
SortOrder int `json:"sortOrder" dc:"排序"`
Version int `json:"version" dc:"版本"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImOrgMemberEditInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" dc:"ID"`
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImFilePolicy) // IM文件策略
}
+22
View File
@@ -296,12 +296,16 @@ type (
ISysImOrg interface {
// DepartmentList 获取IM组织部门列表
DepartmentList(ctx context.Context, in *sysin.ImOrgDepartmentListInp) (list []*sysin.ImOrgDepartmentModel, totalCount int, err error)
// DepartmentExport 导出IM组织部门
DepartmentExport(ctx context.Context, in *sysin.ImOrgDepartmentListInp) (err error)
// DepartmentEdit 新增/编辑IM组织部门
DepartmentEdit(ctx context.Context, in *sysin.ImOrgDepartmentEditInp) (err error)
// DepartmentDelete 删除IM组织部门
DepartmentDelete(ctx context.Context, in *sysin.ImOrgDepartmentDeleteInp) (err error)
// MemberList 获取IM组织成员列表
MemberList(ctx context.Context, in *sysin.ImOrgMemberListInp) (list []*sysin.ImOrgMemberModel, totalCount int, err error)
// MemberExport 导出IM组织成员
MemberExport(ctx context.Context, in *sysin.ImOrgMemberListInp) (err error)
// MemberEdit 新增/编辑IM组织成员
MemberEdit(ctx context.Context, in *sysin.ImOrgMemberEditInp) (err error)
// MemberDelete 删除IM组织成员
@@ -321,6 +325,12 @@ type (
// CleanupOrphan 清理IM孤儿文件
CleanupOrphan(ctx context.Context, in *sysin.ImFileCleanupOrphanInp) (res *sysin.ImFileCleanupOrphanModel, err error)
}
ISysImFilePolicy interface {
// List 获取IM文件策略列表
List(ctx context.Context, in *sysin.ImFilePolicyListInp) (list []*sysin.ImFilePolicyListModel, totalCount int, err error)
// Update 更新IM文件策略
Update(ctx context.Context, in *sysin.ImFilePolicyUpdateInp) (err error)
}
ISysImExternalConfig interface {
// List 获取IM外部服务配置列表
List(ctx context.Context, in *sysin.ImExternalConfigListInp) (list []*sysin.ImExternalConfigListModel, totalCount int, err error)
@@ -602,6 +612,7 @@ var (
localSysImOutbox ISysImOutbox
localSysImOrg ISysImOrg
localSysImFile ISysImFile
localSysImFilePolicy ISysImFilePolicy
localSysImExternalConfig ISysImExternalConfig
localSysImNotification ISysImNotification
localSysImTaskDashboard ISysImTaskDashboard
@@ -819,6 +830,17 @@ func RegisterSysImFile(i ISysImFile) {
localSysImFile = i
}
func SysImFilePolicy() ISysImFilePolicy {
if localSysImFilePolicy == nil {
panic("implement not found for interface ISysImFilePolicy, forgot register?")
}
return localSysImFilePolicy
}
func RegisterSysImFilePolicy(i ISysImFilePolicy) {
localSysImFilePolicy = i
}
func SysImExternalConfig() ISysImExternalConfig {
if localSysImExternalConfig == nil {
panic("implement not found for interface ISysImExternalConfig, forgot register?")