Migrations already run idempotently during RunServer startup via goose (which tracks applied versions in goose_db_version). The separate command is redundant — removed it and consolidated serveCmd/configFile/init into server.go.
24 KiB
Review 决议记录
来源:2026-07-27 对
backend-dev-plan.md的 review,共 45 条问题。 本文档对每条问题给出明确决议,开发时以此为准,不得偏离。 如需变更决议,先修改本文档再改代码。
决议速查
| # | 问题 | 决议 | 落地位置 |
|---|---|---|---|
| 1 | ProxyNode 类型 | map[string]any |
model/types.go |
| 2 | script 过滤器迁移 | 不保留 script 类型 | filter/custom.go |
| 3 | process→FilterRule 转换 | 不实现,前端直发 FilterRule[] | — |
| 4 | defaultSettings 合并 | 实现 mergeSettings + 特殊浅合并 | handler/settings.go |
| 5 | envPayload feature flags | 定义并返回,buildTimeScripts=false | handler/env.go |
| 6 | Flow info URL hash 解析 | 实现完整解析 | handler/flow.go |
| 7 | normalizeTarget UA 推断 | 实现完整关键词映射 | model/target.go |
| 8 | 临时源覆盖 | 实现仅覆盖第一个匹配源 | handler/download.go |
| 9 | parseJsonOrText | 实现 JSON 优先 + 纯文本回退 | handler/ 工具函数 |
| 10 | SW 清理端点 | 不实现(无 CF 迁移) | — |
| 11 | getPublicBaseUrl | 实现 PUBLIC_DOWNLOAD_HOSTS 配置 | handler/link.go |
| 12 | 中文拼音排序 | golang.org/x/text/collate + chinese | filter/sort.go |
| 13 | splitHostPort | 自写 lastIndexOf(":") 版本 | util/ |
| 14 | JS URL vs Go net/url | net/url 为基 + 逐协议测试 + 边缘处理 | proxy/uri_parser.go |
| 15 | URL-safe Base64 | 区分 StdEncoding / RawURLEncoding | util/base64.go |
| 16 | Unicode flag 正则 | RE2 \p{} 支持,实测验证 | util/flag.go |
| 17 | secureRandomInt | crypto/rand.Int(天然无偏) | filter/sort.go |
| 18 | formatDuplicateNumber 自定义数字字符 | 实现完整功能 | filter/dedupe.go |
| 19 | normalizeTaiwanFlag 三模式 | 实现完整 | filter/flag.go |
| 20 | isUsefulProxy ASCII 校验 | 实现完整 | filter/quick.go |
| 21 | applyState 字符串状态值 | 实现兼容解析 | filter/quick.go |
| 22 | snell/ssh/h2-connect | 仅 client_parser 解析 | proxy/client_parser.go |
| 23 | normalizeClientProxyKind 别名 | 实现别名映射 | proxy/client_parser.go |
| 24 | isTokenValid 双哈希 | 先 SHA-256 再 ConstantTimeCompare | util/token.go |
| 25 | CRLF 注入防护 | 校验 + 清洗所有 header 值 | middleware/security.go |
| 26 | getBearerToken 三种提取 | 实现 Bearer / query / x-sub-store-token | middleware/auth.go |
| 27 | CSP 安全头 | 收紧为 script-src 'self'(无 unsafe-eval) | middleware/security.go |
| 28 | SQLite PRAGMA | WAL + busy_timeout + foreign_keys + MaxOpenConns=1 | database/db.go |
| 29 | archiveAndDeleteResource 事务 | BeginTx 包裹三步 | handler/ 删除逻辑 |
| 30 | importStorage 导入顺序 | settings→sources→templates→collections | handler/storage.go |
| 31 | exportStorage 排除内置模板 | 过滤 BuiltinTemplateIDs | handler/storage.go |
| 32 | source_cache TTL 清理 | 后台 goroutine 定时清理 | database/cache_repo.go |
| 33 | goose 迁移嵌入 | //go:embed + SetBaseFS | database/migrations.go |
| 34 | goroutine 关闭安全 | sync.WaitGroup + context | service/subscription.go |
| 35 | 缓存操作错误吞咽 | recover + 日志,不传播 | database/cache_repo.go |
| 36 | 并发 wait 延迟 | 保留 waitMs 参数 | service/concurrency.go |
| 37 | MAX_SCRIPT_ACTIONS | custom 规则上限 32 条 | filter/custom.go |
| 38 | restoreDownloadGrantSnapshot | 恢复时写回 tokenHash | handler/recycle.go |
| 39 | FRONTEND_VERSION 常量 | 定义 = "1.0.0" | config/ |
| 40 | TEST_URL 常量 | 定义 generate_204 | render/ |
| 41 | defaultProxyGroups | 无模板时生成 3 默认组 | template/builtin.go |
| 42 | sing-box 完整结构 | log+inbounds+route+outbounds 全实现 | render/singbox.go |
| 43 | profile-update-interval 默认值 | 默认 "6" | handler/download.go |
| 44 | cache-control: no-store | 下载响应设此头 | handler/download.go |
| 45 | modernc.org/sqlite 性能 | 接受 tradeoff,保留 swap 注释 | database/db.go |
逐条详述
🔴 架构级决策(#1-#3)— 已由 AGENTS.md 约束豁免
#1 ProxyNode 类型 → map[string]any
决议:ProxyNode 定义为 type ProxyNode map[string]any。
理由:
- 13 种协议各有不同可选字段(reality-opts, ws-opts, plugin-opts, auth_str, congestion-controller...),struct 需定义全部字段且无法处理未知字段
- 过滤器通过
getByPath(proxy, "ws-opts.headers.Host")做点号路径访问,map 天然支持 - JSON/YAML 反序列化到 map 保留所有字段;struct 会丢失未知字段
- stripUndefined(删除 nil 和 "")在 map 上语义清晰
- 类型安全损失可接受——节点字段本质是动态的
实现要点:
model/types.go:type ProxyNode map[string]any- 深拷贝:
json.Marshal→json.Unmarshal(简单可靠,性能可接受) - 类型安全访问:在
util/path.go提供GetByPath(node, "a.b.c")/SetByPath/GetString/GetInt/GetBool等辅助函数
#2 script 过滤器类型 → 不保留
决议:Go 版不实现 script 类型过滤器。用户自定义逻辑通过 custom 声明式规则链实现。
理由:全新项目无存量 script 过滤器,无需兼容。
影响:
GET /api/scripts返回空数组[](端点保留,前端兼容)applyFilters中无script分支validateScriptActions不需要
#3 process→FilterRule 转换 → 不实现
决议:Go API 直接接受 FilterRule[] 格式,不实现前端 process 数组到 FilterRule[] 的转换。
理由:前端尚未实现,将适配 Go API 设计。前端直接发送 FilterRule[] 格式。
🟠 关键功能遗漏(#4-#11)
#4 defaultSettings 合并 → 实现完整合并
决议:GET /api/settings 返回 mergeSettings(defaultSettings(), storedSettings)。
合并规则:
- 顶层 key:stored 覆盖 default
theme和appearanceSetting:浅合并(stored 的子 key 覆盖 default 的对应子 key,而非整体替换)- 其他嵌套对象:深合并
落地:handler/settings.go + config/defaults.go(定义 DefaultSettings() 函数)。
#5 envPayload feature flags → 定义并返回
决议:GET /api/env 返回 feature 对象,各 flag 取值如下:
buildTimeScripts: false // 无 JS 引擎
proxyConversion: true // POST /api/proxy/parse
ruleConversion: true // POST /api/rule/parse
scopedShares: true // 分享令牌
recycleBin: true // 回收站
nodeInfo: true // POST /api/utils/node-info
surgeMac: true // surge-mac 渲染
落地:handler/env.go。
#6 Flow info URL hash 解析 → 完整实现
决议:实现 parseFlowRequest,从 URL # fragment 中解析参数。
支持两种格式:
- JSON 格式:
#{"flowUrl":"...","noFlow":true,"flowUserAgent":"...","flowHeaders":{}} - Query-string 格式:
#flowUrl=...&noFlow=true&flowUserAgent=...
额外:fetchFlowHeaders 从响应体中提取 upload= / download= / total= / expire= 流量信息(subscription-userinfo header 优先,body 次之)。
落地:handler/flow.go。
#7 normalizeTarget UA 推断 → 完整关键词映射
决议:实现完整 UA 关键词→目标格式映射,按优先级匹配。
映射表(按检查顺序):
| UA 关键词 | 目标格式 |
|---|---|
sing-box |
singbox |
v2ray / v2rayng |
v2ray |
surge (含 mac) |
surge-mac |
surge (不含 mac) |
surge |
loon |
loon |
egern |
egern |
shadowrocket |
shadowrocket |
quantumult |
qx |
stash |
stash |
落地:model/target.go 的 NormalizeTarget(target, ua string) string。
#8 临时源覆盖 → 仅覆盖第一个匹配源
决议:实现 applyTemporarySourceOverride,通过 ?url= / ?content= / ?ua= query 参数临时覆盖。
关键行为:只覆盖集合中第一个匹配的源(按 sourceIds 顺序),不是全部覆盖。与原项目行为一致。
落地:handler/download.go。
#9 parseJsonOrText → JSON 优先 + 纯文本回退
决议:实现 parseJsonOrText(body []byte) (map[string]any, error)。
逻辑:
- 尝试
json.Unmarshal→ 成功则返回 - 失败则将 body 作为纯文本,返回
{"content": string(body)}
应用端点:template 创建/更新、storage 导入。
落地:handler/ 下的工具函数。
#10 Service Worker 清理端点 → 不实现
决议:不实现 /sw.js 和 /registerSW.js 端点。
理由:全新项目,无 Cloudflare 版 Service Worker 残留需要清理。
#11 getPublicBaseUrl → 支持 PUBLIC_DOWNLOAD_HOSTS
决议:实现 getPublicBaseUrl(r *http.Request) string。
逻辑:
- 读取环境变量 / 配置
SUB_STORE_PUBLIC_DOWNLOAD_HOSTS(逗号分隔域名列表) - 若非空,返回第一个域名作为 base URL
- 否则返回
r.Host(请求自身的 origin)
落地:handler/link.go 的 buildDownloadLink。
🟡 实现细节/陷阱(#12-#23)
#12 中文拼音排序 → golang.org/x/text/collate
决议:引入 golang.org/x/text/collate + golang.org/x/text/language,使用 collate.New(language.SimplifiedChinese) 做中文拼音排序。
落地:filter/sort.go 的 sortProxies。在 go.mod 添加依赖。
#13 splitHostPort → 自写版本
决议:不使用 net.SplitHostPort,自写 splitHostPort(s string) (host, port string)。
行为:用 strings.LastIndex(s, ":") 切分,与原项目一致。能正确处理 IPv6(如 [::1]:443 → host=[::1], port=443)和裸 IPv6(无端口时返回 host=s, port="")。
落地:util/ 或 proxy/uri_parser.go 内部函数。
#14 JS URL vs Go net/url → net/url 为基 + 逐协议测试
决议:以 net/url.Parse 为基础解析器,针对已知差异做适配:
- Fragment 处理:Go 的
url.Fragment不会自动 URL-decode,需要手动url.QueryUnescape(fragment)获取节点名 - Userinfo 提取:用
u.User.Username()和u.User.Password(),不手动切分 - IPv6 hostname:
u.Hostname()自动去掉方括号 - Query().Get() 返回 "":原项目
URLSearchParams.get()返回null,Go 返回""。所有!= null判断改为!= ""
落地:proxy/uri_parser.go。为每种协议编写表驱动测试,覆盖正常/异常/边界 case。
#15 URL-safe Base64 → 明确区分
决议:在 util/base64.go 中提供三组函数:
| 函数 | 编码 | 用途 |
|---|---|---|
DecodeBase64Std |
base64.StdEncoding |
vmess JSON |
DecodeBase64URL |
base64.URLEncoding |
— |
DecodeBase64RawURL |
base64.RawURLEncoding (无填充) |
SSR |
EncodeBase64URL |
base64.URLEncoding |
— |
EncodeBase64RawURL |
base64.RawURLEncoding |
— |
同时提供 DecodeBase64Auto(s string) — 自动尝试 Std 和 RawURL 两种编码(先 Std,失败再 RawURL),因为订阅内容编码不一定规范。
落地:util/base64.go。
#16 Unicode flag 正则 → RE2 支持,实测验证
决议:Go RE2 支持 \p{Regional_Indicator} 和 \uFE0F / \u200D。
验证清单:
detectFlag:用真实国旗 emoji(🇭🇰 🇯🇵 🇺🇸 🇹🇼)测试removeFlag:测试含 ZWJ (\u200D) 和 variation selector (\uFE0F) 的复合 emoji- 编写单元测试覆盖所有 Unicode flag 场景
落地:util/flag.go。
#17 secureRandomInt → crypto/rand.Int
决议:直接使用 crypto/rand.Int(rand.Reader, big.NewInt(int64(max)))。
理由:Go 的 crypto/rand.Int 内部已实现无偏随机(rejection sampling),无需手动实现。比原项目的 JS 版更简洁。
落地:filter/sort.go 的 shuffle 逻辑。
#18 formatDuplicateNumber 自定义数字字符 → 完整实现
决议:实现 formatDuplicateNumber(index int, template string) string。
功能:template 可包含自定义数字字符集,用空格分隔。如 "一 二 三 四 五" → index=2 输出 "二"。默认为阿拉伯数字 "1 2 3 4 5..."。
落地:filter/dedupe.go。
#19 normalizeTaiwanFlag 三模式 → 完整实现
决议:flag 过滤器的 tw 参数支持三种模式:
| 参数值 | 输出 |
|---|---|
ws |
🇼🇸 (萨摩亚旗) |
tw |
🇹🇼 (台湾旗) |
| 默认/其他 | 🇨🇳 (中国旗) |
落地:filter/flag.go。
#20 isUsefulProxy ASCII 校验 → 完整实现
决议:isUsefulProxy 不仅检查节点名关键词,还校验:
cipher值是否为纯 ASCIIpassword值是否为纯 ASCII- WS Host header(
ws-opts.headers.Host)是否为纯 ASCII
任一非 ASCII → 标记为 useless。
落地:filter/quick.go。
#21 applyState 字符串状态值 → 兼容解析
决议:applyState 接受以下值并归一化为 bool:
| 输入 | 输出 |
|---|---|
true / "ENABLED" / "enabled" |
true |
false / "DISABLED" / "disabled" |
false |
落地:filter/quick.go 的 parseState(v any) bool。
#22 snell/ssh/h2-connect → 仅客户端配置行解析
决议:这三种类型仅在 Surge/Loon 客户端配置行解析中出现,不出现在 URI 协议中。
snell:Surge 配置行snell = name, server, port, psk, ...ssh:Surge 配置行ssh = name, server, port, ...h2-connect:Surge 配置行
落地:proxy/client_parser.go,不放入 uri_parser.go。
#23 normalizeClientProxyKind 别名 → 实现映射
决议:客户端配置行的协议类型别名归一化:
shadowsocks → ss
socks5-tls → socks5
https → http
hysteria 2 → hysteria2
tuic-v5 → tuic
落地:proxy/client_parser.go 的 normalizeClientProxyKind(kind string) string。
🔒 安全相关(#24-#27)
#24 isTokenValid 双哈希 → 先 SHA-256 再 ConstantTimeCompare
决议:所有 token 校验先做 SHA-256 哈希,再用 subtle.ConstantTimeCompare 比较哈希值,避免长度泄露。
两种场景:
-
admin_token / download_token(配置文件中的明文 secret):
inputHash = sha256Hex(input) secretHash = sha256Hex(secret) return subtle.ConstantTimeCompare([]byte(inputHash), []byte(secretHash)) == 1 -
scoped grant(数据库存储
sha256Hex(token)):inputHash = sha256Hex(input) return subtle.ConstantTimeCompare([]byte(inputHash), []byte(storedHash)) == 1
落地:util/token.go 的 IsTokenValid(input, secret string) bool 和 IsGrantTokenValid(input string, storedHash string) bool。
#25 CRLF 注入防护 → 校验 + 清洗
决议:
- setResponseHeader:设置任何 header 值前校验
!strings.ContainsAny(value, "\r\n"),不合法则跳过设置并记录警告日志 - safeContentDisposition:清洗文件名——移除
\r\n和控制字符,对特殊字符做引号转义
落地:middleware/security.go 或 handler/download.go 中的工具函数。
#26 getBearerToken 三种提取 → 完整实现
决议:token 从三处提取,优先级从高到低:
Authorization: Bearer <token>header?token=<token>query parameterx-sub-store-tokenheader
落地:middleware/auth.go 的 extractToken(c *fiber.Ctx) string。
#27 CSP 安全头 → 收紧
决议:Go 版前端不含 eval,CSP 收紧为:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
connect-src 'self';
font-src 'self' data:;
与原项目差异:移除 'unsafe-eval'(原项目前端有 eval,Go 版不需要)。
落地:middleware/security.go。
🗄️ 数据库相关(#28-#33)
#28 SQLite PRAGMA → 完整配置
决议:在 database/db.go 打开连接后立即执行:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL;
连接池:
db.SetMaxOpenConns(1)— modernc.org/sqlite 在高并发写时会出现database is locked,单连接 + WAL 足以应对 sub-store 的个人工具场景- 读写都在同一连接上,WAL 模式下读不阻塞写
落地:database/db.go 的 InitDB(path string) (*sqlx.DB, error)。
#29 archiveAndDeleteResource 事务 → BeginTx 包裹
决议:删除资源时在一个事务中执行三步:
BEGIN TRANSACTION;
INSERT INTO recycle_bin (id, resource_type, resource_id, snapshot_json, deleted_at) VALUES (...);
DELETE FROM {sources|collections|templates} WHERE id = ?;
DELETE FROM recycle_bin WHERE id NOT IN (
SELECT id FROM recycle_bin ORDER BY deleted_at DESC LIMIT ?
);
COMMIT;
落地:handler/ 删除逻辑 + database/recycle_repo.go 的 ArchiveAndDelete(tx, resourceType, resourceID, snapshot, maxEntries)。
#30 importStorage 导入顺序 → 强制顺序
决议:导入顺序固定为:
settings— 无依赖sources— 无依赖templates— 无依赖(但 collections 引用 templates)collections— 引用 sources 和 templates
每步独立事务,失败则中止并返回错误。
落地:handler/storage.go 的 importStorage。
#31 exportStorage 排除内置模板 → 过滤
决议:导出时过滤掉 ID 属于 BuiltinTemplateIDs 集合的模板。
var exportedTemplates []Template
for _, t := range allTemplates {
if !builtinTemplateIDs[t.ID] {
exportedTemplates = append(exportedTemplates, t)
}
}
落地:handler/storage.go 的 exportStorage。
#32 source_cache TTL 清理 → 后台 goroutine
决议:启动一个后台 goroutine,每隔 cache_ttl 间隔执行一次清理:
func StartCacheCleaner(ctx context.Context, db *sqlx.DB, interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for {
select {
case <-ticker.C:
db.ExecContext(ctx, "DELETE FROM source_cache WHERE cached_at + ttl < ?", time.Now().Unix())
case <-ctx.Done():
ticker.Stop()
return
}
}
}()
}
在 graceful shutdown 时通过 context 取消。间隔取 cache_ttl 配置值(默认 300s)。
落地:database/cache_repo.go 的 StartCacheCleaner。
#33 goose 迁移嵌入 → //go:embed + SetBaseFS
决议:迁移 SQL 文件嵌入二进制:
//go:embed migrations/*.sql
var embedMigrations embed.FS
func RunMigrations(db *sqlx.DB) error {
goose.SetBaseFS(embedMigrations)
return goose.Up(db.DB, "migrations")
}
落地:database/migrations.go + database/migrations/*.sql。启动时自动执行,幂等(goose 跟踪 goose_db_version)。
🔄 并发/异步(#34-#36)
#34 goroutine 关闭安全 → WaitGroup + context
决议:异步缓存写入通过 sync.WaitGroup 管理生命周期。
type AsyncWriter struct {
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
func (aw *AsyncWriter) Write(fn func()) {
aw.wg.Add(1)
go func() {
defer aw.wg.Done()
select {
case <-aw.ctx.Done():
return
default:
fn()
}
}()
}
func (aw *AsyncWriter) Wait() {
aw.cancel()
aw.wg.Wait()
}
在 graceful shutdown 时调用 Wait(),带 5 秒超时。
落地:service/subscription.go 或 database/cache_repo.go。
#35 缓存操作错误吞咽 → recover + 日志
决议:safeCacheMatch 和 safeCachePut 包装缓存操作:
func safeCacheGet(repo *CacheRepo, key string) (string, bool) {
defer func() {
if r := recover(); r != nil {
logrus.Warnf("cache get panicked: %v", r)
}
}()
return repo.Get(key)
}
缓存操作失败(SQL 错误、panic)只记录日志,不传播给订阅生成管线。
落地:database/cache_repo.go 的 safe wrapper。
#36 并发 wait 延迟 → 保留参数
决议:RunWithConcurrency 和 RunSettledWithConcurrency 的签名保留 wait time.Duration 参数。
行为:每个任务开始前(第一个除外)time.Sleep(wait)。用于避免同时请求同一 CDN 被限流。对应配置 fetcher.concurrency_wait。
落地:service/concurrency.go。
📋 其他遗漏(#37-#45)
#37 MAX_SCRIPT_ACTIONS → custom 规则上限 32 条
决议:custom 过滤器的规则链上限 MAX_CUSTOM_RULES = 32。不保留原项目的 MAX_SCRIPT_ACTIONS = 2(那是 JS 脚本的限制,Go 版无 JS 引擎)。
落地:filter/custom.go 的验证逻辑。
#38 restoreDownloadGrantSnapshot → 恢复时写回 tokenHash
决议:从回收站恢复 share 类型时,快照中的 tokenHash 需写回 download_grants 表。这是 share 恢复的特殊逻辑——其他资源类型(source/collection/template)恢复时不需要处理 tokenHash。
落地:handler/recycle.go 的 restore 逻辑。
#39 FRONTEND_VERSION 常量
决议:定义 const FrontendVersion = "1.0.0",用于 GET /api/env 响应。
落地:internal/config/config.go 或 handler/env.go。
#40 TEST_URL 常量
决议:定义 const TestURL = "https://www.gstatic.com/generate_204",用于 mihomo/sing-box 渲染中 url-test proxy group 的 test URL。
落地:render/mihomo.go / render/singbox.go。
#41 defaultProxyGroups → 无模板时生成 3 默认组
决议:当 collection 未配置模板或模板无 proxy-groups 时,生成 3 个默认代理组:
| 组名 | 类型 | 行为 |
|---|---|---|
| 🚀 节点选择 | select | 手动选择,含所有节点 + ♻️ 自动选择 |
| ♻️ 自动选择 | url-test | 自动测速,含所有节点 |
| 🚀 手动切换 | select | 手动选择,仅含所有节点 |
落地:template/builtin.go 的 DefaultProxyGroups()。
#42 sing-box 完整结构 → 全实现
决议:sing-box 输出包含完整顶层配置:
{
"log": { "level": "info" },
"inbounds": [{
"type": "mixed",
"tag": "mixed-in",
"listen": "0.0.0.0",
"listen_port": 7890,
"sniff": true
}],
"outbounds": [
{ "tag": "PROXY", "type": "selector", "outbounds": [...], "interrupt_exist_connections": false },
{ "tag": "AUTO", "type": "urltest", "outbounds": [...], "url": "https://www.gstatic.com/generate_204", "interrupt_exist_connections": false },
{ "tag": "DIRECT", "type": "direct" },
{ "tag": "REJECT", "type": "block" },
...proxy outbounds...
],
"route": {
"auto_detect_interface": true,
"final": "PROXY"
}
}
落地:render/singbox.go。
#43 profile-update-interval 默认值
决议:下载响应设置 profile-update-interval header,值为 metadata.profileUpdateInterval,若为空则默认 "6"。
落地:handler/download.go。
#44 cache-control: no-store
决议:下载响应设置 Cache-Control: no-store,防止 CDN/浏览器缓存订阅内容。
落地:handler/download.go。
#45 modernc.org/sqlite 性能 → 接受 tradeoff
决议:使用 modernc.org/sqlite(纯 Go),接受写性能低于 CGO 版的 tradeoff。
理由:
- sub-store 是个人工具,写频率低(主要是缓存写入),性能不是瓶颈
- 纯 Go 支持
CGO_ENABLED=0静态编译,跨平台部署简单 - 如未来性能不足,可切换到
mattn/go-sqlite3(接口不变,仅换驱动 + 启用 CGO)
落地:database/db.go 添加注释说明 tradeoff 和替代方案。