Skip to content

Commit ae1b51e

Browse files
authored
feat(common): expose docs_on appconfig toggle (#536)
## Background A new `octo-docs-backend` module service is being introduced (not live yet). Clients need a signal to decide whether to surface the docs module entry. The client reads `/v1/common/appconfig`; without a dedicated field it can only fall back to a hardcoded default. This PR exposes that signal from appconfig. ## Changes Adds a `docs_on` boolean to `GET /v1/common/appconfig`, mirroring the existing `sticker_custom_enabled` toggle pattern exactly: | File | Change | |---|---| | `modules/common/system_settings.go` | New getter `DocsEnabled()` backed by `system_setting docs.enabled`, default `false` | | `modules/common/system_setting_schema.go` | Register `docs.enabled` (bool) so it is admin-tunable and converges across replicas via the settings snapshot | | `modules/common/api.go` | Add `DocsOn bool json:"docs_on"` to `appConfigResp`; emitted in **both** return branches (including the version short-circuit) | | `modules/common/api_test.go` | Integration tests: default false / DB true / version short-circuit | | `modules/common/system_settings_test.go` | Getter unit tests: default false / DB true | ## Design notes - **Default `false`**: `octo-docs-backend` is not live, so the entry stays hidden. Ops flips `docs.enabled` to `1` from the admin console for a controlled rollout — no redeploy/restart required. - **Decoupled from `app_config.version`**: the version short-circuit branch also emits `docs_on`, otherwise clients that hit the cached-version path would never receive the latest value. Same invariant already applied to `LocalLoginOff` / `SearchEnabled` / `StickerCustomEnabled`. - **Presentation toggle only**: it gates client-side display of the docs entry and carries no server-side authorization. ## Client adaptation - Parse as a bool (not `0/1`); default to `false` (hidden) when the field is absent. - Always take the latest value from appconfig, even when a local version cache is hit. ## Testing Go toolchain not available locally; verified via a `golang:1.25` container against the local module cache: - `go build ./modules/common/...` - `go vet ./modules/common/...` - `go test -c` (test binary compiles) Integration tests require MySQL/Redis/WuKongIM and were not run locally — left to CI. ## Test plan - [ ] CI Build / Test green - [ ] `docs_on=false` when `docs.enabled` is unset - [ ] `docs_on=true` after setting `docs.enabled=1` (including requests carrying `version`) Co-authored-by: an9xyz <an9xyz@users.noreply.github.com>
1 parent 232ffaa commit ae1b51e

5 files changed

Lines changed: 105 additions & 0 deletions

File tree

modules/common/api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ func (cn *Common) appConfig(c *wkhttp.Context) {
389389
MessagesSearchOn: searchEnabled,
390390
StickerCustomEnabled: cn.systemSettings.StickerCustomEnabled(),
391391
StickerHandleRequired: cn.systemSettings.StickerHandleRequired(),
392+
DocsOn: cn.systemSettings.DocsEnabled(),
392393
})
393394
return
394395
}
@@ -431,6 +432,7 @@ func (cn *Common) appConfig(c *wkhttp.Context) {
431432
MessagesSearchOn: searchEnabled,
432433
StickerCustomEnabled: cn.systemSettings.StickerCustomEnabled(),
433434
StickerHandleRequired: cn.systemSettings.StickerHandleRequired(),
435+
DocsOn: cn.systemSettings.DocsEnabled(),
434436
})
435437
}
436438

@@ -781,6 +783,14 @@ type appConfigResp struct {
781783
// 客户端命中 version 短路分支也必须拿到最新值,否则被本地缓存住失去实时性,故两个
782784
// 分支都下发。
783785
StickerHandleRequired bool `json:"sticker_handle_required"`
786+
787+
// DocsOn 告知客户端是否展示文档(docs)模块入口。值来源于 system_setting
788+
// docs.enabled;默认 false —— 新增的 octo-docs-backend 服务尚未上线,先隐藏入口,
789+
// 上线后由管理台切 docs.enabled 灰度放开。本字段只表达展示策略,不承担服务端鉴权。
790+
//
791+
// 与 app_config.version 解耦的原因同 LocalLoginOff / SearchEnabled:运维切展示
792+
// 策略后老客户端命中 version 短路分支也必须拿到最新值,故两个分支都下发。
793+
DocsOn bool `json:"docs_on"`
784794
}
785795

