Browse Source

优化:支付功能(10550)

main
科技小王子 4 weeks ago
parent
commit
078b9697e6
  1. 178
      src/main/java/com/gxwebsoft/common/core/config/CertificateConfigProperties.java
  2. 224
      src/main/java/com/gxwebsoft/common/core/utils/CertificateLoader.java
  3. 4
      src/main/java/com/gxwebsoft/common/system/service/impl/SettingServiceImpl.java
  4. 121
      src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java
  5. 59
      src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java
  6. 132
      src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java
  7. 14
      src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java
  8. 3
      src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml
  9. 3
      src/main/java/com/gxwebsoft/shop/param/ShopOrderParam.java
  10. 157
      src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java
  11. 41
      src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java
  12. 9
      src/main/resources/application-dev.yml
  13. 5
      src/main/resources/application-prod.yml
  14. 46
      src/main/resources/application.yml

178
src/main/java/com/gxwebsoft/common/core/config/CertificateConfigProperties.java

@ -0,0 +1,178 @@
package com.gxwebsoft.common.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 证书配置属性
* 支持Docker容器化部署的证书管理
*
* @author 科技小王子
* @since 2025-01-26
*/
@Data
@Component
@ConfigurationProperties(prefix = "certificate")
public class CertificateConfigProperties {
/**
* 证书加载模式
* classpath: 从classpath加载
* filesystem: 从文件系统加载
* volume: 从Docker挂载卷加载
*/
private LoadMode loadMode = LoadMode.FILESYSTEM;
/**
* 证书根目录Docker挂载卷路径
*/
private String certRootPath = "/app/certs";
/**
* 微信支付证书配置
*/
private WechatPayCert wechatPay = new WechatPayCert();
/**
* 支付宝证书配置
*/
private AlipayCert alipay = new AlipayCert();
/**
* 证书加载模式枚举
*/
public enum LoadMode {
CLASSPATH, // 从classpath加载
FILESYSTEM, // 从文件系统加载
VOLUME // 从Docker挂载卷加载
}
@Data
public static class WechatPayCert {
/**
* 开发环境证书配置
*/
private DevCert dev = new DevCert();
/**
* 生产环境证书基础路径
*/
private String prodBasePath = "/file";
@Data
public static class DevCert {
/**
* API V3密钥
*/
private String apiV3Key = "zGufUcqa7ovgxRL0kF5OlPr482EZwtn9";
/**
* 商户私钥证书文件名
*/
private String privateKeyFile = "apiclient_key.pem";
/**
* 商户证书文件名
*/
private String apiclientCertFile = "apiclient_cert.pem";
/**
* 微信支付平台证书文件名
*/
private String wechatpayCertFile = "wechatpay_cert.pem";
}
}
@Data
public static class AlipayCert {
/**
* 应用私钥证书文件名
*/
private String appPrivateKeyFile = "app_private_key.pem";
/**
* 应用公钥证书文件名
*/
private String appCertPublicKeyFile = "appCertPublicKey.crt";
/**
* 支付宝公钥证书文件名
*/
private String alipayCertPublicKeyFile = "alipayCertPublicKey.crt";
/**
* 支付宝根证书文件名
*/
private String alipayRootCertFile = "alipayRootCert.crt";
}
/**
* 获取证书文件的完整路径
*
* @param fileName 证书文件名
* @return 完整路径
*/
public String getCertPath(String fileName) {
switch (loadMode) {
case CLASSPATH:
return "classpath:certs/" + fileName;
case VOLUME:
return certRootPath + "/" + fileName;
case FILESYSTEM:
default:
return fileName; // 使用原有的完整路径
}
}
/**
* 获取微信支付开发环境证书路径
*
* @param certType 证书类型privateKey, apiclientCert, wechatpayCert
* @return 证书路径
*/
public String getWechatDevCertPath(String certType) {
String fileName;
switch (certType) {
case "privateKey":
fileName = wechatPay.getDev().getPrivateKeyFile();
break;
case "apiclientCert":
fileName = wechatPay.getDev().getApiclientCertFile();
break;
case "wechatpayCert":
fileName = wechatPay.getDev().getWechatpayCertFile();
break;
default:
throw new IllegalArgumentException("Unknown cert type: " + certType);
}
return getCertPath(fileName);
}
/**
* 获取支付宝证书路径
*
* @param certType 证书类型
* @return 证书路径
*/
public String getAlipayCertPath(String certType) {
String fileName;
switch (certType) {
case "appPrivateKey":
fileName = alipay.getAppPrivateKeyFile();
break;
case "appCertPublicKey":
fileName = alipay.getAppCertPublicKeyFile();
break;
case "alipayCertPublicKey":
fileName = alipay.getAlipayCertPublicKeyFile();
break;
case "alipayRootCert":
fileName = alipay.getAlipayRootCertFile();
break;
default:
throw new IllegalArgumentException("Unknown cert type: " + certType);
}
return getCertPath(fileName);
}
}

