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