Compare commits

...

4 Commits

Author SHA1 Message Date
irving
eaee5f5aa6 Merge branch 'feat/earnling-line-status'
Some checks failed
Build and Push Backend / docker (push) Failing after 5s
2025-11-07 23:42:43 -05:00
irving
51ec9dd85b 完善后台订单筛选及接口测试 2025-11-07 23:42:15 -05:00
irving
9868fb1bb9 feat: 新增提现审计接口与保障用例 2025-11-07 23:41:39 -05:00
irving
3df1267272 adjust api test mysql version 2025-11-07 23:04:27 -05:00
6 changed files with 771 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
version: "3.9"
services:
mysql-apitest:
image: mysql:8.0.24
container_name: peipei-mysql-apitest
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: peipei_apitest
MYSQL_USER: apitest
MYSQL_PASSWORD: apitest
ports:
- "33306:3306"
command:
- "--default-authentication-plugin=mysql_native_password"
- "--lower_case_table_names=1"
- "--explicit_defaults_for_timestamp=1"
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_general_ci"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
interval: 10s
timeout: 5s
retries: 10

View File

@@ -54,6 +54,12 @@ public class PlayOrderInfoQueryVo extends BasePageEntity {
@ApiModelProperty(value = "是否首单", example = "1", notes = "0不是1") @ApiModelProperty(value = "是否首单", example = "1", notes = "0不是1")
private String firstOrder; private String firstOrder;
/**
* 随机单要求-店员性别0:未知;1:男;2:女)
*/
@ApiModelProperty(value = "店员性别", example = "2", notes = "0:未知;1:男;2:女")
private String sex;
/** /**
* 是否使用优惠券[0:未使用,1:已使用] * 是否使用优惠券[0:未使用,1:已使用]
*/ */

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -421,6 +422,25 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
if (StringUtils.isNotBlank(vo.getOrderType())) { if (StringUtils.isNotBlank(vo.getOrderType())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderType, vo.getOrderType()); lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderType, vo.getOrderType());
} }
if (StringUtils.isNotBlank(vo.getSex())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getSex, vo.getSex());
}
if (StringUtils.isNotBlank(vo.getPayMethod())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getPayMethod, vo.getPayMethod());
}
if (StringUtils.isNotBlank(vo.getUseCoupon())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getUseCoupon, vo.getUseCoupon());
}
if (StringUtils.isNotBlank(vo.getBackendEntry())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getBackendEntry, vo.getBackendEntry());
}
if (StringUtils.isNotBlank(vo.getFirstOrder())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getFirstOrder, vo.getFirstOrder());
}
applyRangeFilter(lambdaQueryWrapper, vo.getPurchaserTime(), PlayOrderInfoEntity::getPurchaserTime);
applyRangeFilter(lambdaQueryWrapper, vo.getAcceptTime(), PlayOrderInfoEntity::getAcceptTime);
applyRangeFilter(lambdaQueryWrapper, vo.getStartOrderTime(), PlayOrderInfoEntity::getOrderStartTime);
applyRangeFilter(lambdaQueryWrapper, vo.getEndOrderTime(), PlayOrderInfoEntity::getOrderEndTime);
// 加入组员的筛选要么acceptBy为空要么就在in里面 // 加入组员的筛选要么acceptBy为空要么就在in里面
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(),
vo.getClerkNickName()); vo.getClerkNickName());
@@ -728,6 +748,33 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
} }
private void applyRangeFilter(MPJLambdaWrapper<PlayOrderInfoEntity> wrapper, List<String> range,
SFunction<PlayOrderInfoEntity, ?> column) {
if (CollectionUtil.isEmpty(range)) {
return;
}
String start = normalizeRangeValue(range, 0, false);
String end = normalizeRangeValue(range, 1, true);
if (StrUtil.isNotBlank(start) && StrUtil.isNotBlank(end)) {
wrapper.between(column, start, end);
} else if (StrUtil.isNotBlank(start)) {
wrapper.ge(column, start);
} else if (StrUtil.isNotBlank(end)) {
wrapper.le(column, end);
}
}
private String normalizeRangeValue(List<String> range, int index, boolean isEnd) {
if (range.size() <= index) {
return null;
}
String raw = range.get(index);
if (StrUtil.isBlank(raw)) {
return null;
}
return isEnd ? DateRangeUtils.normalizeEndOptional(raw) : DateRangeUtils.normalizeStartOptional(raw);
}
public void updateStateTo23(String operatorByType, String operatorBy, String orderState, String orderId) { public void updateStateTo23(String operatorByType, String operatorBy, String orderState, String orderId) {
OperatorType operatorType = resolveOperatorTypeOrThrow(operatorByType); OperatorType operatorType = resolveOperatorTypeOrThrow(operatorByType);
boolean isCustomer = operatorType == OperatorType.CUSTOMER; boolean isCustomer = operatorType == OperatorType.CUSTOMER;

View File

@@ -3,6 +3,9 @@ package com.starry.admin.modules.withdraw.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity; import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity; import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
@@ -13,6 +16,7 @@ import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.modules.withdraw.service.ITenantAlipayConfigService; import com.starry.admin.modules.withdraw.service.ITenantAlipayConfigService;
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService; import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
import com.starry.admin.modules.withdraw.service.IWithdrawalService; import com.starry.admin.modules.withdraw.service.IWithdrawalService;
import com.starry.admin.modules.withdraw.vo.ClerkEarningLineVo;
import com.starry.admin.modules.withdraw.vo.EarningsAdminQueryVo; import com.starry.admin.modules.withdraw.vo.EarningsAdminQueryVo;
import com.starry.admin.modules.withdraw.vo.EarningsAdminSummaryVo; import com.starry.admin.modules.withdraw.vo.EarningsAdminSummaryVo;
import com.starry.admin.modules.withdraw.vo.EarningsBackfillRequest; import com.starry.admin.modules.withdraw.vo.EarningsBackfillRequest;
@@ -24,11 +28,15 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -49,6 +57,8 @@ public class AdminWithdrawalController {
private IEarningsBackfillService earningsBackfillService; private IEarningsBackfillService earningsBackfillService;
@Resource @Resource
private IEarningsBackfillLogService backfillLogService; private IEarningsBackfillLogService backfillLogService;
@Resource
private IPlayOrderInfoService orderInfoService;
@ApiOperation("分页查询提现请求") @ApiOperation("分页查询提现请求")
@PostMapping("/requests/listByPage") @PostMapping("/requests/listByPage")
@@ -72,6 +82,62 @@ public class AdminWithdrawalController {
return TypedR.ok(list); return TypedR.ok(list);
} }
@ApiOperation("提现请求审计")
@GetMapping("/requests/{id}/audit")
public TypedR<List<ClerkEarningLineVo>> getRequestAudit(@PathVariable("id") String id) {
String tenantId = SecurityUtils.getTenantId();
WithdrawalRequestEntity request = withdrawalService.getById(id);
if (request == null || !tenantId.equals(request.getTenantId())) {
throw new CustomException("提现申请不存在或无权查看");
}
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getTenantId, tenantId)
.eq(EarningsLineEntity::getWithdrawalId, id)
.orderByAsc(EarningsLineEntity::getCreatedTime)
.list();
if (lines.isEmpty()) {
return TypedR.ok(Collections.emptyList());
}
List<String> orderIds = lines.stream()
.map(EarningsLineEntity::getOrderId)
.filter(orderId -> orderId != null && !orderId.isEmpty())
.distinct()
.collect(Collectors.toList());
Map<String, PlayOrderInfoEntity> orderMap = orderIds.isEmpty()
? Collections.emptyMap()
: orderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getTenantId, tenantId)
.in(PlayOrderInfoEntity::getId, orderIds)
.list()
.stream()
.collect(Collectors.toMap(PlayOrderInfoEntity::getId, it -> it));
List<ClerkEarningLineVo> vos = new ArrayList<>(lines.size());
for (EarningsLineEntity line : lines) {
ClerkEarningLineVo vo = new ClerkEarningLineVo();
vo.setId(line.getId());
vo.setAmount(line.getAmount());
vo.setStatus(line.getStatus());
vo.setEarningType(line.getEarningType());
vo.setWithdrawalId(line.getWithdrawalId());
vo.setUnlockTime(line.getUnlockTime());
vo.setCreatedTime(toLocalDateTime(line.getCreatedTime()));
vo.setOrderId(line.getOrderId());
if (line.getOrderId() != null) {
PlayOrderInfoEntity order = orderMap.get(line.getOrderId());
if (order != null) {
vo.setOrderNo(order.getOrderNo());
vo.setOrderStatus(order.getOrderStatus());
vo.setOrderEndTime(toLocalDateTime(order.getOrderEndTime()));
}
}
vos.add(vo);
}
return TypedR.ok(vos);
}
@ApiOperation("分页查询收益明细") @ApiOperation("分页查询收益明细")
@PostMapping("/earnings/listByPage") @PostMapping("/earnings/listByPage")
public TypedR<List<EarningsLineEntity>> listEarnings(@RequestBody EarningsAdminQueryVo vo) { public TypedR<List<EarningsLineEntity>> listEarnings(@RequestBody EarningsAdminQueryVo vo) {
@@ -182,4 +248,17 @@ public class AdminWithdrawalController {
q.orderByDesc(EarningsLineEntity::getCreatedTime); q.orderByDesc(EarningsLineEntity::getCreatedTime);
return q; return q;
} }
private LocalDateTime toLocalDateTime(Object value) {
if (value == null) {
return null;
}
if (value instanceof LocalDateTime) {
return (LocalDateTime) value;
}
if (value instanceof Date) {
return ((Date) value).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
return null;
}
} }

