diff --git a/docs/backstage/IM-未实现增强项.md b/docs/backstage/IM-未实现增强项.md index 47b1e70..0bd3f7c 100644 --- a/docs/backstage/IM-未实现增强项.md +++ b/docs/backstage/IM-未实现增强项.md @@ -1,6 +1,6 @@ # IM 未实现增强项 -> 更新时间:2026-08-21 +> 更新时间:2026-08-22 > 范围:HotGo IM 后台、IM 服务端配合项、客户端动态配置来源 本文用于记录当前后台已识别但尚未完成的增强项,作为后续开发排期和验收依据。当前多租户方案为“一个 HotGo 控制库 + 每租户一个独立 IM 业务库”,租户业务表不带 `tenant_id`,后台必须通过登录态解析租户并选择租户库。 @@ -254,15 +254,99 @@ ## 后续 P2 -1. 工作台模块配置。 -2. 会议。 -3. 日报/月报。 -4. 总经理信箱。 -5. App 版本更新策略。 -6. 青少年模式。 -7. 反诈与内容审核策略。 -8. 举报/反馈原因配置、SLA、合并、导出。 -9. 实名审核策略配置。 +### P2-1 工作台模块配置 + +状态:已完成基础后台闭环。 + +已实现: + +1. 租户工作台模块列表、搜索。 +2. 模块新增、编辑、启用/禁用。 +3. 模块排序接口和后台入口。 +4. 默认初始化组织架构、会议通知、日报、月报、总经理信箱五个入口。 +5. 路由选项对齐 IM 服务端当前白名单,避免配置后客户端接口丢弃。 +6. 工作台配置变更写入 IM 后台审计。 + +待 IM 服务端/客户端配合确认: + +1. `GET /v1/im/workbench` 当前读取 `im_workbench_apps` 已具备,需在实际环境确认租户库连接与后台修改后的缓存刷新策略。 +2. 用户或角色级工作台权限表 `im_workbench_user_apps` 尚未接入后台,当前只覆盖租户级模块配置。 +3. 未读角标实时事件 `workbench.badge.updated` 的聚合和增量推送仍由 IM 服务端承接。 +4. 新增自定义入口若超出当前五个路由,需要先扩展 IM 服务端路由白名单和客户端页面。 + +### P2-2 App 版本更新策略 + +状态:已完成基础后台闭环。 + +已实现: + +1. 租户级客户端版本策略列表、搜索。 +2. 按 `platform + channel` 配置最新版本、最低支持版本、更新类型、商店地址、安装包地址、校验值和再次提醒时间。 +3. 默认初始化 iOS/Android stable 策略。 +4. 配置变更写入 IM 后台审计。 + +待 IM 服务端/客户端配合确认: + +1. `GET /v1/im/config/app-version` 已有 IM 服务端读取链路,需确认实际环境租户库数据和客户端版本比较规则。 +2. 灰度比例、分渠道灰度名单、强更弹窗样式尚未建模。 + +### P2-3 举报原因配置 + +状态:已完成基础后台闭环。 + +已实现: + +1. 按用户、群组、消息三类对象配置举报原因。 +2. 支持原因新增、编辑、禁用、排序、是否要求描述、是否允许上传证据。 +3. 默认初始化诈骗、骚扰、垃圾广告、违法违规等基础原因。 +4. 配置变更写入 IM 后台审计。 + +待实现/配合: + +1. 举报工单 SLA、合并策略、批量导出仍未实现。 +2. 举报工单处置结果与客户端通知联动仍由后续工单模块增强。 + +### P2-4 总经理信箱配置 + +状态:已完成基础后台闭环。 + +已实现: + +1. 租户级总经理信箱启用/禁用。 +2. 匿名提交开关。 +3. 必填字段、最大字数、附件策略配置。 +4. 告知标题和告知内容配置,更新时同步刷新 `notice_updated_at`。 +5. 配置变更写入 IM 后台审计。 + +待实现/配合: + +1. 信箱提交列表、处理状态、回复闭环尚未实现。 +2. 受理人/部门、SLA、匿名身份展示范围尚未建模。 + +### P2-5 意见反馈配置 + +状态:已完成基础后台闭环。 + +已实现: + +1. 租户级意见反馈分类配置。 +2. 最大字数、附件策略、联系方式开关、诊断信息提示配置。 +3. 配置变更写入 IM 后台审计。 + +待实现/配合: + +1. 反馈提交列表、合并、回复、导出尚未实现。 +2. 客户端分类文案如需多语言,需要扩展分类结构。 + +### P2 待实现 + +1. 会议。 +2. 日报/月报。 +3. 总经理信箱提交处理闭环。 +4. 青少年模式。 +5. 反诈与内容审核策略。 +6. 举报/反馈 SLA、合并、导出。 +7. 实名审核策略配置。 ## 验收提示 diff --git a/server/api/admin/imabusereason/im_abuse_reason.go b/server/api/admin/imabusereason/im_abuse_reason.go new file mode 100644 index 0000000..f82997c --- /dev/null +++ b/server/api/admin/imabusereason/im_abuse_reason.go @@ -0,0 +1,32 @@ +package imabusereason + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imAbuseReason/list" method:"get" tags:"IM举报原因" summary:"获取IM举报原因列表"` + sysin.ImAbuseReasonListInp +} + +type ListRes struct { + form.PageRes + List []*sysin.ImAbuseReasonModel `json:"list" dc:"数据列表"` +} + +type EditReq struct { + g.Meta `path:"/imAbuseReason/edit" method:"post" tags:"IM举报原因" summary:"新增/编辑IM举报原因"` + sysin.ImAbuseReasonEditInp +} + +type EditRes struct{} + +type DeleteReq struct { + g.Meta `path:"/imAbuseReason/delete" method:"post" tags:"IM举报原因" summary:"禁用IM举报原因"` + sysin.ImAbuseReasonDeleteInp +} + +type DeleteRes struct{} diff --git a/server/api/admin/imapprelease/im_app_release.go b/server/api/admin/imapprelease/im_app_release.go new file mode 100644 index 0000000..65b63c6 --- /dev/null +++ b/server/api/admin/imapprelease/im_app_release.go @@ -0,0 +1,25 @@ +package imapprelease + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imAppRelease/list" method:"get" tags:"IM版本更新" summary:"获取IM客户端版本策略列表"` + sysin.ImAppReleaseListInp +} + +type ListRes struct { + form.PageRes + List []*sysin.ImAppReleaseModel `json:"list" dc:"数据列表"` +} + +type EditReq struct { + g.Meta `path:"/imAppRelease/edit" method:"post" tags:"IM版本更新" summary:"新增/编辑IM客户端版本策略"` + sysin.ImAppReleaseEditInp +} + +type EditRes struct{} diff --git a/server/api/admin/imfeedbackconfig/im_feedback_config.go b/server/api/admin/imfeedbackconfig/im_feedback_config.go new file mode 100644 index 0000000..1474e0a --- /dev/null +++ b/server/api/admin/imfeedbackconfig/im_feedback_config.go @@ -0,0 +1,23 @@ +package imfeedbackconfig + +import ( + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type GetReq struct { + g.Meta `path:"/imFeedbackConfig/get" method:"get" tags:"IM意见反馈配置" summary:"获取IM意见反馈配置"` + sysin.ImFeedbackConfigGetInp +} + +type GetRes struct { + *sysin.ImFeedbackConfigModel +} + +type UpdateReq struct { + g.Meta `path:"/imFeedbackConfig/update" method:"post" tags:"IM意见反馈配置" summary:"更新IM意见反馈配置"` + sysin.ImFeedbackConfigUpdateInp +} + +type UpdateRes struct{} diff --git a/server/api/admin/immailboxconfig/im_mailbox_config.go b/server/api/admin/immailboxconfig/im_mailbox_config.go new file mode 100644 index 0000000..07831eb --- /dev/null +++ b/server/api/admin/immailboxconfig/im_mailbox_config.go @@ -0,0 +1,23 @@ +package immailboxconfig + +import ( + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type GetReq struct { + g.Meta `path:"/imMailboxConfig/get" method:"get" tags:"IM总经理信箱配置" summary:"获取IM总经理信箱配置"` + sysin.ImMailboxConfigGetInp +} + +type GetRes struct { + *sysin.ImMailboxConfigModel +} + +type UpdateReq struct { + g.Meta `path:"/imMailboxConfig/update" method:"post" tags:"IM总经理信箱配置" summary:"更新IM总经理信箱配置"` + sysin.ImMailboxConfigUpdateInp +} + +type UpdateRes struct{} diff --git a/server/api/admin/imworkbench/im_workbench.go b/server/api/admin/imworkbench/im_workbench.go new file mode 100644 index 0000000..3e6db33 --- /dev/null +++ b/server/api/admin/imworkbench/im_workbench.go @@ -0,0 +1,39 @@ +package imworkbench + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imWorkbench/list" method:"get" tags:"IM工作台" summary:"获取IM工作台模块列表"` + sysin.ImWorkbenchAppListInp +} + +type ListRes struct { + form.PageRes + List []*sysin.ImWorkbenchAppModel `json:"list" dc:"数据列表"` +} + +type EditReq struct { + g.Meta `path:"/imWorkbench/edit" method:"post" tags:"IM工作台" summary:"新增/编辑IM工作台模块"` + sysin.ImWorkbenchAppEditInp +} + +type EditRes struct{} + +type DeleteReq struct { + g.Meta `path:"/imWorkbench/delete" method:"post" tags:"IM工作台" summary:"禁用IM工作台模块"` + sysin.ImWorkbenchAppDeleteInp +} + +type DeleteRes struct{} + +type ReorderReq struct { + g.Meta `path:"/imWorkbench/reorder" method:"post" tags:"IM工作台" summary:"调整IM工作台模块排序"` + sysin.ImWorkbenchAppReorderInp +} + +type ReorderRes struct{} diff --git a/server/internal/consts/im_product_config.go b/server/internal/consts/im_product_config.go new file mode 100644 index 0000000..a8503ec --- /dev/null +++ b/server/internal/consts/im_product_config.go @@ -0,0 +1,52 @@ +package consts + +import ( + "hotgo/internal/library/dict" + "hotgo/internal/model" +) + +func init() { + dict.RegisterEnums("ImAppPlatformOptions", "IM客户端平台选项", ImAppPlatformOptions) + dict.RegisterEnums("ImAppUpdateTypeOptions", "IM客户端更新类型选项", ImAppUpdateTypeOptions) + dict.RegisterEnums("ImAbuseTargetTypeOptions", "IM举报对象类型选项", ImAbuseTargetTypeOptions) + dict.RegisterEnums("ImAbuseReasonEnabledOptions", "IM举报原因启用状态", ImAbuseReasonEnabledOptions) +} + +const ( + ImAppPlatformIos = "ios" + ImAppPlatformAndroid = "android" +) + +const ( + ImAppUpdateTypeNone = "none" + ImAppUpdateTypeRecommended = "recommended" + ImAppUpdateTypeForce = "force" +) + +const ( + ImAbuseTargetTypeUser = "user" + ImAbuseTargetTypeGroup = "group" + ImAbuseTargetTypeMessage = "message" +) + +var ImAppPlatformOptions = []*model.Option{ + dict.GenPrimaryOption(ImAppPlatformIos, "iOS"), + dict.GenSuccessOption(ImAppPlatformAndroid, "Android"), +} + +var ImAppUpdateTypeOptions = []*model.Option{ + dict.GenDefaultOption(ImAppUpdateTypeNone, "无需更新"), + dict.GenWarningOption(ImAppUpdateTypeRecommended, "建议更新"), + dict.GenErrorOption(ImAppUpdateTypeForce, "强制更新"), +} + +var ImAbuseTargetTypeOptions = []*model.Option{ + dict.GenPrimaryOption(ImAbuseTargetTypeUser, "用户"), + dict.GenSuccessOption(ImAbuseTargetTypeGroup, "群组"), + dict.GenWarningOption(ImAbuseTargetTypeMessage, "消息"), +} + +var ImAbuseReasonEnabledOptions = []*model.Option{ + dict.GenWarningOption(0, "禁用"), + dict.GenSuccessOption(1, "启用"), +} diff --git a/server/internal/consts/im_workbench.go b/server/internal/consts/im_workbench.go new file mode 100644 index 0000000..42bdc8f --- /dev/null +++ b/server/internal/consts/im_workbench.go @@ -0,0 +1,50 @@ +package consts + +import ( + "hotgo/internal/library/dict" + "hotgo/internal/model" +) + +func init() { + dict.RegisterEnums("ImWorkbenchAppEnabledOptions", "IM工作台模块启用状态", ImWorkbenchAppEnabledOptions) + dict.RegisterEnums("ImWorkbenchPermissionOptions", "IM工作台模块权限策略", ImWorkbenchPermissionOptions) + dict.RegisterEnums("ImWorkbenchRouteOptions", "IM工作台模块路由选项", ImWorkbenchRouteOptions) +} + +const ( + ImWorkbenchAppDisabled = 0 + ImWorkbenchAppEnabled = 1 +) + +const ( + ImWorkbenchPermissionEnabled = "enabled" + ImWorkbenchPermissionRoleRequired = "role_required" + ImWorkbenchPermissionDisabled = "disabled" +) + +const ( + ImWorkbenchRouteOrganization = "/organization" + ImWorkbenchRouteMeetings = "/meetings" + ImWorkbenchRouteDailyReports = "/reports/daily" + ImWorkbenchRouteMonthlyReports = "/reports/monthly" + ImWorkbenchRouteExecutiveMailbox = "/executive-mailbox" +) + +var ImWorkbenchAppEnabledOptions = []*model.Option{ + dict.GenWarningOption(ImWorkbenchAppDisabled, "禁用"), + dict.GenSuccessOption(ImWorkbenchAppEnabled, "启用"), +} + +var ImWorkbenchPermissionOptions = []*model.Option{ + dict.GenSuccessOption(ImWorkbenchPermissionEnabled, "全部可见"), + dict.GenPrimaryOption(ImWorkbenchPermissionRoleRequired, "按角色权限"), + dict.GenWarningOption(ImWorkbenchPermissionDisabled, "不可见"), +} + +var ImWorkbenchRouteOptions = []*model.Option{ + dict.GenPrimaryOption(ImWorkbenchRouteOrganization, "组织架构"), + dict.GenPrimaryOption(ImWorkbenchRouteMeetings, "会议通知"), + dict.GenPrimaryOption(ImWorkbenchRouteDailyReports, "日报"), + dict.GenPrimaryOption(ImWorkbenchRouteMonthlyReports, "月报"), + dict.GenPrimaryOption(ImWorkbenchRouteExecutiveMailbox, "总经理信箱"), +} diff --git a/server/internal/controller/admin/sys/im_abuse_reason.go b/server/internal/controller/admin/sys/im_abuse_reason.go new file mode 100644 index 0000000..8fcc3c8 --- /dev/null +++ b/server/internal/controller/admin/sys/im_abuse_reason.go @@ -0,0 +1,36 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imabusereason" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImAbuseReason = cImAbuseReason{} + +type cImAbuseReason struct{} + +func (c *cImAbuseReason) List(ctx context.Context, req *imabusereason.ListReq) (res *imabusereason.ListRes, err error) { + list, totalCount, err := service.SysImAbuseReason().List(ctx, &req.ImAbuseReasonListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImAbuseReasonModel{} + } + res = new(imabusereason.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +func (c *cImAbuseReason) Edit(ctx context.Context, req *imabusereason.EditReq) (res *imabusereason.EditRes, err error) { + err = service.SysImAbuseReason().Edit(ctx, &req.ImAbuseReasonEditInp) + return +} + +func (c *cImAbuseReason) Delete(ctx context.Context, req *imabusereason.DeleteReq) (res *imabusereason.DeleteRes, err error) { + err = service.SysImAbuseReason().Delete(ctx, &req.ImAbuseReasonDeleteInp) + return +} diff --git a/server/internal/controller/admin/sys/im_app_release.go b/server/internal/controller/admin/sys/im_app_release.go new file mode 100644 index 0000000..2b5023c --- /dev/null +++ b/server/internal/controller/admin/sys/im_app_release.go @@ -0,0 +1,31 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imapprelease" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImAppRelease = cImAppRelease{} + +type cImAppRelease struct{} + +func (c *cImAppRelease) List(ctx context.Context, req *imapprelease.ListReq) (res *imapprelease.ListRes, err error) { + list, totalCount, err := service.SysImAppRelease().List(ctx, &req.ImAppReleaseListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImAppReleaseModel{} + } + res = new(imapprelease.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +func (c *cImAppRelease) Edit(ctx context.Context, req *imapprelease.EditReq) (res *imapprelease.EditRes, err error) { + err = service.SysImAppRelease().Edit(ctx, &req.ImAppReleaseEditInp) + return +} diff --git a/server/internal/controller/admin/sys/im_feedback_config.go b/server/internal/controller/admin/sys/im_feedback_config.go new file mode 100644 index 0000000..11f84f7 --- /dev/null +++ b/server/internal/controller/admin/sys/im_feedback_config.go @@ -0,0 +1,25 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imfeedbackconfig" + "hotgo/internal/service" +) + +var ImFeedbackConfig = cImFeedbackConfig{} + +type cImFeedbackConfig struct{} + +func (c *cImFeedbackConfig) Get(ctx context.Context, req *imfeedbackconfig.GetReq) (res *imfeedbackconfig.GetRes, err error) { + data, err := service.SysImFeedbackConfig().Get(ctx, &req.ImFeedbackConfigGetInp) + if err != nil { + return + } + res = &imfeedbackconfig.GetRes{ImFeedbackConfigModel: data} + return +} + +func (c *cImFeedbackConfig) Update(ctx context.Context, req *imfeedbackconfig.UpdateReq) (res *imfeedbackconfig.UpdateRes, err error) { + err = service.SysImFeedbackConfig().Update(ctx, &req.ImFeedbackConfigUpdateInp) + return +} diff --git a/server/internal/controller/admin/sys/im_mailbox_config.go b/server/internal/controller/admin/sys/im_mailbox_config.go new file mode 100644 index 0000000..2f8e5ee --- /dev/null +++ b/server/internal/controller/admin/sys/im_mailbox_config.go @@ -0,0 +1,25 @@ +package sys + +import ( + "context" + "hotgo/api/admin/immailboxconfig" + "hotgo/internal/service" +) + +var ImMailboxConfig = cImMailboxConfig{} + +type cImMailboxConfig struct{} + +func (c *cImMailboxConfig) Get(ctx context.Context, req *immailboxconfig.GetReq) (res *immailboxconfig.GetRes, err error) { + data, err := service.SysImMailboxConfig().Get(ctx, &req.ImMailboxConfigGetInp) + if err != nil { + return + } + res = &immailboxconfig.GetRes{ImMailboxConfigModel: data} + return +} + +func (c *cImMailboxConfig) Update(ctx context.Context, req *immailboxconfig.UpdateReq) (res *immailboxconfig.UpdateRes, err error) { + err = service.SysImMailboxConfig().Update(ctx, &req.ImMailboxConfigUpdateInp) + return +} diff --git a/server/internal/controller/admin/sys/im_workbench.go b/server/internal/controller/admin/sys/im_workbench.go new file mode 100644 index 0000000..79f6c76 --- /dev/null +++ b/server/internal/controller/admin/sys/im_workbench.go @@ -0,0 +1,41 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imworkbench" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImWorkbench = cImWorkbench{} + +type cImWorkbench struct{} + +func (c *cImWorkbench) List(ctx context.Context, req *imworkbench.ListReq) (res *imworkbench.ListRes, err error) { + list, totalCount, err := service.SysImWorkbench().List(ctx, &req.ImWorkbenchAppListInp) + if err != nil { + return + } + if list == nil { + list = []*sysin.ImWorkbenchAppModel{} + } + res = new(imworkbench.ListRes) + res.List = list + res.PageRes.Pack(req, totalCount) + return +} + +func (c *cImWorkbench) Edit(ctx context.Context, req *imworkbench.EditReq) (res *imworkbench.EditRes, err error) { + err = service.SysImWorkbench().Edit(ctx, &req.ImWorkbenchAppEditInp) + return +} + +func (c *cImWorkbench) Delete(ctx context.Context, req *imworkbench.DeleteReq) (res *imworkbench.DeleteRes, err error) { + err = service.SysImWorkbench().Delete(ctx, &req.ImWorkbenchAppDeleteInp) + return +} + +func (c *cImWorkbench) Reorder(ctx context.Context, req *imworkbench.ReorderReq) (res *imworkbench.ReorderRes, err error) { + err = service.SysImWorkbench().Reorder(ctx, &req.ImWorkbenchAppReorderInp) + return +} diff --git a/server/internal/logic/sys/im_abuse_reason.go b/server/internal/logic/sys/im_abuse_reason.go new file mode 100644 index 0000000..7754e75 --- /dev/null +++ b/server/internal/logic/sys/im_abuse_reason.go @@ -0,0 +1,166 @@ +package sys + +import ( + "context" + "hotgo/internal/consts" + "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 imAbuseReasonsTable = "im_abuse_reasons" + +type sSysImAbuseReason struct{} + +func NewSysImAbuseReason() *sSysImAbuseReason { return &sSysImAbuseReason{} } + +func init() { + service.RegisterSysImAbuseReason(NewSysImAbuseReason()) +} + +func (s *sSysImAbuseReason) List(ctx context.Context, in *sysin.ImAbuseReasonListInp) (list []*sysin.ImAbuseReasonModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaults(ctx, db); err != nil { + return + } + mod := s.model(ctx, db) + if in.TargetType != "" { + mod = mod.Where("target_type", in.TargetType) + } + if in.ReasonCode != "" { + mod = mod.WhereLike("reason_code", "%"+in.ReasonCode+"%") + } + if in.Enabled != nil { + mod = mod.Where("enabled", *in.Enabled) + } + mod = mod.Page(in.Page, in.PerPage).OrderAsc("target_type").OrderAsc("sort_order").OrderAsc("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 *sSysImAbuseReason) Edit(ctx context.Context, in *sysin.ImAbuseReasonEditInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaults(ctx, db); err != nil { + return + } + data := g.Map{ + "target_type": in.TargetType, + "reason_code": in.ReasonCode, + "label": in.Label, + "requires_description": in.RequiresDescription, + "allows_evidence": in.AllowsEvidence, + "sort_order": in.SortOrder, + "enabled": in.Enabled, + "updated_at": gtime.Now(), + } + action := "im_abuse_reason_create" + targetId := in.Id + if in.Id > 0 { + if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).WherePri(in.Id).Data(data).Update(); err != nil { + return gerror.Wrap(err, "更新IM举报原因失败,请稍后重试!") + } + action = "im_abuse_reason_update" + } else { + data["created_at"] = gtime.Now() + if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil { + return gerror.Wrap(err, "新增IM举报原因失败,请稍后重试!") + } + idValue, err := db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Fields("id"). + Where("target_type", in.TargetType).Where("reason_code", in.ReasonCode).Value() + if err != nil { + return gerror.Wrap(err, "获取IM举报原因ID失败,请稍后重试!") + } + targetId = idValue.Int64() + } + _ = writeImAdminAudit(ctx, tenant.Id, action, "im_abuse_reason", targetId, "success", g.Map{ + "target_type": in.TargetType, "reason_code": in.ReasonCode, "enabled": in.Enabled, + }) + return nil +} + +func (s *sSysImAbuseReason) Delete(ctx context.Context, in *sysin.ImAbuseReasonDeleteInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).WherePri(in.Id). + Data(g.Map{"enabled": 0, "updated_at": gtime.Now()}).Update(); err != nil { + return gerror.Wrap(err, "禁用IM举报原因失败,请稍后重试!") + } + _ = writeImAdminAudit(ctx, tenant.Id, "im_abuse_reason_disable", "im_abuse_reason", in.Id, "success", nil) + return nil +} + +func (s *sSysImAbuseReason) model(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Fields( + "id", + "target_type", + "reason_code", + "label", + "requires_description", + "allows_evidence", + "sort_order", + "enabled", + "created_at", + "updated_at", + ) +} + +func (s *sSysImAbuseReason) ensureDefaults(ctx context.Context, db gdb.DB) (err error) { + defaults := []g.Map{ + {"target_type": consts.ImAbuseTargetTypeUser, "reason_code": "fraud", "label": "诈骗欺诈", "requires_description": 0, "allows_evidence": 1, "sort_order": 10}, + {"target_type": consts.ImAbuseTargetTypeUser, "reason_code": "harassment", "label": "骚扰辱骂", "requires_description": 0, "allows_evidence": 1, "sort_order": 20}, + {"target_type": consts.ImAbuseTargetTypeUser, "reason_code": "illegal", "label": "违法违规", "requires_description": 1, "allows_evidence": 1, "sort_order": 30}, + {"target_type": consts.ImAbuseTargetTypeGroup, "reason_code": "fraud", "label": "诈骗欺诈", "requires_description": 0, "allows_evidence": 1, "sort_order": 10}, + {"target_type": consts.ImAbuseTargetTypeGroup, "reason_code": "spam", "label": "垃圾广告", "requires_description": 0, "allows_evidence": 1, "sort_order": 20}, + {"target_type": consts.ImAbuseTargetTypeGroup, "reason_code": "illegal", "label": "违法违规", "requires_description": 1, "allows_evidence": 1, "sort_order": 30}, + {"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "fraud", "label": "诈骗欺诈", "requires_description": 0, "allows_evidence": 1, "sort_order": 10}, + {"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "spam", "label": "垃圾广告", "requires_description": 0, "allows_evidence": 1, "sort_order": 20}, + {"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "harassment", "label": "骚扰辱骂", "requires_description": 0, "allows_evidence": 1, "sort_order": 30}, + {"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "illegal", "label": "违法违规", "requires_description": 1, "allows_evidence": 1, "sort_order": 40}, + } + now := gtime.Now() + for _, item := range defaults { + var count int + if count, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx). + Where("target_type", item["target_type"]).Where("reason_code", item["reason_code"]).Count(); err != nil { + return gerror.Wrap(err, "检查IM举报默认原因失败,请稍后重试!") + } + if count > 0 { + continue + } + item["enabled"] = 1 + item["created_at"] = now + item["updated_at"] = now + if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil { + return gerror.Wrap(err, "初始化IM举报默认原因失败,请稍后重试!") + } + } + return nil +} diff --git a/server/internal/logic/sys/im_app_release.go b/server/internal/logic/sys/im_app_release.go new file mode 100644 index 0000000..396ca55 --- /dev/null +++ b/server/internal/logic/sys/im_app_release.go @@ -0,0 +1,150 @@ +package sys + +import ( + "context" + "hotgo/internal/consts" + "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 imAppReleasesTable = "im_app_releases" + +type sSysImAppRelease struct{} + +func NewSysImAppRelease() *sSysImAppRelease { return &sSysImAppRelease{} } + +func init() { + service.RegisterSysImAppRelease(NewSysImAppRelease()) +} + +func (s *sSysImAppRelease) List(ctx context.Context, in *sysin.ImAppReleaseListInp) (list []*sysin.ImAppReleaseModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaults(ctx, db); err != nil { + return + } + mod := s.model(ctx, db) + if in.Platform != "" { + mod = mod.Where("platform", in.Platform) + } + if in.Channel != "" { + mod = mod.WhereLike("channel", "%"+in.Channel+"%") + } + mod = mod.Page(in.Page, in.PerPage).OrderAsc("platform").OrderAsc("channel") + 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 *sSysImAppRelease) Edit(ctx context.Context, in *sysin.ImAppReleaseEditInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaults(ctx, db); err != nil { + return + } + data := g.Map{ + "platform": in.Platform, + "channel": in.Channel, + "latest_version": in.LatestVersion, + "min_supported_version": in.MinSupportedVersion, + "update_type": in.UpdateType, + "release_notes": in.ReleaseNotes, + "store_url": in.StoreUrl, + "package_url": in.PackageUrl, + "checksum": in.Checksum, + "remind_after_seconds": in.RemindAfterSeconds, + "published_at": gtime.Now(), + "updated_at": gtime.Now(), + } + action := "im_app_release_create" + targetId := in.Id + if in.Id > 0 { + if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).WherePri(in.Id).Data(data).Update(); err != nil { + return gerror.Wrap(err, "更新IM客户端版本策略失败,请稍后重试!") + } + action = "im_app_release_update" + } else { + data["created_at"] = gtime.Now() + if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil { + return gerror.Wrap(err, "新增IM客户端版本策略失败,请稍后重试!") + } + idValue, err := db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields("id"). + Where("platform", in.Platform).Where("channel", in.Channel).Value() + if err != nil { + return gerror.Wrap(err, "获取IM客户端版本策略ID失败,请稍后重试!") + } + targetId = idValue.Int64() + } + _ = writeImAdminAudit(ctx, tenant.Id, action, "im_app_release", targetId, "success", g.Map{ + "platform": in.Platform, "channel": in.Channel, "update_type": in.UpdateType, + }) + return nil +} + +func (s *sSysImAppRelease) model(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields( + "id", + "platform", + "channel", + "latest_version", + "min_supported_version", + "update_type", + "release_notes", + "store_url", + "package_url", + "checksum", + "remind_after_seconds", + "published_at", + "created_at", + "updated_at", + ) +} + +func (s *sSysImAppRelease) ensureDefaults(ctx context.Context, db gdb.DB) (err error) { + defaults := []g.Map{ + {"platform": consts.ImAppPlatformIos, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0}, + {"platform": consts.ImAppPlatformAndroid, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0}, + } + now := gtime.Now() + for _, item := range defaults { + var count int + if count, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx). + Where("platform", item["platform"]).Where("channel", item["channel"]).Count(); err != nil { + return gerror.Wrap(err, "检查IM客户端默认版本策略失败,请稍后重试!") + } + if count > 0 { + continue + } + item["store_url"] = "" + item["package_url"] = "" + item["checksum"] = "" + item["published_at"] = now + item["created_at"] = now + item["updated_at"] = now + if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil { + return gerror.Wrap(err, "初始化IM客户端默认版本策略失败,请稍后重试!") + } + } + return nil +} diff --git a/server/internal/logic/sys/im_feedback_config.go b/server/internal/logic/sys/im_feedback_config.go new file mode 100644 index 0000000..04c2704 --- /dev/null +++ b/server/internal/logic/sys/im_feedback_config.go @@ -0,0 +1,112 @@ +package sys + +import ( + "context" + "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 imFeedbackConfigTable = "im_feedback_config" + +type sSysImFeedbackConfig struct{} + +func NewSysImFeedbackConfig() *sSysImFeedbackConfig { return &sSysImFeedbackConfig{} } + +func init() { + service.RegisterSysImFeedbackConfig(NewSysImFeedbackConfig()) +} + +func (s *sSysImFeedbackConfig) Get(ctx context.Context, in *sysin.ImFeedbackConfigGetInp) (res *sysin.ImFeedbackConfigModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return nil, err + } + if err = s.ensureDefault(ctx, db); err != nil { + return nil, err + } + if err = s.model(ctx, db).OrderAsc("id").Limit(1).Scan(&res); err != nil { + return nil, gerror.Wrap(err, "获取IM意见反馈配置失败,请稍后重试!") + } + if res != nil { + res.TenantId = tenant.Id + res.TenantName = tenant.Name + } + return +} + +func (s *sSysImFeedbackConfig) Update(ctx context.Context, in *sysin.ImFeedbackConfigUpdateInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return err + } + if err = s.ensureDefault(ctx, db); err != nil { + return err + } + categories := `["bug","suggestion","complaint","other"]` + if in.Categories != nil && in.Categories.String() != "" && in.Categories.String() != "null" { + categories = in.Categories.String() + } + attachmentPolicy := `{"max_files":5,"purpose":"feedback"}` + if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" { + attachmentPolicy = in.AttachmentPolicy.String() + } + if _, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Where("id", 1).Data(g.Map{ + "categories": categories, + "max_length": in.MaxLength, + "attachment_policy": attachmentPolicy, + "allow_contact": in.AllowContact, + "diagnostics_notice": in.DiagnosticsNotice, + "updated_at": gtime.Now(), + }).Update(); err != nil { + return gerror.Wrap(err, "更新IM意见反馈配置失败,请稍后重试!") + } + _ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact}) + return nil +} + +func (s *sSysImFeedbackConfig) model(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Fields( + "id", + "CAST(categories AS CHAR) categories", + "max_length", + "CAST(attachment_policy AS CHAR) attachment_policy", + "allow_contact", + "diagnostics_notice", + "updated_at", + ) +} + +func (s *sSysImFeedbackConfig) ensureDefault(ctx context.Context, db gdb.DB) (err error) { + var count int + if count, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Where("id", 1).Count(); err != nil { + return gerror.Wrap(err, "检查IM意见反馈默认配置失败,请稍后重试!") + } + if count > 0 { + return nil + } + _, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Data(g.Map{ + "id": 1, + "categories": `["bug","suggestion","complaint","other"]`, + "max_length": 1000, + "attachment_policy": `{"max_files":5,"purpose":"feedback"}`, + "allow_contact": 1, + "diagnostics_notice": "仅在你同意时上传脱敏诊断信息。", + "created_at": gtime.Now(), + "updated_at": gtime.Now(), + }).Insert() + if err != nil { + return gerror.Wrap(err, "初始化IM意见反馈默认配置失败,请稍后重试!") + } + return nil +} diff --git a/server/internal/logic/sys/im_mailbox_config.go b/server/internal/logic/sys/im_mailbox_config.go new file mode 100644 index 0000000..e92b2aa --- /dev/null +++ b/server/internal/logic/sys/im_mailbox_config.go @@ -0,0 +1,121 @@ +package sys + +import ( + "context" + "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 imMailboxConfigTable = "im_mailbox_config" + +type sSysImMailboxConfig struct{} + +func NewSysImMailboxConfig() *sSysImMailboxConfig { return &sSysImMailboxConfig{} } + +func init() { + service.RegisterSysImMailboxConfig(NewSysImMailboxConfig()) +} + +func (s *sSysImMailboxConfig) Get(ctx context.Context, in *sysin.ImMailboxConfigGetInp) (res *sysin.ImMailboxConfigModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return nil, err + } + if err = s.ensureDefault(ctx, db); err != nil { + return nil, err + } + if err = s.model(ctx, db).Where("id", 1).Scan(&res); err != nil { + return nil, gerror.Wrap(err, "获取IM总经理信箱配置失败,请稍后重试!") + } + if res != nil { + res.TenantId = tenant.Id + res.TenantName = tenant.Name + } + return +} + +func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxConfigUpdateInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return err + } + if err = s.ensureDefault(ctx, db); err != nil { + return err + } + requiredFields := `["subject","content"]` + if in.RequiredFields != nil && in.RequiredFields.String() != "" && in.RequiredFields.String() != "null" { + requiredFields = in.RequiredFields.String() + } + attachmentPolicy := `{"max_files":5,"purpose":"executive_mailbox"}` + if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" { + attachmentPolicy = in.AttachmentPolicy.String() + } + if _, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Where("id", 1).Data(g.Map{ + "enabled": in.Enabled, + "allow_anonymous": in.AllowAnonymous, + "required_fields": requiredFields, + "max_length": in.MaxLength, + "attachment_policy": attachmentPolicy, + "notice_title": in.NoticeTitle, + "notice_content": in.NoticeContent, + "notice_updated_at": gtime.Now(), + "updated_at": gtime.Now(), + }).Update(); err != nil { + return gerror.Wrap(err, "更新IM总经理信箱配置失败,请稍后重试!") + } + _ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled}) + return nil +} + +func (s *sSysImMailboxConfig) model(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Fields( + "id", + "enabled", + "allow_anonymous", + "CAST(required_fields AS CHAR) required_fields", + "max_length", + "CAST(attachment_policy AS CHAR) attachment_policy", + "notice_title", + "notice_content", + "notice_updated_at", + "updated_at", + ) +} + +func (s *sSysImMailboxConfig) ensureDefault(ctx context.Context, db gdb.DB) (err error) { + var count int + if count, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Where("id", 1).Count(); err != nil { + return gerror.Wrap(err, "检查IM总经理信箱默认配置失败,请稍后重试!") + } + if count > 0 { + return nil + } + _, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Data(g.Map{ + "id": 1, + "enabled": 1, + "allow_anonymous": 1, + "required_fields": `["subject","content"]`, + "max_length": 5000, + "attachment_policy": `{"max_files":5,"purpose":"executive_mailbox"}`, + "notice_title": "总经理信箱", + "notice_content": "请如实填写诉求,匿名提交将按企业政策保护展示身份。", + "notice_updated_at": gtime.Now(), + "created_at": gtime.Now(), + "updated_at": gtime.Now(), + }).Insert() + if err != nil { + return gerror.Wrap(err, "初始化IM总经理信箱默认配置失败,请稍后重试!") + } + return nil +} diff --git a/server/internal/logic/sys/im_workbench.go b/server/internal/logic/sys/im_workbench.go new file mode 100644 index 0000000..551c173 --- /dev/null +++ b/server/internal/logic/sys/im_workbench.go @@ -0,0 +1,194 @@ +package sys + +import ( + "context" + "hotgo/internal/consts" + "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 imWorkbenchAppsTable = "im_workbench_apps" + +type sSysImWorkbench struct{} + +func NewSysImWorkbench() *sSysImWorkbench { return &sSysImWorkbench{} } + +func init() { + service.RegisterSysImWorkbench(NewSysImWorkbench()) +} + +func (s *sSysImWorkbench) List(ctx context.Context, in *sysin.ImWorkbenchAppListInp) (list []*sysin.ImWorkbenchAppModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaultApps(ctx, db); err != nil { + return + } + mod := s.model(ctx, db) + if in.Code != "" { + mod = mod.WhereLike("code", "%"+in.Code+"%") + } + if in.Name != "" { + mod = mod.WhereLike("name", "%"+in.Name+"%") + } + if in.Enabled != nil { + mod = mod.Where("enabled", *in.Enabled) + } + mod = mod.Page(in.Page, in.PerPage).OrderAsc("sort_order").OrderAsc("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 *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEditInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaultApps(ctx, db); err != nil { + return + } + data := g.Map{ + "code": in.Code, + "name": in.Name, + "icon_file_id": in.IconFileId, + "route": in.Route, + "sort_order": in.SortOrder, + "unread_key": in.UnreadKey, + "enabled": in.Enabled, + "permission": in.Permission, + "updated_at": gtime.Now(), + } + action := "im_workbench_app_create" + targetId := in.Id + if in.Id > 0 { + if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).WherePri(in.Id).Data(data).Update(); err != nil { + return gerror.Wrap(err, "更新IM工作台模块失败,请稍后重试!") + } + action = "im_workbench_app_update" + } else { + data["created_at"] = gtime.Now() + if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil { + return gerror.Wrap(err, "新增IM工作台模块失败,请稍后重试!") + } + idValue, err := db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Fields("id").Where("code", in.Code).Value() + if err != nil { + return gerror.Wrap(err, "获取IM工作台模块ID失败,请稍后重试!") + } + targetId = idValue.Int64() + } + _ = writeImAdminAudit(ctx, tenant.Id, action, "im_workbench_app", targetId, "success", g.Map{ + "code": in.Code, "route": in.Route, "enabled": in.Enabled, "permission": in.Permission, + }) + return nil +} + +func (s *sSysImWorkbench) Delete(ctx context.Context, in *sysin.ImWorkbenchAppDeleteInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaultApps(ctx, db); err != nil { + return + } + mod := db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx) + if in.Id > 0 { + mod = mod.WherePri(in.Id) + } else { + mod = mod.Where("code", in.Code) + } + if _, err = mod.Data(g.Map{"enabled": consts.ImWorkbenchAppDisabled, "updated_at": gtime.Now()}).Update(); err != nil { + return gerror.Wrap(err, "禁用IM工作台模块失败,请稍后重试!") + } + _ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code}) + return nil +} + +func (s *sSysImWorkbench) Reorder(ctx context.Context, in *sysin.ImWorkbenchAppReorderInp) (err error) { + if err = in.Filter(ctx); err != nil { + return + } + db, tenant, err := resolveImTenantDB(ctx, in.TenantId) + if err != nil { + return + } + if err = s.ensureDefaultApps(ctx, db); err != nil { + return + } + if err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + for _, item := range in.Items { + if _, e := tx.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Where("code", item.Code). + Data(g.Map{"sort_order": item.SortOrder, "updated_at": gtime.Now()}).Update(); e != nil { + return e + } + } + return nil + }); err != nil { + return gerror.Wrap(err, "调整IM工作台模块排序失败,请稍后重试!") + } + _ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_reorder", "im_workbench_app", 0, "success", g.Map{"count": len(in.Items)}) + return nil +} + +func (s *sSysImWorkbench) model(ctx context.Context, db gdb.DB) *gdb.Model { + return db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Fields( + "id", + "code", + "name", + "icon_file_id", + "route", + "sort_order", + "unread_key", + "enabled", + "permission", + "created_at", + "updated_at", + ) +} + +func (s *sSysImWorkbench) ensureDefaultApps(ctx context.Context, db gdb.DB) (err error) { + defaults := []g.Map{ + {"code": "organization", "name": "组织架构", "route": consts.ImWorkbenchRouteOrganization, "sort_order": 10, "unread_key": "organization", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled}, + {"code": "meetings", "name": "会议通知", "route": consts.ImWorkbenchRouteMeetings, "sort_order": 20, "unread_key": "meetings", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled}, + {"code": "daily_reports", "name": "日报", "route": consts.ImWorkbenchRouteDailyReports, "sort_order": 30, "unread_key": "daily_reports", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled}, + {"code": "monthly_reports", "name": "月报", "route": consts.ImWorkbenchRouteMonthlyReports, "sort_order": 40, "unread_key": "monthly_reports", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled}, + {"code": "executive_mailbox", "name": "总经理信箱", "route": consts.ImWorkbenchRouteExecutiveMailbox, "sort_order": 50, "unread_key": "executive_mailbox", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled}, + } + now := gtime.Now() + for _, item := range defaults { + var count int + if count, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Where("code", item["code"]).Count(); err != nil { + return gerror.Wrap(err, "检查IM工作台默认模块失败,请稍后重试!") + } + if count > 0 { + continue + } + item["created_at"] = now + item["updated_at"] = now + if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil { + return gerror.Wrap(err, "初始化IM工作台默认模块失败,请稍后重试!") + } + } + return nil +} diff --git a/server/internal/model/input/sysin/im_abuse_reason.go b/server/internal/model/input/sysin/im_abuse_reason.go new file mode 100644 index 0000000..5fdd042 --- /dev/null +++ b/server/internal/model/input/sysin/im_abuse_reason.go @@ -0,0 +1,96 @@ +package sysin + +import ( + "context" + "hotgo/internal/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImAbuseReasonListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + TargetType string `json:"targetType" dc:"对象类型"` + ReasonCode string `json:"reasonCode" dc:"原因编码"` + Enabled *int `json:"enabled" dc:"启用状态"` +} + +func (in *ImAbuseReasonListInp) Filter(ctx context.Context) (err error) { + in.TargetType = strings.TrimSpace(in.TargetType) + in.ReasonCode = strings.TrimSpace(in.ReasonCode) + if in.TargetType != "" && !dict.HasOptionKey(consts.ImAbuseTargetTypeOptions, in.TargetType) { + return gerror.New("举报对象类型不合法") + } + if in.Enabled != nil && !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, *in.Enabled) { + return gerror.New("启用状态不合法") + } + return nil +} + +type ImAbuseReasonModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + TargetType string `json:"targetType" dc:"对象类型"` + ReasonCode string `json:"reasonCode" dc:"原因编码"` + Label string `json:"label" dc:"展示文案"` + RequiresDescription int `json:"requiresDescription" dc:"要求描述"` + AllowsEvidence int `json:"allowsEvidence" dc:"允许证据"` + SortOrder int `json:"sortOrder" dc:"排序"` + Enabled int `json:"enabled" dc:"启用状态"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ImAbuseReasonEditInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Id int64 `json:"id" dc:"ID"` + TargetType string `json:"targetType" v:"required#对象类型不能为空" dc:"对象类型"` + ReasonCode string `json:"reasonCode" v:"required#原因编码不能为空" dc:"原因编码"` + Label string `json:"label" v:"required#展示文案不能为空" dc:"展示文案"` + RequiresDescription int `json:"requiresDescription" dc:"要求描述"` + AllowsEvidence int `json:"allowsEvidence" dc:"允许证据"` + SortOrder int `json:"sortOrder" dc:"排序"` + Enabled int `json:"enabled" dc:"启用状态"` +} + +func (in *ImAbuseReasonEditInp) Filter(ctx context.Context) (err error) { + in.TargetType = strings.TrimSpace(in.TargetType) + in.ReasonCode = strings.TrimSpace(in.ReasonCode) + in.Label = strings.TrimSpace(in.Label) + if !dict.HasOptionKey(consts.ImAbuseTargetTypeOptions, in.TargetType) { + return gerror.New("举报对象类型不合法") + } + if in.ReasonCode == "" || strings.ContainsAny(in.ReasonCode, " \t\r\n/") || len([]rune(in.ReasonCode)) > 64 { + return gerror.New("原因编码不能为空,且不能包含空格或斜杠") + } + if in.Label == "" || len([]rune(in.Label)) > 128 { + return gerror.New("展示文案不能为空且不能超过128个字符") + } + if !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, in.RequiresDescription) { + return gerror.New("要求描述开关不合法") + } + if !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, in.AllowsEvidence) { + return gerror.New("允许证据开关不合法") + } + if !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, in.Enabled) { + return gerror.New("启用状态不合法") + } + return nil +} + +type ImAbuseReasonDeleteInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Id int64 `json:"id" v:"required|min:1#请选择要禁用的举报原因|请选择要禁用的举报原因" dc:"ID"` +} + +func (in *ImAbuseReasonDeleteInp) Filter(ctx context.Context) (err error) { + if in.Id <= 0 { + return gerror.New("请选择要禁用的举报原因") + } + return nil +} diff --git a/server/internal/model/input/sysin/im_app_release.go b/server/internal/model/input/sysin/im_app_release.go new file mode 100644 index 0000000..5b4f1fb --- /dev/null +++ b/server/internal/model/input/sysin/im_app_release.go @@ -0,0 +1,92 @@ +package sysin + +import ( + "context" + "hotgo/internal/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImAppReleaseListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + Platform string `json:"platform" dc:"平台"` + Channel string `json:"channel" dc:"渠道"` +} + +func (in *ImAppReleaseListInp) Filter(ctx context.Context) (err error) { + in.Platform = strings.TrimSpace(in.Platform) + in.Channel = strings.TrimSpace(in.Channel) + if in.Platform != "" && !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) { + return gerror.New("客户端平台不合法") + } + return nil +} + +type ImAppReleaseModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + Platform string `json:"platform" dc:"平台"` + Channel string `json:"channel" dc:"渠道"` + LatestVersion string `json:"latestVersion" dc:"最新版本"` + MinSupportedVersion string `json:"minSupportedVersion" dc:"最低支持版本"` + UpdateType string `json:"updateType" dc:"更新类型"` + ReleaseNotes string `json:"releaseNotes" dc:"更新说明"` + StoreUrl string `json:"storeUrl" dc:"商店地址"` + PackageUrl string `json:"packageUrl" dc:"安装包地址"` + Checksum string `json:"checksum" dc:"校验值"` + RemindAfterSeconds int `json:"remindAfterSeconds" dc:"再次提醒秒数"` + PublishedAt *gtime.Time `json:"publishedAt" dc:"发布时间"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ImAppReleaseEditInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Id int64 `json:"id" dc:"ID"` + Platform string `json:"platform" v:"required#平台不能为空" dc:"平台"` + Channel string `json:"channel" v:"required#渠道不能为空" dc:"渠道"` + LatestVersion string `json:"latestVersion" v:"required#最新版本不能为空" dc:"最新版本"` + MinSupportedVersion string `json:"minSupportedVersion" v:"required#最低支持版本不能为空" dc:"最低支持版本"` + UpdateType string `json:"updateType" v:"required#更新类型不能为空" dc:"更新类型"` + ReleaseNotes string `json:"releaseNotes" dc:"更新说明"` + StoreUrl string `json:"storeUrl" dc:"商店地址"` + PackageUrl string `json:"packageUrl" dc:"安装包地址"` + Checksum string `json:"checksum" dc:"校验值"` + RemindAfterSeconds int `json:"remindAfterSeconds" dc:"再次提醒秒数"` +} + +func (in *ImAppReleaseEditInp) Filter(ctx context.Context) (err error) { + in.Platform = strings.TrimSpace(in.Platform) + in.Channel = strings.TrimSpace(in.Channel) + in.LatestVersion = strings.TrimSpace(in.LatestVersion) + in.MinSupportedVersion = strings.TrimSpace(in.MinSupportedVersion) + in.UpdateType = strings.TrimSpace(in.UpdateType) + in.StoreUrl = strings.TrimSpace(in.StoreUrl) + in.PackageUrl = strings.TrimSpace(in.PackageUrl) + in.Checksum = strings.TrimSpace(in.Checksum) + if !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) { + return gerror.New("客户端平台不合法") + } + if in.Channel == "" || strings.ContainsAny(in.Channel, " \t\r\n/") || len([]rune(in.Channel)) > 32 { + return gerror.New("渠道不能为空,且不能包含空格或斜杠") + } + if in.LatestVersion == "" || len([]rune(in.LatestVersion)) > 64 { + return gerror.New("最新版本不能为空且不能超过64个字符") + } + if in.MinSupportedVersion == "" || len([]rune(in.MinSupportedVersion)) > 64 { + return gerror.New("最低支持版本不能为空且不能超过64个字符") + } + if !dict.HasOptionKey(consts.ImAppUpdateTypeOptions, in.UpdateType) { + return gerror.New("更新类型不合法") + } + if in.RemindAfterSeconds < 0 { + return gerror.New("再次提醒时间不能小于0") + } + return nil +} diff --git a/server/internal/model/input/sysin/im_feedback_config.go b/server/internal/model/input/sysin/im_feedback_config.go new file mode 100644 index 0000000..a0e8d1d --- /dev/null +++ b/server/internal/model/input/sysin/im_feedback_config.go @@ -0,0 +1,48 @@ +package sysin + +import ( + "context" + "hotgo/internal/consts" + "hotgo/internal/library/dict" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImFeedbackConfigGetInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` +} + +func (in *ImFeedbackConfigGetInp) Filter(ctx context.Context) (err error) { return } + +type ImFeedbackConfigModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + Categories string `json:"categories" dc:"分类JSON"` + MaxLength int `json:"maxLength" dc:"最大字数"` + AttachmentPolicy string `json:"attachmentPolicy" dc:"附件策略JSON"` + AllowContact int `json:"allowContact" dc:"允许联系方式"` + DiagnosticsNotice string `json:"diagnosticsNotice" dc:"诊断信息提示"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ImFeedbackConfigUpdateInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Categories *gjson.Json `json:"categories" dc:"分类JSON"` + MaxLength int `json:"maxLength" dc:"最大字数"` + AttachmentPolicy *gjson.Json `json:"attachmentPolicy" dc:"附件策略JSON"` + AllowContact int `json:"allowContact" dc:"允许联系方式"` + DiagnosticsNotice string `json:"diagnosticsNotice" v:"required#诊断提示不能为空" dc:"诊断信息提示"` +} + +func (in *ImFeedbackConfigUpdateInp) Filter(ctx context.Context) (err error) { + if in.MaxLength <= 0 { + return gerror.New("最大字数必须大于0") + } + if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.AllowContact) { + return gerror.New("联系方式开关不合法") + } + return nil +} diff --git a/server/internal/model/input/sysin/im_mailbox_config.go b/server/internal/model/input/sysin/im_mailbox_config.go new file mode 100644 index 0000000..328915c --- /dev/null +++ b/server/internal/model/input/sysin/im_mailbox_config.go @@ -0,0 +1,59 @@ +package sysin + +import ( + "context" + "hotgo/internal/consts" + "hotgo/internal/library/dict" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImMailboxConfigGetInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` +} + +func (in *ImMailboxConfigGetInp) Filter(ctx context.Context) (err error) { return } + +type ImMailboxConfigModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + Enabled int `json:"enabled" dc:"启用状态"` + AllowAnonymous int `json:"allowAnonymous" dc:"允许匿名"` + RequiredFields string `json:"requiredFields" dc:"必填字段JSON"` + MaxLength int `json:"maxLength" dc:"最大字数"` + AttachmentPolicy string `json:"attachmentPolicy" dc:"附件策略JSON"` + NoticeTitle string `json:"noticeTitle" dc:"告知标题"` + NoticeContent string `json:"noticeContent" dc:"告知内容"` + NoticeUpdatedAt *gtime.Time `json:"noticeUpdatedAt" dc:"告知更新时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ImMailboxConfigUpdateInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Enabled int `json:"enabled" dc:"启用状态"` + AllowAnonymous int `json:"allowAnonymous" dc:"允许匿名"` + RequiredFields *gjson.Json `json:"requiredFields" dc:"必填字段JSON"` + MaxLength int `json:"maxLength" dc:"最大字数"` + AttachmentPolicy *gjson.Json `json:"attachmentPolicy" dc:"附件策略JSON"` + NoticeTitle string `json:"noticeTitle" v:"required#告知标题不能为空" dc:"告知标题"` + NoticeContent string `json:"noticeContent" v:"required#告知内容不能为空" dc:"告知内容"` +} + +func (in *ImMailboxConfigUpdateInp) Filter(ctx context.Context) (err error) { + if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.Enabled) { + return gerror.New("启用状态不合法") + } + if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.AllowAnonymous) { + return gerror.New("匿名开关不合法") + } + if in.MaxLength <= 0 { + return gerror.New("最大字数必须大于0") + } + if len([]rune(in.NoticeTitle)) > 128 { + return gerror.New("告知标题不能超过128个字符") + } + return nil +} diff --git a/server/internal/model/input/sysin/im_workbench.go b/server/internal/model/input/sysin/im_workbench.go new file mode 100644 index 0000000..f33c903 --- /dev/null +++ b/server/internal/model/input/sysin/im_workbench.go @@ -0,0 +1,141 @@ +package sysin + +import ( + "context" + "hotgo/internal/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImWorkbenchAppListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + Code string `json:"code" dc:"模块编码"` + Name string `json:"name" dc:"模块名称"` + Enabled *int `json:"enabled" dc:"启用状态"` +} + +func (in *ImWorkbenchAppListInp) Filter(ctx context.Context) (err error) { + in.Code = strings.TrimSpace(in.Code) + in.Name = strings.TrimSpace(in.Name) + if in.Enabled != nil && !dict.HasOptionKey(consts.ImWorkbenchAppEnabledOptions, *in.Enabled) { + return gerror.New("启用状态不合法") + } + return nil +} + +type ImWorkbenchAppModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + Code string `json:"code" dc:"模块编码"` + Name string `json:"name" dc:"模块名称"` + IconFileId string `json:"iconFileId" dc:"图标文件ID"` + Route string `json:"route" dc:"客户端路由"` + SortOrder int `json:"sortOrder" dc:"排序"` + UnreadKey string `json:"unreadKey" dc:"未读角标Key"` + Enabled int `json:"enabled" dc:"启用状态"` + Permission string `json:"permission" dc:"权限策略"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ImWorkbenchAppEditInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Id int64 `json:"id" dc:"ID"` + Code string `json:"code" v:"required#模块编码不能为空" dc:"模块编码"` + Name string `json:"name" v:"required#模块名称不能为空" dc:"模块名称"` + IconFileId string `json:"iconFileId" dc:"图标文件ID"` + Route string `json:"route" v:"required#客户端路由不能为空" dc:"客户端路由"` + SortOrder int `json:"sortOrder" dc:"排序"` + UnreadKey string `json:"unreadKey" dc:"未读角标Key"` + Enabled int `json:"enabled" dc:"启用状态"` + Permission string `json:"permission" dc:"权限策略"` +} + +func (in *ImWorkbenchAppEditInp) Filter(ctx context.Context) (err error) { + in.Code = strings.TrimSpace(in.Code) + in.Name = strings.TrimSpace(in.Name) + in.IconFileId = strings.TrimSpace(in.IconFileId) + in.Route = strings.TrimSpace(in.Route) + in.UnreadKey = strings.TrimSpace(in.UnreadKey) + in.Permission = strings.TrimSpace(in.Permission) + if in.Code == "" { + return gerror.New("模块编码不能为空") + } + if strings.ContainsAny(in.Code, " \t\r\n/") { + return gerror.New("模块编码不能包含空格或斜杠") + } + if len([]rune(in.Code)) > 64 { + return gerror.New("模块编码不能超过64个字符") + } + if in.Name == "" { + return gerror.New("模块名称不能为空") + } + if len([]rune(in.Name)) > 128 { + return gerror.New("模块名称不能超过128个字符") + } + if !dict.HasOptionKey(consts.ImWorkbenchRouteOptions, in.Route) { + return gerror.New("客户端路由不在当前IM服务端白名单内") + } + if !dict.HasOptionKey(consts.ImWorkbenchAppEnabledOptions, in.Enabled) { + return gerror.New("启用状态不合法") + } + if in.Permission == "" { + in.Permission = consts.ImWorkbenchPermissionEnabled + } + if !dict.HasOptionKey(consts.ImWorkbenchPermissionOptions, in.Permission) { + return gerror.New("权限策略不合法") + } + if len([]rune(in.IconFileId)) > 64 { + return gerror.New("图标文件ID不能超过64个字符") + } + if len([]rune(in.UnreadKey)) > 64 { + return gerror.New("未读角标Key不能超过64个字符") + } + return nil +} + +type ImWorkbenchAppDeleteInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Id int64 `json:"id" dc:"ID"` + Code string `json:"code" dc:"模块编码"` +} + +func (in *ImWorkbenchAppDeleteInp) Filter(ctx context.Context) (err error) { + in.Code = strings.TrimSpace(in.Code) + if in.Id <= 0 && in.Code == "" { + return gerror.New("请选择要禁用的工作台模块") + } + return nil +} + +type ImWorkbenchAppReorderItem struct { + Code string `json:"code" dc:"模块编码"` + SortOrder int `json:"sortOrder" dc:"排序"` +} + +type ImWorkbenchAppReorderInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Items []*ImWorkbenchAppReorderItem `json:"items" dc:"排序列表"` +} + +func (in *ImWorkbenchAppReorderInp) Filter(ctx context.Context) (err error) { + if len(in.Items) == 0 { + return gerror.New("排序列表不能为空") + } + for _, item := range in.Items { + if item == nil { + return gerror.New("排序项不能为空") + } + item.Code = strings.TrimSpace(item.Code) + if item.Code == "" { + return gerror.New("模块编码不能为空") + } + } + return nil +} diff --git a/server/internal/router/genrouter/im_abuse_reason.go b/server/internal/router/genrouter/im_abuse_reason.go new file mode 100644 index 0000000..989a817 --- /dev/null +++ b/server/internal/router/genrouter/im_abuse_reason.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImAbuseReason) // IM举报原因 +} diff --git a/server/internal/router/genrouter/im_app_release.go b/server/internal/router/genrouter/im_app_release.go new file mode 100644 index 0000000..3e9e344 --- /dev/null +++ b/server/internal/router/genrouter/im_app_release.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImAppRelease) // IM版本更新 +} diff --git a/server/internal/router/genrouter/im_feedback_config.go b/server/internal/router/genrouter/im_feedback_config.go new file mode 100644 index 0000000..908f11c --- /dev/null +++ b/server/internal/router/genrouter/im_feedback_config.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImFeedbackConfig) // IM意见反馈配置 +} diff --git a/server/internal/router/genrouter/im_mailbox_config.go b/server/internal/router/genrouter/im_mailbox_config.go new file mode 100644 index 0000000..920556b --- /dev/null +++ b/server/internal/router/genrouter/im_mailbox_config.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImMailboxConfig) // IM总经理信箱配置 +} diff --git a/server/internal/router/genrouter/im_workbench.go b/server/internal/router/genrouter/im_workbench.go new file mode 100644 index 0000000..13f9e58 --- /dev/null +++ b/server/internal/router/genrouter/im_workbench.go @@ -0,0 +1,7 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { + LoginRequiredRouter = append(LoginRequiredRouter, sys.ImWorkbench) // IM工作台 +} diff --git a/server/internal/service/sys.go b/server/internal/service/sys.go index 8c3bb26..4b2f86c 100644 --- a/server/internal/service/sys.go +++ b/server/internal/service/sys.go @@ -347,6 +347,42 @@ type ( // TemplateUpdate 更新IM通知模板 TemplateUpdate(ctx context.Context, in *sysin.ImNotificationTemplateUpdateInp) (err error) } + ISysImAppRelease interface { + // List 获取IM客户端版本策略列表 + List(ctx context.Context, in *sysin.ImAppReleaseListInp) (list []*sysin.ImAppReleaseModel, totalCount int, err error) + // Edit 新增/编辑IM客户端版本策略 + Edit(ctx context.Context, in *sysin.ImAppReleaseEditInp) (err error) + } + ISysImAbuseReason interface { + // List 获取IM举报原因列表 + List(ctx context.Context, in *sysin.ImAbuseReasonListInp) (list []*sysin.ImAbuseReasonModel, totalCount int, err error) + // Edit 新增/编辑IM举报原因 + Edit(ctx context.Context, in *sysin.ImAbuseReasonEditInp) (err error) + // Delete 禁用IM举报原因 + Delete(ctx context.Context, in *sysin.ImAbuseReasonDeleteInp) (err error) + } + ISysImMailboxConfig interface { + // Get 获取IM总经理信箱配置 + Get(ctx context.Context, in *sysin.ImMailboxConfigGetInp) (res *sysin.ImMailboxConfigModel, err error) + // Update 更新IM总经理信箱配置 + Update(ctx context.Context, in *sysin.ImMailboxConfigUpdateInp) (err error) + } + ISysImFeedbackConfig interface { + // Get 获取IM意见反馈配置 + Get(ctx context.Context, in *sysin.ImFeedbackConfigGetInp) (res *sysin.ImFeedbackConfigModel, err error) + // Update 更新IM意见反馈配置 + Update(ctx context.Context, in *sysin.ImFeedbackConfigUpdateInp) (err error) + } + ISysImWorkbench interface { + // List 获取IM工作台模块列表 + List(ctx context.Context, in *sysin.ImWorkbenchAppListInp) (list []*sysin.ImWorkbenchAppModel, totalCount int, err error) + // Edit 新增/编辑IM工作台模块 + Edit(ctx context.Context, in *sysin.ImWorkbenchAppEditInp) (err error) + // Delete 禁用IM工作台模块 + Delete(ctx context.Context, in *sysin.ImWorkbenchAppDeleteInp) (err error) + // Reorder 调整IM工作台模块排序 + Reorder(ctx context.Context, in *sysin.ImWorkbenchAppReorderInp) (err error) + } ISysImTaskDashboard interface { // List 获取IM任务看板列表 List(ctx context.Context, in *sysin.ImTaskDashboardListInp) (list []*sysin.ImTaskDashboardListModel, totalCount int, err error) @@ -615,6 +651,11 @@ var ( localSysImFilePolicy ISysImFilePolicy localSysImExternalConfig ISysImExternalConfig localSysImNotification ISysImNotification + localSysImAppRelease ISysImAppRelease + localSysImAbuseReason ISysImAbuseReason + localSysImMailboxConfig ISysImMailboxConfig + localSysImFeedbackConfig ISysImFeedbackConfig + localSysImWorkbench ISysImWorkbench localSysImTaskDashboard ISysImTaskDashboard localSysImAuditLog ISysImAuditLog localSysDictData ISysDictData @@ -863,6 +904,61 @@ func RegisterSysImNotification(i ISysImNotification) { localSysImNotification = i } +func SysImAppRelease() ISysImAppRelease { + if localSysImAppRelease == nil { + panic("implement not found for interface ISysImAppRelease, forgot register?") + } + return localSysImAppRelease +} + +func RegisterSysImAppRelease(i ISysImAppRelease) { + localSysImAppRelease = i +} + +func SysImAbuseReason() ISysImAbuseReason { + if localSysImAbuseReason == nil { + panic("implement not found for interface ISysImAbuseReason, forgot register?") + } + return localSysImAbuseReason +} + +func RegisterSysImAbuseReason(i ISysImAbuseReason) { + localSysImAbuseReason = i +} + +func SysImMailboxConfig() ISysImMailboxConfig { + if localSysImMailboxConfig == nil { + panic("implement not found for interface ISysImMailboxConfig, forgot register?") + } + return localSysImMailboxConfig +} + +func RegisterSysImMailboxConfig(i ISysImMailboxConfig) { + localSysImMailboxConfig = i +} + +func SysImFeedbackConfig() ISysImFeedbackConfig { + if localSysImFeedbackConfig == nil { + panic("implement not found for interface ISysImFeedbackConfig, forgot register?") + } + return localSysImFeedbackConfig +} + +func RegisterSysImFeedbackConfig(i ISysImFeedbackConfig) { + localSysImFeedbackConfig = i +} + +func SysImWorkbench() ISysImWorkbench { + if localSysImWorkbench == nil { + panic("implement not found for interface ISysImWorkbench, forgot register?") + } + return localSysImWorkbench +} + +func RegisterSysImWorkbench(i ISysImWorkbench) { + localSysImWorkbench = i +} + func SysImTaskDashboard() ISysImTaskDashboard { if localSysImTaskDashboard == nil { panic("implement not found for interface ISysImTaskDashboard, forgot register?") diff --git a/server/storage/data/generate/im_abuse_reason_menu.sql b/server/storage/data/generate/im_abuse_reason_menu.sql new file mode 100644 index 0000000..f235603 --- /dev/null +++ b/server/storage/data/generate/im_abuse_reason_menu.sql @@ -0,0 +1,16 @@ +SET @now := NOW(); +SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '举报原因', 'imAbuseReasonIndex', 'abuse-reason/index', '', '2', '', '/imAbuseReason/list', '', '/imAbuseReason/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '82', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonIndex'); + +SET @imAbuseReasonId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imAbuseReasonId, '原因编辑', 'imAbuseReasonEdit', '', '', '3', '', '/imAbuseReason/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAbuseReasonId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonEdit'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imAbuseReasonId, '原因禁用', 'imAbuseReasonDelete', '', '', '3', '', '/imAbuseReason/delete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAbuseReasonId, ' '), '20', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonDelete'); diff --git a/server/storage/data/generate/im_app_release_menu.sql b/server/storage/data/generate/im_app_release_menu.sql new file mode 100644 index 0000000..20f3635 --- /dev/null +++ b/server/storage/data/generate/im_app_release_menu.sql @@ -0,0 +1,12 @@ +SET @now := NOW(); +SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '版本更新', 'imAppReleaseIndex', 'app-release/index', '', '2', '', '/imAppRelease/list', '', '/imAppRelease/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '81', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseIndex'); + +SET @imAppReleaseId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imAppReleaseId, '版本编辑', 'imAppReleaseEdit', '', '', '3', '', '/imAppRelease/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAppReleaseId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseEdit'); diff --git a/server/storage/data/generate/im_feedback_config_menu.sql b/server/storage/data/generate/im_feedback_config_menu.sql new file mode 100644 index 0000000..711a6f7 --- /dev/null +++ b/server/storage/data/generate/im_feedback_config_menu.sql @@ -0,0 +1,12 @@ +SET @now := NOW(); +SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '意见反馈配置', 'imFeedbackConfigIndex', 'feedback-config/index', '', '2', '', '/imFeedbackConfig/get', '', '/imFeedbackConfig/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '84', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex'); + +SET @imFeedbackConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imFeedbackConfigId, '反馈配置更新', 'imFeedbackConfigUpdate', '', '', '3', '', '/imFeedbackConfig/update', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imFeedbackConfigId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigUpdate'); diff --git a/server/storage/data/generate/im_mailbox_config_menu.sql b/server/storage/data/generate/im_mailbox_config_menu.sql new file mode 100644 index 0000000..ecca619 --- /dev/null +++ b/server/storage/data/generate/im_mailbox_config_menu.sql @@ -0,0 +1,12 @@ +SET @now := NOW(); +SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '总经理信箱配置', 'imMailboxConfigIndex', 'mailbox-config/index', '', '2', '', '/imMailboxConfig/get', '', '/imMailboxConfig/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '83', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex'); + +SET @imMailboxConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imMailboxConfigId, '信箱配置更新', 'imMailboxConfigUpdate', '', '', '3', '', '/imMailboxConfig/update', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imMailboxConfigId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigUpdate'); diff --git a/server/storage/data/generate/im_workbench_menu.sql b/server/storage/data/generate/im_workbench_menu.sql new file mode 100644 index 0000000..0b28785 --- /dev/null +++ b/server/storage/data/generate/im_workbench_menu.sql @@ -0,0 +1,20 @@ +SET @now := NOW(); +SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imRootId, '工作台配置', 'imWorkbenchIndex', 'workbench/index', '', '2', '', '/imWorkbench/list', '', '/imWorkbench/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '80', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex'); + +SET @imWorkbenchId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imWorkbenchId, '工作台编辑', 'imWorkbenchEdit', '', '', '3', '', '/imWorkbench/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '10', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchEdit'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imWorkbenchId, '工作台禁用', 'imWorkbenchDelete', '', '', '3', '', '/imWorkbench/delete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '20', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchDelete'); + +INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`) +SELECT NULL, @imWorkbenchId, '工作台排序', 'imWorkbenchReorder', '', '', '3', '', '/imWorkbench/reorder', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '30', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchReorder'); diff --git a/web/src/api/imAbuseReason/index.ts b/web/src/api/imAbuseReason/index.ts new file mode 100644 index 0000000..acfc94d --- /dev/null +++ b/web/src/api/imAbuseReason/index.ts @@ -0,0 +1,13 @@ +import { http } from '@/utils/http/axios'; + +export function List(params) { + return http.request({ url: '/imAbuseReason/list', method: 'GET', params }); +} + +export function Edit(params) { + return http.request({ url: '/imAbuseReason/edit', method: 'POST', params }); +} + +export function Delete(params) { + return http.request({ url: '/imAbuseReason/delete', method: 'POST', params }); +} diff --git a/web/src/api/imAppRelease/index.ts b/web/src/api/imAppRelease/index.ts new file mode 100644 index 0000000..a77503b --- /dev/null +++ b/web/src/api/imAppRelease/index.ts @@ -0,0 +1,9 @@ +import { http } from '@/utils/http/axios'; + +export function List(params) { + return http.request({ url: '/imAppRelease/list', method: 'GET', params }); +} + +export function Edit(params) { + return http.request({ url: '/imAppRelease/edit', method: 'POST', params }); +} diff --git a/web/src/api/imFeedbackConfig/index.ts b/web/src/api/imFeedbackConfig/index.ts new file mode 100644 index 0000000..02ee4da --- /dev/null +++ b/web/src/api/imFeedbackConfig/index.ts @@ -0,0 +1,9 @@ +import { http } from '@/utils/http/axios'; + +export function Get(params) { + return http.request({ url: '/imFeedbackConfig/get', method: 'GET', params }); +} + +export function Update(params) { + return http.request({ url: '/imFeedbackConfig/update', method: 'POST', params }); +} diff --git a/web/src/api/imMailboxConfig/index.ts b/web/src/api/imMailboxConfig/index.ts new file mode 100644 index 0000000..eb49288 --- /dev/null +++ b/web/src/api/imMailboxConfig/index.ts @@ -0,0 +1,9 @@ +import { http } from '@/utils/http/axios'; + +export function Get(params) { + return http.request({ url: '/imMailboxConfig/get', method: 'GET', params }); +} + +export function Update(params) { + return http.request({ url: '/imMailboxConfig/update', method: 'POST', params }); +} diff --git a/web/src/api/imWorkbench/index.ts b/web/src/api/imWorkbench/index.ts new file mode 100644 index 0000000..38b8847 --- /dev/null +++ b/web/src/api/imWorkbench/index.ts @@ -0,0 +1,17 @@ +import { http } from '@/utils/http/axios'; + +export function List(params) { + return http.request({ url: '/imWorkbench/list', method: 'GET', params }); +} + +export function Edit(params) { + return http.request({ url: '/imWorkbench/edit', method: 'POST', params }); +} + +export function Delete(params) { + return http.request({ url: '/imWorkbench/delete', method: 'POST', params }); +} + +export function Reorder(params) { + return http.request({ url: '/imWorkbench/reorder', method: 'POST', params }); +} diff --git a/web/src/views/imAbuseReason/index.vue b/web/src/views/imAbuseReason/index.vue new file mode 100644 index 0000000..8028f66 --- /dev/null +++ b/web/src/views/imAbuseReason/index.vue @@ -0,0 +1,261 @@ + + + diff --git a/web/src/views/imAbuseReason/model.ts b/web/src/views/imAbuseReason/model.ts new file mode 100644 index 0000000..8fd54f3 --- /dev/null +++ b/web/src/views/imAbuseReason/model.ts @@ -0,0 +1,149 @@ +import { computed, ref } from 'vue'; +import { cloneDeep } from 'lodash-es'; +import { FormSchema } from '@/components/Form'; +import { useDictStore } from '@/store/modules/dict'; +import { List as TenantList } from '@/api/imTenant'; + +const dict = useDictStore(); + +export const tenantOptions = ref([]); + +export class State { + public tenantId = 0; + public tenantName = ''; + public id = 0; + public targetType = 'message'; + public reasonCode = ''; + public label = ''; + public requiresDescription = 0; + public allowsEvidence = 1; + public sortOrder = 0; + public enabled = 1; + public updatedAt = ''; + + constructor(state?: Partial) { + if (state) Object.assign(this, state); + } +} + +export function newState(state: State | Record | null): State { + if (state !== null) { + if (state instanceof State) return cloneDeep(state); + return new State(state); + } + return new State(); +} + +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { + clearable: false, + options: computed(() => tenantOptions.value), + placeholder: '请选择租户', + }, + }, + { + field: 'targetType', + component: 'NSelect', + label: '对象', + componentProps: { + clearable: true, + options: computed(() => dict.getOptions('ImAbuseTargetTypeOptions')), + placeholder: '请选择对象类型', + }, + }, + { + field: 'reasonCode', + component: 'NInput', + label: '编码', + componentProps: { + clearable: true, + placeholder: '请输入原因编码', + }, + }, + { + field: 'enabled', + component: 'NSelect', + label: '状态', + componentProps: { + clearable: true, + options: computed(() => dict.getOptions('ImAbuseReasonEnabledOptions')), + placeholder: '请选择状态', + }, + }, +]); + +export const columns = [ + { + title: '对象', + key: 'targetType', + align: 'left', + width: 100, + render(row: State) { + return dict.getLabel('ImAbuseTargetTypeOptions', row.targetType) || row.targetType; + }, + }, + { title: '编码', key: 'reasonCode', align: 'left', width: 140 }, + { title: '文案', key: 'label', align: 'left', width: 160 }, + { + title: '要求描述', + key: 'requiresDescription', + align: 'left', + width: 100, + render(row: State) { + return dict.getLabel('ImAbuseReasonEnabledOptions', row.requiresDescription); + }, + }, + { + title: '允许证据', + key: 'allowsEvidence', + align: 'left', + width: 100, + render(row: State) { + return dict.getLabel('ImAbuseReasonEnabledOptions', row.allowsEvidence); + }, + }, + { title: '排序', key: 'sortOrder', align: 'left', width: 90 }, + { + title: '状态', + key: 'enabled', + align: 'left', + width: 90, + render(row: State) { + return dict.getLabel('ImAbuseReasonEnabledOptions', row.enabled); + }, + }, + { title: '更新时间', key: 'updatedAt', align: 'left', width: 180 }, +]; + +export const rules = { + targetType: { + required: true, + trigger: ['blur', 'change'], + type: 'string', + message: '请选择对象类型', + }, + reasonCode: { + required: true, + trigger: ['blur', 'input'], + type: 'string', + message: '请输入原因编码', + }, + label: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入展示文案' }, +}; + +export function loadOptions() { + dict.loadOptions(['ImAbuseTargetTypeOptions', 'ImAbuseReasonEnabledOptions']); +} + +export async function loadTenantOptions() { + const res = await TenantList({ page: 1, perPage: 100 }); + tenantOptions.value = (res?.list || []).map((item) => ({ + label: `${item.name}(${item.code})`, + value: item.id, + })); + return tenantOptions.value; +} diff --git a/web/src/views/imAppRelease/index.vue b/web/src/views/imAppRelease/index.vue new file mode 100644 index 0000000..68f2df7 --- /dev/null +++ b/web/src/views/imAppRelease/index.vue @@ -0,0 +1,242 @@ + + + diff --git a/web/src/views/imAppRelease/model.ts b/web/src/views/imAppRelease/model.ts new file mode 100644 index 0000000..caded7d --- /dev/null +++ b/web/src/views/imAppRelease/model.ts @@ -0,0 +1,136 @@ +import { computed, ref } from 'vue'; +import { cloneDeep } from 'lodash-es'; +import { FormSchema } from '@/components/Form'; +import { useDictStore } from '@/store/modules/dict'; +import { List as TenantList } from '@/api/imTenant'; + +const dict = useDictStore(); + +export const tenantOptions = ref([]); + +export class State { + public tenantId = 0; + public tenantName = ''; + public id = 0; + public platform = 'android'; + public channel = 'stable'; + public latestVersion = '1.0.0'; + public minSupportedVersion = '1.0.0'; + public updateType = 'none'; + public releaseNotes = ''; + public storeUrl = ''; + public packageUrl = ''; + public checksum = ''; + public remindAfterSeconds = 0; + public publishedAt = ''; + public updatedAt = ''; + + constructor(state?: Partial) { + if (state) Object.assign(this, state); + } +} + +export function newState(state: State | Record | null): State { + if (state !== null) { + if (state instanceof State) return cloneDeep(state); + return new State(state); + } + return new State(); +} + +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { + clearable: false, + options: computed(() => tenantOptions.value), + placeholder: '请选择租户', + }, + }, + { + field: 'platform', + component: 'NSelect', + label: '平台', + componentProps: { + clearable: true, + options: computed(() => dict.getOptions('ImAppPlatformOptions')), + placeholder: '请选择平台', + }, + }, + { + field: 'channel', + component: 'NInput', + label: '渠道', + componentProps: { + clearable: true, + placeholder: '请输入渠道', + }, + }, +]); + +export const columns = [ + { + title: '平台', + key: 'platform', + align: 'left', + width: 100, + render(row: State) { + return dict.getLabel('ImAppPlatformOptions', row.platform) || row.platform; + }, + }, + { title: '渠道', key: 'channel', align: 'left', width: 100 }, + { title: '最新版本', key: 'latestVersion', align: 'left', width: 120 }, + { title: '最低支持', key: 'minSupportedVersion', align: 'left', width: 120 }, + { + title: '更新类型', + key: 'updateType', + align: 'left', + width: 110, + render(row: State) { + return dict.getLabel('ImAppUpdateTypeOptions', row.updateType) || row.updateType; + }, + }, + { title: '再次提醒', key: 'remindAfterSeconds', align: 'left', width: 110 }, + { title: '商店地址', key: 'storeUrl', align: 'left', width: 220 }, + { title: '安装包地址', key: 'packageUrl', align: 'left', width: 220 }, + { title: '发布时间', key: 'publishedAt', align: 'left', width: 180 }, + { title: '更新时间', key: 'updatedAt', align: 'left', width: 180 }, +]; + +export const rules = { + platform: { required: true, trigger: ['blur', 'change'], type: 'string', message: '请选择平台' }, + channel: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入渠道' }, + latestVersion: { + required: true, + trigger: ['blur', 'input'], + type: 'string', + message: '请输入最新版本', + }, + minSupportedVersion: { + required: true, + trigger: ['blur', 'input'], + type: 'string', + message: '请输入最低支持版本', + }, + updateType: { + required: true, + trigger: ['blur', 'change'], + type: 'string', + message: '请选择更新类型', + }, +}; + +export function loadOptions() { + dict.loadOptions(['ImAppPlatformOptions', 'ImAppUpdateTypeOptions']); +} + +export async function loadTenantOptions() { + const res = await TenantList({ page: 1, perPage: 100 }); + tenantOptions.value = (res?.list || []).map((item) => ({ + label: `${item.name}(${item.code})`, + value: item.id, + })); + return tenantOptions.value; +} diff --git a/web/src/views/imFeedbackConfig/index.vue b/web/src/views/imFeedbackConfig/index.vue new file mode 100644 index 0000000..7dde42f --- /dev/null +++ b/web/src/views/imFeedbackConfig/index.vue @@ -0,0 +1,140 @@ + + + diff --git a/web/src/views/imFeedbackConfig/model.ts b/web/src/views/imFeedbackConfig/model.ts new file mode 100644 index 0000000..0564e25 --- /dev/null +++ b/web/src/views/imFeedbackConfig/model.ts @@ -0,0 +1,58 @@ +import { ref } from 'vue'; +import { cloneDeep } from 'lodash-es'; +import { List as TenantList } from '@/api/imTenant'; +import { useDictStore } from '@/store/modules/dict'; + +export const tenantOptions = ref([]); + +export class State { + public tenantId = 0; + public tenantName = ''; + public id = 1; + public categories = '["bug","suggestion","complaint","other"]'; + public maxLength = 1000; + public attachmentPolicy = '{"max_files":5,"purpose":"feedback"}'; + public allowContact = 1; + public diagnosticsNotice = '仅在你同意时上传脱敏诊断信息。'; + public updatedAt = ''; + + constructor(state?: Partial) { + if (state) Object.assign(this, state); + } +} + +export function newState(state: State | Record | null): State { + if (state !== null) { + if (state instanceof State) return cloneDeep(state); + return new State(state); + } + return new State(); +} + +export const rules = { + diagnosticsNotice: { + required: true, + trigger: ['blur', 'input'], + type: 'string', + message: '请输入诊断提示', + }, + maxLength: { + required: true, + trigger: ['blur', 'input'], + type: 'number', + message: '请输入最大字数', + }, +}; + +export function loadOptions() { + useDictStore().loadOptions(['ImFilePolicySwitchOptions']); +} + +export async function loadTenantOptions() { + const res = await TenantList({ page: 1, perPage: 100 }); + tenantOptions.value = (res?.list || []).map((item) => ({ + label: `${item.name}(${item.code})`, + value: item.id, + })); + return tenantOptions.value; +} diff --git a/web/src/views/imMailboxConfig/index.vue b/web/src/views/imMailboxConfig/index.vue new file mode 100644 index 0000000..bfad819 --- /dev/null +++ b/web/src/views/imMailboxConfig/index.vue @@ -0,0 +1,152 @@ + + + diff --git a/web/src/views/imMailboxConfig/model.ts b/web/src/views/imMailboxConfig/model.ts new file mode 100644 index 0000000..f1c7b2e --- /dev/null +++ b/web/src/views/imMailboxConfig/model.ts @@ -0,0 +1,67 @@ +import { ref } from 'vue'; +import { cloneDeep } from 'lodash-es'; +import { List as TenantList } from '@/api/imTenant'; +import { useDictStore } from '@/store/modules/dict'; + +export const tenantOptions = ref([]); + +export class State { + public tenantId = 0; + public tenantName = ''; + public id = 1; + public enabled = 1; + public allowAnonymous = 1; + public requiredFields = '["subject","content"]'; + public maxLength = 5000; + public attachmentPolicy = '{"max_files":5,"purpose":"executive_mailbox"}'; + public noticeTitle = '总经理信箱'; + public noticeContent = '请如实填写诉求,匿名提交将按企业政策保护展示身份。'; + public noticeUpdatedAt = ''; + public updatedAt = ''; + + constructor(state?: Partial) { + if (state) Object.assign(this, state); + } +} + +export function newState(state: State | Record | null): State { + if (state !== null) { + if (state instanceof State) return cloneDeep(state); + return new State(state); + } + return new State(); +} + +export const rules = { + noticeTitle: { + required: true, + trigger: ['blur', 'input'], + type: 'string', + message: '请输入告知标题', + }, + noticeContent: { + required: true, + trigger: ['blur', 'input'], + type: 'string', + message: '请输入告知内容', + }, + maxLength: { + required: true, + trigger: ['blur', 'input'], + type: 'number', + message: '请输入最大字数', + }, +}; + +export function loadOptions() { + useDictStore().loadOptions(['ImFilePolicySwitchOptions']); +} + +export async function loadTenantOptions() { + const res = await TenantList({ page: 1, perPage: 100 }); + tenantOptions.value = (res?.list || []).map((item) => ({ + label: `${item.name}(${item.code})`, + value: item.id, + })); + return tenantOptions.value; +} diff --git a/web/src/views/imWorkbench/index.vue b/web/src/views/imWorkbench/index.vue new file mode 100644 index 0000000..043f6bd --- /dev/null +++ b/web/src/views/imWorkbench/index.vue @@ -0,0 +1,291 @@ + + + diff --git a/web/src/views/imWorkbench/model.ts b/web/src/views/imWorkbench/model.ts new file mode 100644 index 0000000..16d8baa --- /dev/null +++ b/web/src/views/imWorkbench/model.ts @@ -0,0 +1,143 @@ +import { computed, ref } from 'vue'; +import { cloneDeep } from 'lodash-es'; +import { FormSchema } from '@/components/Form'; +import { useDictStore } from '@/store/modules/dict'; +import { List as TenantList } from '@/api/imTenant'; + +const dict = useDictStore(); + +export const tenantOptions = ref([]); + +export class State { + public tenantId = 0; + public tenantName = ''; + public id = 0; + public code = ''; + public name = ''; + public iconFileId = ''; + public route = ''; + public sortOrder = 0; + public unreadKey = ''; + public enabled = 1; + public permission = 'enabled'; + public createdAt = ''; + public updatedAt = ''; + + constructor(state?: Partial) { + if (state) Object.assign(this, state); + } +} + +export function newState(state: State | Record | null): State { + if (state !== null) { + if (state instanceof State) return cloneDeep(state); + return new State(state); + } + return new State(); +} + +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { + clearable: false, + options: computed(() => tenantOptions.value), + placeholder: '请选择租户', + }, + }, + { + field: 'code', + component: 'NInput', + label: '编码', + componentProps: { + clearable: true, + placeholder: '请输入模块编码', + }, + }, + { + field: 'name', + component: 'NInput', + label: '名称', + componentProps: { + clearable: true, + placeholder: '请输入模块名称', + }, + }, + { + field: 'enabled', + component: 'NSelect', + label: '状态', + componentProps: { + clearable: true, + options: computed(() => dict.getOptions('ImWorkbenchAppEnabledOptions')), + placeholder: '请选择状态', + }, + }, +]); + +export const columns = [ + { title: '编码', key: 'code', align: 'left', width: 150 }, + { title: '名称', key: 'name', align: 'left', width: 140 }, + { + title: '路由', + key: 'route', + align: 'left', + width: 160, + render(row: State) { + return dict.getLabel('ImWorkbenchRouteOptions', row.route) || row.route; + }, + }, + { title: '排序', key: 'sortOrder', align: 'left', width: 90 }, + { title: '图标ID', key: 'iconFileId', align: 'left', width: 150 }, + { title: '角标Key', key: 'unreadKey', align: 'left', width: 130 }, + { + title: '状态', + key: 'enabled', + align: 'left', + width: 90, + render(row: State) { + return dict.getLabel('ImWorkbenchAppEnabledOptions', row.enabled); + }, + }, + { + title: '权限', + key: 'permission', + align: 'left', + width: 120, + render(row: State) { + return dict.getLabel('ImWorkbenchPermissionOptions', row.permission) || row.permission; + }, + }, + { title: '更新时间', key: 'updatedAt', align: 'left', width: 180 }, +]; + +export const rules = { + code: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入模块编码' }, + name: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入模块名称' }, + route: { required: true, trigger: ['blur', 'change'], type: 'string', message: '请选择路由' }, + permission: { + required: true, + trigger: ['blur', 'change'], + type: 'string', + message: '请选择权限策略', + }, +}; + +export function loadOptions() { + dict.loadOptions([ + 'ImWorkbenchAppEnabledOptions', + 'ImWorkbenchPermissionOptions', + 'ImWorkbenchRouteOptions', + ]); +} + +export async function loadTenantOptions() { + const res = await TenantList({ page: 1, perPage: 100 }); + tenantOptions.value = (res?.list || []).map((item) => ({ + label: `${item.name}(${item.code})`, + value: item.id, + })); + return tenantOptions.value; +}