<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>数据开放平台 - category - 沐木</title><link>https://oldletter.cn/categories/%E6%95%B0%E6%8D%AE%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0/</link><description>数据开放平台 - category - 沐木</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://oldletter.cn/categories/%E6%95%B0%E6%8D%AE%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0/" rel="self" type="application/rss+xml"/><item><title>数据开放平台架构设计（一）：从 SQL2API 到数据开放平台的演进</title><link>https://oldletter.cn/posts/dp-01-architecture/</link><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/dp-01-architecture/</guid><description><![CDATA[<p>数据开放平台本质上是把&quot;数据变成服务&quot;这件事系统化。我一开始做的是 SQL2API：页面上写 SQL、配置参数，系统自动生成接口。这个工具确实能省掉很多重复 CRUD，但一旦拿去给多个业务方长期使用，问题就不只是&quot;能不能查出数据&quot;了。</p>
<p>真实落地时，大家会马上追问：谁能调这个接口？能看到哪些字段？一次上限查多少？慢查询谁负责？数据源密码放哪？字段改名会影响哪些接口？有没有审计记录？这些都不是 SQL2API 本身能顺手解决的问题。</p>
<p>这一篇聊背景：为什么要从一个 SQL2API 工具，继续往数据开放平台演进，以及平台的架构怎么拆。</p>
<hr>
<h2 id="一从-sql2api-到数据开放平台">一、从 SQL2API 到数据开放平台</h2>
<p>SQL2API 解决的是一个很具体的问题：<strong>把 SQL 变成 API，省掉 CRUD 的模板代码。</strong> 这个能力很有价值，但它只是数据开放链路里的一个环节。</p>
<p>真实场景里，一个完整的数据开放链路长这样：</p>
<ol>
<li><strong>数据接入</strong>：连各种数据源（MySQL、PostgreSQL、ES、Hive、API），把数据接进来</li>
<li><strong>数据加工</strong>：字段映射、格式转换、数据脱敏、聚合计算</li>
<li><strong>服务发布</strong>：把加工后的数据发布成 API，支持多种协议（REST、gRPC、WebSocket）</li>
<li><strong>访问控制</strong>：AK/SK 认证、租户隔离、流量控制、配额管理</li>
<li><strong>监控治理</strong>：调用链追踪、慢查询告警、数据血缘、审计日志</li>
<li><strong>运维管理</strong>：灰度发布、容灾备份、SDK 生成、文档管理</li>
</ol>
<p>SQL2API 只覆盖了第 3 步的一部分。要从&quot;工具&quot;升级为&quot;平台&quot;，需要把这六步都串起来，而且要能长期运维。</p>
<p>类比一下：SQL2API 是一个厨师，能把食材做成菜。数据开放平台是一个餐厅，从采购（数据接入）、备菜（数据加工）、烹饪（服务发布）、上菜（访问控制）、品控（监控治理）到运营（运维管理），全流程都有。</p>
<h3 id="11-为什么不直接开放数据库">1.1 为什么不直接开放数据库</h3>
<p>有人会问：既然本质是查数据，为什么不直接给业务方一个只读账号，让他们连库查？</p>
<p>这个方案短期速度高，长期更麻烦：</p>
<ul>
<li><strong>权限太粗</strong>：数据库账号一般按库、表授权，很难细到&quot;这个应用只能看订单表里的 5 个字段&quot;。</li>
<li><strong>审计断层</strong>：只能看到某个账号执行了 SQL，很难还原到具体应用、具体接口、具体业务场景。</li>
<li><strong>流量不可控</strong>：一个全表扫描或者大分页，就可能把线上库拖慢。</li>
<li><strong>数据口径失控</strong>：每个调用方自己写 SQL，同一个指标可能有三种算法。</li>
<li><strong>变更风险大</strong>：表结构改了，谁受影响靠猜，没人愿意拍胸脯。</li>
</ul>
<p>所以数据库可以作为底座，但不应该直接暴露给调用方。平台要把权限、限流、审计、口径和 SLA 都挡在前面。</p>
<h3 id="12-为什么也不能只靠网关">1.2 为什么也不能只靠网关</h3>
<p>API Gateway 很重要，但它解决的是入口问题：认证、限流、路由、协议转换。它不知道某个接口背后查了哪些表，也不知道手机号字段该不该脱敏，更不知道这个 SQL 是否会扫全表。</p>
<p>数据开放平台需要补的是数据侧能力：</p>
<ul>
<li><strong>权限</strong>：应用级、API 级、字段级、数据范围级权限。</li>
<li><strong>审计</strong>：谁在什么时间，用什么参数，访问了什么数据。</li>
<li><strong>限流与配额</strong>：保护平台，也保护底层数据库。</li>
<li><strong>元数据</strong>：数据源、表、字段、接口、参数、返回结构都要可管理。</li>
<li><strong>血缘</strong>：一个字段变更，要知道影响哪些 API 和调用方。</li>
<li><strong>数据源管理</strong>：连接池、密钥、健康检查、备用数据源不能散落在代码里。</li>
<li><strong>SLA</strong>：接口超时、成功率、慢查询、告警都要有明确指标。</li>
</ul>
<p>这些能力靠网关很难补齐，必须下沉到数据服务平台本身。</p>]]></description></item><item><title>数据开放平台架构设计（二）：SQL 转 API 引擎</title><link>https://oldletter.cn/posts/dp-02-sql-to-api/</link><pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/dp-02-sql-to-api/</guid><description><![CDATA[<blockquote>
<p><strong>上一篇：</strong> <a href="/posts/dp-01-architecture/" rel="">数据开放平台（一）：从 SQL2API 到数据开放平台的演进</a></p></blockquote>
<p>SQL 转 API 引擎是整个数据开放平台的核心。用户在界面上写一条受控 SQL 模板，配好参数规则，系统自动生成一个可调用的 API。听起来简单，但要做到&quot;运行时解析、校验、执行、审计&quot;这一整套链路，细节其实不少。</p>
<p>下面看看这个引擎到底是怎么实现的。</p>
<hr>
<h2 id="一为什么选-jdbctemplate">一、为什么选 JdbcTemplate</h2>
<p>结论：这个场景里，执行层放在 JdbcTemplate 上。</p>
<p>为什么不是 JPA？因为 JPA 要求编译期定义 Entity，SQL2API 的 SQL 在运行时才确定，核心不知道返回什么字段。JPA 的 EntityManager 也执行原生 SQL，但返回值绑死在 Entity 上，灵活性为零。</p>
<p>为什么不是 MyBatis？MyBatis 的 XML 是编译时写死的，要支持运行时动态 SQL 得 hack 进 Configuration 对象，线程不安全、内存泄漏、复杂度高。收益为零。</p>
<p>JdbcTemplate 的好处是足够薄：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-java">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// 运行时执行任意 SQL，返回 List&lt;Map&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Map</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">Object</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">jdbcTemplate</span><span class="p">.</span><span class="na">queryForList</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s">&#34;SELECT * FROM user WHERE status = ? AND name LIKE ?&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;%&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34;%&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// 结果直接是通用 Map，不用定义任何 Entity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">//</span><span class="w"> </span><span class="n">连接自动归还</span><span class="err">，</span><span class="n">异常自动处理</span><span class="err">，</span><span class="n">参数自动绑定防注入</span></span></span></code></pre></div></div>
<p>它不要求提前定义 Entity，也不强行接管 SQL 生成过程。对于&quot;运行时执行 SQL 模板，返回通用结构&quot;这个场景，JdbcTemplate 的抽象刚刚好：连接释放、异常转换、参数绑定这些脏活它来做，SQL 的控制权仍然在平台手里。</p>]]></description></item><item><title>数据开放平台架构设计（三）：鉴权与访问控制</title><link>https://oldletter.cn/posts/dp-03-auth-and-control/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/dp-03-auth-and-control/</guid><description><![CDATA[<blockquote>
<p><strong>上一篇：</strong> <a href="/posts/dp-02-sql-to-api/" rel="">数据开放平台（二）：SQL 转 API 引擎</a></p></blockquote>
<p>数据开放平台把数据变成 API 给外部调用，安全问题是第一位的。谁都能调不行，调了不该看的字段也不行，更不能让某个调用方把数据库连接池打满。</p>
<p>这一篇聊三个问题：<strong>怎么认证调用方身份</strong>、<strong>怎么隔离不同租户的数据</strong>、<strong>怎么控制调用频率</strong>。</p>
<hr>
<h2 id="一aksk-签名认证">一、AK/SK 签名认证</h2>
<p>认证方案有很多种，为什么选 AK/SK？因为数据开放平台的调用方通常是<strong>服务端应用</strong>，不是浏览器用户。服务端对服务端的认证，AK/SK 是更成熟的方案，AWS、阿里云、腾讯云的开放 API 全用这个。</p>
<h3 id="核心概念">核心概念</h3>
<ul>
<li><strong>Access Key (AK)</strong>：调用方的身份标识，公开的，放在请求头里</li>
<li><strong>Secret Key (SK)</strong>：签名密钥，保密的，不在网络上传输</li>
<li><strong>签名 (Signature)</strong>：用 SK 对请求内容做 HMAC-SHA256，服务端验签</li>
</ul>
<p>为什么 SK 不直接传？因为网络传输可能被中间人截获。签名的好处是：即使签名被截获，攻击者也无法反推出 SK，而且签名是和请求内容绑定的，换个请求签名就失效了。</p>
<h3 id="签名流程">签名流程</h3>
<p>AK/SK 认证的关键是两端对同一份规范化请求做签名，服务端不接收明文 SK：</p>
<div class="mermaid" id="id-3" data-mermaid-definition="c2VxdWVuY2VEaWFncmFtCiAgICAgICAgICAgICAgICAgICAgcGFydGljaXBhbnQgU0RLIGFzIOiwg&#43;eUqOaWuSBTREsKICAgICAgICAgICAgICAgICAgICBwYXJ0aWNpcGFudCBHVyBhcyBBUEkg572R5YWzCiAgICAgICAgICAgICAgICAgICAgcGFydGljaXBhbnQgQXV0aCBhcyDpibTmnYMgSGFuZGxlcgogICAgICAgICAgICAgICAgICAgIHBhcnRpY2lwYW50IFN0b3JlIGFzIOWHreaNruWtmOWCqAogICAgICAgICAgICAgICAgICAgIHBhcnRpY2lwYW50IEN0eCBhcyDor7fmsYLkuIrkuIvmlocKICAgICAgICAgICAgICAgICAgICBTREstPj5TREs6IOinhOiMg&#43;WMluaWueazleOAgei3r&#43;W&#43;hOOAgeWPguaVsOOAgeaXtumXtOaIs&#43;OAgU5vbmNlCiAgICAgICAgICAgICAgICAgICAgU0RLLT4&#43;U0RLOiDkvb/nlKggU0sg6K6h566XIEhNQUMKICAgICAgICAgICAgICAgICAgICBTREstPj5HVzog5pC65bimIEFL44CB562&#43;5ZCN44CB5pe26Ze05oiz44CBTm9uY2UKICAgICAgICAgICAgICAgICAgICBHVy0&#43;PkF1dGg6IOmAj&#43;S8oOiupOivgeWktAogICAgICAgICAgICAgICAgICAgIEF1dGgtPj5TdG9yZTog55SoIEFLIOafpeivoiBTSyDmkZjopoHmiJblr4bpkqXmnZDmlpkKICAgICAgICAgICAgICAgICAgICBTdG9yZS0tPj5BdXRoOiDov5Tlm57osIPnlKjmlrnkuI7np5/miLfkv6Hmga8KICAgICAgICAgICAgICAgICAgICBBdXRoLT4&#43;QXV0aDog5qCh6aqM5pe26Ze056qX5Y&#43;j5LiOIE5vbmNlCiAgICAgICAgICAgICAgICAgICAgQXV0aC0&#43;PkF1dGg6IOmHjeeul&#43;etvuWQjeW5tuavlOi&#43;gwogICAgICAgICAgICAgICAgICAgIEF1dGgtPj5DdHg6IOWGmeWFpeW6lOeUqOOAgeenn&#43;aIt&#43;OAgeadg&#43;mZkOiMg&#43;WbtA==">sequenceDiagram
                    participant SDK as 调用方 SDK
                    participant GW as API 网关
                    participant Auth as 鉴权 Handler
                    participant Store as 凭据存储
                    participant Ctx as 请求上下文
                    SDK-&gt;&gt;SDK: 规范化方法、路径、参数、时间戳、Nonce
                    SDK-&gt;&gt;SDK: 使用 SK 计算 HMAC
                    SDK-&gt;&gt;GW: 携带 AK、签名、时间戳、Nonce
                    GW-&gt;&gt;Auth: 透传认证头
                    Auth-&gt;&gt;Store: 用 AK 查询 SK 摘要或密钥材料
                    Store--&gt;&gt;Auth: 返回调用方与租户信息
                    Auth-&gt;&gt;Auth: 校验时间窗口与 Nonce
                    Auth-&gt;&gt;Auth: 重算签名并比较
                    Auth-&gt;&gt;Ctx: 写入应用、租户、权限范围</div><p>调用方发起请求时：</p>]]></description></item><item><title>数据开放平台架构设计（四）：数据转换与加工</title><link>https://oldletter.cn/posts/dp-04-data-transform/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/dp-04-data-transform/</guid><description><![CDATA[<blockquote>
