Project timeline: 2006 — 2019 Stack: .NET (ASPX) / SQL Server / jQuery / in-house automated sync tooling / distributed file storage


Preface

This is a project I worked on for ten years. It was shut down in 2019, partly because of the pandemic and partly because the industry had moved on. Looking back now, it carries every hallmark of that pre-mobile-internet era: ASPX, jQuery, hand-written SQL, file server clusters — none of which would show up on the first page of anyone's tech-selection deck today. But it ran steadily for a decade, and it actually solved a problem that no one else had solved well at the time.

I want to write down the engineering decisions behind it while I still remember the details — not to argue that any of it was sophisticated, but to remember why we did things the way we did.

What the project actually did

Before mobile internet really took hold — especially between 2009 and 2013 — pricing and itinerary information flowed between travel agencies across China primarily through a very traditional channel: DM magazines (direct-mail advertising magazines). Each province's branch would publish a weekly issue, laying out their products, prices, and departure dates, printing them, and shipping them to peer agencies. Downstream agencies flipped through the magazines, made phone calls, and booked products.

Two pain points were painfully obvious:

  • Poor timeliness. Once a magazine was printed and shipped nationwide, several days had already passed — and prices may have already shifted.
  • Information silos. A single agency would only subscribe to a handful of magazines. They couldn't see nationwide pricing, and they couldn't compare across regions.

What this platform did was digitize the entire magazine, issue by issue, and put it online as a B2B platform so that any travel agency in the country could browse the latest prices in real time. Simple in concept. Very much not simple in execution.

The real challenge was "volume" and "window"

The business shape of the project made its technical characteristics extreme:

  • High page density. A provincial weekly typically ran 200–300 pages, and every page was a print-quality high-resolution sliced image (users needed to zoom in to read the fine print on pricing tables).
  • Synchronized nationwide publishing. The platform covered 25 branch offices, all publishing on the same weekly cadence.
  • Image volume per week. Roughly 5,000–7,500 high-resolution images.
  • Extremely tight publishing window. Every branch finalized their design on Thursday and Friday. All of it had to be digitized and live by Saturday and Sunday, ready for Monday's business open.

In other words — a 48-hour weekend window to ingest a TB-scale pile of images accumulated over the week, with zero room for errors or missing pages.

Under that cadence, "manual upload" was a dead end from day one. If we had relied on operations staff clicking through an upload UI, we would have needed dozens of people pulling weekend overtime across 25 branches — and if even one person uploaded the wrong page number, downstream agencies would be looking at mismatched pricing sheets. The cost model didn't work and the error rate didn't work.

My core solution: an automated pipeline

The technical heart of this project wasn't the frontend reader, and wasn't the database itself — it was an automated import and distribution pipeline that I designed and wrote myself. The goal was singular: keep humans away from the servers. Once the designers finished a page and named and organized the files per convention, the tooling did the rest.

Four key design decisions, broken out below.

1. Standardized directory-to-schema mapping

The earliest version was the most naive one you can imagine — a web page where operations staff clicked "upload" one page at a time. It broke weekly.

What we did next was: turn the directory structure itself into a protocol. Operations just had to name page numbers per convention and organize each issue into the prescribed hierarchy (province / issue / page). Once the import tool kicked off, it would automatically walk the entire directory tree, parse out "this is province X, issue Y, page Z, belonging to section W," and write the metadata straight into the database.

There's nothing technically profound about this design. But it converted uploading from "manual data entry" into a "filesystem convention." Once that convention was in place, the operations team's job shifted from "clicking a mouse" to "organizing folders" — and the latter is scriptable, while the former isn't.

Today we'd call this thinking convention over configuration. I didn't have the vocabulary back then — I just had an intuition that "conventions should replace input." In hindsight, this is the single decision that kept the whole system alive for ten years.

2. Renaming files by database primary key: a lesson in indexing

Not long after the first version shipped, we hit a performance problem: magazine page loads got progressively slower, and query latency degraded non-linearly as the data grew.

The root cause was a classic lazy design I'd made early on — I was joining on filenames directly (the kind with Chinese characters and issue numbers baked in), which meant every single page load triggered a fuzzy match. Combined with an unoptimized SQL Server indexing strategy at the time, I'd walked myself straight into a performance pit.

When I rewrote it, I made one key change: during import, the program renames high-res originals according to the database primary key (PK) and re-establishes the relationship. The physical filename on disk becomes a plain integer ID.

The payoff was multi-dimensional:

  • Database indexes now ran entirely on integer PKs. Queries stabilized at millisecond latency.
  • Filenames no longer broke when issue numbers or section names changed.
  • Cross-server image migrations became trivial — since filenames carried no business semantics, migration scripts were extremely simple.

