Knowledge note
正在加载知识笔记
正在加载知识笔记
Knowledge note
2026年7月9日
面向刚接触项目的学习者。本文基于当前仓库代码整理,帮助你理解业务、架构、核心流程、数据模型、运行方式,并提炼可写进简历的项目亮点。
group-buy-market 是一个拼团营销服务系统,核心目标是为商品提供拼团活动配置、优惠试算、锁单、支付结算、退款、成团通知、超时补偿等能力。
可以把它理解成电商交易链路里的“营销中台/拼团活动服务”:
项目不是简单 CRUD,重点在 DDD 分层、责任链/策略模式、Redis 并发控制、RabbitMQ 可靠通知、定时补偿、动态配置等工程化设计。
| 类别 | 技术 |
|---|---|
| 基础框架 | Spring Boot 2.7.12 |
| 构建工具 | Maven 多模块工程 |
| JDK | Java 8 |
| Web | Spring MVC |
| ORM | MyBatis |
| 数据库 | MySQL 8 |
| 缓存/分布式锁 | Redis、Redisson |
| 消息队列 | RabbitMQ |
| HTTP 客户端 | OkHttp |
| JSON | Fastjson/Fastjson2 |
| 线程池 | ThreadPoolExecutor 自定义配置 |
| 监控 | Spring Boot Actuator、Prometheus、Grafana |
| 日志 | Logback、Logstash Encoder、ELK 配置 |
| 部署 | Docker、Docker Compose、Nginx |
| 辅助组件 | xfg-wrench 设计模式框架、动态配置中心、限流组件 |
group-buy-market
├── group-buy-market-api # API 接口和 DTO
├── group-buy-market-app # Spring Boot 启动模块、配置、MyBatis XML
├── group-buy-market-domain # 领域层,核心业务逻辑
├── group-buy-market-infrastructure # 基础设施层,数据库/Redis/MQ/HTTP 适配
├── group-buy-market-trigger # 触发层,HTTP 接口、定时任务、MQ 监听
├── group-buy-market-types # 通用类型、枚举、异常、常量
└── docs # SQL、Docker Compose、Nginx、前端静态页、监控配置
| 模块 | 主要职责 |
|---|---|
group-buy-market-api | 定义对外服务接口,如首页营销查询、交易锁单、结算、退款;定义请求/响应 DTO。 |
group-buy-market-app | 应用启动入口 cn.bugstack.Application;加载配置、线程池、Redis、MyBatis、日志等。 |
group-buy-market-domain | 项目最核心的业务层,包含活动试算、交易锁单、支付结算、退款、人群标签等领域逻辑。 |
group-buy-market-infrastructure | 实现 domain 中定义的仓储和端口接口,负责 MyBatis DAO、Redis、RabbitMQ、HTTP 回调、动态配置。 |
group-buy-market-trigger | 接收外部请求或系统事件,包括 Controller、Job、MQ Listener。 |
group-buy-market-types | 放通用枚举、响应码、异常、事件基类、常量。 |
项目整体采用 DDD + 分层架构,依赖关系大致如下:
app
├── trigger
│ ├── api
│ └── domain
└── infrastructure
└── domain
└── types
重点是:domain 只定义仓储接口和端口接口,不直接依赖 MyBatis、Redis、RabbitMQ。具体实现放在 infrastructure,这就是“依赖倒置”的体现。
入口接口:
POST /api/v1/gbm/index/query_group_buy_market_config
核心代码:
MarketIndexController
-> IIndexGroupBuyMarketService#indexMarketTrial
-> DefaultActivityStrategyFactory
-> RootNode
-> SwitchNode
-> MarketNode
-> TagNode
-> EndNode
流程说明:
userId、source、channel、goodsId。@RateLimiterAccessInterceptor 限制访问频率。RootNode 做基础参数校验。SwitchNode 判断动态配置:是否降级、用户是否在切量范围内。MarketNode 并发查询活动配置和商品信息,然后根据活动配置选择优惠策略计算支付价。TagNode 判断用户是否在人群标签范围内,决定活动是否可见、是否可参与。EndNode 组装试算结果。优惠策略由 Spring 注入成 Map<String, IDiscountCalculateService>,根据 market_plan 选择:
| 策略 | Bean 名称 | 含义 | 示例 |
|---|---|---|---|
| 直减 | ZJ | 原价直接减固定金额 | 原价 100,表达式 20,支付 80 |
| 满减 | MJ | 满 X 减 Y | 表达式 100,10 |
| 折扣 | ZK | 按比例折扣 | 表达式 0.8 |
| N 元购 | N | 直接指定优惠后价格 | 表达式 9.9 |
入口接口:
POST /api/v1/gbm/trade/lock_market_pay_order
核心代码:
MarketTradeController#lockMarketPayOrder
-> ITradeLockOrderService#lockMarketPayOrder
-> TradeLockRuleFilterFactory
-> ActivityUsabilityRuleFilter
-> UserTakeLimitRuleFilter
-> TeamStockOccupyRuleFilter
-> TradeRepository#lockMarketPayOrder
锁单流程:
outTradeNo 查询是否已经存在未支付订单,用于接口幂等。teamId 为空,创建新的拼团队伍记录 group_buy_order。teamId 不为空,更新已有队伍的锁单数量 lock_count。group_buy_order_list。并发控制点:
| 场景 | 设计 |
|---|---|
| 同一个队伍多人同时加入 | Redis incr 抢占队伍库存,超过目标人数则回滚 decr。 |
| Redis 抢占成功但数据库写入失败 | 使用 recovery key 记录恢复量,避免库存永久被占。 |
| 用户重复参与 | biz_id = activityId_userId_takeCount,结合唯一约束控制。 |
| 重复锁单请求 | outTradeNo 查询已有订单,已有则直接返回。 |
入口接口:
POST /api/v1/gbm/trade/settlement_market_pay_order
核心代码:
MarketTradeController#settlementMarketPayOrder
-> ITradeSettlementOrderService#settlementMarketPayOrder
-> TradeSettlementRuleFilterFactory
-> SCRuleFilter
-> OutTradeNoRuleFilter
-> SettableRuleFilter
-> EndRuleFilter
-> TradeRepository#settlementMarketPayOrder
-> TradeTaskService#execNotifyJob
结算流程:
source、channel、userId、outTradeNo、outTradeTime。complete_count。notify_task。这套设计属于“本地消息表 + 异步投递 + 定时补偿”的可靠通知思路。即使实时通知失败,后续 GroupBuyNotifyJob 也会扫描未完成任务继续补偿。
入口接口:
POST /api/v1/gbm/trade/refund_market_pay_order
定时任务:
TimeoutRefundJob
核心代码:
MarketTradeController#refundMarketPayOrder
-> ITradeRefundOrderService#refundOrder
-> TradeRefundRuleFilterFactory
-> DataNodeFilter
-> UniqueRefundNodeFilter
-> RefundOrderNodeFilter
-> IRefundOrderStrategy
退款流程:
unpaid2RefundStrategypaid2RefundStrategypaidTeam2RefundStrategyRefundSuccessTopicListener 消费退款消息,恢复 Redis 队伍锁单库存。超时补偿:
TimeoutRefundJob 每分钟扫描超时未支付订单,自动发起退款流程,防止用户锁单后不支付导致队伍名额长期占用。
库存恢复幂等:
退款恢复库存时使用 refund_lock_{orderId} 做 Redis 幂等锁,避免 MQ 重复消费导致重复恢复库存。
核心代码:
TagService
TagRepository
ActivityRepository#isTagCrowdRange
人群标签用于控制:
实现方式:
crowd_tags_detail。这种设计适合大量用户标签判断,比每次查数据库更轻。
入口接口:
GET /api/v1/gbm/dcc/update_config?key=downgradeSwitch&value=1
核心配置:
| 配置项 | 默认值 | 作用 |
|---|---|---|
downgradeSwitch | 0 | 活动降级开关,1 表示拦截营销试算。 |
cutRange | 100 | 用户切量范围,根据 userId hash 后两位判断。 |
scBlacklist | s02c02 | 渠道来源黑名单,结算时拦截。 |
cacheSwitch | 0 | 缓存开关,当前 0 表示开启缓存。 |
示例:
curl "http://127.0.0.1:8091/api/v1/gbm/dcc/update_config?key=downgradeSwitch&value=1"
curl "http://127.0.0.1:8091/api/v1/gbm/dcc/update_config?key=cutRange&value=50"
curl "http://127.0.0.1:8091/api/v1/gbm/dcc/update_config?key=rateLimiterSwitch&value=close"
初始化 SQL 主要在:
docs/dev-ops/mysql/sql/2-29-group_buy_market.sql
docs/tag/v3.0/mysql/sql/group_buy_market.sql
| 表名 | 作用 |
|---|---|
sku | 商品基础信息,包含商品 ID、商品名、原价。 |
sc_sku_activity | 渠道、来源、商品与活动的关联。 |
group_buy_activity | 拼团活动配置,如活动 ID、目标人数、有效时间、活动状态、人群标签限制。 |
group_buy_discount | 营销优惠配置,如优惠类型、优惠策略、优惠表达式。 |
group_buy_order | 拼团队伍主单,记录 teamId、目标人数、完成数量、锁单数量、队伍状态、回调配置。 |
group_buy_order_list | 用户参与拼团的订单明细,记录 userId、orderId、outTradeNo、价格、订单状态。 |
notify_task | 本地通知任务表,记录 HTTP/MQ 通知参数、次数、状态,用于可靠通知和重试。 |
crowd_tags | 人群标签主表。 |
crowd_tags_detail | 标签用户明细。 |
crowd_tags_job | 标签批处理任务。 |
常见状态:
| 状态位置 | 值 | 含义 |
|---|---|---|
group_buy_activity.status | 0/1/2/3 | 创建、生效、过期、废弃。 |
group_buy_order.status | 0/1/2/3 | 拼单中、完成、失败、完成但含退单。 |
group_buy_order_list.status | 0/1/2 | 初始锁定、消费完成、用户退单/关闭。 |
notify_task.notify_status | 0/1/2/3 | 初始、完成、重试、失败。 |
curl -X POST "http://127.0.0.1:8091/api/v1/gbm/index/query_group_buy_market_config" \
-H "Content-Type: application/json" \
-d '{
"userId": "xfg01",
"source": "s01",
"channel": "c01",
"goodsId": "9890001"
}'
返回内容包括:
curl -X POST "http://127.0.0.1:8091/api/v1/gbm/trade/lock_market_pay_order" \
-H "Content-Type: application/json" \
-d '{
"userId": "xfg01",
"teamId": null,
"activityId": 100123,
"goodsId": "9890001",
"source": "s01",
"channel": "c01",
"outTradeNo": "100000000001",
"notifyConfigVO": {
"notifyType": "HTTP",
"notifyUrl": "http://127.0.0.1:8091/api/v1/test/group_buy_notify"
}
}'
curl -X POST "http://127.0.0.1:8091/api/v1/gbm/trade/lock_market_pay_order" \
-H "Content-Type: application/json" \
-d '{
"userId": "xfg02",
"teamId": "29487599",
"activityId": 100123,
"goodsId": "9890001",
"source": "s01",
"channel": "c01",
"outTradeNo": "100000000002",
"notifyConfigVO": {
"notifyType": "MQ"
}
}'
curl -X POST "http://127.0.0.1:8091/api/v1/gbm/trade/settlement_market_pay_order" \
-H "Content-Type: application/json" \
-d '{
"source": "s01",
"channel": "c01",
"userId": "xfg01",
"outTradeNo": "100000000001",
"outTradeTime": "2026-07-06T12:00:00.000+08:00"
}'
curl -X POST "http://127.0.0.1:8091/api/v1/gbm/trade/refund_market_pay_order" \
-H "Content-Type: application/json" \
-d '{
"userId": "xfg01",
"outTradeNo": "100000000001",
"source": "s01",
"channel": "c01"
}'
建议准备:
如果使用 Docker Compose,可参考:
docs/dev-ops/docker-compose-environment.yml
启动环境:
cd docs/dev-ops
docker-compose -f docker-compose-environment.yml up -d
该 Compose 会启动:
| 服务 | 端口 |
|---|---|
| MySQL | 13306:3306 |
| phpMyAdmin | 8899 |
| Redis | 16379:6379 |
| Redis Admin | 8081 |
| RabbitMQ | 5672、15672 |
SQL 文件在:
docs/dev-ops/mysql/sql/2-29-group_buy_market.sql
如果使用 compose,MySQL 容器会挂载 docs/dev-ops/mysql/sql 到 /docker-entrypoint-initdb.d,首次启动时自动执行初始化脚本。
默认配置文件:
group-buy-market-app/src/main/resources/application.yml
group-buy-market-app/src/main/resources/application-dev.yml
需要重点检查:
| 配置 | 说明 |
|---|---|
server.port | 默认 8091。 |
spring.datasource.url | 数据库地址,当前配置偏作者本机环境。 |
spring.datasource.username/password | 数据库账号密码,要和你的 MySQL 一致。 |
redis.sdk.config.host/port | Redis 地址端口,要和本地或 Docker 一致。 |
spring.rabbitmq.addresses/port | RabbitMQ 地址端口,要确认是 AMQP 端口。 |
xfg.wrench.config.register.host/port | 动态配置中心使用的 Redis 地址。 |
注意:docker-compose-environment.yml 中 MySQL root 密码是 123456,映射端口是 13306;而 application-dev.yml 中示例密码和端口可能不同。初次启动失败时,优先检查这里。
方式一:IDE 启动
运行 group-buy-market-app/src/main/java/cn/bugstack/Application.java
方式二:Maven 打包后启动
mvn clean package -DskipTests
java -jar group-buy-market-app/target/group-buy-market-app.jar
方式三:Docker 镜像
cd group-buy-market-app
docker build -t group-buy-market-app:local -f Dockerfile .
项目把业务逻辑集中在 domain,把数据库、缓存、MQ、HTTP 调用放在 infrastructure。领域层只依赖接口,不关心具体技术实现。
这让核心业务更容易测试,也方便后续替换 MyBatis、Redis、MQ 等基础设施。
锁单、结算、退款都使用责任链:
| 流程 | 责任链 |
|---|---|
| 锁单 | 活动可用性、用户参与次数、队伍库存占用。 |
| 结算 | 渠道黑名单、外部单号、支付时间、结果封装。 |
| 退款 | 数据加载、重复退款校验、退款策略选择。 |
责任链的好处是:每个规则独立,可扩展,后续新增规则时不需要把一个大方法越写越长。
优惠计算和退款处理都用了策略模式:
ZJ、MJ、ZK、N。策略模式适合“同一个业务动作,有多种算法/处理方式”的场景。
项目使用 Redis 做:
重点 Key:
| Key | 作用 |
|---|---|
group_buy_market_team_stock_key_{activityId}_{teamId} | 队伍锁单库存计数。 |
group_buy_market_team_stock_key_{activityId}_{teamId}_recovery | 失败恢复量。 |
refund_lock_{orderId} | 防止同一订单重复恢复库存。 |
tagId | Redis BitSet 人群标签。 |
notify_task 表用于记录通知任务。任务执行成功就标记成功,失败则重试,超过次数后标记失败。
这解决了一个常见问题:数据库事务成功了,但 HTTP/MQ 通知失败怎么办?
项目的做法是先把任务写到本地表,再异步执行,并用定时任务兜底补偿。
| Job | 作用 |
|---|---|
GroupBuyNotifyJob | 每天扫描未完成通知任务,重试回调。 |
TimeoutRefundJob | 每分钟扫描超时未支付订单,触发退款释放名额。 |
两个任务都使用 Redisson 分布式锁,避免多实例部署时重复执行。
如果你刚开始学,不建议一上来读所有代码。可以按下面顺序:
README.md 和本文,知道项目解决什么问题。pom.xml,理解 Maven 多模块结构。MarketIndexControllerMarketTradeControllerDCCControllerIndexGroupBuyMarketServiceImplDefaultActivityStrategyFactoryRootNode、SwitchNode、MarketNode、TagNode、EndNodeTradeLockOrderServiceTradeLockRuleFilterFactoryTradeRepository#lockMarketPayOrderTradeSettlementOrderServiceTradeSettlementRuleFilterFactoryTradeTaskServiceTradeRefundOrderServiceTradeRefundRuleFilterFactoryRefundSuccessTopicListenerActivityRepository、TradeRepository 和 MyBatis XML,理解数据如何落库。项目名称:拼团营销系统
项目描述:
基于 Spring Boot + MyBatis + Redis + RabbitMQ 实现的拼团营销服务,支持商品营销试算、拼团锁单、支付结算、退款、成团通知、超时补偿、人群标签和动态配置等功能。项目采用 DDD 多模块分层架构,将领域逻辑、接口触发、基础设施适配解耦。
职责描述可以这样写:
技术亮点可以这样提炼:
api、trigger、domain、infrastructure、types 多模块解耦。加入同一个拼团队伍时,如果所有请求都直接打数据库,会产生较高并发压力,也容易超卖队伍名额。项目先用 Redis 原子自增抢占名额,超过目标人数就回滚,数据库只处理抢占成功的请求。
项目会记录 recovery key,后续计算可用名额时把恢复量算进去,避免名额被永久占用。
成团或退款时,数据库事务和外部通知不是一个事务。先把通知任务写入本地表,再异步通知,可以保证业务数据已经落库,通知失败也能通过任务表重试。
责任链解决“多个规则按顺序校验”的问题,比如锁单前的活动状态、用户次数、库存占用。
策略模式解决“同一动作有多种实现”的问题,比如优惠计算有直减、满减、折扣,退款有多种订单状态处理。
主要在拼团锁单和加入队伍时的库存竞争。项目通过 Redis 原子计数、数据库条件更新、唯一索引、分布式锁和异步通知降低并发风险。
outTradeNo。知识笔记会随着实践和认知变化持续更新,不代表最终结论。