跳到内容

营销活动

受众:开发者 / 运营后台开发者 摘要:Marketing 模块用于承载运营活动、用户增长和轻量互动玩法。当前已完成营销活动 Admin contract、后台管理页、签到记录首段、抽奖奖池、参与记录持久化、三端抽奖入口、参与访问权益校验、卡券 / 积分 / 余额 / 额外抽奖次数 / 会员经验自动发放、外部奖品履约请求台账、奖品履约 Provider 扩展目录、抽奖奖励发放 Provider SPI 和后台派奖结果收口,后续补签和更多奖励执行器在业务项目内按 Provider 接入。

模块边界

Marketing 是可插拔业务模块,使用独立前缀:

类型约定
Maven 模块spring-open-module-marketing
API 前缀/api/v1/marketing/**
配置前缀spring.open.marketing.*
表前缀marketing_
权限前缀marketing:*

当前模块默认启用。需要关闭后台活动接口时可设置:

yaml
spring:
  open:
    marketing:
      enabled: false

Marketing 不强依赖 Mall、Distribution、Forum、Marketplace 等业务模块。跨模块奖励和业务联动应通过事件、扩展点或应用装配层桥接。

spring.open.marketing.enabled 已通过模块 ServiceProvider 声明为配置中心元数据。启用配置中心后,后台可以发现并维护 Marketing 模块启停配置;未通过配置中心覆盖时继续使用 application.yml / 环境变量中的默认装配值。

营销活动

当前提供活动主表 marketing_campaign、Admin 管理 API 和后台管理页。活动类型和状态保持中性,方便签到、刮刮乐、转盘等玩法复用。

活动类型:

含义
check-in签到活动
scratch-card刮刮乐
lucky-wheel幸运转盘
custom自定义活动

活动状态:

含义
draft草稿
enabled启用
disabled停用
ended已结束

参与访问策略

签到和抽奖写入前会读取活动 riskPolicyJson 中的 participationAccessparticipationAccessPolicy,并通过 Entitlement 的中立 ContentAccessPolicyService 做参与资格判断。未配置时默认 free,不影响普通活动。

示例:

json
{
  "ipLimit": 20,
  "participationAccess": {
    "accessType": "member",
    "itemCode": "marketing-member-draw"
  }
}

字段说明:

字段说明
accessType支持 freeloginmembervippaidpointslimited-free;未配置时按 free 处理
itemCode需要权益凭证的访问类型使用;未填写且需要凭证时默认使用 marketing-campaign-participation
directUnlock是否允许直接解锁;仅用于业务项目明确放行的特殊活动
limitedFreeUntil限免截止时间,ISO-8601 LocalDateTime 格式,例如 2026-07-31T23:59:59

该策略只控制参与资格,不发放权益、不扣款、不写 Money 账本。用户不满足条件时,签到和抽奖写入会被拒绝;状态查询、日历查询和后台运营查询不受影响。

抽奖限次加成策略

抽奖活动可以在 riskPolicyJson.drawBonusChancesriskPolicyJson.drawLimitBonus 中配置后台侧限次加成,用于广告解锁、邀请助力、运营临时加赠等轻量玩法。该策略只由活动配置决定,不信任用户请求体中的自报次数。

示例:

json
{
  "drawBonusChances": {
    "daily": 1,
    "total": 2,
    "reason": "ad-watch"
  }
}

字段说明:

字段说明
daily / dailyChances / dailyBonus在活动每日参与上限基础上追加的次数;基础每日上限为 0 时仍表示不限次,不额外记录加成
total / totalChances / totalBonus在活动累计参与上限基础上追加的次数;基础累计上限为 0 时仍表示不限次,不额外记录加成
reason可选,记录本次加成来源,例如 ad-watchinvite-helpops-bonus

参与成功时,如果限次加成实际改变了本次活动的有效上限,抽奖记录 extraJson.drawLimitBonus 会写入基础上限、加成次数、有效上限、评估时间和原因,便于后台排查活动玩法。该能力不新增表、不生成永久次数授予;需要可累积、可消耗的次数仍使用 draw-chance 奖项和 marketing_lottery_draw_chance_grant

抽奖玩法

刮刮乐和幸运转盘复用活动主表的 scratch-card / lucky-wheel 活动类型。当前已固化玩法规则、奖项配置、参与策略、参与结果 contract,并落地奖池配置与参与记录持久化。

Contract说明
MarketingLotteryRule约束活动编码、玩法类型、每日 / 总参与限制、奖池和风控策略快照
MarketingLotteryPrize约束奖项编码、奖项类型、名称、权重、库存、单用户限制和外部奖励 payload
MarketingLotteryDrawPolicy约束一次参与前的次数限制、奖池可用性、幂等键和下一步动作
MarketingLotteryDrawResult约束一次参与的中奖快照、派奖状态、幂等键和参与时间

奖项类型:

含义
points积分奖励
coupon卡券奖励(Voucher)
physical实物奖品
virtual虚拟奖品
draw-chance额外抽奖次数
thank-you谢谢参与

派奖状态:

含义
pending等待外部奖励执行器发放
granted已发放
failed发放失败
skipped未中奖或无需发放

中奖奖项必须保存 rewardPayloadJsoncoupon 奖项会把 payload 中的 voucherTemplateId 交给 Voucher 发放;points 奖项会把 payload 中的 points 交给 Member Points 写入 Money 积分账本;balance 奖项会把 payload 中的余额资产和金额交给 Money 用户余额账本;draw-chance 奖项会把 payload 中的 chances 写入 Marketing 额外抽奖次数授予表;member-exp 奖项会把 payload 中的 experience 写入 Marketing 会员经验授予表;physical / virtual 奖项会把 providerCodeexternalPrizeCode 写入外部履约请求台账,真实发货、虚拟码发放和供应商回调仍在部署环境验收。参与前可用 MarketingLotteryDrawPolicydrawsTodaydrawsTotal、奖池可用奖项和 idempotencyKey 得到 drawdaily-limit-reachedtotal-limit-reachedprize-pool-empty 下一步动作;谢谢参与奖项即使库存为 0 也可作为保底可抽奖项。后续奖励执行器应按参与结果的 idempotencyKey 幂等发放;未中奖结果必须使用 skipped,中奖结果不能使用 skipped

coupon 奖项 payload 示例:

json
{
  "voucherTemplateId": 1001,
  "ownerType": "user",
  "grantType": "marketing-lottery",
  "sourceType": "marketing-campaign",
  "remark": "新人活动赠券",
  "metadata": {
    "scene": "new-user"
  }
}

voucherTemplateId 必填且必须大于 0。中奖后 Marketing 使用中奖用户作为 ownerId,使用 marketing-lottery:{idempotencyKey}:voucher 作为发券幂等键;Voucher 发放成功后抽奖记录的 rewardStatus 更新为 granted。无自动发放 Provider 的中奖奖项继续写入 pending,由后续奖励执行器或外部运营流程处理;后台可在外部发放完成、失败或复核后手工收口本地派奖状态,并把操作人、TraceId 和备注写入 extraJson.rewardOperation

points 奖项 payload 示例:

json
{
  "points": 100,
  "symbol": "mall",
  "remark": "新人抽奖积分"
}

points 必填且必须大于 0,symbol 可选,默认沿用 Member Points 默认积分资产。中奖后 Marketing 使用 member-points-ledger Provider 调用 Member Points,幂等键为 member-points:marketing-lottery:{userId}:{idempotencyKey}:points,流水业务类型为 member-points-marketing-lottery-reward;写入成功后抽奖记录的 rewardStatus 更新为 granted

balance 奖项 payload 示例:

json
{
  "amount": "18.60",
  "symbol": "CNY",
  "remark": "新人抽奖余额"
}

amount 必填且必须大于 0。assetCode 可直接指定稳定资产编码;未传 assetCode 时可用 assetTypesymbolchaincontractAddress 组合解析资产;都未传时默认使用法币 CNY。中奖后 Marketing 使用 money-balance-ledger Provider 发布中立余额奖励事件,Money 监听后写入用户余额账户 adjust_in 流水,幂等键为 money:balance:marketing-lottery:{userId}:{idempotencyKey}:balance,流水业务类型为 marketing-lottery-reward;写入成功后抽奖记录的 rewardStatus 更新为 granted

draw-chance 奖项 payload 示例:

json
{
  "chances": 2,
  "remark": "再来两次"
}

chances 必填且必须大于 0。中奖后 Marketing 使用 draw-chance-local Provider 写入 marketing_lottery_draw_chance_grant,来源幂等键为 marketing-lottery:{idempotencyKey}:draw-chance;写入成功后抽奖记录的 rewardStatus 更新为 granted。用户后续参与同一活动时,如果日限或总限已达到,服务端会优先原子消耗一条可用额外次数并继续抽奖,抽奖记录 extraJson.drawChanceConsumption 会保留被消耗授予记录、来源中奖记录和消耗时间。

physical / virtual 奖项 payload 示例:

json
{
  "providerCode": "mock-warehouse",
  "externalPrizeCode": "sku-vip-card",
  "externalOrderNo": "ext-optional",
  "remark": "虚拟卡密"
}

providerCodeexternalPrizeCode 必填。中奖后 Marketing 使用 external-fulfillment-contract Provider 写入 marketing_lottery_external_fulfillment,来源幂等键为 marketing-lottery:{idempotencyKey}:external-fulfillment;抽奖记录保持 pending,因为本地只生成可追踪外部履约请求,不伪装真实供应商已经发货或发码。真实外部处理完成后,运营或项目侧 Provider 需要通过派奖处理接口登记 granted / failed、TraceId、外部单号或失败原因。

抽奖奖项表:marketing_lottery_prize。核心字段包括活动 ID、奖项编码、奖项类型、名称、权重、库存、单用户中奖上限、奖励 payload、排序和启用状态。

抽奖参与记录表:marketing_lottery_draw_record。核心字段包括活动 ID / 编号、用户 ID、玩法类型、奖项 ID / 编码 / 类型快照、中奖状态、派奖状态、幂等键、客户端追踪标识和参与时间。

额外抽奖次数授予表:marketing_lottery_draw_chance_grant。核心字段包括活动 ID / 编号、用户 ID、来源中奖记录、来源派奖幂等键、授予次数、已消耗次数、客户端追踪标识和扩展 JSON。

会员经验授予表:marketing_lottery_member_exp_grant。核心字段包括活动 ID / 编号、用户 ID、来源中奖记录、来源派奖幂等键、经验值、目标会员等级编码、客户端追踪标识和扩展 JSON。

外部奖品履约请求表:marketing_lottery_external_fulfillment。核心字段包括活动 ID / 编号、用户 ID、来源中奖记录、来源派奖幂等键、奖项编码 / 类型、Provider 编码、外部奖品编码、履约状态、外部单号、请求 / 响应 payload 和扩展 JSON。

用户侧 API:

方法路径说明
POST/api/v1/marketing/lotteries/campaigns/{campaignId}/draw当前登录用户参与一次刮刮乐或幸运转盘抽奖;请求体必须携带 idempotencyKey

Admin API:

方法路径权限说明
GET/api/v1/admin/marketing/lotteries/prizesmarketing:lottery:view抽奖奖项分页,支持按活动、奖项类型和启用状态筛选
POST/api/v1/admin/marketing/lotteries/prizesmarketing:lottery:manage按活动 ID 和奖项编码新增或更新抽奖奖项
DELETE/api/v1/admin/marketing/lotteries/prizes/{id}marketing:lottery:manage删除抽奖奖项
GET/api/v1/admin/marketing/lotteries/reward-providersmarketing:lottery:view查询抽奖奖品履约 Provider 目录,区分本地 contract 与真实外部验收要求
GET/api/v1/admin/marketing/lotteries/draw-recordsmarketing:lottery:view抽奖参与记录分页,支持按活动、用户、玩法、奖项类型、中奖状态和派奖状态筛选
GET/api/v1/admin/marketing/lotteries/draw-records/summarymarketing:lottery:view按同一筛选条件汇总参与次数、中奖次数和派奖状态
GET/api/v1/admin/marketing/lotteries/draw-records/reward-compensation-planmarketing:lottery:view只读生成待派奖 / 派奖失败记录的补偿建议,不直接修改派奖状态或触发外部发奖
POST/api/v1/admin/marketing/lotteries/draw-records/{id}/reward-statusmarketing:lottery:manage对单条抽奖记录做本地派奖结果收口,支持 granted / failed / skipped,并记录审计备注

抽奖奖品履约 Provider 目录用于后台运维识别奖品类型和履约边界:

  • voucher-local:卡券奖品走 Voucher 本地自动发放,中奖后使用 marketing-lottery:{idempotencyKey}:voucher 幂等键。
  • member-points-ledger:积分奖品走 Member Points 和 Money 积分账户本地自动发放,中奖后使用 member-points:marketing-lottery:{userId}:{idempotencyKey}:points 幂等键。
  • money-balance-ledger:余额奖品走 Money 用户余额账户本地自动发放,中奖后使用 money:balance:marketing-lottery:{userId}:{idempotencyKey}:balance 幂等键。
  • draw-chance-local:额外抽奖次数走 Marketing 本地自动发放,中奖后使用 marketing-lottery:{idempotencyKey}:draw-chance 幂等键,限次后自动消耗。
  • member-experience-local:会员经验走 Marketing 本地授予台账,中奖后使用 marketing-lottery:{idempotencyKey}:member-exp 幂等键,后续会员等级模块可按台账接入成长规则。
  • external-fulfillment-contract:实物 / 虚拟奖品走 Marketing 外部履约请求台账,中奖后使用 marketing-lottery:{idempotencyKey}:external-fulfillment 幂等键;真实发货 / 发码 / 供应商回调仍是部署环境验收项。
  • account-manual:未知账户类奖品保留人工收口兜底,真实发放需运营或项目侧 Provider 按工单完成。
  • external-fulfillment:实物和虚拟奖品人工兜底说明,用于未接入外部履约请求或业务自定义 Provider 时提示真实外部链路验收要求。
  • no-reward:谢谢参与不触发外部履约,记录直接进入跳过状态。

业务项目可以实现 MarketingRewardProvider 追加自己的奖品履约 Provider 描述,例如把会员等级升级或外部虚拟码接到业务侧自动发放执行器。Marketing 只负责让后台目录、补偿计划和审计报表发现这些能力,不自建余额表或外部履约事实。

实际自动发放走 MarketingLotteryRewardGrantProvider。内置 VoucherMarketingLotteryRewardGrantProvider 已把 coupon 奖项接到 Voucher 发券,MemberPointsMarketingLotteryRewardGrantProvider 已把 points 奖项接到 Member Points / Money 积分账本,MoneyBalanceMarketingLotteryRewardGrantProvider 已把 balance 奖项接到 Money 用户余额账本,DrawChanceMarketingLotteryRewardGrantProvider 已把 draw-chance 奖项接到 Marketing 自有额外抽奖次数表,MemberExperienceMarketingLotteryRewardGrantProvider 已把 member-exp 奖项接到 Marketing 自有会员经验授予表,ExternalFulfillmentMarketingLotteryRewardGrantProvider 已把 physical / virtual 奖项接到 Marketing 外部履约请求表;外部奖品真实供应商接入方可以按同一 SPI 判断奖项类型、读取 rewardPayloadJson、使用抽奖记录 idempotencyKey 幂等发放,并返回 grantedfailed。Provider 返回 granted / failed 后,Marketing 会回写派奖状态和 extraJson.rewardOperation;返回 pending 时只保留本地请求台账和待处理状态,不伪装真实外部发放完成。

派奖补偿计划是只读运营排障入口:它只读取中奖且状态为 pending / failed 的抽奖记录,按奖项类型生成卡券核对、账户类补发、外部履约跟进等建议,并标记需要真实外部验收的实物 / 虚拟奖品。该计划不会写数据库、不会调用真实外部 Provider,也不会替代“派奖处理”;运营确认真实发放或失败原因后,仍需通过派奖处理接口登记本地状态、TraceId、操作人和备注。

前端入口:

路由 / 组件说明
Adminmarketing/lottery-prizes/index抽奖奖池分页、活动 / 类型 / 状态筛选、新增 / 修改和删除
Adminmarketing/lottery-records/index抽奖参与记录分页、多条件筛选、当前筛选运营统计、奖品 Provider 目录、派奖补偿计划和派奖结果处理
Appmarketing/lottery输入活动 ID 后参与刮刮乐 / 幸运转盘并展示中奖结果
PC/marketing/lottery输入活动 ID 后参与刮刮乐 / 幸运转盘并展示中奖结果

抽奖写入规则:

  • 只允许 campaignType=scratch-cardcampaignType=lucky-wheel 的活动抽奖。
  • 活动必须处于 enabled 状态,且当前时间在活动开始和结束时间窗口内。
  • 如果活动 riskPolicyJson.participationAccess 配置了登录、会员、VIP、付费或积分解锁等条件,服务端会先通过 Entitlement 校验当前用户参与资格,未满足时拒绝写入抽奖记录。
  • 如果活动 riskPolicyJson.drawBonusChances / drawLimitBonus 配置了限次加成,服务端会在计算每日 / 累计上限时叠加后台配置的次数,并把加成证据写入抽奖记录 extraJson.drawLimitBonus
  • 同一 idempotencyKey 只能处理一次,避免重复参与或重复发奖。
  • dailyLimit 大于 0 时限制单用户每日参与次数;totalLimit 大于 0 时限制单用户累计参与次数。
  • 当用户已达到 dailyLimittotalLimit 时,如果存在同活动未消耗的额外抽奖次数授予记录,服务端会原子消耗 1 次并允许继续抽奖;没有可用额外次数时仍按原限制拒绝。
  • 服务端只从启用奖项中抽取;中奖奖项必须有库存且未超过单用户中奖上限;thank-you 可作为库存为 0 的保底未中奖奖项。
  • 抽中中奖奖项时扣减 Marketing 奖项库存;couponpointsbalancedraw-chancemember-exp 奖项由内置 Provider 自动发放并写入 grantedphysical / virtual 奖项会生成外部履约请求并保持 pending,无可用 Provider 的中奖奖项保持 pending;抽中 thank-you 时写入 skipped
  • 积分奖励已由内置 Provider 写入 Member Points / Money 积分账本;余额奖励已由内置 Provider 写入 Money 用户余额账本;额外抽奖次数和会员经验已由内置 Provider 写入 Marketing 自有授予表;实物履约等奖励由后续监听方或奖励执行器按 idempotencyKey 幂等发放。若当前仍是外部人工流程,运营可在 Admin 抽奖记录页按真实结果把本地状态收口为 grantedfailed
  • 中奖记录不能手工调整为 skipped;已经 granted 的中奖记录不能降级为失败,避免外部奖品已交付后产生状态回退歧义。

Admin API

方法路径权限说明
GET/api/v1/admin/marketing/campaignsmarketing:campaign:view活动分页
GET/api/v1/admin/marketing/campaigns/{id}marketing:campaign:view活动详情
GET/api/v1/admin/marketing/campaigns/summarymarketing:campaign:view活动状态汇总
GET/api/v1/admin/marketing/operations/summarymarketing:campaign:view营销运营总览,聚合活动状态、签到记录和抽奖派奖积压
GET/api/v1/admin/marketing/operations/audit-reportmarketing:campaign:view营销运营审计报表,聚合派奖积压、外部奖品验收项和运营处理建议
POST/api/v1/admin/marketing/campaignsmarketing:campaign:create创建活动
PUT/api/v1/admin/marketing/campaigns/{id}marketing:campaign:update更新活动
PUT/api/v1/admin/marketing/campaigns/{id}/statusmarketing:campaign:update更新活动状态
DELETE/api/v1/admin/marketing/campaigns/{id}marketing:campaign:delete删除单个活动
DELETE/api/v1/admin/marketing/campaignsmarketing:campaign:delete批量删除活动

后台菜单入口:营销活动 / 营销活动,组件路径 marketing/campaigns/index。页面支持活动分页、状态汇总、运营总览、运营审计报表、创建、编辑、启用 / 停用 / 结束、单删和批删。运营总览会展示今日签到、累计签到、抽奖参与、待派奖和派奖失败,便于运营快速发现需要补偿或人工处理的活动积压。

运营审计报表只读复用现有活动、签到、抽奖记录和奖品 Provider 目录,不新增报表表。它会把派奖失败、待派奖、真实外部奖品验收和无启用活动等情况转成可操作发现,并在后台展示外部验收清单。该入口不会修改派奖状态、不会调用真实外部 Provider,也不会替代抽奖记录页的派奖处理;运营确认真实发放或失败原因后,仍需在派奖处理入口登记状态、TraceId、操作人和备注。

创建和更新活动时,campaignNo 在全局唯一。活动开始时间不能晚于结束时间;启用状态只允许使用 draftenableddisabledended

签到

签到首段基于 check-in 类型活动工作,提供当前用户签到和后台记录审计。

签到记录表:marketing_check_in_record。核心字段包括活动 ID、用户 ID、签到自然日、连续签到天数、累计签到天数、场景、奖励上下文 JSON 快照、客户端追踪标识和签到时间。

用户侧 API:

方法路径说明
GET/api/v1/marketing/check-ins/campaigns/{campaignId}/status查询当前登录用户今日签到状态
GET/api/v1/marketing/check-ins/campaigns/{campaignId}/calendar查询当前登录用户在日期区间内的每日签到状态;startDate / endDate 可选,默认本月一日至今天
POST/api/v1/marketing/check-ins/campaigns/{campaignId}当前登录用户完成今日签到

Admin API:

方法路径权限说明
GET/api/v1/admin/marketing/check-insmarketing:check-in:view签到记录分页,支持按活动、用户、场景和签到日期范围筛选

后台菜单入口:营销活动 / 签到记录,组件路径 marketing/checkins/index

签到写入规则:

  • 只允许 campaignType=check-in 的活动签到。
  • 活动必须处于 enabled 状态,且当前时间在活动开始和结束时间窗口内。
  • 同一用户在同一活动的同一自然日只能签到一次。
  • totalLimit 大于 0 时作为单用户累计签到上限。
  • 签到成功后服务端计算连续天数和累计天数,并把活动 rewardPolicyJson 写入记录的 rewardContextJson,供奖励扩展点消费。
  • 签到记录写入成功后发布 MarketingCheckInCompletedEvent,事件在事务提交后触发;无事务上下文时直接触发。
  • 签到日历复用签到记录表,未签到日期会返回 checked=false,不额外写空白记录。

签到事件扩展

其他业务模块可以监听 com.springopen.module.marketing.event.MarketingCheckInCompletedEvent 接入奖励、通知、任务或会员成长。

事件字段:

字段说明
eventId稳定事件 ID,当前格式为 marketing-check-in:{checkInRecordId}
campaignId / campaignNo签到活动 ID / 编码
userId签到用户 ID
checkInRecordId签到记录 ID
checkInDate签到自然日
streakDays / totalDays连续签到天数 / 累计签到天数
scene签到场景
rewardContextJson活动奖励策略快照
clientTraceId / extraJson客户端追踪标识 / 扩展上下文
checkedAt / occurredAt签到时间 / 事件发生时间

监听示例:

java
@Component
public class CheckInRewardListener {

    @EventListener
    public void onCheckInCompleted(MarketingCheckInCompletedEvent event) {
        String idempotencyKey = event.getEventId() + ":points";
        // 按 idempotencyKey 幂等发放积分、优惠券、会员经验或任务奖励。
    }

}

接入要求:

  • 奖励发放必须幂等,建议使用 eventId + rewardType + rewardTargetcheckInRecordId + rewardType + rewardTarget
  • Marketing 不直接依赖积分、优惠券、余额、会员、通知等业务模块。
  • 奖励流水、到账状态和用户侧展示由监听方所在模块维护。

后续扩展

后续签到、补签、抽奖和奖池能力应继续遵循当前活动 contract:

  • 玩法表只保存 Marketing 自己的规则、参与记录、奖项和中奖审计。
  • 抽奖玩法继续使用 MarketingLotteryRuleMarketingLotteryPrizeMarketingLotteryDrawResult;除 coupon 奖项复用 Voucher 发放外,不把更多奖项 payload 解释逻辑写死在 Marketing 模块。
  • 余额和积分奖品通过中立事件交给 Money / Member Points 写入对应账本;会员经验、徽章、通知等发放不写死在 Marketing 中。
  • 奖励发放通过事件交给对应模块处理,并保持幂等;确有编排需求时再新增 Contributor 扩展点。
  • 注释掉 Marketing 模块后,其他业务模块仍应可以编译、启动和运行。

Released under the Apache License 2.0.