本文说明 DGJ2.0 面向维修厂客户的 C 端营销能力,包括平台活动、服务站自建活动申请、OPS 审核、正式活动生成、活动商品和赠品、适用服务站、活动库存、购买次数限制、APP/E站展示、订单锁量与释放、活动资金和退款,以及平台券、服务站券、新人券、定向发券、券库存和核销统计的完整链路。

这一模块最容易产生三个误解:

  1. 活动申请不等于正式活动。 服务站创建的内容先写 t_mkt_client_activity_apply,OPS 审核通过后才复制成 t_mkt_client_activity 正式活动。
  2. 活动库存不是普通商品库存。 inventory/left_num 是营销额度;下单还应经过普通商品可售库存校验。
  3. 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.phpaddApply/updateApply/applyList服务站创建和编辑活动申请
同上list/details/open/stop/top服务站查看和管理正式活动
同上addInventory/addBuyLimit服务站追加活动库存或购买次数
同上uploadGoodsExcel服务站活动商品导入
application/controllers/inner/ClientActivity.phpadd/update/list/detailsOPS 平台活动维护
同上checked/reject/forbidden审核服务站申请和禁用活动
同上lockInventory/unlockInventory内部订单链路活动额度变更
同上addImg/updateImg/imgListAPP/小程序营销素材库
application/controllers/app/Activity.phplistAPP 按 JXCSID 查询活动
application/controllers/tasks/ClientActivity.phpactivityOverdue/startNotify/endNotify过期和通知任务

5.2 优惠券入口

文件核心方法作用
application/controllers/market/ClientCoupon.phpadd/update/selfList服务站自建券及列表
同上sendCoupon/uploadPhoneExcel服务站按维修厂发券
同上invalid/addStock/details禁用、追加库存、使用详情
同上getNewerSwitch/reversalNewerSwitch服务站新人券开关
application/controllers/inner/ClientCoupon.phpadd/list/delete/invalidOPS 平台券管理
同上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_activityMKT_CLIENT_ACTIVITY正式活动主表
t_mkt_client_activity_goodsMKT_CLIENT_ACTIVITY_GOODS活动商品、赠品、活动库存和购买规则
t_mkt_client_activity_relationsMKT_CLIENT_ACTIVITY_RELATIONS活动适用服务站
t_mkt_client_activity_imgsMKT_CLIENT_ACTIVITY_IMGS活动海报、主图、缩略图、轮播和 APP 图
t_mkt_client_activity_applyMKT_CLIENT_ACTIVITY_APPLY服务站自建活动申请及审核快照
t_mkt_client_coupon_templatesMKT_CLIENT_COUPON_TEMPLATES优惠券模板
t_mkt_client_coupon_send_logMKT_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阶梯赠送规则 JSONsatisfy/give 递增
source_type平台/服务站1/2
source_sid自建活动所属站平台为 0
top是否置顶列表排序使用
is_buy_limit/buy_limit是否限制购买次数及次数活动级
is_seckill秒杀展示标识与 type=7/8 需结合调用方确认
unique_code服务站活动复制码8 位随机字符串
platformAPP/小程序平台任务只处理 APP 平台 1

6.3 活动商品字段

字段含义业务约束
activity_id所属活动与主表一对多
sku_id平台商品标识跨服务站匹配
inv_idDGJ 商品标识实际锁库存查询使用
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模板 IDMQ、订单和外部券实例引用
name券名称展示
begin_time/end_time模板有效期end_time 保存为当天 23:59:59
type1 全场,2 营销活动活动券必须有 activity_ids
amount券面额按分保存
use_condition最低使用金额按分保存且必须大于面额
limit模板其他限制字段当前 Service 写入,具体外部含义待契约确认
is_newer是否新人券0/1
activity_ids可用活动 ID 逗号串非关系表,缺少外键约束
stock数据库库存配置与 Redis 列表可能不一致
status0 未发送,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 正式活动状态

值枚举说明
0STATUS_DRAFT草稿,平台活动创建后的初始状态
1STATUS_OPEN启用,可在时间和服务站规则满足时参与
2STATUS_STOP手工停用
3STATUS_END已结束,任务或运行时校正

7.2 服务站活动申请状态

