diff --git a/server/internal/controller/admin/sys/im_audit_log.go b/server/internal/controller/admin/sys/im_audit_log.go new file mode 100644 index 0000000..460eba1 --- /dev/null +++ b/server/internal/controller/admin/sys/im_audit_log.go @@ -0,0 +1,43 @@ +// 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/imauditlog" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImAuditLog = cImAuditLog{} + +type cImAuditLog struct{} + +// List 查看IM审计日志列表 +func (c *cImAuditLog) List(ctx context.Context, req *imauditlog.ListReq) (res *imauditlog.ListRes, err error) { + list, totalCount, err := service.SysImAuditLog().List(ctx, &req.ImAuditLogListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImAuditLogListModel{} + } + res = new(imauditlog.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// View 获取指定IM审计日志信息 +func (c *cImAuditLog) View(ctx context.Context, req *imauditlog.ViewReq) (res *imauditlog.ViewRes, err error) { + data, err := service.SysImAuditLog().View(ctx, &req.ImAuditLogViewInp) + if err != nil { + return + } + res = new(imauditlog.ViewRes) + res.ImAuditLogViewModel = data + return +} diff --git a/server/internal/controller/admin/sys/im_group.go b/server/internal/controller/admin/sys/im_group.go new file mode 100644 index 0000000..ca171fd --- /dev/null +++ b/server/internal/controller/admin/sys/im_group.go @@ -0,0 +1,151 @@ +// 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/imgroup" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ( + ImGroup = cImGroup{} +) + +type cImGroup struct{} + +// List 查看IM群组列表 +func (c *cImGroup) List(ctx context.Context, req *imgroup.ListReq) (res *imgroup.ListRes, err error) { + list, totalCount, err := service.SysImGroup().List(ctx, &req.ImGroupListInp) + if err != nil { + return + } + + if list == nil { + list = []*sysin.ImGroupListModel{} + } + + res = new(imgroup.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// View 获取指定IM群组信息 +func (c *cImGroup) View(ctx context.Context, req *imgroup.ViewReq) (res *imgroup.ViewRes, err error) { + data, err := service.SysImGroup().View(ctx, &req.ImGroupViewInp) + if err != nil { + return + } + + res = new(imgroup.ViewRes) + res.ImGroupViewModel = data + return +} + +// MemberList 查看IM群成员列表 +func (c *cImGroup) MemberList(ctx context.Context, req *imgroup.MemberListReq) (res *imgroup.MemberListRes, err error) { + list, totalCount, err := service.SysImGroup().MemberList(ctx, &req.ImGroupMemberListInp) + if err != nil { + return + } + + if list == nil { + list = []*sysin.ImGroupMemberListModel{} + } + + res = new(imgroup.MemberListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// Governance 获取IM群治理配置 +func (c *cImGroup) Governance(ctx context.Context, req *imgroup.GovernanceReq) (res *imgroup.GovernanceRes, err error) { + data, err := service.SysImGroup().Governance(ctx, &req.ImGroupTopicInp) + if err != nil { + return + } + res = new(imgroup.GovernanceRes) + res.ImGroupGovernanceModel = data + return +} + +// JoinRequestList 查看IM入群申请列表 +func (c *cImGroup) JoinRequestList(ctx context.Context, req *imgroup.JoinRequestListReq) (res *imgroup.JoinRequestListRes, err error) { + list, totalCount, err := service.SysImGroup().JoinRequestList(ctx, &req.ImGroupJoinRequestListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImGroupJoinRequestListModel{} + } + res = new(imgroup.JoinRequestListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// InvitationList 查看IM群邀请列表 +func (c *cImGroup) InvitationList(ctx context.Context, req *imgroup.InvitationListReq) (res *imgroup.InvitationListRes, err error) { + list, totalCount, err := service.SysImGroup().InvitationList(ctx, &req.ImGroupInvitationListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImGroupInvitationListModel{} + } + res = new(imgroup.InvitationListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// AnnouncementList 查看IM群公告列表 +func (c *cImGroup) AnnouncementList(ctx context.Context, req *imgroup.AnnouncementListReq) (res *imgroup.AnnouncementListRes, err error) { + list, totalCount, err := service.SysImGroup().AnnouncementList(ctx, &req.ImGroupAnnouncementListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImGroupAnnouncementListModel{} + } + res = new(imgroup.AnnouncementListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// MuteList 查看IM群禁言列表 +func (c *cImGroup) MuteList(ctx context.Context, req *imgroup.MuteListReq) (res *imgroup.MuteListRes, err error) { + list, totalCount, err := service.SysImGroup().MuteList(ctx, &req.ImGroupMuteListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImGroupMuteListModel{} + } + res = new(imgroup.MuteListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// AdminLogList 查看IM群管理日志列表 +func (c *cImGroup) AdminLogList(ctx context.Context, req *imgroup.AdminLogListReq) (res *imgroup.AdminLogListRes, err error) { + list, totalCount, err := service.SysImGroup().AdminLogList(ctx, &req.ImGroupAdminLogListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImGroupAdminLogListModel{} + } + res = new(imgroup.AdminLogListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} diff --git a/server/internal/controller/admin/sys/im_invite.go b/server/internal/controller/admin/sys/im_invite.go new file mode 100644 index 0000000..f9761dc --- /dev/null +++ b/server/internal/controller/admin/sys/im_invite.go @@ -0,0 +1,76 @@ +package sys + +import ( + "context" + "hotgo/api/admin/iminvite" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImInvite = cImInvite{} + +type cImInvite struct{} + +func (c *cImInvite) List(ctx context.Context, req *iminvite.ListReq) (res *iminvite.ListRes, err error) { + list, totalCount, err := service.SysImInvite().List(ctx, &req.ImInviteListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImInviteCodeModel{} + } + res = new(iminvite.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +func (c *cImInvite) Create(ctx context.Context, req *iminvite.CreateReq) (res *iminvite.CreateRes, err error) { + data, err := service.SysImInvite().Create(ctx, &req.ImInviteCreateInp) + if err != nil { + return + } + res = new(iminvite.CreateRes) + res.ImInviteCodeModel = data + return +} + +func (c *cImInvite) BatchCreate(ctx context.Context, req *iminvite.BatchCreateReq) (res *iminvite.BatchCreateRes, err error) { + data, err := service.SysImInvite().BatchCreate(ctx, &req.ImInviteBatchCreateInp) + if err != nil { + return + } + res = new(iminvite.BatchCreateRes) + res.ImInviteBatchModel = data + return +} + +func (c *cImInvite) BatchView(ctx context.Context, req *iminvite.BatchViewReq) (res *iminvite.BatchViewRes, err error) { + data, err := service.SysImInvite().BatchView(ctx, &req.ImInviteBatchViewInp) + if err != nil { + return + } + res = new(iminvite.BatchViewRes) + res.ImInviteBatchDetailModel = data + return +} + +func (c *cImInvite) Revoke(ctx context.Context, req *iminvite.RevokeReq) (res *iminvite.RevokeRes, err error) { + err = service.SysImInvite().Revoke(ctx, &req.ImInviteRevokeInp) + return +} + +func (c *cImInvite) BatchRevoke(ctx context.Context, req *iminvite.BatchRevokeReq) (res *iminvite.BatchRevokeRes, err error) { + data, err := service.SysImInvite().BatchRevoke(ctx, &req.ImInviteBatchRevokeInp) + if err != nil { + return + } + res = new(iminvite.BatchRevokeRes) + res.ImInviteBatchModel = data + return +} + +func (c *cImInvite) Export(ctx context.Context, req *iminvite.ExportReq) (res *iminvite.ExportRes, err error) { + err = service.SysImInvite().Export(ctx, &req.ImInviteListInp) + return +} diff --git a/server/internal/controller/admin/sys/im_realname.go b/server/internal/controller/admin/sys/im_realname.go new file mode 100644 index 0000000..f9619ff --- /dev/null +++ b/server/internal/controller/admin/sys/im_realname.go @@ -0,0 +1,53 @@ +// 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/imrealname" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ( + ImRealname = cImRealname{} +) + +type cImRealname struct{} + +// List 查看IM实名审核列表 +func (c *cImRealname) List(ctx context.Context, req *imrealname.ListReq) (res *imrealname.ListRes, err error) { + list, totalCount, err := service.SysImRealname().List(ctx, &req.ImRealnameListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImRealnameListModel{} + } + + res = new(imrealname.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// View 获取指定IM实名审核信息 +func (c *cImRealname) View(ctx context.Context, req *imrealname.ViewReq) (res *imrealname.ViewRes, err error) { + data, err := service.SysImRealname().View(ctx, &req.ImRealnameViewInp) + if err != nil { + return + } + + res = new(imrealname.ViewRes) + res.ImRealnameViewModel = data + return +} + +// Review 审核IM实名 +func (c *cImRealname) Review(ctx context.Context, req *imrealname.ReviewReq) (res *imrealname.ReviewRes, err error) { + err = service.SysImRealname().Review(ctx, &req.ImRealnameReviewInp) + return +} diff --git a/server/internal/controller/admin/sys/im_registration.go b/server/internal/controller/admin/sys/im_registration.go new file mode 100644 index 0000000..834e701 --- /dev/null +++ b/server/internal/controller/admin/sys/im_registration.go @@ -0,0 +1,26 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imregistration" + "hotgo/internal/service" +) + +var ImRegistration = cImRegistration{} + +type cImRegistration struct{} + +func (c *cImRegistration) Get(ctx context.Context, req *imregistration.GetReq) (res *imregistration.GetRes, err error) { + data, err := service.SysImRegistration().GetConfig(ctx) + if err != nil { + return + } + res = new(imregistration.GetRes) + res.ImRegistrationConfigModel = data + return +} + +func (c *cImRegistration) Update(ctx context.Context, req *imregistration.UpdateReq) (res *imregistration.UpdateRes, err error) { + err = service.SysImRegistration().UpdateConfig(ctx, &req.ImRegistrationConfigUpdateInp) + return +} diff --git a/server/internal/controller/admin/sys/im_tenant.go b/server/internal/controller/admin/sys/im_tenant.go new file mode 100644 index 0000000..3acd490 --- /dev/null +++ b/server/internal/controller/admin/sys/im_tenant.go @@ -0,0 +1,60 @@ +// 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/imtenant" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ( + ImTenant = cImTenant{} +) + +type cImTenant struct{} + +// List 查看IM租户列表 +func (c *cImTenant) List(ctx context.Context, req *imtenant.ListReq) (res *imtenant.ListRes, err error) { + list, totalCount, err := service.SysImTenant().List(ctx, &req.ImTenantListInp) + if err != nil { + return + } + + if list == nil { + list = []*sysin.ImTenantListModel{} + } + + res = new(imtenant.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// Edit 更新IM租户 +func (c *cImTenant) Edit(ctx context.Context, req *imtenant.EditReq) (res *imtenant.EditRes, err error) { + err = service.SysImTenant().Edit(ctx, &req.ImTenantEditInp) + return +} + +// View 获取指定IM租户信息 +func (c *cImTenant) View(ctx context.Context, req *imtenant.ViewReq) (res *imtenant.ViewRes, err error) { + data, err := service.SysImTenant().View(ctx, &req.ImTenantViewInp) + if err != nil { + return + } + + res = new(imtenant.ViewRes) + res.ImTenantViewModel = data + return +} + +// Status 更新IM租户状态 +func (c *cImTenant) Status(ctx context.Context, req *imtenant.StatusReq) (res *imtenant.StatusRes, err error) { + err = service.SysImTenant().Status(ctx, &req.ImTenantStatusInp) + return +} diff --git a/server/internal/controller/admin/sys/im_user.go b/server/internal/controller/admin/sys/im_user.go new file mode 100644 index 0000000..f350765 --- /dev/null +++ b/server/internal/controller/admin/sys/im_user.go @@ -0,0 +1,48 @@ +// 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/imuser" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ( + ImUser = cImUser{} +) + +type cImUser struct{} + +// List 查看IM用户列表 +func (c *cImUser) List(ctx context.Context, req *imuser.ListReq) (res *imuser.ListRes, err error) { + list, totalCount, err := service.SysImUser().List(ctx, &req.ImUserListInp) + if err != nil { + return + } + + if list == nil { + list = []*sysin.ImUserListModel{} + } + + res = new(imuser.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// View 获取指定IM用户信息 +func (c *cImUser) View(ctx context.Context, req *imuser.ViewReq) (res *imuser.ViewRes, err error) { + data, err := service.SysImUser().View(ctx, &req.ImUserViewInp) + if err != nil { + return + } + + res = new(imuser.ViewRes) + res.ImUserViewModel = data + return +} diff --git a/server/internal/controller/admin/sys/im_work_order.go b/server/internal/controller/admin/sys/im_work_order.go new file mode 100644 index 0000000..b5d1eb5 --- /dev/null +++ b/server/internal/controller/admin/sys/im_work_order.go @@ -0,0 +1,53 @@ +// 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/imworkorder" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ( + ImWorkOrder = cImWorkOrder{} +) + +type cImWorkOrder struct{} + +// List 查看IM举报反馈工单列表 +func (c *cImWorkOrder) List(ctx context.Context, req *imworkorder.ListReq) (res *imworkorder.ListRes, err error) { + list, totalCount, err := service.SysImWorkOrder().List(ctx, &req.ImWorkOrderListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImWorkOrderListModel{} + } + + res = new(imworkorder.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +// View 获取指定IM举报反馈工单信息 +func (c *cImWorkOrder) View(ctx context.Context, req *imworkorder.ViewReq) (res *imworkorder.ViewRes, err error) { + data, err := service.SysImWorkOrder().View(ctx, &req.ImWorkOrderViewInp) + if err != nil { + return + } + + res = new(imworkorder.ViewRes) + res.ImWorkOrderViewModel = data + return +} + +// Status 更新IM举报反馈工单状态 +func (c *cImWorkOrder) Status(ctx context.Context, req *imworkorder.StatusReq) (res *imworkorder.StatusRes, err error) { + err = service.SysImWorkOrder().Status(ctx, &req.ImWorkOrderStatusInp) + return +} diff --git a/server/internal/dao/im_tenant.go b/server/internal/dao/im_tenant.go new file mode 100644 index 0000000..a39dc63 --- /dev/null +++ b/server/internal/dao/im_tenant.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imTenantDao is the data access object for the table im_tenant. +// You can define custom methods on it to extend its functionality as needed. +type imTenantDao struct { + *internal.ImTenantDao +} + +var ( + // ImTenant is a globally accessible object for table im_tenant operations. + ImTenant = imTenantDao{internal.NewImTenantDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/im_tenant_config.go b/server/internal/dao/im_tenant_config.go new file mode 100644 index 0000000..bb54153 --- /dev/null +++ b/server/internal/dao/im_tenant_config.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imTenantConfigDao is the data access object for the table im_tenant_config. +// You can define custom methods on it to extend its functionality as needed. +type imTenantConfigDao struct { + *internal.ImTenantConfigDao +} + +var ( + // ImTenantConfig is a globally accessible object for table im_tenant_config operations. + ImTenantConfig = imTenantConfigDao{internal.NewImTenantConfigDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/internal/admin_member.go b/server/internal/dao/internal/admin_member.go index 93ebaa7..0d62e56 100755 --- a/server/internal/dao/internal/admin_member.go +++ b/server/internal/dao/internal/admin_member.go @@ -22,6 +22,7 @@ type AdminMemberDao struct { // AdminMemberColumns defines and stores column names for the table hg_admin_member. type AdminMemberColumns struct { Id string // 管理员ID + TenantId string // IM租户ID DeptId string // 部门ID RoleId string // 角色ID RealName string // 真实姓名 @@ -54,6 +55,7 @@ type AdminMemberColumns struct { // adminMemberColumns holds the columns for the table hg_admin_member. var adminMemberColumns = AdminMemberColumns{ Id: "id", + TenantId: "tenant_id", DeptId: "dept_id", RoleId: "role_id", RealName: "real_name", diff --git a/server/internal/dao/internal/im_tenant.go b/server/internal/dao/internal/im_tenant.go new file mode 100644 index 0000000..f86ea9a --- /dev/null +++ b/server/internal/dao/internal/im_tenant.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImTenantDao is the data access object for the table im_tenant. +type ImTenantDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImTenantColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImTenantColumns defines and stores column names for the table im_tenant. +type ImTenantColumns struct { + Id string // + Code string // + Name string // + TenantDesc string // + DbName string // + DbDsn string // + State string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // +} + +// imTenantColumns holds the columns for the table im_tenant. +var imTenantColumns = ImTenantColumns{ + Id: "id", + Code: "code", + Name: "name", + TenantDesc: "tenant_desc", + DbName: "db_name", + DbDsn: "db_dsn", + State: "state", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", +} + +// NewImTenantDao creates and returns a new DAO object for table data access. +func NewImTenantDao(handlers ...gdb.ModelHandler) *ImTenantDao { + return &ImTenantDao{ + group: "default", + table: "im_tenant", + columns: imTenantColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImTenantDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImTenantDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImTenantDao) Columns() ImTenantColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImTenantDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImTenantDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImTenantDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/im_tenant_config.go b/server/internal/dao/internal/im_tenant_config.go new file mode 100644 index 0000000..3e60943 --- /dev/null +++ b/server/internal/dao/internal/im_tenant_config.go @@ -0,0 +1,95 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImTenantConfigDao is the data access object for the table im_tenant_config. +type ImTenantConfigDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImTenantConfigColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImTenantConfigColumns defines and stores column names for the table im_tenant_config. +type ImTenantConfigColumns struct { + Id string // + TenantId string // + MaxUsers string // 0 means unlimited + MaxGroups string // 0 means unlimited + MaxGroupMembers string // 0 means unlimited + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // +} + +// imTenantConfigColumns holds the columns for the table im_tenant_config. +var imTenantConfigColumns = ImTenantConfigColumns{ + Id: "id", + TenantId: "tenant_id", + MaxUsers: "max_users", + MaxGroups: "max_groups", + MaxGroupMembers: "max_group_members", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", +} + +// NewImTenantConfigDao creates and returns a new DAO object for table data access. +func NewImTenantConfigDao(handlers ...gdb.ModelHandler) *ImTenantConfigDao { + return &ImTenantConfigDao{ + group: "default", + table: "im_tenant_config", + columns: imTenantConfigColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImTenantConfigDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImTenantConfigDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImTenantConfigDao) Columns() ImTenantConfigColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImTenantConfigDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImTenantConfigDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImTenantConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/library/contexts/context.go b/server/internal/library/contexts/context.go index b9cd6bd..9962e17 100644 --- a/server/internal/library/contexts/context.go +++ b/server/internal/library/contexts/context.go @@ -78,6 +78,15 @@ func GetUserId(ctx context.Context) int64 { return user.Id } +// GetTenantId 获取当前身份关联的统一租户ID(im_tenant.id)。 +func GetTenantId(ctx context.Context) int64 { + user := GetUser(ctx) + if user == nil { + return 0 + } + return user.TenantId +} + // GetRoleId 获取用户角色ID func GetRoleId(ctx context.Context) int64 { user := GetUser(ctx) diff --git a/server/internal/library/imadmin/client.go b/server/internal/library/imadmin/client.go new file mode 100644 index 0000000..7b99cb3 --- /dev/null +++ b/server/internal/library/imadmin/client.go @@ -0,0 +1,268 @@ +package imadmin + +import ( + "bytes" + "context" + "crypto/hmac" + cryptorand "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gctx" +) + +const maxResponseBody = 10 << 20 + +type AdminScope struct { + TenantId int64 + OperatorId int64 + RoleKey string + RequestId string +} + +type Page[T any] struct { + Items []T `json:"items"` + Page int `json:"page"` + PerPage int `json:"per_page"` + Total int `json:"total"` +} + +type Client struct { + conf Config + http *http.Client +} + +func NewClient(conf Config) *Client { + return &Client{ + conf: conf, + http: &http.Client{Timeout: conf.Timeout}, + } +} + +func DefaultClient(ctx context.Context) *Client { + conf := LoadConfig(ctx) + return NewClient(conf) +} + +func (c *Client) CreateCode(ctx context.Context, scope AdminScope, in any, out any) error { + return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes", scope, in, out) +} + +func (c *Client) CreateBatch(ctx context.Context, scope AdminScope, in any, out any) error { + return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-code-batches", scope, in, out) +} + +func (c *Client) ListCodes(ctx context.Context, scope AdminScope, in any, out any) error { + if hasInviteCode(in) { + return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes/search", scope, in, out) + } + path := "/admin/v1/im/registration-invite-codes" + if query := toQuery(in); query != "" { + path += "?" + query + } + return c.doJSON(ctx, http.MethodGet, path, scope, nil, out) +} + +func (c *Client) GetBatch(ctx context.Context, scope AdminScope, batchId int64, out any) error { + return c.doJSON(ctx, http.MethodGet, fmt.Sprintf("/admin/v1/im/registration-invite-code-batches/%d", batchId), scope, nil, out) +} + +func (c *Client) RevokeCode(ctx context.Context, scope AdminScope, codeId int64) error { + return c.doJSON(ctx, http.MethodPost, fmt.Sprintf("/admin/v1/im/registration-invite-codes/%d/revoke", codeId), scope, nil, nil) +} + +func (c *Client) RevokeBatch(ctx context.Context, scope AdminScope, batchId int64, out any) error { + return c.doJSON(ctx, http.MethodPost, fmt.Sprintf("/admin/v1/im/registration-invite-code-batches/%d/revoke", batchId), scope, nil, out) +} + +func (c *Client) ExportCodes(ctx context.Context, scope AdminScope, in any) (*http.Response, error) { + req, err := c.newRequest(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes/export", scope, in) + if err != nil { + return nil, err + } + resp, err := c.http.Do(req) + if err != nil { + return nil, gerror.New("IM服务暂不可用") + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + defer resp.Body.Close() + return nil, c.parseError(resp) + } + return resp, nil +} + +func (c *Client) doJSON(ctx context.Context, method string, path string, scope AdminScope, in any, out any) error { + req, err := c.newRequest(ctx, method, path, scope, in) + if err != nil { + return err + } + resp, err := c.http.Do(req) + if err != nil { + return gerror.New("IM服务暂不可用") + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return c.parseError(resp) + } + if out == nil { + return nil + } + body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody)) + if err != nil { + return gerror.Wrap(err, "读取IM响应失败") + } + var envelope struct { + Code int `json:"code"` + Message string `json:"message"` + Reason string `json:"reason"` + Data json.RawMessage `json:"data"` + } + if err = json.Unmarshal(body, &envelope); err == nil && len(envelope.Data) > 0 { + if envelope.Code != 0 { + return stableError(envelope.Message, envelope.Reason) + } + return json.Unmarshal(envelope.Data, out) + } + return json.Unmarshal(body, out) +} + +func (c *Client) newRequest(ctx context.Context, method string, path string, scope AdminScope, in any) (*http.Request, error) { + if c.conf.BaseUrl == "" { + return nil, gerror.New("IM管理API未配置") + } + if c.conf.ServiceSecret == "" { + return nil, gerror.New("IM管理API密钥未配置") + } + var body io.Reader + bodyBytes := []byte{} + if in != nil { + b, err := json.Marshal(in) + if err != nil { + return nil, gerror.Wrap(err, "序列化IM请求失败") + } + bodyBytes = b + body = bytes.NewReader(bodyBytes) + } + baseUrl := strings.TrimRight(c.conf.BaseUrl, "/") + req, err := http.NewRequestWithContext(ctx, method, baseUrl+path, body) + if err != nil { + return nil, gerror.Wrap(err, "创建IM请求失败") + } + if scope.RequestId == "" { + scope.RequestId = gctx.CtxId(ctx) + } + timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10) + nonce, err := randomNonce() + if err != nil { + return nil, err + } + bodyHash := sha256.Sum256(bodyBytes) + canonical := strings.Join([]string{ + method, + requestTarget(req), + c.conf.ServiceId, + fmt.Sprintf("%d", scope.TenantId), + fmt.Sprintf("%d", scope.OperatorId), + timestamp, + nonce, + scope.RequestId, + hex.EncodeToString(bodyHash[:]), + }, "\n") + mac := hmac.New(sha256.New, []byte(c.conf.ServiceSecret)) + _, _ = mac.Write([]byte(canonical)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "IM-HMAC "+hex.EncodeToString(mac.Sum(nil))) + req.Header.Set("X-IM-Service-ID", c.conf.ServiceId) + req.Header.Set("X-IM-Tenant-ID", fmt.Sprintf("%d", scope.TenantId)) + req.Header.Set("X-IM-Operator-ID", fmt.Sprintf("%d", scope.OperatorId)) + req.Header.Set("X-IM-Timestamp", timestamp) + req.Header.Set("X-IM-Nonce", nonce) + req.Header.Set("X-HotGo-Role-Key", scope.RoleKey) + req.Header.Set("X-Request-ID", scope.RequestId) + return req, nil +} + +func (c *Client) parseError(resp *http.Response) error { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10)) + var envelope struct { + Message string `json:"message"` + Reason string `json:"reason"` + } + _ = json.Unmarshal(body, &envelope) + return stableError(envelope.Message, envelope.Reason) +} + +func stableError(message string, reason string) error { + if message != "" { + return gerror.New(message) + } + if reason != "" { + return gerror.New(reason) + } + return gerror.New("IM服务暂不可用") +} + +func hasInviteCode(in any) bool { + b, err := json.Marshal(in) + if err != nil { + return false + } + var m map[string]any + if err = json.Unmarshal(b, &m); err != nil { + return false + } + v, ok := m["inviteCode"] + if ok && strings.TrimSpace(fmt.Sprint(v)) != "" { + return true + } + v, ok = m["invite_code"] + return ok && strings.TrimSpace(fmt.Sprint(v)) != "" +} + +func requestTarget(req *http.Request) string { + target := req.URL.EscapedPath() + if req.URL.RawQuery != "" { + target += "?" + req.URL.RawQuery + } + return target +} + +func randomNonce() (string, error) { + buf := make([]byte, 16) + if _, err := cryptorand.Read(buf); err != nil { + return "", gerror.Wrap(err, "生成IM请求随机数失败") + } + return hex.EncodeToString(buf), nil +} + +func toQuery(in any) string { + b, err := json.Marshal(in) + if err != nil { + return "" + } + var m map[string]any + if err = json.Unmarshal(b, &m); err != nil { + return "" + } + values := url.Values{} + for key, value := range m { + if value == nil { + continue + } + text := strings.TrimSpace(fmt.Sprint(value)) + if text == "" || text == "0" || text == "" { + continue + } + values.Set(key, text) + } + return values.Encode() +} diff --git a/server/internal/library/imadmin/client_test.go b/server/internal/library/imadmin/client_test.go new file mode 100644 index 0000000..ef54b6e --- /dev/null +++ b/server/internal/library/imadmin/client_test.go @@ -0,0 +1,109 @@ +package imadmin + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" +) + +type testInviteListRequest struct { + Page int `json:"page,omitempty"` + PerPage int `json:"per_page,omitempty"` + InviteCode string `json:"invite_code,omitempty"` + BatchID int64 `json:"batch_id,omitempty"` +} + +func TestListCodesUsesSignedGetWithSnakeQuery(t *testing.T) { + server := signedTestServer(t, http.MethodGet, "/admin/v1/im/registration-invite-codes", func(t *testing.T, r *http.Request, body []byte) { + if len(body) != 0 { + t.Fatalf("GET list should not send body, got %q", string(body)) + } + query := r.URL.Query() + if query.Get("page") != "2" || query.Get("per_page") != "20" || query.Get("batch_id") != "123" { + t.Fatalf("unexpected query: %s", r.URL.RawQuery) + } + }) + defer server.Close() + + client := NewClient(Config{BaseUrl: server.URL, ServiceId: "hotgo-admin", ServiceSecret: "secret", Timeout: time.Second}) + var out Page[struct{}] + err := client.ListCodes(context.Background(), AdminScope{TenantId: 9, OperatorId: 7, RequestId: "req-1"}, testInviteListRequest{Page: 2, PerPage: 20, BatchID: 123}, &out) + if err != nil { + t.Fatalf("ListCodes returned error: %v", err) + } +} + +func TestListCodesWithInviteCodeUsesSignedSearchPost(t *testing.T) { + server := signedTestServer(t, http.MethodPost, "/admin/v1/im/registration-invite-codes/search", func(t *testing.T, r *http.Request, body []byte) { + if !strings.Contains(string(body), `"invite_code":"Ab123"`) { + t.Fatalf("POST search should send snake_case invite code, got %s", string(body)) + } + }) + defer server.Close() + + client := NewClient(Config{BaseUrl: server.URL, ServiceId: "hotgo-admin", ServiceSecret: "secret", Timeout: time.Second}) + var out Page[struct{}] + err := client.ListCodes(context.Background(), AdminScope{TenantId: 9, OperatorId: 7, RequestId: "req-2"}, testInviteListRequest{Page: 1, PerPage: 20, InviteCode: "Ab123"}, &out) + if err != nil { + t.Fatalf("ListCodes returned error: %v", err) + } +} + +func signedTestServer(t *testing.T, method string, path string, inspect func(t *testing.T, r *http.Request, body []byte)) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != method { + t.Fatalf("method = %s, want %s", r.Method, method) + } + if r.URL.Path != path { + t.Fatalf("path = %s, want %s", r.URL.Path, path) + } + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + inspect(t, r, body) + assertSignedRequest(t, r, body, "secret") + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{"items":[],"page":1,"per_page":20,"total":0}}`)) + })) +} + +func assertSignedRequest(t *testing.T, r *http.Request, body []byte, secret string) { + t.Helper() + for _, name := range []string{"Authorization", "X-IM-Service-ID", "X-IM-Tenant-ID", "X-IM-Operator-ID", "X-IM-Timestamp", "X-IM-Nonce", "X-Request-ID"} { + if r.Header.Get(name) == "" { + t.Fatalf("missing header %s", name) + } + } + if _, err := strconv.ParseInt(r.Header.Get("X-IM-Timestamp"), 10, 64); err != nil { + t.Fatalf("timestamp is not unix seconds: %v", err) + } + bodyHash := sha256.Sum256(body) + canonical := strings.Join([]string{ + r.Method, + r.URL.RequestURI(), + r.Header.Get("X-IM-Service-ID"), + r.Header.Get("X-IM-Tenant-ID"), + r.Header.Get("X-IM-Operator-ID"), + r.Header.Get("X-IM-Timestamp"), + r.Header.Get("X-IM-Nonce"), + r.Header.Get("X-Request-ID"), + hex.EncodeToString(bodyHash[:]), + }, "\n") + mac := hmac.New(sha256.New, []byte(secret)) + _, _ = mac.Write([]byte(canonical)) + expected := fmt.Sprintf("IM-HMAC %s", hex.EncodeToString(mac.Sum(nil))) + if r.Header.Get("Authorization") != expected { + t.Fatalf("bad signature\nwant: %s\n got: %s", expected, r.Header.Get("Authorization")) + } +} diff --git a/server/internal/library/imadmin/config.go b/server/internal/library/imadmin/config.go new file mode 100644 index 0000000..4a78d4e --- /dev/null +++ b/server/internal/library/imadmin/config.go @@ -0,0 +1,37 @@ +package imadmin + +import ( + "context" + "os" + "strings" + "time" + + "github.com/gogf/gf/v2/frame/g" +) + +type Config struct { + BaseUrl string `json:"baseUrl"` + ServiceId string `json:"serviceId"` + ServiceSecret string `json:"serviceSecret"` + Timeout time.Duration `json:"timeout"` +} + +func LoadConfig(ctx context.Context) Config { + timeout := g.Cfg().MustGet(ctx, "imAdminApi.timeout", "5s").Duration() + if timeout <= 0 { + timeout = 5 * time.Second + } + serviceSecret := g.Cfg().MustGet(ctx, "imAdminApi.serviceSecret").String() + if strings.HasPrefix(serviceSecret, "${") && strings.HasSuffix(serviceSecret, "}") { + serviceSecret = os.Getenv(strings.TrimSuffix(strings.TrimPrefix(serviceSecret, "${"), "}")) + } + if serviceSecret == "" { + serviceSecret = os.Getenv("IM_ADMIN_API_SECRET") + } + return Config{ + BaseUrl: g.Cfg().MustGet(ctx, "imAdminApi.baseUrl").String(), + ServiceId: g.Cfg().MustGet(ctx, "imAdminApi.serviceId", "hotgo-admin").String(), + ServiceSecret: serviceSecret, + Timeout: timeout, + } +} diff --git a/server/internal/library/imtenant/db.go b/server/internal/library/imtenant/db.go new file mode 100644 index 0000000..4541f17 --- /dev/null +++ b/server/internal/library/imtenant/db.go @@ -0,0 +1,128 @@ +// Package imtenant provides helpers for accessing tenant business databases. +package imtenant + +import ( + "context" + "fmt" + "hash/fnv" + "hotgo/internal/dao" + "hotgo/internal/model/entity" + "strings" + "sync" + + _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" +) + +var tenantDbGroups sync.Map + +const registryDbGroup = "imRegistry" + +// RegistryDB returns the tenant registry DB. It falls back to the default DB for +// single-database deployments where database.imRegistry is not configured. +func RegistryDB(ctx context.Context) gdb.DB { + if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 { + return g.DB(registryDbGroup) + } + return g.DB() +} + +// DB resolves a tenant registry record and returns the corresponding business DB. +func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant, err error) { + if tenantId <= 0 { + return nil, nil, gerror.New("租户ID不能为空") + } + + if err = RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil { + return nil, nil, gerror.Wrap(err, "获取租户信息失败") + } + if tenant == nil || tenant.Id <= 0 { + return nil, nil, gerror.New("租户不存在") + } + + link, err := tenantLink(ctx, tenant) + if err != nil { + return nil, nil, err + } + + group := fmt.Sprintf("im_tenant_%d_%x", tenant.Id, hash(link)) + if cached, ok := tenantDbGroups.Load(group); ok && cached == link { + db, err = gdb.Instance(group) + return db, tenant, err + } + + if err = gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil { + return nil, nil, gerror.Wrap(err, "初始化租户业务库连接失败") + } + tenantDbGroups.Store(group, link) + + db, err = gdb.Instance(group) + if err != nil { + return nil, nil, gerror.Wrap(err, "获取租户业务库连接失败") + } + return db, tenant, nil +} + +func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) { + if tenant.DbDsn != "" { + return tenant.DbDsn, nil + } + if tenant.DbName == "" { + return "", gerror.New("租户业务库未配置") + } + + defaultLink := defaultDbLink(ctx) + if defaultLink == "" { + return "", gerror.New("默认数据库连接未配置,无法推导租户业务库连接") + } + + link, err := replaceMySQLDatabase(defaultLink, tenant.DbName) + if err != nil { + return "", err + } + return link, nil +} + +func defaultDbLink(ctx context.Context) string { + if configs, err := gdb.GetConfigGroup("default"); err == nil && len(configs) > 0 { + if configs[0].Link != "" { + return configs[0].Link + } + } + + cfg := g.Cfg() + if !cfg.Available(ctx) { + return "" + } + defaultLink := cfg.MustGet(ctx, "database.default.0.link").String() + if defaultLink != "" { + return defaultLink + } + return cfg.MustGet(ctx, "database.default.link").String() +} + +func replaceMySQLDatabase(link string, dbName string) (string, error) { + if !strings.HasPrefix(link, "mysql:") { + return "", gerror.New("仅支持从 MySQL 默认连接推导租户业务库连接") + } + + dbStart := strings.LastIndex(link, ")/") + if dbStart < 0 { + return "", gerror.New("默认数据库连接格式不正确") + } + dbStart += 2 + queryStart := strings.Index(link[dbStart:], "?") + if queryStart < 0 { + return link[:dbStart] + dbName, nil + } + queryStart += dbStart + return link[:dbStart] + dbName + link[queryStart:], nil +} + +func hash(s string) uint64 { + h := fnv.New64a() + _, _ = h.Write([]byte(s)) + return h.Sum64() +} diff --git a/server/internal/logic/admin/member.go b/server/internal/logic/admin/member.go index 1d384fa..c572f21 100644 --- a/server/internal/logic/admin/member.go +++ b/server/internal/logic/admin/member.go @@ -25,6 +25,7 @@ import ( "hotgo/internal/library/hgorm" "hotgo/internal/library/hgorm/handler" "hotgo/internal/library/hgorm/hook" + "hotgo/internal/library/imtenant" "hotgo/internal/model/entity" "hotgo/internal/model/input/adminin" "hotgo/internal/model/input/sysin" @@ -467,6 +468,30 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err return } + if service.AdminMember().VerifySuperId(ctx, opMemberId) { + if in.TenantId > 0 { + count, queryErr := imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx). + Where(dao.ImTenant.Columns().Id, in.TenantId). + Where(dao.ImTenant.Columns().State, consts.ImTenantStateActive). + Count() + if queryErr != nil { + return gerror.Wrap(queryErr, "校验IM租户失败") + } + if count == 0 { + return gerror.New("IM租户不存在或未启用") + } + } + } else if deptType := contexts.GetDeptType(ctx); deptType == consts.DeptTypeTenant || + deptType == consts.DeptTypeMerchant || deptType == consts.DeptTypeUser { + in.TenantId = contexts.GetTenantId(ctx) + if in.TenantId <= 0 { + return gerror.New("当前管理员未关联租户") + } + } else { + // 非租户域管理员不能通过浏览器为账号指定租户。 + in.TenantId = 0 + } + cols := dao.AdminMember.Columns() err = s.VerifyUnique(ctx, &adminin.VerifyUniqueInp{ Id: in.Id, diff --git a/server/internal/logic/admin/site.go b/server/internal/logic/admin/site.go index 0409e1c..3edadd3 100644 --- a/server/internal/logic/admin/site.go +++ b/server/internal/logic/admin/site.go @@ -228,6 +228,7 @@ func (s *sAdminSite) handleLogin(ctx context.Context, mb *entity.AdminMember) (r user := &model.Identity{ Id: mb.Id, + TenantId: mb.TenantId, Pid: mb.Pid, DeptId: dept.Id, DeptType: dept.Type, @@ -319,6 +320,7 @@ func (s *sAdminSite) BindUserContext(ctx context.Context, claims *model.Identity user := &model.Identity{ Id: mb.Id, + TenantId: mb.TenantId, Pid: mb.Pid, DeptId: dept.Id, DeptType: dept.Type, diff --git a/server/internal/logic/sys/im_admin_scope.go b/server/internal/logic/sys/im_admin_scope.go new file mode 100644 index 0000000..df36557 --- /dev/null +++ b/server/internal/logic/sys/im_admin_scope.go @@ -0,0 +1,95 @@ +package sys + +import ( + "context" + "hotgo/internal/consts" + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/library/imadmin" + "hotgo/internal/library/imtenant" + "hotgo/internal/model/entity" + "hotgo/internal/service" + "hotgo/utility/simple" + + "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/net/ghttp" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gtime" +) + +const ( + imAdminOperationAuditTable = "im_admin_operation_audit" +) + +func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope imadmin.AdminScope, err error) { + operatorId := contexts.GetUserId(ctx) + scope.OperatorId = operatorId + scope.RoleKey = contexts.GetRoleKey(ctx) + scope.RequestId = gctx.CtxId(ctx) + if service.AdminMember().VerifySuperId(ctx, operatorId) { + if selectedTenantId <= 0 { + return scope, gerror.New("请选择IM租户") + } + if err = ensureImTenantActive(ctx, selectedTenantId); err != nil { + return scope, err + } + scope.TenantId = selectedTenantId + return scope, nil + } + + if contexts.GetDeptType(ctx) != consts.DeptTypeTenant { + return scope, gerror.New("当前账号不是租户管理员,不能管理邀请码") + } + tenantId := contexts.GetTenantId(ctx) + if tenantId <= 0 { + return scope, gerror.New("当前管理员未关联租户") + } + if err = ensureImTenantActive(ctx, tenantId); err != nil { + return scope, err + } + scope.TenantId = tenantId + return scope, nil +} + +func ensureImTenantActive(ctx context.Context, tenantId int64) (err error) { + var tenant *entity.ImTenant + if err = imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil { + return gerror.Wrap(err, "获取IM租户失败") + } + if tenant == nil || tenant.Id <= 0 { + return gerror.New("IM租户不存在") + } + if tenant.State != consts.ImTenantStateActive { + return gerror.New("IM租户未启用") + } + return nil +} + +func writeImAdminAudit(ctx context.Context, tenantId int64, action string, targetType string, targetId int64, result string, detail g.Map) error { + operatorId := contexts.GetUserId(ctx) + request := ghttp.RequestFromCtx(ctx) + ip := "" + if request != nil { + ip = request.GetClientIp() + } + detailJson := gjson.New(detail) + _, err := g.DB().Exec(ctx, + "INSERT INTO "+imAdminOperationAuditTable+" (request_id, tenant_id, operator_id, action, target_type, target_id, result, ip, detail_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?)", + gctx.CtxId(ctx), + tenantId, + operatorId, + action, + targetType, + targetId, + result, + ip, + detailJson.String(), + gtime.Now(), + ) + if err != nil && !simple.IsDemo(ctx) { + g.Log().Warningf(ctx, "写入IM后台审计失败 action:%s err:%+v", action, err) + } + return err +} diff --git a/server/internal/logic/sys/im_audit_log.go b/server/internal/logic/sys/im_audit_log.go new file mode 100644 index 0000000..943ef75 --- /dev/null +++ b/server/internal/logic/sys/im_audit_log.go @@ -0,0 +1,125 @@ +// 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/library/imtenant" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" +) + +type sSysImAuditLog struct{} + +func NewSysImAuditLog() *sSysImAuditLog { + return &sSysImAuditLog{} +} + +func init() { + service.RegisterSysImAuditLog(NewSysImAuditLog()) +} + +// List 获取IM审计日志列表 +func (s *sSysImAuditLog) List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + mod := s.model(ctx, db) + mod = s.applyWhere(mod, in) + mod = mod.Page(in.Page, in.PerPage).OrderDesc("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 *sSysImAuditLog) model(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model("im_audit_logs").Safe().Ctx(ctx). + Fields( + "id", + "actor_uid", + "actor_role", + "action", + "target_type", + "target_id", + "reason", + "request_id", + "ip", + "user_agent", + "CAST(before_snapshot AS CHAR) before_snapshot", + "CAST(after_snapshot AS CHAR) after_snapshot", + "CAST(metadata AS CHAR) metadata", + "created_at", + ) +} + +func (s *sSysImAuditLog) applyWhere(mod *gdb.Model, in *sysin.ImAuditLogListInp) *gdb.Model { + if in.ActorUid > 0 { + mod = mod.Where("actor_uid", in.ActorUid) + } + if in.ActorRole != "" { + mod = mod.WhereLike("actor_role", "%"+in.ActorRole+"%") + } + if in.Action != "" { + mod = mod.WhereLike("action", "%"+in.Action+"%") + } + if in.TargetType != "" { + mod = mod.Where("target_type", in.TargetType) + } + if in.TargetId != "" { + mod = mod.WhereLike("target_id", "%"+in.TargetId+"%") + } + if in.RequestId != "" { + mod = mod.WhereLike("request_id", "%"+in.RequestId+"%") + } + if in.Ip != "" { + mod = mod.WhereLike("ip", "%"+in.Ip+"%") + } + if in.Keyword != "" { + like := "%" + in.Keyword + "%" + mod = mod.Where("(action LIKE ? OR target_type LIKE ? OR target_id LIKE ? OR reason LIKE ? OR request_id LIKE ?)", like, like, like, like, like) + } + if len(in.CreatedAt) == 2 { + mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1]) + } + return mod +} + +// View 获取IM审计日志指定信息 +func (s *sSysImAuditLog) View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return nil, err + } + + err = s.model(ctx, db).Where("id", in.Id).Scan(&res) + if err != nil { + return nil, gerror.Wrap(err, "获取IM审计日志信息失败,请稍后重试!") + } + if res == nil { + return nil, nil + } + res.TenantId = tenant.Id + res.TenantName = tenant.Name + return +} diff --git a/server/internal/logic/sys/im_group.go b/server/internal/logic/sys/im_group.go new file mode 100644 index 0000000..26cffc6 --- /dev/null +++ b/server/internal/logic/sys/im_group.go @@ -0,0 +1,456 @@ +// 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/library/imtenant" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" +) + +type sSysImGroup struct{} + +func NewSysImGroup() *sSysImGroup { + return &sSysImGroup{} +} + +func init() { + service.RegisterSysImGroup(NewSysImGroup()) +} + +// List 获取IM群组列表 +func (s *sSysImGroup) List(ctx context.Context, in *sysin.ImGroupListInp) (list []*sysin.ImGroupListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + mod := s.groupModel(ctx, db) + mod = s.applyGroupWhere(mod, in) + mod = mod.Page(in.Page, in.PerPage).OrderDesc("t.touchedat").OrderDesc("t.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 *sSysImGroup) groupModel(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model("im_topics t").Safe().Ctx(ctx). + LeftJoin("im_group_settings gs", "gs.topic = t.name"). + Fields( + "t.name topic", + "t.state", + "COALESCE(gs.owner_user_id, t.owner) owner_id", + "t.seqid seq_id", + "t.delid del_id", + "t.subcnt sub_cnt", + "t.touchedat touched_at", + "t.createdat created_at", + "t.updatedat updated_at", + "t.public public_json", + "t.trusted trusted_json", + "t.tags tags_json", + "t.aux aux_json", + "COALESCE(gs.name, '') group_name", + "COALESCE(gs.avatar_file_id, '') avatar_file_id", + "COALESCE(gs.description, '') description", + "COALESCE(gs.group_status, '') group_status", + "COALESCE(gs.join_approval, 0) join_approval", + "COALESCE(gs.mute_all, 0) mute_all", + "COALESCE(gs.member_upload_files, 0) member_upload_files", + "COALESCE(gs.member_private_chat, 0) member_private_chat", + "COALESCE(gs.member_invite, 0) member_invite", + "COALESCE(gs.max_member_count, 0) max_member_count", + "COALESCE(gs.max_admin_count, 0) max_admin_count", + "COALESCE(gs.member_count, 0) business_member_count", + "COALESCE(gs.version, 0) settings_version", + "gs.updated_at settings_updated_at", + ). + WhereLike("t.name", "grp%") +} + +func (s *sSysImGroup) applyGroupWhere(mod *gdb.Model, in *sysin.ImGroupListInp) *gdb.Model { + if in.Topic != "" { + mod = mod.WhereLike("t.name", "%"+in.Topic+"%") + } + if in.OwnerId > 0 { + mod = mod.Where("COALESCE(gs.owner_user_id, t.owner)", in.OwnerId) + } + if in.State != nil { + mod = mod.Where("t.state", *in.State) + } + if in.GroupStatus != "" { + mod = mod.Where("gs.group_status", in.GroupStatus) + } + if len(in.CreatedAt) == 2 { + mod = mod.WhereBetween("t.createdat", in.CreatedAt[0], in.CreatedAt[1]) + } + if len(in.TouchedAt) == 2 { + mod = mod.WhereBetween("t.touchedat", in.TouchedAt[0], in.TouchedAt[1]) + } + return mod +} + +// View 获取IM群组指定信息 +func (s *sSysImGroup) View(ctx context.Context, in *sysin.ImGroupViewInp) (res *sysin.ImGroupViewModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + err = db.Model("im_topics t").Safe().Ctx(ctx). + LeftJoin("im_group_settings gs", "gs.topic = t.name"). + Fields( + "t.name topic", + "t.state", + "COALESCE(gs.owner_user_id, t.owner) owner_id", + "t.seqid seq_id", + "t.delid del_id", + "t.subcnt sub_cnt", + "t.touchedat touched_at", + "t.createdat created_at", + "t.updatedat updated_at", + "t.public public_json", + "t.trusted trusted_json", + "t.tags tags_json", + "t.aux aux_json", + "t.access access_json", + "t.usebt use_bt", + "COALESCE(gs.name, '') group_name", + "COALESCE(gs.avatar_file_id, '') avatar_file_id", + "COALESCE(gs.description, '') description", + "COALESCE(gs.group_status, '') group_status", + "COALESCE(gs.join_approval, 0) join_approval", + "COALESCE(gs.mute_all, 0) mute_all", + "COALESCE(gs.member_upload_files, 0) member_upload_files", + "COALESCE(gs.member_private_chat, 0) member_private_chat", + "COALESCE(gs.member_invite, 0) member_invite", + "COALESCE(gs.max_member_count, 0) max_member_count", + "COALESCE(gs.max_admin_count, 0) max_admin_count", + "COALESCE(gs.member_count, 0) business_member_count", + "COALESCE(gs.version, 0) settings_version", + "gs.updated_at settings_updated_at", + ). + Where("t.name", in.Topic). + WhereLike("t.name", "grp%"). + Scan(&res) + if err != nil { + return nil, gerror.Wrap(err, "获取IM群组信息失败,请稍后重试!") + } + if res == nil { + return nil, nil + } + res.TenantId = tenant.Id + res.TenantName = tenant.Name + return +} + +// MemberList 获取IM群成员列表 +func (s *sSysImGroup) MemberList(ctx context.Context, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + if s.hasGroupMemberTruth(ctx, db, in.Topic) { + return s.memberListFromBusiness(ctx, db, in) + } + return s.memberListFromProjection(ctx, db, in) +} + +func (s *sSysImGroup) hasGroupMemberTruth(ctx context.Context, db gdb.DB, topic string) bool { + count, err := db.Model("im_group_members").Safe().Ctx(ctx).Where("topic", topic).Count() + return err == nil && count > 0 +} + +func (s *sSysImGroup) memberListFromBusiness(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) { + mod := db.Model("im_group_members gm").Safe().Ctx(ctx). + LeftJoin("im_users u", "u.id = gm.user_id"). + LeftJoin("im_user_profiles up", "up.user_id = gm.user_id"). + LeftJoin("im_subscriptions s", "s.topic = gm.topic AND s.userid = gm.user_id"). + Fields( + "gm.user_id", + "up.nickname", + "u.state", + "s.modewant mode_want", + "s.modegiven mode_given", + "s.recvseqid recv_seq_id", + "s.readseqid read_seq_id", + "s.delid del_id", + "s.deletedat deleted_at", + "s.createdat created_at", + "s.updatedat updated_at", + "s.private private_json", + "gm.role", + "gm.member_status", + "gm.nickname group_nickname", + "gm.pinned", + "gm.mute_notifications", + "gm.join_source", + "gm.joined_by", + "gm.joined_at", + "gm.left_at", + "gm.removed_by", + "gm.version", + ). + Where("gm.topic", in.Topic) + if in.UserId > 0 { + mod = mod.Where("gm.user_id", in.UserId) + } + if in.Role != "" { + mod = mod.Where("gm.role", in.Role) + } + if in.MemberStatus != "" { + mod = mod.Where("gm.member_status", in.MemberStatus) + } + mod = mod.Page(in.Page, in.PerPage).OrderAsc("gm.member_status").OrderAsc("gm.role").OrderAsc("gm.id") + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM群成员列表失败,请稍后重试!") + return + } + return +} + +func (s *sSysImGroup) memberListFromProjection(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) { + mod := db.Model("im_subscriptions s").Safe().Ctx(ctx). + LeftJoin("im_users u", "u.id = s.userid"). + LeftJoin("im_user_profiles up", "up.user_id = s.userid"). + Fields( + "s.userid user_id", + "up.nickname", + "u.state", + "s.modewant mode_want", + "s.modegiven mode_given", + "s.recvseqid recv_seq_id", + "s.readseqid read_seq_id", + "s.delid del_id", + "s.deletedat deleted_at", + "s.createdat created_at", + "s.updatedat updated_at", + "s.private private_json", + "'member' role", + "IF(s.deletedat IS NULL, 'active', 'left') member_status", + "'' group_nickname", + "0 pinned", + "0 mute_notifications", + "'projection' join_source", + "0 joined_by", + "s.createdat joined_at", + "s.deletedat left_at", + "0 removed_by", + "0 version", + ). + Where("s.topic", in.Topic) + + if in.UserId > 0 { + mod = mod.Where("s.userid", in.UserId) + } + + mod = mod.Page(in.Page, in.PerPage).OrderAsc("s.deletedat").OrderAsc("s.userid") + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM群成员列表失败,请稍后重试!") + return + } + return +} + +// Governance 获取IM群治理配置 +func (s *sSysImGroup) Governance(ctx context.Context, in *sysin.ImGroupTopicInp) (res *sysin.ImGroupGovernanceModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return nil, err + } + err = db.Model("im_group_settings").Safe().Ctx(ctx). + Fields( + "topic", + "owner_user_id", + "name", + "avatar_file_id", + "description", + "group_status", + "join_approval", + "mute_all", + "member_upload_files", + "member_private_chat", + "member_invite", + "max_member_count", + "max_admin_count", + "member_count", + "version", + "created_by", + "dissolved_by", + "dissolved_at", + "created_at", + "updated_at", + ). + Where("topic", in.Topic). + Scan(&res) + if err != nil { + return nil, gerror.Wrap(err, "获取IM群治理配置失败,请稍后重试!") + } + if res == nil { + return nil, nil + } + res.TenantId = tenant.Id + res.TenantName = tenant.Name + return +} + +// JoinRequestList 获取IM入群申请列表 +func (s *sSysImGroup) JoinRequestList(ctx context.Context, in *sysin.ImGroupJoinRequestListInp) (list []*sysin.ImGroupJoinRequestListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + mod := db.Model("im_group_join_requests").Safe().Ctx(ctx).Where("topic", in.Topic) + if in.Status != "" { + mod = mod.Where("status", in.Status) + } + if in.ApplicantUserId > 0 { + mod = mod.Where("applicant_user_id", in.ApplicantUserId) + } + mod = mod.Page(in.Page, in.PerPage).OrderDesc("id") + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM入群申请列表失败,请稍后重试!") + } + return +} + +// InvitationList 获取IM群邀请列表 +func (s *sSysImGroup) InvitationList(ctx context.Context, in *sysin.ImGroupInvitationListInp) (list []*sysin.ImGroupInvitationListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + mod := db.Model("im_group_invitations").Safe().Ctx(ctx).Where("topic", in.Topic) + if in.Status != "" { + mod = mod.Where("status", in.Status) + } + if in.TargetUserId > 0 { + mod = mod.Where("target_user_id", in.TargetUserId) + } + if in.InviterUserId > 0 { + mod = mod.Where("inviter_user_id", in.InviterUserId) + } + mod = mod.Page(in.Page, in.PerPage).OrderDesc("id") + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM群邀请列表失败,请稍后重试!") + } + return +} + +// AnnouncementList 获取IM群公告列表 +func (s *sSysImGroup) AnnouncementList(ctx context.Context, in *sysin.ImGroupAnnouncementListInp) (list []*sysin.ImGroupAnnouncementListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + mod := db.Model("im_group_announcements").Safe().Ctx(ctx).Where("topic", in.Topic) + if in.Status != "" { + mod = mod.Where("status", in.Status) + } + mod = mod.Page(in.Page, in.PerPage).OrderDesc("published_at").OrderDesc("id") + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM群公告列表失败,请稍后重试!") + } + return +} + +// MuteList 获取IM群禁言列表 +func (s *sSysImGroup) MuteList(ctx context.Context, in *sysin.ImGroupMuteListInp) (list []*sysin.ImGroupMuteListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + mod := db.Model("im_group_mutes").Safe().Ctx(ctx).Where("topic", in.Topic) + if in.Status != "" { + mod = mod.Where("status", in.Status) + } + if in.UserId > 0 { + mod = mod.Where("user_id", in.UserId) + } + mod = mod.Page(in.Page, in.PerPage).OrderDesc("muted_at").OrderDesc("user_id") + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM群禁言列表失败,请稍后重试!") + } + return +} + +// AdminLogList 获取IM群管理日志列表 +func (s *sSysImGroup) AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLogListInp) (list []*sysin.ImGroupAdminLogListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + mod := db.Model("im_group_admin_logs").Safe().Ctx(ctx). + Fields( + "id", + "log_id", + "topic", + "operator_id", + "target_user_id", + "action", + "object_type", + "object_id", + "CAST(before_data AS CHAR) before_data", + "CAST(after_data AS CHAR) after_data", + "request_id", + "idempotency_key", + "created_at", + ). + Where("topic", in.Topic) + if in.Action != "" { + mod = mod.WhereLike("action", "%"+in.Action+"%") + } + if in.OperatorId > 0 { + mod = mod.Where("operator_id", in.OperatorId) + } + mod = mod.Page(in.Page, in.PerPage).OrderDesc("id") + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM群管理日志列表失败,请稍后重试!") + } + return +} diff --git a/server/internal/logic/sys/im_invite.go b/server/internal/logic/sys/im_invite.go new file mode 100644 index 0000000..13bdfd0 --- /dev/null +++ b/server/internal/logic/sys/im_invite.go @@ -0,0 +1,315 @@ +package sys + +import ( + "context" + "fmt" + "hotgo/internal/library/imadmin" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + "io" + "strconv" + "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" +) + +type sSysImInvite struct{} + +func NewSysImInvite() *sSysImInvite { + return &sSysImInvite{} +} + +func init() { + service.RegisterSysImInvite(NewSysImInvite()) +} + +type imInviteListRequest struct { + Page int `json:"page,omitempty"` + PerPage int `json:"per_page,omitempty"` + InviteCode string `json:"invite_code,omitempty"` + Status string `json:"status,omitempty"` + BatchID int64 `json:"batch_id,omitempty"` + CreatedBy int64 `json:"created_by,omitempty"` + CreatedAtStart string `json:"created_at_start,omitempty"` + CreatedAtEnd string `json:"created_at_end,omitempty"` + ExpiresAtStart string `json:"expires_at_start,omitempty"` + ExpiresAtEnd string `json:"expires_at_end,omitempty"` +} + +type imInviteCreateRequest struct { + ExpiresAt string `json:"expires_at"` + Remark string `json:"remark,omitempty"` + IdempotencyKey string `json:"idempotency_key,omitempty"` +} + +type imInviteBatchCreateRequest struct { + Quantity int `json:"quantity"` + ExpiresAt string `json:"expires_at"` + Remark string `json:"remark,omitempty"` + IdempotencyKey string `json:"idempotency_key,omitempty"` +} + +type imInviteCodeResponse struct { + ID int64 `json:"id"` + BatchID *int64 `json:"batch_id"` + Code string `json:"code"` + Status string `json:"status"` + ExpiresAt *time.Time `json:"expires_at"` + UsedBy *int64 `json:"used_by"` + UsedAt *time.Time `json:"used_at"` + RequestID string `json:"request_id"` + CreatedBy int64 `json:"created_by"` + Remark string `json:"remark"` + CreatedAt *time.Time `json:"created_at"` +} + +type imInviteBatchResponse struct { + BatchID int64 `json:"batch_id"` + Quantity int `json:"quantity"` + ActiveCount int `json:"active_count"` + UsedCount int `json:"used_count"` + RevokedCount int `json:"revoked_count"` + ExpiredCount int `json:"expired_count"` + ExpiresAt *time.Time `json:"expires_at"` + CreatedAt *time.Time `json:"created_at"` +} + +func (s *sSysImInvite) List(ctx context.Context, in *sysin.ImInviteListInp) (list []*sysin.ImInviteCodeModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + req, err := buildInviteListRequest(in) + if err != nil { + return + } + var page imadmin.Page[imInviteCodeResponse] + if err = imadmin.DefaultClient(ctx).ListCodes(ctx, scope, req, &page); err != nil { + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, "failed", g.Map{"hasInviteCode": in.InviteCode != ""}) + return + } + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""}) + list = make([]*sysin.ImInviteCodeModel, 0, len(page.Items)) + for i := range page.Items { + list = append(list, convertInviteCode(page.Items[i])) + } + totalCount = page.Total + return +} + +func (s *sSysImInvite) Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + req := imInviteCreateRequest{ExpiresAt: formatRFC3339(in.ExpiresAt), Remark: in.Remark, IdempotencyKey: in.IdempotencyKey} + var upstream imInviteCodeResponse + err = imadmin.DefaultClient(ctx).CreateCode(ctx, scope, req, &upstream) + res = convertInviteCode(upstream) + result := "success" + if err != nil { + result = "failed" + } + targetId, _ := strconv.ParseInt(res.Id, 10, 64) + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_create", "invite_code", targetId, result, g.Map{"expiresAt": in.ExpiresAt, "remark": in.Remark}) + return +} + +func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + req := imInviteBatchCreateRequest{Quantity: in.Quantity, ExpiresAt: formatRFC3339(in.ExpiresAt), Remark: in.Remark, IdempotencyKey: in.IdempotencyKey} + var upstream imInviteBatchResponse + err = imadmin.DefaultClient(ctx).CreateBatch(ctx, scope, req, &upstream) + res = convertInviteBatch(upstream) + result := "success" + if err != nil { + result = "failed" + } + targetId, _ := strconv.ParseInt(res.BatchId, 10, 64) + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", targetId, result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark}) + return +} + +func (s *sSysImInvite) BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error) { + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + batchId, err := sysin.ParseOptionalInt64(in.BatchId) + if err != nil { + return + } + var upstream imInviteBatchResponse + err = imadmin.DefaultClient(ctx).GetBatch(ctx, scope, batchId, &upstream) + res = &sysin.ImInviteBatchDetailModel{ImInviteBatchModel: *convertInviteBatch(upstream)} + return +} + +func (s *sSysImInvite) Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error) { + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + id, err := sysin.ParseOptionalInt64(in.Id) + if err != nil { + return + } + err = imadmin.DefaultClient(ctx).RevokeCode(ctx, scope, id) + result := "success" + if err != nil { + result = "failed" + } + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_revoke", "invite_code", id, result, nil) + return +} + +func (s *sSysImInvite) BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error) { + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + batchId, err := sysin.ParseOptionalInt64(in.BatchId) + if err != nil { + return + } + var upstream struct { + Batch *imInviteBatchResponse `json:"batch"` + RevokedCount int `json:"revoked_count"` + } + err = imadmin.DefaultClient(ctx).RevokeBatch(ctx, scope, batchId, &upstream) + if upstream.Batch != nil { + res = convertInviteBatch(*upstream.Batch) + } else { + res = &sysin.ImInviteBatchModel{BatchId: strconv.FormatInt(batchId, 10), RevokedCount: upstream.RevokedCount} + } + result := "success" + if err != nil { + result = "failed" + } + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_revoke", "invite_batch", batchId, result, nil) + return +} + +func (s *sSysImInvite) Export(ctx context.Context, in *sysin.ImInviteListInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + req, err := buildInviteListRequest(in) + if err != nil { + return + } + resp, err := imadmin.DefaultClient(ctx).ExportCodes(ctx, scope, req) + if err != nil { + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "failed", g.Map{"hasInviteCode": in.InviteCode != ""}) + return + } + defer resp.Body.Close() + request := ghttp.RequestFromCtx(ctx) + if request == nil { + return gerror.New("请求上下文不存在") + } + filename := resp.Header.Get("Content-Disposition") + if filename == "" { + filename = `attachment; filename="registration-invite-codes.csv"` + } + request.Response.Header().Set("Content-Type", resp.Header.Get("Content-Type")) + request.Response.Header().Set("Content-Disposition", filename) + if _, err = io.Copy(request.Response.Writer, resp.Body); err != nil { + return gerror.Wrap(err, "转发导出文件失败") + } + _ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""}) + fmt.Fprint(request.Response.Writer, "") + return +} + +func buildInviteListRequest(in *sysin.ImInviteListInp) (imInviteListRequest, error) { + batchId, err := sysin.ParseOptionalInt64(in.BatchId) + if err != nil { + return imInviteListRequest{}, err + } + createdBy, err := sysin.ParseOptionalInt64(in.CreatedBy) + if err != nil { + return imInviteListRequest{}, err + } + return imInviteListRequest{ + Page: in.Page, + PerPage: in.PerPage, + InviteCode: in.InviteCode, + Status: in.Status, + BatchID: batchId, + CreatedBy: createdBy, + CreatedAtStart: formatRFC3339(in.CreatedAtStart), + CreatedAtEnd: formatRFC3339(in.CreatedAtEnd), + ExpiresAtStart: formatRFC3339(in.ExpiresAtStart), + ExpiresAtEnd: formatRFC3339(in.ExpiresAtEnd), + }, nil +} + +func formatRFC3339(t *gtime.Time) string { + if t == nil { + return "" + } + return t.Time.UTC().Format(time.RFC3339) +} + +func convertTime(t *time.Time) *gtime.Time { + if t == nil { + return nil + } + return gtime.New(t) +} + +func formatOptionalInt64(value *int64) string { + if value == nil || *value == 0 { + return "" + } + return strconv.FormatInt(*value, 10) +} + +func convertInviteCode(item imInviteCodeResponse) *sysin.ImInviteCodeModel { + return &sysin.ImInviteCodeModel{ + Id: strconv.FormatInt(item.ID, 10), + BatchId: formatOptionalInt64(item.BatchID), + Code: item.Code, + Status: item.Status, + ExpiresAt: convertTime(item.ExpiresAt), + UsedBy: formatOptionalInt64(item.UsedBy), + UsedAt: convertTime(item.UsedAt), + RequestId: item.RequestID, + CreatedBy: strconv.FormatInt(item.CreatedBy, 10), + Remark: item.Remark, + CreatedAt: convertTime(item.CreatedAt), + } +} + +func convertInviteBatch(item imInviteBatchResponse) *sysin.ImInviteBatchModel { + return &sysin.ImInviteBatchModel{ + BatchId: strconv.FormatInt(item.BatchID, 10), + Quantity: item.Quantity, + ActiveCount: item.ActiveCount, + UsedCount: item.UsedCount, + RevokedCount: item.RevokedCount, + ExpiredCount: item.ExpiredCount, + ExpiresAt: convertTime(item.ExpiresAt), + CreatedAt: convertTime(item.CreatedAt), + } +} diff --git a/server/internal/logic/sys/im_realname.go b/server/internal/logic/sys/im_realname.go new file mode 100644 index 0000000..a718926 --- /dev/null +++ b/server/internal/logic/sys/im_realname.go @@ -0,0 +1,164 @@ +// 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/imtenant" + "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 sSysImRealname struct{} + +func NewSysImRealname() *sSysImRealname { + return &sSysImRealname{} +} + +func init() { + service.RegisterSysImRealname(NewSysImRealname()) +} + +// List 获取IM实名审核列表 +func (s *sSysImRealname) List(ctx context.Context, in *sysin.ImRealnameListInp) (list []*sysin.ImRealnameListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + mod := s.model(ctx, db) + mod = s.applyWhere(mod, in) + mod = mod.Page(in.Page, in.PerPage).OrderDesc("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 *sSysImRealname) model(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model("im_realname_verifications").Safe().Ctx(ctx). + Fields( + "id", + "user_id", + "submission_id", + "status", + "id_type", + "masked_name", + "masked_id_number", + "review_reason", + "verified_at", + "created_at", + "updated_at", + ) +} + +func (s *sSysImRealname) applyWhere(mod *gdb.Model, in *sysin.ImRealnameListInp) *gdb.Model { + if in.UserId > 0 { + mod = mod.Where("user_id", in.UserId) + } + if in.SubmissionId != "" { + mod = mod.WhereLike("submission_id", "%"+in.SubmissionId+"%") + } + if in.Status != "" { + mod = mod.Where("status", in.Status) + } + if len(in.CreatedAt) == 2 { + mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1]) + } + return mod +} + +// View 获取IM实名审核指定信息 +func (s *sSysImRealname) View(ctx context.Context, in *sysin.ImRealnameViewInp) (res *sysin.ImRealnameViewModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return nil, err + } + + err = db.Model("im_realname_verifications").Safe().Ctx(ctx). + Fields( + "id", + "user_id", + "submission_id", + "status", + "id_type", + "masked_name", + "masked_id_number", + "id_number_hash", + "review_reason", + "verified_at", + "created_at", + "updated_at", + ). + Where("id", in.Id). + Scan(&res) + if err != nil { + return nil, gerror.Wrap(err, "获取IM实名审核信息失败,请稍后重试!") + } + if res == nil { + return nil, nil + } + res.TenantId = tenant.Id + res.TenantName = tenant.Name + return +} + +// Review 审核IM实名 +func (s *sSysImRealname) Review(ctx context.Context, in *sysin.ImRealnameReviewInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + data := g.Map{ + "status": in.Status, + "review_reason": in.ReviewReason, + "updated_at": gtime.Now(), + } + if in.Status == consts.ImRealnameStatusVerified { + data["verified_at"] = gtime.Now() + } else { + data["verified_at"] = nil + } + + result, err := db.Model("im_realname_verifications").Safe().Ctx(ctx). + Where("id", in.Id). + Where("status", consts.ImRealnameStatusPending). + Data(data). + Update() + if err != nil { + return gerror.Wrap(err, "审核IM实名失败,请稍后重试!") + } + affected, err := result.RowsAffected() + if err != nil { + return gerror.Wrap(err, "读取审核结果失败,请稍后重试!") + } + if affected == 0 { + return gerror.New("当前记录不是待审核状态,不能重复审核") + } + return +} diff --git a/server/internal/logic/sys/im_registration.go b/server/internal/logic/sys/im_registration.go new file mode 100644 index 0000000..688e54e --- /dev/null +++ b/server/internal/logic/sys/im_registration.go @@ -0,0 +1,116 @@ +package sys + +import ( + "context" + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "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" +) + +const ( + imRegistrationConfigGroup = "im" + imRegistrationSmsKey = "register_sms_enabled" +) + +type sSysImRegistration struct{} + +func NewSysImRegistration() *sSysImRegistration { + return &sSysImRegistration{} +} + +func init() { + service.RegisterSysImRegistration(NewSysImRegistration()) +} + +func (s *sSysImRegistration) GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error) { + if err = s.verifySuper(ctx); err != nil { + return + } + return s.getConfig(ctx) +} + +func (s *sSysImRegistration) UpdateConfig(ctx context.Context, in *sysin.ImRegistrationConfigUpdateInp) (err error) { + if err = s.verifySuper(ctx); err != nil { + return + } + old, err := s.getConfig(ctx) + if err != nil { + return + } + value := "0" + if in.RegisterSmsEnabled { + value = "1" + } + _, err = dao.SysConfig.Ctx(ctx). + Where(dao.SysConfig.Columns().Group, imRegistrationConfigGroup). + Where(dao.SysConfig.Columns().Key, imRegistrationSmsKey). + Data(g.Map{ + dao.SysConfig.Columns().Value: value, + dao.SysConfig.Columns().UpdatedAt: gtime.Now(), + }). + Update() + if err != nil { + return gerror.Wrap(err, "更新IM注册短信开关失败") + } + _ = writeImAdminAudit(ctx, 0, "registration_sms_config_update", "config", 0, "success", g.Map{ + "old": old.RegisterSmsEnabled, + "new": in.RegisterSmsEnabled, + }) + return +} + +func (s *sSysImRegistration) getConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error) { + var record gdb.Record + record, err = dao.SysConfig.Ctx(ctx). + Where(dao.SysConfig.Columns().Group, imRegistrationConfigGroup). + Where(dao.SysConfig.Columns().Key, imRegistrationSmsKey). + Where(dao.SysConfig.Columns().Status, 1). + One() + if err != nil { + return nil, gerror.Wrap(err, "获取IM注册短信开关失败") + } + if record.IsEmpty() { + if err = s.seedConfig(ctx); err != nil { + return nil, err + } + return s.getConfig(ctx) + } + return &sysin.ImRegistrationConfigModel{ + RegisterSmsEnabled: record[dao.SysConfig.Columns().Value].String() == "1", + UpdatedAt: record[dao.SysConfig.Columns().UpdatedAt].GTime(), + }, nil +} + +func (s *sSysImRegistration) seedConfig(ctx context.Context) (err error) { + _, err = dao.SysConfig.Ctx(ctx).Data(g.Map{ + dao.SysConfig.Columns().Group: imRegistrationConfigGroup, + dao.SysConfig.Columns().Name: "注册短信发送", + dao.SysConfig.Columns().Type: "bool", + dao.SysConfig.Columns().Key: imRegistrationSmsKey, + dao.SysConfig.Columns().Value: "1", + dao.SysConfig.Columns().DefaultValue: "1", + dao.SysConfig.Columns().Sort: 10, + dao.SysConfig.Columns().Tip: "控制 IM 注册验证码是否真实发送", + dao.SysConfig.Columns().IsDefault: 1, + dao.SysConfig.Columns().Status: 1, + dao.SysConfig.Columns().CreatedAt: gtime.Now(), + dao.SysConfig.Columns().UpdatedAt: gtime.Now(), + }).Insert() + if err != nil { + return gerror.Wrap(err, "初始化IM注册短信开关失败") + } + return +} + +func (s *sSysImRegistration) verifySuper(ctx context.Context) error { + if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) { + return gerror.New("仅平台超级管理员可操作IM注册配置") + } + return nil +} diff --git a/server/internal/logic/sys/im_tenant.go b/server/internal/logic/sys/im_tenant.go new file mode 100644 index 0000000..6d9978b --- /dev/null +++ b/server/internal/logic/sys/im_tenant.go @@ -0,0 +1,273 @@ +// 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/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/library/hgorm/handler" + "hotgo/internal/library/hgorm/hook" + "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" +) + +type sSysImTenant struct{} + +func NewSysImTenant() *sSysImTenant { + return &sSysImTenant{} +} + +func init() { + service.RegisterSysImTenant(NewSysImTenant()) +} + +// Model IM租户ORM模型 +func (s *sSysImTenant) Model(ctx context.Context, option ...*handler.Option) *gdb.Model { + return handler.Model(imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...) +} + +func (s *sSysImTenant) configModel(ctx context.Context) *gdb.Model { + return imtenant.RegistryDB(ctx).Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx) +} + +// List 获取IM租户列表 +func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + + mod := s.Model(ctx).Fields( + dao.ImTenant.Columns().Id, + dao.ImTenant.Columns().Code, + dao.ImTenant.Columns().Name, + dao.ImTenant.Columns().TenantDesc, + dao.ImTenant.Columns().DbName, + dao.ImTenant.Columns().State, + dao.ImTenant.Columns().CreatedBy, + dao.ImTenant.Columns().UpdatedBy, + dao.ImTenant.Columns().CreatedAt, + dao.ImTenant.Columns().UpdatedAt, + ) + + if in.Id > 0 { + mod = mod.Where(dao.ImTenant.Columns().Id, in.Id) + } + if in.Code != "" { + mod = mod.WhereLike(dao.ImTenant.Columns().Code, "%"+in.Code+"%") + } + if in.Name != "" { + mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%") + } + if in.DbName != "" { + mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%") + } + if in.State != nil { + mod = mod.Where(dao.ImTenant.Columns().State, *in.State) + } + if len(in.CreatedAt) == 2 { + mod = mod.WhereBetween(dao.ImTenant.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) + } + + mod = mod.Page(in.Page, in.PerPage). + OrderDesc(dao.ImTenant.Columns().Id). + Hook(hook.MemberSummary) + + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM租户列表失败,请稍后重试!") + return + } + if len(list) == 0 { + return + } + + if err = s.fillListConfig(ctx, list); err != nil { + return + } + return +} + +func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenantListModel) (err error) { + ids := make([]int64, 0, len(list)) + for _, item := range list { + ids = append(ids, item.Id) + } + + var configs []*entity.ImTenantConfig + if err = s.configModel(ctx). + WhereIn(dao.ImTenantConfig.Columns().TenantId, ids). + Scan(&configs); err != nil { + return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!") + } + configMap := make(map[int64]*entity.ImTenantConfig, len(configs)) + for _, item := range configs { + configMap[item.TenantId] = item + } + + var tenants []*entity.ImTenant + if err = s.Model(ctx). + Fields(dao.ImTenant.Columns().Id, dao.ImTenant.Columns().DbDsn). + WhereIn(dao.ImTenant.Columns().Id, ids). + Scan(&tenants); err != nil { + return gerror.Wrap(err, "获取IM租户连接配置失败,请稍后重试!") + } + dsnMap := make(map[int64]string, len(tenants)) + for _, item := range tenants { + dsnMap[item.Id] = item.DbDsn + } + + for _, item := range list { + if conf, ok := configMap[item.Id]; ok { + item.MaxUsers = conf.MaxUsers + item.MaxGroups = conf.MaxGroups + item.MaxGroupMembers = conf.MaxGroupMembers + } + item.DbDsnConfigured = dsnMap[item.Id] != "" + } + return +} + +// Edit 修改/新增IM租户 +func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + + return imtenant.RegistryDB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) { + if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil { + return + } + + userId := contexts.GetUserId(ctx) + if in.Id > 0 { + in.UpdatedBy = userId + if _, err = s.Model(ctx). + Fields(sysin.ImTenantUpdateFields{}). + WherePri(in.Id).Data(in).Update(); err != nil { + return gerror.Wrap(err, "修改IM租户失败,请稍后重试!") + } + return s.saveConfig(ctx, in.Id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId) + } + + in.CreatedBy = userId + id, err := s.Model(ctx, &handler.Option{FilterAuth: false}). + Fields(sysin.ImTenantInsertFields{}). + Data(in).InsertAndGetId() + if err != nil { + return gerror.Wrap(err, "新增IM租户失败,请稍后重试!") + } + return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId) + }) +} + +func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) { + mod := s.Model(ctx).Where(dao.ImTenant.Columns().Code, code) + if id > 0 { + mod = mod.Where(dao.ImTenant.Columns().Id+"<>?", id) + } + count, err := mod.Count() + if err != nil { + return gerror.Wrap(err, "检查IM租户编码失败,请稍后重试!") + } + if count > 0 { + return gerror.New("租户编码已存在") + } + return +} + +func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers uint64, maxGroups uint64, maxGroupMembers uint, userId int64) (err error) { + var conf *entity.ImTenantConfig + if err = s.configModel(ctx). + Where(dao.ImTenantConfig.Columns().TenantId, tenantId). + Scan(&conf); err != nil { + return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!") + } + + data := g.Map{ + dao.ImTenantConfig.Columns().MaxUsers: maxUsers, + dao.ImTenantConfig.Columns().MaxGroups: maxGroups, + dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers, + } + if conf != nil && conf.Id > 0 { + data[dao.ImTenantConfig.Columns().UpdatedBy] = userId + if _, err = s.configModel(ctx). + Fields( + dao.ImTenantConfig.Columns().MaxUsers, + dao.ImTenantConfig.Columns().MaxGroups, + dao.ImTenantConfig.Columns().MaxGroupMembers, + dao.ImTenantConfig.Columns().UpdatedBy, + ). + WherePri(conf.Id).Data(data).Update(); err != nil { + return gerror.Wrap(err, "修改IM租户配置失败,请稍后重试!") + } + return + } + + data[dao.ImTenantConfig.Columns().TenantId] = tenantId + data[dao.ImTenantConfig.Columns().CreatedBy] = userId + if _, err = s.configModel(ctx). + Fields( + dao.ImTenantConfig.Columns().TenantId, + dao.ImTenantConfig.Columns().MaxUsers, + dao.ImTenantConfig.Columns().MaxGroups, + dao.ImTenantConfig.Columns().MaxGroupMembers, + dao.ImTenantConfig.Columns().CreatedBy, + ). + Data(data).Insert(); err != nil { + return gerror.Wrap(err, "新增IM租户配置失败,请稍后重试!") + } + return +} + +// View 获取IM租户指定信息 +func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil { + err = gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!") + return + } + if res == nil { + return + } + + var conf *entity.ImTenantConfig + if err = s.configModel(ctx). + Where(dao.ImTenantConfig.Columns().TenantId, in.Id). + Scan(&conf); err != nil { + err = gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!") + return + } + if conf != nil { + res.MaxUsers = conf.MaxUsers + res.MaxGroups = conf.MaxGroups + res.MaxGroupMembers = conf.MaxGroupMembers + } + res.DbDsnConfigured = res.DbDsn != "" + return +} + +// Status 更新IM租户状态 +func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{ + dao.ImTenant.Columns().State: in.State, + dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx), + }).Update(); err != nil { + err = gerror.Wrap(err, "更新IM租户状态失败,请稍后重试!") + return + } + return +} diff --git a/server/internal/logic/sys/im_user.go b/server/internal/logic/sys/im_user.go new file mode 100644 index 0000000..d10ce8d --- /dev/null +++ b/server/internal/logic/sys/im_user.go @@ -0,0 +1,209 @@ +// 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/library/imtenant" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" +) + +type sSysImUser struct{} + +func NewSysImUser() *sSysImUser { + return &sSysImUser{} +} + +func init() { + service.RegisterSysImUser(NewSysImUser()) +} + +// List 获取IM用户列表 +func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + mod := s.listModel(ctx, db) + mod = s.applyListWhere(mod, in) + mod = mod.Page(in.Page, in.PerPage).OrderDesc("u.id") + + if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { + err = gerror.Wrap(err, "获取IM用户列表失败,请稍后重试!") + return + } + if len(list) == 0 { + return + } + + for _, item := range list { + item.TenantId = tenant.Id + item.TenantName = tenant.Name + } + if err = s.fillAuthAndCredentials(ctx, db, list); err != nil { + return + } + return +} + +func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model("im_users u").Safe().Ctx(ctx). + LeftJoin("im_user_profiles up", "up.user_id = u.id"). + LeftJoin("im_user_security us", "us.user_id = u.id"). + Fields( + "u.id user_id", + "u.state", + "u.lastseen last_seen", + "u.useragent user_agent", + "u.createdat created_at", + "u.updatedat updated_at", + "up.nickname", + "up.public_account", + "up.gender", + "up.region", + "us.auth_version", + "us.password_updated_at", + ) +} + +func (s *sSysImUser) applyListWhere(mod *gdb.Model, in *sysin.ImUserListInp) *gdb.Model { + if in.UserId > 0 { + mod = mod.Where("u.id", in.UserId) + } + if in.Nickname != "" { + mod = mod.WhereLike("up.nickname", "%"+in.Nickname+"%") + } + if in.AuthName != "" { + mod = mod.Where("EXISTS (SELECT 1 FROM im_auth a WHERE a.userid = u.id AND a.uname LIKE ?)", "%"+in.AuthName+"%") + } + if in.State != nil { + mod = mod.Where("u.state", *in.State) + } + if len(in.CreatedAt) == 2 { + mod = mod.WhereBetween("u.createdat", in.CreatedAt[0], in.CreatedAt[1]) + } + if len(in.LastSeen) == 2 { + mod = mod.WhereBetween("u.lastseen", in.LastSeen[0], in.LastSeen[1]) + } + return mod +} + +func (s *sSysImUser) fillAuthAndCredentials(ctx context.Context, db gdb.DB, list []*sysin.ImUserListModel) (err error) { + ids := make([]int64, 0, len(list)) + userMap := make(map[int64]*sysin.ImUserListModel, len(list)) + for _, item := range list { + ids = append(ids, item.UserId) + userMap[item.UserId] = item + } + + authRows, err := db.Model("im_auth").Safe().Ctx(ctx). + Fields("userid", "scheme", "uname"). + WhereIn("userid", ids). + OrderAsc("scheme").OrderAsc("uname"). + All() + if err != nil { + return gerror.Wrap(err, "获取IM用户认证标识失败,请稍后重试!") + } + authMap := make(map[int64][]string) + for _, row := range authRows { + userId := row["userid"].Int64() + scheme := row["scheme"].String() + uname := row["uname"].String() + authMap[userId] = append(authMap[userId], scheme+":"+uname) + } + + credentialRows, err := db.Model("im_credentials").Safe().Ctx(ctx). + Fields("userid", "method", "value", "done"). + WhereIn("userid", ids). + WhereNull("deletedat"). + OrderAsc("method").OrderAsc("value"). + All() + if err != nil { + return gerror.Wrap(err, "获取IM用户凭证失败,请稍后重试!") + } + credentialMap := make(map[int64][]string) + for _, row := range credentialRows { + userId := row["userid"].Int64() + method := row["method"].String() + value := row["value"].String() + done := row["done"].Bool() + if !done { + value += "(未验证)" + } + credentialMap[userId] = append(credentialMap[userId], method+":"+value) + } + + for userId, item := range userMap { + item.AuthNames = strings.Join(authMap[userId], ", ") + item.Credentials = strings.Join(credentialMap[userId], ", ") + } + return +} + +// View 获取IM用户指定信息 +func (s *sSysImUser) View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + fields := append(s.viewFields(), "u.access access_json", "u.public profile_json", "u.trusted trusted_json", "u.tags tags_json") + err = db.Model("im_users u").Safe().Ctx(ctx). + LeftJoin("im_user_profiles up", "up.user_id = u.id"). + LeftJoin("im_user_security us", "us.user_id = u.id"). + Fields(fields...). + Where("u.id", in.UserId). + Scan(&res) + if err != nil { + return nil, gerror.Wrap(err, "获取IM用户信息失败,请稍后重试!") + } + if res == nil { + return nil, nil + } + + res.TenantId = tenant.Id + res.TenantName = tenant.Name + list := []*sysin.ImUserListModel{&res.ImUserListModel} + if err = s.fillAuthAndCredentials(ctx, db, list); err != nil { + return nil, err + } + return +} + +func (s *sSysImUser) viewFields() []interface{} { + return []interface{}{ + "u.id user_id", + "u.state", + "u.lastseen last_seen", + "u.useragent user_agent", + "u.createdat created_at", + "u.updatedat updated_at", + "up.nickname", + "up.public_account", + "up.gender", + "up.region", + "up.signature", + "up.avatar_file_id", + "CAST(up.birth_date AS CHAR) birth_date", + "up.version profile_version", + "us.auth_version", + "us.password_updated_at", + } +} diff --git a/server/internal/logic/sys/im_work_order.go b/server/internal/logic/sys/im_work_order.go new file mode 100644 index 0000000..0e9b6fc --- /dev/null +++ b/server/internal/logic/sys/im_work_order.go @@ -0,0 +1,212 @@ +// 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/library/imtenant" + "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 sSysImWorkOrder struct{} + +func NewSysImWorkOrder() *sSysImWorkOrder { + return &sSysImWorkOrder{} +} + +func init() { + service.RegisterSysImWorkOrder(NewSysImWorkOrder()) +} + +// List 获取IM举报反馈工单列表 +func (s *sSysImWorkOrder) List(ctx context.Context, in *sysin.ImWorkOrderListInp) (list []*sysin.ImWorkOrderListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + mod := s.listModel(ctx, db, in.Type) + mod = s.applyWhere(mod, in) + mod = mod.Page(in.Page, in.PerPage).OrderDesc("created_at").OrderDesc("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 *sSysImWorkOrder) listModel(ctx context.Context, db gdb.DB, typ string) *gdb.Model { + if typ == sysin.ImWorkOrderTypeAbuse { + return db.Model("im_abuse_reports").Safe().Ctx(ctx).Fields(s.abuseListFields()...) + } + return db.Model("im_feedback_submissions").Safe().Ctx(ctx).Fields(s.feedbackListFields()...) +} + +func (s *sSysImWorkOrder) abuseListFields() []interface{} { + return []interface{}{ + "'abuse' type", + "id", + "report_id order_id", + "reporter_id user_id", + "target_type", + "CONCAT(target_topic, IF(target_seq>0, CONCAT('#', target_seq), ''), IF(target_user_id>0, CONCAT(' user:', target_user_id), '')) target_summary", + "reason_code category", + "description content", + "status", + "created_at", + "updated_at", + } +} + +func (s *sSysImWorkOrder) feedbackListFields() []interface{} { + return []interface{}{ + "'feedback' type", + "id", + "feedback_id order_id", + "user_id", + "'' target_type", + "'' target_summary", + "category", + "content", + "status", + "created_at", + "updated_at", + } +} + +func (s *sSysImWorkOrder) applyWhere(mod *gdb.Model, in *sysin.ImWorkOrderListInp) *gdb.Model { + if in.UserId > 0 { + mod = mod.Where("user_id", in.UserId) + } + if in.Status != "" { + mod = mod.Where("status", in.Status) + } + if in.Keyword != "" { + mod = mod.WhereLike("content", "%"+in.Keyword+"%") + } + if len(in.CreatedAt) == 2 { + mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1]) + } + return mod +} + +// View 获取IM举报反馈工单指定信息 +func (s *sSysImWorkOrder) View(ctx context.Context, in *sysin.ImWorkOrderViewInp) (res *sysin.ImWorkOrderViewModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return nil, err + } + + if in.Type == sysin.ImWorkOrderTypeAbuse { + res, err = s.viewAbuse(ctx, db, in.Id) + } else { + res, err = s.viewFeedback(ctx, db, in.Id) + } + if err != nil || res == nil { + return + } + res.TenantId = tenant.Id + res.TenantName = tenant.Name + return +} + +func (s *sSysImWorkOrder) viewAbuse(ctx context.Context, db gdb.DB, id int64) (res *sysin.ImWorkOrderViewModel, err error) { + err = db.Model("im_abuse_reports").Safe().Ctx(ctx). + Fields( + "'abuse' type", + "id", + "report_id order_id", + "reporter_id user_id", + "target_type", + "CONCAT(target_topic, IF(target_seq>0, CONCAT('#', target_seq), ''), IF(target_user_id>0, CONCAT(' user:', target_user_id), '')) target_summary", + "reason_code category", + "description content", + "description", + "evidence_file_ids evidence_json", + "snapshot snapshot_json", + "idempotency_key", + "status", + "created_at", + "updated_at", + ). + Where("id", id). + Scan(&res) + if err != nil { + return nil, gerror.Wrap(err, "获取IM举报工单信息失败,请稍后重试!") + } + return +} + +func (s *sSysImWorkOrder) viewFeedback(ctx context.Context, db gdb.DB, id int64) (res *sysin.ImWorkOrderViewModel, err error) { + err = db.Model("im_feedback_submissions").Safe().Ctx(ctx). + Fields( + "'feedback' type", + "id", + "feedback_id order_id", + "user_id", + "'' target_type", + "'' target_summary", + "category", + "content", + "contact", + "attachment_file_ids evidence_json", + "client_context client_context_json", + "include_diagnostics", + "idempotency_key", + "status", + "created_at", + "updated_at", + ). + Where("id", id). + Scan(&res) + if err != nil { + return nil, gerror.Wrap(err, "获取IM反馈工单信息失败,请稍后重试!") + } + return +} + +// Status 更新IM举报反馈工单状态 +func (s *sSysImWorkOrder) Status(ctx context.Context, in *sysin.ImWorkOrderStatusInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, _, err := imtenant.DB(ctx, in.TenantId) + if err != nil { + return + } + + table := "im_abuse_reports" + if in.Type == sysin.ImWorkOrderTypeFeedback { + table = "im_feedback_submissions" + } + if _, err = db.Model(table).Safe().Ctx(ctx). + Where("id", in.Id). + Data(g.Map{ + "status": in.Status, + "updated_at": gtime.Now(), + }). + Update(); err != nil { + return gerror.Wrap(err, "更新IM举报反馈工单状态失败,请稍后重试!") + } + return +} diff --git a/server/internal/logic/sys/log.go b/server/internal/logic/sys/log.go index 147508e..17a0161 100644 --- a/server/internal/logic/sys/log.go +++ b/server/internal/logic/sys/log.go @@ -159,7 +159,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog { module = mctx.Module clientIp = location.GetClientIp(request) postData = gjson.New(consts.NilJsonToString) - getData = gjson.New(request.URL.Query()) + getData = s.MaskSensitiveJson(gjson.New(request.URL.Query())) headerData = gjson.New(consts.NilJsonToString) errorData = gjson.New(consts.NilJsonToString) data entity.SysLog @@ -194,7 +194,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog { // post参数 if body, ok := mctx.Data["request.body"].(*gjson.Json); ok { - postData = body + postData = s.MaskSensitiveJson(body) } // post表单 @@ -208,12 +208,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog { if postData.IsNil() || len(postData.Map()) == 0 { postData = gjson.New(consts.NilJsonToString) } else { - // 隐藏明文显示的密码 - for k := range postData.Map() { - if gstr.ContainsI(k, "password") { - postData.MustSet(k, "******") - } - } + postData = s.MaskSensitiveJson(postData) } // 当前登录用户 @@ -247,7 +242,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog { Url: request.URL.Path, GetData: getData, PostData: postData, - HeaderData: s.SimplifyHeaderParams(headerData), + HeaderData: s.SimplifyHeaderParams(s.MaskSensitiveJson(headerData)), Ip: clientIp, ProvinceId: ipData.ProvinceCode, CityId: ipData.CityCode, @@ -273,6 +268,10 @@ func (s *sSysLog) SimplifyHeaderParams(data *gjson.Json) *gjson.Json { var filters = []string{"Accept", "Authorization", "Cookie"} for _, filter := range filters { v := data.Get(filter) + if isSensitiveLogField(filter) { + data.MustSet(filter, "******") + continue + } if len(v.String()) > 128 { data.MustRemove(filter) } @@ -280,6 +279,58 @@ func (s *sSysLog) SimplifyHeaderParams(data *gjson.Json) *gjson.Json { return data } +// MaskSensitiveJson masks sensitive request fields before access logs are stored. +func (s *sSysLog) MaskSensitiveJson(data *gjson.Json) *gjson.Json { + if data == nil || data.IsNil() { + return data + } + return gjson.New(maskSensitiveValue(data.Map())) +} + +func maskSensitiveValue(v interface{}) interface{} { + switch value := v.(type) { + case map[string]interface{}: + res := make(map[string]interface{}, len(value)) + for k, item := range value { + if isSensitiveLogField(k) { + res[k] = "******" + continue + } + res[k] = maskSensitiveValue(item) + } + return res + case map[string][]string: + res := make(map[string]interface{}, len(value)) + for k, item := range value { + if isSensitiveLogField(k) { + res[k] = "******" + continue + } + res[k] = item + } + return res + case []interface{}: + for i, item := range value { + value[i] = maskSensitiveValue(item) + } + return value + default: + return v + } +} + +func isSensitiveLogField(field string) bool { + sensitive := []string{"password", "invitecode", "invite_code", "servicesecret", "service_secret", "authorization", "x-service-secret"} + normalized := gstr.Replace(gstr.Replace(gstr.ToLower(field), "-", ""), "_", "") + for _, item := range sensitive { + item = gstr.Replace(gstr.Replace(item, "-", ""), "_", "") + if normalized == item || gstr.Contains(normalized, item) { + return true + } + } + return false +} + // View 获取指定请求日志信息 func (s *sSysLog) View(ctx context.Context, in *sysin.LogViewInp) (res *sysin.LogViewModel, err error) { mod := s.Model(ctx) diff --git a/server/internal/model/context.go b/server/internal/model/context.go index 03f5abd..766cc83 100644 --- a/server/internal/model/context.go +++ b/server/internal/model/context.go @@ -22,6 +22,7 @@ type Context struct { // Identity 通用身份模型 type Identity struct { Id int64 `json:"id" description:"用户ID"` + TenantId int64 `json:"tenantId" description:"IM租户ID"` Pid int64 `json:"pid" description:"上级ID"` DeptId int64 `json:"deptId" description:"部门ID"` DeptType string `json:"deptType" description:"部门类型"` diff --git a/server/internal/model/do/admin_member.go b/server/internal/model/do/admin_member.go index 7bfc722..570c656 100755 --- a/server/internal/model/do/admin_member.go +++ b/server/internal/model/do/admin_member.go @@ -14,6 +14,7 @@ import ( type AdminMember struct { g.Meta `orm:"table:hg_admin_member, do:true"` Id any // 管理员ID + TenantId any // IM租户ID DeptId any // 部门ID RoleId any // 角色ID RealName any // 真实姓名 diff --git a/server/internal/model/do/im_tenant.go b/server/internal/model/do/im_tenant.go new file mode 100644 index 0000000..1600c02 --- /dev/null +++ b/server/internal/model/do/im_tenant.go @@ -0,0 +1,26 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImTenant is the golang structure of table im_tenant for DAO operations like Where/Data. +type ImTenant struct { + g.Meta `orm:"table:im_tenant, do:true"` + Id any // + Code any // + Name any // + TenantDesc any // + DbName any // + DbDsn any // + State any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // +} diff --git a/server/internal/model/do/im_tenant_config.go b/server/internal/model/do/im_tenant_config.go new file mode 100644 index 0000000..35ac912 --- /dev/null +++ b/server/internal/model/do/im_tenant_config.go @@ -0,0 +1,24 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImTenantConfig is the golang structure of table im_tenant_config for DAO operations like Where/Data. +type ImTenantConfig struct { + g.Meta `orm:"table:im_tenant_config, do:true"` + Id any // + TenantId any // + MaxUsers any // 0 means unlimited + MaxGroups any // 0 means unlimited + MaxGroupMembers any // 0 means unlimited + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // +} diff --git a/server/internal/model/entity/admin_member.go b/server/internal/model/entity/admin_member.go index 6ba5eff..43ab68d 100755 --- a/server/internal/model/entity/admin_member.go +++ b/server/internal/model/entity/admin_member.go @@ -12,6 +12,7 @@ import ( // AdminMember is the golang structure for table admin_member. type AdminMember struct { Id int64 `json:"id" orm:"id" description:"管理员ID"` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:"IM租户ID"` DeptId int64 `json:"deptId" orm:"dept_id" description:"部门ID"` RoleId int64 `json:"roleId" orm:"role_id" description:"角色ID"` RealName string `json:"realName" orm:"real_name" description:"真实姓名"` diff --git a/server/internal/model/entity/im_tenant.go b/server/internal/model/entity/im_tenant.go new file mode 100644 index 0000000..dc12c0c --- /dev/null +++ b/server/internal/model/entity/im_tenant.go @@ -0,0 +1,24 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// ImTenant is the golang structure for table im_tenant. +type ImTenant struct { + Id int64 `json:"id" orm:"id" description:""` + Code string `json:"code" orm:"code" description:""` + Name string `json:"name" orm:"name" description:""` + TenantDesc string `json:"tenantDesc" orm:"tenant_desc" description:""` + DbName string `json:"dbName" orm:"db_name" description:""` + DbDsn string `json:"dbDsn" orm:"db_dsn" description:""` + State int `json:"state" orm:"state" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` +} diff --git a/server/internal/model/entity/im_tenant_config.go b/server/internal/model/entity/im_tenant_config.go new file mode 100644 index 0000000..a28e460 --- /dev/null +++ b/server/internal/model/entity/im_tenant_config.go @@ -0,0 +1,22 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// ImTenantConfig is the golang structure for table im_tenant_config. +type ImTenantConfig struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:""` + MaxUsers uint64 `json:"maxUsers" orm:"max_users" description:"0 means unlimited"` + MaxGroups uint64 `json:"maxGroups" orm:"max_groups" description:"0 means unlimited"` + MaxGroupMembers uint `json:"maxGroupMembers" orm:"max_group_members" description:"0 means unlimited"` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` +} diff --git a/server/internal/model/input/adminin/member.go b/server/internal/model/input/adminin/member.go index 72a512d..a851b44 100644 --- a/server/internal/model/input/adminin/member.go +++ b/server/internal/model/input/adminin/member.go @@ -115,6 +115,7 @@ type LoginMemberInfoModel struct { // MemberEditInp 修改用户 type MemberEditInp struct { Id int64 `json:"id" dc:"管理员ID"` + TenantId int64 `json:"tenantId" dc:"IM租户ID"` RoleId int64 `json:"roleId" v:"required#角色不能为空" dc:"角色ID"` PostIds []int64 `json:"postIds" dc:"岗位ID"` DeptId int64 `json:"deptId" v:"required#部门不能为空" dc:"部门ID"` diff --git a/server/internal/model/input/sysin/im_audit_log.go b/server/internal/model/input/sysin/im_audit_log.go new file mode 100644 index 0000000..3589a35 --- /dev/null +++ b/server/internal/model/input/sysin/im_audit_log.go @@ -0,0 +1,75 @@ +// 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" +) + +// ImAuditLogListInp 获取IM审计日志列表 +type ImAuditLogListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + ActorUid int64 `json:"actorUid" dc:"操作人UID"` + ActorRole string `json:"actorRole" dc:"操作角色"` + Action string `json:"action" dc:"操作类型"` + TargetType string `json:"targetType" dc:"目标类型"` + TargetId string `json:"targetId" dc:"目标ID"` + RequestId string `json:"requestId" dc:"请求ID"` + Ip string `json:"ip" dc:"IP"` + Keyword string `json:"keyword" dc:"关键词"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"` +} + +func (in *ImAuditLogListInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + return +} + +type ImAuditLogListModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + ActorUid int64 `json:"actorUid" dc:"操作人UID"` + ActorRole string `json:"actorRole" dc:"操作角色"` + Action string `json:"action" dc:"操作类型"` + TargetType string `json:"targetType" dc:"目标类型"` + TargetId string `json:"targetId" dc:"目标ID"` + Reason string `json:"reason" dc:"操作原因"` + RequestId string `json:"requestId" dc:"请求ID"` + Ip string `json:"ip" dc:"IP"` + UserAgent string `json:"userAgent" dc:"客户端"` + BeforeSnapshot string `json:"beforeSnapshot" dc:"变更前快照"` + AfterSnapshot string `json:"afterSnapshot" dc:"变更后快照"` + Metadata string `json:"metadata" dc:"元数据"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} + +// ImAuditLogViewInp 获取指定IM审计日志信息 +type ImAuditLogViewInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"` +} + +func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Id <= 0 { + return gerror.New("ID不能为空") + } + return +} + +type ImAuditLogViewModel struct { + ImAuditLogListModel +} diff --git a/server/internal/model/input/sysin/im_group.go b/server/internal/model/input/sysin/im_group.go new file mode 100644 index 0000000..d2e68b3 --- /dev/null +++ b/server/internal/model/input/sysin/im_group.go @@ -0,0 +1,290 @@ +// 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" +) + +// ImGroupListInp 获取IM群组列表 +type ImGroupListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + Topic string `json:"topic" dc:"Topic"` + OwnerId int64 `json:"ownerId" dc:"群主ID"` + State *int `json:"state" dc:"通信状态"` + GroupStatus string `json:"groupStatus" dc:"业务状态"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"` + TouchedAt []*gtime.Time `json:"touchedAt" dc:"活跃时间"` +} + +func (in *ImGroupListInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + return +} + +type ImGroupListModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Topic string `json:"topic" dc:"Topic"` + State int `json:"state" dc:"状态"` + OwnerId int64 `json:"ownerId" dc:"群主ID"` + SeqId int `json:"seqId" dc:"最新消息序号"` + DelId int `json:"delId" dc:"删除序号"` + SubCnt int `json:"subCnt" dc:"成员数"` + TouchedAt *gtime.Time `json:"touchedAt" dc:"活跃时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` + PublicJson string `json:"publicJson" dc:"公开资料"` + TrustedJson string `json:"trustedJson" dc:"可信资料"` + TagsJson string `json:"tagsJson" dc:"标签"` + AuxJson string `json:"auxJson" dc:"辅助资料"` + + GroupName string `json:"groupName" dc:"群名称"` + AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"` + Description string `json:"description" dc:"简介"` + GroupStatus string `json:"groupStatus" dc:"业务状态"` + JoinApproval int `json:"joinApproval" dc:"入群审批"` + MuteAll int `json:"muteAll" dc:"全员禁言"` + MemberUploadFiles int `json:"memberUploadFiles" dc:"成员上传文件"` + MemberPrivateChat int `json:"memberPrivateChat" dc:"成员私聊"` + MemberInvite int `json:"memberInvite" dc:"成员邀请"` + MaxMemberCount int `json:"maxMemberCount" dc:"最大成员数"` + MaxAdminCount int `json:"maxAdminCount" dc:"最大管理员数"` + BusinessMemberCount int `json:"businessMemberCount" dc:"业务成员数"` + SettingsVersion int `json:"settingsVersion" dc:"治理配置版本"` + SettingsUpdatedAt *gtime.Time `json:"settingsUpdatedAt" dc:"治理配置更新时间"` +} + +// ImGroupViewInp 获取指定IM群组信息 +type ImGroupViewInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"` +} + +func (in *ImGroupViewInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Topic == "" { + return gerror.New("Topic不能为空") + } + return +} + +type ImGroupViewModel struct { + ImGroupListModel + AccessJson string `json:"accessJson" dc:"访问权限"` + UseBt int `json:"useBt" dc:"是否开启BT"` +} + +// ImGroupMemberListInp 获取IM群成员列表 +type ImGroupMemberListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"` + UserId int64 `json:"userId" dc:"用户ID"` + Role string `json:"role" dc:"角色"` + MemberStatus string `json:"memberStatus" dc:"成员状态"` +} + +func (in *ImGroupMemberListInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Topic == "" { + return gerror.New("Topic不能为空") + } + return +} + +type ImGroupMemberListModel struct { + UserId int64 `json:"userId" dc:"用户ID"` + Nickname string `json:"nickname" dc:"昵称"` + State int `json:"state" dc:"用户状态"` + ModeWant string `json:"modeWant" dc:"期望权限"` + ModeGiven string `json:"modeGiven" dc:"授予权限"` + RecvSeqId int `json:"recvSeqId" dc:"接收序号"` + ReadSeqId int `json:"readSeqId" dc:"已读序号"` + DelId int `json:"delId" dc:"删除序号"` + DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"加入时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` + PrivateJson string `json:"privateJson" dc:"私有资料"` + Role string `json:"role" dc:"业务角色"` + MemberStatus string `json:"memberStatus" dc:"业务状态"` + GroupNickname string `json:"groupNickname" dc:"群昵称"` + Pinned int `json:"pinned" dc:"置顶"` + MuteNotifications int `json:"muteNotifications" dc:"免打扰"` + JoinSource string `json:"joinSource" dc:"加入来源"` + JoinedBy int64 `json:"joinedBy" dc:"邀请/处理人"` + JoinedAt *gtime.Time `json:"joinedAt" dc:"业务加入时间"` + LeftAt *gtime.Time `json:"leftAt" dc:"离开时间"` + RemovedBy int64 `json:"removedBy" dc:"移除人"` + Version int `json:"version" dc:"版本"` +} + +// ImGroupTopicInp 群Topic通用入参 +type ImGroupTopicInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"` +} + +func (in *ImGroupTopicInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Topic == "" { + return gerror.New("Topic不能为空") + } + return +} + +type ImGroupGovernanceModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Topic string `json:"topic" dc:"Topic"` + OwnerUserId int64 `json:"ownerUserId" dc:"群主ID"` + Name string `json:"name" dc:"群名称"` + AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"` + Description string `json:"description" dc:"简介"` + GroupStatus string `json:"groupStatus" dc:"业务状态"` + JoinApproval int `json:"joinApproval" dc:"入群审批"` + MuteAll int `json:"muteAll" dc:"全员禁言"` + MemberUploadFiles int `json:"memberUploadFiles" dc:"成员上传文件"` + MemberPrivateChat int `json:"memberPrivateChat" dc:"成员私聊"` + MemberInvite int `json:"memberInvite" dc:"成员邀请"` + MaxMemberCount int `json:"maxMemberCount" dc:"最大成员数"` + MaxAdminCount int `json:"maxAdminCount" dc:"最大管理员数"` + MemberCount int `json:"memberCount" dc:"成员数"` + Version int `json:"version" dc:"版本"` + CreatedBy int64 `json:"createdBy" dc:"创建人"` + DissolvedBy int64 `json:"dissolvedBy" dc:"解散人"` + DissolvedAt *gtime.Time `json:"dissolvedAt" dc:"解散时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +type ImGroupJoinRequestListInp struct { + form.PageReq + ImGroupTopicInp + Status string `json:"status" dc:"状态"` + ApplicantUserId int64 `json:"applicantUserId" dc:"申请人ID"` +} + +type ImGroupJoinRequestListModel struct { + Id int64 `json:"id" dc:"ID"` + RequestId string `json:"requestId" dc:"申请ID"` + Topic string `json:"topic" dc:"Topic"` + ApplicantUserId int64 `json:"applicantUserId" dc:"申请人ID"` + InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"` + Source string `json:"source" dc:"来源"` + Message string `json:"message" dc:"申请消息"` + Status string `json:"status" dc:"状态"` + RejectReason string `json:"rejectReason" dc:"拒绝原因"` + HandledBy int64 `json:"handledBy" dc:"处理人"` + HandledAt *gtime.Time `json:"handledAt" dc:"处理时间"` + ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"` + Version int `json:"version" dc:"版本"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +type ImGroupInvitationListInp struct { + form.PageReq + ImGroupTopicInp + Status string `json:"status" dc:"状态"` + TargetUserId int64 `json:"targetUserId" dc:"目标用户ID"` + InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"` +} + +type ImGroupInvitationListModel struct { + Id int64 `json:"id" dc:"ID"` + InvitationId string `json:"invitationId" dc:"邀请ID"` + Topic string `json:"topic" dc:"Topic"` + InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"` + TargetUserId int64 `json:"targetUserId" dc:"目标用户ID"` + Message string `json:"message" dc:"邀请消息"` + Status string `json:"status" dc:"状态"` + Result string `json:"result" dc:"结果"` + JoinRequestId string `json:"joinRequestId" dc:"入群申请ID"` + ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"` + HandledAt *gtime.Time `json:"handledAt" dc:"处理时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +type ImGroupAnnouncementListInp struct { + form.PageReq + ImGroupTopicInp + Status string `json:"status" dc:"状态"` +} + +type ImGroupAnnouncementListModel struct { + Id int64 `json:"id" dc:"ID"` + AnnouncementId string `json:"announcementId" dc:"公告ID"` + Topic string `json:"topic" dc:"Topic"` + AuthorUserId int64 `json:"authorUserId" dc:"发布人ID"` + Content string `json:"content" dc:"内容"` + Status string `json:"status" dc:"状态"` + Version int `json:"version" dc:"版本"` + PublishedAt *gtime.Time `json:"publishedAt" dc:"发布时间"` + DeletedBy int64 `json:"deletedBy" dc:"删除人"` + DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +type ImGroupMuteListInp struct { + form.PageReq + ImGroupTopicInp + Status string `json:"status" dc:"状态"` + UserId int64 `json:"userId" dc:"用户ID"` +} + +type ImGroupMuteListModel struct { + Topic string `json:"topic" dc:"Topic"` + UserId int64 `json:"userId" dc:"用户ID"` + MutedBy int64 `json:"mutedBy" dc:"禁言人"` + MutedAt *gtime.Time `json:"mutedAt" dc:"禁言时间"` + ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"` + Reason string `json:"reason" dc:"原因"` + Status string `json:"status" dc:"状态"` + RevokedBy int64 `json:"revokedBy" dc:"解除人"` + RevokedAt *gtime.Time `json:"revokedAt" dc:"解除时间"` + Version int `json:"version" dc:"版本"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +type ImGroupAdminLogListInp struct { + form.PageReq + ImGroupTopicInp + Action string `json:"action" dc:"动作"` + OperatorId int64 `json:"operatorId" dc:"操作人ID"` +} + +type ImGroupAdminLogListModel struct { + Id int64 `json:"id" dc:"ID"` + LogId string `json:"logId" dc:"日志ID"` + Topic string `json:"topic" dc:"Topic"` + OperatorId int64 `json:"operatorId" dc:"操作人"` + TargetUserId int64 `json:"targetUserId" dc:"目标用户"` + Action string `json:"action" dc:"动作"` + ObjectType string `json:"objectType" dc:"对象类型"` + ObjectId string `json:"objectId" dc:"对象ID"` + BeforeData string `json:"beforeData" dc:"变更前"` + AfterData string `json:"afterData" dc:"变更后"` + RequestId string `json:"requestId" dc:"请求ID"` + IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} diff --git a/server/internal/model/input/sysin/im_invite.go b/server/internal/model/input/sysin/im_invite.go new file mode 100644 index 0000000..713e6dc --- /dev/null +++ b/server/internal/model/input/sysin/im_invite.go @@ -0,0 +1,140 @@ +package sysin + +import ( + "context" + "hotgo/internal/model/input/form" + "strconv" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImInviteListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"IM租户ID"` + InviteCode string `json:"inviteCode" dc:"完整邀请码"` + Status string `json:"status" dc:"状态"` + BatchId string `json:"batchId" dc:"批次ID"` + CreatedBy string `json:"createdBy" dc:"创建人"` + CreatedAtStart *gtime.Time `json:"createdAtStart" dc:"创建开始时间"` + CreatedAtEnd *gtime.Time `json:"createdAtEnd" dc:"创建结束时间"` + ExpiresAtStart *gtime.Time `json:"expiresAtStart" dc:"过期开始时间"` + ExpiresAtEnd *gtime.Time `json:"expiresAtEnd" dc:"过期结束时间"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"` + ExpiresAt []*gtime.Time `json:"expiresAt" dc:"到期时间"` +} + +func (in *ImInviteListInp) Filter(ctx context.Context) (err error) { + if in.PerPage <= 0 { + in.PerPage = 20 + } + if in.PerPage > 100 { + return gerror.New("每页数量最大值不能大于100") + } + if in.InviteCode != "" && len(in.InviteCode) != 5 { + return gerror.New("邀请码必须为完整5位") + } + if len(in.CreatedAt) == 2 { + in.CreatedAtStart = in.CreatedAt[0] + in.CreatedAtEnd = in.CreatedAt[1] + } + if len(in.ExpiresAt) == 2 { + in.ExpiresAtStart = in.ExpiresAt[0] + in.ExpiresAtEnd = in.ExpiresAt[1] + } + return +} + +type ImInviteCreateInp struct { + TenantId int64 `json:"tenantId" dc:"IM租户ID"` + ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"` + Remark string `json:"remark" dc:"备注"` + IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"` +} + +func (in *ImInviteCreateInp) Filter(ctx context.Context) (err error) { + if in.ExpiresAt == nil || in.ExpiresAt.Before(gtime.Now()) { + return gerror.New("到期时间必须晚于当前时间") + } + if len(in.Remark) > 255 { + return gerror.New("备注不能超过255个字符") + } + return +} + +type ImInviteBatchCreateInp struct { + TenantId int64 `json:"tenantId" dc:"IM租户ID"` + Quantity int `json:"quantity" dc:"数量"` + ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"` + Remark string `json:"remark" dc:"备注"` + IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"` +} + +func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) { + if in.Quantity < 1 || in.Quantity > 1000 { + return gerror.New("批量生成数量必须在1到1000之间") + } + if in.ExpiresAt == nil || in.ExpiresAt.Before(gtime.Now()) { + return gerror.New("到期时间必须晚于当前时间") + } + if len(in.Remark) > 255 { + return gerror.New("备注不能超过255个字符") + } + return +} + +type ImInviteBatchViewInp struct { + TenantId int64 `json:"tenantId" dc:"IM租户ID"` + BatchId string `json:"batchId" v:"required#批次ID不能为空" dc:"批次ID"` +} + +type ImInviteRevokeInp struct { + TenantId int64 `json:"tenantId" dc:"IM租户ID"` + Id string `json:"id" v:"required#邀请码ID不能为空" dc:"邀请码ID"` +} + +type ImInviteBatchRevokeInp struct { + TenantId int64 `json:"tenantId" dc:"IM租户ID"` + BatchId string `json:"batchId" v:"required#批次ID不能为空" dc:"批次ID"` +} + +type ImInviteCodeModel struct { + Id string `json:"id" dc:"ID"` + BatchId string `json:"batchId" dc:"批次ID"` + Code string `json:"code" dc:"邀请码"` + Status string `json:"status" dc:"状态"` + ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"` + UsedBy string `json:"usedBy" dc:"使用用户"` + UsedAt *gtime.Time `json:"usedAt" dc:"使用时间"` + RequestId string `json:"requestId" dc:"请求ID"` + CreatedBy string `json:"createdBy" dc:"创建人"` + Remark string `json:"remark" dc:"备注"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} + +type ImInviteBatchModel struct { + BatchId string `json:"batchId" dc:"批次ID"` + Quantity int `json:"quantity" dc:"数量"` + ActiveCount int `json:"activeCount" dc:"有效数量"` + UsedCount int `json:"usedCount" dc:"已使用数量"` + RevokedCount int `json:"revokedCount" dc:"已撤销数量"` + ExpiredCount int `json:"expiredCount" dc:"已过期数量"` + ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} + +type ImInviteBatchDetailModel struct { + ImInviteBatchModel + List []*ImInviteCodeModel `json:"list" dc:"批次邀请码"` +} + +func ParseOptionalInt64(value string) (int64, error) { + if value == "" { + return 0, nil + } + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil || parsed <= 0 { + return 0, gerror.New("ID格式不正确") + } + return parsed, nil +} diff --git a/server/internal/model/input/sysin/im_realname.go b/server/internal/model/input/sysin/im_realname.go new file mode 100644 index 0000000..d22d1cb --- /dev/null +++ b/server/internal/model/input/sysin/im_realname.go @@ -0,0 +1,96 @@ +// 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/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImRealnameListInp 获取IM实名审核列表 +type ImRealnameListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + UserId int64 `json:"userId" dc:"用户ID"` + SubmissionId string `json:"submissionId" dc:"提交ID"` + Status string `json:"status" dc:"状态"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"` +} + +func (in *ImRealnameListInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + return +} + +type ImRealnameListModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + UserId int64 `json:"userId" dc:"用户ID"` + SubmissionId string `json:"submissionId" dc:"提交ID"` + Status string `json:"status" dc:"状态"` + MaskedName string `json:"maskedName" dc:"脱敏姓名"` + MaskedIdNumber string `json:"maskedIdNumber" dc:"脱敏证件号"` + IdType string `json:"idType" dc:"证件类型"` + ReviewReason string `json:"reviewReason" dc:"审核原因"` + VerifiedAt *gtime.Time `json:"verifiedAt" dc:"通过时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +// ImRealnameViewInp 获取指定IM实名审核信息 +type ImRealnameViewInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"` +} + +func (in *ImRealnameViewInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Id <= 0 { + return gerror.New("ID不能为空") + } + return +} + +type ImRealnameViewModel struct { + ImRealnameListModel + IdNumberHash string `json:"idNumberHash" dc:"证件号哈希"` +} + +// ImRealnameReviewInp 审核IM实名 +type ImRealnameReviewInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"` + Status string `json:"status" dc:"审核状态"` + ReviewReason string `json:"reviewReason" dc:"审核原因"` +} + +func (in *ImRealnameReviewInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Id <= 0 { + return gerror.New("ID不能为空") + } + if !dict.HasOptionKey(consts.ImRealnameStatusOptions, in.Status) { + return gerror.New("审核状态不正确") + } + if in.Status == consts.ImRealnameStatusRejected && in.ReviewReason == "" { + return gerror.New("拒绝原因不能为空") + } + return +} + +type ImRealnameReviewModel struct{} diff --git a/server/internal/model/input/sysin/im_registration.go b/server/internal/model/input/sysin/im_registration.go new file mode 100644 index 0000000..f7c7449 --- /dev/null +++ b/server/internal/model/input/sysin/im_registration.go @@ -0,0 +1,12 @@ +package sysin + +import "github.com/gogf/gf/v2/os/gtime" + +type ImRegistrationConfigModel struct { + RegisterSmsEnabled bool `json:"registerSmsEnabled" dc:"注册短信是否真实发送"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ImRegistrationConfigUpdateInp struct { + RegisterSmsEnabled bool `json:"registerSmsEnabled" dc:"注册短信是否真实发送"` +} diff --git a/server/internal/model/input/sysin/im_tenant.go b/server/internal/model/input/sysin/im_tenant.go new file mode 100644 index 0000000..3b887f7 --- /dev/null +++ b/server/internal/model/input/sysin/im_tenant.go @@ -0,0 +1,157 @@ +// 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/consts" + "hotgo/internal/library/dict" + "hotgo/internal/library/hgorm/hook" + "hotgo/internal/model/entity" + "hotgo/internal/model/input/form" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImTenantUpdateFields 修改IM租户字段过滤 +type ImTenantUpdateFields struct { + Code string `json:"code" dc:"租户编码"` + Name string `json:"name" dc:"租户名称"` + TenantDesc string `json:"tenantDesc" dc:"租户描述"` + DbName string `json:"dbName" dc:"业务库名"` + DbDsn string `json:"dbDsn" dc:"独立连接串"` + State int `json:"state" dc:"状态"` + UpdatedBy int64 `json:"updatedBy" dc:"更新者"` +} + +// ImTenantInsertFields 新增IM租户字段过滤 +type ImTenantInsertFields struct { + Code string `json:"code" dc:"租户编码"` + Name string `json:"name" dc:"租户名称"` + TenantDesc string `json:"tenantDesc" dc:"租户描述"` + DbName string `json:"dbName" dc:"业务库名"` + DbDsn string `json:"dbDsn" dc:"独立连接串"` + State int `json:"state" dc:"状态"` + CreatedBy int64 `json:"createdBy" dc:"创建者"` +} + +// ImTenantConfigUpdateFields 修改IM租户配置字段过滤 +type ImTenantConfigUpdateFields struct { + MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"` + MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"` + MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"` + UpdatedBy int64 `json:"updatedBy" dc:"更新者"` +} + +// ImTenantConfigInsertFields 新增IM租户配置字段过滤 +type ImTenantConfigInsertFields struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"` + MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"` + MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"` + CreatedBy int64 `json:"createdBy" dc:"创建者"` +} + +// ImTenantEditInp 修改/新增IM租户 +type ImTenantEditInp struct { + entity.ImTenant + MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"` + MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"` + MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"` +} + +func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) { + if e := g.Validator().Rules("required|length:2,64").Data(in.Code).Messages("租户编码不能为空|租户编码长度应为2到64个字符").Run(ctx); e != nil { + return e.Current() + } + if e := g.Validator().Rules("required|max-length:128").Data(in.Name).Messages("租户名称不能为空|租户名称不能超过128个字符").Run(ctx); e != nil { + return e.Current() + } + if in.DbName == "" && in.DbDsn == "" { + return gerror.New("业务库名和独立连接串至少填写一个") + } + if !dict.HasOptionKey(consts.ImTenantStateOptions, in.State) { + return gerror.New("租户状态不正确") + } + return +} + +type ImTenantEditModel struct{} + +// ImTenantViewInp 获取指定IM租户信息 +type ImTenantViewInp struct { + Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"` +} + +func (in *ImTenantViewInp) Filter(ctx context.Context) (err error) { + return +} + +type ImTenantViewModel struct { + entity.ImTenant + MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"` + MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"` + MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"` + CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"` + UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"` + DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"` + TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"` + TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"` +} + +// ImTenantListInp 获取IM租户列表 +type ImTenantListInp struct { + form.PageReq + Id int64 `json:"id" dc:"ID"` + Code string `json:"code" dc:"租户编码"` + Name string `json:"name" dc:"租户名称"` + DbName string `json:"dbName" dc:"业务库名"` + State *int `json:"state" dc:"状态"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"` +} + +func (in *ImTenantListInp) Filter(ctx context.Context) (err error) { + return +} + +type ImTenantListModel struct { + Id int64 `json:"id" dc:"ID"` + Code string `json:"code" dc:"租户编码"` + Name string `json:"name" dc:"租户名称"` + TenantDesc string `json:"tenantDesc" dc:"租户描述"` + DbName string `json:"dbName" dc:"业务库名"` + State int `json:"state" dc:"状态"` + MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"` + MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"` + MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"` + DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"` + CreatedBy int64 `json:"createdBy" dc:"创建者"` + CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"` + UpdatedBy int64 `json:"updatedBy" dc:"更新者"` + UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +// ImTenantStatusInp 更新IM租户状态 +type ImTenantStatusInp struct { + Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"` + State int `json:"state" dc:"状态"` +} + +func (in *ImTenantStatusInp) Filter(ctx context.Context) (err error) { + if in.Id <= 0 { + return gerror.New("ID不能为空") + } + if !dict.HasOptionKey(consts.ImTenantStateOptions, in.State) { + return gerror.New("租户状态不正确") + } + return +} + +type ImTenantStatusModel struct{} diff --git a/server/internal/model/input/sysin/im_user.go b/server/internal/model/input/sysin/im_user.go new file mode 100644 index 0000000..9cac3b6 --- /dev/null +++ b/server/internal/model/input/sysin/im_user.go @@ -0,0 +1,80 @@ +// 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" +) + +// ImUserListInp 获取IM用户列表 +type ImUserListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + UserId int64 `json:"userId" dc:"用户ID"` + Nickname string `json:"nickname" dc:"昵称"` + AuthName string `json:"authName" dc:"登录标识"` + State *int `json:"state" dc:"账号状态"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"` + LastSeen []*gtime.Time `json:"lastSeen" dc:"最后在线时间"` +} + +func (in *ImUserListInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + return +} + +type ImUserListModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + UserId int64 `json:"userId" dc:"用户ID"` + Nickname string `json:"nickname" dc:"昵称"` + PublicAccount string `json:"publicAccount" dc:"公开账号"` + Gender string `json:"gender" dc:"性别"` + Region string `json:"region" dc:"地区"` + State int `json:"state" dc:"账号状态"` + AuthVersion int `json:"authVersion" dc:"认证版本"` + PasswordUpdatedAt *gtime.Time `json:"passwordUpdatedAt" dc:"密码更新时间"` + AuthNames string `json:"authNames" dc:"登录标识"` + Credentials string `json:"credentials" dc:"凭证"` + LastSeen *gtime.Time `json:"lastSeen" dc:"最后在线时间"` + UserAgent string `json:"userAgent" dc:"客户端"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +// ImUserViewInp 获取指定IM用户信息 +type ImUserViewInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + UserId int64 `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"` +} + +func (in *ImUserViewInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.UserId <= 0 { + return gerror.New("用户ID不能为空") + } + return +} + +type ImUserViewModel struct { + ImUserListModel + Signature string `json:"signature" dc:"个性签名"` + AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"` + BirthDate string `json:"birthDate" dc:"出生日期"` + ProfileJson string `json:"profileJson" dc:"Tinode Public资料"` + TrustedJson string `json:"trustedJson" dc:"Tinode Trusted资料"` + TagsJson string `json:"tagsJson" dc:"Tinode标签"` + AccessJson string `json:"accessJson" dc:"访问权限"` + ProfileVersion int `json:"profileVersion" dc:"资料版本"` +} diff --git a/server/internal/model/input/sysin/im_work_order.go b/server/internal/model/input/sysin/im_work_order.go new file mode 100644 index 0000000..c6d1fe9 --- /dev/null +++ b/server/internal/model/input/sysin/im_work_order.go @@ -0,0 +1,118 @@ +// 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/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +const ( + ImWorkOrderTypeAbuse = "abuse" + ImWorkOrderTypeFeedback = "feedback" +) + +// ImWorkOrderListInp 获取IM举报反馈工单列表 +type ImWorkOrderListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + Type string `json:"type" dc:"工单类型"` + UserId int64 `json:"userId" dc:"提交人ID"` + Status string `json:"status" dc:"状态"` + Keyword string `json:"keyword" dc:"关键词"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"` +} + +func (in *ImWorkOrderListInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Type == "" { + return gerror.New("请选择工单类型") + } + if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback { + return gerror.New("工单类型不正确") + } + return +} + +type ImWorkOrderListModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Type string `json:"type" dc:"工单类型"` + Id int64 `json:"id" dc:"ID"` + OrderId string `json:"orderId" dc:"工单ID"` + UserId int64 `json:"userId" dc:"提交人ID"` + TargetType string `json:"targetType" dc:"目标类型"` + TargetSummary string `json:"targetSummary" dc:"目标摘要"` + Category string `json:"category" dc:"分类"` + Content string `json:"content" dc:"内容"` + Status string `json:"status" dc:"状态"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"` +} + +// ImWorkOrderViewInp 获取指定IM举报反馈工单信息 +type ImWorkOrderViewInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Type string `json:"type" v:"required#工单类型不能为空" dc:"工单类型"` + Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"` +} + +func (in *ImWorkOrderViewInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback { + return gerror.New("工单类型不正确") + } + if in.Id <= 0 { + return gerror.New("ID不能为空") + } + return +} + +type ImWorkOrderViewModel struct { + ImWorkOrderListModel + Description string `json:"description" dc:"描述"` + Contact string `json:"contact" dc:"联系方式"` + EvidenceJson string `json:"evidenceJson" dc:"证据/附件"` + SnapshotJson string `json:"snapshotJson" dc:"快照"` + ClientContextJson string `json:"clientContextJson" dc:"客户端上下文"` + IncludeDiagnostics int `json:"includeDiagnostics" dc:"是否包含诊断"` + IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"` +} + +// ImWorkOrderStatusInp 更新IM举报反馈工单状态 +type ImWorkOrderStatusInp struct { + TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Type string `json:"type" v:"required#工单类型不能为空" dc:"工单类型"` + Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"` + Status string `json:"status" dc:"状态"` +} + +func (in *ImWorkOrderStatusInp) Filter(ctx context.Context) (err error) { + if in.TenantId <= 0 { + return gerror.New("请选择租户") + } + if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback { + return gerror.New("工单类型不正确") + } + if in.Id <= 0 { + return gerror.New("ID不能为空") + } + if !dict.HasOptionKey(consts.ImWorkOrderStatusOptions, in.Status) { + return gerror.New("工单状态不正确") + } + return +} + +type ImWorkOrderStatusModel struct{} diff --git a/server/internal/router/genrouter/im_audit_log.go b/server/internal/router/genrouter/im_audit_log.go new file mode 100644 index 0000000..a0d04f3 --- /dev/null +++ b/server/internal/router/genrouter/im_audit_log.go @@ -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.ImAuditLog) // IM审计日志 +} diff --git a/server/internal/router/genrouter/im_group.go b/server/internal/router/genrouter/im_group.go new file mode 100644 index 0000000..23ec785 --- /dev/null +++ b/server/internal/router/genrouter/im_group.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImGroup) // IM群组 +} diff --git a/server/internal/router/genrouter/im_invite.go b/server/internal/router/genrouter/im_invite.go new file mode 100644 index 0000000..165faa7 --- /dev/null +++ b/server/internal/router/genrouter/im_invite.go @@ -0,0 +1,8 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImInvite) // IM注册邀请码 +} + diff --git a/server/internal/router/genrouter/im_realname.go b/server/internal/router/genrouter/im_realname.go new file mode 100644 index 0000000..7d2299d --- /dev/null +++ b/server/internal/router/genrouter/im_realname.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImRealname) // IM实名审核 +} diff --git a/server/internal/router/genrouter/im_registration.go b/server/internal/router/genrouter/im_registration.go new file mode 100644 index 0000000..a6a0c26 --- /dev/null +++ b/server/internal/router/genrouter/im_registration.go @@ -0,0 +1,8 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImRegistration) // IM注册配置 +} + diff --git a/server/internal/router/genrouter/im_tenant.go b/server/internal/router/genrouter/im_tenant.go new file mode 100644 index 0000000..d6a76d7 --- /dev/null +++ b/server/internal/router/genrouter/im_tenant.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImTenant) // IM租户 +} diff --git a/server/internal/router/genrouter/im_user.go b/server/internal/router/genrouter/im_user.go new file mode 100644 index 0000000..14174da --- /dev/null +++ b/server/internal/router/genrouter/im_user.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImUser) // IM用户 +} diff --git a/server/internal/router/genrouter/im_work_order.go b/server/internal/router/genrouter/im_work_order.go new file mode 100644 index 0000000..099905f --- /dev/null +++ b/server/internal/router/genrouter/im_work_order.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImWorkOrder) // IM举报反馈 +} diff --git a/server/internal/service/sys.go b/server/internal/service/sys.go index 1b94874..2c7b9d4 100644 --- a/server/internal/service/sys.go +++ b/server/internal/service/sys.go @@ -171,6 +171,88 @@ type ( // Switch 更新CURD列表开关 Switch(ctx context.Context, in *sysin.CurdDemoSwitchInp) (err error) } + ISysImTenant interface { + // Model IM租户ORM模型 + Model(ctx context.Context, option ...*handler.Option) *gdb.Model + // List 获取IM租户列表 + List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error) + // Edit 修改/新增IM租户 + Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) + // View 获取IM租户指定信息 + View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error) + // Status 更新IM租户状态 + Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error) + } + ISysImRegistration interface { + // GetConfig 获取IM注册配置 + GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error) + // UpdateConfig 更新IM注册配置 + UpdateConfig(ctx context.Context, in *sysin.ImRegistrationConfigUpdateInp) (err error) + } + ISysImInvite interface { + // List 查询IM注册邀请码 + List(ctx context.Context, in *sysin.ImInviteListInp) (list []*sysin.ImInviteCodeModel, totalCount int, err error) + // Create 单个生成IM注册邀请码 + Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error) + // BatchCreate 批量生成IM注册邀请码 + BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error) + // BatchView 查看批次详情 + BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error) + // Revoke 撤销邀请码 + Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error) + // BatchRevoke 批量撤销邀请码 + BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error) + // Export 导出邀请码 + Export(ctx context.Context, in *sysin.ImInviteListInp) (err error) + } + ISysImUser interface { + // List 获取IM用户列表 + List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error) + // View 获取IM用户指定信息 + View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error) + } + ISysImGroup interface { + // List 获取IM群组列表 + List(ctx context.Context, in *sysin.ImGroupListInp) (list []*sysin.ImGroupListModel, totalCount int, err error) + // View 获取IM群组指定信息 + View(ctx context.Context, in *sysin.ImGroupViewInp) (res *sysin.ImGroupViewModel, err error) + // MemberList 获取IM群成员列表 + MemberList(ctx context.Context, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) + // Governance 获取IM群治理配置 + Governance(ctx context.Context, in *sysin.ImGroupTopicInp) (res *sysin.ImGroupGovernanceModel, err error) + // JoinRequestList 获取IM入群申请列表 + JoinRequestList(ctx context.Context, in *sysin.ImGroupJoinRequestListInp) (list []*sysin.ImGroupJoinRequestListModel, totalCount int, err error) + // InvitationList 获取IM群邀请列表 + InvitationList(ctx context.Context, in *sysin.ImGroupInvitationListInp) (list []*sysin.ImGroupInvitationListModel, totalCount int, err error) + // AnnouncementList 获取IM群公告列表 + AnnouncementList(ctx context.Context, in *sysin.ImGroupAnnouncementListInp) (list []*sysin.ImGroupAnnouncementListModel, totalCount int, err error) + // MuteList 获取IM群禁言列表 + MuteList(ctx context.Context, in *sysin.ImGroupMuteListInp) (list []*sysin.ImGroupMuteListModel, totalCount int, err error) + // AdminLogList 获取IM群管理日志列表 + AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLogListInp) (list []*sysin.ImGroupAdminLogListModel, totalCount int, err error) + } + ISysImRealname interface { + // List 获取IM实名审核列表 + List(ctx context.Context, in *sysin.ImRealnameListInp) (list []*sysin.ImRealnameListModel, totalCount int, err error) + // View 获取IM实名审核指定信息 + View(ctx context.Context, in *sysin.ImRealnameViewInp) (res *sysin.ImRealnameViewModel, err error) + // Review 审核IM实名 + Review(ctx context.Context, in *sysin.ImRealnameReviewInp) (err error) + } + ISysImWorkOrder interface { + // List 获取IM举报反馈工单列表 + List(ctx context.Context, in *sysin.ImWorkOrderListInp) (list []*sysin.ImWorkOrderListModel, totalCount int, err error) + // View 获取IM举报反馈工单指定信息 + View(ctx context.Context, in *sysin.ImWorkOrderViewInp) (res *sysin.ImWorkOrderViewModel, err error) + // Status 更新IM举报反馈工单状态 + Status(ctx context.Context, in *sysin.ImWorkOrderStatusInp) (err error) + } + ISysImAuditLog interface { + // List 获取IM审计日志列表 + List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error) + // View 获取IM审计日志指定信息 + View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error) + } ISysDictData interface { // Delete 删除 Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error @@ -414,6 +496,14 @@ var ( localSysCron ISysCron localSysCronGroup ISysCronGroup localSysCurdDemo ISysCurdDemo + localSysImTenant ISysImTenant + localSysImRegistration ISysImRegistration + localSysImInvite ISysImInvite + localSysImUser ISysImUser + localSysImGroup ISysImGroup + localSysImRealname ISysImRealname + localSysImWorkOrder ISysImWorkOrder + localSysImAuditLog ISysImAuditLog localSysDictData ISysDictData localSysDictType ISysDictType localSysEmsLog ISysEmsLog @@ -517,6 +607,94 @@ func RegisterSysCurdDemo(i ISysCurdDemo) { localSysCurdDemo = i } +func SysImTenant() ISysImTenant { + if localSysImTenant == nil { + panic("implement not found for interface ISysImTenant, forgot register?") + } + return localSysImTenant +} + +func RegisterSysImTenant(i ISysImTenant) { + localSysImTenant = i +} + +func SysImRegistration() ISysImRegistration { + if localSysImRegistration == nil { + panic("implement not found for interface ISysImRegistration, forgot register?") + } + return localSysImRegistration +} + +func RegisterSysImRegistration(i ISysImRegistration) { + localSysImRegistration = i +} + +func SysImInvite() ISysImInvite { + if localSysImInvite == nil { + panic("implement not found for interface ISysImInvite, forgot register?") + } + return localSysImInvite +} + +func RegisterSysImInvite(i ISysImInvite) { + localSysImInvite = i +} + +func SysImUser() ISysImUser { + if localSysImUser == nil { + panic("implement not found for interface ISysImUser, forgot register?") + } + return localSysImUser +} + +func RegisterSysImUser(i ISysImUser) { + localSysImUser = i +} + +func SysImGroup() ISysImGroup { + if localSysImGroup == nil { + panic("implement not found for interface ISysImGroup, forgot register?") + } + return localSysImGroup +} + +func RegisterSysImGroup(i ISysImGroup) { + localSysImGroup = i +} + +func SysImRealname() ISysImRealname { + if localSysImRealname == nil { + panic("implement not found for interface ISysImRealname, forgot register?") + } + return localSysImRealname +} + +func RegisterSysImRealname(i ISysImRealname) { + localSysImRealname = i +} + +func SysImWorkOrder() ISysImWorkOrder { + if localSysImWorkOrder == nil { + panic("implement not found for interface ISysImWorkOrder, forgot register?") + } + return localSysImWorkOrder +} + +func RegisterSysImWorkOrder(i ISysImWorkOrder) { + localSysImWorkOrder = i +} + +func SysImAuditLog() ISysImAuditLog { + if localSysImAuditLog == nil { + panic("implement not found for interface ISysImAuditLog, forgot register?") + } + return localSysImAuditLog +} + +func RegisterSysImAuditLog(i ISysImAuditLog) { + localSysImAuditLog = i +} + func SysDictData() ISysDictData { if localSysDictData == nil { panic("implement not found for interface ISysDictData, forgot register?") diff --git a/server/main.go b/server/main.go index 22299b7..405dfd6 100644 --- a/server/main.go +++ b/server/main.go @@ -7,6 +7,7 @@ package main import ( _ "hotgo/internal/packed" + _ "time/tzdata" _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" diff --git a/server/manifest/config/config.example.yaml b/server/manifest/config/config.example.yaml index b965641..9a493fd 100644 --- a/server/manifest/config/config.example.yaml +++ b/server/manifest/config/config.example.yaml @@ -237,6 +237,9 @@ database: link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4" debug: true Prefix: "hg_" + imRegistry: + link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4" + debug: true # 链路追踪 @@ -300,4 +303,10 @@ hggen: addon: srcPath: "./resource/generate/default/addon" # 生成模板路径 webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径 - webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径 \ No newline at end of file + webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径 + +imAdminApi: + baseUrl: "http://127.0.0.1:6060" + serviceId: "hotgo-admin" + serviceSecret: "${IM_ADMIN_API_SECRET}" + timeout: "5s" diff --git a/server/manifest/docker/Dockerfile.local b/server/manifest/docker/Dockerfile.local new file mode 100644 index 0000000..d81f6bf --- /dev/null +++ b/server/manifest/docker/Dockerfile.local @@ -0,0 +1,15 @@ +FROM alpine:3.20 + +WORKDIR /app + +RUN apk add --no-cache ca-certificates tzdata + +COPY temp/linux_arm64/hotgo /app/hotgo +COPY hack /app/hack +COPY manifest/config /app/manifest/config +COPY resource /app/resource +COPY storage /app/storage + +EXPOSE 8000 + +CMD ["/app/hotgo"] diff --git a/server/storage/data/generate/im_audit_log_menu.sql b/server/storage/data/generate/im_audit_log_menu.sql new file mode 100644 index 0000000..fcc6e8b --- /dev/null +++ b/server/storage/data/generate/im_audit_log_menu.sql @@ -0,0 +1,31 @@ +SET NAMES utf8mb4; + +SET @now = NOW(); + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now +WHERE @imRootId IS NULL; + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +SELECT @auditDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLog' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '审计日志', 'imAuditLog', 'audit-log', '', '1', '/im/audit-log/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '60', '', '1', @now, @now +WHERE @auditDirId IS NULL; + +SELECT @auditDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLog' LIMIT 1; + +SELECT @auditListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @auditDirId, '审计列表', 'imAuditLogIndex', 'index', '', '2', '', '/imAuditLog/list', '', '/imAuditLog/index', '1', 'imAuditLog', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' '), '10', '', '1', @now, @now +WHERE @auditListId IS NULL; + +SELECT @auditListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @auditListId, '审计详情', 'imAuditLogView', '', '', '3', '', '/imAuditLog/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' tr_', @auditListId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAuditLogView'); diff --git a/server/storage/data/generate/im_audit_logs.sql b/server/storage/data/generate/im_audit_logs.sql new file mode 100644 index 0000000..399e06a --- /dev/null +++ b/server/storage/data/generate/im_audit_logs.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS im_audit_logs ( + id BIGINT NOT NULL AUTO_INCREMENT, + actor_uid BIGINT NOT NULL DEFAULT 0, + actor_role VARCHAR(64) NOT NULL DEFAULT '', + action VARCHAR(128) NOT NULL, + target_type VARCHAR(64) NOT NULL DEFAULT '', + target_id VARCHAR(128) NOT NULL DEFAULT '', + before_snapshot JSON, + after_snapshot JSON, + reason VARCHAR(512) NOT NULL DEFAULT '', + request_id VARCHAR(64) NOT NULL DEFAULT '', + ip VARCHAR(64) NOT NULL DEFAULT '', + user_agent VARCHAR(512) NOT NULL DEFAULT '', + metadata JSON, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + INDEX idx_im_audit_logs_actor (actor_uid, id DESC), + INDEX idx_im_audit_logs_action (action, id DESC), + INDEX idx_im_audit_logs_target (target_type, target_id, id DESC), + INDEX idx_im_audit_logs_request (request_id), + INDEX idx_im_audit_logs_created (created_at, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM租户审计日志'; diff --git a/server/storage/data/generate/im_group_menu.sql b/server/storage/data/generate/im_group_menu.sql new file mode 100644 index 0000000..74af8da --- /dev/null +++ b/server/storage/data/generate/im_group_menu.sql @@ -0,0 +1,59 @@ +SET NAMES utf8mb4; + +SET @now = NOW(); + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now +WHERE @imRootId IS NULL; + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +SELECT @groupDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroup' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '群组管理', 'imGroup', 'group', '', '1', '/im/group/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '30', '', '1', @now, @now +WHERE @groupDirId IS NULL; + +SELECT @groupDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroup' LIMIT 1; + +SELECT @groupListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroupIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupDirId, '群组列表', 'imGroupIndex', 'index', '', '2', '', '/imGroup/list', '', '/imGroup/index', '1', 'imGroup', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' '), '10', '', '1', @now, @now +WHERE @groupListId IS NULL; + +SELECT @groupListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroupIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '群组详情', 'imGroupView', '', '', '3', '', '/imGroup/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupView'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '群成员列表', 'imGroupMemberList', '', '', '3', '', '/imGroup/memberList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '20', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMemberList'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '群治理配置', 'imGroupGovernance', '', '', '3', '', '/imGroup/governance', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '30', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupGovernance'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '入群申请列表', 'imGroupJoinRequestList', '', '', '3', '', '/imGroup/joinRequestList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '40', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupJoinRequestList'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '群邀请列表', 'imGroupInvitationList', '', '', '3', '', '/imGroup/invitationList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '50', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupInvitationList'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '群公告列表', 'imGroupAnnouncementList', '', '', '3', '', '/imGroup/announcementList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '60', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupAnnouncementList'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '群禁言列表', 'imGroupMuteList', '', '', '3', '', '/imGroup/muteList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '70', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMuteList'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @groupListId, '群管理日志', 'imGroupAdminLogList', '', '', '3', '', '/imGroup/adminLogList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '80', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupAdminLogList'); diff --git a/server/storage/data/generate/im_realname_menu.sql b/server/storage/data/generate/im_realname_menu.sql new file mode 100644 index 0000000..64a7933 --- /dev/null +++ b/server/storage/data/generate/im_realname_menu.sql @@ -0,0 +1,35 @@ +SET NAMES utf8mb4; + +SET @now = NOW(); + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now +WHERE @imRootId IS NULL; + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +SELECT @realnameDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealname' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '实名审核', 'imRealname', 'realname', '', '1', '/im/realname/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '40', '', '1', @now, @now +WHERE @realnameDirId IS NULL; + +SELECT @realnameDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealname' LIMIT 1; + +SELECT @realnameListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealnameIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @realnameDirId, '审核列表', 'imRealnameIndex', 'index', '', '2', '', '/imRealname/list', '', '/imRealname/index', '1', 'imRealname', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' '), '10', '', '1', @now, @now +WHERE @realnameListId IS NULL; + +SELECT @realnameListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealnameIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @realnameListId, '实名详情', 'imRealnameView', '', '', '3', '', '/imRealname/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' tr_', @realnameListId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imRealnameView'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @realnameListId, '实名审核操作', 'imRealnameReview', '', '', '3', '', '/imRealname/review', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' tr_', @realnameListId, ' '), '20', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imRealnameReview'); diff --git a/server/storage/data/generate/im_registration_invite_menu.sql b/server/storage/data/generate/im_registration_invite_menu.sql new file mode 100644 index 0000000..eb74f6a --- /dev/null +++ b/server/storage/data/generate/im_registration_invite_menu.sql @@ -0,0 +1,43 @@ +SET NAMES utf8mb4; + +SET @now = NOW(); + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now +WHERE @imRootId IS NULL; + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +SELECT @invitePageId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRegistrationInviteIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '注册邀请码', 'imRegistrationInviteIndex', 'registrationInvite', '', '2', '', '/imInvite/list', '', '/im/registrationInvite/index', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '70', '', '1', @now, @now +WHERE @invitePageId IS NULL; + +SELECT @invitePageId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRegistrationInviteIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @invitePageId, '生成邀请码', 'imInviteCreate', '', '', '3', '', '/imInvite/create', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteCreate'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @invitePageId, '批量生成邀请码', 'imInviteBatchCreate', '', '', '3', '', '/imInvite/batchCreate', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '20', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteBatchCreate'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @invitePageId, '批次详情', 'imInviteBatchView', '', '', '3', '', '/imInvite/batchView', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '30', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteBatchView'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @invitePageId, '撤销邀请码', 'imInviteRevoke', '', '', '3', '', '/imInvite/revoke', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '40', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteRevoke'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @invitePageId, '批量撤销邀请码', 'imInviteBatchRevoke', '', '', '3', '', '/imInvite/batchRevoke', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '50', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteBatchRevoke'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @invitePageId, '导出邀请码', 'imInviteExport', '', '', '3', '', '/imInvite/export', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '60', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteExport'); diff --git a/server/storage/data/generate/im_tenant_menu.sql b/server/storage/data/generate/im_tenant_menu.sql new file mode 100644 index 0000000..d748a02 --- /dev/null +++ b/server/storage/data/generate/im_tenant_menu.sql @@ -0,0 +1,39 @@ +SET NAMES utf8mb4; + +SET @now = NOW(); + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now +WHERE @imRootId IS NULL; + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +SELECT @tenantDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenant' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '租户管理', 'imTenant', 'tenant', '', '1', '/im/tenant/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '10', '', '1', @now, @now +WHERE @tenantDirId IS NULL; + +SELECT @tenantDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenant' LIMIT 1; + +SELECT @tenantListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenantIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @tenantDirId, '租户列表', 'imTenantIndex', 'index', '', '2', '', '/imTenant/list', '', '/imTenant/index', '1', 'imTenant', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' '), '10', '', '1', @now, @now +WHERE @tenantListId IS NULL; + +SELECT @tenantListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenantIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @tenantListId, '租户详情', 'imTenantView', '', '', '3', '', '/imTenant/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantView'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @tenantListId, '编辑/新增租户', 'imTenantEdit', '', '', '3', '', '/imTenant/edit', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '20', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantEdit'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @tenantListId, '修改租户状态', 'imTenantStatus', '', '', '3', '', '/imTenant/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '30', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantStatus'); diff --git a/server/storage/data/generate/im_user_menu.sql b/server/storage/data/generate/im_user_menu.sql new file mode 100644 index 0000000..d544db8 --- /dev/null +++ b/server/storage/data/generate/im_user_menu.sql @@ -0,0 +1,31 @@ +SET NAMES utf8mb4; + +SET @now = NOW(); + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now +WHERE @imRootId IS NULL; + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +SELECT @userDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUser' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '用户管理', 'imUser', 'user', '', '1', '/im/user/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '20', '', '1', @now, @now +WHERE @userDirId IS NULL; + +SELECT @userDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUser' LIMIT 1; + +SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @userDirId, '用户列表', 'imUserIndex', 'index', '', '2', '', '/imUser/list', '', '/imUser/index', '1', 'imUser', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' '), '10', '', '1', @now, @now +WHERE @userListId IS NULL; + +SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @userListId, '用户详情', 'imUserView', '', '', '3', '', '/imUser/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserView'); diff --git a/server/storage/data/generate/im_work_order_menu.sql b/server/storage/data/generate/im_work_order_menu.sql new file mode 100644 index 0000000..0e408d1 --- /dev/null +++ b/server/storage/data/generate/im_work_order_menu.sql @@ -0,0 +1,35 @@ +SET NAMES utf8mb4; + +SET @now = NOW(); + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now +WHERE @imRootId IS NULL; + +SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1; + +SELECT @workOrderDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrder' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '举报反馈', 'imWorkOrder', 'work-order', '', '1', '/im/work-order/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '50', '', '1', @now, @now +WHERE @workOrderDirId IS NULL; + +SELECT @workOrderDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrder' LIMIT 1; + +SELECT @workOrderListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @workOrderDirId, '工单列表', 'imWorkOrderIndex', 'index', '', '2', '', '/imWorkOrder/list', '', '/imWorkOrder/index', '1', 'imWorkOrder', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' '), '10', '', '1', @now, @now +WHERE @workOrderListId IS NULL; + +SELECT @workOrderListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderIndex' LIMIT 1; + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @workOrderListId, '工单详情', 'imWorkOrderView', '', '', '3', '', '/imWorkOrder/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' tr_', @workOrderListId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderView'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) +SELECT NULL, @workOrderListId, '更新工单状态', 'imWorkOrderStatus', '', '', '3', '', '/imWorkOrder/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' tr_', @workOrderListId, ' '), '20', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderStatus'); diff --git a/server/storage/data/hotgo-pg.sql b/server/storage/data/hotgo-pg.sql index bf34441..955a959 100644 --- a/server/storage/data/hotgo-pg.sql +++ b/server/storage/data/hotgo-pg.sql @@ -223,6 +223,7 @@ COMMENT ON COLUMN hg_admin_dept.updated_at IS '更新时间'; CREATE TABLE IF NOT EXISTS hg_admin_member ( id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT, dept_id BIGINT DEFAULT 0, role_id BIGINT DEFAULT 10, real_name VARCHAR(32) DEFAULT '',