在运行的系统上生长新功能

3 min read
Zekari
系统设计架构Purikura 项目渐进式演进

扩展的两条路

系统在运行,用户在使用。现在需要一个管理后台。

第一条路是重构。停下来,重新设计架构,统一新旧功能。这条路的诱惑在于"一次性解决所有问题"。但重构通常意味着风险集中,意味着长时间里用户看不到任何改进。

第二条路是生长。不碰核心,在边缘扩展。管理功能作为观察者和调节器附着在现有系统上,监测它,影响它,但不改变它的运行逻辑。

这不是妥协,而是理解:运行中的系统有它自己的平衡。打破这个平衡需要巨大的成本,而大部分时候,新功能并不需要这样的代价。

💡 Click the maximize icon to view in fullscreen

关键是依赖的方向:管理层依赖核心,核心不依赖管理层。这保证了即使管理功能全部失效,用户服务仍能正常运转。

权限的最小形态

管理后台需要权限控制。传统方案是建立 RBAC 系统:角色、权限、资源、操作,四个维度的组合。

但等等,真的需要这么复杂吗?

从实际需求倒推:有人需要看数据但不能改(观察者),有人需要操作用户和内容(管理员),有人需要完整控制权(超级管理员)。三个角色已经覆盖了所有场景。

不建新表,在现有 profiles 表上加一个字段:

ALTER TABLE profiles ADD COLUMN admin_role TEXT;
CREATE TYPE admin_role_enum AS ENUM ('viewer', 'admin', 'super_admin');

权限验证作为中间件,插入现有路由:

async function withAdminAuth(requiredRole = 'viewer') {
  const user = await getUser(token);
  if (!user.adminRole) throw new Error('Not admin');

  const roleLevel = { viewer: 1, admin: 2, super_admin: 3 };
  if (roleLevel[user.adminRole] < roleLevel[requiredRole]) {
    throw new Error('Insufficient permissions');
  }

  await next();
}

这个设计的本质:权限不是独立的系统,而是用户身份的扩展属性

不需要额外查询,不需要复杂连表。普通用户的 admin_roleNULL,不影响现有逻辑。管理员的权限信息随用户信息一起读取,零额外开销。

复杂的权限系统看起来强大,但维护成本也随之上升。每增加一个维度,复杂度不是线性增长,而是指数增长。

问自己:这个复杂度是真实需求驱动的,还是为了"可能的未来需求"?

大部分时候,三个角色已经足够。如果未来真的需要更细粒度的控制,再扩展 admin_permissions JSONB 字段也不迟。

数据的第二次使用

监控面板需要数据。最直接的想法是:建立新的监控表,定时采集系统指标,存储时间序列数据。

但系统已经在记录日志:任务执行记录、厂商健康检查、退款统计。这些表已经包含了监控需要的所有信息,只是之前没有界面展示。

关键洞见:监控不需要新的数据源,只需要新的查询方式

-- 不建新表,只优化现有表的查询
CREATE INDEX idx_ai_generation_logs_event_time
  ON ai_generation_logs(event_type, created_at DESC);

-- 复杂聚合通过 RPC 函数实现
CREATE FUNCTION get_vendor_stats_7days()
RETURNS TABLE (vendor_id TEXT, error_rate NUMERIC) AS $$
  SELECT vendor_id,
    COUNT(CASE WHEN status = 'failed' THEN 1 END)::NUMERIC /
    NULLIF(COUNT(*), 0) AS error_rate
  FROM ai_generation_logs
  WHERE created_at >= NOW() - INTERVAL '7 days'
  GROUP BY vendor_id;
$$ LANGUAGE sql;

数据被使用了两次:第一次是业务逻辑记录日志,第二次是管理面板查询展示。没有重复存储,没有同步问题,没有额外成本。

这种设计的前提是:日志表记录了监控所需的维度

