From a544eb6d0e66d5b3eec5c5b1add9bf56e9ef94b0 Mon Sep 17 00:00:00 2001 From: qiube <18969599531@163.com> Date: Fri, 19 Dec 2025 18:13:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E6=A8=A1=E5=9D=97=E5=9F=BA=E7=A1=80=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加订单号生成工具类 OrderIdGenerator - 定义订单状态枚举 OrderStatus - 实现OSS文件上传服务接口及阿里云OSS实现 - 添加支付常量类 PaymentConstants - 创建支付控制器 PaymentController 支持下单、查单和收银台页面 - 新增支付记录实体类 PaymentRecord 用于存储回调和查询记录 --- .../common/constants/PaymentConstants.java | 65 +++++++ .../mtkj/mtpay/common/enums/OrderStatus.java | 60 ++++++ .../mtpay/controller/PaymentController.java | 176 ++++++++++++++++++ .../com/mtkj/mtpay/entity/PaymentRecord.java | 75 ++++++++ .../com/mtkj/mtpay/service/OssService.java | 42 +++++ .../mtpay/service/impl/OssServiceImpl.java | 136 ++++++++++++++ .../com/mtkj/mtpay/util/OrderIdGenerator.java | 37 ++++ package-lock.json | 6 + 8 files changed, 597 insertions(+) create mode 100644 mt-pay/src/main/java/com/mtkj/mtpay/common/constants/PaymentConstants.java create mode 100644 mt-pay/src/main/java/com/mtkj/mtpay/common/enums/OrderStatus.java create mode 100644 mt-pay/src/main/java/com/mtkj/mtpay/controller/PaymentController.java create mode 100644 mt-pay/src/main/java/com/mtkj/mtpay/entity/PaymentRecord.java create mode 100644 mt-pay/src/main/java/com/mtkj/mtpay/service/OssService.java create mode 100644 mt-pay/src/main/java/com/mtkj/mtpay/service/impl/OssServiceImpl.java create mode 100644 mt-pay/src/main/java/com/mtkj/mtpay/util/OrderIdGenerator.java create mode 100644 package-lock.json diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/common/constants/PaymentConstants.java b/mt-pay/src/main/java/com/mtkj/mtpay/common/constants/PaymentConstants.java new file mode 100644 index 0000000..971d425 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/common/constants/PaymentConstants.java @@ -0,0 +1,65 @@ +package com.mtkj.mtpay.common.constants; + +/** + * 支付相关常量 + */ +public class PaymentConstants { + + /** + * PingPong成功响应码 + */ + public static final String PINGPONG_SUCCESS_CODE = "001000"; + + /** + * 默认订单状态 + */ + public static final String DEFAULT_ORDER_STATUS = "PENDING"; + + /** + * 签名类型 - MD5 + */ + public static final String SIGN_TYPE_MD5 = "MD5"; + + /** + * 签名类型 - SHA256 + */ + public static final String SIGN_TYPE_SHA256 = "SHA256"; + + /** + * 默认签名类型 + */ + public static final String DEFAULT_SIGN_TYPE = SIGN_TYPE_MD5; + + /** + * 3DS验证 - 否 + */ + public static final String THREE_D_SECURE_NO = "N"; + + /** + * 3DS验证 - 是 + */ + public static final String THREE_D_SECURE_YES = "Y"; + + /** + * 虚拟商品 - 否 + */ + public static final String VIRTUAL_PRODUCT_NO = "N"; + + /** + * 虚拟商品 - 是 + */ + public static final String VIRTUAL_PRODUCT_YES = "Y"; + + /** + * 订单号前缀 + */ + public static final String ORDER_ID_PREFIX = "MTN"; + + /** + * 私有构造函数,防止实例化 + */ + private PaymentConstants() { + throw new UnsupportedOperationException("常量类不能被实例化"); + } +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/common/enums/OrderStatus.java b/mt-pay/src/main/java/com/mtkj/mtpay/common/enums/OrderStatus.java new file mode 100644 index 0000000..6559562 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/common/enums/OrderStatus.java @@ -0,0 +1,60 @@ +package com.mtkj.mtpay.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 订单状态枚举 + */ +@Getter +@AllArgsConstructor +public enum OrderStatus { + + /** + * 待支付 + */ + PENDING("PENDING", "待支付"), + + /** + * 支付成功 + */ + SUCCESS("SUCCESS", "支付成功"), + + /** + * 支付失败 + */ + FAILED("FAILED", "支付失败"), + + /** + * 审核中 + */ + REVIEW("REVIEW", "审核中"), + + /** + * 已取消 + */ + CANCELLED("CANCELLED", "已取消"); + + /** + * 状态码 + */ + private final String code; + + /** + * 状态描述 + */ + private final String description; + + /** + * 根据状态码获取枚举 + */ + public static OrderStatus getByCode(String code) { + for (OrderStatus status : values()) { + if (status.getCode().equalsIgnoreCase(code)) { + return status; + } + } + return null; + } +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/controller/PaymentController.java b/mt-pay/src/main/java/com/mtkj/mtpay/controller/PaymentController.java new file mode 100644 index 0000000..192d7da --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/controller/PaymentController.java @@ -0,0 +1,176 @@ +package com.mtkj.mtpay.controller; + +import com.mtkj.mtpay.common.Result; +import com.mtkj.mtpay.common.ResultCode; +import com.mtkj.mtpay.dto.request.CheckoutRequestDTO; +import com.mtkj.mtpay.entity.PaymentOrder; +import com.mtkj.mtpay.service.PaymentOrderService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 支付控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/payment") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentOrderService paymentOrderService; + + /** + * 创建支付订单 + */ + @PostMapping("/checkout") + public ResponseEntity>> checkout(@Valid @RequestBody CheckoutRequestDTO request) { + log.info("收到创建支付订单请求,商户订单号: {}", request.getMerchantTransactionId()); + + PaymentOrder order = paymentOrderService.createPaymentOrder(request); + + Map data = new HashMap<>(); + data.put("merchantTransactionId", order.getMerchantTransactionId()); + data.put("token", order.getToken()); + data.put("paymentUrl", order.getPaymentUrl()); + data.put("status", order.getStatus()); + + return ResponseEntity.ok(Result.success("订单创建成功", data)); + } + + /** + * 查询订单状态 + */ + @GetMapping("/order/{merchantTransactionId}") + public ResponseEntity>> getOrder(@PathVariable String merchantTransactionId) { + log.info("查询订单状态,商户订单号: {}", merchantTransactionId); + + Optional orderOpt = paymentOrderService.findByMerchantTransactionId(merchantTransactionId); + + if (orderOpt.isPresent()) { + PaymentOrder order = orderOpt.get(); + Map data = new HashMap<>(); + data.put("merchantTransactionId", order.getMerchantTransactionId()); + data.put("transactionId", order.getTransactionId() != null ? order.getTransactionId() : ""); + data.put("status", order.getStatus()); + data.put("amount", order.getAmount()); + data.put("currency", order.getCurrency()); + data.put("createTime", order.getCreateTime()); + data.put("token", order.getToken()); + data.put("paymentUrl", order.getPaymentUrl()); + + return ResponseEntity.ok(Result.success(data)); + } else { + return ResponseEntity.ok(Result.fail(ResultCode.ORDER_NOT_FOUND)); + } + } + + /** + * 获取收银台页面HTML + * 返回包含SDK初始化的HTML页面 + */ + @GetMapping("/checkout/page") + public ResponseEntity getCheckoutPage(@RequestParam String token) { + log.info("获取收银台页面,token: {}", token); + + String html = generateCheckoutPage(token); + return ResponseEntity.ok() + .header("Content-Type", "text/html;charset=UTF-8") + .body(html); + } + + /** + * 生成收银台页面HTML + */ + private String generateCheckoutPage(String token) { + return """ + + + + + + + PingPong支付收银台 + + + +
+ + + + + """.formatted(token); + } +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/entity/PaymentRecord.java b/mt-pay/src/main/java/com/mtkj/mtpay/entity/PaymentRecord.java new file mode 100644 index 0000000..4572063 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/entity/PaymentRecord.java @@ -0,0 +1,75 @@ +package com.mtkj.mtpay.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付记录实体(用于记录回调、查询等操作) + */ +@TableName(value = "payment_record") +@Data +public class PaymentRecord { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * PingPong交易流水号 + */ + @TableField(value = "transaction_id", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String transactionId; + + /** + * 商户订单号 + */ + @TableField(value = "merchant_transaction_id", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String merchantTransactionId; + + /** + * 记录类型:CALLBACK-回调,QUERY-查询,REFUND-退款,CAPTURE-预授权完成,VOID-预授权撤销 + */ + @TableField(value = "record_type", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String recordType; + + /** + * 交易状态 + */ + @TableField(value = "status", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String status; + + /** + * 响应码 + */ + @TableField(value = "code", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String code; + + /** + * 响应描述 + */ + @TableField(value = "description", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR) + private String description; + + /** + * 原始请求数据(JSON格式) + */ + @TableField(value = "request_data", jdbcType = org.apache.ibatis.type.JdbcType.LONGVARCHAR) + private String requestData; + + /** + * 原始响应数据(JSON格式) + */ + @TableField(value = "response_data", jdbcType = org.apache.ibatis.type.JdbcType.LONGVARCHAR) + private String responseData; + + /** + * 创建时间 + */ + @TableField(value = "create_time", fill = FieldFill.INSERT) + private LocalDateTime createTime; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/OssService.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/OssService.java new file mode 100644 index 0000000..53ce30e --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/OssService.java @@ -0,0 +1,42 @@ +package com.mtkj.mtpay.service; + +import java.io.InputStream; + +/** + * OSS服务接口 + */ +public interface OssService { + + /** + * 上传文件 + * @param content 文件内容(字节数组) + * @param originalFilename 原始文件名 + * @return 文件访问URL + * @throws Exception 上传异常 + */ + String upload(byte[] content, String originalFilename) throws Exception; + + /** + * 上传文件(指定对象名) + * @param content 文件内容(字节数组) + * @param objectName OSS对象名 + * @return 文件访问URL + * @throws Exception 上传异常 + */ + String uploadToObject(byte[] content, String objectName) throws Exception; + + /** + * 删除文件 + * @param objectName OSS对象名 + * @throws Exception 删除异常 + */ + void delete(String objectName) throws Exception; + + /** + * 构建OSS访问URL + * @param objectName OSS对象名 + * @return 文件访问URL + */ + String buildUrl(String objectName); +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/OssServiceImpl.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/OssServiceImpl.java new file mode 100644 index 0000000..2c3ee19 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/OssServiceImpl.java @@ -0,0 +1,136 @@ +package com.mtkj.mtpay.service.impl; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.common.auth.CredentialsProvider; +import com.aliyun.oss.common.auth.CredentialsProviderFactory; +import com.aliyun.oss.exception.OSSException; +import com.mtkj.mtpay.config.AliyunOSSProperties; +import com.mtkj.mtpay.service.OssService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * OSS服务实现类 + */ +@Slf4j +@Service +public class OssServiceImpl implements OssService { + + @Autowired + private AliyunOSSProperties ossProperties; + + @Override + public String upload(byte[] content, String originalFilename) throws Exception { + String endpoint = ossProperties.getEndpoint(); + String bucketName = ossProperties.getBucketName(); + + // 创建凭证提供者 + CredentialsProvider credentialsProvider = CredentialsProviderFactory + .newDefaultCredentialProvider(ossProperties.getAccessId(), ossProperties.getAccessKey()); + + // 生成文件路径:yyyy/MM/dd/uuid.扩展名(参考mt-oss实现) + String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + String extension = ""; + if (originalFilename != null && originalFilename.contains(".")) { + extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + } + String newFileName = UUID.randomUUID().toString() + extension; + String objectName = dir + "/" + newFileName; + + // 创建OSS客户端 + OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); + + try { + // 上传文件 + ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content)); + log.info("文件上传成功,objectName: {}", objectName); + + // 构建URL(参考mt-oss实现方式) + return buildUrl(objectName); + } finally { + if (ossClient != null) { + ossClient.shutdown(); + } + } + } + + @Override + public String uploadToObject(byte[] content, String objectName) throws Exception { + String endpoint = ossProperties.getEndpoint(); + String bucketName = ossProperties.getBucketName(); + + CredentialsProvider credentialsProvider = CredentialsProviderFactory + .newDefaultCredentialProvider(ossProperties.getAccessId(), ossProperties.getAccessKey()); + + OSS ossClient = null; + try { + ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); + ByteArrayInputStream inputStream = new ByteArrayInputStream(content); + ossClient.putObject(bucketName, objectName, inputStream); + log.info("文件上传成功,objectName: {}", objectName); + return buildUrl(objectName); + } finally { + if (ossClient != null) { + ossClient.shutdown(); + } + } + } + + @Override + public void delete(String objectName) throws Exception { + String endpoint = ossProperties.getEndpoint(); + String bucketName = ossProperties.getBucketName(); + + CredentialsProvider credentialsProvider = CredentialsProviderFactory + .newDefaultCredentialProvider(ossProperties.getAccessId(), ossProperties.getAccessKey()); + + OSS ossClient = null; + try { + ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); + ossClient.deleteObject(bucketName, objectName); + log.info("文件删除成功,objectName: {}", objectName); + } catch (OSSException e) { + log.error("删除OSS文件失败,objectName: {}", objectName, e); + throw e; + } finally { + if (ossClient != null) { + ossClient.shutdown(); + } + } + } + + @Override + public String buildUrl(String objectName) { + String endpoint = ossProperties.getEndpoint(); + String bucketName = ossProperties.getBucketName(); + + // 参考mt-oss实现:使用字符串拼接方式构建URL + // 格式:https://bucketName.endpoint/objectName + String[] endpointParts = endpoint.split("//"); + if (endpointParts.length >= 2) { + String protocol = endpointParts[0]; // http: 或 https: + String host = endpointParts[1]; // oss-cn-hangzhou.aliyuncs.com + return protocol + "//" + bucketName + "." + host + "/" + objectName; + } else { + // 如果endpoint格式不符合预期,使用URI方式构建 + String host = endpoint.replaceFirst("^https?://", ""); + try { + URI uri = new URI("https", bucketName + "." + host, "/" + objectName, null); + return uri.toASCIIString(); + } catch (URISyntaxException e) { + log.error("构建OSS访问地址失败,objectName: {}", objectName, e); + throw new RuntimeException("构建OSS访问地址失败", e); + } + } + } +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/util/OrderIdGenerator.java b/mt-pay/src/main/java/com/mtkj/mtpay/util/OrderIdGenerator.java new file mode 100644 index 0000000..3c83251 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/util/OrderIdGenerator.java @@ -0,0 +1,37 @@ +package com.mtkj.mtpay.util; + +import com.mtkj.mtpay.common.constants.PaymentConstants; +import lombok.extern.slf4j.Slf4j; + +import java.util.UUID; + +/** + * 订单号生成器 + */ +@Slf4j +public class OrderIdGenerator { + + /** + * 生成商户订单号 + * 格式:MTN + 时间戳 + 随机数 + */ + public static String generateMerchantTransactionId() { + long timestamp = System.currentTimeMillis(); + int random = (int) (Math.random() * 10000); + String orderId = String.format("%s%d%04d", PaymentConstants.ORDER_ID_PREFIX, timestamp, random); + log.debug("生成商户订单号: {}", orderId); + return orderId; + } + + /** + * 生成商户订单号(带UUID后缀) + */ + public static String generateMerchantTransactionIdWithUuid() { + long timestamp = System.currentTimeMillis(); + String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String orderId = String.format("%s%d%s", PaymentConstants.ORDER_ID_PREFIX, timestamp, uuid); + log.debug("生成商户订单号(UUID): {}", orderId); + return orderId; + } +} + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..24f9a8e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "MTKJPAY", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}