Commit Graph

613 Commits

Author SHA1 Message Date
wangjia 344dfe9a82 docs(agent): 记录 auth_user 血泪教训 + 客户端隧道前置 + 验收须真连接
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 07:39:39 +08:00
wangjia 2abe4d0816 fix(agent): ACL 放行用 auth_user 而非 user(VLESS 运行时只认 auth_user)
生产验证发现:sing-box 1.13 的 route rule 里 user 字段对 VLESS/REALITY
入站运行时不匹配(仅 sing-box check 语法通过),导致放行规则永不命中、
白名单用户也被兜底拒绝。本地真 VLESS 连接实测确认 auth_user 生效
(good→通/bad→block),node 端 git 一进一出白名单验证 per-user 生效。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:36:02 +08:00
wangjia e4b527b2c9 docs(agent): 上线验收补「live 生效核实」与「域名+裸IP 双形式测另一账号被拒」
C1 指出 config.json 内容正常不等于线上生效——SIGHUP 走 sing-box 自身校验,若
新配置被拒,它会保留旧实例继续跑,agent 侧仍记"渲染成功"。第 4 步补
systemctl is-active sing-box + journalctl -u sing-box -n5 | grep -v FATAL。

I3 表明 ip_cidr 目的地存在域名形式绕过口子(已修),第 6 步"另一账号被拒"补充
必须分别用域名和裸 IP 两种目的地表达形式各测一遍,只测一种形式验证不到位。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:45:38 +08:00
wangjia aaa3384e3b feat(agent): route 加 resolve action,堵住 ip_cidr 目的地的域名形式绕过
sing-box route 的 ip_cidr 匹配的是已解析的连接目的地地址;客户端若发的是域名
形式(如 nas.51yanmei.com:5001)而不先解析,ip_cidr + port 的拒绝规则不命中,
请求会绕过 ACL 直接落到 final:direct——DSM/RDP/SSH 因而对任何持有有效 dp_uuid
的 pangolin 用户可达,不受白名单约束。攻击者只需让客户端连接目的地时使用域名
而非 IP 即可绕过。

用户已拍板方案:在 sniff 之后紧跟加一条 {"action":"resolve"},让节点自己解析
目的地,使域名形式收敛到 ip_cidr 规则上(已用本机 sing-box 1.13.13 确认接受
[{"action":"sniff"},{"action":"resolve"},...] 这个形状,TestRenderedConfig_
PassesSingBoxCheck 也覆盖了这条产物)。route 规则顺序变为:
sniff → resolve → ACL 放行 → ACL 拒绝 → WARP。resolve 与 sniff 同条件——只在
产出 route 块时才出现,唯一且紧跟 sniff。

代价(用户已知情接受):节点从此会对连接目的地做 DNS 解析,是对"节点只见不透明
dp_uuid、不知道目的地"这条隐私姿态的一次主动后退,详见设计文档 §12。

TDD:更新 TestBuildRoute_Matrix 的四态断言(规则数 2/3/4 → 3/4/5,新增 resolve
计数/位置断言),新增 TestBuildRoute_ResolvePositionedRightAfterSniff(RED:此前
resolve 不存在)与 TestBuildRoute_NeverConfigured_NoResolve(未配置态不受影响)。
确认既有 TestRender_Warp_InjectsEndpointAndRoute(只断言首条 sniff、末条 warp,
不断言精确条数)未受影响,原样绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:44:58 +08:00
wangjia f147ffbe9e fix(agent): 校验 ip_cidr/port,非法即走 fail-closed;渲染产物加 sing-box check 常驻测试
cleanTargets 只做 trim/lower,从不校验取值——port:[70000]、ip_cidr:"not-a-cidr"
这类语法正确但语义非法的值会被顺利渲染进 sing-box 配置。SIGHUP 热重载路径下
sing-box 会自己校验并拒绝这份新配置、保留旧实例继续跑,但 agent 早已把它当
"渲染成功"写盘并记正常日志——于是 /etc/sing-box/config.json 内容看起来正常
(能被 json.load 解析、rules 都在),但线上的 gate 其实完全没生效,而这在
运维层面几乎不可见。

修复:
- LoadACLConfig 解析成功后跑一遍 validate():每个 target 的 ip_cidr 必须能被
  netip.ParsePrefix 解析,每个 port 必须落在 1-65535,否则返回 error——与解析
  失败走同一条路径(fail-closed last-good 兜底),而不是把坏值一路渲染出去。
