md_files/自学/拼团交易系统.md

943 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 拼团交易系统
## 系统设计
### **功能流程**
![image-20250619190759804](https://pic.bitday.top/i/2025/06/19/vjqcr7-0.png)
### **库表设计**
![image-20250624134726763](https://pic.bitday.top/i/2025/06/24/ma2pcj-0.png)
- 首先,站在**运营**的角度,要为这次拼团配置对应的**拼团活动**。那么就会涉及到;给哪个渠道的**什么商品**ID配置拼团这样用户在进入商品页就可以看到带有拼团商品的信息了。之后要考虑这个拼团的商**品所提供的规则信息**,包括:折扣、起止时间、人数等。还要拿到折扣的一个**试算金额**。这个试算出来的金额,就是告诉用户,通过拼团可以拿到的最低价格。
- 之后,站在**用户**的角度,是参与拼团。首次**发起一个拼团**或者**参与已存在的拼团**进行数据的记录,达成拼团约定拼团人数后,开始进行**通知**。这个通知的设计站在平台角度可以提供回调,那么任何的系统也就都可以接入了。
- 另外,为了支持拼团库表,需要先根据业务规则把符合条件的用户 ID 写入 Redis并为这批用户打上可配置的**人群标签**。创建拼团活动时,只需关联对应标签,即可让活动自动面向这部分用户生效,实现精准运营和差异化折扣。
- 那么,拼团活动表,为什么会把**折扣拆分**出来呢。因为这里的折扣**可能有多种**迭代到一个拼团上。比如给一个商品添加了直减10元的优惠又对符合的人群id的用户额外打9折这样就有了2个折扣迭代。所以拆分出来会更好维护。这是对常变的元素和稳定的元素进行设计的思考。
**(一)拼团配置表**
group_buy_activity 拼团活动
| 字段名 | 说明 |
| ---------------- | -------------------------------------------------------- |
| id | 自增ID |
| activity_id | 活动ID |
| source | 来源 |
| channel | 渠道 |
| goods_id | 商品ID |
| discount_id | 折扣ID |
| group_type | 成团方式【0自动成团到时间后自动成团、1达成目标成团】 |
| take_limit_count | 拼团次数限制 |
| target | 达成目标3人单、5人单 |
| valid_time | 拼单时长20分钟未完成拼团则=》自动成功or失败 |
| status | 活动状态 (活动是否有效,运营可临时设置为失效) |
| start_time | 活动开始时间 |
| end_time | 活动结束时间 |
| tag_id | 人群标签规则标识 |
| tag_scope | 人群标签规则范围【多选;可见、参与】 |
| create_time | 创建时间 |
| update_time | 更新时间 |
group_buy_discount 折扣配置
| 字段名 | 说明 |
| ------------- | --------------------------------- |
| id | 自增ID |
| discount_id | 折扣ID |
| discount_name | 折扣标题 |
| discount_desc | 折扣描述 |
| discount_type | 类型【base、tag】 |
| market_plan | 营销优惠计划【直减、满减、N元购】 |
| market_expr | 营销优惠表达式 |
| tag_id | 人群标签,特定优惠限定 |
| create_time | 创建时间 |
| update_time | 更新时间 |
crowd_tags 人群标签
| 字段名 | 说明 |
| ----------- | ----------------------------- |
| id | 自增ID |
| tag_id | 标签ID |
| tag_name | 标签名称 |
| tag_desc | 标签描述 |
| statistics | 人群标签统计量 200\10万\100万 |
| create_time | 创建时间 |
| update_time | 更新时间 |
crowd_tags_detail 人群标签明细(写入缓存)
| 字段名 | 说明 |
| ----------- | -------- |
| id | 自增ID |
| tag_id | 标签ID |
| user_id | 用户ID |
| create_time | 创建时间 |
| update_time | 更新时间 |
crowd_tags_job 人群标签任务
| 字段名 | 说明 |
| --------------- | ---------------------------- |
| id | 自增ID |
| tag_id | 标签ID |
| batch_id | 批次ID |
| tag_type | 标签类型【参与量、消费金额】 |
| tag_rule | 标签规则【限定参与N次】 |
| stat_start_time | 统计开始时间 |
| stat_end_time | 统计结束时间 |
| status | 计划、重置、完成 |
| create_time | 创建时间 |
| update_time | 更新时间 |
- 拼团活动表:设定了拼团的成团规则,人群标签的使用可以限定哪些人可见,哪些人可参与。
- 折扣配置表:拆分出拼团优惠到一个新的表进行多条配置。如果折扣还有更多的复杂规则,则可以配置新的折扣规则表进行处理。
- 人群标签表专门来做人群设计记录的这3张表就是为了把符合规则的人群ID也就是用户ID全部跑任务到一个记录下进行使用。比如黑玫瑰人群、高净值人群、拼团履约率90%以上的人群等。
**(二)参与拼团表**
**group_buy_account 拼团账户**
| 字段名 | 说明 |
| --------------------- | ------------ |
| id | 自增ID |
| user_id | 用户ID |
| activity_id | 活动ID |
| take_limit_count | 拼团次数限制 |
| take_limit_count_used | 拼团次数消耗 |
| create_time | 创建时间 |
| update_time | 更新时间 |
**group_buy_order 用户拼单**
一条记录 = 一个拼团**团队**`team_id` 唯一)
| 字段名 | 说明 |
| --------------- | -------------------------------- |
| id | 自增ID |
| team_id | 拼单组队ID |
| activity_id | 活动ID |
| source | 渠道 |
| channel | 来源 |
| original_price | 原始价格 |
| deduction_price | 折扣金额 |
| pay_price | 支付价格 |
| target_count | 目标数量 |
| complete_count | 完成数量 |
| status | 状态0-拼单中、1-完成、2-失败) |
| create_time | 创建时间 |
| update_time | 更新时间 |
**group_buy_order_list 用户拼单明细**
一条记录 = **某用户**在该团队里锁的一笔单
| 字段名 | 说明 |
| --------------- | ------------------------------------ |
| id | 自增ID |
| user_id | 用户ID |
| team_id | 拼单组队ID |
| order_id | 订单ID |
| activity_id | 活动ID |
| start_time | 活动开始时间 |
| end_time | 活动结束时间 |
| goods_id | 商品ID |
| source | 渠道 |
| channel | 来源 |
| original_price | 原始价格 |
| deduction_price | 折扣金额 |
| status | 状态0 初始锁定、1 消费完成 |
| out_trade_no | 外部交易单号(确保外部调用唯一幂等) |
| create_time | 创建时间 |
| update_time | 更新时间 |
**notify_task 回调任务**
| 字段名 | 说明 |
| -------------- | ---------------------------------- |
| id | 自增ID |
| activity_id | 活动ID |
| order_id | 拼单ID |
| notify_url | 回调接口 |
| notify_count | 回调次数3-5次 |
| notify_status | 回调状态【初始、完成、重试、失败】 |
| parameter_json | 参数对象 |
| create_time | 创建时间 |
| update_time | 更新时间 |
- 拼团账户表:记录用户的拼团参与数据,一个是为了限制用户的参与拼团次数,另外是为了人群标签任务统计数据。
- 用户拼单表当有用户发起首次拼单的时候产生拼单id并记录所需成团的拼单记录另外是写上拼团的状态、唯一索引、回调接口等。这样拼团完成就可以回调对接的平台通知完成了。【微信支付也是这样的设计回调支付结果这样的设计可以方便平台化对接】当再有用户参与后则写入用户拼单明细表。直至达成拼团。
- 回调任务表:当拼团完成后,要做回调处理。但可能会有失败,所以加入任务的方式进行补偿。如果仍然失败,则需要对接的平台,自己查询拼团结果。
### 架构设计
**MVC架构**
![image-20250624143253403](https://pic.bitday.top/i/2025/06/24/nou9d6-0.png)
**DDD架构**
![image-20250624143304200](https://pic.bitday.top/i/2025/06/24/nowdoo-0.png)
## 价格试算
```java
@Service
@RequiredArgsConstructor
public class IndexGroupBuyMarketServiceImpl implements IIndexGroupBuyMarketService {
private final DefaultActivityStrategyFactory defaultActivityStrategyFactory;
@Override
public TrialBalanceEntity indexMarketTrial(MarketProductEntity marketProductEntity) throws Exception {
StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> strategyHandler = defaultActivityStrategyFactory.strategyHandler();
TrialBalanceEntity trialBalanceEntity = strategyHandler.apply(marketProductEntity, new DefaultActivityStrategyFactory.DynamicContext());
return trialBalanceEntity;
}
}
```
```text
IndexGroupBuyMarketService
│ indexMarketTrial()
DefaultActivityStrategyFactory
│ (return rootNode)
RootNode.apply()
│ doApply() (执行)
│ router() 路由到下一node
SwitchNode.apply()
│ ...
... (可能还有其他节点)
EndNode.apply() → 组装结果并返回 TrialBalanceEntity
└────────── 最终一路向上 return
```
`IndexGroupBuyMarketService` 是领域服务,整个价格试算的入口
`DefaultActivityStrategyFactory` 帮你拿到 *根节点*,真正的“工厂”工作(多线程预处理、分支路由)都在各 Node 里完成。
`DynamicContext` 是一次性创建的共享上下文:谁需要谁就往里放
## 人群标签数据采集
| 步骤 | 目的 | 说明 |
| ------------------- | ----------------------------------------------- | ------------------------------------------------------------ |
| **1. 记录日志** | 标明本次批次任务的开始 | 方便后续排查、链路追踪 |
| **2. 读取批次配置** | 拿到该批次**统计范围、规则、时间窗**等 | 若返回 `null` 通常代表批次号错误或已被清理 |
| **3. 采集候选用户** | 从业务数仓/模型结果里拉取符合条件的用户 ID 列表 | 真实场景中会:• 调 REST / RPC 拿画像• 或扫离线结果表• 或读 Kafka 流 |
| **4. 双写标签明细** | 将每个用户与标签的关系永久化 & 提供实时校验能力 | 方法内部两件事:• 插入 `crowd_tags_detail` 表• <br />在 Redis **BitMap** 中把该用户对应位设为 1幂等处理冲突 |
| **5. 更新统计量** | 维护标签当前命中人数,用于运营看板 | 这里简单按“新增条数”累加,也可改为重新 `count(*)` 全量回填 |
| **6. 结束** | 方法返回 void | 如果过程抛异常,调度系统可重试/报警 |
> **一句话总结**
> 这是一个被定时器或消息触发的**离线批量打标签任务**
> 拉取任务规则 → (离线)筛出符合条件的用户 → 写库 + 写 Redis 位图 → 更新命中人数。
> 之后业务系统就能用位图做到毫秒级 `isUserInTag(userId, tagId)` 判断,实现精准运营投放。
### Bitmap位图
**概念**
- Bitmap 又称 Bitset是一种用位bit来表示状态的数据结构。
- 它把一个大的“布尔数组”压缩到最小空间:每个元素只占 1 位,要么 0False、要么 1True
**为什么用 Bitmap**
- **超高空间效率**1000 万个用户,只需要约 10 MB1000 万 / 8
- **超快操作**:检查某个索引位是否为 1、计数所有“1”的个数BITCOUNT、找出第一个“1”的位置BITPOS都是 O(1) 或者极快的位运算。
**典型场景**
- **用户标签 / 权限判断**:把符合某个条件的用户的索引位置设置为 1以后实时判断“用户 X 是否在标签 A 中?”就只需读一个 bit。
- **海量去重 / 布隆过滤器**在超大流量场景下判断“URL 是否已访问过”、“手机号是否已注册”等。
- **统计分析**快速统计某个条件下有多少个用户对象符合BITCOUNT
## 拼团交易锁单
![image-20250630124304410](https://pic.bitday.top/i/2025/06/30/kjsuy7-0.png)
下单到支付中间有一个流程,即锁单,比如淘宝京东中,在这个环节(限定时间内)选择使用优惠券、京豆等,可以得到优惠价,再进行支付;拼团场景同理,先加入拼团,进行锁单,然后优惠试算,最后才付款。
## 收获
### 实体对象
实体是指具有唯一标识的业务对象。
在 DDD 分层里,**Domain Entity ≠ 数据库 PO**。
`edu.whut.domain.*.model.entity` 包下放的是**纯粹的业务对象**,它们只表达业务语义(团队 ID、活动时间、优惠金额……对「数据持久化细节」保持**无感知**。因此它们看起来“字段不全”是正常的:
- 它们不会带 `@TableName` / `@TableId` 等 MyBatis-Plus 注解;
- 也不会出现数据库的技术字段(`id``create_time``update_time``status` 等);
- 只保留聚合根真正**需要的**业务属性与行为。
```java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PayActivityEntity {
/** 拼单组队ID */
private String teamId;
/** 活动ID */
private Long activityId;
/** 活动名称 */
private String activityName;
/** 拼团开始时间 */
private Date startTime;
/** 拼团结束时间 */
private Date endTime;
/** 目标数量 */
private Integer targetCount;
}
```
这个也是实体对象因为多个字段的组合teamId和activityId能唯一标识这个实体。
### 模板方法
**核心思想**
在抽象父类中定义**算法骨架**(固定执行顺序),把某些可变步骤留给子类重写;调用方只用模板方法,保证流程一致。
```text
Client ───▶ AbstractClass
├─ templateMethod() ←—— 固定流程
│ step1()
│ step2() ←—— 抽象,可变
│ step3()
└─ hookMethod() ←—— 可选覆盖
│ extends
┌──────────┴──────────┐
│ ConcreteClassA/B… │
```
**示例:**
```java
// 1. 抽象模板
public abstract class AbstractDialog {
// 模板方法:固定调用顺序,设为 final 防止子类改流程
public final void show() {
initLayout();
bindEvent();
beforeDisplay(); // 钩子,可选
display();
afterDisplay(); // 钩子,可选
}
// 具体公共步骤
private void initLayout() {
System.out.println("加载通用布局文件");
}
// 需要子类实现的抽象步骤
protected abstract void bindEvent();
// 钩子方法,默认空实现
protected void beforeDisplay() {}
protected void afterDisplay() {}
private void display() {
System.out.println("弹出对话框");
}
}
// 2. 子类:登录对话框
public class LoginDialog extends AbstractDialog {
@Override
protected void bindEvent() {
System.out.println("绑定登录按钮事件");
}
@Override
protected void afterDisplay() {
System.out.println("focus 到用户名输入框");
}
}
// 3. 调用
public class Demo {
public static void main(String[] args) {
AbstractDialog dialog = new LoginDialog();
dialog.show();
/* 输出:
加载通用布局文件
绑定登录按钮事件
弹出对话框
focus 到用户名输入框
*/
}
}
```
**要点**
- **复用公共流程**`initLayout()``display()` 写一次即可。
- **限制流程顺序**`show()` 定为 `final`,防止子类乱改步骤。
- **钩子方法**:子类可选择性覆盖(如 `beforeDisplay`)。
### 责任链
应用场景:日志系统、审批流程、权限校验——任何需要将请求按阶段传递、并由某一环节决定是否继续或终止处理的地方,都非常适合职责链模式。
#### 单例链
典型的责任链模式要点:
- **解耦请求发送者和处理者**:调用者只持有链头,不关心中间环节。
- **动态组装**:通过 `appendNext` 可以灵活地增加、删除或重排链上的节点。
- **可扩展**:新增处理逻辑只需继承 `AbstractLogicLink` 并实现 `apply`,不用改动已有代码。
接口定义:`ILogicChainArmory<T, D, R>` 提供添加节点方法和获取节点
```java
//定义了责任链的组装接口:
public interface ILogicChainArmory<T, D, R> {
ILogicLink<T, D, R> next(); //在当前节点中获取下一个节点
ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next); //把下一个处理节点挂接上来
}
```
`ILogicLink<T, D, R>` 继承自 `ILogicChainArmory<T, D, R>`,并额外声明了核心方法 `apply`
```java
public interface ILogicLink<T, D, R> extends ILogicChainArmory<T, D, R> {
R apply(T requestParameter, D dynamicContext) throws Exception; //处理请求
}
```
抽象基类:`AbstractLogicLink`
```java
public abstract class AbstractLogicLink<T, D, R> implements ILogicLink<T, D, R> {
private ILogicLink<T, D, R> next;
@Override
public ILogicLink<T, D, R> next() {
return next;
}
@Override
public ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next) {
this.next = next;
return next;
}
protected R next(T requestParameter, D dynamicContext) throws Exception {
return next.apply(requestParameter, dynamicContext); //交给下一节点处理
}
}
```
子类只需继承它,重写 `apply(...)`,在合适的条件下要么直接处理并返回,要么调用 `next(requestParameter, dynamicContext)` 继续传递。
**使用示例:**
```java
public class AuthLink extends AbstractLogicLink<Request, Context, Response> {
@Override
public Response apply(Request req, Context ctx) throws Exception {
if (!ctx.isAuthenticated()) {
throw new UnauthorizedException();
}
// 认证通过,继续下一个环节
return next(req, ctx);
}
}
public class LoggingLink extends AbstractLogicLink<Request, Context, Response> {
@Override
public Response apply(Request req, Context ctx) throws Exception {
System.out.println("Request received: " + req);
Response resp = next(req, ctx);
System.out.println("Response sent: " + resp);
return resp;
}
}
// 组装责任链 放工厂类factory中实现
ILogicLink<Request, Context, Response> chain =
new AuthLink()
.appendNext(new LoggingLink())
.appendNext(new BusinessLogicLink());
//客户端使用
Request req = new Request(...);
Context ctx = new Context(...);
Response resp = chain.apply(req, ctx);
```
示例图:
```text
AuthLink.apply
└─▶ LoggingLink.apply
└─▶ BusinessLogicLink.apply
└─▶ 返回 Response
```
这种模式链上的每个节点都手动 `next()`到下一节点。
#### 多例链
```java
/**
* 通用逻辑处理器接口 —— 责任链中的「节点」要实现的核心契约。
*/
public interface ILogicHandler<T, D, R> {
/**
* 默认的 next占位实现方便节点若不需要向后传递时直接返回 null。
*/
default R next(T requestParameter, D dynamicContext) {
return null;
}
/**
* 节点的核心处理方法。
*/
R apply(T requestParameter, D dynamicContext) throws Exception;
}
```
```java
/**
* 业务链路容器 —— 双向链表实现,同时实现 ILogicHandler从而可以被当作单个节点使用。
*/
public class BusinessLinkedList<T, D, R> extends LinkedList<ILogicHandler<T, D, R>> implements ILogicHandler<T, D, R>{
public BusinessLinkedList(String name) {
super(name);
}
/**
* BusinessLinkedList是头节点它的apply方法就是循环调用后面的节点直至返回。
* 遍历并执行链路。
*/
@Override
public R apply(T requestParameter, D dynamicContext) throws Exception {
Node<ILogicHandler<T, D, R>> current = this.first;
// 顺序执行,直到链尾或返回结果
while (current != null) {
ILogicHandler<T, D, R> handler = current.item;
R result = handler.apply(requestParameter, dynamicContext);
if (result != null) {
// 节点命中,立即返回
return result;
}
//result==null则交给那一节点继续处理
current = current.next;
}
// 全链未命中
return null;
}
}
```
```java
/**
* 链路装配工厂 —— 负责把一组 ILogicHandler 顺序注册到 BusinessLinkedList 中。
*/
public class LinkArmory<T, D, R> {
private final BusinessLinkedList<T, D, R> logicLink;
/**
* @param linkName 链路名称,便于日志排查
* @param logicHandlers 节点列表,按传入顺序链接
*/
@SafeVarargs
public LinkArmory(String linkName, ILogicHandler<T, D, R>... logicHandlers) {
logicLink = new BusinessLinkedList<>(linkName);
for (ILogicHandler<T, D, R> logicHandler: logicHandlers){
logicLink.add(logicHandler);
}
}
/** 返回组装完成的链路 */
public BusinessLinkedList<T, D, R> getLogicLink() {
return logicLink;
}
}
//工厂类可以定义多条责任链每条有自己的Bean名称区分。
@Bean("tradeRuleFilter")
public BusinessLinkedList<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> tradeRuleFilter(ActivityUsabilityRuleFilter activityUsabilityRuleFilter, UserTakeLimitRuleFilter userTakeLimitRuleFilter) {
// 1. 组装链
LinkArmory<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> linkArmory =
new LinkArmory<>("交易规则过滤链", activityUsabilityRuleFilter, userTakeLimitRuleFilter);
// 2. 返回链容器(即可作为责任链使用)
return linkArmory.getLogicLink();
}
```
示例图:
```text
BusinessLinkedList.apply ←─ 只有这一层在栈里
while 循环:
├─▶ 调用 ActivityUsability.apply → 返回 null → 继续
├─▶ 调用 UserTakeLimit.apply → 返回 null → 继续
└─▶ 调用 ... → 返回 Result → break
```
链头拿着“游标”一个个跑,节点只告诉“命中 / 未命中”。
### 规则树流程
![8d9ca81791b613eb7ff06b096a3d7c4a](https://pic.bitday.top/i/2025/06/24/pgcnns-0.png)
**整体分层思路**
| 分层 | 作用 | 关键对象 |
| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| **通用模板层** | 抽象出与具体业务无关的「规则树」骨架,解决 *如何找到并执行策略* 的共性问题 | `StrategyMapper``StrategyHandler``AbstractStrategyRouter<T,D,R>` |
| **业务装配层** | 基于模板,自由拼装出 *一棵* 贴合业务流程的策略树 | `RootNode / SwitchRoot / MarketNode / EndNode …` |
| **对外暴露层** | 通过 **工厂 + 服务支持类** 将整棵树封装成一个可直接调用的 `StrategyHandler`,并交给 Spring 整体托管 | `DefaultActivityStrategyFactory``AbstractGroupBuyMarketSupport` |
**通用模板层:规则树的“骨架”**
| 角色 | 职责 | 关系 |
| ------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `StrategyMapper` | **映射器**:依据 `requestParameter + dynamicContext` 选出 *下一个* 策略节点 | 被 `AbstractStrategyRouter` 调用 |
| `StrategyHandler` | **处理器**:真正执行业务逻辑;`apply` 结束后可返回结果或继续路由 | 节点本身 / 路由器本身都是它的实现 |
| `AbstractStrategyRouter<T,D,R>` | **路由模板**:① 调用 `get(...)` 找到合适的 `StrategyHandler`;② 调用该 handler 的 `apply(...)`;③ 若未命中则走 `defaultStrategyHandler` | 同时实现 `StrategyMapper``StrategyHandler`,但自身保持 *抽象*,把细节延迟到子类 |
**业务装配层:一棵可编排的策略树**
```text
RootNode -> SwitchRoot -> MarketNode -> EndNode
↘︎ OtherNode ...
```
- 每个节点
继承 `AbstractStrategyRouter`
- 实现 `get(...)`:决定当前节点的下一跳是哪一个节点
- 实现 `apply(...)`:实现节点自身应做的业务动作(或继续下钻)
- 组合方式
比责任链更灵活:
- 一个节点既可以“继续路由”也可以“自己处理完直接返回”
- 可以随时插拔 / 替换子节点,形成多分支、循环、早停等复杂流转
**对外暴露层:工厂 + 服务支持类**
| 组件 | 主要职责 |
| --------------------------------------------- | ------------------------------------------------------------ |
| `DefaultActivityStrategyFactory` (`@Service`) | **工厂**1. 在 Spring 启动时注入根节点 `RootNode`2. 暴露**统一入口** `strategyHandler()` → 返回整个策略树顶点(一个 `StrategyHandler` 实例) |
| `AbstractGroupBuyMarketSupport` | **业务服务基类**:封装拼团场景下共用的查询、工具方法;供每个**节点**继承使用 |
这样,调用方只需
```java
TrialBalanceEntity result =
factory.strategyHandler().apply(product, new DynamicContext(vo1, vo2));
```
就能驱动整棵策略树,而**完全不用关心**节点搭建、依赖注入等细节。
### 策略模式
**核心思想**
把可互换的算法/行为抽成独立策略类,运行时由“上下文”对象选择合适的策略;对调用方来说,只关心统一接口,而非具体实现。
```text
┌───────────────┐
│ Client │
└─────▲─────────┘
│ has-a
┌─────┴─────────┐ implements
│ Context │────────────┐ ┌──────────────┐
│ (使用者) │ strategy └─▶│ Strategy A │
└───────────────┘ ├──────────────┤
│ Strategy B │
└──────────────┘
```
#### 集合自动注入
常见于策略/工厂/插件场景。
```java
@Autowired
private Map<String, IDiscountCalculateService> discountCalculateServiceMap;
```
**字段类型**`Map<String, IDiscountCalculateService>`
- key—— Bean 的名字
- 默认是类名首字母小写 (`mjCalculateService`)
- 或者你在实现类上显式写的 `@Service("MJ")`
- **value** —— 那个实现类对应的实例
- **Spring 机制**
1. 启动时扫描所有实现 `IDiscountCalculateService` 的 Bean。
2. 把它们按 “BeanName → Bean 实例” 的映射注入到这张 `Map` 里。
3. 你一次性就拿到了“策略字典”。
**示例:**
```java
@Service("MJ") // ★ 关键Bean 名即策略键
public class MJCalculateService extends IDiscountCalculateService {
@Override
protected BigDecimal Calculate(String userId, BigDecimal originalPrice,
GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount) {
//忽略实现细节
}
@Component
@RequiredArgsConstructor // 构造器注入更推荐
public class DiscountContext {
private final Map<String, IDiscountCalculateService> discountServiceMap;
public BigDecimal calc(String strategyKey,
String userId,
BigDecimal originalPrice,
GroupBuyActivityDiscountVO.GroupBuyDiscount plan) {
//strategyKey可以是"MJ" ..
IDiscountCalculateService strategy = discountServiceMap.get(strategyKey);
if (strategy == null) {
throw new IllegalArgumentException("无匹配折扣类型: " + strategyKey);
}
return strategy.calculate(userId, originalPrice, plan);
}
}
```
### 多线程异步调用
如果某任务比较耗时(如加载大量数据),可以考虑开多线程异步调用。
```java
// Runnable ➞ 只能 run(),没有返回值
public interface Runnable {
void run();
}
// Callable<V> ➞ call() 能返回 V也能抛检查型异常
public interface Callable<V> {
V call() throws Exception;
}
```
```java
public class MyTask implements Callable<String> {
private final String name;
public MyTask(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
// 模拟耗时操作
TimeUnit.MILLISECONDS.sleep(300);
return "任务[" + name + "]的执行结果";
}
}
```
```java
public class SimpleAsyncDemo {
public static void main(String[] args) {
// 创建大小为 2 的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
try {
// 构造两个任务
MyTask task1 = new MyTask("A");
MyTask task2 = new MyTask("B");
// 用 FutureTask 包装 Callable
FutureTask<String> future1 = new FutureTask<>(task1);
FutureTask<String> future2 = new FutureTask<>(task2);
// 提交给线程池异步执行
pool.execute(future1);
pool.execute(future2);
// 主线程可以先做别的事…
System.out.println("主线程正在做其他事情…");
// 在需要的时候再获取结果(可加超时)
String result1 = future1.get(1, TimeUnit.SECONDS); //设置超时时间1秒
String result2 = future2.get(); //无超时时间
System.out.println("拿到结果1 → " + result1);
System.out.println("拿到结果2 → " + result2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.err.println("任务执行中出错: " + e.getCause());
} catch (TimeoutException e) {
System.err.println("等待结果超时");
} finally {
pool.shutdown();
}
}
}
```
### 动态配置(热更新)
**注解标记**
`@DCCValue("key:default")` 标注需要动态注入的字段,指定对应的 Redis Key带前缀及默认值。
```java
// 标记要动态注入的字段
@Retention(RUNTIME) @Target(FIELD)
public @interface DCCValue {
String value(); // "key:default"
}
```
```java
// 业务使用示例
@Service
public class MyFeature {
@DCCValue("myFlag:0")
private String myFlag;
public boolean enabled() { return "1".equals(myFlag); }
}
```
**启动时注入**
实现一个 `BeanPostProcessor`,在每个 Spring Bean 初始化后:
- 扫描带 `@DCCValue` 的字段;
- 拼出完整 Redis Key`dcc_prefix_key`),若不存在则写入默认值,否则读最新值;
- 反射把值注入到该 Bean 的私有字段;
-`(redisKey → Bean 实例)` 记录到内存映射,用于后续热更新。
```java
@Override
public Object postProcessAfterInitialization(Object bean, String name) {
Class<?> cls = AopUtils.isAopProxy(bean)
? AopUtils.getTargetClass(bean)
: bean.getClass();
for (Field f : cls.getDeclaredFields()) {
DCCValue dv = f.getAnnotation(DCCValue.class);
if (dv==null) continue;
String[] p = dv.value().split(":");
String key = PREFIX + p[0], defaultValue = p[1];
RBucket<String> bucket = redis.getBucket(key);
String val = bucket.isExists() ? bucket.get() : defaultValue;
bucket.trySet(defaultValue); //同步redis内容
injectField(bean, f, val); //反射注入
beans.put(key, bean);
}
return bean;
}
```
**运行时热更新**
- 在同一个组件里,订阅一个 Redis Topic频道比如 `"dcc_update"`
- 外部调用发布接口 `PUBLISH dcc_update "key,newValue"`
```java
//更新配置
@GetMapping("/dcc/update")
public void update(@RequestParam String key, @RequestParam String value) {
dccTopic().publish(key + "," + value);
}
```
- 订阅者收到后:
1. 同步把新值写回 Redis
2. 从映射里取出对应 Bean反射更新它的字段。
```java
// 发布/订阅频道
@Bean
public RTopic dccTopic() {
RTopic t = redis.getTopic("dcc_update");
t.addListener(String.class, (c,msg)->{
String[] a = msg.split(",");
String key = PREFIX + a[0], val = a[1];
RBucket<String> bucket = redis.getBucket(key);
if (!bucket.isExists()) return;
bucket.set(val);
Object bean = beans.get(key);
if (bean!=null) injectField(bean, a[0], val);
});
return t;
}
```