<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
	<channel>
		<title>WEOUR Build —— 公开构建日志</title>
		<link>https://weour.com/zh/build/</link>
		<description>WEOUR OS 公开构建日志：多店铺、库存与仓储系统、SKU 映射与 AI 代理，全部在生产环境中构建。</description>
		<language>zh-CN</language>
		<atom:link href="https://weour.com/zh/build/feed/" rel="self" type="application/rss+xml" />
		<atom:link href="https://weour.com/build/feed/" rel="alternate" hreflang="en" type="application/rss+xml" />
		<atom:link href="https://weour.com/ru/build/feed/" rel="alternate" hreflang="ru" type="application/rss+xml" />
		<atom:link href="https://weour.com/zh/build/feed/" rel="alternate" hreflang="zh-CN" type="application/rss+xml" />
		<lastBuildDate>Wed, 14 Oct 2026 00:00:00 +0000</lastBuildDate>
		<item>
			<title>一张 80×80mm 标签，为什么会打印出两页？</title>
			<link>https://weour.com/zh/build/why-an-80x80mm-label-printed-two-pages/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/why-an-80x80mm-label-printed-two-pages/</guid>
			<pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[一个下载按钮牵出了业务标识、文件命名、浏览器分页与物理尺寸的同一性问题；真正的修复不是缩小标签，而是让打印文档只剩下那一张标签。]]></description>
			<content:encoded><![CDATA[<p>最初的需求很小：把标签编辑器里的“下载 PNG”，改成一次真正的 Save As。用户点击之后，应该能看到有意义的建议文件名，选择保存位置，然后得到可以直接识别的文件。</p>
<p>可是第一个文件名实现就暴露了问题。它用了 <code class="weour-build-inline-code">SKU-001</code> 这类内部 SKU，而标签上真正让用户识别这批货的，是可见的 <code class="weour-build-inline-code">Batch No: 30267-0914</code>。下载动作本身没有失败，但数据语义失败了：系统选了一个技术上存在、业务上却不对的标识。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>生产路由</dt><dd>/tools/label/</dd></div><div class="weour-build-fact"><dt>文件名业务标识</dt><dd>可见 Batch No（存在时）</dd></div><div class="weour-build-fact"><dt>示例</dt><dd>30267-0914-BQ.png · 30267-0914-BQ.pdf</dd></div><div class="weour-build-fact"><dt>物理尺寸</dt><dd>不缩放，80×80mm 仍是 80×80mm</dd></div><div class="weour-build-fact"><dt>分页验证</dt><dd>6 种尺寸在 Chromium 中均为 1 页</dd></div></dl>
<h2 id="section">文件名是数据模型的一部分</h2>
<p>正确做法不是为下载再造一套“看起来对”的命名逻辑，而是让文件名和画面上的 <code class="weour-build-inline-code">Batch No</code> 使用同一个 canonical 数据源。屏幕上显示 <code class="weour-build-inline-code">30267-0914</code>，PNG 就建议保存为 <code class="weour-build-inline-code">30267-0914-BQ.png</code>，PDF 就是 <code class="weour-build-inline-code">30267-0914-BQ.pdf</code>。如果两者不同，那么至少有一处并没有在表达用户眼前的那批货。</p>
<p>这个原则也适用于 PDF，只是浏览器的接口不一样。macOS 上从浏览器进入 Print → Save as PDF 时，建议的 PDF 名称来自打印文档的标题。所以打印路径会在发起打印前，短暂把 <code class="weour-build-inline-code">document.title</code> 替换成 Batch No 派生的名称；打印结束、取消或窗口恢复后，再可靠地还原正常页面标题。这不是 SEO 标题的修改，而是一个有明确进入和退出边界的打印会话状态。</p>
<h2 id="section">刚好一页，反而多了一页</h2>
<p>文件名闭环之后，更有意思的故障出现了：一张 80×80mm 的标签，打印预览显示两页，第二页是空白的。</p>
<p>直觉上，如果纸张和标签都是 80×80mm，它就应该刚好放下。但浏览器排版并不是把两个整数毫米直接比较。CSS 中的毫米会转换成 CSS 像素，打印管线又会把它映射到设备和分页单位。在页面边界上，子像素舍入、默认行盒或 body 的微小参与，都可能让一个“数值上刚好”的底边落入下一个 pagination fragment。</p>
<p>原实现里，打印 sheet 虽然设为与页面完全同尺寸，但它仍留在普通文档流的分页计算中。只隐藏 <code class="weour-build-inline-code">#root</code> 也不够严格：它假设除了这个节点之外，body 里没有任何会占据打印布局的元素。这个假设在精确尺寸分页中太宽松了。</p>
<h2 id="section">修复分页，不是修改标签尺寸</h2>
<p>真正的修复是把那张 print sheet 从普通分页流里隔离出来。打印样式严格移除所有非打印的 body 子元素，清零 <code class="weour-build-inline-code">html</code> 和 <code class="weour-build-inline-code">body</code> 的 margin、padding 以及可能参与高度计算的行盒，再把唯一的 sheet 绝对定位到页面原点。同时保留根据当前标签动态生成的精确规则：</p>
<pre class="weour-build-code"><code class="language-css">@page { size: &lt;width-mm&gt; &lt;height-mm&gt;; margin: 0; }</code></pre>
<p>这里刻意没有用 99% 缩放，没有从宽高里各减 1mm，也没有把它塞进 A4 再选“适应页面”。这些做法可能让空白页消失，但也会改变最重要的输出合同。80×80mm 必须保持物理上的 80×80mm。</p>
<p>编辑器和导出预览可以放大到 150% 以方便操作，但那只是展示层。canonical geometry 和输出像素没有变：100×100mm 仍导出 1000×1000px，70×20mm 仍是 700×200px。屏幕上看得更大，不等于打印物理尺寸变大。</p>
<h2 id="section">怎样证明是分页修复，而不是一次巧合</h2>
<p>验证不能只回看 80×80mm，因为只对一个尺寸特殊处理，也能制造出“已修复”的假象。Chromium 分页验证覆盖了 70×20、70×40、80×60、80×80、100×100mm，以及自定义 93×57mm；六种情况都只生成 1 页。这个自动化环境无法直接读取 macOS 原生打印预览里的页数计数器，所以证据边界也应该说清楚：证明的是 Chromium 打印文档在这个尺寸矩阵中的单页结果，不是自动读取了 macOS 原生 UI。</p>
<p>最后一步不在浏览器里。打印 hotfix 在 Production 验证后，那个已验证的分支内容被对账回 GitHub <code class="weour-build-inline-code">main</code>，最终 source 和 Production 的 tree 一致。这个结尾很重要：否则线上已经解决的问题，下一次从不完整源码发布时还会回来。</p>
<blockquote class="weour-build-quote">“能下载”不是完成，“能打印”也不是完成。数据来源、文件命名、物理尺寸、分页行为和生产源码都一致，才算完成。</blockquote>]]></content:encoded>
			<category>工程</category>
			<category>数据建模</category>
			<category>公开构建</category>
		</item>
		<item>
			<title>我们决定不再开发另一套 ERP</title>
			<link>https://weour.com/zh/build/we-stopped-building-another-erp/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/we-stopped-building-another-erp/</guid>
			<pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[WEOUR OS 不再试图成为一套完整的跨境 ERP，而是转向 ERP 与各电商平台之上的经营智能与 AI Agent 层：把不同系统的数据变成可信、可解释、可行动的经营决策。]]></description>
			<content:encoded><![CDATA[<p>九月份，我们在这里写过一篇《我们没有再买一套 ERP，而是在构建一套 Commerce OS》。这一篇记录的是之后发生的事：2026-10-03，我们决定不再去构建那套系统里 ERP 本来就做得很好的部分。</p>
<p>这是一次架构和产品战略的调整，不是放弃 WEOUR OS。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>决策日期</dt><dd>2026-10-03</dd></div><div class="weour-build-fact"><dt>停止什么</dt><dd>重复建设成熟 ERP 已经提供的订单、库存、采购、仓储、财务等基础功能</dd></div><div class="weour-build-fact"><dt>系统记录（system of record）</dt><dd>ERP 与各平台系统，在它们本来就拥有数据的地方继续担任记录源</dd></div><div class="weour-build-fact"><dt>WEOUR OS 转向</dt><dd>多平台经营数据 · 可信经营分析 · 智能决策 · Control Tower · AI Agent</dd></div><div class="weour-build-fact"><dt>本文性质</dt><dd>一次战略决策的记录，不是版本发布公告</dd></div></dl>
<h2 id="section">我们从哪里出发</h2>
<p>WEOUR OS 一开始的目标很宽：做一套真正理解我们业务的系统——跨多个平台的店铺、商品、listing、仓库和库存，按业务实际的样子建模。这个 Build 日志里更早的几篇记录了这部分工作：多个店铺共享一套商品目录但不共享库存、真实的仓库库位、库存 Ledger，以及&quot;未知库存绝不能悄悄变成 0&quot;这条规则。</p>
<p>这些工作没有白做。它让我们看清了真正的难点在哪里，也让我们比任何规划文档都更清楚地看到：一套完整的 Commerce OS 里，有相当大的部分并不是&quot;新的难题&quot;。</p>
<h2 id="section">发生了什么变化</h2>
<p>真正动手做之后，一个规律变得很明显：订单、库存流转、采购、仓储、财务，都是体量大、要求严谨、而且早已被充分理解的领域。领星等成熟 ERP 已经覆盖了这些领域的大量成熟基础能力，并积累了大量实际业务中的边界情况处理。</p>
<p>我们每花一周重写这些功能，就是在花一周追赶一套早已存在的软件。最好的结果，是得到一份我们本来就能直接用的东西的副本；最坏的结果，是同一笔订单、同一个库存数字出现第二个&quot;真相来源&quot;——而消除这种含糊，恰恰是 WEOUR OS 存在的理由。</p>
<p>所以我们停了下来。今后除非有书面记录说明现有系统无法满足某个具体需求，WEOUR OS 不再重复开发 ERP 的基础功能。</p>
<h2 id="section">为什么这样取舍</h2>
<p>工程时间是我们最稀缺的资源。真正该问的不是&quot;我们能做什么&quot;，而是&quot;在哪里投入，才能做出别人给不了的东西&quot;。</p>
<p>ERP 很擅长记录它自己范围内发生了什么；电商平台很擅长报告它自己平台上发生了什么。但它们都不是为回答多平台经营每天真正要面对的问题而设计的：此刻哪些数字是可信的？各平台之间哪里对不上？哪个异常应该最先处理？接下来该怎么做？</p>
<p>这些问题都落在系统与系统之间。这正是 WEOUR OS 能做出真正差异的地方。</p>
<h2 id="weour-os">WEOUR OS 正在变成什么</h2>
<p>系统的形态，从&quot;又一套一体化电商系统&quot;，变成架在我们已经在用的系统之上的一层：</p>
<pre class="weour-build-code"><code>ERP + 各电商平台  →  WEOUR OS 经营智能与控制层  →  人和 Agent 的决策</code></pre>
<p>具体来说，这定义了接下来建设的五个方向，而不是五项已经全部完成的能力：</p>
<ul class="weour-build-list"><li><strong>多平台经营数据</strong>：把 ERP 和各个平台的数据汇成一个统一、规范化的经营视图，同时保留原始记录和它的来源。</li><li><strong>可信经营分析</strong>：每个数字都带着新鲜度和完整度。不完整的数据就标为不完整，未知永远不会被显示成 0。</li><li><strong>智能决策</strong>：利润、异常和预测，以&quot;需要做出的决定&quot;的形式呈现，而不是又多几张要看的报表。</li><li><strong>Control Tower</strong>：在一个地方看清跨平台的经营状态，以及什么事情最需要先处理。</li><li><strong>AI Agent</strong>：Agent 基于经过验证的数据工作，在明确的权限与审计边界之内行动；高风险操作和关键经营决策保留人工授权。</li></ul>
<p>在合适的地方，ERP 和平台系统继续作为记录源（system of record）。与 ERP 的连接，当前首先采用只读方式：WEOUR OS 读取、解释和交叉核对数据，不回写拥有这条记录的系统。这只是接入的起点，不代表完整的集成已经完成。</p>
<h2 id="section">这不意味着什么</h2>
<p>这不意味着 WEOUR OS 推倒重来。商品与库存身份、库存语义、数据 provenance 和发布安全方面的工作都会延续下去——正是这些基础，让跨平台数据有可能被信任。</p>
<p>这也不意味着这一层已经建成。文中提到的智能决策、Control Tower 和 Agent 能力，很大一部分还是方向，而不是已经可以在生产上使用的功能。它们上线时，会按照和这里其他记录一样的证据标准写进这个日志。</p>
<blockquote class="weour-build-quote">WEOUR OS 的价值不在于拥有最多的功能，而在于把不同系统的数据，变成可信、可解释、可行动的经营决策。</blockquote>
<aside class="weour-build-callout"><p>少做别人已经做好的事，才能多做别人做不了的事。</p></aside>
<p>WEOUR OS 不需要成为 ERP 的替代品。它需要成为 ERP、平台和经营者之间那一层可信的智能基础设施。</p>]]></content:encoded>
			<category>WEOUR OS</category>
			<category>架构</category>
			<category>数据质量</category>
			<category>公开构建</category>
		</item>
		<item>
			<title>把 Yandex 订单接入统一经营分析：一次以证据为准的构建记录</title>
			<link>https://weour.com/zh/build/weour-os-program-milestone-yandex-ozon-yandex/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/weour-os-program-milestone-yandex-ozon-yandex/</guid>
			<pubDate>Fri, 02 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[这次事件把两家 Yandex 店铺的订单事实接入到与 Ozon 共用的经营分析口径，并把调度器边界明确为「代码已部署、但未启用」。本文只记录可反查的证据，以及它不意味着什么。]]></description>
			<content:encoded><![CDATA[<h2 id="section">为什么改</h2>
<p>在此之前，多平台接入与目录证据、Yandex 数据摄取、以及统一的经营分析口径是分散推进的。这次事件的目标是把它们接到同一条链路上：让 Ozon 与 Yandex 共用同一份订单事实，而不是各自维护一套口径。</p>
<p>另一个动机是边界要写清楚。调度器相关的代码已经进入部署产物，但「代码已部署」和「已按计划在生产运行」是两件事。这次记录刻意把后者留白，避免把未启用的能力写成已完成的运行状态。</p>
<h2 id="section">改了什么</h2>
<p>改动集中在渠道接入、经营分析与 Yandex 平台适配这几块：控制台的渠道与洞察页面、分析库、渠道库、Yandex 平台适配层，以及服务端逻辑。</p>
<p>在数据侧，目录新增了外键约束，用来保护观测证据，同时不把任务行本身锁死——这条约束有对应的测试断言。渠道侧则做了一轮 RC 修正，覆盖店铺入驻相关的验收、时区、共享密钥与 PII 允许列表。</p>
<p>文档侧记录了两条部署：一条是 Yandex 调度器 V1 的代码部署与 P5 生产观测，另一条是 Yandex 分析主链路集成。两条部署都记录在概览文档中。</p>
<p>具体版本、提交、部署产物与摘要、测试数量、运行时状态，以及语料计数，见上方 facts。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>版本</dt><dd>20260926-yandex-sched-add2ba4 / 20260926-yaxan-1771e0c</dd></div><div class="weour-build-fact"><dt>主发布提交</dt><dd>025b05e54469a5e8ab636915322cbe478f73131e</dd></div><div class="weour-build-fact"><dt>生产 provenance 提交</dt><dd>025b05e54469a5e8ab636915322cbe478f73131e</dd></div><div class="weour-build-fact"><dt>部署产物与摘要</dt><dd>20260926-yandex-sched-add2ba4（记录于 docs/00-overview/CURRENT_STATE.md）；20260926-yaxan-1771e0c（记录于 docs/00-overview/CURRENT_STATE.md）</dd></div><div class="weour-build-fact"><dt>测试数量</dt><dd>见提交 df4bb40802d4f0ee6af67d3bf7e09d74d39e7b5e 的目录外键断言</dd></div><div class="weour-build-fact"><dt>运行时状态</dt><dd>调度器代码已部署、未启用</dd></div><div class="weour-build-fact"><dt>截至当前仓库语料</dt><dd>8 篇笔记 / 24 个 variant（其中 1 篇仍未发布）</dd></div></dl>
<h2 id="section">怎么验证的</h2>
<p>验证分三层。第一层是提交本身：主发布提交与生产 provenance 提交指向同一个 SHA，说明记录与部署产物对得上。</p>
<p>第二层是测试：目录外键的改动带有断言，验证它保护观测证据的同时不会把任务行困住。</p>
<p>第三层是文档：两条部署各自有 release id 与记录日期，并落在概览文档里；架构文档单独描述了 Yandex 订单调度器 V1 的设计。</p>
<h2 id="section">它不意味着什么</h2>
<p>它不意味着调度器已经在生产按计划运行。代码进入部署产物，与调度器被启用、被排程、被观测到稳定运行，是不同层级的结论。</p>
<p>它也不意味着多平台接入已经收尾。这次事件把 Ozon 与 Yandex 的订单事实接到同一口径上，但接入范围、后续平台与运营流程的覆盖，不在这次证据之内。</p>
<p>它同样不意味着语料已经完整。截至当前仓库中仍有未发布的笔记，语料计数是写作时捕获的仓库状态，之后仓库增长不会改变这个数字的含义。</p>]]></content:encoded>
			<category>公开构建</category>
		</item>
		<item>
			<title>我们暂停了新功能，先给 WEOUR OS 补了一套生产安全网</title>
			<link>https://weour.com/zh/build/weour-os-building-the-safety-net/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/weour-os-building-the-safety-net/</guid>
			<pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[WEOUR OS 这一轮没有加功能，而是补了四件事：release 保留、备份闭环、调度器的时间预算，以及一条 canonical 发布流水线。这篇记录说明改了什么、当时的证据是什么，以及它不意味着什么。]]></description>
			<content:encoded><![CDATA[<p>这不是一次功能发布。它记录的是 WEOUR OS 为什么先停下新功能，去补一层生产安全网：备份、调度、发布、回滚。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>本轮范围</dt><dd>生产基础设施（这轮建设的记录）：备份 · 调度 · 发布 · 回滚</dd></div><div class="weour-build-fact"><dt>备份节奏</dt><dd>每 6 小时一次（launchd 定时）</dd></div><div class="weour-build-fact"><dt>备份成功契约</dt><dd>四道门禁：stream / decrypt / structure / durable</dd></div><div class="weour-build-fact"><dt>备份健康分级</dt><dd>HEALTHY ≤ 7 小时 · DEGRADED ≤ 12 小时 · STALE &gt; 12 小时（FAILED 表示单次尝试失败，不是档位）</dd></div><div class="weour-build-fact"><dt>调度 stagger 预算</dt><dd>默认 30 秒（下限 0 / 上限 60），与 tick 间隔解耦</dd></div><div class="weour-build-fact"><dt>调度 tick P50（4 店）</dt><dd>119.5 s → 31.9 s</dd></div><div class="weour-build-fact"><dt>调度 tick P95（4 店）</dt><dd>369.9 s → 55.3 s</dd></div><div class="weour-build-fact"><dt>调度占空比</dt><dd>83.7% → 17.1%</dd></div><div class="weour-build-fact"><dt>调度验收样本</dt><dd>11 个 tick（4 店）</dd></div><div class="weour-build-fact"><dt>首次 canonical 部署 Trial</dt><dd>2026-09-23 失败，由流水线执行了回滚</dd></div><div class="weour-build-fact"><dt>该轮生产验证</dt><dd>release 20260923-rpg13-7f685ca（CANONICAL，validation / smoke PASS）</dd></div><div class="weour-build-fact"><dt>发布治理用例</dt><dd>123 → 157</dd></div><div class="weour-build-fact"><dt>该轮观测点保留状态</dt><dd>release 19 · 删除候选 1 · 来源未知 6 · 受保护 5（retention dry-run）</dd></div><div class="weour-build-fact"><dt>调度修正提交</dt><dd>Anxin360/weour-os@e92b059</dd></div><div class="weour-build-fact"><dt>readiness / iowait 修正提交</dt><dd>Anxin360/weour-os@d4cbe01</dd></div><div class="weour-build-fact"><dt>Foundation V1.1 trial 分支顶端</dt><dd>Anxin360/weour-os@b8cc817</dd></div><div class="weour-build-fact"><dt>治理 V1.3 提交</dt><dd>Anxin360/weour-os@7f685ca</dd></div></dl>
<h2 id="section">为什么先停下新功能</h2>
<p>WEOUR OS 已经在生产里跑真实数据：真实的店铺、真实的调度循环、真实的发布记录。功能越多，出错的代价越高；但在这之前，有几件事并没有真正闭环：</p>
<ul class="weour-build-list"><li>不可变 release 只会不断累积，磁盘不会自己变干净；</li><li>备份是有的，但&quot;这次备份是否真的成功&quot;没有自动核对，也没有一条可以被自动消费的健康信号；</li><li>调度器看起来很忙，但忙的主要不是业务工作。</li></ul>
<p>这些都不是功能问题，而是运行方式的问题。先把它们修好，后面每加一个功能才会更便宜。</p>
<h2 id="section">改了什么</h2>
<h3 id="1-release-release">1. Release 保留：不可变 release 需要受治理的清理</h3>
<p>发布以不可变的整份构建为单位：切换用符号链接指向新的 release 目录，回滚就是切回旧目录。这个设计让回滚变得便宜，代价是磁盘会一直增长——审计时，release 目录占了该主机已用磁盘的约 74%（44.12 GB / 59.44 GB 已用），而同一块磁盘还要留出构建所需的余量。</p>
<p>于是清理被写成了分类优先级，而不是&quot;删最旧的&quot;：</p>
<pre class="weour-build-code"><code>CURRENT &gt; IMMEDIATE_ROLLBACK &gt; DIAGNOSTIC &gt; FAILED_TRIAL &gt; VALIDATED_ANCHOR
      &gt; MIGRATION_BOUNDARY &gt; MANUAL_PROTECT &gt; UNKNOWN_PROVENANCE &gt; ROUTINE</code></pre>
<p>只有 <code class="weour-build-inline-code">ROUTINE</code> 才进入日常配额清理，其余类别都不会被顺手删掉。其中一条规则尤其重要：一次失败 Trial 的 release，不会因为它的 commit 后来进了 <code class="weour-build-inline-code">main</code> 就被降级成 <code class="weour-build-inline-code">ROUTINE</code>。判定只依据生命周期证据，不依据目录名、时间戳或聊天记录。</p>
<h3 id="2">2. 备份：从&quot;有备份&quot;到&quot;可核对的备份&quot;</h3>
<p>备份的形态没有推倒重来：备份主机主动拉取，生产只提供一条受限的强制命令流，用于解密的私钥从不进入生产主机。改动发生在拉取这一侧：</p>
<ul class="weour-build-list"><li><strong>节奏与 RPO</strong>：每 6 小时一次（launchd 定时），目标 RPO 为 6 小时。节奏是可解释的，不依赖漂移的间隔计时器。</li><li><strong>四道门禁</strong>：一次运行只有在 stream、decrypt、structure、durable 四道门禁全部通过后才算成功。<code class="weour-build-inline-code">pg_dump</code> 退出码为 0 本身从来不是成功。</li><li><strong>失败即失败</strong>：任何一道门禁失败，运行就是 <code class="weour-build-inline-code">FAILED</code>，<code class="weour-build-inline-code">last-success</code> 不被更新，清理也不会发生。</li><li><strong>健康分级</strong>：<code class="weour-build-inline-code">HEALTHY</code>（≤ 7 小时）/ <code class="weour-build-inline-code">DEGRADED</code>（≤ 12 小时）/ <code class="weour-build-inline-code">STALE</code>（&gt; 12 小时）由&quot;距上次成功的小时数&quot;推导，并可以作为退出码被监控直接消费；<code class="weour-build-inline-code">FAILED</code> 描述的是单次尝试，不是档位。</li><li><strong>失败注入测试</strong>：覆盖远端失败、过小产物、解密失败、结构不合法、叠加运行、陈旧锁、保留策略与损坏的健康状态。</li></ul>
<p>最后一步是把健康信号接回生产：备份主机在四道门禁通过之后，通过一个受限身份把快照推给生产，该身份只能写一个固定文件，既不能切换 release，也不能重启服务，更不能读任何密钥。</p>
<h3 id="3-tick-stagger">3. 调度器：把 tick 和 stagger 的语义分开</h3>
<p>诊断先给出了一个不舒服的结论：在 4 家店的运行区间里，tick 墙钟时间的 96.8% 花在确定性的 stagger 睡眠上——2610 秒里有 84 秒在做真实工作。按每个 action 计算，睡眠本身就要 4150 秒/小时，接近墙钟的 115%。</p>
<p>根因是语义混在一起：stagger 窗口被绑定到 tick 间隔（180 秒），而同一个串行 runner 上多个 action 各自的固定睡眠被串行累加。</p>
<p>修正把两件事分开：tick 间隔只用于报告 cadence，不再参与 stagger 计算；stagger 有自己的预算，默认 30 秒，可选 0 / 30 / 60，非法值回落到默认值。部署后的测量结果：</p>
<ul class="weour-build-list"><li>tick P50 119.5 秒 → 31.9 秒，P95 369.9 秒 → 55.3 秒；</li><li>占空比 83.7% → 17.1%；</li><li>计划睡眠与实际睡眠一致，其余时间是可解释的 API 与 DB 工作；</li><li>未改 timer、cadence、店铺开关与并发预算。</li></ul>
<h3 id="4-canonical">4. 一条 canonical 发布流水线</h3>
<p>发布不再是一串手工命令，而是一条有顺序、可复现的流水线：</p>
<pre class="weour-build-code"><code>provenance → preflight → release lock → scheduler quiet window → source
  → build backend → monitored build → immutable release + manifest
  → candidate validation → rollback readiness → atomic switch → restart
  → readiness → functional smoke → state write → history → retention dry-run</code></pre>
<p>其中几处是刻意设计的：构建只在调度器安静窗口内进行；切换是原子的；回滚目标在切换之前和之后各校验一次；<code class="weour-build-inline-code">systemctl restart</code> 的退出码被明确记录为&quot;单元已启动&quot;，而不是&quot;应用已就绪&quot;。</p>
<h3 id="5-trial">5. 第一次 Trial 失败，而且被完整保留</h3>
<p>第一次 canonical 部署 Trial 跑完了自己的成功路径，然后在 smoke 阶段失败，并由流水线执行了自己的 canonical 回滚。</p>
<p>原因不是应用有问题：<code class="weour-build-inline-code">systemctl restart</code> 返回时只代表单元启动了，不代表端口已经绑定。当时的 smoke 是一次立即的 HTTP 检查，没有任何 readiness 等待，于是它观察到了连接被拒绝与网关错误。</p>
<p>处理方式才是重点：没有人绕过流水线去手工修好这一步。流水线按自己的失败契约停下并执行了回滚，该 release 被保留用于诊断，清理阶段一次删除也没有执行。生产继续在切换前的 release 上正常服务。</p>
<p>随后发布的修正把 readiness 变成有界轮询，并让构建监控真实测量 iowait。重新试跑时，此前那个失败模式被新逻辑正确地吸收为瞬时未就绪，而不是一次部署失败。</p>
<h3 id="6">6. 治理：让&quot;谁可以发布&quot;和&quot;发布了什么&quot;都留下证据</h3>
<p>这一轮补的规则是给未来的自己用的：</p>
<ul class="weour-build-list"><li><strong>canonical deploy only</strong>：日常发布只走流水线；例外必须显式标记。</li><li><strong><code class="weour-build-inline-code">deploymentMethod</code></strong>：每条新的生命周期记录都带方法枚举，历史记录追加而不改写；无法判断的历史标记为 <code class="weour-build-inline-code">UNKNOWN_LEGACY</code>，绝不猜成 <code class="weour-build-inline-code">CANONICAL</code>。</li><li><strong>失败 Trial 分类</strong>：<code class="weour-build-inline-code">FAILED_TRIAL</code> 由生命周期证据判定，不占日常清理配额。</li><li><strong>诊断证据优先</strong>：删除前必须先写回 KB 级、不含密钥的诊断记录并校验；写不下去就拒绝删除。</li></ul>
<p>在该轮观测点（生产 release <code class="weour-build-inline-code">20260923-rpg13-7f685ca</code>）上，保留策略以 dry-run 呈现：记录显示 19 个 release，其中 1 个删除候选、6 个来源未知、5 个受保护。清理只在证据齐全（目标已确认不被引用、诊断记录已写回并校验）时才会发生。</p>
<h2 id="section">怎么验证的</h2>
<p>每个环节的验证方式都尽量落在可复查的证据上，而不是&quot;看起来正常&quot;：</p>
<ul class="weour-build-list"><li>备份由失败注入测试与真实四门禁运行验证，健康信号由监控消费；</li><li>调度修正先在隔离环境里用实测 offset 模拟，再在部署后用 11 个真实 tick 前后对比；</li><li>发布流水线的验证方式是让它自己跑完一次真实发布：readiness 通过、functional smoke 六条路由通过、状态由流水线写入，然后才做 retention 的 dry-run；</li><li>治理规则由用例覆盖，从&quot;失败 Trial 不被降级&quot;到&quot;发布器拒绝异常载荷&quot;。</li></ul>
<h2 id="section">它不意味着什么</h2>
<p>它不是一次功能发布，也不意味着基础设施已经完成。</p>
<p>在该轮记录里，备份的局限是明确写下来的：解密密钥只有一份且与密文在同一台机器上；没有第二个异地副本；没有 PITR，RPO 就是 6 小时这个节奏；媒体备份没有保留策略；角色与全局对象不在 dump 里。这些是已知的取舍，不是被忽略的细节。</p>
<p>调度修正只解决&quot;时间花在哪里&quot;，不是并发或性能优化。</p>
<p>治理也没有覆盖全部历史：在该轮观测点上仍然存在来源无法判定的旧 release，它们被保守地保留。</p>
<p>这篇记录属于 2026-09-22 至 2026-09-23 这一轮建设。文中的生产状态都是<strong>在该轮观测点上</strong>的事实，不是此后任何时刻的现状；后续的生产变化不在本文范围内。</p>
<p>最后，它不是对未来版本、时间表或能力的承诺。这里记录的是一次已经发生的基础设施升级，以及支撑它的证据。</p>
<h2 id="section">证据在哪里</h2>
<p>这篇记录里的每条事实性陈述都指向一个可以反查的对象：提交、部署 release 标识与用例数量的对照关系登记在上方的事实表里，也登记在这篇记录的 evidence 条目中。本文的正文只解释含义，不重复罗列标识符。</p>
<aside class="weour-build-callout"><p>安全网的价值不在于它被建起来了，而在于当它真的拦住一次失败时，没有人需要临时决定该怎么办。</p></aside>]]></content:encoded>
			<category>WEOUR OS</category>
			<category>工程</category>
			<category>架构</category>
			<category>公开构建</category>
		</item>
		<item>
			<title>我们把 Build 内容发布变成了一条可验证的流水线</title>
			<link>https://weour.com/zh/build/weour-core-1-3-5-release/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/weour-core-1-3-5-release/</guid>
			<pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[这次事件把 Build 内容的发布路径从人工操作收敛为一条带预览、CI 门禁与内容契约的流水线，并在生产环境完成验证。下面记录它改了什么、证据是什么，以及它不意味着什么。]]></description>
			<content:encoded><![CDATA[<h2 id="section">为什么改</h2>
<p>在此之前，Build 内容的发布依赖人工判断：内容是否合规、结构是否完整、发布后运行时能否正确读取，都缺少统一的检查点。一旦内容格式与运行时预期不一致，问题往往要到发布之后才暴露。</p>
<p>这次改动的目标不是增加功能，而是把「内容能不能发」变成一个可以在发布前回答的问题：内容先经过契约校验，再经过 CI 门禁，最后才进入发布流程。</p>
<h2 id="section">改了什么</h2>
<p>改动集中在几个相互衔接的部分：工作流、内容目录、文档、运行时内容模块、脚本与测试。</p>
<p>内容侧引入了 schema 与 topics 定义，把笔记、变体与主题的关系显式化；脚本侧负责按契约校验内容；测试侧同时覆盖 PHP 与 Node 两条检查路径；工作流侧把预览与门禁串起来，使发布前必须通过检查。</p>
<p>发布本身以一次生产部署收尾，部署记录写入仓库文档，并打上对应的验证标签。具体版本、提交、产物摘要、测试数量与语料计数见上方 facts。</p>
<h2 id="section">怎么验证的</h2>
<p>验证分两层。第一层是自动化检查：PHP 与 Node 两套检查在 CI 中运行，并设有若干门禁，只有全部通过才允许继续。第二层是生产验证：产物部署到生产环境后，运行时报告了自身的状态与所加载的内容规模。</p>
<p>需要区分两个时间点的语料口径：</p>
<ul class="weour-build-list"><li>1.3.5 部署时生产语料 | 5 篇笔记 / 15 个 variant / 13 个 topics</li><li>截至当前仓库语料 | 6 篇笔记 / 18 个 variant（其中 1 篇仍未发布）</li></ul>
<p>前者是部署当时的运行时快照，后者是仓库当前的内容状态。两者不一致是正常的：仓库里可以存在尚未发布的草稿。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>版本</dt><dd>1.3.5</dd></div><div class="weour-build-fact"><dt>主发布提交</dt><dd>57bf785a0bcd6e886397de776cbe7bd7156fd64c</dd></div><div class="weour-build-fact"><dt>生产 provenance 提交</dt><dd>4b4ec32a3aac70a8222ca7255f0db38fb6683200</dd></div><div class="weour-build-fact"><dt>部署产物</dt><dd>weour-core-1.3.5.zip</dd></div><div class="weour-build-fact"><dt>产物 SHA-256</dt><dd>f660c0d9b48cbe774a3ba10d380eedfbf9a4fee5cf1a49c97c024ee7c1ee28ab</dd></div><div class="weour-build-fact"><dt>插件清单 SHA-256</dt><dd>952f570124787702fc8c71ce6ebd5410dca3099730b212d805189f3764d2a703</dd></div><div class="weour-build-fact"><dt>部署环境</dt><dd>production</dd></div><div class="weour-build-fact"><dt>部署状态</dt><dd>deployed</dd></div><div class="weour-build-fact"><dt>运行时状态</dt><dd>ready</dd></div><div class="weour-build-fact"><dt>运行时版本</dt><dd>1.3.5</dd></div><div class="weour-build-fact"><dt>运行时模块数</dt><dd>12</dd></div><div class="weour-build-fact"><dt>测试数量</dt><dd>PHP 1100 项检查 / Node 97 项检查</dd></div><div class="weour-build-fact"><dt>门禁数量</dt><dd>5</dd></div><div class="weour-build-fact"><dt>1.3.5 部署时生产语料</dt><dd>5 篇笔记 / 15 个 variant / 13 个 topics</dd></div><div class="weour-build-fact"><dt>截至当前仓库语料</dt><dd>6 篇笔记 / 18 个 variant（其中 1 篇仍未发布）</dd></div></dl>
<h2 id="section">它不意味着什么</h2>
<p>它不意味着内容发布从此不需要人工判断。流水线检查的是契约与结构，不检查内容本身是否恰当、是否值得发布。</p>
<p>它不意味着仓库里的内容都已经上线。仓库当前的内容数量多于部署时的运行时数量，差额来自尚未发布的草稿。</p>
<p>它也不意味着这套流程已经覆盖未来所有内容形态。这次验证针对的是当前的内容契约与运行时读取路径，契约之外的形态不在本次验证范围内。</p>
<p>最后，它不构成对后续版本、时间线或能力的承诺。这里记录的是一次已经发生的发布与验证，以及支撑它的证据。</p>]]></content:encoded>
			<category>公开构建</category>
		</item>
		<item>
			<title>台账 #1——WEOUR OS 开始真正知道自己有什么</title>
			<link>https://weour.com/zh/build/ledger-1-when-weour-os-started-knowing-what-we-had/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/ledger-1-when-weour-os-started-knowing-what-we-had/</guid>
			<pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[第一次真实实盘进入中央库存台账，以及它如何改变了"知道、观测、决定"这三件事之间的区别。]]></description>
			<content:encoded><![CDATA[<p>有好几个月，对&quot;我们到底有什么&quot;这个问题的诚实回答，是一组来自各处的数字：格式不一致，时间对不上。我们知道各个渠道上报了什么，但我们没有一本台账。</p>
<p>这篇记录的就是改变发生的那一刻。台账 #1 是 WEOUR OS 中第一条把真实实盘结果变成可记录库存的条目，而且它可以被事后审计。</p>
<h2 id="section">在台账出现之前，我们有什么</h2>
<p>那时两个真实仓库已经建模完成，它们的在用库位也已经被替换为经验证的物理库位——也就是上一篇提到的 1,306 个库位。平台和渠道在上报它们能看到的内容。但这些都不等于&quot;WEOUR 知道&quot;。</p>
<p>观测库存是证据：它是某个来源在某个时间点、从它自己的角度告诉我们的内容。中央台账不同：它是 WEOUR 决定自己持有什么的地方，而它只有能够追溯到物理事实，才配得上这个地位。</p>
<h2 id="section">第一次实盘真正改变了什么</h2>
<p>实盘不是同步。它是有人站在某个库位前，确认那里到底有什么。当这个结果被写入台账时，三件事第一次成为可能：</p>
<ul class="weour-build-list"><li>一个数字之所以存在，是因为有人在某个范围、某个时间、某个库位把它数了出来；</li><li>系统此前显示的数字与实盘结果之间的差异，变成了一笔可记录的变动，而不是一个谜；</li><li>之后的观测可以与一个&quot;决定&quot;进行比较，而不是只能与其他观测互相比较。</li></ul>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>范围内的真实仓库</dt><dd>2</dd></div><div class="weour-build-fact"><dt>在用库位</dt><dd>1,306</dd></div><div class="weour-build-fact"><dt>第一条记录</dt><dd>来自实盘的期初余额</dd></div><div class="weour-build-fact"><dt>台账变动</dt><dd><code class="weour-build-inline-code">OPENING_BALANCE +55</code></dd></div><div class="weour-build-fact"><dt>自动库存写入</dt><dd>无</dd></div><div class="weour-build-fact"><dt>审批要求</dt><dd>按变动类别显式审批</dd></div></dl>
<p><code class="weour-build-inline-code">OPENING_BALANCE +55</code> 是一笔记账变动，不是需求信号。它记录的是：在被统计的这个范围内，实盘结果与台账首次就一个数字达成一致。我们公开它，是因为几个月之后，一个中央库存系统正是必须能够解释这类变动——而且因为编造一个看起来更漂亮的数字，会让整件事失去意义。</p>
<h2 id="section">让它得以成立的规则</h2>
<p>这次实盘之所以能被录入，是因为系统里已经存在四条规则：</p>
<ol class="weour-build-list"><li><code class="weour-build-inline-code">NOT_INITIALIZED ≠ 0</code>。从未被统计过的范围不是空的，而是未知的；在有人去数之前，它一直保持未知。</li><li><code class="weour-build-inline-code">NOT_REPORTED ≠ ZERO</code>。一个上报失败的渠道并没有告诉我们任何事，而失败的上报永远不会变成缺货。</li><li><code class="weour-build-inline-code">Observed Inventory ≠ Central Inventory</code>。渠道继续上报它们看到的内容；台账记录的是 WEOUR 在核对之后决定下来的事实。</li><li><code class="weour-build-inline-code">Candidate ≠ Approved</code>。被提出的变动只是提议，只有经过显式审批才会成为库存，绝不会由自动化填补空白来完成。</li></ol>
<aside class="weour-build-callout"><p>第一条台账记录的价值不在于它的数字，而在于围绕它的一切——范围、库位、时间与审批——都能在之后被重建出来。</p></aside>
<h2 id="section">这不意味着什么</h2>
<p>它不意味着库存问题&quot;解决了&quot;。两个仓库被数清楚了，就是两个仓库被数清楚了。台账的质量取决于下一次实盘，而下一个范围，正是那个还没有被初始化的范围。</p>
<p>它也不意味着这些数字是最终结论。实盘是关于某个瞬间的陈述。系统必须把这个瞬间一起保留下来，这样之后出现的差异，才是两个已知状态之间的差异，而不是一场关于谁对谁错的争论。</p>
<h2 id="section">为什么这是第一篇台账记录</h2>
<p>我们公开台账系列，是因为它是整套 Commerce OS 里最不显眼、却最承重的部分。如果系统无法把&quot;它知道的&quot;&quot;它观测到的&quot;&quot;它决定的&quot;这三件事分开说清楚，那么它上面所有的数字都只是装饰。</p>
<p>台账 #1 就是这三件事第一次不再是同一件事的那一条记录。</p>]]></content:encoded>
			<category>库存</category>
			<category>中央库存</category>
			<category>仓储</category>
			<category>WEOUR OS</category>
			<category>公开构建</category>
		</item>
		<item>
			<title>未知库存不等于 0</title>
			<link>https://weour.com/zh/build/unknown-inventory-is-not-zero/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/unknown-inventory-is-not-zero/</guid>
			<pubDate>Sun, 20 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[WEOUR OS 的核心原则：未初始化与未上报的库存不等于 0，而观测库存永远不是中央库存。]]></description>
			<content:encoded><![CDATA[<p>这条规则对 WEOUR OS 的影响比其他任何规则都大，所以它值得单独一篇。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>NOT_INITIALIZED</dt><dd>系统从未被告知这里有什么</dd></div><div class="weour-build-fact"><dt>NOT_REPORTED</dt><dd>一个通常会上报的来源没有上报</dd></div><div class="weour-build-fact"><dt>ZERO</dt><dd>我们知道，来自某个确定来源、在某个确定时间，数量为零</dd></div><div class="weour-build-fact"><dt>观测库存</dt><dd>渠道或仓库上报的内容，作为证据被保存</dd></div><div class="weour-build-fact"><dt>中央库存</dt><dd>WEOUR 在核对之后，决定下来的那个事实</dd></div></dl>
<p>这是五件不同的事情。把它们压进同一列的&quot;简化系统&quot;，并没有更简单——它只是无法告诉你，一个数字为什么是这个数字。</p>
<h2 id="0">为什么&quot;未知&quot;不能变成&quot;0&quot;</h2>
<p>0 是一个断言。它表示某个可计数的数量确实为零。当系统没有任何信息时，写入 0 会把一个未知事实伪装成已知事实，而后续每一个决定都会继承这个偏差：</p>
<ul class="weour-build-list"><li>商城把某个商品标记为不可售，不是因为货没了，而是因为一次上报失败了。</li><li>补货规则因为一个并不存在的缺货被触发。</li><li>一份核对报表显示出的差异，其实是缺失的数据源，团队花一整天去追一个不存在的问题。</li></ul>
<p>这些现象看起来都不像 bug，它们看起来就像正常数据。</p>
<h2 id="section">我们如何表示它</h2>
<p>未设定的库存状态以显式状态存储，而不是以数字存储。状态是模型的一部分：</p>
<ul class="weour-build-list"><li><code class="weour-build-inline-code">NOT_INITIALIZED</code> —— 记录存在，但从未有来源提供过值。</li><li><code class="weour-build-inline-code">NOT_REPORTED</code> —— 某个来源本应上报，但还没有上报，或者上报失败。</li><li>一个数值 —— 一个真实的断言，永远带着它的来源和观测时间。</li></ul>
<p>展示层可以友好，但存储层不允许猜测。</p>
<h2 id="section">观测库存不是中央库存</h2>
<p>这条原则的另一半是：观测值永远不会因为&quot;顺手&quot;而变成台账。</p>
<p>每个平台、每个仓库都会上报一些内容，而这些上报回答的是不同时间点上的不同问题。观测库存是我们被告知的内容；中央库存是 WEOUR 在核对了观测值、实盘结果和已知缺口之后，决定下来的事实。</p>
<aside class="weour-build-callout"><p>系统可以向你展示一次观测，但它不会假装这次观测就是一个决定。</p></aside>
<p>把这个边界保持明确，正是多店铺销售能够诚实运作的原因：五个店铺都可以上报自己看到的结果，而不会有一个店铺在暗中改写 WEOUR 实际拥有什么。</p>
<h2 id="section">它的代价，以及我们为什么愿意承担</h2>
<p>这条原则并不方便。它意味着更多的状态、更显式的处理，以及更少可以用默认值悄悄填坑的地方。它意味着仪表盘必须显示&quot;未知&quot;，而不是显示一个让人安心的 0。</p>
<p>我们承担这个代价，因为另一种选择是一套&quot;自信地出错&quot;的操作系统。一套 Commerce OS 的可信度，来自它知道自己不知道什么——并且把这件事写在数据库里，而不只是写在界面上。</p>
<blockquote class="weour-build-quote"><code class="weour-build-inline-code">NOT_INITIALIZED ≠ 0</code>。<code class="weour-build-inline-code">NOT_REPORTED ≠ ZERO</code>。观测库存不是中央库存。</blockquote>]]></content:encoded>
			<category>库存</category>
			<category>数据建模</category>
			<category>WEOUR OS</category>
			<category>工程</category>
			<category>公开构建</category>
		</item>
		<item>
			<title>1,306 个真实仓库库位</title>
			<link>https://weour.com/zh/build/1306-real-warehouse-locations/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/1306-real-warehouse-locations/</guid>
			<pubDate>Tue, 15 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[占位库位如何被苏州与深圳两地的真实仓库主数据所取代。]]></description>
			<content:encoded><![CDATA[<p>有一段时间，我们的仓库数据看起来很健康：每个商品都有库位，每个库位都存在于系统中。数字彼此一致，也没有任何环节报错。</p>
<p>那些库位是占位的。</p>
<h2 id="section">&quot;仅仅一致&quot;的数字有什么问题</h2>
<p>占位库位很容易生成，也很容易被信任。它们符合字段结构，能通过校验，还能生成图表。问题只有在需要有人真的去把货拿起来时才会暴露，或者当两个不同的人用两种方式描述同一个货架、而系统坚持认为它们是两个不同位置时才会暴露。</p>
<p>这就是陷阱所在：占位数据看起来并不像缺失数据，它看起来就是数据。</p>
<h2 id="section">用真实库位替换占位集合</h2>
<p>我们按照仓库实际运作的方式重建了仓库主数据，然后把它作为权威库位集合加载进系统。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>经验证的库位</dt><dd>1,306</dd></div><div class="weour-build-fact"><dt>建模的仓库</dt><dd>苏州、深圳</dd></div><div class="weour-build-fact"><dt>方法</dt><dd>先有物理位置，再有系统记录</dd></div><div class="weour-build-fact"><dt>占位库位</dt><dd>直接废弃，而不是修补</dd></div><div class="weour-build-fact"><dt>未知映射</dt><dd>在确认真实库位之前保持未知</dd></div></dl>
<p>数量不是重点。重点是这 1,306 个库位中的每一个，都对应着一个真人可以走过去的位置；并且两个仓库遵循同一套规则建模，而不是各自延续本地的习惯。</p>
<h2 id="section">这次变更暴露了什么</h2>
<p>用真实数据替换占位数据，会把所有悄悄依赖过假数据的地方暴露出来：</p>
<ul class="weour-build-list"><li>只有占位库位、没有真实归属的商品，需要一个明确的决定，而不是一个默认值。</li><li>两个仓库中含义不同的历史编码，需要被解析清楚，而不是被合并。</li><li>任何假定&quot;每个商品总在某处&quot;的流程，都必须接受&quot;我们还没有确认&quot;是一个合法状态。</li></ul>
<p>最后这一点对系统的改动最大，它也是这个日志下一篇的主题。</p>
<h2 id="section">为什么我们要公开这件事</h2>
<p>仓库主数据是电商运营里最不显眼的部分，也是几乎所有其他环节都依赖的少数部分之一。把它写成一篇构建记录，是在声明我们现在对自己的标准：库位是物理事实，一个凭空生成库位的系统，比一个承认自己不知道的系统更糟。</p>
<blockquote class="weour-build-quote">一个没有人能走过去的库位记录，不是库存数据，而是装饰。</blockquote>]]></content:encoded>
			<category>仓储</category>
			<category>库存</category>
			<category>数据质量</category>
			<category>WEOUR OS</category>
			<category>工程</category>
		</item>
		<item>
			<title>从 2 个 Ozon 店铺到 5 个</title>
			<link>https://weour.com/zh/build/from-2-ozon-stores-to-5/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/from-2-ozon-stores-to-5/</guid>
			<pubDate>Fri, 11 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[多店铺架构、店铺隔离，以及为什么开一个新店绝不应该复制出第二份库存。]]></description>
			<content:encoded><![CDATA[<p>我们从 2 个 Ozon 店铺起步，现在运营 5 个。这次变化里真正有意思的不是数量，而是我们发现：两个店铺让我们保留了多少未经检验的假设。</p>
<p>只有两个店铺时，整个业务图景可以装在一个人脑子里。哪个 Listing 属于哪个店铺、哪个价格属于哪个渠道、两个店铺同时卖最后一件货会发生什么，都能靠记忆回答。到第三个店铺时这套方法就失效了，到五个店铺时会失效得非常明显。</p>
<h2 id="section">店铺隔离是边界，不是副本</h2>
<p>新增店铺时的第一反应是复制：复制商品目录、复制库存数字、复制映射表，然后把店铺标识改掉。它看起来很快，但会造出第二个版本的&quot;事实&quot;，而它在一周之内就会和第一个版本互相矛盾。</p>
<p>我们选择了相反的立场：</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>原则</dt><dd>在实际系统中的含义</dd></div><div class="weour-build-fact"><dt>店铺是身份，不是库存容器</dt><dd>店铺拥有 Listing、价格和渠道规则，但不拥有自己的库存台账</dd></div><div class="weour-build-fact"><dt>Listing 是渠道产物</dt><dd>两个店铺可以用不同的 Listing 身份销售同一件实物商品</dd></div><div class="weour-build-fact"><dt>商品身份是共享的</dt><dd>被销售的东西只描述一次，其它地方都引用它</dd></div><div class="weour-build-fact"><dt>库存只能被观测，不能被复制</dt><dd>店铺上报它看到的结果，系统记录这次观测，而不是凭空生成一个余额</dd></div></dl>
<p>现在新增一个店铺，是新增一个渠道身份，而不是新增一份库存。</p>
<h2 id="section">为什么用&quot;观测库存&quot;取代&quot;每店余额&quot;</h2>
<p>平台按自己的节奏、用自己的口径告诉你信息。一个渠道上报的是扣除自身预留之后的可售数量；另一个渠道上报的是昨晚被告知的数量。这两个数字都没有错，但也都不是实物库存的全部事实。</p>
<p>所以我们不再要求每个渠道充当&quot;余额的来源&quot;。渠道上报的是一次观测：这是我在这个时间点、从我的口径能看到的结果。观测会被记录、比对和核对，而不会被静默合并成一个谁也无法追溯的单一数字。</p>
<p>正是这一个建模决定，让五个店铺变得可管理，而不是变成五倍的维护量。</p>
<h2 id="section">为了让它成立，我们必须构建什么</h2>
<p>在一套运营模型下同时运行五个店铺，需要三件在两个店铺时并不明显的东西：</p>
<ul class="weour-build-list"><li>一个被所有 Listing 指向的统一商品身份，让同一件实物不会在每个渠道被重新描述一遍。</li><li>一层位于平台 Listing 身份与我们自己的 SKU 之间的映射，因为卖家货号和内部标识并不是同一件事。</li><li>一个解析器，回答&quot;哪些渠道可以卖它、以什么价格卖&quot;，同时不允许任何单一渠道覆盖共享记录。</li></ul>
<p>这些东西都不玄妙。但它们必须被强制执行，因为失败模式是静默的：一个被复制出来的库存数字，看起来依然是一个有效数字。</p>
<blockquote class="weour-build-quote">多店铺的代价不是第五个店铺，而是你没注意到自己创建出来的第二份&quot;事实&quot;。</blockquote>
<h2 id="section">随着店铺增加，什么发生了变化</h2>
<p>当隔离被固定在渠道边界上之后，新增店铺就变成一项运营任务，而不是一个数据迁移项目。下一个店铺可以带着自己的 Listing 组合、自己的定价规则和自己的报表上线，而它引用的商品与实物库存，仍然留在原来的地方。</p>
<p>这才是我们构建这套模型时真正要买的东西：不是更多的店铺，而是每个店铺更少的意外。</p>]]></content:encoded>
			<category>多店铺</category>
			<category>Ozon</category>
			<category>市场渠道</category>
			<category>架构</category>
			<category>库存</category>
		</item>
		<item>
			<title>我们没有再买一套 ERP，而是在构建一套 Commerce OS</title>
			<link>https://weour.com/zh/build/we-didnt-buy-another-erp/</link>
			<guid isPermaLink="true">https://weour.com/zh/build/we-didnt-buy-another-erp/</guid>
			<pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[为什么 WEOUR 不再把多渠道经营当成"再买一套软件"的问题，而是开始构建一套全球贸易操作系统。]]></description>
			<content:encoded><![CDATA[<p>很长一段时间里，摆在我们面前的问题看起来像是一个采购决策。销售渠道在不断增加，库存在各个平台与仓库之间分散，而&quot;我们到底有多少货&quot;这个问题的答案，取决于你去问谁。最直接的做法，就是买一套更大的系统。</p>
<p>我们确实按这个思路试过：比较那些能装下更多 SKU、更多店铺、更多对接的平台。它们都回答了某个版本的问题，但没有一个回答我们的问题。</p>
<h2 id="section">问题从来不是&quot;该买哪一类软件&quot;</h2>
<p>ERP 的前提，是一家已经清楚自己拥有什么的企业：干净的主数据、一个存放事实的地方、基本在一个组织内部运行的流程。当你的业务就是一个目录、通过一两个渠道销售时，这套模型非常有效。</p>
<p>我们的现实完全不同。同一件实物商品，会在多个平台以不同的 Listing 身份存在，来自不同的供应路径，并在不同时间以不同格式上报库存。采购单、平台 Listing 与仓库库位是三种对象，有三种生命周期。把这些压成同一行数据的软件并没有错——它只是在回答一个比我们更简单的问题。</p>
<h2 id="section">我们要构建的东西</h2>
<p>WEOUR OS 是一套 Commerce OS：一组系统，用来描述商品、Listing、仓库与决策之间的关系，并在业务运行的过程中让这些关系保持真实。</p>
<dl class="weour-build-facts"><div class="weour-build-fact"><dt>层</dt><dd>职责</dd></div><div class="weour-build-fact"><dt>渠道</dt><dd>每个店铺保留自己的身份、定价规则与 Listing 生命周期</dd></div><div class="weour-build-fact"><dt>库存</dt><dd>观测值记录来源与时间，不做静默合并</dd></div><div class="weour-build-fact"><dt>仓库</dt><dd>物理库位作为一等记录建模</dd></div><div class="weour-build-fact"><dt>映射</dt><dd>SKU、Listing 与商品身份被关联，而不是被假定为相同</dd></div><div class="weour-build-fact"><dt>Agent</dt><dd>自动化依据明确的系统规则提出建议并执行</dd></div></dl>
<p>每一层都可以被替换，但关系不能。</p>
<h2 id="section">在生产环境里构建，而不是在幻灯片里</h2>
<p>这篇内容以构建日志、而不是成果发布的形式出现，原因是我们并不是从白板开始的：我们改动的是已经承载真实店铺、真实仓库和真实 Listing 的系统。每一个设计决定，都要在它被做出的同一天经受真实业务的检验。</p>
<p>这个限制让人不舒服，但很有用：它排除了无法增量上线的漂亮架构，也迫使我们把业务早已在默认遵循、却从未被明确写下来的规则写出来。</p>
<h2 id="section">这对你正在看的这个商城意味着什么</h2>
<p>weour.com 不会被一个新网站取代，它本身就是系统之一。商城主站、俄语与中文市场渠道、供应商流程，以及内部运营层，正在被纳入同一套关于&quot;店铺、商品与库存状态究竟是什么&quot;的模型。</p>
<blockquote class="weour-build-quote">一套面向全球贸易的操作系统，不是更大的数据库，而是更少的、可以被信任地遵循的规则。</blockquote>
<h2 id="section">接下来</h2>
<p>这个日志的后续内容，会写我们最难处理的部分：店铺如何在共享一个商品目录的同时保持隔离；为什么新增店铺不能复制库存；当占位库位遇到真实库位时发生了什么；以及为什么未知库存永远不会悄悄变成 0。</p>
<p>这就是我们正在构建的东西，我们会持续公开。</p>]]></content:encoded>
			<category>WEOUR OS</category>
			<category>全球贸易</category>
			<category>架构</category>
			<category>公开构建</category>
		</item>
	</channel>
</rss>