- 新增 TestRenderedConfig_PassesSingBoxCheck:把渲染结果喂给真实 sing-box 1.13.13
  二进制的 `check` 子命令断言 exit 0,把设计 §9 "产物合法性" 验收项落成常驻测试
  (exec.LookPath 找不到二进制则 t.Skip,不 fail)。本机 sing-box(homebrew 装,
  无 with_v2ray_api tag)会因实验性 v2ray_api 段落报编译期不支持的 FATAL——与本
  测试要验的 ACL/route 语法合法性无关,故只在这条测试内剥离该段落后再校验。

TDD:acl_test.go 新增 TestLoadACLConfig_RejectsInvalidPort / _RejectsInvalidCIDR
(RED:此前返回 nil error)、TestACL_InvalidPortFallsBackToLastGood(RED:非法端口
被渲染进配置)。三条现全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:43:18 +08:00
wangjia 8e24a157e1 fix(agent): last-good 双失败必须告警 + 降级日志分级 + derr 落日志
loadACL 终点此前有两个问题:(1) 读磁盘 last-good 的 derr 从不落日志;(2) ALERT
只在 err != nil 时触发,但 acl.json 单纯缺失(LoadACLConfig 按设计返回 nil,nil,
不是 error)时 err 恰好是 nil——于是 acl.json 缺失 + 磁盘 last-good 同时损坏这种
"gate 实质消失"的最糟场景,反而完全没有日志。

修复:
- derr 非 nil 时打 ERROR。
- ALERT 判断改用文件是否曾经存在(os.Stat)而非 err 是否非 nil:acl.json 与
  last-good 均 not-exist → 判定"从未配置过该功能",安静返回;否则(其一存在但
  读取/解析失败)→ ALERT。
- 两条降级 fallback 日志补 WARN 级别标签,配合 ERROR/ALERT 可用
  journalctl | grep -E 'ERROR|WARN|ALERT' 一并抓到。

TDD:acl_test.go 新增三条——双失败必须 ALERT(RED)、从未配置不误报(基线即绿,
防止告警刷屏回归)、降级日志缺 WARN 标签(RED)。全部现绿。

复现细节:双失败场景没有采用"把 acl.json 和 last-good 都整个删除"来复现——那种
状态在文件系统层面与"这台节点从没配置过 ACL"完全无法区分(两次 os.Stat 皆
not-exist),任何仅凭当前文件状态判断的实现都做不出区分,要区分需要额外的持久
标记,超出本 finding 范围。改用"acl.json 缺失 + 磁盘 last-good 存在但损坏"复现,
这是一个可被 os.Stat 命中的信号,也更贴近 §5 描述的真实故障。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:39:30 +08:00
wangjia dbc787ac2e docs(agent): 设计 §12 补 SNI 绕过残留 + brain basic auth 已落地
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:38:16 +08:00
wangjia 63f4325e51 fix(agent): 空/手抖 acl.json 不得静默关闸或覆盖 last-good
loadACL 的成功分支此前无条件把解析出的 ACL 存为内存 last-good 并落盘,不管
它是否真的产出任何规则。encoding/json 忽略未知字段(必须如此,样例文件靠
_comment 自文档)——所以 "targetz"(手抖打错 "targets")或 "targets": [] 都能
解析通过,Targets 变 nil,active() 为 false,规则消失(gate 被静默关闭),且同一次
调用还会拿这份空配置覆盖 acl.last-good.json,销毁唯一的恢复快照。

修复:
- enabled=true 但 cleanTargets() 为空 → 视为加载失败,走 last-good 兜底,
  绝不返回、也绝不持久化这份空配置。
- 只有 active() 为真的配置才更新 last-good;active()=false 只可能是显式
  enabled:false(合法关闭),此时原样返回(不产出规则)但不覆盖磁盘上的
  last-good,保留后续误删/损坏时的恢复能力。
- 白名单为空但 target 有效(拒绝所有人的合法配置)不受影响,仍持久化为
  last-good、仍产出拒绝规则。

