Skip to content

29 g3 technical spike report

技术老胡 edited this page Jul 30, 2026 · 1 revision

G3 技术专项验证报告

本文档记录 24-g3-g4-decision-register.md 中专项验证的只读事实、结论和设计回写。专项验证只读取源码、数据库结构和运行页面,不编写业务代码,不修改数据库业务数据。

一、状态总览

编号 验证主题 状态 结论去向
SPIKE-001 CRMEB 商品规格身份稳定性 已完成 本文第二节;回写 151921222428
SPIKE-002 普通订单创建、支付、退款扩展点 已完成 本文第三节;回写 15192021222428
SPIKE-003 用户余额和商户账单入账接口 已完成 本文第四节;回写 1519202122242628
SPIKE-004 独立补运费单支付能力 已完成 本文第五节;回写 1519202122242628
SPIKE-005 uni-app 页面、组件、DIY 和秒杀链路 已完成 25-user-uniapp-reuse-audit.md
SPIKE-006 服务端认证、路由和移动布局 已完成 本文第六节;回写 15171819202122242628
SPIKE-007 地图跨端兼容边界 已完成 本文第七节;回写 0610141516171819212224252628

二、SPIKE-001:商品规格身份稳定性

1. 要回答的问题

  1. CRMEB 的 store_product_attr_value.value_id 是否能作为云仓活动 SKU 的长期身份。
  2. unique 在商品编辑、规格修改和商品复制后是否保持不变。
  3. 来源商品变化后,草稿活动、已发布活动和历史订单分别如何处理。

2. 只读源码证据

证据 当前源码位置 事实
规格值主键 smartfarm/app/common/model/store/product/ProductAttrValue.php:27 store_product_attr_value 使用自增 value_id 作为当前行主键
商品保存 smartfarm/app/common/repositories/store/product/ProductRepository.php:480 保存规格时先调用 clearAttr($product_id),再批量新增规格值
删除规格值 smartfarm/app/common/dao/store/product/ProductAttrValueDao.php:105 clearAttrproduct_id 删除现有规格值行
unique 生成 smartfarm/app/common/repositories/store/product/ProductRepository.php:944 unique 由规格字符串、商品 ID 和商品类型计算
规格载荷构建 smartfarm/app/common/repositories/store/product/ProductRepository.php:763 新增规格值时重算 skuunique,不保留原 value_id
购物车关联 smartfarm/app/common/model/store/StoreCart.php:57 普通商城运行时可按 unique 查当前规格,但这不代表其是不可变历史主键
商品复制载荷 smartfarm/app/common/repositories/store/product/ProductCopyRepository.php:201 复制后以新商品载荷重新保存,商品 ID 和规格值行都会重建

由这些事实可以确定:

  • 普通商品完整编辑会删除并重建规格值,未改变的规格也可能获得新的 value_id
  • 当商品 ID、商品类型或规格字符串变化时,unique 会变化。
  • 商品复制会使用新的商品 ID,因此复制商品的 unique 与规格值主键都不是原商品的延续。
  • value_idunique 适合定位“当前 CRMEB 规格”,不适合作为云仓订单、批次、持仓和账本的永久外键。

3. 冻结结论

G3-INV-001 确认为以下规则:

  1. eb_farm_cloud_activity_sku.activity_sku_id 是云仓活动 SKU 唯一、稳定的业务身份。
  2. 批次、订单明细、持仓、二次库存、分配记录、回购和账本只关联 activity_sku_id 及各自快照。
  3. source_product_idsource_value_idsource_unique 和规范化规格文本只用于来源核验、草稿重匹配和运营诊断。
  4. 草稿或未发布活动允许重新核验来源 SKU;不存在或匹配不唯一时标记 unresolved,禁止发布并要求平台运营重新选择。
  5. 已发布活动不自动重匹配、不自动同步价格和规格规则;来源商品后续修改、删除或复制均不改变已发布活动、批次和历史交易。
  6. 需要采用新来源规格时,关闭未开始的旧配置并新建活动 SKU;不得覆盖已产生订单的快照。

4. 来源同步状态

活动 SKU 保存以下诊断字段:

字段 用途
source_product_id 当前来源商品 ID
source_value_id 最近一次核验到的 CRMEB 规格值行 ID
source_unique 最近一次核验到的 CRMEB unique
source_sku_text 规范化后的规格组合文本
source_snapshot 发布时的商品名、规格、编码、图片、单位和来源价格快照
source_snapshot_hash 判断草稿来源是否发生变化
source_sync_status matchedchangedmissingambiguousunresolved
source_checked_at 最近一次来源核验时间

source_sync_status 只描述来源健康度,不替代活动业务状态。已发布活动即使来源变为 missing,历史交易仍按活动快照继续执行;后台显示告警并限制创建新活动或追加新批次。

5. 采用方案与替代方案

方案 结论 原因
独立 activity_sku_id + 来源快照 采用 历史身份稳定,能隔离 CRMEB 规格重建
永久关联 value_id 否决 商品编辑会删除重建,历史外键会失真
永久关联 unique 否决 商品 ID、类型或规格字符串变化会导致值变化
每次读取 CRMEB 当前商品覆盖活动数据 否决 会改变已付款用户的价格、规则和展示依据
复制整套 CRMEB 商品为云仓商品 V1 不采用 增加商品维护和同步成本,仍不能替代活动规则快照

6. 数据、接口和代码影响

  • 数据模型:活动 SKU 明确独立主键、来源诊断字段、发布快照和同步状态索引。
  • 发布接口:发布前重新核验所有活动 SKU;missingambiguousunresolved 一律失败。
  • 草稿编辑:提供单 SKU 和整活动的只读来源重检;只有运营明确确认后才更新草稿来源引用。
  • 已发布详情:同时返回活动快照与来源健康告警,前端不得用 CRMEB 当前规格覆盖活动快照。
  • Repository:CloudActivitySkuRepository 管理稳定身份与快照;CRMEB ProductAttrValueRepository 只作为当前来源查询适配器。
  • 审计:记录核验前后标识、状态、操作者、时间和活动状态,不记录敏感信息。

7. 必测场景

  1. 保存未改规格的来源商品后 value_id 重建,草稿能够按规范化规格重匹配。
  2. 来源规格改名或删除后,草稿变为 changed/missing 并禁止发布。
  3. 出现多个候选规格时标记 ambiguous,系统不得静默选择。
  4. 来源商品编辑、删除或复制后,已发布活动、订单、批次、持仓和账本关联不变。
  5. 已发布页面显示活动快照和来源告警,不显示新商品价格替代历史价格。
  6. 创建新活动可选择新的来源 SKU,但不得复用旧 activity_sku_id

三、SPIKE-002:订单、支付、取消与退款扩展点

1. 要回答的问题

  1. 云仓首次抢购如何复用 CRMEB 订单和支付,又不要求用户先选择地址或去向。
  2. 二次代销普通订单在何处锁定云仓库存,才能与普通订单创建一起回滚。
  3. 支付成功、未支付取消、退款申请、退款拒绝/取消和资金退款完成分别在哪个事务边界接入。
  4. CRMEB 的普通商户财务、普通库存和履约副作用是否适用于云仓首次订单与二次代销订单。

2. 只读源码证据

