diff --git a/server/internal/library/imtenant/db_test.go b/server/internal/library/imtenant/db_test.go index cbabad0..d668583 100644 --- a/server/internal/library/imtenant/db_test.go +++ b/server/internal/library/imtenant/db_test.go @@ -7,11 +7,11 @@ import ( ) func TestNormalizeMySQLTimeOptions(t *testing.T) { - link, err := normalizeMySQLTimeOptions("user:pass@tcp(127.0.0.1:3306)/tinode?charset=utf8mb4") + link, err := normalizeMySQLTimeOptions("user:pass@tcp(127.0.0.1:3306)/meeting_im?charset=utf8mb4") if err != nil { t.Fatalf("normalize raw mysql dsn: %v", err) } - if !strings.HasPrefix(link, "mysql:user:pass@tcp(127.0.0.1:3306)/tinode?") { + if !strings.HasPrefix(link, "mysql:user:pass@tcp(127.0.0.1:3306)/meeting_im?") { t.Fatalf("unexpected normalized link: %s", link) } if !strings.Contains(link, "loc=UTC") || !strings.Contains(link, "parseTime=true") { @@ -20,17 +20,17 @@ func TestNormalizeMySQLTimeOptions(t *testing.T) { } func TestNormalizeMySQLTimeOptionsRejectsUnsupportedFormat(t *testing.T) { - if _, err := normalizeMySQLTimeOptions("postgres://user:pass@127.0.0.1:5432/tinode"); err == nil { + if _, err := normalizeMySQLTimeOptions("postgres://user:pass@127.0.0.1:5432/meeting_im"); err == nil { t.Fatal("expected unsupported format error") } } func TestReplaceMySQLDatabaseUsesUTC(t *testing.T) { - link, err := replaceMySQLDatabase("mysql:user:pass@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=false&charset=utf8mb4", "tinode") + link, err := replaceMySQLDatabase("mysql:user:pass@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=false&charset=utf8mb4", "meeting_im") if err != nil { t.Fatal(err) } - for _, expected := range []string{"/tinode?", "loc=UTC", "parseTime=true", "charset=utf8mb4"} { + for _, expected := range []string{"/meeting_im?", "loc=UTC", "parseTime=true", "charset=utf8mb4"} { if !strings.Contains(link, expected) { t.Fatalf("link %q does not contain %q", link, expected) } diff --git a/server/internal/library/imworkbench/client.go b/server/internal/library/imworkbench/client.go index 3c3b9ed..e16c9b3 100644 --- a/server/internal/library/imworkbench/client.go +++ b/server/internal/library/imworkbench/client.go @@ -65,27 +65,41 @@ func DefaultClient(ctx context.Context) *Client { } func (c *Client) Get(ctx context.Context, path string, values url.Values, out any) error { + return c.GetForTenant(ctx, path, values, 0, "", out) +} + +func (c *Client) GetForTenant(ctx context.Context, path string, values url.Values, tenantID int64, tenantCode string, out any) error { if values != nil { if query := values.Encode(); query != "" { path += "?" + query } } - return c.doJSON(ctx, http.MethodGet, path, nil, out) + return c.doJSON(ctx, http.MethodGet, path, nil, tenantID, tenantCode, out) } func (c *Client) Post(ctx context.Context, path string, in any, out any) error { + return c.PostForTenant(ctx, path, in, 0, "", out) +} + +func (c *Client) PostForTenant(ctx context.Context, path string, in any, tenantID int64, tenantCode string, out any) error { body, err := json.Marshal(in) if err != nil { return gerror.Wrap(err, "序列化工作台请求失败") } - return c.doJSON(ctx, http.MethodPost, path, body, out) + return c.doJSON(ctx, http.MethodPost, path, body, tenantID, tenantCode, out) } -func (c *Client) doJSON(ctx context.Context, method string, path string, body []byte, out any) error { +func (c *Client) doJSON(ctx context.Context, method string, path string, body []byte, tenantID int64, tenantCode string, out any) error { req, err := c.newRequest(ctx, method, path, body) if err != nil { return err } + if tenantID > 0 { + req.Header.Set("X-Tenant-ID", strconv.FormatInt(tenantID, 10)) + } + if tenantCode = strings.TrimSpace(tenantCode); tenantCode != "" { + req.Header.Set("X-Tenant-Code", tenantCode) + } resp, err := c.http.Do(req) if err != nil { g.Log().Warningf(ctx, "IM workbench request failed url:%s err:%+v", req.URL.String(), err) diff --git a/server/internal/library/imworkbench/client_test.go b/server/internal/library/imworkbench/client_test.go new file mode 100644 index 0000000..54a81dd --- /dev/null +++ b/server/internal/library/imworkbench/client_test.go @@ -0,0 +1,37 @@ +package imworkbench + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestPostForTenantAddsTenantHeaders(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("X-Tenant-ID"); got != "23" { + t.Fatalf("X-Tenant-ID = %q, want 23", got) + } + if got := r.Header.Get("X-Tenant-Code"); got != "merchant-23" { + t.Fatalf("X-Tenant-Code = %q, want merchant-23", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":0,"data":{"tenant_id":23}}`)) + })) + defer server.Close() + + client := &Client{ + conf: Config{BaseURL: server.URL, ServiceName: "test", Secret: "secret", Timeout: time.Second}, + http: server.Client(), + } + var response struct { + TenantID int64 `json:"tenant_id"` + } + if err := client.PostForTenant(context.Background(), "/initialize", map[string]any{"tenant_id": 23}, 23, "merchant-23", &response); err != nil { + t.Fatal(err) + } + if response.TenantID != 23 { + t.Fatalf("tenant_id = %d, want 23", response.TenantID) + } +} diff --git a/server/internal/logic/sys/im_feedback_config.go b/server/internal/logic/sys/im_feedback_config.go index 54a5706..c5a8419 100644 --- a/server/internal/logic/sys/im_feedback_config.go +++ b/server/internal/logic/sys/im_feedback_config.go @@ -26,7 +26,7 @@ func (s *sSysImFeedbackConfig) Get(ctx context.Context, in *sysin.ImFeedbackConf return nil, err } res = &sysin.ImFeedbackConfigModel{} - if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/feedback-config", nil, res); err != nil { + if err = imworkbench.DefaultClient(ctx).GetForTenant(ctx, "/internal/admin/workbench/feedback-config", nil, tenant.Id, tenant.Code, res); err != nil { return nil, err } if res != nil { @@ -52,13 +52,13 @@ func (s *sSysImFeedbackConfig) Update(ctx context.Context, in *sysin.ImFeedbackC if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" { attachmentPolicy = in.AttachmentPolicy.String() } - if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/feedback-config", g.Map{ + if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/feedback-config", g.Map{ "categories": categories, "max_length": in.MaxLength, "attachment_policy": attachmentPolicy, "allow_contact": in.AllowContact, "diagnostics_notice": in.DiagnosticsNotice, - }, nil); err != nil { + }, tenant.Id, tenant.Code, nil); err != nil { return err } _ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact}) diff --git a/server/internal/logic/sys/im_mailbox_config.go b/server/internal/logic/sys/im_mailbox_config.go index 8133a0d..a2beaf4 100644 --- a/server/internal/logic/sys/im_mailbox_config.go +++ b/server/internal/logic/sys/im_mailbox_config.go @@ -26,7 +26,7 @@ func (s *sSysImMailboxConfig) Get(ctx context.Context, in *sysin.ImMailboxConfig return nil, err } res = &sysin.ImMailboxConfigModel{} - if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/mailbox-config", nil, res); err != nil { + if err = imworkbench.DefaultClient(ctx).GetForTenant(ctx, "/internal/admin/workbench/mailbox-config", nil, tenant.Id, tenant.Code, res); err != nil { return nil, err } if res != nil { @@ -52,7 +52,7 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" { attachmentPolicy = in.AttachmentPolicy.String() } - if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/mailbox-config", g.Map{ + if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/mailbox-config", g.Map{ "enabled": in.Enabled, "allow_anonymous": in.AllowAnonymous, "required_fields": requiredFields, @@ -60,7 +60,7 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon "attachment_policy": attachmentPolicy, "notice_title": in.NoticeTitle, "notice_content": in.NoticeContent, - }, nil); err != nil { + }, tenant.Id, tenant.Code, nil); err != nil { return err } _ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled}) diff --git a/server/internal/logic/sys/im_tenant.go b/server/internal/logic/sys/im_tenant.go index 0a84235..1eaa974 100644 --- a/server/internal/logic/sys/im_tenant.go +++ b/server/internal/logic/sys/im_tenant.go @@ -230,6 +230,9 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp if err = provisionTenantMessageTables(ctx, tenantID); err != nil { return gerror.Wrap(err, "商户已登记为初始化中,但业务分表创建失败") } + if err = initializeTenantWorkbench(ctx, tenantID, in.Code); err != nil { + return gerror.Wrap(err, "商户业务分表已创建,但工作台初始化失败") + } if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).WherePri(tenantID).Data(g.Map{ dao.ImTenant.Columns().State: requestedState, dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx), @@ -515,6 +518,9 @@ func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp) if err = provisionTenantMessageTables(ctx, in.Id); err != nil { return gerror.Wrap(err, "启用商户前创建业务分表失败") } + if err = initializeTenantWorkbench(ctx, in.Id, ""); err != nil { + return gerror.Wrap(err, "启用商户前初始化工作台失败") + } } if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{ dao.ImTenant.Columns().State: in.State, @@ -650,6 +656,9 @@ func (s *sSysImTenant) Restore(ctx context.Context, in *sysin.ImTenantLifecycleI if err = provisionTenantMessageTables(ctx, in.Id); err != nil { return gerror.Wrap(err, "恢复商户前创建业务分表失败") } + if err = initializeTenantWorkbench(ctx, in.Id, ""); err != nil { + return gerror.Wrap(err, "恢复商户前初始化工作台失败") + } if _, err = s.Model(ctx). WherePri(in.Id). WhereIn(dao.ImTenant.Columns().State, []int{consts.ImTenantStateSuspended, consts.ImTenantStateDeleting}). diff --git a/server/internal/logic/sys/im_tenant_workbench.go b/server/internal/logic/sys/im_tenant_workbench.go new file mode 100644 index 0000000..96ca227 --- /dev/null +++ b/server/internal/logic/sys/im_tenant_workbench.go @@ -0,0 +1,44 @@ +package sys + +import ( + "context" + "hotgo/internal/dao" + "hotgo/internal/library/hgorm/handler" + "hotgo/internal/library/imworkbench" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" +) + +func initializeTenantWorkbench(ctx context.Context, tenantID int64, tenantCode string) error { + if tenantID <= 0 { + return gerror.New("商户ID不能为空") + } + if tenantCode == "" { + value, err := NewSysImTenant().Model(ctx, &handler.Option{FilterAuth: false}). + Fields(dao.ImTenant.Columns().Code). + WherePri(tenantID). + Value() + if err != nil { + return gerror.Wrap(err, "查询商户编码失败") + } + tenantCode = value.String() + if tenantCode == "" { + return gerror.New("商户不存在") + } + } + + var result struct { + TenantID int64 `json:"tenant_id"` + } + if err := imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/tenants/initialize", g.Map{ + "tenant_id": tenantID, + "tenant_code": tenantCode, + }, tenantID, tenantCode, &result); err != nil { + return err + } + if result.TenantID != tenantID { + return gerror.New("工作台初始化返回的商户ID不一致") + } + return nil +} diff --git a/server/internal/logic/sys/im_workbench.go b/server/internal/logic/sys/im_workbench.go index 8a0e04b..5106b7f 100644 --- a/server/internal/logic/sys/im_workbench.go +++ b/server/internal/logic/sys/im_workbench.go @@ -40,7 +40,7 @@ func (s *sSysImWorkbench) List(ctx context.Context, in *sysin.ImWorkbenchAppList values.Set("enabled", intToString(*in.Enabled)) } var page imworkbench.Page[sysin.ImWorkbenchAppModel] - if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/apps", values, &page); err != nil { + if err = imworkbench.DefaultClient(ctx).GetForTenant(ctx, "/internal/admin/workbench/apps", values, tenant.Id, tenant.Code, &page); err != nil { return nil, 0, err } list = make([]*sysin.ImWorkbenchAppModel, 0, len(page.Items)) @@ -78,7 +78,7 @@ func (s *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEdit action = "im_workbench_app_update" } var res sysin.ImWorkbenchAppModel - if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps", data, &res); err != nil { + if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/apps", data, tenant.Id, tenant.Code, &res); err != nil { return err } if targetId <= 0 { @@ -98,7 +98,7 @@ func (s *sSysImWorkbench) Delete(ctx context.Context, in *sysin.ImWorkbenchAppDe if err != nil { return } - if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps/disable", g.Map{"id": in.Id, "code": in.Code}, nil); err != nil { + if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/apps/disable", g.Map{"id": in.Id, "code": in.Code}, tenant.Id, tenant.Code, nil); err != nil { return err } _ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code}) @@ -120,7 +120,7 @@ func (s *sSysImWorkbench) Reorder(ctx context.Context, in *sysin.ImWorkbenchAppR "sort_order": item.SortOrder, }) } - if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps/reorder", g.Map{"items": items}, nil); err != nil { + if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/apps/reorder", g.Map{"items": items}, tenant.Id, tenant.Code, nil); err != nil { if err.Error() == "" { return gerror.New("调整IM工作台模块排序失败,请稍后重试!") }