Compare commits

..

10 Commits

24 changed files with 396 additions and 52 deletions

View File

@@ -134,6 +134,43 @@ mvn spotless:apply compile
mvn spotless:apply checkstyle:check compile
```
## API 集成测试指南
`play-admin` 模块内提供了基于 `apitest` Profile 的端到端测试套件。为了稳定跑通所有 API 场景,请按以下步骤准备环境:
1. **准备数据库**
默认连接信息为 `jdbc:mysql://127.0.0.1:33306/peipei_apitest`,账号密码均为 `apitest`。可通过以下命令初始化:
```sql
CREATE DATABASE IF NOT EXISTS peipei_apitest CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'apitest'@'%' IDENTIFIED BY 'apitest';
GRANT ALL PRIVILEGES ON peipei_apitest.* TO 'apitest'@'%';
FLUSH PRIVILEGES;
```
若端口或凭证不同,请同步修改 `play-admin/src/main/resources/application-apitest.yml`。
2. **准备 Redis必需**
测试依赖 Redis 记录幂等与缓存信息。可以执行 `docker compose up -d redis`(路径:`docker/docker-compose.yml`)快速启一个实例,默认映射端口为 `36379`。
3. **执行测试**
在仓库根目录运行:
```bash
mvn -pl play-admin -am test
```
如需探查单个用例,可指定测试类:
```bash
mvn -pl play-admin -Dtest=WxCustomRandomOrderApiTest test
```
4. **自动数据播种**
激活 `apitest` Profile 时,`ApiTestDataSeeder` 会自动创建默认租户、顾客、店员、商品、礼物、优惠券等基线数据,并在每次启动时重置关键计数,因此多次执行结果一致。如果需要彻底清理,可直接清空数据库后重新运行测试。
按照上述流程,即可可靠地复现订单、优惠券、礼物等核心链路的 API 行为。
## 部署说明
### Docker 构建和推送
@@ -270,4 +307,4 @@ mvn clean install
✅ 模块间配置一致
✅ Spotless 代码格式化已配置
✅ Checkstyle 代码规范检查已配置
✅ VS Code Java 配置已设置
✅ VS Code Java 配置已设置

View File

@@ -1,9 +1,20 @@
version: "3.8"
services:
redis:
image: redis:7-alpine
container_name: peipei-redis
ports:
- "36379:6379"
command: ["redis-server", "--save", "", "--appendonly", "no"]
restart: unless-stopped
networks:
- peipei-network
peipei-backend:
image: docker-registry.julyhaven.com/peipei/backend:latest
container_name: peipei-backend
platform: linux/amd64
depends_on:
- redis
ports:
- "7003:7002"
environment:

View File