Today this reads as textbook "don't let business semantics contaminate your storage layer." But around 2010, I only learned it after performance slapped me in the face.

3. Distributed file storage and pipelined distribution

A single file server was never going to handle 25 provinces writing simultaneously — you didn't need a load test to predict that. So from the start I designed the storage as a clustered distribution model: after the import tool renamed a file, it would route the image to a designated file server per rules, and the database only stored "which server, which path."

A few decisions that held up well:

  • The upload logic was a pipeline (scan → parse → commit → rename → distribute). Each stage retried independently, and a failure at any step wouldn't roll back the whole batch.
  • Sharded by province. The 25 provinces' uploads didn't block each other, so they could all run in parallel during the weekend window.
  • Image servers were fully separated from application servers. The frontend served images from a dedicated domain, so the business IIS instances never got crushed by image traffic.

The end result was this: the 25 branches' massive image volumes could be fully ingested and distributed within the 48-hour window, with cumulative storage exceeding 1TB. That number is nothing by today's standards, but for a mid-sized business system in China circa 2010, it was substantial.

4. The frontend reader: "HD zoom + drag" in 2010

This part was built on jQuery — yes, that jQuery. React didn't exist yet, and Vue hadn't even been conceived.

The frontend challenge was this: travel pricing sheets are information-dense. Small fonts, tight tables, lots of footnotes. Agency users had to be able to freely zoom and pan around a full magazine page to actually read the pricing and departure dates for a specific tour.

Key implementation decisions at the time:

  • Tiered loading. Thumbnails shipped first, full-resolution images loaded on demand. We never dumped a multi-megabyte image into the browser unprompted.
  • Zoom and drag implemented via CSS transforms, not image resampling — which kept things smooth even at high zoom levels.
  • Gesture hit areas and inertia were hand-rolled, because the jQuery ecosystem had no usable component for this at the time.

All of this is a one-line npm install today. Back then, it was written line by line. Looking at that code now, there's plenty I'd optimize — but it held up in production for ten years without a major incident.

Why it ran for ten years

When we shut the platform down in 2019, I found myself asking the question too: how does a system written in ASPX + jQuery manage to serve travel agencies across China reliably for a decade?

My after-the-fact conclusion: the tech stack wasn't what kept it alive. The architectural constraints were.

Specifically:

  • Turning uploads into a filesystem convention sidestepped the most fragile part of the human workflow.
  • Keeping business semantics out of filenames meant no naming-convention change on the business side ever contaminated the storage layer.
  • Province-level sharding plus a file server cluster made peak-window load horizontally scalable by default.
  • Separating the read and write paths meant production read traffic was never at the mercy of the operations-side upload surge.

All of this reads like common sense today. But at the time, each of these was a decision I pushed through the team only after "the last incident" had forced the point.

Looking back, some honest reflections

Truthfully, there's plenty about this project I wouldn't do the same way now:

  • ASPX was a liability. Every time we needed to add APIs or integrate with mobile later on, we had to work around it. If I'd had the courage to fully rearchitect toward a Web API + decoupled frontend in 2014, the following five years would have been considerably easier.
  • Too much got built in-house instead of using open source. The gesture interaction layer in the frontend reader had mature alternatives by 2013, but "it works" kept us from revisiting it. That's a textbook form of implicit technical debt.
  • SQL Server's vertical scaling was straining hard by the later years. I underestimated read/write separation and sharding approaches at the time, and I missed the best window to refactor.
  • The monitoring story was effectively non-existent. A lot of issues were first reported by operations staff calling in — which would be unacceptable today.

But some things held up — especially that automation pipeline. Ten years of stable operation proves one thing: if you can establish the right constraints at the process level, even an aging tech stack can sustain a system for a very long time.

A closing thought

The platform wound down in 2019, partly because the pandemic hit the travel industry and partly for a deeper reason: mobile internet had fundamentally changed how travel agencies got information. DingTalk groups, WeChat groups, and vertical SaaS had absorbed the intermediary "magazine-online" role the platform used to play.

It wasn't defeated. It was outgrown by its era. And that's fine. Something served the ten years it was built to serve, witnessed a whole industry's transformation, and got to exit with dignity — that's already a better ending than most engineers can expect.

While writing this retrospective I dug up the old codebase (I still have the backup). Seeing those <asp:Repeater> and $.ajax calls again felt slightly surreal. But those 4 AM weekend moments — watching the import jobs for all 25 provinces finally come back green — those actually happened.

Noted. Archived.