224
src/main/java/com/gxwebsoft/common/core/utils/CertificateLoader.java

@ -0,0 +1,224 @@
package com.gxwebsoft.common.core.utils;
import com.gxwebsoft.common.core.config.CertificateConfigProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 证书加载工具类
* 支持多种证书加载方式适配Docker容器化部署
*
* @author 科技小王子
* @since 2025-01-26
*/
@Slf4j
@Component
public class CertificateLoader {
private final CertificateConfigProperties certConfig;
public CertificateLoader(CertificateConfigProperties certConfig) {
this.certConfig = certConfig;
}
@PostConstruct
public void init() {
log.info("证书加载器初始化,加载模式:{}", certConfig.getLoadMode());
if (certConfig.getLoadMode() == CertificateConfigProperties.LoadMode.VOLUME) {
log.info("Docker挂载卷证书路径:{}", certConfig.getCertRootPath());
validateCertDirectory();
}
}
/**
* 验证证书目录是否存在
*/
private void validateCertDirectory() {
File certDir = new File(certConfig.getCertRootPath());
if (!certDir.exists()) {
log.warn("证书目录不存在:{},将尝试创建", certConfig.getCertRootPath());
if (!certDir.mkdirs()) {
log.error("无法创建证书目录:{}", certConfig.getCertRootPath());
}
} else {
log.info("证书目录验证成功:{}", certConfig.getCertRootPath());
}
}
/**
* 加载证书文件路径
*
* @param certPath 证书路径可能是相对路径绝对路径或classpath路径
* @return 实际的证书文件路径
*/
public String loadCertificatePath(String certPath) {
if (!StringUtils.hasText(certPath)) {
throw new IllegalArgumentException("证书路径不能为空");
}
try {
switch (certConfig.getLoadMode()) {
case CLASSPATH:
return loadFromClasspath(certPath);
case VOLUME:
return loadFromVolume(certPath);
case FILESYSTEM:
default:
return loadFromFileSystem(certPath);
}
} catch (Exception e) {
log.error("加载证书失败,路径:{}", certPath, e);
throw new RuntimeException("证书加载失败:" + certPath, e);
}
}
/**
* 从classpath加载证书
*/
private String loadFromClasspath(String certPath) throws IOException {
String resourcePath = certPath.startsWith("classpath:") ?
certPath.substring("classpath:".length()) : certPath;
ClassPathResource resource = new ClassPathResource(resourcePath);
if (!resource.exists()) {
throw new IOException("Classpath中找不到证书文件:" + resourcePath);
}
// 将classpath中的文件复制到临时目录
Path tempFile = Files.createTempFile("cert_", ".pem");
try (InputStream inputStream = resource.getInputStream()) {
Files.copy(inputStream, tempFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
String tempPath = tempFile.toAbsolutePath().toString();
log.debug("从classpath加载证书:{} -> {}", resourcePath, tempPath);
return tempPath;
}
/**
* 从Docker挂载卷加载证书
*/
private String loadFromVolume(String certPath) {
// 如果是完整路径,直接使用
if (certPath.startsWith("/") || certPath.contains(":")) {
File file = new File(certPath);
if (file.exists()) {
log.debug("使用完整路径加载证书:{}", certPath);
return certPath;
}
}
// 否则拼接挂载卷路径
String fullPath = Paths.get(certConfig.getCertRootPath(), certPath).toString();
File file = new File(fullPath);
if (!file.exists()) {
throw new RuntimeException("Docker挂载卷中找不到证书文件:" + fullPath);
}
log.debug("从Docker挂载卷加载证书:{}", fullPath);
return fullPath;
}
/**
* 从文件系统加载证书
*/
private String loadFromFileSystem(String certPath) {
File file = new File(certPath);
if (!file.exists()) {
throw new RuntimeException("文件系统中找不到证书文件:" + certPath);
}
log.debug("从文件系统加载证书:{}", certPath);
return certPath;
}
/**
* 检查证书文件是否存在
*
* @param certPath 证书路径
* @return 是否存在
*/
public boolean certificateExists(String certPath) {
try {
switch (certConfig.getLoadMode()) {
case CLASSPATH:
String resourcePath = certPath.startsWith("classpath:") ?
certPath.substring("classpath:".length()) : certPath;
ClassPathResource resource = new ClassPathResource(resourcePath);
return resource.exists();
case VOLUME:
String fullPath = certPath.startsWith("/") ? certPath :
Paths.get(certConfig.getCertRootPath(), certPath).toString();
return new File(fullPath).exists();
case FILESYSTEM:
default:
return new File(certPath).exists();
}
} catch (Exception e) {
log.warn("检查证书文件存在性时出错:{}", certPath, e);
return false;
}
}
/**
* 获取证书文件的输入流
*
* @param certPath 证书路径
* @return 输入流
*/
public InputStream getCertificateInputStream(String certPath) throws IOException {
switch (certConfig.getLoadMode()) {
case CLASSPATH:
String resourcePath = certPath.startsWith("classpath:") ?
certPath.substring("classpath:".length()) : certPath;
ClassPathResource resource = new ClassPathResource(resourcePath);
return resource.getInputStream();
case VOLUME:
case FILESYSTEM:
default:
String actualPath = loadCertificatePath(certPath);
return Files.newInputStream(Paths.get(actualPath));
}
}
/**
* 列出证书目录中的所有文件
*
* @return 证书文件列表
*/
public String[] listCertificateFiles() {
try {
switch (certConfig.getLoadMode()) {
case VOLUME:
File certDir = new File(certConfig.getCertRootPath());
if (certDir.exists() && certDir.isDirectory()) {
return certDir.list();
}
break;
case CLASSPATH:
// classpath模式下不支持列出文件
log.warn("Classpath模式下不支持列出证书文件");
break;
case FILESYSTEM:
default:
// 文件系统模式下证书可能分散在不同目录,不支持统一列出
log.warn("文件系统模式下不支持列出证书文件");
break;
}
} catch (Exception e) {
log.error("列出证书文件时出错", e);
}
return new String[0];
}
}

4
src/main/java/com/gxwebsoft/common/system/service/impl/SettingServiceImpl.java

@ -129,9 +129,9 @@ public class SettingServiceImpl extends ServiceImpl<SettingMapper, Setting> impl
config =
new RSAConfig.Builder()
.merchantId("1246610101")
.privateKeyFromPath("/Users/gxwebsoft/Documents/uploads/file/20230622/fb193d3bfff0467b83dc498435a4f433.pem")
.privateKeyFromPath("/Users/gxwebsoft/cert/1246610101_20221225_cert/01ac632fea184e248d0375e9917063a4.pem")
.merchantSerialNumber("2903B872D5CA36E525FAEC37AEDB22E54ECDE7B7")
.wechatPayCertificatesFromPath("/Users/gxwebsoft/Documents/uploads/file/20230622/23329e924dae41af9b716825626dd44b.pem")
.wechatPayCertificatesFromPath("/Users/gxwebsoft/cert/1246610101_20221225_cert/bac91dfb3ef143328dde489004c6d002.pem")
.build();
configMap.put(data.getTenantId().toString(),config);
System.out.println("config = " + config);

121
src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java

@ -0,0 +1,121 @@
package com.gxwebsoft.shop.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.List;
/**
* 订单相关配置属性
*
* @author 科技小王子
* @since 2025-01-26
*/
@Data
@Component
@ConfigurationProperties(prefix = "shop.order")
public class OrderConfigProperties {
/**
* 测试账号配置
*/
private TestAccount testAccount = new TestAccount();
/**
* 租户特殊规则配置
*/
private List<TenantRule> tenantRules;
/**
* 默认订单配置
*/
private DefaultConfig defaultConfig = new DefaultConfig();
@Data
public static class TestAccount {
/**
* 测试手机号列表
*/
private List<String> phoneNumbers;
/**
* 测试支付金额
*/
private BigDecimal testPayAmount = new BigDecimal("0.01");
/**
* 是否启用测试模式
*/
private boolean enabled = false;
}
@Data
public static class TenantRule {
/**
* 租户ID
*/
private Integer tenantId;
/**
* 租户名称
*/
private String tenantName;
/**
* 最小金额限制
*/
private BigDecimal minAmount;
/**
* 金额限制提示信息
*/
private String minAmountMessage;
/**
* 是否启用
*/
private boolean enabled = true;
}
@Data
public static class DefaultConfig {
/**
* 默认备注
*/
private String defaultComments = "暂无";
/**
* 最小订单金额
*/
private BigDecimal minOrderAmount = BigDecimal.ZERO;
/**
* 订单超时时间分钟
*/
private Integer orderTimeoutMinutes = 30;
}
/**
* 检查是否为测试账号
*/
public boolean isTestAccount(String phone) {
return testAccount.isEnabled() &&
testAccount.getPhoneNumbers() != null &&
testAccount.getPhoneNumbers().contains(phone);
}
/**
* 获取租户规则
*/
public TenantRule getTenantRule(Integer tenantId) {
if (tenantRules == null) {
return null;
}
return tenantRules.stream()
.filter(rule -> rule.isEnabled() && rule.getTenantId().equals(tenantId))
.findFirst()
.orElse(null);
}
}

59
src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java

@ -10,14 +10,18 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.bszx.entity.BszxBm;
import com.gxwebsoft.bszx.entity.BszxPay;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.config.CertificateConfigProperties;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.utils.CertificateLoader;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.service.OrderBusinessService;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.param.ShopOrderParam;
import com.gxwebsoft.shop.dto.OrderCreateRequest;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
@ -38,6 +42,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@ -58,9 +63,15 @@ public class ShopOrderController extends BaseController {
@Resource
private ShopOrderGoodsService shopOrderGoodsService;
@Resource
private OrderBusinessService orderBusinessService;
@Resource
private RedisUtil redisUtil;
@Resource
private ConfigProperties conf;
@Resource
private CertificateConfigProperties certConfig;
@Resource
private CertificateLoader certificateLoader;
@Value("${spring.profiles.active}")
String active;
@ -88,7 +99,24 @@ public class ShopOrderController extends BaseController {
@ApiOperation("添加订单")
@PostMapping()
public ApiResult<?> save(@RequestBody ShopOrder shopOrder) {
public ApiResult<?> save(@Valid @RequestBody OrderCreateRequest request) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("用户未登录");
}
try {
Map<String, String> wxOrderInfo = orderBusinessService.createOrder(request, loginUser);
return success("下单成功", wxOrderInfo);
} catch (Exception e) {
logger.error("创建订单失败", e);
return fail(e.getMessage());
}
}
@ApiOperation("添加订单(兼容旧版本)")
@PostMapping("/legacy")
public ApiResult<?> saveLegacy(@RequestBody ShopOrder shopOrder) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
@ -188,19 +216,24 @@ public class ShopOrderController extends BaseController {
Payment payment = redisUtil.get(key, Payment.class);
String uploadPath = conf.getUploadPath();
// 开发环境
String apiV3Key = "zGufUcqa7ovgxRL0kF5OlPr482EZwtn9";
String privateKey = "/Users/gxwebsoft/JAVA/site-java/cert/websoft/af7261a1bc2a41f7887dbdf05611bb1f.pem";
String apiclientCert = "/Users/gxwebsoft/JAVA/site-java/cert/websoft/wechatpay_4A3231584E93B6AE77820074D07EADEACCB7E223.pem";
String pubKey = "/Users/gxwebsoft/JAVA/site-java/cert/websoft/wechatpay_4A3231584E93B6AE77820074D07EADEACCB7E223.pem";
// 证书配置
String apiV3Key;
String apiclientCert;
// 生产环境
if (ObjectUtil.isNotEmpty(payment)) {
// 检查 payment 字段是否为空,并避免直接解析为数字
apiV3Key = payment.getApiKey();
privateKey = payment.getApiclientKey();
apiclientCert = conf.getUploadPath().concat("/file").concat(payment.getApiclientCert());
pubKey = uploadPath.concat("file").concat(payment.getPubKey());
// 开发环境 - 使用证书加载器
if (active.equals("dev")) {
apiV3Key = certConfig.getWechatPay().getDev().getApiV3Key();
apiclientCert = certificateLoader.loadCertificatePath(
certConfig.getWechatDevCertPath("wechatpayCert"));
} else {
// 生产环境
if (ObjectUtil.isNotEmpty(payment)) {
apiV3Key = payment.getApiKey();
apiclientCert = certificateLoader.loadCertificatePath(
conf.getUploadPath().concat("/file").concat(payment.getApiclientCert()));
} else {
throw new RuntimeException("生产环境未找到支付配置信息");
}
}
RequestParam requestParam = new RequestParam.Builder()

132
src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java

@ -0,0 +1,132 @@
package com.gxwebsoft.shop.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.*;
import java.math.BigDecimal;
/**
* 订单创建请求DTO
*
* @author 科技小王子
* @since 2025-01-26
*/
@Data
@ApiModel(value = "OrderCreateRequest", description = "订单创建请求")
public class OrderCreateRequest {
@ApiModelProperty(value = "订单类型,0商城订单 1预定订单/外卖 2会员卡")
@NotNull(message = "订单类型不能为空")
@Min(value = 0, message = "订单类型值无效")
@Max(value = 2, message = "订单类型值无效")
private Integer type;
@ApiModelProperty(value = "快递/自提")
private Integer deliveryType;
@ApiModelProperty(value = "下单渠道,0小程序预定 1俱乐部训练场 3活动订场")
private Integer channel;
@ApiModelProperty(value = "商户ID")
private Long merchantId;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@ApiModelProperty(value = "商户编号")
private String merchantCode;
@ApiModelProperty(value = "使用的优惠券id")
private Integer couponId;
@ApiModelProperty(value = "使用的会员卡id")
private String cardId;
@ApiModelProperty(value = "收货地址")
private String address;
@ApiModelProperty(value = "地址纬度")
private String addressLat;
@ApiModelProperty(value = "地址经度")
private String addressLng;
@ApiModelProperty(value = "自提店铺id")
private Integer selfTakeMerchantId;
@ApiModelProperty(value = "自提店铺")
private String selfTakeMerchantName;
@ApiModelProperty(value = "配送开始时间")
private String sendStartTime;
@ApiModelProperty(value = "配送结束时间")
private String sendEndTime;
@ApiModelProperty(value = "发货店铺id")
private Integer expressMerchantId;
@ApiModelProperty(value = "发货店铺")
private String expressMerchantName;
@ApiModelProperty(value = "订单总额")
@NotNull(message = "订单总额不能为空")
@DecimalMin(value = "0.01", message = "订单总额必须大于0")
@Digits(integer = 10, fraction = 2, message = "订单总额格式不正确")
private BigDecimal totalPrice;
@ApiModelProperty(value = "减少的金额,使用VIP会员折扣、优惠券抵扣、优惠券折扣后减去的价格")
@DecimalMin(value = "0", message = "减少金额不能为负数")
private BigDecimal reducePrice;
@ApiModelProperty(value = "实际付款")
@DecimalMin(value = "0", message = "实际付款不能为负数")
private BigDecimal payPrice;
@ApiModelProperty(value = "用于统计")
private BigDecimal price;
@ApiModelProperty(value = "价钱,用于积分赠送")
private BigDecimal money;
@ApiModelProperty(value = "教练价格")
private BigDecimal coachPrice;
@ApiModelProperty(value = "购买数量")
@Min(value = 1, message = "购买数量必须大于0")
private Integer totalNum;
@ApiModelProperty(value = "教练id")
private Integer coachId;
@ApiModelProperty(value = "来源ID,存商品ID")
private Integer formId;
@ApiModelProperty(value = "支付类型,0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付")
private Integer payType;
@ApiModelProperty(value = "代付支付方式")
private Integer friendPayType;
@ApiModelProperty(value = "优惠类型:0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡,5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡")
private Integer couponType;
@ApiModelProperty(value = "优惠说明")
private String couponDesc;
@ApiModelProperty(value = "预约详情开始时间数组")
private String startTime;
@ApiModelProperty(value = "备注")
@Size(max = 500, message = "备注长度不能超过500字符")
private String comments;
@ApiModelProperty(value = "排序号")
private Integer sortNumber;
@ApiModelProperty(value = "租户id")
@NotNull(message = "租户ID不能为空")
private Integer tenantId;
}

14
src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java

@ -15,6 +15,9 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.*;
import javax.validation.groups.Default;
/**
* 订单
*
@ -73,6 +76,9 @@ public class ShopOrder implements Serializable {
@ApiModelProperty(value = "IC卡号")
private String icCard;
@ApiModelProperty(value = "收货人id")
private Integer addressId;
@ApiModelProperty(value = "收货地址")
private String address;
@ -99,12 +105,17 @@ public class ShopOrder implements Serializable {
private String expressMerchantName;
@ApiModelProperty(value = "订单总额")
@NotNull(message = "订单总额不能为空")
@DecimalMin(value = "0.01", message = "订单总额必须大于0")
@Digits(integer = 10, fraction = 2, message = "订单总额格式不正确")
private BigDecimal totalPrice;
@ApiModelProperty(value = "减少的金额,使用VIP会员折扣、优惠券抵扣、优惠券折扣后减去的价格")
@DecimalMin(value = "0", message = "减少金额不能为负数")
private BigDecimal reducePrice;
@ApiModelProperty(value = "实际付款")
@DecimalMin(value = "0", message = "实际付款不能为负数")
private BigDecimal payPrice;
@ApiModelProperty(value = "用于统计")
@ -120,6 +131,7 @@ public class ShopOrder implements Serializable {
private BigDecimal coachPrice;
@ApiModelProperty(value = "购买数量")
@Min(value = 1, message = "购买数量必须大于0")
private Integer totalNum;
@ApiModelProperty(value = "教练id")
@ -218,6 +230,7 @@ public class ShopOrder implements Serializable {
private String mobile;
@ApiModelProperty(value = "备注")
@Size(max = 500, message = "备注长度不能超过500字符")
private String comments;
@ApiModelProperty(value = "排序号")
@ -228,6 +241,7 @@ public class ShopOrder implements Serializable {
private Integer deleted;
@ApiModelProperty(value = "租户id")
@NotNull(message = "租户ID不能为空")
private Integer tenantId;
@ApiModelProperty(value = "修改时间")

3
src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml

@ -59,6 +59,9 @@
<if test="param.phone != null">
AND a.phone LIKE CONCAT('%', #{param.phone}, '%')
</if>
<if test="param.addressId != null">
AND a.address_id = #{param.addressId}
</if>
<if test="param.address != null">
AND a.address LIKE CONCAT('%', #{param.address}, '%')
</if>

3
src/main/java/com/gxwebsoft/shop/param/ShopOrderParam.java

@ -82,6 +82,9 @@ public class ShopOrderParam extends BaseParam {
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "收货人id")
private Integer addressId;
@ApiModelProperty(value = "收货地址")
private String address;

157
src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java

@ -0,0 +1,157 @@
package com.gxwebsoft.shop.service;
import cn.hutool.core.util.IdUtil;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.config.OrderConfigProperties;
import com.gxwebsoft.shop.dto.OrderCreateRequest;
import com.gxwebsoft.shop.entity.ShopOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Map;
/**
* 订单业务服务类
* 处理订单创建的核心业务逻辑
*
* @author 科技小王子
* @since 2025-01-26
*/
@Slf4j
@Service
public class OrderBusinessService {
@Resource
private ShopOrderService shopOrderService;
@Resource
private OrderConfigProperties orderConfig;
/**
* 创建订单
*
* @param request 订单创建请求
* @param loginUser 当前登录用户
* @return 微信支付订单信息
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, String> createOrder(OrderCreateRequest request, User loginUser) {
// 1. 参数校验
validateOrderRequest(request, loginUser);
// 2. 构建订单对象
ShopOrder shopOrder = buildShopOrder(request, loginUser);
// 3. 应用业务规则
applyBusinessRules(shopOrder, loginUser);
// 4. 保存订单
boolean saved = shopOrderService.save(shopOrder);
if (!saved) {
throw new BusinessException("订单保存失败");
}
// 5. 创建微信支付订单
try {
return shopOrderService.createWxOrder(shopOrder);
} catch (Exception e) {
log.error("创建微信支付订单失败,订单号:{}", shopOrder.getOrderNo(), e);
throw new BusinessException("创建支付订单失败:" + e.getMessage());
}
}
/**
* 校验订单请求参数
*/
private void validateOrderRequest(OrderCreateRequest request, User loginUser) {
if (loginUser == null) {
throw new BusinessException("用户未登录");
}
if (request.getTotalPrice() == null || request.getTotalPrice().compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("商品金额不能为0");
}
// 检查租户特殊规则
OrderConfigProperties.TenantRule tenantRule = orderConfig.getTenantRule(request.getTenantId());
if (tenantRule != null && tenantRule.getMinAmount() != null) {
if (request.getTotalPrice().compareTo(tenantRule.getMinAmount()) < 0) {
throw new BusinessException(tenantRule.getMinAmountMessage());
}
}
}
/**
* 构建订单对象
*/
private ShopOrder buildShopOrder(OrderCreateRequest request, User loginUser) {
ShopOrder shopOrder = new ShopOrder();
// 复制请求参数到订单对象
BeanUtils.copyProperties(request, shopOrder);
// 设置用户相关信息
shopOrder.setUserId(loginUser.getUserId());
shopOrder.setOpenid(loginUser.getOpenid());
shopOrder.setPayUserId(loginUser.getUserId());
// 生成订单号
if (shopOrder.getOrderNo() == null) {
shopOrder.setOrderNo(Long.toString(IdUtil.getSnowflakeNextId()));
}
// 设置默认备注
if (shopOrder.getComments() == null) {
shopOrder.setComments(orderConfig.getDefaultConfig().getDefaultComments());
}
// 设置默认支付状态
shopOrder.setPayStatus(false);
return shopOrder;
}
/**
* 应用业务规则
*/
private void applyBusinessRules(ShopOrder shopOrder, User loginUser) {
// 测试账号处理
if (orderConfig.isTestAccount(loginUser.getPhone())) {
BigDecimal testAmount = orderConfig.getTestAccount().getTestPayAmount();
shopOrder.setPrice(testAmount);
shopOrder.setTotalPrice(testAmount);
log.info("应用测试账号规则,用户:{},测试金额:{}", loginUser.getPhone(), testAmount);
}
// 其他业务规则可以在这里添加
// 例如:VIP折扣、优惠券处理等
}
/**
* 校验订单金额
*/
public void validateOrderAmount(BigDecimal amount, Integer tenantId) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("订单金额必须大于0");
}
OrderConfigProperties.TenantRule tenantRule = orderConfig.getTenantRule(tenantId);
if (tenantRule != null && tenantRule.getMinAmount() != null) {
if (amount.compareTo(tenantRule.getMinAmount()) < 0) {
throw new BusinessException(tenantRule.getMinAmountMessage());
}
}
}
/**
* 检查是否为测试账号
*/
public boolean isTestAccount(String phone) {
return orderConfig.isTestAccount(phone);
}
}

41
src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java

@ -7,8 +7,10 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.config.CertificateConfigProperties;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.utils.CertificateLoader;
import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.common.system.param.PaymentParam;
import com.gxwebsoft.common.system.service.PaymentService;
@ -60,10 +62,10 @@
private PaymentService paymentService;
@Resource
private SettingService settingService;
public static String privateKeyPath = "/Users/gxwebsoft/Downloads/ef7f7e0430cb47019d06b93f885bf95f/apiclient_key.pem";
public static String privateCertPath = "/Users/gxwebsoft/JAVA/com.gxwebsoft.core/src/main/resources/cert/apiclient_cert.pem";
public static String wechatpayCertPath = "/Users/gxwebsoft/Downloads/ef7f7e0430cb47019d06b93f885bf95f/wechatpay_55729BDEC2502C301BA02CDC28E4CEE4DE4D1DB9.pem"; // 平台证书
@Resource
private CertificateConfigProperties certConfig;
@Resource
private CertificateLoader certificateLoader;
@Override
public PageResult<ShopOrder> pageRel(ShopOrderParam param) {
@ -216,21 +218,28 @@
* @return
*/
public JsapiServiceExtension getWxService(ShopOrder order) {
final String uploadPath = config.getUploadPath();
final Payment payment = getPayment(order);
String privateKey = uploadPath.concat("/file").concat(payment.getApiclientKey()); // 秘钥证书
String apiclientCert = uploadPath.concat("/file").concat(payment.getApiclientCert());
String pubKey = uploadPath.concat("/file").concat(payment.getPubKey()); // 公钥证书
String privateKey;
String apiclientCert;
String pubKey = null;
// 开发环境配置
// 开发环境配置 - 使用证书加载器
if (active.equals("dev")) {
privateKey = privateKeyPath;
apiclientCert = wechatpayCertPath;
}
// 生成环境配置
if (active.equals("prod")) {
privateKey = certificateLoader.loadCertificatePath(
certConfig.getWechatDevCertPath("privateKey"));
apiclientCert = certificateLoader.loadCertificatePath(
certConfig.getWechatDevCertPath("wechatpayCert"));
} else {
// 生产环境配置 - 从上传目录加载
final String uploadPath = config.getUploadPath();
privateKey = certificateLoader.loadCertificatePath(
uploadPath.concat("/file").concat(payment.getApiclientKey()));
apiclientCert = certificateLoader.loadCertificatePath(
uploadPath.concat("/file").concat(payment.getApiclientCert()));
if (payment.getPubKey() != null && !payment.getPubKey().isEmpty()) {
pubKey = certificateLoader.loadCertificatePath(
uploadPath.concat("/file").concat(payment.getPubKey()));
}
}
// 兼容公钥

9
src/main/resources/application-dev.yml

@ -44,3 +44,12 @@ config:
# 开发环境接口
server-url: http://127.0.0.1:9091/api
upload-path: /Users/gxwebsoft/Documents/uploads/ # window(D:\Temp)
# 开发环境证书配置
certificate:
load-mode: CLASSPATH # 开发环境从classpath加载
wechat-pay:
dev:
private-key-file: "certs/dev/apiclient_key.pem"
apiclient-cert-file: "certs/dev/apiclient_cert.pem"
wechatpay-cert-file: "certs/dev/wechatpay_cert.pem"

5
src/main/resources/application-prod.yml

@ -53,3 +53,8 @@ config:
bucketName: oss-gxwebsoft
bucketDomain: https://oss.wsdns.cn
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
# 生产环境证书配置
certificate:
load-mode: VOLUME # 生产环境从Docker挂载卷加载
cert-root-path: /app/certs

46
src/main/resources/application.yml

@ -100,4 +100,50 @@ config:
bucketDomain: https://oss.wsdns.cn
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
# 商城订单配置
shop:
order:
# 测试账号配置
test-account:
enabled: true
phone-numbers:
- "13737128880"
test-pay-amount: 0.01
# 租户特殊规则配置
tenant-rules:
- tenant-id: 10324
tenant-name: "百色中学"
min-amount: 10
min-amount-message: "捐款金额最低不能少于10元,感谢您的爱心捐赠^_^"
enabled: true
# 默认配置
default-config:
default-comments: "暂无"
min-order-amount: 0
order-timeout-minutes: 30
# 证书配置
certificate:
# 证书加载模式: CLASSPATH, FILESYSTEM, VOLUME
load-mode: FILESYSTEM
# Docker挂载卷证书路径
cert-root-path: /app/certs
# 微信支付证书配置
wechat-pay:
dev:
api-v3-key: "zGufUcqa7ovgxRL0kF5OlPr482EZwtn9"
private-key-file: "apiclient_key.pem"
apiclient-cert-file: "apiclient_cert.pem"
wechatpay-cert-file: "wechatpay_cert.pem"
prod-base-path: "/file"
# 支付宝证书配置
alipay:
app-private-key-file: "app_private_key.pem"
app-cert-public-key-file: "appCertPublicKey.crt"
alipay-cert-public-key-file: "alipayCertPublicKey.crt"
alipay-root-cert-file: "alipayRootCert.crt"

Loading…
Cancel
Save