ruoyi-vue-pro 若依芋道 java springboot +mybatis 生日查询相关
-
-
- [方案一:使用 `DATE_ADD` 动态计算"下一个生日"(推荐,逻辑闭环)](#方案一:使用
DATE_ADD动态计算“下一个生日”(推荐,逻辑闭环)) -
- [1. MyBatis Mapper XML 代码](#1. MyBatis Mapper XML 代码)
- [2. Java Mapper 接口](#2. Java Mapper 接口)
- [3. 调用示例(Service 层)](#3. 调用示例(Service 层))
- [方案二:使用虚拟列 + 索引(针对大数据量优化)](#方案二:使用虚拟列 + 索引(针对大数据量优化))
-
- [1. 修改表结构(MySQL 5.7+)](#1. 修改表结构(MySQL 5.7+))
- [2. 改造 MyBatis SQL(针对指定日期)](#2. 改造 MyBatis SQL(针对指定日期))
- [3. 相对日期(今天/3天/30天)的跨年处理(使用 `birthday_mmdd`)](#3. 相对日期(今天/3天/30天)的跨年处理(使用
birthday_mmdd))
- [关于 2月29日的特别说明](#关于 2月29日的特别说明)
- 总结建议
- [方案一:使用 `DATE_ADD` 动态计算"下一个生日"(推荐,逻辑闭环)](#方案一:使用
-
- [========================================== 优化:](#========================================== 优化:)
- [========================================== vue3代码:](#========================================== vue3代码:)
mysql user表中 生日字段birthday(datetime)(示例:2025-10-21 00:00:00),在springboot项目的mybatis中查询符合指定日期的数据 (前端查询条件为 示例1:今天、3天内、30天内生日的数据;示例2:指定日期 2026-9-8 00:00:00 符合该日期生日的数据)

在 Spring Boot + MyBatis 中处理 birthday(datetime) 的生日查询,核心难点是 "忽略年份,只比较月/日" ,同时处理 "跨年(如12月31日到1月2日)" 的场景。
针对你的需求(今天、3天内、30天内、指定日期),我推荐两种实现方案,方案一逻辑清晰通用 ,方案二性能更优。
方案一:使用 DATE_ADD 动态计算"下一个生日"(推荐,逻辑闭环)
核心思路:将数据库里的出生年份替换为当前年份,计算出"今年的生日 "和"明年的生日",然后判断这两个日期是否落在目标区间内。此方案能完美处理跨年(12月生日的用户,在1月查询3天内也能被查到)。
1. MyBatis Mapper XML 代码
xml
<select id="selectUsersByBirthdayCondition" resultType="com.xxx.User">
SELECT *
FROM member_user
<where>
<choose>
<!-- 1. 指定日期查询(如 2026-09-08) -->
<when test="type == 'SPECIFIC' and specificDate != null">
AND MONTH(birthday) = MONTH(#{specificDate})
AND DAY(birthday) = DAY(#{specificDate})
</when>
<!-- 2. 今天 -->
<when test="type == 'TODAY'">
AND (
DATE_ADD(birthday, INTERVAL (YEAR(CURDATE()) - YEAR(birthday)) YEAR) = CURDATE()
OR
DATE_ADD(birthday, INTERVAL (YEAR(CURDATE()) - YEAR(birthday) + 1) YEAR) = CURDATE()
)
</when>
<!-- 3. 3天内 -->
<when test="type == 'WITHIN_3'">
AND (
DATE_ADD(birthday, INTERVAL (YEAR(CURDATE()) - YEAR(birthday)) YEAR)
BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 DAY)
OR
DATE_ADD(birthday, INTERVAL (YEAR(CURDATE()) - YEAR(birthday) + 1) YEAR)
BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 DAY)
)
</when>
<!-- 4. 30天内 -->
<when test="type == 'WITHIN_30'">
AND (
DATE_ADD(birthday, INTERVAL (YEAR(CURDATE()) - YEAR(birthday)) YEAR)
BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)
OR
DATE_ADD(birthday, INTERVAL (YEAR(CURDATE()) - YEAR(birthday) + 1) YEAR)
BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)
)
</when>
</choose>
</where>
</select>
2. Java Mapper 接口
java
List<User> selectUsersByBirthdayCondition(@Param("type") String type,
@Param("specificDate") LocalDateTime specificDate);
3. 调用示例(Service 层)
java
// 场景1:查询今天过生日的人
List<User> todayUsers = mapper.selectUsersByBirthdayCondition("TODAY", null);
// 场景2:查询3天内过生日的人
List<User> within3Users = mapper.selectUsersByBirthdayCondition("WITHIN_3", null);
// 场景3:查询指定日期 2026-09-08 过生日的人(注意:年份会被忽略)
LocalDateTime date = LocalDateTime.of(2026, 9, 8, 0, 0, 0);
List<User> specificUsers = mapper.selectUsersByBirthdayCondition("SPECIFIC", date);
方案二:使用虚拟列 + 索引(针对大数据量优化)
由于方案一对 birthday 字段使用了 YEAR()、MONTH()、DATE_ADD() 等函数,MySQL 索引会失效 ,导致全表扫描。如果用户表数据量很大(几十万以上),建议增加一个虚拟列存储"月-日",并建立索引。
1. 修改表结构(MySQL 5.7+)
sql
ALTER TABLE `member_user`
ADD COLUMN `birthday_mmdd` CHAR(5)
GENERATED ALWAYS AS (DATE_FORMAT(birthday, '%m-%d')) STORED;
CREATE INDEX `idx_birthday_mmdd` ON `member_user` (`birthday_mmdd`);
2. 改造 MyBatis SQL(针对指定日期)
指定日期查询直接命中索引:
xml
<when test="type == 'SPECIFIC' and specificDate != null">
AND birthday_mmdd = DATE_FORMAT(#{specificDate}, '%m-%d')
</when>
3. 相对日期(今天/3天/30天)的跨年处理(使用 birthday_mmdd)
对于范围查询,需处理跨年(例如 12月30日到1月2日)。可以利用 CONCAT 构造日期字符串进行比较:
xml
<when test="type == 'WITHIN_3'">
AND (
-- 情况1:不跨年(当前日期 > 生日MMDD)
(DATE_FORMAT(CURDATE(), '%m-%d') <= '12-31' AND birthday_mmdd BETWEEN DATE_FORMAT(CURDATE(), '%m-%d') AND DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 3 DAY), '%m-%d'))
OR
-- 情况2:跨年(当前日期在12月,结束日期在次年1月)
(DATE_FORMAT(CURDATE(), '%m-%d') > DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 3 DAY), '%m-%d')
AND (birthday_mmdd >= DATE_FORMAT(CURDATE(), '%m-%d') OR birthday_mmdd <= DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 3 DAY), '%m-%d')))
)
</when>
(此逻辑较绕,需严格测试;若业务允许,推荐方案一,或者使用 Java 代码计算好起始 MMDD 和结束 MMDD 传入 SQL)
关于 2月29日的特别说明
- 方案一 :在非闰年,
DATE_ADD('2020-02-29', INTERVAL 3 YEAR)会被 MySQL 自动转为2023-02-28。这意味着 2月29日出生的人在平年会"过生日"在 2月28日,这符合大多数业务默认规则。 - 方案二 :
birthday_mmdd固定为'02-29',在非闰年查询"今天"时不会被匹配,除非业务特殊处理。
总结建议
- 优先使用方案一,代码可读性强,维护简单,适合绝大多数中小型项目。
- 如果用户量超过 10 万 且查询频繁,采用方案二(虚拟列+索引),并配合 Java 代码计算 MMDD 区间传入,性能最佳。
- 前端传参时,
type建议枚举值:TODAY、WITHIN_3、WITHIN_30、SPECIFIC;specificDate仅在SPECIFIC时传值即可。
==========================================
优化:
==========================================
基于你的需求,采用虚拟列 + 索引方案,对现有的 MyBatis-Plus 分页查询进行增强。核心改动点:
- 数据库增加虚拟列
birthday_mmdd(格式MM-dd)并建立索引。 - 在原有查询条件基础上,根据前端传入的生日类型(今天/3天/30天/指定日期)动态拼接
apply条件,利用索引高效过滤。
一、数据库变更(MySQL 5.7+)
sql
-- 增加虚拟列(存储 'MM-dd'):在插入或编辑用户数据时,数据库会根据birthday值自动处理并对birthday_mmdd赋值,不需要java代码处理
ALTER TABLE `member_user`
ADD COLUMN `birthday_mmdd` CHAR(5)
GENERATED ALWAYS AS (DATE_FORMAT(birthday, '%m-%d')) STORED;
-- 创建索引(覆盖该列)
CREATE INDEX `idx_birthday_mmdd` ON `member_user` (`birthday_mmdd`);
注意 :
STORED会占用磁盘空间(例如:50w条数据 40M左右),但查询时无需回表;VIRTUAL不占空间,但每次查询计算。建议使用STORED以获得最佳查询性能。
二、VO 增加接收参数
在分页请求 VO(例如 MemberUserPageReqVO)中添加:
java
@Data
public class MemberUserPageReqVO extends PageParam {
// 原有字段...
private String mobile;
private LocalDateTime[] loginDate;
private String nickname;
private LocalDateTime[] createTime;
// 新增生日查询参数
private String birthdayType; // TODAY, WITHIN_3, WITHIN_30, SPECIFIC
private LocalDateTime specificDate; // 当 birthdayType=SPECIFIC 时使用
}
三、Service 层构建动态条件(核心)
在原有的 selectPage 方法中,增加生日条件的动态拼接。保持原有 LambdaQueryWrapperX 不变,通过 apply 方法注入 SQL 片段。
java
public IPage<MemberUserDO> getMemberUserPage(MemberUserPageReqVO reqVO) {
// 原有的基础条件(手机、登录日期、昵称、创建时间、标签)
LambdaQueryWrapperX<MemberUserDO> wrapper = new LambdaQueryWrapperX<MemberUserDO>()
.likeIfPresent(MemberUserDO::getMobile, reqVO.getMobile())
.betweenIfPresent(MemberUserDO::getLoginDate, reqVO.getLoginDate())
.likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname())
.betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime())
.apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql);
// ---------- 新增生日条件 ----------
String birthdayCondition = buildBirthdayCondition(reqVO.getBirthdayType(), reqVO.getSpecificDate());
if (StrUtil.isNotBlank(birthdayCondition)) {
wrapper.apply(birthdayCondition);
}
wrapper.orderByDesc(MemberUserDO::getId);
return baseMapper.selectPage(reqVO, wrapper);
}
辅助方法:构建生日 SQL 条件
java
/**
* 根据生日类型构建 SQL 条件(利用虚拟列 birthday_mmdd)
*/
private String buildBirthdayCondition(String type, LocalDateTime specificDate) {
if (StrUtil.isBlank(type)) {
return null;
}
LocalDate today = LocalDate.now();
DateTimeFormatter mmddFormatter = DateTimeFormatter.ofPattern("MM-dd");
switch (type) {
case "TODAY":
return "birthday_mmdd = '" + today.format(mmddFormatter) + "'";
case "WITHIN_3":
return buildRangeCondition(today, 3, mmddFormatter);
case "WITHIN_30":
return buildRangeCondition(today, 30, mmddFormatter);
case "SPECIFIC":
if (specificDate == null) {
return null;
}
return "birthday_mmdd = '" + specificDate.format(mmddFormatter) + "'";
default:
return null;
}
}
/**
* 构建范围条件(处理跨年)
*/
private String buildRangeCondition(LocalDate today, int days, DateTimeFormatter formatter) {
LocalDate endDate = today.plusDays(days);
String startMmdd = today.format(formatter);
String endMmdd = endDate.format(formatter);
// 跨年判断:比较字符串 'MM-dd',如果 start > end 说明跨年
if (startMmdd.compareTo(endMmdd) > 0) {
// 跨年条件:birthday_mmdd >= startMmdd OR birthday_mmdd <= endMmdd
return "(birthday_mmdd >= '" + startMmdd + "' OR birthday_mmdd <= '" + endMmdd + "')";
} else {
// 非跨年:BETWEEN
return "birthday_mmdd BETWEEN '" + startMmdd + "' AND '" + endMmdd + "'";
}
}
四、关于 2月29日的特殊说明
- 虚拟列值 :
birthday_mmdd存储为'02-29'。 - 平年查询 :例如 2026 年 2 月 28 日查询"今天",条件为
birthday_mmdd = '02-28',2 月 29 日出生的人不会被命中(与业务预期一致,平年无 2/29)。 - 跨年范围 :若查询"3天内"且包含 2 月 28 日到 3 月 2 日,则会正常比较字符串,
'02-28'到'03-02'之间不包含'02-29'(因为'02-29'<'03-02'且 >'02-28'吗?实际上字符串比较是按字典序,'02-29'在'02-28'和'03-02'之间,所以会包含。但注意:如果今天是 2 月 28 日,3 天内包含 2/28, 2/29(平年不存在), 3/1, 3/2?实际上today.plusDays(3)会得到 3月3日?需明确业务:3天内通常指今天、明天、后天,即包括今天+未来3天?我们的days参数含义要明确:例如WITHIN_3表示"3天内",通常理解为从今天开始的未来3天(含今天)。我们的代码today.plusDays(days)是截止到未来第 days 天,区间为[today, today+days],与业务一致。
五、性能分析
- 索引利用 :所有生日条件都基于
birthday_mmdd列,查询时命中索引idx_birthday_mmdd,无论=、BETWEEN还是OR(跨年)都能有效使用索引范围扫描。 - 分页友好 :虚拟列是数据库原生列,
BETWEEN和=均可正常分页(LIMIT offset, size会通过索引快速定位),性能远优于对birthday字段使用函数。 - 跨年优化 :跨年时拆分为
OR,MySQL 优化器通常会分别使用索引,然后合并结果(Index Merge),效率依然可控(如果用户量极大,可考虑将跨年查询拆为两次查询或使用UNION,但此处OR已足够)。
六、前端调用示例
http
GET /api/member/user/page?birthdayType=WITHIN_3&pageNo=1&pageSize=10
GET /api/member/user/page?birthdayType=SPECIFIC&specificDate=2026-09-08T00:00:00
七、完整代码整合(你的 Service 方法改造后)
假设你的 Service 类为 MemberUserServiceImpl,原方法为 getMemberUserPage,直接替换内部实现为上述代码即可,其他逻辑不变。
注意事项:
- 确保
MemberUserDO实体中不需要额外字段,虚拟列只存在于数据库,MyBatis-Plus 查询时自动映射(不会插入/更新)。 - 如果原有代码中使用
LambdaQueryWrapperX的apply方法(通常支持),可直接使用;若不确定,改用QueryWrapper或wrapper.apply均可。
这样,你既保留了原有复杂查询条件的灵活性,又通过虚拟列+索引完美解决了生日查询的性能问题,且代码侵入性小,维护简单。如果后续需要按生日排序,只需在 wrapper.orderByAsc("birthday_mmdd") 即可,同样走索引。
==========================================
vue3代码:
==========================================
前端代码
vue
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px" >
<el-form-item label="生日" prop="specificDate" class="!w-220px" ><el-date-picker v-model="queryParams.specificDate" @change="onSpecificDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD HH:mm:ss" class="!w-240px" /></el-form-item>
<el-form-item prop="birthdayType" :style="{marginLeft:20}">
<el-radio-group v-model="queryParams.birthdayType" @change="onBirthdayType">
<el-radio-button value="TODAY">今天</el-radio-button>
<el-radio-button value="WITHIN_3">近3天</el-radio-button>
<el-radio-button value="WITHIN_7">近1周</el-radio-button>
<el-radio-button value="WITHIN_15">近半月</el-radio-button>
<el-radio-button value="WITHIN_30">近1月</el-radio-button>
</el-radio-group>
</el-form-item>
</el-form>
</ContentWrap>
<script lang="ts" setup>
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
nickname: null,
mobile: null,
loginDate: [],
createTime: [],
tagIds: [],
levelId: null,
groupId: null,
birthdayType: 'SPECIFIC', // 类型 默认按日期控件值查询
specificDate: null // 选中的生日具体日期
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await UserApi.getUserPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 生日周期选项选中 */
const onBirthdayType = () => {
queryParams.specificDate = null // 生日日期置空
handleQuery()
}
/** 生日日期控件选中 */
const onSpecificDate = () => {
queryParams.birthdayType = 'SPECIFIC' //默认使用选中日期查询
handleQuery()
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>