优化:采集替代商品按钮不再按错误码白名单限制,交由人工判断 #184

Open
opened 2026-09-01 08:56:12 +08:00 by ila · 4 comments
Owner

Gitea MCP 未向当前会话暴露,按仓库规则回退项目根目录安全配置与 Gitea API 维护本工单;凭据未写入工单、代码或日志。

2026-09-01 范围修订一(已确认):用户进一步要求去掉「任务状态必须为 failed」这道门槛——只要工人通过其他手机/网页验证过 PDD 商品确实失效,或单纯找到了更好的替代品,即便当前采集任务本身是成功的,也应该能从其任务详情发起替换。用户已确认第 1、2 点:去掉 failed 限制、确认弹窗文案追加「会取消该商品下尚未开始执行的采购任务」的明确提示。

2026-09-01 范围修订二(已确认):用户进一步要求,若该 PDD 商品名下存在正在采购(running/order_submit_started)的任务,不允许替换。核验后一并收紧为:running、order_submit_started、order_result_unknown(订单结果未知、需人工核对,同样敏感)三种状态命中任意一个即禁止替换,作为新增的硬门槛,不交给人工临场判断——这与范围修订一放宽的两道门槛(错误码、任务失败状态)性质不同:修订一放宽的是需要人工判断的软信号,本次收紧的是系统自身已经确定的硬事实(是否有采购正在执行或结果待核对)。以下正文已按此修订。

原始需求摘要

来源:用户于 2026-09-01 反馈采集任务 110 失败原因为「未找到 PDD 商品规格入口」(实际是链接失效被跳转到 PDD 首页),但任务详情里没有出现「采集替代商品」按钮。核验后确认根因是服务端按错误码白名单判断是否显示该按钮,白名单覆盖不了这类场景。用户明确要求:不应该用错误码白名单限制过多,任务失败了就应该显示该按钮,交给工人判断失败原因、决定是否需要替换商品;并要求单独建单,不并入 #165 或 #178。

目的:让「采集替代商品」的可见性既不依赖一份不完整的错误码白名单,也不强制要求当前任务本身处于失败状态,改为把"这个 PDD 商品是否值得替换"的判断完全交给人工——判断依据可以来自 Agent 自身的失败记录,也可以来自外部验证(其他手机、网页)或单纯发现了更好的商品;同时保留必要的状态机保护,针对新引入的行为(对健康商品发起替换会取消其未执行的采购任务)加强确认提示,并新增一道系统硬门槛:该商品名下有采购任务正在执行或结果待核对时,直接禁止替换,不留给人工临场判断,避免放宽准入扩大误触代价。

基线与已核实事实

