From a24a79588776810563489d18c00cbc09e069694f Mon Sep 17 00:00:00 2001 From: ziin Date: Fri, 12 Dec 2025 18:18:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(purchase):=20=E6=96=B0=E5=A2=9E=20Apple=20?= =?UTF-8?q?=E5=86=85=E8=B4=AD=E5=AE=8C=E6=95=B4=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppleReceiptController 改造:验签后立刻落库并解锁权益 - 新增 ApplePurchaseService 处理业务:防重、写订单、发道具 - 新增 KeyboardUserPurchaseRecords 实体与 Mapper,记录用户购买 - ErrorCode 补充 RECEIPT_INVALID(50016) - 删除过期 AGENTS.md,修正 i18n_message 表名与 CORS 白名单 --- AGENTS.md | 33 --- pom.xml | 2 +- .../com/yolo/keyborad/common/ErrorCode.java | 3 +- .../keyborad/config/SaTokenConfigure.java | 3 +- .../controller/AppleReceiptController.java | 30 ++- .../KeyboardUserPurchaseRecordsMapper.java | 12 + .../keyborad/model/entity/I18nMessage.java | 2 +- .../entity/KeyboardUserPurchaseRecords.java | 134 +++++++++++ .../service/ApplePurchaseService.java | 18 ++ .../service/KeyboardProductItemsService.java | 5 + .../KeyboardUserPurchaseRecordsService.java | 13 ++ .../impl/ApplePurchaseServiceImpl.java | 216 ++++++++++++++++++ .../impl/KeyboardProductItemsServiceImpl.java | 10 + ...eyboardUserPurchaseRecordsServiceImpl.java | 18 ++ .../resources/mapper/I18nMessageMapper.xml | 4 +- .../KeyboardUserPurchaseRecordsMapper.xml | 30 +++ 16 files changed, 491 insertions(+), 42 deletions(-) delete mode 100644 AGENTS.md create mode 100644 src/main/java/com/yolo/keyborad/mapper/KeyboardUserPurchaseRecordsMapper.java create mode 100644 src/main/java/com/yolo/keyborad/model/entity/KeyboardUserPurchaseRecords.java create mode 100644 src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java create mode 100644 src/main/java/com/yolo/keyborad/service/KeyboardUserPurchaseRecordsService.java create mode 100644 src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java create mode 100644 src/main/java/com/yolo/keyborad/service/impl/KeyboardUserPurchaseRecordsServiceImpl.java create mode 100644 src/main/resources/mapper/KeyboardUserPurchaseRecordsMapper.xml 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