十年项目复盘:一个全国性旅游 DM 杂志的数字化 B2B 发布平台

项目周期:2006 — 2019 技术栈:.NET (ASPX) / SQL Server / jQuery / 自研自动化同步工具 / 分布式文件存储

📖 English version below — scroll down for the English translation.


写在前面

这是一个做了十年、然后在 2019 年随着疫情和行业转型一起关掉的项目。现在回头看,它身上带着非常典型的"移动互联网前夜"的时代痕迹:ASPX、jQuery、手写 SQL、文件服务器集群——没有一个是今天会被写进技术选型第一页的名字。但它确实稳定跑了十年,也确实解决了一个当时没人解决好的问题。

我想趁还记得细节的时候,把这个项目的工程决策记录下来——不是为了证明它有多先进,而是为了记住当年为什么要那样做。

这个项目是干什么的

在移动互联网真正普及之前(尤其是 2009 到 2013 那几年),全国旅行社之间的报价和线路信息主要靠一个传统渠道流转:DM 杂志(直邮广告杂志)。各省公司每周出一本,把自己的产品、价格、出行日期排成版,印刷出来,寄给同业的旅行社。下游旅行社翻杂志、打电话、定产品。

这套流程有两个非常明显的痛点:

  • 时效性差:杂志印出来再寄到全国,最快也是几天后的事,价格可能已经变了。
  • 信息孤岛:一家旅行社只订几本杂志,看不到全国范围的报价,没法横向比价。

这个平台做的事情,就是把 DM 杂志整本数字化搬到线上,作为一个 B2B 平台让全国旅行社都能实时查阅。听起来简单,做起来是另一回事。

真正的挑战在"量"和"窗口"上

项目的业务形态决定了它的技术特征非常极端:

  • 页数密度大:一个省份的周刊常规在 200–300 页,每一页都是高清印刷级切片图(因为要能放大看清报价单的小字)。
  • 全国同步出刊:覆盖 25 个分公司,每周同时出刊。
  • 单周图片总量:约 5,000–7,500 张高清大图
  • 发布窗口极窄:所有分公司都是周四、周五完成设计出稿,周六、周日必须全部数字化上线,以便周一业务开盘。

换句话说——周末 48 小时的窗口,要吞掉一周里堆起来的 TB 级图片,并且不能出错、不能漏页

这种业务节奏下,"人工上传"这条路从第一天就是走不通的。如果靠运营同学点页面按钮传图,25 个分公司至少要配几十号人同时加班,而且只要有一个人传错页码,下游旅行社看到的报价单就会对不上。成本和错误率都不现实。

我的核心方案:一条自动化流水线

项目的技术核心,不在前端阅读器,也不在数据库本身,而在于我自己设计并写的一套自动化导入和分发流水线。目标只有一个:让人不要碰服务器。设计师那边出完图,按规范命名和分目录,剩下的事情工具自己做完。

下面拆开讲四个关键设计。

1. 标准化目录映射机制

最早期做过最朴素的版本——给运营一个网页,让他们一页一页点上传。一周崩一次。

后来做的事情是:把目录结构本身变成协议。业务端只要按约定命名页码、把每期杂志整理进规定的目录层级(省份 / 期号 / 页码),导入工具启动后会自动扫描整棵目录树,解析出"这是哪个省、第几期、第几页、对应哪个栏目",然后把元数据直接写进数据库。

这个设计本身没什么高深技术,但它把上传这件事从"人工录入"变成了"文件系统约定"。一旦约定建立起来,运营同学的工作就从"点鼠标"变成"整理文件夹"——后者是可以用脚本批处理的,前者不行。

这个选择背后的思路,今天叫 convention over configuration,当年我没这个词汇,只是凭经验觉得"让约定去替代输入"。现在回看,这是整个系统能跑十年的根基。

2. 用数据库主键重命名文件:一个关于索引的教训

第一版系统上线没多久就遇到一个性能问题:页面加载杂志时越来越慢,而且查询延迟随数据量增长呈现非线性恶化

查下来原因是早期做了一个典型的偷懒设计——直接用文件名(带中文和期号的那种)去做关联查找,相当于每次读一页图都要跑一次模糊匹配,加上当时 SQL Server 索引策略没优化好,很快就把自己卷进了性能坑里。

重构的时候做了一个关键调整:导入过程中,程序会根据数据库主键(PK)对高清原图进行重命名和重新关联。落到磁盘上的物理文件名就是一串整型 ID。

这个改动带来的收益是多重的:

  • 数据库索引完全走整型 PK,查询稳定在毫秒级;
  • 文件名再也不会因为期号、栏目名改动而失效;
  • 跨服务器迁移图片时,文件名本身不携带业务语义,迁移脚本极其简单。