代码基线:4343ab5(2026-09-01)。核验日期 2026-09-01。功能来源工单 #130(33d4bb1 feat(#130): collect replacement products from Agent),后续 #132、#155、#157、#174 在同一路径上有过维护,本单是对 #130 既有准入条件的一次收窄反转。

  • 显示条件全部集中在 server/app/goauto/replacement/origin.go:InspectOrigin(采集来源 originType=="collection" 分支,:37-46):
    1. task.PDDProductID != nil(:65-68,否则 DisabledReason="原任务没有可替换的拼多多商品")——本单保留;
    2. 该 PDD 商品当前没有处于 active 状态的替换记录(activeForOrigin,:80-83,否则 DisabledReason="该商品已经发起替换")——本单保留;
    3. 该采集任务本身没有一条待处理/失败的替换激活记录(:78-91)——本单保留;
    4. task.Status == failed(:104-106,否则 DisabledReason="只有失败任务可以替换商品")——本单去除;
    5. task.ErrorCode 精确等于 PDD_LINK_INVALID 或 PDD_GOODS_SOLD_OUT(:108-110,ErrorPDDLinkInvalid/ErrorPDDGoodsSoldOut 常量定义于 origin.go:15-16,仅在此处被引用,无其他耦合);否则 DisabledReason="当前失败原因不属于商品失效或售罄"——本单去除。
  • Android 端不需要为此新增入口:renderCollectionDetail(TaskHistoryFragment.kt)当前对任意任务状态都无条件调用 renderReplacementAction,能否显示按钮完全由服务端 InspectOrigin 的返回值决定;去掉第 4、5 两道门槛后,成功任务的详情页会自然出现该按钮,无需额外的前端改动。
  • Android 端 TaskHistoryFragment.kt:92-100(ReplacementActionPolicy.presentation):eligible=false 且无 mappingStatus/activationStatus 时,呈现结果为 HIDDEN,按钮直接不渲染,不显示任何理由文字(:99「ReplacementPresentation.HIDDEN -> Unit」)。这是造成用户「看不出为什么按钮不见了」的直接原因。
  • 采集任务 110 的失败错误码属于通用兜底(如 #165 讨论的 PDD_DETAIL_ENTRY_FAILED 一类),不在上述第 5 条白名单内,因此即便真实原因是商品失效,按钮也不出现。
  • 该按钮触发的动作影响范围较大,已在 replacement/activation.go:239-289 核实:批量把当前挂在该 PDD 商品下的所有虾皮商品改指向新商品并清空其规格映射、取消相关未开始的采购任务、把旧 PDD 商品标记停用,一个事务内批量完成。
  • 去掉 failed 限制后新增的真实后果:此前"取消未开始执行的采购任务"这一步隐含的前提是"商品已经失败,挂在它上面的采购任务本来也成不了";一旦允许对状态正常、甚至采购中的健康商品发起替换,这一步会变成对着完全正常的商品静默取消其未执行的采购任务——这是本次放宽新引入的行为,此前被"必须先失败"这个前提掩盖,现有确认弹窗文案完全没有提及,必须在本单一并加强提示。
  • 现有前端确认弹窗(TaskHistoryFragment.kt:809-818,confirmReplacement)文案仅为「用当前商品替换 #N 的失效商品?」,未提示上述批量影响范围。
  • CorrectAndActivate(replacement/activation.go:70+)已存在,用于纠正误操作的替换记录,说明项目本身已经预期这类操作可能判断有误、需要纠正路径,但纠正本身仍是有成本的额外操作,不能作为放宽准入的免责理由。
  • 已核实:正在执行的采购任务本身机制上是安全的,但整个替换动作不是。applyPurchaseReplacement(activation.go:309-330)已经区分处理:pending/spec_probe_pending 取消;running/order_submit_started 跳过不动(SkippedRunningTasks);其余终态保留不动。核对 purchase/lifecycle.go 的 Claim/Start/Result 全链路,均不重新校验 pdd.Status,因此被跳过的正在执行任务不会被中途打断。但 activate() 仍会无条件把源 PDD 商品标记为 disabled、把该商品下全部虾皮商品关联批量改到新商品(activation.go:255-293)——这一步和"是否有采购正在执行"完全无关,即便任务本身不受影响,也会在一笔采购正处于执行、尤其是刚越过 order_submit_started 这个最接近创建订单的不可逆边界时,制造一个货不对板的时间窗口:任务用旧商品的快照继续跑,但商品本身已经被标记废弃、关联已经改走,一旦该任务紧接着成功创建订单,会让审计和后续排查变得混乱。
  • models.PurchaseTaskStatusRunning、PurchaseTaskStatusOrderSubmitStarted、PurchaseTaskStatusOrderResultUnknown 均已在 models/purchase.go:19-23 定义,无需新增状态常量。

判断边界

  • 已确认:错误码白名单准入方式与项目「证据不足交给人工,不替使用者瞎猜」的一贯原则相悖,用户明确要求去除。
  • 已确认(2026-09-01 修订):放宽后只保留两道硬门槛——PDD 商品必须存在(task.PDDProductID != nil)、该 PDD 商品当前无进行中的替换——因为它们是纯状态机保护,不涉及对失败原因或商品价值的判断。任务状态为 failed 不再作为门槛,用户已明确要求去除。
  • 已确认:放宽会扩大误触代价(批量改关联、清映射、取消采购任务——且去掉 failed 限制后取消采购任务这一步可能作用于健康商品),因此确认弹窗文案必须加重,明确列出「会取消该商品下尚未开始执行的采购任务」,作为放宽准入的配套措施,不是可选项,用户已在 2026-09-01 确认此项。
  • 尚未确认:原任务没有可替换的拼多多商品(task.PDDProductID == nil)这一门槛是否也需要复核。经核实,采集任务从创建起就必须关联一个 PDD 商品(无论是新建还是复用),正常情况下该字段不会为空;本单不改变这一条。
  • 已确认(2026-09-01 修订二):新增门槛不属于"可以放宽、交给人工判断"的范畴——它是系统已经确定的硬事实(是否有采购正在执行、结果是否待核对),不接受人工绕过;发起人若确实需要在这种情况下替换,必须等对应采购任务结束(成功、失败或取消)后再操作,不提供强制跳过的入口。

目标

  1. 「采集替代商品」按钮的显示与否,不再依赖 ErrorCode 是否命中白名单,也不再要求任务状态必须为 failed;只要该 PDD 商品存在、当前没有进行中的替换、且没有正在执行或结果待核对的采购任务,任意状态(成功或失败)的采集任务详情都能发起替换,把"这个商品是否值得替换"的判断交给操作人工,同时用系统硬事实挡住最危险的时间窗口。
  2. 保留 task.PDDProductID != nil、无进行中替换这两条既有状态机保护,不再保留 failed 状态要求;新增第三条硬门槛:该 PDD 商品名下不存在 running、order_submit_started、order_result_unknown 状态的采购任务。
  3. 确认弹窗文案加重,明确告知:此操作会把当前关联该 PDD 商品的全部虾皮商品改指向新商品、清空它们的规格映射、取消该商品下尚未开始执行的采购任务,操作后需要重新完成规格映射;这条提示不因原任务是否失败而省略。
  4. 采购来源(originType=="purchase")分支是否同步放宽,本单需要用户单独确认(详见「非目标」)。
  5. 确认弹窗新增第三层提示:因 running/order_submit_started/order_result_unknown 被拦截时,明确告知"该商品有采购任务正在执行或结果待核对,暂不能替换,请稍后重试",与前两条放宽引入的提示(批量改关联/清映射、取消未执行采购任务)在同一处按场景分别展示,不混在一句话里。

非目标

  • 不改变采购来源(originType=="purchase")分支的准入条件。采购失败与采集失败的业务后果不同(采购失败可能已经产生过订单尝试),是否同步放宽需要单独评估,本单只处理采集来源分支,除非用户在确认阶段明确要求一并处理。
  • 不改变「该 PDD 商品当前无进行中的替换」「PDD 商品必须存在」这两条门槛。任务状态必须为 failed 不属于本条非目标,已按用户要求去除,见「目标」;新增的"无正在执行/结果待核对采购任务"门槛属于本单新增范围,见「目标」第 2 条。
  • 不提供绕过"有采购任务正在执行/结果待核对"这道新门槛的入口(如强制替换、管理员越权按钮等);需要替换时只能等对应采购任务结束。
  • 不改变替换生效后的批量处理逻辑本身(activate() 的关联更新、映射清空、采购任务取消范围),只改变"按钮何时可见"。
  • 不改变 #165 讨论的错误码分类问题;#165 是否落地与本单是否放宽准入互不依赖。
  • 不新增撤销/纠正之外的自动回滚机制;误触后仍依赖既有 CorrectAndActivate 纠正。
  • 不涉及创建订单、支付、修改地址。

前置依赖与并行性

  • 无前置依赖,与 #165、#178、#179 均无代码重叠,可并行。若后续确认要放宽采购来源分支,需要额外评估采购域的特殊风险(订单、支付相邻),届时可能需要单独走高风险确认流程,不在本单默认范围内。

固定实施方案

  1. replacement/origin.go 的 InspectOrigin(originType=="collection" 分支)删除错误码白名单判断(:108-110)和 task.Status == failed 判断(:104-106),改为:PDDProductID 非空检查、无进行中替换检查通过后,进入第 2 步的采购状态检查,全部通过才 result.Eligible = true;不再读取或比较 errorCode,不再读取 task.Status(status 变量若因此无其他用途,一并清理)。
  2. 新增查询:以 result.SourceProductID 关联 purchase_task.pdd_product_id,检查是否存在状态为 running、order_submit_started 或 order_result_unknown 的记录(存在性查询即可,不需要拉全部字段)。命中任意一条,设置 result.DisabledReason = "该商品有采购任务正在执行或结果待核对,暂不能替换,请稍后重试" 并返回,不进入后续判断。该检查放在错误码/失败状态判断被移除后的位置,作为新的强制门槛。
  3. ErrorPDDLinkInvalid/ErrorPDDGoodsSoldOut 常量因本单不再被任何逻辑引用,随本单一并删除,不保留死代码。
  4. Android 端 confirmReplacement(TaskHistoryFragment.kt:809-818)确认弹窗文案更新,同时列出:批量影响范围(关联虾皮商品数量可从 detail/接口已有信息中获取则一并展示,无法预知具体数量时至少说明"会影响该商品下全部关联的虾皮商品")**和「会取消该商品下尚未开始执行的采购任务」**这两条,后者不因原任务是否失败而省略;被第 2 步拦截时展示的独立提示文案不经过此弹窗(按钮不可用或点击后即提示拦截原因,不进入替换确认流程)。
  5. 复核 ReplacementPresentation.HIDDEN 场景在放宽后是否仍会出现(应仅剩「无可替换 PDD 商品」「已有进行中替换」「有采购任务正在执行/结果待核对」三种),若这些情况下仍无任何提示文字,评估是否需要补充简短提示,提升可理解性;是否在本单一并做由用户在确认阶段决定,默认不做,作为相邻问题记录。

设计证据

现有按钮显示条件与确认弹窗文案的调整,不新增页面、不改变整体交互结构;确认弹窗文案变化需要标注截图交用户确认(放宽前/放宽后对比、新文案内容,需体现「取消未执行采购任务」和「有采购任务正在执行/结果待核对,暂不能替换」两句新增提示)。

确定文案(2026-09-01 用户确认)

场景一:可以替换时的确认弹窗(confirmReplacement)

标题:采集替代商品
正文:
用当前商品替换关联的拼多多商品?将会:
· 把所有关联该商品的虾皮商品一并改指向新商品,原有规格映射清空,需重新匹配
· 取消该商品下尚未开始执行的采购任务
原商品会被停用,此操作暂不能自动撤销。
按钮:取消 / 确认替换

场景二:被拦截时(该商品名下存在 running/order_submit_started/order_result_unknown 采购任务)

不使用弹窗,比照 ReplacementPresentation.MANUAL_REQUIRED/MATCHING 的既有做法,新增枚举值(如 PURCHASE_IN_PROGRESS)原地渲染静态文字:

标题:暂不能替换
正文:该商品下有采购任务正在执行或订单结果待核对,任务结束后可再发起替换。

验收标准

  • 该 PDD 商品存在、当前无进行中替换、且无正在执行/结果待核对的采购任务时,无论任务状态是成功还是失败、无论错误码是什么,「采集替代商品」按钮均显示。
  • 该 PDD 商品已有进行中替换,按钮仍不显示,DisabledReason 与既有文案一致。
  • 该 PDD 商品名下存在 running、order_submit_started 或 order_result_unknown 状态的采购任务,按钮不显示或点击后明确提示拦截原因,不进入替换确认流程;任务结束(成功/失败/取消)后按钮恢复可用(其余条件满足时)。
  • 任务的 PDDProductID 为空时按钮仍不显示(既有保护不受影响)。
  • 确认弹窗文案已更新,同时明确说明批量改关联/清映射的影响范围,以及「会取消该商品下尚未开始执行的采购任务」。
  • 采集任务 110(或同类错误码为通用兜底的失败任务)现在可以看到并使用该按钮。
  • 采购来源(originType=="purchase")分支行为未被改动,其既有测试通过。
  • activate() 批量替换的行为、范围、事务性均未改变;running/order_submit_started 任务继续被跳过不动的既有行为不受影响(新门槛是提前拦截整个替换发起,不是改变 activate() 内部对这些任务的处理)。
  • Android 与 Server 单元测试、契约测试与构建通过。

必测场景

  • 采集任务失败,错误码为白名单外的任意值(如通用兜底错误码),按钮正确出现。
  • 采集任务失败,错误码为原白名单内的两个值,行为与放宽前一致(回归)。
  • 采集任务成功(非 failed),按钮正确出现(此前应不出现,本单行为反转,必须专门验证)。
  • 该 PDD 商品已有进行中替换,按钮不出现,提示文案不变。
  • 采集任务 PDDProductID 为空,按钮不出现。
  • 对一个状态正常、名下有未开始执行的采购任务的 PDD 商品发起替换:确认弹窗正确显示「会取消尚未开始执行的采购任务」提示;确认后该商品名下待执行采购任务被正确取消。
  • 对一个名下有 running 采购任务的 PDD 商品发起替换:按钮不可用/点击即提示拦截原因,不能进入确认弹窗;该 running 任务不受影响,继续正常执行到结束。
  • 对一个名下有 order_submit_started 采购任务的 PDD 商品发起替换:同上,明确拦截。
  • 对一个名下有 order_result_unknown 采购任务的 PDD 商品发起替换:同上,明确拦截。
  • 上述被拦截的采购任务结束(成功/失败/取消)后,其余条件满足时按钮恢复可用。
  • 确认放宽后点击按钮 → 查看新确认弹窗文案 → 确认后行为与放宽前完全一致(批量关联更新、映射清空、待执行采购任务取消范围不变;running/order_submit_started 任务跳过不动的既有行为不变)。
  • 采购来源分支(原有两个门槛 + 错误码白名单)行为不受影响,回归验证。

风险与安全门禁

  • 误触代价被放大,且不再局限于失败任务:放宽准入后,任意状态的采集任务(包括成功任务)都可能被人工判断为"该替换了"而触发批量替换,进而清空一批虾皮商品的规格映射、取消该商品下尚未开始执行的采购任务(即便商品本身状态正常)、停用原 PDD 商品。缓解措施为确认弹窗文案必须先行加重且明确提及"取消未执行采购任务"这一具体后果,不能只放宽准入而不加强提示。
  • 误触后果并非不可恢复(CorrectAndActivate 可纠正),但纠正本身是额外人工成本,不作为放宽的免责理由。
  • 新增硬门槛不做例外:即便工人确信判断正确,只要该 PDD 商品名下存在正在执行或结果待核对的采购任务,一律拦截,不提供强制跳过入口;这是本单唯一一处不交给人工判断、由系统直接决定的边界,理由是它关系到是否有真实资金/订单动作正在发生,性质上属于「涉及创建订单」的高风险相邻场景。
  • 不改变数据库结构、权限、并发控制;改动集中在准入判断条件与前端提示文案。
  • 采购来源分支的放宽评估单独进行,避免把采集侧的判断直接套用到涉及订单/支付相邻的采购域。

文档影响

有长期文档影响。 #130 相关的替换业务规则(含准入条件)若在 Wiki 业务规则页面有记录,需同步更新为「按状态而非错误码白名单判断,且新增采购执行中/结果待核对的硬性拦截」,并说明确认弹窗新增的批量影响提示与拦截提示。在线回读 revision 后完成一次 sync 和一次 sync --check,revision 回写本工单;若核实后发现相关内容此前未曾写入 Wiki,在工单说明并跳过同步。

状态

待确认(2026-09-01 创建;同日三次修订与确认弹窗文案均已由用户确认:去除错误码白名单、去除 failed 状态要求、新增"无正在执行/结果待核对采购任务"硬门槛、确定两处提示文案。方案已完整,可进入实施。)

> Gitea MCP 未向当前会话暴露,按仓库规则回退项目根目录安全配置与 Gitea API 维护本工单;凭据未写入工单、代码或日志。 > > **2026-09-01 范围修订一(已确认)**:用户进一步要求去掉「任务状态必须为 failed」这道门槛——只要工人通过其他手机/网页验证过 PDD 商品确实失效,或单纯找到了更好的替代品,即便当前采集任务本身是成功的,也应该能从其任务详情发起替换。用户已确认第 1、2 点:去掉 `failed` 限制、确认弹窗文案追加「会取消该商品下尚未开始执行的采购任务」的明确提示。 > > **2026-09-01 范围修订二(已确认)**:用户进一步要求,若该 PDD 商品名下存在正在采购(`running`/`order_submit_started`)的任务,不允许替换。核验后一并收紧为:`running`、`order_submit_started`、`order_result_unknown`(订单结果未知、需人工核对,同样敏感)三种状态命中任意一个即禁止替换,作为新增的硬门槛,不交给人工临场判断——这与范围修订一放宽的两道门槛(错误码、任务失败状态)性质不同:修订一放宽的是需要人工判断的软信号,本次收紧的是系统自身已经确定的硬事实(是否有采购正在执行或结果待核对)。以下正文已按此修订。 ## 原始需求摘要 来源:用户于 2026-09-01 反馈采集任务 110 失败原因为「未找到 PDD 商品规格入口」(实际是链接失效被跳转到 PDD 首页),但任务详情里没有出现「采集替代商品」按钮。核验后确认根因是服务端按错误码白名单判断是否显示该按钮,白名单覆盖不了这类场景。用户明确要求:不应该用错误码白名单限制过多,任务失败了就应该显示该按钮,交给工人判断失败原因、决定是否需要替换商品;并要求单独建单,不并入 #165 或 #178。 目的:让「采集替代商品」的可见性既不依赖一份不完整的错误码白名单,也不强制要求当前任务本身处于失败状态,改为把"这个 PDD 商品是否值得替换"的判断完全交给人工——判断依据可以来自 Agent 自身的失败记录,也可以来自外部验证(其他手机、网页)或单纯发现了更好的商品;同时保留必要的状态机保护,针对新引入的行为(对健康商品发起替换会取消其未执行的采购任务)加强确认提示,并新增一道系统硬门槛:该商品名下有采购任务正在执行或结果待核对时,直接禁止替换,不留给人工临场判断,避免放宽准入扩大误触代价。 ## 基线与已核实事实 代码基线:`4343ab5`(2026-09-01)。核验日期 2026-09-01。功能来源工单 #130(`33d4bb1 feat(#130): collect replacement products from Agent`),后续 #132、#155、#157、#174 在同一路径上有过维护,本单是对 #130 既有准入条件的一次收窄反转。 - 显示条件全部集中在 `server/app/goauto/replacement/origin.go:InspectOrigin`(采集来源 `originType=="collection"` 分支,`:37-46`): 1. `task.PDDProductID != nil`(`:65-68`,否则 `DisabledReason="原任务没有可替换的拼多多商品"`)——**本单保留**; 2. 该 PDD 商品当前没有处于 `active` 状态的替换记录(`activeForOrigin`,`:80-83`,否则 `DisabledReason="该商品已经发起替换"`)——**本单保留**; 3. 该采集任务本身没有一条待处理/失败的替换激活记录(`:78-91`)——**本单保留**; 4. **`task.Status == failed`**(`:104-106`,否则 `DisabledReason="只有失败任务可以替换商品"`)——**本单去除**; 5. **`task.ErrorCode` 精确等于 `PDD_LINK_INVALID` 或 `PDD_GOODS_SOLD_OUT`**(`:108-110`,`ErrorPDDLinkInvalid`/`ErrorPDDGoodsSoldOut` 常量定义于 `origin.go:15-16`,仅在此处被引用,无其他耦合);否则 `DisabledReason="当前失败原因不属于商品失效或售罄"`——**本单去除**。 - Android 端不需要为此新增入口:`renderCollectionDetail`(`TaskHistoryFragment.kt`)当前对任意任务状态都无条件调用 `renderReplacementAction`,能否显示按钮完全由服务端 `InspectOrigin` 的返回值决定;去掉第 4、5 两道门槛后,成功任务的详情页会自然出现该按钮,无需额外的前端改动。 - Android 端 `TaskHistoryFragment.kt:92-100`(`ReplacementActionPolicy.presentation`):`eligible=false` 且无 `mappingStatus`/`activationStatus` 时,呈现结果为 `HIDDEN`,按钮**直接不渲染,不显示任何理由文字**(`:99`「ReplacementPresentation.HIDDEN -> Unit」)。这是造成用户「看不出为什么按钮不见了」的直接原因。 - 采集任务 110 的失败错误码属于通用兜底(如 #165 讨论的 `PDD_DETAIL_ENTRY_FAILED` 一类),不在上述第 5 条白名单内,因此即便真实原因是商品失效,按钮也不出现。 - 该按钮触发的动作影响范围较大,已在 `replacement/activation.go:239-289` 核实:批量把当前挂在该 PDD 商品下的**所有**虾皮商品改指向新商品并清空其规格映射、取消相关未开始的采购任务、把旧 PDD 商品标记停用,一个事务内批量完成。 - **去掉 `failed` 限制后新增的真实后果**:此前"取消未开始执行的采购任务"这一步隐含的前提是"商品已经失败,挂在它上面的采购任务本来也成不了";一旦允许对状态正常、甚至采购中的健康商品发起替换,这一步会变成对着完全正常的商品**静默取消其未执行的采购任务**——这是本次放宽新引入的行为,此前被"必须先失败"这个前提掩盖,现有确认弹窗文案完全没有提及,必须在本单一并加强提示。 - 现有前端确认弹窗(`TaskHistoryFragment.kt:809-818`,`confirmReplacement`)文案仅为「用当前商品替换 #N 的失效商品?」,未提示上述批量影响范围。 - `CorrectAndActivate`(`replacement/activation.go:70+`)已存在,用于纠正误操作的替换记录,说明项目本身已经预期这类操作可能判断有误、需要纠正路径,但纠正本身仍是有成本的额外操作,不能作为放宽准入的免责理由。 - **已核实:正在执行的采购任务本身机制上是安全的,但整个替换动作不是**。`applyPurchaseReplacement`(`activation.go:309-330`)已经区分处理:`pending`/`spec_probe_pending` 取消;`running`/`order_submit_started` **跳过不动**(`SkippedRunningTasks`);其余终态保留不动。核对 `purchase/lifecycle.go` 的 `Claim`/`Start`/`Result` 全链路,均不重新校验 `pdd.Status`,因此被跳过的正在执行任务不会被中途打断。但 `activate()` 仍会无条件把源 PDD 商品标记为 `disabled`、把该商品下全部虾皮商品关联批量改到新商品(`activation.go:255-293`)——这一步和"是否有采购正在执行"完全无关,即便任务本身不受影响,也会在一笔采购正处于执行、尤其是刚越过 `order_submit_started` 这个最接近创建订单的不可逆边界时,制造一个货不对板的时间窗口:任务用旧商品的快照继续跑,但商品本身已经被标记废弃、关联已经改走,一旦该任务紧接着成功创建订单,会让审计和后续排查变得混乱。 - `models.PurchaseTaskStatusRunning`、`PurchaseTaskStatusOrderSubmitStarted`、`PurchaseTaskStatusOrderResultUnknown` 均已在 `models/purchase.go:19-23` 定义,无需新增状态常量。 ## 判断边界 - 已确认:错误码白名单准入方式与项目「证据不足交给人工,不替使用者瞎猜」的一贯原则相悖,用户明确要求去除。 - **已确认(2026-09-01 修订)**:放宽后只保留两道硬门槛——`PDD 商品必须存在`(`task.PDDProductID != nil`)、`该 PDD 商品当前无进行中的替换`——因为它们是纯状态机保护,不涉及对失败原因或商品价值的判断。`任务状态为 failed` 不再作为门槛,用户已明确要求去除。 - 已确认:放宽会扩大误触代价(批量改关联、清映射、取消采购任务——且去掉 `failed` 限制后取消采购任务这一步可能作用于健康商品),因此确认弹窗文案必须加重,**明确列出「会取消该商品下尚未开始执行的采购任务」**,作为放宽准入的配套措施,不是可选项,用户已在 2026-09-01 确认此项。 - 尚未确认:`原任务没有可替换的拼多多商品`(`task.PDDProductID == nil`)这一门槛是否也需要复核。经核实,采集任务从创建起就必须关联一个 PDD 商品(无论是新建还是复用),正常情况下该字段不会为空;本单不改变这一条。 - **已确认(2026-09-01 修订二)**:新增门槛不属于"可以放宽、交给人工判断"的范畴——它是系统已经确定的硬事实(是否有采购正在执行、结果是否待核对),不接受人工绕过;发起人若确实需要在这种情况下替换,必须等对应采购任务结束(成功、失败或取消)后再操作,不提供强制跳过的入口。 ## 目标 1. 「采集替代商品」按钮的显示与否,不再依赖 `ErrorCode` 是否命中白名单,也不再要求任务状态必须为 `failed`;只要该 PDD 商品存在、当前没有进行中的替换、且没有正在执行或结果待核对的采购任务,任意状态(成功或失败)的采集任务详情都能发起替换,把"这个商品是否值得替换"的判断交给操作人工,同时用系统硬事实挡住最危险的时间窗口。 2. 保留 `task.PDDProductID != nil`、无进行中替换这两条既有状态机保护,不再保留 `failed` 状态要求;新增第三条硬门槛:该 PDD 商品名下不存在 `running`、`order_submit_started`、`order_result_unknown` 状态的采购任务。 3. 确认弹窗文案加重,明确告知:此操作会把当前关联该 PDD 商品的全部虾皮商品改指向新商品、清空它们的规格映射、**取消该商品下尚未开始执行的采购任务**,操作后需要重新完成规格映射;这条提示不因原任务是否失败而省略。 4. 采购来源(`originType=="purchase"`)分支是否同步放宽,本单需要用户单独确认(详见「非目标」)。 5. 确认弹窗新增第三层提示:因 `running`/`order_submit_started`/`order_result_unknown` 被拦截时,明确告知"该商品有采购任务正在执行或结果待核对,暂不能替换,请稍后重试",与前两条放宽引入的提示(批量改关联/清映射、取消未执行采购任务)在同一处按场景分别展示,不混在一句话里。 ## 非目标 - 不改变采购来源(`originType=="purchase"`)分支的准入条件。采购失败与采集失败的业务后果不同(采购失败可能已经产生过订单尝试),是否同步放宽需要单独评估,本单只处理采集来源分支,除非用户在确认阶段明确要求一并处理。 - 不改变「该 PDD 商品当前无进行中的替换」「PDD 商品必须存在」这两条门槛。`任务状态必须为 failed` 不属于本条非目标,已按用户要求去除,见「目标」;新增的"无正在执行/结果待核对采购任务"门槛属于本单新增范围,见「目标」第 2 条。 - 不提供绕过"有采购任务正在执行/结果待核对"这道新门槛的入口(如强制替换、管理员越权按钮等);需要替换时只能等对应采购任务结束。 - 不改变替换生效后的批量处理逻辑本身(`activate()` 的关联更新、映射清空、采购任务取消范围),只改变"按钮何时可见"。 - 不改变 #165 讨论的错误码分类问题;#165 是否落地与本单是否放宽准入互不依赖。 - 不新增撤销/纠正之外的自动回滚机制;误触后仍依赖既有 `CorrectAndActivate` 纠正。 - 不涉及创建订单、支付、修改地址。 ## 前置依赖与并行性 - 无前置依赖,与 #165、#178、#179 均无代码重叠,可并行。若后续确认要放宽采购来源分支,需要额外评估采购域的特殊风险(订单、支付相邻),届时可能需要单独走高风险确认流程,不在本单默认范围内。 ## 固定实施方案 1. `replacement/origin.go` 的 `InspectOrigin`(`originType=="collection"` 分支)删除错误码白名单判断(`:108-110`)**和** `task.Status == failed` 判断(`:104-106`),改为:`PDDProductID` 非空检查、无进行中替换检查通过后,进入第 2 步的采购状态检查,全部通过才 `result.Eligible = true`;不再读取或比较 `errorCode`,不再读取 `task.Status`(`status` 变量若因此无其他用途,一并清理)。 2. 新增查询:以 `result.SourceProductID` 关联 `purchase_task.pdd_product_id`,检查是否存在状态为 `running`、`order_submit_started` 或 `order_result_unknown` 的记录(存在性查询即可,不需要拉全部字段)。命中任意一条,设置 `result.DisabledReason = "该商品有采购任务正在执行或结果待核对,暂不能替换,请稍后重试"` 并返回,不进入后续判断。该检查放在错误码/失败状态判断被移除后的位置,作为新的强制门槛。 3. `ErrorPDDLinkInvalid`/`ErrorPDDGoodsSoldOut` 常量因本单不再被任何逻辑引用,随本单一并删除,不保留死代码。 4. Android 端 `confirmReplacement`(`TaskHistoryFragment.kt:809-818`)确认弹窗文案更新,同时列出:批量影响范围(关联虾皮商品数量可从 `detail`/接口已有信息中获取则一并展示,无法预知具体数量时至少说明"会影响该商品下全部关联的虾皮商品")**和「会取消该商品下尚未开始执行的采购任务」**这两条,后者不因原任务是否失败而省略;被第 2 步拦截时展示的独立提示文案不经过此弹窗(按钮不可用或点击后即提示拦截原因,不进入替换确认流程)。 5. 复核 `ReplacementPresentation.HIDDEN` 场景在放宽后是否仍会出现(应仅剩「无可替换 PDD 商品」「已有进行中替换」「有采购任务正在执行/结果待核对」三种),若这些情况下仍无任何提示文字,评估是否需要补充简短提示,提升可理解性;是否在本单一并做由用户在确认阶段决定,默认不做,作为相邻问题记录。 ## 设计证据 现有按钮显示条件与确认弹窗文案的调整,不新增页面、不改变整体交互结构;确认弹窗文案变化需要标注截图交用户确认(放宽前/放宽后对比、新文案内容,需体现「取消未执行采购任务」和「有采购任务正在执行/结果待核对,暂不能替换」两句新增提示)。 ## 确定文案(2026-09-01 用户确认) **场景一:可以替换时的确认弹窗**(`confirmReplacement`) ``` 标题:采集替代商品 正文: 用当前商品替换关联的拼多多商品?将会: · 把所有关联该商品的虾皮商品一并改指向新商品,原有规格映射清空,需重新匹配 · 取消该商品下尚未开始执行的采购任务 原商品会被停用,此操作暂不能自动撤销。 按钮:取消 / 确认替换 ``` **场景二:被拦截时**(该商品名下存在 `running`/`order_submit_started`/`order_result_unknown` 采购任务) 不使用弹窗,比照 `ReplacementPresentation.MANUAL_REQUIRED`/`MATCHING` 的既有做法,新增枚举值(如 `PURCHASE_IN_PROGRESS`)原地渲染静态文字: ``` 标题:暂不能替换 正文:该商品下有采购任务正在执行或订单结果待核对,任务结束后可再发起替换。 ``` ## 验收标准 - [ ] 该 PDD 商品存在、当前无进行中替换、且无正在执行/结果待核对的采购任务时,无论任务状态是成功还是失败、无论错误码是什么,「采集替代商品」按钮均显示。 - [ ] 该 PDD 商品已有进行中替换,按钮仍不显示,`DisabledReason` 与既有文案一致。 - [ ] 该 PDD 商品名下存在 `running`、`order_submit_started` 或 `order_result_unknown` 状态的采购任务,按钮不显示或点击后明确提示拦截原因,不进入替换确认流程;任务结束(成功/失败/取消)后按钮恢复可用(其余条件满足时)。 - [ ] 任务的 `PDDProductID` 为空时按钮仍不显示(既有保护不受影响)。 - [ ] 确认弹窗文案已更新,同时明确说明批量改关联/清映射的影响范围,以及「会取消该商品下尚未开始执行的采购任务」。 - [ ] 采集任务 110(或同类错误码为通用兜底的失败任务)现在可以看到并使用该按钮。 - [ ] 采购来源(`originType=="purchase"`)分支行为未被改动,其既有测试通过。 - [ ] `activate()` 批量替换的行为、范围、事务性均未改变;`running`/`order_submit_started` 任务继续被跳过不动的既有行为不受影响(新门槛是提前拦截整个替换发起,不是改变 `activate()` 内部对这些任务的处理)。 - [ ] Android 与 Server 单元测试、契约测试与构建通过。 ## 必测场景 - 采集任务失败,错误码为白名单外的任意值(如通用兜底错误码),按钮正确出现。 - 采集任务失败,错误码为原白名单内的两个值,行为与放宽前一致(回归)。 - **采集任务成功**(非 `failed`),按钮正确出现(此前应不出现,本单行为反转,必须专门验证)。 - 该 PDD 商品已有进行中替换,按钮不出现,提示文案不变。 - 采集任务 `PDDProductID` 为空,按钮不出现。 - 对一个**状态正常、名下有未开始执行的采购任务**的 PDD 商品发起替换:确认弹窗正确显示「会取消尚未开始执行的采购任务」提示;确认后该商品名下待执行采购任务被正确取消。 - 对一个**名下有 `running` 采购任务**的 PDD 商品发起替换:按钮不可用/点击即提示拦截原因,不能进入确认弹窗;该 `running` 任务不受影响,继续正常执行到结束。 - 对一个**名下有 `order_submit_started` 采购任务**的 PDD 商品发起替换:同上,明确拦截。 - 对一个**名下有 `order_result_unknown` 采购任务**的 PDD 商品发起替换:同上,明确拦截。 - 上述被拦截的采购任务结束(成功/失败/取消)后,其余条件满足时按钮恢复可用。 - 确认放宽后点击按钮 → 查看新确认弹窗文案 → 确认后行为与放宽前完全一致(批量关联更新、映射清空、待执行采购任务取消范围不变;`running`/`order_submit_started` 任务跳过不动的既有行为不变)。 - 采购来源分支(原有两个门槛 + 错误码白名单)行为不受影响,回归验证。 ## 风险与安全门禁 - **误触代价被放大,且不再局限于失败任务**:放宽准入后,任意状态的采集任务(包括成功任务)都可能被人工判断为"该替换了"而触发批量替换,进而清空一批虾皮商品的规格映射、**取消该商品下尚未开始执行的采购任务(即便商品本身状态正常)**、停用原 PDD 商品。缓解措施为确认弹窗文案必须先行加重且明确提及"取消未执行采购任务"这一具体后果,不能只放宽准入而不加强提示。 - 误触后果并非不可恢复(`CorrectAndActivate` 可纠正),但纠正本身是额外人工成本,不作为放宽的免责理由。 - **新增硬门槛不做例外**:即便工人确信判断正确,只要该 PDD 商品名下存在正在执行或结果待核对的采购任务,一律拦截,不提供强制跳过入口;这是本单唯一一处不交给人工判断、由系统直接决定的边界,理由是它关系到是否有真实资金/订单动作正在发生,性质上属于「涉及创建订单」的高风险相邻场景。 - 不改变数据库结构、权限、并发控制;改动集中在准入判断条件与前端提示文案。 - 采购来源分支的放宽评估单独进行,避免把采集侧的判断直接套用到涉及订单/支付相邻的采购域。 ## 文档影响 **有长期文档影响。** #130 相关的替换业务规则(含准入条件)若在 Wiki 业务规则页面有记录,需同步更新为「按状态而非错误码白名单判断,且新增采购执行中/结果待核对的硬性拦截」,并说明确认弹窗新增的批量影响提示与拦截提示。在线回读 revision 后完成一次 `sync` 和一次 `sync --check`,revision 回写本工单;若核实后发现相关内容此前未曾写入 Wiki,在工单说明并跳过同步。 ## 状态 待确认(2026-09-01 创建;同日三次修订与确认弹窗文案均已由用户确认:去除错误码白名单、去除 `failed` 状态要求、新增"无正在执行/结果待核对采购任务"硬门槛、确定两处提示文案。方案已完整,可进入实施。)
Author
Owner

????(2026-09-01)???????? Gitea MCP,??????? Gitea API;????????????,????????????

????:c07c345????????:??????????/??????;?? running?order_submit_started?order_result_unknown ???????;Android ???????????????;????????????????????? Server/Android ???????,?? #185 ???????????????

????(2026-09-01)???????? Gitea MCP,??????? Gitea API;????????????,???????????? ????:`c07c345`????????:??????????/??????;?? `running`?`order_submit_started`?`order_result_unknown` ???????;Android ???????????????;????????????????????? Server/Android ???????,?? #185 ???????????????
Author
Owner

实施完成,等待验收

实现

  • 采集来源不再按失败状态/错误码白名单限制替代商品入口;只要有关联 PDD 商品且不存在生效中的替代关系即可发起。
  • 新增采购安全硬阻断:关联商品存在 running、order_submit_started 或 order_result_unknown 采购任务时,服务端拒绝发起,Android 明示“暂不能替换”。
  • 采购来源原有“失败状态 + PDD_LINK_INVALID/PDD_GOODS_SOLD_OUT”规则保持不变。
  • Android 使用已确认的强确认文案,明确关联虾皮商品改指向、规格映射清空、取消未开始采购任务、原商品停用且暂不可自动撤销。

验证与证据

  • go test ./... 与服务端构建通过;替代来源边界及采购来源回归测试通过。
  • Android debug/release 单测通过,debug APK 构建通过。
  • python dev_scripts/harness.py check --strict 通过。
  • 一键 verify -Component all 的服务端阶段通过,但被本次未改动的 web/src/views/goauto/purchase-tasks/index.vue 存量 lint(制表符/空格混用,24 errors)阻断;Web 生产构建另行通过。
  • 提交并推送:3aab1f0 feat(goauto): 完善替代采集与颜色图片详情 (#184 #185)。
  • Wiki:Business-Rules-and-Glossary revision 36d7285beb3f0c8441e38b4e9e81e5cf7256b603;Android-Agent-API-Contract revision 0f448673dbec00a351c397d30926f0c5090a1141;已完成一次 sync 和一次 sync --check。

发布与未验证项

  • 已生成本地发布包 20260901-3aab1f0.zip;线上切换、服务重启及设为当前 Agent 版本属于高风险操作,等待本次发布前人工确认。
  • 尚未在真机安装 0.9.37,也未在真实采集/采购任务上点击替代商品;未执行创建订单或付款。
## 实施完成,等待验收 ### 实现 - 采集来源不再按失败状态/错误码白名单限制替代商品入口;只要有关联 PDD 商品且不存在生效中的替代关系即可发起。 - 新增采购安全硬阻断:关联商品存在 `running`、`order_submit_started` 或 `order_result_unknown` 采购任务时,服务端拒绝发起,Android 明示“暂不能替换”。 - 采购来源原有“失败状态 + `PDD_LINK_INVALID`/`PDD_GOODS_SOLD_OUT`”规则保持不变。 - Android 使用已确认的强确认文案,明确关联虾皮商品改指向、规格映射清空、取消未开始采购任务、原商品停用且暂不可自动撤销。 ### 验证与证据 - `go test ./...` 与服务端构建通过;替代来源边界及采购来源回归测试通过。 - Android debug/release 单测通过,debug APK 构建通过。 - `python dev_scripts/harness.py check --strict` 通过。 - 一键 `verify -Component all` 的服务端阶段通过,但被本次未改动的 `web/src/views/goauto/purchase-tasks/index.vue` 存量 lint(制表符/空格混用,24 errors)阻断;Web 生产构建另行通过。 - 提交并推送:`3aab1f0 feat(goauto): 完善替代采集与颜色图片详情 (#184 #185)`。 - Wiki:Business-Rules-and-Glossary revision `36d7285beb3f0c8441e38b4e9e81e5cf7256b603`;Android-Agent-API-Contract revision `0f448673dbec00a351c397d30926f0c5090a1141`;已完成一次 `sync` 和一次 `sync --check`。 ### 发布与未验证项 - 已生成本地发布包 `20260901-3aab1f0.zip`;线上切换、服务重启及设为当前 Agent 版本属于高风险操作,等待本次发布前人工确认。 - 尚未在真机安装 0.9.37,也未在真实采集/采购任务上点击替代商品;未执行创建订单或付款。
Author
Owner

线上发布完成

  • 发布提交:3aab1f0。
  • 新发布目录:/home/goauto/releases/20260901-102000-3aab1f0;回滚目录:/home/goauto/releases/20260901-090504-c07c345,旧发布未删除。
  • 发布前/后运行中采集任务均为 0;处于 running、order_submit_started、order_result_unknown 的采购任务均为 0。
  • goauto.service 为 active;内部及外部 /api/v1/health 均为 HTTP 200,外部 Admin 首页为 HTTP 200;最近发布窗口无 service error 日志。
  • 服务二进制 SHA-256:40569d90bc6bcbd3b3edb14aca41f336a09c07f9a6b2dcadc1b06eca1184ca88。
  • Agent 0.9.37(versionCode 50)已登记为版本 ID 3 并设为当前;线上文件 6,255,365 bytes,SHA-256 8612553a28524d72e095e8e855a5b6a9789db59f0ff9c521e8a37b551c64cb71,与本地构建一致。
  • Agent /api/agent/v1/app/latest 无 Token 返回预期 DEVICE_TOKEN_INVALID,证明外部路由进入业务鉴权层。
  • Admin 页面可访问,但浏览器自动化读取设备页连续超时;因此在复核 Manifest 测试、文件哈希/大小、versionCode 唯一性、任务安全前置条件后,按 agent_app_release / agent_app_release_setting 现有服务契约完成登记与当前版本切换,并回读数据库及文件验证。
  • 已删除服务器 /tmp 上传包与解压暂存目录;正式发布目录、历史发布和 APK 版本均保留。
  • 未安装真机、未执行真实替代操作、采购、创建订单或付款;工单继续保持待用户验收。
## 线上发布完成 - 发布提交:`3aab1f0`。 - 新发布目录:`/home/goauto/releases/20260901-102000-3aab1f0`;回滚目录:`/home/goauto/releases/20260901-090504-c07c345`,旧发布未删除。 - 发布前/后运行中采集任务均为 0;处于 `running`、`order_submit_started`、`order_result_unknown` 的采购任务均为 0。 - `goauto.service` 为 `active`;内部及外部 `/api/v1/health` 均为 HTTP 200,外部 Admin 首页为 HTTP 200;最近发布窗口无 service error 日志。 - 服务二进制 SHA-256:`40569d90bc6bcbd3b3edb14aca41f336a09c07f9a6b2dcadc1b06eca1184ca88`。 - Agent 0.9.37(versionCode 50)已登记为版本 ID 3 并设为当前;线上文件 6,255,365 bytes,SHA-256 `8612553a28524d72e095e8e855a5b6a9789db59f0ff9c521e8a37b551c64cb71`,与本地构建一致。 - Agent `/api/agent/v1/app/latest` 无 Token 返回预期 `DEVICE_TOKEN_INVALID`,证明外部路由进入业务鉴权层。 - Admin 页面可访问,但浏览器自动化读取设备页连续超时;因此在复核 Manifest 测试、文件哈希/大小、versionCode 唯一性、任务安全前置条件后,按 `agent_app_release` / `agent_app_release_setting` 现有服务契约完成登记与当前版本切换,并回读数据库及文件验证。 - 已删除服务器 `/tmp` 上传包与解压暂存目录;正式发布目录、历史发布和 APK 版本均保留。 - 未安装真机、未执行真实替代操作、采购、创建订单或付款;工单继续保持待用户验收。
Author
Owner

线上 Agent APK 下载权限修复

用户在 Agent 0.9.35 点击“下载新版本”时收到“服务端处理失败”。根因确认:goauto.service 以 goauto:goauto 运行,但当前发布目录的 var/goauto-agent-releases 及 APK 属主为 root:root;目录 750、文件 640 导致服务进程无法 stat/open APK,版本检查可成功而下载接口返回 INTERNAL_ERROR。

已修复当前发布 /home/goauto/releases/20260901-102000-3aab1f0:

  • 先校验 current 解析目标及目录内无符号链接;
  • 目录和 48/49/50 三个 APK 属主改为 goauto:goauto;
  • 保持目录 750、文件 640,未放宽公开读取;
  • 以 goauto 用户成功完整读取 versionCode 50 APK,SHA-256 为 8612553a28524d72e095e8e855a5b6a9789db59f0ff9c521e8a37b551c64cb71;
  • goauto.service 为 active,内部和外部健康接口均为 HTTP 200。

未安装手机、未触发采购/订单/付款。等待用户在手机重新点击下载验证真实设备链路。

## 线上 Agent APK 下载权限修复 用户在 Agent 0.9.35 点击“下载新版本”时收到“服务端处理失败”。根因确认:`goauto.service` 以 `goauto:goauto` 运行,但当前发布目录的 `var/goauto-agent-releases` 及 APK 属主为 `root:root`;目录 `750`、文件 `640` 导致服务进程无法 `stat/open` APK,版本检查可成功而下载接口返回 `INTERNAL_ERROR`。 已修复当前发布 `/home/goauto/releases/20260901-102000-3aab1f0`: - 先校验 `current` 解析目标及目录内无符号链接; - 目录和 48/49/50 三个 APK 属主改为 `goauto:goauto`; - 保持目录 `750`、文件 `640`,未放宽公开读取; - 以 `goauto` 用户成功完整读取 versionCode 50 APK,SHA-256 为 `8612553a28524d72e095e8e855a5b6a9789db59f0ff9c521e8a37b551c64cb71`; - `goauto.service` 为 `active`,内部和外部健康接口均为 HTTP 200。 未安装手机、未触发采购/订单/付款。等待用户在手机重新点击下载验证真实设备链路。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#184