feat(purchase): 新增 Apple 内购完整链路
- AppleReceiptController 改造:验签后立刻落库并解锁权益 - 新增 ApplePurchaseService 处理业务:防重、写订单、发道具 - 新增 KeyboardUserPurchaseRecords 实体与 Mapper,记录用户购买 - ErrorCode 补充 RECEIPT_INVALID(50016) - 删除过期 AGENTS.md,修正 i18n_message 表名与 CORS 白名单
This commit is contained in:
33
AGENTS.md
33
AGENTS.md
@@ -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.
|
||||
2
pom.xml
2
pom.xml
@@ -101,7 +101,7 @@
|
||||
<dependency>
|
||||
<groupId>com.apple.itunes.storekit</groupId>
|
||||
<artifactId>app-store-server-library</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>4.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- x-file-storage -->
|
||||
|
||||
@@ -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, "收据无效");
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
|
||||
@@ -97,7 +97,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
"/products/listByType",
|
||||
"/products/detail",
|
||||
"/products/inApp/list",
|
||||
"/products/subscription/list"
|
||||
"/products/subscription/list",
|
||||
"/purchase/handle"
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<String, String> body) {
|
||||
// String receipt = body.get("receipt");
|
||||
// return appleReceiptService.validateReceipt(receipt);
|
||||
// }
|
||||
|
||||
@PostMapping("/validate-receipt")
|
||||
public AppleReceiptValidationResult validateReceipt(@RequestBody Map<String, String> body) {
|
||||
public BaseResponse<Boolean> handlePurchase(@RequestBody Map<String, String> 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<KeyboardUserPurchaseRecords> {
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import lombok.Data;
|
||||
|
||||
@Schema(description="多语言消息表")
|
||||
@Data
|
||||
@TableName("i18n_message")
|
||||
@TableName("keyboard_i18n_message")
|
||||
public class I18nMessage {
|
||||
@TableId("id")
|
||||
@Schema(description="主键")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ public interface KeyboardProductItemsService extends IService<KeyboardProductIte
|
||||
*/
|
||||
KeyboardProductItemRespVO getProductDetailByProductId(String productId);
|
||||
|
||||
/**
|
||||
* 根据 productId 获取商品实体
|
||||
*/
|
||||
KeyboardProductItems getProductEntityByProductId(String productId);
|
||||
|
||||
/**
|
||||
* 根据商品类型查询商品列表
|
||||
* type=all 时返回全部
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.yolo.keyborad.service;
|
||||
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12 15:16
|
||||
*/
|
||||
|
||||
public interface KeyboardUserPurchaseRecordsService extends IService<KeyboardUserPurchaseRecords>{
|
||||
|
||||
|
||||
}
|
||||
@@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,16 @@ public class KeyboardProductItemsServiceImpl extends ServiceImpl<KeyboardProduct
|
||||
return item == null ? null : BeanUtil.copyProperties(item, KeyboardProductItemRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyboardProductItems getProductEntityByProductId(String productId) {
|
||||
if (productId == null || productId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return this.lambdaQuery()
|
||||
.eq(KeyboardProductItems::getProductId, productId)
|
||||
.one();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据类型获取产品列表
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.yolo.keyborad.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import java.util.List;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yolo.keyborad.mapper.KeyboardUserPurchaseRecordsMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords;
|
||||
import com.yolo.keyborad.service.KeyboardUserPurchaseRecordsService;
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12 15:16
|
||||
*/
|
||||
|
||||
@Service
|
||||
public class KeyboardUserPurchaseRecordsServiceImpl extends ServiceImpl<KeyboardUserPurchaseRecordsMapper, KeyboardUserPurchaseRecords> implements KeyboardUserPurchaseRecordsService{
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<mapper namespace="com.yolo.keyborad.mapper.I18nMessageMapper">
|
||||
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.I18nMessage">
|
||||
<!--@mbg.generated-->
|
||||
<!--@Table i18n_message-->
|
||||
<!--@Table keyboard_i18n_message-->
|
||||
<id column="id" jdbcType="BIGINT" property="id" />
|
||||
<result column="code" jdbcType="VARCHAR" property="code" />
|
||||
<result column="locale" jdbcType="VARCHAR" property="locale" />
|
||||
@@ -15,6 +15,6 @@
|
||||
</sql>
|
||||
|
||||
<select id="selectByCodeAndLocale" resultMap="BaseResultMap">
|
||||
SELECT * FROM "i18n_message" WHERE code = #{code,jdbcType=VARCHAR} AND locale = #{locale,jdbcType=VARCHAR}
|
||||
SELECT * FROM "keyboard_i18n_message" WHERE code = #{code,jdbcType=VARCHAR} AND locale = #{locale,jdbcType=VARCHAR}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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.yolo.keyborad.mapper.KeyboardUserPurchaseRecordsMapper">
|
||||
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords">
|
||||
<!--@mbg.generated-->
|
||||
<!--@Table keyboard_user_purchase_records-->
|
||||
<id column="id" jdbcType="INTEGER" property="id" />
|
||||
<result column="user_id" jdbcType="INTEGER" property="userId" />
|
||||
<result column="product_id" jdbcType="VARCHAR" property="productId" />
|
||||
<result column="purchase_quantity" jdbcType="INTEGER" property="purchaseQuantity" />
|
||||
<result column="price" jdbcType="NUMERIC" property="price" />
|
||||
<result column="currency" jdbcType="VARCHAR" property="currency" />
|
||||
<result column="purchase_time" jdbcType="TIMESTAMP" property="purchaseTime" />
|
||||
<result column="purchase_type" jdbcType="VARCHAR" property="purchaseType" />
|
||||
<result column="status" jdbcType="VARCHAR" property="status" />
|
||||
<result column="payment_method" jdbcType="VARCHAR" property="paymentMethod" />
|
||||
<result column="transaction_id" jdbcType="VARCHAR" property="transactionId" />
|
||||
<result column="original_transaction_id" jdbcType="VARCHAR" property="originalTransactionId" />
|
||||
<result column="product_ids" jdbcType="VARCHAR" property="productIds" />
|
||||
<result column="purchase_date" jdbcType="TIMESTAMP" property="purchaseDate" />
|
||||
<result column="expires_date" jdbcType="TIMESTAMP" property="expiresDate" />
|
||||
<result column="environment" jdbcType="VARCHAR" property="environment" />
|
||||
</resultMap>
|
||||
<sql id="Base_Column_List">
|
||||
<!--@mbg.generated-->
|
||||
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
|
||||
</sql>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user