值枚举说明
0APPLY_STATUS_DRAFT草稿
1APPLY_STATUS_CHECKING审核中;服务站提交直接进入此状态
2APPLY_STATUS_CHECK_FAILED审核驳回,可编辑再提交
3APPLY_STATUS_CHECKED已审核,正式活动已尝试创建
4APPLY_STATUS_FORBIDDEN已禁用;正式活动同时停用

7.3 活动类型与金额单位

type玩法limittype_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轮播图
6APP 活动图

7.5 优惠券枚举

维度值说明
模板发送状态0未使用/未发送
模板发送状态1已使用/已发送过
券类型1全场券
券类型2营销活动券
发放对象1按服务站范围
发放对象2按维修厂手机号
来源1OPS 平台券
来源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/checkedOPSid创建正式活动并更新申请
inner/ClientActivity/rejectOPSid,reason申请改驳回并写日志
inner/ClientActivity/addOPS平台活动完整配置写主表、商品、关系
inner/ClientActivity/updateOPS活动完整配置草稿时重建商品和关系
market/ClientActivity/open/stop服务站id正式活动状态变更
inner/ClientActivity/lockInventory订单服务activity_id,goods[]原子减少 left_num
inner/ClientActivity/unlockInventory订单/退款同上受上限保护增加 left_num
app/Activity/listAPPJXCSID、筛选只读活动列表

9.2 优惠券 API

API调用方核心输入持久化/副作用
market/ClientCoupon/add服务站券模板写待审模板和 Redis 库存
inner/ClientCoupon/addOPS券模板写已通过模板和 Redis 库存
market/ClientCoupon/update服务站驳回模板完整内容更新后重新待审
inner/ClientCoupon/checked/rejectOPSid,reason审核状态和日志
market/ClientCoupon/sendCoupon服务站模板、手机号、日期发送日志、模板状态、MQ
inner/ClientCoupon/sendCouponOPS模板、服务站/手机号同上
inner/ClientCoupon/sendNewerCoupon系统phone按新人开关筛模板并发券
inner/ClientCoupon/consumeStock发券链路idRedis RPOP 一个令牌
market/ClientCoupon/addStock服务站id,stockDB 递增和 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():

  1. 平台活动 source_sid=0,初始状态为草稿。
  2. 将平台 sku_id 转成 DGJ 商品 inv_id。
  3. 插入正式活动主表。
  4. 批量插入多个适用服务站。
  5. 批量插入活动商品和赠品。
  6. 所有本地写入在同一个数据库事务中。
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:

  1. 根据随机码找到来源活动 B。
  2. 读取来源活动商品的 sku_id。
  3. 在当前服务站 A 的物料缓存中按 skuId 查商品。
  4. 找不到的 SKU 被跳过。
  5. 返回当前站 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旧/新订单格式化
退款列表refundOrdersProvider::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_type12
source_sid0当前站 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服务站 sidHGET 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_idDGJ 优惠券模板 ID
coupon_id外部用户券实例 ID
disAmountAPP 已分摊后的优惠金额相关字段

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 LLENDB stock 不是实时剩余
编辑券后库存跑到模板 1Redis 键变化update() 覆盖 $id 的确定缺陷
DGJ 显示已发送但客户无券发送日志和 MQDB 提交后 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 RPOPDB 不递减,无恢复凭证
发券DB 事务后 MQ已发送但未建用户券
新人券开关读后反转写重试、并发非幂等
活动通知查当天后直接发 MQ重复调度重复通知

35. 当前代码风险清单

级别风险证据/影响
高活动审核没有申请到正式活动的幂等关联正式活动提交后申请更新失败会重复创建
高券编辑把 DB 更新布尔结果当模板 IDRedis 库存可能写到模板 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_send MQ 及消费者幂等。
  • 外部使用统计和明细。
  • 销售订单 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. 推荐治理顺序

  1. 给活动申请和正式活动增加 source_apply_id 唯一关联,审核接口按申请幂等。
  2. 修复优惠券编辑误用布尔 ID,并停止“覆盖 DB + 追加 Redis”的库存算法。
  3. 明确定义 coupon_templates.stock 是累计额度还是实时剩余,并建立单一权威账本。
  4. 发券采用 Outbox/可靠消息,增加发放批次、目标明细和结果状态。
  5. 给活动库存增加订单号、操作类型和唯一流水,锁定/释放都幂等。
  6. 将 activity_ids、发放 sids 从逗号字符串迁移为关系表。
  7. 将新人券开关改为显式 setSwitch(target)。
  8. 禁用模板时同步券服务,并明确已发券回收策略。
  9. 给到期和通知任务增加时间边界测试和通知幂等记录。
  10. 为活动、券、订单和退款建立统一 traceId/batchNo 日志。

