1. 业务目标与边界
活动域负责“运营配置营销规则,服务站按规则购买,最终转成可支付和履约的业务单据”。代码中包含普通销售活动、赠品活动、客户活动、优惠券、预售和秒杀等能力。本篇以当前代码最完整的秒杀链路为主,说明:
- OPS 创建、编辑、导入活动商品、配置服务站范围、提交 OA、审批和提前结束。
- 服务站查看商品、加购物车、确认订单、提交订单和主动取消。
- 秒杀订单怎样转换为采购单,支付回调怎样确认占用量。
- 待支付超时、采购关闭、回调丢失时怎样释放或补偿。
本篇不展开普通销售活动、赠品和优惠券的全部字段;这些能力与秒杀共处活动域,但拥有各自的 Service、模型和状态,不能直接套用秒杀状态。
2. 参与角色与前置条件
| 角色/系统 | 职责 | 关键前置条件 |
|---|---|---|
| OPS 运营 | 创建活动、维护商品、名单、提交审批、提前结束 | 有活动管理权限;商品和仓库资料有效 |
| OA | 审批秒杀活动 | 模板编码为 callback_20260526;回调能进入 OA 消费者 |
| 服务站用户 | 浏览、加购、确认、提交、支付或取消 | 登录上下文包含服务站 sid;活动对该站可见 |
| DGJ2 秒杀域 | 校验活动、价格、数量、库存,保存秒杀单 | 活动审批通过、启用且在有效时间内 |
| 采购域 | 接收秒杀单并创建待支付采购单 | 秒杀订单和明细已在同一事务中生成 |
| 支付中心 | 返回采购单支付结果 | 回调采购单可通过来源字段关联秒杀单 |
| 定时任务 | 刷状态、清购物车、释放超时、补偿异常 | CLI 环境执行;任务日志可用 |
3. 代码入口地图
| 层级 | 文件 | 主要职责 |
|---|---|---|
| OPS Controller | application/controllers/inner/activity/FlashActivity.php | 活动列表、保存、导入导出、名单、提交、结束 |
| PC 服务站 Controller | application/controllers/scm/FlashSale.php | PC 秒杀商品、购物车、订单接口 |
| 移动商城 Controller | application/controllers/inner/moveMall/FlashSale.php | 移动端同口径秒杀接口 |
| OPS Service | application/Services/Activity/FlashActivitySer.php | 活动校验、保存、OA、状态刷新 |
| 购买 Service | application/Services/Activity/FlashSaleSer.php | 可见性、库存、购物车、订单、采购、释放和补偿 |
| 支付回调 | application/controllers/tasks/PayCenterNotify.php | 采购支付落账并通知秒杀订单 |
| OA 回调 | application/controllers/tasks/OaNotify.php | 消费 OA 审批事件并更新活动状态 |
| 补偿任务 | application/controllers/tasks/FlashSaleTask.php | 状态刷新、超时释放、支付/关闭补偿 |
| 枚举 | application/KzData/Enums/FlashActivityEnums.php | 活动、库存范围、商品、购物车、订单状态 |
| 采购枚举 | application/KzData/Enums/PoOrderEnums.php | 秒杀采购单类型和来源类型 |
已确认:接口方法和参数来自当前 Controller、Request Entry 和 Service。真实 HTTP 前缀会受网关、环境和 CodeIgniter 路由影响,联调时应从浏览器 Network 或环境路由确认,不能把类路径直接当成固定公网 URL。
4. OPS 接口与请求
4.1 OPS 接口总表
| 接口方法 | 核心请求 | 核心 Service | 关键返回 | 数据副作用 |
|---|---|---|---|---|
listFlashActivity | 页码、活动编号/状态/时间/申请人筛选 | listFlashActivity | 活动分页列表 | 只读 |
detailFlashActivity | id/activity_id/activityId | detailFlashActivity | 活动、商品、状态快照 | 只读 |
listFlashActivityGoods | 活动 ID、分页、商品/仓库筛选 | listFlashActivityGoods | 活动商品分页 | 只读 |
saveFlashActivity | 活动对象、时间、goodsList | saveFlashActivity | 完整活动详情 | 新增/更新活动和商品 |
invalidateFlashActivityGoods | 活动 ID、activity_goods_id、reason | invalidateFlashActivityGoods | 更新后结果 | 商品规则置失效 |
importFlashActivityGoods | file_path 上传文件、活动 ID | importGoods | 解析后的商品行 | 不直接保存活动;前端需随保存接口提交 |
downloadFlashActivityGoodsTemplate | 无业务参数 | buildGoodsImportTemplate | Excel 文件流 | 无 |
exportFlashActivityGoods | 活动 ID | buildGoodsExport | Excel 文件流 | 无 |
exportFlashActivity | 列表筛选条件 | buildActivityExport | Excel 文件流 | 无 |
copyFlashActivity | 活动 ID | copyFlashActivity | 新草稿详情 | 新增活动和商品快照 |
saveFlashActivityStationScope | 活动 ID、type、stations | saveStationScope | 名单分页 | 新增/恢复名单记录 |
getFlashActivityStationScope | 活动 ID、type、分页 | getStationScope | 黑/白名单分页 | 只读 |
deleteFlashActivityStationScope | 活动 ID、type、ids/sids | deleteStationScope | 删除后的分页 | 软删名单记录 |
submitFlashActivity | 活动 ID、is_oa、可选 oa_no | submitFlashActivity | OA 单号或冲突清单 | 更新审批状态、调用 OA |
endFlashActivity | 活动 ID、reason | endFlashActivity | 更新后详情 | 活动提前结束、商品同步结束 |
convertFlashActivityToDraft | 活动 ID | convertFlashActivityToDraft | 草稿详情 | 驳回/可转换活动重置草稿 |
4.2 保存活动请求示例
下面是 Controller 可以识别的结构示例,金额按当前秒杀活动代码以分存储,展示时再格式化为元;fixed_price 的前端具体传值需继续与当前 OPS 页面 Network 核对。
{
"activity": {
"id": 0,
"name": "暑期轮胎秒杀",
"start_time": "2026-07-20 09:00:00",
"end_time": "2026-07-20 18:00:00",
"activity_type": 2,
"stock_scope": 2,
"remark": "示例活动"
},
"goodsList": [
{
"goods_id": 10001,
"package_group_no": "PKG-001",
"wms_location_no": "WH001",
"wms_location_name": "示例仓",
"qty": 4,
"price_mode": 1,
"fixed_price": 19900,
"discount_rate": 0,
"date_calc_mode": 2,
"backward_from": 90,
"backward_to": 30,
"sort": 10
}
]
}
关键字段:
| 字段 | 含义和规则 |
|---|---|
id/activity_id/activityId | 活动 ID 兼容别名;新增为 0,编辑传已有 ID |
activityTime | 兼容 [开始时间, 结束时间],服务端归一到 start_time/end_time |
activity_type | 1 单品售卖,2 套包售卖 |
stock_scope | 1 服务站对应仓,2 指定仓,3 全局预留 |
goodsList/goods_list | 商品规则完整快照;导入结果也要随保存接口提交 |
goods_id | 商品/物料档案 ID,后端补充 SKU、名称、品牌、分类和单位快照 |
qty | 单品可为 0 表示不以活动量限制;套包必须大于 0 |
price_mode | 1 一口价,2 固定折扣 |
date_calc_mode | 0 无,1 按月倒推,2 按天倒推 |
wmsAttributeCode | 用于判断生产日期字段是否必填,不落活动商品表 |
4.3 服务站范围请求示例
{
"activity_id": 100,
"type": 2,
"stations": [
{"sid": 20001, "station_name": "示例服务站"},
{"sid": 20002, "station_name": "另一服务站"}
],
"page": 1,
"pageSize": 20
}
type=1是黑名单,type=2是白名单。- 保存兼容
stations、stationList、station_list、sids、单个sid。 - 删除兼容名单记录
ids或服务站sids,并限制在活动和名单类型内软删。
4.4 提交 OA 请求和分支
{
"activity_id": 100,
"is_oa": 1
}
is_oa=1:构造 OA payload,外部提交成功后把活动更新为审批中并保存oa_no。is_oa=0:必须传oa_no,按人工确认路径直接置审批通过。- 提交前会检查商品明细、活动配置和跨活动冲突。
- 有冲突时不是成功提交,返回
can_submit=0、conflict_count、conflictList和可展示消息。
5. OPS 创建到 OA 生效流程
flowchart TD
A[OPS 新建或编辑活动] --> B[saveFlashActivity]
B --> C{活动/时间/商品/价格校验}
C -- 失败 --> X[返回明确校验错误 不落有效配置]
C -- 通过 --> D[保存活动主表和商品快照]
D --> E[配置黑名单或白名单]
E --> F[submitFlashActivity]
F --> G{跨活动商品冲突}
G -- 有冲突 --> H[返回 conflictList 调整后重提]
G -- 无冲突 --> I{是否走 OA}
I -- 否 --> J[校验 oa_no 并直接置通过]
I -- 是 --> K[FlashActivityOaProvider 提交 OA]
K --> L[活动置审批中 保存 oa_no]
L --> M[OaNotify 消费 oa_audiat]
M --> N{审批结果}
N -- 通过 --> O[审批通过 按活动时间决定启用状态]
N -- 驳回 --> P[审批驳回 记录 reject_reason]
O --> Q[同步活动商品状态 服务站可按规则展示]
5.1 OA 系统时序
sequenceDiagram
participant OPS as OPS 前端
participant C as FlashActivity Controller
participant S as FlashActivitySer
participant DB as MySQL 活动表
participant OA as OA Provider
participant MQ as OA MQ
participant T as OaNotify
OPS->>C: submitFlashActivity(activity_id,is_oa)
C->>S: submitFlashActivity(requestData,apiUserHeader)
S->>DB: 读取活动和商品 校验冲突
alt 存在冲突
S-->>OPS: can_submit=0 + conflictList
else 提交 OA
S->>OA: submit(template payload)
OA-->>S: oa_no/requestId
S->>DB: apply_status=1, oa_status=1
S-->>OPS: activity_id + oa_no
OA-->>MQ: oa_audiat(templateCode,status,summaryId)
MQ->>T: consume
T->>S: callback20260526(messageData)
S->>DB: 审批通过或驳回并同步商品状态
end
6. 服务站接口与请求
PC Controller 和移动商城 Controller 最终都调用 FlashSaleSer,业务口径应保持一致;PC 额外提供 checkAccess。
| 接口方法 | 核心请求 | 关键返回 | 主要读写 |
|---|---|---|---|
checkAccess | 登录服务站上下文 | has_access、message | 读活动和白名单 |
goodsList | page、pageSize、商品筛选 | list、total | 读活动商品、名单、库存/物料 |
goodsDetail | 活动商品 ID 或活动 ID | 活动、商品/套包、库存和价格 | 只读 |
packageGoodsList | 套包活动/商品 ID、分页 | 套包及明细 | 只读 |
goodsFilters | 登录服务站上下文 | 品牌、分类、活动类型等 | 只读 |
getCartCount | 活动商品 ID | 本站/供给仓/在途库存、包装、可购量 | 读购物车和库存 |
addCart | activity_goods_id、qty、packSpecType | 更新后的购物车 | 新增或更新购物车 |
cartList | 可选筛选/排序 | 购物车、失效原因、金额门槛 | 读并可能重置历史勾选/刷新状态 |
updateCart | items 或活动商品 ID 集合 | 更新后的购物车 | 事务内更新/删除购物车 |
orderPreview | source_type、activity_goods 或活动 ID | 仓库、商品、金额、额度 | 只读实时校验 |
submitOrder | 与确认页相同,另含收货/采购上下文 | 秒杀订单详情、采购单 ID | 事务新增秒杀单、锁量、创建采购单 |
orderDetail | id 或 bill_no | 秒杀单、明细、关联采购单 | 只读 |
cancelOrder | id/bill_no、可选 reason | 取消后订单详情 | 关闭采购单、释放锁量、更新状态 |
登录上下文由 Controller 注入或透传:JXCSID/sid 表示服务站,JXCUID 表示用户,JXCUNAME 表示用户名。业务请求不应允许前端用任意 sid 越权读取其他服务站数据。
6.1 加购请求
{
"activity_goods_id": 30001,
"qty": 4,
"packSpecType": 1
}
qty最小为 1。packSpecType兼容pack_spec_type,服务端会换算成基础数量。- 单品允许加购;套包不进购物车,必须立即购买。
- 同服务站、同活动商品执行 upsert,不重复生成多行购物车。
6.2 批量更新购物车
{
"items": [
{"activity_goods_id": 30001, "qty": 8, "selected": 1},
{"activity_goods_id": 30002, "deleted": 1}
]
}
服务端在事务中逐行处理:数量小于等于 0 或 deleted=1 进入删除;改数量时重新校验商品可见性、采购限制和实时库存;只改 selected 时不重算数量。任一行抛错会回滚本次批量修改。
6.3 购物车确认和提交
{
"source_type": 2,
"activity_goods": [
{"activity_goods_id": 30001, "qty": 8},
{"activity_goods_id": 30002, "qty": 4}
]
}
source_type=1为立即购买,2为购物车。- 购物车未传
activity_goods时,默认取当前勾选且有效的行。 activity_goods可传 JSON 数组,也兼容 JSON 字符串;每行必须有活动商品 ID 和正数量。- 单品必须先进入购物车;套包立即购买可传
activity_id和购买套数qty。 - 确认页只读计算,提交时会再次加行锁并重新校验,不能依赖确认页库存快照。
6.4 套包立即购买
{
"source_type": 1,
"activity_id": 100,
"qty": 1
}
服务端加载套包全部可见明细,将“套数 × 每行配置数量”展开为采购明细数量。当前常量 PACKAGE_PURCHASE_LIMIT=1,套包购买限制和每行可售量还会结合活动商品锁定量、已使用量及供给库存判断。
7. 服务站购买主流程
flowchart TD
A[服务站进入秒杀专区] --> B[按审批/启用/时间/名单筛选活动]
B --> C[加载物料 价格 本站库存 在途库存 供给仓库存]
C --> D{单品还是套包}
D -- 单品 --> E[查询可购量和包装后加入购物车]
E --> F[购物车实时刷新有效性和金额门槛]
F --> G[orderPreview]
D -- 套包 --> H[直接购买 展开套包明细]
H --> G
G --> I{实时校验是否通过}
I -- 否 --> X[返回库存/状态/金额/权限错误]
I -- 是 --> J[submitOrder 开启事务]
J --> K[FOR UPDATE 锁活动商品行]
K --> L[写秒杀主单和明细]
L --> M[增加 locked_qty]
M --> N[创建并提交秒杀来源采购单]
N --> O{采购单创建成功}
O -- 否 --> P[事务回滚 秒杀单/锁量/采购草稿不得残留]
O -- 是 --> Q[删除已结算购物车并提交事务]
Q --> R[返回秒杀单详情和 po_order_ids]
8. 下单、支付和关闭时序
sequenceDiagram
participant U as 服务站
participant C as FlashSale Controller
participant S as FlashSaleSer
participant FDB as 秒杀表
participant PO as 采购域
participant PDB as 采购表
participant PAY as 支付中心
participant CB as PayCenterNotify
U->>C: submitOrder(source_type,activity_goods)
C->>S: submitOrder(登录上下文+请求)
S->>FDB: BEGIN + 锁 t_flash_activity_goods
S->>FDB: 新增 flash_order/detail
S->>FDB: 已锁数量增加本次订单数量
S->>PO: savePoOrder + InvPoFactory::saveOrder
PO->>PDB: 新增并提交待支付采购单
alt 采购创建失败
S->>FDB: ROLLBACK
S-->>U: 返回提交失败
else 创建成功
S->>FDB: 删除已结算购物车 + COMMIT
S-->>U: 秒杀订单 + po_order_ids
U->>PAY: 支付采购单
PAY-->>CB: 支付结果回调
CB->>PDB: 写付款/明细并推进采购状态
CB->>S: markPaidByPoOrder
S->>FDB: 已锁数量减少,已用数量增加
S->>FDB: flash_order=已支付
end
关闭路径:
- 待支付主动取消:
cancelOrder找关联采购单;待支付采购单由PoOrderSer::cancelWaitPayOrder关闭并触发释放。 - 秒杀单没有关联采购单:秒杀域直接锁订单并释放
locked_qty。 - 采购待支付超时:
expireWaitPayOrders关闭采购单或直接释放,并把秒杀单置已关闭。 - 采购未发货关闭:
PoOrderSer/InvPoService调用releaseByPoCloseItems,按关闭明细释放已占用数量。 - 支付成功但状态未同步:
compensatePaidOrders根据采购状态补调markPaidByPoOrder。
9. 状态机
9.1 活动状态
活动有三组相关状态,页面不能只看一个字段:
| 维度 | 字段/枚举 | 值 |
|---|---|---|
| 申请状态 | apply_status | 0 草稿、1 审批中、2 通过、3 驳回 |
| OA 状态 | oa_status | 0 草稿、1 审批中、2 通过、3 驳回 |
| 启用状态 | enable_status | 0 未启用、1 启用、2 提前结束 |
| 页面状态 | 运行时计算 | 1 未开始、2 进行中、3 已结束 |
页面状态还依赖当前时间:只有审批通过、启用、start_time <= now <= end_time 才是进行中。
stateDiagram-v2
[*] --> 草稿
草稿 --> 审批中: submitFlashActivity
审批中 --> 审批通过: OA PASS
审批中 --> 审批驳回: OA REJECT
审批驳回 --> 草稿: convertFlashActivityToDraft
审批通过 --> 未开始: 当前时间早于开始时间
审批通过 --> 进行中: 启用且进入活动时间
未开始 --> 进行中: 到达开始时间
进行中 --> 已结束: 到达结束时间
未开始 --> 提前结束: endFlashActivity
进行中 --> 提前结束: endFlashActivity
已结束 --> [*]
提前结束 --> [*]
9.2 秒杀订单状态
stateDiagram-v2
[*] --> 待支付: submitOrder 创建成功
待支付 --> 已支付: 支付回调或支付补偿
待支付 --> 已取消: 用户主动取消
待支付 --> 已关闭: 支付超时或采购关闭
已支付 --> 已关闭: 采购未发货关闭并按明细释放
已支付 --> [*]
已取消 --> [*]
已关闭 --> [*]
代码常量还保留 0=已创建,但 submitOrder 当前直接写入 1=待支付。维护旧数据时不能把 0 当作当前主流程必经状态。
10. 单号与数据关系
erDiagram
FLASH_ACTIVITY ||--o{ FLASH_ACTIVITY_GOODS : activity_id
FLASH_ACTIVITY ||--o{ FLASH_ACTIVITY_STATION_SCOPE : activity_id
FLASH_ACTIVITY ||--o{ FLASH_CART : activity_id
FLASH_ACTIVITY ||--o{ FLASH_ORDER : activity_id
FLASH_ORDER ||--|{ FLASH_ORDER_DETAIL : order_id
FLASH_ACTIVITY_GOODS ||--o{ FLASH_CART : activity_goods_id
FLASH_ACTIVITY_GOODS ||--o{ FLASH_ORDER_DETAIL : activity_goods_id
FLASH_ORDER ||--o{ SCM_PO_ORDER : srcOrderId_srcOrderNo
SCM_PO_ORDER ||--|{ SCM_PO_ORDER_INFO : orderId
秒杀单到采购单的定位关系:
| 采购字段 | 秒杀来源值 |
|---|---|
srcOrderType | PoOrderEnums::SRC_ORDER_TYPE_FLASH_ACTIVITY = 13 |
srcOrderId | t_flash_order.id |
srcOrderNo | t_flash_order.bill_no |
orderType | PoOrderEnums::ORDERTYPE_FLASH_ORDER |
sid | 秒杀订单服务站 ID,关联时必须同时校验 |
不能只用 srcOrderId 跨服务站查单,排查和业务更新至少同时带 sid,有条件时再带 srcOrderNo。
11. 核心表读写矩阵
| 物理表 | 关键字段 | 读取时机 | 写入/更新时机 | 一致性说明 |
|---|---|---|---|---|
t_flash_activity | id、activity_no、apply_status、enable_status、oa_status、时间 | OPS 列表详情、服务站可见性、OA 回调 | 保存、提交、审批、结束、状态刷新 | 与商品规则共同决定是否可售 |
t_flash_activity_goods | id、activity_id、qty、locked_qty、used_qty、价格、仓库、状态 | 商品列表、购物车、预览、提交、补偿 | 保存规则;下单锁量;支付转已用;关闭释放 | 提交订单时 FOR UPDATE,支付用条件更新防止锁量不足 |
t_flash_activity_station_scope | activity_id、type、sid、is_deleted | 判断服务站可见性 | 名单保存/恢复/软删 | 白名单和黑名单规则需结合活动类型理解 |
t_flash_cart | id、sid、activity_goods_id、qty、selected、status、reason | 加购、购物车、确认页 | upsert、改量、勾选、失效刷新、结算删除 | 购物车是临时态,提交时仍重新查活动和库存 |
t_flash_order | id、bill_no、sid、source_type、order_status、金额、expired_at | 详情、取消、支付/关闭关联、补偿扫描 | 提交新增;支付/取消/关闭更新 | bill_no 是业务查询键,生命周期更新需锁行 |
t_flash_order_detail | order_id、activity_goods_id、qty、金额快照、closed_qty | 详情、支付转量、关闭释放 | 提交新增;采购关闭时累计释放 | 订单展示和历史金额以快照为准 |
t_scm_po_order | id、billNo、sid、billStatus、来源三字段 | 支付、取消、补偿、详情关联 | 秒杀提交创建;支付/关闭推进 | 最终支付和履约单据 |
t_scm_po_order_info | orderId、商品、数量、来源明细 | 采购详情、关闭数量计算 | 秒杀提交生成采购明细 | 套包会展开为多行 |
12. 数量、库存和金额口径
12.1 活动数量
locked_qty:待支付秒杀订单已经锁住、尚未确认支付的基础数量。used_qty:支付成功后确认占用的基础数量。- 支付成功:同一事务语义内执行
locked_qty -= qty、used_qty += qty。 - 待支付取消/超时:释放
locked_qty,不得增加used_qty。 - 已支付采购关闭:按关闭明细和
closed_qty释放,避免重复关闭多次扣减。
当前 availableQty() 的代码口径:
- 单品活动返回不受活动数量上限限制的极大值,最终仍受实时供给库存、采购限制等校验。
- 套包单行可售基础数量为
max(0, 行配置数量 × 套包购买限制 - locked_qty - used_qty)。 - 套包整体可买套数还要取所有明细按配比折算后的最小值,并受供给仓库存约束。
因此排查“页面有库存但不能下单”时,不能只查 qty-locked_qty-used_qty;必须同时看活动类型、套包配比、仓库、供给库存、本站库存、在途、最小采购量和金额门槛。
12.2 金额
- 秒杀 Service 内部通过
amountToCent()和formatCent()在元展示值与分存储值之间转换。 t_flash_order.total_amount/origin_amount/discount_amount是提交时的订单快照。- 一口价直接取活动价格;折扣价按服务站采购原价和折扣计算。
- 确认页和提交都会计算金额,提交结果以加锁后重新计算为准。
- 前端传入金额不能作为落库依据;价格、优惠和金额限制由服务端重算。
13. 事务、并发与幂等
| 场景 | 保护方式 | 结论 |
|---|---|---|
| 批量改购物车 | 数据库事务 | 任一行失败回滚整批修改 |
| 提交秒杀订单 | 数据库事务 + 活动商品 FOR UPDATE | 主单、明细、锁量、采购创建必须整体成功 |
| 支付转已用 | 锁秒杀订单 + 条件更新 locked_qty >= qty | 重复支付已是支付态直接成功;锁量不足抛错 |
| 取消/超时 | 锁秒杀订单,已取消/已关闭直接返回 | 避免重复释放待支付锁量 |
| 采购关闭释放 | 明细记录 closed_qty | 应按差量释放,避免同一关闭明细重复处理 |
| OA 回调 | 通过 oa_no 找活动并按结果更新 | 外部回调是否保证严格去重仍需联调验证 |
高风险边界:submitOrder 在同一数据库连接事务内调用采购保存流程。若后续把采购拆成远程服务,现有数据库事务不能覆盖远程成功/失败,必须重新设计幂等键、状态中间态和补偿。
14. 自动任务和补偿
CLI 入口都在 application/controllers/tasks/FlashSaleTask.php:
| 方法 | 扫描对象 | 修复动作 | 验证结果 |
|---|---|---|---|
refreshGoodsStatus(limit) | 活动商品 | 按活动时间/状态刷新商品状态 | 返回扫描和更新统计 |
refreshInvalidCarts(limit) | 秒杀购物车 | 重新计算有效状态和原因 | scanned/updated/valid/invalid |
expireWaitPayOrders(limit) | 已过 expired_at 的待支付秒杀单 | 关闭待支付采购或直接释放锁量 | success/fail/errors |
compensatePaidOrders(limit) | 采购已进入支付后状态但秒杀仍待支付 | 调 markPaidByPoOrder | 秒杀已支付,锁量转已用 |
compensateClosedOrders(limit) | 存在采购关闭明细且秒杀仍待支付/已支付 | 调 releaseByPoCloseItems | 关闭数量已释放 |
runCompensate(limit) | 上述所有常规对象 | 顺序执行状态、购物车、超时、支付、关闭补偿 | 分模块结果汇总 |
执行原则:
- 先用只读 SQL 确定异常集合和影响数量。
- 小
limit执行单项任务,保留任务日志和返回 JSON。 - 再查秒杀单、采购单、
locked_qty/used_qty/closed_qty是否符合预期。 - 不要在生产直接手改三个数量字段;手改会绕过订单明细和幂等保护。
15. 按单号排查 SOP
15.1 从秒杀单查采购单
以下 SQL 使用占位符,默认只读;分表或字段大小写以当前环境为准。
SELECT id, bill_no, activity_id, sid, source_type, order_status,
total_amount, origin_amount, discount_amount, expired_at,
cancel_reason, create_time, modify_time
FROM t_flash_order
WHERE sid = :sid AND bill_no = :flash_bill_no;
SELECT id, order_id, activity_goods_id, sku_id, inv_id, qty,
total_amount, closed_qty
FROM t_flash_order_detail
WHERE order_id = :flash_order_id
ORDER BY id;
SELECT id, billNo, sid, billStatus, orderType,
srcOrderType, srcOrderId, srcOrderNo, paymentTime, modifyTime
FROM t_scm_po_order
WHERE sid = :sid
AND srcOrderType = 13
AND srcOrderId = :flash_order_id
AND srcOrderNo = :flash_bill_no;
判断顺序:
- 查不到秒杀单:确认提交请求是否真正成功,按服务站、时间和日志继续找。
- 有秒杀单无采购单:重点查
submitOrder事务异常和采购创建返回;正常情况下应整体回滚。 - 秒杀待支付、采购已支付后状态:检查支付回调日志,必要时评估
compensatePaidOrders。 - 秒杀取消/关闭但
locked_qty未下降:查采购关闭路径和释放异常。
15.2 核对锁定量和已用量
SELECT g.id, g.activity_id, g.sku_id, g.qty,
g.locked_qty, g.used_qty, g.status, g.version,
d.order_id, d.qty AS order_qty, d.closed_qty
FROM t_flash_activity_goods g
LEFT JOIN t_flash_order_detail d ON d.activity_goods_id = g.id
WHERE g.id IN (:activity_goods_ids)
ORDER BY g.id, d.order_id;
核对规则:
- 待支付未取消的订单明细应贡献
locked_qty。 - 已支付且未关闭的订单明细应贡献
used_qty。 - 已取消/已关闭数量不应继续完整占用。
- 汇总差异时要考虑历史数据、部分关闭和同一活动商品的其他订单,不能只拿一个订单明细与总字段直接相等比较。
15.3 代码和日志定位
rg -n "function submitOrder|function cancelOrder|function markPaidByPoOrder" \
application/Services/Activity application/controllers
rg -n "SRC_ORDER_TYPE_FLASH_ACTIVITY|ORDERTYPE_FLASH_ORDER" application
rg -n "秒杀订单提交失败|秒杀商品锁定数量不足|秒杀待支付超时释放|支付消息通知失败" \
application storage logs
常用日志分类:FlashSaleTask 的任务方法名、PayCenterNotify 的支付处理日志、OA 消费的 OaNotify/oaAudit。实际日志目录和容器路径见本知识库的本地联调与日志专题。
16. 常见故障决策表
| 现象 | 第一检查点 | 第二检查点 | 典型原因 |
|---|---|---|---|
| OPS 提交失败 | 返回是否有 conflictList | 活动/商品时间和仓库 | 跨活动冲突或配置校验失败 |
| OA 已通过页面仍审批中 | oa_no 是否匹配 | OaNotify ACK/NACK 和异常 | 模板编码、编号或回调消费失败 |
| 服务站看不到活动 | 审批/启用/时间 | 黑白名单和服务站 sid | 活动未运行或范围不匹配 |
| 列表有库存但加购失败 | 商品类型和 can_add_cart | 包装换算、限购、实时供给库存 | 套包不允许加购或库存维度不同 |
| 购物车有效但确认失败 | 购物车实时状态 | 金额门槛和仓库解析 | 确认页重新计算发现变化 |
| 确认成功提交失败 | 提交时行锁后的库存 | 采购单创建返回 | 并发抢占或采购校验失败 |
| 支付成功秒杀仍待支付 | 采购来源三字段 | PayCenterNotify 调用和事务 | 回调失败或历史漏同步 |
| 取消后数量未释放 | 采购单是否仍待支付 | 秒杀释放日志/数量字段 | 采购已进入后续状态或释放异常 |
| 采购关闭后仍占量 | po_close_info 和明细 | closed_qty、关闭补偿 | 关闭回调/服务调用遗漏 |
17. 改动风险与回归清单
17.1 高风险改动
- 修改
availableQty、套包数量换算、供给库存选择:可能造成超卖或错误拦截。 - 修改
submitOrder事务顺序:可能产生无采购单秒杀单、锁量泄漏或重复采购单。 - 修改采购来源字段:会让支付、取消、详情和补偿无法关联秒杀单。
- 修改支付回调事务:可能出现采购已支付但秒杀未转已用。
- 修改关闭释放:可能重复释放、负数或已支付数量长期占用。
- 修改活动审批和启用状态:可能让未审批活动提前对服务站可见。
17.2 最小回归集
- OPS 新增单品草稿、编辑、导入后保存、提交 OA、OA 通过、驳回转草稿。
- OPS 新增套包、指定仓、生产日期规则、白名单、提前结束。
- 跨活动同商品冲突能返回完整冲突清单。
- 无权限/不在名单/未开始/已结束服务站不能看到或购买。
- 单品加购、包装换算、改量、勾选、批量删除、历史购物车失效。
- 套包不能加购物车,只能立即购买;套数展开数量正确。
- 确认页金额、仓库、商品明细和提交结果一致。
- 两个并发请求抢最后库存,不产生超卖或重复采购单。
- 支付成功、重复支付回调、支付超时、主动取消均保持状态和数量一致。
- 采购部分/全部关闭后
closed_qty、locked_qty、used_qty正确。 - 三项单独补偿和
runCompensate可重复运行且不重复扣减。 - PC 和移动商城对同一活动返回的业务口径一致。
18. 证据索引与待验证项
已由代码确认
- OPS 请求归一:
application/Entries/Activity/FlashActivitySaveRequestEntry.php、FlashActivityActionRequestEntry.php、FlashActivityStationScopeRequestEntry.php。 - OPS/OA:
application/Services/Activity/FlashActivitySer.php::submitFlashActivity()、callback20260526()。 - 服务站接口:
application/Services/Activity/FlashSaleSer.php::goodsList()至cancelOrder()。 - 提交和采购关联:
FlashSaleSer::submitOrder()、createPoOrdersForFlashOrder()。 - 支付和释放:
markPaidByPoOrder()、releaseByPoOrder()、releaseByPoCloseItems()。 - 支付回调:
application/controllers/tasks/PayCenterNotify.php。 - OA 消费:
application/controllers/tasks/OaNotify.php。 - 补偿:
application/controllers/tasks/FlashSaleTask.php。 - 状态:
application/KzData/Enums/FlashActivityEnums.php、PoOrderEnums.php。 - 物理表名:
application/config/tables.php。
待环境联调确认
- 各环境真实 HTTP URL 前缀、网关鉴权头和 OPS 用户头格式。
- OPS 页面传入
fixed_price/discount_rate的最终单位与页面转换位置。 - OA Provider 的超时、重试和外部幂等保证。
- 定时任务在各环境的 crontab/调度频率和告警接收人。
- 真实供给库存服务的降级策略、缓存时效和超时行为。
这些待验证项不影响理解当前代码主链路,但上线联调前必须逐项确认并回填本文。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| OPS 创建/提交 | 活动运营后台 | 活动时间、价格规则、商品、服务站范围、库存口径 | 活动 OPS Controller + Entry | FlashActivitySer | FLASH_ACTIVITY.id | 活动、商品和范围保存并发起 OA |
| OA 审批回调 | OA MQ DEST_OA_NOTIFY | 模板码、业务 ID、审批结果、回调业务键 | tasks/OaNotify.php | FlashActivitySer::callback20260526 | 活动 ID + OA 单号 | 活动审批状态更新,允许后续启用 |
| 服务站加购/提交 | E站秒杀页 | sid、活动商品、数量、收货信息 | 秒杀站端 Controller | FlashSaleSer::goodsList/addCart/submitOrder | 活动 + sid + SKU | 购物车锁量并生成秒杀订单、采购单 |
| 支付回调 | 支付中心 MQ | 采购单号、支付单号、成功状态 | PayCenterNotify | markPaidByPoOrder | 采购单与秒杀订单关系 | 订单已支付,锁定量转已用量 |
| 超时/关闭补偿 | 定时调度、采购关闭事件 | 超时订单/购物车、采购关闭明细 | FlashSaleTask.php 或采购关闭调用方 | releaseByPoOrder、releaseByPoCloseItems、runCompensate | 秒杀订单/采购单 | 释放锁量、关闭订单,修复遗漏状态 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 |
|---|---|---|---|---|---|
| OPS 保存/提交 | Controller、Entry、FlashActivitySer | 活动 ID、操作方法、OA 业务号 | 活动及明细 commit,OA 请求返回受理 | 参数单位错误、商品/范围重复、OA 调用失败 | 活动 ID 查三类活动表和 OA 回调 |
| OA 消费 | OaNotify + callback20260526 | 模板码、活动 ID、审批结果、消息 ID | ACK 且审批状态正确推进 | 未知模板、活动不存在、重复/乱序 | 活动 ID 回查 FLASH_ACTIVITY.status |
| 加购锁量 | FlashSaleSer::addCart | sid、活动 ID、SKU、购物车 ID | locked_qty 增加且购物车有效 | 库存不足、超限、活动非进行中 | 活动商品 + 购物车数量对账 |
| 下单支付 | submitOrder、PayCenterNotify | 秒杀单号、采购单号、支付单号 | 订单/采购关系完整,支付后转已用量 | 重复提交、采购创建失败、回调遗漏 | 关系表串联三种单号 |
| 定时补偿 | FlashSaleTask 方法日志 | task 方法、批次时间、订单/购物车 ID | 过期记录关闭且锁量释放 | 批次失败、部分释放、反复扫描 | 任务批次 + 订单状态 + 数量守恒 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 保存活动 | FlashActivitySer | 活动事务 | 活动参数、商品、站点范围 | FLASH_ACTIVITY、FLASH_ACTIVITY_GOODS、FLASH_ACTIVITY_STATION_SCOPE | insert/update;活动状态进入草稿/待审批;商品总量和价格落库 | 活动 ID、SKU、sid 范围、更新时间 |
| OA 审批 | submitFlashActivity、callback20260526 | 本地提交与 OA 非同一事务 | 当前活动状态、OA 结果 | 活动主表、OA MQ/结果 | 提交后待审批;同意后进入可启用状态;拒绝进入拒绝态 | OA 单号、模板码、状态迁移 |
| 加入购物车 | FlashSaleSer::addCart | 锁量事务 | 活动进行状态、限购、available = total-locked-used | FLASH_CART、FLASH_ACTIVITY_GOODS | 购物车 insert/update;locked_qty: old -> old+n | 活动商品 total >= locked+used |
| 提交订单 | submitOrder、createPoOrdersForFlashOrder | 秒杀订单与采购创建边界需联调确认 | 有效购物车、锁量、供给关系 | FLASH_ORDER、FLASH_ORDER_DETAIL、采购主明细、关系表 | 购物车有效 -> 已使用;秒杀单待支付;锁量暂保留 | 秒杀单号、采购单号、明细数量一致 |
| 支付成功 | markPaidByPoOrder | 回调事务 | 采购支付成功、订单未处理 | 秒杀订单/活动商品 | 订单 wait_pay -> paid;locked_qty: old -> old-n;used_qty: old -> old+n | 支付单、关系表、数量守恒 |
| 关闭/超时释放 | releaseByPoOrder、FlashSaleTask | 幂等释放事务 | 未支付/关闭状态、尚未释放量 | 订单、购物车、活动商品 | 状态 -> closed/invalid;locked_qty: old -> old-n;已用量仅按已支付保留 | locked >= 0,重复执行数量不再变化 |
子模块级独立追踪
子模块追踪:flash-config 秒杀活动配置
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存 | OPS 活动保存接口 | request ID、activity ID、商品、价格、总量、起止时间 | application/controllers/inner/activity/FlashActivity.php -> Entry -> FlashActivitySer | 当前活动状态、效期冲突、商品和价格单位 | 活动事务 insert/update FLASH_ACTIVITY/GOODS;配置 old -> new,草稿态保持可编辑 | request ID + activity ID + SKU;查主表、商品表和操作时间 | 校验失败零写入;更新只允许草稿等代码状态,事务 rollback 后按 activity ID 重试 |
| 启停 | OPS action 接口 | request ID、activity ID、action、操作人 | application/controllers/scm/FlashSale.php -> FlashActivitySer | OA 结果、当前状态和活动时间 | 事务内 enabled/status old -> new;不直接改变 locked/used 数量 | activity ID + action + 状态更新时间 | 非法迁移拒绝;已开始活动修改核心配置需按代码限制,不直接 SQL 覆盖 |
子模块追踪:flash-oa OA 提交与审批回调
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 提交 OA | OPS 提交活动 | request ID、activity ID、模板码、申请人 | application/Services/Activity/FlashActivitySer.php::submitFlashActivity -> OA Provider | 草稿配置完整性、当前审批态 | 本地事务 status draft -> pending;OA HTTP 在外部边界创建申请并回传 OA 单号 | request ID + activity ID + OA request/form ID | OA 超时先查外部申请;本地 pending 但外部无单时按 activity ID 幂等补提交 |
| 审批回调 | DEST_OA_NOTIFY | message ID、模板码、activity ID、审批结果 | application/controllers/tasks/OaNotify.php -> FlashActivitySer::callback20260526 | 当前活动状态、回调模板和是否已处理 | 单消息事务 pending -> approved/rejected;重复同结果 new -> new | template code + activity ID + OA 单号 + message ID | 未知模板/旧回调告警;外部已批本地失败按 OA 单号重放,不新建审批单 |
子模块追踪:flash-scope 服务站范围与商品可见性
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 配置范围 | OPS 保存黑/白名单 | request ID、activity ID、scope type、sid 集合 | application/Entries/Activity/FlashActivityStationScopeRequestEntry.php -> FlashActivitySer | 活动可编辑状态、站点合法性、重复 sid | 配置事务增删 FLASH_ACTIVITY_STATION_SCOPE;scope set old -> new | activity ID + sid + scope type;比较保存前后集合 | 重复/冲突范围 rollback;不清空后分批写造成半套范围 |
| 站端查询 | E站秒杀商品列表 | request ID、sid、activity ID、分页 | application/controllers/inner/moveMall/FlashSale.php -> FlashSaleSer::goodsList | 活动态、时间、范围、商品状态、供给库存 | 只读事务/无业务表写入;返回 visible true/false 和可购量快照 | request ID + sid + activity/SKU;对照范围表和服务时间 | 列表不可见先查范围和时间;缓存/供给失败不得伪装为已售罄,按接口降级合同处理 |
子模块追踪:flash-cart 购物车与原子锁量
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 加购 | E站 addCart | request ID、sid、activity ID、SKU、qty、cart ID | application/Services/Activity/FlashSaleSer.php::addCart | 活动进行态、限购、available=total-locked-used、旧购物车量 | 锁量事务 insert/update FLASH_CART;locked_qty: old -> old+delta | request ID + activity+sid+SKU+cart ID;查 cart 与 goods 数量 | 条件更新 0 行返回库存不足;事务失败两表均 rollback,禁止只补 cart 不补 locked |
| 改删购物车 | 更新数量、删除或失效任务 | cart ID、旧 qty、新 qty、任务批次 | application/Services/Activity/FlashSaleSer.php / FlashSaleTask | cart 有效态、已锁量和活动时间 | 幂等事务 cart qty/status old -> new;locked_qty: old -> old-delta | cart ID + activity/SKU + task batch | 重复失效必须 0 变化;locked 不得小于 0,异常先按 cart 有效量重算差额 |
子模块追踪:flash-preview 订单预览与价格库存复核
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 预览 | E站 preview 接口 | request ID、sid、cart IDs、地址 | application/controllers/inner/moveMall/FlashSale.php -> FlashSaleSer 预览方法 | 有效购物车、活动价、供给关系、地址和配送规则 | 只读事务/不写订单表;返回价格、数量和供给拆分快照 | request ID + sid + cart/activity/SKU | cart 失效、价格/范围变化时拒绝;预览成功不表示库存已再次锁定 |
| 提交前复核 | submitOrder 进入事务前 | request ID、预览 token/购物车、活动版本 | application/Services/Activity/FlashSaleSer.php::submitOrder 前置校验 | 当前时间、价格、locked、限购和 cart 所有人 | 校验阶段 DB 不变;差异时返回新事实,只有通过才进入下单事务 | request ID + cart ID + activity update time | 客户端旧预览不得直接下单;失败保留/释放锁量按当前代码路径,不人工改价格 |
子模块追踪:flash-submit 秒杀下单与转采购
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 建秒杀单 | E站 submitOrder | request ID、sid、cart IDs、收货信息、flash order no | application/Services/Activity/FlashSaleSer.php::submitOrder | 有效 cart、锁量、价格、供给拆分和来源唯一性 | 秒杀事务 insert FLASH_ORDER/DETAIL;订单由无 -> wait_pay,cart 有效 -> 已提交 | request ID + flash order/cart/activity IDs | 重复提交按稳定请求/购物车关系返回原单;事务失败不得消费 cart 或改变 locked |
| 转采购 | 秒杀单创建后 | flash order ID/no、供给项、SKU、qty | FlashSaleSer::createPoOrdersForFlashOrder -> application/Services/PoOrders/* | 秒杀明细、供给仓和地址 | 跨域边界 insert 采购主明细,orderType=30-Cxx-77、srcOrderType=13,来源关系由无 -> 有 | flash order + PO billNo + SKU | 部分 PO 创建失败按来源关系识别成功项;只补缺失 PO,不重建秒杀单/已存在采购单 |
子模块追踪:flash-pay 秒杀支付与锁量转已用
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 支付回调 | PayCenter 成功事件 | message ID、payOrderNo、PO billNo、flash order ID | application/controllers/tasks/PayCenterNotify.php -> FlashSaleSer::markPaidByPoOrder | 采购支付成功、秒杀单待支付、尚未转换数量 | 回调事务 order wait_pay -> paid;goods locked_qty: old -> old-n、used_qty: old -> old+n | payOrderNo + PO + flash order + activity/SKU | 重复回调 0 增量;支付成功但关系缺失先补关系再按原 PO 处理,不直接改 locked/used |
| 支付查询 | E站轮询/订单详情 | request ID、flash/PO/pay order | application/controllers/inner/moveMall/FlashSale.php -> FlashSale/支付查询 Service | 本地订单、采购支付关系和支付中心状态 | 只读,不写业务表;返回当前最终态 | request ID + 三种单号对照支付回调时间 | 本地待支付但中心成功按回调补偿,不以查询接口直接重复记账 |
子模块追踪:flash-release 取消、关闭与数量释放
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 主动取消 | E站 cancelOrder 或采购待支付取消 | request ID、flash/PO order、取消原因 | application/Services/Activity/FlashSaleSer.php::cancelOrder/releaseByPoOrder | 未支付状态、尚未释放 qty、采购关闭结果 | 幂等事务 order wait_pay -> canceled/closed;locked_qty: old -> old-n | request ID + flash/PO + SKU;比较释放前后数量 | 已支付禁止走未支付释放;重复取消为 0 变化,后续采购关闭失败单独补偿 |
| 履约关闭 | 采购关闭明细回传 | message ID、PO、SKU、close qty | FlashSaleSer::releaseByPoCloseItems | paid/used 数量、已关闭和已释放明细 | 单消息事务按代码从 used 或 locked old -> old-n,closed/released old -> old+n | message ID + PO + flash detail + SKU | 关闭数量不得超过关联明细;只补差额,禁止整单重复释放 |
子模块追踪:flash-compensate 超时与补偿任务
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 超时扫描 | CLI 定时任务 | task batch、截止时间、limit、order/cart ID | application/controllers/tasks/FlashSaleTask.php::expireWaitPayOrders/refreshInvalidCarts | 超时待支付订单、无效 cart、活动时间 | 每条幂等事务 status old -> expired/invalid,locked old -> old-n | task 方法 + batch + order/cart ID;记录扫描/成功/跳过/失败数 | 批次中断可按游标续跑;已完成记录跳过,不扩大时间窗整表更新 |
| 状态补偿 | compensatePaid/Closed/runCompensate | task batch、PO/flash order、支付/关闭最终态 | application/controllers/tasks/FlashSaleTask.php -> mark/release Service | 支付中心、采购和本地秒杀三方事实 | 只补目标差异:wait_pay -> paid 或未释放量 old -> expected;重复运行 DB 不变 | batch + 三种单号 + before/after | 补偿前以支付/采购最终态为仲裁;不能只因本地状态旧就猜测改为成功/关闭 |