diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index e048019..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Repository Guidelines
-
-## Project Structure & Module Organization
-- Entrypoint `src/main/java/com/yolo/keyborad/MyApplication.java`; feature code organized by layer: `controller` (REST), `service` (business), `mapper` (MyBatis mappers), `model`/`common`/`constant` for DTOs, responses, and constants, plus `config`, `aop`, `annotation`, `Interceptor`, and `utils` for cross-cutting concerns.
-- Resource configs live in `src/main/resources`: `application.yml` with `application-dev.yml`/`application-prod.yml` profiles, mapper XML files under `mapper/`, and platform keys/certs (Apple, mail, storage). Keep secrets out of commits.
-- Tests belong in `src/test/java/com/yolo/keyborad/...` mirroring package names; add fixtures alongside tests when needed.
-
-## Build, Test, and Development Commands
-- `./mvnw clean install` — full build with tests; requires JDK 17.
-- `./mvnw test` — run test suite only.
-- `./mvnw spring-boot:run -Dspring-boot.run.profiles=dev` — start the API with the dev profile (loads `application-dev.yml`).
-- `./mvnw clean package -DskipTests` — create an artifact when tests are already covered elsewhere.
-
-## Coding Style & Naming Conventions
-- Java 17, Spring Boot 3.5, MyBatis/MyBatis-Plus; prefer Lombok for boilerplate (`@Data`, `@Builder`) and constructor injection for services.
-- Use 4-space indentation, lowercase package names, `UpperCamelCase` for classes, `lowerCamelCase` for fields/params.
-- Controllers end with `*Controller`, services with `*Service`, mapper interfaces with `*Mapper`, and request/response DTOs under `model` or `common` with clear suffixes like `Request`/`Response`.
-- Keep configuration isolated in `config`; shared constants in `constant`; AOP/logging in `aop`; custom annotations in `annotation`.
-
-## Testing Guidelines
-- Use Spring Boot Test + JUnit (via `spring-boot-starter-test`, JUnit 4/5 support) and MockMvc/WebTestClient for HTTP layers when practical.
-- Name classes `*Test` and align packages with the code under test. Cover service logic, mappers, and controller contracts (status + payload shape).
-- For data-access tests, use in-memory setups or dedicated test containers and clean up test data.
-
-## Commit & Pull Request Guidelines
-- Follow the existing conventional style seen in history (e.g., `feat(user): add email registration`); keep scope lowercase and concise.
-- PRs should describe the change, list validation steps/commands run, call out config/profile impacts, and link issues/tasks. Add screenshots or sample requests/responses for API-facing changes when helpful.
-- Ensure secrets (p8 certificates, Mailgun keys, AWS creds) are never committed; rely on environment variables or local config overrides.
-
-## Security & Configuration Tips
-- Activate the intended profile via `SPRING_PROFILES_ACTIVE` or `-Dspring-boot.run.profiles`. Keep `application-dev.yml` local-only; never hardcode production endpoints or credentials.
-- Validate signing/encryption helpers (`SignInterceptor`, JWT, Apple receipt validation) with representative non-production keys before merging.
-- Log only necessary context; avoid logging tokens, receipts, or PII.
diff --git a/pom.xml b/pom.xml
index 4311ad1..ad209e9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -101,7 +101,7 @@
com.apple.itunes.storekit
app-store-server-library
- 3.6.0
+ 4.0.0
diff --git a/src/main/java/com/yolo/keyborad/common/ErrorCode.java b/src/main/java/com/yolo/keyborad/common/ErrorCode.java
index f46962b..82d8eb3 100644
--- a/src/main/java/com/yolo/keyborad/common/ErrorCode.java
+++ b/src/main/java/com/yolo/keyborad/common/ErrorCode.java
@@ -50,7 +50,8 @@ public enum ErrorCode {
INSUFFICIENT_BALANCE(50013, "余额不足"),
THEME_NOT_FOUND(40410, "主题不存在"),
THEME_ALREADY_PURCHASED(50014, "主题已购买"),
- THEME_NOT_AVAILABLE(50015, "主题不可购买");
+ THEME_NOT_AVAILABLE(50015, "主题不可购买"),
+ RECEIPT_INVALID(50016, "收据无效");
/**
* 状态码
*/
diff --git a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java
index ae47f8a..de67f7c 100644
--- a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java
+++ b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java
@@ -97,7 +97,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/products/listByType",
"/products/detail",
"/products/inApp/list",
- "/products/subscription/list"
+ "/products/subscription/list",
+ "/purchase/handle"
};
}
diff --git a/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java b/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java
index 90e436a..6dd3e50 100644
--- a/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java
+++ b/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java
@@ -1,6 +1,12 @@
package com.yolo.keyborad.controller;
+import cn.dev33.satoken.stp.StpUtil;
+import com.yolo.keyborad.common.BaseResponse;
+import com.yolo.keyborad.common.ErrorCode;
+import com.yolo.keyborad.common.ResultUtils;
+import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
+import com.yolo.keyborad.service.ApplePurchaseService;
import com.yolo.keyborad.service.AppleReceiptService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -14,14 +20,32 @@ import java.util.Map;
public class AppleReceiptController {
private final AppleReceiptService appleReceiptService;
+ private final ApplePurchaseService applePurchaseService;
- public AppleReceiptController(AppleReceiptService appleReceiptService) {
+ public AppleReceiptController(AppleReceiptService appleReceiptService,
+ ApplePurchaseService applePurchaseService) {
this.appleReceiptService = appleReceiptService;
+ this.applePurchaseService = applePurchaseService;
}
+// @PostMapping("/validate-receipt")
+// public AppleReceiptValidationResult validateReceipt(@RequestBody Map body) {
+// String receipt = body.get("receipt");
+// return appleReceiptService.validateReceipt(receipt);
+// }
+
@PostMapping("/validate-receipt")
- public AppleReceiptValidationResult validateReceipt(@RequestBody Map body) {
+ public BaseResponse handlePurchase(@RequestBody Map body) {
String receipt = body.get("receipt");
- return appleReceiptService.validateReceipt(receipt);
+ if (receipt == null || receipt.isBlank()) {
+ throw new BusinessException(ErrorCode.PARAMS_ERROR, "receipt 不能为空");
+ }
+ Long userId = StpUtil.getLoginIdAsLong();
+ AppleReceiptValidationResult validationResult = appleReceiptService.validateReceipt(receipt);
+ applePurchaseService.processPurchase(userId, validationResult);
+ return ResultUtils.success(Boolean.TRUE);
}
+
+
+
}
diff --git a/src/main/java/com/yolo/keyborad/mapper/KeyboardUserPurchaseRecordsMapper.java b/src/main/java/com/yolo/keyborad/mapper/KeyboardUserPurchaseRecordsMapper.java
new file mode 100644
index 0000000..6e0acf0
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/mapper/KeyboardUserPurchaseRecordsMapper.java
@@ -0,0 +1,12 @@
+package com.yolo.keyborad.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords;
+
+/*
+* @author: ziin
+* @date: 2025/12/12 15:16
+*/
+
+public interface KeyboardUserPurchaseRecordsMapper extends BaseMapper {
+}
\ No newline at end of file
diff --git a/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java b/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java
index 73f97dd..472b3ce 100644
--- a/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java
+++ b/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java
@@ -13,7 +13,7 @@ import lombok.Data;
@Schema(description="多语言消息表")
@Data
-@TableName("i18n_message")
+@TableName("keyboard_i18n_message")
public class I18nMessage {
@TableId("id")
@Schema(description="主键")
diff --git a/src/main/java/com/yolo/keyborad/model/entity/KeyboardUserPurchaseRecords.java b/src/main/java/com/yolo/keyborad/model/entity/KeyboardUserPurchaseRecords.java
new file mode 100644
index 0000000..934e7ea
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/model/entity/KeyboardUserPurchaseRecords.java
@@ -0,0 +1,134 @@
+package com.yolo.keyborad.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yolo.keyborad.typehandler.StringArrayTypeHandler;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+import java.util.Date;
+import lombok.Data;
+import org.apache.ibatis.type.JdbcType;
+
+/*
+* @author: ziin
+* @date: 2025/12/12 15:16
+*/
+
+@Schema
+@Data
+@TableName(value = "keyboard_user_purchase_records")
+public class KeyboardUserPurchaseRecords {
+ /**
+ * 主键,自增,唯一标识每条购买记录
+ */
+ @TableId(value = "id", type = IdType.AUTO)
+ @Schema(description="主键,自增,唯一标识每条购买记录")
+ private Integer id;
+
+ /**
+ * 用户 ID,关联到用户表,表示是哪位用户购买了产品
+ */
+ @TableField(value = "user_id")
+ @Schema(description="用户 ID,关联到用户表,表示是哪位用户购买了产品")
+ private Integer userId;
+
+ /**
+ * 购买的产品 ID,关联到产品表
+ */
+ @TableField(value = "product_id")
+ @Schema(description="购买的产品 ID,关联到产品表")
+ private String productId;
+
+ /**
+ * 购买数量(如内购的金币数量,订阅的时长)
+ */
+ @TableField(value = "purchase_quantity")
+ @Schema(description="购买数量(如内购的金币数量,订阅的时长)")
+ private Integer purchaseQuantity;
+
+ /**
+ * 实际支付价格
+ */
+ @TableField(value = "price")
+ @Schema(description="实际支付价格")
+ private BigDecimal price;
+
+ /**
+ * 货币类型(如美元 $)
+ */
+ @TableField(value = "currency")
+ @Schema(description="货币类型(如美元 $)")
+ private String currency;
+
+ /**
+ * 购买时间
+ */
+ @TableField(value = "purchase_time")
+ @Schema(description="购买时间")
+ private Date purchaseTime;
+
+ /**
+ * 购买类型(如内购,订阅)
+ */
+ @TableField(value = "purchase_type")
+ @Schema(description="购买类型(如内购,订阅)")
+ private String purchaseType;
+
+ /**
+ * 购买状态(如已支付,待支付,退款)
+ */
+ @TableField(value = "\"status\"")
+ @Schema(description="购买状态(如已支付,待支付,退款)")
+ private String status;
+
+ /**
+ * 支付方式(如信用卡,支付宝等)
+ */
+ @TableField(value = "payment_method")
+ @Schema(description="支付方式(如信用卡,支付宝等)")
+ private String paymentMethod;
+
+ /**
+ * 唯一的交易 ID,用于标识该购买操作
+ */
+ @TableField(value = "transaction_id")
+ @Schema(description="唯一的交易 ID,用于标识该购买操作")
+ private String transactionId;
+
+ /**
+ * 苹果的原始交易 ID
+ */
+ @TableField(value = "original_transaction_id")
+ @Schema(description="苹果的原始交易 ID")
+ private String originalTransactionId;
+
+ /**
+ * 购买的产品 ID 列表(JSON 格式或数组)
+ */
+ @TableField(value = "product_ids", typeHandler = StringArrayTypeHandler.class, jdbcType = JdbcType.ARRAY)
+ @Schema(description="购买的产品 ID 列表(JSON 格式或数组)")
+ private String[] productIds;
+
+ /**
+ * 苹果返回的购买时间
+ */
+ @TableField(value = "purchase_date")
+ @Schema(description="苹果返回的购买时间")
+ private Date purchaseDate;
+
+ /**
+ * 苹果返回的过期时间(如果有)
+ */
+ @TableField(value = "expires_date")
+ @Schema(description="苹果返回的过期时间(如果有)")
+ private Date expiresDate;
+
+ /**
+ * 苹果的环境(如 Sandbox 或 Production)
+ */
+ @TableField(value = "environment")
+ @Schema(description="苹果的环境(如 Sandbox 或 Production)")
+ private String environment;
+}
\ No newline at end of file
diff --git a/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java b/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java
new file mode 100644
index 0000000..3380b66
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java
@@ -0,0 +1,18 @@
+package com.yolo.keyborad.service;
+
+import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
+
+/**
+ * 处理苹果购买后的业务逻辑
+ */
+public interface ApplePurchaseService {
+
+ /**
+ * 基于验签结果处理购买逻辑(订阅 / 内购)
+ *
+ * @param userId 用户ID
+ * @param validationResult 苹果验签结果
+ */
+ void processPurchase(Long userId, AppleReceiptValidationResult validationResult);
+}
+
diff --git a/src/main/java/com/yolo/keyborad/service/KeyboardProductItemsService.java b/src/main/java/com/yolo/keyborad/service/KeyboardProductItemsService.java
index 8a2f7f1..10b4c0b 100644
--- a/src/main/java/com/yolo/keyborad/service/KeyboardProductItemsService.java
+++ b/src/main/java/com/yolo/keyborad/service/KeyboardProductItemsService.java
@@ -27,6 +27,11 @@ public interface KeyboardProductItemsService extends IService{
+
+
+}
diff --git a/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java
new file mode 100644
index 0000000..78e7499
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java
@@ -0,0 +1,216 @@
+package com.yolo.keyborad.service.impl;
+
+import com.yolo.keyborad.common.ErrorCode;
+import com.yolo.keyborad.exception.BusinessException;
+import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
+import com.yolo.keyborad.model.entity.KeyboardProductItems;
+import com.yolo.keyborad.model.entity.KeyboardUser;
+import com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords;
+import com.yolo.keyborad.model.entity.KeyboardUserWallet;
+import com.yolo.keyborad.model.entity.KeyboardWalletTransaction;
+import com.yolo.keyborad.service.ApplePurchaseService;
+import com.yolo.keyborad.service.KeyboardProductItemsService;
+import com.yolo.keyborad.service.KeyboardUserPurchaseRecordsService;
+import com.yolo.keyborad.service.KeyboardUserWalletService;
+import com.yolo.keyborad.service.KeyboardWalletTransactionService;
+import com.yolo.keyborad.service.UserService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 苹果购买后置处理:订阅续期 / 内购充值 + 记录落库
+ */
+@Service
+@Slf4j
+public class ApplePurchaseServiceImpl implements ApplePurchaseService {
+
+ @Resource
+ private KeyboardProductItemsService productItemsService;
+
+ @Resource
+ private KeyboardUserPurchaseRecordsService purchaseRecordsService;
+
+ @Resource
+ private KeyboardUserWalletService walletService;
+
+ @Resource
+ private KeyboardWalletTransactionService walletTransactionService;
+
+ @Resource
+ private UserService userService;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void processPurchase(Long userId, AppleReceiptValidationResult validationResult) {
+ if (validationResult == null || !validationResult.isValid()) {
+ throw new BusinessException(ErrorCode.RECEIPT_INVALID);
+ }
+ String productId = resolveProductId(validationResult.getProductIds());
+ // 幂等:transactionId 已经处理过则跳过
+ boolean handled = purchaseRecordsService.lambdaQuery()
+ .eq(KeyboardUserPurchaseRecords::getTransactionId, validationResult.getTransactionId())
+ .eq(KeyboardUserPurchaseRecords::getStatus, "PAID")
+ .exists();
+ if (handled) {
+ log.info("Apple purchase already handled, transactionId={}", validationResult.getTransactionId());
+ return;
+ }
+
+ KeyboardProductItems product = productItemsService.getProductEntityByProductId(productId);
+ if (product == null) {
+ log.error("Apple purchase not found, productId={}", productId);
+ throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "商品不存在: " + productId);
+ }
+
+ // 写购买记录
+ KeyboardUserPurchaseRecords purchaseRecord = buildPurchaseRecord(userId, validationResult, product);
+ purchaseRecordsService.save(purchaseRecord);
+
+ if ("subscription".equalsIgnoreCase(product.getType())) {
+ handleSubscription(userId, product, validationResult);
+ } else if ("in-app-purchase".equalsIgnoreCase(product.getType())) {
+ handleInAppPurchase(userId, product, purchaseRecord.getId());
+ } else {
+ throw new BusinessException(ErrorCode.OPERATION_ERROR, "未知商品类型: " + product.getType());
+ }
+ }
+
+ private void handleSubscription(Long userId, KeyboardProductItems product, AppleReceiptValidationResult validationResult) {
+ KeyboardUser user = userService.getById(userId);
+ if (user == null) {
+ throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+ }
+ Instant base = resolveBaseExpiry(user.getVipExpiry());
+ Instant newExpiry = base.plus(resolveDurationDays(product), ChronoUnit.DAYS);
+ user.setIsVip(true);
+ user.setVipExpiry(Date.from(newExpiry));
+ boolean updated = userService.updateById(user);
+ if (!updated) {
+ throw new BusinessException(ErrorCode.OPERATION_ERROR, "更新用户VIP失败");
+ }
+ log.info("Extend VIP for user {} to {}", userId, newExpiry);
+ }
+
+ private void handleInAppPurchase(Long userId, KeyboardProductItems product, Integer purchaseRecordId) {
+ KeyboardUserWallet wallet = walletService.lambdaQuery()
+ .eq(KeyboardUserWallet::getUserId, userId)
+ .one();
+ if (wallet == null) {
+ wallet = new KeyboardUserWallet();
+ wallet.setUserId(userId);
+ wallet.setBalance(BigDecimal.ZERO);
+ wallet.setStatus((short) 1);
+ wallet.setVersion(0);
+ wallet.setCreatedAt(new Date());
+ }
+
+ BigDecimal credit = resolveCreditAmount(product);
+ if (credit.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new BusinessException(ErrorCode.OPERATION_ERROR, "商品额度未配置");
+ }
+ BigDecimal before = wallet.getBalance() == null ? BigDecimal.ZERO : wallet.getBalance();
+ BigDecimal after = before.add(credit);
+ wallet.setBalance(after);
+ wallet.setUpdatedAt(new Date());
+ walletService.saveOrUpdate(wallet);
+
+ KeyboardWalletTransaction tx = walletTransactionService.createTransaction(
+ userId,
+ purchaseRecordId == null ? null : purchaseRecordId.longValue(),
+ credit,
+ (short) 2, // 2: 苹果内购充值
+ before,
+ after,
+ "Apple 充值: " + product.getProductId()
+ );
+ log.info("Wallet recharge success, user={}, credit={}, txId={}", userId, credit, tx.getId());
+ }
+
+ private KeyboardUserPurchaseRecords buildPurchaseRecord(Long userId, AppleReceiptValidationResult validationResult, KeyboardProductItems product) {
+ KeyboardUserPurchaseRecords record = new KeyboardUserPurchaseRecords();
+ record.setUserId(userId.intValue());
+ record.setProductId(product.getProductId());
+ record.setPurchaseQuantity(product.getDurationValue());
+ record.setPrice(product.getPrice());
+ record.setCurrency(product.getCurrency());
+ record.setPurchaseTime(Date.from(Objects.requireNonNullElseGet(validationResult.getPurchaseDate(), Instant::now)));
+ record.setPurchaseType(product.getType());
+ record.setStatus("PAID");
+ record.setPaymentMethod("APPLE");
+ record.setTransactionId(validationResult.getTransactionId());
+ record.setOriginalTransactionId(validationResult.getOriginalTransactionId());
+ record.setProductIds(new String[]{String.join(",", validationResult.getProductIds())});
+ if (validationResult.getExpiresDate() != null) {
+ record.setExpiresDate(Date.from(validationResult.getExpiresDate()));
+ }
+ record.setEnvironment(validationResult.getEnvironment() == null ? null : validationResult.getEnvironment().name());
+ record.setPurchaseDate(validationResult.getPurchaseDate() == null ? null : Date.from(validationResult.getPurchaseDate()));
+ return record;
+ }
+
+ private String resolveProductId(List productIds) {
+ if (productIds == null || productIds.isEmpty()) {
+ throw new BusinessException(ErrorCode.PARAMS_ERROR, "productId 缺失");
+ }
+ return productIds.get(0);
+ }
+
+ private Instant resolveBaseExpiry(Date currentExpiry) {
+ if (currentExpiry == null) {
+ return Instant.now();
+ }
+ Instant current = currentExpiry.toInstant();
+ return current.isAfter(Instant.now()) ? current : Instant.now();
+ }
+
+ private long resolveDurationDays(KeyboardProductItems product) {
+ if (product.getDurationDays() != null && product.getDurationDays() > 0) {
+ return product.getDurationDays();
+ }
+ Integer value = product.getDurationValue();
+ String unit = product.getDurationUnit() == null ? "" : product.getDurationUnit().toLowerCase();
+ if (value == null || value <= 0) {
+ return 0;
+ }
+ if (unit.contains("day") || unit.contains("天")) {
+ return value;
+ }
+ if (unit.contains("month") || unit.contains("月")) {
+ return (long) value * 30;
+ }
+ return value;
+ }
+
+ private BigDecimal resolveCreditAmount(KeyboardProductItems product) {
+ // 优先使用 name 中的数值,否则退回 durationValue
+ BigDecimal fromName = parseNumber(product.getName());
+ if (fromName != null) {
+ return fromName;
+ }
+ if (product.getDurationValue() != null) {
+ return BigDecimal.valueOf(product.getDurationValue());
+ }
+ return BigDecimal.ZERO;
+ }
+
+ private BigDecimal parseNumber(String raw) {
+ if (raw == null) {
+ return null;
+ }
+ try {
+ return new BigDecimal(raw.replaceAll("[^\\d.]", ""));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/yolo/keyborad/service/impl/KeyboardProductItemsServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/KeyboardProductItemsServiceImpl.java
index cb58a70..f376e2a 100644
--- a/src/main/java/com/yolo/keyborad/service/impl/KeyboardProductItemsServiceImpl.java
+++ b/src/main/java/com/yolo/keyborad/service/impl/KeyboardProductItemsServiceImpl.java
@@ -59,6 +59,16 @@ public class KeyboardProductItemsServiceImpl extends ServiceImpl implements KeyboardUserPurchaseRecordsService{
+
+}
diff --git a/src/main/resources/mapper/I18nMessageMapper.xml b/src/main/resources/mapper/I18nMessageMapper.xml
index 28aae5d..07ca731 100644
--- a/src/main/resources/mapper/I18nMessageMapper.xml
+++ b/src/main/resources/mapper/I18nMessageMapper.xml
@@ -3,7 +3,7 @@
-
+
@@ -15,6 +15,6 @@
\ No newline at end of file
diff --git a/src/main/resources/mapper/KeyboardUserPurchaseRecordsMapper.xml b/src/main/resources/mapper/KeyboardUserPurchaseRecordsMapper.xml
new file mode 100644
index 0000000..e650d30
--- /dev/null
+++ b/src/main/resources/mapper/KeyboardUserPurchaseRecordsMapper.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, user_id, product_id, purchase_quantity, price, currency, purchase_time, purchase_type,
+ "status", payment_method, transaction_id, original_transaction_id, product_ids, purchase_date,
+ expires_date, environment
+
+
\ No newline at end of file