ai-engineering-init 1.14.1 → 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/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/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/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/bin/index.js +1 -1
- package/package.json +1 -1
|
@@ -115,6 +115,7 @@ const instructions = `## 强制技能激活流程(必须执行)
|
|
|
115
115
|
- add-todo: 添加待办/TODO/任务添加
|
|
116
116
|
- update-status: 更新状态/状态变更
|
|
117
117
|
- skill-creator: 创建技能模板/技能脚手架/skill scaffold
|
|
118
|
+
- leniu-report-scenario: 报表/报表开发/报表查询/报表导出/合计行/totalLine/汇总报表/定制报表/report_order_info/金额处理/分转元/餐次/mealtime/ReportBaseTotalVO/CustomNumberConverter
|
|
118
119
|
|
|
119
120
|
### 步骤 2 - 激活(逐个调用,等待每个完成)
|
|
120
121
|
|
|
@@ -39,6 +39,7 @@ description: |
|
|
|
39
39
|
| 架构 | Controller -> Business -> Service -> Mapper(四层) |
|
|
40
40
|
| 无 DAO 层 | Service 直接注入 Mapper |
|
|
41
41
|
| 对象转换 | `BeanUtil.copyProperties()` (Hutool) |
|
|
42
|
+
| Service 模式 | **两种并存**:简单 CRUD 继承 `ServiceImpl`;业务聚合直接 `@Service` |
|
|
42
43
|
| Entity 基类 | 无基类,自定义审计字段 |
|
|
43
44
|
| 请求封装 | `LeRequest<T>` |
|
|
44
45
|
| 响应封装 | `Page<T>` / `LeResponse<T>` / `void` |
|
|
@@ -110,19 +111,38 @@ private LocalDateTime uptime;
|
|
|
110
111
|
private Integer delFlag; // 1=删除, 2=正常
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
### Service
|
|
114
|
+
### Service 两种模式
|
|
114
115
|
|
|
116
|
+
项目中存在两种 Service 模式,根据业务复杂度选择:
|
|
117
|
+
|
|
118
|
+
**模式 A:简单 CRUD Service**(适用于单表操作,利用 MyBatis-Plus 内置方法)
|
|
115
119
|
```java
|
|
120
|
+
// 接口
|
|
121
|
+
public interface XxxService extends IService<XxxEntity> { }
|
|
122
|
+
|
|
123
|
+
// 实现
|
|
116
124
|
@Slf4j
|
|
117
125
|
@Service
|
|
118
|
-
public class XxxServiceImpl implements XxxService {
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
public class XxxServiceImpl extends ServiceImpl<XxxMapper, XxxEntity> implements XxxService {
|
|
127
|
+
// 继承 ServiceImpl 获得 save/updateById/removeById/page 等内置方法
|
|
128
|
+
// 通过 this.baseMapper 访问 Mapper(父类提供)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
121
131
|
|
|
122
|
-
|
|
132
|
+
**模式 B:业务聚合 Service**(适用于跨表操作、复杂业务编排)
|
|
133
|
+
```java
|
|
134
|
+
@Slf4j
|
|
135
|
+
@Service
|
|
136
|
+
public class XxxService {
|
|
137
|
+
@Autowired
|
|
138
|
+
private XxxMapper xxxMapper; // 直接注入 Mapper,无 DAO 层
|
|
139
|
+
@Autowired
|
|
140
|
+
private YyyMapper yyyMapper; // 可注入多个 Mapper
|
|
123
141
|
}
|
|
124
142
|
```
|
|
125
143
|
|
|
144
|
+
**选择建议**:新建简单单表 CRUD 用模式 A;涉及多表联查、报表、业务编排用模式 B。
|
|
145
|
+
|
|
126
146
|
### Controller 请求封装
|
|
127
147
|
|
|
128
148
|
```java
|
|
@@ -366,7 +386,6 @@ private String createBy; // -> crby
|
|
|
366
386
|
entity.setDelFlag(0); // -> setDelFlag(2) 表示正常
|
|
367
387
|
throw new ServiceException("..."); // -> throw new LeException("...")
|
|
368
388
|
MapstructUtils.convert(src, Dst.class); // -> BeanUtil.copyProperties(src, Dst.class)
|
|
369
|
-
extends ServiceImpl<XxxMapper, Xxx> // -> implements XxxService(不继承)
|
|
370
389
|
@Resource private XxxDao xxxDao; // -> @Resource private XxxMapper xxxMapper
|
|
371
390
|
// XML 放 resources/mapper/ // -> 与 Java 同目录
|
|
372
391
|
return null; // -> return Collections.emptyList()
|
|
@@ -377,7 +396,7 @@ return null; // -> return Collections.emptyList()
|
|
|
377
396
|
## 生成前检查清单
|
|
378
397
|
|
|
379
398
|
- [ ] 包名 `net.xnzn.core.*`
|
|
380
|
-
- [ ] Service
|
|
399
|
+
- [ ] Service 模式选择:简单 CRUD 用模式 A(继承 ServiceImpl);业务聚合用模式 B(直接 @Service)
|
|
381
400
|
- [ ] Service 直接注入 Mapper(无 DAO)
|
|
382
401
|
- [ ] 审计字段 crby/crtime/upby/uptime
|
|
383
402
|
- [ ] delFlag: 1=删除, 2=正常
|
|
@@ -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`
|