diff --git a/src/main/java/com/gxwebsoft/common/core/config/CertificateConfigProperties.java b/src/main/java/com/gxwebsoft/common/core/config/CertificateConfigProperties.java new file mode 100644 index 0000000..b5e7351 --- /dev/null +++ b/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); + } +} diff --git a/src/main/java/com/gxwebsoft/common/core/utils/CertificateLoader.java b/src/main/java/com/gxwebsoft/common/core/utils/CertificateLoader.java new file mode 100644 index 0000000..dbdf9f2 --- /dev/null +++ b/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]; + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/SettingServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/SettingServiceImpl.java index 702c265..d71f89c 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/SettingServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/SettingServiceImpl.java @@ -129,9 +129,9 @@ public class SettingServiceImpl extends ServiceImpl 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); diff --git a/src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java b/src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java new file mode 100644 index 0000000..b21f776 --- /dev/null +++ b/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 tenantRules; + + /** + * 默认订单配置 + */ + private DefaultConfig defaultConfig = new DefaultConfig(); + + @Data + public static class TestAccount { + /** + * 测试手机号列表 + */ + private List 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); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java index 636a96f..2b6a890 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java +++ b/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 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() diff --git a/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java b/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java new file mode 100644 index 0000000..74ecf9d --- /dev/null +++ b/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; +} diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java b/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java index 3ed9660..99ed7be 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java +++ b/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 = "修改时间") diff --git a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml index 575a3e8..f93df33 100644 --- a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml +++ b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml @@ -59,6 +59,9 @@ AND a.phone LIKE CONCAT('%', #{param.phone}, '%') + + AND a.address_id = #{param.addressId} + AND a.address LIKE CONCAT('%', #{param.address}, '%') diff --git a/src/main/java/com/gxwebsoft/shop/param/ShopOrderParam.java b/src/main/java/com/gxwebsoft/shop/param/ShopOrderParam.java index c2014d4..3ae1c99 100644 --- a/src/main/java/com/gxwebsoft/shop/param/ShopOrderParam.java +++ b/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; diff --git a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java new file mode 100644 index 0000000..38de2b9 --- /dev/null +++ b/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 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); + } +} diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java index 9b3395a..51c0338 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java +++ b/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 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())); + } } // 兼容公钥 diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 95dcca2..bb10020 100644 --- a/src/main/resources/application-dev.yml +++ b/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" diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 726af3e..9ad43a9 100644 --- a/src/main/resources/application-prod.yml +++ b/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 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b556d6b..46a5dc7 100644 --- a/src/main/resources/application.yml +++ b/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"