DarkroomFlow的五道工序已经具备顺序约束、暂停恢复、重复确认保护、计划与实际双轨耗时,以及宽屏工作台。前一篇把定影运行到05:30时的超时状态与最终完成确认分开:显影计划08:00、实际08:40,停止计划00:30、实际00:45,两道工序已完成;定影原计划05:00,已经有效运行05:30,仍处于运行中;水洗10:00、干燥20:00等待后续确认。这个会话的模板总时长为43:30,当前修订号为9,预测全程耗时44:55。

当新胶片处理流程调整了定影建议时长,最容易发生的数据问题是历史会话被新配置悄悄改写。例如新模板将定影从05:00调整为06:00。如果读取老记录时永远使用最新模板,老会话中的05:30会突然从“超时00:30”变成“尚未达到计划”,预计总时长也会变化,用户甚至可能以为过去的记录发生了回退。必须把“模板升级”和“已有会话的事实”分开治理。

暗房工序模板升级与历史会话校验

模板是版本化的规则,会话是锁定的事实

当前产品存在两套计划。V1的五项时长依次为480、30、300、600、1200秒,总计2610秒,也就是43:30;V2仅将定影从300秒调整到360秒,其他四项完全相同,总时长2670秒,即44:30。两套都使用同一组稳定工序ID:develop、stop、fix、wash、dry。ID用于事件关联与数据检查,中文名称只是UI的显示文本,不用于查找。

会话创建时写入模板版本和一份计划秒数快照,后续运行从快照读取,不根据当前全局模板动态替换。这样V1已有会话仍按定影五分钟处理,虽然新的V2计划是六分钟。切换模板选择项也不会触碰原会话的运行片段、完成数量、修订号、已处理操作令牌和计划结算字段。新模板只对新建会话生效,保证编辑配置与业务历史不互相污染。

export type StepId = 'develop' | 'stop' | 'fix' | 'wash' | 'dry';
export interface PlanStep {
  id: StepId;
  name: string;
  plannedSeconds: number;
}
export interface ProcessTemplate {
  version: number;
  steps: PlanStep[];
}
export const TEMPLATE_V1: ProcessTemplate = {
  version: 1,
  steps: [
    { id: 'develop', name: '显影', plannedSeconds: 480 },
    { id: 'stop', name: '停止', plannedSeconds: 30 },
    { id: 'fix', name: '定影', plannedSeconds: 300 },
    { id: 'wash', name: '水洗', plannedSeconds: 600 },
    { id: 'dry', name: '干燥', plannedSeconds: 1200 }
  ]
};
export const TEMPLATE_V2: ProcessTemplate = {
  version: 2,
  steps: TEMPLATE_V1.steps.map((item: PlanStep): PlanStep => ({
    id: item.id, name: item.name,
    plannedSeconds: item.id === 'fix' ? 360 : item.plannedSeconds
  }))
};
export function getTemplate(version: number): ProcessTemplate | null {
  if (version === 1) return TEMPLATE_V1;
  if (version === 2) return TEMPLATE_V2;
  return null;
}
export function planTotal(items: PlanStep[]): number {
  return items.reduce((sum: number, item: PlanStep) =>
    sum + item.plannedSeconds, 0);
}

版本号采用整数,避免靠文件名或发布日期推断规则。模板对象应该在版本发布后保持不可变;开发中若修改V1数组,将导致同一版本在不同时期含义不同。getTemplate遇到未知版本返回空值,由读取层给出待校验状态,而不是直接降级到V1。对V2的修改也应当通过生成新数组完成,避免在创建模板时共享了可变对象引用。

会话快照必须有足够的自解释字段

历史存档不宜只保存当前工序索引和倒计时文字。至少应包含数据格式版本、模板版本、实际锁定的五道工序计划值、会话修订号、当前阶段、已完成的工序实际秒数,以及进行中阶段的累计有效毫秒数。完成状态由事件确认产生,进行中的耗时只参与预测,不写入已完成实际账本。老会话当前两个已完成实际分别为520秒与45秒,定影已经累计330秒但尚未确认。

加载时先检查数组是否五项、ID顺序是否固定、计划秒数是否为正整数,再比对当前记录的模板版本。若存在模板版本且能找到对应模板,仍应核验快照与该版本规则是否一致,防止损坏记录被当作正常业务。对于少量旧版格式没有模板版本的记录,可以先匹配其计划时长签名;完整符合V1时才明确归为V1,其余情况保留原数据并提示处理,而不是默认补齐字段覆盖原记录。

export interface FlowSnapshot {
  formatVersion: number;
  templateVersion: number;
  revision: number;
  stepIndex: number;
  phase: 'ready' | 'running' | 'paused' | 'awaitingConfirm';
  plans: PlanStep[];
  actualByStep: (number | null)[];
  activeElapsedSeconds: number;
}
const STEP_IDS: StepId[] = ['develop', 'stop', 'fix', 'wash', 'dry'];
export function plansMatch(a: PlanStep[], b: PlanStep[]): boolean {
  if (a.length !== 5 || b.length !== 5) return false;
  for (let i: number = 0; i < 5; i++) {
    if (a[i].id !== STEP_IDS[i] || b[i].id !== STEP_IDS[i] ||
        !Number.isInteger(a[i].plannedSeconds) ||
        a[i].plannedSeconds <= 0 ||
        a[i].plannedSeconds !== b[i].plannedSeconds) return false;
  }
  return true;
}
export function validateSnapshot(data: FlowSnapshot): string {
  const reference: ProcessTemplate | null = getTemplate(data.templateVersion);
  if (!reference) return 'unknown-template';
  if (data.formatVersion !== 2 || !plansMatch(data.plans, reference.steps)) {
    return 'invalid-plan';
  }
  if (!Number.isInteger(data.revision) || data.revision < 0 ||
      !Number.isInteger(data.stepIndex) || data.stepIndex < 0 ||
      data.stepIndex >= 5 || data.actualByStep.length !== 5) {
    return 'invalid-state';
  }
  if (data.actualByStep.some((value: number | null) =>
      value !== null && (!Number.isInteger(value) || value < 0))) {
    return 'invalid-actual';
  }
  return 'ok';
}