@@ -7,13 +7,19 @@ import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.custom.mapper.PlayCustomGiftInfoMapper;
import com.starry.admin.modules.custom.module.entity.PlayCustomGiftInfoEntity;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.mapper.PlayClerkGiftInfoMapper;
import com.starry.admin.modules.shop.module.entity.PlayClerkGiftInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayCommodityAndLevelInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayCommodityInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService;
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
@@ -78,7 +84,11 @@ public class ApiTestDataSeeder implements CommandLineRunner {
private final IPlayCommodityAndLevelInfoService commodityAndLevelInfoService;
private final IPlayGiftInfoService giftInfoService;
private final IPlayClerkCommodityService clerkCommodityService;
private final IPlayClerkGiftInfoService playClerkGiftInfoService;
private final IPlayCustomUserInfoService customUserInfoService;
private final IPlayCustomGiftInfoService playCustomGiftInfoService;
private final PlayClerkGiftInfoMapper playClerkGiftInfoMapper;
private final PlayCustomGiftInfoMapper playCustomGiftInfoMapper;
private final PasswordEncoder passwordEncoder;
private final WxTokenService wxTokenService;
@@ -93,7 +103,11 @@ public class ApiTestDataSeeder implements CommandLineRunner {
IPlayCommodityAndLevelInfoService commodityAndLevelInfoService,
IPlayGiftInfoService giftInfoService,
IPlayClerkCommodityService clerkCommodityService,
IPlayClerkGiftInfoService playClerkGiftInfoService,
IPlayCustomUserInfoService customUserInfoService,
IPlayCustomGiftInfoService playCustomGiftInfoService,
PlayClerkGiftInfoMapper playClerkGiftInfoMapper,
PlayCustomGiftInfoMapper playCustomGiftInfoMapper,
PasswordEncoder passwordEncoder,
WxTokenService wxTokenService) {
this.tenantPackageService = tenantPackageService;
@@ -106,7 +120,11 @@ public class ApiTestDataSeeder implements CommandLineRunner {
this.commodityAndLevelInfoService = commodityAndLevelInfoService;
this.giftInfoService = giftInfoService;
this.clerkCommodityService = clerkCommodityService;
this.playClerkGiftInfoService = playClerkGiftInfoService;
this.customUserInfoService = customUserInfoService;
this.playCustomGiftInfoService = playCustomGiftInfoService;
this.playClerkGiftInfoMapper = playClerkGiftInfoMapper;
this.playCustomGiftInfoMapper = playCustomGiftInfoMapper;
this.passwordEncoder = passwordEncoder;
this.wxTokenService = wxTokenService;
}
@@ -128,6 +146,7 @@ public class ApiTestDataSeeder implements CommandLineRunner {
seedClerk();
seedClerkCommodity();
seedGift();
resetGiftCounters();
seedCustomer();
} finally {
if (Objects.nonNull(originalTenant)) {
@@ -257,10 +276,53 @@ public class ApiTestDataSeeder implements CommandLineRunner {
parent.setSort(1);
commodityInfoService.save(parent);
log.info("Inserted API test commodity parent {}", DEFAULT_COMMODITY_PARENT_ID);
} else {
boolean parentNeedsUpdate = false;
if (!"00".equals(parent.getPId())) {
parent.setPId("00");
parentNeedsUpdate = true;
}
if (!"service-category".equals(parent.getItemType())) {
parent.setItemType("service-category");
parentNeedsUpdate = true;
}
if (!DEFAULT_TENANT_ID.equals(parent.getTenantId())) {
parent.setTenantId(DEFAULT_TENANT_ID);
parentNeedsUpdate = true;
}
if (!"1".equals(parent.getEnableStace())) {
parent.setEnableStace("1");
parentNeedsUpdate = true;
}
if (parentNeedsUpdate) {
commodityInfoService.updateById(parent);
log.info("Normalized API test commodity parent {}", DEFAULT_COMMODITY_PARENT_ID);
}
}
PlayCommodityInfoEntity child = commodityInfoService.getById(DEFAULT_COMMODITY_ID);
if (child != null) {
boolean childNeedsUpdate = false;
if (!DEFAULT_COMMODITY_PARENT_ID.equals(child.getPId())) {
child.setPId(DEFAULT_COMMODITY_PARENT_ID);
childNeedsUpdate = true;
}
if (!"service".equals(child.getItemType())) {
child.setItemType("service");
childNeedsUpdate = true;
}
if (!DEFAULT_TENANT_ID.equals(child.getTenantId())) {
child.setTenantId(DEFAULT_TENANT_ID);
childNeedsUpdate = true;
}
if (!"1".equals(child.getEnableStace())) {
child.setEnableStace("1");
childNeedsUpdate = true;
}
if (childNeedsUpdate) {
commodityInfoService.updateById(child);
log.info("Normalized API test commodity {}", DEFAULT_COMMODITY_ID);
}
log.info("API test commodity {} already exists", DEFAULT_COMMODITY_ID);
return child;
}
@@ -386,6 +448,38 @@ public class ApiTestDataSeeder implements CommandLineRunner {
log.info("Inserted API test gift {}", DEFAULT_GIFT_ID);
}
private void resetGiftCounters() {
int customerReset = playCustomGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CUSTOMER_ID, DEFAULT_GIFT_ID);
if (customerReset == 0) {
PlayCustomGiftInfoEntity entity = new PlayCustomGiftInfoEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setCustomId(DEFAULT_CUSTOMER_ID);
entity.setGiffId(DEFAULT_GIFT_ID);
entity.setGiffNumber(0L);
try {
playCustomGiftInfoService.save(entity);
} catch (org.springframework.dao.DuplicateKeyException duplicateKeyException) {
playCustomGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CUSTOMER_ID, DEFAULT_GIFT_ID);
}
}
int clerkReset = playClerkGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CLERK_ID, DEFAULT_GIFT_ID);
if (clerkReset == 0) {
PlayClerkGiftInfoEntity entity = new PlayClerkGiftInfoEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setClerkId(DEFAULT_CLERK_ID);
entity.setGiffId(DEFAULT_GIFT_ID);
entity.setGiffNumber(0L);
try {
playClerkGiftInfoService.save(entity);
} catch (org.springframework.dao.DuplicateKeyException duplicateKeyException) {
playClerkGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CLERK_ID, DEFAULT_GIFT_ID);
}
}
}
private void seedCustomer() {
PlayCustomUserInfoEntity customer = customUserInfoService.getById(DEFAULT_CUSTOMER_ID);
String token = wxTokenService.createWxUserToken(DEFAULT_CUSTOMER_ID);

View File

@@ -32,8 +32,10 @@ public class GlobalExceptionHandler {
public R handleServiceException(ServiceException e, HttpServletRequest request) {
if ("token异常".equals(e.getMessage()) || "token为空".equals(e.getMessage())) {
log.error("用户token异常");
} else if (log.isDebugEnabled()) {
log.debug("业务异常", e);
} else {
log.error(e.getMessage(), e);
log.warn("业务异常: {}", e.getMessage());
}
Integer code = e.getCode();
return StringUtils.isNotNull(code) ? R.error(code, e.getMessage()) : R.error(e.getMessage());
@@ -111,10 +113,11 @@ public class GlobalExceptionHandler {
public R customException(CustomException e) {
if ("token异常".equals(e.getMessage()) || "token为空".equals(e.getMessage())) {
log.error("用户token异常");
} else if (log.isDebugEnabled()) {
log.debug("业务异常", e);
} else {
log.error(e.getMessage(), e);
log.warn("业务异常: {}", e.getMessage());
}
return R.error(e.getMessage());
}
}

View File

@@ -18,7 +18,7 @@ import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserReviewInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.common.utils.IdUtils;
import java.util.Arrays;
import javax.annotation.Resource;
@@ -45,7 +45,7 @@ public class PlayClerkUserReviewInfoServiceImpl
@Resource
private IPlayClerkCommodityService clerkCommodityService;
@Resource
private WxCustomMpService wxCustomMpService;
private NotificationSender notificationSender;
@Override
public PlayClerkUserReviewInfoEntity queryByClerkId(String clerkId, String reviewState) {
@@ -186,7 +186,7 @@ public class PlayClerkUserReviewInfoServiceImpl
this.update(entity);
// 发送消息
wxCustomMpService.sendCheckMessage(entity, userInfo, vo.getReviewState());
notificationSender.sendCheckMessage(entity, userInfo, vo.getReviewState());
}
/**

View File

@@ -29,4 +29,9 @@ public interface PlayCustomGiftInfoMapper extends MPJBaseMapper<PlayCustomGiftIn
int incrementGiftCount(@Param("customId") String customId, @Param("giftId") String giftId,
@Param("tenantId") String tenantId, @Param("delta") long delta);
@Update("UPDATE play_custom_gift_info SET giff_number = 0, deleted = 0 "
+ "WHERE tenant_id = #{tenantId} AND custom_id = #{customId} AND giff_id = #{giftId}")
int resetGiftCount(@Param("tenantId") String tenantId, @Param("customId") String customId,
@Param("giftId") String giftId);
}

View File

@@ -47,7 +47,7 @@ import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.SecurityUtils;
@@ -85,7 +85,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
private IEarningsService earningsService;
@Resource
private WxCustomMpService wxCustomMpService;
private NotificationSender notificationSender;
@Resource
private IPlayOrderRefundInfoService orderRefundInfoService;
@@ -466,7 +466,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
}
if (shouldNotify) {
wxCustomMpService.sendOrderFinishMessageAsync(latest);
notificationSender.sendOrderFinishMessageAsync(latest);
}
}

View File

@@ -17,7 +17,7 @@ import com.starry.admin.modules.order.module.vo.PlayOrderComplaintQueryVo;
import com.starry.admin.modules.order.module.vo.PlayOrderComplaintReturnVo;
import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.util.Arrays;
@@ -46,7 +46,7 @@ public class PlayOrderComplaintInfoServiceImpl
@Resource
private IPlayPersonnelGroupInfoService playClerkGroupInfoService;
@Resource
private WxCustomMpService wxCustomMpService;
private NotificationSender notificationSender;
/**
* 查询订单投诉信息
@@ -169,7 +169,7 @@ public class PlayOrderComplaintInfoServiceImpl
playOrderComplaintInfo.setClerkId(orderInfo.getAcceptBy());
playOrderComplaintInfo.setCommodityId(orderInfo.getCommodityId());
// 发送投诉消息给管理员以及客服
wxCustomMpService.sendComplaintMessage(playOrderComplaintInfo, orderInfo);
notificationSender.sendComplaintMessage(playOrderComplaintInfo, orderInfo);
return save(playOrderComplaintInfo);
}

View File

@@ -1,5 +1,6 @@
package com.starry.admin.modules.order.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -41,10 +42,11 @@ import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.weichat.entity.order.*;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.DateRangeUtils;
import com.starry.admin.utils.SecurityUtils;
@@ -95,7 +97,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
private IPlayCouponDetailsService playCouponDetailsService;
@Resource
private WxCustomMpService wxCustomMpService;
private NotificationSender notificationSender;
@Resource
private IPlayCustomUserInfoService customUserInfoService;
@@ -644,7 +646,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
orderInfo.setFirstOrder(firstOrderFlag);
log.info("Order accepted successfully. orderId={}, orderNo={}, acceptBy={}, operatorByType={}",
orderId, orderInfo.getOrderNo(), acceptBy, operatorByType);
wxCustomMpService.sendOrderMessageAsync(orderInfo);
notificationSender.sendOrderMessageAsync(orderInfo);
}
private void validateClerkQualificationForRandomOrder(PlayOrderInfoEntity orderInfo,
@@ -835,7 +837,8 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
playOrderRefundInfoService.add(orderInfo.getId(), orderInfo.getPurchaserBy(), orderInfo.getAcceptBy(),
orderInfo.getPayMethod(), OrderRefundRecordType.FULL.getCode(), orderInfo.getFinalAmount(), refundReason, operatorByType, operatorBy,
OrderRefundState.PROCESSING.getCode(), ReviewRequirement.NOT_REQUIRED.getCode());
wxCustomMpService.sendOrderCancelMessageAsync(orderInfo, refundReason);
restoreCouponsForOrder(orderInfo);
notificationSender.sendOrderCancelMessageAsync(orderInfo, refundReason);
}
/**
@@ -888,8 +891,10 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
orderInfo.getPayMethod(), forceCancelRefundType.getCode(), actualRefundAmount, refundReason, operatorByType, operatorBy,
OrderRefundState.PROCESSING.getCode(), ReviewRequirement.NOT_REQUIRED.getCode());
restoreCouponsForOrder(orderInfo);
PlayOrderInfoEntity latest = this.selectOrderInfoById(orderId);
wxCustomMpService.sendOrderCancelMessageAsync(latest, refundReason);
notificationSender.sendOrderCancelMessageAsync(latest, refundReason);
}
@Override
@@ -1038,4 +1043,11 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
return OrderTriggerSource.SYSTEM;
}
}
private void restoreCouponsForOrder(PlayOrderInfoEntity orderInfo) {
if (orderInfo == null || CollectionUtil.isEmpty(orderInfo.getCouponIds())) {
return;
}
playCouponDetailsService.updateCouponUseStateByIds(orderInfo.getCouponIds(), CouponUseState.UNUSED.getCode());
}
}

View File

@@ -29,4 +29,9 @@ public interface PlayClerkGiftInfoMapper extends BaseMapper<PlayClerkGiftInfoEnt
int incrementGiftCount(@Param("clerkId") String clerkId, @Param("giftId") String giftId,
@Param("tenantId") String tenantId, @Param("delta") long delta);
@Update("UPDATE play_clerk_gift_info SET giff_number = 0, deleted = 0 "
+ "WHERE tenant_id = #{tenantId} AND clerk_id = #{clerkId} AND giff_id = #{giftId}")
int resetGiftCount(@Param("tenantId") String tenantId, @Param("clerkId") String clerkId,
@Param("giftId") String giftId);
}

View File

@@ -103,7 +103,11 @@ public class PlayCommodityInfoServiceImpl extends ServiceImpl<PlayCommodityInfoM
@Override
public List<PlayCommodityInfoEntity> selectByType() {
LambdaQueryWrapper<PlayCommodityInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayCommodityInfoEntity::getItemType, "服务类型");
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isNotBlank(tenantId)) {
lambdaQueryWrapper.eq(PlayCommodityInfoEntity::getTenantId, tenantId);
}
lambdaQueryWrapper.eq(PlayCommodityInfoEntity::getPId, "00");
lambdaQueryWrapper.orderByDesc(PlayCommodityInfoEntity::getSort);
return this.baseMapper.selectList(lambdaQueryWrapper);
}

View File

@@ -3,6 +3,7 @@ package com.starry.admin.modules.shop.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -158,12 +159,12 @@ public class PlayCouponDetailsServiceImpl extends ServiceImpl<PlayCouponDetailsM
@Override
public void updateCouponUseStateByIds(List<String> ids, String useState) {
LocalDateTime useTime = CouponUseState.USED.getCode().equals(useState) ? LocalDateTime.now() : null;
for (String id : ids) {
PlayCouponDetailsEntity entity = new PlayCouponDetailsEntity();
entity.setId(id);
entity.setUseState(useState);
entity.setUseTime(LocalDateTime.now());
baseMapper.updateById(entity);
baseMapper.update(null, com.baomidou.mybatisplus.core.toolkit.Wrappers.<PlayCouponDetailsEntity>lambdaUpdate()
.eq(PlayCouponDetailsEntity::getId, id)
.set(PlayCouponDetailsEntity::getUseState, useState)
.set(PlayCouponDetailsEntity::getUseTime, useTime));
}
}

View File

@@ -0,0 +1,45 @@
package com.starry.admin.modules.weichat.service;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Profile("!apitest")
public class DefaultNotificationSender implements NotificationSender {
@Resource
private WxCustomMpService wxCustomMpService;
@Override
public void sendOrderMessageAsync(PlayOrderInfoEntity orderInfo) {
wxCustomMpService.sendOrderMessageAsync(orderInfo);
}
@Override
public void sendOrderFinishMessageAsync(PlayOrderInfoEntity order) {
wxCustomMpService.sendOrderFinishMessageAsync(order);
}
@Override
public void sendOrderCancelMessageAsync(PlayOrderInfoEntity orderInfo, String refundReason) {
wxCustomMpService.sendOrderCancelMessageAsync(orderInfo, refundReason);
}
@Override
public void sendComplaintMessage(PlayOrderComplaintInfoEntity info, PlayOrderInfoEntity orderInfo) {
wxCustomMpService.sendComplaintMessage(info, orderInfo);
}
@Override
public void sendCheckMessage(PlayClerkUserReviewInfoEntity entity, PlayClerkUserInfoEntity userInfo,
String reviewState) {
wxCustomMpService.sendCheckMessage(entity, userInfo, reviewState);
}
}

View File

@@ -0,0 +1,43 @@
package com.starry.admin.modules.weichat.service;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Slf4j
@Primary
@Component
@Profile("apitest")
public class MockNotificationSender implements NotificationSender {
@Override
public void sendOrderMessageAsync(PlayOrderInfoEntity orderInfo) {
log.debug("[wechat-mock] skip sendOrderMessageAsync orderId={}", orderInfo.getId());
}
@Override
public void sendOrderFinishMessageAsync(PlayOrderInfoEntity order) {
log.debug("[wechat-mock] skip sendOrderFinishMessageAsync orderId={}", order.getId());
}
@Override
public void sendOrderCancelMessageAsync(PlayOrderInfoEntity orderInfo, String refundReason) {
log.debug("[wechat-mock] skip sendOrderCancelMessageAsync orderId={}", orderInfo.getId());
}
@Override
public void sendComplaintMessage(PlayOrderComplaintInfoEntity info, PlayOrderInfoEntity orderInfo) {
log.debug("[wechat-mock] skip sendComplaintMessage orderId={}", orderInfo.getId());
}
@Override
public void sendCheckMessage(PlayClerkUserReviewInfoEntity entity, PlayClerkUserInfoEntity userInfo,
String reviewState) {
log.debug("[wechat-mock] skip sendCheckMessage clerkId={}", userInfo == null ? null : userInfo.getId());
}
}

View File

@@ -0,0 +1,22 @@
package com.starry.admin.modules.weichat.service;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
/**
* 抽象出发送微信通知的接口,便于在测试环境下替换为 Mock 实现。
*/
public interface NotificationSender {
void sendOrderMessageAsync(PlayOrderInfoEntity orderInfo);
void sendOrderFinishMessageAsync(PlayOrderInfoEntity order);
void sendOrderCancelMessageAsync(PlayOrderInfoEntity orderInfo, String refundReason);
void sendComplaintMessage(PlayOrderComplaintInfoEntity info, PlayOrderInfoEntity orderInfo);
void sendCheckMessage(PlayClerkUserReviewInfoEntity entity, PlayClerkUserInfoEntity userInfo, String reviewState);
}

View File

@@ -37,6 +37,7 @@ import me.chanjar.weixin.mp.bean.template.WxMpTemplateData;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
@@ -61,6 +62,7 @@ public class WxCustomMpService {
@Resource
private ThreadPoolTaskExecutor executor;
/**
* 支付成功回调地址
*/

View File

@@ -53,6 +53,7 @@ logging:
level:
root: info
com.starry: debug
business-error-as-warn: true
jwt:
tokenHeader: X-Test-Token

View File

@@ -210,6 +210,7 @@ class PlayCommodityInfoApiTest extends WxCustomOrderApiTestSupport {
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
ensureTenantContext();
PlayCommodityAndLevelInfoEntity pricing = commodityAndLevelInfoService.lambdaQuery()
.eq(PlayCommodityAndLevelInfoEntity::getCommodityId, child.getId())
.eq(PlayCommodityAndLevelInfoEntity::getLevelId, ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)
@@ -266,6 +267,7 @@ class PlayCommodityInfoApiTest extends WxCustomOrderApiTestSupport {
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
ensureTenantContext();
PlayCommodityInfoEntity updated = commodityInfoService.getById(child.getId());
assertThat(updated.getAutomaticSettlementDuration())
.isEqualTo(AutomaticSettlementPolicy.TEN_MINUTES.getSeconds());

View File

@@ -112,6 +112,7 @@ class WxCustomGiftOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(clerkGift).isNotNull();
Assertions.assertThat(clerkGift.getGiffNumber()).isEqualTo((long) giftQuantity);
ensureTenantContext();
BigDecimal finalBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.getAccountBalance();
Assertions.assertThat(finalBalance).isEqualByComparingTo(initialBalance.subtract(totalAmount));

View File

@@ -20,6 +20,7 @@ import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.enums.EarningsType;
@@ -38,6 +39,9 @@ import org.springframework.test.web.servlet.MvcResult;
class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
@MockBean
private NotificationSender notificationSender;
@MockBean
private WxCustomMpService wxCustomMpService;
@@ -158,6 +162,11 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.one();
Assertions.assertThat(order).isNotNull();
PlayCouponDetailsEntity detailBeforeCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeCancel).isNotNull();
Assertions.assertThat(detailBeforeCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeCancel.getUseTime()).isNotNull();
String cancelPayload = "{" +
"\"orderId\":\"" + order.getId() + "\"," +
"\"refundReason\":\"测试取消\"," +
@@ -182,6 +191,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(detail.getUseState())
.as("取消订单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally {
CustomSecurityContextHolder.remove();
}
@@ -194,7 +204,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
String remark = "API random force cancel " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("12.00");
try {
reset(wxCustomMpService);
reset(notificationSender);
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
@@ -249,6 +259,12 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
ensureTenantContext();
PlayCouponDetailsEntity detailBeforeForceCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeForceCancel).isNotNull();
Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeForceCancel.getUseTime()).isNotNull();
ensureTenantContext();
orderInfoService.forceCancelOngoingOrder(
"2",
@@ -267,6 +283,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(detail.getUseState())
.as("强制取消订单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally {
CustomSecurityContextHolder.remove();
}
@@ -280,7 +297,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
String remark = "API random coupon " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("20.00");
try {
reset(wxCustomMpService);
reset(notificationSender);
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
@@ -334,15 +351,13 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
verify(wxCustomMpService).sendCreateOrderMessageBatch(
anyList(),
eq(order.getOrderNo()),
eq(expectedNet.toString()),
eq(order.getCommodityName()),
eq(order.getId()));
String orderId = order.getId();
PlayCouponDetailsEntity detailAfterOrderPlaced = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterOrderPlaced).isNotNull();
Assertions.assertThat(detailAfterOrderPlaced.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterOrderPlaced.getUseTime()).isNotNull();
mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
@@ -352,7 +367,8 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderMessageAsync(argThat(o -> orderId.equals(o.getId())));
verify(notificationSender).sendOrderMessageAsync(argThat(o -> o.getId().equals(orderId)));
mockMvc.perform(get("/wx/clerk/order/start")
.param("id", orderId)
@@ -372,7 +388,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId())));
verify(notificationSender).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId())));
ensureTenantContext();
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
@@ -397,6 +413,11 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(expectedRevenue);
assertCouponUsed(couponDetailId);
PlayCouponDetailsEntity detailAfterLifecycle = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterLifecycle).isNotNull();
Assertions.assertThat(detailAfterLifecycle.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterLifecycle.getUseTime()).isNotNull();
} finally {
CustomSecurityContextHolder.remove();
}
@@ -421,7 +442,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
String orderId = placeRandomOrder(remark, customerToken);
reset(wxCustomMpService);
reset(notificationSender);
ensureTenantContext();
mockMvc.perform(get("/wx/clerk/order/accept")
@@ -433,9 +454,9 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));
verify(notificationSender).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));
reset(wxCustomMpService);
reset(notificationSender);
ensureTenantContext();
mockMvc.perform(get("/wx/clerk/order/start")
@@ -463,7 +484,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId)));
verify(notificationSender).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId)));
ensureTenantContext();
long earningsAfter = earningsService.lambdaQuery()

