Files
sub-store/docs/backend-dev-plan.md
T

947 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sub-Store Go 后端功能开发计划
> 技术栈:Go 1.23+ / Fiber v3 / Cobra / Viper / Logrus / SQLite
> 参考原项目:`ref/sub-store-cloudflare/cloudflare/` (TypeScript, 5404 行)
> 核心原则:优先使用成熟开源库,不自造轮子
---
## 1. 技术选型
### 1.1 核心框架
| 用途 | 库 | 版本 | 理由 |
|------|-----|------|------|
| HTTP 框架 | `github.com/gofiber/fiber/v3` | v3.x | 用户指定 |
| CLI 启动 | `github.com/spf13/cobra` | v1.x | 用户指定 |
| 配置管理 | `github.com/spf13/viper` | v1.x | 用户指定 |
| 日志 | `github.com/sirupsen/logrus` | v1.x | 用户指定 |
| SQLite 驱动 | `modernc.org/sqlite` | latest | 纯 Go,无 CGO,跨平台编译 |
| ORM/查询 | `github.com/jmoiron/sqlx` | latest | 轻量,sql 行映射,不引入 GORM 重量级 |
| 迁移 | `github.com/pressly/goose/v3` | latest | 成熟 SQL 迁移工具,//go:embed 嵌入 |
| 中文排序 | `golang.org/x/text` | latest | collate + language.SimplifiedChinese 拼音排序 |
### 1.2 功能库
| 用途 | 库 | 理由 |
|------|-----|------|
| YAML 序列化 | `gopkg.in/yaml.v3` | mihomo 配置和模板操作 |
| HTTP 客户端 | `github.com/go-resty/resty/v2` | 重试/超时/流式读取,比 net/http 更简洁 |
| UUID | `github.com/google/uuid` | grant/recycle ID 生成 |
| 验证 | `github.com/go-playground/validator/v10` | 请求体校验 |
| 正则 | `regexp` (标准库 RE2) | 注意:RE2 不支持反向引用/lookahead,原项目部分正则需适配 |
| Base64 | `encoding/base64` (标准库) | |
| Crypto | `crypto/subtle`, `crypto/sha256`, `crypto/rand` (标准库) | token 校验、hash、随机 |
| URL 解析 | `net/url` (标准库) | URI 协议解析 |
| JSON | `encoding/json` (标准库) | |
| 信号处理 | `os/signal` (标准库) | 优雅关闭 |
| 嵌入静态资源 | `embed` (标准库) | 前端 SPA |
### 1.3 原项目依赖对照
| 原项目 (TS) | Go 替代 | 说明 |
|-------------|---------|------|
| Hono | Fiber v3 | 路由 + 中间件 |
| D1 (SQLite) | modernc.org/sqlite + sqlx | 直接 SQLite |
| `caches.default` (Cache API) | SQLite `source_cache` 表 | 零额外依赖,重启不丢,WAL 读写不互斥 |
| `crypto.subtle.timingSafeEqual` | `crypto/subtle.ConstantTimeCompare` | |
| `c.req.json()` 等 | Fiber `c.Bind()` / `c.Body()` | |
| `ExecutionCtx.waitUntil` | `go func()` + context | goroutine 异步 |
| `env.DB.batch()` | sqlx `BeginTx()` + 多语句 | 事务 |
| `atob` / `btoa` | `encoding/base64` | |
| `yaml` npm 包 | `gopkg.in/yaml.v3` | |
| `json5` npm 包 | `encoding/json` + 宽容解析 | JSON5 场景仅用于代理节点数组解析,可用标准 JSON + 回退 |
| `structuredClone` | 深拷贝工具函数 | `reflect` 或手写 |
---
## 2. 项目结构
```
sub-store/
├── main.go # 入口,调用 cmd
├── cmd/
│ ├── root.go # cobra root command
│ ├── serve.go # sub-store serve (启动 HTTP 服务,启动时自动 migrate)
│ └── version.go # sub-store version
├── internal/
│ ├── config/
│ │ └── config.go # viper 配置加载,结构体定义
│ ├── model/
│ │ ├── types.go # 核心类型:ProxyNode, FilterRule, SourceRecord, ...
│ │ ├── source.go # SubscriptionSource
│ │ ├── collection.go # SubscriptionCollection
│ │ ├── template.go # RoutingTemplate, RoutingTemplateConfig
│ │ ├── target.go # SubscriptionTarget 常量与别名
│ │ └── response.go # API 响应封装
│ ├── database/
│ │ ├── db.go # SQLite 连接初始化
│ │ ├── migrations/
│ │ │ ├── 0001_initial.sql
│ │ │ ├── 0002_runtime_cleanup.sql
│ │ │ └── 0003_compat_resources.sql
│ │ ├── source_repo.go # sources 表 CRUD
│ │ ├── collection_repo.go # collections 表 CRUD
│ │ ├── template_repo.go # templates 表 CRUD
│ │ ├── settings_repo.go # app_settings 表 CRUD
│ │ ├── grant_repo.go # download_grants 表 CRUD
│ │ ├── recycle_repo.go # recycle_bin 表 CRUD
│ │ └── cache_repo.go # source_cache 表 CRUD
│ ├── handler/
│ │ ├── env.go # GET /api/env
│ │ ├── source.go # /api/sources CRUD
│ │ ├── collection.go # /api/collections CRUD
│ │ ├── template.go # /api/templates CRUD
│ │ ├── settings.go # /api/settings
│ │ ├── storage.go # /api/storage 导出导入
│ │ ├── share.go # /api/shares CRUD
│ │ ├── recycle.go # /api/recycle-bin
│ │ ├── preview.go # /api/preview/*
│ │ ├── link.go # /api/link/*
│ │ ├── flow.go # /api/source/flow/*
│ │ ├── tools.go # /api/proxy/parse, /api/rule/parse, /api/utils/node-info
│ │ ├── script.go # /api/scripts (注册表元数据)
│ │ └── download.go # /download/* 公开下载
│ ├── middleware/
│ │ ├── auth.go # admin token 鉴权
│ │ ├── cors.go # CORS
│ │ ├── security.go # 安全响应头
│ │ ├── bodylimit.go # 请求体大小限制
│ │ └── downloadhost.go # 下载域名隔离
│ ├── service/
│ │ ├── subscription.go # 核心订阅处理管线
│ │ ├── fetcher.go # 远程源抓取 + 缓存查询
│ │ └── concurrency.go # 并发控制 worker pool
│ ├── proxy/
│ │ ├── parser.go # 统一入口:parseProxies
│ │ ├── format.go # 格式检测 + Base64 解码
│ │ ├── uri_parser.go # 13 种 URI 协议解析
│ │ ├── client_parser.go # QX/Surge/Loon 配置行解析
│ │ └── normalize.go # 节点归一化 + 预览 ID
│ ├── filter/
│ │ ├── pipeline.go # applyFilters 管线执行器
│ │ ├── include_exclude.go # include/exclude
│ │ ├── rename.go # rename
│ │ ├── delete_field.go # delete-field
│ │ ├── dedupe.go # dedupe (delete + rename)
│ │ ├── sort.go # sort + regex-sort
│ │ ├── flag.go # flag operator
│ │ ├── quick.go # quick settings
│ │ ├── resolve.go # DNS resolve
│ │ ├── custom.go # 声明式自定义规则链 (替代 JS 脚本)
│ │ └── util.go # getByPath/setByPath/compileRegex
│ ├── render/
│ │ ├── mihomo.go # mihomo/stash YAML
│ │ ├── surge.go # surge + surge-mac
│ │ ├── surfboard.go # surfboard
│ │ ├── loon.go # loon
│ │ ├── qx.go # quantumult x
│ │ ├── egern.go # egern YAML
│ │ ├── singbox.go # sing-box JSON
│ │ ├── uri.go # v2ray/uri/shadowrocket
│ │ └── json.go # JSON 原始输出
│ ├── rules/
│ │ └── converter.go # 分流规则转换
│ ├── template/
│ │ ├── builtin.go # 6 套内置模板定义
│ │ └── render.go # proxy-group 展开 + 模板渲染
│ └── util/
│ ├── token.go # token 生成 + hash + 验证
│ ├── path.go # 点号路径 get/set
│ ├── base64.go # base64 编解码
│ ├── ip.go # IPv4/IPv6 检测
│ └── flag.go # 国旗检测 + 移除
├── config/
│ └── config.example.yaml # 配置文件示例
├── web/
│ └── dist/ # 前端构建产物 (embed.FS)
└── go.mod
```
---
## 3. 配置设计
### 3.1 配置文件 (`config.yaml`)
```yaml
# 服务配置
server:
host: "0.0.0.0"
port: 3000
read_timeout: 30s
write_timeout: 60s
body_limit: 4194304 # 4 MiB
# 数据库
database:
path: "./data/sub-store.db"
# 鉴权
auth:
admin_token: "" # 必填,启动时检查
download_token: "" # 必填,启动时检查
download_hosts: [] # 纯下载域名列表
# 远程源抓取
fetcher:
default_timeout: 30s
default_user_agent: "clash.meta/v1.19.24"
default_flow_user_agent: "clash.meta/v1.19.24"
concurrency: 3
concurrency_wait: 0s
cache_ttl: 300s
cache_stale_on_error: true
max_source_urls: 8
max_response_bytes: 2097152 # 2 MiB
max_total_bytes: 12582912 # 12 MiB
# 回收站
recycle:
max_entries: 50
# 应用
app:
name: "Sub-Store"
version: "1.0.0"
```
### 3.2 Viper 加载策略
1. 默认值 (`config.SetDefault`)
2. 配置文件 (`config.yaml`,路径由 `--config` flag 指定,默认 `./config.yaml`)
3. 环境变量(前缀 `SUB_STORE_``.``_`,如 `SUB_STORE_AUTH_ADMIN_TOKEN`
4. 命令行 flagcobra
---
## 4. 数据库 Schema
直接复用原项目的 3 个 migration SQL,适配 modernc.org/sqlite 语法(基本兼容)。
### 4.1 表结构
```sql
-- 0001_initial.sql
CREATE TABLE IF NOT EXISTS sources (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'remote',
url TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
filters_json TEXT NOT NULL DEFAULT '[]',
meta_json TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS collections (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
source_ids_json TEXT NOT NULL DEFAULT '[]',
filters_json TEXT NOT NULL DEFAULT '[]',
template_id TEXT NOT NULL DEFAULT 'acl4ssr-mihomo',
ignore_failed INTEGER NOT NULL DEFAULT 1,
enabled INTEGER NOT NULL DEFAULT 1,
meta_json TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
target TEXT NOT NULL DEFAULT 'mihomo',
config_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS app_settings (
id TEXT PRIMARY KEY,
value_json TEXT NOT NULL DEFAULT '{}',
updated_at INTEGER NOT NULL
);
-- 0003_compatibility_resources.sql
CREATE TABLE IF NOT EXISTS download_grants (
id TEXT PRIMARY KEY,
token_hash TEXT NOT NULL UNIQUE,
resource_type TEXT NOT NULL CHECK (resource_type IN ('source', 'collection')),
resource_id TEXT NOT NULL,
target TEXT NOT NULL DEFAULT '',
expires_at INTEGER,
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_download_grants_token_hash
ON download_grants(token_hash);
CREATE INDEX IF NOT EXISTS idx_download_grants_resource
ON download_grants(resource_type, resource_id);
CREATE TABLE IF NOT EXISTS recycle_bin (
id TEXT PRIMARY KEY,
resource_type TEXT NOT NULL CHECK (resource_type IN ('source', 'collection', 'template', 'share')),
resource_id TEXT NOT NULL,
snapshot_json TEXT NOT NULL,
deleted_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_recycle_bin_deleted_at
ON recycle_bin(deleted_at DESC);
-- 0004_source_cache.sql
CREATE TABLE IF NOT EXISTS source_cache (
cache_key TEXT PRIMARY KEY, -- sha256(url + "\n" + userAgent)
content TEXT NOT NULL, -- 远程源响应体文本
metadata TEXT NOT NULL DEFAULT '{}', -- JSON: subscription-userinfo / etag / last-modified / ...
cached_at INTEGER NOT NULL, -- 写入时间戳 (unix seconds)
ttl INTEGER NOT NULL DEFAULT 300 -- TTL 秒数
);
CREATE INDEX IF NOT EXISTS idx_source_cache_expiry
ON source_cache(cached_at + ttl);
```
> 注:原项目 0002 migration 是清理旧内置模板行,Go 版无需复制此历史包袱,初始 schema 直接不含内置模板行。
> source_cache 表替代原项目的 Workers Cache API,重启后缓存仍有效。
---
## 5. 功能模块开发计划
### Phase 0:项目骨架 (P0)
**目标**:可编译、可启动、可连接数据库
| 任务 | 产出 | 依赖库 |
|------|------|--------|
| go mod init + 依赖引入 | `go.mod` | 全部 |
| cobra root/serve/version 命令 | `cmd/*.go` | cobra |
| viper 配置加载 | `internal/config/config.go` | viper |
| logrus 日志初始化 | 日志格式化、级别、输出 | logrus |
| SQLite 连接初始化 | `internal/database/db.go` | modernc/sqlite, sqlx |
| goose 迁移执行(启动时自动) | `internal/database/migrations.go` | goose |
| 3 个 migration SQL 文件 | `internal/database/migrations/` | |
| Fiber app 骨架 | 路由注册、健康检查 | fiber/v3 |
| 优雅关闭 | signal handling | |
**验证**`sub-store serve` 启动,`GET /health` 返回 200,数据库表创建成功。
---
### Phase 1:数据层 (P0)
**目标**6 张表的完整 CRUDRepository 模式
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| 类型定义 | `internal/model/*.go` | `types.ts` 全部类型 |
| Target 别名归一化 | `model/target.go` | `targets.ts` |
| SourceRepository | `source_repo.go` | `store.ts` listSources/getSource/upsertSource/deleteSource/sortSources |
| CollectionRepository | `collection_repo.go` | `store.ts` listCollections/... |
| TemplateRepository | `template_repo.go` | `store.ts` listTemplates/... + 内置模板合并 |
| SettingsRepository | `settings_repo.go` | `store.ts` getSettings/updateSettings (深合并) |
| GrantRepository | `grant_repo.go` | `compatibility-resources.ts` |
| RecycleRepository | `recycle_repo.go` | `compatibility-resources.ts` |
| 内置模板定义 | `internal/template/builtin.go` | `defaults.ts` 6 套模板 |
| ID 归一化 `toId()` | `util/` | `store.ts` toId() |
| 深合并 `mergeDeep()` | `util/` | `store.ts` mergeDeep() |
| 排序逻辑 (created_at = now+index) | Repository 层 | `store.ts` sortSources/sortCollections |
**关键设计**
- 内置模板在 Go 代码中定义为 `var BuiltinTemplates``ListTemplates` 合并内置 + DB 查询
- `upsert` 使用 `INSERT ... ON CONFLICT(id) DO UPDATE SET ...`
- `sortSources` 使用事务批量 `UPDATE SET created_at = ? WHERE id = ?`
- settings 深合并:递归 merge map[string]interface{}
**验证**:单元测试覆盖所有 Repository 的 CRUD + 排序 + 深合并。
---
### Phase 2:中间件 + 基础 API (P0)
**目标**:鉴权、CORS、安全头、基础 API 端点
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| Admin 鉴权中间件 | `middleware/auth.go` | `http.ts` requireAdmin + isTokenValid |
| Token 验证 (SHA-256 + ConstantTimeCompare) | `util/token.go` | `http.ts` isTokenValid |
| Bearer token 提取 | `middleware/auth.go` | `http.ts` getBearerToken |
| CORS 中间件 | `middleware/cors.go` | `http.ts` applyCorsHeaders |
| 安全头中间件 | `middleware/security.go` | `http.ts` applySecurityHeaders |
| Body limit 中间件 | `middleware/bodylimit.go` | `bodyLimit()` |
| 下载域名隔离 | `middleware/downloadhost.go` | `index.ts` hostname 检查 |
| 统一响应格式 | `model/response.go` | `http.ts` success/failed |
| `GET /api/env` | `handler/env.go` | `api.ts` envPayload |
| `GET /api/settings` + `PATCH /api/settings` | `handler/settings.go` | `api.ts` settings |
| `GET/POST /api/storage` | `handler/storage.go` | `api.ts` exportStorage/importStorage |
| 静态文件服务 | `embed.FS` + Fiber static | `index.ts` ASSETS fallback |
| 全局错误处理 | Fiber error handler | `index.ts` app.onError |
| SPA fallback | 未匹配路由返回 index.html | `notFound` handler |
**验证**`curl -H "Authorization: Bearer <token>" /api/env` 返回环境信息。
---
### Phase 3:源/集合/模板 API (P0)
**目标**:完整的管理 API,不含订阅处理
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| Source CRUD handler | `handler/source.go` | `api.ts` /api/sources 全部端点 |
| Collection CRUD handler | `handler/collection.go` | `api.ts` /api/collections |
| Template CRUD handler | `handler/template.go` | `api.ts` /api/templates |
| 排序 API | `handler/source.go` | `api.ts` PUT /api/sources, POST /api/sort/sources |
| ID 验证 `^[a-z0-9_-]{1,64}$` | handler 层 | `api.ts` validateRecordId |
| Source 验证 (remote URL / local content) | handler 层 | `api.ts` validateSource |
| Collection 验证 (sourceIds 引用检查) | handler 层 | `api.ts` validateCollection |
| 内置模板保护 (不可删改) | handler 层 | `api.ts` BUILTIN_TEMPLATE_IDS |
| 删除时归档到回收站 | handler 层 | `api.ts` archiveAndDeleteResource |
| 引用检查 (source 被 collection 引用时不可删) | handler 层 | `api.ts` deleteSource |
| Template config 解析 (JSON/YAML → 归一化) | handler 层 | `api.ts` parseTemplateConfig |
| Template alias 归一化 (mixed-port→mixedPort) | `template/` | `store.ts` normalizeMihomoTemplateConfig |
**验证**:完整 API 测试 — 创建源/集合/模板,查询、更新、排序、删除(含回收站归档)。
---
### Phase 4:代理协议解析器 (P0) ⭐ 核心模块
**目标**:解析 13 种 URI 协议 + 2 种客户端配置行格式
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| ProxyNode 类型 | `model/types.go` | `types.ts` ProxyNode |
| 格式检测 (JSON/YAML/URI 行) | `proxy/format.go` | `subscription.ts` looksLikeStructuredSubscription |
| Base64 解码 (maybe) | `proxy/format.go` | `subscription.ts` decodeMaybeBase64 |
| 统一入口 parseProxies | `proxy/parser.go` | `subscription.ts` parseProxies |
| JSON 代理数组解析 | `proxy/parser.go` | `subscription.ts` parseJsonProxies |
| YAML 代理数组解析 | `proxy/parser.go` | `subscription.ts` parseYamlProxies |
| URI 行解析入口 | `proxy/uri_parser.go` | `subscription.ts` parseProxyLines + parseProxyUri |
| `vless://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseVless |
| `vmess://` 解析 (base64 JSON) | `proxy/uri_parser.go` | `subscription.ts` parseVmess |
| `trojan://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseTrojan |
| `ss://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseShadowsocks |
| `ssr://` 解析 (base64) | `proxy/uri_parser.go` | `subscription.ts` parseShadowsocksR |
| `hysteria://` / `hy://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseHysteria |
| `hysteria2://` / `hy2://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseHysteria2 |
| `tuic://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseTuic |
| `anytls://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseAnytls |
| `socks://` / `socks5://` / `socks5+tls://` | `proxy/uri_parser.go` | `subscription.ts` parseSocks |
| `http://` / `https://` (HTTP proxy) | `proxy/uri_parser.go` | `subscription.ts` parseHttpProxy |
| `wireguard://` / `wg://` | `proxy/uri_parser.go` | `subscription.ts` parseWireGuard |
| QX 配置行解析 | `proxy/client_parser.go` | `subscription.ts` parseQxProxyLine |
| Surge/Loon 配置行解析 | `proxy/client_parser.go` | `subscription.ts` parseNamedClientProxyLine |
| 客户端选项解析 (CSV + key=value) | `proxy/client_parser.go` | `subscription.ts` splitClientCsv/parseClientOptions |
| 节点归一化 + stripUndefined | `proxy/normalize.go` | `subscription.ts` normalizeProxy/stripUndefined |
| 预览 ID 生成 | `proxy/normalize.go` | `subscription.ts` addPreviewIds/stableProxyId |
**正则兼容性注意**:原项目用 JS RegExpGo 用 RE2。RE2 不支持反向引用和 lookahead/lookbehind。需要检查以下正则:
- `looksLikeStructuredSubscription` 中的正则 — 应兼容
- `detectFlag` 中的地区匹配正则 — 应兼容
- `compileRegex``(?i)` 前缀 — Go 用 `(?i)` 内联标志,兼容
- filter 中的用户自定义正则 — 大部分兼容,极少数 lookahead 场景需文档说明限制
**验证**:为每种协议准备测试用例(URI → ProxyNode 结构比对),覆盖正常/异常/边界。
---
### Phase 5:过滤器管线 (P0) ⭐ 核心模块
**目标**11 种 FilterRule 类型的完整执行
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| 管线执行器 applyFilters | `filter/pipeline.go` | `subscription.ts` applyFilters |
| include/exclude | `filter/include_exclude.go` | `subscription.ts` matchFilter |
| rename | `filter/rename.go` | `subscription.ts` renameProxies |
| delete-field | `filter/delete_field.go` | `subscription.ts` deleteFieldMatches |
| dedupe (delete + rename 两种模式) | `filter/dedupe.go` | `subscription.ts` handleDuplicateProxies |
| sort (asc/desc/random) | `filter/sort.go` | `subscription.ts` sortProxies |
| regex-sort | `filter/sort.go` | `subscription.ts` regexSortProxies |
| flag (add/remove + 台湾旗) | `filter/flag.go` | `subscription.ts` flagProxies |
| quick (udp/tfo/scert/useless/vmess-aead) | `filter/quick.go` | `subscription.ts` applyQuickSettings |
| resolve (DNS DoH 解析) | `filter/resolve.go` | `subscription.ts` resolveProxyDomains |
| custom (声明式规则链,替代 JS 脚本) | `filter/custom.go` | `subscription.ts` applyScriptAction |
| 点号路径 get/set | `util/path.go` | `subscription.ts` getByPath/setByPath |
| 正则编译 (支持 `(?i)` 前缀) | `util/` | `subscription.ts` compileRegex |
| 国旗检测/移除 | `util/flag.go` | `subscription.ts` detectFlag/removeFlag |
| ensureUniqueProxyNames | `filter/pipeline.go` | `subscription.ts` ensureUniqueProxyNames |
| IP 地址检测 (IPv4/IPv6) | `util/ip.go` | `subscription.ts` isIpv4/isIpv6 |
| isUsefulProxy | `filter/quick.go` | `subscription.ts` isUsefulProxy |
| crypto/rand 安全随机 (shuffle) | `filter/sort.go` | `subscription.ts` secureRandomInt |
**resolve 过滤器细节**
- 5 个 DoH provider: Cloudflare / Google / Ali / Tencent / Custom
- DoH JSON API: `?name=<host>&type=A|AAAA`
- 并发解析 + 过滤模式 (disabled/removeFailed/IPOnly/IPv4Only/IPv6Only)
- 保留原域名为 servername/sni
**验证**:每种 filter 类型独立单元测试 + 管线顺序执行测试。
---
### Phase 6:目标渲染器 (P0) ⭐ 核心模块
**目标**13 种客户端格式的完整渲染
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| 渲染入口 renderTarget | `render/` 分发 | `subscription.ts` renderTarget/renderBuildTarget |
| mihomo YAML (proxy-groups 展开 + 模板) | `render/mihomo.go` | `subscription.ts` renderMihomoYaml |
| proxy-group $all 展开 + filter 正则 | `render/mihomo.go` | `subscription.ts` expandGroupProxies |
| surge 文本行 | `render/surge.go` | `subscription.ts` toSurgeProxyLine |
| surge-mac (ssh/h2-connect/snell) | `render/surge.go` | `subscription.ts` toSurgeMacProxyLine |
| surfboard (ss/vmess/trojan/http/socks5) | `render/surfboard.go` | `subscription.ts` toSurfboardProxyLine |
| loon 文本行 | `render/loon.go` | `subscription.ts` toLoonProxyLine |
| quantumult x 文本行 | `render/qx.go` | `subscription.ts` toQxProxyLine |
| egern YAML | `render/egern.go` | `subscription.ts` toEgernProxy |
| sing-box JSON (含 PROXY/AUTO/DIRECT/REJECT) | `render/singbox.go` | `subscription.ts` renderSingBoxJson |
| URI 行 (13 种协议) | `render/uri.go` | `subscription.ts` toProxyUri |
| v2ray (base64 编码 URI 行) | `render/uri.go` | `subscription.ts` base64Utf8 |
| JSON 原始 | `render/json.go` | `subscription.ts` JSON.stringify |
| 目标兼容性过滤 isTargetCompatible | `render/` | `subscription.ts` isTargetCompatible |
| Content-Type 分配 | `render/` | `subscription.ts` getTargetContentType |
| 文本代理行工具 (joinTextProxy/quoteTextValue/sanitizeName) | `render/` | `subscription.ts` joinTextProxy 等 |
**验证**:每种目标格式用固定 ProxyNode 集合渲染,比对输出。sing-box 输出需通过 JSON schema 验证。
---
### Phase 7:订阅处理管线 (P0) ⭐ 核心模块
**目标**:从源/集合到最终输出的完整管线
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| buildSubscriptionResult 入口 | `service/subscription.go` | `subscription.ts` buildSubscriptionResult |
| getSources (单源/集合模式) | `service/subscription.go` | `subscription.ts` getSources |
| loadProxyNodes (并发 + ignoreFailed) | `service/subscription.go` | `subscription.ts` loadProxyNodes |
| 本地源加载 | `service/subscription.go` | `subscription.ts` loadSubscriptionRaw (local 分支) |
| 远程源 URL 分割 (多行多 URL) | `service/fetcher.go` | `subscription.ts` splitSourceUrls |
| 远程源抓取 (HTTP + 缓存 + 304 + stale) | `service/fetcher.go` | `subscription.ts` fetchSubscriptionUrl |
| UA 选择逻辑 | `service/fetcher.go` | `subscription.ts` getSourceUserAgent |
| 并发控制 worker pool | `service/concurrency.go` | `subscription.ts` runWithConcurrency/runSettledWithConcurrency |
| 缓存层 (SQLite source_cache 表 + SHA-256 key) | `database/cache_repo.go` | `subscription.ts` remoteCacheKey/safeCacheMatch/safeCachePut |
| 缓存元数据存储 (subscription-userinfo 等) | `database/cache_repo.go` | `subscription.ts` setInternalMetadataHeader |
| 响应元数据选择 | `service/subscription.go` | `subscription.ts` selectResponseMetadata |
| 元数据从 source/Response 提取 | `service/subscription.go` | `subscription.ts` metadataFromSource/metadataFromResponse |
| 流式响应读取 (限字节) | `service/fetcher.go` | `read.ts` readResponseText |
| 预览 (previewSource/previewCollection) | `service/subscription.go` | `subscription.ts` previewSubscription/previewSourceContent |
| 一次性转换 (convertSubscriptionContent) | `service/subscription.go` | `subscription.ts` convertSubscriptionContent |
| 下载链接构建 | `handler/link.go` | `api.ts` buildDownloadLink |
| 流量信息查询 (fetchFlowHeaders + parse) | `handler/flow.go` | `api.ts` fetchFlowHeaders/parseFlowHeaders |
**缓存设计**(替代 Workers Cache API):
- 使用 SQLite `source_cache` 表持久化缓存(无需额外依赖,重启不丢)
- key: `sha256(url + "\n" + userAgent)` → 存储内容 + 元数据 JSON
- TTL: 配置项 `cache_ttl`,默认 300s
- 304 Not Modified: HTTP 请求携带 `If-None-Match` / `If-Modified-Since`
- stale on error: 配置项 `cache_stale_on_error`
- 异步写缓存: `go func()` (替代 `waitUntil`)
- 定期清理: `DELETE FROM source_cache WHERE cached_at + ttl < strftime('%s','now')`
**并发设计**
```go
// runWithConcurrency: 固定 worker 池
func RunWithConcurrency[T any](tasks []func() (T, error), concurrency int, wait time.Duration) ([]T, error)
// runSettledWithConcurrency: allSettled 语义 (失败跳过)
func RunSettledWithConcurrency[T any](tasks []func() (T, error), concurrency int, wait time.Duration) ([]Result[T])
```
**验证**:端到端测试 — 创建 remote 源 + local 源 + 集合,下载 mihomo/sing-box/uri 格式,验证节点数量和内容正确。
---
### Phase 8:下载路由 + 分享/回收站 API (P1)
**目标**:公开下载端点 + 完整的分享/回收站功能
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| `GET /collections/:name/:token` | `handler/download.go` | `download.ts` |
| `GET /sources/:name/:token` | `handler/download.go` | `download.ts` |
| Download token 验证 (全局 + scoped grant) | `handler/download.go` | `download.ts` rejectInvalidDownloadToken |
| Target 推断 (显式 + UA 推断) | `handler/download.go` | `download.ts` getDownloadTarget |
| 临时源覆盖 (url/content/ua query) | `handler/download.go` | `download.ts` getTemporarySourceOverride |
| 响应头设置 (content-type, userinfo, etc.) | `handler/download.go` | `download.ts` renderDownload |
| Share CRUD handler | `handler/share.go` | `api.ts` /api/shares |
| Grant token 生成 (24 byte random → base64url) | `util/token.go` | `compatibility-resources.ts` randomToken |
| Grant token hash (SHA-256 hex) | `util/token.go` | `compatibility-resources.ts` sha256Hex |
| Scoped download 授权检查 | `handler/download.go` | `compatibility-resources.ts` authorizeScopedDownload |
| Recycle bin CRUD handler | `handler/recycle.go` | `api.ts` /api/recycle-bin |
| 回收站恢复逻辑 (source/collection/template/share) | `handler/recycle.go` | `api.ts` restore |
| 回收站自动裁剪 (50 条上限) | `recycle_repo.go` | `compatibility-resources.ts` archiveAndDeleteResource |
**验证**:创建分享令牌 → 用令牌下载 → 验证 scoped 限制(资源/目标/过期)。删除源 → 回收站恢复。
---
### Phase 9:工具 API (P1)
**目标**:格式转换器、规则转换、节点信息
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| `POST /api/proxy/parse` | `handler/tools.go` | `api.ts` proxy/parse |
| `POST /api/rule/parse` | `handler/tools.go` | `api.ts` rule/parse |
| 规则转换器 | `rules/converter.go` | `rules.ts` convertRules |
| 规则类型别名归一化 | `rules/converter.go` | `rules.ts` KIND_ALIASES |
| QX 规则特殊处理 | `rules/converter.go` | `rules.ts` qxKind |
| `POST /api/utils/node-info` | `handler/tools.go` | `api.ts` utils/node-info |
| `GET /api/scripts` | `handler/script.go` | `api.ts` scripts |
**规则转换细节**
- 支持规则类型: DOMAIN, DOMAIN-SUFFIX, DOMAIN-KEYWORD, IP-CIDR, IP-CIDR6, GEOIP, GEOSITE, PROCESS-NAME, DST-PORT, MATCH
- 别名映射: HOST→DOMAIN, FINAL→MATCH, DEST-PORT→DST-PORT 等
- QX 特殊: DOMAIN→HOST, IP-CIDR6→IP6-CIDR, MATCH→FINAL
- 输入格式: YAML (`rules:` 列表) 或纯文本行
**验证**:各目标格式规则转换测试 + 节点信息查询测试。
---
### Phase 10:声明式自定义规则 (P2) — 合并入 Phase 5
> 原计划的 goja JS 脚本引擎已移除。原项目的脚本能力(filter/operator 自定义 JS)改为声明式规则链实现,归入 Phase 5 过滤器管线的 `custom` 类型。
**目标**:用声明式规则组合替代 JS 脚本,覆盖原脚本的所有实际使用场景
**设计**`custom` filter 类型,内部为有序规则链,每条规则是一个原子操作:
```json
{
"type": "custom",
"args": {
"rules": [
{ "action": "set", "field": "name", "mode": "regex-replace", "pattern": "【.*?】", "replacement": "" },
{ "action": "include", "field": "name", "pattern": "香港|日本" },
{ "action": "set", "field": "udp", "value": true },
{ "action": "delete", "field": "tls.skipCertVerify" },
{ "action": "rename", "template": "{country}-{server}" }
]
}
}
```
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| custom filter 类型实现 | `filter/custom.go` | `scripts.ts` applyScriptAction |
| 原子操作:set/delete/include/exclude | `filter/custom.go` | 脚本中对节点字段的操作 |
| 原子操作:regex-replace (字段值正则替换) | `filter/custom.go` | 脚本中 `p.name.replace(/.../, ...)` |
| 原子操作:rename (模板化重命名) | `filter/custom.go` | 脚本中节点名拼接逻辑 |
| 原子操作:条件分支 (when 字段匹配) | `filter/custom.go` | 脚本中 if/else 逻辑 |
| 参数验证 | `filter/custom.go` | `scripts.ts` validateScriptActions |
| `$arguments` 支持 | custom args 直接映射 | `scripts.ts` $arguments |
| ProxyUtils 等价能力 | 复用 util/flag.go, util/ip.go, util/base64.go | `scripts.ts` PROXY_UTILS |
**与原项目差异**
- 原项目:用户编写 JS 函数,运行时闭包/eval 执行
- Go 版:用户配置声明式规则链,Go 原生执行
- 优势:无任意代码执行风险、无需 JS 引擎、前端可构建可视化规则编辑器、性能更高
- 限制:不支持 JS 的完整编程能力(循环、递归、复杂条件组合),但实际场景中 99% 的脚本都是字段操作 + 正则 + 条件过滤,声明式规则完全覆盖
**验证**:将原项目常见脚本用例转换为声明式规则,验证等效输出。
---
### Phase 11:集成与打磨 (P1)
| 任务 | 说明 |
|------|------|
| 前端 embed.FS 集成 | `//go:embed web/dist`Fiber static + SPA fallback |
| 请求日志中间件 | logrus 结构化日志 |
| Panic recovery 中间件 | Fiber Recover |
| 速率限制 (可选) | Fiber limiter 中间件 |
| 配置热更新 (可选) | viper WatchConfig |
| 健康检查端点 | `GET /health` |
| pprof 端点 (可选) | debug 模式 |
| 集成测试 | 端到端 API 测试 |
| Dockerfile | 多阶段构建 |
| systemd service 文件 | 可选,用户提供时生成 |
---
## 6. API 端点完整清单
### 管理 API (需 Admin Token)
| 方法 | 路径 | Phase | 说明 |
|------|------|-------|------|
| GET | `/api/env` | 2 | 环境信息 |
| GET | `/api/scripts` | 9 | 脚本注册表 (仅元数据,供前端展示可选脚本) |
| GET | `/api/settings` | 2 | 应用设置 |
| PATCH | `/api/settings` | 2 | 更新设置 |
| GET | `/api/storage` | 2 | 导出备份 |
| POST | `/api/storage` | 2 | 导入备份 |
| GET | `/api/sources` | 3 | 源列表 |
| POST | `/api/sources` | 3 | 创建源 |
| PUT | `/api/sources` | 3 | 排序源 |
| POST | `/api/sort/sources` | 3 | 排序源 (兼容) |
| GET | `/api/sources/:name` | 3 | 查询单源 |
| PATCH | `/api/sources/:name` | 3 | 更新源 |
| DELETE | `/api/sources/:name` | 3 | 删除源 |
| GET | `/api/collections` | 3 | 集合列表 |
| POST | `/api/collections` | 3 | 创建集合 |
| PUT | `/api/collections` | 3 | 排序集合 |
| POST | `/api/sort/collections` | 3 | 排序集合 (兼容) |
| GET | `/api/collections/:name` | 3 | 查询单集合 |
| PATCH | `/api/collections/:name` | 3 | 更新集合 |
| DELETE | `/api/collections/:name` | 3 | 删除集合 |
| GET | `/api/templates` | 3 | 模板列表 |
| POST | `/api/templates` | 3 | 创建模板 |
| GET | `/api/templates/:name` | 3 | 查询模板 |
| PATCH | `/api/templates/:name` | 3 | 更新模板 |
| DELETE | `/api/templates/:name` | 3 | 删除模板 |
| GET | `/api/shares` | 8 | 分享列表 |
| POST | `/api/shares` | 8 | 创建分享 |
| PATCH | `/api/shares/:id` | 8 | 更新分享 |
| DELETE | `/api/shares/:id` | 8 | 删除分享 |
| GET | `/api/recycle-bin` | 8 | 回收站列表 |
| DELETE | `/api/recycle-bin/:id` | 8 | 彻底删除 |
| POST | `/api/recycle-bin/:id/restore` | 8 | 恢复 |
| POST | `/api/preview/source` | 7 | 源预览 |
| POST | `/api/preview/collection` | 7 | 集合预览 |
| GET | `/api/link/source/:name` | 7 | 源下载链接 |
| GET | `/api/link/collection/:name` | 7 | 集合下载链接 |
| GET | `/api/source/flow/:name` | 7 | 流量信息 |
| POST | `/api/proxy/parse` | 9 | 代理转换 |
| POST | `/api/rule/parse` | 9 | 规则转换 |
| POST | `/api/utils/node-info` | 9 | 节点信息 |
### 公开下载 API (需 Download Token 或 Scoped Grant)
| 方法 | 路径 | Phase | 说明 |
|------|------|-------|------|
| GET | `/collections/:name/:token` | 8 | 下载集合 |
| GET | `/sources/:name/:token` | 8 | 下载源 |
### 其他
| 方法 | 路径 | Phase | 说明 |
|------|------|-------|------|
| GET | `/health` | 0 | 健康检查 |
| GET | `/*` | 11 | 静态文件 (SPA fallback) |
---
## 7. 开发顺序与依赖关系
```
Phase 0: 项目骨架
├──→ Phase 1: 数据层 (model + repository)
│ │
│ └──→ Phase 2: 中间件 + 基础 API
│ │
│ └──→ Phase 3: 源/集合/模板 API
│ │
│ ├──→ Phase 4: 代理协议解析器 ─────┐
│ │ │
│ ├──→ Phase 5: 过滤器管线 ────────┤
│ │ │
│ └──→ Phase 6: 目标渲染器 ────────┤
│ │
│ ↓
│ Phase 7: 订阅处理管线 (依赖 4+5+6)
│ │
│ ├──→ Phase 8: 下载 + 分享/回收站
│ │
│ └──→ Phase 9: 工具 API
│ │
└────────────────────────────────────┴──→ Phase 10: 声明式规则 (Phase 5 扩展)
└──→ Phase 11: 集成打磨
```
**关键路径**: Phase 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 11
**可并行**:
- Phase 4/5/6 可并行开发(三者独立,7 依赖三者完成)
- Phase 9 可与 Phase 8 并行
---
## 8. 测试策略
### 8.1 测试分层
| 层级 | 范围 | 工具 |
|------|------|------|
| 单元测试 | 每个函数/方法 | `testing` + `testify/assert` |
| Repository 测试 | 临时 SQLite 文件 | `testing` + sqlx |
| API 集成测试 | HTTP 端到端 | `httptest` + Fiber test utils |
| 协议解析测试 | 每种 URI/客户端格式 | 表驱动测试 |
| 渲染测试 | 每种目标格式 | 固定输入 + golden file 比对 |
| 端到端测试 | 完整订阅流程 | 测试 HTTP server + 真实 HTTP 请求 |
### 8.2 关键测试用例
**协议解析** (Phase 4):
```
vless://uuid@host:443?security=reality&pbk=xxx&fp=chrome&type=ws#name
→ ProxyNode{type:"vless", server:"host", port:443, uuid:"uuid", ...}
```
**过滤器** (Phase 5):
```
input: [节点A(香港), 节点B(日本), 节点C(美国)]
filter: {type:"include", field:"name", pattern:"香港|日本"}
output: [节点A, 节点B]
```
**渲染** (Phase 6):
```
input: [ProxyNode{type:"ss", name:"test", server:"1.2.3.4", port:8388, cipher:"aes-256-gcm", password:"pass"}]
target: mihomo
output: YAML with proxies: [{name:"test", type:"ss", ...}]
```
**端到端** (Phase 7):
```
1. 创建 remote source (url: mock server)
2. 创建 collection (sourceIds: [source.id])
3. GET /collections/:id/xxx?target=mihomo
4. 验证 YAML 输出包含正确的 proxy 节点
```
### 8.3 测试数据
- 13 种协议各准备 2-3 个 URI 测试样本
- 每种 filter 类型准备输入/输出 golden case
- 每种渲染目标准备 golden output file
- Mock 远程订阅服务器返回固定内容
---
## 9. 部署方案
### 9.1 单二进制
```bash
# 构建
go build -o sub-store ./main.go
# 运行(启动时自动执行迁移,幂等)
./sub-store serve --config config.yaml
```
### 9.2 配置
- `--config` flag 指定配置文件路径
- 环境变量 `SUB_STORE_*` 覆盖配置
- admin_token / download_token 必填,启动时检查
### 9.3 Docker (可选,用户需求时)
```dockerfile
# 多阶段构建
FROM golang:1.23 AS builder
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 go build -o /sub-store ./main.go
FROM alpine:latest
COPY --from=builder /sub-store /sub-store
COPY config.yaml /config.yaml
EXPOSE 3000
ENTRYPOINT ["/sub-store", "serve", "--config", "/config.yaml"]
```
> 注:使用 modernc.org/sqlite (纯 Go)`CGO_ENABLED=0` 可静态编译。
---
## 10. 风险与注意事项
### 10.1 正则兼容性
Go 的 `regexp` 包使用 RE2 引擎,不支持:
- 反向引用 (`\1`)
- Lookahead (`(?=...)`, `(?!...)`)
- Lookbehind (`(?<=...)`, `(?<!...)`)
原项目中用户自定义的 filter 正则可能使用这些特性。解决方案:
- 文档说明限制
- 对于 `(?i)` 前缀,Go 支持 `(?i)` 内联标志
- 极端情况考虑引入 `github.com/dlclark/regexp2` (完整 PCRE 支持) 作为回退
### 10.2 JSON5 解析
原项目用 `json5` npm 包解析代理节点 JSON。Go 标准库 `encoding/json` 不支持 JSON5(单引号、尾逗号、注释等)。
解决方案:
- 大多数订阅返回标准 JSON,标准库足够
- 对于需要 JSON5 兼容的场景,使用 `github.com/tidwall/gjson` 进行宽容解析,或 `github.com/yourbasic/json5`
### 10.3 自定义规则安全性
声明式规则链不存在脚本注入风险(无任意代码执行)。需注意:
- 正则 ReDoS:对用户输入的正则设置编译超时或回溯限制
- 规则链深度:限制最大规则数量(默认 32 条)
### 10.4 并发安全
- SQLite 写锁:modernc.org/sqlite 支持 WAL 模式,读写可并发
- `SetMaxOpenConns(1)`:避免多连接 `database is locked`WAL 下读不阻塞写
- `PRAGMA busy_timeout=5000`:写锁等待 5 秒
- source_cache 表:WAL 模式下读写不互斥,缓存读写不影响其他表操作
- 异步缓存写入:`sync.WaitGroup` + `context` 管理生命周期,graceful shutdown 时等待完成(5s 超时)
- 缓存操作错误吞咽:`recover()` + 日志,不影响订阅生成
- Repository 层:无状态,安全
- 全局状态:仅配置和缓存,均为并发安全
### 10.5 ProxyNode 类型与深拷贝
ProxyNode 定义为 `type ProxyNode map[string]any`(详见 `docs/review-resolutions.md` #1)。
- 深拷贝:`json.Marshal``json.Unmarshal`(简单可靠,性能可接受)
- 类型安全访问:`util/path.go` 提供 `GetByPath` / `SetByPath` / `GetString` / `GetInt` / `GetBool` 等辅助函数
- `stripUndefined`:遍历 map,删除值为 `nil``""` 的 key
---
## 11. 原项目代码量评估
| 模块 | 原项目行数 | 预估 Go 行数 | 复杂度 |
|------|-----------|-------------|--------|
| types.ts | 188 | ~250 | 低 |
| store.ts | 644 | ~600 | 中 |
| subscription.ts | 2430 | ~3000 | ⭐ 极高 |
| rules.ts | 159 | ~180 | 低 |
| defaults.ts | 246 | ~300 | 低 |
| targets.ts | 59 | ~80 | 低 |
| scripts.ts | 157 | ~100 | 低 (custom 规则替代,仅保留注册表元数据) |
| compatibility-resources.ts | 261 | ~300 | 中 |
| http.ts | 77 | ~100 | 低 |
| limits.ts | 6 | ~10 | 低 |
| read.ts | 44 | ~50 | 低 |
| api.ts (routes) | 896 | ~1000 | 中 |
| download.ts | 165 | ~200 | 中 |
| index.ts | 72 | ~150 | 低 |
| **合计** | **5404** | **~6320** | |
| + config/cmd/middleware/util | — | ~1500 | |
| **总计** | | **~7800** | |
---
## 12. 里程碑
| 里程碑 | Phase | 交付物 | 验证标准 |
|--------|-------|--------|----------|
| M1: 可启动 | 0-1 | 二进制可运行,DB 初始化 | `serve` 启动,表创建 |
| M2: 基础 API | 2-3 | 源/集合/模板 CRUD | API 测试全通过 |
| M3: 协议解析 | 4 | 13 种 URI 解析 | 解析测试全通过 |
| M4: 过滤+渲染 | 5-6 | 11 种 filter + 13 种渲染 | 单元测试全通过 |
| M5: 订阅管线 | 7 | 完整下载流程 | 端到端测试通过 |
| M6: 完整后端 | 8-9 | 下载/分享/回收站/工具 | 全部 API 测试通过 |
| M7: 声明式规则 | 10 | custom filter 规则链 | 规则转换测试通过 |
| M8: 生产就绪 | 11 | 前端集成/部署/Docker | 集成测试通过 |