ai-engineering-init 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/leniu-java-export/SKILL.md +389 -95
- package/.codex/skills/leniu-java-export/SKILL.md +389 -95
- package/.cursor/skills/bug-detective/SKILL.md +19 -19
- package/.cursor/skills/leniu-java-export/SKILL.md +389 -95
- package/.cursor/skills/project-navigator/SKILL.md +164 -258
- package/package.json +7 -1
- package/scripts/build-skills.js +180 -0
- package/src/platform-map.json +56 -0
- package/src/skills/add-skill/SKILL.md +488 -0
- package/src/skills/add-todo/SKILL.md +269 -0
- package/src/skills/api-development/SKILL.md +266 -0
- package/src/skills/architecture-design/SKILL.md +262 -0
- package/src/skills/backend-annotations/SKILL.md +302 -0
- package/src/skills/banana-image/CHANGELOG.md +37 -0
- package/src/skills/banana-image/README.md +146 -0
- package/src/skills/banana-image/SKILL.md +171 -0
- package/src/skills/banana-image/assets/logo.png +0 -0
- package/src/skills/banana-image/references/advanced-usage.md +189 -0
- package/src/skills/banana-image/scripts/apply_template.py +125 -0
- package/src/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/src/skills/banana-image/scripts/batch_prep.py +82 -0
- package/src/skills/banana-image/scripts/package-lock.json +1437 -0
- package/src/skills/banana-image/scripts/package.json +18 -0
- package/src/skills/banana-image/scripts/requirements.txt +10 -0
- package/src/skills/banana-image/templates/poster.json +22 -0
- package/src/skills/banana-image/templates/product.json +17 -0
- package/src/skills/banana-image/templates/social.json +22 -0
- package/src/skills/banana-image/templates/thumbnail.json +17 -0
- package/src/skills/brainstorm/SKILL.md +216 -0
- package/src/skills/bug-detective/SKILL.md +256 -0
- package/src/skills/bug-detective/references/error-patterns.md +242 -0
- package/src/skills/check/SKILL.md +367 -0
- package/src/skills/code-patterns/SKILL.md +280 -0
- package/src/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/src/skills/codex-code-review/SKILL.md +135 -0
- package/src/skills/collaborating-with-codex/SKILL.md +174 -0
- package/src/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/src/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/src/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/src/skills/crud/SKILL.md +265 -0
- package/src/skills/crud-development/SKILL.md +409 -0
- package/src/skills/data-permission/SKILL.md +292 -0
- package/src/skills/data-permission/references/custom-data-scope.md +90 -0
- package/src/skills/database-ops/SKILL.md +407 -0
- package/src/skills/dev/SKILL.md +187 -0
- package/src/skills/error-handler/SKILL.md +371 -0
- package/src/skills/file-oss-management/SKILL.md +255 -0
- package/src/skills/file-oss-management/references/entities.md +105 -0
- package/src/skills/file-oss-management/references/service-impl.md +104 -0
- package/src/skills/git-workflow/SKILL.md +397 -0
- package/src/skills/init-docs/SKILL.md +194 -0
- package/src/skills/json-serialization/SKILL.md +357 -0
- package/src/skills/leniu-api-development/SKILL.md +319 -0
- package/src/skills/leniu-api-development/references/real-examples.md +273 -0
- package/src/skills/leniu-architecture-design/SKILL.md +383 -0
- package/src/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/src/skills/leniu-brainstorm/SKILL.md +242 -0
- package/src/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/src/skills/leniu-code-patterns/SKILL.md +411 -0
- package/src/skills/leniu-crud-development/SKILL.md +404 -0
- package/src/skills/leniu-crud-development/references/templates.md +597 -0
- package/src/skills/leniu-customization-location/SKILL.md +410 -0
- package/src/skills/leniu-data-permission/SKILL.md +341 -0
- package/src/skills/leniu-database-ops/SKILL.md +426 -0
- package/src/skills/leniu-error-handler/SKILL.md +462 -0
- package/src/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/src/skills/leniu-java-code-style/SKILL.md +510 -0
- package/src/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/src/skills/leniu-java-entity/SKILL.md +237 -0
- package/src/skills/leniu-java-entity/references/templates.md +237 -0
- package/src/skills/leniu-java-export/SKILL.md +570 -0
- package/src/skills/leniu-java-logging/SKILL.md +229 -0
- package/src/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/src/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/src/skills/leniu-java-mq/SKILL.md +338 -0
- package/src/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/src/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/src/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/src/skills/leniu-java-task/SKILL.md +367 -0
- package/src/skills/leniu-java-total-line/SKILL.md +196 -0
- package/src/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/src/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/src/skills/leniu-mealtime/SKILL.md +215 -0
- package/src/skills/leniu-redis-cache/SKILL.md +331 -0
- package/src/skills/leniu-report-customization/SKILL.md +335 -0
- package/src/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/src/skills/leniu-report-standard-customization/SKILL.md +328 -0
- package/src/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/src/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/src/skills/leniu-security-guard/SKILL.md +306 -0
- package/src/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/src/skills/mysql-debug/SKILL.md +364 -0
- package/src/skills/next/SKILL.md +137 -0
- package/src/skills/openspec-apply-change/SKILL.md +165 -0
- package/src/skills/openspec-archive-change/SKILL.md +122 -0
- package/src/skills/openspec-bulk-archive-change/SKILL.md +254 -0
- package/src/skills/openspec-continue-change/SKILL.md +126 -0
- package/src/skills/openspec-explore/SKILL.md +299 -0
- package/src/skills/openspec-ff-change/SKILL.md +109 -0
- package/src/skills/openspec-new-change/SKILL.md +82 -0
- package/src/skills/openspec-onboard/SKILL.md +414 -0
- package/src/skills/openspec-sync-specs/SKILL.md +146 -0
- package/src/skills/openspec-verify-change/SKILL.md +176 -0
- package/src/skills/performance-doctor/SKILL.md +303 -0
- package/src/skills/progress/SKILL.md +193 -0
- package/src/skills/project-navigator/SKILL.md +211 -0
- package/src/skills/redis-cache/SKILL.md +333 -0
- package/src/skills/redis-cache/references/listeners.md +23 -0
- package/src/skills/scheduled-jobs/SKILL.md +314 -0
- package/src/skills/security-guard/SKILL.md +353 -0
- package/src/skills/security-guard/references/encrypt-config.md +103 -0
- package/src/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/src/skills/sms-mail/SKILL.md +308 -0
- package/src/skills/sms-mail/references/mail-config.md +88 -0
- package/src/skills/sms-mail/references/sms-config.md +74 -0
- package/src/skills/social-login/SKILL.md +266 -0
- package/src/skills/social-login/references/provider-configs.md +118 -0
- package/src/skills/start/SKILL.md +154 -0
- package/src/skills/store-pc/SKILL.md +366 -0
- package/src/skills/sync/SKILL.md +149 -0
- package/src/skills/task-tracker/SKILL.md +307 -0
- package/src/skills/tech-decision/SKILL.md +393 -0
- package/src/skills/tenant-management/SKILL.md +288 -0
- package/src/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/src/skills/test-development/SKILL.md +301 -0
- package/src/skills/test-development/references/parameterized-examples.md +119 -0
- package/src/skills/ui-pc/SKILL.md +438 -0
- package/src/skills/update-status/SKILL.md +159 -0
- package/src/skills/utils-toolkit/SKILL.md +362 -0
- package/src/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/src/skills/websocket-sse/SKILL.md +271 -0
- package/src/skills/workflow-engine/SKILL.md +321 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: leniu-java-mq
|
|
3
|
+
description: |
|
|
4
|
+
leniu-tengyun-core / leniu-yunshitang 项目消息队列规范。当使用消息队列(MQ)时使用此skill,包括消息发送、消费和事务消息规范。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 使用消息队列发送/消费消息
|
|
8
|
+
- 实现延迟消息(延迟队列)
|
|
9
|
+
- 消息消费失败重试处理
|
|
10
|
+
- 事务消息(保证消息和数据库事务一致性)
|
|
11
|
+
|
|
12
|
+
适用项目:
|
|
13
|
+
- leniu-tengyun-core:/Users/xujiajun/Developer/gongsi_proj/leniu-api/leniu-tengyun-core
|
|
14
|
+
- leniu-yunshitang:/Users/xujiajun/Developer/gongsi_proj/leniu-api/leniu-tengyun/leniu-yunshitang
|
|
15
|
+
|
|
16
|
+
触发词:消息队列、MQ、MqUtil、@MqConsumer、延迟消息、消息重试、事务消息
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# leniu-tengyun-core 消息队列规范
|
|
20
|
+
|
|
21
|
+
## 项目特征
|
|
22
|
+
|
|
23
|
+
| 特征 | 说明 |
|
|
24
|
+
|------|------|
|
|
25
|
+
| **包名前缀** | `net.xnzn.core.*` |
|
|
26
|
+
| **JDK 版本** | 21 |
|
|
27
|
+
| **消息工具** | `MqUtil` |
|
|
28
|
+
| **消费者注解** | `@MQMessageListener(group, topic, tag)` |
|
|
29
|
+
| **消费者接口** | `implements MQListener<MqPayload<String>>` |
|
|
30
|
+
| **主题常量** | `LeMqConstant.Topic` |
|
|
31
|
+
| **延迟枚举** | `LeMqConstant.DelayDuration` |
|
|
32
|
+
| **JSON工具** | `JacksonUtil` |
|
|
33
|
+
| **异常类** | `LeException` |
|
|
34
|
+
|
|
35
|
+
## 核心架构:三层分工
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
MsgSend(静态工具类)→ Listener(接收消息)→ Handler(分发处理)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| 层 | 职责 | 示例 |
|
|
42
|
+
|----|------|------|
|
|
43
|
+
| **XxxMessageSend** | 静态工具类,封装消息发送逻辑 | `OrderMessageSend` |
|
|
44
|
+
| **XxxMqListenerYyy** | 消费者,接收 MQ 消息并分发到 Handler | `OrderMqListenerAsyncSave` |
|
|
45
|
+
| **XxxMqHandler** | 业务处理,统一处理各类消息 | `OrderMqHandler` |
|
|
46
|
+
|
|
47
|
+
## 消息发送
|
|
48
|
+
|
|
49
|
+
### 三种发送方式
|
|
50
|
+
|
|
51
|
+
```java
|
|
52
|
+
import net.xnzn.core.common.mq.MqUtil;
|
|
53
|
+
import net.xnzn.core.common.constant.LeMqConstant;
|
|
54
|
+
import net.xnzn.core.common.utils.JacksonUtil;
|
|
55
|
+
|
|
56
|
+
// 1. 普通消息(立即发送)
|
|
57
|
+
MqUtil.send(JacksonUtil.writeValueAsString(po), LeMqConstant.Topic.XXX_TOPIC);
|
|
58
|
+
|
|
59
|
+
// 2. 事务消息(在 DB 事务提交后发送,保证一致性)
|
|
60
|
+
MqUtil.sendByTxEnd(JacksonUtil.writeValueAsString(po), LeMqConstant.Topic.XXX_TOPIC);
|
|
61
|
+
|
|
62
|
+
// 3. 延迟消息(指定延迟时长后触发)
|
|
63
|
+
MqUtil.sendDelay(JacksonUtil.writeValueAsString(po), LeMqConstant.Topic.XXX_TOPIC, LeMqConstant.DelayDuration.ONE_MINUTE);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**关键点**:
|
|
67
|
+
- 消息体必须用 `JacksonUtil.writeValueAsString()` 序列化为 **String** 再发送
|
|
68
|
+
- 事务消息用 `sendByTxEnd()`(如 DB 事务回滚则不发送)
|
|
69
|
+
- 延迟时间用 `LeMqConstant.DelayDuration` 枚举(不用 `Duration.ofMinutes()`)
|
|
70
|
+
|
|
71
|
+
### DelayDuration 枚举(常用值)
|
|
72
|
+
|
|
73
|
+
```java
|
|
74
|
+
LeMqConstant.DelayDuration.ONE_MINUTE // 1分钟
|
|
75
|
+
LeMqConstant.DelayDuration.THIRTY_MINUTES // 30分钟
|
|
76
|
+
// 其他枚举值参见 LeMqConstant.DelayDuration
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 消息发送类(静态工具类模式)
|
|
80
|
+
|
|
81
|
+
```java
|
|
82
|
+
/**
|
|
83
|
+
* 订单消息发送
|
|
84
|
+
* 关键特征:
|
|
85
|
+
* 1. 不是 @Component,是纯静态工具类
|
|
86
|
+
* 2. 私有构造器
|
|
87
|
+
* 3. PO 中包含 traceId 和 tenantId(用于跨线程追踪)
|
|
88
|
+
*/
|
|
89
|
+
@Slf4j
|
|
90
|
+
public class XxxMessageSend {
|
|
91
|
+
|
|
92
|
+
private XxxMessageSend() {} // 禁止实例化
|
|
93
|
+
|
|
94
|
+
private static final String MQ_ERROR_LOG = "发送MQ消息失败";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 普通消息(适用于非事务性发送)
|
|
98
|
+
*/
|
|
99
|
+
public static void sendXxxEvent(XxxPO po) {
|
|
100
|
+
log.info("[XxxMQ]发送xxx事件");
|
|
101
|
+
po.setTraceId(LogUtil.getCurrentTraceId());
|
|
102
|
+
po.setTenantId(TenantContextHolder.getTenantId());
|
|
103
|
+
MqUtil.send(JacksonUtil.writeValueAsString(po), LeMqConstant.Topic.XXX_TOPIC);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 事务消息(在 @Transactional 方法中使用,事务提交后才发送)
|
|
108
|
+
*/
|
|
109
|
+
public static void sendXxxEventByTx(XxxPO po) {
|
|
110
|
+
log.info("[XxxMQ]发送xxx事务消息");
|
|
111
|
+
po.setTraceId(LogUtil.getCurrentTraceId());
|
|
112
|
+
po.setTenantId(TenantContextHolder.getTenantId());
|
|
113
|
+
try {
|
|
114
|
+
MqUtil.sendByTxEnd(JacksonUtil.writeValueAsString(po), LeMqConstant.Topic.XXX_TOPIC);
|
|
115
|
+
} catch (Exception e) {
|
|
116
|
+
log.error(MQ_ERROR_LOG, e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 延迟消息(超时取消等场景)
|
|
122
|
+
*/
|
|
123
|
+
public static void sendXxxDelay(XxxPO po, LeMqConstant.DelayDuration delayDuration) {
|
|
124
|
+
log.info("[XxxMQ]发送xxx延迟消息");
|
|
125
|
+
po.setTraceId(LogUtil.getCurrentTraceId());
|
|
126
|
+
po.setTenantId(TenantContextHolder.getTenantId());
|
|
127
|
+
MqUtil.sendDelay(JacksonUtil.writeValueAsString(po), LeMqConstant.Topic.XXX_TOPIC, delayDuration);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### PO 消息体规范
|
|
133
|
+
|
|
134
|
+
```java
|
|
135
|
+
import lombok.Data;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* MQ 消息 PO(Message Payload Object)
|
|
139
|
+
* 必须包含 traceId 和 tenantId 字段
|
|
140
|
+
*/
|
|
141
|
+
@Data
|
|
142
|
+
public class XxxPO {
|
|
143
|
+
|
|
144
|
+
/** 链路追踪ID */
|
|
145
|
+
private String traceId;
|
|
146
|
+
|
|
147
|
+
/** 租户ID */
|
|
148
|
+
private String tenantId;
|
|
149
|
+
|
|
150
|
+
/** 业务字段 */
|
|
151
|
+
private Long orderId;
|
|
152
|
+
private String outTradeNo;
|
|
153
|
+
// 其他字段...
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 消息消费
|
|
158
|
+
|
|
159
|
+
### Listener 类(真实代码模式)
|
|
160
|
+
|
|
161
|
+
```java
|
|
162
|
+
import lombok.extern.slf4j.Slf4j;
|
|
163
|
+
import net.xnzn.core.common.mq.MqPayload;
|
|
164
|
+
import net.xnzn.framework.mq.MQListener;
|
|
165
|
+
import net.xnzn.framework.mq.MQMessageListener;
|
|
166
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
167
|
+
import org.springframework.context.annotation.Lazy;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* MQ 消费者 Listener
|
|
171
|
+
* @see LeMqConstant.Topic#XXX_TOPIC
|
|
172
|
+
*/
|
|
173
|
+
@Slf4j
|
|
174
|
+
@MQMessageListener(
|
|
175
|
+
group = "module-xxx-topic-name", // 消费组,格式:模块名-topic-tag
|
|
176
|
+
topic = "xxx", // Topic 名称
|
|
177
|
+
tag = "xxx-topic-name" // Tag 名称(对应 LeMqConstant.Topic)
|
|
178
|
+
)
|
|
179
|
+
public class XxxMqListenerYyy implements MQListener<MqPayload<String>> {
|
|
180
|
+
|
|
181
|
+
@Autowired
|
|
182
|
+
@Lazy // ⚠️ 必须 @Lazy,避免循环依赖
|
|
183
|
+
private XxxMqHandler xxxMqHandler;
|
|
184
|
+
|
|
185
|
+
@Override
|
|
186
|
+
public void onMessage(MqPayload<String> payload) {
|
|
187
|
+
// 委托给 Handler 处理,使用方法引用
|
|
188
|
+
xxxMqHandler.handleMessage(payload, XxxPO.class, XxxMqHandler::handleXxx);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Handler 类(统一处理消息)
|
|
194
|
+
|
|
195
|
+
```java
|
|
196
|
+
import cn.hutool.core.text.CharSequenceUtil;
|
|
197
|
+
import com.pig4cloud.pigx.common.core.exception.LeException;
|
|
198
|
+
import lombok.extern.slf4j.Slf4j;
|
|
199
|
+
import net.xnzn.core.common.export.util.I18nUtil;
|
|
200
|
+
import net.xnzn.core.common.mq.MqPayload;
|
|
201
|
+
import net.xnzn.core.common.utils.JacksonUtil;
|
|
202
|
+
import net.xnzn.framework.data.tenant.TenantContextHolder;
|
|
203
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
204
|
+
import org.springframework.context.annotation.Lazy;
|
|
205
|
+
import org.springframework.stereotype.Service;
|
|
206
|
+
|
|
207
|
+
import java.util.function.BiConsumer;
|
|
208
|
+
|
|
209
|
+
@Slf4j
|
|
210
|
+
@Service
|
|
211
|
+
public class XxxMqHandler {
|
|
212
|
+
|
|
213
|
+
@Lazy
|
|
214
|
+
@Autowired
|
|
215
|
+
private XxxService xxxService;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 统一处理调用(核心模板方法)
|
|
219
|
+
* 负责:反序列化、设置租户上下文、异常兜底
|
|
220
|
+
*/
|
|
221
|
+
public <T> void handleMessage(MqPayload<String> payload, Class<T> clz, BiConsumer<XxxMqHandler, T> handleFunc) {
|
|
222
|
+
I18nUtil.loadDefaultLocale();
|
|
223
|
+
try {
|
|
224
|
+
log.info("[Xxx消息]收到消息 {}", payload);
|
|
225
|
+
T payloadData = JacksonUtil.readValue(payload.getData(), clz);
|
|
226
|
+
if (payloadData != null) {
|
|
227
|
+
TenantContextHolder.setTenantId(payload.getTenantId()); // 设置租户上下文
|
|
228
|
+
handleFunc.accept(this, payloadData);
|
|
229
|
+
} else {
|
|
230
|
+
log.error("[Xxx消息]解析失败");
|
|
231
|
+
}
|
|
232
|
+
} catch (Exception e) {
|
|
233
|
+
log.error("[Xxx消息]处理异常", e);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 处理 xxx 事件
|
|
239
|
+
*/
|
|
240
|
+
public void handleXxx(XxxPO payload) {
|
|
241
|
+
try {
|
|
242
|
+
log.info("[Xxx事件]MQ消费:开始");
|
|
243
|
+
xxxService.processXxx(payload);
|
|
244
|
+
log.info("[Xxx事件]MQ消费:消息消费完成");
|
|
245
|
+
} catch (Exception e) {
|
|
246
|
+
log.error("[Xxx事件]MQ消费:处理异常", e);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 常见场景
|
|
253
|
+
|
|
254
|
+
### 场景1:事务消息(下单后通知)
|
|
255
|
+
|
|
256
|
+
```java
|
|
257
|
+
@Transactional(rollbackFor = Exception.class)
|
|
258
|
+
public void createOrder(OrderParam param) {
|
|
259
|
+
// 1. 保存订单
|
|
260
|
+
OrderInfo order = OrderInfo.newDefaultInstance();
|
|
261
|
+
order.setCanteenId(param.getCanteenId());
|
|
262
|
+
orderMapper.insert(order);
|
|
263
|
+
|
|
264
|
+
// 2. 事务提交后发送消息(保证一致性)
|
|
265
|
+
OrderPlacedPO po = new OrderPlacedPO();
|
|
266
|
+
po.setOrderInfo(order);
|
|
267
|
+
OrderMessageSend.sendOrderPlacedByTx(po); // 内部使用 sendByTxEnd
|
|
268
|
+
|
|
269
|
+
log.info("订单创建成功,orderId:{}", order.getId());
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 场景2:延迟消息(订单超时取消)
|
|
274
|
+
|
|
275
|
+
```java
|
|
276
|
+
public static LocalDateTime sendOrderTimeout(String macOrderId, LeMqConstant.DelayDuration delayDuration) {
|
|
277
|
+
log.info("[订单MQv3]发送未支付订单异步支付超时通知");
|
|
278
|
+
OrderCancelPO po = new OrderCancelPO();
|
|
279
|
+
po.setMacOrderId(macOrderId);
|
|
280
|
+
po.setTenantId(TenantContextHolder.getTenantId());
|
|
281
|
+
po.setTraceId(LogUtil.getCurrentTraceId());
|
|
282
|
+
|
|
283
|
+
// 延迟发送
|
|
284
|
+
MqUtil.sendDelay(JacksonUtil.writeValueAsString(po), LeMqConstant.Topic.ORDER_V3_ASYNC_TIMEOUT, delayDuration);
|
|
285
|
+
|
|
286
|
+
// 返回预计触发时间
|
|
287
|
+
return LocalDateTime.now().plusSeconds(delayDuration.getMilliseconds() / 1000);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 场景3:带 Redisson 分布式锁的 MQ 消费
|
|
292
|
+
|
|
293
|
+
```java
|
|
294
|
+
public void orderAsyncSave(OrderSavePO payload) {
|
|
295
|
+
// 消费时加分布式锁(防止并发处理同一订单)
|
|
296
|
+
RLock lock = RedisUtil.getLock(OrderCacheConstants.orderCacheSaveLockKey(payload.getMacOrderId()));
|
|
297
|
+
lock.lock();
|
|
298
|
+
try {
|
|
299
|
+
log.info("[订单异步保存]MQ消费:开始");
|
|
300
|
+
doSaveOrder(payload);
|
|
301
|
+
log.info("[订单异步保存]MQ消费:消息消费完成");
|
|
302
|
+
} catch (Exception e) {
|
|
303
|
+
log.error("[订单异步保存]MQ消费:处理异常", e);
|
|
304
|
+
} finally {
|
|
305
|
+
// 安全释放锁
|
|
306
|
+
try {
|
|
307
|
+
if (lock.isHeldByCurrentThread() && lock.isLocked()) {
|
|
308
|
+
lock.unlock();
|
|
309
|
+
}
|
|
310
|
+
} catch (Exception e) {
|
|
311
|
+
log.error("解锁异常", e);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## 日志规范
|
|
318
|
+
|
|
319
|
+
```java
|
|
320
|
+
// 发送端日志格式
|
|
321
|
+
log.info("[模块MQv3]发送xxx事件");
|
|
322
|
+
|
|
323
|
+
// 消费端日志格式
|
|
324
|
+
log.info("[xxx事件]MQ消费:开始");
|
|
325
|
+
log.info("[xxx事件]MQ消费:消息消费完成");
|
|
326
|
+
log.error("[xxx事件]MQ消费:处理异常", e);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## 常见错误
|
|
330
|
+
|
|
331
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
332
|
+
|---------|---------|------|
|
|
333
|
+
| `MqUtil.send(dto, topic)` 直接传对象 | `MqUtil.send(JacksonUtil.writeValueAsString(dto), topic)` | 必须先序列化为 String |
|
|
334
|
+
| `@MqConsumer(topic = ...)` | `@MQMessageListener(group, topic, tag)` + `implements MQListener<MqPayload<String>>` | 实际框架注解不同 |
|
|
335
|
+
| `Duration.ofMinutes(30)` | `LeMqConstant.DelayDuration.THIRTY_MINUTES` | 延迟枚举不是 Duration |
|
|
336
|
+
| 忘记在 PO 中设置 `traceId`/`tenantId` | `po.setTraceId(LogUtil.getCurrentTraceId())` | 多租户追踪必须设置 |
|
|
337
|
+
| 消费方法直接 `@Autowired` 服务 | `@Autowired @Lazy` | 避免循环依赖 |
|
|
338
|
+
| 在 MQ 发送类上加 `@Component` | 纯静态工具类(私有构造器,不注入 Spring) | 发送类是静态工具 |
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: leniu-java-mybatis
|
|
3
|
+
description: |
|
|
4
|
+
leniu-tengyun-core 项目 MyBatis/MyBatis-Plus 使用规范。当编写 Mapper、XML 映射、分页查询时使用此 skill。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 编写 Mapper 接口(extends BaseMapper)
|
|
8
|
+
- 编写 MyBatis XML 映射文件(动态 SQL)
|
|
9
|
+
- 使用 LambdaQueryWrapper 构建查询
|
|
10
|
+
- 分页查询(PageHelper + PageVO)
|
|
11
|
+
- 租户隔离控制(@InterceptorIgnore)
|
|
12
|
+
|
|
13
|
+
触发词:MyBatis、MyBatisPlus、Mapper、LambdaQueryWrapper、XML映射、动态SQL、BaseMapper、分页查询、租户隔离、报表Mapper、PageHelper
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# leniu MyBatis 规范
|
|
17
|
+
|
|
18
|
+
## 项目特征速查
|
|
19
|
+
|
|
20
|
+
| 项 | 值 |
|
|
21
|
+
|---|---|
|
|
22
|
+
| XML 位置 | **与 Mapper 接口同目录**(非 `resources/mapper/`) |
|
|
23
|
+
| 分页 | PageHelper → `PageMethod.startPage(PageDTO)` → `PageVO.of(list)` |
|
|
24
|
+
| 逻辑删除 | **1=删除,2=正常**(与 RuoYi 相反) |
|
|
25
|
+
| Service | 无接口,直接 `@Service` 类,Mapper 字段名统一用 `baseMapper` |
|
|
26
|
+
| 循环依赖 | 跨模块依赖用 `@Autowired @Lazy` |
|
|
27
|
+
|
|
28
|
+
## Mapper 接口模板
|
|
29
|
+
|
|
30
|
+
### 基础 Mapper
|
|
31
|
+
|
|
32
|
+
```java
|
|
33
|
+
@Mapper
|
|
34
|
+
public interface XxxMapper extends BaseMapper<XxxEntity> {
|
|
35
|
+
|
|
36
|
+
List<XxxVO> listByParam(@Param("param") XxxParam param);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 忽略租户隔离(方法级)
|
|
41
|
+
|
|
42
|
+
```java
|
|
43
|
+
@Mapper
|
|
44
|
+
public interface XxxMapper extends BaseMapper<XxxEntity> {
|
|
45
|
+
|
|
46
|
+
@InterceptorIgnore(tenantLine = "true")
|
|
47
|
+
List<XxxVO> queryWithoutTenant(@Param("param") XxxParam param);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 全量忽略拦截器(类级别)
|
|
52
|
+
|
|
53
|
+
```java
|
|
54
|
+
@Mapper
|
|
55
|
+
@InterceptorIgnore // 无参数,跳过所有拦截器(租户、数据权限等)
|
|
56
|
+
public interface XxxMapper extends BaseMapper<XxxEntity>, BaseExistsMapper<XxxEntity> {
|
|
57
|
+
|
|
58
|
+
List<XxxVO> listVoByIds(@Param("orderIds") List<Long> ids, @Param("tenantId") String tenantId);
|
|
59
|
+
|
|
60
|
+
@QueryExtension
|
|
61
|
+
List<XxxIdDateVO> queryXxx(@Param("param") XxxSearchParam param,
|
|
62
|
+
@Param("permission") XxxUserPermissionDTO permission);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `BaseExistsMapper` 提供 `existsOne(Wrapper)` 方法(比 `selectCount > 0` 高效)
|
|
67
|
+
- 适用于数据量大、需跨租户查询的核心表
|
|
68
|
+
|
|
69
|
+
## XML 模板
|
|
70
|
+
|
|
71
|
+
```xml
|
|
72
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
+
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
74
|
+
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
75
|
+
<mapper namespace="net.xnzn.core.xxx.mapper.XxxMapper">
|
|
76
|
+
|
|
77
|
+
<select id="listByParam" resultType="net.xnzn.core.xxx.vo.XxxVO">
|
|
78
|
+
SELECT
|
|
79
|
+
t.id,
|
|
80
|
+
t.name,
|
|
81
|
+
t.status
|
|
82
|
+
FROM table_name t
|
|
83
|
+
<where>
|
|
84
|
+
t.del_flag = 2
|
|
85
|
+
<if test="param.status != null">
|
|
86
|
+
AND t.status = #{param.status}
|
|
87
|
+
</if>
|
|
88
|
+
<if test="param.keyword != null and param.keyword != ''">
|
|
89
|
+
AND t.name LIKE CONCAT('%', #{param.keyword}, '%')
|
|
90
|
+
</if>
|
|
91
|
+
</where>
|
|
92
|
+
ORDER BY t.crtime DESC
|
|
93
|
+
</select>
|
|
94
|
+
</mapper>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### XML 编写规则
|
|
98
|
+
|
|
99
|
+
| 规则 | 说明 |
|
|
100
|
+
|------|------|
|
|
101
|
+
| 禁止 `SELECT *` | 必须明确指定字段 |
|
|
102
|
+
| `del_flag = 2` | 正常数据条件 |
|
|
103
|
+
| `#{}` 占位符 | 禁止 `${}`(SQL 注入) |
|
|
104
|
+
| `<where>` 标签 | 自动处理 AND 前缀 |
|
|
105
|
+
| 特殊字符 | `<` / `>` 或 `<![CDATA[ ]]>` |
|
|
106
|
+
|
|
107
|
+
### 常用动态 SQL 片段
|
|
108
|
+
|
|
109
|
+
```xml
|
|
110
|
+
<!-- IN 查询 -->
|
|
111
|
+
<if test="param.ids != null and param.ids.size() > 0">
|
|
112
|
+
AND t.id IN
|
|
113
|
+
<foreach collection="param.ids" item="id" open="(" separator="," close=")">
|
|
114
|
+
#{id}
|
|
115
|
+
</foreach>
|
|
116
|
+
</if>
|
|
117
|
+
|
|
118
|
+
<!-- 多条件 OR -->
|
|
119
|
+
<if test="param.keyword != null and param.keyword != ''">
|
|
120
|
+
AND (t.name LIKE CONCAT('%', #{param.keyword}, '%')
|
|
121
|
+
OR t.code LIKE CONCAT('%', #{param.keyword}, '%'))
|
|
122
|
+
</if>
|
|
123
|
+
|
|
124
|
+
<!-- 时间范围 -->
|
|
125
|
+
<if test="param.startDate != null">
|
|
126
|
+
AND t.crtime >= #{param.startDate}
|
|
127
|
+
</if>
|
|
128
|
+
<if test="param.endDate != null">
|
|
129
|
+
AND t.crtime <= #{param.endDate}
|
|
130
|
+
</if>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## LambdaQuery 使用
|
|
134
|
+
|
|
135
|
+
```java
|
|
136
|
+
List<XxxEntity> list = mapper.selectList(
|
|
137
|
+
Wrappers.lambdaQuery(XxxEntity.class)
|
|
138
|
+
.eq(XxxEntity::getStatus, 1)
|
|
139
|
+
.eq(XxxEntity::getDelFlag, 2) // 2=正常
|
|
140
|
+
.in(CollUtil.isNotEmpty(idList), XxxEntity::getId, idList)
|
|
141
|
+
.like(StrUtil.isNotBlank(name), XxxEntity::getName, name)
|
|
142
|
+
.ge(startDate != null, XxxEntity::getCrtime, startDate)
|
|
143
|
+
.le(endDate != null, XxxEntity::getCrtime, endDate)
|
|
144
|
+
.orderByDesc(XxxEntity::getCrtime)
|
|
145
|
+
);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Service 注入规范
|
|
149
|
+
|
|
150
|
+
```java
|
|
151
|
+
@Slf4j
|
|
152
|
+
@Service
|
|
153
|
+
@Validated
|
|
154
|
+
public class XxxService {
|
|
155
|
+
|
|
156
|
+
@Autowired
|
|
157
|
+
private XxxMapper baseMapper; // ✅ 统一命名 baseMapper
|
|
158
|
+
|
|
159
|
+
@Autowired @Lazy
|
|
160
|
+
private XxxDetailService xxxDetailService; // ✅ 跨模块用 @Lazy
|
|
161
|
+
|
|
162
|
+
public XxxEntity getOne(Long id) {
|
|
163
|
+
return baseMapper.selectById(id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public boolean exists(String macOrderId) {
|
|
167
|
+
return baseMapper.existsOne(
|
|
168
|
+
Wrappers.lambdaQuery(XxxEntity.class)
|
|
169
|
+
.eq(XxxEntity::getMacOrderId, macOrderId)
|
|
170
|
+
.eq(XxxEntity::getDelFlag, 2)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 分页查询
|
|
177
|
+
|
|
178
|
+
```java
|
|
179
|
+
public PageVO<XxxVO> pageList(XxxParam param) {
|
|
180
|
+
// ✅ 传入 PageDTO 对象(不要拆 pageNum/pageSize)
|
|
181
|
+
if (Objects.nonNull(param.getPage())) {
|
|
182
|
+
PageMethod.startPage(param.getPage());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
List<XxxVO> records = xxxMapper.listByParam(param);
|
|
186
|
+
|
|
187
|
+
return PageVO.of(records); // 自动提取 total 等信息
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Mapper 方法返回 `List<XxxVO>` 即可,PageHelper 拦截器自动处理分页。
|
|
192
|
+
|
|
193
|
+
## 报表 Mapper(无 BaseMapper)
|
|
194
|
+
|
|
195
|
+
```java
|
|
196
|
+
@Mapper
|
|
197
|
+
public interface ReportXxxMapper { // ✅ 不继承 BaseMapper
|
|
198
|
+
|
|
199
|
+
List<XxxVO> listSummary(
|
|
200
|
+
@Param("param") XxxParam param,
|
|
201
|
+
@Param("authPO") MgrUserAuthPO authPO,
|
|
202
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
XxxVO getSummaryTotal( // 合计行
|
|
206
|
+
@Param("param") XxxParam param,
|
|
207
|
+
@Param("authPO") MgrUserAuthPO authPO,
|
|
208
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**命名规律**:`listXxx()` 分页数据 / `listXxx_COUNT()` 计数 / `getSummaryTotal()` 合计行 / `listXxxByDay()` 按维度
|
|
214
|
+
|
|
215
|
+
## 合计行查询
|
|
216
|
+
|
|
217
|
+
```xml
|
|
218
|
+
<!-- 列表查询 -->
|
|
219
|
+
<select id="listByParam" resultType="XxxVO">
|
|
220
|
+
SELECT id, name, amount, count
|
|
221
|
+
FROM table_name
|
|
222
|
+
<where>...</where>
|
|
223
|
+
ORDER BY id DESC
|
|
224
|
+
</select>
|
|
225
|
+
|
|
226
|
+
<!-- 合计查询:只返回数值字段 -->
|
|
227
|
+
<select id="getSummaryTotal" resultType="XxxVO">
|
|
228
|
+
SELECT SUM(amount) AS amount, SUM(count) AS count
|
|
229
|
+
FROM table_name
|
|
230
|
+
<where>...</where>
|
|
231
|
+
</select>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
除零处理:
|
|
235
|
+
```xml
|
|
236
|
+
CASE WHEN SUM(count) = 0 THEN 0 ELSE SUM(amount) / SUM(count) END AS avgAmount
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## 禁止项
|
|
240
|
+
|
|
241
|
+
```java
|
|
242
|
+
// ❌ 继承 RuoYi TenantEntity
|
|
243
|
+
import org.dromara.common.mybatis.core.domain.TenantEntity;
|
|
244
|
+
|
|
245
|
+
// ❌ delFlag: 0=正常(leniu 是 2=正常)
|
|
246
|
+
wrapper.eq(XxxEntity::getDelFlag, 0);
|
|
247
|
+
|
|
248
|
+
// ❌ XML 放 resources/mapper/(必须与 Mapper 接口同目录)
|
|
249
|
+
|
|
250
|
+
// ❌ MapstructUtils(用 BeanUtil.copyProperties)
|
|
251
|
+
MapstructUtils.convert(source, Target.class);
|
|
252
|
+
|
|
253
|
+
// ❌ Service 继承 IService / ServiceImpl
|
|
254
|
+
public interface IXxxService extends IService<XxxEntity> {}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## XML 文件位置
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
net.xnzn.core.xxx.mapper/
|
|
261
|
+
├── XxxMapper.java # 接口
|
|
262
|
+
└── XxxMapper.xml # XML(同目录!)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## 参考文档
|
|
266
|
+
|
|
267
|
+
- 报表 Mapper 完整示例:详见 `references/report-mapper.md`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 报表 Mapper 完整示例
|
|
2
|
+
|
|
3
|
+
## Mapper 接口(来自 ReportAnalysisTurnoverMapper 真实代码)
|
|
4
|
+
|
|
5
|
+
```java
|
|
6
|
+
@Mapper
|
|
7
|
+
public interface ReportXxxMapper {
|
|
8
|
+
|
|
9
|
+
// 分页列表查询(PageHelper 拦截,方法返回 List)
|
|
10
|
+
List<XxxVO> listSummary(
|
|
11
|
+
@Param("param") XxxParam param,
|
|
12
|
+
@Param("authPO") MgrUserAuthPO authPO,
|
|
13
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// 合计行查询(不分页,返回单个汇总 VO)
|
|
17
|
+
XxxVO getSummaryTotal(
|
|
18
|
+
@Param("param") XxxParam param,
|
|
19
|
+
@Param("authPO") MgrUserAuthPO authPO,
|
|
20
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// 按日/按月分组(对应 dateType 参数)
|
|
24
|
+
List<XxxVO> listSummaryByDay(
|
|
25
|
+
@Param("param") XxxParam param,
|
|
26
|
+
@Param("authPO") MgrUserAuthPO authPO,
|
|
27
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
28
|
+
);
|
|
29
|
+
List<XxxVO> listSummaryByMonth(
|
|
30
|
+
@Param("param") XxxParam param,
|
|
31
|
+
@Param("authPO") MgrUserAuthPO authPO,
|
|
32
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// 汇总数据(总金额、人次等)
|
|
36
|
+
ReportTurnoverPO getTurnoverTotal(
|
|
37
|
+
@Param("param") XxxParam param,
|
|
38
|
+
@Param("authPO") MgrUserAuthPO authPO,
|
|
39
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// 排行榜类(不需要 authPO 时可省略)
|
|
43
|
+
List<RankVO> getXxxRank(
|
|
44
|
+
@Param("param") RankParam param,
|
|
45
|
+
@Param("dataPermission") ReportDataPermissionParam dataPermission
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 关键规则
|
|
51
|
+
|
|
52
|
+
1. `@Mapper` 注解,**不继承 BaseMapper**(报表无 CRUD)
|
|
53
|
+
2. 所有参数必须加 `@Param`,顺序:`param` -> `authPO` -> `dataPermission`
|
|
54
|
+
3. 分页列表返回 `List<VO>`,由 Service 层调用 `PageMethod.startPage()` 控制
|
|
55
|
+
4. 合计行返回单个 PO/VO 对象
|
|
56
|
+
5. COUNT 方法(`listXxx_COUNT`)配合 `CompletableFuture` 并发使用
|
|
57
|
+
|
|
58
|
+
## 命名规律
|
|
59
|
+
|
|
60
|
+
| 类型 | 命名 |
|
|
61
|
+
|------|------|
|
|
62
|
+
| 分页数据 | `listXxx()` |
|
|
63
|
+
| 对应 COUNT | `listXxx_COUNT()`(下划线 + COUNT) |
|
|
64
|
+
| 合计行 | `getSummaryTotal()` / `getSummaryXxxTotal()` |
|
|
65
|
+
| 按维度变体 | `listXxxByDay()` / `listXxxByDay_COUNT()` |
|
|
66
|
+
|
|
67
|
+
## 合计 SQL 常用函数
|
|
68
|
+
|
|
69
|
+
| 函数 | 用途 |
|
|
70
|
+
|------|------|
|
|
71
|
+
| `SUM()` | 求和 |
|
|
72
|
+
| `COUNT(*)` | 计数 |
|
|
73
|
+
| `AVG()` | 平均值 |
|
|
74
|
+
| `MAX()` / `MIN()` | 极值 |
|
|
75
|
+
|
|
76
|
+
## 除零处理
|
|
77
|
+
|
|
78
|
+
```xml
|
|
79
|
+
CASE
|
|
80
|
+
WHEN SUM(count) = 0 THEN 0
|
|
81
|
+
ELSE SUM(amount) / SUM(count)
|
|
82
|
+
END AS avgAmount
|
|
83
|
+
|
|
84
|
+
CASE
|
|
85
|
+
WHEN SUM(staff_count) = 0 THEN 0
|
|
86
|
+
ELSE SUM(avg_salary) / COUNT(DISTINCT tenant_id)
|
|
87
|
+
END AS avgSalary
|
|
88
|
+
```
|