chore(lastleg): instrument appointment latency from restored 662dead baseline

This commit is contained in:
2026-10-18 11:43:10 +08:00
parent b0acbfbb34
commit 5cd108b39a
35 changed files with 951 additions and 780 deletions
@@ -4,6 +4,7 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.*;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
@@ -60,14 +61,14 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
var requests=remote.subList(start,Math.min(start+200,remote.size())); timing.stage="FIRSTLEG"; requireToken();
try { var response=timing.measure(timing.firstleg,requests.size(),()->firstleg.facts(token,requests));
if(response==null || !response.successful()) throw unavailable("提柜及还空事实读取失败");
append(result,requests,response.data());
run(Stage.FACTS_VALIDATE,()->append(result,requests,response.data()));
} catch(RuntimeException error) { throw unavailable("Firstleg工作事实不可用",error); }
}
groups.forEach((provider,requests)->{
for(int start=0; start<requests.size(); start+=200) {
var batch=requests.subList(start,Math.min(start+200,requests.size()));
var statistics=timing.provider(provider);
try { append(result,batch,timing.measure(statistics,batch.size(),()->provider.read(batch))); }
try { var response=timing.measure(statistics,batch.size(),()->provider.read(batch));run(Stage.FACTS_VALIDATE,()->append(result,batch,response)); }
catch(RuntimeException error) { throw unavailable("预约/收货/运输事实不可用",error); }
}
});
@@ -76,6 +77,7 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
}
/** 一次 facts 调用的计时,不保存原来源或返回值,不跨调用缓存。 */
private static final class FactTiming {
private final Span prepare=span(Stage.FACTS_PREPARE);
private final long started=System.nanoTime();
private final CallStatistics firstleg=new CallStatistics();
private final Map<String,CallStatistics> providers=new LinkedHashMap<>();
@@ -92,20 +94,24 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
}
/** 只统计真正尝试的提供端调用;校验及组装耗时保留在 totalMs 中。 */
private <T> T measure(CallStatistics statistics,int requests,java.util.function.Supplier<T> operation) {
statistics.calls++;statistics.requests+=requests;long begin=System.nanoTime();
try { return operation.get(); }
finally { statistics.elapsed+=System.nanoTime()-begin; }
prepare.close();statistics.calls++;statistics.requests+=requests;long begin=System.nanoTime();
try(var span=span(switch(stage){case "FIRSTLEG"->Stage.FACTS_FIRSTLEG;case "AppointmentWorkReceivingProvider"->Stage.FACTS_RECEIVING;
case "AppointmentWorkDeliveryProvider"->Stage.FACTS_DELIVERY;case "AppointmentWorkTransportProvider"->Stage.FACTS_TRANSPORT;
case "AppointmentWorkDirectTransportProvider"->Stage.FACTS_DIRECT_TRANSPORT;default->Stage.FACTS_OTHER;})){
try{return operation.get();}catch(RuntimeException|Error error){span.failed();throw error;}
}finally{statistics.elapsed+=System.nanoTime()-begin;}
}
/** 成功慢调用或任意失败只输出一次汇总,日志故障不得遮蔽原异常。 */
private void finish(boolean success) {
if(!success)prepare.failed();prepare.close();
long totalMs=java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-started);
if(success && totalMs<250) return;
try {
var summaries=new LinkedHashMap<String,String>();
providers.forEach((name,value)->summaries.put(name,value.summary()));
LOG.warn("Appointment work facts diagnostic: totalMs={}, firstlegCalls={}, firstlegRequests={}, firstlegMs={}, providers={}, failedStage={}",
totalMs,firstleg.calls,firstleg.requests,firstleg.millis(),summaries,success?"NONE":stage);
} catch(RuntimeException diagnosticFailure) { /* 仅忽略诊断故障,原事实错误仍沿原路径抛出。 */ }
LOG.warn("Appointment work facts diagnostic: totalMs={}, firstlegCalls={}, firstlegRequests={}, firstlegMs={}, providers={}, failedStage={}, requestId={}, traceId={}, scopeId={}",
totalMs,firstleg.calls,firstleg.requests,firstleg.millis(),summaries,success?"NONE":stage,org.slf4j.MDC.get("requestId"),org.slf4j.MDC.get("traceId"),currentScopeId());
} catch(Throwable diagnosticFailure) { /* 仅忽略诊断故障,原事实错误仍沿原路径抛出。 */ }
}
}
/** 仅包含数量和单调时钟耗时的累加器。 */
@@ -117,7 +123,7 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
private String summary() { return "calls="+calls+",requests="+requests+",ms="+millis(); }
}
/** 每次生成建立独立读取器;没有实际需求读取时不引入原业务依赖。 */
public RequirementReader openRequirements(List<RequirementInput> inputs) {
public RequirementReader openRequirements() {
return new RequirementReader() {
private RequirementReader reader;
/** 首次实际读取才取得原域作用域,保留每份需求的完整性校验。 */
@@ -125,7 +131,7 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
if(reader==null) {
var provider=requirements.getIfAvailable();
if(provider==null) throw unavailable("原预约需求与批次清单适配尚未接入,不能生成不完整工作清单");
reader=provider.openRequirements(inputs);
reader=provider.openRequirements();
}
var result=reader.read(source,effectiveFlow);
if(result==null || result.isEmpty()) throw unavailable("原预约需求清单缺失,不能以空清单完成路线生成");
@@ -28,7 +28,7 @@ public interface AppointmentWorkFactGateway {
SourcePage discover(String after, int size);
ContainerSource container(String containerBookingId);
/** 建立本次原预约需求读取作用域,保持完整批次及明确替代关系。 */
RequirementReader openRequirements(List<RequirementInput> inputs);
RequirementReader openRequirements();
Map<String, Fact> facts(List<AppointmentWorkState> states);
Owner owner(String id, Actor actor);
Responsibility supplier(String id);
@@ -43,6 +43,5 @@ public interface AppointmentWorkFactGateway {
interface SupplierDirectory { Responsibility activeSupplier(String id); }
/** 单次生成显式复用目录,不跨请求保存来源事实。 */
interface RequirementReader { List<Requirement> read(ContainerSource source,Flow effectiveFlow); }
record RequirementInput(ContainerSource source,Flow flow) { }
interface RequirementProvider { RequirementReader openRequirements(List<RequirementInput> inputs); }
interface RequirementProvider { RequirementReader openRequirements(); }
}
@@ -9,6 +9,7 @@ import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
@@ -26,10 +27,16 @@ public class AppointmentWorkQueryService {
Snapshot snapshot(Actor actor) {
return snapshot(actor,s->true,true);
}
/** 按原顺序构造完整快照,计时只分离读取、筛选、事实及投影。 */
private Snapshot snapshot(Actor actor,Predicate<AppointmentWorkState> selected,boolean includeNext) {
observeTransaction();var timing=span(Stage.QUERY_GUARD);
try {
String scopeVersion=store.requireInitialized();
timing.close();timing=span(Stage.QUERY_DISCOVER);
if(!Objects.equals(scopeVersion,facts.discover(null,1).scopeVersion())) throw conflict("已开船工作范围变化,请先完成服务端同步");
timing.close();timing=span(Stage.QUERY_STORE);
var all=store.all();
timing.close();timing=span(Stage.QUERY_SELECT);
var stateByKey=all.stream().collect(Collectors.toMap(AppointmentWorkState::getWorkItemKey,Function.identity()));
var selectedKeys=all.stream().filter(s->!s.isRetired() && selected.test(s)).map(AppointmentWorkState::getWorkItemKey).collect(Collectors.toSet());
var needed=new HashSet<>(selectedKeys);
@@ -41,12 +48,16 @@ public class AppointmentWorkQueryService {
}
}
var states=all.stream().filter(s->!s.isRetired() && needed.contains(s.getWorkItemKey())).toList();
timing.close();timing=span(Stage.QUERY_FACTS);
var current=facts.facts(states);
timing.close();timing=span(Stage.QUERY_PROJECT);
var visible=states.stream().filter(s->canRead(s,current.get(s.getWorkItemKey()),actor)).toList();
var items=visible.stream().map(s->project(s,current.get(s.getWorkItemKey()),actor,clock)).toList();
var byKey=items.stream().collect(Collectors.toMap(WorkItem::workItemKey,Function.identity()));
timing.close();timing=span(Stage.QUERY_NEXT);
return new Snapshot(all,current,items.stream().filter(i->selectedKeys.contains(i.workItemKey()))
.map(i->includeNext?withNext(i,next(i,byKey,stateByKey,new HashSet<>())):i).toList());
} catch(RuntimeException|Error error){timing.failed();throw error;} finally{timing.close();}
}
private Action next(WorkItem item,Map<String,WorkItem> items,Map<String,AppointmentWorkState> states,Set<String> visited) {
if(!visited.add(item.workItemKey()) || item.reviewRequired()) return item.nextAction();
@@ -73,23 +84,34 @@ public class AppointmentWorkQueryService {
}
return item.nextAction();
}
/** 完整快照之后单独统计汇总耗时,不改变待办筛选语义。 */
public Summary summary(Actor actor) {
var items=snapshot(actor,s->true,false).items().stream().filter(i->i.executionStatus()!=ExecutionStatus.COMPLETED).toList();
var snapshotItems=snapshot(actor,s->true,false).items();
return measure(Stage.QUERY_FINAL,()->{
var items=snapshotItems.stream().filter(i->i.executionStatus()!=ExecutionStatus.COMPLETED).toList();
return new Summary(items.size(),items.stream().filter(i->followToday(i,clock)).count(),items.stream().filter(i->!i.overdueReasons().isEmpty()).count(),items.stream().filter(i->i.calendarDate()==null).count(),items.stream().filter(WorkItem::reviewRequired).count());
});
}
/** 日历最终分组保持原顺序,单独记录快照后处理。 */
public CalendarResult calendar(LocalDate from,LocalDate to,Flow flow,Set<Type> types,Actor actor) {
if(from==null || to==null || to.isBefore(from) || ChronoUnit.DAYS.between(from,to)>=62) throw bad("日历范围须为1至62天");
if(types!=null && types.contains(Type.ROUTE)) throw bad("路线待确认需求请从全局待办查看");
var items=snapshot(actor,s->s.getType()!=Type.ROUTE && matches(s,flow,types,null),false).items().stream().filter(i->i.type()!=Type.ROUTE && i.calendarDate()!=null && !i.calendarDate().isBefore(from) && !i.calendarDate().isAfter(to))
var snapshotItems=snapshot(actor,s->s.getType()!=Type.ROUTE && matches(s,flow,types,null),false).items();
return measure(Stage.QUERY_FINAL,()->{
var items=snapshotItems.stream().filter(i->i.type()!=Type.ROUTE && i.calendarDate()!=null && !i.calendarDate().isBefore(from) && !i.calendarDate().isAfter(to))
.filter(i->flow==null || i.flow()==flow).filter(i->types==null || types.contains(i.type())).toList();
var groups=items.stream().collect(Collectors.groupingBy(WorkItem::calendarDate,TreeMap::new,Collectors.toList()));
var days=groups.entrySet().stream().map(e->new DayAggregate(e.getKey(),e.getValue().size(),e.getValue().stream().collect(Collectors.groupingBy(WorkItem::type,()->new EnumMap<>(Type.class),Collectors.counting())),counts(e.getValue()))).toList();
return new CalendarResult(from,to,items.size(),counts(items),days);
});
}
/** 保留事实核对之后的最终过滤排序分页,仅拆出其耗时。 */
public PageResult<WorkItem> items(View view,int page,int size,LocalDate date,Flow flow,Set<Type> types,String containerId,Bucket bucket,String keyword,Actor actor) {
if(page<1 || size<1 || size>100 || keyword!=null && keyword.length()>128) throw bad("分页或搜索条件无效");
var word=keyword==null?"":keyword.trim().toLowerCase(Locale.ROOT);
var items=snapshot(actor,s->matches(s,flow,types,containerId) && candidate(s,view),true).items().stream()
var snapshotItems=snapshot(actor,s->matches(s,flow,types,containerId) && candidate(s,view),true).items();
return measure(Stage.QUERY_FINAL,()->{
var items=snapshotItems.stream()
.filter(i->date!=null || i.executionStatus()!=ExecutionStatus.COMPLETED)
.filter(i->date==null || date.equals(i.calendarDate()))
.filter(i->flow==null || flow==i.flow()).filter(i->types==null || types.contains(i.type()))
@@ -100,6 +122,7 @@ public class AppointmentWorkQueryService {
.sorted(Comparator.comparing(WorkItem::followupDate,Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(WorkItem::workItemKey)).toList();
long offset=(long)(page-1)*size; int start=(int)Math.min(offset,items.size());
return new PageResult<>(items.subList(start,Math.min(start+size,items.size())),page,size,items.size(),(items.size()+size-1L)/size);
});
}
public BatchWorkResolution resolveBatch(String batchKey,String customerId,Actor actor) {
if(batchKey==null || !batchKey.matches("(?:FBA_APPOINTMENT|DISPATCH):[1-9][0-9]*")) throw bad("请提供原业务返回的完整批次键");
@@ -117,10 +140,12 @@ public class AppointmentWorkQueryService {
var selected=candidates.get(0);
return new BatchWorkResolution(batchKey,selected.customerId(),detail(selected.workItemKey(),actor),candidates);
}
/** 保留详情完整快照与退休事实读取,历史查询单独观察。 */
public WorkDetail detail(String key,Actor actor) {
var snapshot=snapshot(actor); var state=store.get(key);
var snapshot=snapshot(actor); var state=measure(Stage.DETAIL_STORE,()->store.get(key));
var fact=snapshot.facts().get(key);
if(state.isRetired() && state.getType()!=Type.ROUTE) fact=facts.facts(List.of(state)).get(key);
if(state.isRetired() && state.getType()!=Type.ROUTE) fact=measure(Stage.DETAIL_FACTS,()->facts.facts(List.of(state))).get(key);
try(var timing=span(Stage.DETAIL_PROJECT)) {
if(!canRead(state,fact,actor)) throw forbidden("无权读取关联来源");
var item=state.isRetired()?project(state,fact,actor,clock):snapshot.items().stream().filter(i->i.workItemKey().equals(key)).findFirst().orElseThrow(()->missing("事项不存在"));
var relatedKeys=new HashSet<>(state.getPrerequisites()); relatedKeys.addAll(state.getReplacementKeys());
@@ -128,9 +153,10 @@ public class AppointmentWorkQueryService {
relatedKeys.addAll(effectiveKeys(relatedKeys,statesByKey));
snapshot.states().stream().filter(s->effectiveKeys(s.getPrerequisites(),statesByKey).contains(key)).map(AppointmentWorkState::getWorkItemKey).forEach(relatedKeys::add);
var refs=new ArrayList<>(state.getReferences()); if(fact!=null) refs.addAll(fact.references());
return new WorkDetail(item,snapshot.items().stream().filter(i->relatedKeys.contains(i.workItemKey())).toList(),refs.stream().distinct().toList(),store.history(key),
return new WorkDetail(item,snapshot.items().stream().filter(i->relatedKeys.contains(i.workItemKey())).toList(),refs.stream().distinct().toList(),measure(Stage.DETAIL_HISTORY,()->store.history(key)),
state.isReviewRequired()?new Review(state.getReviewVersion(),List.of(Objects.toString(state.getReviewReason(),"关联安排变化"))):null,
requirementOwned(state,fact)?new RequirementDetails(state.isBookingRequired(),Objects.toString(state.getReceivingRequirements(),""),state.getRequirementReason()):null,state.getReturnHandoff());
}
}
WorkItem current(AppointmentWorkState state,Fact fact,Actor actor) { return project(state,fact,actor,clock); }
private boolean matches(AppointmentWorkState state,Flow flow,Set<Type> types,String containerId) {
@@ -7,6 +7,7 @@ import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.time.*;
import java.util.*;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
@@ -21,13 +22,14 @@ public class AppointmentWorkStore {
public void invalidateRequirements() {
long started = System.nanoTime();
try {
if (mapper.invalidateRequirements() != 1) throw unavailable("工作事项同步守卫未初始化");
if (measure(Stage.GUARD_UPDATE,mapper::invalidateRequirements) != 1) throw unavailable("工作事项同步守卫未初始化");
} finally {
long elapsedMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
if (elapsedMs >= 250) {
// 语句耗时可能包含等锁或连接等待,不能仅凭日志认定具体锁根因;不记录请求业务载荷。
LOG.warn("Appointment work guard update slow: elapsedMs={}, requestId={}, traceId={}",
elapsedMs, MDC.get("requestId"), MDC.get("traceId"));
try { LOG.warn("Appointment work guard update slow: elapsedMs={}, requestId={}, traceId={}, scopeId={}",
elapsedMs, MDC.get("requestId"), MDC.get("traceId"), currentScopeId());
} catch(Throwable ignored) { /* 诊断输出失败不得覆盖原守卫结果或异常。 */ }
}
}
}
@@ -35,20 +37,23 @@ public class AppointmentWorkStore {
public AppointmentWorkMapper.Guard lock() {
long started = System.nanoTime();
try {
var guard = mapper.lockGuard();
observeTransaction();
var guard = measure(Stage.GUARD_LOCK,mapper::lockGuard);
if (guard == null) throw unavailable("工作事项DDL尚未集成");
return guard;
} finally {
long elapsedMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
if (elapsedMs >= 250) {
// 耗时包含连接及语句执行,单条日志不能区分具体等待原因,不记录业务载荷。
LOG.warn("Appointment work guard lock slow: elapsedMs={}, requestId={}, traceId={}",
elapsedMs, MDC.get("requestId"), MDC.get("traceId"));
try { LOG.warn("Appointment work guard lock slow: elapsedMs={}, requestId={}, traceId={}, scopeId={}",
elapsedMs, MDC.get("requestId"), MDC.get("traceId"), currentScopeId());
} catch(Throwable ignored) { /* 诊断输出失败不得覆盖原守卫结果或异常。 */ }
}
}
}
/** 完整性守卫保持原语义,读取耗时与事务完成只作诊断。 */
public String requireInitialized() {
var guard=mapper.guard();
observeTransaction();var guard=measure(Stage.GUARD_READ,mapper::guard);
if(guard==null || !guard.isInitialized()) throw unavailable("完整工作范围尚未完成同步,不能显示零项");
if(guard.getRequirementVersion()!=guard.getSynchronizedRequirementVersion()) throw conflict("预约批次或运输关联已变化,服务端正在协调完整工作清单");
return guard.getScopeVersion();
@@ -8,6 +8,7 @@ import java.time.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
@@ -23,7 +24,7 @@ public class AppointmentWorkSyncService {
@Transactional
public SyncResult synchronize(SyncRequest request) {
if(request==null || request.size()<1 || request.size()>100) throw bad("同步批次须为1至100柜");
var timing=new SyncTiming();boolean success=false;
observeTransaction();var timing=new SyncTiming();boolean success=false;
try {
var guard=store.lock();
if(request.afterContainerBookingId()!=null && !Objects.equals(guard.getScanCursor(),request.afterContainerBookingId())) throw conflict("来源同步游标已变化,请从首批重新核对");
@@ -36,16 +37,7 @@ public class AppointmentWorkSyncService {
timing.next(SyncPhase.GENERATE);
// 保留全部历史与稳定键顺序;逐柜更新立即进入本次工作集,不跨事务缓存。
var known=workingSet();
var requirementInputs=page.containers().stream().filter(source->{
// 坏来源仍由下面原顺序校验并报错,不能在后页原料提示阶段抢先覆盖前柜错误。
try { validateSource(source);return true; } catch(RuntimeException invalid) { return false; }
}).map(source->{
var flow=known.values().stream().filter(s->source.containerBookingId().equals(s.getContainerBookingId()) || s.getContainers().stream().anyMatch(c->source.containerBookingId().equals(c.containerBookingId())))
.filter(s->s.getType()!=Type.ROUTE && !s.isRetired()).map(AppointmentWorkState::getFlow).findFirst().orElse(source.establishedFlow());
return new RequirementInput(source,flow);
}).filter(input->input.flow()!=Flow.UNKNOWN).toList();
// 提示仅用于原料批量读取,下面逐柜 existing/activeFlow 仍根据最新工作集决定。
var requirementReader=gateway.openRequirements(requirementInputs);
var requirementReader=gateway.openRequirements();
int created=0; var ids=new HashSet<String>();
for(var source:page.containers()) {
validateSource(source);
@@ -97,6 +89,8 @@ public class AppointmentWorkSyncService {
private enum SyncPhase { GUARD, DISCOVER, GENERATE, TRANSPORT_FACTS, RECONCILE, ACTIVE_STORE, ACTIVE_FACTS, OBSERVE_CHECKPOINT }
/** 仅统计本次方法执行,不含事务代理提交,不能当作精确锁持有时间。 */
private static final class SyncTiming {
private final Span body=span(Stage.SYNC_BODY);
private Span diagnosticPhase=span(Stage.SYNC_GUARD);
private final long started=System.nanoTime();
private final long[] elapsed=new long[SyncPhase.values().length];
private long phaseStarted=started;
@@ -106,19 +100,24 @@ public class AppointmentWorkSyncService {
/** 完成前一阶段后开始下一阶段;事实阶段包含紧邻的数据库回读。 */
private void next(SyncPhase next) {
long now=System.nanoTime();elapsed[phase.ordinal()]+=now-phaseStarted;phaseStarted=now;phase=next;
diagnosticPhase.close();diagnosticPhase=span(switch(next){
case GUARD->Stage.SYNC_GUARD;case DISCOVER->Stage.SYNC_DISCOVER;case GENERATE->Stage.SYNC_GENERATE;
case TRANSPORT_FACTS->Stage.SYNC_TRANSPORT_FACTS;case RECONCILE->Stage.SYNC_RECONCILE;
case ACTIVE_STORE->Stage.SYNC_ACTIVE_STORE;case ACTIVE_FACTS->Stage.SYNC_ACTIVE_FACTS;case OBSERVE_CHECKPOINT->Stage.SYNC_CHECKPOINT;});
}
/** 成功和失败共用一条慢调用摘要,未到达的计数保持为负一。 */
private void finish(boolean success) {
long now=System.nanoTime();elapsed[phase.ordinal()]+=now-phaseStarted;
if(!success){diagnosticPhase.failed();body.failed();}diagnosticPhase.close();body.close();
long totalMs=java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(now-started);
if(totalMs<250) return;
try {
// 旧 activeFactsMs 仍包含工作表读取、解析及适配;新增明细不能改变旧指标口径。
long activeMs=java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(elapsed[SyncPhase.ACTIVE_STORE.ordinal()]+elapsed[SyncPhase.ACTIVE_FACTS.ordinal()]);
LOG.warn("Appointment work sync slow: totalMs={}, guardMs={}, discoverMs={}, generateMs={}, transportFactsMs={}, reconcileMs={}, activeFactsMs={}, activeStoreMs={}, activeAdapterMs={}, observeCheckpointMs={}, pageContainers={}, transportFactsCount={}, activeFactsCount={}, complete={}, failedStage={}",
LOG.warn("Appointment work sync slow: totalMs={}, guardMs={}, discoverMs={}, generateMs={}, transportFactsMs={}, reconcileMs={}, activeFactsMs={}, activeStoreMs={}, activeAdapterMs={}, observeCheckpointMs={}, pageContainers={}, transportFactsCount={}, activeFactsCount={}, complete={}, failedStage={}, requestId={}, traceId={}, scopeId={}",
totalMs,millis(SyncPhase.GUARD),millis(SyncPhase.DISCOVER),millis(SyncPhase.GENERATE),millis(SyncPhase.TRANSPORT_FACTS),millis(SyncPhase.RECONCILE),activeMs,millis(SyncPhase.ACTIVE_STORE),millis(SyncPhase.ACTIVE_FACTS),millis(SyncPhase.OBSERVE_CHECKPOINT),
pageContainers,transportFactsCount,activeFactsCount,complete,success?"NONE":phase.name());
} catch(RuntimeException diagnosticFailure) { /* 诊断失败不能替换原业务异常或阻断提交。 */ }
pageContainers,transportFactsCount,activeFactsCount,complete,success?"NONE":phase.name(),org.slf4j.MDC.get("requestId"),org.slf4j.MDC.get("traceId"),currentScopeId());
} catch(Throwable diagnosticFailure) { /* 诊断失败不能替换原业务异常或阻断提交。 */ }
}
/** 阶段仅输出整数毫秒,不携带业务标识。 */
private long millis(SyncPhase value) { return java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(elapsed[value.ordinal()]); }
@@ -140,7 +139,7 @@ public class AppointmentWorkSyncService {
}
/** 命令入口仍在调用方守卫事务中即时加载工作集。 */
List<String> generate(ContainerSource source,Flow flow,List<AppointmentWorkState> existing,Actor actor) {
return generate(source,flow,existing,actor,workingSet(),gateway.openRequirements(List.of(new RequirementInput(source,flow))));
return generate(source,flow,existing,actor,workingSet(),gateway.openRequirements());
}
/** 与持久层全量读取保持稳定键顺序,并保留退役事项及原读取上限。 */
private Map<String,AppointmentWorkState> workingSet() {
@@ -0,0 +1,35 @@
package com.baiyue.lastleg.appointmentwork.diagnostics;
import com.zaxxer.hikari.HikariDataSource;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.config.BeanPostProcessor;
import javax.sql.DataSource;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
/** 仅观察原Hikari getConnection;同池同连接,unwrap保持容器bean self契约而非隐藏target身份。 */
final class AppointmentWorkDataSourcePostProcessor implements BeanPostProcessor {
/** 不识别或已有代理的数据源保持原对象,明确记录未覆盖而不主动探测连接。 */
@Override public Object postProcessAfterInitialization(Object bean,String name) {
if(!(bean instanceof DataSource))return bean;
if(!(bean instanceof HikariDataSource) || AopUtils.isAopProxy(bean)) {
warnUnavailable();return bean;
}
try {
var factory=new ProxyFactory(bean);factory.setProxyTargetClass(true);
factory.addAdvice((MethodInterceptor)invocation->{
if(!invocation.getMethod().getName().equals("getConnection") || !active())return invocation.proceed();
try(var span=span(Stage.CONNECTION_ACQUIRE)) {
try{return invocation.proceed();}catch(Throwable error){span.failed();throw error;}
}
});
return factory.getProxy(bean.getClass().getClassLoader());
} catch(RuntimeException failure) { warnUnavailable();return bean; }
}
/** 不输出 Bean 名、数据源属性或异常内容。 */
private static void warnUnavailable() {
try{org.slf4j.LoggerFactory.getLogger(AppointmentWorkDataSourcePostProcessor.class).warn("Appointment connection diagnostic unavailable: unsupported or already proxied datasource");}
catch(Throwable ignored){ /* 日志故障不改变 Bean 注册。 */ }
}
}
@@ -0,0 +1,39 @@
package com.baiyue.lastleg.appointmentwork.diagnostics;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
/** 仅测量服务端同步 filter dispatch,不代表客户端总耗时或异步响应完成时间。 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE+10)
public class AppointmentWorkDiagnosticFilter extends OncePerRequestFilter {
private static final String BASE="/api/platform/v1/last-leg/appointment-work";
/** 仅识别固定模板;路径参数、query string和载荷不进入上下文。 */
static Route route(String method,String path){
if("GET".equals(method)){
if((BASE+"/summary").equals(path))return Route.SUMMARY;
if((BASE+"/calendar").equals(path))return Route.CALENDAR;
if((BASE+"/items").equals(path))return Route.ITEMS;
if((BASE+"/items/resolve").equals(path))return Route.RESOLVE;
if(path!=null && path.matches(BASE+"/items/[^/]+"))return Route.DETAIL;
}
if("POST".equals(method) && path!=null && path.matches(BASE+"/items/[^/]+/commands"))return Route.COMMAND;
return null;
}
/** 不采集其他端点,也不为诊断增加权限判断。 */
@Override protected boolean shouldNotFilter(HttpServletRequest request){return route(request.getMethod(),request.getRequestURI())==null;}
/** 创建及清理线程作用域;不覆写上游相关标识、状态、响应或异常。 */
@Override protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throws ServletException,IOException{
Scope scope=null;
try{scope=open(Origin.HTTP,route(request.getMethod(),request.getRequestURI()));}catch(RuntimeException ignored){ /* 无诊断仍执行原请求。 */ }
try{chain.doFilter(request,response);}
catch(ServletException|IOException|RuntimeException|Error error){if(scope!=null)scope.failed();throw error;}
finally{if(scope!=null){try{scope.status(response.getStatus());}catch(Throwable ignored){ /* 响应诊断读取失败不替换原异常。 */ }finally{scope.close();}}}
}
}
@@ -0,0 +1,128 @@
package com.baiyue.lastleg.appointmentwork.diagnostics;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.*;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
/** 仅在当前线程的预约调用内累加固定计时桶,不保存业务参数或跨请求状态。 */
public final class AppointmentWorkDiagnostics {
public enum Origin { HTTP, QUERY, COMMAND, SYNC }
public enum Route { INTERNAL, SUMMARY, CALENDAR, ITEMS, RESOLVE, DETAIL, COMMAND }
public enum Stage {
TRANSACTION_ADVISOR, QUERY_GUARD, QUERY_DISCOVER, QUERY_STORE, QUERY_SELECT, QUERY_FACTS,
QUERY_PROJECT, QUERY_NEXT, QUERY_FINAL, DETAIL_STORE, DETAIL_FACTS, DETAIL_PROJECT, DETAIL_HISTORY,
GUARD_LOCK, GUARD_READ, GUARD_UPDATE, FACTS_PREPARE, FACTS_FIRSTLEG, FACTS_RECEIVING,
FACTS_DELIVERY, FACTS_TRANSPORT, FACTS_DIRECT_TRANSPORT, FACTS_OTHER, FACTS_VALIDATE,
DELIVERY_SOURCE_MAP, DELIVERY_CARGO, DELIVERY_SNAPSHOTS, DELIVERY_ROWS, DELIVERY_RECEIVING,
DELIVERY_IMPACTS, DELIVERY_ASSEMBLY, SYNC_BODY, SYNC_GUARD, SYNC_DISCOVER, SYNC_GENERATE,
SYNC_TRANSPORT_FACTS, SYNC_RECONCILE, SYNC_ACTIVE_STORE, SYNC_ACTIVE_FACTS, SYNC_CHECKPOINT, CONNECTION_ACQUIRE,
DB_EXECUTOR_QUERY, DB_EXECUTOR_UPDATE, DB_PREPARE, DB_STATEMENT_QUERY, DB_STATEMENT_UPDATE, DB_RESULT_MAPPING
}
public enum StatementBucket { NONE, GUARD_LOCK, GUARD_READ, GUARD_UPDATE, WORK_STATE, WORK_HISTORY, DELIVERY, RECEIVING, OTHER }
public record Metric(long calls,long inclusiveNanos,long selfNanos,long failures,long firstOffsetNanos,long lastOffsetNanos) { }
public record Snapshot(String scopeId,String parentScopeId,Origin origin,Route route,String requestId,String traceId,
long elapsedNanos,boolean failed,int status,Map<String,Metric> metrics,boolean transactionObserved,int completionStatus,long completionOffsetNanos,long startedNanos) { }
private static final org.slf4j.Logger LOG=LoggerFactory.getLogger(AppointmentWorkDiagnostics.class);
private static final ThreadLocal<Scope> CURRENT=new ThreadLocal<>();
private static final Span NO_SPAN=new Span(null,null,null,0);
/** 诊断工具无需实例化。 */
private AppointmentWorkDiagnostics() { }
/** 新建独立作用域;关闭时恢复调用方原作用域,不改变 MDC。 */
public static Scope open(Origin origin,Route route) { return new Scope(origin,route,System::nanoTime); }
/** 返回纯诊断标识,缺少作用域时明确为空,不生成业务关联键。 */
public static String currentScopeId() { var scope=CURRENT.get();return scope==null?null:scope.id; }
/** 无作用域时所有阶段计时均为空操作。 */
public static boolean active() { return CURRENT.get()!=null; }
/** 只监听既有事务完成,不开启事务或改变其同步、隔离级别及连接属性。 */
public static void observeTransaction() {
try {
var scope=CURRENT.get();
if(scope==null || scope.transactionObserved || !org.springframework.transaction.support.TransactionSynchronizationManager.isSynchronizationActive()
|| !org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive())return;
org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(new org.springframework.transaction.support.TransactionSynchronization(){
/** 回调发生在原事务完成后,仅记录相对时刻;不声称这是数据库精确释放锁时刻。 */
@Override public void afterCompletion(int status){try{scope.completionStatus=status;scope.completionOffsetNanos=Math.max(0,scope.clock.getAsLong()-scope.started);}catch(Throwable ignored){}}
});
scope.transactionObserved=true;
}catch(Throwable ignored){ /* 诊断监听失败不能改变事务。 */ }
}
/** 将可信相关标识限制为诊断字符集;任意文本不进入日志。 */
private static String correlation(String value) { return value!=null && value.matches("[A-Za-z0-9_-]{1,80}")?value:null; }
/** 固定阶段开始计时;最多保留64层,避免异常递归造成诊断状态膨胀。 */
public static Span span(Stage stage) { return span(stage,StatementBucket.NONE); }
/** 数据库阶段只接受固定语句桶,不记录 SQL、参数或动态 Mapper 名称。 */
public static Span span(Stage stage,StatementBucket bucket) {
try {
var scope=CURRENT.get();if(scope==null || scope.stack.size()>=64)return NO_SPAN;
var span=new Span(scope,stage,bucket,scope.clock.getAsLong());scope.stack.push(span);return span;
} catch(RuntimeException ignored) { return NO_SPAN; }
}
/** 保留返回对象和原异常;测量只包围原有调用一次。 */
public static <T> T measure(Stage stage,Supplier<T> operation) {
try(var span=span(stage)) { try{return operation.get();}catch(RuntimeException|Error error){span.failed();throw error;} }
}
/** 无返回值调用沿相同路径执行,不增加重试或业务判断。 */
public static void run(Stage stage,Runnable operation) { measure(stage,()->{operation.run();return null;}); }
/** 读取当前统计仅用于固定诊断摘要和隔离验证。 */
static Snapshot currentSnapshot() { var scope=CURRENT.get();return scope==null?null:scope.snapshot(); }
public static final class Scope implements AutoCloseable {
private final Scope previous;
private final String id=UUID.randomUUID().toString();
private final Origin origin;private final Route route;private final LongSupplier clock;
private final String requestId,traceId;private final long started;
private final Deque<Span> stack=new ArrayDeque<>();
private final Map<String,long[]> totals=new LinkedHashMap<>();
private boolean failed,closed,transactionObserved;private int status,completionStatus=-1;private long completionOffsetNanos=-1;
/** 包内时钟注入仅供确定性验证,不修改全局时钟。 */
Scope(Origin origin,Route route,LongSupplier clock) {
this.previous=CURRENT.get();this.origin=origin;this.route=route;this.clock=clock;this.started=clock.getAsLong();
this.requestId=correlation(MDC.get("requestId"));this.traceId=correlation(MDC.get("traceId"));CURRENT.set(this);
}
/** 只标记失败,不保存异常类型以外的业务内容。 */
public void failed() { failed=true; }
/** HTTP状态由容器响应读取,业务code不从载荷解析。 */
public void status(int status) { this.status=status; }
/** 快照为累计已结束阶段;self仅扣同scope直接子段,独立子scope由parentScopeId关联且不可叠加。 */
public Snapshot snapshot() {
var metrics=new LinkedHashMap<String,Metric>();totals.forEach((key,v)->metrics.put(key,new Metric(v[0],v[1],v[2],v[3],v[4],v[5])));
return new Snapshot(id,previous==null?null:previous.id,origin,route,requestId,traceId,Math.max(0,clock.getAsLong()-started),failed,status,Collections.unmodifiableMap(metrics),transactionObserved,completionStatus,completionOffsetNanos,started);
}
/** 只有慢调用或失败输出汇总;清理优先,日志异常不得覆盖原业务异常。 */
@Override public void close() {
if(closed)return;closed=true;
try {
if(CURRENT.get()==this){if(previous==null)CURRENT.remove();else CURRENT.set(previous);}
var s=snapshot();if(!s.failed() && s.status()<500 && s.elapsedNanos()<250_000_000L)return;
// startedNanos及offset只可在同一JVM进程比较,不是数据库锁持有者或跨进程时钟。
LOG.warn("Appointment attribution: scopeId={}, parentScopeId={}, origin={}, route={}, requestId={}, traceId={}, startedNanos={}, elapsedNs={}, failed={}, httpStatus={}, connectionCoverage={}, transactionObserved={}, completionStatus={}, completionOffsetNs={}, boundary=SERVER_DISPATCH_OR_ADVISOR, stages={}",
s.scopeId(),s.parentScopeId(),s.origin(),s.route(),s.requestId(),s.traceId(),s.startedNanos(),s.elapsedNanos(),s.failed(),s.status(),
s.metrics().containsKey(Stage.CONNECTION_ACQUIRE.name())?"OBSERVED":"NOT_OBSERVED",s.transactionObserved(),s.completionStatus(),s.completionOffsetNanos(),s.metrics());
} catch(Throwable ignored) { /* 诊断设施故障不影响返回、原异常或线程清理。 */ }
}
}
public static final class Span implements AutoCloseable {
private final Scope scope;private final Stage stage;private final StatementBucket bucket;private final long started;
private long children;private boolean failed,closed;
/** 保存常量阶段,不引用任何业务对象。 */
private Span(Scope scope,Stage stage,StatementBucket bucket,long started){this.scope=scope;this.stage=stage;this.bucket=bucket;this.started=started;}
/** 失败计数只作为耗时解释,不改变原异常。 */
public void failed(){if(scope!=null){failed=true;scope.failed=true;}}
/** 每个阶段只结束一次;父段self扣除子段inclusive,不能把两种口径相加。 */
@Override public void close(){
if(scope==null || closed)return;closed=true;
try{
long elapsed=Math.max(0,scope.clock.getAsLong()-started);
if(scope.stack.peek()!=this)return;
scope.stack.pop();if(!scope.stack.isEmpty())scope.stack.peek().children+=elapsed;
var key=stage.name()+(bucket==StatementBucket.NONE?"":":"+bucket.name());
var v=scope.totals.computeIfAbsent(key,ignored->new long[]{0,0,0,0,Math.max(0,started-scope.started),0});v[0]++;v[1]+=elapsed;v[2]+=Math.max(0,elapsed-children);if(failed)v[3]++;
v[4]=Math.min(v[4],Math.max(0,started-scope.started));v[5]=Math.max(v[5],Math.max(0,started+elapsed-scope.started));
}catch(Throwable ignored){ /* 诊断计数故障不能替换原结果。 */ }
}
}
}
@@ -0,0 +1,47 @@
package com.baiyue.lastleg.appointmentwork.diagnostics;
import com.baiyue.lastleg.appointmentwork.*;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.Ordered;
import java.lang.reflect.Method;
import java.util.Set;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
/** 使用现有 Spring 代理设施观察事务边界,不新增 AspectJ 或改变事务属性。 */
@Configuration(proxyBeanMethods=false)
public class AppointmentWorkDiagnosticsConfiguration {
/** 基础设施角色确保事务自动代理器纳入此 Advisor;顺序早于默认事务 Advisor。 */
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor appointmentWorkDiagnosticAdvisor() {
var pointcut=new StaticMethodMatcherPointcut() {
/** 仅匹配预约直接入口,不拦截其他模块或内部辅助方法。 */
@Override public boolean matches(Method method,Class<?> targetClass) {
return AppointmentWorkSyncService.class.isAssignableFrom(targetClass) && method.getName().equals("synchronize")
|| AppointmentWorkCommandService.class.isAssignableFrom(targetClass) && method.getName().equals("execute")
|| AppointmentWorkQueryService.class.isAssignableFrom(targetClass) && Set.of("summary","calendar","items","resolveBatch","detail").contains(method.getName());
}
};
MethodInterceptor interceptor=invocation->{
boolean sync=invocation.getThis() instanceof AppointmentWorkSyncService;
Origin origin=sync?Origin.SYNC:invocation.getThis() instanceof AppointmentWorkCommandService?Origin.COMMAND:Origin.QUERY;
Scope owned=null;
try { if(sync || !active())owned=open(origin,Route.INTERNAL); } catch(RuntimeException ignored) { /* 无法计时仍执行原方法。 */ }
try(var span=span(Stage.TRANSACTION_ADVISOR)) {
try{return invocation.proceed();}catch(Throwable error){span.failed();if(owned!=null)owned.failed();throw error;}
} finally { if(owned!=null)owned.close(); }
};
var advisor=new DefaultPointcutAdvisor(pointcut,interceptor);advisor.setOrder(Ordered.HIGHEST_PRECEDENCE+100);return advisor;
}
/** 静态 Bean 避免提早实例化业务依赖,只对既有 Hikari 实例增加委托代理。 */
@Bean public static BeanPostProcessor appointmentWorkDataSourceDiagnostics() { return new AppointmentWorkDataSourcePostProcessor(); }
/** MyBatis 按现有插件入口注册,不注册 Mapper、执行 SQL 或改变缓存配置。 */
@Bean public AppointmentWorkMyBatisDiagnostics appointmentWorkMyBatisDiagnostics() { return new AppointmentWorkMyBatisDiagnostics(); }
}
@@ -0,0 +1,51 @@
package com.baiyue.lastleg.appointmentwork.diagnostics;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.*;
import java.sql.*;
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
/** query 为含映射的 inclusive 调用;结果读取另记子段,不推断为纯服务器锁等待。 */
@Intercepts({
@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),
@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,CacheKey.class,BoundSql.class}),
@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}),
@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class}),
@Signature(type=StatementHandler.class,method="query",args={Statement.class,ResultHandler.class}),
@Signature(type=StatementHandler.class,method="update",args={Statement.class}),
@Signature(type=ResultSetHandler.class,method="handleResultSets",args={Statement.class})
})
public final class AppointmentWorkMyBatisDiagnostics implements Interceptor {
private static final ThreadLocal<StatementBucket> BUCKET=new ThreadLocal<>();
/** 所有调用只 proceed 一次,异常原样返回;固定桶上下文在 finally 恢复。 */
@Override public Object intercept(Invocation invocation)throws Throwable {
if(!active())return invocation.proceed();
var previous=BUCKET.get();boolean executor=invocation.getTarget() instanceof Executor;
var bucket=previous==null?StatementBucket.OTHER:previous;
if(executor){bucket=bucket(((MappedStatement)invocation.getArgs()[0]).getId());BUCKET.set(bucket);}
Stage stage=executor?(invocation.getMethod().getName().equals("query")?Stage.DB_EXECUTOR_QUERY:Stage.DB_EXECUTOR_UPDATE)
:invocation.getTarget() instanceof ResultSetHandler?Stage.DB_RESULT_MAPPING
:switch(invocation.getMethod().getName()){case "prepare"->Stage.DB_PREPARE;case "query"->Stage.DB_STATEMENT_QUERY;default->Stage.DB_STATEMENT_UPDATE;};
try(var span=span(stage,bucket)){
try{return invocation.proceed();}catch(Throwable error){span.failed();throw error;}
}finally{if(executor){if(previous==null)BUCKET.remove();else BUCKET.set(previous);}}
}
/** 只有明确白名单语句有独立桶,其他名字一律合入 OTHER,绝不打印原 ID。 */
private static StatementBucket bucket(String id){
return switch(id){
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.lockGuard"->StatementBucket.GUARD_LOCK;
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.guard"->StatementBucket.GUARD_READ;
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.invalidateRequirements","com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.saveGuard"->StatementBucket.GUARD_UPDATE;
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.all","com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.get"->StatementBucket.WORK_STATE;
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.selectHistory"->StatementBucket.WORK_HISTORY;
case "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDeliveryMapper.batches","com.baiyue.lastleg.inboundplan.work.AppointmentWorkDeliveryMapper.customer","com.baiyue.lastleg.inboundplan.work.AppointmentWorkDeliveryMapper.privateBatch"->StatementBucket.DELIVERY;
default->StatementBucket.OTHER;
};
}
}