提交
This commit is contained in:
@@ -30,6 +30,10 @@
|
||||
|
||||
`fleet.ownedbusiness`只为原排车装车后的自有车辆组合运营资料。五条精确引用限`OwnedBusinessModels→DispatchViews`、`OwnedBusinessProjection→DispatchContracts`、`OwnedBusinessQueryService→DispatchContracts/DispatchTypes/DispatchDetailReader`,均为`dispatch.port`公开事实或只读应用端口;不增加`fleet→dispatch-delivery`通用许可,不共享原排车实体、Mapper或具体Service。原详情适配器在dispatch内部委托唯一提供者。`DispatchAggregateService→OwnedBusinessOwnershipCapture`仅在原装车事务捕获归属,不能反向调用运营查询;捕获失败随原确认回滚。反向合同拒绝具体持久化/Service引用、其他消费者借用端口许可及任何包循环。
|
||||
|
||||
### 客户原单运输照片读取边界(2026-11-03)
|
||||
|
||||
客户订单域通过 `trucking.port.MerchantTransportPhotos` 声明只读照片查询和内容接口,散板及卡车客户 Controller 复用已有原单列表权限与认证客户范围。仅 `dispatch.transport.MerchantTransportPhotoService` 实现此端口,按同客户商业原单的有效排车关联读取散板当前货物/审核轮次照片或本土卡车取货/完成凭证;下载再次核对当前绑定。结构门禁只登记此实现类到端口的精确依赖,不开放客户订单 Controller 到排车具体 Service、Repository 或 Mapper,也不新增包循环。
|
||||
|
||||
### 装车资料触发派送边界(2026-10-21)
|
||||
|
||||
`DispatchLoadingDocumentService`、`DispatchLoadingDepartureService`及平台/PDA两个装车资料Controller只引用`fleet.ownedbusiness.port.DispatchLoadingPhotoPort`的公开照片、版本、绑定请求和结果。`DispatchLoadingPhotoAdapter`在车队内部复用原附加表、上传账本和文件校验,不反向调用排车服务。排车服务持整车父锁,在装车确认或资料保存后核对两项条件,同一事务推进派送并记录原结果。结构门禁仅开放上述四类到此端口的精确引用,不开放车队Service、Repository或Mapper,也不新增包循环。
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.dispatch.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
|
||||
/** 客户照片的真实承运范围,只关联同客户商业原单,不按展示编号猜来源。 */
|
||||
@Mapper
|
||||
public interface MerchantTransportPhotoMapper {
|
||||
/** 申报散板或实收散板均沿原单、入仓计划和排车明细精确关联。 */
|
||||
@Select("""
|
||||
SELECT DISTINCT d.dispatch_no FROM ll_local_looseboard_order o
|
||||
JOIN ll_inbound_plan p ON p.source_order_id=o.lastleg_order_id AND p.customer_id=o.customer_id
|
||||
JOIN ll_inbound_plan_pallet pp ON pp.plan_id=p.id AND pp.customer_id=p.customer_id
|
||||
JOIN ll_dispatch_item i ON i.plan_pallet_id=pp.id AND i.customer_id=o.customer_id
|
||||
JOIN ll_dispatch_order d ON d.id=i.dispatch_order_id AND d.status='ACTIVE'
|
||||
WHERE o.customer_id=#{customer} AND o.order_no=#{orderNo} ORDER BY d.dispatch_no
|
||||
""")
|
||||
List<String> looseboardDispatches(@Param("customer")String customer,@Param("orderNo")String orderNo);
|
||||
|
||||
/** 本土卡车只读取本客户原单当前有效排车,已取消安排不暴露。 */
|
||||
@Select("""
|
||||
SELECT t.dispatch_no FROM ll_dispatch_truck_task t
|
||||
JOIN ll_dispatch_order d ON d.dispatch_no=t.dispatch_no AND d.status='ACTIVE' AND d.task_type='LOCAL_TRUCK'
|
||||
WHERE t.customer_id=#{customer} AND t.order_no=#{orderNo} AND t.active_order IS NOT NULL
|
||||
ORDER BY d.id
|
||||
""")
|
||||
List<String> truckDispatches(@Param("customer")String customer,@Param("orderNo")String orderNo);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.dispatch.repository;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.mapper.MerchantTransportPhotoMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** 客户照片仅查询已核准客户与原单的有效承运关联。 */
|
||||
@Repository
|
||||
public class MerchantTransportPhotoRepository {
|
||||
private final MerchantTransportPhotoMapper mapper;
|
||||
/** 注入本域查询入口。 */
|
||||
public MerchantTransportPhotoRepository(MerchantTransportPhotoMapper mapper){this.mapper=mapper;}
|
||||
/** 返回原散板单实际关联的排车身份。 */
|
||||
public List<String> looseboardDispatches(String customer,String number){return mapper.looseboardDispatches(customer,number);}
|
||||
/** 返回原卡车单有效运输任务。 */
|
||||
public List<String> truckDispatches(String customer,String number){return mapper.truckDispatches(customer,number);}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.baiyue.lastleg.dispatch.transport;
|
||||
|
||||
import com.baiyue.lastleg.trucking.port.MerchantTransportPhotos;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchLooseboardFacts;
|
||||
import com.baiyue.lastleg.dispatch.repository.*;
|
||||
import com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import java.util.*;
|
||||
|
||||
/** 客户读取各自原单照片;复用正式照片事实与文件归属,不授权访问整车资料。 */
|
||||
@Service
|
||||
public class MerchantTransportPhotoService implements MerchantTransportPhotos {
|
||||
private record Bound(Photo photo,Phase phase,Long dispatchId,String truckDispatch) {}
|
||||
private final MerchantTransportPhotoRepository links;
|
||||
private final DispatchAggregateRepository roots;
|
||||
private final DispatchLooseboardFacts looseboard;
|
||||
private final DispatchEvidenceGateway files;
|
||||
private final TransportTaskService trucks;
|
||||
/** 照片查询与下载沿正式承运来源及文件网关。 */
|
||||
public MerchantTransportPhotoService(MerchantTransportPhotoRepository links,DispatchAggregateRepository roots,
|
||||
DispatchLooseboardFacts looseboard,DispatchEvidenceGateway files,TransportTaskService trucks){
|
||||
this.links=links;this.roots=roots;this.looseboard=looseboard;this.files=files;this.trucks=trucks;
|
||||
}
|
||||
/** 只返回当前货物与审核轮次仍有效、且已保存的本票照片。 */
|
||||
@Override @Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public Detail photos(Business business,String customer,String number){
|
||||
var bound=bound(business,customer,number);
|
||||
return new Detail(bound.stream().filter(p->p.phase()==Phase.PICKUP).map(Bound::photo).distinct().toList(),
|
||||
bound.stream().filter(p->p.phase()==Phase.ARRIVAL).map(Bound::photo).distinct().toList());
|
||||
}
|
||||
/** 下载重新限定原单节点;被删除、过期轮次、其他客户与未绑定上传均不能读取。 */
|
||||
@Override @Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public Download content(Business business,String customer,String number,Phase phase,long fileId){
|
||||
var photo=bound(business,customer,number).stream().filter(p->p.phase()==phase && p.photo().fileId().equals(Long.toString(fileId)))
|
||||
.findFirst().orElseThrow(()->new ResponseStatusException(HttpStatus.NOT_FOUND,"当前订单没有该照片"));
|
||||
byte[] bytes=photo.truckDispatch()!=null?trucks.evidence(photo.truckDispatch(),fileId):files.download(photo.dispatchId(),
|
||||
phase==Phase.PICKUP?DispatchEvidenceGateway.Kind.LOOSEBOARD_PICKUP:DispatchEvidenceGateway.Kind.LOOSEBOARD_ARRIVAL,fileId);
|
||||
return new Download(photo.photo(),bytes);
|
||||
}
|
||||
/** 混排只筛选本票照片,卡车复用真实取货和完成命令中的凭证分类。 */
|
||||
private List<Bound> bound(Business business,String customer,String number){
|
||||
var result=new ArrayList<Bound>();
|
||||
if(business==Business.LOOSEBOARD){
|
||||
for(String dispatch:links.looseboardDispatches(customer,number)){
|
||||
var row=roots.find(dispatch);if(row==null || !"ACTIVE".equals(row.getStatus()))continue;
|
||||
long id=row.getId();var cargo=looseboard.cargo(id);
|
||||
for(var evidence:looseboard.current(row)){
|
||||
if(!customer.equals(evidence.customerId()) || !number.equals(evidence.orderNo()))continue;
|
||||
if(cargo.stream().noneMatch(c->DispatchLooseboardFacts.matches(evidence,c)))continue;
|
||||
for(var file:evidence.files())result.add(new Bound(new Photo(file.fileId().toString(),file.fileName(),file.contentType(),file.sizeBytes()),
|
||||
Phase.valueOf(evidence.phase().name()),id,null));
|
||||
}
|
||||
}
|
||||
}else for(String dispatch:links.truckDispatches(customer,number)){
|
||||
var task=trucks.detail(dispatch);
|
||||
if(!customer.equals(task.customerId()) || !number.equals(task.orderNo()))throw new IllegalStateException("卡车照片归属不匹配");
|
||||
for(var file:task.departureEvidence())result.add(new Bound(new Photo(file.fileId(),file.fileName(),file.contentType(),file.sizeBytes()),Phase.PICKUP,null,dispatch));
|
||||
for(var file:task.completionEvidence())result.add(new Bound(new Photo(file.fileId(),file.fileName(),file.contentType(),file.sizeBytes()),Phase.ARRIVAL,null,dispatch));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
}
|
||||
+25
@@ -1,6 +1,10 @@
|
||||
package com.baiyue.lastleg.looseboard.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.trucking.port.MerchantTransportPhotos;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
|
||||
@@ -54,6 +58,27 @@ public class MerchantLocalLooseboardController {
|
||||
return ApiResponse.success(service.update(customer(user), orderNo, operator(user), request));
|
||||
}
|
||||
|
||||
@Resource private MerchantTransportPhotos transportPhotos;
|
||||
|
||||
/** 客户只读自己原单的取货和派送照片,不接受请求指定客户。 */
|
||||
@GetMapping("/{orderNo}/transport-photos")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_LOOSEBOARD_LIST + "')")
|
||||
public ApiResponse<MerchantTransportPhotos.Detail> photos(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String orderNo) {
|
||||
String customer=customer(user);service.detail(customer,orderNo);
|
||||
return ApiResponse.success(transportPhotos.photos(MerchantTransportPhotos.Business.LOOSEBOARD,customer,orderNo));
|
||||
}
|
||||
|
||||
/** 文件内容由同一客户、原单和已保存节点引用重新授权,不提供公共文件地址。 */
|
||||
@GetMapping("/{orderNo}/transport-photos/{phase}/{fileId}/content")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_LOOSEBOARD_LIST + "')")
|
||||
public ResponseEntity<byte[]> photoContent(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String orderNo,
|
||||
@PathVariable MerchantTransportPhotos.Phase phase,@PathVariable long fileId) {
|
||||
String customer=customer(user);service.detail(customer,orderNo);
|
||||
var result=transportPhotos.content(MerchantTransportPhotos.Business.LOOSEBOARD,customer,orderNo,phase,fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore()).contentType(MediaType.parseMediaType(result.photo().contentType()))
|
||||
.header("X-Content-Type-Options","nosniff").body(result.bytes());
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
|
||||
+25
@@ -1,6 +1,10 @@
|
||||
package com.baiyue.lastleg.trucking.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.trucking.port.MerchantTransportPhotos;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
@@ -65,6 +69,27 @@ public class MerchantUsTruckOrderController {
|
||||
return ApiResponse.success(service.changeStatus(customer(user), orderNo, operator(user), request));
|
||||
}
|
||||
|
||||
@Resource private MerchantTransportPhotos transportPhotos;
|
||||
|
||||
/** 客户只读自己原单的取货和派送照片,不接受请求指定客户。 */
|
||||
@GetMapping("/{orderNo}/transport-photos")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRUCKING_LIST + "')")
|
||||
public ApiResponse<MerchantTransportPhotos.Detail> photos(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String orderNo) {
|
||||
String customer=customer(user);service.detail(customer,orderNo);
|
||||
return ApiResponse.success(transportPhotos.photos(MerchantTransportPhotos.Business.TRUCK,customer,orderNo));
|
||||
}
|
||||
|
||||
/** 文件内容由同一客户、原单和已保存节点引用重新授权,不提供公共文件地址。 */
|
||||
@GetMapping("/{orderNo}/transport-photos/{phase}/{fileId}/content")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRUCKING_LIST + "')")
|
||||
public ResponseEntity<byte[]> photoContent(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String orderNo,
|
||||
@PathVariable MerchantTransportPhotos.Phase phase,@PathVariable long fileId) {
|
||||
String customer=customer(user);service.detail(customer,orderNo);
|
||||
var result=transportPhotos.content(MerchantTransportPhotos.Business.TRUCK,customer,orderNo,phase,fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore()).contentType(MediaType.parseMediaType(result.photo().contentType()))
|
||||
.header("X-Content-Type-Options","nosniff").body(result.bytes());
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.trucking.port;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 客户只读运输照片,不公开排车、库存或其他客户的作业资料。 */
|
||||
public interface MerchantTransportPhotos {
|
||||
enum Business { LOOSEBOARD, TRUCK }
|
||||
enum Phase { PICKUP, ARRIVAL }
|
||||
record Photo(String fileId,String fileName,String contentType,long sizeBytes) {}
|
||||
record Detail(List<Photo> pickupPhotos,List<Photo> arrivalPhotos) {}
|
||||
record Download(Photo photo,byte[] bytes) {}
|
||||
/** 仅按已认证客户与商业原单读取当前有效凭证。 */
|
||||
Detail photos(Business business,String customer,String orderNo);
|
||||
/** 必须重新核对本客户、本票与节点当前绑定,不能用上传回执直接下载。 */
|
||||
Download content(Business business,String customer,String orderNo,Phase phase,long fileId);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 客户本土散板及卡车运输照片
|
||||
version: 1.0.0
|
||||
description: 只读当前客户商业原单的已保存取货和派送凭证。散板按当前货物与审核轮次过滤,混排不披露其他客户或其他订单照片;卡车沿有效运输关联读取取货和完成凭证。上传但未绑定、已删除和已取消安排的照片不可读取。
|
||||
paths:
|
||||
/api/merchant/v1/last-leg/local-looseboard-orders/{orderNo}/transport-photos:
|
||||
get:
|
||||
summary: 本客户散板原单取货和派送照片
|
||||
description: 使用 merchant:cross-border:pallet-shipping:list 权限及 Token 客户范围。
|
||||
parameters: [{ $ref: '#/components/parameters/OrderNo' }]
|
||||
responses:
|
||||
'200': { $ref: '#/components/responses/Photos' }
|
||||
/api/merchant/v1/last-leg/us-truck-orders/{orderNo}/transport-photos:
|
||||
get:
|
||||
summary: 本客户卡车原单取货和派送凭证
|
||||
description: 使用 merchant:cross-border:domestic-trucking:list 权限及 Token 客户范围。
|
||||
parameters: [{ $ref: '#/components/parameters/OrderNo' }]
|
||||
responses:
|
||||
'200': { $ref: '#/components/responses/Photos' }
|
||||
/api/merchant/v1/last-leg/local-looseboard-orders/{orderNo}/transport-photos/{phase}/{fileId}/content:
|
||||
get:
|
||||
summary: 读取本客户散板已保存节点照片
|
||||
description: 同散板列表权限;按原单、节点和文件引用重新授权,返回 no-store 及 nosniff。
|
||||
parameters: [{ $ref: '#/components/parameters/OrderNo' }, { $ref: '#/components/parameters/Phase' }, { $ref: '#/components/parameters/FileId' }]
|
||||
responses:
|
||||
'200': { description: 文件二进制或统一业务失败响应, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
|
||||
/api/merchant/v1/last-leg/us-truck-orders/{orderNo}/transport-photos/{phase}/{fileId}/content:
|
||||
get:
|
||||
summary: 读取本客户卡车已保存节点凭证
|
||||
description: 同卡车列表权限;按原单、节点和文件引用重新授权,返回 no-store 及 nosniff。
|
||||
parameters: [{ $ref: '#/components/parameters/OrderNo' }, { $ref: '#/components/parameters/Phase' }, { $ref: '#/components/parameters/FileId' }]
|
||||
responses:
|
||||
'200': { description: 文件二进制或统一业务失败响应, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
|
||||
components:
|
||||
parameters:
|
||||
OrderNo: { name: orderNo, in: path, required: true, schema: { type: string } }
|
||||
Phase: { name: phase, in: path, required: true, schema: { type: string, enum: [PICKUP, ARRIVAL] } }
|
||||
FileId: { name: fileId, in: path, required: true, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
|
||||
responses:
|
||||
Photos:
|
||||
description: HTTP 200统一业务响应;成功 code 为字符串200,data包含两类照片,空集合不生成虚构凭证。
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: string }
|
||||
message: { type: string }
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
pickupPhotos: { type: array, items: { $ref: '#/components/schemas/Photo' } }
|
||||
arrivalPhotos: { type: array, items: { $ref: '#/components/schemas/Photo' } }
|
||||
schemas:
|
||||
Photo:
|
||||
type: object
|
||||
properties:
|
||||
fileId: { type: string }
|
||||
fileName: { type: string }
|
||||
contentType: { type: string }
|
||||
sizeBytes: { type: integer, format: int64 }
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.lastleg.dispatch.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.dispatch.repository.MerchantTransportPhotoRepository;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class MerchantTransportPhotoMapperTest {
|
||||
/** 同编号、混装和撤销安排均不能扩大客户读取范围,实际查询使用正式Mapper。 */
|
||||
@Test void queriesOnlyExactCustomerOrderAndActiveDispatches() throws Exception {
|
||||
var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||
var db=new JdbcTemplate(data);
|
||||
db.execute("CREATE TABLE ll_local_looseboard_order(customer_id VARCHAR(20),order_no VARCHAR(30),lastleg_order_id BIGINT)");
|
||||
db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR(20),source_order_id BIGINT)");
|
||||
db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,customer_id VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_dispatch_item(plan_pallet_id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT)");
|
||||
db.execute("CREATE TABLE ll_dispatch_order(id BIGINT,dispatch_no VARCHAR(30),status VARCHAR(20),task_type VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_dispatch_truck_task(dispatch_no VARCHAR(30),customer_id VARCHAR(20),order_no VARCHAR(30),active_order VARCHAR(50))");
|
||||
db.update("INSERT INTO ll_local_looseboard_order VALUES('C1','LB1',1),('C2','LB1',2)");
|
||||
db.update("INSERT INTO ll_inbound_plan VALUES(11,'C1',1),(12,'C2',2)");
|
||||
db.update("INSERT INTO ll_inbound_plan_pallet VALUES(21,11,'C1'),(22,12,'C2')");
|
||||
db.update("INSERT INTO ll_dispatch_order VALUES(31,'PC1','ACTIVE','DELIVERY'),(32,'PC2','CANCELLED','DELIVERY'),(33,'PC3','ACTIVE','LOCAL_TRUCK')");
|
||||
db.update("INSERT INTO ll_dispatch_item VALUES(21,'C1',31),(22,'C2',31),(21,'C1',32),(22,'C1',33)");
|
||||
db.update("INSERT INTO ll_dispatch_truck_task VALUES('PC3','C1','TR1','C1:TR1'),('PC2','C1','TR1','C1:TR1'),('PC1','C2','TR1','C2:TR1')");
|
||||
var config=new MybatisConfiguration();config.addMapper(MerchantTransportPhotoMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(config);factory.afterPropertiesSet();
|
||||
var repository=new MerchantTransportPhotoRepository(new SqlSessionTemplate(factory.getObject()).getMapper(MerchantTransportPhotoMapper.class));
|
||||
assertThat(repository.looseboardDispatches("C1","LB1")).containsExactly("PC1");
|
||||
assertThat(repository.looseboardDispatches("C3","LB1")).isEmpty();
|
||||
assertThat(repository.truckDispatches("C1","TR1")).containsExactly("PC3");
|
||||
assertThat(repository.truckDispatches("C2","TR1")).isEmpty();
|
||||
db.update("UPDATE ll_dispatch_truck_task SET active_order=NULL WHERE dispatch_no='PC3'");
|
||||
assertThat(repository.truckDispatches("C1","TR1")).isEmpty();
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.trucking.port.MerchantTransportPhotos.*;
|
||||
import com.baiyue.lastleg.dispatch.entity.*;
|
||||
import com.baiyue.lastleg.dispatch.repository.*;
|
||||
import com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway;
|
||||
import com.baiyue.lastleg.dispatch.transport.*;
|
||||
import com.baiyue.lastleg.fleet.reporting.TripOperationsRecordModels.EvidenceFile;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class MerchantTransportPhotoServiceTest {
|
||||
/** 混装只回显本客户本票当前照片,阶段和文件ID不能绕过授权;删除后立即不可下载。 */
|
||||
@Test void looseboardPhotosStayWithinCustomerOrderAndCurrentBinding(){
|
||||
var f=new Fixture();var row=new DispatchAggregate();row.setId(1L);row.setStatus("ACTIVE");
|
||||
when(f.links.looseboardDispatches("C1","LB1")).thenReturn(List.of("PC1"));when(f.roots.find("PC1")).thenReturn(row);
|
||||
var item=new DispatchItem();item.setId(10L);item.setCustomerId("C1");
|
||||
when(f.facts.cargo(1)).thenReturn(List.of(new DispatchLooseboardFacts.Cargo(item,"LB1")));
|
||||
when(f.facts.current(row)).thenReturn(List.of(evidence("C1","LB1",10,DispatchLooseboardFacts.Phase.PICKUP,71),
|
||||
evidence("C1","LB1",10,DispatchLooseboardFacts.Phase.ARRIVAL,72),evidence("C2","LB2",20,DispatchLooseboardFacts.Phase.PICKUP,73),
|
||||
evidence("C1","LB1",99,DispatchLooseboardFacts.Phase.PICKUP,74)));
|
||||
var photos=f.service.photos(Business.LOOSEBOARD,"C1","LB1");
|
||||
assertThat(photos.pickupPhotos()).extracting(Photo::fileId).containsExactly("71");
|
||||
assertThat(photos.arrivalPhotos()).extracting(Photo::fileId).containsExactly("72");
|
||||
assertThatThrownBy(()->f.service.content(Business.LOOSEBOARD,"C1","LB1",Phase.PICKUP,73)).hasMessageContaining("没有该照片");
|
||||
assertThatThrownBy(()->f.service.content(Business.LOOSEBOARD,"C1","LB1",Phase.ARRIVAL,71)).hasMessageContaining("没有该照片");
|
||||
verifyNoInteractions(f.files);
|
||||
when(f.files.download(1,DispatchEvidenceGateway.Kind.LOOSEBOARD_PICKUP,71)).thenReturn(new byte[]{1});
|
||||
assertThat(f.service.content(Business.LOOSEBOARD,"C1","LB1",Phase.PICKUP,71).bytes()).containsExactly(1);
|
||||
when(f.facts.current(row)).thenReturn(List.of());
|
||||
assertThatThrownBy(()->f.service.content(Business.LOOSEBOARD,"C1","LB1",Phase.PICKUP,71)).hasMessageContaining("没有该照片");
|
||||
when(f.facts.current(row)).thenReturn(List.of(evidence("C1","LB1",10,DispatchLooseboardFacts.Phase.PICKUP,71)));
|
||||
row.setStatus("CANCELLED");assertThat(f.service.photos(Business.LOOSEBOARD,"C1","LB1").pickupPhotos()).isEmpty();
|
||||
}
|
||||
/** 卡车保持取货与完成分类,读取已绑定凭证且不披露司机证件;关联客户异常直接拒绝。 */
|
||||
@Test void truckPhotosUseOnlyLinkedTransportEvidence(){
|
||||
var f=new Fixture();when(f.links.truckDispatches("C1","TR1")).thenReturn(List.of("PC2"));
|
||||
when(f.trucks.detail("PC2")).thenReturn(truck("C1","TR1"));
|
||||
var photos=f.service.photos(Business.TRUCK,"C1","TR1");
|
||||
assertThat(photos.pickupPhotos()).extracting(Photo::fileId).containsExactly("81");
|
||||
assertThat(photos.arrivalPhotos()).extracting(Photo::fileId).containsExactly("82");
|
||||
when(f.trucks.evidence("PC2",82)).thenReturn(new byte[]{2});
|
||||
assertThat(f.service.content(Business.TRUCK,"C1","TR1",Phase.ARRIVAL,82).bytes()).containsExactly(2);
|
||||
assertThatThrownBy(()->f.service.content(Business.TRUCK,"C1","TR1",Phase.PICKUP,82)).hasMessageContaining("没有该照片");
|
||||
when(f.trucks.detail("PC2")).thenReturn(truck("C2","TR1"));
|
||||
assertThatThrownBy(()->f.service.photos(Business.TRUCK,"C1","TR1")).hasMessageContaining("归属不匹配");
|
||||
}
|
||||
/** 测试凭证保留真实节点身份,不将上传回执当作保存事实。 */
|
||||
private static DispatchLooseboardFacts.Evidence evidence(String customer,String order,long item,DispatchLooseboardFacts.Phase phase,long file){
|
||||
return new DispatchLooseboardFacts.Evidence(Long.toString(item),customer,order,phase,"hash",1,
|
||||
List.of(new DispatchEvidenceGateway.File(file,"photo.png","image/png",5)),Instant.now(),"operator","key","hash");
|
||||
}
|
||||
/** 卡车资料夹具仅包含取货与完成附件。 */
|
||||
private static TransportTaskModels.Detail truck(String customer,String order){return new TransportTaskModels.Detail("PC2","LOCAL_TRUCK",null,customer,order,List.of(),
|
||||
List.of(new EvidenceFile("81","pickup.png","image/png",5)),List.of(new EvidenceFile("82","arrival.png","image/png",5)),1L,null,null);}
|
||||
static class Fixture {
|
||||
final MerchantTransportPhotoRepository links=mock(MerchantTransportPhotoRepository.class);
|
||||
final DispatchAggregateRepository roots=mock(DispatchAggregateRepository.class);
|
||||
final DispatchLooseboardFacts facts=mock(DispatchLooseboardFacts.class);
|
||||
final DispatchEvidenceGateway files=mock(DispatchEvidenceGateway.class);
|
||||
final TransportTaskService trucks=mock(TransportTaskService.class);
|
||||
final MerchantTransportPhotoService service=new MerchantTransportPhotoService(links,roots,facts,files,trucks);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,11 @@
|
||||
"com.baiyue.lastleg.security.service.SecurityProjectionMonotonicity"
|
||||
],
|
||||
"reviewedClassDependencies": [
|
||||
{
|
||||
"source": "com.baiyue.lastleg.dispatch.transport.MerchantTransportPhotoService",
|
||||
"target": "com.baiyue.lastleg.trucking.port.MerchantTransportPhotos",
|
||||
"reason": "客户订单域声明取货和派送照片只读端口,排车运输域按客户与商业原单精确关联实现;不开放客户对整车、司机证件或其他客户照片的访问,不开放反向具体服务依赖。"
|
||||
},
|
||||
{
|
||||
"source": "com.baiyue.lastleg.looseboard.service.LocalLooseboardService",
|
||||
"target": "com.baiyue.lastleg.dispatch.port.LooseboardTransportPort",
|
||||
|
||||
Reference in New Issue
Block a user