TDD:acl_test.go 新增三条 —— 手抖字段名(RED:拒绝规则消失且 last-good 被覆盖)、
显式 enabled:false(RED:last-good 被覆盖)、空白名单但有效 target(基线即绿,
防止修复过度矫正)。三条现全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:37:47 +08:00
wangjia 60ff7cc586 fix(agent): pangolin-agent.service 补 ExecReload,使 systemctl reload 走 SIGHUP 热重载
[Service] 块只有 ExecStart,没有 ExecReload——systemctl reload pangolin-agent
返回「Job type reload is not applicable」,运维只能退回 restart,而 restart 会让
agent 进程内 s.started 复位为 false,首次渲染走冷启动 systemctl restart sing-box,
瞬断全部在线用户。这正是 Task 5 SIGHUP 热重载路径存在的意义所在,却因为 unit 文件
缺一行而完全走不通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:36:11 +08:00
wangjia 1cb494daa6 docs(agent): 计划回填 cleanHosts 去重的执行后修订
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:28:49 +08:00
wangjia 3324c1d687 docs(agent): 私有目的地 ACL 配置样例与常驻校验测试
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:24:51 +08:00
wangjia 6f183ff0df feat(agent): SIGHUP 重读节点本地配置并热重渲染,不踢在线用户
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:15:27 +08:00
wangjia d216e157c3 test(agent): 覆盖 acl.json 被删时仍走 last-good 兜底
删文件走的是 LoadACLConfig 返回 (nil,nil) 的分支,与坏 JSON 的
err!=nil 分支不同,此前无回归覆盖。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:09:02 +08:00
wangjia 87d5afa703 feat(agent): 渲染时读取 ACL,内存+磁盘 last-good 保证 fail-closed 跨重启
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:01:45 +08:00
wangjia 4155fc8542 refactor(agent): route 块改由 buildRoute 统一产出,合并 ACL 与 WARP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:52:39 +08:00
wangjia 597d6ab68b feat(agent): ACL 渲染放行/拒绝规则对,空白名单仍拒绝
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:40:11 +08:00
wangjia d4156b81e2 refactor(agent): cleanDomains 委托 cleanHosts,消除同包重复逻辑
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:32:43 +08:00
wangjia 6df7c9de44 docs(agent): 修复计划未闭合代码块 + Task 6 改用常驻测试校验样例
未闭合的 ```go 让 Task 4-6 的标题落进代码块,task-brief 无法提取;
Task 6 原用 go run 校验样例文件,该文件在 module 外无法解析 import,
改为常驻测试 TestACLExampleFileStaysValid,并断言样例 uuid 为占位符
以防真实 dp_uuid 入库。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:12:06 +08:00
wangjia e4d014ba99 feat(agent): 私有目的地 ACL 配置类型与 fail-closed 加载语义
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:07:15 +08:00
wangjia b71f6038ae chore: gitignore superpowers 执行暂存目录
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:02:37 +08:00
wangjia 9cbffbb727 docs(agent): 私有目的地 ACL 设计方案与实现计划
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:01:19 +08:00
wangjia 74d8c857be feat(server): 私有服务域名分流(PANGOLIN_PRIVATE_SPLIT_DOMAINS)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Failing after 11s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Failing after 9s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Failing after 9s
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Has been cancelled
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Has been cancelled
ci-pangolin / Go — build + test (push) Has been cancelled
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Has been cancelled
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Has been cancelled
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Has been cancelled
家庭内网穿透域名(nas/git/win.yanmeiai.com)的客户端配置渲染:
- DNS 面: 系统解析器(type=local,底层网络)+私有域名规则置顶+reverse_mapping
- 路由面: 私有域名→强制走隧道,钉在 LAN 直连之后、国内分流(geoip-cn)之前
在家: 局域网 DNS 覆盖→私网IP→LAN直连零绕行; 在外: 锚点(frps@ali,国内IP)
不再被 smartRoute 分流成直连、避开安全组限源。env 不配置=行为零变化。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 07:22:16 +08:00
wangjia 43c76fb978 Merge remote-tracking branch 'origin/main' into feat/pay-v2-integration
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 25s
ci-pangolin / Lint — shellcheck (push) Successful in 29s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 22s
ci-pangolin / OpenAPI Sync Check (push) Successful in 40s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 19s
ci-pangolin / Flutter — analyze + test (push) Failing after 4m59s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 1m51s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Failing after 1m33s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m59s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 4s
# Conflicts:
#	docs/index.html
#	server/cmd/server/main.go
2026-07-11 16:44:05 +08:00
wangjia 05922710b2 feat(server): CORS 中间件(白名单跨域,支持用户中心/官网前端)
控制面无 CORS 导致 pangolin.yanmeiai.com 的用户中心跨域打 api.yanmeiai.com 被浏览器拦(Network error)。
加白名单 CORS(env CORS_ALLOWED_ORIGINS,默认 https://pangolin.yanmeiai.com):回显 Origin、应答预检 OPTIONS、
放行 Authorization/Content-Type;不用 '*'、不放行 credentials(走 Bearer token)。
2026-07-11 15:33:07 +08:00
wangjia 1a7b93ed2f feat(server): /buy 自包含购买页(同源,USDT+哪吒,登录→下单→轮询开通)
单文件 HTML(内联 CSS+JS,go:embed)挂在公开 GET /buy,与 /v1/pay 同源免 CORS:
登录 -> 拉取套餐 -> 选 USDT/哪吒下单 -> 按 render_type(crypto_address/redirect/qr)
展示付款信息 -> 轮询订单直到 activated,页面隐藏/关闭时停止轮询。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-11 14:52:21 +08:00
wangjia 1198ecc556 fix(migrations): 恢复线性迁移编号,与生产已应用的对齐
集成分支曾把已上线的 000020_ad_bonus_minutes(#21)/000021_devices_user_scoped_uuid(#27)
两个迁移错误覆盖成 codes_lib_legacy_rename/pay_purchases,与生产(schema_migrations=21)
的实际血统冲突,导致 golang-migrate 在生产上找不到 version 21 文件而报错。
恢复为:020 ad_bonus / 021 devices / 022 codes_lib_legacy_rename / 023 pay_purchases。
生产库副本完整试跑通过:version→23,pay_purchases 建好,subscriptions CHECK 加 'pay',
codes 库表就位,3 用户/3 订阅数据零丢失。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-11 14:25:09 +08:00
wangjia e6897ecda4 build(server): codes 依赖改伪版本 pin(经 gitea),去本地路径 replace
replace github.com/wangjia/codes => 本地路径 换成
require github.com/wangjia/codes v0.0.0-20260710045055-c772d5256794(gitea @ c772d52)。
本地/CI 需配 GOPRIVATE=github.com/wangjia/codes + git insteadOf(github→gitea ssh)。
交叉编译 linux/amd64 已验证:部署走本地编译+传二进制,目标机无需 go/gitea。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-11 09:21:53 +08:00
wangjia db25bccaba feat(website): /buy 加付款类型选择器(测 redirect/qr/crypto_address 三态渲染)
catalog 步骤新增付款类型分段控件(复用 .price-toggle 样式),选中值写入
下单 body 的 metadata.render(字符串),驱动 pay 侧 fake 渠道渲染分支。
method 仍固定 fake,未新增渠道名。
2026-07-11 07:35:09 +08:00
wangjia 43fead8823 feat(website): 本地联调 /buy 测试购买页(登录→下单→mock 付款→轮询开通)+ dev vite proxy 2026-07-11 01:32:11 +08:00
wangjia bc9aa38392 fix(server): pay webhook 兜底补行不再漏停用用户(修静默丢钱)+ 未知事件日志 + PAY_BIZ_SECRET 启动守卫
- webhook 兜底路径(台账缺行按 biz_ref 定位用户)去掉 status='active' 过滤,
  与正常路径(row.UserID 直接开通不看状态)对齐:钱已实收,不因用户被停用
  (banned)而拒绝补行开通,避免 500→pay 12 次重投后死信→静默丢钱。
- 未知 event_type 分支加 slog.Warn,便于将来 pay 侧误注册无 handler 的事件
  类型时能被观测到。
- PAY_BASE_URL 已设但 PAY_BIZ_SECRET 为空时 log.Fatal 拒绝启动,避免出站
  签名失败+入站验签全 401 的静默瘫痪。
- InsertFromWebhookTx 不再把 channel 冒充 method 写入台账(payload 无 method
  字段可复原,留空并加注释,消除台账观感误导)。

新增 TestWebhook_MissingLedgerFallsBackEvenWhenUserSuspended 覆盖 #1:
台账缺行 + 目标用户 banned 时,兜底补行仍成功开通并回 SUCCESS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-11 00:48:57 +08:00
wangjia 390b8b8f84 chore(server): pay v2 接入终验 + OpenAPI 登记 + 交付说明
全量矩阵本地全绿(build/vet/test/run_sqlite_test.sh + 新增 SQLite 文件库带数据
升级彩排 + MySQL 8 容器验证 000021 MODIFY ENUM;client analyze/test 功能全绿,
27 个既有 golden 像素噪声与本次接入无关)。server/api/openapi.yaml 登记 6 个新
Pay 端点(catalog/orders/orderNo/retry/cancel/webhook)+ schema,通过结构校验。

顺手修复 run_mysql_test.sh 缺 multiStatements=true 的既有 bug(与 pay-v2 无关)。
新增 pay_migration_rehearsal_test.go 验证 000021 升级不丢行/AUTOINCREMENT 续序,
并确认一处安全特性:已有 source='pay' 行后 down 会被 CHECK 约束正确拒绝。

交付说明 docs/pay-v2-integration-delivery.html(含测试矩阵/联调 checklist/部署
附录)已登记 docs/index.html,记录一处既有但未修的部署前置阻断项(go.mod 的
wangjia/codes 本地路径 replace 会阻断异机构建,超出本任务授权范围)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-11 00:34:10 +08:00
wangjia 262773a9de fix(client): dispose 不再静默取消支付订单,redirect launchUrl 加失败兜底
- PaymentFlowController 新增 stopPolling():只停轮询 Timer,不发远程 cancel、
  不改 state;PaymentScreen.dispose() 改调它,不再复用 cancel()——离开支付页
  (切左导航 tab)不再等同于放弃订单,cancel() 只留给「取消订单」按钮的显式路径。
- redirect 分支 launchUrl 前加 canLaunchUrl 判定 + try-catch,失败时用
  showPangolinToast 给可见提示(新增 l10n openAlipayFailed,zh/en 均补)。
- purchase_page.dart::_choose 下单后严格判 phase == awaitingPayment 才跳转
  支付页,建单失败(failed)不再误跳。
- 回归测试:payment_pages_test.dart 补两条用例区分 dispose(不触发 cancel、
  state 不归 idle)与「取消订单」按钮(远程 cancel + state 归 idle);
  payment_flow_test.dart 补 stopPolling() 单元测试。
2026-07-11 00:06:03 +08:00
wangjia a5fff28134 feat(client): 购买页三档 + 支付页 render_type 多态(crypto/redirect/qr 预留)+ 导航接线
购买页三档 pro(月/季/年,金额来自 GET /v1/pay/catalog)选档 → 选支付方式 → 下单;
支付页按 session.render_type 多态渲染(crypto_address 地址+精确金额+复制、redirect
外链拉起、qr 预留复制兜底),轮询用 Task 6 的 paymentFlowProvider,409
CURRENCY_MISMATCH 走「换方式开新单」。同时收口 paymentFlowProvider 非 autoDispose
带来的轮询生命周期缺口——支付页 dispose 时停轮询(cancel() 延后到微任务执行,避免
在自身 unmount 期间同步改 state 炸 Riverpod 断言)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 23:50:35 +08:00
wangjia e66f79fdf6 feat(client): pay 支付领域模型 + PaymentApi + 支付流控制器(轮询 activated)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 23:19:57 +08:00
wangjia 16b8823f1f feat(server): pay webhook 接收器(验签/时间窗/nonce + out_trade_no 幂等开通,回 SUCCESS)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 22:38:01 +08:00
wangjia 932953bafe feat(server): /v1/pay 下单代理端点 + 购买台账(JWT 鉴权,user→biz_ref 映射)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 22:32:43 +08:00
wangjia db2461ed1d feat(server): pay v2 出站客户端(HMAC 签名下单/查单/retry/cancel) 2026-07-10 20:52:33 +08:00
wangjia 7370b17bd9 feat(server): codes.GrantPaidSubscriptionTx 提炼订阅叠加语义供 pay 复用
CreateSubscription/applySubscription 加 source 参数(兑换路径传 "code"
零行为变化);新导出 GrantPaidSubscriptionTx(ctx, tx, ...) 供 pay 侧在
调用方事务内以 source='pay' 复用同一段叠加语义(同 plan 活跃订阅原地
延长,否则新建行),写 pay_grant 审计。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 20:49:25 +08:00
wangjia 85140edf1c feat(server): 迁移 000021 pay_purchases 台账 + subscriptions.source 扩 pay
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 20:46:13 +08:00
wangjia 3df5867c02 docs: pangolin×pay-v2 接入计划(9任务:迁移/codes提炼/PayClient/代理/webhook/Flutter购买支付页/终验)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 20:39:09 +08:00
wangjia 16855a533a chore(server): 清理 stale code_batches 注释 2026-07-10 15:03:45 +08:00
wangjia db3428f622 test(server): mysql 集成测试对齐 codes 库新表(断言不变,环境搭建换 ApplyMigrations)(#codes-lib) 2026-07-10 15:03:43 +08:00
wangjia ef4ed47759 feat(server): admin 批次列表/作废改指 codes 库新表,清理被取代的旧 store 方法(#codes-lib) 2026-07-10 14:53:29 +08:00
wangjia d1e2fed563 feat(server): webhook 薄壳化——保留 X-Pangolin HMAC 协议,铸码走库单事务原语(#codes-lib) 2026-07-10 14:45:08 +08:00
wangjia 0822d22e2c feat(server): Redeem 换芯 GuardedRedeem——grant 回调同事务复刻订阅叠加+双审计(#codes-lib) 2026-07-10 14:34:30 +08:00
wangjia e128c96d22 feat(server): CreateBatch/生成器切换到 codes 库(Mint 单事务全成或全无)(#codes-lib)
Store 挂库 store(NewStore 内部构造 libcodes.Store,签名不变);
generator.go 三函数委托 libcodes,ErrDuplicate 别名同一哨兵;
Service.CreateBatch 走 libcodes.Mint(签名不变)。openMigratedSQLite
挪到共享 sqlite_helper_test.go,backfill_test.go/service_sqlite_test.go
共用。Store.CreateBatch/CreateCode/CodeExistsByHash 原样保留供
webhook.go(Task 6 改用新原语,Task 7 删除)。
2026-07-10 14:24:18 +08:00
wangjia 15f0d78a91 feat(server): legacy 码表 → codes 库新表的幂等回填(Go 双方言)(#codes-lib) 2026-07-10 14:13:21 +08:00
wangjia 7a4c9b97d0 fix(server): migrate down 兼容 codes 库自建表(先 DROP 再改名回)+ 真实接线回环测试(#codes-lib)
Reviewer 复现:cmd/migrate up 接线 ApplyCodesLibMigrations 后,codes 库自建的
codes/codes_batches/codes_audit_log/codes_schema_migrations 四张表不受
golang-migrate 追踪;000020 的 down 脚本做 legacy_codes -> codes 改名回时,
撞上库自建的同名 codes 表,报 "table already exists" 硬失败。

000020 的 sqlite/mysql down 脚本改名前先 DROP TABLE IF EXISTS 掉库自建四张表,
并注明:down = 完整回滚「rename + 库建表」组合,库表数据随 down 销毁属预期
(权威数据仍在 legacy_* 表,回填是后续任务,回填落地后 down 语义需重新审视)。

新增 TestCodesLibMigrateRoundTrip 走真实接线路径(MigrateUp ->
ApplyCodesLibMigrations -> 单步撤销 000020 断言 legacy 复原/库表清空 ->
撤销回去 -> 再跑 ApplyCodesLibMigrations 验幂等 -> 全量 store.MigrateDown 即
cmd/migrate down 的真实调用路径,确认不再硬失败)。先用 git stash 掉修复验证
测试能精确复现 reviewer 报的 "table already exists" 报错(RED),再恢复修复
转绿(GREEN)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 14:06:01 +08:00
wangjia 5a9b5f1d8e feat(server): cmd/migrate up 接入 codes 库自建迁移(ApplyCodesLibMigrations)(#codes-lib)
顺带修 000020 sqlite 索引名冲突:ALTER TABLE RENAME 不会带着重命名索引,
legacy_codes 上遗留的 idx_codes_status 与 codes 库自己迁移里同名索引撞名,
一并改成 idx_legacy_codes_status(up/down 对称)。
2026-07-10 13:55:14 +08:00
wangjia 665749a90b feat(server): 迁移000020 旧码表更名封存 legacy_*(为 codes 库表让位)(#codes-lib)
RENAME codes/code_batches -> legacy_codes/legacy_code_batches(双方言),
为共享库 github.com/wangjia/codes 的同名 codes 表让位。sqlite_migrate_test.go
同步到 version=20 + legacy_* 表清单。

顺带 skip TestSQLite_CodesRedeemFlow(internal/store/sqlite_stores_test.go):
该测试直调 internal/codes.Store 的低层方法(CreateBatch/CreateCode/
FindCodeByHashForUpdate/MarkRedeemed),现在指向被更名的旧表。计划本身在
Task 5(Store 换芯 + Redeem 走库)显式删除此测试、由 Service 级 sqlite 用例
接管等价覆盖 —— 这里先 skip 而非改写/删除,保持 go test ./... 全绿,避免
在 Task 1 提前动到 Task 4/5 范围内的 internal/codes/store.go。
2026-07-10 13:46:17 +08:00