今天看是教科书级别的"不要让业务语义污染存储层",但在 2010 年前后,这是我被性能打脸之后才学到的。

3. 分布式文件存储与流水线分发

单台文件服务器撑不住 25 个省同时写入——这件事不需要压测就能预判。所以从一开始就把文件存储设计成了集群分发模式:导入工具在重命名完文件之后,会按照规则把图片分发到指定的文件服务器上,数据库里只存"这张图落在哪台服务器的哪个路径"。

几个当时做得还算对的决策:

  • 上传逻辑做成流水线(扫描 → 解析 → 入库 → 重命名 → 分发),每一环可以独立重试,中间任何一步失败都不会让整批数据回滚;
  • 按省份做分片,25 个省的上传互不阻塞,这样周末窗口里大家可以并行跑;
  • 图片服务器和应用服务器彻底分离,前端读图走独立域名,避免压垮业务 IIS。

最终效果是:25 个分公司的海量图片能在 48 小时的窗口内全部完成入库和分发,累计数据量超过 1TB。这个数字今天看不算什么,但在 2010 年前后的国内中小型业务系统里,是个不小的量。

4. 前端阅读器:2010 年的"高清缩放 + 拖拽"

这一块是基于 jQuery 做的——对,就是那个 jQuery。当时还没有 React,Vue 连影子都没有。

前端的挑战在于:旅游报价单里的信息非常密,字小、表格密、脚注多。旅行社用户必须能在一张整页杂志上自由缩放和平移,才能看清某一条线路的具体报价和发团日期。

当年实现的几个关键点:

  • 分级加载:缩略图先出,全尺寸图按需加载,避免一开始就怼一张几 MB 的大图进浏览器;
  • 缩放和拖拽基于 CSS transform,不走图片重采样,保证高清下依然流畅;
  • 手势交互的命中区和惯性手写了一版,因为 jQuery 生态里没有现成的能用的组件。

放在今天这些都是一个开源库解决的事情,但当年是一行一行写出来的。这部分代码现在回看有不少可以优化的地方,但它在生产环境里撑了十年没出大问题。

它为什么能跑十年

2019 年关掉这个平台的时候,我自己也想过这个问题:一个用 ASPX + jQuery 写的系统,凭什么能稳定服务全国旅行社十年?

我事后的结论是:技术选型不是它活下来的原因,架构约束是

具体说:

  • 把上传变成文件系统约定,绕开了人工环节最脆弱的部分;
  • 让文件名不携带业务语义,任何一次业务侧命名规则变化都不会污染存储层;
  • 按省份分片 + 文件服务器集群,让峰值窗口的压力天然可以横向扩展;
  • 读写路径分离,让业务读请求永远不会被运营侧的上传洪峰影响。

这些东西放在今天都是常识,但当年在团队里推行的时候,其实每一个都是被"上一次事故"逼出来的。

一些今天回看的反思

老实说,这个项目有很多我现在不会再那样做的地方:

  • ASPX 是个负债。后期业务要加接口、要对接移动端,每次都得绕。如果 2014 年有勇气整体重构到 Web API + 前后端分离,后面五年会轻松很多。
  • 自研了太多本该用开源的东西。前端阅读器那套手势交互,2013 年之后已经有成熟方案了,但因为"能跑"就没动过,这是一种隐性技术债。
  • SQL Server 的垂直扩容撑到后期已经很吃力。当时对读写分离和分库分表的方案评估不足,错过了最佳重构窗口。
  • 监控体系几乎等于没有。很多问题是靠运营电话反馈才发现的,这在今天是不可接受的。

但它也有做对的部分——尤其是那条自动化流水线。十年的稳定运行证明:只要你能在流程上建立起好的约束,哪怕底层技术栈老旧,系统也能活得很久

一点感慨

这个平台在 2019 年关停,一部分是因为疫情冲击了整个旅游行业,另一部分更深层的原因是——移动互联网已经彻底改变了旅行社之间的信息获取方式。钉钉、微信群、垂直 SaaS 把这个"杂志线上化"的中间层需求消化掉了。

它不是被打败的,是被时代翻过去的。这挺好。一个东西服务了它该服务的十年,也见证了一个行业形态的变化,能体面地退场,已经是工程师能期待的不错的结局了。

写这篇复盘的时候翻了一下当年的代码(还有备份),看着那些 <asp:Repeater>$.ajax 有点恍惚。但那些在周末通宵盯着 25 个省份图片全部导入成功的凌晨四点,是确实存在过的。

记一笔,存档。