🎟️ MyBatis 精控:智能优惠券模板创建与币种自动关联的 SQL (Structured Query Language, 结构化查询语言) 之道 ✨
Hello,各位对数据持久化细节有极致追求的开发者们!👋 我们已经探讨了如何使用 Spring Data JPA (Jakarta Persistence API, Jakarta 持久化应用程序接口) 实现一个智能的优惠券模板创建接口,其中后端能根据小程序自动关联币种。今天,我们将把这个功能移植到 MyBatis 的世界,看看如何利用 MyBatis 对 SQL (Structured Query Language, 结构化查询语言) 的精细控制能力,来实现同样优雅且高效的解决方案。如果你钟情于手写 SQL (Structured Query Language, 结构化查询语言) 的自由度,或者你的项目技术栈是 MyBatis,那么这次的探索不容错过!让我们一起深入 MyBatis 的 SQL (Structured Query Language, 结构化查询语言) 映射和 Service 层逻辑,打造一个同样智能的优惠券模板创建接口。🚀
📖 接口功能与技术栈概览 (MyBatis 版 - 创建优惠券模板)
| 特性/方面 | 描述 | 关键技术/模式 (MyBatis 版) |
|---|---|---|
| 🎯 核心功能 | 管理员为其有权操作的小程序创建新的优惠券模板。模板的币种 (currencyId) 由后端根据所选小程序的默认币种自动设置。 | MyBatis Mapper 接口与 XML (Extensible Markup Language, 可扩展标记语言) 映射文件,手动编写 SQL (Structured Query Language, 结构化查询语言) SELECT, INSERT。DTO (Data Transfer Object, 数据传输对象) 模式。 |
| 🆔 小程序绑定 | 所有优惠券模板都必须关联到一个具体的小程序 (miniProgramId 必填)。 | DTO (Data Transfer Object, 数据传输对象) 中 @NotNull 校验,Service 层逻辑校验 miniProgramId 有效性。 |
| 💰 币种自动关联 | 前端在创建模板时无需传递 currencyId。后端 Service 层根据 payload.miniProgramId 查询 MiniProgramConfig,并使用其 currencyId。 | Service 层调用 MiniProgramConfigMapper (或 JPA (Jakarta Persistence API, Jakarta 持久化应用程序接口) Repo) 获取小程序信息。 |
| 📝 类型化面值/折扣 | 模板实体包含独立的 value (金额) 和 discountRate (折扣率) 字段。Service 层根据优惠券类型 (type) 校验并设置相应字段。 | CouponTemplate 实体设计, Service 层 validateAndSetTypeValueAndRate 辅助方法。 |
| 🛡️ 多重校验 | 包括 DTO (Data Transfer Object, 数据传输对象) 格式校验 (@Valid)、小程序存在性、管理员操作权限、模板名称在小程序内唯一性、有效期合法性等。 | Bean Validation, Service 层自定义校验逻辑 (调用 Mapper 进行数据库校验)。 |
| ✨ API (Application Programming Interface, 应用程序编程接口) 响应 | 创建成功后返回统一的 BaseResult 结构,数据部分为 CouponTemplateDto。 | DTO (Data Transfer Object, 数据传输对象) 转换在 Service 层完成。 |
| ⏱️ 时间戳管理 | 使用 MyBatis 时,实体的 createdDate 和 lastModifiedDate 字段需要手动在 Service 层或 SQL (Structured Query Language, 结构化查询语言) 中设置。 | new Date() 设置时间,并在 INSERT 语句中包含这些字段。 |
| 🧩 技术栈核心 | Java (一种面向对象的编程语言), Spring Boot, MyBatis, MySQL (一种关系型数据库管理系统) (或其它), Lombok (一个Java库,可以通过简单的注解形式来帮助消除样板式代码), Swagger (API (Application Programming Interface, 应用程序编程接口) 文档)。 |
🛠️ MyBatis 实现之旅:一步步构建
1. 前端“指挥官”:CouponTemplatePayload.java (DTO (Data Transfer Object, 数据传输对象) - 保持不变)
前端请求的 DTO (Data Transfer Object, 数据传输对象) 保持不变,不包含 currencyId。
// CouponTemplatePayload.java (关键字段)
@Data
public class CouponTemplatePayload {
@NotBlank private String name;
@NotNull private Byte type;
private BigDecimal value;
@DecimalMin("0.0") @DecimalMax("1.0")
private BigDecimal discountRate;
@NotNull private BigDecimal threshold;
@NotNull @Min(1) private Integer total;
@NotNull @Min(1) private Integer limitPerUser;
@NotNull private Date validFrom;
@NotNull private Date validTo;
private String description;
@NotNull private Byte status;
@NotNull private Integer miniProgramId; // 必填
@NotNull private Byte stackable;
}
2. API (Application Programming Interface, 应用程序编程接口) 响应的“形象代言人”:CouponTemplateDto.java (保持不变)
用于返回给前端的 DTO (Data Transfer Object, 数据传输对象) 结构也保持不变。
3. 定义 MyBatis Mapper XML (Extensible Markup Language, 可扩展标记语言) 文件
MiniProgramConfigMapper.xml (或对应的JPA (Jakarta Persistence API, Jakarta 持久化应用程序接口) Repository)
我们需要一个方法根据 id 查询 MiniProgramConfig,特别是要获取其 currency_id 和关联的 Currency 信息。
<!-- MiniProgramConfigMapper.xml (如果用MyBatis) -->
<mapper namespace="com.productQualification.coupon.mapper.MiniProgramConfigMapper">
<resultMap id="MiniProgramConfigWithCurrencyResultMap" type="com.productQualification.coupon.domain.MiniProgramConfig">
<id property="id" column="mpc_id"/>
<result property="name" column="mpc_name"/>
<result property="appId" column="mpc_app_id"/>
<result property="currencyId" column="mpc_currency_id"/>
<association property="currency" javaType="com.productQualification.coupon.domain.Currency">
<id property="id" column="cur_id"/>
<result property="code" column="cur_code"/>
<result property="name" column="cur_name"/>
<result property="symbol" column="cur_symbol"/>
</association>
</resultMap>
<select id="findByIdWithCurrency" resultMap="MiniProgramConfigWithCurrencyResultMap">
SELECT
mpc.id as mpc_id, mpc.name as mpc_name, mpc.app_id as mpc_app_id, mpc.currency_id as mpc_currency_id,
cur.id as cur_id, cur.code as cur_code, cur.name as cur_name, cur.symbol as cur_symbol
FROM mini_program_config mpc
LEFT JOIN currency cur ON mpc.currency_id = cur.id
WHERE mpc.id = #{id}
</select>
</mapper>
CouponTemplateMapper.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.CouponTemplateMapper">
<resultMap id="CouponTemplateResultMap" type="com.productQualification.coupon.domain.CouponTemplate">
<!-- ... 所有 CouponTemplate 字段的映射 ... -->
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="type" column="type"/>
<result property="value" column="value"/>
<result property="discountRate" column="discount_rate"/>
<result property="threshold" column="threshold"/>
<result property="total" column="total"/>
<result property="issued" column="issued"/>
<result property="limitPerUser" column="limit_per_user"/>
<result property="validFrom" column="valid_from"/>
<result property="validTo" column="valid_to"/>
<result property="description" column="description"/>
<result property="status" column="status"/>
<result property="currencyId" column="currency_id"/>
<result property="miniProgramId" column="mini_program_id"/>
<result property="stackable" column="stackable"/>
<result property="createdDate" column="created_date"/>
<result property="lastModifiedDate" column="last_modified_date"/>
</resultMap>
<select id="existsByNameAndMiniProgramId" resultType="boolean">
SELECT EXISTS(SELECT 1 FROM coupon_template WHERE name = #{name} AND mini_program_id = #{miniProgramId})
</select>
<insert id="insertCouponTemplate" parameterType="com.productQualification.coupon.domain.CouponTemplate" useGeneratedKeys="true" keyProperty="id">
INSERT INTO coupon_template
(name, type, value, discount_rate, threshold, total, issued, limit_per_user,
valid_from, valid_to, description, status, currency_id, mini_program_id, stackable,
created_date, last_modified_date)
VALUES
(#{name}, #{type}, #{value}, #{discountRate}, #{threshold}, #{total}, #{issued}, #{limitPerUser},
#{validFrom}, #{validTo}, #{description}, #{status}, #{currencyId}, #{miniProgramId}, #{stackable},
#{createdDate}, #{lastModifiedDate})
</insert>
<!-- findByIdWithAssociations (用于 convertToDto,如果需要加载关联对象) -->
<select id="findByIdWithAssociations" resultMap="CouponTemplateWithAssociationsResultMap">
SELECT
ct.id as ct_id, ct.name as ct_name, ct.type as ct_type, ct.value as ct_value,
ct.discount_rate as ct_discount_rate, ct.threshold as ct_threshold,
ct.total as ct_total, ct.issued as ct_issued, ct.limit_per_user as ct_limit_per_user,
ct.valid_from as ct_valid_from, ct.valid_to as ct_valid_to, ct.description as ct_description,
ct.status as ct_status, ct.currency_id as ct_currency_id, ct.mini_program_id as ct_mini_program_id,
ct.stackable as ct_stackable, ct.created_date as ct_created_date, ct.last_modified_date as ct_last_modified_date,
mpc.id as mpc_id, mpc.name as mpc_name, mpc.app_id as mpc_app_id,
cur.id as cur_id, cur.code as cur_code, cur.symbol as cur_symbol, cur.name as cur_name
FROM coupon_template ct
JOIN mini_program_config mpc ON ct.mini_program_id = mpc.id
JOIN currency cur ON ct.currency_id = cur.id
WHERE ct.id = #{id}
</resultMap>
<resultMap id="CouponTemplateWithAssociationsResultMap" type="com.productQualification.coupon.domain.CouponTemplate" extends="CouponTemplateResultMap">
<association property="miniProgramConfig" javaType="com.productQualification.coupon.domain.MiniProgramConfig">
<id property="id" column="mpc_id"/>
<result property="name" column="mpc_name"/>
<result property="appId" column="mpc_app_id"/>
</association>
<association property="currency" javaType="com.productQualification.coupon.domain.Currency">
<id property="id" column="cur_id"/>
<result property="code" column="cur_code"/>
<result property="name" column="cur_name"/>
<result property="symbol" column="cur_symbol"/>
</association>
</resultMap>
</mapper>
XML (Extensible Markup Language, 可扩展标记语言) 关键点:
MiniProgramConfigMapper.xml: 提供findByIdWithCurrency方法,一次性查出小程序配置及其关联的币种信息。CouponTemplateMapper.xml:existsByNameAndMiniProgramId: 用于名称唯一性校验。insertCouponTemplate: 插入语句,包含所有业务字段和手动设置的时间戳。findByIdWithAssociations和CouponTemplateWithAssociationsResultMap: 用于在创建成功后,如果需要返回包含小程序名称和币种名称的 DTO (Data Transfer Object, 数据传输对象),可以调用此方法获取完整的实体(已 JOIN (一种 SQL (Structured Query Language, 结构化查询语言) 操作,用于基于相关列组合来自两个或多个表的行) 关联数据)。
4. 定义 MyBatis Mapper 接口
MiniProgramConfigMapper.java:
@Mapper
public interface MiniProgramConfigMapper {
Optional<MiniProgramConfig> findByIdWithCurrency(@Param("id") Integer id);
}
CouponTemplateMapper.java:
@Mapper
public interface CouponTemplateMapper {
boolean existsByNameAndMiniProgramId(@Param("name") String name, @Param("miniProgramId") Integer miniProgramId);
int insertCouponTemplate(CouponTemplate couponTemplate);
Optional<CouponTemplate> findByIdWithAssociations(@Param("id") Integer id); // 用于获取完整信息以转换DTO
}
5. Service 层改造:CouponTemplateService.java (MyBatis 版)
package com.productQualification.coupon.service;
import com.productQualification.common.exception.MyRuntimeException;
import com.productQualification.common.exception.NotFoundException;
import com.productQualification.common.exception.PermissionDeniedException;
import com.productQualification.coupon.domain.CouponTemplate;
import com.productQualification.coupon.domain.Currency;
import com.productQualification.coupon.domain.MiniProgramConfig;
import com.productQualification.coupon.dto.CouponTemplateDto;
import com.productQualification.coupon.dto.CouponTemplatePayload;
import com.productQualification.coupon.mapper.AdminMiniProgramMapper; // MyBatis Mapper
import com.productQualification.coupon.mapper.CouponTemplateMapper; // MyBatis Mapper
import com.productQualification.coupon.mapper.MiniProgramConfigMapper; // MyBatis Mapper
// CurrencyRepository 可以保留用于 convertToDto 中根据ID查找,或者也改为Mapper
import com.productQualification.coupon.repository.CurrencyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.Validator; // 如果需要手动校验实体
import java.math.BigDecimal;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
import javax.validation.ConstraintViolation;
@Service
public class CouponTemplateService {
private static final Logger logger = LoggerFactory.getLogger(CouponTemplateService.class);
private final CouponTemplateMapper couponTemplateMapper;
private final MiniProgramConfigMapper miniProgramConfigMapper; // 使用 MyBatis Mapper
private final AdminMiniProgramMapper adminMiniProgramMapper; // 使用 MyBatis Mapper
private final CurrencyRepository currencyRepository; // 暂时保留JPA,或改为CurrencyMapper
private final Validator validator; // 用于手动校验
// ... (常量定义) ...
public static final byte TYPE_THRESHOLD_VOUCHER = 1; /* ... */
@Autowired
public CouponTemplateService(CouponTemplateMapper couponTemplateMapper,
MiniProgramConfigMapper miniProgramConfigMapper,
AdminMiniProgramMapper adminMiniProgramMapper,
CurrencyRepository currencyRepository,
Validator validator) {
this.couponTemplateMapper = couponTemplateMapper;
this.miniProgramConfigMapper = miniProgramConfigMapper;
this.adminMiniProgramMapper = adminMiniProgramMapper;
this.currencyRepository = currencyRepository;
this.validator = validator;
}
@Transactional
public CouponTemplateDto createCouponTemplate(CouponTemplatePayload payload, Integer adminId) {
logger.info("Admin {} creating coupon template (MyBatis): {}", adminId, payload);
// 1. 校验小程序ID并获取其币种信息 (使用MyBatis Mapper)
MiniProgramConfig miniProgram = miniProgramConfigMapper.findByIdWithCurrency(payload.getMiniProgramId())
.orElseThrow(() -> new NotFoundException("指定的小程序ID不存在: " + payload.getMiniProgramId()));
if (miniProgram.getCurrencyId() == null || miniProgram.getCurrency() == null) { // Currency 对象也应存在
throw new MyRuntimeException("小程序 (ID: " + miniProgram.getId() + ") 未配置默认币种或币种信息不完整。");
}
Integer currencyIdFromMiniProgram = miniProgram.getCurrencyId();
Currency currencyForDto = miniProgram.getCurrency(); // 直接从加载的 miniProgram 对象获取
// 2. 校验管理员权限 (使用MyBatis Mapper)
checkAdminPermissionForMiniProgram(adminId, miniProgram.getId());
// 3. 校验模板名称唯一性 (使用MyBatis Mapper)
if (couponTemplateMapper.existsByNameAndMiniProgramId(payload.getName(), payload.getMiniProgramId())) {
throw new MyRuntimeException("优惠券模板名称 '" + payload.getName() + "' 在小程序下已存在。");
}
// 4. 校验有效期
if (payload.getValidFrom() == null || payload.getValidTo() == null || payload.getValidFrom().after(payload.getValidTo())) {
throw new MyRuntimeException("优惠券有效期设置无效。");
}
CouponTemplate newTemplate = new CouponTemplate();
mapPayloadToEntity(payload, newTemplate);
newTemplate.setCurrencyId(currencyIdFromMiniProgram);
newTemplate.setIssued(0);
Date now = new Date();
newTemplate.setCreatedDate(now); // 手动设置创建时间
newTemplate.setLastModifiedDate(now); // 手动设置最后修改时间
validateAndSetTypeValueAndRate(payload, newTemplate);
validateEntity(newTemplate); // 手动触发实体校验
couponTemplateMapper.insertCouponTemplate(newTemplate); // newTemplate.id() 会被填充
logger.info("Coupon template created with ID: {} by admin {}", newTemplate.getId(), adminId);
// 为了返回包含完整关联信息的DTO,可以根据刚插入的ID重新查询一次(包含JOIN)
// 或者,如果insert后newTemplate对象中的关联字段(如miniProgramConfig, currency)能被某种方式填充,则更好
// 这里我们假设 newTemplate 已经有了 miniProgramId 和 currencyId,
// 而 miniProgram 和 currencyForDto 对象我们之前已经加载了。
// 如果 newTemplate 在 insert 后没有自动填充关联对象,我们需要手动设置它们或重新查询。
// 最简单的方式是,既然我们已经有了 miniProgram 和 currencyForDto 对象,直接用它们来构建 DTO。
// newTemplate.setMiniProgramConfig(miniProgram); // 手动设置,以便 convertToDto 能获取
// newTemplate.setCurrency(currencyForDto); // 手动设置
// 或者,更推荐的做法是,如果 DTO 只需要 ID 和名称等,直接用已加载的 miniProgram 和 currencyForDto 对象
return convertToDto(newTemplate, miniProgram, currencyForDto);
}
private void mapPayloadToEntity(CouponTemplatePayload payload, CouponTemplate entity) { /* ... (同JPA版本) ... */ }
private void validateAndSetTypeValueAndRate(CouponTemplatePayload payload, CouponTemplate entity) { /* ... (同JPA版本) ... */ }
private void checkAdminPermissionForMiniProgram(Integer adminId, Integer miniProgramId) {
if (!adminMiniProgramMapper.existsByAdminIdAndMiniProgramId(adminId, miniProgramId)) {
throw new PermissionDeniedException("管理员无权操作此小程序 (ID: " + miniProgramId + ")");
}
}
private <T> void validateEntity(T entity) {
Set<ConstraintViolation<T>> violations = validator.validate(entity);
if (!violations.isEmpty()) { /* ...抛出异常... */ }
}
private CouponTemplateDto convertToDto(CouponTemplate entity, MiniProgramConfig mpConfig, Currency currency) {
// ... (DTO 转换逻辑,同JPA版本,确保使用传入的mpConfig和currency) ...
if (entity == null) return null;
CouponTemplateDto dto = new CouponTemplateDto();
// ... 基础属性 ...
dto.setId(entity.getId());
dto.setName(entity.getName());
// ...
dto.setMiniProgramId(entity.getMiniProgramId());
dto.setCurrencyId(entity.getCurrencyId());
dto.setCreatedDate(entity.getCreatedDate());
dto.setLastModifiedDate(entity.getLastModifiedDate());
if (mpConfig != null) {
dto.setMiniProgramName(mpConfig.getName());
}
if (currency != null) {
dto.setCurrencyCode(currency.getCode());
dto.setCurrencySymbol(currency.getSymbol());
}
return dto;
}
// ... update 和 find 方法也需要用 MyBatis 实现 ...
}
MyBatis 版本 Service 层改动要点:
- 依赖注入: 注入 MyBatis Mapper 接口。
- 数据操作: 调用 Mapper 方法执行 SQL (Structured Query Language, 结构化查询语言)。
- 时间戳管理: 必须手动在创建
CouponTemplate实体时设置createdDate和lastModifiedDate。 - 实体校验: 注入
javax.validation.Validator并在调用 Mapper 的insert方法前手动执行validator.validate(newTemplate)。 - 关联数据获取:
miniProgramConfigMapper.findByIdWithCurrency()用于一次性获取小程序及其默认币种信息。- 在
convertToDto时,直接使用从findByIdWithCurrency加载的MiniProgramConfig和Currency对象,避免了在 DTO (Data Transfer Object, 数据传输对象) 转换时再次查询。
6. Controller 层 (CouponTemplateController.java)
Controller 层无需任何改动,因为它与 Service 层的接口契约保持不变。
📊 交互时序图 (Sequence Diagram - MyBatis 创建优惠券模板)
🔄 状态图与类图 (概念上与之前博客类似)
优惠券模板创建的状态流转概念不变。类图的主要变化是 Service 层现在依赖 MyBatis Mapper 接口。
简化类图 (突出 MyBatis 依赖):
💡 英文缩写全称及中文解释
- API: Application Programming Interface (应用程序编程接口)
- CRUD: Create, Read, Update, Delete (增删改查)
- DB: DataBase (数据库)
- DTO: Data Transfer Object (数据传输对象)
- ID: Identifier (标识符)
- JPA: Jakarta Persistence API (formerly Java Persistence API) (Jakarta 持久化应用程序接口)
- JSON: JavaScript Object Notation (JavaScript 对象表示法)
- ORM: Object-Relational Mapping (对象关系映射)
- POJO: Plain Old Java Object (简单Java对象)
- SQL: Structured Query Language (结构化查询语言)
- XML: Extensible Markup Language (可扩展标记语言)
🧠 思维导图 (Markdown 格式)

🎉 总结:MyBatis 下的智能与掌控!
通过将优惠券模板的币种设置逻辑后移到后端,并利用 MyBatis 的灵活性,我们成功实现了一个既智能又数据一致的创建接口。Service 层通过查询小程序配置自动获取并设置 currencyId,不仅简化了前端的负担,也从源头上保证了优惠券币种与其应用环境的统一。
MyBatis 在这个过程中,让我们能够精确地定义数据加载(通过 resultMap 和 <association> 一次性获取小程序及其币种信息)和数据持久化(通过 insert 语句包含所有必要字段,包括手动设置的时间戳)。虽然需要更多的 SQL (Structured Query Language, 结构化查询语言) 和 XML (Extensible Markup Language, 可扩展标记语言) 配置,但这种对数据访问的完全掌控,正是 MyBatis 在特定场景下的魅力所在。
希望这篇基于 MyBatis 实现的智能优惠券模板创建博客,能为你提供有价值的参考!如果你对 MyBatis 的高级映射或 SQL (Structured Query Language, 结构化查询语言) 优化有更多心得,欢迎在评论区分享!👇 Happy SQL (Structured Query Language, 结构化查询语言) with Precision! 💻🎫
155

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



