-
Notifications
You must be signed in to change notification settings - Fork 0
29 g3 technical spike report
本文档记录 24-g3-g4-decision-register.md 中专项验证的只读事实、结论和设计回写。专项验证只读取源码、数据库结构和运行页面,不编写业务代码,不修改数据库业务数据。
| 编号 | 验证主题 | 状态 | 结论去向 |
|---|---|---|---|
SPIKE-001 |
CRMEB 商品规格身份稳定性 | 已完成 | 本文第二节;回写 15、19、21、22、24、28
|
SPIKE-002 |
普通订单创建、支付、退款扩展点 | 已完成 | 本文第三节;回写 15、19、20、21、22、24、28
|
SPIKE-003 |
用户余额和商户账单入账接口 | 已完成 | 本文第四节;回写 15、19、20、21、22、24、26、28
|
SPIKE-004 |
独立补运费单支付能力 | 已完成 | 本文第五节;回写 15、19、20、21、22、24、26、28
|
SPIKE-005 |
uni-app 页面、组件、DIY 和秒杀链路 | 已完成 | 25-user-uniapp-reuse-audit.md |
SPIKE-006 |
服务端认证、路由和移动布局 | 已完成 | 本文第六节;回写 15、17、18、19、20、21、22、24、26、28
|
SPIKE-007 |
地图跨端兼容边界 | 已完成 | 本文第七节;回写 06、10、14、15、16、17、18、19、21、22、24、25、26、28
|
- CRMEB 的
store_product_attr_value.value_id是否能作为云仓活动 SKU 的长期身份。 -
unique在商品编辑、规格修改和商品复制后是否保持不变。 - 来源商品变化后,草稿活动、已发布活动和历史订单分别如何处理。
| 证据 | 当前源码位置 | 事实 |
|---|---|---|
| 规格值主键 | 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 |
clearAttr 按 product_id 删除现有规格值行 |
unique 生成 |
smartfarm/app/common/repositories/store/product/ProductRepository.php:944 |
unique 由规格字符串、商品 ID 和商品类型计算 |
| 规格载荷构建 | smartfarm/app/common/repositories/store/product/ProductRepository.php:763 |
新增规格值时重算 sku 和 unique,不保留原 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_id和unique适合定位“当前 CRMEB 规格”,不适合作为云仓订单、批次、持仓和账本的永久外键。
G3-INV-001 确认为以下规则:
-
eb_farm_cloud_activity_sku.activity_sku_id是云仓活动 SKU 唯一、稳定的业务身份。 - 批次、订单明细、持仓、二次库存、分配记录、回购和账本只关联
activity_sku_id及各自快照。 -
source_product_id、source_value_id、source_unique和规范化规格文本只用于来源核验、草稿重匹配和运营诊断。 - 草稿或未发布活动允许重新核验来源 SKU;不存在或匹配不唯一时标记
unresolved,禁止发布并要求平台运营重新选择。 - 已发布活动不自动重匹配、不自动同步价格和规格规则;来源商品后续修改、删除或复制均不改变已发布活动、批次和历史交易。
- 需要采用新来源规格时,关闭未开始的旧配置并新建活动 SKU;不得覆盖已产生订单的快照。
活动 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 |
matched、changed、missing、ambiguous、unresolved
|
source_checked_at |
最近一次来源核验时间 |
source_sync_status 只描述来源健康度,不替代活动业务状态。已发布活动即使来源变为 missing,历史交易仍按活动快照继续执行;后台显示告警并限制创建新活动或追加新批次。
| 方案 | 结论 | 原因 |
|---|---|---|
独立 activity_sku_id + 来源快照 |
采用 | 历史身份稳定,能隔离 CRMEB 规格重建 |
永久关联 value_id
|
否决 | 商品编辑会删除重建,历史外键会失真 |
永久关联 unique
|
否决 | 商品 ID、类型或规格字符串变化会导致值变化 |
| 每次读取 CRMEB 当前商品覆盖活动数据 | 否决 | 会改变已付款用户的价格、规则和展示依据 |
| 复制整套 CRMEB 商品为云仓商品 | V1 不采用 | 增加商品维护和同步成本,仍不能替代活动规则快照 |
- 数据模型:活动 SKU 明确独立主键、来源诊断字段、发布快照和同步状态索引。
- 发布接口:发布前重新核验所有活动 SKU;
missing、ambiguous、unresolved一律失败。 - 草稿编辑:提供单 SKU 和整活动的只读来源重检;只有运营明确确认后才更新草稿来源引用。
- 已发布详情:同时返回活动快照与来源健康告警,前端不得用 CRMEB 当前规格覆盖活动快照。
- Repository:
CloudActivitySkuRepository管理稳定身份与快照;CRMEBProductAttrValueRepository只作为当前来源查询适配器。 - 审计:记录核验前后标识、状态、操作者、时间和活动状态,不记录敏感信息。
- 保存未改规格的来源商品后
value_id重建,草稿能够按规范化规格重匹配。 - 来源规格改名或删除后,草稿变为
changed/missing并禁止发布。 - 出现多个候选规格时标记
ambiguous,系统不得静默选择。 - 来源商品编辑、删除或复制后,已发布活动、订单、批次、持仓和账本关联不变。
- 已发布页面显示活动快照和来源告警,不显示新商品价格替代历史价格。
- 创建新活动可选择新的来源 SKU,但不得复用旧
activity_sku_id。
- 云仓首次抢购如何复用 CRMEB 订单和支付,又不要求用户先选择地址或去向。
- 二次代销普通订单在何处锁定云仓库存,才能与普通订单创建一起回滚。
- 支付成功、未支付取消、退款申请、退款拒绝/取消和资金退款完成分别在哪个事务边界接入。
- CRMEB 的普通商户财务、普通库存和履约副作用是否适用于云仓首次订单与二次代销订单。
| 证据 | 当前源码位置 | 事实 |
|---|---|---|
| 用户订单入口 | 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
|
两条路径各自在事务内恢复订单明细可退状态,用户取消路径没有统一事件 |
仅在 order.paySuccess 或 refund.agree 后追加监听器不够:
- 事件发生在核心事务之后,事件丢失时会出现 CRMEB 已支付而云仓仍待付款。
- 普通
paySuccess会把订单实付金额计入商户锁定款;云仓首次订单应按供货价另行结算,二次代销收入应进入用户本金/收益账本,直接复用会重复入账。 - 普通退款会扣减商户锁定款;云仓订单若没有普通商户入账,会产生错误扣款。
- 首次云仓订单不占普通商品库存,普通退款或取消不能把数量加回来源商品。
- 二次代销订单既要恢复 CRMEB 展示库存,又要释放云仓批次分配,两个动作必须可对账。
G3-ARCH-001 确认为“CRMEB 交易载体 + 农业订单绑定 + 事务内薄适配”:
- 云仓首次购买使用独立
CloudOrderCreateService,不经过普通购物车确认;在一个事务中创建 CRMEB 订单组、子订单、订单明细、FarmOrderBinding、云仓首次明细、库存流水和幂等结果。 - 付款继续使用 CRMEB 现有订单支付入口、支付参数和支付渠道,不新建平行支付引擎。
- 新增
eb_farm_order_binding,按订单明细声明business_type、finance_policy、stock_policy和fulfillment_policy,核心适配点只查询该表,不在多个业务表中猜类型。 -
finance_policy=farm_managed的订单不生成普通商户锁定款、普通订单净收入、分销佣金、积分/会员值/赠券、推广资格、普通打印/短信和普通退款扣款;对应履约、通知、统计和资金事实进入农业模块。 - 支付成功事务内由
FarmOrderPaymentAdapter只做幂等状态确认和 outbox 写入;复杂状态推进、通知和结算仍异步执行。 - 退款的三个申请入口、拒绝、取消、
executeRefund()扣商户锁定款前和refundAfter()分别调用FarmOrderRefundAdapter,不能把refund.agree当成唯一事实来源。 - 未支付取消由
FarmOrderCancelAdapter在CancelGroupOrderJob的任何普通库存/活动库存/优惠券恢复之前返回恢复策略,再释放对应云仓锁定。 -
order.paySuccess、refund.agree等现有事件只用于唤醒消费者和兼容修复,业务表与 outbox 才是事实来源。
| 订单策略 | 普通商品库存 | 普通商户财务 | 履约 | 农业适配 |
|---|---|---|---|---|
normal |
CRMEB 扣减/恢复 | 完整保留 | CRMEB 普通履约 | 快速跳过 |
cloud_primary |
不扣、不恢复来源商品库存 | 跳过;商户货款按供货价进入云仓供货账本 | 付款后待选邮寄/自提/代销 | 活动库存、首次明细、去向和商户供货结算 |
cloud_resale |
扣减/恢复 CRMEB 镜像库存 | 跳过;销售收入进入云仓批次与用户账本 | 平台云仓履约 | 创建时 FEFO 分配,支付/完成/退款推进分配 |
V1 约束:
- 一个 CRMEB
group_order_id不得混合normal与farm_managed明细;确认订单时拆成不同订单组或提示分别提交。原因是支付统计、赠券、会员值和部分提交后副作用按订单组执行。 -
cloud_only映射 SKU 不参加代客下单、积分商城或其他营销活动,避免绕过云仓分配事务。 - 云仓首次订单一单一活动 SKU;二次代销订单可以包含多个
cloud_onlySKU,但全部必须由同一云仓履约主体处理。
| 流程 | 同步接入点 | 事务内动作 |
|---|---|---|
| 首次抢购创建 |
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 |
| 方案 | 结论 | 原因 |
|---|---|---|
| CRMEB 订单载体 + 统一绑定表 + 事务适配器 | 采用 | 保留支付售后能力,同时明确库存、财务和履约策略 |
只监听 order.paySuccess/refund.agree
|
否决 | 发生在事务后且退款路径不完整,无法保证原子事实 |
| 首次抢购直接走普通购物车 | 否决 | 强制地址/配送、普通优惠和普通库存语义均不匹配 |
| 新建完全独立的支付订单引擎 | 否决 | 重复支付、回调和退款能力,维护风险最高 |
| 把业务类型塞进订单 JSON 后到处判断 | 否决 | 查询和约束弱,无法稳定支撑取消、退款与对账 |
| 让普通商户财务先入账再冲正 | 否决 | 会形成短时错误余额和重复结算,异常时难以恢复 |
- 首次创建任一步失败,CRMEB 订单、活动库存、首次明细和幂等记录全部回滚。
- 二次普通订单 FEFO 分配失败,普通 SKU 扣减和订单创建全部回滚。
- 普通商品与
cloud_only商品混合提交时形成不同group_order_id或明确拒绝,不产生混合策略订单组。 - 云仓首次和二次订单支付后不增加普通商户锁定款,不生成普通订单净收入。
- 同一支付回调重复十次,云仓付款事实和 outbox 只生成一次。
- 首次未支付取消不增加来源普通商品库存;二次未支付取消同时恢复镜像库存和云仓分配。
- 退款申请后冻结业务动作,拒绝或用户取消后正确解冻。
- 首次实际退款只释放活动/首次库存;二次实际退款同时恢复镜像库存并冲减云仓分配。
-
executeRefund()对普通订单仍按原逻辑扣减商户锁定款,对两类云仓订单在扣款前分流。 - 普通商城订单的创建、支付、取消、退款、库存、券、积分、会员值、推广、打印、短信、同城配送、卡密、统计和商户财务结果与改造前完全一致。
- 用户云仓本金、收益和回购款最终如何进入 CRMEB 可用余额。
- 商户供货货款在满足验收、首次有效购买和观察期后如何进入可提现余额。
- 现有
UserBill、商户冻结款和FinancialRecord能否直接承担幂等、业务账本和结算周期。 - 重复任务、并发入账、事务失败、冲正和金额字段上限如何处理。
| 证据 | 当前源码或数据库位置 | 事实 |
|---|---|---|
| 用户账单写入 | 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_money 为 decimal(8,2),financial_record.number 为 decimal(8,2),merchant.mer_money 为 decimal(12,2)
|
| 当前 SQL 模式 | 当前库 @@GLOBAL.sql_mode
|
未启用严格模式,金额溢出不能依赖数据库抛错,应用必须显式校验且上线前扩大字段容量 |
由这些事实可以确定:
- 现有账单和财务流水是最终账户的展示记录,不是云仓业务资格、公式、审核和幂等事实。
- 用户入账不能调用
changeNowMoney冒充后台人工调账,也不能复用充值单语义。 - 商户货款到
eligible_at时已经完成云仓自己的观察期,再进入全局商户冻结期会造成重复等待。 -
UserBill和FinancialRecord都没有满足云仓 exactly-once 要求的唯一键,必须由独立入账桥接记录提供并发防重。 - 现有余额写法普遍是“读取当前值后保存”,专用入账事务必须先锁定目标账户行,避免与充值、消费、普通订单解冻并发时丢失更新。
G3-CODE-003、G3-FIN-002 和 G3-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、入账前后余额和失败信息。业务账本仍是计算来源,入账记录只负责把一笔已获资格的金额安全映射到最终账户。
冻结规则:
- 一个来源账本或结算单的同一版本、同一方向只能有一个成功入账记录。
- 重试只重用原
posting_key;成功记录再次执行直接返回首次结果。 - 目标账户、CRMEB 流水和入账记录必须在同一个数据库事务中完成。
- 原成功入账不修改、不删除;冲正创建反向入账记录并关联
reversal_of_posting_id。 - 现有
UserBill、FinancialRecord的非唯一来源字段只用于展示和追查,防重以eb_farm_financial_posting唯一键为准。
新增 FarmUserBalancePostingService,一次只处理一条用户业务账本:
- 按
posting_key=farm_user_ledger:{ledger_id}:credit:v{version}创建或锁定入账记录。 -
SELECT ... FOR UPDATE锁定用户账本,校验状态为可入账、金额大于零、无冻结、无未决退款或对账差异。 - 锁定
eb_user目标行,读取入账前余额并显式校验目标字段容量。 - 增加
now_money,再调用UserBillRepository::incBill写入category=now_money、type=farm_cloud_settlement、link_id=ledger_id的流水。 - 保存
bill_id、入账前后余额、完成时间,并把业务账本推进为已结算。 - 同一事务写入 Outbox;用户通知和短信在提交后消费,不作为入账成功条件。
UserBillRepository::TYPE_INFO 增加 now_money/farm_cloud_settlement=云仓结算入账。用户账本详情继续分别展示本金、收益和回购款,CRMEB 余额流水只展示本次合计,不能用一条余额流水反推业务公式。
商户货款采用“按日结单、直接进入可用余额”的方案:
- 每日
02:00按商户汇总前一自然日eligible_at已到期且未入单的供货账本;生成一张eb_farm_merchant_supply_statement和逐账本明细。 - 结单金额为所有明细
settle_amount之和;结单生成后明细不可替换,差异通过调整单和下一张结单处理。 -
FarmMerchantBalancePostingService以posting_key=farm_merchant_statement:{statement_id}:credit:v{version}锁定结单、入账记录和商户账户行。 - 通过现有
MerchantRepository::addMoney增加mer_money,保留核心字段写入许可;由于账户行已在同一事务中锁定,不会与普通订单自动解冻并发覆盖。 - 调用
FinancialRecordRepository::inc创建financial_type=farm_supply_settlement、financial_pm=1、type=0的商户财务流水,order_id使用结单主键,order_sn使用结单号。 - 保存
financial_record_id、入账前后余额和完成时间,同时推进结单及其明细为已入账。
不调用 addLockMoney,也不创建 mer_computed_money:供货账本自身的 eligible_at 已经包含验收、首次有效购买和观察期,再套用全局冻结天数没有业务依据。二次代销进度和是否滞销仍不影响商户已满足条件的供货货款。
V1 自动入账默认阈值:
| 对象 | 默认自动上限 | 超限处理 |
|---|---|---|
| 单条用户入账 |
10,000.00 元 |
转人工复核,不改变应结金额 |
| 单用户自然日累计 |
50,000.00 元 |
当日后续记录转人工复核 |
| 单张商户结单 |
100,000.00 元 |
转人工复核 |
阈值使用系统配置,只有超级管理员可修改并记录审计;规则快照不完整、金额不闭合、有冻结/退款/争议或来源版本变化时,无论金额大小都必须人工。
冲正规则:
- 正常经营亏损、滞销、平台优惠、仓储损耗和平台原因售后不得向用户或商户最终账户发起借方冲正。
- 只有已审核确认的重复/错误入账,或外部支付最终撤销且原权益不再成立,才先创建
eb_farm_cloud_ledger_adjustment,审核后生成新的借方入账记录。 - 自动冲正不得使
now_money或mer_money小于零;余额不足时进入recovery_pending,冻结后续同主体农业入账并由后续可得款先抵扣。 - 不自动部分扣款,不直接改原账本金额,不使用负数写入 unsigned 字段。
- 退款发生在正式入账前时,冻结或重算原业务账本,不先入账再冲回。
上线前执行只扩容、不改语义的字段升级:
-
eb_user.now_money、eb_user_bill.number、eb_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 模式处理溢出。
| 方案 | 结论 | 原因 |
|---|---|---|
| 独立入账桥接 + CRMEB 最终账户 | 采用 | 同时获得业务可审计、数据库幂等和现有余额/提现复用 |
用户直接调用后台 changeNowMoney
|
否决 | 类型和操作者语义错误,没有云仓来源防重 |
| 商户到资格日后再走全局冻结款 | 否决 | 重复等待,无法表达每条供货账本的独立资格时间 |
只写 UserBill/FinancialRecord
|
否决 | 两者都不会自动同步目标余额,也没有来源唯一约束 |
| 只改余额、不写现有流水 | 否决 | 现有余额明细和商户资金页面无法解释变动 |
| 在原成功记录上改金额 | 否决 | 破坏审计链,重试和对账无法判断首次事实 |
- 同一用户账本并发执行十次,只增加一次余额并产生一条成功入账记录和一条 CRMEB 余额流水。
- 用户充值、余额消费和云仓入账并发时不丢失任何一笔变化,流水末余额与账户余额一致。
- 写余额后创建账单失败时整笔事务回滚,重试仍可成功。
- 商户普通订单解冻与云仓结单入账并发时,两笔金额都保留。
- 同一商户结单重跑不重复增加余额或创建财务流水。
- 商户供货账本尚未验收、未过观察期或有首次退款时不进入结单。
- 二次代销滞销不阻止已合格商户货款进入下一日结单。
- 超过自动阈值时只转人工复核,不改变账本金额。
- 冲正余额不足时不产生负余额,进入待追偿并冻结后续农业入账。
- 扩容前后普通充值、余额消费、普通商户解冻、提现和财务报表回归通过。
- 补运费是否必须改写原云仓商品订单,还是可以使用独立支付单。
- 独立支付单如何复用微信、支付宝和余额支付。
- 支付渠道如何把成功回调准确分发给补运费业务。
- 重复回调、支付超时、迟到付款和退款如何闭环。
- 需要修改哪些 CRMEB 公共文件,哪些逻辑必须留在农业领域服务内。
| 证据 | 当前源码位置 | 事实 |
|---|---|---|
| 支付参数最小契约 | smartfarm/app/common/model/user/UserRecharge.php:58 |
独立充值单只需提供 order_sn、pay_price、attach、body 和可选 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。独立云仓运费单可以直接复用现有渠道,且比修改原商品订单金额更符合现有扩展方式。
G3-CODE-004 确认为:
- 新建
eb_farm_cloud_freight_order,不新增store_group_order、store_order或store_order_product记录。 - 原云仓首次订单只记录商品本金,补运费不回写其
pay_price、优惠分摊或商户供货货款。 - 支付回调标识固定为
attach=farm_cloud_freight,事件名固定为pay_success_farm_cloud_freight。 - 每张运费单对应一个云仓首次订单明细的一次地址/运费报价版本;同一明细任一时刻只能有一张当前待支付运费单。
- 地址、运费模板、计费参数和金额在创建运费单时保存快照,支付成功后不可修改。
- V1 支持用户端已启用的微信 H5/公众号/小程序/App、支付宝 H5/App和余额支付;不支持线下、扫码枪、服务商组合支付或商户子账户收款。
- 渠道收款主体为平台,后续实际物流费用属于平台履约成本,不进入用户云仓本金或代销收益。
CloudFreightOrder::getPayParams() 固定返回:
order_sn = freight_order_no
pay_price = freight_amount
attach = farm_cloud_freight
body = 云仓邮寄补运费
return_url = 支付宝 H5 可选
运费单分别保存业务、支付和退款状态:
| 维度 | 状态 |
|---|---|
| 业务状态 | 待支付、邮寄已确认、已取消、已超时、异常 |
| 支付状态 | 未支付、支付中、已支付、迟到支付、退款处理中、已退款 |
| 渠道关单状态 | 无需关单、待关单、已关单、关单失败、不支持 |
还必须保存 freight_order_no、订单明细、用户、选择版本、地址/运费快照、应付/实付金额、支付方式、支付驱动、渠道交易号、渠道回调时间、超时时间、当前版本和异常原因。
发起支付时不接受前端提交金额、订单号、attach 或用户 ID。服务端按已锁定运费单生成支付参数;切换支付渠道时取消旧的未支付运费单并生成新单号,避免一个商户订单号跨渠道留下歧义。
在线渠道流程:
渠道验签
→ pay_success_farm_cloud_freight
→ CloudFreightPaySuccessListen
→ CloudFreightPaymentService::paySuccess(orderNo, callback)
→ 锁运费单和云仓订单明细
→ 校验用户、金额、渠道交易号和当前选择版本
→ 幂等记录支付事实
→ 固化邮寄去向与地址/运费快照
→ 创建邮寄履约单和 Outbox
→ 同一事务提交
金额统一规范化为元后使用精确小数比较:
- 微信 V3 读取
amount.total分,微信 V2 读取total_fee分。 - 支付宝回调适配需把验签后的
total_amount、trade_no一并传入业务事件;不能只凭订单号确认支付。 - 回调金额、币种或订单状态不一致时不确认邮寄,运费单进入异常并告警。
- 重复回调若订单号、金额和渠道交易号一致则直接幂等成功;同订单号出现不同交易号或金额时转人工复核。
余额支付由 CloudFreightPaymentService::payBalance() 完成:按固定顺序锁运费单、订单明细和用户账户,在同一事务内扣减 eb_user.now_money、写 UserBill(type=farm_cloud_freight_pay)、确认邮寄并创建履约。不能照搬现有未显式加锁的余额支付代码。
CloudFreightExpireJob 每分钟扫描到期单,并在事务内锁定运费单和订单明细:
- 已支付时不处理。
- 未支付且仍在选择期时,运费单转已超时,清除临时邮寄选择,订单明细回到待选择。
- 未支付且已过选择截止时,运费单转已超时,并通过同一去向服务自动转为代销。
- 事务提交后通过 Outbox 尝试渠道关单;关单失败可重试,但不回滚本地超时结果。
由于当前 CRMEB 没有统一关单接口,系统必须始终接受“超时任务与成功回调并发”的可能:
- 锁内只有一个流程能先完成状态迁移。
- 如果渠道支付已成功,但本地已经取消、超时或自动代销,则记录为迟到支付,禁止把去向改回邮寄。
- 迟到支付自动创建全额退款单;退款成功前在用户订单中心显示“退款处理中”。
-
pay_result只查询本地运费单状态,不以客户端支付 SDK 的成功回调作为最终结果。
新建 eb_farm_cloud_freight_refund,V1 只允许整张运费单全额退款,不做部分运费退款。
允许退款的场景:
- 支付成功回调迟于运费单超时/取消或订单明细已自动代销。
- 首次云仓订单在发货前完成全量退款。
- 平台在发货前取消邮寄履约。
- 审计确认的重复扣款或错误支付。
已发货后不因用户改选退运费;平台责任取消、物流未揽收或其他异常由客服按异常单审核后全额退款。
在线退款使用原支付驱动和原商户订单号,参数至少包含 pay_price、refund_price、唯一 refund_id 和原因。余额退款在事务内增加用户余额并写 UserBill(type=farm_cloud_freight_refund)。所有退款重试复用同一 refund_no,状态使用待提交、处理中、成功、失败待重试和人工复核。
| 类型 | 文件 | 作用 |
|---|---|---|
| 新建 | 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_amount 和 trade_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(),不把运费单加入普通商城财务、佣金、优惠或商户分账。
| 方案 | 结论 | 原因 |
|---|---|---|
| 独立运费单 + 现有支付驱动 + 独立回调事件 | 采用 | 复用渠道能力,同时保持商品本金、商户货款和运费审计边界 |
| 修改原云仓商品订单金额后二次支付 | 否决 | 已支付订单金额、退款分摊和商户货款都会失真 |
| 生成一张普通商城虚拟商品订单 | 否决 | 会误入商品销量、佣金、优惠、商户财务和发货状态 |
| 只信前端支付成功结果 | 否决 | 客户端结果可中断或伪造,不能形成最终支付事实 |
| 超时后忽略迟到回调 | 否决 | 平台已收款但用户未获得邮寄权益,会造成资金差异 |
| V1 支持部分运费退款 | 否决 | 当前一条明细只有一个整体去向,全额退款更可审计 |
- 微信、支付宝和余额支付各完成一次,运费不进入商品本金或商户货款。
- 同一成功回调重放十次,只确认一次邮寄、创建一张履约单。
- 回调金额或交易号冲突时不确认邮寄并产生告警。
- 支付成功事务中创建履约失败时,本地支付确认和去向迁移全部回滚,回调重试可恢复。
- 超时任务与成功回调并发时,只产生邮寄或超时结果之一。
- 仍在选择期超时后回到待选择,超过截止时间超时后只自动代销一次。
- 已自动代销后收到迟到成功回调,不改去向并自动发起一次全额退款。
- 未支付运费单切换支付渠道时旧单失效,新单使用新订单号。
- 首次商品退款与运费退款并发时不会漏退、重复退或发货。
- 余额支付和普通余额消费并发时余额正确,账单末余额与账户一致。
- 在线退款任务重复执行时始终复用同一退款单号。
- 普通商城订单、充值、普通退款和原支付回调完整回归。
- 现场人员是否需要新建独立账号系统。
- 现有客服 Token 能否同时承载 PC 客服和移动现场作业。
- 客服端是否已有可复用的菜单权限和数据范围能力。
- 三栏客服工作台能否直接改成移动响应式页面。
- 同一前端工程如何形成互不干扰的 PC/移动双入口。
| 证据 | 当前事实 | 设计含义 |
|---|---|---|
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_id、account、pwd、is_open、status、is_del;account 当前只有普通索引 |
可作为统一人员身份,但上线迁移前必须清理重复账号并增加唯一约束 |
| 平台/商户客服维护控制器 | 商户端创建时校验全局账号重复;平台端当前没有完整维护 account、pwd、is_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 不作为实现依据 |
以下属于复用身份前必须处理的基线加固,不是另建认证系统:
-
ServiceTokenMiddleware每次请求同时校验is_open=1、status=1、is_del=0;商户账号继续校验商户存在且启用。 - 移除
userType宏中无意义的未定义变量捕获,并为中间件增加认证回归测试。 - 迁移前检查所有非空
store_service.account是否重复;通过后增加唯一索引,空值继续允许多个。 - 平台端客服维护表单补齐账号、密码、PC/移动登录开关;密码仍写入现有
pwd,不复制凭据。 - 现有
getOrderInfo等宽松接口单列为基线修复任务;农业 Controller 不通过“先按 ID 查出,再在页面隐藏”实现权限。
V1 采用“一个身份、两个工作区、两层授权”:
eb_store_service
└─ ServiceTokenMiddleware:确认是谁、账号是否仍有效
└─ FarmServicePermissionMiddleware:确认能做什么
└─ FarmServiceScopeRepository:确认能操作哪个对象
- PC 客服和移动现场端复用
store_service、serviceJWT 和SerToken。 - 不把职责和长期范围写入 JWT;调岗、停用或撤销范围后,下一次请求立即生效。
- 职责到权限码的映射使用版本化代码配置,避免 V1 再造一套动态菜单/角色系统。
- 具体农场、区域、栏舍、仓库、生产批次和任务授权使用数据库关系。
- 路由权限只解决“动作权限”,Repository 查询范围和对象断言解决“数据权限”,两者必须同时通过。
- 默认拒绝:没有农业档案、没有职责、范围过期或对象无法沿关系链落入范围时均返回 403。
- 平台客服可被授予全部或指定商户范围;商户客服的最大边界永远是自身
mer_id,授权记录不能扩大该租户边界。 - 结算、回购、规则修改、审核发布和最终异常方案仍只在平台管理端执行。
| 职责码 | 主要权限码 | 默认对象范围 |
|---|---|---|
platform_customer_service |
farm.order.read、farm.cloud.order.read、farm.exception.read、farm.exception.propose
|
全部或指定商户 |
merchant_customer_service |
farm.order.read、farm.cloud.order.read、farm.exception.read、farm.exception.propose
|
当前商户 |
crop_operator |
farm.task.read、farm.scan.resolve、farm.production.event.write、farm.output.write、farm.exception.report
|
农场、区域、地块、生产批次或任务 |
livestock_operator |
farm.task.read、farm.scan.resolve、farm.production.event.write、farm.asset.health.write、farm.output.write、farm.exception.report
|
农场、区域、栏舍、资产、生产批次或任务 |
warehouse_operator |
farm.task.read、farm.scan.resolve、farm.warehouse.receive、farm.fulfillment.pack、farm.fulfillment.ship、farm.pickup.verify、farm.exception.report
|
仓库、产出批次、履约单或任务 |
farm_reviewer |
farm.production.review、farm.output.review、farm.exception.read
|
指定农场或业务域 |
同一人员可有多个职责。write 权限允许保存草稿和提交本人记录,不自动包含审核权限;propose 只允许提交建议,不允许直接执行财务或最终处置。
新增两类农业授权数据:
-
eb_farm_service_profile:一对一绑定service_id,记录农业工作区启用状态、默认工作区、移动端开关、授权版本和最近变更时间。 -
eb_farm_service_scope:记录service_id + duty_code + scope_type + scope_id、有效期、状态和授权人。
scope_type 固定为 all、merchant、farm、area、plot、barn、asset、warehouse、production_batch、output_batch、fulfillment、task。范围解析只允许从当前业务对象沿已定义关系链向上匹配,禁止客户端提交一个属于授权农场但与当前订单无关的范围 ID 来绕过校验。
保留原登录响应,新增:
GET /ser/farm/context
返回当前人员可进入的 workspaces、职责码、权限码、范围摘要、scope_version、默认入口和功能开关。前端只用它决定显示和跳转;后端每个请求仍重新校验。旧客服账号没有农业档案时继续进入 /kefu/dashboard,不会因农业功能上线改变原工作流。
登录后入口规则:
- 只有传统客服能力:进入
/kefu/dashboard。 - 只有现场能力:进入
/kefu/farm/work/tasks。 - 同时拥有多个工作区:进入
/kefu/entry选择,记住的默认入口仅用于体验,不作为授权依据。 - 访问无权限路由:显示无权限页,不退回登录页;Token 失效才重新登录。
| 路由族 | 布局 | 页面 |
|---|---|---|
/kefu/dashboard |
原三栏客服页 | 原客服会话,不改布局 |
/kefu/farm/desk/* |
新农业桌面布局 | 农业订单、云仓订单、异常协同、审核 |
/kefu/farm/work/* |
新移动优先布局 | 我的任务、扫码、过程记录、产出、仓储履约、异常上报 |
两个新布局位于同一 smartfarm_service 构建中,复用请求、Token、上传和格式化能力;不把现场人员放入消费者 smartfarm_user,也不新建第六个前端项目。
移动端不新增整套 UI 框架。V1 使用现有主题色、SVG/Iconfont、请求和上传能力,新增少量领域组件;日期、时间、数量等优先使用移动浏览器原生控件。扫码采用 @zxing/browser 的摄像头/图片解码能力,并始终提供手工输入编码的降级入口;摄像头权限被拒绝、浏览器不支持或非 HTTPS 时仍可继续作业。
后端:
- 修改
ServiceTokenMiddleware.php、平台客服维护 Controller/Validate/Repository 和route/service.php。 - 新增
FarmServicePermissionMiddleware、FarmServicePolicy、FarmServiceProfile与FarmServiceScope的 Model/DAO/Repository。 - 新增
service/farm/Common、Order、Cloud、Task、Production、Warehouse、ExceptionEventController。 - 所有领域 Repository 增加
scopeQuery()或assertScopedObject(),Controller 不自行拼接范围 SQL。
前端:
- 保留
src/views/kefu/pc/index.vue。 - 新增
src/router/farm.js、src/store/modules/farmContext.js、src/api/farm.js。 - 新增
src/layout/farmDesktop/index.vue、src/layout/farmMobile/index.vue。 - 新增
src/views/farm/desk/*与src/views/farm/work/*,并复用request.js、auth.js、上传、SVG 和格式化能力。
| 方案 | 结论 | 原因 |
|---|---|---|
| 复用客服身份 + 独立农业授权 | 采用 | 减少凭据与登录系统重复,同时补齐最小权限 |
| 新建现场人员账号系统 | 不采用 | 增加登录、密码、停用、审计和运维成本 |
| 把现场页放入用户 uni-app | 不采用 | 消费者身份与工作人员权限边界混淆 |
| 把三栏客服页做全响应式 | 不采用 | 固定宽度和交互密度不适合手机现场操作 |
| 新建独立现场前端项目 | V1 不采用 | 当前页面量可由同项目独立布局承载 |
| 复用平台/商户动态菜单权限 | 不采用 | 客服工程没有该运行链路,接入成本大于稳定权限码配置 |
- 原客服账号无农业档案时登录、聊天和退出行为不变。
-
status、is_open或is_del变化后,已有 Token 下一请求立即失效。 - 同账号在 PC 和手机并行使用时 Token 续期和退出策略符合预期。
- 无职责、无权限码、范围过期和对象越界分别返回稳定 403 错误。
- 商户客服不能通过指定其他商户订单 ID 越权读取。
- 现场人员可查看授权农场任务,但不能操作同农场内未分配且没有上级范围覆盖的限制对象。
- 撤销范围后,已打开页面的下一次提交失败并刷新上下文。
- PC 客服工作台在
1280x800保持原布局;农业桌面页在1280x800无横向溢出。 - 移动作业页在
360x800、390x844、430x932下无文本或按钮重叠。 - 摄像头成功、权限拒绝、非 HTTPS、无摄像头和图片识别失败均有可继续的手工输入路径。
- 网络失败后表单草稿保留;重复提交使用同一
request_id不生成重复记录。 - 原客服订单查询宽松路径修复后,正常会话内订单仍可查看,跨商户 ID 被拒绝。
- 平台管理端、用户 H5/微信小程序和服务/现场端是否能统一使用同一地图供应商。
- 农场、区域、地块和自提点需要保存哪些坐标,采用哪一种坐标系。
- 浏览器地图 Key、服务端 WebService Key 和签名密钥如何隔离。
- 地图不可用、用户拒绝定位或供应商限额时,核心业务是否还能继续。
- 当前 CRMEB 地图实现可以复用到什么程度,哪些现状缺陷必须单列整改。
| 证据 | 当前事实 | 设计含义 |
|---|---|---|
| 后端公开接口 |
route/api/notLogin.php 已有 GET /api/lbs/geocoder,app/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.getLocation、uni.chooseLocation、uni.openLocation,manifest.json 声明 Maps、Geolocation 和微信位置权限 |
农场位置展示和外部导航无需新增原生地图插件 |
| 用户端版本 | 当前 @dcloudio/uni-app 为 2.0.2-4080720251210002
|
V1 不以升级 uni-app 作为农场地图上线前提,现有定位链路另行回归 |
| 服务端现状 |
smartfarm_service 没有地图组件或定位依赖 |
先提供地址、位置页和外部导航;内嵌地图只能作为增强能力 |
| 数据库配置 |
eb_system_config 已定义 tx_map_key,当前 eb_system_config_value 没有平台值 |
设计可复用配置名,但环境必须在联调前完成实际配置检查 |
| 现有坐标字段 |
eb_delivery_station 使用 lng/lat,eb_merchant 使用 long/lat,eb_store_group 和 eb_user_address 使用 longitude/latitude,类型均为字符型 |
新农业域必须建立唯一命名和精度规范,在适配边界转换旧字段 |
官方兼容性依据:
-
uni.getLocation 说明
<map>相关场景应明确使用gcj02,不同端的定位配置和版本兼容性需要分别验证。 - uni-app map 组件 支持 App、H5 和微信小程序,但 Key、域名白名单、组件层级和供应商额度属于运行环境配置。
- 腾讯位置服务坐标说明 表明腾讯地图国内服务使用 GCJ02。
G3-CODE-008 和 G3-EXT-001 确认为:
- V1 默认地图供应商为腾讯地图。平台管理端、用户端和服务端展示使用同一供应商,避免跨供应商坐标偏移。
- 代码保留
MapProviderInterface,V1 不提供运营侧多供应商切换页面,也不同时实现第二家供应商。 - 新农业业务事实坐标统一为
GCJ02。 - 新表和新 API 只使用
longitude、latitude、coordinate_system,不再新增long、lng或含义不明确的lat/long契约。 - 旧 CRMEB 字段只在 Repository 或前端适配层转换,不通过新接口继续扩散。
- V1 只保存点位和示意材料,不建设专业多边形编辑、路线规划、电子围栏、卫星图或人员实时轨迹。
统一点位字段:
| 字段 | 类型 | 规则 |
|---|---|---|
longitude |
decimal(10,7) |
可空;非空时范围 -180 至 180
|
latitude |
decimal(10,7) |
可空;非空时范围 -90 至 90
|
coordinate_system |
varchar(8) |
点位存在时固定 GCJ02
|
约束:
- 经度和纬度必须同时为空或同时非空,禁止保存半个点位。
- 不使用
0,0表示“未配置”。 - 接口以十进制字符串返回坐标,避免 JavaScript 浮点序列化擅自改变精度。
- 农场和统一自提点必须有真实点位后才能启用或公开。
- 区域、地块和栏舍默认继承农场公开点位,使用示意图表达内部位置;确有必要时可保存内部中心点,但不要求每个对象都有坐标。
- 用户公开接口返回农场公开点位和公开地址,不返回敏感地块、栏舍、仓库内部精确点位。
- V1 不建立空间索引,不以 GIS 多边形计算面积、相交或容量。
新增 eb_farm_pickup_point 统一承载云仓和农业产出的线下自提点。V1 由平台维护,保存名称、业务类型、地址、营业时间、联系人、点位、状态和版本;云仓活动只引用已启用且点位完整的自提点。
| 配置 | 用途 | 可见范围 |
|---|---|---|
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。 - 腾讯地图生产额度、商业授权和采购判断属于发布前外部配置门,不阻塞当前设计,也不默认承诺付费购买。
推荐目录:
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 基线整改,必须单独回归现有首页定位、地址选择和附近门店,不与新农业页面首次提交绑在同一个大改任务中。
- 保留
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 缺失或浏览器不支持时,不阻断任务查看、扫码、记录或提交。
| 场景 | V1 处理 |
|---|---|
| 服务端地图配置缺失 | 后台辅助接口返回 MAP_NOT_CONFIGURED;草稿可保存地址,启用/发布被阻止 |
| 坐标非法或坐标系不支持 | 写接口拒绝,禁止自动保存 0,0
|
| 腾讯地图超时或 5xx | 连接超时 2 秒、总超时 5 秒,仅超时/5xx 重试 1 次 |
| 供应商配额耗尽 | 返回 MAP_QUOTA_EXCEEDED,告警并允许人工填写地址/坐标 |
| 用户端地图脚本失败 | 展示地址、示意图、复制和重试入口 |
| 用户拒绝定位 | 不影响农场查看和导航;只关闭距离类增强 |
| 服务端现场地图失败 | 任务与事实录入继续,只隐藏地图增强 |
地理编码和逆地址结果可在 Redis 按规范化查询与坐标短期缓存。不得把供应商失败静默转换为空地址或默认坐标。
| 方案 | 结论 | 原因 |
|---|---|---|
| 腾讯地图 + 供应商接口抽象 + GCJ02 | 采用 | 与现有管理端、uni-app 和配置最一致,跨端偏移最小 |
| 客户端直接调用全部 WebService | 否决 | 暴露服务端额度和签名边界,难以统一限流与错误处理 |
新农业接口继续混用 lng/long/latitude
|
否决 | 现有字段已经不一致,会继续制造方向错误 |
| V1 强制升级 uni-app 后再做地图 | 否决 | 农场展示可不依赖当前定位,升级风险不应阻塞主流程 |
| V1 建专业 GIS 多边形编辑器 | 否决 | 租地容量依赖业务占用记录,不需要用地图几何替代 |
| V1 同时接腾讯和另一家地图 | 否决 | 增加坐标转换、测试、额度和前端体积,当前没有业务收益 |
| 用户端默认自动获取位置 | 否决 | 增加权限阻断和兼容风险,农场导航只需要目标点位 |
- 农场和自提点经纬度成对保存,边界值和错误方向均被正确校验。
- 新 API 始终返回
longitude/latitude/coordinate_system=GCJ02,不返回旧字段别名。 - 平台地址搜索、地图点选和手工坐标三种入口得到同一规范化结果。
- 服务端 Key/SK 不出现在响应、日志、前端构建和导出中。
- 地图未配置、超时、5xx、配额耗尽和非法响应均映射稳定错误码。
- 地图不可用时草稿可保存,但农场/自提点无法启用;已有交易与现场任务仍可继续。
- 用户 H5、微信小程序和 App 使用保存的 GCJ02 点位打开导航,拒绝当前定位权限仍可查看地址。
- 用户端地图加载失败后地址、示意图、复制和重试入口可用。
- 服务端移动页地图失败不影响扫码、记录、上传和提交。
- 公开农场接口不泄露内部地块、栏舍或仓库精确点位。
- 旧
/api/lbs/geocoder、首页定位、地址选择和附近门店在适配器整改后响应兼容。 - 商户资料经纬度缺陷修复后,旧数据读取和保存方向正确,提交不再出现
log字段。
SPIKE-001 至 SPIKE-007 已全部完成。四端 108 个页面低保真和 15 条跨端 Flow 随后完成并通过结构审计,当前阶段是用页面和 Flow 反向冻结数据、API、事件任务、代码文件与测试契约。后续发现新的事实疑点仍可新增专项验证,但不能用“开发时再看”代替明确记录。
为关闭 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,继续使用现有通道;通道失败不得回滚订单、结算或现场事实 |
最终边界:
- V1 不自建支付、快递轨迹、电子面单、短信或微信消息供应商实现。
- 领域 Service 只调用 SmartFarm 适配层,不直接散落调用第三方 SDK。
- 第三方超时统一映射稳定错误、进入重试和告警;消息发送属于提交后副作用。
- 未配置短信、模板消息或电子面单时,站内状态、人工快递单号和业务流程仍可使用。
- 生产账号、额度、模板审核和可能的商业开通费用属于上线前环境门;发生付费采购时仍须由项目方确认。
对当前 www_smartfarm_co 数据库执行只读 information_schema.REFERENTIAL_CONSTRAINTS / KEY_COLUMN_USAGE 核验,物理外键约束数量为 0。现有 CRMEB 表间关系依赖应用层校验、索引、事务与约定删除顺序,而不是数据库级 FOREIGN KEY。
V1 因此冻结为:
- 90 张农业扩展表不新增物理外键,避免同一业务库混用两套删除、升级和恢复语义。
- 每个
RID/CID引用字段必须在34中给出必要普通/组合索引,并在 Repository 或领域 Service 写事务内验证对象存在、状态、归属和版本。 - 订单、账本、库存、生产和溯源事实只作废或冲正,不因主数据停用而物理删除。
-
verify_before/verify_after与日常对账增加必需引用的孤儿扫描;发现孤儿时阻断发布或进入修复队列,不静默补默认对象。 - 如果未来要改用物理外键,必须作为全库级架构迁移单独评审,不能只给农业表局部添加。
- 首页
- 项目总览
- 项目立项
- 端与角色
- 供应链经营闭环
- 模块地图
- V1 范围 PRD
- 任务拆解
- 结算与账本
- V1 结算口径
- 云仓秒杀
- CRMEB 底座能力映射
- 当前菜单页面审计
- V1 决策与术语
- 云仓流程与状态机
- 租地认养与溯源 PRD
- V1 数据模型草案
- 开发前设计计划
- V1 信息架构
- 页面与原型规格
- V1 API 契约草案
- 事件任务权限通知
- 五项目代码改造蓝图
- 测试验收发布与手册计划
- CRMEB 视觉基线审计
- G3/G4 决策登记表
- 用户端 uni-app 复用审计
- 开发前主清单
- 自主设计工作指引
- V1 需求追踪矩阵
- G3 技术专项验证报告
- V1 P0 页面矩阵
- 写操作契约注册表
- 事件任务操作注册表
- 状态异常事务矩阵
- 数据库字段字典
- 页面 API 文件追踪
- V1 ER 图
- API 字段契约注册表
- 平台管理员手册
- 商户手册
- 用户帮助
- 服务与现场手册
- 开发任务包
- 测试目录与 Fixtures
- 发布与运维手册
- 设计交付与研发交接