| name | backend-development |
| description | 在后端/服务端工作项(HTTP/REST/GraphQL API、服务与仓库层、数据库访问、缓存、鉴权、限流、后台任务、可观测性、配置与机密、弹性容错、生产就绪)的规格、设计、实现或评审中使用,涉及接口契约、分层与依赖方向、配置与机密、错误模型、数据一致性、幂等、认证授权、依赖超时重试熔断、过载保护、优雅停机时。只承载服务端/API 领域约束;客户端/UI、行业专属服务或其他相邻领域规则由命中 description 的领域技能叠加,语言级规则见适用 `<language>-coding-standards`。 |
Backend Development
总览
后端约束的共同点:违反时单次请求看着正常,规模化和并发下才坏——数据不一致、重复扣款、N+1 拖垮数据库、限流失效被打挂、依赖一抖动连接耗尽雪崩、错误被静默吞掉、机密硬编码进仓库。所以这些约束必须前置到规格和设计:在 spec 里有接口契约与一致性要求、在 design 里有分层边界、事务边界、幂等与容错策略、在测试里有证据,而不是在压测或线上事故时才发现。本技能按维度给出"在哪个阶段定什么、实现红线、用什么证据",每条红线尽量配最小正反例;详表与扩展模式见末尾参考。语言写法见适用语言技能,本文只承载后端领域维度。
维度速览:分层与依赖方向 · 配置与机密 · API 契约 · 错误模型 · 数据访问与一致性 · 幂等与并发 · 认证与授权 · 缓存与失效 · 弹性与依赖容错 · 限流与过载保护 · 后台任务与异步 · 可观测性 · 生产就绪
分层与依赖方向
- 设计定:请求处理分层——传输/控制器层(解析、校验、序列化)、服务/用例层(业务规则、编排、事务边界)、仓库/网关层(持久化与外部调用);依赖方向由外向内,业务层不依赖框架的 HTTP 请求/响应类型。
- 实现红线:控制器不写业务规则、不直接拼 SQL / 调外部 API;服务层不 import HTTP
req/res、不读全局请求上下文;跨层依赖通过接口注入而非具体类硬连,以便服务层用 fake 仓库单测。
app.post('/orders', async (req, res) => {
if (req.body.items.length === 0) return res.status(400).end();
const total = req.body.items.reduce((s, i) => s + i.price, 0);
await db.query('INSERT INTO orders ...', [total]);
});
app.post('/orders', async (req, res) => {
const dto = parseCreateOrder(req.body);
const order = await orderService.create(req.user, dto);
res.status(201).json(toOrderResponse(order));
});
- 证据:服务层用 fake/in-memory 仓库的单测(无需起 HTTP/DB 即可跑);控制器层只测边界翻译与状态码映射。
配置与机密
- 设计定:配置来源(环境变量/配置中心)、必填项清单、各环境差异、机密管理方式;启动时集中校验、缺失即 fail-fast。
- 实现红线:配置集中读取并在启动时校验类型与必填(缺失直接拒绝启动,不在运行期热路径才崩);机密/连接串/URL 不硬编码、不进仓库;
.env 不提交、提供 .env.example 占位;不在代码各处散读 process.env / os.environ。
const url = process.env.DB_URL || 'postgres://localhost/dev';
const jwt = process.env.JWT_SECRET || 'dev-secret';
const config = { dbUrl: required('DATABASE_URL'), jwtSecret: required('JWT_SECRET') };
function required(k: string): string {
const v = process.env[k];
if (!v) throw new Error(`Missing required env: ${k}`);
return v;
}
- 证据:缺必填项时启动失败的测试;机密扫描(仓库与构建产物无明文密钥)。
API 契约
- 设计定:资源命名与 URL 结构、HTTP 方法语义、状态码、统一错误信封、分页/过滤/排序约定、版本策略;接口是组件间契约,变更走
devflow-specify 的 IFR + 基线纪律,列出已知消费者与兼容策略。
- 实现红线:状态码语义化(2xx 成功;400/422 校验、401 未认证、403 无权、404 不存在、409 冲突、429 限流;5xx 服务端错误);错误响应统一信封且 5xx 不泄漏内部细节;破坏性变更走
modify。
HTTP 200 { "success": false, "error": "not found" }
HTTP 404 { "error": { "code": "market_not_found", "message": "Market not found" } }
HTTP 422 { "error": { "code": "validation_failed", "fields": { "name": "required" } } }
错误模型
API 契约管的是对外的线格式;错误模型管的是进程内怎么表达和处理错误,两者要对齐。
- 设计定:领域错误分类(未找到/校验失败/冲突/无权/依赖不可用…)与到 HTTP 状态码的映射表;区分可预期的操作型错误与编程型缺陷。
- 实现红线:抛带类型/错误码的领域错误而非裸
Error('xxx');统一错误处理在边界把领域错误映射成状态码 + 错误信封;编程型异常记录完整上下文并回笼统 5xx,不把堆栈/内部细节返回客户端;不 catch 后静默吞。
throw new Error('user not found');
class NotFoundError extends AppError {
constructor(resource: string, id: string) { super('not_found', 404, `${resource} ${id}`); }
}
throw new NotFoundError('User', id);
- 证据:领域错误→状态码映射有用例覆盖;编程异常路径回 5xx 且不泄漏内部的测试。
数据访问与一致性
- 设计定:事务边界与隔离级别、N+1 规避策略、连接池上限、迁移的前后兼容与回滚。
- 实现红线:多步写入在一个事务内(失败整体回滚)或有显式补偿;列表关联用批量取数消灭 N+1;查询只取需要的列且走索引;schema 迁移可回滚、分步发布。
const orders = await getOrders();
for (const o of orders) o.user = await getUser(o.userId);
const orders = await getOrders();
const users = await getUsers(orders.map(o => o.userId));
const byId = new Map(users.map(u => [u.id, u]));
orders.forEach(o => { o.user = byId.get(o.userId); });
- 证据:关键查询的执行计划/慢查询日志;事务回滚路径有测试;迁移在预生产演练。
幂等与并发
- 设计定:写接口的幂等键、重试语义、乐观锁(版本号)或悲观锁的选型;超卖/重复扣减的防护点。
- 实现红线:非幂等的副作用操作(支付、扣减库存、发消息)有幂等保护;不依赖"客户端不会重试"或"网络不会重复投递"。
async function charge(req) { await payments.create(req.amount); }
async function charge(req) {
const existing = await payments.findByKey(req.idempotencyKey);
if (existing) return existing;
return payments.create({ ...req, key: req.idempotencyKey });
}
- 证据:重复请求/并发请求下的不变量测试(同一幂等键只生效一次)。
认证与授权
- 设计定:认证机制(token/session)、权限模型(RBAC/ABAC);授权检查在服务端每个入口,不只在 UI 隐藏入口。
- 实现红线:不信任客户端声明的身份/角色/资源归属,服务端校验对象级权限;鉴权统一拦截不可绕过;密钥/token/PII 不进日志。
if (req.body.role === 'admin') return allOrders();
return getOrder(req.params.id);
const user = await authenticate(req);
const order = await getOrder(req.params.id);
if (order.ownerId !== user.id && !user.isAdmin) throw new Forbidden();
- 证据:未认证/越权用例返回正确状态码;权限矩阵有测试。安全敏感变更协同
security-review/团队安全负责人。
缓存与失效
- 设计定:缓存层(HTTP/CDN/Redis/进程内)、TTL、失效路径、一致性容忍度(能容忍多旧)。
- 实现红线:写路径同步失效或用短 TTL;不跨用户缓存与身份/权限相关的响应(缓存投毒/越权泄漏);缓存击穿/雪崩有保护(单飞、随机 TTL)。
- 证据:失效路径有测试;缓存命中/未命中行为一致(缓存只影响延迟不影响正确性)。
弹性与依赖容错
- 设计定:每个外部依赖(DB/缓存/下游服务/MQ)的超时值、重试策略(哪些可重试、退避与抖动、上限)、熔断/隔离与降级行为;区分幂等可重试与不可重试。
- 实现红线:所有跨进程调用设显式超时,不用框架默认的"无限等";只对幂等且瞬时的失败重试,用指数退避 + 抖动并设上限;对持续失败的依赖熔断快速失败,避免线程/连接池耗尽;关键依赖不可用时有降级路径而非整体雪崩。
const r = await fetch(url);
for (let i = 0; i < 5; i++) { try { return await call(); } catch {} }
const r = await fetch(url, { signal: AbortSignal.timeout(2000) });
for (let i = 0; i < MAX; i++) {
try { return await call(); }
catch (e) {
if (!isTransient(e) || i === MAX - 1) throw e;
await sleep(base * 2 ** i + jitter());
}
}
限流与过载保护
- 设计定:限流维度(用户/IP/租户/接口)与配额、超额行为、过载时的降级策略。
- 实现红线:生产限流用共享存储,不用进程内计数器;超额返回
429 + Retry-After;关键依赖有超时与熔断。
const hits = new Map<string, number>();
if ((hits.get(ip) ?? 0) > LIMIT) return res.status(429).end();
const n = await redis.incr(`rl:${ip}`);
if (n === 1) await redis.expire(`rl:${ip}`, WINDOW);
if (n > LIMIT) return res.status(429).set('Retry-After', WINDOW).end();
- 证据:限流在多实例下生效的验证;超时/熔断路径有测试。
后台任务与异步
- 设计定:哪些工作移出请求路径(发邮件、生成报表、调慢下游);任务投递语义(至少一次)、重试与死信、可见性超时;worker 与 API 进程分离。
- 实现红线:请求处理器不做长耗时/重 IO 工作,入队交后台;任务幂等(同一任务跑两次结果一致),不假设"恰好一次";失败任务有重试上限 + 死信 + 告警,不静默丢;任务读取自身所需上下文,不依赖请求级状态。
app.post('/signup', async (req, res) => { await createUser(req.body); await sendEmail(); res.end(); });
app.post('/signup', async (req, res) => {
const u = await createUser(req.body);
await queue.add('welcome', { userId: u.id });
res.status(202).end();
});
async function welcome({ userId }) {
if (await alreadySent(userId)) return;
await sendEmail(userId); await markSent(userId);
}
- 证据:任务幂等性测试(重复投递只生效一次);重试到死信的路径有覆盖。
可观测性
- 设计定:结构化日志字段(含 request/trace id)、关键路径指标(延迟、错误率、吞吐)、告警阈值。
- 实现红线:日志结构化且可关联(贯穿 request id);错误记录足够定位的上下文,不静默吞异常;不在日志/指标里写敏感数据。
- 证据:关键路径有日志/指标埋点;错误路径产生可观测信号。
生产就绪
- 设计定:健康检查(liveness)与就绪检查(readiness,含关键依赖探测)、优雅停机流程、CORS 允许来源、安全响应头清单。
- 实现红线:
/health 与 /ready 分离,ready 探测 DB/缓存等关键依赖(依赖不通即不就绪);收到 SIGTERM 先停接新请求、排空在途、再关连接退出,不硬杀在途请求;CORS 用显式来源白名单不用 *(带凭证时尤甚);设安全响应头(HSTS、内容类型嗅探防护、frame 策略等)。
- 证据:readiness 在依赖不可用时返回非 200 的测试;优雅停机不丢在途请求的验证;CORS/安全头配置有检查。停机时序、探针语义与安全头清单见
references/resilience-and-jobs.md。
测试与证据策略
| 层级 | 覆盖什么 | 注意 |
|---|
| 单元 | 业务逻辑、校验、错误映射、权限判定 | 主力层;外部依赖在边界处 mock |
| 集成 | 仓库/DB、事务回滚、缓存、迁移 | 用真实 DB(容器/Dev Services),不 mock 掉被测的持久化语义 |
| 契约/E2E | API 契约、鉴权、限流、幂等 | 在接近生产的环境验证并发与过载相关维度 |
- 一致性/幂等/限流类维度不能只靠单测充数——并发与多实例行为在集成/接近生产环境验证。
- 评审时(
devflow-review):本文件各维度的"证据"项即检查清单;适用维度无证据且无 N/A 理由 → critical。
合理化反驳
| 话术 | 现实 |
|---|
| 「逻辑写控制器里少几层,简单」 | 业务逻辑混入控制器 → 不可复用/不可单测/与框架死耦合;分层是设计红线 |
| 「兜底个默认密钥免得本地起不来」 | 兜底机密会被带进生产;机密缺失就 fail-fast,配 .env.example |
| 「统一返回 200,错误放 body 里前端好处理」 | 状态码是 HTTP 契约;200 包错误破坏缓存/重试/监控语义 |
| 「抛个 Error 带上 message 就够了」 | 裸 Error 导致状态码/信封不一致、易泄漏内部;用类型化领域错误 + 统一映射 |
| 「下游应该不会依赖这个错误码」 | 可观察的接口语义都有消费者;变更走基线 + 列消费者 |
| 「客户端不会重复提交,不用做幂等」 | 网络重试与重复投递必然发生;副作用操作必须幂等 |
| 「调下游不设超时,正常都很快」 | 下游一抖动,无超时调用就耗尽连接/线程;跨进程调用必设超时 + 退避重试 |
| 「邮件在请求里发了就完事」 | 慢/易失败的副作用应入队后台并做幂等,否则请求超时且重投重复 |
| 「先放宽权限跑通,上线前收紧」 | 越权是高危事故;对象级鉴权从第一天起,且服务端权威 |
| 「限流用内存计数器够了」 | 多副本/无服务器下进程内计数器失效;用共享存储 |
| 「N+1 数据量小,先不管」 | 数据量会增长;N+1 是规模化下的典型拖垮点,设计阶段消灭 |
自检清单
参考