feat(payment): 初始化支付模块基础功能

- 添加订单号生成工具类 OrderIdGenerator
- 定义订单状态枚举 OrderStatus
- 实现OSS文件上传服务接口及阿里云OSS实现
- 添加支付常量类 PaymentConstants
- 创建支付控制器 PaymentController 支持下单、查单和收银台页面
- 新增支付记录实体类 PaymentRecord 用于存储回调和查询记录
This commit is contained in:
2025-12-19 18:13:20 +08:00
parent c338571dc1
commit a544eb6d0e
8 changed files with 597 additions and 0 deletions

View File

@@ -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);
}
}