“无技术小哥靠 3 个‘枯燥’的网站,月入 22 万人民币”,这类标题在各类信息流里出现频率很高,但里面的收入数字来自第三方内容,没有任何可核验的成本、流量、客单价数据支撑,本文不对这个数字做任何背书,也不做任何收益承诺。真正值得技术人关注的是另一件事:所谓的“枯燥网站”,绝大多数是功能极窄的工具站——PDF 转 Word、图片格式互转、文本大小写与去重、单位换算、时间戳转换、Base64 编解码这一类。它们没有炫酷的 UI,没有复杂的业务逻辑,用户点进来三十秒用完就走,但正是这种“用完就走”的确定性需求,构成了稳定的搜索流量池。
从技术实现角度看,这类站点的门槛低到超出很多人想象:前端可以是纯静态 HTML 加一点原生 JavaScript,后端只需要一个极薄的 HTTP 服务,把 FFmpeg、LibreOffice、Pillow 这类成熟命令行工具包装成接口就行。真正吃功夫的不是写代码,而是任务队列、文件清理、并发限流、错误兜底这些“脏活”,以及页面结构、加载速度、可索引性这些搜索侧工程。
这篇文章不谈收入,只谈落地。我会把三类典型工具站拆开讲:文件格式转换站、文本批量处理站、纯前端计算站,分别对应“重后端”“中后端”“零后端”三种形态。读者看完应该能做到几件事:在本地把一个转换服务跑起来、用 curl 和 Python 跑通接口、搭一套带队列和失败重试的批量任务、判断自己机器的 CPU 和内存够不够、以及在上线前把隐私和版权边界想清楚。适合已经会一点 Python 或 Node、想动手做一个真实小项目练手、或者正在评估工具站技术成本的开发者。
1. 工具型网站的核心能力速览
先把三类站点的技术画像摊开。这张表里没有任何收入指标,只有工程参数,因为这些才是你能控制的东西。
| 能力项 | 格式转换站 | 文本批量处理站 | 纯前端计算站 |
|---|---|---|---|
| 站点类型 | 重后端,依赖外部二进制工具 | 轻后端,逻辑以代码为主 | 零后端,纯静态 |
| 典型功能 | PDF/Word/图片/音视频格式互转 | 大小写转换、去重、排序、正则替换、字数统计 | 单位换算、时间戳、进制转换、编码解码 |
| 核心技术栈 | FastAPI/Express + FFmpeg + LibreOffice + Pillow | FastAPI/Express + 标准库 | HTML + 原生 JS 或轻框架 |
| 是否必须 GPU | 不需要,纯 CPU 场景 | 不需要 | 不需要 |
| 内存门槛 | 较高,取决于文件大小与并发数 | 低 | 极低 |
| 磁盘门槛 | 高,临时文件需要定期清理 | 低 | 无 |
| 启动方式 | Docker 或 systemd 常驻服务 | 同左 | 静态托管或 Nginx 直出 |
| 是否支持 API | 建议支持 | 建议支持 | 通常不需要 |
| 是否支持批量任务 | 需要,且必须带队列 | 需要,同步处理即可 | 前端循环即可 |
| 适合场景 | 有明确搜索需求的工具流量 | 开发者与办公人群 | 长尾关键词覆盖 |
| 主要风险 | 恶意文件、超大文件、版权素材 | 隐私文本上传 | 几乎无 |
把这张表读一遍就能发现一个规律:三类站点的复杂度差异极大,但它们的共性都是“功能单一、边界清晰”。这也是它们容易被做出来的原因——你不需要设计一套业务模型,只需要把一个已经被验证过的需求,用最朴素的方式实现一遍,然后把稳定性和速度做到位。真正拉开差距的地方在于:别人上传一个 200MB 的 PDF,你的服务是直接 502,还是能排队处理完并给出明确提示。这类细节决定了站点是“能跑”还是“能用”。
从材料给出的标题看,三个网站的定位大概率就是这三类。需要说明的是,标题里的“无技术”表述并不准确——能把转换服务、队列、清理任务、反滥用做稳的人,技术底子不会差。反过来讲,如果你是真正的新手,这三类站点也是最好的练手项目,因为它们需求明确、验收标准清晰、出问题容易定位。
2. 适用场景与使用边界
先说适合谁。第一类是想通过一个完整小项目把“部署—接口—队列—监控”这条链路走通的后端初学者,工具站是极佳的载体,因为业务逻辑简单,你可以把全部精力放在工程能力上。第二类是已经有一定运维经验、想验证某个细分需求是否有搜索量的开发者,先用一个最小版本上线,看真实请求再来决定要不要投入。第三类是内部有批量处理需求的小团队,比如每天要把一批合同 PDF 转成可检索文本,与其买服务,不如自己搭一个内网版本。
再说能解决什么问题。工具站的核心价值是“把一次性的、低频但确定的操作,变成无需安装软件的网页动作”。用户不想为了转一个格式去装一个几百兆的客户端,也不想为了去重一份名单去写脚本,这就是需求存在的基础。这类需求的用户意图极其明确,搜索词就是操作本身,转化路径短,这也是工具站在搜索侧相对友好的原因。
不适合的场景同样要说清楚。第一,不适合把它当成被动收入来规划,流量获取、内容维护、反滥用、合规审查都是持续投入,没有任何一个环节可以省略。第二,不适合承接超大文件或超高并发的免费场景,一个没有限流的转换接口,很容易被批量请求打满 CPU,最终拖垮整台机器。第三,不适合处理来源不明的文件,尤其是可执行文件改后缀、带宏文档、加密压缩包这类,服务端必须做类型校验和大小限制。
合规边界必须写进设计里。格式转换站会接触到用户的文档、图片、音视频,这些内容可能包含个人信息、商业机密甚至他人作品。工程上至少要做到三点:临时文件设定明确的存活时间并由定时任务强制清理;不在日志里记录文件内容或完整文件名;对外明确告知文件保留策略。文本处理站接收的是用户粘贴的文本,同样可能包含隐私信息,接口应当避免把原文写入持久化存储。如果站点涉及图片、音视频、文档的二次分发,必须提醒用户确认自己拥有相应授权,不得用于侵权用途。做工具站最忌讳的是“先上线再说”,因为数据处理类服务的合规成本,往往在出问题那一刻才显现。
3. 环境准备与前置条件
环境这块不需要复杂,一台普通的 Linux 云主机就够跑前两类站点,第三类甚至可以只放在静态托管上。下面给的是通用检查清单,具体版本号请以你实际安装的为准,不同发行版的软件源差异很大,写死版本号反而容易踩坑。
操作系统层面,建议用主流的 Linux 发行版,Ubuntu LTS 或 Debian 稳定版都可以。不建议在 Windows 上直接跑生产服务,虽然本地开发没问题,但定时清理、权限控制、进程守护这些在 Linux 上省事得多。如果你只有 Windows 环境,用 WSL2 或者 Docker Desktop 做本地验证,正式部署还是走 Linux。
运行时层面,Python 建议 3.10 及以上,Node 建议 18 LTS 及以上,二选一即可,不要两套都上,否则依赖管理会很乱。Python 侧推荐用 venv 隔离,Node 侧用 pnpm 或 npm 都可以。依赖管理工具不要混用,一个项目里出现 requirements.txt 和 package.json 同时承担主逻辑,后期维护会很痛苦。
外部二进制工具是格式转换站的关键。音视频处理依赖 FFmpeg,文档转换依赖 LibreOffice 的无头模式,图片处理依赖 Pillow 或 ImageMagick。这几个工具的共同特点是安装包大、启动慢、对字体和语言包敏感。特别是 LibreOffice,如果没有装中文字体,转换出来的 PDF 会出现方块字,这个问题在本地测试时很容易被忽略,上线后才被用户发现。
资源门槛方面,纯文本处理和前端计算站 1 核 1G 就能跑,格式转换站建议起步 2 核 4G,磁盘至少留 40G 给临时文件,并且要和系统盘分开或者单独挂载一个数据盘,避免临时文件写满根分区导致服务起不来。端口方面,Web 服务默认监听 127.0.0.1 的高位端口,由 Nginx 反向代理对外,不要把应用端口直接暴露在公网。
最后一项经常被忘掉:时区和时间同步。临时文件的过期清理、任务队列的超时判断、日志时间戳,全都依赖准确的系统时间。上线前确认 NTP 正常工作,否则你会遇到“文件明明设了 10 分钟过期却一直不删”这种诡异问题。
4. 三类工具站的部署方式
部署这块我按三类站点分别给方案,命令都是通用模板,路径、端口、镜像名需要按你的实际情况替换。不要直接复制粘贴到生产环境,先在一台测试机上跑通再迁移。
4.1 格式转换站:Docker 化部署
转换站最麻烦的是外部依赖,所以强烈建议容器化,把 FFmpeg、LibreOffice、字体一次性打进镜像,避免宿主机环境污染。
# 通用模板:实际基础镜像和依赖请按项目调整 FROM python:3.11-slim # 安装转换所需的外部工具与中文字体 RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ libreoffice-writer \ libreoffice-calc \ fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # 临时目录独立挂载,避免写满容器层 ENV TMP_DIR=/data/tmp RUN mkdir -p /data/tmp EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]配套的 compose 文件负责挂载数据盘和设置资源上限,这一步很关键,不给容器设上限,一个异常请求就可能把整台机器拖死。
# docker-compose.yml 通用模板,按实际项目替换镜像名与路径 version: "3.8" services: converter: build: . restart: unless-stopped ports: - "127.0.0.1:8000:8000" volumes: - /data/tmp:/data/tmp - /data/uploads:/data/uploads environment: - TMP_DIR=/data/tmp - MAX_FILE_SIZE_MB=100 - FILE_TTL_MINUTES=15 - WORKER_CONCURRENCY=2 deploy: resources: limits: cpus: "3.0" memory: 3g tmpfs: - /tmp:size=512mNginx 放在最前面做反向代理、限流和上传大小控制。客户端上传限制一定要在网关层先拦一道,别指望应用层去扛一个 2G 的请求体。
# nginx.conf 片段:上传大小、超时与基础限流 server { listen 80; server_name example.com; client_max_body_size 100m; client_body_timeout 60s; location /api/ { limit_req zone=toolapi burst=5 nodelay; proxy_pass http://127.0.0.1:8000; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/toolsite; try_files $uri $uri/ /index.html; } } # 在 http 块中定义限流区 # limit_req_zone $binary_remote_addr zone=toolapi:10m rate=30r/m;4.2 文本处理站:轻量服务直跑
文本处理站没有重依赖,用 venv 加 systemd 就够了,不必上容器。好处是启动快、排查方便,日志直接看 journalctl。
# 通用模板:创建虚拟环境并安装依赖 cd /opt/texttool python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install fastapi uvicorn[standard] pydantic # 本地验证启动 uvicorn app.main:app --host 127.0.0.1 --port 8010 --workers 2systemd 单元负责守护进程和开机自启,这里重点配置 Restart 和内存限制,防止异常输入把进程撑爆。
# /etc/systemd/system/texttool.service 通用模板 [Unit] Description=Text Tool Service After=network.target [Service] Type=simple User=www-data WorkingDirectory=/opt/texttool Environment="PATH=/opt/texttool/venv/bin" ExecStart=/opt/texttool/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8010 --workers 2 Restart=always RestartSec=3 MemoryMax=1G NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target4.3 纯前端计算站:静态直出
第三类最简单,编译产物丢给 Nginx 或对象存储即可,不需要任何后端进程。这类站点的技术重点从“服务稳定性”转移到“首屏加载速度”和“可索引性”。首屏要快,就不要引入大体积框架,用原生 JS 足够;可索引要稳,就要保证每个功能都有独立 URL 和独立标题,别把十几个工具塞进一个页面靠切换显示,那样搜索侧几乎拿不到任何长尾流量。
# 通用模板:本地构建后同步到静态目录 npm install npm run build rsync -av --delete ./dist/ /var/www/toolsite/5. 核心功能实现与本地验证
这一节把三类站点的核心逻辑各写一段,代码是通用实现思路,字段名和路径按你的项目调整。验证顺序统一为:先本地跑通最小用例,再加批量,最后压一下边界。
5.1 格式转换站的上传转换下载闭环
转换站的核心不是“转换”本身,因为 FFmpeg 和 LibreOffice 已经把转换做好了,核心是“安全地接收文件、可控地调用外部进程、可靠地返回结果”。下面这段 FastAPI 实现把这三步串起来,重点看文件大小校验、临时目录隔离和超时控制。
import os import shutil import subprocess import tempfile import uuid from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.responses import FileResponse app = FastAPI() MAX_SIZE = 100 * 1024 * 1024 TMP_ROOT = os.environ.get("TMP_DIR", "/data/tmp") ALLOWED_EXT = {".docx", ".pdf", ".png", ".jpg"} @app.post("/api/convert") async def convert(file: UploadFile = File(...), target: str = "pdf"): ext = os.path.splitext(file.filename or "")[1].lower() if ext not in ALLOWED_EXT: raise HTTPException(status_code=400, detail="不支持的文件类型") job_id = uuid.uuid4().hex workdir = os.path.join(TMP_ROOT, job_id) os.makedirs(workdir, exist_ok=True) src_path = os.path.join(workdir, "input" + ext) size = 0 with open(src_path, "wb") as f: while chunk := await file.read(1024 * 1024): size += len(chunk) if size > MAX_SIZE: shutil.rmtree(workdir, ignore_errors=True) raise HTTPException(status_code=413, detail="文件超过大小限制") f.write(chunk) out_path = os.path.join(workdir, "output.pdf") try: # 通用模板:实际转换命令需按目标格式替换 subprocess.run( ["libreoffice", "--headless", "--convert-to", "pdf", "--outdir", workdir, src_path], check=True, timeout=120, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, ) produced = os.path.join(workdir, os.path.basename(src_path) + ".pdf") if not os.path.exists(produced): raise HTTPException(status_code=500, detail="转换未产生输出") except subprocess.TimeoutExpired: raise HTTPException(status_code=504, detail="转换超时") except subprocess.CalledProcessError: raise HTTPException(status_code=500, detail="转换失败") return FileResponse(produced, media_type="application/pdf", filename="output.pdf")验证方法很直接:先传一个几 KB 的小文档,看输出是否正确;再传一个带中文的文档,检查字体是否正常;再传一个超过限制的文件,确认返回 413 而不是把服务打挂;最后并发两个请求,观察 LibreOffice 是否出现进程抢占。这四步走完,你对这个服务的真实能力就有一个大致判断了。
5.2 文本处理站的批量去重与统计
文本处理站的逻辑全在自己手里,没有外部依赖,所以可以做得更细。下面这段实现批量去重、行数统计和大小写转换,重点看输入长度限制和结果结构。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field app = FastAPI() MAX_CHARS = 500_000 class TextRequest(BaseModel): text: str = Field(..., description="待处理文本") mode: str = Field("dedup", description="dedup | upper | lower | stats") keep_order: bool = True class TextResponse(BaseModel): result: str line_count: int char_count: int removed: int @app.post("/api/process", response_model=TextResponse) def process(req: TextRequest): if len(req.text) > MAX_CHARS: raise HTTPException(status_code=413, detail="文本超过长度限制") lines = req.text.splitlines() total = len(lines) if req.mode == "dedup": if req.keep_order: seen, out = set(), [] for line in lines: if line not in seen: seen.add(line) out.append(line) else: out = sorted(set(lines)) result = "\n".join(out) elif req.mode == "upper": result = req.text.upper() elif req.mode == "lower": result = req.text.lower() elif req.mode == "stats": result = req.text else: raise HTTPException(status_code=400, detail="不支持的 mode") return TextResponse( result=result, line_count=total, char_count=len(req.text), removed=total - len(result.splitlines()) if req.mode == "dedup" else 0, )验证时容易忽略两个点。一是换行符差异,Windows 的\r\n和 Linux 的\n混在一起时,去重结果可能不符合预期,接口里应当统一做一次规范化。二是空行处理,很多用户粘贴的文本首尾和中间带大量空行,直接按行去重会把空行也当有效数据,最好提供“忽略空行”的开关。这类细节不影响功能能不能跑,但直接影响用户会不会用第二次。
5.3 纯前端计算站的实现要点
计算站没有后端,所有逻辑在浏览器里,所以验收标准变成“页面能不能被搜到、打开快不快、计算结果准不准”。这里给一个时间戳转换的核心逻辑,注意时间戳单位判断和本地时区显示。
// 通用实现:时间戳与日期互转 function tsToDate(input) { const num = Number(String(input).trim()); if (!Number.isFinite(num)) return { ok: false, msg: "请输入合法数字" }; // 10 位按秒处理,13 位按毫秒处理 const ms = String(Math.trunc(num)).length <= 10 ? num * 1000 : num; const d = new Date(ms); if (Number.isNaN(d.getTime())) return { ok: false, msg: "时间戳超出范围" }; return { ok: true, local: d.toLocaleString(), iso: d.toISOString(), utc: d.toUTCString(), }; } function dateToTs(dateStr) { const d = new Date(dateStr); if (Number.isNaN(d.getTime())) return { ok: false, msg: "日期格式无法识别" }; return { ok: true, seconds: Math.floor(d.getTime() / 1000), ms: d.getTime() }; }这类站点的工程重点不在代码,而在页面结构。每个功能一个独立路径,比如/tools/timestamp、/tools/base64,页面标题写清楚功能和使用场景,正文里给出输入示例和常见格式说明。不要做成一堆按钮挤在同一页,那样既不利于搜索收录,也不利于用户理解。
6. 接口 API 设计与批量任务
工具站要不要开放 API,取决于你的目标用户。如果只是面向普通访客,API 不是必需项;但如果你想承接开发者和小团队的批量需求,API 几乎是唯一的接入方式,因为没人会愿意在网页上一个个点几百次。
接口设计上有几个原则值得坚持。输入输出用 JSON,不要用奇怪的表单编码;错误必须返回结构化信息,包含错误码和可读描述;所有长耗时操作要么同步返回结果,要么返回任务 ID 让客户端轮询,不要既不返回也不超时。下面给一个任务化的批量处理设计,适用于转换站这种耗时不可控的场景。
# 批量任务提交与查询的通用模板 from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List import uuid, time app = FastAPI() TASKS = {} # 生产环境请换成 Redis 或数据库 class BatchRequest(BaseModel): items: List[str] mode: str = "dedup" @app.post("/api/batch") def submit_batch(req: BatchRequest): if not req.items or len(req.items) > 100: raise HTTPException(status_code=400, detail="单次批量条数需在 1-100 之间") task_id = uuid.uuid4().hex TASKS[task_id] = { "status": "queued", "total": len(req.items), "done": 0, "created_at": time.time(), } # 实际项目中这里把任务推入队列,由 worker 消费 return {"task_id": task_id, "status": "queued"} @app.get("/api/batch/{task_id}") def query_batch(task_id: str): task = TASKS.get(task_id) if not task: raise HTTPException(status_code=404, detail="任务不存在") return taskcurl 调用示例,注意带上超时和明确的 Content-Type,这两个小细节能省掉大量排查时间。
# 提交单文件转换任务,路径与端口按实际服务替换 curl -X POST "http://127.0.0.1:8000/api/convert?target=pdf" \ -H "Content-Type: multipart/form-data" \ -F "file=@./sample.docx" \ --max-time 180 \ -o ./output.pdf # 提交批量文本任务 curl -X POST "http://127.0.0.1:8010/api/batch" \ -H "Content-Type: application/json" \ -d '{"items":["line A","line B","line A"],"mode":"dedup"}' \ --max-time 30 # 轮询任务状态 curl "http://127.0.0.1:8010/api/batch/<task_id>" --max-time 10Python 侧的批量调用要注意两件事:一是加并发上限,别一次性开五百个线程把服务打崩;二是加失败重试,但重试必须带退避,且只对可重试的错误码重试,413 和 400 这类参数错误重试一万次也不会成功。
import time import requests from concurrent.futures import ThreadPoolExecutor BASE = "http://127.0.0.1:8000" def convert_one(path): for attempt in range(3): try: with open(path, "rb") as f: resp = requests.post( f"{BASE}/api/convert", params={"target": "pdf"}, files={"file": f}, timeout=180, ) # 仅对服务端错误做重试,参数类错误直接失败 if resp.status_code == 200: return {"file": path, "ok": True, "size": len(resp.content)} if resp.status_code >= 500: raise RuntimeError(f"server error {resp.status_code}") return {"file": path, "ok": False, "reason": resp.text[:200]} except Exception as e: if attempt == 2: return {"file": path, "ok": False, "reason": str(e)} time.sleep(2 ** attempt) # 退避重试 with ThreadPoolExecutor(max_workers=4) as pool: results = list(pool.map(convert_one, ["a.docx", "b.docx", "c.docx"])) for r in results: print(r)队列设计上给一个务实建议:不要一上来就上 Kafka 或 RabbitMQ,Redis 加一个简单的 list 结构就能撑住绝大多数工具站的量级。关键是把“任务状态”和“结果文件”分开存,任务完成后再让客户端来取结果,不要把结果文件塞进队列消息里,那样很容易把内存撑爆。
7. 资源占用与性能观察
工具站的性能瓶颈非常集中,不会像业务系统那样分散在数据库和缓存上。你需要盯的指标只有四个:CPU 使用率、内存占用、磁盘写入量、单请求耗时。
观察方法上,top和htop看整体,docker stats看容器维度,iostat看磁盘,最有用的是给每个请求打一条结构化日志,记录文件大小、处理耗时、退出码。只有这样你才能回答“到底是 200MB 的文件慢,还是所有文件都慢”。
格式转换站的 CPU 消耗几乎线性正比于文件大小和页数,LibreOffice 处理一个几十页的文档可能要十几秒,这期间它会占满一个核。并发数不要超过 CPU 核数,否则进程之间互相抢占,整体耗时反而更长。音视频转码是同样的道理,FFmpeg 本身就支持多线程,你再开多个进程只会让机器更忙。
内存方面,最大的坑是“一次性读入内存”。上传文件时如果用await file.read()把整个文件读进来,一个 500MB 的文件就吃掉 500MB 内存,几个并发就是几个 G。正确做法是分块读写,每次 1MB 左右,内存占用基本恒定。图片处理和 PDF 渲染也很吃内存,这类操作要设单独的并发上限。
文本处理站的性能主要受限于字符串操作和输入长度。十万行以内的去重在几十毫秒级,但如果你用时间复杂度高的写法,比如在循环里反复list.count(),输入长度翻十倍耗时会翻百倍。这类问题在本地小样本测试时完全看不出来,必须用接近上限的数据压一次。
前端计算站几乎没有服务端压力,真正影响体验的是首屏资源大小。一个几百 KB 的 JS 包和一堆字体文件,在弱网环境下的加载时间会明显拖慢体验,而工具站的用户耐心通常只有几秒。所以计算站要尽量做小,能不用框架就不用,图片用 SVG,字体用系统默认。
降低资源占用的手段也很具体:给所有上传设置硬性大小上限;把转换任务改成异步加轮询,避免长连接占着 worker;给临时文件设定明确 TTL,并用定时任务强制清理;给接口加基于 IP 的限流,防止单个来源刷爆;容器层面设置 CPU 和内存上限,宁可返回 503 也不要让整机失联。这些配置写起来不复杂,但能否坚持做,直接决定了站点能不能长期稳定跑着。
8. 常见问题与排查方法
这一类项目的问题高度集中,下面这张表覆盖了绝大多数场景,遇到问题先对照排查,不要一上来就重装环境。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 服务启动报模块找不到 | 依赖装到了全局而非虚拟环境 | which python、pip list | 重新在 venv 内安装依赖 |
| 容器启动后立即退出 | 启动命令路径错误或端口被占 | docker logs <id> | 修正 CMD,检查端口占用 |
| 页面打不开但进程在跑 | 只监听了 127.0.0.1 或网关未转发 | ss -lntp看监听地址 | 检查 Nginx 反向代理配置 |
| 转换输出中文变方块 | 容器内缺少中文字体 | 用中文文档做一次转换 | 安装 CJK 字体后重建镜像 |
| 转换超时 | 文件过大或 LibreOffice 卡死 | 看单请求耗时日志 | 提高超时并限制文件大小 |
| 上传大文件直接 413 | 网关层大小限制未放开 | 看 Nginx 错误日志 | 调整client_max_body_size |
| 内存持续上涨不回落 | 上传文件被整体读入内存 | docker stats观察 | 改成分块读写 |
| 磁盘被写满 | 临时文件未清理 | du -sh /data/tmp | 加定时清理任务并设 TTL |
| 批量任务一直排队 | worker 未启动或并发设为 0 | 查看队列长度和 worker 日志 | 启动 worker,调整并发数 |
| 部分请求 502 | 上游超时或 worker 全部阻塞 | 看 Nginx 与上游日志 | 改异步任务,提高 worker 数 |
| 接口返回 400 但参数没错 | 字段名或 Content-Type 不匹配 | 打印请求体 | 对照接口文档修正调用 |
| 结果文件下载 404 | 临时文件已被清理 | 查文件 TTL 与任务耗时 | 延长 TTL 或改成立即返回 |
几个高频问题的深入说明。第一是字体问题,它在本地几乎不会出现,因为你的开发机装了完整字体,但容器里是精简镜像,中文字体一定缺失,必须在构建阶段显式安装。第二是进程残留,LibreOffice 在异常退出时可能留下后台进程,日积月累会吃满内存,需要在清理脚本里顺带处理。第三是端口冲突,本地开发时经常遇到默认端口被占,养成启动前先ss -lntp | grep 端口的习惯,比事后排查快得多。
还有一个容易被忽略的问题是时区和夏令时。时间戳转换类工具如果只做toString()而不明确标注时区,用户在不同地区看到的结果会不一致,这不是 bug 但会引发大量反馈。接口和页面都应当明确输出 UTC 和本地时间两套结果,让用户自己判断。
9. 最佳实践与使用建议
第一条,先用最小可运行版本上线,不要一开始就把三类站点全做。选一个你最能判断需求真伪的功能,只做输入、处理、输出三步,把错误处理和限流做完,跑一周看看有没有真实请求,再决定要不要扩展。工具站最大的浪费不是代码写得差,而是做了一个没人用的功能。
第二条,把目录结构固定下来。上传、临时、输出、日志、配置分开放,每类文件有明确的保留策略。这个习惯在项目小的时候看不出价值,但当你需要排查“某个文件到底有没有被清理”时,清晰的目录结构能省掉大量时间。
第三条,日志要结构化但不要过度。每个请求记录时间、来源 IP 的哈希、文件大小、处理耗时、结果状态,这五项足够定位绝大多数问题。不要把文件名和文件内容写进日志,这是隐私底线。
第四条,限流和配额必须从第一天就有。免费工具站被刷是常态,不是意外。按 IP 做基础限流,按天做总量配额,超过阈值返回明确的提示而不是默默失败。这既是保护服务器,也是保护正常用户。
第五条,接口服务要限制访问范围。如果 API 是给内部或少量合作方用的,就用白名单加密钥,不要公开在文档里;如果是公开的,就必须有频率限制和用量上限。任何无限制的公开接口,最终都会变成别人的免费算力。
第六条,涉及用户上传的文档、图片、音视频,必须在上线前把授权和隐私问题想清楚。页面上要明确告知文件保留时间,服务端要真的执行清理,不能只写不删。如果站点涉及对他人作品的转换、二次分发,要提醒使用者自行确认授权,工具本身不承担内容合规责任。
第七条,保留一套最小可复现配置。把 Dockerfile、compose 文件、Nginx 配置、systemd 单元都放进版本管理,任何一台新机器都能在半小时内复现出同样的环境。这件事在项目只有你一个人维护时尤其重要,因为半年后你大概率已经不记得当初改过哪些参数。
第八条,发布前做一轮真实场景复核。用手机网络打开一次页面,用一个接近大小上限的文件跑一次转换,用一个格式混乱的文本跑一次去重,用一个错误参数调一次接口。这四件事能覆盖大部分线上事故的成因。
10. 收尾:先跑通一个,再谈其他的
回到最初那个标题。三个“枯燥”的网站能被反复拿出来讲,本质上说明这类需求的真实存在:用户就是要转个格式、去个重、算个时间戳,而且他们希望三十秒内搞定,不想注册、不想装软件。这个需求不会消失,但它也从来不是“做出来就有流量”的生意,中间隔着搜索优化、稳定性、反滥用、内容维护一整套工程工作。
如果你要动手,最先验证的应该是那个你最有把握判断需求真伪的功能。具体做法是:本地起一个最小服务,用 5.1 节那段代码把上传转换下载跑通,用一个中文文档验证字体,用一个超大文件验证限制,用一个并发请求验证稳定性。这四步走完,你对整个技术链路就有了体感,剩下的事情都是在这个骨架上加东西。
最容易踩的坑不是代码写错,而是三件事:没装中文字体导致输出乱码,没做临时文件清理导致磁盘写满,没做限流导致被刷到服务不可用。这三个问题都不难修,但都需要在设计和部署阶段就考虑到,而不是等出事再补。
后续可以扩展的方向也很清晰。如果你做的是转换站,可以往批量任务和接口化走,服务小团队的自动化流程;如果你做的是计算站,可以往长尾工具覆盖走,用几十个独立页面吃掉细分搜索词;如果你做的是文本处理站,可以往规则可配置走,让用户自己组合处理步骤。技术上的天花板不高,但工程上的细节很多,这恰好是练手和积累的好地方,建议收藏这几段配置,下次部署时直接拿来改。