<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>胡萝卜的博客</title><description>记录技术、项目与思考</description><link>http://localhost:4321/</link><language>zh_CN</language><item><title>三端的部署：jar 脚本与 Caddy 静态托管</title><link>http://localhost:4321/posts/moneythunder-deploy/</link><guid isPermaLink="true">http://localhost:4321/posts/moneythunder-deploy/</guid><description>build-jar/deploy-jar 管后端，Caddy 托管后台，docker-compose 编排，H2 让演示零依赖。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;后端：jar 脚本两步走&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;build-jar.sh&lt;/code&gt; 打包，&lt;code&gt;deploy-jar.sh&lt;/code&gt; 部署，&lt;code&gt;backend.jar&lt;/code&gt; 构建产物可直接跑。H2 内嵌数据库意味着&lt;strong&gt;演示环境零依赖&lt;/strong&gt;：一个 jar + 一个脚本，借台机器就能跑通全链路。生产切 MySQL 改配置即可，JPA 兜底。&lt;/p&gt;
&lt;h2&gt;后台：Caddy 静态托管&lt;/h2&gt;
&lt;p&gt;Vue 构建产物是纯静态，&lt;code&gt;Caddyfile&lt;/code&gt; 一行反代 + 自动 HTTPS，&lt;code&gt;docker-compose.yml&lt;/code&gt; 编排。&lt;code&gt;dev.sh&lt;/code&gt; 一键起开发环境（vite 代理后端 6880），&lt;code&gt;dist/&lt;/code&gt; 产物 &lt;code&gt;deploy-dist.sh&lt;/code&gt; 发版——前后端发版节奏解耦。&lt;/p&gt;
&lt;h2&gt;演示闭环&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd backend &amp;amp;&amp;amp; ./mvnw spring-boot:run  # H2，无需 MySQL
cd admin &amp;amp;&amp;amp; ./dev.sh                   # http://localhost:8848
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新成员（或面试官）两条命令看到完整系统，&lt;strong&gt;onboarding 成本就是项目的脸面&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;money_thunder 系列五篇完：架构、后端、后台、采集端、部署。三端齐了。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>拼车分摊：中转站的计费与费率</title><link>http://localhost:4321/posts/sub2api-billing/</link><guid isPermaLink="true">http://localhost:4321/posts/sub2api-billing/</guid><description>Token 级追踪、订阅拼车分摊成本、内置支付自助充值，中转站的钱要算到每一笔。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;商业模式一句话&lt;/h2&gt;
&lt;p&gt;官方订阅按月固定，个人用不完、团队不够用——中转站把订阅配额&lt;strong&gt;拼车分摊&lt;/strong&gt;，用户按 token 消费，成本摊薄。这就是 sub2api 存在的经济学。&lt;/p&gt;
&lt;h2&gt;Token 级账单&lt;/h2&gt;
&lt;p&gt;用量追踪精确到 token，成本逐笔可算。拼车场景下这是信任基础：&lt;strong&gt;账单可核验，站长才敢收钱，用户才敢充值&lt;/strong&gt;。分组计费（不同分组不同倍率）让高峰模型和便宜模型各归其位。&lt;/p&gt;
&lt;h2&gt;内置支付：充值闭环&lt;/h2&gt;
&lt;p&gt;EasyPay、支付宝、微信、Stripe 自助充值内置在网关里，不用另搭支付服务。用户侧：充值→调用→扣费→查账单，全自助；站长侧：定价、分组、赠额，全配置。&lt;/p&gt;
&lt;h2&gt;我的计费清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;计量先行：先有准确的 token 计数，再谈定价&lt;/li&gt;
&lt;li&gt;分组隔离：测试分组、高峰分组、福利分区分开&lt;/li&gt;
&lt;li&gt;配额被动显示：上游配额只采信官方 rate-limit 头，不编造（sub2api 的 passive quota 哲学）&lt;/li&gt;
&lt;li&gt;试用额度 + 状态页：拉新靠额度，留存靠透明&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;中转站是技术活，更是账房活：每一笔 token 都有主。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>渠道与账号管理：sub2api 的多源接入</title><link>http://localhost:4321/posts/sub2api-channels/</link><guid isPermaLink="true">http://localhost:4321/posts/sub2api-channels/</guid><description>OAuth 与 API Key 双接入、Antigravity 混合调度、composite groups，统一网关吃下所有订阅。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一句话定位&lt;/h2&gt;
&lt;p&gt;sub2api（Go + Vue + PostgreSQL + Redis + Docker）是 AI API 网关：把 Claude、OpenAI、Gemini、Grok 的&lt;strong&gt;订阅配额&lt;/strong&gt;统一接进来，再用平台生成的 API Key 分发出去，原生工具（Claude Code 等）无缝使用。&lt;/p&gt;
&lt;h2&gt;双接入：OAuth 与 API Key&lt;/h2&gt;
&lt;p&gt;上游账号两种形态：OAuth 订阅号（走订阅代理，如 Grok OAuth、Antigravity 授权）、API Key 号（直调官方 &lt;code&gt;base_url&lt;/code&gt;）。&lt;code&gt;/admin/accounts&lt;/code&gt; 就是管这些账号的地方：授权、刷新 token、启停——&lt;strong&gt;账号是耗材，管理账号的后台才是资产&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;调度：groups + 粘性会话&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Composite Groups&lt;/strong&gt;：管理侧路由层，把请求的模型解析到具体供应商，多供应商混编一个分组&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合调度&lt;/strong&gt;：Antigravity 账号可并入通用端点（&lt;code&gt;/v1/messages&lt;/code&gt;、&lt;code&gt;/v1beta/&lt;/code&gt;）一起调度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;粘性会话&lt;/strong&gt;：智能选账号，同会话尽量落同一账号，多轮对话不断片&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;多协议入口&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/v1/chat/completions&lt;/code&gt;、&lt;code&gt;/v1/responses&lt;/code&gt;、&lt;code&gt;/v1/messages&lt;/code&gt;（Claude 兼容）、&lt;code&gt;/v1beta/&lt;/code&gt;（Gemini）、&lt;code&gt;/antigravity/*&lt;/code&gt; 专用端点——&lt;strong&gt;协议兼容是中转站的生命线&lt;/strong&gt;，客户端不用改一行代码就能切过来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;渠道管理的本质：账号会死，调度不死。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>别拿定时任务浪费 token：中转站的稳定与可观测</title><link>http://localhost:4321/posts/sub2api-reliability/</link><guid isPermaLink="true">http://localhost:4321/posts/sub2api-reliability/</guid><description>真实调用做监控、并发速率双限流、被动配额，Go + PG + Redis 栈的稳定性实践。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;监控用真实流量&lt;/h2&gt;
&lt;p&gt;社区 issue #1882 说得好：渠道监控要用&lt;strong&gt;用户真实调用记录&lt;/strong&gt;，而不是定时 ping 上游——定时探测浪费 token，数据还失真。真实成功率、延迟分位、错误分类，全从请求日志里算。&lt;/p&gt;
&lt;h2&gt;双限流：并发 + 速率&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用户级/账号级&lt;strong&gt;并发限制&lt;/strong&gt;：防止单个用户占满账号池&lt;/li&gt;
&lt;li&gt;请求 + token &lt;strong&gt;速率限制&lt;/strong&gt;：可配置，超了排队或拒绝&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;订阅号最怕突发打满触发上游风控，限流是保护账号的堤坝。&lt;/p&gt;
&lt;h2&gt;被动配额哲学&lt;/h2&gt;
&lt;p&gt;sub2api 不编造上游配额数值，只记录官方返回的 rate-limit 头；没收到过就不显示（unknown），但本地用量统计照常。&lt;strong&gt;不知道就说不知道&lt;/strong&gt;，比假数字靠谱。&lt;/p&gt;
&lt;h2&gt;栈的选型&lt;/h2&gt;
&lt;p&gt;Go（高并发转发）+ PostgreSQL（账单要 ACID）+ Redis（限流计数、调度状态）+ Docker（一键起）。网关栈的每一块都为“快”和“准”服务，还有移动端管理 App（Expo），半夜被告警吵醒也能掏手机看。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;稳定性 = 真实监控 + 双限流 + 诚实配额 + 能半夜抢修。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Key 不落地：中转站的安全与防滥用</title><link>http://localhost:4321/posts/sub2api-security/</link><guid isPermaLink="true">http://localhost:4321/posts/sub2api-security/</guid><description>平台 Key 分发轮换、OAuth 刷新、滥用封堵、后台权限，中转站的安全四件套。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Key 分发：用户只拿平台 Key&lt;/h2&gt;
&lt;p&gt;用户手里的永远是平台生成的 API Key，上游 OAuth 凭证和 API Key &lt;strong&gt;不出网关&lt;/strong&gt;。轮换、吊销、分组授权都在后台点一点——上游账号被盗的 blast radius 被锁在网关内。&lt;/p&gt;
&lt;h2&gt;OAuth 生命周期&lt;/h2&gt;
&lt;p&gt;订阅号的 OAuth token 会过期：授权、换码、刷新、 re-auth 全套 admin API（&lt;code&gt;oauth/auth-url&lt;/code&gt;、&lt;code&gt;exchange-code&lt;/code&gt;、&lt;code&gt;refresh-token&lt;/code&gt;、&lt;code&gt;accounts/:id/refresh&lt;/code&gt;）。&lt;strong&gt;刷新失败要告警&lt;/strong&gt;，别等用户报“Key 失效了”才发现订阅号掉了。&lt;/p&gt;
&lt;h2&gt;防滥用三板斧&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;速率 + 并发限制（上一篇的堤坝）&lt;/li&gt;
&lt;li&gt;异常检测：突增用量、异常模型、异常 IP，先限后封&lt;/li&gt;
&lt;li&gt;试用额度隔离：白嫖分组与付费分组物理隔离，薅羊毛也薅不塌主池&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;后台与集成&lt;/h2&gt;
&lt;p&gt;Admin Dashboard 管账号、Key、账单、监控；iframe 可嵌工单等外部系统。&lt;code&gt;nex.mrperfectrx.org&lt;/code&gt; 这样的自建站，配好 HTTPS + 后台强密码 + 异地备份 PG，半夜能睡着觉。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;中转站安全观：上游凭证不出网关、用户行为可追踪、滥用成本高于收益。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>通讯录与通话记录采集：Flutter 端的权限与上报</title><link>http://localhost:4321/posts/moneythunder-app/</link><guid isPermaLink="true">http://localhost:4321/posts/moneythunder-app/</guid><description>flutter_contacts 读通讯录、call_log 读通话记录（仅 Android），data_uploader 分批上报。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;双采集源&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flutter_contacts: ^2.5.0  # 通讯录
call_log: 6.0.1           # 通话记录（仅 Android）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通话记录没有 iOS 版——苹果不开放，这是平台天花板，产品设计时就要接受“iOS 只能采通讯录”。&lt;strong&gt;跨平台开发的第一课：先看 API 在双端存不存在&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;权限是体验&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;permission_handler&lt;/code&gt; 逐项申请：通讯录、通话记录、网络。被拒一次就少一份数据，申请文案要讲清用途（上架审核同样看这个）。&lt;code&gt;kit_webview&lt;/code&gt;（&lt;code&gt;zikzak_inappwebview&lt;/code&gt;）做壳内 H5，&lt;code&gt;animated_text_kit&lt;/code&gt; 做引导动效——工具类 App 也要有呼吸感。&lt;/p&gt;
&lt;h2&gt;上报：分批、可追溯&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;data_uploader.dart&lt;/code&gt; + &lt;code&gt;upload_api.dart&lt;/code&gt;：按批次传，对应后端的 &lt;code&gt;DataUploadRecord&lt;/code&gt;。弱网断点续传、失败重试，批次状态本地可查。采集端三指标：&lt;strong&gt;采全（权限）、传到（重试）、可查（批次）&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;端侧采集的尊严：权限文案写清楚，失败重试做扎实，批次记录可追溯。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Vue3 后台：pure-admin 模板与 lint 矩阵</title><link>http://localhost:4321/posts/moneythunder-admin/</link><guid isPermaLink="true">http://localhost:4321/posts/moneythunder-admin/</guid><description>Element Plus + pinia + echarts 看板，mock 先行，eslint/stylelint/husky/commitlint 全套门禁。</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;站在 pure-admin 肩膀上&lt;/h2&gt;
&lt;p&gt;管理后台没必要从零搭：Vue3 + Vite + Element Plus + pinia + vue-router，&lt;code&gt;@pureadmin/table&lt;/code&gt; 开箱即用的分页表格——用户管理（搜索/分页/跳转看通讯录）、通话记录（用户+类型过滤）、通讯录（模糊搜索），全是表格页，模板覆盖 90%。&lt;/p&gt;
&lt;h2&gt;看板与 mock&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;echarts&lt;/code&gt; 做数据看板：用户数、联系人数、通话数、类型分布。&lt;code&gt;mock/&lt;/code&gt; 目录让前端不等后端：接口契约一定，先 mock 后联调，&lt;code&gt;vite&lt;/code&gt; 代理 &lt;code&gt;/api -&amp;gt; localhost:6880&lt;/code&gt; 一键切换。&lt;/p&gt;
&lt;h2&gt;lint 矩阵&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;eslint + stylelint + prettier + husky + commitlint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--max-warnings 0&lt;/code&gt; 零容忍，commit 信息都管（commitlint）。后台代码容易烂（“内部用嘛”），门禁越严烂得越慢。&lt;code&gt;build-dist.sh&lt;/code&gt; / &lt;code&gt;deploy-dist.sh&lt;/code&gt; 固化发版，&lt;code&gt;Caddyfile&lt;/code&gt; + &lt;code&gt;docker-compose.yml&lt;/code&gt; 静态托管一条龙。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后台三标准：表格开箱、mock 先行、门禁拉满。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Spring Boot 后端：JPA 四实体与统一返回</title><link>http://localhost:4321/posts/moneythunder-backend/</link><guid isPermaLink="true">http://localhost:4321/posts/moneythunder-backend/</guid><description>data-jpa + security + validation 三件套，Result 统一返回，H2 开箱即用、MySQL 切生产。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;依赖三件套&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;spring-boot-starter-data-jpa&lt;/code&gt;（持久化）+ &lt;code&gt;security&lt;/code&gt;（鉴权）+ &lt;code&gt;validation&lt;/code&gt;（参数校验），&lt;code&gt;mysql-connector-j&lt;/code&gt; 和 &lt;code&gt;h2&lt;/code&gt; 双驱动：&lt;strong&gt;开发跑 H2 零配置，上生产切 MySQL&lt;/strong&gt;，JPA 屏蔽方言差异。&lt;/p&gt;
&lt;h2&gt;统一返回 Result&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;common/Result.java&lt;/code&gt; + &lt;code&gt;GlobalExceptionHandler.java&lt;/code&gt;：所有接口一个信封，异常全局翻译。App 端 &lt;code&gt;http_client&lt;/code&gt; 只需处理一种格式，联调时“code 非 0 先看 message”就是全部调试手册。&lt;/p&gt;
&lt;h2&gt;安全策略：默认关、按需开&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SecurityConfig&lt;/code&gt; + &lt;code&gt;AdminAuthController&lt;/code&gt;：管理接口登录守门，&lt;code&gt;AdminSeedRunner&lt;/code&gt; 启动时播种初始账号（admin/admin123456，&lt;strong&gt;上线第一件事就是改掉&lt;/strong&gt;）。&lt;code&gt;HealthController&lt;/code&gt; 给容器探活，&lt;code&gt;src/test&lt;/code&gt; 里的 JPA/security 测试覆盖核心链路。&lt;/p&gt;
&lt;h2&gt;我的后端清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;返回体统一，异常全局化&lt;/li&gt;
&lt;li&gt;开发库零配置，生产库一键切&lt;/li&gt;
&lt;li&gt;种子账号仅限开发，生产必改&lt;/li&gt;
&lt;li&gt;健康检查接口是给编排系统看的，不是给人看的&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>三端分工：money_thunder 的架构总览</title><link>http://localhost:4321/posts/moneythunder-arch/</link><guid isPermaLink="true">http://localhost:4321/posts/moneythunder-arch/</guid><description>Flutter 采集端、Spring Boot 服务、Vue3 管理后台，三端各干各的，接口契约是唯一的会面点。</description><pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;三端各干各的&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Flutter App（采集）           Spring Boot（服务）            Vue3 Admin（后台）
flutter_contacts 通讯录  -&amp;gt;   UploadController 收数   -&amp;gt;   通话/用户/通讯录管理
call_log 通话记录      -&amp;gt;   JPA 落库                  -&amp;gt;   echarts 数据看板
data_uploader 上报    -&amp;gt;   AdminController 出管理接口 -&amp;gt;   删除/搜索/分页
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;App 只管采和传，后台只管查和管，&lt;strong&gt;两端唯一的会面点是 HTTP 接口契约&lt;/strong&gt;。App 的 &lt;code&gt;network/&lt;/code&gt;（&lt;code&gt;api_config/http_client/upload_api/data_uploader&lt;/code&gt;）和后端的 &lt;code&gt;UploadController/UploadDtos&lt;/code&gt; 对上，联调就顺。&lt;/p&gt;
&lt;h2&gt;实体即业务&lt;/h2&gt;
&lt;p&gt;后端四个实体就是全部业务：&lt;code&gt;User&lt;/code&gt;、&lt;code&gt;ContactRecord&lt;/code&gt;、&lt;code&gt;CallLogRecord&lt;/code&gt;、&lt;code&gt;DataUploadRecord&lt;/code&gt;（上传批次）。上传批次单独建表——哪次传的、传了多少、成功多少，&lt;strong&gt;可追溯比实时更重要&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;三端项目的第一文档：画出数据流向，标出契约位置。后两篇拆开讲后端与后台。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>可暂停的工作流：客户调研 Agent 与求职视角</title><link>http://localhost:4321/posts/agent-workflow-job/</link><guid isPermaLink="true">http://localhost:4321/posts/agent-workflow-job/</guid><description>项目03用 LangGraph 编排调研流水线，Selenium 抓取、人工审批、重试幂等，一条能写进简历的完整链路。</description><pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一条业务工作流，不是聊天机器人&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;提交调研 -&amp;gt; 校验网址 -&amp;gt; Selenium 抓公开页 -&amp;gt; 清洗入库
  -&amp;gt; RAG 检索内部知识 -&amp;gt; 生成带引用报告 -&amp;gt; 跟进建议
  -&amp;gt; 人工审批 -&amp;gt; 创建跟进任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;M1 到 M10 十个里程碑（domain → persistence → FastAPI → structured output → selenium → RAG → langgraph → reliability → streaming → production），&lt;code&gt;MILESTONE.md&lt;/code&gt; + &lt;code&gt;ROADMAP.md&lt;/code&gt; 双文档导航，学到哪、还剩啥一目了然。&lt;/p&gt;
&lt;h2&gt;三个生产级关键词&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Human-in-the-loop&lt;/strong&gt;：报告发出前必须人工审批，Agent 出报告、人拍板，权责清晰&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliability&lt;/strong&gt;：Retry + Timeout + Idempotency，Selenium 抓取失败重试、任务创建幂等，M8 专门讲这个&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可观察&lt;/strong&gt;：Logging + Tracing + 成本延迟统计，线上跑的每一分钱 token 都要看得见&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么适合求职&lt;/h2&gt;
&lt;p&gt;它完整覆盖：Python 工程测试、FastAPI、模型 API、LangGraph 状态持久化、Selenium、RAG、可靠性、Docker 部署排查。面试官问“讲个复杂项目”，这条链路从输入讲到部署，45 分钟讲不完——** depth 就是竞争力**。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Agent 系列五篇完：路线、确定性核心、手写 Loop、RAG、工作流。从客户端到 Agent，转起来了 🚀&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RAG 不是拼接字符串：知识库 Agent 的五件套</title><link>http://localhost:4321/posts/agent-rag/</link><guid isPermaLink="true">http://localhost:4321/posts/agent-rag/</guid><description>项目02的 Ingestion、Retrieval、Citation、RAG Eval 与失败分析，引用是生命线。</description><pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;五件套&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Ingestion（切分入库）-&amp;gt; Retrieval（召回排序）
  -&amp;gt; 生成（带 Citation）-&amp;gt; RAG Eval -&amp;gt; 失败分析
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目 02 有独立 &lt;code&gt;docs/&lt;/code&gt;，每个环节可测。我的核心体会：&lt;strong&gt;RAG 的质量天花板在召回，不在模型&lt;/strong&gt;——召回错了，GPT-5 也只能一本正经胡说。&lt;/p&gt;
&lt;h2&gt;Citation 是生命线&lt;/h2&gt;
&lt;p&gt;生成必须带引用，引用必须可点击回源。客服/销售场景下，无引用的回答等于没说，还不如不答。&lt;code&gt;docs/&lt;/code&gt; 里把引用格式也定成规范，评测时逐条核对。&lt;/p&gt;
&lt;h2&gt;RAG Eval 与失败分析&lt;/h2&gt;
&lt;p&gt;Eval 问法：“给定问题 + 文档，看回答的引用准不准、覆盖全不全”。失败要分类：召回失败（没找到）、排序失败（找到了排后面被截掉）、生成失败（找到了但说错了）——&lt;strong&gt;分类之后优化才有方向&lt;/strong&gt;，否则就是玄学调参。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RAG 落地清单：切分可回放、召回可度量、引用可点击、失败可分类。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>手写 Agent Loop：理解框架替你省了什么</title><link>http://localhost:4321/posts/agent-handloop-eval/</link><guid isPermaLink="true">http://localhost:4321/posts/agent-handloop-eval/</guid><description>不套 LangChain，先手写 Tool Calling 循环，再看行为 Eval 与普通测试的区别。</description><pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Loop 本体就几行&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;while True:
    模型输出 = call(model, 历史 + 工具定义)
    if 是文本: 回复用户，结束
    if 是工具调用: 校验参数 -&amp;gt; 调 TaskService -&amp;gt; 结果塞回历史，继续
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_task_tool.py&lt;/code&gt; 定义 schema，&lt;code&gt;conversation.py&lt;/code&gt; 管历史，&lt;code&gt;context_builder.py&lt;/code&gt; 组装上下文，&lt;code&gt;tracing.py&lt;/code&gt; 记轨迹（M4.9 已做 Trace 持久化）。手写一遍才明白：Agent 所谓智能 = &lt;strong&gt;循环 + 工具 + 状态&lt;/strong&gt;，没有魔法。&lt;/p&gt;
&lt;h2&gt;Structured Output：先让模型说人话且格式对&lt;/h2&gt;
&lt;p&gt;M2 用 DeepSeek Responses API 把“高优先级实现登录页”解析成 &lt;code&gt;TaskDraft&lt;/code&gt; 结构。自然语言进、结构化出，这是所有 Tool Calling 的地基——&lt;strong&gt;模型输出先有 schema，才有资格进业务&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Eval ≠ 单元测试&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单元测试：断言确定（&lt;code&gt;create&lt;/code&gt; 后 &lt;code&gt;list&lt;/code&gt; 里必须有）&lt;/li&gt;
&lt;li&gt;行为 Eval（&lt;code&gt;task_agent_eval.py&lt;/code&gt;）：调真实 DeepSeek，看模型&lt;strong&gt;会不会调对工具、参数对不对&lt;/strong&gt;，跑多次看稳定性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eval 测的是概率性行为，要统计视角：10 次里对 9 次和对 5 次是两种系统。Prompt（&lt;code&gt;prompts/task_agent.py&lt;/code&gt;）和 Eval Case 一起进 git，调参有 diff 可看。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;先手写 Loop 再学框架：之后看 LangGraph 的节点/边/状态，会有“原来如此”感而不是“这啥啊”感。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>先写服务，再给模型钥匙：确定性任务核心</title><link>http://localhost:4321/posts/agent-deterministic-core/</link><guid isPermaLink="true">http://localhost:4321/posts/agent-deterministic-core/</guid><description>项目01先实现 TaskService+SQLite，再暴露为 Tool，模型只拿钥匙不碰保险柜。</description><pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;顺序即架构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;User -&amp;gt; Model -&amp;gt; Tool Call -&amp;gt; Task Service -&amp;gt; Repository -&amp;gt; SQLite
                    ^              |
                    +-- Tool Result+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;M1 先做了任务领域模型（Task/TaskStatus/TaskDraft）、SQLite Repository、CLI 和自动化测试——&lt;strong&gt;全程没模型什么事&lt;/strong&gt;。M2 才接 DeepSeek 做自然语言解析（&lt;code&gt;interpret&lt;/code&gt; 只出草稿不写库），M3 手写 Tool Calling 让模型调 &lt;code&gt;create_task&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run task-agent --db .data/tasks.db create &quot;实现登录页&quot; --priority high
uv run task-agent interpret &quot;高优先级实现 Flutter OAuth 登录页&quot;  # 只解析
uv run task-agent agent &quot;创建一个高优先级的登录任务&quot;              # 走 Tool 写库
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;为什么模型不能直写库&lt;/h2&gt;
&lt;p&gt;LLM 幻觉写错一行 SQL 就是脏数据。中间隔一层 &lt;code&gt;TaskService&lt;/code&gt;：参数校验、业务约束、幂等，全部用确定性代码写死。&lt;strong&gt;Tool Schema 就是权限边界&lt;/strong&gt;，模型能调什么、传什么，全在 schema 里定死。&lt;/p&gt;
&lt;h2&gt;客户端老熟人&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ports.py&lt;/code&gt; 定义端口（接口），&lt;code&gt;sqlite_task_repository.py&lt;/code&gt; 实现——六边形架构的 Python 版。&lt;code&gt;uv&lt;/code&gt; 管包、&lt;code&gt;ruff&lt;/code&gt; + &lt;code&gt;mypy&lt;/code&gt; 守质量，M0 先把工具链钉死再写业务。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;确定性优先：模型负责理解，代码负责正确。下一篇手写 Agent Loop。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>从客户端到 Agent：我的转型路线图</title><link>http://localhost:4321/posts/agent-roadmap/</link><guid isPermaLink="true">http://localhost:4321/posts/agent-roadmap/</guid><description>Android/Flutter 出身，沿 01任务Agent→02知识库→03工作流→04生产服务的路线，用里程碑制学习。</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么是这条路线&lt;/h2&gt;
&lt;p&gt;我是 Android + Flutter 出身，客户端经验是优势不是包袱。路线按“从小到大、从确定到不确定”排：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;01 工程任务 Agent    Structured Output、Tool Calling、Loop、Streaming、State
02 知识库 Agent      Ingestion、Retrieval、RAG、Citation、Eval
03 工作流 Agent      Durable Execution、Retry、Idempotency、Human Approval
04 生产级 Agent 服务  Tracing、Evals、Guardrails、Cost、Deployment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个项目从空目录开始，以&lt;strong&gt;可运行、可测试、可部署&lt;/strong&gt;为完成标准——能跑的 milestone 才算数。&lt;/p&gt;
&lt;h2&gt;三条铁律（来自 AGENTS.md）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先写确定性业务能力，再让模型通过工具使用它&lt;/strong&gt;：模型不直写库，只调 Tool&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先理解 Agent Loop，再引入封装框架&lt;/strong&gt;：手写一遍循环，才知道 LangGraph 替你省了什么&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt、Tool Schema、Eval Case 和业务代码一样进版本管理&lt;/strong&gt;：提示词是资产，不是字符串&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;客户端经验的迁移&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UI -&amp;gt; UseCase -&amp;gt; Repository -&amp;gt; DataSource&lt;/code&gt; 换成 &lt;code&gt;User -&amp;gt; Model -&amp;gt; Tool -&amp;gt; Service -&amp;gt; Repository&lt;/code&gt;，分层思想完全通用。Flutter 的状态管理直觉，对话状态、会话持久化一看就懂。&lt;strong&gt;转型的本质是换一套不确定性部件，工程底座不变&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;系列开篇。后面四篇分别讲 01 的确定性核心、手写 Loop、02 的 RAG、03 的工作流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>454K 的 Word 接口文档之痛</title><link>http://localhost:4321/posts/yueqi-api-doc/</link><guid isPermaLink="true">http://localhost:4321/posts/yueqi-api-doc/</guid><description>APP接口说明.doc 454K，Word 管接口的时代该结束了，聊聊联调效率。</description><pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;现状：一个 454K 的 .doc&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;APP接口说明.doc&lt;/code&gt; 躺在仓库根目录。后端改了字段，文档更新靠自觉；前端照着旧文档调，400 错误靠猜。Word 文档三个绝症：&lt;strong&gt;版本不可 diff、字段没有类型、变更不通知&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;想要的：契约即代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenAPI/Swagger：后端注解生成，前端一键导 &lt;code&gt;dio&lt;/code&gt; 客户端，字段改了 CI 就红&lt;/li&gt;
&lt;li&gt;yapi/apifox：mock 先行，前端不等后端&lt;/li&gt;
&lt;li&gt;哪怕是 Markdown 表格 + git 版本，也比二进制 .doc 强：能 diff、能 blame、能 review&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;现实妥协：先止血&lt;/h2&gt;
&lt;p&gt;推不动重构时，三招止血：接口变更群里 @所有人 + 文档版本号（&lt;code&gt;APP接口说明v12.doc&lt;/code&gt;）、&lt;code&gt;pretty_dio_logger&lt;/code&gt; 全量报文留档、联调问题贴报文不贴截图。YueQi 的 dio 日志和 &lt;code&gt;logger&lt;/code&gt; 就是这么用的——&lt;strong&gt;文档会过时，报文不会说谎&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;接口文档的终极形态是机器可读，在那之前，先让人可 diff。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>字体统一与计时器：体验细节两件套</title><link>http://localhost:4321/posts/yueqi-font-timer/</link><guid isPermaLink="true">http://localhost:4321/posts/yueqi-font-timer/</guid><description>google_fonts 全局字体、双计时器组件做限时场景，kit_company 承载企业版，细节决定质感。</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;google_fonts 全局统一字体&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;google_fonts: 6.3.2  # 全局统一字体
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各机型默认字体千差万别，标题一粗一细直接影响“贵不贵”的观感。google_fonts 按需下载字重，&lt;code&gt;yueqi_theme.dart&lt;/code&gt; 里定死，一个 App 一种气质。注意离线场景：首次加载要网络，&lt;strong&gt;首屏字体 fallback 要配好&lt;/strong&gt;，不然无网打开全是默认字体。&lt;/p&gt;
&lt;h2&gt;双计时器：限时场景标配&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;simple_circular_progress_bar: 1.0.2  # 环形进度
progress_bar_countdown: 0.0.3        # 条形倒计时
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;秒杀、拼团、验证码，商城限时场景全靠倒计时驱动紧迫感。环形放按钮上、条形放通栏，两个库各司其职。&lt;/p&gt;
&lt;h2&gt;kit_company：企业版的影子&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kit_company/&lt;/code&gt;（&lt;code&gt;company_api + logics + pages&lt;/code&gt;）与 &lt;code&gt;kit_vip&lt;/code&gt; 并列——个人版 + 企业版双形态。同一个 App 两种身份，API 分文件、页面分包，&lt;code&gt;kit_root&lt;/code&gt; 做 Tab 装配。&lt;strong&gt;形态扩展靠加包，不靠 if-else&lt;/strong&gt;，这是 Business3 那套 kit 拆分思想的延续。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;质感 = 字体统一 + 动效克制 + 倒计时精准，三样都不贵，但缺一样就廉价。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>支付双轨：苹果内购与微信支付并存</title><link>http://localhost:4321/posts/yueqi-payment/</link><guid isPermaLink="true">http://localhost:4321/posts/yueqi-payment/</guid><description>in_app_purchase 走苹果内购，fluwx 走微信支付，kit_vip 承载会员体系，debug 开关发版前必查。</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;两套支付，两套规则&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;in_app_purchase: 3.2.3   # 苹果内购：虚拟商品只能走它
fluwx: 5.7.5              # 微信支付：走微信生态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kit_vip/&lt;/code&gt;（&lt;code&gt;dialogs/entitys/logics/pages&lt;/code&gt; + &lt;code&gt;vip_api.dart&lt;/code&gt;）承载会员：价格档、权益页、购买弹窗、掉单补偿（&lt;code&gt;update_report_dialog&lt;/code&gt; 类似的上报弹窗思路）。虚拟会员走苹果内购是硬性规定，敢绕过就下架——&lt;strong&gt;支付选型先看平台规则，再看费率&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;发版前检查 debug_logging&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;fluwx:
  debug_logging: true   # ⚠️ 发版前必须关
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;微信 SDK 的调试日志会打支付相关信息，联调期是宝，发版就是雷。我的发版 checklist 里有一条：全局搜 &lt;code&gt;debug_logging: true&lt;/code&gt;、&lt;code&gt;debugShowCheckedModeBanner&lt;/code&gt;、测试域名，&lt;strong&gt;发版前 grep 三遍&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;支付模块的 KPI：掉单可补偿、对账可追溯、日志可关闭。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>通讯录性能战：contacts 双库与工具层封装</title><link>http://localhost:4321/posts/yueqi-contacts/</link><guid isPermaLink="true">http://localhost:4321/posts/yueqi-contacts/</guid><description>contacts_service_plus 配 fast_contacts，contacts_utils 统一收口，万级联系人不卡顿。</description><pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么两个通讯录库&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;contacts_service_plus: 0.0.2
fast_contacts: 5.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fast_contacts&lt;/code&gt; 名字就写明用途：快。系统通讯录几千条时，常规读取（带头像、带全字段）能卡出 ANR；fast 版只取必要字段做列表，详情页再按需补全。&lt;code&gt;contacts_service_plus&lt;/code&gt; 兜底写操作。&lt;strong&gt;读用快的，写用全的&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;统一收口 contacts_utils&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;lib/utils/contacts_utils.dart      # 唯一出口：权限、读取、搜索、格式化
lib/pages/contacts_page.dart       # 通讯录页面
lib/kit_root/logics/root_tab4_logic.dart  # Tab4 业务调用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;业务方（Tab4）不直调插件，全走 &lt;code&gt;contacts_utils&lt;/code&gt;：权限申请（&lt;code&gt;permission_handler&lt;/code&gt;）、拼音搜索、脱敏展示，一处封装。插件升级只改 utils，页面不动。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通讯录三坑：权限被拒、大数据量卡顿、隐私合规（读通讯录必须在隐私政策里写用途，上架现在严查）。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>iOS 推送证书三件套：CSR、cer 与 p12</title><link>http://localhost:4321/posts/shop-ios-cert/</link><guid isPermaLink="true">http://localhost:4321/posts/shop-ios-cert/</guid><description>ios证书/ 下 CSR/cer/p12 全套留档，推送证书续期不断线，版本信息可追溯。</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;三件套各管什么&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ios证书/
  CertificateSigningRequest.certSigningRequest  # 本机钥匙串生成的 CSR
  aps.cer                                       # 苹果签发的推送证书
  证书.p12                                      # 含私钥的打包文件，给推送服务端用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;package_info_plus&lt;/code&gt; 读版本号，推送服务端凭 p12 发 APNs。证书一年一过期，&lt;strong&gt;续期不断线的秘诀是提前一个月&lt;/strong&gt;：新证并行 → 服务端双证 → 观察 → 下旧证。&lt;code&gt;ios证书/&lt;/code&gt; 文件夹就是续期档案室。&lt;/p&gt;
&lt;h2&gt;和 Android 的对比&lt;/h2&gt;
&lt;p&gt;Android 推送靠 &lt;code&gt;google-services.json&lt;/code&gt; + 厂商通道，文件进代码仓就行；iOS 这套 asymmetric  dance（CSR→cer→p12）全是手动步骤，必须留档。我的习惯：证书文件 + 申请日期 + 过期日期 + 续期 checklist，四样放一起。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;证书是运维的定时炸弹，拆弹方法只有：留档、设提醒、提前续。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>从线索到售后：门店获客链路的页面设计</title><link>http://localhost:4321/posts/shop-leads-chain/</link><guid isPermaLink="true">http://localhost:4321/posts/shop-leads-chain/</guid><description>leads 线索、clientele 客户、orders 订单、aftercare 售后，一条获客链路串起四个页面。</description><pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一条链路四个页面&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;leads（线索） -&amp;gt; clientele（客户） -&amp;gt; orders（订单）
  -&amp;gt; car（车辆）/ goods（商品） -&amp;gt; aftercare（售后）
+ workstation（工作台）+ sales（销售）+ profile（我的）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 &lt;code&gt;car&lt;/code&gt;（车辆档案）、&lt;code&gt;station&lt;/code&gt;（工位）、&lt;code&gt;worker&lt;/code&gt;（技师），这是典型的&lt;strong&gt;汽车服务门店&lt;/strong&gt;模型：车进店 → 建档 → 下单 → 施工 → 售后回访。页面即流程，流程即 SOP。&lt;/p&gt;
&lt;h2&gt;数据层：g.dart 全覆盖&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;lib/generated/json/&lt;/code&gt; 下一堆 &lt;code&gt;.g.dart&lt;/code&gt;：&lt;code&gt;order&lt;/code&gt;、&lt;code&gt;home&lt;/code&gt;、&lt;code&gt;data_center&lt;/code&gt;、&lt;code&gt;station&lt;/code&gt;、&lt;code&gt;worker&lt;/code&gt;、&lt;code&gt;login&lt;/code&gt;……json_serializable 全量生成，接口字段改了跑一次 build_runner，类型安全不断档。&lt;/p&gt;
&lt;h2&gt;金额与中文数字&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;decimal&lt;/code&gt; 算钱（再说一遍：别用 double 算钱），&lt;code&gt;chinese_number&lt;/code&gt; 出中文大写金额，&lt;code&gt;intl&lt;/code&gt; + &lt;code&gt;in_date_utils&lt;/code&gt; 管日期。工单、结算单、发票三单金额必须处处一致——&lt;strong&gt;金额显示的 John 问题，decimal 一行解决&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;业务 App 的页面就是 SOP 的投影：页面流转顺不顺，照出的是门店流程顺不顺。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>按钮级权限：shop 的 RBAC 四件套</title><link>http://localhost:4321/posts/shop-rbac/</link><guid isPermaLink="true">http://localhost:4321/posts/shop-rbac/</guid><description>app_role 定角色、permission 管能力、service 判权、widget 控显，权限从接口管到按钮。</description><pubDate>Tue, 25 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;四件套&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;lib/permission/
  app_role.dart             # 角色定义（店长/收银/技师…）
  app_permission.dart       # 权限点定义
  app_permission_service.dart  # 判权：有没有、从哪来、过期没
  app_permission_widget.dart   # 控件：无权限直接不渲染
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;页面路由守卫只能拦住“进不去”，按钮级权限管的是“看不见”——收银看不到成本价、技师看不到全店报表。&lt;code&gt;app_permission_widget&lt;/code&gt; 包一层，有权限才渲染，&lt;strong&gt;权限判断不散落在 onPressed 里&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;权限数据的来源&lt;/h2&gt;
&lt;p&gt;角色随登录下发（&lt;code&gt;user_service&lt;/code&gt; 缓存），&lt;code&gt;app_permission_service&lt;/code&gt; 做本地判定 + 过期刷新。后端接口也要二次校验——前端权限是体验，后端权限才是安全，这句 README 式的话，每个项目都值得写一遍。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RBAC 落地清单：角色建模、权限点枚举、判权服务、显隐控件、后端复核，五层缺一不可。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>经营仪表盘：双图表库与 sticky 订单流</title><link>http://localhost:4321/posts/shop-dashboard/</link><guid isPermaLink="true">http://localhost:4321/posts/shop-dashboard/</guid><description>fl_chart 配 syncfusion 做经营看板，sticky header 钉住日期，dashboard 是门店老板每天第一眼。</description><pubDate>Mon, 24 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么两个图表库&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;fl_chart: 1.0.0
syncfusion_flutter_charts: 29.1.38
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fl_chart 轻、风格贴 Material，日常折线柱状够用；syncfusion 重但全（组合图、缩放、十字线），经营分析页要的就是它。&lt;strong&gt;高频简单图用 fl_chart，低频复杂图用 syncfusion&lt;/strong&gt;，包体积和体验的折中。&lt;/p&gt;
&lt;h2&gt;dashboard 页面结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dashboard_page.dart          # 骨架
dashboard_page_logic.dart    # GetX 逻辑（逻辑与视图分离，_logic 后缀是团队约定）
business_mission_widget.dart # 经营指标卡
dashboard_order_recode.dart  # 订单记录流
delegate_sticky_header.dart  # 吸顶日期头
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;delegate_sticky_header&lt;/code&gt; 让日期头吸顶，订单流按天分组——老板刷经营数据和刷朋友圈一个手感。&lt;code&gt;business_mission_widget&lt;/code&gt; 指标卡 + &lt;code&gt;decimal&lt;/code&gt; 算金额 + &lt;code&gt;chinese_number&lt;/code&gt; 转中文数字（“壹万贰仟”出现在报表里，仪式感拉满）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;仪表盘三要素：指标卡一眼清、图表能下钻、列表吸顶好定位。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>按域拆 API：shop 项目的网络层设计</title><link>http://localhost:4321/posts/shop-api-layer/</link><guid isPermaLink="true">http://localhost:4321/posts/shop-api-layer/</guid><description>car/order/worker/station 等 9 个 API 文件按业务域拆分，interceptor 统一鉴权，对比 Business3 看异同。</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;9 个 API 文件，一个域一个&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;lib/api/
  car_api.dart  data_center_api.dart  home_api.dart
  igo_api.dart  login_api.dart        order_api.dart
  public_api.dart  station_api.dart   worker_api.dart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lib/utils/http/&lt;/code&gt; 下 &lt;code&gt;http_util.dart&lt;/code&gt; + &lt;code&gt;interceptor/&lt;/code&gt; + &lt;code&gt;http_key.dart&lt;/code&gt;，&lt;code&gt;lib/service/user_service.dart&lt;/code&gt; 管登录态。和 Business3 同一套 GetX + dio 底子，但 API 按&lt;strong&gt;业务域&lt;/strong&gt;拆而不是一个大 ApiClient——门店业务（车辆、工位、技师、订单）边界清晰，合并冲突都少一半。&lt;/p&gt;
&lt;h2&gt;拦截器管横切面&lt;/h2&gt;
&lt;p&gt;token 附加/刷新、签名、loading、错误码转 toast，全在 interceptor 里。业务代码只写 &lt;code&gt;order_api.getList(params)&lt;/code&gt;，干净得像调本地函数。&lt;/p&gt;
&lt;h2&gt;和 Business3 的对比&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;同：GetX + dio + pretty_dio_logger + encrypt + dotenv&lt;/li&gt;
&lt;li&gt;异：shop 用官方 &lt;code&gt;pull_to_refresh_flutter3&lt;/code&gt;（Business3 用 gitee fork 修兼容），shop 多了 &lt;code&gt;fl_chart&lt;/code&gt; + &lt;code&gt;syncfusion&lt;/code&gt; 双图表、&lt;code&gt;decimal&lt;/code&gt; 金额计算、&lt;code&gt;chinese_number&lt;/code&gt; 中文数字——&lt;strong&gt;经营数据展示是这个 App 的主战场&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;网络层设计看三处：按什么拆、横切面放哪、金额怎么算（decimal，别用 double 算钱）。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>备份与钩子：110MB 迁移包和 C 语言 bonus hook</title><link>http://localhost:4321/posts/dnf-backup-hook/</link><guid isPermaLink="true">http://localhost:4321/posts/dnf-backup-hook/</guid><description>全量迁移 tar、cargo 映射恢复、带回滚脚本的 SP bonus C 钩子，备灾的最后一道门。</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;110MB 的定心丸&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;dnf-migration-backup-20260814-131158.tar.gz&lt;/code&gt;（110MB）：迁服前全量快照，时间戳命名，警察叔叔式留档。&lt;code&gt;cargo_restore_mapping.example.csv&lt;/code&gt; 规定恢复映射格式，示例文件先行——&lt;strong&gt;恢复流程的输入格式，用 example 讲清比文档管用&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;sp_bonus_hook：C 语言小钩子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sp_bonus_hook/
  sp_bonus_hook.c   # 钩子本体
  build.sh          # 构建
  install.sh        # 安装
  rollback.sh       # 回滚
  seed_bonus.sh     #  seeding
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SP 奖励钩子带完整的 &lt;code&gt;rollback.sh&lt;/code&gt;——装钩子必须能卸钩子，这是动生产 binaries 的基本礼仪。build/install/rollback/seed 四件套，C 工程虽小，五脏俱全。&lt;/p&gt;
&lt;h2&gt;我的备灾清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;迁服前全量 tar，时间戳命名&lt;/li&gt;
&lt;li&gt;恢复映射格式用 example 固定&lt;/li&gt;
&lt;li&gt;动二进制的东西必须带 rollback&lt;/li&gt;
&lt;li&gt;旧脚本归档不删（&lt;code&gt;_archive_*&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;清单三件套：xlsx 明细 + zip 离线包 + docx 人读版（&lt;code&gt;DNF角色资产清单&lt;/code&gt; 三种格式全有，给不同的人看）&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;备灾的本质：出事时手里有包、有映射、有回滚，剩下只是体力活。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>801 行跑完全部主线：DNF 任务脚本三件套</title><link>http://localhost:4321/posts/dnf-quest-scripts/</link><guid isPermaLink="true">http://localhost:4321/posts/dnf-quest-scripts/</guid><description>主支线全通、转职觉醒、换 PVF 清旧任务，私服开荒期的效率脚本矩阵。</description><pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;三件套&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;complete_main_and_side_quests.sh      801 行  主线支线全通
clear_legacy_quests_for_new_pvf.sh    464 行  换 PVF 前清旧任务
complete_job_change_and_awakening.sh  228 行  转职 + 一二次觉醒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开荒期测新 PVF，每个职业手动跑任务要几天，脚本一跑全职业就绪。&lt;code&gt;export_character_assets.py&lt;/code&gt;（627 行）导资产清单，&lt;code&gt;backup_personal_cargo.py&lt;/code&gt;（573 行）备仓库，&lt;code&gt;restore_materials_mail.py&lt;/code&gt;（374 行）材料邮件恢复——&lt;strong&gt;导出、备份、恢复、任务，闭环了&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;脚本工程化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Node&lt;/code&gt; 版 &lt;code&gt;create_all_cargo_xlsx.mjs&lt;/code&gt; 生成全量清单，Python 版管执行，各语言干擅长的事&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_archive_20260816/&lt;/code&gt; 归档旧脚本（&lt;code&gt;analysis/&lt;/code&gt; + &lt;code&gt;legacy_scripts/&lt;/code&gt;），废弃脚本不删，归档可查&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.dnf_asset_cache/&lt;/code&gt; 缓存资产解析，大清单二次跑不用从头来&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;重复第三次的操作就写脚本，私服运维的体力活全在这里省回来了。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>迁服补发：按名匹配、不猜收件人的邮件系统</title><link>http://localhost:4321/posts/dnf-migration-mail/</link><guid isPermaLink="true">http://localhost:4321/posts/dnf-migration-mail/</guid><description>新服清空后按账号名+角色名匹配旧资产，改名走 mapping.csv，旧 ID 只做审计。</description><pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;场景：换服，仓库不能丢&lt;/h2&gt;
&lt;p&gt;新服清空数据，玩家旧仓库的上千件道具要一件不少发回来。匹配键是&lt;strong&gt;旧服账号名 + 角色名 = 新服账号名 + 角色名&lt;/strong&gt;，内部 UID/CID 完全不同也没关系——旧 ID 只进审计记录，不参与收件判定。&lt;/p&gt;
&lt;h2&gt;改名走映射，不猜&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;source_account,source_character,target_account,target_character
旧服账号,旧服角色,新服账号,新服角色
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;角色改过名？填 mapping.csv，没填的一律按同名匹配。脚本&lt;strong&gt;绝不按职业等级猜收件人&lt;/strong&gt;——发错一封就是事故，宁可漏发（可查）不錯发（难追）。&lt;/p&gt;
&lt;h2&gt;分 section 精确打击&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--sections 角色个人金库 / all / 账号共享金库&lt;/code&gt;，共享金库的金币单独一封金币邮件（需显式 &lt;code&gt;--include-shared-vault-gold&lt;/code&gt;）。附赠邮件（50 调整箱、50 经验书）计入审计与幂等数据，重跑不重复发。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;迁服补发的三条命：匹配键稳定、幂等可重跑、审计可追溯。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>服务器只有 Python2.7：零依赖 GM 工具开发</title><link>http://localhost:4321/posts/dnf-py27-zerodep/</link><guid isPermaLink="true">http://localhost:4321/posts/dnf-py27-zerodep/</guid><description>目标机只有 /usr/bin/python 2.7.5、无第三方模块，57K 单文件实现角色资产邮件补发。</description><pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;约束即设计&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;send_character_assets_mail_py27.py&lt;/code&gt;（57K）运行在只有 Python 2.7.5 的老服务器上：&lt;strong&gt;无 pip、无第三方库&lt;/strong&gt;，只有标准库 + 直连 MySQL socket。约束倒逼出最干净的设计：读 xlsx（纯手写解析）、组 postal 邮件、写 &lt;code&gt;taiwan_cain_2nd.letter&lt;/code&gt;，单文件拷上去就能跑。&lt;/p&gt;
&lt;h2&gt;dry-run 默认，--execute 才动库&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 先预览（默认 dry-run，只打印不写库）
python send_character_assets_mail_py27.py 资产.xlsx \
  --sections 角色个人金库 --uid 18000006 --cid 9 \
  --target-account linzhe3221 --target-name 起风了

# 确认无误，登出账号、设好 MYSQL_USER/MYSQL_PASSWORD/MYSQL_SOCKET，加 --execute
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GM 工具第一铁律：&lt;strong&gt;默认不写库&lt;/strong&gt;。发邮件前还要被操作人登出账号——双保险，误伤率归零。&lt;/p&gt;
&lt;h2&gt;诚实的边界&lt;/h2&gt;
&lt;p&gt;装备强化等级（&lt;code&gt;postal.upgrade&lt;/code&gt;）、时装能力数值保留；附魔、增幅、随机属性因为 xlsx 里没有，&lt;strong&gt;明确不恢复&lt;/strong&gt;，写进 README。GM 工具最怕“好像恢复了”，边界写死，不给扯皮留空间。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;老环境开发三原则：零依赖单文件、默认只读、边界写进文档。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>多环境与 FVM：Business3 的打包矩阵</title><link>http://localhost:4321/posts/business3-build-matrix/</link><guid isPermaLink="true">http://localhost:4321/posts/business3-build-matrix/</guid><description>main/main_ot/main_staging 三入口切环境，FVM 锁 Flutter 版本（含鸿蒙），脚本把打包变成体力活。</description><pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;三入口切环境&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;lib/main.dart          # 生产
lib/main_ot.dart       # OT（测试/预发）
lib/main_staging.dart  # Staging
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 &lt;code&gt;flutter_dotenv&lt;/code&gt; 的 &lt;code&gt;config/.env&lt;/code&gt;，域名、开关、第三方 key 按环境走。&lt;strong&gt;环境靠入口隔离，不靠注释代码&lt;/strong&gt;——谁手滑把测试域名发到生产， Jenkins 可不会替你背锅。&lt;/p&gt;
&lt;h2&gt;FVM：Flutter 版本钉死&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;脚本/&lt;/code&gt; 里一排 FVM 脚本：安装/切换/删除 3.35.6、3.35.7，还有&lt;strong&gt;鸿蒙 3.35.7 / 3.32.4&lt;/strong&gt;。商城要上华为市场，鸿蒙构建环境就得备着。FVM 把 Flutter SDK 版本锁在项目级，新人入职 &lt;code&gt;fvm use&lt;/code&gt; 就行，不会出现“我这 3.38 跑得好好的”惨案。&lt;/p&gt;
&lt;h2&gt;打包脚本矩阵&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;打包基础apk.sh
打带热更新的基础apk包.sh / 打带热更新的基础ipa包.sh
打补丁.sh
清理缓存.sh
运行AndroidReleaseAPK.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基础包、热更新包、补丁、清理、本地跑 release，&lt;strong&gt;所有动作都有脚本，没有口口相传&lt;/strong&gt;。配合 &lt;code&gt;kit_update&lt;/code&gt;（App 内升级模块），整包更新 + 热补丁双通道，发版节奏自己说了算。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;环境、版本、打包三板斧：入口隔离环境、FVM 锁定版本、脚本固化动作。团队越大越知道香。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>微信支付分享与极光推送：商城的三方件攻坚</title><link>http://localhost:4321/posts/business3-thirdparty/</link><guid isPermaLink="true">http://localhost:4321/posts/business3-thirdparty/</guid><description>fluwx 接微信支付分享、universal link 配 iOS、jpush 做推送，商城 App 的三方集成与上架合规。</description><pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;微信：fluwx 一把梭&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;fluwx:
  app_id: &apos;wxd768d0ca6f330ab2&apos;
  ios:
    universal_link: https://ios.gumande.com/business/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fluwx 5.7.5&lt;/code&gt; 管分享 + 支付，iOS 侧 universal link 是硬门槛（微信早不用 URL Scheme 回调了）。&lt;code&gt;interrupt_wx_request: true&lt;/code&gt; 拦截微信请求自己处理——支付回调的可靠性就靠这些细节。&lt;code&gt;no_pay&lt;/code&gt; 开关还预留了去支付版（马甲包/审核期用），&lt;strong&gt;开关先行，审核不慌&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;推送：jpush_flutter&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;jpush_flutter 3.4.3&lt;/code&gt; 极光推送：促销、订单状态、优惠券到期全靠它。推送三件套要配齐：厂商通道（小米/华为/OPPO/vivo）、点击跳转（deeplink 进订单页）、角标清除。漏一个，用户就说“你们 App 推送点不进去”。&lt;/p&gt;
&lt;h2&gt;上架资料：技术之外的战场&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;上架资料/&lt;/code&gt; 里有营业执照、软著、隐私政策备份、截图—— AI 生成不了这些。商城 App 上架：隐私政策 URL 必填、权限申请一个一个过（定位/相机/相册/推送都要在政策里写用途）、截图尺寸按市场要求切。&lt;strong&gt;&lt;code&gt;上架资料&lt;/code&gt; 文件夹本身就是交付物&lt;/strong&gt;，和代码同等重要。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;三方集成拼的是细心：universal link、厂商通道、隐私政策，漏一项就打回重来。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>不等应用市场审核：Shorebird 热更新实战</title><link>http://localhost:4321/posts/business3-shorebird/</link><guid isPermaLink="true">http://localhost:4321/posts/business3-shorebird/</guid><description>基础包 + 补丁两步走，线上 bug 当天修，Business3 的热更新流水线。</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么需要热更新&lt;/h2&gt;
&lt;p&gt;商城 App 发版被应用市场审核卡两天是常态，线上支付 bug 等不起。Shorebird（Flutter 官方前成员做的 code push）允许只更新 Dart 层：&lt;code&gt;shorebird.yaml&lt;/code&gt; 里一个 &lt;code&gt;app_id&lt;/code&gt;，启动自动后台拉补丁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app_id: bb7b4b89-eb65-43f9-91ad-04164ce5798c
# auto_update 默认 true，启动自动更新
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;两步流水线&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;脚本/&lt;/code&gt; 目录把流程固化死了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;打带热更新的基础apk包.sh / 打带热更新的基础ipa包.sh  # 大版本才打
打补丁.sh                                              # 日常只打补丁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基础包进市场，补丁走 Shorebird 通道。补丁只能改 Dart（加插件、改原生配置必须重新打基础包）——** boundary 先讲清，免得有人想拿补丁改 App 图标**。&lt;/p&gt;
&lt;h2&gt;我的发版纪律&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;基础包版本号 +88 这种 build 号每次递增，可追溯&lt;/li&gt;
&lt;li&gt;补丁先灰度：内部号验证 → 小比例 → 全量（Shorebird 通道支持 staged rollout）&lt;/li&gt;
&lt;li&gt;回滚也是发补丁：上一个稳定版的补丁重新发布，比下架快十倍&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;热更新不是免死金牌，是止血带：能当天修 bug，也要求补丁流程比整包发版更严谨。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>一个商城 App 的依赖清单解读：Business3 的 pubspec</title><link>http://localhost:4321/posts/business3-deps/</link><guid isPermaLink="true">http://localhost:4321/posts/business3-deps/</guid><description>GetX 状态管理、dio 网络、hive 本地库、微信极光三方件，从一份 pubspec 看商城 App 的技术选型。</description><pubDate>Sun, 09 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一眼结构&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;business3 v3.1.30+88&lt;/code&gt;，商城 + 订单 + 优惠券 + 积分 + 钱包 + 购物车，&lt;code&gt;lib/&lt;/code&gt; 下按 &lt;code&gt;kit_*&lt;/code&gt; 分业务包（&lt;code&gt;kit_mall/kit_order/kit_coupon/kit_wallet/kit_shopping_cart…&lt;/code&gt;），&lt;code&gt;base/&lt;/code&gt; 沉淀通用能力——&lt;strong&gt;按业务垂直 slicing，而不是按技术横向分层&lt;/strong&gt;，商城这种多业务 App 这样拆最耐改。&lt;/p&gt;
&lt;h2&gt;核心选型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态管理 GetX 4.7.3&lt;/strong&gt;：路由 + 依赖注入 + 响应式一把梭，中小团队效率最高的选择&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络 dio 5.9.0 + pretty_dio_logger&lt;/strong&gt;：拦截器统一加签/鉴权，日志拦截器联调期保留&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地 hive_ce + shared_preferences&lt;/strong&gt;：重数据（缓存）走 hive 盒子，轻配置走 prefs，分级存储&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全 encrypt + flutter_dotenv&lt;/strong&gt;：加解密 + &lt;code&gt;.env&lt;/code&gt; 环境隔离，密钥不进代码（&lt;code&gt;config/.env&lt;/code&gt; 还进了 assets，注意别把生产密钥打进包——这就是 dotenv + 多环境要配合的点，下篇细说）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;git 依赖：爽但要 pin&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pull_to_refresh_flutter3:
  git:
    url: https://gitee.com/huanglingbin/pull_to_refresh_flutter3.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个组件直接引了 gitee fork（官方版停更，fork 修了 Flutter 3 兼容）。git 依赖一时爽，&lt;strong&gt;不 pin commit 哪天上游 force-push 就构建失败&lt;/strong&gt;。我的习惯：git 依赖必须带 &lt;code&gt;ref&lt;/code&gt;，定期手动 bump。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;依赖清单就是架构文档：状态、网络、存储、安全、三方，一行一个决策。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>联调页与版本基线：SDK 交付的最后一公里</title><link>http://localhost:4321/posts/supersms-delivery/</link><guid isPermaLink="true">http://localhost:4321/posts/supersms-delivery/</guid><description>SuperSmsTestActivity 默认走正式接口、请求日志、minSdk 24 到 target 36 的取舍，交付清单。</description><pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;联调页：给对接方留的后门&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ui/SuperSmsTestActivity.java&lt;/code&gt; 默认走&lt;strong&gt;正式接口&lt;/strong&gt;：登录、查短信、发短信全流程点一点就通。对接方apkapk 装上先跑联调页，通了再接业务——&lt;strong&gt;SDK 的 Demo 不是摆设，是验收工具&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;请求/响应日志同理：联调时把报文打出来，扯皮时直接贴日志，省掉“我这边没收到”“你参数传错了”三轮拉扯。&lt;/p&gt;
&lt;h2&gt;版本基线：minSdk 24 / target 36 / Java 11&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;minSdk = 24        // Android 7.0，覆盖绝大多数活跃设备
targetSdk = 36
compileSdk = 36
sourceCompatibility = JavaVersion.VERSION_11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;minSdk 24  couper 了 7.0 以下的老古董，换来 desugar 和 API 的省心；target 紧跟最新，不被 Google Play 卡脖子。&lt;code&gt;settings.gradle.kts&lt;/code&gt; 用 &lt;code&gt;FAIL_ON_PROJECT_REPOS&lt;/code&gt; 锁仓库来源，依赖只从 google()/mavenCentral() 来，构建可复现。&lt;/p&gt;
&lt;h2&gt;交付清单（README 即合同）&lt;/h2&gt;
&lt;p&gt;SuperSDK 的 README 写得像合同：功能概览、环境要求、两种引入方式、AAR 产物路径、传递依赖清单、已知未完成项（上报接口后续接）。对接方照着走，跑不通就是我的 bug——&lt;strong&gt;敢这么写，是因为联调页 + 单测兜着底&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SuperSMS 系列四篇完：架构、自动发送、细节、交付。SDK 能力的终极指标：对接方不用来问你。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>实时定位与到期保护：SDK 里魔鬼细节</title><link>http://localhost:4321/posts/supersms-location-guard/</link><guid isPermaLink="true">http://localhost:4321/posts/supersms-location-guard/</guid><description>不读系统定位缓存、请求间隔服务端下发、到期自动退出，商用 SDK 的细节清单。</description><pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;实时定位：不要缓存&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SuperSmsLocationHelper&lt;/code&gt; 强调“获取本次请求后产生的当前坐标，不读取系统定位缓存”。查短信要提交经纬度，缓存坐标会导致位置漂移——对风控/调度类业务，&lt;strong&gt;旧坐标比没坐标更危险&lt;/strong&gt;。代价是每次多等几秒 GPS，但正确性面前，体验让路。&lt;/p&gt;
&lt;h2&gt;节奏听服务端的&lt;/h2&gt;
&lt;p&gt;查询间隔不写死在客户端，由服务端下发（&lt;code&gt;MessagesResponse&lt;/code&gt; 里带间隔参数）。业务方想调频控策略，改服务端配置即可，不用发版。SDK 里凡是“频率”“开关”“阈值”，默认都该是服务端说了算。&lt;/p&gt;
&lt;h2&gt;到期保护 AppExpiryGuard&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 超过 2026-07-29 00:00:00（Asia/Shanghai）后立即退出应用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;试点/联调包加个到期保护是常规操作：外发测试包不回收，旧包一直在外面跑，接口一升级就满地报错。&lt;code&gt;AppExpiryGuardTest&lt;/code&gt; 把边界时间测了——连“自杀开关”都有测试，这就是商用代码和 demo 的区别。&lt;/p&gt;
&lt;h2&gt;我的 SDK 细节清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定位用实时，不用缓存&lt;/li&gt;
&lt;li&gt;频率开关服务端下发&lt;/li&gt;
&lt;li&gt;外发包加到期保护&lt;/li&gt;
&lt;li&gt;未完成接口写进 README，不藏&lt;/li&gt;
&lt;li&gt;请求/响应打日志，联调时能自证清白&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>无障碍服务自动点发送：SuperSMS 的核心黑科技</title><link>http://localhost:4321/posts/supersms-accessibility-sender/</link><guid isPermaLink="true">http://localhost:4321/posts/supersms-accessibility-sender/</guid><description>smsto 调起系统短信 + AccessibilityService 查找发送按钮自动点击，发完回到宿主，全程无需人手。</description><pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;链路总览&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;服务端拉取待发短信
  -&amp;gt; smsto: 调起系统默认短信 App（内容预填）
  -&amp;gt; AutoSmsAccessibilityService 找到发送按钮并点击
  -&amp;gt; 回到宿主 App
  -&amp;gt; 发送结果本地缓存，宿主回前台后消费
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Android 不允许第三方 App 静默发短信（要 &lt;code&gt;SEND_SMS&lt;/code&gt; 强权限且上架受限），于是走“系统 App 代发 + 无障碍辅助点击”的合规路线：&lt;strong&gt;真正点发送的是用户手机里的系统短信应用&lt;/strong&gt;，SDK 只是帮用户省了那一下手指。&lt;/p&gt;
&lt;h2&gt;无障碍服务的正确姿势&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;accessibility/AutoSmsAccessibilityService.java&lt;/code&gt; + &lt;code&gt;res/xml/supersdk_auto_sms_accessibility_service.xml&lt;/code&gt; 声明服务能力。关键细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只在发送流程中激活找按钮，不常驻监听，耗电与隐私影响最小&lt;/li&gt;
&lt;li&gt;按文本/控件特征查找发送按钮，多语言 ROM 兼容要实测（这也是 &lt;code&gt;SuperSmsTestActivity&lt;/code&gt; 联调页存在的意义）&lt;/li&gt;
&lt;li&gt;发完主动回宿主，任务闭环，不把用户晾在短信 App 里&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结果缓存：异步的体面收尾&lt;/h2&gt;
&lt;p&gt;发送结果不强求实时上报（服务端接口还没 ready），先本地缓存，宿主回前台后消费。README 诚实写明“发送结果上报接口后续再接”——&lt;strong&gt;把未完成项写进文档，比藏着强一百倍&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;合规约束下的自动化：权限不够，就用系统能力 + 无障碍拼出体验，前提是只做用户明确授权的事。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>SDK 该长什么样：SuperSMS 的模块架构与双接入</title><link>http://localhost:4321/posts/supersms-sdk-arch/</link><guid isPermaLink="true">http://localhost:4321/posts/supersms-sdk-arch/</guid><description>本地 Module 与 AAR 双接入、consumer-rules 混淆跟随、Retrofit 三层网络，看一个商用级 Android SDK 的骨架。</description><pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;双模块：app 是 demo，SuperSDK 是本体&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SuperSMS/
  app/        # 宿主 Demo：登录框、消息列表、联调入口
  SuperSDK/   # SDK 本体：网络、定位、发送、缓存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;app&lt;/code&gt; 只依赖 &lt;code&gt;project(&quot;:SuperSDK&quot;)&lt;/code&gt;，自己几乎没业务——&lt;strong&gt;Demo 越薄，越证明 SDK 自给自足&lt;/strong&gt;。&lt;code&gt;consumer-rules.pro&lt;/code&gt; 跟着 AAR 走，宿主引入自动合并混淆规则，不用抄文档配 ProGuard。&lt;/p&gt;
&lt;h2&gt;双接入：Module 与 AAR&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 方式一：本地 Module（联调期）
include(&quot;:SuperSDK&quot;)
implementation(project(&quot;:SuperSDK&quot;))

// 方式二：AAR（交付期）
// ./gradlew :SuperSDK:assembleRelease
// 产物：SuperSDK/build/outputs/aar/SuperSDK-release.aar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AAR 方式要手动补传递依赖，README 里列得清清楚楚。交付物 + 依赖清单一次给全，对接方不踩坑。&lt;/p&gt;
&lt;h2&gt;网络层三件套&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;network/&lt;/code&gt; 下三个类分工明确：&lt;code&gt;SuperSmsRetrofitClient&lt;/code&gt; 管 Retrofit 实例与超时重试，&lt;code&gt;SuperSmsApiService&lt;/code&gt; 声明接口，&lt;code&gt;SuperSmsApiHelper&lt;/code&gt; 做业务封装（登录拿 &lt;code&gt;userId/smsToken&lt;/code&gt;、查短信带经纬度）。&lt;code&gt;SuperSmsApiHelperTest&lt;/code&gt; 把登录与查询逻辑先测了——SDK 的网络层必须有单测，因为宿主出问题时第一句话就是“是不是 SDK 的 bug”。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SDK 设计三原则：Demo 要薄、接入要双轨、甩锅要有测试。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>给钱包 App 改包：一次真实的业务场景复盘</title><link>http://localhost:4321/posts/apktool-wallet-scene/</link><guid isPermaLink="true">http://localhost:4321/posts/apktool-wallet-scene/</guid><description>IMToken、TokenPocket 固定包下拉选择，一键改包名重签名，工具如何贴合真实业务流。</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;场景：一个包，多马甲&lt;/h2&gt;
&lt;p&gt;钱包类 App 常要出多个马甲包：同功能、不同包名、不同签名上架。手工流程是灾难：解包 → 全局搜旧包名 → 逐处替换 → 回编 → 对齐 → 签名 → 验签，一步错就白干。&lt;code&gt;change_apk_package.sh&lt;/code&gt; 把它收成一条命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./change_apk_package.sh \
  --apk TokenPocket.apk \
  --new-package com.example.mtsports.lab \
  --build --sign
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;旧包名自动从 &lt;code&gt;AndroidManifest.xml&lt;/code&gt; 读，新包名不给就按时间戳生成，&lt;code&gt;--dry-run&lt;/code&gt; 先演练。&lt;/p&gt;
&lt;h2&gt;网页端：把门槛降到点鼠标&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;apks/&lt;/code&gt; 里放好 &lt;code&gt;IMToken.apk&lt;/code&gt;、&lt;code&gt;TokenPocket.apk&lt;/code&gt;，客户打开页面，下拉选包、点按钮、等进度条、下载 &lt;code&gt;_signed.apk&lt;/code&gt;。固定 APK 只读挂载，客户传不了乱七八糟的文件；签名配置全在服务端环境变量，密码一个字都看不到。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;start_web.sh&lt;/code&gt; 连启动都照顾到了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BIND_HOST=0.0.0.0 PORT=8090 ./start_web.sh  # 局域网/服务器
./start_web.sh                               # 本机默认 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;我的复盘&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具贴合业务流&lt;/strong&gt;：固定包 + 自动包名 + 一键签名，每一步都省掉一次人工决策&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全是默认项&lt;/strong&gt;：密钥不出服务端、上传目录只读，不靠客户自觉&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档即交付&lt;/strong&gt;：&lt;code&gt;DOCKER_DEPLOY.md&lt;/code&gt; 把本机测试、上传清单、启动命令写死，客户照抄就行&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;apktool 系列六篇完：Web 服务、签名链路、Docker、安全隔离、安装脚本、任务系统、业务复盘。工具类项目的全部功课都在这了。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>800MB 上传也不怕：APK 工具的任务系统设计</title><link>http://localhost:4321/posts/apktool-job-system/</link><guid isPermaLink="true">http://localhost:4321/posts/apktool-job-system/</guid><description>uuid 任务、线程池执行、实时日志、24 小时过期清理，一个改包任务从上传到签名的全生命周期。</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;改包是长任务，只能异步&lt;/h2&gt;
&lt;p&gt;apktool 解包一个大包 + 回编 + 签名，几分钟起步。&lt;code&gt;web_server.py&lt;/code&gt; 的任务模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST 上传  -&amp;gt;  秒回 { job_id }
前端轮询  -&amp;gt;  { status: queued|running|done|failed, log }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;threading&lt;/code&gt; 跑任务不 block 主服务，多人同时改包互不干扰。日志实时追加，前端看着 apktool 一行行输出，卡在哪一步一目了然——&lt;strong&gt;长任务的可观测性就是把日志流出来&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;大文件：800MB 是认真的&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MAX_UPLOAD_MB=800&lt;/code&gt;，钱包类 App 的包体就是这个量级。800MB 上传意味着：磁盘预留、超时设置、失败续传（前端分片是后续优化点），以及最重要的——&lt;strong&gt;做完就清理&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;lab 目录：真实的产出物&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;lab/TokenPocket_decoded_com.wallet.app.android.pkg1788080769_signed.apk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解包目录 + 时间戳包名 + &lt;code&gt;_signed&lt;/code&gt; 后缀，一看就是实战跑出来的。&lt;code&gt;web_jobs/&lt;/code&gt; 24 小时过期清理，保证磁盘只增不爆。改包名前缀 &lt;code&gt;com.wallet.app.android.pkg + 时间戳&lt;/code&gt; 自动生成，不传参也能跑，传参则精确控制。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;长任务三件套：秒回 ID、轮询日志、过期清理。这套 pattern 从改包工具到任何耗时服务都通用。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>安装脚本的自我修养：apktool 三模式安装</title><link>http://localhost:4321/posts/apktool-install-script/</link><guid isPermaLink="true">http://localhost:4321/posts/apktool-install-script/</guid><description>install_apktool.sh 用 auto/brew/manual 三模式、dry-run 预演和版本锁定，把环境准备做成一件不翻车的事。</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;环境准备是最被低估的环节&lt;/h2&gt;
&lt;p&gt;apktool 依赖 Java + Android build-tools + apktool.jar，三者版本错一个就全崩。&lt;code&gt;install_apktool.sh&lt;/code&gt;（289 行）把“装环境”做成可靠操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./install_apktool.sh --dry-run              # 先看它要干什么
./install_apktool.sh --yes                   # 一键装
./install_apktool.sh --method manual --version 3.0.3  # 锁定版本
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三模式：auto / brew / manual&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;auto&lt;/strong&gt;：有 Homebrew 就用 brew，没有就 fallback 手动安装，智能但不自作主张&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;brew&lt;/strong&gt;：macOS 最省心的一路&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;manual&lt;/strong&gt;：下载 &lt;code&gt;apktool.jar&lt;/code&gt; + 官方 wrapper 脚本，&lt;code&gt;--prefix&lt;/code&gt; 指定目录，服务器裸机就用它&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;两个好习惯：dry-run 与版本 pin&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--dry-run&lt;/code&gt; 只打印不执行，跑在客户机器前先看一遍，心里有底。&lt;code&gt;--version&lt;/code&gt; 锁定 3.0.3——Dockerfile 里 &lt;code&gt;APKTOOL_VERSION&lt;/code&gt; 也是 ARG 固定的，&lt;strong&gt;本地、CI、容器三处同版本&lt;/strong&gt;，排除了“我这能跑”类问题的一大半。&lt;/p&gt;
&lt;p&gt;装完还校验 &lt;code&gt;apktool&lt;/code&gt; 进没进 PATH，不可用直接报错，不留“好像装好了”的薛定谔状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;安装脚本是项目的第一印象：新人（或客户）第一条命令就翻车，后面说什么都没用。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>27 种语言：VSports 的国际化实战</title><link>http://localhost:4321/posts/vsports-i18n-observability/</link><guid isPermaLink="true">http://localhost:4321/posts/vsports-i18n-observability/</guid><description>前端 27 个语言包 + 服务端球队译名管线 + 可观测与限流，一套组合拳 serve 全球球迷。</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前端：27 个语言包&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/i18n/&lt;/code&gt; 下 27 个文件：&lt;code&gt;ar/cs/de/en/es/fa/fr/hr/id/it/ja/ko/nl/no/pt-BR/pt/ru/sv/tr/uk/uz/zh-TW/zh&lt;/code&gt;……时区、主题、单位（公制/英制）还在设置页可调。看球的人遍布全球，语言和单位一个不能少。&lt;/p&gt;
&lt;h2&gt;服务端：球队名是另一套管线&lt;/h2&gt;
&lt;p&gt;界面文案走前端语言包，球队、联赛、球员译名走 MySQL（&lt;code&gt;football_translations&lt;/code&gt; + 三层覆盖），原因很简单：球队几千支，译名天天变，&lt;strong&gt;发版周期跟不上数据变化&lt;/strong&gt;，必须走库 + 后台实时改。&lt;/p&gt;
&lt;h2&gt;可观测与限流：藏在 internal 里&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;internal/observability&lt;/code&gt;：&lt;code&gt;logger.go&lt;/code&gt; 统一日志、&lt;code&gt;client_ip&lt;/code&gt; 真实 IP 识别（反代后面拿 IP 是基本功）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;internal/ratelimit&lt;/code&gt;：&lt;code&gt;limiter.go&lt;/code&gt; 接口限流，公开 API 不设防等于邀请爬虫&lt;/li&gt;
&lt;li&gt;&lt;code&gt;internal/store&lt;/code&gt;：repository 分层 + &lt;code&gt;migrations.go&lt;/code&gt; 版本化建表，建表语句跟着代码走，新环境一把起&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我的 i18n 清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;界面文案与数据译名分离&lt;/li&gt;
&lt;li&gt;复数/单位/时区从第一天设计&lt;/li&gt;
&lt;li&gt;未翻译词条可追踪（&lt;code&gt;admin_untranslated_translations&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;限流日志先行，开源 API 才敢开放&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;VSports 系列六篇完：前端工程化、数据链路、部署、测试、后台、国际化。一套全栈基本功，都在这了。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>后台也是产品：VSports 的管理后台与 TOTP 登录</title><link>http://localhost:4321/posts/vsports-admin-totp/</link><guid isPermaLink="true">http://localhost:4321/posts/vsports-admin-totp/</guid><description>Admin.tsx 管赛事和翻译，xlsx 批量导入词典，TOTP 守住登录口，后台体验决定运营效率。</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;后台管什么&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/pages/Admin.tsx&lt;/code&gt; + &lt;code&gt;AdminLogin.tsx&lt;/code&gt;：赛事维护（新增/修改自定义赛事，直接合并进首页接口）、热门赛事配置、翻译修正。运营改个球队中文名，保存即生效，前端实时读库——&lt;strong&gt;后台改数据，不是提需求&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;翻译管线：三层覆盖&lt;/h2&gt;
&lt;p&gt;服务端翻译是三层结构，一层压一层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;批量词典（machine_generated / batch）
  -&amp;gt; 导入词典（imported）
    -&amp;gt; 人工修正（overrides，最高优先级）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;admin_translation_dictionary.go&lt;/code&gt;、&lt;code&gt;admin_translation_xlsx.go&lt;/code&gt;：运营用 Excel 批量导词典，&lt;code&gt;admin_untranslated_translations.go&lt;/code&gt; 还能列出“还没翻译的词条”，翻译进度一目了然。前端 27 个语言包（&lt;code&gt;ar/cs/de/en/es/fa/ja/ko/pt/ru/zh…&lt;/code&gt;）只管界面文案，球队队名走服务端管线——&lt;strong&gt;界面语言和数据翻译分开治&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;TOTP 守门&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;totp.go&lt;/code&gt; + &lt;code&gt;ADMIN_TOTP_SECRET&lt;/code&gt;：后台登录除了密码还要动态口令。比分站后台能改全站数据，被脱库撞库的代价极高，多一行 TOTP，多一层命。&lt;code&gt;security_test.go&lt;/code&gt; 把这些守卫都测了一遍。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后台的用户是运营，运营的时间也是钱：批量导入、未翻译清单、一键生效，这些才是后台的 KPI。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>每个文件都配测试：VSports 的 Go 质量观</title><link>http://localhost:4321/posts/vsports-go-quality/</link><guid isPermaLink="true">http://localhost:4321/posts/vsports-go-quality/</guid><description>server/internal/app 下几乎每个 Go 文件都有 _test.go，配额、同步锁、缓存、TOTP 全覆盖，测试即设计。</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;测试目录一眼震撼&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;server/internal/app/&lt;/code&gt; 里 &lt;code&gt;.go&lt;/code&gt; 和 &lt;code&gt;_test.go&lt;/code&gt; 一一配对：&lt;code&gt;bulk_sync&lt;/code&gt; 有测试、&lt;code&gt;football_quota&lt;/code&gt; 有测试、&lt;code&gt;sync_lock&lt;/code&gt; 有测试、&lt;code&gt;totp&lt;/code&gt; 有测试、&lt;code&gt;security&lt;/code&gt; 有测试，连 &lt;code&gt;mysql_deadlock&lt;/code&gt; 都有专门的测试。这不是“补测试”，这是&lt;strong&gt;测试即设计&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;配额账本：上游 API 是花钱的&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;provider_budget_test.go&lt;/code&gt; + &lt;code&gt;provider_usage.go&lt;/code&gt; + &lt;code&gt;football_quota_test.go&lt;/code&gt;：每次向上游请求都记账，超预算就停。职业联赛数据接口按调用量计费，没有配额账本，半夜一个 bug 能把月预算烧光。我的原则：&lt;strong&gt;凡是花钱的调用，必须有计数器和熔断&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;同步锁与死锁测试&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sync_lock_test.go&lt;/code&gt;：定时同步 120 秒一轮，多实例部署时不能重复跑，锁先行。&lt;code&gt;mysql_deadlock_test.go&lt;/code&gt;：高并发写赛程时死锁怎么解，测试里先演练一遍再上线。&lt;code&gt;fixture_retention_test.go&lt;/code&gt;：历史数据保留策略，旧数据按时归档，库不无限膨胀。&lt;/p&gt;
&lt;h2&gt;缓存策略&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;football_cache_policy.go&lt;/code&gt; + &lt;code&gt;reference_cache.go&lt;/code&gt;：球队、联赛这种几乎不变的引用数据缓存起来，实时比分才走库。动静分离，首页 150 场分页才能稳在毫秒级。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;测试覆盖率不是 KPI，是睡眠质量。下篇聊聊后台和 TOTP 登录。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>全球玩家都能玩：OpenRA 的音频、字体与本地化</title><link>http://localhost:4321/posts/openra-audio-i18n/</link><guid isPermaLink="true">http://localhost:4321/posts/openra-audio-i18n/</guid><description>AudioLoaders 管 BGM 与音效，Fluent 管多语言，字体管 CJK，一套引擎如何 serve 全世界。</description><pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;BGM 一响，DNA 就动了&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenRA.Mods.Common/AudioLoaders/&lt;/code&gt; 负责解码原版音频格式。复刻 pain 点：90 年代西木的音频是专有格式，加载器要一字节不差地还原。当年 Hell March 前奏一响，全网吧回头——这种“味道正确”全靠加载器 fidelity。&lt;/p&gt;
&lt;p&gt;音效还有距离衰减：交火在屏幕外只能听个闷响，方位+距离混音，战场感就有了。小细节，大沉浸。&lt;/p&gt;
&lt;h2&gt;Fluent 本地化：不只是翻译&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;FluentBundle.cs&lt;/code&gt; / &lt;code&gt;FluentProvider.cs&lt;/code&gt; 表明用的是 Mozilla Fluent 方案。Fluent 比传统 key-value 强在&lt;strong&gt;语法感知&lt;/strong&gt;：复数、性别、格变化写进表达式，比如“3 辆坦克”中英俄语的复数规则各不相同，Fluent 一行表达式全搞定。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FluentTest&lt;/code&gt; 保证改文案不断键——本地化 key 也是代码，断了编译期（测试期）就要叫。&lt;/p&gt;
&lt;h2&gt;字体：CJK 是硬骨头&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Fonts.cs&lt;/code&gt; + 像素风字体的矛盾：英文 26 字母好办，中日韩几千字形，位图字体包体积爆炸。方案一般是动态字形缓存：用到了才渲染进图集。中文玩家能玩到完整汉化，背后是这套缓存。&lt;/p&gt;
&lt;h2&gt;我的收获&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;味道正确是 feature：音频格式还原、UI 动效，还原度就是口碑&lt;/li&gt;
&lt;li&gt;本地化前置：文案从第一天走 Fluent，后期补翻译不用重构&lt;/li&gt;
&lt;li&gt;字体、音频、文案三件套，决定了你的游戏能走多远——引擎跑得再快，玩家看不懂听不清也白搭&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;游戏开发系列七篇完。从 Trait、寻路、渲染、联机、测试、Mod 到本地化，OpenRA 通关 🎮 下个系列想看什么，留言板吼一声。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>配表也是代码：OpenRA 的测试与 Lint 双保险</title><link>http://localhost:4321/posts/openra-testing-lint/</link><guid isPermaLink="true">http://localhost:4321/posts/openra-testing-lint/</guid><description>OpenRA.Test 测引擎，Lint 测配表，一个 Mod 几千行 YAML 靠什么不翻车。</description><pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引擎测试：OpenRA.Test&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenRA.Test/&lt;/code&gt; 里按模块分了测试集，打开一看全是基本功：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MiniYamlTest&lt;/code&gt; / &lt;code&gt;FieldLoaderTest&lt;/code&gt; / &lt;code&gt;FieldSaverTest&lt;/code&gt;：配表解析与回写的 round-trip，改解析器不丢数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CPosTest&lt;/code&gt; / &lt;code&gt;CoordinateTest&lt;/code&gt;：格子坐标、像素坐标、亚格子坐标互转——RTS 里坐标错 1 像素，单位就卡墙里&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ColorTests&lt;/code&gt;、&lt;code&gt;FluentTest&lt;/code&gt;、&lt;code&gt;ActorInfoTest&lt;/code&gt;：颜色、本地化、单位装配，各管一摊&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;坐标和配表解析被重点照顾，一点不意外：这是引擎里&lt;strong&gt;错一次、全盘崩&lt;/strong&gt;的两处。&lt;/p&gt;
&lt;h2&gt;配表 Lint：策划的编译器&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenRA.Mods.Common/Lint/&lt;/code&gt; 是我最想抄的一套：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CheckUnknownTraitFields&lt;/code&gt;：Trait 名拼错、字段写错直接报错&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CheckTraitPrerequisites&lt;/code&gt;：前置条件缺失（比如挂了武器没挂弹药）提前揪出&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CheckTraitLocation&lt;/code&gt;：Trait 放错位置（只能挂建筑的挂到了步兵上）拦下&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;策划改几千行 YAML，进游戏前先过 Lint，等于给配表配了个“编译器”。我们之前 apktool 项目里缺的就是这环，深有体会。&lt;/p&gt;
&lt;h2&gt;我的收获&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;测试分层：引擎逻辑用单元测试，数据内容用 Lint，各扫各的门前雪&lt;/li&gt;
&lt;li&gt;高风险点重点投：坐标、序列化、配表解析，值得用测试集围起来&lt;/li&gt;
&lt;li&gt;开源协作靠门禁：&lt;code&gt;make test&lt;/code&gt; + CI 全绿才合入，人一多就知道香了&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>124 行开一个服：OpenRA 的专用服务器与联机同步</title><link>http://localhost:4321/posts/openra-netcode-server/</link><guid isPermaLink="true">http://localhost:4321/posts/openra-netcode-server/</guid><description>OpenRA.Server 入口只有 124 行，一键开服背后是 RTS 经典的指令同步 + 确定性模拟。</description><pubDate>Sun, 19 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;小而美的入口&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenRA.Server/Program.cs&lt;/code&gt; 一共 124 行：解析参数、起端口、进主循环。开服就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./launch-dedicated.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;玩家连进来选图、选阵营、倒计时、开打。没有大厅 microservice，没有 K8s，就是一个进程——RTS 专用服的典范：&lt;strong&gt;够用即最好&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;只同步指令，不同步状态&lt;/h2&gt;
&lt;p&gt;RTS 一局几千个单位，逐帧同步状态能把带宽打爆。OpenRA 用经典方案：&lt;strong&gt;所有客户端跑完全相同的确定性模拟，网络只传玩家指令&lt;/strong&gt;（“3 号坦克去 (56,78)”）。&lt;/p&gt;
&lt;p&gt;这要求极严的确定性：随机数种子同步、浮点行为一致、遍历顺序固定。之前聊寻路时说的定点数，根子就在这里—— physics 稍微一飘，8 个客户端就打出 8 种结局，也就是联机玩家最恨的“同步错误”。&lt;/p&gt;
&lt;h2&gt;ServerTraits：服务器也有 Trait&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenRA.Mods.Common/ServerTraits/&lt;/code&gt; 表明连服务器行为都是可组合的：大厅管理、观战、录像，各 Mod 按需挂载。Trait 思想从 gameplay 一直贯穿到联机层，前后一致的设计语言，读代码特别顺。&lt;/p&gt;
&lt;h2&gt;我的收获&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;联机方案选型看品类：RTS 用指令同步 + 确定性模拟，FPS 用状态同步 + 预测回滚，没有银弹&lt;/li&gt;
&lt;li&gt;专用服保持小而专：一个进程做好一件事，比大而全好运维&lt;/li&gt;
&lt;li&gt;确定性是 feature：从随机数到容器遍历顺序，都要为“多端一致”让路&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>同一张贴图两种阵营色：OpenRA 的渲染与调色板魔法</title><link>http://localhost:4321/posts/openra-rendering-palette/</link><guid isPermaLink="true">http://localhost:4321/posts/openra-rendering-palette/</guid><description>调色板重映射实现阵营色、GLSL 后期做超时空传送特效，复古像素风背后的现代渲染管线。</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;复古外皮，现代心脏&lt;/h2&gt;
&lt;p&gt;OpenRA 要还原 90 年代西木像素风，但跑在现代 GPU 上。&lt;code&gt;OpenRA.Game/Graphics/&lt;/code&gt; 里是自研渲染器：&lt;code&gt;Palette.cs&lt;/code&gt;（调色板）、&lt;code&gt;PlayerColorRemap.cs&lt;/code&gt;（阵营色重映射）、&lt;code&gt;HardwarePalette.cs&lt;/code&gt;，配 &lt;code&gt;glsl/&lt;/code&gt; 下一堆 shader。&lt;/p&gt;
&lt;h2&gt;阵营色：换色不换图&lt;/h2&gt;
&lt;p&gt;红军坦克和蓝军坦克用&lt;strong&gt;同一张&lt;/strong&gt;贴图。原理是调色板索引：贴图里留一段索引专门表示“阵营色”，渲染时按玩家颜色实时重映射。&lt;code&gt;PlayerColorRemap&lt;/code&gt; 就是干这个的——内存省一半，Mod 作者也不用为 8 个颜色各画一套。&lt;/p&gt;
&lt;h2&gt;后期特效全是 shader&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;glsl/&lt;/code&gt; 目录像个特效菜单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;postprocess_chronoshift.frag&lt;/code&gt;：超时空传送的扭曲溶解&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postprocess_flash.frag&lt;/code&gt;：核弹/超武白闪&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postprocess_menufade.frag&lt;/code&gt;：菜单过渡&lt;/li&gt;
&lt;li&gt;&lt;code&gt;combined.vert/frag&lt;/code&gt;、&lt;code&gt;model.vert/frag&lt;/code&gt;：精灵与模型的常规管线&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;红警老玩家看到超时空传送的光效 DNA 动了——底层就是这几十行 GLSL。&lt;/p&gt;
&lt;h2&gt;我的收获&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;约束即风格&lt;/strong&gt;：调色板限制反而成就了像素风，还顺手解决了阵营色问题&lt;/li&gt;
&lt;li&gt;渲染与逻辑分离：&lt;code&gt;Renderable&lt;/code&gt; 只描述“画什么”，管线决定“怎么画”，加特效不用碰 gameplay 代码&lt;/li&gt;
&lt;li&gt;复刻经典 ≠ 复刻技术，用现代管线实现复古观感才是正解&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>不写引擎也能做游戏：OpenRA 的 Mod 生态</title><link>http://localhost:4321/posts/openra-mod-ecosystem/</link><guid isPermaLink="true">http://localhost:4321/posts/openra-mod-ecosystem/</guid><description>YAML 配规则、Lua 写任务、地图编辑器画地图，自带红警、泰伯利亚、沙丘三个完整 Mod，这才叫生态。</description><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;三个官方 Mod 就是最好文档&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mods/&lt;/code&gt; 下躺着 &lt;code&gt;ra&lt;/code&gt;（红警）、&lt;code&gt;cnc&lt;/code&gt;（泰伯利亚）、&lt;code&gt;d2k&lt;/code&gt;（沙丘2000），每个都是完整游戏：规则、任务、过场、平衡性。要学怎么写单位行为，直接抄官方 Mod 的 YAML，比看文档快十倍。&lt;/p&gt;
&lt;h2&gt;三件套：YAML + Lua + 地图编辑器&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;YAML&lt;/strong&gt;：单位、武器、建筑、科技树全是配表，&lt;code&gt;Trait&lt;/code&gt; 文档自动生成（&lt;code&gt;docs.openra.net&lt;/code&gt;），配错有 &lt;code&gt;Lint/&lt;/code&gt; 检查器兜底&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lua&lt;/strong&gt;：&lt;code&gt;Scripting/&lt;/code&gt; 模块提供任务脚本 API，做战役关卡、触发器、过场剧情，不用碰 C#&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地图编辑器&lt;/strong&gt;：&lt;code&gt;EditorBrushes/&lt;/code&gt;，画地形、摆出生点，&lt;code&gt;MapGenerator&lt;/code&gt; 还能随机生成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Lint&lt;/code&gt; 检查器这点我要单独夸：配表在进游戏前先过一遍静态检查，拼错 Trait 名、缺字段直接报错。一个 Mod 几千行 YAML，没有 Lint 会疯。&lt;/p&gt;
&lt;h2&gt;跨平台与打包&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;packaging/&lt;/code&gt; 下 &lt;code&gt;linux&lt;/code&gt;、&lt;code&gt;macos&lt;/code&gt;、&lt;code&gt;windows&lt;/code&gt;、&lt;code&gt;source&lt;/code&gt;、&lt;code&gt;package-all.sh&lt;/code&gt; 全齐，.NET 一套代码三端发。&lt;code&gt;OpenRA.Test/&lt;/code&gt; 保证重构不翻车，开源协作的标配。&lt;/p&gt;
&lt;h2&gt;我的 takeaway&lt;/h2&gt;
&lt;p&gt;好引擎的标志：&lt;strong&gt;把 90% 的创造力交给不会写引擎的人&lt;/strong&gt;。Trait 管能力、YAML 管数值、Lua 管剧情、编辑器管地图——分层之后，每个人只玩自己那一层。作为又爱打游戏又爱折腾的动手党，这套设计我直接抄作业：以后自己做小项目，也要先想好“别人怎么不写代码就玩起来”。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;游戏开发系列三篇完。下次开新坑，可能是自己动手写个小 Mod 实战。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RTS 最硬的两块骨头：寻路与战争迷雾</title><link>http://localhost:4321/posts/openra-pathfinding-shroud/</link><guid isPermaLink="true">http://localhost:4321/posts/openra-pathfinding-shroud/</guid><description>从 OpenRA 的 Pathfinder 和 Shroud 系统，看 RTS 引擎如何让几百个单位不撞车、不穿墙，还互相看不见。</description><pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;寻路：几百个单位同时找路&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenRA.Mods.Common/Pathfinder/&lt;/code&gt; 是独立模块。RTS 寻路的难点从来不是单个 A*，而是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;量大&lt;/strong&gt;：框选 50 个坦克点一下右键，50 条路径同时算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态&lt;/strong&gt;：友军自己就是障碍物，路要边走边修&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层&lt;/strong&gt;：步兵能过桥、坦克不能下水，地形对不同移动类型是不同的图&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OpenRA 的做法是分层抽象 + 增量修路，一帧摊一点，不让寻路卡住主循环。&lt;code&gt;Activities/&lt;/code&gt; 里的 &lt;code&gt;Move&lt;/code&gt; 行为只管“沿着路径走”，路径本身由寻路系统异步供给——&lt;strong&gt;寻路与移动解耦&lt;/strong&gt;，这个分工值得抄。&lt;/p&gt;
&lt;h2&gt;战争迷雾：看不见的艺术&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ShroudExts.cs&lt;/code&gt; + &lt;code&gt;AffectsShroud&lt;/code&gt; Trait：每个单位自带视野半径，迷雾是全图一张逐格状态（可见 / 曾见 / 未见）。建筑、步兵、飞机的视野各不相同，叠加计算。&lt;/p&gt;
&lt;p&gt;迷雾不只是“黑布一盖”，它直接参与玩法：看不见就不能选中、不能攻击，侦察单位因此有了战术价值。&lt;/p&gt;
&lt;h2&gt;联机：指令同步而非状态同步&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenRA.Server/&lt;/code&gt; 是独立的专用服务器。RTS 联机的经典方案是&lt;strong&gt;只同步玩家指令、各端确定性模拟&lt;/strong&gt;，流量极小，&lt;code&gt;launch-dedicated.sh&lt;/code&gt; 一键就能开服。代价是模拟必须完全确定性——随机数、浮点都要管住，这也是引擎里随处可见定点数的原因。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;打游戏时觉得“这波操作变形了”，现在知道可能是寻路在修路；觉得“视野被压制”，背后是一张逐格计算的迷雾表。懂引擎之后，看比赛都更香了。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Trait 组合模式：OpenRA 的类 ECS 单位设计</title><link>http://localhost:4321/posts/openra-trait-design/</link><guid isPermaLink="true">http://localhost:4321/posts/openra-trait-design/</guid><description>不写深继承树，用 Trait 拼装坦克、步兵和建筑，聊聊 OpenRA 这套组合式设计的聪明之处。</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;深受震撼的目录&lt;/h2&gt;
&lt;p&gt;第一次打开 &lt;code&gt;OpenRA.Mods.Common/Traits/&lt;/code&gt;，里面躺着几百个 Trait：&lt;code&gt;Armor.cs&lt;/code&gt;（护甲）、&lt;code&gt;Armament.cs&lt;/code&gt;（武器挂载）、&lt;code&gt;Attack/&lt;/code&gt;（攻击逻辑）、&lt;code&gt;Air/&lt;/code&gt;（飞行）、&lt;code&gt;AmmoPool.cs&lt;/code&gt;（弹药）。一个单位是什么？&lt;strong&gt;是一堆 Trait 的组合&lt;/strong&gt;，不是七层继承下来的子类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mods/ra/rules/infantry.yaml 大概长这样
E1:
  Health:
    HP: 5000
  Armor:
    Type: None
  Mobile:
    Speed: 56
  AttackFrontal:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;策划配 YAML，引擎负责把 Trait 拼成活的单位。加新单位常常&lt;strong&gt;零代码&lt;/strong&gt;，配表就行。&lt;/p&gt;
&lt;h2&gt;为什么是 Trait 而不是继承&lt;/h2&gt;
&lt;p&gt;RTS 单位的能力是正交的：能移动 × 能攻击 × 能运输 × 能部署，继承树会被组合爆炸撑死。Trait 把每个能力做成独立小类，挂载即拥有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AffectsShroud&lt;/code&gt;：影响战争迷雾&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AcceptsDeliveredCash&lt;/code&gt;：能收钱（矿车卸货）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ActorSpawner&lt;/code&gt;：能下崽（母巢、兵营出兵都用它）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我的收获&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;组合优于继承&lt;/strong&gt;：能力正交时，用组件拼装代替类层次&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据驱动&lt;/strong&gt;：YAML 描述 + &lt;code&gt;FieldLoader&lt;/code&gt; 反射灌入，策划和程序解耦&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Actor.cs&lt;/code&gt; + &lt;code&gt;Activities/&lt;/code&gt;（行为）+ &lt;code&gt;Orders/&lt;/code&gt;（指令）三层分工，单位的状态机清清爽爽&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;写业务代码也一样：if-else 堆到三层，就该想想是不是缺了一套 Trait。下篇聊 RTS 最硬的两块骨头：寻路和迷雾。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>一个镜像装下全套工具链：apktool 项目的 Docker 与安全隔离</title><link>http://localhost:4321/posts/apktool-docker-delivery/</link><guid isPermaLink="true">http://localhost:4321/posts/apktool-docker-delivery/</guid><description>Python + Java 17 + Android SDK + apktool 打进单个镜像，只读挂载签名密钥，客户页面永远看不到密码。</description><pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;单镜像：网页、apktool、Java、SDK 全在一起&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;FROM python:3.11-slim-bookworm
# openjdk-17-jre + Android cmdline-tools + build-tools:36.0.0
# + apktool_3.0.3.jar，一次 RUN 装完，sdkmanager licenses 自动确认
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;版本全部 ARG 化：&lt;code&gt;APKTOOL_VERSION&lt;/code&gt;、&lt;code&gt;ANDROID_BUILD_TOOLS_VERSION&lt;/code&gt;，升级只改两个数字。部署时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d --build
# 打开 http://服务器IP:8090
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安全隔离：签名密钥不出服务端&lt;/h2&gt;
&lt;p&gt;这是我最在意的设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volumes:
  - ./wallet.jks:/app/wallet.jks:ro   # 只读挂载
  - ./apks:/app/apks:ro                # 固定 APK 只读
environment:
  SIGN_KEYSTORE: &quot;/app/wallet.jks&quot;
  SIGN_KEY_ALIAS: &quot;wallet&quot;
  SIGN_PASSWORD: &quot;abc1234&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.dockerignore&lt;/code&gt; 默认&lt;strong&gt;不把&lt;/strong&gt; &lt;code&gt;wallet.jks&lt;/code&gt; 打进镜像，密钥只存在服务器目录&lt;/li&gt;
&lt;li&gt;客户页面只能选固定 APK、下点按钮，签名路径、别名、密码全部在服务端，HTML 里一个字都不出现&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web_jobs/&lt;/code&gt; 可写挂载做任务区，&lt;code&gt;lab/&lt;/code&gt; 做解包工作区，权限边界清晰&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我的运维技能点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;多工具链镜像：apt 体积控制（&lt;code&gt;--no-install-recommends&lt;/code&gt; + 清 lists）、SDK 缓存清理，镜像不臃肿&lt;/li&gt;
&lt;li&gt;配置分离：&lt;code&gt;.env&lt;/code&gt; / compose environment 管密码，仓库只有示例&lt;/li&gt;
&lt;li&gt;交付闭环：&lt;code&gt;DOCKER_DEPLOY.md&lt;/code&gt; 写清本机测试 → 服务器上传清单 → 启动命令，客户照着敲就行&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;从 Python 后端、Android 打包链路到 Docker 交付，这就是我在 apktool 项目里的一条龙技能。更多实战，欢迎翻 Kanshi 板块其他文章。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>APK 改包签名链路：反编译、改包名、回编、签名一次讲清</title><link>http://localhost:4321/posts/apk-package-sign/</link><guid isPermaLink="true">http://localhost:4321/posts/apk-package-sign/</guid><description>change_apk_package.sh 600+ 行，把 apktool 解包、包名替换、zipalign 对齐、apksigner 签名验签串成一条可靠流水线。</description><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;链路总览&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.apk
 -&amp;gt; apktool decode（反编译）
 -&amp;gt; 改包名（AndroidManifest + 资源 + smali）
 -&amp;gt; apktool build（回编译出 unsigned 包）
 -&amp;gt; zipalign（4 字节对齐）
 -&amp;gt; apksigner sign（v1+v2 签名）
 -&amp;gt; apksigner verify（验签确认）
 -&amp;gt; _signed.apk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;change_apk_package.sh&lt;/code&gt;（600+ 行）把这条链串起来，&lt;code&gt;set -euo pipefail&lt;/code&gt; 打底，任何一步失败立刻停，不产出半成品包。&lt;/p&gt;
&lt;h2&gt;CLI 设计：默认能跑，参数能调&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;./change_apk_package.sh \
  --apk TokenPocket.apk \
  --new-package com.example.mtsports.lab \
  --build --sign
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--old-package&lt;/code&gt; 省略时自动从 &lt;code&gt;AndroidManifest.xml&lt;/code&gt; 读&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--new-package&lt;/code&gt; 省略时按 &lt;code&gt;com.wallet.app.android.pkg + 时间戳&lt;/code&gt; 自动生成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--force-decode&lt;/code&gt; 覆盖旧的解包目录，&lt;code&gt;--dry-run&lt;/code&gt; 只演练不落盘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--apksigner/--zipalign&lt;/code&gt; 默认从 Android SDK build-tools 自动探测，也支持手动指定&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;签名：对齐 → 签名 → 验签，缺一不可&lt;/h2&gt;
&lt;p&gt;顺序不能错：先 &lt;code&gt;zipalign&lt;/code&gt; 再 &lt;code&gt;sign&lt;/code&gt;，签完立刻 &lt;code&gt;verify&lt;/code&gt;。脚本里签名密码支持三种来源——参数传入、环境变量 &lt;code&gt;APK_SIGN_PASSWORD&lt;/code&gt;、交互式输入，CI 和手工两不误。&lt;/p&gt;
&lt;h2&gt;踩过的坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;包名替换不能只改 Manifest，provider 的 &lt;code&gt;authorities&lt;/code&gt;、smali 里的引用要一起走，否则装得上、跑不起来&lt;/li&gt;
&lt;li&gt;build-tools 版本要锁死（我们用 36.0.0），不同版本 apksigner 行为有差异，Docker 构建时用 ARG 固定&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Android 打包链路是体力 + 细心活，我的原则：失败即停、产物可验、版本锁定。下篇聊怎么把这一整套装进 Docker。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>零依赖 Python Web 服务：APK 改包工具的后端是这样写的</title><link>http://localhost:4321/posts/apktool-web-server/</link><guid isPermaLink="true">http://localhost:4321/posts/apktool-web-server/</guid><description>只用 Python 标准库手写 ThreadingHTTPServer，实现上传、任务队列、进度查询和过期清理，不引入任何框架。</description><pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么零依赖&lt;/h2&gt;
&lt;p&gt;APK 改包工具要跑在客户服务器上，环境越简单越可靠。&lt;code&gt;web_server.py&lt;/code&gt;（800+ 行）只用标准库：&lt;code&gt;http.server&lt;/code&gt; 的 &lt;code&gt;ThreadingHTTPServer&lt;/code&gt; 做并发，&lt;code&gt;threading + uuid&lt;/code&gt; 管任务，&lt;code&gt;subprocess&lt;/code&gt; 调改包脚本——&lt;strong&gt;不装 Flask/Django，拷过去就能跑&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;任务模型：提交即返回，轮询查进度&lt;/h2&gt;
&lt;p&gt;改包动辄几分钟，不能让 HTTP 请求一直挂着。我的设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /api/jobs  -&amp;gt;  { job_id }           # 秒回
GET  /api/jobs/:id  -&amp;gt;  { status, log }  # 前端轮询
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任务状态机：&lt;code&gt;queued → running → done/failed&lt;/code&gt;，日志实时追加，前端页面能看到 apktool 每一行输出，出错直接定位。&lt;/p&gt;
&lt;h2&gt;过期清理：磁盘不会被撑爆&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;JOB_RETENTION_SECONDS = int(os.environ.get(&quot;JOB_RETENTION_HOURS&quot;, &quot;24&quot;)) * 3600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个 job 带时间戳，后台线程定期扫 &lt;code&gt;web_jobs/&lt;/code&gt;，超 24 小时自动删。APK 动辄几百 MB（&lt;code&gt;MAX_UPLOAD_MB=800&lt;/code&gt;），没有这套机制，跑一周磁盘就满了。&lt;/p&gt;
&lt;h2&gt;前端：单文件内嵌页面&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;INDEX_HTML&lt;/code&gt; 直接嵌在 Python 文件里：上传表单、固定 APK 下拉框、进度条、日志框一次给全。无构建步骤，改个文案保存即生效，交付给客户时少一个构建环节就少一分风险。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我的后端习惯：并发用线程池、任务可追踪、磁盘有上限。下篇聊真正的硬核部分——apktool 改包签名链路。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Docker Compose 一键部署：web + api + mysql 全栈上线</title><link>http://localhost:4321/posts/docker-fullstack-deploy/</link><guid isPermaLink="true">http://localhost:4321/posts/docker-fullstack-deploy/</guid><description>VSports 的 web、Go API、MySQL 三件套，用 Docker Compose 编排，健康检查 + 环境变量隔离，Token 不出服务端。</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;编排结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;services:
  web:    # React 静态产物 + Node 代理，:80 对外
  api:    # Go 中台，:8090，只对内
  mysql:  # MySQL 8.4，数据持久化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一条命令起全栈：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp .env.docker.example .env   # 填 Token 和强密码
docker compose -p vsports up -d --build
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;我的 DevOps 习惯&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置与代码分离&lt;/strong&gt;：&lt;code&gt;API_FOOTBALL_TOKEN&lt;/code&gt;、&lt;code&gt;EURO365_DATA_API_KEY&lt;/code&gt;、&lt;code&gt;MYSQL_PASSWORD&lt;/code&gt;、&lt;code&gt;ADMIN_TOTP_SECRET&lt;/code&gt; 全走 &lt;code&gt;.env&lt;/code&gt;，仓库里只有 &lt;code&gt;.example&lt;/code&gt;，密钥不进 git。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健康检查&lt;/strong&gt;：web 服务对 &lt;code&gt;http://127.0.0.1/&lt;/code&gt; 做 30 秒间隔探活，挂了自动重启（&lt;code&gt;unless-stopped&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志收敛&lt;/strong&gt;：json-file 驱动限制 &lt;code&gt;max-size: 10m&lt;/code&gt;、&lt;code&gt;max-file: 3&lt;/code&gt;，磁盘不会被日志打爆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端代理&lt;/strong&gt;：开发期 Vite 把 &lt;code&gt;/api/*&lt;/code&gt; 代理到本地 &lt;code&gt;127.0.0.1:8090&lt;/code&gt;，生产由 web 镜像内的代理层转发，前后端联调零跨域烦恼。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发版产物&lt;/strong&gt;：&lt;code&gt;releases/&lt;/code&gt; 下按时间戳打 &lt;code&gt;tar&lt;/code&gt; 包 + &lt;code&gt;sha256&lt;/code&gt;，可回滚、可校验。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安全小结&lt;/h2&gt;
&lt;p&gt;上游 Token、数据库密码、后台 TOTP 密钥全部只存在服务端环境里。Nginx 只暴露 80，前端拿到的永远是加工过的业务数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;三篇连起来就是我的全栈技能：React 前端、Go + MySQL 中台、Docker 运维。代码之外的思考，欢迎来留言板找我聊。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Go 中台 + MySQL：多数据源比分链路是这样搭的</title><link>http://localhost:4321/posts/go-mysql-pipeline/</link><guid isPermaLink="true">http://localhost:4321/posts/go-mysql-pipeline/</guid><description>API-FOOTBALL、Euro365、BSD 三路数据，经 Go 定时同步、去重合并后落 MySQL，再经分页接口喂给前端。</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;数据链路总览&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;API-FOOTBALL + Euro365 + BSD
  -&amp;gt; Go API 定时同步、赛事去重、按需补详情
  -&amp;gt; MySQL
  -&amp;gt; /api/v1/*
  -&amp;gt; React 前端
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心原则只有一条：&lt;strong&gt;Token 只在服务端，浏览器永远碰不到上游密钥&lt;/strong&gt;。前端要数据，一律走 &lt;code&gt;/api/v1/*&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;定时同步：首页只读库&lt;/h2&gt;
&lt;p&gt;Go 服务默认每 120 秒把当天赛程同步进 MySQL。首页翻页直接读库，每页 150 场，状态筛选、分组统计、优先级排序、&lt;code&gt;LIMIT/OFFSET&lt;/code&gt; 全在 SQL 里做完。&lt;/p&gt;
&lt;p&gt;切换到还没同步过的日期时，中台只在&lt;strong&gt;首次访问&lt;/strong&gt;时向上游补一次数，之后翻页全走库——即使当天没比赛，也不会重复打上游，省配额又快。&lt;/p&gt;
&lt;h2&gt;多源合并去重&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Euro365 的直播、赛前、比分数据进来后，和 API-FOOTBALL 重复的比赛按&lt;strong&gt;开赛时间 + 双方球队&lt;/strong&gt;合并，Euro365 独有的比赛用独立 ID 保存。&lt;/li&gt;
&lt;li&gt;BSD（Bzzoiro Sports Data）走独立 Tab，按天拉原始赛事，前后日期可浏览。&lt;/li&gt;
&lt;li&gt;详情页的事件、阵容、技术统计、球员数据，由中台&lt;strong&gt;按需&lt;/strong&gt;向上游补充，不预全量拉。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;翻译：词典打底，人工说了算&lt;/h2&gt;
&lt;p&gt;中文译名存在 &lt;code&gt;football_translations&lt;/code&gt; 表：批量词典先铺底，后台人工修正直接覆盖。运营改名不需要后端发版，前端实时生效。&lt;/p&gt;
&lt;h2&gt;我的后端技能点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Go：定时任务、HTTP 中台、数据转换与合并&lt;/li&gt;
&lt;li&gt;MySQL 8.4：表结构设计、分页与筛选下沉、翻译表&lt;/li&gt;
&lt;li&gt;数据意识：去重策略、补数节流、配额保护&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;前端 + 中台都有了，还差上线。下一篇聊 Docker Compose 一键部署。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>React 19 + TypeScript：我是怎么做前端工程化的</title><link>http://localhost:4321/posts/react-frontend-engineering/</link><guid isPermaLink="true">http://localhost:4321/posts/react-frontend-engineering/</guid><description>以 VSports 足球比分站为例，聊聊 React 19、Vite、PWA、多语言和质量门禁在我手里的实践。</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;项目背景&lt;/h2&gt;
&lt;p&gt;VSports 是一个足球比分与赛事数据站：比分首页、热门赛事、比赛 / 联赛 / 球队 / 球员 / 场馆详情页，再加设置页和管理后台。页面多、数据密、还要多语言，对前端工程化要求不低。&lt;/p&gt;
&lt;p&gt;我的前端选型：&lt;strong&gt;React 19 + TypeScript + Vite + react-router-dom 7&lt;/strong&gt;，包管理用 &lt;strong&gt;Bun&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;PWA：比分站就该能离线看&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;vite-plugin-pwa&lt;/code&gt; + &lt;code&gt;workbox-window&lt;/code&gt; 把站点做成 PWA，比赛进行中时弱网也能看上次缓存的比分，体验差距很明显。&lt;/p&gt;
&lt;h2&gt;i18n：多语言从第一天就设计好&lt;/h2&gt;
&lt;p&gt;中文译名、界面文案全部走翻译层，而不是写死在组件里。球队联赛译名支持后台人工修正覆盖批量词典——运营改个名字，前端不用发版。&lt;/p&gt;
&lt;h2&gt;质量门禁：类型、规范、无障碍一个不少&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bun run typecheck   # tsc -b，全量类型检查
bun run lint        # Biome 检查
bun run smoke       # 冒烟测试
bun run a11y        # axe-core + puppeteer 无障碍测试
bun run checkall:build  # 一条龙：检查 + 构建 + 冒烟 + 无障碍
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的习惯：&lt;strong&gt;能进 CI 的检查，就不要靠人眼&lt;/strong&gt;。&lt;code&gt;checkall:build&lt;/code&gt; 跑通才算真正可发布。&lt;/p&gt;
&lt;h2&gt;路由与数据&lt;/h2&gt;
&lt;p&gt;前端路由用 Hash（&lt;code&gt;/#/football&lt;/code&gt; 等），根地址和未知路由统一跳转到比分首页，避免静态部署时的 404 坑。首页数据走中台分页接口（每页 150 场），状态筛选、分组、排序、分页全部由 MySQL 完成，前端只做分组展示：进行中 → 半场 → 即将开赛 → 完场 → 其他。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是我技能栈的前端部分：React 19、TS 强类型、PWA、i18n、自动化质量门禁。下一篇聊聊 Go 中台。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item></channel></rss>