feat(payment): 初始化支付模块基础功能
- 添加订单号生成工具类 OrderIdGenerator - 定义订单状态枚举 OrderStatus - 实现OSS文件上传服务接口及阿里云OSS实现 - 添加支付常量类 PaymentConstants - 创建支付控制器 PaymentController 支持下单、查单和收银台页面 - 新增支付记录实体类 PaymentRecord 用于存储回调和查询记录
This commit is contained in:
@@ -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("常量类不能被实例化");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Result<Map<String, Object>>> checkout(@Valid @RequestBody CheckoutRequestDTO request) {
|
||||
log.info("收到创建支付订单请求,商户订单号: {}", request.getMerchantTransactionId());
|
||||
|
||||
PaymentOrder order = paymentOrderService.createPaymentOrder(request);
|
||||
|
||||
Map<String, Object> 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<Result<Map<String, Object>>> getOrder(@PathVariable String merchantTransactionId) {
|
||||
log.info("查询订单状态,商户订单号: {}", merchantTransactionId);
|
||||
|
||||
Optional<PaymentOrder> orderOpt = paymentOrderService.findByMerchantTransactionId(merchantTransactionId);
|
||||
|
||||
if (orderOpt.isPresent()) {
|
||||
PaymentOrder order = orderOpt.get();
|
||||
Map<String, Object> 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<String> 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 """
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>PingPong支付收银台</title>
|
||||
<style>
|
||||
#ufo-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="ufo-container"></div>
|
||||
<script src="https://pay-cdn.pingpongx.com/production/static/sdk/1.2.0/ppPay.min.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
let client = new ppPay({
|
||||
lang: 'zh',
|
||||
root: '#ufo-container',
|
||||
manul: false,
|
||||
located: true,
|
||||
showPrice: true,
|
||||
bill: true,
|
||||
mode: 'sandbox',
|
||||
menu: false,
|
||||
base: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
fontSize: '14px',
|
||||
backgroundColor: '#fff',
|
||||
showHeader: true,
|
||||
showHeaderLabel: true,
|
||||
headerLabelFont: "支付",
|
||||
headerColor: '#333333',
|
||||
headerSize: '16px',
|
||||
headerBackgroundColor: '#fff',
|
||||
headerPadding: '20px',
|
||||
btnSize: '100%',
|
||||
btnColor: '#fff',
|
||||
btnFontSize: '14px',
|
||||
btnPaddingX: '20px',
|
||||
btnPaddingY: '10px',
|
||||
btnBackgroundColor: '#1fa0e8',
|
||||
btnBorderRadius: '4px',
|
||||
btnMarginTop: '20px'
|
||||
}
|
||||
});
|
||||
|
||||
let sdkConfig = {
|
||||
token: '%s'
|
||||
};
|
||||
|
||||
client.createPayment(sdkConfig);
|
||||
|
||||
function setPPPayPropWin() {
|
||||
let winWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
let winHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
|
||||
let ufoContainer = document.getElementById('ufo-container');
|
||||
|
||||
if (winWidth >= 500) {
|
||||
let clientW = Math.floor(winWidth / 3);
|
||||
clientW = clientW >= 500 ? clientW : 500;
|
||||
ufoContainer.style.width = clientW + 'px';
|
||||
} else {
|
||||
ufoContainer.style.width = winWidth + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
setPPPayPropWin();
|
||||
window.addEventListener('resize', setPPPayPropWin);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""".formatted(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
42
mt-pay/src/main/java/com/mtkj/mtpay/service/OssService.java
Normal file
42
mt-pay/src/main/java/com/mtkj/mtpay/service/OssService.java
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "MTKJPAY",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user