From ad1e3d238fd6ffb1b98882c26128c954edc23add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E6=97=BB=E5=B3=B0?= <1441170195@qq.com> Date: Mon, 21 Sep 2026 16:46:59 +0800 Subject: [PATCH] feat:Add check in API --- pom.xml | 2 +- .../opsapp/common/config/WebFilterConfig.java | 20 +- .../audit/dtos/EsignAuditLogResponse.java | 51 -- .../esign/audit/models/EsignAuditLog.java | 73 -- .../repositories/EsignAuditLogRepository.java | 16 - .../audit/services/EsignAuditLogService.java | 240 ------ .../support/ThirdPartyAuditDetailBuilder.java | 92 -- .../opsapp/esign/common/EsignApiPaths.java | 26 - .../common/EsignLegacyApiProperties.java | 17 - .../common/dtos/EsignSimpleResponse.java | 29 - .../dtos/OhipProfileFieldCatalogResponse.java | 38 - .../common/dtos/OhipProfileFieldDef.java | 26 - .../common/dtos/SignFieldOhipMappingItem.java | 23 - .../common/filters/EsignAccessLogFilter.java | 104 --- .../EsignLegacyApiDeprecationFilter.java | 70 -- .../mappers/SignFieldOhipValueMapper.java | 207 ----- .../common/support/EsignLogMaskUtil.java | 103 --- .../support/EsignReservationParamUtil.java | 24 - .../support/OhipProfileFieldCatalog.java | 135 --- .../support/ProfileDetailsJsonReader.java | 188 ----- .../EsignReservationTaskCompleteRequest.java | 20 - .../EsignSignFieldOhipMappingResponse.java | 48 -- .../EsignSignFieldOhipMappingSaveRequest.java | 16 - .../EsignSignFieldOhipPreviewRequest.java | 20 - .../EsignSignFieldOhipPreviewResponse.java | 48 -- .../dtos/GuestProfileIdResponse.java | 50 -- .../reservation/dtos/GuestProfileRef.java | 9 - .../dtos/HospitalityOAuthSettings.java | 32 - .../reservation/dtos/OAuthTokenResponse.java | 21 - .../dtos/ReservationLookupHit.java | 5 - .../dtos/ReservationThirdPartyResult.java | 45 - .../SignedRegistrationCardSubmitRequest.java | 44 - .../SignedRegistrationCardUploadResult.java | 72 -- .../models/EsignReservationTask.java | 45 - .../models/EsignSignFieldOhipConfig.java | 34 - .../EsignReservationTaskRepository.java | 24 - .../EsignSignFieldOhipConfigRepository.java | 13 - .../services/EsignReservationTaskService.java | 105 --- .../EsignSignFieldOhipMappingService.java | 194 ----- .../services/ReservationThirdPartyClient.java | 733 ---------------- .../HospitalityOAuthTokenConfigurer.java | 91 -- .../HospitalityOAuthTokenSingleton.java | 240 ------ .../support/OperaFlexLinkPageBuilder.java | 797 ------------------ .../support/ProfileDetailsJsonBuilder.java | 353 -------- .../RegistrationConfirmationExtractor.java | 94 --- .../ReservationDetailResponseParser.java | 138 --- .../ReservationLookupResponseParser.java | 103 --- src/main/resources/application-dev.yml | 5 + src/main/resources/application-local.yml | 19 +- 49 files changed, 28 insertions(+), 4874 deletions(-) delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/audit/dtos/EsignAuditLogResponse.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/audit/models/EsignAuditLog.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/audit/repositories/EsignAuditLogRepository.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/audit/services/EsignAuditLogService.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/audit/support/ThirdPartyAuditDetailBuilder.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/EsignApiPaths.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/EsignLegacyApiProperties.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/dtos/EsignSimpleResponse.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldCatalogResponse.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldDef.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/dtos/SignFieldOhipMappingItem.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignAccessLogFilter.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignLegacyApiDeprecationFilter.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/mappers/SignFieldOhipValueMapper.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignLogMaskUtil.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignReservationParamUtil.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/support/OhipProfileFieldCatalog.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/common/support/ProfileDetailsJsonReader.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignReservationTaskCompleteRequest.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingResponse.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingSaveRequest.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewRequest.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewResponse.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileIdResponse.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileRef.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/HospitalityOAuthSettings.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/OAuthTokenResponse.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationLookupHit.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationThirdPartyResult.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardSubmitRequest.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardUploadResult.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignReservationTask.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignSignFieldOhipConfig.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignReservationTaskRepository.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignSignFieldOhipConfigRepository.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationTaskService.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignSignFieldOhipMappingService.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/services/ReservationThirdPartyClient.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenConfigurer.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenSingleton.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/support/OperaFlexLinkPageBuilder.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ProfileDetailsJsonBuilder.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/support/RegistrationConfirmationExtractor.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationDetailResponseParser.java delete mode 100644 src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationLookupResponseParser.java diff --git a/pom.xml b/pom.xml index 21e95dc..8cb34d0 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 - 21 + 24 1.5.20 ${java.version} ${java.version} diff --git a/src/main/java/com/sjmresorts/opsapp/common/config/WebFilterConfig.java b/src/main/java/com/sjmresorts/opsapp/common/config/WebFilterConfig.java index 0857f9c..8e10ab9 100644 --- a/src/main/java/com/sjmresorts/opsapp/common/config/WebFilterConfig.java +++ b/src/main/java/com/sjmresorts/opsapp/common/config/WebFilterConfig.java @@ -69,20 +69,20 @@ public class WebFilterConfig { "/api/v1/logging/auditLog/**", "/api/v1/logging/apiLog/**", "/api/v1/logs/**", - "/api/v1/esign/capture-logs/**", - "/api/v1/esign/auth/**", - "/api/v1/esign/**", - "/esign-api/api/v1/esign/**", - "**/api/v1/esign/**", + "/api/v1/checkin/capture-logs/**", + "/api/v1/checkin/auth/**", + "/api/v1/checkin/**", + "/esign-api/api/v1/checkin/**", + "**/api/v1/checkin/**", "/api/v1/sinosecu/auth/**", "/api/v1/sinosecu/**", "/esign-api/api/v1/sinosecu/**", "**/api/v1/sinosecu/**", - "/api/v1/esign/ios-capture-submit", - "/api/v1/esign/ios-pending-tasks", - "/api/v1/esign/ios-task-list", - "/api/v1/esign/ios-task-sign", - "/api/v1/esign/ios-task/*", + "/api/v1/checkin/ios-capture-submit", + "/api/v1/checkin/ios-pending-tasks", + "/api/v1/checkin/ios-task-list", + "/api/v1/checkin/ios-task-sign", + "/api/v1/checkin/ios-task/*", "/api/v1/sinosecu/ios-capture-submit", "/api/v1/sinosecu/ios-pending-tasks", "/api/v1/sinosecu/ios-task-list", diff --git a/src/main/java/com/sjmresorts/opsapp/esign/audit/dtos/EsignAuditLogResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/audit/dtos/EsignAuditLogResponse.java deleted file mode 100644 index be7d900..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/audit/dtos/EsignAuditLogResponse.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.sjmresorts.opsapp.esign.audit.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "e-sign 審計日誌列表") -public class EsignAuditLogResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignAuditLogData data; - - @Data - @NoArgsConstructor - public static class EsignAuditLogData { - private long total; - private int page; - private int size; - private List logs; - } - - @Data - @NoArgsConstructor - public static class EsignAuditLogItem { - private String logId; - private String logCategory; - private String eventType; - private String message; - private String operatorName; - private Long userId; - private String taskId; - private String fileName; - private String agentSource; - private String hotelId; - private String httpMethod; - private String requestUrl; - private Integer responseStatus; - private Long durationMs; - private String clientIp; - private String detailJson; - private String createdAt; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/audit/models/EsignAuditLog.java b/src/main/java/com/sjmresorts/opsapp/esign/audit/models/EsignAuditLog.java deleted file mode 100644 index ef4bc90..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/audit/models/EsignAuditLog.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.sjmresorts.opsapp.esign.audit.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Lob; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_audit_logs") -public class EsignAuditLog { - - @Id - @Column(name = "log_id", nullable = false, length = 36) - private String logId; - - @Column(name = "log_category", nullable = false, length = 32) - private String logCategory; - - @Column(name = "event_type", nullable = false, length = 64) - private String eventType; - - @Column(name = "message", length = 512) - private String message; - - @Column(name = "operator_name", length = 128) - private String operatorName; - - @Column(name = "user_id") - private Long userId; - - @Column(name = "task_id", length = 36) - private String taskId; - - @Column(name = "file_name", length = 512) - private String fileName; - - @Column(name = "agent_source", length = 256) - private String agentSource; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "http_method", length = 16) - private String httpMethod; - - @Column(name = "request_url", length = 1024) - private String requestUrl; - - @Column(name = "response_status") - private Integer responseStatus; - - @Column(name = "duration_ms") - private Long durationMs; - - @Column(name = "client_ip", length = 64) - private String clientIp; - - @Lob - @Column(name = "detail_json") - private String detailJson; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/audit/repositories/EsignAuditLogRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/audit/repositories/EsignAuditLogRepository.java deleted file mode 100644 index 8a111e9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/audit/repositories/EsignAuditLogRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.esign.audit.repositories; - -import com.sjmresorts.opsapp.esign.audit.models.EsignAuditLog; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface EsignAuditLogRepository extends JpaRepository { - - Page findByLogCategoryOrderByCreatedAtDesc(String logCategory, Pageable pageable); - - Page findByLogCategoryAndEventTypeOrderByCreatedAtDesc( - String logCategory, String eventType, Pageable pageable); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/audit/services/EsignAuditLogService.java b/src/main/java/com/sjmresorts/opsapp/esign/audit/services/EsignAuditLogService.java deleted file mode 100644 index 1441f95..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/audit/services/EsignAuditLogService.java +++ /dev/null @@ -1,240 +0,0 @@ -package com.sjmresorts.opsapp.esign.audit.services; - -import com.sjmresorts.opsapp.esign.audit.dtos.EsignAuditLogResponse; -import com.sjmresorts.opsapp.esign.audit.models.EsignAuditLog; -import com.sjmresorts.opsapp.esign.audit.repositories.EsignAuditLogRepository; -import jakarta.annotation.Resource; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Service("esignAuditLogService") -public class EsignAuditLogService { - - public static final String CATEGORY_OPERATION = "OPERATION"; - public static final String CATEGORY_THIRD_PARTY = "THIRD_PARTY"; - public static final String CATEGORY_CONNECTION = "CONNECTION"; - - public static final String EVT_WS_CONNECTED = "WS_CONNECTED"; - public static final String EVT_WS_DISCONNECTED = "WS_DISCONNECTED"; - public static final String EVT_POLL_FAILED = "POLL_FAILED"; - public static final String EVT_POLL_RECOVERED = "POLL_RECOVERED"; - public static final String EVT_WS_ERROR = "WS_ERROR"; - - /** USB 直传(PC Agent ↔ App,PDF 不经后端;仅审计元数据) */ - public static final String EVT_USB_LINK_UP = "USB_LINK_UP"; - public static final String EVT_USB_LINK_DOWN = "USB_LINK_DOWN"; - public static final String EVT_USB_PUSH_QUEUED = "USB_PUSH_QUEUED"; - public static final String EVT_USB_PUSH_SENT = "USB_PUSH_SENT"; - public static final String EVT_USB_PUSH_ACK = "USB_PUSH_ACK"; - public static final String EVT_USB_PUSH_RECEIVED = "USB_PUSH_RECEIVED"; - public static final String EVT_USB_PUSH_OPENED = "USB_PUSH_OPENED"; - public static final String EVT_USB_PUSH_FAILED = "USB_PUSH_FAILED"; - - public static final String EVT_OHIP_GET_REGISTRATION = "OHIP_GET_REGISTRATION"; - public static final String EVT_OHIP_GET_RESERVATIONS = "OHIP_GET_RESERVATIONS"; - public static final String EVT_OHIP_POST_ATTACHMENT = "OHIP_POST_ATTACHMENT"; - public static final String EVT_OHIP_POST_PROFILE_FIELDS = "OHIP_POST_PROFILE_FIELDS"; - public static final String EVT_OHIP_PUT_PROFILE = "OHIP_PUT_PROFILE"; - - @Resource(name = "esignAuditLogRepository") - private EsignAuditLogRepository esignAuditLogRepository; - - @Transactional - public void appendOperation(String eventType, String message, String operatorName, String detailJson) { - append(CATEGORY_OPERATION, eventType, message, operatorName, null, null, null, null, null, - null, null, null, null, null, detailJson); - } - - @Transactional - public void appendCmsOperation( - String httpMethod, - String requestUri, - int responseStatus, - long durationMs, - String operatorName, - String clientIp) { - String eventType = resolveOperationEventType(httpMethod, requestUri); - String message = String.format("%s %s → HTTP %d (%dms)", httpMethod, requestUri, responseStatus, durationMs); - append(CATEGORY_OPERATION, eventType, message, operatorName, null, null, null, null, null, - httpMethod, requestUri, responseStatus, durationMs, clientIp, null); - } - - @Transactional - public void appendThirdParty( - String eventType, - String httpMethod, - String requestUrl, - Integer responseStatus, - long durationMs, - String hotelId, - String message, - String detailJson) { - append(CATEGORY_THIRD_PARTY, eventType, message, null, null, null, null, null, hotelId, - httpMethod, requestUrl, responseStatus, durationMs, null, detailJson); - } - - @Transactional - public void appendConnection( - String eventType, - Long userId, - String message, - String transport, - String clientIp, - String detailJson) { - String detail = detailJson; - if (StringUtils.isNotBlank(transport) && StringUtils.isBlank(detailJson)) { - detail = "{\"transport\":\"" + escapeJson(transport) + "\"}"; - } - append(CATEGORY_CONNECTION, eventType, message, null, userId, null, null, null, null, - null, null, null, null, clientIp, detail); - } - - private void append( - String category, - String eventType, - String message, - String operatorName, - Long userId, - String taskId, - String fileName, - String agentSource, - String hotelId, - String httpMethod, - String requestUrl, - Integer responseStatus, - Long durationMs, - String clientIp, - String detailJson) { - if (StringUtils.isBlank(category) || StringUtils.isBlank(eventType)) { - return; - } - EsignAuditLog row = new EsignAuditLog(); - row.setLogId(UUID.randomUUID().toString()); - row.setLogCategory(category.trim()); - row.setEventType(eventType.trim()); - row.setMessage(trim(message, 512)); - row.setOperatorName(trim(operatorName, 128)); - row.setUserId(userId); - row.setTaskId(trim(taskId, 36)); - row.setFileName(trim(fileName, 512)); - row.setAgentSource(trim(agentSource, 256)); - row.setHotelId(trim(hotelId, 64)); - row.setHttpMethod(trim(httpMethod, 16)); - row.setRequestUrl(trim(requestUrl, 1024)); - row.setResponseStatus(responseStatus); - row.setDurationMs(durationMs); - row.setClientIp(trim(clientIp, 64)); - row.setDetailJson(detailJson); - row.setCreatedAt(Instant.now()); - esignAuditLogRepository.save(row); - } - - @Transactional(readOnly = true) - public EsignAuditLogResponse listLogs(String category, int page, int size, String eventType) { - int safePage = Math.max(page, 0); - int safeSize = size > 0 ? Math.min(size, 200) : 50; - PageRequest pageable = PageRequest.of(safePage, safeSize); - Page result; - String cat = StringUtils.trimToEmpty(category); - if (StringUtils.isBlank(cat)) { - cat = CATEGORY_OPERATION; - } - if (StringUtils.isNotBlank(eventType)) { - result = esignAuditLogRepository.findByLogCategoryAndEventTypeOrderByCreatedAtDesc( - cat, eventType.trim(), pageable); - } else { - result = esignAuditLogRepository.findByLogCategoryOrderByCreatedAtDesc(cat, pageable); - } - - List logs = new ArrayList<>(); - for (EsignAuditLog row : result.getContent()) { - EsignAuditLogResponse.EsignAuditLogItem item = - new EsignAuditLogResponse.EsignAuditLogItem(); - item.setLogId(row.getLogId()); - item.setLogCategory(row.getLogCategory()); - item.setEventType(row.getEventType()); - item.setMessage(row.getMessage()); - item.setOperatorName(row.getOperatorName()); - item.setUserId(row.getUserId()); - item.setTaskId(row.getTaskId()); - item.setFileName(row.getFileName()); - item.setAgentSource(row.getAgentSource()); - item.setHotelId(row.getHotelId()); - item.setHttpMethod(row.getHttpMethod()); - item.setRequestUrl(row.getRequestUrl()); - item.setResponseStatus(row.getResponseStatus()); - item.setDurationMs(row.getDurationMs()); - item.setClientIp(row.getClientIp()); - item.setDetailJson(row.getDetailJson()); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - logs.add(item); - } - - EsignAuditLogResponse resp = new EsignAuditLogResponse(); - EsignAuditLogResponse.EsignAuditLogData data = - new EsignAuditLogResponse.EsignAuditLogData(); - data.setTotal(result.getTotalElements()); - data.setPage(safePage); - data.setSize(safeSize); - data.setLogs(logs); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("ok"); - resp.setData(data); - return resp; - } - - private static String resolveOperationEventType(String method, String uri) { - if (uri == null) { - return "CMS_MUTATION"; - } - if (uri.contains("/printer-device-link/review")) { - return "CMS_DEVICE_LINK_REVIEW"; - } - if (uri.contains("/printer-push-rules")) { - return "CMS_PUSH_RULES"; - } - if (uri.contains("/printer-pdf-task-push") || uri.contains("/printer-pdf-task-quick-push")) { - return "CMS_PDF_PUSH"; - } - if (uri.contains("/ios-printer-pdf-task-update")) { - return "CMS_PDF_UPDATE"; - } - if (uri.contains("/hub-promotions")) { - return "CMS_HUB_PROMOTION"; - } - if (uri.contains("/hub-settings")) { - return "CMS_HUB_SETTINGS"; - } - if (uri.contains("/surveys")) { - return "CMS_SURVEY"; - } - if (uri.contains("/printer-push-groups")) { - return "CMS_PUSH_GROUP"; - } - return method + " " + uri; - } - - private static String escapeJson(String s) { - return StringUtils.defaultString(s).replace("\\", "\\\\").replace("\"", "\\\""); - } - - private static String trim(String s, int max) { - if (s == null) { - return null; - } - String t = s.trim(); - if (t.isEmpty()) { - return null; - } - return t.length() > max ? t.substring(0, max) : t; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/audit/support/ThirdPartyAuditDetailBuilder.java b/src/main/java/com/sjmresorts/opsapp/esign/audit/support/ThirdPartyAuditDetailBuilder.java deleted file mode 100644 index 485dd34..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/audit/support/ThirdPartyAuditDetailBuilder.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.sjmresorts.opsapp.esign.audit.support; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.common.dtos.httpclient.HttpResponseResult; -import com.sjmresorts.opsapp.common.utils.SimpleHttpClient; -import org.apache.commons.lang3.StringUtils; - -/** - * 第三方审计 detail_json:结构化 request / response,便于 CMS 查看。 - */ -public final class ThirdPartyAuditDetailBuilder { - - private static final int MAX_TEXT_CHARS = 16_000; - - private ThirdPartyAuditDetailBuilder() {} - - public static String build(SimpleHttpClient.RequestBuilder request, HttpResponseResult response) { - JSONObject root = new JSONObject(); - if (request != null) { - root.put("request", buildRequest(request)); - } - root.put("response", buildResponse(response)); - return root.toJSONString(); - } - - private static JSONObject buildRequest(SimpleHttpClient.RequestBuilder request) { - JSONObject obj = new JSONObject(); - obj.put("method", request.getMethod()); - obj.put("url", request.getRequestUrlWithQuery()); - obj.put("headers", SimpleHttpClient.maskHeadersForLog(request.getHeaders())); - String body = request.getBodyContent(); - if (StringUtils.isNotBlank(body)) { - obj.put("body", sanitizeRequestBody(body)); - } - return obj; - } - - private static JSONObject buildResponse(HttpResponseResult response) { - JSONObject obj = new JSONObject(); - if (response == null) { - return obj; - } - obj.put("status", response.getStatusCode()); - if (response.isTransportError()) { - obj.put("transportError", StringUtils.defaultIfBlank(response.getErrorMessage(), "transport error")); - } - String body = response.getResponseContent(); - if (StringUtils.isNotBlank(body)) { - obj.put("body", parseJsonOrText(truncate(body))); - } - return obj; - } - - /** 请求体中的 PDF base64 不落库全文,只保留长度。 */ - private static Object sanitizeRequestBody(String body) { - try { - JSONObject json = JSON.parseObject(body); - if (json.containsKey("fileAttachment")) { - String att = json.getString("fileAttachment"); - JSONObject redacted = new JSONObject(); - redacted.put("_redacted", "base64"); - redacted.put("length", att != null ? att.length() : 0); - json.put("fileAttachment", redacted); - } - return json; - } catch (Exception e) { - return parseJsonOrText(truncate(body)); - } - } - - private static Object parseJsonOrText(String text) { - if (StringUtils.isBlank(text)) { - return null; - } - try { - return JSON.parse(text); - } catch (Exception e) { - return text; - } - } - - private static String truncate(String value) { - if (value == null) { - return null; - } - if (value.length() <= MAX_TEXT_CHARS) { - return value; - } - return value.substring(0, MAX_TEXT_CHARS) + "...(truncated)"; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/EsignApiPaths.java b/src/main/java/com/sjmresorts/opsapp/esign/common/EsignApiPaths.java deleted file mode 100644 index b69f9b3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/EsignApiPaths.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sjmresorts.opsapp.esign.common; - -import java.time.LocalDate; - -/** - * E-Sign HTTP API 路径常量;{@link #LEGACY_PREFIX} 计划于 {@link #LEGACY_SUNSET_DATE} 下线。 - */ -public final class EsignApiPaths { - - public static final String PREFIX = "/api/v1/esign"; - public static final String LEGACY_PREFIX = "/api/v1/sinosecu"; - - /** 兼容路径计划移除日期(含当日仍可用,次日仅返回 410) */ - public static final LocalDate LEGACY_SUNSET_DATE = LocalDate.of(2026, 9, 30); - - public static final String DEPRECATION_HEADER = "Deprecation"; - public static final String SUNSET_HEADER = "Sunset"; - public static final String LINK_HEADER = "Link"; - - private EsignApiPaths() { - } - - public static boolean isLegacyPath(String uri) { - return uri != null && uri.contains(LEGACY_PREFIX); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/EsignLegacyApiProperties.java b/src/main/java/com/sjmresorts/opsapp/esign/common/EsignLegacyApiProperties.java deleted file mode 100644 index 94c0bf7..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/EsignLegacyApiProperties.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.esign.common; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Data -@Component -@ConfigurationProperties(prefix = "ops-app.esign.legacy-api") -public class EsignLegacyApiProperties { - - /** 是否响应 /api/v1/sinosecu/** 请求;false 时返回 410 Gone */ - private boolean enabled = true; - - /** 是否在响应头附加 Deprecation / Sunset 警告 */ - private boolean warnHeaders = true; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/EsignSimpleResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/EsignSimpleResponse.java deleted file mode 100644 index 8968627..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/EsignSimpleResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.dtos; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class EsignSimpleResponse { - - private boolean success; - private String code; - private String message; - - public static EsignSimpleResponse ok(String message) { - EsignSimpleResponse r = new EsignSimpleResponse(); - r.setSuccess(true); - r.setCode("200"); - r.setMessage(message != null ? message : "ok"); - return r; - } - - public static EsignSimpleResponse fail(String code, String message) { - EsignSimpleResponse r = new EsignSimpleResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldCatalogResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldCatalogResponse.java deleted file mode 100644 index 15c34ae..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldCatalogResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class OhipProfileFieldCatalogResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private CatalogData data; - - @Data - @NoArgsConstructor - public static class CatalogData { - private List groups; - private List fields; - private List commonFields; - } - - public static OhipProfileFieldCatalogResponse ok( - List groups, List fields, List commonFields) { - OhipProfileFieldCatalogResponse r = new OhipProfileFieldCatalogResponse(); - r.setMessage("ok"); - CatalogData data = new CatalogData(); - data.setGroups(groups); - data.setFields(fields); - data.setCommonFields(commonFields); - r.setData(data); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldDef.java b/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldDef.java deleted file mode 100644 index 205f349..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/OhipProfileFieldDef.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "OHIP profileDetails 固定字段定义") -public class OhipProfileFieldDef { - - @Schema(description = "稳定字段代码,存入 CMS 映射配置") - private String code; - - @Schema(description = "profileDetails 内路径") - private String path; - - @Schema(description = "分组:personNamePrimary / customer / identification …") - private String group; - - @Schema(description = "是否列入 CMS 下拉「常用字段」") - private Boolean common; - - private String labelEn; - private String labelZh; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/SignFieldOhipMappingItem.java b/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/SignFieldOhipMappingItem.java deleted file mode 100644 index 860ee51..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/dtos/SignFieldOhipMappingItem.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.dtos; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class SignFieldOhipMappingItem { - - /** App 签名字段名,如 field_1、field_2 */ - private String fieldName; - - /** CMS 备注 / 显示名 */ - private String label; - - /** 固定 OHIP 字段代码,见 {@link com.sjmresorts.opsapp.esign.common.support.OhipProfileFieldCatalog} */ - private String ohipFieldCode; - - /** text | signature;signature 通常仅 PDF 附件,text 可映射到 OHIP 字段 */ - private String valueType; - - private Boolean enabled; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignAccessLogFilter.java b/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignAccessLogFilter.java deleted file mode 100644 index 99cfdb0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignAccessLogFilter.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.filters; - -import com.sjmresorts.opsapp.esign.common.support.EsignLogMaskUtil; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.util.ContentCachingRequestWrapper; -import org.springframework.web.util.ContentCachingResponseWrapper; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -/** Esign 请求/响应摘要日志(脱敏,不含 Base64)。 */ -@Slf4j -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -public class EsignAccessLogFilter extends OncePerRequestFilter { - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String uri = request.getRequestURI(); - if (uri == null || (!uri.contains("/esign/") && !uri.contains("/sinosecu/"))) { - return true; - } - // SSE 长连接不能被 ContentCachingResponseWrapper 缓冲 - return uri.contains("/printer-queue/stream"); - } - - @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - long start = System.currentTimeMillis(); - String method = request.getMethod(); - String uri = request.getRequestURI(); - String query = request.getQueryString(); - - ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); - ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); - - log.info(">>> Esign {} {}{}", method, uri, query != null ? "?" + query : ""); - - try { - filterChain.doFilter(wrappedRequest, wrappedResponse); - } finally { - logRequestBodyIfPresent(wrappedRequest); - logResponseBodyIfPresent(wrappedResponse); - log.info( - "<<< Esign {} {} status={} {}ms", - method, - uri, - wrappedResponse.getStatus(), - System.currentTimeMillis() - start); - wrappedResponse.copyBodyToResponse(); - } - } - - private void logRequestBodyIfPresent(ContentCachingRequestWrapper request) { - if (!isJsonBody(request)) { - return; - } - byte[] buf = request.getContentAsByteArray(); - if (buf == null || buf.length == 0) { - return; - } - String raw = new String(buf, StandardCharsets.UTF_8); - log.info(" request-body: {}", EsignLogMaskUtil.maskJsonForLog(raw)); - } - - private void logResponseBodyIfPresent(ContentCachingResponseWrapper response) { - if (!isJsonResponse(response)) { - return; - } - byte[] buf = response.getContentAsByteArray(); - if (buf == null || buf.length == 0) { - return; - } - String raw = new String(buf, StandardCharsets.UTF_8); - log.info(" response-body: {}", EsignLogMaskUtil.maskJsonForLog(raw)); - } - - private static boolean isJsonBody(HttpServletRequest request) { - String method = request.getMethod(); - if (!"POST".equalsIgnoreCase(method) && !"PUT".equalsIgnoreCase(method) && !"PATCH".equalsIgnoreCase(method)) { - return false; - } - String ct = StringUtils.defaultString(request.getContentType()).toLowerCase(); - return ct.contains(MediaType.APPLICATION_JSON_VALUE) || ct.contains("json"); - } - - private static boolean isJsonResponse(HttpServletResponse response) { - String ct = StringUtils.defaultString(response.getContentType()).toLowerCase(); - return ct.contains(MediaType.APPLICATION_JSON_VALUE) || ct.contains("json"); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignLegacyApiDeprecationFilter.java b/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignLegacyApiDeprecationFilter.java deleted file mode 100644 index a37db5b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignLegacyApiDeprecationFilter.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.filters; - -import com.sjmresorts.opsapp.esign.common.EsignApiPaths; -import com.sjmresorts.opsapp.esign.common.EsignLegacyApiProperties; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.time.LocalDate; - -/** - * /api/v1/sinosecu/** 兼容层:可配置关闭、响应 Deprecation 头、Sunset 后返回 410。 - */ -@Slf4j -@Component -@Order(Ordered.HIGHEST_PRECEDENCE + 5) -@RequiredArgsConstructor -public class EsignLegacyApiDeprecationFilter extends OncePerRequestFilter { - - private final EsignLegacyApiProperties legacyApiProperties; - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String uri = request.getRequestURI(); - return !EsignApiPaths.isLegacyPath(uri); - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - if (!legacyApiProperties.isEnabled()) { - response.setStatus(HttpStatus.GONE.value()); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write( - "{\"code\":\"LEGACY_API_DISABLED\",\"message\":\"/api/v1/sinosecu/ 已下线,请改用 /api/v1/esign/\"}"); - return; - } - - LocalDate today = LocalDate.now(); - if (today.isAfter(EsignApiPaths.LEGACY_SUNSET_DATE)) { - response.setStatus(HttpStatus.GONE.value()); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write( - "{\"code\":\"LEGACY_API_SUNSET\",\"message\":\"/api/v1/sinosecu/ 已于 " - + EsignApiPaths.LEGACY_SUNSET_DATE + " 下线\"}"); - return; - } - - if (legacyApiProperties.isWarnHeaders()) { - response.setHeader(EsignApiPaths.DEPRECATION_HEADER, "true"); - response.setHeader(EsignApiPaths.SUNSET_HEADER, EsignApiPaths.LEGACY_SUNSET_DATE.toString()); - response.setHeader(EsignApiPaths.LINK_HEADER, - "<" + EsignApiPaths.PREFIX + ">; rel=\"successor-version\""); - } - - if (log.isDebugEnabled()) { - log.debug("Legacy sinosecu API: {} {}", request.getMethod(), request.getRequestURI()); - } - filterChain.doFilter(request, response); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/mappers/SignFieldOhipValueMapper.java b/src/main/java/com/sjmresorts/opsapp/esign/common/mappers/SignFieldOhipValueMapper.java deleted file mode 100644 index 3cbd456..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/mappers/SignFieldOhipValueMapper.java +++ /dev/null @@ -1,207 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.mappers; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.esign.common.dtos.OhipProfileFieldDef; -import com.sjmresorts.opsapp.esign.common.dtos.SignFieldOhipMappingItem; -import com.sjmresorts.opsapp.esign.common.support.OhipProfileFieldCatalog; -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -/** 按 CMS 映射将 App signaturesJson + signFieldsJson 转为 OHIP 字段代码 → 值。 */ -public final class SignFieldOhipValueMapper { - - private SignFieldOhipValueMapper() { - } - - public static Map mapValues( - String signaturesJson, - String signFieldsJson, - List mappings) { - Map out = new LinkedHashMap<>(); - if (mappings == null || mappings.isEmpty()) { - return out; - } - JSONObject signatures = parseObject(signaturesJson); - JSONArray fields = parseArray(signFieldsJson); - if (signatures == null || signatures.isEmpty() || fields == null || fields.isEmpty()) { - return out; - } - - for (SignFieldOhipMappingItem mapping : mappings) { - if (mapping == null || Boolean.FALSE.equals(mapping.getEnabled())) { - continue; - } - String ohipCode = StringUtils.trimToEmpty(mapping.getOhipFieldCode()); - String fieldName = StringUtils.trimToEmpty(mapping.getFieldName()); - String ohipPath = OhipProfileFieldCatalog.resolvePath(ohipCode).orElse(null); - if (ohipPath == null || StringUtils.isBlank(fieldName)) { - continue; - } - JSONObject field = findFieldByName(fields, fieldName); - if (field == null) { - field = findFieldByOhipCode(fields, ohipCode); - } - if (field == null) { - continue; - } - String sigKey = StringUtils.defaultIfBlank(field.getString("id"), fieldName); - String raw = signatures.getString(sigKey); - if (StringUtils.isBlank(raw)) { - continue; - } - String valueType = normalizeValueType(mapping.getValueType(), field.getString("type")); - if ("signature".equals(valueType)) { - continue; - } - String text = StringUtils.trimToEmpty(raw); - if (text.startsWith("data:image")) { - continue; - } - if (!text.isEmpty()) { - out.put(ohipPath, text); - } - } - return out; - } - - private static JSONObject findFieldByName(JSONArray fields, String fieldName) { - String target = fieldName.toLowerCase(Locale.ROOT); - for (int i = 0; i < fields.size(); i++) { - JSONObject obj = fields.getJSONObject(i); - if (obj == null) { - continue; - } - String name = StringUtils.trimToEmpty(obj.getString("fieldName")); - if (name.equalsIgnoreCase(fieldName)) { - return obj; - } - String id = StringUtils.trimToEmpty(obj.getString("id")); - if (id.equalsIgnoreCase(fieldName) || id.toLowerCase(Locale.ROOT).equals(target)) { - return obj; - } - } - return null; - } - - /** CMS 映射 fieldName 与热区不一致时,按 OHIP 字段类型匹配 phone/email 文字热区。 */ - private static JSONObject findFieldByOhipCode(JSONArray fields, String ohipCode) { - String code = StringUtils.trimToEmpty(ohipCode).toLowerCase(Locale.ROOT); - String wantFormat = null; - if (code.startsWith("phone")) { - wantFormat = "phone"; - } else if (code.startsWith("email")) { - wantFormat = "email"; - } - if (wantFormat == null) { - return null; - } - for (int i = 0; i < fields.size(); i++) { - JSONObject obj = fields.getJSONObject(i); - if (obj == null) { - continue; - } - if (!"text".equalsIgnoreCase(StringUtils.trimToEmpty(obj.getString("type")))) { - continue; - } - if (wantFormat.equals(normalizeInputFormat(obj.getString("inputFormat")))) { - return obj; - } - } - return null; - } - - private static String normalizeInputFormat(String raw) { - String s = StringUtils.trimToEmpty(raw).toLowerCase(Locale.ROOT); - if (s.isEmpty()) { - return "text"; - } - if (s.contains("mail") || s.contains("邮") || s.contains("郵") || "email".equals(s)) { - return "email"; - } - if (s.contains("phone") || s.contains("tel") || s.contains("电话") || s.contains("電話")) { - return "phone"; - } - return s; - } - - private static String normalizeValueType(String mappingType, String fieldType) { - String mt = StringUtils.trimToEmpty(mappingType).toLowerCase(Locale.ROOT); - if ("text".equals(mt) || "signature".equals(mt)) { - return mt; - } - if (fieldType == null) { - return "text"; - } - return "text".equalsIgnoreCase(StringUtils.trimToEmpty(fieldType)) ? "text" : "signature"; - } - - private static JSONObject parseObject(String json) { - if (StringUtils.isBlank(json)) { - return new JSONObject(); - } - try { - JSONObject obj = JSON.parseObject(json.trim()); - return obj != null ? obj : new JSONObject(); - } catch (Exception e) { - return new JSONObject(); - } - } - - private static JSONArray parseArray(String json) { - if (StringUtils.isBlank(json)) { - return new JSONArray(); - } - try { - JSONArray arr = JSON.parseArray(json.trim()); - return arr != null ? arr : new JSONArray(); - } catch (Exception e) { - return new JSONArray(); - } - } - - public static List normalizeMappings(List raw) { - List out = new ArrayList<>(); - if (raw == null) { - return out; - } - for (SignFieldOhipMappingItem item : raw) { - if (item == null) { - continue; - } - String fieldName = sanitizeFieldName(item.getFieldName()); - String ohipFieldCode = resolveStoredOhipCode(item.getOhipFieldCode()); - if (StringUtils.isBlank(fieldName) && StringUtils.isBlank(ohipFieldCode)) { - continue; - } - SignFieldOhipMappingItem next = new SignFieldOhipMappingItem(); - next.setFieldName(fieldName); - next.setLabel(StringUtils.trimToNull(item.getLabel())); - next.setOhipFieldCode(ohipFieldCode); - next.setValueType(normalizeValueType(item.getValueType(), null)); - next.setEnabled(item.getEnabled() == null || Boolean.TRUE.equals(item.getEnabled())); - out.add(next); - } - return out; - } - - private static String sanitizeFieldName(String raw) { - String s = StringUtils.trimToEmpty(raw); - if (s.isEmpty()) { - return ""; - } - return s.replaceAll("[^A-Za-z0-9_]", "_").replaceAll("_+", "_").replaceAll("^_|_$", ""); - } - - private static String resolveStoredOhipCode(String raw) { - return OhipProfileFieldCatalog.findByCodeOrPath(StringUtils.trimToEmpty(raw)) - .map(OhipProfileFieldDef::getCode) - .orElse(""); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignLogMaskUtil.java b/src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignLogMaskUtil.java deleted file mode 100644 index 80f22e5..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignLogMaskUtil.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.support; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import org.apache.commons.lang3.StringUtils; - -import java.util.Locale; -import java.util.Set; - -/** 日志脱敏:Base64 / 大字段只打长度,不打内容。 */ -public final class EsignLogMaskUtil { - - private static final Set BINARY_KEYS = Set.of( - "base64", - "pdfbase64", - "fileattachment", - "signedpdfbase64", - "image", - "data"); - - private static final int MAX_TEXT_LEN = 240; - - private EsignLogMaskUtil() { - } - - public static String maskJsonForLog(String raw) { - if (StringUtils.isBlank(raw)) { - return ""; - } - String trimmed = raw.trim(); - try { - Object parsed = JSON.parse(trimmed); - return JSON.toJSONString(maskNode(parsed)); - } catch (Exception e) { - return truncate(trimmed); - } - } - - public static String sizeTag(Object value) { - if (value == null) { - return "[null]"; - } - if (value instanceof byte[] bytes) { - return "[bytes len=" + bytes.length + "]"; - } - String s = String.valueOf(value); - if (s.isEmpty()) { - return "[empty]"; - } - return "[len=" + s.length() + "]"; - } - - public static String truncate(String text) { - if (text == null) { - return ""; - } - String oneLine = text.replace('\n', ' ').replace('\r', ' '); - if (oneLine.length() <= MAX_TEXT_LEN) { - return oneLine; - } - return oneLine.substring(0, MAX_TEXT_LEN) + "…"; - } - - private static Object maskNode(Object node) { - if (node instanceof JSONObject obj) { - JSONObject masked = new JSONObject(); - for (String key : obj.keySet()) { - Object value = obj.get(key); - if (isBinaryKey(key)) { - masked.put(key, sizeTag(value)); - } else if ("signaturesjson".equals(key.toLowerCase(Locale.ROOT))) { - masked.put(key, truncate(String.valueOf(value))); - } else { - masked.put(key, maskNode(value)); - } - } - return masked; - } - if (node instanceof JSONArray arr) { - JSONArray masked = new JSONArray(); - for (int i = 0; i < arr.size(); i++) { - masked.add(maskNode(arr.get(i))); - } - return masked; - } - if (node instanceof String s && s.length() > MAX_TEXT_LEN) { - return truncate(s); - } - return node; - } - - private static boolean isBinaryKey(String key) { - if (key == null) { - return false; - } - String lower = key.toLowerCase(Locale.ROOT); - if (BINARY_KEYS.contains(lower)) { - return true; - } - return lower.contains("base64") || lower.contains("attachment"); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignReservationParamUtil.java b/src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignReservationParamUtil.java deleted file mode 100644 index 58f34cd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/support/EsignReservationParamUtil.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.support; - -/** 预订/酒店 ID 等请求参数的规范化(去空白、去误传引号)。 */ -public final class EsignReservationParamUtil { - - private EsignReservationParamUtil() { - } - - /** 去掉首尾空白及误传的引号(如 'grand-dragon') */ - public static String normalizeParam(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - if (trimmed.length() >= 2) { - char first = trimmed.charAt(0); - char last = trimmed.charAt(trimmed.length() - 1); - if ((first == '\'' && last == '\'') || (first == '"' && last == '"')) { - trimmed = trimmed.substring(1, trimmed.length() - 1).trim(); - } - } - return trimmed; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/support/OhipProfileFieldCatalog.java b/src/main/java/com/sjmresorts/opsapp/esign/common/support/OhipProfileFieldCatalog.java deleted file mode 100644 index 784cb2f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/support/OhipProfileFieldCatalog.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.support; - -import com.sjmresorts.opsapp.esign.common.dtos.OhipProfileFieldDef; -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** OHIP CRM profileDetails 固定字段目录(CMS 仅能选择此处定义的字段)。 */ -public final class OhipProfileFieldCatalog { - - private static final List COMMON_FIELD_CODES = List.of( - "primary.givenName", - "primary.surname", - "alternate.givenName", - "alternate.surname", - "customer.gender", - "customer.birthDate", - "customer.nationality", - "id.idType", - "id.idNumber", - "phone.number", - "email.address"); - - private static final List FIELDS = List.of( - def("primary.givenName", "customer.personName[Primary].givenName", "personNamePrimary", "First name", "英文名", true), - def("primary.middleName", "customer.personName[Primary].middleName", "personNamePrimary", "Middle name", "中间名", false), - def("primary.surname", "customer.personName[Primary].surname", "personNamePrimary", "Last name", "英文姓", true), - def("primary.nameTitle", "customer.personName[Primary].nameTitle", "personNamePrimary", "Title", "称谓", false), - def("alternate.givenName", "customer.personName[Alternate].givenName", "personNameAlternate", "Alt. first name", "中文名", true), - def("alternate.surname", "customer.personName[Alternate].surname", "personNameAlternate", "Alt. full name", "中文全名", true), - def("customer.language", "customer.language", "customer", "Language", "语言", false), - def("customer.gender", "customer.gender", "customer", "Gender (M/F/U)", "性别", true), - def("customer.birthDate", "customer.birthDate", "customer", "Date of birth", "出生日期", true), - def("customer.birthPlace", "customer.birthPlace", "customer", "Place of birth", "出生地", false), - def("customer.nationality", "customer.nationality", "customer", "Nationality", "国籍", true), - def("id.idType", "customer.identifications.identificationInfo[0].identification.idType", "identification", "Document type", "证件类型", true), - def("id.idNumber", "customer.identifications.identificationInfo[0].identification.idNumber", "identification", "Document number", "证件号码", true), - def("id.issuedCountry", "customer.identifications.identificationInfo[0].identification.issuedCountry", "identification", "Issue country", "签发国", false), - def("id.issuedDate", "customer.identifications.identificationInfo[0].identification.issuedDate", "identification", "Issue date", "签发日期", false), - def("id.issuedPlace", "customer.identifications.identificationInfo[0].identification.issuedPlace", "identification", "Issue place", "签发地", false), - def("id.expirationDate", "customer.identifications.identificationInfo[0].identification.expirationDate", "identification", "Expiration date", "到期日", false), - def("address.line1", "addresses.addressInfo[0].address.addressLine[0]", "address", "Address line 1", "地址行 1", false), - def("address.line2", "addresses.addressInfo[0].address.addressLine[1]", "address", "Address line 2", "地址行 2", false), - def("address.city", "addresses.addressInfo[0].address.cityName", "address", "City", "城市", false), - def("address.state", "addresses.addressInfo[0].address.state", "address", "State", "省/州", false), - def("address.country", "addresses.addressInfo[0].address.country.value", "address", "Country", "国家", false), - def("phone.useType", "telephones.telephoneInfo[0].telephone.phoneUseType", "telephone", "Phone type", "电话类型", false), - def("phone.number", "telephones.telephoneInfo[0].telephone.phoneNumber", "telephone", "Phone number", "电话号码", true), - def("email.address", "emails.emailInfo[0].email.emailAddress", "email", "Email", "电子邮箱", true), - def("email.type", "emails.emailInfo[0].email.type", "email", "Email type", "邮箱类型", false), - def("keyword.type", "keywords.keyword[0].type", "keyword", "Keyword type", "关键字类型", false), - def("keyword.value", "keywords.keyword[0].keyword", "keyword", "Keyword", "关键字", false)); - - private static final Map BY_CODE; - private static final Map BY_PATH; - - static { - Map byCode = new LinkedHashMap<>(); - Map byPath = new LinkedHashMap<>(); - for (OhipProfileFieldDef field : FIELDS) { - byCode.put(field.getCode(), field); - byPath.put(field.getPath(), field); - } - BY_CODE = Collections.unmodifiableMap(byCode); - BY_PATH = Collections.unmodifiableMap(byPath); - } - - private OhipProfileFieldCatalog() { - } - - public static List all() { - return FIELDS; - } - - public static List commonFields() { - List out = new ArrayList<>(); - for (String code : COMMON_FIELD_CODES) { - OhipProfileFieldDef field = BY_CODE.get(code); - if (field != null) { - out.add(field); - } - } - return out; - } - - public static Optional findByCode(String code) { - if (StringUtils.isBlank(code)) { - return Optional.empty(); - } - return Optional.ofNullable(BY_CODE.get(code.trim())); - } - - public static Optional findByCodeOrPath(String raw) { - String key = StringUtils.trimToNull(raw); - if (key == null) { - return Optional.empty(); - } - OhipProfileFieldDef byCode = BY_CODE.get(key); - if (byCode != null) { - return Optional.of(byCode); - } - return Optional.ofNullable(BY_PATH.get(key)); - } - - public static Optional resolvePath(String codeOrPath) { - return findByCodeOrPath(codeOrPath).map(OhipProfileFieldDef::getPath); - } - - public static List groupOrder() { - List groups = new ArrayList<>(); - for (OhipProfileFieldDef field : FIELDS) { - if (!groups.contains(field.getGroup())) { - groups.add(field.getGroup()); - } - } - return groups; - } - - private static OhipProfileFieldDef def( - String code, String path, String group, String labelEn, String labelZh, boolean common) { - OhipProfileFieldDef def = new OhipProfileFieldDef(); - def.setCode(code); - def.setPath(path); - def.setGroup(group); - def.setLabelEn(labelEn); - def.setLabelZh(labelZh); - def.setCommon(common); - return def; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/support/ProfileDetailsJsonReader.java b/src/main/java/com/sjmresorts/opsapp/esign/common/support/ProfileDetailsJsonReader.java deleted file mode 100644 index 8681ee8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/support/ProfileDetailsJsonReader.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.support; - -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** 从 OHIP GET profile 响应的 profileDetails 中按路径读取字符串值。 */ -public final class ProfileDetailsJsonReader { - - private static final Pattern SEGMENT = - Pattern.compile("([^.\\[]+)(?:\\[([^\\]]+)\\])?"); - - private ProfileDetailsJsonReader() { - } - - public static JSONObject unwrapProfileDetails(String responseBody) { - if (StringUtils.isBlank(responseBody)) { - return new JSONObject(); - } - try { - JSONObject root = JSONObject.parseObject(responseBody.trim()); - if (root == null) { - return new JSONObject(); - } - JSONObject details = root.getJSONObject("profileDetails"); - return details != null ? details : root; - } catch (Exception e) { - return new JSONObject(); - } - } - - public static String getString(JSONObject profileDetails, String path) { - if (profileDetails == null || profileDetails.isEmpty() || StringUtils.isBlank(path)) { - return null; - } - String normalized = normalizePath(path); - if (normalized == null) { - return null; - } - Object node = profileDetails; - List segments = parsePath(normalized); - if (segments.isEmpty()) { - return null; - } - for (int i = 0; i < segments.size() - 1; i++) { - node = navigate(node, segments.get(i)); - if (node == null) { - return null; - } - } - Segment leaf = segments.get(segments.size() - 1); - if (!(node instanceof JSONObject obj)) { - return null; - } - Selector leafSelector = leaf.selector(); - if (leafSelector != null && leafSelector.kind == SelectorKind.INDEX) { - JSONArray arr = obj.getJSONArray(leaf.name()); - if (arr == null || arr.size() <= leafSelector.index) { - return null; - } - Object item = arr.get(leafSelector.index); - return item != null ? StringUtils.trimToNull(String.valueOf(item)) : null; - } - return StringUtils.trimToNull(obj.getString(leaf.name())); - } - - private static String normalizePath(String raw) { - String path = StringUtils.trimToEmpty(raw); - if (path.startsWith("profileDetails.")) { - path = path.substring("profileDetails.".length()); - } - return StringUtils.trimToNull(path); - } - - private static List parsePath(String path) { - List out = new ArrayList<>(); - Matcher matcher = SEGMENT.matcher(path); - int pos = 0; - while (matcher.find(pos)) { - if (matcher.start() != pos) { - break; - } - String name = matcher.group(1); - String selector = StringUtils.trimToNull(matcher.group(2)); - out.add(new Segment(name, parseSelector(name, selector))); - pos = matcher.end(); - if (pos < path.length() && path.charAt(pos) == '.') { - pos++; - } - } - return out; - } - - private static Selector parseSelector(String segmentName, String raw) { - if (raw == null) { - return null; - } - if (raw.contains("=")) { - int eq = raw.indexOf('='); - return Selector.match(raw.substring(0, eq).trim(), raw.substring(eq + 1).trim()); - } - if (NumberUtils.isDigits(raw)) { - return Selector.index(Integer.parseInt(raw)); - } - if ("personName".equals(segmentName)) { - return Selector.match("nameType", raw); - } - if ("keyword".equals(segmentName)) { - return Selector.index(0); - } - return Selector.match("type", raw); - } - - private static Object navigate(Object current, Segment segment) { - if (!(current instanceof JSONObject obj)) { - return null; - } - Selector selector = segment.selector(); - if (selector == null) { - return obj.get(segment.name()); - } - Object node = obj.get(segment.name()); - if (!(node instanceof JSONArray arr)) { - return null; - } - if (selector.kind == SelectorKind.INDEX) { - if (arr.size() <= selector.index) { - return null; - } - return arr.get(selector.index); - } - for (int i = 0; i < arr.size(); i++) { - Object item = arr.get(i); - if (item instanceof JSONObject itemObj && matches(itemObj, selector)) { - return itemObj; - } - } - return null; - } - - private static boolean matches(JSONObject obj, Selector selector) { - if (selector.kind != SelectorKind.MATCH) { - return false; - } - String actual = StringUtils.trimToEmpty(obj.getString(selector.matchKey)); - return actual.equalsIgnoreCase(selector.matchValue); - } - - private record Segment(String name, Selector selector) { - } - - private enum SelectorKind { - INDEX, - MATCH - } - - private static final class Selector { - private final SelectorKind kind; - private final int index; - private final String matchKey; - private final String matchValue; - - private Selector(SelectorKind kind, int index, String matchKey, String matchValue) { - this.kind = kind; - this.index = index; - this.matchKey = matchKey; - this.matchValue = matchValue; - } - - static Selector index(int index) { - return new Selector(SelectorKind.INDEX, index, null, null); - } - - static Selector match(String key, String value) { - return new Selector( - SelectorKind.MATCH, - -1, - StringUtils.trimToEmpty(key), - StringUtils.trimToEmpty(value)); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignReservationTaskCompleteRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignReservationTaskCompleteRequest.java deleted file mode 100644 index 33d2b53..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignReservationTaskCompleteRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "预订待处理任务完成(从待签列表移除)") -public class EsignReservationTaskCompleteRequest { - - @Schema(description = "任务 ID(与 reservationId+hotelId 二选一)") - private String taskId; - - @Schema(description = "预订 ID") - private String reservationId; - - @Schema(description = "酒店 ID") - private String hotelId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingResponse.java deleted file mode 100644 index 128b005..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.sjmresorts.opsapp.esign.common.dtos.SignFieldOhipMappingItem; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignSignFieldOhipMappingResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private SignFieldOhipMappingData data; - - @Data - @NoArgsConstructor - public static class SignFieldOhipMappingData { - private SignFieldOhipMappingConfigItem config; - } - - @Data - @NoArgsConstructor - public static class SignFieldOhipMappingConfigItem { - private String configId; - private String hotelId; - private List mappings; - private String updatedAt; - } - - public static EsignSignFieldOhipMappingResponse ok(String message) { - EsignSignFieldOhipMappingResponse r = new EsignSignFieldOhipMappingResponse(); - r.setMessage(message); - return r; - } - - public static EsignSignFieldOhipMappingResponse fail(String code, String message) { - EsignSignFieldOhipMappingResponse r = new EsignSignFieldOhipMappingResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingSaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingSaveRequest.java deleted file mode 100644 index 62b1c37..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipMappingSaveRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import com.sjmresorts.opsapp.esign.common.dtos.SignFieldOhipMappingItem; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@NoArgsConstructor -public class EsignSignFieldOhipMappingSaveRequest { - - /** 留空表示全酒店默认 */ - private String hotelId; - private List mappings; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewRequest.java deleted file mode 100644 index 7e0e60c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import com.sjmresorts.opsapp.esign.common.dtos.SignFieldOhipMappingItem; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@NoArgsConstructor -@Schema(description = "签名字段 OHIP 映射 → profileDetails JSON 预览请求") -public class EsignSignFieldOhipPreviewRequest { - - @Schema(description = "当前编辑中的映射行(未保存也可预览)") - private List mappings; - - @Schema(description = "预览用 profileType,默认 Guest") - private String profileType; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewResponse.java deleted file mode 100644 index f28e293..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignSignFieldOhipPreviewResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "profileDetails JSON 预览响应") -public class EsignSignFieldOhipPreviewResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private PreviewData data; - - @Data - @NoArgsConstructor - public static class PreviewData { - @Schema(description = "PUT /crm/v1/profiles/{profileId} 请求体(含 profileDetails)") - private Map profileBody; - - @Schema(description = "格式化后的 JSON 字符串,便于 CMS 展示") - private String jsonPreview; - - @Schema(description = "参与组装的映射字段数量(仅 enabled 且 text)") - private Integer mappedFieldCount; - } - - public static EsignSignFieldOhipPreviewResponse ok(PreviewData data) { - EsignSignFieldOhipPreviewResponse r = new EsignSignFieldOhipPreviewResponse(); - r.setMessage("ok"); - r.setData(data); - return r; - } - - public static EsignSignFieldOhipPreviewResponse fail(String code, String message) { - EsignSignFieldOhipPreviewResponse r = new EsignSignFieldOhipPreviewResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileIdResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileIdResponse.java deleted file mode 100644 index 43d550f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileIdResponse.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "预订关联的 OHIP Guest Profile ID") -public class GuestProfileIdResponse { - - private boolean success; - private String code; - private String message; - private String reservationId; - private String hotelId; - - @Schema(description = "reservationGuests.profileInfo.profileIdList 中 type=Profile 的 id") - private String profileId; - - @Schema(description = "reservationGuests.profileInfo.profile.profileType,例如 Guest") - private String profileType; - - public static GuestProfileIdResponse ok( - String reservationId, String hotelId, String profileId, String profileType) { - GuestProfileIdResponse r = new GuestProfileIdResponse(); - r.setSuccess(true); - r.setCode("200"); - r.setMessage("ok"); - r.setReservationId(reservationId); - r.setHotelId(hotelId); - r.setProfileId(profileId); - r.setProfileType(profileType); - return r; - } - - public static GuestProfileIdResponse ok(String reservationId, String hotelId, String profileId) { - return ok(reservationId, hotelId, profileId, null); - } - - public static GuestProfileIdResponse fail(String code, String message, String reservationId, String hotelId) { - GuestProfileIdResponse r = new GuestProfileIdResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - r.setReservationId(reservationId); - r.setHotelId(hotelId); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileRef.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileRef.java deleted file mode 100644 index c27ec3d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/GuestProfileRef.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -/** OHIP 预订旅客 Profile 引用(id + type)。 */ -public record GuestProfileRef(String profileId, String profileType) { - - public boolean hasProfileId() { - return profileId != null && !profileId.isBlank(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/HospitalityOAuthSettings.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/HospitalityOAuthSettings.java deleted file mode 100644 index 049952b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/HospitalityOAuthSettings.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import lombok.Builder; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; - -@Getter -@Builder -public class HospitalityOAuthSettings { - - private final boolean enabled; - private final String baseUrl; - private final String tokenPath; - private final String clientId; - private final String clientSecret; - private final String appKey; - private final String enterpriseId; - private final String hotelId; - private final String scope; - private final String grantType; - private final int timeoutMs; - - public boolean isComplete() { - return enabled - && StringUtils.isNotBlank(baseUrl) - && StringUtils.isNotBlank(clientId) - && StringUtils.isNotBlank(clientSecret) - && StringUtils.isNotBlank(appKey) - && StringUtils.isNotBlank(enterpriseId) - && StringUtils.isNotBlank(hotelId); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/OAuthTokenResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/OAuthTokenResponse.java deleted file mode 100644 index 54a1e25..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/OAuthTokenResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public class OAuthTokenResponse { - - @JsonProperty("access_token") - private String accessToken; - - @JsonProperty("token_type") - private String tokenType; - - @JsonProperty("expires_in") - private Long expiresIn; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationLookupHit.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationLookupHit.java deleted file mode 100644 index 79b6005..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationLookupHit.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -/** OHIP 按确认号查预订时的 reservationId + 可选 Guest Profile。 */ -public record ReservationLookupHit(String reservationId, GuestProfileRef guestProfile) { -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationThirdPartyResult.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationThirdPartyResult.java deleted file mode 100644 index c6679a6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationThirdPartyResult.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -public class ReservationThirdPartyResult { - - private boolean invoked; - private int statusCode; - private String responseBody; - private String errorMessage; - - public static ReservationThirdPartyResult skipped(String reason) { - ReservationThirdPartyResult r = new ReservationThirdPartyResult(); - r.setInvoked(false); - r.setStatusCode(0); - r.setErrorMessage(reason); - return r; - } - - public static ReservationThirdPartyResult success(int statusCode, String responseBody) { - ReservationThirdPartyResult r = new ReservationThirdPartyResult(); - r.setInvoked(true); - r.setStatusCode(statusCode); - r.setResponseBody(responseBody); - return r; - } - - public static ReservationThirdPartyResult failure(int statusCode, String responseBody, String errorMessage) { - ReservationThirdPartyResult r = new ReservationThirdPartyResult(); - r.setInvoked(true); - r.setStatusCode(statusCode); - r.setResponseBody(responseBody); - r.setErrorMessage(errorMessage); - return r; - } - - public boolean isHttpSuccess() { - return invoked && statusCode >= 200 && statusCode < 300; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardSubmitRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardSubmitRequest.java deleted file mode 100644 index 5809e03..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardSubmitRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "登记卡签名提交 OPERA(通知/预订流程,无需打印机任务 ID)") -public class SignedRegistrationCardSubmitRequest { - - @Schema(description = "OHIP reservation id(linkId)", requiredMode = Schema.RequiredMode.REQUIRED) - private String reservationId; - - @Schema(description = "酒店 ID") - private String hotelId; - - @Schema(description = "文件名") - private String fileName; - - @Schema(description = "原始登记卡 PDF Base64(用于合成签名或与 signaturesJson 配合)") - private String pdfBase64; - - @Schema(description = "签名后完整 PDF Base64(可选,优先使用)") - private String base64; - - @Schema(description = "签名图层 JSON") - private String signaturesJson; - - @Schema(description = "签名数量") - private Integer signatureCount; - - @Schema(description = "确认号(可选,未传则从 PDF 解析)") - private String confirmationNumber; - - @Schema(description = "签名热区 JSON(与 App loadSignFields 一致)") - private String signFieldsJson; - - @Schema(description = "OHIP Guest Profile ID(可选,未传则服务端从预订详情解析)") - private String profileId; - - @Schema(description = "OHIP profile.profileType(可选,例如 Guest)") - private String profileType; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardUploadResult.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardUploadResult.java deleted file mode 100644 index eccadaa..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/SignedRegistrationCardUploadResult.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -public class SignedRegistrationCardUploadResult { - - private boolean invoked; - private boolean success; - private int statusCode; - private String confirmationNumber; - private String reservationId; - private String fileName; - /** 从预订查询/详情解析的 OHIP Profile ID(用于签名字段回写)。 */ - private String profileId; - private String profileType; - private String errorMessage; - private String responseSummary; - - public static SignedRegistrationCardUploadResult skipped(String reason) { - SignedRegistrationCardUploadResult r = new SignedRegistrationCardUploadResult(); - r.setInvoked(false); - r.setSuccess(false); - r.setErrorMessage(reason); - return r; - } - - public static SignedRegistrationCardUploadResult failure( - int statusCode, String summary, String errorMessage) { - return failure(statusCode, summary, errorMessage, null, null, null); - } - - public static SignedRegistrationCardUploadResult failure( - int statusCode, - String summary, - String errorMessage, - String confirmationNumber, - String reservationId, - String fileName) { - SignedRegistrationCardUploadResult r = new SignedRegistrationCardUploadResult(); - r.setInvoked(true); - r.setSuccess(false); - r.setStatusCode(statusCode); - r.setResponseSummary(summary); - r.setErrorMessage(errorMessage); - r.setConfirmationNumber(confirmationNumber); - r.setReservationId(reservationId); - r.setFileName(fileName); - return r; - } - - public static SignedRegistrationCardUploadResult success( - int statusCode, - String confirmationNumber, - String reservationId, - String fileName, - String responseSummary) { - SignedRegistrationCardUploadResult r = new SignedRegistrationCardUploadResult(); - r.setInvoked(true); - r.setSuccess(true); - r.setStatusCode(statusCode); - r.setConfirmationNumber(confirmationNumber); - r.setReservationId(reservationId); - r.setFileName(fileName); - r.setResponseSummary(responseSummary); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignReservationTask.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignReservationTask.java deleted file mode 100644 index e2402ed..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignReservationTask.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_reservation_tasks") -public class EsignReservationTask { - - @Id - @Column(name = "task_id", nullable = false, length = 36) - private String taskId; - - @Column(name = "reservation_id", nullable = false, length = 64) - private String reservationId; - - @Column(name = "hotel_id", nullable = false, length = 64) - private String hotelId; - - @Column(name = "status", nullable = false, length = 32) - private String status; - - @Column(name = "title", length = 256) - private String title; - - @Column(name = "body", length = 512) - private String body; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - /** 用户打开登记卡并完成处理的时间 */ - @Column(name = "completed_at") - private Instant completedAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignSignFieldOhipConfig.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignSignFieldOhipConfig.java deleted file mode 100644 index 093809d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/models/EsignSignFieldOhipConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Lob; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_sign_field_ohip_config") -public class EsignSignFieldOhipConfig { - - @Id - @Column(name = "config_id", nullable = false, length = 36) - private String configId; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Lob - @Column(name = "mappings_json", nullable = false) - private String mappingsJson; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignReservationTaskRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignReservationTaskRepository.java deleted file mode 100644 index 05c02f2..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignReservationTaskRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.repositories; - -import com.sjmresorts.opsapp.esign.reservation.models.EsignReservationTask; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; - -@Repository -public interface EsignReservationTaskRepository extends JpaRepository { - - Optional findFirstByReservationIdAndHotelIdAndStatusOrderByCreatedAtDesc( - String reservationId, String hotelId, String status); - - List findTop100ByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc( - String status, Instant createdAtMinInclusive); - - List findTop100ByStatusAndCreatedAtLessThanOrderByCreatedAtDesc( - String status, Instant createdAtMaxExclusive); - - List findTop100ByStatusOrderByCompletedAtDesc(String status); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignSignFieldOhipConfigRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignSignFieldOhipConfigRepository.java deleted file mode 100644 index e7d2bd6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/repositories/EsignSignFieldOhipConfigRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.repositories; - -import com.sjmresorts.opsapp.esign.reservation.models.EsignSignFieldOhipConfig; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface EsignSignFieldOhipConfigRepository extends JpaRepository { - - Optional findFirstByHotelIdOrderByUpdatedAtDesc(String hotelId); - - Optional findFirstByHotelIdIsNullOrderByUpdatedAtDesc(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationTaskService.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationTaskService.java deleted file mode 100644 index 9ef128e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationTaskService.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.services; - -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import com.sjmresorts.opsapp.esign.reservation.models.EsignReservationTask; -import com.sjmresorts.opsapp.esign.reservation.repositories.EsignReservationTaskRepository; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Slf4j -@Service -public class EsignReservationTaskService { - - public static final String TASK_TYPE = "RESERVATION"; - public static final String STATUS_PENDING = "PENDING"; - public static final String STATUS_COMPLETED = "COMPLETED"; - - private static final int TASK_WINDOW_HOURS = 24; - - @Resource - private EsignReservationTaskRepository reservationTaskRepository; - - private Instant taskWindowCutoff() { - return Instant.now().minus(TASK_WINDOW_HOURS, ChronoUnit.HOURS); - } - - /** - * 预订通知触发时落库;同 reservationId+hotelId 若已有待处理则刷新 created_at。 - * - * @return 任务 ID(供 FCM data 携带) - */ - @Transactional - public String createOrRefreshPendingTask(String reservationId, String hotelId, String title, String body) { - String rid = EsignReservationParamUtil.normalizeParam(reservationId); - String hid = EsignReservationParamUtil.normalizeParam(hotelId); - if (StringUtils.isAnyBlank(rid, hid)) { - return null; - } - - Instant now = Instant.now(); - Optional existing = - reservationTaskRepository.findFirstByReservationIdAndHotelIdAndStatusOrderByCreatedAtDesc( - rid, hid, STATUS_PENDING); - if (existing.isPresent()) { - EsignReservationTask t = existing.get(); - t.setCreatedAt(now); - if (StringUtils.isNotBlank(title)) { - t.setTitle(trim(title, 256)); - } - if (StringUtils.isNotBlank(body)) { - t.setBody(trim(body, 512)); - } - reservationTaskRepository.save(t); - log.info("Reservation task refreshed taskId={}, reservationId={}, hotelId={}", t.getTaskId(), rid, hid); - return t.getTaskId(); - } - - EsignReservationTask task = new EsignReservationTask(); - task.setTaskId(UUID.randomUUID().toString()); - task.setReservationId(rid); - task.setHotelId(hid); - task.setStatus(STATUS_PENDING); - task.setTitle(trim(title, 256)); - task.setBody(trim(body, 512)); - task.setCreatedAt(now); - reservationTaskRepository.save(task); - log.info("Reservation task created taskId={}, reservationId={}, hotelId={}", task.getTaskId(), rid, hid); - return task.getTaskId(); - } - - - private Optional resolveTask(String taskId, String reservationId, String hotelId) { - if (StringUtils.isNotBlank(taskId)) { - return reservationTaskRepository.findById(taskId.trim()); - } - String rid = EsignReservationParamUtil.normalizeParam(reservationId); - String hid = EsignReservationParamUtil.normalizeParam(hotelId); - if (StringUtils.isAnyBlank(rid, hid)) { - return Optional.empty(); - } - return reservationTaskRepository.findFirstByReservationIdAndHotelIdAndStatusOrderByCreatedAtDesc( - rid, hid, STATUS_PENDING); - } - - private static String trim(String s, int max) { - if (s == null) { - return null; - } - String t = s.trim(); - if (t.isEmpty()) { - return null; - } - return t.length() > max ? t.substring(0, max) : t; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignSignFieldOhipMappingService.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignSignFieldOhipMappingService.java deleted file mode 100644 index a353138..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignSignFieldOhipMappingService.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.services; - -import com.alibaba.fastjson2.JSON; -import com.sjmresorts.opsapp.esign.common.dtos.OhipProfileFieldCatalogResponse; -import com.sjmresorts.opsapp.esign.common.dtos.SignFieldOhipMappingItem; -import com.sjmresorts.opsapp.esign.reservation.dtos.EsignSignFieldOhipMappingResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.EsignSignFieldOhipMappingSaveRequest; -import com.sjmresorts.opsapp.esign.reservation.dtos.EsignSignFieldOhipPreviewRequest; -import com.sjmresorts.opsapp.esign.reservation.dtos.EsignSignFieldOhipPreviewResponse; -import com.sjmresorts.opsapp.esign.reservation.models.EsignSignFieldOhipConfig; -import com.sjmresorts.opsapp.esign.reservation.repositories.EsignSignFieldOhipConfigRepository; -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import com.sjmresorts.opsapp.esign.common.support.OhipProfileFieldCatalog; -import com.sjmresorts.opsapp.esign.reservation.support.ProfileDetailsJsonBuilder; -import com.sjmresorts.opsapp.esign.common.mappers.SignFieldOhipValueMapper; -import jakarta.annotation.Resource; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@Service -public class EsignSignFieldOhipMappingService { - - public static final String DEFAULT_CONFIG_ID = "DEFAULT"; - - @Resource - private EsignSignFieldOhipConfigRepository configRepository; - - public EsignSignFieldOhipMappingResponse getForAdmin() { - return buildOk(ensureDefaultRow()); - } - - public EsignSignFieldOhipMappingResponse getForAdmin(String hotelId) { - return buildOk(resolveConfig(hotelId)); - } - - public List getMappingsForHotel(String hotelId) { - return parseMappings(resolveConfig(hotelId).getMappingsJson()); - } - - public Map mapSignatureValues( - String hotelId, String signaturesJson, String signFieldsJson) { - List mappings = getMappingsForHotel(hotelId); - return SignFieldOhipValueMapper.mapValues(signaturesJson, signFieldsJson, mappings); - } - - public OhipProfileFieldCatalogResponse listProfileFields() { - return OhipProfileFieldCatalogResponse.ok( - OhipProfileFieldCatalog.groupOrder(), - OhipProfileFieldCatalog.all(), - OhipProfileFieldCatalog.commonFields()); - } - - /** - * 根据当前映射配置生成 PUT /crm/v1/profiles/{profileId} 请求体预览。 - * 示例值优先取备注 label,其次 fieldName。 - */ - public EsignSignFieldOhipPreviewResponse previewProfileBody(EsignSignFieldOhipPreviewRequest request) { - List mappings = request != null && request.getMappings() != null - ? SignFieldOhipValueMapper.normalizeMappings(request.getMappings()) - : Collections.emptyList(); - Map sampleFields = new LinkedHashMap<>(); - for (SignFieldOhipMappingItem item : mappings) { - if (item == null || Boolean.FALSE.equals(item.getEnabled())) { - continue; - } - String valueType = StringUtils.trimToEmpty(item.getValueType()).toLowerCase(); - if ("signature".equals(valueType)) { - continue; - } - String path = OhipProfileFieldCatalog.resolvePath(item.getOhipFieldCode()).orElse(null); - if (path == null) { - continue; - } - String sample = StringUtils.firstNonBlank( - StringUtils.trimToNull(item.getLabel()), - StringUtils.trimToNull(item.getFieldName()), - "示例值"); - sampleFields.put(path, sample); - } - String profileType = StringUtils.defaultIfBlank( - request != null ? StringUtils.trimToNull(request.getProfileType()) : null, - "Guest"); - var body = ProfileDetailsJsonBuilder.buildBody(sampleFields, profileType); - String jsonPreview = JSON.toJSONString(body, com.alibaba.fastjson2.JSONWriter.Feature.PrettyFormat); - - EsignSignFieldOhipPreviewResponse.PreviewData data = - new EsignSignFieldOhipPreviewResponse.PreviewData(); - data.setProfileBody(JSON.parseObject(body.toJSONString(), Map.class)); - data.setJsonPreview(jsonPreview); - data.setMappedFieldCount(sampleFields.size()); - return EsignSignFieldOhipPreviewResponse.ok(data); - } - - @Transactional - public EsignSignFieldOhipMappingResponse save(EsignSignFieldOhipMappingSaveRequest request) { - if (request == null) { - return EsignSignFieldOhipMappingResponse.fail("400", "request required"); - } - String hotelId = normalizeHotelId(request.getHotelId()); - List mappings = - SignFieldOhipValueMapper.normalizeMappings(request.getMappings()); - - EsignSignFieldOhipConfig row = hotelId == null - ? ensureDefaultRow() - : configRepository - .findFirstByHotelIdOrderByUpdatedAtDesc(hotelId) - .orElseGet(() -> { - EsignSignFieldOhipConfig created = new EsignSignFieldOhipConfig(); - created.setConfigId(generateConfigId(hotelId)); - created.setHotelId(hotelId); - return created; - }); - - row.setMappingsJson(JSON.toJSONString(mappings)); - row.setUpdatedAt(Instant.now()); - configRepository.save(row); - return buildOk(row); - } - - private EsignSignFieldOhipConfig resolveConfig(String hotelId) { - String normalized = normalizeHotelId(hotelId); - if (normalized != null) { - EsignSignFieldOhipConfig hotelRow = - configRepository.findFirstByHotelIdOrderByUpdatedAtDesc(normalized).orElse(null); - if (hotelRow != null) { - return hotelRow; - } - } - return ensureDefaultRow(); - } - - private EsignSignFieldOhipConfig ensureDefaultRow() { - return configRepository - .findById(DEFAULT_CONFIG_ID) - .orElseGet(() -> { - EsignSignFieldOhipConfig row = new EsignSignFieldOhipConfig(); - row.setConfigId(DEFAULT_CONFIG_ID); - row.setHotelId(null); - row.setMappingsJson("[]"); - row.setUpdatedAt(Instant.now()); - return configRepository.save(row); - }); - } - - private List parseMappings(String json) { - if (StringUtils.isBlank(json)) { - return Collections.emptyList(); - } - try { - List list = - JSON.parseArray(json.trim(), SignFieldOhipMappingItem.class); - return SignFieldOhipValueMapper.normalizeMappings(list); - } catch (Exception e) { - return Collections.emptyList(); - } - } - - private EsignSignFieldOhipMappingResponse buildOk(EsignSignFieldOhipConfig row) { - EsignSignFieldOhipMappingResponse resp = EsignSignFieldOhipMappingResponse.ok("ok"); - EsignSignFieldOhipMappingResponse.SignFieldOhipMappingData data = - new EsignSignFieldOhipMappingResponse.SignFieldOhipMappingData(); - EsignSignFieldOhipMappingResponse.SignFieldOhipMappingConfigItem item = - new EsignSignFieldOhipMappingResponse.SignFieldOhipMappingConfigItem(); - item.setConfigId(row.getConfigId()); - item.setHotelId(row.getHotelId()); - item.setMappings(parseMappings(row.getMappingsJson())); - item.setUpdatedAt(row.getUpdatedAt() != null ? row.getUpdatedAt().toString() : null); - data.setConfig(item); - resp.setData(data); - return resp; - } - - private static String normalizeHotelId(String hotelId) { - if (StringUtils.isBlank(hotelId)) { - return null; - } - try { - return EsignReservationParamUtil.normalizeParam(hotelId); - } catch (Exception e) { - return StringUtils.trimToNull(hotelId); - } - } - - private static String generateConfigId(String hotelId) { - return "HOTEL_" + StringUtils.defaultString(hotelId).replaceAll("[^A-Za-z0-9]", "_"); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/ReservationThirdPartyClient.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/ReservationThirdPartyClient.java deleted file mode 100644 index 907c453..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/ReservationThirdPartyClient.java +++ /dev/null @@ -1,733 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.services; - -import com.sjmresorts.opsapp.common.dtos.httpclient.HttpResponseResult; -import com.sjmresorts.opsapp.common.utils.SimpleHttpClient; -import com.sjmresorts.opsapp.esign.audit.services.EsignAuditLogService; -import com.sjmresorts.opsapp.esign.audit.support.ThirdPartyAuditDetailBuilder; -import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationThirdPartyResult; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; -import com.sjmresorts.opsapp.esign.reservation.support.HospitalityOAuthTokenSingleton; -import com.sjmresorts.opsapp.esign.reservation.dtos.GuestProfileRef; -import com.sjmresorts.opsapp.esign.reservation.support.ProfileDetailsJsonBuilder; -import com.sjmresorts.opsapp.esign.common.support.ProfileDetailsJsonReader; -import com.sjmresorts.opsapp.esign.reservation.support.ReservationDetailResponseParser; -import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationLookupHit; -import com.sjmresorts.opsapp.esign.reservation.support.ReservationLookupResponseParser; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Base64; -import java.util.Map; -import java.util.Optional; - -@Slf4j -@Service -public class ReservationThirdPartyClient { - - private static final int RESPONSE_BODY_LOG_MAX = 2000; - - private final HospitalityOAuthTokenSingleton oauthTokenSingleton = HospitalityOAuthTokenSingleton.getInstance(); - - @Value("${ops-app.esign.reservation.third-party.base-url:}") - private String baseUrl; - - @Value("${ops-app.esign.reservation.third-party.timeout-ms:30000}") - private int timeoutMs; - - @Value("${ops-app.esign.reservation.third-party.registration-card.regenerate:false}") - private boolean regenerate; - - @Value("${ops-app.esign.reservation.third-party.registration-card.signed-only:false}") - private boolean signedOnly; - - @Value("${ops-app.esign.reservation.third-party.registration-card.reservation-id-context:OPERA}") - private String reservationIdContext; - - @Value("${ops-app.esign.reservation.third-party.registration-card.reservation-id-type:Reservation}") - private String reservationIdType; - - @Value("${ops-app.esign.reservation.third-party.registration-card.language:EN}") - private String language; - - @Value("${ops-app.esign.reservation.third-party.signed-upload.enabled:true}") - private boolean signedUploadEnabled; - - @Value("${ops-app.esign.reservation.third-party.reservation-lookup.path-template:/rsv/v1/hotels/{hotelId}/reservations}") - private String reservationLookupPathTemplate; - - @Value("${ops-app.esign.reservation.third-party.reservation-lookup.fetch-instructions:Reservation}") - private String reservationLookupFetchInstructions; - - @Value("${ops-app.esign.reservation.third-party.reservation-detail.path-template:/rsv/v1/hotels/{hotelId}/reservations/{reservationId}}") - private String reservationDetailPathTemplate; - - @Value("${ops-app.esign.reservation.third-party.file-attachment.path:/med/config/v1/fileAttachments}") - private String fileAttachmentPath; - - @Value("${ops-app.esign.reservation.third-party.file-attachment.user-name:USER@HOTEL}") - private String fileAttachmentUserName; - - @Value("${ops-app.esign.reservation.third-party.file-attachment.description:E}") - private String fileAttachmentDescription; - - @Value("${ops-app.esign.reservation.third-party.file-attachment.link-type:Reservation}") - private String fileAttachmentLinkType; - - @Value("${ops-app.esign.reservation.third-party.file-attachment.overwrite-existing:Y}") - private String fileAttachmentOverwrite; - - @Value("${ops-app.esign.reservation.third-party.file-attachment.global-yn:N}") - private String fileAttachmentGlobalYn; - - @Value("${ops-app.esign.reservation.third-party.profile-fields.enabled:false}") - private boolean profileFieldsEnabled; - - @Value("${ops-app.esign.reservation.third-party.profile-fields.path-template:/crm/v1/profiles/{profileId}}") - private String profileFieldsPathTemplate; - - @Resource(name = "esignAuditLogService") - private EsignAuditLogService auditLogService; - - /** notify 流程仍跳过第三方,仅发推送 */ - public ReservationThirdPartyResult notifyReservation(String reservationId, String hotelId) { - log.debug("notifyReservation skips third party, reservationId={}, hotelId={}", reservationId, hotelId); - return ReservationThirdPartyResult.skipped("Third party disabled in notify flow"); - } - - /** - * 签名登记卡提交 OPERA:确认号 → reservation id → fileAttachments。 - */ - public SignedRegistrationCardUploadResult uploadSignedRegistrationCard( - String hotelId, - String confirmationNumber, - String signedPdfBase64, - String preferredFileName) { - log.info( - "OHIP uploadSignedRegistrationCard start hotelId={}, confirmation={}, signedPdfLen={}, fileName={}", - hotelId, - confirmationNumber, - StringUtils.length(signedPdfBase64), - preferredFileName); - if (!signedUploadEnabled) { - log.info("OHIP uploadSignedRegistrationCard skip: signed-upload disabled"); - return SignedRegistrationCardUploadResult.skipped("Signed upload disabled"); - } - if (StringUtils.isBlank(baseUrl)) { - log.info("OHIP uploadSignedRegistrationCard skip: base-url not configured"); - return SignedRegistrationCardUploadResult.skipped("Third party base-url not configured"); - } - if (!oauthTokenSingleton.isConfigured()) { - log.info("OHIP uploadSignedRegistrationCard skip: OAuth not configured"); - return SignedRegistrationCardUploadResult.skipped("OHIP OAuth is not configured"); - } - if (StringUtils.isBlank(confirmationNumber)) { - return SignedRegistrationCardUploadResult.failure( - 400, null, "confirmationNumber required"); - } - if (StringUtils.isBlank(signedPdfBase64)) { - log.warn("OHIP uploadSignedRegistrationCard fail: signed PDF empty"); - return SignedRegistrationCardUploadResult.failure( - 400, null, "signed PDF base64 required"); - } - - String ohipHotelId = resolveOhipHotelId(hotelId); - String confNo = confirmationNumber.trim(); - log.info("OHIP step1 reservation lookup hotelId={} (resolved={}), confirmation={}", hotelId, ohipHotelId, confNo); - - Optional lookupHit = lookupReservationByConfirmation(ohipHotelId, confNo); - if (lookupHit.isEmpty()) { - log.warn("OHIP step1 reservation lookup miss confirmation={}", confNo); - return SignedRegistrationCardUploadResult.failure( - 404, null, "Reservation not found for confirmation " + confNo); - } - ReservationLookupHit hit = lookupHit.get(); - String reservationId = hit.reservationId(); - log.info("OHIP step1 reservation lookup hit confirmation={}, reservationId={}", confNo, reservationId); - SignedRegistrationCardUploadResult uploaded = uploadFileAttachment( - ohipHotelId, reservationId, confNo, signedPdfBase64, preferredFileName); - attachGuestProfile(uploaded, hit.guestProfile()); - return uploaded; - } - - private static void attachGuestProfile( - SignedRegistrationCardUploadResult result, GuestProfileRef guestProfile) { - if (result == null || guestProfile == null || !result.isSuccess()) { - return; - } - if (StringUtils.isNotBlank(guestProfile.profileId())) { - result.setProfileId(guestProfile.profileId()); - } - if (StringUtils.isNotBlank(guestProfile.profileType())) { - result.setProfileType(guestProfile.profileType()); - } - } - - /** - * 已知 reservation id 时直接上传(登记卡通知流程),跳过确认号查询。 - */ - public SignedRegistrationCardUploadResult uploadSignedRegistrationCardByReservationId( - String hotelId, - String reservationId, - String confirmationNumber, - String signedPdfBase64, - String preferredFileName) { - if (!signedUploadEnabled) { - return SignedRegistrationCardUploadResult.skipped("Signed upload disabled"); - } - if (StringUtils.isBlank(baseUrl)) { - return SignedRegistrationCardUploadResult.skipped("Third party base-url not configured"); - } - if (!oauthTokenSingleton.isConfigured()) { - return SignedRegistrationCardUploadResult.skipped("OHIP OAuth is not configured"); - } - if (StringUtils.isAnyBlank(reservationId, signedPdfBase64)) { - return SignedRegistrationCardUploadResult.failure( - 400, null, "reservationId and signed PDF base64 required"); - } - String ohipHotelId = resolveOhipHotelId(hotelId); - String confNo = StringUtils.defaultIfBlank(confirmationNumber, reservationId.trim()); - return uploadFileAttachment( - ohipHotelId, reservationId.trim(), confNo, signedPdfBase64, preferredFileName); - } - - public Optional lookupReservationIdByConfirmation(String hotelId, String confirmationNumber) { - return lookupReservationByConfirmation(hotelId, confirmationNumber).map(ReservationLookupHit::reservationId); - } - - public Optional lookupReservationByConfirmation(String hotelId, String confirmationNumber) { - String ohipHotelId = resolveOhipHotelId(hotelId); - if (StringUtils.isAnyBlank(baseUrl, ohipHotelId, confirmationNumber)) { - log.warn( - "OHIP reservation lookup skip: baseUrl={}, hotelId={}, resolvedHotelId={}, confirmation={}", - StringUtils.isNotBlank(baseUrl), - hotelId, - ohipHotelId, - confirmationNumber); - return Optional.empty(); - } - if (!oauthTokenSingleton.isConfigured()) { - log.warn("OHIP reservation lookup skip: OAuth not configured"); - return Optional.empty(); - } - String path = reservationLookupPathTemplate.replace("{hotelId}", urlEncodePathSegment(ohipHotelId)); - String url = SimpleHttpClient.joinUrl(baseUrl, path); - try { - SimpleHttpClient.RequestBuilder request = SimpleHttpClient.get(url) - .timeoutMs(timeoutMs) - .queryParam("confirmationNumberList", confirmationNumber.trim()) - .queryParam("fetchInstructions", reservationLookupFetchInstructions); - applyBusinessAuth(request, ohipHotelId); - log.info( - "OHIP GET reservations request url={}, hotelId={}, confirmationNumber={}, headers={}", - request.getRequestUrlWithQuery(), - ohipHotelId, - confirmationNumber, - SimpleHttpClient.maskHeadersForLog(request.getHeaders())); - long start = System.currentTimeMillis(); - HttpResponseResult response = request.execute(); - auditThirdParty( - EsignAuditLogService.EVT_OHIP_GET_RESERVATIONS, - request, - response, - start, - ohipHotelId, - "GET reservations confirmation=" + confirmationNumber); - String body = response.getResponseContent(); - log.info( - "OHIP GET reservations response http={}, body={}", - response.getStatusCode(), - truncate(body)); - if (response.isTransportError()) { - log.error("OHIP GET reservations transport error: {}", response.errorOrEmpty()); - return Optional.empty(); - } - if (!response.isSuccessful()) { - return Optional.empty(); - } - Optional reservationId = - ReservationLookupResponseParser.extractReservationId(body, confirmationNumber.trim()); - Optional guestProfile = ReservationDetailResponseParser.extractGuestProfile(body); - if (reservationId.isPresent()) { - log.info( - "OHIP GET reservations parsed reservationId={} profileId={} for confirmation={}", - reservationId.get(), - guestProfile.map(GuestProfileRef::profileId).orElse(null), - confirmationNumber); - return Optional.of(new ReservationLookupHit(reservationId.get(), guestProfile.orElse(null))); - } else { - log.warn( - "OHIP GET reservations HTTP 200 but no reservationId for confirmation={}, body={}", - confirmationNumber, - truncate(body)); - } - return Optional.empty(); - } catch (Exception e) { - log.error("OHIP GET reservations failed", e); - return Optional.empty(); - } - } - - public Optional fetchGuestProfileId(String hotelId, String reservationId) { - return fetchGuestProfile(hotelId, reservationId).map(GuestProfileRef::profileId); - } - - /** - * GET /rsv/v1/hotels/{hotelId}/reservations/{reservationId}?fetchInstructions=Reservation - * 解析 reservationGuests[].profileInfo 中的 profileId 与 profile.profileType。 - */ - public Optional fetchGuestProfile(String hotelId, String reservationId) { - String reservationKey = StringUtils.trimToNull(reservationId); - if (StringUtils.isNotBlank(reservationKey) && "printer-import".equalsIgnoreCase(reservationKey)) { - reservationKey = null; - } - String ohipHotelId = resolveOhipHotelId(hotelId); - if (StringUtils.isAnyBlank(baseUrl, ohipHotelId, reservationKey)) { - log.warn( - "OHIP reservation detail skip: baseUrl={}, hotelId={}, resolvedHotelId={}, reservationId={}", - StringUtils.isNotBlank(baseUrl), - hotelId, - ohipHotelId, - reservationId); - return Optional.empty(); - } - if (!oauthTokenSingleton.isConfigured()) { - log.warn("OHIP reservation detail skip: OAuth not configured"); - return Optional.empty(); - } - String path = reservationDetailPathTemplate - .replace("{hotelId}", urlEncodePathSegment(ohipHotelId)) - .replace("{reservationId}", urlEncodePathSegment(reservationKey)); - String url = SimpleHttpClient.joinUrl(baseUrl, path); - try { - SimpleHttpClient.RequestBuilder request = SimpleHttpClient.get(url) - .timeoutMs(timeoutMs) - .queryParam("fetchInstructions", reservationLookupFetchInstructions); - applyBusinessAuth(request, ohipHotelId); - log.info( - "OHIP GET reservation detail request url={}, hotelId={}, reservationId={}, headers={}", - request.getRequestUrlWithQuery(), - ohipHotelId, - reservationKey, - SimpleHttpClient.maskHeadersForLog(request.getHeaders())); - long start = System.currentTimeMillis(); - HttpResponseResult response = request.execute(); - auditThirdParty( - EsignAuditLogService.EVT_OHIP_GET_RESERVATIONS, - request, - response, - start, - ohipHotelId, - "GET reservation detail reservationId=" + reservationId); - String body = response.getResponseContent(); - log.info( - "OHIP GET reservation detail response http={}, body={}", - response.getStatusCode(), - truncate(body)); - if (response.isTransportError()) { - log.error("OHIP GET reservation detail transport error: {}", response.errorOrEmpty()); - return Optional.empty(); - } - if (!response.isSuccessful()) { - return Optional.empty(); - } - Optional profile = ReservationDetailResponseParser.extractGuestProfile(body); - if (profile.isPresent()) { - log.info( - "OHIP GET reservation detail profileId={}, profileType={}, reservationId={}", - profile.get().profileId(), - profile.get().profileType(), - reservationKey); - } else { - log.warn( - "OHIP GET reservation detail HTTP 200 but no profile reservationId={}, body={}", - reservationId, - truncate(body)); - } - return profile; - } catch (Exception e) { - log.error("OHIP GET reservation detail failed reservationId={}", reservationId, e); - return Optional.empty(); - } - } - - /** - * GET /crm/v1/profiles/{profileId},返回 profileDetails 对象(用于签名字段预填)。 - */ - public Optional fetchGuestProfileDetails(String hotelId, String profileId) { - if (StringUtils.isAnyBlank(baseUrl, profileId)) { - return Optional.empty(); - } - if (!oauthTokenSingleton.isConfigured()) { - log.warn("OHIP GET profile skip: OAuth not configured"); - return Optional.empty(); - } - String ohipHotelId = StringUtils.isNotBlank(hotelId) ? resolveOhipHotelId(hotelId) : null; - String path = profileFieldsPathTemplate.replace( - "{profileId}", urlEncodePathSegment(profileId.trim())); - String url = SimpleHttpClient.joinUrl(baseUrl, path); - try { - SimpleHttpClient.RequestBuilder request = SimpleHttpClient.get(url).timeoutMs(timeoutMs); - if (StringUtils.isNotBlank(ohipHotelId)) { - applyBusinessAuth(request, ohipHotelId); - } - log.info( - "OHIP GET profile request url={}, profileId={}, headers={}", - url, - profileId, - SimpleHttpClient.maskHeadersForLog(request.getHeaders())); - long start = System.currentTimeMillis(); - HttpResponseResult response = request.execute(); - auditThirdParty( - EsignAuditLogService.EVT_OHIP_PUT_PROFILE, - request, - response, - start, - ohipHotelId, - "GET profile profileId=" + profileId); - String body = response.getResponseContent(); - log.info( - "OHIP GET profile response http={}, body={}", - response.getStatusCode(), - truncate(body)); - if (response.isTransportError()) { - log.error("OHIP GET profile transport error: {}", response.errorOrEmpty()); - return Optional.empty(); - } - if (!response.isSuccessful()) { - return Optional.empty(); - } - JSONObject details = ProfileDetailsJsonReader.unwrapProfileDetails(body); - if (details == null || details.isEmpty()) { - log.warn("OHIP GET profile empty profileId={}, body={}", profileId, truncate(body)); - return Optional.empty(); - } - return Optional.of(details); - } catch (Exception e) { - log.error("OHIP GET profile failed profileId={}", profileId, e); - return Optional.empty(); - } - } - - private SignedRegistrationCardUploadResult uploadFileAttachment( - String hotelId, - String reservationId, - String confirmationNumber, - String signedPdfBase64, - String preferredFileName) { - String url = SimpleHttpClient.joinUrl(baseUrl, fileAttachmentPath); - String fileName = buildSignedFileName(preferredFileName, confirmationNumber); - JSONObject body = new JSONObject(); - body.put("fileName", fileName); - body.put("linkId", reservationId); - body.put("overwriteExistingFileYN", fileAttachmentOverwrite); - body.put("description", fileAttachmentDescription); - body.put("linkType", fileAttachmentLinkType); - body.put("hotelId", hotelId); - body.put("userName", fileAttachmentUserName); - body.put("globalYN", fileAttachmentGlobalYn); - String attachmentBase64 = normalizePdfBase64(signedPdfBase64); - body.put("fileAttachment", attachmentBase64); - logAttachmentPreview(attachmentBase64); - - try { - SimpleHttpClient.RequestBuilder request = SimpleHttpClient.post(url) - .timeoutMs(timeoutMs) - .jsonBody(body.toJSONString()); - applyBusinessAuth(request, hotelId); - log.info( - "OHIP POST fileAttachments request url={}, hotelId={}, reservationId={}, confirmation={}, fileName={}, attachmentLen={}, headers={}", - url, - hotelId, - reservationId, - confirmationNumber, - fileName, - StringUtils.length(signedPdfBase64), - SimpleHttpClient.maskHeadersForLog(request.getHeaders())); - long start = System.currentTimeMillis(); - HttpResponseResult response = request.execute(); - auditThirdParty( - EsignAuditLogService.EVT_OHIP_POST_ATTACHMENT, - request, - response, - start, - hotelId, - "POST fileAttachments confirmation=" + confirmationNumber); - String summary = truncate(response.getResponseContent()); - log.info( - "OHIP POST fileAttachments response http={}, body={}", - response.getStatusCode(), - summary); - if (response.isTransportError()) { - return SignedRegistrationCardUploadResult.failure( - response.getStatusCode(), - summary, - StringUtils.defaultIfBlank(response.getErrorMessage(), "fileAttachments transport error"), - confirmationNumber, - reservationId, - fileName); - } - if (!response.isSuccessful()) { - return SignedRegistrationCardUploadResult.failure( - response.getStatusCode(), - summary, - "fileAttachments returned HTTP " + response.getStatusCode(), - confirmationNumber, - reservationId, - fileName); - } - return SignedRegistrationCardUploadResult.success( - response.getStatusCode(), - confirmationNumber, - reservationId, - fileName, - summary); - } catch (IllegalStateException e) { - log.error("FileAttachment upload failed", e); - return SignedRegistrationCardUploadResult.failure(-1, null, e.getMessage()); - } - } - - /** - * 将 CMS 映射后的文字字段通过 CRM PUT /profiles/{profileId} 更新旅客 Profile(需 profile-fields.enabled=true)。 - */ - public SignedRegistrationCardUploadResult submitMappedProfileFields( - String hotelId, String reservationId, Map fields) { - return submitMappedProfileFields(hotelId, reservationId, fields, null); - } - - public SignedRegistrationCardUploadResult submitMappedProfileFields( - String hotelId, String reservationId, Map fields, String profileId) { - return submitMappedProfileFields(hotelId, reservationId, fields, profileId, null); - } - - public SignedRegistrationCardUploadResult submitMappedProfileFields( - String hotelId, - String reservationId, - Map fields, - String profileId, - String profileType) { - if (fields == null || fields.isEmpty()) { - return SignedRegistrationCardUploadResult.skipped("No mapped profile fields"); - } - if (StringUtils.isBlank(baseUrl)) { - return SignedRegistrationCardUploadResult.skipped("Third party base-url not configured"); - } - if (!oauthTokenSingleton.isConfigured()) { - return SignedRegistrationCardUploadResult.skipped("OHIP OAuth is not configured"); - } - if (!profileFieldsEnabled) { - return SignedRegistrationCardUploadResult.skipped( - "Profile update mapped but disabled (profile-fields.enabled=false)"); - } - - String resolvedProfileId = StringUtils.trimToNull(profileId); - String resolvedProfileType = StringUtils.trimToNull(profileType); - String reservationKey = StringUtils.trimToNull(reservationId); - if (StringUtils.isNotBlank(reservationKey) && "printer-import".equalsIgnoreCase(reservationKey)) { - reservationKey = null; - } - String effectiveHotelId = resolveOhipHotelId(hotelId); - if (StringUtils.isBlank(resolvedProfileId) && StringUtils.isNotBlank(reservationKey)) { - Optional guest = fetchGuestProfile(effectiveHotelId, reservationKey); - if (guest.isPresent()) { - resolvedProfileId = guest.get().profileId(); - if (StringUtils.isBlank(resolvedProfileType)) { - resolvedProfileType = guest.get().profileType(); - } - } else { - log.warn( - "OHIP CRM profile lookup miss hotelId={}, resolvedHotelId={}, reservationId={}", - hotelId, - effectiveHotelId, - reservationKey); - } - } - if (StringUtils.isBlank(resolvedProfileId)) { - log.warn( - "OHIP CRM profile update skipped: profileId unresolved hotelId={}, reservationId={}", - effectiveHotelId, - reservationKey); - return SignedRegistrationCardUploadResult.skipped( - "profileId not resolved for CRM profile update"); - } - - String ohipHotelId = effectiveHotelId; - log.info( - "OHIP CRM profile update prepared reservationId={}, profileId={}, profileType={}, keys={}", - reservationId, - resolvedProfileId, - resolvedProfileType, - fields.keySet()); - - String path = profileFieldsPathTemplate.replace( - "{profileId}", urlEncodePathSegment(resolvedProfileId)); - String url = SimpleHttpClient.joinUrl(baseUrl, path); - JSONObject body = ProfileDetailsJsonBuilder.buildBody(fields, resolvedProfileType); - String bodyJson = body.toJSONString(); - - try { - SimpleHttpClient.RequestBuilder request = SimpleHttpClient.put(url) - .timeoutMs(timeoutMs) - .jsonBody(bodyJson); - applyBusinessAuth(request, ohipHotelId); - log.info( - "OHIP PUT profile request url={}, profileId={}, reservationId={}, fieldCount={}, body={}, headers={}", - url, - resolvedProfileId, - reservationId, - fields.size(), - truncate(bodyJson), - SimpleHttpClient.maskHeadersForLog(request.getHeaders())); - long start = System.currentTimeMillis(); - HttpResponseResult response = request.execute(); - auditThirdParty( - EsignAuditLogService.EVT_OHIP_PUT_PROFILE, - request, - response, - start, - ohipHotelId, - "PUT profile profileId=" + resolvedProfileId + " reservationId=" + reservationId); - String summary = truncate(response.getResponseContent()); - log.info("OHIP PUT profile response http={}, body={}", response.getStatusCode(), summary); - if (response.isTransportError()) { - return SignedRegistrationCardUploadResult.failure( - response.getStatusCode(), - summary, - StringUtils.defaultIfBlank(response.getErrorMessage(), "profile update transport error"), - null, - reservationId, - null); - } - if (!response.isSuccessful()) { - return SignedRegistrationCardUploadResult.failure( - response.getStatusCode(), - summary, - "profile update returned HTTP " + response.getStatusCode(), - null, - reservationId, - null); - } - return SignedRegistrationCardUploadResult.success( - response.getStatusCode(), null, reservationId, null, summary); - } catch (IllegalStateException e) { - log.error("Profile update failed", e); - return SignedRegistrationCardUploadResult.failure(-1, null, e.getMessage()); - } - } - - private static String buildSignedFileName(String preferredFileName, String confirmationNumber) { - String attachmentExt = ".pdf"; - if (StringUtils.isNotBlank(preferredFileName)) { - String trimmed = preferredFileName.trim(); - int dot = trimmed.lastIndexOf('.'); - if (dot > 0 && dot < trimmed.length() - 1) { - attachmentExt = trimmed.substring(dot).toLowerCase(); - } - } - // OPERA fileAttachments:.regcard 需 HTML 内容;我们上传的是签名 PDF,必须用 .pdf - if (".regcard".equals(attachmentExt)) { - attachmentExt = ".pdf"; - } - String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm")); - return "e-SignedRegCard-" + confirmationNumber + "-" + ts + attachmentExt; - } - - private static void logAttachmentPreview(String attachmentBase64) { - if (StringUtils.isBlank(attachmentBase64)) { - log.warn("OHIP fileAttachments attachment base64 empty"); - return; - } - try { - byte[] bytes = Base64.getDecoder().decode(attachmentBase64); - String magic = bytes.length >= 4 - ? new String(bytes, 0, Math.min(16, bytes.length), java.nio.charset.StandardCharsets.ISO_8859_1) - : ""; - log.info( - "OHIP fileAttachments payload decodedBytes={}, magic={}", - bytes.length, - magic.replaceAll("[\\r\\n]", "")); - } catch (IllegalArgumentException e) { - log.warn("OHIP fileAttachments attachment base64 invalid: {}", e.getMessage()); - } - } - - private static String normalizePdfBase64(String raw) { - if (StringUtils.isBlank(raw)) { - return ""; - } - String trimmed = raw.trim(); - int comma = trimmed.indexOf(','); - if (trimmed.startsWith("data:") && comma > 0) { - return trimmed.substring(comma + 1).trim(); - } - return trimmed.replaceAll("\\s", ""); - } - - private String resolveOhipHotelId(String requestHotelId) { - String configured = oauthTokenSingleton.getHotelId(); - if (StringUtils.isNotBlank(configured)) { - return configured.trim(); - } - return StringUtils.defaultString(requestHotelId).trim(); - } - - private static String buildRegistrationCardPath(String hotelId, String reservationId) { - return "/med/config/v1/hotels/" + urlEncodePathSegment(hotelId) - + "/reservations/" + urlEncodePathSegment(reservationId) - + "/registrationCard"; - } - - private static String urlEncodePathSegment(String value) { - return StringUtils.defaultString(value).trim(); - } - - private void applyBusinessAuth(SimpleHttpClient.RequestBuilder request, String hotelId) { - String accessToken = oauthTokenSingleton.getAccessToken(); - Map headers = oauthTokenSingleton.buildBusinessHeaders(hotelId); - request.bearerToken(accessToken).headers(headers); - } - - private static String truncate(String value) { - if (value == null) { - return null; - } - if (value.length() <= RESPONSE_BODY_LOG_MAX) { - return value; - } - return value.substring(0, RESPONSE_BODY_LOG_MAX) + "...(truncated)"; - } - - private void auditThirdParty( - String eventType, - SimpleHttpClient.RequestBuilder request, - HttpResponseResult response, - long startMs, - String hotelId, - String message) { - if (auditLogService == null) { - return; - } - try { - Integer status = response != null ? response.getStatusCode() : null; - String detail = ThirdPartyAuditDetailBuilder.build(request, response); - auditLogService.appendThirdParty( - eventType, - request != null ? request.getMethod() : null, - request != null ? request.getRequestUrlWithQuery() : null, - status, - System.currentTimeMillis() - startMs, - hotelId, - message, - detail); - } catch (Exception e) { - log.debug("audit third party log failed", e); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenConfigurer.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenConfigurer.java deleted file mode 100644 index 1a5226d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenConfigurer.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.support; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.esign.reservation.dtos.HospitalityOAuthSettings; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class HospitalityOAuthTokenConfigurer { - - private final ObjectMapper objectMapper; - - @Value("${ops-app.esign.reservation.third-party.base-url:}") - private String baseUrl; - - @Value("${ops-app.esign.reservation.third-party.oauth.token-path:/oauth/v1/tokens}") - private String tokenPath; - - /** Postman Basic Auth 用户名,对应 CLIENT_ID */ - @Value("${ops-app.esign.reservation.third-party.oauth.client-id:}") - private String clientId; - - /** Postman Basic Auth 密码,对应 CLIENT_SECRET */ - @Value("${ops-app.esign.reservation.third-party.oauth.client-secret:}") - private String clientSecret; - - @Value("${ops-app.esign.reservation.third-party.oauth.app-key:}") - private String appKey; - - @Value("${ops-app.esign.reservation.third-party.oauth.enterprise-id:}") - private String enterpriseId; - - @Value("${ops-app.esign.reservation.third-party.oauth.hotel-id:}") - private String hotelId; - - @Value("${ops-app.esign.reservation.third-party.oauth.scope:}") - private String scope; - - @Value("${ops-app.esign.reservation.third-party.oauth.grant-type:client_credentials}") - private String grantType; - - @Value("${ops-app.esign.reservation.third-party.oauth.timeout-ms:30000}") - private int timeoutMs; - - @Value("${ops-app.esign.reservation.third-party.oauth.enabled:true}") - private boolean oauthEnabled; - - @PostConstruct - public void configureSingleton() { - HospitalityOAuthSettings settings = HospitalityOAuthSettings.builder() - .enabled(oauthEnabled) - .baseUrl(baseUrl) - .tokenPath(tokenPath) - .clientId(clientId) - .clientSecret(clientSecret) - .appKey(appKey) - .enterpriseId(enterpriseId) - .hotelId(hotelId) - .scope(scope) - .grantType(grantType) - .timeoutMs(timeoutMs) - .build(); - HospitalityOAuthTokenSingleton.getInstance().configure(settings, objectMapper); - log.info( - "OHIP OAuth singleton loaded: baseUrl={}, clientId={}, enterpriseId={}, hotelId={}, appKeyPrefix={}, scopeConfigured={}", - baseUrl, - clientId, - enterpriseId, - hotelId, - maskPrefix(appKey), - StringUtils.isNotBlank(scope)); - if (!settings.isComplete()) { - log.warn( - "OHIP OAuth 未完整配置:签名 PDF 归档后不会请求 OPERA(无 THIRD_PARTY 审计日志)。" - + " 请检查 ops-app.esign.reservation.third-party.oauth.* 与 base-url"); - } - } - - private static String maskPrefix(String value) { - if (StringUtils.isBlank(value)) { - return "(empty)"; - } - return value.length() <= 8 ? value : value.substring(0, 8) + "..."; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenSingleton.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenSingleton.java deleted file mode 100644 index a54f059..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/HospitalityOAuthTokenSingleton.java +++ /dev/null @@ -1,240 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.support; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.common.dtos.httpclient.HttpResponseResult; -import com.sjmresorts.opsapp.common.utils.SimpleHttpClient; -import com.sjmresorts.opsapp.esign.reservation.dtos.HospitalityOAuthSettings; -import com.sjmresorts.opsapp.esign.reservation.dtos.OAuthTokenResponse; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.UUID; - -/** - * OHIP OAuth Token 单例:全局仅维护一份 access token,过期前自动刷新。 - */ -public final class HospitalityOAuthTokenSingleton { - - private static final Logger log = LoggerFactory.getLogger(HospitalityOAuthTokenSingleton.class); - private static final long DEFAULT_EXPIRES_SECONDS = 3600; - private static final long REFRESH_BUFFER_SECONDS = 120; - - private static volatile HospitalityOAuthTokenSingleton instance; - private static final Object INSTANCE_LOCK = new Object(); - - private final Object refreshLock = new Object(); - - private volatile HospitalityOAuthSettings settings; - private volatile ObjectMapper objectMapper; - private volatile CachedToken cachedToken; - - private HospitalityOAuthTokenSingleton() { - } - - public static HospitalityOAuthTokenSingleton getInstance() { - if (instance == null) { - synchronized (INSTANCE_LOCK) { - if (instance == null) { - instance = new HospitalityOAuthTokenSingleton(); - } - } - } - return instance; - } - - public void configure(HospitalityOAuthSettings newSettings, ObjectMapper mapper) { - synchronized (INSTANCE_LOCK) { - this.settings = newSettings; - this.objectMapper = mapper; - this.cachedToken = null; - } - log.info("Hospitality OAuth token singleton configured, enabled={}", - newSettings != null && newSettings.isEnabled()); - } - - public boolean isConfigured() { - HospitalityOAuthSettings current = settings; - return current != null && current.isComplete(); - } - - public String getHotelId() { - HospitalityOAuthSettings current = settings; - return current != null ? current.getHotelId() : null; - } - - public String getAppKey() { - HospitalityOAuthSettings current = settings; - return current != null ? current.getAppKey() : null; - } - - public String getAccessToken() { - if (!isConfigured()) { - throw new IllegalStateException("OHIP OAuth is not configured"); - } - - CachedToken current = cachedToken; - if (current != null && current.isValid()) { - return current.accessToken; - } - - synchronized (refreshLock) { - current = cachedToken; - if (current != null && current.isValid()) { - return current.accessToken; - } - return refreshAccessToken(); - } - } - - /** 业务 API Header(如 registrationCard):x-hotelid + x-app-key */ - public Map buildBusinessHeaders(String hotelId) { - HospitalityOAuthSettings current = settings; - if (current == null) { - throw new IllegalStateException("OHIP OAuth is not configured"); - } - Map headers = new LinkedHashMap<>(); - headers.put("x-hotelid", StringUtils.defaultIfBlank(hotelId, current.getHotelId())); - headers.put("x-app-key", current.getAppKey()); - headers.put("Accept", "application/json"); - headers.put("X-Request-Id", UUID.randomUUID().toString()); - return headers; - } - - public Map buildCommonHeaders() { - HospitalityOAuthSettings current = settings; - if (current == null) { - throw new IllegalStateException("OHIP OAuth is not configured"); - } - Map headers = new LinkedHashMap<>(); - headers.put("x-app-key", current.getAppKey()); - headers.put("enterpriseId", current.getEnterpriseId()); - headers.put("X-Request-Id", UUID.randomUUID().toString()); - return headers; - } - - /** 仅用于测试或强制续期。 */ - public void invalidate() { - cachedToken = null; - } - - private static Map buildOAuthTokenHeaders(HospitalityOAuthSettings current, String requestId) { - // 与 Postman /oauth/v1/tokens 一致:仅 x-app-key、enterpriseId、X-Request-Id(不含 hotelId) - Map headers = new LinkedHashMap<>(); - headers.put("x-app-key", current.getAppKey()); - headers.put("enterpriseId", current.getEnterpriseId()); - headers.put("X-Request-Id", requestId); - return headers; - } - - private String refreshAccessToken() { - HospitalityOAuthSettings current = settings; - ObjectMapper mapper = objectMapper; - if (current == null || mapper == null) { - throw new IllegalStateException("OHIP OAuth is not configured"); - } - - String url = SimpleHttpClient.joinUrl(current.getBaseUrl(), current.getTokenPath()); - String requestId = UUID.randomUUID().toString(); - Map form = new LinkedHashMap<>(); - form.put("grant_type", StringUtils.defaultIfBlank(current.getGrantType(), "client_credentials")); - if (StringUtils.isNotBlank(current.getScope())) { - form.put("scope", current.getScope().trim()); - } - - SimpleHttpClient.RequestBuilder requestBuilder = SimpleHttpClient.post(url) - .basicAuth(current.getClientId(), current.getClientSecret()) - .headers(buildOAuthTokenHeaders(current, requestId)) - .formBody(form) - .timeoutMs(current.getTimeoutMs()); - - String basicAuthBase64 = SimpleHttpClient.buildBasicAuthBase64( - current.getClientId(), current.getClientSecret()); - - log.info( - "OHIP OAuth token request: method={}, url={}, headers={}, basicAuth={{clientId={}, clientSecret={}}}, body={}", - requestBuilder.getMethod(), - requestBuilder.getRequestUrlWithQuery(), - SimpleHttpClient.maskHeadersForLog(requestBuilder.getHeaders()), - current.getClientId(), - maskSecret(current.getClientSecret()), - form); - log.info( - "OHIP OAuth Authorization compare with Postman: Authorization: Basic {}", - basicAuthBase64); - - HttpResponseResult response = requestBuilder.execute(); - - if (response.isTransportError()) { - log.error("OHIP OAuth token request failed: error={}", response.errorOrEmpty()); - throw new IllegalStateException("OAuth token request failed: " + response.errorOrEmpty()); - } - if (!response.isSuccessful()) { - log.warn("OHIP OAuth token request rejected: status={}, body={}", - response.getStatusCode(), truncate(response.bodyOrEmpty())); - throw new IllegalStateException( - "OAuth token request returned HTTP " + response.getStatusCode() + ": " + response.bodyOrEmpty()); - } - - try { - OAuthTokenResponse tokenResponse = mapper.readValue(response.bodyOrEmpty(), OAuthTokenResponse.class); - if (tokenResponse == null || StringUtils.isBlank(tokenResponse.getAccessToken())) { - throw new IllegalStateException("OAuth token response missing access_token"); - } - - long expiresIn = tokenResponse.getExpiresIn() != null && tokenResponse.getExpiresIn() > 0 - ? tokenResponse.getExpiresIn() - : DEFAULT_EXPIRES_SECONDS; - long effectiveSeconds = Math.max(60, expiresIn - REFRESH_BUFFER_SECONDS); - cachedToken = new CachedToken( - tokenResponse.getAccessToken(), - Instant.now().plusSeconds(effectiveSeconds)); - - log.info("OHIP OAuth token obtained, expiresIn={}s, accessTokenPrefix={}", - expiresIn, maskSecret(tokenResponse.getAccessToken())); - return cachedToken.accessToken; - } catch (IllegalStateException e) { - throw e; - } catch (Exception e) { - throw new IllegalStateException("Failed to parse OAuth token response: " + e.getMessage(), e); - } - } - - private static final class CachedToken { - private final String accessToken; - private final Instant expiresAt; - - private CachedToken(String accessToken, Instant expiresAt) { - this.accessToken = accessToken; - this.expiresAt = expiresAt; - } - - private boolean isValid() { - return Instant.now().isBefore(expiresAt); - } - } - - private static String maskSecret(String value) { - if (StringUtils.isBlank(value)) { - return "(empty)"; - } - if (value.length() <= 8) { - return "****"; - } - return value.substring(0, 4) + "****" + value.substring(value.length() - 4); - } - - private static String truncate(String value) { - if (value == null) { - return null; - } - int max = 500; - if (value.length() <= max) { - return value; - } - return value.substring(0, max) + "...(truncated)"; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/OperaFlexLinkPageBuilder.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/OperaFlexLinkPageBuilder.java deleted file mode 100644 index e5231e5..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/OperaFlexLinkPageBuilder.java +++ /dev/null @@ -1,797 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.support; - -import org.apache.commons.lang3.StringUtils; - -/** - * Opera Flex Link 页面。 - * Opera iframe(内层):只 postMessage 发信号,不操作外层 DOM。 - * Opera 主页面(外层代理):注入 opera-parent-close-listener.js 负责销毁 Pop-up 外壳。 - * 独立浏览器标签:myapp:// + window.close()。 - */ -public final class OperaFlexLinkPageBuilder { - - private OperaFlexLinkPageBuilder() { - } - - public static String buildLaunchPage( - String deepLinkUrl, - String launchMode, - boolean useBrowserDeepLink, - String serverLaunchMessage, - boolean closePopupEnabled, - boolean closeModalMessage, - String configuredParentOrigin, - String flexLinkOrigin, - long fallbackExtAppId, - int openDelayMs, - int closeDelayMs, - boolean debugPage, - String popupWidth, - String popupHeight) { - String link = escapeJsString(deepLinkUrl); - String launchMsg = escapeJsString(StringUtils.defaultString(serverLaunchMessage)); - String mode = normalizeLaunchMode(launchMode); - int openWaitMs = useBrowserDeepLink ? Math.max(600, openDelayMs) : Math.max(150, openDelayMs); - int closeRetryMs = Math.max(80, closeDelayMs); - String closeScript = buildCloseScript( - mode, closePopupEnabled, closeModalMessage, configuredParentOrigin, flexLinkOrigin, - fallbackExtAppId, openWaitMs, closeRetryMs, debugPage, popupWidth, popupHeight); - boolean newWindow = "new-window".equals(mode); - - if (newWindow) { - return """ - - - - - - 登记程序 - - - -
%s
- - - - %s - - - - """.formatted( - useBrowserDeepLink - ? "正在唤起登记程序…
若浏览器询问是否打开应用,请先点击「打开」。登记程序启动后本标签将自动关闭。" - : escapeHtml("登记程序已由本机服务启动。" + (StringUtils.isNotBlank(serverLaunchMessage) ? " " + serverLaunchMessage : "") + " 本页将自动关闭。"), - useBrowserDeepLink ? "再次打开登记程序" : "再次启动", - debugPanelHtml(debugPage), - closeScript, - link, - useBrowserDeepLink, - launchMsg, - openWaitMs, - closeRetryMs); - } - - String hintHtml = debugPage - ? (useBrowserDeepLink ? "正在唤起登记程序并关闭 Pop-up…" : escapeHtml("登记程序已由本机服务启动。")) - : ""; - String bodyClass = debugPage ? "" : " stealth"; - String extraStyles = debugPage ? "" : """ - html,body{margin:0!important;padding:0!important;background:transparent!important;overflow:hidden!important} - body.stealth .warn,body.stealth .ok,body.stealth .btn,body.stealth .btn2{display:none!important} - """; - String manualButtonsHtml = debugPage - ? """ - - - """ - : ""; - - return """ - - - - - - 登记程序 - - - - %s -
%s
-
- %s - %s - - - - """.formatted( - extraStyles, - bodyClass.trim(), - debugPage ? "" : "", - hintHtml, - manualButtonsHtml, - debugPanelHtml(debugPage), - closeScript, - link, - useBrowserDeepLink, - openWaitMs, - closeRetryMs, - debugPage); - } - - public static String buildNotifyPage( - boolean success, - String message, - String reservationId, - String launchMode, - boolean closePopupEnabled, - boolean closeModalMessage, - String configuredParentOrigin, - String flexLinkOrigin, - long fallbackExtAppId, - int openDelayMs, - int closeDelayMs, - boolean debugPage, - String popupWidth, - String popupHeight) { - String statusText = success ? "已通知前台" : "通知失败"; - String detail = escapeHtml(StringUtils.defaultIfBlank(message, statusText)); - String rid = escapeHtml(StringUtils.defaultIfBlank(reservationId, "—")); - String mode = normalizeLaunchMode(launchMode); - int openWaitMs = Math.max(600, openDelayMs); - int closeRetryMs = Math.max(80, closeDelayMs); - String closeScript = buildCloseScript( - mode, closePopupEnabled, closeModalMessage, configuredParentOrigin, flexLinkOrigin, - fallbackExtAppId, openWaitMs, closeRetryMs, debugPage, popupWidth, popupHeight); - String boxClass = success ? "ok" : "err"; - int notifyCloseDelay = "new-window".equals(mode) ? openWaitMs : Math.max(50, closeRetryMs); - String initScript = "new-window".equals(mode) - ? "setTimeout(function () { var r = closeLaunchWindow(); renderLaunchDebug(r); }, " + notifyCloseDelay + ");" - : "initOperaPopupSession(); setTimeout(function () { var r = closeLaunchWindow(); renderLaunchDebug(r); }, " - + notifyCloseDelay + ");"; - - return """ - - - - - - %s - - - -
- %s
预订号:%s
%s -
- %s - - - - """.formatted( - statusText, - boxClass, - statusText, - rid, - detail, - debugPanelHtml(debugPage), - closeScript, - initScript); - } - - private static String normalizeLaunchMode(String launchMode) { - return "popup".equalsIgnoreCase(StringUtils.trimToEmpty(launchMode)) ? "popup" : "new-window"; - } - - private static String debugPanelHtml(boolean debugPage) { - if (!debugPage) { - return ""; - } - return "
调试信息
"; - } - - /** - * 内层脚本:Opera iframe 内只 emit 信号;独立标签页才 window.close()。 - * 外层脚本见 {@link #buildOperaParentCloseListenerScript(String)}。 - */ - private static String buildCloseScript( - String launchMode, - boolean closeEnabled, - boolean closeModalMessage, - String configuredParentOrigin, - String flexLinkOrigin, - long fallbackExtAppId, - int openDelayMs, - int closeRetryMs, - boolean debugPage, - String popupWidth, - String popupHeight) { - String origin = escapeJsString(StringUtils.defaultString(configuredParentOrigin)); - String flexOrigin = escapeJsString(StringUtils.defaultIfBlank(flexLinkOrigin, "http://127.0.0.1:8066")); - String proxyInstallUrl = escapeJsString(StringUtils.defaultIfBlank(flexLinkOrigin, "http://127.0.0.1:8066") - + "/api/v1/esign/app/opera-parent-close-listener.user.js"); - String mode = escapeJsString(normalizeLaunchMode(launchMode)); - int popupCloseDelayMs = Math.max(50, closeRetryMs); - return """ - (function () { - var launchMode = "{{LAUNCH_MODE}}"; - var closeEnabled = {{CLOSE_ENABLED}}; - var closeModalMessage = {{CLOSE_MODAL_MESSAGE}}; - var operaOrigin = "{{OPERA_ORIGIN}}"; - var flexLinkOrigin = "{{FLEX_LINK_ORIGIN}}"; - var proxyInstallUrl = "{{PROXY_INSTALL_URL}}"; - var fallbackExtAppId = {{FALLBACK_EXT_APP_ID}}; - var openDelayMs = {{OPEN_DELAY_MS}}; - var closeDelayMs = {{CLOSE_DELAY_MS}}; - var debugPage = {{DEBUG_PAGE}}; - var lastDetail = ""; - var shellProxyAck = false; - - function isOperaPopupIframe() { - try { - if (window.self !== window.top) return true; - } catch (e) { - return true; - } - return false; - } - - function resolveExtAppId() { - try { - var q = new URLSearchParams(location.search).get("ocextappid"); - if (q) return parseInt(q, 10); - } catch (e) {} - return fallbackExtAppId > 0 ? fallbackExtAppId : null; - } - - function resolveOperaOrigin() { - if (operaOrigin) return operaOrigin; - try { - if (document.referrer) return new URL(document.referrer).origin; - } catch (e) {} - return null; - } - - function postToWindow(win, payload, targetOrigin, label) { - try { - win.postMessage(payload, targetOrigin); - return label + "@" + targetOrigin; - } catch (e1) { - try { - win.postMessage(payload, "*"); - return label + "@*"; - } catch (e2) { - return label + ":fail"; - } - } - } - - /** 向内层→外层:纯 postMessage,不触碰 parent DOM */ - function emitShellSignal(action, extra) { - if (!closeModalMessage && (action === "CLOSE_SHELL" || action === "closeModal")) { - return action + ":disabled"; - } - var targetOrigin = resolveOperaOrigin(); - if (!targetOrigin) return action + ":skip no origin"; - var payload = { action: action, source: "auto-check-in-launch", ts: Date.now() }; - if (extra) { - for (var k in extra) { - if (Object.prototype.hasOwnProperty.call(extra, k)) payload[k] = extra[k]; - } - } - var sent = []; - var wins = []; - try { if (window.parent && window.parent !== window) wins.push(["parent", window.parent]); } catch (e) {} - try { if (window.top && window.top !== window) wins.push(["top", window.top]); } catch (e2) {} - for (var i = 0; i < wins.length; i++) { - var r = postToWindow(wins[i][1], payload, targetOrigin, wins[i][0]); - if (r.indexOf(":fail") < 0) sent.push(r); - } - return sent.length ? (action + "->" + sent.join(",")) : (action + ":skip"); - } - - function emitCloseShellBurst() { - var delays = [0, 40, 90, 180]; - delays.forEach(function (ms) { - setTimeout(function () { - emitShellSignal("CLOSE_SHELL"); - emitShellSignal("closeModal"); - }, ms); - }); - return "CLOSE_SHELL burst x" + delays.length; - } - - function emitDoReturnSignal() { - var appId = resolveExtAppId(); - var targetOrigin = resolveOperaOrigin(); - if (!appId || !targetOrigin) return "DO_RETURN:skip"; - var payload = { ocextappid: appId, eventId: "DO_RETURN_EVENT" }; - var sent = false; - var wins = []; - try { if (window.parent && window.parent !== window) wins.push(window.parent); } catch (e) {} - try { if (window.top && window.top !== window) wins.push(window.top); } catch (e2) {} - for (var i = 0; i < wins.length; i++) { - var r = postToWindow(wins[i], payload, targetOrigin, "doReturn"); - if (r.indexOf(":fail") < 0) sent = true; - } - return sent ? "DO_RETURN sent" : "DO_RETURN:skip"; - } - - function emitOperaSetupSignal(eventId, args) { - var appId = resolveExtAppId(); - var targetOrigin = resolveOperaOrigin(); - if (!appId || !targetOrigin) return null; - var payload = { ocextappid: appId, eventId: eventId }; - if (args) payload.args = args; - try { - return postToWindow(window.top, payload, targetOrigin, eventId); - } catch (e) { - return eventId + " error"; - } - } - - function showProxyMissingHelp() { - if (shellProxyAck || !isOperaPopupIframe()) return; - var el = document.getElementById("proxyHelp"); - if (!el) { - el = document.createElement("div"); - el.id = "proxyHelp"; - el.style.cssText = "margin:12px 0;padding:14px;background:#ffebee;border:2px solid #e57373;border-radius:8px;text-align:left;font-size:13px;color:#b71c1c;line-height:1.6"; - var anchor = document.getElementById("hintBox") || document.body.firstChild; - if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(el, anchor.nextSibling); - else document.body.insertBefore(el, document.body.firstChild); - } - el.innerHTML = - "Pop-up 未关闭:Opera 主页面缺少外层代理

" + - "内层已发 CLOSE_SHELL,但未收到 SHELL_CLOSE_ACK。

" + - "一次性安装(推荐 Tampermonkey):
" + - "1. 安装 Tampermonkey 扩展
" + - "2. 打开 " + proxyInstallUrl + " 并「安装此脚本」
" + - "3. 刷新 Opera 主页面后再点 Flex Link

" + - "或手动注入: 在 Opera 主页面(非本 Pop-up)按 F12 → Console,执行:
" + - "" + - "fetch(\\"" + flexLinkOrigin + "/api/v1/esign/app/opera-parent-close-listener.js\\").then(r=>r.text()).then(eval)" + - ""; - } - - window.initOperaPopupSession = function () { - var h = emitOperaSetupSignal("SET_HEADER_EVENT", { title: "登记程序" }); - lastDetail = "init: " + (h || "skip"); - renderLaunchDebug("内层初始化:SET_HEADER(关窗由延迟 CLOSE_SHELL 触发)"); - return lastDetail; - }; - - function closeStandaloneBrowserTab() { - if (!closeEnabled) return "tab-close:disabled"; - try { - window.close(); - return "tab-close:window.close()"; - } catch (e) { - return "tab-close:error"; - } - } - - /** Opera iframe:只发信号,由外层代理销毁外壳 */ - function requestShellCloseViaProxy() { - if (!closeEnabled) return "signal:disabled"; - shellProxyAck = false; - var steps = []; - steps.push(emitCloseShellBurst()); - steps.push(emitDoReturnSignal()); - lastDetail = steps.join(" | "); - renderLaunchDebug("内层已发 CLOSE_SHELL + DO_RETURN,等待外层代理…"); - setTimeout(function () { - if (!shellProxyAck) { - steps.push(emitShellSignal("CLOSE_SHELL")); - steps.push(emitShellSignal("closeModal")); - steps.push(emitDoReturnSignal()); - lastDetail = steps.join(" | "); - renderLaunchDebug("内层第 2 轮信号已发"); - } - }, Math.max(80, closeDelayMs)); - setTimeout(showProxyMissingHelp, 2500); - return lastDetail; - } - - window.closeLaunchWindow = function () { - if (isOperaPopupIframe()) { - return requestShellCloseViaProxy(); - } - return emitShellSignal("CLOSE_SHELL") + " | " + closeStandaloneBrowserTab(); - }; - - window.addEventListener("message", function (event) { - if (!event.data) return; - if (event.data.action === "SHELL_CLOSE_ACK") { - shellProxyAck = true; - var help = document.getElementById("proxyHelp"); - if (help) help.remove(); - lastDetail = (lastDetail || "") + " | outer ACK ok=" + event.data.ok - + (event.data.frame ? (" frame=" + event.data.frame) : ""); - renderLaunchDebug("外层代理回执 ✓"); - } - }); - - window.renderLaunchDebug = function (note) { - if (!debugPage) return; - var box = document.getElementById("debugBox"); - if (!box) return; - var inIframe = isOperaPopupIframe(); - var appId = resolveExtAppId(); - var targetOrigin = resolveOperaOrigin(); - box.textContent = - (note ? ("[" + note + "]\\n\\n") : "") + - "mode: " + (inIframe ? "iframe-proxy (signal only)" : "standalone-tab") + "\\n" + - "launchMode config: " + launchMode + "\\n" + - "URL: " + location.href + "\\n" + - "shellProxyAck: " + shellProxyAck + "\\n" + - (inIframe ? ("ocextappid: " + (appId || "null") + "\\npostMessage target: " + (targetOrigin || "null") + "\\nproxy: " + proxyInstallUrl + "\\n") : "") + - "lastDetail: " + lastDetail + "\\n\\n" + - (inIframe - ? "内层只发 CLOSE_SHELL / DO_RETURN。外层须装 opera-parent-close-listener.user.js" - : "独立标签:myapp:// 后 window.close()"); - }; - - window.renderOperaDebug = window.renderLaunchDebug; - })(); - """ - .replace("{{LAUNCH_MODE}}", mode) - .replace("{{CLOSE_ENABLED}}", String.valueOf(closeEnabled)) - .replace("{{CLOSE_MODAL_MESSAGE}}", String.valueOf(closeModalMessage)) - .replace("{{OPERA_ORIGIN}}", origin) - .replace("{{FLEX_LINK_ORIGIN}}", flexOrigin) - .replace("{{PROXY_INSTALL_URL}}", proxyInstallUrl) - .replace("{{FALLBACK_EXT_APP_ID}}", String.valueOf(fallbackExtAppId)) - .replace("{{OPEN_DELAY_MS}}", String.valueOf(openDelayMs)) - .replace("{{CLOSE_DELAY_MS}}", String.valueOf(popupCloseDelayMs)) - .replace("{{DEBUG_PAGE}}", String.valueOf(debugPage)); - } - - /** - * 供 Opera 主页面注入(Tampermonkey / Console 一次粘贴)。 - * Flex Link iframe 会 postMessage({ action: 'CLOSE_SHELL' | 'closeModal' }) 触发此逻辑。 - */ - public static String buildOperaParentCloseListenerScript(String flexLinkOrigin) { - String origin = escapeJsString(StringUtils.defaultIfBlank(flexLinkOrigin, "http://127.0.0.1:8066")); - return """ - (function () { - if (window.__autoCheckInShellProxy) return; - window.__autoCheckInShellProxy = true; - var flexOrigin = "{{FLEX_ORIGIN}}"; - - function isFlexOrigin(origin) { - if (!origin) return false; - if (flexOrigin && origin === flexOrigin) return true; - try { - var u = new URL(origin); - if (u.hostname === "127.0.0.1" || u.hostname === "localhost") return true; - } catch (e) {} - return false; - } - - function isCloseSignal(data) { - if (!data) return false; - if (data.source && data.source !== "auto-check-in-launch") return false; - return data.action === "CLOSE_SHELL" || data.action === "closeModal"; - } - - function clickEl(el) { - if (!el) return false; - try { el.focus(); } catch (e1) {} - try { el.click(); return true; } catch (e2) {} - try { - el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window })); - return true; - } catch (e3) {} - return false; - } - - function destroyAdfPopupShell() { - var closeSelectors = [ - 'a[id$="::close"][aria-label="Close"][title="Close"]', - 'a.x2c5[title="Close"][id$="::close"]', - '.af_panelWindow_header-close-icon-wrapper a[title="Close"]', - 'a[id$="::close"]' - ]; - var clicked = false; - for (var i = 0; i < closeSelectors.length; i++) { - var el = document.querySelector(closeSelectors[i]); - if (clickEl(el)) { - console.log("[auto-check-in proxy] 点击 Close:", el.id); - clicked = true; - break; - } - } - document.querySelectorAll('div[id*="ocExtWrpBack"] a, div[id*="ocExtWrpPp"] a').forEach(function (a) { - if (clickEl(a)) { - console.log("[auto-check-in proxy] 点击 ocExtWrp 隐藏链:", a.id || a.className); - clicked = true; - } - }); - var removed = 0; - document.querySelectorAll([ - 'div[id$="::popup-container"][role="dialog"]', - 'div[id$="::popup-container"]', - 'div[data-afr-popupid]' - ].join(",")).forEach(function (el) { - el.remove(); - removed++; - console.log("[auto-check-in proxy] 移除 popup-container"); - }); - document.querySelectorAll([ - 'div.x2px[data-afr-panelwindowbackground="1"]', - 'div.AFPopupTail', - '.AFModalGlassPane', - '.oj-dialog-layer', - '.oj-component-layer' - ].join(",")).forEach(function (el) { el.remove(); }); - document.body.style.overflow = "auto"; - document.documentElement.style.overflow = "auto"; - document.body.classList.remove("p_OraModalGlass"); - return { clicked: clicked, removed: removed }; - } - - function ackShellClose(event, ok, detail) { - if (!event || !event.source) return; - try { - event.source.postMessage({ - action: "SHELL_CLOSE_ACK", - ok: ok, - detail: detail, - frame: location.pathname, - source: "auto-check-in-proxy" - }, event.origin); - } catch (e) { - console.warn("[auto-check-in proxy] ACK 失败", e); - } - } - - window.addEventListener("message", function (event) { - if (!isFlexOrigin(event.origin)) return; - if (!isCloseSignal(event.data)) return; - console.log("[auto-check-in proxy] 收到内层信号:", event.data.action, - "from", event.origin, "frame:", location.href); - var last = null; - [0, 30, 80, 150, 300].forEach(function (ms, idx, arr) { - setTimeout(function () { - last = destroyAdfPopupShell(); - if (idx === arr.length - 1) { - ackShellClose(event, true, JSON.stringify(last)); - } - }, ms); - }); - }); - - window.__autoCheckInDestroyAdfPopupShell = destroyAdfPopupShell; - window.__autoCheckInCloseAdfPopup = function () { return destroyAdfPopupShell(); }; - console.log("[auto-check-in proxy] 外层代理已就绪 @", location.href, - "flexOrigin=", flexOrigin, "手动: __autoCheckInDestroyAdfPopupShell()"); - })(); - """.replace("{{FLEX_ORIGIN}}", origin); - } - - public static String buildOperaParentCloseListenerUserScript(String flexLinkOrigin) { - String origin = StringUtils.defaultIfBlank(flexLinkOrigin, "http://127.0.0.1:8066"); - String userJsUrl = origin + "/api/v1/esign/app/opera-parent-close-listener.user.js"; - return """ - // ==UserScript== - // @name Opera Auto Check-in Close Popup - // @namespace auto-check-in - // @version 1.2 - // @description 外层代理:接收 CLOSE_SHELL 信号并销毁 Flex Link Pop-up 外壳(装一次,永久自动) - // @match *://*.oraclehospitality.* - // @match *://*.oraclecloud.com/* - // @match *://*.ocs.oraclecloud.com/* - // @updateURL %s - // @downloadURL %s - // @run-at document-idle - // @allFrames true - // @grant none - // ==/UserScript== - """ - .formatted(userJsUrl, userJsUrl) - + buildOperaParentCloseListenerScript(origin); - } - - public static String buildOperaProxyInstallPage(String flexLinkOrigin) { - String base = StringUtils.defaultIfBlank(flexLinkOrigin, "http://127.0.0.1:8066"); - String userJs = escapeHtml(base + "/api/v1/esign/app/opera-parent-close-listener.user.js"); - String installPage = escapeHtml(base + "/api/v1/esign/app/opera-proxy-install"); - String extPath = escapeHtml("scripts/opera-close-extension(项目内文件夹,需「加载已解压的扩展程序」)"); - return """ - - - - - Opera 关窗代理 — 安装向导 - - - -

Opera Flex Link 自动关 Pop-up — 安装向导

-
- 为什么 Chrome 扩展不能「一条链接自动装上」?
- 浏览器安全策略禁止从任意网址一键安装扩展(防恶意软件)。 - 只有 应用商店「添加到 Chrome」 或 IT 组策略强制下发 才能接近「一点安装」。 -
-
- ✓ 最接近「一条链接自动装」:Tampermonkey 脚本
- 先装一次 Tampermonkey 扩展(应用商店),之后点下面链接即可一键安装关窗脚本,无需加载文件夹。 -
一键安装 Tampermonkey 脚本 - 安装 Tampermonkey(首次) -
-

方式对比

- - - - - - -
方式能否一条链接说明
Tampermonkey + .user.js✓ 脚本能首次需装 Tampermonkey,之后点链接即装脚本
自研 Chrome 扩展(加载已解压)✗ 不能须 chrome://extensions → 加载文件夹(约 4 步)
上架 Chrome 网上应用店✓ 能需 $5 开发者账号 + 审核,适合全公司统一下载
IT 组策略强制安装✓ 能用户无感,适合企业批量部署
-
- 方式 A:Tampermonkey(推荐,一条链接装脚本) -
    -
  1. 安装 Tampermonkey(只做一次)
  2. -
  3. 打开 %s → 点「安装」
  4. -
  5. 刷新 Opera Cloud 主页面
  6. -
-
-
- 方式 B:自研 Chrome 扩展(不能一条链接,但无需 Tampermonkey) -
    -
  1. 打开 chrome://extensions,开启「开发者模式」
  2. -
  3. 「加载已解压的扩展程序」→ 选项目内 %s
  4. -
  5. 刷新 Opera Cloud 主页面
  6. -
-

Console 应出现:[auto-check-in ext] 外层代理已就绪

-
-
- 验证 -

点 Flex Link → Pop-up 应能自动关闭;调试框可见 shellProxyAck: true

-

主页面 Console:__autoCheckInDestroyAdfPopupShell()

-
-

本页地址:%s

- - - """.formatted(userJs, userJs, userJs, extPath, installPage, installPage); - } - - private static String escapeHtml(String value) { - return String.valueOf(value) - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """); - } - - private static String escapeJsString(String value) { - return String.valueOf(value) - .replace("\\", "\\\\") - .replace("\"", "\\\""); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ProfileDetailsJsonBuilder.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ProfileDetailsJsonBuilder.java deleted file mode 100644 index 0271737..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ProfileDetailsJsonBuilder.java +++ /dev/null @@ -1,353 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.support; - -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * 将 CMS 配置的扁平 OHIP 字段路径(相对 profileDetails)构造成 CRM PUT /profiles/{id} 请求体。 - *

- * 示例路径:{@code customer.personName[Primary].givenName}、 - * {@code customer.identifications.identificationInfo[0].identification.idNumber}、 - * {@code addresses.addressInfo[0].address.cityName}、{@code profileType} - */ -public final class ProfileDetailsJsonBuilder { - - private static final Pattern SEGMENT = - Pattern.compile("([^.\\[]+)(?:\\[([^\\]]+)\\])?"); - - /** OHIP telephone.phoneUseType 未映射时,更新 phoneNumber 的默认值。 */ - private static final String DEFAULT_PHONE_USE_TYPE = "HOME"; - private static final String PHONE_NUMBER_PATH = - "telephones.telephoneInfo[0].telephone.phoneNumber"; - private static final String PHONE_USE_TYPE_PATH = - "telephones.telephoneInfo[0].telephone.phoneUseType"; - private static final String PHONE_PRIMARY_IND_PATH = - "telephones.telephoneInfo[0].telephone.primaryInd"; - - /** OHIP email.type 未映射时,更新 emailAddress 的默认值。 */ - private static final String DEFAULT_EMAIL_TYPE = "EMAIL"; - private static final String EMAIL_ADDRESS_PATH = - "emails.emailInfo[0].email.emailAddress"; - private static final String EMAIL_TYPE_PATH = - "emails.emailInfo[0].email.type"; - private static final String EMAIL_PRIMARY_IND_PATH = - "emails.emailInfo[0].email.primaryInd"; - - private ProfileDetailsJsonBuilder() { - } - - public static JSONObject buildBody(Map mappedFields, String profileType) { - JSONObject profileDetails = new JSONObject(); - if (mappedFields != null) { - for (Map.Entry entry : mappedFields.entrySet()) { - String path = normalizePath(entry.getKey()); - String value = StringUtils.trimToNull(entry.getValue()); - if (path == null || value == null) { - continue; - } - setValue(profileDetails, path, value); - } - } - ensureTelephoneDefaults(profileDetails); - ensureEmailDefaults(profileDetails); - if (StringUtils.isNotBlank(profileType)) { - profileDetails.put("profileType", profileType.trim()); - } - JSONObject root = new JSONObject(); - root.put("profileDetails", profileDetails); - return root; - } - - private static void ensureTelephoneDefaults(JSONObject profileDetails) { - if (StringUtils.isBlank(getStringAtPath(profileDetails, PHONE_NUMBER_PATH))) { - return; - } - if (StringUtils.isBlank(getStringAtPath(profileDetails, PHONE_USE_TYPE_PATH))) { - setValue(profileDetails, PHONE_USE_TYPE_PATH, DEFAULT_PHONE_USE_TYPE); - } - if (!hasLeafAtPath(profileDetails, PHONE_PRIMARY_IND_PATH)) { - setBooleanAtPath(profileDetails, PHONE_PRIMARY_IND_PATH, true); - } - } - - private static void ensureEmailDefaults(JSONObject profileDetails) { - if (StringUtils.isBlank(getStringAtPath(profileDetails, EMAIL_ADDRESS_PATH))) { - return; - } - if (StringUtils.isBlank(getStringAtPath(profileDetails, EMAIL_TYPE_PATH))) { - setValue(profileDetails, EMAIL_TYPE_PATH, DEFAULT_EMAIL_TYPE); - } - if (!hasLeafAtPath(profileDetails, EMAIL_PRIMARY_IND_PATH)) { - setBooleanAtPath(profileDetails, EMAIL_PRIMARY_IND_PATH, true); - } - } - - private static String normalizePath(String raw) { - String path = StringUtils.trimToEmpty(raw); - if (path.startsWith("profileDetails.")) { - path = path.substring("profileDetails.".length()); - } - return StringUtils.trimToNull(path); - } - - private static void setValue(JSONObject root, String path, String value) { - List segments = parsePath(path); - if (segments.isEmpty()) { - return; - } - Object current = root; - for (int i = 0; i < segments.size() - 1; i++) { - current = navigate(current, segments.get(i), true); - if (current == null) { - return; - } - } - Segment leaf = segments.get(segments.size() - 1); - if (!(current instanceof JSONObject obj)) { - return; - } - Selector leafSelector = leaf.selector(); - if (leafSelector != null && leafSelector.kind == SelectorKind.INDEX) { - JSONArray arr = obj.getJSONArray(leaf.name()); - if (arr == null) { - arr = new JSONArray(); - obj.put(leaf.name(), arr); - } - while (arr.size() <= leafSelector.index) { - arr.add(defaultArrayElement(leaf.name())); - } - arr.set(leafSelector.index, value); - return; - } - obj.put(leaf.name(), value); - } - - private static String getStringAtPath(JSONObject root, String path) { - List segments = parsePath(path); - if (segments.isEmpty()) { - return null; - } - Object current = root; - for (int i = 0; i < segments.size() - 1; i++) { - current = navigate(current, segments.get(i), false); - if (current == null) { - return null; - } - } - Segment leaf = segments.get(segments.size() - 1); - if (!(current instanceof JSONObject obj)) { - return null; - } - Selector leafSelector = leaf.selector(); - if (leafSelector != null && leafSelector.kind == SelectorKind.INDEX) { - JSONArray arr = obj.getJSONArray(leaf.name()); - if (arr == null || arr.size() <= leafSelector.index) { - return null; - } - Object value = arr.get(leafSelector.index); - return value != null ? StringUtils.trimToNull(String.valueOf(value)) : null; - } - return StringUtils.trimToNull(obj.getString(leaf.name())); - } - - private static boolean hasLeafAtPath(JSONObject root, String path) { - List segments = parsePath(path); - if (segments.isEmpty()) { - return false; - } - Object current = root; - for (int i = 0; i < segments.size() - 1; i++) { - current = navigate(current, segments.get(i), false); - if (current == null) { - return false; - } - } - Segment leaf = segments.get(segments.size() - 1); - if (!(current instanceof JSONObject obj)) { - return false; - } - Selector leafSelector = leaf.selector(); - if (leafSelector != null && leafSelector.kind == SelectorKind.INDEX) { - JSONArray arr = obj.getJSONArray(leaf.name()); - return arr != null && arr.size() > leafSelector.index && arr.get(leafSelector.index) != null; - } - return obj.containsKey(leaf.name()); - } - - private static void setBooleanAtPath(JSONObject root, String path, boolean value) { - List segments = parsePath(path); - if (segments.isEmpty()) { - return; - } - Object current = root; - for (int i = 0; i < segments.size() - 1; i++) { - current = navigate(current, segments.get(i), true); - if (current == null) { - return; - } - } - Segment leaf = segments.get(segments.size() - 1); - if (!(current instanceof JSONObject obj)) { - return; - } - Selector leafSelector = leaf.selector(); - if (leafSelector != null && leafSelector.kind == SelectorKind.INDEX) { - JSONArray arr = obj.getJSONArray(leaf.name()); - if (arr == null) { - arr = new JSONArray(); - obj.put(leaf.name(), arr); - } - while (arr.size() <= leafSelector.index) { - arr.add(defaultArrayElement(leaf.name())); - } - arr.set(leafSelector.index, value); - return; - } - obj.put(leaf.name(), value); - } - - private static Object defaultArrayElement(String segmentName) { - return "addressLine".equals(segmentName) ? "" : new JSONObject(); - } - - private static List parsePath(String path) { - List out = new ArrayList<>(); - Matcher matcher = SEGMENT.matcher(path); - int pos = 0; - while (matcher.find(pos)) { - if (matcher.start() != pos) { - break; - } - String name = matcher.group(1); - String selector = StringUtils.trimToNull(matcher.group(2)); - out.add(new Segment(name, parseSelector(name, selector))); - pos = matcher.end(); - if (pos < path.length() && path.charAt(pos) == '.') { - pos++; - } - } - return out; - } - - private static Selector parseSelector(String segmentName, String raw) { - if (raw == null) { - return null; - } - if (raw.contains("=")) { - int eq = raw.indexOf('='); - return Selector.match(raw.substring(0, eq).trim(), raw.substring(eq + 1).trim()); - } - if (NumberUtils.isDigits(raw)) { - return Selector.index(Integer.parseInt(raw)); - } - if ("personName".equals(segmentName)) { - return Selector.match("nameType", raw); - } - if ("keyword".equals(segmentName)) { - return Selector.index(0); - } - return Selector.match("type", raw); - } - - private static Object navigate(Object current, Segment segment, boolean create) { - if (!(current instanceof JSONObject obj)) { - return null; - } - Selector selector = segment.selector(); - if (selector == null) { - if (!obj.containsKey(segment.name())) { - if (!create) { - return null; - } - obj.put(segment.name(), new JSONObject()); - } - return obj.get(segment.name()); - } - if (!obj.containsKey(segment.name())) { - if (!create) { - return null; - } - obj.put(segment.name(), new JSONArray()); - } - Object node = obj.get(segment.name()); - final JSONArray arr; - if (node instanceof JSONArray existing) { - arr = existing; - } else { - if (!create) { - return null; - } - arr = new JSONArray(); - obj.put(segment.name(), arr); - } - if (selector.kind == SelectorKind.INDEX) { - while (arr.size() <= selector.index) { - arr.add(defaultArrayElement(segment.name())); - } - return arr.get(selector.index); - } - for (int i = 0; i < arr.size(); i++) { - Object item = arr.get(i); - if (item instanceof JSONObject itemObj && matches(itemObj, selector)) { - return itemObj; - } - } - if (!create) { - return null; - } - JSONObject created = new JSONObject(); - created.put(selector.matchKey, selector.matchValue); - arr.add(created); - return created; - } - - private static boolean matches(JSONObject obj, Selector selector) { - if (selector.kind != SelectorKind.MATCH) { - return false; - } - String actual = StringUtils.trimToEmpty(obj.getString(selector.matchKey)); - return actual.equalsIgnoreCase(selector.matchValue); - } - - private record Segment(String name, Selector selector) { - } - - private enum SelectorKind { - INDEX, - MATCH - } - - private static final class Selector { - private final SelectorKind kind; - private final int index; - private final String matchKey; - private final String matchValue; - - private Selector(SelectorKind kind, int index, String matchKey, String matchValue) { - this.kind = kind; - this.index = index; - this.matchKey = matchKey; - this.matchValue = matchValue; - } - - static Selector index(int index) { - return new Selector(SelectorKind.INDEX, index, null, null); - } - - static Selector match(String key, String value) { - return new Selector( - SelectorKind.MATCH, - -1, - StringUtils.trimToEmpty(key), - StringUtils.trimToEmpty(value)); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/RegistrationConfirmationExtractor.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/RegistrationConfirmationExtractor.java deleted file mode 100644 index fe5af1e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/RegistrationConfirmationExtractor.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.support; - -import org.apache.commons.lang3.StringUtils; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.text.PDFTextStripper; - -import java.util.Base64; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** 从登记卡 PDF 文本中提取 OPERA 确认号(Conf. No / 確認號碼)。 */ -public final class RegistrationConfirmationExtractor { - - private static final Pattern[] TEXT_PATTERNS = { - Pattern.compile("(?is)conf\\.?\\s*no\\s*確認號碼\\s*[::]?\\s*(\\d{4,12})"), - Pattern.compile("(?is)confirmation\\s*no\\.?[^0-9]{0,48}(\\d{4,12})"), - Pattern.compile("(?is)(?:訂房編號|订房编号|訂房號碼|订房号码)\\s*[::]?\\s*(\\d{4,12})"), - Pattern.compile("(?is)確認號碼\\s*[::]?\\s*(\\d{4,12})"), - Pattern.compile("(?is)confirmation\\s*(?:no\\.?|number)\\s*[::]?\\s*(\\d{4,12})"), - Pattern.compile("(?is)conf\\.?\\s*no\\.?\\s*[::]?\\s*(\\d{4,12})"), - // OCR 常见误识别:Conf. Nof#aa sk: 3041736(No 与数字间可有字母/符号) - Pattern.compile("(?is)conf[\\s.]*no[^0-9]{0,48}(\\d{6,12})"), - Pattern.compile("(?is)conf\\W{0,20}(?:no|n[o0])[^0-9]{0,48}(\\d{6,12})"), - Pattern.compile("(?is)(?:確認號碼|确认号码|確認号码)\\W{0,16}(\\d{4,12})"), - Pattern.compile("(?is)(?:conf|corn|conl)[\\s.]*(?:no|n[o0])[^0-9]{0,48}(\\d{6,12})") - }; - - private RegistrationConfirmationExtractor() { - } - - public static Optional extractFromPdfBase64(String pdfBase64) { - if (StringUtils.isBlank(pdfBase64)) { - return Optional.empty(); - } - try { - byte[] bytes = Base64.getDecoder().decode(normalizeBase64(pdfBase64)); - return extractFromPdfBytes(bytes); - } catch (IllegalArgumentException e) { - return Optional.empty(); - } - } - - public static Optional extractFromPdfBytes(byte[] pdfBytes) { - if (pdfBytes == null || pdfBytes.length == 0) { - return Optional.empty(); - } - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - PDFTextStripper stripper = new PDFTextStripper(); - String text = stripper.getText(doc); - return extractFromText(text); - } catch (Exception e) { - return Optional.empty(); - } - } - - public static Optional extractFromText(String text) { - if (StringUtils.isBlank(text)) { - return Optional.empty(); - } - String normalized = text.replace('\u00a0', ' '); - for (Pattern pattern : TEXT_PATTERNS) { - Matcher matcher = pattern.matcher(normalized); - if (matcher.find()) { - return Optional.of(matcher.group(1).trim()); - } - } - return extractLooseConfNo(normalized); - } - - /** OCR 兜底:Conf / No 附近 48 字符内的 6–12 位数字 */ - private static Optional extractLooseConfNo(String text) { - Matcher label = Pattern.compile("(?is)conf[\\s.]*no").matcher(text); - while (label.find()) { - int start = label.end(); - int end = Math.min(text.length(), start + 48); - Matcher digits = Pattern.compile("(\\d{6,12})").matcher(text.substring(start, end)); - if (digits.find()) { - return Optional.of(digits.group(1).trim()); - } - } - return Optional.empty(); - } - - private static String normalizeBase64(String raw) { - String trimmed = raw.trim(); - int comma = trimmed.indexOf(','); - if (trimmed.startsWith("data:") && comma > 0) { - return trimmed.substring(comma + 1).trim(); - } - return trimmed.replaceAll("\\s", ""); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationDetailResponseParser.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationDetailResponseParser.java deleted file mode 100644 index d8bdacf..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationDetailResponseParser.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.support; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.esign.reservation.dtos.GuestProfileRef; -import org.apache.commons.lang3.StringUtils; - -import java.util.Optional; - -/** 解析 OHIP GET /hotels/{hotelId}/reservations/{reservationId} 响应中的 Guest Profile。 */ -public final class ReservationDetailResponseParser { - - private ReservationDetailResponseParser() { - } - - public static Optional extractProfileId(String body) { - return extractGuestProfile(body).map(GuestProfileRef::profileId); - } - - public static Optional extractGuestProfile(String body) { - if (StringUtils.isBlank(body)) { - return Optional.empty(); - } - try { - Object parsed = JSON.parse(body.trim()); - return extractGuestProfile(parsed); - } catch (Exception e) { - return Optional.empty(); - } - } - - private static Optional extractGuestProfile(Object node) { - if (node instanceof JSONObject obj) { - Optional fromGuests = extractFromReservationGuests(obj.get("reservationGuests")); - if (fromGuests.isPresent()) { - return fromGuests; - } - Object reservations = obj.get("reservations"); - if (reservations instanceof JSONObject reservationsObj) { - Optional nested = extractFromReservationsContainer(reservationsObj); - if (nested.isPresent()) { - return nested; - } - } - for (String key : obj.keySet()) { - Optional nested = extractGuestProfile(obj.get(key)); - if (nested.isPresent()) { - return nested; - } - } - } else if (node instanceof JSONArray arr) { - for (int i = 0; i < arr.size(); i++) { - Optional nested = extractGuestProfile(arr.get(i)); - if (nested.isPresent()) { - return nested; - } - } - } - return Optional.empty(); - } - - private static Optional extractFromReservationsContainer(JSONObject reservationsObj) { - Object reservation = reservationsObj.get("reservation"); - if (reservation instanceof JSONArray arr) { - for (int i = 0; i < arr.size(); i++) { - if (arr.get(i) instanceof JSONObject resObj) { - Optional profile = extractFromReservationGuests(resObj.get("reservationGuests")); - if (profile.isPresent()) { - return profile; - } - } - } - } else if (reservation instanceof JSONObject resObj) { - return extractFromReservationGuests(resObj.get("reservationGuests")); - } - return Optional.empty(); - } - - private static Optional extractFromReservationGuests(Object guestsNode) { - if (!(guestsNode instanceof JSONArray guests)) { - return Optional.empty(); - } - Optional primary = Optional.empty(); - for (int i = 0; i < guests.size(); i++) { - if (!(guests.get(i) instanceof JSONObject guest)) { - continue; - } - Optional profile = extractFromProfileInfo(guest.getJSONObject("profileInfo")); - if (profile.isEmpty()) { - continue; - } - if (Boolean.TRUE.equals(guest.getBoolean("primary"))) { - return profile; - } - if (primary.isEmpty()) { - primary = profile; - } - } - return primary; - } - - private static Optional extractFromProfileInfo(JSONObject profileInfo) { - if (profileInfo == null) { - return Optional.empty(); - } - String profileType = readProfileType(profileInfo.getJSONObject("profile")); - Object list = profileInfo.get("profileIdList"); - if (!(list instanceof JSONArray arr)) { - return Optional.empty(); - } - Optional fallbackId = Optional.empty(); - for (int i = 0; i < arr.size(); i++) { - if (!(arr.get(i) instanceof JSONObject idObj)) { - continue; - } - String id = StringUtils.trimToNull(idObj.getString("id")); - if (id == null) { - continue; - } - String type = StringUtils.defaultString(idObj.getString("type")); - if ("Profile".equalsIgnoreCase(type)) { - return Optional.of(new GuestProfileRef(id, profileType)); - } - if (fallbackId.isEmpty()) { - fallbackId = Optional.of(id); - } - } - return fallbackId.map(id -> new GuestProfileRef(id, profileType)); - } - - private static String readProfileType(JSONObject profile) { - if (profile == null) { - return null; - } - return StringUtils.trimToNull(profile.getString("profileType")); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationLookupResponseParser.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationLookupResponseParser.java deleted file mode 100644 index a1f25ee..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/support/ReservationLookupResponseParser.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.support; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import org.apache.commons.lang3.StringUtils; - -import java.util.Optional; - -/** 解析 OHIP GET /reservations 响应中的 reservation id。 */ -public final class ReservationLookupResponseParser { - - private ReservationLookupResponseParser() { - } - - public static Optional extractReservationId(String body, String confirmationNumber) { - if (StringUtils.isBlank(body)) { - return Optional.empty(); - } - try { - Object parsed = JSON.parse(body.trim()); - Optional matched = findReservationId(parsed, confirmationNumber); - if (matched.isPresent()) { - return matched; - } - return findReservationId(parsed, null); - } catch (Exception e) { - return Optional.empty(); - } - } - - private static Optional findReservationId(Object node, String confirmationNumber) { - if (node instanceof JSONObject obj) { - String confInNode = firstNonBlank( - obj.getString("confirmationNumber"), - obj.getString("confirmationNo"), - obj.getString("confirmationId")); - String reservationId = firstReservationId(obj); - if (StringUtils.isNotBlank(reservationId)) { - if (StringUtils.isBlank(confirmationNumber) - || StringUtils.isBlank(confInNode) - || confirmationNumber.equals(confInNode.trim())) { - return Optional.of(reservationId.trim()); - } - } - for (String key : obj.keySet()) { - Optional nested = findReservationId(obj.get(key), confirmationNumber); - if (nested.isPresent()) { - return nested; - } - } - } else if (node instanceof JSONArray arr) { - for (int i = 0; i < arr.size(); i++) { - Optional nested = findReservationId(arr.get(i), confirmationNumber); - if (nested.isPresent()) { - return nested; - } - } - } - return Optional.empty(); - } - - private static String firstReservationId(JSONObject obj) { - String direct = firstNonBlank( - obj.getString("reservationId"), - obj.getString("reservationID"), - obj.getString("id")); - if (StringUtils.isNotBlank(direct) && looksLikeReservationId(direct)) { - return direct; - } - Object list = obj.get("reservationIdList"); - if (list instanceof JSONArray arr) { - for (int i = 0; i < arr.size(); i++) { - Object item = arr.get(i); - if (item instanceof JSONObject idObj) { - String type = StringUtils.defaultString(idObj.getString("type")); - String id = firstNonBlank(idObj.getString("id"), idObj.getString("reservationId")); - if (StringUtils.isNotBlank(id) - && (StringUtils.isBlank(type) || type.toLowerCase().contains("reservation"))) { - return id; - } - } else if (item != null && StringUtils.isNotBlank(String.valueOf(item))) { - return String.valueOf(item); - } - } - } - return null; - } - - private static boolean looksLikeReservationId(String value) { - String trimmed = value.trim(); - return trimmed.matches("\\d{3,12}"); - } - - private static String firstNonBlank(String... values) { - for (String value : values) { - if (StringUtils.isNotBlank(value)) { - return value.trim(); - } - } - return null; - } -} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f4c1459..3c64307 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -140,6 +140,11 @@ ops-app: reservation-lookup: path-template: /rsv/v1/hotels/{hotelId}/reservations fetch-instructions: Reservation + booking-check-in: + auto-room-assignment: + path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments + check-in: + path-template: /fof/v1/hotels/{hotelId}/reservations/{reservationId}/checkIns file-attachment: path: /med/config/v1/fileAttachments user-name: USER@HOTEL diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 6e36659..bf15843 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -69,7 +69,7 @@ ops-app: close-popup: true close-modal-message: true flex-link-origin: http://127.0.0.1:8066 - parent-origin: https://mtcs2ua.hospitality-api.ap-singapore-1.ocs.oc-test.com + parent-origin: https://mucu1ua.hospitality-api.us-ashburn-1.ocs.oc-test.com open-delay-ms: 1200 # popup 关窗不再等待 open-delay;仅 new-window 标签关窗使用。越小关 Pop-up 越快(建议 50~120) close-delay-ms: 50 @@ -124,7 +124,7 @@ ops-app: action: OPEN_RESERVATION third-party: # Oracle Hospitality (OHIP) Gateway - base-url: https://mtcs2ua.hospitality-api.ap-singapore-1.ocs.oc-test.com + base-url: https://mucu1ua.hospitality-api.us-ashburn-1.ocs.oc-test.com timeout-ms: 30000 registration-card: regenerate: false @@ -137,6 +137,11 @@ ops-app: reservation-lookup: path-template: /rsv/v1/hotels/{hotelId}/reservations fetch-instructions: Reservation + booking-check-in: + auto-room-assignment: + path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments + check-in: + path-template: /fof/v1/hotels/{hotelId}/reservations/{reservationId}/checkIns file-attachment: path: /med/config/v1/fileAttachments user-name: USER@HOTEL @@ -151,12 +156,12 @@ ops-app: enabled: true token-path: /oauth/v1/tokens # Postman Authorization → Basic Auth:用户名=CLIENT_ID,密码=CLIENT_SECRET - client-id: RPHALVA-UAT-SHKPRH-mtcs2ua-willtech_test - client-secret: idcscs-138f2ac5-563c-47fc-ab8f-5ff8b7177af4 + client-id: WTMLQA-UAT-WTML-mucu1ua-OCR_Scanner + client-secret: e85ac385-fe94-4189-84e6-ee3568ac2be9 # Postman Header {{AppKey}}(非 HashedAppKey) - app-key: a895c827-603e-4cf8-8aaf-7631dbd0d309 - enterprise-id: SHKPRH - hotel-id: RPH + app-key: 2dddec89-6e37-403f-8a5b-7aa1d3fd46bf + enterprise-id: WTML + hotel-id: WTQA scope: urn:opc:hgbu:ws:__myscopes__ grant-type: client_credentials timeout-ms: 30000