View File

@@ -86,6 +86,16 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
.one();
Assertions.assertThat(order).isNotNull();
PlayCouponDetailsEntity detailBeforeForceCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeForceCancel).isNotNull();
Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeForceCancel.getUseTime()).isNotNull();
PlayCouponDetailsEntity detailBeforeCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeCancel).isNotNull();
Assertions.assertThat(detailBeforeCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeCancel.getUseTime()).isNotNull();
String cancelPayload = "{" +
"\"orderId\":\"" + order.getId() + "\"," +
"\"refundReason\":\"测试取消\"," +
@@ -106,9 +116,11 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detail).isNotNull();
Assertions.assertThat(detail.getUseState())
.as("取消指定单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally {
CustomSecurityContextHolder.remove();
}
@@ -172,6 +184,11 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
PlayCouponDetailsEntity detailAfterOrder = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterOrder).isNotNull();
Assertions.assertThat(detailAfterOrder.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterOrder.getUseTime()).isNotNull();
verify(wxCustomMpService).sendCreateOrderMessage(
eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
@@ -194,6 +211,10 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(order.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);
assertCouponUsed(couponDetailId);
PlayCouponDetailsEntity detailAfterComplete = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterComplete).isNotNull();
Assertions.assertThat(detailAfterComplete.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterComplete.getUseTime()).isNotNull();
} finally {
CustomSecurityContextHolder.remove();
}
@@ -273,9 +294,11 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detail).isNotNull();
Assertions.assertThat(detail.getUseState())
.as("强制取消指定单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally {
CustomSecurityContextHolder.remove();
}

