本文说明 DGJ2.0 面向维修厂客户的 C 端营销能力,包括平台活动、服务站自建活动申请、OPS 审核、正式活动生成、活动商品和赠品、适用服务站、活动库存、购买次数限制、APP/E站展示、订单锁量与释放、活动资金和退款,以及平台券、服务站券、新人券、定向发券、券库存和核销统计的完整链路。
这一模块最容易产生三个误解:
- 活动申请不等于正式活动。 服务站创建的内容先写
t_mkt_client_activity_apply,OPS 审核通过后才复制成t_mkt_client_activity正式活动。 - 活动库存不是普通商品库存。
inventory/left_num是营销额度;下单还应经过普通商品可售库存校验。 - DGJ 优惠券模板不等于用户持有的券。 DGJ 保存模板、发放意图、Redis 库存令牌和发送日志,真正的券实例、使用明细和统计由 SAAS/优惠券服务维护。
证据范围:本文基于当前仓库 Controller、Service、Model、枚举、任务和调用方静态代码。SAAS 券实例表、MQ 消费端、APP 实际核销接口、生产调度和唯一索引需要跨仓库或环境确认,均在文末列出。
1. 业务目标
- 平台运营可以创建覆盖多个服务站的营销活动。
- 服务站可以创建自有活动申请,由 OPS 审核后上线。
- 支持满量折、满额折、满量赠、满额赠、满量减、满额减以及小程序秒杀展示类型。
- 活动可配置商品、赠品、活动价、起订量、倍数、单次限购、活动库存和总购买次数限制。
- APP/E站按服务站、平台、状态和时间展示可参与活动。
- 下单时原子锁定活动库存,失败/关单/退款时释放,但不能超过活动总量。
- 平台或服务站可以创建全场券、活动券和新人券。
- 优惠券可以按服务站范围或维修厂手机号定向发放。
- 发券后由 MQ 驱动外部券服务生成用户券,DGJ 查询外部服务获取发放和核销统计。
- 定时任务负责活动到期状态和开始/结束通知。
2. 业务边界
2.1 本模块负责
| 能力 | DGJ 责任 |
|---|---|
| 活动配置 | 主表、商品、赠品、服务站关系和素材 |
| 服务站自建 | 申请、审核日志、正式活动生成和禁用 |
| 活动额度 | inventory/left_num 增加、锁定和释放 |
| 活动可见性 | 状态、时间、服务站关系和平台筛选 |
| 活动订单查询 | 聚合 DGJ/GRS 订单、退款和资金流 |
| 优惠券模板 | 金额、门槛、有效期、活动范围、来源和审核 |
| 发券意图 | 目标服务站/维修厂、发送日志、MQ 消息 |
| 新人券开关 | 平台全局开关和服务站级开关 |
2.2 外部系统或其他模块负责
| 能力 | 责任系统/模块 |
|---|---|
| 用户持有券实例 | SAAS/优惠券服务,当前通过 CouponProvider 访问 |
| 维修厂手机号识别 | SAAS 用户服务 UserProvider::aptitudeExist |
| 券领取、核销、回退 | 外部券服务和下单链路;当前仓库需继续追跨服务契约 |
| 普通商品库存 | DGJ 库存域,不由活动 left_num 替代 |
| 销售订单落单 | SAAS/GRS/DGJ 销售订单链路 |
| 秒杀专项采购与支付 | 详见《17_秒杀活动专项手册》 |
| 通用销售活动 | SaleActivitySer 等另一套活动体系,不等同于本模块 |
3. 参与者与术语
3.1 参与者
| 参与者 | 入口 | 主要动作 |
|---|---|---|
| OPS 运营人员 | inner/ClientActivity、inner/ClientCoupon | 平台创建、审核服务站申请、禁用、发券 |
| 服务站管理员 | market/ClientActivity、market/ClientCoupon | 创建活动申请、自建券、追加额度、定向发券 |
| 维修厂客户 | APP/E站 | 浏览活动、下单、使用券、退款 |
| 订单服务 | 内部活动库存接口 | 锁定/释放活动库存、记录活动订单 |
| SAAS 用户服务 | UserProvider | 由手机号识别维修厂和归属服务站 |
| SAAS 券服务 | CouponProvider + MQ 消费端 | 建券实例、发放、核销和统计 |
| 调度系统 | CLI tasks/ClientActivity | 过期、开始通知、结束通知 |
3.2 关键术语
| 术语 | 含义 | 不要混淆 |
|---|---|---|
| 平台活动 | source_type=1/source_sid=0 | 服务站自建活动 |
| 服务站活动申请 | t_mkt_client_activity_apply 的审核记录 | APP 可直接购买的正式活动 |
| 正式活动 | t_mkt_client_activity | 活动申请本身 |
| 活动库存 | 商品行 inventory/left_num | 普通仓库可售库存 |
| 单次限购 | 商品行 purchase_limit | 活动级购买次数 buy_limit |
| 全场券 | coupon.type=1 | 可用于任意业务场景仍需外部券服务判断 |
| 活动券 | coupon.type=2 且关联 activity_ids | 活动本身的折扣规则 |
模板状态 status | 模板是否已经被发送过 | 用户券是否核销 |
valid | 模板是否禁用 | 有效期是否结束 |
| Redis 库存令牌 | 发券前可消费的列表元素 | 数据库 stock 的绝对可靠余额 |
4. 系统边界图
flowchart LR
OPS["OPS 运营"] --> IC["inner/ClientActivity"]
ST["服务站管理员"] --> MC["market/ClientActivity"]
ST --> MCO["market/ClientCoupon"]
OPS --> ICO["inner/ClientCoupon"]
MC --> APPLY[("活动申请表")]
IC --> ACT[("正式活动及商品关系表")]
APPLY -->|"审核通过复制"| ACT
APP["APP / E站"] --> ACT
ORDER["订单服务"] -->|"锁定/释放"| ACT
MCO --> CT[("优惠券模板和发送日志")]
ICO --> CT
CT --> REDIS[("Redis 券库存")]
CT -->|"coupon_send"| MQ["RabbitMQ"]
MQ --> SAAS["SAAS 券服务"]
SAAS --> USERCOUPON[("用户券实例")]
APP --> USERCOUPON
ORDER --> USERCOUPON
5. 代码入口地图
5.1 活动入口
| 文件 | 核心方法 | 作用 |
|---|---|---|
application/controllers/market/ClientActivity.php | addApply/updateApply/applyList | 服务站创建和编辑活动申请 |
| 同上 | list/details/open/stop/top | 服务站查看和管理正式活动 |
| 同上 | addInventory/addBuyLimit | 服务站追加活动库存或购买次数 |
| 同上 | uploadGoodsExcel | 服务站活动商品导入 |
application/controllers/inner/ClientActivity.php | add/update/list/details | OPS 平台活动维护 |
| 同上 | checked/reject/forbidden | 审核服务站申请和禁用活动 |
| 同上 | lockInventory/unlockInventory | 内部订单链路活动额度变更 |
| 同上 | addImg/updateImg/imgList | APP/小程序营销素材库 |
application/controllers/app/Activity.php | list | APP 按 JXCSID 查询活动 |
application/controllers/tasks/ClientActivity.php | activityOverdue/startNotify/endNotify | 过期和通知任务 |
5.2 优惠券入口
| 文件 | 核心方法 | 作用 |
|---|---|---|
application/controllers/market/ClientCoupon.php | add/update/selfList | 服务站自建券及列表 |
| 同上 | sendCoupon/uploadPhoneExcel | 服务站按维修厂发券 |
| 同上 | invalid/addStock/details | 禁用、追加库存、使用详情 |
| 同上 | getNewerSwitch/reversalNewerSwitch | 服务站新人券开关 |
application/controllers/inner/ClientCoupon.php | add/list/delete/invalid | OPS 平台券管理 |
| 同上 | checked/reject/getRejectLog | 审核服务站自建券 |
| 同上 | sendCoupon/sendNewerCoupon | 平台或系统发券 |
| 同上 | consumeStock/addStock | 券库存令牌消费和追加 |
5.3 Service 与 Provider
| 文件 | 责任 |
|---|---|
application/Services/Marketing/ClientActivitySer.php | 活动、申请、商品、库存、订单、退款和资金聚合 |
application/Services/Marketing/ClientCouponSer.php | 券模板、审核、发券、库存和外部统计 |
application/Providers/KzSaas/CouponProvider.php | 查询外部券使用统计和明细 |
application/Providers/KzSaas/UserProvider.php | 手机号对应维修厂资质和归属站 |
application/Providers/KzSaas/OrdersProvider.php | 旧订单、退款和资金数据 |
application/Providers/KzSaas/GrsOrderProvider.php | 新 GRS 订单、退款和资金数据 |
application/Services/Mq/MqSer.php | 向 SAAS 发送活动开始/结束通知 |
6. 核心数据模型
6.1 表清单
| 物理表 | 常量 | 主要用途 |
|---|---|---|
t_mkt_client_activity | MKT_CLIENT_ACTIVITY | 正式活动主表 |
t_mkt_client_activity_goods | MKT_CLIENT_ACTIVITY_GOODS | 活动商品、赠品、活动库存和购买规则 |
t_mkt_client_activity_relations | MKT_CLIENT_ACTIVITY_RELATIONS | 活动适用服务站 |
t_mkt_client_activity_imgs | MKT_CLIENT_ACTIVITY_IMGS | 活动海报、主图、缩略图、轮播和 APP 图 |
t_mkt_client_activity_apply | MKT_CLIENT_ACTIVITY_APPLY | 服务站自建活动申请及审核快照 |
t_mkt_client_coupon_templates | MKT_CLIENT_COUPON_TEMPLATES | 优惠券模板 |
t_mkt_client_coupon_send_log | MKT_CLIENT_COUPON_SEND_LOG | 发券操作日志 |
| 销售订单/出库表 | SCM_SA_ORDER* 等 | 活动订单和券字段的最终业务单据 |
6.2 正式活动字段
| 字段 | 含义 | 单位/规则 |
|---|---|---|
id | 活动主键 | 订单和商品关系引用 |
name | 活动名称 | 搜索展示 |
type | 玩法类型 | 1 到 8,见枚举 |
begin_time/end_time | 参与时间 | 与 online_time 分离 |
online_time | 页面可提前上线时间 | 不能晚于开始时间 |
status | 草稿、启用、停用、结束 | 正式活动状态机 |
limit | 满足门槛 | 满金额类型按分保存,满数量按件 |
type_num | 折扣或直减参数 | 折扣比例乘 100;直减按分;赠送固定 1 |
type_rule | 阶梯赠送规则 JSON | satisfy/give 递增 |
source_type | 平台/服务站 | 1/2 |
source_sid | 自建活动所属站 | 平台为 0 |
top | 是否置顶 | 列表排序使用 |
is_buy_limit/buy_limit | 是否限制购买次数及次数 | 活动级 |
is_seckill | 秒杀展示标识 | 与 type=7/8 需结合调用方确认 |
unique_code | 服务站活动复制码 | 8 位随机字符串 |
platform | APP/小程序平台 | 任务只处理 APP 平台 1 |
6.3 活动商品字段
| 字段 | 含义 | 业务约束 |
|---|---|---|
activity_id | 所属活动 | 与主表一对多 |
sku_id | 平台商品标识 | 跨服务站匹配 |
inv_id | DGJ 商品标识 | 实际锁库存查询使用 |
price | 活动价 | 按分保存 |
is_gift | 是否赠品 | 0 商品,1 赠品 |
inventory | 活动总额度 | 创建时等于 left_num |
left_num | 当前剩余额度 | 原子减/受上限保护地加 |
purchase_limit | 单次限购量 | 必须大于 0;赠品为 0 |
multiple | 购买倍数/赠送倍数 | 非正数默认 1 |
minimum | 起订量 | 非正数最终默认 1 |
ClientActivityGoodsModel::$fields 当前未列出 inv_id,但锁定、释放和部分查询直接使用该列。修改字段投影时需特别回归详情接口。
6.4 活动申请字段
申请表把待审活动和商品配置保存成快照:
| 字段 | 含义 |
|---|---|
| 活动公共字段 | 名称、类型、时间、门槛、图片、限购等 |
goods | 商品数组 JSON,尚未拆入正式商品表 |
gifts | 赠品数组 JSON |
status | 申请审核状态 0 到 4 |
examine_logs | 审核/驳回历史 JSON |
examine_time | 最近审核时间 |
source_sid | 申请服务站 |
6.5 优惠券模板字段
| 字段 | 含义 | 注意 |
|---|---|---|
id | 模板 ID | MQ、订单和外部券实例引用 |
name | 券名称 | 展示 |
begin_time/end_time | 模板有效期 | end_time 保存为当天 23:59:59 |
type | 1 全场,2 营销活动 | 活动券必须有 activity_ids |
amount | 券面额 | 按分保存 |
use_condition | 最低使用金额 | 按分保存且必须大于面额 |
limit | 模板其他限制字段 | 当前 Service 写入,具体外部含义待契约确认 |
is_newer | 是否新人券 | 0/1 |
activity_ids | 可用活动 ID 逗号串 | 非关系表,缺少外键约束 |
stock | 数据库库存配置 | 与 Redis 列表可能不一致 |
status | 0 未发送,1 已发送 | 不是单张券使用状态 |
valid | 是否可发放 | invalid() 写 0 |
invalid_time | 禁用时间 | 不代表已发券被回收 |
source_type/source_sid | 平台或服务站来源 | 服务站券需审核 |
examine_status | 草稿、待审、通过、驳回、禁用 | 模板审核状态 |
examine_logs | 审核日志 JSON | 驳回后允许编辑 |
6.6 关系图
erDiagram
ACTIVITY_APPLY {
bigint id PK
bigint source_sid
int status
text goods
text gifts
}
ACTIVITY {
bigint id PK
int type
int status
bigint source_sid
string unique_code
}
ACTIVITY_GOODS {
bigint id PK
bigint activity_id FK
bigint sku_id
bigint inv_id
int inventory
int left_num
}
ACTIVITY_RELATION {
bigint id PK
bigint activity_id FK
bigint sid
}
COUPON_TEMPLATE {
bigint id PK
int type
string activity_ids
int stock
int valid
}
COUPON_SEND_LOG {
bigint id PK
bigint template_id
string sids
string user
}
ACTIVITY ||--o{ ACTIVITY_GOODS : "商品和赠品"
ACTIVITY ||--o{ ACTIVITY_RELATION : "适用服务站"
ACTIVITY_APPLY }o..o| ACTIVITY : "审核通过后复制"
COUPON_TEMPLATE ||--o{ COUPON_SEND_LOG : "发放日志"
COUPON_TEMPLATE }o..o{ ACTIVITY : "activity_ids 字符串"
图中虚线表示逻辑关联,当前代码没有证明数据库外键或申请到正式活动的关联字段。
7. 枚举与单位
7.1 正式活动状态
| 值 | 枚举 | 说明 |
|---|---|---|
| 0 | STATUS_DRAFT | 草稿,平台活动创建后的初始状态 |
| 1 | STATUS_OPEN | 启用,可在时间和服务站规则满足时参与 |
| 2 | STATUS_STOP | 手工停用 |
| 3 | STATUS_END | 已结束,任务或运行时校正 |
7.2 服务站活动申请状态
| 值 | 枚举 | 说明 |
|---|---|---|
| 0 | APPLY_STATUS_DRAFT | 草稿 |
| 1 | APPLY_STATUS_CHECKING | 审核中;服务站提交直接进入此状态 |
| 2 | APPLY_STATUS_CHECK_FAILED | 审核驳回,可编辑再提交 |
| 3 | APPLY_STATUS_CHECKED | 已审核,正式活动已尝试创建 |
| 4 | APPLY_STATUS_FORBIDDEN | 已禁用;正式活动同时停用 |
7.3 活动类型与金额单位
type | 玩法 | limit | type_num |
|---|---|---|---|
| 1 | 满数量折扣 | 件数 | 折扣比例 × 100,如 0.8 存 80 |
| 2 | 满金额折扣 | 金额 × 100,按分 | 折扣比例 × 100 |
| 3 | 满数量赠送 | 第一阶梯满足件数 | 固定 1,真实规则在 type_rule |
| 4 | 满金额赠送 | 第一阶梯金额 × 100 | 固定 1,真实规则在 type_rule |
| 5 | 满数量直减 | 件数 | 直减金额 × 100 |
| 6 | 满金额直减 | 金额 × 100 | 直减金额 × 100 |
| 7 | 小程序单品秒杀 | 依接口配置 | 依接口配置 |
| 8 | 小程序专题秒杀 | 依接口配置 | 依接口配置 |
7.4 图片位置
position | 素材 |
|---|---|
| 1 | 活动海报 |
| 2 | 活动页主图 |
| 3 | 商品缩略图 |
| 4 | 商品主图 |
| 5 | 轮播图 |
| 6 | APP 活动图 |
7.5 优惠券枚举
| 维度 | 值 | 说明 |
|---|---|---|
| 模板发送状态 | 0 | 未使用/未发送 |
| 模板发送状态 | 1 | 已使用/已发送过 |
| 券类型 | 1 | 全场券 |
| 券类型 | 2 | 营销活动券 |
| 发放对象 | 1 | 按服务站范围 |
| 发放对象 | 2 | 按维修厂手机号 |
| 来源 | 1 | OPS 平台券 |
| 来源 | 2 | 服务站自建券 |
| 审核 | 0/1/2/3/4 | 草稿/待审/通过/驳回/禁用 |
8. 三条主业务链
flowchart TD
A["平台直接创建活动"] --> B["正式活动草稿"]
B --> C["启用"]
D["服务站创建申请"] --> E["活动申请审核中"]
E -->|"OPS 通过"| F["复制成正式活动且启用"]
E -->|"OPS 驳回"| G["服务站编辑再提交"]
C --> H["APP/E站展示"]
F --> H
H --> I["下单锁活动库存"]
I --> J["订单履约/支付"]
I -->|"失败、关单、退款"| K["释放活动库存"]
L["创建优惠券模板"] --> M["平台直接通过或服务站待审"]
M --> N["选择服务站/维修厂发券"]
N --> O["DB日志提交"]
O --> P["MQ生成用户券"]
P --> Q["下单核销和统计"]
9. API 总览
路径基于 CodeIgniter 默认路由,以 /index.php/ 为前缀。inner/* 使用内部 API 身份,market/* 使用当前服务站 Session,app/* 使用 APP 请求封装。
9.1 活动 API
| API | 调用方 | 核心输入 | 持久化/副作用 |
|---|---|---|---|
market/ClientActivity/addApply | 服务站 | 活动、商品、赠品、规则 JSON | 写申请表 |
market/ClientActivity/updateApply | 服务站 | 申请 id 和完整配置 | 更新驳回申请 |
inner/ClientActivity/checked | OPS | id | 创建正式活动并更新申请 |
inner/ClientActivity/reject | OPS | id,reason | 申请改驳回并写日志 |
inner/ClientActivity/add | OPS | 平台活动完整配置 | 写主表、商品、关系 |
inner/ClientActivity/update | OPS | 活动完整配置 | 草稿时重建商品和关系 |
market/ClientActivity/open/stop | 服务站 | id | 正式活动状态变更 |
inner/ClientActivity/lockInventory | 订单服务 | activity_id,goods[] | 原子减少 left_num |
inner/ClientActivity/unlockInventory | 订单/退款 | 同上 | 受上限保护增加 left_num |
app/Activity/list | APP | JXCSID、筛选 | 只读活动列表 |
9.2 优惠券 API
| API | 调用方 | 核心输入 | 持久化/副作用 |
|---|---|---|---|
market/ClientCoupon/add | 服务站 | 券模板 | 写待审模板和 Redis 库存 |
inner/ClientCoupon/add | OPS | 券模板 | 写已通过模板和 Redis 库存 |
market/ClientCoupon/update | 服务站 | 驳回模板完整内容 | 更新后重新待审 |
inner/ClientCoupon/checked/reject | OPS | id,reason | 审核状态和日志 |
market/ClientCoupon/sendCoupon | 服务站 | 模板、手机号、日期 | 发送日志、模板状态、MQ |
inner/ClientCoupon/sendCoupon | OPS | 模板、服务站/手机号 | 同上 |
inner/ClientCoupon/sendNewerCoupon | 系统 | phone | 按新人开关筛模板并发券 |
inner/ClientCoupon/consumeStock | 发券链路 | id | Redis RPOP 一个令牌 |
market/ClientCoupon/addStock | 服务站 | id,stock | DB 递增和 Redis LPUSH |
market/ClientCoupon/details | 服务站 | id,page,limit | 调外部券服务查使用明细 |
10. 服务站活动申请接口
10.1 代表性请求
POST /index.php/market/ClientActivity/addApply
Content-Type: application/json
Cookie: 服务站登录 Session
{
"name": "示例满额减活动",
"type": 6,
"begin_time": "2026-07-20 00:00:00",
"end_time": "2026-07-31 23:59:59",
"online_time": "2026-07-18 00:00:00",
"limit": 500,
"disamount": 50,
"banner": "https://已授权文件地址/示例.png",
"share_img": "https://已授权文件地址/分享.png",
"description": "活动说明",
"description_img": "https://已授权文件地址/详情.png",
"top": 0,
"tags": "夏季保养",
"is_buy_limit": 1,
"buy_limit": 2,
"is_seckill": 0,
"type_rule": [],
"goods": [
{
"inv_id": 12345,
"price": 120.00,
"inventory": 100,
"purchase_limit": 5,
"multiple": 1,
"minimum": 1
}
],
"gifts": []
}
身份字段 source_sid 不从请求读取,而是由当前 Session jxcsys.sid 注入。
10.2 Controller 规则
| 规则 | 行为 |
|---|---|
| 折扣活动 | discount > 0 && discount <= 1,存 discount*100 |
| 满赠活动 | 必须有赠品和 type_rule |
| 阶梯赠送 | 每一行 satisfy 和 give 都必须严格大于上一行 |
| 直减活动 | disamount >= 0,存 disamount*100 |
| 满金额类 | 请求 limit 乘 100 后存储 |
| 上线时间 | 必填,且不能晚于开始时间 |
| 购买次数 | 开启后 buy_limit 必须是正数 |
| 商品 | 不允许重复 inv_id |
| 商品额度 | inventory > 0 |
| 单次限购 | purchase_limit > 0 |
| 起订量 | minimum >= 0,最终 0 会归一为 1 |
10.3 申请写入
sequenceDiagram
participant U as 服务站管理员
participant C as Market Controller
participant S as ClientActivitySer
participant A as 活动申请表
U->>C: JSON 活动配置
C->>C: 玩法、金额、阶梯、时间校验
C->>S: addApply(...,session.sid)
S->>S: 商品重复/库存/限购/起订量校验
S->>S: goods/gifts/type_rule 转 JSON
S->>A: 插入 status=审核中
A-->>S: apply_id
S-->>C: 提交成功
申请表不拆商品明细,审批前修改的是 JSON 快照。
11. OPS 审核与正式活动生成
11.1 审核请求
POST /index.php/inner/ClientActivity/checked
Content-Type: application/x-www-form-urlencoded
Internal-Auth: 内部鉴权信息
id=申请ID
驳回:
POST /index.php/inner/ClientActivity/reject
id=申请ID
reason=驳回原因
11.2 审核通过数据流
sequenceDiagram
participant OPS as OPS
participant C as Inner Controller
participant S as ClientActivitySer
participant A as 活动申请表
participant M as 正式活动主表
participant G as 活动商品表
participant R as 活动服务站关系表
OPS->>C: checked(apply_id)
C->>S: verify(user,id,已审核)
S->>A: 查询且必须为审核中
S->>S: 解码 goods/gifts/type_rule
S->>M: add(source_sid=申请站)
S->>G: 写商品和赠品
S->>R: 仅关联申请站 sid
S->>A: status=已审核 + examine_logs
S-->>C: 审核成功
正式活动创建规则:
source_type=2、source_sid=申请站。- 状态直接为
STATUS_OPEN,不是草稿。 - 生成 8 位
unique_code,供其他服务站复制活动配置。 - 活动关系只包含申请服务站。
- 商品从申请
inv_id反查sku_id后写正式商品表。
11.3 当前一致性缺口
verify() 没有用一个外层事务包住“创建正式活动”和“更新申请状态”。add() 自己提交正式活动事务后,才更新申请表。
失败窗口:
flowchart TD
A["申请状态=审核中"] --> B["正式活动事务提交成功"]
B --> C{"申请状态更新是否成功"}
C -->|"是"| D["申请=已审核"]
C -->|"否"| E["正式活动已存在但申请仍审核中"]
E --> F["再次点击审核"]
F --> G["可能创建第二个正式活动"]
当前正式活动表未看到 apply_id,无法天然根据申请幂等。建议增加 source_apply_id 唯一关系或幂等表,再把状态更新设计成可补偿流程。
12. 平台直接创建正式活动
OPS inner/ClientActivity/add 直接调用 ClientActivitySer::add():
- 平台活动
source_sid=0,初始状态为草稿。 - 将平台
sku_id转成 DGJ 商品inv_id。 - 插入正式活动主表。
- 批量插入多个适用服务站。
- 批量插入活动商品和赠品。
- 所有本地写入在同一个数据库事务中。
flowchart TD
A["OPS 活动请求"] --> B["校验结束时间和购买限制"]
B --> C["sku_id 映射 inv_id"]
C --> D["插入草稿主表"]
D --> E["批量写活动服务站"]
E --> F["批量写商品 inventory=left_num"]
F --> G{"满赠活动"}
G -->|"是"| H["写赠品 is_gift=1 price=0"]
G -->|"否"| I["跳过赠品"]
H --> J["事务提交"]
I --> J
13. 正式活动编辑和状态机
13.1 正式活动状态机
stateDiagram-v2
[*] --> Draft: "平台创建"
[*] --> Open: "服务站申请审核通过"
Draft --> Open: "open"
Open --> Stop: "stop 或 OPS forbidden"
Stop --> Open: "open,且未过期"
Draft --> End: "过期任务"
Open --> End: "过期任务或运行时校正"
Stop --> End: "过期任务或运行时校正"
End --> [*]
switchStatus() 应结合代码确认目标状态、来源站和结束时间;结束状态不可继续变更。forbidden() 仅允许处理服务站来源活动,并同时写 examine_status=禁用 和操作人。
13.2 编辑边界
平台正式活动只有在草稿状态时,update() 才会修改核心规则、删除并重建服务站关系和商品。非草稿状态仍可能更新 top/tags/share_img/is_seckill 等外围字段。
草稿编辑采用“先删除关系和商品,再批量重建”,依赖外层数据库事务回滚。修改时必须回归:
- 删除后插入失败是否完整恢复。
- 商品
sku_id映射缺失。 - 从满赠切到非满赠时旧赠品是否删除。
- 活动已启用后的可编辑字段是否符合产品要求。
14. 活动列表与可见性
APP 入口:
POST /index.php/app/Activity/list
Content-Type: APP 业务请求封装
{
"JXCSID": 10001,
"page": 1,
"pageSize": 20
}
Service getListBySid(..., sid, is_app=1) 综合:
- 活动与
t_mkt_client_activity_relations的服务站关系。 - 正式活动状态。
online_time/begin_time/end_time。- 平台类型。
- 活动商品和服务站商品资料。
- 活动来源及展示字段。
flowchart TD
A["APP 带 JXCSID 查询"] --> B["筛选关联该 sid 的活动"]
B --> C["筛选启用、上线和未结束"]
C --> D["加载活动商品"]
D --> E["按 sku_id/inv_id 补商品资料"]
E --> F["金额从分转元、补玩法名称"]
F --> G["返回活动列表"]
“活动不可见”不能只查 status,还要依次查关系表、上线时间、开始/结束时间、平台和商品映射。
15. 活动复制码
服务站正式活动生成 unique_code 后,其他服务站可以调用:
GET /index.php/market/ClientActivity/applyDetailByUniqueCode?unique_code=XXXXXXXX
Cookie: 当前服务站 Session
复制过程不是直接复制 inv_id:
- 根据随机码找到来源活动 B。
- 读取来源活动商品的
sku_id。 - 在当前服务站 A 的物料缓存中按
skuId查商品。 - 找不到的 SKU 被跳过。
- 返回当前站
invId和原活动规则,供前端再次提交申请。
flowchart LR
B["来源站活动 inv_id B"] --> SKU["共同 sku_id"]
SKU --> A["当前站商品 inv_id A"]
A --> FORM["生成当前站申请表单"]
随机码生成通过“查询不存在后使用”,没有数据库唯一约束证据;并发创建仍可能碰撞。do...while 中对空查询结果直接读取 ['id'] 也可能产生运行时警告。
16. 活动库存模型
16.1 三类库存不要混淆
| 数量 | 表/系统 | 作用 |
|---|---|---|
| 普通可售库存 | 库存域 | 商品实际是否可以出库 |
活动总额度 inventory | 活动商品表 | 活动最多允许卖多少 |
活动剩余额度 left_num | 活动商品表 | 当前还可锁多少 |
活动剩余额度守恒目标:
0 <= left_num <= inventory
已锁/已用额度 = inventory - left_num
16.2 锁定请求
POST /index.php/inner/ClientActivity/lockInventory
Content-Type: application/json
{
"activity_id": 9001,
"goods": [
{"inv_id": 12345, "num": 2},
{"inv_id": 12346, "num": 1}
]
}
原子 SQL 语义:
UPDATE activity_goods
SET left_num = left_num - num
WHERE activity_id = ?
AND inv_id = ?
AND is_gift = 0
AND left_num >= num
多商品在一个数据库事务内依次锁定;任一商品影响 0 行则全部回滚,影响超过 1 行则认为存在重复商品数据并要求人工介入。
16.3 解锁请求
POST /index.php/inner/ClientActivity/unlockInventory
Content-Type: application/json
{
"activity_id": 9001,
"goods": [
{"inv_id": 12345, "sku_id": 55555, "num": 2}
]
}
解锁不要求活动仍启用或未过期,因为关单和退款可能发生在活动结束后。更新条件保证解锁后不超过总库存:
left_num <= inventory - num
16.4 库存状态图
stateDiagram-v2
[*] --> Available: "创建 inventory=left_num"
Available --> Locked: "下单 lockInventory"
Locked --> Available: "下单失败/关单/退款 unlockInventory"
Locked --> Consumed: "订单最终履约,保持扣减"
Available --> Available: "addInventory 同增 inventory 和 left_num"
Consumed --> Available: "售后释放活动额度,依业务契约"
当前锁定接口没有 order_no/request_id,只根据活动、商品和数量扣减。重复调用在库存充足时会重复扣减;重复解锁依赖上限条件阻止超量,但不能识别业务请求是否已处理。需要调用方幂等或增加活动库存流水。
17. 追加库存与购买次数
活动商品追加:
POST /index.php/market/ClientActivity/addInventory
detail_id=商品行ID
activity_id=活动ID
add_num=50
同时执行:
inventory += add_num
left_num += add_num
校验活动属于当前服务站、商品行属于活动且不是赠品。代码检查 add_num <= 0,但错误提示写“必须大于等于 0”,文字与真实规则不一致,真实规则是严格大于 0。
活动购买次数追加:
POST /index.php/market/ClientActivity/addBuyLimit
activity_id=9001
add_num=100
通过原子表达式 buy_limit = buy_limit + add_num 增加。代码命名和历史页面需要进一步确认这里代表“总次数额度”还是“单客户购买次数上限”;静态代码只能证明字段被递增。
18. 活动订单、资金和退款
ClientActivitySer 同时聚合旧 OrdersProvider 和新 GrsOrderProvider 数据:
| 能力 | Service 方法 | 数据源选择 |
|---|---|---|
| 活动订单列表 | getActivityOrderList | 根据参数决定旧订单或 GRS |
| 导出 | getActivityOrderListExport | 对应数据源批量查询 |
| 订单详情 | getOrderDetail | 旧/新订单格式化 |
| 退款列表 | refund | OrdersProvider::refundList 或 GRS |
| 确认线下退款 | confirmRefund | 按 bill_no/refundId 路由 |
| 资金流水 | getPayOrderList | 新旧销售/退款流水 |
flowchart TD
A["活动订单/退款查询"] --> B{"是否满足 GRS 数据条件"}
B -->|"是"| C["GrsOrderProvider"]
B -->|"否"| D["OrdersProvider"]
C --> E["字段格式化和客户名称补全"]
D --> E
E --> F["统一列表/导出"]
确认退款属于外部副作用:GRS 失败会记录 sid/billNo 日志并抛错。请求超时不能直接判定未成功,应按退款单号查询最终状态再决定重试。
19. 活动到期与通知任务
19.1 到期任务
命令入口:
php index.php tasks/ClientActivity/activityOverdue
筛选:
platform = APP(1)
end_time <= 当天 YYYY-MM-DD
status != END
批量更新为 STATUS_END。
风险:用日期字符串和可能带时间的 end_time 比较,活动当天何时结束取决于字段格式和 SQL 比较;必须用边界样本确认是否会在结束日零点提前结束。
19.2 开始/结束通知
sequenceDiagram
participant CRON as 调度
participant T as ClientActivity Task
participant A as 活动表
participant R as 服务站关系表
participant M as MqSer
participant S as SAAS
CRON->>T: startNotify 或 endNotify
T->>A: 查当天开始/结束且启用的 APP 活动
T->>R: 查询关联 sid
T->>M: sendActivityStart/EndToSaas(sids)
M->>S: MQ 通知
代码没有看到按活动/服务站记录“已通知”的幂等表,且 sids 未显式去重。若调度重复执行,当天同一活动可能重复通知。消费者必须幂等,或生产端增加通知记录。
20. 优惠券模板创建
20.1 代表性请求
服务站:
POST /index.php/market/ClientCoupon/add
Content-Type: application/x-www-form-urlencoded
Cookie: 服务站 Session
name=示例活动券
type=2
begin_time=2026-07-20
end_time=2026-07-31
amount=50
use_condition=500
limit=1
is_newer=0
activity_ids=9001,9002
stock=1000
平台使用 inner/ClientCoupon/add,主要字段相同,但 source_sid=0。
20.2 写入规则
| 规则 | 平台券 | 服务站券 |
|---|---|---|
source_type | 1 | 2 |
source_sid | 0 | 当前站 sid |
| 初始审核状态 | 已通过 2 | 待审核 1 |
| 面额/门槛 | 元乘 100 | 元乘 100 |
| 活动券 | 校验活动 ID 存在 | 还要求活动属于当前站 |
| Redis 库存 | 创建后 LPUSH stock 个令牌 | 同左 |
amount 必须严格小于 use_condition。这意味着当前实现不支持“无门槛券”或“满 50 减 50”,除非外部传入其他约定。
flowchart TD
A["创建券模板"] --> B{"amount < use_condition"}
B -->|"否"| C["拒绝"]
B -->|"是"| D{"type=活动券"}
D -->|"是"| E["校验 activity_ids"]
D -->|"否"| F["跳过活动校验"]
E --> G["金额元转分"]
F --> G
G --> H["写模板"]
H --> I["向 Redis 列表压入 stock 个令牌"]
数据库写和 Redis 初始化不在同一事务内,任一步失败都可能留下单边状态。
21. 服务站券审核
21.1 状态机
stateDiagram-v2
[*] --> Checking: "服务站创建"
Checking --> Checked: "OPS checked"
Checking --> Rejected: "OPS reject"
Rejected --> Checking: "服务站 update"
Checked --> Disabled: "invalid/禁用"
Rejected --> Disabled: "invalid/禁用"
Disabled --> [*]
update() 只允许:
- 模板属于当前服务站。
- 当前审核状态为驳回。
更新后重置为待审核。OPS 审核把操作人、时间和原因追加到 examine_logs JSON。
21.2 更新库存的确定缺陷
当前 ClientCouponSer::update():
$id = $this->clientCouponTemplatesModel->updateById($id, $set, true);
$this->addListStock($id, $stock);
Model updateById() 返回数据库更新结果,通常是布尔值,而不是模板 ID。于是 Redis 库存很可能被追加到模板 1,而不是原模板。
同时更新把数据库 stock 设置成新值,却没有清空和重建旧 Redis 列表,只是追加 stock 个令牌。即使 ID 正确,也会把旧剩余库存和新配置相加。
这是高风险确定性缺陷,修复时应明确采用:
- 库存不可编辑,只允许追加;或
- 在安全锁下按数据库目标值重建 Redis;或
- 以唯一券号/额度账本替代匿名列表令牌。
22. 新人券开关
22.1 Redis 键
| 键 | 粒度 | 读取方法 |
|---|---|---|
MKT_NEWER_COUPON_SWITCH | 平台全局 | GET |
HASH_DGJ_MKT_NEWER_COUPON_SWITCH | 服务站 sid | HGET sid |
切换接口都是“读取旧值后写 1-old”,不是设置目标值:
reversalNewerSwitch()
reversalNewerSwitchDgj(sid)
重复请求不幂等,并发请求可能丢失切换。
22.2 新人券发放过滤
flowchart TD
A["维修厂资质审核后触发 sendNewerCoupon"] --> B["查询所有 valid 且未过期的新人券"]
B --> C{"平台券"}
C -->|"是"| D{"全局开关开启"}
C -->|"否,服务站券"| E{"所属站开关开启且审核通过"}
D -->|"否"| F["跳过该模板"]
E -->|"否"| F
D -->|"是"| G["按手机号识别维修厂"]
E -->|"是"| G
G --> H["发券日志 + MQ"]
服务站新人券还要求维修厂归属 sid 等于券 source_sid。
23. 发券接口与对象解析
23.1 按维修厂发券
服务站端只支持按维修厂手机号发放:
POST /index.php/market/ClientCoupon/sendCoupon
Content-Type: application/x-www-form-urlencoded
template_id=7001
send_type=2
phones=13800000001,13800000002
begin_time=2026-07-20
end_time=2026-07-31
前端也可先上传 Excel:
POST /index.php/market/ClientCoupon/uploadPhoneExcel
Content-Type: multipart/form-data
file=<xls或xlsx>
只读取第一张 Sheet 的 A 列手机号,调用 UserProvider::aptitudeExist 过滤不存在或不属于当前服务站的维修厂,返回手机号和维修厂名称。
23.2 平台按服务站发券
代表性请求:
POST /index.php/inner/ClientCoupon/sendCoupon
template_id=7001,7002
send_type=1
is_all=0
sids=10001,10002
begin_time=2026-07-20
end_time=2026-07-31
is_all=1时日志sids=0表示全部服务站。- 指定服务站时
sids保存为逗号字符串。 - 自定义开始时间不能早于模板开始。
- 自定义结束时间不能晚于模板结束,且开始必须早于结束。
23.3 发券数据流
sequenceDiagram
participant U as OPS/服务站/系统
participant S as ClientCouponSer
participant T as 券模板表
participant US as SAAS用户服务
participant L as 发放日志表
participant MQ as RabbitMQ
participant CS as 券服务
U->>S: sendCoupon(params)
S->>T: 查询 valid 且未过期模板
alt 按维修厂
S->>US: aptitudeExist(phones)
US-->>S: 维修厂和归属 sid
end
S->>T: status=已发送
S->>L: 写 template_id/sids/user/时间
S->>S: 提交数据库事务
S->>MQ: coupon_send 模板和目标
MQ->>CS: 创建用户券实例
MQ 目标和路由:
destination = MqEventEnums::DEST_DGJ_NOTIFY
routing_key = MqEventEnums::APP_COUPON_SEND = coupon_send
message_id = uniqid()
24. 发券事务和一致性
数据库事务覆盖:
- 模板
status=1。 - 发放日志插入。
事务提交后才逐条发布 MQ。没有看到 Outbox、生产确认或失败补偿:
flowchart TD
A["DB 事务提交"] --> B["模板已发送 + 日志存在"]
B --> C{"MQ publish 是否成功"}
C -->|"是"| D["外部券服务生成用户券"]
C -->|"否/进程退出"| E["DGJ 显示已发送但用户没有券"]
其他风险:
- 所有新人券因开关被跳过时,
$mqArr可能未初始化,后续foreach产生告警。 - MQ 消息 ID 用
uniqid(),业务重试会产生新 ID,外部消费者不能只按消息 ID 幂等。 - 发送日志没有目标级成功结果,只记录计划发送范围。
- 重复点击发券不会根据业务批次去重,可能给同一用户重复发券,取决于外部券服务幂等规则。
25. 优惠券库存
25.1 双库存模型
| 库存 | 位置 | 更新方式 |
|---|---|---|
| 模板配置库存 | coupon_templates.stock | 创建写值、追加原子加、编辑直接覆盖 |
| 可消费令牌 | Redis LIST_CLIENT_COUPON_STOCK:<id> | 创建/追加 LPUSH,消费 RPOP |
创建时执行:
LPUSH coupon_stock:id 1 2 3 ... stock
消费时:
RPOP coupon_stock:id
返回空表示没有库存。
25.2 库存一致性图
flowchart LR
DB["DB stock 配置"] -->|"创建/追加"| RL["Redis List 令牌"]
RL -->|"RPOP"| SEND["发券动作"]
SEND --> EXT["外部用户券"]
DB -."当前没有随 RPOP 递减".-> GAP["DB 与 Redis 差异"]
当前 consumeStock() 只弹 Redis,不递减数据库 stock,因此 DB 字段更像“累计配置量”而不是实时剩余。列表页面若展示 DB stock,不能解释为可发余额。
range(1, stock) 会一次构建完整数组并展开传给 lPush,大库存可能造成 PHP 内存、参数数量或 Redis 请求体压力。
25.3 追加库存
POST /index.php/market/ClientCoupon/addStock
id=7001
stock=100
校验模板存在、未禁用、属于当前服务站;随后 DB stock += 100,Redis 再压入 100 个令牌。两个动作没有事务,任一步失败会不一致。
26. 券实例、核销与销售单
DGJ 查询使用详情时调用:
CouponProvider::useDetail(sid, coupon_template_id, page, limit, is_all)
CouponProvider::useStatistics(coupon_template_id列表)
返回外部用户券列表后,DGJ 用 contact_id 回表补维修厂名称。
销售订单链路会保存:
| 字段 | 含义 |
|---|---|
coupon_temp_id | DGJ 优惠券模板 ID |
coupon_id | 外部用户券实例 ID |
disAmount | APP 已分摊后的优惠金额相关字段 |
SaOrderSer 注释说明 E站 APP 已分摊优惠券时,订单金额可能是折后金额,同时记录 disAmount。因此报表不能再次无条件减券面额,否则会重复优惠。
sequenceDiagram
participant APP as E站APP
participant CS as 券服务
participant OS as 订单服务
participant DGJ as DGJ销售单
APP->>CS: 查询可用券/预占或核销
CS-->>APP: coupon_id/template_id/优惠金额
APP->>OS: 折后订单和券信息
OS->>DGJ: 创建销售订单
DGJ->>DGJ: 保存 coupon_temp_id/coupon_id/disAmount
OS->>CS: 支付成功核销或失败释放
上图后半段是从字段和系统边界作出的合理链路说明;具体预占、核销和释放接口必须以 SAAS/订单仓库为准,当前 DGJ 仓库不能证明完整时序。
27. 禁用、删除和失效
27.1 优惠券禁用
invalid(id,sid) 只更新:
valid = 0
invalid_time = 当前时间
它没有:
- 删除 Redis 剩余库存。
- 发布“模板禁用”MQ。
- 回收已经发到用户账户的券。
- 自动把审核状态改为禁用。
因此“禁用模板”是否影响已发券,要看外部券服务核销时是否实时查询模板或是否另有同步接口。
27.2 删除模板
delete() 只允许模板 status=0 时删除,即尚未被标记为已发送。删除条件没有显式处理 Redis 库存,可能留下孤儿 Redis List。
27.3 活动结束
活动 STATUS_END 只改变正式活动可用状态,不会自动:
- 释放已锁但未完成订单的活动库存。
- 禁用关联活动券。
- 关闭外部用户券。
- 清理商品和关系表。
这些需要订单关单/退款和券服务共同完成。
28. 导入导出
28.1 活动商品 Excel
uploadGoodsExcel() 使用 PhpSpreadsheet 读取 xls/xlsx,按商品编码、活动价、库存、限购、倍数、起订量等字段解析,再映射当前服务站商品。
需要检查:
- 文件扩展名与真实 MIME。
- 第一 Sheet 和表头版本。
- 商品在当前站是否存在。
- 重复商品、空行、公式单元格和科学计数法。
- 金额元转分的精度。
- 大文件行数和内存上限。
28.2 手机号 Excel
- 只读第一 Sheet。
- 从第一行开始读取 A 列,没有明确跳过表头。
- 遇到空单元格会结束当前行列读取。
- 手机号去重后调用外部用户服务。
- 服务站端过滤非本服务站维修厂。
- 文件被复制到按日目录,代码片段未看到清理任务。
29. 常见异常与排查入口
| 症状 | 首查 | 进一步检查 |
|---|---|---|
| 活动申请一直审核中 | 申请表状态和审核日志 | 是否已生成无关联的正式活动 |
| 审核后出现两个活动 | 同站同名同时间活动 | 审核重试、缺少 apply_id 幂等 |
| APP 看不到活动 | 关系表、状态、时间、平台 | 商品映射和 JXCSID |
| 活动商品显示但不能下单 | left_num 和普通库存 | 单次限购、起订量、购买次数 |
| 提示活动库存不足 | 商品行 left_num | 重复锁定、未执行解锁 |
| 退款后额度未恢复 | unlockInventory 调用日志 | 商品 inv_id/num、重复解锁上限 |
| 新人券未发 | 两级开关 | 模板 valid、过期、审核、维修厂归属站 |
| 页面显示有券库存但发不出 | Redis LLEN | DB stock 不是实时剩余 |
| 编辑券后库存跑到模板 1 | Redis 键变化 | update() 覆盖 $id 的确定缺陷 |
| DGJ 显示已发送但客户无券 | 发送日志和 MQ | DB 提交后 MQ 失败窗口 |
| 使用明细为空 | 外部 CouponProvider 响应 | 模板 ID、sid、外部券实例 |
| 券禁用后用户仍能用 | 外部券核销规则 | DGJ 只改模板 valid,未同步回收 |
30. 排查决策图
flowchart TD
A["营销/优惠券异常"] --> B{"活动还是优惠券"}
B -->|"活动"| C{"申请阶段还是正式活动"}
C -->|"申请"| D["查 apply 状态、日志、正式活动重复"]
C -->|"正式"| E{"展示异常还是下单异常"}
E -->|"展示"| F["关系、状态、时间、平台、商品映射"]
E -->|"下单"| G["活动 left_num + 普通库存 + 限购"]
B -->|"优惠券"| H{"模板、发券还是核销"}
H -->|"模板"| I["valid/审核/时间/活动关系"]
H -->|"发券"| J["Redis库存、发送日志、MQ、外部实例"]
H -->|"核销"| K["CouponProvider、订单coupon字段、外部状态"]
31. 只读 SQL
生产环境只使用只读账号并替换参数。不得直接修状态、库存或 JSON。
31.1 查活动申请
SELECT id, name, source_sid, type, status,
begin_time, end_time, online_time,
is_buy_limit, buy_limit, examine_time,
examine_logs, create_time
FROM t_mkt_client_activity_apply
WHERE id = :apply_id;
31.2 查同站疑似重复正式活动
SELECT id, name, source_type, source_sid, status,
begin_time, end_time, unique_code, create_time
FROM t_mkt_client_activity
WHERE source_sid = :sid
AND name = :activity_name
AND begin_time = :begin_time
AND end_time = :end_time
ORDER BY id DESC;
31.3 查活动、服务站和商品
SELECT a.id, a.name, a.type, a.status, a.source_type, a.source_sid,
a.online_time, a.begin_time, a.end_time,
r.sid,
g.id AS detail_id, g.sku_id, g.inv_id, g.is_gift,
g.price, g.inventory, g.left_num,
g.purchase_limit, g.multiple, g.minimum
FROM t_mkt_client_activity a
LEFT JOIN t_mkt_client_activity_relations r ON r.activity_id = a.id
LEFT JOIN t_mkt_client_activity_goods g ON g.activity_id = a.id
WHERE a.id = :activity_id
ORDER BY r.sid, g.is_gift, g.id;
31.4 检查库存守恒
SELECT id, activity_id, inv_id, inventory, left_num,
inventory - left_num AS locked_or_used,
CASE
WHEN left_num < 0 THEN 'LEFT_NEGATIVE'
WHEN left_num > inventory THEN 'LEFT_OVER_TOTAL'
ELSE 'OK'
END AS check_result
FROM t_mkt_client_activity_goods
WHERE activity_id = :activity_id
AND is_gift = 0;
31.5 查券模板
SELECT id, name, type, begin_time, end_time,
amount, use_condition, is_newer, activity_ids,
stock, status, valid, invalid_time,
source_type, source_sid, examine_status, examine_logs
FROM t_mkt_client_coupon_templates
WHERE id = :template_id;
31.6 查发放日志
SELECT id, template_id, sids, user, begin_time, end_time, create_time
FROM t_mkt_client_coupon_send_log
WHERE template_id = :template_id
ORDER BY id DESC;
sids 是逗号字符串,不能用简单 LIKE '%12%' 判断站点 12,否则会误命中 112。只读排查可临时用 FIND_IN_SET(:sid, sids),长期应规范化为发放批次和目标明细表。
32. Redis 排查
# 平台新人券开关
redis-cli --raw GET '<环境前缀>:mkt_newer_coupon_switch'
# 服务站新人券开关
redis-cli --raw HGET '<环境前缀>:hash_dgj_mkt_newer_coupon_switch' '<sid>'
# 券剩余令牌数量
redis-cli LLEN '<环境前缀>:list_client_coupon_stock:<template_id>'
# 只查看少量元素,避免大列表全量输出
redis-cli LRANGE '<环境前缀>:list_client_coupon_stock:<template_id>' 0 9
完整键名包含 RedisKeys::PREFIX,必须从环境配置确认。禁止在生产通过 LPUSH/RPOP/DEL 手工修复,除非已有审批、备份、差异计算和回滚方案。
33. 日志与代码检索
rg -n "addApply|verify\(|getApplyByUniqueCode|forbidden" \
application/controllers/market/ClientActivity.php \
application/controllers/inner/ClientActivity.php \
application/Services/Marketing/ClientActivitySer.php
rg -n "lockInventory|unlockInventory|AddInventoryById|left_num" \
application/Services/Marketing application/models/marketing application/controllers/inner
rg -n "sendCoupon|sendNewerCoupon|APP_COUPON_SEND|coupon_send" \
application/controllers application/Services/Marketing application/KzData/Enums
rg -n "LIST_CLIENT_COUPON_STOCK|MKT_NEWER_COUPON_SWITCH|HASH_DGJ_MKT_NEWER_COUPON_SWITCH" \
application/KzData/Enums application/Services/Marketing
rg -n "coupon_temp_id|coupon_id|disAmount" \
application/Services/SaOrders application/service/scm application/models/scm
rg -n "activityOverdue|startNotify|endNotify|sendActivity(Start|End)ToSaas" \
application/controllers/tasks application/Services/Mq
检索业务键优先顺序:
activity_id/apply_id > template_id > sid > order_no/refund_no > contact_id/脱敏手机号 > MQ message id
34. 事务、幂等和失败窗口
| 场景 | 当前实现 | 风险 |
|---|---|---|
| 平台活动创建 | 主表、商品、关系同库事务 | 本地原子性较完整 |
| 活动审核通过 | 正式活动事务与申请更新分离 | 重复正式活动 |
| 活动随机码 | 查询后生成 | 并发碰撞,唯一索引未知 |
| 活动库存锁定 | 条件更新 + 多商品事务 | 无业务请求幂等键 |
| 活动库存释放 | 条件更新限制上限 | 无订单级释放流水 |
| 活动追加库存 | 单条原子加 | 无操作流水和重复请求保护 |
| 券模板创建 | DB 后写 Redis | 双写不一致 |
| 券模板编辑 | DB 覆盖 + Redis 追加 | ID 使用错误且库存叠加 |
| 券库存消费 | Redis RPOP | DB 不递减,无恢复凭证 |
| 发券 | DB 事务后 MQ | 已发送但未建用户券 |
| 新人券开关 | 读后反转写 | 重试、并发非幂等 |
| 活动通知 | 查当天后直接发 MQ | 重复调度重复通知 |
35. 当前代码风险清单
| 级别 | 风险 | 证据/影响 |
|---|---|---|
| 高 | 活动审核没有申请到正式活动的幂等关联 | 正式活动提交后申请更新失败会重复创建 |
| 高 | 券编辑把 DB 更新布尔结果当模板 ID | Redis 库存可能写到模板 1 |
| 高 | 券编辑不清理旧 Redis 库存 | DB 目标库存与 Redis 累加库存严重不一致 |
| 高 | 发券 DB 提交后才发 MQ且无 Outbox | 发送日志成功但用户无券 |
| 高 | 券模板禁用不撤销 Redis/外部券 | 禁用语义可能失效 |
| 高 | 活动锁定没有订单/请求幂等键 | 重试重复扣活动库存 |
| 中 | DB stock 不随 Redis 消费递减 | 页面库存口径误导 |
| 中 | 大库存使用 range 一次性 LPUSH | 内存和 Redis 请求压力 |
| 中 | 新人券开关使用反转操作 | 重试导致开关恢复原值 |
| 中 | 发放目标 sids 逗号字符串 | 查询误匹配、无法记录目标级结果 |
| 中 | getSendLogsBySid 调用参数与 Model 签名疑似不一致 | 服务站可见平台券列表可能不完整 |
| 中 | $mqArr 可能未初始化 | 所有新人券跳过时产生告警 |
| 中 | 活动复制码无已确认唯一约束 | 并发碰撞或错误复制 |
| 中 | 过期任务日期边界模糊 | 活动可能提前结束或延迟结束 |
| 中 | 开始通知任务无幂等记录且 sid 未去重 | 重复通知 |
| 中 | 活动商品 Model 投影遗漏 inv_id | 某些详情/锁量调用字段缺失 |
| 低 | add_num 错误文案与 >0 规则不一致 | 使用者误解允许 0 |
36. 修改影响面
36.1 修改活动规则
必须同时回归:
- OPS 平台活动创建和服务站活动申请两套入口。
- 申请审核复制时的元/分转换。
- APP、E站、小程序展示。
- 活动商品、赠品和服务站关系。
- 订单锁定、关单释放、退款释放。
- 新旧 GRS 订单和资金列表。
- 开始/结束通知任务。
- 活动券
activity_ids关联。
36.2 修改优惠券
必须同时回归:
- 平台券和服务站券。
- 全场券、活动券、新人券。
- 审核、驳回编辑、禁用、删除。
- DB 库存和 Redis 库存。
- 按站发券、按维修厂发券、Excel 导入。
coupon_sendMQ 及消费者幂等。- 外部使用统计和明细。
- 销售订单
coupon_temp_id/coupon_id/disAmount。 - 退款、关单时券释放或恢复。
37. 回归测试清单
37.1 平台活动
- 八种玩法分别创建,检查金额单位和规则字段。
- 一个/多个服务站关系。
- 普通商品、赠品、重复 SKU、无映射 SKU。
- 库存 0/1、大值,限购 0/1,起订量 0/1。
- 上线时间等于、早于、晚于开始时间。
- 草稿编辑删除重建商品失败时事务回滚。
- 草稿启用、启用停用、停用恢复、过期结束。
37.2 服务站申请
- 正常提交进入审核中。
- 满赠阶梯相等、倒序和正常递增。
- OPS 通过后只生成一个正式活动。
- 正式活动创建成功、申请更新失败后的补偿。
- OPS 驳回、查看原因、服务站编辑再提交。
- 活动禁用和已结束不可禁用。
- 复制码在当前站有全部/部分/无 SKU 映射。
- 并发生成复制码和重复审核。
37.3 活动库存
- 单商品和多商品锁定。
left_num恰好等于请求数量。- 任一商品不足时整批回滚。
- 重复商品数据影响多行时拒绝。
- 重复锁定同一订单。
- 关单、超时、支付失败和退款分别释放。
- 活动结束后仍可合法释放。
- 重复释放不会超过
inventory。 - 追加库存同时增加总量和剩余量。
- 普通库存不足但活动库存充足的下单失败。
37.4 活动展示和任务
- 关系表不含当前站时不可见。
online_time/begin_time/end_time每个边界秒。- APP 平台和小程序平台区分。
- 当天结束任务不会提前结束。
- 开始/结束任务重复执行的消费者幂等。
- 多活动关联同一站时通知去重。
37.5 券模板
- 平台券直接通过,服务站券进入待审。
- 面额小于、等于、大于使用门槛。
- 全场券和活动券;活动不存在、跨站活动。
- 新人券两级开关组合。
- 驳回后编辑重新待审,非驳回状态禁止编辑。
- 编辑库存不会写错模板且 Redis 与 DB 口径一致。
- 禁用后不再发新券,并明确已发券行为。
- 未发送模板删除时清理 Redis。
37.6 发券
- 平台全站、指定站、指定维修厂。
- 服务站只允许本店维修厂。
- 手机号重复、不存在、跨站和未审核资质。
- 自定义有效期早于/晚于模板边界。
- 新人券归属站过滤。
- 所有模板被开关跳过时正常返回。
- DB 提交后 MQ 失败可补偿。
- 同一批次重复请求不重复发券。
- 外部消费者重复消息幂等。
37.7 券库存与核销
- 创建库存 1、1000 和超大值。
- 并发
RPOP不超发。 - Redis 丢失、重启、DB 与 Redis 数量不一致。
- 追加库存 DB 成功 Redis 失败及反向失败。
- 用户券领取、支付核销、支付失败释放、退款恢复。
- 销售单券字段和折后金额不重复扣减。
- 使用统计与用户券明细一致。
38. 推荐治理顺序
- 给活动申请和正式活动增加
source_apply_id唯一关联,审核接口按申请幂等。 - 修复优惠券编辑误用布尔 ID,并停止“覆盖 DB + 追加 Redis”的库存算法。
- 明确定义
coupon_templates.stock是累计额度还是实时剩余,并建立单一权威账本。 - 发券采用 Outbox/可靠消息,增加发放批次、目标明细和结果状态。
- 给活动库存增加订单号、操作类型和唯一流水,锁定/释放都幂等。
- 将
activity_ids、发放sids从逗号字符串迁移为关系表。 - 将新人券开关改为显式
setSwitch(target)。 - 禁用模板时同步券服务,并明确已发券回收策略。
- 给到期和通知任务增加时间边界测试和通知幂等记录。
- 为活动、券、订单和退款建立统一
traceId/batchNo日志。
39. 证据文件
| 结论 | 权威代码 |
|---|---|
| 活动玩法、状态、图片位置 | application/KzData/Enums/ClientActivityEnums.php |
| 券类型、来源、审核状态 | application/KzData/Enums/ClientCouponEnums.php |
| 服务站活动申请 API | application/controllers/market/ClientActivity.php |
| OPS 活动审核和库存 API | application/controllers/inner/ClientActivity.php |
| APP 活动列表 | application/controllers/app/Activity.php |
| 活动创建、复制、库存、订单和退款 | application/Services/Marketing/ClientActivitySer.php |
| 活动和商品模型 | application/models/marketing/ClientActivity*Model.php |
| 活动任务 | application/controllers/tasks/ClientActivity.php |
| 平台/服务站券 API | application/controllers/inner/ClientCoupon.php、market/ClientCoupon.php |
| 券模板、发券、库存和新人开关 | application/Services/Marketing/ClientCouponSer.php |
| 券模板和日志字段 | application/models/marketing/ClientCoupon*Model.php |
| 销售单券字段 | application/Services/SaOrders/SaOrderSer.php、application/service/scm/InvSaService.php |
| Redis 键 | application/KzData/Enums/RedisKeys.php |
| 发券路由 | application/KzData/Enums/MqEventEnums.php |
40. 待跨系统或环境确认
- SAAS 券服务的用户券表、状态机、唯一键和领取/核销/退款恢复接口。
coupon_send的真实消费者、ACK/NACK、重试、死信和业务幂等键。- APP 下单对活动库存和券的准确调用顺序、事务边界和补偿任务。
- 活动
buy_limit的最终业务含义和实际计数存储位置。 platform=1/2的完整页面分流和小程序活动是否仍使用此任务。- 活动、关系、商品、复制码和券模板的生产唯一索引。
- 活动开始/结束任务真实 Cron 表达式、时区和重复执行策略。
- DGJ 与 GRS 新旧订单的切流日期和历史查询边界。
- 模板禁用是否由券服务实时读取,以及已发券是否继续有效。
- 优惠券
limit字段在外部券服务中的精确定义。 - Excel 上传大小、临时文件清理周期和公式安全策略。
- 运营确认的新人定义:首次注册、首次审核、首次下单还是首次在某服务站建档。
41. 一句话记住这条业务
C 端营销链路的核心是 服务站配置先作为申请快照,经 OPS 审核复制成正式活动;活动商品用独立 left_num 控制营销额度,订单失败和退款必须幂等释放;优惠券在 DGJ 只是模板和发放意图,DB/Redis 库存控制发放数量,MQ 之后的用户券生成、核销和统计属于外部券服务。排查时必须分别找到申请 ID、正式活动 ID、券模板 ID和外部用户券 ID,不能只看其中一层。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 站点申请 | market/inner ClientActivity | sid、活动、商品、图片、规则 | ClientActivity Controller | ClientActivitySer | apply ID | 保存申请快照等待 OPS 审核 |
| OPS 审核发布 | task/OPS 回调 | apply ID、审核结果、正式配置 | tasks/ClientActivity | ClientActivitySer | apply + activity ID | 复制正式活动并发布 |
| C端下单/退款 | App Activity/订单回调 | activity、商品、数量、用户、订单 | app/Activity.php/Consumer | Marketing/Orders Provider | activity + order ID | 扣减/释放营销 left_num |
| 优惠券发放核销 | ClientCoupon 入口/MQ | template、用户、数量、外部券 ID | ClientCoupon Controller/Task | ClientCouponSer、CouponProvider | template+user coupon ID | 本地库存控制,外部券服务生成/核销 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 申请审核 | Controller/Task/Service | request_id、apply/activity ID、sid | apply 审核后正式活动关系完整 | 重复复制、旧审批覆盖 | apply ID 映射 activity ID | | 下单扣量 | ActivitySer/OrdersProvider | activity、order、SKU、用户 | left_num -n 且订单关系一次保存 | 超卖、重复扣量、外部下单失败 | order ID 查活动关系 | | 退款释放 | 订单回调/ActivitySer | original order、refund ID、message ID | left_num +n 一次 | 重复释放、超过原扣量 | 原订单关联扣/释放流水 | | 券发放核销 | CouponSer/Provider/MQ | template ID、user coupon ID、message ID | DB/Redis 扣库存,外部券创建/核销 | 本地扣了外部未发、重复发券 | 模板+外部券 ID 串联 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 保存申请 | ClientActivitySer | 申请事务 | 站点、规则、商品、图片 | MKT_CLIENT_ACTIVITY_APPLY 等申请快照 | insert/update;status pending | apply ID、商品/关系/图片集合 |
| 审核复制 | ClientActivity Task/Service | 审核事务 | approved apply 快照、是否已复制 | MKT_CLIENT_ACTIVITY* 正式表 | activity insert;apply 写正式 ID/status | apply/activity 双向关系 |
| 下单占用 | Activity/Order Service | 订单事务/外部边界 | left_num、限购、订单幂等 | 活动商品/订单关系、外部订单 | left_num -n,关系 insert | activity+order+SKU |
| 失败退款释放 | Callback Service | 单事件事务 | 原扣量、已释放量 | 活动商品/释放记录 | left_num +n,最大等于原扣量 | 原订单、refund/message ID |
| 券发放 | ClientCouponSer -> CouponProvider/MQ | 本地与外部非原子 | 模板 DB/Redis 剩余量、用户资格 | 模板/发放意图、Redis、MQ/外部券 | remaining -1;外部券 pending -> issued/used | template/user/external coupon ID |
子模块追踪:client-activity-apply 服务站活动申请
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存申请 | 服务站新建/编辑 C 端活动申请 | sid、apply ID、rules/products/images | application/controllers/market/ClientActivity.php -> application/Services/Marketing/ClientActivitySer.php | 站点资格、规则商品、时间冲突、图片和当前态 | 申请本地事务写完整快照,status none/draft -> pending | request ID + sid/apply + item/image counts | 任一校验失败整批回滚;提交后编辑按状态规则处理 |
| 申请回查 | 提交后内容/状态异常 | apply ID、version | application/KzData/Enums/ClientActivityEnums.php | 申请主明细/关系/图片和审核历史 | 查询只读 不写 | apply + version + relation counts | 申请表不等于正式活动;不直接改正式表补申请 |
子模块追踪:client-activity-audit OPS 审核与正式活动生成
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 审核 | OPS 通过/拒绝申请 | apply ID、action、operator | application/controllers/inner/ClientActivity.php -> application/Services/Marketing/ClientActivitySer.php | pending、最新申请快照、冲突和审核权限 | 审核本地事务 pending -> approved/rejected,通过时复制正式活动并回写 ID | request ID + apply/activity + action | 复制任一关系失败整事务回滚;重复审核 0 新活动 |
| 后置通知 | 正式活动生成后通知站端 | apply/activity ID、event | application/controllers/tasks/ClientActivity.php | 已提交正式活动和发送状态 | commit 后事务外发 MQ/通知,业务表不再变化 | task/message ID + apply/activity | 发送失败只补通知,不重复审核复制 |
子模块追踪:client-activity-manage 正式活动编辑、状态与可见性
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 正式维护 | OPS 编辑/启停正式活动 | activity ID、status/rules/time | application/controllers/inner/ClientActivity.php -> application/Services/Marketing/ClientActivitySer.php | 正式活动、订单使用、时间和站点范围 | 活动本地事务 fields/status old -> new,保留申请快照 | request ID + activity + old/new status | 有订单字段按规则不可改;终态不回退 |
| C 端可见 | App 活动列表/详情 | user/sid、activity ID、time | application/controllers/app/Activity.php | approved/enabled/time、范围、库存和用户资格 | 查询只读 不写 | request ID + user/sid + activity + filter reason | 不可见逐层查条件;缓存旧只补刷新 |
子模块追踪:client-activity-stock 活动库存追加与订单扣减
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 追加库存 | OPS 增加活动商品数量 | activity/goods ID、addQty、operator | application/Services/Marketing/ClientActivitySer.php | total/used/left、活动状态和重复调整 | 数量本地事务 total/left old -> old+n,写调整记录 | request ID + activity/goods + before/after | 只允许增加或按合同调整;重复调整键 0 增量 |
| 下单扣减 | 用户活动下单 | activity/goods、orderNo、qty | application/Services/SaOrders/SaOrderSer.php | left_num、限购、订单幂等和业务可用性 | 订单本地事务条件更新 left_num old -> old-n 并写活动订单关系 | request ID + activity/order/SKU + affected rows | 库存不足整单回滚;外部下单失败按原关系释放 |
子模块追踪:client-activity-refund 活动退款与数量释放
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 退款回调 | 活动订单取消/退款 | message ID、order/refundNo、qty | application/controllers/tasks/ClientActivity.php | 原扣量、已释放量、退款终态和活动关系 | 单事件本地事务 left_num old -> old+n、release record insert,最大原扣量 | message ID + activity/order/refund + qty | 重复/部分退款按 release key 幂等;不超原占用 |
| 对账补偿 | left_num 与订单关系不平 | activity/goods、order set | application/Services/Marketing/ClientActivitySer.php | total、有效扣减/释放记录和当前 left | 查询只读;left=total-used+released | activity + orders/releases + totals | 只补缺失释放/扣减记录,不直接裸改 left_num |
子模块追踪:client-coupon-template 优惠券模板与审核
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存模板 | 新建/编辑券面额、门槛、范围 | template ID、sid、rules/stock/time | application/controllers/market/ClientCoupon.php -> application/Services/Marketing/ClientCouponSer.php | 模板状态、范围、库存、已发/已用和冲突 | 模板本地事务 none/draft -> pending 或字段更新 | request ID + template + version | 已发券后关键规则不可改;校验失败零写入 |
| 审核发布 | OPS 审核券模板 | template ID、action、operator | application/controllers/inner/ClientCoupon.php | pending、最新版本和审核权限 | 审核本地事务 pending -> approved/rejected,通过后可发放 | request ID + template + action | 旧版本审核不覆盖新稿;索引/缓存 commit 后刷新 |
子模块追踪:client-coupon-issue 优惠券发放与库存
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 领取/发放 | 用户领取或运营定向发券 | template/user ID、issue request、qty=1 | application/Services/Marketing/ClientCouponSer.php | approved、DB/Redis remaining、用户资格/限领和重复键 | 本地发放事务/原子缓存 remaining old -> old-1,写 issuance pending | request ID + template/user + issue ID | 无库存/超限零写入;DB/Redis 顺序差异需补偿 |
| 外部发券 | 调 KzSaaS Coupon 服务 | issue ID、external coupon ID | application/Providers/KzSaas/CouponProvider.php | 本地发放意图和外部受理态 | 外部调用事务外;回写 pending -> issued/failed | request ID + issue/external ID + code | timeout 按 issue/user/template 查外部;失败释放库存一次 |
子模块追踪:client-coupon-use 券实例、核销与销售单
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 下单核销 | C 端订单使用优惠券 | coupon instance、user、sale/source order、amount | application/Providers/KzSaas/OrdersProvider.php -> application/service/scm/InvSaService.php | 券有效/未用、门槛范围、订单金额和幂等关系 | 订单/券本地或跨系统边界:实例 issued -> locked/used,销售单保存优惠关系 | request ID + coupon/order/sale IDs | 外部超时先查券与订单终态,禁止重复核销 |
| 取消返券 | 订单取消/退款 | order/refundNo、coupon instance | application/Providers/KzSaas/GrsOrderProvider.php | 原核销、退款规则和已返状态 | 回调本地事务/外部操作 used/locked -> issued/expired,单键一次 | message/request ID + coupon/order/refund | 过期是否返还按合同;失败只补返券,不重做退款 |
子模块追踪:client-marketing-import 营销导入导出与到期任务
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 导入导出 | 批量活动/券配置和结果导出 | file/task、rowNo、activity/template key | application/Services/Marketing/ClientActivitySer.php | 模板、字段、业务冲突和现有记录 | 校验不写;每批本地事务 upsert old -> new,导出只读 | task + rows/errors/affected | 错误带行号;只重跑失败键,导出不改业务状态 |
| 到期任务 | 扫描活动/券到期 | task batch、time window、business IDs | application/controllers/tasks/ClientActivity.php | enabled、endTime、未释放库存/券实例 | 每记录本地事务 active -> expired,必要缓存/MQ commit 后 | task + batch + old/new statuses | 已过期重跑 0 变化;时区/调度配置需环境确认 |