39. 证据文件

结论权威代码
活动玩法、状态、图片位置application/KzData/Enums/ClientActivityEnums.php
券类型、来源、审核状态application/KzData/Enums/ClientCouponEnums.php
服务站活动申请 APIapplication/controllers/market/ClientActivity.php
OPS 活动审核和库存 APIapplication/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
平台/服务站券 APIapplication/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/ConsumerService/Provider汇合点最终业务事实
站点申请market/inner ClientActivitysid、活动、商品、图片、规则ClientActivity ControllerClientActivitySerapply ID保存申请快照等待 OPS 审核
OPS 审核发布task/OPS 回调apply ID、审核结果、正式配置tasks/ClientActivityClientActivitySerapply + activity ID复制正式活动并发布
C端下单/退款App Activity/订单回调activity、商品、数量、用户、订单app/Activity.php/ConsumerMarketing/Orders Provideractivity + order ID扣减/释放营销 left_num
优惠券发放核销ClientCoupon 入口/MQtemplate、用户、数量、外部券 IDClientCoupon Controller/TaskClientCouponSer、CouponProvidertemplate+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 pendingapply ID、商品/关系/图片集合
审核复制ClientActivity Task/Service审核事务approved apply 快照、是否已复制MKT_CLIENT_ACTIVITY* 正式表activity insert;apply 写正式 ID/statusapply/activity 双向关系
下单占用Activity/Order Service订单事务/外部边界left_num、限购、订单幂等活动商品/订单关系、外部订单left_num -n,关系 insertactivity+order+SKU
失败退款释放Callback Service单事件事务原扣量、已释放量活动商品/释放记录left_num +n,最大等于原扣量原订单、refund/message ID
券发放ClientCouponSer -> CouponProvider/MQ本地与外部非原子模板 DB/Redis 剩余量、用户资格模板/发放意图、Redis、MQ/外部券remaining -1;外部券 pending -> issued/usedtemplate/user/external coupon ID

子模块追踪:client-activity-apply 服务站活动申请

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
保存申请服务站新建/编辑 C 端活动申请sid、apply ID、rules/products/imagesapplication/controllers/market/ClientActivity.php -> application/Services/Marketing/ClientActivitySer.php站点资格、规则商品、时间冲突、图片和当前态申请本地事务写完整快照,status none/draft -> pendingrequest ID + sid/apply + item/image counts任一校验失败整批回滚;提交后编辑按状态规则处理
申请回查提交后内容/状态异常apply ID、versionapplication/KzData/Enums/ClientActivityEnums.php申请主明细/关系/图片和审核历史查询只读 不写apply + version + relation counts申请表不等于正式活动;不直接改正式表补申请

子模块追踪:client-activity-audit OPS 审核与正式活动生成

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
审核OPS 通过/拒绝申请apply ID、action、operatorapplication/controllers/inner/ClientActivity.php -> application/Services/Marketing/ClientActivitySer.phppending、最新申请快照、冲突和审核权限审核本地事务 pending -> approved/rejected,通过时复制正式活动并回写 IDrequest ID + apply/activity + action复制任一关系失败整事务回滚;重复审核 0 新活动
后置通知正式活动生成后通知站端apply/activity ID、eventapplication/controllers/tasks/ClientActivity.php已提交正式活动和发送状态commit 后事务外发 MQ/通知,业务表不再变化task/message ID + apply/activity发送失败只补通知,不重复审核复制

子模块追踪:client-activity-manage 正式活动编辑、状态与可见性

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
正式维护OPS 编辑/启停正式活动activity ID、status/rules/timeapplication/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、timeapplication/controllers/app/Activity.phpapproved/enabled/time、范围、库存和用户资格查询只读 不写request ID + user/sid + activity + filter reason不可见逐层查条件;缓存旧只补刷新