View File

@@ -50,7 +50,7 @@ import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import java.math.BigDecimal;
@@ -81,7 +81,7 @@ class OrderLifecycleServiceImplTest {
private IEarningsService earningsService;
@Mock
private WxCustomMpService wxCustomMpService;
private NotificationSender notificationSender;
@Mock
private IPlayOrderRefundInfoService orderRefundInfoService;
@@ -565,7 +565,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
lifecycleService.placeOrder(command(context, null, false, null, null));
verify(orderInfoMapper, never()).selectById(anyString());
verify(wxCustomMpService, never()).sendOrderFinishMessageAsync(any());
verify(notificationSender, never()).sendOrderFinishMessageAsync(any());
verify(earningsService, never()).createFromOrder(any());
}
@@ -594,7 +594,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
verify(orderInfoMapper, atLeastOnce()).selectById(anyString());
verify(customUserInfoService).handleOrderCompletion(any());
verify(earningsService).createFromOrder(completed);
verify(wxCustomMpService).sendOrderFinishMessageAsync(completed);
verify(notificationSender).sendOrderFinishMessageAsync(completed);
}
@Test
@@ -610,7 +610,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
lifecycleService.placeOrder(command(context, null, false, null, null));
verify(orderInfoMapper, never()).selectById(anyString());
verify(wxCustomMpService, never()).sendOrderFinishMessageAsync(any());
verify(notificationSender, never()).sendOrderFinishMessageAsync(any());
verify(earningsService, never()).createFromOrder(any());
}
@@ -639,7 +639,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
verify(orderInfoMapper, atLeastOnce()).selectById(anyString());
verify(customUserInfoService).handleOrderCompletion(any());
verify(earningsService).createFromOrder(completed);
verify(wxCustomMpService).sendOrderFinishMessageAsync(completed);
verify(notificationSender).sendOrderFinishMessageAsync(completed);
}
@Test
@@ -1031,7 +1031,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
verify(orderInfoMapper).update(isNull(), any());
verify(customUserInfoService).handleOrderCompletion(completed);
verify(earningsService).createFromOrder(completed);
verify(wxCustomMpService).sendOrderFinishMessageAsync(completed);
verify(notificationSender).sendOrderFinishMessageAsync(completed);
}
@Test
@@ -1052,7 +1052,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
OrderTriggerSource.ADMIN_CONSOLE));
verify(earningsService, never()).createFromOrder(any());
verify(wxCustomMpService, never()).sendOrderFinishMessageAsync(any());
verify(notificationSender, never()).sendOrderFinishMessageAsync(any());
}
@Test
@@ -1074,7 +1074,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
verify(customUserInfoService, never()).handleOrderCompletion(any());
verify(earningsService, never()).createFromOrder(any());
verify(wxCustomMpService, never()).sendOrderFinishMessageAsync(any());
verify(notificationSender, never()).sendOrderFinishMessageAsync(any());
verify(orderLogInfoMapper, never()).insert(any());
}