<p><strong>上一篇：</strong> <a href="/posts/dp-03-auth-and-control/" rel="">数据开放平台（三）：鉴权与访问控制</a></p></blockquote>
<p>SQL 查出来的原始数据，通常不能直接给调用方用。数据库字段名是 <code>user_name</code>，调用方要 <code>userName</code>；手机号 <code>13812345678</code> 不能直接暴露，得打成 <code>138****5678</code>；日期格式是 <code>2026-04-23 10:30:00</code>，调用方只要 <code>2026-04-23</code>。</p>
<p>这些&quot;末尾一公里&quot;的数据加工，就是数据转换引擎干的事。它不是替代数仓 ETL，也不应该把复杂计算都塞到接口层；它更适合做轻量、在线、和接口返回强相关的处理。</p>
<hr>
<h2 id="一整体设计">一、整体设计</h2>
<p>数据转换引擎用<strong>规则驱动</strong>的架构。用户在界面上配置转换规则，运行时引擎按规则依次处理。每条规则是一个独立的 Handler，可插拔、可组合。</p>
<p>数据转换流水线要把字段形态写清楚，避免后续在响应层临时拼逻辑：</p>
<div class="mermaid" id="id-2" data-mermaid-definition="Zmxvd2NoYXJ0IExSCiAgICAgICAgICAgICAgICAgICAgQVsi5Y6f5aeL57uT5p6c6ZuGPGJyLz5kYl9jb2x1bW4iXSAtLT4gQlsi5a2X5q615pig5bCEPGJyLz5hcGlGaWVsZCJdCiAgICAgICAgICAgICAgICAgICAgQiAtLT4gQ1si6ISx5pWPPGJyLz5tYXNrZWRGaWVsZCJdCiAgICAgICAgICAgICAgICAgICAgQyAtLT4gRFsi5qC85byP5YyWPGJyLz5kaXNwbGF5VmFsdWUiXQogICAgICAgICAgICAgICAgICAgIEQgLS0&#43;IEVbIuWtl&#43;auteiuoeeulzxici8&#43;Y29tcHV0ZWRGaWVsZCJdCiAgICAgICAgICAgICAgICAgICAgRSAtLT4gRlsi5bCP57uT5p6c6ZuG6IGa5ZCIPGJyLz5zdW1tYXJ5Il0KICAgICAgICAgICAgICAgICAgICBGIC0tPiBHWyLlrZfmrrXmnYPpmZDov4fmu6Q8YnIvPnZpc2libGVGaWVsZHMiXQogICAgICAgICAgICAgICAgICAgIEcgLS0&#43;IEhbIuWTjeW6lOWMheijhTxici8&#43;ZGF0YeOAgW1ldGHjgIF0cmFjZUlkIl0=">flowchart LR
                    A[&#34;原始结果集&lt;br/&gt;db_column&#34;] --&gt; B[&#34;字段映射&lt;br/&gt;apiField&#34;]
                    B --&gt; C[&#34;脱敏&lt;br/&gt;maskedField&#34;]
                    C --&gt; D[&#34;格式化&lt;br/&gt;displayValue&#34;]
                    D --&gt; E[&#34;字段计算&lt;br/&gt;computedField&#34;]
                    E --&gt; F[&#34;小结果集聚合&lt;br/&gt;summary&#34;]
                    F --&gt; G[&#34;字段权限过滤&lt;br/&gt;visibleFields&#34;]
                    G --&gt; H[&#34;响应包装&lt;br/&gt;data、meta、traceId&#34;]</div><div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-java">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Component</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">DataTransformEngine</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 所有 TransformHandler 按 Order 排序</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Autowired</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">List</span><span class="o">&lt;</span><span class="n">TransformHandler</span><span class="o">&gt;</span><span class="w"> </span><span class="n">handlers</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">List</span><span class="o">&lt;</span><span class="n">Map</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">Object</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="nf">transform</span><span class="p">(</span><span class="n">TransformConfig</span><span class="w"> </span><span class="n">config</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                 </span><span class="n">List</span><span class="o">&lt;</span><span class="n">Map</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">Object</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="n">rawData</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">config</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">config</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w"> </span><span class="n">rawData</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">List</span><span class="o">&lt;</span><span class="n">Map</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">Object</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">rawData</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">TransformHandler</span><span class="w"> </span><span class="n">handler</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">handlers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">handler</span><span class="p">.</span><span class="na">supports</span><span class="p">(</span><span class="n">config</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">handler</span><span class="p">.</span><span class="na">apply</span><span class="p">(</span><span class="n">config</span><span class="p">,</span><span class="w"> </span><span class="n">result</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">result</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span></span></span></code></pre></div></div>
<p>生产里我会给转换引擎加两个边界：第一，单次处理行数不能无限大，超过阈值必须分页或异步导出；第二，规则执行要记录耗时，某条自定义正则特别慢时能快速定位。</p>]]></description></item><item><title>数据开放平台架构设计（五）：监控与治理</title><link>https://oldletter.cn/posts/dp-05-monitor-and-governance/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/dp-05-monitor-and-governance/</guid><description><![CDATA[<blockquote>
<p><strong>上一篇：</strong> <a href="/posts/dp-04-data-transform/" rel="">数据开放平台（四）：数据转换与加工</a></p></blockquote>
<p>数据开放平台上线之后，更怕两件事：一是出了问题不知道，二是知道了问题找不到原因。API 响应慢了，是 SQL 没走索引还是数据库负载高？某个调用方报错，是参数传错了还是数据源挂了？某个字段被改了，谁改的？什么时候改的？</p>
<p>这些问题靠翻代码加日志是搞不定的，需要系统化的监控和治理能力。这一篇聊四个核心能力：<strong>调用链追踪</strong>、<strong>慢查询告警</strong>、<strong>数据血缘</strong>、<strong>审计日志</strong>。</p>
<hr>
<h2 id="一调用链追踪">一、调用链追踪</h2>
<p>每个 API 调用都要记录完整的执行链路：从请求进来到响应返回，中间经历了哪些步骤，每步耗时多少。</p>
<h3 id="traceid-机制">TraceId 机制</h3>
<p>每个请求分配一个全局唯一的 TraceId，贯穿整个调用链。后续所有日志、指标、告警都带上这个 TraceId，方便关联查询。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-java">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Component</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nd">@Order</span><span class="p">(</span><span class="o">-</span><span class="n">100</span><span class="p">)</span><span class="w"> </span><span class="c1">// 最先执行</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">TraceHandler</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">PipelineHandler</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">TRACE_ID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;X-Trace-Id&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">before</span><span class="p">(</span><span class="n">SqlApiConfig</span><span class="w"> </span><span class="n">config</span><span class="p">,</span><span class="w"> </span><span class="n">Map</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">Object</span><span class="o">&gt;</span><span class="w"> </span><span class="n">params</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                        </span><span class="n">PipelineContext</span><span class="w"> </span><span class="n">context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 优先用调用方传来的 TraceId（链路透传）</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">traceId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">RequestContext</span><span class="p">.</span><span class="na">getHeader</span><span class="p">(</span><span class="n">TRACE_ID</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">traceId</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">traceId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">generateTraceId</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">context</span><span class="p">.</span><span class="na">setTraceId</span><span class="p">(</span><span class="n">traceId</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 记录请求开始时间</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">context</span><span class="p">.</span><span class="na">setStartTime</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="na">currentTimeMillis</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// TraceId 放入 MDC，后续日志自动带上</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">MDC</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="s">&#34;traceId&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">traceId</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">MDC</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="s">&#34;apiId&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">config</span><span class="p">.</span><span class="na">getApiId</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">MDC</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="s">&#34;apiPath&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">config</span><span class="p">.</span><span class="na">getApiPath</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">after</span><span class="p">(</span><span class="n">SqlApiConfig</span><span class="w"> </span><span class="n">config</span><span class="p">,</span><span class="w"> </span><span class="n">Object</span><span class="w"> </span><span class="n">result</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                       </span><span class="n">PipelineContext</span><span class="w"> </span><span class="n">context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kt">long</span><span class="w"> </span><span class="n">elapsed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">System</span><span class="p">.</span><span class="na">currentTimeMillis</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">context</span><span class="p">.</span><span class="na">getStartTime</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">log</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">&#34;API 调用完成 traceId={} api={} elapsed={}ms rows={}&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">context</span><span class="p">.</span><span class="na">getTraceId</span><span class="p">(),</span><span class="w"> </span><span class="n">config</span><span class="p">.</span><span class="na">getApiPath</span><span class="p">(),</span><span class="w"> </span><span class="n">elapsed</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">result</span><span class="w"> </span><span class="k">instanceof</span><span class="w"> </span><span class="n">Collection</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="p">((</span><span class="n">Collection</span><span class="o">&lt;?&gt;</span><span class="p">)</span><span class="w"> </span><span class="n">result</span><span class="p">).</span><span class="na">size</span><span class="p">()</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">MDC</span><span class="p">.</span><span class="na">clear</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="nf">generateTraceId</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">UUID</span><span class="p">.</span><span class="na">randomUUID</span><span class="p">().</span><span class="na">toString</span><span class="p">().</span><span class="na">replace</span><span class="p">(</span><span class="s">&#34;-&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">).</span><span class="na">substring</span><span class="p">(</span><span class="n">0</span><span class="p">,</span><span class="w"> </span><span class="n">16</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span></span></span></code></pre></div></div>
<h3 id="调用链记录">调用链记录</h3>
<p>每个步骤的耗时和结果都记录下来，形成完整的调用链：</p>]]></description></item><item><title>数据开放平台架构设计（六）：落地实践与运维</title><link>https://oldletter.cn/posts/dp-06-practice-and-ops/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/dp-06-practice-and-ops/</guid><description><![CDATA[<blockquote>
<p><strong>上一篇：</strong> <a href="/posts/dp-05-monitor-and-governance/" rel="">数据开放平台（五）：监控与治理</a></p></blockquote>
<p>前面五篇聊了架构、引擎、鉴权、转换、监控，都是&quot;怎么设计&quot;。这一篇聊&quot;怎么落地&quot;，接入流程怎么走、SDK 和文档怎么生成、上线怎么灰度、出了问题怎么兜底。</p>
<p>我自己的经验是，数据开放平台成不成，末尾不只看代码写得多漂亮，更看流程有没有闭环：申请、审批、联调、发布、监控、回滚、下线，每一步都要有人负责、有记录可查。</p>
<hr>
<h2 id="一接入流程">一、接入流程</h2>
<p>一个新调用方从注册到正式使用，标准流程分四步：</p>
<p>接入流程要把审批、联调、发布、观察放进同一条链路，避免接口上线后没人认领：</p>
<div class="mermaid" id="id-4" data-mermaid-definition="Zmxvd2NoYXJ0IExSCiAgICAgICAgICAgICAgICAgICAgQVsi5bqU55So55Sz6K&#43;3Il0gLS0&#43;IEJbIuadg&#43;mZkOWuoeaJuSJdCiAgICAgICAgICAgICAgICAgICAgQiAtLT4gQ1siQUsvU0sg5Y&#43;R5pS&#43;Il0KICAgICAgICAgICAgICAgICAgICBDIC0tPiBEWyJTYW5kYm94IOiBlOiwgyJdCiAgICAgICAgICAgICAgICAgICAgRCAtLT4gRVsiQVBJIOmFjee9ruivhOWuoSJdCiAgICAgICAgICAgICAgICAgICAgRSAtLT4gRlsi54Gw5bqm5Y&#43;R5biDIl0KICAgICAgICAgICAgICAgICAgICBGIC0tPiBHWyLnm5Hmjqfop4Llr58iXQogICAgICAgICAgICAgICAgICAgIEcgLS0&#43;IEh7IuaMh&#43;agh&#43;i&#43;vuaghyJ9CiAgICAgICAgICAgICAgICAgICAgSCAtLSAi5pivIiAtLT4gSVsi5YWo6YeP5Y&#43;R5biDIl0KICAgICAgICAgICAgICAgICAgICBIIC0tICLlkKYiIC0tPiBKWyLlm57mu5oiXQogICAgICAgICAgICAgICAgICAgIEkgLS0&#43;IEtbIuWumuacn&#43;WkjeebmCJdCiAgICAgICAgICAgICAgICAgICAgSiAtLT4gSw==">flowchart LR
                    A[&#34;应用申请&#34;] --&gt; B[&#34;权限审批&#34;]
                    B --&gt; C[&#34;AK/SK 发放&#34;]
                    C --&gt; D[&#34;Sandbox 联调&#34;]
                    D --&gt; E[&#34;API 配置评审&#34;]
                    E --&gt; F[&#34;灰度发布&#34;]
                    F --&gt; G[&#34;监控观察&#34;]
                    G --&gt; H{&#34;指标达标&#34;}
                    H -- &#34;是&#34; --&gt; I[&#34;全量发布&#34;]
                    H -- &#34;否&#34; --&gt; J[&#34;回滚&#34;]
                    I --&gt; K[&#34;定期复盘&#34;]
                    J --&gt; K</div><h3 id="第一步申请接入">第一步：申请接入</h3>
<p>调用方在管理后台提交接入申请，填写：应用名称、负责人、使用场景、预期 QPS、需要访问的数据范围。审批通过后，系统自动生成 AK/SK 并分配配额。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-java">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">AppRegistrationService</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Transactional</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">AppInfo</span><span class="w"> </span><span class="nf">register</span><span class="p">(</span><span class="n">AppRegistrationRequest</span><span class="w"> </span><span class="n">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 1. 生成 AK/SK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">accessKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;ak_&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">UUID</span><span class="p">.</span><span class="na">randomUUID</span><span class="p">().</span><span class="na">toString</span><span class="p">().</span><span class="na">replace</span><span class="p">(</span><span class="s">&#34;-&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">secretKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;sk_&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">UUID</span><span class="p">.</span><span class="na">randomUUID</span><span class="p">().</span><span class="na">toString</span><span class="p">().</span><span class="na">replace</span><span class="p">(</span><span class="s">&#34;-&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 2. 保存应用信息</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">AppInfo</span><span class="w"> </span><span class="n">app</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">AppInfo</span><span class="p">.</span><span class="na">builder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">appName</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="na">getAppName</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">accessKey</span><span class="p">(</span><span class="n">accessKey</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">secretKey</span><span class="p">(</span><span class="n">encrypt</span><span class="p">(</span><span class="n">secretKey</span><span class="p">))</span><span class="w"> </span><span class="c1">// SK 加密存储</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">owner</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="na">getOwner</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">status</span><span class="p">(</span><span class="n">AppStatus</span><span class="p">.</span><span class="na">PENDING</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">dailyQuota</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="na">getDailyQuota</span><span class="p">()</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">?</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">request</span><span class="p">.</span><span class="na">getDailyQuota</span><span class="p">()</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">10000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">qpsLimit</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="na">getQpsLimit</span><span class="p">()</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">?</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">request</span><span class="p">.</span><span class="na">getQpsLimit</span><span class="p">()</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">100</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">appMapper</span><span class="p">.</span><span class="na">insert</span><span class="p">(</span><span class="n">app</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 3. 发送审批通知</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">notifyAdmin</span><span class="p">(</span><span class="s">&#34;新接入申请: &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">request</span><span class="p">.</span><span class="na">getAppName</span><span class="p">(),</span><span class="w"> </span><span class="n">app</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">app</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span></span></span></code></pre></div></div>
<p>SK 只在创建时展示一次，后续后台只能重置不能查看。这个规则一定要写进产品交互里，否则密钥加密存储就只做了一半。</p>]]></description></item></channel></rss>