786796
type oidcProviderResp struct {

modules/common/api_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ func setStickerCustomEnabledSetting(t *testing.T, ctx *config.Context, enabled b
9797
require.NoError(t, EnsureSystemSettings(ctx).Reload())
9898
}
9999

100+
// setDocsEnabledSetting upserts system_setting docs.enabled and reloads the
101+
// shared snapshot. This is the appconfig-facing display toggle for the docs
102+
// module (octo-docs-backend).
103+
func setDocsEnabledSetting(t *testing.T, ctx *config.Context, enabled bool) {
104+
t.Helper()
105+
v := "0"
106+
if enabled {
107+
v = "1"
108+
}
109+
_, err := ctx.DB().InsertInto("system_setting").
110+
Columns("category", "key_name", "value", "value_type").
111+
Values("docs", "enabled", v, "bool").Exec()
112+
require.NoError(t, err)
113+
require.NoError(t, EnsureSystemSettings(ctx).Reload())
114+
}
115+
100116
func TestAddVersion(t *testing.T) {
101117
t.Skip("OCTO migration TODO: see https://github.com/Mininglamp-OSS/octo-server/issues/17")
102118
s, ctx := testutil.NewTestServer()
@@ -741,3 +757,52 @@ func TestGetAppConfig_StickerCustomEnabled_OnVersionShortCircuit(t *testing.T) {
741757
assert.Equal(t, http.StatusOK, w.Code)
742758
assert.Contains(t, w.Body.String(), `"sticker_custom_enabled":true`)
743759
}
760+
761+
// appconfig 必须下发 docs_on:值来源于 system_setting docs.enabled。默认 false,
762+
// 客户端据此隐藏 docs 模块入口(octo-docs-backend 上线前)。
763+
func TestGetAppConfig_DocsOn_DefaultFalse(t *testing.T) {
764+
s, ctx := testutil.NewTestServer()
765+
f := New(ctx)
766+
cleanAllTablesAndReloadSettings(t, ctx)
767+
err := f.appConfigDB.insert(&appConfigModel{})
768+
assert.NoError(t, err)
769+
w := httptest.NewRecorder()
770+
req, _ := http.NewRequest("GET", "/v1/common/appconfig", nil)
771+
req.Header.Set("token", testutil.Token)
772+
s.GetRoute().ServeHTTP(w, req)
773+
assert.Equal(t, http.StatusOK, w.Code)
774+
assert.Contains(t, w.Body.String(), `"docs_on":false`)
775+
}
776+
777+
// system_setting docs.enabled=true → appconfig 下发 true,客户端展示 docs 模块入口。
778+
func TestGetAppConfig_DocsOn_True(t *testing.T) {
779+
s, ctx := testutil.NewTestServer()
780+
f := New(ctx)
781+
cleanAllTablesAndReloadSettings(t, ctx)
782+
setDocsEnabledSetting(t, ctx, true)
783+
err := f.appConfigDB.insert(&appConfigModel{})
784+
assert.NoError(t, err)
785+
w := httptest.NewRecorder()
786+
req, _ := http.NewRequest("GET", "/v1/common/appconfig", nil)
787+
req.Header.Set("token", testutil.Token)
788+
s.GetRoute().ServeHTTP(w, req)
789+
assert.Equal(t, http.StatusOK, w.Code)
790+
assert.Contains(t, w.Body.String(), `"docs_on":true`)
791+
}
792+
793+
// version 短路分支同样要下发 docs_on:展示开关需与 app_config.version 解耦,
794+
// 避免 admin 切换后老客户端命中版本短路而继续使用旧值。
795+
func TestGetAppConfig_DocsOn_OnVersionShortCircuit(t *testing.T) {
796+
s, ctx := testutil.NewTestServer()
797+
f := New(ctx)
798+
cleanAllTablesAndReloadSettings(t, ctx)
799+
setDocsEnabledSetting(t, ctx, true)
800+
err := f.appConfigDB.insert(&appConfigModel{})
801+
assert.NoError(t, err)
802+
w := httptest.NewRecorder()
803+
req, _ := http.NewRequest("GET", "/v1/common/appconfig?version=99999999", nil)
804+
req.Header.Set("token", testutil.Token)
805+
s.GetRoute().ServeHTTP(w, req)
806+
assert.Equal(t, http.StatusOK, w.Code)
807+
assert.Contains(t, w.Body.String(), `"docs_on":true`)
808+
}

modules/common/system_setting_schema.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ var systemSettingSchema = []settingDef{
147147
{Category: "sticker", Key: "handle_required", Type: settingTypeBool, Description: "新增自定义贴纸是否强制校验上传句柄 handle(关闭=兼容期放行缺失句柄并观测,开启=缺/伪造一律拒;需服务端配有效 OCTO_MASTER_KEY 才有校验能力)",
148148
Effective: func(s *SystemSettings) string { return boolToCanonical(s.StickerHandleRequired()) }},
149149

150+
// docs 模块展示开关(客户端据此决定是否展示 docs 入口)。新增的 octo-docs-backend
151+
// 服务尚未上线,默认关闭;上线后由管理台切 docs.enabled 灰度放量。仅表达展示策略,
152+
// 不承担任何服务端鉴权。经 GET /v1/common/appconfig 的 docs_on 下发给客户端。
153+
{Category: "docs", Key: "enabled", Type: settingTypeBool, Description: "是否向客户端展示文档(docs)模块入口(octo-docs-backend 上线前默认关闭)",
154+
Effective: func(s *SystemSettings) string { return boolToCanonical(s.DocsEnabled()) }},
155+
150156
// Email server config — formerly yaml-only (Support.* in config.go).
151157
{Category: "support", Key: "email", Type: settingTypeString, Description: "技术支持邮箱(发件人)",
152158
Effective: func(s *SystemSettings) string { return s.SupportEmail() }},

modules/common/system_settings.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,3 +776,13 @@ func (s *SystemSettings) StickerCustomEnabled() bool {
776776
func (s *SystemSettings) StickerHandleRequired() bool {
777777
return s.getBool("sticker", "handle_required", false)
778778
}
779+
780+
// DocsEnabled reports whether clients should surface the docs module (backed by
781+
// the new octo-docs-backend service). This is a presentation toggle only: it
782+
// gates client-side display of the docs entry and does not itself grant or
783+
// enforce any server-side authorization. Default false so the module stays
784+
// hidden until octo-docs-backend is live and the admin flips docs.enabled for a
785+
// controlled rollout. Value source: system_setting docs.enabled (DB, hot-reloaded).
786+
func (s *SystemSettings) DocsEnabled() bool {
787+
return s.getBool("docs", "enabled", false)
788+
}

modules/common/system_settings_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,17 @@ func TestSystemSettings_StickerCustomEnabled_DBTrueWins(t *testing.T) {
682682

683683
assert.True(t, s.StickerCustomEnabled(), "DB true -> custom sticker management enabled")
684684
}
685+
686+
func TestSystemSettings_DocsEnabled_DefaultsFalse(t *testing.T) {
687+
s := newTestSystemSettings(t, nil)
688+
689+
assert.False(t, s.DocsEnabled(), "DB empty -> docs module hidden by default")
690+
}
691+
692+
func TestSystemSettings_DocsEnabled_DBTrueWins(t *testing.T) {
693+
s := newTestSystemSettings(t, nil)
694+
require.NoError(t, s.db.upsert("docs", "enabled", "1", settingTypeBool, ""))
695+
require.NoError(t, s.Reload())
696+
697+
assert.True(t, s.DocsEnabled(), "DB true -> docs module shown")
698+
}

0 commit comments

Comments
 (0)