@@ -48,6 +48,8 @@ import cn.nianxx.thhotel.workflows.reservation.service.ReservationTaskCardFieldD
import cn.nianxx.thhotel.workflows.reservation.service.ReservationTaskWorkflowService ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.fasterxml.jackson.databind.node.ArrayNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import java.math.BigDecimal ;
import java.time.LocalDate ;
import java.time.LocalDateTime ;
@@ -79,7 +81,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
private static final String ACTION_OPERA_OPERATION_RETRY = " OPERA_OPERATION_RETRY " ;
private static final String ACTOR_TYPE_USER = " USER " ;
private static final String ACTOR_ID_LOCAL = " local-user " ;
private static final String PAYLOAD_SCHEMA_VERSION = " field_p ath -v1 " ;
private static final String PAYLOAD_SCHEMA_VERSION = " field_m atrix-p0-room-items -v1 " ;
private static final String PAYLOAD_SOURCE_DRAFT = " TASK_DRAFT " ;
private static final String PAYLOAD_SOURCE_CONFIRMATION = " TASK_CONFIRMATION " ;
private static final String PAYLOAD_SOURCE_MANUAL_REVIEW_RESOLUTION = " MANUAL_REVIEW_RESOLUTION " ;
@@ -202,8 +204,14 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
ensureTaskEditable ( task ) ;
JsonNode aiPayload = parseJson ( taskCard . aiPayloadJson ( ) ) ;
Map < String , Object > submittedValues = normalizeFieldValues ( request = = null ? null : request . fieldValues ( ) ) ;
Map < String , Object > draftValues = fieldValuesFromPayloadJs on ( taskCard . draftPayloadJson ( ) ) ;
List < ReservationTaskCardFieldDefinition > allDefinitions =
fieldDefinitionProvider . listDefiniti ons ( task. taskCardType ( ) , task . resultType ( ) ) ;
Map < String , Object > submittedValues = normalizeSubmittedFieldValues (
allDefinitions ,
request = = null ? null : request . fieldValues ( ) ) ;
Map < String , Object > draftValues = normalizeStoredFieldValues (
allDefinitions ,
fieldValuesFromPayloadJson ( taskCard . draftPayloadJson ( ) ) ) ;
Map < String , Object > mergedDraftValues = mergeFieldValues ( draftValues , submittedValues ) ;
List < ReservationTaskCardFieldDefinition > activeDefinitions =
activeDefinitions ( task , aiPayload , mergedDraftValues ) ;
@@ -244,13 +252,19 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
ensureGenericConfirmAllowed ( task , taskCard ) ;
JsonNode aiPayload = parseJson ( taskCard . aiPayloadJson ( ) ) ;
Map < String , Object > submittedValues = normalizeFieldValues ( request = = null ? null : request . fieldValues ( ) ) ;
Map < String , Object > draftValues = fieldValuesFromPayloadJs on ( taskCard . draftPayloadJson ( ) ) ;
List < ReservationTaskCardFieldDefinition > allDefinitions =
fieldDefinitionProvider . listDefiniti ons ( task. taskCardType ( ) , task . resultType ( ) ) ;
Map < String , Object > submittedValues = normalizeSubmittedFieldValues (
allDefinitions ,
request = = null ? null : request . fieldValues ( ) ) ;
Map < String , Object > draftValues = normalizeStoredFieldValues (
allDefinitions ,
fieldValuesFromPayloadJson ( taskCard . draftPayloadJson ( ) ) ) ;
Map < String , Object > editedValues = mergeFieldValues ( draftValues , submittedValues ) ;
List < ReservationTaskCardFieldDefinition > activeDefinitions = activeDefinitions ( task , aiPayload , editedValues ) ;
List < String > validationErrors = validateSubmittedFields ( activeDefinitions , submittedValues ) ;
validationErrors . addAll ( validateDefinitionValues ( task , activeDefinitions , aiPayload , editedValues ,
definitionFieldPaths ( activeDefinitions ) , true ) ) ;
definitionFieldPaths ( activeDefinitions ) , true , true )) ;
validationErrors . addAll ( validateDateRange ( activeDefinitions , aiPayload , editedValues ) ) ;
if ( ! validationErrors . isEmpty ( ) ) {
throw validationError ( validationErrors ) ;
@@ -818,6 +832,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
results . add ( new ManualReviewResolutionFieldOverrideResult (
fieldPointer ,
definition . fieldPath ( ) ,
legacyFieldPathFor ( definition . fieldPath ( ) ) ,
request . value ( ) ) ) ;
}
return results ;
@@ -869,7 +884,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
}
/**
* 将 RFC 6901 JSON Pointer 转换为可能的矩阵 field_path, 兼容数组下标到 [] 的映射 。
* 将 RFC 6901 JSON Pointer 转换为可能的矩阵 field_path, 兼容数组下标到 [] 与旧扁平路径 。
*/
private List < String > fieldPathCandidatesFromPointer ( String fieldPointer ) {
String [ ] rawTokens = fieldPointer . substring ( 1 ) . split ( " / " , - 1 ) ;
@@ -898,22 +913,27 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
if ( ! collapsed . equals ( candidates . get ( 0 ) ) ) {
candidates . add ( collapsed ) ;
}
String legacyAlias = p0Review PointerLegacy Alias ( candidates . get ( 0 ) , collapsed ) ;
if (legacyAlias ! = null & & ! candidates . contains ( legacyAlias ) ) {
candidates . add ( legacyAlias ) ;
for ( String legacyAlias : p0PointerFieldPath Aliases ( candidates . get ( 0 ) , collapsed ) ) {
if ( ! candidates . contains ( legacyAlias ) ) {
candidates . add ( legacyAlias ) ;
}
}
return candidates ;
}
/**
* 兼容 0711 P0 fixtures 中已迁移为数组结构、但当前后端矩阵仍是 扁平字段的 复核 pointer 。
* 兼容 P0 room_items[] 和旧 扁平字段在 复核指针中的双向过渡 。
*/
private String p0Review PointerLegacy Alias ( String plainFieldPath , String collapsedFieldPath ) {
if ( " extracted_fields.room_items.0.pms_room_type_code " . equals ( plainFieldPath )
& & " extracted_fields.room_items[].pms_room_type_code " . equals ( collapsedFieldPath ) ) {
return " extracted_fields.pms_room_type_code " ;
private List < String> p0PointerFieldPath Aliases ( String plainFieldPath , String collapsedFieldPath ) {
String legacyAlias = legacyFieldPathFor ( plainFieldPath ) ;
if ( legacyAlias ! = null ) {
return List . of ( legacyAlias ) ;
}
return null ;
String canonicalAlias = canonicalP0RoomItemFieldPath ( plainFieldPath ) ;
if ( canonicalAlias ! = null ) {
return List . of ( canonicalAlias ) ;
}
return List . of ( ) ;
}
/**
@@ -1053,14 +1073,82 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
}
/**
* 标准化请求中的 field_values。空请求按空草稿处理 。
* 标准化请求中的 field_values。支持前端传矩阵 field_path、RFC 6901 pointer 或旧扁平字段 。
*/
private Map < String , Object > normalizeFieldValues ( Map < String , Object > raw FieldValues ) {
private Map < String , Object > normalizeSubmitted FieldValues (
List < ReservationTaskCardFieldDefinition > definitions ,
Map < String , Object > rawFieldValues ) {
Map < String , Object > normalized = new LinkedHashMap < > ( ) ;
if ( rawFieldValues = = null ) {
return normalized ;
}
rawFieldValues . forEach ( ( fieldPath , value ) - > normalized . put ( fieldPath = = null ? " " : fieldPath . trim ( ) , value ) ) ;
Map < String , ReservationTaskCardFieldDefinition > definitionByPath = definitionByPath ( definitions ) ;
List < String > errors = new ArrayList < > ( ) ;
rawFieldValues . forEach ( ( rawFieldPath , value ) - > {
String fieldPath = rawFieldPath = = null ? " " : rawFieldPath . trim ( ) ;
String canonicalFieldPath = canonicalSubmittedFieldPath ( definitionByPath , fieldPath ) ;
if ( normalized . containsKey ( canonicalFieldPath ) ) {
errors . add ( fieldPath + " : 与其他提交字段指向同一字段 " + canonicalFieldPath + " 。 " ) ;
return ;
}
normalized . put ( canonicalFieldPath , value ) ;
} ) ;
if ( ! errors . isEmpty ( ) ) {
throw validationError ( errors ) ;
}
return normalized ;
}
/**
* 标准化已保存草稿里的历史字段 key, 避免旧 payload 影响 P0 新矩阵校验。
*/
private Map < String , Object > normalizeStoredFieldValues (
List < ReservationTaskCardFieldDefinition > definitions ,
Map < String , Object > storedFieldValues ) {
Map < String , ReservationTaskCardFieldDefinition > definitionByPath = definitionByPath ( definitions ) ;
Map < String , Object > normalized = new LinkedHashMap < > ( ) ;
storedFieldValues . forEach ( ( fieldPath , value ) - > {
String canonicalFieldPath = canonicalSubmittedFieldPath ( definitionByPath , fieldPath ) ;
normalized . put ( canonicalFieldPath , value ) ;
} ) ;
return normalized ;
}
/**
* 构建字段定义索引,供提交字段和复核 pointer 映射使用。
*/
private Map < String , ReservationTaskCardFieldDefinition > definitionByPath (
List < ReservationTaskCardFieldDefinition > definitions ) {
Map < String , ReservationTaskCardFieldDefinition > definitionByPath = new LinkedHashMap < > ( ) ;
definitions . forEach ( definition - > definitionByPath . put ( definition . fieldPath ( ) , definition ) ) ;
return definitionByPath ;
}
/**
* 将提交字段 key 转成当前矩阵主 field_path; 未知字段保留原值, 后续由矩阵校验报错。
*/
private String canonicalSubmittedFieldPath (
Map < String , ReservationTaskCardFieldDefinition > definitionByPath ,
String fieldPathOrPointer ) {
String normalized = trimToNull ( fieldPathOrPointer ) ;
if ( normalized = = null ) {
return " " ;
}
if ( normalized . startsWith ( " / " ) ) {
for ( String candidate : fieldPathCandidatesFromPointer ( normalized ) ) {
if ( definitionByPath . containsKey ( candidate ) ) {
return candidate ;
}
}
return normalized ;
}
if ( definitionByPath . containsKey ( normalized ) ) {
return normalized ;
}
String canonicalAlias = canonicalP0RoomItemFieldPath ( normalized ) ;
if ( canonicalAlias ! = null & & definitionByPath . containsKey ( canonicalAlias ) ) {
return canonicalAlias ;
}
return normalized ;
}
@@ -1240,7 +1328,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
}
/**
* 生成第一版 任务卡 payload JSON。当前按 field_path 保存, 后续 OPERA 转换层再消费 。
* 生成任务卡 payload JSON。field_values 保留矩阵主路径, effective_payload 给 后续 OPERA 映射使用 。
*/
private String toPayloadJson ( String generatedFrom , Map < String , Object > fieldValues , LocalDateTime now ) {
Map < String , Object > payload = new LinkedHashMap < > ( ) ;
@@ -1248,6 +1336,8 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
payload . put ( " generated_from " , generatedFrom ) ;
payload . put ( " generated_at " , now . toString ( ) ) ;
payload . put ( " field_values " , new LinkedHashMap < > ( fieldValues ) ) ;
payload . put ( " legacy_field_values " , legacyFieldValues ( fieldValues ) ) ;
payload . put ( " effective_payload " , effectivePayload ( fieldValues ) ) ;
try {
return objectMapper . writeValueAsString ( payload ) ;
} catch ( Exception exception ) {
@@ -1255,6 +1345,98 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
}
}
/**
* 生成旧扁平字段兼容值,供前端过渡读取;主业务逻辑不再依赖这些 key。
*/
private Map < String , Object > legacyFieldValues ( Map < String , Object > fieldValues ) {
Map < String , Object > legacyValues = new LinkedHashMap < > ( ) ;
fieldValues . forEach ( ( fieldPath , value ) - > {
String legacyFieldPath = legacyFieldPathFor ( fieldPath ) ;
if ( legacyFieldPath ! = null ) {
legacyValues . put ( legacyFieldPath , value ) ;
}
} ) ;
return legacyValues ;
}
/**
* 按矩阵 field_path 生成嵌套 effective payload, 后续 OPERA 参数组装优先消费该结构。
*/
private Object effectivePayload ( Map < String , Object > fieldValues ) {
ObjectNode root = objectMapper . createObjectNode ( ) ;
fieldValues . forEach ( ( fieldPath , value ) - > setEffectivePayloadValue ( root , fieldPath , objectMapper . valueToTree ( value ) ) ) ;
return objectMapper . convertValue ( root , Object . class ) ;
}
/**
* 将单个点号 field_path 写入嵌套 JSON, 支持 room_items.0 这类显式数组下标。
*/
private void setEffectivePayloadValue ( ObjectNode root , String fieldPath , JsonNode valueNode ) {
if ( trimToNull ( fieldPath ) = = null ) {
return ;
}
setEffectivePayloadValue ( root , fieldPath . split ( " \\ . " ) , 0 , valueNode ) ;
}
/**
* 递归写入嵌套 effective payload。
*/
private void setEffectivePayloadValue ( JsonNode current , String [ ] rawParts , int index , JsonNode valueNode ) {
if ( index > = rawParts . length ) {
return ;
}
String part = rawParts [ index ] . replace ( " [] " , " " ) ;
boolean last = index = = rawParts . length - 1 ;
if ( current instanceof ObjectNode objectNode ) {
if ( last ) {
objectNode . set ( part , valueNode ) ;
return ;
}
String nextPart = rawParts [ index + 1 ] . replace ( " [] " , " " ) ;
JsonNode child = objectNode . path ( part ) ;
if ( child . isMissingNode ( ) | | child . isNull ( ) | | ! matchesExpectedContainer ( child , nextPart ) ) {
child = isNumericToken ( nextPart ) ? objectMapper . createArrayNode ( ) : objectMapper . createObjectNode ( ) ;
objectNode . set ( part , child ) ;
}
setEffectivePayloadValue ( child , rawParts , index + 1 , valueNode ) ;
return ;
}
if ( current instanceof ArrayNode arrayNode ) {
if ( ! isNumericToken ( part ) ) {
return ;
}
int arrayIndex = Integer . parseInt ( part ) ;
ensureArraySize ( arrayNode , arrayIndex ) ;
if ( last ) {
arrayNode . set ( arrayIndex , valueNode ) ;
return ;
}
String nextPart = rawParts [ index + 1 ] . replace ( " [] " , " " ) ;
JsonNode child = arrayNode . get ( arrayIndex ) ;
if ( child = = null | | child . isNull ( ) | | ! matchesExpectedContainer ( child , nextPart ) ) {
child = isNumericToken ( nextPart ) ? objectMapper . createArrayNode ( ) : objectMapper . createObjectNode ( ) ;
arrayNode . set ( arrayIndex , child ) ;
}
setEffectivePayloadValue ( child , rawParts , index + 1 , valueNode ) ;
}
}
/**
* 判断当前 JSON 容器是否匹配下一段路径需要的对象或数组。
*/
private boolean matchesExpectedContainer ( JsonNode child , String nextPart ) {
return isNumericToken ( nextPart ) ? child . isArray ( ) : child . isObject ( ) ;
}
/**
* 扩展数组到指定下标,缺位填 null。
*/
private void ensureArraySize ( ArrayNode arrayNode , int index ) {
while ( arrayNode . size ( ) < = index ) {
arrayNode . addNull ( ) ;
}
}
/**
* 将已保存 payload JSON 转为响应对象。空 payload 返回 null, 避免前端误认为已有内容。
*/
@@ -1732,11 +1914,16 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
if ( editedValues . containsKey ( fieldPath ) ) {
return editedValues . get ( fieldPath ) ;
}
for ( String aliasPath : p0ValuePathCandidates ( fieldPath ) ) {
if ( editedValues . containsKey ( aliasPath ) ) {
return editedValues . get ( aliasPath ) ;
}
}
Object value = valueAt ( aiPayload , fieldPath ) ;
if ( value ! = null ) {
return value ;
}
for ( String legacyPath : p0Legacy ValuePathCandidates ( fieldPath ) ) {
for ( String legacyPath : p0ValuePathCandidates ( fieldPath ) ) {
Object legacyValue = valueAt ( aiPayload , legacyPath ) ;
if ( legacyValue ! = null ) {
return legacyValue ;
@@ -1746,9 +1933,9 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
}
/**
* 0711 P0 已将房型字段迁移到 room_items[]; 当前矩阵仍使用扁平字段,读取时做最小别名 兼容。
* 0711 P0 已将房型字段迁移到 room_items[]; 读取时保留旧扁平字段双向 兼容。
*/
private List < String > p0Legacy ValuePathCandidates ( String fieldPath ) {
private List < String > p0ValuePathCandidates ( String fieldPath ) {
String safeFieldPath = fieldPath = = null ? " " : fieldPath ;
return switch ( safeFieldPath ) {
case " extracted_fields.room_type " - > List . of (
@@ -1757,10 +1944,66 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
case " extracted_fields.room_quantity " - > List . of ( " extracted_fields.room_items.0.room_quantity " ) ;
case " extracted_fields.pms_room_type_code " - > List . of (
" extracted_fields.room_items.0.pms_room_type_code " ) ;
case " extracted_fields.room_items.0.room_type_raw " - > List . of (
" extracted_fields.room_type " ,
" extracted_fields.room_items.0.room_type_normalized " ) ;
case " extracted_fields.room_items.0.room_quantity " - > List . of ( " extracted_fields.room_quantity " ) ;
case " extracted_fields.room_items.0.pms_room_type_code " - > List . of (
" extracted_fields.pms_room_type_code " ) ;
default - > List . of ( ) ;
} ;
}
/**
* 旧扁平字段转 P0 room_items[0] 主字段路径。未迁移字段返回 null。
*/
private String canonicalP0RoomItemFieldPath ( String fieldPath ) {
return switch ( fieldPath = = null ? " " : fieldPath ) {
case " extracted_fields.room_quantity " - > " extracted_fields.room_items.0.room_quantity " ;
case " extracted_fields.room_type " - > " extracted_fields.room_items.0.room_type_raw " ;
case " extracted_fields.pms_room_type_code " - > " extracted_fields.room_items.0.pms_room_type_code " ;
default - > null ;
} ;
}
/**
* P0 room_items[0] 主字段转旧扁平字段路径,用于前端兼容显示。
*/
private String legacyFieldPathFor ( String fieldPath ) {
return switch ( fieldPath = = null ? " " : fieldPath ) {
case " extracted_fields.room_items.0.room_quantity " - > " extracted_fields.room_quantity " ;
case " extracted_fields.room_items.0.room_type_raw " - > " extracted_fields.room_type " ;
case " extracted_fields.room_items.0.pms_room_type_code " - > " extracted_fields.pms_room_type_code " ;
default - > null ;
} ;
}
/**
* 将矩阵 field_path 转为 RFC 6901 JSON Pointer, 供前端编辑和复核定位。
*/
private String fieldPointerFor ( String fieldPath ) {
String normalized = trimToNull ( fieldPath ) ;
if ( normalized = = null ) {
return null ;
}
StringBuilder pointer = new StringBuilder ( ) ;
for ( String rawPart : normalized . split ( " \\ . " ) ) {
String part = rawPart . replace ( " [] " , " " ) ;
if ( part . isBlank ( ) ) {
continue ;
}
pointer . append ( '/' ) . append ( escapeJsonPointerToken ( part ) ) ;
}
return pointer . isEmpty ( ) ? null : pointer . toString ( ) ;
}
/**
* 转义 JSON Pointer token。
*/
private String escapeJsonPointerToken ( String token ) {
return token . replace ( " ~ " , " ~0 " ) . replace ( " / " , " ~1 " ) ;
}
/**
* 判断值是否为空。false 和 0 是有效值,不当作空。
*/
@@ -1903,6 +2146,8 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
definition . taskSubtype ( ) ,
definition . displayArea ( ) ,
definition . fieldPath ( ) ,
fieldPointerFor ( definition . fieldPath ( ) ) ,
legacyFieldPathFor ( definition . fieldPath ( ) ) ,
definition . displayName ( ) ,
definition . visible ( ) ,
definition . editable ( ) ,