MyBatis实战:动态查询用户核销日志 ✨

🧾 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 层通过 AdminMiniProgramMapperMemberUserMapper (或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.xmlMemberUserMapper.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.javaMemberUserMapper.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 校验管理员权限。
  • 参数构造: 将 targetMemberUserIdPageWithSearch 中的搜索条件放入一个 Map 中,传递给 RedemptionLogMapper
  • 分页与排序 (使用 PageHelper): 与之前 MyBatis 博客中的实现类似。
  • DTO (Data Transfer Object, 数据传输对象) 转换: convertToDto 方法现在依赖于 RedemptionLogMapper.xml<resultMap> 配置的 <association> 来获取关联的 MemberUser(至少昵称)。

6. Controller 层 (RedemptionLogController.java)

Controller 层无需任何改动


📊 交互时序图 (Sequence Diagram - MyBatis 查询核销日志)

"前端/管理员""Controller""Service (MyBatis)""MemberUserMapper""AdminMiniProgramMapper""RedemptionLogMapper""PageHelper Plugin""数据库"GET /admin/redemption-logs/member/{memberUserId} (PageWithSearch)findRedemptionLogsForMember(targetMId, adminId, pageWithSearch)findById(targetMemberUserId)Optional<MemberUser> (targetUser)findMiniProgramIdsByAdminId(adminId)manageableMiniProgramIds校验 targetUser.miniProgramId 是否在 manageableMiniProgramIds 中pageWithSearch.toPageable() -> pageable构建搜索参数 Map (params - 含targetMemberUserId, field, value)PageHelper.startPage(pageable.pageNumber+1, pageable.pageSize)PageHelper.orderBy(sortClause)findRedemptionLogsByParams(params) %% PageHelper 拦截执行动态构建的SQL (含JOIN, WHERE, ORDER BY, LIMIT) SELECT rl.*, mu.nickname ...List<RedemptionLog> (当前页数据, 已含关联对象部分信息)logEntitiesnew PageInfo(logEntities)遍历 PageInfo.getList(), 调用 convertToDto()Page<RedemptionLogDto> (via PageImpl)构造 BaseResultJSON(BaseResult with Page<RedemptionLogDto>)"前端/管理员""Controller""Service (MyBatis)""MemberUserMapper""AdminMiniProgramMapper""RedemptionLogMapper""PageHelper Plugin""数据库"

🔄 状态图与类图 (概念上与之前博客类似)

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! 🔎💻

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值