From 5d6b2812cf89622ed9e848ade61257c152030309 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 3 Nov 2026 16:51:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...astleg-internal-modularization-baseline.md | 4 ++ .../mapper/MerchantTransportPhotoMapper.java | 28 ++++++++ .../MerchantTransportPhotoRepository.java | 17 +++++ .../MerchantTransportPhotoService.java | 65 ++++++++++++++++++ .../MerchantLocalLooseboardController.java | 25 +++++++ .../MerchantUsTruckOrderController.java | 25 +++++++ .../port/MerchantTransportPhotos.java | 16 +++++ .../openapi/merchant-transport-photos-v1.yaml | 62 +++++++++++++++++ .../MerchantTransportPhotoMapperTest.java | 40 +++++++++++ .../MerchantTransportPhotoServiceTest.java | 67 +++++++++++++++++++ .../lastleg-slice-dependencies.json | 5 ++ 11 files changed, 354 insertions(+) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapper.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/MerchantTransportPhotoRepository.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/transport/MerchantTransportPhotoService.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/trucking/port/MerchantTransportPhotos.java create mode 100644 lastleg-service/src/main/resources/openapi/merchant-transport-photos-v1.yaml create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapperTest.java create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/MerchantTransportPhotoServiceTest.java diff --git a/lastleg-service/docs/lastleg-internal-modularization-baseline.md b/lastleg-service/docs/lastleg-internal-modularization-baseline.md index fd1ca3f46..e61b1a913 100644 --- a/lastleg-service/docs/lastleg-internal-modularization-baseline.md +++ b/lastleg-service/docs/lastleg-internal-modularization-baseline.md @@ -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,也不新增包循环。 diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapper.java new file mode 100644 index 000000000..661b8ed3d --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapper.java @@ -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 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 truckDispatches(@Param("customer")String customer,@Param("orderNo")String orderNo); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/MerchantTransportPhotoRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/MerchantTransportPhotoRepository.java new file mode 100644 index 000000000..b96c60ed3 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/MerchantTransportPhotoRepository.java @@ -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 looseboardDispatches(String customer,String number){return mapper.looseboardDispatches(customer,number);} + /** 返回原卡车单有效运输任务。 */ + public List truckDispatches(String customer,String number){return mapper.truckDispatches(customer,number);} +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/transport/MerchantTransportPhotoService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/transport/MerchantTransportPhotoService.java new file mode 100644 index 000000000..531b1ebbf --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/transport/MerchantTransportPhotoService.java @@ -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(Business business,String customer,String number){ + var result=new ArrayList(); + 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); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/MerchantLocalLooseboardController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/MerchantLocalLooseboardController.java index 0793d2aeb..239fd97e9 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/MerchantLocalLooseboardController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/MerchantLocalLooseboardController.java @@ -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 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 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); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/MerchantUsTruckOrderController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/MerchantUsTruckOrderController.java index 9759325a0..41608f21e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/MerchantUsTruckOrderController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/MerchantUsTruckOrderController.java @@ -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 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 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) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/trucking/port/MerchantTransportPhotos.java b/lastleg-service/src/main/java/com/baiyue/lastleg/trucking/port/MerchantTransportPhotos.java new file mode 100644 index 000000000..df111d4b1 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/trucking/port/MerchantTransportPhotos.java @@ -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 pickupPhotos,List 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); +} diff --git a/lastleg-service/src/main/resources/openapi/merchant-transport-photos-v1.yaml b/lastleg-service/src/main/resources/openapi/merchant-transport-photos-v1.yaml new file mode 100644 index 000000000..91b686d85 --- /dev/null +++ b/lastleg-service/src/main/resources/openapi/merchant-transport-photos-v1.yaml @@ -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 } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapperTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapperTest.java new file mode 100644 index 000000000..9ef61fa52 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/mapper/MerchantTransportPhotoMapperTest.java @@ -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(); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/MerchantTransportPhotoServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/MerchantTransportPhotoServiceTest.java new file mode 100644 index 000000000..7c966fe39 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/MerchantTransportPhotoServiceTest.java @@ -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); + } +} diff --git a/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json b/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json index 3e978779b..291ac2746 100644 --- a/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json +++ b/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json @@ -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",