14 changed files with 965 additions and 31 deletions
@ -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); |
|||
} |
|||
} |
@ -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]; |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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; |
|||
} |
@ -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); |
|||
} |
|||
} |
Loading…
Reference in new issue