🧾 MyBatis 精控审计:构建动态可搜索的用户核销日志查询 API (Application Programming Interface, 应用程序编程接口) 🕵️♂️
Hello,各位对数据持久化细节与 SQL (Structured Query Language, 结构化查询语言) 艺术有不懈追求的开发者们!👋 在任何一个严谨的系统中,能够清晰、准确地追溯每一笔交易或核销记录都是至关重要的。今天,我们将再次挑战一个核心的管理功能——管理员根据用户 ID (Identifier, 标识符) 查询特定用户的核销日志列表,但这次我们将运用 MyBatis 的强大力量。我们将深入探索如何通过手写 SQL (Structured Query Language, 结构化查询语言)、利用 MyBatis 的动态 SQL (Structured Query Language, 结构化查询语言)特性以及精巧的 <resultMap>,来打造一个同样支持分页、排序、动态搜索,并且严格执行权限控制的查询接口。如果你享受对每一行 SQL (Structured Query Language, 结构化查询语言) 的精准控制,或者你的项目技术栈是 MyBatis,那么这次的探索之旅将为你揭示 MyBatis 在处理此类查询时的独特魅力和实践智慧!🛠️
📖 接口功能与技术栈概览 (MyBatis 版 - 查询核销日志)
| 特性/方面 | 描述 | 关键技术/模式 (MyBatis 版) |
|---|---|---|
| 🎯 核心功能 | 管理员根据指定的 memberUserId 查询该用户的核销日志 (RedemptionLog) 列表。 | MyBatis Mapper 接口与 XML (Extensible Markup Language, 可扩展标记语言) 映射文件,手动编写 SQL (Structured Query Language, 结构化查询语言)。 |
| 🛡️ 权限控制 | 核心:确保当前操作的管理员 (adminId) 有权查看目标 memberUserId 的数据(基于管理员管辖的小程序范围及目标用户归属)。 | Service 层通过 AdminMiniProgramMapper 和 MemberUserMapper (或JPA (Jakarta Persistence API, Jakarta 持久化应用程序接口) Repo) 进行权限校验。 |
| 📄 分页支持 | 支持标准的分页参数 page (页码) 和 size (每页大小)。 | 自定义 PageWithSearch 类,Service 层配合 MyBatis 分页插件 (如 PageHelper) 实现分页。 |
| ↕️ 排序支持 | 支持基于一个或多个核销日志字段 (properties) 的升序 (ASC) 或降序 (DESC) (direction) 排序。 | PageWithSearch 处理排序参数,Service 层将 Sort 信息转换为分页插件可识别的排序字符串,并在动态 SQL (Structured Query Language, 结构化查询语言) 中安全地应用。 |
| 🔍 动态搜索 | 支持前端传递 field (要搜索的字段名,如 “transactionReference”, “adminId”) 和 value (搜索值) 进行通用条件查询。 | MyBatis 动态 SQL (Structured Query Language, 结构化查询语言) (<if>, <choose>, <where>) 在 XML (Extensible Markup Language, 可扩展标记语言) 映射文件中构建动态 WHERE 子句。 |
| ✨ API (Application Programming Interface, 应用程序编程接口) 响应 | 返回统一的 BaseResult 结构,数据部分为 Page<RedemptionLogDto>,使用 DTO (Data Transfer Object, 数据传输对象) 避免序列化问题并丰富展示信息(如用户昵称)。 | DTO (Data Transfer Object, 数据传输对象) 转换在 Service 层完成。 |
| 🔗 关联数据加载 | 在一次 SQL (Structured Query Language, 结构化查询语言) 查询中通过 JOIN 加载关联的 MemberUser (昵称) 和 Admin (管理员名,如果需要) 信息,并通过 MyBatis 的 <resultMap> 和 <association> 进行映射。 | MyBatis <resultMap> 的高级映射功能,实现高效的关联数据加载,避免 N+1。 |
| 🧩 技术栈核心 | Java (一种面向对象的编程语言), Spring Boot, MyBatis, MySQL (一种关系型数据库管理系统) (或其它), Lombok (一个Java库,可以通过简单的注解形式来帮助消除样板式代码), Swagger (API (Application Programming Interface, 应用程序编程接口) 文档), PageHelper (MyBatis 分页插件 - 推荐)。 |
🛠️ MyBatis 实现之旅:一步步构建
1. 前端请求参数载体:PageWithSearch.java (保持不变)
继续使用此类接收前端的分页、排序和通用搜索字段。
2. API (Application Programming Interface, 应用程序编程接口) 响应的“形象代言人”:RedemptionLogDto.java (保持不变)
DTO (Data Transfer Object, 数据传输对象) 结构保持不变,用于封装返回给前端的数据。
3. 定义 MyBatis Mapper XML (Extensible Markup Language, 可扩展标记语言) 文件
AdminMiniProgramMapper.xml 和 MemberUserMapper.xml (需要相关查询方法):
AdminMiniProgramMapper:findMiniProgramIdsByAdminId(adminId)。MemberUserMapper:findById(id)(用于权限校验时获取目标用户的miniProgramId)。
RedemptionLogMapper.xml (查询核销日志,支持动态条件、关联加载和分页):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.productQualification.coupon.mapper.RedemptionLogMapper">
<resultMap id="RedemptionLogWithDetailsResultMap" type="com.productQualification.coupon.domain.RedemptionLog">
<id property="id" column="rl_id"/>
<result property="transactionReference" column="rl_transaction_reference"/>
<result property="adminId" column="rl_admin_id"/>
<result property="memberUserId" column="rl_member_user_id"/>
<result property="originalAmount" column="rl_original_amount"/>
<result property="usedUserCouponId" column="rl_used_user_coupon_id"/>
<result property="couponDiscountAmount" column="rl_coupon_discount_amount"/>
<result property="pointsUsed" column="rl_points_used"/>
<result property="pointsDeductionAmount" column="rl_points_deduction_amount"/>
<result property="totalDiscountAmount" column="rl_total_discount_amount"/>
<result property="finalAmount" column="rl_final_amount"/>
<result property="redemptionDate" column="rl_redemption_date"/>
<result property="description" column="rl_description"/>
<result property="createdDate" column="rl_created_date"/>
<!-- 关联 MemberUser (获取昵称) -->
<association property="memberUser" javaType="com.productQualification.coupon.domain.MemberUser">
<id property="id" column="mu_id"/>
<result property="nickname" column="mu_nickname"/>
</association>
<!-- 关联 Admin (如果需要获取管理员名称) -->
<!--
<association property="admin" javaType="com.productQualification.user.domain.Admin">
<id property="id" column="admin_id"/>
<result property="username" column="admin_username"/>
</association>
-->
</resultMap>
<sql id="selectRedemptionLogFields">
rl.id as rl_id, rl.transaction_reference as rl_transaction_reference, rl.admin_id as rl_admin_id,
rl.member_user_id as rl_member_user_id, rl.original_amount as rl_original_amount,
rl.used_user_coupon_id as rl_used_user_coupon_id, rl.coupon_discount_amount as rl_coupon_discount_amount,
rl.points_used as rl_points_used, rl.points_deduction_amount as rl_points_deduction_amount,
rl.total_discount_amount as rl_total_discount_amount, rl.final_amount as rl_final_amount,
rl.redemption_date as rl_redemption_date, rl.description as rl_description, rl.created_date as rl_created_date,
mu.id as mu_id, mu.nickname as mu_nickname <!-- MemberUser fields -->
<!-- , adm.id as admin_id, adm.username as admin_username --> <!-- Admin fields -->
</sql>
<sql id="redemptionLogJoins">
LEFT JOIN member_user mu ON rl.member_user_id = mu.id
<!-- LEFT JOIN admin adm ON rl.admin_id = adm.id -->
</sql>
<sql id="redemptionLogWhereConditions">
<where>
<!-- 核心过滤:指定用户ID -->
<if test="targetMemberUserId != null">
AND rl.member_user_id = #{targetMemberUserId}
</if>
<!-- 权限过滤:如果需要,可以再加一层小程序ID的过滤 -->
<!--
<if test="manageableMiniProgramIds != null and !manageableMiniProgramIds.isEmpty()">
AND mu.mini_program_id IN
<foreach item="item" collection="manageableMiniProgramIds" open="(" separator="," close=")">
#{item}
</foreach>
</if>
-->
<!-- 通用字段搜索 -->
<if test="field != null and field != '' and value != null and value != ''">
<choose>
<when test="field == 'transactionReference'">
AND rl.transaction_reference LIKE CONCAT('%', #{value}, '%')
</when>
<when test="field == 'adminId'">
AND rl.admin_id = #{value} <!-- 假设value是Integer类型 -->
</when>
<!-- 更多可搜索字段 -->
</choose>
</if>
<!-- 其他来自 PageWithSearch 的特定搜索条件,如核销日期范围 -->
</where>
</sql>
<!-- 查询列表 (PageHelper 会自动处理分页和总数) -->
<select id="findRedemptionLogsByParams" resultMap="RedemptionLogWithDetailsResultMap">
SELECT <include refid="selectRedemptionLogFields"/>
FROM redemption_log rl
<include refid="redemptionLogJoins"/>
<include refid="redemptionLogWhereConditions"/>
<!-- 排序由 PageHelper 根据传入的 Pageable/Sort 信息处理,或通过 PageHelper.orderBy() 设置 -->
</select>
<!-- 如果不用 PageHelper,你需要一个单独的 count 查询 -->
</mapper>
XML (Extensible Markup Language, 可扩展标记语言) 关键点:
RedemptionLogWithDetailsResultMap: 使用<association>映射关联的MemberUser(获取昵称)。- 列别名:
rl_id,mu_nickname等用于清晰映射。 - 动态 SQL (Structured Query Language, 结构化查询语言):
redemptionLogWhereConditions使用动态 SQL (Structured Query Language, 结构化查询语言) 构建WHERE子句。
4. 定义 MyBatis Mapper 接口
AdminMiniProgramMapper.java 和 MemberUserMapper.java (与之前 MyBatis 博客中的一致)
RedemptionLogMapper.java:
package com.productQualification.coupon.mapper;
import com.productQualification.coupon.domain.RedemptionLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface RedemptionLogMapper {
// PageHelper 会拦截这个方法进行分页
// Map<String, Object> params 将包含 targetMemberUserId 和 PageWithSearch 中的搜索字段
List<RedemptionLog> findRedemptionLogsByParams(@Param("params") Map<String, Object> params);
}
5. Service 层改造:RedemptionLogService.java (MyBatis 版)
package com.productQualification.coupon.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.productQualification.common.entity.PageWithSearch;
import com.productQualification.common.exception.PermissionDeniedException;
import com.productQualification.common.exception.NotFoundException;
import com.productQualification.common.util.SqlUtil;
import com.productQualification.coupon.domain.AdminMiniProgram;
import com.productQualification.coupon.domain.MemberUser;
import com.productQualification.coupon.domain.RedemptionLog;
import com.productQualification.coupon.dto.RedemptionLogDto;
import com.productQualification.coupon.mapper.AdminMiniProgramMapper;
import com.productQualification.coupon.mapper.MemberUserMapper;
import com.productQualification.coupon.mapper.RedemptionLogMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class RedemptionLogService {
private static final Logger logger = LoggerFactory.getLogger(RedemptionLogService.class);
private final RedemptionLogMapper redemptionLogMapper;
private final AdminMiniProgramMapper adminMiniProgramMapper;
private final MemberUserMapper memberUserMapper;
@Autowired
public RedemptionLogService(RedemptionLogMapper redemptionLogMapper,
AdminMiniProgramMapper adminMiniProgramMapper,
MemberUserMapper memberUserMapper) {
this.redemptionLogMapper = redemptionLogMapper;
this.adminMiniProgramMapper = adminMiniProgramMapper;
this.memberUserMapper = memberUserMapper;
}
@Transactional(readOnly = true)
public Page<RedemptionLogDto> findRedemptionLogsForMember(
Integer targetMemberUserId,
Integer operatingAdminId,
PageWithSearch pageWithSearch) {
logger.info("Admin {} searching redemption logs (MyBatis) for member {}: {}",
operatingAdminId, targetMemberUserId, pageWithSearch);
// 1. 权限校验
MemberUser targetUser = memberUserMapper.findById(targetMemberUserId)
.orElseThrow(() -> new NotFoundException("目标用户 (ID: " + targetMemberUserId + ") 不存在。"));
checkAdminPermissionForMiniProgram(operatingAdminId, targetUser.getMiniProgramId());
Pageable pageable = pageWithSearch.toPageable();
Map<String, Object> params = new HashMap<>();
params.put("targetMemberUserId", targetMemberUserId);
if (StringUtils.hasText(pageWithSearch.getField())) params.put("field", pageWithSearch.getField().trim());
if (StringUtils.hasText(pageWithSearch.getValue())) params.put("value", pageWithSearch.getValue().trim());
// ... 其他搜索参数 ...
// 2. 使用 PageHelper 进行分页和排序
PageHelper.startPage(pageable.getPageNumber() + 1, pageable.getPageSize());
if (pageable.getSort().isSorted()) {
String orderByClause = pageable.getSort().stream()
.map(order -> {
String property = order.getProperty();
String dbColumn;
// 安全转换属性名到列名,并添加表别名
if (property.startsWith("memberUser.")) {
dbColumn = SqlUtil.camelToUnderlineForMybatis("mu." + property.substring("memberUser.".length()));
} else {
dbColumn = SqlUtil.camelToUnderlineForMybatis("rl." + property); // 主表 redemption_log
}
return dbColumn != null ? dbColumn + " " + order.getDirection().name() : null;
})
.filter(Objects::nonNull)
.collect(Collectors.joining(", "));
if (StringUtils.hasText(orderByClause)) PageHelper.orderBy(orderByClause);
} else {
PageHelper.orderBy("rl.redemption_date DESC"); // 默认按核销时间降序
}
// 3. 执行查询
List<RedemptionLog> logEntities = redemptionLogMapper.findRedemptionLogsByParams(params);
PageInfo<RedemptionLog> pageInfo = new PageInfo<>(logEntities);
// 4. 转换为 DTO
List<RedemptionLogDto> dtos = pageInfo.getList().stream()
.map(log -> convertToDto(log, log.getMemberUser())) // MyBatis resultMap 已加载关联的 MemberUser
.collect(Collectors.toList());
return new PageImpl<>(dtos, pageable, pageInfo.getTotal());
}
private void checkAdminPermissionForMiniProgram(Integer adminId, Integer miniProgramId) {
if (miniProgramId == null) throw new MyRuntimeException("权限校验失败:用户未关联小程序。");
if (!adminMiniProgramMapper.existsByAdminIdAndMiniProgramId(adminId, miniProgramId)) {
throw new PermissionDeniedException("管理员无权查看此小程序的用户数据。");
}
}
private RedemptionLogDto convertToDto(RedemptionLog entity, MemberUser memberUser) {
if (entity == null) return null;
RedemptionLogDto dto = new RedemptionLogDto();
BeanUtils.copyProperties(entity, dto, "memberUserNickname");
if (memberUser != null && memberUser.getId() != null) {
dto.setMemberUserNickname(memberUser.getNickname());
}
return dto;
}
}
MyBatis 版本 Service 层改动要点:
- 依赖注入: 注入 MyBatis Mapper 接口。
- 权限校验: 在执行查询前,先通过
MemberUserMapper加载目标用户,再通过AdminMiniProgramMapper校验管理员权限。 - 参数构造: 将
targetMemberUserId和PageWithSearch中的搜索条件放入一个Map中,传递给RedemptionLogMapper。 - 分页与排序 (使用 PageHelper): 与之前 MyBatis 博客中的实现类似。
- DTO (Data Transfer Object, 数据传输对象) 转换:
convertToDto方法现在依赖于RedemptionLogMapper.xml中<resultMap>配置的<association>来获取关联的MemberUser(至少昵称)。
6. Controller 层 (RedemptionLogController.java)
Controller 层无需任何改动。
📊 交互时序图 (Sequence Diagram - MyBatis 查询核销日志)
🔄 状态图与类图 (概念上与之前博客类似)
RedemptionLog 记录的查询和 DTO (Data Transfer Object, 数据传输对象) 转换的状态流转概念不变。类图的主要变化是 Service 层现在依赖 MyBatis Mapper 接口。
💡 英文缩写全称及中文解释
(与上一篇博客中的列表一致)
🧠 思维导图 (Markdown 格式)

🎉 总结:MyBatis 下的精准审计与数据掌控!
通过 MyBatis 的精细化 SQL (Structured Query Language, 结构化查询语言) 控制和强大的 resultMap 功能,我们成功地为管理员构建了一个功能全面的用户核销日志查询接口。它不仅支持分页、排序和基于多种条件的动态搜索,还能在一次数据库查询中高效地加载所需的关联数据(如用户昵称),并通过 DTO (Data Transfer Object, 数据传输对象) 模式清晰地呈现给前端,有效避免了 N+1 问题。
在 Service 层,我们首先进行了严格的权限校验,确保了数据的安全隔离。然后,通过将所有查询条件封装到 Map 中并传递给 MyBatis Mapper,结合 PageHelper 插件,我们实现了高效且灵活的数据检索。
虽然 MyBatis 要求开发者更深入地参与 SQL (Structured Query Language, 结构化查询语言) 的编写和结果集映射,但这种投入换来的是对数据访问过程的完全掌控和极致的优化潜力。这再次证明,根据项目需求和团队偏好选择合适的持久层技术至关重要。
希望这篇基于 MyBatis 实现的核销日志查询接口博客,能为你提供有价值的参考!如果你在 MyBatis 的高级应用或复杂查询优化方面有更多心得,欢迎在评论区分享你的智慧!👇 Happy SQL (Structured Query Language, 结构化查询语言) Auditing! 🔎💻
155

被折叠的 条评论
为什么被折叠?