证据 当前源码位置 事实
用户订单入口 smartfarm/app/controller/api/store/order/StoreOrder.php:83 v2CreateOrder 校验购物车后使用 LockService('order.create') 调用订单 Repository
普通订单创建事务 smartfarm/app/common/repositories/store/order/StoreOrderCreateRepository.php:1177 库存扣减、订单组、子订单和订单明细在同一 Db::transaction 中完成
订单明细完成点 smartfarm/app/common/repositories/store/order/StoreOrderCreateRepository.php:1417 StoreOrderProduct 批量插入后仍在事务内,随后才触发 order.create
批量插入返回值 smartfarm/app/common/dao/BaseDao.php:219 insertAll 返回批量插入结果,不返回每个订单明细 ID
代客下单独立路径 smartfarm/app/common/repositories/store/order/MerchantOrderCreateRepository.php:276 商户代客下单有另一套创建事务,不经过用户端 StoreOrderCreateRepository
支付成功事务 smartfarm/app/common/repositories/store/order/StoreOrderRepository.php:282 paySuccess 在事务内更新订单、生成商户锁定款和财务记录
普通商户入账副作用 smartfarm/app/common/repositories/store/order/StoreOrderRepository.php:396 普通订单按订单实付、佣金和平台券生成财务流水,并调用 addLockMoney
支付成功其他副作用 smartfarm/app/common/repositories/store/order/StoreOrderRepository.php:282-590 同一流程还处理普通活动、卡密、同城配送、推广关系、商户通知/打印、用户支付统计、积分、赠券、短信、会员值和数据大屏
支付后事件 smartfarm/app/common/repositories/store/order/StoreOrderRepository.php:591 order.paySuccess 在核心支付事务提交后触发,当前不能作为原子事实写入点
外部支付回调防重 smartfarm/crmeb/listens/pay/OrderPaySuccessListen.php:25 回调先检查 groupOrder.paid,再调用 paySuccess;没有云仓业务幂等
支付参数 smartfarm/app/common/model/store/order/StoreGroupOrder.php:73 普通、合单支付均使用订单组编号和 attach=order,可继续复用现有支付入口
未支付取消 smartfarm/app/common/repositories/store/order/StoreGroupOrderRepository.php:153 先在事务中标记订单删除,再投递 CancelGroupOrderJob
取消库存恢复 smartfarm/crmeb/jobs/CancelGroupOrderJob.php:29 队列任务在独立事务中恢复普通商品库存和优惠券
退款申请 smartfarm/app/common/repositories/store/order/StoreRefundOrderRepository.php:805 退款单、退款明细和订单明细可退数量在同一事务中更新
退款同意 smartfarm/app/common/repositories/store/order/StoreRefundOrderRepository.php:1427 实际退款、退款后处理和退款状态在事务内推进,refund.agree 在其后触发
资金退款与商户扣款 smartfarm/app/common/repositories/store/order/StoreRefundOrderRepository.php:2592 普通退款调用支付适配器后会直接扣减商户锁定款
退款后副作用 smartfarm/app/common/repositories/store/order/StoreRefundOrderRepository.php:1721 普通逻辑恢复商品库存、处理分佣和商户财务记录
退款拒绝/取消 smartfarm/app/common/repositories/store/order/StoreRefundOrderRepository.php:1374:2031 两条路径各自在事务内恢复订单明细可退状态,用户取消路径没有统一事件

3. 关键风险

仅在 order.paySuccessrefund.agree 后追加监听器不够:

  • 事件发生在核心事务之后,事件丢失时会出现 CRMEB 已支付而云仓仍待付款。
  • 普通 paySuccess 会把订单实付金额计入商户锁定款;云仓首次订单应按供货价另行结算,二次代销收入应进入用户本金/收益账本,直接复用会重复入账。
  • 普通退款会扣减商户锁定款;云仓订单若没有普通商户入账,会产生错误扣款。
  • 首次云仓订单不占普通商品库存,普通退款或取消不能把数量加回来源商品。
  • 二次代销订单既要恢复 CRMEB 展示库存,又要释放云仓批次分配,两个动作必须可对账。

4. 冻结架构

G3-ARCH-001 确认为“CRMEB 交易载体 + 农业订单绑定 + 事务内薄适配”:

  1. 云仓首次购买使用独立 CloudOrderCreateService,不经过普通购物车确认;在一个事务中创建 CRMEB 订单组、子订单、订单明细、FarmOrderBinding、云仓首次明细、库存流水和幂等结果。
  2. 付款继续使用 CRMEB 现有订单支付入口、支付参数和支付渠道,不新建平行支付引擎。
  3. 新增 eb_farm_order_binding,按订单明细声明 business_typefinance_policystock_policyfulfillment_policy,核心适配点只查询该表,不在多个业务表中猜类型。
  4. finance_policy=farm_managed 的订单不生成普通商户锁定款、普通订单净收入、分销佣金、积分/会员值/赠券、推广资格、普通打印/短信和普通退款扣款;对应履约、通知、统计和资金事实进入农业模块。
  5. 支付成功事务内由 FarmOrderPaymentAdapter 只做幂等状态确认和 outbox 写入;复杂状态推进、通知和结算仍异步执行。
  6. 退款的三个申请入口、拒绝、取消、executeRefund() 扣商户锁定款前和 refundAfter() 分别调用 FarmOrderRefundAdapter,不能把 refund.agree 当成唯一事实来源。
  7. 未支付取消由 FarmOrderCancelAdapterCancelGroupOrderJob 的任何普通库存/活动库存/优惠券恢复之前返回恢复策略,再释放对应云仓锁定。
  8. order.paySuccessrefund.agree 等现有事件只用于唤醒消费者和兼容修复,业务表与 outbox 才是事实来源。

5. 三类订单策略

订单策略 普通商品库存 普通商户财务 履约 农业适配
normal CRMEB 扣减/恢复 完整保留 CRMEB 普通履约 快速跳过
cloud_primary 不扣、不恢复来源商品库存 跳过;商户货款按供货价进入云仓供货账本 付款后待选邮寄/自提/代销 活动库存、首次明细、去向和商户供货结算
cloud_resale 扣减/恢复 CRMEB 镜像库存 跳过;销售收入进入云仓批次与用户账本 平台云仓履约 创建时 FEFO 分配,支付/完成/退款推进分配

V1 约束:

  • 一个 CRMEB group_order_id 不得混合 normalfarm_managed 明细;确认订单时拆成不同订单组或提示分别提交。原因是支付统计、赠券、会员值和部分提交后副作用按订单组执行。
  • cloud_only 映射 SKU 不参加代客下单、积分商城或其他营销活动,避免绕过云仓分配事务。
  • 云仓首次订单一单一活动 SKU;二次代销订单可以包含多个 cloud_only SKU,但全部必须由同一云仓履约主体处理。

6. 精确接入点

流程 同步接入点 事务内动作
首次抢购创建 CloudOrderCreateService 自有事务 条件扣减活动库存,创建 CRMEB 三层订单、绑定、首次明细、库存流水、幂等记录和 outbox
二次普通订单创建 StoreOrderCreateRepository 在订单明细 insertAll 后、order.create order_id 回查明细,创建绑定,FEFO 锁定批次和库存流水;失败则整单回滚
支付成功 StoreOrderRepository::paySuccess 核心事务结束前 根据绑定执行财务/履约策略,确认首次或二次付款事实并写 outbox
未支付取消 CancelGroupOrderJob 进入任何恢复动作之前 取得 restore_product_stock/restore_activity_stock/restore_coupon 决策;首次只释放活动锁,二次恢复镜像库存和 FEFO 分配
退款申请 退款单创建事务 冻结首次选择或二次分配,记录申请事实
退款拒绝/取消 各自状态事务 解冻首次选择或二次分配,恢复观察期
退款资金执行 StoreRefundOrderRepository::executeRefund() 调用 subLockMoney() 之前 farm_managed 跳过不存在的普通商户锁定款扣减;normal 保持原逻辑
退款后处理 refundAfter() 事务 首次不恢复来源商品库存;二次恢复镜像库存和 FEFO 分配;平台券按原规则恢复;写农业反向流水和 outbox

7. 采用方案与替代方案

方案 结论 原因
CRMEB 订单载体 + 统一绑定表 + 事务适配器 采用 保留支付售后能力,同时明确库存、财务和履约策略
只监听 order.paySuccess/refund.agree 否决 发生在事务后且退款路径不完整,无法保证原子事实
首次抢购直接走普通购物车 否决 强制地址/配送、普通优惠和普通库存语义均不匹配
新建完全独立的支付订单引擎 否决 重复支付、回调和退款能力,维护风险最高
把业务类型塞进订单 JSON 后到处判断 否决 查询和约束弱,无法稳定支撑取消、退款与对账
让普通商户财务先入账再冲正 否决 会形成短时错误余额和重复结算,异常时难以恢复

