优化
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.lastleg.service.CustomerOrderProgressService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/lastleg/v1/customer-order-progress")
|
||||
public class InternalCustomerOrderProgressController {
|
||||
private final InternalServiceTokenGuard guard;
|
||||
private final CustomerOrderProgressService service;
|
||||
public InternalCustomerOrderProgressController(InternalServiceTokenGuard guard, CustomerOrderProgressService service) {
|
||||
this.guard = guard; this.service = service;
|
||||
}
|
||||
@PostMapping
|
||||
public ApiResponse<CustomerOrderProgress.Response> read(
|
||||
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||
@Valid @RequestBody CustomerOrderProgress.Request request) {
|
||||
guard.verify(token);
|
||||
return ApiResponse.success(service.read(request.customerId()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import java.util.List;
|
||||
|
||||
public final class CustomerOrderProgress {
|
||||
private CustomerOrderProgress() {}
|
||||
public record Request(@NotNull @Positive Long customerId) {}
|
||||
public record Source(String orderId, long packages, long scheduledPackages, long deliveringPackages) {}
|
||||
public record Progress(long orderId, String stage, boolean partial) {}
|
||||
public record Response(List<Progress> progress) {}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.lastleg.mapper;
|
||||
|
||||
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
|
||||
/** Batch read of every actual source, never the representative MIN(order_id) on a shared pallet. */
|
||||
@Mapper
|
||||
public interface CustomerOrderProgressMapper {
|
||||
@Select("""
|
||||
WITH scheduled AS (
|
||||
SELECT line.order_id,SUM(a.planned_package_count) AS packages
|
||||
FROM ll_dispatch_order d
|
||||
JOIN ll_dispatch_item i ON i.dispatch_order_id=d.id AND i.customer_id=d.customer_id
|
||||
JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=i.plan_pallet_id
|
||||
JOIN ll_inbound_plan_line line ON line.id=a.line_id AND line.customer_id=d.customer_id
|
||||
WHERE d.customer_id=#{customerId} AND d.status IN ('PENDING_DEPARTURE','PICKING','READY_TO_DEPART')
|
||||
AND i.warehouse_pallet_id IS NULL
|
||||
GROUP BY line.order_id
|
||||
UNION ALL
|
||||
SELECT b.inbound_order_id AS order_id,SUM(p.available_packages+COALESCE(pk.packages,0)) AS packages
|
||||
FROM ll_dispatch_order d
|
||||
JOIN ll_dispatch_item i ON i.dispatch_order_id=d.id AND i.customer_id=d.customer_id
|
||||
JOIN ll_inventory_position p ON p.customer_id=d.customer_id AND p.pallet_id=i.warehouse_pallet_id
|
||||
AND p.overseas_warehouse_id=i.overseas_warehouse_id
|
||||
JOIN ll_inventory_balance b ON b.id=p.balance_id AND b.customer_id=d.customer_id
|
||||
LEFT JOIN (SELECT dispatch_item_id,inventory_position_id,SUM(packages) AS packages
|
||||
FROM ll_dispatch_pda_pick WHERE customer_id=#{customerId}
|
||||
GROUP BY dispatch_item_id,inventory_position_id) pk
|
||||
ON pk.dispatch_item_id=i.id AND pk.inventory_position_id=p.id
|
||||
WHERE d.customer_id=#{customerId} AND d.status IN ('PENDING_DEPARTURE','PICKING','READY_TO_DEPART')
|
||||
AND (i.plan_pallet_id IS NULL OR EXISTS (
|
||||
SELECT 1 FROM ll_inbound_plan_pallet_allocation a
|
||||
JOIN ll_inbound_plan_line line ON line.id=a.line_id AND line.customer_id=d.customer_id
|
||||
WHERE a.pallet_id=i.plan_pallet_id AND line.order_id=b.inbound_order_id))
|
||||
GROUP BY b.inbound_order_id
|
||||
), departed AS (
|
||||
SELECT a.order_id,SUM(a.packages) AS packages
|
||||
FROM ll_dispatch_pda_pick_order a
|
||||
JOIN ll_dispatch_pda_pick p ON p.id=a.pick_id AND p.customer_id=a.customer_id
|
||||
JOIN ll_dispatch_pda_task t ON t.id=p.task_id AND t.customer_id=a.customer_id
|
||||
JOIN ll_dispatch_order ds ON ds.id=t.dispatch_order_id AND ds.customer_id=a.customer_id
|
||||
JOIN ll_dispatch_item i ON i.id=p.dispatch_item_id AND i.dispatch_order_id=ds.id AND i.customer_id=a.customer_id
|
||||
WHERE a.customer_id=#{customerId} AND ds.status='DEPARTED' AND ds.depart_at IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ll_delivery_order d
|
||||
JOIN ll_outbound_order outb ON outb.id=d.outbound_order_id AND outb.customer_id=d.customer_id
|
||||
WHERE d.customer_id=a.customer_id AND d.dispatch_order_id=ds.id
|
||||
AND outb.status='OUTBOUNDED' AND d.status IN ('WAIT_DELIVERY','DELIVERING')
|
||||
AND d.delivery_group_key=CASE
|
||||
WHEN i.destination_type='PRIVATE' AND NULLIF(TRIM(i.delivery_address),'') IS NOT NULL
|
||||
THEN CONCAT('PRIVATE:',TRIM(i.delivery_address),':',TRIM(COALESCE(i.postal_code,'')))
|
||||
WHEN COALESCE(NULLIF(TRIM(i.destination_warehouse_code),''),NULLIF(TRIM(i.fba_warehouse_code),'')) IS NOT NULL
|
||||
THEN CONCAT('CODE:',UPPER(COALESCE(NULLIF(TRIM(i.destination_warehouse_code),''),NULLIF(TRIM(i.fba_warehouse_code),''))))
|
||||
WHEN NULLIF(TRIM(i.destination_warehouse_id),'') IS NOT NULL THEN CONCAT('ID:',TRIM(i.destination_warehouse_id))
|
||||
ELSE NULL END)
|
||||
GROUP BY a.order_id
|
||||
)
|
||||
SELECT o.source_order_id AS order_id,COALESCE(o.package_count,0) AS packages,
|
||||
COALESCE((SELECT SUM(s.packages) FROM scheduled s WHERE s.order_id=o.id),0) AS scheduled_packages,
|
||||
COALESCE((SELECT SUM(d.packages) FROM departed d WHERE d.order_id=o.id),0) AS delivering_packages
|
||||
FROM ll_order o WHERE o.customer_id=#{customerId} AND o.source_region='FIRSTLEG'
|
||||
AND o.source_order_id IS NOT NULL AND o.source_inbound_id IS NOT NULL
|
||||
AND o.order_type NOT IN ('RELABEL_APPLICATION','WAREHOUSE_TRANSFER')
|
||||
ORDER BY o.id
|
||||
""")
|
||||
List<CustomerOrderProgress.Source> sources(@Param("customerId") String customerId);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||
import com.baiyue.lastleg.mapper.CustomerOrderProgressMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class CustomerOrderProgressService {
|
||||
private final CustomerOrderProgressMapper mapper;
|
||||
public CustomerOrderProgressService(CustomerOrderProgressMapper mapper) { this.mapper = mapper; }
|
||||
@Transactional(readOnly=true)
|
||||
public CustomerOrderProgress.Response read(long customerId) {
|
||||
return new CustomerOrderProgress.Response(project(mapper.sources(Long.toString(customerId))));
|
||||
}
|
||||
static List<CustomerOrderProgress.Progress> project(List<CustomerOrderProgress.Source> sources) {
|
||||
return sources.stream().collect(Collectors.groupingBy(CustomerOrderProgress.Source::orderId))
|
||||
.entrySet().stream().map(entry -> {
|
||||
long total = entry.getValue().stream().mapToLong(CustomerOrderProgress.Source::packages).sum();
|
||||
long delivering = entry.getValue().stream().mapToLong(CustomerOrderProgress.Source::deliveringPackages).sum();
|
||||
long scheduled = entry.getValue().stream().mapToLong(CustomerOrderProgress.Source::scheduledPackages).sum();
|
||||
String stage = delivering > 0 ? "delivering" : scheduled > 0 ? "scheduled" : "none";
|
||||
long covered = delivering > 0 ? delivering : scheduled;
|
||||
// Equality is deliberate: duplicate or incomplete attribution must not claim full coverage.
|
||||
boolean partial = total <= 0 || covered != total || entry.getValue().stream().anyMatch(s -> s.packages() <= 0);
|
||||
return new CustomerOrderProgress.Progress(Long.parseLong(entry.getKey()), stage, partial);
|
||||
}).filter(p -> !"none".equals(p.stage())).sorted(java.util.Comparator.comparingLong(CustomerOrderProgress.Progress::orderId)).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 客户来源头程订单尾程进度内部查询
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/internal/lastleg/v1/customer-order-progress:
|
||||
post:
|
||||
summary: Order按认证客户读取排车和实际派送进度
|
||||
description: >-
|
||||
只读内部接口,无公网路由。customerId由Order认证上下文提供,所有业务表在Lastleg自身Schema内按客户读取。
|
||||
正式排车单PENDING_DEPARTURE/PICKING/READY_TO_DEPART按计划或库存来源计算,预排草稿和取消单不计入。
|
||||
派送必须已实际DEPARTED、出库OUTBOUNDED且对应目的地派送单仍WAIT_DELIVERY/DELIVERING。
|
||||
混托使用所有库存及不可变拣货来源,不使用代表orderId;数量覆盖不完整或异常时标记partial,不签发派送完成事实。
|
||||
security: [{internalServiceToken: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [customerId]
|
||||
properties:
|
||||
customerId: {type: integer, format: int64, minimum: 1}
|
||||
responses:
|
||||
'200':
|
||||
description: ApiResponse,只有code为200表示成功;progress为空表示客户当前无有效排车或派送
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
data:
|
||||
type: object
|
||||
required: [progress]
|
||||
properties:
|
||||
progress:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [orderId, stage, partial]
|
||||
properties:
|
||||
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
stage: {type: string, enum: [scheduled, delivering]}
|
||||
partial: {type: boolean}
|
||||
'400': {description: 客户参数无效}
|
||||
'401': {description: 内部服务令牌缺失或无效}
|
||||
'503': {description: 内部令牌未配置}
|
||||
components:
|
||||
securitySchemes:
|
||||
internalServiceToken:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-Internal-Service-Token
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.lastleg.controller;
|
||||
|
||||
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.lastleg.service.CustomerOrderProgressService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class InternalCustomerOrderProgressControllerTest {
|
||||
@Test void validatesInternalIdentityBeforeReadingCustomerFacts() {
|
||||
var service=mock(CustomerOrderProgressService.class);
|
||||
var controller=new InternalCustomerOrderProgressController(new InternalServiceTokenGuard("test-token"),service);
|
||||
var request=new CustomerOrderProgress.Request(11L);
|
||||
assertThatThrownBy(()->controller.read(null,request)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
|
||||
assertThatThrownBy(()->controller.read("wrong",request)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
|
||||
verifyNoInteractions(service);
|
||||
controller.read("test-token",request);verify(service).read(11);
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.baiyue.lastleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class CustomerOrderProgressMapperTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private CustomerOrderProgressMapper mapper;
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var ds=new JdbcDataSource();ds.setURL("jdbc:h2:mem:tail-progress-"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
jdbc=new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR,source_order_id VARCHAR,source_inbound_id VARCHAR,source_region VARCHAR,order_type VARCHAR,package_count INT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_order(id BIGINT,customer_id VARCHAR,status VARCHAR,depart_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_item(id BIGINT,dispatch_order_id BIGINT,customer_id VARCHAR,plan_pallet_id BIGINT,warehouse_pallet_id BIGINT,overseas_warehouse_id VARCHAR,destination_type VARCHAR,delivery_address VARCHAR,postal_code VARCHAR,destination_warehouse_code VARCHAR,fba_warehouse_code VARCHAR,destination_warehouse_id VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT,planned_package_count INT)");
|
||||
jdbc.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR,order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR,pallet_id BIGINT,overseas_warehouse_id VARCHAR,balance_id BIGINT,available_packages INT)");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR,inbound_order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_pick(id BIGINT,customer_id VARCHAR,dispatch_item_id BIGINT,inventory_position_id BIGINT,packages INT,task_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_pick_order(id BIGINT,pick_id BIGINT,customer_id VARCHAR,order_id BIGINT,packages INT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,customer_id VARCHAR,dispatch_order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_delivery_order(id BIGINT,customer_id VARCHAR,dispatch_order_id BIGINT,outbound_order_id BIGINT,status VARCHAR,delivery_group_key VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE ll_outbound_order(id BIGINT,customer_id VARCHAR,status VARCHAR)");
|
||||
jdbc.update("INSERT INTO ll_order VALUES(1,'11','101','201','FIRSTLEG','FBA',10),(2,'11','102','202','FIRSTLEG','FBA',20),(3,'22','103','203','FIRSTLEG','FBA',30)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_order VALUES(10,'11','PENDING_DEPARTURE',NULL)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_item(id,dispatch_order_id,customer_id,plan_pallet_id,overseas_warehouse_id,destination_warehouse_code) VALUES(100,10,'11',500,'W1','FBA1')");
|
||||
jdbc.update("INSERT INTO ll_inbound_plan_line VALUES(1000,'11',1),(1001,'11',2),(1002,'22',3)");
|
||||
jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(500,1000,10),(500,1001,20),(500,1002,30)");
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(CustomerOrderProgressMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(CustomerOrderProgressMapper.class);
|
||||
}
|
||||
@Test void plannedSharedPalletIncludesEveryOwnSourceAndExcludesCancellation() {
|
||||
var sources=mapper.sources("11");assertThat(sources).hasSize(2);
|
||||
assertThat(sources.get(0).scheduledPackages()).isEqualTo(10);
|
||||
assertThat(sources.get(1).scheduledPackages()).isEqualTo(20);
|
||||
assertThat(sources).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_dispatch_order SET status='CANCELLED'");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.scheduledPackages()==0);
|
||||
assertThat(mapper.sources("22")).hasSize(1).allMatch(s->s.scheduledPackages()==0);
|
||||
}
|
||||
@Test void receivedPalletUsesInventoryAndActualPicksWithoutCountingBothPlanAndStock() {
|
||||
jdbc.update("UPDATE ll_dispatch_item SET warehouse_pallet_id=800");
|
||||
jdbc.update("INSERT INTO ll_inventory_balance VALUES(300,'11',1),(301,'11',2)");
|
||||
jdbc.update("INSERT INTO ll_inventory_position VALUES(400,'11',800,'W1',300,6),(401,'11',800,'W1',301,20)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_pick VALUES(600,'11',100,400,4,700)");
|
||||
var sources=mapper.sources("11");assertThat(sources.get(0).scheduledPackages()).isEqualTo(10);
|
||||
assertThat(sources.get(1).scheduledPackages()).isEqualTo(20);
|
||||
}
|
||||
@Test void deliveryRequiresActualDepartureOutboundAndMatchingDestinationAttribution() {
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_task VALUES(700,'11',10)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_pick VALUES(600,'11',100,400,4,700)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_pick_order VALUES(900,600,'11',1,4)");
|
||||
jdbc.update("INSERT INTO ll_outbound_order VALUES(10000,'11','WAIT_OUTBOUND')");
|
||||
jdbc.update("INSERT INTO ll_delivery_order VALUES(10001,'11',10,10000,'DELIVERING','CODE:FBA1')");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_dispatch_order SET status='DEPARTED',depart_at=CURRENT_TIMESTAMP");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_outbound_order SET status='OUTBOUNDED'");
|
||||
assertThat(mapper.sources("11").get(0).deliveringPackages()).isEqualTo(4);
|
||||
assertThat(mapper.sources("11").get(1).deliveringPackages()).isZero();
|
||||
jdbc.update("UPDATE ll_delivery_order SET delivery_group_key='CODE:FBA2'");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_delivery_order SET delivery_group_key='CODE:FBA1',status='RETURN_PENDING'");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.dto.CustomerOrderProgress.Source;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class CustomerOrderProgressServiceTest {
|
||||
@Test void aggregatesAllInboundFragmentsAndNeverTreatsDuplicateOrUnknownCoverageAsComplete() {
|
||||
var result=CustomerOrderProgressService.project(List.of(
|
||||
new Source("1",10,10,0),new Source("1",10,0,0),
|
||||
new Source("2",10,0,10),new Source("3",10,0,20),new Source("4",0,5,0),new Source("5",10,0,0)));
|
||||
assertThat(result).hasSize(4);
|
||||
assertThat(result.get(0).stage()).isEqualTo("scheduled");assertThat(result.get(0).partial()).isTrue();
|
||||
assertThat(result.get(1).stage()).isEqualTo("delivering");assertThat(result.get(1).partial()).isFalse();
|
||||
assertThat(result.get(2).partial()).isTrue();assertThat(result.get(3).partial()).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user