feat(fleet): integrate costs quotes operations and in-transit handovers
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.fleet.maintenance;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/fleet/vehicles/{vehicleId}/records")
|
||||
public class PlatformVehicleCareFinanceController {
|
||||
private final VehicleCareFinanceService service;
|
||||
public PlatformVehicleCareFinanceController(VehicleCareFinanceService service) { this.service = service; }
|
||||
@PostMapping("/finance-status")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-master-data:vehicle:list')")
|
||||
public ApiResponse<List<VehicleCareFinanceModels.Status>> statuses(@PathVariable long vehicleId, @Valid @RequestBody VehicleCareFinanceModels.Query request) {
|
||||
return ApiResponse.success(service.statuses(vehicleId, request.recordNos()));
|
||||
}
|
||||
@PostMapping("/{recordNo}/finance")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-master-data:vehicle:record-finance-submit')")
|
||||
public ApiResponse<VehicleCareFinanceModels.Submitted> submit(@PathVariable long vehicleId, @PathVariable String recordNo,
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody VehicleCareFinanceModels.Submit request) {
|
||||
return ApiResponse.success(service.submit(vehicleId, recordNo, user.userId().toString(), request));
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.fleet.maintenance;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name="finance-service", contextId="vehicleCareFinance", url="${baiyue.clients.finance.url:}")
|
||||
public interface VehicleCareFinanceClient {
|
||||
@PostMapping("/internal/finance/v1/vehicle-costs/sources/query")
|
||||
ApiResponse<List<VehicleCareFinanceModels.Cost>> query(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody VehicleCareFinanceModels.SourceQuery request);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.fleet.maintenance;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** The fee slice owns currency conversion and the shared reliable outbox. */
|
||||
public interface VehicleCareFinanceGateway {
|
||||
record Conversion(String originalCurrency, BigDecimal originalAmount, BigDecimal exchangeRate,
|
||||
String exchangeRateMonth, BigDecimal settlementAmount) { }
|
||||
Conversion convert(String currency, BigDecimal amount, LocalDate businessDate);
|
||||
/** Must join the caller's transaction; failure must roll back the fleet source mapping too. */
|
||||
void publish(EventEnvelope event);
|
||||
Map<String, String> deliveryStates(List<String> eventKeys);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.fleet.maintenance;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public final class VehicleCareFinanceModels {
|
||||
private VehicleCareFinanceModels() { }
|
||||
public record Submit(@NotNull @Min(0) Long recordVersion, @NotNull @Positive Long supplierId,
|
||||
@NotNull LocalDate businessDate, @NotBlank @Size(min=8,max=128) String idempotencyKey) { }
|
||||
public record Query(@NotEmpty @Size(max=100) List<@NotBlank @Size(max=64) String> recordNos) { }
|
||||
public record Source(String sourceType, String sourceNo, long sourceVersion, String vehicleId, String vehiclePlate,
|
||||
String tripNo, long supplierId, String feeName, LocalDate businessDate, String originalCurrency, BigDecimal originalAmount,
|
||||
BigDecimal exchangeRate, String exchangeRateMonth, BigDecimal amount, String remark) { }
|
||||
public record SourceKey(String sourceType, String sourceNo) { }
|
||||
public record SourceQuery(List<SourceKey> sources) { }
|
||||
public record Cost(String costNo, String subjectCode, Source source, String supplierName, String currency, String status,
|
||||
BigDecimal paidAmount, BigDecimal outstandingAmount, long recordVersion) { }
|
||||
public record Status(String recordNo, String status, Long sourceVersion, Long submittedRecordVersion, Cost finance) { }
|
||||
public record Submitted(String recordNo, long sourceVersion, long submittedRecordVersion, boolean replayed, String status) { }
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package com.baiyue.lastleg.fleet.maintenance;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.event.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class VehicleCareFinanceService {
|
||||
private static final String EVENT_TYPE = EventTypes.COMPANY_VEHICLE_COST_SUBMITTED;
|
||||
private final VehicleCareMapper records;
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper json;
|
||||
private final VehicleCareFinanceGateway accounting;
|
||||
private final VehicleCareFinanceClient finance;
|
||||
private final VehicleCareSupplierClient suppliers;
|
||||
private final String token;
|
||||
record Submission(long recordId, long sourceVersion, long recordVersion, String eventId, String requestHash, String payload) { }
|
||||
public VehicleCareFinanceService(VehicleCareMapper records, JdbcTemplate jdbc, ObjectMapper json, VehicleCareFinanceGateway accounting,
|
||||
VehicleCareFinanceClient finance, VehicleCareSupplierClient suppliers,
|
||||
@Value("${baiyue.security.internal-service-token:}") String token) {
|
||||
this.records = records; this.jdbc = jdbc; this.json = json; this.accounting = accounting;
|
||||
this.finance = finance; this.suppliers = suppliers; this.token = token;
|
||||
}
|
||||
@Transactional(isolation=Isolation.READ_COMMITTED)
|
||||
public VehicleCareFinanceModels.Submitted submit(long vehicleId, String number, String operator, VehicleCareFinanceModels.Submit command) {
|
||||
var row = records.lock(vehicleId, number); requireCostRecord(row);
|
||||
String hash = hash(command);
|
||||
var prior = jdbc.query("SELECT * FROM ll_fleet_finance_source WHERE record_id=? AND operator_id=? AND request_key=?",
|
||||
(r, i) -> submission(r), row.getId(), operator, command.idempotencyKey());
|
||||
if (!prior.isEmpty()) {
|
||||
if (!hash.equals(prior.get(0).requestHash())) throw conflict("财务提交标识已用于其他内容");
|
||||
return new VehicleCareFinanceModels.Submitted(number, prior.get(0).sourceVersion(), prior.get(0).recordVersion(), true, "PENDING_FINANCE");
|
||||
}
|
||||
if (!row.getRecordVersion().equals(command.recordVersion())) throw conflict("运维记录已变化,请重新核对后提交");
|
||||
var latest = latest(row.getId());
|
||||
var current = queryFinance(List.of(row)).get(number); // Fail closed if Finance cannot establish current facts.
|
||||
if (latest != null) {
|
||||
if (current == null || current.source().sourceVersion() != latest.sourceVersion()) throw conflict("上次提交尚未完成财务接收,请稍后核对");
|
||||
if (!"VOIDED".equals(current.status())) throw conflict("费用已提交,重新登记须先在财务作废;已付款费用不可改写");
|
||||
} else if (current != null) throw conflict("财务来源已有记录但本地提交关系不一致,请核对");
|
||||
var details = draft(row); var incident = details.incident();
|
||||
if (incident == null || incident.reportedCost() == null || incident.reportedCost().signum() <= 0) throw bad("请先保存正数申报费用;未填写或明确零费用不生成财务应付");
|
||||
requireSupplier(command.supplierId());
|
||||
var converted = accounting.convert(incident.currency(), incident.reportedCost(), command.businessDate());
|
||||
if (converted.settlementAmount().signum() <= 0 || converted.settlementAmount().precision() - converted.settlementAmount().scale() > 15)
|
||||
throw bad("折算金额必须为正数且在财务支持范围内");
|
||||
var source = new VehicleCareFinanceModels.Source(row.getRecordType(), row.getRecordNo(), latest == null ? 1 : latest.sourceVersion() + 1,
|
||||
row.getVehicleId().toString(), row.getVehiclePlate(), null, command.supplierId(), incident.item(), command.businessDate(),
|
||||
converted.originalCurrency(), converted.originalAmount(), converted.exchangeRate(), converted.exchangeRateMonth(), converted.settlementAmount(), details.remark());
|
||||
if (source.feeName() == null || source.feeName().length() > 200) throw bad("提交财务的维修或救援项目须在200字以内");
|
||||
var payload = json.valueToTree(source); String eventId = UUID.randomUUID().toString();
|
||||
String eventKey = EVENT_TYPE + ':' + row.getRecordType() + ':' + number + ':' + source.sourceVersion();
|
||||
var envelope = new EventEnvelope(eventId, EVENT_TYPE, 1, Instant.now(), "0", "COMPANY_VEHICLE_COST",
|
||||
row.getRecordType() + ':' + number, source.sourceVersion(), null, UUID.randomUUID().toString(), eventKey, null,
|
||||
payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload))).requireValid();
|
||||
jdbc.update("INSERT INTO ll_fleet_finance_source(id,record_id,source_version,record_version,operator_id,request_key,request_hash,event_id,payload_json,created_at) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
||||
IdWorker.getId(), row.getId(), source.sourceVersion(), row.getRecordVersion(), operator, command.idempotencyKey(), hash, eventId, write(source), LocalDateTime.now(ZoneOffset.UTC));
|
||||
accounting.publish(envelope);
|
||||
return new VehicleCareFinanceModels.Submitted(number, source.sourceVersion(), row.getRecordVersion(), false, "PENDING_FINANCE");
|
||||
}
|
||||
public List<VehicleCareFinanceModels.Status> statuses(long vehicleId, List<String> numbers) {
|
||||
if (numbers == null || numbers.isEmpty() || numbers.size() > 100 || numbers.stream().anyMatch(n -> n == null || n.isBlank())) throw bad("财务状态查询须提供1至100个有效记录号");
|
||||
var recordsByNo = records.findFinanceRecords(vehicleId, List.copyOf(new LinkedHashSet<>(numbers)));
|
||||
if (recordsByNo.size() != new HashSet<>(numbers).size()) throw missing();
|
||||
var latestById = latest(recordsByNo.stream().map(VehicleCareEntity::getId).toList());
|
||||
var remote = latestById.isEmpty() ? Map.<String, VehicleCareFinanceModels.Cost>of() : queryFinance(recordsByNo.stream().filter(r -> latestById.containsKey(r.getId())).toList());
|
||||
var deliveryKeys = recordsByNo.stream().filter(r -> latestById.containsKey(r.getId()))
|
||||
.map(r -> EVENT_TYPE + ':' + r.getRecordType() + ':' + r.getRecordNo() + ':' + latestById.get(r.getId()).sourceVersion()).toList();
|
||||
var deliveries = accounting.deliveryStates(deliveryKeys);
|
||||
return recordsByNo.stream().map(row -> {
|
||||
var local = latestById.get(row.getId()); if (local == null) return new VehicleCareFinanceModels.Status(row.getRecordNo(), "UNLINKED", null, null, null);
|
||||
var cost = remote.get(row.getRecordNo());
|
||||
if (cost == null || cost.source().sourceVersion() < local.sourceVersion()) {
|
||||
var delivery = deliveries.get(EVENT_TYPE + ':' + row.getRecordType() + ':' + row.getRecordNo() + ':' + local.sourceVersion());
|
||||
if (delivery == null) throw unavailable("财务投递关系缺失,状态尚未确认");
|
||||
return new VehicleCareFinanceModels.Status(row.getRecordNo(), "DEAD".equals(delivery) ? "FINANCE_ATTENTION" : "PENDING_FINANCE", local.sourceVersion(), local.recordVersion(), null);
|
||||
}
|
||||
if (cost.source().sourceVersion() != local.sourceVersion()) throw conflict("财务来源版本与本地提交不一致");
|
||||
if (!hash(cost.source()).equals(hash(read(local.payload(), VehicleCareFinanceModels.Source.class)))) throw conflict("财务来源内容与本地提交快照不一致");
|
||||
return new VehicleCareFinanceModels.Status(row.getRecordNo(), cost.status(), local.sourceVersion(), local.recordVersion(), cost);
|
||||
}).toList();
|
||||
}
|
||||
/** Called under the existing record lock; ordinary processing updates do not depend on Finance. */
|
||||
public void guardFinancialEdit(VehicleCareEntity row, VehicleCareModels.Draft next) {
|
||||
if (!Set.of("REPAIR", "RESCUE").contains(row.getRecordType())) return;
|
||||
var before = draft(row);
|
||||
if (Objects.equals(before.incident().item(), next.incident().item())
|
||||
&& Objects.equals(before.incident().currency(), next.incident().currency())
|
||||
&& equalAmount(before.incident().reportedCost(), next.incident().reportedCost())) return;
|
||||
var latest = latest(row.getId()); if (latest == null) return;
|
||||
var remote = queryFinance(List.of(row)).get(row.getRecordNo());
|
||||
if (remote == null || remote.source().sourceVersion() != latest.sourceVersion()) throw conflict("财务接收状态未确认,暂不能更改费用字段");
|
||||
if (!"VOIDED".equals(remote.status())) throw conflict("已提交费用须先在财务作废,已付款费用禁止改写");
|
||||
}
|
||||
private Map<String, VehicleCareFinanceModels.Cost> queryFinance(List<VehicleCareEntity> rows) {
|
||||
requireToken();
|
||||
try {
|
||||
var keys = rows.stream().map(r -> new VehicleCareFinanceModels.SourceKey(r.getRecordType(), r.getRecordNo())).toList();
|
||||
var response = finance.query(token, new VehicleCareFinanceModels.SourceQuery(keys));
|
||||
if (response == null || !response.successful() || response.data() == null) throw unavailable("财务状态读取失败,未确认入账或付款");
|
||||
var result = new HashMap<String, VehicleCareFinanceModels.Cost>();
|
||||
for (var cost : response.data()) {
|
||||
if (cost == null || cost.source() == null || !"PLATFORM_DEFAULT".equals(cost.subjectCode()) || !"CNY".equals(cost.currency())
|
||||
|| !Set.of("PENDING","APPROVED","PARTIALLY_PAID","PAID","VOIDED").contains(Objects.toString(cost.status(), ""))
|
||||
|| !keys.contains(new VehicleCareFinanceModels.SourceKey(cost.source().sourceType(), cost.source().sourceNo()))
|
||||
|| rows.stream().noneMatch(r -> r.getRecordNo().equals(cost.source().sourceNo()) && r.getVehicleId().toString().equals(cost.source().vehicleId()))
|
||||
|| cost.paidAmount() == null || cost.paidAmount().signum() < 0
|
||||
|| result.put(cost.source().sourceNo(), cost) != null) throw unavailable("财务返回的来源归属或状态无效");
|
||||
}
|
||||
return result;
|
||||
} catch (feign.FeignException error) { throw unavailable("Finance暂不可用,请稍后核对"); }
|
||||
}
|
||||
private void requireSupplier(long id) {
|
||||
requireToken();
|
||||
try { var r = suppliers.get(token, id, true);
|
||||
if (r == null || !r.successful() || r.data() == null || r.data().supplierId() != id || !"ACTIVE".equals(r.data().status())) throw bad("供应商不存在或未启用");
|
||||
} catch (feign.FeignException error) { throw unavailable("供应商主数据暂不可用"); }
|
||||
}
|
||||
private Submission latest(long recordId) { return latest(List.of(recordId)).get(recordId); }
|
||||
private Map<Long, Submission> latest(List<Long> ids) {
|
||||
if (ids.isEmpty()) return Map.of();
|
||||
var rows = jdbc.query("SELECT s.* FROM ll_fleet_finance_source s JOIN (SELECT record_id,MAX(source_version) source_version FROM ll_fleet_finance_source WHERE record_id IN (" + placeholders(ids.size()) + ") GROUP BY record_id) v ON v.record_id=s.record_id AND v.source_version=s.source_version",
|
||||
(r, i) -> submission(r), ids.toArray());
|
||||
var map = new HashMap<Long, Submission>(); rows.forEach(r -> map.put(r.recordId(), r)); return map;
|
||||
}
|
||||
private static Submission submission(java.sql.ResultSet r) throws java.sql.SQLException { return new Submission(r.getLong("record_id"), r.getLong("source_version"), r.getLong("record_version"), r.getString("event_id"), r.getString("request_hash"), r.getString("payload_json")); }
|
||||
private static String placeholders(int count) { return String.join(",", Collections.nCopies(count, "?")); }
|
||||
private VehicleCareModels.Draft draft(VehicleCareEntity row) { return read(row.getDetailsJson(), VehicleCareModels.Draft.class); }
|
||||
private static boolean equalAmount(BigDecimal a, BigDecimal b) { return a == null ? b == null : b != null && a.compareTo(b) == 0; }
|
||||
private String hash(Object value) { return EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(json.valueToTree(value))); }
|
||||
private String write(Object value) { try { return json.writeValueAsString(value); } catch (Exception e) { throw new IllegalStateException("车辆财务来源无法保存", e); } }
|
||||
private <T> T read(String value, Class<T> type) { try { return json.readValue(value, type); } catch (Exception e) { throw new IllegalStateException("车辆财务来源无法读取", e); } }
|
||||
private static void requireCostRecord(VehicleCareEntity row) { if (row == null || !Set.of("REPAIR","RESCUE").contains(row.getRecordType())) throw missing(); }
|
||||
private void requireToken() { if (token == null || token.isBlank()) throw unavailable("内部服务身份未配置"); }
|
||||
private static ResponseStatusException bad(String text) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, text); }
|
||||
private static ResponseStatusException conflict(String text) { return new ResponseStatusException(HttpStatus.CONFLICT, text); }
|
||||
private static ResponseStatusException unavailable(String text) { return new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, text); }
|
||||
private static ResponseStatusException missing() { return new ResponseStatusException(HttpStatus.NOT_FOUND, "当前车辆维修救援记录不存在"); }
|
||||
}
|
||||
+4
@@ -5,6 +5,10 @@ import org.apache.ibatis.annotations.*;
|
||||
|
||||
@Mapper
|
||||
public interface VehicleCareMapper extends BaseMapper<VehicleCareEntity> {
|
||||
@Select({"<script>SELECT * FROM ll_fleet_vehicle_record WHERE vehicle_id=#{vehicleId} AND record_no IN",
|
||||
"<foreach collection='numbers' item='number' open='(' separator=',' close=')'>#{number}</foreach>",
|
||||
"AND record_type IN ('REPAIR','RESCUE')</script>"})
|
||||
java.util.List<VehicleCareEntity> findFinanceRecords(@Param("vehicleId") long vehicleId, @Param("numbers") java.util.List<String> numbers);
|
||||
@Select("SELECT * FROM ll_fleet_vehicle_record WHERE vehicle_id=#{vehicleId} AND record_no=#{recordNo}")
|
||||
VehicleCareEntity find(@Param("vehicleId") long vehicleId, @Param("recordNo") String recordNo);
|
||||
@Select("SELECT * FROM ll_fleet_vehicle_record WHERE vehicle_id=#{vehicleId} AND record_no=#{recordNo} FOR UPDATE")
|
||||
|
||||
+5
-3
@@ -27,9 +27,10 @@ public class VehicleCareService {
|
||||
private final VehicleRepository vehicles;
|
||||
private final VehicleCareFileGateway files;
|
||||
private final ObjectMapper json;
|
||||
private final VehicleCareFinanceService finance;
|
||||
public VehicleCareService(VehicleCareMapper records, VehicleCareHistoryMapper histories,
|
||||
VehicleRepository vehicles, VehicleCareFileGateway files, ObjectMapper json) {
|
||||
this.records = records; this.histories = histories; this.vehicles = vehicles; this.files = files; this.json = json;
|
||||
VehicleRepository vehicles, VehicleCareFileGateway files, ObjectMapper json, VehicleCareFinanceService finance) {
|
||||
this.records = records; this.histories = histories; this.vehicles = vehicles; this.files = files; this.json = json; this.finance = finance;
|
||||
}
|
||||
public PageResult<VehicleCareModels.Response> page(long vehicleId, VehicleCareModels.Query query) {
|
||||
vehicles.findById(vehicleId).orElseThrow(VehicleCareService::missing);
|
||||
@@ -65,6 +66,7 @@ public class VehicleCareService {
|
||||
public VehicleCareModels.Response update(long vehicleId, String number, String operator, VehicleCareModels.Update request) {
|
||||
var row = required(vehicleId, number, true); version(row, request.recordVersion()); validate(request.details());
|
||||
if (!row.getRecordType().equals(request.details().recordType())) throw conflict("已保存记录不能改变运维类型");
|
||||
finance.guardFinancialEdit(row, request.details());
|
||||
if (request.reason() == null || request.reason().isBlank()) throw bad("请填写更正原因");
|
||||
row.setOccurredAt(utc(request.details().occurredAt())); row.setDetailsJson(write(request.details()));
|
||||
persist(row, request.recordVersion(), "UPDATE", request.reason(), operator);
|
||||
@@ -121,7 +123,7 @@ public class VehicleCareService {
|
||||
return new VehicleCareModels.Response(row.getRecordNo(), row.getVehicleId().toString(), row.getVehiclePlate(),
|
||||
read(row.getDetailsJson(), VehicleCareModels.Draft.class), attached(row).stream().map(VehicleCareModels.Attached::file).toList(),
|
||||
row.getRecordVersion(), row.getUpdatedBy(), row.getUpdatedAt().toInstant(ZoneOffset.UTC),
|
||||
Set.of("REPAIR", "RESCUE").contains(row.getRecordType()) ? "UNLINKED" : null);
|
||||
null); // Current financial facts are queried in a separate bounded batch, never frozen into operational history.
|
||||
}
|
||||
private VehicleCareEntity required(long vehicleId, String number, boolean lock) {
|
||||
var row = lock ? records.lock(vehicleId, number) : records.find(vehicleId, number);
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.fleet.maintenance;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@FeignClient(name="warehouse-service", contextId="vehicleCareSupplier", url="${baiyue.clients.warehouse.url:}")
|
||||
public interface VehicleCareSupplierClient {
|
||||
@GetMapping("/internal/warehouse/v1/suppliers/{supplierId}")
|
||||
ApiResponse<Supplier> get(@RequestHeader("X-Internal-Service-Token") String token, @PathVariable long supplierId,
|
||||
@RequestParam boolean requireActive);
|
||||
record Supplier(long supplierId, String name, String category, String status) { }
|
||||
}
|
||||
Reference in New Issue
Block a user