feat: add IM org and notification admin

This commit is contained in:
richjohnrambo
2026-08-21 17:54:54 +08:00
parent c89e35d0e0
commit 204f96e07a
22 changed files with 2354 additions and 0 deletions
@@ -0,0 +1,186 @@
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/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type sSysImNotification struct{}
func NewSysImNotification() *sSysImNotification {
return &sSysImNotification{}
}
func init() {
service.RegisterSysImNotification(NewSysImNotification())
}
func (s *sSysImNotification) TemplateList(ctx context.Context, in *sysin.ImNotificationTemplateListInp) (list []*sysin.ImNotificationTemplateListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScopeWithAll(ctx, in.TenantId, true)
if err != nil {
return
}
tenants, err := s.listTenants(ctx, scope.TenantId, in.TenantId)
if err != nil {
return
}
list = []*sysin.ImNotificationTemplateListModel{}
for _, tenant := range tenants {
if err = s.ensureTemplates(ctx, tenant.Id); err != nil {
return
}
var tenantList []*sysin.ImNotificationTemplateListModel
mod := imtenant.RegistryDB().Model("im_notification_template").Safe().Ctx(ctx).
Where("tenant_id", tenant.Id)
if in.Scene != "" {
mod = mod.Where("scene", in.Scene)
}
if in.Status != nil {
mod = mod.Where("status", *in.Status)
}
if err = mod.OrderAsc("id").Scan(&tenantList); err != nil {
return nil, 0, gerror.Wrap(err, "获取IM通知模板失败,请稍后重试!")
}
for _, item := range tenantList {
item.TenantName = tenant.Name
if parsed, parseErr := gjson.DecodeToJson(item.PayloadJson); parseErr == nil && parsed != nil {
item.PayloadJson = parsed.MustToJsonString()
}
}
list = append(list, tenantList...)
}
totalCount = len(list)
start, end := pageBounds(in.Page, in.PerPage, totalCount)
if start >= totalCount {
return []*sysin.ImNotificationTemplateListModel{}, totalCount, nil
}
return list[start:end], totalCount, nil
}
func (s *sSysImNotification) TemplateUpdate(ctx context.Context, in *sysin.ImNotificationTemplateUpdateInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureTemplates(ctx, scope.TenantId); err != nil {
return
}
payload := "{}"
if in.PayloadJson != nil {
payload = in.PayloadJson.MustToJsonString()
}
if _, err = imtenant.RegistryDB().Model("im_notification_template").Safe().Ctx(ctx).
Where("tenant_id", scope.TenantId).
Where("scene", in.Scene).
Data(g.Map{
"in_app_enabled": in.InAppEnabled,
"push_enabled": in.PushEnabled,
"sms_enabled": in.SmsEnabled,
"status": in.Status,
"title_template": in.TitleTemplate,
"content_template": in.ContentTemplate,
"payload_json": payload,
"updated_at": gtime.Now(),
}).Update(); err != nil {
return gerror.Wrap(err, "更新IM通知模板失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, scope.TenantId, "im_notification_template_update", "im_notification_template", 0, "success", g.Map{"scene": in.Scene})
return nil
}
func (s *sSysImNotification) listTenants(ctx context.Context, scopeTenantId, requestedTenantId int64) (tenants []*entity.ImTenant, err error) {
mod := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx)
if scopeTenantId > 0 {
mod = mod.WherePri(scopeTenantId)
} else if requestedTenantId > 0 {
mod = mod.WherePri(requestedTenantId)
}
if err = mod.OrderAsc(dao.ImTenant.Columns().Id).Scan(&tenants); err != nil {
return nil, gerror.Wrap(err, "获取IM租户失败,请稍后重试!")
}
return tenants, nil
}
func (s *sSysImNotification) ensureTemplates(ctx context.Context, tenantId int64) error {
for _, item := range defaultNotificationTemplates() {
count, err := imtenant.RegistryDB().Model("im_notification_template").Safe().Ctx(ctx).
Where("tenant_id", tenantId).
Where("scene", item.Scene).
Count()
if err != nil {
return gerror.Wrap(err, "检查IM通知模板失败,请稍后重试!")
}
if count > 0 {
continue
}
if _, err = imtenant.RegistryDB().Model("im_notification_template").Safe().Ctx(ctx).Data(g.Map{
"tenant_id": tenantId,
"scene": item.Scene,
"name": item.Name,
"in_app_enabled": 1,
"push_enabled": item.PushEnabled,
"sms_enabled": 0,
"status": consts.ImNotificationTemplateEnabled,
"title_template": item.Title,
"content_template": item.Content,
"payload_json": gdb.Raw("CAST('{}' AS JSON)"),
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}).Insert(); err != nil {
return gerror.Wrap(err, "初始化IM通知模板失败,请稍后重试!")
}
}
return nil
}
type notificationTemplateSeed struct {
Scene string
Name string
Title string
Content string
PushEnabled int
}
func defaultNotificationTemplates() []*notificationTemplateSeed {
return []*notificationTemplateSeed{
{Scene: consts.ImNotificationSceneAccountSecurity, Name: "账号安全", Title: "账号安全提醒", Content: "你的账号发生安全事件:{{event}}", PushEnabled: 1},
{Scene: consts.ImNotificationSceneFriendRequest, Name: "好友申请", Title: "新的好友申请", Content: "{{user}} 请求添加你为好友", PushEnabled: 1},
{Scene: consts.ImNotificationSceneGroupEvent, Name: "群事件", Title: "群通知", Content: "{{group}} 有新的群事件:{{event}}", PushEnabled: 1},
{Scene: consts.ImNotificationSceneFileEvent, Name: "文件事件", Title: "文件通知", Content: "{{file}} 状态已更新", PushEnabled: 0},
{Scene: consts.ImNotificationSceneMeetingReminder, Name: "会议提醒", Title: "会议提醒", Content: "{{meeting}} 将于 {{time}} 开始", PushEnabled: 1},
{Scene: consts.ImNotificationSceneReportReminder, Name: "日报月报提醒", Title: "填报提醒", Content: "请及时提交 {{report_type}}", PushEnabled: 1},
{Scene: consts.ImNotificationSceneWorkOrder, Name: "工单处理", Title: "工单状态更新", Content: "你的工单 {{ticket}} 已更新", PushEnabled: 1},
}
}
func pageBounds(page, perPage, total int) (start, end int) {
if page <= 0 {
page = 1
}
if perPage <= 0 {
perPage = 10
}
start = (page - 1) * perPage
end = start + perPage
if end > total {
end = total
}
return
}
+362
View File
@@ -0,0 +1,362 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
"github.com/gogf/gf/v2/util/guid"
)
type sSysImOrg struct{}
func NewSysImOrg() *sSysImOrg {
return &sSysImOrg{}
}
func init() {
service.RegisterSysImOrg(NewSysImOrg())
}
func (s *sSysImOrg) DepartmentList(ctx context.Context, in *sysin.ImOrgDepartmentListInp) (list []*sysin.ImOrgDepartmentModel, 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.departmentModel(ctx, db)
if in.DepartmentId != "" {
mod = mod.Where("d.department_id", strings.TrimSpace(in.DepartmentId))
}
if in.ParentId != "" {
mod = mod.Where("d.parent_id", strings.TrimSpace(in.ParentId))
}
if in.Name != "" {
mod = mod.WhereLike("d.name", "%"+strings.TrimSpace(in.Name)+"%")
}
if in.State != "" {
mod = mod.Where("d.state", strings.TrimSpace(in.State))
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("d.sort_order").OrderAsc("d.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
}
func (s *sSysImOrg) DepartmentEdit(ctx context.Context, in *sysin.ImOrgDepartmentEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if in.ParentId != "root" {
if exists, err := s.departmentExists(ctx, db, in.ParentId); err != nil {
return err
} else if !exists {
return gerror.New("上级部门不存在")
}
}
if in.Id > 0 {
current, err := db.Model("im_org_departments").Safe().Ctx(ctx).Where("id", in.Id).One()
if err != nil {
return gerror.Wrap(err, "获取IM组织部门失败,请稍后重试!")
}
if current.IsEmpty() {
return gerror.New("部门不存在")
}
departmentId := current["department_id"].String()
if in.ParentId == departmentId {
return gerror.New("上级部门不能选择自身")
}
if err = s.ensureNoDepartmentCycle(ctx, db, departmentId, in.ParentId); err != nil {
return err
}
if _, err = db.Model("im_org_departments").Safe().Ctx(ctx).
Where("id", in.Id).
Data(g.Map{
"name": in.Name,
"parent_id": in.ParentId,
"sort_order": in.SortOrder,
"state": in.State,
"version": gdb.Raw("version+1"),
"updated_at": gtime.Now(),
}).Update(); err != nil {
return gerror.Wrap(err, "更新IM组织部门失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_org_department_update", "im_org_department", in.Id, "success", g.Map{"department_id": departmentId})
return nil
}
departmentId := in.DepartmentId
if departmentId == "" {
departmentId = "dep_" + strings.ToLower(guid.S())
}
if exists, err := s.departmentExists(ctx, db, departmentId); err != nil {
return err
} else if exists {
return gerror.New("部门ID已存在")
}
if _, err = db.Model("im_org_departments").Safe().Ctx(ctx).Data(g.Map{
"department_id": departmentId,
"name": in.Name,
"parent_id": in.ParentId,
"sort_order": in.SortOrder,
"state": in.State,
"version": 1,
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}).Insert(); err != nil {
return gerror.Wrap(err, "创建IM组织部门失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_org_department_create", "im_org_department", 0, "success", g.Map{"department_id": departmentId})
return nil
}
func (s *sSysImOrg) DepartmentDelete(ctx context.Context, in *sysin.ImOrgDepartmentDeleteInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if in.DepartmentId == "root" {
return gerror.New("根部门不能删除")
}
if exists, err := s.departmentExists(ctx, db, in.DepartmentId); err != nil {
return err
} else if !exists {
return gerror.New("部门不存在")
}
childCount, err := db.Model("im_org_departments").Safe().Ctx(ctx).Where("parent_id", in.DepartmentId).Count()
if err != nil {
return gerror.Wrap(err, "检查子部门失败,请稍后重试!")
}
if childCount > 0 {
return gerror.New("存在子部门,请先处理后再删除")
}
memberCount, err := db.Model("im_org_members").Safe().Ctx(ctx).Where("department_id", in.DepartmentId).Count()
if err != nil {
return gerror.Wrap(err, "检查部门成员失败,请稍后重试!")
}
if memberCount > 0 {
return gerror.New("存在部门成员,请先移除后再删除")
}
if _, err = db.Model("im_org_departments").Safe().Ctx(ctx).Where("department_id", in.DepartmentId).Delete(); err != nil {
return gerror.Wrap(err, "删除IM组织部门失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_org_department_delete", "im_org_department", 0, "success", g.Map{"department_id": in.DepartmentId})
return nil
}
func (s *sSysImOrg) MemberList(ctx context.Context, in *sysin.ImOrgMemberListInp) (list []*sysin.ImOrgMemberModel, 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.memberModel(ctx, db)
if in.DepartmentId != "" {
mod = mod.Where("m.department_id", strings.TrimSpace(in.DepartmentId))
}
if in.UserId > 0 {
mod = mod.Where("m.user_id", in.UserId)
}
if in.EmploymentState != "" {
mod = mod.Where("m.employment_state", strings.TrimSpace(in.EmploymentState))
}
if q := strings.TrimSpace(in.Query); q != "" {
like := "%" + q + "%"
mod = mod.Where("(m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?)", like, like, like, like)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("m.sort_order").OrderAsc("m.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
}
func (s *sSysImOrg) MemberEdit(ctx context.Context, in *sysin.ImOrgMemberEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if exists, err := s.departmentExists(ctx, db, in.DepartmentId); err != nil {
return err
} else if !exists {
return gerror.New("部门不存在")
}
userCount, err := db.Model("im_users").Safe().Ctx(ctx).Where("id", in.UserId).Count()
if err != nil {
return gerror.Wrap(err, "检查IM用户失败,请稍后重试!")
}
if userCount == 0 {
return gerror.New("IM用户不存在")
}
data := g.Map{
"department_id": in.DepartmentId,
"user_id": in.UserId,
"display_name": in.DisplayName,
"position": in.Position,
"avatar_file_id": in.AvatarFileId,
"mobile": in.Mobile,
"email": in.Email,
"employment_state": in.EmploymentState,
"contactable": in.Contactable,
"sort_order": in.SortOrder,
"updated_at": gtime.Now(),
}
if in.Id > 0 {
data["version"] = gdb.Raw("version+1")
if _, err = db.Model("im_org_members").Safe().Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新IM组织成员失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_org_member_update", "im_org_member", in.Id, "success", g.Map{"department_id": in.DepartmentId, "user_id": in.UserId})
return nil
}
data["version"] = 1
data["created_at"] = gtime.Now()
if _, err = db.Model("im_org_members").Safe().Ctx(ctx).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "添加IM组织成员失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_org_member_create", "im_org_member", 0, "success", g.Map{"department_id": in.DepartmentId, "user_id": in.UserId})
return nil
}
func (s *sSysImOrg) MemberDelete(ctx context.Context, in *sysin.ImOrgMemberDeleteInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
current, err := db.Model("im_org_members").Safe().Ctx(ctx).Where("id", in.Id).One()
if err != nil {
return gerror.Wrap(err, "获取IM组织成员失败,请稍后重试!")
}
if current.IsEmpty() {
return gerror.New("组织成员不存在")
}
if _, err = db.Model("im_org_members").Safe().Ctx(ctx).Where("id", in.Id).Delete(); err != nil {
return gerror.Wrap(err, "删除IM组织成员失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_org_member_delete", "im_org_member", in.Id, "success", g.Map{"department_id": current["department_id"].String(), "user_id": current["user_id"].Int64()})
return nil
}
func (s *sSysImOrg) Search(ctx context.Context, in *sysin.ImOrgSearchInp) (res *sysin.ImOrgSearchModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
departments, _, err := s.DepartmentList(ctx, &sysin.ImOrgDepartmentListInp{
PageReq: form.PageReq{Page: 1, PerPage: 20, Pagination: true},
TenantId: in.TenantId,
Name: in.Query,
State: consts.ImOrgDepartmentStateActive,
})
if err != nil {
return nil, err
}
members, _, err := s.MemberList(ctx, &sysin.ImOrgMemberListInp{
PageReq: form.PageReq{Page: 1, PerPage: 20, Pagination: true},
TenantId: in.TenantId,
Query: in.Query,
EmploymentState: consts.ImOrgEmploymentStateActive,
})
if err != nil {
return nil, err
}
res = &sysin.ImOrgSearchModel{Departments: departments, Members: members}
return
}
func (s *sSysImOrg) departmentModel(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model("im_org_departments d").Safe().Ctx(ctx).Fields(
"d.id",
"d.department_id",
"d.name",
"d.parent_id",
"d.sort_order",
"d.version",
"d.state",
"d.created_at",
"d.updated_at",
"(SELECT COUNT(*) FROM im_org_departments child WHERE child.parent_id=d.department_id AND child.state='active') child_count",
"(SELECT COUNT(*) FROM im_org_members m WHERE m.department_id=d.department_id) member_count",
)
}
func (s *sSysImOrg) memberModel(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model("im_org_members m").Safe().Ctx(ctx).
LeftJoin("im_org_departments d", "d.department_id=m.department_id").
Fields(
"m.id",
"m.department_id",
"COALESCE(d.name,'') department_name",
"m.user_id",
"m.display_name",
"m.position",
"m.avatar_file_id",
"m.mobile",
"m.email",
"m.employment_state",
"m.contactable",
"m.sort_order",
"m.version",
"m.created_at",
"m.updated_at",
)
}
func (s *sSysImOrg) departmentExists(ctx context.Context, db gdb.DB, departmentId string) (bool, error) {
count, err := db.Model("im_org_departments").Safe().Ctx(ctx).Where("department_id", strings.TrimSpace(departmentId)).Count()
if err != nil {
return false, gerror.Wrap(err, "检查IM组织部门失败,请稍后重试!")
}
return count > 0, nil
}
func (s *sSysImOrg) ensureNoDepartmentCycle(ctx context.Context, db gdb.DB, departmentId, parentId string) error {
next := strings.TrimSpace(parentId)
for next != "" && next != "root" {
if next == departmentId {
return gerror.New("上级部门不能选择自身或下级部门")
}
row, err := db.Model("im_org_departments").Safe().Ctx(ctx).Fields("parent_id").Where("department_id", next).One()
if err != nil {
return gerror.Wrap(err, "检查部门层级失败,请稍后重试!")
}
if row.IsEmpty() {
return gerror.New("上级部门不存在")
}
next = row["parent_id"].String()
}
return nil
}