ai-engineering-init 1.14.0 → 1.14.2
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/hooks/skill-forced-eval.js +1 -0
- package/.claude/hooks/stop.js +1 -2
- package/.claude/skills/auto-test/SKILL.md +182 -10
- package/.claude/skills/code-patterns/SKILL.md +119 -0
- package/.claude/skills/codex-code-review/SKILL.md +39 -0
- package/.claude/skills/leniu-code-patterns/SKILL.md +179 -2
- package/.claude/skills/leniu-crud-development/SKILL.md +26 -7
- package/.claude/skills/leniu-java-mybatis/SKILL.md +25 -16
- package/.claude/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.claude/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.claude/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.claude/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/.codex/skills/code-patterns/SKILL.md +119 -0
- package/.codex/skills/leniu-code-patterns/SKILL.md +179 -2
- package/.codex/skills/leniu-crud-development/SKILL.md +26 -7
- package/.codex/skills/leniu-java-mybatis/SKILL.md +25 -16
- package/.codex/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.codex/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.codex/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.codex/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/.cursor/hooks/cursor-skill-eval.js +4 -29
- package/.cursor/rules/skill-activation.mdc +1 -7
- package/.cursor/skills/code-patterns/SKILL.md +119 -0
- package/.cursor/skills/leniu-code-patterns/SKILL.md +179 -2
- package/.cursor/skills/leniu-crud-development/SKILL.md +26 -7
- package/.cursor/skills/leniu-java-mybatis/SKILL.md +25 -16
- package/.cursor/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.cursor/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.cursor/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.cursor/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/CLAUDE.md +0 -28
- package/bin/index.js +145 -52
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ description: |
|
|
|
22
22
|
| XML 位置 | **与 Mapper 接口同目录**(非 `resources/mapper/`) |
|
|
23
23
|
| 分页 | PageHelper → `PageMethod.startPage(PageDTO)` → `PageVO.of(list)` |
|
|
24
24
|
| 逻辑删除 | **1=删除,2=正常**(与 RuoYi 相反) |
|
|
25
|
-
| Service |
|
|
25
|
+
| Service | **两种模式并存**:简单 CRUD 继承 `ServiceImpl`(`this.baseMapper`);业务聚合直接 `@Service`(`@Autowired XxxMapper xxxMapper`) |
|
|
26
26
|
| 循环依赖 | 跨模块依赖用 `@Autowired @Lazy` |
|
|
27
27
|
|
|
28
28
|
## Mapper 接口模板
|
|
@@ -147,28 +147,36 @@ List<XxxEntity> list = mapper.selectList(
|
|
|
147
147
|
|
|
148
148
|
## Service 注入规范
|
|
149
149
|
|
|
150
|
+
项目中 Service 有两种模式,Mapper 字段名也不统一——参考周围已有代码的风格即可。
|
|
151
|
+
|
|
152
|
+
**模式 A:继承 ServiceImpl(简单 CRUD)**
|
|
153
|
+
```java
|
|
154
|
+
@Service
|
|
155
|
+
public class XxxServiceImpl extends ServiceImpl<XxxMapper, XxxEntity> implements XxxService {
|
|
156
|
+
// 通过 this.baseMapper 访问 Mapper(父类自动注入)
|
|
157
|
+
public boolean exists(String macOrderId) {
|
|
158
|
+
return this.baseMapper.existsOne(
|
|
159
|
+
Wrappers.lambdaQuery(XxxEntity.class)
|
|
160
|
+
.eq(XxxEntity::getMacOrderId, macOrderId)
|
|
161
|
+
.eq(XxxEntity::getDelFlag, 2)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**模式 B:直接 @Service(业务聚合)**
|
|
150
168
|
```java
|
|
151
169
|
@Slf4j
|
|
152
170
|
@Service
|
|
153
|
-
@Validated
|
|
154
171
|
public class XxxService {
|
|
155
|
-
|
|
156
172
|
@Autowired
|
|
157
|
-
private XxxMapper
|
|
173
|
+
private XxxMapper xxxMapper; // 字段名跟随 Mapper 类名
|
|
158
174
|
|
|
159
175
|
@Autowired @Lazy
|
|
160
|
-
private
|
|
176
|
+
private YyyService yyyService; // 跨模块用 @Lazy
|
|
161
177
|
|
|
162
178
|
public XxxEntity getOne(Long id) {
|
|
163
|
-
return
|
|
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
|
-
);
|
|
179
|
+
return xxxMapper.selectById(id);
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
```
|
|
@@ -250,8 +258,9 @@ wrapper.eq(XxxEntity::getDelFlag, 0);
|
|
|
250
258
|
// ❌ MapstructUtils(用 BeanUtil.copyProperties)
|
|
251
259
|
MapstructUtils.convert(source, Target.class);
|
|
252
260
|
|
|
253
|
-
// ❌ Service
|
|
254
|
-
|
|
261
|
+
// ❌ 业务聚合 Service 不要继承 IService / ServiceImpl(简单 CRUD Service 可以继承)
|
|
262
|
+
// 错误:在报表/业务编排 Service 中继承 ServiceImpl
|
|
263
|
+
// 正确:简单单表 CRUD 可以继承 ServiceImpl,复杂业务直接 @Service
|
|
255
264
|
```
|
|
256
265
|
|
|
257
266
|
## XML 文件位置
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: leniu-report-scenario
|
|
3
|
+
description: |
|
|
4
|
+
leniu-tengyun-core 报表开发场景化技能。统一覆盖报表查询、导出、合计行、金额处理、餐次、汇总表定制等全场景。
|
|
5
|
+
当用户提到任何与报表、统计汇总、数据导出相关的开发需求时,都应使用此技能——即使用户没有明确说"报表",
|
|
6
|
+
只要涉及分页查询+导出、金额分转元、合计行、按日期/食堂/餐次维度汇总,就属于本技能的范畴。
|
|
7
|
+
|
|
8
|
+
触发场景:
|
|
9
|
+
- 开发报表分页查询 + 导出接口
|
|
10
|
+
- 基于 report_order_info / report_account_flow 开发汇总报表
|
|
11
|
+
- 报表 VO 金额字段处理(BigDecimal 分→元、CustomNumberConverter)
|
|
12
|
+
- 报表合计行查询(ReportBaseTotalVO + totalLine)
|
|
13
|
+
- 报表餐次筛选与转换(MealtimeTypeConverter)
|
|
14
|
+
- 定制汇总表(ReportOrderConsumeService + MQ 消费 + fix 重算)
|
|
15
|
+
- 用户说"做个统计页面"、"按食堂汇总消费数据"、"导出 Excel"等
|
|
16
|
+
|
|
17
|
+
触发词:报表、报表开发、报表查询、报表导出、统计、汇总、合计行、totalLine、汇总报表、定制报表、report_order_info、report_account_flow、金额处理、分转元、餐次、mealtime、ReportBaseTotalVO、CustomNumberConverter、ExcelProperty、ReportOrderConsumeService、ExportApi、PageVO、导出Excel
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# leniu 报表开发场景化指南
|
|
21
|
+
|
|
22
|
+
> **本技能合并了 7 个报表碎片技能的核心知识**。按需加载详细参考:
|
|
23
|
+
> - 汇总表定制(MQ/fix/batchConsume)→ 读 `references/customization.md`
|
|
24
|
+
> - report_order_info 完整字段 → 读 `references/report-tables.md`
|
|
25
|
+
> - 数据权限集成 → 读 `references/data-permission.md`
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 一、决策树(开发前先过一遍)
|
|
30
|
+
|
|
31
|
+
### Q1: 数据来源?
|
|
32
|
+
|
|
33
|
+
| 来源 | 说明 | 操作 |
|
|
34
|
+
|------|------|------|
|
|
35
|
+
| **A. 基于 report_order_info** | 订单/退款汇总报表 | 读 `references/report-tables.md` 了解字段 |
|
|
36
|
+
| **B. 基于 report_account_flow** | 账户流水报表 | 读 `references/report-tables.md` |
|
|
37
|
+
| **C. 已有业务表** | 非报表基础表的查询 | 直接用本文模板 |
|
|
38
|
+
| **D. 全新表** | 新建汇总表 | 读 `references/customization.md` |
|
|
39
|
+
|
|
40
|
+
### Q2: 需要哪些功能?
|
|
41
|
+
|
|
42
|
+
| 功能 | 是否必选 | 说明 |
|
|
43
|
+
|------|---------|------|
|
|
44
|
+
| 分页查询 | **必选** | PageMethod + PageVO |
|
|
45
|
+
| 导出 | **必选** | ExportApi / EasyExcelUtil |
|
|
46
|
+
| 合计行 | 可选 | ReportBaseTotalVO + totalLine |
|
|
47
|
+
| 餐次筛选 | 可选 | mealtimeTypes + MealtimeTypeConverter |
|
|
48
|
+
| 数据权限 | 默认不加 | 用户要求时读 `references/data-permission.md` |
|
|
49
|
+
| MQ 消费/fix | 仅汇总表 | 读 `references/customization.md` |
|
|
50
|
+
|
|
51
|
+
### Q3: 版本?
|
|
52
|
+
|
|
53
|
+
| 判断方式 | 标准版(core-report) | v5.29 版本(sys-canteen) |
|
|
54
|
+
|---------|---------------------|-------------------------|
|
|
55
|
+
| 退款存储 | 独立 `report_refund` 表(**正数**) | 合并入 `report_order_info`(`consumeType=2`,**负数**) |
|
|
56
|
+
| 第二阶段 | `fix()` 按日重算 | `batchConsume()` 增量累加 |
|
|
57
|
+
| consumeType | **无此字段** | 1=消费,2=退款 |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 二、金额处理规范
|
|
62
|
+
|
|
63
|
+
### 存储模式
|
|
64
|
+
|
|
65
|
+
| 模块 | Entity 类型 | 值单位 | VO 类型 |
|
|
66
|
+
|------|------------|--------|---------|
|
|
67
|
+
| 订单/报表 | `BigDecimal` | 分 | `BigDecimal` |
|
|
68
|
+
| 钱包/账户 | `Long` | 分 | `BigDecimal`(元,MyBatis 自动转) |
|
|
69
|
+
|
|
70
|
+
### 为什么这样设计
|
|
71
|
+
|
|
72
|
+
金额在数据库中以**分**为单位存储(避免浮点精度问题),转换为**元**的职责交给展示层:
|
|
73
|
+
- 页面展示:前端 `money()` 过滤器自动除以 100
|
|
74
|
+
- Excel 导出:`CustomNumberConverter` 自动除以 100
|
|
75
|
+
- SQL 中不做除法:保证聚合计算精度不丢失
|
|
76
|
+
|
|
77
|
+
```java
|
|
78
|
+
// ❌ SQL 中做 /100.0 会导致聚合精度丢失,且前端会再除一次变成万分之一
|
|
79
|
+
SELECT order_amount / 100.0 AS order_amount
|
|
80
|
+
|
|
81
|
+
// ✅ 直接查原始值,展示层负责转换
|
|
82
|
+
SELECT order_amount AS order_amount
|
|
83
|
+
|
|
84
|
+
// ✅ 导出时 CustomNumberConverter 自动 ÷100
|
|
85
|
+
@ExcelProperty(value = "金额(元)", converter = CustomNumberConverter.class)
|
|
86
|
+
private BigDecimal amount;
|
|
87
|
+
|
|
88
|
+
// ❌ @NumberFormat 只做格式化,不做 ÷100 转换
|
|
89
|
+
@NumberFormat("#,##0.00")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 合计行 SQL 只返回数值字段
|
|
93
|
+
|
|
94
|
+
合计行的目的是在表格底部显示各列的汇总值。非数值列(日期、名称、ID)在合计行中无意义,返回它们会导致前端渲染混乱或 MyBatis 映射错误。
|
|
95
|
+
|
|
96
|
+
```xml
|
|
97
|
+
<select id="getSummaryTotal" resultType="XxxVO">
|
|
98
|
+
SELECT
|
|
99
|
+
SUM(real_amount) AS realAmount,
|
|
100
|
+
SUM(refund_amount) AS refundAmount,
|
|
101
|
+
SUM(order_count) AS orderCount
|
|
102
|
+
FROM xxx_table
|
|
103
|
+
<where>...</where>
|
|
104
|
+
</select>
|
|
105
|
+
<!-- 注意:不要 SELECT statistic_date、canteen_name 等非数值字段 -->
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 三、Param 模板
|
|
111
|
+
|
|
112
|
+
```java
|
|
113
|
+
@Data
|
|
114
|
+
@EqualsAndHashCode(callSuper = true)
|
|
115
|
+
@ApiModel(value = "XXX报表查询参数")
|
|
116
|
+
public class XxxParam extends ReportBaseParam {
|
|
117
|
+
// ReportBaseParam 已有: page(PageDTO), startDate, endDate, exportCols, sumType, sumDimension
|
|
118
|
+
|
|
119
|
+
@ApiModelProperty("食堂ID列表")
|
|
120
|
+
private List<Long> canteenIds;
|
|
121
|
+
|
|
122
|
+
@ApiModelProperty("组织ID列表")
|
|
123
|
+
private List<Long> orgIds;
|
|
124
|
+
|
|
125
|
+
@ApiModelProperty("餐次集合") // 可选
|
|
126
|
+
private List<Integer> mealtimeTypes;
|
|
127
|
+
|
|
128
|
+
@ApiModelProperty("关键字")
|
|
129
|
+
private String keyword;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**两层继承**(经营分析类报表):
|
|
134
|
+
```
|
|
135
|
+
ReportAnalysisParam(含 dateType: 1按月/2按日, canteenStallIds)
|
|
136
|
+
└── ReportAnalysisTurnoverParam(具体 Param)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 四、VO 模板
|
|
142
|
+
|
|
143
|
+
```java
|
|
144
|
+
@Data
|
|
145
|
+
@Accessors(chain = true)
|
|
146
|
+
@ApiModel("XXX报表")
|
|
147
|
+
public class XxxVO {
|
|
148
|
+
|
|
149
|
+
@ExcelProperty(value = "日期", order = 1)
|
|
150
|
+
@ApiModelProperty("统计日期")
|
|
151
|
+
private String statisticDate;
|
|
152
|
+
|
|
153
|
+
@ExcelProperty(value = "食堂", order = 2)
|
|
154
|
+
@ApiModelProperty("食堂名称")
|
|
155
|
+
private String canteenName;
|
|
156
|
+
|
|
157
|
+
// 可选:餐次字段
|
|
158
|
+
@ExcelProperty(value = "餐次", order = 3, converter = MealtimeTypeConverter.class)
|
|
159
|
+
@ApiModelProperty("餐次")
|
|
160
|
+
private Integer mealtimeType;
|
|
161
|
+
|
|
162
|
+
// 金额字段:必须用 CustomNumberConverter
|
|
163
|
+
@ExcelProperty(value = "消费金额(元)", order = 4, converter = CustomNumberConverter.class)
|
|
164
|
+
@ApiModelProperty("消费金额(分)")
|
|
165
|
+
private BigDecimal consumeAmount;
|
|
166
|
+
|
|
167
|
+
@ExcelProperty(value = "退款金额(元)", order = 5, converter = CustomNumberConverter.class)
|
|
168
|
+
@ApiModelProperty("退款金额(分)")
|
|
169
|
+
private BigDecimal refundAmount;
|
|
170
|
+
|
|
171
|
+
@ExcelProperty(value = "净金额(元)", order = 6, converter = CustomNumberConverter.class)
|
|
172
|
+
@ApiModelProperty("净金额(分)")
|
|
173
|
+
private BigDecimal netAmount;
|
|
174
|
+
|
|
175
|
+
@ExcelProperty(value = "订单数", order = 7)
|
|
176
|
+
@ApiModelProperty("订单数")
|
|
177
|
+
private Integer orderCount;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**必要 import**:
|
|
182
|
+
```java
|
|
183
|
+
import com.alibaba.excel.annotation.ExcelProperty;
|
|
184
|
+
import net.xnzn.core.common.export.converter.CustomNumberConverter;
|
|
185
|
+
import net.xnzn.core.common.export.converter.MealtimeTypeConverter; // 可选
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 五、Controller 模板
|
|
191
|
+
|
|
192
|
+
```java
|
|
193
|
+
@Api(tags = "XXX报表")
|
|
194
|
+
@RestController
|
|
195
|
+
@RequestMapping("/api/v2/web/report/xxx")
|
|
196
|
+
@RequiresAuthentication
|
|
197
|
+
public class XxxReportWebController {
|
|
198
|
+
|
|
199
|
+
@Autowired
|
|
200
|
+
private XxxReportService xxxReportService;
|
|
201
|
+
@Autowired
|
|
202
|
+
private ExportApi exportApi;
|
|
203
|
+
|
|
204
|
+
@PostMapping("/page")
|
|
205
|
+
@ApiOperation("分页查询(带合计)")
|
|
206
|
+
public ReportBaseTotalVO<XxxVO> page(@RequestBody LeRequest<XxxParam> request) {
|
|
207
|
+
return xxxReportService.pageWithTotal(request.getContent());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@PostMapping("/export")
|
|
211
|
+
@ApiOperation("导出")
|
|
212
|
+
public void export(@RequestBody LeRequest<XxxParam> request) {
|
|
213
|
+
XxxParam param = request.getContent();
|
|
214
|
+
XxxVO totalLine = xxxReportService.getSummaryTotal(param);
|
|
215
|
+
exportApi.startExcelExportTaskByPage(
|
|
216
|
+
I18n.getMessage("report.xxx.title"),
|
|
217
|
+
I18n.getMessage(ReportConstant.REPORT_TITLE_DETAILS),
|
|
218
|
+
XxxVO.class,
|
|
219
|
+
param.getExportCols(),
|
|
220
|
+
param.getPage(),
|
|
221
|
+
totalLine,
|
|
222
|
+
() -> xxxReportService.pageWithTotal(param).getResultPage()
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**同步导出**(数据量小时):
|
|
229
|
+
```java
|
|
230
|
+
@PostMapping("/export")
|
|
231
|
+
@SneakyThrows
|
|
232
|
+
public void export(@RequestBody LeRequest<XxxParam> request, HttpServletResponse response) {
|
|
233
|
+
XxxParam param = request.getContent();
|
|
234
|
+
ReportBaseTotalVO<XxxVO> result = xxxReportService.pageWithTotal(param);
|
|
235
|
+
List<XxxVO> records = result.getResultPage().getRecords();
|
|
236
|
+
CollUtil.addAll(records, result.getTotalLine());
|
|
237
|
+
EasyExcelUtil.writeExcelByDownLoadIncludeWrite(
|
|
238
|
+
response, I18n.getMessage("report.xxx.title"),
|
|
239
|
+
XxxVO.class, I18n.getMessage(ReportConstant.REPORT_TITLE_DETAILS),
|
|
240
|
+
records, param.getExportCols()
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 六、Service 模板
|
|
248
|
+
|
|
249
|
+
```java
|
|
250
|
+
@Service
|
|
251
|
+
@Slf4j
|
|
252
|
+
public class XxxReportService {
|
|
253
|
+
|
|
254
|
+
@Autowired
|
|
255
|
+
private XxxReportMapper xxxReportMapper;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 分页查询(带合计行)
|
|
259
|
+
*/
|
|
260
|
+
public ReportBaseTotalVO<XxxVO> pageWithTotal(XxxParam param) {
|
|
261
|
+
ReportBaseTotalVO<XxxVO> result = new ReportBaseTotalVO<>();
|
|
262
|
+
|
|
263
|
+
// 1. 非导出时查合计行
|
|
264
|
+
if (CollUtil.isEmpty(param.getExportCols())) {
|
|
265
|
+
XxxVO totalLine = xxxReportMapper.getSummaryTotal(param);
|
|
266
|
+
result.setTotalLine(totalLine);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 2. 导出时不分页
|
|
270
|
+
if (CollUtil.isEmpty(param.getExportCols())) {
|
|
271
|
+
PageMethod.startPage(param.getPage());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 3. 查询
|
|
275
|
+
List<XxxVO> list = xxxReportMapper.getSummaryList(param);
|
|
276
|
+
result.setResultPage(PageVO.of(list));
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 单独合计行(导出用)
|
|
282
|
+
*/
|
|
283
|
+
public XxxVO getSummaryTotal(XxxParam param) {
|
|
284
|
+
return xxxReportMapper.getSummaryTotal(param);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**高性能版(三并行 CompletableFuture,参考 ReportSumMealtimeService)**:
|
|
290
|
+
```java
|
|
291
|
+
@Autowired
|
|
292
|
+
private AsyncTaskExecutor asyncTaskExecutor;
|
|
293
|
+
|
|
294
|
+
public ReportBaseTotalVO<XxxVO> pageWithTotal(XxxParam param) {
|
|
295
|
+
CompletableFuture<Long> countF = CompletableFuture.supplyAsync(
|
|
296
|
+
() -> xxxReportMapper.listSummary_COUNT(param), asyncTaskExecutor);
|
|
297
|
+
CompletableFuture<List<XxxVO>> listF = CompletableFuture.supplyAsync(() -> {
|
|
298
|
+
PageMethod.startPage(param.getPage());
|
|
299
|
+
return xxxReportMapper.getSummaryList(param);
|
|
300
|
+
}, asyncTaskExecutor);
|
|
301
|
+
CompletableFuture<XxxVO> totalF = CompletableFuture.supplyAsync(
|
|
302
|
+
() -> xxxReportMapper.getSummaryTotal(param), asyncTaskExecutor);
|
|
303
|
+
CompletableFuture.allOf(countF, listF, totalF).join();
|
|
304
|
+
|
|
305
|
+
PageVO<XxxVO> pageVO = PageVO.of(listF.join());
|
|
306
|
+
pageVO.setTotal(countF.join());
|
|
307
|
+
return new ReportBaseTotalVO<XxxVO>()
|
|
308
|
+
.setResultPage(pageVO)
|
|
309
|
+
.setTotalLine(totalF.join());
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 七、Mapper XML 模板
|
|
316
|
+
|
|
317
|
+
```xml
|
|
318
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
319
|
+
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
320
|
+
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
321
|
+
<mapper namespace="net.xnzn.core.xxx.mapper.XxxReportMapper">
|
|
322
|
+
|
|
323
|
+
<!-- 公共查询条件片段 -->
|
|
324
|
+
<sql id="queryParam">
|
|
325
|
+
<if test="startDate != null">
|
|
326
|
+
AND a.statistic_date >= #{startDate}
|
|
327
|
+
</if>
|
|
328
|
+
<if test="endDate != null">
|
|
329
|
+
AND a.statistic_date <= #{endDate}
|
|
330
|
+
</if>
|
|
331
|
+
<if test="canteenIds != null and canteenIds.size() > 0">
|
|
332
|
+
AND a.canteen_id IN
|
|
333
|
+
<foreach collection="canteenIds" item="id" open="(" separator="," close=")">
|
|
334
|
+
#{id}
|
|
335
|
+
</foreach>
|
|
336
|
+
</if>
|
|
337
|
+
<if test="mealtimeTypes != null and mealtimeTypes.size() > 0">
|
|
338
|
+
AND a.mealtime_type IN
|
|
339
|
+
<foreach collection="mealtimeTypes" item="type" open="(" separator="," close=")">
|
|
340
|
+
#{type}
|
|
341
|
+
</foreach>
|
|
342
|
+
</if>
|
|
343
|
+
<if test="keyword != null and keyword != ''">
|
|
344
|
+
AND (a.canteen_name LIKE CONCAT('%', #{keyword}, '%')
|
|
345
|
+
OR a.stall_name LIKE CONCAT('%', #{keyword}, '%'))
|
|
346
|
+
</if>
|
|
347
|
+
</sql>
|
|
348
|
+
|
|
349
|
+
<!-- 分页列表 -->
|
|
350
|
+
<select id="getSummaryList" resultType="net.xnzn.core.xxx.vo.XxxVO">
|
|
351
|
+
SELECT
|
|
352
|
+
a.statistic_date AS statisticDate,
|
|
353
|
+
a.canteen_name AS canteenName,
|
|
354
|
+
a.mealtime_type AS mealtimeType,
|
|
355
|
+
a.consume_amount AS consumeAmount,
|
|
356
|
+
a.refund_amount AS refundAmount,
|
|
357
|
+
a.net_amount AS netAmount,
|
|
358
|
+
a.order_count AS orderCount
|
|
359
|
+
FROM report_sum_xxx a
|
|
360
|
+
WHERE a.del_flag = 2
|
|
361
|
+
<include refid="queryParam"/>
|
|
362
|
+
ORDER BY a.statistic_date DESC
|
|
363
|
+
</select>
|
|
364
|
+
|
|
365
|
+
<!-- 合计行:只返回数值字段 -->
|
|
366
|
+
<select id="getSummaryTotal" resultType="net.xnzn.core.xxx.vo.XxxVO">
|
|
367
|
+
SELECT
|
|
368
|
+
SUM(a.consume_amount) AS consumeAmount,
|
|
369
|
+
SUM(a.refund_amount) AS refundAmount,
|
|
370
|
+
SUM(a.net_amount) AS netAmount,
|
|
371
|
+
SUM(a.order_count) AS orderCount
|
|
372
|
+
FROM report_sum_xxx a
|
|
373
|
+
WHERE a.del_flag = 2
|
|
374
|
+
<include refid="queryParam"/>
|
|
375
|
+
</select>
|
|
376
|
+
|
|
377
|
+
</mapper>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 八、MySQL only_full_group_by 规范
|
|
383
|
+
|
|
384
|
+
```sql
|
|
385
|
+
-- ❌ GROUP BY 表达式与 SELECT 不一致
|
|
386
|
+
SELECT DATE_FORMAT(pay_time, '%Y-%m-%d') AS statisticDate, SUM(real_amount)
|
|
387
|
+
GROUP BY DATE(pay_time) -- ❌ DATE() ≠ DATE_FORMAT()
|
|
388
|
+
|
|
389
|
+
-- ✅ GROUP BY 与 SELECT 完全一致(复制粘贴,不要重写)
|
|
390
|
+
SELECT DATE_FORMAT(pay_time, '%Y-%m-%d') AS statisticDate, SUM(real_amount)
|
|
391
|
+
GROUP BY DATE_FORMAT(pay_time, '%Y-%m-%d')
|
|
392
|
+
|
|
393
|
+
-- ❌ 非聚合字段未加入 GROUP BY
|
|
394
|
+
SELECT canteen_id, canteen_name, SUM(real_amount)
|
|
395
|
+
GROUP BY canteen_id -- ❌ canteen_name 缺失
|
|
396
|
+
|
|
397
|
+
-- ✅ 所有非聚合字段都在 GROUP BY
|
|
398
|
+
SELECT canteen_id, canteen_name, SUM(real_amount)
|
|
399
|
+
GROUP BY canteen_id, canteen_name
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**检查项**:SELECT 非聚合字段 == GROUP BY 字段,表达式逐字一致,ORDER BY 同理。
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 九、退款净额速查
|
|
407
|
+
|
|
408
|
+
### v5.29(consumeType 模式,退款为负数)
|
|
409
|
+
|
|
410
|
+
```sql
|
|
411
|
+
-- 方式一(推荐):直接 SUM,退款已为负数
|
|
412
|
+
SELECT SUM(real_amount) AS netAmount FROM report_order_info
|
|
413
|
+
-- 方式二:分别统计
|
|
414
|
+
SELECT SUM(CASE WHEN consume_type=1 THEN real_amount ELSE 0 END) AS consume,
|
|
415
|
+
SUM(CASE WHEN consume_type=2 THEN ABS(real_refund_amount) ELSE 0 END) AS refund
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### 标准版(独立 report_refund 表,退款为正数)
|
|
419
|
+
|
|
420
|
+
```sql
|
|
421
|
+
-- 方式一(推荐):主表 refundAmount 减退
|
|
422
|
+
SELECT SUM(real_amount - IFNULL(refund_amount, 0)) AS netAmount FROM report_order_info
|
|
423
|
+
-- 方式二:关联退款表
|
|
424
|
+
SELECT SUM(o.real_amount) - IFNULL(SUM(r.real_refund_amount), 0) AS netAmount
|
|
425
|
+
FROM report_order_info o LEFT JOIN report_refund r ON o.order_id = r.order_id
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### 菜品级别退款
|
|
429
|
+
|
|
430
|
+
```sql
|
|
431
|
+
SELECT goods_dishes_name,
|
|
432
|
+
SUM(quantity - IFNULL(goods_refund_num, 0)) AS netQuantity,
|
|
433
|
+
SUM(total_amount - IFNULL(refund_amount, 0)) AS netAmount
|
|
434
|
+
FROM report_order_detail WHERE detail_state IN (1, 3) GROUP BY goods_dishes_name
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## 十、定制报表对齐陷阱
|
|
440
|
+
|
|
441
|
+
### pay_time vs order_date
|
|
442
|
+
|
|
443
|
+
report_order_info 有两个时间维度,选错会导致金额与汇总表不一致:
|
|
444
|
+
|
|
445
|
+
| 字段 | 类型 | 含义 | 退款时的值 |
|
|
446
|
+
|------|------|------|-----------|
|
|
447
|
+
| `order_date` | DATE | 就餐日期 | 原订单就餐日 |
|
|
448
|
+
| `pay_time` | DATETIME | 支付/退款时间 | 退款审核时间(checkTime) |
|
|
449
|
+
|
|
450
|
+
**汇总表(report_sum_canteen 等)按 `pay_time` 聚合**。定制报表如需与汇总表金额对齐,时间条件用 `pay_time`:
|
|
451
|
+
|
|
452
|
+
```sql
|
|
453
|
+
-- ❌ 用 order_date,会与汇总表不一致(预订餐/隔日退款场景)
|
|
454
|
+
WHERE o.order_date BETWEEN #{param.startDate} AND #{param.endDate}
|
|
455
|
+
|
|
456
|
+
-- ✅ 用 pay_time,与汇总表对齐
|
|
457
|
+
WHERE DATE(o.pay_time) BETWEEN #{param.startDate} AND #{param.endDate}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### order_type 覆盖范围
|
|
461
|
+
|
|
462
|
+
汇总表**不过滤 order_type**,包含全部类型(含 type=4 商超)。定制报表如排除了某些 type,金额会比汇总表少。
|
|
463
|
+
|
|
464
|
+
| order_type | 含义 |
|
|
465
|
+
|-----------|------|
|
|
466
|
+
| 1, 2, 3 | 线上点餐 |
|
|
467
|
+
| 4 | 商超自助结算 |
|
|
468
|
+
| 6, 7, 11, 13, 21, 22 | 线下消费机/手持机 |
|
|
469
|
+
|
|
470
|
+
### 上线前数据验证检查项
|
|
471
|
+
|
|
472
|
+
- [ ] 确认时间字段:与汇总表对齐用 `pay_time`,统计就餐消费用 `order_date`
|
|
473
|
+
- [ ] 确认 `order_type` 覆盖范围(是否包含 type=4 商超)
|
|
474
|
+
- [ ] 选定一个日期,对比定制报表金额与标准报表(`/classify/page`)金额
|
|
475
|
+
- [ ] 验证退款金额方向正确(退款记录的 wallet_amount/subsidy_amount 为负数)
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## 十一、餐次速查
|
|
480
|
+
|
|
481
|
+
| 枚举值 | key | 名称 | 分类 |
|
|
482
|
+
|--------|-----|------|------|
|
|
483
|
+
| MEALTIME_BREAKFAST | 1 | 早餐 | 正餐 |
|
|
484
|
+
| MEALTIME_LUNCH | 2 | 午餐 | 正餐 |
|
|
485
|
+
| MEALTIME_AFTERNOON_TEA | 3 | 下午茶 | 非正餐 |
|
|
486
|
+
| MEALTIME_DINNER | 4 | 晚餐 | 正餐 |
|
|
487
|
+
| MEALTIME_MIDNIGHT_SNACK | 5 | 夜宵 | 非正餐 |
|
|
488
|
+
|
|
489
|
+
```java
|
|
490
|
+
AllocMealtimeTypeEnum.allTypeList(); // [1,2,3,4,5]
|
|
491
|
+
AllocMealtimeTypeEnum.normalTypeList(); // [1,2,4] 正餐
|
|
492
|
+
AllocMealtimeTypeEnum.getValueByKey(1); // "早餐"
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## 十二、边界与延伸
|
|
498
|
+
|
|
499
|
+
本技能覆盖报表开发的完整场景。以下情况请使用其他技能:
|
|
500
|
+
|
|
501
|
+
- 非报表的普通 CRUD 开发 → `leniu-crud-development`
|
|
502
|
+
- MyBatis XML 通用编写规范 → `leniu-java-mybatis`
|
|
503
|
+
- CompletableFuture 线程池高级配置 → `leniu-java-concurrent`
|
|
504
|
+
|
|
505
|
+
本技能的 references 目录按需加载:
|
|
506
|
+
- 新建汇总表(MQ 消费链 + fix 重算)→ 读 `references/customization.md`
|
|
507
|
+
- 需要数据权限过滤 → 读 `references/data-permission.md`
|
|
508
|
+
- 需要 report_order_info 完整字段 → 读 `references/report-tables.md`
|