feat(shop): 添加用户订单和卡包统计功能
- 在 ShopOrderMapper 中新增 selectUserOrderStats 方法用于订单状态统计 - 在 ShopOrderService 和 ShopOrderServiceImpl 中实现用户订单统计功能 - 添加 UserOrderStats DTO 类定义订单各状态数量统计 - 创建 UserOrderController 提供用户订单统计 API 接口 - 实现用户卡包统计功能,包括余额、积分、优惠券、礼品卡统计 - 添加 UserCardController 和 UserCardStats DTO 类 - 优化 Swagger 配置以支持 /api/user/** 路径的 API 文档 - 为统计接口添加 Redis 缓存以提升性能 - 清理 ShopOrderController 中不必要的导入依赖
This commit is contained in:
@@ -80,7 +80,8 @@ public class SwaggerConfig {
|
||||
public GroupedOpenApi shopApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("shop")
|
||||
.pathsToMatch("/api/shop/**")
|
||||
// 订单等用户侧接口在 shop 包内,但路径使用 /api/user/**(前端统一 user 侧 API 前缀)
|
||||
.pathsToMatch("/api/shop/**", "/api/user/**")
|
||||
.packagesToScan("com.gxwebsoft.shop")
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.gxwebsoft.shop.controller;
|
||||
|
||||
import cn.hutool.core.date.DateField;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
@@ -14,9 +12,7 @@ import com.gxwebsoft.common.core.utils.WechatCertAutoConfig;
|
||||
import com.gxwebsoft.common.core.utils.WechatPayConfigValidator;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.system.entity.Payment;
|
||||
import com.gxwebsoft.shop.entity.ShopExpress;
|
||||
import com.gxwebsoft.shop.entity.ShopOrderDelivery;
|
||||
import com.gxwebsoft.shop.entity.ShopUserAddress;
|
||||
import com.gxwebsoft.shop.service.*;
|
||||
import com.gxwebsoft.shop.service.impl.KuaiDi100Impl;
|
||||
import com.gxwebsoft.shop.task.OrderAutoCancelTask;
|
||||
@@ -31,7 +27,6 @@ import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.core.web.BatchParam;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.kuaidi100.sdk.request.BOrderReq;
|
||||
import com.wechat.pay.java.core.notification.NotificationConfig;
|
||||
import com.wechat.pay.java.core.notification.NotificationParser;
|
||||
import com.wechat.pay.java.core.notification.RequestParam;
|
||||
@@ -51,7 +46,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.gxwebsoft.shop.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.service.UserService;
|
||||
import com.gxwebsoft.shop.dto.UserCardStats;
|
||||
import com.gxwebsoft.shop.entity.ShopGift;
|
||||
import com.gxwebsoft.shop.entity.ShopUserCoupon;
|
||||
import com.gxwebsoft.shop.service.ShopGiftService;
|
||||
import com.gxwebsoft.shop.service.ShopUserCouponService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Tag(name = "用户卡包")
|
||||
@RestController
|
||||
@RequestMapping("/api/user/card")
|
||||
public class UserCardController extends BaseController {
|
||||
|
||||
private static final long USER_CARD_STATS_CACHE_SECONDS = 60L;
|
||||
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
@Resource
|
||||
private UserService userService;
|
||||
@Resource
|
||||
private ShopUserCouponService shopUserCouponService;
|
||||
@Resource
|
||||
private ShopGiftService shopGiftService;
|
||||
|
||||
@Operation(summary = "获取当前用户卡包统计(余额/积分/优惠券/礼品卡,60s 缓存)")
|
||||
@GetMapping("/stats")
|
||||
public ApiResult<UserCardStats> getUserCardStats() {
|
||||
Integer userId = getLoginUserId();
|
||||
if (userId == null) {
|
||||
return fail("请先登录", null);
|
||||
}
|
||||
|
||||
String cacheKey = "UserCard:Stats:"
|
||||
+ (getTenantId() == null ? "0" : getTenantId())
|
||||
+ ":" + userId;
|
||||
UserCardStats cached = redisUtil.get(cacheKey, UserCardStats.class);
|
||||
if (cached != null) {
|
||||
return success("ok", cached);
|
||||
}
|
||||
|
||||
// 余额/积分从用户表取;优惠券/礼品卡用 COUNT 聚合
|
||||
User user = userService.getById(userId);
|
||||
BigDecimal balance = user == null || user.getBalance() == null ? BigDecimal.ZERO : user.getBalance();
|
||||
Integer points = user == null || user.getPoints() == null ? 0 : user.getPoints();
|
||||
|
||||
long coupons = shopUserCouponService.count(new LambdaQueryWrapper<ShopUserCoupon>()
|
||||
.eq(ShopUserCoupon::getUserId, userId)
|
||||
.eq(ShopUserCoupon::getStatus, ShopUserCoupon.STATUS_UNUSED));
|
||||
|
||||
long giftCards = shopGiftService.count(new LambdaQueryWrapper<ShopGift>()
|
||||
.eq(ShopGift::getUserId, userId)
|
||||
.eq(ShopGift::getStatus, 0));
|
||||
|
||||
UserCardStats stats = new UserCardStats();
|
||||
stats.setBalance(balance.setScale(2, RoundingMode.HALF_UP).toPlainString());
|
||||
stats.setPoints(points);
|
||||
stats.setCoupons(toInt(coupons));
|
||||
stats.setGiftCards(toInt(giftCards));
|
||||
stats.setLastUpdateTime(LocalDateTime.now().format(DATETIME_FMT));
|
||||
|
||||
redisUtil.set(cacheKey, stats, USER_CARD_STATS_CACHE_SECONDS, TimeUnit.SECONDS);
|
||||
return success("ok", stats);
|
||||
}
|
||||
|
||||
private static Integer toInt(long value) {
|
||||
if (value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return (int) value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.gxwebsoft.shop.controller;
|
||||
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.shop.dto.UserOrderStats;
|
||||
import com.gxwebsoft.shop.service.ShopOrderService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 用户侧订单接口
|
||||
*/
|
||||
@Tag(name = "用户订单")
|
||||
@RestController
|
||||
@RequestMapping("/api/user/orders")
|
||||
public class UserOrderController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private ShopOrderService shopOrderService;
|
||||
|
||||
@Operation(summary = "获取当前用户订单状态统计(一次聚合查询,带 60s 缓存)")
|
||||
@GetMapping("/stats")
|
||||
public ApiResult<UserOrderStats> getUserOrderStats(@RequestParam(value = "type", required = false) Integer type) {
|
||||
Integer userId = getLoginUserId();
|
||||
if (userId == null) {
|
||||
return fail("用户未登录", null);
|
||||
}
|
||||
return success(shopOrderService.getUserOrderStats(userId, getTenantId(), type));
|
||||
}
|
||||
}
|
||||
|
||||
31
src/main/java/com/gxwebsoft/shop/dto/UserCardStats.java
Normal file
31
src/main/java/com/gxwebsoft/shop/dto/UserCardStats.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.gxwebsoft.shop.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户卡包统计(余额/积分/优惠券/礼品卡)
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "UserCardStats", description = "用户卡包统计")
|
||||
public class UserCardStats implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "用户余额(字符串/BigDecimal 字符串)")
|
||||
private String balance;
|
||||
|
||||
@Schema(description = "用户积分")
|
||||
private Integer points;
|
||||
|
||||
@Schema(description = "可用优惠券数量(按 status=0 口径)")
|
||||
private Integer coupons;
|
||||
|
||||
@Schema(description = "未使用礼品卡数量(按 status=0 口径)")
|
||||
private Integer giftCards;
|
||||
|
||||
@Schema(description = "最后更新时间(yyyy-MM-dd HH:mm:ss)")
|
||||
private String lastUpdateTime;
|
||||
}
|
||||
|
||||
50
src/main/java/com/gxwebsoft/shop/dto/UserOrderStats.java
Normal file
50
src/main/java/com/gxwebsoft/shop/dto/UserOrderStats.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.gxwebsoft.shop.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户订单各状态数量统计(用于订单页徽标/统计)
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "UserOrderStats", description = "用户订单各状态数量统计")
|
||||
public class UserOrderStats implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "全部(默认过滤已取消订单:order_status != 2)")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "待支付(statusFilter=0)")
|
||||
private Long waitPay;
|
||||
|
||||
@Schema(description = "待发货(statusFilter=1)")
|
||||
private Long waitDeliver;
|
||||
|
||||
@Schema(description = "待核销(statusFilter=2)")
|
||||
private Long waitVerify;
|
||||
|
||||
@Schema(description = "待收货(statusFilter=3)")
|
||||
private Long waitReceive;
|
||||
|
||||
@Schema(description = "待评价(statusFilter=4)")
|
||||
private Long waitComment;
|
||||
|
||||
@Schema(description = "已完成(statusFilter=5)")
|
||||
private Long completed;
|
||||
|
||||
@Schema(description = "退款/售后(statusFilter=6)")
|
||||
private Long refund;
|
||||
|
||||
@Schema(description = "已删除(statusFilter=7)")
|
||||
private Long deleted;
|
||||
|
||||
@Schema(description = "已取消(statusFilter=8)")
|
||||
private Long canceled;
|
||||
|
||||
@Schema(description = "按 statusFilter 聚合的数量映射,key 为 \"0\"~\"8\"")
|
||||
private Map<String, Long> statusFilter;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 订单Mapper
|
||||
@@ -52,4 +53,11 @@ public interface ShopOrderMapper extends BaseMapper<ShopOrder> {
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(pay_price), 0) FROM shop_order WHERE pay_status = 1 AND deleted = 0 AND pay_price IS NOT NULL")
|
||||
BigDecimal selectTotalAmount();
|
||||
|
||||
/**
|
||||
* 获取用户订单各状态数量(用于前端订单页徽标/统计,一次查询返回多项聚合结果)
|
||||
*/
|
||||
Map<String, Object> selectUserOrderStats(@Param("userId") Integer userId,
|
||||
@Param("tenantId") Integer tenantId,
|
||||
@Param("type") Integer type);
|
||||
}
|
||||
|
||||
@@ -281,6 +281,33 @@
|
||||
<include refid="selectSql"></include>
|
||||
</select>
|
||||
|
||||
<!--
|
||||
用户订单状态统计:
|
||||
对齐 ShopOrderMapper.xml 里 statusFilter 的含义(0/1/2/3/4/5/6/7/8),避免前端通过分页接口串行请求统计数量。
|
||||
-->
|
||||
<select id="selectUserOrderStats" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
-- “全部”默认也会过滤已关闭(取消)订单:statusFilter != 8 时会加 a.order_status != 2
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 THEN 1 ELSE 0 END), 0) AS total,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 AND a.pay_status = 0 AND a.order_status = 0 THEN 1 ELSE 0 END), 0) AS waitPay,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 AND a.pay_status = 1 AND a.delivery_status = 10 AND a.order_status = 0 THEN 1 ELSE 0 END), 0) AS waitDeliver,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 AND a.pay_status = 1 AND a.order_status = 0 THEN 1 ELSE 0 END), 0) AS waitVerify,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 AND a.delivery_status = 20 AND a.order_status != 1 THEN 1 ELSE 0 END), 0) AS waitReceive,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 AND a.order_status = 1 AND a.evaluate_status = 0 THEN 1 ELSE 0 END), 0) AS waitComment,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 AND a.order_status = 1 THEN 1 ELSE 0 END), 0) AS completed,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status != 2 AND (a.order_status = 4 OR a.order_status = 5 OR a.order_status = 6 OR a.order_status = 7) THEN 1 ELSE 0 END), 0) AS refund,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 1 THEN 1 ELSE 0 END), 0) AS deleted,
|
||||
COALESCE(SUM(CASE WHEN a.deleted = 0 AND a.order_status = 2 THEN 1 ELSE 0 END), 0) AS canceled
|
||||
FROM shop_order a
|
||||
WHERE a.user_id = #{userId}
|
||||
<if test="tenantId != null">
|
||||
AND a.tenant_id = #{tenantId}
|
||||
</if>
|
||||
<if test="type != null">
|
||||
AND a.type = #{type}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 根据订单号查询订单 -->
|
||||
<select id="getByOutTradeNo" resultType="com.gxwebsoft.shop.entity.ShopOrder">
|
||||
SELECT * FROM shop_order WHERE order_no = #{outTradeNo}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.gxwebsoft.shop.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.shop.dto.UserOrderStats;
|
||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||
import com.gxwebsoft.shop.param.ShopOrderParam;
|
||||
|
||||
@@ -76,4 +77,13 @@ public interface ShopOrderService extends IService<ShopOrder> {
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
boolean syncPaymentStatus(String orderNo, Integer paymentStatus, String transactionId, String payTime, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 获取当前用户订单状态统计(一次聚合查询,避免前端用分页接口串行拉取多个状态数量)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID(可为空,优先走租户插件)
|
||||
* @param type 订单类型(可为空)
|
||||
*/
|
||||
UserOrderStats getUserOrderStats(Integer userId, Integer tenantId, Integer type);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -84,6 +85,8 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
|
||||
@Resource
|
||||
private ShopExpressService shopExpressService;
|
||||
|
||||
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L;
|
||||
|
||||
|
||||
@Override
|
||||
public PageResult<ShopOrder> pageRel(ShopOrderParam param) {
|
||||
@@ -135,6 +138,80 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
|
||||
return param.getOne(baseMapper.selectListRel(param));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.gxwebsoft.shop.dto.UserOrderStats getUserOrderStats(Integer userId, Integer tenantId, Integer type) {
|
||||
if (userId == null) {
|
||||
// 保持调用方逻辑简单:未登录场景由 controller 处理,这里返回全 0
|
||||
return new com.gxwebsoft.shop.dto.UserOrderStats();
|
||||
}
|
||||
|
||||
String cacheKey = "ShopOrder:UserOrderStats:"
|
||||
+ (tenantId == null ? "0" : tenantId)
|
||||
+ ":" + userId
|
||||
+ ":" + (type == null ? "all" : type);
|
||||
|
||||
com.gxwebsoft.shop.dto.UserOrderStats cached = redisUtil.get(cacheKey, com.gxwebsoft.shop.dto.UserOrderStats.class);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
Map<String, Object> raw = baseMapper.selectUserOrderStats(userId, tenantId, type);
|
||||
if (raw == null) {
|
||||
raw = Collections.emptyMap();
|
||||
}
|
||||
com.gxwebsoft.shop.dto.UserOrderStats stats = new com.gxwebsoft.shop.dto.UserOrderStats();
|
||||
stats.setTotal(getLong(raw, "total"));
|
||||
stats.setWaitPay(getLong(raw, "waitPay"));
|
||||
stats.setWaitDeliver(getLong(raw, "waitDeliver"));
|
||||
stats.setWaitVerify(getLong(raw, "waitVerify"));
|
||||
stats.setWaitReceive(getLong(raw, "waitReceive"));
|
||||
stats.setWaitComment(getLong(raw, "waitComment"));
|
||||
stats.setCompleted(getLong(raw, "completed"));
|
||||
stats.setRefund(getLong(raw, "refund"));
|
||||
stats.setDeleted(getLong(raw, "deleted"));
|
||||
stats.setCanceled(getLong(raw, "canceled"));
|
||||
|
||||
Map<String, Long> statusFilter = new HashMap<>();
|
||||
statusFilter.put("0", stats.getWaitPay());
|
||||
statusFilter.put("1", stats.getWaitDeliver());
|
||||
statusFilter.put("2", stats.getWaitVerify());
|
||||
statusFilter.put("3", stats.getWaitReceive());
|
||||
statusFilter.put("4", stats.getWaitComment());
|
||||
statusFilter.put("5", stats.getCompleted());
|
||||
statusFilter.put("6", stats.getRefund());
|
||||
statusFilter.put("7", stats.getDeleted());
|
||||
statusFilter.put("8", stats.getCanceled());
|
||||
stats.setStatusFilter(statusFilter);
|
||||
|
||||
redisUtil.set(cacheKey, stats, USER_ORDER_STATS_CACHE_SECONDS, TimeUnit.SECONDS);
|
||||
return stats;
|
||||
}
|
||||
|
||||
private static Long getLong(Map<String, Object> raw, String key) {
|
||||
Object value = raw.get(key);
|
||||
if (value == null) {
|
||||
value = raw.get(key.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
if (value == null) {
|
||||
value = raw.get(key.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return toLong(value);
|
||||
}
|
||||
|
||||
private static Long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return 0L;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
} catch (Exception e) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<String, String> createWxOrder(ShopOrder order) {
|
||||
Payment payment = null; // 声明在try块外面,这样catch块也能访问
|
||||
|
||||
Reference in New Issue
Block a user