完整的日志至少包含:状态(成功/失败)、时间戳(趋势分析)、关联ID(追踪上下文)、耗时(性能监控)。如果日志只记录成功不记录失败,错误率无法统计。如果日志不记录时间,趋势分析无法进行。如果日志不关联用户或任务ID,问题无法定位。

好的日志设计不只是为了当下的调试,而是为了未来可能的各种使用方式。相关思考见 调用链追踪的设计

告警的克制艺术

Telegram 告警看起来简单:错误发生时发送消息。但这样的系统会迅速失控。

没有克制的告警系统会变成噪音制造机。同一个问题在五分钟内触发二十次告警,半夜被相同的错误消息轰炸。人会学会忽略告警,这比没有告警更危险。

关键是理解:不是所有错误都需要告警

const alertRules = {
  highErrorRate: {
    threshold: 0.1,     // 10% 错误率才告警
    window: 300,        // 5分钟窗口
    cooldown: 1800      // 30分钟内不重复告警
  },
  vendorDown: {
    threshold: 3,       // 连续3次失败才告警
    cooldown: 3600      // 1小时冷却期
  }
};

冷却期(cooldown)是核心机制。它不是为了隐藏问题,而是为了保持告警的有效性。当你看到告警时,你知道:这是新问题,或者旧问题恶化了,需要立即处理。

告警的哲学:只告知需要人工介入的情况

大部分错误应该由系统自动重试解决。只有重试失败,或者影响扩大到阈值以上,才需要通知人。错误是常态,告警应该是例外。

好的告警系统让你安心睡觉,而不是让你时刻紧张。

如果告警太频繁,人会学会忽略它。当真正严重的问题出现时,你已经习惯性地把消息划掉了。

告警的价值在于信噪比。每一条告警都应该意味着:"现在就需要处理。"

批量操作的边界

批量重试看起来高效:一次操作处理几十个失败任务。但批量操作很危险。

如果逻辑有问题,破坏也是批量的。一次操作影响一百个任务,如果失败,损失也是一百倍。

所以设计必须有严格的限制:

const LIMITS = {
  maxBatchSize: 50,      // 最多50个
  maxConcurrent: 10,     // 最多10个并发
  timeoutPerTask: 30000  // 每个30秒超时
};

这些数字从实际约束倒推:

批量上限50个:DLQ 中的任务通常不超过百个量级。50个能处理大部分场景(覆盖约50-80%的积压),但失败时影响可控。如果设100个,单次失败影响过大;如果设20个,处理效率太低,需要频繁操作。

并发限制10个:AI 厂商的速率限制通常是10-20 req/s。10个并发不会触发限流,同时保证批量操作不会瞬间产生流量尖峰。

超时30秒:单个 AI 任务正常耗时5-15秒,30秒超时能容忍网络波动,但避免慢任务阻塞整个批次。

关键原则:即使失败,也要能安全失败

批量操作不应该全有或全无。处理50个任务,如果其中10个失败,应该返回详细结果:哪些成功了,哪些失败了,为什么失败。操作者可以根据结果决定下一步:是重试失败的,还是检查代码逻辑。

相关模式见 故障隔离的实现

缓存的分层逻辑

管理查询可能很重:聚合大量日志,计算多维度统计。不能每次请求都执行,需要缓存。

但缓存不是简单的"存起来"。不同数据有不同的新鲜度要求。

class CacheManager {
  async get(key, fetcher, ttl) {
    // L1: 请求内存(毫秒级生命周期)
    if (this.memory[key]) return this.memory[key];

    // L2: KV 缓存(全球分布)
    const cached = await kv.get(key);
    if (cached && !isStale(cached, ttl)) {
      this.memory[key] = cached.data;
      return cached.data;
    }

    // L3: 数据源
    const data = await fetcher();
    await kv.put(key, data, { expirationTtl: ttl });
    this.memory[key] = data;
    return data;
  }
}