8. 必测场景

  1. 首次创建任一步失败,CRMEB 订单、活动库存、首次明细和幂等记录全部回滚。
  2. 二次普通订单 FEFO 分配失败,普通 SKU 扣减和订单创建全部回滚。
  3. 普通商品与 cloud_only 商品混合提交时形成不同 group_order_id 或明确拒绝,不产生混合策略订单组。
  4. 云仓首次和二次订单支付后不增加普通商户锁定款,不生成普通订单净收入。
  5. 同一支付回调重复十次,云仓付款事实和 outbox 只生成一次。
  6. 首次未支付取消不增加来源普通商品库存;二次未支付取消同时恢复镜像库存和云仓分配。
  7. 退款申请后冻结业务动作,拒绝或用户取消后正确解冻。
  8. 首次实际退款只释放活动/首次库存;二次实际退款同时恢复镜像库存并冲减云仓分配。
  9. executeRefund() 对普通订单仍按原逻辑扣减商户锁定款,对两类云仓订单在扣款前分流。
  10. 普通商城订单的创建、支付、取消、退款、库存、券、积分、会员值、推广、打印、短信、同城配送、卡密、统计和商户财务结果与改造前完全一致。

四、SPIKE-003:用户余额与商户供货货款正式入账

1. 要回答的问题

  1. 用户云仓本金、收益和回购款最终如何进入 CRMEB 可用余额。
  2. 商户供货货款在满足验收、首次有效购买和观察期后如何进入可提现余额。
  3. 现有 UserBill、商户冻结款和 FinancialRecord 能否直接承担幂等、业务账本和结算周期。
  4. 重复任务、并发入账、事务失败、冲正和金额字段上限如何处理。

2. 只读源码与数据库证据

证据 当前源码或数据库位置 事实
用户账单写入 smartfarm/app/common/repositories/user/UserBillRepository.php:243 bill/incBill/decBill 只创建 eb_user_bill,不修改 eb_user.now_money
后台余额调整 smartfarm/app/common/repositories/user/UserRepository.php:706 changeNowMoney 在事务中改余额并写 sys_inc_money/sys_dec_money,但语义是人工系统调整,且没有业务来源幂等键
充值入账 smartfarm/app/common/repositories/user/UserRechargeRepository.php:199 充值回调以充值单 paid 防重,在事务中写账单和余额;不能代替云仓账本资格校验
商户可用余额 smartfarm/app/common/repositories/system/merchant/MerchantRepository.php:113 addMoney 只增加 mer_money,不创建商户财务流水;调用时必须经现有核心字段写入许可
商户冻结款 smartfarm/app/common/repositories/system/merchant/MerchantRepository.php:907 addLockMoney 受全局 mer_lock_time 控制,启用时只创建冻结账单
商户自动解冻 smartfarm/crmeb/listens/AutoUnlockMerchantMoneyListen.php:24 每 20 分钟按冻结账单创建时间和全局天数解冻,不能表达不同云仓供货账本各自的 eligible_at
商户财务流水 smartfarm/app/common/dao/system/merchant/FinancialRecordDao.php:61 inc/dec 只创建 eb_financial_record,自身不修改商户余额
普通订单财务 smartfarm/app/common/repositories/store/order/StoreOrderRepository.php:396 普通订单同时生成多条财务记录并创建商户冻结款,金额口径是普通订单净收入,不是云仓供货价
用户账单索引 当前库 information_schema.STATISTICS category + type + link_id 只是普通索引,没有唯一约束
商户流水索引 当前库 information_schema.STATISTICS financial_record_sn、来源业务号均没有唯一约束,不能作为并发防重设施
当前金额字段 当前库 information_schema.COLUMNS user.now_moneydecimal(8,2)financial_record.numberdecimal(8,2)merchant.mer_moneydecimal(12,2)
当前 SQL 模式 当前库 @@GLOBAL.sql_mode 未启用严格模式,金额溢出不能依赖数据库抛错,应用必须显式校验且上线前扩大字段容量

由这些事实可以确定:

  • 现有账单和财务流水是最终账户的展示记录,不是云仓业务资格、公式、审核和幂等事实。
  • 用户入账不能调用 changeNowMoney 冒充后台人工调账,也不能复用充值单语义。
  • 商户货款到 eligible_at 时已经完成云仓自己的观察期,再进入全局商户冻结期会造成重复等待。
  • UserBillFinancialRecord 都没有满足云仓 exactly-once 要求的唯一键,必须由独立入账桥接记录提供并发防重。
  • 现有余额写法普遍是“读取当前值后保存”,专用入账事务必须先锁定目标账户行,避免与充值、消费、普通订单解冻并发时丢失更新。

3. 冻结架构

G3-CODE-003G3-FIN-002G3-CODE-007 确认为“业务账本/结算单 + 入账桥接 + CRMEB 最终账户”:

用户云仓账本
  -> eb_farm_financial_posting
  -> eb_user.now_money + eb_user_bill

商户供货账本
  -> 商户日结单及明细
  -> eb_farm_financial_posting
  -> eb_merchant.mer_money + eb_financial_record

统一新增 eb_farm_financial_posting,保存来源类型、来源 ID、入账方向、目标账户、金额、唯一 posting_key、处理状态、CRMEB 流水 ID、入账前后余额和失败信息。业务账本仍是计算来源,入账记录只负责把一笔已获资格的金额安全映射到最终账户。

冻结规则:

  1. 一个来源账本或结算单的同一版本、同一方向只能有一个成功入账记录。
  2. 重试只重用原 posting_key;成功记录再次执行直接返回首次结果。
  3. 目标账户、CRMEB 流水和入账记录必须在同一个数据库事务中完成。
  4. 原成功入账不修改、不删除;冲正创建反向入账记录并关联 reversal_of_posting_id
  5. 现有 UserBillFinancialRecord 的非唯一来源字段只用于展示和追查,防重以 eb_farm_financial_posting 唯一键为准。

4. 用户余额入账事务

新增 FarmUserBalancePostingService,一次只处理一条用户业务账本:

  1. posting_key=farm_user_ledger:{ledger_id}:credit:v{version} 创建或锁定入账记录。
  2. SELECT ... FOR UPDATE 锁定用户账本,校验状态为可入账、金额大于零、无冻结、无未决退款或对账差异。
  3. 锁定 eb_user 目标行,读取入账前余额并显式校验目标字段容量。
  4. 增加 now_money,再调用 UserBillRepository::incBill 写入 category=now_moneytype=farm_cloud_settlementlink_id=ledger_id 的流水。
  5. 保存 bill_id、入账前后余额、完成时间,并把业务账本推进为已结算。
  6. 同一事务写入 Outbox;用户通知和短信在提交后消费,不作为入账成功条件。

UserBillRepository::TYPE_INFO 增加 now_money/farm_cloud_settlement=云仓结算入账。用户账本详情继续分别展示本金、收益和回购款,CRMEB 余额流水只展示本次合计,不能用一条余额流水反推业务公式。

5. 商户货款入账事务

商户货款采用“按日结单、直接进入可用余额”的方案:

  1. 每日 02:00 按商户汇总前一自然日 eligible_at 已到期且未入单的供货账本;生成一张 eb_farm_merchant_supply_statement 和逐账本明细。
  2. 结单金额为所有明细 settle_amount 之和;结单生成后明细不可替换,差异通过调整单和下一张结单处理。
  3. FarmMerchantBalancePostingServiceposting_key=farm_merchant_statement:{statement_id}:credit:v{version} 锁定结单、入账记录和商户账户行。
  4. 通过现有 MerchantRepository::addMoney 增加 mer_money,保留核心字段写入许可;由于账户行已在同一事务中锁定,不会与普通订单自动解冻并发覆盖。
  5. 调用 FinancialRecordRepository::inc 创建 financial_type=farm_supply_settlementfinancial_pm=1type=0 的商户财务流水,order_id 使用结单主键,order_sn 使用结单号。
  6. 保存 financial_record_id、入账前后余额和完成时间,同时推进结单及其明细为已入账。

不调用 addLockMoney,也不创建 mer_computed_money:供货账本自身的 eligible_at 已经包含验收、首次有效购买和观察期,再套用全局冻结天数没有业务依据。二次代销进度和是否滞销仍不影响商户已满足条件的供货货款。

6. 自动审核、冲正与容量

V1 自动入账默认阈值:

对象 默认自动上限 超限处理
单条用户入账 10,000.00 转人工复核,不改变应结金额
单用户自然日累计 50,000.00 当日后续记录转人工复核
单张商户结单 100,000.00 转人工复核