子模块追踪:client-activity-stock 活动库存追加与订单扣减

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
追加库存OPS 增加活动商品数量activity/goods ID、addQty、operatorapplication/Services/Marketing/ClientActivitySer.phptotal/used/left、活动状态和重复调整数量本地事务 total/left old -> old+n,写调整记录request ID + activity/goods + before/after只允许增加或按合同调整;重复调整键 0 增量
下单扣减用户活动下单activity/goods、orderNo、qtyapplication/Services/SaOrders/SaOrderSer.phpleft_num、限购、订单幂等和业务可用性订单本地事务条件更新 left_num old -> old-n 并写活动订单关系request ID + activity/order/SKU + affected rows库存不足整单回滚;外部下单失败按原关系释放

子模块追踪:client-activity-refund 活动退款与数量释放

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
退款回调活动订单取消/退款message ID、order/refundNo、qtyapplication/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 setapplication/Services/Marketing/ClientActivitySer.phptotal、有效扣减/释放记录和当前 left查询只读;left=total-used+releasedactivity + orders/releases + totals只补缺失释放/扣减记录,不直接裸改 left_num

子模块追踪:client-coupon-template 优惠券模板与审核

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
保存模板新建/编辑券面额、门槛、范围template ID、sid、rules/stock/timeapplication/controllers/market/ClientCoupon.php -> application/Services/Marketing/ClientCouponSer.php模板状态、范围、库存、已发/已用和冲突模板本地事务 none/draft -> pending 或字段更新request ID + template + version已发券后关键规则不可改;校验失败零写入
审核发布OPS 审核券模板template ID、action、operatorapplication/controllers/inner/ClientCoupon.phppending、最新版本和审核权限审核本地事务 pending -> approved/rejected,通过后可发放request ID + template + action旧版本审核不覆盖新稿;索引/缓存 commit 后刷新

子模块追踪:client-coupon-issue 优惠券发放与库存

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
领取/发放用户领取或运营定向发券template/user ID、issue request、qty=1application/Services/Marketing/ClientCouponSer.phpapproved、DB/Redis remaining、用户资格/限领和重复键本地发放事务/原子缓存 remaining old -> old-1,写 issuance pendingrequest ID + template/user + issue ID无库存/超限零写入;DB/Redis 顺序差异需补偿
外部发券调 KzSaaS Coupon 服务issue ID、external coupon IDapplication/Providers/KzSaas/CouponProvider.php本地发放意图和外部受理态外部调用事务外;回写 pending -> issued/failedrequest ID + issue/external ID + codetimeout 按 issue/user/template 查外部;失败释放库存一次

子模块追踪:client-coupon-use 券实例、核销与销售单

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
下单核销C 端订单使用优惠券coupon instance、user、sale/source order、amountapplication/Providers/KzSaas/OrdersProvider.php -> application/service/scm/InvSaService.php券有效/未用、门槛范围、订单金额和幂等关系订单/券本地或跨系统边界:实例 issued -> locked/used,销售单保存优惠关系request ID + coupon/order/sale IDs外部超时先查券与订单终态,禁止重复核销
取消返券订单取消/退款order/refundNo、coupon instanceapplication/Providers/KzSaas/GrsOrderProvider.php原核销、退款规则和已返状态回调本地事务/外部操作 used/locked -> issued/expired,单键一次message/request ID + coupon/order/refund过期是否返还按合同;失败只补返券,不重做退款

子模块追踪:client-marketing-import 营销导入导出与到期任务

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
导入导出批量活动/券配置和结果导出file/task、rowNo、activity/template keyapplication/Services/Marketing/ClientActivitySer.php模板、字段、业务冲突和现有记录校验不写;每批本地事务 upsert old -> new,导出只读task + rows/errors/affected错误带行号;只重跑失败键,导出不改业务状态
到期任务扫描活动/券到期task batch、time window、business IDsapplication/controllers/tasks/ClientActivity.phpenabled、endTime、未释放库存/券实例每记录本地事务 active -> expired,必要缓存/MQ commit 后task + batch + old/new statuses已过期重跑 0 变化;时区/调度配置需环境确认