上面的结构体现了版本识别和基础校验;在业务服务中还需要将每一项实际耗时与阶段完成事件相互校对,例如定影仍运行时对应实际应是空值,已完成两项的真实耗时才允许进入结算。activeElapsedSeconds须为非负整数,暂停恢复时由之前的计时服务恢复有效累计,再根据页面生命周期控制刷新资源。外部数据读取失败时应保留原始文件内容供用户重试,不在异常处理里执行空记录覆盖。

旧格式迁移先识别,再提交

早期会话可能只存了五项计划和运行状态,字段中没有formatVersion与templateVersion。迁移不能单凭“看见五项”就直接生成V1,因为未来可能有一套不同顺序的自定义模板。更稳妥的方式是先把旧计划与已知V1做逐项匹配,检查稳定ID和秒数;符合后填入版本信息,连同旧记录内容构造新的快照。这里的“迁移”应是一次明确的数据转换,写入新格式需要在成功校验后进行,并保留能够回退到旧数据的路径。

export interface LegacyFlow {
  revision: number;
  stepIndex: number;
  phase: 'ready' | 'running' | 'paused' | 'awaitingConfirm';
  plans: PlanStep[];
  actualByStep: (number | null)[];
  activeElapsedSeconds: number;
}
export function upgradeLegacy(old: LegacyFlow): FlowSnapshot | null {
  if (!plansMatch(old.plans, TEMPLATE_V1.steps)) return null;
  const converted: FlowSnapshot = {
    formatVersion: 2, templateVersion: 1,
    revision: old.revision, stepIndex: old.stepIndex,
    phase: old.phase,
    plans: old.plans.map((step: PlanStep): PlanStep => ({
      id: step.id, name: step.name, plannedSeconds: step.plannedSeconds
    })),
    actualByStep: [...old.actualByStep],
    activeElapsedSeconds: old.activeElapsedSeconds
  };
  return validateSnapshot(converted) === 'ok' ? converted : null;
}
export function newSessionWithV2(): FlowSnapshot {
  return {
    formatVersion: 2, templateVersion: 2,
    revision: 0, stepIndex: 0, phase: 'ready',
    plans: TEMPLATE_V2.steps.map((step: PlanStep): PlanStep => ({
      id: step.id, name: step.name, plannedSeconds: step.plannedSeconds
    })),
    actualByStep: [null, null, null, null, null],
    activeElapsedSeconds: 0
  };
}

使用者点击“按V2新建会话”时,调用newSessionWithV2创建一份全新计划,修订号从零开始。旧V1会话不应因为打开了比较页就把修订号9更新为10,也不会将定影计划时长从300秒改成360秒。V1的显影、停止两项实际秒数仍是520和45,历史完成偏差为+00:55;定影仍运行05:30,当前超时+00:30,预测44:55。新的V2尚未开始,只有计划总长44:30,没有历史实际耗时。

版本路由、快照校验与双模板界面

UI读的是会话快照,不是最新配置

页面可以在上方显示两个模板配置摘要,下方固定显示当前会话。当前会话卡片写清“V1锁定”“定影进行中”“计划05:00”“已运行05:30”;新建区域写明“V2仅影响新会话”“定影06:00”“总计划44:30”。若扫描到未知V3或结构有损坏的记录,卡片状态为“待校验”,其启动与确认按钮保持禁用,直到有明确的兼容处理策略。将未知记录自动转换成最新模板会掩盖原始问题。

宽屏工作台仍复用第五篇的主从布局:左侧选择会话,右侧查看锁定计划与工序统计;手机页面则让模板比较和会话信息上下排列。变化的是展示容器,而不是会话的唯一数据源。选中会话应以稳定会话键维护,页面尺寸变化不改变当前工序,也不产生新版本写入。若多处页面同时编辑同一会话,第三篇的修订号和操作令牌校验仍需要在写入端执行,不能依赖按钮灰化来保障一致性。

用确定样本核对升级后的计算

应将V1和V2分别覆盖三个数字检查:旧模板总计2610秒;新模板总计2670秒;差额60秒恰好来自定影。老会话用V1解释,定影已用330秒,比原计划300秒超出30秒;已完成两项的偏差55秒,加总得到暂时的85秒预测偏差,因此整段预测时间是2610加85,即2695秒,格式为44:55。如果把同一份老记录按V2误读,定影便会变成尚未完成计划,这就是必须阻断的版本漂移。

输入校验还要覆盖负数时长、重复工序ID、错序、未知版本、缺少实际耗时数组和异常阶段索引。每一种情况都应留下明确的错误类型,供页面提示与排查。数据层先保证能判定、能拒绝,再考虑增加新的配置编辑能力;否则模板扩展越多,历史流程的含义越难保持一致。

旧V1会话与V2新模板的手机端对照

这套版本路由没有替换任何系统级工序能力,全部围绕ArkTS业务数据的版本契约和ArkUI界面展开。模板比较可继续参考华为开发者的ContainerReader容器断点,数据类型与函数组织参考ArkTS开发概览。下一篇进行历史归档时,每条记录已经携带可追溯的模板版本、计划快照和实际时间,归档页便能按历史发生时的规则还原流程,而不是用今天的配置重新解释昨天的数据。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