阈值使用系统配置,只有超级管理员可修改并记录审计;规则快照不完整、金额不闭合、有冻结/退款/争议或来源版本变化时,无论金额大小都必须人工。

冲正规则:

  • 正常经营亏损、滞销、平台优惠、仓储损耗和平台原因售后不得向用户或商户最终账户发起借方冲正。
  • 只有已审核确认的重复/错误入账,或外部支付最终撤销且原权益不再成立,才先创建 eb_farm_cloud_ledger_adjustment,审核后生成新的借方入账记录。
  • 自动冲正不得使 now_moneymer_money 小于零;余额不足时进入 recovery_pending,冻结后续同主体农业入账并由后续可得款先抵扣。
  • 不自动部分扣款,不直接改原账本金额,不使用负数写入 unsigned 字段。
  • 退款发生在正式入账前时,冻结或重算原业务账本,不先入账再冲回。

上线前执行只扩容、不改语义的字段升级:

  • eb_user.now_moneyeb_user_bill.numbereb_user_bill.balance 扩为 decimal(14,2) unsigned
  • eb_merchant.mer_money 扩为 decimal(14,2)
  • eb_financial_record.number 扩为 decimal(14,2)
  • 所有新增农业金额字段使用 decimal(14,2);中间比例计算使用更高精度,最终入账按分舍入。

即使字段已扩容,服务仍需校验金额、目标余额和实际更新后的余额,不能依赖当前非严格 SQL 模式处理溢出。

7. 采用方案与替代方案

方案 结论 原因
独立入账桥接 + CRMEB 最终账户 采用 同时获得业务可审计、数据库幂等和现有余额/提现复用
用户直接调用后台 changeNowMoney 否决 类型和操作者语义错误,没有云仓来源防重
商户到资格日后再走全局冻结款 否决 重复等待,无法表达每条供货账本的独立资格时间
只写 UserBill/FinancialRecord 否决 两者都不会自动同步目标余额,也没有来源唯一约束
只改余额、不写现有流水 否决 现有余额明细和商户资金页面无法解释变动
在原成功记录上改金额 否决 破坏审计链,重试和对账无法判断首次事实

8. 必测场景

  1. 同一用户账本并发执行十次,只增加一次余额并产生一条成功入账记录和一条 CRMEB 余额流水。
  2. 用户充值、余额消费和云仓入账并发时不丢失任何一笔变化,流水末余额与账户余额一致。
  3. 写余额后创建账单失败时整笔事务回滚,重试仍可成功。
  4. 商户普通订单解冻与云仓结单入账并发时,两笔金额都保留。
  5. 同一商户结单重跑不重复增加余额或创建财务流水。
  6. 商户供货账本尚未验收、未过观察期或有首次退款时不进入结单。
  7. 二次代销滞销不阻止已合格商户货款进入下一日结单。
  8. 超过自动阈值时只转人工复核,不改变账本金额。
  9. 冲正余额不足时不产生负余额,进入待追偿并冻结后续农业入账。
  10. 扩容前后普通充值、余额消费、普通商户解冻、提现和财务报表回归通过。

五、SPIKE-004:独立补运费单支付能力

1. 要回答的问题

  1. 补运费是否必须改写原云仓商品订单,还是可以使用独立支付单。
  2. 独立支付单如何复用微信、支付宝和余额支付。
  3. 支付渠道如何把成功回调准确分发给补运费业务。
  4. 重复回调、支付超时、迟到付款和退款如何闭环。
  5. 需要修改哪些 CRMEB 公共文件,哪些逻辑必须留在农业领域服务内。

2. 只读源码证据

证据 当前源码位置 事实
支付参数最小契约 smartfarm/app/common/model/user/UserRecharge.php:58 独立充值单只需提供 order_snpay_priceattachbody 和可选 return_url
支付驱动入口 smartfarm/crmeb/services/pay/Pay.php:16 Pay 根据驱动名加载微信、支付宝等支付实现,不要求订单必须是商城订单
支付驱动接口 smartfarm/crmeb/services/pay/PayInterface.php:13 公共接口提供支付、退款和通知能力
微信支付参数 smartfarm/crmeb/services/pay/storage/Weixin.php:22 H5、公众号、小程序、App 等渠道都直接消费上述支付参数,并把 attach 传给微信
支付宝支付参数 smartfarm/crmeb/services/pay/storage/Alipay.php:21 支付宝把 attach 写入 passback_params,订单号和金额不依赖商城表
微信回调分发 smartfarm/crmeb/services/wechat/Payment.php:888 验签成功后触发 pay_success_{attach},并携带订单号、渠道原始数据和组合支付标记
支付宝回调分发 smartfarm/crmeb/services/AlipayService.php:333 验签成功后同样触发 pay_success_{attach}
事件注册 smartfarm/app/event.php:98 充值、商城订单、预售等支付业务均通过独立事件监听器接收回调
独立业务范例 smartfarm/crmeb/listens/pay/UserRechargeSuccessListen.php:20 监听器仅按订单号调用业务仓库,证明回调无需经过商城订单
充值成功事务 smartfarm/app/common/repositories/user/UserRechargeRepository.php:201 充值单使用本地 paid 状态防重复,并在事务内更新余额、账单和支付状态
余额支付范例 smartfarm/app/common/repositories/store/order/StoreOrderRepository.php:188 余额支付可直接在本地事务内扣余额、写用户账单并调用业务成功逻辑
微信退款适配 smartfarm/crmeb/services/pay/storage/Weixin.php:140 可按原商户订单号、原支付金额、退款金额和退款单号发起退款
支付宝退款适配 smartfarm/crmeb/services/pay/storage/Alipay.php:82 可按原商户订单号和独立退款单号发起退款
通用查询限制 smartfarm/crmeb/services/PayStatusService.php:27 当前只实现扫码枪支付查询,不能直接作为所有补运费渠道的统一主动查询器
通用关单限制 smartfarm/crmeb/services/pay/PayInterface.php:13 当前接口没有统一关单方法,超时后仍必须防御迟到成功回调

结论:CRMEB 支付层按“支付参数 + attach 事件”工作,不要求业务单来自 store_group_order。独立云仓运费单可以直接复用现有渠道,且比修改原商品订单金额更符合现有扩展方式。

3. 冻结决策

G3-CODE-004 确认为:

  • 新建 eb_farm_cloud_freight_order,不新增 store_group_orderstore_orderstore_order_product 记录。
  • 原云仓首次订单只记录商品本金,补运费不回写其 pay_price、优惠分摊或商户供货货款。
  • 支付回调标识固定为 attach=farm_cloud_freight,事件名固定为 pay_success_farm_cloud_freight
  • 每张运费单对应一个云仓首次订单明细的一次地址/运费报价版本;同一明细任一时刻只能有一张当前待支付运费单。
  • 地址、运费模板、计费参数和金额在创建运费单时保存快照,支付成功后不可修改。
  • V1 支持用户端已启用的微信 H5/公众号/小程序/App、支付宝 H5/App和余额支付;不支持线下、扫码枪、服务商组合支付或商户子账户收款。
  • 渠道收款主体为平台,后续实际物流费用属于平台履约成本,不进入用户云仓本金或代销收益。

4. 支付参数与状态

CloudFreightOrder::getPayParams() 固定返回:

order_sn  = freight_order_no
pay_price = freight_amount
attach    = farm_cloud_freight
body      = 云仓邮寄补运费
return_url = 支付宝 H5 可选

运费单分别保存业务、支付和退款状态:

维度 状态
业务状态 待支付、邮寄已确认、已取消、已超时、异常
支付状态 未支付、支付中、已支付、迟到支付、退款处理中、已退款
渠道关单状态 无需关单、待关单、已关单、关单失败、不支持

还必须保存 freight_order_no、订单明细、用户、选择版本、地址/运费快照、应付/实付金额、支付方式、支付驱动、渠道交易号、渠道回调时间、超时时间、当前版本和异常原因。

发起支付时不接受前端提交金额、订单号、attach 或用户 ID。服务端按已锁定运费单生成支付参数;切换支付渠道时取消旧的未支付运费单并生成新单号,避免一个商户订单号跨渠道留下歧义。

5. 支付成功事务

在线渠道流程:

渠道验签
→ pay_success_farm_cloud_freight
→ CloudFreightPaySuccessListen
→ CloudFreightPaymentService::paySuccess(orderNo, callback)
→ 锁运费单和云仓订单明细
→ 校验用户、金额、渠道交易号和当前选择版本
→ 幂等记录支付事实
→ 固化邮寄去向与地址/运费快照
→ 创建邮寄履约单和 Outbox
→ 同一事务提交

金额统一规范化为元后使用精确小数比较:

  • 微信 V3 读取 amount.total 分,微信 V2 读取 total_fee 分。
  • 支付宝回调适配需把验签后的 total_amounttrade_no 一并传入业务事件;不能只凭订单号确认支付。
  • 回调金额、币种或订单状态不一致时不确认邮寄,运费单进入异常并告警。
  • 重复回调若订单号、金额和渠道交易号一致则直接幂等成功;同订单号出现不同交易号或金额时转人工复核。

余额支付由 CloudFreightPaymentService::payBalance() 完成:按固定顺序锁运费单、订单明细和用户账户,在同一事务内扣减 eb_user.now_money、写 UserBill(type=farm_cloud_freight_pay)、确认邮寄并创建履约。不能照搬现有未显式加锁的余额支付代码。

6. 超时、并发与迟到付款

CloudFreightExpireJob 每分钟扫描到期单,并在事务内锁定运费单和订单明细:

  1. 已支付时不处理。
  2. 未支付且仍在选择期时,运费单转已超时,清除临时邮寄选择,订单明细回到待选择。
  3. 未支付且已过选择截止时,运费单转已超时,并通过同一去向服务自动转为代销。
  4. 事务提交后通过 Outbox 尝试渠道关单;关单失败可重试,但不回滚本地超时结果。

由于当前 CRMEB 没有统一关单接口,系统必须始终接受“超时任务与成功回调并发”的可能:

  • 锁内只有一个流程能先完成状态迁移。
  • 如果渠道支付已成功,但本地已经取消、超时或自动代销,则记录为迟到支付,禁止把去向改回邮寄。
  • 迟到支付自动创建全额退款单;退款成功前在用户订单中心显示“退款处理中”。
  • pay_result 只查询本地运费单状态,不以客户端支付 SDK 的成功回调作为最终结果。

7. 退款设计

新建 eb_farm_cloud_freight_refund,V1 只允许整张运费单全额退款,不做部分运费退款。

允许退款的场景:

  • 支付成功回调迟于运费单超时/取消或订单明细已自动代销。
  • 首次云仓订单在发货前完成全量退款。
  • 平台在发货前取消邮寄履约。
  • 审计确认的重复扣款或错误支付。

已发货后不因用户改选退运费;平台责任取消、物流未揽收或其他异常由客服按异常单审核后全额退款。

在线退款使用原支付驱动和原商户订单号,参数至少包含 pay_pricerefund_price、唯一 refund_id 和原因。余额退款在事务内增加用户余额并写 UserBill(type=farm_cloud_freight_refund)。所有退款重试复用同一 refund_no,状态使用待提交、处理中、成功、失败待重试和人工复核。

8. 最小代码影响

类型 文件 作用
新建 app/common/model/farm/cloud/CloudFreightOrder.php 运费支付参数和关联
新建 app/common/model/farm/cloud/CloudFreightRefund.php 独立退款记录
新建 对应 DAO/Repository 加锁查询、当前运费单和退款幂等
新建 crmeb/services/farm/cloud/CloudFreightQuoteService.php 复用 CRMEB 运费模板并生成报价快照
新建 crmeb/services/farm/cloud/CloudFreightPaymentService.php 创建、发起支付、余额支付、成功事务和迟到支付
新建 crmeb/services/farm/cloud/CloudFreightRefundService.php 在线/余额全额退款和重试
新建 crmeb/listens/pay/CloudFreightPaySuccessListen.php 接收独立支付事件
修改 app/event.php 注册 pay_success_farm_cloud_freight
修改 crmeb/services/AlipayService.php 将验签后的 total_amounttrade_no安全传给监听器
新建 crmeb/listens/farm/timer/CloudFreightExpireListen.php + crmeb/jobs/farm/CloudFreightExpireJob.php 监听器按间隔投递;Job 处理超时、回待选择/自动代销和关单 Outbox
新建 crmeb/listens/farm/timer/CloudFreightPaymentRepairListen.php + crmeb/jobs/farm/CloudFreightPaymentRepairJob.php 监听器按间隔投递;Job 修复支付中、迟到回调和本地状态
新建 crmeb/jobs/farm/RefundCloudFreightJob.php 渠道退款提交与重试

不修改微信/支付宝支付驱动的下单与退款主流程,不调用 StoreOrderRepository::paySuccess(),不把运费单加入普通商城财务、佣金、优惠或商户分账。

9. 采用方案与替代方案

方案 结论 原因
独立运费单 + 现有支付驱动 + 独立回调事件 采用 复用渠道能力,同时保持商品本金、商户货款和运费审计边界
修改原云仓商品订单金额后二次支付 否决 已支付订单金额、退款分摊和商户货款都会失真
生成一张普通商城虚拟商品订单 否决 会误入商品销量、佣金、优惠、商户财务和发货状态
只信前端支付成功结果 否决 客户端结果可中断或伪造,不能形成最终支付事实
超时后忽略迟到回调 否决 平台已收款但用户未获得邮寄权益,会造成资金差异
V1 支持部分运费退款 否决 当前一条明细只有一个整体去向,全额退款更可审计

10. 必测场景

  1. 微信、支付宝和余额支付各完成一次,运费不进入商品本金或商户货款。
  2. 同一成功回调重放十次,只确认一次邮寄、创建一张履约单。
  3. 回调金额或交易号冲突时不确认邮寄并产生告警。
  4. 支付成功事务中创建履约失败时,本地支付确认和去向迁移全部回滚,回调重试可恢复。
  5. 超时任务与成功回调并发时,只产生邮寄或超时结果之一。
  6. 仍在选择期超时后回到待选择,超过截止时间超时后只自动代销一次。
  7. 已自动代销后收到迟到成功回调,不改去向并自动发起一次全额退款。
  8. 未支付运费单切换支付渠道时旧单失效,新单使用新订单号。
  9. 首次商品退款与运费退款并发时不会漏退、重复退或发货。
  10. 余额支付和普通余额消费并发时余额正确,账单末余额与账户一致。
  11. 在线退款任务重复执行时始终复用同一退款单号。
  12. 普通商城订单、充值、普通退款和原支付回调完整回归。

六、SPIKE-006:服务端认证、路由与移动布局

1. 要回答的问题

  1. 现场人员是否需要新建独立账号系统。
  2. 现有客服 Token 能否同时承载 PC 客服和移动现场作业。
  3. 客服端是否已有可复用的菜单权限和数据范围能力。
  4. 三栏客服工作台能否直接改成移动响应式页面。
  5. 同一前端工程如何形成互不干扰的 PC/移动双入口。

2. 只读源码、数据库与运行态证据

证据 当前事实 设计含义
route/service.php 后端统一使用 /ser 前缀,公开登录接口和受 ServiceTokenMiddleware 保护的固定路由都在一个文件中 V1 继续局部扩展该路由文件,不同时重构路由加载器
ServiceTokenMiddleware JWT 的 jti 类型固定为 service,每次请求重新读取 store_service;当前只校验 is_open 和商户状态,没有农业职责、权限或对象范围 身份可复用,但必须增加账号状态加固和独立农业授权层
StoreServiceRepository Token 使用 service_{token} 缓存并按活动请求续期 PC 与移动入口可共享同一 Token,不需要第二种 guard
eb_store_service 实际表结构 主键为 service_id,含 mer_idaccountpwdis_openstatusis_delaccount 当前只有普通索引 可作为统一人员身份,但上线迁移前必须清理重复账号并增加唯一约束
平台/商户客服维护控制器 商户端创建时校验全局账号重复;平台端当前没有完整维护 accountpwdis_open 平台管理端需扩展现有客服表单,不另建人员账号表
app/controller/service/Service.php 现有客服接口有按会话/商户约束的查询,也有按订单 ID 直接读取的宽松模式 新农业接口禁止复制宽松模式,查询和写入都必须在 Repository 层套用范围
smartfarm_service/package.json Vue 2.6、Vue Router 3、Vuex 3、Element UI 2;没有移动 UI 库 PC 可复用 Element UI;移动作业页使用独立轻量组件层,不把 Element 表格缩成手机页面
src/router/index.js 只有 /kefu/login/kefu/dashboard 和 404,均为静态路由 农业路由可继续静态注册并按 meta.permission 控制,不需要引入后台动态菜单体系
src/views/kefu/pc/index.vue 三栏聊天页最小宽度 1000px,内容区固定在 1000-1200px,最低高度 600px 不能把现有客服页直接响应式压缩为现场移动端
运行页面 /kefu/dashboard 未登录时实际跳转 /kefu/login?redirect=%2Fkefu%2Fdashboard 正式前端路由前缀是 /kefu,旧文档中的 /service/dashboard 不作为实现依据

