docs(readme): 添加项目启动和编译问题解决方案文档

- 新增 FIX_COMPILE.md 文件,提供修复IDE编译问题的四种方法
- 新增 HOW_TO_START.md 文件,详细说明如何正确启动后端服务
- 强调必须启动 mt-pay 模块的 MtPayApplication 类
- 提供 IntelliJ IDEA 和 Maven 两种启动方式
- 列出常见启动错误及解决方案
- 添加快速检查清单帮助验证启动状态
This commit is contained in:
2025-12-19 18:34:00 +08:00
parent efa56da5b2
commit 3133369053
13 changed files with 1185 additions and 0 deletions

View File

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

View File

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

View 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;
}

View File

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

View File

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

View File

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