不同类型的数据,不同的 TTL:

  • 用户信息:5分钟。用户的积分、权限变化不频繁,管理员查看时允许几分钟延迟。
  • 任务统计:1分钟。反映当前系统负载,需要相对新鲜的数据,但不必秒级刷新。
  • 历史趋势:1小时。过去一周的成本曲线,数据已固化,长缓存不影响准确性。

关键理解:管理后台通常不需要实时数据

几分钟的延迟对大部分管理操作是可接受的。查看用户列表、分析成本趋势,这些场景不像用户下单那样要求毫秒级响应。这个认知让缓存策略变得简单:大胆地缓存,适当的 TTL,用性能换取开发复杂度的降低。

真正需要实时的场景(比如监控告警触发),通过 Supabase Realtime 绕过缓存直接推送。相关实现见 Redis 连接事件处理

审计的静默记录

所有管理操作都需要记录,但记录不应该阻塞业务。

async function updateUserCredits(c, userId, credits) {
  // 获取原始数据
  const { data: oldProfile } = await supabase
    .from('profiles')
    .select('video_credits')
    .eq('id', userId)
    .single();

  // 业务操作
  const result = await supabase
    .from('profiles')
    .update({ video_credits: credits })
    .eq('id', userId);

  // 异步记录审计日志(不阻塞响应)
  c.executionCtx.waitUntil(
    logAdminAction({
      action: 'update_credits',
      resourceId: userId,
      changes: { from: oldProfile.video_credits, to: credits }
    })
  );

  return result;
}

审计日志的价值不在于实时性,而在于完整性。当你需要回溯某个操作时,信息就在那里。当日志写入失败时,业务操作不应该回滚。

这看起来不够严谨:如果日志失败了,操作就没有记录。但在实践中,这是正确的权衡。管理操作本身比它的记录更重要。如果因为日志系统故障导致操作失败,用户会更不满意。

关键是监控审计系统本身:如果日志写入持续失败,应该触发告警,而不是阻塞所有管理操作。

渐进的节奏

第一阶段构建基础:权限系统、核心 API、基本监控面板。完成后,系统可用。管理员能看到数据,能执行基本操作。

第二阶段增强能力:任务管理、告警系统、高级分析。完成后,运维效率显著提升。很多问题能自动发现,批量操作减少重复劳动。

第三阶段是规划中的智能化:基于历史数据的告警优化、预测性分析、自动化运维。但这些是未来的方向,不是当下的必需。

每个阶段都独立有价值。不是"完成全部才能用",而是"每一步都让系统变得更好"。

这种节奏的本质:只做当下最需要的

第二阶段的很多功能(批量重试、告警冷却期、成本分析)都是在第一阶段使用后才发现需要的。如果一开始就规划完整,可能会做很多最终用不到的功能。更糟的是,可能会误判优先级,先做不重要的,后做真正需要的。

软件是活的,需求是动态的。渐进式不是妥协,而是承认:我们无法预见所有需求,但可以快速响应真实需求。

相关方法论见 从意图到架构

每次迭代后,问自己:如果停在这里,系统能用吗?

如果答案是"还差一点",说明步子迈得太大了。好的渐进是:每一步都是完整的,下一步是增强,而不是补全。

生长的本质

在运行的系统上构建管理层,核心不是技术选择,而是理解:

尊重边界。管理层观察和调节核心,但不改变核心逻辑。依赖是单向的。

复用数据。不建新表,不建新的数据管道。在现有数据上找到新的查询方式。

克制告警。只告警需要人工介入的情况。保持信噪比,而不是记录所有异常。

限制批量。批量操作有明确上限。限制不是妥协,而是安全失败的保障。

分层缓存。不同数据有不同的新鲜度要求。管理后台不需要实时数据。

静默审计。记录不阻塞业务。监控审计系统本身,而不是让它阻止业务操作。

渐进节奏。每个迭代独立有价值。在使用中发现下一步需求,而不是预见完美。

Purikura 的 CMS 系统不是一次性设计出来的,而是在生产环境中生长出来的。它从最小的权限验证开始,逐步加入监控、告警、分析。每一步都解决了真实的问题,每一步都建立在前一步的基础上。

