feat: implement order relation type tracking

- Add OrderRelationType enum (UNASSIGNED, LEGACY, FIRST, CONTINUED, NEUTRAL)
- Create play_clerk_customer_relation table to track first-completed history
- Add order_relation_type column to play_order_info
- Migrate existing orders to set relation types based on completion history
- Update order services to determine relation type on creation
- Update order VOs and controllers to expose relation type in API responses
- Update clerk performance calculations to account for relation types
- Update revenue calculations to distinguish between first and continued orders
- Add comprehensive API and unit tests for order relation functionality
This commit is contained in:
irving
2025-12-31 22:06:05 -05:00
parent f39b560a05
commit 911a974e51
36 changed files with 5420 additions and 179 deletions

View File

@@ -0,0 +1,197 @@
package com.starry.admin.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
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.node.ObjectNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
class LegacyOrderIntegrationTest extends WxCustomOrderApiTestSupport {
private String orderIdToCleanup;
@AfterEach
void tearDown() {
if (orderIdToCleanup != null) {
ensureTenantContext();
playOrderInfoService.removeById(orderIdToCleanup);
orderIdToCleanup = null;
}
}
@Test
void getLegacyOrderDetails_Admin_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
mockMvc.perform(get("/order/order/" + legacyOrder.getId())
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(USER_HEADER, DEFAULT_USER)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.orderRelationType").value("LEGACY"));
}
@Test
void listByPage_Admin_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
ObjectNode payload = objectMapper.createObjectNode();
payload.put("pageNum", 1);
payload.put("pageSize", 10);
payload.put("orderNo", legacyOrder.getOrderNo());
String response = mockMvc.perform(post("/order/order/listByPage")
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(USER_HEADER, ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn().getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(response);
JsonNode data = root.path("data");
JsonNode records = data.isArray() ? data : data.path("records");
String type = records.get(0).path("orderRelationType").asText();
assertThat(type).isEqualTo("LEGACY");
}
private String ensureCustomerToken() {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
return customerToken;
}
@Test
void queryById_WxCustom_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
String customerToken = ensureCustomerToken();
mockMvc.perform(get("/wx/custom/order/queryById")
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.param("id", legacyOrder.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.orderRelationType").value("LEGACY"));
}
@Test
void queryByPage_WxCustom_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
String customerToken = ensureCustomerToken();
ObjectNode payload = objectMapper.createObjectNode();
payload.put("pageNum", 1);
payload.put("pageSize", 20);
String response = mockMvc.perform(post("/wx/custom/order/queryByPage")
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn().getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(response);
JsonNode records = root.path("data").isArray() ? root.path("data") : root.path("data").path("records");
boolean found = false;
for (JsonNode node : records) {
if (legacyOrder.getId().equals(node.path("id").asText())) {
assertThat(node.path("orderRelationType").asText()).isEqualTo("LEGACY");
found = true;
break;
}
}
assertThat(found).withFailMessage("Legacy order not found in customer list").isTrue();
}
@Test
void calculateEstimatedRevenue_ForLegacyOrder_ShouldNotFail() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
BigDecimal revenue = playOrderInfoService.getEstimatedRevenue(
legacyOrder.getAcceptBy(),
legacyOrder.getPlaceType(),
legacyOrder.getOrderRelationType(),
legacyOrder.getOrderMoney()
);
assertThat(revenue).isNotNull();
assertThat(revenue).isGreaterThanOrEqualTo(BigDecimal.ZERO);
}
@Test
void calculateEstimatedRevenue_ForUnassignedOrder_ShouldThrowException() {
ensureTenantContext();
try {
playOrderInfoService.getEstimatedRevenue(
ApiTestDataSeeder.DEFAULT_CLERK_ID,
"0", // Specified
OrderConstant.OrderRelationType.UNASSIGNED,
new BigDecimal("100.00")
);
assertThat(true).withFailMessage("Should have thrown exception for UNASSIGNED relation type").isFalse();
} catch (Exception e) {
assertThat(e).isInstanceOf(RuntimeException.class);
assertThat(e.getMessage()).contains("未分配订单不可计算预计收益");
}
}
private PlayOrderInfoEntity createLegacyOrder() {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
String id = "legacy-" + IdUtils.getUuid();
order.setId(id);
order.setOrderNo("LEGACY" + IdUtils.getUuid().substring(0, 6));
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
order.setOrderStatus("3"); // Completed
order.setOrderType("2"); // Normal
order.setPlaceType("0"); // Specified
order.setOrderRelationType(OrderConstant.OrderRelationType.LEGACY);
order.setOrderMoney(new BigDecimal("100.00"));
order.setFinalAmount(new BigDecimal("100.00"));
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
order.setPurchaserTime(LocalDateTime.now().minusDays(100));
order.setCreatedTime(new Date());
order.setUpdatedTime(new Date());
order.setDeleted(false);
order.setPayMethod("2");
order.setUseCoupon("0");
order.setBackendEntry("0");
order.setSex("2");
order.setCommodityType("1");
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
order.setCommodityName("Legacy Service");
order.setCommodityPrice(new BigDecimal("100.00"));
order.setCommodityNumber("1");
order.setRefundType("0");
order.setRefundAmount(BigDecimal.ZERO);
playOrderInfoService.save(order);
orderIdToCleanup = id;
return order;
}
}