diff --git a/sql/optional/mall/order.sql b/sql/optional/mall/order.sql new file mode 100644 index 000000000..21e60200d --- /dev/null +++ b/sql/optional/mall/order.sql @@ -0,0 +1,76 @@ +/**todo cancelType 设置默认值 0?*/ +CREATE TABLE `trade_order` +( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `sn` varchar(32) NOT NULL COMMENT '订单流水号', + `type` int NOT NULL DEFAULT '0' COMMENT '订单类型:[0:普通订单 1:秒杀订单 2:拼团订单 3:砍价订单]', + `terminal` int NOT NULL COMMENT '订单来源终端:[1:小程序 2:H5 3:iOS 4:安卓]', + `user_id` bigint unsigned NOT NULL COMMENT '用户编号', + `user_ip` varchar(30) NOT NULL DEFAULT '' COMMENT '用户 IP', + `user_remark` varchar(200) DEFAULT NULL COMMENT '用户备注', + `status` int NOT NULL DEFAULT '0' COMMENT '订单状态:[0:待付款 1:待发货 2:待收货 3:已完成 4:已关闭]', + `product_count` int NOT NULL COMMENT '购买的商品数量', + `cancel_type` int NOT NULL COMMENT '取消类型:[10:超时未支付 20:退款关闭 30:买家取消 40:已通过货到付款交易]', + `remark` varchar(200) DEFAULT NULL COMMENT '商家备注', + `payed` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已支付:[0:未支付 1:已经支付过]', + `finish_time` datetime DEFAULT NULL COMMENT '订单完成时间', + `cancel_time` datetime DEFAULT NULL COMMENT '订单取消时间', + `sku_original_price` int NOT NULL DEFAULT '0' COMMENT '商品原价(总),单位:分', + `sku_promotion_price` int NOT NULL DEFAULT '0' COMMENT '商品优惠(总),单位:分', + `order_promotion_price` int NOT NULL DEFAULT '0' COMMENT '订单优惠(总),单位:分', + `delivery_price` int NOT NULL DEFAULT '0' COMMENT '运费金额,单位:分', + `pay_price` int NOT NULL DEFAULT '0' COMMENT '应付金额(总),单位:分', + `pay_order_id` int NOT NULL COMMENT '支付订单编号', + `pay_channel` int NOT NULL COMMENT '支付成功的支付渠道', + `delivery_type` int NOT NULL DEFAULT '1' COMMENT '配送方式:[1:快递发货 2:自提]', + `actual_delivery_type` int NOT NULL DEFAULT '1' COMMENT '实际的配送方式:[1:快递发货 2:自提]', + `delivery_templateid` int DEFAULT NULL COMMENT '配置模板的编号', + `express_no` int DEFAULT NULL COMMENT '物流公司单号', + `delivery_status` bit(1) NOT NULL DEFAULT b'0' COMMENT '发货状态[0:未发货 1:已发货]', + `delivery_time` datetime DEFAULT NULL COMMENT '发货时间', + `receive_time` datetime DEFAULT NULL COMMENT '收货时间', + `receiver_name` varchar(20) NOT NULL COMMENT '收件人名称', + `receiver_mobile` varchar(20) NOT NULL COMMENT '收件人手机', + `receiver_area_id` int NOT NULL COMMENT '收件人地区编号', + `receiver_post_code` int DEFAULT NULL COMMENT '收件人邮编', + `receiver_detail_address` varchar(255) NOT NULL COMMENT '收件人详细地址', + `refund_status` int NOT NULL DEFAULT '0' COMMENT '订单状态:[0:未退款 1:部分退款 2:全部退款]', + `refund_price` int NOT NULL DEFAULT '0' COMMENT '退款金额,单位:分', + `coupon_id` bigint unsigned NOT NULL COMMENT '优惠劵编号', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB COMMENT ='交易订单表'; + + +DROP TABLE IF EXISTS `trade_order_item`; +CREATE TABLE `trade_order_item` +( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `user_id` bigint unsigned NOT NULL COMMENT '用户编号', + `order_Id` bigint unsigned NOT NULL COMMENT '订单编号', + `spu_id` bigint unsigned NOT NULL COMMENT '商品 SPU 编号', + `sku_id` bigint unsigned NOT NULL COMMENT '商品 SKU 编号', + `properties` json DEFAULT NULL COMMENT '规格值数组,JSON 格式', + `name` varchar(128) NOT NULL DEFAULT '' COMMENT '商品名称', + `pic_url` varchar(200) DEFAULT NULL COMMENT '商品图片', + `count` int NOT NULL COMMENT '购买数量', + `commented` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否评论:[0:未评论 1:已评论]', + `original_price` int NOT NULL DEFAULT '0' COMMENT '商品原价(单),单位:分', + `total_original_price` int NOT NULL DEFAULT '0' COMMENT '商品原价(总),单位:分', + `total_promotion_price` int NOT NULL DEFAULT '0' COMMENT '商品级优惠(总),单位:分', + `present_price` int NOT NULL DEFAULT '0' COMMENT '最终购买金额(单),单位:分。', + `total_present_price` int NOT NULL DEFAULT '0' COMMENT '最终购买金额(总),单位:分。', + `total_pay_price` int NOT NULL DEFAULT '0' COMMENT '应付金额(总),单位:分', + `refund_status` int NOT NULL DEFAULT '0' COMMENT '退款状态:[0:未申请退款 1:申请退款 2:等待退款 3:退款成功]', + `refund_total` int NOT NULL DEFAULT '0' COMMENT '退款总金额,单位:分', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB COMMENT ='交易订单明细表'; \ No newline at end of file diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java index 7a9cd1ec6..cb9e530b7 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java @@ -15,6 +15,7 @@ import java.util.Arrays; @Getter public enum TerminalEnum implements IntArrayValuable { + //TODO terminal 重复,请参考 '订单来源终端:[1:小程序 2:H5 3:iOS 4:安卓]' MINI_PROGRAM(1, "小程序"), H5(2, "H5"), IOS(3, "iOS"), diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java new file mode 100644 index 000000000..b3915407f --- /dev/null +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.product.api.sku; + +import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO; +import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO; + +import java.util.Collection; +import java.util.List; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface ProductSkuApi { + + + /** + * 根据skuId列表 查询sku信息 + * + * @param skuIds sku ID列表 + * @return sku信息列表 + */ + List getSkusByIds(Collection skuIds); + + /** + * 批量扣减sku库存 + * + * @param batchReqDTO sku库存信息列表 + */ + void decrementStockBatch(SkuDecrementStockBatchReqDTO batchReqDTO); +} diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuDecrementStockBatchReqDTO.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuDecrementStockBatchReqDTO.java new file mode 100644 index 000000000..c0cee91ba --- /dev/null +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuDecrementStockBatchReqDTO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.product.api.sku.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SkuDecrementStockBatchReqDTO { + + + private List items; + + @Data + public static class Item { + + /** + * 商品 SPU 编号,自增 + */ + private Long productId; + + /** + * 商品 SKU 编号,自增 + */ + private Long skuId; + + /** + * 数量 + */ + private Integer count; + + } + + public static SkuDecrementStockBatchReqDTO of(List items) { + return new SkuDecrementStockBatchReqDTO(items); + } + +} diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuInfoRespDTO.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuInfoRespDTO.java new file mode 100644 index 000000000..f9d349e48 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuInfoRespDTO.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.product.api.sku.dto; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +import java.util.List; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Data +public class SkuInfoRespDTO { + + /** + * 商品 SKU 编号,自增 + */ + private Long id; + /** + * 商品 SKU 名字 + */ + private String name; + /** + * SPU 编号 + */ + private Long spuId; + + /** + * 规格值数组,JSON 格式 + */ + private List properties; + /** + * 销售价格,单位:分 + */ + private Integer price; + /** + * 市场价,单位:分 + */ + private Integer marketPrice; + /** + * 成本价,单位:分 + */ + private Integer costPrice; + /** + * SKU 的条形码 + */ + private String barCode; + /** + * 图片地址 + */ + private String picUrl; + /** + * SKU 状态 + *

+ * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 库存 + */ + private Integer stock; + /** + * 预警预存 + */ + private Integer warnStock; + /** + * 商品重量,单位:kg 千克 + */ + private Double weight; + /** + * 商品体积,单位:m^3 平米 + */ + private Double volume; + + /** + * 商品属性 + */ + @Data + public static class Property { + + /** + * 属性编号 + */ + private Long propertyId; + /** + * 属性值编号 + */ + private Long valueId; + + } + + +} diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java new file mode 100644 index 000000000..5dc2bf4cf --- /dev/null +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.product.api.spu; + +import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO; +import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO; + +import java.util.Collection; +import java.util.List; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface ProductSpuApi { + + + /** + * 根据spuId列表 查询spu信息 + * + * @param spuIds spu ID列表 + * @return spu信息列表 + */ + List getSpusByIds(Collection spuIds); +} diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/SpuInfoRespDTO.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/SpuInfoRespDTO.java new file mode 100644 index 000000000..6d0206b7d --- /dev/null +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/SpuInfoRespDTO.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.product.api.spu.dto; + +import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO; +import cn.iocoder.yudao.module.product.enums.spu.ProductSpuSpecTypeEnum; +import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; +import lombok.Data; + +import java.util.List; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Data +public class SpuInfoRespDTO { + + /** + * 商品 SPU 编号,自增 + */ + private Long id; + + // ========== 基本信息 ========= + + /** + * 商品名称 + */ + private String name; + /** + * 商品编码 + */ + private String code; + /** + * 商品卖点 + */ + private String sellPoint; + /** + * 商品详情 + */ + private String description; + /** + * 商品分类编号 + */ + private Long categoryId; + /** + * 商品品牌编号 + */ + private Long brandId; + /** + * 商品图片的数组 + *

+ * 1. 第一张图片将作为商品主图,支持同时上传多张图; + * 2. 建议使用尺寸 800x800 像素以上、大小不超过 1M 的正方形图片; + * 3. 至少 1 张,最多上传 10 张 + */ + private List picUrls; + /** + * 商品视频 + */ + private String videoUrl; + + /** + * 排序字段 + */ + private Integer sort; + /** + * 商品状态 + *

+ * 枚举 {@link ProductSpuStatusEnum} + */ + private Integer status; + + // ========== SKU 相关字段 ========= + + /** + * 规格类型 + *

+ * 枚举 {@link ProductSpuSpecTypeEnum} + */ + private Integer specType; + /** + * 最小价格,单位使用:分 + *

+ * 基于其对应的 {@link SkuInfoRespDTO#getPrice()} 最小值 + */ + private Integer minPrice; + /** + * 最大价格,单位使用:分 + *

+ * 基于其对应的 {@link SkuInfoRespDTO#getPrice()} 最大值 + */ + private Integer maxPrice; + /** + * 市场价,单位使用:分 + *

+ * 基于其对应的 {@link SkuInfoRespDTO#getMarketPrice()} 最大值 + */ + private Integer marketPrice; + /** + * 总库存 + *

+ * 基于其对应的 {@link SkuInfoRespDTO#getStock()} 求和 + */ + private Integer totalStock; + /** + * 是否展示库存 + */ + private Boolean showStock; + + // ========== 统计相关字段 ========= + + /** + * 商品销量 + */ + private Integer salesCount; + /** + * 虚拟销量 + */ + private Integer virtualSalesCount; + /** + * 商品点击量 + */ + private Integer clickCount; + +} diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/enums/ErrorCodeConstants.java new file mode 100644 index 000000000..5bdc0ff82 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/enums/ErrorCodeConstants.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.trade.enums.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * Trade-Order 错误码 Core 枚举类 + *

+ * Trade-Order 系统,使用 1-011-000-000 段 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface ErrorCodeConstants { + + /** + * ========== Order 模块 1-011-000-000 ========== + */ + ErrorCode ORDER_SKU_NOT_FOUND = new ErrorCode(1011000001, "商品不存在"); + ErrorCode ORDER_SPU_NOT_SALE = new ErrorCode(1011000002, "商品不可售卖"); + ErrorCode ORDER_SKU_NOT_SALE = new ErrorCode(1011000003, "商品Sku不可售卖"); + ErrorCode ORDER_SKU_STOCK_NOT_ENOUGH = new ErrorCode(1011000004, "商品库存不足"); + +} diff --git a/yudao-module-mall/yudao-module-trade-biz/pom.xml b/yudao-module-mall/yudao-module-trade-biz/pom.xml index ee95393a9..c9b582d93 100644 --- a/yudao-module-mall/yudao-module-trade-biz/pom.xml +++ b/yudao-module-mall/yudao-module-trade-biz/pom.xml @@ -23,12 +23,25 @@ yudao-module-trade-api ${revision} + cn.iocoder.boot yudao-module-product-api ${revision} + + cn.iocoder.boot + yudao-module-pay-api + ${revision} + + + + cn.iocoder.boot + yudao-module-market-api + ${revision} + + cn.iocoder.boot diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java index da027e1fa..7b7ed4b83 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java @@ -1,15 +1,19 @@ package cn.iocoder.yudao.module.trade.controller.app.order; +import cn.hutool.extra.servlet.ServletUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderGetCreateInfoRespVO; import cn.iocoder.yudao.module.trade.controller.app.order.vo.TradeOrderPageReqVO; import cn.iocoder.yudao.module.trade.controller.app.order.vo.TradeOrderRespVO; +import cn.iocoder.yudao.module.trade.service.order.TradeOrderService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -19,12 +23,16 @@ import javax.servlet.http.HttpServletRequest; @Api(tags = "用户 App - 交易订单") @RestController @RequestMapping("/trade/order") +@RequiredArgsConstructor @Validated @Slf4j public class AppTradeOrderController { // TODO 在思考下; + private final TradeOrderService tradeOrderService; + + @GetMapping("/get-create-info") @ApiOperation("基于商品,确认创建订单") @PreAuthenticated @@ -36,11 +44,18 @@ public class AppTradeOrderController { @PostMapping("/create") @ApiOperation("创建订单") @PreAuthenticated - public CommonResult createTradeOrder(@RequestBody AppTradeOrderCreateReqVO createReqVO, - HttpServletRequest servletRequest) { + public CommonResult createTradeOrder(@RequestBody AppTradeOrderCreateReqVO createReqVO, + HttpServletRequest servletRequest) { // return success(tradeOrderService.createTradeOrder(UserSecurityContextHolder.getUserId(), // HttpUtil.getIp(servletRequest), createReqVO)); - return null; + // 获取登录用户 + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + // 获取用户ip地址 + String clientIp = ServletUtil.getClientIP(servletRequest); + // 创建交易订单,预支付记录 + Long result = tradeOrderService.createTradeOrder(loginUserId, clientIp, createReqVO); + + return CommonResult.success(result); } @GetMapping("/get") diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java index 85edde55d..d454fd63c 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java @@ -38,7 +38,7 @@ public class AppTradeOrderCreateReqVO { @ApiModelProperty(name = "商品 SKU 编号", required = true, example = "111") @NotNull(message = "商品 SKU 编号不能为空") - private Integer skuId; + private Long skuId; @ApiModelProperty(name = "商品 SKU 购买数量", required = true, example = "1024") @NotNull(message = "商品 SKU 购买数量不能为空") diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java new file mode 100644 index 000000000..dd3cca38e --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.trade.convert.order; + +import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO; +import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Mapper +public interface TradeOrderConvert { + + TradeOrderConvert INSTANCE = Mappers.getMapper(TradeOrderConvert.class); + + + TradeOrderDO convert(AppTradeOrderCreateReqVO createReqVO, PriceCalculateRespDTO.Order order); +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderItemConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderItemConvert.java new file mode 100644 index 000000000..f9c44b4a6 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderItemConvert.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.trade.convert.order; + +import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Mapper +public interface TradeOrderItemConvert { + + TradeOrderItemConvert INSTANCE = Mappers.getMapper(TradeOrderItemConvert.class); + + /** + * + * @param tradeOrder 交易订单 + * @param items sku列表价格 + * @return 订单项 + */ + @Mappings({ + @Mapping(source = "tradeOrder.userId", target = "userId"), + @Mapping(source = "tradeOrder.orderId", target = "orderId") + }) + List convertList(TradeOrderDO tradeOrder, List items); +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/pay/PayOrderConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/pay/PayOrderConvert.java new file mode 100644 index 000000000..22dd11fe9 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/pay/PayOrderConvert.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.trade.convert.pay; + +import cn.iocoder.yudao.module.pay.api.order.PayOrderDataCreateReqDTO; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; +import org.mapstruct.factory.Mappers; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface PayOrderConvert { + + PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class); + + + PayOrderDataCreateReqDTO convert(TradeOrderDO tradeOrderDO); +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/price/PriceConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/price/PriceConvert.java new file mode 100644 index 000000000..3b2d173c3 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/price/PriceConvert.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.trade.convert.price; + +import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO; +import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Mapper +public interface PriceConvert { + + PriceConvert INSTANCE = Mappers.getMapper(PriceConvert.class); + + @Mappings( + @Mapping(source = "userId" , target = "userId") + ) + PriceCalculateReqDTO convert(AppTradeOrderCreateReqVO createReqVO , Long userId); +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/sku/ProductSkuConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/sku/ProductSkuConvert.java new file mode 100644 index 000000000..fffc6fa42 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/sku/ProductSkuConvert.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.trade.convert.sku; + +import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Mapper +public interface ProductSkuConvert { + + ProductSkuConvert INSTANCE = Mappers.getMapper(ProductSkuConvert.class); + + List convert(List tradeOrderItems); +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderMapper.java new file mode 100644 index 000000000..ed74f9b6c --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderMapper.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.trade.dal.mysql.order; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface TradeOrderMapper extends BaseMapperX { +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/orderitem/TradeOrderItemMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/orderitem/TradeOrderItemMapper.java new file mode 100644 index 000000000..4e33cddd1 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/orderitem/TradeOrderItemMapper.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.trade.dal.mysql.orderitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface TradeOrderItemMapper extends BaseMapperX { +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderService.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderService.java new file mode 100644 index 000000000..f3fc4b5e1 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderService.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.trade.service.order; + +import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface TradeOrderService { + + /** + * 创建交易订单 + * @param loginUserId 登录用户 + * @param clientIp 用户ip地址 + * @param createReqVO 创建交易订单请求模型 + * @return 交易订单创建结果 + */ + Long createTradeOrder(Long loginUserId, String clientIp, AppTradeOrderCreateReqVO createReqVO); +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderServiceImpl.java new file mode 100644 index 000000000..3554e7d4c --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderServiceImpl.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.module.trade.service.order; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.market.api.price.PriceApi; +import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO; +import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO; +import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; +import cn.iocoder.yudao.module.pay.api.order.PayOrderDataCreateReqDTO; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO; +import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO; +import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; +import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO.Item; +import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; +import cn.iocoder.yudao.module.trade.convert.order.TradeOrderItemConvert; +import cn.iocoder.yudao.module.trade.convert.pay.PayOrderConvert; +import cn.iocoder.yudao.module.trade.convert.price.PriceConvert; +import cn.iocoder.yudao.module.trade.convert.sku.ProductSkuConvert; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper; +import cn.iocoder.yudao.module.trade.dal.mysql.orderitem.TradeOrderItemMapper; +import cn.iocoder.yudao.module.trade.enums.enums.ErrorCodeConstants; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +@Service +@RequiredArgsConstructor +public class TradeOrderServiceImpl implements TradeOrderService { + + private final TradeOrderMapper tradeOrderMapper; + + private final TradeOrderItemMapper tradeOrderItemMapper; + + private final PriceApi priceApi; + + private final ProductSkuApi productSkuApi; + + private final ProductSpuApi productSpuApi; + + private final PayOrderApi payOrderApi; + + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createTradeOrder(Long loginUserId, String clientIp, AppTradeOrderCreateReqVO createReqVO) { + + List items = createReqVO.getItems(); + // 商品SKU检查 sku可售状态,库存 + List skuInfos = productSkuApi.getSkusByIds(CollectionUtils.convertSet(items, Item::getSkuId)); + Map skuInfoMap = CollectionUtils.convertMap(skuInfos, SkuInfoRespDTO::getId); + checkSaleableAndStockFromSpu(skuInfoMap, items); + + // 商品SPU检查 sku可售状态,库存 + List spuInfos = productSpuApi.getSpusByIds(CollectionUtils.convertSet(skuInfos, SkuInfoRespDTO::getSpuId)); + checkSaleableFromSpu(spuInfos); + + // 价格计算 + PriceCalculateReqDTO priceCalculateReqDTO = PriceConvert.INSTANCE.convert(createReqVO, loginUserId); + PriceCalculateRespDTO priceResp = priceApi.calculatePrice(priceCalculateReqDTO); + + // 订单信息记录 + TradeOrderDO tradeOrderDO = TradeOrderConvert.INSTANCE.convert(createReqVO, priceResp.getOrder()); + tradeOrderMapper.insert(tradeOrderDO); + + // 订单项信息记录 + List tradeOrderItems = TradeOrderItemConvert.INSTANCE.convertList(tradeOrderDO, priceResp.getItems()); + //-填充订单项-SKU信息 + fillItemsInfoFromSku(tradeOrderItems, skuInfoMap); + tradeOrderItemMapper.insertBatch(tradeOrderItems); + + // 库存扣减 + List skuDecrementStockItems = ProductSkuConvert.INSTANCE.convert(tradeOrderItems); + productSkuApi.decrementStockBatch(SkuDecrementStockBatchReqDTO.of(skuDecrementStockItems)); + // 生成预支付 + + PayOrderDataCreateReqDTO payOrderCreateReqDTO = PayOrderConvert.INSTANCE.convert(tradeOrderDO); + return payOrderApi.createPayOrder(payOrderCreateReqDTO); + } + + private void fillItemsInfoFromSku(List tradeOrderItems, + Map spuInfos) { + for (TradeOrderItemDO tradeOrderItem : tradeOrderItems) { + // 填充SKU信息 + SkuInfoRespDTO skuInfoRespDTO = spuInfos.get(tradeOrderItem.getSkuId()); + tradeOrderItem.setSpuId(skuInfoRespDTO.getSpuId()); + tradeOrderItem.setPicUrl(skuInfoRespDTO.getPicUrl()); + tradeOrderItem.setName(skuInfoRespDTO.getName()); + // todo + List property = + BeanUtil.copyToList(skuInfoRespDTO.getProperties(), TradeOrderItemDO.Property.class); + tradeOrderItem.setProperties(property); + } + } + + private void checkSaleableFromSpu(List spuInfos) { + SpuInfoRespDTO spu = CollectionUtils.findFirst(spuInfos, + spuInfoDTO -> !Objects.equals(ProductSpuStatusEnum.ENABLE.getStyle(), spuInfoDTO.getStatus())); + if (Objects.isNull(spu)) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.ORDER_SPU_NOT_SALE); + } + } + + private void checkSaleableAndStockFromSpu(Map skuInfoMap, + List items) { + // sku 不存在 + if (items.size() != skuInfoMap.size()) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.ORDER_SKU_NOT_FOUND); + } + for (Item item : items) { + SkuInfoRespDTO skuInfoDTO = skuInfoMap.get(item.getSkuId()); + // sku禁用 + if (!Objects.equals(CommonStatusEnum.ENABLE.getStatus(), skuInfoDTO.getStatus())) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.ORDER_SKU_NOT_SALE); + } + // sku库存不足 + if (item.getCount() > skuInfoDTO.getStock()) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.ORDER_SKU_STOCK_NOT_ENOUGH); + } + } + + } +} diff --git a/yudao-module-pay/yudao-module-pay-api/pom.xml b/yudao-module-pay/yudao-module-pay-api/pom.xml index a7bebc2f1..0903abed5 100644 --- a/yudao-module-pay/yudao-module-pay-api/pom.xml +++ b/yudao-module-pay/yudao-module-pay-api/pom.xml @@ -21,6 +21,13 @@ cn.iocoder.boot yudao-common + + + + org.springframework.boot + spring-boot-starter-validation + true + diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java new file mode 100644 index 000000000..204ce7a32 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pay.api.order; + +import javax.validation.Valid; + +/** + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface PayOrderApi { + + + /** + * 创建支付单 + * + * @param reqDTO 创建请求 + * @return 支付单编号 + */ + Long createPayOrder(@Valid PayOrderDataCreateReqDTO reqDTO); + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderDataCreateReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderDataCreateReqDTO.java new file mode 100644 index 000000000..acdfa7a20 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderDataCreateReqDTO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.pay.api.order; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Date; + +/** + * 支付单创建 Request DTO + * @author LeeYan9 + */ +@Data +public class PayOrderDataCreateReqDTO implements Serializable { + + /** + * 应用编号 + */ + @NotNull(message = "应用编号不能为空") + private Long appId; + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = 32, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述 + */ + @NotEmpty(message = "商品描述信息不能为空") + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer amount; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private Date expireTime; + +} \ No newline at end of file