View File

@@ -0,0 +1,292 @@
package com.starry.admin.api;
import static org.hamcrest.Matchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
import com.starry.admin.modules.withdraw.enums.EarningsType;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MvcResult;
class AdminWithdrawalControllerApiTest extends AbstractApiTest {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private IEarningsService earningsService;
@Autowired
private IWithdrawalService withdrawalService;
@Autowired
private IPlayOrderInfoService orderInfoService;
private final ObjectMapper objectMapper = new ObjectMapper();
private final List<String> earningsToCleanup = new ArrayList<>();
private final List<String> withdrawalsToCleanup = new ArrayList<>();
private final List<String> ordersToCleanup = new ArrayList<>();
@AfterEach
void tearDown() {
if (!earningsToCleanup.isEmpty()) {
earningsService.removeByIds(earningsToCleanup);
earningsToCleanup.clear();
}
if (!withdrawalsToCleanup.isEmpty()) {
withdrawalService.removeByIds(withdrawalsToCleanup);
withdrawalsToCleanup.clear();
}
if (!ordersToCleanup.isEmpty()) {
orderInfoService.removeByIds(ordersToCleanup);
ordersToCleanup.clear();
}
}
@Test
void auditReturnsEarningLinesWithOrderDetails() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(2));
WithdrawalRequestEntity withdrawal = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("88.60"));
LocalDateTime now = LocalDateTime.now();
seedEarningLine(withdrawal.getId(), order.getId(), new BigDecimal("50.30"), "withdrawn", now.minusMinutes(30), EarningsType.ORDER);
seedEarningLine(withdrawal.getId(), null, new BigDecimal("38.30"), "withdrawing", now.minusMinutes(10), EarningsType.ORDER);
MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + withdrawal.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(2))
.andReturn();
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");
boolean foundOrder = false;
boolean foundMissing = false;
for (JsonNode node : data) {
String orderNo = node.path("orderNo").isMissingNode() ? null : node.path("orderNo").asText(null);
if (order.getOrderNo().equals(orderNo)) {
Assertions.assertThat(node.path("orderStatus").asText()).isEqualTo(order.getOrderStatus());
Assertions.assertThat(node.path("earningType").asText()).isEqualTo(EarningsType.ORDER.name());
foundOrder = true;
}
if (node.path("orderNo").isNull()) {
foundMissing = true;
}
}
Assertions.assertThat(foundOrder).isTrue();
Assertions.assertThat(foundMissing).isTrue();
}
@Test
void auditRejectsMissingRequest() throws Exception {
ensureTenantContext();
mockMvc.perform(get("/admin/withdraw/requests/missing-request/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(500))
.andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
}
@Test
void auditRejectsCrossTenantAccess() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity outsider = seedWithdrawal("tenant-other", new BigDecimal("30.00"));
mockMvc.perform(get("/admin/withdraw/requests/" + outsider.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(500))
.andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
}
@Test
void auditReturnsEmptyListWhenNoEarningsFound() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("22.00"));
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.length()").value(0));
}
@Test
void auditHandlesMissingOrderRecordsGracefully() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("40.00"));
String orphanOrderId = "order-orphan-" + IdUtils.getUuid();
seedEarningLine(request.getId(), orphanOrderId, new BigDecimal("15.00"), "withdrawn", LocalDateTime.now().minusMinutes(5), EarningsType.ORDER);
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data[0].orderNo").value(nullValue()))
.andExpect(jsonPath("$.data[0].orderId").value(orphanOrderId));
}
@Test
void auditReturnsLinesSortedByCreatedTime() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(1));
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("90.00"));
LocalDateTime now = LocalDateTime.now();
String firstId = seedEarningLine(request.getId(), order.getId(), new BigDecimal("10"), "withdrawn", now.minusMinutes(20), EarningsType.ORDER);
Thread.sleep(5L);
String secondId = seedEarningLine(request.getId(), null, new BigDecimal("20"), "withdrawn", now.minusMinutes(10), EarningsType.ORDER);
Thread.sleep(5L);
String thirdId = seedEarningLine(request.getId(), null, new BigDecimal("30"), "withdrawn", now.minusMinutes(5), EarningsType.ORDER);
MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.length()").value(3))
.andReturn();
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");
List<String> ids = new ArrayList<>();
LocalDateTime previous = null;
for (JsonNode node : data) {
ids.add(node.get("id").asText());
String createdText = node.path("createdTime").asText();
if (createdText != null && !createdText.isEmpty()) {
LocalDateTime created = LocalDateTime.parse(createdText, DATE_TIME_FORMATTER);
if (previous != null) {
Assertions.assertThat(created.isBefore(previous)).isFalse();
}
previous = created;
}
}
Assertions.assertThat(ids).containsExactlyInAnyOrder(firstId, secondId, thirdId);
}
@Test
void auditSupportsCommissionEarnings() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("55.00"));
seedEarningLine(request.getId(), null, new BigDecimal("55.00"), "withdrawn", LocalDateTime.now().minusMinutes(2), EarningsType.COMMISSION);
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data[0].earningType").value(EarningsType.COMMISSION.name()));
}
@SuppressWarnings("deprecation")
private PlayOrderInfoEntity seedOrder(LocalDateTime endTime) {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
String id = "order-audit-" + IdUtils.getUuid();
order.setId(id);
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
order.setOrderNo("ORD-" + System.currentTimeMillis());
order.setOrderStatus("3");
order.setOrderType("2");
order.setPlaceType("0");
order.setRewardType("0");
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
order.setOrderMoney(new BigDecimal("120.50"));
order.setFinalAmount(order.getOrderMoney());
order.setEstimatedRevenue(new BigDecimal("80.25"));
order.setOrderSettlementState("1");
order.setOrderEndTime(endTime);
order.setOrderSettlementTime(endTime);
Date nowDate = toDate(LocalDateTime.now());
order.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
order.setCreatedTime(nowDate);
order.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
order.setUpdatedTime(nowDate);
order.setDeleted(false);
orderInfoService.save(order);
ordersToCleanup.add(id);
return order;
}
private WithdrawalRequestEntity seedWithdrawal(String tenantId, BigDecimal amount) {
WithdrawalRequestEntity entity = new WithdrawalRequestEntity();
String id = "withdraw-audit-" + IdUtils.getUuid();
entity.setId(id);
entity.setTenantId(tenantId);
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
entity.setAmount(amount);
entity.setFee(BigDecimal.ZERO);
entity.setNetAmount(amount);
entity.setDestAccount("alipay:audit@test.com");
entity.setStatus("processing");
entity.setPayeeSnapshot("{\"displayName\":\"审计专用\"}");
Date nowDate = toDate(LocalDateTime.now());
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setCreatedTime(nowDate);
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setUpdatedTime(nowDate);
withdrawalService.save(entity);
withdrawalsToCleanup.add(id);
return entity;
}
private String seedEarningLine(String withdrawalId, String orderId, BigDecimal amount, String status, LocalDateTime createdAt, EarningsType earningType) {
EarningsLineEntity entity = new EarningsLineEntity();
String id = "earn-audit-" + IdUtils.getUuid();
entity.setId(id);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
String resolvedOrderId = orderId != null ? orderId : "order-missing-" + IdUtils.getUuid();
entity.setOrderId(resolvedOrderId);
entity.setAmount(amount);
entity.setStatus(status);
entity.setEarningType(earningType);
entity.setUnlockTime(createdAt.minusHours(1));
entity.setWithdrawalId(withdrawalId);
Date createdDate = toDate(createdAt);
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setCreatedTime(createdDate);
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setUpdatedTime(createdDate);
entity.setDeleted(false);
earningsService.save(entity);
earningsToCleanup.add(id);
return id;
}
private void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
private Date toDate(LocalDateTime value) {
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
}
}

