fix(cache): 存量空/url 策略规范为 all,避免静默收窄

评审修复:enabled 且 policy 为空或 url 时,写入/展示/快照/渲染均映射为 all,
保证旧站点宽缓存范围不变;新建 UI 仍显式提交 static 作为推荐默认。
This commit is contained in:
ryan
2026-07-19 00:02:52 +08:00
parent 04f029c705
commit 204f6d9a8b
11 changed files with 113 additions and 38 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ sidebar: false
### 变更
- 边缘缓存默认策略调整为「标准静态资源」:开启站点缓存后默认仅缓存 css/js/图片/字体等扩展名(不含 HTML);原「按 URL」行为保留为「所有可缓存 GET」。
- 边缘缓存默认策略调整为「标准静态资源」:新建开启缓存时推荐仅缓存 css/js/图片/字体等扩展名(不含 HTML);原「按 URL」与空策略存量行为保留为「所有可缓存 GET」。重新发布节点配置后生效。
- 优化访问日志概览及其他图表全局排行榜 (RankChart) 的样式布局:将每一项改为单行横向排布(左侧标签、中间进度条、右侧数值),数值支持自动格式化为 Compact 形式(如 39.57k、1.2M),同时调整默认高度为 320px 并隐藏滚动条。
- 访问日志页面重构为「概览」与「日志明细」两个标签页:概览展示请求量、访问量、带宽趋势与 Top Paths/Hosts/IPs,明细列表保留检索能力;已移除时间折叠与 IP 汇总视图。
- 边缘访问日志现支持上报并存储 User-Agent;概览新增设备类型饼图,以及浏览器、操作系统、User-Agent 排行。
+4 -3
View File
@@ -142,9 +142,10 @@ access.log cache_status=$upstream_cache_status
| 数据 | 处理 |
| --- | --- |
| DB 中 `cache_policy=''` 或 `url` | 读取/发布时规范为 `all`;可选一次性 SQL 更新为 `all` |
| 新建路由 | 默认 `cache_enabled=false`;若用户开启缓存,表单默认策略 **`static`** |
| 已开启且 `url` 的站点 | 迁移后为 `all`,**缓存范围不变** |
| DB 中 `cache_policy=''` 或 `url`(且已启用缓存) | 读取 / 快照 / 渲染均规范为 **`all`**,保证存量「宽缓存」不变 |
| API 写入时 `enabled` 且 policy 为空 | 规范为 **`all`**(兼容旧客户端);UI 新建开启时**显式提交** `static` |
| 新建路由 | 默认 `cache_enabled=false`;表单开启缓存时默认策略 **`static`** |
| 已开启且 `url` 的站点 | 显示与发布为 `all`,**缓存范围不变** |
| 期望「只缓存静态」的旧站点 | 用户在 UI 改为 `static` 或自定义 `suffix` |
**发布说明建议:** 说明默认策略变更仅影响**新配置**;存量 `url` 视为 `all`。
@@ -4,19 +4,35 @@
## 目标
路由开启缓存后默认仅缓存标准静态扩展名(`static`);存量 `url` 映射为 `all`。
路由开启缓存后,**新建推荐**仅缓存标准静态扩展名(`static`);存量 `url`/空策略映射为 `all`,不收窄缓存范围。
## 兼容规则(评审后定稿)
| 场景 | 行为 |
| --- | --- |
| 已启用 + `''` / `url` | 读 API / 快照 / 渲染 → **`all`** |
| 写入时 enabled 且 policy 为空 | 规范为 **`all`**(旧客户端兼容) |
| UI 新建/推荐默认 | **显式提交** `static` |
| 关闭缓存 | policy 存 `''`,rules 清空 |
## 修改清单
1. **渲染** `pkg/render/openresty/render.go`:`static` 内置扩展名;`url`/`all` 无路径限制
2. **校验** `internal/apps/openflare/proxy_route/helpers.go`:策略枚举与规范化
3. **前端** `cache-section.tsx` + helpers:默认 `static`,选项文案
4. **测试** render + helpers
5. **changelog**
1. **渲染** `pkg/render/openresty/render.go`:`static` 内置扩展名;空/`url`/`all` 无路径限制
2. **校验/展示** `proxy_route/helpers.go`:`normalizeCachePolicy` + `displayCachePolicy`
3. **快照** `config_version/logics.go`:`normalizeSnapshotCachePolicy`
4. **前端** `cache-section.tsx` + helpers:存量 empty/url→`all`;关闭时提交 `''`;新建默认 `static`
5. **测试** render + proxy_route
6. **设计/changelog** 同步兼容说明
## 验证
```bash
go test ./pkg/render/openresty/ ./internal/apps/openflare/proxy_route/
make code-check # 或至少 go test + frontend tsc
go test ./pkg/render/openresty/ ./internal/apps/openflare/proxy_route/ ./internal/apps/openflare/config_version/
# 已通过(2026-07-18)
```
## 状态
- [x] 功能实现 + 评审修复(empty→all,禁止静默收窄)
- [ ] 提交 `fix(cache): ...`(待用户确认)
- [ ] 合并 / 发布后需重新发布节点配置
@@ -335,12 +335,17 @@ export function buildPayloadFromRoute(
limit_conn_per_ip: route.limit_conn_per_ip,
limit_rate: route.limit_rate,
cache_enabled: route.cache_enabled,
cache_policy:
!route.cache_policy || route.cache_policy === 'url'
? route.cache_policy === 'url'
? 'all'
: 'static'
: route.cache_policy,
cache_policy: (() => {
if (!route.cache_enabled) {
return 'static';
}
const policy = (route.cache_policy || '').trim();
// Legacy empty/url → all (same as backend displayCachePolicy).
if (!policy || policy === 'url' || policy === 'all') {
return 'all';
}
return policy;
})(),
cache_rules: route.cache_rule_list ?? [],
custom_headers: route.custom_header_list ?? [],
basic_auth_enabled: route.basic_auth_enabled,
@@ -39,7 +39,6 @@ const cacheSchema = z
cache_policy: z.enum([
'static',
'all',
'url',
'suffix',
'path_prefix',
'path_exact',
@@ -70,10 +69,17 @@ interface CacheSectionProps {
onSavingChange?: (saving: boolean) => void;
}
function normalizeCachePolicyValue(policy: string | undefined | null) {
/** Map API/DB values for the form. Legacy empty/url → all (compat). */
function normalizeCachePolicyValue(
policy: string | undefined | null,
enabled = true,
) {
if (!enabled) {
return 'static';
}
const value = (policy || '').trim();
if (!value || value === 'static') return 'static';
if (value === 'url' || value === 'all') return 'all';
if (!value || value === 'url' || value === 'all') return 'all';
if (value === 'static') return 'static';
if (value === 'suffix' || value === 'path_prefix' || value === 'path_exact') {
return value;
}
@@ -103,6 +109,7 @@ export function CacheSection({
cache_enabled: route.cache_enabled,
cache_policy: normalizeCachePolicyValue(
route.cache_policy,
route.cache_enabled,
) as CacheValues['cache_policy'],
cache_rules_text: route.cache_rule_list.join('\n'),
},
@@ -113,6 +120,7 @@ export function CacheSection({
cache_enabled: route.cache_enabled,
cache_policy: normalizeCachePolicyValue(
route.cache_policy,
route.cache_enabled,
) as CacheValues['cache_policy'],
cache_rules_text: route.cache_rule_list.join('\n'),
});
@@ -161,9 +169,7 @@ export function CacheSection({
await save(
{
cache_enabled: values.cache_enabled,
cache_policy: values.cache_enabled
? normalizeCachePolicyValue(values.cache_policy)
: 'static',
cache_policy: values.cache_enabled ? values.cache_policy : '',
cache_rules:
values.cache_enabled &&
needsRulesForPolicy(values.cache_policy)
@@ -182,8 +188,9 @@ export function CacheSection({
<div className='space-y-0.5'>
<FormLabel>启用站点缓存</FormLabel>
<FormDescription>
开启后默认仅缓存标准静态扩展名(不含 HTML)。仍会自动绕过非
GET、Authorization 与常见登录态 Cookie。
新建推荐「标准静态资源」(不含
HTML)。须同时开启性能设置中的全局 OpenResty
缓存。仍会绕过非 GET、Authorization 与常见登录 Cookie。
</FormDescription>
</div>
<FormControl>
@@ -347,10 +347,35 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
routes[index].BasicAuthPassword = ""
}
routes[index].UpstreamType = normalizeUpstreamType(routes[index].UpstreamType)
routes[index].CachePolicy = normalizeSnapshotCachePolicy(
routes[index].CacheEnabled,
routes[index].CachePolicy,
)
if !routes[index].CacheEnabled {
routes[index].CacheRules = nil
}
}
return routes
}
// normalizeSnapshotCachePolicy aligns published policy with edge-cache-design:
// legacy empty/url → all; disabled → empty; static/suffix/... kept.
func normalizeSnapshotCachePolicy(enabled bool, raw string) string {
if !enabled {
return ""
}
policy := strings.TrimSpace(strings.ToLower(raw))
switch policy {
case "", "url", "all":
return "all"
case "static", "suffix", "path_prefix", "path_exact":
return policy
default:
// Unknown: prefer static over caching everything.
return "static"
}
}
func flattenSnapshotRoutesBySite(routes []snapshotRoute) map[string]snapshotRoute {
siteMap := make(map[string]snapshotRoute)
for _, route := range normalizeSnapshotRoutes(routes) {
+15 -4
View File
@@ -436,17 +436,20 @@ func decodeStoredCustomHeaders(raw string) ([]CustomHeaderInput, error) {
return normalizeCustomHeaders(headers)
}
// normalizeCachePolicy stores API write values.
// When enabling with empty/url policy, keep legacy "all" semantics so old rows
// and clients that omit policy do not silently narrow cache to static extensions.
// New UI should send policy=static explicitly when choosing the recommended default.
func normalizeCachePolicy(enabled bool, raw string) string {
if !enabled {
return ""
}
policy := strings.TrimSpace(strings.ToLower(raw))
switch policy {
case "", proxyRouteCachePolicyStatic:
return proxyRouteCachePolicyStatic
case proxyRouteCachePolicyURL, proxyRouteCachePolicyAll:
// Legacy url is normalized to all (full GET allow after security bypass).
case "", proxyRouteCachePolicyURL, proxyRouteCachePolicyAll:
return proxyRouteCachePolicyAll
case proxyRouteCachePolicyStatic:
return proxyRouteCachePolicyStatic
case proxyRouteCachePolicySuffix, proxyRouteCachePolicyPathPrefix, proxyRouteCachePolicyPathExact:
return policy
default:
@@ -454,6 +457,14 @@ func normalizeCachePolicy(enabled bool, raw string) string {
}
}
// displayCachePolicy normalizes values for API list/get (and UI).
func displayCachePolicy(enabled bool, raw string) string {
if !enabled {
return ""
}
return normalizeCachePolicy(true, raw)
}
func normalizeCacheRules(enabled bool, rawPolicy string, rules []string) ([]string, error) {
if !enabled {
return []string{}, nil
@@ -309,7 +309,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e
LimitConnPerIP: route.LimitConnPerIP,
LimitRate: route.LimitRate,
CacheEnabled: route.CacheEnabled,
CachePolicy: route.CachePolicy,
CachePolicy: displayCachePolicy(route.CacheEnabled, route.CachePolicy),
CacheRules: route.CacheRules,
CacheRuleList: cacheRules,
CustomHeaders: route.CustomHeaders,
@@ -83,12 +83,18 @@ func TestCreateProxyRouteHTTPSRequiresCoveringCertificate(t *testing.T) {
func TestNormalizeCachePolicyDefaultsAndLegacy(t *testing.T) {
assert.Equal(t, "", normalizeCachePolicy(false, "static"))
assert.Equal(t, proxyRouteCachePolicyStatic, normalizeCachePolicy(true, ""))
// Empty/url on write = legacy all (compat); UI sends static explicitly for new default.
assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, ""))
assert.Equal(t, proxyRouteCachePolicyStatic, normalizeCachePolicy(true, "static"))
assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "url"))
assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "all"))
assert.Equal(t, proxyRouteCachePolicySuffix, normalizeCachePolicy(true, "suffix"))
assert.Equal(t, "", displayCachePolicy(false, "all"))
assert.Equal(t, proxyRouteCachePolicyAll, displayCachePolicy(true, ""))
assert.Equal(t, proxyRouteCachePolicyAll, displayCachePolicy(true, "url"))
assert.Equal(t, proxyRouteCachePolicyStatic, displayCachePolicy(true, "static"))
rules, err := normalizeCacheRules(true, "url", []string{"css"})
require.NoError(t, err)
assert.Empty(t, rules)
+6 -3
View File
@@ -497,13 +497,16 @@ func renderRouteCachePolicyCondition(cacheConfig routeCacheConfig) string {
}
}
// normalizeRenderCachePolicy maps stored policy for OpenResty generation.
// Legacy empty and "url" mean "all GETs after security bypass" (pre-static default).
// Explicit "static" uses the built-in extension allowlist. Unknown policies fall back to static.
func normalizeRenderCachePolicy(raw string) string {
policy := strings.TrimSpace(strings.ToLower(raw))
switch policy {
case "", cachePolicyStatic:
return cachePolicyStatic
case cachePolicyURL, cachePolicyAll:
case "", cachePolicyURL, cachePolicyAll:
return cachePolicyAll
case cachePolicyStatic:
return cachePolicyStatic
case cachePolicySuffix, cachePolicyPathPrefix, cachePolicyPathExact:
return policy
default:
+3 -2
View File
@@ -411,9 +411,10 @@ func TestRenderRouteCachePolicyConditionStaticDefault(t *testing.T) {
t.Fatalf("static policy must not include html, got:\n%s", staticBlock)
}
// Legacy empty/url = all (wide cache after security bypass).
emptyPolicy := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: ""})
if emptyPolicy == "" {
t.Fatal("empty policy should default to static condition")
if emptyPolicy != "" {
t.Fatalf("empty policy should map to all (no path filter), got %q", emptyPolicy)
}
allBlock := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: "all"})