View File

@@ -40,6 +40,9 @@ public class ThreadPoolConfig {
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix("async-pool-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;

View File

@@ -8,6 +8,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@@ -25,6 +26,9 @@ public class RequestLoggingInterceptor implements HandlerInterceptor {
private static final String START_TIME_ATTRIBUTE = "startTime";
public static final String BUSINESS_RESULT_ATTRIBUTE = "requestLoggingBusinessResult";
@Value("${logging.business-error-as-warn:false}")
private boolean businessErrorAsWarn;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
@@ -208,8 +212,13 @@ public class RequestLoggingInterceptor implements HandlerInterceptor {
long duration, BusinessResult businessResult) {
String template = "Request completed: {} {} - {} {} ({}ms) businessCode={} success={} message={}";
if (isBusinessError(businessResult)) {
log.error(template, method, uri, status, statusText, duration,
businessResult.getCode(), businessResult.isSuccess(), businessResult.getMessage());
if (businessErrorAsWarn) {
log.warn(template, method, uri, status, statusText, duration,
businessResult.getCode(), businessResult.isSuccess(), businessResult.getMessage());
} else {
log.error(template, method, uri, status, statusText, duration,
businessResult.getCode(), businessResult.isSuccess(), businessResult.getMessage());
}
} else if (isBusinessWarn(businessResult)) {
log.warn(template, method, uri, status, statusText, duration,
businessResult.getCode(), businessResult.isSuccess(), businessResult.getMessage());