diff --git a/mt-pay/SERVICE_STRUCTURE.md b/mt-pay/SERVICE_STRUCTURE.md new file mode 100644 index 0000000..c077a08 --- /dev/null +++ b/mt-pay/SERVICE_STRUCTURE.md @@ -0,0 +1,135 @@ +# Service层结构说明 + +## 目录结构 + +``` +service/ +├── SignatureService.java # 签名服务接口 +├── PingPongPayService.java # PingPong支付服务接口 +├── PaymentOrderService.java # 支付订单服务接口 +├── CallbackService.java # 回调处理服务接口 +└── impl/ # 实现类目录 + ├── SignatureServiceImpl.java # 签名服务实现类 + ├── PingPongPayServiceImpl.java # PingPong支付服务实现类 + ├── PaymentOrderServiceImpl.java # 支付订单服务实现类 + └── CallbackServiceImpl.java # 回调处理服务实现类 +``` + +## 设计原则 + +### 1. 接口与实现分离 +- **service文件夹**:只存放接口文件 +- **service/impl文件夹**:存放所有实现类 + +### 2. 命名规范 +- 接口:`XxxService` +- 实现类:`XxxServiceImpl` + +### 3. 依赖注入 +- Controller层注入接口,不直接依赖实现类 +- Spring会自动根据接口找到对应的实现类(通过@Service注解) + +## Service接口说明 + +### SignatureService +**功能**:签名生成和验证服务 + +**方法**: +- `generateSign(Map params)` - 生成签名 +- `generateSign(Map params, String secret, String signType)` - 生成签名(指定密钥和类型) +- `verifySign(Map params)` - 验证签名 + +### PingPongPayService +**功能**:PingPong支付API调用服务 + +**方法**: +- `checkout(CheckoutRequestDTO request)` - 创建支付订单 + +### PaymentOrderService +**功能**:支付订单业务服务 + +**方法**: +- `createPaymentOrder(CheckoutRequestDTO request)` - 创建支付订单 +- `findByMerchantTransactionId(String merchantTransactionId)` - 根据商户订单号查询 +- `findByTransactionId(String transactionId)` - 根据PingPong交易流水号查询 +- `updateOrderStatus(String merchantTransactionId, String status, String transactionId)` - 更新订单状态 + +### CallbackService +**功能**:回调处理服务 + +**方法**: +- `handleCallback(Map callbackData)` - 处理支付回调 + +## 实现类说明 + +### SignatureServiceImpl +- 实现签名生成和验证逻辑 +- 支持MD5和SHA256签名算法 +- 自动筛选参与签名的参数 + +### PingPongPayServiceImpl +- 实现PingPong API调用 +- 自动生成请求签名 +- 验证响应签名 + +### PaymentOrderServiceImpl +- 实现支付订单业务逻辑 +- 调用PingPong API创建订单 +- 保存订单和支付记录 + +### CallbackServiceImpl +- 实现回调处理逻辑 +- 验证回调签名 +- 更新订单状态 +- 保存回调记录 + +## 使用示例 + +### Controller中使用Service + +```java +@RestController +@RequiredArgsConstructor +public class PaymentController { + // 注入接口,Spring会自动找到对应的实现类 + private final PaymentOrderService paymentOrderService; + + @PostMapping("/checkout") + public ResponseEntity checkout(@RequestBody CheckoutRequestDTO request) { + PaymentOrder order = paymentOrderService.createPaymentOrder(request); + return ResponseEntity.ok(order); + } +} +``` + +### Service实现类中注入其他Service + +```java +@Service +@RequiredArgsConstructor +public class PaymentOrderServiceImpl implements PaymentOrderService { + // 注入其他Service接口 + private final PingPongPayService pingPongPayService; + + // 实现接口方法 + @Override + public PaymentOrder createPaymentOrder(CheckoutRequestDTO request) { + // 实现逻辑 + } +} +``` + +## 优势 + +1. **解耦**:Controller只依赖接口,不依赖具体实现 +2. **扩展性**:可以轻松替换实现类,不影响调用方 +3. **测试**:可以方便地创建Mock实现进行单元测试 +4. **规范**:统一的代码结构,便于维护 + +## 注意事项 + +1. 所有Service实现类必须使用`@Service`注解 +2. 实现类必须实现对应的接口 +3. Controller中注入的是接口类型,不是实现类 +4. 如果接口有多个实现类,需要使用`@Qualifier`指定 + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/common/ResultCode.java b/mt-pay/src/main/java/com/mtkj/mtpay/common/ResultCode.java new file mode 100644 index 0000000..65e3374 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/common/ResultCode.java @@ -0,0 +1,98 @@ +package com.mtkj.mtpay.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 响应码枚举 + */ +@Getter +@AllArgsConstructor +public enum ResultCode { + + /** + * 成功 + */ + SUCCESS("0000", "操作成功"), + + /** + * 失败 + */ + FAIL("9999", "操作失败"), + + /** + * 参数错误 + */ + PARAM_ERROR("4000", "参数错误"), + + /** + * 参数验证失败 + */ + VALIDATION_ERROR("4001", "参数验证失败"), + + /** + * 未授权 + */ + UNAUTHORIZED("4002", "未授权"), + + /** + * 禁止访问 + */ + FORBIDDEN("4003", "禁止访问"), + + /** + * 资源不存在 + */ + NOT_FOUND("4004", "资源不存在"), + + /** + * 数据不存在 + */ + DATA_NOT_FOUND("4005", "数据不存在"), + + /** + * 订单不存在 + */ + ORDER_NOT_FOUND("1001", "订单不存在"), + + /** + * 订单已存在 + */ + ORDER_EXISTS("1002", "订单已存在"), + + /** + * 订单状态错误 + */ + ORDER_STATUS_ERROR("1003", "订单状态错误"), + + /** + * 签名验证失败 + */ + SIGNATURE_ERROR("2001", "签名验证失败"), + + /** + * PingPong API调用失败 + */ + PINGPONG_API_ERROR("2002", "PingPong API调用失败"), + + /** + * 系统异常 + */ + SYSTEM_ERROR("5000", "系统异常"), + + /** + * 服务不可用 + */ + SERVICE_UNAVAILABLE("5001", "服务不可用"); + + /** + * 响应码 + */ + private final String code; + + /** + * 响应消息 + */ + private final String message; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/dto/RiskInfoDTO.java b/mt-pay/src/main/java/com/mtkj/mtpay/dto/RiskInfoDTO.java new file mode 100644 index 0000000..a8ad244 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/dto/RiskInfoDTO.java @@ -0,0 +1,33 @@ +package com.mtkj.mtpay.dto; + +import com.mtkj.mtpay.dto.risk.*; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 风控信息DTO + */ +@Data +public class RiskInfoDTO implements Serializable { + + private DeviceDTO device; + + private CustomerDTO customer; + + private List goods; + + private ShippingDTO shipping; + + private BillingDTO billing; + + private EcommerceDTO ecommerce; + + private AirlineDTO airline; + + private RechargeDTO reCharge; + + private CarRentalDTO carRental; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/entity/MtProductLink.java b/mt-pay/src/main/java/com/mtkj/mtpay/entity/MtProductLink.java new file mode 100644 index 0000000..4d9d450 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/entity/MtProductLink.java @@ -0,0 +1,69 @@ +package com.mtkj.mtpay.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 商品链接实体类(用于持久化商品详情页链接) + */ +@TableName(value = "mt_product_link") +@Data +public class MtProductLink { + + /** + * 链接ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 商品ID + */ + @TableField(value = "product_id", jdbcType = org.apache.ibatis.type.JdbcType.BIGINT) + private Long productId; + + /** + * 短链接码(唯一标识,用于生成URL) + */ + @TableField(value = "link_code", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String linkCode; + + /** + * 完整URL + */ + @TableField(value = "full_url", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String fullUrl; + + /** + * 有效期(天数,默认90天) + */ + @TableField(value = "expire_days", jdbcType = org.apache.ibatis.type.JdbcType.INTEGER) + private Integer expireDays; + + /** + * 过期时间(创建时间 + 有效期) + */ + @TableField(value = "expire_time", jdbcType = org.apache.ibatis.type.JdbcType.TIMESTAMP) + private LocalDateTime expireTime; + + /** + * 状态:ACTIVE-有效,EXPIRED-已过期,INACTIVE-已禁用 + */ + @TableField(value = "status", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String status; + + /** + * 创建时间 + */ + @TableField(value = "create_time", fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/mapper/MtProductLinkMapper.java b/mt-pay/src/main/java/com/mtkj/mtpay/mapper/MtProductLinkMapper.java new file mode 100644 index 0000000..ddcc10d --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/mapper/MtProductLinkMapper.java @@ -0,0 +1,13 @@ +package com.mtkj.mtpay.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.mtkj.mtpay.entity.MtProductLink; +import org.apache.ibatis.annotations.Mapper; + +/** + * 商品链接Mapper接口 + */ +@Mapper +public interface MtProductLinkMapper extends BaseMapper { +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/ProductLinkService.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/ProductLinkService.java new file mode 100644 index 0000000..28eb84d --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/ProductLinkService.java @@ -0,0 +1,45 @@ +package com.mtkj.mtpay.service; + +import com.mtkj.mtpay.entity.MtProductLink; + +/** + * 商品链接服务接口 + */ +public interface ProductLinkService { + + /** + * 为商品创建链接(如果已存在有效链接则返回现有链接,否则创建新链接) + * @param productId 商品ID + * @param expireDays 有效期(天数,默认90天) + * @return 商品链接对象 + */ + MtProductLink createOrGetProductLink(Long productId, Integer expireDays); + + /** + * 根据链接码获取商品链接 + * @param linkCode 链接码 + * @return 商品链接对象 + */ + MtProductLink getProductLinkByCode(String linkCode); + + /** + * 根据链接码获取商品ID(验证链接有效性) + * @param linkCode 链接码 + * @return 商品ID,如果链接无效则返回null + */ + Long getProductIdByLinkCode(String linkCode); + + /** + * 检查链接是否有效(未过期且状态为ACTIVE) + * @param linkCode 链接码 + * @return true-有效,false-无效 + */ + boolean isLinkValid(String linkCode); + + /** + * 更新链接状态为已过期 + * @param linkCode 链接码 + */ + void markLinkAsExpired(String linkCode); +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/ProductLinkServiceImpl.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/ProductLinkServiceImpl.java new file mode 100644 index 0000000..dc84fd5 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/ProductLinkServiceImpl.java @@ -0,0 +1,148 @@ +package com.mtkj.mtpay.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.mtkj.mtpay.common.ResultCode; +import com.mtkj.mtpay.entity.MtProductLink; +import com.mtkj.mtpay.exception.BusinessException; +import com.mtkj.mtpay.mapper.MtProductLinkMapper; +import com.mtkj.mtpay.service.ProductLinkService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * 商品链接服务实现类 + */ +@Slf4j +@Service +public class ProductLinkServiceImpl implements ProductLinkService { + + @Autowired + private MtProductLinkMapper productLinkMapper; + + @Value("${app.frontend.url:http://localhost:3000}") + private String frontendUrl; + + private static final int DEFAULT_EXPIRE_DAYS = 90; + + @Override + @Transactional(rollbackFor = Exception.class) + public MtProductLink createOrGetProductLink(Long productId, Integer expireDays) { + log.debug("创建或获取商品链接,商品ID: {}, 有效期: {}天", productId, expireDays); + + if (expireDays == null || expireDays <= 0) { + expireDays = DEFAULT_EXPIRE_DAYS; + } + + // 查找是否已存在有效的链接 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MtProductLink::getProductId, productId) + .eq(MtProductLink::getStatus, "ACTIVE") + .gt(MtProductLink::getExpireTime, LocalDateTime.now()) + .orderByDesc(MtProductLink::getCreateTime) + .last("LIMIT 1"); + + MtProductLink existingLink = productLinkMapper.selectOne(wrapper); + + if (existingLink != null) { + log.debug("找到现有有效链接,链接码: {}, 商品ID: {}", existingLink.getLinkCode(), productId); + return existingLink; + } + + // 创建新链接 + MtProductLink newLink = new MtProductLink(); + newLink.setProductId(productId); + + // 生成唯一链接码(使用UUID去掉横线,取前32位) + String linkCode = UUID.randomUUID().toString().replace("-", "").substring(0, 32); + newLink.setLinkCode(linkCode); + + // 构建完整URL + String fullUrl = frontendUrl + "/product/" + linkCode; + newLink.setFullUrl(fullUrl); + + // 设置有效期 + newLink.setExpireDays(expireDays); + LocalDateTime now = LocalDateTime.now(); + newLink.setExpireTime(now.plusDays(expireDays)); + newLink.setStatus("ACTIVE"); + + int result = productLinkMapper.insert(newLink); + if (result <= 0) { + log.error("创建商品链接失败,商品ID: {}", productId); + throw new BusinessException(ResultCode.SYSTEM_ERROR, "创建商品链接失败"); + } + + log.info("创建商品链接成功,商品ID: {}, 链接码: {}, 过期时间: {}", + productId, linkCode, newLink.getExpireTime()); + + return newLink; + } + + @Override + public MtProductLink getProductLinkByCode(String linkCode) { + log.debug("根据链接码获取商品链接,链接码: {}", linkCode); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MtProductLink::getLinkCode, linkCode) + .last("LIMIT 1"); + + MtProductLink link = productLinkMapper.selectOne(wrapper); + + if (link == null) { + log.warn("商品链接不存在,链接码: {}", linkCode); + return null; + } + + // 检查是否过期 + if (link.getExpireTime().isBefore(LocalDateTime.now())) { + log.warn("商品链接已过期,链接码: {}, 过期时间: {}", linkCode, link.getExpireTime()); + // 更新状态为已过期 + link.setStatus("EXPIRED"); + productLinkMapper.updateById(link); + return null; + } + + // 检查状态 + if (!"ACTIVE".equals(link.getStatus())) { + log.warn("商品链接状态无效,链接码: {}, 状态: {}", linkCode, link.getStatus()); + return null; + } + + return link; + } + + @Override + public Long getProductIdByLinkCode(String linkCode) { + MtProductLink link = getProductLinkByCode(linkCode); + return link != null ? link.getProductId() : null; + } + + @Override + public boolean isLinkValid(String linkCode) { + MtProductLink link = getProductLinkByCode(linkCode); + return link != null; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void markLinkAsExpired(String linkCode) { + log.debug("标记链接为已过期,链接码: {}", linkCode); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MtProductLink::getLinkCode, linkCode); + + MtProductLink link = productLinkMapper.selectOne(wrapper); + if (link != null) { + link.setStatus("EXPIRED"); + productLinkMapper.updateById(link); + log.info("链接已标记为过期,链接码: {}", linkCode); + } + } +} +