docs(readme): 添加项目启动和编译问题解决方案文档
- 新增 FIX_COMPILE.md 文件,提供修复IDE编译问题的四种方法 - 新增 HOW_TO_START.md 文件,详细说明如何正确启动后端服务 - 强调必须启动 mt-pay 模块的 MtPayApplication 类 - 提供 IntelliJ IDEA 和 Maven 两种启动方式 - 列出常见启动错误及解决方案 - 添加快速检查清单帮助验证启动状态
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package com.mtkj.mtpay.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PaymentType {
|
||||
|
||||
/**
|
||||
* 直接付款
|
||||
*/
|
||||
SALE("SALE", "直接付款"),
|
||||
|
||||
/**
|
||||
* 预授权
|
||||
*/
|
||||
AUTH("AUTH", "预授权");
|
||||
|
||||
/**
|
||||
* 类型码
|
||||
*/
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* 类型描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据类型码获取枚举
|
||||
*/
|
||||
public static PaymentType getByCode(String code) {
|
||||
for (PaymentType type : values()) {
|
||||
if (type.getCode().equalsIgnoreCase(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.mtkj.mtpay.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* PingPong支付配置属性
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "pingpong")
|
||||
public class PingPongProperties {
|
||||
|
||||
/**
|
||||
* PingPong商户号
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* PingPong商户店铺编号
|
||||
*/
|
||||
private String accId;
|
||||
|
||||
/**
|
||||
* 签名密钥(secret/salt)
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* 签名类型:MD5或SHA256
|
||||
*/
|
||||
private String signType = "MD5";
|
||||
|
||||
/**
|
||||
* API网关地址
|
||||
*/
|
||||
private String gateway = "https://sandbox-acquirer-payment.pingpongx.com";
|
||||
|
||||
/**
|
||||
* 收银台CDN地址
|
||||
*/
|
||||
private String sdkCdnUrl = "https://pay-cdn.pingpongx.com/production/static/sdk/1.2.0/ppPay.min.js";
|
||||
|
||||
/**
|
||||
* 环境模式:sandbox-沙箱,test-测试,build-生产
|
||||
*/
|
||||
private String mode = "sandbox";
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enabled = true;
|
||||
}
|
||||
|
||||
64
mt-pay/src/main/java/com/mtkj/mtpay/entity/MtProduct.java
Normal file
64
mt-pay/src/main/java/com/mtkj/mtpay/entity/MtProduct.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package com.mtkj.mtpay.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 商品实体类
|
||||
*/
|
||||
@TableName(value = "mt_product")
|
||||
@Data
|
||||
public class MtProduct {
|
||||
|
||||
/**
|
||||
* 商品ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
@TableField(value = "name", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 商品价格(基础价格,SKU可能有不同价格)
|
||||
*/
|
||||
@TableField(value = "price", jdbcType = org.apache.ibatis.type.JdbcType.DECIMAL)
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 主图URL(最大4000字符)
|
||||
*/
|
||||
@TableField(value = "main_image", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
|
||||
private String mainImage;
|
||||
|
||||
/**
|
||||
* 商品状态:ACTIVE-上架,INACTIVE-下架,DELETED-已删除
|
||||
*/
|
||||
@TableField(value = "status", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 店铺ID
|
||||
*/
|
||||
@TableField(value = "shop_id", jdbcType = org.apache.ibatis.type.JdbcType.BIGINT)
|
||||
private Long shopId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.mtkj.mtpay.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mtkj.mtpay.entity.PaymentRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付记录Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PaymentRecordMapper extends BaseMapper<PaymentRecord> {
|
||||
|
||||
/**
|
||||
* 根据商户订单号查询记录(按创建时间倒序)
|
||||
*/
|
||||
default List<PaymentRecord> findByMerchantTransactionIdOrderByCreateTimeDesc(String merchantTransactionId) {
|
||||
LambdaQueryWrapper<PaymentRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PaymentRecord::getMerchantTransactionId, merchantTransactionId)
|
||||
.orderByDesc(PaymentRecord::getCreateTime);
|
||||
return selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据PingPong交易流水号查询记录(按创建时间倒序)
|
||||
*/
|
||||
default List<PaymentRecord> findByTransactionIdOrderByCreateTimeDesc(String transactionId) {
|
||||
LambdaQueryWrapper<PaymentRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PaymentRecord::getTransactionId, transactionId)
|
||||
.orderByDesc(PaymentRecord::getCreateTime);
|
||||
return selectList(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.mtkj.mtpay.service;
|
||||
|
||||
import com.mtkj.mtpay.dto.request.CheckoutRequestDTO;
|
||||
import com.mtkj.mtpay.dto.response.CheckoutResponseDTO;
|
||||
|
||||
/**
|
||||
* PingPong支付服务接口
|
||||
* 负责调用PingPong支付API
|
||||
*/
|
||||
public interface PingPongPayService {
|
||||
|
||||
/**
|
||||
* 创建支付订单(调用checkout接口)
|
||||
*
|
||||
* @param request 支付请求
|
||||
* @return 支付响应
|
||||
*/
|
||||
CheckoutResponseDTO checkout(CheckoutRequestDTO request);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.mtkj.mtpay.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mtkj.mtpay.config.PingPongProperties;
|
||||
import com.mtkj.mtpay.dto.request.CheckoutRequestDTO;
|
||||
import com.mtkj.mtpay.dto.response.CheckoutResponseDTO;
|
||||
import com.mtkj.mtpay.service.PingPongPayService;
|
||||
import com.mtkj.mtpay.service.SignatureService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* PingPong支付服务实现类
|
||||
* 负责调用PingPong支付API
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PingPongPayServiceImpl implements PingPongPayService {
|
||||
|
||||
private final PingPongProperties pingPongProperties;
|
||||
private final SignatureService signatureService;
|
||||
private final RestClient restClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public CheckoutResponseDTO checkout(CheckoutRequestDTO request) {
|
||||
log.info("创建支付订单,商户订单号: {}", request.getMerchantTransactionId());
|
||||
|
||||
// 构建请求参数Map
|
||||
Map<String, Object> requestMap = buildRequestMap(request);
|
||||
|
||||
// 生成签名
|
||||
String sign = signatureService.generateSign(requestMap);
|
||||
requestMap.put("sign", sign);
|
||||
|
||||
// 调用PingPong API
|
||||
String url = pingPongProperties.getGateway() + "/v2/checkout";
|
||||
log.info("调用PingPong API: {}", url);
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
String requestBody = objectMapper.writeValueAsString(requestMap);
|
||||
log.debug("请求体: {}", requestBody);
|
||||
|
||||
ResponseEntity<CheckoutResponseDTO> response = restClient.post()
|
||||
.uri(url)
|
||||
.headers(httpHeaders -> httpHeaders.addAll(headers))
|
||||
.body(requestBody)
|
||||
.retrieve()
|
||||
.toEntity(CheckoutResponseDTO.class);
|
||||
|
||||
CheckoutResponseDTO responseDTO = response.getBody();
|
||||
if (responseDTO == null) {
|
||||
throw new RuntimeException("PingPong API返回响应为空");
|
||||
}
|
||||
|
||||
log.info("PingPong API响应,code: {}, description: {}", responseDTO.getCode(), responseDTO.getDescription());
|
||||
|
||||
// 验证响应签名
|
||||
Map<String, Object> responseMap = objectMapper.convertValue(responseDTO, Map.class);
|
||||
if (!signatureService.verifySign(responseMap)) {
|
||||
log.warn("PingPong响应签名验证失败");
|
||||
// 根据业务需求决定是否抛出异常
|
||||
}
|
||||
|
||||
return responseDTO;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("调用PingPong API失败", e);
|
||||
throw new RuntimeException("调用PingPong API失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求参数Map
|
||||
*/
|
||||
private Map<String, Object> buildRequestMap(CheckoutRequestDTO request) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
// 设置基础参数
|
||||
map.put("accId", request.getAccId() != null ? request.getAccId() : pingPongProperties.getAccId());
|
||||
map.put("amount", request.getAmount());
|
||||
map.put("currency", request.getCurrency());
|
||||
map.put("merchantTransactionId", request.getMerchantTransactionId());
|
||||
map.put("paymentType", request.getPaymentType());
|
||||
map.put("shopperResultUrl", request.getShopperResultUrl());
|
||||
map.put("shopperCancelUrl", request.getShopperCancelUrl());
|
||||
map.put("signType", request.getSignType() != null ? request.getSignType() : pingPongProperties.getSignType());
|
||||
|
||||
// 可选参数
|
||||
if (request.getPaymentBrand() != null && !request.getPaymentBrand().isEmpty()) {
|
||||
map.put("paymentBrand", request.getPaymentBrand());
|
||||
}
|
||||
if (request.getMerchantUserId() != null && !request.getMerchantUserId().isEmpty()) {
|
||||
map.put("merchantUserId", request.getMerchantUserId());
|
||||
}
|
||||
if (request.getLanguage() != null && !request.getLanguage().isEmpty()) {
|
||||
map.put("language", request.getLanguage());
|
||||
}
|
||||
if (request.getThreeDSecure() != null && !request.getThreeDSecure().isEmpty()) {
|
||||
map.put("threeDSecure", request.getThreeDSecure());
|
||||
}
|
||||
if (request.getPrimaryMerchantTransactionId() != null && !request.getPrimaryMerchantTransactionId().isEmpty()) {
|
||||
map.put("primaryMerchantTransactionId", request.getPrimaryMerchantTransactionId());
|
||||
}
|
||||
if (request.getPeriodsNum() != null) {
|
||||
map.put("periodsNum", request.getPeriodsNum());
|
||||
}
|
||||
if (request.getNotificationUrl() != null && !request.getNotificationUrl().isEmpty()) {
|
||||
map.put("notificationUrl", request.getNotificationUrl());
|
||||
}
|
||||
if (request.getRemark() != null && !request.getRemark().isEmpty()) {
|
||||
map.put("remark", request.getRemark());
|
||||
}
|
||||
|
||||
// 风控信息
|
||||
if (request.getRiskInfo() != null) {
|
||||
map.put("riskInfo", objectMapper.convertValue(request.getRiskInfo(), Map.class));
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user