3. 现状安全前置项

以下属于复用身份前必须处理的基线加固,不是另建认证系统:

  1. ServiceTokenMiddleware 每次请求同时校验 is_open=1status=1is_del=0;商户账号继续校验商户存在且启用。
  2. 移除 userType 宏中无意义的未定义变量捕获,并为中间件增加认证回归测试。
  3. 迁移前检查所有非空 store_service.account 是否重复;通过后增加唯一索引,空值继续允许多个。
  4. 平台端客服维护表单补齐账号、密码、PC/移动登录开关;密码仍写入现有 pwd,不复制凭据。
  5. 现有 getOrderInfo 等宽松接口单列为基线修复任务;农业 Controller 不通过“先按 ID 查出,再在页面隐藏”实现权限。

4. 冻结身份与授权架构

V1 采用“一个身份、两个工作区、两层授权”:

eb_store_service
  └─ ServiceTokenMiddleware:确认是谁、账号是否仍有效
       └─ FarmServicePermissionMiddleware:确认能做什么
            └─ FarmServiceScopeRepository:确认能操作哪个对象
  • PC 客服和移动现场端复用 store_serviceservice JWT 和 SerToken
  • 不把职责和长期范围写入 JWT;调岗、停用或撤销范围后,下一次请求立即生效。
  • 职责到权限码的映射使用版本化代码配置,避免 V1 再造一套动态菜单/角色系统。
  • 具体农场、区域、栏舍、仓库、生产批次和任务授权使用数据库关系。
  • 路由权限只解决“动作权限”,Repository 查询范围和对象断言解决“数据权限”,两者必须同时通过。
  • 默认拒绝:没有农业档案、没有职责、范围过期或对象无法沿关系链落入范围时均返回 403。
  • 平台客服可被授予全部或指定商户范围;商户客服的最大边界永远是自身 mer_id,授权记录不能扩大该租户边界。
  • 结算、回购、规则修改、审核发布和最终异常方案仍只在平台管理端执行。

5. 职责与权限码

职责码 主要权限码 默认对象范围
platform_customer_service farm.order.readfarm.cloud.order.readfarm.exception.readfarm.exception.propose 全部或指定商户
merchant_customer_service farm.order.readfarm.cloud.order.readfarm.exception.readfarm.exception.propose 当前商户
crop_operator farm.task.readfarm.scan.resolvefarm.production.event.writefarm.output.writefarm.exception.report 农场、区域、地块、生产批次或任务
livestock_operator farm.task.readfarm.scan.resolvefarm.production.event.writefarm.asset.health.writefarm.output.writefarm.exception.report 农场、区域、栏舍、资产、生产批次或任务
warehouse_operator farm.task.readfarm.scan.resolvefarm.warehouse.receivefarm.fulfillment.packfarm.fulfillment.shipfarm.pickup.verifyfarm.exception.report 仓库、产出批次、履约单或任务
farm_reviewer farm.production.reviewfarm.output.reviewfarm.exception.read 指定农场或业务域

同一人员可有多个职责。write 权限允许保存草稿和提交本人记录,不自动包含审核权限;propose 只允许提交建议,不允许直接执行财务或最终处置。

6. 数据结构

新增两类农业授权数据:

  1. eb_farm_service_profile:一对一绑定 service_id,记录农业工作区启用状态、默认工作区、移动端开关、授权版本和最近变更时间。
  2. eb_farm_service_scope:记录 service_id + duty_code + scope_type + scope_id、有效期、状态和授权人。

scope_type 固定为 allmerchantfarmareaplotbarnassetwarehouseproduction_batchoutput_batchfulfillmenttask。范围解析只允许从当前业务对象沿已定义关系链向上匹配,禁止客户端提交一个属于授权农场但与当前订单无关的范围 ID 来绕过校验。

7. 登录后上下文

保留原登录响应,新增:

GET /ser/farm/context

返回当前人员可进入的 workspaces、职责码、权限码、范围摘要、scope_version、默认入口和功能开关。前端只用它决定显示和跳转;后端每个请求仍重新校验。旧客服账号没有农业档案时继续进入 /kefu/dashboard,不会因农业功能上线改变原工作流。

登录后入口规则:

  1. 只有传统客服能力:进入 /kefu/dashboard
  2. 只有现场能力:进入 /kefu/farm/work/tasks
  3. 同时拥有多个工作区:进入 /kefu/entry 选择,记住的默认入口仅用于体验,不作为授权依据。
  4. 访问无权限路由:显示无权限页,不退回登录页;Token 失效才重新登录。

8. PC/移动双入口