View File

@@ -0,0 +1,323 @@
package com.starry.admin.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class PlayOrderInfoControllerApiTest extends AbstractApiTest {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final BigDecimal DEFAULT_AMOUNT = new BigDecimal("188.00");
@Autowired
private IPlayOrderInfoService orderInfoService;
private final ObjectMapper objectMapper = new ObjectMapper();
private final List<String> orderIdsToCleanup = new ArrayList<>();
@AfterEach
void tearDown() {
ensureTenantContext();
if (!orderIdsToCleanup.isEmpty()) {
orderInfoService.removeByIds(orderIdsToCleanup);
orderIdsToCleanup.clear();
}
CustomSecurityContextHolder.remove();
}
@Test
void listByPage_honorsAllSupportedFilters() throws Exception {
ensureTenantContext();
String marker = ("FT" + IdUtils.getUuid().replace("-", "").substring(0, 6)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(6).withNano(0);
PlayOrderInfoEntity matching = persistOrder(marker, "match", reference, order -> {
order.setOrderStatus("3");
order.setPlaceType("2");
order.setPayMethod("2");
order.setUseCoupon("1");
order.setBackendEntry("1");
order.setFirstOrder("0");
order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
order.setSex("2");
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
});
persistOrder(marker, "noise", reference.minusDays(3), order -> {
order.setOrderStatus("0");
order.setPlaceType("0");
order.setPayMethod("0");
order.setUseCoupon("0");
order.setBackendEntry("0");
order.setFirstOrder("1");
order.setGroupId(marker + "-grp");
order.setSex("1");
order.setAcceptBy(null);
order.setPurchaserBy("customer-" + marker);
});
// orderNo filter (exact)
ObjectNode orderNoPayload = baseQuery();
orderNoPayload.put("orderNo", matching.getOrderNo());
assertFilterMatches(orderNoPayload, matching.getId());
// acceptBy filter
ObjectNode acceptPayload = queryWithMarker(marker);
acceptPayload.put("acceptBy", matching.getAcceptBy());
assertFilterMatches(acceptPayload, matching.getId());
// purchaserBy filter
ObjectNode purchaserPayload = queryWithMarker(marker);
purchaserPayload.put("purchaserBy", matching.getPurchaserBy());
assertFilterMatches(purchaserPayload, matching.getId());
// orderStatus filter
ObjectNode statusPayload = queryWithMarker(marker);
statusPayload.put("orderStatus", matching.getOrderStatus());
assertFilterMatches(statusPayload, matching.getId());
// placeType filter
ObjectNode placePayload = queryWithMarker(marker);
placePayload.put("placeType", matching.getPlaceType());
assertFilterMatches(placePayload, matching.getId());
// payMethod filter
ObjectNode payPayload = queryWithMarker(marker);
payPayload.put("payMethod", matching.getPayMethod());
assertFilterMatches(payPayload, matching.getId());
// useCoupon filter
ObjectNode couponPayload = queryWithMarker(marker);
couponPayload.put("useCoupon", matching.getUseCoupon());
assertFilterMatches(couponPayload, matching.getId());
// backendEntry filter
ObjectNode backendPayload = queryWithMarker(marker);
backendPayload.put("backendEntry", matching.getBackendEntry());
assertFilterMatches(backendPayload, matching.getId());
// firstOrder filter
ObjectNode firstOrderPayload = queryWithMarker(marker);
firstOrderPayload.put("firstOrder", matching.getFirstOrder());
assertFilterMatches(firstOrderPayload, matching.getId());
// groupId filter
ObjectNode groupPayload = queryWithMarker(marker);
groupPayload.put("groupId", matching.getGroupId());
assertFilterMatches(groupPayload, matching.getId());
// sex filter
ObjectNode sexPayload = queryWithMarker(marker);
sexPayload.put("sex", matching.getSex());
assertFilterMatches(sexPayload, matching.getId());
// purchaserTime range filter
ObjectNode purchaserTimePayload = queryWithMarker(marker);
purchaserTimePayload.set("purchaserTime", range(
reference.minusMinutes(30),
reference.plusMinutes(30)));
assertFilterMatches(purchaserTimePayload, matching.getId());
// acceptTime range filter
ObjectNode acceptTimePayload = queryWithMarker(marker);
acceptTimePayload.set("acceptTime", range(
matching.getAcceptTime().minusMinutes(15),
matching.getAcceptTime().plusMinutes(15)));
assertFilterMatches(acceptTimePayload, matching.getId());
// endOrderTime range filter
ObjectNode endTimePayload = queryWithMarker(marker);
endTimePayload.set("endOrderTime", range(
matching.getOrderEndTime().minusMinutes(15),
matching.getOrderEndTime().plusMinutes(15)));
assertFilterMatches(endTimePayload, matching.getId());
// Combined filters to verify logical AND behaviour
ObjectNode combinedPayload = queryWithMarker(marker);
combinedPayload.put("acceptBy", matching.getAcceptBy());
combinedPayload.put("purchaserBy", matching.getPurchaserBy());
combinedPayload.put("orderStatus", matching.getOrderStatus());
combinedPayload.put("placeType", matching.getPlaceType());
combinedPayload.put("payMethod", matching.getPayMethod());
combinedPayload.put("useCoupon", matching.getUseCoupon());
combinedPayload.put("backendEntry", matching.getBackendEntry());
combinedPayload.put("firstOrder", matching.getFirstOrder());
combinedPayload.put("groupId", matching.getGroupId());
combinedPayload.put("sex", matching.getSex());
combinedPayload.set("purchaserTime", range(
reference.minusMinutes(5),
reference.plusMinutes(5)));
combinedPayload.set("acceptTime", range(
matching.getAcceptTime().minusMinutes(5),
matching.getAcceptTime().plusMinutes(5)));
combinedPayload.set("endOrderTime", range(
matching.getOrderEndTime().minusMinutes(5),
matching.getOrderEndTime().plusMinutes(5)));
assertFilterMatches(combinedPayload, matching.getId());
}
private PlayOrderInfoEntity persistOrder(
String marker,
String token,
LocalDateTime purchaserTime,
Consumer<PlayOrderInfoEntity> customizer) {
PlayOrderInfoEntity order = buildBaselineOrder(marker, token, purchaserTime);
customizer.accept(order);
assertThat(orderInfoService.save(order))
.withFailMessage("Failed to persist order %s", order.getOrderNo())
.isTrue();
orderIdsToCleanup.add(order.getId());
return order;
}
private PlayOrderInfoEntity buildBaselineOrder(String marker, String token, LocalDateTime purchaserTime) {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
order.setId("order-" + token + "-" + IdUtils.getUuid().substring(0, 8));
String tokenFragment = token.length() >= 2 ? token.substring(0, 2) : token;
order.setOrderNo(marker + tokenFragment.toUpperCase() + IdUtils.getUuid().substring(0, 4));
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
order.setOrderType("2");
order.setPlaceType("1");
order.setRewardType("0");
order.setFirstOrder("0");
order.setRefundType("0");
order.setRefundAmount(BigDecimal.ZERO);
order.setRefundReason(null);
order.setOrderMoney(DEFAULT_AMOUNT);
order.setDiscountAmount(BigDecimal.ZERO);
order.setFinalAmount(DEFAULT_AMOUNT);
order.setEstimatedRevenue(new BigDecimal("88.00"));
order.setEstimatedRevenueRatio(50);
order.setLabels(Collections.singletonList("label-" + marker));
order.setUseCoupon("1");
order.setCouponIds(Collections.singletonList("coupon-" + marker));
order.setBackendEntry("1");
order.setPaymentSource("balance");
order.setPayMethod("2");
order.setPayState("2");
order.setWeiChatCode("wx-" + marker);
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
order.setPurchaserTime(purchaserTime);
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
order.setAcceptTime(purchaserTime.plusMinutes(20));
order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
order.setOrderStartTime(purchaserTime.plusMinutes(30));
order.setOrderEndTime(purchaserTime.plusHours(2));
order.setOrderSettlementState("0");
order.setOrdersExpiredState("0");
order.setSex("2");
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
order.setCommodityType("1");
order.setCommodityPrice(DEFAULT_AMOUNT);
order.setCommodityName("API Filter Commodity");
order.setServiceDuration("60min");
order.setCommodityNumber("1");
order.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
order.setExcludeHistory("0");
order.setRemark("marker-" + marker);
order.setBackendRemark("backend-" + marker);
order.setProfitSharingAmount(BigDecimal.ZERO);
order.setDeleted(Boolean.FALSE);
order.setCreatedBy("apitest");
order.setUpdatedBy("apitest");
order.setCreatedTime(toDate(purchaserTime));
order.setUpdatedTime(toDate(purchaserTime.plusMinutes(45)));
return order;
}
private ObjectNode baseQuery() {
ObjectNode node = objectMapper.createObjectNode();
node.put("pageNum", 1);
node.put("pageSize", 20);
return node;
}
private ObjectNode queryWithMarker(String marker) {
ObjectNode node = baseQuery();
node.put("orderNo", marker);
return node;
}
private ArrayNode range(LocalDateTime start, LocalDateTime end) {
ArrayNode array = objectMapper.createArrayNode();
array.add(DATE_TIME_FORMATTER.format(start));
array.add(DATE_TIME_FORMATTER.format(end));
return array;
}
private void assertFilterMatches(ObjectNode payload, String expectedOrderId) throws Exception {
RecordsResponse response = executeList(payload);
JsonNode records = response.records;
assertThat(records.isArray())
.withFailMessage("Records payload is not an array for body=%s | response=%s", payload, response.rawResponse)
.isTrue();
assertThat(records.size())
.withFailMessage("Unexpected record count for body=%s | response=%s", payload, response.rawResponse)
.isEqualTo(1);
assertThat(records.get(0).path("id").asText())
.withFailMessage("Unexpected order id for body=%s | response=%s", payload, response.rawResponse)
.isEqualTo(expectedOrderId);
}
private RecordsResponse executeList(ObjectNode payload) throws Exception {
MvcResult result = mockMvc.perform(post("/order/order/listByPage")
.contentType(MediaType.APPLICATION_JSON)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(USER_HEADER, DEFAULT_USER)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
String responseBody = result.getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(responseBody);
JsonNode data = root.path("data");
JsonNode records = data.isArray() ? data : data.path("records");
return new RecordsResponse(records, responseBody);
}
private Date toDate(LocalDateTime time) {
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
}
private void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
private static class RecordsResponse {
private final JsonNode records;
private final String rawResponse;
private RecordsResponse(JsonNode records, String rawResponse) {
this.records = records;
this.rawResponse = rawResponse;
}
}
}