feat: initialize shared tenant workbench data

This commit is contained in:
richjohnrambo
2026-09-23 17:04:30 +08:00
parent 5a07f63716
commit abadcceb19
8 changed files with 122 additions and 18 deletions
+5 -5
View File
@@ -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)
}
+17 -3
View File
@@ -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)
@@ -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)
}
}
@@ -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})
@@ -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})
+9
View File
@@ -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}).
@@ -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
}
+4 -4
View File
@@ -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工作台模块排序失败,请稍后重试!")
}