ai-engineering-init 1.16.2 → 1.16.4
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/code-patterns/SKILL.md +0 -119
- package/.claude/skills/codex-code-review/SKILL.md +0 -39
- package/.claude/skills/fix-bug/SKILL.md +57 -0
- package/.claude/skills/leniu-code-patterns/SKILL.md +2 -179
- package/.claude/skills/leniu-crud-development/SKILL.md +7 -26
- package/.claude/skills/leniu-java-mybatis/SKILL.md +16 -25
- package/.claude/skills/leniu-report-scenario/references/amount-handling.md +448 -0
- package/.claude/skills/leniu-report-scenario/references/analysis-module.md +64 -0
- package/.claude/skills/leniu-report-scenario/references/customization-table-fields.md +93 -0
- package/.claude/skills/leniu-report-scenario/references/export.md +553 -0
- package/.claude/skills/leniu-report-scenario/references/mealtime.md +197 -0
- package/.claude/skills/leniu-report-scenario/references/query-param.md +274 -0
- package/.claude/skills/leniu-report-scenario/references/standard-customization.md +112 -0
- package/.claude/skills/leniu-report-scenario/references/standard-table-fields.md +113 -0
- package/.claude/skills/leniu-report-scenario/references/total-line.md +179 -0
- package/.claude/skills/loki-log-query/SKILL.md +30 -33
- package/.claude/skills/mysql-debug/SKILL.md +17 -35
- package/.codex/skills/code-patterns/SKILL.md +0 -119
- package/.codex/skills/fix-bug/SKILL.md +57 -0
- package/.codex/skills/leniu-code-patterns/SKILL.md +2 -179
- package/.codex/skills/leniu-crud-development/SKILL.md +7 -26
- package/.codex/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/.codex/skills/leniu-java-mybatis/SKILL.md +16 -25
- package/.codex/skills/leniu-report-scenario/references/amount-handling.md +448 -0
- package/.codex/skills/leniu-report-scenario/references/analysis-module.md +64 -0
- package/.codex/skills/leniu-report-scenario/references/customization-table-fields.md +93 -0
- package/.codex/skills/leniu-report-scenario/references/export.md +553 -0
- package/.codex/skills/leniu-report-scenario/references/mealtime.md +197 -0
- package/.codex/skills/leniu-report-scenario/references/query-param.md +274 -0
- package/.codex/skills/leniu-report-scenario/references/standard-customization.md +112 -0
- package/.codex/skills/leniu-report-scenario/references/standard-table-fields.md +113 -0
- package/.codex/skills/leniu-report-scenario/references/total-line.md +179 -0
- package/.codex/skills/loki-log-query/SKILL.md +55 -25
- package/.codex/skills/mysql-debug/SKILL.md +12 -6
- package/.cursor/skills/code-patterns/SKILL.md +0 -119
- package/.cursor/skills/fix-bug/SKILL.md +57 -0
- package/.cursor/skills/leniu-code-patterns/SKILL.md +2 -179
- package/.cursor/skills/leniu-crud-development/SKILL.md +7 -26
- package/.cursor/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/.cursor/skills/leniu-java-mybatis/SKILL.md +16 -25
- package/.cursor/skills/leniu-report-scenario/references/amount-handling.md +448 -0
- package/.cursor/skills/leniu-report-scenario/references/analysis-module.md +64 -0
- package/.cursor/skills/leniu-report-scenario/references/customization-table-fields.md +93 -0
- package/.cursor/skills/leniu-report-scenario/references/export.md +553 -0
- package/.cursor/skills/leniu-report-scenario/references/mealtime.md +197 -0
- package/.cursor/skills/leniu-report-scenario/references/query-param.md +274 -0
- package/.cursor/skills/leniu-report-scenario/references/standard-customization.md +112 -0
- package/.cursor/skills/leniu-report-scenario/references/standard-table-fields.md +113 -0
- package/.cursor/skills/leniu-report-scenario/references/total-line.md +179 -0
- package/.cursor/skills/loki-log-query/SKILL.md +30 -33
- package/.cursor/skills/mysql-debug/SKILL.md +20 -36
- package/CLAUDE.md +34 -0
- package/package.json +1 -1
- package/src/skills/fix-bug/SKILL.md +57 -0
|
@@ -394,187 +394,10 @@ public class OrderService {
|
|
|
394
394
|
}
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
## 数据类型规范
|
|
398
|
-
|
|
399
|
-
### 布尔字段命名
|
|
400
|
-
|
|
401
|
-
```java
|
|
402
|
-
// ❌ 错误:前缀冗余、类型错误
|
|
403
|
-
private Integer ifNarrow;
|
|
404
|
-
private Integer isEnabled;
|
|
405
|
-
|
|
406
|
-
// ✅ 正确:Boolean 类型,无前缀
|
|
407
|
-
private Boolean narrow; // getter → isNarrow()
|
|
408
|
-
private Boolean enabled; // getter → isEnabled()
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### 枚举字段标注
|
|
412
|
-
|
|
413
|
-
```java
|
|
414
|
-
// ❌ 错误:只靠注释 @see
|
|
415
|
-
/** @see AccTradeTypeEnum */
|
|
416
|
-
private Integer tradeType;
|
|
417
|
-
|
|
418
|
-
// ✅ 方案一:VO/DTO 用枚举类型(配合 @JsonValue)
|
|
419
|
-
private AccTradeTypeEnum tradeType;
|
|
420
|
-
|
|
421
|
-
// ✅ 方案二:@ApiModelProperty 标注合法值
|
|
422
|
-
@ApiModelProperty(value = "操作类型:1-充值 2-消费 3-退款", allowableValues = "1,2,3")
|
|
423
|
-
private Integer tradeType;
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
## MyBatis-Plus 安全规范
|
|
427
|
-
|
|
428
|
-
### selectOne 必须有唯一保障
|
|
429
|
-
|
|
430
|
-
```java
|
|
431
|
-
// ❌ 危险:多条记录时抛 TooManyResultsException
|
|
432
|
-
Entity entity = mapper.selectOne(wrapper);
|
|
433
|
-
|
|
434
|
-
// ✅ 方案一:LIMIT 1
|
|
435
|
-
Entity entity = mapper.selectOne(wrapper.last("LIMIT 1"));
|
|
436
|
-
|
|
437
|
-
// ✅ 方案二:selectList 取第一条
|
|
438
|
-
List<Entity> list = mapper.selectList(wrapper);
|
|
439
|
-
Entity entity = CollUtil.isNotEmpty(list) ? list.get(0) : null;
|
|
440
|
-
|
|
441
|
-
// ✅ 方案三:确保有唯一索引(注释说明)
|
|
442
|
-
// 唯一索引:UNIQUE KEY (order_no, del_flag)
|
|
443
|
-
Entity entity = mapper.selectOne(wrapper);
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
### 存在性判断用 EXISTS,禁用 selectCount
|
|
447
|
-
|
|
448
|
-
```java
|
|
449
|
-
// ❌ 低效:100万行表 ~200ms
|
|
450
|
-
Long count = mapper.selectCount(wrapper);
|
|
451
|
-
if (count > 0) { ... }
|
|
452
|
-
|
|
453
|
-
// ✅ 高效:~2ms(MyBatis-Plus 3.5.4+)
|
|
454
|
-
boolean exists = mapper.exists(wrapper);
|
|
455
|
-
|
|
456
|
-
// ✅ 或 selectList + LIMIT 1
|
|
457
|
-
boolean exists = CollUtil.isNotEmpty(mapper.selectList(wrapper.last("LIMIT 1")));
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
### Wrapper 嵌套不超过 2 层
|
|
461
|
-
|
|
462
|
-
```java
|
|
463
|
-
// ❌ 过于复杂的 Wrapper
|
|
464
|
-
wrapper.and(w -> w.eq(A::getType, type)
|
|
465
|
-
.or(q -> q.eq(A::getType, 0))
|
|
466
|
-
.or(!PersonTypeEnum.LABOUR.getKey().equals(type),
|
|
467
|
-
q -> q.eq(A::getType, PSN_TYPE_SHARED)));
|
|
468
|
-
|
|
469
|
-
// ✅ 复杂查询写到 XML 中
|
|
470
|
-
List<Entity> list = mapper.selectByTypeCondition(type, isLabour);
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
### 禁止 SELECT *
|
|
474
|
-
|
|
475
|
-
```xml
|
|
476
|
-
<!-- ❌ 禁止 -->
|
|
477
|
-
<select id="selectAll">SELECT * FROM t_order WHERE del_flag = 2</select>
|
|
478
|
-
|
|
479
|
-
<!-- ✅ 明确列出字段 -->
|
|
480
|
-
<select id="selectAll">SELECT id, order_no, amount, status, crtime FROM t_order WHERE del_flag = 2</select>
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
## Redis 使用规范
|
|
484
|
-
|
|
485
|
-
### 禁止 KEYS 命令
|
|
486
|
-
|
|
487
|
-
```java
|
|
488
|
-
// ❌ 严禁:KEYS 阻塞 Redis
|
|
489
|
-
Set<Object> keys = keysByPattern(pattern);
|
|
490
|
-
Set<Object> keys = redisTemplate.keys(pattern);
|
|
491
|
-
|
|
492
|
-
// ✅ 使用 Redisson deleteByPattern(内部 SCAN + UNLINK)
|
|
493
|
-
RedissonClient redisson = SpringUtil.getBean(RedissonClient.class);
|
|
494
|
-
redisson.getKeys().deleteByPattern(keyPattern);
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
## Optional 使用规范
|
|
498
|
-
|
|
499
|
-
```java
|
|
500
|
-
// ❌ 错误:of() 不接受 null
|
|
501
|
-
Optional.of(value).orElse(defaultValue);
|
|
502
|
-
|
|
503
|
-
// ✅ 正确:ofNullable()
|
|
504
|
-
Optional.ofNullable(value).orElse(defaultValue);
|
|
505
|
-
|
|
506
|
-
// ✅ 链式安全转换
|
|
507
|
-
Optional.ofNullable(model.getReserveRate())
|
|
508
|
-
.map(BigDecimal::new)
|
|
509
|
-
.orElse(BigDecimal.ZERO);
|
|
510
|
-
|
|
511
|
-
// ❌ 禁止作为方法参数或类字段
|
|
512
|
-
// ✅ 允许作为方法返回值
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
## @Transactional 规范
|
|
516
|
-
|
|
517
|
-
```java
|
|
518
|
-
// ❌ 默认只回滚 RuntimeException
|
|
519
|
-
@Transactional
|
|
520
|
-
public void createOrder() { ... }
|
|
521
|
-
|
|
522
|
-
// ✅ 显式指定 rollbackFor
|
|
523
|
-
@Transactional(rollbackFor = Exception.class)
|
|
524
|
-
public void createOrder() { ... }
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
- 事务方法不要 try-catch 吞掉异常
|
|
528
|
-
- 只读查询不加 `@Transactional`
|
|
529
|
-
|
|
530
|
-
## 业务逻辑分层规范
|
|
531
|
-
|
|
532
|
-
```java
|
|
533
|
-
// ❌ 错误:业务判断混在数据操作中
|
|
534
|
-
public void processOrder(Long orderId) {
|
|
535
|
-
OrderInfo order = orderMapper.selectById(orderId);
|
|
536
|
-
if (order.getStatus() == 1 && order.getPayTime() != null
|
|
537
|
-
&& ChronoUnit.HOURS.between(order.getPayTime(), LocalDateTime.now()) < 24) {
|
|
538
|
-
order.setStatus(2);
|
|
539
|
-
orderMapper.updateById(order);
|
|
540
|
-
accWalletService.deduct(order.getCustId(), order.getAmount());
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// ✅ 正确:分层清晰
|
|
545
|
-
public void processOrder(Long orderId) {
|
|
546
|
-
OrderInfo order = orderMapper.selectById(orderId);
|
|
547
|
-
if (ObjectUtil.isNull(order)) {
|
|
548
|
-
throw new LeException(I18n.getMessage("order_not_found"));
|
|
549
|
-
}
|
|
550
|
-
checkCanProcess(order); // 业务校验(独立方法)
|
|
551
|
-
order.markAsProcessed(); // 状态变更(Entity 方法封装)
|
|
552
|
-
orderMapper.updateById(order);
|
|
553
|
-
afterOrderProcessed(order); // 后续动作(独立方法)
|
|
554
|
-
}
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
| 层 | 职责 | 不应做的 |
|
|
558
|
-
|----|------|---------|
|
|
559
|
-
| Controller | 参数接收、格式转换 | 不含业务判断 |
|
|
560
|
-
| Business | 业务编排、跨 Service 协调 | 不直接操作 Mapper |
|
|
561
|
-
| Service | 单表 CRUD、单表事务 | 不含跨表业务逻辑 |
|
|
562
|
-
| Mapper | SQL 映射 | 不含业务逻辑 |
|
|
563
|
-
|
|
564
|
-
## TODO 管理规范
|
|
565
|
-
|
|
566
|
-
```java
|
|
567
|
-
// ❌ 错误
|
|
568
|
-
// TODO 修改一下
|
|
569
|
-
|
|
570
|
-
// ✅ 正确
|
|
571
|
-
// TODO(@陈沈杰, 2026-03-20, #TASK-1234): 移动端 AppId 赋值逻辑待产品确认
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
- 不用的代码直接删除,不要注释保留
|
|
575
|
-
|
|
576
397
|
## 通用代码规范
|
|
577
398
|
|
|
399
|
+
无论使用哪种项目架构,以下规范都是通用的:
|
|
400
|
+
|
|
578
401
|
1. **禁止使用 `SELECT *`**:明确指定字段
|
|
579
402
|
2. **使用参数化查询**:`#{}` 而非 `${}`
|
|
580
403
|
3. **异常必须处理**:不能吞掉异常
|
|
@@ -39,7 +39,6 @@ description: |
|
|
|
39
39
|
| 架构 | Controller -> Business -> Service -> Mapper(四层) |
|
|
40
40
|
| 无 DAO 层 | Service 直接注入 Mapper |
|
|
41
41
|
| 对象转换 | `BeanUtil.copyProperties()` (Hutool) |
|
|
42
|
-
| Service 模式 | **两种并存**:简单 CRUD 继承 `ServiceImpl`;业务聚合直接 `@Service` |
|
|
43
42
|
| Entity 基类 | 无基类,自定义审计字段 |
|
|
44
43
|
| 请求封装 | `LeRequest<T>` |
|
|
45
44
|
| 响应封装 | `Page<T>` / `LeResponse<T>` / `void` |
|
|
@@ -111,38 +110,19 @@ private LocalDateTime uptime;
|
|
|
111
110
|
private Integer delFlag; // 1=删除, 2=正常
|
|
112
111
|
```
|
|
113
112
|
|
|
114
|
-
### Service
|
|
113
|
+
### Service 注入模式
|
|
115
114
|
|
|
116
|
-
项目中存在两种 Service 模式,根据业务复杂度选择:
|
|
117
|
-
|
|
118
|
-
**模式 A:简单 CRUD Service**(适用于单表操作,利用 MyBatis-Plus 内置方法)
|
|
119
|
-
```java
|
|
120
|
-
// 接口
|
|
121
|
-
public interface XxxService extends IService<XxxEntity> { }
|
|
122
|
-
|
|
123
|
-
// 实现
|
|
124
|
-
@Slf4j
|
|
125
|
-
@Service
|
|
126
|
-
public class XxxServiceImpl extends ServiceImpl<XxxMapper, XxxEntity> implements XxxService {
|
|
127
|
-
// 继承 ServiceImpl 获得 save/updateById/removeById/page 等内置方法
|
|
128
|
-
// 通过 this.baseMapper 访问 Mapper(父类提供)
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**模式 B:业务聚合 Service**(适用于跨表操作、复杂业务编排)
|
|
133
115
|
```java
|
|
134
116
|
@Slf4j
|
|
135
117
|
@Service
|
|
136
|
-
public class XxxService {
|
|
137
|
-
@
|
|
118
|
+
public class XxxServiceImpl implements XxxService {
|
|
119
|
+
@Resource
|
|
138
120
|
private XxxMapper xxxMapper; // 直接注入 Mapper,无 DAO 层
|
|
139
|
-
|
|
140
|
-
|
|
121
|
+
|
|
122
|
+
// 不继承 ServiceImpl,只实现接口
|
|
141
123
|
}
|
|
142
124
|
```
|
|
143
125
|
|
|
144
|
-
**选择建议**:新建简单单表 CRUD 用模式 A;涉及多表联查、报表、业务编排用模式 B。
|
|
145
|
-
|
|
146
126
|
### Controller 请求封装
|
|
147
127
|
|
|
148
128
|
```java
|
|
@@ -386,6 +366,7 @@ private String createBy; // -> crby
|
|
|
386
366
|
entity.setDelFlag(0); // -> setDelFlag(2) 表示正常
|
|
387
367
|
throw new ServiceException("..."); // -> throw new LeException("...")
|
|
388
368
|
MapstructUtils.convert(src, Dst.class); // -> BeanUtil.copyProperties(src, Dst.class)
|
|
369
|
+
extends ServiceImpl<XxxMapper, Xxx> // -> implements XxxService(不继承)
|
|
389
370
|
@Resource private XxxDao xxxDao; // -> @Resource private XxxMapper xxxMapper
|
|
390
371
|
// XML 放 resources/mapper/ // -> 与 Java 同目录
|
|
391
372
|
return null; // -> return Collections.emptyList()
|
|
@@ -396,7 +377,7 @@ return null; // -> return Collections.emptyList()
|
|
|
396
377
|
## 生成前检查清单
|
|
397
378
|
|
|
398
379
|
- [ ] 包名 `net.xnzn.core.*`
|
|
399
|
-
- [ ] Service
|
|
380
|
+
- [ ] Service 只实现接口,不继承基类
|
|
400
381
|
- [ ] Service 直接注入 Mapper(无 DAO)
|
|
401
382
|
- [ ] 审计字段 crby/crtime/upby/uptime
|
|
402
383
|
- [ ] delFlag: 1=删除, 2=正常
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: leniu-java-amount-handling
|
|
3
|
+
description: |
|
|
4
|
+
leniu-tengyun-core 项目金额处理规范。金融系统中金额以分(Long 类型)存储,展示时转换为元。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- VO/Entity 类含金额字段(amountFen/amountYuan)
|
|
8
|
+
- MyBatis XML 查询含金额字段
|
|
9
|
+
- Excel 导出含金额列,需分转元
|
|
10
|
+
- 报表合计查询含金额汇总
|
|
11
|
+
|
|
12
|
+
触发词:金额处理、分转元、元转分、Long金额、money、fen、BigDecimal金额、金额字段
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# leniu-tengyun-core 金额处理规范
|
|
16
|
+
|
|
17
|
+
## 两种金额存储模式
|
|
18
|
+
|
|
19
|
+
项目中存在两种金额存储模式,根据业务模块选择:
|
|
20
|
+
|
|
21
|
+
### 模式 A:Long(分)→ BigDecimal(元)[钱包/账户模块]
|
|
22
|
+
|
|
23
|
+
适用于余额、充值、补贴等精度要求高的模块:
|
|
24
|
+
|
|
25
|
+
- **数据库**:存储为 Long(分)
|
|
26
|
+
- **Entity**:使用 Long 类型
|
|
27
|
+
- **VO**:使用 BigDecimal 类型(MyBatis 自动转换分→元)
|
|
28
|
+
- **Excel 导出**:使用 `CustomNumberConverter` 转换器
|
|
29
|
+
|
|
30
|
+
### 模式 B:BigDecimal(分)[订单/报表模块]
|
|
31
|
+
|
|
32
|
+
适用于订单金额、报表汇总等复杂计算模块:
|
|
33
|
+
|
|
34
|
+
- **数据库**:存储为 BigDecimal(值为分,如 10000 = 100.00元)
|
|
35
|
+
- **Entity**:直接使用 BigDecimal 类型(以分为单位)
|
|
36
|
+
- **VO**:同样使用 BigDecimal(字段注释需标注单位)
|
|
37
|
+
- **SQL SUM**:直接 `SUM(amount)`,不需要 /100
|
|
38
|
+
|
|
39
|
+
> 参考:`OrderInfo.payableAmount`(BigDecimal,以分为单位)
|
|
40
|
+
|
|
41
|
+
## 金额类型速查
|
|
42
|
+
|
|
43
|
+
| 类型 | 用途 | 示例 |
|
|
44
|
+
|------|------|------|
|
|
45
|
+
| `Long` | 钱包/账户存储(分) | `private Long orderAmount; // 10000 = 100.00元` |
|
|
46
|
+
| `BigDecimal` | 订单/报表存储(分) | `private BigDecimal payableAmount; // 10000 = 100.00元` |
|
|
47
|
+
| `BigDecimal` | VO 展示(元,模式A) | `private BigDecimal amount; // 100.00` |
|
|
48
|
+
|
|
49
|
+
## Entity(模式A:Long存储)
|
|
50
|
+
|
|
51
|
+
```java
|
|
52
|
+
import com.baomidou.mybatisplus.annotation.*;
|
|
53
|
+
import io.swagger.annotations.ApiModel;
|
|
54
|
+
import io.swagger.annotations.ApiModelProperty;
|
|
55
|
+
import lombok.Data;
|
|
56
|
+
|
|
57
|
+
@Data
|
|
58
|
+
@TableName(value = "wallet_table", autoResultMap = true)
|
|
59
|
+
public class WalletEntity {
|
|
60
|
+
|
|
61
|
+
@TableId
|
|
62
|
+
@ApiModelProperty(value = "钱包ID")
|
|
63
|
+
private Long id;
|
|
64
|
+
|
|
65
|
+
@ApiModelProperty(value = "余额(分)")
|
|
66
|
+
private Long balance;
|
|
67
|
+
|
|
68
|
+
@ApiModelProperty(value = "充值金额(分)")
|
|
69
|
+
private Long rechargeAmount;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**要点**:
|
|
74
|
+
- Entity 使用 `Long` 类型存储金额(分)
|
|
75
|
+
- 字段注释明确标注"(分)"
|
|
76
|
+
|
|
77
|
+
## Entity(模式B:BigDecimal存储,适用订单模块)
|
|
78
|
+
|
|
79
|
+
```java
|
|
80
|
+
@Data
|
|
81
|
+
@TableName("order_info")
|
|
82
|
+
public class OrderInfo {
|
|
83
|
+
|
|
84
|
+
@ApiModelProperty(value = "应付金额(分)")
|
|
85
|
+
private BigDecimal payableAmount;
|
|
86
|
+
|
|
87
|
+
@ApiModelProperty(value = "实付金额(分)")
|
|
88
|
+
private BigDecimal realAmount;
|
|
89
|
+
|
|
90
|
+
@ApiModelProperty(value = "优惠金额(分)")
|
|
91
|
+
private BigDecimal discountsAmount;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**要点**:
|
|
96
|
+
- 字段类型为 BigDecimal,但值以分为单位存储
|
|
97
|
+
- 不需要 MyBatis 类型转换
|
|
98
|
+
|
|
99
|
+
## VO(响应层)
|
|
100
|
+
|
|
101
|
+
```java
|
|
102
|
+
import com.alibaba.excel.annotation.ExcelProperty;
|
|
103
|
+
import io.swagger.annotations.ApiModel;
|
|
104
|
+
import io.swagger.annotations.ApiModelProperty;
|
|
105
|
+
import lombok.Data;
|
|
106
|
+
import lombok.experimental.Accessors;
|
|
107
|
+
import net.xnzn.core.common.export.converter.CustomNumberConverter;
|
|
108
|
+
|
|
109
|
+
import java.math.BigDecimal;
|
|
110
|
+
|
|
111
|
+
@Data
|
|
112
|
+
@Accessors(chain = true)
|
|
113
|
+
@ApiModel("订单信息")
|
|
114
|
+
public class OrderVO {
|
|
115
|
+
|
|
116
|
+
@ApiModelProperty("订单ID")
|
|
117
|
+
private Long id;
|
|
118
|
+
|
|
119
|
+
@ApiModelProperty("订单金额(元)")
|
|
120
|
+
@ExcelProperty(value = "订单金额(元)", converter = CustomNumberConverter.class)
|
|
121
|
+
private BigDecimal orderAmount;
|
|
122
|
+
|
|
123
|
+
@ApiModelProperty("优惠金额(元)")
|
|
124
|
+
@ExcelProperty(value = "优惠金额(元)", converter = CustomNumberConverter.class)
|
|
125
|
+
private BigDecimal discountAmount;
|
|
126
|
+
|
|
127
|
+
@ApiModelProperty("实付金额(元)")
|
|
128
|
+
@ExcelProperty(value = "实付金额(元)", converter = CustomNumberConverter.class)
|
|
129
|
+
private BigDecimal payAmount;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**要点**:
|
|
134
|
+
- VO 使用 `BigDecimal` 类型(元)
|
|
135
|
+
- Excel 导出必须使用 `converter = CustomNumberConverter.class`
|
|
136
|
+
- MyBatis 会自动将 Long 分转换为 BigDecimal 元
|
|
137
|
+
|
|
138
|
+
## MyBatis XML 查询
|
|
139
|
+
|
|
140
|
+
### 列表查询
|
|
141
|
+
|
|
142
|
+
```xml
|
|
143
|
+
<select id="listOrders" resultType="OrderVO">
|
|
144
|
+
SELECT
|
|
145
|
+
order_id,
|
|
146
|
+
order_amount, -- ❌ 不要 /100.0
|
|
147
|
+
discount_amount, -- ❌ 不要 /100.0
|
|
148
|
+
pay_amount -- ❌ 不要 /100.0
|
|
149
|
+
FROM order_table
|
|
150
|
+
<where>...</where>
|
|
151
|
+
ORDER BY crtime DESC
|
|
152
|
+
</select>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 合计查询
|
|
156
|
+
|
|
157
|
+
```xml
|
|
158
|
+
<select id="getOrderTotal" resultType="OrderVO">
|
|
159
|
+
SELECT
|
|
160
|
+
SUM(order_amount) AS order_amount, -- ❌ 不要 /100.0
|
|
161
|
+
SUM(discount_amount) AS discount_amount,-- ❌ 不要 /100.0
|
|
162
|
+
SUM(pay_amount) AS pay_amount -- ❌ 不要 /100.0
|
|
163
|
+
FROM order_table
|
|
164
|
+
<where>...</where>
|
|
165
|
+
</select>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**重要**:
|
|
169
|
+
- 永远不要在 SQL 中使用 `/100.0`
|
|
170
|
+
- 合计查询只返回数值字段,不返回非数值字段(如日期、名称)
|
|
171
|
+
|
|
172
|
+
## 金额工具类
|
|
173
|
+
|
|
174
|
+
### AmountUtil(分转元)
|
|
175
|
+
|
|
176
|
+
```java
|
|
177
|
+
import java.math.BigDecimal;
|
|
178
|
+
import java.math.RoundingMode;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 分与元金额转换
|
|
182
|
+
*/
|
|
183
|
+
public class AmountUtil {
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 分转元(String)
|
|
187
|
+
*/
|
|
188
|
+
public static String fen2YuanStr(Integer fen) {
|
|
189
|
+
if (fen == null) {
|
|
190
|
+
return "0.00";
|
|
191
|
+
}
|
|
192
|
+
BigDecimal amountFen = new BigDecimal(fen);
|
|
193
|
+
BigDecimal amountYuan = amountFen.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
|
|
194
|
+
return amountYuan.toPlainString();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 分转元(String 重载)
|
|
199
|
+
*/
|
|
200
|
+
public static String fen2YuanStr(String fen) {
|
|
201
|
+
if (fen == null || fen.isEmpty()) {
|
|
202
|
+
return "0.00";
|
|
203
|
+
}
|
|
204
|
+
BigDecimal amountFen = new BigDecimal(fen);
|
|
205
|
+
BigDecimal amountYuan = amountFen.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
|
|
206
|
+
return amountYuan.toPlainString();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 分转元(BigDecimal)
|
|
211
|
+
*/
|
|
212
|
+
public static BigDecimal fen2Yuan(Integer fen) {
|
|
213
|
+
if (fen == null) {
|
|
214
|
+
return BigDecimal.ZERO;
|
|
215
|
+
}
|
|
216
|
+
BigDecimal amountFen = new BigDecimal(fen);
|
|
217
|
+
return amountFen.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 分转元(Long)
|
|
222
|
+
*/
|
|
223
|
+
public static BigDecimal fen2Yuan(Long fen) {
|
|
224
|
+
if (fen == null) {
|
|
225
|
+
return BigDecimal.ZERO;
|
|
226
|
+
}
|
|
227
|
+
BigDecimal amountFen = new BigDecimal(fen);
|
|
228
|
+
return amountFen.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 元转分
|
|
233
|
+
*/
|
|
234
|
+
public static Integer yuan2Fen(String yuan) {
|
|
235
|
+
if (yuan == null || yuan.isEmpty()) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
BigDecimal balance = new BigDecimal(yuan);
|
|
239
|
+
return Integer.parseInt(balance.multiply(new BigDecimal("100"))
|
|
240
|
+
.setScale(0, RoundingMode.HALF_UP)
|
|
241
|
+
.toPlainString());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 元转分(BigDecimal)
|
|
246
|
+
*/
|
|
247
|
+
public static Integer yuan2Fen(BigDecimal yuan) {
|
|
248
|
+
if (yuan == null) {
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
return yuan.multiply(new BigDecimal("100"))
|
|
252
|
+
.setScale(0, RoundingMode.HALF_UP)
|
|
253
|
+
.intValue();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 使用示例
|
|
259
|
+
|
|
260
|
+
```java
|
|
261
|
+
// 分转元
|
|
262
|
+
Integer fenAmount = 10000; // 10000分
|
|
263
|
+
BigDecimal yuanAmount = AmountUtil.fen2Yuan(fenAmount); // 100.00元
|
|
264
|
+
|
|
265
|
+
// 元转分
|
|
266
|
+
BigDecimal yuan = new BigDecimal("99.99");
|
|
267
|
+
Integer fenAmount = AmountUtil.yuan2Fen(yuan); // 9999分
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 常见场景
|
|
271
|
+
|
|
272
|
+
### 场景1:订单报表 VO
|
|
273
|
+
|
|
274
|
+
```java
|
|
275
|
+
@Data
|
|
276
|
+
@Accessors(chain = true)
|
|
277
|
+
@ApiModel("订单报表")
|
|
278
|
+
public class OrderReportVO {
|
|
279
|
+
|
|
280
|
+
@ExcelProperty(value = "月度", order = 1)
|
|
281
|
+
@ApiModelProperty("月度")
|
|
282
|
+
private String dateMonth;
|
|
283
|
+
|
|
284
|
+
@ExcelProperty(value = "订单总额(元)", order = 2, converter = CustomNumberConverter.class)
|
|
285
|
+
@ApiModelProperty("订单总额(元)")
|
|
286
|
+
private BigDecimal totalAmount;
|
|
287
|
+
|
|
288
|
+
@ExcelProperty(value = "订单数量", order = 3)
|
|
289
|
+
@ApiModelProperty("订单数量")
|
|
290
|
+
private Integer totalCount;
|
|
291
|
+
|
|
292
|
+
@ExcelProperty(value = "平均金额(元)", order = 4, converter = CustomNumberConverter.class)
|
|
293
|
+
@ApiModelProperty("平均金额(元)")
|
|
294
|
+
private BigDecimal avgAmount;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### 场景2:工资汇总 VO
|
|
299
|
+
|
|
300
|
+
```java
|
|
301
|
+
@Data
|
|
302
|
+
@Accessors(chain = true)
|
|
303
|
+
@ApiModel("工资汇总")
|
|
304
|
+
public class SalarySummaryVO {
|
|
305
|
+
|
|
306
|
+
@ExcelProperty(value = "月度", order = 1)
|
|
307
|
+
@ApiModelProperty("月度")
|
|
308
|
+
private String dateMonth;
|
|
309
|
+
|
|
310
|
+
@ExcelProperty(value = "基本工资(元)", order = 2, converter = CustomNumberConverter.class)
|
|
311
|
+
@ApiModelProperty("基本工资(元)")
|
|
312
|
+
private BigDecimal basicSalary;
|
|
313
|
+
|
|
314
|
+
@ExcelProperty(value = "绩效工资(元)", order = 3, converter = CustomNumberConverter.class)
|
|
315
|
+
@ApiModelProperty("绩效工资(元)")
|
|
316
|
+
private BigDecimal performanceSalary;
|
|
317
|
+
|
|
318
|
+
@ExcelProperty(value = "应发工资(元)", order = 4, converter = CustomNumberConverter.class)
|
|
319
|
+
@ApiModelProperty("应发工资(元)")
|
|
320
|
+
private BigDecimal totalSalary;
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 场景3:合计行 SQL
|
|
325
|
+
|
|
326
|
+
```xml
|
|
327
|
+
<!-- 列表查询 -->
|
|
328
|
+
<select id="pageSalary" resultType="SalarySummaryVO">
|
|
329
|
+
SELECT
|
|
330
|
+
date_month,
|
|
331
|
+
staff_count,
|
|
332
|
+
basic_salary,
|
|
333
|
+
performance_salary,
|
|
334
|
+
total_salary
|
|
335
|
+
FROM salary_summary
|
|
336
|
+
<where>...</where>
|
|
337
|
+
ORDER BY date_month DESC
|
|
338
|
+
</select>
|
|
339
|
+
|
|
340
|
+
<!-- 合计查询:只返回数值字段 -->
|
|
341
|
+
<select id="getSalaryTotal" resultType="SalarySummaryVO">
|
|
342
|
+
SELECT
|
|
343
|
+
SUM(staff_count) AS staff_count,
|
|
344
|
+
SUM(basic_salary) AS basic_salary,
|
|
345
|
+
SUM(performance_salary) AS performance_salary,
|
|
346
|
+
SUM(total_salary) AS total_salary
|
|
347
|
+
FROM salary_summary
|
|
348
|
+
<where>...</where>
|
|
349
|
+
</select>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 场景4:平均值计算
|
|
353
|
+
|
|
354
|
+
```xml
|
|
355
|
+
<!-- 平均值计算(除零处理) -->
|
|
356
|
+
CASE
|
|
357
|
+
WHEN SUM(staff_count) = 0 THEN 0
|
|
358
|
+
ELSE SUM(total_salary) / SUM(staff_count)
|
|
359
|
+
END AS avgSalary
|
|
360
|
+
|
|
361
|
+
<!-- 按维度平均 -->
|
|
362
|
+
CASE
|
|
363
|
+
WHEN SUM(staff_count) = 0 THEN 0
|
|
364
|
+
ELSE SUM(total_salary) / COUNT(DISTINCT tenant_id)
|
|
365
|
+
END AS avgSalary
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## 数据流
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
数据库(Long/分) → MyBatis → VO(BigDecimal/元) → 前端(自动 /100)
|
|
372
|
+
↓
|
|
373
|
+
Excel(converter /100)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Excel 导出
|
|
377
|
+
|
|
378
|
+
### 转换器
|
|
379
|
+
|
|
380
|
+
```java
|
|
381
|
+
@ExcelProperty(value = "订单金额(元)", converter = CustomNumberConverter.class)
|
|
382
|
+
private BigDecimal orderAmount;
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 必要的导入
|
|
386
|
+
|
|
387
|
+
```java
|
|
388
|
+
import com.alibaba.excel.annotation.ExcelProperty;
|
|
389
|
+
import net.xnzn.core.common.export.converter.CustomNumberConverter;
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## 常见错误
|
|
393
|
+
|
|
394
|
+
### 错误1:在 SQL 中使用 /100.0
|
|
395
|
+
|
|
396
|
+
```xml
|
|
397
|
+
<!-- ❌ 错误:在 SQL 中除以 100 -->
|
|
398
|
+
SELECT
|
|
399
|
+
order_amount / 100.0 AS order_amount
|
|
400
|
+
FROM order_table
|
|
401
|
+
|
|
402
|
+
<!-- ✅ 正确:不进行除法,MyBatis 自动转换 -->
|
|
403
|
+
SELECT
|
|
404
|
+
order_amount AS order_amount
|
|
405
|
+
FROM order_table
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 错误2:钱包模块 Entity 使用 BigDecimal(模式A适用)
|
|
409
|
+
|
|
410
|
+
```java
|
|
411
|
+
// ❌ 错误:钱包/账户模块 Entity 使用 BigDecimal(应用 Long)
|
|
412
|
+
@Data
|
|
413
|
+
public class WalletEntity {
|
|
414
|
+
@ApiModelProperty("余额(元)")
|
|
415
|
+
private BigDecimal balance;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ✅ 正确:钱包/账户模块 Entity 使用 Long(分)
|
|
419
|
+
@Data
|
|
420
|
+
public class WalletEntity {
|
|
421
|
+
@ApiModelProperty("余额(分)")
|
|
422
|
+
private Long balance;
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
> **注意**:订单模块(OrderInfo)使用 BigDecimal 存储金额(以分为单位),这是模式B,属于正确用法,不是错误。
|
|
427
|
+
|
|
428
|
+
### 错误3:VO 不使用转换器
|
|
429
|
+
|
|
430
|
+
```java
|
|
431
|
+
// ❌ 错误:Excel 导出不使用转换器
|
|
432
|
+
@ExcelProperty(value = "订单金额(元)")
|
|
433
|
+
private BigDecimal orderAmount;
|
|
434
|
+
|
|
435
|
+
// ✅ 正确:使用 CustomNumberConverter
|
|
436
|
+
@ExcelProperty(value = "订单金额(元)", converter = CustomNumberConverter.class)
|
|
437
|
+
private BigDecimal orderAmount;
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### 错误4:合计查询返回非数值字段
|
|
441
|
+
|
|
442
|
+
```xml
|
|
443
|
+
<!-- ❌ 错误:合计查询返回非数值字段 -->
|
|
444
|
+
<select id="getOrderTotal" resultType="OrderVO">
|
|
445
|
+
SELECT
|
|
446
|
+
SUM(order_amount) AS order_amount,
|
|
447
|
+
order_date -- ❌ 非数值字段
|
|
448
|
+
FROM order_table
|
|
449
|
+
</select>
|
|
450
|
+
|
|
451
|
+
<!-- ✅ 正确:只返回数值字段 -->
|
|
452
|
+
<select id="getOrderTotal" resultType="OrderVO">
|
|
453
|
+
SELECT
|
|
454
|
+
SUM(order_amount) AS order_amount
|
|
455
|
+
FROM order_table
|
|
456
|
+
</select>
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## 参考文档
|
|
460
|
+
|
|
461
|
+
详见:[leniu-tengyun-core 源码](/Users/xujiajun/Developer/gongsi_proj/core/leniu-tengyun-core)
|