这才是扩展的真实样子:不是推倒重来,而是理解边界,找到生长点,让新功能自然地附着在旧系统上。

Related Posts

Articles you might also find interesting

第三方回调的状态映射完整性

5 min read

KIE.AI 视频生成的三个修复揭示了一个本质问题:回调不只是接收结果,是建立状态映射。没有 vendor_task_id,系统就失去了追溯能力。

Purikura 项目系统设计
Read More
Featured

定价界面优化的三层方法

4 min read

数据诚实、决策引导、视觉精调——三层递进的优化方法。从移除虚假功能到帮助用户选择,再到像素级修复,每一步都在解决真实问题

UI/UX定价策略
Read More

管理后台需要两次设计

3 min read

第一次设计回答"发生了什么",第二次设计回答"我能做什么"。在第一次就试图解决所有问题,结果是功能很多但都不够深入。

系统设计API 设计
Read More

告警分级与响应时间

2 min read

不是所有问题都需要立即响应。RPC失败会在凌晨3点叫醒人。安全事件每15分钟检查一次。支付成功只记录,不告警。系统的响应时间应该匹配问题的紧急程度。

系统设计监控
Read More

文档标准是成本计算的前提

3 min read

API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。

API文档
Read More

BullMQ 队列

3 min read

队列不是技术选型,而是对时间的承认,对顺序的尊重,对不确定性的应对

系统设计异步处理
Read More

BullMQ Worker

2 min read

Worker 的本质是对时间的重新分配,是对主线的解放,也是对专注的追求

系统设计异步处理
Read More

调用链路追踪法:从断点到根因

2 min read

功能失效的背后,是一条完整的调用链路。追踪这条链路,定位断点,才能从根本上解决问题。

架构调试
Read More

离屏渲染:照片捕获为什么需要独立的 canvas

2 min read

实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。

架构设计前端开发
Read More

集中式配置:让 Reddit 组件脱离重复泥潭

2 min read

当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。

架构设计React组件
Read More

配置不会自动同步

2 min read

视频生成任务永远pending,代码完美部署,队列正确配置。问题不在代码,在于配置的独立性被低估。静默失败比错误更危险。

部署配置管理
Read More

CRUD 操作

2 min read

四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑

系统设计软件工程
Read More

数据库参数国际化:从 13 个迁移学到的设计原则

3 min read

数据不该懂语言。当数据库参数嵌入中文标签时,系统的边界就被语言限制了。这篇文章从 13 个参数对齐迁移中提炼出设计原则——国际化不是功能,是系统设计的底层约束。

数据库国际化
Read More

Stripe Webhook中的防御性编程

2 min read

三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。

Web开发系统设计
Read More

让文档跟着代码走

2 min read

文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。

文档软件工程
Read More

双重导出管道的架构选择

2 min read

在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。

架构设计图像导出
Read More

双重验证:Stripe生产模式的防御性切换

7 min read

从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。

系统设计Stripe
Read More

错误隔离

3 min read

失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。

系统设计可靠性工程
Read More

Purikura的页面系统

3 min read

通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。

架构设计React
Read More

从意图到架构

3 min read

技术方案不是设计出来的,而是从问题中涌现的。理解这个过程,就理解了软件设计的本质。

软件设计架构
Read More

重复数据的迁移实践:从 N 个文件到 1 个真相源

3 min read

当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。

架构设计配置管理
Read More

实现幂等性处理,忽略已处理的任务

3 min read

在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解

系统设计并发控制
Read More

单例模式管理 Redis 连接

5 min read

连接不是技术细节,而是系统与外部世界的第一次握手,是可靠性的起点

系统设计后端架构
Read More

分层修复

3 min read

生产问题没有银弹。P0 止血,P1 加固,P2 优化。优先级不是排序,而是在不确定性下的决策框架。

工程实践问题修复
Read More

缺失值的级联效应

3 min read

一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。

系统设计防御性编程
Read More