路由族 布局 页面
/kefu/dashboard 原三栏客服页 原客服会话,不改布局
/kefu/farm/desk/* 新农业桌面布局 农业订单、云仓订单、异常协同、审核
/kefu/farm/work/* 新移动优先布局 我的任务、扫码、过程记录、产出、仓储履约、异常上报

两个新布局位于同一 smartfarm_service 构建中,复用请求、Token、上传和格式化能力;不把现场人员放入消费者 smartfarm_user,也不新建第六个前端项目。

移动端不新增整套 UI 框架。V1 使用现有主题色、SVG/Iconfont、请求和上传能力,新增少量领域组件;日期、时间、数量等优先使用移动浏览器原生控件。扫码采用 @zxing/browser 的摄像头/图片解码能力,并始终提供手工输入编码的降级入口;摄像头权限被拒绝、浏览器不支持或非 HTTPS 时仍可继续作业。

9. 最小文件影响

后端:

  • 修改 ServiceTokenMiddleware.php、平台客服维护 Controller/Validate/Repository 和 route/service.php
  • 新增 FarmServicePermissionMiddlewareFarmServicePolicyFarmServiceProfileFarmServiceScope 的 Model/DAO/Repository。
  • 新增 service/farm/CommonOrderCloudTaskProductionWarehouseExceptionEvent Controller。
  • 所有领域 Repository 增加 scopeQuery()assertScopedObject(),Controller 不自行拼接范围 SQL。

前端:

  • 保留 src/views/kefu/pc/index.vue
  • 新增 src/router/farm.jssrc/store/modules/farmContext.jssrc/api/farm.js
  • 新增 src/layout/farmDesktop/index.vuesrc/layout/farmMobile/index.vue
  • 新增 src/views/farm/desk/*src/views/farm/work/*,并复用 request.jsauth.js、上传、SVG 和格式化能力。

10. 采用方案与替代方案

方案 结论 原因
复用客服身份 + 独立农业授权 采用 减少凭据与登录系统重复,同时补齐最小权限
新建现场人员账号系统 不采用 增加登录、密码、停用、审计和运维成本
把现场页放入用户 uni-app 不采用 消费者身份与工作人员权限边界混淆
把三栏客服页做全响应式 不采用 固定宽度和交互密度不适合手机现场操作
新建独立现场前端项目 V1 不采用 当前页面量可由同项目独立布局承载
复用平台/商户动态菜单权限 不采用 客服工程没有该运行链路,接入成本大于稳定权限码配置

11. 必测场景

  1. 原客服账号无农业档案时登录、聊天和退出行为不变。
  2. statusis_openis_del 变化后,已有 Token 下一请求立即失效。
  3. 同账号在 PC 和手机并行使用时 Token 续期和退出策略符合预期。
  4. 无职责、无权限码、范围过期和对象越界分别返回稳定 403 错误。
  5. 商户客服不能通过指定其他商户订单 ID 越权读取。
  6. 现场人员可查看授权农场任务,但不能操作同农场内未分配且没有上级范围覆盖的限制对象。
  7. 撤销范围后,已打开页面的下一次提交失败并刷新上下文。
  8. PC 客服工作台在 1280x800 保持原布局;农业桌面页在 1280x800 无横向溢出。
  9. 移动作业页在 360x800390x844430x932 下无文本或按钮重叠。
  10. 摄像头成功、权限拒绝、非 HTTPS、无摄像头和图片识别失败均有可继续的手工输入路径。
  11. 网络失败后表单草稿保留;重复提交使用同一 request_id 不生成重复记录。
  12. 原客服订单查询宽松路径修复后,正常会话内订单仍可查看,跨商户 ID 被拒绝。

七、SPIKE-007:地图跨端兼容边界

1. 要回答的问题

  1. 平台管理端、用户 H5/微信小程序和服务/现场端是否能统一使用同一地图供应商。
  2. 农场、区域、地块和自提点需要保存哪些坐标,采用哪一种坐标系。
  3. 浏览器地图 Key、服务端 WebService Key 和签名密钥如何隔离。
  4. 地图不可用、用户拒绝定位或供应商限额时,核心业务是否还能继续。
  5. 当前 CRMEB 地图实现可以复用到什么程度,哪些现状缺陷必须单列整改。

2. 只读源码、数据库与运行态证据

证据 当前事实 设计含义
后端公开接口 route/api/notLogin.php 已有 GET /api/lbs/geocoderapp/controller/api/Common.php::lbs_geocoder 接收 lat,long 并调用腾讯地图 现有用户定位链路可兼容保留,但不作为新农业 API 的字段规范
后端腾讯地图依赖 composer.json 使用 joypack/tencent-map:^1.0,该依赖支持地址、逆地址和坐标转换 可参考返回结构,不能直接作为新农业地图适配器
旧依赖传输安全 当前 joypack/tencent-map 请求实现关闭 TLS 证书和主机校验 新农业服务必须使用开启 TLS 校验的 HTTP 客户端
平台地图组件 smartfarm_admin/src/components/map/Map.vue 动态加载腾讯 GL JS,并在浏览器直接调用逆地址 JSONP 视觉和点选交互可复用,WebService 查询应改走后端代理
商户地图组件 smartfarm_mer/src/components/map/map.vue 支持点选和 pickMode 当前农业口径不要求商户维护农场点位,不新增商户农业地图页面
商户坐标缺陷 商户资料表单把 lat 标为经度、long 标为纬度,提交又写入 log;数据库实际约定 long 为经度、lat 为纬度 作为 CRMEB 基线缺陷单独修复并回归,不沿用到新接口
用户端定位 store/modules/location.js 使用 uni.getLocation({type: "wgs84"}) 后调用腾讯逆地址接口 与腾讯地图和 uni-app <map> 常用的 GCJ02 存在坐标系错配风险
用户端地图能力 已使用 uni.getLocationuni.chooseLocationuni.openLocationmanifest.json 声明 Maps、Geolocation 和微信位置权限 农场位置展示和外部导航无需新增原生地图插件
用户端版本 当前 @dcloudio/uni-app2.0.2-4080720251210002 V1 不以升级 uni-app 作为农场地图上线前提,现有定位链路另行回归
服务端现状 smartfarm_service 没有地图组件或定位依赖 先提供地址、位置页和外部导航;内嵌地图只能作为增强能力
数据库配置 eb_system_config 已定义 tx_map_key,当前 eb_system_config_value 没有平台值 设计可复用配置名,但环境必须在联调前完成实际配置检查
现有坐标字段 eb_delivery_station 使用 lng/lateb_merchant 使用 long/lateb_store_groupeb_user_address 使用 longitude/latitude,类型均为字符型 新农业域必须建立唯一命名和精度规范,在适配边界转换旧字段

官方兼容性依据:

  • uni.getLocation 说明 <map> 相关场景应明确使用 gcj02,不同端的定位配置和版本兼容性需要分别验证。
  • uni-app map 组件 支持 App、H5 和微信小程序,但 Key、域名白名单、组件层级和供应商额度属于运行环境配置。
  • 腾讯位置服务坐标说明 表明腾讯地图国内服务使用 GCJ02。

3. 冻结供应商与坐标规范

G3-CODE-008G3-EXT-001 确认为:

  1. V1 默认地图供应商为腾讯地图。平台管理端、用户端和服务端展示使用同一供应商,避免跨供应商坐标偏移。
  2. 代码保留 MapProviderInterface,V1 不提供运营侧多供应商切换页面,也不同时实现第二家供应商。
  3. 新农业业务事实坐标统一为 GCJ02
  4. 新表和新 API 只使用 longitudelatitudecoordinate_system,不再新增 longlng 或含义不明确的 lat/long 契约。
  5. 旧 CRMEB 字段只在 Repository 或前端适配层转换,不通过新接口继续扩散。
  6. V1 只保存点位和示意材料,不建设专业多边形编辑、路线规划、电子围栏、卫星图或人员实时轨迹。

4. 数据字段与公开边界

统一点位字段:

字段 类型 规则
longitude decimal(10,7) 可空;非空时范围 -180180
latitude decimal(10,7) 可空;非空时范围 -9090
coordinate_system varchar(8) 点位存在时固定 GCJ02

约束:

  • 经度和纬度必须同时为空或同时非空,禁止保存半个点位。
  • 不使用 0,0 表示“未配置”。
  • 接口以十进制字符串返回坐标,避免 JavaScript 浮点序列化擅自改变精度。
  • 农场和统一自提点必须有真实点位后才能启用或公开。
  • 区域、地块和栏舍默认继承农场公开点位,使用示意图表达内部位置;确有必要时可保存内部中心点,但不要求每个对象都有坐标。
  • 用户公开接口返回农场公开点位和公开地址,不返回敏感地块、栏舍、仓库内部精确点位。
  • V1 不建立空间索引,不以 GIS 多边形计算面积、相交或容量。

新增 eb_farm_pickup_point 统一承载云仓和农业产出的线下自提点。V1 由平台维护,保存名称、业务类型、地址、营业时间、联系人、点位、状态和版本;云仓活动只引用已启用且点位完整的自提点。

5. Key 与调用边界

配置 用途 可见范围
tx_map_key 腾讯 GL JS、uni.openLocation 相关客户端展示 Key 可按现有方式提供给受控前端,配置域名/小程序白名单
farm_tencent_map_server_key 地理编码、逆地址、地点建议等 WebService 调用 仅后端读取,不返回前端
farm_tencent_map_server_sk WebService 签名密钥,启用签名时使用 仅后端密钥配置,后台只显示掩码

安全规则:

  • 文档、日志、错误响应、前端构建产物和数据库导出中不记录真实 Key 或 SK。
  • 服务端 WebService 调用使用 Guzzle 或项目等价 HTTP 客户端,必须开启 TLS 证书与主机校验。
  • 后端限制请求频率、输入长度和调用来源;地点建议不得成为无认证公共代理。
  • 客户端 Key 与服务端 Key 分离额度和白名单,不能把服务端 SK 写入 manifest.json
  • 腾讯地图生产额度、商业授权和采购判断属于发布前外部配置门,不阻塞当前设计,也不默认承诺付费购买。

6. 后端抽象与接口

推荐目录:

crmeb/services/farm/support/map/
  MapProviderInterface.php
  TencentMapProvider.php
  FarmMapService.php
  GeoPoint.php

职责:

  • GeoPoint 负责坐标范围、成对空值和坐标系校验。
  • TencentMapProvider 只处理腾讯请求签名、超时、响应转换和供应商错误映射。
  • FarmMapService 负责缓存、限流、公开字段裁剪和供应商选择。
  • 领域 Repository 只保存规范化点位,不直接调用腾讯 SDK。

平台管理端增加只读辅助接口:

GET farm/map/geocode
GET farm/map/reverse_geocode
GET farm/map/suggest

接口必须经过平台认证和操作权限,返回统一地址结构与 GCJ02 坐标,不透传供应商完整原始响应。农场、自提点等业务写接口仍自行验证点位,不能因为点位来自地图辅助接口就跳过校验。

/api/lbs/* 保持响应兼容,后续通过同一 FarmMapService 收口并补充参数验证、TLS 和错误映射。该工作是 CRMEB 基线整改,必须单独回归现有首页定位、地址选择和附近门店,不与新农业页面首次提交绑在同一个大改任务中。

7. 四端页面策略

平台管理端

  • 保留 src/components/map/Map.vue 的腾讯 GL JS 加载、点选和标记能力。
  • 以向后兼容方式增加规范化 longitude/latitude 输出和“服务端地理编码”模式。
  • 新增 FarmMapPicker.vue 组合地址搜索、点选、坐标只读值、重新定位和失败状态。
  • 农场和自提点编辑使用真实点位;区域/地块编辑以农场继承点位和上传示意图为主。
  • 地图依赖不可用时允许保存草稿地址,但禁止启用或发布缺少有效点位的农场和自提点。

商户端

  • V1 不新增农场地图模块。
  • 商户资料页现有 lat/long/log 缺陷列为基线修复任务,修复时保持后端旧字段兼容并增加回归测试。

用户端

  • 新增农场位置卡片和全屏位置页,展示服务端保存的农场 GCJ02 点位。
  • 用户点击“导航”时调用 uni.openLocation,显式传入农场点位,不要求先获取用户当前位置。
  • “距我多远/附近农场”不是 V1 必要能力;只有用户主动使用时才申请定位权限。
  • 用户拒绝权限、H5 定位失败或当前 uni-app 版本不支持定位时,仍可查看地址、示意图并复制地址。
  • 现有 wgs84 -> 腾讯逆地址 链路单独列入基线整改,不能在农业页面任务中静默改动全站定位行为。

服务/现场端

  • 任务和履约页先显示结构化地址、位置摘要、复制地址与导航按钮。
  • 可增加轻量腾讯 GL 地图作为增强,不引入完整移动地图框架。
  • 地图加载失败、客户端 Key 缺失或浏览器不支持时,不阻断任务查看、扫码、记录或提交。

8. 可用性、超时与降级

场景 V1 处理
服务端地图配置缺失 后台辅助接口返回 MAP_NOT_CONFIGURED;草稿可保存地址,启用/发布被阻止
坐标非法或坐标系不支持 写接口拒绝,禁止自动保存 0,0
腾讯地图超时或 5xx 连接超时 2 秒、总超时 5 秒,仅超时/5xx 重试 1 次
供应商配额耗尽 返回 MAP_QUOTA_EXCEEDED,告警并允许人工填写地址/坐标
用户端地图脚本失败 展示地址、示意图、复制和重试入口
用户拒绝定位 不影响农场查看和导航;只关闭距离类增强
服务端现场地图失败 任务与事实录入继续,只隐藏地图增强

地理编码和逆地址结果可在 Redis 按规范化查询与坐标短期缓存。不得把供应商失败静默转换为空地址或默认坐标。

9. 采用方案与替代方案

方案 结论 原因
腾讯地图 + 供应商接口抽象 + GCJ02 采用 与现有管理端、uni-app 和配置最一致,跨端偏移最小
客户端直接调用全部 WebService 否决 暴露服务端额度和签名边界,难以统一限流与错误处理
新农业接口继续混用 lng/long/latitude 否决 现有字段已经不一致,会继续制造方向错误
V1 强制升级 uni-app 后再做地图 否决 农场展示可不依赖当前定位,升级风险不应阻塞主流程
V1 建专业 GIS 多边形编辑器 否决 租地容量依赖业务占用记录,不需要用地图几何替代
V1 同时接腾讯和另一家地图 否决 增加坐标转换、测试、额度和前端体积,当前没有业务收益
用户端默认自动获取位置 否决 增加权限阻断和兼容风险,农场导航只需要目标点位

10. 必测场景

  1. 农场和自提点经纬度成对保存,边界值和错误方向均被正确校验。
  2. 新 API 始终返回 longitude/latitude/coordinate_system=GCJ02,不返回旧字段别名。
  3. 平台地址搜索、地图点选和手工坐标三种入口得到同一规范化结果。
  4. 服务端 Key/SK 不出现在响应、日志、前端构建和导出中。
  5. 地图未配置、超时、5xx、配额耗尽和非法响应均映射稳定错误码。
  6. 地图不可用时草稿可保存,但农场/自提点无法启用;已有交易与现场任务仍可继续。
  7. 用户 H5、微信小程序和 App 使用保存的 GCJ02 点位打开导航,拒绝当前定位权限仍可查看地址。
  8. 用户端地图加载失败后地址、示意图、复制和重试入口可用。
  9. 服务端移动页地图失败不影响扫码、记录、上传和提交。
  10. 公开农场接口不泄露内部地块、栏舍或仓库精确点位。
  11. /api/lbs/geocoder、首页定位、地址选择和附近门店在适配器整改后响应兼容。
  12. 商户资料经纬度缺陷修复后,旧数据读取和保存方向正确,提交不再出现 log 字段。

八、专项验证结论

1. 七项专项验证

SPIKE-001SPIKE-007 已全部完成。四端 108 个页面低保真和 15 条跨端 Flow 随后完成并通过结构审计,当前阶段是用页面和 Flow 反向冻结数据、API、事件任务、代码文件与测试契约。后续发现新的事实疑点仍可新增专项验证,但不能用“开发时再看”代替明确记录。

2. 支付、物流与消息能力补充核验

为关闭 G3-EXT-003,本轮又对 CRMEB 现有第三方能力边界进行了只读源码核验:

能力 当前源码证据 V1 复用结论
支付 crmeb/services/PayService.php:29 按支付类型分派,微信/支付宝接口均携带业务 attach;各驱动实现 crmeb/services/pay/PayInterface.php 云仓首次订单和独立补运费单复用现有支付/退款驱动,以不同 attach 分流;不新增支付供应商
物流查询与电子面单 crmeb/services/express/storage/Express.php:122 提供快递公司、轨迹、面单和寄件接口;app/common/repositories/store/order/StoreOrderRepository.php:1889 已封装发货、消息和物流事件 农业履约保留独立业务履约单,通过薄适配调用现有快递能力;未开通面单服务时允许人工录入快递公司和单号,不能阻断履约
短信与微信消息 crmeb/services/sms/Sms.php:25 使用驱动管理器;crmeb/services/RoutineTemplateService.php:37 按模板编码发送小程序消息 新增农业模板编码、队列消费者和 eb_farm_notification_log,继续使用现有通道;通道失败不得回滚订单、结算或现场事实

最终边界:

  1. V1 不自建支付、快递轨迹、电子面单、短信或微信消息供应商实现。
  2. 领域 Service 只调用 SmartFarm 适配层,不直接散落调用第三方 SDK。
  3. 第三方超时统一映射稳定错误、进入重试和告警;消息发送属于提交后副作用。
  4. 未配置短信、模板消息或电子面单时,站内状态、人工快递单号和业务流程仍可使用。
  5. 生产账号、额度、模板审核和可能的商业开通费用属于上线前环境门;发生付费采购时仍须由项目方确认。

3. 数据库参照完整性补充核验

对当前 www_smartfarm_co 数据库执行只读 information_schema.REFERENTIAL_CONSTRAINTS / KEY_COLUMN_USAGE 核验,物理外键约束数量为 0。现有 CRMEB 表间关系依赖应用层校验、索引、事务与约定删除顺序,而不是数据库级 FOREIGN KEY

V1 因此冻结为:

  1. 90 张农业扩展表不新增物理外键,避免同一业务库混用两套删除、升级和恢复语义。
  2. 每个 RID/CID 引用字段必须在 34 中给出必要普通/组合索引,并在 Repository 或领域 Service 写事务内验证对象存在、状态、归属和版本。
  3. 订单、账本、库存、生产和溯源事实只作废或冲正,不因主数据停用而物理删除。
  4. verify_before/verify_after 与日常对账增加必需引用的孤儿扫描;发现孤儿时阻断发布或进入修复队列,不静默补默认对象。
  5. 如果未来要改用物理外键,必须作为全库级架构迁移单独评审,不能只给农业表局部添加。

Clone this wiki locally