diff --git a/agileboot-admin/src/main/java/com/agileboot/admin/controller/ab98/Ab98UserController.java b/agileboot-admin/src/main/java/com/agileboot/admin/controller/ab98/Ab98UserController.java index 4da3d9b..a269aa9 100644 --- a/agileboot-admin/src/main/java/com/agileboot/admin/controller/ab98/Ab98UserController.java +++ b/agileboot-admin/src/main/java/com/agileboot/admin/controller/ab98/Ab98UserController.java @@ -21,6 +21,7 @@ import com.agileboot.domain.ab98.user.command.UpdateAb98UserCommand; import com.agileboot.domain.ab98.user.db.Ab98UserEntity; import com.agileboot.domain.ab98.user.dto.Ab98UserDTO; import com.agileboot.domain.ab98.user.query.SearchAb98UserQuery; +import com.agileboot.domain.ab98.user.query.SearchAb98UserWithWxQuery; import com.agileboot.domain.wx.user.WxUserApplicationService; import com.agileboot.domain.wx.utils.DynamicCodeGenerator; import io.swagger.v3.oas.annotations.Operation; @@ -58,7 +59,7 @@ public class Ab98UserController extends BaseController { @Operation(summary = "带微信信息的用户列表") @GetMapping("/withWx") - public ResponseDTO> listWithWx(SearchAb98UserQuery query) { + public ResponseDTO> listWithWx(SearchAb98UserWithWxQuery query) { PageDTO page = userApplicationService.getUserListWithWx(query); return ResponseDTO.ok(page); } diff --git a/agileboot-api/src/main/java/com/agileboot/api/controller/ApprovalApiController.java b/agileboot-api/src/main/java/com/agileboot/api/controller/ApprovalApiController.java index 6c2a8d8..3672f4e 100644 --- a/agileboot-api/src/main/java/com/agileboot/api/controller/ApprovalApiController.java +++ b/agileboot-api/src/main/java/com/agileboot/api/controller/ApprovalApiController.java @@ -23,6 +23,7 @@ import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity; import com.agileboot.domain.shop.paymentOperationLog.PaymentOperationLogApplicationService; import com.agileboot.domain.shop.paymentOperationLog.command.AddPaymentOperationLogCommand; +import com.agileboot.domain.common.cache.CaffeineCacheService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -56,6 +57,7 @@ public class ApprovalApiController { /** 订单应用服务,用于处理订单相关查询操作 */ private final OrderApplicationService orderApplicationService; private final PaymentOperationLogApplicationService paymentOperationLogApplicationService; + private final CaffeineCacheService caffeineCacheService; /** * 处理审批操作 @@ -74,6 +76,18 @@ public class ApprovalApiController { return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "操作状态不能为空")); } + // 重复请求过滤 + String cacheKey = "handleApproval:" + command.getApprovalId() + ":" + command.getStatus(); + if (command.getStatus() == 2 && command.getReturnAmount() != null) { + cacheKey += ":amount:" + command.getReturnAmount().stripTrailingZeros().toPlainString(); + } + Boolean cached = caffeineCacheService.apiCache.get(cacheKey); + if (cached != null && cached) { + return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "重复请求,请稍后重试")); + } + // 放入缓存,标记请求已处理 + caffeineCacheService.apiCache.put(cacheKey, Boolean.TRUE); + try { if (command.getStatus() == 2) { if (command.getReturnAmount() == null || command.getReturnAmount().compareTo(BigDecimal.ZERO) <= 0) { @@ -102,6 +116,15 @@ public class ApprovalApiController { return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "操作状态不能为空")); } + // 重复请求过滤 + String cacheKey = "handleAssetApproval:" + command.getApprovalId() + ":" + command.getStatus(); + Boolean cached = caffeineCacheService.apiCache.get(cacheKey); + if (cached != null && cached) { + return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "重复请求,请稍后重试")); + } + // 放入缓存,标记请求已处理 + caffeineCacheService.apiCache.put(cacheKey, Boolean.TRUE); + try { if (command.getStatus() == 2) { approvalApplicationService.updateApprovalStatusAndComplete(command); @@ -132,6 +155,15 @@ public class ApprovalApiController { return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "审批ID不能为空")); } + // 重复请求过滤 + String cacheKey = "allocateApprovalGoods:" + command.getApprovalId(); + Boolean cached = caffeineCacheService.apiCache.get(cacheKey); + if (cached != null && cached) { + return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "重复请求,请稍后重试")); + } + // 放入缓存,标记请求已处理 + caffeineCacheService.apiCache.put(cacheKey, Boolean.TRUE); + try { approvalApplicationService.allocateApprovalGoodsCells(command); return ResponseDTO.ok("操作成功"); @@ -169,6 +201,15 @@ public class ApprovalApiController { return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "归还图片不能为空")); } + // 重复请求过滤 + String cacheKey = "submitApproval:" + command.getOrderGoodsId() + ":" + command.getReturnQuantity(); + Boolean cached = caffeineCacheService.apiCache.get(cacheKey); + if (cached != null && cached) { + return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "重复请求,请稍后重试")); + } + // 放入缓存,标记请求已处理 + caffeineCacheService.apiCache.put(cacheKey, Boolean.TRUE); + // 查询订单商品信息 ShopOrderGoodsEntity orderGoods = orderApplicationService.getOrderGoodsById(command.getOrderGoodsId()); if (null == orderGoods) { diff --git a/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java b/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java index b4f3e26..7c987d2 100644 --- a/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java +++ b/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java @@ -18,18 +18,23 @@ import com.agileboot.domain.shop.payment.PaymentApplicationService; import com.agileboot.domain.shop.payment.dto.RefundVO; import com.agileboot.domain.shop.paymentOperationLog.PaymentOperationLogApplicationService; import com.agileboot.domain.shop.paymentOperationLog.command.AddPaymentOperationLogCommand; +import com.agileboot.domain.common.cache.CaffeineCacheService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import com.agileboot.common.core.dto.ResponseDTO; import com.agileboot.domain.shop.order.OrderApplicationService; import com.agileboot.domain.shop.order.command.SubmitOrderCommand; +import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity; import lombok.RequiredArgsConstructor; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.Comparator; +import java.util.stream.Collectors; /** * 订单控制器,处理与订单相关的HTTP请求 @@ -44,6 +49,7 @@ public class OrderController extends BaseController { private final OrderApplicationService orderApplicationService; private final PaymentApplicationService paymentApplicationService; private final PaymentOperationLogApplicationService paymentOperationLogApplicationService; + private final CaffeineCacheService caffeineCacheService; @Autowired private WxshopConfig wxshopConfig; @@ -54,6 +60,42 @@ public class OrderController extends BaseController { */ @PostMapping("/submit") public ResponseDTO submitOrder(@Validated @RequestBody SubmitOrderCommand command) { + // 构造缓存键 + String userId = command.getOpenid(); + if (StringUtils.isBlank(userId)) { + userId = command.getQyUserid(); + } + if (StringUtils.isBlank(userId) && command.getAb98UserId() != null) { + userId = "ab98:" + command.getAb98UserId(); + } + if (StringUtils.isBlank(userId)) { + userId = "anonymous"; + } + + // 商品列表哈希 + int goodsHash = 0; + if (command.getGoodsList() != null) { + goodsHash = command.getGoodsList().stream() + .sorted(Comparator.comparing(ShopOrderGoodsEntity::getGoodsId)) + .map(goods -> goods.getGoodsId() + ":" + goods.getQuantity()) + .collect(Collectors.joining(",")) + .hashCode(); + } + + String cacheKey = "submitOrder:" + userId + ":" + + (StringUtils.isBlank(command.getCorpid()) ? "nocorpid" : command.getCorpid()) + ":" + + (StringUtils.isBlank(command.getPaymentType()) ? "nopayment" : command.getPaymentType()) + ":" + + (command.getMode() == null ? "0" : command.getMode()) + ":" + + goodsHash; + + // 重复请求过滤 + Boolean cached = caffeineCacheService.apiCache.get(cacheKey); + if (cached != null && cached) { + return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "重复请求,请稍后重试")); + } + // 放入缓存,标记请求已处理 + caffeineCacheService.apiCache.put(cacheKey, Boolean.TRUE); + try { CreateOrderResult result = orderApplicationService.createOrder(command); return ResponseDTO.ok(result); @@ -118,6 +160,17 @@ public class OrderController extends BaseController { */ @PostMapping("/refund/{orderId}") public ResponseDTO refundOrder(@PathVariable Long orderId, @RequestParam int money) { + // 构造缓存键 + String cacheKey = "refundOrder:" + orderId + ":" + money; + + // 重复请求过滤 + Boolean cached = caffeineCacheService.apiCache.get(cacheKey); + if (cached != null && cached) { + return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "重复请求,请稍后重试")); + } + // 放入缓存,标记请求已处理 + caffeineCacheService.apiCache.put(cacheKey, Boolean.TRUE); + OrderModel orderModel = orderApplicationService.loadById(orderId); try { // 退款金额对比, 退款金额不能大于订单金额 @@ -125,7 +178,7 @@ public class OrderController extends BaseController { BigDecimal amountInFen = orderModel.getTotalAmount() .multiply(new BigDecimal("100")) .setScale(0, RoundingMode.HALF_UP); - + if (money < 0) { throw new IllegalArgumentException("退款金额不能为负数"); } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/Ab98UserApplicationService.java b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/Ab98UserApplicationService.java index 5c75d4f..bfad31d 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/Ab98UserApplicationService.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/Ab98UserApplicationService.java @@ -19,12 +19,15 @@ import com.agileboot.domain.ab98.user.dto.Ab98UserDTO; import com.agileboot.domain.ab98.user.model.Ab98UserModel; import com.agileboot.domain.ab98.user.model.Ab98UserModelFactory; import com.agileboot.domain.ab98.user.query.SearchAb98UserQuery; +import com.agileboot.domain.ab98.user.query.SearchAb98UserWithWxQuery; import com.agileboot.domain.qywx.user.db.QyUserEntity; import com.agileboot.domain.qywx.user.db.QyUserService; import com.agileboot.domain.qywx.userQySys.db.SysUserQyUserEntity; import com.agileboot.domain.qywx.userQySys.db.SysUserQyUserService; import com.agileboot.domain.system.user.db.SysUserEntity; import com.agileboot.domain.system.user.db.SysUserService; +import com.agileboot.domain.wx.user.db.WxUserService; +import com.agileboot.domain.wx.user.db.WxUserEntity; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -46,6 +49,7 @@ public class Ab98UserApplicationService { private final SysUserQyUserService sysUserQyUserService; private final SysUserService sysUserService; private final UserBalanceService userBalanceService; + private final WxUserService wxUserService; public PageDTO getUserList(SearchAb98UserQuery query) { Page page = userService.getUserListWithTagFilter(query); @@ -55,7 +59,7 @@ public class Ab98UserApplicationService { return new PageDTO<>(dtoList, page.getTotal()); } - public PageDTO getUserListWithWx(SearchAb98UserQuery query) { + public PageDTO getUserListWithWx(SearchAb98UserWithWxQuery query) { Page page = userService.getUserListWithWx(query); List dtoList = page.getRecords().stream() .map(Ab98UserDTO::new) @@ -108,6 +112,9 @@ public class Ab98UserApplicationService { } Ab98UserDetailDTO dto = new Ab98UserDetailDTO(ab98UserEntity); dto.setUserBalanceEntity(userBalanceEntity); + // 查询关联的微信用户列表 + List wxUserList = wxUserService.getAllByAb98UserId(ab98UserId); + dto.setWxUserList(wxUserList); return dto; } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/db/Ab98UserMapper.java b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/db/Ab98UserMapper.java index 45d2226..9c29ab4 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/db/Ab98UserMapper.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/db/Ab98UserMapper.java @@ -66,12 +66,11 @@ public interface Ab98UserMapper extends BaseMapper { "LEFT JOIN ab98_user_tag t ON u.ab98_user_id = t.ab98_user_id " + "LEFT JOIN user_balance ub ON u.ab98_user_id = ub.ab98_user_id " + "LEFT JOIN wx_user w ON u.ab98_user_id = w.ab98_user_id " + - "${ew.customSqlSegment}" + " UNION ALL " + "SELECT NULL as ab98_user_id, NULL as openid, NULL as userid, NULL as qy_user_id, NULL as name, NULL as tel, NULL as idnum, NULL as sex, NULL as face_img, NULL as idcard_front, NULL as idcard_back, NULL as address, NULL as registered, NULL as creator_id, w.create_time as create_time, NULL as updater_id, w.update_time as update_time, NULL as deleted, NULL as ab98_balance, NULL as balance, NULL as use_balance, NULL as balance_limit, w.wx_user_id, w.openid as wx_user_openid, w.nick_name as wx_nick_name, w.avatar as wx_avatar " + "FROM wx_user w " + "WHERE w.ab98_user_id IS NULL" + - ") t ORDER BY create_time DESC") + ") t ${ew.customSqlSegment} ORDER BY create_time DESC") Page getUserListWithWx( Page page, @Param(Constants.WRAPPER) Wrapper queryWrapper diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/dto/Ab98UserDetailDTO.java b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/dto/Ab98UserDetailDTO.java index 8f9ae00..781b0b6 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/dto/Ab98UserDetailDTO.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/dto/Ab98UserDetailDTO.java @@ -6,6 +6,8 @@ import com.agileboot.common.annotation.ExcelSheet; import com.agileboot.common.core.base.BaseEntity; import com.agileboot.domain.ab98.user.db.Ab98UserEntity; import com.agileboot.domain.ab98.user_balance.db.UserBalanceEntity; +import com.agileboot.domain.wx.user.db.WxUserEntity; +import java.util.List; import lombok.Data; import lombok.EqualsAndHashCode; @@ -76,4 +78,6 @@ public class Ab98UserDetailDTO extends BaseEntity { @ExcelColumn(name = "注册状态") private String registeredStatus; + + private List wxUserList; } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/query/SearchAb98UserWithWxQuery.java b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/query/SearchAb98UserWithWxQuery.java new file mode 100644 index 0000000..a1a416c --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/query/SearchAb98UserWithWxQuery.java @@ -0,0 +1,58 @@ +package com.agileboot.domain.ab98.user.query; + +import cn.hutool.core.util.StrUtil; +import com.agileboot.common.core.page.AbstractPageQuery; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import java.util.Date; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchAb98UserWithWxQuery extends AbstractPageQuery { + + private Long ab98UserId; + private String openid; + private String userid; + private String name; + private String tel; + private String idnum; + private String sex; + private Boolean registered; + private Date startTime; + private Date endTime; + private String tagName; + private String corpid; + private String wxUserOpenid; + private Boolean hasAb98UserId; + + @Override + public QueryWrapper addQueryCondition() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + queryWrapper + .eq(ab98UserId != null, "ab98_user_id", ab98UserId) + .eq(StrUtil.isNotEmpty(openid), "openid", openid) + .eq(StrUtil.isNotEmpty(userid), "userid", userid) + .like(StrUtil.isNotEmpty(name), "name", name) + .like(StrUtil.isNotEmpty(tel), "tel", tel) + .like(StrUtil.isNotEmpty(idnum), "idnum", idnum) + .eq(StrUtil.isNotEmpty(sex), "sex", sex) + .eq(registered != null, "registered", registered) + .eq(StrUtil.isNotEmpty(wxUserOpenid), "wx_user_openid", wxUserOpenid) + .between(startTime != null && endTime != null, "create_time", startTime, endTime); + + // 添加是否绑定ab98UserId的条件 + if (hasAb98UserId != null) { + if (hasAb98UserId) { + queryWrapper.isNotNull("ab98_user_id"); + } else { + queryWrapper.isNull("ab98_user_id"); + } + } + + this.timeRangeColumn = "create_time"; + + return queryWrapper; + } +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java index 5c7e847..50d7ee8 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java @@ -42,6 +42,8 @@ public class CacheCenter { public static AbstractCaffeineCacheTemplate corpidCache; + public static AbstractCaffeineCacheTemplate apiCache; + @PostConstruct public void init() { GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class); @@ -59,6 +61,7 @@ public class CacheCenter { dynamicCodeCache = caffeineCache.dynamicCodeCache; accessTokenCache = caffeineCache.accessTokenCache; corpidCache = caffeineCache.corpidCache; + apiCache = caffeineCache.apiCache; } } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java index 67b34a7..f990352 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java @@ -128,6 +128,17 @@ public class CaffeineCacheService { } }; + // API缓存:30秒过期和刷新(API调用参数) + public AbstractCaffeineCacheTemplate apiCache = new AbstractCaffeineCacheTemplate( + 30, TimeUnit.SECONDS, + 30, TimeUnit.SECONDS) { + @Override + public Boolean getObjectFromDb(Object id) { + // API缓存通常不需要从数据库获取,这里返回null + return null; + } + }; + /** * 获取缓存统计信息 * @return 统计信息字符串 @@ -144,6 +155,7 @@ public class CaffeineCacheService { stats.append("Dynamic Code Cache: ").append(dynamicCodeCache.getStats()).append("\n"); stats.append("Access Token Cache: ").append(accessTokenCache.getStats()).append("\n"); stats.append("Corpid Cache: ").append(corpidCache.getStats()).append("\n"); + stats.append("API Cache: ").append(apiCache.getStats()).append("\n"); return stats.toString(); } @@ -172,6 +184,8 @@ public class CaffeineCacheService { return accessTokenCache; case "corpidCache": return corpidCache; + case "apiCache": + return apiCache; default: return null; } @@ -184,7 +198,7 @@ public class CaffeineCacheService { public String[] getAllCacheNames() { return new String[]{ "captchaCache", "loginUserCache", "userCache", - "roleCache", "postCache", "qyUseridCache", "dynamicCodeCache", "accessTokenCache", "corpidCache" + "roleCache", "postCache", "qyUseridCache", "dynamicCodeCache", "accessTokenCache", "corpidCache", "apiCache" }; } } \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java index 1228453..c527a3e 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java @@ -518,6 +518,10 @@ public class OrderApplicationService { orderQueryWrapper.eq("pay_status", 2); List orderList = orderService.list(orderQueryWrapper); + if (orderList == null || orderList.isEmpty()) { + return new GetOrdersByOpenIdDTO(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + // 构建订单商品查询条件 QueryWrapper orderGoodsQueryWrapper = new QueryWrapper<>(); // 添加订单 ID 作为查询条件,查询属于这些订单的商品 @@ -530,6 +534,10 @@ public class OrderApplicationService { // 根据查询条件获取订单商品列表 List orderGoods = orderGoodsService.list(orderGoodsQueryWrapper); + if (orderGoods == null || orderGoods.isEmpty()) { + return new GetOrdersByOpenIdDTO(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + Set orderIds = orderGoods.stream().map(ShopOrderGoodsEntity::getOrderId).collect(Collectors.toSet()); orderList = orderList.stream() .filter(order -> orderIds.contains(order.getOrderId()))